Compare commits

..

26 Commits

Author SHA1 Message Date
Pavel Yaumenau 541437e83b Feat: Fixed push notifications on VPN connect/disconnect 2026-04-06 17:45:24 +03:00
vkamn ccf8b63633 chore: hide extend buttons on external premium 2026-04-06 20:04:39 +08:00
vkamn af3a0e1701 chore: simplify benefits 2026-04-03 20:04:52 +08:00
vkamn d3eaead779 chore: minor codestyle fixes 2026-04-02 17:32:42 +08:00
vkamn 6249eea905 feat: additional parsing for storekit subscription plans 2026-04-01 14:25:47 +08:00
vkamn fa0ba4afd4 chore: minor fixes 2026-04-01 13:06:33 +08:00
vkamn 15fb5b20f0 chore: minor fix 2026-03-31 16:31:37 +08:00
vkamn 4b625fe70f chore: minor fixes 2026-03-31 16:17:55 +08:00
vkamn a4b97e8764 feat: add iap support for new premium info page 2026-03-31 16:12:34 +08:00
vkamn 285b9344c4 feat: move privacy policy and term of use to gateway 2026-03-30 19:21:27 +08:00
vkamn 2db1416d9f chore: add api message parsing for 422 error 2026-03-30 12:33:16 +08:00
vkamn bae2dd452b feat: add trial api support 2026-03-26 20:03:18 +08:00
vkamn 041219187b fix: fixed expired status when configs without an end date 2026-03-26 17:23:19 +08:00
vkamn a231bf9ab7 refactor: move plan and benefits into separate models 2026-03-26 17:16:41 +08:00
vkamn c29984ce60 chore: minor fixes 2026-03-26 14:53:45 +08:00
vkamn cb5cde1a37 feat: add new free info page 2026-03-26 14:17:54 +08:00
vkamn 7da5f1c368 feat: add new premium info page 2026-03-25 23:57:52 +08:00
vkamn ea645df7ec fix: hide renew button for appstore purchases 2026-03-25 22:21:58 +08:00
vkamn 8799d841a3 fix: hide renew button for free 2026-03-25 22:21:13 +08:00
vkamn 5a51814b2a refactor: use end_date from primary config for renew ui 2026-03-25 21:53:25 +08:00
vkamn 4531a0b4b7 Merge branch 'dev' of github-amnezia:amnezia-vpn/amnezia-client into HEAD 2026-03-25 19:51:26 +08:00
vkamn b9f4ff56d8 chore: add isInAppPurchase and isTestPurchase in primary config 2026-03-25 13:57:47 +08:00
vkamn edc42fb7e4 Merge branch 'dev' of github-amnezia:amnezia-vpn/amnezia-client into HEAD 2026-03-25 12:56:23 +08:00
spectrum 4e4f8a5ec5 feat: enhance StoreKit2Helper to handle entitlements and improve restore service from App Store functionality 2026-03-24 17:52:46 +02:00
vkamn 6de556e730 fix: fixed error 101 on connection event 2026-03-24 15:56:37 +08:00
vkamn 1134dc194b feat: iap for apple now use storekit2 2026-03-24 15:56:01 +08:00
79 changed files with 659 additions and 1918 deletions
+1 -8
View File
@@ -17,7 +17,6 @@ jobs:
QIF_VERSION: 4.7 QIF_VERSION: 4.7
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }} DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }} DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
@@ -99,7 +98,6 @@ jobs:
BUILD_ARCH: 64 BUILD_ARCH: 64
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }} DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }} DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
@@ -206,7 +204,6 @@ jobs:
CXX: c++ CXX: c++
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }} DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }} DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
@@ -321,7 +318,6 @@ jobs:
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }} DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }} DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
@@ -399,7 +395,6 @@ jobs:
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }} DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }} DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
@@ -482,7 +477,6 @@ jobs:
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }} DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }} DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
@@ -547,11 +541,10 @@ jobs:
env: env:
ANDROID_BUILD_PLATFORM: android-36 ANDROID_BUILD_PLATFORM: android-36
QT_VERSION: 6.11.1 QT_VERSION: 6.10.1
QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools' QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools'
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }} DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }} DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
-1
View File
@@ -17,7 +17,6 @@ jobs:
QIF_VERSION: 4.5 QIF_VERSION: 4.5
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }} DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }} DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
+2 -2
View File
@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
set(PROJECT AmneziaVPN) set(PROJECT AmneziaVPN)
set(AMNEZIAVPN_VERSION 4.8.18.0) set(AMNEZIAVPN_VERSION 4.8.15.0)
project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION} project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
DESCRIPTION "AmneziaVPN" DESCRIPTION "AmneziaVPN"
@@ -12,7 +12,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
set(RELEASE_DATE "${CURRENT_DATE}") set(RELEASE_DATE "${CURRENT_DATE}")
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
set(APP_ANDROID_VERSION_CODE 2128) set(APP_ANDROID_VERSION_CODE 2118)
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(MZ_PLATFORM_NAME "linux") set(MZ_PLATFORM_NAME "linux")
-1
View File
@@ -25,7 +25,6 @@ add_definitions(-DGIT_COMMIT_HASH="${GIT_COMMIT_HASH}")
add_definitions(-DPROD_AGW_PUBLIC_KEY="$ENV{PROD_AGW_PUBLIC_KEY}") add_definitions(-DPROD_AGW_PUBLIC_KEY="$ENV{PROD_AGW_PUBLIC_KEY}")
add_definitions(-DPROD_S3_ENDPOINT="$ENV{PROD_S3_ENDPOINT}") add_definitions(-DPROD_S3_ENDPOINT="$ENV{PROD_S3_ENDPOINT}")
add_definitions(-DFALLBACK_S3_ENDPOINT="$ENV{FALLBACK_S3_ENDPOINT}")
add_definitions(-DDEV_AGW_PUBLIC_KEY="$ENV{DEV_AGW_PUBLIC_KEY}") add_definitions(-DDEV_AGW_PUBLIC_KEY="$ENV{DEV_AGW_PUBLIC_KEY}")
add_definitions(-DDEV_AGW_ENDPOINT="$ENV{DEV_AGW_ENDPOINT}") add_definitions(-DDEV_AGW_ENDPOINT="$ENV{DEV_AGW_ENDPOINT}")
+3
View File
@@ -24,5 +24,8 @@
<string name="notificationSettingsDialogMessage">Для показа уведомлений необходимо включить уведомления в системных настройках</string> <string name="notificationSettingsDialogMessage">Для показа уведомлений необходимо включить уведомления в системных настройках</string>
<string name="openNotificationSettings">Открыть настройки уведомлений</string> <string name="openNotificationSettings">Открыть настройки уведомлений</string>
<string name="vpnStateEventChannelName">Уведомления о VPN</string>
<string name="vpnStateEventChannelDescription">Краткие оповещения при подключении и отключении VPN</string>
<string name="tvNoFileBrowser">Пожалуйста, установите приложение для просмотра файлов</string> <string name="tvNoFileBrowser">Пожалуйста, установите приложение для просмотра файлов</string>
</resources> </resources>
+3
View File
@@ -24,5 +24,8 @@
<string name="notificationSettingsDialogMessage">To show notifications, you must enable notifications in the system settings</string> <string name="notificationSettingsDialogMessage">To show notifications, you must enable notifications in the system settings</string>
<string name="openNotificationSettings">Open notification settings</string> <string name="openNotificationSettings">Open notification settings</string>
<string name="vpnStateEventChannelName">VPN connection alerts</string>
<string name="vpnStateEventChannelDescription">Brief alerts when VPN connects or disconnects</string>
<string name="tvNoFileBrowser">Please install a file management utility to browse files</string> <string name="tvNoFileBrowser">Please install a file management utility to browse files</string>
</resources> </resources>
@@ -1006,6 +1006,12 @@ class AmneziaActivity : QtActivity() {
@Suppress("unused") @Suppress("unused")
fun isNotificationPermissionGranted(): Boolean = applicationContext.isNotificationPermissionGranted() fun isNotificationPermissionGranted(): Boolean = applicationContext.isNotificationPermissionGranted()
/** Called from Qt (AndroidController) — CLI-570 VPN connect/disconnect heads-up. */
@Suppress("unused")
fun showVpnStateNotification(title: String, message: String) {
ServiceNotification.showVpnStateEvent(applicationContext, title, message)
}
@Suppress("unused") @Suppress("unused")
fun requestNotificationPermission() { fun requestNotificationPermission() {
val shouldShowPreRequest = shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS) val shouldShowPreRequest = shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)
@@ -26,6 +26,9 @@ private const val OLD_NOTIFICATION_CHANNEL_ID: String = "org.amnezia.vpn.notific
private const val NOTIFICATION_CHANNEL_ID: String = "org.amnezia.vpn.notifications" private const val NOTIFICATION_CHANNEL_ID: String = "org.amnezia.vpn.notifications"
const val NOTIFICATION_ID = 1337 const val NOTIFICATION_ID = 1337
const val VPN_STATE_EVENT_NOTIFICATION_ID = 1338
private const val VPN_STATE_EVENT_CHANNEL_ID = "org.amnezia.vpn.vpn_state_events"
private const val GET_ACTIVITY_REQUEST_CODE = 0 private const val GET_ACTIVITY_REQUEST_CODE = 0
private const val CONNECT_REQUEST_CODE = 1 private const val CONNECT_REQUEST_CODE = 1
private const val DISCONNECT_REQUEST_CODE = 2 private const val DISCONNECT_REQUEST_CODE = 2
@@ -162,8 +165,42 @@ class ServiceNotification(private val context: Context) {
.setDescription(context.resources.getString(R.string.notificationChannelDescription)) .setDescription(context.resources.getString(R.string.notificationChannelDescription))
.build() .build()
) )
createNotificationChannel(
Builder(VPN_STATE_EVENT_CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_DEFAULT)
.setShowBadge(false)
.setSound(null, null)
.setVibrationEnabled(false)
.setLightsEnabled(false)
.setName(context.getString(R.string.vpnStateEventChannelName))
.setDescription(context.getString(R.string.vpnStateEventChannelDescription))
.build()
)
} }
} }
/** Brief alert when VPN connects or disconnects (invoked from Qt via AmneziaActivity). */
fun showVpnStateEvent(context: Context, title: String, message: String) {
if (!context.isNotificationPermissionGranted()) return
val nm = NotificationManagerCompat.from(context)
val notification = NotificationCompat.Builder(context, VPN_STATE_EVENT_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_amnezia_round)
.setContentTitle(title)
.setContentText(message)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setCategory(NotificationCompat.CATEGORY_STATUS)
.setAutoCancel(true)
.setOnlyAlertOnce(true)
.setContentIntent(
PendingIntent.getActivity(
context,
GET_ACTIVITY_REQUEST_CODE,
Intent(context, AmneziaActivity::class.java),
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
)
.build()
nm.notify(VPN_STATE_EVENT_NOTIFICATION_ID, notification)
}
} }
} }
+2 -75
View File
@@ -4,9 +4,6 @@ import android.content.Context
import android.net.VpnService.Builder import android.net.VpnService.Builder
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.net.InetAddress
import java.net.ServerSocket
import java.util.UUID
import go.Seq import go.Seq
import org.amnezia.vpn.protocol.BadConfigException import org.amnezia.vpn.protocol.BadConfigException
import org.amnezia.vpn.protocol.Protocol import org.amnezia.vpn.protocol.Protocol
@@ -22,32 +19,11 @@ import org.amnezia.vpn.util.Log
import org.amnezia.vpn.util.net.InetNetwork import org.amnezia.vpn.util.net.InetNetwork
import org.amnezia.vpn.util.net.ip import org.amnezia.vpn.util.net.ip
import org.amnezia.vpn.util.net.parseInetAddress import org.amnezia.vpn.util.net.parseInetAddress
import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
private const val TAG = "Xray" private const val TAG = "Xray"
private const val LIBXRAY_TAG = "libXray" private const val LIBXRAY_TAG = "libXray"
private fun findSocksInboundIndex(inbounds: JSONArray): Int {
for (i in 0 until inbounds.length()) {
val o = inbounds.optJSONObject(i) ?: continue
if (o.optString("protocol").equals("socks", ignoreCase = true)) {
return i
}
}
return -1
}
private fun acquireFreeLocalPort(): Int {
try {
ServerSocket(0, 1, InetAddress.getByName("127.0.0.1")).use { return it.localPort }
} catch (e: Exception) {
throw VpnStartException(
"Failed to acquire free TCP port on 127.0.0.1 for SOCKS inbound: ${e.message}"
)
}
}
class Xray : Protocol() { class Xray : Protocol() {
private var isRunning: Boolean = false private var isRunning: Boolean = false
@@ -80,10 +56,6 @@ class Xray : Protocol() {
val xrayJsonConfig = config.optJSONObject("xray_config_data") val xrayJsonConfig = config.optJSONObject("xray_config_data")
?: config.optJSONObject("ssxray_config_data") ?: config.optJSONObject("ssxray_config_data")
?: throw BadConfigException("config_data not found") ?: throw BadConfigException("config_data not found")
// Inject SOCKS5 auth before starting xray. Re-uses existing credentials if present.
ensureInboundAuth(xrayJsonConfig)
val xrayConfig = parseConfig(config, xrayJsonConfig) val xrayConfig = parseConfig(config, xrayJsonConfig)
(xrayJsonConfig.optJSONObject("log") ?: JSONObject().also { xrayJsonConfig.put("log", it) }) (xrayJsonConfig.optJSONObject("log") ?: JSONObject().also { xrayJsonConfig.put("log", it) })
@@ -125,22 +97,9 @@ class Xray : Protocol() {
if (it.isNotBlank()) setMtu(it.toInt()) if (it.isNotBlank()) setMtu(it.toInt())
} }
val inbounds = xrayJsonConfig.getJSONArray("inbounds") val socksConfig = xrayJsonConfig.getJSONArray("inbounds")[0] as JSONObject
val socksIdx = findSocksInboundIndex(inbounds)
if (socksIdx < 0) {
throw BadConfigException("socks inbound not found")
}
val socksConfig = inbounds.getJSONObject(socksIdx)
socksConfig.getInt("port").let { setSocksPort(it) } socksConfig.getInt("port").let { setSocksPort(it) }
val socksSettings = socksConfig.optJSONObject("settings")
val accounts = socksSettings?.optJSONArray("accounts")
if (accounts != null && accounts.length() > 0) {
val account = accounts.getJSONObject(0)
setSocksUser(account.optString("user"))
setSocksPass(account.optString("pass"))
}
configSplitTunneling(config) configSplitTunneling(config)
configAppSplitTunneling(config) configAppSplitTunneling(config)
} }
@@ -203,10 +162,9 @@ class Xray : Protocol() {
} }
private fun runTun2Socks(config: XrayConfig, fd: Int) { private fun runTun2Socks(config: XrayConfig, fd: Int) {
val proxyUrl = "socks5://${config.socksUser}:${config.socksPass}@127.0.0.1:${config.socksPort}"
val tun2SocksConfig = Tun2SocksConfig().apply { val tun2SocksConfig = Tun2SocksConfig().apply {
mtu = config.mtu.toLong() mtu = config.mtu.toLong()
proxy = proxyUrl proxy = "socks5://127.0.0.1:${config.socksPort}"
device = "fd://$fd" device = "fd://$fd"
logLevel = "warn" logLevel = "warn"
} }
@@ -215,37 +173,6 @@ class Xray : Protocol() {
} }
} }
// Ensures SOCKS5 auth is present on the socks inbound settings.
// Re-uses existing credentials if already configured; otherwise generates random ones.
private fun ensureInboundAuth(xrayConfig: JSONObject) {
val inbounds = xrayConfig.optJSONArray("inbounds") ?: return
val socksIdx = findSocksInboundIndex(inbounds)
if (socksIdx < 0) return
val inbound = inbounds.getJSONObject(socksIdx)
inbound.put("port", acquireFreeLocalPort())
val settings = inbound.optJSONObject("settings") ?: JSONObject().also { inbound.put("settings", it) }
val accounts = settings.optJSONArray("accounts")
if (accounts != null && accounts.length() > 0) {
val account = accounts.getJSONObject(0)
if (account.optString("user").isNotEmpty() && account.optString("pass").isNotEmpty()) {
// Ensure auth mode is enforced even for imported configs that had accounts
// but auth: "noauth" (or no auth field).
settings.put("auth", "password")
inbound.put("settings", settings)
inbounds.put(socksIdx, inbound)
return
}
}
val user = UUID.randomUUID().toString().replace("-", "").substring(0, 16)
val pass = UUID.randomUUID().toString().replace("-", "")
settings.put("auth", "password")
settings.put("accounts", JSONArray().put(JSONObject().put("user", user).put("pass", pass)))
inbound.put("settings", settings)
inbounds.put(socksIdx, inbound)
}
companion object { companion object {
val instance: Xray by lazy { Xray() } val instance: Xray by lazy { Xray() }
} }
@@ -9,16 +9,12 @@ private const val XRAY_DEFAULT_MAX_MEMORY: Long = 50 shl 20 // 50 MB
class XrayConfig protected constructor( class XrayConfig protected constructor(
protocolConfigBuilder: ProtocolConfig.Builder, protocolConfigBuilder: ProtocolConfig.Builder,
val socksPort: Int, val socksPort: Int,
val socksUser: String,
val socksPass: String,
val maxMemory: Long, val maxMemory: Long,
) : ProtocolConfig(protocolConfigBuilder) { ) : ProtocolConfig(protocolConfigBuilder) {
protected constructor(builder: Builder) : this( protected constructor(builder: Builder) : this(
builder, builder,
builder.socksPort, builder.socksPort,
builder.socksUser,
builder.socksPass,
builder.maxMemory builder.maxMemory
) )
@@ -26,12 +22,6 @@ class XrayConfig protected constructor(
internal var socksPort: Int = 0 internal var socksPort: Int = 0
private set private set
internal var socksUser: String = ""
private set
internal var socksPass: String = ""
private set
internal var maxMemory: Long = XRAY_DEFAULT_MAX_MEMORY internal var maxMemory: Long = XRAY_DEFAULT_MAX_MEMORY
private set private set
@@ -39,10 +29,6 @@ class XrayConfig protected constructor(
fun setSocksPort(port: Int) = apply { socksPort = port } fun setSocksPort(port: Int) = apply { socksPort = port }
fun setSocksUser(user: String) = apply { socksUser = user }
fun setSocksPass(pass: String) = apply { socksPass = pass }
fun setMaxMemory(maxMemory: Long) = apply { this.maxMemory = maxMemory } fun setMaxMemory(maxMemory: Long) = apply { this.maxMemory = maxMemory }
override fun build(): XrayConfig = configBuild().run { XrayConfig(this@Builder) } override fun build(): XrayConfig = configBuild().run { XrayConfig(this@Builder) }
+2
View File
@@ -32,6 +32,7 @@ set(LIBS ${LIBS}
set(HEADERS ${HEADERS} set(HEADERS ${HEADERS}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/macos/macos_ne_vpn_notification.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.h ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.h ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.h ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.h
@@ -43,6 +44,7 @@ set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_contro
set(SOURCES ${SOURCES} set(SOURCES ${SOURCES}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/macos/macos_ne_vpn_notification.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.mm ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.mm ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.mm ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.mm
+10 -4
View File
@@ -45,9 +45,12 @@ if(NOT IOS AND NOT MACOS_NE)
) )
endif() endif()
if(NOT ANDROID) set(HEADERS ${HEADERS}
set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/ui/notificationhandler.h ${CLIENT_ROOT_DIR}/ui/notificationhandler.h
)
if(ANDROID)
set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/platforms/android/android_notificationhandler.h
) )
endif() endif()
@@ -109,9 +112,12 @@ if(APPLE AND NOT IOS)
) )
endif() endif()
if(NOT ANDROID) set(SOURCES ${SOURCES}
set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/ui/notificationhandler.cpp ${CLIENT_ROOT_DIR}/ui/notificationhandler.cpp
)
if(ANDROID)
set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/platforms/android/android_notificationhandler.cpp
) )
endif() endif()
+2 -2
View File
@@ -298,14 +298,14 @@ bool ContainerProps::isSupportedByCurrentPlatform(DockerContainer c)
} }
#elif defined(MACOS_NE) #elif defined(MACOS_NE)
// macOS build using Network Extension allow OpenVPN for parity with iOS. // macOS build using Network Extension hide OpenVPN-based containers
switch (c) { switch (c) {
case DockerContainer::OpenVpn: return true;
case DockerContainer::WireGuard: return true; case DockerContainer::WireGuard: return true;
case DockerContainer::Awg2: return true; case DockerContainer::Awg2: return true;
case DockerContainer::Awg: return true; case DockerContainer::Awg: return true;
case DockerContainer::Xray: return true; case DockerContainer::Xray: return true;
case DockerContainer::SSXray: return true; case DockerContainer::SSXray: return true;
case DockerContainer::OpenVpn:
case DockerContainer::Cloak: case DockerContainer::Cloak:
case DockerContainer::ShadowSocks: case DockerContainer::ShadowSocks:
return false; return false;
+1 -2
View File
@@ -10,6 +10,7 @@ namespace apiDefs
AmneziaFreeV3, AmneziaFreeV3,
AmneziaPremiumV1, AmneziaPremiumV1,
AmneziaPremiumV2, AmneziaPremiumV2,
AmneziaTrialV2,
SelfHosted, SelfHosted,
ExternalPremium, ExternalPremium,
ExternalTrial ExternalTrial
@@ -56,7 +57,6 @@ namespace apiDefs
constexpr QLatin1String maxDeviceCount("max_device_count"); constexpr QLatin1String maxDeviceCount("max_device_count");
constexpr QLatin1String subscriptionEndDate("subscription_end_date"); constexpr QLatin1String subscriptionEndDate("subscription_end_date");
constexpr QLatin1String subscriptionExpiredByServer("subscription_expired_by_server"); constexpr QLatin1String subscriptionExpiredByServer("subscription_expired_by_server");
constexpr QLatin1String subscriptionStatus("subscription_status");
constexpr QLatin1String subscription("subscription"); constexpr QLatin1String subscription("subscription");
constexpr QLatin1String endDate("end_date"); constexpr QLatin1String endDate("end_date");
constexpr QLatin1String issuedConfigs("issued_configs"); constexpr QLatin1String issuedConfigs("issued_configs");
@@ -83,7 +83,6 @@ namespace apiDefs
constexpr QLatin1String serviceInfo("service_info"); constexpr QLatin1String serviceInfo("service_info");
constexpr QLatin1String isAdVisible("is_ad_visible"); constexpr QLatin1String isAdVisible("is_ad_visible");
constexpr QLatin1String isRenewalAvailable("is_renewal_available");
constexpr QLatin1String adHeader("ad_header"); constexpr QLatin1String adHeader("ad_header");
constexpr QLatin1String adDescription("ad_description"); constexpr QLatin1String adDescription("ad_description");
constexpr QLatin1String adEndpoint("ad_endpoint"); constexpr QLatin1String adEndpoint("ad_endpoint");
+9 -8
View File
@@ -10,7 +10,6 @@ namespace
const QByteArray AMNEZIA_CONFIG_SIGNATURE = QByteArray::fromHex("000000ff"); const QByteArray AMNEZIA_CONFIG_SIGNATURE = QByteArray::fromHex("000000ff");
constexpr QLatin1String unprocessableSubscriptionMessage("Failed to retrieve subscription information. Is it activated?"); constexpr QLatin1String unprocessableSubscriptionMessage("Failed to retrieve subscription information. Is it activated?");
constexpr QLatin1String trialAlreadyUsedMessage("trial subscription already used");
QDateTime subscriptionEndUtcFromString(const QString &subscriptionEndDate) QDateTime subscriptionEndUtcFromString(const QString &subscriptionEndDate)
{ {
@@ -101,6 +100,7 @@ apiDefs::ConfigType apiUtils::getConfigType(const QJsonObject &serverConfigObjec
}; };
case apiDefs::ConfigSource::AmneziaGateway: { case apiDefs::ConfigSource::AmneziaGateway: {
constexpr QLatin1String servicePremium("amnezia-premium"); constexpr QLatin1String servicePremium("amnezia-premium");
constexpr QLatin1String serviceTrial("amnezia-trial");
constexpr QLatin1String serviceFree("amnezia-free"); constexpr QLatin1String serviceFree("amnezia-free");
constexpr QLatin1String serviceExternalPremium("external-premium"); constexpr QLatin1String serviceExternalPremium("external-premium");
constexpr QLatin1String serviceExternalTrial("external-trial"); constexpr QLatin1String serviceExternalTrial("external-trial");
@@ -110,6 +110,8 @@ apiDefs::ConfigType apiUtils::getConfigType(const QJsonObject &serverConfigObjec
if (serviceType == servicePremium) { if (serviceType == servicePremium) {
return apiDefs::ConfigType::AmneziaPremiumV2; return apiDefs::ConfigType::AmneziaPremiumV2;
} else if (serviceType == serviceTrial) {
return apiDefs::ConfigType::AmneziaTrialV2;
} else if (serviceType == serviceFree) { } else if (serviceType == serviceFree) {
return apiDefs::ConfigType::AmneziaFreeV3; return apiDefs::ConfigType::AmneziaFreeV3;
} else if (serviceType == serviceExternalPremium) { } else if (serviceType == serviceExternalPremium) {
@@ -158,6 +160,7 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
qDebug() << QString::fromUtf8(responseBody); qDebug() << QString::fromUtf8(responseBody);
qDebug() << replyError; qDebug() << replyError;
qDebug() << replyErrorString;
qDebug() << httpStatusCode; qDebug() << httpStatusCode;
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody); QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
@@ -165,9 +168,6 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
QJsonObject jsonObj = jsonDoc.object(); QJsonObject jsonObj = jsonDoc.object();
const int httpStatusFromBody = jsonObj.value(QStringLiteral("http_status")).toInt(-1); const int httpStatusFromBody = jsonObj.value(QStringLiteral("http_status")).toInt(-1);
if (httpStatusFromBody == httpStatusCodeConflict) { if (httpStatusFromBody == httpStatusCodeConflict) {
if (apiErrorMessageFromJson(jsonObj).contains(trialAlreadyUsedMessage, Qt::CaseInsensitive)) {
return amnezia::ErrorCode::ApiTrialAlreadyUsedError;
}
return amnezia::ErrorCode::ApiConfigLimitError; return amnezia::ErrorCode::ApiConfigLimitError;
} }
if (httpStatusFromBody == httpStatusCodeNotFound) { if (httpStatusFromBody == httpStatusCodeNotFound) {
@@ -189,13 +189,14 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
} }
qDebug() << "something went wrong"; qDebug() << "something went wrong";
return amnezia::ErrorCode::ApiConfigDownloadError; return amnezia::ErrorCode::InternalError;
} }
bool apiUtils::isPremiumServer(const QJsonObject &serverConfigObject) bool apiUtils::isPremiumServer(const QJsonObject &serverConfigObject)
{ {
static const QSet<apiDefs::ConfigType> premiumTypes = { apiDefs::ConfigType::AmneziaPremiumV1, apiDefs::ConfigType::AmneziaPremiumV2, static const QSet<apiDefs::ConfigType> premiumTypes = { apiDefs::ConfigType::AmneziaPremiumV1, apiDefs::ConfigType::AmneziaPremiumV2,
apiDefs::ConfigType::ExternalPremium, apiDefs::ConfigType::ExternalTrial }; apiDefs::ConfigType::AmneziaTrialV2, apiDefs::ConfigType::ExternalPremium,
apiDefs::ConfigType::ExternalTrial };
return premiumTypes.contains(getConfigType(serverConfigObject)); return premiumTypes.contains(getConfigType(serverConfigObject));
} }
@@ -240,8 +241,8 @@ QString apiUtils::getPremiumV1VpnKey(const QJsonObject &serverConfigObject)
QString apiUtils::getPremiumV2VpnKey(const QJsonObject &serverConfigObject) QString apiUtils::getPremiumV2VpnKey(const QJsonObject &serverConfigObject)
{ {
auto configType = apiUtils::getConfigType(serverConfigObject); auto configType = apiUtils::getConfigType(serverConfigObject);
if (configType != apiDefs::ConfigType::AmneziaPremiumV2 && configType != apiDefs::ConfigType::ExternalPremium if (configType != apiDefs::ConfigType::AmneziaPremiumV2 && configType != apiDefs::ConfigType::AmneziaTrialV2
&& configType != apiDefs::ConfigType::ExternalTrial) { && configType != apiDefs::ConfigType::ExternalPremium && configType != apiDefs::ConfigType::ExternalTrial) {
return {}; return {};
} }
+1 -1
View File
@@ -13,7 +13,7 @@ namespace apiUtils
bool isSubscriptionExpired(const QString &subscriptionEndDate); bool isSubscriptionExpired(const QString &subscriptionEndDate);
bool isSubscriptionExpiringSoon(const QString &subscriptionEndDate, int withinDays = 30); bool isSubscriptionExpiringSoon(const QString &subscriptionEndDate, int withinDays = 10);
bool isPremiumServer(const QJsonObject &serverConfigObject); bool isPremiumServer(const QJsonObject &serverConfigObject);
+8 -2
View File
@@ -8,6 +8,11 @@
#include "platforms/android/android_controller.h" #include "platforms/android/android_controller.h"
#endif #endif
// SystemTrayNotificationHandler exists only on desktop + macOS NE builds (see client/cmake/sources.cmake).
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
#include "ui/systemtray_notificationhandler.h"
#endif
#if defined(Q_OS_IOS) #if defined(Q_OS_IOS)
#include "platforms/ios/ios_controller.h" #include "platforms/ios/ios_controller.h"
#include <AmneziaVPN-Swift.h> #include <AmneziaVPN-Swift.h>
@@ -242,7 +247,6 @@ void CoreController::initSignalHandlers()
void CoreController::initNotificationHandler() void CoreController::initNotificationHandler()
{ {
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
m_notificationHandler.reset(NotificationHandler::create(nullptr)); m_notificationHandler.reset(NotificationHandler::create(nullptr));
connect(m_vpnConnection.get(), &VpnConnection::connectionStateChanged, m_notificationHandler.get(), connect(m_vpnConnection.get(), &VpnConnection::connectionStateChanged, m_notificationHandler.get(),
@@ -255,8 +259,10 @@ void CoreController::initNotificationHandler()
&ConnectionController::closeConnection); &ConnectionController::closeConnection);
connect(this, &CoreController::translationsUpdated, m_notificationHandler.get(), &NotificationHandler::onTranslationsUpdated); connect(this, &CoreController::translationsUpdated, m_notificationHandler.get(), &NotificationHandler::onTranslationsUpdated);
auto* trayHandler = qobject_cast<SystemTrayNotificationHandler*>(m_notificationHandler.get()); #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
if (auto *trayHandler = qobject_cast<SystemTrayNotificationHandler *>(m_notificationHandler.get())) {
connect(this, &CoreController::websiteUrlChanged, trayHandler, &SystemTrayNotificationHandler::updateWebsiteUrl); connect(this, &CoreController::websiteUrlChanged, trayHandler, &SystemTrayNotificationHandler::updateWebsiteUrl);
}
#endif #endif
} }
+1 -9
View File
@@ -5,9 +5,7 @@
#include <QQmlContext> #include <QQmlContext>
#include <QThread> #include <QThread>
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) #include "ui/notificationhandler.h"
#include "ui/systemtray_notificationhandler.h"
#endif
#include "ui/controllers/api/apiConfigsController.h" #include "ui/controllers/api/apiConfigsController.h"
#include "ui/controllers/api/apiSettingsController.h" #include "ui/controllers/api/apiSettingsController.h"
@@ -51,10 +49,6 @@
#include "ui/models/sites_model.h" #include "ui/models/sites_model.h"
#include "ui/models/newsModel.h" #include "ui/models/newsModel.h"
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
#include "ui/notificationhandler.h"
#endif
class CoreController : public QObject class CoreController : public QObject
{ {
Q_OBJECT Q_OBJECT
@@ -101,9 +95,7 @@ private:
QSharedPointer<VpnConnection> m_vpnConnection; QSharedPointer<VpnConnection> m_vpnConnection;
QSharedPointer<QTranslator> m_translator; QSharedPointer<QTranslator> m_translator;
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
QScopedPointer<NotificationHandler> m_notificationHandler; QScopedPointer<NotificationHandler> m_notificationHandler;
#endif
QMetaObject::Connection m_reloadConfigErrorOccurredConnection; QMetaObject::Connection m_reloadConfigErrorOccurredConnection;
+76 -124
View File
@@ -18,7 +18,6 @@
#include "amnezia_application.h" #include "amnezia_application.h"
#include "core/api/apiUtils.h" #include "core/api/apiUtils.h"
#include "core/networkUtilities.h" #include "core/networkUtilities.h"
#include "settings.h"
#include "utilities.h" #include "utilities.h"
#ifdef AMNEZIA_DESKTOP #ifdef AMNEZIA_DESKTOP
@@ -50,80 +49,15 @@ namespace
constexpr int httpStatusCodeUnprocessableEntity = 422; constexpr int httpStatusCodeUnprocessableEntity = 422;
constexpr QLatin1String unprocessableSubscriptionMessage("Failed to retrieve subscription information. Is it activated?"); constexpr QLatin1String unprocessableSubscriptionMessage("Failed to retrieve subscription information. Is it activated?");
constexpr int proxyStorageRequestTimeoutMsecs = 3000;
QStringList shuffledProxyUrls(const QStringList &proxyUrls)
{
QStringList shuffled = proxyUrls;
std::random_device randomDevice;
std::mt19937 generator(randomDevice());
std::shuffle(shuffled.begin(), shuffled.end(), generator);
return shuffled;
}
QString getProxyUrlsCacheKey(const QString &serviceType, const QString &userCountryCode)
{
return QStringLiteral("service_%1_country_%2").arg(serviceType, userCountryCode);
}
bool decryptProxyUrlsPayload(const QByteArray &encryptedPayload, bool isDevEnvironment, QByteArray &decryptedPayload)
{
try {
QByteArray key = isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
if (!isDevEnvironment) {
QCryptographicHash hash(QCryptographicHash::Sha512);
hash.addData(key);
QByteArray h = hash.result().toHex();
QByteArray decKey = QByteArray::fromHex(h.left(64));
QByteArray iv = QByteArray::fromHex(h.mid(64, 32));
QByteArray ba = QByteArray::fromBase64(encryptedPayload);
QSimpleCrypto::QBlockCipher cipher;
decryptedPayload = cipher.decryptAesBlockCipher(ba, decKey, iv);
} else {
decryptedPayload = encryptedPayload;
}
return true;
} catch (...) {
Utils::logException();
return false;
}
}
QStringList readCachedProxyUrls(const QByteArray &cachedProxyUrlsEncrypted, bool isDevEnvironment)
{
if (cachedProxyUrlsEncrypted.isEmpty()) {
return {};
}
QByteArray cachedProxyUrlsDecrypted;
if (!decryptProxyUrlsPayload(cachedProxyUrlsEncrypted, isDevEnvironment, cachedProxyUrlsDecrypted)) {
qCritical() << "error decrypting cached proxy urls payload";
return {};
}
QJsonArray endpointsArray = QJsonDocument::fromJson(cachedProxyUrlsDecrypted).array();
QStringList endpoints;
endpoints.reserve(endpointsArray.size());
for (const QJsonValue &endpoint : endpointsArray) {
endpoints.push_back(endpoint.toString());
}
return endpoints;
}
} }
GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs, GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
const bool isStrictKillSwitchEnabled, const std::shared_ptr<Settings> &settings, const bool isStrictKillSwitchEnabled, QObject *parent)
QObject *parent)
: QObject(parent), : QObject(parent),
m_gatewayEndpoint(gatewayEndpoint), m_gatewayEndpoint(gatewayEndpoint),
m_isDevEnvironment(isDevEnvironment), m_isDevEnvironment(isDevEnvironment),
m_requestTimeoutMsecs(requestTimeoutMsecs), m_requestTimeoutMsecs(requestTimeoutMsecs),
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled), m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled)
m_settings(settings)
{ {
} }
@@ -350,33 +284,25 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString(""); auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString("");
auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString(""); auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString("");
QStringList primaryBaseUrls; QStringList baseUrls;
QStringList fallbackBaseUrls;
if (m_isDevEnvironment) { if (m_isDevEnvironment) {
primaryBaseUrls = QString(DEV_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts); baseUrls = QString(DEV_S3_ENDPOINT).split(", ");
} else { } else {
primaryBaseUrls = QString(PROD_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts); baseUrls = QString(PROD_S3_ENDPOINT).split(", ");
fallbackBaseUrls = QString(FALLBACK_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
} }
auto appendStorageUrls = [&serviceType, &userCountryCode](const QStringList &baseUrls, QStringList &target) { QStringList proxyStorageUrls;
if (!serviceType.isEmpty()) { if (!serviceType.isEmpty()) {
for (const auto &baseUrl : baseUrls) { for (const auto &baseUrl : baseUrls) {
QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8(); QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8();
target.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) + ".json"); proxyStorageUrls.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)
+ ".json");
} }
} }
for (const auto &baseUrl : baseUrls) { for (const auto &baseUrl : baseUrls)
target.push_back(baseUrl + "endpoints.json"); proxyStorageUrls.push_back(baseUrl + "endpoints.json");
}
};
QStringList proxyStorageUrls; getProxyUrlsAsync(proxyStorageUrls, 0, [this, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) {
appendStorageUrls(primaryBaseUrls, proxyStorageUrls);
appendStorageUrls(fallbackBaseUrls, proxyStorageUrls);
const QString proxyUrlsCacheKey = getProxyUrlsCacheKey(serviceType, userCountryCode);
getProxyUrlsAsync(proxyStorageUrls, 0, proxyUrlsCacheKey, [this, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) {
getProxyUrlAsync(proxyUrls, 0, [this, encRequestData, endpoint, processResponse](const QString &proxyUrl) { getProxyUrlAsync(proxyUrls, 0, [this, encRequestData, endpoint, processResponse](const QString &proxyUrl) {
bypassProxyAsync(endpoint, proxyUrl, encRequestData, bypassProxyAsync(endpoint, proxyUrl, encRequestData,
[processResponse, this](const QByteArray &decryptedBody, bool isDecryptionSuccessful, [processResponse, this](const QByteArray &decryptedBody, bool isDecryptionSuccessful,
@@ -401,48 +327,40 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
QStringList GatewayController::getProxyUrls(const QString &serviceType, const QString &userCountryCode) QStringList GatewayController::getProxyUrls(const QString &serviceType, const QString &userCountryCode)
{ {
QNetworkRequest request; QNetworkRequest request;
request.setTransferTimeout(proxyStorageRequestTimeoutMsecs); request.setTransferTimeout(m_requestTimeoutMsecs);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QEventLoop wait; QEventLoop wait;
QList<QSslError> sslErrors; QList<QSslError> sslErrors;
QNetworkReply *reply; QNetworkReply *reply;
QStringList primaryBaseUrls; QStringList baseUrls;
QStringList fallbackBaseUrls;
if (m_isDevEnvironment) { if (m_isDevEnvironment) {
primaryBaseUrls = QString(DEV_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts); baseUrls = QString(DEV_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
} else { } else {
primaryBaseUrls = QString(PROD_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts); baseUrls = QString(PROD_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
fallbackBaseUrls = QString(FALLBACK_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts); }
if (baseUrls.empty()) {
qDebug() << "empty storage endpoint list";
return {};
} }
std::random_device randomDevice; std::random_device randomDevice;
std::mt19937 generator(randomDevice()); std::mt19937 generator(randomDevice());
std::shuffle(primaryBaseUrls.begin(), primaryBaseUrls.end(), generator); std::shuffle(baseUrls.begin(), baseUrls.end(), generator);
std::shuffle(fallbackBaseUrls.begin(), fallbackBaseUrls.end(), generator);
auto appendStorageUrls = [&serviceType, &userCountryCode](const QStringList &baseUrls, QStringList &target) { QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
QStringList proxyStorageUrls;
if (!serviceType.isEmpty()) { if (!serviceType.isEmpty()) {
for (const auto &baseUrl : baseUrls) { for (const auto &baseUrl : baseUrls) {
QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8(); QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8();
target.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) + ".json"); proxyStorageUrls.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) + ".json");
} }
} }
for (const auto &baseUrl : baseUrls) { for (const auto &baseUrl : baseUrls) {
target.push_back(baseUrl + "endpoints.json"); proxyStorageUrls.push_back(baseUrl + "endpoints.json");
}
};
QStringList proxyStorageUrls;
appendStorageUrls(primaryBaseUrls, proxyStorageUrls);
appendStorageUrls(fallbackBaseUrls, proxyStorageUrls);
const QString proxyUrlsCacheKey = getProxyUrlsCacheKey(serviceType, userCountryCode);
const QByteArray cachedProxyUrlsEncrypted = m_settings->readGatewayProxyUrls(proxyUrlsCacheKey);
if (proxyStorageUrls.empty()) {
qDebug() << "empty storage endpoint list";
return readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment);
} }
for (const auto &proxyStorageUrl : proxyStorageUrls) { for (const auto &proxyStorageUrl : proxyStorageUrls) {
@@ -457,8 +375,26 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
auto encryptedResponseBody = reply->readAll(); auto encryptedResponseBody = reply->readAll();
reply->deleteLater(); reply->deleteLater();
EVP_PKEY *privateKey = nullptr;
QByteArray responseBody; QByteArray responseBody;
if (!decryptProxyUrlsPayload(encryptedResponseBody, m_isDevEnvironment, responseBody)) { try {
if (!m_isDevEnvironment) {
QCryptographicHash hash(QCryptographicHash::Sha512);
hash.addData(key);
QByteArray hashResult = hash.result().toHex();
QByteArray key = QByteArray::fromHex(hashResult.left(64));
QByteArray iv = QByteArray::fromHex(hashResult.mid(64, 32));
QByteArray ba = QByteArray::fromBase64(encryptedResponseBody);
QSimpleCrypto::QBlockCipher blockCipher;
responseBody = blockCipher.decryptAesBlockCipher(ba, key, iv);
} else {
responseBody = encryptedResponseBody;
}
} catch (...) {
Utils::logException();
qCritical() << "error loading private key from environment variables or decrypting payload" << encryptedResponseBody; qCritical() << "error loading private key from environment variables or decrypting payload" << encryptedResponseBody;
continue; continue;
} }
@@ -469,8 +405,6 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
for (const auto &endpoint : endpointsArray) { for (const auto &endpoint : endpointsArray) {
endpoints.push_back(endpoint.toString()); endpoints.push_back(endpoint.toString());
} }
m_settings->writeGatewayProxyUrls(proxyUrlsCacheKey, encryptedResponseBody);
return endpoints; return endpoints;
} else { } else {
auto replyError = reply->error(); auto replyError = reply->error();
@@ -482,7 +416,7 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
reply->deleteLater(); reply->deleteLater();
} }
} }
return readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment); return {};
} }
bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody, bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody,
@@ -620,17 +554,15 @@ void GatewayController::bypassProxy(const QString &endpoint, const QString &serv
} }
void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex, void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
const QString &proxyUrlsCacheKey, std::function<void(const QStringList &)> onComplete) std::function<void(const QStringList &)> onComplete)
{ {
const QByteArray cachedProxyUrlsEncrypted = m_settings->readGatewayProxyUrls(proxyUrlsCacheKey);
if (currentProxyStorageIndex >= proxyStorageUrls.size()) { if (currentProxyStorageIndex >= proxyStorageUrls.size()) {
onComplete(shuffledProxyUrls(readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment))); onComplete({});
return; return;
} }
QNetworkRequest request; QNetworkRequest request;
request.setTransferTimeout(proxyStorageRequestTimeoutMsecs); request.setTransferTimeout(m_requestTimeoutMsecs);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setUrl(proxyStorageUrls[currentProxyStorageIndex]); request.setUrl(proxyStorageUrls[currentProxyStorageIndex]);
@@ -638,17 +570,33 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co
// connect(reply, &QNetworkReply::sslErrors, this, [state](const QList<QSslError> &e) { *(state->sslErrors) = e; }); // connect(reply, &QNetworkReply::sslErrors, this, [state](const QList<QSslError> &e) { *(state->sslErrors) = e; });
connect(reply, &QNetworkReply::finished, this, connect(reply, &QNetworkReply::finished, this, [this, proxyStorageUrls, currentProxyStorageIndex, onComplete, reply]() {
[this, proxyStorageUrls, currentProxyStorageIndex, proxyUrlsCacheKey, onComplete, reply]() {
if (reply->error() == QNetworkReply::NoError) { if (reply->error() == QNetworkReply::NoError) {
QByteArray encrypted = reply->readAll(); QByteArray encrypted = reply->readAll();
reply->deleteLater(); reply->deleteLater();
QByteArray responseBody; QByteArray responseBody;
if (!decryptProxyUrlsPayload(encrypted, m_isDevEnvironment, responseBody)) { try {
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
if (!m_isDevEnvironment) {
QCryptographicHash hash(QCryptographicHash::Sha512);
hash.addData(key);
QByteArray h = hash.result().toHex();
QByteArray decKey = QByteArray::fromHex(h.left(64));
QByteArray iv = QByteArray::fromHex(h.mid(64, 32));
QByteArray ba = QByteArray::fromBase64(encrypted);
QSimpleCrypto::QBlockCipher cipher;
responseBody = cipher.decryptAesBlockCipher(ba, decKey, iv);
} else {
responseBody = encrypted;
}
} catch (...) {
Utils::logException();
qCritical() << "error decrypting payload"; qCritical() << "error decrypting payload";
QMetaObject::invokeMethod( QMetaObject::invokeMethod(
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, proxyUrlsCacheKey, onComplete); }, Qt::QueuedConnection); this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection);
return; return;
} }
@@ -656,9 +604,13 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co
QStringList endpoints; QStringList endpoints;
for (const QJsonValue &endpoint : endpointsArray) for (const QJsonValue &endpoint : endpointsArray)
endpoints.push_back(endpoint.toString()); endpoints.push_back(endpoint.toString());
m_settings->writeGatewayProxyUrls(proxyUrlsCacheKey, encrypted);
onComplete(shuffledProxyUrls(endpoints)); QStringList shuffled = endpoints;
std::random_device randomDevice;
std::mt19937 generator(randomDevice());
std::shuffle(shuffled.begin(), shuffled.end(), generator);
onComplete(shuffled);
return; return;
} }
@@ -667,7 +619,7 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co
qDebug() << "go to the next storage endpoint"; qDebug() << "go to the next storage endpoint";
reply->deleteLater(); reply->deleteLater();
QMetaObject::invokeMethod( QMetaObject::invokeMethod(
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, proxyUrlsCacheKey, onComplete); }, Qt::QueuedConnection); this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection);
}); });
} }
+2 -9
View File
@@ -7,9 +7,6 @@
#include <QPair> #include <QPair>
#include <QPromise> #include <QPromise>
#include <QSharedPointer> #include <QSharedPointer>
#include <QString>
#include <QStringList>
#include <memory>
#include "core/defs.h" #include "core/defs.h"
@@ -17,16 +14,13 @@
#include "platforms/ios/ios_controller.h" #include "platforms/ios/ios_controller.h"
#endif #endif
class Settings;
class GatewayController : public QObject class GatewayController : public QObject
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs, explicit GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
const bool isStrictKillSwitchEnabled, const std::shared_ptr<Settings> &settings, const bool isStrictKillSwitchEnabled, QObject *parent = nullptr);
QObject *parent = nullptr);
amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody); amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody);
QFuture<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject apiPayload); QFuture<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject apiPayload);
@@ -59,7 +53,7 @@ private:
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction); std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction);
void getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex, void getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
const QString &proxyUrlsCacheKey, std::function<void(const QStringList &)> onComplete); std::function<void(const QStringList &)> onComplete);
void getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex, std::function<void(const QString &)> onComplete); void getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex, std::function<void(const QString &)> onComplete);
void bypassProxyAsync( void bypassProxyAsync(
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData, const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
@@ -69,7 +63,6 @@ private:
QString m_gatewayEndpoint; QString m_gatewayEndpoint;
bool m_isDevEnvironment = false; bool m_isDevEnvironment = false;
bool m_isStrictKillSwitchEnabled = false; bool m_isStrictKillSwitchEnabled = false;
std::shared_ptr<Settings> m_settings;
inline static QString m_proxyUrl; inline static QString m_proxyUrl;
}; };
-1
View File
@@ -125,7 +125,6 @@ namespace amnezia
ApiPurchaseError = 1113, ApiPurchaseError = 1113,
ApiSubscriptionNotActiveError = 1114, ApiSubscriptionNotActiveError = 1114,
ApiNoPurchasedSubscriptionsError = 1115, ApiNoPurchasedSubscriptionsError = 1115,
ApiTrialAlreadyUsedError = 1116,
// QFile errors // QFile errors
OpenError = 1200, OpenError = 1200,
-1
View File
@@ -82,7 +82,6 @@ QString errorString(ErrorCode code) {
case (ErrorCode::ApiPurchaseError): errorMessage = QObject::tr("Unable to process purchase"); break; case (ErrorCode::ApiPurchaseError): errorMessage = QObject::tr("Unable to process purchase"); break;
case (ErrorCode::ApiSubscriptionNotActiveError): errorMessage = QObject::tr("No active subscription found"); break; case (ErrorCode::ApiSubscriptionNotActiveError): errorMessage = QObject::tr("No active subscription found"); break;
case (ErrorCode::ApiNoPurchasedSubscriptionsError): errorMessage = QObject::tr("No purchased subscriptions found. Please purchase a subscription first"); break; case (ErrorCode::ApiNoPurchasedSubscriptionsError): errorMessage = QObject::tr("No purchased subscriptions found. Please purchase a subscription first"); break;
case (ErrorCode::ApiTrialAlreadyUsedError): errorMessage = QObject::tr("This email address has already been used to activate a trial"); break;
// QFile errors // QFile errors
case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break; case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break;
+2 -107
View File
@@ -1,11 +1,6 @@
#include <QString> #include <QString>
#include <QJsonArray>
#include <QJsonObject> #include <QJsonObject>
#include <QList> #include <QList>
#include <QHostAddress>
#include <QRandomGenerator>
#include <QTcpServer>
#include <stdexcept>
#include "3rd/QJsonStruct/QJsonIO.hpp" #include "3rd/QJsonStruct/QJsonIO.hpp"
#include "transfer.h" #include "transfer.h"
#include "serialization.h" #include "serialization.h"
@@ -19,125 +14,25 @@ namespace amnezia::serialization::inbounds
// "port": 10808, // "port": 10808,
// "protocol": "socks", // "protocol": "socks",
// "settings": { // "settings": {
// "auth": "password",
// "accounts": [{"user": "...", "pass": "..."}],
// "udp": true // "udp": true
// } // }
// } // }
//], //],
const static QString listen = "127.0.0.1"; const static QString listen = "127.0.0.1";
const static int defaultPort = 10808; const static int port = 10808;
const static QString protocol = "socks"; const static QString protocol = "socks";
static int indexOfSocksInbound(const QJsonArray &inbounds)
{
for (int i = 0; i < inbounds.size(); ++i) {
const QString p = inbounds.at(i).toObject().value(QLatin1String("protocol")).toString();
if (p.compare(QLatin1String("socks"), Qt::CaseInsensitive) == 0)
return i;
}
return -1;
}
// Ask the OS for a free TCP port on loopback (same stack as inbound "listen": "127.0.0.1").
static int acquireFreeLocalPort()
{
QTcpServer probe;
if (!probe.listen(QHostAddress(QStringLiteral("127.0.0.1")), 0)) {
throw std::runtime_error(
"Failed to bind a local TCP port on 127.0.0.1 for SOCKS inbound "
"(QTcpServer::listen failed; possible permission or OS network error).");
}
return static_cast<int>(probe.serverPort());
}
// Generates a hex string of `byteCount` random bytes (URL-safe, no special chars).
static QString generateRandomHex(int byteCount)
{
if (byteCount <= 0)
return {};
// fillRange writes full quint32 words; size the buffer to a multiple of 4 bytes to avoid
// overrunning a short buffer when byteCount is not divisible by 4.
const int numUint32 = (byteCount + int(sizeof(quint32)) - 1) / int(sizeof(quint32));
QByteArray buf(numUint32 * int(sizeof(quint32)), '\0');
QRandomGenerator::system()->fillRange(reinterpret_cast<quint32 *>(buf.data()), numUint32);
return QString::fromLatin1(buf.left(byteCount).toHex());
}
QJsonObject GenerateInboundEntry() QJsonObject GenerateInboundEntry()
{ {
QJsonObject root; QJsonObject root;
QJsonIO::SetValue(root, listen, "listen"); QJsonIO::SetValue(root, listen, "listen");
QJsonIO::SetValue(root, defaultPort, "port"); QJsonIO::SetValue(root, port, "port");
QJsonIO::SetValue(root, protocol, "protocol"); QJsonIO::SetValue(root, protocol, "protocol");
QJsonIO::SetValue(root, true, "settings", "udp"); QJsonIO::SetValue(root, true, "settings", "udp");
return root; return root;
} }
InboundCredentials GetInboundCredentials(const QJsonObject &xrayConfig)
{
InboundCredentials creds;
creds.port = defaultPort;
const QJsonArray inbounds = xrayConfig.value("inbounds").toArray();
const int socksIdx = indexOfSocksInbound(inbounds);
if (socksIdx < 0)
return creds;
const QJsonObject inbound = inbounds.at(socksIdx).toObject();
creds.port = inbound.value("port").toInt(defaultPort);
const QJsonObject settings = inbound.value("settings").toObject();
const QJsonArray accounts = settings.value("accounts").toArray();
if (accounts.isEmpty())
return creds;
const QJsonObject account = accounts.first().toObject();
creds.username = account.value("user").toString();
creds.password = account.value("pass").toString();
return creds;
}
InboundCredentials EnsureInboundAuth(QJsonObject &xrayConfig)
{
QJsonArray inbounds = xrayConfig.value("inbounds").toArray();
const int socksIdx = indexOfSocksInbound(inbounds);
if (socksIdx < 0)
return GetInboundCredentials(xrayConfig); // no SOCKS inbound to patch
QJsonObject inbound = inbounds.at(socksIdx).toObject();
InboundCredentials creds;
creds.port = acquireFreeLocalPort();
inbound["port"] = creds.port;
QJsonObject settings = inbound.value("settings").toObject();
const QJsonArray accounts = settings.value("accounts").toArray();
if (!accounts.isEmpty()) {
const QJsonObject account = accounts.first().toObject();
creds.username = account.value("user").toString();
creds.password = account.value("pass").toString();
}
if (creds.username.isEmpty() || creds.password.isEmpty()) {
// Generate fresh credentials for this session (never persisted)
creds.username = generateRandomHex(8); // 16 hex chars
creds.password = generateRandomHex(16); // 32 hex chars
QJsonObject account;
account["user"] = creds.username;
account["pass"] = creds.password;
settings["accounts"] = QJsonArray{ account };
}
// Always ensure auth mode is enforced, even for imported configs that had
// accounts but auth: "noauth" (or no auth field at all).
settings["auth"] = QStringLiteral("password");
inbound["settings"] = settings;
inbounds[socksIdx] = inbound;
xrayConfig["inbounds"] = inbounds;
return creds;
}
} // namespace amnezia::serialization::inbounds } // namespace amnezia::serialization::inbounds
-17
View File
@@ -60,24 +60,7 @@ namespace amnezia::serialization
namespace inbounds namespace inbounds
{ {
struct InboundCredentials {
QString username;
QString password;
int port;
};
QJsonObject GenerateInboundEntry(); QJsonObject GenerateInboundEntry();
// Reads existing SOCKS5 auth from the first inbound with protocol "socks"
// (.settings.accounts[0]). Returns empty username/password if none.
InboundCredentials GetInboundCredentials(const QJsonObject &xrayConfig);
// Ensures SOCKS5 auth is present on the inbound whose protocol is "socks".
// Re-uses existing credentials if already set; otherwise generates random ones
// and writes them into the config. Assigns a free loopback TCP port each session
// (OS-assigned). Throws std::runtime_error if a SOCKS inbound exists but binding
// a local port on 127.0.0.1 fails (e.g. permissions or OS error).
InboundCredentials EnsureInboundAuth(QJsonObject &xrayConfig);
} }
} }
+1 -1
View File
@@ -16,7 +16,7 @@ set_target_properties(AmneziaVPNNetworkExtension PROPERTIES
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_NAME "${BUILD_IOS_APP_IDENTIFIER}.network-extension" XCODE_ATTRIBUTE_PRODUCT_BUNDLE_NAME "${BUILD_IOS_APP_IDENTIFIER}.network-extension"
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS ${CMAKE_CURRENT_SOURCE_DIR}/AmneziaVPNNetworkExtension.entitlements XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS ${CMAKE_CURRENT_SOURCE_DIR}/AmneziaVPNNetworkExtension.entitlements
XCODE_ATTRIBUTE_MARKETING_VERSION "${APP_MAJOR_VERSION}" XCODE_ATTRIBUTE_MARKETING_VERSION "${APP_MAJOR_VERSION}"
XCODE_ATTRIBUTE_CURRENT_PROJECT_VERSION "${CMAKE_PROJECT_VERSION_TWEAK}" XCODE_ATTRIBUTE_CURRENT_PROJECT_VERSION "${BUILD_ID}"
XCODE_ATTRIBUTE_PRODUCT_NAME "AmneziaVPNNetworkExtension" XCODE_ATTRIBUTE_PRODUCT_NAME "AmneziaVPNNetworkExtension"
XCODE_ATTRIBUTE_APPLICATION_EXTENSION_API_ONLY "YES" XCODE_ATTRIBUTE_APPLICATION_EXTENSION_API_ONLY "YES"
+4 -4
View File
@@ -8,7 +8,7 @@
<string>AmneziaVPNNetworkExtension</string> <string>AmneziaVPNNetworkExtension</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> <string>org.amnezia.AmneziaVPN.network-extension</string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
@@ -16,9 +16,9 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string> <string>${APPLE_PROJECT_VERSION}</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string> <string>${CMAKE_PROJECT_VERSION_TWEAK}</string>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>
<false/> <false/>
@@ -41,6 +41,6 @@
<string>group.org.amnezia.AmneziaVPN</string> <string>group.org.amnezia.AmneziaVPN</string>
<key>com.wireguard.macos.app_group_id</key> <key>com.wireguard.macos.app_group_id</key>
<string>$(DEVELOPMENT_TEAM).group.org.amnezia.AmneziaVPN</string> <string>${BUILD_VPN_DEVELOPMENT_TEAM}.group.org.amnezia.AmneziaVPN</string>
</dict> </dict>
</plist> </plist>
@@ -307,6 +307,16 @@ void AndroidController::requestNotificationPermission()
callActivityMethod("requestNotificationPermission", "()V"); callActivityMethod("requestNotificationPermission", "()V");
} }
void AndroidController::showVpnStateNotification(const QString &title, const QString &message)
{
if (!isNotificationPermissionGranted()) {
return;
}
callActivityMethod("showVpnStateNotification", "(Ljava/lang/String;Ljava/lang/String;)V",
QJniObject::fromString(title).object<jstring>(),
QJniObject::fromString(message).object<jstring>());
}
bool AndroidController::requestAuthentication() bool AndroidController::requestAuthentication()
{ {
QEventLoop wait; QEventLoop wait;
@@ -53,6 +53,7 @@ public:
QPixmap getAppIcon(const QString &package, QSize *size, const QSize &requestedSize); QPixmap getAppIcon(const QString &package, QSize *size, const QSize &requestedSize);
bool isNotificationPermissionGranted(); bool isNotificationPermissionGranted();
void requestNotificationPermission(); void requestNotificationPermission();
void showVpnStateNotification(const QString &title, const QString &message);
bool requestAuthentication(); bool requestAuthentication();
void sendTouch(float x, float y); void sendTouch(float x, float y);
@@ -0,0 +1,19 @@
#include "android_notificationhandler.h"
#include "android_controller.h"
AndroidNotificationHandler::AndroidNotificationHandler(QObject *parent)
: NotificationHandler(parent)
{
}
void AndroidNotificationHandler::notify(Message type, const QString &title, const QString &message, int timerMsec)
{
Q_UNUSED(type);
Q_UNUSED(timerMsec);
// Permission is checked on the Kotlin side as well; avoid JNI if already denied.
if (!AndroidController::instance()->isNotificationPermissionGranted()) {
return;
}
AndroidController::instance()->showVpnStateNotification(title, message);
}
@@ -0,0 +1,16 @@
#ifndef ANDROID_NOTIFICATIONHANDLER_H
#define ANDROID_NOTIFICATIONHANDLER_H
#include "ui/notificationhandler.h"
class AndroidNotificationHandler final : public NotificationHandler {
Q_OBJECT
public:
explicit AndroidNotificationHandler(QObject *parent = nullptr);
protected:
void notify(Message type, const QString &title, const QString &message, int timerMsec) override;
};
#endif // ANDROID_NOTIFICATIONHANDLER_H
@@ -15,12 +15,6 @@ struct OpenVPNConfig: Decodable {
extension PacketTunnelProvider { extension PacketTunnelProvider {
func startOpenVPN(completionHandler: @escaping (Error?) -> Void) { func startOpenVPN(completionHandler: @escaping (Error?) -> Void) {
// Reset session-derived state so reconnects never reuse stale gateway/address data.
openVpnGatewayAddress = nil
openVpnLocalAddress = nil
openVpnLocalMask = nil
lastOpenVPNSettings = nil
guard let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol, guard let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol,
let providerConfiguration = protocolConfiguration.providerConfiguration, let providerConfiguration = protocolConfiguration.providerConfiguration,
let openVPNConfigData = providerConfiguration[Constants.ovpnConfigKey] as? Data else { let openVPNConfigData = providerConfiguration[Constants.ovpnConfigKey] as? Data else {
@@ -31,25 +25,7 @@ extension PacketTunnelProvider {
do { do {
let openVPNConfig = try JSONDecoder().decode(OpenVPNConfig.self, from: openVPNConfigData) let openVPNConfig = try JSONDecoder().decode(OpenVPNConfig.self, from: openVPNConfigData)
ovpnLog(.info, title: "config: ", message: openVPNConfig.str) ovpnLog(.info, title: "config: ", message: openVPNConfig.str)
let wrapperPreview = String(decoding: openVPNConfigData.prefix(512), as: UTF8.self)
let ovpnPreview = String(openVPNConfig.config.prefix(512))
ovpnLog(.info, title: "config wrapper", message: "bytes=\(openVPNConfigData.count) preview=\(wrapperPreview)")
ovpnLog(.info, title: "config raw", message: "chars=\(openVPNConfig.config.count) preview=\(ovpnPreview)")
let ovpnConfiguration = Data(openVPNConfig.config.utf8) let ovpnConfiguration = Data(openVPNConfig.config.utf8)
splitTunnelType = openVPNConfig.splitTunnelType
splitTunnelSites = openVPNConfig.splitTunnelSites
openVpnDnsServers = Self.extractDnsServers(from: openVPNConfig.config)
openVpnRemoteAddress = Self.extractRemoteHost(from: openVPNConfig.config)
openVpnRedirectGatewayDef1 = Self.hasRedirectGatewayDef1(in: openVPNConfig.config)
if let openVpnRemoteAddress {
ovpnLog(.info, title: "Remote", message: "host=\(openVpnRemoteAddress)")
}
if !openVpnDnsServers.isEmpty {
ovpnLog(.info, title: "DNS", message: "servers=\(openVpnDnsServers)")
}
if openVpnRedirectGatewayDef1 {
ovpnLog(.info, title: "IPv4Routes", message: "redirect-gateway def1 detected")
}
setupAndlaunchOpenVPN(withConfig: ovpnConfiguration, completionHandler: completionHandler) setupAndlaunchOpenVPN(withConfig: ovpnConfiguration, completionHandler: completionHandler)
} catch { } catch {
ovpnLog(.error, message: "Can't parse OpenVPN config: \(error.localizedDescription)") ovpnLog(.error, message: "Can't parse OpenVPN config: \(error.localizedDescription)")
@@ -97,11 +73,6 @@ extension PacketTunnelProvider {
let digestString = digest.map { String(format: "%02x", $0) }.joined() let digestString = digest.map { String(format: "%02x", $0) }.joined()
ovpnLog(.info, title: "ConfigDigest", message: digestString) ovpnLog(.info, title: "ConfigDigest", message: digestString)
let hasCertTag = configString.contains("<cert>") && configString.contains("</cert>")
let hasKeyTag = configString.contains("<key>") && configString.contains("</key>")
let hasAuthUserPass = configString.contains("auth-user-pass")
ovpnLog(.info, title: "ConfigCreds", message: "inlineCert=\(hasCertTag) inlineKey=\(hasKeyTag) authUserPass=\(hasAuthUserPass)")
let hasTlsAuthOpen = configString.contains("<tls-auth>") let hasTlsAuthOpen = configString.contains("<tls-auth>")
let hasTlsAuthClose = configString.contains("</tls-auth>") let hasTlsAuthClose = configString.contains("</tls-auth>")
ovpnLog(.info, title: "ConfigFlags", message: "tls-auth open=\(hasTlsAuthOpen) close=\(hasTlsAuthClose)") ovpnLog(.info, title: "ConfigFlags", message: "tls-auth open=\(hasTlsAuthOpen) close=\(hasTlsAuthClose)")
@@ -112,98 +83,27 @@ extension PacketTunnelProvider {
ovpnLog(.debug, title: "ConfigHead", message: head) ovpnLog(.debug, title: "ConfigHead", message: head)
ovpnLog(.debug, title: "ConfigTail", message: tail) ovpnLog(.debug, title: "ConfigTail", message: tail)
if hasTlsAuthOpen && hasTlsAuthClose { if let start = configString.range(of: "<tls-auth>"),
ovpnLog(.info, title: "TLSAuthSanitized", message: "preserve original tls-auth block") 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)
} }
var normalizedConfig = configString.replacingOccurrences(of: "\r\n", with: "\n") let normalizedConfig = configString.replacingOccurrences(of: "\r\n", with: "\n")
normalizedConfig = Self.normalizeInlineBlock(
in: normalizedConfig,
tag: "ca",
beginMarkers: ["-----BEGIN CERTIFICATE-----"],
endMarkers: ["-----END CERTIFICATE-----"]
)
normalizedConfig = Self.normalizeInlineBlock(
in: normalizedConfig,
tag: "cert",
beginMarkers: ["-----BEGIN CERTIFICATE-----"],
endMarkers: ["-----END CERTIFICATE-----"]
)
normalizedConfig = Self.normalizeInlineBlock(
in: normalizedConfig,
tag: "key",
beginMarkers: [
"-----BEGIN PRIVATE KEY-----",
"-----BEGIN RSA PRIVATE KEY-----",
"-----BEGIN EC PRIVATE KEY-----",
"-----BEGIN ENCRYPTED PRIVATE KEY-----"
],
endMarkers: [
"-----END PRIVATE KEY-----",
"-----END RSA PRIVATE KEY-----",
"-----END EC PRIVATE KEY-----",
"-----END ENCRYPTED PRIVATE KEY-----"
]
)
normalizedConfig = Self.normalizeInlineBlock(
in: normalizedConfig,
tag: "tls-auth",
beginMarkers: ["-----BEGIN OpenVPN Static key V1-----"],
endMarkers: ["-----END OpenVPN Static key V1-----"]
)
normalizedConfig = Self.stripUnsupportedOptions(forOpenVPNAdapter: normalizedConfig)
if !normalizedConfig.hasSuffix("\n") {
normalizedConfig.append("\n")
}
let normalizedLines = normalizedConfig.split(whereSeparator: \.isNewline)
let normalizedTail = normalizedLines.suffix(10).joined(separator: "\n")
ovpnLog(.debug, title: "ConfigTailSanitized", message: normalizedTail)
let redirectLines = normalizedLines
.map(String.init)
.filter { $0.lowercased().contains("redirect-gateway") }
if !redirectLines.isEmpty {
ovpnLog(.info, title: "ConfigRedirect", message: redirectLines.joined(separator: " | "))
}
let controlScalars = normalizedConfig.unicodeScalars.filter {
($0.value < 0x20 && $0 != "\n" && $0 != "\r" && $0 != "\t")
}
if !controlScalars.isEmpty {
ovpnLog(.error, title: "ConfigChars", message: "nonPrintableControlCount=\(controlScalars.count)")
}
#if os(macOS)
let dumpBaseURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first
?? FileManager.default.temporaryDirectory
let dumpURL = dumpBaseURL.appendingPathComponent("amnezia_ovpn_adapter_config.conf")
do {
try normalizedConfig.write(to: dumpURL, atomically: true, encoding: .utf8)
ovpnLog(.info, title: "ConfigDump", message: "path=\(dumpURL.path) bytes=\(normalizedConfig.utf8.count)")
} catch {
ovpnLog(.error, title: "ConfigDump", message: "write failed: \(error.localizedDescription)")
}
#endif
let sanitizedData = Data(normalizedConfig.utf8) let sanitizedData = Data(normalizedConfig.utf8)
let configuration = OpenVPNConfiguration() let configuration = OpenVPNConfiguration()
configuration.fileContent = sanitizedData configuration.fileContent = sanitizedData
// Be explicit: enum default is 0 (enabled), we need stubs-only behavior.
configuration.compressionMode = .disabled
// A-012: emulate OpenVPN2 CLI capability advertisement as closely as possible.
configuration.peerInfo = [
"IV_VER": "2.6.10",
"IV_PLAT": "mac",
"IV_TCPNL": "1",
"IV_MTU": "1600",
"IV_NCP": "2",
"IV_CIPHERS": "AES-256-GCM:AES-128-GCM:CHACHA20-POLY1305",
"IV_PROTO": "990",
"IV_LZO_STUB": "1",
"IV_COMP_STUB": "1",
"IV_COMP_STUBv2": "1"
]
if let peerInfo = configuration.peerInfo {
let peerInfoSummary = peerInfo.keys.sorted().map { "\($0)=\(peerInfo[$0] ?? "")" }.joined(separator: " ")
ovpnLog(.info, title: "PeerInfoOverride", message: peerInfoSummary)
}
if configString.contains("cloak") { if configString.contains("cloak") {
configuration.setPTCloak() configuration.setPTCloak()
} }
@@ -224,15 +124,10 @@ extension PacketTunnelProvider {
if evaluation?.autologin == false { if evaluation?.autologin == false {
ovpnLog(.info, message: "Implement login with user credentials") ovpnLog(.info, message: "Implement login with user credentials")
} }
if let evaluation {
ovpnLog(.info, title: "ConfigEval", message: "autologin=\(evaluation.autologin) externalPki=\(evaluation.externalPki)")
}
#if !os(macOS)
vpnReachability.startTracking { [weak self] status in vpnReachability.startTracking { [weak self] status in
self?.handleOpenVPNReachabilityChange(status) self?.handleOpenVPNReachabilityChange(status)
} }
#endif
startHandler = completionHandler startHandler = completionHandler
ovpnAdapter?.connect(using: openVPNPacketFlow()) ovpnAdapter?.connect(using: openVPNPacketFlow())
@@ -248,8 +143,6 @@ extension PacketTunnelProvider {
return return
} }
ovpnLog(.info, title: "Transport", message: "bytesIn=\(bytesin) bytesOut=\(bytesout)")
let response: [String: Any] = [ let response: [String: Any] = [
"rx_bytes": bytesin, "rx_bytes": bytesin,
"tx_bytes": bytesout "tx_bytes": bytesout
@@ -262,10 +155,6 @@ extension PacketTunnelProvider {
ovpnLog(.info, message: "Stopping tunnel: reason: \(reason.amneziaDescription)") ovpnLog(.info, message: "Stopping tunnel: reason: \(reason.amneziaDescription)")
stopHandler = completionHandler stopHandler = completionHandler
openVpnGatewayAddress = nil
openVpnLocalAddress = nil
openVpnLocalMask = nil
lastOpenVPNSettings = nil
if vpnReachability.isTracking { if vpnReachability.isTracking {
vpnReachability.stopTracking() vpnReachability.stopTracking()
} }
@@ -285,99 +174,11 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings?, configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings?,
completionHandler: @escaping (Error?) -> Void completionHandler: @escaping (Error?) -> Void
) { ) {
guard var effectiveSettings = networkSettings else {
ovpnLog(.info, title: "SetTunnelNetworkSettings", message: "nil settings; skipping update")
completionHandler(nil)
return
}
let splitType = splitTunnelType ?? 0
if let ipv4Settings = effectiveSettings.ipv4Settings {
openVpnLocalAddress = ipv4Settings.addresses.first
openVpnLocalMask = ipv4Settings.subnetMasks.first
}
let serverIP = openVPNAdapter.connectionInformation?.serverIP
let configRemote = openVpnRemoteAddress
let serverEndpoint: String? = {
if let ip = serverIP, Self.isIPv4Address(ip) { return ip }
if let ip = configRemote, Self.isIPv4Address(ip) { return ip }
return effectiveSettings.tunnelRemoteAddress
}()
if let serverEndpoint,
Self.isIPv4Address(serverEndpoint),
effectiveSettings.tunnelRemoteAddress != serverEndpoint {
let updatedSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: serverEndpoint)
updatedSettings.ipv4Settings = effectiveSettings.ipv4Settings
updatedSettings.ipv6Settings = effectiveSettings.ipv6Settings
updatedSettings.dnsSettings = effectiveSettings.dnsSettings
updatedSettings.proxySettings = effectiveSettings.proxySettings
updatedSettings.mtu = effectiveSettings.mtu
effectiveSettings = updatedSettings
ovpnLog(.info, title: "Remote", message: "tunnelRemoteAddress set to server=\(serverEndpoint)")
} else if let serverEndpoint, !Self.isIPv4Address(serverEndpoint) {
ovpnLog(.info, title: "Remote", message: "skip tunnelRemoteAddress override; non-ip serverEndpoint=\(serverEndpoint)")
}
// In order to direct all DNS queries first to the VPN DNS servers before the primary DNS servers // In order to direct all DNS queries first to the VPN DNS servers before the primary DNS servers
// send empty string to NEDNSSettings.matchDomains // send empty string to NEDNSSettings.matchDomains
if let dnsSettings = effectiveSettings.dnsSettings { networkSettings?.dnsSettings?.matchDomains = [""]
if dnsSettings.servers.isEmpty, !openVpnDnsServers.isEmpty {
let newSettings = NEDNSSettings(servers: openVpnDnsServers)
newSettings.matchDomains = dnsSettings.matchDomains
effectiveSettings.dnsSettings = newSettings
}
} else if !openVpnDnsServers.isEmpty {
let newSettings = NEDNSSettings(servers: openVpnDnsServers)
effectiveSettings.dnsSettings = newSettings
}
effectiveSettings.dnsSettings?.matchDomains = [""] if splitTunnelType == 1 {
if let dnsSettings = effectiveSettings.dnsSettings {
let servers = dnsSettings.servers.joined(separator: ",")
let domains = dnsSettings.matchDomains?.joined(separator: ",") ?? ""
ovpnLog(.info, title: "DNS", message: "servers=[\(servers)] matchDomains=[\(domains)]")
} else {
ovpnLog(.error, title: "DNS", message: "dnsSettings is nil")
}
let tunnelRemote = effectiveSettings.tunnelRemoteAddress
if !tunnelRemote.isEmpty {
ovpnLog(.info, title: "Remote", message: "tunnelRemoteAddress=\(tunnelRemote)")
} else if let remoteAddress = openVpnRemoteAddress {
ovpnLog(.info, title: "Remote", message: "tunnelRemoteAddress is empty, configRemote=\(remoteAddress)")
}
if let ipv4Settings = effectiveSettings.ipv4Settings {
let included = (ipv4Settings.includedRoutes ?? []).map { "\($0.destinationAddress)/\($0.destinationSubnetMask)" }
let excluded = (ipv4Settings.excludedRoutes ?? []).map { "\($0.destinationAddress)/\($0.destinationSubnetMask)" }
let addresses = ipv4Settings.addresses.joined(separator: ",")
let masks = ipv4Settings.subnetMasks.joined(separator: ",")
let router: String
#if os(macOS)
if #available(macOS 13.0, *) {
router = ipv4Settings.router ?? ""
} else {
router = ""
}
#else
router = ""
#endif
ovpnLog(.info, title: "IPv4RoutesPre", message: "addresses=[\(addresses)] masks=[\(masks)] router=\(router) included=\(included) excluded=\(excluded)")
} else {
ovpnLog(.error, title: "IPv4RoutesPre", message: "ipv4Settings is nil")
}
if let ipv6Settings = effectiveSettings.ipv6Settings {
let included = (ipv6Settings.includedRoutes ?? []).map { "\($0.destinationAddress)/\($0.destinationNetworkPrefixLength)" }
let excluded = (ipv6Settings.excludedRoutes ?? []).map { "\($0.destinationAddress)/\($0.destinationNetworkPrefixLength)" }
let addresses = ipv6Settings.addresses.joined(separator: ",")
let prefixes = ipv6Settings.networkPrefixLengths.map { "\($0)" }.joined(separator: ",")
ovpnLog(.info, title: "IPv6RoutesPre", message: "addresses=[\(addresses)] prefixes=[\(prefixes)] included=\(included) excluded=\(excluded)")
}
if splitType == 1 {
var ipv4IncludedRoutes = [NEIPv4Route]() var ipv4IncludedRoutes = [NEIPv4Route]()
guard let splitTunnelSites else { guard let splitTunnelSites else {
@@ -393,8 +194,9 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
} }
} }
effectiveSettings.ipv4Settings?.includedRoutes = ipv4IncludedRoutes networkSettings?.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
} else if splitType == 2 { } else {
if splitTunnelType == 2 {
var ipv4ExcludedRoutes = [NEIPv4Route]() var ipv4ExcludedRoutes = [NEIPv4Route]()
var ipv4IncludedRoutes = [NEIPv4Route]() var ipv4IncludedRoutes = [NEIPv4Route]()
var ipv6IncludedRoutes = [NEIPv6Route]() var ipv6IncludedRoutes = [NEIPv6Route]()
@@ -422,418 +224,14 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
destinationAddress: "\(allIPv6.address)", destinationAddress: "\(allIPv6.address)",
networkPrefixLength: NSNumber(value: allIPv6.networkPrefixLength))) networkPrefixLength: NSNumber(value: allIPv6.networkPrefixLength)))
} }
effectiveSettings.ipv4Settings?.includedRoutes = ipv4IncludedRoutes networkSettings?.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
effectiveSettings.ipv6Settings?.includedRoutes = ipv6IncludedRoutes networkSettings?.ipv6Settings?.includedRoutes = ipv6IncludedRoutes
effectiveSettings.ipv4Settings?.excludedRoutes = ipv4ExcludedRoutes networkSettings?.ipv4Settings?.excludedRoutes = ipv4ExcludedRoutes
} else {
// Full tunnel: rely on adapter-provided routes.
}
if let serverEndpoint,
Self.isIPv4Address(serverEndpoint),
let ipv4Settings = effectiveSettings.ipv4Settings {
let hostMask = "255.255.255.255"
var excluded = ipv4Settings.excludedRoutes ?? []
let alreadyExcluded = excluded.contains {
$0.destinationAddress == serverEndpoint && $0.destinationSubnetMask == hostMask
}
if !alreadyExcluded {
excluded.append(NEIPv4Route(destinationAddress: serverEndpoint, subnetMask: hostMask))
ipv4Settings.excludedRoutes = excluded
ovpnLog(.info, title: "IPv4Routes", message: "excluded remoteAddress=\(serverEndpoint)")
}
} else if let serverEndpoint {
ovpnLog(.info, title: "IPv4Routes", message: "skip explicit remote exclude; non-ip server=\(serverEndpoint)")
}
let localAddr = openVpnLocalAddress
var net30Gateway: String?
if let localAddr, let mask = openVpnLocalMask {
net30Gateway = Self.net30Peer(for: localAddr, mask: mask)
}
var gateway = net30Gateway
if let adapterGateway = openVPNAdapter.connectionInformation?.gatewayIPv4, !adapterGateway.isEmpty {
if let localAddr, adapterGateway == localAddr {
ovpnLog(.info, title: "IPv4Gateway", message: "ignore adapter gateway equal to local address=\(adapterGateway)")
} else if let net30Gateway, net30Gateway != adapterGateway {
ovpnLog(.info, title: "IPv4Gateway", message: "ignore mismatched adapter gateway=\(adapterGateway), using net30 peer=\(net30Gateway)")
} else {
gateway = adapterGateway
}
}
openVpnGatewayAddress = gateway
if let gateway, !gateway.isEmpty {
ovpnLog(.info, title: "IPv4Gateway", message: "gateway=\(gateway)")
}
#if os(macOS)
if splitType == 0, let gateway, !gateway.isEmpty, effectiveSettings.tunnelRemoteAddress != gateway {
let updatedSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: gateway)
updatedSettings.ipv4Settings = effectiveSettings.ipv4Settings
updatedSettings.ipv6Settings = effectiveSettings.ipv6Settings
updatedSettings.dnsSettings = effectiveSettings.dnsSettings
updatedSettings.proxySettings = effectiveSettings.proxySettings
updatedSettings.mtu = effectiveSettings.mtu
effectiveSettings = updatedSettings
ovpnLog(.info, title: "Remote", message: "tunnelRemoteAddress set to gateway=\(gateway) on macOS full-tunnel")
}
#endif
#if os(macOS)
if var ipv4Settings = effectiveSettings.ipv4Settings {
if splitType == 0 {
let hasNet30Mask = ipv4Settings.subnetMasks.contains("255.255.255.252")
if hasNet30Mask {
let normalizedMasks = Array(repeating: "255.255.255.255",
count: ipv4Settings.subnetMasks.count)
let normalized = NEIPv4Settings(addresses: ipv4Settings.addresses,
subnetMasks: normalizedMasks)
normalized.includedRoutes = ipv4Settings.includedRoutes
normalized.excludedRoutes = ipv4Settings.excludedRoutes
if #available(macOS 13.0, *) {
normalized.router = ipv4Settings.router
}
ipv4Settings = normalized
ovpnLog(.info, title: "IPv4Routes", message: "normalized net30 /30 masks to /32 on macOS full-tunnel")
}
if let gateway, !gateway.isEmpty {
if #available(macOS 13.0, *) {
ipv4Settings.router = gateway
ovpnLog(.info, title: "IPv4Routes", message: "set ipv4 router=\(gateway) on macOS full-tunnel")
}
}
var included = ipv4Settings.includedRoutes ?? []
let hasDefault = included.contains {
$0.destinationAddress == "0.0.0.0" && $0.destinationSubnetMask == "0.0.0.0"
}
if hasDefault {
included.removeAll {
$0.destinationAddress == "0.0.0.0" && $0.destinationSubnetMask == "0.0.0.0"
}
}
let hasDef1Low = included.contains {
$0.destinationAddress == "0.0.0.0" && $0.destinationSubnetMask == "128.0.0.0"
}
let hasDef1High = included.contains {
$0.destinationAddress == "128.0.0.0" && $0.destinationSubnetMask == "128.0.0.0"
}
if (hasDefault || openVpnRedirectGatewayDef1) && !(hasDef1Low && hasDef1High) {
if !hasDef1Low {
let route = NEIPv4Route(destinationAddress: "0.0.0.0", subnetMask: "128.0.0.0")
if let gateway, !gateway.isEmpty {
route.gatewayAddress = gateway
}
included.append(route)
}
if !hasDef1High {
let route = NEIPv4Route(destinationAddress: "128.0.0.0", subnetMask: "128.0.0.0")
if let gateway, !gateway.isEmpty {
route.gatewayAddress = gateway
}
included.append(route)
}
ovpnLog(.info, title: "IPv4Routes", message: "ensured def1 routes (/1 + /1) on macOS full-tunnel")
}
if let gateway, !gateway.isEmpty {
included = included.map { route in
let isDef1 =
(route.destinationAddress == "0.0.0.0" && route.destinationSubnetMask == "128.0.0.0") ||
(route.destinationAddress == "128.0.0.0" && route.destinationSubnetMask == "128.0.0.0")
guard isDef1 else { return route }
if route.gatewayAddress == gateway {
return route
}
let updatedRoute = NEIPv4Route(destinationAddress: route.destinationAddress,
subnetMask: route.destinationSubnetMask)
updatedRoute.gatewayAddress = gateway
return updatedRoute
}
ovpnLog(.info, title: "IPv4Routes", message: "set gateway=\(gateway) on macOS def1 routes")
}
ipv4Settings.includedRoutes = included
effectiveSettings.ipv4Settings = ipv4Settings
}
}
#endif
if let ipv4Settings = effectiveSettings.ipv4Settings {
let included = (ipv4Settings.includedRoutes ?? []).map {
let gw = $0.gatewayAddress ?? ""
return "\($0.destinationAddress)/\($0.destinationSubnetMask) gw=\(gw)"
}
let excluded = (ipv4Settings.excludedRoutes ?? []).map { "\($0.destinationAddress)/\($0.destinationSubnetMask)" }
let addresses = ipv4Settings.addresses.joined(separator: ",")
let masks = ipv4Settings.subnetMasks.joined(separator: ",")
let router: String
#if os(macOS)
if #available(macOS 13.0, *) {
router = ipv4Settings.router ?? ""
} else {
router = ""
}
#else
router = ""
#endif
ovpnLog(.info, title: "IPv4Routes", message: "addresses=[\(addresses)] masks=[\(masks)] router=\(router) included=\(included) excluded=\(excluded)")
} else {
ovpnLog(.error, title: "IPv4Routes", message: "ipv4Settings is nil")
}
if let ipv6Settings = effectiveSettings.ipv6Settings {
let included = (ipv6Settings.includedRoutes ?? []).map { "\($0.destinationAddress)/\($0.destinationNetworkPrefixLength)" }
let excluded = (ipv6Settings.excludedRoutes ?? []).map { "\($0.destinationAddress)/\($0.destinationNetworkPrefixLength)" }
let addresses = ipv6Settings.addresses.joined(separator: ",")
let prefixes = ipv6Settings.networkPrefixLengths.map { "\($0)" }.joined(separator: ",")
ovpnLog(.info, title: "IPv6Routes", message: "addresses=[\(addresses)] prefixes=[\(prefixes)] included=\(included) excluded=\(excluded)")
}
#if os(macOS)
if effectiveSettings.ipv6Settings != nil {
effectiveSettings.ipv6Settings = nil
ovpnLog(.info, title: "IPv6", message: "cleared ipv6Settings on macOS")
}
#endif
lastOpenVPNSettings = effectiveSettings
// Set the network settings for the current tunneling session.
setTunnelNetworkSettings(effectiveSettings) { error in
if let error {
ovpnLog(.error, title: "SetTunnelNetworkSettings", message: error.localizedDescription)
} else {
ovpnLog(.info, title: "SetTunnelNetworkSettings", message: "ok")
}
completionHandler(error)
} }
} }
private static func extractDnsServers(from config: String) -> [String] { // Set the network settings for the current tunneling session.
let lines = config.split(whereSeparator: \.isNewline) setTunnelNetworkSettings(networkSettings, completionHandler: completionHandler)
var servers: [String] = []
for line in lines {
let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.hasPrefix("dhcp-option DNS ") {
let parts = trimmed.split(separator: " ")
if let last = parts.last {
servers.append(String(last))
}
}
}
return servers
}
private static func extractRemoteHost(from config: String) -> String? {
let lines = config.split(whereSeparator: \.isNewline)
for line in lines {
let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.hasPrefix("remote ") {
let parts = trimmed.split(separator: " ")
if parts.count >= 2 {
return String(parts[1])
}
}
}
return nil
}
private static func hasRedirectGatewayDef1(in config: String) -> Bool {
let lines = config.split(whereSeparator: \.isNewline)
for line in lines {
let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.hasPrefix("redirect-gateway") {
return trimmed.split(whereSeparator: { $0 == " " || $0 == "\t" }).contains("def1")
}
}
return false
}
private static func net30Peer(for address: String, mask: String) -> String? {
guard mask == "255.255.255.252" else { return nil }
let parts = address.split(separator: ".")
guard parts.count == 4 else { return nil }
var octets: [Int] = []
for part in parts {
guard let num = Int(part), num >= 0 && num <= 255 else { return nil }
octets.append(num)
}
let ip = (octets[0] << 24) | (octets[1] << 16) | (octets[2] << 8) | octets[3]
let network = ip & ~3
let host = ip - network
let peerHost: Int
switch host {
case 1: peerHost = 2
case 2: peerHost = 1
default: return nil
}
let peerIP = network + peerHost
return "\((peerIP >> 24) & 0xff).\((peerIP >> 16) & 0xff).\((peerIP >> 8) & 0xff).\(peerIP & 0xff)"
}
private func logOpenVPNConnectionInfo() {
guard let info = ovpnAdapter?.connectionInformation else { return }
let message = "vpnIPv4=\(info.vpnIPv4 ?? "") gatewayIPv4=\(info.gatewayIPv4 ?? "") serverIP=\(info.serverIP ?? "") tun=\(info.tunName ?? "")"
ovpnLog(.info, title: "ConnInfo", message: message)
}
private static func normalizeInlineBlock(
in config: String,
tag: String,
beginMarkers: [String],
endMarkers: [String]
) -> String {
guard !beginMarkers.isEmpty, !endMarkers.isEmpty else { return config }
var normalizedConfig = config
let openTag = "<\(tag)>"
let closeTag = "</\(tag)>"
var searchStart = normalizedConfig.startIndex
while let openRange = normalizedConfig.range(of: openTag, range: searchStart..<normalizedConfig.endIndex),
let closeRange = normalizedConfig.range(of: closeTag, range: openRange.upperBound..<normalizedConfig.endIndex) {
let rawBody = String(normalizedConfig[openRange.upperBound..<closeRange.lowerBound])
let lines = rawBody
.split(whereSeparator: \.isNewline)
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
var beginIndex: Int?
var endIndex: Int?
for (idx, line) in lines.enumerated() {
if beginIndex == nil,
beginMarkers.contains(where: { line.contains($0) }) {
beginIndex = idx
}
if beginIndex != nil,
endMarkers.contains(where: { line.contains($0) }) {
endIndex = idx
}
}
if let beginIndex,
let endIndex,
endIndex >= beginIndex {
let extracted = lines[beginIndex...endIndex].joined(separator: "\n")
let replacement = "<\(tag)>\n\(extracted)\n</\(tag)>"
normalizedConfig.replaceSubrange(openRange.lowerBound..<closeRange.upperBound, with: replacement)
ovpnLog(.info, title: "ConfigInline", message: "tag=<\(tag)> linesIn=\(lines.count) linesOut=\(endIndex - beginIndex + 1)")
searchStart = normalizedConfig.index(openRange.lowerBound, offsetBy: replacement.count)
} else {
ovpnLog(.error, title: "ConfigInline", message: "tag=<\(tag)> missing markers, keeping original body")
searchStart = closeRange.upperBound
}
}
return normalizedConfig
}
private static func stripUnsupportedOptions(forOpenVPNAdapter config: String) -> String {
let unsupportedTokens: Set<String> = [
"block-ipv6",
"script-security",
"up",
"down",
"resolv-retry",
"persist-key",
"persist-tun",
"compat-mode",
"disable-dco"
]
let inlineBlockTags: Set<String> = [
"ca",
"cert",
"key",
"pkcs12",
"tls-auth",
"tls-crypt",
"tls-crypt-v2",
"secret",
"crl-verify",
"extra-certs"
]
var removed: [String: Int] = [:]
var normalized: [String: Int] = [:]
var output: [String] = []
var activeInlineTag: String?
for rawLine in config.split(whereSeparator: \.isNewline) {
let line = String(rawLine)
let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty {
output.append(line)
continue
}
let trimmedLowercased = trimmed.lowercased()
if let currentInlineTag = activeInlineTag {
output.append(line)
if trimmedLowercased == "</\(currentInlineTag)>" {
activeInlineTag = nil
}
continue
}
if trimmedLowercased.hasPrefix("<"),
trimmedLowercased.hasSuffix(">"),
!trimmedLowercased.hasPrefix("</") {
let tagContent = String(trimmedLowercased.dropFirst().dropLast())
let tagName = tagContent
.split(whereSeparator: { $0 == " " || $0 == "\t" })
.first
.map(String.init) ?? ""
if inlineBlockTags.contains(tagName) {
activeInlineTag = tagName
output.append(line)
continue
}
}
if trimmed.hasPrefix("#") || trimmed.hasPrefix(";") {
output.append(line)
continue
}
let parts = trimmed.split(whereSeparator: { $0 == " " || $0 == "\t" })
let token = parts.first.map(String.init)?.lowercased() ?? ""
if trimmedLowercased.hasPrefix("redirect-gateway") || token.hasPrefix("redirect-gateway") {
let hasDef1 = parts.dropFirst().contains { String($0).lowercased().hasPrefix("def1") }
if hasDef1 {
output.append("redirect-gateway def1")
normalized["redirect-gateway", default: 0] += 1
} else {
removed["redirect-gateway", default: 0] += 1
}
continue
}
if let matchedUnsupported = unsupportedTokens.first(where: { token.hasPrefix($0) }) {
removed[matchedUnsupported, default: 0] += 1
continue
}
output.append(line)
}
if !removed.isEmpty {
let summary = removed.keys.sorted().map { "\($0)=\(removed[$0] ?? 0)" }.joined(separator: " ")
ovpnLog(.info, title: "ConfigStrip", message: summary)
}
if !normalized.isEmpty {
let summary = normalized.keys.sorted().map { "\($0)=\(normalized[$0] ?? 0)" }.joined(separator: " ")
ovpnLog(.info, title: "ConfigNormalize", message: summary)
}
return output.joined(separator: "\n")
}
private static func isIPv4Address(_ value: String) -> Bool {
let parts = value.split(separator: ".")
if parts.count != 4 { return false }
for part in parts {
guard let num = Int(part), num >= 0 && num <= 255 else { return false }
}
return true
} }
// Process events returned by the OpenVPN library // Process events returned by the OpenVPN library
@@ -851,9 +249,6 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
startHandler(nil) startHandler(nil)
self.startHandler = nil self.startHandler = nil
logOpenVPNConnectionInfo()
refreshOpenVPNSettingsAfterConnect()
case .disconnected: case .disconnected:
guard let stopHandler = stopHandler else { return } guard let stopHandler = stopHandler else { return }
@@ -896,41 +291,4 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
// Handle log messages // Handle log messages
ovpnLog(.info, message: logMessage) ovpnLog(.info, message: logMessage)
} }
func openVPNAdapterDidReceiveClockTick(_ openVPNAdapter: OpenVPNAdapter) {
let now = Date()
if now.timeIntervalSince(lastOpenVPNStatsLogTime) < 5 {
return
}
lastOpenVPNStatsLogTime = now
let transport = openVPNAdapter.transportStatistics
let iface = openVPNAdapter.interfaceStatistics
let transportLine = "transport bytesIn=\(transport.bytesIn) bytesOut=\(transport.bytesOut) packetsIn=\(transport.packetsIn) packetsOut=\(transport.packetsOut)"
let ifaceLine = "iface bytesIn=\(iface.bytesIn) bytesOut=\(iface.bytesOut) packetsIn=\(iface.packetsIn) packetsOut=\(iface.packetsOut) errorsIn=\(iface.errorsIn) errorsOut=\(iface.errorsOut)"
ovpnLog(.info, title: "Stats", message: "\(transportLine) | \(ifaceLine)")
}
private func refreshOpenVPNSettingsAfterConnect() {
let localAddr = openVpnLocalAddress
var net30Gateway: String?
if let localAddr, let mask = openVpnLocalMask {
net30Gateway = Self.net30Peer(for: localAddr, mask: mask)
}
var gateway = net30Gateway
if let adapterGateway = ovpnAdapter?.connectionInformation?.gatewayIPv4, !adapterGateway.isEmpty {
if let localAddr, adapterGateway == localAddr {
ovpnLog(.info, title: "IPv4Gateway", message: "post-connect ignoring adapter gateway equal to local address=\(adapterGateway)")
} else if let net30Gateway, net30Gateway != adapterGateway {
ovpnLog(.info, title: "IPv4Gateway", message: "post-connect keeping net30 peer=\(net30Gateway), adapter gateway=\(adapterGateway)")
} else {
gateway = adapterGateway
}
}
guard let gateway, !gateway.isEmpty else { return }
openVpnGatewayAddress = gateway
ovpnLog(.info, title: "IPv4Gateway", message: "post-connect gateway=\(gateway)")
}
} }
@@ -1,4 +1,3 @@
import Darwin
import Foundation import Foundation
import NetworkExtension import NetworkExtension
@@ -7,7 +6,6 @@ enum XrayErrors: Error {
case xrayConfigIsWrong case xrayConfigIsWrong
case cantSaveXrayConfig case cantSaveXrayConfig
case cantParseListenAndPort case cantParseListenAndPort
case cantAcquireLocalPort
case cantSaveHevSocksConfig case cantSaveHevSocksConfig
} }
@@ -23,42 +21,6 @@ extension Constants {
} }
extension PacketTunnelProvider { extension PacketTunnelProvider {
/// TCP port chosen by the OS on IPv6 loopback (::1), matching inbound listen address.
private func acquireFreeLocalPort() throws -> Int {
let fd = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP)
guard fd != -1 else {
throw XrayErrors.cantAcquireLocalPort
}
defer { close(fd) }
var reuse: Int32 = 1
_ = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, socklen_t(MemoryLayout<Int32>.size))
var addr = sockaddr_in6()
addr.sin6_len = UInt8(MemoryLayout<sockaddr_in6>.size)
addr.sin6_family = sa_family_t(AF_INET6)
addr.sin6_port = in_port_t(0).bigEndian
addr.sin6_addr = in6addr_loopback
addr.sin6_scope_id = 0
let bindResult = withUnsafePointer(to: &addr) { ptr in
ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { p in
bind(fd, p, socklen_t(MemoryLayout<sockaddr_in6>.size))
}
}
guard bindResult == 0 else {
throw XrayErrors.cantAcquireLocalPort
}
var bound = sockaddr_in6()
var len = socklen_t(MemoryLayout<sockaddr_in6>.size)
let gr = withUnsafeMutablePointer(to: &bound) { p in
p.withMemoryRebound(to: sockaddr.self, capacity: 1) { bp in
getsockname(fd, bp, &len)
}
}
guard gr == 0 else {
throw XrayErrors.cantAcquireLocalPort
}
return Int(bound.sin6_port.byteSwapped)
}
private func applyXraySplitTunnel(_ xrayConfig: XrayConfig, private func applyXraySplitTunnel(_ xrayConfig: XrayConfig,
settings: NEPacketTunnelNetworkSettings) { settings: NEPacketTunnelNetworkSettings) {
guard let splitTunnelType = xrayConfig.splitTunnelType else { guard let splitTunnelType = xrayConfig.splitTunnelType else {
@@ -167,11 +129,14 @@ extension PacketTunnelProvider {
return return
} }
let port = try acquireFreeLocalPort() let port = 10808
let address = "::1" let address = "::1"
// Extract existing SOCKS5 credentials or generate new ones per session. if var inboundsArray = jsonDict["inbounds"] as? [[String: Any]], !inboundsArray.isEmpty {
let socksCredentials = ensureInboundAuth(jsonDict: &jsonDict, port: port, address: address) inboundsArray[0]["port"] = port
inboundsArray[0]["listen"] = address
jsonDict["inbounds"] = inboundsArray
}
let updatedData = try JSONSerialization.data(withJSONObject: jsonDict, options: []) let updatedData = try JSONSerialization.data(withJSONObject: jsonDict, options: [])
@@ -194,8 +159,6 @@ extension PacketTunnelProvider {
self?.setupAndRunTun2socks(configData: updatedData, self?.setupAndRunTun2socks(configData: updatedData,
address: address, address: address,
port: port, port: port,
username: socksCredentials.username,
password: socksCredentials.password,
completionHandler: completionHandler) completionHandler: completionHandler)
} }
} }
@@ -220,62 +183,6 @@ extension PacketTunnelProvider {
} }
} }
private struct SocksCredentials {
let username: String
let password: String
}
private func indexOfSocksInbound(in inboundsArray: [[String: Any]]) -> Int? {
for (i, inbound) in inboundsArray.enumerated() {
guard let proto = inbound["protocol"] as? String else { continue }
if proto.caseInsensitiveCompare("socks") == .orderedSame {
return i
}
}
return nil
}
// Returns existing SOCKS5 credentials from the inbound config, or generates and injects
// new random ones. Also sets port and address on the socks inbound entry.
private func ensureInboundAuth(jsonDict: inout [String: Any], port: Int, address: String) -> SocksCredentials {
var inboundsArray = jsonDict["inbounds"] as? [[String: Any]] ?? []
if let socksIdx = indexOfSocksInbound(in: inboundsArray) {
var inbound = inboundsArray[socksIdx]
inbound["port"] = port
inbound["listen"] = address
var settings = inbound["settings"] as? [String: Any] ?? [:]
if let accounts = settings["accounts"] as? [[String: Any]],
let first = accounts.first,
let user = first["user"] as? String, !user.isEmpty,
let pass = first["pass"] as? String, !pass.isEmpty {
// Re-use existing credentials, but always enforce auth mode in case the
// imported config had accounts but auth: "noauth" (or no auth field).
settings["auth"] = "password"
inbound["settings"] = settings
inboundsArray[socksIdx] = inbound
jsonDict["inbounds"] = inboundsArray
return SocksCredentials(username: user, password: pass)
}
// Generate new random credentials for this session
let user = UUID().uuidString.replacingOccurrences(of: "-", with: "").lowercased().prefix(16)
let pass = UUID().uuidString.replacingOccurrences(of: "-", with: "").lowercased()
settings["auth"] = "password"
settings["accounts"] = [["user": String(user), "pass": pass]]
inbound["settings"] = settings
inboundsArray[socksIdx] = inbound
jsonDict["inbounds"] = inboundsArray
return SocksCredentials(username: String(user), password: pass)
}
// Fallback: no socks inbound generate credentials but can't inject
let user = UUID().uuidString.replacingOccurrences(of: "-", with: "").lowercased().prefix(16)
let pass = UUID().uuidString.replacingOccurrences(of: "-", with: "").lowercased()
return SocksCredentials(username: String(user), password: pass)
}
private func setupAndStartXray(configData: Data, private func setupAndStartXray(configData: Data,
completionHandler: @escaping (Error?) -> Void) { completionHandler: @escaping (Error?) -> Void) {
let path = Constants.cachesDirectory.appendingPathComponent("config.json", isDirectory: false).path let path = Constants.cachesDirectory.appendingPathComponent("config.json", isDirectory: false).path
@@ -307,8 +214,6 @@ extension PacketTunnelProvider {
private func setupAndRunTun2socks(configData: Data, private func setupAndRunTun2socks(configData: Data,
address: String, address: String,
port: Int, port: Int,
username: String,
password: String,
completionHandler: @escaping (Error?) -> Void) { completionHandler: @escaping (Error?) -> Void) {
let config = """ let config = """
tunnel: tunnel:
@@ -316,8 +221,6 @@ extension PacketTunnelProvider {
socks5: socks5:
port: \(port) port: \(port)
address: \(address) address: \(address)
username: \(username)
password: \(password)
udp: 'udp' udp: 'udp'
misc: misc:
task-stack-size: 20480 task-stack-size: 20480
+4 -114
View File
@@ -53,14 +53,6 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
var splitTunnelType: Int? var splitTunnelType: Int?
var splitTunnelSites: [String]? var splitTunnelSites: [String]?
var openVpnDnsServers: [String] = []
var openVpnRemoteAddress: String?
var openVpnRedirectGatewayDef1 = false
var openVpnLocalAddress: String?
var openVpnLocalMask: String?
var openVpnGatewayAddress: String?
var lastOpenVPNSettings: NEPacketTunnelNetworkSettings?
var lastOpenVPNStatsLogTime = Date.distantPast
let vpnReachability = OpenVPNReachability() let vpnReachability = OpenVPNReachability()
@@ -91,8 +83,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
guard hasMeaningfulChange, let proto = self.protoType else { return } guard hasMeaningfulChange, let proto = self.protoType else { return }
// WireGuard/AWG and OpenVPN manages network changes internally in its own adapter. // WireGuard/AWG manages network changes internally in its own adapter.
if proto == .wireguard || proto == .openvpn { if proto == .wireguard {
return return
} }
@@ -200,26 +192,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
let errorNotifier = ErrorNotifier(activationAttemptId: activationAttemptId) let errorNotifier = ErrorNotifier(activationAttemptId: activationAttemptId)
neLog(.info, message: "Start tunnel") neLog(.info, message: "Start tunnel")
if let vpnProto = protocolConfiguration as? NEVPNProtocol {
if #available(iOS 14.0, macOS 11.0, *) {
var details = "includeAllNetworks=\(vpnProto.includeAllNetworks)"
if #available(iOS 14.2, macOS 11.0, *) {
details += " excludeLocalNetworks=\(vpnProto.excludeLocalNetworks)"
}
neLog(.info, title: "Protocol", message: details)
}
}
if let protocolConfiguration = protocolConfiguration as? NETunnelProviderProtocol { if let protocolConfiguration = protocolConfiguration as? NETunnelProviderProtocol {
let providerConfiguration = protocolConfiguration.providerConfiguration let providerConfiguration = protocolConfiguration.providerConfiguration
let providerKeys = providerConfiguration?.keys.sorted().joined(separator: ",") ?? ""
var protocolDetails = "bundleId=\(protocolConfiguration.providerBundleIdentifier ?? "") keys=[\(providerKeys)]"
if let ovpnData = providerConfiguration?[Constants.ovpnConfigKey] as? Data {
let preview = String(decoding: ovpnData.prefix(512), as: UTF8.self)
protocolDetails += " ovpnBytes=\(ovpnData.count) ovpnPreview=\(preview)"
}
neLog(.info, title: "Protocol", message: protocolDetails)
if (providerConfiguration?[Constants.ovpnConfigKey] as? Data) != nil { if (providerConfiguration?[Constants.ovpnConfigKey] as? Data) != nil {
protoType = .openvpn protoType = .openvpn
} else if (providerConfiguration?[Constants.wireGuardConfigKey] as? Data) != nil { } else if (providerConfiguration?[Constants.wireGuardConfigKey] as? Data) != nil {
@@ -474,8 +449,6 @@ extension WireGuardLogLevel {
final class PacketTunnelFlowAdapter: NSObject, OpenVPNAdapterPacketFlow { final class PacketTunnelFlowAdapter: NSObject, OpenVPNAdapterPacketFlow {
private let flow: NEPacketTunnelFlow private let flow: NEPacketTunnelFlow
private var readLogCounter = 0
private var writeLogCounter = 0
init(flow: NEPacketTunnelFlow) { init(flow: NEPacketTunnelFlow) {
self.flow = flow self.flow = flow
@@ -484,98 +457,15 @@ final class PacketTunnelFlowAdapter: NSObject, OpenVPNAdapterPacketFlow {
@objc(readPacketsWithCompletionHandler:) @objc(readPacketsWithCompletionHandler:)
func readPackets(completionHandler: @escaping ([Data], [NSNumber]) -> Void) { func readPackets(completionHandler: @escaping ([Data], [NSNumber]) -> Void) {
flow.readPackets { packets, protocols in flow.readPackets(completionHandler: completionHandler)
#if os(macOS)
if self.readLogCounter < 20, let firstPacket = packets.first, let firstProtocol = protocols.first {
let prefix = firstPacket.prefix(12).map { String(format: "%02x", $0) }.joined()
let header = Self.describePacketHeader(firstPacket)
ovpnLog(.info, title: "FlowRead", message: "count=\(packets.count) proto0=\(firstProtocol) len0=\(firstPacket.count) prefix=\(prefix) \(header)")
self.readLogCounter += 1
}
#endif
completionHandler(packets, protocols)
}
} }
@objc(writePackets:withProtocols:) @objc(writePackets:withProtocols:)
func writePackets(_ packets: [Data], withProtocols protocols: [NSNumber]) -> Bool { func writePackets(_ packets: [Data], withProtocols protocols: [NSNumber]) -> Bool {
#if os(macOS) flow.writePackets(packets, withProtocols: protocols)
if writeLogCounter < 20, let firstPacket = packets.first, let firstProtocol = protocols.first {
let prefix = firstPacket.prefix(12).map { String(format: "%02x", $0) }.joined()
let header = Self.describePacketHeader(firstPacket)
ovpnLog(.info, title: "FlowWrite", message: "count=\(packets.count) proto0=\(firstProtocol) len0=\(firstPacket.count) prefix=\(prefix) \(header)")
writeLogCounter += 1
}
#endif
return flow.writePackets(packets, withProtocols: protocols)
}
private static func describePacketHeader(_ packet: Data) -> String {
guard let versionNibble = packet.first.map({ Int($0 >> 4) }) else {
return "ip=unknown"
}
if versionNibble == 4, packet.count >= 20 {
let ihl = Int(packet[0] & 0x0f) * 4
guard ihl >= 20, packet.count >= ihl else {
return "ip=ipv4 malformed"
}
let proto = packet[9]
let src = "\(packet[12]).\(packet[13]).\(packet[14]).\(packet[15])"
let dst = "\(packet[16]).\(packet[17]).\(packet[18]).\(packet[19])"
let l4Offset = ihl
let ports: String
if (proto == 6 || proto == 17) && packet.count >= l4Offset + 4 {
let srcPort = (UInt16(packet[l4Offset]) << 8) | UInt16(packet[l4Offset + 1])
let dstPort = (UInt16(packet[l4Offset + 2]) << 8) | UInt16(packet[l4Offset + 3])
ports = "sport=\(srcPort) dport=\(dstPort)"
} else {
ports = "sport=- dport=-"
}
let protoName: String
switch proto {
case 1: protoName = "ICMP"
case 6: protoName = "TCP"
case 17: protoName = "UDP"
default: protoName = "P\(proto)"
}
return "ip=ipv4 src=\(src) dst=\(dst) proto=\(protoName) \(ports)"
}
if versionNibble == 6, packet.count >= 40 {
let proto = packet[6]
func hex16(_ start: Int) -> String {
let value = (UInt16(packet[start]) << 8) | UInt16(packet[start + 1])
return String(format: "%x", value)
}
let src = stride(from: 8, to: 24, by: 2).map(hex16).joined(separator: ":")
let dst = stride(from: 24, to: 40, by: 2).map(hex16).joined(separator: ":")
let l4Offset = 40
let ports: String
if (proto == 6 || proto == 17) && packet.count >= l4Offset + 4 {
let srcPort = (UInt16(packet[l4Offset]) << 8) | UInt16(packet[l4Offset + 1])
let dstPort = (UInt16(packet[l4Offset + 2]) << 8) | UInt16(packet[l4Offset + 3])
ports = "sport=\(srcPort) dport=\(dstPort)"
} else {
ports = "sport=- dport=-"
}
let protoName: String
switch proto {
case 58: protoName = "ICMPv6"
case 6: protoName = "TCP"
case 17: protoName = "UDP"
default: protoName = "P\(proto)"
}
return "ip=ipv6 src=\(src) dst=\(dst) proto=\(protoName) \(ports)"
}
return "ip=v\(versionNibble) len=\(packet.count)"
} }
} }
extension NEPacketTunnelFlow: OpenVPNAdapterPacketFlow {}
extension NEProviderStopReason { extension NEProviderStopReason {
var amneziaDescription: String { var amneziaDescription: String {
switch self { switch self {
+11 -70
View File
@@ -218,13 +218,16 @@ bool IosController::connectVpn(amnezia::Proto proto, const QJsonObject& configur
m_rawConfig = configuration; m_rawConfig = configuration;
m_serverAddress = configuration.value(config_key::hostName).toString().toNSString(); m_serverAddress = configuration.value(config_key::hostName).toString().toNSString();
const QString serverDescription = configuration.value(config_key::description).toString().trimmed();
QString tunnelName; QString tunnelName;
if (serverDescription.isEmpty()) { if (configuration.value(config_key::description).toString().isEmpty()) {
tunnelName = ProtocolProps::protoToString(proto);
} else {
tunnelName = QString("%1 %2") tunnelName = QString("%1 %2")
.arg(serverDescription) .arg(configuration.value(config_key::hostName).toString())
.arg(ProtocolProps::protoToString(proto));
}
else {
tunnelName = QString("%1 (%2) %3")
.arg(configuration.value(config_key::description).toString())
.arg(configuration.value(config_key::hostName).toString())
.arg(ProtocolProps::protoToString(proto)); .arg(ProtocolProps::protoToString(proto));
} }
@@ -549,16 +552,6 @@ bool IosController::setupOpenVPN()
QJsonDocument openVPNConfigDoc(openVPNConfig); QJsonDocument openVPNConfigDoc(openVPNConfig);
QString openVPNConfigStr(openVPNConfigDoc.toJson(QJsonDocument::Compact)); QString openVPNConfigStr(openVPNConfigDoc.toJson(QJsonDocument::Compact));
QString openVPNConfigPreview = openVPNConfigStr.left(512);
QString ovpnPreview = ovpnConfig.left(512);
qDebug().noquote() << "IosController::setupOpenVPN payload"
<< "jsonBytes=" << openVPNConfigStr.toUtf8().size()
<< "ovpnChars=" << ovpnConfig.size()
<< "splitTunnelType=" << m_rawConfig[config_key::splitTunnelType].toInt()
<< "splitTunnelSites=" << splitTunnelSites;
qDebug().noquote() << "IosController::setupOpenVPN payload jsonPreview=" << openVPNConfigPreview;
qDebug().noquote() << "IosController::setupOpenVPN payload ovpnPreview=" << ovpnPreview;
return startOpenVPN(openVPNConfigStr); return startOpenVPN(openVPNConfigStr);
} }
@@ -807,59 +800,11 @@ bool IosController::startOpenVPN(const QString &config)
NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init]; NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init];
tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID]; tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID];
QByteArray configUtf8 = config.toUtf8(); tunnelProtocol.providerConfiguration = @{@"ovpn": [[NSString stringWithUTF8String:config.toStdString().c_str()] dataUsingEncoding:NSUTF8StringEncoding]};
NSData *ovpnConfigData = [NSData dataWithBytes:configUtf8.constData() length:configUtf8.size()];
tunnelProtocol.providerConfiguration = @{@"ovpn": ovpnConfigData};
tunnelProtocol.serverAddress = m_serverAddress; tunnelProtocol.serverAddress = m_serverAddress;
if (@available(iOS 14.0, macOS 11.0, *)) {
int splitTunnelType = 0;
QJsonParseError parseError;
QJsonDocument doc = QJsonDocument::fromJson(config.toUtf8(), &parseError);
if (parseError.error == QJsonParseError::NoError && doc.isObject()) {
QJsonObject obj = doc.object();
splitTunnelType = obj.value(config_key::splitTunnelType).toInt(0);
}
#if defined(MACOS_NE)
// On macOS NE use route-based full tunnel. includeAllNetworks enables
// policy-based drop-all mode and causes enforceRoutes to be ignored.
tunnelProtocol.includeAllNetworks = NO;
if (splitTunnelType == 0) {
tunnelProtocol.enforceRoutes = YES;
if (@available(iOS 14.2, macOS 11.0, *)) {
tunnelProtocol.excludeLocalNetworks = YES;
}
}
#else
tunnelProtocol.includeAllNetworks = (splitTunnelType == 0);
if (@available(iOS 14.2, macOS 11.0, *)) {
// Keep existing iOS behavior.
if (splitTunnelType == 0) {
tunnelProtocol.excludeLocalNetworks = NO;
}
}
#endif
}
m_currentTunnel.protocolConfiguration = tunnelProtocol; m_currentTunnel.protocolConfiguration = tunnelProtocol;
NETunnelProviderProtocol *appliedProtocol = (NETunnelProviderProtocol *)m_currentTunnel.protocolConfiguration;
NSData *ovpnPayload = appliedProtocol.providerConfiguration[@"ovpn"];
NSString *payloadPreview = @"";
if (ovpnPayload != nil) {
NSString *decodedPayload = [[NSString alloc] initWithData:ovpnPayload encoding:NSUTF8StringEncoding];
if (decodedPayload != nil) {
payloadPreview = [decodedPayload substringToIndex:MIN((NSUInteger)512, decodedPayload.length)];
}
}
qDebug().noquote() << "IosController::startOpenVPN protocolConfiguration"
<< "bundleId=" << QString::fromNSString(appliedProtocol.providerBundleIdentifier ?: @"")
<< "serverAddress=" << QString::fromNSString(appliedProtocol.serverAddress ?: @"")
<< "providerKeys=" << QString::fromNSString([[appliedProtocol.providerConfiguration.allKeys description] copy])
<< "ovpnBytes=" << (ovpnPayload != nil ? ovpnPayload.length : 0);
qDebug().noquote() << "IosController::startOpenVPN protocolConfiguration payloadPreview="
<< QString::fromNSString(payloadPreview);
startTunnel(); startTunnel();
} }
@@ -869,9 +814,7 @@ bool IosController::startWireGuard(const QString &config)
NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init]; NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init];
tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID]; tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID];
QByteArray configUtf8 = config.toUtf8(); tunnelProtocol.providerConfiguration = @{@"wireguard": [[NSString stringWithUTF8String:config.toStdString().c_str()] dataUsingEncoding:NSUTF8StringEncoding]};
NSData *wgConfigData = [NSData dataWithBytes:configUtf8.constData() length:configUtf8.size()];
tunnelProtocol.providerConfiguration = @{@"wireguard": wgConfigData};
tunnelProtocol.serverAddress = m_serverAddress; tunnelProtocol.serverAddress = m_serverAddress;
m_currentTunnel.protocolConfiguration = tunnelProtocol; m_currentTunnel.protocolConfiguration = tunnelProtocol;
@@ -885,9 +828,7 @@ bool IosController::startXray(const QString &config)
NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init]; NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init];
tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID]; tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID];
QByteArray configUtf8 = config.toUtf8(); tunnelProtocol.providerConfiguration = @{@"xray": [[NSString stringWithUTF8String:config.toStdString().c_str()] dataUsingEncoding:NSUTF8StringEncoding]};
NSData *xrayConfigData = [NSData dataWithBytes:configUtf8.constData() length:configUtf8.size()];
tunnelProtocol.providerConfiguration = @{@"xray": xrayConfigData};
tunnelProtocol.serverAddress = m_serverAddress; tunnelProtocol.serverAddress = m_serverAddress;
m_currentTunnel.protocolConfiguration = tunnelProtocol; m_currentTunnel.protocolConfiguration = tunnelProtocol;
+13 -6
View File
@@ -61,6 +61,8 @@ IOSNotificationHandler::~IOSNotificationHandler() { }
void IOSNotificationHandler::notify(NotificationHandler::Message type, const QString& title, void IOSNotificationHandler::notify(NotificationHandler::Message type, const QString& title,
const QString& message, int timerMsec) { const QString& message, int timerMsec) {
Q_UNUSED(type); Q_UNUSED(type);
// timerMsec is tray display hint on Windows, not a schedule delay — was wrongly used as seconds (CLI-570).
Q_UNUSED(timerMsec);
if (!m_delegate) { if (!m_delegate) {
return; return;
@@ -71,11 +73,13 @@ void IOSNotificationHandler::notify(NotificationHandler::Message type, const QSt
content.body = message.toNSString(); content.body = message.toNSString();
content.sound = [UNNotificationSound defaultSound]; content.sound = [UNNotificationSound defaultSound];
int timerSec = timerMsec / 1000; NSTimeInterval delay = 0.1;
UNTimeIntervalNotificationTrigger* trigger = UNTimeIntervalNotificationTrigger* trigger =
[UNTimeIntervalNotificationTrigger triggerWithTimeInterval:timerSec repeats:NO]; [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:delay repeats:NO];
UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:@"amneziavpn" NSString* requestId = [NSString stringWithFormat:@"amneziavpn.vpnstate.%lld",
(long long)([[NSDate date] timeIntervalSince1970] * 1000.0)];
UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:requestId
content:content content:content
trigger:trigger]; trigger:trigger];
@@ -143,6 +147,7 @@ IOSNotificationHandler::~IOSNotificationHandler() { }
void IOSNotificationHandler::notify(NotificationHandler::Message type, const QString& title, void IOSNotificationHandler::notify(NotificationHandler::Message type, const QString& title,
const QString& message, int timerMsec) { const QString& message, int timerMsec) {
Q_UNUSED(type); Q_UNUSED(type);
Q_UNUSED(timerMsec);
if (!m_delegate) { if (!m_delegate) {
return; return;
@@ -153,11 +158,13 @@ void IOSNotificationHandler::notify(NotificationHandler::Message type, const QSt
content.body = message.toNSString(); content.body = message.toNSString();
content.sound = [UNNotificationSound defaultSound]; content.sound = [UNNotificationSound defaultSound];
int timerSec = timerMsec / 1000; NSTimeInterval delay = 0.1;
UNTimeIntervalNotificationTrigger* trigger = UNTimeIntervalNotificationTrigger* trigger =
[UNTimeIntervalNotificationTrigger triggerWithTimeInterval:timerSec repeats:NO]; [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:delay repeats:NO];
UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:@"amneziavpn" NSString* requestId = [NSString stringWithFormat:@"amneziavpn.vpnstate.%lld",
(long long)([[NSDate date] timeIntervalSince1970] * 1000.0)];
UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:requestId
content:content content:content
trigger:trigger]; trigger:trigger];
@@ -164,13 +164,8 @@ bool LinuxRouteMonitor::rtmSendRoute(int action, int flags, int type,
} }
if (rtm->rtm_type == RTN_THROW) { if (rtm->rtm_type == RTN_THROW) {
QString gateway = NetworkUtilities::getGatewayAndIface().first;
if (gateway.isEmpty()) {
logger.warning() << "No default gateway available, skipping exclusion route";
return false;
}
struct in_addr ip4; struct in_addr ip4;
inet_pton(AF_INET, gateway.toUtf8(), &ip4); inet_pton(AF_INET, NetworkUtilities::getGatewayAndIface().first.toUtf8(), &ip4);
nlmsg_append_attr(nlmsg, sizeof(buf), RTA_GATEWAY, &ip4, sizeof(ip4)); nlmsg_append_attr(nlmsg, sizeof(buf), RTA_GATEWAY, &ip4, sizeof(ip4));
nlmsg_append_attr32(nlmsg, sizeof(buf), RTA_PRIORITY, 0); nlmsg_append_attr32(nlmsg, sizeof(buf), RTA_PRIORITY, 0);
rtm->rtm_type = RTN_UNICAST; rtm->rtm_type = RTN_UNICAST;
@@ -44,9 +44,6 @@ void LinuxNetworkWatcher::initialize() {
connect(m_worker, &LinuxNetworkWatcherWorker::wakeup, this, connect(m_worker, &LinuxNetworkWatcherWorker::wakeup, this,
&NetworkWatcherImpl::wakeup); &NetworkWatcherImpl::wakeup);
connect(m_worker, &LinuxNetworkWatcherWorker::networkChanged, this,
[this]() { emit networkChanged(""); });
// Let's wait a few seconds to allow the UI to be fully loaded and shown. // Let's wait a few seconds to allow the UI to be fully loaded and shown.
// This is not strictly needed, but it's better for user experience because // This is not strictly needed, but it's better for user experience because
// it makes the UI faster to appear, plus it gives a bit of delay between the // it makes the UI faster to appear, plus it gives a bit of delay between the
@@ -37,7 +37,6 @@
enum NMState { enum NMState {
NM_STATE_UNKNOWN = 0, NM_STATE_UNKNOWN = 0,
NM_STATE_ASLEEP = 10, NM_STATE_ASLEEP = 10,
NM_STATE_DISABLED = 10,
NM_STATE_DISCONNECTED = 20, NM_STATE_DISCONNECTED = 20,
NM_STATE_DISCONNECTING = 30, NM_STATE_DISCONNECTING = 30,
NM_STATE_CONNECTING = 40, NM_STATE_CONNECTING = 40,
@@ -200,11 +199,10 @@ void LinuxNetworkWatcherWorker::checkDevices() {
void LinuxNetworkWatcherWorker::NMStateChanged(quint32 state) void LinuxNetworkWatcherWorker::NMStateChanged(quint32 state)
{ {
logger.debug() << "NMStateChanged " << state; if (state == NM_STATE_ASLEEP) {
if (state == NM_STATE_ASLEEP || state == NM_STATE_DISABLED) {
emit wakeup(); emit wakeup();
} else if (state == NM_STATE_CONNECTED_GLOBAL) {
emit networkChanged();
} }
logger.debug() << "NMStateChanged " << state;
} }
@@ -24,7 +24,6 @@ class LinuxNetworkWatcherWorker final : public QObject {
signals: signals:
void unsecuredNetwork(const QString& networkName, const QString& networkId); void unsecuredNetwork(const QString& networkName, const QString& networkId);
void wakeup(); void wakeup();
void networkChanged();
public slots: public slots:
void initialize(); void initialize();
@@ -0,0 +1,8 @@
#ifndef MACOS_NE_VPN_NOTIFICATION_H
#define MACOS_NE_VPN_NOTIFICATION_H
class QString;
void macosNePostVpnStateNotification(const QString &title, const QString &message);
#endif
@@ -0,0 +1,91 @@
#include "macos_ne_vpn_notification.h"
#include <QtGlobal>
#include <QString>
#import <Foundation/Foundation.h>
#import <UserNotifications/UserNotifications.h>
namespace {
@interface MacosNeVpnNotificationDelegate : NSObject <UNUserNotificationCenterDelegate>
@end
@implementation MacosNeVpnNotificationDelegate
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
willPresentNotification:(UNNotification *)notification
withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler
{
Q_UNUSED(center)
Q_UNUSED(notification)
completionHandler(UNNotificationPresentationOptionList | UNNotificationPresentationOptionBanner);
}
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
didReceiveNotificationResponse:(UNNotificationResponse *)response
withCompletionHandler:(void (^)(void))completionHandler
{
Q_UNUSED(center)
Q_UNUSED(response)
completionHandler();
}
@end
MacosNeVpnNotificationDelegate *delegateInstance()
{
static MacosNeVpnNotificationDelegate *d;
static dispatch_once_t once;
dispatch_once(&once, ^{
d = [[MacosNeVpnNotificationDelegate alloc] init];
});
return d;
}
void ensureNotificationCenterSetup()
{
static dispatch_once_t once;
dispatch_once(&once, ^{
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center requestAuthorizationWithOptions:(UNAuthorizationOptionSound | UNAuthorizationOptionAlert |
UNAuthorizationOptionBadge)
completionHandler:^(BOOL granted, NSError *_Nullable error) {
Q_UNUSED(granted);
if (!error) {
center.delegate = delegateInstance();
}
}];
});
}
} // namespace
void macosNePostVpnStateNotification(const QString &title, const QString &message)
{
ensureNotificationCenterSetup();
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
content.title = title.toNSString();
content.body = message.toNSString();
content.sound = nil;
NSTimeInterval delay = 0.1;
UNTimeIntervalNotificationTrigger *trigger =
[UNTimeIntervalNotificationTrigger triggerWithTimeInterval:delay repeats:NO];
NSString *identifier =
[NSString stringWithFormat:@"amneziavpn.vpnstate.%lld", (long long)([[NSDate date] timeIntervalSince1970] * 1000.0)];
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:identifier
content:content
trigger:trigger];
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center addNotificationRequest:request
withCompletionHandler:^(NSError *_Nullable error) {
if (error) {
NSLog(@"macosNePostVpnStateNotification failed: %@", error);
}
}];
}
+2 -21
View File
@@ -1,7 +1,6 @@
#include "xrayprotocol.h" #include "xrayprotocol.h"
#include "core/ipcclient.h" #include "core/ipcclient.h"
#include "core/serialization/serialization.h"
#include "ipc.h" #include "ipc.h"
#include "utilities.h" #include "utilities.h"
#include "core/networkUtilities.h" #include "core/networkUtilities.h"
@@ -15,8 +14,6 @@
#include <QtCore/qobjectdefs.h> #include <QtCore/qobjectdefs.h>
#include <QtCore/qprocess.h> #include <QtCore/qprocess.h>
#include <exception>
#ifdef Q_OS_MACOS #ifdef Q_OS_MACOS
static const QString tunName = "utun22"; static const QString tunName = "utun22";
#else #else
@@ -56,19 +53,6 @@ ErrorCode XrayProtocol::start()
{ {
qDebug() << "XrayProtocol::start()"; qDebug() << "XrayProtocol::start()";
// Inject SOCKS5 auth into the inbound before starting xray.
// Re-uses existing credentials if the config already has them (e.g. imported config).
amnezia::serialization::inbounds::InboundCredentials creds;
try {
creds = amnezia::serialization::inbounds::EnsureInboundAuth(m_xrayConfig);
} catch (const std::exception &e) {
qCritical() << "EnsureInboundAuth failed:" << e.what();
return ErrorCode::InternalError;
}
m_socksUser = creds.username;
m_socksPassword = creds.password;
m_socksPort = creds.port;
return IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) { return IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
auto xrayStart = iface->xrayStart(QJsonDocument(m_xrayConfig).toJson()); auto xrayStart = iface->xrayStart(QJsonDocument(m_xrayConfig).toJson());
if (!xrayStart.waitForFinished() || !xrayStart.returnValue()) { if (!xrayStart.waitForFinished() || !xrayStart.returnValue()) {
@@ -137,11 +121,8 @@ ErrorCode XrayProtocol::startTun2Socks()
return ErrorCode::AmneziaServiceConnectionFailed; return ErrorCode::AmneziaServiceConnectionFailed;
} }
const QString proxyUrl = QString("socks5://%1:%2@127.0.0.1:%3")
.arg(m_socksUser, m_socksPassword, QString::number(m_socksPort));
m_tun2socksProcess->setProgram(PermittedProcess::Tun2Socks); m_tun2socksProcess->setProgram(PermittedProcess::Tun2Socks);
m_tun2socksProcess->setArguments({"-device", QString("tun://%1").arg(tunName), "-proxy", proxyUrl}); m_tun2socksProcess->setArguments({"-device", QString("tun://%1").arg(tunName), "-proxy", "socks5://127.0.0.1:10808" });
connect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardOutput, this, [this]() { connect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardOutput, this, [this]() {
auto readAllStandardOutput = m_tun2socksProcess->readAllStandardOutput(); auto readAllStandardOutput = m_tun2socksProcess->readAllStandardOutput();
@@ -155,7 +136,7 @@ ErrorCode XrayProtocol::startTun2Socks()
if (!line.contains("[TCP]") && !line.contains("[UDP]")) if (!line.contains("[TCP]") && !line.contains("[UDP]"))
qDebug() << "[tun2socks]:" << line; qDebug() << "[tun2socks]:" << line;
if (line.contains("[STACK] tun://") && line.contains("<-> socks5://")) { if (line.contains("[STACK] tun://") && line.contains("<-> socks5://127.0.0.1")) {
disconnect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardOutput, this, nullptr); disconnect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardOutput, this, nullptr);
if (ErrorCode res = setupRouting(); res != ErrorCode::NoError) { if (ErrorCode res = setupRouting(); res != ErrorCode::NoError) {
-4
View File
@@ -26,10 +26,6 @@ private:
QList<QHostAddress> m_dnsServers; QList<QHostAddress> m_dnsServers;
QString m_remoteAddress; QString m_remoteAddress;
QString m_socksUser;
QString m_socksPassword;
int m_socksPort = 10808;
QSharedPointer<IpcProcessInterfaceReplica> m_tun2socksProcess; QSharedPointer<IpcProcessInterfaceReplica> m_tun2socksProcess;
}; };
@@ -1,5 +1,5 @@
sudo docker ps -a | grep amnezia | awk '{print $1}' | xargs sudo docker stop;\ sudo docker ps -a | grep amnezia | awk '{print $1}' | xargs sudo docker stop;\
sudo docker ps -a | grep amnezia | awk '{print $1}' | xargs sudo docker rm -fv;\ sudo docker ps -a | grep amnezia | awk '{print $1}' | xargs sudo docker rm -fv;\
sudo docker images -a --format table | grep amnezia | awk '{print $3, $1 ":" $2}' | xargs sudo docker rmi;\ sudo docker images -a --format table | grep amnezia | awk '{print $3}' | xargs sudo docker rmi;\
sudo docker network ls | grep amnezia-dns-net | awk '{print $1}' | xargs sudo docker network rm;\ sudo docker network ls | grep amnezia-dns-net | awk '{print $1}' | xargs sudo docker network rm;\
sudo rm -frd /opt/amnezia sudo rm -frd /opt/amnezia
@@ -1,4 +1,4 @@
FROM 3proxy/3proxy:0.9.5 FROM 3proxy/3proxy:latest
LABEL maintainer="AmneziaVPN" LABEL maintainer="AmneziaVPN"
-19
View File
@@ -15,7 +15,6 @@ namespace
const char cloudFlareNs2[] = "1.0.0.1"; const char cloudFlareNs2[] = "1.0.0.1";
constexpr char gatewayEndpoint[] = "http://gw.amnezia.org:80/"; constexpr char gatewayEndpoint[] = "http://gw.amnezia.org:80/";
constexpr char proxyUrlsKey[] = "Conf/proxyUrls/";
} }
Settings::Settings(QObject *parent) : QObject(parent), m_settings(ORGANIZATION_NAME, APPLICATION_NAME, this) Settings::Settings(QObject *parent) : QObject(parent), m_settings(ORGANIZATION_NAME, APPLICATION_NAME, this)
@@ -527,24 +526,6 @@ void Settings::toggleDevGatewayEnv(bool enabled)
m_settings.setValue("Conf/devGatewayEnv", enabled); m_settings.setValue("Conf/devGatewayEnv", enabled);
} }
QByteArray Settings::readGatewayProxyUrls(const QString &cacheKey) const
{
if (cacheKey.isEmpty()) {
return {};
}
return m_settings.value(QString(proxyUrlsKey) + cacheKey).toByteArray();
}
void Settings::writeGatewayProxyUrls(const QString &cacheKey, const QByteArray &proxyUrlsEncrypted)
{
if (cacheKey.isEmpty()) {
return;
}
m_settings.setValue(QString(proxyUrlsKey) + cacheKey, proxyUrlsEncrypted);
}
bool Settings::isHomeAdLabelVisible() bool Settings::isHomeAdLabelVisible()
{ {
return m_settings.value("Conf/homeAdLabelVisible", true).toBool(); return m_settings.value("Conf/homeAdLabelVisible", true).toBool();
-3
View File
@@ -4,7 +4,6 @@
#include <QObject> #include <QObject>
#include <QSettings> #include <QSettings>
#include <QString> #include <QString>
#include <QByteArray>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
@@ -235,8 +234,6 @@ public:
QString getGatewayEndpoint(bool isTestPurchase = false); QString getGatewayEndpoint(bool isTestPurchase = false);
bool isDevGatewayEnv(bool isTestPurchase = false); bool isDevGatewayEnv(bool isTestPurchase = false);
void toggleDevGatewayEnv(bool enabled); void toggleDevGatewayEnv(bool enabled);
QByteArray readGatewayProxyUrls(const QString &cacheKey) const;
void writeGatewayProxyUrls(const QString &cacheKey, const QByteArray &proxyUrlsEncrypted);
bool isHomeAdLabelVisible(); bool isHomeAdLabelVisible();
void disableHomeAdLabel(); void disableHomeAdLabel();
+122 -323
View File
@@ -52,18 +52,18 @@
<context> <context>
<name>ApiAccountInfoModel</name> <name>ApiAccountInfoModel</name>
<message> <message>
<location filename="../ui/models/api/apiAccountInfoModel.cpp" line="32"/> <location filename="../ui/models/api/apiAccountInfoModel.cpp" line="31"/>
<location filename="../ui/models/api/apiAccountInfoModel.cpp" line="37"/> <location filename="../ui/models/api/apiAccountInfoModel.cpp" line="35"/>
<source>Active</source> <source>Active</source>
<translation>Активна</translation> <translation>Активна</translation>
</message> </message>
<message> <message>
<location filename="../ui/models/api/apiAccountInfoModel.cpp" line="36"/> <location filename="../ui/models/api/apiAccountInfoModel.cpp" line="34"/>
<source>Inactive</source> <source>&lt;p&gt;&lt;a style=&quot;color: #EB5757;&quot;&gt;Inactive&lt;/a&gt;</source>
<translation>Не активна</translation> <translation>Не активна</translation>
</message> </message>
<message> <message>
<location filename="../ui/models/api/apiAccountInfoModel.cpp" line="50"/> <location filename="../ui/models/api/apiAccountInfoModel.cpp" line="48"/>
<source>%1 out of %2</source> <source>%1 out of %2</source>
<translation>%1 из %2</translation> <translation>%1 из %2</translation>
</message> </message>
@@ -71,51 +71,23 @@
<context> <context>
<name>ApiConfigsController</name> <name>ApiConfigsController</name>
<message> <message>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="859"/> <location filename="../ui/controllers/api/apiConfigsController.cpp" line="514"/>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="929"/> <location filename="../ui/controllers/api/apiConfigsController.cpp" line="690"/>
<source>%1 installed successfully.</source> <source>%1 installed successfully.</source>
<translation>%1 успешно установлен.</translation> <translation>%1 успешно установлен.</translation>
</message> </message>
<message> <message>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="810"/> <location filename="../ui/controllers/api/apiConfigsController.cpp" line="637"/>
<source>Subscription restored successfully.</source> <source>Subscription restored successfully.</source>
<translation>Подписка успешно восстановлена.</translation> <translation>Подписка успешно восстановлена.</translation>
</message> </message>
<message> <message>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="391"/> <location filename="../ui/controllers/api/apiConfigsController.cpp" line="751"/>
<source>%1/mo</source>
<comment>IAP: price per month in plan subtitle</comment>
<translation>%1/мес</translation>
</message>
<message>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="410"/>
<source>from %1 per month</source>
<comment>IAP: card footer minimum monthly price from StoreKit</comment>
<translation>от %1 в месяц</translation>
</message>
<message>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="664"/>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="802"/>
<source>This subscription has already been added</source>
<translation>Эта подписка уже добавлена</translation>
</message>
<message>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="672"/>
<source>%1 has been added to the app</source>
<translation>%1 добавлено в приложение</translation>
</message>
<message>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="897"/>
<source>This email address has already been used to activate a trial. If you like the service, you can upgrade to Premium</source>
<translation>Этот адрес электронной почты уже использовался для активации пробного периода. Если вам понравился сервис, вы можете оформить подписку Premium</translation>
</message>
<message>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="998"/>
<source>API config reloaded</source> <source>API config reloaded</source>
<translation>Конфигурация API перезагружена</translation> <translation>Конфигурация API перезагружена</translation>
</message> </message>
<message> <message>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="1002"/> <location filename="../ui/controllers/api/apiConfigsController.cpp" line="755"/>
<source>Successfully changed the country of connection to %1</source> <source>Successfully changed the country of connection to %1</source>
<translation>Страна подключения изменена на %1</translation> <translation>Страна подключения изменена на %1</translation>
</message> </message>
@@ -210,24 +182,29 @@
<translation>&lt;p&gt;&lt;a style=&quot;color: #EB5757;&quot;&gt;Недоступно в вашем регионе. Если у вас включен VPN, отключите его, вернитесь на предыдущий экран и попробуйте снова.&lt;/a&gt;</translation> <translation>&lt;p&gt;&lt;a style=&quot;color: #EB5757;&quot;&gt;Недоступно в вашем регионе. Если у вас включен VPN, отключите его, вернитесь на предыдущий экран и попробуйте снова.&lt;/a&gt;</translation>
</message> </message>
<message> <message>
<location filename="../ui/models/api/apiServicesModel.cpp" line="95"/>
<source>%1 MBit/s</source> <source>%1 MBit/s</source>
<translation type="vanished">%1 Мбит/с</translation> <translation>%1 Мбит/с</translation>
</message> </message>
<message> <message>
<location filename="../ui/models/api/apiServicesModel.cpp" line="102"/>
<source>%1 days</source> <source>%1 days</source>
<translation type="vanished">%1 дней</translation> <translation>%1 дней</translation>
</message> </message>
<message> <message>
<location filename="../ui/models/api/apiServicesModel.cpp" line="113"/>
<source>Free</source> <source>Free</source>
<translation type="vanished">Бесплатно</translation> <translation>Бесплатно</translation>
</message> </message>
<message> <message>
<location filename="../ui/models/api/apiServicesModel.cpp" line="116"/>
<source>%1 $</source> <source>%1 $</source>
<translation type="vanished">%1 $</translation> <translation>%1 $</translation>
</message> </message>
<message> <message>
<location filename="../ui/models/api/apiServicesModel.cpp" line="118"/>
<source>%1 $/month</source> <source>%1 $/month</source>
<translation type="vanished">%1 $/месяц</translation> <translation>%1 $/месяц</translation>
</message> </message>
</context> </context>
<context> <context>
@@ -264,45 +241,45 @@
<context> <context>
<name>ConnectionController</name> <name>ConnectionController</name>
<message> <message>
<location filename="../ui/controllers/connectionController.cpp" line="82"/> <location filename="../ui/controllers/connectionController.cpp" line="81"/>
<source>Connecting...</source> <source>Connecting...</source>
<translation>Подключение...</translation> <translation>Подключение...</translation>
</message> </message>
<message> <message>
<location filename="../ui/controllers/connectionController.cpp" line="89"/> <location filename="../ui/controllers/connectionController.cpp" line="86"/>
<source>Connected</source> <source>Connected</source>
<translation>Подключено</translation> <translation>Подключено</translation>
</message> </message>
<message> <message>
<location filename="../ui/controllers/connectionController.cpp" line="113"/> <location filename="../ui/controllers/connectionController.cpp" line="110"/>
<source>Preparing...</source> <source>Preparing...</source>
<translation>Подготовка...</translation> <translation>Подготовка...</translation>
</message> </message>
<message> <message>
<location filename="../ui/controllers/connectionController.cpp" line="135"/> <location filename="../ui/controllers/connectionController.cpp" line="132"/>
<source>Settings updated successfully, reconnnection...</source> <source>Settings updated successfully, reconnnection...</source>
<translation>Настройки успешно обновлены, переподключение...</translation> <translation>Настройки успешно обновлены, переподключение...</translation>
</message> </message>
<message> <message>
<location filename="../ui/controllers/connectionController.cpp" line="138"/> <location filename="../ui/controllers/connectionController.cpp" line="135"/>
<source>Settings updated successfully</source> <source>Settings updated successfully</source>
<translation>Настройки успешно обновлены</translation> <translation>Настройки успешно обновлены</translation>
</message> </message>
<message> <message>
<location filename="../ui/controllers/connectionController.cpp" line="98"/> <location filename="../ui/controllers/connectionController.cpp" line="95"/>
<source>Reconnecting...</source> <source>Reconnecting...</source>
<translation>Переподключение...</translation> <translation>Переподключение...</translation>
</message> </message>
<message> <message>
<location filename="../ui/controllers/connectionController.h" line="70"/> <location filename="../ui/controllers/connectionController.h" line="70"/>
<location filename="../ui/controllers/connectionController.cpp" line="103"/> <location filename="../ui/controllers/connectionController.cpp" line="100"/>
<location filename="../ui/controllers/connectionController.cpp" line="118"/> <location filename="../ui/controllers/connectionController.cpp" line="115"/>
<location filename="../ui/controllers/connectionController.cpp" line="124"/> <location filename="../ui/controllers/connectionController.cpp" line="121"/>
<source>Connect</source> <source>Connect</source>
<translation>Подключиться</translation> <translation>Подключиться</translation>
</message> </message>
<message> <message>
<location filename="../ui/controllers/connectionController.cpp" line="108"/> <location filename="../ui/controllers/connectionController.cpp" line="105"/>
<source>Disconnecting...</source> <source>Disconnecting...</source>
<translation>Отключение...</translation> <translation>Отключение...</translation>
</message> </message>
@@ -1720,32 +1697,17 @@ Thank you for staying with us!</source>
<context> <context>
<name>PageSettingsApiAvailableCountries</name> <name>PageSettingsApiAvailableCountries</name>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiAvailableCountries.qml" line="129"/> <location filename="../ui/qml/Pages2/PageSettingsApiAvailableCountries.qml" line="84"/>
<source>Subscription expired</source>
<translation>Подписка закончилась</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiAvailableCountries.qml" line="129"/>
<source>Subscription expiring soon</source>
<translation>Подписка скоро закончится</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiAvailableCountries.qml" line="148"/>
<source>Renew subscription</source>
<translation>Продлить подписку</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiAvailableCountries.qml" line="162"/>
<source>Location for connection</source> <source>Location for connection</source>
<translation>Страны для подключения</translation> <translation>Страны для подключения</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiAvailableCountries.qml" line="191"/> <location filename="../ui/qml/Pages2/PageSettingsApiAvailableCountries.qml" line="123"/>
<source>Unable change server location while trying to make an active connection</source> <source>Unable change server location while trying to make an active connection</source>
<translation>Невозможно изменить локацию во время попытки соединения</translation> <translation>Невозможно изменить локацию во время попытки соединения</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiAvailableCountries.qml" line="195"/> <location filename="../ui/qml/Pages2/PageSettingsApiAvailableCountries.qml" line="127"/>
<source>Unable change server location while there is an active connection</source> <source>Unable change server location while there is an active connection</source>
<translation>Невозможно изменить локацию во время активного соединения</translation> <translation>Невозможно изменить локацию во время активного соединения</translation>
</message> </message>
@@ -1977,12 +1939,12 @@ Thank you for staying with us!</source>
<context> <context>
<name>PageSettingsApiServerInfo</name> <name>PageSettingsApiServerInfo</name>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="298"/> <location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="190"/>
<source>Configurations have been updated for some countries. Download and install the updated configuration files</source> <source>Configurations have been updated for some countries. Download and install the updated configuration files</source>
<translation>Сетевые адреса одного или нескольких серверов были обновлены. Пожалуйста, удалите старые конфигурацию и загрузите новые файлы</translation> <translation>Сетевые адреса одного или нескольких серверов были обновлены. Пожалуйста, удалите старые конфигурацию и загрузите новые файлы</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="343"/> <location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="235"/>
<source>Manage configuration files</source> <source>Manage configuration files</source>
<translation>Управление файлами конфигурации</translation> <translation>Управление файлами конфигурации</translation>
</message> </message>
@@ -2002,122 +1964,106 @@ Thank you for staying with us!</source>
<translation>Активные соединения</translation> <translation>Активные соединения</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="150"/> <location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="166"/>
<source>Subscription expired</source>
<translation>Подписка закончилась</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="151"/>
<source>Subscription expiring soon</source>
<translation>Подписка скоро закончится</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="181"/>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="246"/>
<source>Renew subscription</source>
<translation>Продлить подписку</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="270"/>
<source>Use VLESS protocol</source> <source>Use VLESS protocol</source>
<translation>Использовать протокол VLESS</translation> <translation>Использовать протокол VLESS</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="274"/> <location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="170"/>
<source>Cannot change protocol during active connection</source> <source>Cannot change protocol during active connection</source>
<translation>Невозможно изменить протокол во время активного соединения</translation> <translation>Невозможно изменить протокол во время активного соединения</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="319"/> <location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="211"/>
<source>Subscription Key</source> <source>Subscription Key</source>
<translation>Ключ для подключения</translation> <translation>Ключ для подключения</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="341"/> <location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="233"/>
<source>Configuration Files</source> <source>Configuration Files</source>
<translation>Файлы конфигурации</translation> <translation>Файлы конфигурации</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="361"/> <location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="253"/>
<source>Active Devices</source> <source>Active Devices</source>
<translation>Активные устройства</translation> <translation>Активные устройства</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="363"/> <location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="255"/>
<source>Manage currently connected devices</source> <source>Manage currently connected devices</source>
<translation>Управление подключенными устройствами</translation> <translation>Управление подключенными устройствами</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="380"/> <location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="272"/>
<source>Support</source> <source>Support</source>
<translation>Поддержка</translation> <translation>Поддержка</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="395"/> <location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="287"/>
<source>How to connect on another device</source> <source>How to connect on another device</source>
<translation>Как подключить другие устройства</translation> <translation>Как подключить другие устройства</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="420"/> <location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="312"/>
<source>Reload API config</source> <source>Reload API config</source>
<translation>Перезагрузить конфигурацию API</translation> <translation>Перезагрузить конфигурацию API</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="423"/> <location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="315"/>
<source>Reload API config?</source> <source>Reload API config?</source>
<translation>Перезагрузить конфигурацию API?</translation> <translation>Перезагрузить конфигурацию API?</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="424"/> <location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="316"/>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="462"/> <location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="354"/>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="499"/> <location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="391"/>
<source>Continue</source> <source>Continue</source>
<translation>Продолжить</translation> <translation>Продолжить</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="425"/> <location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="317"/>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="463"/> <location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="355"/>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="500"/> <location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="392"/>
<source>Cancel</source> <source>Cancel</source>
<translation>Отменить</translation> <translation>Отменить</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="429"/> <location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="321"/>
<source>Cannot reload API config during active connection</source> <source>Cannot reload API config during active connection</source>
<translation>Невозможно перзагрузить API конфигурацию при активном соединении</translation> <translation>Невозможно перзагрузить API конфигурацию при активном соединении</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="457"/> <location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="349"/>
<source>Unlink this device</source> <source>Unlink this device</source>
<translation>Отвязать это устройство</translation> <translation>Отвязать это устройство</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="460"/> <location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="352"/>
<source>Are you sure you want to unlink this device?</source> <source>Are you sure you want to unlink this device?</source>
<translation>Вы уверены, что хотите отвязать это устройство?</translation> <translation>Вы уверены, что хотите отвязать это устройство?</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="461"/> <location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="353"/>
<source>This will unlink the device from your subscription. You can reconnect it anytime by pressing&#xa0;&quot;Reload API config&quot; in subscription settings on device.</source> <source>This will unlink the device from your subscription. You can reconnect it anytime by pressing&#xa0;&quot;Reload API config&quot; in subscription settings on device.</source>
<translation>Это отключит устройство от вашей подписки. Вы можете повторно подключить его в любое время, нажав &quot;Перезагрузить конфигурацию API&quot; в настройках подписки на устройстве.</translation> <translation>Это отключит устройство от вашей подписки. Вы можете повторно подключить его в любое время, нажав &quot;Перезагрузить конфигурацию API&quot; в настройках подписки на устройстве.</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="467"/> <location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="359"/>
<source>Cannot unlink device during active connection</source> <source>Cannot unlink device during active connection</source>
<translation>Невозможно отвязать устройство во время активного соединения</translation> <translation>Невозможно отвязать устройство во время активного соединения</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="495"/> <location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="387"/>
<source>Remove from application</source> <source>Remove from application</source>
<translation>Удалить из приложения</translation> <translation>Удалить из приложения</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="498"/> <location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="390"/>
<source>Remove from application?</source> <source>Remove from application?</source>
<translation>Удалить из приложения?</translation> <translation>Удалить из приложения?</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="504"/> <location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="396"/>
<source>Cannot remove server during active connection</source> <source>Cannot remove server during active connection</source>
<translation>Невозможно удалить сервер во время активного соединения</translation> <translation>Невозможно удалить сервер во время активного соединения</translation>
</message> </message>
@@ -3165,83 +3111,51 @@ Thank you for staying with us!</source>
</message> </message>
</context> </context>
<context> <context>
<name>PageSetupWizardApiFreeInfo</name> <name>PageSetupWizardApiServiceInfo</name>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardApiFreeInfo.qml" line="74"/> <location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="113"/>
<source>Free features</source>
<translation>Возможности Free</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardApiFreeInfo.qml" line="125"/>
<source>Continue</source>
<translation>Продолжить</translation>
</message>
</context>
<context>
<name>PageSetupWizardApiPremiumInfo</name>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardApiPremiumInfo.qml" line="91"/>
<source>Recommended</source>
<translation>Рекомендуется</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardApiPremiumInfo.qml" line="103"/>
<source>Premium features</source>
<translation>Возможности Premium</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardApiPremiumInfo.qml" line="132"/>
<source>Charged to your Apple ID at confirmation. Renews automatically unless auto-renew is turned off at least 24 hours before period end. Manage in Apple ID settings.</source> <source>Charged to your Apple ID at confirmation. Renews automatically unless auto-renew is turned off at least 24 hours before period end. Manage in Apple ID settings.</source>
<translation>Списание с Apple ID при подтверждении. Продление автоматическое, если автопродление не отключено минимум за 24 часа до окончания периода. Управление в настройках Apple ID.</translation> <translation>Списание с Apple ID при подтверждении. Продление автоматическое, если автопродление не отключено минимум за 24 часа до окончания периода. Управление в настройках Apple ID.</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardApiPremiumInfo.qml" line="169"/> <location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="125"/>
<source>Continue</source>
<translation>Продолжить</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardApiPremiumInfo.qml" line="171"/>
<source>Subscribe %1 for %2</source>
<translation>Подписаться %1 за %2</translation>
</message>
</context>
<context>
<name>PageSetupWizardApiServiceInfo</name>
<message>
<source>Charged to your Apple ID at confirmation. Renews automatically unless auto-renew is turned off at least 24 hours before period end. Manage in Apple ID settings.</source>
<translation type="vanished">Списание с Apple ID при подтверждении. Продление автоматическое, если автопродление не отключено минимум за 24 часа до окончания периода. Управление в настройках Apple ID.</translation>
</message>
<message>
<source>Subscribe Now</source> <source>Subscribe Now</source>
<translation type="vanished">Подписаться сейчас</translation> <translation>Подписаться сейчас</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="158"/>
<source>By continuing, you agree to the &lt;a href=&quot;%1&quot; style=&quot;color: #FBB26A;&quot;&gt;Terms of Use&lt;/a&gt; and &lt;a href=&quot;%2&quot; style=&quot;color: #FBB26A;&quot;&gt;Privacy Policy&lt;/a&gt;</source> <source>By continuing, you agree to the &lt;a href=&quot;%1&quot; style=&quot;color: #FBB26A;&quot;&gt;Terms of Use&lt;/a&gt; and &lt;a href=&quot;%2&quot; style=&quot;color: #FBB26A;&quot;&gt;Privacy Policy&lt;/a&gt;</source>
<translation type="vanished">Продолжая, вы соглашаетесь с &lt;a href=&quot;%1&quot; style=&quot;color: #FBB26A;&quot;&gt;Условиями использования&lt;/a&gt; и &lt;a href=&quot;%2&quot; style=&quot;color: #FBB26A;&quot;&gt;Политикой конфиденциальности&lt;/a&gt;</translation> <translation>Продолжая, вы соглашаетесь с &lt;a href=&quot;%1&quot; style=&quot;color: #FBB26A;&quot;&gt;Условиями использования&lt;/a&gt; и &lt;a href=&quot;%2&quot; style=&quot;color: #FBB26A;&quot;&gt;Политикой конфиденциальности&lt;/a&gt;</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="186"/>
<source>For the region</source> <source>For the region</source>
<translation type="vanished">Для региона</translation> <translation>Для региона</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="195"/>
<source>Price</source> <source>Price</source>
<translation type="vanished">Цена</translation> <translation>Цена</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="204"/>
<source>Work period</source> <source>Work period</source>
<translation type="vanished">Период работы</translation> <translation>Период работы</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="213"/>
<source>Speed</source> <source>Speed</source>
<translation type="vanished">Скорость</translation> <translation>Скорость</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="222"/>
<source>Features</source> <source>Features</source>
<translation type="vanished">Особенности</translation> <translation>Особенности</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="125"/>
<source>Connect</source> <source>Connect</source>
<translation type="vanished">Подключиться</translation> <translation>Подключиться</translation>
</message> </message>
</context> </context>
<context> <context>
@@ -3256,50 +3170,11 @@ Thank you for staying with us!</source>
<source>Choose a VPN service that suits your needs.</source> <source>Choose a VPN service that suits your needs.</source>
<translation>Выберите VPN-сервис, который подходит именно вам.</translation> <translation>Выберите VPN-сервис, который подходит именно вам.</translation>
</message> </message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardApiServicesList.qml" line="88"/>
<source>Recommended</source>
<translation>Рекомендуется</translation>
</message>
</context>
<context>
<name>PageSetupWizardApiTrialEmail</name>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardApiTrialEmail.qml" line="65"/>
<source>Create an account</source>
<translation>Создайте учётную запись</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardApiTrialEmail.qml" line="66"/>
<source>To manage your subscription</source>
<translation>Для управления подпиской</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardApiTrialEmail.qml" line="77"/>
<location filename="../ui/qml/Pages2/PageSetupWizardApiTrialEmail.qml" line="78"/>
<source>Email</source>
<translation>Электронная почта</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardApiTrialEmail.qml" line="102"/>
<source>We will create an account for your trial subscription and send important subscription updates to this email address</source>
<translation>Мы создадим учётную запись для вашей пробной подписки и будем отправлять на этот адрес электронной почты важные уведомления о подписке</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardApiTrialEmail.qml" line="118"/>
<source>Continue</source>
<translation>Продолжить</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardApiTrialEmail.qml" line="126"/>
<source>Enter a valid email address</source>
<translation>Введите корректный адрес электронной почты</translation>
</message>
</context> </context>
<context> <context>
<name>PageSetupWizardConfigSource</name> <name>PageSetupWizardConfigSource</name>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="331"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="324"/>
<source>File with connection settings</source> <source>File with connection settings</source>
<translation>Файл с настройками подключения</translation> <translation>Файл с настройками подключения</translation>
</message> </message>
@@ -3374,80 +3249,71 @@ Thank you for staying with us!</source>
<translation>Другие варианты подключения</translation> <translation>Другие варианты подключения</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="226"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="253"/>
<source>Recommended</source>
<translation>Рекомендуется</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="256"/>
<source>Site Amnezia</source> <source>Site Amnezia</source>
<translation>Сайт Amnezia</translation> <translation>Сайт Amnezia</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="281"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="358"/>
<source>The easiest way to connect to the VPN</source>
<translation>Самый простой способ подключиться к VPN</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="367"/>
<source>Restore purchases</source> <source>Restore purchases</source>
<translation>Восстановить покупки</translation> <translation>Восстановить покупки</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="280"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="277"/>
<source>VPN by Amnezia</source> <source>VPN by Amnezia</source>
<translation>VPN от Amnezia</translation> <translation>VPN от Amnezia</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="278"/>
<source>Connect to classic paid and free VPN services from Amnezia</source> <source>Connect to classic paid and free VPN services from Amnezia</source>
<translation type="vanished">Подключайтесь к классическим платным и бесплатным VPN-сервисам от Amnezia</translation> <translation>Подключайтесь к классическим платным и бесплатным VPN-сервисам от Amnezia</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="299"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="294"/>
<source>Self-hosted VPN</source> <source>Self-hosted VPN</source>
<translation>Self-hosted VPN</translation> <translation>Self-hosted VPN</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="300"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="295"/>
<source>Configure Amnezia VPN on your own server</source> <source>Configure Amnezia VPN on your own server</source>
<translation>Настроить VPN на собственном сервере</translation> <translation>Настроить VPN на собственном сервере</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="312"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="306"/>
<source>Restore from backup</source> <source>Restore from backup</source>
<translation>Восстановить из резервной копии</translation> <translation>Восстановить из резервной копии</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="313"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="307"/>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="332"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="325"/>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="352"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="344"/>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="368"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="359"/>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="383"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="373"/>
<source></source> <source></source>
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="317"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="311"/>
<source>Open backup file</source> <source>Open backup file</source>
<translation>Открыть резервную копию</translation> <translation>Открыть резервную копию</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="318"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="312"/>
<source>Backup files (*.backup)</source> <source>Backup files (*.backup)</source>
<translation>Файлы резервных копий (*.backup)</translation> <translation>Файлы резервных копий (*.backup)</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="338"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="331"/>
<source>Open config file</source> <source>Open config file</source>
<translation>Открыть файл с конфигурацией</translation> <translation>Открыть файл с конфигурацией</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="351"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="343"/>
<source>QR code</source> <source>QR code</source>
<translation>QR-код</translation> <translation>QR-код</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="382"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="372"/>
<source>I have nothing</source> <source>I have nothing</source>
<translation>У меня ничего нет</translation> <translation>У меня ничего нет</translation>
</message> </message>
@@ -3455,17 +3321,17 @@ Thank you for staying with us!</source>
<context> <context>
<name>PageSetupWizardCredentials</name> <name>PageSetupWizardCredentials</name>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="206"/> <location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="194"/>
<source>Server IP address [:port]</source> <source>Server IP address [:port]</source>
<translation>IP-адрес[:порт] сервера</translation> <translation>IP-адрес[:порт] сервера</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="112"/> <location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="100"/>
<source>Continue</source> <source>Continue</source>
<translation>Продолжить</translation> <translation>Продолжить</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="179"/> <location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="167"/>
<source>Enter the address in the format 255.255.255.255:88</source> <source>Enter the address in the format 255.255.255.255:88</source>
<translation>Введите адрес в формате 255.255.255.255:88</translation> <translation>Введите адрес в формате 255.255.255.255:88</translation>
</message> </message>
@@ -3475,54 +3341,48 @@ Thank you for staying with us!</source>
<translation>Настроить ваш сервер</translation> <translation>Настроить ваш сервер</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="207"/> <location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="195"/>
<source>255.255.255.255:22</source> <source>255.255.255.255:22</source>
<translation>255.255.255.255:22</translation> <translation>255.255.255.255:22</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="215"/> <location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="203"/>
<source>SSH Username</source> <source>SSH Username</source>
<translation>Имя пользователя SSH</translation> <translation>Имя пользователя SSH</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="82"/> <location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="82"/>
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="94"/> <location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="212"/>
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="224"/>
<source>Password or SSH private key</source> <source>Password or SSH private key</source>
<translation>Пароль или закрытый ключ SSH</translation> <translation>Пароль или закрытый ключ SSH</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="97"/> <location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="132"/>
<source>SSH key requirements: supported key types are ED25519 and RSA in PEM format. Paste the private key, including the BEGIN/END lines. If your key doesnt work, generate a compatible one</source>
<translation>Требования к SSH-ключу: поддерживаются ключи ED25519 и RSA в формате PEM. Вставьте закрытый ключ целиком, включая строки BEGIN/END. Если ваш ключ не подходит, создайте совместимый ключ</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="144"/>
<source>All data you enter will remain strictly confidential and will not be shared or disclosed to the Amnezia or any third parties</source> <source>All data you enter will remain strictly confidential and will not be shared or disclosed to the Amnezia or any third parties</source>
<translation>Все данные, которые вы вводите, останутся строго конфиденциальными и не будут переданы или раскрыты Amnezia или каким-либо третьим лицам</translation> <translation>Все данные, которые вы вводите, останутся строго конфиденциальными и не будут переданы или раскрыты Amnezia или каким-либо третьим лицам</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="155"/> <location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="143"/>
<source>How to run your VPN server</source> <source>How to run your VPN server</source>
<translation>Как создать VPN на собственном сервере</translation> <translation>Как создать VPN на собственном сервере</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="156"/> <location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="144"/>
<source>Where to get connection data, step-by-step instructions for buying a VPS</source> <source>Where to get connection data, step-by-step instructions for buying a VPS</source>
<translation>Где взять данные для подключения, пошаговые инструкции по покупке VPS</translation> <translation>Где взять данные для подключения, пошаговые инструкции по покупке VPS</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="176"/> <location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="164"/>
<source>Ip address cannot be empty</source> <source>Ip address cannot be empty</source>
<translation>Поле с IP-адресом не может быть пустым</translation> <translation>Поле с IP-адресом не может быть пустым</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="184"/> <location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="172"/>
<source>Login cannot be empty</source> <source>Login cannot be empty</source>
<translation>Поле с логином не может быть пустым</translation> <translation>Поле с логином не может быть пустым</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="190"/> <location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="178"/>
<source>Password/private key cannot be empty</source> <source>Password/private key cannot be empty</source>
<translation>Поле с паролем/закрытым ключом не может быть пустым</translation> <translation>Поле с паролем/закрытым ключом не может быть пустым</translation>
</message> </message>
@@ -3656,7 +3516,7 @@ Thank you for staying with us!</source>
<context> <context>
<name>PageSetupWizardStart</name> <name>PageSetupWizardStart</name>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardStart.qml" line="42"/> <location filename="../ui/qml/Pages2/PageSetupWizardStart.qml" line="41"/>
<source>Let&apos;s get started</source> <source>Let&apos;s get started</source>
<translation>Приступим</translation> <translation>Приступим</translation>
</message> </message>
@@ -4466,22 +4326,7 @@ Thank you for staying with us!</source>
<translation>Не удалось обработать покупку</translation> <translation>Не удалось обработать покупку</translation>
</message> </message>
<message> <message>
<location filename="../core/errorstrings.cpp" line="83"/> <location filename="../core/errorstrings.cpp" line="97"/>
<source>No active subscription found</source>
<translation>Активная подписка не найдена</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="84"/>
<source>No purchased subscriptions found. Please purchase a subscription first</source>
<translation>Платные подписки не найдены. Сначала оформите подписку</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="85"/>
<source>This email address has already been used to activate a trial</source>
<translation>Этот адрес электронной почты уже использовался для активации пробного периода</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="100"/>
<source>ErrorCode: %1. </source> <source>ErrorCode: %1. </source>
<translation>Код ошибки: %1. </translation> <translation>Код ошибки: %1. </translation>
</message> </message>
@@ -4581,37 +4426,37 @@ Thank you for staying with us!</source>
<translation>Превышен лимит разрешенных конфигураций для одной подписки</translation> <translation>Превышен лимит разрешенных конфигураций для одной подписки</translation>
</message> </message>
<message> <message>
<location filename="../core/errorstrings.cpp" line="88"/> <location filename="../core/errorstrings.cpp" line="85"/>
<source>QFile error: The file could not be opened</source> <source>QFile error: The file could not be opened</source>
<translation>Ошибка QFile: не удалось открыть файл</translation> <translation>Ошибка QFile: не удалось открыть файл</translation>
</message> </message>
<message> <message>
<location filename="../core/errorstrings.cpp" line="89"/> <location filename="../core/errorstrings.cpp" line="86"/>
<source>QFile error: An error occurred when reading from the file</source> <source>QFile error: An error occurred when reading from the file</source>
<translation>Ошибка QFile: произошла ошибка при чтении из файла</translation> <translation>Ошибка QFile: произошла ошибка при чтении из файла</translation>
</message> </message>
<message> <message>
<location filename="../core/errorstrings.cpp" line="90"/> <location filename="../core/errorstrings.cpp" line="87"/>
<source>QFile error: The file could not be accessed</source> <source>QFile error: The file could not be accessed</source>
<translation>Ошибка QFile: не удалось получить доступ к файлу</translation> <translation>Ошибка QFile: не удалось получить доступ к файлу</translation>
</message> </message>
<message> <message>
<location filename="../core/errorstrings.cpp" line="91"/> <location filename="../core/errorstrings.cpp" line="88"/>
<source>QFile error: An unspecified error occurred</source> <source>QFile error: An unspecified error occurred</source>
<translation>Ошибка QFile: произошла неизвестная ошибка</translation> <translation>Ошибка QFile: произошла неизвестная ошибка</translation>
</message> </message>
<message> <message>
<location filename="../core/errorstrings.cpp" line="92"/> <location filename="../core/errorstrings.cpp" line="89"/>
<source>QFile error: A fatal error occurred</source> <source>QFile error: A fatal error occurred</source>
<translation>Ошибка QFile: произошла фатальная ошибка</translation> <translation>Ошибка QFile: произошла фатальная ошибка</translation>
</message> </message>
<message> <message>
<location filename="../core/errorstrings.cpp" line="93"/> <location filename="../core/errorstrings.cpp" line="90"/>
<source>QFile error: The operation was aborted</source> <source>QFile error: The operation was aborted</source>
<translation>Ошибка QFile: операция была прервана</translation> <translation>Ошибка QFile: операция была прервана</translation>
</message> </message>
<message> <message>
<location filename="../core/errorstrings.cpp" line="97"/> <location filename="../core/errorstrings.cpp" line="94"/>
<source>Internal error</source> <source>Internal error</source>
<translation>Внутренняя ошибка</translation> <translation>Внутренняя ошибка</translation>
</message> </message>
@@ -5140,17 +4985,7 @@ FileZilla или другие SFTP-клиенты, а также смонтир
<context> <context>
<name>ServersListView</name> <name>ServersListView</name>
<message> <message>
<location filename="../ui/qml/Components/ServersListView.qml" line="71"/> <location filename="../ui/qml/Components/ServersListView.qml" line="79"/>
<source>Subscription expired. Please renew</source>
<translation>Подписка закончилась. Пожалуйста, продлите её</translation>
</message>
<message>
<location filename="../ui/qml/Components/ServersListView.qml" line="71"/>
<source>Subscription expiring soon</source>
<translation>Подписка скоро закончится</translation>
</message>
<message>
<location filename="../ui/qml/Components/ServersListView.qml" line="84"/>
<source>Unable change server while there is an active connection</source> <source>Unable change server while there is an active connection</source>
<translation>Невозможно изменить сервер во время активного соединения</translation> <translation>Невозможно изменить сервер во время активного соединения</translation>
</message> </message>
@@ -5172,17 +5007,12 @@ FileZilla или другие SFTP-клиенты, а также смонтир
<context> <context>
<name>SettingsController</name> <name>SettingsController</name>
<message> <message>
<location filename="../ui/controllers/settingsController.cpp" line="185"/> <location filename="../ui/controllers/settingsController.cpp" line="270"/>
<source>Can&apos;t open file: %1</source>
<translation>Невозможно открыть файл: %1</translation>
</message>
<message>
<location filename="../ui/controllers/settingsController.cpp" line="271"/>
<source>All settings have been reset to default values</source> <source>All settings have been reset to default values</source>
<translation>Все настройки сброшены до значений по умолчанию</translation> <translation>Все настройки сброшены до значений по умолчанию</translation>
</message> </message>
<message> <message>
<location filename="../ui/controllers/settingsController.cpp" line="248"/> <location filename="../ui/controllers/settingsController.cpp" line="247"/>
<source>Backup file is corrupted</source> <source>Backup file is corrupted</source>
<translation>Файл резервной копии поврежден</translation> <translation>Файл резервной копии поврежден</translation>
</message> </message>
@@ -5235,29 +5065,6 @@ FileZilla или другие SFTP-клиенты, а также смонтир
<translation>Экспорт завершен</translation> <translation>Экспорт завершен</translation>
</message> </message>
</context> </context>
<context>
<name>SubscriptionExpiredDrawer</name>
<message>
<location filename="../ui/qml/Components/SubscriptionExpiredDrawer.qml" line="47"/>
<source>Amnezia Premium subscription has expired</source>
<translation>Подписка Amnezia Premium закончилась</translation>
</message>
<message>
<location filename="../ui/qml/Components/SubscriptionExpiredDrawer.qml" line="60"/>
<source>Renew to continue using VPN</source>
<translation>Продлите подписку, чтобы продолжить использовать VPN</translation>
</message>
<message>
<location filename="../ui/qml/Components/SubscriptionExpiredDrawer.qml" line="72"/>
<source>Renew</source>
<translation>Продлить</translation>
</message>
<message>
<location filename="../ui/qml/Components/SubscriptionExpiredDrawer.qml" line="96"/>
<source>Support</source>
<translation>Поддержка</translation>
</message>
</context>
<context> <context>
<name>SystemTrayNotificationHandler</name> <name>SystemTrayNotificationHandler</name>
<message> <message>
@@ -5291,14 +5098,6 @@ FileZilla или другие SFTP-клиенты, а также смонтир
<translation>Закрыть</translation> <translation>Закрыть</translation>
</message> </message>
</context> </context>
<context>
<name>TermsAndPrivacyText</name>
<message>
<location filename="../ui/qml/Components/TermsAndPrivacyText.qml" line="23"/>
<source>By continuing, you agree to the &lt;a href=&quot;%1&quot; style=&quot;color: %3;&quot;&gt;Terms of Use&lt;/a&gt; and &lt;a href=&quot;%2&quot; style=&quot;color: %3;&quot;&gt;Privacy Policy&lt;/a&gt;</source>
<translation>Продолжая, вы соглашаетесь с &lt;a href=&quot;%1&quot; style=&quot;color: %3;&quot;&gt;Условиями использования&lt;/a&gt; и &lt;a href=&quot;%2&quot; style=&quot;color: %3;&quot;&gt;Политикой конфиденциальности&lt;/a&gt;</translation>
</message>
</context>
<context> <context>
<name>TextFieldWithHeaderType</name> <name>TextFieldWithHeaderType</name>
<message> <message>
@@ -5374,12 +5173,12 @@ FileZilla или другие SFTP-клиенты, а также смонтир
<context> <context>
<name>main2</name> <name>main2</name>
<message> <message>
<location filename="../ui/qml/main2.qml" line="247"/> <location filename="../ui/qml/main2.qml" line="230"/>
<source>Private key passphrase</source> <source>Private key passphrase</source>
<translation>Парольная фраза для закрытого ключа</translation> <translation>Парольная фраза для закрытого ключа</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/main2.qml" line="268"/> <location filename="../ui/qml/main2.qml" line="251"/>
<source>Save</source> <source>Save</source>
<translation>Сохранить</translation> <translation>Сохранить</translation>
</message> </message>
@@ -488,6 +488,8 @@ bool ApiConfigsController::exportNativeConfig(const QString &serverCountryCode,
return false; return false;
} }
qDebug() << responseBody;
QJsonObject jsonConfig = QJsonDocument::fromJson(responseBody).object(); QJsonObject jsonConfig = QJsonDocument::fromJson(responseBody).object();
QString nativeConfig = jsonConfig.value(configKey::config).toString(); QString nativeConfig = jsonConfig.value(configKey::config).toString();
nativeConfig.replace("$WIREGUARD_CLIENT_PRIVATE_KEY", protocolData.wireGuardClientPrivKey); nativeConfig.replace("$WIREGUARD_CLIENT_PRIVATE_KEY", protocolData.wireGuardClientPrivKey);
@@ -661,7 +663,7 @@ bool ApiConfigsController::importPremiumFromAppStore(const QString &storeProduct
int duplicateServerIndex = -1; int duplicateServerIndex = -1;
errorCode = importServiceFromBilling(responseBody, isTestPurchase, duplicateServerIndex); errorCode = importServiceFromBilling(responseBody, isTestPurchase, duplicateServerIndex);
if (errorCode == ErrorCode::ApiConfigAlreadyAdded) { if (errorCode == ErrorCode::ApiConfigAlreadyAdded) {
emit installServerFromApiFinished(tr("This subscription has already been added"), duplicateServerIndex); emit installServerFromApiFinished(tr("This subscription is already in the app."), duplicateServerIndex);
return true; return true;
} }
if (errorCode != ErrorCode::NoError) { if (errorCode != ErrorCode::NoError) {
@@ -669,7 +671,7 @@ bool ApiConfigsController::importPremiumFromAppStore(const QString &storeProduct
return false; return false;
} }
emit installServerFromApiFinished( emit installServerFromApiFinished(
tr("%1 has been added to the app").arg(m_apiServicesModel->getSelectedServiceName())); tr("%1 was added to the app.").arg(m_apiServicesModel->getSelectedServiceName()));
return true; return true;
#else #else
Q_UNUSED(storeProductId); Q_UNUSED(storeProductId);
@@ -799,7 +801,7 @@ bool ApiConfigsController::restoreServiceFromAppStore()
if (!hasInstalledConfig) { if (!hasInstalledConfig) {
if (duplicateConfigAlreadyPresent) { if (duplicateConfigAlreadyPresent) {
emit installServerFromApiFinished(tr("This subscription has already been added"), duplicateServerIndex); emit installServerFromApiFinished(tr("This subscription is already in the app."), duplicateServerIndex);
return true; return true;
} }
@@ -866,8 +868,6 @@ bool ApiConfigsController::importFreeFromGateway()
bool ApiConfigsController::importTrialFromGateway(const QString &email) bool ApiConfigsController::importTrialFromGateway(const QString &email)
{ {
emit trialEmailError(QString());
const QString trimmedEmail = email.trimmed(); const QString trimmedEmail = email.trimmed();
if (trimmedEmail.isEmpty()) { if (trimmedEmail.isEmpty()) {
emit errorOccurred(ErrorCode::ApiConfigEmptyError); emit errorOccurred(ErrorCode::ApiConfigEmptyError);
@@ -884,6 +884,12 @@ bool ApiConfigsController::importTrialFromGateway(const QString &email)
m_apiServicesModel->getSelectedServiceProtocol(), m_apiServicesModel->getSelectedServiceProtocol(),
QJsonObject() }; QJsonObject() };
if (m_serversModel->isServerFromApiAlreadyExists(gatewayRequestData.userCountryCode, gatewayRequestData.serviceType,
gatewayRequestData.serviceProtocol)) {
emit errorOccurred(ErrorCode::ApiConfigAlreadyAdded);
return false;
}
ProtocolData protocolData = generateProtocolData(gatewayRequestData.serviceProtocol); ProtocolData protocolData = generateProtocolData(gatewayRequestData.serviceProtocol);
QJsonObject apiPayload = gatewayRequestData.toJsonObject(); QJsonObject apiPayload = gatewayRequestData.toJsonObject();
@@ -893,10 +899,6 @@ bool ApiConfigsController::importTrialFromGateway(const QString &email)
QByteArray responseBody; QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/trial"), apiPayload, responseBody); ErrorCode errorCode = executeRequest(QString("%1v1/trial"), apiPayload, responseBody);
if (errorCode != ErrorCode::NoError) { if (errorCode != ErrorCode::NoError) {
if (errorCode == ErrorCode::ApiTrialAlreadyUsedError) {
emit trialEmailError(tr("This email address has already been used to activate a trial. If you like the service, you can upgrade to Premium"));
return false;
}
emit errorOccurred(errorCode); emit errorOccurred(errorCode);
return false; return false;
} }
@@ -1027,7 +1029,7 @@ bool ApiConfigsController::updateServiceFromTelegram(const int serverIndex)
#endif #endif
GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs, GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled(), m_settings); m_settings->isStrictKillSwitchEnabled());
auto serverConfig = m_serversModel->getServerConfig(serverIndex); auto serverConfig = m_serversModel->getServerConfig(serverIndex);
auto installationUuid = m_settings->getInstallationUuid(true); auto installationUuid = m_settings->getInstallationUuid(true);
@@ -1273,6 +1275,6 @@ ErrorCode ApiConfigsController::executeRequest(const QString &endpoint, const QJ
bool isTestPurchase) bool isTestPurchase)
{ {
GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase), m_settings->isDevGatewayEnv(isTestPurchase), GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase), m_settings->isDevGatewayEnv(isTestPurchase),
apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled(), m_settings); apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled());
return gatewayController.post(endpoint, apiPayload, responseBody); return gatewayController.post(endpoint, apiPayload, responseBody);
} }
@@ -49,7 +49,6 @@ public slots:
signals: signals:
void errorOccurred(ErrorCode errorCode); void errorOccurred(ErrorCode errorCode);
void trialEmailError(const QString &message);
void subscriptionExpiredOnServer(); void subscriptionExpiredOnServer();
void subscriptionRefreshNeeded(); void subscriptionRefreshNeeded();
@@ -32,8 +32,7 @@ void ApiNewsController::fetchNews(bool showError)
} }
auto gatewayController = QSharedPointer<GatewayController>::create(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), auto gatewayController = QSharedPointer<GatewayController>::create(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(),
apiDefs::requestTimeoutMsecs, apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled());
m_settings->isStrictKillSwitchEnabled(), m_settings);
QJsonObject payload; QJsonObject payload;
payload.insert("locale", m_settings->getAppLanguage().name().split("_").first()); payload.insert("locale", m_settings->getAppLanguage().name().split("_").first());
@@ -23,19 +23,6 @@ namespace
} }
const int requestTimeoutMsecs = 12 * 1000; // 12 secs const int requestTimeoutMsecs = 12 * 1000; // 12 secs
QString getSubscriptionStatusForRenewal(const QSharedPointer<ApiAccountInfoModel> &accountInfoModel)
{
if (!accountInfoModel.isNull() && accountInfoModel->data(QStringLiteral("isSubscriptionExpired")).toBool()) {
return QStringLiteral("expired");
}
if (!accountInfoModel.isNull() && accountInfoModel->data(QStringLiteral("isSubscriptionExpiringSoon")).toBool()) {
return QStringLiteral("expire_soon");
}
return QStringLiteral("active");
}
} }
ApiSettingsController::ApiSettingsController(const QSharedPointer<ServersModel> &serversModel, ApiSettingsController::ApiSettingsController(const QSharedPointer<ServersModel> &serversModel,
@@ -71,7 +58,7 @@ bool ApiSettingsController::getAccountInfo(bool reload)
bool isTestPurchase = apiConfig.value(apiDefs::key::isTestPurchase).toBool(false); bool isTestPurchase = apiConfig.value(apiDefs::key::isTestPurchase).toBool(false);
GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase), m_settings->isDevGatewayEnv(isTestPurchase), GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase), m_settings->isDevGatewayEnv(isTestPurchase),
requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled(), m_settings); requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled());
QJsonObject apiPayload; QJsonObject apiPayload;
apiPayload[configKey::userCountryCode] = apiConfig.value(configKey::userCountryCode).toString(); apiPayload[configKey::userCountryCode] = apiConfig.value(configKey::userCountryCode).toString();
@@ -110,7 +97,7 @@ void ApiSettingsController::getRenewalLink()
auto gatewayController = QSharedPointer<GatewayController>::create(m_settings->getGatewayEndpoint(isTestPurchase), auto gatewayController = QSharedPointer<GatewayController>::create(m_settings->getGatewayEndpoint(isTestPurchase),
m_settings->isDevGatewayEnv(isTestPurchase), m_settings->isDevGatewayEnv(isTestPurchase),
requestTimeoutMsecs, requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled(), m_settings); m_settings->isStrictKillSwitchEnabled());
QJsonObject apiPayload; QJsonObject apiPayload;
apiPayload[configKey::userCountryCode] = apiConfig.value(configKey::userCountryCode).toString(); apiPayload[configKey::userCountryCode] = apiConfig.value(configKey::userCountryCode).toString();
@@ -118,7 +105,6 @@ void ApiSettingsController::getRenewalLink()
apiPayload[configKey::authData] = authData; apiPayload[configKey::authData] = authData;
apiPayload[apiDefs::key::cliVersion] = QString(APP_VERSION); apiPayload[apiDefs::key::cliVersion] = QString(APP_VERSION);
apiPayload[apiDefs::key::appLanguage] = m_settings->getAppLanguage().name().split("_").first(); apiPayload[apiDefs::key::appLanguage] = m_settings->getAppLanguage().name().split("_").first();
apiPayload[apiDefs::key::subscriptionStatus] = getSubscriptionStatusForRenewal(m_apiAccountInfoModel);
auto future = gatewayController->postAsync(QString("%1v1/renewal_link"), apiPayload); auto future = gatewayController->postAsync(QString("%1v1/renewal_link"), apiPayload);
future.then(this, [this, gatewayController](QPair<ErrorCode, QByteArray> result) { future.then(this, [this, gatewayController](QPair<ErrorCode, QByteArray> result) {
+1 -10
View File
@@ -217,8 +217,6 @@ bool ImportController::extractConfigFromData(QString data)
bool ImportController::extractConfigFromQr(const QByteArray &data) bool ImportController::extractConfigFromQr(const QByteArray &data)
{ {
m_configType = checkConfigFormat(QString::fromUtf8(data));
QJsonObject dataObj = QJsonDocument::fromJson(data).object(); QJsonObject dataObj = QJsonDocument::fromJson(data).object();
if (!dataObj.isEmpty()) { if (!dataObj.isEmpty()) {
m_config = dataObj; m_config = dataObj;
@@ -228,13 +226,10 @@ bool ImportController::extractConfigFromQr(const QByteArray &data)
QByteArray ba_uncompressed = qUncompress(data); QByteArray ba_uncompressed = qUncompress(data);
if (!ba_uncompressed.isEmpty()) { if (!ba_uncompressed.isEmpty()) {
m_config = QJsonDocument::fromJson(ba_uncompressed).object(); m_config = QJsonDocument::fromJson(ba_uncompressed).object();
if (m_config.isEmpty()) {
return false;
}
m_configType = checkConfigFormat(QString::fromUtf8(ba_uncompressed));
return true; return true;
} }
m_configType = checkConfigFormat(data);
if (m_configType == ConfigTypes::Invalid) { if (m_configType == ConfigTypes::Invalid) {
QByteArray ba = QByteArray::fromBase64(data, QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); QByteArray ba = QByteArray::fromBase64(data, QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
QByteArray baUncompressed = qUncompress(ba); QByteArray baUncompressed = qUncompress(ba);
@@ -245,10 +240,6 @@ bool ImportController::extractConfigFromQr(const QByteArray &data)
if (!ba.isEmpty()) { if (!ba.isEmpty()) {
m_config = QJsonDocument::fromJson(ba).object(); m_config = QJsonDocument::fromJson(ba).object();
if (m_config.isEmpty()) {
return false;
}
m_configType = checkConfigFormat(QString::fromUtf8(ba));
return true; return true;
} }
} }
+6 -5
View File
@@ -32,9 +32,8 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
return tr("Active"); return tr("Active");
} }
return apiUtils::isSubscriptionExpired(m_accountInfoData.subscriptionEndDate) return apiUtils::isSubscriptionExpired(m_accountInfoData.subscriptionEndDate) ? tr("<p><a style=\"color: #EB5757;\">Inactive</a>")
? QStringLiteral("<p><a style=\"color: #EB5757;\">%1</a>").arg(tr("Inactive")) : tr("<p><a style=\"color: #28c840;\">Active</a>");
: QStringLiteral("<p><a style=\"color: #28c840;\">%1</a>").arg(tr("Active"));
} }
case EndDateRole: { case EndDateRole: {
if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) { if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) {
@@ -54,11 +53,14 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
} }
case IsComponentVisibleRole: { case IsComponentVisibleRole: {
return m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2 return m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2
|| m_accountInfoData.configType == apiDefs::ConfigType::AmneziaTrialV2
|| m_accountInfoData.configType == apiDefs::ConfigType::ExternalPremium || m_accountInfoData.configType == apiDefs::ConfigType::ExternalPremium
|| m_accountInfoData.configType == apiDefs::ConfigType::ExternalTrial; || m_accountInfoData.configType == apiDefs::ConfigType::ExternalTrial;
} }
case IsSubscriptionRenewalAvailableRole: { case IsSubscriptionRenewalAvailableRole: {
return m_accountInfoData.isRenewalAvailable; return m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2
|| m_accountInfoData.configType == apiDefs::ConfigType::AmneziaTrialV2
|| m_accountInfoData.configType == apiDefs::ConfigType::ExternalTrial;
} }
case HasExpiredWorkerRole: { case HasExpiredWorkerRole: {
for (int i = 0; i < m_issuedConfigsInfo.size(); i++) { for (int i = 0; i < m_issuedConfigsInfo.size(); i++) {
@@ -130,7 +132,6 @@ void ApiAccountInfoModel::updateModel(const QJsonObject &accountInfoObject, cons
accountInfoData.isInAppPurchase = apiConfig.value(apiDefs::key::isInAppPurchase).toBool(false); accountInfoData.isInAppPurchase = apiConfig.value(apiDefs::key::isInAppPurchase).toBool(false);
accountInfoData.subscriptionDescription = accountInfoObject.value(apiDefs::key::subscriptionDescription).toString(); accountInfoData.subscriptionDescription = accountInfoObject.value(apiDefs::key::subscriptionDescription).toString();
accountInfoData.isRenewalAvailable = accountInfoObject.value(apiDefs::key::isRenewalAvailable).toBool(false);
for (const auto &protocol : accountInfoObject.value(apiDefs::key::supportedProtocols).toArray()) { for (const auto &protocol : accountInfoObject.value(apiDefs::key::supportedProtocols).toArray()) {
accountInfoData.supportedProtocols.push_back(protocol.toString()); accountInfoData.supportedProtocols.push_back(protocol.toString());
@@ -61,7 +61,6 @@ private:
QString subscriptionDescription; QString subscriptionDescription;
bool isInAppPurchase = false; bool isInAppPurchase = false;
bool isRenewalAvailable = false;
}; };
AccountInfoData m_accountInfoData; AccountInfoData m_accountInfoData;
+5 -9
View File
@@ -12,7 +12,7 @@ namespace configKey
constexpr char title[] = "title"; constexpr char title[] = "title";
constexpr char body[] = "body"; constexpr char body[] = "body";
constexpr char icon[] = "icon"; constexpr char icon[] = "icon";
constexpr char link[] = "link"; constexpr char accent[] = "accent";
} }
QString gatewayIconKeyToUrl(const QString &iconKey) QString gatewayIconKeyToUrl(const QString &iconKey)
@@ -62,8 +62,8 @@ QVariant ApiBenefitsModel::data(const QModelIndex &index, int role) const
return item.title; return item.title;
case BodyRole: case BodyRole:
return item.body; return item.body;
case LinkRole: case AccentRole:
return item.link; return item.accent;
default: default:
return {}; return {};
} }
@@ -75,7 +75,7 @@ QHash<int, QByteArray> ApiBenefitsModel::roleNames() const
{ IconRole, "icon" }, { IconRole, "icon" },
{ TitleRole, "title" }, { TitleRole, "title" },
{ BodyRole, "body" }, { BodyRole, "body" },
{ LinkRole, "link" }, { AccentRole, "accent" },
}; };
} }
@@ -90,11 +90,7 @@ void ApiBenefitsModel::updateModel(const QJsonArray &benefits)
const QJsonObject benefitObject = benefitValue.toObject(); const QJsonObject benefitObject = benefitValue.toObject();
QString title = benefitObject.value(configKey::title).toString(); QString title = benefitObject.value(configKey::title).toString();
QString body = benefitObject.value(configKey::body).toString(); QString body = benefitObject.value(configKey::body).toString();
const bool isLink = benefitObject.value(configKey::link).toBool();
const QString iconKey = benefitObject.value(configKey::icon).toString(); const QString iconKey = benefitObject.value(configKey::icon).toString();
if (isLink) {
body = body.trimmed();
}
if (title.isEmpty() && body.isEmpty()) { if (title.isEmpty() && body.isEmpty()) {
continue; continue;
} }
@@ -102,7 +98,7 @@ void ApiBenefitsModel::updateModel(const QJsonArray &benefits)
item.icon = gatewayIconKeyToUrl(iconKey); item.icon = gatewayIconKeyToUrl(iconKey);
item.title = std::move(title); item.title = std::move(title);
item.body = std::move(body); item.body = std::move(body);
item.link = isLink; item.accent = benefitObject.value(configKey::accent).toBool();
m_serviceBenefits.append(std::move(item)); m_serviceBenefits.append(std::move(item));
} }
endResetModel(); endResetModel();
+2 -2
View File
@@ -15,7 +15,7 @@ public:
IconRole = Qt::UserRole + 1, IconRole = Qt::UserRole + 1,
TitleRole, TitleRole,
BodyRole, BodyRole,
LinkRole AccentRole
}; };
Q_ENUM(Roles) Q_ENUM(Roles)
@@ -34,7 +34,7 @@ private:
QString icon; QString icon;
QString title; QString title;
QString body; QString body;
bool link = false; bool accent = false;
}; };
QVector<ServiceBenefitItem> m_serviceBenefits; QVector<ServiceBenefitItem> m_serviceBenefits;
@@ -91,12 +91,6 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
} }
return true; return true;
} }
case IsPremiumRole: {
return serviceType == serviceType::amneziaPremium;
}
case HasSubscriptionPlansRole: {
return !apiServiceData.subscriptionPlansJson.isEmpty();
}
case PriceRole: { case PriceRole: {
return apiServiceData.minPriceLabel; return apiServiceData.minPriceLabel;
} }
@@ -239,8 +233,6 @@ QHash<int, QByteArray> ApiServicesModel::roleNames() const
roles[CardDescriptionRole] = "cardDescription"; roles[CardDescriptionRole] = "cardDescription";
roles[ServiceDescriptionRole] = "serviceDescription"; roles[ServiceDescriptionRole] = "serviceDescription";
roles[IsServiceAvailableRole] = "isServiceAvailable"; roles[IsServiceAvailableRole] = "isServiceAvailable";
roles[IsPremiumRole] = "isPremium";
roles[HasSubscriptionPlansRole] = "hasSubscriptionPlans";
roles[PriceRole] = "price"; roles[PriceRole] = "price";
roles[EndDateRole] = "endDate"; roles[EndDateRole] = "endDate";
roles[TermsOfUseUrlRole] = "termsOfUseUrl"; roles[TermsOfUseUrlRole] = "termsOfUseUrl";
-2
View File
@@ -54,8 +54,6 @@ public:
CardDescriptionRole, CardDescriptionRole,
ServiceDescriptionRole, ServiceDescriptionRole,
IsServiceAvailableRole, IsServiceAvailableRole,
IsPremiumRole,
HasSubscriptionPlansRole,
PriceRole, PriceRole,
EndDateRole, EndDateRole,
TermsOfUseUrlRole, TermsOfUseUrlRole,
-4
View File
@@ -179,9 +179,6 @@ QVariant ServersModel::data(const QModelIndex &index, int role) const
case AdEndpointRole: { case AdEndpointRole: {
return apiConfig.value(apiDefs::key::serviceInfo).toObject().value(apiDefs::key::adEndpoint).toString(); return apiConfig.value(apiDefs::key::serviceInfo).toObject().value(apiDefs::key::adEndpoint).toString();
} }
case IsRenewalAvailableRole: {
return apiConfig.value(apiDefs::key::serviceInfo).toObject().value(apiDefs::key::isRenewalAvailable).toBool(false);
}
case IsSubscriptionExpiredRole: { case IsSubscriptionExpiredRole: {
if (configVersion != apiDefs::ConfigSource::AmneziaGateway) { if (configVersion != apiDefs::ConfigSource::AmneziaGateway) {
return false; return false;
@@ -476,7 +473,6 @@ QHash<int, QByteArray> ServersModel::roleNames() const
roles[AdHeaderRole] = "adHeader"; roles[AdHeaderRole] = "adHeader";
roles[AdDescriptionRole] = "adDescription"; roles[AdDescriptionRole] = "adDescription";
roles[AdEndpointRole] = "adEndpoint"; roles[AdEndpointRole] = "adEndpoint";
roles[IsRenewalAvailableRole] = "isRenewalAvailable";
roles[IsSubscriptionExpiredRole] = "isSubscriptionExpired"; roles[IsSubscriptionExpiredRole] = "isSubscriptionExpired";
roles[IsSubscriptionExpiringSoonRole] = "isSubscriptionExpiringSoon"; roles[IsSubscriptionExpiringSoonRole] = "isSubscriptionExpiringSoon";
-1
View File
@@ -51,7 +51,6 @@ public:
AdHeaderRole, AdHeaderRole,
AdDescriptionRole, AdDescriptionRole,
AdEndpointRole, AdEndpointRole,
IsRenewalAvailableRole,
IsSubscriptionExpiredRole, IsSubscriptionExpiredRole,
IsSubscriptionExpiringSoonRole, IsSubscriptionExpiringSoonRole,
+6 -3
View File
@@ -5,16 +5,19 @@
#include <QDebug> #include <QDebug>
#include "notificationhandler.h" #include "notificationhandler.h"
#if defined(Q_OS_IOS) #if defined(Q_OS_ANDROID)
# include "platforms/android/android_notificationhandler.h"
#elif defined(Q_OS_IOS)
# include "platforms/ios/iosnotificationhandler.h" # include "platforms/ios/iosnotificationhandler.h"
#else #else
# include "systemtray_notificationhandler.h" # include "systemtray_notificationhandler.h"
#endif #endif
// static // static
NotificationHandler* NotificationHandler::create(QObject* parent) { NotificationHandler* NotificationHandler::create(QObject* parent) {
#if defined(Q_OS_IOS) #if defined(Q_OS_ANDROID)
return new AndroidNotificationHandler(parent);
#elif defined(Q_OS_IOS)
return new IOSNotificationHandler(parent); return new IOSNotificationHandler(parent);
#else #else
return new SystemTrayNotificationHandler(parent); return new SystemTrayNotificationHandler(parent);
+10 -9
View File
@@ -11,11 +11,7 @@ RowLayout {
property string iconSource: "" property string iconSource: ""
property string titleText: "" property string titleText: ""
property string bodyText: "" property string bodyText: ""
property bool link: false property bool accent: false
readonly property string bodyLineText: root.link && root.bodyText.length > 0 ? "@" + root.bodyText : root.bodyText
readonly property bool bodyClickable: root.link && root.bodyText.length > 0
spacing: 12 spacing: 12
@@ -47,17 +43,22 @@ RowLayout {
LabelTextType { LabelTextType {
id: bodyLabel id: bodyLabel
width: parent.width width: parent.width
text: root.bodyLineText text: root.bodyText
color: root.link ? AmneziaStyle.color.goldenApricot : AmneziaStyle.color.mutedGray color: root.accent ? AmneziaStyle.color.goldenApricot : AmneziaStyle.color.mutedGray
font.pixelSize: 14 font.pixelSize: 14
wrapMode: Text.Wrap wrapMode: Text.Wrap
} }
MouseArea { MouseArea {
anchors.fill: bodyLabel anchors.fill: bodyLabel
visible: root.bodyClickable visible: root.accent && root.bodyText.length > 0
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: Qt.openUrlExternally("https://t.me/" + root.bodyText) onClicked: {
var t = root.bodyText.trim()
if (t.startsWith("@")) {
Qt.openUrlExternally("https://t.me/" + t.substring(1))
}
}
} }
} }
} }
+1 -1
View File
@@ -33,7 +33,7 @@ Rectangle {
iconSource: model.icon iconSource: model.icon
titleText: model.title titleText: model.title
bodyText: model.body bodyText: model.body
link: !!model.link accent: !!model.accent
} }
} }
} }
+1 -1
View File
@@ -68,7 +68,7 @@ ListViewType {
text: name text: name
descriptionText: isServerFromGatewayApi && (isSubscriptionExpired || isSubscriptionExpiringSoon) descriptionText: isServerFromGatewayApi && (isSubscriptionExpired || isSubscriptionExpiringSoon)
? (isSubscriptionExpired ? qsTr("Subscription expired. Please renew") : qsTr("Subscription expiring soon")) ? (isSubscriptionExpired ? qsTr("Subscription expired. Please renew.") : qsTr("Subscription expiring soon."))
: serverDescription : serverDescription
descriptionColor: isServerFromGatewayApi && (isSubscriptionExpired || isSubscriptionExpiringSoon) descriptionColor: isServerFromGatewayApi && (isSubscriptionExpired || isSubscriptionExpiringSoon)
? (isSubscriptionExpired ? AmneziaStyle.color.vibrantRed : AmneziaStyle.color.goldenApricot) ? (isSubscriptionExpired ? AmneziaStyle.color.vibrantRed : AmneziaStyle.color.goldenApricot)
@@ -12,10 +12,11 @@ import "../Controls2/TextTypes"
DrawerType2 { DrawerType2 {
id: root id: root
property bool isRenewalAvailable: false property bool isRenewalActionAvailable: false
onOpened: { onOpened: {
isRenewalAvailable = ServersModel.getProcessedServerData("isRenewalAvailable") && !ApiAccountInfoModel.data("isInAppPurchase") isRenewalActionAvailable = ApiAccountInfoModel.data("isSubscriptionRenewalAvailable")
&& !ApiAccountInfoModel.data("isInAppPurchase")
} }
expandedStateContent: ColumnLayout { expandedStateContent: ColumnLayout {
@@ -43,25 +44,25 @@ DrawerType2 {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
text: ServersModel.getProcessedServerData("name") + qsTr(" subscription has expired") text: qsTr("Amnezia Premium subscription has expired")
horizontalAlignment: Text.AlignLeft horizontalAlignment: Text.AlignLeft
} }
} }
ParagraphTextType { ParagraphTextType {
visible: root.isRenewalAvailable visible: root.isRenewalActionAvailable
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 8 Layout.topMargin: 8
Layout.rightMargin: 16 Layout.rightMargin: 16
Layout.leftMargin: 16 Layout.leftMargin: 16
text: qsTr("Renew to continue using VPN") text: qsTr("Renew your subscription to continue using VPN")
horizontalAlignment: Text.AlignLeft horizontalAlignment: Text.AlignLeft
} }
BasicButtonType { BasicButtonType {
visible: root.isRenewalAvailable visible: root.isRenewalActionAvailable
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 16 Layout.topMargin: 16
@@ -95,14 +96,9 @@ DrawerType2 {
text: qsTr("Support") text: qsTr("Support")
clickedFunc: function() { clickedFunc: function() {
PageController.showBusyIndicator(true)
let result = ApiSettingsController.getAccountInfo(false)
PageController.showBusyIndicator(false)
if (result) {
root.closeTriggered() root.closeTriggered()
PageController.goToPage(PageEnum.PageSettingsApiSupport) PageController.goToPage(PageEnum.PageSettingsApiSupport)
} }
} }
} }
}
} }
@@ -17,8 +17,6 @@ ParagraphTextType {
textFormat: Text.RichText textFormat: Text.RichText
color: AmneziaStyle.color.mutedGray color: AmneziaStyle.color.mutedGray
font.pixelSize: 12 font.pixelSize: 12
lineHeight: 1.35
lineHeightMode: Text.ProportionalHeight
text: qsTr("By continuing, you agree to the <a href=\"%1\" style=\"color: %3;\">Terms of Use</a> and <a href=\"%2\" style=\"color: %3;\">Privacy Policy</a>") text: qsTr("By continuing, you agree to the <a href=\"%1\" style=\"color: %3;\">Terms of Use</a> and <a href=\"%2\" style=\"color: %3;\">Privacy Policy</a>")
.arg(root.termsUrl) .arg(root.termsUrl)
+44 -11
View File
@@ -28,20 +28,20 @@ Button {
property string leftImageSource property string leftImageSource
property real textOpacity: 1.0
property alias focusItem: rightImage property alias focusItem: rightImage
hoverEnabled: true hoverEnabled: true
clip: false clip: false
readonly property real cardTextOpacity: !enabled ? 1.0 : pressed ? 0.7 : hovered ? 0.8 : 1.0
background: Rectangle { background: Rectangle {
id: backgroundRect id: backgroundRect
anchors.fill: parent anchors.fill: parent
radius: 16 radius: 16
color: root.hovered && root.enabled ? root.hoveredColor : root.defaultColor color: defaultColor
Behavior on color { Behavior on color {
PropertyAnimation { duration: 200 } PropertyAnimation { duration: 200 }
@@ -51,7 +51,6 @@ Button {
contentItem: Item { contentItem: Item {
id: contentRoot id: contentRoot
z: 1
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
@@ -130,7 +129,7 @@ Button {
Layout.topMargin: contentRoot.badgeVisible ? 0 : 16 Layout.topMargin: contentRoot.badgeVisible ? 0 : 16
Layout.bottomMargin: root.bodyText !== "" ? 0 : 16 Layout.bottomMargin: root.bodyText !== "" ? 0 : 16
opacity: root.cardTextOpacity opacity: root.textOpacity
} }
CaptionTextType { CaptionTextType {
@@ -139,16 +138,13 @@ Button {
color: root.bodyTextColor color: root.bodyTextColor
textFormat: Text.RichText textFormat: Text.RichText
onLinkActivated: function(link) {
Qt.openUrlExternally(link)
}
Layout.fillWidth: true Layout.fillWidth: true
Layout.rightMargin: 16 Layout.rightMargin: 16
Layout.leftMargin: 16 Layout.leftMargin: 16
Layout.bottomMargin: root.footerText !== "" ? 0 : 8 Layout.bottomMargin: root.footerText !== "" ? 0 : 8
opacity: root.cardTextOpacity opacity: root.textOpacity
} }
ButtonTextType { ButtonTextType {
@@ -163,7 +159,7 @@ Button {
Layout.topMargin: 16 Layout.topMargin: 16
Layout.bottomMargin: 16 Layout.bottomMargin: 16
opacity: root.cardTextOpacity opacity: root.textOpacity
} }
} }
@@ -188,7 +184,7 @@ Button {
anchors.fill: parent anchors.fill: parent
radius: 12 radius: 12
color: root.pressed ? rightImage.pressedColor : root.hovered ? rightImage.hoveredColor : rightImage.defaultColor color: "transparent"
Behavior on color { Behavior on color {
PropertyAnimation { duration: 200 } PropertyAnimation { duration: 200 }
@@ -202,4 +198,41 @@ Button {
} }
} }
} }
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
enabled: root.enabled
onEntered: {
backgroundRect.color = root.hoveredColor
if (rightImageSource) {
rightImageBackground.color = rightImage.hoveredColor
}
root.textOpacity = 0.8
}
onExited: {
backgroundRect.color = root.defaultColor
if (rightImageSource) {
rightImageBackground.color = rightImage.defaultColor
}
root.textOpacity = 1
}
onPressedChanged: {
if (rightImageSource) {
rightImageBackground.color = pressed ? rightImage.pressedColor : entered ? rightImage.hoveredColor : rightImage.defaultColor
}
root.textOpacity = 0.7
}
onClicked: {
root.clicked()
}
}
} }
+1 -1
View File
@@ -153,7 +153,7 @@ Switch {
Keys.onSpacePressed: event => handleSwitch(event) Keys.onSpacePressed: event => handleSwitch(event)
function handleSwitch(event) { function handleSwitch(event) {
if (root.enabled && !event.isAutoRepeat) { if (!event.isAutoRepeat) {
root.checked = !root.checked root.checked = !root.checked
root.toggled() root.toggled()
} }
+6 -18
View File
@@ -2,6 +2,8 @@ import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import SortFilterProxyModel 0.2
import PageEnum 1.0 import PageEnum 1.0
import Style 1.0 import Style 1.0
@@ -50,8 +52,6 @@ PageType {
width: listView.width width: listView.width
TextFieldWithHeaderType { TextFieldWithHeaderType {
id: gatewayEndpointField
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 16 Layout.topMargin: 16
Layout.rightMargin: 16 Layout.rightMargin: 16
@@ -64,25 +64,13 @@ PageType {
clickedFunc: function() { clickedFunc: function() {
SettingsController.resetGatewayEndpoint() SettingsController.resetGatewayEndpoint()
gatewayEndpointField.textField.text = SettingsController.gatewayEndpoint
}
} }
BasicButtonType { textField.onEditingFinished: {
id: saveButton textField.text = textField.text.replace(/^\s+|\s+$/g, '')
if (textField.text !== SettingsController.gatewayEndpoint) {
Layout.fillWidth: true SettingsController.gatewayEndpoint = textField.text
Layout.margins: 16
text: qsTr("Save")
clickedFunc: function() {
var trimmed = gatewayEndpointField.textField.text.replace(/^\s+|\s+$/g, '')
gatewayEndpointField.textField.text = trimmed
if (trimmed !== SettingsController.gatewayEndpoint) {
SettingsController.gatewayEndpoint = trimmed
} }
PageController.showNotificationMessage(qsTr("Settings saved"))
} }
} }
} }
@@ -482,7 +482,6 @@ PageType {
headerText: qsTr("I5 - Special junk 5") headerText: qsTr("I5 - Special junk 5")
textField.text: serverSpecialJunk5 textField.text: serverSpecialJunk5
checkEmptyText: false
textField.onEditingFinished: { textField.onEditingFinished: {
if (textField.text !== serverSpecialJunk5) { if (textField.text !== serverSpecialJunk5) {
@@ -259,7 +259,6 @@ PageType {
id: switcher id: switcher
readonly property bool isVlessProtocol: ApiConfigsController.isVlessProtocol() readonly property bool isVlessProtocol: ApiConfigsController.isVlessProtocol()
readonly property bool isProtocolSwitchBlocked: ServersModel.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 24 Layout.topMargin: 24
@@ -267,7 +266,6 @@ PageType {
Layout.leftMargin: 16 Layout.leftMargin: 16
visible: ApiAccountInfoModel.data("isProtocolSelectionSupported") visible: ApiAccountInfoModel.data("isProtocolSelectionSupported")
enabled: !switcher.isProtocolSwitchBlocked
text: qsTr("Use VLESS protocol") text: qsTr("Use VLESS protocol")
checked: switcher.isVlessProtocol checked: switcher.isVlessProtocol
@@ -67,11 +67,8 @@ PageType {
} }
delegate: ColumnLayout { delegate: ColumnLayout {
property bool hideCard: isPremium && !hasSubscriptionPlans
width: listView.width width: listView.width
visible: !hideCard
height: hideCard ? 0 : implicitHeight
enabled: isServiceAvailable enabled: isServiceAvailable
@@ -13,16 +13,6 @@ import "../Components"
PageType { PageType {
id: root id: root
property string trialEmailErrorMessage: ""
Connections {
target: ApiConfigsController
function onTrialEmailError(message) {
root.trialEmailErrorMessage = message
emailField.errorText = message
}
}
BackButtonType { BackButtonType {
id: backButton id: backButton
@@ -77,17 +67,6 @@ PageType {
headerText: qsTr("Email") headerText: qsTr("Email")
textField.placeholderText: qsTr("Email") textField.placeholderText: qsTr("Email")
textField.inputMethodHints: Qt.ImhEmailCharactersOnly textField.inputMethodHints: Qt.ImhEmailCharactersOnly
Connections {
target: emailField.textField
function onTextChanged() {
if (root.trialEmailErrorMessage !== "") {
root.trialEmailErrorMessage = ""
emailField.errorText = ""
}
}
}
} }
ParagraphTextType { ParagraphTextType {
@@ -99,7 +78,7 @@ PageType {
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
color: AmneziaStyle.color.mutedGray color: AmneziaStyle.color.mutedGray
font.pixelSize: 12 font.pixelSize: 12
text: qsTr("We will create an account for your trial subscription and send important subscription updates to this email address") text: qsTr("We will create an account for your trial subscription and send important subscription updates to this email.")
} }
} }
} }
@@ -118,9 +97,6 @@ PageType {
text: qsTr("Continue") text: qsTr("Continue")
clickedFunc: function() { clickedFunc: function() {
root.trialEmailErrorMessage = ""
emailField.errorText = ""
var raw = emailField.textField.text.trim() var raw = emailField.textField.text.trim()
if (raw.length === 0 || raw.indexOf("@") < 0) { if (raw.length === 0 || raw.indexOf("@") < 0) {
PageController.showNotificationMessage(qsTr("Enter a valid email address")) PageController.showNotificationMessage(qsTr("Enter a valid email address"))
@@ -278,7 +278,7 @@ PageType {
id: amneziaVpn id: amneziaVpn
property string title: qsTr("VPN by Amnezia") property string title: qsTr("VPN by Amnezia")
property string description: qsTr("The easiest way to connect to the VPN") property string description: qsTr("The easiest way to connect to VPN")
property string imageSource: "qrc:/images/controls/amnezia.svg" property string imageSource: "qrc:/images/controls/amnezia.svg"
property bool featuredAmneziaConnection: true property bool featuredAmneziaConnection: true
property bool isVisible: true property bool isVisible: true
@@ -94,7 +94,7 @@ PageType {
visible: title === qsTr("Password or SSH private key") visible: title === qsTr("Password or SSH private key")
backGroundColor: AmneziaStyle.color.translucentWhite backGroundColor: AmneziaStyle.color.translucentWhite
iconPath: "qrc:/images/controls/alert-circle.svg" iconPath: "qrc:/images/controls/alert-circle.svg"
textString: qsTr("SSH key requirements: supported key types are ED25519 and RSA in PEM format. Paste the private key, including the BEGIN/END lines. If your key doesnt work, generate a compatible one") textString: qsTr("SSH key requirements: supported ED25519 or RSA in PEM. Paste the private key including BEGIN/END lines. If your key doesnt work, generate a compatible one.")
} }
} }
@@ -10,6 +10,10 @@
# include "platforms/macos/macosutils.h" # include "platforms/macos/macosutils.h"
#endif #endif
#ifdef MACOS_NE
# include "platforms/macos/macos_ne_vpn_notification.h"
#endif
#include <QApplication> #include <QApplication>
#include <QDesktopServices> #include <QDesktopServices>
#include <QIcon> #include <QIcon>
@@ -152,6 +156,12 @@ void SystemTrayNotificationHandler::notify(NotificationHandler::Message type,
int timerMsec) { int timerMsec) {
Q_UNUSED(type); Q_UNUSED(type);
#ifdef MACOS_NE
Q_UNUSED(timerMsec);
macosNePostVpnStateNotification(title, message);
return;
#endif
QIcon icon(ConnectedTrayIconName); QIcon icon(ConnectedTrayIconName);
m_systemTrayIcon.showMessage(title, message, icon, timerMsec); m_systemTrayIcon.showMessage(title, message, icon, timerMsec);
} }
+17
View File
@@ -97,6 +97,17 @@ echo "Using Qt in $QT_BIN_DIR"
echo "Using Android SDK in $ANDROID_SDK_ROOT" echo "Using Android SDK in $ANDROID_SDK_ROOT"
echo "Using Android NDK in $ANDROID_NDK_ROOT" echo "Using Android NDK in $ANDROID_NDK_ROOT"
if [[ -z "${ANDROID_NDK_ROOT:-}" ]]; then
echo "ANDROID_NDK_ROOT is not set. Example: export ANDROID_NDK_ROOT=\"\$HOME/Library/Android/sdk/ndk/<version>\""
exit 1
fi
NDK_TOOLCHAIN="$ANDROID_NDK_ROOT/build/cmake/android.toolchain.cmake"
if [[ ! -f "$NDK_TOOLCHAIN" ]]; then
echo "Missing NDK CMake toolchain: $NDK_TOOLCHAIN"
echo "Install a complete NDK (SDK Manager → NDK Side by side) or fix ANDROID_NDK_ROOT."
exit 1
fi
# Run qt-cmake to configure build # Run qt-cmake to configure build
qt_cmake_opts=() qt_cmake_opts=()
@@ -106,6 +117,12 @@ else
qt_cmake_opts+=(-DQT_ANDROID_ABIS="$ABIS") qt_cmake_opts+=(-DQT_ANDROID_ABIS="$ABIS")
fi fi
# Pin SDK/NDK on every configure so CMakeCache does not keep a stale NDK after you change ANDROID_NDK_ROOT.
if [[ -n "${ANDROID_SDK_ROOT:-}" ]]; then
qt_cmake_opts+=(-DANDROID_SDK_ROOT="$ANDROID_SDK_ROOT")
fi
qt_cmake_opts+=(-DANDROID_NDK_ROOT="$ANDROID_NDK_ROOT")
# QT_NO_GLOBAL_APK_TARGET_PART_OF_ALL=ON - Skip building apks as part of the default 'ALL' target # QT_NO_GLOBAL_APK_TARGET_PART_OF_ALL=ON - Skip building apks as part of the default 'ALL' target
# We'll build apks during androiddeployqt # We'll build apks during androiddeployqt
$QT_BIN_DIR/qt-cmake -S $PROJECT_DIR -B $BUILD_DIR \ $QT_BIN_DIR/qt-cmake -S $PROJECT_DIR -B $BUILD_DIR \