mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-05-29 23:00:35 +03:00
Compare commits
50 Commits
fix/androi
...
feat/imple
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67dcadcd42 | ||
|
|
a4b2b3e3ad | ||
|
|
3d540b22bf | ||
|
|
d670bca9d4 | ||
|
|
b09a4ecd8d | ||
|
|
c327d3e3c8 | ||
|
|
9c9e1700af | ||
|
|
eba2097d1d | ||
|
|
22de0c2a16 | ||
|
|
614973a4ce | ||
|
|
e554e9b8b4 | ||
|
|
d4833454ef | ||
|
|
9851b4bacb | ||
|
|
29ad1f0c02 | ||
|
|
d6c34b3f60 | ||
|
|
d8668742b4 | ||
|
|
fb5666057b | ||
|
|
5eab5fc18b | ||
|
|
a49892c7e7 | ||
|
|
277b295fd8 | ||
|
|
8c33779fc3 | ||
|
|
f0299ca9fe | ||
|
|
b46a9e389f | ||
|
|
81b8cd05c2 | ||
|
|
d0a9f6e4d5 | ||
|
|
8a29b49fd7 | ||
|
|
1baa2d85bd | ||
|
|
e226fadb07 | ||
|
|
bf4bf9972d | ||
|
|
f781bf6a23 | ||
|
|
2fa0ec81ad | ||
|
|
1ee0a6c9c7 | ||
|
|
14c7aab0fb | ||
|
|
d3347e6007 | ||
|
|
026826970c | ||
|
|
d2d3545961 | ||
|
|
b7e2847393 | ||
|
|
bb56008c3d | ||
|
|
a53db6eafe | ||
|
|
433ecb448f | ||
|
|
ab12a0b3f0 | ||
|
|
5a192cec15 | ||
|
|
6fc65dba8a | ||
|
|
f65fd4a8c5 | ||
|
|
c877e1e5cb | ||
|
|
2cb12c596c | ||
|
|
5beae954c7 | ||
|
|
5583c0a2a9 | ||
|
|
2cb7b30d8a | ||
|
|
2f6714e278 |
@@ -4,7 +4,7 @@ set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
set(PROJECT AmneziaVPN)
|
||||
set(AMNEZIAVPN_VERSION 4.8.15.4)
|
||||
set(AMNEZIAVPN_VERSION 4.9.0.2)
|
||||
|
||||
set(QT_CREATOR_SKIP_PACKAGE_MANAGER_SETUP ON CACHE BOOL "" FORCE)
|
||||
set(CMAKE_PROJECT_TOP_LEVEL_INCLUDES
|
||||
|
||||
@@ -109,6 +109,9 @@ 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) {
|
||||
|
||||
10
client/android/res/drawable/ic_pairing_back.xml
Normal file
10
client/android/res/drawable/ic_pairing_back.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFE8E8EC"
|
||||
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z" />
|
||||
</vector>
|
||||
5
client/android/res/drawable/torch_fab_bg.xml
Normal file
5
client/android/res/drawable/torch_fab_bg.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="#38FFFFFF" />
|
||||
</shape>
|
||||
@@ -8,4 +8,75 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_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,5 +24,13 @@
|
||||
<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,5 +24,13 @@
|
||||
<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,6 +42,9 @@ 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
|
||||
@@ -73,12 +76,18 @@ 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() {
|
||||
class AmneziaActivity : QtActivity(), LifecycleOwner {
|
||||
|
||||
private val lifecycleRegistry = LifecycleRegistry(this)
|
||||
|
||||
override val lifecycle: Lifecycle
|
||||
get() = lifecycleRegistry
|
||||
|
||||
private lateinit var mainScope: CoroutineScope
|
||||
private val qtInitialized = CompletableDeferred<Unit>()
|
||||
@@ -99,6 +108,8 @@ class AmneziaActivity : QtActivity() {
|
||||
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) {
|
||||
@@ -205,6 +216,7 @@ class AmneziaActivity : QtActivity() {
|
||||
registerBroadcastReceivers()
|
||||
intent?.let(::processIntent)
|
||||
runBlocking { vpnProto = proto.await() }
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
@@ -262,6 +274,7 @@ class AmneziaActivity : QtActivity() {
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
|
||||
Log.d(TAG, "Start Amnezia activity")
|
||||
mainScope.launch {
|
||||
qtInitialized.await()
|
||||
@@ -285,6 +298,7 @@ class AmneziaActivity : QtActivity() {
|
||||
qtInitialized.await()
|
||||
QtAndroidController.onServiceDisconnected()
|
||||
}
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
@@ -357,6 +371,7 @@ class AmneziaActivity : QtActivity() {
|
||||
if (qtInitialized.isCompleted) {
|
||||
QtAndroidController.onActivityPaused()
|
||||
}
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
|
||||
super.onPause()
|
||||
isActivityResumed = false
|
||||
// Cancel all pending operations when activity pauses
|
||||
@@ -367,6 +382,7 @@ class AmneziaActivity : QtActivity() {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
||||
isActivityResumed = true
|
||||
Log.d(TAG, "Resume Amnezia activity")
|
||||
if (qtInitialized.isCompleted) {
|
||||
@@ -483,6 +499,7 @@ class AmneziaActivity : QtActivity() {
|
||||
unregisterBroadcastReceiver(notificationStateReceiver)
|
||||
notificationStateReceiver = null
|
||||
mainScope.cancel()
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
@@ -880,6 +897,66 @@ class AmneziaActivity : QtActivity() {
|
||||
@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)
|
||||
|
||||
@@ -928,6 +1005,19 @@ class AmneziaActivity : QtActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun startPairingQrCodeReader() {
|
||||
val now = SystemClock.uptimeMillis()
|
||||
if (now - lastPairingQrReaderStartUptimeMs < 1200L) {
|
||||
return
|
||||
}
|
||||
lastPairingQrReaderStartUptimeMs = now
|
||||
Intent(this, CameraActivity::class.java).also {
|
||||
it.putExtra(CameraActivity.EXTRA_PAIRING_QR_CAMERA, true)
|
||||
startActivity(it)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun setSaveLogs(enabled: Boolean) {
|
||||
Log.v(TAG, "Set save logs: $enabled")
|
||||
@@ -1179,6 +1269,7 @@ class AmneziaActivity : QtActivity() {
|
||||
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,47 +2,384 @@ 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 lateinit var cameraProvider: ProcessCameraProvider
|
||||
private var cameraProvider: ProcessCameraProvider? = null
|
||||
private var boundCamera: Camera? = null
|
||||
private var boundImageAnalysis: ImageAnalysis? = null
|
||||
private var torchOn: Boolean = false
|
||||
|
||||
private var imageAnalysisExecutor: ExecutorService? = null
|
||||
|
||||
private val qrHandledOrClosing = AtomicBoolean(false)
|
||||
|
||||
private var pairingQrDeliveredToQt = false
|
||||
|
||||
private var pairingQrUserDismissedCamera = false
|
||||
|
||||
private var barcodeScanner: BarcodeScanner? = null
|
||||
|
||||
private val cachedPreviewOutputTransform = AtomicReference<OutputTransform?>(null)
|
||||
|
||||
private var previewTransformLayoutListener: View.OnLayoutChangeListener? = null
|
||||
|
||||
private var previewStreamStateObserver: Observer<PreviewView.StreamState>? = null
|
||||
|
||||
@Volatile
|
||||
private var pairingGeomHeaderBottomPx = 0f
|
||||
|
||||
@Volatile
|
||||
private var pairingGeomStatusBarTopPx = 0f
|
||||
|
||||
@Volatile
|
||||
private var pairingGeomDensity = 1f
|
||||
|
||||
@ExperimentalGetImage
|
||||
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()
|
||||
@@ -67,26 +404,41 @@ class CameraActivity : ComponentActivity() {
|
||||
|
||||
cameraProviderFuture.addListener({
|
||||
cameraProvider = cameraProviderFuture.get()
|
||||
bindPreview()
|
||||
bindImageAnalysis()
|
||||
bindCameraUseCases()
|
||||
}, ContextCompat.getMainExecutor(this))
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private fun bindPreview() {
|
||||
@ExperimentalGetImage
|
||||
private fun bindCameraUseCases() {
|
||||
val provider = cameraProvider ?: return
|
||||
imageAnalysisExecutor?.shutdown()
|
||||
imageAnalysisExecutor = Executors.newSingleThreadExecutor()
|
||||
|
||||
val viewFinder = viewBinding.viewFinder
|
||||
val preview = Preview.Builder().build().also {
|
||||
it.setSurfaceProvider(viewFinder.surfaceProvider)
|
||||
}
|
||||
|
||||
val camera = cameraProvider.bindToLifecycle(this, CameraSelector.DEFAULT_BACK_CAMERA, preview)
|
||||
val imageAnalysis = ImageAnalysis.Builder()
|
||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||
.build()
|
||||
|
||||
val camera = provider.bindToLifecycle(
|
||||
this,
|
||||
CameraSelector.DEFAULT_BACK_CAMERA,
|
||||
preview,
|
||||
imageAnalysis
|
||||
)
|
||||
boundCamera = camera
|
||||
boundImageAnalysis = imageAnalysis
|
||||
|
||||
viewFinder.setOnTouchListener { _, motionEvent ->
|
||||
when (motionEvent.action) {
|
||||
ACTION_DOWN -> true
|
||||
ACTION_UP -> {
|
||||
val point = viewFinder
|
||||
.meteringPointFactory.createPoint(motionEvent.x, motionEvent.x)
|
||||
.meteringPointFactory.createPoint(motionEvent.x, motionEvent.y)
|
||||
|
||||
val action = FocusMeteringAction
|
||||
.Builder(point, FLAG_AF or FLAG_AE).build()
|
||||
@@ -98,58 +450,121 @@ class CameraActivity : ComponentActivity() {
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ExperimentalGetImage
|
||||
private fun bindImageAnalysis() {
|
||||
val imageAnalysis = ImageAnalysis.Builder().build()
|
||||
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()
|
||||
}
|
||||
|
||||
val camera = cameraProvider.bindToLifecycle(this, CameraSelector.DEFAULT_BACK_CAMERA, imageAnalysis)
|
||||
try {
|
||||
barcodeScanner?.close()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
|
||||
val barcodeScanner = BarcodeScanning.getClient(
|
||||
barcodeScanner = BarcodeScanning.getClient(
|
||||
Builder()
|
||||
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
|
||||
.setZoomSuggestionOptions(
|
||||
ZoomSuggestionOptions.Builder { zoomLevel ->
|
||||
camera.cameraControl.setZoomRatio(zoomLevel)
|
||||
true
|
||||
}.apply {
|
||||
camera.cameraInfo.zoomState.value?.maxZoomRatio?.let { maxZoomRation ->
|
||||
setMaxSupportedZoomRatio(maxZoomRation)
|
||||
}
|
||||
}.build()
|
||||
).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(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()
|
||||
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
|
||||
}
|
||||
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() {
|
||||
cameraProvider.unbindAll()
|
||||
cleanupCameraResources()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
101
client/android/src/org/amnezia/vpn/PairingQrScanBracketPaths.kt
Normal file
101
client/android/src/org/amnezia/vpn/PairingQrScanBracketPaths.kt
Normal file
@@ -0,0 +1,101 @@
|
||||
package org.amnezia.vpn
|
||||
|
||||
import android.graphics.Path
|
||||
import android.graphics.RectF
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
object PairingQrScanBracketPaths {
|
||||
|
||||
private fun Path.addCornerMinorArc(
|
||||
cx: Float,
|
||||
cy: Float,
|
||||
r: Float,
|
||||
sx: Float,
|
||||
sy: Float,
|
||||
ex: Float,
|
||||
ey: Float
|
||||
) {
|
||||
var asRad = atan2((sy - cy).toDouble(), (sx - cx).toDouble())
|
||||
var aeRad = atan2((ey - cy).toDouble(), (ex - cx).toDouble())
|
||||
while (aeRad - asRad > PI) {
|
||||
aeRad -= 2.0 * PI
|
||||
}
|
||||
while (aeRad - asRad < -PI) {
|
||||
aeRad += 2.0 * PI
|
||||
}
|
||||
val minor = aeRad - asRad
|
||||
val startDeg = Math.toDegrees(asRad).toFloat()
|
||||
val sweepDeg = Math.toDegrees(minor).toFloat()
|
||||
addArc(RectF(cx - r, cy - r, cx + r, cy + r), startDeg, sweepDeg)
|
||||
}
|
||||
|
||||
fun bracketStrokePath(corner: Int, x0: Float, y0: Float, s: Float, R: Float, L: Float, t: Float): Path {
|
||||
val r = max(1.5f, R - t * 0.5f)
|
||||
val p = Path()
|
||||
val yy = y0 + t * 0.5f
|
||||
val yyb = y0 + s - t * 0.5f
|
||||
val xx = x0 + t * 0.5f
|
||||
val xxb = x0 + s - t * 0.5f
|
||||
|
||||
when (corner) {
|
||||
0 -> {
|
||||
val cTLx = x0 + R
|
||||
val cTLy = y0 + R
|
||||
val sTLx = x0 + R
|
||||
val sTLy = yy
|
||||
val eTLx = xx
|
||||
val eTLy = y0 + R
|
||||
p.moveTo(x0 + R + L, yy)
|
||||
p.lineTo(sTLx, sTLy)
|
||||
p.addCornerMinorArc(cTLx, cTLy, r, sTLx, sTLy, eTLx, eTLy)
|
||||
val yEndTL = min(y0 + R + L, y0 + s - R - t * 0.5f)
|
||||
p.lineTo(xx, max(yEndTL, y0 + R + 2f))
|
||||
}
|
||||
1 -> {
|
||||
val cTRx = x0 + s - R
|
||||
val cTRy = y0 + R
|
||||
val sTRx = x0 + s - R
|
||||
val sTRy = yy
|
||||
val eTRx = xxb
|
||||
val eTRy = y0 + R
|
||||
p.moveTo(x0 + s - R - L, yy)
|
||||
p.lineTo(sTRx, sTRy)
|
||||
p.addCornerMinorArc(cTRx, cTRy, r, sTRx, sTRy, eTRx, eTRy)
|
||||
val yEndTR = min(y0 + R + L, y0 + s - R - t * 0.5f)
|
||||
p.lineTo(xxb, max(yEndTR, y0 + R + 2f))
|
||||
}
|
||||
2 -> {
|
||||
val cBLx = x0 + R
|
||||
val cBLy = y0 + s - R
|
||||
val sBLx = x0 + R
|
||||
val sBLy = yyb
|
||||
val eBLx = xx
|
||||
val eBLy = y0 + s - R
|
||||
p.moveTo(x0 + R + L, yyb)
|
||||
p.lineTo(sBLx, sBLy)
|
||||
p.addCornerMinorArc(cBLx, cBLy, r, sBLx, sBLy, eBLx, eBLy)
|
||||
val yEndTopRef = max(min(y0 + R + L, y0 + s - R - t * 0.5f), y0 + R + 2f)
|
||||
val yLegBL = y0 + s + y0 - yEndTopRef
|
||||
p.lineTo(xx, yLegBL)
|
||||
}
|
||||
3 -> {
|
||||
val cBRx = x0 + s - R
|
||||
val cBRy = y0 + s - R
|
||||
val sBRx = x0 + s - R
|
||||
val sBRy = yyb
|
||||
val eBRx = xxb
|
||||
val eBRy = y0 + s - R
|
||||
p.moveTo(x0 + s - R - L, yyb)
|
||||
p.lineTo(sBRx, sBRy)
|
||||
p.addCornerMinorArc(cBRx, cBRy, r, sBRx, sBRy, eBRx, eBRy)
|
||||
val yEndTopRef = max(min(y0 + R + L, y0 + s - R - t * 0.5f), y0 + R + 2f)
|
||||
val yLegBR = y0 + s + y0 - yEndTopRef
|
||||
p.lineTo(xxb, yLegBR)
|
||||
}
|
||||
}
|
||||
return p
|
||||
}
|
||||
}
|
||||
152
client/android/src/org/amnezia/vpn/PairingQrScanGeometry.kt
Normal file
152
client/android/src/org/amnezia/vpn/PairingQrScanGeometry.kt
Normal file
@@ -0,0 +1,152 @@
|
||||
package org.amnezia.vpn
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import com.google.mlkit.vision.barcode.common.Barcode
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
object PairingQrScanGeometry {
|
||||
fun viewRectToInputImageRectFillCenter(
|
||||
viewW: Int,
|
||||
viewH: Int,
|
||||
imageW: Int,
|
||||
imageH: Int,
|
||||
viewRect: RectF
|
||||
): RectF {
|
||||
val scale = max(viewW / imageW.toFloat(), viewH / imageH.toFloat())
|
||||
val drawLeft = (viewW - imageW * scale) / 2f
|
||||
val drawTop = (viewH - imageH * scale) / 2f
|
||||
return RectF(
|
||||
(viewRect.left - drawLeft) / scale,
|
||||
(viewRect.top - drawTop) / scale,
|
||||
(viewRect.right - drawLeft) / scale,
|
||||
(viewRect.bottom - drawTop) / scale
|
||||
)
|
||||
}
|
||||
|
||||
fun pairingIosStyleHoleCornerRadiusPx(sidePx: Float, density: Float): Float {
|
||||
val d = density
|
||||
var holeR = min(28f * d, max(10f * d, sidePx * 0.056f))
|
||||
val half = 0.5f * sidePx
|
||||
holeR = min(holeR, max(6f * d, half - 2f * d))
|
||||
return max(holeR, 1f)
|
||||
}
|
||||
|
||||
fun barcodeBoxOverlapFraction(roi: RectF, box: Rect): Float {
|
||||
val bf = RectF(box)
|
||||
val inter = RectF(roi)
|
||||
if (!inter.intersect(bf)) return 0f
|
||||
val interArea = inter.width() * inter.height()
|
||||
val boxArea = bf.width() * bf.height()
|
||||
return if (boxArea <= 0f) 0f else interArea / boxArea
|
||||
}
|
||||
|
||||
fun barcodeMatchesPairingHole(
|
||||
roiInImageSpace: RectF,
|
||||
imageW: Int,
|
||||
imageH: Int,
|
||||
barcode: Barcode,
|
||||
minOverlapFraction: Float = PAIRING_SEND_MIN_OVERLAP_BBOX_FALLBACK
|
||||
): Boolean {
|
||||
if (imageW <= 0 || imageH <= 0) {
|
||||
return false
|
||||
}
|
||||
val roi = RectF(roiInImageSpace)
|
||||
val iw = imageW.toFloat()
|
||||
val ih = imageH.toFloat()
|
||||
roi.left = max(0f, roi.left)
|
||||
roi.top = max(0f, roi.top)
|
||||
roi.right = min(iw, roi.right)
|
||||
roi.bottom = min(ih, roi.bottom)
|
||||
if (roi.width() <= 0f || roi.height() <= 0f) {
|
||||
return false
|
||||
}
|
||||
|
||||
val corners = barcode.cornerPoints
|
||||
if (corners != null && corners.size >= 4) {
|
||||
for (p in corners) {
|
||||
if (!roi.contains(p.x.toFloat(), p.y.toFloat())) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
val box = barcode.boundingBox ?: return false
|
||||
val cx = box.centerX().toFloat()
|
||||
val cy = box.centerY().toFloat()
|
||||
if (!roi.contains(cx, cy)) {
|
||||
return false
|
||||
}
|
||||
return barcodeBoxOverlapFraction(roi, box) >= minOverlapFraction
|
||||
}
|
||||
|
||||
private const val PAIRING_SEND_MIN_OVERLAP_BBOX_FALLBACK = 0.72f
|
||||
|
||||
fun pairingIosStyleHoleRectF(
|
||||
viewW: Int,
|
||||
viewH: Int,
|
||||
headerBottomPx: Float,
|
||||
statusBarTopPx: Float,
|
||||
density: Float
|
||||
): RectF {
|
||||
val w = viewW.toFloat()
|
||||
val h = viewH.toFloat()
|
||||
val d = density
|
||||
if (w < 32f || h < 32f) {
|
||||
return RectF()
|
||||
}
|
||||
var hdrBottom = headerBottomPx
|
||||
if (hdrBottom < 8f * d) {
|
||||
hdrBottom = 132f * d + statusBarTopPx
|
||||
}
|
||||
val sqSz = floor(min(w, h) * 0.72).toFloat()
|
||||
var sqX = (w - sqSz) / 2f
|
||||
var sqY = (h - sqSz) / 2f
|
||||
sqY = max(sqY, hdrBottom + 8f * d)
|
||||
val kBottomBand = 80f * d
|
||||
val maxHoleBottom = h - kBottomBand
|
||||
if (sqY + sqSz > maxHoleBottom) {
|
||||
sqY = maxHoleBottom - sqSz
|
||||
sqY = max(sqY, hdrBottom + 8f * d)
|
||||
}
|
||||
sqX = max(8f * d, min(sqX, w - sqSz - 8f * d))
|
||||
sqY = max(hdrBottom + 4f * d, min(sqY, h - sqSz - 8f * d))
|
||||
return RectF(sqX, sqY, sqX + sqSz, sqY + sqSz)
|
||||
}
|
||||
|
||||
fun pairingIosStyleTorchCenterYPx(
|
||||
holeBottomPx: Float,
|
||||
bandBottomPx: Float,
|
||||
headerBottomPx: Float,
|
||||
safeBottomPx: Float,
|
||||
density: Float
|
||||
): Float {
|
||||
val torchH = 56f * density
|
||||
val d = density
|
||||
var torchCy = (holeBottomPx + bandBottomPx) * 0.5f
|
||||
val minC = holeBottomPx + torchH * 0.5f + 6f * d
|
||||
val maxC = bandBottomPx - torchH * 0.5f - max(6f * d, safeBottomPx)
|
||||
torchCy = max(minC, min(maxC, torchCy))
|
||||
if (minC > maxC) {
|
||||
torchCy = (minC + maxC) * 0.5f
|
||||
}
|
||||
val hdr = headerBottomPx + torchH * 0.5f + 10f * d
|
||||
return max(torchCy, hdr)
|
||||
}
|
||||
|
||||
fun pairingIosStyleHoleInImageCoords(
|
||||
viewW: Int,
|
||||
viewH: Int,
|
||||
headerBottomPx: Float,
|
||||
statusBarTopPx: Float,
|
||||
density: Float,
|
||||
imageW: Int,
|
||||
imageH: Int
|
||||
): RectF {
|
||||
val hv = pairingIosStyleHoleRectF(viewW, viewH, headerBottomPx, statusBarTopPx, density)
|
||||
return viewRectToInputImageRectFillCenter(viewW, viewH, imageW, imageH, hv)
|
||||
}
|
||||
}
|
||||
115
client/android/src/org/amnezia/vpn/PairingQrScanOverlayView.kt
Normal file
115
client/android/src/org/amnezia/vpn/PairingQrScanOverlayView.kt
Normal file
@@ -0,0 +1,115 @@
|
||||
package org.amnezia.vpn
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Path
|
||||
import android.graphics.RectF
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import kotlin.math.max
|
||||
|
||||
class PairingQrScanOverlayView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : View(context, attrs, defStyleAttr) {
|
||||
|
||||
init {
|
||||
isClickable = false
|
||||
isFocusable = false
|
||||
}
|
||||
|
||||
@Suppress("ClickableViewAccessibility")
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean = false
|
||||
|
||||
private val dimPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = 0x8C000000.toInt()
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
|
||||
private val bracketPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = 0xFFE8E8EC.toInt()
|
||||
style = Paint.Style.STROKE
|
||||
strokeCap = Paint.Cap.ROUND
|
||||
strokeJoin = Paint.Join.ROUND
|
||||
}
|
||||
|
||||
private var hole = RectF()
|
||||
|
||||
private val bracketPaths = arrayOfNulls<Path>(4)
|
||||
|
||||
private val dimPath = Path()
|
||||
|
||||
private var pairingHeaderBottomPx = 0f
|
||||
|
||||
fun setPairingHeaderBottomPx(px: Float) {
|
||||
if (pairingHeaderBottomPx == px) {
|
||||
return
|
||||
}
|
||||
pairingHeaderBottomPx = px
|
||||
recomputePairingHole()
|
||||
invalidate()
|
||||
}
|
||||
|
||||
private fun recomputePairingHole() {
|
||||
val w = width
|
||||
val h = height
|
||||
if (w <= 0 || h <= 0) {
|
||||
return
|
||||
}
|
||||
val topInset = ViewCompat.getRootWindowInsets(this)
|
||||
?.getInsets(WindowInsetsCompat.Type.statusBars())?.top?.toFloat() ?: 0f
|
||||
val d = resources.displayMetrics.density
|
||||
hole = PairingQrScanGeometry.pairingIosStyleHoleRectF(w, h, pairingHeaderBottomPx, topInset, d)
|
||||
rebuildBracketPaths()
|
||||
}
|
||||
|
||||
private fun rebuildBracketPaths() {
|
||||
val s = hole.width()
|
||||
if (s <= 0f) {
|
||||
bracketPaths.fill(null)
|
||||
return
|
||||
}
|
||||
val x0 = hole.left
|
||||
val y0 = hole.top
|
||||
val t = bracketPaint.strokeWidth
|
||||
val d = resources.displayMetrics.density
|
||||
val l = max(28f * d, s * 0.13f)
|
||||
val r = PairingQrScanGeometry.pairingIosStyleHoleCornerRadiusPx(s, d)
|
||||
for (i in 0..3) {
|
||||
bracketPaths[i] = PairingQrScanBracketPaths.bracketStrokePath(i, x0, y0, s, r, l, t)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
super.onSizeChanged(w, h, oldw, oldh)
|
||||
bracketPaint.strokeWidth = max(3f, 5f * resources.displayMetrics.density)
|
||||
recomputePairingHole()
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
val w = width.toFloat()
|
||||
val h = height.toFloat()
|
||||
val side = hole.width()
|
||||
if (side > 0f) {
|
||||
val d = resources.displayMetrics.density
|
||||
val rx = PairingQrScanGeometry.pairingIosStyleHoleCornerRadiusPx(side, d)
|
||||
dimPath.rewind()
|
||||
dimPath.fillType = Path.FillType.EVEN_ODD
|
||||
dimPath.addRect(0f, 0f, w, h, Path.Direction.CW)
|
||||
dimPath.addRoundRect(hole, rx, rx, Path.Direction.CW)
|
||||
canvas.drawPath(dimPath, dimPaint)
|
||||
} else {
|
||||
canvas.drawRect(0f, 0f, w, h, dimPaint)
|
||||
}
|
||||
|
||||
for (i in 0..3) {
|
||||
bracketPaths[i]?.let { canvas.drawPath(it, bracketPaint) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,4 +34,10 @@ object QtAndroidController {
|
||||
|
||||
external fun onActivityPaused()
|
||||
external fun onActivityResumed()
|
||||
|
||||
external fun onCameraPermissionResult(granted: Boolean)
|
||||
|
||||
external fun onPairingQrCameraClosed()
|
||||
|
||||
external fun onPairingQrCameraUserDismissed()
|
||||
}
|
||||
@@ -28,6 +28,7 @@ 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
|
||||
@@ -44,6 +45,8 @@ 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,6 +49,7 @@ 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)
|
||||
|
||||
@@ -35,6 +35,8 @@ set(HEADERS ${HEADERS}
|
||||
${CLIENT_ROOT_DIR}/core/installers/torInstaller.h
|
||||
${CLIENT_ROOT_DIR}/core/installers/sftpInstaller.h
|
||||
${CLIENT_ROOT_DIR}/core/installers/socks5Installer.h
|
||||
${CLIENT_ROOT_DIR}/core/installers/mtProxyInstaller.h
|
||||
${CLIENT_ROOT_DIR}/core/installers/telemtInstaller.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/appSplitTunnelingController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/ipSplitTunnelingController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/allowedDnsController.h
|
||||
@@ -43,6 +45,7 @@ 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
|
||||
@@ -63,6 +66,8 @@ 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
|
||||
@@ -110,6 +115,8 @@ set(SOURCES ${SOURCES}
|
||||
${CLIENT_ROOT_DIR}/core/installers/torInstaller.cpp
|
||||
${CLIENT_ROOT_DIR}/core/installers/sftpInstaller.cpp
|
||||
${CLIENT_ROOT_DIR}/core/installers/socks5Installer.cpp
|
||||
${CLIENT_ROOT_DIR}/core/installers/mtProxyInstaller.cpp
|
||||
${CLIENT_ROOT_DIR}/core/installers/telemtInstaller.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/appSplitTunnelingController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/ipSplitTunnelingController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/allowedDnsController.cpp
|
||||
@@ -118,6 +125,7 @@ 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
|
||||
@@ -153,6 +161,7 @@ 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()
|
||||
|
||||
@@ -201,12 +210,14 @@ file(GLOB UI_MODELS_H CONFIGURE_DEPENDS
|
||||
${CLIENT_ROOT_DIR}/ui/models/*.h
|
||||
${CLIENT_ROOT_DIR}/ui/models/protocols/*.h
|
||||
${CLIENT_ROOT_DIR}/ui/models/services/*.h
|
||||
${CLIENT_ROOT_DIR}/ui/models/utils/*.h
|
||||
${CLIENT_ROOT_DIR}/ui/models/api/*.h
|
||||
)
|
||||
file(GLOB UI_MODELS_CPP CONFIGURE_DEPENDS
|
||||
${CLIENT_ROOT_DIR}/ui/models/*.cpp
|
||||
${CLIENT_ROOT_DIR}/ui/models/protocols/*.cpp
|
||||
${CLIENT_ROOT_DIR}/ui/models/services/*.cpp
|
||||
${CLIENT_ROOT_DIR}/ui/models/utils/*.cpp
|
||||
${CLIENT_ROOT_DIR}/ui/models/api/*.cpp
|
||||
)
|
||||
|
||||
|
||||
@@ -20,14 +20,123 @@
|
||||
#include "core/models/protocols/xrayProtocolConfig.h"
|
||||
|
||||
namespace {
|
||||
Logger logger("XrayConfigurator");
|
||||
}
|
||||
Logger logger("XrayConfigurator");
|
||||
|
||||
QString normalizeXhttpMode(const QString &m) {
|
||||
const QString t = m.trimmed();
|
||||
if (t.isEmpty() || t.compare(QLatin1String("Auto"), Qt::CaseInsensitive) == 0) {
|
||||
return QStringLiteral("auto");
|
||||
}
|
||||
if (t.compare(QLatin1String("Packet-up"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("packet-up");
|
||||
if (t.compare(QLatin1String("Stream-up"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("stream-up");
|
||||
if (t.compare(QLatin1String("Stream-one"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("stream-one");
|
||||
return t.toLower();
|
||||
}
|
||||
|
||||
// Xray-core: empty → path; "None" in UI → omit (core default path)
|
||||
QString normalizeSessionSeqPlacement(const QString &p)
|
||||
{
|
||||
if (p.isEmpty() || p.compare(QLatin1String("None"), Qt::CaseInsensitive) == 0)
|
||||
return {};
|
||||
return p.toLower();
|
||||
}
|
||||
|
||||
QString normalizeUplinkDataPlacement(const QString &p)
|
||||
{
|
||||
if (p.isEmpty() || p.compare(QLatin1String("Body"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("body");
|
||||
if (p.compare(QLatin1String("Auto"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("auto");
|
||||
if (p.compare(QLatin1String("Query"), Qt::CaseInsensitive) == 0)
|
||||
// "Query" is not valid for uplink payload in splithttp; closest documented mode
|
||||
return QStringLiteral("header");
|
||||
return p.toLower();
|
||||
}
|
||||
|
||||
// splithttp: cookie | header | query | queryInHeader (not "body")
|
||||
QString normalizeXPaddingPlacement(const QString &p)
|
||||
{
|
||||
QString t = p.trimmed();
|
||||
if (t.isEmpty())
|
||||
return QString::fromLatin1(amnezia::protocols::xray::defaultXPaddingPlacement).toLower();
|
||||
if (t.compare(QLatin1String("Body"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("queryInHeader");
|
||||
if (t.contains(QLatin1String("queryInHeader"), Qt::CaseInsensitive)
|
||||
|| t.compare(QLatin1String("Query in header"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("queryInHeader");
|
||||
return t.toLower();
|
||||
}
|
||||
|
||||
// splithttp: repeat-x | tokenish
|
||||
QString normalizeXPaddingMethod(const QString &m)
|
||||
{
|
||||
QString t = m.trimmed();
|
||||
if (t.isEmpty() || t.compare(QLatin1String("Repeat-x"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("repeat-x");
|
||||
if (t.compare(QLatin1String("Tokenish"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("tokenish");
|
||||
if (t.compare(QLatin1String("Random"), Qt::CaseInsensitive) == 0
|
||||
|| t.compare(QLatin1String("Zero"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("repeat-x");
|
||||
return t.toLower();
|
||||
}
|
||||
|
||||
void putIntRangeIfAny(QJsonObject &obj, const char *key, QString minV, QString maxV, const char *fallbackMin,
|
||||
const char *fallbackMax)
|
||||
{
|
||||
if (minV.isEmpty() && maxV.isEmpty())
|
||||
return;
|
||||
if (minV.isEmpty())
|
||||
minV = QString::fromLatin1(fallbackMin);
|
||||
if (maxV.isEmpty())
|
||||
maxV = QString::fromLatin1(fallbackMax);
|
||||
QJsonObject r;
|
||||
r[QStringLiteral("from")] = minV.toInt();
|
||||
r[QStringLiteral("to")] = maxV.toInt();
|
||||
obj[QString::fromUtf8(key)] = r;
|
||||
}
|
||||
|
||||
// Desktop applies this in XrayProtocol::start(); iOS/Android pass JSON straight to libxray — same fixes here.
|
||||
void sanitizeXrayNativeConfig(amnezia::ProtocolConfig &pc)
|
||||
{
|
||||
QString c = pc.nativeConfig();
|
||||
if (c.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
bool changed = false;
|
||||
if (c.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) {
|
||||
c.replace(QLatin1String("Mozilla/5.0"), QString::fromLatin1(amnezia::protocols::xray::defaultFingerprint),
|
||||
Qt::CaseInsensitive);
|
||||
changed = true;
|
||||
}
|
||||
const QString legacyListen = QString::fromLatin1(amnezia::protocols::xray::defaultLocalAddr);
|
||||
const QString listenOk = QString::fromLatin1(amnezia::protocols::xray::defaultLocalListenAddr);
|
||||
if (c.contains(legacyListen)) {
|
||||
c.replace(legacyListen, listenOk);
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
pc.setNativeConfig(c);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
XrayConfigurator::XrayConfigurator(SshSession* sshSession, QObject *parent)
|
||||
: ConfiguratorBase(sshSession, parent)
|
||||
{
|
||||
}
|
||||
|
||||
amnezia::ProtocolConfig XrayConfigurator::processConfigWithLocalSettings(const amnezia::ConnectionSettings &settings,
|
||||
amnezia::ProtocolConfig protocolConfig)
|
||||
{
|
||||
applyDnsToNativeConfig(settings.dns, protocolConfig);
|
||||
sanitizeXrayNativeConfig(protocolConfig);
|
||||
return protocolConfig;
|
||||
}
|
||||
|
||||
QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentials, DockerContainer container,
|
||||
const ContainerConfig &containerConfig,
|
||||
const DnsSettings &dnsSettings,
|
||||
@@ -35,11 +144,19 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia
|
||||
{
|
||||
// Generate new UUID for client
|
||||
QString clientId = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// Get current server config
|
||||
QString currentConfig = m_sshSession->getTextFileFromContainer(
|
||||
container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode);
|
||||
|
||||
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Failed to get server config file";
|
||||
return "";
|
||||
@@ -54,7 +171,7 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia
|
||||
}
|
||||
|
||||
QJsonObject serverConfig = doc.object();
|
||||
|
||||
|
||||
// Validate server config structure
|
||||
if (!serverConfig.contains(amnezia::protocols::xray::inbounds)) {
|
||||
logger.error() << "Server config missing 'inbounds' field";
|
||||
@@ -68,7 +185,7 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
QJsonObject inbound = inbounds[0].toObject();
|
||||
if (!inbound.contains(amnezia::protocols::xray::settings)) {
|
||||
logger.error() << "Inbound missing 'settings' field";
|
||||
@@ -84,26 +201,29 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia
|
||||
}
|
||||
|
||||
QJsonArray clients = settings[amnezia::protocols::xray::clients].toArray();
|
||||
|
||||
|
||||
// Create configuration for new client
|
||||
QJsonObject clientConfig {
|
||||
{amnezia::protocols::xray::id, clientId},
|
||||
{amnezia::protocols::xray::flow, "xtls-rprx-vision"}
|
||||
};
|
||||
|
||||
clientConfig[amnezia::protocols::xray::id] = clientId;
|
||||
if (!flowValue.isEmpty()) {
|
||||
clientConfig[amnezia::protocols::xray::flow] = flowValue;
|
||||
}
|
||||
|
||||
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,
|
||||
container,
|
||||
credentials,
|
||||
updatedConfig,
|
||||
amnezia::protocols::xray::serverConfigPath,
|
||||
libssh::ScpOverwriteMode::ScpOverwriteExisting
|
||||
@@ -116,7 +236,7 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia
|
||||
// Restart container
|
||||
QString restartScript = QString("sudo docker restart $CONTAINER_NAME");
|
||||
errorCode = m_sshSession->runScript(
|
||||
credentials,
|
||||
credentials,
|
||||
m_sshSession->replaceVars(restartScript, amnezia::genBaseVars(credentials, container, dnsSettings.primaryDns, dnsSettings.secondaryDns))
|
||||
);
|
||||
|
||||
@@ -128,75 +248,286 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia
|
||||
return clientId;
|
||||
}
|
||||
|
||||
ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container,
|
||||
const ContainerConfig &containerConfig,
|
||||
const DnsSettings &dnsSettings,
|
||||
ErrorCode &errorCode)
|
||||
QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, const QString &clientId) const
|
||||
{
|
||||
const XrayServerConfig* serverConfig = nullptr;
|
||||
if (auto* xrayConfig = containerConfig.protocolConfig.as<XrayProtocolConfig>()) {
|
||||
serverConfig = &xrayConfig->serverConfig;
|
||||
QJsonObject streamSettings;
|
||||
const auto &xhttp = srv.xhttp;
|
||||
const auto &mkcp = srv.mkcp;
|
||||
namespace px = amnezia::protocols::xray;
|
||||
|
||||
QString networkValue = QStringLiteral("tcp");
|
||||
if (srv.transport == QLatin1String("xhttp"))
|
||||
networkValue = QStringLiteral("xhttp");
|
||||
else if (srv.transport == QLatin1String("mkcp"))
|
||||
networkValue = QStringLiteral("kcp");
|
||||
streamSettings[px::network] = networkValue;
|
||||
|
||||
streamSettings[px::security] = srv.security;
|
||||
|
||||
if (srv.security == QLatin1String("tls")) {
|
||||
QJsonObject tlsSettings;
|
||||
const QString sniEff = srv.sni.isEmpty() ? QString::fromLatin1(px::defaultSni) : srv.sni;
|
||||
tlsSettings[px::serverName] = sniEff;
|
||||
const QString alpnEff = srv.alpn.isEmpty() ? QString::fromLatin1(px::defaultAlpn) : srv.alpn;
|
||||
QJsonArray alpnArray;
|
||||
for (const QString &a : alpnEff.split(QLatin1Char(','))) {
|
||||
const QString t = a.trimmed();
|
||||
if (!t.isEmpty())
|
||||
alpnArray.append(t);
|
||||
}
|
||||
if (!alpnArray.isEmpty())
|
||||
tlsSettings[QStringLiteral("alpn")] = alpnArray;
|
||||
const QString fpEff = srv.fingerprint.isEmpty() ? QString::fromLatin1(px::defaultFingerprint) : srv.fingerprint;
|
||||
tlsSettings[px::fingerprint] = fpEff;
|
||||
streamSettings[QStringLiteral("tlsSettings")] = tlsSettings;
|
||||
}
|
||||
|
||||
|
||||
if (srv.security == QLatin1String("reality")) {
|
||||
QJsonObject realSettings;
|
||||
const QString fpEff = srv.fingerprint.isEmpty() ? QString::fromLatin1(px::defaultFingerprint) : srv.fingerprint;
|
||||
realSettings[px::fingerprint] = fpEff;
|
||||
const QString sniEff = srv.sni.isEmpty() ? QString::fromLatin1(px::defaultSni) : srv.sni;
|
||||
realSettings[px::serverName] = sniEff;
|
||||
streamSettings[px::realitySettings] = realSettings;
|
||||
}
|
||||
|
||||
// XHTTP — JSON must match Xray-core SplitHTTPConfig (flat xPadding fields, see transport_internet.go)
|
||||
if (srv.transport == QLatin1String("xhttp")) {
|
||||
QJsonObject xo;
|
||||
const QString hostEff = xhttp.host.isEmpty() ? QString::fromLatin1(px::defaultXhttpHost) : xhttp.host;
|
||||
xo[QStringLiteral("host")] = hostEff;
|
||||
if (!xhttp.path.isEmpty())
|
||||
xo[QStringLiteral("path")] = xhttp.path;
|
||||
xo[QStringLiteral("mode")] = normalizeXhttpMode(xhttp.mode);
|
||||
|
||||
if (xhttp.headersTemplate.compare(QLatin1String("HTTP"), Qt::CaseInsensitive) == 0) {
|
||||
QJsonObject headers;
|
||||
headers[QStringLiteral("Host")] = hostEff;
|
||||
xo[QStringLiteral("headers")] = headers;
|
||||
}
|
||||
|
||||
const QString methodEff =
|
||||
xhttp.uplinkMethod.isEmpty() ? QString::fromLatin1(px::defaultXhttpUplinkMethod) : xhttp.uplinkMethod;
|
||||
xo[QStringLiteral("uplinkHTTPMethod")] = methodEff.toUpper();
|
||||
|
||||
xo[QStringLiteral("noGRPCHeader")] = xhttp.disableGrpc;
|
||||
xo[QStringLiteral("noSSEHeader")] = xhttp.disableSse;
|
||||
|
||||
const QString sessPl = normalizeSessionSeqPlacement(xhttp.sessionPlacement);
|
||||
if (!sessPl.isEmpty())
|
||||
xo[QStringLiteral("sessionPlacement")] = sessPl;
|
||||
const QString seqPl = normalizeSessionSeqPlacement(xhttp.seqPlacement);
|
||||
if (!seqPl.isEmpty())
|
||||
xo[QStringLiteral("seqPlacement")] = seqPl;
|
||||
if (!xhttp.sessionKey.isEmpty())
|
||||
xo[QStringLiteral("sessionKey")] = xhttp.sessionKey;
|
||||
if (!xhttp.seqKey.isEmpty())
|
||||
xo[QStringLiteral("seqKey")] = xhttp.seqKey;
|
||||
|
||||
xo[QStringLiteral("uplinkDataPlacement")] = normalizeUplinkDataPlacement(xhttp.uplinkDataPlacement);
|
||||
if (!xhttp.uplinkDataKey.isEmpty())
|
||||
xo[QStringLiteral("uplinkDataKey")] = xhttp.uplinkDataKey;
|
||||
|
||||
const QString ucs = xhttp.uplinkChunkSize.isEmpty() ? QString::fromLatin1(px::defaultXhttpUplinkChunkSize)
|
||||
: xhttp.uplinkChunkSize;
|
||||
if (!ucs.isEmpty() && ucs != QLatin1String("0")) {
|
||||
const int v = ucs.toInt();
|
||||
QJsonObject chunkR;
|
||||
chunkR[QStringLiteral("from")] = v;
|
||||
chunkR[QStringLiteral("to")] = v;
|
||||
xo[QStringLiteral("uplinkChunkSize")] = chunkR;
|
||||
}
|
||||
|
||||
if (!xhttp.scMaxBufferedPosts.isEmpty())
|
||||
xo[QStringLiteral("scMaxBufferedPosts")] = xhttp.scMaxBufferedPosts.toLongLong();
|
||||
|
||||
putIntRangeIfAny(xo, "scMaxEachPostBytes", xhttp.scMaxEachPostBytesMin, xhttp.scMaxEachPostBytesMax,
|
||||
px::defaultXhttpScMaxEachPostBytesMin, px::defaultXhttpScMaxEachPostBytesMax);
|
||||
putIntRangeIfAny(xo, "scMinPostsIntervalMs", xhttp.scMinPostsIntervalMsMin, xhttp.scMinPostsIntervalMsMax,
|
||||
px::defaultXhttpScMinPostsIntervalMsMin, px::defaultXhttpScMinPostsIntervalMsMax);
|
||||
putIntRangeIfAny(xo, "scStreamUpServerSecs", xhttp.scStreamUpServerSecsMin, xhttp.scStreamUpServerSecsMax,
|
||||
px::defaultXhttpScStreamUpServerSecsMin, px::defaultXhttpScStreamUpServerSecsMax);
|
||||
|
||||
const auto &pad = xhttp.xPadding;
|
||||
xo[QStringLiteral("xPaddingObfsMode")] = pad.obfsMode;
|
||||
if (pad.obfsMode) {
|
||||
if (!pad.bytesMin.isEmpty() || !pad.bytesMax.isEmpty()) {
|
||||
QJsonObject br;
|
||||
br[QStringLiteral("from")] = pad.bytesMin.isEmpty() ? 1 : pad.bytesMin.toInt();
|
||||
br[QStringLiteral("to")] = pad.bytesMax.isEmpty() ? (pad.bytesMin.isEmpty() ? 256 : pad.bytesMin.toInt())
|
||||
: pad.bytesMax.toInt();
|
||||
xo[QStringLiteral("xPaddingBytes")] = br;
|
||||
}
|
||||
xo[QStringLiteral("xPaddingKey")] = pad.key.isEmpty() ? QStringLiteral("x_padding") : pad.key;
|
||||
xo[QStringLiteral("xPaddingHeader")] = pad.header.isEmpty() ? QStringLiteral("X-Padding") : pad.header;
|
||||
xo[QStringLiteral("xPaddingPlacement")] = normalizeXPaddingPlacement(
|
||||
pad.placement.isEmpty() ? QString::fromLatin1(px::defaultXPaddingPlacement) : pad.placement);
|
||||
xo[QStringLiteral("xPaddingMethod")] = normalizeXPaddingMethod(
|
||||
pad.method.isEmpty() ? QString::fromLatin1(px::defaultXPaddingMethod) : pad.method);
|
||||
}
|
||||
|
||||
// xmux: Xray has no "enabled" flag; omit object when UI disables multiplex tuning.
|
||||
if (xhttp.xmux.enabled) {
|
||||
QJsonObject mux;
|
||||
auto addMuxRange = [&](const char *key, const QString &a, const QString &b) {
|
||||
if (a.isEmpty() && b.isEmpty())
|
||||
return;
|
||||
QJsonObject r;
|
||||
r[QStringLiteral("from")] = a.isEmpty() ? 0 : a.toInt();
|
||||
r[QStringLiteral("to")] = b.isEmpty() ? 0 : b.toInt();
|
||||
mux[QString::fromUtf8(key)] = r;
|
||||
};
|
||||
addMuxRange("maxConcurrency", xhttp.xmux.maxConcurrencyMin, xhttp.xmux.maxConcurrencyMax);
|
||||
addMuxRange("maxConnections", xhttp.xmux.maxConnectionsMin, xhttp.xmux.maxConnectionsMax);
|
||||
addMuxRange("cMaxReuseTimes", xhttp.xmux.cMaxReuseTimesMin, xhttp.xmux.cMaxReuseTimesMax);
|
||||
addMuxRange("hMaxRequestTimes", xhttp.xmux.hMaxRequestTimesMin, xhttp.xmux.hMaxRequestTimesMax);
|
||||
addMuxRange("hMaxReusableSecs", xhttp.xmux.hMaxReusableSecsMin, xhttp.xmux.hMaxReusableSecsMax);
|
||||
if (!xhttp.xmux.hKeepAlivePeriod.isEmpty())
|
||||
mux[QStringLiteral("hKeepAlivePeriod")] = xhttp.xmux.hKeepAlivePeriod.toLongLong();
|
||||
if (!mux.isEmpty())
|
||||
xo[QStringLiteral("xmux")] = mux;
|
||||
}
|
||||
|
||||
streamSettings[QStringLiteral("xhttpSettings")] = xo;
|
||||
}
|
||||
|
||||
if (srv.transport == QLatin1String("mkcp")) {
|
||||
QJsonObject kcpObj;
|
||||
const QString ttiEff = mkcp.tti.isEmpty() ? QString::fromLatin1(px::defaultMkcpTti) : mkcp.tti;
|
||||
const QString upEff = mkcp.uplinkCapacity.isEmpty() ? QString::fromLatin1(px::defaultMkcpUplinkCapacity)
|
||||
: mkcp.uplinkCapacity;
|
||||
const QString downEff = mkcp.downlinkCapacity.isEmpty() ? QString::fromLatin1(px::defaultMkcpDownlinkCapacity)
|
||||
: mkcp.downlinkCapacity;
|
||||
const QString rbufEff = mkcp.readBufferSize.isEmpty() ? QString::fromLatin1(px::defaultMkcpReadBufferSize)
|
||||
: mkcp.readBufferSize;
|
||||
const QString wbufEff = mkcp.writeBufferSize.isEmpty() ? QString::fromLatin1(px::defaultMkcpWriteBufferSize)
|
||||
: mkcp.writeBufferSize;
|
||||
kcpObj[QStringLiteral("tti")] = ttiEff.toInt();
|
||||
kcpObj[QStringLiteral("uplinkCapacity")] = upEff.toInt();
|
||||
kcpObj[QStringLiteral("downlinkCapacity")] = downEff.toInt();
|
||||
kcpObj[QStringLiteral("readBufferSize")] = rbufEff.toInt();
|
||||
kcpObj[QStringLiteral("writeBufferSize")] = wbufEff.toInt();
|
||||
kcpObj[QStringLiteral("congestion")] = mkcp.congestion;
|
||||
streamSettings[QStringLiteral("kcpSettings")] = kcpObj;
|
||||
}
|
||||
|
||||
return streamSettings;
|
||||
}
|
||||
|
||||
ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container,
|
||||
const ContainerConfig &containerConfig,
|
||||
const DnsSettings &dnsSettings,
|
||||
ErrorCode &errorCode)
|
||||
{
|
||||
const XrayServerConfig *serverConfig = nullptr;
|
||||
if (const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>()) {
|
||||
serverConfig = &xrayCfg->serverConfig;
|
||||
}
|
||||
|
||||
if (!serverConfig) {
|
||||
logger.error() << "No XrayProtocolConfig found";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return XrayProtocolConfig{};
|
||||
}
|
||||
|
||||
const XrayServerConfig &srv = *serverConfig;
|
||||
|
||||
QString xrayClientId = prepareServerConfig(credentials, container, containerConfig, dnsSettings, errorCode);
|
||||
if (errorCode != ErrorCode::NoError || xrayClientId.isEmpty()) {
|
||||
logger.error() << "Failed to prepare server config";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
errorCode = ErrorCode::InternalError;
|
||||
}
|
||||
return XrayProtocolConfig{};
|
||||
}
|
||||
|
||||
amnezia::ScriptVars vars = amnezia::genBaseVars(credentials, container, dnsSettings.primaryDns, dnsSettings.secondaryDns);
|
||||
vars.append(amnezia::genProtocolVarsForContainer(container, containerConfig));
|
||||
QString config = m_sshSession->replaceVars(amnezia::scriptData(ProtocolScriptType::xray_template, container), vars);
|
||||
|
||||
if (config.isEmpty()) {
|
||||
logger.error() << "Failed to get config template";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
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", "");
|
||||
}
|
||||
|
||||
QString xrayPublicKey =
|
||||
m_sshSession->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::PublicKeyPath, errorCode);
|
||||
if (errorCode != ErrorCode::NoError || xrayPublicKey.isEmpty()) {
|
||||
logger.error() << "Failed to get public key";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return XrayProtocolConfig{};
|
||||
}
|
||||
xrayPublicKey.replace("\n", "");
|
||||
|
||||
QString xrayShortId =
|
||||
m_sshSession->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::shortidPath, errorCode);
|
||||
if (errorCode != ErrorCode::NoError || xrayShortId.isEmpty()) {
|
||||
logger.error() << "Failed to get short ID";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return XrayProtocolConfig{};
|
||||
}
|
||||
xrayShortId.replace("\n", "");
|
||||
|
||||
if (!config.contains("$XRAY_CLIENT_ID") || !config.contains("$XRAY_PUBLIC_KEY") || !config.contains("$XRAY_SHORT_ID")) {
|
||||
logger.error() << "Config template missing required variables:"
|
||||
<< "XRAY_CLIENT_ID:" << !config.contains("$XRAY_CLIENT_ID")
|
||||
<< "XRAY_PUBLIC_KEY:" << !config.contains("$XRAY_PUBLIC_KEY")
|
||||
<< "XRAY_SHORT_ID:" << !config.contains("$XRAY_SHORT_ID");
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return XrayProtocolConfig{};
|
||||
// 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;
|
||||
}
|
||||
|
||||
config.replace("$XRAY_CLIENT_ID", xrayClientId);
|
||||
config.replace("$XRAY_PUBLIC_KEY", xrayPublicKey);
|
||||
config.replace("$XRAY_SHORT_ID", xrayShortId);
|
||||
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;
|
||||
if (serverConfig) {
|
||||
protocolConfig.serverConfig = *serverConfig;
|
||||
}
|
||||
|
||||
protocolConfig.serverConfig = srv;
|
||||
|
||||
XrayClientConfig clientConfig;
|
||||
clientConfig.nativeConfig = config;
|
||||
clientConfig.localPort = "";
|
||||
qDebug() << "config:" << config;
|
||||
clientConfig.localPort = QString(amnezia::protocols::xray::defaultLocalProxyPort);
|
||||
clientConfig.id = xrayClientId;
|
||||
|
||||
|
||||
protocolConfig.setClientConfig(clientConfig);
|
||||
|
||||
|
||||
return protocolConfig;
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,13 @@
|
||||
#define XRAY_CONFIGURATOR_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "configuratorBase.h"
|
||||
#include "core/utils/errorCodes.h"
|
||||
#include "core/utils/routeModes.h"
|
||||
#include "core/utils/commonStructs.h"
|
||||
#include "core/models/protocols/xrayProtocolConfig.h"
|
||||
|
||||
class XrayConfigurator : public ConfiguratorBase
|
||||
{
|
||||
@@ -18,10 +20,17 @@ public:
|
||||
const amnezia::DnsSettings &dnsSettings,
|
||||
amnezia::ErrorCode &errorCode) override;
|
||||
|
||||
amnezia::ProtocolConfig processConfigWithLocalSettings(const amnezia::ConnectionSettings &settings,
|
||||
amnezia::ProtocolConfig protocolConfig) override;
|
||||
|
||||
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
|
||||
QJsonObject buildStreamSettings(const amnezia::XrayServerConfig &srv,
|
||||
const QString &clientId) const;
|
||||
};
|
||||
|
||||
#endif // XRAY_CONFIGURATOR_H
|
||||
|
||||
@@ -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);
|
||||
auto future = gatewayController->postAsync(QString("%1v1/news"), payload, nullptr, gatewayController);
|
||||
return future.then([gatewayController](QPair<ErrorCode, QByteArray> result) -> QPair<ErrorCode, QJsonArray> {
|
||||
auto [errorCode, responseBody] = result;
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
|
||||
204
client/core/controllers/api/pairingController.cpp
Normal file
204
client/core/controllers/api/pairingController.cpp
Normal file
@@ -0,0 +1,204 @@
|
||||
#include "pairingController.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QSysInfo>
|
||||
#include "core/repositories/secureAppSettingsRepository.h"
|
||||
#include "core/utils/api/apiUtils.h"
|
||||
#include "core/utils/constants/apiConstants.h"
|
||||
#include "core/utils/constants/apiKeys.h"
|
||||
#include "version.h"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr qsizetype kPairingMaxQrUuidChars = 128;
|
||||
constexpr qsizetype kPairingMaxVpnConfigChars = 256 * 1024;
|
||||
constexpr qsizetype kPairingMaxApiKeyChars = 8192;
|
||||
constexpr qsizetype kPairingMaxServiceTypeChars = 64;
|
||||
constexpr qsizetype kPairingMaxUserCountryCodeChars = 32;
|
||||
|
||||
ErrorCode applyGatewayOrOpenApiGenerateError(const QJsonObject &obj, PairingController::QrPairingConfigPayload &outPayload)
|
||||
{
|
||||
ErrorCode apiStatus = apiUtils::errorCodeFromGatewayJsonHttpStatus(obj);
|
||||
if (apiStatus != ErrorCode::NoError) {
|
||||
return apiStatus;
|
||||
}
|
||||
|
||||
const QString config = obj.value(apiDefs::key::config).toString();
|
||||
if (!config.isEmpty()) {
|
||||
outPayload.config = config;
|
||||
outPayload.serviceInfo = obj.value(apiDefs::key::serviceInfo).toObject();
|
||||
outPayload.supportedProtocols = obj.value(apiDefs::key::supportedProtocols).toArray();
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
if (obj.contains(QStringLiteral("detail"))) {
|
||||
return ErrorCode::ApiConfigEmptyError;
|
||||
}
|
||||
|
||||
const QString msg = obj.value(QStringLiteral("message")).toString();
|
||||
if (msg.contains(QStringLiteral("timeout"), Qt::CaseInsensitive)) {
|
||||
return ErrorCode::ApiConfigTimeoutError;
|
||||
}
|
||||
if (msg.contains(QStringLiteral("Too Many"), Qt::CaseInsensitive)) {
|
||||
return ErrorCode::ApiPairingRateLimitedError;
|
||||
}
|
||||
if (msg.contains(QStringLiteral("Unavailable"), Qt::CaseInsensitive)) {
|
||||
return ErrorCode::ApiPairingServiceUnavailableError;
|
||||
}
|
||||
if (!msg.isEmpty()) {
|
||||
return ErrorCode::ApiConfigDownloadError;
|
||||
}
|
||||
|
||||
return ErrorCode::ApiConfigEmptyError;
|
||||
}
|
||||
|
||||
ErrorCode applyGatewayOrOpenApiScanError(const QJsonObject &obj)
|
||||
{
|
||||
const QString msgProbe = obj.value(QStringLiteral("message")).toString();
|
||||
if (msgProbe.contains(QStringLiteral("limit"), Qt::CaseInsensitive)
|
||||
&& (msgProbe.contains(QStringLiteral("device"), Qt::CaseInsensitive)
|
||||
|| msgProbe.contains(QStringLiteral("maximum"), Qt::CaseInsensitive)
|
||||
|| msgProbe.contains(QStringLiteral("max"), Qt::CaseInsensitive))) {
|
||||
return ErrorCode::ApiConfigLimitError;
|
||||
}
|
||||
|
||||
ErrorCode apiStatus = apiUtils::errorCodeFromGatewayJsonHttpStatus(obj);
|
||||
if (apiStatus != ErrorCode::NoError) {
|
||||
return apiStatus;
|
||||
}
|
||||
|
||||
if (obj.value(QStringLiteral("message")).toString() == QLatin1String("OK")) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
if (obj.contains(QStringLiteral("detail"))) {
|
||||
return ErrorCode::ApiPairingForbiddenError;
|
||||
}
|
||||
|
||||
const QString msg = obj.value(QStringLiteral("message")).toString();
|
||||
if (msg.contains(QStringLiteral("QR session"), Qt::CaseInsensitive)
|
||||
&& (msg.contains(QStringLiteral("not found"), Qt::CaseInsensitive)
|
||||
|| msg.contains(QStringLiteral("expired"), Qt::CaseInsensitive))) {
|
||||
return ErrorCode::ApiPairingSessionExpiredError;
|
||||
}
|
||||
if (msg.contains(QStringLiteral("not found"), Qt::CaseInsensitive) || msg.contains(QStringLiteral("expired"), Qt::CaseInsensitive)) {
|
||||
return ErrorCode::ApiNotFoundError;
|
||||
}
|
||||
if (msg.contains(QStringLiteral("Conflict"), Qt::CaseInsensitive) || msg.contains(QStringLiteral("already"), Qt::CaseInsensitive)) {
|
||||
return ErrorCode::ApiPairingConflictError;
|
||||
}
|
||||
if (msg.contains(QStringLiteral("Too Many"), Qt::CaseInsensitive)) {
|
||||
return ErrorCode::ApiPairingRateLimitedError;
|
||||
}
|
||||
if (msg.contains(QStringLiteral("Unavailable"), Qt::CaseInsensitive)) {
|
||||
return ErrorCode::ApiPairingServiceUnavailableError;
|
||||
}
|
||||
if (!msg.isEmpty()) {
|
||||
return ErrorCode::ApiConfigDownloadError;
|
||||
}
|
||||
|
||||
return ErrorCode::ApiConfigEmptyError;
|
||||
}
|
||||
|
||||
ErrorCode interpretGenerateQrJson(const QJsonObject &obj, PairingController::QrPairingConfigPayload &outPayload)
|
||||
{
|
||||
return applyGatewayOrOpenApiGenerateError(obj, outPayload);
|
||||
}
|
||||
|
||||
ErrorCode interpretScanQrJson(const QJsonObject &obj)
|
||||
{
|
||||
return applyGatewayOrOpenApiScanError(obj);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ErrorCode PairingController::parseGenerateQrResponseBody(const QByteArray &responseBody, QrPairingConfigPayload &outPayload)
|
||||
{
|
||||
outPayload = QrPairingConfigPayload {};
|
||||
const QJsonObject obj = QJsonDocument::fromJson(responseBody).object();
|
||||
return interpretGenerateQrJson(obj, outPayload);
|
||||
}
|
||||
|
||||
ErrorCode PairingController::parseScanQrResponseBody(const QByteArray &responseBody, QString *outOptionalDisplayName)
|
||||
{
|
||||
if (outOptionalDisplayName) {
|
||||
outOptionalDisplayName->clear();
|
||||
}
|
||||
const QJsonObject obj = QJsonDocument::fromJson(responseBody).object();
|
||||
const ErrorCode err = interpretScanQrJson(obj);
|
||||
if (err != ErrorCode::NoError) {
|
||||
return err;
|
||||
}
|
||||
if (outOptionalDisplayName) {
|
||||
const QString deviceName = obj.value(QStringLiteral("device_name")).toString().trimmed();
|
||||
if (!deviceName.isEmpty()) {
|
||||
*outOptionalDisplayName = deviceName;
|
||||
}
|
||||
}
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode PairingController::validatePairingScanFields(const QString &qrUuid, const QString &vpnConfig, const QString &apiKey,
|
||||
const QString &serviceType, const QString &userCountryCode)
|
||||
{
|
||||
if (qrUuid.size() > kPairingMaxQrUuidChars) {
|
||||
return ErrorCode::ApiConfigEmptyError;
|
||||
}
|
||||
if (vpnConfig.size() > kPairingMaxVpnConfigChars) {
|
||||
return ErrorCode::ApiPairingPayloadTooLargeError;
|
||||
}
|
||||
if (apiKey.size() > kPairingMaxApiKeyChars) {
|
||||
return ErrorCode::ApiPairingPayloadTooLargeError;
|
||||
}
|
||||
const QString st = serviceType.trimmed();
|
||||
const QString cc = userCountryCode.trimmed();
|
||||
if (st.isEmpty() || cc.isEmpty()) {
|
||||
return ErrorCode::ApiPairingMissingMetadataError;
|
||||
}
|
||||
if (st.size() > kPairingMaxServiceTypeChars || cc.size() > kPairingMaxUserCountryCodeChars) {
|
||||
return ErrorCode::ApiPairingPayloadTooLargeError;
|
||||
}
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
PairingController::PairingController(SecureAppSettingsRepository *appSettingsRepository)
|
||||
: m_appSettingsRepository(appSettingsRepository)
|
||||
{
|
||||
}
|
||||
|
||||
int PairingController::pairingLongPollTimeoutMsecs() const
|
||||
{
|
||||
return 60 * 1000;
|
||||
}
|
||||
|
||||
QJsonObject PairingController::buildGenerateQrPayload(const QString &qrUuid) const
|
||||
{
|
||||
QJsonObject o;
|
||||
o[apiDefs::key::qrUuid] = qrUuid;
|
||||
o[apiDefs::key::installationUuid] = m_appSettingsRepository->getInstallationUuid(true);
|
||||
o[apiDefs::key::appVersion] = QString(APP_VERSION);
|
||||
o[apiDefs::key::osVersion] = QSysInfo::productType();
|
||||
return o;
|
||||
}
|
||||
|
||||
QJsonObject PairingController::buildScanQrPayload(const QString &qrUuid, const QString &vpnConfig, const QJsonObject &serviceInfo,
|
||||
const QJsonArray &supportedProtocols, const QString &apiKey,
|
||||
const QString &serviceType, const QString &userCountryCode) const
|
||||
{
|
||||
QJsonObject auth;
|
||||
auth[apiDefs::key::apiKey] = apiKey;
|
||||
|
||||
QJsonObject o;
|
||||
o[apiDefs::key::qrUuid] = qrUuid;
|
||||
o[apiDefs::key::config] = vpnConfig;
|
||||
o[apiDefs::key::serviceInfo] = serviceInfo;
|
||||
o[apiDefs::key::supportedProtocols] = supportedProtocols;
|
||||
o[apiDefs::key::authData] = auth;
|
||||
o[apiDefs::key::installationUuid] = m_appSettingsRepository->getInstallationUuid(true);
|
||||
o[apiDefs::key::appVersion] = QString(APP_VERSION);
|
||||
o[apiDefs::key::osVersion] = QSysInfo::productType();
|
||||
o[apiDefs::key::serviceType] = serviceType.trimmed();
|
||||
o[apiDefs::key::userCountryCode] = userCountryCode.trimmed();
|
||||
return o;
|
||||
}
|
||||
41
client/core/controllers/api/pairingController.h
Normal file
41
client/core/controllers/api/pairingController.h
Normal file
@@ -0,0 +1,41 @@
|
||||
#ifndef PAIRINGCONTROLLER_H
|
||||
#define PAIRINGCONTROLLER_H
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
|
||||
#include "core/utils/errorCodes.h"
|
||||
|
||||
class SecureAppSettingsRepository;
|
||||
|
||||
class PairingController
|
||||
{
|
||||
public:
|
||||
struct QrPairingConfigPayload
|
||||
{
|
||||
QString config;
|
||||
QJsonObject serviceInfo;
|
||||
QJsonArray supportedProtocols;
|
||||
};
|
||||
|
||||
explicit PairingController(SecureAppSettingsRepository *appSettingsRepository);
|
||||
|
||||
int pairingLongPollTimeoutMsecs() const;
|
||||
|
||||
QJsonObject buildGenerateQrPayload(const QString &qrUuid) const;
|
||||
QJsonObject buildScanQrPayload(const QString &qrUuid, const QString &vpnConfig, const QJsonObject &serviceInfo,
|
||||
const QJsonArray &supportedProtocols, const QString &apiKey, const QString &serviceType,
|
||||
const QString &userCountryCode) const;
|
||||
|
||||
static amnezia::ErrorCode parseGenerateQrResponseBody(const QByteArray &responseBody, QrPairingConfigPayload &outPayload);
|
||||
static amnezia::ErrorCode parseScanQrResponseBody(const QByteArray &responseBody, QString *outOptionalDisplayName = nullptr);
|
||||
|
||||
static amnezia::ErrorCode validatePairingScanFields(const QString &qrUuid, const QString &vpnConfig, const QString &apiKey,
|
||||
const QString &serviceType, const QString &userCountryCode);
|
||||
|
||||
private:
|
||||
SecureAppSettingsRepository *m_appSettingsRepository;
|
||||
};
|
||||
|
||||
#endif // PAIRINGCONTROLLER_H
|
||||
@@ -312,6 +312,71 @@ ErrorCode SubscriptionController::importTrialFromGateway(const QString &userCoun
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -934,7 +999,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);
|
||||
auto postFuture = gatewayController->postAsync(QString("%1v1/renewal_link"), apiPayload, nullptr, gatewayController);
|
||||
auto *watcher = new QFutureWatcher<QPair<ErrorCode, QByteArray>>();
|
||||
QObject::connect(watcher, &QFutureWatcher<QPair<ErrorCode, QByteArray>>::finished,
|
||||
[promise, watcher, gatewayController]() {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef SUBSCRIPTIONCONTROLLER_H
|
||||
#define SUBSCRIPTIONCONTROLLER_H
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QByteArray>
|
||||
#include <QFuture>
|
||||
@@ -53,6 +54,9 @@ public:
|
||||
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,
|
||||
|
||||
@@ -86,6 +86,9 @@ void CoreController::initModels()
|
||||
m_xrayConfigModel = new XrayConfigModel(this);
|
||||
setQmlContextProperty("XrayConfigModel", m_xrayConfigModel);
|
||||
|
||||
m_xrayConfigSnapshotsModel = new XrayConfigSnapshotsModel(m_appSettingsRepository, m_xrayConfigModel, this);
|
||||
setQmlContextProperty("XrayConfigSnapshotsModel", m_xrayConfigSnapshotsModel);
|
||||
|
||||
m_torConfigModel = new TorConfigModel(this);
|
||||
setQmlContextProperty("TorConfigModel", m_torConfigModel);
|
||||
|
||||
@@ -100,6 +103,12 @@ void CoreController::initModels()
|
||||
m_socks5ConfigModel = new Socks5ProxyConfigModel(this);
|
||||
setQmlContextProperty("Socks5ProxyConfigModel", m_socks5ConfigModel);
|
||||
|
||||
m_mtProxyConfigModel = new MtProxyConfigModel(this);
|
||||
setQmlContextProperty("MtProxyConfigModel", m_mtProxyConfigModel);
|
||||
|
||||
m_telemtConfigModel = new TelemtConfigModel(this);
|
||||
setQmlContextProperty("TelemtConfigModel", m_telemtConfigModel);
|
||||
|
||||
m_clientManagementModel = new ClientManagementModel(this);
|
||||
setQmlContextProperty("ClientManagementModel", m_clientManagementModel);
|
||||
|
||||
@@ -144,6 +153,7 @@ 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);
|
||||
|
||||
@@ -169,7 +179,7 @@ void CoreController::initControllers()
|
||||
#ifdef Q_OS_WINDOWS
|
||||
m_ikev2ConfigModel,
|
||||
#endif
|
||||
m_sftpConfigModel, m_socks5ConfigModel, this);
|
||||
m_sftpConfigModel, m_socks5ConfigModel, m_mtProxyConfigModel, m_telemtConfigModel, this);
|
||||
setQmlContextProperty("InstallController", m_installUiController);
|
||||
|
||||
m_importController = new ImportUiController(m_importCoreController, this);
|
||||
@@ -202,6 +212,10 @@ void CoreController::initControllers()
|
||||
m_systemController = new SystemController(this);
|
||||
setQmlContextProperty("SystemController", m_systemController);
|
||||
|
||||
m_networkReachabilityController = new NetworkReachabilityController(this);
|
||||
m_engine->rootContext()->setContextProperty("NetworkReachabilityController", m_networkReachabilityController);
|
||||
m_engine->rootContext()->setContextProperty("NetworkReachability", m_networkReachabilityController);
|
||||
|
||||
m_servicesCatalogUiController = new ServicesCatalogUiController(m_servicesCatalogController, m_apiServicesModel, this);
|
||||
setQmlContextProperty("ServicesCatalogUiController", m_servicesCatalogUiController);
|
||||
|
||||
@@ -210,6 +224,9 @@ void CoreController::initControllers()
|
||||
m_apiCountryModel, m_apiDevicesModel, m_settingsController, 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);
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
#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"
|
||||
@@ -28,6 +30,7 @@
|
||||
#include "ui/controllers/languageUiController.h"
|
||||
#include "ui/controllers/updateUiController.h"
|
||||
#include "ui/controllers/api/servicesCatalogUiController.h"
|
||||
#include "ui/controllers/networkReachabilityController.h"
|
||||
|
||||
#include "core/controllers/serversController.h"
|
||||
#include "core/controllers/selfhosted/usersController.h"
|
||||
@@ -64,11 +67,15 @@
|
||||
#include "ui/models/protocols/openvpnConfigModel.h"
|
||||
#include "ui/models/protocols/wireguardConfigModel.h"
|
||||
#include "ui/models/protocols/xrayConfigModel.h"
|
||||
#include "ui/models/protocols/xrayConfigSnapshotsModel.h"
|
||||
#include "ui/models/protocolsModel.h"
|
||||
#include "ui/models/services/torConfigModel.h"
|
||||
#include "ui/models/serversModel.h"
|
||||
#include "ui/models/services/sftpConfigModel.h"
|
||||
#include "ui/models/services/socks5ProxyConfigModel.h"
|
||||
#include "ui/models/services/mtProxyConfigModel.h"
|
||||
#include "ui/models/services/telemtConfigModel.h"
|
||||
|
||||
#include "ui/models/ipSplitTunnelingModel.h"
|
||||
#include "ui/models/newsModel.h"
|
||||
|
||||
@@ -156,12 +163,14 @@ private:
|
||||
ServersUiController* m_serversUiController;
|
||||
IpSplitTunnelingUiController* m_ipSplitTunnelingUiController;
|
||||
SystemController* m_systemController;
|
||||
NetworkReachabilityController* m_networkReachabilityController;
|
||||
AppSplitTunnelingUiController* m_appSplitTunnelingUiController;
|
||||
AllowedDnsUiController* m_allowedDnsUiController;
|
||||
LanguageUiController* m_languageUiController;
|
||||
UpdateUiController* m_updateUiController;
|
||||
|
||||
SubscriptionUiController* m_subscriptionUiController;
|
||||
PairingUiController* m_pairingUiController;
|
||||
ApiNewsUiController* m_apiNewsUiController;
|
||||
|
||||
ServicesCatalogUiController* m_servicesCatalogUiController;
|
||||
@@ -173,6 +182,7 @@ private:
|
||||
AllowedDnsController* m_allowedDnsController;
|
||||
ServicesCatalogController* m_servicesCatalogController;
|
||||
SubscriptionController* m_subscriptionController;
|
||||
PairingController* m_pairingController;
|
||||
NewsController* m_newsController;
|
||||
UpdateController* m_updateController;
|
||||
InstallController* m_installController;
|
||||
@@ -200,6 +210,7 @@ private:
|
||||
|
||||
OpenVpnConfigModel* m_openVpnConfigModel;
|
||||
XrayConfigModel* m_xrayConfigModel;
|
||||
XrayConfigSnapshotsModel* m_xrayConfigSnapshotsModel;
|
||||
TorConfigModel* m_torConfigModel;
|
||||
WireGuardConfigModel* m_wireGuardConfigModel;
|
||||
AwgConfigModel* m_awgConfigModel;
|
||||
@@ -208,6 +219,8 @@ private:
|
||||
#endif
|
||||
SftpConfigModel* m_sftpConfigModel;
|
||||
Socks5ProxyConfigModel* m_socks5ConfigModel;
|
||||
MtProxyConfigModel* m_mtProxyConfigModel;
|
||||
TelemtConfigModel* m_telemtConfigModel;
|
||||
|
||||
CoreSignalHandlers* m_signalHandlers;
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#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"
|
||||
@@ -98,6 +99,9 @@ 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));
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkReply>
|
||||
#include <QPromise>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
|
||||
#include "QBlockCipher.h"
|
||||
@@ -21,12 +22,25 @@
|
||||
#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.");
|
||||
@@ -42,12 +56,24 @@ namespace
|
||||
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(gatewayEndpoint),
|
||||
m_gatewayEndpoint(normalizedGatewayBase(gatewayEndpoint)),
|
||||
m_isDevEnvironment(isDevEnvironment),
|
||||
m_requestTimeoutMsecs(requestTimeoutMsecs),
|
||||
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled)
|
||||
@@ -135,6 +161,8 @@ 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;
|
||||
@@ -151,6 +179,29 @@ 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);
|
||||
@@ -165,7 +216,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; });
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
execNetworkWaitLoop(wait);
|
||||
|
||||
QByteArray encryptedResponseBody = reply->readAll();
|
||||
QString replyErrorString = reply->errorString();
|
||||
@@ -174,8 +225,18 @@ 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 =
|
||||
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
|
||||
resolveResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
|
||||
|
||||
if (sslErrors.isEmpty() && shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
|
||||
auto requestFunction = [&encRequestData, &encryptedResponseBody](const QString &url) {
|
||||
@@ -191,7 +252,7 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
|
||||
httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
|
||||
decryptionResult =
|
||||
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
|
||||
resolveResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
|
||||
|
||||
if (!sslErrors.isEmpty()
|
||||
|| shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
|
||||
@@ -221,11 +282,15 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString &endpoint, const QJsonObject apiPayload)
|
||||
QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString &endpoint, const QJsonObject &apiPayload,
|
||||
QNetworkReply **activeReplyOut,
|
||||
const QSharedPointer<GatewayController> &keepAlive)
|
||||
{
|
||||
auto promise = QSharedPointer<QPromise<QPair<ErrorCode, QByteArray>>>::create();
|
||||
promise->start();
|
||||
|
||||
const QSharedPointer<GatewayController> life = keepAlive;
|
||||
|
||||
EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload);
|
||||
if (encRequestData.errorCode != ErrorCode::NoError) {
|
||||
promise->addResult(qMakePair(encRequestData.errorCode, QByteArray()));
|
||||
@@ -234,12 +299,22 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
|
||||
}
|
||||
|
||||
QNetworkReply *reply = amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody);
|
||||
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, this, [promise, sslErrors, encRequestData, endpoint, apiPayload, reply, this]() mutable {
|
||||
connect(reply, &QNetworkReply::finished, reply, [promise, sslErrors, encRequestData, endpoint, apiPayload, reply, life]() mutable {
|
||||
if (!life) {
|
||||
promise->addResult(qMakePair(ErrorCode::ApiConfigDecryptionError, QByteArray()));
|
||||
promise->finish();
|
||||
return;
|
||||
}
|
||||
|
||||
GatewayController *const ctl = life.data();
|
||||
QByteArray encryptedResponseBody = reply->readAll();
|
||||
QString replyErrorString = reply->errorString();
|
||||
auto replyError = reply->error();
|
||||
@@ -247,8 +322,20 @@ 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 =
|
||||
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
|
||||
ctl->resolveResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
|
||||
|
||||
auto processResponse = [promise, encRequestData](const GatewayController::DecryptionResult &decryptionResult,
|
||||
const QList<QSslError> &sslErrors, QNetworkReply::NetworkError replyError,
|
||||
@@ -273,13 +360,13 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
|
||||
promise->finish();
|
||||
};
|
||||
|
||||
if (sslErrors->isEmpty() && shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
|
||||
if (sslErrors->isEmpty() && ctl->shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
|
||||
auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString("");
|
||||
auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString("");
|
||||
|
||||
QStringList primaryBaseUrls;
|
||||
QStringList fallbackBaseUrls;
|
||||
if (m_isDevEnvironment) {
|
||||
if (ctl->m_isDevEnvironment) {
|
||||
primaryBaseUrls = QString(DEV_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
} else {
|
||||
primaryBaseUrls = QString(PROD_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
@@ -306,19 +393,27 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
|
||||
appendStorageUrls(primaryBaseUrls, proxyStorageUrls);
|
||||
appendStorageUrls(fallbackBaseUrls, proxyStorageUrls);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
} else {
|
||||
processResponse(decryptionResult, *sslErrors, replyError, replyErrorString, httpStatusCode);
|
||||
@@ -381,7 +476,7 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
|
||||
|
||||
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
|
||||
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
execNetworkWaitLoop(wait);
|
||||
|
||||
if (reply->error() == QNetworkReply::NetworkError::NoError) {
|
||||
auto encryptedResponseBody = reply->readAll();
|
||||
@@ -434,6 +529,10 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
|
||||
bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody,
|
||||
bool isDecryptionSuccessful)
|
||||
{
|
||||
if (m_isDevEnvironment) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QByteArray &responseBody = decryptedResponseBody;
|
||||
|
||||
int apiHttpStatus = -1;
|
||||
@@ -514,7 +613,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; });
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
execNetworkWaitLoop(wait);
|
||||
|
||||
auto result = replyProcessingFunction(reply, sslErrors);
|
||||
reply->deleteLater();
|
||||
@@ -536,7 +635,7 @@ void GatewayController::bypassProxy(const QString &endpoint, const QString &serv
|
||||
|
||||
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
|
||||
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
execNetworkWaitLoop(wait);
|
||||
|
||||
if (reply->error() == QNetworkReply::NetworkError::NoError) {
|
||||
reply->deleteLater();
|
||||
@@ -565,9 +664,14 @@ void GatewayController::bypassProxy(const QString &endpoint, const QString &serv
|
||||
}
|
||||
}
|
||||
|
||||
void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
|
||||
std::function<void(const QStringList &)> onComplete)
|
||||
void GatewayController::getProxyUrlsAsync(const QSharedPointer<GatewayController> &life, const QStringList &proxyStorageUrls,
|
||||
const int currentProxyStorageIndex, const std::function<void(const QStringList &)> &onComplete)
|
||||
{
|
||||
if (!life) {
|
||||
onComplete({});
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentProxyStorageIndex >= proxyStorageUrls.size()) {
|
||||
onComplete({});
|
||||
return;
|
||||
@@ -580,17 +684,23 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co
|
||||
|
||||
QNetworkReply *reply = amnApp->networkManager()->get(request);
|
||||
|
||||
// connect(reply, &QNetworkReply::sslErrors, this, [state](const QList<QSslError> &e) { *(state->sslErrors) = e; });
|
||||
connect(reply, &QNetworkReply::finished, reply, [life, proxyStorageUrls, currentProxyStorageIndex, onComplete, reply]() {
|
||||
if (!life) {
|
||||
onComplete({});
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
GatewayController *const ctl = life.data();
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [this, proxyStorageUrls, currentProxyStorageIndex, onComplete, reply]() {
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
QByteArray encrypted = reply->readAll();
|
||||
reply->deleteLater();
|
||||
|
||||
QByteArray responseBody;
|
||||
try {
|
||||
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||
if (!m_isDevEnvironment) {
|
||||
QByteArray key = ctl->m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||
if (!ctl->m_isDevEnvironment) {
|
||||
QCryptographicHash hash(QCryptographicHash::Sha512);
|
||||
hash.addData(key);
|
||||
QByteArray h = hash.result().toHex();
|
||||
@@ -607,15 +717,21 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co
|
||||
} catch (...) {
|
||||
Utils::logException();
|
||||
qCritical() << "error decrypting payload";
|
||||
QMetaObject::invokeMethod(
|
||||
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection);
|
||||
QTimer::singleShot(0, ctl, [life, proxyStorageUrls, currentProxyStorageIndex, onComplete]() {
|
||||
if (life) {
|
||||
life->getProxyUrlsAsync(life, proxyStorageUrls, currentProxyStorageIndex + 1, onComplete);
|
||||
} else {
|
||||
onComplete({});
|
||||
}
|
||||
});
|
||||
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;
|
||||
@@ -630,16 +746,26 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co
|
||||
qDebug() << httpStatusCode;
|
||||
qDebug() << "go to the next storage endpoint";
|
||||
reply->deleteLater();
|
||||
QMetaObject::invokeMethod(
|
||||
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection);
|
||||
QTimer::singleShot(0, ctl, [life, proxyStorageUrls, currentProxyStorageIndex, onComplete]() {
|
||||
if (life) {
|
||||
life->getProxyUrlsAsync(life, proxyStorageUrls, currentProxyStorageIndex + 1, onComplete);
|
||||
} else {
|
||||
onComplete({});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void GatewayController::getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex,
|
||||
std::function<void(const QString &)> onComplete)
|
||||
void GatewayController::getProxyUrlAsync(const QSharedPointer<GatewayController> &life, const QStringList &proxyUrls,
|
||||
const int currentProxyIndex, const std::function<void(const QString &)> &onComplete)
|
||||
{
|
||||
if (!life) {
|
||||
onComplete(QString());
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentProxyIndex >= proxyUrls.size()) {
|
||||
onComplete("");
|
||||
onComplete(QString());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -650,13 +776,16 @@ void GatewayController::getProxyUrlAsync(const QStringList proxyUrls, const int
|
||||
|
||||
QNetworkReply *reply = amnApp->networkManager()->get(request);
|
||||
|
||||
// connect(reply, &QNetworkReply::sslErrors, this, [state](const QList<QSslError> &e) {
|
||||
// *(state->sslErrors) = e;
|
||||
// });
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [this, proxyUrls, currentProxyIndex, onComplete, reply]() {
|
||||
connect(reply, &QNetworkReply::finished, reply, [life, 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);
|
||||
@@ -664,15 +793,28 @@ void GatewayController::getProxyUrlAsync(const QStringList proxyUrls, const int
|
||||
}
|
||||
|
||||
qDebug() << "go to the next proxy endpoint";
|
||||
QMetaObject::invokeMethod(this, [=]() { getProxyUrlAsync(proxyUrls, currentProxyIndex + 1, onComplete); }, Qt::QueuedConnection);
|
||||
QTimer::singleShot(0, ctl, [life, proxyUrls, currentProxyIndex, onComplete]() {
|
||||
if (life) {
|
||||
life->getProxyUrlAsync(life, proxyUrls, currentProxyIndex + 1, onComplete);
|
||||
} else {
|
||||
onComplete(QString());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void GatewayController::bypassProxyAsync(
|
||||
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
|
||||
std::function<void(const QByteArray &, bool, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> onComplete)
|
||||
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)
|
||||
{
|
||||
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;
|
||||
@@ -683,9 +825,9 @@ void GatewayController::bypassProxyAsync(
|
||||
|
||||
QNetworkReply *reply = amnApp->networkManager()->post(request, encRequestData.requestBody);
|
||||
|
||||
connect(reply, &QNetworkReply::sslErrors, this, [sslErrors](const QList<QSslError> &errors) { *sslErrors = errors; });
|
||||
connect(reply, &QNetworkReply::sslErrors, reply, [sslErrors](const QList<QSslError> &errors) { *sslErrors = errors; });
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [sslErrors, onComplete, encRequestData, reply, this]() {
|
||||
connect(reply, &QNetworkReply::finished, reply, [life, sslErrors, onComplete, encRequestData, reply]() {
|
||||
QByteArray encryptedResponseBody = reply->readAll();
|
||||
QString replyErrorString = reply->errorString();
|
||||
auto replyError = reply->error();
|
||||
@@ -693,8 +835,13 @@ void GatewayController::bypassProxyAsync(
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
auto decryptionResult =
|
||||
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
|
||||
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);
|
||||
|
||||
onComplete(decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful, *sslErrors, replyError, replyErrorString,
|
||||
httpStatusCode);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#ifndef GATEWAYCONTROLLER_H
|
||||
#define GATEWAYCONTROLLER_H
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include <QFuture>
|
||||
#include <QNetworkReply>
|
||||
#include <QObject>
|
||||
@@ -25,7 +27,9 @@ 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);
|
||||
QFuture<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject &apiPayload,
|
||||
QNetworkReply **activeReplyOut = nullptr,
|
||||
const QSharedPointer<GatewayController> &keepAlive = {});
|
||||
|
||||
private:
|
||||
struct EncryptedRequestData
|
||||
@@ -36,6 +40,7 @@ private:
|
||||
QByteArray iv;
|
||||
QByteArray salt;
|
||||
amnezia::ErrorCode errorCode;
|
||||
bool isPlaintextLocalGateway = false;
|
||||
};
|
||||
|
||||
struct DecryptionResult
|
||||
@@ -47,6 +52,8 @@ 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);
|
||||
@@ -54,12 +61,13 @@ private:
|
||||
std::function<QNetworkReply *(const QString &url)> requestFunction,
|
||||
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction);
|
||||
|
||||
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 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 bypassProxyAsync(
|
||||
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
|
||||
std::function<void(const QByteArray &, bool, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> onComplete);
|
||||
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);
|
||||
|
||||
int m_requestTimeoutMsecs;
|
||||
QString m_gatewayEndpoint;
|
||||
|
||||
@@ -323,6 +323,18 @@ ExportController::ExportResult ExportController::generateXrayConfig(const QStrin
|
||||
vlessServer.shortId = realitySettings.value(amnezia::protocols::xray::shortId).toString();
|
||||
vlessServer.fingerprint = realitySettings.value(amnezia::protocols::xray::fingerprint).toString("chrome");
|
||||
vlessServer.spiderX = realitySettings.value(amnezia::protocols::xray::spiderX).toString("");
|
||||
} else if (vlessServer.security == "tls") {
|
||||
QJsonObject tlsSettings = streamSettings.value("tlsSettings").toObject();
|
||||
vlessServer.serverName = tlsSettings.value(amnezia::protocols::xray::serverName).toString();
|
||||
vlessServer.fingerprint = tlsSettings.value(amnezia::protocols::xray::fingerprint).toString();
|
||||
// alpn: serialize array back to comma-separated for VLESS URI
|
||||
QJsonArray alpnArr = tlsSettings.value("alpn").toArray();
|
||||
QStringList alpnList;
|
||||
for (const QJsonValue &v : alpnArr) {
|
||||
alpnList << v.toString();
|
||||
}
|
||||
// alpn goes into vless URI query param — handled by Serialize via serverName/alpn fields
|
||||
// VlessServerObject doesn't have alpn field, so we embed in serverName if needed
|
||||
}
|
||||
|
||||
result.nativeConfigString = amnezia::serialization::vless::Serialize(vlessServer, "AmneziaVPN");
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
#include "core/installers/openvpnInstaller.h"
|
||||
#include "core/installers/sftpInstaller.h"
|
||||
#include "core/installers/socks5Installer.h"
|
||||
#include "core/installers/mtProxyInstaller.h"
|
||||
#include "core/installers/telemtInstaller.h"
|
||||
#include "core/installers/torInstaller.h"
|
||||
#include "core/installers/wireguardInstaller.h"
|
||||
#include "core/installers/xrayInstaller.h"
|
||||
@@ -34,6 +36,7 @@
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include "core/models/containerConfig.h"
|
||||
#include "core/models/protocols/mtProxyProtocolConfig.h"
|
||||
#include "core/models/protocols/awgProtocolConfig.h"
|
||||
#include "ui/models/protocols/wireguardConfigModel.h"
|
||||
#include "core/utils/utilities.h"
|
||||
@@ -53,6 +56,21 @@ using namespace ProtocolUtils;
|
||||
namespace
|
||||
{
|
||||
Logger logger("InstallController");
|
||||
|
||||
bool dockerDaemonContainerMissing(const QString &out, const QString &containerDockerName)
|
||||
{
|
||||
if (!out.contains(QLatin1String("Error response from daemon"), Qt::CaseInsensitive)) {
|
||||
return false;
|
||||
}
|
||||
if (out.contains(QLatin1String("No such container"), Qt::CaseInsensitive)
|
||||
&& out.contains(containerDockerName, Qt::CaseInsensitive)) {
|
||||
return true;
|
||||
}
|
||||
if (out.size() < 700 && out.contains(QLatin1String("is not running"), Qt::CaseInsensitive)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
InstallController::InstallController(SecureServersRepository *serversRepository,
|
||||
@@ -136,6 +154,15 @@ ErrorCode InstallController::updateContainer(const QString &serverId, DockerCont
|
||||
if (!adminConfig.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
if (container == DockerContainer::MtProxy) {
|
||||
ServerCredentials credentials = adminConfig->credentials();
|
||||
SshSession sshSession(this);
|
||||
MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
|
||||
} else if (container == DockerContainer::Telemt) {
|
||||
ServerCredentials credentials = adminConfig->credentials();
|
||||
SshSession sshSession(this);
|
||||
TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
|
||||
}
|
||||
adminConfig->updateContainerConfig(container, newConfig);
|
||||
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
|
||||
return ErrorCode::NoError;
|
||||
@@ -165,6 +192,11 @@ ErrorCode InstallController::updateContainer(const QString &serverId, DockerCont
|
||||
}
|
||||
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
if (container == DockerContainer::MtProxy) {
|
||||
MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
|
||||
} else if (container == DockerContainer::Telemt) {
|
||||
TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
|
||||
}
|
||||
clearCachedProfile(serverId, container);
|
||||
adminConfig->updateContainerConfig(container, newConfig);
|
||||
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
|
||||
@@ -408,9 +440,24 @@ ErrorCode InstallController::configureContainerWorker(const ServerCredentials &c
|
||||
sshSession.replaceVars(amnezia::scriptData(ProtocolScriptType::configure_container, container), baseVars),
|
||||
cbReadStdOut, cbReadStdErr);
|
||||
|
||||
if (e != ErrorCode::NoError) {
|
||||
return e;
|
||||
}
|
||||
|
||||
if (dockerDaemonContainerMissing(stdOut, ContainerUtils::containerToString(container))) {
|
||||
qDebug() << "configureContainerWorker: Docker daemon reports container missing/stopped, output:" << stdOut;
|
||||
return ErrorCode::ServerContainerMissingError;
|
||||
}
|
||||
|
||||
updateContainerConfigAfterInstallation(container, config, stdOut);
|
||||
|
||||
return e;
|
||||
if (container == DockerContainer::MtProxy) {
|
||||
MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, config);
|
||||
} else if (container == DockerContainer::Telemt) {
|
||||
TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, config);
|
||||
}
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode InstallController::startupContainerWorker(const ServerCredentials &credentials, DockerContainer container, const ContainerConfig &config, SshSession &sshSession)
|
||||
@@ -563,6 +610,79 @@ bool InstallController::isReinstallContainerRequired(DockerContainer container,
|
||||
}
|
||||
}
|
||||
|
||||
if (container == DockerContainer::MtProxy) {
|
||||
const auto *oldMt = oldConfig.getMtProxyProtocolConfig();
|
||||
const auto *newMt = newConfig.getMtProxyProtocolConfig();
|
||||
if (oldMt && newMt) {
|
||||
const QString oldPort =
|
||||
oldMt->port.isEmpty() ? QString(protocols::mtProxy::defaultPort) : oldMt->port;
|
||||
const QString newPort =
|
||||
newMt->port.isEmpty() ? QString(protocols::mtProxy::defaultPort) : newMt->port;
|
||||
if (oldPort != newPort) {
|
||||
return true;
|
||||
}
|
||||
const QString oldTransport = oldMt->transportMode.isEmpty() ? QString(
|
||||
protocols::mtProxy::transportModeStandard)
|
||||
: oldMt->transportMode;
|
||||
const QString newTransport = newMt->transportMode.isEmpty() ? QString(
|
||||
protocols::mtProxy::transportModeStandard)
|
||||
: newMt->transportMode;
|
||||
if (oldTransport != newTransport) {
|
||||
return true;
|
||||
}
|
||||
if (oldMt->tlsDomain != newMt->tlsDomain) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (container == DockerContainer::Telemt) {
|
||||
const auto *oldT = oldConfig.getTelemtProtocolConfig();
|
||||
const auto *newT = newConfig.getTelemtProtocolConfig();
|
||||
if (oldT && newT) {
|
||||
const QString oldPort =
|
||||
oldT->port.isEmpty() ? QString(protocols::telemt::defaultPort) : oldT->port;
|
||||
const QString newPort =
|
||||
newT->port.isEmpty() ? QString(protocols::telemt::defaultPort) : newT->port;
|
||||
if (oldPort != newPort) {
|
||||
return true;
|
||||
}
|
||||
const QString oldTransport = oldT->transportMode.isEmpty()
|
||||
? QString(protocols::telemt::transportModeStandard)
|
||||
: oldT->transportMode;
|
||||
const QString newTransport = newT->transportMode.isEmpty()
|
||||
? QString(protocols::telemt::transportModeStandard)
|
||||
: newT->transportMode;
|
||||
if (oldTransport != newTransport) {
|
||||
return true;
|
||||
}
|
||||
if (oldT->tlsDomain != newT->tlsDomain) {
|
||||
return true;
|
||||
}
|
||||
if (oldT->maskEnabled != newT->maskEnabled) {
|
||||
return true;
|
||||
}
|
||||
if (oldT->tlsEmulation != newT->tlsEmulation) {
|
||||
return true;
|
||||
}
|
||||
if (oldT->useMiddleProxy != newT->useMiddleProxy) {
|
||||
return true;
|
||||
}
|
||||
if (oldT->tag != newT->tag) {
|
||||
return true;
|
||||
}
|
||||
const QString oldUser = oldT->userName.isEmpty()
|
||||
? QString::fromUtf8(protocols::telemt::defaultUserName)
|
||||
: oldT->userName;
|
||||
const QString newUser = newT->userName.isEmpty()
|
||||
? QString::fromUtf8(protocols::telemt::defaultUserName)
|
||||
: newT->userName;
|
||||
if (oldUser != newUser) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (container == DockerContainer::Socks5Proxy) {
|
||||
return true;
|
||||
}
|
||||
@@ -654,7 +774,7 @@ ErrorCode InstallController::isUserInSudo(const ServerCredentials &credentials,
|
||||
return ErrorCode::ServerUserDirectoryNotAccessible;
|
||||
if (stdOut.contains("sudoers") || stdOut.contains("is not allowed to run sudo on"))
|
||||
return ErrorCode::ServerUserNotAllowedInSudoers;
|
||||
if (stdOut.contains("password is required"))
|
||||
if (stdOut.contains("password is required") || stdOut.contains("authentication is required"))
|
||||
return ErrorCode::ServerUserPasswordRequired;
|
||||
|
||||
return error;
|
||||
@@ -823,6 +943,8 @@ QScopedPointer<InstallerBase> InstallController::createInstaller(DockerContainer
|
||||
case DockerContainer::TorWebSite: return QScopedPointer<InstallerBase>(new TorInstaller(this));
|
||||
case DockerContainer::Sftp: return QScopedPointer<InstallerBase>(new SftpInstaller(this));
|
||||
case DockerContainer::Socks5Proxy: return QScopedPointer<InstallerBase>(new Socks5Installer(this));
|
||||
case DockerContainer::MtProxy: return QScopedPointer<InstallerBase>(new MtProxyInstaller(this));
|
||||
case DockerContainer::Telemt: return QScopedPointer<InstallerBase>(new TelemtInstaller(this));
|
||||
default: return QScopedPointer<InstallerBase>(new InstallerBase(this));
|
||||
}
|
||||
}
|
||||
@@ -861,6 +983,20 @@ bool InstallController::isUpdateDockerContainerRequired(DockerContainer containe
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else if (container == DockerContainer::MtProxy) {
|
||||
const auto *oldMt = oldConfig.getMtProxyProtocolConfig();
|
||||
const auto *newMt = newConfig.getMtProxyProtocolConfig();
|
||||
if (!oldMt || !newMt) {
|
||||
return true;
|
||||
}
|
||||
return !oldMt->equalsDockerDeploymentSettings(*newMt);
|
||||
} else if (container == DockerContainer::Telemt) {
|
||||
const auto *oldT = oldConfig.getTelemtProtocolConfig();
|
||||
const auto *newT = newConfig.getTelemtProtocolConfig();
|
||||
if (!oldT || !newT) {
|
||||
return true;
|
||||
}
|
||||
return !oldT->equalsDockerDeploymentSettings(*newT);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -1164,6 +1300,56 @@ void InstallController::updateContainerConfigAfterInstallation(DockerContainer c
|
||||
onion.replace("\n", "");
|
||||
torProtocolConfig->serverConfig.site = onion;
|
||||
}
|
||||
} else if (container == DockerContainer::MtProxy) {
|
||||
if (auto* mtProxyConfig = containerConfig.getMtProxyProtocolConfig()) {
|
||||
qDebug() << "amnezia mtproxy" << stdOut;
|
||||
|
||||
static const QRegularExpression reSecret(
|
||||
QStringLiteral(R"(\[\*\]\s+Secret:\s+([0-9a-fA-F]{32}))"),
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
static const QRegularExpression reTgLink(QStringLiteral(R"(\[\*\]\s+tg://\s+link:\s+(tg://proxy\?[^\s]+))"));
|
||||
static const QRegularExpression reTmeLink(
|
||||
QStringLiteral(R"(\[\*\]\s+t\.me\s+link:\s+(https://t\.me/proxy\?[^\s]+))"));
|
||||
|
||||
const QRegularExpressionMatch mSecret = reSecret.match(stdOut);
|
||||
const QRegularExpressionMatch mTgLink = reTgLink.match(stdOut);
|
||||
const QRegularExpressionMatch mTmeLink = reTmeLink.match(stdOut);
|
||||
|
||||
if (mSecret.hasMatch()) {
|
||||
mtProxyConfig->secret = mSecret.captured(1);
|
||||
}
|
||||
if (mTgLink.hasMatch()) {
|
||||
mtProxyConfig->tgLink = mTgLink.captured(1);
|
||||
}
|
||||
if (mTmeLink.hasMatch()) {
|
||||
mtProxyConfig->tmeLink = mTmeLink.captured(1);
|
||||
}
|
||||
}
|
||||
} else if (container == DockerContainer::Telemt) {
|
||||
if (auto *telemtConfig = containerConfig.getTelemtProtocolConfig()) {
|
||||
qDebug() << "amnezia-telemt configure stdout" << stdOut;
|
||||
|
||||
static const QRegularExpression reSecret(
|
||||
QStringLiteral(R"(\[\*\]\s+Secret:\s+([0-9a-fA-F]{32}))"),
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
static const QRegularExpression reTgLink(QStringLiteral(R"(\[\*\]\s+tg://\s+link:\s+(tg://proxy\?[^\s]+))"));
|
||||
static const QRegularExpression reTmeLink(
|
||||
QStringLiteral(R"(\[\*\]\s+t\.me\s+link:\s+(https://t\.me/proxy\?[^\s]+))"));
|
||||
|
||||
const QRegularExpressionMatch mSecret = reSecret.match(stdOut);
|
||||
const QRegularExpressionMatch mTgLink = reTgLink.match(stdOut);
|
||||
const QRegularExpressionMatch mTmeLink = reTmeLink.match(stdOut);
|
||||
|
||||
if (mSecret.hasMatch()) {
|
||||
telemtConfig->secret = mSecret.captured(1);
|
||||
}
|
||||
if (mTgLink.hasMatch()) {
|
||||
telemtConfig->tgLink = mTgLink.captured(1);
|
||||
}
|
||||
if (mTmeLink.hasMatch()) {
|
||||
telemtConfig->tmeLink = mTmeLink.captured(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1248,3 +1434,126 @@ ErrorCode InstallController::getAlreadyInstalledContainers(const ServerCredentia
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode InstallController::setDockerContainerEnabledState(const QString &serverId, DockerContainer container, bool enabled)
|
||||
{
|
||||
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) {
|
||||
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;
|
||||
}
|
||||
const QString containerName = ContainerUtils::containerToString(container);
|
||||
SshSession sshSession(this);
|
||||
const QString script = enabled ? QStringLiteral("sudo docker start %1").arg(containerName)
|
||||
: QStringLiteral("sudo docker stop %1").arg(containerName);
|
||||
const ErrorCode runError = sshSession.runScript(credentials, script);
|
||||
if (runError != ErrorCode::NoError) {
|
||||
return runError;
|
||||
}
|
||||
ContainerConfig currentConfig = adminConfig->containerConfig(container);
|
||||
bool persist = false;
|
||||
if (auto *mtConfig = currentConfig.getMtProxyProtocolConfig()) {
|
||||
mtConfig->isEnabled = enabled;
|
||||
persist = true;
|
||||
} else if (auto *telemtConfig = currentConfig.getTelemtProtocolConfig()) {
|
||||
telemtConfig->isEnabled = enabled;
|
||||
persist = true;
|
||||
}
|
||||
if (persist) {
|
||||
adminConfig->updateContainerConfig(container, currentConfig);
|
||||
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
|
||||
}
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode InstallController::queryDockerContainerStatus(const QString &serverId, DockerContainer container, int &statusOut)
|
||||
{
|
||||
statusOut = 3;
|
||||
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
if (!adminConfig.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
ServerCredentials credentials = adminConfig->credentials();
|
||||
if (!credentials.isValid()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
const QString containerName = ContainerUtils::containerToString(container);
|
||||
QString stdOut;
|
||||
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
|
||||
stdOut += data;
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
SshSession sshSession(this);
|
||||
const QString script = QStringLiteral(
|
||||
"sudo docker inspect --format '{{.State.Status}}' %1 2>/dev/null || echo 'not_found'")
|
||||
.arg(containerName);
|
||||
const ErrorCode errorCode = sshSession.runScript(credentials, script, cbReadStdOut);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
const QString status = stdOut.trimmed();
|
||||
if (status == QLatin1String("running")) {
|
||||
statusOut = 1;
|
||||
} else if (status == QLatin1String("not_found") || status.isEmpty()) {
|
||||
statusOut = 0;
|
||||
} else if (status == QLatin1String("exited") || status == QLatin1String("created")
|
||||
|| status == QLatin1String("paused")) {
|
||||
statusOut = 2;
|
||||
} else {
|
||||
statusOut = 3;
|
||||
}
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode InstallController::queryMtProxyDiagnostics(const QString &serverId, DockerContainer container, int listenPort,
|
||||
MtProxyContainerDiagnostics &out)
|
||||
{
|
||||
out = {};
|
||||
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(this);
|
||||
return MtProxyInstaller::queryDiagnostics(sshSession, credentials, container, listenPort, out);
|
||||
}
|
||||
|
||||
QString InstallController::fetchDockerContainerSecret(const QString &serverId, DockerContainer container)
|
||||
{
|
||||
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) {
|
||||
return {};
|
||||
}
|
||||
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
if (!adminConfig.has_value()) {
|
||||
return {};
|
||||
}
|
||||
ServerCredentials credentials = adminConfig->credentials();
|
||||
if (!credentials.isValid()) {
|
||||
return {};
|
||||
}
|
||||
const QString containerName = ContainerUtils::containerToString(container);
|
||||
QString stdOut;
|
||||
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
|
||||
stdOut += data;
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
SshSession sshSession(this);
|
||||
const QString path = QStringLiteral("/data/secret");
|
||||
const QString cmd = QStringLiteral("sudo docker exec %1 cat %2").arg(containerName, path);
|
||||
const ErrorCode errorCode = sshSession.runScript(credentials, cmd, cbReadStdOut);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return {};
|
||||
}
|
||||
const QString secret = stdOut.trimmed();
|
||||
static const QRegularExpression hex32(QStringLiteral("^[0-9a-fA-F]{32}$"));
|
||||
return hex32.match(secret).hasMatch() ? secret : QString();
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "core/models/containerConfig.h"
|
||||
#include "core/repositories/secureServersRepository.h"
|
||||
#include "core/repositories/secureAppSettingsRepository.h"
|
||||
#include "core/installers/mtProxyInstaller.h"
|
||||
|
||||
class SshSession;
|
||||
class InstallerBase;
|
||||
@@ -39,6 +40,16 @@ public:
|
||||
ErrorCode removeAllContainers(const QString &serverId);
|
||||
ErrorCode removeContainer(const QString &serverId, DockerContainer container);
|
||||
|
||||
ErrorCode setDockerContainerEnabledState(const QString &serverId, DockerContainer container, bool enabled);
|
||||
|
||||
/// statusOut: 0 = not deployed, 1 = running, 2 = stopped, 3 = error
|
||||
ErrorCode queryDockerContainerStatus(const QString &serverId, DockerContainer container, int &statusOut);
|
||||
|
||||
ErrorCode queryMtProxyDiagnostics(const QString &serverId, DockerContainer container, int listenPort,
|
||||
MtProxyContainerDiagnostics &out);
|
||||
|
||||
QString fetchDockerContainerSecret(const QString &serverId, DockerContainer container);
|
||||
|
||||
ContainerConfig generateConfig(DockerContainer container, int port, TransportProto transportProto);
|
||||
ErrorCode getAlreadyInstalledContainers(const ServerCredentials &credentials, QMap<DockerContainer, ContainerConfig> &installedContainers, SshSession &sshSession);
|
||||
|
||||
|
||||
@@ -57,6 +57,10 @@ void UpdateController::checkForUpdates()
|
||||
if (m_updateCheckRunning || !m_appSettingsRepository) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_appSettingsRepository->isDevGatewayEnv()) {
|
||||
return;
|
||||
}
|
||||
m_updateCheckRunning = true;
|
||||
|
||||
fetchGatewayUrl();
|
||||
@@ -93,6 +97,11 @@ 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,
|
||||
@@ -105,11 +114,19 @@ 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]() {
|
||||
gatewayController->postAsync(QStringLiteral("%1v1/updater_endpoint"), 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) {
|
||||
auto [err, gatewayResponse] = result;
|
||||
if (err != ErrorCode::NoError) {
|
||||
logger.error() << errorString(err);
|
||||
if (err == ErrorCode::ApiNotFoundError) {
|
||||
logger.debug() << "Update check: updater_endpoint not found on gateway";
|
||||
} else {
|
||||
logger.error() << errorString(err);
|
||||
}
|
||||
finishUpdateCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
16
client/core/diagnostics/containerDiagnostics.h
Normal file
16
client/core/diagnostics/containerDiagnostics.h
Normal file
@@ -0,0 +1,16 @@
|
||||
#ifndef CONTAINERDIAGNOSTICS_H
|
||||
#define CONTAINERDIAGNOSTICS_H
|
||||
|
||||
namespace amnezia
|
||||
{
|
||||
struct ContainerDiagnostics
|
||||
{
|
||||
bool available = false;
|
||||
bool portReachable = false;
|
||||
|
||||
virtual ~ContainerDiagnostics() = default;
|
||||
};
|
||||
|
||||
} // namespace amnezia
|
||||
|
||||
#endif // CONTAINERDIAGNOSTICS_H
|
||||
18
client/core/diagnostics/mtProxyDiagnostics.h
Normal file
18
client/core/diagnostics/mtProxyDiagnostics.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#ifndef MTPROXYDIAGNOSTICS_H
|
||||
#define MTPROXYDIAGNOSTICS_H
|
||||
|
||||
#include "containerDiagnostics.h"
|
||||
|
||||
#include <QString>
|
||||
|
||||
namespace amnezia {
|
||||
struct MtProxyDiagnostics : ContainerDiagnostics {
|
||||
bool upstreamReachable = false;
|
||||
int clientsConnected = -1;
|
||||
QString lastConfigRefresh;
|
||||
QString statsEndpoint;
|
||||
};
|
||||
|
||||
} // namespace amnezia
|
||||
|
||||
#endif // MTPROXYDIAGNOSTICS_H
|
||||
20
client/core/diagnostics/telemtDiagnostics.h
Normal file
20
client/core/diagnostics/telemtDiagnostics.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#ifndef TELEMTDIAGNOSTICS_H
|
||||
#define TELEMTDIAGNOSTICS_H
|
||||
|
||||
#include "containerDiagnostics.h"
|
||||
|
||||
#include <QString>
|
||||
|
||||
namespace amnezia
|
||||
{
|
||||
struct TelemtDiagnostics : ContainerDiagnostics
|
||||
{
|
||||
bool upstreamReachable = false;
|
||||
int clientsConnected = -1;
|
||||
QString lastConfigRefresh;
|
||||
QString statsEndpoint;
|
||||
};
|
||||
|
||||
} // namespace amnezia
|
||||
|
||||
#endif // TELEMTDIAGNOSTICS_H
|
||||
@@ -14,6 +14,8 @@
|
||||
#include "core/models/protocols/xrayProtocolConfig.h"
|
||||
#include "core/models/protocols/sftpProtocolConfig.h"
|
||||
#include "core/models/protocols/socks5ProxyProtocolConfig.h"
|
||||
#include "core/models/protocols/mtProxyProtocolConfig.h"
|
||||
#include "core/models/protocols/telemtProtocolConfig.h"
|
||||
#include "core/models/protocols/ikev2ProtocolConfig.h"
|
||||
#include "core/models/protocols/torProtocolConfig.h"
|
||||
|
||||
@@ -91,6 +93,18 @@ ContainerConfig InstallerBase::createBaseConfig(DockerContainer container, int p
|
||||
config.protocolConfig = socks5Config;
|
||||
break;
|
||||
}
|
||||
case Proto::MtProxy: {
|
||||
MtProxyProtocolConfig mtConfig;
|
||||
mtConfig.port = portStr;
|
||||
config.protocolConfig = mtConfig;
|
||||
break;
|
||||
}
|
||||
case Proto::Telemt: {
|
||||
TelemtProtocolConfig telemtConfig;
|
||||
telemtConfig.port = portStr;
|
||||
config.protocolConfig = telemtConfig;
|
||||
break;
|
||||
}
|
||||
case Proto::Ikev2: {
|
||||
Ikev2ProtocolConfig ikev2Config;
|
||||
config.protocolConfig = ikev2Config;
|
||||
|
||||
130
client/core/installers/mtProxyInstaller.cpp
Normal file
130
client/core/installers/mtProxyInstaller.cpp
Normal file
@@ -0,0 +1,130 @@
|
||||
#include "mtProxyInstaller.h"
|
||||
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/utils/selfhosted/sshSession.h"
|
||||
#include "core/models/containerConfig.h"
|
||||
#include "core/models/protocols/mtProxyProtocolConfig.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
#include <QRegularExpression>
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
namespace {
|
||||
constexpr QLatin1String kMtProxyClientJsonPath("/data/amnezia-mtproxy-client.json");
|
||||
constexpr QLatin1String kMtProxyClientJsonUploadPath("data/amnezia-mtproxy-client.json");
|
||||
constexpr QLatin1String kMtProxySecretPath("/data/secret");
|
||||
}
|
||||
|
||||
MtProxyInstaller::MtProxyInstaller(QObject *parent)
|
||||
: InstallerBase(parent) {
|
||||
}
|
||||
|
||||
ErrorCode MtProxyInstaller::extractConfigFromContainer(DockerContainer container, const ServerCredentials &credentials,
|
||||
SshSession *sshSession, ContainerConfig &config) {
|
||||
if (container != DockerContainer::MtProxy || !sshSession) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
MtProxyProtocolConfig *mt = config.getMtProxyProtocolConfig();
|
||||
if (!mt) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode jsonErr = ErrorCode::NoError;
|
||||
const QByteArray jsonRaw =
|
||||
sshSession->getTextFileFromContainer(container, credentials, QString(kMtProxyClientJsonPath), jsonErr);
|
||||
if (jsonErr == ErrorCode::NoError && !jsonRaw.trimmed().isEmpty()) {
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(jsonRaw.trimmed(), &parseError);
|
||||
if (parseError.error == QJsonParseError::NoError && doc.isObject()) {
|
||||
QJsonObject merged = mt->toJson();
|
||||
const QJsonObject snap = doc.object();
|
||||
for (auto it = snap.constBegin(); it != snap.constEnd(); ++it) {
|
||||
merged.insert(it.key(), it.value());
|
||||
}
|
||||
*mt = MtProxyProtocolConfig::fromJson(merged);
|
||||
}
|
||||
}
|
||||
|
||||
ErrorCode secretErr = ErrorCode::NoError;
|
||||
const QByteArray secretRaw =
|
||||
sshSession->getTextFileFromContainer(container, credentials, QString(kMtProxySecretPath), secretErr);
|
||||
const QString sec = QString::fromUtf8(secretRaw).trimmed();
|
||||
if (sec.length() == 32) {
|
||||
static const QRegularExpression hex32(QStringLiteral("^[0-9a-fA-F]{32}$"));
|
||||
if (hex32.match(sec).hasMatch()) {
|
||||
mt->secret = sec;
|
||||
}
|
||||
}
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode MtProxyInstaller::queryDiagnostics(SshSession &sshSession, const ServerCredentials &credentials,
|
||||
DockerContainer container, int listenPort,
|
||||
MtProxyContainerDiagnostics &out)
|
||||
{
|
||||
out = {};
|
||||
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
const QString containerName = ContainerUtils::containerToString(container);
|
||||
const QString script =
|
||||
QStringLiteral(
|
||||
"PORT_OK=$(sudo docker exec %1 sh -c 'ss -tlnp 2>/dev/null | grep -q :%2 && echo yes || echo no' 2>/dev/null || echo no); "
|
||||
"TG_OK=$(curl -s --max-time 5 -o /dev/null -w '%%{http_code}' https://core.telegram.org/getProxySecret 2>/dev/null | grep -q '200' && echo yes || echo no); "
|
||||
"CLIENTS=$(sudo docker exec amnezia-mtproxy sh -c 'curl -s --max-time 3 http://localhost:2398/stats 2>/dev/null | grep -o \"total_special_connections:[0-9]*\" | cut -d: -f2' 2>/dev/null); "
|
||||
"CONF_TIME=$(sudo docker exec amnezia-mtproxy sh -c 'stat -c \"%%y\" /data/proxy-multi.conf 2>/dev/null | cut -d. -f1' 2>/dev/null || echo unknown); "
|
||||
"echo \"PORT_OK=${PORT_OK}\"; "
|
||||
"echo \"TG_OK=${TG_OK}\"; "
|
||||
"echo \"CLIENTS=${CLIENTS:-0}\"; "
|
||||
"echo \"CONF_TIME=${CONF_TIME}\"; "
|
||||
"echo \"STATS=http://localhost:2398/stats\";")
|
||||
.arg(containerName)
|
||||
.arg(listenPort);
|
||||
|
||||
QString stdOut;
|
||||
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
|
||||
stdOut += data;
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
const ErrorCode errorCode = sshSession.runScript(credentials, script, cbReadStdOut);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
for (const QString &line : stdOut.split('\n', Qt::SkipEmptyParts)) {
|
||||
if (line.startsWith(QLatin1String("PORT_OK="))) {
|
||||
out.portReachable = line.mid(8).trimmed() == QLatin1String("yes");
|
||||
} else if (line.startsWith(QLatin1String("TG_OK="))) {
|
||||
out.upstreamReachable = line.mid(6).trimmed() == QLatin1String("yes");
|
||||
} else if (line.startsWith(QLatin1String("CLIENTS="))) {
|
||||
out.clientsConnected = line.mid(8).trimmed().toInt();
|
||||
} else if (line.startsWith(QLatin1String("CONF_TIME="))) {
|
||||
out.lastConfigRefresh = line.mid(10).trimmed();
|
||||
} else if (line.startsWith(QLatin1String("STATS="))) {
|
||||
out.statsEndpoint = line.mid(6).trimmed();
|
||||
}
|
||||
}
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
void MtProxyInstaller::uploadClientSettingsSnapshot(SshSession &sshSession, const ServerCredentials &credentials,
|
||||
DockerContainer container, const ContainerConfig &config) {
|
||||
const MtProxyProtocolConfig *mt = config.getMtProxyProtocolConfig();
|
||||
if (!mt) {
|
||||
return;
|
||||
}
|
||||
const QByteArray payload = QJsonDocument(mt->toJson()).toJson(QJsonDocument::Compact);
|
||||
const ErrorCode err = sshSession.uploadTextFileToContainer(container, credentials, QString::fromUtf8(payload),
|
||||
QString(kMtProxyClientJsonUploadPath));
|
||||
if (err != ErrorCode::NoError) {
|
||||
qWarning() << "MtProxyInstaller::uploadClientSettingsSnapshot failed" << err;
|
||||
}
|
||||
}
|
||||
34
client/core/installers/mtProxyInstaller.h
Normal file
34
client/core/installers/mtProxyInstaller.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#ifndef MTPROXYINSTALLER_H
|
||||
#define MTPROXYINSTALLER_H
|
||||
|
||||
#include "installerBase.h"
|
||||
|
||||
#include <QString>
|
||||
|
||||
struct MtProxyContainerDiagnostics {
|
||||
bool portReachable = false;
|
||||
bool upstreamReachable = false;
|
||||
int clientsConnected = -1;
|
||||
QString lastConfigRefresh;
|
||||
QString statsEndpoint;
|
||||
};
|
||||
|
||||
class MtProxyInstaller : public InstallerBase {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit MtProxyInstaller(QObject *parent = nullptr);
|
||||
|
||||
amnezia::ErrorCode
|
||||
extractConfigFromContainer(amnezia::DockerContainer container, const amnezia::ServerCredentials &credentials,
|
||||
SshSession *sshSession, amnezia::ContainerConfig &config) override;
|
||||
|
||||
static void uploadClientSettingsSnapshot(SshSession &sshSession, const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container,
|
||||
const amnezia::ContainerConfig &config);
|
||||
|
||||
static amnezia::ErrorCode queryDiagnostics(SshSession &sshSession, const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container, int listenPort,
|
||||
MtProxyContainerDiagnostics &out);
|
||||
};
|
||||
|
||||
#endif // MTPROXYINSTALLER_H
|
||||
79
client/core/installers/telemtInstaller.cpp
Normal file
79
client/core/installers/telemtInstaller.cpp
Normal file
@@ -0,0 +1,79 @@
|
||||
#include "telemtInstaller.h"
|
||||
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "core/utils/selfhosted/sshSession.h"
|
||||
#include "core/models/containerConfig.h"
|
||||
#include "core/models/protocols/telemtProtocolConfig.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
#include <QRegularExpression>
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
namespace {
|
||||
constexpr QLatin1String kTelemtClientJsonPath("/data/amnezia-telemt-client.json");
|
||||
constexpr QLatin1String kTelemtClientJsonUploadPath("data/amnezia-telemt-client.json");
|
||||
constexpr QLatin1String kTelemtSecretPath("/data/secret");
|
||||
}
|
||||
|
||||
TelemtInstaller::TelemtInstaller(QObject *parent) : InstallerBase(parent) {}
|
||||
|
||||
ErrorCode TelemtInstaller::extractConfigFromContainer(DockerContainer container, const ServerCredentials &credentials,
|
||||
SshSession *sshSession, ContainerConfig &config) {
|
||||
if (container != DockerContainer::Telemt || !sshSession) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
TelemtProtocolConfig *tc = config.getTelemtProtocolConfig();
|
||||
if (!tc) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode jsonErr = ErrorCode::NoError;
|
||||
const QByteArray jsonRaw =
|
||||
sshSession->getTextFileFromContainer(container, credentials, QString(kTelemtClientJsonPath), jsonErr);
|
||||
if (jsonErr == ErrorCode::NoError && !jsonRaw.trimmed().isEmpty()) {
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(jsonRaw.trimmed(), &parseError);
|
||||
if (parseError.error == QJsonParseError::NoError && doc.isObject()) {
|
||||
QJsonObject merged = tc->toJson();
|
||||
const QJsonObject snap = doc.object();
|
||||
for (auto it = snap.constBegin(); it != snap.constEnd(); ++it) {
|
||||
merged.insert(it.key(), it.value());
|
||||
}
|
||||
*tc = TelemtProtocolConfig::fromJson(merged);
|
||||
}
|
||||
}
|
||||
|
||||
ErrorCode secretErr = ErrorCode::NoError;
|
||||
const QByteArray secretRaw =
|
||||
sshSession->getTextFileFromContainer(container, credentials, QString(kTelemtSecretPath), secretErr);
|
||||
const QString sec = QString::fromUtf8(secretRaw).trimmed();
|
||||
if (sec.length() == 32) {
|
||||
static const QRegularExpression hex32(QStringLiteral("^[0-9a-fA-F]{32}$"));
|
||||
if (hex32.match(sec).hasMatch()) {
|
||||
tc->secret = sec;
|
||||
}
|
||||
}
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
void TelemtInstaller::uploadClientSettingsSnapshot(SshSession &sshSession, const ServerCredentials &credentials,
|
||||
DockerContainer container, const ContainerConfig &config) {
|
||||
const TelemtProtocolConfig *tc = config.getTelemtProtocolConfig();
|
||||
if (!tc) {
|
||||
return;
|
||||
}
|
||||
const QByteArray payload = QJsonDocument(tc->toJson()).toJson(QJsonDocument::Compact);
|
||||
const ErrorCode err = sshSession.uploadTextFileToContainer(container, credentials, QString::fromUtf8(payload),
|
||||
QString(kTelemtClientJsonUploadPath));
|
||||
if (err != ErrorCode::NoError) {
|
||||
qWarning() << "TelemtInstaller::uploadClientSettingsSnapshot failed" << err;
|
||||
}
|
||||
}
|
||||
20
client/core/installers/telemtInstaller.h
Normal file
20
client/core/installers/telemtInstaller.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#ifndef TELEMTINSTALLER_H
|
||||
#define TELEMTINSTALLER_H
|
||||
|
||||
#include "installerBase.h"
|
||||
|
||||
class TelemtInstaller : public InstallerBase {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit TelemtInstaller(QObject *parent = nullptr);
|
||||
|
||||
amnezia::ErrorCode
|
||||
extractConfigFromContainer(amnezia::DockerContainer container, const amnezia::ServerCredentials &credentials,
|
||||
SshSession *sshSession, amnezia::ContainerConfig &config) override;
|
||||
|
||||
static void uploadClientSettingsSnapshot(SshSession &sshSession, const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container,
|
||||
const amnezia::ContainerConfig &config);
|
||||
};
|
||||
|
||||
#endif // TELEMTINSTALLER_H
|
||||
@@ -14,8 +14,18 @@
|
||||
#include "core/models/protocols/xrayProtocolConfig.h"
|
||||
#include "logger.h"
|
||||
|
||||
namespace {
|
||||
namespace
|
||||
{
|
||||
Logger logger("XrayInstaller");
|
||||
|
||||
// Xray expects uTLS preset names (chrome, firefox, …). Old Amnezia/server templates used "Mozilla/5.0".
|
||||
QString normalizeXrayFingerprint(const QString &fp)
|
||||
{
|
||||
if (fp.isEmpty() || fp.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) {
|
||||
return QString::fromLatin1(protocols::xray::defaultFingerprint);
|
||||
}
|
||||
return fp;
|
||||
}
|
||||
}
|
||||
|
||||
using namespace amnezia;
|
||||
@@ -63,18 +73,251 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c
|
||||
}
|
||||
|
||||
QJsonObject streamSettings = inbound[protocols::xray::streamSettings].toObject();
|
||||
QJsonObject realitySettings = streamSettings[protocols::xray::realitySettings].toObject();
|
||||
if (!realitySettings.contains(protocols::xray::serverNames)) {
|
||||
logger.error() << "Settings missing 'serverNames' field";
|
||||
auto *xrayConfig = config.getXrayProtocolConfig();
|
||||
if (!xrayConfig) {
|
||||
logger.error() << "No XrayProtocolConfig in ContainerConfig";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
QString siteName = realitySettings[protocols::xray::serverNames][0].toString();
|
||||
XrayServerConfig &srv = xrayConfig->serverConfig;
|
||||
|
||||
if (auto* xrayConfig = config.getXrayProtocolConfig()) {
|
||||
xrayConfig->serverConfig.site = siteName;
|
||||
// ── Port ─────────────────────────────────────────────────────────
|
||||
if (inbound.contains(protocols::xray::port)) {
|
||||
srv.port = QString::number(inbound[protocols::xray::port].toInt());
|
||||
}
|
||||
|
||||
|
||||
// ── Network (transport) ───────────────────────────────────────────
|
||||
QString networkVal = streamSettings.value(protocols::xray::network).toString("tcp");
|
||||
if (networkVal == "xhttp") {
|
||||
srv.transport = "xhttp";
|
||||
} else if (networkVal == "kcp") {
|
||||
srv.transport = "mkcp";
|
||||
} else {
|
||||
srv.transport = "raw";
|
||||
}
|
||||
|
||||
// ── Security ──────────────────────────────────────────────────────
|
||||
srv.security = streamSettings.value(protocols::xray::security).toString("reality");
|
||||
|
||||
// ── Reality settings ──────────────────────────────────────────────
|
||||
if (srv.security == "reality") {
|
||||
QJsonObject rs = streamSettings.value(protocols::xray::realitySettings).toObject();
|
||||
|
||||
// serverNames array → site + sni
|
||||
if (rs.contains(protocols::xray::serverNames)) {
|
||||
QString sniVal = rs[protocols::xray::serverNames].toArray().first().toString();
|
||||
srv.sni = sniVal;
|
||||
srv.site = sniVal;
|
||||
} else if (rs.contains(protocols::xray::serverName)) {
|
||||
srv.sni = rs[protocols::xray::serverName].toString();
|
||||
srv.site = srv.sni;
|
||||
}
|
||||
|
||||
srv.fingerprint = normalizeXrayFingerprint(rs.value(protocols::xray::fingerprint).toString());
|
||||
}
|
||||
|
||||
// ── TLS settings ──────────────────────────────────────────────────
|
||||
if (srv.security == "tls") {
|
||||
QJsonObject tls = streamSettings.value("tlsSettings").toObject();
|
||||
srv.sni = tls.value(protocols::xray::serverName).toString();
|
||||
srv.fingerprint = normalizeXrayFingerprint(tls.value(protocols::xray::fingerprint).toString());
|
||||
|
||||
QJsonArray alpnArr = tls.value("alpn").toArray();
|
||||
QStringList alpnList;
|
||||
for (const QJsonValue &v : alpnArr) {
|
||||
alpnList << v.toString();
|
||||
}
|
||||
srv.alpn = alpnList.join(",");
|
||||
}
|
||||
|
||||
// ── Flow (from users array) ───────────────────────────────────────
|
||||
if (inbound.contains(protocols::xray::settings)) {
|
||||
QJsonObject s = inbound[protocols::xray::settings].toObject();
|
||||
QJsonArray clientsArr = s.value(protocols::xray::clients).toArray();
|
||||
if (!clientsArr.isEmpty()) {
|
||||
srv.flow = clientsArr[0].toObject().value(protocols::xray::flow).toString();
|
||||
}
|
||||
}
|
||||
|
||||
// ── XHTTP settings (Xray-core SplitHTTPConfig + legacy Amnezia keys) ──
|
||||
if (srv.transport == "xhttp") {
|
||||
QJsonObject xhttpObj = streamSettings.value("xhttpSettings").toObject();
|
||||
{
|
||||
const QString m = xhttpObj.value("mode").toString();
|
||||
if (m.isEmpty() || m == QLatin1String("auto"))
|
||||
srv.xhttp.mode = QStringLiteral("Auto");
|
||||
else if (m == QLatin1String("packet-up"))
|
||||
srv.xhttp.mode = QStringLiteral("Packet-up");
|
||||
else if (m == QLatin1String("stream-up"))
|
||||
srv.xhttp.mode = QStringLiteral("Stream-up");
|
||||
else if (m == QLatin1String("stream-one"))
|
||||
srv.xhttp.mode = QStringLiteral("Stream-one");
|
||||
else
|
||||
srv.xhttp.mode = m;
|
||||
}
|
||||
|
||||
srv.xhttp.host = xhttpObj.value("host").toString();
|
||||
srv.xhttp.path = xhttpObj.value("path").toString();
|
||||
|
||||
{
|
||||
const QJsonObject hdrs = xhttpObj.value("headers").toObject();
|
||||
if (hdrs.contains(QLatin1String("Host")) || !hdrs.isEmpty())
|
||||
srv.xhttp.headersTemplate = QStringLiteral("HTTP");
|
||||
}
|
||||
|
||||
if (xhttpObj.contains(QLatin1String("uplinkHTTPMethod")))
|
||||
srv.xhttp.uplinkMethod = xhttpObj.value("uplinkHTTPMethod").toString();
|
||||
else
|
||||
srv.xhttp.uplinkMethod = xhttpObj.value("method").toString();
|
||||
|
||||
srv.xhttp.disableGrpc = xhttpObj.value("noGRPCHeader").toBool(true);
|
||||
srv.xhttp.disableSse = xhttpObj.value("noSSEHeader").toBool(true);
|
||||
|
||||
auto sessionSeqUi = [](const QString &core) -> QString {
|
||||
if (core.isEmpty() || core == QLatin1String("path"))
|
||||
return QStringLiteral("Path");
|
||||
if (core == QLatin1String("cookie"))
|
||||
return QStringLiteral("Cookie");
|
||||
if (core == QLatin1String("header"))
|
||||
return QStringLiteral("Header");
|
||||
if (core == QLatin1String("query"))
|
||||
return QStringLiteral("Query");
|
||||
return core;
|
||||
};
|
||||
QString sess = xhttpObj.value("sessionPlacement").toString();
|
||||
if (sess.isEmpty())
|
||||
sess = xhttpObj.value("scSessionPlacement").toString();
|
||||
srv.xhttp.sessionPlacement = sessionSeqUi(sess);
|
||||
|
||||
QString seq = xhttpObj.value("seqPlacement").toString();
|
||||
if (seq.isEmpty())
|
||||
seq = xhttpObj.value("scSeqPlacement").toString();
|
||||
srv.xhttp.seqPlacement = sessionSeqUi(seq);
|
||||
|
||||
auto uplinkDataUi = [](const QString &core) -> QString {
|
||||
if (core.isEmpty() || core == QLatin1String("body"))
|
||||
return QStringLiteral("Body");
|
||||
if (core == QLatin1String("auto"))
|
||||
return QStringLiteral("Auto");
|
||||
if (core == QLatin1String("header"))
|
||||
return QStringLiteral("Header");
|
||||
if (core == QLatin1String("cookie"))
|
||||
return QStringLiteral("Cookie");
|
||||
return core;
|
||||
};
|
||||
QString udata = xhttpObj.value("uplinkDataPlacement").toString();
|
||||
if (udata.isEmpty())
|
||||
udata = xhttpObj.value("scUplinkDataPlacement").toString();
|
||||
srv.xhttp.uplinkDataPlacement = uplinkDataUi(udata);
|
||||
|
||||
srv.xhttp.sessionKey = xhttpObj.value("sessionKey").toString();
|
||||
srv.xhttp.seqKey = xhttpObj.value("seqKey").toString();
|
||||
srv.xhttp.uplinkDataKey = xhttpObj.value("uplinkDataKey").toString();
|
||||
|
||||
if (xhttpObj.contains(QLatin1String("uplinkChunkSize"))) {
|
||||
QJsonObject uc = xhttpObj.value("uplinkChunkSize").toObject();
|
||||
if (!uc.isEmpty())
|
||||
srv.xhttp.uplinkChunkSize = QString::number(uc.value("from").toInt());
|
||||
} else if (xhttpObj.contains(QLatin1String("xhttpUplinkChunkSize"))) {
|
||||
srv.xhttp.uplinkChunkSize = QString::number(xhttpObj.value("xhttpUplinkChunkSize").toInt());
|
||||
}
|
||||
if (xhttpObj.contains(QLatin1String("scMaxBufferedPosts"))) {
|
||||
srv.xhttp.scMaxBufferedPosts = QString::number(xhttpObj.value("scMaxBufferedPosts").toVariant().toLongLong());
|
||||
}
|
||||
|
||||
auto readRange = [&](const char *key, QString &minOut, QString &maxOut) {
|
||||
QJsonObject r = xhttpObj.value(QLatin1String(key)).toObject();
|
||||
if (!r.isEmpty()) {
|
||||
minOut = QString::number(r.value("from").toInt());
|
||||
maxOut = QString::number(r.value("to").toInt());
|
||||
}
|
||||
};
|
||||
readRange("scMaxEachPostBytes", srv.xhttp.scMaxEachPostBytesMin, srv.xhttp.scMaxEachPostBytesMax);
|
||||
readRange("scMinPostsIntervalMs", srv.xhttp.scMinPostsIntervalMsMin, srv.xhttp.scMinPostsIntervalMsMax);
|
||||
readRange("scStreamUpServerSecs", srv.xhttp.scStreamUpServerSecsMin, srv.xhttp.scStreamUpServerSecsMax);
|
||||
|
||||
auto loadPaddingFromObject = [&](const QJsonObject &pad) {
|
||||
if (pad.contains(QLatin1String("xPaddingObfsMode")))
|
||||
srv.xhttp.xPadding.obfsMode = pad.value("xPaddingObfsMode").toBool(true);
|
||||
srv.xhttp.xPadding.key = pad.value("xPaddingKey").toString();
|
||||
srv.xhttp.xPadding.header = pad.value("xPaddingHeader").toString();
|
||||
srv.xhttp.xPadding.placement = pad.value("xPaddingPlacement").toString();
|
||||
srv.xhttp.xPadding.method = pad.value("xPaddingMethod").toString();
|
||||
QJsonObject bytesRange = pad.value("xPaddingBytes").toObject();
|
||||
if (!bytesRange.isEmpty()) {
|
||||
srv.xhttp.xPadding.bytesMin = QString::number(bytesRange.value("from").toInt());
|
||||
srv.xhttp.xPadding.bytesMax = QString::number(bytesRange.value("to").toInt());
|
||||
}
|
||||
QString pl = srv.xhttp.xPadding.placement.toLower();
|
||||
if (pl == QLatin1String("cookie"))
|
||||
srv.xhttp.xPadding.placement = QStringLiteral("Cookie");
|
||||
else if (pl == QLatin1String("header"))
|
||||
srv.xhttp.xPadding.placement = QStringLiteral("Header");
|
||||
else if (pl == QLatin1String("query"))
|
||||
srv.xhttp.xPadding.placement = QStringLiteral("Query");
|
||||
else if (pl == QLatin1String("queryinheader"))
|
||||
srv.xhttp.xPadding.placement = QStringLiteral("Query in header");
|
||||
QString met = srv.xhttp.xPadding.method.toLower();
|
||||
if (met == QLatin1String("repeat-x"))
|
||||
srv.xhttp.xPadding.method = QStringLiteral("Repeat-x");
|
||||
else if (met == QLatin1String("tokenish"))
|
||||
srv.xhttp.xPadding.method = QStringLiteral("Tokenish");
|
||||
};
|
||||
if (xhttpObj.contains(QLatin1String("xPaddingObfsMode")) || xhttpObj.contains(QLatin1String("xPaddingKey"))
|
||||
|| !xhttpObj.value("xPaddingBytes").toObject().isEmpty()) {
|
||||
loadPaddingFromObject(xhttpObj);
|
||||
} else if (xhttpObj.contains(QLatin1String("xPadding")) && xhttpObj.value("xPadding").isObject()) {
|
||||
const QJsonObject nested = xhttpObj.value("xPadding").toObject();
|
||||
if (!nested.isEmpty()) {
|
||||
loadPaddingFromObject(nested);
|
||||
if (!nested.contains(QLatin1String("xPaddingObfsMode")))
|
||||
srv.xhttp.xPadding.obfsMode = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (xhttpObj.contains(QLatin1String("xmux"))) {
|
||||
QJsonObject mux = xhttpObj.value("xmux").toObject();
|
||||
srv.xhttp.xmux.enabled = true;
|
||||
|
||||
auto readMuxRange = [&](const char *key, QString &minOut, QString &maxOut) {
|
||||
QJsonObject r = mux.value(QLatin1String(key)).toObject();
|
||||
if (!r.isEmpty()) {
|
||||
minOut = QString::number(r.value("from").toInt());
|
||||
maxOut = QString::number(r.value("to").toInt());
|
||||
}
|
||||
};
|
||||
readMuxRange("maxConcurrency", srv.xhttp.xmux.maxConcurrencyMin, srv.xhttp.xmux.maxConcurrencyMax);
|
||||
readMuxRange("maxConnections", srv.xhttp.xmux.maxConnectionsMin, srv.xhttp.xmux.maxConnectionsMax);
|
||||
readMuxRange("cMaxReuseTimes", srv.xhttp.xmux.cMaxReuseTimesMin, srv.xhttp.xmux.cMaxReuseTimesMax);
|
||||
readMuxRange("hMaxRequestTimes", srv.xhttp.xmux.hMaxRequestTimesMin, srv.xhttp.xmux.hMaxRequestTimesMax);
|
||||
readMuxRange("hMaxReusableSecs", srv.xhttp.xmux.hMaxReusableSecsMin, srv.xhttp.xmux.hMaxReusableSecsMax);
|
||||
|
||||
if (mux.contains(QLatin1String("hKeepAlivePeriod")))
|
||||
srv.xhttp.xmux.hKeepAlivePeriod = QString::number(mux.value("hKeepAlivePeriod").toVariant().toLongLong());
|
||||
}
|
||||
}
|
||||
|
||||
// ── mKCP settings ─────────────────────────────────────────────────
|
||||
if (srv.transport == "mkcp") {
|
||||
QJsonObject kcp = streamSettings.value("kcpSettings").toObject();
|
||||
if (kcp.contains("tti")) {
|
||||
srv.mkcp.tti = QString::number(kcp["tti"].toInt());
|
||||
}
|
||||
if (kcp.contains("uplinkCapacity")) {
|
||||
srv.mkcp.uplinkCapacity = QString::number(kcp["uplinkCapacity"].toInt());
|
||||
}
|
||||
if (kcp.contains("downlinkCapacity")) {
|
||||
srv.mkcp.downlinkCapacity = QString::number(kcp["downlinkCapacity"].toInt());
|
||||
}
|
||||
if (kcp.contains("readBufferSize")) {
|
||||
srv.mkcp.readBufferSize = QString::number(kcp["readBufferSize"].toInt());
|
||||
}
|
||||
if (kcp.contains("writeBufferSize")) {
|
||||
srv.mkcp.writeBufferSize = QString::number(kcp["writeBufferSize"].toInt());
|
||||
}
|
||||
srv.mkcp.congestion = kcp.value("congestion").toBool(true);
|
||||
}
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
|
||||
@@ -113,6 +113,26 @@ const Socks5ProxyProtocolConfig* ContainerConfig::getSocks5ProxyProtocolConfig()
|
||||
return protocolConfig.as<Socks5ProxyProtocolConfig>();
|
||||
}
|
||||
|
||||
MtProxyProtocolConfig* ContainerConfig::getMtProxyProtocolConfig()
|
||||
{
|
||||
return protocolConfig.as<MtProxyProtocolConfig>();
|
||||
}
|
||||
|
||||
const MtProxyProtocolConfig* ContainerConfig::getMtProxyProtocolConfig() const
|
||||
{
|
||||
return protocolConfig.as<MtProxyProtocolConfig>();
|
||||
}
|
||||
|
||||
TelemtProtocolConfig* ContainerConfig::getTelemtProtocolConfig()
|
||||
{
|
||||
return protocolConfig.as<TelemtProtocolConfig>();
|
||||
}
|
||||
|
||||
const TelemtProtocolConfig* ContainerConfig::getTelemtProtocolConfig() const
|
||||
{
|
||||
return protocolConfig.as<TelemtProtocolConfig>();
|
||||
}
|
||||
|
||||
Ikev2ProtocolConfig* ContainerConfig::getIkev2ProtocolConfig()
|
||||
{
|
||||
return protocolConfig.as<Ikev2ProtocolConfig>();
|
||||
|
||||
@@ -57,6 +57,12 @@ struct ContainerConfig {
|
||||
Socks5ProxyProtocolConfig* getSocks5ProxyProtocolConfig();
|
||||
const Socks5ProxyProtocolConfig* getSocks5ProxyProtocolConfig() const;
|
||||
|
||||
MtProxyProtocolConfig* getMtProxyProtocolConfig();
|
||||
const MtProxyProtocolConfig* getMtProxyProtocolConfig() const;
|
||||
|
||||
TelemtProtocolConfig* getTelemtProtocolConfig();
|
||||
const TelemtProtocolConfig* getTelemtProtocolConfig() const;
|
||||
|
||||
Ikev2ProtocolConfig* getIkev2ProtocolConfig();
|
||||
const Ikev2ProtocolConfig* getIkev2ProtocolConfig() const;
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/models/protocols/ikev2ProtocolConfig.h"
|
||||
#include "core/models/protocols/dnsProtocolConfig.h"
|
||||
#include "core/models/protocols/mtProxyProtocolConfig.h"
|
||||
#include "core/models/protocols/telemtProtocolConfig.h"
|
||||
|
||||
namespace amnezia
|
||||
{
|
||||
@@ -38,6 +40,10 @@ Proto ProtocolConfig::type() const
|
||||
return Proto::TorWebSite;
|
||||
} else if constexpr (std::is_same_v<T, DnsProtocolConfig>) {
|
||||
return Proto::Dns;
|
||||
} else if constexpr (std::is_same_v<T, MtProxyProtocolConfig>) {
|
||||
return Proto::MtProxy;
|
||||
} else if constexpr (std::is_same_v<T, TelemtProtocolConfig>) {
|
||||
return Proto::Telemt;
|
||||
}
|
||||
return Proto::Unknown;
|
||||
}, data);
|
||||
@@ -65,6 +71,10 @@ QString ProtocolConfig::port() const
|
||||
return QString();
|
||||
} else if constexpr (std::is_same_v<T, DnsProtocolConfig>) {
|
||||
return QString();
|
||||
} else if constexpr (std::is_same_v<T, MtProxyProtocolConfig>) {
|
||||
return arg.port.isEmpty() ? QString(protocols::mtProxy::defaultPort) : arg.port;
|
||||
} else if constexpr (std::is_same_v<T, TelemtProtocolConfig>) {
|
||||
return arg.port.isEmpty() ? QString(protocols::telemt::defaultPort) : arg.port;
|
||||
}
|
||||
return QString();
|
||||
}, data);
|
||||
@@ -88,6 +98,10 @@ QString ProtocolConfig::transportProto() const
|
||||
return QString();
|
||||
} else if constexpr (std::is_same_v<T, DnsProtocolConfig>) {
|
||||
return QString();
|
||||
} else if constexpr (std::is_same_v<T, MtProxyProtocolConfig>) {
|
||||
return QStringLiteral("tcp");
|
||||
} else if constexpr (std::is_same_v<T, TelemtProtocolConfig>) {
|
||||
return QStringLiteral("tcp");
|
||||
}
|
||||
return QString();
|
||||
}, data);
|
||||
@@ -299,6 +313,10 @@ ProtocolConfig ProtocolConfig::fromJson(const QJsonObject& json, Proto type)
|
||||
return ProtocolConfig{TorProtocolConfig::fromJson(json)};
|
||||
case Proto::Dns:
|
||||
return ProtocolConfig{DnsProtocolConfig::fromJson(json)};
|
||||
case Proto::MtProxy:
|
||||
return ProtocolConfig{MtProxyProtocolConfig::fromJson(json)};
|
||||
case Proto::Telemt:
|
||||
return ProtocolConfig{TelemtProtocolConfig::fromJson(json)};
|
||||
default:
|
||||
return ProtocolConfig{AwgProtocolConfig{}};
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
#include "core/models/protocols/ikev2ProtocolConfig.h"
|
||||
#include "core/models/protocols/torProtocolConfig.h"
|
||||
#include "core/models/protocols/dnsProtocolConfig.h"
|
||||
#include "core/models/protocols/mtProxyProtocolConfig.h"
|
||||
#include "core/models/protocols/telemtProtocolConfig.h"
|
||||
|
||||
namespace amnezia
|
||||
{
|
||||
@@ -36,6 +38,8 @@ struct ProtocolConfig {
|
||||
XrayProtocolConfig,
|
||||
SftpProtocolConfig,
|
||||
Socks5ProxyProtocolConfig,
|
||||
MtProxyProtocolConfig,
|
||||
TelemtProtocolConfig,
|
||||
Ikev2ProtocolConfig,
|
||||
TorProtocolConfig,
|
||||
DnsProtocolConfig
|
||||
|
||||
147
client/core/models/protocols/mtProxyProtocolConfig.cpp
Normal file
147
client/core/models/protocols/mtProxyProtocolConfig.cpp
Normal file
@@ -0,0 +1,147 @@
|
||||
#include "mtProxyProtocolConfig.h"
|
||||
|
||||
#include "../../../core/utils/protocolEnum.h"
|
||||
#include "../../../core/protocols/protocolUtils.h"
|
||||
#include "../../../core/utils/constants/configKeys.h"
|
||||
#include "../../../core/utils/constants/protocolConstants.h"
|
||||
#include <QJsonArray>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
namespace amnezia {
|
||||
|
||||
QJsonObject MtProxyProtocolConfig::toJson() const {
|
||||
QJsonObject obj;
|
||||
|
||||
if (!port.isEmpty()) {
|
||||
obj[configKey::port] = port;
|
||||
}
|
||||
if (!secret.isEmpty()) {
|
||||
obj[protocols::mtProxy::secretKey] = secret;
|
||||
}
|
||||
if (!tag.isEmpty()) {
|
||||
obj[protocols::mtProxy::tagKey] = tag;
|
||||
}
|
||||
if (!tgLink.isEmpty()) {
|
||||
obj[protocols::mtProxy::tgLinkKey] = tgLink;
|
||||
}
|
||||
if (!tmeLink.isEmpty()) {
|
||||
obj[protocols::mtProxy::tmeLinkKey] = tmeLink;
|
||||
}
|
||||
obj[protocols::mtProxy::isEnabledKey] = isEnabled;
|
||||
if (!publicHost.isEmpty()) {
|
||||
obj[protocols::mtProxy::publicHostKey] = publicHost;
|
||||
}
|
||||
if (!transportMode.isEmpty()) {
|
||||
obj[protocols::mtProxy::transportModeKey] = transportMode;
|
||||
}
|
||||
if (!tlsDomain.isEmpty()) {
|
||||
obj[protocols::mtProxy::tlsDomainKey] = tlsDomain;
|
||||
}
|
||||
if (!additionalSecrets.isEmpty()) {
|
||||
obj[protocols::mtProxy::additionalSecretsKey] = QJsonArray::fromStringList(additionalSecrets);
|
||||
}
|
||||
if (!workersMode.isEmpty()) {
|
||||
obj[protocols::mtProxy::workersModeKey] = workersMode;
|
||||
}
|
||||
if (!workers.isEmpty()) {
|
||||
obj[protocols::mtProxy::workersKey] = workers;
|
||||
}
|
||||
obj[protocols::mtProxy::natEnabledKey] = natEnabled;
|
||||
if (!natInternalIp.isEmpty()) {
|
||||
obj[protocols::mtProxy::natInternalIpKey] = natInternalIp;
|
||||
}
|
||||
if (!natExternalIp.isEmpty()) {
|
||||
obj[protocols::mtProxy::natExternalIpKey] = natExternalIp;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
MtProxyProtocolConfig MtProxyProtocolConfig::fromJson(const QJsonObject &json) {
|
||||
MtProxyProtocolConfig config;
|
||||
|
||||
config.port = json.value(configKey::port).toString();
|
||||
config.secret = json.value(protocols::mtProxy::secretKey).toString();
|
||||
config.tag = json.value(protocols::mtProxy::tagKey).toString();
|
||||
config.tgLink = json.value(protocols::mtProxy::tgLinkKey).toString();
|
||||
config.tmeLink = json.value(protocols::mtProxy::tmeLinkKey).toString();
|
||||
config.isEnabled = json.value(protocols::mtProxy::isEnabledKey).toBool(true);
|
||||
config.publicHost = json.value(protocols::mtProxy::publicHostKey).toString();
|
||||
config.transportMode = json.value(protocols::mtProxy::transportModeKey).toString();
|
||||
config.tlsDomain = json.value(protocols::mtProxy::tlsDomainKey).toString();
|
||||
for (const auto &v: json.value(protocols::mtProxy::additionalSecretsKey).toArray()) {
|
||||
const QString s = v.toString();
|
||||
if (!s.isEmpty()) {
|
||||
config.additionalSecrets.append(s);
|
||||
}
|
||||
}
|
||||
config.workersMode = json.value(protocols::mtProxy::workersModeKey).toString();
|
||||
config.workers = json.value(protocols::mtProxy::workersKey).toString();
|
||||
config.natEnabled = json.value(protocols::mtProxy::natEnabledKey).toBool(false);
|
||||
config.natInternalIp = json.value(protocols::mtProxy::natInternalIpKey).toString();
|
||||
config.natExternalIp = json.value(protocols::mtProxy::natExternalIpKey).toString();
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
bool MtProxyProtocolConfig::equalsDockerDeploymentSettings(const MtProxyProtocolConfig &other) const {
|
||||
const auto normPort = [](const QString &p) {
|
||||
return p.isEmpty() ? QString(protocols::mtProxy::defaultPort) : p;
|
||||
};
|
||||
const auto normTransport = [](const QString &t) {
|
||||
return t.isEmpty() ? QString(protocols::mtProxy::transportModeStandard) : t;
|
||||
};
|
||||
const auto normWorkersMode = [](const QString &m) {
|
||||
return m.isEmpty() ? QString(protocols::mtProxy::workersModeAuto) : m;
|
||||
};
|
||||
|
||||
if (normPort(port) != normPort(other.port)) {
|
||||
return false;
|
||||
}
|
||||
if (normTransport(transportMode) != normTransport(other.transportMode)) {
|
||||
return false;
|
||||
}
|
||||
if (tlsDomain != other.tlsDomain) {
|
||||
return false;
|
||||
}
|
||||
if (secret != other.secret) {
|
||||
return false;
|
||||
}
|
||||
if (tag != other.tag) {
|
||||
return false;
|
||||
}
|
||||
if (publicHost != other.publicHost) {
|
||||
return false;
|
||||
}
|
||||
if (normWorkersMode(workersMode) != normWorkersMode(other.workersMode)) {
|
||||
return false;
|
||||
}
|
||||
if (workers != other.workers) {
|
||||
return false;
|
||||
}
|
||||
if (natEnabled != other.natEnabled) {
|
||||
return false;
|
||||
}
|
||||
if (natInternalIp != other.natInternalIp) {
|
||||
return false;
|
||||
}
|
||||
if (natExternalIp != other.natExternalIp) {
|
||||
return false;
|
||||
}
|
||||
if (isEnabled != other.isEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QStringList aa = additionalSecrets;
|
||||
QStringList bb = other.additionalSecrets;
|
||||
aa.removeAll(QString());
|
||||
bb.removeAll(QString());
|
||||
std::sort(aa.begin(), aa.end());
|
||||
std::sort(bb.begin(), bb.end());
|
||||
return aa == bb;
|
||||
}
|
||||
|
||||
} // namespace amnezia
|
||||
38
client/core/models/protocols/mtProxyProtocolConfig.h
Normal file
38
client/core/models/protocols/mtProxyProtocolConfig.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#ifndef MTPROXYPROTOCOLCONFIG_H
|
||||
#define MTPROXYPROTOCOLCONFIG_H
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
namespace amnezia {
|
||||
|
||||
struct MtProxyProtocolConfig {
|
||||
QString port;
|
||||
QString secret;
|
||||
QString tag;
|
||||
QString tgLink;
|
||||
QString tmeLink;
|
||||
bool isEnabled = true;
|
||||
QString publicHost;
|
||||
QString transportMode;
|
||||
QString tlsDomain;
|
||||
QStringList additionalSecrets;
|
||||
QString workersMode;
|
||||
QString workers;
|
||||
bool natEnabled = false;
|
||||
QString natInternalIp;
|
||||
QString natExternalIp;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
|
||||
static MtProxyProtocolConfig fromJson(const QJsonObject &json);
|
||||
|
||||
// Port, transport, TLS, secrets, NAT, workers, isEnabled, additionalSecrets (order-independent).
|
||||
// Ignores tgLink / tmeLink (derived / display).
|
||||
bool equalsDockerDeploymentSettings(const MtProxyProtocolConfig &other) const;
|
||||
};
|
||||
|
||||
} // namespace amnezia
|
||||
|
||||
#endif // MTPROXYPROTOCOLCONFIG_H
|
||||
162
client/core/models/protocols/telemtProtocolConfig.cpp
Normal file
162
client/core/models/protocols/telemtProtocolConfig.cpp
Normal file
@@ -0,0 +1,162 @@
|
||||
#include "telemtProtocolConfig.h"
|
||||
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <algorithm>
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
QJsonObject TelemtProtocolConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
if (!port.isEmpty()) {
|
||||
obj[QString(configKey::port)] = port;
|
||||
}
|
||||
if (!secret.isEmpty()) {
|
||||
obj[protocols::telemt::secretKey] = secret;
|
||||
}
|
||||
if (!tag.isEmpty()) {
|
||||
obj[protocols::telemt::tagKey] = tag;
|
||||
}
|
||||
if (!tgLink.isEmpty()) {
|
||||
obj[protocols::telemt::tgLinkKey] = tgLink;
|
||||
}
|
||||
if (!tmeLink.isEmpty()) {
|
||||
obj[protocols::telemt::tmeLinkKey] = tmeLink;
|
||||
}
|
||||
obj[protocols::telemt::isEnabledKey] = isEnabled;
|
||||
if (!publicHost.isEmpty()) {
|
||||
obj[protocols::telemt::publicHostKey] = publicHost;
|
||||
}
|
||||
if (!transportMode.isEmpty()) {
|
||||
obj[protocols::telemt::transportModeKey] = transportMode;
|
||||
}
|
||||
if (!tlsDomain.isEmpty()) {
|
||||
obj[protocols::telemt::tlsDomainKey] = tlsDomain;
|
||||
}
|
||||
obj[protocols::telemt::maskEnabledKey] = maskEnabled;
|
||||
obj[protocols::telemt::tlsEmulationKey] = tlsEmulation;
|
||||
obj[protocols::telemt::useMiddleProxyKey] = useMiddleProxy;
|
||||
if (!userName.isEmpty()) {
|
||||
obj[protocols::telemt::userNameKey] = userName;
|
||||
}
|
||||
if (!additionalSecrets.isEmpty()) {
|
||||
obj[protocols::telemt::additionalSecretsKey] = QJsonArray::fromStringList(additionalSecrets);
|
||||
}
|
||||
if (!workersMode.isEmpty()) {
|
||||
obj[protocols::telemt::workersModeKey] = workersMode;
|
||||
}
|
||||
if (!workers.isEmpty()) {
|
||||
obj[protocols::telemt::workersKey] = workers;
|
||||
}
|
||||
obj[protocols::telemt::natEnabledKey] = natEnabled;
|
||||
if (!natInternalIp.isEmpty()) {
|
||||
obj[protocols::telemt::natInternalIpKey] = natInternalIp;
|
||||
}
|
||||
if (!natExternalIp.isEmpty()) {
|
||||
obj[protocols::telemt::natExternalIpKey] = natExternalIp;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
TelemtProtocolConfig TelemtProtocolConfig::fromJson(const QJsonObject &json)
|
||||
{
|
||||
TelemtProtocolConfig c;
|
||||
c.port = json.value(QString(configKey::port)).toString();
|
||||
c.secret = json.value(protocols::telemt::secretKey).toString();
|
||||
c.tag = json.value(protocols::telemt::tagKey).toString();
|
||||
c.tgLink = json.value(protocols::telemt::tgLinkKey).toString();
|
||||
c.tmeLink = json.value(protocols::telemt::tmeLinkKey).toString();
|
||||
c.isEnabled = json.value(protocols::telemt::isEnabledKey).toBool(true);
|
||||
c.publicHost = json.value(protocols::telemt::publicHostKey).toString();
|
||||
c.transportMode = json.value(protocols::telemt::transportModeKey).toString();
|
||||
c.tlsDomain = json.value(protocols::telemt::tlsDomainKey).toString();
|
||||
c.maskEnabled = json.value(protocols::telemt::maskEnabledKey).toBool(true);
|
||||
c.tlsEmulation = json.value(protocols::telemt::tlsEmulationKey).toBool(false);
|
||||
c.useMiddleProxy = json.value(protocols::telemt::useMiddleProxyKey).toBool(true);
|
||||
c.userName = json.value(protocols::telemt::userNameKey).toString();
|
||||
for (const auto &v : json.value(protocols::telemt::additionalSecretsKey).toArray()) {
|
||||
const QString s = v.toString();
|
||||
if (!s.isEmpty()) {
|
||||
c.additionalSecrets.append(s);
|
||||
}
|
||||
}
|
||||
c.workersMode = json.value(protocols::telemt::workersModeKey).toString();
|
||||
c.workers = json.value(protocols::telemt::workersKey).toString();
|
||||
c.natEnabled = json.value(protocols::telemt::natEnabledKey).toBool(false);
|
||||
c.natInternalIp = json.value(protocols::telemt::natInternalIpKey).toString();
|
||||
c.natExternalIp = json.value(protocols::telemt::natExternalIpKey).toString();
|
||||
return c;
|
||||
}
|
||||
|
||||
bool TelemtProtocolConfig::equalsDockerDeploymentSettings(const TelemtProtocolConfig &other) const
|
||||
{
|
||||
const auto normPort = [](const QString &p) {
|
||||
return p.isEmpty() ? QString(protocols::telemt::defaultPort) : p;
|
||||
};
|
||||
const auto normTransport = [](const QString &t) {
|
||||
return t.isEmpty() ? QString(protocols::telemt::transportModeStandard) : t;
|
||||
};
|
||||
const auto normWorkersMode = [](const QString &m) {
|
||||
return m.isEmpty() ? QString(protocols::telemt::workersModeAuto) : m;
|
||||
};
|
||||
|
||||
if (normPort(port) != normPort(other.port)) {
|
||||
return false;
|
||||
}
|
||||
if (normTransport(transportMode) != normTransport(other.transportMode)) {
|
||||
return false;
|
||||
}
|
||||
if (tlsDomain != other.tlsDomain) {
|
||||
return false;
|
||||
}
|
||||
if (secret != other.secret) {
|
||||
return false;
|
||||
}
|
||||
if (tag != other.tag) {
|
||||
return false;
|
||||
}
|
||||
if (publicHost != other.publicHost) {
|
||||
return false;
|
||||
}
|
||||
if (maskEnabled != other.maskEnabled) {
|
||||
return false;
|
||||
}
|
||||
if (tlsEmulation != other.tlsEmulation) {
|
||||
return false;
|
||||
}
|
||||
if (useMiddleProxy != other.useMiddleProxy) {
|
||||
return false;
|
||||
}
|
||||
if (userName != other.userName) {
|
||||
return false;
|
||||
}
|
||||
if (normWorkersMode(workersMode) != normWorkersMode(other.workersMode)) {
|
||||
return false;
|
||||
}
|
||||
if (workers != other.workers) {
|
||||
return false;
|
||||
}
|
||||
if (natEnabled != other.natEnabled) {
|
||||
return false;
|
||||
}
|
||||
if (natInternalIp != other.natInternalIp) {
|
||||
return false;
|
||||
}
|
||||
if (natExternalIp != other.natExternalIp) {
|
||||
return false;
|
||||
}
|
||||
if (isEnabled != other.isEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QStringList aa = additionalSecrets;
|
||||
QStringList bb = other.additionalSecrets;
|
||||
aa.removeAll(QString());
|
||||
bb.removeAll(QString());
|
||||
std::sort(aa.begin(), aa.end());
|
||||
std::sort(bb.begin(), bb.end());
|
||||
return aa == bb;
|
||||
}
|
||||
38
client/core/models/protocols/telemtProtocolConfig.h
Normal file
38
client/core/models/protocols/telemtProtocolConfig.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#ifndef TELEMTPROTOCOLCONFIG_H
|
||||
#define TELEMTPROTOCOLCONFIG_H
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
namespace amnezia {
|
||||
|
||||
struct TelemtProtocolConfig {
|
||||
QString port;
|
||||
QString secret;
|
||||
QString tag;
|
||||
QString tgLink;
|
||||
QString tmeLink;
|
||||
bool isEnabled = true;
|
||||
QString publicHost;
|
||||
QString transportMode;
|
||||
QString tlsDomain;
|
||||
bool maskEnabled = true;
|
||||
bool tlsEmulation = false;
|
||||
bool useMiddleProxy = true;
|
||||
QString userName;
|
||||
QStringList additionalSecrets;
|
||||
QString workersMode;
|
||||
QString workers;
|
||||
bool natEnabled = false;
|
||||
QString natInternalIp;
|
||||
QString natExternalIp;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static TelemtProtocolConfig fromJson(const QJsonObject &json);
|
||||
bool equalsDockerDeploymentSettings(const TelemtProtocolConfig &other) const;
|
||||
};
|
||||
|
||||
} // namespace amnezia
|
||||
|
||||
#endif // TELEMTPROTOCOLCONFIG_H
|
||||
@@ -3,20 +3,173 @@
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonArray>
|
||||
|
||||
#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/protocolEnum.h"
|
||||
#include "core/protocols/protocolUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
|
||||
using namespace amnezia;
|
||||
using namespace ProtocolUtils;
|
||||
|
||||
namespace amnezia
|
||||
{
|
||||
QJsonObject XrayXPaddingConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
if (!bytesMin.isEmpty()) obj[configKey::xPaddingBytesMin] = bytesMin;
|
||||
if (!bytesMax.isEmpty()) obj[configKey::xPaddingBytesMax] = bytesMax;
|
||||
obj[configKey::xPaddingObfsMode] = obfsMode;
|
||||
if (!key.isEmpty()) obj[configKey::xPaddingKey] = key;
|
||||
if (!header.isEmpty()) obj[configKey::xPaddingHeader] = header;
|
||||
if (!placement.isEmpty()) obj[configKey::xPaddingPlacement] = placement;
|
||||
if (!method.isEmpty()) obj[configKey::xPaddingMethod] = method;
|
||||
return obj;
|
||||
}
|
||||
|
||||
XrayXPaddingConfig XrayXPaddingConfig::fromJson(const QJsonObject &json)
|
||||
{
|
||||
XrayXPaddingConfig c;
|
||||
c.bytesMin = json.value(configKey::xPaddingBytesMin).toString();
|
||||
c.bytesMax = json.value(configKey::xPaddingBytesMax).toString();
|
||||
c.obfsMode = json.value(configKey::xPaddingObfsMode).toBool(true);
|
||||
c.key = json.value(configKey::xPaddingKey).toString(protocols::xray::defaultSite);
|
||||
c.header = json.value(configKey::xPaddingHeader).toString();
|
||||
c.placement = json.value(configKey::xPaddingPlacement).toString(protocols::xray::defaultXPaddingPlacement);
|
||||
c.method = json.value(configKey::xPaddingMethod).toString(protocols::xray::defaultXPaddingMethod);
|
||||
return c;
|
||||
}
|
||||
|
||||
QJsonObject XrayXmuxConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
obj[configKey::xmuxEnabled] = enabled;
|
||||
if (!maxConcurrencyMin.isEmpty()) obj[configKey::xmuxMaxConcurrencyMin] = maxConcurrencyMin;
|
||||
if (!maxConcurrencyMax.isEmpty()) obj[configKey::xmuxMaxConcurrencyMax] = maxConcurrencyMax;
|
||||
if (!maxConnectionsMin.isEmpty()) obj[configKey::xmuxMaxConnectionsMin] = maxConnectionsMin;
|
||||
if (!maxConnectionsMax.isEmpty()) obj[configKey::xmuxMaxConnectionsMax] = maxConnectionsMax;
|
||||
if (!cMaxReuseTimesMin.isEmpty()) obj[configKey::xmuxCMaxReuseTimesMin] = cMaxReuseTimesMin;
|
||||
if (!cMaxReuseTimesMax.isEmpty()) obj[configKey::xmuxCMaxReuseTimesMax] = cMaxReuseTimesMax;
|
||||
if (!hMaxRequestTimesMin.isEmpty()) obj[configKey::xmuxHMaxRequestTimesMin] = hMaxRequestTimesMin;
|
||||
if (!hMaxRequestTimesMax.isEmpty()) obj[configKey::xmuxHMaxRequestTimesMax] = hMaxRequestTimesMax;
|
||||
if (!hMaxReusableSecsMin.isEmpty()) obj[configKey::xmuxHMaxReusableSecsMin] = hMaxReusableSecsMin;
|
||||
if (!hMaxReusableSecsMax.isEmpty()) obj[configKey::xmuxHMaxReusableSecsMax] = hMaxReusableSecsMax;
|
||||
if (!hKeepAlivePeriod.isEmpty()) obj[configKey::xmuxHKeepAlivePeriod] = hKeepAlivePeriod;
|
||||
return obj;
|
||||
}
|
||||
|
||||
XrayXmuxConfig XrayXmuxConfig::fromJson(const QJsonObject &json)
|
||||
{
|
||||
XrayXmuxConfig c;
|
||||
c.enabled = json.value(configKey::xmuxEnabled).toBool(true);
|
||||
c.maxConcurrencyMin = json.value(configKey::xmuxMaxConcurrencyMin).toString("0");
|
||||
c.maxConcurrencyMax = json.value(configKey::xmuxMaxConcurrencyMax).toString("0");
|
||||
c.maxConnectionsMin = json.value(configKey::xmuxMaxConnectionsMin).toString("0");
|
||||
c.maxConnectionsMax = json.value(configKey::xmuxMaxConnectionsMax).toString("0");
|
||||
c.cMaxReuseTimesMin = json.value(configKey::xmuxCMaxReuseTimesMin).toString("0");
|
||||
c.cMaxReuseTimesMax = json.value(configKey::xmuxCMaxReuseTimesMax).toString("0");
|
||||
c.hMaxRequestTimesMin = json.value(configKey::xmuxHMaxRequestTimesMin).toString("0");
|
||||
c.hMaxRequestTimesMax = json.value(configKey::xmuxHMaxRequestTimesMax).toString("0");
|
||||
c.hMaxReusableSecsMin = json.value(configKey::xmuxHMaxReusableSecsMin).toString("0");
|
||||
c.hMaxReusableSecsMax = json.value(configKey::xmuxHMaxReusableSecsMax).toString("0");
|
||||
c.hKeepAlivePeriod = json.value(configKey::xmuxHKeepAlivePeriod).toString();
|
||||
return c;
|
||||
}
|
||||
|
||||
QJsonObject XrayXhttpConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
if (!mode.isEmpty()) obj[configKey::xhttpMode] = mode;
|
||||
if (!host.isEmpty()) obj[configKey::xhttpHost] = host;
|
||||
if (!path.isEmpty()) obj[configKey::xhttpPath] = path;
|
||||
if (!headersTemplate.isEmpty()) obj[configKey::xhttpHeadersTemplate] = headersTemplate;
|
||||
if (!uplinkMethod.isEmpty()) obj[configKey::xhttpUplinkMethod] = uplinkMethod;
|
||||
obj[configKey::xhttpDisableGrpc] = disableGrpc;
|
||||
obj[configKey::xhttpDisableSse] = disableSse;
|
||||
|
||||
if (!sessionPlacement.isEmpty()) obj[configKey::xhttpSessionPlacement] = sessionPlacement;
|
||||
if (!sessionKey.isEmpty()) obj[configKey::xhttpSessionKey] = sessionKey;
|
||||
if (!seqPlacement.isEmpty()) obj[configKey::xhttpSeqPlacement] = seqPlacement;
|
||||
if (!seqKey.isEmpty()) obj[configKey::xhttpSeqKey] = seqKey;
|
||||
if (!uplinkDataPlacement.isEmpty()) obj[configKey::xhttpUplinkDataPlacement] = uplinkDataPlacement;
|
||||
if (!uplinkDataKey.isEmpty()) obj[configKey::xhttpUplinkDataKey] = uplinkDataKey;
|
||||
|
||||
if (!uplinkChunkSize.isEmpty()) obj[configKey::xhttpUplinkChunkSize] = uplinkChunkSize;
|
||||
if (!scMaxBufferedPosts.isEmpty()) obj[configKey::xhttpScMaxBufferedPosts] = scMaxBufferedPosts;
|
||||
if (!scMaxEachPostBytesMin.isEmpty()) obj[configKey::xhttpScMaxEachPostBytesMin] = scMaxEachPostBytesMin;
|
||||
if (!scMaxEachPostBytesMax.isEmpty()) obj[configKey::xhttpScMaxEachPostBytesMax] = scMaxEachPostBytesMax;
|
||||
if (!scMinPostsIntervalMsMin.isEmpty()) obj[configKey::xhttpScMinPostsIntervalMsMin] = scMinPostsIntervalMsMin;
|
||||
if (!scMinPostsIntervalMsMax.isEmpty()) obj[configKey::xhttpScMinPostsIntervalMsMax] = scMinPostsIntervalMsMax;
|
||||
if (!scStreamUpServerSecsMin.isEmpty()) obj[configKey::xhttpScStreamUpServerSecsMin] = scStreamUpServerSecsMin;
|
||||
if (!scStreamUpServerSecsMax.isEmpty()) obj[configKey::xhttpScStreamUpServerSecsMax] = scStreamUpServerSecsMax;
|
||||
|
||||
obj["xPadding"] = xPadding.toJson();
|
||||
obj["xmux"] = xmux.toJson();
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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();
|
||||
|
||||
c.uplinkChunkSize = json.value(configKey::xhttpUplinkChunkSize).toString("0");
|
||||
c.scMaxBufferedPosts = json.value(configKey::xhttpScMaxBufferedPosts).toString();
|
||||
c.scMaxEachPostBytesMin = json.value(configKey::xhttpScMaxEachPostBytesMin).toString("1");
|
||||
c.scMaxEachPostBytesMax = json.value(configKey::xhttpScMaxEachPostBytesMax).toString("100");
|
||||
c.scMinPostsIntervalMsMin = json.value(configKey::xhttpScMinPostsIntervalMsMin).toString("100");
|
||||
c.scMinPostsIntervalMsMax = json.value(configKey::xhttpScMinPostsIntervalMsMax).toString("800");
|
||||
c.scStreamUpServerSecsMin = json.value(configKey::xhttpScStreamUpServerSecsMin).toString("1");
|
||||
c.scStreamUpServerSecsMax = json.value(configKey::xhttpScStreamUpServerSecsMax).toString("100");
|
||||
|
||||
c.xPadding = XrayXPaddingConfig::fromJson(json.value("xPadding").toObject());
|
||||
c.xmux = XrayXmuxConfig::fromJson(json.value("xmux").toObject());
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
QJsonObject XrayMkcpConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
if (!tti.isEmpty()) obj[configKey::mkcpTti] = tti;
|
||||
if (!uplinkCapacity.isEmpty()) obj[configKey::mkcpUplinkCapacity] = uplinkCapacity;
|
||||
if (!downlinkCapacity.isEmpty()) obj[configKey::mkcpDownlinkCapacity] = downlinkCapacity;
|
||||
if (!readBufferSize.isEmpty()) obj[configKey::mkcpReadBufferSize] = readBufferSize;
|
||||
if (!writeBufferSize.isEmpty()) obj[configKey::mkcpWriteBufferSize] = writeBufferSize;
|
||||
obj[configKey::mkcpCongestion] = congestion;
|
||||
return obj;
|
||||
}
|
||||
|
||||
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);
|
||||
return c;
|
||||
}
|
||||
|
||||
QJsonObject XrayServerConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
|
||||
|
||||
// Existing fields
|
||||
if (!port.isEmpty()) {
|
||||
obj[configKey::port] = port;
|
||||
}
|
||||
@@ -29,60 +182,96 @@ QJsonObject XrayServerConfig::toJson() const
|
||||
if (!site.isEmpty()) {
|
||||
obj[configKey::site] = site;
|
||||
}
|
||||
|
||||
|
||||
if (isThirdPartyConfig) {
|
||||
obj[configKey::isThirdPartyConfig] = isThirdPartyConfig;
|
||||
}
|
||||
|
||||
|
||||
// New: Security
|
||||
if (!security.isEmpty()) {
|
||||
obj[configKey::xraySecurity] = security;
|
||||
}
|
||||
if (!flow.isEmpty()) {
|
||||
obj[configKey::xrayFlow] = flow;
|
||||
}
|
||||
if (!fingerprint.isEmpty()) {
|
||||
obj[configKey::xrayFingerprint] = fingerprint;
|
||||
}
|
||||
if (!sni.isEmpty()) {
|
||||
obj[configKey::xraySni] = sni;
|
||||
}
|
||||
if (!alpn.isEmpty()) {
|
||||
obj[configKey::xrayAlpn] = alpn;
|
||||
}
|
||||
|
||||
// New: Transport
|
||||
if (!transport.isEmpty()) {
|
||||
obj[configKey::xrayTransport] = transport;
|
||||
}
|
||||
obj["xhttp"] = xhttp.toJson();
|
||||
obj["mkcp"] = mkcp.toJson();
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
XrayServerConfig XrayServerConfig::fromJson(const QJsonObject& json)
|
||||
XrayServerConfig XrayServerConfig::fromJson(const QJsonObject &json)
|
||||
{
|
||||
XrayServerConfig config;
|
||||
|
||||
config.port = json.value(configKey::port).toString();
|
||||
config.transportProto = json.value(configKey::transportProto).toString();
|
||||
config.subnetAddress = json.value(configKey::subnetAddress).toString();
|
||||
config.site = json.value(configKey::site).toString();
|
||||
|
||||
config.isThirdPartyConfig = json.value(configKey::isThirdPartyConfig).toBool(false);
|
||||
|
||||
return config;
|
||||
XrayServerConfig c;
|
||||
|
||||
// Existing fields
|
||||
c.port = json.value(configKey::port).toString();
|
||||
c.transportProto = json.value(configKey::transportProto).toString();
|
||||
c.subnetAddress = json.value(configKey::subnetAddress).toString();
|
||||
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);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
bool XrayServerConfig::hasEqualServerSettings(const XrayServerConfig& other) const
|
||||
bool XrayServerConfig::hasEqualServerSettings(const XrayServerConfig &other) const
|
||||
{
|
||||
return port == other.port && site == other.site;
|
||||
return port == other.port
|
||||
&& site == other.site
|
||||
&& security == other.security
|
||||
&& flow == other.flow
|
||||
&& transport == other.transport
|
||||
&& fingerprint == other.fingerprint
|
||||
&& sni == other.sni;
|
||||
}
|
||||
|
||||
QJsonObject XrayClientConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
|
||||
if (!nativeConfig.isEmpty()) {
|
||||
obj[configKey::config] = nativeConfig;
|
||||
}
|
||||
if (!localPort.isEmpty()) {
|
||||
obj[configKey::localPort] = localPort;
|
||||
}
|
||||
if (!id.isEmpty()) {
|
||||
obj[configKey::clientId] = id;
|
||||
}
|
||||
|
||||
if (!nativeConfig.isEmpty()) obj[configKey::config] = nativeConfig;
|
||||
if (!localPort.isEmpty()) obj[configKey::localPort] = localPort;
|
||||
if (!id.isEmpty()) obj[configKey::clientId] = id;
|
||||
return obj;
|
||||
}
|
||||
|
||||
XrayClientConfig XrayClientConfig::fromJson(const QJsonObject& json)
|
||||
XrayClientConfig XrayClientConfig::fromJson(const QJsonObject &json)
|
||||
{
|
||||
XrayClientConfig config;
|
||||
|
||||
config.nativeConfig = json.value(configKey::config).toString();
|
||||
config.localPort = json.value(configKey::localPort).toString();
|
||||
config.id = json.value(configKey::clientId).toString();
|
||||
|
||||
if (config.id.isEmpty() && !config.nativeConfig.isEmpty()) {
|
||||
QJsonDocument doc = QJsonDocument::fromJson(config.nativeConfig.toUtf8());
|
||||
XrayClientConfig c;
|
||||
c.nativeConfig = json.value(configKey::config).toString();
|
||||
c.localPort = json.value(configKey::localPort).toString();
|
||||
c.id = json.value(configKey::clientId).toString();
|
||||
|
||||
if (c.id.isEmpty() && !c.nativeConfig.isEmpty()) {
|
||||
QJsonDocument doc = QJsonDocument::fromJson(c.nativeConfig.toUtf8());
|
||||
if (!doc.isNull() && doc.isObject()) {
|
||||
QJsonObject configObj = doc.object();
|
||||
if (configObj.contains(protocols::xray::outbounds)) {
|
||||
@@ -100,7 +289,7 @@ XrayClientConfig XrayClientConfig::fromJson(const QJsonObject& json)
|
||||
if (!users.isEmpty()) {
|
||||
QJsonObject user = users[0].toObject();
|
||||
if (user.contains(protocols::xray::id)) {
|
||||
config.id = user[protocols::xray::id].toString();
|
||||
c.id = user[protocols::xray::id].toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,16 +300,15 @@ XrayClientConfig XrayClientConfig::fromJson(const QJsonObject& json)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
QJsonObject XrayProtocolConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj = serverConfig.toJson();
|
||||
|
||||
|
||||
if (clientConfig.has_value()) {
|
||||
// Third-party import: nativeConfig is raw Xray JSON (inbounds/outbounds)
|
||||
QJsonDocument doc = QJsonDocument::fromJson(clientConfig->nativeConfig.toUtf8());
|
||||
if (!doc.isNull() && doc.isObject() && doc.object().contains(protocols::xray::outbounds)
|
||||
&& !doc.object().contains(configKey::config)) {
|
||||
@@ -130,22 +318,20 @@ QJsonObject XrayProtocolConfig::toJson() const
|
||||
obj[configKey::lastConfig] = QString::fromUtf8(QJsonDocument(clientJson).toJson(QJsonDocument::Compact));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
XrayProtocolConfig XrayProtocolConfig::fromJson(const QJsonObject& json)
|
||||
XrayProtocolConfig XrayProtocolConfig::fromJson(const QJsonObject &json)
|
||||
{
|
||||
XrayProtocolConfig config;
|
||||
|
||||
config.serverConfig = XrayServerConfig::fromJson(json);
|
||||
|
||||
XrayProtocolConfig c;
|
||||
c.serverConfig = XrayServerConfig::fromJson(json);
|
||||
|
||||
QString lastConfigStr = json.value(configKey::lastConfig).toString();
|
||||
if (!lastConfigStr.isEmpty()) {
|
||||
QJsonDocument doc = QJsonDocument::fromJson(lastConfigStr.toUtf8());
|
||||
if (doc.isObject()) {
|
||||
QJsonObject parsed = doc.object();
|
||||
// Third-party import stores raw Xray config (inbounds/outbounds) directly
|
||||
if (parsed.contains(protocols::xray::outbounds) && !parsed.contains(configKey::config)) {
|
||||
XrayClientConfig clientCfg;
|
||||
clientCfg.nativeConfig = lastConfigStr;
|
||||
@@ -158,14 +344,14 @@ XrayProtocolConfig XrayProtocolConfig::fromJson(const QJsonObject& json)
|
||||
}
|
||||
}
|
||||
}
|
||||
config.clientConfig = clientCfg;
|
||||
c.clientConfig = clientCfg;
|
||||
} else {
|
||||
config.clientConfig = XrayClientConfig::fromJson(parsed);
|
||||
c.clientConfig = XrayClientConfig::fromJson(parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
bool XrayProtocolConfig::hasClientConfig() const
|
||||
@@ -173,7 +359,7 @@ bool XrayProtocolConfig::hasClientConfig() const
|
||||
return clientConfig.has_value();
|
||||
}
|
||||
|
||||
void XrayProtocolConfig::setClientConfig(const XrayClientConfig& config)
|
||||
void XrayProtocolConfig::setClientConfig(const XrayClientConfig &config)
|
||||
{
|
||||
clientConfig = config;
|
||||
}
|
||||
@@ -184,4 +370,3 @@ void XrayProtocolConfig::clearClientConfig()
|
||||
}
|
||||
|
||||
} // namespace amnezia
|
||||
|
||||
|
||||
@@ -2,47 +2,145 @@
|
||||
#define XRAYPROTOCOLCONFIG_H
|
||||
|
||||
#include <QJsonObject>
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include <QString>
|
||||
#include <optional>
|
||||
|
||||
namespace amnezia
|
||||
{
|
||||
|
||||
// ── xPadding ─────────────────────────────────────────────────────────────────
|
||||
struct XrayXPaddingConfig {
|
||||
QString bytesMin; // xPaddingBytes min
|
||||
QString bytesMax; // xPaddingBytes max
|
||||
bool obfsMode = true; // xPaddingObfsMode
|
||||
QString key; // xPaddingKey
|
||||
QString header; // xPaddingHeader
|
||||
QString placement = protocols::xray::defaultXPaddingPlacement; // xPaddingPlacement: Cookie|Header|Query|Body
|
||||
QString method = protocols::xray::defaultXPaddingMethod; // xPaddingMethod: Repeat-x|Random|Zero
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static XrayXPaddingConfig fromJson(const QJsonObject &json);
|
||||
};
|
||||
|
||||
// ── xmux ─────────────────────────────────────────────────────────────────────
|
||||
struct XrayXmuxConfig {
|
||||
bool enabled = true;
|
||||
|
||||
QString maxConcurrencyMin = "0";
|
||||
QString maxConcurrencyMax = "0";
|
||||
QString maxConnectionsMin = "0";
|
||||
QString maxConnectionsMax = "0";
|
||||
QString cMaxReuseTimesMin = "0";
|
||||
QString cMaxReuseTimesMax = "0";
|
||||
QString hMaxRequestTimesMin = "0";
|
||||
QString hMaxRequestTimesMax = "0";
|
||||
QString hMaxReusableSecsMin = "0";
|
||||
QString hMaxReusableSecsMax = "0";
|
||||
QString hKeepAlivePeriod;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static XrayXmuxConfig fromJson(const QJsonObject &json);
|
||||
};
|
||||
|
||||
// ── XHTTP transport ───────────────────────────────────────────────────────────
|
||||
struct XrayXhttpConfig {
|
||||
QString mode = protocols::xray::defaultXhttpMode; // Auto|Packet-up|Stream-up|Stream-one
|
||||
QString host = protocols::xray::defaultXhttpHost;
|
||||
QString path;
|
||||
QString headersTemplate = protocols::xray::defaultXhttpHeadersTemplate; // HTTP|None
|
||||
QString uplinkMethod = protocols::xray::defaultXhttpUplinkMethod; // POST|PUT|PATCH
|
||||
bool disableGrpc = true;
|
||||
bool disableSse = true;
|
||||
|
||||
// Session & Sequence
|
||||
QString sessionPlacement = protocols::xray::defaultXhttpSessionPlacement;
|
||||
QString sessionKey = protocols::xray::defaultXhttpSessionKey;
|
||||
QString seqPlacement = protocols::xray::defaultXhttpSeqPlacement;
|
||||
QString seqKey;
|
||||
QString uplinkDataPlacement = protocols::xray::defaultXhttpUplinkDataPlacement;
|
||||
QString uplinkDataKey;
|
||||
|
||||
// Traffic Shaping
|
||||
QString uplinkChunkSize = protocols::xray::defaultXhttpUplinkChunkSize;
|
||||
QString scMaxBufferedPosts;
|
||||
QString scMaxEachPostBytesMin = protocols::xray::defaultXhttpScMaxEachPostBytesMin;
|
||||
QString scMaxEachPostBytesMax = protocols::xray::defaultXhttpScMaxEachPostBytesMax;
|
||||
QString scMinPostsIntervalMsMin = protocols::xray::defaultXhttpScMinPostsIntervalMsMin;
|
||||
QString scMinPostsIntervalMsMax = protocols::xray::defaultXhttpScMinPostsIntervalMsMax;
|
||||
QString scStreamUpServerSecsMin = protocols::xray::defaultXhttpScStreamUpServerSecsMin;
|
||||
QString scStreamUpServerSecsMax = protocols::xray::defaultXhttpScStreamUpServerSecsMax;
|
||||
|
||||
XrayXPaddingConfig xPadding;
|
||||
XrayXmuxConfig xmux;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static XrayXhttpConfig fromJson(const QJsonObject &json);
|
||||
};
|
||||
|
||||
// ── mKCP transport ────────────────────────────────────────────────────────────
|
||||
struct XrayMkcpConfig {
|
||||
QString tti;
|
||||
QString uplinkCapacity;
|
||||
QString downlinkCapacity;
|
||||
QString readBufferSize;
|
||||
QString writeBufferSize;
|
||||
bool congestion = true;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static XrayMkcpConfig fromJson(const QJsonObject &json);
|
||||
};
|
||||
|
||||
// ── Server config (settings editable by user) ─────────────────────────────────
|
||||
struct XrayServerConfig {
|
||||
QString port;
|
||||
QString transportProto;
|
||||
QString subnetAddress;
|
||||
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;
|
||||
|
||||
// New: Transport
|
||||
QString transport = protocols::xray::defaultTransport;
|
||||
XrayXhttpConfig xhttp;
|
||||
XrayMkcpConfig mkcp;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static XrayServerConfig fromJson(const QJsonObject& json);
|
||||
|
||||
bool hasEqualServerSettings(const XrayServerConfig& other) const;
|
||||
|
||||
static XrayServerConfig fromJson(const QJsonObject &json);
|
||||
|
||||
bool hasEqualServerSettings(const XrayServerConfig &other) const;
|
||||
};
|
||||
|
||||
// ── Client config (generated, not edited by user) ─────────────────────────────
|
||||
struct XrayClientConfig {
|
||||
QString nativeConfig;
|
||||
QString localPort;
|
||||
QString id;
|
||||
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static XrayClientConfig fromJson(const QJsonObject& json);
|
||||
static XrayClientConfig fromJson(const QJsonObject &json);
|
||||
};
|
||||
|
||||
// ── Top-level protocol config ──────────────────────────────────────────────────
|
||||
struct XrayProtocolConfig {
|
||||
XrayServerConfig serverConfig;
|
||||
std::optional<XrayClientConfig> clientConfig;
|
||||
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static XrayProtocolConfig fromJson(const QJsonObject& json);
|
||||
|
||||
static XrayProtocolConfig fromJson(const QJsonObject &json);
|
||||
|
||||
bool hasClientConfig() const;
|
||||
void setClientConfig(const XrayClientConfig& config);
|
||||
void setClientConfig(const XrayClientConfig &config);
|
||||
void clearClientConfig();
|
||||
};
|
||||
|
||||
} // namespace amnezia
|
||||
|
||||
#endif // XRAYPROTOCOLCONFIG_H
|
||||
|
||||
|
||||
@@ -68,7 +68,10 @@ QMap<Proto, QString> ProtocolUtils::protocolHumanNames()
|
||||
{ Proto::TorWebSite, "Website in Tor network" },
|
||||
{ Proto::Dns, "DNS Service" },
|
||||
{ Proto::Sftp, QObject::tr("SFTP service") },
|
||||
{ Proto::Socks5Proxy, QObject::tr("SOCKS5 proxy server") } };
|
||||
{ Proto::Socks5Proxy, QObject::tr("SOCKS5 proxy server") },
|
||||
{ Proto::MtProxy, QObject::tr("MTProxy (Telegram)") },
|
||||
{ Proto::Telemt, QObject::tr("Telemt (Telegram)") },
|
||||
};
|
||||
}
|
||||
|
||||
QMap<Proto, QString> ProtocolUtils::protocolDescriptions()
|
||||
@@ -92,6 +95,8 @@ ServiceType ProtocolUtils::protocolService(Proto p)
|
||||
case Proto::Dns: return ServiceType::Other;
|
||||
case Proto::Sftp: return ServiceType::Other;
|
||||
case Proto::Socks5Proxy: return ServiceType::Other;
|
||||
case Proto::MtProxy: return ServiceType::Other;
|
||||
case Proto::Telemt: return ServiceType::Other;
|
||||
default: return ServiceType::Other;
|
||||
}
|
||||
}
|
||||
@@ -104,6 +109,8 @@ int ProtocolUtils::getPortForInstall(Proto p)
|
||||
case OpenVpn:
|
||||
case Socks5Proxy:
|
||||
return QRandomGenerator::global()->bounded(30000, 50000);
|
||||
case MtProxy:
|
||||
case Telemt:
|
||||
default:
|
||||
return defaultPort(p);
|
||||
}
|
||||
@@ -123,6 +130,8 @@ int ProtocolUtils::defaultPort(Proto p)
|
||||
case Proto::Dns: return 53;
|
||||
case Proto::Sftp: return 222;
|
||||
case Proto::Socks5Proxy: return 38080;
|
||||
case Proto::MtProxy: return QString(protocols::mtProxy::defaultPort).toInt();
|
||||
case Proto::Telemt: return QString(protocols::telemt::defaultPort).toInt();
|
||||
default: return -1;
|
||||
}
|
||||
}
|
||||
@@ -141,6 +150,8 @@ bool ProtocolUtils::defaultPortChangeable(Proto p)
|
||||
case Proto::Dns: return false;
|
||||
case Proto::Sftp: return true;
|
||||
case Proto::Socks5Proxy: return true;
|
||||
case Proto::MtProxy: return true;
|
||||
case Proto::Telemt: return true;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
@@ -161,6 +172,8 @@ TransportProto ProtocolUtils::defaultTransportProto(Proto p)
|
||||
case Proto::Dns: return TransportProto::Udp;
|
||||
case Proto::Sftp: return TransportProto::Tcp;
|
||||
case Proto::Socks5Proxy: return TransportProto::Tcp;
|
||||
case Proto::MtProxy: return TransportProto::Tcp;
|
||||
case Proto::Telemt: return TransportProto::Tcp;
|
||||
default: return TransportProto::Udp;
|
||||
}
|
||||
}
|
||||
@@ -180,9 +193,10 @@ bool ProtocolUtils::defaultTransportProtoChangeable(Proto p)
|
||||
case Proto::Dns: return false;
|
||||
case Proto::Sftp: return false;
|
||||
case Proto::Socks5Proxy: return false;
|
||||
case Proto::MtProxy: return false;
|
||||
case Proto::Telemt: return false;
|
||||
default: return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
QString ProtocolUtils::key_proto_config_data(Proto p)
|
||||
@@ -208,4 +222,3 @@ QString ProtocolUtils::getProtocolVersionString(const QJsonObject &protocolConfi
|
||||
if (version == protocols::awg::awgV1_5) return QObject::tr(" (version 1.5)");
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
48
client/core/protocols/xrayProtocol.cpp
Executable file → Normal file
48
client/core/protocols/xrayProtocol.cpp
Executable file → Normal file
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "core/protocols/protocolUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include "core/utils/ipcClient.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
#include "core/utils/serialization/serialization.h"
|
||||
@@ -9,6 +10,7 @@
|
||||
|
||||
#include <QCryptographicHash>
|
||||
#include <QJsonDocument>
|
||||
#include <QTimer>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkInterface>
|
||||
#include <QtCore/qlogging.h>
|
||||
@@ -79,12 +81,29 @@ ErrorCode XrayProtocol::start()
|
||||
m_socksPassword = creds.password;
|
||||
m_socksPort = creds.port;
|
||||
|
||||
const QString xrayConfigStr = QJsonDocument(m_xrayConfig).toJson(QJsonDocument::Compact);
|
||||
QString xrayConfigStr = QJsonDocument(m_xrayConfig).toJson(QJsonDocument::Compact);
|
||||
if (xrayConfigStr.isEmpty()) {
|
||||
qCritical() << "Xray config is empty";
|
||||
return ErrorCode::XrayExecutableCrashed;
|
||||
}
|
||||
|
||||
// Fix fingerprint: old configs may contain "Mozilla/5.0" which xray-core rejects.
|
||||
// Replace with the correct default at runtime so stale stored configs still work.
|
||||
if (xrayConfigStr.contains("Mozilla/5.0", Qt::CaseInsensitive)) {
|
||||
xrayConfigStr.replace("Mozilla/5.0", amnezia::protocols::xray::defaultFingerprint,
|
||||
Qt::CaseInsensitive);
|
||||
qDebug() << "XrayProtocol: patched legacy fingerprint to"
|
||||
<< amnezia::protocols::xray::defaultFingerprint;
|
||||
}
|
||||
|
||||
// Fix inbound listen address: old configs may use "10.33.0.2" which doesn't exist
|
||||
// until TUN is created. xray must listen on 127.0.0.1 so tun2socks can connect.
|
||||
if (xrayConfigStr.contains(amnezia::protocols::xray::defaultLocalAddr)) {
|
||||
xrayConfigStr.replace(amnezia::protocols::xray::defaultLocalAddr,
|
||||
amnezia::protocols::xray::defaultLocalListenAddr);
|
||||
qDebug() << "XrayProtocol: patched legacy inbound listen address to 127.0.0.1";
|
||||
}
|
||||
|
||||
return IpcClient::withInterface(
|
||||
[&](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
auto xrayStart = iface->xrayStart(xrayConfigStr);
|
||||
@@ -188,6 +207,33 @@ ErrorCode XrayProtocol::startTun2Socks()
|
||||
connect(
|
||||
m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::finished, this,
|
||||
[this](int exitCode, QProcess::ExitStatus exitStatus) {
|
||||
// Check stdout for "resource busy" — the TUN device was not yet released
|
||||
// by the previous tun2socks instance. Retry after a short delay.
|
||||
bool resourceBusy = false;
|
||||
if (m_tun2socksProcess) {
|
||||
auto readOut = m_tun2socksProcess->readAllStandardOutput();
|
||||
if (readOut.waitForFinished()) {
|
||||
resourceBusy = readOut.returnValue().contains("resource busy");
|
||||
}
|
||||
}
|
||||
|
||||
if (resourceBusy && m_tun2socksRetryCount < maxTun2SocksRetries) {
|
||||
m_tun2socksRetryCount++;
|
||||
qWarning() << QString("Tun2socks: TUN resource busy, retrying (%1/%2) in %3ms...")
|
||||
.arg(m_tun2socksRetryCount)
|
||||
.arg(maxTun2SocksRetries)
|
||||
.arg(tun2socksRetryDelayMs);
|
||||
QTimer::singleShot(tun2socksRetryDelayMs, this, [this]() {
|
||||
if (ErrorCode err = startTun2Socks(); err != ErrorCode::NoError) {
|
||||
stop();
|
||||
setLastError(err);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
m_tun2socksRetryCount = 0;
|
||||
|
||||
if (exitStatus == QProcess::ExitStatus::CrashExit) {
|
||||
qCritical() << "Tun2socks process crashed!";
|
||||
} else {
|
||||
|
||||
@@ -35,6 +35,9 @@ private:
|
||||
int m_socksPort = 10808;
|
||||
|
||||
QSharedPointer<IpcProcessInterfaceReplica> m_tun2socksProcess;
|
||||
int m_tun2socksRetryCount = 0;
|
||||
static constexpr int maxTun2SocksRetries = 5;
|
||||
static constexpr int tun2socksRetryDelayMs = 400;
|
||||
};
|
||||
|
||||
#endif // XRAYPROTOCOL_H
|
||||
|
||||
@@ -451,4 +451,12 @@ void SecureAppSettingsRepository::setInstallationUuid(const QString &uuid)
|
||||
m_settings->setValue("Conf/installationUuid", uuid);
|
||||
}
|
||||
|
||||
QByteArray SecureAppSettingsRepository::xraySavedConfigs() const
|
||||
{
|
||||
return value("Xray/savedConfigs").toByteArray();
|
||||
}
|
||||
|
||||
void SecureAppSettingsRepository::setXraySavedConfigs(const QByteArray &data)
|
||||
{
|
||||
setValue("Xray/savedConfigs", data);
|
||||
}
|
||||
|
||||
@@ -92,6 +92,9 @@ public:
|
||||
|
||||
QString nextAvailableServerName() const;
|
||||
|
||||
QByteArray xraySavedConfigs() const;
|
||||
void setXraySavedConfigs(const QByteArray &data);
|
||||
|
||||
signals:
|
||||
void appLanguageChanged(QLocale locale);
|
||||
void allowedDnsServersChanged(const QStringList &servers);
|
||||
|
||||
@@ -77,6 +77,26 @@ 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)
|
||||
@@ -133,9 +153,28 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
|
||||
if (httpStatusFromBody == httpStatusCodePaymentRequired) {
|
||||
return amnezia::ErrorCode::ApiSubscriptionNotActiveError;
|
||||
}
|
||||
|
||||
const QString msg = apiErrorMessageFromJson(jsonObj);
|
||||
if (msg.contains(QStringLiteral("QR session"), Qt::CaseInsensitive)
|
||||
&& (msg.contains(QStringLiteral("not found"), Qt::CaseInsensitive)
|
||||
|| msg.contains(QStringLiteral("expired"), Qt::CaseInsensitive))) {
|
||||
return amnezia::ErrorCode::ApiPairingSessionExpiredError;
|
||||
}
|
||||
if (msg.contains(QStringLiteral("not found"), Qt::CaseInsensitive)
|
||||
|| msg.contains(QStringLiteral("expired"), Qt::CaseInsensitive)) {
|
||||
return amnezia::ErrorCode::ApiNotFoundError;
|
||||
}
|
||||
if (httpStatusCode == httpStatusCodeNotFound) {
|
||||
return amnezia::ErrorCode::ApiNotFoundError;
|
||||
}
|
||||
|
||||
return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||
}
|
||||
|
||||
if (httpStatusCode == httpStatusCodeNotFound) {
|
||||
return amnezia::ErrorCode::ApiNotFoundError;
|
||||
}
|
||||
|
||||
qDebug() << "something went wrong";
|
||||
return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||
}
|
||||
|
||||
@@ -23,6 +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);
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ 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");
|
||||
|
||||
@@ -93,6 +93,8 @@ namespace amnezia
|
||||
constexpr QLatin1String xray("xray");
|
||||
constexpr QLatin1String ssxray("ssxray");
|
||||
constexpr QLatin1String socks5proxy("socks5proxy");
|
||||
constexpr QLatin1String mtproxy("mtproxy");
|
||||
constexpr QLatin1String telemt("telemt");
|
||||
|
||||
constexpr QLatin1String splitTunnelSites("splitTunnelSites");
|
||||
constexpr QLatin1String splitTunnelType("splitTunnelType");
|
||||
@@ -124,6 +126,76 @@ namespace amnezia
|
||||
constexpr QLatin1String dataSent("dataSent");
|
||||
|
||||
constexpr QLatin1String storageServerId("storageServerId");
|
||||
|
||||
// ── Xray-specific keys ────────────────────────────────────────
|
||||
|
||||
// Security
|
||||
constexpr QLatin1String xraySecurity("xray_security"); // none | tls | reality
|
||||
constexpr QLatin1String xrayFlow("xray_flow"); // "" | xtls-rprx-vision | xtls-rprx-vision-udp443
|
||||
constexpr QLatin1String xrayFingerprint("xray_fingerprint"); // Mozilla/5.0 | chrome | firefox | ...
|
||||
constexpr QLatin1String xraySni("xray_sni"); // Server Name (SNI)
|
||||
constexpr QLatin1String xrayAlpn("xray_alpn"); // HTTP/2 | HTTP/1.1 | HTTP/2,HTTP/1.1
|
||||
|
||||
// Transport — common
|
||||
constexpr QLatin1String xrayTransport("xray_transport"); // raw | xhttp | mkcp
|
||||
|
||||
// Transport — XHTTP
|
||||
constexpr QLatin1String xhttpMode("xhttp_mode"); // Auto | Packet-up | Stream-up | Stream-one
|
||||
constexpr QLatin1String xhttpHost("xhttp_host");
|
||||
constexpr QLatin1String xhttpPath("xhttp_path");
|
||||
constexpr QLatin1String xhttpHeadersTemplate("xhttp_headers_template"); // HTTP | None
|
||||
constexpr QLatin1String xhttpUplinkMethod("xhttp_uplink_method"); // POST | PUT | PATCH
|
||||
constexpr QLatin1String xhttpDisableGrpc("xhttp_disable_grpc"); // bool
|
||||
constexpr QLatin1String xhttpDisableSse("xhttp_disable_sse"); // bool
|
||||
|
||||
// Transport — XHTTP Session & Sequence
|
||||
constexpr QLatin1String xhttpSessionPlacement("xhttp_session_placement"); // Path | Header | Cookie | None
|
||||
constexpr QLatin1String xhttpSessionKey("xhttp_session_key");
|
||||
constexpr QLatin1String xhttpSeqPlacement("xhttp_seq_placement");
|
||||
constexpr QLatin1String xhttpSeqKey("xhttp_seq_key");
|
||||
constexpr QLatin1String xhttpUplinkDataPlacement("xhttp_uplink_data_placement"); // Body | Query
|
||||
constexpr QLatin1String xhttpUplinkDataKey("xhttp_uplink_data_key");
|
||||
|
||||
// Transport — XHTTP Traffic Shaping
|
||||
constexpr QLatin1String xhttpUplinkChunkSize("xhttp_uplink_chunk_size");
|
||||
constexpr QLatin1String xhttpScMaxBufferedPosts("xhttp_sc_max_buffered_posts");
|
||||
constexpr QLatin1String xhttpScMaxEachPostBytesMin("xhttp_sc_max_each_post_bytes_min");
|
||||
constexpr QLatin1String xhttpScMaxEachPostBytesMax("xhttp_sc_max_each_post_bytes_max");
|
||||
constexpr QLatin1String xhttpScMinPostsIntervalMsMin("xhttp_sc_min_posts_interval_ms_min");
|
||||
constexpr QLatin1String xhttpScMinPostsIntervalMsMax("xhttp_sc_min_posts_interval_ms_max");
|
||||
constexpr QLatin1String xhttpScStreamUpServerSecsMin("xhttp_sc_stream_up_server_secs_min");
|
||||
constexpr QLatin1String xhttpScStreamUpServerSecsMax("xhttp_sc_stream_up_server_secs_max");
|
||||
|
||||
// Transport — mKCP
|
||||
constexpr QLatin1String mkcpTti("mkcp_tti");
|
||||
constexpr QLatin1String mkcpUplinkCapacity("mkcp_uplink_capacity");
|
||||
constexpr QLatin1String mkcpDownlinkCapacity("mkcp_downlink_capacity");
|
||||
constexpr QLatin1String mkcpReadBufferSize("mkcp_read_buffer_size");
|
||||
constexpr QLatin1String mkcpWriteBufferSize("mkcp_write_buffer_size");
|
||||
constexpr QLatin1String mkcpCongestion("mkcp_congestion"); // bool
|
||||
|
||||
// xPadding
|
||||
constexpr QLatin1String xPaddingBytesMin("xpadding_bytes_min");
|
||||
constexpr QLatin1String xPaddingBytesMax("xpadding_bytes_max");
|
||||
constexpr QLatin1String xPaddingObfsMode("xpadding_obfs_mode"); // bool
|
||||
constexpr QLatin1String xPaddingKey("xpadding_key");
|
||||
constexpr QLatin1String xPaddingHeader("xpadding_header");
|
||||
constexpr QLatin1String xPaddingPlacement("xpadding_placement"); // Cookie | Header | Query | Body
|
||||
constexpr QLatin1String xPaddingMethod("xpadding_method"); // Repeat-x | Random | Zero
|
||||
|
||||
// xmux
|
||||
constexpr QLatin1String xmuxEnabled("xmux_enabled"); // bool
|
||||
constexpr QLatin1String xmuxMaxConcurrencyMin("xmux_max_concurrency_min");
|
||||
constexpr QLatin1String xmuxMaxConcurrencyMax("xmux_max_concurrency_max");
|
||||
constexpr QLatin1String xmuxMaxConnectionsMin("xmux_max_connections_min");
|
||||
constexpr QLatin1String xmuxMaxConnectionsMax("xmux_max_connections_max");
|
||||
constexpr QLatin1String xmuxCMaxReuseTimesMin("xmux_c_max_reuse_times_min");
|
||||
constexpr QLatin1String xmuxCMaxReuseTimesMax("xmux_c_max_reuse_times_max");
|
||||
constexpr QLatin1String xmuxHMaxRequestTimesMin("xmux_h_max_request_times_min");
|
||||
constexpr QLatin1String xmuxHMaxRequestTimesMax("xmux_h_max_request_times_max");
|
||||
constexpr QLatin1String xmuxHMaxReusableSecsMin("xmux_h_max_reusable_secs_min");
|
||||
constexpr QLatin1String xmuxHMaxReusableSecsMax("xmux_h_max_reusable_secs_max");
|
||||
constexpr QLatin1String xmuxHKeepAlivePeriod("xmux_h_keep_alive_period");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
namespace amnezia
|
||||
{
|
||||
|
||||
namespace protocols
|
||||
{
|
||||
|
||||
@@ -57,6 +58,40 @@ namespace amnezia
|
||||
constexpr char defaultPort[] = "443";
|
||||
constexpr char defaultLocalProxyPort[] = "10808";
|
||||
constexpr char defaultLocalAddr[] = "10.33.0.2";
|
||||
constexpr char defaultLocalListenAddr[] = "127.0.0.1";
|
||||
|
||||
constexpr char defaultSecurity[] = "reality";
|
||||
constexpr char defaultFlow[] = "xtls-rprx-vision";
|
||||
constexpr char defaultTransport[] = "raw";
|
||||
constexpr char defaultFingerprint[] = "chrome";
|
||||
constexpr char defaultSni[] = "cdn.example.com";
|
||||
constexpr char defaultAlpn[] = "HTTP/2";
|
||||
|
||||
constexpr char defaultXhttpMode[] = "Auto";
|
||||
constexpr char defaultXhttpHeadersTemplate[] = "HTTP";
|
||||
constexpr char defaultXhttpUplinkMethod[] = "POST";
|
||||
constexpr char defaultXhttpSessionPlacement[] = "Path";
|
||||
constexpr char defaultXhttpSessionKey[] = "Path";
|
||||
constexpr char defaultXhttpSeqPlacement[] = "Path";
|
||||
constexpr char defaultXhttpUplinkDataPlacement[] = "Body";
|
||||
|
||||
constexpr char defaultXhttpHost[] = "www.googletagmanager.com";
|
||||
constexpr char defaultXhttpUplinkChunkSize[] = "0";
|
||||
constexpr char defaultXhttpScMaxEachPostBytesMin[] = "1";
|
||||
constexpr char defaultXhttpScMaxEachPostBytesMax[] = "100";
|
||||
constexpr char defaultXhttpScMinPostsIntervalMsMin[] = "100";
|
||||
constexpr char defaultXhttpScMinPostsIntervalMsMax[] = "800";
|
||||
constexpr char defaultXhttpScStreamUpServerSecsMin[] = "1";
|
||||
constexpr char defaultXhttpScStreamUpServerSecsMax[] = "100";
|
||||
|
||||
constexpr char defaultXPaddingPlacement[] = "Cookie";
|
||||
constexpr char defaultXPaddingMethod[] = "Repeat-x";
|
||||
|
||||
constexpr char defaultMkcpTti[] = "50";
|
||||
constexpr char defaultMkcpUplinkCapacity[] = "5";
|
||||
constexpr char defaultMkcpDownlinkCapacity[] = "20";
|
||||
constexpr char defaultMkcpReadBufferSize[] = "2";
|
||||
constexpr char defaultMkcpWriteBufferSize[] = "2";
|
||||
|
||||
constexpr char outbounds[] = "outbounds";
|
||||
constexpr char inbounds[] = "inbounds";
|
||||
@@ -174,9 +209,71 @@ namespace amnezia
|
||||
constexpr char proxyConfigPath[] = "/usr/local/3proxy/conf/3proxy.cfg";
|
||||
}
|
||||
|
||||
namespace mtProxy
|
||||
{
|
||||
constexpr char secretKey[] = "mtproxy_secret";
|
||||
constexpr char tagKey[] = "mtproxy_tag";
|
||||
constexpr char tgLinkKey[] = "mtproxy_tg_link";
|
||||
constexpr char tmeLinkKey[] = "mtproxy_tme_link";
|
||||
constexpr char isEnabledKey[] = "mtproxy_is_enabled";
|
||||
constexpr char publicHostKey[] = "mtproxy_public_host";
|
||||
constexpr char transportModeKey[] = "mtproxy_transport_mode";
|
||||
constexpr char tlsDomainKey[] = "mtproxy_tls_domain";
|
||||
constexpr char additionalSecretsKey[] = "mtproxy_additional_secrets";
|
||||
constexpr char workersKey[] = "mtproxy_workers";
|
||||
constexpr char workersModeKey[] = "mtproxy_workers_mode";
|
||||
constexpr char natEnabledKey[] = "mtproxy_nat_enabled";
|
||||
constexpr char natInternalIpKey[] = "mtproxy_nat_internal_ip";
|
||||
constexpr char natExternalIpKey[] = "mtproxy_nat_external_ip";
|
||||
|
||||
constexpr char transportModeStandard[] = "standard";
|
||||
constexpr char transportModeFakeTLS[] = "faketls";
|
||||
|
||||
constexpr char workersModeAuto[] = "auto";
|
||||
constexpr char workersModeManual[] = "manual";
|
||||
|
||||
constexpr char defaultPort[] = "443";
|
||||
constexpr char defaultWorkers[] = "2";
|
||||
constexpr int maxWorkers = 32;
|
||||
constexpr int botTagHexLength = 32;
|
||||
constexpr char defaultTlsDomain[] = "googletagmanager.com";
|
||||
}
|
||||
|
||||
namespace telemt
|
||||
{
|
||||
constexpr char secretKey[] = "telemt_secret";
|
||||
constexpr char tagKey[] = "telemt_tag";
|
||||
constexpr char tgLinkKey[] = "telemt_tg_link";
|
||||
constexpr char tmeLinkKey[] = "telemt_tme_link";
|
||||
constexpr char isEnabledKey[] = "telemt_is_enabled";
|
||||
constexpr char publicHostKey[] = "telemt_public_host";
|
||||
constexpr char transportModeKey[] = "telemt_transport_mode";
|
||||
constexpr char tlsDomainKey[] = "telemt_tls_domain";
|
||||
constexpr char maskEnabledKey[] = "telemt_mask_enabled";
|
||||
constexpr char tlsEmulationKey[] = "telemt_tls_emulation";
|
||||
constexpr char useMiddleProxyKey[] = "telemt_use_middle_proxy";
|
||||
constexpr char userNameKey[] = "telemt_user_name";
|
||||
// Stored for UI only (Telemt server ignores these; same controls as MTProxy page)
|
||||
constexpr char additionalSecretsKey[] = "telemt_additional_secrets";
|
||||
constexpr char workersKey[] = "telemt_workers";
|
||||
constexpr char workersModeKey[] = "telemt_workers_mode";
|
||||
constexpr char natEnabledKey[] = "telemt_nat_enabled";
|
||||
constexpr char natInternalIpKey[] = "telemt_nat_internal_ip";
|
||||
constexpr char natExternalIpKey[] = "telemt_nat_external_ip";
|
||||
|
||||
constexpr char transportModeStandard[] = "standard";
|
||||
constexpr char transportModeFakeTLS[] = "faketls";
|
||||
|
||||
constexpr char defaultPort[] = "443";
|
||||
constexpr char defaultTlsDomain[] = "googletagmanager.com";
|
||||
constexpr char defaultUserName[] = "amnezia";
|
||||
constexpr char defaultWorkers[] = "2";
|
||||
constexpr char workersModeAuto[] = "auto";
|
||||
constexpr char workersModeManual[] = "manual";
|
||||
constexpr int maxWorkers = 32;
|
||||
}
|
||||
|
||||
} // namespace protocols
|
||||
}
|
||||
|
||||
#endif // PROTOCOLCONSTANTS_H
|
||||
|
||||
|
||||
|
||||
@@ -23,7 +23,9 @@ namespace amnezia
|
||||
TorWebSite,
|
||||
Dns,
|
||||
Sftp,
|
||||
Socks5Proxy
|
||||
Socks5Proxy,
|
||||
MtProxy,
|
||||
Telemt,
|
||||
};
|
||||
Q_ENUM_NS(DockerContainer)
|
||||
} // namespace ContainerEnumNS
|
||||
|
||||
@@ -72,7 +72,10 @@ QMap<DockerContainer, QString> ContainerUtils::containerHumanNames()
|
||||
{ DockerContainer::TorWebSite, QObject::tr("Website in Tor network") },
|
||||
{ DockerContainer::Dns, QObject::tr("AmneziaDNS") },
|
||||
{ DockerContainer::Sftp, QObject::tr("SFTP file sharing service") },
|
||||
{ DockerContainer::Socks5Proxy, QObject::tr("SOCKS5 proxy server") } };
|
||||
{ DockerContainer::Socks5Proxy, QObject::tr("SOCKS5 proxy server") },
|
||||
{ DockerContainer::MtProxy, QObject::tr("MTProxy (Telegram)") },
|
||||
{ DockerContainer::Telemt, QObject::tr("Telemt (Telegram)") },
|
||||
};
|
||||
}
|
||||
|
||||
QMap<DockerContainer, QString> ContainerUtils::containerDescriptions()
|
||||
@@ -102,7 +105,12 @@ QMap<DockerContainer, QString> ContainerUtils::containerDescriptions()
|
||||
{ DockerContainer::Sftp,
|
||||
QObject::tr("Create a file vault on your server to securely store and transfer files.") },
|
||||
{ DockerContainer::Socks5Proxy,
|
||||
QObject::tr("") } };
|
||||
QObject::tr("") },
|
||||
{ DockerContainer::MtProxy,
|
||||
QObject::tr("Telegram MTProto proxy server") },
|
||||
{ DockerContainer::Telemt,
|
||||
QObject::tr("Telegram MTProto proxy (Telemt, Rust)") },
|
||||
};
|
||||
}
|
||||
|
||||
QMap<DockerContainer, QString> ContainerUtils::containerDetailedDescriptions()
|
||||
@@ -172,7 +180,15 @@ QMap<DockerContainer, QString> ContainerUtils::containerDetailedDescriptions()
|
||||
"You will be able to access it using\n FileZilla or other SFTP clients, "
|
||||
"as well as mount the disk on your device to access\n it directly from your device.\n\n"
|
||||
"For more detailed information, you can\n find it in the support section under \"Create SFTP file storage.\" ") },
|
||||
{ DockerContainer::Socks5Proxy, QObject::tr("SOCKS5 proxy server") }
|
||||
{ DockerContainer::Socks5Proxy, QObject::tr("SOCKS5 proxy server") },
|
||||
{ DockerContainer::MtProxy,
|
||||
QObject::tr("Telegram MTProto proxy server. "
|
||||
"Allows Telegram clients to connect through your server "
|
||||
"using the MTProto protocol. Supports FakeTLS mode for "
|
||||
"bypassing DPI-based blocking.") },
|
||||
{ DockerContainer::Telemt,
|
||||
QObject::tr("Telegram MTProto proxy powered by Telemt (Rust). "
|
||||
"Supports secure and TLS fronting modes with optional traffic masking.") },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -197,6 +213,8 @@ Proto ContainerUtils::defaultProtocol(DockerContainer c)
|
||||
case DockerContainer::Dns: return Proto::Dns;
|
||||
case DockerContainer::Sftp: return Proto::Sftp;
|
||||
case DockerContainer::Socks5Proxy: return Proto::Socks5Proxy;
|
||||
case DockerContainer::MtProxy: return Proto::MtProxy;
|
||||
case DockerContainer::Telemt: return Proto::Telemt;
|
||||
default: return Proto::Unknown;
|
||||
}
|
||||
}
|
||||
@@ -224,6 +242,8 @@ bool ContainerUtils::isSupportedByCurrentPlatform(DockerContainer c)
|
||||
case DockerContainer::Awg: return true;
|
||||
case DockerContainer::Xray: return true;
|
||||
case DockerContainer::SSXray: return true;
|
||||
case DockerContainer::MtProxy: return true;
|
||||
case DockerContainer::Telemt: return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -237,7 +257,8 @@ bool ContainerUtils::isSupportedByCurrentPlatform(DockerContainer c)
|
||||
case DockerContainer::Awg: return true;
|
||||
case DockerContainer::Xray: return true;
|
||||
case DockerContainer::SSXray: return true;
|
||||
return false;
|
||||
case DockerContainer::MtProxy: return true;
|
||||
case DockerContainer::Telemt: return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -256,6 +277,8 @@ bool ContainerUtils::isSupportedByCurrentPlatform(DockerContainer c)
|
||||
case DockerContainer::Awg: return true;
|
||||
case DockerContainer::Xray: return true;
|
||||
case DockerContainer::SSXray: return true;
|
||||
case DockerContainer::MtProxy: return true;
|
||||
case DockerContainer::Telemt: return true;
|
||||
default: return false;
|
||||
}
|
||||
|
||||
@@ -318,6 +341,8 @@ bool ContainerUtils::isShareable(DockerContainer container)
|
||||
case DockerContainer::Dns: return false;
|
||||
case DockerContainer::Sftp: return false;
|
||||
case DockerContainer::Socks5Proxy: return false;
|
||||
case DockerContainer::MtProxy: return false;
|
||||
case DockerContainer::Telemt: return false;
|
||||
default: return true;
|
||||
}
|
||||
}
|
||||
@@ -346,8 +371,10 @@ int ContainerUtils::installPageOrder(DockerContainer container)
|
||||
case DockerContainer::Xray: return 3;
|
||||
case DockerContainer::Ipsec: return 7;
|
||||
case DockerContainer::SSXray: return 8;
|
||||
case DockerContainer::MtProxy:
|
||||
case DockerContainer::Telemt:
|
||||
return 20;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -99,6 +99,15 @@ namespace amnezia
|
||||
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,
|
||||
|
||||
// QFile errors
|
||||
OpenError = 1200,
|
||||
ReadError = 1201,
|
||||
|
||||
@@ -84,6 +84,13 @@ 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;
|
||||
|
||||
// QFile errors
|
||||
case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break;
|
||||
|
||||
@@ -30,7 +30,9 @@ namespace amnezia
|
||||
TorWebSite,
|
||||
Dns,
|
||||
Sftp,
|
||||
Socks5Proxy
|
||||
Socks5Proxy,
|
||||
MtProxy,
|
||||
Telemt,
|
||||
};
|
||||
Q_ENUM_NS(Proto)
|
||||
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
#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,6 +10,7 @@ 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);
|
||||
};
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#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"
|
||||
@@ -20,6 +19,8 @@
|
||||
#include "core/models/protocols/xrayProtocolConfig.h"
|
||||
#include "core/models/protocols/sftpProtocolConfig.h"
|
||||
#include "core/models/protocols/socks5ProxyProtocolConfig.h"
|
||||
#include "core/models/protocols/mtProxyProtocolConfig.h"
|
||||
#include "core/models/protocols/telemtProtocolConfig.h"
|
||||
|
||||
using namespace amnezia;
|
||||
using namespace ProtocolUtils;
|
||||
@@ -38,6 +39,8 @@ QString amnezia::scriptFolder(amnezia::DockerContainer container)
|
||||
case DockerContainer::Dns: return QLatin1String("dns");
|
||||
case DockerContainer::Sftp: return QLatin1String("sftp");
|
||||
case DockerContainer::Socks5Proxy: return QLatin1String("socks5_proxy");
|
||||
case DockerContainer::MtProxy: return QLatin1String("mtproxy");
|
||||
case DockerContainer::Telemt: return QLatin1String("telemt");
|
||||
default: return QString();
|
||||
}
|
||||
}
|
||||
@@ -284,6 +287,86 @@ amnezia::ScriptVars amnezia::genSocks5ProxyVars(const ContainerConfig &container
|
||||
return vars;
|
||||
}
|
||||
|
||||
amnezia::ScriptVars amnezia::genMtProxyVars(const ContainerConfig &containerConfig) {
|
||||
ScriptVars vars;
|
||||
|
||||
if (auto *mtProxyProtocolConfig = containerConfig.getMtProxyProtocolConfig()) {
|
||||
const MtProxyProtocolConfig &c = *mtProxyProtocolConfig;
|
||||
|
||||
vars.append({{"$MTPROXY_PORT", c.port.isEmpty() ? QString(protocols::mtProxy::defaultPort) : c.port}});
|
||||
vars.append({{"$MTPROXY_SECRET", c.secret}});
|
||||
vars.append({{"$MTPROXY_TAG", c.tag}});
|
||||
vars.append({{"$MTPROXY_TRANSPORT_MODE",
|
||||
c.transportMode.isEmpty() ? QString(protocols::mtProxy::transportModeStandard)
|
||||
: c.transportMode}});
|
||||
|
||||
QString tlsDomain = c.tlsDomain;
|
||||
if (tlsDomain.isEmpty()) {
|
||||
tlsDomain = QString(protocols::mtProxy::defaultTlsDomain);
|
||||
}
|
||||
vars.append({{"$MTPROXY_TLS_DOMAIN", tlsDomain}});
|
||||
vars.append({{"$MTPROXY_PUBLIC_HOST", c.publicHost}});
|
||||
|
||||
QStringList additionalList;
|
||||
for (const QString &s: c.additionalSecrets) {
|
||||
if (!s.isEmpty()) {
|
||||
additionalList << s;
|
||||
}
|
||||
}
|
||||
vars.append({{"$MTPROXY_ADDITIONAL_SECRETS", additionalList.join(QLatin1Char(','))}});
|
||||
|
||||
const QString workersMode = c.workersMode.isEmpty() ? QString(protocols::mtProxy::workersModeAuto)
|
||||
: c.workersMode;
|
||||
QString workers;
|
||||
if (workersMode == QLatin1String(protocols::mtProxy::workersModeManual)) {
|
||||
workers = c.workers.isEmpty() ? QString(protocols::mtProxy::defaultWorkers) : c.workers;
|
||||
} else {
|
||||
const QString transportMode =
|
||||
c.transportMode.isEmpty() ? QString(protocols::mtProxy::transportModeStandard) : c.transportMode;
|
||||
workers = (transportMode == QLatin1String(protocols::mtProxy::transportModeFakeTLS)) ? QStringLiteral("0")
|
||||
: QStringLiteral("2");
|
||||
}
|
||||
vars.append({{"$MTPROXY_WORKERS", workers}});
|
||||
|
||||
vars.append({{"$MTPROXY_NAT_ENABLED", c.natEnabled ? QStringLiteral("1") : QStringLiteral("0")}});
|
||||
vars.append({{"$MTPROXY_NAT_INTERNAL_IP", c.natInternalIp}});
|
||||
vars.append({{"$MTPROXY_NAT_EXTERNAL_IP", c.natExternalIp}});
|
||||
}
|
||||
|
||||
return vars;
|
||||
}
|
||||
|
||||
amnezia::ScriptVars amnezia::genTelemtVars(const ContainerConfig &containerConfig)
|
||||
{
|
||||
ScriptVars vars;
|
||||
|
||||
if (auto *telemtProtocolConfig = containerConfig.getTelemtProtocolConfig()) {
|
||||
const TelemtProtocolConfig &c = *telemtProtocolConfig;
|
||||
|
||||
const QString transport = c.transportMode.isEmpty() ? QString(protocols::telemt::transportModeStandard)
|
||||
: c.transportMode;
|
||||
const bool faketls = (transport == QLatin1String(protocols::telemt::transportModeFakeTLS));
|
||||
vars.append({ { "$TELEMT_TOML_SECURE", faketls ? QLatin1String("false") : QLatin1String("true") } });
|
||||
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_TAG", c.tag } });
|
||||
QString tlsDomain = c.tlsDomain;
|
||||
if (tlsDomain.isEmpty()) {
|
||||
tlsDomain = QString(protocols::telemt::defaultTlsDomain);
|
||||
}
|
||||
vars.append({ { "$TELEMT_TLS_DOMAIN", tlsDomain } });
|
||||
vars.append({ { "$TELEMT_PUBLIC_HOST", c.publicHost } });
|
||||
vars.append({ { "$TELEMT_USER_NAME",
|
||||
c.userName.isEmpty() ? QString::fromUtf8(protocols::telemt::defaultUserName) : c.userName } });
|
||||
vars.append({ { "$TELEMT_USE_MIDDLE_PROXY", c.useMiddleProxy ? QLatin1String("true") : QLatin1String("false") } });
|
||||
vars.append({ { "$TELEMT_MASK", c.maskEnabled ? QLatin1String("true") : QLatin1String("false") } });
|
||||
vars.append({ { "$TELEMT_TLS_EMULATION", c.tlsEmulation ? QLatin1String("true") : QLatin1String("false") } });
|
||||
}
|
||||
|
||||
return vars;
|
||||
}
|
||||
|
||||
amnezia::ScriptVars amnezia::genProtocolVarsForContainer(DockerContainer container, const ContainerConfig &containerConfig)
|
||||
{
|
||||
ScriptVars vars;
|
||||
@@ -308,6 +391,12 @@ amnezia::ScriptVars amnezia::genProtocolVarsForContainer(DockerContainer contain
|
||||
case Proto::Socks5Proxy:
|
||||
vars.append(genSocks5ProxyVars(containerConfig));
|
||||
break;
|
||||
case Proto::MtProxy:
|
||||
vars.append(genMtProxyVars(containerConfig));
|
||||
break;
|
||||
case Proto::Telemt:
|
||||
vars.append(genTelemtVars(containerConfig));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -67,6 +67,8 @@ ScriptVars genWireGuardVars(const ContainerConfig &containerConfig);
|
||||
ScriptVars genAwgVars(const ContainerConfig &containerConfig);
|
||||
ScriptVars genSftpVars(const ContainerConfig &containerConfig);
|
||||
ScriptVars genSocks5ProxyVars(const ContainerConfig &containerConfig);
|
||||
ScriptVars genMtProxyVars(const ContainerConfig &containerConfig);
|
||||
ScriptVars genTelemtVars(const ContainerConfig &containerConfig);
|
||||
|
||||
ScriptVars genProtocolVarsForContainer(DockerContainer container, const ContainerConfig &containerConfig);
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ namespace libssh {
|
||||
QEventLoop wait;
|
||||
connect(&watcher, &QFutureWatcher<ErrorCode>::finished, &wait, &QEventLoop::quit);
|
||||
watcher.setFuture(future);
|
||||
wait.exec();
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
|
||||
int connectionResult = watcher.result();
|
||||
|
||||
@@ -189,7 +189,7 @@ namespace libssh {
|
||||
|
||||
QEventLoop wait;
|
||||
QObject::connect(this, &Client::writeToChannelFinished, &wait, &QEventLoop::quit);
|
||||
wait.exec();
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
|
||||
return watcher.result();
|
||||
}
|
||||
@@ -284,7 +284,7 @@ namespace libssh {
|
||||
|
||||
QEventLoop wait;
|
||||
QObject::connect(this, &Client::scpFileCopyFinished, &wait, &QEventLoop::quit);
|
||||
wait.exec();
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
|
||||
closeScpSession();
|
||||
return watcher.result();
|
||||
|
||||
@@ -103,8 +103,8 @@ ErrorCode SshSession::runContainerScript(const ServerCredentials &credentials, D
|
||||
if (e)
|
||||
return e;
|
||||
|
||||
QString runner =
|
||||
QString("sudo docker exec -i $CONTAINER_NAME %2 %1 ").arg(fileName, (container == DockerContainer::Socks5Proxy ? "sh" : "bash"));
|
||||
const bool useSh = container == DockerContainer::Socks5Proxy || container == DockerContainer::MtProxy || container == DockerContainer::Telemt;
|
||||
QString runner = QString("sudo docker exec -i $CONTAINER_NAME %2 %1 ").arg(fileName, useSh ? "sh" : "bash");
|
||||
e = runScript(credentials, replaceVars(runner, amnezia::genBaseVars(credentials, container, QString(), QString())), cbReadStdOut, cbReadStdErr);
|
||||
|
||||
QString remover = QString("sudo docker exec -i $CONTAINER_NAME rm %1 ").arg(fileName);
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "android_controller.h"
|
||||
#include "android_utils.h"
|
||||
#include "ui/controllers/importUiController.h"
|
||||
#include "ui/controllers/api/pairingUiController.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -103,7 +104,10 @@ 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)}
|
||||
{"onActivityResumed", "()V", reinterpret_cast<void *>(onActivityResumed)},
|
||||
{"onCameraPermissionResult", "(Z)V", reinterpret_cast<void *>(onCameraPermissionResult)},
|
||||
{"onPairingQrCameraClosed", "()V", reinterpret_cast<void *>(onPairingQrCameraClosed)},
|
||||
{"onPairingQrCameraUserDismissed", "()V", reinterpret_cast<void *>(onPairingQrCameraUserDismissed)}
|
||||
};
|
||||
|
||||
QJniEnvironment env;
|
||||
@@ -201,6 +205,21 @@ 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");
|
||||
@@ -226,6 +245,11 @@ void AndroidController::startQrReaderActivity()
|
||||
callActivityMethod("startQrCodeReader", "()V");
|
||||
}
|
||||
|
||||
void AndroidController::startPairingQrReaderActivity()
|
||||
{
|
||||
callActivityMethod("startPairingQrCodeReader", "()V");
|
||||
}
|
||||
|
||||
void AndroidController::setSaveLogs(bool enabled)
|
||||
{
|
||||
callActivityMethod("setSaveLogs", "(Z)V", enabled);
|
||||
@@ -538,7 +562,11 @@ bool AndroidController::decodeQrCode(JNIEnv *env, jobject thiz, jstring data)
|
||||
{
|
||||
Q_UNUSED(thiz);
|
||||
|
||||
return ImportUiController::decodeQrCode(AndroidUtils::convertJString(env, data));
|
||||
const QString code = AndroidUtils::convertJString(env, data);
|
||||
if (PairingUiController::tryConsumeAndroidQrScan(code)) {
|
||||
return true;
|
||||
}
|
||||
return ImportUiController::decodeQrCode(code);
|
||||
}
|
||||
// static
|
||||
void AndroidController::onImeInsetsChanged(JNIEnv *env, jobject thiz, jint heightDp)
|
||||
@@ -578,4 +606,31 @@ 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,11 +38,15 @@ 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();
|
||||
@@ -77,6 +81,7 @@ signals:
|
||||
void systemBarsInsetsChanged(int navBarHeightDp, int statusBarHeightDp);
|
||||
void activityPaused();
|
||||
void activityResumed();
|
||||
void cameraPermissionResult(bool granted);
|
||||
|
||||
private:
|
||||
bool isWaitingStatus = true;
|
||||
@@ -109,6 +114,9 @@ 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,3 +12,4 @@ QRect QRCodeReader::cameraSize() {
|
||||
void QRCodeReader::startReading() {}
|
||||
void QRCodeReader::stopReading() {}
|
||||
void QRCodeReader::setCameraSize(QRect) {}
|
||||
void QRCodeReader::notifyCodeRead(const QString &) {}
|
||||
|
||||
@@ -16,6 +16,7 @@ public slots:
|
||||
void startReading();
|
||||
void stopReading();
|
||||
void setCameraSize(QRect value);
|
||||
void notifyCodeRead(const QString &code);
|
||||
|
||||
signals:
|
||||
void codeReaded(QString code);
|
||||
|
||||
@@ -1,16 +1,56 @@
|
||||
#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) QRCodeReader* qrCodeReader;
|
||||
@property (nonatomic, strong) AVCaptureSession *captureSession;
|
||||
@property (nonatomic, strong) AVCaptureVideoPreviewLayer *videoPreviewPlayer;
|
||||
@property (nonatomic, assign) QRCodeReader *qrCodeReader;
|
||||
@property (nonatomic, retain) AVCaptureSession *captureSession;
|
||||
@property (nonatomic, retain) AVCaptureVideoPreviewLayer *videoPreviewPlayer;
|
||||
@property (nonatomic) dispatch_queue_t sessionQueue;
|
||||
@end
|
||||
|
||||
|
||||
@@ -19,61 +59,115 @@
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
_captureSession = nil;
|
||||
self.captureSession = nil;
|
||||
if (!_sessionQueue) {
|
||||
_sessionQueue = dispatch_queue_create("org.amnezia.qr.session", DISPATCH_QUEUE_SERIAL);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setQrCodeReader: (QRCodeReader*)value {
|
||||
- (void)setQrCodeReader:(QRCodeReader *)value {
|
||||
_qrCodeReader = value;
|
||||
}
|
||||
|
||||
- (BOOL)startReading {
|
||||
NSError *error;
|
||||
- (BOOL)startReadingOnMainThread {
|
||||
[self stopReadingOnMainThread];
|
||||
|
||||
AVCaptureDevice *captureDevice = [AVCaptureDevice defaultDeviceWithMediaType: AVMediaTypeVideo];
|
||||
AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice: captureDevice error: &error];
|
||||
NSError *error = nil;
|
||||
|
||||
if(!deviceInput) {
|
||||
NSLog(@"Error %@", error.localizedDescription);
|
||||
AVCaptureDevice *captureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
|
||||
if (!captureDevice) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
_captureSession = [[AVCaptureSession alloc]init];
|
||||
[_captureSession addInput:deviceInput];
|
||||
AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice:captureDevice error:&error];
|
||||
|
||||
if (!deviceInput) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
AVCaptureSession *session = [[AVCaptureSession alloc] init];
|
||||
[session addInput:deviceInput];
|
||||
|
||||
AVCaptureMetadataOutput *capturedMetadataOutput = [[AVCaptureMetadataOutput alloc] init];
|
||||
[_captureSession addOutput:capturedMetadataOutput];
|
||||
[session addOutput:capturedMetadataOutput];
|
||||
|
||||
dispatch_queue_t dispatchQueue;
|
||||
dispatchQueue = dispatch_queue_create("myQueue", NULL);
|
||||
[capturedMetadataOutput setMetadataObjectsDelegate: self queue: dispatchQueue];
|
||||
[capturedMetadataOutput setMetadataObjectTypes: [NSArray arrayWithObject:AVMetadataObjectTypeQRCode]];
|
||||
if (!_sessionQueue) {
|
||||
_sessionQueue = dispatch_queue_create("org.amnezia.qr.session", DISPATCH_QUEUE_SERIAL);
|
||||
}
|
||||
[capturedMetadataOutput setMetadataObjectsDelegate:self queue:_sessionQueue];
|
||||
[capturedMetadataOutput setMetadataObjectTypes:[NSArray arrayWithObject:AVMetadataObjectTypeQRCode]];
|
||||
|
||||
_videoPreviewPlayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession: _captureSession];
|
||||
|
||||
CGFloat statusBarHeight = [UIApplication sharedApplication].statusBarFrame.size.height;
|
||||
self.captureSession = session;
|
||||
[session release];
|
||||
|
||||
QRect cameraRect = _qrCodeReader->cameraSize();
|
||||
CGRect cameraCGRect = CGRectMake(cameraRect.x(),
|
||||
cameraRect.y() + statusBarHeight,
|
||||
cameraRect.width(),
|
||||
cameraRect.height());
|
||||
AVCaptureVideoPreviewLayer *preview = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.captureSession];
|
||||
[preview setVideoGravity:AVLayerVideoGravityResizeAspectFill];
|
||||
self.videoPreviewPlayer = preview;
|
||||
[preview release];
|
||||
|
||||
[_videoPreviewPlayer setVideoGravity: AVLayerVideoGravityResizeAspectFill];
|
||||
[_videoPreviewPlayer setFrame: cameraCGRect];
|
||||
UIWindow *keyWindow = amneziaKeyWindowForQrCamera();
|
||||
if (!keyWindow) {
|
||||
[self stopReadingOnMainThread];
|
||||
return NO;
|
||||
}
|
||||
|
||||
CALayer* layer = [UIApplication sharedApplication].keyWindow.layer;
|
||||
[layer addSublayer: _videoPreviewPlayer];
|
||||
CGRect bounds = keyWindow.bounds;
|
||||
[self.videoPreviewPlayer setFrame:bounds];
|
||||
self.videoPreviewPlayer.zPosition = -1000.f;
|
||||
[keyWindow.layer insertSublayer:self.videoPreviewPlayer atIndex:0];
|
||||
|
||||
[_captureSession startRunning];
|
||||
AVCaptureSession *runningSession = self.captureSession;
|
||||
dispatch_async(_sessionQueue, ^{
|
||||
[runningSession startRunning];
|
||||
});
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)stopReading {
|
||||
[_captureSession stopRunning];
|
||||
_captureSession = nil;
|
||||
- (BOOL)startReading {
|
||||
if ([NSThread isMainThread]) {
|
||||
return [self startReadingOnMainThread];
|
||||
}
|
||||
__block BOOL ok = NO;
|
||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||
ok = [self startReadingOnMainThread];
|
||||
});
|
||||
return ok;
|
||||
}
|
||||
|
||||
[_videoPreviewPlayer removeFromSuperlayer];
|
||||
- (void)stopReadingOnMainThread {
|
||||
AVCaptureSession *session = self.captureSession;
|
||||
self.captureSession = nil;
|
||||
|
||||
if (session) {
|
||||
if (!_sessionQueue) {
|
||||
_sessionQueue = dispatch_queue_create("org.amnezia.qr.session", DISPATCH_QUEUE_SERIAL);
|
||||
}
|
||||
dispatch_sync(_sessionQueue, ^{
|
||||
@try {
|
||||
if ([session isRunning]) {
|
||||
[session stopRunning];
|
||||
}
|
||||
} @catch (NSException *ex) {
|
||||
NSLog(@"Session stopRunning exception: %@", ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (self.videoPreviewPlayer) {
|
||||
[self.videoPreviewPlayer removeFromSuperlayer];
|
||||
self.videoPreviewPlayer = nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)stopReading {
|
||||
if ([NSThread isMainThread]) {
|
||||
[self stopReadingOnMainThread];
|
||||
} else {
|
||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||
[self stopReadingOnMainThread];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection {
|
||||
@@ -82,7 +176,15 @@
|
||||
AVMetadataMachineReadableCodeObject *metadataObject = [metadataObjects objectAtIndex:0];
|
||||
|
||||
if ([[metadataObject type] isEqualToString: AVMetadataObjectTypeQRCode]) {
|
||||
_qrCodeReader->emit codeReaded([metadataObject stringValue].UTF8String);
|
||||
NSString *value = [metadataObject stringValue];
|
||||
if (value.length == 0) {
|
||||
return;
|
||||
}
|
||||
QRCodeReader *cpp = _qrCodeReader;
|
||||
const QByteArray utf8([value UTF8String]);
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
cpp->notifyCodeRead(QString::fromUtf8(utf8));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,6 +211,10 @@ void QRCodeReader::startReading() {
|
||||
void QRCodeReader::stopReading() {
|
||||
[m_qrCodeReader stopReading];
|
||||
}
|
||||
|
||||
void QRCodeReader::notifyCodeRead(const QString &code) {
|
||||
emit codeReaded(code);
|
||||
}
|
||||
#else
|
||||
#include "QRCodeReaderBase.h"
|
||||
|
||||
@@ -124,4 +230,5 @@ QRect QRCodeReader::cameraSize() {
|
||||
void QRCodeReader::startReading() {}
|
||||
void QRCodeReader::stopReading() {}
|
||||
void QRCodeReader::setCameraSize(QRect) {}
|
||||
void QRCodeReader::notifyCodeRead(const QString &) {}
|
||||
#endif
|
||||
|
||||
10
client/platforms/ios/iosPairingCameraAccess.h
Normal file
10
client/platforms/ios/iosPairingCameraAccess.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#ifndef IOS_PAIRING_CAMERA_ACCESS_H
|
||||
#define IOS_PAIRING_CAMERA_ACCESS_H
|
||||
|
||||
#include <functional>
|
||||
|
||||
bool amneziaIosPairingCameraAccessGranted();
|
||||
void amneziaIosRequestPairingCameraAccess(const std::function<void(bool)> &onDone);
|
||||
void amneziaIosOpenApplicationSettings();
|
||||
|
||||
#endif
|
||||
37
client/platforms/ios/iosPairingCameraAccess.mm
Normal file
37
client/platforms/ios/iosPairingCameraAccess.mm
Normal file
@@ -0,0 +1,37 @@
|
||||
#include "platforms/ios/iosPairingCameraAccess.h"
|
||||
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
bool amneziaIosPairingCameraAccessGranted()
|
||||
{
|
||||
const AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
|
||||
return status == AVAuthorizationStatusAuthorized;
|
||||
}
|
||||
|
||||
void amneziaIosRequestPairingCameraAccess(const std::function<void(bool)> &onDone)
|
||||
{
|
||||
const AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
|
||||
if (status == AVAuthorizationStatusAuthorized) {
|
||||
onDone(true);
|
||||
return;
|
||||
}
|
||||
if (status == AVAuthorizationStatusDenied || status == AVAuthorizationStatusRestricted) {
|
||||
onDone(false);
|
||||
return;
|
||||
}
|
||||
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo
|
||||
completionHandler:^(BOOL granted) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
onDone(static_cast<bool>(granted));
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
void amneziaIosOpenApplicationSettings()
|
||||
{
|
||||
NSURL *url = [NSURL URLWithString:UIApplicationOpenSettingsURLString];
|
||||
if (url != nil) {
|
||||
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
|
||||
}
|
||||
}
|
||||
13
client/platforms/ios/iosPairingCameraAccess_stub.cpp
Normal file
13
client/platforms/ios/iosPairingCameraAccess_stub.cpp
Normal file
@@ -0,0 +1,13 @@
|
||||
#include "platforms/ios/iosPairingCameraAccess.h"
|
||||
|
||||
bool amneziaIosPairingCameraAccessGranted()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void amneziaIosRequestPairingCameraAccess(const std::function<void(bool)> &onDone)
|
||||
{
|
||||
onDone(true);
|
||||
}
|
||||
|
||||
void amneziaIosOpenApplicationSettings() {}
|
||||
16
client/platforms/ios/iosPairingQrOverlayWindow.h
Normal file
16
client/platforms/ios/iosPairingQrOverlayWindow.h
Normal file
@@ -0,0 +1,16 @@
|
||||
#ifndef IOS_PAIRING_QR_OVERLAY_WINDOW_H
|
||||
#define IOS_PAIRING_QR_OVERLAY_WINDOW_H
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
using AmneziaPairingQrScannedUtf8Handler = std::function<void(const char *)>;
|
||||
using AmneziaPairingQrOverlayBackHandler = std::function<void()>;
|
||||
|
||||
void amneziaIosPairingQrOverlayPresent(AmneziaPairingQrScannedUtf8Handler onScanned, AmneziaPairingQrOverlayBackHandler onBack,
|
||||
const std::string &titleUtf8, const std::string &subtitleUtf8);
|
||||
void amneziaIosPairingQrOverlayDismiss();
|
||||
void amneziaIosPairingQrOverlaySetTorchEnabled(bool on);
|
||||
void amneziaIosPairingQrOverlayRestartCapture();
|
||||
|
||||
#endif
|
||||
836
client/platforms/ios/iosPairingQrOverlayWindow.mm
Normal file
836
client/platforms/ios/iosPairingQrOverlayWindow.mm
Normal file
@@ -0,0 +1,836 @@
|
||||
#include "platforms/ios/iosPairingQrOverlayWindow.h"
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <QuartzCore/QuartzCore.h>
|
||||
#import <math.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
static const CGFloat kAmneziaPairingQrOverlayWindowLevel = (CGFloat)UIWindowLevelAlert + 1000.f;
|
||||
|
||||
static AmneziaPairingQrScannedUtf8Handler gOnScanned;
|
||||
static AmneziaPairingQrOverlayBackHandler gOnBack;
|
||||
static UIWindow *gPairingQrOverlayWindow = nil;
|
||||
static bool gTorchRequested = false;
|
||||
static CFAbsoluteTime gPairingQrOverlayKeySince = -1.0;
|
||||
|
||||
static UIWindowScene *amneziaForegroundWindowScene(void)
|
||||
{
|
||||
for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) {
|
||||
if (scene.activationState == UISceneActivationStateForegroundActive
|
||||
&& [scene isKindOfClass:[UIWindowScene class]]) {
|
||||
return (UIWindowScene *)scene;
|
||||
}
|
||||
}
|
||||
for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) {
|
||||
if ([scene isKindOfClass:[UIWindowScene class]]) {
|
||||
return (UIWindowScene *)scene;
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
static UIWindow *amneziaPickQtAppWindowToRestore(void)
|
||||
{
|
||||
UIWindow *best = nil;
|
||||
for (UIWindow *cw in UIApplication.sharedApplication.windows) {
|
||||
if (cw == gPairingQrOverlayWindow || cw.hidden) {
|
||||
continue;
|
||||
}
|
||||
if (cw.windowScene && cw.windowLevel <= UIWindowLevelNormal + 1) {
|
||||
if (!best || cw.isKeyWindow) {
|
||||
best = cw;
|
||||
}
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
static CGFloat amneziaPairingQrBottomTabStripReserve(UIWindowScene *scene)
|
||||
{
|
||||
Class qios = NSClassFromString(@"QIOSViewController");
|
||||
if (!qios) {
|
||||
return 83.f;
|
||||
}
|
||||
for (UIWindow *cw in scene.windows) {
|
||||
if (!cw.rootViewController) {
|
||||
continue;
|
||||
}
|
||||
if ([cw.rootViewController isKindOfClass:qios]) {
|
||||
const CGFloat inset = cw.safeAreaInsets.bottom;
|
||||
const CGFloat reserve = inset + 49.f;
|
||||
return MIN(MAX(reserve, 72.f), 140.f);
|
||||
}
|
||||
}
|
||||
return 83.f;
|
||||
}
|
||||
|
||||
static void amneziaApplyReadableOverCameraShadow(UIView *v)
|
||||
{
|
||||
v.layer.shadowColor = [UIColor blackColor].CGColor;
|
||||
v.layer.shadowOffset = CGSizeMake(0, 1);
|
||||
v.layer.shadowRadius = 4;
|
||||
v.layer.shadowOpacity = 0.9;
|
||||
v.layer.masksToBounds = NO;
|
||||
}
|
||||
|
||||
static UIColor *amneziaPaleGray(void)
|
||||
{
|
||||
return [UIColor colorWithRed:(CGFloat)0xD7 / 255.0 green:(CGFloat)0xD8 / 255.0 blue:(CGFloat)0xDB / 255.0 alpha:1.0];
|
||||
}
|
||||
|
||||
static void amneziaAddCornerMinorArc(UIBezierPath *p, CGPoint C, CGFloat r, CGPoint S, CGPoint E)
|
||||
{
|
||||
const CGFloat as = atan2f((float)(S.y - C.y), (float)(S.x - C.x));
|
||||
CGFloat ae = atan2f((float)(E.y - C.y), (float)(E.x - C.x));
|
||||
while (ae - as > (CGFloat)M_PI) {
|
||||
ae -= (CGFloat)(2.0 * M_PI);
|
||||
}
|
||||
while (ae - as < (CGFloat)(-M_PI)) {
|
||||
ae += (CGFloat)(2.0 * M_PI);
|
||||
}
|
||||
const CGFloat minor = ae - as;
|
||||
const BOOL cw = minor > 0;
|
||||
[p addArcWithCenter:C radius:r startAngle:as endAngle:ae clockwise:cw];
|
||||
}
|
||||
|
||||
static UIBezierPath *amneziaScanBracketStrokePath(int corner, CGFloat x0, CGFloat y0, CGFloat s, CGFloat R, CGFloat L, CGFloat t)
|
||||
{
|
||||
const CGFloat r = MAX(1.5, R - t * 0.5);
|
||||
UIBezierPath *p = [UIBezierPath bezierPath];
|
||||
const CGFloat yy = y0 + t * 0.5f;
|
||||
const CGFloat yyb = y0 + s - t * 0.5f;
|
||||
const CGFloat xx = x0 + t * 0.5f;
|
||||
const CGFloat xxb = x0 + s - t * 0.5f;
|
||||
|
||||
switch (corner) {
|
||||
case 0: {
|
||||
const CGPoint cTL = CGPointMake(x0 + R, y0 + R);
|
||||
const CGPoint sTL = CGPointMake(x0 + R, yy);
|
||||
const CGPoint eTL = CGPointMake(xx, y0 + R);
|
||||
[p moveToPoint:CGPointMake(x0 + R + L, yy)];
|
||||
[p addLineToPoint:sTL];
|
||||
amneziaAddCornerMinorArc(p, cTL, r, sTL, eTL);
|
||||
const CGFloat yEndTL = MIN(y0 + R + L, y0 + s - R - t * 0.5f);
|
||||
[p addLineToPoint:CGPointMake(xx, MAX(yEndTL, y0 + R + 2.f))];
|
||||
} break;
|
||||
case 1: {
|
||||
const CGPoint cTR = CGPointMake(x0 + s - R, y0 + R);
|
||||
const CGPoint sTR = CGPointMake(x0 + s - R, yy);
|
||||
const CGPoint eTR = CGPointMake(xxb, y0 + R);
|
||||
[p moveToPoint:CGPointMake(x0 + s - R - L, yy)];
|
||||
[p addLineToPoint:sTR];
|
||||
amneziaAddCornerMinorArc(p, cTR, r, sTR, eTR);
|
||||
const CGFloat yEndTR = MIN(y0 + R + L, y0 + s - R - t * 0.5f);
|
||||
[p addLineToPoint:CGPointMake(xxb, MAX(yEndTR, y0 + R + 2.f))];
|
||||
} break;
|
||||
case 2: {
|
||||
const CGPoint cBL = CGPointMake(x0 + R, y0 + s - R);
|
||||
const CGPoint sBL = CGPointMake(x0 + R, yyb);
|
||||
const CGPoint eBL = CGPointMake(xx, y0 + s - R);
|
||||
[p moveToPoint:CGPointMake(x0 + R + L, yyb)];
|
||||
[p addLineToPoint:sBL];
|
||||
amneziaAddCornerMinorArc(p, cBL, r, sBL, eBL);
|
||||
const CGFloat yEndTopRef = MAX(MIN(y0 + R + L, y0 + s - R - t * 0.5f), y0 + R + 2.f);
|
||||
const CGFloat yLegBL = y0 + s + y0 - yEndTopRef;
|
||||
[p addLineToPoint:CGPointMake(xx, yLegBL)];
|
||||
} break;
|
||||
case 3: {
|
||||
const CGPoint cBR = CGPointMake(x0 + s - R, y0 + s - R);
|
||||
const CGPoint sBR = CGPointMake(x0 + s - R, yyb);
|
||||
const CGPoint eBR = CGPointMake(xxb, y0 + s - R);
|
||||
[p moveToPoint:CGPointMake(x0 + s - R - L, yyb)];
|
||||
[p addLineToPoint:sBR];
|
||||
amneziaAddCornerMinorArc(p, cBR, r, sBR, eBR);
|
||||
const CGFloat yEndTopRef = MAX(MIN(y0 + R + L, y0 + s - R - t * 0.5f), y0 + R + 2.f);
|
||||
const CGFloat yLegBR = y0 + s + y0 - yEndTopRef;
|
||||
[p addLineToPoint:CGPointMake(xxb, yLegBR)];
|
||||
} break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
@interface AmneziaPairingQrOverlayViewController : UIViewController
|
||||
@end
|
||||
|
||||
@interface AmneziaPairingQrOverlayViewController () <AVCaptureMetadataOutputObjectsDelegate>
|
||||
@property (nonatomic, strong) AVCaptureSession *captureSession;
|
||||
@property (nonatomic, strong) AVCaptureMetadataOutput *metadataOutput;
|
||||
@property (nonatomic, strong) AVCaptureVideoPreviewLayer *previewLayer;
|
||||
@property (nonatomic, strong) AVCaptureDevice *videoDevice;
|
||||
@property (nonatomic, strong) dispatch_queue_t sessionQueue;
|
||||
@property (nonatomic, strong) UIView *cameraContainer;
|
||||
@property (nonatomic, strong) UIView *headerContainer;
|
||||
@property (nonatomic, strong) UIButton *backButton;
|
||||
@property (nonatomic, strong) UILabel *titleLabel;
|
||||
@property (nonatomic, strong) UILabel *subtitleLabel;
|
||||
@property (nonatomic, strong) UIButton *torchButton;
|
||||
@property (nonatomic, strong) NSLayoutConstraint *torchCenterYConstraint;
|
||||
@property (nonatomic, copy) NSString *chromeTitleText;
|
||||
@property (nonatomic, copy) NSString *chromeSubtitleText;
|
||||
@property (nonatomic, strong) UIView *scanDimView;
|
||||
@property (nonatomic, strong) CAShapeLayer *scanDimMaskLayer;
|
||||
@property (nonatomic, strong) UIView *scanHoleFillView;
|
||||
@property (nonatomic, strong) CAShapeLayer *scanHoleHighlightLayer;
|
||||
@property (nonatomic, strong) UIView *bracketContainer;
|
||||
@property (nonatomic, strong) NSMutableArray<CAShapeLayer *> *bracketCornerLayers;
|
||||
@end
|
||||
|
||||
@implementation AmneziaPairingQrOverlayViewController
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
self.view.backgroundColor = [UIColor clearColor];
|
||||
if (!self.sessionQueue) {
|
||||
self.sessionQueue = dispatch_queue_create("org.amnezia.pairingqr.overlay", DISPATCH_QUEUE_SERIAL);
|
||||
}
|
||||
[self buildChromeUi];
|
||||
}
|
||||
|
||||
- (void)buildChromeUi
|
||||
{
|
||||
if (self.headerContainer) {
|
||||
return;
|
||||
}
|
||||
UIView *cam = [[UIView alloc] init];
|
||||
cam.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
cam.backgroundColor = [UIColor clearColor];
|
||||
cam.clipsToBounds = YES;
|
||||
self.cameraContainer = cam;
|
||||
[self.view addSubview:cam];
|
||||
|
||||
UIView *holeFill = [[UIView alloc] init];
|
||||
holeFill.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
holeFill.backgroundColor = [UIColor clearColor];
|
||||
holeFill.opaque = NO;
|
||||
holeFill.userInteractionEnabled = NO;
|
||||
self.scanHoleFillView = holeFill;
|
||||
CAShapeLayer *hi = [CAShapeLayer layer];
|
||||
hi.fillColor = [UIColor colorWithWhite:1.0 alpha:0.14].CGColor;
|
||||
hi.strokeColor = nil;
|
||||
[holeFill.layer addSublayer:hi];
|
||||
self.scanHoleHighlightLayer = hi;
|
||||
[self.view addSubview:holeFill];
|
||||
|
||||
UIView *dim = [[UIView alloc] init];
|
||||
dim.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
dim.backgroundColor = [UIColor colorWithWhite:0.02 alpha:0.55];
|
||||
dim.userInteractionEnabled = NO;
|
||||
dim.opaque = NO;
|
||||
self.scanDimView = dim;
|
||||
[self.view addSubview:dim];
|
||||
|
||||
CAShapeLayer *dimMask = [CAShapeLayer layer];
|
||||
dimMask.fillRule = kCAFillRuleEvenOdd;
|
||||
dimMask.fillColor = [UIColor blackColor].CGColor;
|
||||
dim.layer.mask = dimMask;
|
||||
self.scanDimMaskLayer = dimMask;
|
||||
|
||||
UIView *bracketHost = [[UIView alloc] init];
|
||||
bracketHost.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
bracketHost.backgroundColor = [UIColor clearColor];
|
||||
bracketHost.opaque = NO;
|
||||
bracketHost.userInteractionEnabled = NO;
|
||||
self.bracketContainer = bracketHost;
|
||||
[self.view addSubview:bracketHost];
|
||||
|
||||
self.bracketCornerLayers = [NSMutableArray arrayWithCapacity:4];
|
||||
for (NSInteger i = 0; i < 4; i++) {
|
||||
CAShapeLayer *sl = [CAShapeLayer layer];
|
||||
sl.fillColor = nil;
|
||||
sl.strokeColor = [UIColor colorWithWhite:0.94 alpha:1].CGColor;
|
||||
sl.lineWidth = 5.0;
|
||||
sl.lineCap = kCALineCapRound;
|
||||
sl.lineJoin = kCALineJoinRound;
|
||||
[bracketHost.layer addSublayer:sl];
|
||||
[self.bracketCornerLayers addObject:sl];
|
||||
}
|
||||
|
||||
UIView *header = [[UIView alloc] init];
|
||||
header.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
header.backgroundColor = [UIColor clearColor];
|
||||
header.opaque = NO;
|
||||
header.userInteractionEnabled = YES;
|
||||
self.headerContainer = header;
|
||||
[self.view addSubview:header];
|
||||
|
||||
UIButton *back = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
back.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
back.tintColor = amneziaPaleGray();
|
||||
if (@available(iOS 13.0, *)) {
|
||||
const CGFloat kBackArrowPt = 20.0;
|
||||
UIImageSymbolConfiguration *sym =
|
||||
[UIImageSymbolConfiguration configurationWithPointSize:kBackArrowPt weight:UIImageSymbolWeightMedium
|
||||
scale:UIImageSymbolScaleDefault];
|
||||
UIImage *img = [UIImage systemImageNamed:@"arrow.left" withConfiguration:sym];
|
||||
[back setImage:[img imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] forState:UIControlStateNormal];
|
||||
} else {
|
||||
[back setTitle:@"<" forState:UIControlStateNormal];
|
||||
}
|
||||
[back addTarget:self action:@selector(backTapped) forControlEvents:UIControlEventTouchUpInside];
|
||||
self.backButton = back;
|
||||
[header addSubview:back];
|
||||
|
||||
UILabel *title = [[UILabel alloc] init];
|
||||
title.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
title.textColor = [UIColor colorWithWhite:0.96 alpha:1];
|
||||
title.font = [UIFont systemFontOfSize:22 weight:UIFontWeightBold];
|
||||
title.numberOfLines = 0;
|
||||
title.text = self.chromeTitleText.length ? self.chromeTitleText : @"Add device via QR";
|
||||
self.titleLabel = title;
|
||||
[header addSubview:title];
|
||||
amneziaApplyReadableOverCameraShadow(title);
|
||||
|
||||
UILabel *sub = [[UILabel alloc] init];
|
||||
sub.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
sub.textColor = [UIColor colorWithWhite:0.88 alpha:0.95];
|
||||
sub.font = [UIFont systemFontOfSize:14 weight:UIFontWeightRegular];
|
||||
sub.numberOfLines = 0;
|
||||
sub.text = self.chromeSubtitleText.length
|
||||
? self.chromeSubtitleText
|
||||
: @"Scan the session QR shown on the device you want to add.";
|
||||
self.subtitleLabel = sub;
|
||||
[header addSubview:sub];
|
||||
amneziaApplyReadableOverCameraShadow(sub);
|
||||
|
||||
UIButton *torch = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
torch.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[torch setTitle:@"🔦" forState:UIControlStateNormal];
|
||||
torch.titleLabel.font = [UIFont systemFontOfSize:26];
|
||||
torch.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.22];
|
||||
torch.layer.cornerRadius = 28;
|
||||
torch.clipsToBounds = YES;
|
||||
[torch addTarget:self action:@selector(torchTapped) forControlEvents:UIControlEventTouchUpInside];
|
||||
self.torchButton = torch;
|
||||
[self.view addSubview:torch];
|
||||
|
||||
UILayoutGuide *safe = self.view.safeAreaLayoutGuide;
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[cam.topAnchor constraintEqualToAnchor:self.view.topAnchor],
|
||||
[cam.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
|
||||
[cam.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
|
||||
[cam.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor],
|
||||
|
||||
[holeFill.topAnchor constraintEqualToAnchor:self.view.topAnchor],
|
||||
[holeFill.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
|
||||
[holeFill.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
|
||||
[holeFill.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor],
|
||||
|
||||
[dim.topAnchor constraintEqualToAnchor:self.view.topAnchor],
|
||||
[dim.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
|
||||
[dim.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
|
||||
[dim.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor],
|
||||
|
||||
[bracketHost.topAnchor constraintEqualToAnchor:self.view.topAnchor],
|
||||
[bracketHost.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
|
||||
[bracketHost.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
|
||||
[bracketHost.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor],
|
||||
|
||||
[header.topAnchor constraintEqualToAnchor:safe.topAnchor],
|
||||
[header.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
|
||||
[header.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
|
||||
[header.heightAnchor constraintGreaterThanOrEqualToConstant:120],
|
||||
|
||||
[back.leadingAnchor constraintEqualToAnchor:header.leadingAnchor constant:8],
|
||||
[back.topAnchor constraintEqualToAnchor:header.topAnchor constant:20],
|
||||
[back.widthAnchor constraintEqualToConstant:40],
|
||||
[back.heightAnchor constraintEqualToConstant:40],
|
||||
|
||||
[title.leadingAnchor constraintEqualToAnchor:header.leadingAnchor constant:16],
|
||||
[title.trailingAnchor constraintEqualToAnchor:header.trailingAnchor constant:-16],
|
||||
[title.topAnchor constraintEqualToAnchor:back.bottomAnchor],
|
||||
|
||||
[sub.leadingAnchor constraintEqualToAnchor:title.leadingAnchor],
|
||||
[sub.trailingAnchor constraintEqualToAnchor:title.trailingAnchor],
|
||||
[sub.topAnchor constraintEqualToAnchor:title.bottomAnchor constant:8],
|
||||
[sub.bottomAnchor constraintEqualToAnchor:header.bottomAnchor constant:-10],
|
||||
|
||||
[torch.topAnchor constraintGreaterThanOrEqualToAnchor:header.bottomAnchor constant:8],
|
||||
[torch.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor],
|
||||
[torch.widthAnchor constraintEqualToConstant:56],
|
||||
[torch.heightAnchor constraintEqualToConstant:56],
|
||||
]];
|
||||
NSLayoutConstraint *torchCy = [torch.centerYAnchor constraintEqualToAnchor:self.view.topAnchor constant:200.0];
|
||||
self.torchCenterYConstraint = torchCy;
|
||||
torchCy.active = YES;
|
||||
[header setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical];
|
||||
[header setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical];
|
||||
}
|
||||
|
||||
- (void)applyMetadataRectOfInterestForScanHole:(CGRect)holeInScanDimBounds
|
||||
{
|
||||
if (!self.previewLayer || !self.metadataOutput || !self.scanDimView || !self.cameraContainer) {
|
||||
return;
|
||||
}
|
||||
if (CGRectIsEmpty(holeInScanDimBounds) || holeInScanDimBounds.size.width < 24.0 || holeInScanDimBounds.size.height < 24.0) {
|
||||
return;
|
||||
}
|
||||
|
||||
CGRect holeInCam = [self.scanDimView convertRect:holeInScanDimBounds toView:self.cameraContainer];
|
||||
holeInCam = CGRectIntersection(holeInCam, self.cameraContainer.bounds);
|
||||
if (CGRectIsEmpty(holeInCam)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const CGRect plFrame = self.previewLayer.frame;
|
||||
CGRect holeInPreview = CGRectOffset(holeInCam, -plFrame.origin.x, -plFrame.origin.y);
|
||||
holeInPreview = CGRectIntersection(holeInPreview, self.previewLayer.bounds);
|
||||
if (CGRectIsEmpty(holeInPreview)) {
|
||||
return;
|
||||
}
|
||||
|
||||
CGRect roi = [self.previewLayer metadataOutputRectOfInterestForRect:holeInPreview];
|
||||
roi.origin.x = MAX(0.0, MIN(1.0, roi.origin.x));
|
||||
roi.origin.y = MAX(0.0, MIN(1.0, roi.origin.y));
|
||||
roi.size.width = MAX(0.02, MIN(1.0 - roi.origin.x, roi.size.width));
|
||||
roi.size.height = MAX(0.02, MIN(1.0 - roi.origin.y, roi.size.height));
|
||||
|
||||
AVCaptureMetadataOutput *mo = self.metadataOutput;
|
||||
dispatch_queue_t sq = self.sessionQueue;
|
||||
if (!mo || !sq) {
|
||||
return;
|
||||
}
|
||||
dispatch_async(sq, ^{
|
||||
mo.rectOfInterest = roi;
|
||||
});
|
||||
}
|
||||
|
||||
- (void)layoutScanOverlayGeometry
|
||||
{
|
||||
if (!self.scanDimView || !self.scanDimMaskLayer || !self.scanHoleHighlightLayer || self.bracketCornerLayers.count != 4) {
|
||||
return;
|
||||
}
|
||||
const CGRect vb = self.scanDimView.bounds;
|
||||
if (vb.size.width < 32 || vb.size.height < 32) {
|
||||
return;
|
||||
}
|
||||
|
||||
CGFloat sqSz = floor(MIN(vb.size.width, vb.size.height) * 0.72);
|
||||
CGFloat sqX = (vb.size.width - sqSz) / 2.0;
|
||||
CGFloat sqY = (vb.size.height - sqSz) / 2.0;
|
||||
|
||||
CGFloat headerBottom = CGRectGetMaxY(self.headerContainer.frame);
|
||||
if (headerBottom < 8.0) {
|
||||
headerBottom = 132.0 + self.view.safeAreaInsets.top;
|
||||
}
|
||||
sqY = MAX(sqY, headerBottom + 8.0);
|
||||
|
||||
const CGFloat kBottomBandForTorch = 80.0;
|
||||
const CGFloat maxHoleBottom = vb.size.height - kBottomBandForTorch;
|
||||
if (sqY + sqSz > maxHoleBottom) {
|
||||
sqY = maxHoleBottom - sqSz;
|
||||
sqY = MAX(sqY, headerBottom + 8.0);
|
||||
}
|
||||
|
||||
sqX = MAX(8.0, MIN(sqX, vb.size.width - sqSz - 8.0));
|
||||
sqY = MAX(headerBottom + 4.0, MIN(sqY, vb.size.height - sqSz - 8.0));
|
||||
|
||||
const CGRect hole = CGRectMake(sqX, sqY, sqSz, sqSz);
|
||||
CGFloat holeR = MIN(28.0, MAX(10.0, sqSz * 0.056));
|
||||
{
|
||||
const CGFloat half = 0.5 * MIN(hole.size.width, hole.size.height);
|
||||
holeR = MIN(holeR, MAX(6.0, half - 2.0));
|
||||
}
|
||||
UIBezierPath *holeRoundPath = [UIBezierPath bezierPathWithRoundedRect:hole cornerRadius:holeR];
|
||||
|
||||
UIBezierPath *path = [UIBezierPath bezierPathWithRect:vb];
|
||||
[path appendPath:holeRoundPath];
|
||||
self.scanDimMaskLayer.frame = vb;
|
||||
self.scanDimMaskLayer.path = path.CGPath;
|
||||
|
||||
self.scanHoleHighlightLayer.frame = CGRectMake(0, 0, vb.size.width, vb.size.height);
|
||||
self.scanHoleHighlightLayer.path = holeRoundPath.CGPath;
|
||||
|
||||
const CGFloat bracketThick = 5.0;
|
||||
const CGFloat bracketLen = (CGFloat)MAX(28, (NSInteger)floor(sqSz * 0.13));
|
||||
const CGFloat x0 = hole.origin.x;
|
||||
const CGFloat y0 = hole.origin.y;
|
||||
const CGFloat s = hole.size.width;
|
||||
|
||||
const CGFloat t = bracketThick;
|
||||
const CGFloat L = bracketLen;
|
||||
|
||||
for (NSUInteger i = 0; i < 4; i++) {
|
||||
CAShapeLayer *layer = self.bracketCornerLayers[i];
|
||||
layer.lineWidth = t;
|
||||
layer.path = amneziaScanBracketStrokePath((int)i, x0, y0, s, holeR, L, t).CGPath;
|
||||
}
|
||||
|
||||
if (self.torchCenterYConstraint && self.torchButton) {
|
||||
const CGFloat holeBottom = CGRectGetMaxY(hole);
|
||||
const CGFloat bandBottom = vb.size.height;
|
||||
const CGFloat torchH = 56.0;
|
||||
CGFloat torchCenterY = (holeBottom + bandBottom) * 0.5;
|
||||
const CGFloat minC = holeBottom + torchH * 0.5 + 6.0;
|
||||
const CGFloat maxC = bandBottom - torchH * 0.5 - MAX(6.0, self.view.safeAreaInsets.bottom);
|
||||
torchCenterY = MAX(minC, MIN(maxC, torchCenterY));
|
||||
if (minC > maxC) {
|
||||
torchCenterY = (minC + maxC) * 0.5;
|
||||
}
|
||||
const CGFloat hdr = headerBottom + torchH * 0.5 + 10.0;
|
||||
torchCenterY = MAX(torchCenterY, hdr);
|
||||
self.torchCenterYConstraint.constant = torchCenterY;
|
||||
}
|
||||
|
||||
[self applyMetadataRectOfInterestForScanHole:hole];
|
||||
}
|
||||
|
||||
- (void)backTapped
|
||||
{
|
||||
if (gOnBack) {
|
||||
gOnBack();
|
||||
}
|
||||
}
|
||||
|
||||
- (void)torchTapped
|
||||
{
|
||||
gTorchRequested = !gTorchRequested;
|
||||
[self applyTorchFromGlobalFlag];
|
||||
if (gTorchRequested) {
|
||||
self.torchButton.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.42];
|
||||
self.torchButton.layer.borderWidth = 2;
|
||||
self.torchButton.layer.borderColor = [UIColor colorWithRed:1 green:0.75 blue:0.45 alpha:1].CGColor;
|
||||
} else {
|
||||
self.torchButton.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.22];
|
||||
self.torchButton.layer.borderWidth = 0;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewDidLayoutSubviews
|
||||
{
|
||||
[super viewDidLayoutSubviews];
|
||||
if (self.previewLayer && self.cameraContainer) {
|
||||
self.previewLayer.frame = self.cameraContainer.bounds;
|
||||
}
|
||||
[self layoutScanOverlayGeometry];
|
||||
if (self.scanHoleFillView) {
|
||||
[self.view bringSubviewToFront:self.scanHoleFillView];
|
||||
}
|
||||
if (self.scanDimView) {
|
||||
[self.view bringSubviewToFront:self.scanDimView];
|
||||
}
|
||||
if (self.bracketContainer) {
|
||||
[self.view bringSubviewToFront:self.bracketContainer];
|
||||
}
|
||||
if (self.headerContainer) {
|
||||
[self.view bringSubviewToFront:self.headerContainer];
|
||||
}
|
||||
if (self.torchButton) {
|
||||
[self.view bringSubviewToFront:self.torchButton];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applyTorchOnMainThread:(BOOL)on
|
||||
{
|
||||
AVCaptureDevice *device = self.videoDevice;
|
||||
if (!device || ![device hasTorch]) {
|
||||
if (on && gTorchRequested) {
|
||||
__unsafe_unretained AmneziaPairingQrOverlayViewController *unsafeSelf = self;
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.12 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
AmneziaPairingQrOverlayViewController *strongSelf = unsafeSelf;
|
||||
if (strongSelf && gTorchRequested) {
|
||||
[strongSelf applyTorchOnMainThread:YES];
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
AVCaptureSession *session = self.captureSession;
|
||||
if (on && session && ![session isRunning]) {
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
if (gTorchRequested) {
|
||||
[self applyTorchOnMainThread:YES];
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
NSError *err = nil;
|
||||
if (![device lockForConfiguration:&err]) {
|
||||
return;
|
||||
}
|
||||
if (on) {
|
||||
err = nil;
|
||||
if (![device setTorchModeOnWithLevel:AVCaptureMaxAvailableTorchLevel error:&err]) {
|
||||
if ([device isTorchModeSupported:AVCaptureTorchModeOn]) {
|
||||
device.torchMode = AVCaptureTorchModeOn;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
device.torchMode = AVCaptureTorchModeOff;
|
||||
}
|
||||
[device unlockForConfiguration];
|
||||
}
|
||||
|
||||
- (void)applyTorchFromGlobalFlag
|
||||
{
|
||||
[self applyTorchOnMainThread:gTorchRequested ? YES : NO];
|
||||
}
|
||||
|
||||
- (void)stopCapturePipelineOnMainThread
|
||||
{
|
||||
[self applyTorchOnMainThread:NO];
|
||||
self.videoDevice = nil;
|
||||
|
||||
AVCaptureSession *session = self.captureSession;
|
||||
self.captureSession = nil;
|
||||
self.metadataOutput = nil;
|
||||
|
||||
if (self.previewLayer) {
|
||||
[self.previewLayer removeFromSuperlayer];
|
||||
self.previewLayer = nil;
|
||||
}
|
||||
|
||||
if (session) {
|
||||
dispatch_queue_t q = self.sessionQueue;
|
||||
if (!q) {
|
||||
q = dispatch_queue_create("org.amnezia.pairingqr.overlay", DISPATCH_QUEUE_SERIAL);
|
||||
self.sessionQueue = q;
|
||||
}
|
||||
dispatch_sync(q, ^{
|
||||
@try {
|
||||
if ([session isRunning]) {
|
||||
[session stopRunning];
|
||||
}
|
||||
} @catch (NSException *ex) {
|
||||
NSLog(@"Stop running exception: %@", ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)startCapturePipelineOnMainThread
|
||||
{
|
||||
|
||||
[self stopCapturePipelineOnMainThread];
|
||||
|
||||
if (!self.cameraContainer) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSError *error = nil;
|
||||
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
|
||||
if (!device) {
|
||||
return NO;
|
||||
}
|
||||
AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:device error:&error];
|
||||
if (!input) {
|
||||
return NO;
|
||||
}
|
||||
self.videoDevice = device;
|
||||
|
||||
AVCaptureSession *session = [[AVCaptureSession alloc] init];
|
||||
if ([session canSetSessionPreset:AVCaptureSessionPresetHigh]) {
|
||||
session.sessionPreset = AVCaptureSessionPresetHigh;
|
||||
}
|
||||
|
||||
[session addInput:input];
|
||||
|
||||
AVCaptureMetadataOutput *meta = [[AVCaptureMetadataOutput alloc] init];
|
||||
if (![session canAddOutput:meta]) {
|
||||
return NO;
|
||||
}
|
||||
[session addOutput:meta];
|
||||
dispatch_queue_t q = self.sessionQueue;
|
||||
if (!q) {
|
||||
q = dispatch_queue_create("org.amnezia.pairingqr.overlay", DISPATCH_QUEUE_SERIAL);
|
||||
self.sessionQueue = q;
|
||||
}
|
||||
[meta setMetadataObjectsDelegate:self queue:q];
|
||||
meta.metadataObjectTypes = @[ AVMetadataObjectTypeQRCode ];
|
||||
|
||||
self.captureSession = session;
|
||||
self.metadataOutput = meta;
|
||||
|
||||
AVCaptureVideoPreviewLayer *preview = [[AVCaptureVideoPreviewLayer alloc] initWithSession:session];
|
||||
preview.videoGravity = AVLayerVideoGravityResizeAspectFill;
|
||||
self.previewLayer = preview;
|
||||
[self.cameraContainer.layer insertSublayer:preview atIndex:0];
|
||||
preview.frame = self.cameraContainer.bounds;
|
||||
|
||||
[self.view layoutIfNeeded];
|
||||
[self layoutScanOverlayGeometry];
|
||||
|
||||
AVCaptureSession *runningSession = session;
|
||||
__unsafe_unretained AmneziaPairingQrOverlayViewController *weakSelf = self;
|
||||
dispatch_async(q, ^{
|
||||
@try {
|
||||
[runningSession startRunning];
|
||||
} @catch (NSException *ex) {
|
||||
NSLog(@"Start running exception: %@", ex);
|
||||
}
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
AmneziaPairingQrOverlayViewController *strongSelf = weakSelf;
|
||||
if (!strongSelf) {
|
||||
return;
|
||||
}
|
||||
[strongSelf applyTorchFromGlobalFlag];
|
||||
});
|
||||
});
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)captureOutput:(AVCaptureOutput *)output
|
||||
didOutputMetadataObjects:(NSArray<__kindof AVMetadataMachineReadableCodeObject *> *)metadataObjects
|
||||
fromConnection:(AVCaptureConnection *)connection
|
||||
{
|
||||
(void)output;
|
||||
(void)connection;
|
||||
for (AVMetadataMachineReadableCodeObject *obj in metadataObjects) {
|
||||
NSString *value = obj.stringValue;
|
||||
if (value.length == 0) {
|
||||
continue;
|
||||
}
|
||||
const char *utf8 = value.UTF8String;
|
||||
std::string copy(utf8 ? utf8 : "");
|
||||
if (copy.empty()) {
|
||||
continue;
|
||||
}
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (gOnScanned) {
|
||||
gOnScanned(copy.c_str());
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
static void amneziaPairingQrOverlayTeardownOnMain(void)
|
||||
{
|
||||
UIWindow *w = gPairingQrOverlayWindow;
|
||||
gPairingQrOverlayWindow = nil;
|
||||
gOnScanned = nullptr;
|
||||
gOnBack = nullptr;
|
||||
gTorchRequested = false;
|
||||
gPairingQrOverlayKeySince = -1.0;
|
||||
|
||||
if (w) {
|
||||
UIViewController *root = w.rootViewController;
|
||||
w.rootViewController = nil;
|
||||
w.hidden = YES;
|
||||
if ([root isKindOfClass:[AmneziaPairingQrOverlayViewController class]]) {
|
||||
[(AmneziaPairingQrOverlayViewController *)root stopCapturePipelineOnMainThread];
|
||||
}
|
||||
}
|
||||
|
||||
UIWindow *restore = amneziaPickQtAppWindowToRestore();
|
||||
if (restore) {
|
||||
[restore makeKeyWindow];
|
||||
} else {
|
||||
}
|
||||
}
|
||||
|
||||
void amneziaIosPairingQrOverlayPresent(AmneziaPairingQrScannedUtf8Handler onScanned, AmneziaPairingQrOverlayBackHandler onBack,
|
||||
const std::string &titleUtf8, const std::string &subtitleUtf8)
|
||||
{
|
||||
const bool hasScan = static_cast<bool>(onScanned);
|
||||
const bool hasBack = static_cast<bool>(onBack);
|
||||
AmneziaPairingQrScannedUtf8Handler scanH = std::move(onScanned);
|
||||
AmneziaPairingQrOverlayBackHandler backH = std::move(onBack);
|
||||
const std::string titleCopy = titleUtf8;
|
||||
const std::string subCopy = subtitleUtf8;
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
|
||||
amneziaPairingQrOverlayTeardownOnMain();
|
||||
gOnScanned = std::move(scanH);
|
||||
gOnBack = std::move(backH);
|
||||
|
||||
UIWindowScene *scene = amneziaForegroundWindowScene();
|
||||
if (!scene) {
|
||||
gOnScanned = nullptr;
|
||||
gOnBack = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
const CGFloat bottomReserve = amneziaPairingQrBottomTabStripReserve(scene);
|
||||
const CGRect sceneBounds = scene.coordinateSpace.bounds;
|
||||
const CGRect overlayFrame = CGRectMake(0, 0, sceneBounds.size.width, sceneBounds.size.height - bottomReserve);
|
||||
|
||||
AmneziaPairingQrOverlayViewController *vc = [[AmneziaPairingQrOverlayViewController alloc] init];
|
||||
NSString *nsTitle = titleCopy.empty() ? nil : [NSString stringWithUTF8String:titleCopy.c_str()];
|
||||
NSString *nsSub = subCopy.empty() ? nil : [NSString stringWithUTF8String:subCopy.c_str()];
|
||||
vc.chromeTitleText = nsTitle;
|
||||
vc.chromeSubtitleText = nsSub;
|
||||
|
||||
UIWindow *w = [[UIWindow alloc] initWithWindowScene:scene];
|
||||
w.frame = overlayFrame;
|
||||
w.windowLevel = kAmneziaPairingQrOverlayWindowLevel;
|
||||
w.backgroundColor = [UIColor blackColor];
|
||||
w.rootViewController = vc;
|
||||
gPairingQrOverlayWindow = w;
|
||||
|
||||
[w makeKeyAndVisible];
|
||||
[w layoutIfNeeded];
|
||||
[vc.view setNeedsLayout];
|
||||
[vc.view layoutIfNeeded];
|
||||
|
||||
gPairingQrOverlayKeySince = CFAbsoluteTimeGetCurrent();
|
||||
|
||||
if (![vc startCapturePipelineOnMainThread]) {
|
||||
NSLog(@"Start capture failed");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void amneziaIosPairingQrOverlayDismiss()
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
amneziaPairingQrOverlayTeardownOnMain();
|
||||
});
|
||||
}
|
||||
|
||||
void amneziaIosPairingQrOverlaySetTorchEnabled(bool on)
|
||||
{
|
||||
gTorchRequested = on;
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
UIWindow *win = gPairingQrOverlayWindow;
|
||||
if (!win) {
|
||||
return;
|
||||
}
|
||||
UIViewController *root = win.rootViewController;
|
||||
if ([root isKindOfClass:[AmneziaPairingQrOverlayViewController class]]) {
|
||||
AmneziaPairingQrOverlayViewController *vc = (AmneziaPairingQrOverlayViewController *)root;
|
||||
[vc applyTorchFromGlobalFlag];
|
||||
if (vc.torchButton) {
|
||||
if (on) {
|
||||
vc.torchButton.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.42];
|
||||
vc.torchButton.layer.borderWidth = 2;
|
||||
vc.torchButton.layer.borderColor = [UIColor colorWithRed:1 green:0.75 blue:0.45 alpha:1].CGColor;
|
||||
} else {
|
||||
vc.torchButton.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.18];
|
||||
vc.torchButton.layer.borderWidth = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void amneziaIosPairingQrOverlayRestartCapture()
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
const CFAbsoluteTime now = CFAbsoluteTimeGetCurrent();
|
||||
if (gPairingQrOverlayKeySince > 0 && (now - gPairingQrOverlayKeySince) < 1.0) {
|
||||
return;
|
||||
}
|
||||
UIWindow *w = gPairingQrOverlayWindow;
|
||||
if (!w) {
|
||||
return;
|
||||
}
|
||||
UIViewController *root = w.rootViewController;
|
||||
if (![root isKindOfClass:[AmneziaPairingQrOverlayViewController class]]) {
|
||||
return;
|
||||
}
|
||||
AmneziaPairingQrOverlayViewController *vc = (AmneziaPairingQrOverlayViewController *)root;
|
||||
[vc stopCapturePipelineOnMainThread];
|
||||
if (![vc startCapturePipelineOnMainThread]) {
|
||||
NSLog(@"Restart startCapture failed");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -220,7 +220,7 @@ bool IosController::connectVpn(amnezia::Proto proto, const QJsonObject& configur
|
||||
m_rawConfig = configuration;
|
||||
m_serverAddress = configuration.value(configKey::hostName).toString().toNSString();
|
||||
|
||||
const QString serverDescription = configuration.value(config_key::description).toString().trimmed();
|
||||
const QString serverDescription = configuration.value(configKey::description).toString().trimmed();
|
||||
QString tunnelName;
|
||||
if (serverDescription.isEmpty()) {
|
||||
tunnelName = ProtocolUtils::protoToString(proto);
|
||||
|
||||
@@ -97,8 +97,8 @@ void SecureQSettings::setValue(const QString &key, const QVariant &value)
|
||||
QByteArray encryptedValue = encryptText(decryptedValue);
|
||||
m_settings.setValue(key, magicString + encryptedValue);
|
||||
} else {
|
||||
qCritical() << "SecureQSettings::setValue Keystore unavailable, storing unencrypted";
|
||||
m_settings.setValue(key, value);
|
||||
qCritical() << "SecureQSettings::setValue Encryption required, but key is empty";
|
||||
return;
|
||||
}
|
||||
|
||||
} else {
|
||||
@@ -211,7 +211,7 @@ bool SecureQSettings::encryptionRequired() const
|
||||
// QtKeyChain failing on Linux
|
||||
return false;
|
||||
#endif
|
||||
return m_keystoreAvailable;
|
||||
return true;
|
||||
}
|
||||
|
||||
QByteArray SecureQSettings::getEncKey() const
|
||||
@@ -237,7 +237,6 @@ QByteArray SecureQSettings::getEncKey() const
|
||||
m_key = getSecTag(settingsKeyTag);
|
||||
if (key != m_key) {
|
||||
qCritical() << "SecureQSettings::getEncKey Unable to store key in keychain" << key.size() << m_key.size();
|
||||
m_keystoreAvailable = false;
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -266,7 +265,6 @@ QByteArray SecureQSettings::getEncIv() const
|
||||
m_iv = getSecTag(settingsIvTag);
|
||||
if (iv != m_iv) {
|
||||
qCritical() << "SecureQSettings::getEncIv Unable to store IV in keychain" << iv.size() << m_iv.size();
|
||||
m_keystoreAvailable = false;
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,6 @@ private:
|
||||
|
||||
mutable QByteArray m_key;
|
||||
mutable QByteArray m_iv;
|
||||
mutable bool m_keystoreAvailable = true;
|
||||
|
||||
const QByteArray magicString { "EncData" }; // Magic keyword used for mark encrypted QByteArray
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
if which apt-get > /dev/null 2>&1; then pm=$(which apt-get); silent_inst="-yq install"; check_pkgs="-yq update"; docker_pkg="docker.io"; dist="debian";\
|
||||
if which apt-get > /dev/null 2>&1; then pm=$(which apt-get); silent_inst="-yq install --install-recommends"; check_pkgs="-yq update"; docker_pkg="docker.io"; dist="debian";\
|
||||
elif which dnf > /dev/null 2>&1; then pm=$(which dnf); silent_inst="-yq install"; check_pkgs="-yq check-update"; docker_pkg="docker"; dist="fedora";\
|
||||
elif which yum > /dev/null 2>&1; then pm=$(which yum); silent_inst="-y -q install"; check_pkgs="-y -q check-update"; docker_pkg="docker"; dist="centos";\
|
||||
elif which zypper > /dev/null 2>&1; then pm=$(which zypper); silent_inst="-nq install"; check_pkgs="-nq refresh"; docker_pkg="docker"; dist="opensuse";\
|
||||
|
||||
9
client/server_scripts/mtproxy/Dockerfile
Normal file
9
client/server_scripts/mtproxy/Dockerfile
Normal file
@@ -0,0 +1,9 @@
|
||||
FROM amneziavpn/mtproxy:latest
|
||||
|
||||
RUN mkdir -p /opt/amnezia /data
|
||||
RUN printf '#!/bin/sh\ntail -f /dev/null\n' > /opt/amnezia/start.sh && \
|
||||
chmod a+x /opt/amnezia/start.sh
|
||||
|
||||
VOLUME /data
|
||||
ENTRYPOINT ["/bin/sh", "/opt/amnezia/start.sh"]
|
||||
CMD [""]
|
||||
60
client/server_scripts/mtproxy/configure_container.sh
Normal file
60
client/server_scripts/mtproxy/configure_container.sh
Normal file
@@ -0,0 +1,60 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Download Telegram config files
|
||||
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
|
||||
SECRET="$MTPROXY_SECRET"
|
||||
elif [ -f /data/secret ]; then
|
||||
SECRET=$(cat /data/secret)
|
||||
else
|
||||
SECRET=$(openssl rand -hex 16)
|
||||
fi
|
||||
|
||||
# Validate: must be exactly 32 hex chars
|
||||
echo "$SECRET" | grep -qE '^[0-9a-fA-F]{32}$' || SECRET=$(openssl rand -hex 16)
|
||||
|
||||
# Persist secret for start.sh restarts
|
||||
echo "$SECRET" > /data/secret
|
||||
|
||||
# Detect external IP
|
||||
IP=$(curl -s --max-time 5 https://api.ipify.org 2>/dev/null)
|
||||
[ -z "$IP" ] && IP=$(curl -s --max-time 5 https://ifconfig.me 2>/dev/null)
|
||||
[ -z "$IP" ] && IP=$(curl -s --max-time 5 https://icanhazip.com 2>/dev/null)
|
||||
|
||||
# Use custom public host/domain if provided, otherwise fall back to detected IP
|
||||
if [ -n "$MTPROXY_PUBLIC_HOST" ]; then
|
||||
LINK_HOST="$MTPROXY_PUBLIC_HOST"
|
||||
else
|
||||
LINK_HOST="$IP"
|
||||
fi
|
||||
|
||||
PORT=$MTPROXY_PORT
|
||||
|
||||
# Transport mode is substituted by replaceVars — plain variable, no curly braces
|
||||
TRANSPORT_MODE=$MTPROXY_TRANSPORT_MODE
|
||||
|
||||
PADDED_SECRET="dd${SECRET}"
|
||||
|
||||
if [ "$TRANSPORT_MODE" = "faketls" ] && [ -n "$MTPROXY_TLS_DOMAIN" ]; then
|
||||
DOMAIN_HEX=$(echo -n "$MTPROXY_TLS_DOMAIN" | od -A n -t x1 | tr -d ' \n')
|
||||
FAKETLS_SECRET="ee${SECRET}${DOMAIN_HEX}"
|
||||
else
|
||||
FAKETLS_SECRET=""
|
||||
fi
|
||||
|
||||
# Active link secret depends on transport mode
|
||||
if [ "$TRANSPORT_MODE" = "faketls" ] && [ -n "$FAKETLS_SECRET" ]; then
|
||||
LINK_SECRET="$FAKETLS_SECRET"
|
||||
else
|
||||
LINK_SECRET="$PADDED_SECRET"
|
||||
fi
|
||||
|
||||
# Output stable markers — parsed by updateContainerConfigAfterInstallation()
|
||||
echo "[*] MTProxy configuration"
|
||||
echo "[*] Secret: ${SECRET}"
|
||||
echo "[*] FakeTLS: ${FAKETLS_SECRET}"
|
||||
echo "[*] tg:// link: tg://proxy?server=${LINK_HOST}&port=${PORT}&secret=${LINK_SECRET}"
|
||||
echo "[*] t.me link: https://t.me/proxy?server=${LINK_HOST}&port=${PORT}&secret=${LINK_SECRET}"
|
||||
9
client/server_scripts/mtproxy/run_container.sh
Normal file
9
client/server_scripts/mtproxy/run_container.sh
Normal file
@@ -0,0 +1,9 @@
|
||||
# Run container
|
||||
sudo docker run -d \
|
||||
--log-driver none \
|
||||
--restart always \
|
||||
-p $MTPROXY_PORT:$MTPROXY_PORT/tcp \
|
||||
-v amnezia-mtproxy-data:/data \
|
||||
--name $CONTAINER_NAME \
|
||||
$CONTAINER_NAME
|
||||
|
||||
71
client/server_scripts/mtproxy/start.sh
Normal file
71
client/server_scripts/mtproxy/start.sh
Normal file
@@ -0,0 +1,71 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "Container startup"
|
||||
|
||||
# Read persisted secret
|
||||
SECRET=""
|
||||
if [ -f /data/secret ]; then
|
||||
SECRET=$(cat /data/secret)
|
||||
fi
|
||||
|
||||
if [ -z "$SECRET" ]; then
|
||||
echo "ERROR: /data/secret not found — run configure_container first"
|
||||
tail -f /dev/null
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build tag argument
|
||||
TAG_ARG=""
|
||||
if [ -n "$MTPROXY_TAG" ]; then
|
||||
TAG_ARG="-P $MTPROXY_TAG"
|
||||
fi
|
||||
|
||||
# Build domain argument for FakeTLS mode
|
||||
DOMAIN_ARG=""
|
||||
if [ "$MTPROXY_TRANSPORT_MODE" = "faketls" ] && [ -n "$MTPROXY_TLS_DOMAIN" ]; then
|
||||
DOMAIN_ARG="--domain $MTPROXY_TLS_DOMAIN"
|
||||
fi
|
||||
|
||||
WORKERS=$MTPROXY_WORKERS
|
||||
STATS_PORT=2398
|
||||
LISTEN_PORT=$MTPROXY_PORT
|
||||
|
||||
NAT_FLAG=""
|
||||
NAT_VALUE=""
|
||||
if [ "$MTPROXY_NAT_ENABLED" = "1" ] && [ -n "$MTPROXY_NAT_INTERNAL_IP" ] && [ -n "$MTPROXY_NAT_EXTERNAL_IP" ]; then
|
||||
NAT_FLAG="--nat-info"
|
||||
NAT_VALUE="$MTPROXY_NAT_INTERNAL_IP:$MTPROXY_NAT_EXTERNAL_IP"
|
||||
else
|
||||
INTERNAL_IP=$(hostname -i 2>/dev/null | awk '{print $1}')
|
||||
EXTERNAL_IP=$(curl -s --max-time 5 https://api.ipify.org 2>/dev/null)
|
||||
[ -z "$EXTERNAL_IP" ] && EXTERNAL_IP=$(curl -s --max-time 5 https://ifconfig.me 2>/dev/null)
|
||||
|
||||
if [ -n "$INTERNAL_IP" ] && [ -n "$EXTERNAL_IP" ] && [ "$INTERNAL_IP" != "$EXTERNAL_IP" ]; then
|
||||
NAT_FLAG="--nat-info"
|
||||
NAT_VALUE="${INTERNAL_IP}:${EXTERNAL_IP}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Build additional secrets arguments
|
||||
ADDITIONAL_SECRETS_ARG=""
|
||||
if [ -n "$MTPROXY_ADDITIONAL_SECRETS" ]; then
|
||||
for S in $(echo "$MTPROXY_ADDITIONAL_SECRETS" | tr ',' ' '); do
|
||||
ADDITIONAL_SECRETS_ARG="$ADDITIONAL_SECRETS_ARG -S $S"
|
||||
done
|
||||
fi
|
||||
|
||||
# Start proxy (foreground)
|
||||
exec mtproto-proxy \
|
||||
-u root \
|
||||
-p ${STATS_PORT} \
|
||||
-H ${LISTEN_PORT} \
|
||||
-S ${SECRET} \
|
||||
${ADDITIONAL_SECRETS_ARG} \
|
||||
--aes-pwd /data/proxy-secret \
|
||||
-M ${WORKERS} \
|
||||
-C 60000 \
|
||||
--allow-skip-dh \
|
||||
${NAT_FLAG:+${NAT_FLAG} ${NAT_VALUE}} \
|
||||
${TAG_ARG} \
|
||||
${DOMAIN_ARG} \
|
||||
/data/proxy-multi.conf
|
||||
@@ -24,6 +24,14 @@
|
||||
<file>ipsec/run_container.sh</file>
|
||||
<file>ipsec/start.sh</file>
|
||||
<file>ipsec/strongswan.profile</file>
|
||||
<file>mtproxy/configure_container.sh</file>
|
||||
<file>mtproxy/Dockerfile</file>
|
||||
<file>mtproxy/run_container.sh</file>
|
||||
<file>mtproxy/start.sh</file>
|
||||
<file>telemt/configure_container.sh</file>
|
||||
<file>telemt/Dockerfile</file>
|
||||
<file>telemt/run_container.sh</file>
|
||||
<file>telemt/start.sh</file>
|
||||
<file>openvpn/configure_container.sh</file>
|
||||
<file>openvpn/Dockerfile</file>
|
||||
<file>openvpn/run_container.sh</file>
|
||||
@@ -55,4 +63,3 @@
|
||||
<file>xray/template.json</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
|
||||
42
client/server_scripts/telemt/Dockerfile
Normal file
42
client/server_scripts/telemt/Dockerfile
Normal file
@@ -0,0 +1,42 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# Debian-based image with Telemt binary (shell + jq for Amnezia configure scripts).
|
||||
# Binary from https://github.com/telemt/telemt releases (same pattern as upstream Dockerfile minimal stage).
|
||||
|
||||
FROM debian:12-slim
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
binutils \
|
||||
ca-certificates \
|
||||
curl \
|
||||
jq \
|
||||
openssl \
|
||||
tar \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Use machine arch (works with classic `docker build`; TARGETARCH is only set with BuildKit).
|
||||
RUN set -eux; \
|
||||
ARCH="$(uname -m)"; \
|
||||
case "$ARCH" in \
|
||||
x86_64) ASSET="telemt-x86_64-linux-musl.tar.gz" ;; \
|
||||
aarch64|arm64) ASSET="telemt-aarch64-linux-musl.tar.gz" ;; \
|
||||
*) echo "Unsupported architecture: $ARCH" >&2; exit 1 ;; \
|
||||
esac; \
|
||||
curl -fL --retry 5 --retry-delay 3 --connect-timeout 10 --max-time 120 \
|
||||
-o "/tmp/${ASSET}" "https://github.com/telemt/telemt/releases/latest/download/${ASSET}"; \
|
||||
curl -fL --retry 5 --retry-delay 3 --connect-timeout 10 --max-time 120 \
|
||||
-o "/tmp/${ASSET}.sha256" "https://github.com/telemt/telemt/releases/latest/download/${ASSET}.sha256"; \
|
||||
cd /tmp && sha256sum -c "${ASSET}.sha256"; \
|
||||
tar -xzf "${ASSET}" -C /tmp; \
|
||||
test -f /tmp/telemt; \
|
||||
install -m 0755 /tmp/telemt /usr/local/bin/telemt; \
|
||||
strip --strip-unneeded /usr/local/bin/telemt || true; \
|
||||
rm -f "/tmp/${ASSET}" "/tmp/${ASSET}.sha256" /tmp/telemt
|
||||
|
||||
RUN mkdir -p /opt/amnezia /data
|
||||
RUN printf '#!/bin/sh\ntail -f /dev/null\n' > /opt/amnezia/start.sh && \
|
||||
chmod a+x /opt/amnezia/start.sh
|
||||
|
||||
VOLUME /data
|
||||
ENTRYPOINT ["/bin/sh", "/opt/amnezia/start.sh"]
|
||||
CMD [""]
|
||||
73
client/server_scripts/telemt/configure_container.sh
Normal file
73
client/server_scripts/telemt/configure_container.sh
Normal file
@@ -0,0 +1,73 @@
|
||||
#!/bin/sh
|
||||
# Do not use set -e: Telemt / curl / kill edge cases should not abort the whole configure step.
|
||||
|
||||
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="$TELEMT_SECRET"
|
||||
elif [ -f /data/secret ]; then
|
||||
SECRET=$(cat /data/secret)
|
||||
else
|
||||
SECRET=$(openssl rand -hex 16)
|
||||
fi
|
||||
# Must be exactly 32 hex chars
|
||||
echo "$SECRET" | grep -qE '^[0-9a-fA-F]{32}$' || SECRET=$(openssl rand -hex 16)
|
||||
|
||||
# Build config.toml (other variables substituted on the host by Amnezia before upload)
|
||||
rm -f /data/config.toml
|
||||
|
||||
{
|
||||
echo "### Amnezia Telemt — generated"
|
||||
echo "[general]"
|
||||
echo "use_middle_proxy = $TELEMT_USE_MIDDLE_PROXY"
|
||||
echo "log_level = \"normal\""
|
||||
if [ -n "$TELEMT_TAG" ]; then
|
||||
echo "ad_tag = \"$TELEMT_TAG\""
|
||||
fi
|
||||
echo ""
|
||||
echo "[general.modes]"
|
||||
echo "classic = false"
|
||||
echo "secure = $TELEMT_TOML_SECURE"
|
||||
echo "tls = $TELEMT_TOML_TLS"
|
||||
echo ""
|
||||
echo "[general.links]"
|
||||
echo "show = \"*\""
|
||||
if [ -n "$TELEMT_PUBLIC_HOST" ]; then
|
||||
echo "public_host = \"$TELEMT_PUBLIC_HOST\""
|
||||
fi
|
||||
echo "public_port = $TELEMT_PORT"
|
||||
echo ""
|
||||
echo "[server]"
|
||||
echo "port = $TELEMT_PORT"
|
||||
echo ""
|
||||
echo "[server.api]"
|
||||
echo "enabled = true"
|
||||
echo "listen = \"0.0.0.0:9091\""
|
||||
# Match upstream Telemt default: localhost API only (curl in this script uses 127.0.0.1).
|
||||
echo "whitelist = [\"127.0.0.0/8\"]"
|
||||
echo ""
|
||||
echo "[[server.listeners]]"
|
||||
echo "ip = \"0.0.0.0\""
|
||||
echo ""
|
||||
echo "[censorship]"
|
||||
echo "tls_domain = \"$TELEMT_TLS_DOMAIN\""
|
||||
echo "mask = $TELEMT_MASK"
|
||||
echo "tls_emulation = $TELEMT_TLS_EMULATION"
|
||||
echo "tls_front_dir = \"/data/tlsfront\""
|
||||
echo ""
|
||||
echo "[access.users]"
|
||||
echo "$TELEMT_USER_NAME = \"$SECRET\""
|
||||
} > /data/config.toml
|
||||
|
||||
echo "$SECRET" > /data/secret
|
||||
chmod 600 /data/secret 2>/dev/null || true
|
||||
|
||||
# Do not start telemt here: a long-lived process + curl loop inside `docker exec` can confuse SSH/Docker
|
||||
# timing and is unnecessary — start.sh runs telemt after configure. Links can be empty until the service
|
||||
# is up; the client still parses Secret below.
|
||||
echo "[*] Telemt configuration"
|
||||
echo "[*] Secret: $SECRET"
|
||||
echo "[*] tg:// link: "
|
||||
echo "[*] t.me link: "
|
||||
9
client/server_scripts/telemt/run_container.sh
Normal file
9
client/server_scripts/telemt/run_container.sh
Normal file
@@ -0,0 +1,9 @@
|
||||
# Run container (ulimit per Telemt docs — avoids "Too many open files" under load)
|
||||
sudo docker run -d \
|
||||
--log-driver none \
|
||||
--restart always \
|
||||
--ulimit nofile=65536:65536 \
|
||||
-p $TELEMT_PORT:$TELEMT_PORT/tcp \
|
||||
-v amnezia-telemt-data:/data \
|
||||
--name $CONTAINER_NAME \
|
||||
$CONTAINER_NAME
|
||||
12
client/server_scripts/telemt/start.sh
Normal file
12
client/server_scripts/telemt/start.sh
Normal file
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "Container startup (Telemt)"
|
||||
|
||||
if [ ! -f /data/config.toml ]; then
|
||||
echo "ERROR: /data/config.toml not found — run configure_container first"
|
||||
tail -f /dev/null
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p /data/tlsfront
|
||||
exec /usr/local/bin/telemt /data/config.toml
|
||||
@@ -131,6 +131,15 @@ target_link_libraries(test_self_hosted_server_setup PRIVATE
|
||||
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)
|
||||
@@ -143,3 +152,4 @@ 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)
|
||||
|
||||
165
client/tests/testPairingParsers.cpp
Normal file
165
client/tests/testPairingParsers.cpp
Normal file
@@ -0,0 +1,165 @@
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QSignalSpy>
|
||||
#include <QTest>
|
||||
|
||||
#include "core/controllers/api/pairingController.h"
|
||||
#include "ui/controllers/api/pairingUiController.h"
|
||||
#include "core/utils/constants/apiKeys.h"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
class TestPairingParsers : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void generateQr_success_extractsConfigAndMeta()
|
||||
{
|
||||
PairingController::QrPairingConfigPayload out;
|
||||
QJsonObject o;
|
||||
o[apiDefs::key::config] = QStringLiteral("vpn://dummy");
|
||||
o[apiDefs::key::serviceInfo] = QJsonObject { { QStringLiteral("is_ad_visible"), false } };
|
||||
o[apiDefs::key::supportedProtocols] = QJsonArray { QStringLiteral("awg") };
|
||||
const QByteArray body = QJsonDocument(o).toJson();
|
||||
|
||||
QCOMPARE(PairingController::parseGenerateQrResponseBody(body, out), ErrorCode::NoError);
|
||||
QCOMPARE(out.config, QStringLiteral("vpn://dummy"));
|
||||
QCOMPARE(out.supportedProtocols.size(), 1);
|
||||
}
|
||||
|
||||
void generateQr_http408()
|
||||
{
|
||||
PairingController::QrPairingConfigPayload out;
|
||||
QJsonObject o;
|
||||
o[QStringLiteral("http_status")] = 408;
|
||||
o[QStringLiteral("message")] = QStringLiteral("Request Timeout");
|
||||
const QByteArray body = QJsonDocument(o).toJson();
|
||||
|
||||
QCOMPARE(PairingController::parseGenerateQrResponseBody(body, out), ErrorCode::ApiConfigTimeoutError);
|
||||
QVERIFY(out.config.isEmpty());
|
||||
}
|
||||
|
||||
void generateQr_http429()
|
||||
{
|
||||
PairingController::QrPairingConfigPayload out;
|
||||
QJsonObject o;
|
||||
o[QStringLiteral("http_status")] = 429;
|
||||
o[QStringLiteral("message")] = QStringLiteral("Too Many Requests");
|
||||
const QByteArray body = QJsonDocument(o).toJson();
|
||||
|
||||
QCOMPARE(PairingController::parseGenerateQrResponseBody(body, out), ErrorCode::ApiPairingRateLimitedError);
|
||||
}
|
||||
|
||||
void scanQr_messageOk()
|
||||
{
|
||||
QJsonObject o;
|
||||
o[QStringLiteral("message")] = QStringLiteral("OK");
|
||||
const QByteArray body = QJsonDocument(o).toJson();
|
||||
|
||||
QCOMPARE(PairingController::parseScanQrResponseBody(body), ErrorCode::NoError);
|
||||
}
|
||||
|
||||
void scanQr_messageOk_extractsDeviceName()
|
||||
{
|
||||
QJsonObject o;
|
||||
o[QStringLiteral("message")] = QStringLiteral("OK");
|
||||
o[QStringLiteral("device_name")] = QStringLiteral("TestPhone");
|
||||
const QByteArray body = QJsonDocument(o).toJson();
|
||||
QString name;
|
||||
QCOMPARE(PairingController::parseScanQrResponseBody(body, &name), ErrorCode::NoError);
|
||||
QCOMPARE(name, QStringLiteral("TestPhone"));
|
||||
}
|
||||
|
||||
void scanQr_deviceLimitMessage()
|
||||
{
|
||||
QJsonObject o;
|
||||
o[QStringLiteral("message")] = QStringLiteral("Device limit reached for subscription");
|
||||
const QByteArray body = QJsonDocument(o).toJson();
|
||||
|
||||
QCOMPARE(PairingController::parseScanQrResponseBody(body), ErrorCode::ApiConfigLimitError);
|
||||
}
|
||||
|
||||
void scanQr_http403()
|
||||
{
|
||||
QJsonObject o;
|
||||
o[QStringLiteral("http_status")] = 403;
|
||||
const QByteArray body = QJsonDocument(o).toJson();
|
||||
|
||||
QCOMPARE(PairingController::parseScanQrResponseBody(body), ErrorCode::ApiPairingForbiddenError);
|
||||
}
|
||||
|
||||
void scanQr_http409()
|
||||
{
|
||||
QJsonObject o;
|
||||
o[QStringLiteral("http_status")] = 409;
|
||||
const QByteArray body = QJsonDocument(o).toJson();
|
||||
|
||||
QCOMPARE(PairingController::parseScanQrResponseBody(body), ErrorCode::ApiPairingConflictError);
|
||||
}
|
||||
|
||||
void scanQr_notFoundMessage()
|
||||
{
|
||||
QJsonObject o;
|
||||
o[QStringLiteral("message")] = QStringLiteral("Session not found");
|
||||
const QByteArray body = QJsonDocument(o).toJson();
|
||||
|
||||
QCOMPARE(PairingController::parseScanQrResponseBody(body), ErrorCode::ApiNotFoundError);
|
||||
}
|
||||
|
||||
void scanQr_qrSessionExpiredMessage()
|
||||
{
|
||||
QJsonObject o;
|
||||
o[QStringLiteral("message")] = QStringLiteral("QR session not found or expired");
|
||||
const QByteArray body = QJsonDocument(o).toJson();
|
||||
|
||||
QCOMPARE(PairingController::parseScanQrResponseBody(body), ErrorCode::ApiPairingSessionExpiredError);
|
||||
}
|
||||
|
||||
void validateScanFields_oversizedVpnKey()
|
||||
{
|
||||
QString vpnKey;
|
||||
vpnKey.fill(QLatin1Char('x'), 256 * 1024 + 1);
|
||||
QCOMPARE(PairingController::validatePairingScanFields(QStringLiteral("ab"), vpnKey, QStringLiteral("k"),
|
||||
QStringLiteral("amnezia-premium"), QStringLiteral("ru")),
|
||||
ErrorCode::ApiPairingPayloadTooLargeError);
|
||||
}
|
||||
|
||||
void validateScanFields_uuidTooLong()
|
||||
{
|
||||
QString uuid(200, QLatin1Char('a'));
|
||||
QCOMPARE(PairingController::validatePairingScanFields(uuid, QStringLiteral("vpn://a"), QStringLiteral("k"),
|
||||
QStringLiteral("amnezia-premium"), QStringLiteral("ru")),
|
||||
ErrorCode::ApiConfigEmptyError);
|
||||
}
|
||||
|
||||
void validateScanFields_missingServiceType()
|
||||
{
|
||||
QCOMPARE(PairingController::validatePairingScanFields(QStringLiteral("ab"), QStringLiteral("vpn://x"),
|
||||
QStringLiteral("k"), QString(),
|
||||
QStringLiteral("ru")),
|
||||
ErrorCode::ApiPairingMissingMetadataError);
|
||||
}
|
||||
|
||||
void pairingUi_applyScanned_extractsUuid_emitsSignal()
|
||||
{
|
||||
PairingUiController ctl(nullptr, nullptr, nullptr, nullptr);
|
||||
QSignalSpy spy(&ctl, &PairingUiController::pairingUuidFromScan);
|
||||
const QString u = QStringLiteral("123e4567-e89b-12d3-a456-426614174000");
|
||||
QVERIFY(ctl.applyScannedTextAsPairingUuid(QStringLiteral("prefix ") + u + QStringLiteral(" suffix")));
|
||||
QCOMPARE(spy.count(), 1);
|
||||
QCOMPARE(spy.first().first().toString(), u);
|
||||
}
|
||||
|
||||
void pairingUi_applyScanned_rejectsVpnKey()
|
||||
{
|
||||
PairingUiController ctl(nullptr, nullptr, nullptr, nullptr);
|
||||
QSignalSpy spy(&ctl, &PairingUiController::pairingUuidFromScan);
|
||||
QVERIFY(!ctl.applyScannedTextAsPairingUuid(QStringLiteral("vpn://AAAA")));
|
||||
QCOMPARE(spy.count(), 0);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestPairingParsers)
|
||||
#include "testPairingParsers.moc"
|
||||
@@ -1312,6 +1312,21 @@ Thank you for staying with us!</source>
|
||||
</context>
|
||||
<context>
|
||||
<name>PageProtocolXraySettings</name>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageProtocolXraySettings.qml" line="61"/>
|
||||
<source>XRay VLESS settings</source>
|
||||
<translation>Настройки XRay VLESS</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageProtocolXraySettings.qml" line="80"/>
|
||||
<source>More about settings</source>
|
||||
<translation>Подробнее о настройках</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageProtocolXraySettings.qml" line="188"/>
|
||||
<source>Reset settings</source>
|
||||
<translation>Сбросить настройки</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageProtocolXraySettings.qml" line="57"/>
|
||||
<source>XRay settings</source>
|
||||
@@ -1807,6 +1822,16 @@ 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>
|
||||
|
||||
739
client/ui/controllers/api/pairingUiController.cpp
Normal file
739
client/ui/controllers/api/pairingUiController.cpp
Normal file
@@ -0,0 +1,739 @@
|
||||
#include "pairingUiController.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDateTime>
|
||||
#include <QIODevice>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QMetaObject>
|
||||
#include <QPointer>
|
||||
#include <QRegularExpression>
|
||||
#include <QSet>
|
||||
#include <QTimer>
|
||||
#include <QUuid>
|
||||
#include <string>
|
||||
|
||||
#include "platforms/ios/iosPairingCameraAccess.h"
|
||||
#if defined(Q_OS_IOS)
|
||||
#include "platforms/ios/iosPairingQrOverlayWindow.h"
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_ANDROID)
|
||||
#include "platforms/android/android_controller.h"
|
||||
#endif
|
||||
|
||||
#include "core/controllers/gatewayController.h"
|
||||
#include "core/models/api/apiV2ServerConfig.h"
|
||||
#include "core/utils/constants/apiConstants.h"
|
||||
#include "core/utils/constants/apiKeys.h"
|
||||
#include "core/utils/qrCodeUtils.h"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr auto kGenerateQrPath = "%1v1/generate_qr";
|
||||
constexpr auto kScanQrPath = "%1v1/scan_qr";
|
||||
constexpr auto kGatewayProbePath = "%1v1/news";
|
||||
constexpr int kPairingRetryMaxAttempts = 3;
|
||||
constexpr int kGatewayProbeTimeoutMsecs = 3000;
|
||||
|
||||
QJsonObject apiGatewayServicesFromServers(const ServersController *serversController)
|
||||
{
|
||||
if (!serversController || serversController->getServersCount() == 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QSet<QString> userCountryCodes;
|
||||
QSet<QString> serviceTypes;
|
||||
for (int i = 0; i < serversController->getServersCount(); ++i) {
|
||||
const QString serverId = serversController->getServerId(i);
|
||||
const auto apiV2 = serversController->apiV2Config(serverId);
|
||||
if (!apiV2.has_value()) {
|
||||
continue;
|
||||
}
|
||||
if (!apiV2->apiConfig.userCountryCode.isEmpty()) {
|
||||
userCountryCodes.insert(apiV2->apiConfig.userCountryCode);
|
||||
}
|
||||
const QString serviceType = apiV2->serviceType();
|
||||
if (!serviceType.isEmpty()) {
|
||||
serviceTypes.insert(serviceType);
|
||||
}
|
||||
}
|
||||
|
||||
if (userCountryCodes.isEmpty() && serviceTypes.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QJsonObject json;
|
||||
QJsonArray userCountryCodesArray;
|
||||
for (const QString &code : userCountryCodes) {
|
||||
userCountryCodesArray.append(code);
|
||||
}
|
||||
json.insert(apiDefs::key::userCountryCode, userCountryCodesArray);
|
||||
|
||||
QJsonArray serviceTypesArray;
|
||||
for (const QString &type : serviceTypes) {
|
||||
serviceTypesArray.append(type);
|
||||
}
|
||||
json.insert(apiDefs::key::serviceType, serviceTypesArray);
|
||||
return json;
|
||||
}
|
||||
|
||||
bool isPairingRetriableError(ErrorCode code)
|
||||
{
|
||||
switch (code) {
|
||||
case ErrorCode::ApiPairingRateLimitedError:
|
||||
case ErrorCode::ApiPairingServiceUnavailableError:
|
||||
case ErrorCode::ApiConfigDownloadError:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int pairingRetryDelayMs(int zeroBasedAttempt)
|
||||
{
|
||||
constexpr int baseMs = 500;
|
||||
return baseMs * (1 << zeroBasedAttempt);
|
||||
}
|
||||
|
||||
QString extractPairingSessionUuidFromScanText(const QString &raw)
|
||||
{
|
||||
const QString t = raw.trimmed();
|
||||
if (t.startsWith(QStringLiteral("vpn://"), Qt::CaseInsensitive)) {
|
||||
return {};
|
||||
}
|
||||
static const QRegularExpression reV4(QStringLiteral(
|
||||
"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}"));
|
||||
const QRegularExpressionMatch m = reV4.match(t);
|
||||
if (m.hasMatch()) {
|
||||
return m.captured(0);
|
||||
}
|
||||
const QUuid parsed = QUuid::fromString(t);
|
||||
if (!parsed.isNull()) {
|
||||
return parsed.toString(QUuid::WithoutBraces);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
#if defined(Q_OS_ANDROID)
|
||||
namespace {
|
||||
PairingUiController *g_pairingUiForAndroidQr = nullptr;
|
||||
}
|
||||
#endif
|
||||
|
||||
PairingUiController::PairingUiController(PairingController *pairingController, ServersController *serversController,
|
||||
SubscriptionController *subscriptionController,
|
||||
SecureAppSettingsRepository *appSettingsRepository, QObject *parent)
|
||||
: QObject(parent),
|
||||
m_pairingController(pairingController),
|
||||
m_serversController(serversController),
|
||||
m_subscriptionController(subscriptionController),
|
||||
m_appSettingsRepository(appSettingsRepository)
|
||||
{
|
||||
#if defined(Q_OS_ANDROID)
|
||||
g_pairingUiForAndroidQr = this;
|
||||
connect(AndroidController::instance(), &AndroidController::cameraPermissionResult, this,
|
||||
[this](bool granted) { emit pairingCameraAccessFinished(granted); });
|
||||
#endif
|
||||
}
|
||||
|
||||
PairingUiController::~PairingUiController()
|
||||
{
|
||||
#if defined(Q_OS_ANDROID)
|
||||
if (g_pairingUiForAndroidQr == this) {
|
||||
g_pairingUiForAndroidQr = nullptr;
|
||||
}
|
||||
#endif
|
||||
#if defined(Q_OS_IOS)
|
||||
amneziaIosPairingQrOverlayDismiss();
|
||||
#endif
|
||||
}
|
||||
|
||||
void PairingUiController::setPendingPhonePairingUuid(const QString &uuid)
|
||||
{
|
||||
const QString trimmed = uuid.trimmed();
|
||||
if (m_pendingPhonePairingUuid == trimmed) {
|
||||
return;
|
||||
}
|
||||
m_pendingPhonePairingUuid = trimmed;
|
||||
emit pendingPhonePairingUuidChanged();
|
||||
}
|
||||
|
||||
void PairingUiController::clearPendingPhonePairingUuid()
|
||||
{
|
||||
if (m_pendingPhonePairingUuid.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
m_pendingPhonePairingUuid.clear();
|
||||
emit pendingPhonePairingUuidChanged();
|
||||
}
|
||||
|
||||
void PairingUiController::openPairingQrScanner()
|
||||
{
|
||||
#if defined(Q_OS_ANDROID)
|
||||
AndroidController::instance()->startPairingQrReaderActivity();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool PairingUiController::isPairingCameraAccessGranted() const
|
||||
{
|
||||
#if defined(Q_OS_ANDROID)
|
||||
return AndroidController::instance()->isCameraPermissionGranted();
|
||||
#elif defined(Q_OS_IOS)
|
||||
return amneziaIosPairingCameraAccessGranted();
|
||||
#else
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
void PairingUiController::requestPairingCameraAccess()
|
||||
{
|
||||
#if defined(Q_OS_ANDROID)
|
||||
AndroidController::instance()->requestCameraPermissionForQrPairing();
|
||||
#elif defined(Q_OS_IOS)
|
||||
amneziaIosRequestPairingCameraAccess([this](bool granted) {
|
||||
QMetaObject::invokeMethod(
|
||||
this, [this, granted]() { emit pairingCameraAccessFinished(granted); }, Qt::QueuedConnection);
|
||||
});
|
||||
#else
|
||||
emit pairingCameraAccessFinished(true);
|
||||
#endif
|
||||
}
|
||||
|
||||
void PairingUiController::openPairingCameraAppSettings()
|
||||
{
|
||||
#if defined(Q_OS_ANDROID)
|
||||
AndroidController::instance()->openApplicationDetailsSettings();
|
||||
#elif defined(Q_OS_IOS)
|
||||
amneziaIosOpenApplicationSettings();
|
||||
#endif
|
||||
}
|
||||
|
||||
void PairingUiController::setPairingQrTorchEnabled(bool enabled)
|
||||
{
|
||||
#if defined(Q_OS_ANDROID)
|
||||
Q_UNUSED(enabled);
|
||||
#elif defined(Q_OS_IOS)
|
||||
amneziaIosPairingQrOverlaySetTorchEnabled(enabled);
|
||||
#else
|
||||
Q_UNUSED(enabled);
|
||||
#endif
|
||||
}
|
||||
|
||||
void PairingUiController::presentIosPairingQrNativeOverlayScanner(const QString &title, const QString &subtitle)
|
||||
{
|
||||
#if defined(Q_OS_IOS)
|
||||
const std::string titleUtf8 = title.isEmpty() ? std::string() : title.toStdString();
|
||||
const std::string subtitleUtf8 = subtitle.isEmpty() ? std::string() : subtitle.toStdString();
|
||||
amneziaIosPairingQrOverlayPresent(
|
||||
[this](const char *utf8) {
|
||||
const QString code = QString::fromUtf8(utf8);
|
||||
QMetaObject::invokeMethod(
|
||||
this,
|
||||
[this, code]() {
|
||||
if (!applyScannedTextAsPairingUuid(code)) {
|
||||
emit pairingSendQrScanRejectedInvalidPayload();
|
||||
}
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
},
|
||||
[this]() {
|
||||
QMetaObject::invokeMethod(
|
||||
this,
|
||||
[this]() { emit pairingIosNativeQrOverlayBackRequested(); },
|
||||
Qt::QueuedConnection);
|
||||
},
|
||||
titleUtf8, subtitleUtf8);
|
||||
#else
|
||||
Q_UNUSED(title);
|
||||
Q_UNUSED(subtitle);
|
||||
#endif
|
||||
}
|
||||
|
||||
void PairingUiController::dismissIosPairingQrNativeOverlayScanner()
|
||||
{
|
||||
#if defined(Q_OS_IOS)
|
||||
amneziaIosPairingQrOverlayDismiss();
|
||||
#endif
|
||||
}
|
||||
|
||||
void PairingUiController::restartIosPairingQrNativeOverlayCapture()
|
||||
{
|
||||
#if defined(Q_OS_IOS)
|
||||
amneziaIosPairingQrOverlayRestartCapture();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool PairingUiController::applyScannedTextAsPairingUuid(const QString &raw)
|
||||
{
|
||||
const QString uuid = extractPairingSessionUuidFromScanText(raw);
|
||||
if (uuid.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
emit pairingUuidFromScan(uuid);
|
||||
return true;
|
||||
}
|
||||
|
||||
#if defined(Q_OS_ANDROID)
|
||||
bool PairingUiController::tryConsumeAndroidQrScan(const QString &code)
|
||||
{
|
||||
if (!g_pairingUiForAndroidQr) {
|
||||
return false;
|
||||
}
|
||||
const QString codeCopy = code;
|
||||
// Parse on this thread: while CameraActivity is foreground, AmneziaActivity is stopped and the Qt
|
||||
// event loop may not process BlockingQueuedConnection until the user returns — UI would lag behind.
|
||||
if (extractPairingSessionUuidFromScanText(codeCopy).isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
PairingUiController *const ctl = g_pairingUiForAndroidQr;
|
||||
QPointer<PairingUiController> ctlPtr(ctl);
|
||||
QTimer::singleShot(0, ctl, [ctlPtr, codeCopy]() {
|
||||
if (!ctlPtr) {
|
||||
return;
|
||||
}
|
||||
ctlPtr->applyScannedTextAsPairingUuid(codeCopy);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
void PairingUiController::notifyAndroidPairingQrCameraClosed()
|
||||
{
|
||||
if (g_pairingUiForAndroidQr) {
|
||||
g_pairingUiForAndroidQr->suppressAndroidNativePairingReaderStarts(2000);
|
||||
}
|
||||
}
|
||||
|
||||
void PairingUiController::notifyAndroidPairingQrCameraUserDismissed()
|
||||
{
|
||||
if (!g_pairingUiForAndroidQr) {
|
||||
return;
|
||||
}
|
||||
PairingUiController *const ctl = g_pairingUiForAndroidQr;
|
||||
QPointer<PairingUiController> ptr(ctl);
|
||||
QTimer::singleShot(0, ctl, [ptr]() {
|
||||
if (!ptr) {
|
||||
return;
|
||||
}
|
||||
emit ptr->pairingAndroidNativeQrScannerUserDismissed();
|
||||
});
|
||||
}
|
||||
#endif
|
||||
|
||||
void PairingUiController::suppressAndroidNativePairingReaderStarts(int ms)
|
||||
{
|
||||
if (ms <= 0) {
|
||||
return;
|
||||
}
|
||||
#if defined(Q_OS_ANDROID)
|
||||
const qint64 now = QDateTime::currentMSecsSinceEpoch();
|
||||
const qint64 until = now + ms;
|
||||
if (until <= m_androidPairingReaderCooldownUntilEpochMs) {
|
||||
return;
|
||||
}
|
||||
m_androidPairingReaderCooldownUntilEpochMs = until;
|
||||
emit androidPairingReaderCooldownUntilEpochMsChanged();
|
||||
#else
|
||||
Q_UNUSED(ms);
|
||||
#endif
|
||||
}
|
||||
|
||||
QVariantList PairingUiController::tvQrCodes() const
|
||||
{
|
||||
QVariantList list;
|
||||
list.reserve(m_tvQrCodes.size());
|
||||
for (const QString &s : m_tvQrCodes) {
|
||||
list.append(s);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
int PairingUiController::tvQrCodesCount() const
|
||||
{
|
||||
return m_tvQrCodes.size();
|
||||
}
|
||||
|
||||
int PairingUiController::tvPairingWaitWindowSeconds() const
|
||||
{
|
||||
if (!m_pairingController) {
|
||||
return 30;
|
||||
}
|
||||
const int msec = m_pairingController->pairingLongPollTimeoutMsecs();
|
||||
return qMax(1, (msec + 999) / 1000);
|
||||
}
|
||||
|
||||
bool PairingUiController::phonePairingBusy() const
|
||||
{
|
||||
return m_phonePairingBusy;
|
||||
}
|
||||
|
||||
void PairingUiController::setTvBusy(bool busy)
|
||||
{
|
||||
m_tvPairingBusy = busy;
|
||||
}
|
||||
|
||||
void PairingUiController::setPhoneBusy(bool busy)
|
||||
{
|
||||
if (m_phonePairingBusy == busy) {
|
||||
return;
|
||||
}
|
||||
m_phonePairingBusy = busy;
|
||||
emit phonePairingBusyChanged();
|
||||
}
|
||||
|
||||
bool PairingUiController::canOpenTvQrPairingPage()
|
||||
{
|
||||
if (!m_appSettingsRepository) {
|
||||
emit errorOccurred(ErrorCode::InternalError);
|
||||
return false;
|
||||
}
|
||||
|
||||
const QJsonObject gatewayServices = apiGatewayServicesFromServers(m_serversController);
|
||||
if (gatewayServices.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
QJsonObject payload;
|
||||
payload.insert(QStringLiteral("locale"), m_appSettingsRepository->getAppLanguage().name().split(QLatin1Char('_')).first());
|
||||
|
||||
if (gatewayServices.contains(apiDefs::key::userCountryCode)) {
|
||||
payload.insert(apiDefs::key::userCountryCode, gatewayServices.value(apiDefs::key::userCountryCode));
|
||||
}
|
||||
if (gatewayServices.contains(apiDefs::key::serviceType)) {
|
||||
payload.insert(apiDefs::key::serviceType, gatewayServices.value(apiDefs::key::serviceType));
|
||||
}
|
||||
|
||||
const bool isTestPurchase = false;
|
||||
GatewayController gatewayController(m_appSettingsRepository->getGatewayEndpoint(isTestPurchase),
|
||||
m_appSettingsRepository->isDevGatewayEnv(isTestPurchase), kGatewayProbeTimeoutMsecs,
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled());
|
||||
QByteArray responseBody;
|
||||
const ErrorCode err = gatewayController.post(QString::fromLatin1(kGatewayProbePath), payload, responseBody);
|
||||
if (err != ErrorCode::NoError) {
|
||||
emit errorOccurred(err);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void PairingUiController::resetTvQrDisplay()
|
||||
{
|
||||
m_tvQrCodes.clear();
|
||||
m_tvSessionUuid.clear();
|
||||
emit tvQrCodesChanged();
|
||||
}
|
||||
|
||||
void PairingUiController::startTvQrSession()
|
||||
{
|
||||
if (!m_pairingController || !m_appSettingsRepository) {
|
||||
return;
|
||||
}
|
||||
if (m_tvPairingBusy) {
|
||||
return;
|
||||
}
|
||||
rotateTvQrSession();
|
||||
}
|
||||
|
||||
void PairingUiController::rotateTvQrSession()
|
||||
{
|
||||
if (!m_pairingController || !m_appSettingsRepository) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_tvWatcher) {
|
||||
m_tvWatcher->disconnect();
|
||||
m_tvWatcher->deleteLater();
|
||||
m_tvWatcher.clear();
|
||||
}
|
||||
if (m_tvNetworkReply) {
|
||||
m_tvNetworkReply->abort();
|
||||
m_tvNetworkReply.clear();
|
||||
}
|
||||
|
||||
++m_tvSessionGeneration;
|
||||
const quint64 generation = m_tvSessionGeneration;
|
||||
|
||||
m_tvSessionUuid = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
const QByteArray qrPayload = m_tvSessionUuid.toUtf8();
|
||||
m_tvQrCodes = qrCodeUtils::generateQrCodeImageSeriesPlainText(qrPayload);
|
||||
emit tvQrCodesChanged();
|
||||
|
||||
setTvBusy(true);
|
||||
|
||||
dispatchTvGenerateQrAttempt(generation, 0);
|
||||
}
|
||||
|
||||
void PairingUiController::dispatchTvGenerateQrAttempt(quint64 generation, int retryAttempt)
|
||||
{
|
||||
if (!m_pairingController || !m_appSettingsRepository) {
|
||||
return;
|
||||
}
|
||||
if (generation != m_tvSessionGeneration) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool isTestPurchase = false;
|
||||
auto gatewayController = QSharedPointer<GatewayController>::create(m_appSettingsRepository->getGatewayEndpoint(isTestPurchase),
|
||||
m_appSettingsRepository->isDevGatewayEnv(isTestPurchase),
|
||||
m_pairingController->pairingLongPollTimeoutMsecs(),
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled());
|
||||
|
||||
const QJsonObject payload = m_pairingController->buildGenerateQrPayload(m_tvSessionUuid);
|
||||
QNetworkReply *replyRaw = nullptr;
|
||||
const QFuture<QPair<ErrorCode, QByteArray>> future =
|
||||
gatewayController->postAsync(QString::fromLatin1(kGenerateQrPath), payload, &replyRaw, gatewayController);
|
||||
m_tvNetworkReply = replyRaw;
|
||||
|
||||
auto *watcher = new QFutureWatcher<QPair<ErrorCode, QByteArray>>(this);
|
||||
m_tvWatcher = watcher;
|
||||
QObject::connect(watcher, &QFutureWatcher<QPair<ErrorCode, QByteArray>>::finished, this,
|
||||
[this, gatewayController, watcher, generation, retryAttempt]() {
|
||||
Q_UNUSED(gatewayController);
|
||||
const auto result = watcher->result();
|
||||
watcher->deleteLater();
|
||||
if (m_tvWatcher == watcher) {
|
||||
m_tvWatcher.clear();
|
||||
}
|
||||
|
||||
if (generation != m_tvSessionGeneration) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_tvNetworkReply.clear();
|
||||
|
||||
PairingController::QrPairingConfigPayload out;
|
||||
ErrorCode logicalErr = result.first;
|
||||
if (logicalErr == ErrorCode::NoError) {
|
||||
logicalErr = PairingController::parseGenerateQrResponseBody(result.second, out);
|
||||
}
|
||||
|
||||
if (logicalErr == ErrorCode::NoError) {
|
||||
const ErrorCode impErr = m_subscriptionController->importServerFromQrPairingResponse(
|
||||
out.config, out.serviceInfo, out.supportedProtocols);
|
||||
setTvBusy(false);
|
||||
if (impErr != ErrorCode::NoError) {
|
||||
emit errorOccurred(impErr);
|
||||
if (impErr == ErrorCode::ApiConfigAlreadyAdded) {
|
||||
emit tvPairingConfigAlreadyAdded();
|
||||
QTimer::singleShot(0, this, [this]() { rotateTvQrSession(); });
|
||||
return;
|
||||
}
|
||||
resetTvQrDisplay();
|
||||
return;
|
||||
}
|
||||
resetTvQrDisplay();
|
||||
emit tvPairingConfigReceived();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPairingRetriableError(logicalErr) && retryAttempt + 1 < kPairingRetryMaxAttempts) {
|
||||
const int delayMs = pairingRetryDelayMs(retryAttempt);
|
||||
QTimer::singleShot(delayMs, this, [this, generation, retryAttempt]() {
|
||||
if (generation != m_tvSessionGeneration) {
|
||||
return;
|
||||
}
|
||||
dispatchTvGenerateQrAttempt(generation, retryAttempt + 1);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (logicalErr == ErrorCode::ApiConfigTimeoutError) {
|
||||
setTvBusy(false);
|
||||
QTimer::singleShot(0, this, [this]() { rotateTvQrSession(); });
|
||||
return;
|
||||
}
|
||||
|
||||
setTvBusy(false);
|
||||
emit errorOccurred(logicalErr);
|
||||
});
|
||||
watcher->setFuture(future);
|
||||
}
|
||||
|
||||
void PairingUiController::cancelTvQrSession()
|
||||
{
|
||||
++m_tvSessionGeneration;
|
||||
if (m_tvNetworkReply) {
|
||||
m_tvNetworkReply->abort();
|
||||
}
|
||||
m_tvNetworkReply.clear();
|
||||
if (m_tvWatcher) {
|
||||
m_tvWatcher->disconnect();
|
||||
m_tvWatcher->deleteLater();
|
||||
m_tvWatcher.clear();
|
||||
}
|
||||
setTvBusy(false);
|
||||
resetTvQrDisplay();
|
||||
}
|
||||
|
||||
void PairingUiController::cancelAllPairingActivity()
|
||||
{
|
||||
++m_phoneSessionGeneration;
|
||||
if (m_phoneNetworkReply) {
|
||||
m_phoneNetworkReply->abort();
|
||||
}
|
||||
m_phoneNetworkReply.clear();
|
||||
if (m_phoneWatcher) {
|
||||
m_phoneWatcher->disconnect();
|
||||
m_phoneWatcher->deleteLater();
|
||||
m_phoneWatcher.clear();
|
||||
}
|
||||
setPhoneBusy(false);
|
||||
|
||||
clearPendingPhonePairingUuid();
|
||||
if (!m_lastSuccessfulPhonePairingDisplayName.isEmpty()) {
|
||||
m_lastSuccessfulPhonePairingDisplayName.clear();
|
||||
emit lastSuccessfulPhonePairingDisplayNameChanged();
|
||||
}
|
||||
|
||||
cancelTvQrSession();
|
||||
}
|
||||
|
||||
void PairingUiController::submitPhonePairing(const QString &qrUuid, int serverIndex)
|
||||
{
|
||||
if (!m_pairingController || !m_serversController || !m_subscriptionController || !m_appSettingsRepository) {
|
||||
return;
|
||||
}
|
||||
if (m_phonePairingBusy) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QString trimmedUuid = qrUuid.trimmed();
|
||||
if (trimmedUuid.isEmpty()) {
|
||||
emit errorOccurred(ErrorCode::ApiConfigEmptyError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (serverIndex < 0 || serverIndex >= m_serversController->getServersCount()) {
|
||||
emit errorOccurred(ErrorCode::InternalError);
|
||||
return;
|
||||
}
|
||||
|
||||
const QString serverId = m_serversController->getServerId(serverIndex);
|
||||
const auto apiV2Opt = m_serversController->apiV2Config(serverId);
|
||||
if (!apiV2Opt.has_value()) {
|
||||
emit errorOccurred(ErrorCode::InternalError);
|
||||
return;
|
||||
}
|
||||
|
||||
const ApiV2ServerConfig &apiV2 = *apiV2Opt;
|
||||
|
||||
QString vpnKey;
|
||||
const ErrorCode keyErr = m_subscriptionController->prepareVpnKeyExport(serverId, vpnKey);
|
||||
if (keyErr != ErrorCode::NoError) {
|
||||
emit errorOccurred(keyErr);
|
||||
return;
|
||||
}
|
||||
|
||||
const QJsonObject serviceInfo = apiV2.apiConfig.serviceInfo.toJson();
|
||||
const QJsonArray supportedProtocols = apiV2.apiConfig.supportedProtocols;
|
||||
const QString apiKey = apiV2.authData.apiKey;
|
||||
if (apiKey.isEmpty()) {
|
||||
emit errorOccurred(ErrorCode::ApiConfigEmptyError);
|
||||
return;
|
||||
}
|
||||
|
||||
const QString serviceType = apiV2.apiConfig.serviceType.trimmed();
|
||||
const QString userCountryCode = apiV2.apiConfig.userCountryCode.trimmed();
|
||||
|
||||
const ErrorCode fieldErr =
|
||||
PairingController::validatePairingScanFields(trimmedUuid, vpnKey, apiKey, serviceType, userCountryCode);
|
||||
if (fieldErr != ErrorCode::NoError) {
|
||||
emit errorOccurred(fieldErr);
|
||||
return;
|
||||
}
|
||||
|
||||
++m_phoneSessionGeneration;
|
||||
const quint64 phoneGeneration = m_phoneSessionGeneration;
|
||||
|
||||
if (!m_lastSuccessfulPhonePairingDisplayName.isEmpty()) {
|
||||
m_lastSuccessfulPhonePairingDisplayName.clear();
|
||||
emit lastSuccessfulPhonePairingDisplayNameChanged();
|
||||
}
|
||||
|
||||
setPhoneBusy(true);
|
||||
|
||||
dispatchPhoneScanQrAttempt(trimmedUuid, apiV2.apiConfig.isTestPurchase, vpnKey, serviceInfo, supportedProtocols, apiKey,
|
||||
serviceType, userCountryCode, phoneGeneration, 0);
|
||||
}
|
||||
|
||||
void PairingUiController::dispatchPhoneScanQrAttempt(const QString &qrUuid, const bool isTestPurchase, const QString &vpnKey,
|
||||
const QJsonObject &serviceInfo, const QJsonArray &supportedProtocols,
|
||||
const QString &apiKey, const QString &serviceType, const QString &userCountryCode,
|
||||
quint64 generation, int retryAttempt)
|
||||
{
|
||||
if (!m_pairingController || !m_appSettingsRepository) {
|
||||
return;
|
||||
}
|
||||
if (generation != m_phoneSessionGeneration) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto gatewayController = QSharedPointer<GatewayController>::create(m_appSettingsRepository->getGatewayEndpoint(isTestPurchase),
|
||||
m_appSettingsRepository->isDevGatewayEnv(isTestPurchase),
|
||||
apiDefs::requestTimeoutMsecs,
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled());
|
||||
|
||||
const QJsonObject payload = m_pairingController->buildScanQrPayload(qrUuid, vpnKey, serviceInfo, supportedProtocols, apiKey,
|
||||
serviceType, userCountryCode);
|
||||
QNetworkReply *replyRaw = nullptr;
|
||||
const QFuture<QPair<ErrorCode, QByteArray>> future =
|
||||
gatewayController->postAsync(QString::fromLatin1(kScanQrPath), payload, &replyRaw, gatewayController);
|
||||
m_phoneNetworkReply = replyRaw;
|
||||
|
||||
auto *watcher = new QFutureWatcher<QPair<ErrorCode, QByteArray>>(this);
|
||||
m_phoneWatcher = watcher;
|
||||
QObject::connect(watcher, &QFutureWatcher<QPair<ErrorCode, QByteArray>>::finished, this,
|
||||
[this, gatewayController, watcher, generation, retryAttempt, qrUuid, isTestPurchase, vpnKey, serviceInfo,
|
||||
supportedProtocols, apiKey, serviceType, userCountryCode]() {
|
||||
Q_UNUSED(gatewayController);
|
||||
const auto result = watcher->result();
|
||||
watcher->deleteLater();
|
||||
if (m_phoneWatcher == watcher) {
|
||||
m_phoneWatcher.clear();
|
||||
}
|
||||
|
||||
if (generation != m_phoneSessionGeneration) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_phoneNetworkReply.clear();
|
||||
|
||||
ErrorCode logicalErr = result.first;
|
||||
QString scanDisplayName;
|
||||
if (logicalErr == ErrorCode::NoError) {
|
||||
logicalErr = PairingController::parseScanQrResponseBody(result.second, &scanDisplayName);
|
||||
}
|
||||
|
||||
if (logicalErr == ErrorCode::NoError) {
|
||||
setPhoneBusy(false);
|
||||
if (m_lastSuccessfulPhonePairingDisplayName != scanDisplayName) {
|
||||
m_lastSuccessfulPhonePairingDisplayName = scanDisplayName;
|
||||
emit lastSuccessfulPhonePairingDisplayNameChanged();
|
||||
}
|
||||
clearPendingPhonePairingUuid();
|
||||
emit phonePairingSucceeded();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPairingRetriableError(logicalErr) && retryAttempt + 1 < kPairingRetryMaxAttempts) {
|
||||
const int delayMs = pairingRetryDelayMs(retryAttempt);
|
||||
QTimer::singleShot(delayMs, this, [this, qrUuid, isTestPurchase, vpnKey, serviceInfo, supportedProtocols,
|
||||
apiKey, serviceType, userCountryCode, generation, retryAttempt]() {
|
||||
if (generation != m_phoneSessionGeneration) {
|
||||
return;
|
||||
}
|
||||
dispatchPhoneScanQrAttempt(qrUuid, isTestPurchase, vpnKey, serviceInfo, supportedProtocols, apiKey,
|
||||
serviceType, userCountryCode, generation, retryAttempt + 1);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setPhoneBusy(false);
|
||||
emit errorOccurred(logicalErr);
|
||||
});
|
||||
watcher->setFuture(future);
|
||||
}
|
||||
131
client/ui/controllers/api/pairingUiController.h
Normal file
131
client/ui/controllers/api/pairingUiController.h
Normal file
@@ -0,0 +1,131 @@
|
||||
#ifndef PAIRINGUICONTROLLER_H
|
||||
#define PAIRINGUICONTROLLER_H
|
||||
|
||||
#include <QFutureWatcher>
|
||||
#include <QNetworkReply>
|
||||
#include <QObject>
|
||||
#include <QVariantList>
|
||||
#include <QPointer>
|
||||
#include <QStringList>
|
||||
|
||||
#include "core/controllers/api/pairingController.h"
|
||||
#include "core/controllers/api/subscriptionController.h"
|
||||
#include "core/controllers/serversController.h"
|
||||
#include "core/repositories/secureAppSettingsRepository.h"
|
||||
|
||||
#include "core/utils/errorCodes.h"
|
||||
|
||||
class PairingUiController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(QVariantList tvQrCodes READ tvQrCodes NOTIFY tvQrCodesChanged)
|
||||
Q_PROPERTY(int tvQrCodesCount READ tvQrCodesCount NOTIFY tvQrCodesChanged)
|
||||
Q_PROPERTY(int tvPairingWaitWindowSeconds READ tvPairingWaitWindowSeconds NOTIFY tvQrCodesChanged)
|
||||
|
||||
Q_PROPERTY(bool phonePairingBusy READ phonePairingBusy NOTIFY phonePairingBusyChanged)
|
||||
Q_PROPERTY(QString pendingPhonePairingUuid READ pendingPhonePairingUuid WRITE setPendingPhonePairingUuid NOTIFY
|
||||
pendingPhonePairingUuidChanged)
|
||||
Q_PROPERTY(QString lastSuccessfulPhonePairingDisplayName READ lastSuccessfulPhonePairingDisplayName NOTIFY
|
||||
lastSuccessfulPhonePairingDisplayNameChanged)
|
||||
Q_PROPERTY(qint64 androidPairingReaderCooldownUntilEpochMs READ androidPairingReaderCooldownUntilEpochMs NOTIFY
|
||||
androidPairingReaderCooldownUntilEpochMsChanged)
|
||||
|
||||
public:
|
||||
PairingUiController(PairingController *pairingController, ServersController *serversController,
|
||||
SubscriptionController *subscriptionController, SecureAppSettingsRepository *appSettingsRepository,
|
||||
QObject *parent = nullptr);
|
||||
~PairingUiController() override;
|
||||
|
||||
QVariantList tvQrCodes() const;
|
||||
int tvQrCodesCount() const;
|
||||
int tvPairingWaitWindowSeconds() const;
|
||||
|
||||
bool phonePairingBusy() const;
|
||||
QString pendingPhonePairingUuid() const { return m_pendingPhonePairingUuid; }
|
||||
void setPendingPhonePairingUuid(const QString &uuid);
|
||||
QString lastSuccessfulPhonePairingDisplayName() const { return m_lastSuccessfulPhonePairingDisplayName; }
|
||||
|
||||
qint64 androidPairingReaderCooldownUntilEpochMs() const { return m_androidPairingReaderCooldownUntilEpochMs; }
|
||||
|
||||
Q_INVOKABLE void presentIosPairingQrNativeOverlayScanner(const QString &title = QString(),
|
||||
const QString &subtitle = QString());
|
||||
Q_INVOKABLE void dismissIosPairingQrNativeOverlayScanner();
|
||||
Q_INVOKABLE void restartIosPairingQrNativeOverlayCapture();
|
||||
|
||||
#if defined(Q_OS_ANDROID)
|
||||
static bool tryConsumeAndroidQrScan(const QString &code);
|
||||
static void notifyAndroidPairingQrCameraClosed();
|
||||
static void notifyAndroidPairingQrCameraUserDismissed();
|
||||
#endif
|
||||
|
||||
public slots:
|
||||
bool canOpenTvQrPairingPage();
|
||||
void startTvQrSession();
|
||||
void rotateTvQrSession();
|
||||
void cancelTvQrSession();
|
||||
void cancelAllPairingActivity();
|
||||
|
||||
void submitPhonePairing(const QString &qrUuid, int serverIndex);
|
||||
|
||||
void openPairingQrScanner();
|
||||
|
||||
Q_INVOKABLE bool isPairingCameraAccessGranted() const;
|
||||
Q_INVOKABLE void requestPairingCameraAccess();
|
||||
Q_INVOKABLE void openPairingCameraAppSettings();
|
||||
Q_INVOKABLE void setPairingQrTorchEnabled(bool enabled);
|
||||
|
||||
bool applyScannedTextAsPairingUuid(const QString &raw);
|
||||
|
||||
signals:
|
||||
void errorOccurred(amnezia::ErrorCode errorCode);
|
||||
void tvQrCodesChanged();
|
||||
void phonePairingBusyChanged();
|
||||
void pendingPhonePairingUuidChanged();
|
||||
void lastSuccessfulPhonePairingDisplayNameChanged();
|
||||
|
||||
void tvPairingConfigReceived();
|
||||
void tvPairingConfigAlreadyAdded();
|
||||
void phonePairingSucceeded();
|
||||
|
||||
void pairingUuidFromScan(const QString &uuid);
|
||||
void pairingCameraAccessFinished(bool granted);
|
||||
void androidPairingReaderCooldownUntilEpochMsChanged();
|
||||
void pairingSendQrScanRejectedInvalidPayload();
|
||||
void pairingIosNativeQrOverlayBackRequested();
|
||||
void pairingAndroidNativeQrScannerUserDismissed();
|
||||
|
||||
private:
|
||||
void setTvBusy(bool busy);
|
||||
void setPhoneBusy(bool busy);
|
||||
void resetTvQrDisplay();
|
||||
void clearPendingPhonePairingUuid();
|
||||
void suppressAndroidNativePairingReaderStarts(int ms);
|
||||
void dispatchTvGenerateQrAttempt(quint64 generation, int retryAttempt);
|
||||
void dispatchPhoneScanQrAttempt(const QString &qrUuid, bool isTestPurchase, const QString &vpnKey, const QJsonObject &serviceInfo,
|
||||
const QJsonArray &supportedProtocols, const QString &apiKey, const QString &serviceType,
|
||||
const QString &userCountryCode, quint64 generation, int retryAttempt);
|
||||
|
||||
PairingController *m_pairingController {};
|
||||
ServersController *m_serversController {};
|
||||
SubscriptionController *m_subscriptionController {};
|
||||
SecureAppSettingsRepository *m_appSettingsRepository {};
|
||||
|
||||
QList<QString> m_tvQrCodes;
|
||||
QString m_tvSessionUuid;
|
||||
bool m_tvPairingBusy = false;
|
||||
QPointer<QFutureWatcher<QPair<amnezia::ErrorCode, QByteArray>>> m_tvWatcher;
|
||||
QPointer<QNetworkReply> m_tvNetworkReply;
|
||||
quint64 m_tvSessionGeneration { 0 };
|
||||
|
||||
bool m_phonePairingBusy = false;
|
||||
QString m_pendingPhonePairingUuid;
|
||||
QString m_lastSuccessfulPhonePairingDisplayName;
|
||||
QPointer<QFutureWatcher<QPair<amnezia::ErrorCode, QByteArray>>> m_phoneWatcher;
|
||||
QPointer<QNetworkReply> m_phoneNetworkReply;
|
||||
quint64 m_phoneSessionGeneration { 0 };
|
||||
|
||||
qint64 m_androidPairingReaderCooldownUntilEpochMs = 0;
|
||||
};
|
||||
|
||||
#endif // PAIRINGUICONTROLLER_H
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user