Compare commits

...

50 Commits

Author SHA1 Message Date
NickVs2015 2c168bc9ed fix: add logs and rename functions 2026-02-25 11:44:34 +03:00
NickVs2015 0d7f9381c1 feat: add test purchase android 2026-02-20 13:24:37 +03:00
NickVs2015 fe99cdeb85 feat: add purchase to UI 2026-02-20 12:35:21 +03:00
albexk 4c03463344 refactor: modify response json 2026-02-20 12:35:21 +03:00
albexk d4f6add807 Add methods to verify and purchase subscriptions 2026-02-20 12:35:21 +03:00
albexk 545c766732 Add method to get the list of offers 2026-02-20 12:35:21 +03:00
albexk 494e93d4ab Build script refactor to support AAB build for Google Play 2026-02-20 12:35:21 +03:00
albexk 4b6ec29761 Add a method to detect Google Play build 2026-02-20 12:35:21 +03:00
albexk daa44a2672 Add billing module, provide a separate build for Google Play 2026-02-20 12:35:21 +03:00
albexk 5e23eed2bc Disable Qt debugger sleep 2026-02-20 12:35:21 +03:00
albexk 012135aea6 Bump Android version code to 59 2026-02-20 12:35:21 +03:00
albexk 58acf71858 Add Google Play Billing Library 2026-02-20 12:31:52 +03:00
Mitternacht822 02be6dc5f9 chore: add license to msi installer (#2227) 2026-02-20 12:13:08 +08:00
vkamn bfcf7f0305 chore: bump version (#2244) 2026-02-19 20:27:42 +08:00
Mitternacht822 2bce595ade fix: remove revoke from remove subscription flow (#2226)
* fix(revoke): now revoke calls only for unlink device action

* fix: removed revoke call when removing a subscription from the app
2026-02-19 20:23:13 +08:00
Yaroslav Gurov cd1e561fd4 fix: add network watcher back (#2240)
* feat: add reconnect in case of changing network

* fix: reconnect to VPN on wakeup

* fix: linux wakeup build
2026-02-19 20:21:49 +08:00
Mitternacht822 9bd1e6a0f5 fix: added stop and delete AmneziaVPNSplitTunnel on uninstall (#2222) 2026-02-18 11:21:59 +08:00
Yaroslav Gurov 5058c9aa6f fix: do not enable killswitch by default when service starts (#2232) 2026-02-18 10:59:16 +08:00
vkamn d78416835c chore: change default i1 value (#2216) 2026-02-13 17:10:10 +08:00
vkamn 40e6c6aae3 feat: native wg with obfuscation (#2209)
* chore: change default i1 value

* feat: add i1 to native wg with obfuscation
2026-02-12 11:34:52 +08:00
Yaroslav Gurov 911a999c64 fix: xray stability and split-tunneling (#2187)
* fix: xray heap corruption

* fix: use proper configuration for split-tunneled apps

* chore: enable killswitch

* chore: xray windows split-tunneling cleanup

* chore: proper xray killswitch log

* feat: add wait for the tun device

* chore: update amnezia_xray deps for macos

* fix: add nullptr check for split-tunnel on win

* fix: modernize vpnAdapter grabbing function

* fix: remove network watcher due to its fragileness

* chore: xrayprotocol cleanup

* fix: correct wrong iface index on win

* chore: move tun2socks implementation to the client from the service

* chore: xrayprotocol cleanup

* chore: more xrayprotocol cleanup

* fix: consistent tun device with GUID specified

* chore: tun2socks logs

* chore: PrivilegedProcess cleanup
* better error handling in establishment phase
* terminate&kill ops for remote process

* fix: straighforward killing the process on windows

* fix: finally remove GUID setting from tun2socks due to instability

* fix: add sanitizer to ipc process

* chore: do not collect sensitive info from tun2socks
2026-02-11 23:47:28 +08:00
MrMirDan b4f4184aa6 fix: returned mentioned lines (#2205) 2026-02-11 23:44:11 +08:00
NickVs2015 5c6db4b7a4 fix OpenGl error (#2185) 2026-02-10 11:15:31 +08:00
vkamn f6277cdbb2 fix: native wg obfuscation (#2199)
* chore: bump version

* fix: fixed native wg obfuscation
2026-02-09 10:54:30 +08:00
NickVs2015 99312e61d3 fix: allow start Gamepad only Android (#2198) 2026-02-09 10:40:48 +08:00
NickVs2015 9f0ae75a2f feat: add gamepad buttons support android (#2066)
* feat: add support gamepad buttons

* feat: add support gamepad with github repo

* feat: add gitmodules dependency

* feat: add submodule qtgamepad

* chore: update qtgamepad submodule to commit 4e57142e563b931766056b4c7507c16892260222

* fix: update qtgamepad with standard CMake and private headers support

Update qtgamepad to commit f72b3e0 which:
- Replaces qt_add_library with standard add_library to avoid Qt 6.10 macro conflicts
- Copies private headers to build include tree for Android backend
- Creates Qt:: and Qt6:: namespace aliases for proper linking
2026-02-05 22:57:15 +08:00
vkamn 7960d8015d feat: add EULA and policy on IAP page (#2189) 2026-02-05 20:23:06 +08:00
vkamn 5dcc64e5e5 fix: deploy qopensslbackend on windows (#2190) 2026-02-05 20:22:47 +08:00
MrMirDan 964436ad43 fix: placeholder color, hide button image transparency, removed some lines (#2123)
* fix: placeholder color, hide button image transparency, removed unneccessary lines

* update: removed opacity on tunneling page

* update: remove opacity on app tunneling page
2026-02-05 12:56:41 +08:00
ik 4fc3900fd5 Merge pull request #2184 from amnezia-vpn/chore/add-release-date-upload
chore: add sending of release_date to s3
2026-02-04 12:20:23 +03:00
irvinklause 8f5e42dd61 chore: add sending of release_date to s3 2026-02-04 07:38:44 +00:00
Yaroslav Gurov 24895752c1 fix: added enablePeerTraffic call to xray (#2179)
* fix: add enablePeerTraffic call to xray

* chore: remove unnecessary steps during xray TUN setup phase

* chore: move tun init from tun2socks code to ipcserver

* chore: rework xray routing
* get rid of redundant delays
* check if remote calls are successful

* chore: xray routing fine-tuning

* fix: add service qt deps to deployment build
2026-02-04 12:35:53 +08:00
vkamn 87eccfb4ca fix: fix scrolling on drawers (#2183) 2026-02-04 12:35:17 +08:00
ik a983d0504e fix: add checks for script components to find out where it can fall (#2169) 2026-01-30 14:43:30 +08:00
vkamn d0b8535395 fix: update tag deploy (#2168) 2026-01-30 13:15:50 +08:00
dpamnezia f84480cf56 chore: fix artifacts upload (#1961) 2026-01-30 12:43:21 +08:00
MrMirDan de7a026ec1 fix: change drawer parents interactivity (#2004)
* fix: change drawer parents interactivity

* update: better vars names
2026-01-30 12:42:53 +08:00
MrMirDan a128c7d247 fix: keyboard navigation (#2023)
* fix: self-hosted easy install card

* fix: label double click when enter/return pressed
2026-01-30 12:42:29 +08:00
MrMirDan f316f0e25a feat: news notifications switch (#2126)
* feat: news notifications switch

* update: text changes

* fix: notifications enabled by default
2026-01-30 12:19:50 +08:00
NickVs2015 ea5242e29b fix: fixed cipher selection (#2110) 2026-01-30 12:18:54 +08:00
NickVs2015 b31a62c55f feat: add support open files by atv (#2082) 2026-01-30 12:11:26 +08:00
yyy-amnezia 02e3107a23 feat: implement service kickstart and improve macos post install script (#2131) 2026-01-30 12:05:20 +08:00
lunardunno 1862850108 feat: checking linux kernel version when installing amneziawg-go (#2098)
* Checking Linux kernel version when installing amneziawg-go

print the Linux kernel version to stdOut for subsequent checking by the server controller.

* Add error for old linux kernel

Add error 214 ServerLinuxKernelTooOld

* Add case for old linux kernel

Add case for error 214 ServerLinuxKernelTooOld

* Added kernel check for Awg2

Added Linux kernel version check and introduced corresponding ServerLinuxKernelTooOld error for Awg2.
2026-01-30 12:04:27 +08:00
vkamn f73792844c chore: revoke #2148 (#2160) 2026-01-26 19:39:47 +08:00
Yaroslav Gurov a7199ca6f5 fix: add +x permissions to wireguard-go on linux (#2159) 2026-01-26 19:16:39 +08:00
vkamn 5e757cdd3b chore: bump qt version for linux build (#2157) 2026-01-25 21:35:16 +08:00
vkamn 92af1f3268 chore: runners (#2150)
* chore: change runner for linux and android

* chore: add libsecret to linux build

* chore: bump version
2026-01-23 12:05:31 +08:00
Yaroslav Gurov aad9d6dae2 chore: remove redundant gateway (#2148) 2026-01-22 18:21:15 +08:00
Yaroslav Gurov 423fe3fd4f fix: remove redundant gateway from xrayprotocol (#2147) 2026-01-22 18:03:36 +08:00
vkamn b591dd7445 fix: minor fixes (#2137)
* refactor: removed premv1 migration code

* fix: i1-i5 parsing when scaning server

* chore: bump version
2026-01-19 14:03:54 +08:00
116 changed files with 2469 additions and 1697 deletions
+61 -11
View File
@@ -10,10 +10,10 @@ env:
jobs:
Build-Linux-Ubuntu:
runs-on: 4-core
runs-on: android-runner
env:
QT_VERSION: 6.8.3
QT_VERSION: 6.10.1
QIF_VERSION: 4.7
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
@@ -58,7 +58,7 @@ jobs:
- name: 'Build project'
run: |
sudo apt-get install libxkbcommon-x11-0
sudo apt-get install libxkbcommon-x11-0 libsecret-1-dev
export QT_BIN_DIR=${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/gcc_64/bin
export QIF_BIN_DIR=${{ runner.temp }}/Qt/Tools/QtInstallerFramework/${{ env.QIF_VERSION }}/bin
bash deploy/build_linux.sh
@@ -537,7 +537,7 @@ jobs:
# ------------------------------------------------------
Build-Android:
runs-on: 4-core
runs-on: android-runner
env:
ANDROID_BUILD_PLATFORM: android-36
@@ -660,15 +660,57 @@ jobs:
ANDROID_KEYSTORE_KEY_ALIAS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_ALIAS }}
ANDROID_KEYSTORE_KEY_PASS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_PASS }}
shell: bash
run: ./deploy/build_android.sh --aab --apk all --build-platform ${{ env.ANDROID_BUILD_PLATFORM }}
run: ./deploy/build_android.sh --aab --play --apk all --build-platform ${{ env.ANDROID_BUILD_PLATFORM }}
- name: 'Build OSS AAB (in-app purchase)'
env:
ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }}
QT_HOST_PATH: ${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/gcc_64
ANDROID_KEYSTORE_PATH: ${{ github.workspace }}/android.keystore
ANDROID_KEYSTORE_KEY_ALIAS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_ALIAS }}
ANDROID_KEYSTORE_KEY_PASS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_PASS }}
shell: bash
run: ./deploy/build_android.sh --aab --build-platform ${{ env.ANDROID_BUILD_PLATFORM }}
- name: 'Upload OSS x86_64 apk'
uses: actions/upload-artifact@v4
with:
name: AmneziaVPN-android-x86_64
path: deploy/build/AmneziaVPN-oss-x86_64-release.apk
compression-level: 0
retention-days: 7
- name: 'Upload OSS x86 apk'
uses: actions/upload-artifact@v4
with:
name: AmneziaVPN-android-x86
path: deploy/build/AmneziaVPN-oss-x86-release.apk
compression-level: 0
retention-days: 7
- name: 'Upload OSS arm64-v8a apk'
uses: actions/upload-artifact@v4
with:
name: AmneziaVPN-android-arm64-v8a
path: deploy/build/AmneziaVPN-oss-arm64-v8a-release.apk
compression-level: 0
retention-days: 7
- name: 'Upload OSS armeabi-v7a apk'
uses: actions/upload-artifact@v4
with:
name: AmneziaVPN-android-armeabi-v7a
path: deploy/build/AmneziaVPN-oss-armeabi-v7a-release.apk
compression-level: 0
retention-days: 7
- name: 'Rename Android APKs'
run: |
cd deploy/build
mv AmneziaVPN-x86_64-release.apk AmneziaVPN_${VERSION}_android9+_x86_64.apk
mv AmneziaVPN-x86-release.apk AmneziaVPN_${VERSION}_android9+_x86.apk
mv AmneziaVPN-arm64-v8a-release.apk AmneziaVPN_${VERSION}_android9+_arm64-v8a.apk
mv AmneziaVPN-armeabi-v7a-release.apk AmneziaVPN_${VERSION}_android9+_armeabi-v7a.apk
mv AmneziaVPN-oss-x86_64-release.apk AmneziaVPN_${VERSION}_android9+_x86_64.apk
mv AmneziaVPN-oss-x86-release.apk AmneziaVPN_${VERSION}_android9+_x86.apk
mv AmneziaVPN-oss-arm64-v8a-release.apk AmneziaVPN_${VERSION}_android9+_arm64-v8a.apk
mv AmneziaVPN-oss-armeabi-v7a-release.apk AmneziaVPN_${VERSION}_android9+_armeabi-v7a.apk
cd ../..
- name: 'Upload x86_64 apk'
@@ -703,11 +745,19 @@ jobs:
compression-level: 0
retention-days: 7
- name: 'Upload aab'
- name: 'Upload Play AAB'
uses: actions/upload-artifact@v4
with:
name: AmneziaVPN-android
path: deploy/build/AmneziaVPN-release.aab
path: deploy/build/AmneziaVPN-play-release.aab
compression-level: 0
retention-days: 7
- name: 'Upload OSS AAB (in-app purchase)'
uses: actions/upload-artifact@v4
with:
name: AmneziaVPN-android-oss-aab
path: deploy/build/AmneziaVPN-oss-release.aab
compression-level: 0
retention-days: 7
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
- name: Verify git tag
run: |
TAG_NAME=${{ inputs.RELEASE_VERSION }}
CMAKE_TAG=$(grep 'project.*VERSION' CMakeLists.txt | sed -E 's/.* ([0-9]+.[0-9]+.[0-9]+.[0-9]+)$/\1/')
CMAKE_TAG=$(grep 'set(AMNEZIAVPN_VERSION' CMakeLists.txt | sed -E 's/.*AMNEZIAVPN_VERSION ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+).*/\1/')
if [[ "$TAG_NAME" == "$CMAKE_TAG" ]]; then
echo "Git tag ($TAG_NAME) matches CMakeLists.txt version ($CMAKE_TAG)."
else
+3
View File
@@ -140,3 +140,6 @@ ios-ne-build.sh
macos-ne-build.sh
macos-signed-build.sh
macos-with-sign-build.sh
DeveloperIdApplicationCertificate.p12
DeveloperIdInstallerCertificate.p12
+4
View File
@@ -14,3 +14,7 @@
[submodule "client/3rd/QSimpleCrypto"]
path = client/3rd/QSimpleCrypto
url = https://github.com/amnezia-vpn/QSimpleCrypto.git
[submodule "client/3rd/qtgamepad"]
path = client/3rd/qtgamepad
url = https://github.com/amnezia-vpn/qtgamepad.git
branch = 6.6
+5 -2
View File
@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
set(PROJECT AmneziaVPN)
set(AMNEZIAVPN_VERSION 4.8.12.7)
set(AMNEZIAVPN_VERSION 4.8.13.1)
project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
DESCRIPTION "AmneziaVPN"
@@ -12,7 +12,9 @@ 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 2103)
set(APP_ANDROID_VERSION_CODE 2107)
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(MZ_PLATFORM_NAME "linux")
@@ -61,6 +63,7 @@ if(WIN32 AND NOT IOS AND NOT ANDROID AND NOT MACOS_NE)
set(CPACK_PACKAGE_VENDOR "AmneziaVPN")
set(CPACK_PACKAGE_VERSION ${AMNEZIAVPN_VERSION})
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "AmneziaVPN client")
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE")
set(CPACK_PACKAGE_INSTALL_DIRECTORY "AmneziaVPN")
set(CPACK_PACKAGE_DIRECTORY "${CMAKE_BINARY_DIR}")
set(CPACK_PACKAGE_EXECUTABLES "AmneziaVPN" "AmneziaVPN")
Vendored Submodule
+1
Submodule client/3rd/qtgamepad added at f72b3e0c62
+10 -2
View File
@@ -59,7 +59,6 @@ target_include_directories(${PROJECT} PUBLIC
if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID))
qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_interface.rep)
qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_process_interface.rep)
qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_process_tun2socks.rep)
endif()
qt6_add_resources(QRC ${QRC} ${CMAKE_CURRENT_LIST_DIR}/resources.qrc)
@@ -228,4 +227,13 @@ if(NOT IOS AND NOT ANDROID AND NOT MACOS_NE)
endif()
target_sources(${PROJECT} PRIVATE ${SOURCES} ${HEADERS} ${RESOURCES} ${QRC} ${I18NQRC})
qt_finalize_target(${PROJECT})
# Finalize the executable so Qt can gather/deploy QML modules and plugins correctly (Android needs this).
if(COMMAND qt_import_qml_plugins)
qt_import_qml_plugins(${PROJECT})
endif()
if(COMMAND qt_finalize_executable)
qt_finalize_executable(${PROJECT})
else()
qt_finalize_target(${PROJECT})
endif()
+19
View File
@@ -0,0 +1,19 @@
plugins {
id(libs.plugins.android.library.get().pluginId)
id(libs.plugins.kotlin.android.get().pluginId)
}
kotlin {
jvmToolchain(17)
}
android {
namespace = "org.amnezia.vpn.billing"
}
dependencies {
compileOnly(project(":utils"))
implementation(libs.androidx.core)
implementation(libs.kotlinx.coroutines)
implementation(libs.android.billing)
}
@@ -0,0 +1,65 @@
import com.android.billingclient.api.BillingClient.BillingResponseCode.BILLING_UNAVAILABLE
import com.android.billingclient.api.BillingClient.BillingResponseCode.DEVELOPER_ERROR
import com.android.billingclient.api.BillingClient.BillingResponseCode.ERROR
import com.android.billingclient.api.BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED
import com.android.billingclient.api.BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED
import com.android.billingclient.api.BillingClient.BillingResponseCode.ITEM_NOT_OWNED
import com.android.billingclient.api.BillingClient.BillingResponseCode.ITEM_UNAVAILABLE
import com.android.billingclient.api.BillingClient.BillingResponseCode.NETWORK_ERROR
import com.android.billingclient.api.BillingClient.BillingResponseCode.SERVICE_DISCONNECTED
import com.android.billingclient.api.BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE
import com.android.billingclient.api.BillingClient.BillingResponseCode.USER_CANCELED
import com.android.billingclient.api.BillingResult
import org.amnezia.vpn.util.ErrorCode
internal class BillingException(
billingResult: BillingResult,
retryable: Boolean = false
) : Exception(billingResult.toString()) {
constructor(msg: String) : this(BillingResult.newBuilder()
.setResponseCode(DEVELOPER_ERROR)
.setDebugMessage(msg)
.build())
val errorCode: Int
val isCanceled = billingResult.responseCode == USER_CANCELED
val isRetryable = retryable || billingResult.responseCode in setOf(
NETWORK_ERROR,
SERVICE_DISCONNECTED,
SERVICE_UNAVAILABLE,
ERROR
)
init {
when (billingResult.responseCode) {
ERROR -> {
errorCode = ErrorCode.BillingGooglePlayError
}
BILLING_UNAVAILABLE, SERVICE_DISCONNECTED, SERVICE_UNAVAILABLE -> {
errorCode = ErrorCode.BillingUnavailable
}
DEVELOPER_ERROR, FEATURE_NOT_SUPPORTED, ITEM_NOT_OWNED -> {
errorCode = ErrorCode.BillingError
}
ITEM_ALREADY_OWNED -> {
errorCode = ErrorCode.SubscriptionAlreadyOwned
}
ITEM_UNAVAILABLE -> {
errorCode = ErrorCode.SubscriptionUnavailable
}
NETWORK_ERROR -> {
errorCode = ErrorCode.BillingNetworkError
}
else -> {
errorCode = ErrorCode.BillingError
}
}
}
}
@@ -0,0 +1,320 @@
import android.app.Activity
import android.content.Context
import com.android.billingclient.api.AcknowledgePurchaseParams
import com.android.billingclient.api.BillingClient
import com.android.billingclient.api.BillingClient.BillingResponseCode
import com.android.billingclient.api.BillingClient.ProductType
import com.android.billingclient.api.BillingClientStateListener
import com.android.billingclient.api.BillingFlowParams
import com.android.billingclient.api.BillingFlowParams.SubscriptionUpdateParams.ReplacementMode
import com.android.billingclient.api.BillingResult
import com.android.billingclient.api.GetBillingConfigParams
import com.android.billingclient.api.PendingPurchasesParams
import com.android.billingclient.api.ProductDetails
import com.android.billingclient.api.Purchase
import com.android.billingclient.api.PurchasesUpdatedListener
import com.android.billingclient.api.QueryProductDetailsParams
import com.android.billingclient.api.QueryProductDetailsParams.Product
import com.android.billingclient.api.QueryPurchasesParams
import com.android.billingclient.api.acknowledgePurchase
import com.android.billingclient.api.queryProductDetails
import com.android.billingclient.api.queryPurchasesAsync
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.withContext
import org.amnezia.vpn.util.ErrorCode
import org.amnezia.vpn.util.Log
import org.json.JSONArray
import org.json.JSONObject
private const val TAG = "BillingProvider"
private const val PRODUCT_ID = "premium"
class BillingProvider(context: Context) : AutoCloseable {
private var billingClient: BillingClient
private var subscriptionPurchases = MutableStateFlow<Pair<BillingResult, List<Purchase>?>?>(null)
private val purchasesUpdatedListeners = PurchasesUpdatedListener { billingResult, purchases ->
Log.v(TAG, "Purchases updated: $billingResult")
subscriptionPurchases.value = billingResult to purchases
}
init {
billingClient = BillingClient.newBuilder(context)
.setListener(purchasesUpdatedListeners)
.enablePendingPurchases(PendingPurchasesParams.newBuilder().enableOneTimeProducts().build())
.build()
}
private suspend fun connect() {
if (billingClient.isReady) return
Log.v(TAG, "Billing client connection")
val connection = CompletableDeferred<Unit>()
withContext(Dispatchers.IO) {
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
Log.v(TAG, "Billing setup finished: $billingResult")
if (billingResult.isOk) {
connection.complete(Unit)
} else {
Log.e(TAG, "Billing setup failed: $billingResult")
connection.completeExceptionally(BillingException(billingResult))
}
}
override fun onBillingServiceDisconnected() {
Log.w(TAG, "Billing service disconnected")
}
})
}
connection.await()
}
private suspend fun handleBillingApiCall(block: suspend () -> JSONObject): JSONObject {
val numberAttempts = 3
var attemptCount = 0
while (true) {
try {
return block()
} catch (e: BillingException) {
if (e.isCanceled) {
Log.w(TAG, "Billing canceled")
return JSONObject().put("responseCode", ErrorCode.BillingCanceled)
} else if (e.isRetryable && attemptCount < numberAttempts) {
Log.d(TAG, "Retryable error: $e")
++attemptCount
delay(1000)
} else {
Log.e(TAG, "Billing error: $e")
return JSONObject().put("responseCode", e.errorCode)
}
} catch (_: CancellationException) {
Log.w(TAG, "Billing coroutine canceled")
return JSONObject().put("responseCode", ErrorCode.BillingCanceled)
}
}
}
suspend fun getSubscriptionPlans(): JSONObject {
Log.v(TAG, "Get subscription plans")
val productDetailsList = getProductDetails()
val resultJson = JSONObject().put("responseCode", ErrorCode.NoError)
val productArray = JSONArray().also { resultJson.put("products", it) }
productDetailsList?.forEach { productDetails ->
val product = JSONObject().also { productArray.put(it) }
.put("productId", productDetails.productId)
.put("name", productDetails.name)
val offers = JSONArray().also { product.put("offers", it) }
productDetails.subscriptionOfferDetails?.forEach { offerDetails ->
val offer = JSONObject().also { offers.put(it) }
.put("basePlanId", offerDetails.basePlanId)
.put("offerId", offerDetails.offerId)
.put("offerToken", offerDetails.offerToken)
val pricingPhases = JSONArray().also { offer.put("pricingPhases", it) }
offerDetails.pricingPhases.pricingPhaseList.forEach { phase ->
JSONObject().also { pricingPhases.put(it) }
.put("billingCycleCount", phase.billingCycleCount)
.put("billingPeriod", phase.billingPeriod)
.put("formatedPrice", phase.formattedPrice)
.put("recurrenceMode", phase.recurrenceMode)
}
}
}
return resultJson
}
private suspend fun getProductDetails(): List<ProductDetails>? {
Log.v(TAG, "Get product details")
val productDetailsParams = Product.newBuilder()
.setProductId(PRODUCT_ID)
.setProductType(ProductType.SUBS)
.build()
val queryProductDetailsParams = QueryProductDetailsParams.newBuilder()
.setProductList(listOf(productDetailsParams))
.build()
val result = withContext(Dispatchers.IO) {
billingClient.queryProductDetails(queryProductDetailsParams)
}
Log.v(TAG, "Query product details result: ${result.billingResult}")
if (!result.billingResult.isOk) {
Log.e(TAG, "Failed to get product details: ${result.billingResult}")
throw BillingException(result.billingResult)
}
return result.productDetailsList
}
suspend fun getCustomerCountryCode(): JSONObject {
Log.v(TAG, "Get customer country code")
val deferred = CompletableDeferred<String>()
withContext(Dispatchers.IO) {
billingClient.getBillingConfigAsync(GetBillingConfigParams.newBuilder().build(),
{ billingResult, billingConfig ->
Log.v(TAG, "Billing config: $billingResult, ${billingConfig?.countryCode}")
if (billingResult.isOk) {
deferred.complete(billingConfig?.countryCode ?: "")
} else {
deferred.completeExceptionally(BillingException(billingResult))
}
})
}
val countryCode = deferred.await()
return JSONObject()
.put("responseCode", ErrorCode.NoError)
.put("countryCode", countryCode)
}
suspend fun purchaseSubscription(
activity: Activity,
offerToken: String,
oldPurchaseToken: String? = null
): JSONObject {
Log.v(TAG, "Purchase subscription")
Log.v(TAG, "Offer token: $offerToken")
oldPurchaseToken?.let { Log.v(TAG, "Old purchase token: $it") }
if (offerToken.isBlank()) throw BillingException("offerToken can not be empty")
val productDetails = getProductDetails()?.let {
it.filter { it.productId == PRODUCT_ID }
}?.firstOrNull() ?: throw BillingException("Product details not found")
Log.v(TAG, "Filtered product details:\n$productDetails")
val productDetail = BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetails)
.setOfferToken(offerToken)
.build()
val subscriptionUpdateParams = oldPurchaseToken?.let {
BillingFlowParams.SubscriptionUpdateParams.newBuilder()
.setOldPurchaseToken(oldPurchaseToken)
.setSubscriptionReplacementMode(ReplacementMode.WITHOUT_PRORATION)
.build()
}
val billingResult = billingClient.launchBillingFlow(activity, BillingFlowParams.newBuilder()
.setProductDetailsParamsList(listOf(productDetail))
.apply { subscriptionUpdateParams?.let { setSubscriptionUpdateParams(it) } }
.build())
Log.v(TAG, "Start billing flow result: $billingResult")
if (billingResult.responseCode == BillingResponseCode.ITEM_ALREADY_OWNED) {
Log.w(TAG, "Attempting to purchase already owned product")
val purchases = queryPurchases()
if (purchases.any { PRODUCT_ID in it.products }) throw BillingException(billingResult)
else throw BillingException(billingResult, retryable = true)
} else if (billingResult.responseCode == BillingResponseCode.ITEM_NOT_OWNED) {
Log.w(TAG, "Attempting to replace not owned product")
val purchases = queryPurchases()
if (purchases.all { PRODUCT_ID !in it.products }) throw BillingException(billingResult)
else throw BillingException(billingResult, retryable = true)
} else if (!billingResult.isOk) throw BillingException(billingResult)
subscriptionPurchases.firstOrNull { it != null }?.let { (billingResult, purchases) ->
if (!billingResult.isOk) throw BillingException(billingResult)
return JSONObject()
.put("responseCode", ErrorCode.NoError)
.put("purchases", processPurchases(purchases))
} ?: throw BillingException("Purchase failed")
}
private fun processPurchases(purchases: List<Purchase>?): JSONArray {
val purchaseArray = JSONArray()
purchases?.forEach { purchase ->
/* val purchaseJson = */ JSONObject().also { purchaseArray.put(it) }
.put("purchaseToken", purchase.purchaseToken)
.put("purchaseTime", purchase.purchaseTime)
.put("purchaseState", purchase.purchaseState)
.put("isAcknowledged", purchase.isAcknowledged)
.put("isAutoRenewing", purchase.isAutoRenewing)
.put("orderId", purchase.orderId)
// .put("productIds", JSONArray(purchase.products))
/* purchase.pendingPurchaseUpdate?.let { purchaseUpdate ->
JSONObject()
.put("purchaseToken", purchaseUpdate.purchaseToken)
// .put("productIds", JSONArray(purchaseUpdate.products))
}.also { purchaseJson.put("pendingPurchaseUpdate", it) } */
}
return purchaseArray
}
suspend fun acknowledge(purchaseToken: String): JSONObject {
Log.v(TAG, "Acknowledge purchase: $purchaseToken")
val result = withContext(Dispatchers.IO) {
billingClient.acknowledgePurchase(
AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchaseToken)
.build()
)
}
Log.v(TAG, "Acknowledge purchase result: $result")
if (result.responseCode == BillingResponseCode.ITEM_NOT_OWNED) {
Log.w(TAG, "Attempting to acknowledge not owned product")
val purchases = queryPurchases()
if (purchases.all { PRODUCT_ID !in it.products }) throw BillingException(result)
else throw BillingException(result, retryable = true)
} else if (!result.isOk && result.responseCode != BillingResponseCode.ITEM_ALREADY_OWNED) {
throw BillingException(result)
}
return JSONObject().put("responseCode", ErrorCode.NoError)
}
suspend fun getPurchases(): JSONObject {
Log.v(TAG, "Get purchases")
val purchases = queryPurchases()
return JSONObject()
.put("responseCode", ErrorCode.NoError)
.put("purchases", processPurchases(purchases))
}
private suspend fun queryPurchases(): List<Purchase> {
Log.v(TAG, "Query purchases")
val result = withContext(Dispatchers.IO) {
billingClient.queryPurchasesAsync(
QueryPurchasesParams.newBuilder().setProductType(ProductType.SUBS).build()
)
}
Log.v(TAG, "Query purchases result: ${result.billingResult}")
if (!result.billingResult.isOk) throw BillingException(result.billingResult)
return result.purchasesList
}
override fun close() {
Log.v(TAG, "Close billing client connection")
billingClient.endConnection()
}
companion object {
suspend fun withBillingProvider(context: Context, block: suspend BillingProvider.() -> JSONObject): String =
BillingProvider(context).use { bp ->
bp.handleBillingApiCall {
bp.connect()
bp.block()
}.toString()
}
}
}
internal val BillingResult.isOk: Boolean
get() = responseCode == BillingResponseCode.OK
+36 -11
View File
@@ -20,6 +20,7 @@ android {
namespace = "org.amnezia.vpn"
buildFeatures {
buildConfig = true
viewBinding = true
}
@@ -41,17 +42,6 @@ android {
resourceConfigurations += listOf("en", "ru", "b+zh+Hans")
}
sourceSets {
getByName("main") {
manifest.srcFile("AndroidManifest.xml")
java.setSrcDirs(listOf("src"))
res.setSrcDirs(listOf("res"))
// androyddeployqt creates the folders below
assets.setSrcDirs(listOf("assets"))
jniLibs.setSrcDirs(listOf("libs"))
}
}
signingConfigs {
register("release") {
storeFile = providers.environmentVariable("ANDROID_KEYSTORE_PATH").orNull?.let { file(it) }
@@ -77,6 +67,36 @@ android {
}
}
flavorDimensions += "billing"
productFlavors {
create("oss") {
dimension = "billing"
}
create("play") {
dimension = "billing"
}
}
sourceSets {
getByName("main") {
manifest.srcFile("AndroidManifest.xml")
java.setSrcDirs(listOf("src"))
res.setSrcDirs(listOf("res"))
// androyddeployqt creates the folders below
assets.setSrcDirs(listOf("assets"))
jniLibs.setSrcDirs(listOf("libs"))
}
getByName("oss") {
java.setSrcDirs(listOf("oss"))
}
getByName("play") {
java.setSrcDirs(listOf("play"))
}
}
splits {
abi {
isEnable = true
@@ -122,4 +142,9 @@ dependencies {
implementation(libs.google.mlkit)
implementation(libs.androidx.datastore)
implementation(libs.androidx.biometric)
playImplementation(project(":billing"))
}
fun DependencyHandler.playImplementation(dependency: Any): Dependency? =
add("playImplementation", dependency)
+2
View File
@@ -1,6 +1,7 @@
[versions]
agp = "8.5.2"
kotlin = "1.9.24"
android-billing = "7.0.0"
androidx-core = "1.13.1"
androidx-activity = "1.9.1"
androidx-annotation = "1.8.2"
@@ -14,6 +15,7 @@ kotlinx-serialization = "1.6.3"
google-mlkit = "17.3.0"
[libraries]
android-billing = { module = "com.android.billingclient:billing-ktx", version.ref = "android-billing" }
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
androidx-activity = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity" }
androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "androidx-annotation" }
@@ -0,0 +1,13 @@
package org.amnezia.vpn
import android.app.Activity
import android.content.Context
class BillingPaymentRepository(@Suppress("UNUSED_PARAMETER") context: Context) : BillingRepository {
override suspend fun getCountryCode(): String = ""
override suspend fun getSubscriptionPlans(): String = ""
override suspend fun purchaseSubscription(activity: Activity, offerToken: String): String = ""
override suspend fun upgradeSubscription(activity: Activity, offerToken: String, oldPurchaseToken: String): String = ""
override suspend fun acknowledge(purchaseToken: String): String = ""
override suspend fun queryPurchases(): String = ""
}
@@ -0,0 +1,34 @@
package org.amnezia.vpn
import android.app.Activity
import android.content.Context
import BillingProvider.Companion.withBillingProvider
class BillingPaymentRepository(private val context: Context) : BillingRepository {
override suspend fun getCountryCode(): String = withBillingProvider(context) {
getCustomerCountryCode()
}
override suspend fun getSubscriptionPlans(): String = withBillingProvider(context) {
getSubscriptionPlans()
}
override suspend fun purchaseSubscription(activity: Activity, offerToken: String): String =
withBillingProvider(context) {
purchaseSubscription(activity, offerToken)
}
override suspend fun upgradeSubscription(activity: Activity, offerToken: String, oldPurchaseToken: String): String =
withBillingProvider(context) {
purchaseSubscription(activity, offerToken, oldPurchaseToken)
}
override suspend fun acknowledge(purchaseToken: String): String = withBillingProvider(context) {
acknowledge(purchaseToken)
}
override suspend fun queryPurchases(): String = withBillingProvider(context) {
getPurchases()
}
}
+1
View File
@@ -31,6 +31,7 @@ rootProject.buildFileName = "build.gradle.kts"
include(":qt")
include(":utils")
include(":billing")
include(":protocolApi")
include(":wireguard")
include(":awg")
@@ -26,6 +26,8 @@ import android.os.ParcelFileDescriptor
import android.os.SystemClock
import android.provider.OpenableColumns
import android.provider.Settings
import android.view.InputDevice
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
@@ -53,7 +55,6 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.amnezia.vpn.protocol.getStatistics
import org.amnezia.vpn.protocol.getStatus
import org.amnezia.vpn.qt.QtAndroidController
@@ -85,9 +86,14 @@ class AmneziaActivity : QtActivity() {
private var notificationStateReceiver: BroadcastReceiver? = null
private lateinit var vpnServiceMessenger: IpcMessenger
private var pfd: ParcelFileDescriptor? = null
private lateinit var billingRepository: BillingRepository
private val actionResultHandlers = mutableMapOf<Int, ActivityResultHandler>()
private val permissionRequestHandlers = mutableMapOf<Int, PermissionRequestHandler>()
private var isActivityResumed = false
private var hasWindowFocus = false
private val resumeHandler = Handler(Looper.getMainLooper())
private val vpnServiceEventHandler: Handler by lazy(NONE) {
object : Handler(Looper.getMainLooper()) {
@@ -193,6 +199,7 @@ class AmneziaActivity : QtActivity() {
registerBroadcastReceivers()
intent?.let(::processIntent)
runBlocking { vpnProto = proto.await() }
billingRepository = BillingPaymentRepository(applicationContext)
}
private fun loadLibs() {
@@ -260,6 +267,10 @@ class AmneziaActivity : QtActivity() {
}
override fun onStop() {
isActivityResumed = false
hasWindowFocus = false
// Cancel all pending operations when activity stops
resumeHandler.removeCallbacksAndMessages(null)
Log.d(TAG, "Stop Amnezia activity")
doUnbindService()
mainScope.launch {
@@ -271,35 +282,91 @@ class AmneziaActivity : QtActivity() {
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
hasWindowFocus = hasFocus
Log.d(TAG, "Window focus changed: hasFocus=$hasFocus")
// Cancel pending operations if window loses focus
if (!hasFocus) {
resumeHandler.removeCallbacksAndMessages(null)
}
}
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
val deviceId = event.deviceId
val keyCode = event.keyCode
val pressed = event.action == KeyEvent.ACTION_DOWN
val source = event.source
if (deviceId < 0 && pressed) {
when (keyCode) {
KeyEvent.KEYCODE_BUTTON_A,
KeyEvent.KEYCODE_BUTTON_B,
KeyEvent.KEYCODE_BUTTON_X,
KeyEvent.KEYCODE_BUTTON_Y,
KeyEvent.KEYCODE_BUTTON_START,
KeyEvent.KEYCODE_BUTTON_SELECT,
KeyEvent.KEYCODE_DPAD_CENTER -> {
nativeGamepadKeyEvent(0, keyCode, true)
nativeGamepadKeyEvent(0, keyCode, false)
return true
}
}
}
// Real gamepad events (deviceId >= 0)
if (deviceId >= 0) {
val isGamepad = (source and InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD
val isJoystick = (source and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK
val isDpad = (source and InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD
if (isGamepad || isJoystick || isDpad) {
nativeGamepadKeyEvent(deviceId, keyCode, pressed)
return true
}
}
return super.dispatchKeyEvent(event)
}
private external fun nativeGamepadKeyEvent(deviceId: Int, keyCode: Int, pressed: Boolean)
override fun onPause() {
super.onPause()
isActivityResumed = false
// Cancel all pending operations when activity pauses
resumeHandler.removeCallbacksAndMessages(null)
Log.d(TAG, "Pause Amnezia activity")
}
override fun onResume() {
super.onResume()
/* if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
isActivityResumed = true
Log.d(TAG, "Resume Amnezia activity")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
window.decorView.apply {
invalidate()
postDelayed({
sendTouch(1f, 1f)
resumeHandler.postDelayed({
// Check if activity is still resumed and has focus before executing
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
sendTouch(1f, 1f)
}
}, 100)
postDelayed({
sendTouch(2f, 2f)
resumeHandler.postDelayed({
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
sendTouch(2f, 2f)
}
}, 200)
postDelayed({
requestLayout()
invalidate()
resumeHandler.postDelayed({
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
requestLayout()
invalidate()
}
}, 250)
}
} */
Log.d(TAG, "Resume Amnezia activity")
}
}
private fun configureWindowForEdgeToEdge() {
@@ -362,6 +429,10 @@ class AmneziaActivity : QtActivity() {
}
override fun onDestroy() {
isActivityResumed = false
hasWindowFocus = false
// Cancel all pending operations when activity is destroyed
resumeHandler.removeCallbacksAndMessages(null)
Log.d(TAG, "Destroy Amnezia activity")
unregisterBroadcastReceiver(notificationStateReceiver)
notificationStateReceiver = null
@@ -862,15 +933,9 @@ class AmneziaActivity : QtActivity() {
@Suppress("unused")
fun getAppList(): String {
Log.v(TAG, "Get app list")
var appList = ""
runBlocking {
mainScope.launch {
withContext(Dispatchers.IO) {
appList = AppListProvider.getAppList(packageManager, packageName)
}
}.join()
return blockingCall(Dispatchers.IO) {
AppListProvider.getAppList(packageManager, packageName)
}
return appList
}
@Suppress("unused")
@@ -1041,11 +1106,59 @@ class AmneziaActivity : QtActivity() {
return super.dispatchTrackballEvent(ev)
}
@Suppress("unused")
fun isPlay(): Boolean = BuildConfig.FLAVOR == "play"
@Suppress("unused")
fun isTestPurchaseEnvironment(): Boolean {
if (BuildConfig.DEBUG) return true
val appInfo = packageManager.getApplicationInfo(packageName, 0)
return (appInfo.flags and android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE) != 0
}
@Suppress("unused")
fun getCountryCode(): String {
Log.v(TAG, "Get country code")
return blockingCall { billingRepository.getCountryCode() }
}
@Suppress("unused")
fun getSubscriptionPlans(): String {
Log.v(TAG, "Get subscription plans")
return blockingCall { billingRepository.getSubscriptionPlans() }
}
@Suppress("unused")
fun purchaseSubscription(offerToken: String): String {
Log.v(TAG, "Purchase subscription")
return blockingCall { billingRepository.purchaseSubscription(this@AmneziaActivity, offerToken) }
}
@Suppress("unused")
fun upgradeSubscription(offerToken: String, oldPurchaseToken: String): String {
Log.v(TAG, "Upgrade subscription")
return blockingCall {
billingRepository.upgradeSubscription(this@AmneziaActivity, offerToken, oldPurchaseToken)
}
}
@Suppress("unused")
fun acknowledgePurchase(purchaseToken: String): String {
Log.v(TAG, "Acknowledge purchase")
return blockingCall { billingRepository.acknowledge(purchaseToken) }
}
@Suppress("unused")
fun queryPurchases(): String {
Log.v(TAG, "Query purchases")
return blockingCall { billingRepository.queryPurchases() }
}
/**
* Utils methods
*/
private fun <T> blockingCall(
context: CoroutineContext = Dispatchers.Main.immediate,
context: CoroutineContext = Dispatchers.Default,
block: suspend () -> T
) = runBlocking {
mainScope.async(context) { block() }.await()
@@ -1,5 +1,6 @@
package org.amnezia.vpn
import android.system.Os
import androidx.camera.camera2.Camera2Config
import androidx.camera.core.CameraSelector
import androidx.camera.core.CameraXConfig
@@ -12,6 +13,9 @@ private const val TAG = "AmneziaApplication"
class AmneziaApplication : QtApplication(), CameraXConfig.Provider {
override fun onCreate() {
if (BuildConfig.DEBUG) {
Os.setenv("QT_ANDROID_DEBUGGER_MAIN_THREAD_SLEEP_MS", "0", true)
}
super.onCreate()
Prefs.init(this)
Log.init(this)
@@ -0,0 +1,12 @@
package org.amnezia.vpn
import android.app.Activity
interface BillingRepository {
suspend fun getCountryCode(): String
suspend fun getSubscriptionPlans(): String
suspend fun purchaseSubscription(activity: Activity, offerToken: String): String
suspend fun upgradeSubscription(activity: Activity, offerToken: String, oldPurchaseToken: String): String
suspend fun acknowledge(purchaseToken: String): String
suspend fun queryPurchases(): String
}
@@ -1,7 +1,10 @@
package org.amnezia.vpn
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
@@ -11,7 +14,25 @@ private const val TAG = "TvFilePicker"
class TvFilePicker : ComponentActivity() {
private val fileChooseResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) {
private val fileChooseResultLauncher = registerForActivityResult(object : ActivityResultContracts.OpenDocument() {
override fun createIntent(context: Context, input: Array<String>): Intent {
val intent = super.createIntent(context, input)
val activitiesToResolveIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.packageManager.queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()))
} else {
@Suppress("DEPRECATION")
context.packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
}
if (activitiesToResolveIntent.all {
val name = it.activityInfo.packageName
name.startsWith("com.google.android.tv.frameworkpackagestubs") || name.startsWith("com.android.tv.frameworkpackagestubs")
}) {
throw ActivityNotFoundException()
}
return intent
}
}) {
setResult(RESULT_OK, Intent().apply { data = it })
finish()
}
@@ -31,7 +52,7 @@ class TvFilePicker : ComponentActivity() {
private fun getFile() {
try {
Log.v(TAG, "getFile")
fileChooseResultLauncher.launch("*/*")
fileChooseResultLauncher.launch(arrayOf("*/*"))
} catch (_: ActivityNotFoundException) {
Log.w(TAG, "Activity not found")
setResult(RESULT_CANCELED, Intent().apply { putExtra("activityNotFound", true) })
@@ -0,0 +1,14 @@
package org.amnezia.vpn.util
// keep synchronized with client/core/defs.h error_code_ns::ErrorCode
object ErrorCode {
const val NoError = 0
const val BillingCanceled = 1300
const val BillingError = 1301
const val BillingGooglePlayError = 1302
const val BillingUnavailable = 1303
const val SubscriptionAlreadyOwned = 1304
const val SubscriptionUnavailable = 1305
const val BillingNetworkError = 1306
}
+20
View File
@@ -83,6 +83,26 @@ add_compile_definitions(_WINSOCKAPI_)
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
set(BUILD_WITH_QT6 ON)
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/qtkeychain)
if(ANDROID)
# Use qtgamepad from amnezia-vpn/qtgamepad repository
# Only if Qt6CorePrivate is available (required by qtgamepad)
find_package(Qt6CorePrivate CONFIG QUIET)
if(Qt6CorePrivate_FOUND)
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/qtgamepad)
# Link both the C++ module and QML plugin
if(TARGET GamepadLegacy)
target_link_libraries(${PROJECT} PRIVATE GamepadLegacy)
endif()
if(TARGET GamepadLegacyQuickPrivate)
target_link_libraries(${PROJECT} PRIVATE GamepadLegacyQuickPrivate)
endif()
message(STATUS "Gamepad support enabled for Android")
else()
message(STATUS "Qt6CorePrivate not found. Gamepad support disabled for Android.")
endif()
endif()
set(LIBS ${LIBS} qt6keychain)
include_directories(
+23
View File
@@ -1,5 +1,9 @@
message("Client android ${CMAKE_ANDROID_ARCH_ABI} build")
# Option to build Play variant (with Google Play Billing) instead of OSS
# When ON, adds target android_play_apk: cmake --build . --target android_play_apk
option(ANDROID_BUILD_PLAY "Add android_play_apk target for Google Play Billing build" OFF)
set(APP_ANDROID_MIN_SDK 28)
set(ANDROID_PLATFORM "android-${APP_ANDROID_MIN_SDK}" CACHE STRING
"The minimum API level supported by the application or library" FORCE)
@@ -57,3 +61,22 @@ endforeach()
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/xray/android/libxray.aar
DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/android/xray/libXray)
# Custom target to build Play variant (with Google Play Billing)
# Enable with: cmake -DANDROID_BUILD_PLAY=ON ...
# Then run: cmake --build <build_dir> --target android_play_apk
# Note: Do a normal build first so androiddeployqt creates the android-build folder
if(ANDROID_BUILD_PLAY)
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
set(_gradle_suffix "Debug")
else()
set(_gradle_suffix "Release")
endif()
set(_android_build_dir "${CMAKE_CURRENT_BINARY_DIR}/android-build-${PROJECT}")
add_custom_target(android_play_apk
COMMAND ./gradlew assemblePlay${_gradle_suffix} -DexplicitRun=1
WORKING_DIRECTORY "${_android_build_dir}"
COMMENT "Building Android Play variant (assemblePlay${_gradle_suffix})"
DEPENDS ${PROJECT}
)
endif()
-2
View File
@@ -181,7 +181,6 @@ if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID))
set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/core/ipcclient.h
${CLIENT_ROOT_DIR}/core/privileged_process.h
${CLIENT_ROOT_DIR}/ui/systemtray_notificationhandler.h
${CLIENT_ROOT_DIR}/protocols/openvpnprotocol.h
${CLIENT_ROOT_DIR}/protocols/openvpnovercloakprotocol.h
@@ -194,7 +193,6 @@ if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID))
set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/core/ipcclient.cpp
${CLIENT_ROOT_DIR}/core/privileged_process.cpp
${CLIENT_ROOT_DIR}/mozilla/localsocketcontroller.cpp
${CLIENT_ROOT_DIR}/ui/systemtray_notificationhandler.cpp
${CLIENT_ROOT_DIR}/protocols/openvpnprotocol.cpp
@@ -154,9 +154,6 @@ void CoreController::initControllers()
m_apiConfigsController.reset(new ApiConfigsController(m_serversModel, m_apiServicesModel, m_settings));
m_engine->rootContext()->setContextProperty("ApiConfigsController", m_apiConfigsController.get());
m_apiPremV1MigrationController.reset(new ApiPremV1MigrationController(m_serversModel, m_settings, this));
m_engine->rootContext()->setContextProperty("ApiPremV1MigrationController", m_apiPremV1MigrationController.get());
m_apiNewsController.reset(new ApiNewsController(m_newsModel, m_settings, m_serversModel, this));
m_engine->rootContext()->setContextProperty("ApiNewsController", m_apiNewsController.get());
}
@@ -231,8 +228,6 @@ void CoreController::initSignalHandlers()
initAutoConnectHandler();
initAmneziaDnsToggledHandler();
initPrepareConfigHandler();
initImportPremiumV2VpnKeyHandler();
initShowMigrationDrawerHandler();
initStrictKillSwitchHandler();
}
@@ -382,25 +377,6 @@ void CoreController::initPrepareConfigHandler()
});
}
void CoreController::initImportPremiumV2VpnKeyHandler()
{
connect(m_apiPremV1MigrationController.get(), &ApiPremV1MigrationController::importPremiumV2VpnKey, this, [this](const QString &vpnKey) {
m_importController->extractConfigFromData(vpnKey);
m_importController->importConfig();
emit m_apiPremV1MigrationController->migrationFinished();
});
}
void CoreController::initShowMigrationDrawerHandler()
{
QTimer::singleShot(1000, this, [this]() {
if (m_apiPremV1MigrationController->isPremV1MigrationReminderActive() && m_apiPremV1MigrationController->hasConfigsToMigration()) {
m_apiPremV1MigrationController->showMigrationDrawer();
}
});
}
void CoreController::initStrictKillSwitchHandler()
{
connect(m_settingsController.get(), &SettingsController::strictKillSwitchEnabledChanged, m_vpnConnection.get(),
-4
View File
@@ -11,7 +11,6 @@
#include "ui/controllers/api/apiConfigsController.h"
#include "ui/controllers/api/apiSettingsController.h"
#include "ui/controllers/api/apiPremV1MigrationController.h"
#include "ui/controllers/api/apiNewsController.h"
#include "ui/controllers/appSplitTunnelingController.h"
#include "ui/controllers/allowedDnsController.h"
@@ -93,8 +92,6 @@ private:
void initAutoConnectHandler();
void initAmneziaDnsToggledHandler();
void initPrepareConfigHandler();
void initImportPremiumV2VpnKeyHandler();
void initShowMigrationDrawerHandler();
void initStrictKillSwitchHandler();
QQmlApplicationEngine *m_engine {}; // TODO use parent child system here?
@@ -122,7 +119,6 @@ private:
QScopedPointer<ApiSettingsController> m_apiSettingsController;
QScopedPointer<ApiConfigsController> m_apiConfigsController;
QScopedPointer<ApiPremV1MigrationController> m_apiPremV1MigrationController;
QScopedPointer<ApiNewsController> m_apiNewsController;
QSharedPointer<ContainersModel> m_containersModel;
@@ -419,6 +419,18 @@ ErrorCode ServerController::installDockerWorker(const ServerCredentials &credent
cbReadStdOut, cbReadStdErr);
qDebug().noquote() << "ServerController::installDockerWorker" << stdOut;
if (container == DockerContainer::Awg2) {
QRegularExpression regex(R"(Linux\s+(\d+)\.(\d+)[^\d]*)");
QRegularExpressionMatch match = regex.match(stdOut);
if (match.hasMatch()) {
int majorVersion = match.captured(1).toInt();
int minorVersion = match.captured(2).toInt();
if (majorVersion < 4 || (majorVersion == 4 && minorVersion < 14)) {
return ErrorCode::ServerLinuxKernelTooOld;
}
}
}
if (stdOut.contains("lock"))
return ErrorCode::ServerPacketManagerError;
if (stdOut.contains("command not found"))
+12 -1
View File
@@ -61,6 +61,7 @@ namespace amnezia
ServerDockerOnCgroupsV2 = 211,
ServerCgroupMountpoint = 212,
DockerPullRateLimit = 213,
ServerLinuxKernelTooOld = 214,
// Ssh connection errors
SshRequestDeniedError = 300,
@@ -122,6 +123,7 @@ namespace amnezia
ApiUpdateRequestError = 1111,
ApiSubscriptionExpiredError = 1112,
ApiPurchaseError = 1113,
ApiNoPurchasesToRestore = 1114,
// QFile errors
OpenError = 1200,
@@ -129,7 +131,16 @@ namespace amnezia
PermissionsError = 1202,
UnspecifiedError = 1203,
FatalError = 1204,
AbortError = 1205
AbortError = 1205,
// Billing errors
BillingCanceled = 1300,
BillingError = 1301,
BillingGooglePlayError = 1302,
BillingUnavailable = 1303,
SubscriptionAlreadyOwned = 1304,
SubscriptionUnavailable = 1305,
BillingNetworkError = 1306,
};
Q_ENUM_NS(ErrorCode)
}
+19
View File
@@ -29,6 +29,7 @@ QString errorString(ErrorCode code) {
case(ErrorCode::ServerDockerOnCgroupsV2): errorMessage = QObject::tr("Docker error: runc doesn't work on cgroups v2"); break;
case(ErrorCode::ServerCgroupMountpoint): errorMessage = QObject::tr("Server error: cgroup mountpoint does not exist"); break;
case(ErrorCode::DockerPullRateLimit): errorMessage = QObject::tr("Docker error: The pull rate limit has been reached"); break;
case(ErrorCode::ServerLinuxKernelTooOld): errorMessage = QObject::tr("Server error: Linux kernel is too old"); break;
// Libssh errors
case(ErrorCode::SshRequestDeniedError): errorMessage = QObject::tr("SSH request was denied"); break;
@@ -79,6 +80,15 @@ QString errorString(ErrorCode code) {
case (ErrorCode::ApiUpdateRequestError): errorMessage = QObject::tr("Please update the application to use this feature"); break;
case (ErrorCode::ApiSubscriptionExpiredError): errorMessage = QObject::tr("Your Amnezia Premium subscription has expired.\n Please check your email for renewal instructions.\n If you haven't received an email, please contact our support."); break;
case (ErrorCode::ApiPurchaseError): errorMessage = QObject::tr("Unable to process purchase"); break;
case (ErrorCode::ApiNoPurchasesToRestore):
#if defined(Q_OS_ANDROID)
errorMessage = QObject::tr("No purchases to restore. If you have an active subscription, make sure you're signed in with the same Google account used for the purchase.");
#elif defined(Q_OS_IOS) || defined(MACOS_NE)
errorMessage = QObject::tr("No purchases to restore. If you have an active subscription, make sure you're signed in with the same Apple ID used for the purchase.");
#else
errorMessage = QObject::tr("No purchases to restore. If you have an active subscription, make sure you're signed in with the same account used for the purchase.");
#endif
break;
// QFile errors
case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break;
@@ -88,6 +98,15 @@ QString errorString(ErrorCode code) {
case(ErrorCode::FatalError): errorMessage = QObject::tr("QFile error: A fatal error occurred"); break;
case(ErrorCode::AbortError): errorMessage = QObject::tr("QFile error: The operation was aborted"); break;
// Billing errors
case(ErrorCode::BillingCanceled): errorMessage = QObject::tr("Transaction was canceled by the user"); break;
case(ErrorCode::BillingError): errorMessage = QObject::tr("Billing error"); break;
case(ErrorCode::BillingGooglePlayError): errorMessage = QObject::tr("Internal Google Play error, please try again later"); break;
case(ErrorCode::BillingUnavailable): errorMessage = QObject::tr("Billing is unavailable, please try again later"); break;
case(ErrorCode::SubscriptionAlreadyOwned): errorMessage = QObject::tr("You already own this subscription"); break;
case(ErrorCode::SubscriptionUnavailable): errorMessage = QObject::tr("The requested subscription is not available for purchase"); break;
case(ErrorCode::BillingNetworkError): errorMessage = QObject::tr("A network error occurred during the operation, please check the Internet connection"); break;
case(ErrorCode::InternalError):
default:
errorMessage = QObject::tr("Internal error"); break;
+35 -61
View File
@@ -7,7 +7,6 @@ IpcClient::IpcClient(QObject *parent) : QObject(parent)
{
m_node.connectToNode(QUrl("local:" + amnezia::getIpcServiceUrl()));
m_interface.reset(m_node.acquire<IpcInterfaceReplica>());
m_tun2socks.reset(m_node.acquire<IpcProcessTun2SocksReplica>());
}
IpcClient& IpcClient::Instance()
@@ -33,68 +32,43 @@ QSharedPointer<IpcInterfaceReplica> IpcClient::Interface()
return rep;
}
QSharedPointer<IpcProcessTun2SocksReplica> IpcClient::InterfaceTun2Socks()
QSharedPointer<IpcProcessInterfaceReplica> IpcClient::CreatePrivilegedProcess()
{
QSharedPointer<IpcProcessTun2SocksReplica> rep = Instance().m_tun2socks;
if (rep.isNull()) {
qCritical() << "IpcClient::InterfaceTun2Socks: Replica is undefined";
return nullptr;
}
if (!rep->waitForSource(1000)) {
qCritical() << "IpcClient::InterfaceTun2Socks: Failed to initialize replica";
return nullptr;
}
if (!rep->isReplicaValid()) {
qWarning() << "IpcClient::InterfaceTun2Socks(): Replica is invalid";
}
return rep;
}
QSharedPointer<PrivilegedProcess> IpcClient::CreatePrivilegedProcess()
{
QSharedPointer<IpcInterfaceReplica> rep = Interface();
if (!rep) {
qCritical() << "IpcClient::createPrivilegedProcess: Replica is invalid";
return nullptr;
}
QRemoteObjectPendingReply<int> pidReply = rep->createPrivilegedProcess();
if (!pidReply.waitForFinished(5000)){
qCritical() << "IpcClient::createPrivilegedProcess: Failed to execute RO createPrivilegedProcess call";
return nullptr;
}
int pid = pidReply.returnValue();
QSharedPointer<ProcessDescriptor> pd(new ProcessDescriptor());
pd->localSocket.reset(new QLocalSocket(pd->replicaNode.data()));
connect(pd->localSocket.data(), &QLocalSocket::connected, pd->replicaNode.data(), [pd]() {
pd->replicaNode->addClientSideConnection(pd->localSocket.data());
IpcProcessInterfaceReplica *repl = pd->replicaNode->acquire<IpcProcessInterfaceReplica>();
// TODO: rework the unsafe cast below
PrivilegedProcess *priv = static_cast<PrivilegedProcess *>(repl);
pd->ipcProcess.reset(priv);
if (!pd->ipcProcess) {
qWarning() << "Acquire PrivilegedProcess failed";
} else {
pd->ipcProcess->waitForSource(1000);
if (!pd->ipcProcess->isReplicaValid()) {
qWarning() << "PrivilegedProcess replica is not connected!";
}
QObject::connect(pd->ipcProcess.data(), &PrivilegedProcess::destroyed, pd->ipcProcess.data(),
[pd]() { pd->replicaNode->deleteLater(); });
return withInterface([](QSharedPointer<IpcInterfaceReplica> &iface) -> QSharedPointer<IpcProcessInterfaceReplica> {
auto createPrivilegedProcess = iface->createPrivilegedProcess();
if (!createPrivilegedProcess.waitForFinished()) {
qCritical() << "Failed to create privileged process";
return nullptr;
}
});
pd->localSocket->connectToServer(amnezia::getIpcProcessUrl(pid));
if (!pd->localSocket->waitForConnected()) {
qCritical() << "IpcClient::createPrivilegedProcess: Failed to connect to process' socket";
const int pid = createPrivilegedProcess.returnValue();
auto* node = new QRemoteObjectNode();
node->connectToNode(QUrl(QString("local:%1").arg(amnezia::getIpcProcessUrl(pid))));
QSharedPointer<IpcProcessInterfaceReplica> rep(
node->acquire<IpcProcessInterfaceReplica>(),
[node] (IpcProcessInterfaceReplica *ptr) {
delete ptr;
node->deleteLater();
}
);
if (rep.isNull()) {
qCritical() << "IpcClient::CreatePrivilegedProcess(): Failed to acquire replica";
return nullptr;
}
if (!rep->waitForSource()) {
qCritical() << "IpcClient::CreatePrivilegedProcess(): Failed to initialize replica";
return nullptr;
}
if (!rep->isReplicaValid()) {
qCritical() << "IpcClient::CreatePrivilegedProcess(): Replica is invalid";
return nullptr;
}
return rep;
},
[]() -> QSharedPointer<IpcProcessInterfaceReplica> {
return nullptr;
}
auto processReplica = QSharedPointer<PrivilegedProcess>(pd->ipcProcess);
return processReplica;
});
}
+2 -17
View File
@@ -5,9 +5,7 @@
#include <QObject>
#include "rep_ipc_interface_replica.h"
#include "rep_ipc_process_tun2socks_replica.h"
#include "privileged_process.h"
#include "rep_ipc_process_interface_replica.h"
class IpcClient : public QObject
{
@@ -18,8 +16,7 @@ public:
static IpcClient& Instance();
static QSharedPointer<IpcInterfaceReplica> Interface();
static QSharedPointer<IpcProcessTun2SocksReplica> InterfaceTun2Socks();
static QSharedPointer<PrivilegedProcess> CreatePrivilegedProcess();
static QSharedPointer<IpcProcessInterfaceReplica> CreatePrivilegedProcess();
template <typename Func>
static auto withInterface(Func func)
@@ -54,18 +51,6 @@ signals:
private:
QRemoteObjectNode m_node;
QSharedPointer<IpcInterfaceReplica> m_interface;
QSharedPointer<IpcProcessTun2SocksReplica> m_tun2socks;
struct ProcessDescriptor {
ProcessDescriptor () {
replicaNode = QSharedPointer<QRemoteObjectNode>(new QRemoteObjectNode());
ipcProcess = QSharedPointer<PrivilegedProcess>();
localSocket = QSharedPointer<QLocalSocket>();
}
QSharedPointer<PrivilegedProcess> ipcProcess;
QSharedPointer<QRemoteObjectNode> replicaNode;
QSharedPointer<QLocalSocket> localSocket;
};
};
#endif // IPCCLIENT_H
-27
View File
@@ -1,27 +0,0 @@
#include "privileged_process.h"
PrivilegedProcess::PrivilegedProcess() :
IpcProcessInterfaceReplica()
{
}
PrivilegedProcess::~PrivilegedProcess()
{
qDebug() << "PrivilegedProcess::~PrivilegedProcess()";
}
void PrivilegedProcess::waitForFinished(int msecs)
{
QSharedPointer<QEventLoop> loop(new QEventLoop);
connect(this, &PrivilegedProcess::finished, this, [this, loop](int exitCode, QProcess::ExitStatus exitStatus) mutable{
loop->quit();
loop.clear();
});
QTimer::singleShot(msecs, this, [this, loop]() mutable {
loop->quit();
loop.clear();
});
loop->exec();
}
-24
View File
@@ -1,24 +0,0 @@
#ifndef PRIVILEGED_PROCESS_H
#define PRIVILEGED_PROCESS_H
#include <QObject>
#include "rep_ipc_process_interface_replica.h"
// This class is dangerous - instance of this class casted from base class,
// so it support only functions
// Do not add any members into it
//
class PrivilegedProcess : public IpcProcessInterfaceReplica
{
Q_OBJECT
public:
PrivilegedProcess();
~PrivilegedProcess() override;
void waitForFinished(int msecs);
};
#endif // PRIVILEGED_PROCESS_H
+1 -1
View File
@@ -170,7 +170,7 @@ QJsonObject Deserialize(const QString &vmessStr, QString *alias, QString *errMes
// - It can be empty, if so, if the key is not in the JSON, or the value is empty, report an error.
// - Else if it contains one thing. if the key is not in the JSON, or the value is empty, use that one.
// - Else if it contains many things, when the key IS in the JSON but not within the THINGS, use the first in the THINGS
// - Else -------------------------------------------->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> use the JSON value
// - Else -------------------------------------------- use the JSON value
//
#define __vmess_checker__func(key, values) \
{ \
+1 -6
View File
@@ -270,12 +270,7 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
&& !wgConfig.value(amnezia::config_key::initPacketMagicHeader).isUndefined()
&& !wgConfig.value(amnezia::config_key::responsePacketMagicHeader).isUndefined()
&& !wgConfig.value(amnezia::config_key::underloadPacketMagicHeader).isUndefined()
&& !wgConfig.value(amnezia::config_key::transportPacketMagicHeader).isUndefined()
&& !wgConfig.value(amnezia::config_key::specialJunk1).isUndefined()
&& !wgConfig.value(amnezia::config_key::specialJunk2).isUndefined()
&& !wgConfig.value(amnezia::config_key::specialJunk3).isUndefined()
&& !wgConfig.value(amnezia::config_key::specialJunk4).isUndefined()
&& !wgConfig.value(amnezia::config_key::specialJunk5).isUndefined()) {
&& !wgConfig.value(amnezia::config_key::transportPacketMagicHeader).isUndefined()) {
json.insert(amnezia::config_key::junkPacketCount, wgConfig.value(amnezia::config_key::junkPacketCount));
json.insert(amnezia::config_key::junkPacketMinSize, wgConfig.value(amnezia::config_key::junkPacketMinSize));
json.insert(amnezia::config_key::junkPacketMaxSize, wgConfig.value(amnezia::config_key::junkPacketMaxSize));
+3 -9
View File
@@ -72,9 +72,9 @@ void NetworkWatcher::initialize() {
connect(m_impl, &NetworkWatcherImpl::unsecuredNetwork, this,
&NetworkWatcher::unsecuredNetwork);
connect(m_impl, &NetworkWatcherImpl::networkChanged, this,
&NetworkWatcher::networkChange);
connect(m_impl, &NetworkWatcherImpl::sleepMode, this,
&NetworkWatcher::onSleepMode);
&NetworkWatcher::networkChanged);
connect(m_impl, &NetworkWatcherImpl::wakeup, this,
&NetworkWatcher::wakeup);
m_impl->initialize();
// Enable sleep/wake monitoring for VPN auto-reconnection
@@ -97,12 +97,6 @@ void NetworkWatcher::settingsChanged() {
logger.debug() << "NetworkWatcher settings changed - keeping sleep monitoring active";
}
void NetworkWatcher::onSleepMode()
{
logger.debug() << "Resumed from sleep mode";
emit sleepMode();
}
void NetworkWatcher::unsecuredNetwork(const QString& networkName,
const QString& networkId) {
logger.debug() << "Unsecured network:" << logger.sensitive(networkName)
+2 -4
View File
@@ -29,13 +29,11 @@ public:
// false to restore.
void simulateDisconnection(bool simulatedDisconnection);
void onSleepMode();
QNetworkInformation::Reachability getReachability();
signals:
void networkChange();
void sleepMode();
void networkChanged();
void wakeup();
private:
void settingsChanged();
+1 -1
View File
@@ -41,7 +41,7 @@ signals:
// TODO: Only windows-networkwatcher has this, the other plattforms should
// too.
void networkChanged(QString newBSSID);
void sleepMode();
void wakeup();
private:
@@ -326,6 +326,57 @@ void AndroidController::sendTouch(float x, float y)
callActivityMethod("sendTouch", "(FF)V", x, y);
}
bool AndroidController::isPlay()
{
return callActivityMethod<jboolean>("isPlay", "()Z");
}
bool AndroidController::isTestPurchaseEnvironment()
{
return callActivityMethod<jboolean>("isTestPurchaseEnvironment", "()Z");
}
QJsonObject AndroidController::getSubscriptionPlans()
{
QJniObject subscriptionPlans = callActivityMethod<jstring>("getSubscriptionPlans", "()Ljava/lang/String;");
QJsonObject json = QJsonDocument::fromJson(subscriptionPlans.toString().toUtf8()).object();
return json;
}
QJsonObject AndroidController::purchaseSubscription(const QString &offerToken)
{
QJniObject result = callActivityMethod<jstring, jstring>("purchaseSubscription", "(Ljava/lang/String;)Ljava/lang/String;",
QJniObject::fromString(offerToken).object<jstring>());
QJsonObject json = QJsonDocument::fromJson(result.toString().toUtf8()).object();
return json;
}
QJsonObject AndroidController::upgradeSubscription(const QString &offerToken, const QString &oldPurchaseToken)
{
QJniObject result = callActivityMethod<jstring, jstring, jstring>("upgradeSubscription",
"(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;",
QJniObject::fromString(offerToken).object<jstring>(),
QJniObject::fromString(oldPurchaseToken).object<jstring>());
QJsonObject json = QJsonDocument::fromJson(result.toString().toUtf8()).object();
return json;
}
QJsonObject AndroidController::acknowledgePurchase(const QString &purchaseToken)
{
QJniObject result = callActivityMethod<jstring, jstring>("acknowledgePurchase", "(Ljava/lang/String;)Ljava/lang/String;",
QJniObject::fromString(purchaseToken).object<jstring>());
QJsonObject json = QJsonDocument::fromJson(result.toString().toUtf8()).object();
return json;
}
QJsonObject AndroidController::queryPurchases()
{
QJniObject result = callActivityMethod<jstring>("queryPurchases", "()Ljava/lang/String;");
QJsonObject json = QJsonDocument::fromJson(result.toString().toUtf8()).object();
return json;
}
// Moving log processing to the Android side
jclass AndroidController::log;
jmethodID AndroidController::logDebug;
@@ -55,6 +55,13 @@ public:
void requestNotificationPermission();
bool requestAuthentication();
void sendTouch(float x, float y);
bool isPlay();
bool isTestPurchaseEnvironment();
QJsonObject getSubscriptionPlans();
QJsonObject purchaseSubscription(const QString &offerToken);
QJsonObject upgradeSubscription(const QString &offerToken, const QString &oldPurchaseToken);
QJsonObject acknowledgePurchase(const QString &purchaseToken);
QJsonObject queryPurchases();
static bool initLogging();
static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message);
@@ -41,8 +41,8 @@ void LinuxNetworkWatcher::initialize() {
connect(m_worker, &LinuxNetworkWatcherWorker::unsecuredNetwork, this,
&LinuxNetworkWatcher::unsecuredNetwork);
connect(m_worker, &LinuxNetworkWatcherWorker::sleepMode, this,
&NetworkWatcherImpl::sleepMode);
connect(m_worker, &LinuxNetworkWatcherWorker::wakeup, this,
&NetworkWatcherImpl::wakeup);
// 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
@@ -200,7 +200,7 @@ void LinuxNetworkWatcherWorker::checkDevices() {
void LinuxNetworkWatcherWorker::NMStateChanged(quint32 state)
{
if (state == NM_STATE_ASLEEP) {
emit sleepMode();
emit wakeup();
}
logger.debug() << "NMStateChanged " << state;
@@ -23,7 +23,7 @@ class LinuxNetworkWatcherWorker final : public QObject {
signals:
void unsecuredNetwork(const QString& networkName, const QString& networkId);
void sleepMode();
void wakeup();
public slots:
void initialize();
@@ -173,10 +173,10 @@ void PowerNotificationsListener::sleepWakeupCallBack(void *refParam, io_service_
case kIOMessageSystemHasPoweredOn:
/* Announces that the system and its devices have woken up. */
logger.debug() << "System has powered on - emitting sleepMode signal from dedicated CFRunLoop thread";
logger.debug() << "System has powered on - emitting wakeup signal from dedicated CFRunLoop thread";
if (listener->m_watcher) {
// Use QMetaObject::invokeMethod for thread-safe signal emission
QMetaObject::invokeMethod(listener->m_watcher, "sleepMode", Qt::QueuedConnection);
QMetaObject::invokeMethod(listener->m_watcher, "wakeup", Qt::QueuedConnection);
}
break;
@@ -62,6 +62,9 @@ void WindowsDaemon::prepareActivation(const InterfaceConfig& config, int inetAda
}
void WindowsDaemon::activateSplitTunnel(const InterfaceConfig& config, int vpnAdapterIndex) {
if (m_splitTunnelManager == nullptr)
return;
if (config.m_vpnDisabledApps.length() > 0) {
m_splitTunnelManager->start(m_inetAdapterIndex, vpnAdapterIndex);
m_splitTunnelManager->excludeApps(config.m_vpnDisabledApps);
@@ -41,7 +41,7 @@ LRESULT WindowsNetworkWatcher::PowerWndProcCallback(HWND hwnd, UINT uMsg, WPARAM
switch (uMsg) {
case WM_POWERBROADCAST:
if (wParam == PBT_APMRESUMESUSPEND) {
emit obj->sleepMode();
emit obj->wakeup();
}
break;
default:
+3 -9
View File
@@ -232,12 +232,6 @@ ErrorCode OpenVpnProtocol::start()
return ErrorCode::AmneziaServiceConnectionFailed;
}
m_openVpnProcess->waitForSource(5000);
if (!m_openVpnProcess->isInitialized()) {
qWarning() << "IpcProcess replica is not connected!";
setLastError(ErrorCode::AmneziaServiceConnectionFailed);
return ErrorCode::AmneziaServiceConnectionFailed;
}
m_openVpnProcess->setProgram(PermittedProcess::OpenVPN);
QStringList arguments({
"--config", configPath(), "--management", m_managementHost, QString::number(mgmtPort),
@@ -246,13 +240,13 @@ ErrorCode OpenVpnProtocol::start()
m_openVpnProcess->setArguments(arguments);
qDebug() << arguments.join(" ");
connect(m_openVpnProcess.data(), &PrivilegedProcess::errorOccurred,
connect(m_openVpnProcess.data(), &IpcProcessInterfaceReplica::errorOccurred,
[&](QProcess::ProcessError error) { qDebug() << "PrivilegedProcess errorOccurred" << error; });
connect(m_openVpnProcess.data(), &PrivilegedProcess::stateChanged,
connect(m_openVpnProcess.data(), &IpcProcessInterfaceReplica::stateChanged,
[&](QProcess::ProcessState newState) { qDebug() << "PrivilegedProcess stateChanged" << newState; });
connect(m_openVpnProcess.data(), &PrivilegedProcess::finished, this,
connect(m_openVpnProcess.data(), &IpcProcessInterfaceReplica::finished, this,
[&]() { setConnectionState(Vpn::ConnectionState::Disconnected); });
m_openVpnProcess->start();
+1 -1
View File
@@ -53,7 +53,7 @@ private:
void updateRouteGateway(QString line);
void updateVpnGateway(const QString &line);
QSharedPointer<PrivilegedProcess> m_openVpnProcess;
QSharedPointer<IpcProcessInterfaceReplica> m_openVpnProcess;
};
#endif // OPENVPNPROTOCOL_H
+1 -1
View File
@@ -233,7 +233,7 @@ namespace amnezia
constexpr char defaultResponsePacketMagicHeader[] = "3288052141";
constexpr char defaultTransportPacketMagicHeader[] = "2528465083";
constexpr char defaultUnderloadPacketMagicHeader[] = "1766607858";
constexpr char defaultSpecialJunk1[] = "<b 0x084481800001000300000000077469636b65747306776964676574096b696e6f706f69736b0272750000010001c00c0005000100000039001806776964676574077469636b6574730679616e646578c025c0390005000100000039002b1765787465726e616c2d7469636b6574732d776964676574066166697368610679616e646578036e657400c05d000100010000001c000457fafe25>";
constexpr char defaultSpecialJunk1[] = "<r 2><b 0x858000010001000000000669636c6f756403636f6d0000010001c00c000100010000105a00044d583737>";
constexpr char defaultSpecialJunk2[] = "";
constexpr char defaultSpecialJunk3[] = "";
constexpr char defaultSpecialJunk4[] = "";
+2 -2
View File
@@ -15,7 +15,7 @@ WireguardProtocol::WireguardProtocol(const QJsonObject &configuration, QObject *
m_impl.reset(new LocalSocketController());
connect(m_impl.get(), &ControllerImpl::connected, this,
[this](const QString &pubkey, const QDateTime &connectionTimestamp) {
emit connectionStateChanged(Vpn::ConnectionState::Connected);
setConnectionState(Vpn::ConnectionState::Connected);
});
connect(m_impl.get(), &ControllerImpl::statusUpdated, this,
[this](const QString& serverIpv4Gateway,
@@ -38,7 +38,7 @@ WireguardProtocol::WireguardProtocol(const QJsonObject &configuration, QObject *
});
connect(m_impl.get(), &ControllerImpl::disconnected, this,
[this]() { emit connectionStateChanged(Vpn::ConnectionState::Disconnected); });
[this]() { setConnectionState(Vpn::ConnectionState::Disconnected); });
m_impl->initialize(nullptr, nullptr);
}
+196 -94
View File
@@ -1,6 +1,7 @@
#include "xrayprotocol.h"
#include "core/ipcclient.h"
#include "ipc.h"
#include "utilities.h"
#include "core/networkUtilities.h"
@@ -9,14 +10,37 @@
#include <QJsonObject>
#include <QNetworkInterface>
#include <QJsonDocument>
#include <QtCore/qlogging.h>
#include <QtCore/qobjectdefs.h>
#include <QtCore/qprocess.h>
#ifdef Q_OS_MACOS
static const QString tunName = "utun22";
#else
static const QString tunName = "tun2";
#endif
XrayProtocol::XrayProtocol(const QJsonObject &configuration, QObject *parent) : VpnProtocol(configuration, parent)
{
readXrayConfiguration(configuration);
m_routeGateway = NetworkUtilities::getGatewayAndIface().first;
m_vpnGateway = amnezia::protocols::xray::defaultLocalAddr;
m_vpnLocalAddress = amnezia::protocols::xray::defaultLocalAddr;
m_t2sProcess = IpcClient::InterfaceTun2Socks();
m_routeGateway = NetworkUtilities::getGatewayAndIface().first;
m_routeMode = static_cast<Settings::RouteMode>(configuration.value(amnezia::config_key::splitTunnelType).toInt());
m_remoteAddress = NetworkUtilities::getIPAddress(m_rawConfig.value(amnezia::config_key::hostName).toString());
const QString primaryDns = configuration.value(amnezia::config_key::dns1).toString();
m_dnsServers.push_back(QHostAddress(primaryDns));
if (primaryDns != amnezia::protocols::dns::amneziaDnsIp) {
const QString secondaryDns = configuration.value(amnezia::config_key::dns2).toString();
m_dnsServers.push_back(QHostAddress(secondaryDns));
}
QJsonObject xrayConfiguration = configuration.value(ProtocolProps::key_proto_config_data(Proto::Xray)).toObject();
if (xrayConfiguration.isEmpty()) {
xrayConfiguration = configuration.value(ProtocolProps::key_proto_config_data(Proto::SSXray)).toObject();
}
m_xrayConfig = xrayConfiguration;
}
XrayProtocol::~XrayProtocol()
@@ -29,72 +53,16 @@ ErrorCode XrayProtocol::start()
{
qDebug() << "XrayProtocol::start()";
const ErrorCode err = IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
iface->xrayStart(QJsonDocument(m_xrayConfig).toJson());
return ErrorCode::NoError;
return IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
auto xrayStart = iface->xrayStart(QJsonDocument(m_xrayConfig).toJson());
if (!xrayStart.waitForFinished() || !xrayStart.returnValue()) {
qCritical() << "Failed to start xray";
return ErrorCode::XrayExecutableCrashed;
}
return startTun2Socks();
}, [] () {
return ErrorCode::AmneziaServiceConnectionFailed;
});
if (err != ErrorCode::NoError)
return err;
setConnectionState(Vpn::ConnectionState::Connecting);
return startTun2Sock();
}
ErrorCode XrayProtocol::startTun2Sock()
{
m_t2sProcess->start();
connect(m_t2sProcess.data(), &IpcProcessTun2SocksReplica::stateChanged, this,
[&](QProcess::ProcessState newState) { qDebug() << "PrivilegedProcess stateChanged" << newState; });
connect(m_t2sProcess.data(), &IpcProcessTun2SocksReplica::setConnectionState, this, [&](int vpnState) {
qDebug() << "PrivilegedProcess setConnectionState " << vpnState;
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
if (vpnState == Vpn::ConnectionState::Connected) {
setConnectionState(Vpn::ConnectionState::Connecting);
QList<QHostAddress> dnsAddr;
dnsAddr.push_back(QHostAddress(m_primaryDNS));
// We don't use secondary DNS if primary DNS is AmneziaDNS
if (!m_primaryDNS.contains(amnezia::protocols::dns::amneziaDnsIp)) {
dnsAddr.push_back(QHostAddress(m_secondaryDNS));
}
#ifdef Q_OS_WIN
QThread::msleep(8000);
#endif
#ifdef Q_OS_MACOS
QThread::msleep(5000);
iface->createTun("utun22", amnezia::protocols::xray::defaultLocalAddr);
iface->updateResolvers("utun22", dnsAddr);
#endif
#ifdef Q_OS_LINUX
QThread::msleep(1000);
iface->createTun("tun2", amnezia::protocols::xray::defaultLocalAddr);
iface->updateResolvers("tun2", dnsAddr);
#endif
if (m_routeMode == Settings::RouteMode::VpnAllSites) {
iface->routeAddList(m_vpnGateway, QStringList() << "1.0.0.0/8" << "2.0.0.0/7" << "4.0.0.0/6" << "8.0.0.0/5" << "16.0.0.0/4" << "32.0.0.0/3" << "64.0.0.0/2" << "128.0.0.0/1");
}
iface->StopRoutingIpv6();
#ifdef Q_OS_WIN
iface->updateResolvers("tun2", dnsAddr);
#endif
setConnectionState(Vpn::ConnectionState::Connected);
}
#if !defined(Q_OS_MACOS)
if (vpnState == Vpn::ConnectionState::Disconnected) {
setConnectionState(Vpn::ConnectionState::Disconnected);
iface->deleteTun("tun2");
iface->StartRoutingIpv6();
iface->clearSavedRoutes();
}
#endif
});
});
return ErrorCode::NoError;
}
void XrayProtocol::stop()
@@ -102,43 +70,177 @@ void XrayProtocol::stop()
qDebug() << "XrayProtocol::stop()";
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
#ifdef AMNEZIA_DESKTOP
QRemoteObjectPendingReply<bool> StartRoutingIpv6Resp = iface->StartRoutingIpv6();
if (!StartRoutingIpv6Resp.waitForFinished(1000)) {
qWarning() << "XrayProtocol::stop(): Failed to start routing ipv6";
}
auto disableKillSwitch = iface->disableKillSwitch();
if (!disableKillSwitch.waitForFinished() || !disableKillSwitch.returnValue())
qWarning() << "Failed to disable killswitch";
QRemoteObjectPendingReply<bool> restoreResolvers = iface->restoreResolvers();
if (!restoreResolvers.waitForFinished(1000)) {
qWarning() << "XrayProtocol::stop(): Failed to restore resolvers";
}
auto StartRoutingIpv6 = iface->StartRoutingIpv6();
if (!StartRoutingIpv6.waitForFinished() || !StartRoutingIpv6.returnValue())
qWarning() << "Failed to start routing ipv6";
#if !defined(Q_OS_MACOS)
QRemoteObjectPendingReply<bool> deleteTunResp = iface->deleteTun("tun2");
if (!deleteTunResp.waitForFinished(1000)) {
qWarning() << "XrayProtocol::stop(): Failed to delete tun";
}
#endif
#endif
iface->xrayStop();
auto restoreResolvers = iface->restoreResolvers();
if (!restoreResolvers.waitForFinished() || !restoreResolvers.returnValue())
qWarning() << "Failed to restore resolvers";
auto deleteTun = iface->deleteTun(tunName);
if (!deleteTun.waitForFinished() || !deleteTun.returnValue())
qWarning() << "Failed to delete tun";
auto xrayStop = iface->xrayStop();
if (!xrayStop.waitForFinished() || !xrayStop.returnValue())
qWarning() << "Failed to stop xray";
});
if (m_t2sProcess) {
m_t2sProcess->stop();
QThread::msleep(200);
if (m_tun2socksProcess) {
m_tun2socksProcess->blockSignals(true);
#ifndef Q_OS_WIN
m_tun2socksProcess->terminate();
auto waitForFinished = m_tun2socksProcess->waitForFinished(1000);
if (!waitForFinished.waitForFinished() || !waitForFinished.returnValue()) {
qWarning() << "Failed to terminate tun2socks. Killing the process...";
m_tun2socksProcess->kill();
}
#else
// terminate does not do anything useful on Windows
// so just kill the process
m_tun2socksProcess->kill();
#endif
m_tun2socksProcess->close();
m_tun2socksProcess.reset();
}
setConnectionState(Vpn::ConnectionState::Disconnected);
}
void XrayProtocol::readXrayConfiguration(const QJsonObject &configuration)
ErrorCode XrayProtocol::startTun2Socks()
{
QJsonObject xrayConfiguration = configuration.value(ProtocolProps::key_proto_config_data(Proto::Xray)).toObject();
if (xrayConfiguration.isEmpty()) {
xrayConfiguration = configuration.value(ProtocolProps::key_proto_config_data(Proto::SSXray)).toObject();
m_tun2socksProcess = IpcClient::CreatePrivilegedProcess();
if (!m_tun2socksProcess->waitForSource()) {
return ErrorCode::AmneziaServiceConnectionFailed;
}
m_xrayConfig = xrayConfiguration;
m_routeMode = static_cast<Settings::RouteMode>(configuration.value(amnezia::config_key::splitTunnelType).toInt());
m_primaryDNS = configuration.value(amnezia::config_key::dns1).toString();
m_secondaryDNS = configuration.value(amnezia::config_key::dns2).toString();
m_tun2socksProcess->setProgram(PermittedProcess::Tun2Socks);
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();
if (!readAllStandardOutput.waitForFinished()) {
qWarning() << "Failed to read output from tun2socks";
return;
}
const QString line = readAllStandardOutput.returnValue();
if (!line.contains("[TCP]") && !line.contains("[UDP]"))
qDebug() << "[tun2socks]:" << line;
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) {
stop();
setLastError(res);
} else {
setConnectionState(Vpn::ConnectionState::Connected);
}
}
}, Qt::QueuedConnection);
connect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::finished, this, [this](int exitCode, QProcess::ExitStatus exitStatus) {
if (exitStatus == QProcess::ExitStatus::CrashExit) {
qCritical() << "Tun2socks process crashed!";
} else {
qCritical() << QString("Tun2socks process was closed with %1 exit code").arg(exitCode);
}
stop();
setLastError(ErrorCode::Tun2SockExecutableCrashed);
}, Qt::QueuedConnection);
m_tun2socksProcess->start();
return ErrorCode::NoError;
}
ErrorCode XrayProtocol::setupRouting() {
return IpcClient::withInterface([this](QSharedPointer<IpcInterfaceReplica> iface) -> ErrorCode {
#ifdef Q_OS_WIN
const int inetAdapterIndex = NetworkUtilities::AdapterIndexTo(QHostAddress(m_remoteAddress));
#endif
auto createTun = iface->createTun(tunName, amnezia::protocols::xray::defaultLocalAddr);
if (!createTun.waitForFinished() || !createTun.returnValue()) {
qCritical() << "Failed to assign IP address for TUN";
return ErrorCode::InternalError;
}
auto updateResolvers = iface->updateResolvers(tunName, m_dnsServers);
if (!updateResolvers.waitForFinished() || !updateResolvers.returnValue()) {
qCritical() << "Failed to set DNS resolvers for TUN";
return ErrorCode::InternalError;
}
#ifdef Q_OS_WIN
int vpnAdapterIndex = -1;
QList<QNetworkInterface> netInterfaces = QNetworkInterface::allInterfaces();
for (auto& netInterface : netInterfaces) {
for (auto& address : netInterface.addressEntries()) {
if (m_vpnLocalAddress == address.ip().toString())
vpnAdapterIndex = netInterface.index();
}
}
#else
static const int vpnAdapterIndex = 0;
#endif
const bool killSwitchEnabled = QVariant(m_rawConfig.value(config_key::killSwitchOption).toString()).toBool();
if (killSwitchEnabled) {
if (vpnAdapterIndex != -1) {
QJsonObject config = m_rawConfig;
config.insert("vpnServer", m_remoteAddress);
auto enableKillSwitch = IpcClient::Interface()->enableKillSwitch(config, vpnAdapterIndex);
if (!enableKillSwitch.waitForFinished() || !enableKillSwitch.returnValue()) {
qCritical() << "Failed to enable killswitch";
return ErrorCode::InternalError;
}
} else
qWarning() << "Failed to get vpnAdapterIndex. Killswitch disabled";
}
if (m_routeMode == Settings::RouteMode::VpnAllSites) {
static const QStringList subnets = { "1.0.0.0/8", "2.0.0.0/7", "4.0.0.0/6", "8.0.0.0/5", "16.0.0.0/4", "32.0.0.0/3", "64.0.0.0/2", "128.0.0.0/1" };
auto routeAddList = iface->routeAddList(m_vpnGateway, subnets);
if (!routeAddList.waitForFinished() || routeAddList.returnValue() != subnets.count()) {
qCritical() << "Failed to set routes for TUN";
return ErrorCode::InternalError;
}
}
auto StopRoutingIpv6 = iface->StopRoutingIpv6();
if (!StopRoutingIpv6.waitForFinished() || !StopRoutingIpv6.returnValue()) {
qCritical() << "Failed to disable IPv6 routing";
return ErrorCode::InternalError;
}
#ifdef Q_OS_WIN
if (inetAdapterIndex != -1 && vpnAdapterIndex != -1) {
QJsonObject config = m_rawConfig;
config.insert("inetAdapterIndex", inetAdapterIndex);
config.insert("vpnAdapterIndex", vpnAdapterIndex);
config.insert("vpnGateway", m_vpnGateway);
config.insert("vpnServer", m_remoteAddress);
auto enablePeerTraffic = iface->enablePeerTraffic(config);
if (!enablePeerTraffic.waitForFinished() || !enablePeerTraffic.returnValue()) {
qCritical() << "Failed to enable peer traffic";
return ErrorCode::InternalError;
}
} else
qWarning() << "Failed to get adapter indexes. Split-tunneling disabled";
#endif
return ErrorCode::NoError;
},
[] () {
return ErrorCode::AmneziaServiceConnectionFailed;
});
}
+8 -8
View File
@@ -6,6 +6,7 @@
#include "core/ipcclient.h"
#include "vpnprotocol.h"
#include "settings.h"
#include <QtCore/qsharedpointer.h>
class XrayProtocol : public VpnProtocol
{
@@ -14,19 +15,18 @@ public:
virtual ~XrayProtocol() override;
ErrorCode start() override;
ErrorCode startTun2Sock();
void stop() override;
private:
void readXrayConfiguration(const QJsonObject &configuration);
ErrorCode setupRouting();
ErrorCode startTun2Socks();
QJsonObject m_xrayConfig;
Settings::RouteMode m_routeMode;
QString m_primaryDNS;
QString m_secondaryDNS;
#ifndef Q_OS_IOS
QSharedPointer<IpcProcessTun2SocksReplica> m_t2sProcess;
#endif
QList<QHostAddress> m_dnsServers;
QString m_remoteAddress;
QSharedPointer<IpcProcessInterfaceReplica> m_tun2socksProcess;
};
#endif // XRAYPROTOCOL_H
+1 -3
View File
@@ -129,6 +129,7 @@
<file>ui/qml/Components/AdLabel.qml</file>
<file>ui/qml/Components/ConnectButton.qml</file>
<file>ui/qml/Components/ConnectionTypeSelectionDrawer.qml</file>
<file>ui/qml/Components/GamepadLoader.qml</file>
<file>ui/qml/Components/HomeContainersListView.qml</file>
<file>ui/qml/Components/HomeSplitTunnelingDrawer.qml</file>
<file>ui/qml/Components/InstalledAppsDrawer.qml</file>
@@ -247,9 +248,6 @@
<file>ui/qml/Pages2/PageSettingsApiNativeConfigs.qml</file>
<file>ui/qml/Pages2/PageSettingsApiDevices.qml</file>
<file>images/controls/monitor.svg</file>
<file>ui/qml/Components/ApiPremV1MigrationDrawer.qml</file>
<file>ui/qml/Components/ApiPremV1SubListDrawer.qml</file>
<file>ui/qml/Components/OtpCodeDrawer.qml</file>
<file>ui/qml/Components/AwgTextField.qml</file>
<file>ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml</file>
<file>ui/qml/Components/SmartScroll.qml</file>
+2 -1
View File
@@ -21,4 +21,5 @@ if [ "$(systemctl is-active docker)" != "active" ]; then \
sleep 5; sudo systemctl start docker; sleep 5;\
fi;\
if ! command -v sudo > /dev/null 2>&1; then echo "Failed to install sudo, command not found"; exit 1; fi;\
docker --version
docker --version;\
uname -sr
+9
View File
@@ -94,6 +94,15 @@ public:
setValue("Conf/startMinimized", enabled);
}
bool isNewsNotifications() const
{
return value("Conf/newsNotifications", true).toBool();
}
void setNewsNotifications(bool enabled)
{
setValue("Conf/newsNotifications", enabled);
}
bool isSaveLogs() const
{
return value("Conf/saveLogs", false).toBool();
@@ -11,9 +11,14 @@
#include <QClipboard>
#include <QDebug>
#include <QEventLoop>
#include <QFutureWatcher>
#include <QSet>
#include <QtConcurrent>
#include "platforms/ios/ios_controller.h"
#ifdef Q_OS_ANDROID
#include "platforms/android/android_controller.h"
#endif
namespace
{
@@ -370,6 +375,7 @@ bool ApiConfigsController::fillAvailableServices()
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/services"), apiPayload, responseBody);
qDebug().noquote() << "[Billing] gateway response v1/services responseBody:" << responseBody;
if (errorCode == ErrorCode::NoError) {
if (!responseBody.contains("services")) {
errorCode = ErrorCode::ApiServicesMissingError;
@@ -382,6 +388,142 @@ bool ApiConfigsController::fillAvailableServices()
}
QJsonObject data = QJsonDocument::fromJson(responseBody).object();
#if defined(Q_OS_IOS) || defined(MACOS_NE)
QEventLoop waitProducts;
bool productsFetched = false;
QString productPrice;
QString productCurrency;
IosController::Instance()->fetchProducts(QStringList() << QStringLiteral("amnezia_premium_6_month"),
[&](const QList<QVariantMap> &products,
const QStringList &invalidIds,
const QString &errorString) {
if (!errorString.isEmpty() || products.isEmpty()) {
qWarning().noquote() << "[IAP] Failed to fetch product price:" << errorString;
} else {
const auto &product = products.first();
productPrice = product.value("price").toString();
productCurrency = product.value("currencyCode").toString();
productsFetched = true;
qInfo().noquote() << "[IAP] Fetched product price:" << productPrice << productCurrency;
}
waitProducts.quit();
});
waitProducts.exec();
if (productsFetched && !productPrice.isEmpty()) {
QJsonArray services = data.value("services").toArray();
for (int i = 0; i < services.size(); ++i) {
QJsonObject service = services[i].toObject();
if (service.value(configKey::serviceType).toString() == serviceType::amneziaPremium) {
QJsonObject serviceInfo = service.value(configKey::serviceInfo).toObject();
QString formattedPrice = productPrice;
if (!productCurrency.isEmpty()) {
formattedPrice += " " + productCurrency;
}
serviceInfo["price"] = formattedPrice;
service[configKey::serviceInfo] = serviceInfo;
services[i] = service;
data["services"] = services;
qInfo().noquote() << "[IAP] Updated premium service price in data:" << formattedPrice;
break;
}
}
}
#elif defined(Q_OS_ANDROID)
// Get price from Google Play Billing
auto androidController = AndroidController::instance();
QJsonObject plansResult = androidController->getSubscriptionPlans();
int responseCode = plansResult.value("responseCode").toInt(-1);
qDebug().noquote() << "[Billing] getSubscriptionPlans plansResult:" << QJsonDocument(plansResult).toJson(QJsonDocument::Compact);
qDebug() << "[Billing] getSubscriptionPlans responseCode:" << responseCode;
if (responseCode == 0) {
QJsonArray products = plansResult.value("products").toArray();
QString formattedPrice;
int billingPeriodDays = 180;
for (const QJsonValue &productValue : products) {
QJsonObject product = productValue.toObject();
const QString productId = product.value("productId").toString();
const bool isPremium = (productId == "premium") || productId.contains("premium");
if (isPremium) {
QJsonArray offers = product.value("offers").toArray();
if (!offers.isEmpty()) {
QJsonObject firstOffer = offers.at(0).toObject();
QJsonArray pricingPhases = firstOffer.value("pricingPhases").toArray();
if (!pricingPhases.isEmpty()) {
QJsonObject pricingPhase = pricingPhases.at(0).toObject();
formattedPrice = pricingPhase.value("formatedPrice").toString();
if (formattedPrice.isEmpty()) {
formattedPrice = pricingPhase.value("formattedPrice").toString();
}
QString billingPeriod = pricingPhase.value("billingPeriod").toString();
if (billingPeriod.contains("Y")) {
int idx = billingPeriod.indexOf("Y");
int years = billingPeriod.mid(1, idx - 1).toInt();
if (years > 0) billingPeriodDays = years * 365;
} else if (billingPeriod.contains("M")) {
int idx = billingPeriod.indexOf("M");
int months = billingPeriod.mid(1, idx - 1).toInt();
if (months > 0) billingPeriodDays = months * 30;
} else if (billingPeriod.contains("D")) {
int idx = billingPeriod.indexOf("D");
billingPeriodDays = billingPeriod.mid(1, idx - 1).toInt();
}
}
}
break;
}
}
if (!formattedPrice.isEmpty()) {
QJsonArray services = data.value("services").toArray();
bool premiumFound = false;
for (int i = 0; i < services.size(); ++i) {
QJsonObject service = services[i].toObject();
if (service.value(configKey::serviceType).toString() == serviceType::amneziaPremium) {
QJsonObject serviceInfo = service.value(configKey::serviceInfo).toObject();
serviceInfo["price"] = formattedPrice;
service[configKey::serviceInfo] = serviceInfo;
services[i] = service;
data["services"] = services;
premiumFound = true;
qInfo() << "[Billing] Updated premium service price in data:" << formattedPrice;
break;
}
}
/* if (!premiumFound) {
// Gateway did not return premium; add it from billing data
QString region = data.value(configKey::userCountryCode).toString();
QJsonObject serviceInfo;
serviceInfo["name"] = tr("Amnezia Premium");
serviceInfo["price"] = formattedPrice;
serviceInfo["region"] = region;
serviceInfo["speed"] = "200";
serviceInfo["timelimit"] = QString::number(billingPeriodDays);
QJsonObject serviceDescription;
serviceDescription["card_description"] = tr("Amnezia Premium is classic VPN for seamless work, downloading large files, and watching videos.");
serviceDescription["description"] = serviceDescription["card_description"];
serviceDescription["features"] = "";
QJsonObject premiumService;
premiumService[configKey::serviceType] = serviceType::amneziaPremium;
premiumService[configKey::serviceProtocol] = "amnezia-premium";
premiumService[configKey::serviceInfo] = serviceInfo;
premiumService["service_description"] = serviceDescription;
premiumService["available_countries"] = QJsonArray();
premiumService["is_available"] = true;
premiumService["store_endpoint"] = "";
premiumService["subscription"] = QJsonObject();
services.prepend(premiumService);
data["services"] = services;
qInfo() << "[Billing] Added premium service from billing (gateway did not return it)";
}*/
}
} else {
qWarning() << "[Billing] Failed to fetch product price, responseCode:" << responseCode;
}
#endif
m_apiServicesModel->updateModel(data);
if (m_apiServicesModel->rowCount() > 0) {
m_apiServicesModel->setServiceIndex(0);
@@ -391,25 +533,19 @@ bool ApiConfigsController::fillAvailableServices()
bool ApiConfigsController::importService()
{
#if defined(Q_OS_IOS) || defined(MACOS_NE)
bool isIosOrMacOsNe = true;
#else
bool isIosOrMacOsNe = false;
#endif
if (m_apiServicesModel->getSelectedServiceType() == serviceType::amneziaPremium) {
if (isIosOrMacOsNe) {
importSerivceFromAppStore();
return true;
}
} else {
importServiceFromGateway();
#if defined(Q_OS_IOS) || defined(MACOS_NE) || defined(Q_OS_ANDROID)
importServiceFromPaymentMarket();
return true;
#else
return false; // premium only via App Store / Play
#endif
}
return false;
importServiceFromGateway();
return true;
}
bool ApiConfigsController::importSerivceFromAppStore()
bool ApiConfigsController::importServiceFromPaymentMarket()
{
#if defined(Q_OS_IOS) || defined(MACOS_NE)
bool purchaseOk = false;
@@ -466,12 +602,116 @@ bool ApiConfigsController::importSerivceFromAppStore()
return false;
}
emit installServerFromApiFinished(tr("%1 installed successfully.").arg(m_apiServicesModel->getSelectedServiceName()));
#elif defined(Q_OS_ANDROID)
auto androidController = AndroidController::instance();
QString purchaseToken;
bool purchaseOk = false;
QFutureWatcher<QPair<bool, QString>> watcher;
QEventLoop waitLoop;
connect(&watcher, &QFutureWatcher<QPair<bool, QString>>::finished, &waitLoop, &QEventLoop::quit);
QFuture<QPair<bool, QString>> future = QtConcurrent::run([androidController]() {
QJsonObject plansResult = androidController->getSubscriptionPlans();
int responseCode = plansResult.value("responseCode").toInt(-1);
qDebug().noquote() << "[Billing] importService getSubscriptionPlans plansResult:" << QJsonDocument(plansResult).toJson(QJsonDocument::Compact);
qDebug() << "[Billing] importService getSubscriptionPlans responseCode:" << responseCode;
if (responseCode != 0) {
qWarning() << "[Billing] Failed to get subscription plans, responseCode:" << responseCode;
return qMakePair(false, QString());
}
QJsonArray products = plansResult.value("products").toArray();
QString offerToken;
for (const QJsonValue &productValue : products) {
QJsonObject product = productValue.toObject();
const QString productId = product.value("productId").toString();
const bool isPremium = (productId == "premium") || productId.contains("premium");
if (isPremium) {
QJsonArray offers = product.value("offers").toArray();
if (!offers.isEmpty()) {
QJsonObject firstOffer = offers.at(0).toObject();
offerToken = firstOffer.value("offerToken").toString();
qInfo() << "[Billing] Found offer token:" << offerToken;
break;
}
}
}
if (offerToken.isEmpty()) {
qWarning() << "[Billing] No offer token found for premium subscription";
return qMakePair(false, QString());
}
QJsonObject purchaseResult = androidController->purchaseSubscription(offerToken);
responseCode = purchaseResult.value("responseCode").toInt(-1);
if (responseCode != 0) {
qWarning() << "[Billing] Purchase failed, responseCode:" << responseCode;
return qMakePair(false, QString());
}
QJsonArray purchases = purchaseResult.value("purchases").toArray();
if (purchases.isEmpty()) {
qWarning() << "[Billing] Purchase succeeded but no purchases returned";
return qMakePair(false, QString());
}
QJsonObject purchase = purchases.at(0).toObject();
QString token = purchase.value("purchaseToken").toString();
bool isAcknowledged = purchase.value("isAcknowledged").toBool();
qInfo() << "[Billing] Purchase success. purchaseToken:" << token << "isAcknowledged:" << isAcknowledged;
if (!isAcknowledged) {
QJsonObject ackResult = androidController->acknowledgePurchase(token);
if (ackResult.value("responseCode").toInt(-1) != 0) {
qWarning() << "[Billing] Acknowledge failed";
} else {
qInfo() << "[Billing] Purchase acknowledged successfully";
}
}
return qMakePair(true, token);
});
watcher.setFuture(future);
waitLoop.exec();
purchaseOk = watcher.result().first;
purchaseToken = watcher.result().second;
if (!purchaseOk || purchaseToken.isEmpty()) {
emit errorOccurred(ErrorCode::ApiPurchaseError);
return false;
}
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
m_settings->getInstallationUuid(true),
m_apiServicesModel->getCountryCode(),
"",
m_apiServicesModel->getSelectedServiceType(),
m_apiServicesModel->getSelectedServiceProtocol(),
QJsonObject() };
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
apiPayload[apiDefs::key::transactionId] = purchaseToken;
bool isTestPurchase = m_settings->isDevGatewayEnv(false) || androidController->isTestPurchaseEnvironment();
ErrorCode errorCode;
QByteArray responseBody;
errorCode = executeRequest(QString("%1v1/subscriptions"), apiPayload, responseBody, isTestPurchase);
if (errorCode != ErrorCode::NoError) {
emit errorOccurred(errorCode);
return false;
}
errorCode = importServiceFromBilling(responseBody, isTestPurchase);
if (errorCode != ErrorCode::NoError) {
emit errorOccurred(errorCode);
return false;
}
emit installServerFromApiFinished(tr("%1 installed successfully.").arg(m_apiServicesModel->getSelectedServiceName()));
#endif
return true;
}
bool ApiConfigsController::restoreSerivceFromAppStore()
bool ApiConfigsController::restoreServiceFromPaymentMarket()
{
#if defined(Q_OS_IOS) || defined(MACOS_NE)
const QString premiumServiceType = QStringLiteral("amnezia-premium");
@@ -594,6 +834,131 @@ bool ApiConfigsController::restoreSerivceFromAppStore()
qInfo().noquote() << "[IAP] Skipped" << duplicateCount
<< "duplicate restored transactions for original transaction IDs already processed";
}
#elif defined(Q_OS_ANDROID)
// Android Google Play Billing restore implementation
const QString premiumServiceType = QStringLiteral("amnezia-premium");
if (!fillAvailableServices()) {
qWarning() << "[Billing] Unable to fetch services list before restore";
emit errorOccurred(ErrorCode::ApiServicesMissingError);
return false;
}
if (m_apiServicesModel->rowCount() <= 0) {
emit errorOccurred(ErrorCode::ApiServicesMissingError);
return false;
}
// Ensure we have a valid premium selection for gateway requests
bool premiumSelected = false;
for (int i = 0; i < m_apiServicesModel->rowCount(); ++i) {
m_apiServicesModel->setServiceIndex(i);
if (m_apiServicesModel->getSelectedServiceType() == premiumServiceType) {
premiumSelected = true;
break;
}
}
if (!premiumSelected) {
emit errorOccurred(ErrorCode::ApiServicesMissingError);
return false;
}
auto androidController = AndroidController::instance();
// Query existing purchases
QJsonObject purchasesResult = androidController->queryPurchases();
int responseCode = purchasesResult.value("responseCode").toInt(-1);
if (responseCode != 0) {
qWarning() << "[Billing] Failed to query purchases, responseCode:" << responseCode;
emit errorOccurred(ErrorCode::ApiPurchaseError);
return false;
}
QJsonArray purchases = purchasesResult.value("purchases").toArray();
if (purchases.isEmpty()) {
qInfo() << "[Billing] No purchases found to restore";
emit errorOccurred(ErrorCode::ApiNoPurchasesToRestore);
return false;
}
bool hasInstalledConfig = false;
bool duplicateConfigAlreadyPresent = false;
QSet<QString> processedTokens;
for (const QJsonValue &purchaseValue : purchases) {
QJsonObject purchase = purchaseValue.toObject();
QString purchaseToken = purchase.value("purchaseToken").toString();
bool isAcknowledged = purchase.value("isAcknowledged").toBool();
if (purchaseToken.isEmpty()) {
qWarning() << "[Billing] Skipping purchase without token";
continue;
}
if (processedTokens.contains(purchaseToken)) {
continue;
}
processedTokens.insert(purchaseToken);
qInfo() << "[Billing] Restoring purchase. purchaseToken:" << purchaseToken
<< "isAcknowledged:" << isAcknowledged;
// Acknowledge purchase if needed
if (!isAcknowledged) {
QJsonObject ackResult = androidController->acknowledgePurchase(purchaseToken);
int ackResponseCode = ackResult.value("responseCode").toInt(-1);
if (ackResponseCode != 0) {
qWarning() << "[Billing] Acknowledge failed, responseCode:" << ackResponseCode;
} else {
qInfo() << "[Billing] Purchase acknowledged successfully";
}
}
// Send purchase token to gateway
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
m_settings->getInstallationUuid(true),
m_apiServicesModel->getCountryCode(),
"",
m_apiServicesModel->getSelectedServiceType(),
m_apiServicesModel->getSelectedServiceProtocol(),
QJsonObject() };
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
apiPayload[apiDefs::key::transactionId] = purchaseToken;
bool isTestPurchase = m_settings->isDevGatewayEnv(false) || androidController->isTestPurchaseEnvironment();
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/subscriptions"), apiPayload, responseBody, isTestPurchase);
if (errorCode != ErrorCode::NoError) {
qWarning() << "[Billing] Failed to restore purchase" << purchaseToken
<< "errorCode =" << static_cast<int>(errorCode);
continue;
}
ErrorCode installError = importServiceFromBilling(responseBody, isTestPurchase);
if (installError == ErrorCode::ApiConfigAlreadyAdded) {
duplicateConfigAlreadyPresent = true;
qInfo() << "[Billing] Skipping restored purchase" << purchaseToken
<< "because subscription config with the same vpn_key already exists";
} else if (installError != ErrorCode::NoError) {
qWarning() << "[Billing] Failed to process restored subscription response for purchase" << purchaseToken;
} else {
hasInstalledConfig = true;
}
}
if (!hasInstalledConfig) {
const ErrorCode restoreError = duplicateConfigAlreadyPresent ? ErrorCode::ApiConfigAlreadyAdded : ErrorCode::ApiPurchaseError;
emit errorOccurred(restoreError);
return false;
}
emit installServerFromApiFinished(tr("Subscription restored successfully."));
#endif
return true;
}
@@ -899,16 +1264,16 @@ QString ApiConfigsController::getVpnKey()
ErrorCode ApiConfigsController::importServiceFromBilling(const QByteArray &responseBody, const bool isTestPurchase)
{
#ifdef Q_OS_IOS
#if defined(Q_OS_IOS) || defined(Q_OS_ANDROID)
QJsonObject responseObject = QJsonDocument::fromJson(responseBody).object();
QString key = responseObject.value(QStringLiteral("key")).toString();
if (key.isEmpty()) {
qWarning().noquote() << "[IAP] Subscription response does not contain a key field";
qWarning().noquote() << "[IAP/Billing] Subscription response does not contain a key field";
return ErrorCode::ApiPurchaseError;
}
if (m_serversModel->hasServerWithVpnKey(key)) {
qInfo().noquote() << "[IAP] Subscription config with the same vpn_key already exists";
qInfo().noquote() << "[IAP/Billing] Subscription config with the same vpn_key already exists";
return ErrorCode::ApiConfigAlreadyAdded;
}
@@ -922,7 +1287,7 @@ ErrorCode ApiConfigsController::importServiceFromBilling(const QByteArray &respo
}
if (configString.isEmpty()) {
qWarning().noquote() << "[IAP] Subscription response config payload is empty";
qWarning().noquote() << "[IAP/Billing] Subscription response config payload is empty";
return ErrorCode::ApiPurchaseError;
}
@@ -27,8 +27,8 @@ public slots:
bool fillAvailableServices();
bool importService();
bool importSerivceFromAppStore();
bool restoreSerivceFromAppStore();
bool importServiceFromPaymentMarket();
bool restoreServiceFromPaymentMarket();
bool importServiceFromGateway();
bool updateServiceFromGateway(const int serverIndex, const QString &newCountryCode, const QString &newCountryName,
bool reloadServiceConfig = false);
@@ -1,133 +0,0 @@
#include "apiPremV1MigrationController.h"
#include <QEventLoop>
#include <QTimer>
#include "core/api/apiDefs.h"
#include "core/api/apiUtils.h"
#include "core/controllers/gatewayController.h"
ApiPremV1MigrationController::ApiPremV1MigrationController(const QSharedPointer<ServersModel> &serversModel,
const std::shared_ptr<Settings> &settings, QObject *parent)
: QObject(parent), m_serversModel(serversModel), m_settings(settings)
{
}
bool ApiPremV1MigrationController::hasConfigsToMigration()
{
QJsonArray vpnKeys;
auto serversCount = m_serversModel->getServersCount();
for (size_t i = 0; i < serversCount; i++) {
auto serverConfigObject = m_serversModel->getServerConfig(i);
if (apiUtils::getConfigType(serverConfigObject) != apiDefs::ConfigType::AmneziaPremiumV1) {
continue;
}
QString vpnKey = apiUtils::getPremiumV1VpnKey(serverConfigObject);
vpnKeys.append(vpnKey);
}
if (!vpnKeys.isEmpty()) {
GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled());
QJsonObject apiPayload;
apiPayload["configs"] = vpnKeys;
QByteArray responseBody;
ErrorCode errorCode = gatewayController.post(QString("%1v1/prem-v1/is-active-subscription"), apiPayload, responseBody);
auto migrationsStatus = QJsonDocument::fromJson(responseBody).object();
for (const auto &migrationStatus : migrationsStatus) {
if (migrationStatus == "not_found") {
return true;
}
}
}
return false;
}
void ApiPremV1MigrationController::getSubscriptionList(const QString &email)
{
GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled());
QJsonObject apiPayload;
apiPayload[apiDefs::key::email] = email;
QByteArray responseBody;
ErrorCode errorCode = gatewayController.post(QString("%1v1/prem-v1/subscription-list"), apiPayload, responseBody);
if (errorCode == ErrorCode::NoError) {
m_email = email;
m_subscriptionsModel = QJsonDocument::fromJson(responseBody).array();
if (m_subscriptionsModel.isEmpty()) {
emit noSubscriptionToMigrate();
return;
}
emit subscriptionsModelChanged();
} else {
emit errorOccurred(ErrorCode::ApiMigrationError);
}
}
QJsonArray ApiPremV1MigrationController::getSubscriptionModel()
{
return m_subscriptionsModel;
}
void ApiPremV1MigrationController::sendMigrationCode(const int subscriptionIndex)
{
QEventLoop wait;
QTimer::singleShot(1000, &wait, &QEventLoop::quit);
wait.exec(QEventLoop::ExcludeUserInputEvents);
GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled());
QJsonObject apiPayload;
apiPayload[apiDefs::key::email] = m_email;
QByteArray responseBody;
ErrorCode errorCode = gatewayController.post(QString("%1v1/prem-v1/migration-code"), apiPayload, responseBody);
if (errorCode == ErrorCode::NoError) {
m_subscriptionIndex = subscriptionIndex;
emit otpSuccessfullySent();
} else {
emit errorOccurred(ErrorCode::ApiMigrationError);
}
}
void ApiPremV1MigrationController::migrate(const QString &migrationCode)
{
GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled());
QJsonObject apiPayload;
apiPayload[apiDefs::key::email] = m_email;
apiPayload[apiDefs::key::orderId] = m_subscriptionsModel.at(m_subscriptionIndex).toObject().value(apiDefs::key::id).toString();
apiPayload[apiDefs::key::migrationCode] = migrationCode;
QByteArray responseBody;
ErrorCode errorCode = gatewayController.post(QString("%1v1/prem-v1/migrate"), apiPayload, responseBody);
if (errorCode == ErrorCode::NoError) {
auto responseObject = QJsonDocument::fromJson(responseBody).object();
QString premiumV2VpnKey = responseObject.value(apiDefs::key::config).toString();
emit importPremiumV2VpnKey(premiumV2VpnKey);
} else {
emit errorOccurred(ErrorCode::ApiMigrationError);
}
}
bool ApiPremV1MigrationController::isPremV1MigrationReminderActive()
{
return m_settings->isPremV1MigrationReminderActive();
}
void ApiPremV1MigrationController::disablePremV1MigrationReminder()
{
m_settings->disablePremV1MigrationReminder();
}
@@ -1,50 +0,0 @@
#ifndef APIPREMV1MIGRATIONCONTROLLER_H
#define APIPREMV1MIGRATIONCONTROLLER_H
#include <QObject>
#include "ui/models/servers_model.h"
class ApiPremV1MigrationController : public QObject
{
Q_OBJECT
public:
ApiPremV1MigrationController(const QSharedPointer<ServersModel> &serversModel, const std::shared_ptr<Settings> &settings,
QObject *parent = nullptr);
Q_PROPERTY(QJsonArray subscriptionsModel READ getSubscriptionModel NOTIFY subscriptionsModelChanged)
public slots:
bool hasConfigsToMigration();
void getSubscriptionList(const QString &email);
QJsonArray getSubscriptionModel();
void sendMigrationCode(const int subscriptionIndex);
void migrate(const QString &migrationCode);
bool isPremV1MigrationReminderActive();
void disablePremV1MigrationReminder();
signals:
void subscriptionsModelChanged();
void otpSuccessfullySent();
void importPremiumV2VpnKey(const QString &vpnKey);
void errorOccurred(ErrorCode errorCode);
void showMigrationDrawer();
void migrationFinished();
void noSubscriptionToMigrate();
private:
QSharedPointer<ServersModel> m_serversModel;
std::shared_ptr<Settings> m_settings;
QJsonArray m_subscriptionsModel;
int m_subscriptionIndex;
QString m_email;
};
#endif // APIPREMV1MIGRATIONCONTROLLER_H
@@ -291,6 +291,8 @@ void ImportController::processNativeWireGuardConfig()
clientProtocolConfig[config_key::cookieReplyPacketJunkSize] = "0";
clientProtocolConfig[config_key::transportPacketJunkSize] = "0";
clientProtocolConfig[config_key::specialJunk1] = protocols::awg::defaultSpecialJunk1;
clientProtocolConfig[config_key::isObfuscationEnabled] = true;
serverProtocolConfig[config_key::last_config] = QString(QJsonDocument(clientProtocolConfig).toJson());
@@ -451,6 +451,13 @@ ErrorCode InstallController::getAlreadyInstalledContainers(const ServerCredentia
containerConfig[config_key::transportPacketMagicHeader] =
serverConfigMap.value(config_key::transportPacketMagicHeader);
// hack to parse i1-i5 from commented lines in server config
containerConfig[config_key::specialJunk1] = serverConfigMap.value(QString("# ") + config_key::specialJunk1);
containerConfig[config_key::specialJunk2] = serverConfigMap.value(QString("# ") + config_key::specialJunk2);
containerConfig[config_key::specialJunk3] = serverConfigMap.value(QString("# ") + config_key::specialJunk3);
containerConfig[config_key::specialJunk4] = serverConfigMap.value(QString("# ") + config_key::specialJunk4);
containerConfig[config_key::specialJunk5] = serverConfigMap.value(QString("# ") + config_key::specialJunk5);
if (container == DockerContainer::Awg2) {
containerConfig[config_key::protocolVersion] = "2";
containerConfig[config_key::cookieReplyPacketJunkSize] =
@@ -308,6 +308,15 @@ void SettingsController::toggleStartMinimized(bool enable)
emit startMinimizedChanged();
}
bool SettingsController::isNewsNotificationsEnabled()
{
return m_settings->isNewsNotifications();
}
void SettingsController::toggleNewsNotificationsEnabled(bool enable)
{
m_settings->setNewsNotifications(enable);
}
bool SettingsController::isScreenshotsEnabled()
{
return m_settings->isScreenshotsEnabled();
@@ -73,6 +73,9 @@ public slots:
bool isStartMinimizedEnabled();
void toggleStartMinimized(bool enable);
bool isNewsNotificationsEnabled();
void toggleNewsNotificationsEnabled(bool enable);
bool isScreenshotsEnabled();
void toggleScreenshotsEnabled(bool enable);
@@ -112,7 +112,13 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
if (price == "free") {
return tr("Free");
}
#if defined(Q_OS_IOS) || defined(MACOS_NE)
return tr("%1 $").arg(price);
#elif defined(Q_OS_ANDROID)
return price;
#else
return tr("%1 $/month").arg(price);
#endif
}
case EndDateRole: {
return QDateTime::fromString(apiServiceData.subscription.endDate, Qt::ISODate).toLocalTime().toString("d MMM yyyy");
@@ -1,194 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtCore
import PageEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
import "../Components"
DrawerType2 {
id: root
expandedHeight: parent.height * 0.9
Connections {
target: ApiPremV1MigrationController
function onErrorOccurred(error, goToPageHome) {
PageController.showErrorMessage(error)
root.closeTriggered()
}
}
expandedStateContent: Item {
implicitHeight: root.expandedHeight
ListViewType {
id: listView
anchors.fill: parent
model: 1 // fake model to force the ListView to be created without a model
snapMode: ListView.NoSnap
header: ColumnLayout {
width: listView.width
Header2Type {
id: header
Layout.fillWidth: true
Layout.topMargin: 20
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: qsTr("Switch to the new Amnezia Premium subscription")
}
}
delegate: ColumnLayout {
width: listView.width
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: 16
anchors.rightMargin: 16
ParagraphTextType {
Layout.fillWidth: true
Layout.topMargin: 24
Layout.bottomMargin: 24
horizontalAlignment: Text.AlignLeft
textFormat: Text.RichText
text: {
var str = qsTr("We'll preserve all remaining days of your current subscription and give you an extra month as a thank you. ")
str += qsTr("This new subscription type will be actively developed with more locations and features added regularly. Currently available:")
str += "<ul style='margin-left: -16px;'>"
str += qsTr("<li>20 locations (with more coming soon)</li>")
str += qsTr("<li>Easier switching between countries in the app</li>")
str += qsTr("<li>Personal dashboard to manage your subscription</li>")
str += "</ul>"
str += qsTr("Old keys will be deactivated after switching.")
}
}
TextFieldWithHeaderType {
id: emailLabel
Layout.fillWidth: true
borderColor: AmneziaStyle.color.mutedGray
headerTextColor: AmneziaStyle.color.paleGray
headerText: qsTr("Email")
textField.placeholderText: qsTr("mail@example.com")
textField.onFocusChanged: {
textField.text = textField.text.replace(/^\s+|\s+$/g, '')
}
Connections {
target: ApiPremV1MigrationController
function onNoSubscriptionToMigrate() {
emailLabel.errorText = qsTr("No old format subscriptions for a given email")
}
}
}
CaptionTextType {
Layout.fillWidth: true
Layout.topMargin: 16
color: AmneziaStyle.color.mutedGray
text: qsTr("Enter the email you used for your current subscription")
}
ApiPremV1SubListDrawer {
id: apiPremV1SubListDrawer
parent: root
anchors.fill: parent
}
OtpCodeDrawer {
id: otpCodeDrawer
parent: root
anchors.fill: parent
}
BasicButtonType {
id: yesButton
Layout.fillWidth: true
Layout.topMargin: 32
text: qsTr("Continue")
clickedFunc: function() {
PageController.showBusyIndicator(true)
ApiPremV1MigrationController.getSubscriptionList(emailLabel.textField.text)
PageController.showBusyIndicator(false)
}
}
BasicButtonType {
id: noButton
Layout.fillWidth: true
defaultColor: AmneziaStyle.color.transparent
hoveredColor: AmneziaStyle.color.translucentWhite
pressedColor: AmneziaStyle.color.sheerWhite
disabledColor: AmneziaStyle.color.mutedGray
textColor: AmneziaStyle.color.paleGray
borderWidth: 1
text: qsTr("Remind me later")
clickedFunc: function() {
root.closeTriggered()
}
}
BasicButtonType {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 32
Layout.bottomMargin: 32
implicitHeight: 32
defaultColor: "transparent"
hoveredColor: AmneziaStyle.color.translucentWhite
pressedColor: AmneziaStyle.color.sheerWhite
textColor: AmneziaStyle.color.vibrantRed
text: qsTr("Don't remind me again")
clickedFunc: function() {
var headerText = qsTr("No more reminders? You can always switch to the new format in the server settings")
var yesButtonText = qsTr("Continue")
var noButtonText = qsTr("Cancel")
var yesButtonFunction = function() {
ApiPremV1MigrationController.disablePremV1MigrationReminder()
root.closeTriggered()
}
var noButtonFunction = function() {
}
showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
}
}
}
}
}
}
@@ -1,89 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Style 1.0
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
DrawerType2 {
id: root
Connections {
target: ApiPremV1MigrationController
function onSubscriptionsModelChanged() {
if (ApiPremV1MigrationController.subscriptionsModel.length > 1) {
root.openTriggered()
} else {
sendMigrationCode(0)
}
}
}
function sendMigrationCode(index) {
PageController.showBusyIndicator(true)
ApiPremV1MigrationController.sendMigrationCode(index)
root.closeTriggered()
PageController.showBusyIndicator(false)
}
expandedHeight: parent.height * 0.9
expandedStateContent: Item {
implicitHeight: root.expandedHeight
ListViewType {
id: listView
anchors.fill: parent
model: ApiPremV1MigrationController.subscriptionsModel
header: ColumnLayout {
width: listView.width
Header2Type {
id: header
Layout.fillWidth: true
Layout.topMargin: 20
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: qsTr("Choose Subscription")
}
}
delegate: Item {
implicitWidth: listView.width
implicitHeight: delegateContent.implicitHeight
ColumnLayout {
id: delegateContent
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
LabelWithButtonType {
id: server
Layout.fillWidth: true
text: qsTr("Order ID: ") + modelData.id
descriptionText: qsTr("Purchase Date: ") + Qt.formatDateTime(new Date(modelData.created_at), "dd.MM.yyyy hh:mm")
rightImageSource: "qrc:/images/controls/chevron-right.svg"
clickedFunction: function() {
sendMigrationCode(index)
}
}
DividerType {}
}
}
}
}
}
@@ -0,0 +1,38 @@
import QtQuick
import QtGamepadLegacy
Item {
id: root
property alias gamepad: gamepad
property alias gamepadKeyNav: gamepadKeyNav
Gamepad {
id: gamepad
deviceId: GamepadManager.connectedGamepads.length > 0 ? GamepadManager.connectedGamepads[0] : -1
onButtonStartChanged: {
if (buttonStart) {
ServersModel.setProcessedServerIndex(ServersModel.defaultIndex)
ConnectionController.connectButtonClicked()
}
}
}
GamepadKeyNavigation {
id: gamepadKeyNav
gamepad: gamepad
active: true
}
Connections {
target: GamepadManager
function onConnectedGamepadsChanged() {
if (GamepadManager.connectedGamepads.length > 0) {
gamepad.deviceId = GamepadManager.connectedGamepads[0]
} else {
gamepad.deviceId = -1
}
}
}
}
@@ -1,77 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Style 1.0
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
DrawerType2 {
id: root
Connections {
target: ApiPremV1MigrationController
function onOtpSuccessfullySent() {
root.openTriggered()
}
}
expandedHeight: parent.height * 0.6
expandedStateContent: Item {
implicitHeight: root.expandedHeight
ColumnLayout {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: 16
anchors.rightMargin: 16
spacing: 0
Header2Type {
id: header
Layout.fillWidth: true
Layout.topMargin: 20
headerText: qsTr("OTP code was sent to your email")
}
TextFieldWithHeaderType {
id: otpFiled
borderColor: AmneziaStyle.color.mutedGray
headerTextColor: AmneziaStyle.color.paleGray
Layout.fillWidth: true
Layout.topMargin: 16
headerText: qsTr("OTP Code")
textField.maximumLength: 30
checkEmptyText: true
}
BasicButtonType {
id: saveButton
Layout.fillWidth: true
Layout.topMargin: 16
text: qsTr("Continue")
clickedFunc: function() {
PageController.showBusyIndicator(true)
ApiPremV1MigrationController.migrate(otpFiled.textField.text)
PageController.showBusyIndicator(false)
root.closeTriggered()
}
}
}
}
}
+3 -3
View File
@@ -111,11 +111,11 @@ Button {
color: {
if (root.enabled) {
if (root.pressed) {
return pressedColor
return root.pressedColor
}
return root.hovered ? hoveredColor : defaultColor
return root.hovered ? root.hoveredColor : root.defaultColor
} else {
return disabledColor
return root.disabledColor
}
}
+53
View File
@@ -49,6 +49,55 @@ Item {
return drawerContent.state === stateName
}
function isDrawerType2(obj) {
return obj && typeof obj.drawerExpandedStateName !== "undefined" &&
typeof obj.drawerCollapsedStateName !== "undefined"
}
function isDescendantOfDrawer(obj) {
var current = obj
while (current && current !== root.parent) {
if (isDrawerType2(current)) {
return true
}
current = current.parent
}
return false
}
function findComponent(obj, typeCtor) {
if (!obj)
return null
if (isDrawerType2(obj) || isDescendantOfDrawer(obj))
return null
if (obj instanceof typeCtor)
return obj
if (obj.children && obj.children.length > 0) {
for (var i = 0; i < obj.children.length; i++) {
var matchingChildren = findComponent(obj.children[i], typeCtor)
if (matchingChildren) return matchingChildren
}
}
if (obj.contentItem) {
var matchingContentItem = findComponent(obj.contentItem, typeCtor)
if (matchingContentItem) return matchingContentItem
}
return null
}
function setParentInteractive(value) {
var flickableType = findComponent(root.parent, Flickable)
var listViewType = findComponent(root.parent, ListView)
if (flickableType) flickableType.interactive = value
if (listViewType) listViewType.interactive = value
}
Connections {
target: Qt.application
@@ -93,6 +142,8 @@ Item {
aboutToHide()
setParentInteractive(true)
closed()
}
@@ -118,6 +169,8 @@ Item {
root.aboutToShow()
setParentInteractive(false)
root.opened()
}
@@ -71,6 +71,8 @@ Item {
implicitHeight: content.implicitHeight + content.anchors.leftMargin + content.anchors.rightMargin
MouseArea {
id: mouseArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: root.enabled
@@ -296,13 +298,13 @@ Item {
}
Keys.onEnterPressed: {
if (clickedFunction && typeof clickedFunction === "function") {
if (!rightImageSource && clickedFunction && typeof clickedFunction === "function") {
clickedFunction()
}
}
Keys.onReturnPressed: {
if (clickedFunction && typeof clickedFunction === "function") {
if (!rightImageSource && clickedFunction && typeof clickedFunction === "function") {
clickedFunction()
}
}
@@ -19,9 +19,6 @@ Item {
property string buttonText
property string buttonImageSource
property string buttonImageColor: AmneziaStyle.color.midnightBlack
property string buttonBackgroundColor: AmneziaStyle.color.paleGray
property string buttonHoveredColor: AmneziaStyle.color.lightGray
property var clickedFunc
property alias textField: textField
@@ -70,7 +67,7 @@ Item {
border.width: 1
Behavior on border.color {
PropertyAnimation { duration: 100 }
PropertyAnimation { duration: 200 }
}
RowLayout {
@@ -124,7 +121,7 @@ Item {
background: Rectangle {
anchors.fill: parent
color: root.enabled ? root.backgroundColor : root.backgroundDisabledColor
color: root.backgroundDisabledColor
}
onTextChanged: {
@@ -189,14 +186,6 @@ Item {
focusPolicy: Qt.NoFocus
text: root.buttonText
leftImageSource: root.buttonImageSource
leftImageColor: root.buttonImageColor
defaultColor: root.buttonBackgroundColor
hoveredColor: root.buttonHoveredColor
pressedColor: root.buttonHoveredColor
disabledColor: AmneziaStyle.color.transparent
borderWidth: 0
anchors.top: content.top
anchors.bottom: content.bottom
@@ -204,7 +193,7 @@ Item {
height: content.implicitHeight
width: content.implicitHeight
squareLeftSide: false
squareLeftSide: true
clickedFunc: function() {
if (root.clickedFunc && typeof root.clickedFunc === "function") {
-29
View File
@@ -48,30 +48,6 @@ PageType {
}
}
Connections {
target: ApiPremV1MigrationController
function onMigrationFinished() {
apiPremV1MigrationDrawer.closeTriggered()
var headerText = qsTr("You've successfully switched to the new Amnezia Premium subscription!")
var descriptionText = qsTr("Old keys will no longer work. Please use your new subscription key to connect. \nThank you for staying with us!")
var yesButtonText = qsTr("Continue")
var noButtonText = ""
var yesButtonFunction = function() {
}
var noButtonFunction = function() {
}
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
}
function onShowMigrationDrawer() {
apiPremV1MigrationDrawer.openTriggered()
}
}
Item {
objectName: "homeColumnItem"
@@ -500,9 +476,4 @@ PageType {
}
}
}
ApiPremV1MigrationDrawer {
id: apiPremV1MigrationDrawer
anchors.fill: parent
}
}
@@ -140,6 +140,16 @@ PageType {
ListElement { name : "aes-128-gcm" }
}
function updateSelectedIndex() {
cipherDropDown.text = cipher
for (var i = 0; i < cipherListView.model.count; i++) {
if (cipherListView.model.get(i).name === cipher) {
selectedIndex = i
break
}
}
}
clickedFunction: function() {
cipherDropDown.text = selectedText
cipher = cipherDropDown.text
@@ -147,13 +157,14 @@ PageType {
}
Component.onCompleted: {
cipherDropDown.text = cipher
updateSelectedIndex()
}
}
for (var i = 0; i < cipherListView.model.count; i++) {
if (cipherListView.model.get(i).name === cipherDropDown.text) {
selectedIndex = i
}
}
Connections {
target: listView.model
function onDataChanged() {
cipherListView.updateSelectedIndex()
}
}
}
@@ -192,6 +192,16 @@ PageType {
ListElement { name : qsTr("SHA1") }
}
function updateSelectedIndex() {
hashDropDown.text = hash
for (var i = 0; i < hashListView.model.count; i++) {
if (hashListView.model.get(i).name === hash) {
selectedIndex = i
break
}
}
}
clickedFunction: function() {
hashDropDown.text = selectedText
hash = hashDropDown.text
@@ -199,13 +209,14 @@ PageType {
}
Component.onCompleted: {
hashDropDown.text = hash
updateSelectedIndex()
}
}
for (var i = 0; i < hashListView.model.count; i++) {
if (hashListView.model.get(i).name === hashDropDown.text) {
currentIndex = i
}
}
Connections {
target: listView.model
function onDataChanged() {
hashListView.updateSelectedIndex()
}
}
}
@@ -242,6 +253,16 @@ PageType {
ListElement { name : qsTr("none") }
}
function updateSelectedIndex() {
cipherDropDown.text = cipher
for (var i = 0; i < cipherListView.model.count; i++) {
if (cipherListView.model.get(i).name === cipher) {
selectedIndex = i
break
}
}
}
clickedFunction: function() {
cipherDropDown.text = selectedText
cipher = cipherDropDown.text
@@ -249,13 +270,14 @@ PageType {
}
Component.onCompleted: {
cipherDropDown.text = cipher
updateSelectedIndex()
}
}
for (var i = 0; i < cipherListView.model.count; i++) {
if (cipherListView.model.get(i).name === cipherDropDown.text) {
currentIndex = i
}
}
Connections {
target: listView.model
function onDataChanged() {
cipherListView.updateSelectedIndex()
}
}
}
@@ -109,6 +109,16 @@ PageType {
ListElement { name : "aes-128-gcm" }
}
function updateSelectedIndex() {
cipherDropDown.text = cipher
for (var i = 0; i < cipherListView.model.count; i++) {
if (cipherListView.model.get(i).name === cipher) {
selectedIndex = i
break
}
}
}
clickedFunction: function() {
cipherDropDown.text = selectedText
cipher = cipherDropDown.text
@@ -116,13 +126,14 @@ PageType {
}
Component.onCompleted: {
cipherDropDown.text = cipher
updateSelectedIndex()
}
}
for (var i = 0; i < cipherListView.model.count; i++) {
if (cipherListView.model.get(i).name === cipherDropDown.text) {
currentIndex = i
}
}
Connections {
target: listView.model
function onDataChanged() {
cipherListView.updateSelectedIndex()
}
}
}
+1 -1
View File
@@ -148,7 +148,7 @@ PageType {
id: news
property string title: qsTr("News & Notifications")
readonly property string leftImagePath: NewsModel.hasUnread ? "qrc:/images/controls/news-unread.svg" : "qrc:/images/controls/news.svg"
readonly property string leftImagePath: NewsModel.hasUnread && SettingsController.isNewsNotificationsEnabled() ? "qrc:/images/controls/news-unread.svg" : "qrc:/images/controls/news.svg"
property bool isVisible: ServersModel.hasServersFromGatewayApi
readonly property var clickedHandler: function() {
if (!ServersModel.hasServersFromGatewayApi) {
@@ -396,9 +396,7 @@ PageType {
PageController.showNotificationMessage(qsTr("Cannot remove server during active connection"))
} else {
PageController.showBusyIndicator(true)
if (ApiConfigsController.deactivateDevice(true)) {
InstallController.removeProcessedServer()
}
InstallController.removeProcessedServer()
PageController.showBusyIndicator(false)
}
}
@@ -224,7 +224,6 @@ PageType {
height: addAppButton.implicitHeight + 48 + SettingsController.safeAreaBottomMargin
color: AmneziaStyle.color.midnightBlack
opacity: 0.8
RowLayout {
id: addAppButton
@@ -168,6 +168,29 @@ PageType {
DividerType {
visible: !GC.isMobile()
}
SwitcherType {
id: switcherNewsNotificationEnabled
visible: ServersModel.hasServersFromGatewayApi
Layout.fillWidth: true
Layout.margins: 16
text: qsTr("News Notification")
descriptionText: qsTr("Show notification icon when has unread news")
checked: SettingsController.isNewsNotificationsEnabled()
onToggled: function() {
if (checked !== SettingsController.isNewsNotificationsEnabled()) {
SettingsController.toggleNewsNotificationsEnabled(checked)
}
}
}
DividerType {
visible: !GC.isMobile()
}
}
footer: ColumnLayout {
@@ -101,7 +101,6 @@ PageType {
remove,
clear,
reset,
switch_to_premium,
]
QtObject {
@@ -237,16 +236,4 @@ PageType {
}
}
QtObject {
id: switch_to_premium
property bool isVisible: ServersModel.getProcessedServerData("isServerFromTelegramApi") && ServersModel.processedServerIsPremium
readonly property string title: qsTr("Switch to the new Amnezia Premium subscription")
readonly property string description: ""
readonly property var tColor: AmneziaStyle.color.vibrantRed
readonly property var clickedHandler: function() {
PageController.goToPageHome()
ApiPremV1MigrationController.showMigrationDrawer()
}
}
}
@@ -240,7 +240,6 @@ PageType {
height: addSiteButton.implicitHeight + 48
color: AmneziaStyle.color.midnightBlack
opacity: 0.8
RowLayout {
id: addSiteButton
@@ -1,179 +1,237 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import PageEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
import "../Components"
PageType {
id: root
BackButtonType {
id: backButton
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onFocusChanged: {
if (this.activeFocus) {
listView.positionViewAtBeginning()
}
}
}
ListViewType {
id: listView
anchors.top: backButton.bottom
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.left: parent.left
header: ColumnLayout {
width: listView.width
BaseHeaderType {
Layout.fillWidth: true
Layout.topMargin: 8
Layout.rightMargin: 16
Layout.leftMargin: 16
Layout.bottomMargin: 32
headerText: ApiServicesModel.getSelectedServiceData("name")
descriptionText: ApiServicesModel.getSelectedServiceData("serviceDescription")
}
}
model: inputFields
spacing: 0
delegate: ColumnLayout {
width: listView.width
LabelWithImageType {
Layout.fillWidth: true
Layout.margins: 16
imageSource: imagePath
leftText: lText
rightText: rText
visible: isVisible
}
}
footer: ColumnLayout {
width: listView.width
spacing: 0
ParagraphTextType {
Layout.fillWidth: true
Layout.rightMargin: 16
Layout.leftMargin: 16
onLinkActivated: function(link) {
Qt.openUrlExternally(link)
}
textFormat: Text.RichText
text: {
var text = ApiServicesModel.getSelectedServiceData("features")
return text.replace("%1", LanguageModel.getCurrentSiteUrl("free")).replace("/free", "") // todo link should come from gateway
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
}
}
BasicButtonType {
id: continueButton
Layout.fillWidth: true
Layout.topMargin: 32
Layout.bottomMargin: 32
Layout.leftMargin: 16
Layout.rightMargin: 16
text: qsTr("Connect")
clickedFunc: function() {
PageController.showBusyIndicator(true)
var result = ApiConfigsController.importService()
PageController.showBusyIndicator(false)
if (!result) {
var endpoint = ApiServicesModel.getStoreEndpoint()
Qt.openUrlExternally(endpoint)
PageController.closePage()
PageController.closePage()
}
}
}
}
}
property list<QtObject> inputFields: [
region,
price,
timeLimit,
speed,
features
]
QtObject {
id: region
readonly property string imagePath: "qrc:/images/controls/map-pin.svg"
readonly property string lText: qsTr("For the region")
readonly property string rText: ApiServicesModel.getSelectedServiceData("region")
property bool isVisible: true
}
QtObject {
id: price
readonly property string imagePath: "qrc:/images/controls/tag.svg"
readonly property string lText: qsTr("Price")
readonly property string rText: ApiServicesModel.getSelectedServiceData("price")
property bool isVisible: true
}
QtObject {
id: timeLimit
readonly property string imagePath: "qrc:/images/controls/history.svg"
readonly property string lText: qsTr("Work period")
readonly property string rText: ApiServicesModel.getSelectedServiceData("timeLimit")
property bool isVisible: rText !== ""
}
QtObject {
id: speed
readonly property string imagePath: "qrc:/images/controls/gauge.svg"
readonly property string lText: qsTr("Speed")
readonly property string rText: ApiServicesModel.getSelectedServiceData("speed")
property bool isVisible: true
}
QtObject {
id: features
readonly property string imagePath: "qrc:/images/controls/info.svg"
readonly property string lText: qsTr("Features")
readonly property string rText: ""
property bool isVisible: true
}
}
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import PageEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
import "../Components"
PageType {
id: root
BackButtonType {
id: backButton
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onFocusChanged: {
if (this.activeFocus) {
listView.positionViewAtBeginning()
}
}
}
ListViewType {
id: listView
anchors.top: backButton.bottom
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.left: parent.left
header: ColumnLayout {
width: listView.width
BaseHeaderType {
Layout.fillWidth: true
Layout.topMargin: 8
Layout.rightMargin: 16
Layout.leftMargin: 16
Layout.bottomMargin: 32
headerText: ApiServicesModel.getSelectedServiceData("name")
descriptionText: ApiServicesModel.getSelectedServiceData("serviceDescription")
}
}
model: inputFields
spacing: 0
delegate: ColumnLayout {
width: listView.width
LabelWithImageType {
Layout.fillWidth: true
Layout.margins: 16
imageSource: imagePath
leftText: lText
rightText: rText
visible: isVisible
}
}
footer: ColumnLayout {
width: listView.width
spacing: 0
ParagraphTextType {
Layout.fillWidth: true
Layout.rightMargin: 16
Layout.leftMargin: 16
onLinkActivated: function(link) {
Qt.openUrlExternally(link)
}
textFormat: Text.RichText
text: {
var text = ApiServicesModel.getSelectedServiceData("features")
return text.replace("%1", LanguageModel.getCurrentSiteUrl("free")).replace("/free", "") // todo link should come from gateway
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
}
}
ParagraphTextType {
Layout.fillWidth: true
Layout.topMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
visible: ((Qt.platform.os === "ios" || IsMacOsNeBuild) && ApiServicesModel.getSelectedServiceType() === "amnezia-premium") ||
(Qt.platform.os === "android" && ApiServicesModel.getSelectedServiceType() === "amnezia-premium")
horizontalAlignment: Text.AlignHCenter
textFormat: Text.PlainText
color: AmneziaStyle.color.mutedGray
font.pixelSize: 12
text: {
if (Qt.platform.os === "ios" || IsMacOsNeBuild) {
return qsTr("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.")
} else if (Qt.platform.os === "android") {
return qsTr("Charged to your Google Play account at confirmation. Renews automatically unless auto-renew is turned off at least 24 hours before period end. Manage in Google Play settings.")
}
return ""
}
}
BasicButtonType {
id: continueButton
Layout.fillWidth: true
Layout.topMargin: 32
Layout.bottomMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
text: ApiServicesModel.getSelectedServiceType() === "amnezia-premium" ? qsTr("Subscribe Now") : qsTr("Connect")
clickedFunc: function() {
PageController.showBusyIndicator(true)
var result = ApiConfigsController.importService()
PageController.showBusyIndicator(false)
if (!result) {
var endpoint = ApiServicesModel.getStoreEndpoint()
Qt.openUrlExternally(endpoint)
PageController.closePage()
PageController.closePage()
}
}
}
ParagraphTextType {
Layout.fillWidth: true
Layout.topMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 32
visible: ((Qt.platform.os === "ios" || IsMacOsNeBuild) && ApiServicesModel.getSelectedServiceType() === "amnezia-premium") ||
(Qt.platform.os === "android" && ApiServicesModel.getSelectedServiceType() === "amnezia-premium")
horizontalAlignment: Text.AlignHCenter
textFormat: Text.RichText
color: AmneziaStyle.color.mutedGray
font.pixelSize: 12
text: {
var termsUrl = Qt.platform.os === "ios" || IsMacOsNeBuild ?
"https://www.apple.com/legal/internet-services/itunes/dev/stdeula/" :
"https://play.google.com/intl/en_us/about/play-terms/"
var privacyUrl = LanguageModel.getCurrentSiteUrl("policy")
return qsTr("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>").arg(termsUrl).arg(privacyUrl)
}
onLinkActivated: function(link) {
Qt.openUrlExternally(link)
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
}
}
}
}
property list<QtObject> inputFields: [
region,
price,
timeLimit,
speed,
features
]
QtObject {
id: region
readonly property string imagePath: "qrc:/images/controls/map-pin.svg"
readonly property string lText: qsTr("For the region")
readonly property string rText: ApiServicesModel.getSelectedServiceData("region")
property bool isVisible: true
}
QtObject {
id: price
readonly property string imagePath: "qrc:/images/controls/tag.svg"
readonly property string lText: qsTr("Price")
readonly property string rText: ApiServicesModel.getSelectedServiceData("price")
property bool isVisible: true
}
QtObject {
id: timeLimit
readonly property string imagePath: "qrc:/images/controls/history.svg"
readonly property string lText: qsTr("Work period")
readonly property string rText: ApiServicesModel.getSelectedServiceData("timeLimit")
property bool isVisible: rText !== ""
}
QtObject {
id: speed
readonly property string imagePath: "qrc:/images/controls/gauge.svg"
readonly property string lText: qsTr("Speed")
readonly property string rText: ApiServicesModel.getSelectedServiceData("speed")
property bool isVisible: true
}
QtObject {
id: features
readonly property string imagePath: "qrc:/images/controls/info.svg"
readonly property string lText: qsTr("Features")
readonly property string rText: ""
property bool isVisible: true
}
}
@@ -358,10 +358,10 @@ PageType {
property string title: qsTr("Restore purchases")
property string description: qsTr("")
property string imageSource: "qrc:/images/controls/refresh-cw.svg"
property bool isVisible: Qt.platform.os === "ios" || IsMacOsNeBuild
property bool isVisible: Qt.platform.os === "ios" || IsMacOsNeBuild || Qt.platform.os === "android"
property var handler: function() {
PageController.showBusyIndicator(true)
ApiConfigsController.restoreSerivceFromAppStore()
ApiConfigsController.restoreServiceFromPaymentMarket()
PageController.showBusyIndicator(false)
}
}
@@ -107,6 +107,7 @@ PageType {
onClicked: function() {
isEasySetup = true
checked = true
var defaultContainerProto = ContainerProps.defaultProtocol(dockerContainer)
listView.dockerContainer = dockerContainer
+1 -1
View File
@@ -383,7 +383,7 @@ PageType {
objectName: "settingsTabButton"
isSelected: tabBar.currentIndex === 2
image: (ServersModel.hasServersFromGatewayApi && NewsModel.hasUnread) ? "qrc:/images/controls/settings-news.svg" : "qrc:/images/controls/settings.svg"
image: (ServersModel.hasServersFromGatewayApi && NewsModel.hasUnread && SettingsController.isNewsNotificationsEnabled()) ? "qrc:/images/controls/settings-news.svg" : "qrc:/images/controls/settings.svg"
Binding {
target: settingsTabButton
property: "defaultColor"
+5
View File
@@ -83,6 +83,11 @@ Window {
}
}
Loader {
active: Qt.platform.os === "android"
source: Qt.platform.os === "android" ? "Components/GamepadLoader.qml" : ""
}
Connections {
objectName: "pageControllerConnections"
+100 -223
View File
@@ -41,7 +41,6 @@ VpnConnection::VpnConnection(std::shared_ptr<Settings> settings, QObject *parent
m_checkTimer.setInterval(1000);
connect(IosController::Instance(), &IosController::connectionStateChanged, this, &VpnConnection::onConnectionStateChanged);
connect(IosController::Instance(), &IosController::bytesChanged, this, &VpnConnection::onBytesChanged);
#endif
}
@@ -59,7 +58,7 @@ void VpnConnection::onKillSwitchModeChanged(bool enabled)
#ifdef AMNEZIA_DESKTOP
IpcClient::withInterface([enabled](QSharedPointer<IpcInterfaceReplica> iface){
QRemoteObjectPendingReply<bool> reply = iface->refreshKillSwitch(enabled);
if (reply.waitForFinished(1000) && reply.returnValue())
if (reply.waitForFinished() && reply.returnValue())
qDebug() << "VpnConnection::onKillSwitchModeChanged: Killswitch refreshed";
else
qWarning() << "VpnConnection::onKillSwitchModeChanged: Failed to execute remote refreshKillSwitch call";
@@ -73,60 +72,57 @@ void VpnConnection::onConnectionStateChanged(Vpn::ConnectionState state)
auto container = m_settings->defaultContainer(m_settings->defaultServerIndex());
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
if (state == Vpn::ConnectionState::Connected) {
iface->resetIpStack();
iface->flushDns();
switch (state) {
case Vpn::ConnectionState::Connected: {
iface->resetIpStack();
if (!ContainerProps::isAwgContainer(container) &&
container != DockerContainer::WireGuard) {
QString dns1 = m_vpnConfiguration.value(config_key::dns1).toString();
QString dns2 = m_vpnConfiguration.value(config_key::dns2).toString();
auto flushDns = iface->flushDns();
if (flushDns.waitForFinished() && flushDns.returnValue())
qDebug() << "VpnConnection::onConnectionStateChanged: Successfully flushed DNS";
else
qWarning() << "VpnConnection::onConnectionStateChanged: Failed to clear saved routes";
iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << dns1 << dns2);
if (m_settings->isSitesSplitTunnelingEnabled()) {
iface->routeDeleteList(m_vpnProtocol->vpnGateway(), QStringList() << "0.0.0.0");
// qDebug() << "VpnConnection::onConnectionStateChanged :: adding custom routes, count:" << forwardIps.size();
if (m_settings->routeMode() == Settings::VpnOnlyForwardSites) {
QTimer::singleShot(1000, m_vpnProtocol.data(),
[this]() { addSitesRoutes(m_vpnProtocol->vpnGateway(), m_settings->routeMode()); });
} else if (m_settings->routeMode() == Settings::VpnAllExceptSites) {
iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << "0.0.0.0/1");
iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << "128.0.0.0/1");
if (!ContainerProps::isAwgContainer(container) &&
container != DockerContainer::WireGuard) {
QString dns1 = m_vpnConfiguration.value(config_key::dns1).toString();
QString dns2 = m_vpnConfiguration.value(config_key::dns2).toString();
iface->routeAddList(m_vpnProtocol->routeGateway(), QStringList() << remoteAddress());
addSitesRoutes(m_vpnProtocol->routeGateway(), m_settings->routeMode());
// TODO: add error code handling for all routeAddList (or rework the code below)
iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << dns1 << dns2);
if (m_settings->isSitesSplitTunnelingEnabled()) {
iface->routeDeleteList(m_vpnProtocol->vpnGateway(), QStringList() << "0.0.0.0");
// qDebug() << "VpnConnection::onConnectionStateChanged :: adding custom routes, count:" << forwardIps.size();
if (m_settings->routeMode() == Settings::VpnOnlyForwardSites) {
QTimer::singleShot(1000, m_vpnProtocol.data(),
[this]() { addSitesRoutes(m_vpnProtocol->vpnGateway(), m_settings->routeMode()); });
} else if (m_settings->routeMode() == Settings::VpnAllExceptSites) {
iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << "0.0.0.0/1");
iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << "128.0.0.0/1");
iface->routeAddList(m_vpnProtocol->routeGateway(), QStringList() << remoteAddress());
addSitesRoutes(m_vpnProtocol->routeGateway(), m_settings->routeMode());
}
}
}
}
} break;
case Vpn::ConnectionState::Disconnected:
case Vpn::ConnectionState::Error: {
auto flushDns = iface->flushDns();
if (flushDns.waitForFinished() && flushDns.returnValue())
qDebug() << "VpnConnection::onConnectionStateChanged: Successfully flushed DNS";
else
qWarning() << "VpnConnection::onConnectionStateChanged: Failed to flush DNS";
if (container != DockerContainer::Ipsec) {
if (startNetworkCheckIfReady()) {
m_pendingNetworkCheck = false;
} else {
m_pendingNetworkCheck = true;
qWarning() << "Deferring startNetworkCheck; missing gateway/local address"
<< m_vpnProtocol->vpnGateway() << m_vpnProtocol->vpnLocalAddress();
}
} else {
m_pendingNetworkCheck = false;
}
} else if (state == Vpn::ConnectionState::Error) {
m_pendingNetworkCheck = false;
iface->flushDns();
if (m_settings->isSitesSplitTunnelingEnabled()) {
if (m_settings->routeMode() == Settings::VpnOnlyForwardSites) {
iface->clearSavedRoutes();
}
}
} else if (state == Vpn::ConnectionState::Connecting) {
} else if (state == Vpn::ConnectionState::Disconnected) {
m_pendingNetworkCheck = false;
auto result = iface->stopNetworkCheck();
result.waitForFinished(3000);
auto clearSavedRoutes = iface->clearSavedRoutes();
if (clearSavedRoutes.waitForFinished() && clearSavedRoutes.returnValue())
qDebug() << "VpnConnection::onConnectionStateChanged: Successfully cleared saved routes";
else
qWarning() << "VpnConnection::onConnectionStateChanged: Failed to clear saved routes";
} break;
default:
break;
}
});
#endif
@@ -140,7 +136,6 @@ void VpnConnection::onConnectionStateChanged(Vpn::ConnectionState state)
m_checkTimer.stop();
}
#endif
emit connectionStateChanged(state);
}
const QString &VpnConnection::remoteAddress() const
@@ -185,7 +180,11 @@ void VpnConnection::addSitesRoutes(const QString &gw, Settings::RouteMode mode)
});
m_settings->addVpnSite(mode, site, ip);
}
flushDns();
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
auto reply = iface->flushDns();
if (reply.waitForFinished() || !reply.returnValue())
qWarning() << "VpnConnection::addSitesRoutes: Failed to flush DNS";
});
break;
}
}
@@ -200,48 +199,6 @@ QSharedPointer<VpnProtocol> VpnConnection::vpnProtocol() const
return m_vpnProtocol;
}
void VpnConnection::addRoutes(const QStringList &ips)
{
#ifdef AMNEZIA_DESKTOP
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
if (connectionState() == Vpn::ConnectionState::Connected) {
if (m_settings->routeMode() == Settings::VpnOnlyForwardSites) {
iface->routeAddList(m_vpnProtocol->vpnGateway(), ips);
} else if (m_settings->routeMode() == Settings::VpnAllExceptSites) {
iface->routeAddList(m_vpnProtocol->routeGateway(), ips);
}
}
});
#endif
}
void VpnConnection::deleteRoutes(const QStringList &ips)
{
#ifdef AMNEZIA_DESKTOP
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
if (connectionState() == Vpn::ConnectionState::Connected) {
if (m_settings->routeMode() == Settings::VpnOnlyForwardSites) {
iface->routeDeleteList(vpnProtocol()->vpnGateway(), ips);
} else if (m_settings->routeMode() == Settings::VpnAllExceptSites) {
iface->routeDeleteList(m_vpnProtocol->routeGateway(), ips);
}
}
});
#endif
}
void VpnConnection::flushDns()
{
#ifdef AMNEZIA_DESKTOP
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
auto reply = iface->flushDns();
if (reply.waitForFinished(1000) || !reply.returnValue()) {
qWarning() << "VpnConnection::flushDns(): Failed to flush DNS";
}
});
#endif
}
void VpnConnection::disconnectSlots()
{
if (m_vpnProtocol) {
@@ -265,19 +222,15 @@ ErrorCode VpnConnection::lastError() const
void VpnConnection::connectToVpn(int serverIndex, const ServerCredentials &credentials, DockerContainer container,
const QJsonObject &vpnConfiguration)
{
qDebug() << QString("ConnectToVpn, Server index is %1, container is %2, route mode is")
qDebug() << QString("Trying to connect to VPN, server index is %1, container is %2, route mode is")
.arg(serverIndex)
.arg(ContainerProps::containerToString(container))
<< m_settings->routeMode();
m_remoteAddress = NetworkUtilities::getIPAddress(credentials.hostName);
emit connectionStateChanged(Vpn::ConnectionState::Connecting);
setConnectionState(Vpn::ConnectionState::Connecting);
m_pendingNetworkCheck = false;
m_vpnConfiguration = vpnConfiguration;
m_serverIndex = serverIndex;
m_serverCredentials = credentials;
m_dockerContainer = container;
#ifdef AMNEZIA_DESKTOP
if (m_vpnProtocol) {
@@ -293,7 +246,7 @@ void VpnConnection::connectToVpn(int serverIndex, const ServerCredentials &crede
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
m_vpnProtocol.reset(VpnProtocol::factory(container, m_vpnConfiguration));
if (!m_vpnProtocol) {
emit connectionStateChanged(Vpn::ConnectionState::Error);
setConnectionState(Vpn::ConnectionState::Error);
return;
}
m_vpnProtocol->prepare();
@@ -311,75 +264,23 @@ void VpnConnection::connectToVpn(int serverIndex, const ServerCredentials &crede
createProtocolConnections();
ErrorCode errorCode = m_vpnProtocol->start();
if (errorCode != ErrorCode::NoError)
emit connectionStateChanged(Vpn::ConnectionState::Error);
}
void VpnConnection::restartConnection()
{
// Only reconnect if VPN was connected before sleep/network change
if (!m_wasConnectedBeforeSleep) {
qDebug() << "VPN was not connected before sleep/network change, skipping reconnection";
return;
if (ErrorCode err = m_vpnProtocol->start(); err != ErrorCode::NoError) {
setConnectionState(Vpn::ConnectionState::Error);
emit vpnProtocolError(err);
}
qDebug() << "VPN was connected before sleep/network change, attempting reconnection";
this->disconnectFromVpn();
#ifdef Q_OS_LINUX
QThread::msleep(5000);
#endif
this->connectToVpn(m_serverIndex, m_serverCredentials, m_dockerContainer, m_vpnConfiguration);
// Reset the flag after reconnection attempt
m_wasConnectedBeforeSleep = false;
}
void VpnConnection::createProtocolConnections()
{
connect(m_vpnProtocol.data(), &VpnProtocol::protocolError, this, &VpnConnection::vpnProtocolError);
connect(m_vpnProtocol.data(), SIGNAL(connectionStateChanged(Vpn::ConnectionState)), this,
SLOT(onConnectionStateChanged(Vpn::ConnectionState)));
connect(m_vpnProtocol.data(), &VpnProtocol::connectionStateChanged, this, &VpnConnection::setConnectionState);
connect(m_vpnProtocol.data(), SIGNAL(bytesChanged(quint64, quint64)), this, SLOT(onBytesChanged(quint64, quint64)));
#ifdef AMNEZIA_DESKTOP
if (m_connectionLoseHandle)
disconnect(m_connectionLoseHandle);
if (m_networkChangeHandle)
disconnect(m_networkChangeHandle);
m_connectionLoseHandle = QMetaObject::Connection();
m_networkChangeHandle = QMetaObject::Connection();
// TODO: replace unsafe IpcClient::Interface() calls
m_connectionLoseHandle = connect(IpcClient::Interface().data(), &IpcInterfaceReplica::connectionLose,
this, [this]() {
qDebug() << "Connection Lose";
auto result = IpcClient::Interface()->stopNetworkCheck();
result.waitForFinished(3000);
// Track VPN state before connection loss
m_wasConnectedBeforeSleep = isConnected();
qDebug() << "VPN was connected before connection loss:" << m_wasConnectedBeforeSleep;
this->restartConnection();
});
m_networkChangeHandle = connect(IpcClient::Interface().data(), &IpcInterfaceReplica::networkChange,
this, [this]() {
qDebug() << "Network change";
// Track VPN state before network change (including sleep/wake)
m_wasConnectedBeforeSleep = isConnected();
qDebug() << "VPN was connected before network change:" << m_wasConnectedBeforeSleep;
this->restartConnection();
});
connect(m_vpnProtocol.data(), &VpnProtocol::tunnelAddressesUpdated,
this, [this](const QString& gateway, const QString& localAddress) {
Q_UNUSED(gateway)
Q_UNUSED(localAddress)
if (connectionState() != Vpn::ConnectionState::Connected) {
return;
}
if (startNetworkCheckIfReady()) {
m_pendingNetworkCheck = false;
}
});
IpcClient::withInterface([this](QSharedPointer<IpcInterfaceReplica> rep) {
connect(rep.data(), &IpcInterfaceReplica::networkChanged, this, &VpnConnection::reconnectToVpn, Qt::QueuedConnection);
connect(rep.data(), &IpcInterfaceReplica::wakeup, this, &VpnConnection::reconnectToVpn, Qt::QueuedConnection);
});
#endif
}
@@ -482,28 +383,13 @@ void VpnConnection::appendSplitTunnelingConfig()
m_vpnConfiguration.insert(config_key::appSplitTunnelType, appsRouteMode);
m_vpnConfiguration.insert(config_key::splitTunnelApps, appsJsonArray);
}
bool VpnConnection::startNetworkCheckIfReady()
{
#ifdef AMNEZIA_DESKTOP
if (!m_vpnProtocol || m_dockerContainer == DockerContainer::Ipsec) {
return false;
}
const QString gateway = m_vpnProtocol->vpnGateway();
const QString localAddress = m_vpnProtocol->vpnLocalAddress();
if (gateway.isEmpty() || localAddress.isEmpty()) {
return false;
}
return IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
QRemoteObjectPendingReply<bool> reply = iface->startNetworkCheck(gateway, localAddress);
return reply.waitForFinished() && reply.returnValue();
});
#else
return false;
#endif
qDebug() << QString("Site split tunneling is %1, route mode is %2")
.arg(m_settings->isSitesSplitTunnelingEnabled() ? "enabled" : "disabled")
.arg(routeMode);
qDebug() << QString("App split tunneling is %1, route mode is %2")
.arg(m_settings->isAppsSplitTunnelingEnabled() ? "enabled" : "disabled")
.arg(appsRouteMode);
}
#ifdef Q_OS_ANDROID
@@ -537,6 +423,27 @@ QString VpnConnection::bytesPerSecToText(quint64 bytes)
return QString("%1 %2").arg(QString::number(mbps, 'f', 2)).arg(tr("Mbps")); // Mbit/s
}
void VpnConnection::reconnectToVpn() {
if (m_vpnProtocol.isNull())
return;
if (m_connectionState != Vpn::ConnectionState::Connected) {
qWarning() << QString("Reconnect triggered on %1 during inappropriate state: %2; ignoring slot")
.arg(QMetaEnum::fromType<Vpn::ConnectionState>().valueToKey(m_connectionState));
return;
}
qDebug() << "Reconnect triggered. Reconnecting to the server";
setConnectionState(Vpn::ConnectionState::Reconnecting);
m_vpnProtocol->stop();
if (ErrorCode err = m_vpnProtocol->start(); err != ErrorCode::NoError) {
setConnectionState(Vpn::ConnectionState::Error);
emit vpnProtocolError(err);
}
}
void VpnConnection::disconnectFromVpn()
{
#if defined(Q_OS_IOS) || defined(MACOS_NE)
@@ -546,27 +453,11 @@ void VpnConnection::disconnectFromVpn()
#endif
if (m_vpnProtocol.isNull()) {
emit connectionStateChanged(Vpn::ConnectionState::Disconnected);
setConnectionState(Vpn::ConnectionState::Disconnected);
return;
}
m_vpnProtocol->stop();
#ifdef AMNEZIA_DESKTOP
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
QRemoteObjectPendingReply<bool> flushReply = iface->flushDns();
if (flushReply.waitForFinished(5000) && flushReply.returnValue())
qDebug() << "VpnConnection::disconnectFromVpn(): Successfully flushed DNS";
else
qWarning() << "VpnConnection::disconnectFromVpn(): Failed to flush DNS";
QRemoteObjectPendingReply<bool> clearSavedRoutesReply = iface->clearSavedRoutes();
if (clearSavedRoutesReply.waitForFinished(5000) && clearSavedRoutesReply.returnValue())
qDebug() << "VpnConnection::disconnectFromVpn(): Successfully cleared saved routes";
else
qWarning() << "VpnConnection::disconnectFromVpn(): Failed to clear saved routes";
});
#endif
setConnectionState(Vpn::ConnectionState::Disconnecting);
#ifdef Q_OS_ANDROID
auto *const connection = new QMetaObject::Connection;
@@ -578,9 +469,10 @@ void VpnConnection::disconnectFromVpn()
delete connection;
}
});
m_vpnProtocol->stop();
#endif
m_vpnProtocol->stop();
#if !defined(Q_OS_ANDROID) && !defined(AMNEZIA_DESKTOP)
m_vpnProtocol->deleteLater();
#endif
@@ -588,27 +480,12 @@ void VpnConnection::disconnectFromVpn()
m_vpnProtocol = nullptr;
}
Vpn::ConnectionState VpnConnection::connectionState()
{
if (!m_vpnProtocol)
return Vpn::ConnectionState::Disconnected;
return m_vpnProtocol->connectionState();
}
bool VpnConnection::isConnected() const
{
if (m_vpnProtocol.isNull()) {
return false;
}
return m_vpnProtocol->isConnected();
}
bool VpnConnection::isDisconnected() const
{
if (m_vpnProtocol.isNull()) {
return true;
}
return m_vpnProtocol->isDisconnected();
void VpnConnection::setConnectionState(Vpn::ConnectionState state) {
onConnectionStateChanged(state);
if (state == Vpn::Disconnected && m_connectionState == Vpn::Reconnecting)
return;
m_connectionState = state;
emit connectionStateChanged(state);
}
+6 -22
View File
@@ -34,10 +34,6 @@ public:
ErrorCode lastError() const;
bool isConnected() const;
bool isDisconnected() const;
Vpn::ConnectionState connectionState();
QSharedPointer<VpnProtocol> vpnProtocol() const;
const QString &remoteAddress() const;
@@ -48,15 +44,10 @@ public:
#endif
public slots:
void connectToVpn(int serverIndex,
const ServerCredentials &credentials, DockerContainer container, const QJsonObject &vpnConfiguration);
void connectToVpn(int serverIndex, const ServerCredentials &credentials, DockerContainer container, const QJsonObject &vpnConfiguration);
void reconnectToVpn();
void disconnectFromVpn();
void restartConnection();
void addRoutes(const QStringList &ips);
void deleteRoutes(const QStringList &ips);
void flushDns();
void onKillSwitchModeChanged(bool enabled);
void disconnectSlots();
@@ -71,10 +62,10 @@ protected slots:
void onBytesChanged(quint64 receivedBytes, quint64 sentBytes);
void onConnectionStateChanged(Vpn::ConnectionState state);
void setConnectionState(Vpn::ConnectionState state);
protected:
QSharedPointer<VpnProtocol> m_vpnProtocol;
QMetaObject::Connection m_connectionLoseHandle;
QMetaObject::Connection m_networkChangeHandle;
private:
std::shared_ptr<Settings> m_settings;
@@ -82,14 +73,6 @@ private:
QJsonObject m_routeMode;
QString m_remoteAddress;
ServerCredentials m_serverCredentials;
int m_serverIndex;
DockerContainer m_dockerContainer;
// Track VPN state before sleep for smart reconnection
bool m_wasConnectedBeforeSleep = false;
bool m_pendingNetworkCheck = false;
// Only for iOS for now, check counters
QTimer m_checkTimer;
@@ -100,11 +83,12 @@ private:
void createAndroidConnections();
#endif
Vpn::ConnectionState m_connectionState;
void createProtocolConnections();
void appendSplitTunnelingConfig();
void appendKillSwitchConfig();
bool startNetworkCheckIfReady();
};
#endif // VPNCONNECTION_H
+14 -6
View File
@@ -23,6 +23,7 @@ Options:
By default, the latest available platform is used
-m, --move Move the build result to the root of the build directory
-f, --fdroid Build for F-Droid
-p, --play Build AAB for Google Play
-h, --help Display this help
EOT
@@ -30,7 +31,7 @@ EOT
BUILD_TYPE="release"
opts=$(getopt -l debug,aab,apk:,build-platform:,move,fdroid,help -o "dua:b:mfh" -- "$@")
opts=$(getopt -l debug,aab,apk:,build-platform:,move,fdroid,play,help -o "dua:b:mfph" -- "$@")
eval set -- "$opts"
while true; do
case "$1" in
@@ -40,6 +41,7 @@ while true; do
-b | --build-platform) ANDROID_BUILD_PLATFORM=$2; shift 2;;
-m | --move) MOVE_RESULT=1; shift;;
-f | --fdroid) FDROID=1; shift;;
-p | --play) PLAY=1; shift;;
-h | --help) usage; exit 0;;
--) shift; break;;
esac
@@ -149,11 +151,17 @@ if [ -v FDROID ]; then
BUILD_TYPE="fdroid"
fi
if [ -v PLAY ]; then
AAB_FLAVOR="play"
else
AAB_FLAVOR="oss"
fi
if [ -v AAB ]; then
gradle_opts+=(bundle"${BUILD_TYPE^}")
gradle_opts+=(bundle"${AAB_FLAVOR^}${BUILD_TYPE^}")
fi
if [ -v ABIS ]; then
gradle_opts+=(assemble"${BUILD_TYPE^}")
gradle_opts+=(assembleOss"${BUILD_TYPE^}")
fi
$OUT_APP_DIR/android-build/gradlew \
@@ -164,7 +172,7 @@ $OUT_APP_DIR/android-build/gradlew \
if [[ -v CI || -v MOVE_RESULT ]]; then
echo "Moving APK/AAB..."
if [ -v AAB ]; then
mv -u $OUT_APP_DIR/android-build/build/outputs/bundle/$BUILD_TYPE/AmneziaVPN-$BUILD_TYPE.aab \
mv -u $OUT_APP_DIR/android-build/build/outputs/bundle/$AAB_FLAVOR"${BUILD_TYPE^}"/AmneziaVPN-$AAB_FLAVOR-$BUILD_TYPE.aab \
$PROJECT_DIR/deploy/build/
fi
@@ -181,8 +189,8 @@ if [[ -v CI || -v MOVE_RESULT ]]; then
IFS=';' read -r -a abi_array <<< "$ABIS"
for ABI in "${abi_array[@]}"
do
mv -u $OUT_APP_DIR/android-build/build/outputs/apk/$BUILD_TYPE/AmneziaVPN-$ABI-$suffix.apk \
mv -u $OUT_APP_DIR/android-build/build/outputs/apk/oss/$BUILD_TYPE/AmneziaVPN-oss-$ABI-$suffix.apk \
$PROJECT_DIR/deploy/build/
done
fi
fi
fi
+5 -2
View File
@@ -31,6 +31,7 @@ set SCRIPT_DIR=%PROJECT_DIR:"=%\deploy
set WORK_DIR=%SCRIPT_DIR:"=%\build_%BUILD_ARCH:"=%
set APP_NAME=AmneziaVPN
set APP_FILENAME=%APP_NAME:"=%.exe
set SERVICE_FILENAME=%APP_NAME:"=%-service.exe
set APP_DOMAIN=org.amneziavpn.package
set OUT_APP_DIR=%WORK_DIR:"=%\client\release
set PREBILT_DEPLOY_DATA_DIR=%PROJECT_DIR:"=%\client\3rd-prebuilt\deploy-prebuilt\windows\x%BUILD_ARCH:"=%
@@ -43,6 +44,7 @@ set STAGE_DIR=%WORK_DIR:"=%\stage
echo "Environment:"
echo "WORK_DIR: %WORK_DIR%"
echo "APP_FILENAME: %APP_FILENAME%"
echo "SERVICE_FILENAME: %SERVICE_FILENAME%"
echo "PROJECT_DIR: %PROJECT_DIR%"
echo "SCRIPT_DIR: %SCRIPT_DIR%"
echo "OUT_APP_DIR: %OUT_APP_DIR%"
@@ -74,7 +76,7 @@ if %errorlevel% neq 0 exit /b %errorlevel%
echo "Deploying..."
mkdir "%OUT_APP_DIR%"
copy "%WORK_DIR%\service\server\release\%APP_NAME%-service.exe" "%OUT_APP_DIR%"
copy "%WORK_DIR%\service\server\release\%SERVICE_FILENAME%" "%OUT_APP_DIR%"
rem copy "%WORK_DIR%\client\%APP_FILENAME%" "%OUT_APP_DIR%"
copy /Y "%PROJECT_DIR%\client\images\app.ico" "%OUT_APP_DIR%\AmneziaVPN.ico" >nul
@@ -83,7 +85,8 @@ echo "Signing exe"
cd %OUT_APP_DIR%
signtool sign /v /n "Privacy Technologies OU" /fd sha256 /tr http://timestamp.comodoca.com/?td=sha256 /td sha256 *.exe
"%QT_BIN_DIR:"=%\windeployqt" --release --qmldir "%PROJECT_DIR:"=%\client" --force --no-translations "%OUT_APP_DIR:"=%\%APP_FILENAME:"=%"
"%QT_BIN_DIR:"=%\windeployqt" --release --qmldir "%PROJECT_DIR:"=%\client" --force --no-translations --force-openssl "%OUT_APP_DIR:"=%\%APP_FILENAME:"=%"
"%QT_BIN_DIR:"=%\windeployqt" --release "%OUT_APP_DIR:"=%\%SERVICE_FILENAME:"=%"
signtool sign /v /n "Privacy Technologies OU" /fd sha256 /tr http://timestamp.comodoca.com/?td=sha256 /td sha256 *.dll
+2
View File
@@ -10,6 +10,8 @@
</array>
<key>KeepAlive</key>
<true/>
<key>RunAtLoad</key>
<true/>
<key>Sockets</key>
<dict>
<key>Listeners</key>
+38 -26
View File
@@ -7,42 +7,54 @@ LOG_FOLDER=/var/log/$APP_NAME
LOG_FILE="$LOG_FOLDER/post-install.log"
APP_PATH=/Applications/$APP_NAME.app
rm -rf "$LOG_FOLDER"
mkdir -p "$LOG_FOLDER"
echo "`date` Script started" > "$LOG_FILE"
log() {
echo "`date` $*" >> "$LOG_FILE"
}
run_cmd() {
log "CMD: $*"
"$@" >> "$LOG_FILE" 2>&1
local ec=$?
log "EXIT: $ec"
return $ec
}
# Handle new installations unpacked into localized folder
if [ -d "/Applications/${APP_NAME}.localized" ]; then
echo "`date` Detected ${APP_NAME}.localized, migrating to standard path" >> $LOG_FILE
sudo rm -rf "$APP_PATH"
sudo mv "/Applications/${APP_NAME}.localized/${APP_NAME}.app" "$APP_PATH"
sudo rm -rf "/Applications/${APP_NAME}.localized"
log "Detected ${APP_NAME}.localized, migrating to standard path"
run_cmd sudo rm -rf "$APP_PATH"
run_cmd sudo mv "/Applications/${APP_NAME}.localized/${APP_NAME}.app" "$APP_PATH"
run_cmd sudo rm -rf "/Applications/${APP_NAME}.localized"
fi
if launchctl list "$APP_NAME-service" &> /dev/null; then
launchctl unload "$LAUNCH_DAEMONS_PLIST_NAME"
rm -f "$LAUNCH_DAEMONS_PLIST_NAME"
fi
run_cmd launchctl bootout system "$LAUNCH_DAEMONS_PLIST_NAME" || run_cmd launchctl unload "$LAUNCH_DAEMONS_PLIST_NAME"
run_cmd rm -f "$LAUNCH_DAEMONS_PLIST_NAME"
sudo chmod -R a-w "$APP_PATH/"
sudo chown -R root "$APP_PATH/"
sudo chgrp -R wheel "$APP_PATH/"
run_cmd sudo chmod -R a-w "$APP_PATH/"
run_cmd sudo chown -R root "$APP_PATH/"
run_cmd sudo chgrp -R wheel "$APP_PATH/"
rm -rf $LOG_FOLDER
mkdir -p $LOG_FOLDER
echo "`date` Script started" > $LOG_FILE
echo "Requesting ${APP_NAME} to quit gracefully" >> "$LOG_FILE"
osascript -e 'tell application "AmneziaVPN" to quit'
log "Requesting ${APP_NAME} to quit gracefully"
run_cmd osascript -e 'tell application "AmneziaVPN" to quit' || true
PLIST_SOURCE="$APP_PATH/Contents/Resources/$PLIST_NAME"
if [ -f "$PLIST_SOURCE" ]; then
mv -f "$PLIST_SOURCE" "$LAUNCH_DAEMONS_PLIST_NAME" 2>> $LOG_FILE
run_cmd mv -f "$PLIST_SOURCE" "$LAUNCH_DAEMONS_PLIST_NAME"
else
echo "`date` ERROR: service plist not found at $PLIST_SOURCE" >> $LOG_FILE
log "ERROR: service plist not found at $PLIST_SOURCE"
fi
chown root:wheel "$LAUNCH_DAEMONS_PLIST_NAME"
launchctl load "$LAUNCH_DAEMONS_PLIST_NAME"
echo "`date` Launching ${APP_NAME} application" >> $LOG_FILE
open -a "$APP_PATH" 2>> $LOG_FILE || true
run_cmd chown root:wheel "$LAUNCH_DAEMONS_PLIST_NAME"
run_cmd chmod 644 "$LAUNCH_DAEMONS_PLIST_NAME"
run_cmd launchctl bootstrap system "$LAUNCH_DAEMONS_PLIST_NAME" || run_cmd launchctl load "$LAUNCH_DAEMONS_PLIST_NAME"
run_cmd launchctl enable "system/$APP_NAME-service" || true
run_cmd launchctl kickstart -k "system/$APP_NAME-service" || true
run_cmd launchctl print "system/$APP_NAME-service" || true
log "Launching ${APP_NAME} application"
run_cmd open -a "$APP_PATH" || true
echo "`date` Service status: $?" >> $LOG_FILE
echo "`date` Script finished" >> $LOG_FILE
log "Script finished"
+1 -1
View File
@@ -29,7 +29,7 @@ fi
# Unload the service if loaded and remove its plist file regardless
if launchctl list "${APP_NAME}-service" &> /dev/null; then
sudo launchctl unload "$LAUNCH_DAEMONS_PLIST_NAME"
sudo launchctl bootout system "$LAUNCH_DAEMONS_PLIST_NAME" || sudo launchctl unload "$LAUNCH_DAEMONS_PLIST_NAME"
fi
sudo rm -f "$LAUNCH_DAEMONS_PLIST_NAME"
@@ -14,6 +14,8 @@ sc stop AmneziaVPN-service
sc delete AmneziaVPN-service
sc stop AmneziaWGTunnel$AmneziaVPN
sc delete AmneziaWGTunnel$AmneziaVPN
sc stop AmneziaVPNSplitTunnel
sc delete AmneziaVPNSplitTunnel
taskkill /IM "AmneziaVPN-service.exe" /F
taskkill /IM "AmneziaVPN.exe" /F
@@ -14,6 +14,8 @@ sc stop AmneziaVPN-service
sc delete AmneziaVPN-service
sc stop AmneziaWGTunnel$AmneziaVPN
sc delete AmneziaWGTunnel$AmneziaVPN
sc stop AmneziaVPNSplitTunnel
sc delete AmneziaVPNSplitTunnel
taskkill /IM "AmneziaVPN-service.exe" /F
taskkill /IM "AmneziaVPN.exe" /F
+29 -15
View File
@@ -1,9 +1,9 @@
#!/bin/sh
#!/bin/bash
set -e
VERSION=$1
if [[ $VERSION = '' ]]; then
if [[ -z "$VERSION" ]]; then
echo '::error::VERSION does not set. Exiting with error...'
exit 1
fi
@@ -14,25 +14,39 @@ cd dist
echo $VERSION >> VERSION
curl -s https://api.github.com/repos/amnezia-vpn/amnezia-client/releases/tags/$VERSION | jq -r .body | tr -d '\r' > CHANGELOG
curl -s https://api.github.com/repos/amnezia-vpn/amnezia-client/releases/tags/$VERSION | jq -r .published_at > RELEASE_DATE
if [[ $(cat CHANGELOG) = null ]]; then
echo '::error::Release does not exists. Exiting with error...'
exit 1
fi
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android8+_arm64-v8a.apk
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android8+_armeabi-v7a.apk
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android8+_x86.apk
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android8+_x86_64.apk
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android_7_arm64-v8a.apk
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android_7_armeabi-v7a.apk
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android_7_x86.apk
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android_7_x86_64.apk
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_linux_x64.tar.zip
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_macos.dmg
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_macos_old.dmg
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_windows_x64.exe
# Download files with error handling
download_file() {
local url=$1
local filename=$(basename "$url")
echo "Downloading $filename..."
if ! wget -q "$url"; then
echo "::error::Failed to download $filename from $url"
exit 8
fi
echo "Successfully downloaded $filename"
}
download_file https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android9+_arm64-v8a.apk
download_file https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android9+_armeabi-v7a.apk
download_file https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android9+_x86.apk
download_file https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android9+_x86_64.apk
download_file https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_linux_x64.tar
download_file https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_macos.pkg
download_file https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_x64.exe
cd ../
rclone sync ./dist/ r2:/updates/
echo "Syncing to R2..."
if ! rclone sync ./dist/ r2:/updates/; then
echo "::error::Failed to sync files to R2"
exit 8
fi
echo "Deployment completed successfully!"
+57 -9
View File
@@ -11,6 +11,7 @@
namespace amnezia {
enum PermittedProcess {
Invalid,
OpenVPN,
Wireguard,
Tun2Socks,
@@ -19,16 +20,18 @@ enum PermittedProcess {
inline QString permittedProcessPath(PermittedProcess pid)
{
if (pid == PermittedProcess::OpenVPN) {
return Utils::openVpnExecPath();
} else if (pid == PermittedProcess::Wireguard) {
return Utils::wireguardExecPath();
} else if (pid == PermittedProcess::CertUtil) {
return Utils::certUtilPath();
} else if (pid == PermittedProcess::Tun2Socks) {
return Utils::tun2socksPath();
switch (pid) {
case PermittedProcess::OpenVPN:
return Utils::openVpnExecPath();
case PermittedProcess::Wireguard:
return Utils::wireguardExecPath();
case PermittedProcess::CertUtil:
return Utils::certUtilPath();
case PermittedProcess::Tun2Socks:
return Utils::tun2socksPath();
default:
return "";
}
return "";
}
@@ -48,6 +51,51 @@ inline QString getIpcProcessUrl(int pid) {
#endif
}
inline QStringList sanitizeArguments(PermittedProcess proc, const QStringList &args) {
using Validator = std::function<bool(const QString&)>;
QMap<QString, Validator> namedArgs;
QList<Validator> positionalArgs;
switch (proc) {
case Tun2Socks:
namedArgs["-device"] = [](const QString& v) { return v.startsWith("tun://"); };
namedArgs["-proxy"] = [](const QString& v) { return v.startsWith("socks5://"); };
break;
default:
//FIXME
return args;
}
QStringList sanitized;
for (int i = 0, pos = 0; i < args.size(); i++) {
const auto& key = args[i];
if (const auto found = namedArgs.find(key); found != namedArgs.end()) {
const auto validator = found.value();
if (validator) {
if (i + 1 < args.size()) {
const auto& value = args[i+1];
if (validator(value)) {
sanitized << key << value;
i++;
}
}
} else {
sanitized << key;
}
} else if (pos < positionalArgs.size()) {
if (const auto validator = positionalArgs[pos]; validator && validator(key)) {
sanitized << key;
pos++;
}
}
}
return sanitized;
}
} // namespace amnezia
+4 -3
View File
@@ -38,12 +38,13 @@ class IpcInterface
SLOT( bool updateResolvers(const QString& ifname, const QList<QHostAddress>& resolvers) );
SLOT( bool restoreResolvers() );
SLOT(void xrayStart(const QString &config));
SLOT(void xrayStop());
SLOT(bool xrayStart(const QString &config));
SLOT(bool xrayStop());
SLOT( bool startNetworkCheck(const QString& serverIpv4Gateway, const QString& deviceIpv4Address) );
SLOT( bool stopNetworkCheck() );
SIGNAL( connectionLose() );
SIGNAL( networkChange() );
SIGNAL( wakeup() );
SIGNAL( networkChanged() );
};
+7
View File
@@ -4,6 +4,8 @@
class IpcProcessInterface
{
SLOT( start() );
SLOT( terminate() );
SLOT( kill() );
SLOT( close() );
SLOT( setArguments(const QStringList &arguments) );
@@ -17,6 +19,11 @@ class IpcProcessInterface
SLOT( QByteArray readAllStandardError() );
SLOT( QByteArray readAllStandardOutput() );
SLOT( bool waitForFinished() );
SLOT( bool waitForFinished(int msecs) );
SLOT( bool waitForStarted() );
SLOT( bool waitForStarted(int msecs) );
SIGNAL( errorOccurred(QProcess::ProcessError error) );
SIGNAL( finished(int exitCode, QProcess::ExitStatus exitStatus) );

Some files were not shown because too many files have changed in this diff Show More