Compare commits

..
Author SHA1 Message Date
albexk 29d98846d3 Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
2025-10-10 13:33:28 +03:00
vkamn f6e7d3ccf1 fix: minor ui fixes (#1917)
* feat: improve storage processing

* fix: minor ui fixes
2025-10-09 23:22:58 +08:00
Mitternacht822 a754a11913 fix: added displaying vpn_key field added in older version of the app (#1873)
* fix(api_key): added displaying vpn_key field added in older version of the app

* revert changes

* fix: implemented generation of api key text for PremiumV2

* fix: deleted unnecessary code

* saving apikey text when generating

* added method for vpn key export, fixed wrong saving file
2025-10-07 23:16:28 +08:00
vkamn 4d25e3b6f6 chore: minor bugfixes (#1915) 2025-10-07 23:15:06 +08:00
MrMirDan 1fac280497 fix: main app info added after clearing logs (#1913) 2025-10-06 21:07:04 +08:00
Yaroslav c886c5e6a7 feat: enhance OpenVPN configuration handling and logging for iOS plat… (#1910)
* feat: enhance OpenVPN configuration handling and logging for iOS platform

* refactor: remove $OPENVPN_TA_KEY_SANITIZED and use $OPENVPN_TA_KEY instead
2025-10-06 21:04:49 +08:00
aiamneziaandvkamn cd7f78b9ca feat: news and notifications page (#1660)
* Add news and notifications

* Add localization for news and notifications

* Remove news caching

* Add fetching news befor openning news page

* Fix not updating news page

* Delete debug output

* Remove news and notificztions with only self-hosted servers

* Add stack filters to fetching news request

* Add fetching news with changing stack in the client

* small refactoring

* polishing

* Rename newsModel files and fix naming in code

* fix: remove custom signals; fetch news only on stack expansion

* chore: delete unnecessary code

* chore: code style fixes

* fix: fixed memory leak in gateway controller

---------

Co-authored-by: vkamn <vk@amnezia.org>
2025-10-06 12:06:36 +08:00
vkamn a587d3230f fix: again fixed site link for features field (#1908) 2025-10-06 11:38:57 +08:00
MrMirDan 93e7b45136 fix: removed 'clear site list' button icon (#1909) 2025-10-06 11:37:42 +08:00
vkamn e024f71ce1 fix: allow remove expired api configs (#1907) 2025-10-03 14:45:12 +08:00
MrMirDan 50d1be7b4a chore: update for RU translation (#1893) 2025-10-02 20:59:45 +08:00
MrMirDan 3ec6d8973b fix: warning visible only on windows (#1900) 2025-10-02 20:59:23 +08:00
Yaroslav Gurov 3ea47d31a9 fix: restore dns after using xray (#1902) 2025-10-02 20:58:53 +08:00
vkamn 30c8cc4548 feat: add isConnectEvent field to api request (#1896) 2025-09-30 12:10:27 +08:00
vkamn 98586d2dd9 fix: fixed site link (#1897) 2025-09-30 12:07:27 +08:00
albexk 4177c8e780 Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
2025-09-10 14:48:44 +03:00
albexk adcb90a7ac fix: fix import TimeZone 2025-08-27 19:48:51 +03:00
albexk 880057641e Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
#	client/android/utils/src/main/kotlin/Log.kt
2025-08-27 18:34:58 +03:00
albexk 765964bdb2 Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
2025-07-31 13:25:03 +03:00
albexk 584ead4a33 Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
2025-07-25 12:36:35 +03:00
albexk 5f86957556 Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
2025-07-10 16:04:34 +03:00
albexk 215b417da3 Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
2025-07-07 16:01:29 +03:00
albexk 97dd76ea7b Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
2025-05-26 12:33:40 +03:00
albexk 35d762ccf9 Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
2025-05-20 11:42:18 +03:00
albexk d0086de333 Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
2025-03-21 16:11:42 +03:00
albexk 9eef389cdb Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
2025-03-10 19:20:13 +03:00
albexk b9a0364b3b Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
2025-03-04 12:15:37 +03:00
albexk 0b63efcd67 Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
2025-02-25 18:33:47 +03:00
albexk b3060187ef Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
2025-02-24 17:43:02 +03:00
albexk b6118e4c9f Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
2025-02-13 20:34:31 +03:00
pokamest 4135eb0110 Merge pull request #1389 from amnezia-vpn/android7-pull
pull fixes
2025-01-31 23:02:15 +01:00
pokamest 936adcafa6 Merge branch 'android-7' into android7-pull 2025-01-31 23:00:45 +01:00
albexk ad62fc4aca Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
2025-01-15 17:33:36 +03:00
albexk aaa12e51f0 Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
#	client/android/src/org/amnezia/vpn/AmneziaActivity.kt
2025-01-13 19:14:03 +03:00
albexk a440ddd7e7 Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
2024-11-08 11:54:44 +03:00
albexk 0e571af728 Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
2024-10-25 19:24:54 +03:00
albexk e46e983bb8 Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
2024-10-24 19:39:00 +03:00
albexk 1ae9a57b57 Fix merge 2024-10-22 14:19:44 +03:00
albexk 5e80223e7a Merge branch 'dev' into android-7
# Conflicts:
#	.github/workflows/deploy.yml
#	CMakeLists.txt
#	client/android/src/org/amnezia/vpn/AmneziaActivity.kt
#	client/android/src/org/amnezia/vpn/AmneziaVpnService.kt
#	client/android/src/org/amnezia/vpn/ServiceNotification.kt
#	client/android/utils/src/main/kotlin/Log.kt
#	client/android/utils/src/main/kotlin/net/NetworkState.kt
2024-10-22 12:51:10 +03:00
albexk 4d6174f5d8 Fix GA 2024-10-02 15:25:25 +03:00
albexk ce9a062bea Update version code to separate versions for new and old Androids 2024-10-02 15:20:55 +03:00
albexk 4910dcfa96 Set the maximum version of Androids to 7.1 (API 25) 2024-10-02 15:18:05 +03:00
albexk 744b45476c Bump version to 4.8.2.0 2024-10-01 20:23:06 +03:00
albexk ca43c6e69e Up Qt to 6.7.3 2024-10-01 20:21:56 +03:00
74 changed files with 1486 additions and 770 deletions

No files matched your search

+10 -9
View File
@@ -238,13 +238,14 @@ jobs:
IOS_APP_PROVISIONING_PROFILE: ${{ secrets.IOS_APP_PROVISIONING_PROFILE }}
IOS_NE_PROVISIONING_PROFILE: ${{ secrets.IOS_NE_PROVISIONING_PROFILE }}
- name: 'Upload unsigned .ipa to artifacts'
uses: actions/upload-artifact@v4
with:
name: AmneziaVPN-unsigned-ipa
path: |
${{ github.workspace }}/build-ios/AmneziaVPN_unsigned.ipa
retention-days: 7
# - name: 'Upload appstore .ipa and dSYMs to artifacts'
# uses: actions/upload-artifact@v4
# with:
# name: app-store ipa & dsyms
# path: |
# ${{ github.workspace }}/AmneziaVPN-iOS.ipa
# ${{ github.workspace }}/*.app.dSYM.zip
# retention-days: 7
# ------------------------------------------------------
@@ -469,7 +470,7 @@ jobs:
env:
ANDROID_BUILD_PLATFORM: android-34
QT_VERSION: 6.7.3
QT_VERSION: 6.6.3
QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools'
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
@@ -486,7 +487,7 @@ jobs:
version: ${{ env.QT_VERSION }}
host: 'linux'
target: 'desktop'
arch: 'linux_gcc_64'
arch: 'gcc_64'
modules: ${{ env.QT_MODULES }}
dir: ${{ runner.temp }}
py7zrversion: '==0.22.*'
+1 -1
View File
@@ -4,7 +4,7 @@ on:
workflow_dispatch:
# push:
# tags:
# - "**"
# - **
jobs:
+1
View File
@@ -9,6 +9,7 @@ deploy/build_32/*
deploy/build_64/*
winbuild*.bat
.cache/
.vscode/
# Qt-es
+1 -1
View File
@@ -12,7 +12,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
set(RELEASE_DATE "${CURRENT_DATE}")
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
set(APP_ANDROID_VERSION_CODE 2095)
set(APP_ANDROID_VERSION_CODE 1095)
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(MZ_PLATFORM_NAME "linux")
+6
View File
@@ -3,10 +3,13 @@
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.amnezia.vpn"
android:versionName="-- %%INSERT_VERSION_NAME%% --"
android:versionCode="-- %%INSERT_VERSION_CODE%% --"
android:installLocation="auto">
<uses-sdk android:maxSdkVersion="25" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.any" android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
@@ -67,6 +70,9 @@
android:name="android.app.lib_name"
android:value="-- %%INSERT_APP_LIB_NAME%% --" />
<meta-data
android:name="android.app.extract_android_style"
android:value="minimal" />
</activity>
<activity
+1 -1
View File
@@ -33,7 +33,7 @@ android.library.defaults.buildfeatures.androidresources=false
# For development copy and set local values for these parameters in local.properties
#androidCompileSdkVersion=android-34
#androidBuildToolsVersion=34.0.0
#qtMinSdkVersion=26
#qtMinSdkVersion=24
#qtTargetSdkVersion=34
#androidNdkVersion=26.1.10909125
#qtTargetAbiList=x86_64
@@ -183,14 +183,6 @@ class OpenVpnClient(
// Never called more than once per tun_builder session.
override fun tun_builder_set_proxy_http(host: String, port: Int): Boolean {
Log.d(TAG, "tun_builder_set_proxy_http: $host, $port")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
try {
configBuilder.setHttpProxy(ProxyInfo.buildDirectProxy(host, port))
} catch (e: Exception) {
Log.e(TAG, "Could not set proxy: ${e.message}")
return false
}
}
return true
}
@@ -113,12 +113,7 @@ abstract class Protocol {
Log.d(TAG, "addRoute: $inetNetwork")
vpnBuilder.addRoute(inetNetwork)
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Log.d(TAG, "excludeRoute: $inetNetwork")
vpnBuilder.excludeRoute(inetNetwork)
} else {
Log.e(TAG, "Trying to exclude route $inetNetwork on old Android")
}
Log.e(TAG, "Trying to exclude route $inetNetwork on old Android")
}
}
@@ -135,13 +130,6 @@ abstract class Protocol {
Log.d(TAG, "setMtu: ${config.mtu}")
vpnBuilder.setMtu(config.mtu)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
config.httpProxy?.let {
Log.d(TAG, "setHttpProxy: $it")
vpnBuilder.setHttpProxy(it)
}
}
if (config.allowAllAF) {
Log.d(TAG, "allowFamily")
vpnBuilder.allowFamily(OsConstants.AF_INET)
@@ -151,8 +139,6 @@ abstract class Protocol {
Log.d(TAG, "setBlocking: ${config.blockingMode}")
vpnBuilder.setBlocking(config.blockingMode)
vpnBuilder.setUnderlyingNetworks(null)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
vpnBuilder.setMetered(false)
}
}
@@ -145,7 +145,7 @@ open class ProtocolConfig protected constructor(
}
// for older versions of Android, build a list of subnets without excluded routes
// and add them to routes
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU && routes.any { !it.include }) {
if (routes.any { !it.include }) {
val ipRangeSet = IpRangeSet()
routes.forEach {
if (it.include) ipRangeSet.add(IpRange(it.inetNetwork))
+1 -1
View File
@@ -21,5 +21,5 @@ android {
}
dependencies {
api(fileTree(mapOf("dir" to "../libs", "include" to listOf("*.jar"))))
implementation(fileTree(mapOf("dir" to "../libs", "include" to listOf("*.jar"))))
}
@@ -3,9 +3,7 @@ package org.amnezia.vpn
import android.Manifest
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.app.NotificationManager
import android.content.ActivityNotFoundException
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Intent
import android.content.Intent.EXTRA_MIME_TYPES
@@ -77,7 +75,6 @@ class AmneziaActivity : QtActivity() {
private var isWaitingStatus = true
private var isServiceConnected = false
private var isInBoundState = false
private var notificationStateReceiver: BroadcastReceiver? = null
private lateinit var vpnServiceMessenger: IpcMessenger
private var pfd: ParcelFileDescriptor? = null
@@ -186,7 +183,6 @@ class AmneziaActivity : QtActivity() {
doBindService()
}
)
registerBroadcastReceivers()
intent?.let(::processIntent)
runBlocking { vpnProto = proto.await() }
}
@@ -202,26 +198,6 @@ class AmneziaActivity : QtActivity() {
}
}
private fun registerBroadcastReceivers() {
notificationStateReceiver = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
registerBroadcastReceiver(
arrayOf(
NotificationManager.ACTION_NOTIFICATION_CHANNEL_BLOCK_STATE_CHANGED,
NotificationManager.ACTION_APP_BLOCK_STATE_CHANGED
)
) {
Log.v(
TAG, "Notification state changed: ${it?.action}, blocked = " +
"${it?.getBooleanExtra(NotificationManager.EXTRA_BLOCKED_STATE, false)}"
)
mainScope.launch {
qtInitialized.await()
QtAndroidController.onNotificationStateChanged()
}
}
} else null
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
Log.v(TAG, "onNewIntent: $intent")
@@ -267,8 +243,6 @@ class AmneziaActivity : QtActivity() {
override fun onDestroy() {
Log.d(TAG, "Destroy Amnezia activity")
unregisterBroadcastReceiver(notificationStateReceiver)
notificationStateReceiver = null
mainScope.cancel()
super.onDestroy()
}
@@ -747,7 +721,7 @@ class AmneziaActivity : QtActivity() {
}
@Suppress("unused")
fun isNotificationPermissionGranted(): Boolean = applicationContext.isNotificationPermissionGranted()
fun isNotificationPermissionGranted(): Boolean = true
@Suppress("unused")
fun requestNotificationPermission() {
@@ -847,67 +821,6 @@ class AmneziaActivity : QtActivity() {
0, 0, 1.0f, 1.0f, 0, 0, 0,0
)
// workaround for a bug in Qt that causes the mouse click event not to be handled
// also disable right-click, as it causes the application to crash
private var lastButtonState = 0
private fun MotionEvent.fixCopy(): MotionEvent = MotionEvent.obtain(
downTime,
eventTime,
action,
pointerCount,
(0 until pointerCount).map { i ->
MotionEvent.PointerProperties().apply {
getPointerProperties(i, this)
}
}.toTypedArray(),
(0 until pointerCount).map { i ->
MotionEvent.PointerCoords().apply {
getPointerCoords(i, this)
}
}.toTypedArray(),
metaState,
MotionEvent.BUTTON_PRIMARY,
xPrecision,
yPrecision,
deviceId,
edgeFlags,
source,
flags
)
private fun handleMouseEvent(ev: MotionEvent, superDispatch: (MotionEvent?) -> Boolean): Boolean {
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
lastButtonState = ev.buttonState
if (ev.buttonState == MotionEvent.BUTTON_SECONDARY) return true
}
MotionEvent.ACTION_UP -> {
when (lastButtonState) {
MotionEvent.BUTTON_SECONDARY -> return true
MotionEvent.BUTTON_PRIMARY -> {
val modEvent = ev.fixCopy()
return superDispatch(modEvent).apply { modEvent.recycle() }
}
}
}
}
return superDispatch(ev)
}
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
Log.v(TAG, "dispatchTouch: $ev")
if (ev != null && ev.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) {
return handleMouseEvent(ev) { super.dispatchTouchEvent(it) }
}
return super.dispatchTouchEvent(ev)
}
override fun dispatchTrackballEvent(ev: MotionEvent?): Boolean {
ev?.let { return handleMouseEvent(ev) { super.dispatchTrackballEvent(it) }}
return super.dispatchTrackballEvent(ev)
}
/**
* Utils methods
*/
@@ -1,12 +1,9 @@
package org.amnezia.vpn
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.net.VpnService
import android.os.Build
import android.os.IBinder
import android.os.Messenger
import android.service.quicksettings.Tile
@@ -148,7 +145,8 @@ class AmneziaTileService : TileService() {
Intent(this, AmneziaActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}.also {
startActivityAndCollapseCompat(it)
@Suppress("DEPRECATION")
startActivityAndCollapse(it)
}
}
}
@@ -192,7 +190,8 @@ class AmneziaTileService : TileService() {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
putExtra(EXTRA_PROTOCOL, vpnProto)
}.also {
startActivityAndCollapseCompat(it)
@Suppress("DEPRECATION")
startActivityAndCollapse(it)
}
false
} else {
@@ -216,23 +215,6 @@ class AmneziaTileService : TileService() {
private fun stopVpn() = vpnServiceMessenger.send(Action.DISCONNECT)
@SuppressLint("StartActivityAndCollapseDeprecated")
private fun startActivityAndCollapseCompat(intent: Intent) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startActivityAndCollapse(
PendingIntent.getActivity(
applicationContext,
0,
intent,
PendingIntent.FLAG_IMMUTABLE
)
)
} else {
@Suppress("DEPRECATION")
startActivityAndCollapse(intent)
}
}
private fun updateVpnState(state: ProtocolState) =
scope.launch { VpnStateStore.store { it.copy(protocolState = state) } }
@@ -249,17 +231,14 @@ class AmneziaTileService : TileService() {
when (val protocolState = vpnState.protocolState) {
CONNECTED -> {
state = Tile.STATE_ACTIVE
subtitleCompat = null
}
DISCONNECTED, UNKNOWN -> {
state = Tile.STATE_INACTIVE
subtitleCompat = null
}
CONNECTING, DISCONNECTING, RECONNECTING -> {
state = Tile.STATE_UNAVAILABLE
subtitleCompat = getString(protocolState)
}
}
updateTile()
@@ -267,17 +246,4 @@ class AmneziaTileService : TileService() {
// double update to fix weird visual glitches
tile.updateTile()
}
private var Tile.subtitleCompat: CharSequence?
set(value) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
this.subtitle = value
}
}
get() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
return this.subtitle
}
return null
}
}
@@ -3,14 +3,10 @@ package org.amnezia.vpn
import android.annotation.SuppressLint
import android.app.ActivityManager
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE
import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED
import android.net.VpnService
import android.os.Build
import android.os.Handler
import android.os.IBinder
import android.os.Looper
@@ -104,7 +100,6 @@ open class AmneziaVpnService : VpnService() {
private lateinit var networkState: NetworkState
private lateinit var trafficStats: TrafficStats
private var controlReceiver: BroadcastReceiver? = null
private var notificationStateReceiver: BroadcastReceiver? = null
private var screenOnReceiver: BroadcastReceiver? = null
private var screenOffReceiver: BroadcastReceiver? = null
private val clientMessengers = ConcurrentHashMap<Messenger, IpcMessenger>()
@@ -189,16 +184,6 @@ open class AmneziaVpnService : VpnService() {
Messenger(actionMessageHandler)
}
/**
* Notification setup
*/
private val foregroundServiceTypeCompat
get() = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE -> FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> FOREGROUND_SERVICE_TYPE_MANIFEST
else -> 0
}
private val serviceNotification: ServiceNotification by lazy(NONE) { ServiceNotification(this) }
/**
@@ -232,7 +217,7 @@ open class AmneziaVpnService : VpnService() {
ServiceCompat.startForeground(
this, NOTIFICATION_ID,
serviceNotification.buildNotification(serverName, vpnProto?.label, protocolState.value),
foregroundServiceTypeCompat
0
)
return START_REDELIVER_INTENT
}
@@ -309,23 +294,6 @@ open class AmneziaVpnService : VpnService() {
}
}
notificationStateReceiver = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
registerBroadcastReceiver(
arrayOf(
NotificationManager.ACTION_NOTIFICATION_CHANNEL_BLOCK_STATE_CHANGED,
NotificationManager.ACTION_APP_BLOCK_STATE_CHANGED
)
) {
val state = it?.getBooleanExtra(NotificationManager.EXTRA_BLOCKED_STATE, false)
Log.v(TAG, "Notification state changed: ${it?.action}, blocked = $state")
if (state == false) {
enableNotification()
} else {
disableNotification()
}
}
} else null
registerScreenStateBroadcastReceivers()
}
@@ -353,10 +321,8 @@ open class AmneziaVpnService : VpnService() {
private fun unregisterBroadcastReceivers() {
Log.d(TAG, "Unregister broadcast receivers")
unregisterBroadcastReceiver(controlReceiver)
unregisterBroadcastReceiver(notificationStateReceiver)
unregisterScreenStateBroadcastReceivers()
controlReceiver = null
notificationStateReceiver = null
}
/**
@@ -1,19 +1,15 @@
package org.amnezia.vpn
import android.Manifest.permission
import android.annotation.SuppressLint
import android.app.Notification
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.app.NotificationChannelCompat.Builder
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.Action
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import org.amnezia.vpn.protocol.ProtocolState
import org.amnezia.vpn.protocol.ProtocolState.CONNECTED
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
@@ -85,27 +81,17 @@ class ServiceNotification(private val context: Context) {
.setSubText(getSpeedString(speed))
.build()
fun isNotificationEnabled(): Boolean {
if (!context.isNotificationPermissionGranted()) return false
if (!notificationManager.areNotificationsEnabled()) return false
return notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID)?.let {
it.importance != NotificationManager.IMPORTANCE_NONE
} ?: true
}
fun isNotificationEnabled(): Boolean = notificationManager.areNotificationsEnabled()
@SuppressLint("MissingPermission")
fun updateNotification(serverName: String?, protocol: String?, state: ProtocolState) {
if (context.isNotificationPermissionGranted()) {
Log.v(TAG, "Update notification: $serverName, $state")
notificationManager.notify(NOTIFICATION_ID, buildNotification(serverName, protocol, state))
}
Log.v(TAG, "Update notification: $serverName, $state")
notificationManager.notify(NOTIFICATION_ID, buildNotification(serverName, protocol, state))
}
@SuppressLint("MissingPermission")
fun updateSpeed(speed: TrafficData) {
if (context.isNotificationPermissionGranted()) {
notificationManager.notify(NOTIFICATION_ID, buildNotification(speed))
}
notificationManager.notify(NOTIFICATION_ID, buildNotification(speed))
}
private fun getSpeedString(traffic: TrafficData) =
@@ -166,8 +152,3 @@ class ServiceNotification(private val context: Context) {
}
}
}
fun Context.isNotificationPermissionGranted(): Boolean =
Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU ||
ContextCompat.checkSelfPermission(this, permission.POST_NOTIFICATIONS) ==
PackageManager.PERMISSION_GRANTED
@@ -7,7 +7,6 @@ import android.content.Intent
import android.content.res.Configuration.UI_MODE_NIGHT_MASK
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.net.VpnService
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.widget.Toast
@@ -31,12 +30,9 @@ class VpnRequestActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(TAG, "Start request activity")
vpnProto = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.extras?.getSerializable(EXTRA_PROTOCOL, VpnProto::class.java)
} else {
@Suppress("DEPRECATION")
intent.extras?.getSerializable(EXTRA_PROTOCOL) as VpnProto
}
@Suppress("DEPRECATION")
vpnProto = intent.extras?.getSerializable(EXTRA_PROTOCOL) as VpnProto
val requestIntent = VpnService.prepare(applicationContext)
if (requestIntent != null) {
if (getSystemService<KeyguardManager>()!!.isKeyguardLocked) {
+11 -6
View File
@@ -1,6 +1,9 @@
package org.amnezia.vpn.util
import android.content.Context
import android.icu.text.DateFormat
import android.icu.text.SimpleDateFormat
import android.icu.util.TimeZone
import android.os.Build
import android.os.Process
import java.io.File
@@ -8,10 +11,8 @@ import java.io.IOException
import java.io.RandomAccessFile
import java.nio.channels.FileChannel
import java.nio.channels.FileLock
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.time.ZonedDateTime
import java.time.ZoneOffset
import java.util.Date
import java.util.Locale
import java.util.concurrent.locks.ReentrantLock
import org.amnezia.vpn.util.Log.Priority.D
import org.amnezia.vpn.util.Log.Priority.E
@@ -39,7 +40,11 @@ private const val LOG_MAX_FILE_SIZE = 1024 * 1024
* | | | create a report and/or terminate the process |
*/
object Log {
private val dateTimeFormat: DateTimeFormatter = DateTimeFormatter.ofPattern(DATE_TIME_PATTERN)
private val dateTimeFormat = object : ThreadLocal<DateFormat>() {
override fun initialValue(): DateFormat = SimpleDateFormat(DATE_TIME_PATTERN, Locale.US).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
}
private lateinit var logDir: File
private val logFile: File by lazy { File(logDir, LOG_FILE_NAME) }
@@ -137,7 +142,7 @@ object Log {
}
private fun formatLogMsg(tag: String, msg: String, priority: Priority): String {
val utcDate = ZonedDateTime.now(ZoneOffset.UTC).format(dateTimeFormat)
val utcDate = dateTimeFormat.get()?.format(Date())
return "${utcDate}Z ${Process.myPid()} ${Process.myTid()} $priority [${Thread.currentThread().name}] " +
"$tag: $msg\n"
}
@@ -8,11 +8,9 @@ import android.net.NetworkCapabilities
import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
import android.net.NetworkRequest
import android.os.Build
import android.os.Handler
import androidx.core.content.getSystemService
import kotlin.LazyThreadSafetyMode.NONE
import kotlinx.coroutines.delay
import org.amnezia.vpn.util.Log
private const val TAG = "NetworkState"
@@ -47,7 +45,9 @@ class NetworkState(
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
Log.v(TAG, "onCapabilitiesChanged: $network, $networkCapabilities")
checkNetworkState(network, networkCapabilities)
handler.post {
checkNetworkState(network, networkCapabilities)
}
}
private fun checkNetworkState(network: Network, networkCapabilities: NetworkCapabilities) {
@@ -76,33 +76,10 @@ class NetworkState(
}
}
suspend fun bindNetworkListener() {
fun bindNetworkListener() {
if (isListenerBound) return
Log.d(TAG, "Bind network listener")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
connectivityManager.registerBestMatchingNetworkCallback(networkRequest, networkCallback, handler)
} else {
val numberAttempts = 300
var attemptCount = 0
while(true) {
try {
connectivityManager.requestNetwork(networkRequest, networkCallback, handler)
break
} catch (e: SecurityException) {
Log.e(TAG, "Failed to bind network listener: $e")
// Android 11 bug: https://issuetracker.google.com/issues/175055271
if (e.message?.startsWith("Package android does not belong to") == true) {
if (++attemptCount > numberAttempts) {
throw e
}
delay(1000)
continue
} else {
throw e
}
}
}
}
connectivityManager.requestNetwork(networkRequest, networkCallback)
isListenerBound = true
}
@@ -1,7 +1,6 @@
package org.amnezia.vpn.util.net
import android.net.TrafficStats
import android.os.Build
import android.os.Process
import android.os.SystemClock
import kotlin.math.roundToLong
@@ -17,18 +16,12 @@ class TrafficStats {
private var lastTrafficData = TrafficData.ZERO
private var lastTimestamp = 0L
private val getTrafficDataCompat: () -> TrafficData =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val iface = "tun0"
fun(): TrafficData {
return TrafficData(TrafficStats.getRxBytes(iface), TrafficStats.getTxBytes(iface))
}
} else {
val uid = Process.myUid()
fun(): TrafficData {
return TrafficData(TrafficStats.getUidRxBytes(uid), TrafficStats.getUidTxBytes(uid))
}
private val getTrafficDataCompat: () -> TrafficData = run {
val uid = Process.myUid()
fun(): TrafficData {
return TrafficData(TrafficStats.getUidRxBytes(uid), TrafficStats.getUidTxBytes(uid))
}
}
fun reset() {
lastTrafficData = getTrafficDataCompat()
+1 -1
View File
@@ -1,6 +1,6 @@
message("Client android ${CMAKE_ANDROID_ARCH_ABI} build")
set(APP_ANDROID_MIN_SDK 26)
set(APP_ANDROID_MIN_SDK 24)
set(ANDROID_PLATFORM "android-${APP_ANDROID_MIN_SDK}" CACHE STRING
"The minimum API level supported by the application or library" FORCE)
+15 -4
View File
@@ -136,10 +136,21 @@ set_property(TARGET ${PROJECT} APPEND PROPERTY RESOURCE
add_subdirectory(ios/networkextension)
add_dependencies(${PROJECT} networkextension)
set_property(TARGET ${PROJECT} PROPERTY XCODE_EMBED_FRAMEWORKS
"${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-ios/OpenVPNAdapter.framework"
set(OPENVPN_FRAMEWORK_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-ios")
set(OPENVPN_EMBEDDED_FRAMEWORKS
"${OPENVPN_FRAMEWORK_DIR}/OpenVPNAdapter.framework"
"${OPENVPN_FRAMEWORK_DIR}/OpenVPNClient.framework"
"${OPENVPN_FRAMEWORK_DIR}/mbedTLS.framework"
"${OPENVPN_FRAMEWORK_DIR}/LZ4.framework"
)
set(CMAKE_XCODE_ATTRIBUTE_FRAMEWORK_SEARCH_PATHS ${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-ios/)
target_link_libraries("networkextension" PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-ios/OpenVPNAdapter.framework")
set_property(TARGET ${PROJECT} PROPERTY XCODE_EMBED_FRAMEWORKS "${OPENVPN_EMBEDDED_FRAMEWORKS}")
set(CMAKE_XCODE_ATTRIBUTE_FRAMEWORK_SEARCH_PATHS "$(inherited) ${OPENVPN_FRAMEWORK_DIR}")
foreach(_framework ${OPENVPN_EMBEDDED_FRAMEWORKS})
target_link_libraries(networkextension PRIVATE "${_framework}")
endforeach()
set_property(TARGET networkextension PROPERTY XCODE_EMBED_FRAMEWORKS "${OPENVPN_EMBEDDED_FRAMEWORKS}")
set_property(TARGET networkextension PROPERTY XCODE_EMBED_FRAMEWORKS_CODE_SIGN_ON_COPY ON)
set_property(TARGET networkextension PROPERTY XCODE_ATTRIBUTE_FRAMEWORK_SEARCH_PATHS "$(inherited) ${OPENVPN_FRAMEWORK_DIR}")
+20 -2
View File
@@ -83,12 +83,30 @@ QString OpenVpnConfigurator::createConfig(const ServerCredentials &credentials,
return "";
}
auto sanitizeStaticKey = [](const QString &key) {
QStringList lines = key.split('\n');
QStringList filtered;
filtered.reserve(lines.size());
for (const QString &line : lines) {
const QString trimmed = line.trimmed();
if (trimmed.startsWith('#')) {
continue;
}
filtered.append(line);
}
QString result = filtered.join('\n');
if (!result.endsWith('\n')) {
result.append('\n');
}
return result;
};
config.replace("$OPENVPN_CA_CERT", connData.caCert);
config.replace("$OPENVPN_CLIENT_CERT", connData.clientCert);
config.replace("$OPENVPN_PRIV_KEY", connData.privKey);
if (config.contains("$OPENVPN_TA_KEY")) {
config.replace("$OPENVPN_TA_KEY", connData.taKey);
config.replace("$OPENVPN_TA_KEY", sanitizeStaticKey(connData.taKey));
} else {
config.replace("<tls-auth>", "");
config.replace("</tls-auth>", "");
@@ -117,7 +135,7 @@ QString OpenVpnConfigurator::processConfigWithLocalSettings(const QPair<QString,
if (!isApiConfig) {
QRegularExpression regex("redirect-gateway.*");
config.replace(regex, "");
// We don't use secondary DNS if primary DNS is AmneziaDNS
if (dns.first.contains(protocols::dns::amneziaDnsIp)) {
QRegularExpression dnsRegex("dhcp-option DNS " + dns.second);
+4
View File
@@ -64,6 +64,10 @@ namespace apiDefs
constexpr QLatin1String id("id");
constexpr QLatin1String orderId("order_id");
constexpr QLatin1String migrationCode("migration_code");
constexpr QLatin1String transactionId("transaction_id");
constexpr QLatin1String userCountryCode("user_country_code");
}
const int requestTimeoutMsecs = 12 * 1000; // 12 secs
+60 -12
View File
@@ -82,7 +82,9 @@ apiDefs::ConfigSource apiUtils::getConfigSource(const QJsonObject &serverConfigO
return static_cast<apiDefs::ConfigSource>(serverConfigObject.value(apiDefs::key::configVersion).toInt());
}
amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &sslErrors, QNetworkReply *reply)
amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &sslErrors, const QString &replyErrorString,
const QNetworkReply::NetworkError &replyError, const int httpStatusCode,
const QByteArray &responseBody)
{
const int httpStatusCodeConflict = 409;
const int httpStatusCodeNotFound = 404;
@@ -90,21 +92,19 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
if (!sslErrors.empty()) {
qDebug().noquote() << sslErrors;
return amnezia::ErrorCode::ApiConfigSslError;
} else if (reply->error() == QNetworkReply::NoError) {
} else if (replyError == QNetworkReply::NoError) {
return amnezia::ErrorCode::NoError;
} else if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError
|| reply->error() == QNetworkReply::NetworkError::TimeoutError) {
qDebug() << reply->error();
} else if (replyError == QNetworkReply::NetworkError::OperationCanceledError
|| replyError == QNetworkReply::NetworkError::TimeoutError) {
qDebug() << replyError;
return amnezia::ErrorCode::ApiConfigTimeoutError;
} else if (reply->error() == QNetworkReply::NetworkError::OperationNotImplementedError) {
qDebug() << reply->error();
} else if (replyError == QNetworkReply::NetworkError::OperationNotImplementedError) {
qDebug() << replyError;
return amnezia::ErrorCode::ApiUpdateRequestError;
} else {
QString err = reply->errorString();
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
qDebug() << QString::fromUtf8(reply->readAll());
qDebug() << reply->error();
qDebug() << err;
qDebug() << QString::fromUtf8(responseBody);
qDebug() << replyError;
qDebug() << replyErrorString;
qDebug() << httpStatusCode;
if (httpStatusCode == httpStatusCodeConflict) {
return amnezia::ErrorCode::ApiConfigLimitError;
@@ -162,3 +162,51 @@ QString apiUtils::getPremiumV1VpnKey(const QJsonObject &serverConfigObject)
return QString("vpn://%1").arg(QString(signedData.toBase64(QByteArray::Base64UrlEncoding)));
}
QString apiUtils::getPremiumV2VpnKey(const QJsonObject &serverConfigObject)
{
if (apiUtils::getConfigType(serverConfigObject) != apiDefs::ConfigType::AmneziaPremiumV2) {
return {};
}
QString vpnKeyText = "";
auto apiConfig = serverConfigObject.value(apiDefs::key::apiConfig).toObject();
auto authData = serverConfigObject.value(QLatin1String("auth_data")).toObject();
const QString name = serverConfigObject.value(apiDefs::key::name).toString();
const QString description = serverConfigObject.value(apiDefs::key::description).toString();
const double configVersion = serverConfigObject.value(apiDefs::key::configVersion).toDouble();
const QString serviceType = apiConfig.value(apiDefs::key::serviceType).toString();
const QString serviceProtocol = apiConfig.value(QLatin1String("service_protocol")).toString();
const QString userCountryCode = apiConfig.value(QLatin1String("user_country_code")).toString();
const QString apiKey = authData.value(apiDefs::key::apiKey).toString();
QString vpnKeyStr = "{";
vpnKeyStr += "\"" + QString(apiDefs::key::name) + "\": \"" + name + "\", ";
vpnKeyStr += "\"" + QString(apiDefs::key::description) + "\": \"" + description + "\", ";
vpnKeyStr += "\"" + QString(apiDefs::key::configVersion) + "\": " + QString::number(static_cast<int>(configVersion)) + ", ";
vpnKeyStr += "\"" + QString(apiDefs::key::apiConfig) + "\": {";
vpnKeyStr += "\"" + QString(apiDefs::key::serviceType) + "\": \"" + serviceType + "\", ";
vpnKeyStr += "\"service_protocol\": \"" + serviceProtocol + "\", ";
vpnKeyStr += "\"user_country_code\": \"" + userCountryCode + "\"";
vpnKeyStr += "}, ";
vpnKeyStr += "\"auth_data\": {";
vpnKeyStr += "\"" + QString(apiDefs::key::apiKey) + "\": \"" + apiKey + "\"";
vpnKeyStr += "}";
vpnKeyStr += "}";
QByteArray vpnKeyCompressed = escapeUnicode(vpnKeyStr).toUtf8();
vpnKeyCompressed = qCompress(vpnKeyCompressed, 6);
vpnKeyCompressed = vpnKeyCompressed.mid(4);
QByteArray signedData = AMNEZIA_CONFIG_SIGNATURE + vpnKeyCompressed;
vpnKeyText = QString("vpn://%1").arg(QString(signedData.toBase64(QByteArray::Base64UrlEncoding)));
return vpnKeyText;
}
+4 -1
View File
@@ -18,9 +18,12 @@ namespace apiUtils
apiDefs::ConfigType getConfigType(const QJsonObject &serverConfigObject);
apiDefs::ConfigSource getConfigSource(const QJsonObject &serverConfigObject);
amnezia::ErrorCode checkNetworkReplyErrors(const QList<QSslError> &sslErrors, QNetworkReply *reply);
amnezia::ErrorCode checkNetworkReplyErrors(const QList<QSslError> &sslErrors, const QString &replyErrorString,
const QNetworkReply::NetworkError &replyError, const int httpStatusCode,
const QByteArray &responseBody);
QString getPremiumV1VpnKey(const QJsonObject &serverConfigObject);
QString getPremiumV2VpnKey(const QJsonObject &serverConfigObject);
}
#endif // APIUTILS_H
@@ -99,6 +99,9 @@ void CoreController::initModels()
m_apiDevicesModel.reset(new ApiDevicesModel(m_settings, this));
m_engine->rootContext()->setContextProperty("ApiDevicesModel", m_apiDevicesModel.get());
m_newsModel.reset(new NewsModel(m_settings, this));
m_engine->rootContext()->setContextProperty("NewsModel", m_newsModel.get());
}
void CoreController::initControllers()
@@ -153,6 +156,9 @@ void CoreController::initControllers()
m_apiPremV1MigrationController.reset(new ApiPremV1MigrationController(m_serversModel, m_settings, this));
m_engine->rootContext()->setContextProperty("ApiPremV1MigrationController", m_apiPremV1MigrationController.get());
m_apiNewsController.reset(new ApiNewsController(m_newsModel, m_settings, m_serversModel, this));
m_engine->rootContext()->setContextProperty("ApiNewsController", m_apiNewsController.get());
}
void CoreController::initAndroidController()
@@ -316,6 +322,11 @@ void CoreController::initContainerModelUpdateHandler()
connect(m_serversModel.get(), &ServersModel::containersUpdated, m_containersModel.get(), &ContainersModel::updateModel);
connect(m_serversModel.get(), &ServersModel::defaultServerContainersUpdated, m_defaultServerContainersModel.get(),
&ContainersModel::updateModel);
connect(m_serversModel.get(), &ServersModel::gatewayStacksExpanded, this, [this]() {
if (m_serversModel->hasServersFromGatewayApi()) {
m_apiNewsController->fetchNews();
}
});
m_serversModel->resetModel();
}
+4
View File
@@ -12,6 +12,7 @@
#include "ui/controllers/api/apiConfigsController.h"
#include "ui/controllers/api/apiSettingsController.h"
#include "ui/controllers/api/apiPremV1MigrationController.h"
#include "ui/controllers/api/apiNewsController.h"
#include "ui/controllers/appSplitTunnelingController.h"
#include "ui/controllers/allowedDnsController.h"
#include "ui/controllers/connectionController.h"
@@ -47,6 +48,7 @@
#include "ui/models/services/sftpConfigModel.h"
#include "ui/models/services/socks5ProxyConfigModel.h"
#include "ui/models/sites_model.h"
#include "ui/models/newsModel.h"
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
#include "ui/notificationhandler.h"
@@ -118,6 +120,7 @@ private:
QScopedPointer<ApiSettingsController> m_apiSettingsController;
QScopedPointer<ApiConfigsController> m_apiConfigsController;
QScopedPointer<ApiPremV1MigrationController> m_apiPremV1MigrationController;
QScopedPointer<ApiNewsController> m_apiNewsController;
QSharedPointer<ContainersModel> m_containersModel;
QSharedPointer<ContainersModel> m_defaultServerContainersModel;
@@ -125,6 +128,7 @@ private:
QSharedPointer<LanguageModel> m_languageModel;
QSharedPointer<ProtocolsModel> m_protocolsModel;
QSharedPointer<SitesModel> m_sitesModel;
QSharedPointer<NewsModel> m_newsModel;
QSharedPointer<AllowedDnsModel> m_allowedDnsModel;
QSharedPointer<AppSplitTunnelingModel> m_appSplitTunnelingModel;
QSharedPointer<ClientManagementModel> m_clientManagementModel;
+55 -98
View File
@@ -50,69 +50,6 @@ GatewayController::GatewayController(const QString &gatewayEndpoint, const bool
{
}
ErrorCode GatewayController::get(const QString &endpoint, QByteArray &responseBody)
{
#ifdef Q_OS_IOS
IosController::Instance()->requestInetAccess();
QThread::msleep(10);
#endif
QNetworkRequest request;
request.setTransferTimeout(m_requestTimeoutMsecs);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader(QString("X-Client-Request-ID").toUtf8(), QUuid::createUuid().toString(QUuid::WithoutBraces).toUtf8());
request.setUrl(QString(endpoint).arg(m_proxyUrl.isEmpty() ? m_gatewayEndpoint : m_proxyUrl));
// bypass killSwitch exceptions for API-gateway
#ifdef AMNEZIA_DESKTOP
if (m_isStrictKillSwitchEnabled) {
QString host = QUrl(request.url()).host();
QString ip = NetworkUtilities::getIPAddress(host);
if (!ip.isEmpty()) {
IpcClient::Interface()->addKillSwitchAllowedRange(QStringList { ip });
}
}
#endif
QNetworkReply *reply;
reply = amnApp->networkManager()->get(request);
QEventLoop wait;
QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
QList<QSslError> sslErrors;
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec();
responseBody = reply->readAll();
if (sslErrors.isEmpty() && shouldBypassProxy(reply, responseBody, false)) {
auto requestFunction = [&request, &responseBody](const QString &url) {
request.setUrl(url);
return amnApp->networkManager()->get(request);
};
auto replyProcessingFunction = [&responseBody, &reply, &sslErrors, this](QNetworkReply *nestedReply,
const QList<QSslError> &nestedSslErrors) {
responseBody = nestedReply->readAll();
if (!sslErrors.isEmpty() || !shouldBypassProxy(nestedReply, responseBody, false)) {
sslErrors = nestedSslErrors;
reply = nestedReply;
return true;
}
return false;
};
bypassProxy(endpoint, reply, requestFunction, replyProcessingFunction);
}
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, reply);
reply->deleteLater();
return errorCode;
}
ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody)
{
#ifdef Q_OS_IOS
@@ -188,29 +125,37 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
wait.exec();
QByteArray encryptedResponseBody = reply->readAll();
QString replyErrorString = reply->errorString();
auto replyError = reply->error();
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (sslErrors.isEmpty() && shouldBypassProxy(reply, encryptedResponseBody, true, key, iv, salt)) {
reply->deleteLater();
if (sslErrors.isEmpty() && shouldBypassProxy(replyError, encryptedResponseBody, true, key, iv, salt)) {
auto requestFunction = [&request, &encryptedResponseBody, &requestBody](const QString &url) {
request.setUrl(url);
return amnApp->networkManager()->post(request, QJsonDocument(requestBody).toJson());
};
auto replyProcessingFunction = [&encryptedResponseBody, &reply, &sslErrors, &key, &iv, &salt,
this](QNetworkReply *nestedReply, const QList<QSslError> &nestedSslErrors) {
encryptedResponseBody = nestedReply->readAll();
reply = nestedReply;
if (!sslErrors.isEmpty() || shouldBypassProxy(nestedReply, encryptedResponseBody, true, key, iv, salt)) {
auto replyProcessingFunction = [&encryptedResponseBody, &replyErrorString, &replyError, &httpStatusCode, &sslErrors, &key, &iv,
&salt, this](QNetworkReply *reply, const QList<QSslError> &nestedSslErrors) {
encryptedResponseBody = reply->readAll();
replyErrorString = reply->errorString();
replyError = reply->error();
httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (!sslErrors.isEmpty() || shouldBypassProxy(replyError, encryptedResponseBody, true, key, iv, salt)) {
sslErrors = nestedSslErrors;
return false;
}
return true;
};
bypassProxy(endpoint, reply, requestFunction, replyProcessingFunction);
auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString("");
auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString("");
bypassProxy(endpoint, serviceType, userCountryCode, requestFunction, replyProcessingFunction);
}
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, reply);
reply->deleteLater();
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, encryptedResponseBody);
if (errorCode) {
return errorCode;
}
@@ -225,7 +170,7 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
}
}
QStringList GatewayController::getProxyUrls()
QStringList GatewayController::getProxyUrls(const QString &serviceType, const QString &userCountryCode)
{
QNetworkRequest request;
request.setTransferTimeout(m_requestTimeoutMsecs);
@@ -235,15 +180,26 @@ QStringList GatewayController::getProxyUrls()
QList<QSslError> sslErrors;
QNetworkReply *reply;
QStringList proxyStorageUrls;
QStringList baseUrls;
if (m_isDevEnvironment) {
proxyStorageUrls = QString(DEV_S3_ENDPOINT).split(", ");
baseUrls = QString(DEV_S3_ENDPOINT).split(", ");
} else {
proxyStorageUrls = QString(PROD_S3_ENDPOINT).split(", ");
baseUrls = QString(PROD_S3_ENDPOINT).split(", ");
}
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
QStringList proxyStorageUrls;
if (!serviceType.isEmpty()) {
for (const auto &baseUrl : baseUrls) {
QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8();
proxyStorageUrls.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) + ".json");
}
}
for (const auto &baseUrl : baseUrls) {
proxyStorageUrls.push_back(baseUrl + "endpoints.json");
}
for (const auto &proxyStorageUrl : proxyStorageUrls) {
request.setUrl(proxyStorageUrl);
reply = amnApp->networkManager()->get(request);
@@ -288,7 +244,10 @@ QStringList GatewayController::getProxyUrls()
}
return endpoints;
} else {
apiUtils::checkNetworkReplyErrors(sslErrors, reply);
auto replyError = reply->error();
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
qDebug() << replyError;
qDebug() << httpStatusCode;
qDebug() << "go to the next storage endpoint";
reply->deleteLater();
@@ -297,33 +256,33 @@ QStringList GatewayController::getProxyUrls()
return {};
}
bool GatewayController::shouldBypassProxy(QNetworkReply *reply, const QByteArray &responseBody, bool checkEncryption, const QByteArray &key,
const QByteArray &iv, const QByteArray &salt)
bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &responseBody,
bool checkEncryption, const QByteArray &key, const QByteArray &iv, const QByteArray &salt)
{
if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError || reply->error() == QNetworkReply::NetworkError::TimeoutError) {
if (replyError == QNetworkReply::NetworkError::OperationCanceledError || replyError == QNetworkReply::NetworkError::TimeoutError) {
qDebug() << "timeout occurred";
qDebug() << reply->error();
qDebug() << replyError;
return true;
} else if (responseBody.contains("html")) {
qDebug() << "the response contains an html tag";
return true;
} else if (reply->error() == QNetworkReply::NetworkError::ContentNotFoundError) {
} else if (replyError == QNetworkReply::NetworkError::ContentNotFoundError) {
if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2)
|| responseBody.contains(errorResponsePattern3)) {
return false;
} else {
qDebug() << reply->error();
qDebug() << replyError;
return true;
}
} else if (reply->error() == QNetworkReply::NetworkError::OperationNotImplementedError) {
} else if (replyError == QNetworkReply::NetworkError::OperationNotImplementedError) {
if (responseBody.contains(updateRequestResponsePattern)) {
return false;
} else {
qDebug() << reply->error();
qDebug() << replyError;
return true;
}
} else if (reply->error() != QNetworkReply::NetworkError::NoError) {
qDebug() << reply->error();
} else if (replyError != QNetworkReply::NetworkError::NoError) {
qDebug() << replyError;
return true;
} else if (checkEncryption) {
try {
@@ -337,35 +296,33 @@ bool GatewayController::shouldBypassProxy(QNetworkReply *reply, const QByteArray
return false;
}
void GatewayController::bypassProxy(const QString &endpoint, QNetworkReply *reply,
void GatewayController::bypassProxy(const QString &endpoint, const QString &serviceType, const QString &userCountryCode,
std::function<QNetworkReply *(const QString &url)> requestFunction,
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction)
{
QStringList proxyUrls = getProxyUrls();
QStringList proxyUrls = getProxyUrls(serviceType, userCountryCode);
std::random_device randomDevice;
std::mt19937 generator(randomDevice());
std::shuffle(proxyUrls.begin(), proxyUrls.end(), generator);
QByteArray responseBody;
auto bypassFunction = [this](const QString &endpoint, const QString &proxyUrl, QNetworkReply *reply,
auto bypassFunction = [this](const QString &endpoint, const QString &proxyUrl,
std::function<QNetworkReply *(const QString &url)> requestFunction,
std::function<bool(QNetworkReply * reply, const QList<QSslError> &sslErrors)> replyProcessingFunction) {
QEventLoop wait;
QList<QSslError> sslErrors;
qDebug() << "go to the next proxy endpoint";
reply->deleteLater(); // delete the previous reply
reply = requestFunction(endpoint.arg(proxyUrl));
QNetworkReply *reply = requestFunction(endpoint.arg(proxyUrl));
QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec();
if (replyProcessingFunction(reply, sslErrors)) {
return true;
}
return false;
auto result = replyProcessingFunction(reply, sslErrors);
reply->deleteLater();
return result;
};
if (m_proxyUrl.isEmpty()) {
@@ -399,13 +356,13 @@ void GatewayController::bypassProxy(const QString &endpoint, QNetworkReply *repl
}
if (!m_proxyUrl.isEmpty()) {
if (bypassFunction(endpoint, m_proxyUrl, reply, requestFunction, replyProcessingFunction)) {
if (bypassFunction(endpoint, m_proxyUrl, requestFunction, replyProcessingFunction)) {
return;
}
}
for (const QString &proxyUrl : proxyUrls) {
if (bypassFunction(endpoint, proxyUrl, reply, requestFunction, replyProcessingFunction)) {
if (bypassFunction(endpoint, proxyUrl, requestFunction, replyProcessingFunction)) {
m_proxyUrl = proxyUrl;
break;
}
+5 -5
View File
@@ -18,14 +18,14 @@ public:
explicit GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
const bool isStrictKillSwitchEnabled, QObject *parent = nullptr);
amnezia::ErrorCode get(const QString &endpoint, QByteArray &responseBody);
amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody);
private:
QStringList getProxyUrls();
bool shouldBypassProxy(QNetworkReply *reply, const QByteArray &responseBody, bool checkEncryption, const QByteArray &key = "",
const QByteArray &iv = "", const QByteArray &salt = "");
void bypassProxy(const QString &endpoint, QNetworkReply *reply, std::function<QNetworkReply *(const QString &url)> requestFunction,
QStringList getProxyUrls(const QString &serviceType, const QString &userCountryCode);
bool shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &responseBody, bool checkEncryption,
const QByteArray &key = "", const QByteArray &iv = "", const QByteArray &salt = "");
void bypassProxy(const QString &endpoint, const QString &serviceType, const QString &userCountryCode,
std::function<QNetworkReply *(const QString &url)> requestFunction,
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction);
int m_requestTimeoutMsecs;
+14
View File
@@ -0,0 +1,14 @@
<svg width="24" height="24" viewBox="0 0 74 74" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_4_34)">
<path d="M55.5 12.3333H18.5C15.0942 12.3333 12.3333 15.0943 12.3333 18.5V55.5C12.3333 58.9058 15.0942 61.6667 18.5 61.6667H55.5C58.9057 61.6667 61.6666 58.9058 61.6666 55.5V18.5C61.6666 15.0943 58.9057 12.3333 55.5 12.3333Z" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21.5833 24.6667H52.4167" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21.5833 37H52.4167" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21.5833 49.3333H40.0833" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="61.5" cy="12.5" r="15" fill="#FBB36B" stroke="#1C1D21" stroke-width="5"/>
</g>
<defs>
<clipPath id="clip0_4_34">
<rect width="74" height="74" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 982 B

+8
View File
@@ -0,0 +1,8 @@
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#CBCAC8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<!-- Основа газеты -->
<rect x="4" y="4" width="16" height="16" rx="2"/>
<!-- Линии текста -->
<line x1="7" y1="8" x2="17" y2="8"/>
<line x1="7" y1="12" x2="17" y2="12"/>
<line x1="7" y1="16" x2="13" y2="16"/>
</svg>

After

Width:  |  Height:  |  Size: 410 B

+5
View File
@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.0001 12C14.0001 10.8954 13.1047 10 12.0001 10C10.8955 10 10.0001 10.8954 10.0001 12C10.0001 13.1046 10.8955 14 12.0001 14C13.1047 14 14.0001 13.1046 14.0001 12ZM16.0001 12C16.0001 14.2091 14.2092 16 12.0001 16C9.79094 16 8.00008 14.2091 8.00008 12C8.00008 9.79086 9.79094 8 12.0001 8C14.2092 8 16.0001 9.79086 16.0001 12Z" fill="#D7D8DB"/>
<path d="M20.5001 6C21.8808 6 23.0001 4.88071 23.0001 3.5C23.0001 2.11929 21.8808 1 20.5001 1C19.1194 1 18.0001 2.11929 18.0001 3.5C18.0001 4.88071 19.1194 6 20.5001 6Z" fill="#FBB36A"/>
<path d="M12.2198 1C12.9657 1 13.6811 1.28098 14.2315 1.7793C14.0814 2.32745 14.0001 2.90421 14.0001 3.5C14.0001 4.75704 14.3578 5.93021 14.9757 6.9248L14.7198 6.77539C14.2644 6.51244 13.8864 6.13489 13.6231 5.67969C13.3599 5.22423 13.2203 4.70672 13.2198 4.18066V4C13.2198 3.73478 13.1144 3.48051 12.9268 3.29297C12.7393 3.10557 12.4849 3 12.2198 3H11.7803C11.5153 3.00005 11.2608 3.10556 11.0733 3.29297C10.8858 3.4805 10.7803 3.73482 10.7803 4V4.18066C10.7798 4.70675 10.6403 5.22422 10.377 5.67969C10.1138 6.1349 9.73575 6.51343 9.28035 6.77637L9.27937 6.77539L8.85261 7.02441L8.84968 7.02637C8.39376 7.28946 7.87607 7.42773 7.34968 7.42773C6.82327 7.42764 6.30558 7.28958 5.84968 7.02637L5.72956 6.96191L5.70027 6.94629C5.47084 6.81401 5.1983 6.77827 4.94245 6.84668C4.71846 6.90662 4.52295 7.04188 4.38874 7.22852L4.33503 7.31152L4.11531 7.69141C3.98385 7.92039 3.94855 8.19212 4.01667 8.44727C4.07655 8.6712 4.21199 8.86674 4.39851 9.00098L4.48152 9.05469L4.53523 9.08789L4.68464 9.1875C5.11379 9.44747 5.47116 9.8098 5.7237 10.2441C5.98685 10.6968 6.12679 11.2108 6.12995 11.7344V12.2461L6.1241 12.4434C6.09561 12.9034 5.96204 13.3518 5.73152 13.7529C5.47084 14.2065 5.09507 14.5827 4.64363 14.8467L4.6446 14.8477L4.49421 14.9375L4.48152 14.9453C4.25235 15.0781 4.08509 15.2969 4.01667 15.5527C3.94855 15.8079 3.98385 16.0796 4.11531 16.3086L4.33406 16.6885C4.4668 16.9177 4.68662 17.0849 4.94245 17.1533C5.1983 17.2217 5.47084 17.186 5.70027 17.0537L5.72956 17.0381L5.84968 16.9736L6.02351 16.8809C6.43513 16.678 6.88902 16.5724 7.34968 16.5723C7.81025 16.5723 8.26427 16.6781 8.67585 16.8809L8.84968 16.9736L8.85261 16.9756L9.27937 17.2236C9.73477 17.4866 10.1138 17.8651 10.377 18.3203C10.6403 18.7758 10.7798 19.2932 10.7803 19.8193V20C10.7803 20.2652 10.8858 20.5195 11.0733 20.707C11.2608 20.8944 11.5153 21 11.7803 21H12.2198C12.4849 21 12.7393 20.8944 12.9268 20.707C13.1144 20.5195 13.2198 20.2652 13.2198 20V19.8193L13.2266 19.6221C13.2572 19.1646 13.3928 18.7188 13.6231 18.3203C13.8534 17.9221 14.1718 17.5828 14.5528 17.3281L14.7198 17.2236L15.1475 16.9756L15.1505 16.9736L15.3243 16.8809C15.7359 16.6781 16.1899 16.5723 16.6505 16.5723C17.1769 16.5723 17.6946 16.7104 18.1505 16.9736L18.2999 17.0537C18.5293 17.186 18.8018 17.2217 19.0577 17.1533C19.3136 17.0849 19.5323 16.9177 19.6651 16.6885L19.879 16.3086L19.8839 16.3008L19.9288 16.2119C20.0233 16.0026 20.0433 15.7658 19.9835 15.542C19.915 15.2863 19.7477 15.0683 19.5186 14.9355L19.3995 14.8721C19.3898 14.8669 19.3797 14.862 19.3702 14.8564C18.9124 14.5921 18.532 14.2112 18.2686 13.7529C18.0053 13.2946 17.8681 12.7746 17.8702 12.2461V11.75C17.8688 11.2228 18.0059 10.7042 18.2686 10.2471C18.3758 10.0606 18.5033 9.88765 18.6466 9.72949C19.2341 9.90406 19.8558 10 20.5001 10C20.8249 10 21.144 9.97473 21.4561 9.92871C21.2126 10.2693 20.9013 10.5607 20.5333 10.7764L20.5343 10.7773L20.3702 10.876C20.2176 10.9641 20.0908 11.0914 20.003 11.2441C19.9153 11.3968 19.8695 11.57 19.8702 11.7461V12.2539C19.8695 12.43 19.9153 12.6032 20.003 12.7559C20.0894 12.9062 20.214 13.0312 20.3634 13.1191L20.4903 13.1875L20.5216 13.2051L20.7706 13.3662C21.3298 13.7689 21.7365 14.3539 21.9161 15.0254C22.1205 15.7898 22.0131 16.6035 21.6202 17.29L21.6212 17.291L21.4014 17.6816L21.3956 17.6914C20.9972 18.379 20.342 18.8807 19.5743 19.0859C18.817 19.2883 18.0116 19.1847 17.3292 18.8008V18.8027L17.1798 18.7227C17.17 18.7175 17.16 18.7116 17.1505 18.7061C16.9986 18.6184 16.8258 18.5723 16.6505 18.5723C16.4751 18.5723 16.3024 18.6184 16.1505 18.7061L16.1495 18.7051L15.7227 18.9541L15.6114 19.0283C15.5077 19.1078 15.4204 19.2075 15.3546 19.3213C15.2668 19.4731 15.22 19.646 15.2198 19.8213V20C15.2198 20.7955 14.9043 21.5585 14.3419 22.1211C13.7793 22.6836 13.0154 23 12.2198 23H11.7803C10.9848 23 10.2208 22.6837 9.65828 22.1211C9.09594 21.5585 8.78035 20.7954 8.78035 20V19.8213L8.77156 19.6904C8.75435 19.5609 8.71141 19.4352 8.64558 19.3213C8.57971 19.2075 8.49243 19.1078 8.38874 19.0283L8.27742 18.9541L7.84968 18.7051C7.69776 18.6175 7.52506 18.5723 7.34968 18.5723C7.21824 18.5723 7.08852 18.5985 6.96785 18.6484L6.84968 18.7061C6.84011 18.7116 6.83013 18.7175 6.82038 18.7227L6.67097 18.8027L6.66999 18.8008C5.98771 19.1844 5.18286 19.2883 4.42585 19.0859C3.75418 18.9063 3.16836 18.4998 2.7657 17.9404L2.60456 17.6914L2.38484 17.3105L2.38386 17.3096C1.98692 16.6214 1.87897 15.8037 2.08406 15.0361C2.28803 14.2733 2.78502 13.6217 3.46589 13.2227L3.62995 13.124L3.73933 13.0508C3.84344 12.9708 3.93128 12.8704 3.99714 12.7559C4.08484 12.6032 4.13065 12.43 4.12995 12.2539V11.7461L4.12117 11.6162C4.10349 11.4873 4.06003 11.3622 3.99421 11.249C3.92841 11.136 3.84152 11.037 3.73835 10.958L3.62995 10.8857L3.57527 10.8516L3.42585 10.752C2.76614 10.351 2.2839 9.71122 2.08406 8.96387C1.87897 8.19633 1.98692 7.37864 2.38386 6.69043L2.38484 6.68848L2.60456 6.30859L2.7657 6.05957C3.16836 5.50016 3.75418 5.09371 4.42585 4.91406C5.1827 4.71172 5.9878 4.81485 6.66999 5.19824L6.67097 5.19727L6.82038 5.27734L6.84968 5.29395L6.967Line truncated
</svg>

After

Width:  |  Height:  |  Size: 5.9 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 35 35" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="17.5" cy="17.5" r="15" fill="#FBB36B" stroke="#1C1D21" stroke-width="5"/>
</svg>

After

Width:  |  Height:  |  Size: 188 B

+35 -3
View File
@@ -2,7 +2,8 @@ import Foundation
import os.log
struct Log {
static let osLog = Logger()
private static let subsystemIdentifier = Bundle.main.bundleIdentifier ?? "org.amnezia.AmneziaVPN"
static let osLog = Logger(subsystem: subsystemIdentifier, category: "App")
private static let IsLoggingEnabledKey = "IsLoggingEnabled"
static var isLoggingEnabled: Bool {
@@ -77,9 +78,40 @@ struct Log {
static func log(_ type: OSLogType, title: String = "", message: String, url: URL = neLogURL) {
NSLog("\(title) \(message)")
guard isLoggingEnabled else { return }
switch type {
case .debug:
if title.isEmpty {
osLog.debug("\(message, privacy: .public)")
} else {
osLog.debug("\(title, privacy: .public) \(message, privacy: .public)")
}
case .info:
if title.isEmpty {
osLog.info("\(message, privacy: .public)")
} else {
osLog.info("\(title, privacy: .public) \(message, privacy: .public)")
}
case .error:
if title.isEmpty {
osLog.error("\(message, privacy: .public)")
} else {
osLog.error("\(title, privacy: .public) \(message, privacy: .public)")
}
case .fault:
if title.isEmpty {
osLog.fault("\(message, privacy: .public)")
} else {
osLog.fault("\(title, privacy: .public) \(message, privacy: .public)")
}
default:
if title.isEmpty {
osLog.log("\(message, privacy: .public)")
} else {
osLog.log("\(title, privacy: .public) \(message, privacy: .public)")
}
}
osLog.log(level: type, "\(title) \(message)")
guard isLoggingEnabled else { return }
let date = Date()
let level = Record.Level(from: type)
+55 -1
View File
@@ -1,22 +1,76 @@
import Foundation
import os.log
private let subsystemIdentifier = Bundle.main.bundleIdentifier ?? "org.amnezia.AmneziaVPN"
private let wireGuardSystemLogger = Logger(subsystem: subsystemIdentifier, category: "WireGuard")
private let openVPNSystemLogger = Logger(subsystem: subsystemIdentifier, category: "OpenVPN")
private let xraySystemLogger = Logger(subsystem: subsystemIdentifier, category: "Xray")
private let networkExtensionLogger = Logger(subsystem: subsystemIdentifier, category: "NetworkExtension")
private func logToSystem(_ logger: Logger, type: OSLogType, prefix: String, title: String, message: String) {
let combinedTitle: String
if title.isEmpty {
combinedTitle = prefix
} else {
combinedTitle = "\(prefix): \(title)"
}
switch type {
case .debug:
if combinedTitle.isEmpty {
logger.debug("\(message, privacy: .public)")
} else {
logger.debug("\(combinedTitle, privacy: .public) \(message, privacy: .public)")
}
case .info:
if combinedTitle.isEmpty {
logger.info("\(message, privacy: .public)")
} else {
logger.info("\(combinedTitle, privacy: .public) \(message, privacy: .public)")
}
case .error:
if combinedTitle.isEmpty {
logger.error("\(message, privacy: .public)")
} else {
logger.error("\(combinedTitle, privacy: .public) \(message, privacy: .public)")
}
case .fault:
if combinedTitle.isEmpty {
logger.fault("\(message, privacy: .public)")
} else {
logger.fault("\(combinedTitle, privacy: .public) \(message, privacy: .public)")
}
default:
if combinedTitle.isEmpty {
logger.log("\(message, privacy: .public)")
} else {
logger.log("\(combinedTitle, privacy: .public) \(message, privacy: .public)")
}
}
}
public func wg_log(_ type: OSLogType, title: String = "", staticMessage: StaticString) {
neLog(type, title: "WG: \(title)", message: "\(staticMessage)")
let stringMessage = String(describing: staticMessage)
logToSystem(wireGuardSystemLogger, type: type, prefix: "WG", title: title, message: stringMessage)
neLog(type, title: "WG: \(title)", message: stringMessage)
}
public func wg_log(_ type: OSLogType, title: String = "", message: String) {
logToSystem(wireGuardSystemLogger, type: type, prefix: "WG", title: title, message: message)
neLog(type, title: "WG: \(title)", message: message)
}
public func ovpnLog(_ type: OSLogType, title: String = "", message: String) {
logToSystem(openVPNSystemLogger, type: type, prefix: "OVPN", title: title, message: message)
neLog(type, title: "OVPN: \(title)", message: message)
}
public func xrayLog(_ type: OSLogType, title: String = "", message: String) {
logToSystem(xraySystemLogger, type: type, prefix: "XRAY", title: title, message: message)
neLog(type, title: "XRAY: \(title)", message: message)
}
public func neLog(_ type: OSLogType, title: String = "", message: String) {
logToSystem(networkExtensionLogger, type: type, prefix: "NE", title: title, message: message)
Log.log(type, title: "NE: \(title)", message: message)
}
@@ -1,6 +1,7 @@
import Foundation
import NetworkExtension
import OpenVPNAdapter
import CryptoKit
struct OpenVPNConfig: Decodable {
let config: String
@@ -27,26 +28,83 @@ extension PacketTunnelProvider {
let ovpnConfiguration = Data(openVPNConfig.config.utf8)
setupAndlaunchOpenVPN(withConfig: ovpnConfiguration, completionHandler: completionHandler)
} catch {
ovpnLog(.error, message: "Can't parse config: \(error.localizedDescription)")
if let underlyingError = (error as NSError).userInfo[NSUnderlyingErrorKey] as? NSError {
ovpnLog(.error, message: "Can't parse config: \(underlyingError.localizedDescription)")
}
ovpnLog(.error, message: "Can't parse OpenVPN config: \(error.localizedDescription)")
return
}
}
private func logOpenVPNError(_ error: NSError) {
let fatalFlag = (error.userInfo[OpenVPNAdapterErrorFatalKey] as? Bool) ?? false
var lines: [String] = []
lines.append("domain=\(error.domain) code=\(error.code) fatal=\(fatalFlag)")
if let adapterMessage = error.userInfo[OpenVPNAdapterErrorMessageKey] as? String, !adapterMessage.isEmpty {
lines.append("message=\(adapterMessage)")
}
let userInfoKeys = error.userInfo.keys.map { String(describing: $0) }.sorted()
if !userInfoKeys.isEmpty {
lines.append("userInfoKeys=[\(userInfoKeys.joined(separator: ","))]")
}
if let underlying = error.userInfo[NSUnderlyingErrorKey] as? NSError {
lines.append("underlying=\(underlying.domain)#\(underlying.code) fatal=\((underlying.userInfo[OpenVPNAdapterErrorFatalKey] as? Bool) ?? false)")
if let underlyingMessage = underlying.userInfo[OpenVPNAdapterErrorMessageKey] as? String, !underlyingMessage.isEmpty {
lines.append("underlyingMessage=\(underlyingMessage)")
} else if !underlying.localizedDescription.isEmpty {
lines.append("underlyingLocalized=\(underlying.localizedDescription)")
}
} else if let underlying = error.userInfo[NSUnderlyingErrorKey] {
lines.append("underlyingRaw=\(underlying)")
}
let formatted = lines.joined(separator: "\n ")
ovpnLog(.error, title: "Error", message: formatted)
}
private func setupAndlaunchOpenVPN(withConfig ovpnConfiguration: Data,
withShadowSocks viaSS: Bool = false,
completionHandler: @escaping (Error?) -> Void) {
ovpnLog(.info, message: "Setup and launch")
let str = String(decoding: ovpnConfiguration, as: UTF8.self)
var configString = String(decoding: ovpnConfiguration, as: UTF8.self)
let digest = SHA256.hash(data: ovpnConfiguration)
let digestString = digest.map { String(format: "%02x", $0) }.joined()
ovpnLog(.info, title: "ConfigDigest", message: digestString)
let hasTlsAuthOpen = configString.contains("<tls-auth>")
let hasTlsAuthClose = configString.contains("</tls-auth>")
ovpnLog(.info, title: "ConfigFlags", message: "tls-auth open=\(hasTlsAuthOpen) close=\(hasTlsAuthClose)")
let lines = configString.split(separator: "\n")
let head = lines.prefix(10).joined(separator: "\n")
let tail = lines.suffix(10).joined(separator: "\n")
ovpnLog(.debug, title: "ConfigHead", message: head)
ovpnLog(.debug, title: "ConfigTail", message: tail)
if let start = configString.range(of: "<tls-auth>"),
let end = configString.range(of: "</tls-auth>", range: start.upperBound..<configString.endIndex) {
let keyBody = String(configString[start.upperBound..<end.lowerBound])
ovpnLog(.debug, title: "TLSAuthInline", message: keyBody)
let sanitizedLines = keyBody
.split(whereSeparator: { $0.isNewline })
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
.filter { !$0.hasPrefix("#") }
let sanitizedKey = sanitizedLines.joined(separator: "\n")
ovpnLog(.debug, title: "TLSAuthSanitized", message: sanitizedKey)
let sanitizedBlock = "<tls-auth>\n\(sanitizedKey)\n</tls-auth>"
configString.replaceSubrange(start.lowerBound..<end.upperBound, with: sanitizedBlock)
}
let normalizedConfig = configString.replacingOccurrences(of: "\r\n", with: "\n")
let sanitizedData = Data(normalizedConfig.utf8)
let configuration = OpenVPNConfiguration()
configuration.fileContent = ovpnConfiguration
if str.contains("cloak") {
configuration.fileContent = sanitizedData
if configString.contains("cloak") {
configuration.setPTCloak()
}
@@ -57,6 +115,8 @@ extension PacketTunnelProvider {
evaluation = try ovpnAdapter?.apply(configuration: configuration)
} catch {
let nsError = error as NSError
ovpnLog(.error, title: "ApplyConfig", message: "domain=\(nsError.domain) code=\(nsError.code) info=\(nsError.userInfo)")
completionHandler(error)
return
}
@@ -73,7 +133,7 @@ extension PacketTunnelProvider {
startHandler = completionHandler
ovpnAdapter?.connect(using: packetFlow)
}
func handleOpenVPNStatusMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
guard let completionHandler = completionHandler else { return }
let bytesin = ovpnAdapter?.transportStatistics.bytesIn
@@ -169,20 +229,8 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
networkSettings?.ipv6Settings?.includedRoutes = ipv6IncludedRoutes
networkSettings?.ipv4Settings?.excludedRoutes = ipv4ExcludedRoutes
}
if splitTunnelType == 0 || splitTunnelType == nil {
// Full tunnel: send all traffic via VPN
if let ipv4Settings = networkSettings?.ipv4Settings {
ipv4Settings.includedRoutes = [NEIPv4Route.default()]
NSLog("[Route] Added default IPv4 route (0.0.0.0/0)")
}
if let ipv6Settings = networkSettings?.ipv6Settings {
let ipv6DefaultRoute = NEIPv6Route(destinationAddress: "::", networkPrefixLength: 0)
ipv6Settings.includedRoutes = [ipv6DefaultRoute]
NSLog("[Route] Added default IPv6 route (::/0)")
}
}
}
// Set the network settings for the current tunneling session.
setTunnelNetworkSettings(networkSettings, completionHandler: completionHandler)
}
@@ -220,8 +268,11 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
// Handle errors thrown by the OpenVPN library
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleError error: Error) {
let nsError = error as NSError
logOpenVPNError(nsError)
// Handle only fatal errors
guard let fatal = (error as NSError).userInfo[OpenVPNAdapterErrorFatalKey] as? Bool,
guard let fatal = nsError.userInfo[OpenVPNAdapterErrorFatalKey] as? Bool,
fatal == true else { return }
if vpnReachability.isTracking {
+43 -42
View File
@@ -27,6 +27,7 @@ const char* MessageKey::isOnDemand = "is-on-demand";
const char* MessageKey::SplitTunnelType = "SplitTunnelType";
const char* MessageKey::SplitTunnelSites = "SplitTunnelSites";
#if !MACOS_NE
static UIViewController* getViewController() {
UIApplication *application = [UIApplication sharedApplication];
@@ -70,6 +71,7 @@ static UIViewController* getViewController() {
return nil;
}
#endif
Vpn::ConnectionState iosStatusToState(NEVPNStatus status) {
switch (status) {
@@ -160,39 +162,6 @@ bool IosController::connectVpn(amnezia::Proto proto, const QJsonObject& configur
m_rawConfig = configuration;
m_serverAddress = configuration.value(config_key::hostName).toString().toNSString();
if (proto == amnezia::Proto::OpenVpn) {
QJsonObject ovpn = configuration["openvpn_config_data"].toObject();
QString ovpnConfig = ovpn["config"].toString();
QStringList unsupportedDirectives = {
"resolv-retry",
"persist-key",
"persist-tun",
"block-ipv6",
"redirect-gateway"
};
QStringList lines = ovpnConfig.split('\n');
QStringList filteredLines;
for (const QString &line : lines) {
QString trimmedLine = line.trimmed();
bool shouldIgnore = false;
for (const QString &bad : unsupportedDirectives) {
if (trimmedLine.startsWith(bad)) {
shouldIgnore = true;
break;
}
}
if (!shouldIgnore) {
filteredLines.append(line);
}
}
ovpnConfig = filteredLines.join("\n");
ovpn["config"] = ovpnConfig;
m_rawConfig["openvpn_config_data"] = ovpn;
}
QString tunnelName;
if (configuration.value(config_key::description).toString().isEmpty()) {
tunnelName = QString("%1 %2")
@@ -316,6 +285,21 @@ void IosController::checkStatus()
sendVpnExtensionMessage(message, [&](NSDictionary* response){
uint64_t txBytes = [response[@"tx_bytes"] intValue];
uint64_t rxBytes = [response[@"rx_bytes"] intValue];
uint64_t last_handshake_time_sec = 0;
#if !MACOS_NE
if (response[@"last_handshake_time_sec"] && ![response[@"last_handshake_time_sec"] isKindOfClass:[NSNull class]]) {
last_handshake_time_sec = [response[@"last_handshake_time_sec"] intValue];
} else {
qDebug() << "Key last_handshake_time_sec is missing or null";
}
if (last_handshake_time_sec < 0) {
disconnectVpn();
qDebug() << "Invalid handshake time, disconnecting VPN.";
}
#endif
emit bytesChanged(rxBytes - m_rxBytes, txBytes - m_txBytes);
m_rxBytes = rxBytes;
m_txBytes = txBytes;
@@ -574,6 +558,8 @@ bool IosController::setupWireGuard()
wgConfig.insert(config_key::initPacketJunkSize, config[config_key::initPacketJunkSize]);
wgConfig.insert(config_key::responsePacketJunkSize, config[config_key::responsePacketJunkSize]);
wgConfig.insert(config_key::cookieReplyPacketJunkSize, config[config_key::cookieReplyPacketJunkSize]);
wgConfig.insert(config_key::transportPacketJunkSize, config[config_key::transportPacketJunkSize]);
wgConfig.insert(config_key::junkPacketCount, config[config_key::junkPacketCount]);
wgConfig.insert(config_key::junkPacketMinSize, config[config_key::junkPacketMinSize]);
@@ -672,11 +658,23 @@ bool IosController::setupAwg()
wgConfig.insert(config_key::initPacketJunkSize, config[config_key::initPacketJunkSize]);
wgConfig.insert(config_key::responsePacketJunkSize, config[config_key::responsePacketJunkSize]);
wgConfig.insert(config_key::cookieReplyPacketJunkSize, config[config_key::cookieReplyPacketJunkSize]);
wgConfig.insert(config_key::transportPacketJunkSize, config[config_key::transportPacketJunkSize]);
wgConfig.insert(config_key::junkPacketCount, config[config_key::junkPacketCount]);
wgConfig.insert(config_key::junkPacketMinSize, config[config_key::junkPacketMinSize]);
wgConfig.insert(config_key::junkPacketMaxSize, config[config_key::junkPacketMaxSize]);
wgConfig.insert(config_key::specialJunk1, config[config_key::specialJunk1]);
wgConfig.insert(config_key::specialJunk2, config[config_key::specialJunk2]);
wgConfig.insert(config_key::specialJunk3, config[config_key::specialJunk3]);
wgConfig.insert(config_key::specialJunk4, config[config_key::specialJunk4]);
wgConfig.insert(config_key::specialJunk5, config[config_key::specialJunk5]);
wgConfig.insert(config_key::controlledJunk1, config[config_key::controlledJunk1]);
wgConfig.insert(config_key::controlledJunk2, config[config_key::controlledJunk2]);
wgConfig.insert(config_key::controlledJunk3, config[config_key::controlledJunk3]);
wgConfig.insert(config_key::specialHandshakeTimeout, config[config_key::specialHandshakeTimeout]);
QJsonDocument wgConfigDoc(wgConfig);
QString wgConfigDocStr(wgConfigDoc.toJson(QJsonDocument::Compact));
@@ -856,14 +854,14 @@ bool IosController::shareText(const QStringList& filesToSend) {
NSURL *logFileUrl = [[NSURL alloc] initFileURLWithPath:filesToSend[i].toNSString()];
[sharingItems addObject:logFileUrl];
}
#if !MACOS_NE
UIViewController *qtController = getViewController();
if (!qtController) return;
UIActivityViewController *activityController = [[UIActivityViewController alloc] initWithActivityItems:sharingItems applicationActivities:nil];
#endif
__block bool isAccepted = false;
#if !MACOS_NE
[activityController setCompletionWithItemsHandler:^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) {
isAccepted = completed;
emit finished();
@@ -875,15 +873,17 @@ bool IosController::shareText(const QStringList& filesToSend) {
popController.sourceView = qtController.view;
popController.sourceRect = CGRectMake(100, 100, 100, 100);
}
#endif
QEventLoop wait;
QObject::connect(this, &IosController::finished, &wait, &QEventLoop::quit);
wait.exec();
return isAccepted;
}
QString IosController::openFile() {
#if !MACOS_NE
UIDocumentPickerViewController *documentPicker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[@"public.item"] inMode:UIDocumentPickerModeOpen];
DocumentPickerDelegate *documentPickerDelegate = [[DocumentPickerDelegate alloc] init];
@@ -893,9 +893,10 @@ QString IosController::openFile() {
if (!qtController) return;
[qtController presentViewController:documentPicker animated:YES completion:nil];
__block QString filePath;
#endif
__block QString filePath;
#if !MACOS_NE
documentPickerDelegate.documentPickerClosedCallback = ^(NSString *path) {
if (path) {
filePath = QString::fromUtf8(path.UTF8String);
@@ -904,11 +905,11 @@ QString IosController::openFile() {
}
emit finished();
};
#endif
QEventLoop wait;
QObject::connect(this, &IosController::finished, &wait, &QEventLoop::quit);
wait.exec();
return filePath;
}
+1
View File
@@ -169,6 +169,7 @@ void XrayProtocol::stop()
#if defined(Q_OS_WIN) || defined(Q_OS_LINUX) || defined(Q_OS_MACOS)
IpcClient::Interface()->disableKillSwitch();
IpcClient::Interface()->StartRoutingIpv6();
IpcClient::Interface()->restoreResolvers();
#endif
qDebug() << "XrayProtocol::stop()";
m_xrayProcess.disconnect();
+6
View File
@@ -35,6 +35,9 @@
<file>images/controls/mail.svg</file>
<file>images/controls/map-pin.svg</file>
<file>images/controls/more-vertical.svg</file>
<file>images/controls/news.svg</file>
<file>images/controls/news-unread.svg</file>
<file>images/controls/unread-dot.svg</file>
<file>images/controls/plus.svg</file>
<file>images/controls/qr-code.svg</file>
<file>images/controls/radio-button-inner-circle-pressed.png</file>
@@ -49,6 +52,7 @@
<file>images/controls/server.svg</file>
<file>images/controls/settings-2.svg</file>
<file>images/controls/settings.svg</file>
<file>images/controls/settings-news.svg</file>
<file>images/controls/share-2.svg</file>
<file>images/controls/split-tunneling.svg</file>
<file>images/controls/tag.svg</file>
@@ -212,6 +216,8 @@
<file>ui/qml/Pages2/PageSettingsServerServices.qml</file>
<file>ui/qml/Pages2/PageSettingsServersList.qml</file>
<file>ui/qml/Pages2/PageSettingsSplitTunneling.qml</file>
<file>ui/qml/Pages2/PageSettingsNewsNotifications.qml</file>
<file>ui/qml/Pages2/PageSettingsNewsDetail.qml</file>
<file>ui/qml/Pages2/PageProtocolAwgClientSettings.qml</file>
<file>ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml</file>
<file>ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml</file>
+12 -2
View File
@@ -541,12 +541,12 @@ QString Settings::getGatewayEndpoint()
bool Settings::isDevGatewayEnv()
{
return m_isDevGatewayEnv;
return value("Conf/devGatewayEnv", false).toBool();
}
void Settings::toggleDevGatewayEnv(bool enabled)
{
m_isDevGatewayEnv = enabled;
setValue("Conf/devGatewayEnv", enabled);
}
bool Settings::isHomeAdLabelVisible()
@@ -578,3 +578,13 @@ void Settings::setAllowedDnsServers(const QStringList &servers)
{
setValue("Conf/allowedDnsServers", servers);
}
QStringList Settings::readNewsIds() const
{
return value("News/readIds").toStringList();
}
void Settings::setReadNewsIds(const QStringList &ids)
{
setValue("News/readIds", ids);
}
+3 -1
View File
@@ -236,6 +236,9 @@ public:
QStringList allowedDnsServers() const;
void setAllowedDnsServers(const QStringList &servers);
QStringList readNewsIds() const;
void setReadNewsIds(const QStringList &ids);
signals:
void saveLogsChanged(bool enabled);
void screenshotsEnabledChanged(bool enabled);
@@ -251,7 +254,6 @@ private:
mutable SecureQSettings m_settings;
QString m_gatewayEndpoint;
bool m_isDevGatewayEnv = false;
};
#endif // SETTINGS_H
+103 -110
View File
@@ -94,12 +94,12 @@
<translation>%1 успешно установлен.</translation>
</message>
<message>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="472"/>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="473"/>
<source>API config reloaded</source>
<translation>Конфигурация API перезагружена</translation>
</message>
<message>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="476"/>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="477"/>
<source>Successfully changed the country of connection to %1</source>
<translation>Страна подключения изменена на %1</translation>
</message>
@@ -2079,52 +2079,40 @@ Thank you for staying with us!</source>
<context>
<name>PageSettingsApiSubscriptionKey</name>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="43"/>
<source>Amnezia Premium
subscription key</source>
<translation>Amnezia Premium
ключ подключения</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="56"/>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="85"/>
<source>Copy key</source>
<translation>Скопировать ключ</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="61"/>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="90"/>
<source>Copied</source>
<translation>Скопировано</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="77"/>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="106"/>
<source>Save key as a file</source>
<translation>Сохранить ключ как файл</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="84"/>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="113"/>
<source>Save AmneziaVPN config</source>
<translation>Сохранить конфигурацию AmneziaVPN</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="85"/>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="114"/>
<source>Config files (*.vpn)</source>
<translation>Файлы конфигов (*.vpn)</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="110"/>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="139"/>
<source>Show key text</source>
<translation>Показать ключ</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="147"/>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="180"/>
<source>To read the QR code in the Amnezia app, tap + in the main menu → &apos;QR code&apos;</source>
<translation>Для считывания QR-кода в приложении Amnezia выберите + в главном меню → &apos;QR-код&apos;</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="176"/>
<source>Amnezia Premium Subscription key</source>
<translation>Ключ подключения Amnezia Premium</translation>
</message>
</context>
<context>
<name>PageSettingsApiSupport</name>
@@ -2197,32 +2185,37 @@ subscription key</source>
<translation>Режим</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="185"/>
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="155"/>
<source>Only &quot;Apps from the list should not have access via VPN&quot; mode is available on Windows</source>
<translation>На Windows доступен только режим &quot;Приложения из списка не должны работать через VPN&quot;</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="199"/>
<source>Remove </source>
<translation>Удалить </translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="186"/>
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="200"/>
<source>Continue</source>
<translation>Продолжить</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="187"/>
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="201"/>
<source>Cancel</source>
<translation>Отменить</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="228"/>
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="242"/>
<source>application name</source>
<translation>название приложения</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="238"/>
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="252"/>
<source>Open executable file</source>
<translation>Открыть исполняемый файл</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="239"/>
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="253"/>
<source>Executable files (*.*)</source>
<translation>Исполняемые файлы (*.*)</translation>
</message>
@@ -3013,19 +3006,19 @@ subscription key</source>
<translation>Режим</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="207"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="209"/>
<source>Remove </source>
<translation>Удалить </translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="208"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="356"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="210"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="358"/>
<source>Continue</source>
<translation>Продолжить</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="209"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="357"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="211"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="359"/>
<source>Cancel</source>
<translation>Отменить</translation>
</message>
@@ -3040,70 +3033,70 @@ subscription key</source>
<translation>Невозможно изменить настройки раздельного туннелирования во время активного соединения</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="257"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="259"/>
<source>website or IP</source>
<translation>веб-сайт или IP</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="302"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="304"/>
<source>Additional options</source>
<translation>Дополнительные настройки</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="309"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="311"/>
<source>Import</source>
<translation>Импорт</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="322"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="324"/>
<source>Save site list</source>
<translation>Сохранить список сайтов</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="329"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="331"/>
<source>Save sites</source>
<translation>Сохранить сайты</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="330"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="457"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="470"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="332"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="459"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="472"/>
<source>Sites files (*.json)</source>
<translation>Файлы сайтов (*.json)</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="350"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="352"/>
<source>Clear site list</source>
<translation>Очистить список сайтов</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="354"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="356"/>
<source>Clear site list?</source>
<translation>Очистить список сайтов?</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="355"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="357"/>
<source>All sites will be removed from list.</source>
<translation>Все сайты будут удалены из списка.</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="419"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="421"/>
<source>Import a list of sites</source>
<translation>Импортировать список с сайтами</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="454"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="456"/>
<source>Replace site list</source>
<translation>Заменить список с сайтами</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="456"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="469"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="458"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="471"/>
<source>Open sites file</source>
<translation>Открыть список с сайтами</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="467"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="469"/>
<source>Add imported sites to existing ones</source>
<translation>Добавить импортированные сайты к существующим</translation>
</message>
@@ -3394,38 +3387,38 @@ subscription key</source>
<context>
<name>PageSetupWizardInstalling</name>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="59"/>
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="63"/>
<source>The server has already been added to the application</source>
<translation>Сервер уже был добавлен в приложение</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="65"/>
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="69"/>
<source>Amnezia has detected that your server is currently </source>
<translation>Amnezia обнаружила, что ваш сервер в настоящее время </translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="66"/>
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="70"/>
<source>busy installing other software. Amnezia installation </source>
<translation>занят установкой других протоколов или сервисов. Установка Amnezia </translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="67"/>
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="71"/>
<source>will pause until the server finishes installing other software</source>
<translation>будет приостановлена до тех пор, пока сервер не завершит установку другого ПО</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="106"/>
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="110"/>
<source>Installing</source>
<translation>Установка</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="151"/>
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="155"/>
<source>Cancel installation</source>
<translation>Отменить установку</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="22"/>
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="71"/>
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="75"/>
<source>Usually it takes no more than 5 minutes</source>
<translation>Обычно это занимает не более 5 минут</translation>
</message>
@@ -3561,217 +3554,217 @@ subscription key</source>
<context>
<name>PageShare</name>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="130"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="125"/>
<source>OpenVPN native format</source>
<translation>Оригинальный формат OpenVPN</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="135"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="130"/>
<source>WireGuard native format</source>
<translation>Оригинальный формат WireGuard</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="260"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="255"/>
<source>Connection</source>
<translation>Соединение</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="328"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="329"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="323"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="324"/>
<source>Server</source>
<translation>Сервер</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="39"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="37"/>
<source>Config revoked</source>
<translation>Конфигурация отозвана</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="55"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="50"/>
<source>Save AmneziaVPN config</source>
<translation>Сохранить конфигурацию AmneziaVPN</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="62"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="57"/>
<source>Save OpenVPN config</source>
<translation>Сохранить конфигурацию OpenVPN</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="69"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="64"/>
<source>Save WireGuard config</source>
<translation>Сохранить конфигурацию WireGuard</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="76"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="71"/>
<source>Save AmneziaWG config</source>
<translation>Сохранить конфигурацию AmneziaWG</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="83"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="78"/>
<source>Save Shadowsocks config</source>
<translation>Сохранить конфигурацию Shadowsocks</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="90"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="85"/>
<source>Save Cloak config</source>
<translation>Сохранить конфигурацию Cloak</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="97"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="92"/>
<source>Save XRay config</source>
<translation>Сохранить конфигурацию XRay</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="106"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="101"/>
<source>Connection to </source>
<translation>Подключение к </translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="107"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="102"/>
<source>File with connection settings to </source>
<translation>Файл с настройками подключения к </translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="125"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="120"/>
<source>For the AmneziaVPN app</source>
<translation>Для приложения AmneziaVPN</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="140"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="135"/>
<source>AmneziaWG native format</source>
<translation>Оригинальный формат AmneziaWG</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="145"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="140"/>
<source>Shadowsocks native format</source>
<translation>Оригинальный формат Shadowsocks</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="150"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="145"/>
<source>Cloak native format</source>
<translation>Оригинальный формат Cloak</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="155"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="150"/>
<source>XRay native format</source>
<translation>Оригинальный формат XRay</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="183"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="178"/>
<source>Share VPN Access</source>
<translation>Поделиться VPN</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="217"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="212"/>
<source>Share full access to the server and VPN</source>
<translation>Поделиться полным доступом к серверу и VPN</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="218"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="213"/>
<source>Use for your own devices, or share with those you trust to manage the server.</source>
<translation>Используйте для собственных устройств или передайте управление сервером тем, кому вы доверяете.</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="275"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="555"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="270"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="550"/>
<source>Users</source>
<translation>Пользователи</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="309"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="304"/>
<source>User name</source>
<translation>Имя пользователя</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="571"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="566"/>
<source>Search</source>
<translation>Поиск</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="694"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="691"/>
<source>Creation date: %1</source>
<translation>Дата создания: %1</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="706"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="703"/>
<source>Latest handshake: %1</source>
<translation>Последнее рукопожатие: %1</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="718"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="715"/>
<source>Data received: %1</source>
<translation>Получено данных: %1</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="730"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="727"/>
<source>Data sent: %1</source>
<translation>Отправлено данных: %1</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="740"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="737"/>
<source>Allowed IPs: %1</source>
<translation>Разрешенные подсети: %1</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="755"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="752"/>
<source>Rename</source>
<translation>Переименовать</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="780"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="777"/>
<source>Client name</source>
<translation>Имя клиента</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="791"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="788"/>
<source>Save</source>
<translation>Сохранить</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="825"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="824"/>
<source>Revoke</source>
<translation>Отозвать</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="828"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="827"/>
<source>Revoke the config for a user - %1?</source>
<translation>Отозвать конфигурацию для пользователя - %1?</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="829"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="828"/>
<source>The user will no longer be able to connect to your server.</source>
<translation>Пользователь больше не сможет подключаться к вашему серверу.</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="830"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="829"/>
<source>Continue</source>
<translation>Продолжить</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="831"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="830"/>
<source>Cancel</source>
<translation>Отменить</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="298"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="293"/>
<source>Share VPN access without the ability to manage the server</source>
<translation>Поделиться доступом к VPN без возможности управления сервером</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="389"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="390"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="384"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="385"/>
<source>Protocol</source>
<translation>Протокол</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="496"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="497"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="491"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="492"/>
<source>Connection format</source>
<translation>Формат подключения</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="225"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="537"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="220"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="532"/>
<source>Share</source>
<translation>Поделиться</translation>
</message>
@@ -3810,7 +3803,7 @@ subscription key</source>
<translation>Скопировано</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShareConnection.qml" line="319"/>
<location filename="../ui/qml/Pages2/PageShareConnection.qml" line="323"/>
<source>To read the QR code in the Amnezia app, select &quot;Add server&quot; → &quot;I have data to connect&quot; → &quot;QR code, key or settings file&quot;</source>
<translation>Для считывания QR-кода в приложении Amnezia выберите &quot;Добавить сервер&quot; → &quot;У меня есть данные для подключения&quot; → &quot;Открыть файл конфигурации, ключ или QR-код&quot;</translation>
</message>
@@ -3841,22 +3834,22 @@ subscription key</source>
<translation>Сервер</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="114"/>
<location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="115"/>
<source>Accessing </source>
<translation>Доступ </translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="115"/>
<location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="116"/>
<source>File with accessing settings to </source>
<translation>Файл с настройками доступа к </translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="146"/>
<location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="147"/>
<source>Share</source>
<translation>Поделиться</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="154"/>
<location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="155"/>
<source>Access error!</source>
<translation>Ошибка доступа!</translation>
</message>
@@ -4957,12 +4950,12 @@ FileZilla или другие SFTP-клиенты, а также смонтир
<context>
<name>SettingsController</name>
<message>
<location filename="../ui/controllers/settingsController.cpp" line="242"/>
<location filename="../ui/controllers/settingsController.cpp" line="250"/>
<source>All settings have been reset to default values</source>
<translation>Все настройки сброшены до значений по умолчанию</translation>
</message>
<message>
<location filename="../ui/controllers/settingsController.cpp" line="219"/>
<location filename="../ui/controllers/settingsController.cpp" line="227"/>
<source>Backup file is corrupted</source>
<translation>Файл резервной копии поврежден</translation>
</message>
@@ -5051,7 +5044,7 @@ FileZilla или другие SFTP-клиенты, а также смонтир
<context>
<name>TextFieldWithHeaderType</name>
<message>
<location filename="../ui/qml/Controls2/TextFieldWithHeaderType.qml" line="117"/>
<location filename="../ui/qml/Controls2/TextFieldWithHeaderType.qml" line="133"/>
<source>The field can&apos;t be empty</source>
<translation>Поле не может быть пустым</translation>
</message>
@@ -47,6 +47,8 @@ namespace
constexpr char subscription[] = "subscription";
constexpr char endDate[] = "end_date";
constexpr char isConnectEvent[] = "is_connect_event";
}
struct ProtocolData
@@ -249,6 +251,23 @@ ApiConfigsController::ApiConfigsController(const QSharedPointer<ServersModel> &s
{
}
bool ApiConfigsController::exportVpnKey(const QString &fileName)
{
if (fileName.isEmpty()) {
emit errorOccurred(ErrorCode::PermissionsError);
return false;
}
prepareVpnKeyExport();
if (m_vpnKey.isEmpty()) {
emit errorOccurred(ErrorCode::ApiConfigEmptyError);
return false;
}
SystemController::saveFile(fileName, m_vpnKey);
return true;
}
bool ApiConfigsController::exportNativeConfig(const QString &serverCountryCode, const QString &fileName)
{
if (fileName.isEmpty()) {
@@ -330,6 +349,13 @@ void ApiConfigsController::prepareVpnKeyExport()
auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject();
auto vpnKey = apiConfigObject.value(apiDefs::key::vpnKey).toString();
if (vpnKey.isEmpty()) {
vpnKey = apiUtils::getPremiumV2VpnKey(serverConfigObject);
apiConfigObject.insert(apiDefs::key::vpnKey, vpnKey);
serverConfigObject.insert(configKey::apiConfig, apiConfigObject);
m_serversModel->editServer(serverConfigObject, m_serversModel->getProcessedServerIndex());
}
m_vpnKey = vpnKey;
vpnKey.replace("vpn://", "");
@@ -443,6 +469,10 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
appendProtocolDataToApiPayload(gatewayRequestData.serviceProtocol, protocolData, apiPayload);
if (newCountryCode.isEmpty() && newCountryName.isEmpty() && !reloadServiceConfig) {
apiPayload.insert(configKey::isConnectEvent, true);
}
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody);
@@ -526,7 +556,7 @@ bool ApiConfigsController::updateServiceFromTelegram(const int serverIndex)
}
}
bool ApiConfigsController::deactivateDevice()
bool ApiConfigsController::deactivateDevice(const bool isRemoveEvent)
{
auto serverIndex = m_serversModel->getProcessedServerIndex();
auto serverConfigObject = m_serversModel->getServerConfig(serverIndex);
@@ -537,8 +567,12 @@ bool ApiConfigsController::deactivateDevice()
}
if (isSubscriptionExpired(apiConfigObject)) {
emit errorOccurred(ErrorCode::ApiSubscriptionExpiredError);
return false;
if (isRemoveEvent) {
return true;
} else {
emit errorOccurred(ErrorCode::ApiSubscriptionExpiredError);
return false;
}
}
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
@@ -21,7 +21,7 @@ public:
public slots:
bool exportNativeConfig(const QString &serverCountryCode, const QString &fileName);
bool revokeNativeConfig(const QString &serverCountryCode);
// bool exportVpnKey(const QString &fileName);
bool exportVpnKey(const QString &fileName);
void prepareVpnKeyExport();
void copyVpnKeyToClipboard();
@@ -30,7 +30,7 @@ public slots:
bool updateServiceFromGateway(const int serverIndex, const QString &newCountryCode, const QString &newCountryName,
bool reloadServiceConfig = false);
bool updateServiceFromTelegram(const int serverIndex);
bool deactivateDevice();
bool deactivateDevice(const bool isRemoveEvent);
bool deactivateExternalDevice(const QString &uuid, const QString &serverCountryCode);
bool isConfigValid();
@@ -0,0 +1,65 @@
#include "apiNewsController.h"
#include "core/api/apiUtils.h"
#include <QJsonDocument>
#include <QJsonObject>
namespace
{
namespace configKey
{
constexpr char userCountryCode[] = "user_country_code";
constexpr char serviceType[] = "service_type";
}
}
ApiNewsController::ApiNewsController(const QSharedPointer<NewsModel> &newsModel, const std::shared_ptr<Settings> &settings,
const QSharedPointer<ServersModel> &serversModel, QObject *parent)
: QObject(parent), m_newsModel(newsModel), m_settings(settings), m_serversModel(serversModel)
{
}
void ApiNewsController::fetchNews()
{
if (m_serversModel.isNull()) {
qWarning() << "ServersModel is null, skip fetchNews";
return;
}
const auto stacks = m_serversModel->gatewayStacks();
if (stacks.isEmpty()) {
qDebug() << "No Gateway stacks, skip fetchNews";
return;
}
GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled());
QByteArray responseBody;
QJsonObject payload;
payload.insert("locale", m_settings->getAppLanguage().name().split("_").first());
const QJsonObject stacksJson = stacks.toJson();
if (stacksJson.contains(configKey::userCountryCode)) {
payload.insert(configKey::userCountryCode, stacksJson.value(configKey::userCountryCode));
}
if (stacksJson.contains(configKey::serviceType)) {
payload.insert(configKey::serviceType, stacksJson.value(configKey::serviceType));
}
ErrorCode errorCode = gatewayController.post(QString("%1v1/news"), payload, responseBody);
if (errorCode != ErrorCode::NoError) {
emit errorOccurred(errorCode);
return;
}
QJsonDocument doc = QJsonDocument::fromJson(responseBody);
QJsonArray newsArray;
if (doc.isArray()) {
newsArray = doc.array();
} else if (doc.isObject()) {
QJsonObject obj = doc.object();
if (obj.value("news").isArray()) {
newsArray = obj.value("news").toArray();
}
}
m_newsModel->updateModel(newsArray);
}
@@ -0,0 +1,33 @@
#ifndef APINEWSCONTROLLER_H
#define APINEWSCONTROLLER_H
#include <QJsonArray>
#include <QObject>
#include <QSharedPointer>
#include <memory>
#include "core/api/apiDefs.h"
#include "core/controllers/gatewayController.h"
#include "settings.h"
#include "ui/models/newsModel.h"
#include "ui/models/servers_model.h"
class ApiNewsController : public QObject
{
Q_OBJECT
public:
explicit ApiNewsController(const QSharedPointer<NewsModel> &newsModel, const std::shared_ptr<Settings> &settings,
const QSharedPointer<ServersModel> &serversModel, QObject *parent = nullptr);
Q_INVOKABLE void fetchNews();
signals:
void errorOccurred(ErrorCode errorCode);
private:
QSharedPointer<NewsModel> m_newsModel;
std::shared_ptr<Settings> m_settings;
QSharedPointer<ServersModel> m_serversModel;
};
#endif // APINEWSCONTROLLER_H
+2
View File
@@ -26,6 +26,8 @@ namespace PageLoader
PageSettingsConnection,
PageSettingsDns,
PageSettingsApplication,
PageSettingsNewsNotifications,
PageSettingsNewsDetail,
PageSettingsBackup,
PageSettingsAbout,
PageSettingsLogging,
@@ -5,6 +5,7 @@
#include "logger.h"
#include "systemController.h"
#include "ui/qautostart.h"
#include "amnezia_application.h"
#include "version.h"
#ifdef Q_OS_ANDROID
#include "platforms/android/android_controller.h"
@@ -33,6 +34,9 @@ SettingsController::SettingsController(const QSharedPointer<ServersModel> &serve
#ifdef Q_OS_ANDROID
connect(AndroidController::instance(), &AndroidController::notificationStateChanged, this, &SettingsController::onNotificationStateChanged);
#endif
m_isDevModeEnabled = m_settings->isDevGatewayEnv();
toggleDevGatewayEnv(m_isDevModeEnabled);
}
QString getPlatformName()
@@ -139,6 +143,10 @@ void SettingsController::clearLogs()
Logger::clearLogs(false);
Logger::clearServiceLogs();
#endif
qInfo().noquote() << QString("Started %1 version %2 %3").arg(APPLICATION_NAME, APP_VERSION, GIT_COMMIT_HASH);
qInfo().noquote() << QString("%1 (%2)").arg(QSysInfo::prettyProductName(), QSysInfo::currentCpuArchitecture());
qInfo().noquote() << QString("SSL backend: %1").arg(QSslSocket::sslLibraryVersionString());
}
void SettingsController::backupAppConfig(const QString &fileName)
+130
View File
@@ -0,0 +1,130 @@
#include "ui/models/newsModel.h"
#include <QDir>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QQmlEngine>
#include <QStandardPaths>
#include <algorithm>
NewsModel::NewsModel(const std::shared_ptr<Settings> &settings, QObject *parent) : QAbstractListModel(parent), m_settings(settings)
{
loadReadIds();
}
int NewsModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return m_items.size();
}
QVariant NewsModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() < 0 || index.row() >= m_items.size())
return QVariant();
const NewsItem &item = m_items.at(index.row());
switch (role) {
case IdRole: return item.id;
case TitleRole: return item.title;
case ContentRole: return item.content;
case TimestampRole: return item.timestamp.toString(Qt::ISODate);
case IsReadRole: return item.read;
case IsProcessedRole: return index.row() == m_processedIndex;
default: return QVariant();
}
}
QHash<int, QByteArray> NewsModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[IdRole] = "id";
roles[TitleRole] = "title";
roles[ContentRole] = "content";
roles[TimestampRole] = "timestamp";
roles[IsReadRole] = "read";
roles[IsProcessedRole] = "isProcessed";
return roles;
}
void NewsModel::markAsRead(int index)
{
if (index < 0 || index >= m_items.size())
return;
if (!m_items[index].read) {
m_items[index].read = true;
m_readIds.insert(m_items[index].id);
saveReadIds();
QModelIndex idx = createIndex(index, 0);
emit dataChanged(idx, idx, { IsReadRole });
emit hasUnreadChanged();
}
}
int NewsModel::processedIndex() const
{
return m_processedIndex;
}
void NewsModel::setProcessedIndex(int index)
{
if (index < 0 || index >= m_items.size() || m_processedIndex == index)
return;
m_processedIndex = index;
emit processedIndexChanged(index);
}
void NewsModel::updateModel(const QJsonArray &serverItems)
{
QSet<QString> existingIds;
for (const NewsItem &item : m_items) {
existingIds.insert(item.id);
}
QList<NewsItem> newItems;
for (const QJsonValue &value : serverItems) {
if (!value.isObject())
continue;
const QJsonObject obj = value.toObject();
QString id = obj.value("id").toString();
if (!existingIds.contains(id)) {
NewsItem item;
item.id = id;
item.title = obj.value("title").toString();
item.content = obj.value("content").toString();
item.timestamp = QDateTime::fromString(obj.value("timestamp").toString(), Qt::ISODate);
item.read = m_readIds.contains(id);
newItems.append(item);
existingIds.insert(id);
}
}
beginResetModel();
m_items.append(newItems);
std::sort(m_items.begin(), m_items.end(), [](const NewsItem &a, const NewsItem &b) { return a.timestamp > b.timestamp; });
endResetModel();
emit hasUnreadChanged();
}
bool NewsModel::hasUnread() const
{
for (const NewsItem &item : m_items) {
if (!item.read)
return true;
}
return false;
}
void NewsModel::loadReadIds()
{
QStringList ids = m_settings->readNewsIds();
m_readIds = QSet<QString>(ids.begin(), ids.end());
}
void NewsModel::saveReadIds() const
{
m_settings->setReadNewsIds(QStringList(m_readIds.begin(), m_readIds.end()));
}
+62
View File
@@ -0,0 +1,62 @@
#ifndef NEWSMODEL_H
#define NEWSMODEL_H
#include "settings.h"
#include <QAbstractListModel>
#include <QDateTime>
#include <QJsonArray>
#include <QSet>
#include <QString>
#include <QVector>
#include <memory>
struct NewsItem
{
QString id;
QString title;
QString content;
QDateTime timestamp;
bool read;
};
class NewsModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Roles {
IdRole = Qt::UserRole + 1,
TitleRole,
ContentRole,
TimestampRole,
IsReadRole,
IsProcessedRole
};
explicit NewsModel(const std::shared_ptr<Settings> &settings, QObject *parent = nullptr);
Q_INVOKABLE void markAsRead(int index);
Q_PROPERTY(int processedIndex READ processedIndex WRITE setProcessedIndex NOTIFY processedIndexChanged)
Q_PROPERTY(bool hasUnread READ hasUnread NOTIFY hasUnreadChanged)
int processedIndex() const;
void setProcessedIndex(int index);
void updateModel(const QJsonArray &items);
bool hasUnread() const;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override;
signals:
void processedIndexChanged(int index);
void hasUnreadChanged();
private:
QVector<NewsItem> m_items;
int m_processedIndex = -1;
std::shared_ptr<Settings> m_settings;
QSet<QString> m_readIds;
void loadReadIds();
void saveReadIds() const;
};
#endif // NEWSMODEL_H
+64 -1
View File
@@ -44,6 +44,8 @@ ServersModel::ServersModel(std::shared_ptr<Settings> settings, QObject *parent)
connect(this, &ServersModel::processedServerIndexChanged, this, &ServersModel::processedServerChanged);
connect(this, &ServersModel::dataChanged, this, &ServersModel::processedServerChanged);
connect(this, &QAbstractItemModel::modelReset, this, &ServersModel::recomputeGatewayStacks);
}
int ServersModel::rowCount(const QModelIndex &parent) const
@@ -375,7 +377,6 @@ QHash<int, QByteArray> ServersModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[NameRole] = "serverName";
roles[NameRole] = "name";
roles[ServerDescriptionRole] = "serverDescription";
roles[CollapsedServerDescriptionRole] = "collapsedServerDescription";
@@ -756,6 +757,68 @@ bool ServersModel::isServerFromApi(const int serverIndex)
return data(serverIndex, IsServerFromTelegramApiRole).toBool() || data(serverIndex, IsServerFromGatewayApiRole).toBool();
}
bool ServersModel::hasServersFromGatewayApi()
{
return !m_gatewayStacks.isEmpty();
}
bool ServersModel::GatewayStacks::operator==(const GatewayStacks &other) const
{
return userCountryCodes == other.userCountryCodes && serviceTypes == other.serviceTypes;
}
QJsonObject ServersModel::GatewayStacks::toJson() const
{
QJsonObject obj;
if (!userCountryCodes.isEmpty()) {
obj.insert(configKey::userCountryCode, QJsonArray::fromStringList(userCountryCodes.values()));
}
if (!serviceTypes.isEmpty()) {
obj.insert(configKey::serviceType, QJsonArray::fromStringList(serviceTypes.values()));
}
return obj;
}
void ServersModel::recomputeGatewayStacks()
{
const bool wasEmpty = m_gatewayStacks.isEmpty();
GatewayStacks computed;
bool hasNewTags = false;
for (int i = 0; i < m_servers.count(); ++i) {
if (data(i, IsServerFromGatewayApiRole).toBool()) {
const QJsonObject server = m_servers.at(i).toObject();
const QJsonObject apiConfig = server.value(configKey::apiConfig).toObject();
const QString userCountryCode = apiConfig.value(configKey::userCountryCode).toString();
const QString serviceType = apiConfig.value(configKey::serviceType).toString();
if (!userCountryCode.isEmpty()) {
if (!m_gatewayStacks.userCountryCodes.contains(userCountryCode)) {
hasNewTags = true;
}
computed.userCountryCodes.insert(userCountryCode);
}
if (!serviceType.isEmpty()) {
if (!m_gatewayStacks.serviceTypes.contains(serviceType)) {
hasNewTags = true;
}
computed.serviceTypes.insert(serviceType);
}
}
}
m_gatewayStacks = std::move(computed);
if (hasNewTags) {
emit gatewayStacksExpanded();
}
if (wasEmpty != m_gatewayStacks.isEmpty()) {
emit hasServersFromGatewayApiChanged();
}
}
bool ServersModel::isApiKeyExpired(const int serverIndex)
{
auto serverConfig = m_servers.at(serverIndex).toObject();
+22
View File
@@ -10,6 +10,16 @@ class ServersModel : public QAbstractListModel
{
Q_OBJECT
public:
struct GatewayStacks
{
QSet<QString> userCountryCodes;
QSet<QString> serviceTypes;
bool isEmpty() const { return userCountryCodes.isEmpty() && serviceTypes.isEmpty(); }
bool operator==(const GatewayStacks &other) const;
QJsonObject toJson() const;
};
enum Roles {
NameRole = Qt::UserRole + 1,
ServerDescriptionRole,
@@ -52,6 +62,8 @@ public:
void resetModel();
GatewayStacks gatewayStacks() const { return m_gatewayStacks; }
Q_PROPERTY(int defaultIndex READ getDefaultServerIndex WRITE setDefaultServerIndex NOTIFY defaultServerIndexChanged)
Q_PROPERTY(QString defaultServerName READ getDefaultServerName NOTIFY defaultServerNameChanged)
Q_PROPERTY(QString defaultServerDefaultContainerName READ getDefaultServerDefaultContainerName NOTIFY defaultServerDefaultContainerChanged)
@@ -62,6 +74,8 @@ public:
defaultServerDefaultContainerChanged)
Q_PROPERTY(bool isDefaultServerFromApi READ isDefaultServerFromApi NOTIFY defaultServerIndexChanged)
Q_PROPERTY(bool hasServersFromGatewayApi READ hasServersFromGatewayApi NOTIFY hasServersFromGatewayApiChanged)
Q_PROPERTY(int processedIndex READ getProcessedServerIndex WRITE setProcessedServerIndex NOTIFY processedServerIndexChanged)
Q_PROPERTY(bool processedServerIsPremium READ processedServerIsPremium NOTIFY processedServerChanged)
@@ -82,6 +96,8 @@ public slots:
bool isDefaultServerHasWriteAccess();
bool hasServerWithWriteAccess();
bool hasServersFromGatewayApi();
const int getServersCount();
void setProcessedServerIndex(const int index);
@@ -147,6 +163,9 @@ signals:
void updateApiCountryModel();
void updateApiServicesModel();
void hasServersFromGatewayApiChanged();
void gatewayStacksExpanded();
private:
ServerCredentials serverCredentials(int index) const;
@@ -167,6 +186,9 @@ private:
int m_processedServerIndex;
bool m_isAmneziaDnsEnabled = m_settings->useAmneziaDns();
GatewayStacks m_gatewayStacks;
void recomputeGatewayStacks();
};
#endif // SERVERSMODEL_H
+28
View File
@@ -109,6 +109,34 @@ PageType {
}
}
BasicButtonType {
id: devGatewayButton
objectName: "devGatewayButton"
property bool isDevGatewayEnabled: SettingsController.isDevGatewayEnv
Layout.alignment: Qt.AlignHCenter
implicitHeight: 36
defaultColor: AmneziaStyle.color.transparent
hoveredColor: AmneziaStyle.color.translucentWhite
pressedColor: AmneziaStyle.color.sheerWhite
disabledColor: AmneziaStyle.color.mutedGray
textColor: AmneziaStyle.color.mutedGray
borderWidth: 0
visible: SettingsController.isDevModeEnabled && isDevGatewayEnabled
text: qsTr("Dev gateway enabled")
Keys.onEnterPressed: this.clicked()
Keys.onReturnPressed: this.clicked()
onClicked: {
PageController.goToPage(PageEnum.PageDevMenu)
}
}
ConnectButton {
id: connectButton
objectName: "connectButton"
+181 -163
View File
@@ -1,163 +1,181 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import PageEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
PageType {
id: root
ListViewType {
id: listView
anchors.fill: parent
header: ColumnLayout {
width: listView.width
BaseHeaderType {
id: header
Layout.fillWidth: true
Layout.topMargin: 24
Layout.bottomMargin: 16
Layout.rightMargin: 16
Layout.leftMargin: 16
headerText: qsTr("Settings")
}
}
model: settingsEntries
delegate: ColumnLayout {
width: listView.width
spacing: 0
LabelWithButtonType {
Layout.fillWidth: true
visible: isVisible
text: title
rightImageSource: "qrc:/images/controls/chevron-right.svg"
leftImageSource: leftImagePath
clickedFunction: clickedHandler
}
DividerType {
visible: isVisible
}
}
footer: ColumnLayout {
width: listView.width
LabelWithButtonType {
id: close
visible: GC.isDesktop()
Layout.fillWidth: true
text: qsTr("Close application")
leftImageSource: "qrc:/images/controls/x-circle.svg"
isLeftImageHoverEnabled: false
clickedFunction: function() {
PageController.closeApplication()
}
}
DividerType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
visible: GC.isDesktop()
}
}
}
property list<QtObject> settingsEntries: [
servers,
connection,
application,
backup,
about,
devConsole
]
QtObject {
id: servers
property string title: qsTr("Servers")
readonly property string leftImagePath: "qrc:/images/controls/server.svg"
property bool isVisible: true
readonly property var clickedHandler: function() {
PageController.goToPage(PageEnum.PageSettingsServersList)
}
}
QtObject {
id: connection
property string title: qsTr("Connection")
readonly property string leftImagePath: "qrc:/images/controls/radio.svg"
property bool isVisible: true
readonly property var clickedHandler: function() {
PageController.goToPage(PageEnum.PageSettingsConnection)
}
}
QtObject {
id: application
property string title: qsTr("Application")
readonly property string leftImagePath: "qrc:/images/controls/app.svg"
property bool isVisible: true
readonly property var clickedHandler: function() {
PageController.goToPage(PageEnum.PageSettingsApplication)
}
}
QtObject {
id: backup
property string title: qsTr("Backup")
readonly property string leftImagePath: "qrc:/images/controls/save.svg"
property bool isVisible: true
readonly property var clickedHandler: function() {
PageController.goToPage(PageEnum.PageSettingsBackup)
}
}
QtObject {
id: about
property string title: qsTr("About AmneziaVPN")
readonly property string leftImagePath: "qrc:/images/controls/amnezia.svg"
property bool isVisible: true
readonly property var clickedHandler: function() {
PageController.goToPage(PageEnum.PageSettingsAbout)
}
}
QtObject {
id: devConsole
property string title: qsTr("Dev console")
readonly property string leftImagePath: "qrc:/images/controls/bug.svg"
property bool isVisible: SettingsController.isDevModeEnabled
readonly property var clickedHandler: function() {
PageController.goToPage(PageEnum.PageDevMenu)
}
}
}
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import PageEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
PageType {
id: root
ListViewType {
id: listView
anchors.fill: parent
header: ColumnLayout {
width: listView.width
BaseHeaderType {
id: header
Layout.fillWidth: true
Layout.topMargin: 24
Layout.bottomMargin: 16
Layout.rightMargin: 16
Layout.leftMargin: 16
headerText: qsTr("Settings")
}
}
model: settingsEntries
delegate: ColumnLayout {
width: listView.width
spacing: 0
LabelWithButtonType {
Layout.fillWidth: true
visible: isVisible
text: title
rightImageSource: "qrc:/images/controls/chevron-right.svg"
leftImageSource: leftImagePath
clickedFunction: clickedHandler
}
DividerType {
visible: isVisible
}
}
footer: ColumnLayout {
width: listView.width
LabelWithButtonType {
id: close
visible: GC.isDesktop()
Layout.fillWidth: true
text: qsTr("Close application")
leftImageSource: "qrc:/images/controls/x-circle.svg"
isLeftImageHoverEnabled: false
clickedFunction: function() {
PageController.closeApplication()
}
}
DividerType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
visible: GC.isDesktop()
}
}
}
property list<QtObject> settingsEntries: [
servers,
connection,
application,
news,
backup,
about,
devConsole
]
QtObject {
id: servers
property string title: qsTr("Servers")
readonly property string leftImagePath: "qrc:/images/controls/server.svg"
property bool isVisible: true
readonly property var clickedHandler: function() {
PageController.goToPage(PageEnum.PageSettingsServersList)
}
}
QtObject {
id: connection
property string title: qsTr("Connection")
readonly property string leftImagePath: "qrc:/images/controls/radio.svg"
property bool isVisible: true
readonly property var clickedHandler: function() {
PageController.goToPage(PageEnum.PageSettingsConnection)
}
}
QtObject {
id: application
property string title: qsTr("Application")
readonly property string leftImagePath: "qrc:/images/controls/app.svg"
property bool isVisible: true
readonly property var clickedHandler: function() {
PageController.goToPage(PageEnum.PageSettingsApplication)
}
}
QtObject {
id: news
property string title: qsTr("News & Notifications")
readonly property string leftImagePath: NewsModel.hasUnread ? "qrc:/images/controls/news-unread.svg" : "qrc:/images/controls/news.svg"
property bool isVisible: ServersModel.hasServersFromGatewayApi
readonly property var clickedHandler: function() {
if (!ServersModel.hasServersFromGatewayApi) {
return;
}
PageController.showBusyIndicator(true)
ApiNewsController.fetchNews();
PageController.goToPage(PageEnum.PageSettingsNewsNotifications)
PageController.showBusyIndicator(false)
}
}
QtObject {
id: backup
property string title: qsTr("Backup")
readonly property string leftImagePath: "qrc:/images/controls/save.svg"
property bool isVisible: true
readonly property var clickedHandler: function() {
PageController.goToPage(PageEnum.PageSettingsBackup)
}
}
QtObject {
id: about
property string title: qsTr("About AmneziaVPN")
readonly property string leftImagePath: "qrc:/images/controls/amnezia.svg"
property bool isVisible: true
readonly property var clickedHandler: function() {
PageController.goToPage(PageEnum.PageSettingsAbout)
}
}
QtObject {
id: devConsole
property string title: qsTr("Dev console")
readonly property string leftImagePath: "qrc:/images/controls/bug.svg"
property bool isVisible: SettingsController.isDevModeEnabled
readonly property var clickedHandler: function() {
PageController.goToPage(PageEnum.PageDevMenu)
}
}
}
@@ -359,7 +359,7 @@ PageType {
PageController.showNotificationMessage(qsTr("Cannot unlink device during active connection"))
} else {
PageController.showBusyIndicator(true)
if (ApiConfigsController.deactivateDevice()) {
if (ApiConfigsController.deactivateDevice(false)) {
ApiSettingsController.getAccountInfo(true)
}
PageController.showBusyIndicator(false)
@@ -396,7 +396,7 @@ PageType {
PageController.showNotificationMessage(qsTr("Cannot remove server during active connection"))
} else {
PageController.showBusyIndicator(true)
if (ApiConfigsController.deactivateDevice()) {
if (ApiConfigsController.deactivateDevice(true)) {
InstallController.removeProcessedServer()
}
PageController.showBusyIndicator(false)
@@ -119,7 +119,7 @@ PageType {
if (fileName !== "") {
PageController.showBusyIndicator(true)
ExportController.exportConfig(fileName)
ApiConfigsController.exportVpnKey(fileName)
PageController.showBusyIndicator(false)
}
}
@@ -155,7 +155,7 @@ PageType {
textString: qsTr("Only \"Apps from the list should not have access via VPN\" mode is available on Windows")
iconPath: "qrc:/images/controls/alert-circle.svg"
enabled: (Qt.platform.os === "windows") && root.pageEnabled
visible: (Qt.platform.os === "windows") && root.pageEnabled
}
}
@@ -0,0 +1,69 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import PageEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
import SortFilterProxyModel 0.2
PageType {
id: root
property var newsItem
SortFilterProxyModel {
id: proxyNews
sourceModel: NewsModel
filters: [ ValueFilter { roleName: "isProcessed"; value: true } ]
Component.onCompleted: root.newsItem = proxyNews.get(0)
}
Connections {
target: NewsModel
function onProcessedIndexChanged() {
root.newsItem = proxyNews.get(0)
}
}
BackButtonType {
id: backButton
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20
}
FlickableType {
id: fl
anchors.top: backButton.bottom
anchors.bottom: parent.bottom
contentHeight: content.height
ColumnLayout {
id: content
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
spacing: 0
BaseHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: newsItem.title
}
ParagraphTextType {
Layout.fillWidth: true
Layout.topMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
text: newsItem.content
}
}
}
}
@@ -0,0 +1,81 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import PageEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
PageType {
id: root
ColumnLayout {
id: header
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20
BackButtonType {
id: backButton
}
BaseHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: qsTr("News & Notifications")
}
}
ListView {
id: newsList
width: parent.width
anchors.top: header.bottom
anchors.topMargin: 16
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
property bool isFocusable: true
model: NewsModel
clip: true
reuseItems: true
delegate: Item {
implicitWidth: newsList.width
implicitHeight: content.implicitHeight
ColumnLayout {
id: content
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
LabelWithButtonType {
Layout.fillWidth: true
leftImageSource: read ? "" : "qrc:/images/controls/unread-dot.svg"
isSmallLeftImage: !read
text: title
rightImageSource: "qrc:/images/controls/chevron-right.svg"
clickedFunction: function() {
NewsModel.markAsRead(index)
NewsModel.processedIndex = index
PageController.goToPage(PageEnum.PageSettingsNewsDetail)
}
}
DividerType {}
}
}
}
}
@@ -350,7 +350,6 @@ PageType {
Layout.fillWidth: true
text: qsTr("Clear site list")
rightImageSource: "qrc:/images/controls/trash.svg"
clickedFunction: function() {
var headerText = qsTr("Clear site list?")
@@ -87,7 +87,7 @@ PageType {
textFormat: Text.RichText
text: {
var text = ApiServicesModel.getSelectedServiceData("features")
return text.replace("%1", LanguageModel.getCurrentSiteUrl("free"))
return text.replace("%1", LanguageModel.getCurrentSiteUrl("free")).replace("/free", "") // todo link should come from gateway
}
MouseArea {
@@ -35,6 +35,7 @@ PageType {
target: ImportController
function onImportErrorOccurred(error, goToPageHome) {
PageController.showBusyIndicator(false)
if (goToPageHome) {
PageController.goToStartPage()
} else {
@@ -43,6 +44,7 @@ PageType {
}
function onImportFinished() {
PageController.showBusyIndicator(false)
if (!ConnectionController.isConnected) {
ServersModel.setDefaultServerIndex(ServersModel.getServersCount() - 1);
ServersModel.processedIndex = ServersModel.defaultIndex
@@ -216,6 +218,7 @@ PageType {
if (cloakingCheckBoxItem.checked) {
ImportController.processNativeWireGuardConfig()
}
PageController.showBusyIndicator(true)
ImportController.importConfig()
}
}
+7 -1
View File
@@ -380,7 +380,13 @@ PageType {
objectName: "settingsTabButton"
isSelected: tabBar.currentIndex === 2
image: "qrc:/images/controls/settings.svg"
image: (ServersModel.hasServersFromGatewayApi && NewsModel.hasUnread) ? "qrc:/images/controls/settings-news.svg" : "qrc:/images/controls/settings.svg"
Binding {
target: settingsTabButton
property: "defaultColor"
value: "transparent"
when: (ServersModel.hasServersFromGatewayApi && NewsModel.hasUnread)
}
clickedFunc: function () {
tabBarStackView.goToTabBarPage(PageEnum.PageSettings)
tabBar.currentIndex = 2
+57 -14
View File
@@ -34,23 +34,66 @@ clang -v
# Generate XCodeProj
$QT_BIN_DIR/qt-cmake . -B $BUILD_DIR -GXcode -DQT_HOST_PATH=$QT_MACOS_ROOT_DIR -DDEPLOY=ON
KEYCHAIN=amnezia.build.ios.keychain
KEYCHAIN_FILE=$HOME/Library/Keychains/${KEYCHAIN}-db
cd $BUILD_DIR
xcodebuild archive \
-project AmneziaVPN.xcodeproj \
-scheme AmneziaVPN \
-configuration Release \
-archivePath ./build/AmneziaVPN.xcarchive \
CODE_SIGNING_ALLOWED=NO \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO
# Setup keychain
if [ "${IOS_SIGNING_CERT_BASE64+x}" ]; then
echo "Import certificate"
mkdir -p Payload
TRUST_CERT_CER=$BUILD_DIR/trust-cert.cer
SIGNING_CERT_P12=$BUILD_DIR/signing-cert.p12
cp -R ./build/AmneziaVPN.xcarchive/Products/Applications/AmneziaVPN.app Payload/
echo $IOS_TRUST_CERT_BASE64 | base64 --decode > $TRUST_CERT_CER
echo $IOS_SIGNING_CERT_BASE64 | base64 --decode > $SIGNING_CERT_P12
zip -r AmneziaVPN_unsigned.ipa Payload
shasum -a 256 $TRUST_CERT_CER
shasum -a 256 $SIGNING_CERT_P12
rm -rf Payload
KEYCHAIN_PASS=$IOS_SIGNING_CERT_PASSWORD
echo " Build setup completed successfully."
security create-keychain -p $KEYCHAIN_PASS $KEYCHAIN || true
security default-keychain -s $KEYCHAIN
security unlock-keychain -p $KEYCHAIN_PASS $KEYCHAIN
security default-keychain
security list-keychains
security import $TRUST_CERT_CER -k $KEYCHAIN -P "" -T /usr/bin/codesign
security import $SIGNING_CERT_P12 -k $KEYCHAIN -P $IOS_SIGNING_CERT_PASSWORD -T /usr/bin/codesign
security set-key-partition-list -S "apple-tool:,apple:,codesign:" -s -k $KEYCHAIN_PASS $KEYCHAIN
security find-identity -p codesigning
security set-keychain-settings $KEYCHAIN_FILE
security set-keychain-settings -t 3600 $KEYCHAIN_FILE
security unlock-keychain -p $KEYCHAIN_PASS $KEYCHAIN_FILE
# Copy provisioning prifiles
mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles/"
echo $IOS_APP_PROVISIONING_PROFILE | base64 --decode > ~/Library/MobileDevice/Provisioning\ Profiles/app.mobileprovision
echo $IOS_NE_PROVISIONING_PROFILE | base64 --decode > ~/Library/MobileDevice/Provisioning\ Profiles/ne.mobileprovision
shasum -a 256 ~/Library/MobileDevice/Provisioning\ Profiles/app.mobileprovision
shasum -a 256 ~/Library/MobileDevice/Provisioning\ Profiles/ne.mobileprovision
profile_uuid=`grep UUID -A1 -a ~/Library/MobileDevice/Provisioning\ Profiles/app.mobileprovision | grep -io "[-A-F0-9]\{36\}"`
profile_ne_uuid=`grep UUID -A1 -a ~/Library/MobileDevice/Provisioning\ Profiles/ne.mobileprovision | grep -io "[-A-F0-9]\{36\}"`
mv ~/Library/MobileDevice/Provisioning\ Profiles/app.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/$profile_uuid.mobileprovision
mv ~/Library/MobileDevice/Provisioning\ Profiles/ne.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/$profile_ne_uuid.mobileprovision
else
echo "Failed to import certificate, aborting..."
exit 1
fi
# Build project
xcodebuild \
"OTHER_CODE_SIGN_FLAGS=--keychain '$KEYCHAIN_FILE'" \
-configuration Release \
-scheme AmneziaVPN \
-destination "generic/platform=iOS,name=Any iOS'" \
-project $BUILD_DIR/AmneziaVPN.xcodeproj
# restore keychain
security default-keychain -s login.keychain
+1
View File
@@ -36,5 +36,6 @@ class IpcInterface
SLOT( bool enablePeerTraffic( const QJsonObject &configStr) );
SLOT( bool enableKillSwitch( const QJsonObject &excludeAddr, int vpnAdapterIndex) );
SLOT( bool updateResolvers(const QString& ifname, const QList<QHostAddress>& resolvers) );
SLOT( bool restoreResolvers() );
};
+4
View File
@@ -157,6 +157,10 @@ bool IpcServer::updateResolvers(const QString &ifname, const QList<QHostAddress>
return Router::updateResolvers(ifname, resolvers);
}
bool IpcServer::restoreResolvers() {
return Router::restoreResolvers();
}
void IpcServer::StartRoutingIpv6()
{
Router::StartRoutingIpv6();
+1
View File
@@ -42,6 +42,7 @@ public:
virtual bool disableKillSwitch() override;
virtual bool refreshKillSwitch( bool enabled ) override;
virtual bool updateResolvers(const QString& ifname, const QList<QHostAddress>& resolvers) override;
virtual bool restoreResolvers() override;
private:
int m_localpid = 0;
+11
View File
@@ -99,6 +99,17 @@ bool Router::updateResolvers(const QString& ifname, const QList<QHostAddress>& r
#endif
}
bool Router::restoreResolvers() {
#ifdef Q_OS_LINUX
return RouterLinux::Instance().restoreResolvers();
#endif
#ifdef Q_OS_MACOS
return RouterMac::Instance().restoreResolvers();
#endif
#ifdef Q_OS_WIN
return RouterWin::Instance().restoreResolvers();
#endif
}
void Router::StopRoutingIpv6()
{
+1
View File
@@ -26,6 +26,7 @@ public:
static void StartRoutingIpv6();
static void StopRoutingIpv6();
static bool updateResolvers(const QString& ifname, const QList<QHostAddress>& resolvers);
static bool restoreResolvers();
};
#endif // ROUTER_H
+4
View File
@@ -279,6 +279,10 @@ bool RouterLinux::updateResolvers(const QString& ifname, const QList<QHostAddres
return m_dnsUtil->updateResolvers(ifname, resolvers);
}
bool RouterLinux::restoreResolvers() {
return m_dnsUtil->restoreResolvers();
}
void RouterLinux::StartRoutingIpv6()
{
QProcess process;
+1
View File
@@ -36,6 +36,7 @@ public:
void StartRoutingIpv6();
void StopRoutingIpv6();
bool updateResolvers(const QString& ifname, const QList<QHostAddress>& resolvers);
bool restoreResolvers();
public slots:
private:
+3
View File
@@ -158,6 +158,9 @@ bool RouterMac::updateResolvers(const QString& ifname, const QList<QHostAddress>
return m_dnsUtil->updateResolvers(ifname, resolvers);
}
bool RouterMac::restoreResolvers() {
return m_dnsUtil->restoreResolvers();
}
bool RouterMac::deleteTun(const QString &dev)
{
+1
View File
@@ -33,6 +33,7 @@ public:
bool createTun(const QString &dev, const QString &subnet);
bool deleteTun(const QString &dev);
bool updateResolvers(const QString& ifname, const QList<QHostAddress>& resolvers);
bool restoreResolvers();
public slots:
+3
View File
@@ -443,6 +443,9 @@ bool RouterWin::updateResolvers(const QString& ifname, const QList<QHostAddress>
return m_dnsUtil->updateResolvers(ifname, resolvers);
}
bool RouterWin::restoreResolvers() {
return m_dnsUtil->restoreResolvers();
}
void RouterWin::StopRoutingIpv6()
{
+1
View File
@@ -47,6 +47,7 @@ public:
void suspendWcmSvc(bool suspend);
bool updateResolvers(const QString& ifname, const QList<QHostAddress>& resolvers);
bool restoreResolvers();
private:
RouterWin(RouterWin const &) = delete;