mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-21 02:01:03 +07:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 541437e83b | |||
| ccf8b63633 | |||
| af3a0e1701 | |||
| d3eaead779 | |||
| 6249eea905 | |||
| fa0ba4afd4 | |||
| 15fb5b20f0 | |||
| 4b625fe70f | |||
| a4b97e8764 | |||
| 285b9344c4 | |||
| 2db1416d9f | |||
| bae2dd452b | |||
| 041219187b | |||
| a231bf9ab7 | |||
| c29984ce60 | |||
| cb5cde1a37 | |||
| 7da5f1c368 | |||
| ea645df7ec | |||
| 8799d841a3 | |||
| 5a51814b2a | |||
| 4531a0b4b7 | |||
| b9f4ff56d8 | |||
| edc42fb7e4 | |||
| 4e4f8a5ec5 | |||
| 6de556e730 | |||
| 1134dc194b |
@@ -17,7 +17,6 @@ jobs:
|
||||
QIF_VERSION: 4.7
|
||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||
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_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
||||
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
||||
@@ -99,7 +98,6 @@ jobs:
|
||||
BUILD_ARCH: 64
|
||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||
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_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
||||
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
||||
@@ -206,7 +204,6 @@ jobs:
|
||||
CXX: c++
|
||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||
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_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
||||
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
||||
@@ -321,7 +318,6 @@ jobs:
|
||||
|
||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||
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_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
||||
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
||||
@@ -399,7 +395,6 @@ jobs:
|
||||
|
||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||
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_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
||||
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
||||
@@ -482,7 +477,6 @@ jobs:
|
||||
|
||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||
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_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
||||
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
||||
@@ -547,11 +541,10 @@ jobs:
|
||||
|
||||
env:
|
||||
ANDROID_BUILD_PLATFORM: android-36
|
||||
QT_VERSION: 6.11.1
|
||||
QT_VERSION: 6.10.1
|
||||
QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools'
|
||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||
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_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
||||
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
||||
|
||||
@@ -17,7 +17,6 @@ jobs:
|
||||
QIF_VERSION: 4.5
|
||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||
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_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
||||
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
|
||||
|
||||
set(PROJECT AmneziaVPN)
|
||||
set(AMNEZIAVPN_VERSION 4.8.18.0)
|
||||
set(AMNEZIAVPN_VERSION 4.8.15.0)
|
||||
|
||||
project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
|
||||
DESCRIPTION "AmneziaVPN"
|
||||
@@ -12,7 +12,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
|
||||
set(RELEASE_DATE "${CURRENT_DATE}")
|
||||
|
||||
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
|
||||
set(APP_ANDROID_VERSION_CODE 2128)
|
||||
set(APP_ANDROID_VERSION_CODE 2118)
|
||||
|
||||
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
||||
set(MZ_PLATFORM_NAME "linux")
|
||||
|
||||
+1
-1
Submodule client/3rd-prebuilt updated: 4680bd8fb4...51bb4703a4
@@ -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_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_ENDPOINT="$ENV{DEV_AGW_ENDPOINT}")
|
||||
|
||||
@@ -24,5 +24,8 @@
|
||||
<string name="notificationSettingsDialogMessage">Для показа уведомлений необходимо включить уведомления в системных настройках</string>
|
||||
<string name="openNotificationSettings">Открыть настройки уведомлений</string>
|
||||
|
||||
<string name="vpnStateEventChannelName">Уведомления о VPN</string>
|
||||
<string name="vpnStateEventChannelDescription">Краткие оповещения при подключении и отключении VPN</string>
|
||||
|
||||
<string name="tvNoFileBrowser">Пожалуйста, установите приложение для просмотра файлов</string>
|
||||
</resources>
|
||||
@@ -24,5 +24,8 @@
|
||||
<string name="notificationSettingsDialogMessage">To show notifications, you must enable notifications in the system 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>
|
||||
</resources>
|
||||
@@ -1006,6 +1006,12 @@ class AmneziaActivity : QtActivity() {
|
||||
@Suppress("unused")
|
||||
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")
|
||||
fun requestNotificationPermission() {
|
||||
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"
|
||||
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 CONNECT_REQUEST_CODE = 1
|
||||
private const val DISCONNECT_REQUEST_CODE = 2
|
||||
@@ -162,8 +165,42 @@ class ServiceNotification(private val context: Context) {
|
||||
.setDescription(context.resources.getString(R.string.notificationChannelDescription))
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,6 @@ import android.content.Context
|
||||
import android.net.VpnService.Builder
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.net.InetAddress
|
||||
import java.net.ServerSocket
|
||||
import java.util.UUID
|
||||
import go.Seq
|
||||
import org.amnezia.vpn.protocol.BadConfigException
|
||||
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.ip
|
||||
import org.amnezia.vpn.util.net.parseInetAddress
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
private const val TAG = "Xray"
|
||||
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() {
|
||||
|
||||
private var isRunning: Boolean = false
|
||||
@@ -80,10 +56,6 @@ class Xray : Protocol() {
|
||||
val xrayJsonConfig = config.optJSONObject("xray_config_data")
|
||||
?: config.optJSONObject("ssxray_config_data")
|
||||
?: 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)
|
||||
|
||||
(xrayJsonConfig.optJSONObject("log") ?: JSONObject().also { xrayJsonConfig.put("log", it) })
|
||||
@@ -125,22 +97,9 @@ class Xray : Protocol() {
|
||||
if (it.isNotBlank()) setMtu(it.toInt())
|
||||
}
|
||||
|
||||
val inbounds = xrayJsonConfig.getJSONArray("inbounds")
|
||||
val socksIdx = findSocksInboundIndex(inbounds)
|
||||
if (socksIdx < 0) {
|
||||
throw BadConfigException("socks inbound not found")
|
||||
}
|
||||
val socksConfig = inbounds.getJSONObject(socksIdx)
|
||||
val socksConfig = xrayJsonConfig.getJSONArray("inbounds")[0] as JSONObject
|
||||
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)
|
||||
configAppSplitTunneling(config)
|
||||
}
|
||||
@@ -203,10 +162,9 @@ class Xray : Protocol() {
|
||||
}
|
||||
|
||||
private fun runTun2Socks(config: XrayConfig, fd: Int) {
|
||||
val proxyUrl = "socks5://${config.socksUser}:${config.socksPass}@127.0.0.1:${config.socksPort}"
|
||||
val tun2SocksConfig = Tun2SocksConfig().apply {
|
||||
mtu = config.mtu.toLong()
|
||||
proxy = proxyUrl
|
||||
proxy = "socks5://127.0.0.1:${config.socksPort}"
|
||||
device = "fd://$fd"
|
||||
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 {
|
||||
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(
|
||||
protocolConfigBuilder: ProtocolConfig.Builder,
|
||||
val socksPort: Int,
|
||||
val socksUser: String,
|
||||
val socksPass: String,
|
||||
val maxMemory: Long,
|
||||
) : ProtocolConfig(protocolConfigBuilder) {
|
||||
|
||||
protected constructor(builder: Builder) : this(
|
||||
builder,
|
||||
builder.socksPort,
|
||||
builder.socksUser,
|
||||
builder.socksPass,
|
||||
builder.maxMemory
|
||||
)
|
||||
|
||||
@@ -26,12 +22,6 @@ class XrayConfig protected constructor(
|
||||
internal var socksPort: Int = 0
|
||||
private set
|
||||
|
||||
internal var socksUser: String = ""
|
||||
private set
|
||||
|
||||
internal var socksPass: String = ""
|
||||
private set
|
||||
|
||||
internal var maxMemory: Long = XRAY_DEFAULT_MAX_MEMORY
|
||||
private set
|
||||
|
||||
@@ -39,10 +29,6 @@ class XrayConfig protected constructor(
|
||||
|
||||
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 }
|
||||
|
||||
override fun build(): XrayConfig = configBuild().run { XrayConfig(this@Builder) }
|
||||
|
||||
@@ -32,6 +32,7 @@ set(LIBS ${LIBS}
|
||||
|
||||
|
||||
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_wrapper.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}
|
||||
${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_wrapper.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.mm
|
||||
|
||||
@@ -45,9 +45,12 @@ if(NOT IOS AND NOT MACOS_NE)
|
||||
)
|
||||
endif()
|
||||
|
||||
if(NOT ANDROID)
|
||||
set(HEADERS ${HEADERS}
|
||||
set(HEADERS ${HEADERS}
|
||||
${CLIENT_ROOT_DIR}/ui/notificationhandler.h
|
||||
)
|
||||
if(ANDROID)
|
||||
set(HEADERS ${HEADERS}
|
||||
${CLIENT_ROOT_DIR}/platforms/android/android_notificationhandler.h
|
||||
)
|
||||
endif()
|
||||
|
||||
@@ -109,9 +112,12 @@ if(APPLE AND NOT IOS)
|
||||
)
|
||||
endif()
|
||||
|
||||
if(NOT ANDROID)
|
||||
set(SOURCES ${SOURCES}
|
||||
set(SOURCES ${SOURCES}
|
||||
${CLIENT_ROOT_DIR}/ui/notificationhandler.cpp
|
||||
)
|
||||
if(ANDROID)
|
||||
set(SOURCES ${SOURCES}
|
||||
${CLIENT_ROOT_DIR}/platforms/android/android_notificationhandler.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
|
||||
@@ -298,14 +298,14 @@ bool ContainerProps::isSupportedByCurrentPlatform(DockerContainer c)
|
||||
}
|
||||
|
||||
#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) {
|
||||
case DockerContainer::OpenVpn: return true;
|
||||
case DockerContainer::WireGuard: return true;
|
||||
case DockerContainer::Awg2: return true;
|
||||
case DockerContainer::Awg: return true;
|
||||
case DockerContainer::Xray: return true;
|
||||
case DockerContainer::SSXray: return true;
|
||||
case DockerContainer::OpenVpn:
|
||||
case DockerContainer::Cloak:
|
||||
case DockerContainer::ShadowSocks:
|
||||
return false;
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace apiDefs
|
||||
AmneziaFreeV3,
|
||||
AmneziaPremiumV1,
|
||||
AmneziaPremiumV2,
|
||||
AmneziaTrialV2,
|
||||
SelfHosted,
|
||||
ExternalPremium,
|
||||
ExternalTrial
|
||||
@@ -56,7 +57,6 @@ namespace apiDefs
|
||||
constexpr QLatin1String maxDeviceCount("max_device_count");
|
||||
constexpr QLatin1String subscriptionEndDate("subscription_end_date");
|
||||
constexpr QLatin1String subscriptionExpiredByServer("subscription_expired_by_server");
|
||||
constexpr QLatin1String subscriptionStatus("subscription_status");
|
||||
constexpr QLatin1String subscription("subscription");
|
||||
constexpr QLatin1String endDate("end_date");
|
||||
constexpr QLatin1String issuedConfigs("issued_configs");
|
||||
@@ -83,7 +83,6 @@ namespace apiDefs
|
||||
|
||||
constexpr QLatin1String serviceInfo("service_info");
|
||||
constexpr QLatin1String isAdVisible("is_ad_visible");
|
||||
constexpr QLatin1String isRenewalAvailable("is_renewal_available");
|
||||
constexpr QLatin1String adHeader("ad_header");
|
||||
constexpr QLatin1String adDescription("ad_description");
|
||||
constexpr QLatin1String adEndpoint("ad_endpoint");
|
||||
|
||||
@@ -10,7 +10,6 @@ namespace
|
||||
const QByteArray AMNEZIA_CONFIG_SIGNATURE = QByteArray::fromHex("000000ff");
|
||||
|
||||
constexpr QLatin1String unprocessableSubscriptionMessage("Failed to retrieve subscription information. Is it activated?");
|
||||
constexpr QLatin1String trialAlreadyUsedMessage("trial subscription already used");
|
||||
|
||||
QDateTime subscriptionEndUtcFromString(const QString &subscriptionEndDate)
|
||||
{
|
||||
@@ -101,6 +100,7 @@ apiDefs::ConfigType apiUtils::getConfigType(const QJsonObject &serverConfigObjec
|
||||
};
|
||||
case apiDefs::ConfigSource::AmneziaGateway: {
|
||||
constexpr QLatin1String servicePremium("amnezia-premium");
|
||||
constexpr QLatin1String serviceTrial("amnezia-trial");
|
||||
constexpr QLatin1String serviceFree("amnezia-free");
|
||||
constexpr QLatin1String serviceExternalPremium("external-premium");
|
||||
constexpr QLatin1String serviceExternalTrial("external-trial");
|
||||
@@ -110,6 +110,8 @@ apiDefs::ConfigType apiUtils::getConfigType(const QJsonObject &serverConfigObjec
|
||||
|
||||
if (serviceType == servicePremium) {
|
||||
return apiDefs::ConfigType::AmneziaPremiumV2;
|
||||
} else if (serviceType == serviceTrial) {
|
||||
return apiDefs::ConfigType::AmneziaTrialV2;
|
||||
} else if (serviceType == serviceFree) {
|
||||
return apiDefs::ConfigType::AmneziaFreeV3;
|
||||
} else if (serviceType == serviceExternalPremium) {
|
||||
@@ -158,6 +160,7 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
|
||||
|
||||
qDebug() << QString::fromUtf8(responseBody);
|
||||
qDebug() << replyError;
|
||||
qDebug() << replyErrorString;
|
||||
qDebug() << httpStatusCode;
|
||||
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
|
||||
@@ -165,9 +168,6 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
|
||||
QJsonObject jsonObj = jsonDoc.object();
|
||||
const int httpStatusFromBody = jsonObj.value(QStringLiteral("http_status")).toInt(-1);
|
||||
if (httpStatusFromBody == httpStatusCodeConflict) {
|
||||
if (apiErrorMessageFromJson(jsonObj).contains(trialAlreadyUsedMessage, Qt::CaseInsensitive)) {
|
||||
return amnezia::ErrorCode::ApiTrialAlreadyUsedError;
|
||||
}
|
||||
return amnezia::ErrorCode::ApiConfigLimitError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodeNotFound) {
|
||||
@@ -189,13 +189,14 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
|
||||
}
|
||||
|
||||
qDebug() << "something went wrong";
|
||||
return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||
return amnezia::ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
bool apiUtils::isPremiumServer(const QJsonObject &serverConfigObject)
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -240,8 +241,8 @@ QString apiUtils::getPremiumV1VpnKey(const QJsonObject &serverConfigObject)
|
||||
QString apiUtils::getPremiumV2VpnKey(const QJsonObject &serverConfigObject)
|
||||
{
|
||||
auto configType = apiUtils::getConfigType(serverConfigObject);
|
||||
if (configType != apiDefs::ConfigType::AmneziaPremiumV2 && configType != apiDefs::ConfigType::ExternalPremium
|
||||
&& configType != apiDefs::ConfigType::ExternalTrial) {
|
||||
if (configType != apiDefs::ConfigType::AmneziaPremiumV2 && configType != apiDefs::ConfigType::AmneziaTrialV2
|
||||
&& configType != apiDefs::ConfigType::ExternalPremium && configType != apiDefs::ConfigType::ExternalTrial) {
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace apiUtils
|
||||
|
||||
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);
|
||||
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
#include "platforms/android/android_controller.h"
|
||||
#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)
|
||||
#include "platforms/ios/ios_controller.h"
|
||||
#include <AmneziaVPN-Swift.h>
|
||||
@@ -242,7 +247,6 @@ void CoreController::initSignalHandlers()
|
||||
|
||||
void CoreController::initNotificationHandler()
|
||||
{
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
m_notificationHandler.reset(NotificationHandler::create(nullptr));
|
||||
|
||||
connect(m_vpnConnection.get(), &VpnConnection::connectionStateChanged, m_notificationHandler.get(),
|
||||
@@ -255,9 +259,11 @@ void CoreController::initNotificationHandler()
|
||||
&ConnectionController::closeConnection);
|
||||
connect(this, &CoreController::translationsUpdated, m_notificationHandler.get(), &NotificationHandler::onTranslationsUpdated);
|
||||
|
||||
auto* trayHandler = qobject_cast<SystemTrayNotificationHandler*>(m_notificationHandler.get());
|
||||
connect(this, &CoreController::websiteUrlChanged, trayHandler, &SystemTrayNotificationHandler::updateWebsiteUrl);
|
||||
#endif
|
||||
#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);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void CoreController::updateTranslator(const QLocale &locale)
|
||||
|
||||
@@ -5,9 +5,7 @@
|
||||
#include <QQmlContext>
|
||||
#include <QThread>
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
#include "ui/systemtray_notificationhandler.h"
|
||||
#endif
|
||||
#include "ui/notificationhandler.h"
|
||||
|
||||
#include "ui/controllers/api/apiConfigsController.h"
|
||||
#include "ui/controllers/api/apiSettingsController.h"
|
||||
@@ -51,10 +49,6 @@
|
||||
#include "ui/models/sites_model.h"
|
||||
#include "ui/models/newsModel.h"
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
#include "ui/notificationhandler.h"
|
||||
#endif
|
||||
|
||||
class CoreController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
@@ -101,9 +95,7 @@ private:
|
||||
QSharedPointer<VpnConnection> m_vpnConnection;
|
||||
QSharedPointer<QTranslator> m_translator;
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
QScopedPointer<NotificationHandler> m_notificationHandler;
|
||||
#endif
|
||||
|
||||
QMetaObject::Connection m_reloadConfigErrorOccurredConnection;
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
#include "amnezia_application.h"
|
||||
#include "core/api/apiUtils.h"
|
||||
#include "core/networkUtilities.h"
|
||||
#include "settings.h"
|
||||
#include "utilities.h"
|
||||
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
@@ -50,80 +49,15 @@ namespace
|
||||
constexpr int httpStatusCodeUnprocessableEntity = 422;
|
||||
|
||||
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,
|
||||
const bool isStrictKillSwitchEnabled, const std::shared_ptr<Settings> &settings,
|
||||
QObject *parent)
|
||||
const bool isStrictKillSwitchEnabled, QObject *parent)
|
||||
: QObject(parent),
|
||||
m_gatewayEndpoint(gatewayEndpoint),
|
||||
m_isDevEnvironment(isDevEnvironment),
|
||||
m_requestTimeoutMsecs(requestTimeoutMsecs),
|
||||
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled),
|
||||
m_settings(settings)
|
||||
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -350,33 +284,25 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
|
||||
auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString("");
|
||||
auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString("");
|
||||
|
||||
QStringList primaryBaseUrls;
|
||||
QStringList fallbackBaseUrls;
|
||||
QStringList baseUrls;
|
||||
if (m_isDevEnvironment) {
|
||||
primaryBaseUrls = QString(DEV_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
baseUrls = QString(DEV_S3_ENDPOINT).split(", ");
|
||||
} else {
|
||||
primaryBaseUrls = QString(PROD_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
fallbackBaseUrls = QString(FALLBACK_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
baseUrls = QString(PROD_S3_ENDPOINT).split(", ");
|
||||
}
|
||||
|
||||
auto appendStorageUrls = [&serviceType, &userCountryCode](const QStringList &baseUrls, QStringList &target) {
|
||||
if (!serviceType.isEmpty()) {
|
||||
for (const auto &baseUrl : baseUrls) {
|
||||
QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8();
|
||||
target.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) + ".json");
|
||||
}
|
||||
}
|
||||
for (const auto &baseUrl : baseUrls) {
|
||||
target.push_back(baseUrl + "endpoints.json");
|
||||
}
|
||||
};
|
||||
|
||||
QStringList proxyStorageUrls;
|
||||
appendStorageUrls(primaryBaseUrls, proxyStorageUrls);
|
||||
appendStorageUrls(fallbackBaseUrls, proxyStorageUrls);
|
||||
const QString proxyUrlsCacheKey = getProxyUrlsCacheKey(serviceType, userCountryCode);
|
||||
if (!serviceType.isEmpty()) {
|
||||
for (const auto &baseUrl : baseUrls) {
|
||||
QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8();
|
||||
proxyStorageUrls.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)
|
||||
+ ".json");
|
||||
}
|
||||
}
|
||||
for (const auto &baseUrl : baseUrls)
|
||||
proxyStorageUrls.push_back(baseUrl + "endpoints.json");
|
||||
|
||||
getProxyUrlsAsync(proxyStorageUrls, 0, proxyUrlsCacheKey, [this, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) {
|
||||
getProxyUrlsAsync(proxyStorageUrls, 0, [this, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) {
|
||||
getProxyUrlAsync(proxyUrls, 0, [this, encRequestData, endpoint, processResponse](const QString &proxyUrl) {
|
||||
bypassProxyAsync(endpoint, proxyUrl, encRequestData,
|
||||
[processResponse, this](const QByteArray &decryptedBody, bool isDecryptionSuccessful,
|
||||
@@ -401,48 +327,40 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
|
||||
QStringList GatewayController::getProxyUrls(const QString &serviceType, const QString &userCountryCode)
|
||||
{
|
||||
QNetworkRequest request;
|
||||
request.setTransferTimeout(proxyStorageRequestTimeoutMsecs);
|
||||
request.setTransferTimeout(m_requestTimeoutMsecs);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
|
||||
QEventLoop wait;
|
||||
QList<QSslError> sslErrors;
|
||||
QNetworkReply *reply;
|
||||
|
||||
QStringList primaryBaseUrls;
|
||||
QStringList fallbackBaseUrls;
|
||||
QStringList baseUrls;
|
||||
if (m_isDevEnvironment) {
|
||||
primaryBaseUrls = QString(DEV_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
baseUrls = QString(DEV_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
} else {
|
||||
primaryBaseUrls = QString(PROD_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
fallbackBaseUrls = QString(FALLBACK_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
baseUrls = QString(PROD_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
}
|
||||
|
||||
if (baseUrls.empty()) {
|
||||
qDebug() << "empty storage endpoint list";
|
||||
return {};
|
||||
}
|
||||
|
||||
std::random_device randomDevice;
|
||||
std::mt19937 generator(randomDevice());
|
||||
std::shuffle(primaryBaseUrls.begin(), primaryBaseUrls.end(), generator);
|
||||
std::shuffle(fallbackBaseUrls.begin(), fallbackBaseUrls.end(), generator);
|
||||
std::shuffle(baseUrls.begin(), baseUrls.end(), generator);
|
||||
|
||||
auto appendStorageUrls = [&serviceType, &userCountryCode](const QStringList &baseUrls, QStringList &target) {
|
||||
if (!serviceType.isEmpty()) {
|
||||
for (const auto &baseUrl : baseUrls) {
|
||||
QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8();
|
||||
target.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) + ".json");
|
||||
}
|
||||
}
|
||||
for (const auto &baseUrl : baseUrls) {
|
||||
target.push_back(baseUrl + "endpoints.json");
|
||||
}
|
||||
};
|
||||
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||
|
||||
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);
|
||||
if (!serviceType.isEmpty()) {
|
||||
for (const auto &baseUrl : baseUrls) {
|
||||
QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8();
|
||||
proxyStorageUrls.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) + ".json");
|
||||
}
|
||||
}
|
||||
for (const auto &baseUrl : baseUrls) {
|
||||
proxyStorageUrls.push_back(baseUrl + "endpoints.json");
|
||||
}
|
||||
|
||||
for (const auto &proxyStorageUrl : proxyStorageUrls) {
|
||||
@@ -457,8 +375,26 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
|
||||
auto encryptedResponseBody = reply->readAll();
|
||||
reply->deleteLater();
|
||||
|
||||
EVP_PKEY *privateKey = nullptr;
|
||||
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;
|
||||
continue;
|
||||
}
|
||||
@@ -469,8 +405,6 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
|
||||
for (const auto &endpoint : endpointsArray) {
|
||||
endpoints.push_back(endpoint.toString());
|
||||
}
|
||||
m_settings->writeGatewayProxyUrls(proxyUrlsCacheKey, encryptedResponseBody);
|
||||
|
||||
return endpoints;
|
||||
} else {
|
||||
auto replyError = reply->error();
|
||||
@@ -482,7 +416,7 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
|
||||
reply->deleteLater();
|
||||
}
|
||||
}
|
||||
return readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment);
|
||||
return {};
|
||||
}
|
||||
|
||||
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,
|
||||
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()) {
|
||||
onComplete(shuffledProxyUrls(readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment)));
|
||||
onComplete({});
|
||||
return;
|
||||
}
|
||||
|
||||
QNetworkRequest request;
|
||||
request.setTransferTimeout(proxyStorageRequestTimeoutMsecs);
|
||||
request.setTransferTimeout(m_requestTimeoutMsecs);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
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::finished, this,
|
||||
[this, proxyStorageUrls, currentProxyStorageIndex, proxyUrlsCacheKey, onComplete, reply]() {
|
||||
connect(reply, &QNetworkReply::finished, this, [this, proxyStorageUrls, currentProxyStorageIndex, onComplete, reply]() {
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
QByteArray encrypted = reply->readAll();
|
||||
reply->deleteLater();
|
||||
|
||||
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";
|
||||
QMetaObject::invokeMethod(
|
||||
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, proxyUrlsCacheKey, onComplete); }, Qt::QueuedConnection);
|
||||
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -656,9 +604,13 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co
|
||||
QStringList endpoints;
|
||||
for (const QJsonValue &endpoint : endpointsArray)
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -667,7 +619,7 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co
|
||||
qDebug() << "go to the next storage endpoint";
|
||||
reply->deleteLater();
|
||||
QMetaObject::invokeMethod(
|
||||
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, proxyUrlsCacheKey, onComplete); }, Qt::QueuedConnection);
|
||||
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,6 @@
|
||||
#include <QPair>
|
||||
#include <QPromise>
|
||||
#include <QSharedPointer>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <memory>
|
||||
|
||||
#include "core/defs.h"
|
||||
|
||||
@@ -17,16 +14,13 @@
|
||||
#include "platforms/ios/ios_controller.h"
|
||||
#endif
|
||||
|
||||
class Settings;
|
||||
|
||||
class GatewayController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
|
||||
const bool isStrictKillSwitchEnabled, const std::shared_ptr<Settings> &settings,
|
||||
QObject *parent = nullptr);
|
||||
const bool isStrictKillSwitchEnabled, QObject *parent = nullptr);
|
||||
|
||||
amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody);
|
||||
QFuture<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject apiPayload);
|
||||
@@ -59,7 +53,7 @@ private:
|
||||
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction);
|
||||
|
||||
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 bypassProxyAsync(
|
||||
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
|
||||
@@ -69,7 +63,6 @@ private:
|
||||
QString m_gatewayEndpoint;
|
||||
bool m_isDevEnvironment = false;
|
||||
bool m_isStrictKillSwitchEnabled = false;
|
||||
std::shared_ptr<Settings> m_settings;
|
||||
|
||||
inline static QString m_proxyUrl;
|
||||
};
|
||||
|
||||
@@ -125,7 +125,6 @@ namespace amnezia
|
||||
ApiPurchaseError = 1113,
|
||||
ApiSubscriptionNotActiveError = 1114,
|
||||
ApiNoPurchasedSubscriptionsError = 1115,
|
||||
ApiTrialAlreadyUsedError = 1116,
|
||||
|
||||
// QFile errors
|
||||
OpenError = 1200,
|
||||
|
||||
@@ -82,7 +82,6 @@ QString errorString(ErrorCode code) {
|
||||
case (ErrorCode::ApiPurchaseError): errorMessage = QObject::tr("Unable to process purchase"); 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::ApiTrialAlreadyUsedError): errorMessage = QObject::tr("This email address has already been used to activate a trial"); break;
|
||||
|
||||
// QFile errors
|
||||
case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break;
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
#include <QString>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QList>
|
||||
#include <QHostAddress>
|
||||
#include <QRandomGenerator>
|
||||
#include <QTcpServer>
|
||||
#include <stdexcept>
|
||||
#include "3rd/QJsonStruct/QJsonIO.hpp"
|
||||
#include "transfer.h"
|
||||
#include "serialization.h"
|
||||
@@ -19,125 +14,25 @@ namespace amnezia::serialization::inbounds
|
||||
// "port": 10808,
|
||||
// "protocol": "socks",
|
||||
// "settings": {
|
||||
// "auth": "password",
|
||||
// "accounts": [{"user": "...", "pass": "..."}],
|
||||
// "udp": true
|
||||
// }
|
||||
// }
|
||||
//],
|
||||
|
||||
const static QString listen = "127.0.0.1";
|
||||
const static int defaultPort = 10808;
|
||||
const static int port = 10808;
|
||||
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 root;
|
||||
QJsonIO::SetValue(root, listen, "listen");
|
||||
QJsonIO::SetValue(root, defaultPort, "port");
|
||||
QJsonIO::SetValue(root, port, "port");
|
||||
QJsonIO::SetValue(root, protocol, "protocol");
|
||||
QJsonIO::SetValue(root, true, "settings", "udp");
|
||||
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
|
||||
|
||||
|
||||
@@ -60,24 +60,7 @@ namespace amnezia::serialization
|
||||
|
||||
namespace inbounds
|
||||
{
|
||||
struct InboundCredentials {
|
||||
QString username;
|
||||
QString password;
|
||||
int port;
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ set_target_properties(AmneziaVPNNetworkExtension PROPERTIES
|
||||
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_NAME "${BUILD_IOS_APP_IDENTIFIER}.network-extension"
|
||||
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS ${CMAKE_CURRENT_SOURCE_DIR}/AmneziaVPNNetworkExtension.entitlements
|
||||
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_APPLICATION_EXTENSION_API_ONLY "YES"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<string>AmneziaVPNNetworkExtension</string>
|
||||
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<string>org.amnezia.AmneziaVPN.network-extension</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
@@ -16,9 +16,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<string>${APPLE_PROJECT_VERSION}</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<string>${CMAKE_PROJECT_VERSION_TWEAK}</string>
|
||||
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
@@ -41,6 +41,6 @@
|
||||
<string>group.org.amnezia.AmneziaVPN</string>
|
||||
|
||||
<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>
|
||||
</plist>
|
||||
|
||||
@@ -307,6 +307,16 @@ void AndroidController::requestNotificationPermission()
|
||||
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()
|
||||
{
|
||||
QEventLoop wait;
|
||||
|
||||
@@ -53,6 +53,7 @@ public:
|
||||
QPixmap getAppIcon(const QString &package, QSize *size, const QSize &requestedSize);
|
||||
bool isNotificationPermissionGranted();
|
||||
void requestNotificationPermission();
|
||||
void showVpnStateNotification(const QString &title, const QString &message);
|
||||
bool requestAuthentication();
|
||||
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 {
|
||||
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,
|
||||
let providerConfiguration = protocolConfiguration.providerConfiguration,
|
||||
let openVPNConfigData = providerConfiguration[Constants.ovpnConfigKey] as? Data else {
|
||||
@@ -31,25 +25,7 @@ extension PacketTunnelProvider {
|
||||
do {
|
||||
let openVPNConfig = try JSONDecoder().decode(OpenVPNConfig.self, from: openVPNConfigData)
|
||||
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)
|
||||
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)
|
||||
} catch {
|
||||
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()
|
||||
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 hasTlsAuthClose = configString.contains("</tls-auth>")
|
||||
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: "ConfigTail", message: tail)
|
||||
|
||||
if hasTlsAuthOpen && hasTlsAuthClose {
|
||||
ovpnLog(.info, title: "TLSAuthSanitized", message: "preserve original tls-auth block")
|
||||
if let start = configString.range(of: "<tls-auth>"),
|
||||
let end = configString.range(of: "</tls-auth>", range: start.upperBound..<configString.endIndex) {
|
||||
let keyBody = String(configString[start.upperBound..<end.lowerBound])
|
||||
ovpnLog(.debug, title: "TLSAuthInline", message: keyBody)
|
||||
let sanitizedLines = keyBody
|
||||
.split(whereSeparator: { $0.isNewline })
|
||||
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
.filter { !$0.isEmpty }
|
||||
.filter { !$0.hasPrefix("#") }
|
||||
|
||||
let sanitizedKey = sanitizedLines.joined(separator: "\n")
|
||||
ovpnLog(.debug, title: "TLSAuthSanitized", message: sanitizedKey)
|
||||
let sanitizedBlock = "<tls-auth>\n\(sanitizedKey)\n</tls-auth>"
|
||||
configString.replaceSubrange(start.lowerBound..<end.upperBound, with: sanitizedBlock)
|
||||
}
|
||||
|
||||
var 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 normalizedConfig = configString.replacingOccurrences(of: "\r\n", with: "\n")
|
||||
let sanitizedData = Data(normalizedConfig.utf8)
|
||||
|
||||
let configuration = OpenVPNConfiguration()
|
||||
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") {
|
||||
configuration.setPTCloak()
|
||||
}
|
||||
@@ -224,15 +124,10 @@ extension PacketTunnelProvider {
|
||||
if evaluation?.autologin == false {
|
||||
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
|
||||
self?.handleOpenVPNReachabilityChange(status)
|
||||
}
|
||||
#endif
|
||||
|
||||
startHandler = completionHandler
|
||||
ovpnAdapter?.connect(using: openVPNPacketFlow())
|
||||
@@ -248,8 +143,6 @@ extension PacketTunnelProvider {
|
||||
return
|
||||
}
|
||||
|
||||
ovpnLog(.info, title: "Transport", message: "bytesIn=\(bytesin) bytesOut=\(bytesout)")
|
||||
|
||||
let response: [String: Any] = [
|
||||
"rx_bytes": bytesin,
|
||||
"tx_bytes": bytesout
|
||||
@@ -262,10 +155,6 @@ extension PacketTunnelProvider {
|
||||
ovpnLog(.info, message: "Stopping tunnel: reason: \(reason.amneziaDescription)")
|
||||
|
||||
stopHandler = completionHandler
|
||||
openVpnGatewayAddress = nil
|
||||
openVpnLocalAddress = nil
|
||||
openVpnLocalMask = nil
|
||||
lastOpenVPNSettings = nil
|
||||
if vpnReachability.isTracking {
|
||||
vpnReachability.stopTracking()
|
||||
}
|
||||
@@ -285,99 +174,11 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
|
||||
configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings?,
|
||||
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
|
||||
// send empty string to NEDNSSettings.matchDomains
|
||||
if let dnsSettings = effectiveSettings.dnsSettings {
|
||||
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
|
||||
}
|
||||
networkSettings?.dnsSettings?.matchDomains = [""]
|
||||
|
||||
effectiveSettings.dnsSettings?.matchDomains = [""]
|
||||
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 {
|
||||
if splitTunnelType == 1 {
|
||||
var ipv4IncludedRoutes = [NEIPv4Route]()
|
||||
|
||||
guard let splitTunnelSites else {
|
||||
@@ -393,8 +194,9 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
effectiveSettings.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
|
||||
} else if splitType == 2 {
|
||||
networkSettings?.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
|
||||
} else {
|
||||
if splitTunnelType == 2 {
|
||||
var ipv4ExcludedRoutes = [NEIPv4Route]()
|
||||
var ipv4IncludedRoutes = [NEIPv4Route]()
|
||||
var ipv6IncludedRoutes = [NEIPv6Route]()
|
||||
@@ -422,418 +224,14 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
|
||||
destinationAddress: "\(allIPv6.address)",
|
||||
networkPrefixLength: NSNumber(value: allIPv6.networkPrefixLength)))
|
||||
}
|
||||
effectiveSettings.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
|
||||
effectiveSettings.ipv6Settings?.includedRoutes = ipv6IncludedRoutes
|
||||
effectiveSettings.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
|
||||
networkSettings?.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
|
||||
networkSettings?.ipv6Settings?.includedRoutes = ipv6IncludedRoutes
|
||||
networkSettings?.ipv4Settings?.excludedRoutes = ipv4ExcludedRoutes
|
||||
}
|
||||
}
|
||||
|
||||
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] {
|
||||
let lines = config.split(whereSeparator: \.isNewline)
|
||||
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
|
||||
setTunnelNetworkSettings(networkSettings, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
// Process events returned by the OpenVPN library
|
||||
@@ -851,9 +249,6 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
|
||||
|
||||
startHandler(nil)
|
||||
self.startHandler = nil
|
||||
|
||||
logOpenVPNConnectionInfo()
|
||||
refreshOpenVPNSettingsAfterConnect()
|
||||
case .disconnected:
|
||||
guard let stopHandler = stopHandler else { return }
|
||||
|
||||
@@ -896,41 +291,4 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
|
||||
// Handle log messages
|
||||
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 NetworkExtension
|
||||
|
||||
@@ -7,7 +6,6 @@ enum XrayErrors: Error {
|
||||
case xrayConfigIsWrong
|
||||
case cantSaveXrayConfig
|
||||
case cantParseListenAndPort
|
||||
case cantAcquireLocalPort
|
||||
case cantSaveHevSocksConfig
|
||||
}
|
||||
|
||||
@@ -23,42 +21,6 @@ extension Constants {
|
||||
}
|
||||
|
||||
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,
|
||||
settings: NEPacketTunnelNetworkSettings) {
|
||||
guard let splitTunnelType = xrayConfig.splitTunnelType else {
|
||||
@@ -167,11 +129,14 @@ extension PacketTunnelProvider {
|
||||
return
|
||||
}
|
||||
|
||||
let port = try acquireFreeLocalPort()
|
||||
let port = 10808
|
||||
let address = "::1"
|
||||
|
||||
// Extract existing SOCKS5 credentials or generate new ones per session.
|
||||
let socksCredentials = ensureInboundAuth(jsonDict: &jsonDict, port: port, address: address)
|
||||
if var inboundsArray = jsonDict["inbounds"] as? [[String: Any]], !inboundsArray.isEmpty {
|
||||
inboundsArray[0]["port"] = port
|
||||
inboundsArray[0]["listen"] = address
|
||||
jsonDict["inbounds"] = inboundsArray
|
||||
}
|
||||
|
||||
let updatedData = try JSONSerialization.data(withJSONObject: jsonDict, options: [])
|
||||
|
||||
@@ -194,8 +159,6 @@ extension PacketTunnelProvider {
|
||||
self?.setupAndRunTun2socks(configData: updatedData,
|
||||
address: address,
|
||||
port: port,
|
||||
username: socksCredentials.username,
|
||||
password: socksCredentials.password,
|
||||
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,
|
||||
completionHandler: @escaping (Error?) -> Void) {
|
||||
let path = Constants.cachesDirectory.appendingPathComponent("config.json", isDirectory: false).path
|
||||
@@ -307,8 +214,6 @@ extension PacketTunnelProvider {
|
||||
private func setupAndRunTun2socks(configData: Data,
|
||||
address: String,
|
||||
port: Int,
|
||||
username: String,
|
||||
password: String,
|
||||
completionHandler: @escaping (Error?) -> Void) {
|
||||
let config = """
|
||||
tunnel:
|
||||
@@ -316,8 +221,6 @@ extension PacketTunnelProvider {
|
||||
socks5:
|
||||
port: \(port)
|
||||
address: \(address)
|
||||
username: \(username)
|
||||
password: \(password)
|
||||
udp: 'udp'
|
||||
misc:
|
||||
task-stack-size: 20480
|
||||
|
||||
@@ -53,14 +53,6 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
|
||||
var splitTunnelType: Int?
|
||||
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()
|
||||
|
||||
@@ -91,8 +83,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
|
||||
guard hasMeaningfulChange, let proto = self.protoType else { return }
|
||||
|
||||
// WireGuard/AWG and OpenVPN manages network changes internally in its own adapter.
|
||||
if proto == .wireguard || proto == .openvpn {
|
||||
// WireGuard/AWG manages network changes internally in its own adapter.
|
||||
if proto == .wireguard {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -200,26 +192,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
let errorNotifier = ErrorNotifier(activationAttemptId: activationAttemptId)
|
||||
|
||||
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 {
|
||||
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 {
|
||||
protoType = .openvpn
|
||||
} else if (providerConfiguration?[Constants.wireGuardConfigKey] as? Data) != nil {
|
||||
@@ -474,8 +449,6 @@ extension WireGuardLogLevel {
|
||||
|
||||
final class PacketTunnelFlowAdapter: NSObject, OpenVPNAdapterPacketFlow {
|
||||
private let flow: NEPacketTunnelFlow
|
||||
private var readLogCounter = 0
|
||||
private var writeLogCounter = 0
|
||||
|
||||
init(flow: NEPacketTunnelFlow) {
|
||||
self.flow = flow
|
||||
@@ -484,98 +457,15 @@ final class PacketTunnelFlowAdapter: NSObject, OpenVPNAdapterPacketFlow {
|
||||
|
||||
@objc(readPacketsWithCompletionHandler:)
|
||||
func readPackets(completionHandler: @escaping ([Data], [NSNumber]) -> Void) {
|
||||
flow.readPackets { packets, protocols in
|
||||
#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)
|
||||
}
|
||||
flow.readPackets(completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@objc(writePackets:withProtocols:)
|
||||
func writePackets(_ packets: [Data], withProtocols protocols: [NSNumber]) -> Bool {
|
||||
#if os(macOS)
|
||||
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)"
|
||||
flow.writePackets(packets, withProtocols: protocols)
|
||||
}
|
||||
}
|
||||
|
||||
extension NEPacketTunnelFlow: OpenVPNAdapterPacketFlow {}
|
||||
|
||||
extension NEProviderStopReason {
|
||||
var amneziaDescription: String {
|
||||
switch self {
|
||||
|
||||
@@ -218,13 +218,16 @@ bool IosController::connectVpn(amnezia::Proto proto, const QJsonObject& configur
|
||||
m_rawConfig = configuration;
|
||||
m_serverAddress = configuration.value(config_key::hostName).toString().toNSString();
|
||||
|
||||
const QString serverDescription = configuration.value(config_key::description).toString().trimmed();
|
||||
QString tunnelName;
|
||||
if (serverDescription.isEmpty()) {
|
||||
tunnelName = ProtocolProps::protoToString(proto);
|
||||
} else {
|
||||
if (configuration.value(config_key::description).toString().isEmpty()) {
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -549,16 +552,6 @@ bool IosController::setupOpenVPN()
|
||||
|
||||
QJsonDocument openVPNConfigDoc(openVPNConfig);
|
||||
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);
|
||||
}
|
||||
@@ -807,59 +800,11 @@ bool IosController::startOpenVPN(const QString &config)
|
||||
|
||||
NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init];
|
||||
tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID];
|
||||
QByteArray configUtf8 = config.toUtf8();
|
||||
NSData *ovpnConfigData = [NSData dataWithBytes:configUtf8.constData() length:configUtf8.size()];
|
||||
tunnelProtocol.providerConfiguration = @{@"ovpn": ovpnConfigData};
|
||||
tunnelProtocol.providerConfiguration = @{@"ovpn": [[NSString stringWithUTF8String:config.toStdString().c_str()] dataUsingEncoding:NSUTF8StringEncoding]};
|
||||
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;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -869,9 +814,7 @@ bool IosController::startWireGuard(const QString &config)
|
||||
|
||||
NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init];
|
||||
tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID];
|
||||
QByteArray configUtf8 = config.toUtf8();
|
||||
NSData *wgConfigData = [NSData dataWithBytes:configUtf8.constData() length:configUtf8.size()];
|
||||
tunnelProtocol.providerConfiguration = @{@"wireguard": wgConfigData};
|
||||
tunnelProtocol.providerConfiguration = @{@"wireguard": [[NSString stringWithUTF8String:config.toStdString().c_str()] dataUsingEncoding:NSUTF8StringEncoding]};
|
||||
tunnelProtocol.serverAddress = m_serverAddress;
|
||||
|
||||
m_currentTunnel.protocolConfiguration = tunnelProtocol;
|
||||
@@ -885,9 +828,7 @@ bool IosController::startXray(const QString &config)
|
||||
|
||||
NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init];
|
||||
tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID];
|
||||
QByteArray configUtf8 = config.toUtf8();
|
||||
NSData *xrayConfigData = [NSData dataWithBytes:configUtf8.constData() length:configUtf8.size()];
|
||||
tunnelProtocol.providerConfiguration = @{@"xray": xrayConfigData};
|
||||
tunnelProtocol.providerConfiguration = @{@"xray": [[NSString stringWithUTF8String:config.toStdString().c_str()] dataUsingEncoding:NSUTF8StringEncoding]};
|
||||
tunnelProtocol.serverAddress = m_serverAddress;
|
||||
|
||||
m_currentTunnel.protocolConfiguration = tunnelProtocol;
|
||||
|
||||
@@ -61,6 +61,8 @@ IOSNotificationHandler::~IOSNotificationHandler() { }
|
||||
void IOSNotificationHandler::notify(NotificationHandler::Message type, const QString& title,
|
||||
const QString& message, int timerMsec) {
|
||||
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) {
|
||||
return;
|
||||
@@ -71,11 +73,13 @@ void IOSNotificationHandler::notify(NotificationHandler::Message type, const QSt
|
||||
content.body = message.toNSString();
|
||||
content.sound = [UNNotificationSound defaultSound];
|
||||
|
||||
int timerSec = timerMsec / 1000;
|
||||
NSTimeInterval delay = 0.1;
|
||||
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
|
||||
trigger:trigger];
|
||||
|
||||
@@ -143,6 +147,7 @@ IOSNotificationHandler::~IOSNotificationHandler() { }
|
||||
void IOSNotificationHandler::notify(NotificationHandler::Message type, const QString& title,
|
||||
const QString& message, int timerMsec) {
|
||||
Q_UNUSED(type);
|
||||
Q_UNUSED(timerMsec);
|
||||
|
||||
if (!m_delegate) {
|
||||
return;
|
||||
@@ -153,11 +158,13 @@ void IOSNotificationHandler::notify(NotificationHandler::Message type, const QSt
|
||||
content.body = message.toNSString();
|
||||
content.sound = [UNNotificationSound defaultSound];
|
||||
|
||||
int timerSec = timerMsec / 1000;
|
||||
NSTimeInterval delay = 0.1;
|
||||
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
|
||||
trigger:trigger];
|
||||
|
||||
|
||||
@@ -164,13 +164,8 @@ bool LinuxRouteMonitor::rtmSendRoute(int action, int flags, int type,
|
||||
}
|
||||
|
||||
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;
|
||||
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_attr32(nlmsg, sizeof(buf), RTA_PRIORITY, 0);
|
||||
rtm->rtm_type = RTN_UNICAST;
|
||||
|
||||
@@ -44,9 +44,6 @@ void LinuxNetworkWatcher::initialize() {
|
||||
connect(m_worker, &LinuxNetworkWatcherWorker::wakeup, this,
|
||||
&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.
|
||||
// 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
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
enum NMState {
|
||||
NM_STATE_UNKNOWN = 0,
|
||||
NM_STATE_ASLEEP = 10,
|
||||
NM_STATE_DISABLED = 10,
|
||||
NM_STATE_DISCONNECTED = 20,
|
||||
NM_STATE_DISCONNECTING = 30,
|
||||
NM_STATE_CONNECTING = 40,
|
||||
@@ -200,11 +199,10 @@ void LinuxNetworkWatcherWorker::checkDevices() {
|
||||
|
||||
void LinuxNetworkWatcherWorker::NMStateChanged(quint32 state)
|
||||
{
|
||||
logger.debug() << "NMStateChanged " << state;
|
||||
if (state == NM_STATE_ASLEEP) {
|
||||
emit wakeup();
|
||||
}
|
||||
|
||||
logger.debug() << "NMStateChanged " << state;
|
||||
}
|
||||
|
||||
if (state == NM_STATE_ASLEEP || state == NM_STATE_DISABLED) {
|
||||
emit wakeup();
|
||||
} else if (state == NM_STATE_CONNECTED_GLOBAL) {
|
||||
emit networkChanged();
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,6 @@ class LinuxNetworkWatcherWorker final : public QObject {
|
||||
signals:
|
||||
void unsecuredNetwork(const QString& networkName, const QString& networkId);
|
||||
void wakeup();
|
||||
void networkChanged();
|
||||
|
||||
public slots:
|
||||
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);
|
||||
}
|
||||
}];
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "xrayprotocol.h"
|
||||
|
||||
#include "core/ipcclient.h"
|
||||
#include "core/serialization/serialization.h"
|
||||
#include "ipc.h"
|
||||
#include "utilities.h"
|
||||
#include "core/networkUtilities.h"
|
||||
@@ -15,8 +14,6 @@
|
||||
#include <QtCore/qobjectdefs.h>
|
||||
#include <QtCore/qprocess.h>
|
||||
|
||||
#include <exception>
|
||||
|
||||
#ifdef Q_OS_MACOS
|
||||
static const QString tunName = "utun22";
|
||||
#else
|
||||
@@ -56,19 +53,6 @@ ErrorCode 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) {
|
||||
auto xrayStart = iface->xrayStart(QJsonDocument(m_xrayConfig).toJson());
|
||||
if (!xrayStart.waitForFinished() || !xrayStart.returnValue()) {
|
||||
@@ -137,11 +121,8 @@ ErrorCode XrayProtocol::startTun2Socks()
|
||||
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->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]() {
|
||||
auto readAllStandardOutput = m_tun2socksProcess->readAllStandardOutput();
|
||||
@@ -155,7 +136,7 @@ ErrorCode XrayProtocol::startTun2Socks()
|
||||
if (!line.contains("[TCP]") && !line.contains("[UDP]"))
|
||||
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);
|
||||
|
||||
if (ErrorCode res = setupRouting(); res != ErrorCode::NoError) {
|
||||
|
||||
@@ -26,10 +26,6 @@ private:
|
||||
QList<QHostAddress> m_dnsServers;
|
||||
QString m_remoteAddress;
|
||||
|
||||
QString m_socksUser;
|
||||
QString m_socksPassword;
|
||||
int m_socksPort = 10808;
|
||||
|
||||
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 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 rm -frd /opt/amnezia
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM 3proxy/3proxy:0.9.5
|
||||
FROM 3proxy/3proxy:latest
|
||||
|
||||
LABEL maintainer="AmneziaVPN"
|
||||
|
||||
@@ -7,4 +7,4 @@ RUN echo -e "#!/bin/bash\ntail -f /dev/null" > /opt/amnezia/start.sh
|
||||
RUN chmod a+x /opt/amnezia/start.sh
|
||||
|
||||
ENTRYPOINT [ "/bin/sh", "/opt/amnezia/start.sh" ]
|
||||
CMD [ "" ]
|
||||
CMD [ "" ]
|
||||
@@ -15,7 +15,6 @@ namespace
|
||||
const char cloudFlareNs2[] = "1.0.0.1";
|
||||
|
||||
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)
|
||||
@@ -527,24 +526,6 @@ void Settings::toggleDevGatewayEnv(bool 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()
|
||||
{
|
||||
return m_settings.value("Conf/homeAdLabelVisible", true).toBool();
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include <QObject>
|
||||
#include <QSettings>
|
||||
#include <QString>
|
||||
#include <QByteArray>
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
@@ -235,8 +234,6 @@ public:
|
||||
QString getGatewayEndpoint(bool isTestPurchase = false);
|
||||
bool isDevGatewayEnv(bool isTestPurchase = false);
|
||||
void toggleDevGatewayEnv(bool enabled);
|
||||
QByteArray readGatewayProxyUrls(const QString &cacheKey) const;
|
||||
void writeGatewayProxyUrls(const QString &cacheKey, const QByteArray &proxyUrlsEncrypted);
|
||||
|
||||
bool isHomeAdLabelVisible();
|
||||
void disableHomeAdLabel();
|
||||
|
||||
@@ -52,18 +52,18 @@
|
||||
<context>
|
||||
<name>ApiAccountInfoModel</name>
|
||||
<message>
|
||||
<location filename="../ui/models/api/apiAccountInfoModel.cpp" line="32"/>
|
||||
<location filename="../ui/models/api/apiAccountInfoModel.cpp" line="37"/>
|
||||
<location filename="../ui/models/api/apiAccountInfoModel.cpp" line="31"/>
|
||||
<location filename="../ui/models/api/apiAccountInfoModel.cpp" line="35"/>
|
||||
<source>Active</source>
|
||||
<translation>Активна</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/models/api/apiAccountInfoModel.cpp" line="36"/>
|
||||
<source>Inactive</source>
|
||||
<location filename="../ui/models/api/apiAccountInfoModel.cpp" line="34"/>
|
||||
<source><p><a style="color: #EB5757;">Inactive</a></source>
|
||||
<translation>Не активна</translation>
|
||||
</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>
|
||||
<translation>%1 из %2</translation>
|
||||
</message>
|
||||
@@ -71,51 +71,23 @@
|
||||
<context>
|
||||
<name>ApiConfigsController</name>
|
||||
<message>
|
||||
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="859"/>
|
||||
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="929"/>
|
||||
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="514"/>
|
||||
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="690"/>
|
||||
<source>%1 installed successfully.</source>
|
||||
<translation>%1 успешно установлен.</translation>
|
||||
</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>
|
||||
<translation>Подписка успешно восстановлена.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="391"/>
|
||||
<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"/>
|
||||
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="751"/>
|
||||
<source>API config reloaded</source>
|
||||
<translation>Конфигурация API перезагружена</translation>
|
||||
</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>
|
||||
<translation>Страна подключения изменена на %1</translation>
|
||||
</message>
|
||||
@@ -210,24 +182,29 @@
|
||||
<translation><p><a style="color: #EB5757;">Недоступно в вашем регионе. Если у вас включен VPN, отключите его, вернитесь на предыдущий экран и попробуйте снова.</a></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/models/api/apiServicesModel.cpp" line="95"/>
|
||||
<source>%1 MBit/s</source>
|
||||
<translation type="vanished">%1 Мбит/с</translation>
|
||||
<translation>%1 Мбит/с</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/models/api/apiServicesModel.cpp" line="102"/>
|
||||
<source>%1 days</source>
|
||||
<translation type="vanished">%1 дней</translation>
|
||||
<translation>%1 дней</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/models/api/apiServicesModel.cpp" line="113"/>
|
||||
<source>Free</source>
|
||||
<translation type="vanished">Бесплатно</translation>
|
||||
<translation>Бесплатно</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/models/api/apiServicesModel.cpp" line="116"/>
|
||||
<source>%1 $</source>
|
||||
<translation type="vanished">%1 $</translation>
|
||||
<translation>%1 $</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/models/api/apiServicesModel.cpp" line="118"/>
|
||||
<source>%1 $/month</source>
|
||||
<translation type="vanished">%1 $/месяц</translation>
|
||||
<translation>%1 $/месяц</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
@@ -264,45 +241,45 @@
|
||||
<context>
|
||||
<name>ConnectionController</name>
|
||||
<message>
|
||||
<location filename="../ui/controllers/connectionController.cpp" line="82"/>
|
||||
<location filename="../ui/controllers/connectionController.cpp" line="81"/>
|
||||
<source>Connecting...</source>
|
||||
<translation>Подключение...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/controllers/connectionController.cpp" line="89"/>
|
||||
<location filename="../ui/controllers/connectionController.cpp" line="86"/>
|
||||
<source>Connected</source>
|
||||
<translation>Подключено</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/controllers/connectionController.cpp" line="113"/>
|
||||
<location filename="../ui/controllers/connectionController.cpp" line="110"/>
|
||||
<source>Preparing...</source>
|
||||
<translation>Подготовка...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/controllers/connectionController.cpp" line="135"/>
|
||||
<location filename="../ui/controllers/connectionController.cpp" line="132"/>
|
||||
<source>Settings updated successfully, reconnnection...</source>
|
||||
<translation>Настройки успешно обновлены, переподключение...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/controllers/connectionController.cpp" line="138"/>
|
||||
<location filename="../ui/controllers/connectionController.cpp" line="135"/>
|
||||
<source>Settings updated successfully</source>
|
||||
<translation>Настройки успешно обновлены</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/controllers/connectionController.cpp" line="98"/>
|
||||
<location filename="../ui/controllers/connectionController.cpp" line="95"/>
|
||||
<source>Reconnecting...</source>
|
||||
<translation>Переподключение...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/controllers/connectionController.h" line="70"/>
|
||||
<location filename="../ui/controllers/connectionController.cpp" line="103"/>
|
||||
<location filename="../ui/controllers/connectionController.cpp" line="118"/>
|
||||
<location filename="../ui/controllers/connectionController.cpp" line="124"/>
|
||||
<location filename="../ui/controllers/connectionController.cpp" line="100"/>
|
||||
<location filename="../ui/controllers/connectionController.cpp" line="115"/>
|
||||
<location filename="../ui/controllers/connectionController.cpp" line="121"/>
|
||||
<source>Connect</source>
|
||||
<translation>Подключиться</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/controllers/connectionController.cpp" line="108"/>
|
||||
<location filename="../ui/controllers/connectionController.cpp" line="105"/>
|
||||
<source>Disconnecting...</source>
|
||||
<translation>Отключение...</translation>
|
||||
</message>
|
||||
@@ -1720,32 +1697,17 @@ Thank you for staying with us!</source>
|
||||
<context>
|
||||
<name>PageSettingsApiAvailableCountries</name>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiAvailableCountries.qml" line="129"/>
|
||||
<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"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiAvailableCountries.qml" line="84"/>
|
||||
<source>Location for connection</source>
|
||||
<translation>Страны для подключения</translation>
|
||||
</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>
|
||||
<translation>Невозможно изменить локацию во время попытки соединения</translation>
|
||||
</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>
|
||||
<translation>Невозможно изменить локацию во время активного соединения</translation>
|
||||
</message>
|
||||
@@ -1977,12 +1939,12 @@ Thank you for staying with us!</source>
|
||||
<context>
|
||||
<name>PageSettingsApiServerInfo</name>
|
||||
<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>
|
||||
<translation>Сетевые адреса одного или нескольких серверов были обновлены. Пожалуйста, удалите старые конфигурацию и загрузите новые файлы</translation>
|
||||
</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>
|
||||
<translation>Управление файлами конфигурации</translation>
|
||||
</message>
|
||||
@@ -2002,122 +1964,106 @@ Thank you for staying with us!</source>
|
||||
<translation>Активные соединения</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="150"/>
|
||||
<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"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="166"/>
|
||||
<source>Use VLESS protocol</source>
|
||||
<translation>Использовать протокол VLESS</translation>
|
||||
</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>
|
||||
<translation>Невозможно изменить протокол во время активного соединения</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="319"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="211"/>
|
||||
<source>Subscription Key</source>
|
||||
<translation>Ключ для подключения</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="341"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="233"/>
|
||||
<source>Configuration Files</source>
|
||||
<translation>Файлы конфигурации</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="361"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="253"/>
|
||||
<source>Active Devices</source>
|
||||
<translation>Активные устройства</translation>
|
||||
</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>
|
||||
<translation>Управление подключенными устройствами</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="380"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="272"/>
|
||||
<source>Support</source>
|
||||
<translation>Поддержка</translation>
|
||||
</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>
|
||||
<translation>Как подключить другие устройства</translation>
|
||||
</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>
|
||||
<translation>Перезагрузить конфигурацию API</translation>
|
||||
</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>
|
||||
<translation>Перезагрузить конфигурацию API?</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="424"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="462"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="499"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="316"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="354"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="391"/>
|
||||
<source>Continue</source>
|
||||
<translation>Продолжить</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="425"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="463"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="500"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="317"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="355"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="392"/>
|
||||
<source>Cancel</source>
|
||||
<translation>Отменить</translation>
|
||||
</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>
|
||||
<translation>Невозможно перзагрузить API конфигурацию при активном соединении</translation>
|
||||
</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>
|
||||
<translation>Отвязать это устройство</translation>
|
||||
</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>
|
||||
<translation>Вы уверены, что хотите отвязать это устройство?</translation>
|
||||
</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 "Reload API config" in subscription settings on device.</source>
|
||||
<translation>Это отключит устройство от вашей подписки. Вы можете повторно подключить его в любое время, нажав "Перезагрузить конфигурацию API" в настройках подписки на устройстве.</translation>
|
||||
</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>
|
||||
<translation>Невозможно отвязать устройство во время активного соединения</translation>
|
||||
</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>
|
||||
<translation>Удалить из приложения</translation>
|
||||
</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>
|
||||
<translation>Удалить из приложения?</translation>
|
||||
</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>
|
||||
<translation>Невозможно удалить сервер во время активного соединения</translation>
|
||||
</message>
|
||||
@@ -3165,83 +3111,51 @@ Thank you for staying with us!</source>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>PageSetupWizardApiFreeInfo</name>
|
||||
<name>PageSetupWizardApiServiceInfo</name>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardApiFreeInfo.qml" line="74"/>
|
||||
<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"/>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="113"/>
|
||||
<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>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardApiPremiumInfo.qml" line="169"/>
|
||||
<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>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="125"/>
|
||||
<source>Subscribe Now</source>
|
||||
<translation type="vanished">Подписаться сейчас</translation>
|
||||
<translation>Подписаться сейчас</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="158"/>
|
||||
<source>By continuing, you agree to the <a href="%1" style="color: #FBB26A;">Terms of Use</a> and <a href="%2" style="color: #FBB26A;">Privacy Policy</a></source>
|
||||
<translation type="vanished">Продолжая, вы соглашаетесь с <a href="%1" style="color: #FBB26A;">Условиями использования</a> и <a href="%2" style="color: #FBB26A;">Политикой конфиденциальности</a></translation>
|
||||
<translation>Продолжая, вы соглашаетесь с <a href="%1" style="color: #FBB26A;">Условиями использования</a> и <a href="%2" style="color: #FBB26A;">Политикой конфиденциальности</a></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="186"/>
|
||||
<source>For the region</source>
|
||||
<translation type="vanished">Для региона</translation>
|
||||
<translation>Для региона</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="195"/>
|
||||
<source>Price</source>
|
||||
<translation type="vanished">Цена</translation>
|
||||
<translation>Цена</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="204"/>
|
||||
<source>Work period</source>
|
||||
<translation type="vanished">Период работы</translation>
|
||||
<translation>Период работы</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="213"/>
|
||||
<source>Speed</source>
|
||||
<translation type="vanished">Скорость</translation>
|
||||
<translation>Скорость</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="222"/>
|
||||
<source>Features</source>
|
||||
<translation type="vanished">Особенности</translation>
|
||||
<translation>Особенности</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="125"/>
|
||||
<source>Connect</source>
|
||||
<translation type="vanished">Подключиться</translation>
|
||||
<translation>Подключиться</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
@@ -3256,50 +3170,11 @@ Thank you for staying with us!</source>
|
||||
<source>Choose a VPN service that suits your needs.</source>
|
||||
<translation>Выберите VPN-сервис, который подходит именно вам.</translation>
|
||||
</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>
|
||||
<name>PageSetupWizardConfigSource</name>
|
||||
<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>
|
||||
<translation>Файл с настройками подключения</translation>
|
||||
</message>
|
||||
@@ -3374,80 +3249,71 @@ Thank you for staying with us!</source>
|
||||
<translation>Другие варианты подключения</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="226"/>
|
||||
<source>Recommended</source>
|
||||
<translation>Рекомендуется</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="256"/>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="253"/>
|
||||
<source>Site Amnezia</source>
|
||||
<translation>Сайт Amnezia</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="281"/>
|
||||
<source>The easiest way to connect to the VPN</source>
|
||||
<translation>Самый простой способ подключиться к VPN</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="367"/>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="358"/>
|
||||
<source>Restore purchases</source>
|
||||
<translation>Восстановить покупки</translation>
|
||||
</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>
|
||||
<translation>VPN от Amnezia</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="278"/>
|
||||
<source>Connect to classic paid and free VPN services from Amnezia</source>
|
||||
<translation type="vanished">Подключайтесь к классическим платным и бесплатным VPN-сервисам от Amnezia</translation>
|
||||
<translation>Подключайтесь к классическим платным и бесплатным VPN-сервисам от Amnezia</translation>
|
||||
</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>
|
||||
<translation>Self-hosted VPN</translation>
|
||||
</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>
|
||||
<translation>Настроить VPN на собственном сервере</translation>
|
||||
</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>
|
||||
<translation>Восстановить из резервной копии</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="313"/>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="332"/>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="352"/>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="368"/>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="383"/>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="307"/>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="325"/>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="344"/>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="359"/>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="373"/>
|
||||
<source></source>
|
||||
<translation></translation>
|
||||
</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>
|
||||
<translation>Открыть резервную копию</translation>
|
||||
</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>
|
||||
<translation>Файлы резервных копий (*.backup)</translation>
|
||||
</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>
|
||||
<translation>Открыть файл с конфигурацией</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="351"/>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="343"/>
|
||||
<source>QR code</source>
|
||||
<translation>QR-код</translation>
|
||||
</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>
|
||||
<translation>У меня ничего нет</translation>
|
||||
</message>
|
||||
@@ -3455,17 +3321,17 @@ Thank you for staying with us!</source>
|
||||
<context>
|
||||
<name>PageSetupWizardCredentials</name>
|
||||
<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>
|
||||
<translation>IP-адрес[:порт] сервера</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="112"/>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="100"/>
|
||||
<source>Continue</source>
|
||||
<translation>Продолжить</translation>
|
||||
</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>
|
||||
<translation>Введите адрес в формате 255.255.255.255:88</translation>
|
||||
</message>
|
||||
@@ -3475,54 +3341,48 @@ Thank you for staying with us!</source>
|
||||
<translation>Настроить ваш сервер</translation>
|
||||
</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>
|
||||
<translation>255.255.255.255:22</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="215"/>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="203"/>
|
||||
<source>SSH Username</source>
|
||||
<translation>Имя пользователя SSH</translation>
|
||||
</message>
|
||||
<message>
|
||||
<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="224"/>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="212"/>
|
||||
<source>Password or SSH private key</source>
|
||||
<translation>Пароль или закрытый ключ SSH</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="97"/>
|
||||
<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 doesn’t 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"/>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="132"/>
|
||||
<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>
|
||||
</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>
|
||||
<translation>Как создать VPN на собственном сервере</translation>
|
||||
</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>
|
||||
<translation>Где взять данные для подключения, пошаговые инструкции по покупке VPS</translation>
|
||||
</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>
|
||||
<translation>Поле с IP-адресом не может быть пустым</translation>
|
||||
</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>
|
||||
<translation>Поле с логином не может быть пустым</translation>
|
||||
</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>
|
||||
<translation>Поле с паролем/закрытым ключом не может быть пустым</translation>
|
||||
</message>
|
||||
@@ -3656,7 +3516,7 @@ Thank you for staying with us!</source>
|
||||
<context>
|
||||
<name>PageSetupWizardStart</name>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardStart.qml" line="42"/>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardStart.qml" line="41"/>
|
||||
<source>Let's get started</source>
|
||||
<translation>Приступим</translation>
|
||||
</message>
|
||||
@@ -4466,22 +4326,7 @@ Thank you for staying with us!</source>
|
||||
<translation>Не удалось обработать покупку</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../core/errorstrings.cpp" line="83"/>
|
||||
<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"/>
|
||||
<location filename="../core/errorstrings.cpp" line="97"/>
|
||||
<source>ErrorCode: %1. </source>
|
||||
<translation>Код ошибки: %1. </translation>
|
||||
</message>
|
||||
@@ -4581,37 +4426,37 @@ Thank you for staying with us!</source>
|
||||
<translation>Превышен лимит разрешенных конфигураций для одной подписки</translation>
|
||||
</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>
|
||||
<translation>Ошибка QFile: не удалось открыть файл</translation>
|
||||
</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>
|
||||
<translation>Ошибка QFile: произошла ошибка при чтении из файла</translation>
|
||||
</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>
|
||||
<translation>Ошибка QFile: не удалось получить доступ к файлу</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../core/errorstrings.cpp" line="91"/>
|
||||
<location filename="../core/errorstrings.cpp" line="88"/>
|
||||
<source>QFile error: An unspecified error occurred</source>
|
||||
<translation>Ошибка QFile: произошла неизвестная ошибка</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../core/errorstrings.cpp" line="92"/>
|
||||
<location filename="../core/errorstrings.cpp" line="89"/>
|
||||
<source>QFile error: A fatal error occurred</source>
|
||||
<translation>Ошибка QFile: произошла фатальная ошибка</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../core/errorstrings.cpp" line="93"/>
|
||||
<location filename="../core/errorstrings.cpp" line="90"/>
|
||||
<source>QFile error: The operation was aborted</source>
|
||||
<translation>Ошибка QFile: операция была прервана</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../core/errorstrings.cpp" line="97"/>
|
||||
<location filename="../core/errorstrings.cpp" line="94"/>
|
||||
<source>Internal error</source>
|
||||
<translation>Внутренняя ошибка</translation>
|
||||
</message>
|
||||
@@ -5140,17 +4985,7 @@ FileZilla или другие SFTP-клиенты, а также смонтир
|
||||
<context>
|
||||
<name>ServersListView</name>
|
||||
<message>
|
||||
<location filename="../ui/qml/Components/ServersListView.qml" line="71"/>
|
||||
<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"/>
|
||||
<location filename="../ui/qml/Components/ServersListView.qml" line="79"/>
|
||||
<source>Unable change server while there is an active connection</source>
|
||||
<translation>Невозможно изменить сервер во время активного соединения</translation>
|
||||
</message>
|
||||
@@ -5172,17 +5007,12 @@ FileZilla или другие SFTP-клиенты, а также смонтир
|
||||
<context>
|
||||
<name>SettingsController</name>
|
||||
<message>
|
||||
<location filename="../ui/controllers/settingsController.cpp" line="185"/>
|
||||
<source>Can't open file: %1</source>
|
||||
<translation>Невозможно открыть файл: %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/controllers/settingsController.cpp" line="271"/>
|
||||
<location filename="../ui/controllers/settingsController.cpp" line="270"/>
|
||||
<source>All settings have been reset to default values</source>
|
||||
<translation>Все настройки сброшены до значений по умолчанию</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/controllers/settingsController.cpp" line="248"/>
|
||||
<location filename="../ui/controllers/settingsController.cpp" line="247"/>
|
||||
<source>Backup file is corrupted</source>
|
||||
<translation>Файл резервной копии поврежден</translation>
|
||||
</message>
|
||||
@@ -5235,29 +5065,6 @@ FileZilla или другие SFTP-клиенты, а также смонтир
|
||||
<translation>Экспорт завершен</translation>
|
||||
</message>
|
||||
</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>
|
||||
<name>SystemTrayNotificationHandler</name>
|
||||
<message>
|
||||
@@ -5291,14 +5098,6 @@ FileZilla или другие SFTP-клиенты, а также смонтир
|
||||
<translation>Закрыть</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>TermsAndPrivacyText</name>
|
||||
<message>
|
||||
<location filename="../ui/qml/Components/TermsAndPrivacyText.qml" line="23"/>
|
||||
<source>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></source>
|
||||
<translation>Продолжая, вы соглашаетесь с <a href="%1" style="color: %3;">Условиями использования</a> и <a href="%2" style="color: %3;">Политикой конфиденциальности</a></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>TextFieldWithHeaderType</name>
|
||||
<message>
|
||||
@@ -5374,12 +5173,12 @@ FileZilla или другие SFTP-клиенты, а также смонтир
|
||||
<context>
|
||||
<name>main2</name>
|
||||
<message>
|
||||
<location filename="../ui/qml/main2.qml" line="247"/>
|
||||
<location filename="../ui/qml/main2.qml" line="230"/>
|
||||
<source>Private key passphrase</source>
|
||||
<translation>Парольная фраза для закрытого ключа</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/main2.qml" line="268"/>
|
||||
<location filename="../ui/qml/main2.qml" line="251"/>
|
||||
<source>Save</source>
|
||||
<translation>Сохранить</translation>
|
||||
</message>
|
||||
|
||||
@@ -488,6 +488,8 @@ bool ApiConfigsController::exportNativeConfig(const QString &serverCountryCode,
|
||||
return false;
|
||||
}
|
||||
|
||||
qDebug() << responseBody;
|
||||
|
||||
QJsonObject jsonConfig = QJsonDocument::fromJson(responseBody).object();
|
||||
QString nativeConfig = jsonConfig.value(configKey::config).toString();
|
||||
nativeConfig.replace("$WIREGUARD_CLIENT_PRIVATE_KEY", protocolData.wireGuardClientPrivKey);
|
||||
@@ -661,7 +663,7 @@ bool ApiConfigsController::importPremiumFromAppStore(const QString &storeProduct
|
||||
int duplicateServerIndex = -1;
|
||||
errorCode = importServiceFromBilling(responseBody, isTestPurchase, duplicateServerIndex);
|
||||
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;
|
||||
}
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
@@ -669,7 +671,7 @@ bool ApiConfigsController::importPremiumFromAppStore(const QString &storeProduct
|
||||
return false;
|
||||
}
|
||||
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;
|
||||
#else
|
||||
Q_UNUSED(storeProductId);
|
||||
@@ -799,7 +801,7 @@ bool ApiConfigsController::restoreServiceFromAppStore()
|
||||
|
||||
if (!hasInstalledConfig) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -866,8 +868,6 @@ bool ApiConfigsController::importFreeFromGateway()
|
||||
|
||||
bool ApiConfigsController::importTrialFromGateway(const QString &email)
|
||||
{
|
||||
emit trialEmailError(QString());
|
||||
|
||||
const QString trimmedEmail = email.trimmed();
|
||||
if (trimmedEmail.isEmpty()) {
|
||||
emit errorOccurred(ErrorCode::ApiConfigEmptyError);
|
||||
@@ -884,6 +884,12 @@ bool ApiConfigsController::importTrialFromGateway(const QString &email)
|
||||
m_apiServicesModel->getSelectedServiceProtocol(),
|
||||
QJsonObject() };
|
||||
|
||||
if (m_serversModel->isServerFromApiAlreadyExists(gatewayRequestData.userCountryCode, gatewayRequestData.serviceType,
|
||||
gatewayRequestData.serviceProtocol)) {
|
||||
emit errorOccurred(ErrorCode::ApiConfigAlreadyAdded);
|
||||
return false;
|
||||
}
|
||||
|
||||
ProtocolData protocolData = generateProtocolData(gatewayRequestData.serviceProtocol);
|
||||
|
||||
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
|
||||
@@ -893,10 +899,6 @@ bool ApiConfigsController::importTrialFromGateway(const QString &email)
|
||||
QByteArray responseBody;
|
||||
ErrorCode errorCode = executeRequest(QString("%1v1/trial"), apiPayload, responseBody);
|
||||
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);
|
||||
return false;
|
||||
}
|
||||
@@ -1027,7 +1029,7 @@ bool ApiConfigsController::updateServiceFromTelegram(const int serverIndex)
|
||||
#endif
|
||||
|
||||
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 installationUuid = m_settings->getInstallationUuid(true);
|
||||
@@ -1273,6 +1275,6 @@ ErrorCode ApiConfigsController::executeRequest(const QString &endpoint, const QJ
|
||||
bool 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);
|
||||
}
|
||||
|
||||
@@ -49,7 +49,6 @@ public slots:
|
||||
|
||||
signals:
|
||||
void errorOccurred(ErrorCode errorCode);
|
||||
void trialEmailError(const QString &message);
|
||||
void subscriptionExpiredOnServer();
|
||||
void subscriptionRefreshNeeded();
|
||||
|
||||
|
||||
@@ -32,8 +32,7 @@ void ApiNewsController::fetchNews(bool showError)
|
||||
}
|
||||
|
||||
auto gatewayController = QSharedPointer<GatewayController>::create(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(),
|
||||
apiDefs::requestTimeoutMsecs,
|
||||
m_settings->isStrictKillSwitchEnabled(), m_settings);
|
||||
apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled());
|
||||
QJsonObject payload;
|
||||
payload.insert("locale", m_settings->getAppLanguage().name().split("_").first());
|
||||
|
||||
|
||||
@@ -23,19 +23,6 @@ namespace
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -71,7 +58,7 @@ bool ApiSettingsController::getAccountInfo(bool reload)
|
||||
|
||||
bool isTestPurchase = apiConfig.value(apiDefs::key::isTestPurchase).toBool(false);
|
||||
GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase), m_settings->isDevGatewayEnv(isTestPurchase),
|
||||
requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled(), m_settings);
|
||||
requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled());
|
||||
|
||||
QJsonObject apiPayload;
|
||||
apiPayload[configKey::userCountryCode] = apiConfig.value(configKey::userCountryCode).toString();
|
||||
@@ -110,7 +97,7 @@ void ApiSettingsController::getRenewalLink()
|
||||
auto gatewayController = QSharedPointer<GatewayController>::create(m_settings->getGatewayEndpoint(isTestPurchase),
|
||||
m_settings->isDevGatewayEnv(isTestPurchase),
|
||||
requestTimeoutMsecs,
|
||||
m_settings->isStrictKillSwitchEnabled(), m_settings);
|
||||
m_settings->isStrictKillSwitchEnabled());
|
||||
|
||||
QJsonObject apiPayload;
|
||||
apiPayload[configKey::userCountryCode] = apiConfig.value(configKey::userCountryCode).toString();
|
||||
@@ -118,7 +105,6 @@ void ApiSettingsController::getRenewalLink()
|
||||
apiPayload[configKey::authData] = authData;
|
||||
apiPayload[apiDefs::key::cliVersion] = QString(APP_VERSION);
|
||||
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);
|
||||
future.then(this, [this, gatewayController](QPair<ErrorCode, QByteArray> result) {
|
||||
|
||||
@@ -217,8 +217,6 @@ bool ImportController::extractConfigFromData(QString data)
|
||||
|
||||
bool ImportController::extractConfigFromQr(const QByteArray &data)
|
||||
{
|
||||
m_configType = checkConfigFormat(QString::fromUtf8(data));
|
||||
|
||||
QJsonObject dataObj = QJsonDocument::fromJson(data).object();
|
||||
if (!dataObj.isEmpty()) {
|
||||
m_config = dataObj;
|
||||
@@ -228,13 +226,10 @@ bool ImportController::extractConfigFromQr(const QByteArray &data)
|
||||
QByteArray ba_uncompressed = qUncompress(data);
|
||||
if (!ba_uncompressed.isEmpty()) {
|
||||
m_config = QJsonDocument::fromJson(ba_uncompressed).object();
|
||||
if (m_config.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
m_configType = checkConfigFormat(QString::fromUtf8(ba_uncompressed));
|
||||
return true;
|
||||
}
|
||||
|
||||
m_configType = checkConfigFormat(data);
|
||||
if (m_configType == ConfigTypes::Invalid) {
|
||||
QByteArray ba = QByteArray::fromBase64(data, QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
|
||||
QByteArray baUncompressed = qUncompress(ba);
|
||||
@@ -245,10 +240,6 @@ bool ImportController::extractConfigFromQr(const QByteArray &data)
|
||||
|
||||
if (!ba.isEmpty()) {
|
||||
m_config = QJsonDocument::fromJson(ba).object();
|
||||
if (m_config.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
m_configType = checkConfigFormat(QString::fromUtf8(ba));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,9 +32,8 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
|
||||
return tr("Active");
|
||||
}
|
||||
|
||||
return apiUtils::isSubscriptionExpired(m_accountInfoData.subscriptionEndDate)
|
||||
? QStringLiteral("<p><a style=\"color: #EB5757;\">%1</a>").arg(tr("Inactive"))
|
||||
: QStringLiteral("<p><a style=\"color: #28c840;\">%1</a>").arg(tr("Active"));
|
||||
return apiUtils::isSubscriptionExpired(m_accountInfoData.subscriptionEndDate) ? tr("<p><a style=\"color: #EB5757;\">Inactive</a>")
|
||||
: tr("<p><a style=\"color: #28c840;\">Active</a>");
|
||||
}
|
||||
case EndDateRole: {
|
||||
if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) {
|
||||
@@ -54,11 +53,14 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
|
||||
}
|
||||
case IsComponentVisibleRole: {
|
||||
return m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2
|
||||
|| m_accountInfoData.configType == apiDefs::ConfigType::AmneziaTrialV2
|
||||
|| m_accountInfoData.configType == apiDefs::ConfigType::ExternalPremium
|
||||
|| m_accountInfoData.configType == apiDefs::ConfigType::ExternalTrial;
|
||||
}
|
||||
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: {
|
||||
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.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()) {
|
||||
accountInfoData.supportedProtocols.push_back(protocol.toString());
|
||||
|
||||
@@ -61,7 +61,6 @@ private:
|
||||
QString subscriptionDescription;
|
||||
|
||||
bool isInAppPurchase = false;
|
||||
bool isRenewalAvailable = false;
|
||||
};
|
||||
|
||||
AccountInfoData m_accountInfoData;
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace configKey
|
||||
constexpr char title[] = "title";
|
||||
constexpr char body[] = "body";
|
||||
constexpr char icon[] = "icon";
|
||||
constexpr char link[] = "link";
|
||||
constexpr char accent[] = "accent";
|
||||
}
|
||||
|
||||
QString gatewayIconKeyToUrl(const QString &iconKey)
|
||||
@@ -62,8 +62,8 @@ QVariant ApiBenefitsModel::data(const QModelIndex &index, int role) const
|
||||
return item.title;
|
||||
case BodyRole:
|
||||
return item.body;
|
||||
case LinkRole:
|
||||
return item.link;
|
||||
case AccentRole:
|
||||
return item.accent;
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
@@ -75,7 +75,7 @@ QHash<int, QByteArray> ApiBenefitsModel::roleNames() const
|
||||
{ IconRole, "icon" },
|
||||
{ TitleRole, "title" },
|
||||
{ BodyRole, "body" },
|
||||
{ LinkRole, "link" },
|
||||
{ AccentRole, "accent" },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -90,11 +90,7 @@ void ApiBenefitsModel::updateModel(const QJsonArray &benefits)
|
||||
const QJsonObject benefitObject = benefitValue.toObject();
|
||||
QString title = benefitObject.value(configKey::title).toString();
|
||||
QString body = benefitObject.value(configKey::body).toString();
|
||||
const bool isLink = benefitObject.value(configKey::link).toBool();
|
||||
const QString iconKey = benefitObject.value(configKey::icon).toString();
|
||||
if (isLink) {
|
||||
body = body.trimmed();
|
||||
}
|
||||
if (title.isEmpty() && body.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
@@ -102,7 +98,7 @@ void ApiBenefitsModel::updateModel(const QJsonArray &benefits)
|
||||
item.icon = gatewayIconKeyToUrl(iconKey);
|
||||
item.title = std::move(title);
|
||||
item.body = std::move(body);
|
||||
item.link = isLink;
|
||||
item.accent = benefitObject.value(configKey::accent).toBool();
|
||||
m_serviceBenefits.append(std::move(item));
|
||||
}
|
||||
endResetModel();
|
||||
|
||||
@@ -15,7 +15,7 @@ public:
|
||||
IconRole = Qt::UserRole + 1,
|
||||
TitleRole,
|
||||
BodyRole,
|
||||
LinkRole
|
||||
AccentRole
|
||||
};
|
||||
Q_ENUM(Roles)
|
||||
|
||||
@@ -34,7 +34,7 @@ private:
|
||||
QString icon;
|
||||
QString title;
|
||||
QString body;
|
||||
bool link = false;
|
||||
bool accent = false;
|
||||
};
|
||||
|
||||
QVector<ServiceBenefitItem> m_serviceBenefits;
|
||||
|
||||
@@ -91,12 +91,6 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case IsPremiumRole: {
|
||||
return serviceType == serviceType::amneziaPremium;
|
||||
}
|
||||
case HasSubscriptionPlansRole: {
|
||||
return !apiServiceData.subscriptionPlansJson.isEmpty();
|
||||
}
|
||||
case PriceRole: {
|
||||
return apiServiceData.minPriceLabel;
|
||||
}
|
||||
@@ -239,8 +233,6 @@ QHash<int, QByteArray> ApiServicesModel::roleNames() const
|
||||
roles[CardDescriptionRole] = "cardDescription";
|
||||
roles[ServiceDescriptionRole] = "serviceDescription";
|
||||
roles[IsServiceAvailableRole] = "isServiceAvailable";
|
||||
roles[IsPremiumRole] = "isPremium";
|
||||
roles[HasSubscriptionPlansRole] = "hasSubscriptionPlans";
|
||||
roles[PriceRole] = "price";
|
||||
roles[EndDateRole] = "endDate";
|
||||
roles[TermsOfUseUrlRole] = "termsOfUseUrl";
|
||||
|
||||
@@ -54,8 +54,6 @@ public:
|
||||
CardDescriptionRole,
|
||||
ServiceDescriptionRole,
|
||||
IsServiceAvailableRole,
|
||||
IsPremiumRole,
|
||||
HasSubscriptionPlansRole,
|
||||
PriceRole,
|
||||
EndDateRole,
|
||||
TermsOfUseUrlRole,
|
||||
|
||||
@@ -179,9 +179,6 @@ QVariant ServersModel::data(const QModelIndex &index, int role) const
|
||||
case AdEndpointRole: {
|
||||
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: {
|
||||
if (configVersion != apiDefs::ConfigSource::AmneziaGateway) {
|
||||
return false;
|
||||
@@ -476,7 +473,6 @@ QHash<int, QByteArray> ServersModel::roleNames() const
|
||||
roles[AdHeaderRole] = "adHeader";
|
||||
roles[AdDescriptionRole] = "adDescription";
|
||||
roles[AdEndpointRole] = "adEndpoint";
|
||||
roles[IsRenewalAvailableRole] = "isRenewalAvailable";
|
||||
|
||||
roles[IsSubscriptionExpiredRole] = "isSubscriptionExpired";
|
||||
roles[IsSubscriptionExpiringSoonRole] = "isSubscriptionExpiringSoon";
|
||||
|
||||
@@ -51,7 +51,6 @@ public:
|
||||
AdHeaderRole,
|
||||
AdDescriptionRole,
|
||||
AdEndpointRole,
|
||||
IsRenewalAvailableRole,
|
||||
|
||||
IsSubscriptionExpiredRole,
|
||||
IsSubscriptionExpiringSoonRole,
|
||||
|
||||
@@ -5,16 +5,19 @@
|
||||
#include <QDebug>
|
||||
#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"
|
||||
#else
|
||||
# include "systemtray_notificationhandler.h"
|
||||
#endif
|
||||
|
||||
|
||||
// static
|
||||
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);
|
||||
#else
|
||||
return new SystemTrayNotificationHandler(parent);
|
||||
|
||||
@@ -11,11 +11,7 @@ RowLayout {
|
||||
property string iconSource: ""
|
||||
property string titleText: ""
|
||||
property string bodyText: ""
|
||||
property bool link: 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
|
||||
property bool accent: false
|
||||
|
||||
spacing: 12
|
||||
|
||||
@@ -47,17 +43,22 @@ RowLayout {
|
||||
LabelTextType {
|
||||
id: bodyLabel
|
||||
width: parent.width
|
||||
text: root.bodyLineText
|
||||
color: root.link ? AmneziaStyle.color.goldenApricot : AmneziaStyle.color.mutedGray
|
||||
text: root.bodyText
|
||||
color: root.accent ? AmneziaStyle.color.goldenApricot : AmneziaStyle.color.mutedGray
|
||||
font.pixelSize: 14
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: bodyLabel
|
||||
visible: root.bodyClickable
|
||||
visible: root.accent && root.bodyText.length > 0
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ Rectangle {
|
||||
iconSource: model.icon
|
||||
titleText: model.title
|
||||
bodyText: model.body
|
||||
link: !!model.link
|
||||
accent: !!model.accent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ ListViewType {
|
||||
|
||||
text: name
|
||||
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
|
||||
descriptionColor: isServerFromGatewayApi && (isSubscriptionExpired || isSubscriptionExpiringSoon)
|
||||
? (isSubscriptionExpired ? AmneziaStyle.color.vibrantRed : AmneziaStyle.color.goldenApricot)
|
||||
|
||||
@@ -12,10 +12,11 @@ import "../Controls2/TextTypes"
|
||||
DrawerType2 {
|
||||
id: root
|
||||
|
||||
property bool isRenewalAvailable: false
|
||||
property bool isRenewalActionAvailable: false
|
||||
|
||||
onOpened: {
|
||||
isRenewalAvailable = ServersModel.getProcessedServerData("isRenewalAvailable") && !ApiAccountInfoModel.data("isInAppPurchase")
|
||||
isRenewalActionAvailable = ApiAccountInfoModel.data("isSubscriptionRenewalAvailable")
|
||||
&& !ApiAccountInfoModel.data("isInAppPurchase")
|
||||
}
|
||||
|
||||
expandedStateContent: ColumnLayout {
|
||||
@@ -43,25 +44,25 @@ DrawerType2 {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
text: ServersModel.getProcessedServerData("name") + qsTr(" subscription has expired")
|
||||
text: qsTr("Amnezia Premium subscription has expired")
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
}
|
||||
}
|
||||
|
||||
ParagraphTextType {
|
||||
visible: root.isRenewalAvailable
|
||||
visible: root.isRenewalActionAvailable
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
|
||||
text: qsTr("Renew to continue using VPN")
|
||||
text: qsTr("Renew your subscription to continue using VPN")
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
visible: root.isRenewalAvailable
|
||||
visible: root.isRenewalActionAvailable
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
@@ -95,13 +96,8 @@ DrawerType2 {
|
||||
text: qsTr("Support")
|
||||
|
||||
clickedFunc: function() {
|
||||
PageController.showBusyIndicator(true)
|
||||
let result = ApiSettingsController.getAccountInfo(false)
|
||||
PageController.showBusyIndicator(false)
|
||||
if (result) {
|
||||
root.closeTriggered()
|
||||
PageController.goToPage(PageEnum.PageSettingsApiSupport)
|
||||
}
|
||||
root.closeTriggered()
|
||||
PageController.goToPage(PageEnum.PageSettingsApiSupport)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,6 @@ ParagraphTextType {
|
||||
textFormat: Text.RichText
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
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>")
|
||||
.arg(root.termsUrl)
|
||||
|
||||
@@ -28,20 +28,20 @@ Button {
|
||||
|
||||
property string leftImageSource
|
||||
|
||||
property real textOpacity: 1.0
|
||||
|
||||
property alias focusItem: rightImage
|
||||
|
||||
hoverEnabled: true
|
||||
clip: false
|
||||
|
||||
readonly property real cardTextOpacity: !enabled ? 1.0 : pressed ? 0.7 : hovered ? 0.8 : 1.0
|
||||
|
||||
background: Rectangle {
|
||||
id: backgroundRect
|
||||
|
||||
anchors.fill: parent
|
||||
radius: 16
|
||||
|
||||
color: root.hovered && root.enabled ? root.hoveredColor : root.defaultColor
|
||||
color: defaultColor
|
||||
|
||||
Behavior on color {
|
||||
PropertyAnimation { duration: 200 }
|
||||
@@ -51,7 +51,6 @@ Button {
|
||||
contentItem: Item {
|
||||
id: contentRoot
|
||||
|
||||
z: 1
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
@@ -130,7 +129,7 @@ Button {
|
||||
Layout.topMargin: contentRoot.badgeVisible ? 0 : 16
|
||||
Layout.bottomMargin: root.bodyText !== "" ? 0 : 16
|
||||
|
||||
opacity: root.cardTextOpacity
|
||||
opacity: root.textOpacity
|
||||
}
|
||||
|
||||
CaptionTextType {
|
||||
@@ -139,16 +138,13 @@ Button {
|
||||
|
||||
color: root.bodyTextColor
|
||||
textFormat: Text.RichText
|
||||
onLinkActivated: function(link) {
|
||||
Qt.openUrlExternally(link)
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.bottomMargin: root.footerText !== "" ? 0 : 8
|
||||
|
||||
opacity: root.cardTextOpacity
|
||||
opacity: root.textOpacity
|
||||
}
|
||||
|
||||
ButtonTextType {
|
||||
@@ -163,7 +159,7 @@ Button {
|
||||
Layout.topMargin: 16
|
||||
Layout.bottomMargin: 16
|
||||
|
||||
opacity: root.cardTextOpacity
|
||||
opacity: root.textOpacity
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +184,7 @@ Button {
|
||||
|
||||
anchors.fill: parent
|
||||
radius: 12
|
||||
color: root.pressed ? rightImage.pressedColor : root.hovered ? rightImage.hoveredColor : rightImage.defaultColor
|
||||
color: "transparent"
|
||||
|
||||
Behavior on color {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ Switch {
|
||||
Keys.onSpacePressed: event => handleSwitch(event)
|
||||
|
||||
function handleSwitch(event) {
|
||||
if (root.enabled && !event.isAutoRepeat) {
|
||||
if (!event.isAutoRepeat) {
|
||||
root.checked = !root.checked
|
||||
root.toggled()
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import SortFilterProxyModel 0.2
|
||||
|
||||
import PageEnum 1.0
|
||||
import Style 1.0
|
||||
|
||||
@@ -50,8 +52,6 @@ PageType {
|
||||
width: listView.width
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
id: gatewayEndpointField
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
@@ -64,25 +64,13 @@ PageType {
|
||||
|
||||
clickedFunc: function() {
|
||||
SettingsController.resetGatewayEndpoint()
|
||||
gatewayEndpointField.textField.text = SettingsController.gatewayEndpoint
|
||||
}
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: saveButton
|
||||
|
||||
Layout.fillWidth: true
|
||||
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
|
||||
textField.onEditingFinished: {
|
||||
textField.text = textField.text.replace(/^\s+|\s+$/g, '')
|
||||
if (textField.text !== SettingsController.gatewayEndpoint) {
|
||||
SettingsController.gatewayEndpoint = textField.text
|
||||
}
|
||||
PageController.showNotificationMessage(qsTr("Settings saved"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -482,7 +482,6 @@ PageType {
|
||||
|
||||
headerText: qsTr("I5 - Special junk 5")
|
||||
textField.text: serverSpecialJunk5
|
||||
checkEmptyText: false
|
||||
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== serverSpecialJunk5) {
|
||||
|
||||
@@ -259,7 +259,6 @@ PageType {
|
||||
id: switcher
|
||||
|
||||
readonly property bool isVlessProtocol: ApiConfigsController.isVlessProtocol()
|
||||
readonly property bool isProtocolSwitchBlocked: ServersModel.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 24
|
||||
@@ -267,7 +266,6 @@ PageType {
|
||||
Layout.leftMargin: 16
|
||||
|
||||
visible: ApiAccountInfoModel.data("isProtocolSelectionSupported")
|
||||
enabled: !switcher.isProtocolSwitchBlocked
|
||||
|
||||
text: qsTr("Use VLESS protocol")
|
||||
checked: switcher.isVlessProtocol
|
||||
|
||||
@@ -67,11 +67,8 @@ PageType {
|
||||
}
|
||||
|
||||
delegate: ColumnLayout {
|
||||
property bool hideCard: isPremium && !hasSubscriptionPlans
|
||||
|
||||
width: listView.width
|
||||
visible: !hideCard
|
||||
height: hideCard ? 0 : implicitHeight
|
||||
|
||||
enabled: isServiceAvailable
|
||||
|
||||
|
||||
@@ -13,16 +13,6 @@ import "../Components"
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
property string trialEmailErrorMessage: ""
|
||||
|
||||
Connections {
|
||||
target: ApiConfigsController
|
||||
|
||||
function onTrialEmailError(message) {
|
||||
root.trialEmailErrorMessage = message
|
||||
emailField.errorText = message
|
||||
}
|
||||
}
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
@@ -77,17 +67,6 @@ PageType {
|
||||
headerText: qsTr("Email")
|
||||
textField.placeholderText: qsTr("Email")
|
||||
textField.inputMethodHints: Qt.ImhEmailCharactersOnly
|
||||
|
||||
Connections {
|
||||
target: emailField.textField
|
||||
|
||||
function onTextChanged() {
|
||||
if (root.trialEmailErrorMessage !== "") {
|
||||
root.trialEmailErrorMessage = ""
|
||||
emailField.errorText = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ParagraphTextType {
|
||||
@@ -99,7 +78,7 @@ PageType {
|
||||
wrapMode: Text.WordWrap
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
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")
|
||||
|
||||
clickedFunc: function() {
|
||||
root.trialEmailErrorMessage = ""
|
||||
emailField.errorText = ""
|
||||
|
||||
var raw = emailField.textField.text.trim()
|
||||
if (raw.length === 0 || raw.indexOf("@") < 0) {
|
||||
PageController.showNotificationMessage(qsTr("Enter a valid email address"))
|
||||
|
||||
@@ -278,7 +278,7 @@ PageType {
|
||||
id: amneziaVpn
|
||||
|
||||
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 bool featuredAmneziaConnection: true
|
||||
property bool isVisible: true
|
||||
|
||||
@@ -94,7 +94,7 @@ PageType {
|
||||
visible: title === qsTr("Password or SSH private key")
|
||||
backGroundColor: AmneziaStyle.color.translucentWhite
|
||||
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 doesn’t 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 doesn’t work, generate a compatible one.")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
# include "platforms/macos/macosutils.h"
|
||||
#endif
|
||||
|
||||
#ifdef MACOS_NE
|
||||
# include "platforms/macos/macos_ne_vpn_notification.h"
|
||||
#endif
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDesktopServices>
|
||||
#include <QIcon>
|
||||
@@ -152,6 +156,12 @@ void SystemTrayNotificationHandler::notify(NotificationHandler::Message type,
|
||||
int timerMsec) {
|
||||
Q_UNUSED(type);
|
||||
|
||||
#ifdef MACOS_NE
|
||||
Q_UNUSED(timerMsec);
|
||||
macosNePostVpnStateNotification(title, message);
|
||||
return;
|
||||
#endif
|
||||
|
||||
QIcon icon(ConnectedTrayIconName);
|
||||
m_systemTrayIcon.showMessage(title, message, icon, timerMsec);
|
||||
}
|
||||
|
||||
@@ -97,6 +97,17 @@ echo "Using Qt in $QT_BIN_DIR"
|
||||
echo "Using Android SDK in $ANDROID_SDK_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
|
||||
qt_cmake_opts=()
|
||||
|
||||
@@ -106,6 +117,12 @@ else
|
||||
qt_cmake_opts+=(-DQT_ANDROID_ABIS="$ABIS")
|
||||
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
|
||||
# We'll build apks during androiddeployqt
|
||||
$QT_BIN_DIR/qt-cmake -S $PROJECT_DIR -B $BUILD_DIR \
|
||||
|
||||
Reference in New Issue
Block a user