mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-07-18 14:49:50 +03:00
Compare commits
34 Commits
dev
...
feature/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e79186908 | ||
|
|
eb9dfdaa11 | ||
|
|
11f6fed98a | ||
|
|
7b3f7e852b | ||
|
|
2b4d9bfbe5 | ||
|
|
f8c4db2f08 | ||
|
|
83ab95db7a | ||
|
|
f42992912e | ||
|
|
f802efb2f4 | ||
|
|
88018944ba | ||
|
|
1340679621 | ||
|
|
4e900d4bab | ||
|
|
c4656ad2e9 | ||
|
|
24d1ed0333 | ||
|
|
5b0afa3686 | ||
|
|
4fd6d5f5cd | ||
|
|
25bae06cbe | ||
|
|
8132a29199 | ||
|
|
07a11679f5 | ||
|
|
ae7edda889 | ||
|
|
62c6723662 | ||
|
|
3d7e9e4333 | ||
|
|
d57d9ab6a9 | ||
|
|
b52676a20b | ||
|
|
199fddcd5b | ||
|
|
854e95c173 | ||
|
|
c0e13f5959 | ||
|
|
6a49557852 | ||
|
|
4fe8923e0d | ||
|
|
1c0a8a7741 | ||
|
|
27924e996a | ||
|
|
4f1039926a | ||
|
|
43b85e0e95 | ||
|
|
961782c426 |
72
.github/workflows/deploy.yml
vendored
72
.github/workflows/deploy.yml
vendored
@@ -414,10 +414,6 @@ jobs:
|
||||
matrix:
|
||||
xcode-version: [16.2, 16.4, 26.4]
|
||||
include:
|
||||
- xcode-version: 16.2
|
||||
os: macos-15
|
||||
- xcode-version: 16.4
|
||||
os: macos-15
|
||||
- xcode-version: 26.4
|
||||
os: macos-26
|
||||
|
||||
@@ -454,7 +450,7 @@ jobs:
|
||||
# ------------------------------------------------------
|
||||
|
||||
Build-MacOS:
|
||||
runs-on: macos-15
|
||||
runs-on: macos-latest
|
||||
|
||||
needs: Bake-Prebuilts-MacOS
|
||||
if: ${{ always() }}
|
||||
@@ -501,7 +497,7 @@ jobs:
|
||||
- name: 'Setup xcode'
|
||||
uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: '26.3.0'
|
||||
xcode-version: '16.2.0'
|
||||
|
||||
- name: 'Install Qt'
|
||||
uses: jurplel/install-qt-action@v4
|
||||
@@ -554,10 +550,6 @@ jobs:
|
||||
matrix:
|
||||
xcode-version: [16.2, 16.4, 26.4]
|
||||
include:
|
||||
- xcode-version: 16.2
|
||||
os: macos-15
|
||||
- xcode-version: 16.4
|
||||
os: macos-15
|
||||
- xcode-version: 26.4
|
||||
os: macos-26
|
||||
|
||||
@@ -594,7 +586,7 @@ jobs:
|
||||
# ------------------------------------------------------
|
||||
|
||||
Build-MacOS-NE:
|
||||
runs-on: macos-26
|
||||
runs-on: macos-latest
|
||||
|
||||
needs: Bake-Prebuilts-MacOS-NE
|
||||
if: ${{ always() }}
|
||||
@@ -618,12 +610,6 @@ jobs:
|
||||
PREM_V1_ENDPOINT: ${{ secrets.PREM_V1_ENDPOINT }}
|
||||
|
||||
steps:
|
||||
- name: 'Get sources'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: 'true'
|
||||
fetch-depth: 10
|
||||
|
||||
- uses: ./.github/actions/apple-setup-provisioning-profile
|
||||
with:
|
||||
provisioning_profile_base64: ${{ secrets.MAC_APP_PROVISIONING_PROFILE }}
|
||||
@@ -661,6 +647,12 @@ jobs:
|
||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||
gomobile init
|
||||
|
||||
- name: 'Get sources'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: 'true'
|
||||
fetch-depth: 10
|
||||
|
||||
- name: 'Setup python'
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
@@ -862,13 +854,41 @@ jobs:
|
||||
VERSION=$(grep CMAKE_PROJECT_VERSION:STATIC deploy/build/CMakeCache.txt | cut -d= -f2)
|
||||
|
||||
(cd deploy/build/client/android-build && mv AmneziaVPN.apk AmneziaVPN_${VERSION}_android9+_universal.apk)
|
||||
(cd deploy/build/client/android-build/build/outputs/bundle/release && mv android-build-release.aab AmneziaVPN_${VERSION}.aab)
|
||||
(cd deploy/build/client/android-build/build/outputs/bundle/release && mv android-build-release.aab AmneziaVPN_${VERSION}_oss.aab)
|
||||
|
||||
for abi in arm64-v8a armeabi-v7a x86 x86_64; do
|
||||
deploy/build.sh -t android --sign --abi ${abi} --build ./deploy/build/${abi}
|
||||
(cd deploy/build/${abi}/client/android-build && mv AmneziaVPN.apk AmneziaVPN_${VERSION}_android9+_${abi}.apk)
|
||||
done
|
||||
|
||||
- name: 'Build Play AAB'
|
||||
env:
|
||||
QT_INSTALL_DIR: ${{ runner.temp }}
|
||||
QT_ANDROID_KEYSTORE_PATH: ${{ github.workspace }}/android.keystore
|
||||
QT_ANDROID_KEYSTORE_ALIAS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_ALIAS }}
|
||||
QT_ANDROID_KEYSTORE_STORE_PASS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_PASS }}
|
||||
shell: bash
|
||||
run: |
|
||||
VERSION=$(grep CMAKE_PROJECT_VERSION:STATIC deploy/build/CMakeCache.txt | cut -d= -f2)
|
||||
|
||||
deploy/build.sh -t android --sign --aab --play --build ./deploy/build/play
|
||||
|
||||
(cd deploy/build/play/client/android-build/build/outputs/bundle/playRelease && mv *.aab AmneziaVPN_${VERSION}_play.aab)
|
||||
|
||||
- name: 'Build Play APK'
|
||||
env:
|
||||
QT_INSTALL_DIR: ${{ runner.temp }}
|
||||
QT_ANDROID_KEYSTORE_PATH: ${{ github.workspace }}/android.keystore
|
||||
QT_ANDROID_KEYSTORE_ALIAS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_ALIAS }}
|
||||
QT_ANDROID_KEYSTORE_STORE_PASS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_PASS }}
|
||||
shell: bash
|
||||
run: |
|
||||
VERSION=$(grep CMAKE_PROJECT_VERSION:STATIC deploy/build/CMakeCache.txt | cut -d= -f2)
|
||||
|
||||
deploy/build.sh -t android --sign --apk --play --abi arm64-v8a --build ./deploy/build/play-apk
|
||||
|
||||
(cd deploy/build/play-apk/client/android-build/build/outputs/apk/play/release && mv *.apk AmneziaVPN_${VERSION}_play.apk)
|
||||
|
||||
- name: 'Upload universal APK'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
@@ -876,13 +896,27 @@ jobs:
|
||||
archive: false
|
||||
retention-days: 7
|
||||
|
||||
- name: 'Upload AAB'
|
||||
- name: 'Upload OSS AAB'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
path: deploy/build/client/android-build/build/outputs/bundle/release/*.aab
|
||||
archive: false
|
||||
retention-days: 7
|
||||
|
||||
- name: 'Upload Play AAB'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
path: deploy/build/play/client/android-build/build/outputs/bundle/playRelease/*.aab
|
||||
archive: false
|
||||
retention-days: 7
|
||||
|
||||
- name: 'Upload Play APK'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
path: deploy/build/play-apk/client/android-build/build/outputs/apk/play/release/*.apk
|
||||
archive: false
|
||||
retention-days: 7
|
||||
|
||||
- name: 'Upload arm64-v8a APK'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -7,6 +7,9 @@
|
||||
[submodule "client/3rd/amneziawg-apple"]
|
||||
path = client/3rd/amneziawg-apple
|
||||
url = https://github.com/amnezia-vpn/amneziawg-apple
|
||||
[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
|
||||
|
||||
@@ -4,7 +4,7 @@ set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
set(PROJECT AmneziaVPN)
|
||||
set(AMNEZIAVPN_VERSION 4.9.0.4)
|
||||
set(AMNEZIAVPN_VERSION 4.8.16.3)
|
||||
|
||||
set(QT_CREATOR_SKIP_PACKAGE_MANAGER_SETUP ON CACHE BOOL "" FORCE)
|
||||
set(CMAKE_PROJECT_TOP_LEVEL_INCLUDES
|
||||
@@ -28,7 +28,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
|
||||
set(RELEASE_DATE "${CURRENT_DATE}")
|
||||
|
||||
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
|
||||
set(APP_ANDROID_VERSION_CODE 2133)
|
||||
set(APP_ANDROID_VERSION_CODE 2134)
|
||||
|
||||
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
||||
set(MZ_PLATFORM_NAME "linux")
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
- Простой в использовании — введите IP-адрес, SSH-логин и пароль, и Amnezia автоматически установит VPN-контейнеры Docker на ваш сервер и подключится к VPN.
|
||||
- Классические VPN-протоколы: OpenVPN, WireGuard и IKEv2.
|
||||
- Протоколы с маскировкой трафика (обфускацией): OpenVPN с плагином [Cloak](https://github.com/cbeuw/Cloak), Shadowsocks (OpenVPN over Shadowsocks), [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) и XRay.
|
||||
- Протоколы с маскировкой трафика (обфускацией): OpenVPN с плагином [Cloak](https://github.com/cbeuw/Cloak), Shadowsocks (OpenVPN over Shadowsocks), [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) and XRay.
|
||||
- Поддержка Split Tunneling — добавляйте любые сайты или приложения в список, чтобы включить VPN только для них.
|
||||
- Поддерживает платформы: Windows, macOS, Linux, Android, iOS.
|
||||
- Поддержка конфигурации протокола AmneziaWG на [бета-прошивке Keenetic](https://docs.keenetic.com/ua/air/kn-1611/en/6319-latest-development-release.html#UUID-186c4108-5afd-c10b-f38a-cdff6c17fab3_section-idm33192196168192-improved).
|
||||
|
||||
1
client/3rd/QSimpleCrypto
vendored
Submodule
1
client/3rd/QSimpleCrypto
vendored
Submodule
Submodule client/3rd/QSimpleCrypto added at c99b33f0e0
@@ -103,7 +103,6 @@ include(${CMAKE_CURRENT_LIST_DIR}/cmake/sources.cmake)
|
||||
include_directories(
|
||||
${CMAKE_CURRENT_LIST_DIR}/../ipc
|
||||
${CMAKE_CURRENT_LIST_DIR}/../common/logger
|
||||
${CMAKE_CURRENT_LIST_DIR}/../common/crypto
|
||||
${CMAKE_CURRENT_LIST_DIR}
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
)
|
||||
@@ -258,10 +257,6 @@ install(SCRIPT ${QT_DEPLOY_SCRIPT}
|
||||
|
||||
if (APPLE AND NOT IOS AND NOT MACOS_NE)
|
||||
list(APPEND OVPN_SCRIPTS "${CMAKE_SOURCE_DIR}/deploy/data/macos/update-resolv-conf.sh")
|
||||
set_target_properties(${PROJECT} PROPERTIES
|
||||
XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "NO"
|
||||
XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED "NO"
|
||||
)
|
||||
endif()
|
||||
if (LINUX AND NOT ANDROID)
|
||||
list(APPEND OVPN_SCRIPTS "${CMAKE_SOURCE_DIR}/deploy/data/linux/update-resolv-conf.sh")
|
||||
|
||||
@@ -26,6 +26,10 @@
|
||||
#include "version.h"
|
||||
|
||||
#include "platforms/ios/QRCodeReaderBase.h"
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include "platforms/android/android_controller.h"
|
||||
#endif
|
||||
|
||||
|
||||
bool AmneziaApplication::m_forceQuit = false;
|
||||
@@ -139,6 +143,12 @@ void AmneziaApplication::init()
|
||||
m_engine->rootContext()->setContextProperty("IsMacOsNeBuild", false);
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
m_engine->rootContext()->setContextProperty("IsPlayBuild", AndroidController::instance()->isPlay());
|
||||
#else
|
||||
m_engine->rootContext()->setContextProperty("IsPlayBuild", false);
|
||||
#endif
|
||||
|
||||
m_vpnConnection.reset(new VpnConnection(nullptr, nullptr));
|
||||
m_vpnConnection->moveToThread(&m_vpnConnectionThread);
|
||||
m_vpnConnectionThread.start();
|
||||
|
||||
19
client/android/billing/build.gradle.kts
Normal file
19
client/android/billing/build.gradle.kts
Normal 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)
|
||||
}
|
||||
65
client/android/billing/src/main/kotlin/BillingException.kt
Normal file
65
client/android/billing/src/main/kotlin/BillingException.kt
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
321
client/android/billing/src/main/kotlin/BillingProvider.kt
Normal file
321
client/android/billing/src/main/kotlin/BillingProvider.kt
Normal file
@@ -0,0 +1,321 @@
|
||||
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 ->
|
||||
Log.v(TAG, "processPurchases: purchaseToken=${purchase.purchaseToken} orderId=${purchase.orderId} state=${purchase.purchaseState}")
|
||||
/* 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
|
||||
@@ -20,6 +20,7 @@ android {
|
||||
namespace = "org.amnezia.vpn"
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
@@ -33,13 +34,56 @@ android {
|
||||
jniLibs.useLegacyPackaging = true
|
||||
}
|
||||
|
||||
val abiList = qtTargetAbiList.split(",")
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "org.amnezia.vpn"
|
||||
targetSdk = qtTargetSdkVersion.toInt()
|
||||
|
||||
// keeps language resources for only the locales specified below
|
||||
resourceConfigurations += listOf("en", "ru", "b+zh+Hans")
|
||||
ndk.abiFilters += qtTargetAbiList.split(",")
|
||||
// ndk.abiFilters is only used for single-ABI builds; multi-ABI uses splits below
|
||||
if (abiList.size == 1) {
|
||||
ndk.abiFilters += abiList
|
||||
}
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
register("release") {
|
||||
storeFile = providers.environmentVariable("QT_ANDROID_KEYSTORE_PATH").orNull?.let { file(it) }
|
||||
storePassword = providers.environmentVariable("QT_ANDROID_KEYSTORE_STORE_PASS").orNull
|
||||
keyAlias = providers.environmentVariable("QT_ANDROID_KEYSTORE_ALIAS").orNull
|
||||
keyPassword = providers.environmentVariable("QT_ANDROID_KEYSTORE_STORE_PASS").orNull
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// exclude coroutine debug resource from release build
|
||||
packaging {
|
||||
resources.excludes += "DebugProbesKt.bin"
|
||||
}
|
||||
signingConfig = signingConfigs["release"]
|
||||
}
|
||||
|
||||
create("fdroid") {
|
||||
initWith(getByName("release"))
|
||||
signingConfig = null
|
||||
matchingFallbacks += "release"
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions += "billing"
|
||||
|
||||
productFlavors {
|
||||
create("oss") {
|
||||
dimension = "billing"
|
||||
buildConfigField("boolean", "IS_PLAY_BUILD", "false")
|
||||
}
|
||||
create("play") {
|
||||
dimension = "billing"
|
||||
buildConfigField("boolean", "IS_PLAY_BUILD", "true")
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
@@ -51,13 +95,74 @@ android {
|
||||
assets.setSrcDirs(listOf("assets"))
|
||||
jniLibs.setSrcDirs(listOf("libs"))
|
||||
}
|
||||
|
||||
getByName("oss") {
|
||||
java.setSrcDirs(listOf("oss"))
|
||||
}
|
||||
|
||||
getByName("play") {
|
||||
java.setSrcDirs(listOf("play"))
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// exclude coroutine debug resource from release build
|
||||
packaging {
|
||||
resources.excludes += "DebugProbesKt.bin"
|
||||
splits {
|
||||
abi {
|
||||
// splits only make sense for multi-ABI builds; single-ABI uses ndk.abiFilters
|
||||
isEnable = abiList.size > 1
|
||||
reset()
|
||||
include(*abiList.toTypedArray())
|
||||
isUniversalApk = false
|
||||
}
|
||||
}
|
||||
|
||||
// fix for Qt Creator to allow deploying the application to a device
|
||||
// to enable this fix, add the line outputBaseName=android-build to local.properties
|
||||
if (outputBaseName.isNotEmpty()) {
|
||||
applicationVariants.all {
|
||||
outputs.map { it as BaseVariantOutputImpl }
|
||||
.forEach { output ->
|
||||
if (output.outputFileName.endsWith(".apk")) {
|
||||
output.outputFileName = "$outputBaseName-${buildType.name}.apk"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// androiddeployqt expects:
|
||||
// APK: build/outputs/apk/{base}-{buildType}[-unsigned].apk (no flavor subdir)
|
||||
// AAB: build/outputs/bundle/{buildType}/{base}-{buildType}.aab (no flavor subdir)
|
||||
// where {base} = outputBaseName (set by Qt Creator) or "android-build" (CI fallback).
|
||||
// Release APK gets -unsigned suffix (Qt cmake signs it); debug does not.
|
||||
// Copy only oss flavor to the flat output dir that androiddeployqt/Qt Creator expect.
|
||||
// Play flavor is built via android_play_apk/android_play_aab cmake targets and uses
|
||||
// its native Gradle output paths directly.
|
||||
applicationVariants.all {
|
||||
val flavorName = productFlavors.firstOrNull()?.name ?: ""
|
||||
val buildTypeName = buildType.name
|
||||
if (flavorName == "oss") {
|
||||
val base = outputBaseName.ifEmpty { "android-build" }
|
||||
val unsignedSuffix = if (buildTypeName == "release") "-unsigned" else ""
|
||||
|
||||
packageApplicationProvider.configure {
|
||||
doLast {
|
||||
val srcDir = layout.buildDirectory.dir("outputs/apk/oss/$buildTypeName").get().asFile
|
||||
val dstDir = layout.buildDirectory.dir("outputs/apk").get().asFile
|
||||
dstDir.mkdirs()
|
||||
srcDir.listFiles()?.filter { it.name.endsWith(".apk") }?.forEach { apk ->
|
||||
apk.copyTo(File(dstDir, "$base-$buildTypeName$unsignedSuffix.apk"), overwrite = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("bundle${name.replaceFirstChar { it.uppercase() }}") {
|
||||
doLast {
|
||||
val srcDir = layout.buildDirectory.dir("outputs/bundle/ossRelease").get().asFile
|
||||
val dstDir = layout.buildDirectory.dir("outputs/bundle/$buildTypeName").get().asFile
|
||||
dstDir.mkdirs()
|
||||
srcDir.listFiles()?.filter { it.name.endsWith(".aab") }?.forEach { aab ->
|
||||
aab.copyTo(File(dstDir, "$base-$buildTypeName.aab"), overwrite = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,4 +189,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)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[versions]
|
||||
agp = "8.6.1"
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ rootProject.buildFileName = "build.gradle.kts"
|
||||
|
||||
include(":qt")
|
||||
include(":utils")
|
||||
include(":billing")
|
||||
include(":protocolApi")
|
||||
include(":wireguard")
|
||||
include(":awg")
|
||||
|
||||
@@ -56,7 +56,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
|
||||
@@ -90,6 +89,7 @@ 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>()
|
||||
@@ -206,6 +206,7 @@ class AmneziaActivity : QtActivity() {
|
||||
registerBroadcastReceivers()
|
||||
intent?.let(::processIntent)
|
||||
runBlocking { vpnProto = proto.await() }
|
||||
billingRepository = BillingPaymentRepository(applicationContext)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
@@ -999,15 +1000,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")
|
||||
@@ -1176,11 +1171,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)
|
||||
|
||||
12
client/android/src/org/amnezia/vpn/BillingRepository.kt
Normal file
12
client/android/src/org/amnezia/vpn/BillingRepository.kt
Normal file
@@ -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
|
||||
}
|
||||
14
client/android/utils/src/main/kotlin/ErrorCode.kt
Normal file
14
client/android/utils/src/main/kotlin/ErrorCode.kt
Normal file
@@ -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
|
||||
}
|
||||
@@ -4,6 +4,7 @@ set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/Modules;${CMAKE_MODULE_PATH}")
|
||||
|
||||
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/SortFilterProxyModel ${CMAKE_BINARY_DIR}/3rd/SortFilterProxyModel)
|
||||
set(LIBS ${LIBS} SortFilterProxyModel)
|
||||
include(${CLIENT_ROOT_DIR}/cmake/QSimpleCrypto.cmake)
|
||||
|
||||
include(${CLIENT_ROOT_DIR}/3rd/qrcodegen/qrcodegen.cmake)
|
||||
|
||||
@@ -39,6 +40,7 @@ endif()
|
||||
set(LIBS ${LIBS} qt6keychain)
|
||||
|
||||
include_directories(
|
||||
${CLIENT_ROOT_DIR}/3rd/QSimpleCrypto/src/include
|
||||
${CLIENT_ROOT_DIR}/3rd/qtkeychain/qtkeychain
|
||||
${CMAKE_CURRENT_BINARY_DIR}/3rd/qtkeychain
|
||||
)
|
||||
|
||||
21
client/cmake/QSimpleCrypto.cmake
Normal file
21
client/cmake/QSimpleCrypto.cmake
Normal file
@@ -0,0 +1,21 @@
|
||||
set(CLIENT_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR}/..)
|
||||
set(QSIMPLECRYPTO_DIR ${CLIENT_ROOT_DIR}/3rd/QSimpleCrypto/src)
|
||||
|
||||
include_directories(${QSIMPLECRYPTO_DIR})
|
||||
|
||||
set(HEADERS ${HEADERS}
|
||||
${QSIMPLECRYPTO_DIR}/include/QAead.h
|
||||
${QSIMPLECRYPTO_DIR}/include/QBlockCipher.h
|
||||
${QSIMPLECRYPTO_DIR}/include/QRsa.h
|
||||
${QSIMPLECRYPTO_DIR}/include/QSimpleCrypto_global.h
|
||||
${QSIMPLECRYPTO_DIR}/include/QX509.h
|
||||
${QSIMPLECRYPTO_DIR}/include/QX509Store.h
|
||||
)
|
||||
|
||||
set(SOURCES ${SOURCES}
|
||||
${QSIMPLECRYPTO_DIR}/sources/QAead.cpp
|
||||
${QSIMPLECRYPTO_DIR}/sources/QBlockCipher.cpp
|
||||
${QSIMPLECRYPTO_DIR}/sources/QRsa.cpp
|
||||
${QSIMPLECRYPTO_DIR}/sources/QX509.cpp
|
||||
${QSIMPLECRYPTO_DIR}/sources/QX509Store.cpp
|
||||
)
|
||||
@@ -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)
|
||||
@@ -53,3 +57,43 @@ file(COPY ${AMNEZIA_LIBXRAY_PATH} DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/androi
|
||||
find_package(openvpn-pt-android REQUIRED)
|
||||
set(LIBS ${LIBS} amnezia::openvpn-pt-android)
|
||||
set_property(TARGET ${PROJECT} APPEND PROPERTY QT_ANDROID_EXTRA_LIBS ${OPENVPN_PT_ANDROID_LIBCK_OVPN_PLUGIN_PATH})
|
||||
|
||||
if(QT_USE_TARGET_ANDROID_BUILD_DIR)
|
||||
set(_android_build_dir "${CMAKE_CURRENT_BINARY_DIR}/android-build-${PROJECT}")
|
||||
else()
|
||||
set(_android_build_dir "${CMAKE_CURRENT_BINARY_DIR}/android-build")
|
||||
endif()
|
||||
|
||||
add_custom_target(android_gradle_clean
|
||||
COMMAND ./gradlew clean
|
||||
WORKING_DIRECTORY "${_android_build_dir}"
|
||||
COMMENT "Cleaning Android Gradle build cache"
|
||||
)
|
||||
|
||||
# Always-available debug target: build Play Debug APK and copy to standard output path
|
||||
# so Qt Creator's deploy step picks it up automatically
|
||||
add_custom_target(android_play_debug_install
|
||||
COMMAND ./gradlew assemblePlayDebug
|
||||
COMMAND sh -c "cp build/outputs/apk/play/debug/*.apk build/outputs/apk/android-build-${PROJECT}-debug.apk"
|
||||
WORKING_DIRECTORY "${_android_build_dir}"
|
||||
COMMENT "Building Android Play Debug APK and copying to deploy path"
|
||||
DEPENDS ${PROJECT}
|
||||
)
|
||||
|
||||
if(ANDROID_BUILD_PLAY)
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
set(_gradle_suffix "Debug")
|
||||
else()
|
||||
set(_gradle_suffix "Release")
|
||||
endif()
|
||||
add_custom_target(android_play_apk
|
||||
COMMAND ./gradlew assemblePlay${_gradle_suffix} WORKING_DIRECTORY "${_android_build_dir}"
|
||||
COMMENT "Building Android Play APK (assemblePlay${_gradle_suffix})"
|
||||
DEPENDS ${PROJECT}
|
||||
)
|
||||
add_custom_target(android_play_aab
|
||||
COMMAND ./gradlew bundlePlay${_gradle_suffix} WORKING_DIRECTORY "${_android_build_dir}"
|
||||
COMMENT "Building Android Play AAB (bundlePlay${_gradle_suffix})"
|
||||
DEPENDS ${PROJECT}
|
||||
)
|
||||
endif()
|
||||
|
||||
@@ -57,11 +57,9 @@ set(HEADERS ${HEADERS}
|
||||
${CMAKE_CURRENT_BINARY_DIR}/version.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/selfhosted/sshClient.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/networkUtilities.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/payloadSender.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/serialization/serialization.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/serialization/transfer.h
|
||||
${CLIENT_ROOT_DIR}/../common/logger/logger.h
|
||||
${CLIENT_ROOT_DIR}/../common/crypto/cryptoUtils.h
|
||||
${CLIENT_ROOT_DIR}/ui/utils/qmlUtils.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/api/apiUtils.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/osSignalHandler.h
|
||||
@@ -134,7 +132,6 @@ set(SOURCES ${SOURCES}
|
||||
${CLIENT_ROOT_DIR}/core/protocols/vpnProtocol.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/selfhosted/sshClient.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/networkUtilities.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/payloadSender.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/serialization/outbound.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/serialization/inbound.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/serialization/ss.cpp
|
||||
@@ -144,7 +141,6 @@ set(SOURCES ${SOURCES}
|
||||
${CLIENT_ROOT_DIR}/core/utils/serialization/vmess.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/serialization/vmess_new.cpp
|
||||
${CLIENT_ROOT_DIR}/../common/logger/logger.cpp
|
||||
${CLIENT_ROOT_DIR}/../common/crypto/cryptoUtils.cpp
|
||||
${CLIENT_ROOT_DIR}/ui/utils/qmlUtils.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/api/apiUtils.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/serverConfigUtils.cpp
|
||||
|
||||
@@ -220,6 +220,8 @@ ErrorCode ServicesCatalogController::fillAvailableServices(QJsonObject &services
|
||||
|
||||
QByteArray responseBody;
|
||||
ErrorCode errorCode = executeRequest(QString("%1v1/services"), apiPayload, responseBody);
|
||||
qWarning() << "[ServicesCatalog] errorCode:" << static_cast<int>(errorCode)
|
||||
<< "response:" << QString::fromLocal8Bit(responseBody);
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
if (!responseBody.contains(apiDefs::key::services.data())) {
|
||||
errorCode = ErrorCode::ApiServicesMissingError;
|
||||
@@ -241,7 +243,10 @@ ErrorCode ServicesCatalogController::fillAvailableServices(QJsonObject &services
|
||||
|
||||
ErrorCode ServicesCatalogController::executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody)
|
||||
{
|
||||
GatewayController gatewayController(m_appSettingsRepository->getGatewayEndpoint(), m_appSettingsRepository->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs,
|
||||
QString gatewayEndpoint = m_appSettingsRepository->getGatewayEndpoint();
|
||||
qWarning() << "[ServicesCatalog] request URL:" << endpoint.arg(gatewayEndpoint)
|
||||
<< "isDevEnv:" << m_appSettingsRepository->isDevGatewayEnv();
|
||||
GatewayController gatewayController(gatewayEndpoint, m_appSettingsRepository->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs,
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled(), m_appSettingsRepository);
|
||||
return gatewayController.post(endpoint, apiPayload, responseBody);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,9 @@
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
#include "platforms/ios/ios_controller.h"
|
||||
#include <AmneziaVPN-Swift.h>
|
||||
#elif defined(Q_OS_ANDROID)
|
||||
#include "platforms/android/android_controller.h"
|
||||
#include <QtConcurrent>
|
||||
#endif
|
||||
|
||||
using namespace amnezia;
|
||||
@@ -56,38 +59,6 @@ QString getSubscriptionStatusForRenewal(const ApiConfig &apiConfig)
|
||||
|
||||
return QStringLiteral("active");
|
||||
}
|
||||
|
||||
QString normalizeCaptchaSolution(const QString &captchaSolution)
|
||||
{
|
||||
QString normalizedSolution;
|
||||
normalizedSolution.reserve(captchaSolution.size());
|
||||
for (const QChar &ch : captchaSolution) {
|
||||
const ushort u = ch.unicode();
|
||||
if (u >= '0' && u <= '9') {
|
||||
normalizedSolution += ch;
|
||||
} else if (u >= 0xFF10 && u <= 0xFF19) {
|
||||
normalizedSolution += QChar(static_cast<char16_t>(u - 0xFF10 + '0'));
|
||||
}
|
||||
}
|
||||
return normalizedSolution.isEmpty() ? captchaSolution.trimmed() : normalizedSolution;
|
||||
}
|
||||
|
||||
bool fillCaptchaInfoFromResponse(const QByteArray &responseBody, SubscriptionController::CaptchaInfo &captchaInfo)
|
||||
{
|
||||
const QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
|
||||
if (!jsonDoc.isObject()) {
|
||||
return false;
|
||||
}
|
||||
const QJsonObject jsonObj = jsonDoc.object();
|
||||
if (!jsonObj.contains(QStringLiteral("captcha_id")) || !jsonObj.contains(QStringLiteral("captcha_image"))) {
|
||||
return false;
|
||||
}
|
||||
captchaInfo.captchaId = jsonObj.value(QStringLiteral("captcha_id")).toString();
|
||||
captchaInfo.captchaImageBase64 = jsonObj.value(QStringLiteral("captcha_image")).toString();
|
||||
captchaInfo.hint = jsonObj.value(QStringLiteral("hint")).toString();
|
||||
captchaInfo.isRequired = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -127,6 +98,9 @@ QJsonObject SubscriptionController::GatewayRequestData::toJsonObject() const
|
||||
if (!authData.isEmpty()) {
|
||||
obj[apiDefs::key::authData] = authData;
|
||||
}
|
||||
if (!transactionId.isEmpty()) {
|
||||
obj[apiDefs::key::transactionId] = transactionId;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
@@ -153,7 +127,7 @@ void SubscriptionController::appendProtocolDataToApiPayload(const QString &proto
|
||||
}
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::extractServerConfigJsonFromResponse(const QByteArray &apiResponseBody, const QString &protocol,
|
||||
ErrorCode SubscriptionController::extractServerConfigJsonFromResponse(const QByteArray &apiResponseBody, const QString &protocol,
|
||||
const ProtocolData &protocolData, QJsonObject &serverConfigJson)
|
||||
{
|
||||
QString data = QJsonDocument::fromJson(apiResponseBody).object().value(configKey::config).toString();
|
||||
@@ -218,23 +192,26 @@ ErrorCode SubscriptionController::extractServerConfigJsonFromResponse(const QByt
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
void SubscriptionController::updateApiConfigInJson(QJsonObject &serverConfigJson, const QString &serviceType,
|
||||
void SubscriptionController::updateApiConfigInJson(QJsonObject &serverConfigJson, const QString &serviceType,
|
||||
const QString &serviceProtocol, const QString &userCountryCode,
|
||||
const QByteArray &apiResponseBody)
|
||||
{
|
||||
QJsonObject apiConfig = serverConfigJson.value(apiDefs::key::apiConfig).toObject();
|
||||
|
||||
|
||||
apiConfig[apiDefs::key::serviceType] = serviceType;
|
||||
apiConfig[apiDefs::key::serviceProtocol] = serviceProtocol;
|
||||
apiConfig[apiDefs::key::userCountryCode] = userCountryCode;
|
||||
|
||||
|
||||
if (serverConfigJson.value(configKey::configVersion).toInt() == serverConfigUtils::ConfigSource::AmneziaGateway) {
|
||||
QJsonObject responseObj = QJsonDocument::fromJson(apiResponseBody).object();
|
||||
if (responseObj.contains(apiDefs::key::supportedProtocols)) {
|
||||
apiConfig.insert(apiDefs::key::supportedProtocols, responseObj.value(apiDefs::key::supportedProtocols).toArray());
|
||||
}
|
||||
if (responseObj.contains(apiDefs::key::serviceInfo)) {
|
||||
apiConfig.insert(apiDefs::key::serviceInfo, responseObj.value(apiDefs::key::serviceInfo).toObject());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
serverConfigJson[apiDefs::key::apiConfig] = apiConfig;
|
||||
}
|
||||
|
||||
@@ -245,6 +222,26 @@ ErrorCode SubscriptionController::executeRequest(const QString &endpoint, const
|
||||
return gatewayController.post(endpoint, apiPayload, responseBody);
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::getSubscriptionInfo(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const QString &purchaseToken,
|
||||
QByteArray &responseBody)
|
||||
{
|
||||
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
||||
QString(APP_VERSION),
|
||||
m_appSettingsRepository->getAppLanguage().name().split("_").first(),
|
||||
m_appSettingsRepository->getInstallationUuid(true),
|
||||
userCountryCode,
|
||||
"",
|
||||
serviceType,
|
||||
serviceProtocol,
|
||||
QJsonObject(),
|
||||
purchaseToken };
|
||||
|
||||
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
|
||||
qWarning() << "[Billing][getSubscriptionInfo] request:" << QJsonDocument(apiPayload).toJson(QJsonDocument::Compact);
|
||||
return executeRequest(QString("%1v1/get_subscription_info"), apiPayload, responseBody, false);
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::importServiceFromGateway(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const ProtocolData &protocolData,
|
||||
CaptchaInfo &captchaInfo)
|
||||
@@ -266,7 +263,14 @@ ErrorCode SubscriptionController::importServiceFromGateway(const QString &userCo
|
||||
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody);
|
||||
|
||||
if (errorCode == ErrorCode::ApiCaptchaRequiredError) {
|
||||
fillCaptchaInfoFromResponse(responseBody, captchaInfo);
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
|
||||
if (jsonDoc.isObject()) {
|
||||
QJsonObject jsonObj = jsonDoc.object();
|
||||
captchaInfo.captchaId = jsonObj.value("captcha_id").toString();
|
||||
captchaInfo.captchaImageBase64 = jsonObj.value("captcha_image").toString();
|
||||
captchaInfo.hint = jsonObj.value("hint").toString();
|
||||
captchaInfo.isRequired = true;
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
@@ -349,10 +353,10 @@ ErrorCode SubscriptionController::importTrialFromGateway(const QString &userCoun
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::importServiceFromAppStore(const QString &userCountryCode, const QString &serviceType,
|
||||
ErrorCode SubscriptionController::importServiceFromMarket(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const ProtocolData &protocolData,
|
||||
const QString &transactionId, bool isTestPurchase,
|
||||
int *duplicateServerIndex)
|
||||
int *duplicateServerIndex, const QString &endpoint)
|
||||
{
|
||||
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
||||
QString(APP_VERSION),
|
||||
@@ -362,14 +366,16 @@ ErrorCode SubscriptionController::importServiceFromAppStore(const QString &userC
|
||||
"",
|
||||
serviceType,
|
||||
serviceProtocol,
|
||||
QJsonObject() };
|
||||
QJsonObject(),
|
||||
transactionId};
|
||||
|
||||
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
|
||||
appendProtocolDataToApiPayload(serviceProtocol, protocolData, apiPayload);
|
||||
apiPayload[apiDefs::key::transactionId] = transactionId;
|
||||
|
||||
QByteArray responseBody;
|
||||
ErrorCode errorCode = executeRequest(QString("%1v1/subscriptions"), apiPayload, responseBody, isTestPurchase);
|
||||
qWarning() << "[Billing][importServiceFromMarket] endpoint:" << endpoint << "isTestPurchase:" << isTestPurchase;
|
||||
ErrorCode errorCode = executeRequest(QString("%1") + endpoint, apiPayload, responseBody, isTestPurchase);
|
||||
qWarning() << "[Billing][importServiceFromMarket] errorCode:" << static_cast<int>(errorCode) << "response:" << responseBody;
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
@@ -413,7 +419,7 @@ ErrorCode SubscriptionController::importServiceFromAppStore(const QString &userC
|
||||
QJsonObject configObject = QJsonDocument::fromJson(configString).object();
|
||||
|
||||
quint16 crc = qChecksum(QJsonDocument(configObject).toJson());
|
||||
|
||||
|
||||
if (configObject.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
@@ -432,8 +438,7 @@ ErrorCode SubscriptionController::importServiceFromAppStore(const QString &userC
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, bool isConnectEvent,
|
||||
CaptchaInfo *captchaInfoOut, ProtocolData *usedProtocolDataOut)
|
||||
ErrorCode SubscriptionController::updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, bool isConnectEvent)
|
||||
{
|
||||
auto apiV2 = m_serversRepository->apiV2Config(serverId);
|
||||
if (!apiV2.has_value()) {
|
||||
@@ -441,23 +446,6 @@ ErrorCode SubscriptionController::updateServiceFromGateway(const QString &server
|
||||
}
|
||||
const bool isTestPurchase = apiV2->apiConfig.isTestPurchase;
|
||||
QString serviceProtocol = apiV2->serviceProtocol();
|
||||
|
||||
if (!newCountryCode.isEmpty()) {
|
||||
const auto availableCountries = apiV2->apiConfig.availableCountries;
|
||||
for (const auto &country : availableCountries) {
|
||||
const auto countryObject = country.toObject();
|
||||
if (countryObject.value(apiDefs::key::serverCountryCode).toString() != newCountryCode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto availableProtocols = countryObject.value(apiDefs::key::availableProtocols).toArray();
|
||||
if (!availableProtocols.isEmpty() && !availableProtocols.contains(serviceProtocol)) {
|
||||
serviceProtocol = availableProtocols.first().toString();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ProtocolData protocolData = generateProtocolData(serviceProtocol);
|
||||
|
||||
QJsonObject authDataJson = apiV2->authData.toJson();
|
||||
@@ -481,11 +469,6 @@ ErrorCode SubscriptionController::updateServiceFromGateway(const QString &server
|
||||
QByteArray responseBody;
|
||||
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody, isTestPurchase);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
if (errorCode == ErrorCode::ApiCaptchaRequiredError && captchaInfoOut) {
|
||||
if (fillCaptchaInfoFromResponse(responseBody, *captchaInfoOut) && usedProtocolDataOut) {
|
||||
*usedProtocolDataOut = protocolData;
|
||||
}
|
||||
}
|
||||
if (errorCode == ErrorCode::ApiSubscriptionExpiredError && !apiV2->apiConfig.isInAppPurchase) {
|
||||
ApiV2ServerConfig expiredApiV2 = *apiV2;
|
||||
expiredApiV2.apiConfig.subscriptionExpiredByServer = true;
|
||||
@@ -495,19 +478,8 @@ ErrorCode SubscriptionController::updateServiceFromGateway(const QString &server
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
return applyUpdatedServiceConfig(serverId, serviceProtocol, protocolData, responseBody);
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::applyUpdatedServiceConfig(const QString &serverId, const QString &serviceProtocol,
|
||||
const ProtocolData &protocolData, const QByteArray &responseBody)
|
||||
{
|
||||
auto apiV2 = m_serversRepository->apiV2Config(serverId);
|
||||
if (!apiV2.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
QJsonObject serverConfigJson;
|
||||
ErrorCode errorCode = extractServerConfigJsonFromResponse(responseBody, serviceProtocol, protocolData, serverConfigJson);
|
||||
errorCode = extractServerConfigJsonFromResponse(responseBody, serviceProtocol, protocolData, serverConfigJson);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
@@ -540,66 +512,13 @@ ErrorCode SubscriptionController::applyUpdatedServiceConfig(const QString &serve
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::resolveUpdateServiceCaptcha(const QString &serverId, const QString &newCountryCode,
|
||||
bool isConnectEvent, const ProtocolData &protocolData,
|
||||
const QString &captchaId, const QString &captchaSolution,
|
||||
CaptchaInfo *retryCaptchaOut)
|
||||
{
|
||||
auto apiV2 = m_serversRepository->apiV2Config(serverId);
|
||||
if (!apiV2.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
const bool isTestPurchase = apiV2->apiConfig.isTestPurchase;
|
||||
QString serviceProtocol = apiV2->serviceProtocol();
|
||||
|
||||
QJsonObject authDataJson = apiV2->authData.toJson();
|
||||
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
||||
QString(APP_VERSION),
|
||||
m_appSettingsRepository->getAppLanguage().name().split("_").first(),
|
||||
m_appSettingsRepository->getInstallationUuid(true),
|
||||
apiV2->apiConfig.userCountryCode,
|
||||
newCountryCode,
|
||||
apiV2->serviceType(),
|
||||
serviceProtocol,
|
||||
authDataJson };
|
||||
|
||||
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
|
||||
appendProtocolDataToApiPayload(serviceProtocol, protocolData, apiPayload);
|
||||
|
||||
if (isConnectEvent) {
|
||||
apiPayload[apiDefs::key::isConnectEvent] = true;
|
||||
}
|
||||
|
||||
apiPayload["captcha_id"] = captchaId;
|
||||
apiPayload["captcha_solution"] = normalizeCaptchaSolution(captchaSolution);
|
||||
|
||||
QByteArray responseBody;
|
||||
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody, isTestPurchase);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
if (retryCaptchaOut
|
||||
&& (errorCode == ErrorCode::ApiCaptchaInvalidError || errorCode == ErrorCode::ApiCaptchaRefreshError
|
||||
|| errorCode == ErrorCode::ApiCaptchaRequiredError)) {
|
||||
fillCaptchaInfoFromResponse(responseBody, *retryCaptchaOut);
|
||||
}
|
||||
if (errorCode == ErrorCode::ApiSubscriptionExpiredError && !apiV2->apiConfig.isInAppPurchase) {
|
||||
ApiV2ServerConfig expiredApiV2 = *apiV2;
|
||||
expiredApiV2.apiConfig.subscriptionExpiredByServer = true;
|
||||
m_serversRepository->editServer(serverId, expiredApiV2.toJson(),
|
||||
serverConfigUtils::configTypeFromJson(expiredApiV2.toJson()));
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
return applyUpdatedServiceConfig(serverId, serviceProtocol, protocolData, responseBody);
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::deactivateDevice(const QString &serverId)
|
||||
{
|
||||
auto apiV2 = m_serversRepository->apiV2Config(serverId);
|
||||
if (!apiV2.has_value()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
|
||||
if (!apiV2->isPremium() && !apiV2->isExternalPremium()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
@@ -636,7 +555,7 @@ ErrorCode SubscriptionController::deactivateExternalDevice(const QString &server
|
||||
if (!apiV2.has_value()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
|
||||
if (!apiV2->isPremium() && !apiV2->isExternalPremium()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
@@ -757,20 +676,19 @@ ErrorCode SubscriptionController::prepareVpnKeyExport(const QString &serverId, Q
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::validateAndUpdateConfig(const QString &serverId, bool hasInstalledContainers,
|
||||
CaptchaInfo *captchaInfoOut, ProtocolData *usedProtocolDataOut)
|
||||
ErrorCode SubscriptionController::validateAndUpdateConfig(const QString &serverId, bool hasInstalledContainers)
|
||||
{
|
||||
if (!m_serversRepository->apiV2Config(serverId).has_value()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
if (!hasInstalledContainers) {
|
||||
return updateServiceFromGateway(serverId, "", true, captchaInfoOut, usedProtocolDataOut);
|
||||
return updateServiceFromGateway(serverId, "", true);
|
||||
}
|
||||
|
||||
if (isApiKeyExpired(serverId)) {
|
||||
qDebug() << "attempt to update api config by expires_at event";
|
||||
return updateServiceFromGateway(serverId, "", true, captchaInfoOut, usedProtocolDataOut);
|
||||
return updateServiceFromGateway(serverId, "", true);
|
||||
}
|
||||
|
||||
return ErrorCode::NoError;
|
||||
@@ -833,7 +751,7 @@ bool SubscriptionController::isApiKeyExpired(const QString &serverId) const
|
||||
return false;
|
||||
}
|
||||
const QString expiresAt = apiV2->apiConfig.publicKey.expiresAt;
|
||||
|
||||
|
||||
if (expiresAt.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
@@ -842,7 +760,7 @@ bool SubscriptionController::isApiKeyExpired(const QString &serverId) const
|
||||
if (expiresAtDateTime < QDateTime::currentDateTimeUtc()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -862,36 +780,6 @@ bool SubscriptionController::isVlessProtocol(const QString &serverId) const
|
||||
return apiV2.has_value() && apiV2->serviceProtocol() == "vless";
|
||||
}
|
||||
|
||||
QString SubscriptionController::currentProtocol(const QString &serverId) const
|
||||
{
|
||||
auto apiV2 = m_serversRepository->apiV2Config(serverId);
|
||||
return apiV2.has_value() ? apiV2->serviceProtocol() : QString();
|
||||
}
|
||||
|
||||
QStringList SubscriptionController::availableProtocols(const QString &serverId) const
|
||||
{
|
||||
auto apiV2 = m_serversRepository->apiV2Config(serverId);
|
||||
if (!apiV2.has_value()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto currentCountryCode = apiV2->apiConfig.serverCountryCode;
|
||||
const auto availableCountries = apiV2->apiConfig.availableCountries;
|
||||
|
||||
QStringList protocols;
|
||||
for (const auto &country : availableCountries) {
|
||||
const auto countryObject = country.toObject();
|
||||
if (countryObject.value(apiDefs::key::serverCountryCode).toString() != currentCountryCode) {
|
||||
continue;
|
||||
}
|
||||
for (const auto &protocol : countryObject.value(apiDefs::key::availableProtocols).toArray()) {
|
||||
protocols.push_back(protocol.toString());
|
||||
}
|
||||
break;
|
||||
}
|
||||
return protocols;
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::processAppStorePurchase(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const QString &productId,
|
||||
int *duplicateServerIndex)
|
||||
@@ -926,8 +814,123 @@ ErrorCode SubscriptionController::processAppStorePurchase(const QString &userCou
|
||||
bool isTestPurchase = IosController::Instance()->isTestFlight();
|
||||
|
||||
ProtocolData protocolData = generateProtocolData(serviceProtocol);
|
||||
return importServiceFromAppStore(userCountryCode, serviceType, serviceProtocol, protocolData,
|
||||
originalTransactionId, isTestPurchase, duplicateServerIndex);
|
||||
return importServiceFromMarket(userCountryCode, serviceType, serviceProtocol, protocolData,
|
||||
originalTransactionId, isTestPurchase, duplicateServerIndex,
|
||||
QStringLiteral("v1/subscriptions"));
|
||||
#else
|
||||
Q_UNUSED(userCountryCode);
|
||||
Q_UNUSED(serviceType);
|
||||
Q_UNUSED(serviceProtocol);
|
||||
Q_UNUSED(productId);
|
||||
return ErrorCode::ApiPurchaseError;
|
||||
#endif
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::processPlayMarketPurchase(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const QString &productId,
|
||||
int *duplicateServerIndex)
|
||||
{
|
||||
#if defined(Q_OS_ANDROID)
|
||||
auto androidController = AndroidController::instance();
|
||||
QString purchaseToken;
|
||||
bool purchaseOk = false;
|
||||
|
||||
QFutureWatcher<QPair<bool, QString>> watcher;
|
||||
QEventLoop waitLoop;
|
||||
QObject::connect(&watcher, &QFutureWatcher<QPair<bool, QString>>::finished, &waitLoop, &QEventLoop::quit);
|
||||
|
||||
QFuture<QPair<bool, QString>> future = QtConcurrent::run([androidController, productId]() {
|
||||
QJsonObject plansResult = androidController->getSubscriptionPlans();
|
||||
int responseCode = plansResult.value("responseCode").toInt(-1);
|
||||
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();
|
||||
QJsonArray offers = product.value("offers").toArray();
|
||||
for (const QJsonValue &offerValue : offers) {
|
||||
QJsonObject offer = offerValue.toObject();
|
||||
if (offer.value("basePlanId").toString() == productId) {
|
||||
offerToken = offer.value("offerToken").toString();
|
||||
qInfo() << "[Billing] Found offer token for basePlanId:" << productId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!offerToken.isEmpty()) break;
|
||||
}
|
||||
if (offerToken.isEmpty()) {
|
||||
qWarning() << "[Billing] No offer token found for basePlanId:" << productId;
|
||||
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();
|
||||
int purchaseState = purchase.value("purchaseState").toInt(-1);
|
||||
qInfo() << "[Billing] Purchase success. purchaseToken:" << token << "isAcknowledged:" << isAcknowledged << "purchaseState:" << purchaseState;
|
||||
// purchaseState 1 = PURCHASED, 0 = PENDING (user must confirm payment in Google Play)
|
||||
if (purchaseState != 1) {
|
||||
qWarning() << "[Billing] Purchase is in PENDING state, waiting for user to confirm payment";
|
||||
return qMakePair(false, QStringLiteral("pending"));
|
||||
}
|
||||
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) {
|
||||
if (purchaseToken == QStringLiteral("pending")) {
|
||||
return ErrorCode::ApiPurchasePendingError;
|
||||
}
|
||||
return ErrorCode::ApiPurchaseError;
|
||||
}
|
||||
if (purchaseToken.isEmpty()) {
|
||||
return ErrorCode::ApiPurchaseError;
|
||||
}
|
||||
|
||||
// First call: determine if this is a test purchase
|
||||
QByteArray checkResponse;
|
||||
ErrorCode checkError = getSubscriptionInfo(userCountryCode, serviceType, serviceProtocol, purchaseToken, checkResponse);
|
||||
qWarning() << "[Billing][processPlayMarketPurchase] getSubscriptionInfo errorCode:" << static_cast<int>(checkError) << "response:" << checkResponse;
|
||||
if (checkError != ErrorCode::NoError) {
|
||||
qWarning().noquote() << "[Billing] Initial subscriptions check failed:" << static_cast<int>(checkError);
|
||||
return checkError;
|
||||
}
|
||||
|
||||
QJsonObject checkObject = QJsonDocument::fromJson(checkResponse).object();
|
||||
bool isTestPurchase = checkObject.value(apiDefs::key::isTestPurchase).toBool(false);
|
||||
qInfo().noquote() << "[Billing] Purchase isTestPurchase =" << isTestPurchase;
|
||||
|
||||
// Second call: import service with correct isTestPurchase flag
|
||||
ProtocolData protocolData = generateProtocolData(serviceProtocol);
|
||||
return importServiceFromMarket(userCountryCode, serviceType, serviceProtocol, protocolData,
|
||||
purchaseToken, isTestPurchase, duplicateServerIndex,
|
||||
QStringLiteral("v1/subscriptions"));
|
||||
#else
|
||||
Q_UNUSED(userCountryCode);
|
||||
Q_UNUSED(serviceType);
|
||||
@@ -948,6 +951,9 @@ SubscriptionController::AppStoreRestoreResult SubscriptionController::processApp
|
||||
QString restoreError;
|
||||
QEventLoop waitRestore;
|
||||
|
||||
qInfo().noquote() << "[IAP][processAppStoreRestore] Starting restore. userCountryCode=" << userCountryCode
|
||||
<< "serviceType=" << serviceType << "serviceProtocol=" << serviceProtocol;
|
||||
|
||||
IosController::Instance()->restorePurchases([&](bool success, const QList<QVariantMap> &transactions, const QString &errorString) {
|
||||
restoreSuccess = success;
|
||||
restoredTransactions = transactions;
|
||||
@@ -956,6 +962,10 @@ SubscriptionController::AppStoreRestoreResult SubscriptionController::processApp
|
||||
});
|
||||
waitRestore.exec();
|
||||
|
||||
qInfo().noquote() << "[IAP][processAppStoreRestore] restorePurchases result: success=" << restoreSuccess
|
||||
<< "transactions count=" << restoredTransactions.size()
|
||||
<< "error=" << restoreError;
|
||||
|
||||
if (!restoreSuccess) {
|
||||
qWarning().noquote() << "[IAP] Restore failed:" << restoreError;
|
||||
result.errorCode = ErrorCode::ApiPurchaseError;
|
||||
@@ -969,6 +979,7 @@ SubscriptionController::AppStoreRestoreResult SubscriptionController::processApp
|
||||
}
|
||||
|
||||
bool isTestPurchase = IosController::Instance()->isTestFlight();
|
||||
qInfo().noquote() << "[IAP][processAppStoreRestore] isTestFlight=" << isTestPurchase;
|
||||
QSet<QString> processedTransactions;
|
||||
|
||||
for (const QVariantMap &transaction : restoredTransactions) {
|
||||
@@ -976,6 +987,9 @@ SubscriptionController::AppStoreRestoreResult SubscriptionController::processApp
|
||||
const QString transactionId = transaction.value(QStringLiteral("transactionId")).toString();
|
||||
const QString transactionProductId = transaction.value(QStringLiteral("productId")).toString();
|
||||
|
||||
qInfo().noquote() << "[IAP][processAppStoreRestore] Processing transaction: transactionId=" << transactionId
|
||||
<< "originalTransactionId=" << originalTransactionId << "productId=" << transactionProductId;
|
||||
|
||||
if (originalTransactionId.isEmpty()) {
|
||||
qWarning().noquote() << "[IAP] Skipping restored transaction without originalTransactionId" << transactionId;
|
||||
continue;
|
||||
@@ -983,6 +997,7 @@ SubscriptionController::AppStoreRestoreResult SubscriptionController::processApp
|
||||
|
||||
if (processedTransactions.contains(originalTransactionId)) {
|
||||
result.duplicateCount++;
|
||||
qInfo().noquote() << "[IAP][processAppStoreRestore] Skipping duplicate originalTransactionId=" << originalTransactionId;
|
||||
continue;
|
||||
}
|
||||
processedTransactions.insert(originalTransactionId);
|
||||
@@ -992,9 +1007,13 @@ SubscriptionController::AppStoreRestoreResult SubscriptionController::processApp
|
||||
|
||||
ProtocolData protocolData = generateProtocolData(serviceProtocol);
|
||||
int currentDuplicateServerIndex = -1;
|
||||
ErrorCode errorCode = importServiceFromAppStore(userCountryCode, serviceType, serviceProtocol, protocolData,
|
||||
ErrorCode errorCode = importServiceFromMarket(userCountryCode, serviceType, serviceProtocol, protocolData,
|
||||
originalTransactionId, isTestPurchase,
|
||||
¤tDuplicateServerIndex);
|
||||
¤tDuplicateServerIndex,
|
||||
QStringLiteral("v1/restore_subscription"));
|
||||
|
||||
qInfo().noquote() << "[IAP][processAppStoreRestore] importServiceFromMarket errorCode=" << static_cast<int>(errorCode)
|
||||
<< "for originalTransactionId=" << originalTransactionId;
|
||||
|
||||
if (errorCode == ErrorCode::ApiConfigAlreadyAdded) {
|
||||
result.duplicateConfigAlreadyPresent = true;
|
||||
@@ -1004,7 +1023,8 @@ SubscriptionController::AppStoreRestoreResult SubscriptionController::processApp
|
||||
qInfo().noquote() << "[IAP] Skipping restored transaction" << originalTransactionId
|
||||
<< "because subscription config with the same vpn_key already exists";
|
||||
} else if (errorCode != ErrorCode::NoError) {
|
||||
qWarning().noquote() << "[IAP] Failed to process restored subscription response for transaction" << originalTransactionId;
|
||||
qWarning().noquote() << "[IAP] Failed to process restored subscription response for transaction" << originalTransactionId
|
||||
<< "errorCode=" << static_cast<int>(errorCode);
|
||||
result.errorCode = errorCode;
|
||||
} else {
|
||||
result.hasInstalledConfig = true;
|
||||
@@ -1015,6 +1035,132 @@ SubscriptionController::AppStoreRestoreResult SubscriptionController::processApp
|
||||
result.errorCode = result.duplicateConfigAlreadyPresent ? ErrorCode::ApiConfigAlreadyAdded : ErrorCode::ApiPurchaseError;
|
||||
}
|
||||
|
||||
qInfo().noquote() << "[IAP][processAppStoreRestore] Done. hasInstalledConfig=" << result.hasInstalledConfig
|
||||
<< "duplicateConfigAlreadyPresent=" << result.duplicateConfigAlreadyPresent
|
||||
<< "duplicateCount=" << result.duplicateCount
|
||||
<< "errorCode=" << static_cast<int>(result.errorCode);
|
||||
|
||||
return result;
|
||||
#else
|
||||
Q_UNUSED(userCountryCode);
|
||||
Q_UNUSED(serviceType);
|
||||
Q_UNUSED(serviceProtocol);
|
||||
result.errorCode = ErrorCode::ApiPurchaseError;
|
||||
return result;
|
||||
#endif
|
||||
}
|
||||
|
||||
SubscriptionController::PlayMarketRestoreResult SubscriptionController::processPlayMarketRestore(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol)
|
||||
{
|
||||
PlayMarketRestoreResult result;
|
||||
|
||||
#if defined(Q_OS_ANDROID)
|
||||
auto androidController = AndroidController::instance();
|
||||
|
||||
QJsonObject purchasesResult;
|
||||
{
|
||||
QFutureWatcher<QJsonObject> queryWatcher;
|
||||
QEventLoop queryLoop;
|
||||
QObject::connect(&queryWatcher, &QFutureWatcher<QJsonObject>::finished, &queryLoop, &QEventLoop::quit);
|
||||
QFuture<QJsonObject> queryFuture = QtConcurrent::run([androidController]() {
|
||||
return androidController->queryPurchases();
|
||||
});
|
||||
queryWatcher.setFuture(queryFuture);
|
||||
queryLoop.exec();
|
||||
purchasesResult = queryWatcher.result();
|
||||
}
|
||||
|
||||
int responseCode = purchasesResult.value("responseCode").toInt(-1);
|
||||
if (responseCode != 0) {
|
||||
qWarning().noquote() << "[Billing] queryPurchases failed, responseCode =" << responseCode;
|
||||
result.errorCode = ErrorCode::ApiPurchaseError;
|
||||
return result;
|
||||
}
|
||||
|
||||
QJsonArray purchases = purchasesResult.value("purchases").toArray();
|
||||
if (purchases.isEmpty()) {
|
||||
qInfo().noquote() << "[Billing] Restore completed, but no purchases were found";
|
||||
result.errorCode = ErrorCode::ApiNoPurchasesToRestore;
|
||||
return result;
|
||||
}
|
||||
|
||||
QSet<QString> processedTokens;
|
||||
for (const QJsonValue &purchaseValue : std::as_const(purchases)) {
|
||||
const QJsonObject purchaseObj = purchaseValue.toObject();
|
||||
const QString purchaseToken = purchaseObj.value("purchaseToken").toString();
|
||||
|
||||
if (purchaseToken.isEmpty()) {
|
||||
qWarning().noquote() << "[Billing] Skipping purchase without purchaseToken";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (processedTokens.contains(purchaseToken)) {
|
||||
result.duplicateCount++;
|
||||
continue;
|
||||
}
|
||||
processedTokens.insert(purchaseToken);
|
||||
|
||||
qInfo().noquote() << "[Billing] Restoring subscription with purchaseToken =" << purchaseToken;
|
||||
|
||||
{
|
||||
QFutureWatcher<QJsonObject> ackWatcher;
|
||||
QEventLoop ackLoop;
|
||||
QObject::connect(&ackWatcher, &QFutureWatcher<QJsonObject>::finished, &ackLoop, &QEventLoop::quit);
|
||||
QFuture<QJsonObject> ackFuture = QtConcurrent::run([androidController, purchaseToken]() {
|
||||
return androidController->acknowledgePurchase(purchaseToken);
|
||||
});
|
||||
ackWatcher.setFuture(ackFuture);
|
||||
ackLoop.exec();
|
||||
QJsonObject ackResult = ackWatcher.result();
|
||||
int ackCode = ackResult.value("responseCode").toInt(-1);
|
||||
if (ackCode != 0) {
|
||||
qWarning().noquote() << "[Billing] acknowledgePurchase failed, responseCode =" << ackCode;
|
||||
} else {
|
||||
qInfo().noquote() << "[Billing] Purchase acknowledged successfully";
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray checkResponse;
|
||||
ErrorCode checkError = getSubscriptionInfo(userCountryCode, serviceType, serviceProtocol, purchaseToken, checkResponse);
|
||||
qWarning() << "[Billing][processPlayMarketRestore] getSubscriptionInfo errorCode:" << static_cast<int>(checkError) << "response:" << checkResponse;
|
||||
if (checkError != ErrorCode::NoError) {
|
||||
qWarning().noquote() << "[Billing] Initial subscriptions check failed:" << static_cast<int>(checkError);
|
||||
result.errorCode = checkError;
|
||||
continue;
|
||||
}
|
||||
|
||||
QJsonObject checkObject = QJsonDocument::fromJson(checkResponse).object();
|
||||
bool isTestPurchase = checkObject.value(apiDefs::key::isTestPurchase).toBool(false);
|
||||
qInfo().noquote() << "[Billing] Purchase isTestPurchase =" << isTestPurchase;
|
||||
|
||||
ProtocolData protocolData = generateProtocolData(serviceProtocol);
|
||||
int currentDuplicateServerIndex = -1;
|
||||
ErrorCode errorCode = importServiceFromMarket(userCountryCode, serviceType, serviceProtocol, protocolData,
|
||||
purchaseToken, isTestPurchase,
|
||||
¤tDuplicateServerIndex,
|
||||
QStringLiteral("v1/restore_subscription"));
|
||||
|
||||
if (errorCode == ErrorCode::ApiConfigAlreadyAdded) {
|
||||
result.duplicateConfigAlreadyPresent = true;
|
||||
if (result.duplicateServerIndex < 0) {
|
||||
result.duplicateServerIndex = currentDuplicateServerIndex;
|
||||
}
|
||||
qInfo().noquote() << "[Billing] Skipping purchase" << purchaseToken
|
||||
<< "because subscription config with the same vpn_key already exists";
|
||||
} else if (errorCode != ErrorCode::NoError) {
|
||||
qWarning().noquote() << "[Billing] Failed to process restored subscription for purchaseToken =" << purchaseToken
|
||||
<< "errorCode =" << static_cast<int>(errorCode);
|
||||
result.errorCode = errorCode;
|
||||
} else {
|
||||
result.hasInstalledConfig = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.hasInstalledConfig) {
|
||||
result.errorCode = result.duplicateConfigAlreadyPresent ? ErrorCode::ApiConfigAlreadyAdded : ErrorCode::ApiNoPurchasesToRestore;
|
||||
}
|
||||
|
||||
return result;
|
||||
#else
|
||||
Q_UNUSED(userCountryCode);
|
||||
@@ -1027,12 +1173,14 @@ SubscriptionController::AppStoreRestoreResult SubscriptionController::processApp
|
||||
|
||||
ErrorCode SubscriptionController::getAccountInfo(const QString &serverId, QJsonObject &accountInfo)
|
||||
{
|
||||
auto apiV2 = m_serversRepository->apiV2Config(serverId);
|
||||
if (!apiV2.has_value()) {
|
||||
auto apiV2Opt = m_serversRepository->apiV2Config(serverId);
|
||||
if (!apiV2Opt.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
const ApiV2ServerConfig* apiV2 = &apiV2Opt.value();
|
||||
bool isTestPurchase = apiV2->apiConfig.isTestPurchase;
|
||||
|
||||
|
||||
QJsonObject authDataJson = apiV2->authData.toJson();
|
||||
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
||||
QString(APP_VERSION),
|
||||
@@ -1134,7 +1282,17 @@ ErrorCode SubscriptionController::resolveImportServiceCaptcha(const QString &use
|
||||
appendProtocolDataToApiPayload(serviceProtocol, protocolData, apiPayload);
|
||||
|
||||
apiPayload["captcha_id"] = captchaId;
|
||||
apiPayload["captcha_solution"] = normalizeCaptchaSolution(captchaSolution);
|
||||
QString normalizedSolution;
|
||||
normalizedSolution.reserve(captchaSolution.size());
|
||||
for (const QChar &ch : captchaSolution) {
|
||||
const ushort u = ch.unicode();
|
||||
if (u >= '0' && u <= '9') {
|
||||
normalizedSolution += ch;
|
||||
} else if (u >= 0xFF10 && u <= 0xFF19) {
|
||||
normalizedSolution += QChar(static_cast<char16_t>(u - 0xFF10 + '0'));
|
||||
}
|
||||
}
|
||||
apiPayload["captcha_solution"] = normalizedSolution.isEmpty() ? captchaSolution.trimmed() : normalizedSolution;
|
||||
|
||||
QByteArray responseBody;
|
||||
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody);
|
||||
@@ -1142,7 +1300,16 @@ ErrorCode SubscriptionController::resolveImportServiceCaptcha(const QString &use
|
||||
if (retryCaptchaOut
|
||||
&& (errorCode == ErrorCode::ApiCaptchaInvalidError || errorCode == ErrorCode::ApiCaptchaRefreshError
|
||||
|| errorCode == ErrorCode::ApiCaptchaRequiredError)) {
|
||||
fillCaptchaInfoFromResponse(responseBody, *retryCaptchaOut);
|
||||
const QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
|
||||
if (jsonDoc.isObject()) {
|
||||
const QJsonObject jsonObj = jsonDoc.object();
|
||||
if (jsonObj.contains(QStringLiteral("captcha_id")) && jsonObj.contains(QStringLiteral("captcha_image"))) {
|
||||
retryCaptchaOut->captchaId = jsonObj.value(QStringLiteral("captcha_id")).toString();
|
||||
retryCaptchaOut->captchaImageBase64 = jsonObj.value(QStringLiteral("captcha_image")).toString();
|
||||
retryCaptchaOut->hint = jsonObj.value(QStringLiteral("hint")).toString();
|
||||
retryCaptchaOut->isRequired = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ public:
|
||||
QString serviceType;
|
||||
QString serviceProtocol;
|
||||
QJsonObject authData;
|
||||
QString transactionId;
|
||||
|
||||
QJsonObject toJsonObject() const;
|
||||
};
|
||||
@@ -61,17 +62,17 @@ public:
|
||||
ErrorCode importTrialFromGateway(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const QString &email);
|
||||
|
||||
ErrorCode importServiceFromAppStore(const QString &userCountryCode, const QString &serviceType,
|
||||
ErrorCode importServiceFromMarket(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const ProtocolData &protocolData,
|
||||
const QString &transactionId, bool isTestPurchase,
|
||||
int *duplicateServerIndex = nullptr);
|
||||
int *duplicateServerIndex,
|
||||
const QString &endpoint);
|
||||
|
||||
ErrorCode updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, bool isConnectEvent,
|
||||
CaptchaInfo *captchaInfoOut = nullptr, ProtocolData *usedProtocolDataOut = nullptr);
|
||||
ErrorCode getSubscriptionInfo(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const QString &purchaseToken,
|
||||
QByteArray &responseBody);
|
||||
|
||||
ErrorCode resolveUpdateServiceCaptcha(const QString &serverId, const QString &newCountryCode, bool isConnectEvent,
|
||||
const ProtocolData &protocolData, const QString &captchaId,
|
||||
const QString &captchaSolution, CaptchaInfo *retryCaptchaOut = nullptr);
|
||||
ErrorCode updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, bool isConnectEvent);
|
||||
|
||||
ErrorCode deactivateDevice(const QString &serverId);
|
||||
|
||||
@@ -83,8 +84,7 @@ public:
|
||||
|
||||
ErrorCode prepareVpnKeyExport(const QString &serverId, QString &vpnKey);
|
||||
|
||||
ErrorCode validateAndUpdateConfig(const QString &serverId, bool hasInstalledContainers,
|
||||
CaptchaInfo *captchaInfoOut = nullptr, ProtocolData *usedProtocolDataOut = nullptr);
|
||||
ErrorCode validateAndUpdateConfig(const QString &serverId, bool hasInstalledContainers);
|
||||
|
||||
void removeApiConfig(const QString &serverId);
|
||||
|
||||
@@ -92,8 +92,6 @@ public:
|
||||
|
||||
void setCurrentProtocol(const QString &serverId, const QString &protocolName);
|
||||
bool isVlessProtocol(const QString &serverId) const;
|
||||
QString currentProtocol(const QString &serverId) const;
|
||||
QStringList availableProtocols(const QString &serverId) const;
|
||||
|
||||
ErrorCode getAccountInfo(const QString &serverId, QJsonObject &accountInfo);
|
||||
QFuture<QPair<ErrorCode, QString>> getRenewalLink(const QString &serverId);
|
||||
@@ -107,10 +105,23 @@ public:
|
||||
ErrorCode errorCode = ErrorCode::NoError;
|
||||
};
|
||||
|
||||
struct PlayMarketRestoreResult
|
||||
{
|
||||
bool hasInstalledConfig = false;
|
||||
bool duplicateConfigAlreadyPresent = false;
|
||||
int duplicateCount = 0;
|
||||
int duplicateServerIndex = -1;
|
||||
ErrorCode errorCode = ErrorCode::NoError;
|
||||
};
|
||||
|
||||
ErrorCode processAppStorePurchase(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const QString &productId,
|
||||
int *duplicateServerIndex = nullptr);
|
||||
|
||||
ErrorCode processPlayMarketPurchase(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const QString &productId,
|
||||
int *duplicateServerIndex = nullptr);
|
||||
|
||||
AppStoreRestoreResult processAppStoreRestore(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol);
|
||||
|
||||
@@ -119,14 +130,15 @@ public:
|
||||
const QString &captchaId, const QString &captchaSolution,
|
||||
CaptchaInfo *retryCaptchaOut = nullptr);
|
||||
|
||||
PlayMarketRestoreResult processPlayMarketRestore(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol);
|
||||
|
||||
private:
|
||||
ErrorCode executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody, bool isTestPurchase = false);
|
||||
bool isApiKeyExpired(const QString &serverId) const;
|
||||
|
||||
ErrorCode extractServerConfigJsonFromResponse(const QByteArray &apiResponseBody, const QString &protocol,
|
||||
ErrorCode extractServerConfigJsonFromResponse(const QByteArray &apiResponseBody, const QString &protocol,
|
||||
const ProtocolData &protocolData, QJsonObject &serverConfigJson);
|
||||
ErrorCode applyUpdatedServiceConfig(const QString &serverId, const QString &serviceProtocol,
|
||||
const ProtocolData &protocolData, const QByteArray &responseBody);
|
||||
void updateApiConfigInJson(QJsonObject &serverConfigJson, const QString &serviceType,
|
||||
const QString &serviceProtocol, const QString &userCountryCode,
|
||||
const QByteArray &apiResponseBody);
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/protocols/protocolUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/payloadSender.h"
|
||||
#include "core/utils/utilities.h"
|
||||
#include "core/utils/serverConfigUtils.h"
|
||||
#include "version.h"
|
||||
@@ -219,11 +218,6 @@ ErrorCode ConnectionController::openConnection(const QString &serverId)
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
const auto apiV2 = m_serversRepository->apiV2Config(serverId);
|
||||
if (apiV2.has_value() && !apiV2->sendPayload.isEmpty()) {
|
||||
PayloadSender::sendAll(apiV2->sendPayload);
|
||||
}
|
||||
|
||||
emit openConnectionRequested(serverId, container, vpnConfiguration);
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
@@ -205,8 +205,10 @@ void CoreSignalHandlers::initAdminConfigRevokedHandler()
|
||||
{
|
||||
connect(m_coreController->m_installController, &InstallController::clientRevocationRequested, this,
|
||||
[this](const QString &serverId, const ContainerConfig &containerConfig, DockerContainer container) {
|
||||
m_coreController->m_usersController->revokeClient(serverId, containerConfig, container);
|
||||
}, Qt::DirectConnection);
|
||||
QtConcurrent::run([this, serverId, containerConfig, container]() {
|
||||
m_coreController->m_usersController->revokeClient(serverId, containerConfig, container);
|
||||
});
|
||||
});
|
||||
|
||||
connect(m_coreController->m_installController, &InstallController::clientAppendRequested, this,
|
||||
[this](const QString &serverId, const QString &clientId, const QString &clientName, DockerContainer container) {
|
||||
|
||||
@@ -12,14 +12,15 @@
|
||||
#include <QPromise>
|
||||
#include <QUrl>
|
||||
|
||||
#include <openssl/rsa.h>
|
||||
#include "QBlockCipher.h"
|
||||
#include "QRsa.h"
|
||||
|
||||
#include "amneziaApplication.h"
|
||||
#include "core/repositories/secureAppSettingsRepository.h"
|
||||
#include "core/utils/api/apiUtils.h"
|
||||
#include "core/utils/constants/apiKeys.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
#include "cryptoUtils.h"
|
||||
#include "core/utils/utilities.h"
|
||||
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
#include "core/utils/ipcClient.h"
|
||||
@@ -62,24 +63,27 @@ namespace
|
||||
|
||||
bool decryptProxyUrlsPayload(const QByteArray &encryptedPayload, bool isDevEnvironment, QByteArray &decryptedPayload)
|
||||
{
|
||||
QByteArray key = isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||
if (!isDevEnvironment) {
|
||||
QCryptographicHash hash(QCryptographicHash::Sha512);
|
||||
hash.addData(key);
|
||||
QByteArray h = hash.result().toHex();
|
||||
try {
|
||||
QByteArray key = isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||
if (!isDevEnvironment) {
|
||||
QCryptographicHash hash(QCryptographicHash::Sha512);
|
||||
hash.addData(key);
|
||||
QByteArray h = hash.result().toHex();
|
||||
|
||||
QByteArray decKey = QByteArray::fromHex(h.left(64));
|
||||
QByteArray iv = QByteArray::fromHex(h.mid(64, 32));
|
||||
QByteArray ba = QByteArray::fromBase64(encryptedPayload);
|
||||
QByteArray decKey = QByteArray::fromHex(h.left(64));
|
||||
QByteArray iv = QByteArray::fromHex(h.mid(64, 32));
|
||||
QByteArray ba = QByteArray::fromBase64(encryptedPayload);
|
||||
|
||||
decryptedPayload = CryptoUtils::decryptAes256Cbc(ba, decKey, iv);
|
||||
if (decryptedPayload.isEmpty()) {
|
||||
return false;
|
||||
QSimpleCrypto::QBlockCipher cipher;
|
||||
decryptedPayload = cipher.decryptAesBlockCipher(ba, decKey, iv);
|
||||
} else {
|
||||
decryptedPayload = encryptedPayload;
|
||||
}
|
||||
} else {
|
||||
decryptedPayload = encryptedPayload;
|
||||
return true;
|
||||
} catch (...) {
|
||||
Utils::logException();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
QStringList readCachedProxyUrls(const QByteArray &cachedProxyUrlsEncrypted, bool isDevEnvironment)
|
||||
@@ -147,29 +151,40 @@ GatewayController::EncryptedRequestData GatewayController::prepareRequest(const
|
||||
}
|
||||
#endif
|
||||
|
||||
encRequestData.key = CryptoUtils::generateRandomBytes(32);
|
||||
encRequestData.iv = CryptoUtils::generateRandomBytes(32);
|
||||
encRequestData.salt = CryptoUtils::generateRandomBytes(8);
|
||||
QSimpleCrypto::QBlockCipher blockCipher;
|
||||
encRequestData.key = blockCipher.generatePrivateSalt(32);
|
||||
encRequestData.iv = blockCipher.generatePrivateSalt(32);
|
||||
encRequestData.salt = blockCipher.generatePrivateSalt(8);
|
||||
|
||||
QJsonObject keyPayload;
|
||||
keyPayload[apiDefs::key::aesKey] = QString(encRequestData.key.toBase64());
|
||||
keyPayload[apiDefs::key::aesIv] = QString(encRequestData.iv.toBase64());
|
||||
keyPayload[apiDefs::key::aesSalt] = QString(encRequestData.salt.toBase64());
|
||||
|
||||
QByteArray rsaKey = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||
EVP_PKEY *publicKey = CryptoUtils::loadPublicKeyFromPem(rsaKey);
|
||||
if (publicKey == nullptr) {
|
||||
qCritical() << "error loading public key from environment variables";
|
||||
encRequestData.errorCode = ErrorCode::ApiMissingAgwPublicKey;
|
||||
return encRequestData;
|
||||
}
|
||||
QByteArray encryptedKeyPayload;
|
||||
QByteArray encryptedApiPayload;
|
||||
try {
|
||||
QSimpleCrypto::QRsa rsa;
|
||||
|
||||
QByteArray encryptedKeyPayload = CryptoUtils::rsaEncrypt(QJsonDocument(keyPayload).toJson(), publicKey, RSA_PKCS1_PADDING);
|
||||
EVP_PKEY_free(publicKey);
|
||||
EVP_PKEY *publicKey = nullptr;
|
||||
try {
|
||||
QByteArray rsaKey = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||
QSimpleCrypto::QRsa rsa;
|
||||
publicKey = rsa.getPublicKeyFromByteArray(rsaKey);
|
||||
} catch (...) {
|
||||
Utils::logException();
|
||||
qCritical() << "error loading public key from environment variables";
|
||||
encRequestData.errorCode = ErrorCode::ApiMissingAgwPublicKey;
|
||||
return encRequestData;
|
||||
}
|
||||
|
||||
QByteArray encryptedApiPayload = CryptoUtils::encryptAes256Cbc(QJsonDocument(apiPayload).toJson(), encRequestData.key, encRequestData.iv);
|
||||
encryptedKeyPayload = rsa.encrypt(QJsonDocument(keyPayload).toJson(), publicKey, RSA_PKCS1_PADDING);
|
||||
EVP_PKEY_free(publicKey);
|
||||
|
||||
if (encryptedKeyPayload.isEmpty() || encryptedApiPayload.isEmpty()) {
|
||||
encryptedApiPayload = blockCipher.encryptAesBlockCipher(QJsonDocument(apiPayload).toJson(), encRequestData.key, encRequestData.iv,
|
||||
"", encRequestData.salt);
|
||||
} catch (...) {
|
||||
Utils::logException();
|
||||
qCritical() << "error when encrypting the request body";
|
||||
encRequestData.errorCode = ErrorCode::ApiConfigDecryptionError;
|
||||
return encRequestData;
|
||||
@@ -191,11 +206,11 @@ GatewayController::DecryptionResult GatewayController::tryDecryptResponseBody(co
|
||||
result.decryptedBody = encryptedResponseBody;
|
||||
result.isDecryptionSuccessful = false;
|
||||
|
||||
QByteArray decrypted = CryptoUtils::decryptAes256Cbc(encryptedResponseBody, key, iv);
|
||||
if (!decrypted.isEmpty()) {
|
||||
result.decryptedBody = decrypted;
|
||||
try {
|
||||
QSimpleCrypto::QBlockCipher blockCipher;
|
||||
result.decryptedBody = blockCipher.decryptAesBlockCipher(encryptedResponseBody, key, iv, "", salt);
|
||||
result.isDecryptionSuccessful = true;
|
||||
} else {
|
||||
} catch (...) {
|
||||
result.decryptedBody = encryptedResponseBody;
|
||||
result.isDecryptionSuccessful = false;
|
||||
}
|
||||
@@ -314,6 +329,7 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
|
||||
}
|
||||
|
||||
if (!decryptionResult.isDecryptionSuccessful) {
|
||||
Utils::logException();
|
||||
qCritical() << "error when decrypting the request body";
|
||||
promise->addResult(qMakePair(ErrorCode::ApiConfigDecryptionError, QByteArray()));
|
||||
promise->finish();
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include "core/models/protocolConfig.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDebug>
|
||||
#include <QEventLoop>
|
||||
#include <QFutureWatcher>
|
||||
@@ -21,6 +20,7 @@
|
||||
#include "core/installers/sftpInstaller.h"
|
||||
#include "core/installers/socks5Installer.h"
|
||||
#include "core/installers/mtProxyInstaller.h"
|
||||
#include "core/configurators/xrayConfigurator.h"
|
||||
#include "core/installers/telemtInstaller.h"
|
||||
#include "core/installers/torInstaller.h"
|
||||
#include "core/installers/wireguardInstaller.h"
|
||||
@@ -193,6 +193,16 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
|
||||
bool reinstallRequired = isReinstallContainerRequired(container, oldConfig, newConfig);
|
||||
qDebug() << "InstallController::updateServerConfig for container" << container << "reinstall required is" << reinstallRequired;
|
||||
|
||||
bool xrayServerSettingsChanged = false;
|
||||
if (container == DockerContainer::Xray || container == DockerContainer::SSXray) {
|
||||
const auto *oldXrayConfig = oldConfig.getXrayProtocolConfig();
|
||||
const auto *newXrayConfig = newConfig.getXrayProtocolConfig();
|
||||
if (oldXrayConfig && newXrayConfig) {
|
||||
xrayServerSettingsChanged =
|
||||
!oldXrayConfig->serverConfig.hasEqualServerSettings(newXrayConfig->serverConfig);
|
||||
}
|
||||
}
|
||||
|
||||
ErrorCode errorCode = ErrorCode::NoError;
|
||||
if (reinstallRequired) {
|
||||
errorCode = setupContainer(credentials, container, newConfig, true);
|
||||
@@ -209,6 +219,21 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
|
||||
}
|
||||
}
|
||||
|
||||
const bool skipXrayInboundSync =
|
||||
newConfig.getXrayProtocolConfig() && newConfig.getXrayProtocolConfig()->serverConfig.isThirdPartyConfig;
|
||||
|
||||
if (errorCode == ErrorCode::NoError && xrayServerSettingsChanged && !skipXrayInboundSync) {
|
||||
DnsSettings dnsSettings = { m_appSettingsRepository->primaryDns(), m_appSettingsRepository->secondaryDns() };
|
||||
XrayConfigurator xrayConfigurator(&sshSession);
|
||||
qDebug() << "InstallController::updateServerConfig applying Xray server inbound sync, reinstall="
|
||||
<< reinstallRequired;
|
||||
errorCode = xrayConfigurator.applyServerSettingsToRemote(credentials, container, newConfig, dnsSettings, false);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
qDebug() << "InstallController::updateServerConfig Xray inbound sync failed, error="
|
||||
<< static_cast<int>(errorCode);
|
||||
}
|
||||
}
|
||||
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
if (container == DockerContainer::MtProxy) {
|
||||
MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
|
||||
@@ -697,7 +722,13 @@ bool InstallController::isReinstallContainerRequired(DockerContainer container,
|
||||
const auto *newXrayConfig = newConfig.getXrayProtocolConfig();
|
||||
|
||||
if (oldXrayConfig && newXrayConfig) {
|
||||
if (!oldXrayConfig->serverConfig.hasEqualServerSettings(newXrayConfig->serverConfig)) {
|
||||
const QString oldPort = oldXrayConfig->serverConfig.port.isEmpty()
|
||||
? QString(protocols::xray::defaultPort)
|
||||
: oldXrayConfig->serverConfig.port;
|
||||
const QString newPort = newXrayConfig->serverConfig.port.isEmpty()
|
||||
? QString(protocols::xray::defaultPort)
|
||||
: newXrayConfig->serverConfig.port;
|
||||
if (oldPort != newPort) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,6 +144,10 @@ QJsonObject ApiConfig::toJson() const
|
||||
obj[apiDefs::key::availableCountries] = availableCountries;
|
||||
}
|
||||
|
||||
if (!supportedProtocols.isEmpty()) {
|
||||
obj[apiDefs::key::supportedProtocols] = supportedProtocols;
|
||||
}
|
||||
|
||||
QJsonObject serviceInfoObj = serviceInfo.toJson();
|
||||
if (!serviceInfoObj.isEmpty()) {
|
||||
obj[apiDefs::key::serviceInfo] = serviceInfoObj;
|
||||
@@ -194,6 +198,7 @@ ApiConfig ApiConfig::fromJson(const QJsonObject& json)
|
||||
config.issuedConfigs = json.value(apiDefs::key::issuedConfigs).toInt(0);
|
||||
|
||||
config.availableCountries = json.value(apiDefs::key::availableCountries).toArray();
|
||||
config.supportedProtocols = json.value(apiDefs::key::supportedProtocols).toArray();
|
||||
|
||||
QJsonObject serviceInfoObj = json.value(apiDefs::key::serviceInfo).toObject();
|
||||
if (!serviceInfoObj.isEmpty()) {
|
||||
|
||||
@@ -34,6 +34,7 @@ struct ApiConfig
|
||||
int maxDeviceCount;
|
||||
int issuedConfigs;
|
||||
QJsonArray availableCountries;
|
||||
QJsonArray supportedProtocols;
|
||||
|
||||
struct ServiceInfo {
|
||||
bool isAdVisible = false;
|
||||
|
||||
@@ -121,11 +121,7 @@ QJsonObject ApiV2ServerConfig::toJson() const
|
||||
if (!dns2.isEmpty()) {
|
||||
obj[configKey::dns2] = dns2;
|
||||
}
|
||||
|
||||
if (!sendPayload.isEmpty()) {
|
||||
obj[configKey::sendPayload] = sendPayload;
|
||||
}
|
||||
|
||||
|
||||
if (crc > 0) {
|
||||
obj[configKey::crc] = crc;
|
||||
}
|
||||
@@ -169,7 +165,6 @@ ApiV2ServerConfig ApiV2ServerConfig::fromJson(const QJsonObject& json)
|
||||
|
||||
config.dns1 = json.value(configKey::dns1).toString();
|
||||
config.dns2 = json.value(configKey::dns2).toString();
|
||||
config.sendPayload = json.value(configKey::sendPayload).toArray();
|
||||
|
||||
config.crc = json.value(configKey::crc).toInt(0);
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#ifndef APIV2SERVERCONFIG_H
|
||||
#define APIV2SERVERCONFIG_H
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QMap>
|
||||
#include <QPair>
|
||||
@@ -29,7 +28,6 @@ struct ApiV2ServerConfig {
|
||||
DockerContainer defaultContainer;
|
||||
QString dns1;
|
||||
QString dns2;
|
||||
QJsonArray sendPayload;
|
||||
|
||||
QString name;
|
||||
bool nameOverriddenByUser = false;
|
||||
|
||||
@@ -18,8 +18,8 @@ namespace apiDefs
|
||||
constexpr QLatin1String stackType("stack_type");
|
||||
constexpr QLatin1String cliVersion("cli_version");
|
||||
constexpr QLatin1String cliName("cli_name");
|
||||
constexpr QLatin1String supportedProtocols("supported_protocols");
|
||||
constexpr QLatin1String availableCountries("available_countries");
|
||||
constexpr QLatin1String availableProtocols("available_protocols");
|
||||
constexpr QLatin1String installationUuid("installation_uuid");
|
||||
constexpr QLatin1String uuid("installation_uuid");
|
||||
constexpr QLatin1String osVersion("os_version");
|
||||
|
||||
@@ -25,13 +25,6 @@ namespace amnezia
|
||||
constexpr QLatin1String config("config");
|
||||
constexpr QLatin1String configVersion("config_version");
|
||||
|
||||
constexpr QLatin1String sendPayload("send_payload");
|
||||
constexpr QLatin1String sendPayloadEndpoint("endpoint");
|
||||
constexpr QLatin1String sendPayloadTimeoutMs("timeout_ms");
|
||||
constexpr QLatin1String sendPayloadProtocol("protocol");
|
||||
constexpr QLatin1String sendPayloadData("payload");
|
||||
constexpr QLatin1String sendPayloadExpectedResponse("expected_response");
|
||||
|
||||
constexpr QLatin1String containers("containers");
|
||||
constexpr QLatin1String container("container");
|
||||
constexpr QLatin1String defaultContainer("defaultContainer");
|
||||
|
||||
@@ -108,6 +108,8 @@ namespace amnezia
|
||||
ApiCaptchaInvalidError = 1118,
|
||||
ApiCaptchaRefreshError = 1119,
|
||||
ApiRateLimitError = 1120,
|
||||
ApiNoPurchasesToRestore = 1121,
|
||||
ApiPurchasePendingError = 1122,
|
||||
|
||||
// QFile errors
|
||||
OpenError = 1200,
|
||||
@@ -115,7 +117,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)
|
||||
}
|
||||
|
||||
@@ -100,6 +100,16 @@ QString errorString(ErrorCode code) {
|
||||
case (ErrorCode::ApiCaptchaInvalidError): errorMessage = QObject::tr("CAPTCHA was incorrect. Please try again"); break;
|
||||
case (ErrorCode::ApiCaptchaRefreshError): errorMessage = QObject::tr("CAPTCHA refreshed. Please try again"); break;
|
||||
case (ErrorCode::ApiRateLimitError): errorMessage = QObject::tr("Too many requests. Please try again later"); break;
|
||||
case (ErrorCode::ApiPurchasePendingError): errorMessage = QObject::tr("Your payment is pending confirmation in Google Play. Please complete the payment and then restore your subscription."); 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;
|
||||
@@ -109,6 +119,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;
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
#include "payloadSender.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QEventLoop>
|
||||
#include <QHostAddress>
|
||||
#include <QJsonObject>
|
||||
#include <QRandomGenerator>
|
||||
#include <QRegularExpression>
|
||||
#include <QTcpSocket>
|
||||
#include <QTimer>
|
||||
#include <QUdpSocket>
|
||||
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr char protoUdp[] = "udp";
|
||||
constexpr char protoTcp[] = "tcp";
|
||||
|
||||
QByteArray randomBytes(int count)
|
||||
{
|
||||
if (count <= 0) {
|
||||
return {};
|
||||
}
|
||||
QByteArray bytes(count, Qt::Uninitialized);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
bytes[i] = static_cast<char>(QRandomGenerator::global()->bounded(256));
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
bool parseEndpoint(const QString &endpoint, QString &host, quint16 &port)
|
||||
{
|
||||
const int separatorIndex = endpoint.lastIndexOf(QLatin1Char(':'));
|
||||
if (separatorIndex <= 0 || separatorIndex == endpoint.size() - 1) {
|
||||
return false;
|
||||
}
|
||||
host = endpoint.left(separatorIndex);
|
||||
bool ok = false;
|
||||
const uint parsedPort = endpoint.mid(separatorIndex + 1).toUInt(&ok);
|
||||
if (!ok || parsedPort == 0 || parsedPort > 65535) {
|
||||
return false;
|
||||
}
|
||||
port = static_cast<quint16>(parsedPort);
|
||||
return true;
|
||||
}
|
||||
|
||||
enum class ExchangeResult { Sent, Matched, Mismatch, Timeout };
|
||||
|
||||
ExchangeResult waitForResponse(QAbstractSocket &socket, const QByteArray &expectedResponse, int timeoutMs)
|
||||
{
|
||||
if (expectedResponse.isEmpty()) {
|
||||
return ExchangeResult::Sent;
|
||||
}
|
||||
|
||||
QByteArray response;
|
||||
QEventLoop loop;
|
||||
QTimer timer;
|
||||
timer.setSingleShot(true);
|
||||
QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
|
||||
QObject::connect(&socket, &QIODevice::readyRead, &loop, [&]() {
|
||||
response.append(socket.readAll());
|
||||
if (response.size() >= expectedResponse.size()) {
|
||||
loop.quit();
|
||||
}
|
||||
});
|
||||
|
||||
timer.start(timeoutMs);
|
||||
loop.exec();
|
||||
|
||||
if (response.isEmpty()) {
|
||||
return ExchangeResult::Timeout;
|
||||
}
|
||||
return response == expectedResponse ? ExchangeResult::Matched : ExchangeResult::Mismatch;
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray PayloadSender::buildPayload(const QString &tagString)
|
||||
{
|
||||
QByteArray result;
|
||||
if (tagString.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
static const QRegularExpression tagRegExp(QStringLiteral("<([^>]*)>"));
|
||||
QRegularExpressionMatchIterator it = tagRegExp.globalMatch(tagString);
|
||||
while (it.hasNext()) {
|
||||
const QRegularExpressionMatch match = it.next();
|
||||
const QStringList parts = match.captured(1).simplified().split(QLatin1Char(' '), Qt::SkipEmptyParts);
|
||||
if (parts.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const QString tag = parts.at(0).toLower();
|
||||
if (tag == QLatin1String("b")) {
|
||||
if (parts.size() < 2) {
|
||||
continue;
|
||||
}
|
||||
QString hex = parts.at(1);
|
||||
if (hex.startsWith(QLatin1String("0x"), Qt::CaseInsensitive)) {
|
||||
hex = hex.mid(2);
|
||||
}
|
||||
result.append(QByteArray::fromHex(hex.toLatin1()));
|
||||
} else if (tag == QLatin1String("r")) {
|
||||
if (parts.size() < 2) {
|
||||
continue;
|
||||
}
|
||||
bool ok = false;
|
||||
const int count = parts.at(1).toInt(&ok);
|
||||
if (ok) {
|
||||
result.append(randomBytes(count));
|
||||
}
|
||||
} else {
|
||||
qWarning() << "PayloadSender: unknown payload tag" << tag;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void PayloadSender::sendAll(const QJsonArray &sendPayload)
|
||||
{
|
||||
for (int i = 0; i < sendPayload.size(); ++i) {
|
||||
const QJsonValue value = sendPayload.at(i);
|
||||
if (!value.isObject()) {
|
||||
continue;
|
||||
}
|
||||
sendEntry(value.toObject(), i);
|
||||
}
|
||||
}
|
||||
|
||||
void PayloadSender::sendEntry(const QJsonObject &entry, int index)
|
||||
{
|
||||
const QString endpoint = entry.value(configKey::sendPayloadEndpoint).toString();
|
||||
QString host;
|
||||
quint16 port = 0;
|
||||
if (!parseEndpoint(endpoint, host, port)) {
|
||||
qWarning() << "PayloadSender: skipping entry" << index << "- invalid endpoint" << endpoint;
|
||||
return;
|
||||
}
|
||||
|
||||
const QString protocol = entry.value(configKey::sendPayloadProtocol).toString().toLower();
|
||||
const int timeoutMs = entry.value(configKey::sendPayloadTimeoutMs).toInt();
|
||||
const QByteArray payload = buildPayload(entry.value(configKey::sendPayloadData).toString());
|
||||
const QByteArray expectedResponse = buildPayload(entry.value(configKey::sendPayloadExpectedResponse).toString());
|
||||
|
||||
const QString resolvedHost = NetworkUtilities::getIPAddress(host);
|
||||
const QHostAddress hostAddress(resolvedHost.isEmpty() ? host : resolvedHost);
|
||||
|
||||
QUdpSocket udpSocket;
|
||||
QTcpSocket tcpSocket;
|
||||
QAbstractSocket *socket = nullptr;
|
||||
if (protocol == protoUdp) {
|
||||
socket = &udpSocket;
|
||||
if (udpSocket.writeDatagram(payload, hostAddress, port) < 0) {
|
||||
qWarning() << "PayloadSender: entry" << index << "- udp write failed to" << endpoint << udpSocket.errorString();
|
||||
return;
|
||||
}
|
||||
} else if (protocol == protoTcp) {
|
||||
socket = &tcpSocket;
|
||||
tcpSocket.connectToHost(hostAddress, port);
|
||||
if (!tcpSocket.waitForConnected(timeoutMs)) {
|
||||
qWarning() << "PayloadSender: entry" << index << "- tcp connect failed to" << endpoint << tcpSocket.errorString();
|
||||
return;
|
||||
}
|
||||
if (tcpSocket.write(payload) != payload.size()) {
|
||||
qWarning() << "PayloadSender: entry" << index << "- tcp write failed to" << endpoint << tcpSocket.errorString();
|
||||
return;
|
||||
}
|
||||
tcpSocket.flush();
|
||||
} else {
|
||||
qWarning() << "PayloadSender: skipping entry" << index << "- unsupported protocol" << protocol;
|
||||
return;
|
||||
}
|
||||
|
||||
switch (waitForResponse(*socket, expectedResponse, timeoutMs)) {
|
||||
case ExchangeResult::Sent:
|
||||
qInfo() << "PayloadSender: entry" << index << "-" << protocol << "payload sent to" << endpoint;
|
||||
break;
|
||||
case ExchangeResult::Matched:
|
||||
qInfo() << "PayloadSender: entry" << index << "-" << protocol << "expected response received from" << endpoint;
|
||||
break;
|
||||
case ExchangeResult::Mismatch:
|
||||
qWarning() << "PayloadSender: entry" << index << "-" << protocol << "response mismatch from" << endpoint;
|
||||
break;
|
||||
case ExchangeResult::Timeout:
|
||||
qWarning() << "PayloadSender: entry" << index << "-" << protocol << "response timeout from" << endpoint;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
#ifndef PAYLOADSENDER_H
|
||||
#define PAYLOADSENDER_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QJsonArray>
|
||||
#include <QString>
|
||||
|
||||
class PayloadSender
|
||||
{
|
||||
public:
|
||||
static void sendAll(const QJsonArray &sendPayload);
|
||||
static QByteArray buildPayload(const QString &tagString);
|
||||
|
||||
private:
|
||||
static void sendEntry(const QJsonObject &entry, int index);
|
||||
};
|
||||
|
||||
#endif // PAYLOADSENDER_H
|
||||
@@ -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 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17 11L21 7L17 3" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M21 7H9" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7 21L3 17L7 13" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M15 17H3" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 525 B |
@@ -10,7 +10,6 @@
|
||||
<file>controls/app.svg</file>
|
||||
<file>controls/archive-restore.svg</file>
|
||||
<file>controls/arrow-left.svg</file>
|
||||
<file>controls/arrow-left-right.svg</file>
|
||||
<file>controls/arrow-right.svg</file>
|
||||
<file>controls/bug.svg</file>
|
||||
<file>controls/check.svg</file>
|
||||
|
||||
@@ -328,6 +328,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);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#include "secureQSettings.h"
|
||||
|
||||
#include "../client/3rd/QSimpleCrypto/src/include/QAead.h"
|
||||
#include "../client/3rd/QSimpleCrypto/src/include/QBlockCipher.h"
|
||||
#include "core/utils/utilities.h"
|
||||
#include "cryptoUtils.h"
|
||||
#include <QDataStream>
|
||||
#include <QDebug>
|
||||
#include <QEventLoop>
|
||||
@@ -179,8 +180,11 @@ void SecureQSettings::clearSettings()
|
||||
|
||||
QByteArray SecureQSettings::encryptText(const QByteArray &value) const
|
||||
{
|
||||
QByteArray result = CryptoUtils::encryptAes256Cbc(value, getEncKey(), getEncIv());
|
||||
if (result.isEmpty() && !value.isEmpty()) {
|
||||
QSimpleCrypto::QBlockCipher cipher;
|
||||
QByteArray result;
|
||||
try {
|
||||
result = cipher.encryptAesBlockCipher(value, getEncKey(), getEncIv());
|
||||
} catch (...) { // todo change error handling in QSimpleCrypto?
|
||||
qCritical() << "error when encrypting the settings value";
|
||||
}
|
||||
return result;
|
||||
@@ -188,8 +192,11 @@ QByteArray SecureQSettings::encryptText(const QByteArray &value) const
|
||||
|
||||
QByteArray SecureQSettings::decryptText(const QByteArray &ba) const
|
||||
{
|
||||
QByteArray result = CryptoUtils::decryptAes256Cbc(ba, getEncKey(), getEncIv());
|
||||
if (result.isEmpty() && !ba.isEmpty()) {
|
||||
QSimpleCrypto::QBlockCipher cipher;
|
||||
QByteArray result;
|
||||
try {
|
||||
result = cipher.decryptAesBlockCipher(ba, getEncKey(), getEncIv());
|
||||
} catch (...) { // todo change error handling in QSimpleCrypto?
|
||||
qCritical() << "error when decrypting the settings value";
|
||||
}
|
||||
return result;
|
||||
@@ -218,7 +225,8 @@ QByteArray SecureQSettings::getEncKey() const
|
||||
|
||||
if (m_key.isEmpty()) {
|
||||
// Create new key
|
||||
QByteArray key = CryptoUtils::generateRandomBytes(32);
|
||||
QSimpleCrypto::QBlockCipher cipher;
|
||||
QByteArray key = cipher.generatePrivateSalt(32);
|
||||
if (key.isEmpty()) {
|
||||
qCritical() << "SecureQSettings::getEncKey Unable to generate new enc key";
|
||||
}
|
||||
@@ -246,7 +254,8 @@ QByteArray SecureQSettings::getEncIv() const
|
||||
|
||||
if (m_iv.isEmpty()) {
|
||||
// Create new IV
|
||||
QByteArray iv = CryptoUtils::generateRandomBytes(32);
|
||||
QSimpleCrypto::QBlockCipher cipher;
|
||||
QByteArray iv = cipher.generatePrivateSalt(32);
|
||||
if (iv.isEmpty()) {
|
||||
qCritical() << "SecureQSettings::getEncIv Unable to generate new enc IV";
|
||||
}
|
||||
|
||||
@@ -227,6 +227,36 @@ bool SubscriptionUiController::importPremiumFromAppStore(const QString &storePro
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SubscriptionUiController::importPremiumFromPlayMarket(const QString &storeProductId)
|
||||
{
|
||||
#if defined(Q_OS_ANDROID)
|
||||
QString productId = storeProductId.trimmed();
|
||||
if (productId.isEmpty()) {
|
||||
productId = QStringLiteral("premium");
|
||||
}
|
||||
|
||||
int duplicateServerIndex = -1;
|
||||
ErrorCode errorCode = m_subscriptionController->processPlayMarketPurchase(
|
||||
m_apiServicesModel->getCountryCode(),
|
||||
m_apiServicesModel->getSelectedServiceType(),
|
||||
m_apiServicesModel->getSelectedServiceProtocol(),
|
||||
productId,
|
||||
&duplicateServerIndex);
|
||||
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
if (errorCode == ErrorCode::ApiConfigAlreadyAdded) {
|
||||
emit installServerFromApiFinished(tr("This subscription has already been added"), duplicateServerIndex);
|
||||
return true;
|
||||
}
|
||||
emit errorOccurred(errorCode);
|
||||
return false;
|
||||
}
|
||||
|
||||
emit installServerFromApiFinished(tr("%1 has been added to the app").arg(m_apiServicesModel->getSelectedServiceName()));
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SubscriptionUiController::restoreServiceFromAppStore()
|
||||
{
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
@@ -281,6 +311,59 @@ bool SubscriptionUiController::restoreServiceFromAppStore()
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SubscriptionUiController::restoreServiceFromPlayMarket()
|
||||
{
|
||||
#if defined(Q_OS_ANDROID)
|
||||
const QString premiumServiceType = QStringLiteral("amnezia-premium");
|
||||
|
||||
if (!fillAvailableServices()) {
|
||||
qWarning().noquote() << "[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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
SubscriptionController::PlayMarketRestoreResult result = m_subscriptionController->processPlayMarketRestore(
|
||||
m_apiServicesModel->getCountryCode(),
|
||||
m_apiServicesModel->getSelectedServiceType(),
|
||||
m_apiServicesModel->getSelectedServiceProtocol());
|
||||
|
||||
if (!result.hasInstalledConfig) {
|
||||
if (result.duplicateConfigAlreadyPresent) {
|
||||
emit installServerFromApiFinished(tr("This subscription has already been added"), result.duplicateServerIndex);
|
||||
return true;
|
||||
}
|
||||
emit errorOccurred(result.errorCode);
|
||||
return false;
|
||||
}
|
||||
|
||||
emit installServerFromApiFinished(tr("Subscription restored successfully."));
|
||||
if (result.duplicateCount > 0) {
|
||||
qInfo().noquote() << "[Billing] Skipped" << result.duplicateCount
|
||||
<< "duplicate restored purchases for tokens already processed";
|
||||
}
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SubscriptionUiController::importFreeFromGateway()
|
||||
{
|
||||
QString userCountryCode = m_apiServicesModel->getCountryCode();
|
||||
@@ -303,8 +386,6 @@ bool SubscriptionUiController::importFreeFromGateway()
|
||||
emit installServerFromApiFinished(tr("%1 installed successfully.").arg(m_apiServicesModel->getSelectedServiceName()));
|
||||
return true;
|
||||
} else if (errorCode == ErrorCode::ApiCaptchaRequiredError && captchaInfo.isRequired) {
|
||||
m_captchaState = CaptchaState{};
|
||||
m_captchaState.flow = CaptchaFlow::Import;
|
||||
m_captchaState.userCountryCode = userCountryCode;
|
||||
m_captchaState.serviceType = serviceType;
|
||||
m_captchaState.serviceProtocol = serviceProtocol;
|
||||
@@ -329,11 +410,6 @@ void SubscriptionUiController::onCaptchaSolved(const QString &captchaId, const Q
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_captchaState.flow == CaptchaFlow::Update) {
|
||||
resolveUpdateCaptcha(captchaId, solution);
|
||||
return;
|
||||
}
|
||||
|
||||
SubscriptionController::ProtocolData protocolData;
|
||||
protocolData.certPrivKey = m_captchaState.openvpnPrivKey;
|
||||
protocolData.wireGuardClientPrivKey = m_captchaState.wireguardClientPrivKey;
|
||||
@@ -375,33 +451,6 @@ void SubscriptionUiController::onRefreshCaptchaRequested()
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_captchaState.flow == CaptchaFlow::Update) {
|
||||
SubscriptionController::CaptchaInfo captchaInfo;
|
||||
SubscriptionController::ProtocolData usedProtocolData;
|
||||
ErrorCode errorCode = m_subscriptionController->updateServiceFromGateway(
|
||||
m_captchaState.serverId,
|
||||
m_captchaState.newCountryCode,
|
||||
m_captchaState.isConnectEvent,
|
||||
&captchaInfo,
|
||||
&usedProtocolData);
|
||||
|
||||
if (errorCode == ErrorCode::ApiCaptchaRequiredError && captchaInfo.isRequired) {
|
||||
m_captchaState.updateProtocolData = usedProtocolData;
|
||||
emit captchaRequired(captchaInfo.captchaId, captchaInfo.captchaImageBase64,
|
||||
captchaInfo.hint.isEmpty() ? tr("Enter the digits from the image to continue") : captchaInfo.hint);
|
||||
} else if (errorCode == ErrorCode::NoError) {
|
||||
emitCaptchaUpdateSuccess();
|
||||
} else {
|
||||
m_captchaState.isPending = false;
|
||||
if (errorCode == ErrorCode::ApiSubscriptionExpiredError) {
|
||||
emit subscriptionExpiredOnServer();
|
||||
} else {
|
||||
emit errorOccurred(errorCode);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
SubscriptionController::ProtocolData protocolData;
|
||||
protocolData.certPrivKey = m_captchaState.openvpnPrivKey;
|
||||
protocolData.wireGuardClientPrivKey = m_captchaState.wireguardClientPrivKey;
|
||||
@@ -426,39 +475,6 @@ void SubscriptionUiController::onRefreshCaptchaRequested()
|
||||
}
|
||||
}
|
||||
|
||||
void SubscriptionUiController::resolveUpdateCaptcha(const QString &captchaId, const QString &solution)
|
||||
{
|
||||
SubscriptionController::CaptchaInfo retryCaptcha;
|
||||
ErrorCode errorCode = m_subscriptionController->resolveUpdateServiceCaptcha(
|
||||
m_captchaState.serverId,
|
||||
m_captchaState.newCountryCode,
|
||||
m_captchaState.isConnectEvent,
|
||||
m_captchaState.updateProtocolData,
|
||||
captchaId,
|
||||
solution,
|
||||
&retryCaptcha);
|
||||
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
emitCaptchaUpdateSuccess();
|
||||
return;
|
||||
}
|
||||
|
||||
if ((errorCode == ErrorCode::ApiCaptchaInvalidError || errorCode == ErrorCode::ApiCaptchaRefreshError
|
||||
|| errorCode == ErrorCode::ApiCaptchaRequiredError)
|
||||
&& retryCaptcha.isRequired) {
|
||||
emit captchaRequired(retryCaptcha.captchaId, retryCaptcha.captchaImageBase64,
|
||||
retryCaptcha.hint.isEmpty() ? tr("Enter the digits from the image to continue") : retryCaptcha.hint);
|
||||
return;
|
||||
}
|
||||
|
||||
m_captchaState.isPending = false;
|
||||
if (errorCode == ErrorCode::ApiSubscriptionExpiredError) {
|
||||
emit subscriptionExpiredOnServer();
|
||||
} else {
|
||||
emit errorOccurred(errorCode);
|
||||
}
|
||||
}
|
||||
|
||||
bool SubscriptionUiController::importTrialFromGateway(const QString &email)
|
||||
{
|
||||
emit trialEmailError(QString());
|
||||
@@ -490,29 +506,20 @@ bool SubscriptionUiController::updateServiceFromGateway(const QString &serverId,
|
||||
|| oldApiV2->apiConfig.isSubscriptionExpired();
|
||||
}
|
||||
|
||||
SubscriptionController::CaptchaInfo captchaInfo;
|
||||
SubscriptionController::ProtocolData usedProtocolData;
|
||||
ErrorCode errorCode = m_subscriptionController->updateServiceFromGateway(serverId, newCountryCode, isConnectEvent,
|
||||
&captchaInfo, &usedProtocolData);
|
||||
ErrorCode errorCode = m_subscriptionController->updateServiceFromGateway(serverId, newCountryCode, isConnectEvent);
|
||||
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
emitUpdateSuccess(wasSubscriptionExpired, reloadServiceConfig, newCountryName);
|
||||
if (wasSubscriptionExpired) {
|
||||
emit subscriptionRefreshNeeded();
|
||||
}
|
||||
if (reloadServiceConfig) {
|
||||
emit reloadServerFromApiFinished(tr("API config reloaded"));
|
||||
} else if (newCountryName.isEmpty()) {
|
||||
emit updateServerFromApiFinished();
|
||||
} else {
|
||||
emit changeApiCountryFinished(tr("Successfully changed the country of connection to %1").arg(newCountryName));
|
||||
}
|
||||
return true;
|
||||
} else if (errorCode == ErrorCode::ApiCaptchaRequiredError && captchaInfo.isRequired) {
|
||||
m_captchaState = CaptchaState{};
|
||||
m_captchaState.flow = CaptchaFlow::Update;
|
||||
m_captchaState.serverId = serverId;
|
||||
m_captchaState.newCountryCode = newCountryCode;
|
||||
m_captchaState.newCountryName = newCountryName;
|
||||
m_captchaState.isConnectEvent = isConnectEvent;
|
||||
m_captchaState.reloadServiceConfig = reloadServiceConfig;
|
||||
m_captchaState.wasSubscriptionExpired = wasSubscriptionExpired;
|
||||
m_captchaState.updateProtocolData = usedProtocolData;
|
||||
m_captchaState.isPending = true;
|
||||
|
||||
emit captchaRequired(captchaInfo.captchaId, captchaInfo.captchaImageBase64,
|
||||
captchaInfo.hint.isEmpty() ? tr("Enter the digits from the image to continue") : captchaInfo.hint);
|
||||
return false;
|
||||
} else {
|
||||
if (errorCode == ErrorCode::ApiSubscriptionExpiredError) {
|
||||
emit subscriptionExpiredOnServer();
|
||||
@@ -523,37 +530,6 @@ bool SubscriptionUiController::updateServiceFromGateway(const QString &serverId,
|
||||
}
|
||||
}
|
||||
|
||||
void SubscriptionUiController::emitUpdateSuccess(bool wasSubscriptionExpired, bool reloadServiceConfig, const QString &newCountryName)
|
||||
{
|
||||
if (wasSubscriptionExpired) {
|
||||
emit subscriptionRefreshNeeded();
|
||||
}
|
||||
if (reloadServiceConfig) {
|
||||
emit reloadServerFromApiFinished(tr("API config reloaded"));
|
||||
} else if (newCountryName.isEmpty()) {
|
||||
emit updateServerFromApiFinished();
|
||||
} else {
|
||||
emit changeApiCountryFinished(tr("Successfully changed the country of connection to %1").arg(newCountryName));
|
||||
}
|
||||
}
|
||||
|
||||
void SubscriptionUiController::emitCaptchaUpdateSuccess()
|
||||
{
|
||||
const bool fromValidateConfig = m_captchaState.fromValidateConfig;
|
||||
const bool wasSubscriptionExpired = m_captchaState.wasSubscriptionExpired;
|
||||
const bool reloadServiceConfig = m_captchaState.reloadServiceConfig;
|
||||
const QString newCountryName = m_captchaState.newCountryName;
|
||||
|
||||
m_captchaState.isPending = false;
|
||||
emit captchaFlowDismissRequested();
|
||||
|
||||
if (fromValidateConfig) {
|
||||
emit configValidated(true);
|
||||
return;
|
||||
}
|
||||
emitUpdateSuccess(wasSubscriptionExpired, reloadServiceConfig, newCountryName);
|
||||
}
|
||||
|
||||
|
||||
bool SubscriptionUiController::deactivateDevice(const QString &serverId)
|
||||
{
|
||||
@@ -589,25 +565,7 @@ void SubscriptionUiController::validateConfig()
|
||||
|
||||
bool hasInstalledContainers = m_serversController->hasInstalledContainers(serverId);
|
||||
|
||||
SubscriptionController::CaptchaInfo captchaInfo;
|
||||
SubscriptionController::ProtocolData usedProtocolData;
|
||||
ErrorCode errorCode = m_subscriptionController->validateAndUpdateConfig(serverId, hasInstalledContainers,
|
||||
&captchaInfo, &usedProtocolData);
|
||||
|
||||
if (errorCode == ErrorCode::ApiCaptchaRequiredError && captchaInfo.isRequired) {
|
||||
m_captchaState = CaptchaState{};
|
||||
m_captchaState.flow = CaptchaFlow::Update;
|
||||
m_captchaState.fromValidateConfig = true;
|
||||
m_captchaState.serverId = serverId;
|
||||
m_captchaState.isConnectEvent = true;
|
||||
m_captchaState.updateProtocolData = usedProtocolData;
|
||||
m_captchaState.isPending = true;
|
||||
|
||||
emit captchaRequired(captchaInfo.captchaId, captchaInfo.captchaImageBase64,
|
||||
captchaInfo.hint.isEmpty() ? tr("Enter the digits from the image to continue") : captchaInfo.hint);
|
||||
emit configValidated(false);
|
||||
return;
|
||||
}
|
||||
ErrorCode errorCode = m_subscriptionController->validateAndUpdateConfig(serverId, hasInstalledContainers);
|
||||
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
if (errorCode == ErrorCode::ApiSubscriptionExpiredError) {
|
||||
@@ -633,18 +591,6 @@ bool SubscriptionUiController::isVlessProtocol(const QString &serverId)
|
||||
}
|
||||
|
||||
|
||||
QString SubscriptionUiController::currentProtocol(const QString &serverId)
|
||||
{
|
||||
return m_subscriptionController->currentProtocol(serverId);
|
||||
}
|
||||
|
||||
|
||||
QStringList SubscriptionUiController::availableProtocols(const QString &serverId)
|
||||
{
|
||||
return m_subscriptionController->availableProtocols(serverId);
|
||||
}
|
||||
|
||||
|
||||
void SubscriptionUiController::removeApiConfig(const QString &serverId)
|
||||
{
|
||||
m_subscriptionController->removeApiConfig(serverId);
|
||||
|
||||
@@ -45,8 +45,10 @@ public slots:
|
||||
|
||||
bool fillAvailableServices();
|
||||
bool importPremiumFromAppStore(const QString &storeProductId);
|
||||
bool importPremiumFromPlayMarket(const QString &storeProductId);
|
||||
bool importFreeFromGateway();
|
||||
bool restoreServiceFromAppStore();
|
||||
bool restoreServiceFromPlayMarket();
|
||||
bool importTrialFromGateway(const QString &email);
|
||||
bool updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, const QString &newCountryName,
|
||||
bool reloadServiceConfig = false);
|
||||
@@ -57,8 +59,6 @@ public slots:
|
||||
|
||||
void setCurrentProtocol(const QString &serverId, const QString &protocolName);
|
||||
bool isVlessProtocol(const QString &serverId);
|
||||
QString currentProtocol(const QString &serverId);
|
||||
QStringList availableProtocols(const QString &serverId);
|
||||
|
||||
bool isCaptchaAwaitingUser() const;
|
||||
void onCaptchaSolved(const QString &captchaId, const QString &solution);
|
||||
@@ -97,15 +97,7 @@ signals:
|
||||
void unsupportedConnectDrawerRequested();
|
||||
|
||||
private:
|
||||
enum class CaptchaFlow {
|
||||
Import,
|
||||
Update
|
||||
};
|
||||
|
||||
struct CaptchaState {
|
||||
CaptchaFlow flow = CaptchaFlow::Import;
|
||||
|
||||
// Import flow
|
||||
QString userCountryCode;
|
||||
QString serviceType;
|
||||
QString serviceProtocol;
|
||||
@@ -113,25 +105,10 @@ private:
|
||||
QString wireguardClientPrivKey;
|
||||
QString wireguardClientPubKey;
|
||||
QString xrayUuid;
|
||||
|
||||
// Update flow
|
||||
QString serverId;
|
||||
QString newCountryCode;
|
||||
QString newCountryName;
|
||||
bool isConnectEvent = false;
|
||||
bool reloadServiceConfig = false;
|
||||
bool wasSubscriptionExpired = false;
|
||||
bool fromValidateConfig = false;
|
||||
SubscriptionController::ProtocolData updateProtocolData;
|
||||
|
||||
bool isPending = false;
|
||||
} m_captchaState;
|
||||
|
||||
private:
|
||||
void emitUpdateSuccess(bool wasSubscriptionExpired, bool reloadServiceConfig, const QString &newCountryName);
|
||||
void emitCaptchaUpdateSuccess();
|
||||
void resolveUpdateCaptcha(const QString &captchaId, const QString &solution);
|
||||
|
||||
QList<QString> getQrCodes();
|
||||
int getQrCodesCount();
|
||||
QString getVpnKey();
|
||||
|
||||
@@ -73,6 +73,12 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case IsProtocolSelectionSupportedRole: {
|
||||
if (m_accountInfoData.supportedProtocols.size() > 1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case IsSubscriptionExpiredRole: {
|
||||
if (m_accountInfoData.configType == serverConfigUtils::ConfigType::AmneziaFreeV3) {
|
||||
return false;
|
||||
@@ -126,6 +132,10 @@ void ApiAccountInfoModel::updateModel(const QJsonObject &accountInfoObject, cons
|
||||
accountInfoData.subscriptionDescription = accountInfoObject.value(apiDefs::key::subscriptionDescription).toString();
|
||||
accountInfoData.isRenewalAvailable = accountInfoObject.value(apiDefs::key::isRenewalAvailable).toBool(false);
|
||||
|
||||
for (const auto &protocol : accountInfoObject.value(apiDefs::key::supportedProtocols).toArray()) {
|
||||
accountInfoData.supportedProtocols.push_back(protocol.toString());
|
||||
}
|
||||
|
||||
m_accountInfoData = accountInfoData;
|
||||
|
||||
m_supportInfo = accountInfoObject.value(apiDefs::key::supportInfo).toObject();
|
||||
@@ -191,6 +201,7 @@ QHash<int, QByteArray> ApiAccountInfoModel::roleNames() const
|
||||
roles[IsComponentVisibleRole] = "isComponentVisible";
|
||||
roles[IsSubscriptionRenewalAvailableRole] = "isSubscriptionRenewalAvailable";
|
||||
roles[HasExpiredWorkerRole] = "hasExpiredWorker";
|
||||
roles[IsProtocolSelectionSupportedRole] = "isProtocolSelectionSupported";
|
||||
roles[IsSubscriptionExpiredRole] = "isSubscriptionExpired";
|
||||
roles[IsSubscriptionExpiringSoonRole] = "isSubscriptionExpiringSoon";
|
||||
roles[IsInAppPurchaseRole] = "isInAppPurchase";
|
||||
|
||||
@@ -22,6 +22,7 @@ public:
|
||||
IsComponentVisibleRole,
|
||||
IsSubscriptionRenewalAvailableRole,
|
||||
HasExpiredWorkerRole,
|
||||
IsProtocolSelectionSupportedRole,
|
||||
IsSubscriptionExpiredRole,
|
||||
IsSubscriptionExpiringSoonRole,
|
||||
IsInAppPurchaseRole
|
||||
@@ -57,6 +58,8 @@ private:
|
||||
|
||||
serverConfigUtils::ConfigType configType;
|
||||
|
||||
QStringList supportedProtocols;
|
||||
|
||||
QString subscriptionDescription;
|
||||
|
||||
bool isInAppPurchase = false;
|
||||
|
||||
@@ -20,42 +20,13 @@ Popup {
|
||||
signal captchaSolved(string captchaId, string solution)
|
||||
signal refreshCaptchaRequested()
|
||||
|
||||
readonly property int keyboardHeight: {
|
||||
if (PageController.imeHeight > 0) {
|
||||
return PageController.imeHeight
|
||||
}
|
||||
if (Qt.platform.os === "ios" && Qt.inputMethod.visible) {
|
||||
const r = Qt.inputMethod.keyboardRectangle
|
||||
if (r.height <= 0) {
|
||||
return 0
|
||||
}
|
||||
return Math.max(0, Math.round(parent.height - r.y))
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
leftMargin: 25
|
||||
rightMargin: 25
|
||||
bottomMargin: 70 + SettingsController.safeAreaBottomMargin
|
||||
|
||||
width: parent.width - leftMargin - rightMargin
|
||||
|
||||
x: Math.round((parent.width - width) / 2)
|
||||
y: {
|
||||
const centered = Math.round((parent.height - height) / 2)
|
||||
if (root.keyboardHeight === 0) {
|
||||
return centered
|
||||
}
|
||||
return Math.max(20, Math.min(centered, parent.height - root.keyboardHeight - height - 16))
|
||||
}
|
||||
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: 150
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
anchors.centerIn: parent
|
||||
modal: true
|
||||
closePolicy: Popup.NoAutoClose
|
||||
|
||||
@@ -66,7 +37,7 @@ Popup {
|
||||
onOpened: {
|
||||
timer.start()
|
||||
solutionField.textField.text = ""
|
||||
solutionField.textField.forceActiveFocus()
|
||||
solutionField.textField.focus = true
|
||||
}
|
||||
|
||||
onCaptchaIdChanged: {
|
||||
|
||||
@@ -4,7 +4,7 @@ import QtQuick.Controls
|
||||
Menu {
|
||||
property var textObj
|
||||
|
||||
popupType: Qt.platform.os === "ios" ? Popup.Item : Popup.Native
|
||||
popupType: Popup.Native
|
||||
|
||||
property Item inputBlocker: null
|
||||
|
||||
|
||||
@@ -93,9 +93,7 @@ Rectangle {
|
||||
|
||||
wrapMode: Text.Wrap
|
||||
|
||||
ContextMenu.menu: Qt.platform.os === "ios" ? null : contextMenu
|
||||
|
||||
ContextMenuType {
|
||||
ContextMenu.menu: ContextMenuType {
|
||||
id: contextMenu
|
||||
textObj: textArea
|
||||
}
|
||||
|
||||
@@ -79,9 +79,7 @@ Rectangle {
|
||||
|
||||
wrapMode: Text.Wrap
|
||||
|
||||
ContextMenu.menu: Qt.platform.os === "ios" ? null : contextMenu
|
||||
|
||||
ContextMenuType {
|
||||
ContextMenu.menu: ContextMenuType {
|
||||
id: contextMenu
|
||||
textObj: textArea
|
||||
}
|
||||
|
||||
@@ -144,9 +144,7 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
ContextMenu.menu: Qt.platform.os === "ios" ? null : contextMenu
|
||||
|
||||
ContextMenuType {
|
||||
ContextMenu.menu: ContextMenuType {
|
||||
id: contextMenu
|
||||
textObj: textField
|
||||
}
|
||||
|
||||
@@ -21,41 +21,6 @@ PageType {
|
||||
|
||||
property var containersDropDownRef: null
|
||||
|
||||
property var apiAvailableProtocols: []
|
||||
property string apiCurrentProtocol: ""
|
||||
|
||||
readonly property bool isApiProtocolSelectionVisible: ServersUiController.isDefaultServerFromApi && root.apiAvailableProtocols.length > 0
|
||||
|
||||
function updateApiProtocolState() {
|
||||
if (ServersUiController.isDefaultServerFromApi) {
|
||||
root.apiAvailableProtocols = SubscriptionUiController.availableProtocols(ServersUiController.defaultServerId)
|
||||
root.apiCurrentProtocol = SubscriptionUiController.currentProtocol(ServersUiController.defaultServerId)
|
||||
} else {
|
||||
root.apiAvailableProtocols = []
|
||||
root.apiCurrentProtocol = ""
|
||||
}
|
||||
}
|
||||
|
||||
function protocolDisplayName(protocol) {
|
||||
switch (protocol) {
|
||||
case "awg": return "AmneziaWG"
|
||||
case "vless": return "VLESS"
|
||||
default: return protocol
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
root.updateApiProtocolState()
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ServersUiController
|
||||
|
||||
function onDefaultServerIdChanged() {
|
||||
root.updateApiProtocolState()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Qt.application
|
||||
|
||||
@@ -245,10 +210,6 @@ PageType {
|
||||
drawer.collapsedHeight = collapsed.implicitHeight
|
||||
}
|
||||
|
||||
onImplicitHeightChanged: {
|
||||
drawer.collapsedHeight = collapsed.implicitHeight
|
||||
}
|
||||
|
||||
DividerType {
|
||||
Layout.topMargin: 10
|
||||
Layout.fillWidth: false
|
||||
@@ -350,7 +311,7 @@ PageType {
|
||||
objectName: "rowLayoutLabel"
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
Layout.topMargin: 8
|
||||
Layout.bottomMargin: root.isApiProtocolSelectionVisible ? 8 : (drawer.isCollapsedStateActive ? 44 : ServersUiController.isDefaultServerFromApi ? 61 : 16)
|
||||
Layout.bottomMargin: drawer.isCollapsedStateActive ? 44 : ServersUiController.isDefaultServerFromApi ? 61 : 16
|
||||
spacing: 0
|
||||
|
||||
BasicButtonType {
|
||||
@@ -404,61 +365,6 @@ PageType {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
objectName: "protocolRowLayout"
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
Layout.bottomMargin: drawer.isCollapsedStateActive ? 44 : 61
|
||||
spacing: 0
|
||||
|
||||
visible: root.isApiProtocolSelectionVisible
|
||||
|
||||
BasicButtonType {
|
||||
id: protocolButton
|
||||
objectName: "protocolButton"
|
||||
|
||||
enabled: root.apiAvailableProtocols.length > 1
|
||||
hoverEnabled: enabled
|
||||
|
||||
implicitHeight: 36
|
||||
|
||||
leftPadding: 16
|
||||
rightPadding: 16
|
||||
|
||||
defaultColor: AmneziaStyle.color.transparent
|
||||
hoveredColor: AmneziaStyle.color.translucentWhite
|
||||
pressedColor: AmneziaStyle.color.sheerWhite
|
||||
disabledColor: AmneziaStyle.color.transparent
|
||||
textColor: AmneziaStyle.color.mutedGray
|
||||
|
||||
buttonTextLabel.lineHeight: 16
|
||||
buttonTextLabel.font.pixelSize: 13
|
||||
buttonTextLabel.font.weight: 400
|
||||
|
||||
text: root.apiAvailableProtocols.length > 1
|
||||
? root.protocolDisplayName(root.apiCurrentProtocol)
|
||||
: root.protocolDisplayName(root.apiAvailableProtocols[0])
|
||||
leftImageSource: "qrc:/images/controls/arrow-left-right.svg"
|
||||
leftImageColor: AmneziaStyle.color.mutedGray
|
||||
|
||||
rightImageSource: enabled ? "qrc:/images/controls/chevron-down.svg" : ""
|
||||
|
||||
Keys.onEnterPressed: this.clicked()
|
||||
Keys.onReturnPressed: this.clicked()
|
||||
|
||||
onClicked: {
|
||||
if (ConnectionController.isConnectionInProgress) {
|
||||
PageController.showNotificationMessage(qsTr("Unable change protocol while trying to make an active connection"))
|
||||
return
|
||||
}
|
||||
if (ConnectionController.isConnected) {
|
||||
PageController.showNotificationMessage(qsTr("Cannot change protocol during active connection"))
|
||||
return
|
||||
}
|
||||
protocolSelectionDrawer.openTriggered()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
@@ -573,119 +479,4 @@ PageType {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DrawerType2 {
|
||||
id: protocolSelectionDrawer
|
||||
objectName: "protocolSelectionDrawer"
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
expandedStateContent: Item {
|
||||
id: protocolDrawerContainer
|
||||
|
||||
implicitHeight: root.height * 0.5
|
||||
|
||||
Component.onCompleted: {
|
||||
protocolSelectionDrawer.expandedHeight = protocolDrawerContainer.implicitHeight
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: protocolDrawerHeader
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 16
|
||||
|
||||
BackButtonType {
|
||||
id: protocolDrawerBackButton
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
backButtonImage: "qrc:/images/controls/arrow-left.svg"
|
||||
backButtonFunction: function() { protocolSelectionDrawer.closeTriggered() }
|
||||
}
|
||||
|
||||
Header2Type {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
headerText: qsTr("VPN protocol")
|
||||
}
|
||||
}
|
||||
|
||||
ListViewType {
|
||||
id: protocolDrawerListView
|
||||
|
||||
anchors.top: protocolDrawerHeader.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.topMargin: 16
|
||||
|
||||
model: root.apiAvailableProtocols
|
||||
|
||||
ButtonGroup {
|
||||
id: protocolDrawerButtonGroup
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
implicitWidth: protocolDrawerListView.width
|
||||
implicitHeight: protocolDrawerDelegate.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: protocolDrawerDelegate
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
|
||||
VerticalRadioButton {
|
||||
id: protocolDrawerRadioButton
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: root.protocolDisplayName(modelData)
|
||||
|
||||
ButtonGroup.group: protocolDrawerButtonGroup
|
||||
|
||||
checkable: !ConnectionController.isConnected
|
||||
checked: modelData === root.apiCurrentProtocol
|
||||
|
||||
onClicked: {
|
||||
protocolSelectionDrawer.closeTriggered()
|
||||
|
||||
if (modelData === root.apiCurrentProtocol) {
|
||||
return
|
||||
}
|
||||
|
||||
if (ConnectionController.isConnected) {
|
||||
PageController.showNotificationMessage(qsTr("Cannot change protocol during active connection"))
|
||||
return
|
||||
}
|
||||
|
||||
PageController.showBusyIndicator(true)
|
||||
ServersUiController.setProcessedServerId(ServersUiController.defaultServerId)
|
||||
SubscriptionUiController.setCurrentProtocol(ServersUiController.defaultServerId, modelData)
|
||||
if (!SubscriptionUiController.updateServiceFromGateway(ServersUiController.defaultServerId, "", "", true)) {
|
||||
SubscriptionUiController.setCurrentProtocol(ServersUiController.defaultServerId, root.apiCurrentProtocol)
|
||||
}
|
||||
root.updateApiProtocolState()
|
||||
PageController.showBusyIndicator(false)
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: protocolDrawerRadioButton.clicked()
|
||||
Keys.onReturnPressed: protocolDrawerRadioButton.clicked()
|
||||
}
|
||||
|
||||
DividerType {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,9 +259,41 @@ PageType {
|
||||
}
|
||||
|
||||
DividerType {
|
||||
visible: (!root.isSubscriptionExpired && !root.isSubscriptionExpiringSoon
|
||||
&& root.isSubscriptionRenewalAvailable && !root.isInAppPurchase)
|
||||
|| footer.isVisibleForAmneziaFree
|
||||
visible: !root.isSubscriptionExpired && !root.isSubscriptionExpiringSoon
|
||||
&& root.isSubscriptionRenewalAvailable && !root.isInAppPurchase
|
||||
}
|
||||
|
||||
SwitcherType {
|
||||
id: switcher
|
||||
|
||||
readonly property bool isVlessProtocol: SubscriptionUiController.isVlessProtocol(ServersUiController.processedServerId)
|
||||
readonly property bool isProtocolSwitchBlocked: ServersUiController.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 24
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.bottomMargin: 24
|
||||
|
||||
visible: ApiAccountInfoModel.data("isProtocolSelectionSupported")
|
||||
enabled: !switcher.isProtocolSwitchBlocked
|
||||
|
||||
text: qsTr("Use VLESS protocol")
|
||||
checked: switcher.isVlessProtocol
|
||||
onToggled: function() {
|
||||
if (ServersUiController.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected) {
|
||||
PageController.showNotificationMessage(qsTr("Cannot change protocol during active connection"))
|
||||
} else {
|
||||
PageController.showBusyIndicator(true)
|
||||
SubscriptionUiController.setCurrentProtocol(ServersUiController.processedServerId, switcher.isVlessProtocol ? "awg" : "vless")
|
||||
SubscriptionUiController.updateServiceFromGateway(ServersUiController.processedServerId, "", "", true)
|
||||
PageController.showBusyIndicator(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
visible: footer.isVisibleForAmneziaFree
|
||||
}
|
||||
|
||||
WarningType {
|
||||
|
||||
@@ -187,6 +187,13 @@ PageType {
|
||||
PageController.showBusyIndicator(false)
|
||||
return
|
||||
}
|
||||
if (Qt.platform.os === "android") {
|
||||
PageController.showBusyIndicator(true)
|
||||
var androidStoreId = plan.storeProductId !== undefined ? String(plan.storeProductId) : ""
|
||||
SubscriptionUiController.importPremiumFromPlayMarket(androidStoreId)
|
||||
PageController.showBusyIndicator(false)
|
||||
return
|
||||
}
|
||||
if (plan.checkoutUrl) {
|
||||
Qt.openUrlExternally(plan.checkoutUrl)
|
||||
PageController.closePage()
|
||||
|
||||
@@ -377,10 +377,14 @@ 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 || IsPlayBuild
|
||||
property var handler: function() {
|
||||
PageController.showBusyIndicator(true)
|
||||
SubscriptionUiController.restoreServiceFromAppStore()
|
||||
if (Qt.platform.os === "android") {
|
||||
SubscriptionUiController.restoreServiceFromPlayMarket()
|
||||
} else {
|
||||
SubscriptionUiController.restoreServiceFromAppStore()
|
||||
}
|
||||
PageController.showBusyIndicator(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ if(APPLE)
|
||||
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "" FORCE)
|
||||
else()
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET "13.0" CACHE STRING "" FORCE)
|
||||
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "" FORCE)
|
||||
set(CMAKE_OSX_ARCHITECTURES "x86_64" CACHE STRING "" FORCE)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
#include "cryptoUtils.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
#include <openssl/err.h>
|
||||
#include <openssl/pem.h>
|
||||
#include <openssl/rand.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr int kAesKeySize = 32; // AES-256
|
||||
constexpr int kAesIvSize = 16; // CBC block size
|
||||
constexpr int kAesBlockSize = 16;
|
||||
|
||||
QByteArray lastOpenSslError()
|
||||
{
|
||||
return QByteArray(ERR_error_string(ERR_get_error(), nullptr));
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray CryptoUtils::generateRandomBytes(int size)
|
||||
{
|
||||
if (size <= 0) {
|
||||
qCritical() << "CryptoUtils::generateRandomBytes invalid size" << size;
|
||||
return {};
|
||||
}
|
||||
|
||||
QByteArray buffer(size, Qt::Uninitialized);
|
||||
if (RAND_bytes(reinterpret_cast<unsigned char *>(buffer.data()), size) != 1) {
|
||||
qCritical() << "CryptoUtils::generateRandomBytes RAND_bytes failed:" << lastOpenSslError();
|
||||
return {};
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
QByteArray CryptoUtils::encryptAes256Cbc(const QByteArray &data, const QByteArray &key, const QByteArray &iv)
|
||||
{
|
||||
if (key.size() != kAesKeySize || iv.size() < kAesIvSize) {
|
||||
qCritical() << "CryptoUtils::encryptAes256Cbc invalid key/iv size" << key.size() << iv.size();
|
||||
return {};
|
||||
}
|
||||
|
||||
std::unique_ptr<EVP_CIPHER_CTX, decltype(&EVP_CIPHER_CTX_free)> ctx { EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free };
|
||||
if (!ctx) {
|
||||
qCritical() << "CryptoUtils::encryptAes256Cbc EVP_CIPHER_CTX_new failed:" << lastOpenSslError();
|
||||
return {};
|
||||
}
|
||||
|
||||
if (EVP_EncryptInit_ex(ctx.get(), EVP_aes_256_cbc(), nullptr, reinterpret_cast<const unsigned char *>(key.constData()),
|
||||
reinterpret_cast<const unsigned char *>(iv.constData()))
|
||||
!= 1) {
|
||||
qCritical() << "CryptoUtils::encryptAes256Cbc EVP_EncryptInit_ex failed:" << lastOpenSslError();
|
||||
return {};
|
||||
}
|
||||
|
||||
QByteArray cipherText(data.size() + kAesBlockSize, Qt::Uninitialized);
|
||||
int length = 0;
|
||||
int cipherTextLength = 0;
|
||||
|
||||
if (EVP_EncryptUpdate(ctx.get(), reinterpret_cast<unsigned char *>(cipherText.data()), &length,
|
||||
reinterpret_cast<const unsigned char *>(data.constData()), data.size())
|
||||
!= 1) {
|
||||
qCritical() << "CryptoUtils::encryptAes256Cbc EVP_EncryptUpdate failed:" << lastOpenSslError();
|
||||
return {};
|
||||
}
|
||||
cipherTextLength = length;
|
||||
|
||||
if (EVP_EncryptFinal_ex(ctx.get(), reinterpret_cast<unsigned char *>(cipherText.data()) + cipherTextLength, &length) != 1) {
|
||||
qCritical() << "CryptoUtils::encryptAes256Cbc EVP_EncryptFinal_ex failed:" << lastOpenSslError();
|
||||
return {};
|
||||
}
|
||||
cipherTextLength += length;
|
||||
|
||||
cipherText.resize(cipherTextLength);
|
||||
return cipherText;
|
||||
}
|
||||
|
||||
QByteArray CryptoUtils::decryptAes256Cbc(const QByteArray &data, const QByteArray &key, const QByteArray &iv)
|
||||
{
|
||||
if (key.size() != kAesKeySize || iv.size() < kAesIvSize) {
|
||||
qCritical() << "CryptoUtils::decryptAes256Cbc invalid key/iv size" << key.size() << iv.size();
|
||||
return {};
|
||||
}
|
||||
|
||||
std::unique_ptr<EVP_CIPHER_CTX, decltype(&EVP_CIPHER_CTX_free)> ctx { EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free };
|
||||
if (!ctx) {
|
||||
qCritical() << "CryptoUtils::decryptAes256Cbc EVP_CIPHER_CTX_new failed:" << lastOpenSslError();
|
||||
return {};
|
||||
}
|
||||
|
||||
if (EVP_DecryptInit_ex(ctx.get(), EVP_aes_256_cbc(), nullptr, reinterpret_cast<const unsigned char *>(key.constData()),
|
||||
reinterpret_cast<const unsigned char *>(iv.constData()))
|
||||
!= 1) {
|
||||
qCritical() << "CryptoUtils::decryptAes256Cbc EVP_DecryptInit_ex failed:" << lastOpenSslError();
|
||||
return {};
|
||||
}
|
||||
|
||||
QByteArray plainText(data.size() + kAesBlockSize, Qt::Uninitialized);
|
||||
int length = 0;
|
||||
int plainTextLength = 0;
|
||||
|
||||
if (EVP_DecryptUpdate(ctx.get(), reinterpret_cast<unsigned char *>(plainText.data()), &length,
|
||||
reinterpret_cast<const unsigned char *>(data.constData()), data.size())
|
||||
!= 1) {
|
||||
qCritical() << "CryptoUtils::decryptAes256Cbc EVP_DecryptUpdate failed:" << lastOpenSslError();
|
||||
return {};
|
||||
}
|
||||
plainTextLength = length;
|
||||
|
||||
if (EVP_DecryptFinal_ex(ctx.get(), reinterpret_cast<unsigned char *>(plainText.data()) + plainTextLength, &length) != 1) {
|
||||
qCritical() << "CryptoUtils::decryptAes256Cbc EVP_DecryptFinal_ex failed:" << lastOpenSslError();
|
||||
return {};
|
||||
}
|
||||
plainTextLength += length;
|
||||
|
||||
plainText.resize(plainTextLength);
|
||||
return plainText;
|
||||
}
|
||||
|
||||
EVP_PKEY *CryptoUtils::loadPublicKeyFromPem(const QByteArray &pem)
|
||||
{
|
||||
std::unique_ptr<BIO, decltype(&BIO_free)> bio { BIO_new_mem_buf(pem.constData(), pem.size()), BIO_free };
|
||||
if (!bio) {
|
||||
qCritical() << "CryptoUtils::loadPublicKeyFromPem BIO_new_mem_buf failed:" << lastOpenSslError();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
EVP_PKEY *key = PEM_read_bio_PUBKEY(bio.get(), nullptr, nullptr, nullptr);
|
||||
if (!key) {
|
||||
qCritical() << "CryptoUtils::loadPublicKeyFromPem PEM_read_bio_PUBKEY failed:" << lastOpenSslError();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
QByteArray CryptoUtils::rsaEncrypt(const QByteArray &data, EVP_PKEY *publicKey, int padding)
|
||||
{
|
||||
if (!publicKey) {
|
||||
qCritical() << "CryptoUtils::rsaEncrypt public key is null";
|
||||
return {};
|
||||
}
|
||||
|
||||
std::unique_ptr<EVP_PKEY_CTX, decltype(&EVP_PKEY_CTX_free)> ctx { EVP_PKEY_CTX_new(publicKey, nullptr), EVP_PKEY_CTX_free };
|
||||
if (!ctx) {
|
||||
qCritical() << "CryptoUtils::rsaEncrypt EVP_PKEY_CTX_new failed:" << lastOpenSslError();
|
||||
return {};
|
||||
}
|
||||
|
||||
if (EVP_PKEY_encrypt_init(ctx.get()) != 1) {
|
||||
qCritical() << "CryptoUtils::rsaEncrypt EVP_PKEY_encrypt_init failed:" << lastOpenSslError();
|
||||
return {};
|
||||
}
|
||||
|
||||
if (EVP_PKEY_CTX_set_rsa_padding(ctx.get(), padding) != 1) {
|
||||
qCritical() << "CryptoUtils::rsaEncrypt EVP_PKEY_CTX_set_rsa_padding failed:" << lastOpenSslError();
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto *plainData = reinterpret_cast<const unsigned char *>(data.constData());
|
||||
|
||||
std::size_t cipherTextLength = 0;
|
||||
if (EVP_PKEY_encrypt(ctx.get(), nullptr, &cipherTextLength, plainData, data.size()) != 1) {
|
||||
qCritical() << "CryptoUtils::rsaEncrypt couldn't determine buffer length:" << lastOpenSslError();
|
||||
return {};
|
||||
}
|
||||
|
||||
QByteArray cipherText(static_cast<int>(cipherTextLength), Qt::Uninitialized);
|
||||
if (EVP_PKEY_encrypt(ctx.get(), reinterpret_cast<unsigned char *>(cipherText.data()), &cipherTextLength, plainData, data.size())
|
||||
!= 1) {
|
||||
qCritical() << "CryptoUtils::rsaEncrypt EVP_PKEY_encrypt failed:" << lastOpenSslError();
|
||||
return {};
|
||||
}
|
||||
|
||||
cipherText.resize(static_cast<int>(cipherTextLength));
|
||||
return cipherText;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
#ifndef CRYPTOUTILS_H
|
||||
#define CRYPTOUTILS_H
|
||||
|
||||
#include <QByteArray>
|
||||
|
||||
#include <openssl/evp.h>
|
||||
|
||||
// Thin wrapper around OpenSSL EVP used for local settings encryption and for
|
||||
// the API-gateway request/response envelope. Replaces the unmaintained
|
||||
// QSimpleCrypto library. All functions report failures by returning an empty
|
||||
// QByteArray (or nullptr) and logging, without throwing.
|
||||
//
|
||||
// Wire compatibility note: encryptAes256Cbc/decryptAes256Cbc reproduce the exact
|
||||
// behaviour of QSimpleCrypto::QBlockCipher (AES-256-CBC, PKCS7 padding, first
|
||||
// 16 bytes of the IV buffer), so data written by previous app versions stays
|
||||
// readable and gateway requests keep the same format.
|
||||
namespace CryptoUtils
|
||||
{
|
||||
// Cryptographically secure random bytes of the requested size.
|
||||
QByteArray generateRandomBytes(int size);
|
||||
|
||||
// AES-256-CBC with PKCS7 padding. key must be 32 bytes, iv must be at least
|
||||
// 16 bytes (only the first 16 are used, matching the legacy behaviour).
|
||||
QByteArray encryptAes256Cbc(const QByteArray &data, const QByteArray &key, const QByteArray &iv);
|
||||
QByteArray decryptAes256Cbc(const QByteArray &data, const QByteArray &key, const QByteArray &iv);
|
||||
|
||||
// Loads an RSA public key from PEM bytes. Returns nullptr on failure.
|
||||
// The caller owns the result and must free it with EVP_PKEY_free().
|
||||
EVP_PKEY *loadPublicKeyFromPem(const QByteArray &pem);
|
||||
|
||||
// RSA encryption with the given OpenSSL padding (e.g. RSA_PKCS1_PADDING).
|
||||
QByteArray rsaEncrypt(const QByteArray &data, EVP_PKEY *publicKey, int padding);
|
||||
}
|
||||
|
||||
#endif // CRYPTOUTILS_H
|
||||
@@ -33,6 +33,8 @@ while [[ $# -gt 0 ]]; do
|
||||
--abi) abis+=("$2"); shift 2 ;;
|
||||
--sign) : ${SIGN:=true}; shift ;;
|
||||
--aab) : ${BUILD_AAB=true}; shift ;;
|
||||
--apk) : ${BUILD_APK=true}; shift ;;
|
||||
--play) : ${BUILD_PLAY=true}; shift ;;
|
||||
--help|-h|?)
|
||||
echo "Usage: $0 [options]"
|
||||
echo " Options:"
|
||||
@@ -45,6 +47,8 @@ while [[ $# -gt 0 ]]; do
|
||||
echo " --abi - specify Android ABIs for target to build for. all by default"
|
||||
echo " --sign - whether to sign the resulting files. only appicable to Android"
|
||||
echo " --aab - whether to build AAB. only applicable to Android"
|
||||
echo " --apk - whether to build APK. use with --play. only applicable to Android"
|
||||
echo " --play - build Play flavor (Google Play Billing). use with --aab or --apk. only applicable to Android"
|
||||
exit 0
|
||||
;;
|
||||
*) echo "Unknown arg \"$1\". Use $0 -h to get help"; exit 1 ;;
|
||||
@@ -201,6 +205,7 @@ args=()
|
||||
[[ -n "$QT_ANDROID_SIGN_AAB" ]] && args+=("-DQT_ANDROID_SIGN_AAB=$QT_ANDROID_SIGN_AAB")
|
||||
[[ -n "$QT_ANDROID_ABIS" ]] && args+=("-DQT_ANDROID_ABIS=$QT_ANDROID_ABIS")
|
||||
[[ -n "$QT_ANDROID_BUILD_ALL_ABIS" ]] && args+=("-DQT_ANDROID_BUILD_ALL_ABIS=$QT_ANDROID_BUILD_ALL_ABIS")
|
||||
[[ -n "$BUILD_PLAY" ]] && args+=("-DANDROID_BUILD_PLAY=ON")
|
||||
|
||||
if [[ -n "$FORCE" ]]; then
|
||||
run_traced rm -rf "$BUILD_PATH"
|
||||
@@ -209,7 +214,17 @@ fi
|
||||
run_traced cmake -S "$SOURCE_PATH" -B "$BUILD_PATH" "${args[@]}"
|
||||
run_traced cmake --build "$BUILD_PATH" --config "$CMAKE_BUILD_TYPE"
|
||||
|
||||
[[ -n "$BUILD_AAB" ]] && run_traced cmake --build "$BUILD_PATH" --config "$CMAKE_BUILD_TYPE" -t "aab"
|
||||
if [[ -n "$BUILD_AAB" ]]; then
|
||||
if [[ -n "$BUILD_PLAY" ]]; then
|
||||
run_traced cmake --build "$BUILD_PATH" --config "$CMAKE_BUILD_TYPE" -t "android_play_aab"
|
||||
else
|
||||
run_traced cmake --build "$BUILD_PATH" --config "$CMAKE_BUILD_TYPE" -t "aab"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "$BUILD_APK" ]] && [[ -n "$BUILD_PLAY" ]]; then
|
||||
run_traced cmake --build "$BUILD_PATH" --config "$CMAKE_BUILD_TYPE" -t "android_play_apk"
|
||||
fi
|
||||
|
||||
if [ -z "$no_installers" ]; then
|
||||
for installer in $INSTALLERS; do
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from conan import ConanFile
|
||||
from conan.tools.files import get, copy
|
||||
from conan.tools.files import get, copy, replace_in_file
|
||||
from conan.tools.layout import basic_layout
|
||||
from conan.errors import ConanInvalidConfiguration
|
||||
from conan.tools.env import Environment
|
||||
@@ -23,7 +23,7 @@ class AmneziaLibxray(ConanFile):
|
||||
|
||||
def build_requirements(self):
|
||||
self.tool_requires("go/1.26.0")
|
||||
|
||||
|
||||
def validate(self):
|
||||
if self.settings.os != "Android":
|
||||
raise ConanInvalidConfiguration(f"{self.name} v{self.version} does not support {self.settings.os}")
|
||||
@@ -47,15 +47,28 @@ class AmneziaLibxray(ConanFile):
|
||||
build_path = os.path.join(self.build_folder, "build.sh")
|
||||
build_stat = os.stat(build_path)
|
||||
os.chmod(build_path, build_stat.st_mode | stat.S_IEXEC)
|
||||
replace_in_file(self,
|
||||
build_path,
|
||||
'-ldflags="-w -s -buildid="',
|
||||
'-ldflags="-w -s -buildid= -extldflags=-Wl,-z,max-page-size=16384"',
|
||||
)
|
||||
replace_in_file(self,
|
||||
build_path,
|
||||
" rm -f go.mod\n rm -f go.sum\n go mod init github.com/amnezia-vpn/amnezia-libxray\n go mod tidy",
|
||||
" go mod tidy",
|
||||
)
|
||||
|
||||
def build(self):
|
||||
self._patch_sources()
|
||||
self.run("./build.sh android")
|
||||
if self.settings_build.os == "Windows":
|
||||
self.run("bash build.sh android")
|
||||
else:
|
||||
self.run("./build.sh android")
|
||||
|
||||
def package(self):
|
||||
copy(self, "libxray.aar", src=self.build_folder, dst=os.path.join(self.package_folder, "aar"))
|
||||
|
||||
def package_info(self):
|
||||
self.cpp_info.set_property("cmake_extra_variables", {
|
||||
"AMNEZIA_LIBXRAY_PATH": os.path.join(self.package_folder, "aar", "libxray.aar"),
|
||||
"AMNEZIA_LIBXRAY_PATH": Path(self.package_folder, "aar", "libxray.aar").as_posix(),
|
||||
})
|
||||
|
||||
@@ -3,25 +3,14 @@ from conan.tools.files import get, copy, collect_libs, chdir, rename
|
||||
from conan.tools.layout import basic_layout
|
||||
from conan.errors import ConanInvalidConfiguration
|
||||
from conan.tools.gnu import Autotools, AutotoolsToolchain
|
||||
from conan.tools.apple import XCRun, is_apple_os
|
||||
from conan.tools.apple.apple import _to_apple_arch
|
||||
from conan.tools.env import Environment
|
||||
|
||||
import os
|
||||
import shlex
|
||||
|
||||
|
||||
class AmneziaXrayBindings(ConanFile):
|
||||
name = "amnezia-xray-bindings"
|
||||
version = "1.1.0"
|
||||
settings = "os", "arch", "compiler"
|
||||
|
||||
_arch_map = {
|
||||
"x86": "386",
|
||||
"x86_64": "amd64",
|
||||
"armv8": "arm64"
|
||||
}
|
||||
|
||||
@property
|
||||
def _goos(self):
|
||||
return {
|
||||
@@ -30,15 +19,15 @@ class AmneziaXrayBindings(ConanFile):
|
||||
"Macos": "darwin",
|
||||
"Windows": "windows"
|
||||
}.get(str(self.settings.os))
|
||||
|
||||
|
||||
@property
|
||||
def _archs(self):
|
||||
return str(self.settings.arch).split("|")
|
||||
|
||||
@property
|
||||
def _is_multiarch(self):
|
||||
return len(self._archs) > 1
|
||||
|
||||
def _goarch(self):
|
||||
return {
|
||||
"x86": "386",
|
||||
"x86_64": "amd64",
|
||||
"armv8": "arm64"
|
||||
}.get(str(self.settings.arch))
|
||||
|
||||
@property
|
||||
def _is_windows(self):
|
||||
return str(self.settings.os).startswith("Windows")
|
||||
@@ -65,80 +54,55 @@ class AmneziaXrayBindings(ConanFile):
|
||||
self.tool_requires("mingw-builds/15.1.0")
|
||||
|
||||
def validate(self):
|
||||
if not self._goos or not all(arch in self._arch_map for arch in self._archs):
|
||||
if not self._goos or not self._goarch:
|
||||
raise ConanInvalidConfiguration(
|
||||
f"{self.name} v{self.version} does not support {self.settings.os} {self.settings.arch}"
|
||||
)
|
||||
|
||||
if self._is_multiarch and not is_apple_os(self):
|
||||
raise ConanInvalidConfiguration(
|
||||
f"{self.name} v{self.version} does not support multiarch builds"
|
||||
)
|
||||
|
||||
def source(self):
|
||||
get(self, "https://github.com/amnezia-vpn/amnezia-xray-bindings/archive/v1.1.0.zip",
|
||||
sha256="6ea768ec7002cedd422a39aea17704b888acaf794432aa5937cfc92fb6d80eb5", strip_root=True)
|
||||
|
||||
def generate(self):
|
||||
tc = AutotoolsToolchain(self)
|
||||
tc.apple_arch_flag = None
|
||||
tc.make_args = [
|
||||
"LIB_ARC=libamnezia_xray.a"
|
||||
]
|
||||
env = tc.environment()
|
||||
env.define("GOPATH", os.path.join(self.build_folder, "gopath"))
|
||||
env.define("GOMODCACHE", os.path.join(self.build_folder, "gopath", "pkg", "mod"))
|
||||
env.define("GOCACHE", os.path.join(self.build_folder, "gocache"))
|
||||
env.define("ARCH", self._goarch)
|
||||
env.define("GOARCH", self._goarch)
|
||||
env.define("GOOS", self._goos)
|
||||
env.define("CGO_LDFLAGS", tc.ldflags)
|
||||
env.define("CGO_CFLAGS", tc.cflags)
|
||||
if self._is_windows:
|
||||
env.define("OS", "windows")
|
||||
self._ldflags = tc.ldflags
|
||||
self._cflags = tc.cflags
|
||||
tc.generate(env)
|
||||
|
||||
def build(self):
|
||||
with chdir(self, self.source_folder):
|
||||
for arch in self._archs:
|
||||
build_dir = os.path.join(self.build_folder, arch) if self._is_multiarch else self.build_folder
|
||||
goarch = self._arch_map.get(arch)
|
||||
|
||||
cflags = list(self._cflags)
|
||||
ldflags = list(self._ldflags)
|
||||
if is_apple_os(self):
|
||||
cflags.append(f"-arch {_to_apple_arch(arch)}")
|
||||
ldflags.append(f"-arch {_to_apple_arch(arch)}")
|
||||
|
||||
env = Environment()
|
||||
env.define("ARCH", goarch)
|
||||
env.define("CGO_CFLAGS", " ".join(cflags))
|
||||
env.define("CGO_LDFLAGS", " ".join(ldflags))
|
||||
with env.vars(self).apply():
|
||||
at = Autotools(self)
|
||||
at.make(args=[
|
||||
f"BUILD_DIR={build_dir.replace("\\", "/") if self._is_windows else build_dir}"
|
||||
])
|
||||
|
||||
if is_apple_os(self) and self._is_multiarch:
|
||||
lipo = XCRun(self).find('lipo')
|
||||
archives = [os.path.join(self.build_folder, arch, "amnezia_xray.a") for arch in self._archs]
|
||||
output = os.path.join(self.build_folder, "amnezia_xray.a")
|
||||
self.run("{} -create -output {} {}".format(
|
||||
shlex.quote(lipo),
|
||||
shlex.quote(output),
|
||||
shlex.join(archives)
|
||||
))
|
||||
|
||||
copy(self, "*.h", os.path.join(self.build_folder, self._archs[0]), self.build_folder)
|
||||
autotools = Autotools(self)
|
||||
autotools.make()
|
||||
|
||||
def _rename_header(self):
|
||||
if not self._is_windows:
|
||||
rename(self,
|
||||
os.path.join(self.package_folder, "lib", "amnezia_xray.a"),
|
||||
os.path.join(self.package_folder, "lib", "libamnezia_xray.a")
|
||||
)
|
||||
rename(self, os.path.join(self.package_folder, "include", "libamnezia_xray.h"),
|
||||
os.path.join(self.package_folder, "include", "amnezia_xray.h"))
|
||||
|
||||
def _rename_libs(self):
|
||||
# workaround of bad naming strategy in amnezia-xray-bindings
|
||||
# TODO: change it and kick out the code below
|
||||
lib_dir = os.path.join(self.package_folder, "lib")
|
||||
for fname in os.listdir(lib_dir):
|
||||
if not fname.startswith("lib"):
|
||||
src = os.path.join(lib_dir, fname)
|
||||
dst = os.path.join(lib_dir, "lib" + fname)
|
||||
os.rename(src, dst)
|
||||
|
||||
def package(self):
|
||||
copy(self, "amnezia_xray.h", src=self.build_folder, dst=os.path.join(self.package_folder, "include"), keep_path=False)
|
||||
copy(self, "amnezia_xray.a", src=self.build_folder, dst=os.path.join(self.package_folder, "lib"), keep_path=False)
|
||||
copy(self, "amnezia_xray.lib", src=self.build_folder, dst=os.path.join(self.package_folder, "lib"), keep_path=False)
|
||||
copy(self, "amnezia_xray.dll", src=self.build_folder, dst=os.path.join(self.package_folder, "bin"), keep_path=False)
|
||||
copy(self, "*.h", src=self.build_folder, dst=os.path.join(self.package_folder, "include"))
|
||||
copy(self, "*.a", src=self.build_folder, dst=os.path.join(self.package_folder, "lib"))
|
||||
copy(self, "*.lib", src=self.build_folder, dst=os.path.join(self.package_folder, "lib"))
|
||||
copy(self, "*.dll", src=self.build_folder, dst=os.path.join(self.package_folder, "bin"))
|
||||
self._rename_header()
|
||||
|
||||
def package_info(self):
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
from conan import ConanFile
|
||||
from conan.tools.cmake import cmake_layout, CMake, CMakeToolchain
|
||||
from conan.tools.files import copy, replace_in_file
|
||||
from conan.tools.env import VirtualBuildEnv, Environment
|
||||
from conan.errors import ConanInvalidConfiguration
|
||||
from conan.tools.scm import Git
|
||||
|
||||
import os
|
||||
import platform
|
||||
from pathlib import Path
|
||||
|
||||
class AwgAndroid(ConanFile):
|
||||
name = "awg-android"
|
||||
@@ -21,6 +23,11 @@ class AwgAndroid(ConanFile):
|
||||
|
||||
def build_requirements(self):
|
||||
self.tool_requires("cmake/[>=3.4.1 <4]")
|
||||
if platform.system() == "Windows":
|
||||
self.tool_requires("ninja/[*]")
|
||||
self.tool_requires("go/[*]")
|
||||
if not self.conf.get("tools.microsoft.bash:path", check_type=str):
|
||||
self.tool_requires("msys2/cci.latest")
|
||||
|
||||
def validate(self):
|
||||
if self.settings.os != "Android":
|
||||
@@ -35,9 +42,11 @@ class AwgAndroid(ConanFile):
|
||||
)
|
||||
|
||||
def generate(self):
|
||||
VirtualBuildEnv(self).generate()
|
||||
|
||||
tc = CMakeToolchain(self)
|
||||
tc.variables["GRADLE_USER_HOME"] = os.path.join(self.build_folder, "gradle_user_home")
|
||||
tc.variables["CMAKE_LIBRARY_OUTPUT_DIRECTORY"] = os.path.join(self.build_folder, "out")
|
||||
tc.variables["GRADLE_USER_HOME"] = Path(os.path.join(self.build_folder, "gradle_user_home")).as_posix()
|
||||
tc.variables["CMAKE_LIBRARY_OUTPUT_DIRECTORY"] = Path(os.path.join(self.build_folder, "out")).as_posix()
|
||||
# not to warn in case of strtok() usage
|
||||
tc.extra_cflags = ["-Wno-deprecated-declarations"]
|
||||
tc.generate()
|
||||
@@ -64,6 +73,31 @@ class AwgAndroid(ConanFile):
|
||||
'sha256sum -c',
|
||||
'shasum -a 256 -c'
|
||||
)
|
||||
elif platform.system() == 'Windows':
|
||||
# elf-cleaner uses sys/mman.h (POSIX only) and cannot be built on Windows;
|
||||
# skip it — DT_FLAGS_1 warnings only affect Android < 6.0
|
||||
replace_in_file(self,
|
||||
os.path.join(self.source_folder, "tunnel", "tools", "CMakeLists.txt"),
|
||||
'# Strip unwanted ELF sections to prevent DT_FLAGS_1 warnings on old Android versions\n'
|
||||
'file(GLOB ELF_CLEANER_SOURCES elf-cleaner/*.c elf-cleaner/*.cpp)\n'
|
||||
'add_custom_target(elf-cleaner COMMENT "Building elf-cleaner" VERBATIM COMMAND cc\n'
|
||||
' -O2 -DPACKAGE_NAME="elf-cleaner" -DPACKAGE_VERSION="" -DCOPYRIGHT=""\n'
|
||||
' -o "${CMAKE_CURRENT_BINARY_DIR}/elf-cleaner" ${ELF_CLEANER_SOURCES}\n'
|
||||
')\n'
|
||||
'add_custom_command(TARGET libwg.so POST_BUILD VERBATIM COMMAND "${CMAKE_CURRENT_BINARY_DIR}/elf-cleaner"\n'
|
||||
' --api-level "${ANDROID_NATIVE_API_LEVEL}" "$<TARGET_FILE:libwg.so>")\n'
|
||||
'add_dependencies(libwg.so elf-cleaner)\n'
|
||||
'add_custom_command(TARGET libwg-quick.so POST_BUILD VERBATIM COMMAND "${CMAKE_CURRENT_BINARY_DIR}/elf-cleaner"\n'
|
||||
' --api-level "${ANDROID_NATIVE_API_LEVEL}" "$<TARGET_FILE:libwg-quick.so>")\n'
|
||||
'add_dependencies(libwg-quick.so elf-cleaner)',
|
||||
'',
|
||||
)
|
||||
# patch Makefile: skip Go download, use 'go' already in PATH from tool_requires
|
||||
replace_in_file(self,
|
||||
os.path.join(self.source_folder, "tunnel", "tools", "libwg-go", "Makefile"),
|
||||
'$(DESTDIR)/libwg-go.so: export PATH := $(BUILDDIR)/go-$(GO_VERSION)/bin/:$(PATH)\n$(DESTDIR)/libwg-go.so: $(BUILDDIR)/go-$(GO_VERSION)/.prepared go.mod',
|
||||
'$(DESTDIR)/libwg-go.so: go.mod',
|
||||
)
|
||||
|
||||
def build(self):
|
||||
self._patch_sources()
|
||||
@@ -81,6 +115,6 @@ class AwgAndroid(ConanFile):
|
||||
self.cpp_info.set_property("cmake_target_name", "amnezia::awg-android")
|
||||
self.cpp_info.libs = [ "wg-go" ]
|
||||
self.cpp_info.set_property("cmake_extra_variables", {
|
||||
"AMNEZIA_ANDROID_LIBWG_PATH": os.path.join(self.package_folder, "bin", "libwg.so"),
|
||||
"AMNEZIA_ANDROID_LIBWG_QUICK_PATH": os.path.join(self.package_folder, "bin", "libwg-quick.so"),
|
||||
"AMNEZIA_ANDROID_LIBWG_PATH": Path(os.path.join(self.package_folder, "bin", "libwg.so")).as_posix(),
|
||||
"AMNEZIA_ANDROID_LIBWG_QUICK_PATH": Path(os.path.join(self.package_folder, "bin", "libwg-quick.so")).as_posix(),
|
||||
})
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
from conan import ConanFile
|
||||
from conan.errors import ConanInvalidConfiguration
|
||||
from conan.tools.layout import basic_layout
|
||||
from conan.tools.files import get, chdir
|
||||
from conan.tools.apple import XCRun
|
||||
from conan.tools.files import get, copy
|
||||
from conan.tools.gnu import Autotools, AutotoolsToolchain
|
||||
from conan.tools.apple import is_apple_os
|
||||
from conan.tools.apple.apple import _to_apple_arch
|
||||
from conan.tools.env import Environment
|
||||
|
||||
import os
|
||||
import shlex
|
||||
|
||||
|
||||
class AwgGo(ConanFile):
|
||||
name = "awg-go"
|
||||
@@ -18,14 +12,6 @@ class AwgGo(ConanFile):
|
||||
package_type = "application"
|
||||
settings = "os", "arch"
|
||||
|
||||
_binary_name = "amneziawg-go"
|
||||
|
||||
_arch_map = {
|
||||
"x86": "386",
|
||||
"x86_64": "amd64",
|
||||
"armv8": "arm64"
|
||||
}
|
||||
|
||||
@property
|
||||
def _goos(self):
|
||||
return {
|
||||
@@ -35,12 +21,12 @@ class AwgGo(ConanFile):
|
||||
}.get(str(self.settings.os))
|
||||
|
||||
@property
|
||||
def _archs(self):
|
||||
return str(self.settings.arch).split("|")
|
||||
|
||||
@property
|
||||
def _is_multiarch(self):
|
||||
return len(self._archs) > 1
|
||||
def _goarch(self):
|
||||
return {
|
||||
"x86": "386",
|
||||
"x86_64": "amd64",
|
||||
"armv8": "arm64"
|
||||
}.get(str(self.settings.arch))
|
||||
|
||||
def layout(self):
|
||||
basic_layout(self, build_folder=".")
|
||||
@@ -49,15 +35,10 @@ class AwgGo(ConanFile):
|
||||
self.tool_requires("go/1.26.0")
|
||||
|
||||
def validate(self):
|
||||
if not self._goos or not all(arch in self._arch_map for arch in self._archs):
|
||||
if not self._goos or not self._goarch:
|
||||
raise ConanInvalidConfiguration(
|
||||
f"{self.name} v{self.version} does not support {self.settings.os} {self.settings.arch}"
|
||||
)
|
||||
|
||||
if self._is_multiarch and not is_apple_os(self):
|
||||
raise ConanInvalidConfiguration(
|
||||
f"{self.name} v{self.version} does not support multiarch builds"
|
||||
)
|
||||
|
||||
def source(self):
|
||||
get(self, f"https://github.com/amnezia-vpn/amneziawg-go/archive/refs/tags/v{self.version}.zip",
|
||||
@@ -66,59 +47,21 @@ class AwgGo(ConanFile):
|
||||
|
||||
def generate(self):
|
||||
tc = AutotoolsToolchain(self)
|
||||
tc.apple_arch_flag = None
|
||||
env = tc.environment()
|
||||
env.define("GOPATH", os.path.join(self.build_folder, "gopath"))
|
||||
env.define("GOMODCACHE", os.path.join(self.build_folder, "gopath", "pkg", "mod"))
|
||||
env.define("GOCACHE", os.path.join(self.build_folder, "gocache"))
|
||||
env.define("GOTELEMETRY", "off")
|
||||
env.define("GOOS", self._goos)
|
||||
self._ldflags = tc.ldflags
|
||||
self._cflags = tc.cflags
|
||||
env.define("GOARCH", self._goarch)
|
||||
env.define("CGO_LDFLAGS", tc.ldflags)
|
||||
env.define("CGO_CFLAGS", tc.cflags)
|
||||
tc.generate(env)
|
||||
|
||||
def build(self):
|
||||
with chdir(self, self.source_folder):
|
||||
for arch in self._archs:
|
||||
goarch = self._arch_map.get(arch)
|
||||
|
||||
ldflags = self._ldflags
|
||||
cflags = self._cflags
|
||||
if is_apple_os(self):
|
||||
ldflags.append(f"-arch {_to_apple_arch(arch)}")
|
||||
cflags.append(f"-arch {_to_apple_arch(arch)}")
|
||||
|
||||
env = Environment()
|
||||
env.define("GOARCH", goarch)
|
||||
env.define("CGO_LDFLAGS", " ".join(ldflags))
|
||||
env.define("CGO_CFLAGS", " ".join(cflags))
|
||||
with env.vars(self).apply():
|
||||
at = Autotools(self)
|
||||
at.make()
|
||||
if self._is_multiarch:
|
||||
os.rename(
|
||||
os.path.join(self.source_folder, self._binary_name),
|
||||
os.path.join(self.source_folder, f"{self._binary_name}-{arch}")
|
||||
)
|
||||
|
||||
if is_apple_os(self) and self._is_multiarch:
|
||||
lipo = XCRun(self).find("lipo")
|
||||
output = os.path.join(self.build_folder, self._binary_name)
|
||||
binaries = [os.path.join(self.build_folder, f"{self._binary_name}-{arch}") for arch in self._archs]
|
||||
self.run("{} -create -output {} {}".format(
|
||||
shlex.quote(lipo),
|
||||
shlex.quote(output),
|
||||
shlex.join(binaries)
|
||||
))
|
||||
at = Autotools(self)
|
||||
at.make()
|
||||
|
||||
def package(self):
|
||||
with chdir(self, self.source_folder):
|
||||
at = Autotools(self)
|
||||
at.make("install", args=[
|
||||
f"BINDIR={self.package_folder}",
|
||||
])
|
||||
copy(self, "amneziawg-go", src=self.build_folder, dst=self.package_folder)
|
||||
|
||||
def package_info(self):
|
||||
self.cpp_info.exe = True
|
||||
self.cpp_info.location = os.path.join(self.package_folder, self._binary_name)
|
||||
self.cpp_info.location = os.path.join(self.package_folder, "amneziawg-go")
|
||||
self.cpp_info.set_property("cmake_target_name", "amnezia::awg-go")
|
||||
|
||||
@@ -5,6 +5,7 @@ from conan.tools.scm import Git
|
||||
from conan.errors import ConanInvalidConfiguration
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
class OpenvpnPtAndroid(ConanFile):
|
||||
name = "openvpn-pt-android"
|
||||
@@ -53,5 +54,5 @@ class OpenvpnPtAndroid(ConanFile):
|
||||
self.cpp_info.set_property("cmake_target_name", "amnezia::openvpn-pt-android")
|
||||
self.cpp_info.libs = [ "ovpn3", "ovpnutil", "rsapss" ]
|
||||
self.cpp_info.set_property("cmake_extra_variables", {
|
||||
"OPENVPN_PT_ANDROID_LIBCK_OVPN_PLUGIN_PATH": os.path.join(self.package_folder, "lib", "libck-ovpn-plugin.so")
|
||||
"OPENVPN_PT_ANDROID_LIBCK_OVPN_PLUGIN_PATH": Path(self.package_folder, "lib", "libck-ovpn-plugin.so").as_posix()
|
||||
})
|
||||
|
||||
@@ -2,15 +2,10 @@ from conan import ConanFile
|
||||
from conan.tools.layout import basic_layout
|
||||
from conan.tools.files import get, copy, chdir
|
||||
from conan.errors import ConanInvalidConfiguration
|
||||
from conan.tools.apple import XCRun
|
||||
from conan.tools.gnu import Autotools, AutotoolsToolchain
|
||||
from conan.tools.env import Environment
|
||||
from conan.tools.apple import is_apple_os
|
||||
from conan.tools.apple.apple import _to_apple_arch
|
||||
|
||||
import os
|
||||
import shlex
|
||||
|
||||
|
||||
class Tun2Socks(ConanFile):
|
||||
name = "tun2socks"
|
||||
@@ -18,14 +13,6 @@ class Tun2Socks(ConanFile):
|
||||
package_type = "application"
|
||||
settings = "os", "arch"
|
||||
|
||||
_binary_name = "tun2socks"
|
||||
|
||||
_arch_map = {
|
||||
"x86": "386",
|
||||
"x86_64": "amd64",
|
||||
"armv8": "arm64"
|
||||
}
|
||||
|
||||
@property
|
||||
def _goos(self):
|
||||
return {
|
||||
@@ -35,35 +22,29 @@ class Tun2Socks(ConanFile):
|
||||
}.get(str(self.settings.os))
|
||||
|
||||
@property
|
||||
def _archs(self):
|
||||
return str(self.settings.arch).split("|")
|
||||
def _goarch(self):
|
||||
return {
|
||||
"x86": "386",
|
||||
"x86_64": "amd64",
|
||||
"armv8": "arm64"
|
||||
}.get(str(self.settings.arch))
|
||||
|
||||
@property
|
||||
def _is_multiarch(self):
|
||||
return len(self._archs) > 1
|
||||
|
||||
@property
|
||||
def _is_windows(self):
|
||||
return str(self.settings.get_safe("os")).startswith("Windows")
|
||||
|
||||
|
||||
@property
|
||||
def _binary_name_ext(self):
|
||||
ext = ".exe" if self._is_windows else ""
|
||||
return f"{self._binary_name}{ext}"
|
||||
def _ext(self):
|
||||
return ".exe" if self._is_windows else ""
|
||||
|
||||
def layout(self):
|
||||
basic_layout(self)
|
||||
|
||||
def validate(self):
|
||||
if not self._goos or not all(arch in self._arch_map for arch in self._archs):
|
||||
if not self._goos or not self._goarch:
|
||||
raise ConanInvalidConfiguration(
|
||||
f"{self.name} v{self.version} does not support {self.settings.os} {self.settings.arch}"
|
||||
)
|
||||
|
||||
if self._is_multiarch and not is_apple_os(self):
|
||||
raise ConanInvalidConfiguration(
|
||||
f"{self.name} v{self.version} does not support multiarch builds"
|
||||
)
|
||||
|
||||
def build_requirements(self):
|
||||
self.tool_requires("go/1.26.0")
|
||||
@@ -84,59 +65,26 @@ class Tun2Socks(ConanFile):
|
||||
|
||||
def generate(self):
|
||||
tc = AutotoolsToolchain(self)
|
||||
tc.apple_arch_flag = None
|
||||
env = tc.environment()
|
||||
env.define("GOPATH", os.path.join(self.build_folder, "gopath"))
|
||||
env.define("GOMODCACHE", os.path.join(self.build_folder, "gopath", "pkg", "mod"))
|
||||
env.define("GOCACHE", os.path.join(self.build_folder, "gocache"))
|
||||
env.define("GOTELEMETRY", "off")
|
||||
env.define("LDFLAGS", "")
|
||||
env.define("CGO_LDFLAGS", tc.ldflags)
|
||||
env.define("CGO_CFLAGS", tc.cflags)
|
||||
env.define("GOOS", self._goos)
|
||||
self._ldflags = tc.ldflags
|
||||
self._cflags = tc.cflags
|
||||
env.define("GOARCH", self._goarch)
|
||||
tc.generate(env)
|
||||
|
||||
def build(self):
|
||||
with chdir(self, self.source_folder):
|
||||
for arch in self._archs:
|
||||
build_dir = os.path.join(self.build_folder, arch) if self._is_multiarch else self.build_folder
|
||||
goarch = self._arch_map.get(arch)
|
||||
|
||||
ldflags = self._ldflags
|
||||
cflags = self._cflags
|
||||
if is_apple_os(self):
|
||||
ldflags.append(f"-arch {_to_apple_arch(arch)}")
|
||||
cflags.append(f"-arch {_to_apple_arch(arch)}")
|
||||
|
||||
env = Environment()
|
||||
env.define("GOARCH", goarch)
|
||||
env.define("CGO_LDFLAGS", " ".join(ldflags))
|
||||
env.define("CGO_CFLAGS", " ".join(cflags))
|
||||
with env.vars(self).apply():
|
||||
at = Autotools(self)
|
||||
at.make("tun2socks", args=[
|
||||
f"BUILD_DIR={build_dir.replace("\\", "/") if self._is_windows else build_dir}"
|
||||
])
|
||||
if self._is_windows:
|
||||
os.rename(
|
||||
os.path.join(build_dir, self._binary_name),
|
||||
os.path.join(build_dir, self._binary_name_ext)
|
||||
)
|
||||
|
||||
if self._is_multiarch:
|
||||
lipo = XCRun(self).find('lipo')
|
||||
output = os.path.join(self.build_folder, self._binary_name_ext)
|
||||
binaries = [os.path.join(self.build_folder, arch, self._binary_name_ext) for arch in self._archs]
|
||||
self.run("{} -create -output {} {}".format(
|
||||
shlex.quote(lipo),
|
||||
shlex.quote(output),
|
||||
shlex.join(binaries)
|
||||
))
|
||||
at = Autotools(self)
|
||||
at.make("tun2socks")
|
||||
|
||||
def package(self):
|
||||
copy(self, self._binary_name_ext, src=self.build_folder, dst=self.package_folder)
|
||||
copy(self, "tun2socks", src=self.build_folder, dst=self.package_folder)
|
||||
if self._is_windows:
|
||||
with chdir(self, self.package_folder):
|
||||
os.rename(src="tun2socks", dst="tun2socks.exe")
|
||||
|
||||
def package_info(self):
|
||||
self.cpp_info.exe = True
|
||||
self.cpp_info.location = os.path.join(self.package_folder, self._binary_name_ext)
|
||||
self.cpp_info.location = os.path.join(self.package_folder, f"tun2socks{self._ext}")
|
||||
self.cpp_info.set_property("cmake_target_name", "xjasonlyu::tun2socks")
|
||||
|
||||
@@ -18,9 +18,14 @@ qt_standard_project_setup()
|
||||
|
||||
configure_file(${CMAKE_SOURCE_DIR}/version.h.in ${CMAKE_CURRENT_BINARY_DIR}/version.h)
|
||||
|
||||
set(QSIMPLECRYPTO_DIR ${CMAKE_CURRENT_LIST_DIR}/../../client/3rd/QSimpleCrypto/src)
|
||||
|
||||
include_directories(
|
||||
${QSIMPLECRYPTO_DIR}
|
||||
)
|
||||
|
||||
set(HEADERS
|
||||
${CMAKE_CURRENT_LIST_DIR}/../../client/core/utils/utilities.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/../../common/crypto/cryptoUtils.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/../../client/secureQSettings.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/../../client/core/utils/networkUtilities.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipc.h
|
||||
@@ -33,11 +38,16 @@ set(HEADERS
|
||||
${CMAKE_CURRENT_LIST_DIR}/systemservice.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/xray.h
|
||||
${CMAKE_CURRENT_BINARY_DIR}/version.h
|
||||
${QSIMPLECRYPTO_DIR}/include/QAead.h
|
||||
${QSIMPLECRYPTO_DIR}/include/QBlockCipher.h
|
||||
${QSIMPLECRYPTO_DIR}/include/QRsa.h
|
||||
${QSIMPLECRYPTO_DIR}/include/QSimpleCrypto_global.h
|
||||
${QSIMPLECRYPTO_DIR}/include/QX509.h
|
||||
${QSIMPLECRYPTO_DIR}/include/QX509Store.h
|
||||
)
|
||||
|
||||
set(SOURCES
|
||||
${CMAKE_CURRENT_LIST_DIR}/../../client/core/utils/utilities.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/../../common/crypto/cryptoUtils.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/../../client/secureQSettings.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/../../client/core/utils/networkUtilities.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipcserver.cpp
|
||||
@@ -49,6 +59,11 @@ set(SOURCES
|
||||
${CMAKE_CURRENT_LIST_DIR}/killswitch.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/systemservice.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/xray.cpp
|
||||
${QSIMPLECRYPTO_DIR}/sources/QAead.cpp
|
||||
${QSIMPLECRYPTO_DIR}/sources/QBlockCipher.cpp
|
||||
${QSIMPLECRYPTO_DIR}/sources/QRsa.cpp
|
||||
${QSIMPLECRYPTO_DIR}/sources/QX509.cpp
|
||||
${QSIMPLECRYPTO_DIR}/sources/QX509Store.cpp
|
||||
)
|
||||
|
||||
# Mozilla headres
|
||||
@@ -282,7 +297,6 @@ include_directories(
|
||||
${CMAKE_CURRENT_LIST_DIR}/../../client
|
||||
${CMAKE_CURRENT_LIST_DIR}/../../ipc
|
||||
${CMAKE_CURRENT_LIST_DIR}/../../common/logger
|
||||
${CMAKE_CURRENT_LIST_DIR}/../../common/crypto
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user