mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-08-10 01:29:02 +03:00
Compare commits
44 Commits
fix/ssh-cr
...
feature/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8dca6ea333 | ||
|
|
1377330554 | ||
|
|
773183ec58 | ||
|
|
c05da504fe | ||
|
|
551e46c558 | ||
|
|
40a2a07c30 | ||
|
|
e79582187d | ||
|
|
e3b21ada55 | ||
|
|
21783159d0 | ||
|
|
c0e3ea32fe | ||
|
|
d928ab5021 | ||
|
|
e49abbd5fc | ||
|
|
bf4505494e | ||
|
|
4a608ce1db | ||
|
|
6d52df2e2d | ||
|
|
b1b6ef2781 | ||
|
|
bdadbb1d63 | ||
|
|
b894d9be1b | ||
|
|
d23061085d | ||
|
|
06798f734a | ||
|
|
1aaa31f3d8 | ||
|
|
8b726d774f | ||
|
|
56d0e9ccff | ||
|
|
973b18a9b7 | ||
|
|
4612791caf | ||
|
|
26ff347366 | ||
|
|
ac37e1df91 | ||
|
|
bad93ec4b0 | ||
|
|
5ae26ee9f5 | ||
|
|
d7e581dbfc | ||
|
|
c76971a73b | ||
|
|
0a13838220 | ||
|
|
def61def08 | ||
|
|
5a155e3ba5 | ||
|
|
d887bc69c5 | ||
|
|
4424e5aba1 | ||
|
|
05ece4cb12 | ||
|
|
15d225264a | ||
|
|
e0ee26e2e6 | ||
|
|
8d7e6637ee | ||
|
|
a8f35395c4 | ||
|
|
6338538977 | ||
|
|
2e9439aacf | ||
|
|
49eb31f90b |
46
.github/workflows/deploy.yml
vendored
46
.github/workflows/deploy.yml
vendored
@@ -864,13 +864,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:
|
||||
@@ -878,13 +906,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:
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -10,8 +10,6 @@ deploy/build_64/*
|
||||
winbuild*.bat
|
||||
.cache/
|
||||
.vscode/
|
||||
.venv/
|
||||
.cursor*
|
||||
|
||||
|
||||
# Qt-es
|
||||
|
||||
@@ -4,7 +4,7 @@ set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
set(PROJECT AmneziaVPN)
|
||||
set(AMNEZIAVPN_VERSION 5.0.1.0)
|
||||
set(AMNEZIAVPN_VERSION 5.0.1.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 2141)
|
||||
set(APP_ANDROID_VERSION_CODE 2143)
|
||||
|
||||
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
||||
set(MZ_PLATFORM_NAME "linux")
|
||||
|
||||
@@ -26,8 +26,9 @@
|
||||
#include "version.h"
|
||||
|
||||
#include "platforms/ios/QRCodeReaderBase.h"
|
||||
#ifdef Q_OS_IOS
|
||||
#include "platforms/ios/ioscontextmenu.h"
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include "platforms/android/android_controller.h"
|
||||
#endif
|
||||
|
||||
|
||||
@@ -142,8 +143,10 @@ void AmneziaApplication::init()
|
||||
m_engine->rootContext()->setContextProperty("IsMacOsNeBuild", false);
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
m_engine->rootContext()->setContextProperty("IosContextMenu", new IosContextMenu(this));
|
||||
#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));
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
386
client/android/billing/src/main/kotlin/BillingProvider.kt
Normal file
386
client/android/billing/src/main/kotlin/BillingProvider.kt
Normal file
@@ -0,0 +1,386 @@
|
||||
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
|
||||
import java.text.NumberFormat
|
||||
import java.util.Currency
|
||||
|
||||
private const val TAG = "BillingProvider"
|
||||
private const val PRODUCT_ID = "premium"
|
||||
|
||||
private val billingPeriodRegex = Regex("""P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?""")
|
||||
|
||||
private fun billingPeriodToMonths(period: String): Double {
|
||||
val match = billingPeriodRegex.matchEntire(period) ?: return 0.0
|
||||
val years = match.groupValues[1].toDoubleOrNull() ?: 0.0
|
||||
val months = match.groupValues[2].toDoubleOrNull() ?: 0.0
|
||||
val weeks = match.groupValues[3].toDoubleOrNull() ?: 0.0
|
||||
val days = match.groupValues[4].toDoubleOrNull() ?: 0.0
|
||||
return years * 12.0 + months + weeks * 7.0 / 30.0 + days / 30.0
|
||||
}
|
||||
|
||||
private fun billingPeriodToDays(period: String): Int {
|
||||
val match = billingPeriodRegex.matchEntire(period) ?: return 0
|
||||
val years = match.groupValues[1].toIntOrNull() ?: 0
|
||||
val months = match.groupValues[2].toIntOrNull() ?: 0
|
||||
val weeks = match.groupValues[3].toIntOrNull() ?: 0
|
||||
val days = match.groupValues[4].toIntOrNull() ?: 0
|
||||
return years * 365 + months * 30 + weeks * 7 + days
|
||||
}
|
||||
|
||||
private fun displayPricePerMonth(priceAmountMicros: Long, currencyCode: String, billingMonths: Double): String? {
|
||||
if (billingMonths <= 1e-6 || currencyCode.isBlank()) return null
|
||||
return try {
|
||||
val amountPerMonth = (priceAmountMicros / 1_000_000.0) / billingMonths
|
||||
NumberFormat.getCurrencyInstance().apply { currency = Currency.getInstance(currencyCode) }
|
||||
.format(amountPerMonth)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log.w(TAG, "Unknown currency code from Play Billing: $currencyCode")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
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 ->
|
||||
val billingMonths = billingPeriodToMonths(phase.billingPeriod)
|
||||
JSONObject().also { pricingPhases.put(it) }
|
||||
.put("billingCycleCount", phase.billingCycleCount)
|
||||
.put("billingPeriod", phase.billingPeriod)
|
||||
.put("formatedPrice", phase.formattedPrice)
|
||||
.put("priceAmountMicros", phase.priceAmountMicros)
|
||||
.put("recurrenceMode", phase.recurrenceMode)
|
||||
.put("subscriptionBillingMonths", billingMonths)
|
||||
.apply {
|
||||
displayPricePerMonth(phase.priceAmountMicros, phase.priceCurrencyCode, billingMonths)?.let {
|
||||
put("displayPricePerMonth", it)
|
||||
}
|
||||
}
|
||||
}
|
||||
val regularPhase = offerDetails.pricingPhases.pricingPhaseList.lastOrNull()
|
||||
Log.v(TAG, "Offer ${offerDetails.basePlanId}: regular price = ${regularPhase?.formattedPrice}")
|
||||
|
||||
val trialPhase = offerDetails.pricingPhases.pricingPhaseList.firstOrNull()
|
||||
val hasFreeTrial = trialPhase != null && trialPhase.priceAmountMicros == 0L
|
||||
offer.put("hasFreeTrial", hasFreeTrial)
|
||||
if (hasFreeTrial) {
|
||||
offer.put("trialDays", billingPeriodToDays(trialPhase!!.billingPeriod))
|
||||
}
|
||||
|
||||
val phasesSummary = offerDetails.pricingPhases.pricingPhaseList.joinToString(", ") {
|
||||
"${it.billingPeriod}:${it.formattedPrice}"
|
||||
}
|
||||
/* Log.i(
|
||||
TAG,
|
||||
"Raw offer from Play: basePlanId=${offerDetails.basePlanId} offerId=${offerDetails.offerId} " +
|
||||
"offerToken=${offerDetails.offerToken} phases=[$phasesSummary] hasFreeTrial=$hasFreeTrial" +
|
||||
if (hasFreeTrial) " trialDays=${offer.getInt("trialDays")}" else ""
|
||||
)*/
|
||||
}
|
||||
}
|
||||
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 {
|
||||
// Per Google Play docs, switching between auto-renewing plans within the same
|
||||
// subscription only supports CHARGE_FULL_PRICE or WITHOUT_PRORATION - any other mode
|
||||
// (e.g. CHARGE_PRORATED_PRICE) is rejected with DEVELOPER_ERROR "Requested replacement
|
||||
// mode is not supported for this request".
|
||||
BillingFlowParams.SubscriptionUpdateParams.newBuilder()
|
||||
.setOldPurchaseToken(oldPurchaseToken)
|
||||
.setSubscriptionReplacementMode(ReplacementMode.CHARGE_FULL_PRICE)
|
||||
.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"
|
||||
kotlin = "2.1.0"
|
||||
android-billing = "8.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")
|
||||
|
||||
@@ -63,7 +63,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
|
||||
@@ -97,6 +96,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>()
|
||||
@@ -215,6 +215,7 @@ class AmneziaActivity : QtActivity() {
|
||||
registerBroadcastReceivers()
|
||||
intent?.let(::processIntent)
|
||||
runBlocking { vpnProto = proto.await() }
|
||||
billingRepository = BillingPaymentRepository(applicationContext)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
@@ -1118,15 +1119,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")
|
||||
@@ -1295,11 +1290,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
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -31,7 +31,6 @@ set(HEADERS ${HEADERS}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ioscontextmenu.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate-C-Interface.h
|
||||
@@ -43,7 +42,6 @@ set(SOURCES ${SOURCES}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ioscontextmenu.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.mm
|
||||
@@ -51,12 +49,6 @@ set(SOURCES ${SOURCES}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/AmneziaSceneDelegateHooks.mm
|
||||
)
|
||||
|
||||
# The context menu helper uses ARC-only constructs (weak references); the
|
||||
# rest of the Objective-C++ sources build with manual reference counting.
|
||||
set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ioscontextmenu.mm
|
||||
PROPERTIES COMPILE_OPTIONS "-fobjc-arc"
|
||||
)
|
||||
|
||||
|
||||
target_include_directories(${PROJECT} PRIVATE ${Qt6Gui_PRIVATE_INCLUDE_DIRS})
|
||||
|
||||
@@ -77,6 +69,7 @@ set_target_properties(${PROJECT} PROPERTIES
|
||||
XCODE_ATTRIBUTE_PRODUCT_NAME "AmneziaVPN"
|
||||
XCODE_ATTRIBUTE_BUNDLE_INFO_STRING "AmneziaVPN"
|
||||
XCODE_GENERATE_SCHEME TRUE
|
||||
XCODE_ATTRIBUTE_ENABLE_BITCODE "NO"
|
||||
XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME "AppIcon"
|
||||
XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2"
|
||||
XCODE_EMBED_FRAMEWORKS_CODE_SIGN_ON_COPY ON
|
||||
@@ -85,7 +78,7 @@ set_target_properties(${PROJECT} PROPERTIES
|
||||
XCODE_EMBED_APP_EXTENSIONS networkextension
|
||||
)
|
||||
|
||||
if(DEPLOY)
|
||||
if(DEFINED DEPLOY)
|
||||
set_target_properties(${PROJECT} PROPERTIES
|
||||
XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "Apple Distribution"
|
||||
XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY[variant=Debug] "Apple Development"
|
||||
|
||||
@@ -5,7 +5,6 @@ set_target_properties(${PROJECT} PROPERTIES MACOSX_BUNDLE TRUE)
|
||||
set(APPLE_PROJECT_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
|
||||
|
||||
enable_language(OBJC)
|
||||
enable_language(OBJCXX)
|
||||
enable_language(Swift)
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS ShaderTools Widgets)
|
||||
@@ -34,6 +33,7 @@ set(LIBS ${LIBS}
|
||||
set(HEADERS ${HEADERS}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate-C-Interface.h
|
||||
@@ -44,12 +44,19 @@ set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_contro
|
||||
set(SOURCES ${SOURCES}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.mm
|
||||
)
|
||||
|
||||
set(ICON_FILE ${CMAKE_CURRENT_SOURCE_DIR}/images/app.icns)
|
||||
set(MACOSX_BUNDLE_ICON_FILE app.icns)
|
||||
set_source_files_properties(${ICON_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
|
||||
set(SOURCES ${SOURCES} ${ICON_FILE})
|
||||
|
||||
|
||||
target_include_directories(${PROJECT} PRIVATE
|
||||
${Qt6Gui_PRIVATE_INCLUDE_DIRS}
|
||||
${Qt6Widgets_PRIVATE_INCLUDE_DIRS}
|
||||
@@ -62,7 +69,6 @@ set_target_properties(${PROJECT} PROPERTIES
|
||||
MACOSX_BUNDLE_ICON_FILE "AppIcon"
|
||||
MACOSX_BUNDLE_INFO_STRING "AmneziaVPN"
|
||||
MACOSX_BUNDLE_BUNDLE_NAME "AmneziaVPN"
|
||||
MACOSX_BUNDLE_GUI_IDENTIFIER "${BUILD_IOS_APP_IDENTIFIER}"
|
||||
MACOSX_BUNDLE_BUNDLE_VERSION "${CMAKE_PROJECT_VERSION_TWEAK}"
|
||||
MACOSX_BUNDLE_LONG_VERSION_STRING "${APPLE_PROJECT_VERSION}-${CMAKE_PROJECT_VERSION_TWEAK}"
|
||||
MACOSX_BUNDLE_SHORT_VERSION_STRING "${APPLE_PROJECT_VERSION}"
|
||||
@@ -73,10 +79,14 @@ set_target_properties(${PROJECT} PROPERTIES
|
||||
XCODE_ATTRIBUTE_PRODUCT_NAME "AmneziaVPN"
|
||||
XCODE_ATTRIBUTE_BUNDLE_INFO_STRING "AmneziaVPN"
|
||||
XCODE_GENERATE_SCHEME TRUE
|
||||
XCODE_ATTRIBUTE_ENABLE_BITCODE "NO"
|
||||
XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME "AppIcon"
|
||||
XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2"
|
||||
XCODE_EMBED_FRAMEWORKS_CODE_SIGN_ON_COPY "NO"
|
||||
XCODE_EMBED_FRAMEWORKS_REMOVE_HEADERS_ON_COPY "YES"
|
||||
XCODE_ATTRIBUTE_MACOSX_DEPLOYMENT_TARGET "11.0"
|
||||
|
||||
XCODE_LINK_BUILD_PHASE_MODE KNOWN_LOCATION
|
||||
XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/../Frameworks"
|
||||
XCODE_EMBED_APP_EXTENSIONS AmneziaVPNNetworkExtension
|
||||
)
|
||||
|
||||
@@ -96,17 +96,13 @@ ProtocolConfig AwgConfigurator::createConfig(const ServerCredentials &credential
|
||||
newClientConfig.specialJunk4 = configMap.value(configKey::specialJunk4);
|
||||
newClientConfig.specialJunk5 = configMap.value(configKey::specialJunk5);
|
||||
|
||||
newClientConfig.cookieReplyPacketJunkSize = configMap.value(configKey::cookieReplyPacketJunkSize);
|
||||
newClientConfig.transportPacketJunkSize = configMap.value(configKey::transportPacketJunkSize);
|
||||
|
||||
newClientConfig.headerProtectionKey = configMap.value(configKey::headerProtectionKey);
|
||||
newClientConfig.contentPaddingAddition = configMap.value(configKey::contentPaddingAddition);
|
||||
newClientConfig.rekeyAfterTime = configMap.value(configKey::rekeyAfterTime);
|
||||
newClientConfig.rekeyTimeout = configMap.value(configKey::rekeyTimeout);
|
||||
newClientConfig.rejectAfterTime = configMap.value(configKey::rejectAfterTime);
|
||||
newClientConfig.keepaliveTimeout = configMap.value(configKey::keepaliveTimeout);
|
||||
newClientConfig.maxHandshakeAttempts = configMap.value(configKey::maxHandshakeAttempts);
|
||||
|
||||
if (container == DockerContainer::Awg2) {
|
||||
newClientConfig.cookieReplyPacketJunkSize = configMap.value(configKey::cookieReplyPacketJunkSize);
|
||||
newClientConfig.transportPacketJunkSize = configMap.value(configKey::transportPacketJunkSize);
|
||||
}
|
||||
|
||||
newClientConfig.isObfuscationEnabled = false;
|
||||
|
||||
protocolConfig.setClientConfig(newClientConfig);
|
||||
|
||||
return protocolConfig;
|
||||
|
||||
@@ -228,20 +228,11 @@ ProtocolConfig WireguardConfigurator::createConfig(const ServerCredentials &cred
|
||||
}
|
||||
}
|
||||
|
||||
const bool isAwg3 = awgServerConfig && awgServerConfig->protocolVersion == protocols::awg::awgV3;
|
||||
|
||||
amnezia::ScriptVars vars = amnezia::genBaseVars(credentials, container, dnsSettings.primaryDns, dnsSettings.secondaryDns);
|
||||
vars.append(amnezia::genProtocolVarsForContainer(container, containerConfig));
|
||||
QString scriptData = amnezia::scriptData(m_configTemplate, container);
|
||||
QString config = m_sshSession->replaceVars(scriptData, vars);
|
||||
|
||||
// The template lists every possible key, but each parameter is optional -
|
||||
// drop the lines whose value came out empty
|
||||
static const QRegularExpression emptyValueLine(R"(^\s*\S+\s*=\s*$)");
|
||||
auto configTemplateLines = config.split("\n");
|
||||
configTemplateLines.removeIf([](const QString &line) { return emptyValueLine.match(line).hasMatch(); });
|
||||
config = configTemplateLines.join("\n");
|
||||
|
||||
ConnectionData connData = prepareWireguardConfig(credentials, container, wireguardServerConfig, awgServerConfig, dnsSettings, errorCode);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return WireGuardProtocolConfig{};
|
||||
@@ -275,8 +266,7 @@ ProtocolConfig WireguardConfigurator::createConfig(const ServerCredentials &cred
|
||||
clientConfig.presharedKey = connData.pskKey;
|
||||
clientConfig.clientId = connData.clientPubKey;
|
||||
clientConfig.allowedIps = QStringList { "0.0.0.0/0", "::/0" };
|
||||
clientConfig.persistentKeepAlive = isAwg3 ? protocols::awg::defaultPersistentKeepAlive
|
||||
: protocols::wireguard::defaultPersistentKeepAlive;
|
||||
clientConfig.persistentKeepAlive = protocols::wireguard::defaultPersistentKeepAlive;
|
||||
clientConfig.mtu = mtu;
|
||||
clientConfig.isObfuscationEnabled = false;
|
||||
|
||||
|
||||
@@ -85,12 +85,6 @@ namespace {
|
||||
return t.toLower();
|
||||
}
|
||||
|
||||
// xray wants int ranges as "from-to" string, not a {from,to} object.
|
||||
QString makeRangeString(const QString &minV, const QString &maxV)
|
||||
{
|
||||
return minV + QLatin1Char('-') + maxV;
|
||||
}
|
||||
|
||||
void putIntRangeIfAny(QJsonObject &obj, const char *key, QString minV, QString maxV, const char *fallbackMin,
|
||||
const char *fallbackMax)
|
||||
{
|
||||
@@ -100,23 +94,10 @@ namespace {
|
||||
minV = QString::fromLatin1(fallbackMin);
|
||||
if (maxV.isEmpty())
|
||||
maxV = QString::fromLatin1(fallbackMax);
|
||||
obj[QString::fromUtf8(key)] = makeRangeString(minV, maxV);
|
||||
}
|
||||
|
||||
QString effectiveClientFlow(const amnezia::XrayServerConfig &srv)
|
||||
{
|
||||
const bool rawTransport = srv.transport.isEmpty() || srv.transport == QLatin1String("raw");
|
||||
const bool secureFlow =
|
||||
srv.security == QLatin1String("tls") || srv.security == QLatin1String("reality");
|
||||
return (rawTransport && secureFlow) ? srv.flow : QString();
|
||||
}
|
||||
|
||||
QString effectiveSecurity(const amnezia::XrayServerConfig &srv)
|
||||
{
|
||||
if (srv.transport == QLatin1String("mkcp") && srv.security == QLatin1String("reality")) {
|
||||
return QStringLiteral("none");
|
||||
}
|
||||
return srv.security;
|
||||
QJsonObject r;
|
||||
r[QStringLiteral("from")] = minV.toInt();
|
||||
r[QStringLiteral("to")] = maxV.toInt();
|
||||
obj[QString::fromUtf8(key)] = r;
|
||||
}
|
||||
|
||||
// Desktop applies this in XrayProtocol::start(); iOS/Android pass JSON straight to libxray — same fixes here.
|
||||
@@ -216,7 +197,7 @@ QJsonObject XrayConfigurator::mergeStreamSettingsForServerInbound(const XrayServ
|
||||
{
|
||||
QJsonObject streamSettings = buildStreamSettings(srv, QString());
|
||||
|
||||
if (effectiveSecurity(srv) != QLatin1String("reality")) {
|
||||
if (srv.security != QLatin1String("reality")) {
|
||||
return streamSettings;
|
||||
}
|
||||
|
||||
@@ -263,10 +244,10 @@ ErrorCode XrayConfigurator::applyServerSettingsToRemote(const ServerCredentials
|
||||
<< "container=" << static_cast<int>(container) << "host=" << credentials.hostName
|
||||
<< "transport=" << srv.transport << "security=" << srv.security << "port=" << srv.port
|
||||
<< "appendClient=" << appendNewClient;
|
||||
const QString flowValue = effectiveClientFlow(srv);
|
||||
const QString flowValue = srv.flow;
|
||||
QString realityPublicKey;
|
||||
QString realityShortId;
|
||||
if (effectiveSecurity(srv) == QLatin1String("reality")) {
|
||||
if (srv.security == QLatin1String("reality")) {
|
||||
errorCode = readRealityKeyFiles(container, credentials, realityPublicKey, realityShortId);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Xray applyServerSettings: readRealityKeyFiles failed, error="
|
||||
@@ -382,129 +363,6 @@ ErrorCode XrayConfigurator::applyServerSettingsToRemote(const ServerCredentials
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode XrayConfigurator::readContainerKeyFile(DockerContainer container, const ServerCredentials &credentials,
|
||||
const QString &path, QString &out) const
|
||||
{
|
||||
out.clear();
|
||||
for (int attempt = 0; attempt < 3; ++attempt) {
|
||||
ErrorCode fileError = ErrorCode::NoError;
|
||||
out = QString::fromUtf8(m_sshSession->getTextFileFromContainer(container, credentials, path, fileError));
|
||||
out.replace(QLatin1Char('\n'), QString());
|
||||
out.replace(QLatin1Char('\r'), QString());
|
||||
if (fileError == ErrorCode::NoError && !out.isEmpty()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
if (attempt < 2) {
|
||||
QThread::msleep(500);
|
||||
}
|
||||
}
|
||||
logger.error() << "Xray readContainerKeyFile: failed path=" << path;
|
||||
return ErrorCode::XrayRealityKeysReadFailed;
|
||||
}
|
||||
|
||||
ErrorCode XrayConfigurator::writeServerConfigForSetup(const ServerCredentials &credentials, DockerContainer container,
|
||||
ContainerConfig &containerConfig, const DnsSettings &dnsSettings)
|
||||
{
|
||||
Q_UNUSED(dnsSettings);
|
||||
namespace px = amnezia::protocols::xray;
|
||||
|
||||
const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>();
|
||||
if (!xrayCfg) {
|
||||
logger.error() << "Xray writeServerConfigForSetup: missing XrayProtocolConfig";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
const XrayServerConfig &srv = xrayCfg->serverConfig;
|
||||
if (srv.isThirdPartyConfig) {
|
||||
logger.info() << "Xray writeServerConfigForSetup: skipped (third-party/native profile)";
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
logger.info() << "Xray writeServerConfigForSetup: start container=" << static_cast<int>(container)
|
||||
<< "transport=" << srv.transport << "security=" << srv.security << "port=" << srv.port;
|
||||
|
||||
ErrorCode errorCode = ErrorCode::NoError;
|
||||
|
||||
QString clientId;
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(px::uuidPath), clientId);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
const QString securityEff = effectiveSecurity(srv);
|
||||
|
||||
QString realityPrivateKey;
|
||||
QString realityPublicKey;
|
||||
QString realityShortId;
|
||||
if (securityEff == QLatin1String("reality")) {
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(px::PrivateKeyPath), realityPrivateKey);
|
||||
if (errorCode != ErrorCode::NoError)
|
||||
return errorCode;
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(px::PublicKeyPath), realityPublicKey);
|
||||
if (errorCode != ErrorCode::NoError)
|
||||
return errorCode;
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(px::shortidPath), realityShortId);
|
||||
if (errorCode != ErrorCode::NoError)
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
QJsonObject streamSettings = buildStreamSettings(srv, clientId);
|
||||
if (securityEff == QLatin1String("reality")) {
|
||||
const QString siteEff = srv.site.isEmpty() ? QString::fromLatin1(px::defaultSite) : srv.site;
|
||||
const QString sniEff = srv.sni.isEmpty() ? siteEff : srv.sni;
|
||||
const QString fpEff = srv.fingerprint.isEmpty() ? QString::fromLatin1(px::defaultFingerprint) : srv.fingerprint;
|
||||
QJsonObject rs;
|
||||
rs[QStringLiteral("dest")] = siteEff + QStringLiteral(":443");
|
||||
rs[px::fingerprint] = fpEff;
|
||||
rs[QStringLiteral("privateKey")] = realityPrivateKey;
|
||||
rs[px::serverNames] = QJsonArray { sniEff };
|
||||
rs[QStringLiteral("shortIds")] = QJsonArray { realityShortId };
|
||||
streamSettings[px::realitySettings] = rs;
|
||||
}
|
||||
|
||||
QJsonObject clientEntry;
|
||||
clientEntry[px::id] = clientId;
|
||||
const QString flowValue = effectiveClientFlow(srv);
|
||||
if (!flowValue.isEmpty()) {
|
||||
clientEntry[px::flow] = flowValue;
|
||||
}
|
||||
|
||||
QJsonObject settings;
|
||||
settings[px::clients] = QJsonArray { clientEntry };
|
||||
settings[QStringLiteral("decryption")] = QStringLiteral("none");
|
||||
|
||||
QJsonObject inbound;
|
||||
inbound[px::port] = srv.port.isEmpty() ? QString(px::defaultPort).toInt() : srv.port.toInt();
|
||||
inbound[QStringLiteral("protocol")] = QStringLiteral("vless");
|
||||
inbound[px::settings] = settings;
|
||||
inbound[px::streamSettings] = streamSettings;
|
||||
|
||||
QJsonObject serverConfig;
|
||||
serverConfig[QStringLiteral("log")] = QJsonObject { { QStringLiteral("loglevel"), QStringLiteral("error") } };
|
||||
serverConfig[px::inbounds] = QJsonArray { inbound };
|
||||
serverConfig[px::outbounds] =
|
||||
QJsonArray { QJsonObject { { QStringLiteral("protocol"), QStringLiteral("freedom") } } };
|
||||
|
||||
const QString json = QString::fromUtf8(QJsonDocument(serverConfig).toJson());
|
||||
errorCode = m_sshSession->uploadTextFileToContainer(container, credentials, json,
|
||||
QString::fromLatin1(px::serverConfigPath),
|
||||
libssh::ScpOverwriteMode::ScpOverwriteExisting);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Xray writeServerConfigForSetup: upload failed, error=" << static_cast<int>(errorCode);
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
XrayProtocolConfig updated =
|
||||
buildClientProtocolConfig(credentials, container, srv, clientId, errorCode, realityPublicKey, realityShortId);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Xray writeServerConfigForSetup: buildClientProtocolConfig failed, error="
|
||||
<< static_cast<int>(errorCode);
|
||||
return errorCode;
|
||||
}
|
||||
containerConfig.protocolConfig = updated;
|
||||
logger.info() << "Xray writeServerConfigForSetup: done, clientId=" << clientId;
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentials, DockerContainer container,
|
||||
const ContainerConfig &containerConfig,
|
||||
const DnsSettings &dnsSettings,
|
||||
@@ -531,9 +389,7 @@ XrayProtocolConfig XrayConfigurator::buildClientProtocolConfig(const ServerCrede
|
||||
QString xrayPublicKey = prefetchedRealityPublicKey;
|
||||
QString xrayShortId = prefetchedRealityShortId;
|
||||
|
||||
const QString securityEff = effectiveSecurity(srv);
|
||||
|
||||
if (securityEff == QLatin1String("reality")) {
|
||||
if (srv.security == QLatin1String("reality")) {
|
||||
if (xrayPublicKey.isEmpty() || xrayShortId.isEmpty()) {
|
||||
errorCode = readRealityKeyFiles(container, credentials, xrayPublicKey, xrayShortId);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
@@ -545,9 +401,8 @@ XrayProtocolConfig XrayConfigurator::buildClientProtocolConfig(const ServerCrede
|
||||
QJsonObject userObj;
|
||||
userObj[amnezia::protocols::xray::id] = clientId;
|
||||
userObj[amnezia::protocols::xray::encryption] = QStringLiteral("none");
|
||||
const QString flowValue = effectiveClientFlow(srv);
|
||||
if (!flowValue.isEmpty()) {
|
||||
userObj[amnezia::protocols::xray::flow] = flowValue;
|
||||
if (!srv.flow.isEmpty()) {
|
||||
userObj[amnezia::protocols::xray::flow] = srv.flow;
|
||||
}
|
||||
|
||||
QJsonObject vnextEntry;
|
||||
@@ -564,7 +419,7 @@ XrayProtocolConfig XrayConfigurator::buildClientProtocolConfig(const ServerCrede
|
||||
outbound[amnezia::protocols::xray::settings] = outboundSettings;
|
||||
|
||||
QJsonObject streamObj = buildStreamSettings(srv, clientId);
|
||||
if (securityEff == QLatin1String("reality")) {
|
||||
if (srv.security == QLatin1String("reality")) {
|
||||
QJsonObject rs = streamObj[amnezia::protocols::xray::realitySettings].toObject();
|
||||
rs[amnezia::protocols::xray::publicKey] = xrayPublicKey;
|
||||
rs[amnezia::protocols::xray::shortId] = xrayShortId;
|
||||
@@ -613,24 +468,18 @@ QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, c
|
||||
networkValue = QStringLiteral("kcp");
|
||||
streamSettings[px::network] = networkValue;
|
||||
|
||||
const QString securityEff = effectiveSecurity(srv);
|
||||
streamSettings[px::security] = securityEff;
|
||||
streamSettings[px::security] = srv.security;
|
||||
|
||||
if (securityEff == QLatin1String("tls")) {
|
||||
if (srv.security == QLatin1String("tls")) {
|
||||
QJsonObject tlsSettings;
|
||||
const QString sniEff = srv.sni.isEmpty() ? QString::fromLatin1(px::defaultSni) : srv.sni;
|
||||
tlsSettings[px::serverName] = sniEff;
|
||||
const QString alpnEff = srv.alpn.isEmpty() ? QString::fromLatin1(px::defaultAlpn) : srv.alpn;
|
||||
QJsonArray alpnArray;
|
||||
for (const QString &a : alpnEff.split(QLatin1Char(','))) {
|
||||
QString t = a.trimmed();
|
||||
if (t.isEmpty())
|
||||
continue;
|
||||
if (t.compare(QLatin1String("HTTP/2"), Qt::CaseInsensitive) == 0)
|
||||
t = QStringLiteral("h2");
|
||||
else if (t.compare(QLatin1String("HTTP/1.1"), Qt::CaseInsensitive) == 0)
|
||||
t = QStringLiteral("http/1.1");
|
||||
alpnArray.append(t);
|
||||
const QString t = a.trimmed();
|
||||
if (!t.isEmpty())
|
||||
alpnArray.append(t);
|
||||
}
|
||||
if (!alpnArray.isEmpty())
|
||||
tlsSettings[QStringLiteral("alpn")] = alpnArray;
|
||||
@@ -639,7 +488,7 @@ QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, c
|
||||
streamSettings[QStringLiteral("tlsSettings")] = tlsSettings;
|
||||
}
|
||||
|
||||
if (securityEff == QLatin1String("reality")) {
|
||||
if (srv.security == QLatin1String("reality")) {
|
||||
QJsonObject realSettings;
|
||||
const QString fpEff = srv.fingerprint.isEmpty() ? QString::fromLatin1(px::defaultFingerprint) : srv.fingerprint;
|
||||
realSettings[px::fingerprint] = fpEff;
|
||||
@@ -655,13 +504,13 @@ QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, c
|
||||
xo[QStringLiteral("host")] = hostEff;
|
||||
if (!xhttp.path.isEmpty())
|
||||
xo[QStringLiteral("path")] = xhttp.path;
|
||||
QString modeEff = normalizeXhttpMode(xhttp.mode);
|
||||
if (modeEff == QLatin1String("auto") || modeEff == QLatin1String("packet-up")) {
|
||||
modeEff = QStringLiteral("stream-one");
|
||||
}
|
||||
xo[QStringLiteral("mode")] = modeEff;
|
||||
xo[QStringLiteral("mode")] = normalizeXhttpMode(xhttp.mode);
|
||||
|
||||
// No "Host" in headers: xray rejects it when the top-level "host" field is set.
|
||||
if (xhttp.headersTemplate.compare(QLatin1String("HTTP"), Qt::CaseInsensitive) == 0) {
|
||||
QJsonObject headers;
|
||||
headers[QStringLiteral("Host")] = hostEff;
|
||||
xo[QStringLiteral("headers")] = headers;
|
||||
}
|
||||
|
||||
const QString methodEff =
|
||||
xhttp.uplinkMethod.isEmpty() ? QString::fromLatin1(px::defaultXhttpUplinkMethod) : xhttp.uplinkMethod;
|
||||
@@ -672,27 +521,27 @@ QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, c
|
||||
|
||||
const QString sessPl = normalizeSessionSeqPlacement(xhttp.sessionPlacement);
|
||||
if (!sessPl.isEmpty())
|
||||
xo[QStringLiteral("sessionIDPlacement")] = sessPl;
|
||||
xo[QStringLiteral("sessionPlacement")] = sessPl;
|
||||
const QString seqPl = normalizeSessionSeqPlacement(xhttp.seqPlacement);
|
||||
if (!seqPl.isEmpty())
|
||||
xo[QStringLiteral("seqPlacement")] = seqPl;
|
||||
if (!xhttp.sessionKey.isEmpty())
|
||||
xo[QStringLiteral("sessionIDKey")] = xhttp.sessionKey;
|
||||
xo[QStringLiteral("sessionKey")] = xhttp.sessionKey;
|
||||
if (!xhttp.seqKey.isEmpty())
|
||||
xo[QStringLiteral("seqKey")] = xhttp.seqKey;
|
||||
|
||||
const QString uDataPl = normalizeUplinkDataPlacement(xhttp.uplinkDataPlacement);
|
||||
const bool uDataNeedsPacketUp =
|
||||
uDataPl == QLatin1String("header") || uDataPl == QLatin1String("cookie");
|
||||
if (!(uDataNeedsPacketUp && modeEff != QLatin1String("packet-up")))
|
||||
xo[QStringLiteral("uplinkDataPlacement")] = uDataPl;
|
||||
xo[QStringLiteral("uplinkDataPlacement")] = normalizeUplinkDataPlacement(xhttp.uplinkDataPlacement);
|
||||
if (!xhttp.uplinkDataKey.isEmpty())
|
||||
xo[QStringLiteral("uplinkDataKey")] = xhttp.uplinkDataKey;
|
||||
|
||||
const QString ucs = xhttp.uplinkChunkSize.isEmpty() ? QString::fromLatin1(px::defaultXhttpUplinkChunkSize)
|
||||
: xhttp.uplinkChunkSize;
|
||||
if (!ucs.isEmpty() && ucs != QLatin1String("0")) {
|
||||
xo[QStringLiteral("uplinkChunkSize")] = ucs.toInt();
|
||||
const int v = ucs.toInt();
|
||||
QJsonObject chunkR;
|
||||
chunkR[QStringLiteral("from")] = v;
|
||||
chunkR[QStringLiteral("to")] = v;
|
||||
xo[QStringLiteral("uplinkChunkSize")] = chunkR;
|
||||
}
|
||||
|
||||
if (!xhttp.scMaxBufferedPosts.isEmpty())
|
||||
@@ -709,20 +558,17 @@ QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, c
|
||||
xo[QStringLiteral("xPaddingObfsMode")] = pad.obfsMode;
|
||||
if (pad.obfsMode) {
|
||||
if (!pad.bytesMin.isEmpty() || !pad.bytesMax.isEmpty()) {
|
||||
const int fromV = pad.bytesMin.isEmpty()
|
||||
? QString::fromLatin1(px::defaultXPaddingBytesMin).toInt()
|
||||
: pad.bytesMin.toInt();
|
||||
int toV = pad.bytesMax.isEmpty()
|
||||
? QString::fromLatin1(px::defaultXPaddingBytesMax).toInt()
|
||||
: pad.bytesMax.toInt();
|
||||
QJsonObject br;
|
||||
const int fromV = pad.bytesMin.isEmpty() ? 1 : pad.bytesMin.toInt();
|
||||
int toV = pad.bytesMax.isEmpty() ? 256 : pad.bytesMax.toInt();
|
||||
if (toV < fromV)
|
||||
toV = fromV;
|
||||
xo[QStringLiteral("xPaddingBytes")] = makeRangeString(QString::number(fromV), QString::number(toV));
|
||||
br[QStringLiteral("from")] = fromV;
|
||||
br[QStringLiteral("to")] = toV;
|
||||
xo[QStringLiteral("xPaddingBytes")] = br;
|
||||
}
|
||||
xo[QStringLiteral("xPaddingKey")] =
|
||||
pad.key.isEmpty() ? QString::fromLatin1(px::defaultXPaddingKey) : pad.key;
|
||||
xo[QStringLiteral("xPaddingHeader")] =
|
||||
pad.header.isEmpty() ? QString::fromLatin1(px::defaultXPaddingHeader) : pad.header;
|
||||
xo[QStringLiteral("xPaddingKey")] = pad.key.isEmpty() ? QStringLiteral("x_padding") : pad.key;
|
||||
xo[QStringLiteral("xPaddingHeader")] = pad.header.isEmpty() ? QStringLiteral("X-Padding") : pad.header;
|
||||
xo[QStringLiteral("xPaddingPlacement")] = normalizeXPaddingPlacement(
|
||||
pad.placement.isEmpty() ? QString::fromLatin1(px::defaultXPaddingPlacement) : pad.placement);
|
||||
xo[QStringLiteral("xPaddingMethod")] = normalizeXPaddingMethod(
|
||||
@@ -733,14 +579,12 @@ QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, c
|
||||
if (xhttp.xmux.enabled) {
|
||||
QJsonObject mux;
|
||||
auto addMuxRange = [&](const char *key, const QString &a, const QString &b) {
|
||||
// omit empty / 0-0 ranges (xray may reject "0-0")
|
||||
const bool aZero = a.isEmpty() || a == QLatin1String("0");
|
||||
const bool bZero = b.isEmpty() || b == QLatin1String("0");
|
||||
if (aZero && bZero)
|
||||
if (a.isEmpty() && b.isEmpty())
|
||||
return;
|
||||
const QString aV = a.isEmpty() ? QStringLiteral("0") : a;
|
||||
const QString bV = b.isEmpty() ? QStringLiteral("0") : b;
|
||||
mux[QString::fromUtf8(key)] = makeRangeString(aV, bV);
|
||||
QJsonObject r;
|
||||
r[QStringLiteral("from")] = a.isEmpty() ? 0 : a.toInt();
|
||||
r[QStringLiteral("to")] = b.isEmpty() ? 0 : b.toInt();
|
||||
mux[QString::fromUtf8(key)] = r;
|
||||
};
|
||||
addMuxRange("maxConcurrency", xhttp.xmux.maxConcurrencyMin, xhttp.xmux.maxConcurrencyMax);
|
||||
addMuxRange("maxConnections", xhttp.xmux.maxConnectionsMin, xhttp.xmux.maxConnectionsMax);
|
||||
|
||||
@@ -30,16 +30,7 @@ public:
|
||||
bool appendNewClient,
|
||||
QString *outClientId = nullptr);
|
||||
|
||||
amnezia::ErrorCode writeServerConfigForSetup(const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container,
|
||||
amnezia::ContainerConfig &containerConfig,
|
||||
const amnezia::DnsSettings &dnsSettings);
|
||||
|
||||
private:
|
||||
amnezia::ErrorCode readContainerKeyFile(amnezia::DockerContainer container,
|
||||
const amnezia::ServerCredentials &credentials,
|
||||
const QString &path, QString &out) const;
|
||||
|
||||
QString prepareServerConfig(const amnezia::ServerCredentials &credentials, amnezia::DockerContainer container, const amnezia::ContainerConfig &containerConfig,
|
||||
const amnezia::DnsSettings &dnsSettings,
|
||||
amnezia::ErrorCode &errorCode);
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
#include "platforms/ios/ios_controller.h"
|
||||
#elif defined(Q_OS_ANDROID)
|
||||
#include "platforms/android/android_controller.h"
|
||||
#include <QFutureWatcher>
|
||||
#include <QtConcurrent>
|
||||
#endif
|
||||
|
||||
namespace
|
||||
@@ -31,6 +35,8 @@ namespace
|
||||
constexpr char subtitle[] = "subtitle";
|
||||
constexpr char isTrial[] = "is_trial";
|
||||
constexpr char minPriceLabel[] = "min_price_label";
|
||||
constexpr char hasFreeTrial[] = "has_free_trial";
|
||||
constexpr char trialDays[] = "trial_days";
|
||||
}
|
||||
|
||||
namespace serviceType
|
||||
@@ -38,12 +44,14 @@ namespace
|
||||
constexpr char amneziaPremium[] = "amnezia-premium";
|
||||
}
|
||||
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
struct StoreKitPlanQuote {
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE) || defined(Q_OS_ANDROID)
|
||||
struct SubscriptionPlanQuote {
|
||||
QString displayPrice;
|
||||
double priceAmount = 0.0;
|
||||
double subscriptionBillingMonths = 0.0;
|
||||
QString displayPricePerMonth;
|
||||
bool hasFreeTrial = false;
|
||||
int trialDays = 0;
|
||||
};
|
||||
|
||||
constexpr double oneMonthThreshold = 1.0 + 1e-6;
|
||||
@@ -76,48 +84,9 @@ namespace
|
||||
return productIds;
|
||||
}
|
||||
|
||||
QHash<QString, StoreKitPlanQuote> buildStoreKitQuoteMap(const QList<QVariantMap> &fetchedProducts)
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
QHash<QString, SubscriptionPlanQuote> buildStoreKitQuoteMap(const QStringList &productIds)
|
||||
{
|
||||
QHash<QString, StoreKitPlanQuote> quotesByProductId;
|
||||
quotesByProductId.reserve(fetchedProducts.size());
|
||||
|
||||
for (const QVariantMap &productInfo : fetchedProducts) {
|
||||
const QString productId = productInfo.value(QStringLiteral("productId")).toString();
|
||||
if (productId.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString displayPrice = productInfo.value(QStringLiteral("displayPrice")).toString();
|
||||
if (displayPrice.isEmpty()) {
|
||||
const QString price = productInfo.value(QStringLiteral("price")).toString();
|
||||
const QString currencyCode = productInfo.value(QStringLiteral("currencyCode")).toString();
|
||||
displayPrice = currencyCode.isEmpty() ? price : (price + QLatin1Char(' ') + currencyCode);
|
||||
}
|
||||
|
||||
StoreKitPlanQuote quote;
|
||||
quote.displayPrice = displayPrice;
|
||||
quote.priceAmount = productInfo.value(QStringLiteral("priceAmount")).toDouble();
|
||||
quote.subscriptionBillingMonths = productInfo.value(QStringLiteral("subscriptionBillingMonths")).toDouble();
|
||||
quote.displayPricePerMonth = productInfo.value(QStringLiteral("displayPricePerMonth")).toString();
|
||||
quotesByProductId.insert(productId, quote);
|
||||
}
|
||||
|
||||
return quotesByProductId;
|
||||
}
|
||||
|
||||
void mergeStoreKitPricesIntoPremiumPlans(QJsonObject &data)
|
||||
{
|
||||
QJsonArray services = data.value(apiDefs::key::services).toArray();
|
||||
if (services.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QStringList productIds = collectPremiumStoreProductIds(services);
|
||||
if (productIds.isEmpty()) {
|
||||
qInfo().noquote() << "[IAP] No store_product_id in premium plans; skip StoreKit merge into services payload";
|
||||
return;
|
||||
}
|
||||
|
||||
QList<QVariantMap> fetchedProducts;
|
||||
QEventLoop loop;
|
||||
IosController::Instance()->fetchProducts(productIds,
|
||||
@@ -134,8 +103,134 @@ namespace
|
||||
});
|
||||
loop.exec();
|
||||
|
||||
const QHash<QString, StoreKitPlanQuote> quotesByProductId = buildStoreKitQuoteMap(fetchedProducts);
|
||||
QHash<QString, SubscriptionPlanQuote> quotesByProductId;
|
||||
quotesByProductId.reserve(fetchedProducts.size());
|
||||
|
||||
for (const QVariantMap &productInfo : fetchedProducts) {
|
||||
const QString productId = productInfo.value(QStringLiteral("productId")).toString();
|
||||
if (productId.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString displayPrice = productInfo.value(QStringLiteral("displayPrice")).toString();
|
||||
if (displayPrice.isEmpty()) {
|
||||
const QString price = productInfo.value(QStringLiteral("price")).toString();
|
||||
const QString currencyCode = productInfo.value(QStringLiteral("currencyCode")).toString();
|
||||
displayPrice = currencyCode.isEmpty() ? price : (price + QLatin1Char(' ') + currencyCode);
|
||||
}
|
||||
|
||||
SubscriptionPlanQuote quote;
|
||||
quote.priceAmount = productInfo.value(QStringLiteral("priceAmount")).toDouble();
|
||||
quote.subscriptionBillingMonths = productInfo.value(QStringLiteral("subscriptionBillingMonths")).toDouble();
|
||||
quote.displayPricePerMonth = productInfo.value(QStringLiteral("displayPricePerMonth")).toString();
|
||||
quote.hasFreeTrial = productInfo.value(QStringLiteral("hasFreeTrial")).toBool();
|
||||
quote.trialDays = productInfo.value(QStringLiteral("trialDays")).toInt();
|
||||
|
||||
// If the account is eligible for a paid introductory discount (not a free trial, which
|
||||
// stays invisible here and just applies silently at purchase time), show that price instead.
|
||||
const QString introOfferDisplayPrice = productInfo.value(QStringLiteral("introOfferDisplayPrice")).toString();
|
||||
if (introOfferDisplayPrice.isEmpty()) {
|
||||
quote.displayPrice = displayPrice;
|
||||
} else {
|
||||
quote.displayPrice = introOfferDisplayPrice;
|
||||
qInfo().noquote() << "[IAP] Applying" << productInfo.value(QStringLiteral("introOfferPaymentMode")).toString()
|
||||
<< "intro offer price for" << productId << ":" << introOfferDisplayPrice;
|
||||
}
|
||||
quotesByProductId.insert(productId, quote);
|
||||
}
|
||||
|
||||
qInfo().noquote() << "[IAP] Built StoreKit quote map, quotes:" << quotesByProductId.size();
|
||||
return quotesByProductId;
|
||||
}
|
||||
#elif defined(Q_OS_ANDROID)
|
||||
QHash<QString, SubscriptionPlanQuote> buildGooglePlayQuoteMap(const QStringList &productIds)
|
||||
{
|
||||
Q_UNUSED(productIds); // Google Play returns every offer for the app's single product in one call
|
||||
|
||||
auto androidController = AndroidController::instance();
|
||||
QFutureWatcher<QJsonObject> watcher;
|
||||
QEventLoop waitLoop;
|
||||
QObject::connect(&watcher, &QFutureWatcher<QJsonObject>::finished, &waitLoop, &QEventLoop::quit);
|
||||
|
||||
QFuture<QJsonObject> future = QtConcurrent::run([androidController]() {
|
||||
return androidController->getSubscriptionPlans();
|
||||
});
|
||||
watcher.setFuture(future);
|
||||
waitLoop.exec();
|
||||
|
||||
QJsonObject plansResult = watcher.result();
|
||||
QHash<QString, SubscriptionPlanQuote> quotesByProductId;
|
||||
if (plansResult.value("responseCode").toInt(-1) != 0) {
|
||||
qWarning() << "[Billing] Failed to get subscription plans for price display, responseCode:"
|
||||
<< plansResult.value("responseCode").toInt(-1);
|
||||
return quotesByProductId;
|
||||
}
|
||||
qInfo() << "[Billing] Fetched subscription plans for price display";
|
||||
|
||||
const QJsonArray products = plansResult.value("products").toArray();
|
||||
|
||||
// A base plan can have several offers (e.g. a plain listing plus a separate
|
||||
// promotional free-trial offer). Scan all offers per basePlanId up front so a
|
||||
// trial is found even when it isn't the first offer encountered below.
|
||||
QHash<QString, int> trialDaysByBasePlanId;
|
||||
for (const QJsonValue &productValue : products) {
|
||||
const QJsonArray offers = productValue.toObject().value("offers").toArray();
|
||||
for (const QJsonValue &offerValue : offers) {
|
||||
const QJsonObject offer = offerValue.toObject();
|
||||
const QString basePlanId = offer.value("basePlanId").toString();
|
||||
if (basePlanId.isEmpty() || trialDaysByBasePlanId.contains(basePlanId)) {
|
||||
continue;
|
||||
}
|
||||
if (offer.value("hasFreeTrial").toBool()) {
|
||||
trialDaysByBasePlanId.insert(basePlanId, offer.value("trialDays").toInt());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const QJsonValue &productValue : products) {
|
||||
const QJsonArray offers = productValue.toObject().value("offers").toArray();
|
||||
for (const QJsonValue &offerValue : offers) {
|
||||
const QJsonObject offer = offerValue.toObject();
|
||||
const QString basePlanId = offer.value("basePlanId").toString();
|
||||
if (basePlanId.isEmpty() || quotesByProductId.contains(basePlanId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const QJsonArray pricingPhases = offer.value("pricingPhases").toArray();
|
||||
if (pricingPhases.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
// Last phase is always the ongoing recurring price, after any trial/intro phases
|
||||
const QJsonObject regularPhase = pricingPhases.last().toObject();
|
||||
|
||||
SubscriptionPlanQuote quote;
|
||||
quote.displayPrice = regularPhase.value("formatedPrice").toString();
|
||||
if (quote.displayPrice.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
quote.priceAmount = regularPhase.value("priceAmountMicros").toDouble() / 1000000.0;
|
||||
quote.subscriptionBillingMonths = regularPhase.value("subscriptionBillingMonths").toDouble();
|
||||
quote.displayPricePerMonth = regularPhase.value("displayPricePerMonth").toString();
|
||||
quote.hasFreeTrial = trialDaysByBasePlanId.contains(basePlanId);
|
||||
quote.trialDays = trialDaysByBasePlanId.value(basePlanId, 0);
|
||||
qInfo().noquote() << "[Billing] Quote for" << basePlanId << "price:" << quote.displayPrice
|
||||
<< "hasFreeTrial:" << quote.hasFreeTrial << "trialDays:" << quote.trialDays;
|
||||
quotesByProductId.insert(basePlanId, quote);
|
||||
}
|
||||
}
|
||||
qInfo() << "[Billing] Built Google Play quote map, quotes:" << quotesByProductId.size();
|
||||
return quotesByProductId;
|
||||
}
|
||||
#endif
|
||||
|
||||
void mergeQuotesIntoPremiumPlans(QJsonObject &data, const QHash<QString, SubscriptionPlanQuote> "esByProductId)
|
||||
{
|
||||
QJsonArray services = data.value(apiDefs::key::services).toArray();
|
||||
if (services.isEmpty() || quotesByProductId.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int mergedPlanCount = 0;
|
||||
for (int serviceIndex = 0; serviceIndex < services.size(); ++serviceIndex) {
|
||||
QJsonObject serviceObject = services.at(serviceIndex).toObject();
|
||||
if (serviceObject.value(apiDefs::key::serviceType).toString() != serviceType::amneziaPremium) {
|
||||
@@ -166,8 +261,11 @@ namespace
|
||||
}
|
||||
|
||||
const bool isTrialPlan = planObject.value(configKey::isTrial).toBool();
|
||||
const StoreKitPlanQuote "e = *quoteIterator;
|
||||
const SubscriptionPlanQuote "e = *quoteIterator;
|
||||
planObject.insert(configKey::priceLabel, quote.displayPrice);
|
||||
planObject.insert(configKey::hasFreeTrial, quote.hasFreeTrial);
|
||||
planObject.insert(configKey::trialDays, quote.trialDays);
|
||||
++mergedPlanCount;
|
||||
|
||||
const double months = quote.subscriptionBillingMonths;
|
||||
if (!isTrialPlan && months > oneMonthThreshold && !quote.displayPricePerMonth.isEmpty()) {
|
||||
@@ -200,9 +298,32 @@ namespace
|
||||
serviceObject.insert(configKey::serviceDescription, descriptionObject);
|
||||
services.replace(serviceIndex, serviceObject);
|
||||
}
|
||||
qInfo().noquote() << "[Store] Merged store quotes into" << mergedPlanCount << "premium plan(s)";
|
||||
data.insert(apiDefs::key::services, services);
|
||||
}
|
||||
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
void mergeStoreKitPricesIntoPremiumPlans(QJsonObject &data)
|
||||
{
|
||||
const QStringList productIds = collectPremiumStoreProductIds(data.value(apiDefs::key::services).toArray());
|
||||
if (productIds.isEmpty()) {
|
||||
qInfo().noquote() << "[IAP] No store_product_id in premium plans; skip StoreKit merge into services payload";
|
||||
return;
|
||||
}
|
||||
mergeQuotesIntoPremiumPlans(data, buildStoreKitQuoteMap(productIds));
|
||||
}
|
||||
#elif defined(Q_OS_ANDROID)
|
||||
void mergeGooglePlayPricesIntoPremiumPlans(QJsonObject &data)
|
||||
{
|
||||
const QStringList productIds = collectPremiumStoreProductIds(data.value(apiDefs::key::services).toArray());
|
||||
if (productIds.isEmpty()) {
|
||||
qInfo().noquote() << "[Billing] No store_product_id in premium plans; skip Google Play merge into services payload";
|
||||
return;
|
||||
}
|
||||
mergeQuotesIntoPremiumPlans(data, buildGooglePlayQuoteMap(productIds));
|
||||
}
|
||||
#endif
|
||||
#endif // Q_OS_IOS || MACOS_NE || Q_OS_ANDROID
|
||||
}
|
||||
|
||||
ServicesCatalogController::ServicesCatalogController(SecureAppSettingsRepository* appSettingsRepository)
|
||||
@@ -220,6 +341,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;
|
||||
@@ -234,6 +357,8 @@ ErrorCode ServicesCatalogController::fillAvailableServices(QJsonObject &services
|
||||
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
mergeStoreKitPricesIntoPremiumPlans(servicesData);
|
||||
#elif defined(Q_OS_ANDROID)
|
||||
mergeGooglePlayPricesIntoPremiumPlans(servicesData);
|
||||
#endif
|
||||
|
||||
return ErrorCode::NoError;
|
||||
@@ -241,7 +366,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;
|
||||
@@ -127,6 +130,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 +159,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();
|
||||
@@ -188,14 +194,24 @@ ErrorCode SubscriptionController::extractServerConfigJsonFromResponse(const QByt
|
||||
|
||||
// TODO looks like this block can be removed after v1 configs EOL
|
||||
|
||||
const QStringList awgProtocolKeys = configKey::awgProtocolKeys();
|
||||
serverProtocolConfig[configKey::junkPacketCount] = clientProtocolConfig.value(configKey::junkPacketCount);
|
||||
serverProtocolConfig[configKey::junkPacketMinSize] = clientProtocolConfig.value(configKey::junkPacketMinSize);
|
||||
serverProtocolConfig[configKey::junkPacketMaxSize] = clientProtocolConfig.value(configKey::junkPacketMaxSize);
|
||||
serverProtocolConfig[configKey::initPacketJunkSize] = clientProtocolConfig.value(configKey::initPacketJunkSize);
|
||||
serverProtocolConfig[configKey::responsePacketJunkSize] = clientProtocolConfig.value(configKey::responsePacketJunkSize);
|
||||
serverProtocolConfig[configKey::initPacketMagicHeader] = clientProtocolConfig.value(configKey::initPacketMagicHeader);
|
||||
serverProtocolConfig[configKey::responsePacketMagicHeader] = clientProtocolConfig.value(configKey::responsePacketMagicHeader);
|
||||
serverProtocolConfig[configKey::underloadPacketMagicHeader] = clientProtocolConfig.value(configKey::underloadPacketMagicHeader);
|
||||
serverProtocolConfig[configKey::transportPacketMagicHeader] = clientProtocolConfig.value(configKey::transportPacketMagicHeader);
|
||||
|
||||
for (const QString &key : awgProtocolKeys) {
|
||||
const QJsonValue value = clientProtocolConfig.value(key);
|
||||
if (value.isString() && !value.toString().isEmpty()) {
|
||||
serverProtocolConfig[key] = value;
|
||||
}
|
||||
}
|
||||
serverProtocolConfig[configKey::cookieReplyPacketJunkSize] = clientProtocolConfig.value(configKey::cookieReplyPacketJunkSize);
|
||||
serverProtocolConfig[configKey::transportPacketJunkSize] = clientProtocolConfig.value(configKey::transportPacketJunkSize);
|
||||
serverProtocolConfig[configKey::specialJunk1] = clientProtocolConfig.value(configKey::specialJunk1);
|
||||
serverProtocolConfig[configKey::specialJunk2] = clientProtocolConfig.value(configKey::specialJunk2);
|
||||
serverProtocolConfig[configKey::specialJunk3] = clientProtocolConfig.value(configKey::specialJunk3);
|
||||
serverProtocolConfig[configKey::specialJunk4] = clientProtocolConfig.value(configKey::specialJunk4);
|
||||
serverProtocolConfig[configKey::specialJunk5] = clientProtocolConfig.value(configKey::specialJunk5);
|
||||
serverProtocolConfig[configKey::headerProtectionKey] = clientProtocolConfig.value(configKey::headerProtectionKey);
|
||||
|
||||
//
|
||||
|
||||
@@ -209,23 +225,23 @@ 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::serviceInfo)) {
|
||||
apiConfig.insert(apiDefs::key::serviceInfo, responseObj.value(apiDefs::key::serviceInfo).toObject());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
serverConfigJson[apiDefs::key::apiConfig] = apiConfig;
|
||||
}
|
||||
|
||||
@@ -236,6 +252,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)
|
||||
@@ -340,10 +376,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),
|
||||
@@ -353,14 +389,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;
|
||||
}
|
||||
@@ -404,7 +442,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;
|
||||
}
|
||||
@@ -590,7 +628,7 @@ ErrorCode SubscriptionController::deactivateDevice(const QString &serverId)
|
||||
if (!apiV2.has_value()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
|
||||
if (!apiV2->isPremium() && !apiV2->isExternalPremium()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
@@ -627,7 +665,7 @@ ErrorCode SubscriptionController::deactivateExternalDevice(const QString &server
|
||||
if (!apiV2.has_value()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
|
||||
if (!apiV2->isPremium() && !apiV2->isExternalPremium()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
@@ -824,7 +862,7 @@ bool SubscriptionController::isApiKeyExpired(const QString &serverId) const
|
||||
return false;
|
||||
}
|
||||
const QString expiresAt = apiV2->apiConfig.publicKey.expiresAt;
|
||||
|
||||
|
||||
if (expiresAt.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
@@ -833,7 +871,7 @@ bool SubscriptionController::isApiKeyExpired(const QString &serverId) const
|
||||
if (expiresAtDateTime < QDateTime::currentDateTimeUtc()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -917,8 +955,158 @@ 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]() {
|
||||
// If the user already has an active "premium" subscription, upgrade/replace it with proration
|
||||
// instead of stacking a second, independent purchase that Google Play would just queue behind it.
|
||||
QString oldPurchaseToken;
|
||||
QJsonObject existingPurchasesResult = androidController->queryPurchases();
|
||||
if (existingPurchasesResult.value("responseCode").toInt(-1) == 0) {
|
||||
const QJsonArray existingPurchases = existingPurchasesResult.value("purchases").toArray();
|
||||
for (const QJsonValue &purchaseValue : existingPurchases) {
|
||||
const QJsonObject existingPurchase = purchaseValue.toObject();
|
||||
if (existingPurchase.value("purchaseState").toInt(-1) == 1) { // PURCHASED
|
||||
oldPurchaseToken = existingPurchase.value("purchaseToken").toString();
|
||||
qInfo() << "[Billing] Found existing active subscription, will upgrade instead of purchasing a new one";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
QString fallbackOfferToken;
|
||||
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) continue;
|
||||
|
||||
const QString token = offer.value("offerToken").toString();
|
||||
QJsonArray pricingPhases = offer.value("pricingPhases").toArray();
|
||||
const bool hasFreeTrial = !pricingPhases.isEmpty()
|
||||
&& pricingPhases.first().toObject().value("priceAmountMicros").toDouble() == 0;
|
||||
|
||||
// Google Play's subscription replacement API rejects switching to an offer with an
|
||||
// introductory/trial phase ("Requested replacement mode is not supported for this
|
||||
// request"), so an upgrade must always target the regular, non-trial offer - skip
|
||||
// trial offers entirely here rather than just deprioritizing them.
|
||||
if (hasFreeTrial && !oldPurchaseToken.isEmpty()) continue;
|
||||
|
||||
if (fallbackOfferToken.isEmpty()) fallbackOfferToken = token;
|
||||
|
||||
if (hasFreeTrial) {
|
||||
offerToken = token;
|
||||
qInfo() << "[Billing] Found free trial offer for basePlanId:" << productId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!offerToken.isEmpty()) break;
|
||||
}
|
||||
if (offerToken.isEmpty()) offerToken = fallbackOfferToken;
|
||||
if (offerToken.isEmpty()) {
|
||||
qWarning() << "[Billing] No offer token found for basePlanId:" << productId;
|
||||
return qMakePair(false, QString());
|
||||
}
|
||||
QJsonObject purchaseResult = oldPurchaseToken.isEmpty()
|
||||
? androidController->purchaseSubscription(offerToken)
|
||||
: androidController->upgradeSubscription(offerToken, oldPurchaseToken);
|
||||
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);
|
||||
@@ -939,6 +1127,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;
|
||||
@@ -947,6 +1138,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;
|
||||
@@ -960,6 +1155,7 @@ SubscriptionController::AppStoreRestoreResult SubscriptionController::processApp
|
||||
}
|
||||
|
||||
bool isTestPurchase = IosController::Instance()->isTestFlight();
|
||||
qInfo().noquote() << "[IAP][processAppStoreRestore] isTestFlight=" << isTestPurchase;
|
||||
QSet<QString> processedTransactions;
|
||||
|
||||
for (const QVariantMap &transaction : restoredTransactions) {
|
||||
@@ -967,6 +1163,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;
|
||||
@@ -974,6 +1173,7 @@ SubscriptionController::AppStoreRestoreResult SubscriptionController::processApp
|
||||
|
||||
if (processedTransactions.contains(originalTransactionId)) {
|
||||
result.duplicateCount++;
|
||||
qInfo().noquote() << "[IAP][processAppStoreRestore] Skipping duplicate originalTransactionId=" << originalTransactionId;
|
||||
continue;
|
||||
}
|
||||
processedTransactions.insert(originalTransactionId);
|
||||
@@ -983,9 +1183,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;
|
||||
@@ -995,7 +1199,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;
|
||||
@@ -1006,6 +1211,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);
|
||||
@@ -1018,12 +1349,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),
|
||||
|
||||
@@ -38,6 +38,7 @@ public:
|
||||
QString serviceType;
|
||||
QString serviceProtocol;
|
||||
QJsonObject authData;
|
||||
QString transactionId;
|
||||
|
||||
QJsonObject toJsonObject() const;
|
||||
};
|
||||
@@ -61,10 +62,15 @@ 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 getSubscriptionInfo(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const QString &purchaseToken,
|
||||
QByteArray &responseBody);
|
||||
|
||||
ErrorCode updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, bool isConnectEvent,
|
||||
CaptchaInfo *captchaInfoOut = nullptr, ProtocolData *usedProtocolDataOut = nullptr);
|
||||
@@ -107,10 +113,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,6 +138,9 @@ 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;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "ipSplitTunnelingController.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
#include <QJsonObject>
|
||||
#include <QDebug>
|
||||
|
||||
IpSplitTunnelingController::IpSplitTunnelingController(SecureAppSettingsRepository* appSettingsRepository, QObject* parent)
|
||||
: QObject(parent),
|
||||
@@ -15,56 +14,47 @@ IpSplitTunnelingController::IpSplitTunnelingController(SecureAppSettingsReposito
|
||||
fillSites();
|
||||
}
|
||||
|
||||
bool IpSplitTunnelingController::addSiteInternal(const QString &hostname, const QStringList &ips)
|
||||
bool IpSplitTunnelingController::addSiteInternal(const QString &hostname, const QString &ip)
|
||||
{
|
||||
QVariantMap existing = m_appSettingsRepository->vpnSites(m_currentRouteMode);
|
||||
if (existing.contains(hostname) && ips.isEmpty()) {
|
||||
if (existing.contains(hostname) && ip.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < m_sites.size(); i++) {
|
||||
if (m_sites[i].first == hostname) {
|
||||
bool changed = false;
|
||||
for (const QString &ip : ips) {
|
||||
if (!ip.isEmpty() && !m_sites[i].second.contains(ip)) {
|
||||
m_sites[i].second.append(ip);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (!changed) {
|
||||
return false;
|
||||
}
|
||||
m_appSettingsRepository->addVpnSite(m_currentRouteMode, hostname, ips);
|
||||
if (m_sites[i].first == hostname && (m_sites[i].second.isEmpty() && !ip.isEmpty())) {
|
||||
m_sites[i].second = ip;
|
||||
m_appSettingsRepository->addVpnSite(m_currentRouteMode, hostname, ip);
|
||||
return true;
|
||||
} else if (m_sites[i].first == hostname && (m_sites[i].second == ip)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
m_sites.append(qMakePair(hostname, ips));
|
||||
m_appSettingsRepository->addVpnSite(m_currentRouteMode, hostname, ips);
|
||||
m_sites.append(qMakePair(hostname, ip));
|
||||
m_appSettingsRepository->addVpnSite(m_currentRouteMode, hostname, ip);
|
||||
return true;
|
||||
}
|
||||
|
||||
void IpSplitTunnelingController::addSites(const QMap<QString, QStringList> &sites, bool replaceExisting)
|
||||
void IpSplitTunnelingController::addSites(const QMap<QString, QString> &sites, bool replaceExisting)
|
||||
{
|
||||
if (replaceExisting) {
|
||||
m_sites.clear();
|
||||
}
|
||||
for (auto it = sites.constBegin(); it != sites.constEnd(); ++it) {
|
||||
const QString &hostname = it.key();
|
||||
const QStringList &ips = it.value();
|
||||
const QString &ip = it.value();
|
||||
bool found = false;
|
||||
for (int i = 0; i < m_sites.size(); i++) {
|
||||
if (m_sites[i].first == hostname) {
|
||||
for (const QString &ip : ips) {
|
||||
if (!ip.isEmpty() && !m_sites[i].second.contains(ip)) {
|
||||
m_sites[i].second.append(ip);
|
||||
}
|
||||
if (!ip.isEmpty()) {
|
||||
m_sites[i].second = ip;
|
||||
}
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
m_sites.append(qMakePair(hostname, ips));
|
||||
m_sites.append(qMakePair(hostname, ip));
|
||||
}
|
||||
}
|
||||
if (replaceExisting) {
|
||||
@@ -82,11 +72,11 @@ bool IpSplitTunnelingController::addSite(const QString &hostname)
|
||||
}
|
||||
|
||||
if (NetworkUtilities::ipAddressWithSubnetRegExp().exactMatch(normalizedHostname)) {
|
||||
processSite(normalizedHostname, {});
|
||||
processSite(normalizedHostname, "");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (addSiteInternal(normalizedHostname, {})) {
|
||||
if (addSiteInternal(normalizedHostname, "")) {
|
||||
QHostInfo::lookupHost(normalizedHostname, this, SLOT(onHostResolved(QHostInfo)));
|
||||
return true;
|
||||
}
|
||||
@@ -134,7 +124,7 @@ bool IpSplitTunnelingController::isSplitTunnelingEnabled() const
|
||||
return m_appSettingsRepository->isSitesSplitTunnelingEnabled();
|
||||
}
|
||||
|
||||
QVector<QPair<QString, QStringList>> IpSplitTunnelingController::getCurrentSites() const
|
||||
QVector<QPair<QString, QString>> IpSplitTunnelingController::getCurrentSites() const
|
||||
{
|
||||
return m_sites;
|
||||
}
|
||||
@@ -144,7 +134,7 @@ void IpSplitTunnelingController::fillSites()
|
||||
QVariantMap sitesMap = m_appSettingsRepository->vpnSites(m_currentRouteMode);
|
||||
m_sites.clear();
|
||||
for (auto it = sitesMap.begin(); it != sitesMap.end(); ++it) {
|
||||
m_sites.append(qMakePair(it.key(), SecureAppSettingsRepository::siteIpList(it.value())));
|
||||
m_sites.append(qMakePair(it.key(), it.value().toString()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,40 +164,29 @@ void IpSplitTunnelingController::onHostResolved(const QHostInfo &hostInfo)
|
||||
{
|
||||
const QList<QHostAddress> &addresses = hostInfo.addresses();
|
||||
QString hostname = hostInfo.hostName();
|
||||
|
||||
QStringList allIpv4;
|
||||
|
||||
for (const QHostAddress &addr : addresses) {
|
||||
if (addr.protocol() == QAbstractSocket::NetworkLayerProtocol::IPv4Protocol) {
|
||||
allIpv4.append(addr.toString());
|
||||
}
|
||||
}
|
||||
allIpv4.removeDuplicates();
|
||||
qDebug() << "[SplitTunneling] Host resolved:" << hostname
|
||||
<< "-> adding all IPv4 addresses to list:" << allIpv4;
|
||||
|
||||
if (!allIpv4.isEmpty()) {
|
||||
processSiteAfterResolve(hostname, allIpv4);
|
||||
}
|
||||
}
|
||||
|
||||
void IpSplitTunnelingController::processSiteAfterResolve(const QString &hostname, const QStringList &ips)
|
||||
{
|
||||
for (int i = 0; i < m_sites.size(); i++) {
|
||||
if (m_sites[i].first == hostname) {
|
||||
for (const QString &ip : ips) {
|
||||
if (!ip.isEmpty() && !m_sites[i].second.contains(ip)) {
|
||||
m_sites[i].second.append(ip);
|
||||
}
|
||||
}
|
||||
processSiteAfterResolve(hostname, addr.toString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
m_appSettingsRepository->addVpnSite(m_currentRouteMode, hostname, ips);
|
||||
}
|
||||
|
||||
void IpSplitTunnelingController::processSite(const QString &hostname, const QStringList &ips)
|
||||
void IpSplitTunnelingController::processSiteAfterResolve(const QString &hostname, const QString &ip)
|
||||
{
|
||||
addSiteInternal(hostname, ips);
|
||||
for (int i = 0; i < m_sites.size(); i++) {
|
||||
if (m_sites[i].first == hostname && m_sites[i].second.isEmpty()) {
|
||||
m_sites[i].second = ip;
|
||||
m_appSettingsRepository->addVpnSite(m_currentRouteMode, hostname, ip);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void IpSplitTunnelingController::processSite(const QString &hostname, const QString &ip)
|
||||
{
|
||||
addSiteInternal(hostname, ip);
|
||||
}
|
||||
|
||||
bool IpSplitTunnelingController::importSitesFromJson(const QByteArray& jsonData, bool replaceExisting, QString &errorMessage)
|
||||
@@ -226,25 +205,12 @@ bool IpSplitTunnelingController::importSitesFromJson(const QByteArray& jsonData,
|
||||
}
|
||||
|
||||
QJsonArray jsonArray = jsonDocument.array();
|
||||
QMap<QString, QStringList> sites;
|
||||
QMap<QString, QString> sites;
|
||||
|
||||
for (auto jsonValue : jsonArray) {
|
||||
QJsonObject jsonObject = jsonValue.toObject();
|
||||
QString hostname = jsonObject.value("hostname").toString("");
|
||||
|
||||
QStringList ips;
|
||||
if (jsonObject.value("ips").isArray()) {
|
||||
const QJsonArray ipsArray = jsonObject.value("ips").toArray();
|
||||
for (const auto &ipValue : ipsArray) {
|
||||
ips.append(ipValue.toString());
|
||||
}
|
||||
}
|
||||
const QString singleIp = jsonObject.value("ip").toString("");
|
||||
if (!singleIp.isEmpty()) {
|
||||
ips.append(singleIp);
|
||||
}
|
||||
ips.removeAll(QString());
|
||||
ips.removeDuplicates();
|
||||
QString ip = jsonObject.value("ip").toString("");
|
||||
|
||||
QString normalizedHostname = normalizeHostname(hostname);
|
||||
|
||||
@@ -253,7 +219,7 @@ bool IpSplitTunnelingController::importSitesFromJson(const QByteArray& jsonData,
|
||||
continue;
|
||||
}
|
||||
|
||||
sites.insert(normalizedHostname, ips);
|
||||
sites.insert(normalizedHostname, ip);
|
||||
}
|
||||
|
||||
addSites(sites, replaceExisting);
|
||||
@@ -263,21 +229,13 @@ bool IpSplitTunnelingController::importSitesFromJson(const QByteArray& jsonData,
|
||||
|
||||
QByteArray IpSplitTunnelingController::exportSitesToJson() const
|
||||
{
|
||||
QVector<QPair<QString, QStringList>> sites = getCurrentSites();
|
||||
QVector<QPair<QString, QString>> sites = getCurrentSites();
|
||||
QJsonArray jsonArray;
|
||||
|
||||
for (const auto &site : sites) {
|
||||
QJsonObject jsonObject;
|
||||
jsonObject["hostname"] = site.first;
|
||||
|
||||
QJsonArray ipsArray;
|
||||
for (const QString &ip : site.second) {
|
||||
ipsArray.append(ip);
|
||||
}
|
||||
jsonObject["ips"] = ipsArray;
|
||||
// Keep the legacy "ip" field (first address) for backward compatibility.
|
||||
jsonObject["ip"] = site.second.isEmpty() ? QString() : site.second.first();
|
||||
|
||||
jsonObject["ip"] = site.second;
|
||||
jsonArray.append(jsonObject);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ public:
|
||||
explicit IpSplitTunnelingController(SecureAppSettingsRepository* appSettingsRepository, QObject* parent = nullptr);
|
||||
|
||||
bool addSite(const QString &hostname);
|
||||
void addSites(const QMap<QString, QStringList> &sites, bool replaceExisting);
|
||||
void addSites(const QMap<QString, QString> &sites, bool replaceExisting);
|
||||
bool removeSite(const QString &hostname);
|
||||
void removeSites();
|
||||
void setRouteMode(RouteMode routeMode);
|
||||
@@ -33,7 +33,7 @@ public:
|
||||
|
||||
RouteMode getRouteMode() const;
|
||||
bool isSplitTunnelingEnabled() const;
|
||||
QVector<QPair<QString, QStringList>> getCurrentSites() const;
|
||||
QVector<QPair<QString, QString>> getCurrentSites() const;
|
||||
|
||||
bool importSitesFromJson(const QByteArray& jsonData, bool replaceExisting, QString &errorMessage);
|
||||
QByteArray exportSitesToJson() const;
|
||||
@@ -43,15 +43,15 @@ private slots:
|
||||
|
||||
private:
|
||||
void fillSites();
|
||||
bool addSiteInternal(const QString &hostname, const QStringList &ips);
|
||||
bool addSiteInternal(const QString &hostname, const QString &ip);
|
||||
QString normalizeHostname(const QString &hostname) const;
|
||||
bool validateHostname(const QString &hostname) const;
|
||||
void processSiteAfterResolve(const QString &hostname, const QStringList &ips);
|
||||
void processSite(const QString &hostname, const QStringList &ips);
|
||||
void processSiteAfterResolve(const QString &hostname, const QString &ip);
|
||||
void processSite(const QString &hostname, const QString &ip);
|
||||
|
||||
SecureAppSettingsRepository* m_appSettingsRepository;
|
||||
RouteMode m_currentRouteMode;
|
||||
QVector<QPair<QString, QStringList>> m_sites;
|
||||
QVector<QPair<QString, QString>> m_sites;
|
||||
};
|
||||
|
||||
#endif // IPSPLITTUNNELINGCONTROLLER_H
|
||||
|
||||
@@ -572,19 +572,47 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data, Config
|
||||
lastConfig[configKey::allowedIps] = allowedIpsJsonArray;
|
||||
|
||||
QString protocolName = configKey::wireguard;
|
||||
QString protocolVersion;
|
||||
ConfigTypes detectedType = ConfigTypes::WireGuard;
|
||||
|
||||
const QStringList awgProtocolKeys = configKey::awgProtocolKeys();
|
||||
const QStringList requiredJunkFields = { configKey::junkPacketCount, configKey::junkPacketMinSize,
|
||||
configKey::junkPacketMaxSize, configKey::initPacketJunkSize,
|
||||
configKey::responsePacketJunkSize, configKey::initPacketMagicHeader,
|
||||
configKey::responsePacketMagicHeader, configKey::underloadPacketMagicHeader,
|
||||
configKey::transportPacketMagicHeader };
|
||||
|
||||
bool hasAwgKeys = std::any_of(awgProtocolKeys.begin(), awgProtocolKeys.end(),
|
||||
[&configMap](const QString &field) { return !configMap.value(field).isEmpty(); });
|
||||
if (hasAwgKeys) {
|
||||
for (const QString &key : awgProtocolKeys) {
|
||||
if (!configMap.value(key).isEmpty()) {
|
||||
lastConfig[key] = configMap.value(key);
|
||||
const QStringList optionalJunkFields = { configKey::cookieReplyPacketJunkSize,
|
||||
configKey::transportPacketJunkSize,
|
||||
configKey::specialJunk1, configKey::specialJunk2, configKey::specialJunk3,
|
||||
configKey::specialJunk4, configKey::specialJunk5
|
||||
};
|
||||
|
||||
bool hasAllRequiredFields = std::all_of(requiredJunkFields.begin(), requiredJunkFields.end(),
|
||||
[&configMap](const QString &field) { return !configMap.value(field).isEmpty(); });
|
||||
if (hasAllRequiredFields) {
|
||||
for (const QString &field : requiredJunkFields) {
|
||||
lastConfig[field] = configMap.value(field);
|
||||
}
|
||||
|
||||
for (const QString &field : optionalJunkFields) {
|
||||
if (!configMap.value(field).isEmpty()) {
|
||||
lastConfig[field] = configMap.value(field);
|
||||
}
|
||||
}
|
||||
|
||||
bool hasCookieReplyPacketJunkSize = !configMap.value(configKey::cookieReplyPacketJunkSize).isEmpty();
|
||||
bool hasTransportPacketJunkSize = !configMap.value(configKey::transportPacketJunkSize).isEmpty();
|
||||
bool hasSpecialJunk = !configMap.value(configKey::specialJunk1).isEmpty() ||
|
||||
!configMap.value(configKey::specialJunk2).isEmpty() ||
|
||||
!configMap.value(configKey::specialJunk3).isEmpty() ||
|
||||
!configMap.value(configKey::specialJunk4).isEmpty() ||
|
||||
!configMap.value(configKey::specialJunk5).isEmpty();
|
||||
|
||||
if (hasCookieReplyPacketJunkSize && hasTransportPacketJunkSize) {
|
||||
protocolVersion = "2";
|
||||
} else if (hasSpecialJunk && !hasCookieReplyPacketJunkSize && !hasTransportPacketJunkSize) {
|
||||
protocolVersion = "1.5";
|
||||
}
|
||||
protocolName = configKey::awg;
|
||||
detectedType = ConfigTypes::Awg;
|
||||
}
|
||||
@@ -602,6 +630,9 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data, Config
|
||||
wireguardConfig[configKey::isThirdPartyConfig] = true;
|
||||
wireguardConfig[configKey::port] = port;
|
||||
wireguardConfig[configKey::transportProto] = protocols::openvpn::defaultTransportProto;
|
||||
if (protocolName == configKey::awg && !protocolVersion.isEmpty()) {
|
||||
wireguardConfig[configKey::protocolVersion] = protocolVersion;
|
||||
}
|
||||
|
||||
QJsonObject containers;
|
||||
QString containerName = (protocolName == configKey::awg) ? configKey::amneziaAwg : configKey::amneziaWireguard;
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#include <QtConcurrent>
|
||||
|
||||
#include "core/configurators/configuratorBase.h"
|
||||
#include "core/configurators/xrayConfigurator.h"
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
@@ -153,15 +152,6 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
|
||||
return e;
|
||||
qDebug().noquote() << "InstallController::setupContainer configureContainerWorker finished";
|
||||
|
||||
if (container == DockerContainer::Xray || container == DockerContainer::SSXray) {
|
||||
DnsSettings dnsSettings = { m_appSettingsRepository->primaryDns(), m_appSettingsRepository->secondaryDns() };
|
||||
XrayConfigurator xrayConfigurator(&sshSession);
|
||||
e = xrayConfigurator.writeServerConfigForSetup(credentials, container, config, dnsSettings);
|
||||
if (e)
|
||||
return e;
|
||||
qDebug().noquote() << "InstallController::setupContainer xray writeServerConfigForSetup finished";
|
||||
}
|
||||
|
||||
setupServerFirewall(credentials, sshSession);
|
||||
qDebug().noquote() << "InstallController::setupContainer setupServerFirewall finished";
|
||||
|
||||
@@ -201,21 +191,11 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
|
||||
SshSession sshSession;
|
||||
|
||||
bool reinstallRequired = isReinstallContainerRequired(container, oldConfig, newConfig);
|
||||
if (container == DockerContainer::Xray || container == DockerContainer::SSXray) {
|
||||
reinstallRequired = true;
|
||||
}
|
||||
qDebug() << "InstallController::updateServerConfig for container" << container << "reinstall required is" << reinstallRequired;
|
||||
|
||||
ErrorCode errorCode = ErrorCode::NoError;
|
||||
if (reinstallRequired) {
|
||||
errorCode = setupContainer(credentials, container, newConfig, true);
|
||||
|
||||
// Reinstall pulls the latest container image, so the server runs the latest protocol version
|
||||
if (errorCode == ErrorCode::NoError && container == DockerContainer::Awg2) {
|
||||
if (auto* awgConfig = newConfig.getAwgProtocolConfig()) {
|
||||
awgConfig->serverConfig.protocolVersion = protocols::awg::awgV3;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
errorCode = configureContainerWorker(credentials, container, newConfig, sshSession);
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
@@ -417,11 +397,6 @@ ErrorCode InstallController::prepareContainerConfig(DockerContainer container, c
|
||||
}
|
||||
|
||||
if (ContainerUtils::containerService(container) != ServiceType::Other) {
|
||||
if ((container == DockerContainer::Xray || container == DockerContainer::SSXray)
|
||||
&& containerConfig.protocolConfig.hasClientConfig()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
Proto protocol = ContainerUtils::defaultProtocol(container);
|
||||
|
||||
DnsSettings dnsSettings = {
|
||||
@@ -509,12 +484,6 @@ ErrorCode InstallController::buildContainerWorker(const ServerCredentials &crede
|
||||
if (stdOut.contains("have reached") && stdOut.contains("pull rate limit"))
|
||||
return ErrorCode::DockerPullRateLimit;
|
||||
|
||||
if (stdOut.contains("returned a non-zero code")
|
||||
|| stdOut.contains("failed to solve")
|
||||
|| stdOut.contains("Unable to find image")
|
||||
|| stdOut.contains("Couldn't connect to server"))
|
||||
return ErrorCode::ServerDockerFailedError;
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
@@ -539,8 +508,6 @@ ErrorCode InstallController::runContainerWorker(const ServerCredentials &credent
|
||||
return ErrorCode::ServerPortAlreadyAllocatedError;
|
||||
if (stdOut.contains("invalid publish"))
|
||||
return ErrorCode::ServerDockerFailedError;
|
||||
if (stdOut.contains("Unable to find image") || stdOut.contains("No such image"))
|
||||
return ErrorCode::ServerDockerFailedError;
|
||||
|
||||
return e;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
#include "awgInstaller.h"
|
||||
|
||||
#include <QPair>
|
||||
#include <QRandomGenerator>
|
||||
#include <QSet>
|
||||
#include <QStringList>
|
||||
#include <QVector>
|
||||
|
||||
#include "core/configurators/wireguardConfigurator.h"
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
@@ -27,57 +28,109 @@ AwgInstaller::AwgInstaller(QObject *parent)
|
||||
ContainerConfig AwgInstaller::generateConfig(DockerContainer container, int port, TransportProto transportProto)
|
||||
{
|
||||
ContainerConfig config = createBaseConfig(container, port, transportProto);
|
||||
|
||||
bool isAwg2 = (container == DockerContainer::Awg2);
|
||||
|
||||
if (auto* awgConfig = config.getAwgProtocolConfig()) {
|
||||
generateAwgParameters(awgConfig->serverConfig);
|
||||
awgConfig->serverConfig.protocolVersion = protocols::awg::awgV3;
|
||||
generateAwgParameters(awgConfig->serverConfig, isAwg2);
|
||||
|
||||
if (isAwg2) {
|
||||
awgConfig->serverConfig.protocolVersion = "2";
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
void AwgInstaller::generateAwgParameters(AwgServerConfig &serverConfig)
|
||||
void AwgInstaller::generateAwgParameters(AwgServerConfig &serverConfig, bool isAwg2)
|
||||
{
|
||||
QString junkPacketCount = QString::number(QRandomGenerator::global()->bounded(4, 7));
|
||||
QString junkPacketMinSize = QString::number(10);
|
||||
QString junkPacketMaxSize = QString::number(50);
|
||||
|
||||
int s1 = QRandomGenerator::global()->bounded(protocols::awg::junkPacketSizeMin, protocols::awg::initPacketJunkSizeMax);
|
||||
int s2 = QRandomGenerator::global()->bounded(protocols::awg::junkPacketSizeMin, protocols::awg::responsePacketJunkSizeMax);
|
||||
int s3 = QRandomGenerator::global()->bounded(protocols::awg::junkPacketSizeMin, protocols::awg::cookieReplyPacketJunkSizeMax);
|
||||
int s4 = protocols::awg::defaultTransportPacketJunkSize;
|
||||
int s1 = QRandomGenerator::global()->bounded(15, 150);
|
||||
int s2 = QRandomGenerator::global()->bounded(15, 150);
|
||||
int s3 = QRandomGenerator::global()->bounded(0, 64);
|
||||
int s4 = QRandomGenerator::global()->bounded(0, 20);
|
||||
|
||||
// Ensure all values are unique and don't create equal packet sizes
|
||||
QSet<int> usedValues { s1, s4 };
|
||||
QSet<int> usedValues;
|
||||
usedValues.insert(s1);
|
||||
|
||||
while (usedValues.contains(s2) || s1 + amnezia::AwgConstant::messageInitiationSize == s2 + amnezia::AwgConstant::messageResponseSize) {
|
||||
s2 = QRandomGenerator::global()->bounded(protocols::awg::junkPacketSizeMin, protocols::awg::responsePacketJunkSizeMax);
|
||||
s2 = QRandomGenerator::global()->bounded(15, 150);
|
||||
}
|
||||
usedValues.insert(s2);
|
||||
|
||||
while (usedValues.contains(s3) || s1 + amnezia::AwgConstant::messageInitiationSize == s3 + amnezia::AwgConstant::messageCookieReplySize
|
||||
|| s2 + amnezia::AwgConstant::messageResponseSize == s3 + amnezia::AwgConstant::messageCookieReplySize) {
|
||||
s3 = QRandomGenerator::global()->bounded(protocols::awg::junkPacketSizeMin, protocols::awg::cookieReplyPacketJunkSizeMax);
|
||||
s3 = QRandomGenerator::global()->bounded(0, 64);
|
||||
}
|
||||
usedValues.insert(s3);
|
||||
|
||||
while (usedValues.contains(s4)) {
|
||||
s4 = QRandomGenerator::global()->bounded(0, 20);
|
||||
}
|
||||
|
||||
QString initPacketJunkSize = QString::number(s1);
|
||||
QString responsePacketJunkSize = QString::number(s2);
|
||||
QString cookieReplyPacketJunkSize = QString::number(s3);
|
||||
QString transportPacketJunkSize = QString::number(s4);
|
||||
|
||||
QString initPacketMagicHeader;
|
||||
QString responsePacketMagicHeader;
|
||||
QString underloadPacketMagicHeader;
|
||||
QString transportPacketMagicHeader;
|
||||
|
||||
if (isAwg2) {
|
||||
// AWG 2.0: use range format for magic headers
|
||||
QVector<QPair<QString, QString>> headersValue;
|
||||
int min = 5;
|
||||
auto max = (std::numeric_limits<qint32>::max)();
|
||||
while (headersValue.size() != 4) {
|
||||
auto first = QRandomGenerator::global()->bounded(min, max);
|
||||
auto second = QRandomGenerator::global()->bounded(first, max);
|
||||
min = second;
|
||||
headersValue.push_back(QPair<QString, QString>(QString::number(first), QString::number(second)));
|
||||
}
|
||||
|
||||
initPacketMagicHeader = headersValue.at(0).first + "-" + headersValue.at(0).second;
|
||||
responsePacketMagicHeader = headersValue.at(1).first + "-" + headersValue.at(1).second;
|
||||
underloadPacketMagicHeader = headersValue.at(2).first + "-" + headersValue.at(2).second;
|
||||
transportPacketMagicHeader = headersValue.at(3).first + "-" + headersValue.at(3).second;
|
||||
} else {
|
||||
// AWG legacy: use single values for magic headers
|
||||
QSet<QString> headersValue;
|
||||
while (headersValue.size() != 4) {
|
||||
auto max = (std::numeric_limits<qint32>::max)();
|
||||
headersValue.insert(QString::number(QRandomGenerator::global()->bounded(5, max)));
|
||||
}
|
||||
|
||||
auto headersValueList = headersValue.values();
|
||||
initPacketMagicHeader = headersValueList.at(0);
|
||||
responsePacketMagicHeader = headersValueList.at(1);
|
||||
underloadPacketMagicHeader = headersValueList.at(2);
|
||||
transportPacketMagicHeader = headersValueList.at(3);
|
||||
}
|
||||
|
||||
serverConfig.junkPacketCount = junkPacketCount;
|
||||
serverConfig.junkPacketMinSize = junkPacketMinSize;
|
||||
serverConfig.junkPacketMaxSize = junkPacketMaxSize;
|
||||
serverConfig.initPacketJunkSize = QString::number(s1);
|
||||
serverConfig.responsePacketJunkSize = QString::number(s2);
|
||||
serverConfig.cookieReplyPacketJunkSize = QString::number(s3);
|
||||
serverConfig.transportPacketJunkSize = QString::number(s4);
|
||||
serverConfig.initPacketJunkSize = initPacketJunkSize;
|
||||
serverConfig.responsePacketJunkSize = responsePacketJunkSize;
|
||||
serverConfig.initPacketMagicHeader = initPacketMagicHeader;
|
||||
serverConfig.responsePacketMagicHeader = responsePacketMagicHeader;
|
||||
serverConfig.underloadPacketMagicHeader = underloadPacketMagicHeader;
|
||||
serverConfig.transportPacketMagicHeader = transportPacketMagicHeader;
|
||||
|
||||
serverConfig.initPacketMagicHeader = protocols::awg::defaultInitPacketMagicHeader;
|
||||
serverConfig.responsePacketMagicHeader = protocols::awg::defaultResponsePacketMagicHeader;
|
||||
serverConfig.underloadPacketMagicHeader = protocols::awg::defaultUnderloadPacketMagicHeader;
|
||||
serverConfig.transportPacketMagicHeader = protocols::awg::defaultTransportPacketMagicHeader;
|
||||
serverConfig.cookieReplyPacketJunkSize = cookieReplyPacketJunkSize;
|
||||
serverConfig.transportPacketJunkSize = transportPacketJunkSize;
|
||||
|
||||
serverConfig.headerProtectionKey = WireguardConfigurator::genClientKeys().clientPrivKey;
|
||||
serverConfig.contentPaddingAddition = protocols::awg::defaultContentPaddingAddition;
|
||||
serverConfig.rekeyAfterTime = protocols::awg::defaultRekeyAfterTime;
|
||||
serverConfig.rekeyTimeout = protocols::awg::defaultRekeyTimeout;
|
||||
serverConfig.rejectAfterTime = protocols::awg::defaultRejectAfterTime;
|
||||
serverConfig.keepaliveTimeout = protocols::awg::defaultKeepaliveTimeout;
|
||||
serverConfig.maxHandshakeAttempts = protocols::awg::defaultMaxHandshakeAttempts;
|
||||
serverConfig.specialJunk1 = protocols::awg::defaultSpecialJunk1;
|
||||
serverConfig.specialJunk2 = protocols::awg::defaultSpecialJunk2;
|
||||
serverConfig.specialJunk3 = protocols::awg::defaultSpecialJunk3;
|
||||
serverConfig.specialJunk4 = protocols::awg::defaultSpecialJunk4;
|
||||
serverConfig.specialJunk5 = protocols::awg::defaultSpecialJunk5;
|
||||
}
|
||||
|
||||
ErrorCode AwgInstaller::extractConfigFromContainer(DockerContainer container, const ServerCredentials &credentials,
|
||||
@@ -134,20 +187,14 @@ ErrorCode AwgInstaller::extractConfigFromContainer(DockerContainer container, co
|
||||
awgConfig->serverConfig.specialJunk4 = serverConfigMap.value(QString("# ") + configKey::specialJunk4);
|
||||
awgConfig->serverConfig.specialJunk5 = serverConfigMap.value(QString("# ") + configKey::specialJunk5);
|
||||
|
||||
awgConfig->serverConfig.cookieReplyPacketJunkSize = serverConfigMap.value(configKey::cookieReplyPacketJunkSize);
|
||||
awgConfig->serverConfig.transportPacketJunkSize = serverConfigMap.value(configKey::transportPacketJunkSize);
|
||||
|
||||
awgConfig->serverConfig.headerProtectionKey = serverConfigMap.value(configKey::headerProtectionKey);
|
||||
awgConfig->serverConfig.contentPaddingAddition = serverConfigMap.value(configKey::contentPaddingAddition);
|
||||
awgConfig->serverConfig.rekeyAfterTime = serverConfigMap.value(configKey::rekeyAfterTime);
|
||||
awgConfig->serverConfig.rekeyTimeout = serverConfigMap.value(configKey::rekeyTimeout);
|
||||
awgConfig->serverConfig.rejectAfterTime = serverConfigMap.value(configKey::rejectAfterTime);
|
||||
awgConfig->serverConfig.keepaliveTimeout = serverConfigMap.value(configKey::keepaliveTimeout);
|
||||
awgConfig->serverConfig.maxHandshakeAttempts = serverConfigMap.value(configKey::maxHandshakeAttempts);
|
||||
|
||||
awgConfig->serverConfig.protocolVersion = awgConfig->serverProtocolVersion();
|
||||
// AWG 2.0 specific fields
|
||||
if (container == DockerContainer::Awg2) {
|
||||
awgConfig->serverConfig.protocolVersion = "2";
|
||||
awgConfig->serverConfig.cookieReplyPacketJunkSize = serverConfigMap.value(configKey::cookieReplyPacketJunkSize);
|
||||
awgConfig->serverConfig.transportPacketJunkSize = serverConfigMap.value(configKey::transportPacketJunkSize);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ public:
|
||||
SshSession* serverController, amnezia::ContainerConfig &config) override;
|
||||
|
||||
private:
|
||||
void generateAwgParameters(amnezia::AwgServerConfig &serverConfig);
|
||||
void generateAwgParameters(amnezia::AwgServerConfig &serverConfig, bool isAwg2 = false);
|
||||
};
|
||||
|
||||
#endif // AWGINSTALLER_H
|
||||
|
||||
@@ -56,7 +56,6 @@ ContainerConfig InstallerBase::createBaseConfig(DockerContainer container, int p
|
||||
AwgProtocolConfig awgConfig;
|
||||
awgConfig.serverConfig.port = portStr;
|
||||
awgConfig.serverConfig.transportProto = transportProtoStr;
|
||||
awgConfig.serverConfig.subnetAddress = protocols::wireguard::defaultSubnetAddress;
|
||||
config.protocolConfig = awgConfig;
|
||||
break;
|
||||
}
|
||||
@@ -64,7 +63,6 @@ ContainerConfig InstallerBase::createBaseConfig(DockerContainer container, int p
|
||||
WireGuardProtocolConfig wgConfig;
|
||||
wgConfig.serverConfig.port = portStr;
|
||||
wgConfig.serverConfig.transportProto = transportProtoStr;
|
||||
wgConfig.serverConfig.subnetAddress = protocols::wireguard::defaultSubnetAddress;
|
||||
config.protocolConfig = wgConfig;
|
||||
break;
|
||||
}
|
||||
@@ -78,16 +76,8 @@ ContainerConfig InstallerBase::createBaseConfig(DockerContainer container, int p
|
||||
case Proto::Xray:
|
||||
case Proto::SSXray: {
|
||||
XrayProtocolConfig xrayConfig;
|
||||
XrayServerConfig &srv = xrayConfig.serverConfig;
|
||||
srv.port = portStr;
|
||||
srv.transportProto = transportProtoStr;
|
||||
srv.transport = protocols::xray::defaultTransport;
|
||||
srv.security = protocols::xray::defaultSecurity;
|
||||
srv.flow = protocols::xray::defaultFlow;
|
||||
srv.site = protocols::xray::defaultSite;
|
||||
srv.sni = protocols::xray::defaultSni;
|
||||
srv.fingerprint = protocols::xray::defaultFingerprint;
|
||||
srv.alpn = protocols::xray::defaultAlpn;
|
||||
xrayConfig.serverConfig.port = portStr;
|
||||
xrayConfig.serverConfig.transportProto = transportProtoStr;
|
||||
config.protocolConfig = xrayConfig;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/utils/selfhosted/sshSession.h"
|
||||
#include "core/models/containerConfig.h"
|
||||
@@ -21,8 +20,6 @@ namespace {
|
||||
constexpr QLatin1String kMtProxyClientJsonPath("/data/amnezia-mtproxy-client.json");
|
||||
constexpr QLatin1String kMtProxyClientJsonUploadPath("data/amnezia-mtproxy-client.json");
|
||||
constexpr QLatin1String kMtProxySecretPath("/data/secret");
|
||||
constexpr QLatin1String kMtProxyMetaPath("/data/mtproxy-meta");
|
||||
constexpr QLatin1String kMtProxyStartScriptPath("/opt/amnezia/start.sh");
|
||||
}
|
||||
|
||||
MtProxyInstaller::MtProxyInstaller(QObject *parent)
|
||||
@@ -56,98 +53,14 @@ ErrorCode MtProxyInstaller::extractConfigFromContainer(DockerContainer container
|
||||
}
|
||||
}
|
||||
|
||||
static const QRegularExpression hex32(QStringLiteral("^[0-9a-fA-F]{32}$"));
|
||||
const auto addExtra = [&](const QString &s) {
|
||||
if (hex32.match(s).hasMatch() && !mt->additionalSecrets.contains(s)) {
|
||||
mt->additionalSecrets.append(s);
|
||||
}
|
||||
};
|
||||
|
||||
ErrorCode secretErr = ErrorCode::NoError;
|
||||
const QByteArray secretRaw =
|
||||
sshSession->getTextFileFromContainer(container, credentials, QString(kMtProxySecretPath), secretErr);
|
||||
const QString sec = QString::fromUtf8(secretRaw).trimmed();
|
||||
if (sec.length() == 32 && hex32.match(sec).hasMatch()) {
|
||||
mt->secret = sec;
|
||||
}
|
||||
|
||||
bool metaRestored = false;
|
||||
ErrorCode metaErr = ErrorCode::NoError;
|
||||
const QByteArray metaRaw =
|
||||
sshSession->getTextFileFromContainer(container, credentials, QString(kMtProxyMetaPath), metaErr);
|
||||
if (metaErr == ErrorCode::NoError && !metaRaw.trimmed().isEmpty()) {
|
||||
QString mode, domain, workersMode, workers, natInternal, natExternal;
|
||||
bool natEnabled = false;
|
||||
const QList<QByteArray> lines = metaRaw.split('\n');
|
||||
for (const QByteArray &rawLine : lines) {
|
||||
const QString line = QString::fromUtf8(rawLine).trimmed();
|
||||
const int eq = line.indexOf('=');
|
||||
if (eq < 0) {
|
||||
continue;
|
||||
}
|
||||
const QString key = line.left(eq);
|
||||
const QString val = line.mid(eq + 1).trimmed();
|
||||
if (key == QLatin1String("mode")) mode = val;
|
||||
else if (key == QLatin1String("domain")) domain = val;
|
||||
else if (key == QLatin1String("tag")) { if (mt->tag.isEmpty()) mt->tag = val; }
|
||||
else if (key == QLatin1String("additional")) {
|
||||
for (const QString &s : val.split(',', Qt::SkipEmptyParts)) addExtra(s.trimmed());
|
||||
}
|
||||
else if (key == QLatin1String("workers_mode")) workersMode = val;
|
||||
else if (key == QLatin1String("workers")) workers = val;
|
||||
else if (key == QLatin1String("nat_enabled")) natEnabled = (val == QLatin1String("1"));
|
||||
else if (key == QLatin1String("nat_internal")) natInternal = val;
|
||||
else if (key == QLatin1String("nat_external")) natExternal = val;
|
||||
else if (key == QLatin1String("public_host")) { if (mt->publicHost.isEmpty()) mt->publicHost = val; }
|
||||
}
|
||||
if (!mode.isEmpty()) {
|
||||
mt->transportMode = mode;
|
||||
if (!domain.isEmpty()) mt->tlsDomain = domain;
|
||||
if (!workersMode.isEmpty()) mt->workersMode = workersMode;
|
||||
if (workersMode == QLatin1String(protocols::mtProxy::workersModeManual) && !workers.isEmpty()) {
|
||||
mt->workers = workers;
|
||||
}
|
||||
if (natEnabled) {
|
||||
mt->natEnabled = true;
|
||||
mt->natInternalIp = natInternal;
|
||||
mt->natExternalIp = natExternal;
|
||||
}
|
||||
metaRestored = true;
|
||||
}
|
||||
}
|
||||
if (!metaRestored) {
|
||||
ErrorCode startErr = ErrorCode::NoError;
|
||||
const QByteArray startRaw =
|
||||
sshSession->getTextFileFromContainer(container, credentials, QString(kMtProxyStartScriptPath), startErr);
|
||||
if (startErr == ErrorCode::NoError && !startRaw.trimmed().isEmpty()) {
|
||||
const QString start = QString::fromUtf8(startRaw);
|
||||
static const QRegularExpression modeRe(QStringLiteral("\\[ \"(standard|faketls)\" = \"faketls\" \\]"));
|
||||
const QRegularExpressionMatch m = modeRe.match(start);
|
||||
if (m.hasMatch()) {
|
||||
mt->transportMode = m.captured(1);
|
||||
if (m.captured(1) == QLatin1String(protocols::mtProxy::transportModeFakeTLS)) {
|
||||
static const QRegularExpression domRe(QStringLiteral("--domain ([A-Za-z0-9.\\-]+)"));
|
||||
const QRegularExpressionMatch dm = domRe.match(start);
|
||||
if (dm.hasMatch()) mt->tlsDomain = dm.captured(1);
|
||||
}
|
||||
}
|
||||
static const QRegularExpression tagRe(QStringLiteral("-P ([0-9a-fA-F]{32})"));
|
||||
const QRegularExpressionMatch tm = tagRe.match(start);
|
||||
if (tm.hasMatch() && mt->tag.isEmpty()) mt->tag = tm.captured(1);
|
||||
|
||||
static const QRegularExpression addRe(QStringLiteral("echo \"([0-9a-fA-F,]+)\" \\| tr ',' ' '"));
|
||||
const QRegularExpressionMatch am = addRe.match(start);
|
||||
if (am.hasMatch()) {
|
||||
for (const QString &s : am.captured(1).split(',', Qt::SkipEmptyParts)) addExtra(s.trimmed());
|
||||
}
|
||||
|
||||
static const QRegularExpression natRe(QStringLiteral("NAT_VALUE=\"([0-9.]+):([0-9.]+)\""));
|
||||
const QRegularExpressionMatch nm = natRe.match(start);
|
||||
if (nm.hasMatch()) {
|
||||
mt->natEnabled = true;
|
||||
mt->natInternalIp = nm.captured(1);
|
||||
mt->natExternalIp = nm.captured(2);
|
||||
}
|
||||
if (sec.length() == 32) {
|
||||
static const QRegularExpression hex32(QStringLiteral("^[0-9a-fA-F]{32}$"));
|
||||
if (hex32.match(sec).hasMatch()) {
|
||||
mt->secret = sec;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include "core/utils/selfhosted/sshSession.h"
|
||||
#include "core/models/containerConfig.h"
|
||||
#include "core/models/protocols/telemtProtocolConfig.h"
|
||||
@@ -20,7 +19,6 @@ namespace {
|
||||
constexpr QLatin1String kTelemtClientJsonPath("/data/amnezia-telemt-client.json");
|
||||
constexpr QLatin1String kTelemtClientJsonUploadPath("data/amnezia-telemt-client.json");
|
||||
constexpr QLatin1String kTelemtSecretPath("/data/secret");
|
||||
constexpr QLatin1String kTelemtConfigTomlPath("/data/config.toml");
|
||||
}
|
||||
|
||||
TelemtInstaller::TelemtInstaller(QObject *parent) : InstallerBase(parent) {}
|
||||
@@ -56,83 +54,10 @@ ErrorCode TelemtInstaller::extractConfigFromContainer(DockerContainer container,
|
||||
const QByteArray secretRaw =
|
||||
sshSession->getTextFileFromContainer(container, credentials, QString(kTelemtSecretPath), secretErr);
|
||||
const QString sec = QString::fromUtf8(secretRaw).trimmed();
|
||||
static const QRegularExpression hex32(QStringLiteral("^[0-9a-fA-F]{32}$"));
|
||||
if (sec.length() == 32 && hex32.match(sec).hasMatch()) {
|
||||
tc->secret = sec;
|
||||
}
|
||||
|
||||
ErrorCode tomlErr = ErrorCode::NoError;
|
||||
const QByteArray tomlRaw =
|
||||
sshSession->getTextFileFromContainer(container, credentials, QString(kTelemtConfigTomlPath), tomlErr);
|
||||
if (tomlErr == ErrorCode::NoError && !tomlRaw.trimmed().isEmpty()) {
|
||||
QString section;
|
||||
bool userNameSet = false;
|
||||
const QList<QByteArray> lines = tomlRaw.split('\n');
|
||||
for (const QByteArray &rawLine : lines) {
|
||||
const QString line = QString::fromUtf8(rawLine).trimmed();
|
||||
if (line.isEmpty() || line.startsWith('#')) {
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith('[')) {
|
||||
section = line;
|
||||
continue;
|
||||
}
|
||||
const int eq = line.indexOf('=');
|
||||
if (eq < 0) {
|
||||
continue;
|
||||
}
|
||||
const QString key = line.left(eq).trimmed();
|
||||
QString val = line.mid(eq + 1).trimmed();
|
||||
if (val.startsWith('"')) {
|
||||
const int last = val.lastIndexOf('"');
|
||||
val = (last > 0) ? val.mid(1, last - 1) : val.mid(1);
|
||||
} else {
|
||||
const int inlineComment = val.indexOf(QLatin1String(" #"));
|
||||
if (inlineComment >= 0) {
|
||||
val = val.left(inlineComment).trimmed();
|
||||
}
|
||||
}
|
||||
|
||||
if (section == QLatin1String("[access.users]")) {
|
||||
if (key.startsWith(QLatin1String("extra"))) {
|
||||
if (hex32.match(val).hasMatch() && !tc->additionalSecrets.contains(val)) {
|
||||
tc->additionalSecrets.append(val);
|
||||
}
|
||||
} else if (!userNameSet) {
|
||||
tc->userName = key;
|
||||
userNameSet = true;
|
||||
if (tc->secret.isEmpty() && hex32.match(val).hasMatch()) {
|
||||
tc->secret = val;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key == QLatin1String("tls") && section == QLatin1String("[general.modes]")) {
|
||||
tc->transportMode = (val == QLatin1String("true"))
|
||||
? QString::fromUtf8(protocols::telemt::transportModeFakeTLS)
|
||||
: QString::fromUtf8(protocols::telemt::transportModeStandard);
|
||||
} else if (key == QLatin1String("tls_domain") && section == QLatin1String("[censorship]")) {
|
||||
tc->tlsDomain = val;
|
||||
} else if (key == QLatin1String("mask") && section == QLatin1String("[censorship]")) {
|
||||
tc->maskEnabled = (val == QLatin1String("true"));
|
||||
} else if (key == QLatin1String("tls_emulation") && section == QLatin1String("[censorship]")) {
|
||||
tc->tlsEmulation = (val == QLatin1String("true"));
|
||||
} else if (key == QLatin1String("use_middle_proxy") && section == QLatin1String("[general]")) {
|
||||
tc->useMiddleProxy = (val == QLatin1String("true"));
|
||||
} else if (key == QLatin1String("middle_proxy_nat_ip") && section == QLatin1String("[general]")) {
|
||||
if (!val.isEmpty()) {
|
||||
tc->natExternalIp = val;
|
||||
tc->natEnabled = true;
|
||||
}
|
||||
} else if (key == QLatin1String("ad_tag") && section == QLatin1String("[general]") && tc->tag.isEmpty()) {
|
||||
tc->tag = val;
|
||||
} else if (key == QLatin1String("public_host") && section == QLatin1String("[general.links]")
|
||||
&& tc->publicHost.isEmpty()) {
|
||||
tc->publicHost = val;
|
||||
} else if (key == QLatin1String("port") && section == QLatin1String("[server]") && tc->port.isEmpty()) {
|
||||
tc->port = val;
|
||||
}
|
||||
if (sec.length() == 32) {
|
||||
static const QRegularExpression hex32(QStringLiteral("^[0-9a-fA-F]{32}$"));
|
||||
if (hex32.match(sec).hasMatch()) {
|
||||
tc->secret = sec;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,31 +26,6 @@ namespace
|
||||
}
|
||||
return fp;
|
||||
}
|
||||
|
||||
// Parse an xray int range: "from-to" string, plain int, or legacy {from,to} object.
|
||||
void parseIntRange(const QJsonValue &v, QString &minOut, QString &maxOut)
|
||||
{
|
||||
if (v.isString()) {
|
||||
const QString s = v.toString().trimmed();
|
||||
const int dash = s.indexOf(QLatin1Char('-'), 1);
|
||||
if (dash > 0) {
|
||||
minOut = s.left(dash).trimmed();
|
||||
maxOut = s.mid(dash + 1).trimmed();
|
||||
} else if (!s.isEmpty()) {
|
||||
minOut = s;
|
||||
maxOut = s;
|
||||
}
|
||||
} else if (v.isDouble()) {
|
||||
minOut = QString::number(v.toInt());
|
||||
maxOut = minOut;
|
||||
} else if (v.isObject()) {
|
||||
const QJsonObject o = v.toObject();
|
||||
if (o.contains(QLatin1String("from")) || o.contains(QLatin1String("to"))) {
|
||||
minOut = QString::number(o.value(QLatin1String("from")).toInt());
|
||||
maxOut = QString::number(o.value(QLatin1String("to")).toInt());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
using namespace amnezia;
|
||||
@@ -150,13 +125,7 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c
|
||||
QJsonArray alpnArr = tls.value("alpn").toArray();
|
||||
QStringList alpnList;
|
||||
for (const QJsonValue &v : alpnArr) {
|
||||
QString t = v.toString().trimmed();
|
||||
if (t.compare(QLatin1String("HTTP/2"), Qt::CaseInsensitive) == 0)
|
||||
t = QStringLiteral("h2");
|
||||
else if (t.compare(QLatin1String("HTTP/1.1"), Qt::CaseInsensitive) == 0)
|
||||
t = QStringLiteral("http/1.1");
|
||||
if (!t.isEmpty())
|
||||
alpnList << t;
|
||||
alpnList << v.toString();
|
||||
}
|
||||
srv.alpn = alpnList.join(",");
|
||||
}
|
||||
@@ -190,6 +159,12 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c
|
||||
srv.xhttp.host = xhttpObj.value("host").toString();
|
||||
srv.xhttp.path = xhttpObj.value("path").toString();
|
||||
|
||||
{
|
||||
const QJsonObject hdrs = xhttpObj.value("headers").toObject();
|
||||
if (hdrs.contains(QLatin1String("Host")) || !hdrs.isEmpty())
|
||||
srv.xhttp.headersTemplate = QStringLiteral("HTTP");
|
||||
}
|
||||
|
||||
if (xhttpObj.contains(QLatin1String("uplinkHTTPMethod")))
|
||||
srv.xhttp.uplinkMethod = xhttpObj.value("uplinkHTTPMethod").toString();
|
||||
else
|
||||
@@ -209,9 +184,7 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c
|
||||
return QStringLiteral("Query");
|
||||
return core;
|
||||
};
|
||||
QString sess = xhttpObj.value("sessionIDPlacement").toString();
|
||||
if (sess.isEmpty())
|
||||
sess = xhttpObj.value("sessionPlacement").toString();
|
||||
QString sess = xhttpObj.value("sessionPlacement").toString();
|
||||
if (sess.isEmpty())
|
||||
sess = xhttpObj.value("scSessionPlacement").toString();
|
||||
srv.xhttp.sessionPlacement = sessionSeqUi(sess);
|
||||
@@ -237,17 +210,14 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c
|
||||
udata = xhttpObj.value("scUplinkDataPlacement").toString();
|
||||
srv.xhttp.uplinkDataPlacement = uplinkDataUi(udata);
|
||||
|
||||
srv.xhttp.sessionKey = xhttpObj.value("sessionIDKey").toString();
|
||||
if (srv.xhttp.sessionKey.isEmpty())
|
||||
srv.xhttp.sessionKey = xhttpObj.value("sessionKey").toString();
|
||||
srv.xhttp.sessionKey = xhttpObj.value("sessionKey").toString();
|
||||
srv.xhttp.seqKey = xhttpObj.value("seqKey").toString();
|
||||
srv.xhttp.uplinkDataKey = xhttpObj.value("uplinkDataKey").toString();
|
||||
|
||||
if (xhttpObj.contains(QLatin1String("uplinkChunkSize"))) {
|
||||
QString ucMin, ucMax;
|
||||
parseIntRange(xhttpObj.value("uplinkChunkSize"), ucMin, ucMax);
|
||||
if (!ucMin.isEmpty())
|
||||
srv.xhttp.uplinkChunkSize = ucMin;
|
||||
QJsonObject uc = xhttpObj.value("uplinkChunkSize").toObject();
|
||||
if (!uc.isEmpty())
|
||||
srv.xhttp.uplinkChunkSize = QString::number(uc.value("from").toInt());
|
||||
} else if (xhttpObj.contains(QLatin1String("xhttpUplinkChunkSize"))) {
|
||||
srv.xhttp.uplinkChunkSize = QString::number(xhttpObj.value("xhttpUplinkChunkSize").toInt());
|
||||
}
|
||||
@@ -256,7 +226,11 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c
|
||||
}
|
||||
|
||||
auto readRange = [&](const char *key, QString &minOut, QString &maxOut) {
|
||||
parseIntRange(xhttpObj.value(QLatin1String(key)), minOut, maxOut);
|
||||
QJsonObject r = xhttpObj.value(QLatin1String(key)).toObject();
|
||||
if (!r.isEmpty()) {
|
||||
minOut = QString::number(r.value("from").toInt());
|
||||
maxOut = QString::number(r.value("to").toInt());
|
||||
}
|
||||
};
|
||||
readRange("scMaxEachPostBytes", srv.xhttp.scMaxEachPostBytesMin, srv.xhttp.scMaxEachPostBytesMax);
|
||||
readRange("scMinPostsIntervalMs", srv.xhttp.scMinPostsIntervalMsMin, srv.xhttp.scMinPostsIntervalMsMax);
|
||||
@@ -269,11 +243,10 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c
|
||||
srv.xhttp.xPadding.header = pad.value("xPaddingHeader").toString();
|
||||
srv.xhttp.xPadding.placement = pad.value("xPaddingPlacement").toString();
|
||||
srv.xhttp.xPadding.method = pad.value("xPaddingMethod").toString();
|
||||
QString bytesMin, bytesMax;
|
||||
parseIntRange(pad.value("xPaddingBytes"), bytesMin, bytesMax);
|
||||
if (!bytesMin.isEmpty()) {
|
||||
srv.xhttp.xPadding.bytesMin = bytesMin;
|
||||
srv.xhttp.xPadding.bytesMax = bytesMax;
|
||||
QJsonObject bytesRange = pad.value("xPaddingBytes").toObject();
|
||||
if (!bytesRange.isEmpty()) {
|
||||
srv.xhttp.xPadding.bytesMin = QString::number(bytesRange.value("from").toInt());
|
||||
srv.xhttp.xPadding.bytesMax = QString::number(bytesRange.value("to").toInt());
|
||||
}
|
||||
QString pl = srv.xhttp.xPadding.placement.toLower();
|
||||
if (pl == QLatin1String("cookie"))
|
||||
@@ -291,7 +264,7 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c
|
||||
srv.xhttp.xPadding.method = QStringLiteral("Tokenish");
|
||||
};
|
||||
if (xhttpObj.contains(QLatin1String("xPaddingObfsMode")) || xhttpObj.contains(QLatin1String("xPaddingKey"))
|
||||
|| xhttpObj.contains(QLatin1String("xPaddingBytes"))) {
|
||||
|| !xhttpObj.value("xPaddingBytes").toObject().isEmpty()) {
|
||||
loadPaddingFromObject(xhttpObj);
|
||||
} else if (xhttpObj.contains(QLatin1String("xPadding")) && xhttpObj.value("xPadding").isObject()) {
|
||||
const QJsonObject nested = xhttpObj.value("xPadding").toObject();
|
||||
@@ -307,7 +280,11 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c
|
||||
srv.xhttp.xmux.enabled = true;
|
||||
|
||||
auto readMuxRange = [&](const char *key, QString &minOut, QString &maxOut) {
|
||||
parseIntRange(mux.value(QLatin1String(key)), minOut, maxOut);
|
||||
QJsonObject r = mux.value(QLatin1String(key)).toObject();
|
||||
if (!r.isEmpty()) {
|
||||
minOut = QString::number(r.value("from").toInt());
|
||||
maxOut = QString::number(r.value("to").toInt());
|
||||
}
|
||||
};
|
||||
readMuxRange("maxConcurrency", srv.xhttp.xmux.maxConcurrencyMin, srv.xhttp.xmux.maxConcurrencyMax);
|
||||
readMuxRange("maxConnections", srv.xhttp.xmux.maxConnectionsMin, srv.xhttp.xmux.maxConnectionsMax);
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonArray>
|
||||
#include <QObject>
|
||||
#include <QSet>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "../../../core/utils/protocolEnum.h"
|
||||
#include "../../../core/protocols/protocolUtils.h"
|
||||
@@ -17,41 +13,6 @@ using namespace ProtocolUtils;
|
||||
namespace amnezia
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
template <typename T>
|
||||
QString awgVersionOf(const T &config)
|
||||
{
|
||||
auto hasValue = [](const QString &value) { return !value.trimmed().isEmpty(); };
|
||||
|
||||
const QStringList awg3Params = { config.headerProtectionKey, config.contentPaddingAddition,
|
||||
config.rekeyAfterTime, config.rekeyTimeout,
|
||||
config.rejectAfterTime, config.keepaliveTimeout,
|
||||
config.maxHandshakeAttempts };
|
||||
if (std::any_of(awg3Params.begin(), awg3Params.end(), hasValue)) {
|
||||
return protocols::awg::awgV3;
|
||||
}
|
||||
|
||||
const QStringList junkSizes = { config.cookieReplyPacketJunkSize, config.transportPacketJunkSize };
|
||||
const QStringList magicHeaders = { config.initPacketMagicHeader, config.responsePacketMagicHeader,
|
||||
config.underloadPacketMagicHeader, config.transportPacketMagicHeader };
|
||||
bool hasJunkSizes = std::any_of(junkSizes.begin(), junkSizes.end(), hasValue);
|
||||
bool hasHeaderRanges = std::any_of(magicHeaders.begin(), magicHeaders.end(),
|
||||
[](const QString &header) { return header.contains('-'); });
|
||||
if (hasJunkSizes || hasHeaderRanges) {
|
||||
return protocols::awg::awgV2;
|
||||
}
|
||||
|
||||
const QStringList specialJunk = { config.specialJunk1, config.specialJunk2, config.specialJunk3,
|
||||
config.specialJunk4, config.specialJunk5 };
|
||||
if (std::any_of(specialJunk.begin(), specialJunk.end(), hasValue)) {
|
||||
return protocols::awg::awgV1_5;
|
||||
}
|
||||
|
||||
return QString();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
QJsonObject AwgServerConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
@@ -134,7 +95,7 @@ QJsonObject AwgServerConfig::toJson() const
|
||||
if (!maxHandshakeAttempts.isEmpty()) {
|
||||
obj[configKey::maxHandshakeAttempts] = maxHandshakeAttempts;
|
||||
}
|
||||
|
||||
|
||||
if (isThirdPartyConfig) {
|
||||
obj[configKey::isThirdPartyConfig] = isThirdPartyConfig;
|
||||
}
|
||||
@@ -178,7 +139,7 @@ AwgServerConfig AwgServerConfig::fromJson(const QJsonObject& json)
|
||||
config.rejectAfterTime = json.value(configKey::rejectAfterTime).toString();
|
||||
config.keepaliveTimeout = json.value(configKey::keepaliveTimeout).toString();
|
||||
config.maxHandshakeAttempts = json.value(configKey::maxHandshakeAttempts).toString();
|
||||
|
||||
|
||||
config.isThirdPartyConfig = json.value(configKey::isThirdPartyConfig).toBool(false);
|
||||
|
||||
return config;
|
||||
@@ -302,7 +263,11 @@ QJsonObject AwgClientConfig::toJson() const
|
||||
if (!maxHandshakeAttempts.isEmpty()) {
|
||||
obj[configKey::maxHandshakeAttempts] = maxHandshakeAttempts;
|
||||
}
|
||||
|
||||
|
||||
if (isObfuscationEnabled) {
|
||||
obj[configKey::isObfuscationEnabled] = isObfuscationEnabled;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
@@ -353,18 +318,21 @@ AwgClientConfig AwgClientConfig::fromJson(const QJsonObject& json)
|
||||
config.rejectAfterTime = json.value(configKey::rejectAfterTime).toString();
|
||||
config.keepaliveTimeout = json.value(configKey::keepaliveTimeout).toString();
|
||||
config.maxHandshakeAttempts = json.value(configKey::maxHandshakeAttempts).toString();
|
||||
|
||||
config.isObfuscationEnabled = json.value(configKey::isObfuscationEnabled).toBool(false);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
QJsonObject AwgProtocolConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj = serverConfig.toJson();
|
||||
|
||||
|
||||
if (clientConfig.has_value()) {
|
||||
QJsonObject clientJson = clientConfig->toJson();
|
||||
obj[configKey::lastConfig] = QString::fromUtf8(QJsonDocument(clientJson).toJson(QJsonDocument::Compact));
|
||||
}
|
||||
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
@@ -385,24 +353,6 @@ AwgProtocolConfig AwgProtocolConfig::fromJson(const QJsonObject& json)
|
||||
return config;
|
||||
}
|
||||
|
||||
QString AwgProtocolConfig::serverProtocolVersion() const
|
||||
{
|
||||
return awgVersionOf(serverConfig);
|
||||
}
|
||||
|
||||
QString AwgProtocolConfig::clientProtocolVersion() const
|
||||
{
|
||||
return clientConfig.has_value() ? awgVersionOf(clientConfig.value()) : QString();
|
||||
}
|
||||
|
||||
QString AwgProtocolConfig::protocolVersionString(const QString &version)
|
||||
{
|
||||
if (version == protocols::awg::awgV3) return QObject::tr(" (version 3)");
|
||||
if (version == protocols::awg::awgV2) return QObject::tr(" (version 2)");
|
||||
if (version == protocols::awg::awgV1_5) return QObject::tr(" (version 1.5)");
|
||||
return "";
|
||||
}
|
||||
|
||||
bool AwgProtocolConfig::hasClientConfig() const
|
||||
{
|
||||
return clientConfig.has_value();
|
||||
@@ -431,30 +381,24 @@ bool AwgServerConfig::hasEqualServerSettings(const AwgServerConfig& other) const
|
||||
specialJunk1 != other.specialJunk1 || specialJunk2 != other.specialJunk2 ||
|
||||
specialJunk3 != other.specialJunk3 || specialJunk4 != other.specialJunk4 ||
|
||||
specialJunk5 != other.specialJunk5 ||
|
||||
cookieReplyPacketJunkSize != other.cookieReplyPacketJunkSize ||
|
||||
transportPacketJunkSize != other.transportPacketJunkSize ||
|
||||
headerProtectionKey != other.headerProtectionKey ||
|
||||
contentPaddingAddition != other.contentPaddingAddition ||
|
||||
rekeyAfterTime != other.rekeyAfterTime || rekeyTimeout != other.rekeyTimeout ||
|
||||
rejectAfterTime != other.rejectAfterTime || keepaliveTimeout != other.keepaliveTimeout ||
|
||||
maxHandshakeAttempts != other.maxHandshakeAttempts) {
|
||||
headerProtectionKey != other.headerProtectionKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isV2 = protocolVersion == protocols::awg::awgV2;
|
||||
if (isV2) {
|
||||
if (cookieReplyPacketJunkSize != other.cookieReplyPacketJunkSize ||
|
||||
transportPacketJunkSize != other.transportPacketJunkSize) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AwgProtocolConfig::isHeadersEqual(const QString &h1, const QString &h2, const QString &h3, const QString &h4)
|
||||
{
|
||||
QSet<QString> uniqueHeaders;
|
||||
int filledHeaders = 0;
|
||||
for (const QString &header : { h1, h2, h3, h4 }) {
|
||||
if (!header.trimmed().isEmpty()) {
|
||||
++filledHeaders;
|
||||
uniqueHeaders.insert(header);
|
||||
}
|
||||
}
|
||||
return uniqueHeaders.size() != filledHeaders;
|
||||
return (h1 == h2) || (h1 == h3) || (h1 == h4) || (h2 == h3) || (h2 == h4) || (h3 == h4);
|
||||
}
|
||||
|
||||
bool AwgProtocolConfig::isPacketSizeEqual(int s1, int s2, int s3, int s4)
|
||||
|
||||
@@ -90,6 +90,8 @@ struct AwgClientConfig {
|
||||
QString rejectAfterTime;
|
||||
QString keepaliveTimeout;
|
||||
QString maxHandshakeAttempts;
|
||||
bool isObfuscationEnabled = false;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static AwgClientConfig fromJson(const QJsonObject& json);
|
||||
};
|
||||
@@ -101,10 +103,6 @@ struct AwgProtocolConfig {
|
||||
QJsonObject toJson() const;
|
||||
static AwgProtocolConfig fromJson(const QJsonObject& json);
|
||||
|
||||
QString serverProtocolVersion() const;
|
||||
QString clientProtocolVersion() const;
|
||||
static QString protocolVersionString(const QString &version);
|
||||
|
||||
bool hasClientConfig() const;
|
||||
void setClientConfig(const AwgClientConfig& config);
|
||||
void clearClientConfig();
|
||||
|
||||
@@ -99,6 +99,9 @@ bool TelemtProtocolConfig::equalsDockerDeploymentSettings(const TelemtProtocolCo
|
||||
const auto normTransport = [](const QString &t) {
|
||||
return t.isEmpty() ? QString(protocols::telemt::transportModeStandard) : t;
|
||||
};
|
||||
const auto normWorkersMode = [](const QString &m) {
|
||||
return m.isEmpty() ? QString(protocols::telemt::workersModeAuto) : m;
|
||||
};
|
||||
|
||||
if (normPort(port) != normPort(other.port)) {
|
||||
return false;
|
||||
@@ -130,9 +133,18 @@ bool TelemtProtocolConfig::equalsDockerDeploymentSettings(const TelemtProtocolCo
|
||||
if (userName != other.userName) {
|
||||
return false;
|
||||
}
|
||||
if (normWorkersMode(workersMode) != normWorkersMode(other.workersMode)) {
|
||||
return false;
|
||||
}
|
||||
if (workers != other.workers) {
|
||||
return false;
|
||||
}
|
||||
if (natEnabled != other.natEnabled) {
|
||||
return false;
|
||||
}
|
||||
if (natInternalIp != other.natInternalIp) {
|
||||
return false;
|
||||
}
|
||||
if (natExternalIp != other.natExternalIp) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -105,17 +105,11 @@ QJsonObject WireGuardClientConfig::toJson() const
|
||||
if (!mtu.isEmpty()) {
|
||||
obj[configKey::mtu] = mtu;
|
||||
}
|
||||
|
||||
for (auto it = awgParams.constBegin(); it != awgParams.constEnd(); ++it) {
|
||||
if (!it.value().isEmpty()) {
|
||||
obj[it.key()] = it.value();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (isObfuscationEnabled) {
|
||||
obj[configKey::isObfuscationEnabled] = isObfuscationEnabled;
|
||||
}
|
||||
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
@@ -139,16 +133,9 @@ WireGuardClientConfig WireGuardClientConfig::fromJson(const QJsonObject& json)
|
||||
}
|
||||
config.persistentKeepAlive = json.value(configKey::persistentKeepAlive).toString();
|
||||
config.mtu = json.value(configKey::mtu).toString();
|
||||
|
||||
for (const QString &key : configKey::awgProtocolKeys()) {
|
||||
const QString value = json.value(key).toString();
|
||||
if (!value.isEmpty()) {
|
||||
config.awgParams.insert(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
config.isObfuscationEnabled = json.value(configKey::isObfuscationEnabled).toBool(false);
|
||||
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
#define WIREGUARDPROTOCOLCONFIG_H
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <optional>
|
||||
@@ -37,10 +36,8 @@ struct WireGuardClientConfig {
|
||||
QStringList allowedIps;
|
||||
QString persistentKeepAlive;
|
||||
QString mtu;
|
||||
|
||||
QMap<QString, QString> awgParams;
|
||||
bool isObfuscationEnabled = false;
|
||||
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static WireGuardClientConfig fromJson(const QJsonObject& json);
|
||||
};
|
||||
|
||||
@@ -81,6 +81,7 @@ QJsonObject XrayXhttpConfig::toJson() const
|
||||
if (!mode.isEmpty()) obj[configKey::xhttpMode] = mode;
|
||||
if (!host.isEmpty()) obj[configKey::xhttpHost] = host;
|
||||
if (!path.isEmpty()) obj[configKey::xhttpPath] = path;
|
||||
if (!headersTemplate.isEmpty()) obj[configKey::xhttpHeadersTemplate] = headersTemplate;
|
||||
if (!uplinkMethod.isEmpty()) obj[configKey::xhttpUplinkMethod] = uplinkMethod;
|
||||
obj[configKey::xhttpDisableGrpc] = disableGrpc;
|
||||
obj[configKey::xhttpDisableSse] = disableSse;
|
||||
@@ -115,6 +116,7 @@ namespace
|
||||
c.mode = QString();
|
||||
c.host = QString();
|
||||
c.path = QString();
|
||||
c.headersTemplate = QString();
|
||||
c.uplinkMethod = QString();
|
||||
c.disableGrpc = false;
|
||||
c.disableSse = false;
|
||||
@@ -153,6 +155,9 @@ XrayXhttpConfig XrayXhttpConfig::fromJson(const QJsonObject &json)
|
||||
if (json.contains(configKey::xhttpPath)) {
|
||||
c.path = json.value(configKey::xhttpPath).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpHeadersTemplate)) {
|
||||
c.headersTemplate = json.value(configKey::xhttpHeadersTemplate).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpUplinkMethod)) {
|
||||
c.uplinkMethod = json.value(configKey::xhttpUplinkMethod).toString();
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ struct XrayXhttpConfig {
|
||||
QString mode = protocols::xray::defaultXhttpMode; // Auto|Packet-up|Stream-up|Stream-one
|
||||
QString host = protocols::xray::defaultXhttpHost;
|
||||
QString path;
|
||||
QString headersTemplate = protocols::xray::defaultXhttpHeadersTemplate; // HTTP|None
|
||||
QString uplinkMethod = protocols::xray::defaultXhttpUplinkMethod; // POST|PUT|PATCH
|
||||
bool disableGrpc = true;
|
||||
bool disableSse = true;
|
||||
|
||||
@@ -64,11 +64,7 @@ QString getProtocolName(DockerContainer defaultContainer, const QMap<DockerConta
|
||||
const auto it = containers.constFind(defaultContainer);
|
||||
if (it != containers.cend()) {
|
||||
if (const AwgProtocolConfig *awg = it->getAwgProtocolConfig()) {
|
||||
QString version = awg->clientProtocolVersion();
|
||||
if (version.isEmpty()) {
|
||||
version = awg->serverProtocolVersion();
|
||||
}
|
||||
protocolVersion = AwgProtocolConfig::protocolVersionString(version);
|
||||
protocolVersion = ProtocolUtils::getProtocolVersionString(awg->toJson());
|
||||
if (defaultContainer == DockerContainer::Awg && !awg->serverConfig.isThirdPartyConfig) {
|
||||
containerName = QStringLiteral("AmneziaWG Legacy");
|
||||
}
|
||||
|
||||
@@ -222,18 +222,6 @@ ErrorCode OpenVpnProtocol::start()
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
// In "all except sites" mode the config uses redirect-gateway !ipv4, so OpenVPN
|
||||
// never reports net_route_v4_best_gw and m_routeGateway would stay empty
|
||||
const QString winGateway = NetworkUtilities::getGatewayAndIface().first;
|
||||
if (!winGateway.isEmpty()) {
|
||||
m_routeGateway = winGateway;
|
||||
qDebug() << "Set VPN route gateway" << m_routeGateway;
|
||||
} else {
|
||||
qWarning() << "Unable to detect physical default gateway";
|
||||
}
|
||||
#endif
|
||||
|
||||
uint mgmtPort = selectMgmtPort();
|
||||
qDebug() << "OpenVpnProtocol::start mgmt port selected:" << mgmtPort;
|
||||
|
||||
|
||||
@@ -209,3 +209,16 @@ QString ProtocolUtils::key_proto_config_path(Proto p)
|
||||
return protoToString(p) + "_config_path";
|
||||
}
|
||||
|
||||
QString ProtocolUtils::getProtocolVersion(const QJsonObject &protocolConfig)
|
||||
{
|
||||
return protocolConfig.value(configKey::protocolVersion).toString();
|
||||
}
|
||||
|
||||
QString ProtocolUtils::getProtocolVersionString(const QJsonObject &protocolConfig)
|
||||
{
|
||||
auto version = getProtocolVersion(protocolConfig);
|
||||
|
||||
if (version == protocols::awg::awgV2) return QObject::tr(" (version 2)");
|
||||
if (version == protocols::awg::awgV1_5) return QObject::tr(" (version 1.5)");
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -39,6 +39,8 @@ namespace amnezia
|
||||
QString key_proto_config_data(Proto p);
|
||||
QString key_proto_config_path(Proto p);
|
||||
|
||||
QString getProtocolVersion(const QJsonObject &protocolConfig);
|
||||
QString getProtocolVersionString(const QJsonObject &protocolConfig);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -120,60 +120,34 @@ QVariantMap SecureAppSettingsRepository::vpnSites(RouteMode mode) const
|
||||
return value("Conf/" + routeModeString(mode)).toMap();
|
||||
}
|
||||
|
||||
QStringList SecureAppSettingsRepository::siteIpList(const QVariant &value)
|
||||
{
|
||||
// QVariant::toStringList() handles both a QStringList/QVariantList and a single QString
|
||||
// (a single string is returned as a one-element list), which covers the legacy format.
|
||||
QStringList result = value.toStringList();
|
||||
result.removeAll(QString());
|
||||
result.removeDuplicates();
|
||||
return result;
|
||||
}
|
||||
|
||||
void SecureAppSettingsRepository::setVpnSites(RouteMode mode, const QVariantMap &sites)
|
||||
{
|
||||
setValue("Conf/" + routeModeString(mode), sites);
|
||||
}
|
||||
|
||||
bool SecureAppSettingsRepository::addVpnSite(RouteMode mode, const QString &site, const QStringList &ips)
|
||||
bool SecureAppSettingsRepository::addVpnSite(RouteMode mode, const QString &site, const QString &ip)
|
||||
{
|
||||
QVariantMap sites = vpnSites(mode);
|
||||
const bool siteExisted = sites.contains(site);
|
||||
|
||||
if (siteExisted && ips.isEmpty())
|
||||
if (sites.contains(site) && ip.isEmpty())
|
||||
return false;
|
||||
|
||||
QStringList mergedIps = siteIpList(sites.value(site));
|
||||
bool changed = !siteExisted;
|
||||
for (const QString &ip : ips) {
|
||||
if (!ip.isEmpty() && !mergedIps.contains(ip)) {
|
||||
mergedIps.append(ip);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!changed)
|
||||
return false;
|
||||
|
||||
sites.insert(site, mergedIps);
|
||||
sites.insert(site, ip);
|
||||
setVpnSites(mode, sites);
|
||||
emit sitesChanged(mode);
|
||||
return true;
|
||||
}
|
||||
|
||||
void SecureAppSettingsRepository::addVpnSites(RouteMode mode, const QMap<QString, QStringList> &sites)
|
||||
void SecureAppSettingsRepository::addVpnSites(RouteMode mode, const QMap<QString, QString> &sites)
|
||||
{
|
||||
QVariantMap allSites = vpnSites(mode);
|
||||
for (auto i = sites.constBegin(); i != sites.constEnd(); ++i) {
|
||||
const QString &site = i.key();
|
||||
const QString &ip = i.value();
|
||||
|
||||
QStringList mergedIps = siteIpList(allSites.value(site));
|
||||
for (const QString &ip : i.value()) {
|
||||
if (!ip.isEmpty() && !mergedIps.contains(ip))
|
||||
mergedIps.append(ip);
|
||||
}
|
||||
if (allSites.contains(site) && allSites.value(site) == ip)
|
||||
continue;
|
||||
|
||||
allSites.insert(site, mergedIps);
|
||||
allSites.insert(site, ip);
|
||||
}
|
||||
|
||||
setVpnSites(mode, allSites);
|
||||
|
||||
@@ -38,15 +38,11 @@ public:
|
||||
|
||||
RouteMode routeMode() const;
|
||||
void setRouteMode(RouteMode mode);
|
||||
bool addVpnSite(RouteMode mode, const QString &site, const QStringList &ips = {});
|
||||
void addVpnSites(RouteMode mode, const QMap<QString, QStringList> &sites);
|
||||
bool addVpnSite(RouteMode mode, const QString &site, const QString &ip = "");
|
||||
void addVpnSites(RouteMode mode, const QMap<QString, QString> &sites);
|
||||
void removeVpnSite(RouteMode mode, const QString &site);
|
||||
void removeAllVpnSites(RouteMode mode);
|
||||
QVariantMap vpnSites(RouteMode mode) const;
|
||||
|
||||
// Normalizes a stored vpn site value into a list of IPs.
|
||||
// Supports both the legacy format (a single IP string) and the current one (a list of IPs).
|
||||
static QStringList siteIpList(const QVariant &value);
|
||||
bool isSitesSplitTunnelingEnabled() const;
|
||||
void setSitesSplitTunnelingEnabled(bool enabled);
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
#define CONFIGKEYS_H
|
||||
|
||||
#include <QLatin1String>
|
||||
#include <QStringList>
|
||||
|
||||
namespace amnezia
|
||||
{
|
||||
@@ -71,8 +70,6 @@ namespace amnezia
|
||||
|
||||
constexpr QLatin1String lastConfig("last_config");
|
||||
|
||||
constexpr QLatin1String protocolVersion("protocol_version");
|
||||
|
||||
constexpr QLatin1String isThirdPartyConfig("isThirdPartyConfig");
|
||||
constexpr QLatin1String isObfuscationEnabled("isObfuscationEnabled");
|
||||
|
||||
@@ -101,32 +98,7 @@ namespace amnezia
|
||||
constexpr QLatin1String keepaliveTimeout("KeepaliveTimeout");
|
||||
constexpr QLatin1String maxHandshakeAttempts("MaxHandshakeAttempts");
|
||||
|
||||
inline QStringList awgProtocolKeys()
|
||||
{
|
||||
return { junkPacketCount,
|
||||
junkPacketMinSize,
|
||||
junkPacketMaxSize,
|
||||
initPacketJunkSize,
|
||||
responsePacketJunkSize,
|
||||
cookieReplyPacketJunkSize,
|
||||
transportPacketJunkSize,
|
||||
initPacketMagicHeader,
|
||||
responsePacketMagicHeader,
|
||||
underloadPacketMagicHeader,
|
||||
transportPacketMagicHeader,
|
||||
specialJunk1,
|
||||
specialJunk2,
|
||||
specialJunk3,
|
||||
specialJunk4,
|
||||
specialJunk5,
|
||||
headerProtectionKey,
|
||||
contentPaddingAddition,
|
||||
rekeyAfterTime,
|
||||
rekeyTimeout,
|
||||
rejectAfterTime,
|
||||
keepaliveTimeout,
|
||||
maxHandshakeAttempts };
|
||||
}
|
||||
constexpr QLatin1String protocolVersion("protocol_version");
|
||||
|
||||
constexpr QLatin1String openvpn("openvpn");
|
||||
constexpr QLatin1String wireguard("wireguard");
|
||||
@@ -186,6 +158,7 @@ namespace amnezia
|
||||
constexpr QLatin1String xhttpMode("xhttp_mode"); // Auto | Packet-up | Stream-up | Stream-one
|
||||
constexpr QLatin1String xhttpHost("xhttp_host");
|
||||
constexpr QLatin1String xhttpPath("xhttp_path");
|
||||
constexpr QLatin1String xhttpHeadersTemplate("xhttp_headers_template"); // HTTP | None
|
||||
constexpr QLatin1String xhttpUplinkMethod("xhttp_uplink_method"); // POST | PUT | PATCH
|
||||
constexpr QLatin1String xhttpDisableGrpc("xhttp_disable_grpc"); // bool
|
||||
constexpr QLatin1String xhttpDisableSse("xhttp_disable_sse"); // bool
|
||||
|
||||
@@ -65,12 +65,13 @@ namespace amnezia
|
||||
constexpr char defaultTransport[] = "raw";
|
||||
constexpr char defaultFingerprint[] = "chrome";
|
||||
constexpr char defaultSni[] = "www.googletagmanager.com";
|
||||
constexpr char defaultAlpn[] = "h2";
|
||||
constexpr char defaultAlpn[] = "HTTP/2";
|
||||
|
||||
constexpr char defaultXhttpMode[] = "Auto";
|
||||
constexpr char defaultXhttpHeadersTemplate[] = "HTTP";
|
||||
constexpr char defaultXhttpUplinkMethod[] = "POST";
|
||||
constexpr char defaultXhttpSessionPlacement[] = "Path";
|
||||
constexpr char defaultXhttpSessionKey[] = "";
|
||||
constexpr char defaultXhttpSessionKey[] = "Path";
|
||||
constexpr char defaultXhttpSeqPlacement[] = "Path";
|
||||
constexpr char defaultXhttpUplinkDataPlacement[] = "Body";
|
||||
|
||||
@@ -85,10 +86,6 @@ namespace amnezia
|
||||
|
||||
constexpr char defaultXPaddingPlacement[] = "Cookie";
|
||||
constexpr char defaultXPaddingMethod[] = "Repeat-x";
|
||||
constexpr char defaultXPaddingKey[] = "x_padding";
|
||||
constexpr char defaultXPaddingHeader[] = "X-Padding";
|
||||
constexpr char defaultXPaddingBytesMin[] = "1";
|
||||
constexpr char defaultXPaddingBytesMax[] = "256";
|
||||
|
||||
constexpr char defaultMkcpTti[] = "50";
|
||||
constexpr char defaultMkcpUplinkCapacity[] = "5";
|
||||
@@ -186,16 +183,15 @@ namespace amnezia
|
||||
constexpr char defaultJunkPacketCount[] = "3";
|
||||
constexpr char defaultJunkPacketMinSize[] = "10";
|
||||
constexpr char defaultJunkPacketMaxSize[] = "30";
|
||||
constexpr int junkPacketSizeMin = 12;
|
||||
constexpr int initPacketJunkSizeMax = 150;
|
||||
constexpr int responsePacketJunkSizeMax = 150;
|
||||
constexpr int cookieReplyPacketJunkSizeMax = 64;
|
||||
constexpr int defaultTransportPacketJunkSize = 12;
|
||||
constexpr char defaultInitPacketJunkSize[] = "15";
|
||||
constexpr char defaultResponsePacketJunkSize[] = "18";
|
||||
constexpr char defaultCookieReplyPacketJunkSize[] = "20";
|
||||
constexpr char defaultTransportPacketJunkSize[] = "23";
|
||||
|
||||
constexpr char defaultInitPacketMagicHeader[] = "1";
|
||||
constexpr char defaultResponsePacketMagicHeader[] = "2";
|
||||
constexpr char defaultUnderloadPacketMagicHeader[] = "3";
|
||||
constexpr char defaultTransportPacketMagicHeader[] = "4";
|
||||
constexpr char defaultInitPacketMagicHeader[] = "1020325451";
|
||||
constexpr char defaultResponsePacketMagicHeader[] = "3288052141";
|
||||
constexpr char defaultTransportPacketMagicHeader[] = "2528465083";
|
||||
constexpr char defaultUnderloadPacketMagicHeader[] = "1766607858";
|
||||
constexpr char defaultSpecialJunk1[] = "<r 2><b 0x858000010001000000000669636c6f756403636f6d0000010001c00c000100010000105a00044d583737>";
|
||||
constexpr char defaultSpecialJunk2[] = "";
|
||||
constexpr char defaultSpecialJunk3[] = "";
|
||||
@@ -204,17 +200,6 @@ namespace amnezia
|
||||
|
||||
constexpr char awgV1_5[] = "1.5";
|
||||
constexpr char awgV2[] = "2";
|
||||
constexpr char awgV3[] = "3";
|
||||
|
||||
constexpr char defaultContentPaddingAddition[] = "10-100";
|
||||
constexpr char defaultRekeyAfterTime[] = "100-120";
|
||||
constexpr char defaultRekeyTimeout[] = "3-7";
|
||||
constexpr char defaultRejectAfterTime[] = "150-180";
|
||||
constexpr char defaultKeepaliveTimeout[] = "5-15";
|
||||
constexpr char defaultMaxHandshakeAttempts[] = "15-20";
|
||||
constexpr char defaultPersistentKeepAlive[] = "25-35";
|
||||
|
||||
|
||||
}
|
||||
|
||||
namespace socks5Proxy
|
||||
@@ -250,8 +235,7 @@ namespace amnezia
|
||||
|
||||
constexpr char defaultPort[] = "443";
|
||||
constexpr char defaultWorkers[] = "2";
|
||||
// mtproto-proxy loses connectivity with -M >= 20; keep the cap at the highest known-good value.
|
||||
constexpr int maxWorkers = 19;
|
||||
constexpr int maxWorkers = 32;
|
||||
constexpr int botTagHexLength = 32;
|
||||
constexpr char defaultTlsDomain[] = "googletagmanager.com";
|
||||
}
|
||||
@@ -270,6 +254,7 @@ namespace amnezia
|
||||
constexpr char tlsEmulationKey[] = "telemt_tls_emulation";
|
||||
constexpr char useMiddleProxyKey[] = "telemt_use_middle_proxy";
|
||||
constexpr char userNameKey[] = "telemt_user_name";
|
||||
// Stored for UI only (Telemt server ignores these; same controls as MTProxy page)
|
||||
constexpr char additionalSecretsKey[] = "telemt_additional_secrets";
|
||||
constexpr char workersKey[] = "telemt_workers";
|
||||
constexpr char workersModeKey[] = "telemt_workers_mode";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -24,13 +24,6 @@ QList<QString> qrCodeUtils::generateQrCodeImageSeries(const QByteArray &data)
|
||||
return chunks;
|
||||
}
|
||||
|
||||
QString qrCodeUtils::generatePlainQrCodeImage(const QByteArray &data)
|
||||
{
|
||||
qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(data, qrcodegen::QrCode::Ecc::LOW);
|
||||
QString svg = QString::fromStdString(toSvgString(qr, 1));
|
||||
return svgToBase64(svg);
|
||||
}
|
||||
|
||||
QString qrCodeUtils::svgToBase64(const QString &image)
|
||||
{
|
||||
return "data:image/svg;base64," + QString::fromLatin1(image.toUtf8().toBase64().data());
|
||||
|
||||
@@ -10,7 +10,6 @@ namespace qrCodeUtils
|
||||
constexpr const qint16 qrMagicCode = 1984;
|
||||
|
||||
QList<QString> generateQrCodeImageSeries(const QByteArray &data);
|
||||
QString generatePlainQrCodeImage(const QByteArray &data);
|
||||
qrcodegen::QrCode generateQrCode(const QByteArray &data);
|
||||
QString svgToBase64(const QString &image);
|
||||
};
|
||||
|
||||
@@ -254,20 +254,8 @@ amnezia::ScriptVars amnezia::genAwgVars(const ContainerConfig &containerConfig)
|
||||
vars.append({ { "$SPECIAL_JUNK_3", config.specialJunk3 } });
|
||||
vars.append({ { "$SPECIAL_JUNK_4", config.specialJunk4 } });
|
||||
vars.append({ { "$SPECIAL_JUNK_5", config.specialJunk5 } });
|
||||
|
||||
const bool isAwg3 = config.protocolVersion == protocols::awg::awgV3;
|
||||
vars.append({ { "$PERSISTENT_KEEPALIVE", isAwg3 ? QString(protocols::awg::defaultPersistentKeepAlive)
|
||||
: QString(protocols::wireguard::defaultPersistentKeepAlive) } });
|
||||
|
||||
vars.append({ { "$HEADER_PROTECTION_KEY", config.headerProtectionKey } });
|
||||
vars.append({ { "$CONTENT_PADDING_ADDITION", config.contentPaddingAddition } });
|
||||
vars.append({ { "$REKEY_AFTER_TIME", config.rekeyAfterTime } });
|
||||
vars.append({ { "$REKEY_TIMEOUT", config.rekeyTimeout } });
|
||||
vars.append({ { "$REJECT_AFTER_TIME", config.rejectAfterTime } });
|
||||
vars.append({ { "$KEEPALIVE_TIMEOUT", config.keepaliveTimeout } });
|
||||
vars.append({ { "$MAX_HANDSHAKE_ATTEMPTS", config.maxHandshakeAttempts } });
|
||||
}
|
||||
|
||||
|
||||
return vars;
|
||||
}
|
||||
|
||||
@@ -341,7 +329,6 @@ amnezia::ScriptVars amnezia::genMtProxyVars(const ContainerConfig &containerConf
|
||||
workers = (transportMode == QLatin1String(protocols::mtProxy::transportModeFakeTLS)) ? QStringLiteral("0")
|
||||
: QStringLiteral("2");
|
||||
}
|
||||
vars.append({{"$MTPROXY_WORKERS_MODE", workersMode}});
|
||||
vars.append({{"$MTPROXY_WORKERS", workers}});
|
||||
|
||||
vars.append({{"$MTPROXY_NAT_ENABLED", c.natEnabled ? QStringLiteral("1") : QStringLiteral("0")}});
|
||||
@@ -388,12 +375,6 @@ amnezia::ScriptVars amnezia::genTelemtVars(const ContainerConfig &containerConfi
|
||||
}
|
||||
}
|
||||
vars.append({ { "$TELEMT_ADDITIONAL_SECRETS", additionalList.join(QLatin1Char(',')) } });
|
||||
|
||||
QString middleProxyNatIp;
|
||||
if (c.natEnabled && !c.natExternalIp.isEmpty()) {
|
||||
middleProxyNatIp = c.natExternalIp;
|
||||
}
|
||||
vars.append({ { "$TELEMT_MIDDLE_PROXY_NAT_IP", middleProxyNatIp } });
|
||||
}
|
||||
|
||||
return vars;
|
||||
|
||||
@@ -176,8 +176,7 @@ QByteArray SshSession::getTextFileFromContainer(DockerContainer container, const
|
||||
|
||||
errorCode = ErrorCode::NoError;
|
||||
|
||||
QString script = QStringLiteral("sudo docker exec -i %1 sh -c \"xxd -p '%2' 2>/dev/null || od -An -v -tx1 '%2'\"")
|
||||
.arg(ContainerUtils::containerToString(container), path);
|
||||
QString script = QStringLiteral("sudo docker exec -i %1 sh -c \"xxd -p '%2'\"").arg(ContainerUtils::containerToString(container), path);
|
||||
|
||||
QString stdOut;
|
||||
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
|
||||
|
||||
@@ -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) \
|
||||
{ \
|
||||
|
||||
@@ -118,44 +118,42 @@ QString InterfaceConfig::toWgConf(const QMap<QString, QString>& extra) const {
|
||||
out << "DNS = " << dnsServers.join(", ") << "\n";
|
||||
}
|
||||
|
||||
if (!m_junkPacketCount.isEmpty()) {
|
||||
if (!m_junkPacketCount.isNull()) {
|
||||
out << "Jc = " << m_junkPacketCount << "\n";
|
||||
}
|
||||
if (!m_junkPacketMinSize.isEmpty()) {
|
||||
if (!m_junkPacketMinSize.isNull()) {
|
||||
out << "JMin = " << m_junkPacketMinSize << "\n";
|
||||
}
|
||||
if (!m_junkPacketMaxSize.isEmpty()) {
|
||||
if (!m_junkPacketMaxSize.isNull()) {
|
||||
out << "JMax = " << m_junkPacketMaxSize << "\n";
|
||||
}
|
||||
if (!m_initPacketJunkSize.isEmpty()) {
|
||||
if (!m_initPacketJunkSize.isNull()) {
|
||||
out << "S1 = " << m_initPacketJunkSize << "\n";
|
||||
}
|
||||
if (!m_responsePacketJunkSize.isEmpty()) {
|
||||
if (!m_responsePacketJunkSize.isNull()) {
|
||||
out << "S2 = " << m_responsePacketJunkSize << "\n";
|
||||
}
|
||||
if (!m_cookieReplyPacketJunkSize.isEmpty()) {
|
||||
if (!m_cookieReplyPacketJunkSize.isNull()) {
|
||||
out << "S3 = " << m_cookieReplyPacketJunkSize << "\n";
|
||||
}
|
||||
if (!m_transportPacketJunkSize.isEmpty()) {
|
||||
if (!m_transportPacketJunkSize.isNull()) {
|
||||
out << "S4 = " << m_transportPacketJunkSize << "\n";
|
||||
}
|
||||
if (!m_initPacketMagicHeader.isEmpty()) {
|
||||
if (!m_initPacketMagicHeader.isNull()) {
|
||||
out << "H1 = " << m_initPacketMagicHeader << "\n";
|
||||
}
|
||||
if (!m_responsePacketMagicHeader.isEmpty()) {
|
||||
if (!m_responsePacketMagicHeader.isNull()) {
|
||||
out << "H2 = " << m_responsePacketMagicHeader << "\n";
|
||||
}
|
||||
if (!m_underloadPacketMagicHeader.isEmpty()) {
|
||||
if (!m_underloadPacketMagicHeader.isNull()) {
|
||||
out << "H3 = " << m_underloadPacketMagicHeader << "\n";
|
||||
}
|
||||
if (!m_transportPacketMagicHeader.isEmpty()) {
|
||||
if (!m_transportPacketMagicHeader.isNull()) {
|
||||
out << "H4 = " << m_transportPacketMagicHeader << "\n";
|
||||
}
|
||||
|
||||
for (const QString& key : m_specialJunk.keys()) {
|
||||
if (!m_specialJunk[key].isEmpty()) {
|
||||
out << key << " = " << m_specialJunk[key] << "\n";
|
||||
}
|
||||
out << key << " = " << m_specialJunk[key] << "\n";
|
||||
}
|
||||
|
||||
if (!m_headerProtectionKey.isEmpty()) {
|
||||
|
||||
@@ -18,10 +18,11 @@ set_target_properties(networkextension PROPERTIES
|
||||
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "${BUILD_IOS_APP_IDENTIFIER}.network-extension"
|
||||
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS ${CMAKE_CURRENT_SOURCE_DIR}/AmneziaVPNNetworkExtension.entitlements
|
||||
XCODE_ATTRIBUTE_MARKETING_VERSION "${APP_MAJOR_VERSION}"
|
||||
XCODE_ATTRIBUTE_CURRENT_PROJECT_VERSION "${CMAKE_PROJECT_VERSION_TWEAK}"
|
||||
XCODE_ATTRIBUTE_CURRENT_PROJECT_VERSION "${BUILD_ID}"
|
||||
XCODE_ATTRIBUTE_PRODUCT_NAME "AmneziaVPNNetworkExtension"
|
||||
|
||||
XCODE_ATTRIBUTE_APPLICATION_EXTENSION_API_ONLY "YES"
|
||||
XCODE_ATTRIBUTE_ENABLE_BITCODE "NO"
|
||||
XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2"
|
||||
|
||||
XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/../../Frameworks"
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(DEVELOPMENT_TEAM).*</string>
|
||||
|
||||
@@ -7,14 +7,21 @@ add_executable(AmneziaVPNNetworkExtension)
|
||||
message("executable_path is: @executable_path/../../Frameworks")
|
||||
set_target_properties(AmneziaVPNNetworkExtension PROPERTIES
|
||||
XCODE_PRODUCT_TYPE com.apple.product-type.app-extension
|
||||
# MACOSX_BUNDLE YES
|
||||
BUNDLE_EXTENSION appex
|
||||
MACOSX_BUNDLE_SHORT_VERSION_STRING "${APPLE_PROJECT_VERSION}"
|
||||
MACOSX_BUNDLE_INFO_STRING "AmneziaVPNNetworkExtension"
|
||||
MACOSX_BUNDLE_BUNDLE_NAME "AmneziaVPNNetworkExtension"
|
||||
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "${BUILD_IOS_APP_IDENTIFIER}.network-extension"
|
||||
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_NAME "${BUILD_IOS_APP_IDENTIFIER}.network-extension"
|
||||
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS ${CMAKE_CURRENT_SOURCE_DIR}/AmneziaVPNNetworkExtension.entitlements
|
||||
XCODE_ATTRIBUTE_MARKETING_VERSION "${APP_MAJOR_VERSION}"
|
||||
XCODE_ATTRIBUTE_CURRENT_PROJECT_VERSION "${CMAKE_PROJECT_VERSION_TWEAK}"
|
||||
XCODE_ATTRIBUTE_PRODUCT_NAME "AmneziaVPNNetworkExtension"
|
||||
|
||||
XCODE_ATTRIBUTE_APPLICATION_EXTENSION_API_ONLY "YES"
|
||||
XCODE_ATTRIBUTE_ENABLE_BITCODE "NO"
|
||||
XCODE_ATTRIBUTE_MACOSX_DEPLOYMENT_TARGET "11.0"
|
||||
|
||||
XCODE_ATTRIBUTE_INFOPLIST_FILE ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist.in
|
||||
XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/../../../../Frameworks @loader_path/../../../../Frameworks"
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<false/>
|
||||
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||
<string>${CMAKE_OSX_DEPLOYMENT_TARGET}</string>
|
||||
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>AmneziaVPNNetworkExtension</string>
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
#include <QLocalSocket>
|
||||
#include <QObject>
|
||||
#include <QStandardPaths>
|
||||
#include <QStringList>
|
||||
#include <QTimer>
|
||||
|
||||
#include "leakdetector.h"
|
||||
@@ -252,13 +251,64 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
|
||||
|
||||
json.insert(amnezia::configKey::killSwitchOption, rawConfig.value(amnezia::configKey::killSwitchOption));
|
||||
|
||||
const QStringList awgProtocolKeys = amnezia::configKey::awgProtocolKeys();
|
||||
|
||||
for (const QString &key : awgProtocolKeys) {
|
||||
const QJsonValue value = wgConfig.value(key);
|
||||
if (value.isString() && !value.toString().isEmpty()) {
|
||||
json.insert(key, value);
|
||||
}
|
||||
if (protocolName == amnezia::configKey::awg) {
|
||||
json.insert(amnezia::configKey::junkPacketCount, wgConfig.value(amnezia::configKey::junkPacketCount));
|
||||
json.insert(amnezia::configKey::junkPacketMinSize, wgConfig.value(amnezia::configKey::junkPacketMinSize));
|
||||
json.insert(amnezia::configKey::junkPacketMaxSize, wgConfig.value(amnezia::configKey::junkPacketMaxSize));
|
||||
json.insert(amnezia::configKey::initPacketJunkSize, wgConfig.value(amnezia::configKey::initPacketJunkSize));
|
||||
json.insert(amnezia::configKey::responsePacketJunkSize, wgConfig.value(amnezia::configKey::responsePacketJunkSize));
|
||||
json.insert(amnezia::configKey::cookieReplyPacketJunkSize, wgConfig.value(amnezia::configKey::cookieReplyPacketJunkSize));
|
||||
json.insert(amnezia::configKey::transportPacketJunkSize, wgConfig.value(amnezia::configKey::transportPacketJunkSize));
|
||||
json.insert(amnezia::configKey::initPacketMagicHeader, wgConfig.value(amnezia::configKey::initPacketMagicHeader));
|
||||
json.insert(amnezia::configKey::responsePacketMagicHeader, wgConfig.value(amnezia::configKey::responsePacketMagicHeader));
|
||||
json.insert(amnezia::configKey::underloadPacketMagicHeader, wgConfig.value(amnezia::configKey::underloadPacketMagicHeader));
|
||||
json.insert(amnezia::configKey::transportPacketMagicHeader, wgConfig.value(amnezia::configKey::transportPacketMagicHeader));
|
||||
json.insert(amnezia::configKey::specialJunk1, wgConfig.value(amnezia::configKey::specialJunk1));
|
||||
json.insert(amnezia::configKey::specialJunk2, wgConfig.value(amnezia::configKey::specialJunk2));
|
||||
json.insert(amnezia::configKey::specialJunk3, wgConfig.value(amnezia::configKey::specialJunk3));
|
||||
json.insert(amnezia::configKey::specialJunk4, wgConfig.value(amnezia::configKey::specialJunk4));
|
||||
json.insert(amnezia::configKey::specialJunk5, wgConfig.value(amnezia::configKey::specialJunk5));
|
||||
json.insert(amnezia::configKey::headerProtectionKey, wgConfig.value(amnezia::configKey::headerProtectionKey));
|
||||
json.insert(amnezia::configKey::contentPaddingAddition, wgConfig.value(amnezia::configKey::contentPaddingAddition));
|
||||
json.insert(amnezia::configKey::rekeyAfterTime, wgConfig.value(amnezia::configKey::rekeyAfterTime));
|
||||
json.insert(amnezia::configKey::rekeyTimeout, wgConfig.value(amnezia::configKey::rekeyTimeout));
|
||||
json.insert(amnezia::configKey::rejectAfterTime, wgConfig.value(amnezia::configKey::rejectAfterTime));
|
||||
json.insert(amnezia::configKey::keepaliveTimeout, wgConfig.value(amnezia::configKey::keepaliveTimeout));
|
||||
json.insert(amnezia::configKey::maxHandshakeAttempts, wgConfig.value(amnezia::configKey::maxHandshakeAttempts));
|
||||
} else if (!wgConfig.value(amnezia::configKey::junkPacketCount).isUndefined()
|
||||
&& !wgConfig.value(amnezia::configKey::junkPacketMinSize).isUndefined()
|
||||
&& !wgConfig.value(amnezia::configKey::junkPacketMaxSize).isUndefined()
|
||||
&& !wgConfig.value(amnezia::configKey::initPacketJunkSize).isUndefined()
|
||||
&& !wgConfig.value(amnezia::configKey::responsePacketJunkSize).isUndefined()
|
||||
&& !wgConfig.value(amnezia::configKey::cookieReplyPacketJunkSize).isUndefined()
|
||||
&& !wgConfig.value(amnezia::configKey::transportPacketJunkSize).isUndefined()
|
||||
&& !wgConfig.value(amnezia::configKey::initPacketMagicHeader).isUndefined()
|
||||
&& !wgConfig.value(amnezia::configKey::responsePacketMagicHeader).isUndefined()
|
||||
&& !wgConfig.value(amnezia::configKey::underloadPacketMagicHeader).isUndefined()
|
||||
&& !wgConfig.value(amnezia::configKey::transportPacketMagicHeader).isUndefined()) {
|
||||
json.insert(amnezia::configKey::junkPacketCount, wgConfig.value(amnezia::configKey::junkPacketCount));
|
||||
json.insert(amnezia::configKey::junkPacketMinSize, wgConfig.value(amnezia::configKey::junkPacketMinSize));
|
||||
json.insert(amnezia::configKey::junkPacketMaxSize, wgConfig.value(amnezia::configKey::junkPacketMaxSize));
|
||||
json.insert(amnezia::configKey::initPacketJunkSize, wgConfig.value(amnezia::configKey::initPacketJunkSize));
|
||||
json.insert(amnezia::configKey::responsePacketJunkSize, wgConfig.value(amnezia::configKey::responsePacketJunkSize));
|
||||
json.insert(amnezia::configKey::cookieReplyPacketJunkSize, wgConfig.value(amnezia::configKey::cookieReplyPacketJunkSize));
|
||||
json.insert(amnezia::configKey::transportPacketJunkSize, wgConfig.value(amnezia::configKey::transportPacketJunkSize));
|
||||
json.insert(amnezia::configKey::initPacketMagicHeader, wgConfig.value(amnezia::configKey::initPacketMagicHeader));
|
||||
json.insert(amnezia::configKey::responsePacketMagicHeader, wgConfig.value(amnezia::configKey::responsePacketMagicHeader));
|
||||
json.insert(amnezia::configKey::underloadPacketMagicHeader, wgConfig.value(amnezia::configKey::underloadPacketMagicHeader));
|
||||
json.insert(amnezia::configKey::transportPacketMagicHeader, wgConfig.value(amnezia::configKey::transportPacketMagicHeader));
|
||||
json.insert(amnezia::configKey::specialJunk1, wgConfig.value(amnezia::configKey::specialJunk1));
|
||||
json.insert(amnezia::configKey::specialJunk2, wgConfig.value(amnezia::configKey::specialJunk2));
|
||||
json.insert(amnezia::configKey::specialJunk3, wgConfig.value(amnezia::configKey::specialJunk3));
|
||||
json.insert(amnezia::configKey::specialJunk4, wgConfig.value(amnezia::configKey::specialJunk4));
|
||||
json.insert(amnezia::configKey::specialJunk5, wgConfig.value(amnezia::configKey::specialJunk5));
|
||||
json.insert(amnezia::configKey::headerProtectionKey, wgConfig.value(amnezia::configKey::headerProtectionKey));
|
||||
json.insert(amnezia::configKey::contentPaddingAddition, wgConfig.value(amnezia::configKey::contentPaddingAddition));
|
||||
json.insert(amnezia::configKey::rekeyAfterTime, wgConfig.value(amnezia::configKey::rekeyAfterTime));
|
||||
json.insert(amnezia::configKey::rekeyTimeout, wgConfig.value(amnezia::configKey::rekeyTimeout));
|
||||
json.insert(amnezia::configKey::rejectAfterTime, wgConfig.value(amnezia::configKey::rejectAfterTime));
|
||||
json.insert(amnezia::configKey::keepaliveTimeout, wgConfig.value(amnezia::configKey::keepaliveTimeout));
|
||||
json.insert(amnezia::configKey::maxHandshakeAttempts, wgConfig.value(amnezia::configKey::maxHandshakeAttempts));
|
||||
}
|
||||
|
||||
write(json);
|
||||
|
||||
@@ -350,6 +350,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();
|
||||
|
||||
void showUpdateCover();
|
||||
void hideUpdateCover();
|
||||
|
||||
@@ -7,6 +7,10 @@ struct OpenVPNConfig: Decodable {
|
||||
let config: String
|
||||
let splitTunnelType: Int
|
||||
let splitTunnelSites: [String]
|
||||
|
||||
var str: String {
|
||||
"splitTunnelType: \(splitTunnelType) splitTunnelSites: \(splitTunnelSites) config: \(config)"
|
||||
}
|
||||
}
|
||||
|
||||
extension PacketTunnelProvider {
|
||||
@@ -26,6 +30,11 @@ extension PacketTunnelProvider {
|
||||
|
||||
do {
|
||||
let openVPNConfig = try JSONDecoder().decode(OpenVPNConfig.self, from: openVPNConfigData)
|
||||
ovpnLog(.info, title: "config: ", message: openVPNConfig.str)
|
||||
let wrapperPreview = String(decoding: openVPNConfigData.prefix(512), as: UTF8.self)
|
||||
let ovpnPreview = String(openVPNConfig.config.prefix(512))
|
||||
ovpnLog(.info, title: "config wrapper", message: "bytes=\(openVPNConfigData.count) preview=\(wrapperPreview)")
|
||||
ovpnLog(.info, title: "config raw", message: "chars=\(openVPNConfig.config.count) preview=\(ovpnPreview)")
|
||||
let ovpnConfiguration = Data(openVPNConfig.config.utf8)
|
||||
splitTunnelType = openVPNConfig.splitTunnelType
|
||||
splitTunnelSites = openVPNConfig.splitTunnelSites
|
||||
@@ -96,6 +105,12 @@ extension PacketTunnelProvider {
|
||||
let hasTlsAuthClose = configString.contains("</tls-auth>")
|
||||
ovpnLog(.info, title: "ConfigFlags", message: "tls-auth open=\(hasTlsAuthOpen) close=\(hasTlsAuthClose)")
|
||||
|
||||
let lines = configString.split(separator: "\n")
|
||||
let head = lines.prefix(10).joined(separator: "\n")
|
||||
let tail = lines.suffix(10).joined(separator: "\n")
|
||||
ovpnLog(.debug, title: "ConfigHead", message: head)
|
||||
ovpnLog(.debug, title: "ConfigTail", message: tail)
|
||||
|
||||
if hasTlsAuthOpen && hasTlsAuthClose {
|
||||
ovpnLog(.info, title: "TLSAuthSanitized", message: "preserve original tls-auth block")
|
||||
}
|
||||
@@ -140,6 +155,8 @@ extension PacketTunnelProvider {
|
||||
normalizedConfig.append("\n")
|
||||
}
|
||||
let normalizedLines = normalizedConfig.split(whereSeparator: \.isNewline)
|
||||
let normalizedTail = normalizedLines.suffix(10).joined(separator: "\n")
|
||||
ovpnLog(.debug, title: "ConfigTailSanitized", message: normalizedTail)
|
||||
let redirectLines = normalizedLines
|
||||
.map(String.init)
|
||||
.filter { $0.lowercased().contains("redirect-gateway") }
|
||||
|
||||
@@ -16,6 +16,7 @@ extension PacketTunnelProvider {
|
||||
do {
|
||||
let wgConfig = try JSONDecoder().decode(WGConfig.self, from: wgConfigData)
|
||||
let wgConfigStr = wgConfig.str
|
||||
wg_log(.info, title: "config: ", message: wgConfig.redux)
|
||||
|
||||
let tunnelConfiguration = try TunnelConfiguration(fromWgQuickConfig: wgConfigStr)
|
||||
|
||||
|
||||
@@ -115,7 +115,10 @@ public class StoreKit2Helper: NSObject {
|
||||
Task {
|
||||
do {
|
||||
let products = try await Product.products(for: identifiers)
|
||||
let productDicts = products.map { product in productDictionary(for: product) }
|
||||
var productDicts: [NSDictionary] = []
|
||||
for product in products {
|
||||
productDicts.append(await productDictionary(for: product))
|
||||
}
|
||||
let fetchedIds = Set(products.map { $0.id })
|
||||
let invalidIdentifiers = identifiers.filter { !fetchedIds.contains($0) }
|
||||
DispatchQueue.main.async { completion(productDicts, Array(invalidIdentifiers), nil) }
|
||||
@@ -140,7 +143,35 @@ public class StoreKit2Helper: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
private func productDictionary(for product: Product) -> NSDictionary {
|
||||
private func introOfferPaymentModeString(_ mode: Product.SubscriptionOffer.PaymentMode) -> String {
|
||||
switch mode {
|
||||
case .freeTrial:
|
||||
return "freeTrial"
|
||||
case .payAsYouGo:
|
||||
return "payAsYouGo"
|
||||
case .payUpFront:
|
||||
return "payUpFront"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
private func introOfferTrialDays(_ period: Product.SubscriptionPeriod) -> Int {
|
||||
switch period.unit {
|
||||
case .day:
|
||||
return period.value
|
||||
case .week:
|
||||
return period.value * 7
|
||||
case .month:
|
||||
return period.value * 30
|
||||
case .year:
|
||||
return period.value * 365
|
||||
@unknown default:
|
||||
return period.value
|
||||
}
|
||||
}
|
||||
|
||||
private func productDictionary(for product: Product) async -> NSDictionary {
|
||||
let currencyCode = storefrontCurrencyCode(for: product)
|
||||
var productData: [String: Any] = [
|
||||
"productId": product.id,
|
||||
@@ -157,6 +188,16 @@ public class StoreKit2Helper: NSObject {
|
||||
if let perMonthPrice = displayPricePerMonth(for: product, billingMonths: billingMonths, currencyCode: currencyCode) {
|
||||
productData["displayPricePerMonth"] = perMonthPrice
|
||||
}
|
||||
|
||||
if let introOffer = subscription.introductoryOffer, await subscription.isEligibleForIntroOffer {
|
||||
if introOffer.paymentMode == .freeTrial {
|
||||
productData["hasFreeTrial"] = true
|
||||
productData["trialDays"] = introOfferTrialDays(introOffer.period)
|
||||
} else {
|
||||
productData["introOfferDisplayPrice"] = introOffer.displayPrice
|
||||
productData["introOfferPaymentMode"] = introOfferPaymentModeString(introOffer.paymentMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
return productData as NSDictionary
|
||||
}
|
||||
|
||||
@@ -148,4 +148,24 @@ struct WGConfig: Decodable {
|
||||
\(persistentKeepAlive == nil ? "" : "PersistentKeepalive = \(persistentKeepAlive!)")
|
||||
"""
|
||||
}
|
||||
|
||||
var redux: String {
|
||||
"""
|
||||
[Interface]
|
||||
Address = \(clientIP)
|
||||
DNS = \(dns1), \(dns2)
|
||||
MTU = \(mtu)
|
||||
PrivateKey = ***
|
||||
\(settings)
|
||||
[Peer]
|
||||
PublicKey = ***
|
||||
PresharedKey = ***
|
||||
AllowedIPs = \(allowedIPs.joined(separator: ", "))
|
||||
Endpoint = \(hostName):\(port)
|
||||
\(persistentKeepAlive == nil ? "" : "PersistentKeepalive = \(persistentKeepAlive!)")
|
||||
|
||||
SplitTunnelType = \(splitTunnelType)
|
||||
SplitTunnelSites = \(splitTunnelSites.joined(separator: ", "))
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -552,18 +552,6 @@ bool IosController::setupOpenVPN()
|
||||
return startOpenVPN(openVPNConfigStr);
|
||||
}
|
||||
|
||||
static void insertNonEmptyAwgParams(QJsonObject &wgConfig, const QJsonObject &config)
|
||||
{
|
||||
const QStringList awgProtocolKeys = configKey::awgProtocolKeys();
|
||||
|
||||
for (const QString &key : awgProtocolKeys) {
|
||||
const QJsonValue value = config.value(key);
|
||||
if (value.isString() && !value.toString().isEmpty()) {
|
||||
wgConfig.insert(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool IosController::setupWireGuard()
|
||||
{
|
||||
QJsonObject config = m_rawConfig[ProtocolUtils::key_proto_config_data(amnezia::Proto::WireGuard)].toObject();
|
||||
@@ -605,7 +593,29 @@ bool IosController::setupWireGuard()
|
||||
wgConfig.insert(configKey::persistentKeepAlive, config[configKey::persistentKeepAlive]);
|
||||
}
|
||||
|
||||
insertNonEmptyAwgParams(wgConfig, config);
|
||||
if (config.contains(configKey::isObfuscationEnabled) && config.value(configKey::isObfuscationEnabled).toBool()) {
|
||||
wgConfig.insert(configKey::initPacketMagicHeader, config[configKey::initPacketMagicHeader]);
|
||||
wgConfig.insert(configKey::responsePacketMagicHeader, config[configKey::responsePacketMagicHeader]);
|
||||
wgConfig.insert(configKey::underloadPacketMagicHeader, config[configKey::underloadPacketMagicHeader]);
|
||||
wgConfig.insert(configKey::transportPacketMagicHeader, config[configKey::transportPacketMagicHeader]);
|
||||
|
||||
wgConfig.insert(configKey::initPacketJunkSize, config[configKey::initPacketJunkSize]);
|
||||
wgConfig.insert(configKey::responsePacketJunkSize, config[configKey::responsePacketJunkSize]);
|
||||
wgConfig.insert(configKey::cookieReplyPacketJunkSize, config[configKey::cookieReplyPacketJunkSize]);
|
||||
wgConfig.insert(configKey::transportPacketJunkSize, config[configKey::transportPacketJunkSize]);
|
||||
|
||||
wgConfig.insert(configKey::junkPacketCount, config[configKey::junkPacketCount]);
|
||||
wgConfig.insert(configKey::junkPacketMinSize, config[configKey::junkPacketMinSize]);
|
||||
wgConfig.insert(configKey::junkPacketMaxSize, config[configKey::junkPacketMaxSize]);
|
||||
|
||||
wgConfig.insert(configKey::headerProtectionKey, config[configKey::headerProtectionKey]);
|
||||
wgConfig.insert(configKey::contentPaddingAddition, config[configKey::contentPaddingAddition]);
|
||||
wgConfig.insert(configKey::rekeyAfterTime, config[configKey::rekeyAfterTime]);
|
||||
wgConfig.insert(configKey::rekeyTimeout, config[configKey::rekeyTimeout]);
|
||||
wgConfig.insert(configKey::rejectAfterTime, config[configKey::rejectAfterTime]);
|
||||
wgConfig.insert(configKey::keepaliveTimeout, config[configKey::keepaliveTimeout]);
|
||||
wgConfig.insert(configKey::maxHandshakeAttempts, config[configKey::maxHandshakeAttempts]);
|
||||
}
|
||||
|
||||
QJsonDocument wgConfigDoc(wgConfig);
|
||||
QString wgConfigDocStr(wgConfigDoc.toJson(QJsonDocument::Compact));
|
||||
@@ -695,7 +705,33 @@ bool IosController::setupAwg()
|
||||
wgConfig.insert(configKey::persistentKeepAlive, config[configKey::persistentKeepAlive]);
|
||||
}
|
||||
|
||||
insertNonEmptyAwgParams(wgConfig, config);
|
||||
wgConfig.insert(configKey::initPacketMagicHeader, config[configKey::initPacketMagicHeader]);
|
||||
wgConfig.insert(configKey::responsePacketMagicHeader, config[configKey::responsePacketMagicHeader]);
|
||||
wgConfig.insert(configKey::underloadPacketMagicHeader, config[configKey::underloadPacketMagicHeader]);
|
||||
wgConfig.insert(configKey::transportPacketMagicHeader, config[configKey::transportPacketMagicHeader]);
|
||||
|
||||
wgConfig.insert(configKey::initPacketJunkSize, config[configKey::initPacketJunkSize]);
|
||||
wgConfig.insert(configKey::responsePacketJunkSize, config[configKey::responsePacketJunkSize]);
|
||||
wgConfig.insert(configKey::cookieReplyPacketJunkSize, config[configKey::cookieReplyPacketJunkSize]);
|
||||
wgConfig.insert(configKey::transportPacketJunkSize, config[configKey::transportPacketJunkSize]);
|
||||
|
||||
wgConfig.insert(configKey::junkPacketCount, config[configKey::junkPacketCount]);
|
||||
wgConfig.insert(configKey::junkPacketMinSize, config[configKey::junkPacketMinSize]);
|
||||
wgConfig.insert(configKey::junkPacketMaxSize, config[configKey::junkPacketMaxSize]);
|
||||
|
||||
wgConfig.insert(configKey::specialJunk1, config[configKey::specialJunk1]);
|
||||
wgConfig.insert(configKey::specialJunk2, config[configKey::specialJunk2]);
|
||||
wgConfig.insert(configKey::specialJunk3, config[configKey::specialJunk3]);
|
||||
wgConfig.insert(configKey::specialJunk4, config[configKey::specialJunk4]);
|
||||
wgConfig.insert(configKey::specialJunk5, config[configKey::specialJunk5]);
|
||||
|
||||
wgConfig.insert(configKey::headerProtectionKey, config[configKey::headerProtectionKey]);
|
||||
wgConfig.insert(configKey::contentPaddingAddition, config[configKey::contentPaddingAddition]);
|
||||
wgConfig.insert(configKey::rekeyAfterTime, config[configKey::rekeyAfterTime]);
|
||||
wgConfig.insert(configKey::rekeyTimeout, config[configKey::rekeyTimeout]);
|
||||
wgConfig.insert(configKey::rejectAfterTime, config[configKey::rejectAfterTime]);
|
||||
wgConfig.insert(configKey::keepaliveTimeout, config[configKey::keepaliveTimeout]);
|
||||
wgConfig.insert(configKey::maxHandshakeAttempts, config[configKey::maxHandshakeAttempts]);
|
||||
|
||||
QJsonDocument wgConfigDoc(wgConfig);
|
||||
QString wgConfigDocStr(wgConfigDoc.toJson(QJsonDocument::Compact));
|
||||
@@ -1129,6 +1165,18 @@ void IosController::fetchProducts(const QStringList &productIds,
|
||||
if (productInfo[@"displayPricePerMonth"]) {
|
||||
productData["displayPricePerMonth"] = QString::fromUtf8([productInfo[@"displayPricePerMonth"] UTF8String]);
|
||||
}
|
||||
if (productInfo[@"introOfferDisplayPrice"]) {
|
||||
productData["introOfferDisplayPrice"] = QString::fromUtf8([productInfo[@"introOfferDisplayPrice"] UTF8String]);
|
||||
}
|
||||
if (productInfo[@"introOfferPaymentMode"]) {
|
||||
productData["introOfferPaymentMode"] = QString::fromUtf8([productInfo[@"introOfferPaymentMode"] UTF8String]);
|
||||
}
|
||||
if (productInfo[@"hasFreeTrial"]) {
|
||||
productData["hasFreeTrial"] = [productInfo[@"hasFreeTrial"] boolValue];
|
||||
}
|
||||
if (productInfo[@"trialDays"]) {
|
||||
productData["trialDays"] = [productInfo[@"trialDays"] intValue];
|
||||
}
|
||||
outProducts.push_back(productData);
|
||||
}
|
||||
|
||||
@@ -1174,7 +1222,7 @@ void IosController::requestInetAccess() {
|
||||
|
||||
bool IosController::isTestFlight() {
|
||||
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
|
||||
return receiptURL && [[receiptURL lastPathComponent] isEqualToString:@"sandboxReceipt"];
|
||||
return true; //receiptURL && [[receiptURL lastPathComponent] isEqualToString:@"sandboxReceipt"];
|
||||
}
|
||||
|
||||
#if !MACOS_NE
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
#ifndef IOSCONTEXTMENU_H
|
||||
#define IOSCONTEXTMENU_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QQuickItem>
|
||||
|
||||
// Presents the native iOS edit menu (UIEditMenuInteraction) for a text
|
||||
// control. The menu items (Cut/Copy/Paste/Select All) are provided by the
|
||||
// system based on the first responder, which is Qt's text input responder
|
||||
// for the focused control.
|
||||
//
|
||||
// Needed because the ContextMenu attached type is backed by a native menu
|
||||
// on iOS only since Qt 6.10 — on Qt 6.9 it opens a Qt-drawn menu instead.
|
||||
class IosContextMenu : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
using QObject::QObject;
|
||||
|
||||
Q_INVOKABLE bool isAvailable() const;
|
||||
Q_INVOKABLE void present(QQuickItem *target, qreal x, qreal y);
|
||||
};
|
||||
|
||||
#endif // IOSCONTEXTMENU_H
|
||||
@@ -1,215 +0,0 @@
|
||||
#import "ioscontextmenu.h"
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <objc/runtime.h>
|
||||
|
||||
#include <QtCore/QCoreApplication>
|
||||
#include <QtCore/QPointer>
|
||||
#include <QtGui/QGuiApplication>
|
||||
#include <QtGui/QInputMethod>
|
||||
#include <QtQuick/QQuickWindow>
|
||||
|
||||
namespace
|
||||
{
|
||||
// Keys for attaching the helper objects to the UIView.
|
||||
const void *kEditMenuInteractionKey = &kEditMenuInteractionKey;
|
||||
const void *kEditMenuDelegateKey = &kEditMenuDelegateKey;
|
||||
const void *kEditMenuResponderKey = &kEditMenuResponderKey;
|
||||
|
||||
// Menu titles reuse the ContextMenuType translations; the accelerator
|
||||
// ampersands are meaningless on iOS and get stripped.
|
||||
NSString *menuTitle(const char *sourceText)
|
||||
{
|
||||
QString title = QCoreApplication::translate("ContextMenuType", sourceText);
|
||||
title.remove(QLatin1Char('&'));
|
||||
return title.toNSString();
|
||||
}
|
||||
|
||||
// The handler outlives the delegate call, so it must own its own copy of the
|
||||
// guarded pointer: the local variable here is captured by the block by value
|
||||
// (copy-constructed when the block is copied to the heap). Capturing a
|
||||
// C++ lambda's reference capture instead would leave the block with a
|
||||
// dangling pointer into the delegate method's stack frame.
|
||||
API_AVAILABLE(ios(16.0))
|
||||
UIAction *makeEditAction(NSString *title, const QPointer<QQuickItem> &target, const char *slot)
|
||||
{
|
||||
const QPointer<QQuickItem> guardedTarget = target;
|
||||
return [UIAction actionWithTitle:title
|
||||
image:nil
|
||||
identifier:nil
|
||||
handler:^(UIAction *) {
|
||||
if (QQuickItem *item = guardedTarget.data()) {
|
||||
// Queued: the handler fires mid-dismissal
|
||||
// of the menu, let UIKit unwind first.
|
||||
QMetaObject::invokeMethod(item, slot, Qt::QueuedConnection);
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
// UIKit presents an edit menu only when the first responder is inside the
|
||||
// interaction view's hierarchy. While the virtual keyboard is up that is
|
||||
// Qt's text input responder, but for read-only fields (or before the
|
||||
// keyboard appears) nothing suitable is first responder and the present is
|
||||
// silently ignored ("did not have performable commands and/or actions").
|
||||
// This zero-sized subview steps in as the first responder for those cases.
|
||||
@interface AmneziaEditMenuResponderView : UIView
|
||||
@end
|
||||
|
||||
@implementation AmneziaEditMenuResponderView
|
||||
|
||||
- (BOOL)canBecomeFirstResponder
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
// Builds the edit menu from the state of the focused QML text control and
|
||||
// invokes its slots directly. The system's suggested actions can't be used:
|
||||
// they are collected from the first responder, and Qt's text responder is
|
||||
// first responder only while the virtual keyboard is up (never for read-only
|
||||
// fields), which would leave the menu empty.
|
||||
API_AVAILABLE(ios(16.0))
|
||||
@interface AmneziaEditMenuDelegate : NSObject <UIEditMenuInteractionDelegate>
|
||||
@property (nonatomic, weak) UIView *responderView;
|
||||
- (void)setTargetItem:(QQuickItem *)item;
|
||||
@end
|
||||
|
||||
@implementation AmneziaEditMenuDelegate {
|
||||
QPointer<QQuickItem> m_target;
|
||||
}
|
||||
|
||||
- (void)setTargetItem:(QQuickItem *)item
|
||||
{
|
||||
m_target = item;
|
||||
}
|
||||
|
||||
- (UIMenu *)editMenuInteraction:(UIEditMenuInteraction *)interaction
|
||||
menuForConfiguration:(UIEditMenuConfiguration *)configuration
|
||||
suggestedActions:(NSArray<UIMenuElement *> *)suggestedActions
|
||||
{
|
||||
QQuickItem *item = m_target.data();
|
||||
if (!item) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
// The system's suggested actions are UICommands and get re-validated
|
||||
// against the first responder right before the menu shows, which makes
|
||||
// them hostage to Qt's input-method state. UIActions with handlers skip
|
||||
// that validation entirely, so the menu is always built by hand from the
|
||||
// QML control's state.
|
||||
const bool hasSelection = !item->property("selectedText").toString().isEmpty();
|
||||
const bool readOnly = item->property("readOnly").toBool();
|
||||
const bool canPaste = item->property("canPaste").toBool();
|
||||
const bool hasText = item->property("length").toInt() > 0;
|
||||
|
||||
NSMutableArray<UIMenuElement *> *actions = [NSMutableArray array];
|
||||
if (hasSelection && !readOnly) {
|
||||
[actions addObject:makeEditAction(menuTitle("C&ut"), m_target, "cut")];
|
||||
}
|
||||
if (hasSelection) {
|
||||
[actions addObject:makeEditAction(menuTitle("&Copy"), m_target, "copy")];
|
||||
}
|
||||
if (canPaste && !readOnly) {
|
||||
[actions addObject:makeEditAction(menuTitle("&Paste"), m_target, "paste")];
|
||||
}
|
||||
if (hasText) {
|
||||
[actions addObject:makeEditAction(menuTitle("&SelectAll"), m_target, "selectAll")];
|
||||
}
|
||||
|
||||
if (actions.count == 0) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [UIMenu menuWithTitle:@"" children:actions];
|
||||
}
|
||||
|
||||
- (void)editMenuInteraction:(UIEditMenuInteraction *)interaction
|
||||
willDismissMenuForConfiguration:(UIEditMenuConfiguration *)configuration
|
||||
animator:(id<UIEditMenuInteractionAnimating>)animator
|
||||
{
|
||||
// Give the borrowed first-responder status back once the menu goes away.
|
||||
if (self.responderView.isFirstResponder) {
|
||||
[self.responderView resignFirstResponder];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
bool IosContextMenu::isAvailable() const
|
||||
{
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
|
||||
// Since Qt 6.10 the ContextMenu attached type is backed by a native menu
|
||||
// on iOS, so the helper must stay out of the way.
|
||||
return false;
|
||||
#else
|
||||
// UIEditMenuInteraction needs iOS 16, which is the deployment target.
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
void IosContextMenu::present(QQuickItem *target, qreal x, qreal y)
|
||||
{
|
||||
if (!target || !target->window()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// On iOS QWindow::winId() is the backing UIView.
|
||||
UIView *view = (__bridge UIView *)reinterpret_cast<void *>(target->window()->winId());
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Scene coordinates match the backing view's coordinate space.
|
||||
const QPointF scenePos = target->mapToScene(QPointF(x, y));
|
||||
|
||||
AmneziaEditMenuDelegate *delegate = objc_getAssociatedObject(view, kEditMenuDelegateKey);
|
||||
UIEditMenuInteraction *interaction = objc_getAssociatedObject(view, kEditMenuInteractionKey);
|
||||
AmneziaEditMenuResponderView *responderView = objc_getAssociatedObject(view, kEditMenuResponderKey);
|
||||
if (!interaction) {
|
||||
responderView = [[AmneziaEditMenuResponderView alloc] initWithFrame:CGRectZero];
|
||||
[view addSubview:responderView];
|
||||
|
||||
delegate = [[AmneziaEditMenuDelegate alloc] init];
|
||||
delegate.responderView = responderView;
|
||||
|
||||
interaction = [[UIEditMenuInteraction alloc] initWithDelegate:delegate];
|
||||
[view addInteraction:interaction];
|
||||
|
||||
objc_setAssociatedObject(view, kEditMenuResponderKey, responderView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
objc_setAssociatedObject(view, kEditMenuDelegateKey, delegate, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
objc_setAssociatedObject(view, kEditMenuInteractionKey, interaction, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
}
|
||||
|
||||
[delegate setTargetItem:target];
|
||||
|
||||
// While the keyboard is up for the target field, Qt's text input
|
||||
// responder is the first responder and lives in this view's responder
|
||||
// chain, which satisfies UIKit. Otherwise (read-only fields never raise
|
||||
// the keyboard) borrow first-responder status. Qt's idea of the keyboard
|
||||
// state is not trustworthy here, so key off the field being editable.
|
||||
const bool readOnly = target->property("readOnly").toBool();
|
||||
if (readOnly || !QGuiApplication::inputMethod()->isVisible()) {
|
||||
[responderView becomeFirstResponder];
|
||||
}
|
||||
|
||||
// Defer the actual present to the next main-loop iteration: the request
|
||||
// arrives while the long-press touch is still active, and presenting
|
||||
// mid-gesture makes UIKit discard the menu. Requests are also coalesced —
|
||||
// a double tap asks for the menu twice (the TapHandler and the ContextMenu
|
||||
// attached type both fire), and re-presenting makes the menu flicker.
|
||||
static BOOL presentPending = NO;
|
||||
if (presentPending) {
|
||||
return;
|
||||
}
|
||||
presentPending = YES;
|
||||
|
||||
UIEditMenuConfiguration *configuration =
|
||||
[UIEditMenuConfiguration configurationWithIdentifier:nil
|
||||
sourcePoint:CGPointMake(scenePos.x(), scenePos.y())];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
presentPending = NO;
|
||||
[interaction presentEditMenuWithConfiguration:configuration];
|
||||
});
|
||||
}
|
||||
@@ -5,10 +5,8 @@
|
||||
#ifndef MACOSSTATUSICON_H
|
||||
#define MACOSSTATUSICON_H
|
||||
|
||||
#include <QMenu>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
class QMenu;
|
||||
|
||||
class MacOSStatusIcon final : public QObject {
|
||||
Q_OBJECT
|
||||
@@ -18,12 +16,12 @@ class MacOSStatusIcon final : public QObject {
|
||||
explicit MacOSStatusIcon(QObject* parent);
|
||||
~MacOSStatusIcon();
|
||||
|
||||
void setIcon(const QString& iconPath);
|
||||
void setMenu(QMenu* menu);
|
||||
public:
|
||||
void setIcon(const QString& iconUrl);
|
||||
void setIndicatorColor(const QColor& indicatorColor);
|
||||
void setMenu(NSMenu* statusBarMenu);
|
||||
void setToolTip(const QString& tooltip);
|
||||
void showMessage(const QString& title, const QString& message);
|
||||
|
||||
private:
|
||||
void* m_statusItem = nullptr;
|
||||
};
|
||||
|
||||
#endif // MACOSSTATUSICON_H
|
||||
|
||||
@@ -4,79 +4,201 @@
|
||||
|
||||
#include "macosstatusicon.h"
|
||||
#include "leakdetector.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QMenu>
|
||||
#include "logger.h"
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <UserNotifications/UserNotifications.h>
|
||||
#import <QResource>
|
||||
|
||||
/**
|
||||
* Creates a NSStatusItem with that can hold an icon. Additionally a NSView is
|
||||
* set as a subview to the button item of the status item. The view serves as
|
||||
* an indicator that can be displayed in color eventhough the icon is set as a
|
||||
* template. In that way we give the system control over it’s effective
|
||||
* appearance.
|
||||
*/
|
||||
@interface MacOSStatusIconDelegate : NSObject
|
||||
@property(assign) NSStatusItem* statusItem;
|
||||
@property(assign) NSView* statusIndicator;
|
||||
|
||||
- (void)setIcon:(NSData*)imageData;
|
||||
- (void)setIndicator;
|
||||
- (void)setIndicatorColor:(NSColor*)color;
|
||||
- (void)setMenu:(NSMenu*)statusBarMenu;
|
||||
- (void)setToolTip:(NSString*)tooltip;
|
||||
@end
|
||||
|
||||
@implementation MacOSStatusIconDelegate
|
||||
/**
|
||||
* Initializes and sets the status item and indicator objects.
|
||||
*
|
||||
* @return An instance of MacOSStatusIconDelegate.
|
||||
*/
|
||||
- (id)init {
|
||||
self = [super init];
|
||||
|
||||
// Create status item
|
||||
self.statusItem =
|
||||
[[[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength] retain];
|
||||
self.statusItem.visible = true;
|
||||
// Add the indicator as a subview
|
||||
[self setIndicator];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the image for the status icon.
|
||||
*
|
||||
* @param iconPath The data for the icon image.
|
||||
*/
|
||||
- (void)setIcon:(NSData*)imageData {
|
||||
NSImage* image = [[NSImage alloc] initWithData:imageData];
|
||||
[image setTemplate:true];
|
||||
|
||||
[self.statusItem.button setImage:image];
|
||||
[image release];
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds status indicator as a subview to the status item button.
|
||||
*/
|
||||
- (void)setIndicator {
|
||||
float viewHeight = NSHeight([self.statusItem.button bounds]);
|
||||
float dotSize = viewHeight * 0.35;
|
||||
float dotOrigin = (viewHeight - dotSize) * 0.8;
|
||||
|
||||
NSView* dot = [[NSView alloc] initWithFrame:NSMakeRect(dotOrigin, dotOrigin, dotSize, dotSize)];
|
||||
self.statusIndicator = dot;
|
||||
self.statusIndicator.wantsLayer = true;
|
||||
self.statusIndicator.layer.cornerRadius = dotSize * 0.5;
|
||||
|
||||
[self.statusItem.button addSubview:self.statusIndicator];
|
||||
[dot release];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the color if the indicator.
|
||||
*
|
||||
* @param color The indicator background color.
|
||||
*/
|
||||
- (void)setIndicatorColor:(NSColor*)color {
|
||||
if (self.statusIndicator) {
|
||||
self.statusIndicator.layer.backgroundColor = color.CGColor;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the status bar menu to the status item.
|
||||
*
|
||||
* @param statusBarMenu The menu object that is passed from QT.
|
||||
*/
|
||||
- (void)setMenu:(NSMenu*)statusBarMenu {
|
||||
[self.statusItem setMenu:statusBarMenu];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the tooltip string for the status item.
|
||||
*
|
||||
* @param tooltip The tooltip string.
|
||||
*/
|
||||
- (void)setToolTip:(NSString*)tooltip {
|
||||
[self.statusItem.button setToolTip:tooltip];
|
||||
}
|
||||
@end
|
||||
|
||||
namespace {
|
||||
Logger logger("MacOSStatusIcon");
|
||||
|
||||
MacOSStatusIconDelegate* m_statusBarIcon = nullptr;
|
||||
}
|
||||
|
||||
MacOSStatusIcon::MacOSStatusIcon(QObject* parent) : QObject(parent) {
|
||||
MZ_COUNT_CTOR(MacOSStatusIcon);
|
||||
|
||||
NSStatusItem* item = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength];
|
||||
item.visible = YES;
|
||||
m_statusItem = [item retain];
|
||||
logger.debug() << "Register delegate";
|
||||
Q_ASSERT(!m_statusBarIcon);
|
||||
|
||||
m_statusBarIcon = [[MacOSStatusIconDelegate alloc] init];
|
||||
}
|
||||
|
||||
MacOSStatusIcon::~MacOSStatusIcon() {
|
||||
MZ_COUNT_DTOR(MacOSStatusIcon);
|
||||
|
||||
NSStatusItem* item = static_cast<NSStatusItem*>(m_statusItem);
|
||||
item.menu = nil;
|
||||
[[NSStatusBar systemStatusBar] removeStatusItem:item];
|
||||
[item release];
|
||||
m_statusItem = nullptr;
|
||||
logger.debug() << "Remove delegate";
|
||||
Q_ASSERT(m_statusBarIcon);
|
||||
|
||||
[static_cast<MacOSStatusIconDelegate*>(m_statusBarIcon) dealloc];
|
||||
m_statusBarIcon = nullptr;
|
||||
}
|
||||
|
||||
void MacOSStatusIcon::setIcon(const QString& iconPath) {
|
||||
QResource resource(iconPath);
|
||||
if (!resource.isValid()) {
|
||||
qWarning() << "MacOSStatusIcon: invalid icon resource" << iconPath;
|
||||
logger.debug() << "Set icon" << iconPath;
|
||||
|
||||
QResource imageResource = QResource(iconPath);
|
||||
Q_ASSERT(imageResource.isValid());
|
||||
|
||||
[m_statusBarIcon setIcon:imageResource.uncompressedData().toNSData()];
|
||||
}
|
||||
|
||||
void MacOSStatusIcon::setIndicatorColor(const QColor& indicatorColor) {
|
||||
logger.debug() << "Set indicator color";
|
||||
|
||||
if (!indicatorColor.isValid()) {
|
||||
[m_statusBarIcon setIndicatorColor:[NSColor clearColor]];
|
||||
return;
|
||||
}
|
||||
|
||||
NSImage* image = [[NSImage alloc] initWithData:resource.uncompressedData().toNSData()];
|
||||
CGFloat side = [[NSStatusBar systemStatusBar] thickness] - 4.0;
|
||||
image.size = NSMakeSize(side, side);
|
||||
static_cast<NSStatusItem*>(m_statusItem).button.image = image;
|
||||
[image release];
|
||||
NSColor* color = [NSColor colorWithCalibratedRed:indicatorColor.red() / 255.0f
|
||||
green:indicatorColor.green() / 255.0f
|
||||
blue:indicatorColor.blue() / 255.0f
|
||||
alpha:indicatorColor.alpha() / 255.0f];
|
||||
[m_statusBarIcon setIndicatorColor:color];
|
||||
}
|
||||
|
||||
void MacOSStatusIcon::setMenu(QMenu* menu) {
|
||||
static_cast<NSStatusItem*>(m_statusItem).menu = menu ? menu->toNSMenu() : nil;
|
||||
void MacOSStatusIcon::setMenu(NSMenu* statusBarMenu) {
|
||||
logger.debug() << "Set menu";
|
||||
[m_statusBarIcon setMenu:statusBarMenu];
|
||||
}
|
||||
|
||||
void MacOSStatusIcon::setToolTip(const QString& tooltip) {
|
||||
logger.debug() << "Set tooltip";
|
||||
[m_statusBarIcon setToolTip:tooltip.toNSString()];
|
||||
}
|
||||
|
||||
void MacOSStatusIcon::showMessage(const QString& title, const QString& message) {
|
||||
logger.debug() << "Show message";
|
||||
|
||||
UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
|
||||
|
||||
// This is a no-op if authorization has already been granted.
|
||||
// This is a no-op is authorization has been granted.
|
||||
[center requestAuthorizationWithOptions:(UNAuthorizationOptionSound | UNAuthorizationOptionAlert |
|
||||
UNAuthorizationOptionBadge)
|
||||
completionHandler:^(BOOL, NSError* _Nullable error) {
|
||||
completionHandler:^(BOOL granted, NSError* _Nullable error) {
|
||||
if (error) {
|
||||
// Note: this error may happen if the application is not signed.
|
||||
qWarning() << "MacOSStatusIcon: notification authorization error:"
|
||||
<< QString::fromNSString(error.localizedDescription);
|
||||
// Note: This error may happen if the application is not signed.
|
||||
NSLog(@"Error asking for permission to send notifications %@", error);
|
||||
return;
|
||||
}
|
||||
}];
|
||||
|
||||
UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init];
|
||||
content.title = title.toNSString();
|
||||
content.body = message.toNSString();
|
||||
|
||||
content.title = [title.toNSString() autorelease];
|
||||
content.body = [message.toNSString() autorelease];
|
||||
content.sound = [UNNotificationSound defaultSound];
|
||||
|
||||
UNTimeIntervalNotificationTrigger* trigger =
|
||||
[UNTimeIntervalNotificationTrigger triggerWithTimeInterval:1 repeats:NO];
|
||||
|
||||
UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:@"amneziavpn"
|
||||
content:content
|
||||
trigger:nil];
|
||||
[content release];
|
||||
trigger:trigger];
|
||||
|
||||
[center addNotificationRequest:request
|
||||
withCompletionHandler:^(NSError* _Nullable error) {
|
||||
if (error) {
|
||||
qWarning() << "MacOSStatusIcon: local notification failed:"
|
||||
<< QString::fromNSString(error.localizedDescription);
|
||||
logger.error() << "Local Notification failed" << error;
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -25,19 +25,9 @@ H1 = $INIT_PACKET_MAGIC_HEADER
|
||||
H2 = $RESPONSE_PACKET_MAGIC_HEADER
|
||||
H3 = $UNDERLOAD_PACKET_MAGIC_HEADER
|
||||
H4 = $TRANSPORT_PACKET_MAGIC_HEADER
|
||||
HeaderProtectionKey = $HEADER_PROTECTION_KEY
|
||||
ContentPaddingAddition = $CONTENT_PADDING_ADDITION
|
||||
RekeyAfterTime = $REKEY_AFTER_TIME
|
||||
RekeyTimeout = $REKEY_TIMEOUT
|
||||
RejectAfterTime = $REJECT_AFTER_TIME
|
||||
KeepaliveTimeout = $KEEPALIVE_TIMEOUT
|
||||
MaxHandshakeAttempts = $MAX_HANDSHAKE_ATTEMPTS
|
||||
# I1 = $SPECIAL_JUNK_1
|
||||
# I2 = $SPECIAL_JUNK_2
|
||||
# I3 = $SPECIAL_JUNK_3
|
||||
# I4 = $SPECIAL_JUNK_4
|
||||
# I5 = $SPECIAL_JUNK_5
|
||||
EOF
|
||||
|
||||
# Every AWG parameter is optional - drop the lines whose value came out empty
|
||||
sed -i '/^[^=]*= *$/d' /opt/amnezia/awg/awg0.conf
|
||||
|
||||
@@ -18,17 +18,10 @@ I2 = $SPECIAL_JUNK_2
|
||||
I3 = $SPECIAL_JUNK_3
|
||||
I4 = $SPECIAL_JUNK_4
|
||||
I5 = $SPECIAL_JUNK_5
|
||||
HeaderProtectionKey = $HEADER_PROTECTION_KEY
|
||||
ContentPaddingAddition = $CONTENT_PADDING_ADDITION
|
||||
RekeyAfterTime = $REKEY_AFTER_TIME
|
||||
RekeyTimeout = $REKEY_TIMEOUT
|
||||
RejectAfterTime = $REJECT_AFTER_TIME
|
||||
KeepaliveTimeout = $KEEPALIVE_TIMEOUT
|
||||
MaxHandshakeAttempts = $MAX_HANDSHAKE_ATTEMPTS
|
||||
|
||||
[Peer]
|
||||
PublicKey = $WIREGUARD_SERVER_PUBLIC_KEY
|
||||
PresharedKey = $WIREGUARD_PSK
|
||||
AllowedIPs = 0.0.0.0/0, ::/0
|
||||
Endpoint = $SERVER_IP_ADDRESS:$AWG_SERVER_PORT
|
||||
PersistentKeepalive = $PERSISTENT_KEEPALIVE
|
||||
PersistentKeepalive = 25
|
||||
|
||||
@@ -47,20 +47,6 @@ else
|
||||
FAKETLS_SECRET=""
|
||||
fi
|
||||
|
||||
# Persist deployment state for restore on re-scan.
|
||||
{
|
||||
printf 'mode=%s\n' "$TRANSPORT_MODE"
|
||||
printf 'domain=%s\n' "$MTPROXY_TLS_DOMAIN"
|
||||
printf 'tag=%s\n' "$MTPROXY_TAG"
|
||||
printf 'additional=%s\n' "$MTPROXY_ADDITIONAL_SECRETS"
|
||||
printf 'workers_mode=%s\n' "$MTPROXY_WORKERS_MODE"
|
||||
printf 'workers=%s\n' "$MTPROXY_WORKERS"
|
||||
printf 'nat_enabled=%s\n' "$MTPROXY_NAT_ENABLED"
|
||||
printf 'nat_internal=%s\n' "$MTPROXY_NAT_INTERNAL_IP"
|
||||
printf 'nat_external=%s\n' "$MTPROXY_NAT_EXTERNAL_IP"
|
||||
printf 'public_host=%s\n' "$MTPROXY_PUBLIC_HOST"
|
||||
} > /data/mtproxy-meta
|
||||
|
||||
# Active link secret depends on transport mode
|
||||
if [ "$TRANSPORT_MODE" = "faketls" ] && [ -n "$FAKETLS_SECRET" ]; then
|
||||
LINK_SECRET="$FAKETLS_SECRET"
|
||||
|
||||
@@ -28,9 +28,6 @@ rm -f /data/config.toml
|
||||
if [ -n "$TELEMT_TAG" ]; then
|
||||
echo "ad_tag = \"$TELEMT_TAG\""
|
||||
fi
|
||||
if [ -n "$TELEMT_MIDDLE_PROXY_NAT_IP" ]; then
|
||||
echo "middle_proxy_nat_ip = \"$TELEMT_MIDDLE_PROXY_NAT_IP\""
|
||||
fi
|
||||
echo ""
|
||||
echo "[general.modes]"
|
||||
echo "classic = false"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
FROM alpine:3.15
|
||||
LABEL maintainer="AmneziaVPN"
|
||||
|
||||
ARG XRAY_RELEASE="v26.7.28"
|
||||
ARG XRAY_RELEASE="v25.8.3"
|
||||
|
||||
RUN apk add --no-cache curl unzip bash openssl netcat-openbsd dumb-init rng-tools xz
|
||||
RUN apk --update upgrade --no-cache
|
||||
|
||||
@@ -2,11 +2,17 @@ cd /opt/amnezia/xray
|
||||
XRAY_CLIENT_ID=$(xray uuid) && echo $XRAY_CLIENT_ID > /opt/amnezia/xray/xray_uuid.key
|
||||
XRAY_SHORT_ID=$(openssl rand -hex 8) && echo $XRAY_SHORT_ID > /opt/amnezia/xray/xray_short_id.key
|
||||
|
||||
# Parse x25519 keypair by label (v26.7 output has an extra Hash32 line; line-index parsing breaks).
|
||||
KEYPAIR=$(xray x25519)
|
||||
XRAY_PRIVATE_KEY=$(printf '%s\n' "$KEYPAIR" | sed -n 's/.*[Pp]rivate[ ]*[Kk]ey:[[:space:]]*//p' | head -1)
|
||||
XRAY_PUBLIC_KEY=$(printf '%s\n' "$KEYPAIR" | sed -n 's/.*(PublicKey):[[:space:]]*//p' | head -1)
|
||||
[ -z "$XRAY_PUBLIC_KEY" ] && XRAY_PUBLIC_KEY=$(printf '%s\n' "$KEYPAIR" | sed -n 's/.*[Pp]ublic[ ]*[Kk]ey:[[:space:]]*//p' | head -1)
|
||||
LINE_NUM=1
|
||||
while IFS= read -r line; do
|
||||
if [[ $LINE_NUM -gt 1 ]]
|
||||
then
|
||||
IFS=":" read FIST XRAY_PUBLIC_KEY <<< "$line"
|
||||
else
|
||||
LINE_NUM=$((LINE_NUM + 1))
|
||||
IFS=":" read FIST XRAY_PRIVATE_KEY <<< "$line"
|
||||
fi
|
||||
done <<< "$KEYPAIR"
|
||||
|
||||
XRAY_PRIVATE_KEY=$(echo $XRAY_PRIVATE_KEY | tr -d ' ')
|
||||
XRAY_PUBLIC_KEY=$(echo $XRAY_PUBLIC_KEY | tr -d ' ')
|
||||
@@ -15,4 +21,47 @@ XRAY_PUBLIC_KEY=$(echo $XRAY_PUBLIC_KEY | tr -d ' ')
|
||||
echo $XRAY_PUBLIC_KEY > /opt/amnezia/xray/xray_public.key
|
||||
echo $XRAY_PRIVATE_KEY > /opt/amnezia/xray/xray_private.key
|
||||
|
||||
# server.json is written by the client (writeServerConfigForSetup); this script only makes keys.
|
||||
|
||||
cat > /opt/amnezia/xray/server.json <<EOF
|
||||
{
|
||||
"log": {
|
||||
"loglevel": "error"
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"port": $XRAY_SERVER_PORT,
|
||||
"protocol": "vless",
|
||||
"settings": {
|
||||
"clients": [
|
||||
{
|
||||
"id": "$XRAY_CLIENT_ID",
|
||||
"flow": "xtls-rprx-vision"
|
||||
}
|
||||
],
|
||||
"decryption": "none"
|
||||
},
|
||||
"streamSettings": {
|
||||
"network": "tcp",
|
||||
"security": "reality",
|
||||
"realitySettings": {
|
||||
"dest": "$XRAY_SITE_NAME:443",
|
||||
"serverNames": [
|
||||
"$XRAY_SITE_NAME"
|
||||
],
|
||||
"privateKey": "$XRAY_PRIVATE_KEY",
|
||||
"shortIds": [
|
||||
"$XRAY_SHORT_ID"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"protocol": "freedom"
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ sudo docker run -d \
|
||||
--restart always \
|
||||
--cap-add=NET_ADMIN \
|
||||
-p $XRAY_SERVER_PORT:$XRAY_SERVER_PORT/tcp \
|
||||
-p $XRAY_SERVER_PORT:$XRAY_SERVER_PORT/udp \
|
||||
--name $CONTAINER_NAME $CONTAINER_NAME
|
||||
|
||||
sudo docker network connect amnezia-dns-net $CONTAINER_NAME
|
||||
|
||||
@@ -10,8 +10,6 @@ iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
|
||||
iptables -A INPUT -p icmp -j ACCEPT
|
||||
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
|
||||
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
|
||||
iptables -A INPUT -p tcp --dport $XRAY_SERVER_PORT -j ACCEPT
|
||||
iptables -A INPUT -p udp --dport $XRAY_SERVER_PORT -j ACCEPT
|
||||
iptables -P INPUT DROP
|
||||
|
||||
ip6tables -A INPUT -i lo -j ACCEPT
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -69,14 +69,6 @@ void ExportUiController::generateQrFromString(const QString &text)
|
||||
emit exportConfigChanged();
|
||||
}
|
||||
|
||||
void ExportUiController::generateQrFromStringRaw(const QString &text)
|
||||
{
|
||||
clearPreviousConfig();
|
||||
m_config = text;
|
||||
m_qrCodes = { qrCodeUtils::generatePlainQrCodeImage(text.toUtf8()) };
|
||||
emit exportConfigChanged();
|
||||
}
|
||||
|
||||
QString ExportUiController::getConfig()
|
||||
{
|
||||
return m_config;
|
||||
|
||||
@@ -26,7 +26,6 @@ public slots:
|
||||
void generateAwgConfig(const QString &serverId, int containerIndex, const QString &clientName);
|
||||
void generateXrayConfig(const QString &serverId, const QString &clientName);
|
||||
void generateQrFromString(const QString &text);
|
||||
void generateQrFromStringRaw(const QString &text);
|
||||
|
||||
QString getConfig();
|
||||
QString getNativeConfigString();
|
||||
|
||||
@@ -567,12 +567,12 @@ void InstallUiController::clearProcessedServerCredentials()
|
||||
|
||||
void InstallUiController::setProcessedServerCredentials(const QString &hostName, const QString &userName, const QString &secretData)
|
||||
{
|
||||
m_processedServerCredentials.hostName = hostName.trimmed();
|
||||
m_processedServerCredentials.hostName = hostName;
|
||||
if (m_processedServerCredentials.hostName.contains(":")) {
|
||||
m_processedServerCredentials.port = m_processedServerCredentials.hostName.split(":").at(1).toInt();
|
||||
m_processedServerCredentials.hostName = m_processedServerCredentials.hostName.split(":").at(0);
|
||||
}
|
||||
m_processedServerCredentials.userName = userName.trimmed();
|
||||
m_processedServerCredentials.userName = userName;
|
||||
m_processedServerCredentials.secretData = secretData;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/models/protocolConfig.h"
|
||||
#include "core/models/containerConfig.h"
|
||||
#include "core/models/protocols/awgProtocolConfig.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
@@ -387,59 +385,6 @@ bool ServersUiController::isDefaultServerCurrentlyProcessed() const
|
||||
return m_serversController->getDefaultServerId() == m_processedServerId;
|
||||
}
|
||||
|
||||
bool ServersUiController::serverHasOutdatedAwgContainer(const QString &serverId) const
|
||||
{
|
||||
// The warning suggests reinstalling the container, so it only makes sense
|
||||
// for servers the user can administer
|
||||
if (!isServerHasWriteAccess(serverId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QMap<DockerContainer, ContainerConfig> containers = m_serversController->getServerContainersMap(serverId);
|
||||
for (DockerContainer container : { DockerContainer::Awg, DockerContainer::Awg2 }) {
|
||||
if (!containers.contains(container)) {
|
||||
continue;
|
||||
}
|
||||
if (const auto* awgConfig = containers.value(container).getAwgProtocolConfig()) {
|
||||
if (awgConfig->serverConfig.protocolVersion != protocols::awg::awgV3) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ServersUiController::defaultServerHasOutdatedAwgContainer() const
|
||||
{
|
||||
return serverHasOutdatedAwgContainer(getDefaultServerId());
|
||||
}
|
||||
|
||||
bool ServersUiController::isContainerOutdatedAwg(int containerIndex) const
|
||||
{
|
||||
if (!isProcessedServerHasWriteAccess()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
DockerContainer container = static_cast<DockerContainer>(containerIndex);
|
||||
if (!ContainerUtils::isAwgContainer(container)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QMap<DockerContainer, ContainerConfig> containers = m_serversController->getServerContainersMap(m_processedServerId);
|
||||
if (!containers.contains(container)) {
|
||||
return false;
|
||||
}
|
||||
if (const auto* awgConfig = containers.value(container).getAwgProtocolConfig()) {
|
||||
return awgConfig->serverConfig.protocolVersion != protocols::awg::awgV3;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ServersUiController::isProcessedContainerOutdatedAwg() const
|
||||
{
|
||||
return isContainerOutdatedAwg(m_processedContainerIndex);
|
||||
}
|
||||
|
||||
bool ServersUiController::isProcessedServerHasWriteAccess() const
|
||||
{
|
||||
return isServerHasWriteAccess(m_processedServerId);
|
||||
|
||||
@@ -27,7 +27,6 @@ class ServersUiController : public QObject
|
||||
Q_PROPERTY(QString defaultServerDescriptionExpanded READ getDefaultServerDescriptionExpanded NOTIFY defaultServerIdChanged)
|
||||
Q_PROPERTY(bool isDefaultServerDefaultContainerHasSplitTunneling READ isDefaultServerDefaultContainerHasSplitTunneling NOTIFY defaultServerIdChanged)
|
||||
Q_PROPERTY(bool isDefaultServerFromApi READ isDefaultServerFromApi NOTIFY defaultServerIdChanged)
|
||||
Q_PROPERTY(bool defaultServerHasOutdatedAwgContainer READ defaultServerHasOutdatedAwgContainer NOTIFY defaultServerIdChanged)
|
||||
|
||||
Q_PROPERTY(QString processedServerId READ getProcessedServerId WRITE setProcessedServerId NOTIFY processedServerIdChanged)
|
||||
Q_PROPERTY(int processedContainerIndex READ getProcessedContainerIndex WRITE setProcessedContainerIndex NOTIFY processedContainerIndexChanged)
|
||||
@@ -73,11 +72,6 @@ public slots:
|
||||
bool isDefaultServerFromApi() const;
|
||||
bool hasServerWithWriteAccess() const;
|
||||
|
||||
bool serverHasOutdatedAwgContainer(const QString &serverId) const;
|
||||
bool defaultServerHasOutdatedAwgContainer() const;
|
||||
bool isContainerOutdatedAwg(int containerIndex) const;
|
||||
bool isProcessedContainerOutdatedAwg() const;
|
||||
|
||||
QString serverName(const QString &serverId) const;
|
||||
QString serverHostName(const QString &serverId) const;
|
||||
int serverDefaultContainer(const QString &serverId) const;
|
||||
|
||||
@@ -17,6 +17,8 @@ namespace configKey
|
||||
constexpr char isTrial[] = "is_trial";
|
||||
constexpr char serviceProtocol[] = "service_protocol";
|
||||
constexpr char storeProductId[] = "store_product_id";
|
||||
constexpr char hasFreeTrial[] = "has_free_trial";
|
||||
constexpr char trialDays[] = "trial_days";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +58,10 @@ QVariant ApiSubscriptionPlansModel::data(const QModelIndex &index, int role) con
|
||||
return plan.serviceProtocol;
|
||||
case StoreProductIdRole:
|
||||
return plan.storeProductId;
|
||||
case HasFreeTrialRole:
|
||||
return plan.hasFreeTrial;
|
||||
case TrialDaysRole:
|
||||
return plan.trialDays;
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
@@ -72,6 +78,8 @@ QHash<int, QByteArray> ApiSubscriptionPlansModel::roleNames() const
|
||||
{ IsTrialRole, "isTrial" },
|
||||
{ ServiceProtocolRole, "serviceProtocol" },
|
||||
{ StoreProductIdRole, "storeProductId" },
|
||||
{ HasFreeTrialRole, "hasFreeTrial" },
|
||||
{ TrialDaysRole, "trialDays" },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -94,6 +102,8 @@ void ApiSubscriptionPlansModel::updateModel(const QJsonArray &arr)
|
||||
subscriptionPlan.isTrial = planObject.value(configKey::isTrial).toBool();
|
||||
subscriptionPlan.serviceProtocol = planObject.value(configKey::serviceProtocol).toString();
|
||||
subscriptionPlan.storeProductId = planObject.value(configKey::storeProductId).toString();
|
||||
subscriptionPlan.hasFreeTrial = planObject.value(configKey::hasFreeTrial).toBool();
|
||||
subscriptionPlan.trialDays = planObject.value(configKey::trialDays).toInt();
|
||||
m_subscriptionPlans.append(std::move(subscriptionPlan));
|
||||
}
|
||||
endResetModel();
|
||||
@@ -129,3 +139,13 @@ int ApiSubscriptionPlansModel::recommendedRowIndex() const
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool ApiSubscriptionPlansModel::hasAnyFreeTrial() const
|
||||
{
|
||||
for (const SubscriptionPlanItem &plan : m_subscriptionPlans) {
|
||||
if (plan.hasFreeTrial) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,9 @@ public:
|
||||
CheckoutUrlRole,
|
||||
IsTrialRole,
|
||||
ServiceProtocolRole,
|
||||
StoreProductIdRole
|
||||
StoreProductIdRole,
|
||||
HasFreeTrialRole,
|
||||
TrialDaysRole
|
||||
};
|
||||
Q_ENUM(Roles)
|
||||
|
||||
@@ -33,6 +35,7 @@ public:
|
||||
|
||||
Q_INVOKABLE QVariantMap planAt(int row) const;
|
||||
Q_INVOKABLE int recommendedRowIndex() const;
|
||||
Q_INVOKABLE bool hasAnyFreeTrial() const;
|
||||
|
||||
private:
|
||||
struct SubscriptionPlanItem
|
||||
@@ -45,6 +48,8 @@ private:
|
||||
bool isTrial = false;
|
||||
QString serviceProtocol;
|
||||
QString storeProductId;
|
||||
bool hasFreeTrial = false;
|
||||
int trialDays = 0;
|
||||
};
|
||||
|
||||
QVector<SubscriptionPlanItem> m_subscriptionPlans;
|
||||
|
||||
@@ -22,7 +22,7 @@ QVariant IpSplitTunnelingModel::data(const QModelIndex &index, int role) const
|
||||
break;
|
||||
}
|
||||
case IpRole: {
|
||||
return m_sites.at(index.row()).second.join(", ");
|
||||
return m_sites.at(index.row()).second;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
@@ -33,7 +33,7 @@ QVariant IpSplitTunnelingModel::data(const QModelIndex &index, int role) const
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
void IpSplitTunnelingModel::updateModel(const QVector<QPair<QString, QStringList>> &sites)
|
||||
void IpSplitTunnelingModel::updateModel(const QVector<QPair<QString, QString>> &sites)
|
||||
{
|
||||
beginResetModel();
|
||||
m_sites = sites;
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include <QAbstractListModel>
|
||||
#include <QVector>
|
||||
#include <QPair>
|
||||
#include <QStringList>
|
||||
|
||||
class IpSplitTunnelingModel : public QAbstractListModel
|
||||
{
|
||||
@@ -23,13 +22,13 @@ public:
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
public slots:
|
||||
void updateModel(const QVector<QPair<QString, QStringList>> &sites);
|
||||
void updateModel(const QVector<QPair<QString, QString>> &sites);
|
||||
|
||||
protected:
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
private:
|
||||
QVector<QPair<QString, QStringList>> m_sites;
|
||||
QVector<QPair<QString, QString>> m_sites;
|
||||
};
|
||||
|
||||
#endif // IPSPLITTUNNELINGMODEL_H
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
#include <QJsonDocument>
|
||||
|
||||
#include "core/configurators/wireguardConfigurator.h"
|
||||
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/protocols/protocolUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
@@ -44,12 +42,6 @@ bool AwgConfigModel::setData(const QModelIndex &index, const QVariant &value, in
|
||||
case Roles::ClientSpecialJunk3Role: m_protocolConfig.clientConfig->specialJunk3 = strValue; break;
|
||||
case Roles::ClientSpecialJunk4Role: m_protocolConfig.clientConfig->specialJunk4 = strValue; break;
|
||||
case Roles::ClientSpecialJunk5Role: m_protocolConfig.clientConfig->specialJunk5 = strValue; break;
|
||||
case Roles::ClientContentPaddingAdditionRole: m_protocolConfig.clientConfig->contentPaddingAddition = strValue; break;
|
||||
case Roles::ClientRekeyAfterTimeRole: m_protocolConfig.clientConfig->rekeyAfterTime = strValue; break;
|
||||
case Roles::ClientRekeyTimeoutRole: m_protocolConfig.clientConfig->rekeyTimeout = strValue; break;
|
||||
case Roles::ClientRejectAfterTimeRole: m_protocolConfig.clientConfig->rejectAfterTime = strValue; break;
|
||||
case Roles::ClientKeepaliveTimeoutRole: m_protocolConfig.clientConfig->keepaliveTimeout = strValue; break;
|
||||
case Roles::ClientMaxHandshakeAttemptsRole: m_protocolConfig.clientConfig->maxHandshakeAttempts = strValue; break;
|
||||
case Roles::ServerJunkPacketCountRole: m_protocolConfig.serverConfig.junkPacketCount = strValue; break;
|
||||
case Roles::ServerJunkPacketMinSizeRole: m_protocolConfig.serverConfig.junkPacketMinSize = strValue; break;
|
||||
case Roles::ServerJunkPacketMaxSizeRole: m_protocolConfig.serverConfig.junkPacketMaxSize = strValue; break;
|
||||
@@ -66,24 +58,6 @@ bool AwgConfigModel::setData(const QModelIndex &index, const QVariant &value, in
|
||||
case Roles::ServerSpecialJunk3Role: m_protocolConfig.serverConfig.specialJunk3 = strValue; break;
|
||||
case Roles::ServerSpecialJunk4Role: m_protocolConfig.serverConfig.specialJunk4 = strValue; break;
|
||||
case Roles::ServerSpecialJunk5Role: m_protocolConfig.serverConfig.specialJunk5 = strValue; break;
|
||||
case Roles::ServerContentPaddingAdditionRole: m_protocolConfig.serverConfig.contentPaddingAddition = strValue; break;
|
||||
case Roles::ServerRekeyAfterTimeRole: m_protocolConfig.serverConfig.rekeyAfterTime = strValue; break;
|
||||
case Roles::ServerRekeyTimeoutRole: m_protocolConfig.serverConfig.rekeyTimeout = strValue; break;
|
||||
case Roles::ServerRejectAfterTimeRole: m_protocolConfig.serverConfig.rejectAfterTime = strValue; break;
|
||||
case Roles::ServerKeepaliveTimeoutRole: m_protocolConfig.serverConfig.keepaliveTimeout = strValue; break;
|
||||
case Roles::ServerMaxHandshakeAttemptsRole: m_protocolConfig.serverConfig.maxHandshakeAttempts = strValue; break;
|
||||
case Roles::ServerHeaderProtectionEnabledRole: {
|
||||
if (value.toBool()) {
|
||||
if (m_protocolConfig.serverConfig.headerProtectionKey.isEmpty()) {
|
||||
const QString originalKey = m_originalProtocolConfig.serverConfig.headerProtectionKey;
|
||||
m_protocolConfig.serverConfig.headerProtectionKey =
|
||||
originalKey.isEmpty() ? WireguardConfigurator::genClientKeys().clientPrivKey : originalKey;
|
||||
}
|
||||
} else {
|
||||
m_protocolConfig.serverConfig.headerProtectionKey.clear();
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -111,13 +85,6 @@ QVariant AwgConfigModel::data(const QModelIndex &index, int role) const
|
||||
case Roles::ClientSpecialJunk3Role: return m_protocolConfig.clientConfig->specialJunk3;
|
||||
case Roles::ClientSpecialJunk4Role: return m_protocolConfig.clientConfig->specialJunk4;
|
||||
case Roles::ClientSpecialJunk5Role: return m_protocolConfig.clientConfig->specialJunk5;
|
||||
case Roles::ClientContentPaddingAdditionRole: return m_protocolConfig.clientConfig->contentPaddingAddition;
|
||||
case Roles::ClientRekeyAfterTimeRole: return m_protocolConfig.clientConfig->rekeyAfterTime;
|
||||
case Roles::ClientRekeyTimeoutRole: return m_protocolConfig.clientConfig->rekeyTimeout;
|
||||
case Roles::ClientRejectAfterTimeRole: return m_protocolConfig.clientConfig->rejectAfterTime;
|
||||
case Roles::ClientKeepaliveTimeoutRole: return m_protocolConfig.clientConfig->keepaliveTimeout;
|
||||
case Roles::ClientMaxHandshakeAttemptsRole: return m_protocolConfig.clientConfig->maxHandshakeAttempts;
|
||||
case Roles::ClientHeaderProtectionEnabledRole: return !m_protocolConfig.clientConfig->headerProtectionKey.isEmpty();
|
||||
|
||||
case Roles::ServerJunkPacketCountRole: return m_protocolConfig.serverConfig.junkPacketCount;
|
||||
case Roles::ServerJunkPacketMinSizeRole: return m_protocolConfig.serverConfig.junkPacketMinSize;
|
||||
@@ -136,19 +103,7 @@ QVariant AwgConfigModel::data(const QModelIndex &index, int role) const
|
||||
case Roles::ServerSpecialJunk4Role: return m_protocolConfig.serverConfig.specialJunk4;
|
||||
case Roles::ServerSpecialJunk5Role: return m_protocolConfig.serverConfig.specialJunk5;
|
||||
|
||||
case Roles::ServerContentPaddingAdditionRole: return m_protocolConfig.serverConfig.contentPaddingAddition;
|
||||
case Roles::ServerRekeyAfterTimeRole: return m_protocolConfig.serverConfig.rekeyAfterTime;
|
||||
case Roles::ServerRekeyTimeoutRole: return m_protocolConfig.serverConfig.rekeyTimeout;
|
||||
case Roles::ServerRejectAfterTimeRole: return m_protocolConfig.serverConfig.rejectAfterTime;
|
||||
case Roles::ServerKeepaliveTimeoutRole: return m_protocolConfig.serverConfig.keepaliveTimeout;
|
||||
case Roles::ServerMaxHandshakeAttemptsRole: return m_protocolConfig.serverConfig.maxHandshakeAttempts;
|
||||
case Roles::ServerHeaderProtectionEnabledRole: return !m_protocolConfig.serverConfig.headerProtectionKey.isEmpty();
|
||||
|
||||
case Roles::IsAwg2Role: {
|
||||
QString version = serverProtocolVersion();
|
||||
return version == protocols::awg::awgV2 || version == protocols::awg::awgV3;
|
||||
}
|
||||
case Roles::IsAwg3Role: return serverProtocolVersion() == protocols::awg::awgV3;
|
||||
case Roles::IsAwg2Role: return m_protocolConfig.serverConfig.protocolVersion == protocols::awg::awgV2;
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
@@ -161,6 +116,8 @@ void AwgConfigModel::updateModel(amnezia::DockerContainer container, const amnez
|
||||
|
||||
m_protocolConfig = protocolConfig;
|
||||
|
||||
applyDefaultsToServerConfig(m_protocolConfig.serverConfig);
|
||||
|
||||
if (!m_protocolConfig.clientConfig.has_value()) {
|
||||
m_protocolConfig.clientConfig = amnezia::AwgClientConfig{};
|
||||
}
|
||||
@@ -171,9 +128,68 @@ void AwgConfigModel::updateModel(amnezia::DockerContainer container, const amnez
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
QString AwgConfigModel::serverProtocolVersion() const
|
||||
void AwgConfigModel::applyDefaultsToServerConfig(amnezia::AwgServerConfig& config)
|
||||
{
|
||||
return m_protocolConfig.serverConfig.protocolVersion;
|
||||
if (config.subnetAddress.isEmpty()) {
|
||||
config.subnetAddress = protocols::wireguard::defaultSubnetAddress;
|
||||
}
|
||||
if (config.port.isEmpty()) {
|
||||
config.port = protocols::awg::defaultPort;
|
||||
}
|
||||
if (config.transportProto.isEmpty()) {
|
||||
config.transportProto = ProtocolUtils::transportProtoToString(
|
||||
ProtocolUtils::defaultTransportProto(amnezia::Proto::Awg), amnezia::Proto::Awg);
|
||||
}
|
||||
if (config.junkPacketCount.isEmpty()) {
|
||||
config.junkPacketCount = protocols::awg::defaultJunkPacketCount;
|
||||
}
|
||||
if (config.junkPacketMinSize.isEmpty()) {
|
||||
config.junkPacketMinSize = protocols::awg::defaultJunkPacketMinSize;
|
||||
}
|
||||
if (config.junkPacketMaxSize.isEmpty()) {
|
||||
config.junkPacketMaxSize = protocols::awg::defaultJunkPacketMaxSize;
|
||||
}
|
||||
if (config.initPacketJunkSize.isEmpty()) {
|
||||
config.initPacketJunkSize = protocols::awg::defaultInitPacketJunkSize;
|
||||
}
|
||||
if (config.responsePacketJunkSize.isEmpty()) {
|
||||
config.responsePacketJunkSize = protocols::awg::defaultResponsePacketJunkSize;
|
||||
}
|
||||
if (config.protocolVersion == protocols::awg::awgV2) {
|
||||
if (config.cookieReplyPacketJunkSize.isEmpty()) {
|
||||
config.cookieReplyPacketJunkSize = protocols::awg::defaultCookieReplyPacketJunkSize;
|
||||
}
|
||||
if (config.transportPacketJunkSize.isEmpty()) {
|
||||
config.transportPacketJunkSize = protocols::awg::defaultTransportPacketJunkSize;
|
||||
}
|
||||
}
|
||||
if (config.initPacketMagicHeader.isEmpty()) {
|
||||
config.initPacketMagicHeader = protocols::awg::defaultInitPacketMagicHeader;
|
||||
}
|
||||
if (config.responsePacketMagicHeader.isEmpty()) {
|
||||
config.responsePacketMagicHeader = protocols::awg::defaultResponsePacketMagicHeader;
|
||||
}
|
||||
if (config.underloadPacketMagicHeader.isEmpty()) {
|
||||
config.underloadPacketMagicHeader = protocols::awg::defaultUnderloadPacketMagicHeader;
|
||||
}
|
||||
if (config.transportPacketMagicHeader.isEmpty()) {
|
||||
config.transportPacketMagicHeader = protocols::awg::defaultTransportPacketMagicHeader;
|
||||
}
|
||||
if (config.specialJunk1.isEmpty()) {
|
||||
config.specialJunk1 = protocols::awg::defaultSpecialJunk1;
|
||||
}
|
||||
if (config.specialJunk2.isEmpty()) {
|
||||
config.specialJunk2 = protocols::awg::defaultSpecialJunk2;
|
||||
}
|
||||
if (config.specialJunk3.isEmpty()) {
|
||||
config.specialJunk3 = protocols::awg::defaultSpecialJunk3;
|
||||
}
|
||||
if (config.specialJunk4.isEmpty()) {
|
||||
config.specialJunk4 = protocols::awg::defaultSpecialJunk4;
|
||||
}
|
||||
if (config.specialJunk5.isEmpty()) {
|
||||
config.specialJunk5 = protocols::awg::defaultSpecialJunk5;
|
||||
}
|
||||
}
|
||||
|
||||
void AwgConfigModel::applyDefaultsToClientConfig(amnezia::AwgClientConfig& config)
|
||||
@@ -181,6 +197,46 @@ void AwgConfigModel::applyDefaultsToClientConfig(amnezia::AwgClientConfig& confi
|
||||
if (config.mtu.isEmpty()) {
|
||||
config.mtu = protocols::awg::defaultMtu;
|
||||
}
|
||||
if (config.junkPacketCount.isEmpty()) {
|
||||
config.junkPacketCount = m_protocolConfig.serverConfig.junkPacketCount.isEmpty()
|
||||
? protocols::awg::defaultJunkPacketCount
|
||||
: m_protocolConfig.serverConfig.junkPacketCount;
|
||||
}
|
||||
if (config.junkPacketMinSize.isEmpty()) {
|
||||
config.junkPacketMinSize = m_protocolConfig.serverConfig.junkPacketMinSize.isEmpty()
|
||||
? protocols::awg::defaultJunkPacketMinSize
|
||||
: m_protocolConfig.serverConfig.junkPacketMinSize;
|
||||
}
|
||||
if (config.junkPacketMaxSize.isEmpty()) {
|
||||
config.junkPacketMaxSize = m_protocolConfig.serverConfig.junkPacketMaxSize.isEmpty()
|
||||
? protocols::awg::defaultJunkPacketMaxSize
|
||||
: m_protocolConfig.serverConfig.junkPacketMaxSize;
|
||||
}
|
||||
if (config.specialJunk1.isEmpty()) {
|
||||
config.specialJunk1 = m_protocolConfig.serverConfig.specialJunk1.isEmpty()
|
||||
? protocols::awg::defaultSpecialJunk1
|
||||
: m_protocolConfig.serverConfig.specialJunk1;
|
||||
}
|
||||
if (config.specialJunk2.isEmpty()) {
|
||||
config.specialJunk2 = m_protocolConfig.serverConfig.specialJunk2.isEmpty()
|
||||
? protocols::awg::defaultSpecialJunk2
|
||||
: m_protocolConfig.serverConfig.specialJunk2;
|
||||
}
|
||||
if (config.specialJunk3.isEmpty()) {
|
||||
config.specialJunk3 = m_protocolConfig.serverConfig.specialJunk3.isEmpty()
|
||||
? protocols::awg::defaultSpecialJunk3
|
||||
: m_protocolConfig.serverConfig.specialJunk3;
|
||||
}
|
||||
if (config.specialJunk4.isEmpty()) {
|
||||
config.specialJunk4 = m_protocolConfig.serverConfig.specialJunk4.isEmpty()
|
||||
? protocols::awg::defaultSpecialJunk4
|
||||
: m_protocolConfig.serverConfig.specialJunk4;
|
||||
}
|
||||
if (config.specialJunk5.isEmpty()) {
|
||||
config.specialJunk5 = m_protocolConfig.serverConfig.specialJunk5.isEmpty()
|
||||
? protocols::awg::defaultSpecialJunk5
|
||||
: m_protocolConfig.serverConfig.specialJunk5;
|
||||
}
|
||||
}
|
||||
|
||||
amnezia::AwgProtocolConfig AwgConfigModel::getProtocolConfig()
|
||||
@@ -191,6 +247,21 @@ amnezia::AwgProtocolConfig AwgConfigModel::getProtocolConfig()
|
||||
m_protocolConfig.clearClientConfig();
|
||||
}
|
||||
|
||||
if (m_protocolConfig.serverConfig.protocolVersion.isEmpty() ||
|
||||
m_protocolConfig.serverConfig.protocolVersion != protocols::awg::awgV2) {
|
||||
bool hasSpecialJunk = !m_protocolConfig.serverConfig.specialJunk1.trimmed().isEmpty() ||
|
||||
!m_protocolConfig.serverConfig.specialJunk2.trimmed().isEmpty() ||
|
||||
!m_protocolConfig.serverConfig.specialJunk3.trimmed().isEmpty() ||
|
||||
!m_protocolConfig.serverConfig.specialJunk4.trimmed().isEmpty() ||
|
||||
!m_protocolConfig.serverConfig.specialJunk5.trimmed().isEmpty();
|
||||
|
||||
if (hasSpecialJunk) {
|
||||
m_protocolConfig.serverConfig.protocolVersion = protocols::awg::awgV1_5;
|
||||
} else if (m_protocolConfig.serverConfig.protocolVersion.isEmpty()) {
|
||||
m_protocolConfig.serverConfig.protocolVersion = QString();
|
||||
}
|
||||
}
|
||||
|
||||
return m_protocolConfig;
|
||||
}
|
||||
|
||||
@@ -225,13 +296,6 @@ QHash<int, QByteArray> AwgConfigModel::roleNames() const
|
||||
roles[ClientSpecialJunk3Role] = "clientSpecialJunk3";
|
||||
roles[ClientSpecialJunk4Role] = "clientSpecialJunk4";
|
||||
roles[ClientSpecialJunk5Role] = "clientSpecialJunk5";
|
||||
roles[ClientContentPaddingAdditionRole] = "clientContentPaddingAddition";
|
||||
roles[ClientRekeyAfterTimeRole] = "clientRekeyAfterTime";
|
||||
roles[ClientRekeyTimeoutRole] = "clientRekeyTimeout";
|
||||
roles[ClientRejectAfterTimeRole] = "clientRejectAfterTime";
|
||||
roles[ClientKeepaliveTimeoutRole] = "clientKeepaliveTimeout";
|
||||
roles[ClientMaxHandshakeAttemptsRole] = "clientMaxHandshakeAttempts";
|
||||
roles[ClientHeaderProtectionEnabledRole] = "clientHeaderProtectionEnabled";
|
||||
|
||||
roles[ServerJunkPacketCountRole] = "serverJunkPacketCount";
|
||||
roles[ServerJunkPacketMinSizeRole] = "serverJunkPacketMinSize";
|
||||
@@ -251,16 +315,7 @@ QHash<int, QByteArray> AwgConfigModel::roleNames() const
|
||||
roles[ServerSpecialJunk4Role] = "serverSpecialJunk4";
|
||||
roles[ServerSpecialJunk5Role] = "serverSpecialJunk5";
|
||||
|
||||
roles[ServerContentPaddingAdditionRole] = "serverContentPaddingAddition";
|
||||
roles[ServerRekeyAfterTimeRole] = "serverRekeyAfterTime";
|
||||
roles[ServerRekeyTimeoutRole] = "serverRekeyTimeout";
|
||||
roles[ServerRejectAfterTimeRole] = "serverRejectAfterTime";
|
||||
roles[ServerKeepaliveTimeoutRole] = "serverKeepaliveTimeout";
|
||||
roles[ServerMaxHandshakeAttemptsRole] = "serverMaxHandshakeAttempts";
|
||||
roles[ServerHeaderProtectionEnabledRole] = "serverHeaderProtectionEnabled";
|
||||
|
||||
roles[IsAwg2Role] = "isAwg2";
|
||||
roles[IsAwg3Role] = "isAwg3";
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
@@ -26,13 +26,6 @@ public:
|
||||
ClientSpecialJunk3Role,
|
||||
ClientSpecialJunk4Role,
|
||||
ClientSpecialJunk5Role,
|
||||
ClientContentPaddingAdditionRole,
|
||||
ClientRekeyAfterTimeRole,
|
||||
ClientRekeyTimeoutRole,
|
||||
ClientRejectAfterTimeRole,
|
||||
ClientKeepaliveTimeoutRole,
|
||||
ClientMaxHandshakeAttemptsRole,
|
||||
ClientHeaderProtectionEnabledRole,
|
||||
|
||||
ServerJunkPacketCountRole,
|
||||
ServerJunkPacketMinSizeRole,
|
||||
@@ -52,16 +45,7 @@ public:
|
||||
ServerSpecialJunk4Role,
|
||||
ServerSpecialJunk5Role,
|
||||
|
||||
ServerContentPaddingAdditionRole,
|
||||
ServerRekeyAfterTimeRole,
|
||||
ServerRekeyTimeoutRole,
|
||||
ServerRejectAfterTimeRole,
|
||||
ServerKeepaliveTimeoutRole,
|
||||
ServerMaxHandshakeAttemptsRole,
|
||||
ServerHeaderProtectionEnabledRole,
|
||||
|
||||
IsAwg2Role,
|
||||
IsAwg3Role
|
||||
IsAwg2Role
|
||||
};
|
||||
|
||||
explicit AwgConfigModel(QObject *parent = nullptr);
|
||||
@@ -87,7 +71,7 @@ private:
|
||||
amnezia::AwgProtocolConfig m_protocolConfig;
|
||||
amnezia::AwgProtocolConfig m_originalProtocolConfig;
|
||||
|
||||
QString serverProtocolVersion() const;
|
||||
void applyDefaultsToServerConfig(amnezia::AwgServerConfig& config);
|
||||
void applyDefaultsToClientConfig(amnezia::AwgClientConfig& config);
|
||||
};
|
||||
|
||||
|
||||
@@ -71,6 +71,8 @@ void WireGuardConfigModel::updateModel(amnezia::DockerContainer container, const
|
||||
|
||||
m_protocolConfig = protocolConfig;
|
||||
|
||||
applyDefaultsToServerConfig(m_protocolConfig.serverConfig);
|
||||
|
||||
if (!m_protocolConfig.clientConfig.has_value()) {
|
||||
m_protocolConfig.clientConfig = amnezia::WireGuardClientConfig{};
|
||||
}
|
||||
@@ -81,6 +83,20 @@ void WireGuardConfigModel::updateModel(amnezia::DockerContainer container, const
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void WireGuardConfigModel::applyDefaultsToServerConfig(amnezia::WireGuardServerConfig& config)
|
||||
{
|
||||
if (config.subnetAddress.isEmpty()) {
|
||||
config.subnetAddress = protocols::wireguard::defaultSubnetAddress;
|
||||
}
|
||||
if (config.port.isEmpty()) {
|
||||
config.port = protocols::wireguard::defaultPort;
|
||||
}
|
||||
if (config.transportProto.isEmpty()) {
|
||||
config.transportProto = ProtocolUtils::transportProtoToString(
|
||||
ProtocolUtils::defaultTransportProto(amnezia::Proto::WireGuard), amnezia::Proto::WireGuard);
|
||||
}
|
||||
}
|
||||
|
||||
void WireGuardConfigModel::applyDefaultsToClientConfig(amnezia::WireGuardClientConfig& config)
|
||||
{
|
||||
if (config.mtu.isEmpty()) {
|
||||
|
||||
@@ -40,6 +40,7 @@ private:
|
||||
amnezia::WireGuardProtocolConfig m_protocolConfig;
|
||||
amnezia::WireGuardProtocolConfig m_originalProtocolConfig;
|
||||
|
||||
void applyDefaultsToServerConfig(amnezia::WireGuardServerConfig& config);
|
||||
void applyDefaultsToClientConfig(amnezia::WireGuardClientConfig& config);
|
||||
};
|
||||
|
||||
|
||||
@@ -70,6 +70,8 @@ bool XrayConfigModel::setData(const QModelIndex& index, const QVariant& value, i
|
||||
break;
|
||||
case Roles::XhttpPathRole: xhttp.path = str;
|
||||
break;
|
||||
case Roles::XhttpHeadersTemplateRole: xhttp.headersTemplate = str;
|
||||
break;
|
||||
case Roles::XhttpUplinkMethodRole: xhttp.uplinkMethod = str;
|
||||
break;
|
||||
case Roles::XhttpDisableGrpcRole: xhttp.disableGrpc = value.toBool();
|
||||
@@ -204,6 +206,7 @@ QVariant XrayConfigModel::data(const QModelIndex& index, int role) const
|
||||
case Roles::XhttpModeRole: return xhttp.mode;
|
||||
case Roles::XhttpHostRole: return xhttp.host;
|
||||
case Roles::XhttpPathRole: return xhttp.path;
|
||||
case Roles::XhttpHeadersTemplateRole: return xhttp.headersTemplate;
|
||||
case Roles::XhttpUplinkMethodRole: return xhttp.uplinkMethod;
|
||||
case Roles::XhttpDisableGrpcRole: return xhttp.disableGrpc;
|
||||
case Roles::XhttpDisableSseRole: return xhttp.disableSse;
|
||||
@@ -332,6 +335,9 @@ void XrayConfigModel::applyDefaultsToServerConfig(amnezia::XrayServerConfig &con
|
||||
if (config.xhttp.mode.isEmpty()) {
|
||||
config.xhttp.mode = protocols::xray::defaultXhttpMode;
|
||||
}
|
||||
if (config.xhttp.headersTemplate.isEmpty()) {
|
||||
config.xhttp.headersTemplate = protocols::xray::defaultXhttpHeadersTemplate;
|
||||
}
|
||||
if (config.xhttp.uplinkMethod.isEmpty()) {
|
||||
config.xhttp.uplinkMethod = protocols::xray::defaultXhttpUplinkMethod;
|
||||
}
|
||||
@@ -398,6 +404,7 @@ QHash<int, QByteArray> XrayConfigModel::roleNames() const
|
||||
roles[XhttpModeRole] = "xhttpMode";
|
||||
roles[XhttpHostRole] = "xhttpHost";
|
||||
roles[XhttpPathRole] = "xhttpPath";
|
||||
roles[XhttpHeadersTemplateRole] = "xhttpHeadersTemplate";
|
||||
roles[XhttpUplinkMethodRole] = "xhttpUplinkMethod";
|
||||
roles[XhttpDisableGrpcRole] = "xhttpDisableGrpc";
|
||||
roles[XhttpDisableSseRole] = "xhttpDisableSse";
|
||||
@@ -474,6 +481,7 @@ void XrayConfigModel::applyServerConfig(const amnezia::XrayServerConfig &serverC
|
||||
m_protocolConfig.serverConfig = serverConfig;
|
||||
// Clear client config since server settings changed
|
||||
m_protocolConfig.clearClientConfig();
|
||||
m_originalProtocolConfig = m_protocolConfig;
|
||||
endResetModel();
|
||||
|
||||
if (wasUnsavedChanges != hasUnsavedChanges()) {
|
||||
@@ -507,7 +515,7 @@ QStringList XrayConfigModel::fingerprintOptions()
|
||||
|
||||
QStringList XrayConfigModel::alpnOptions()
|
||||
{
|
||||
return { "h2", "http/1.1", "h2,http/1.1" };
|
||||
return { "HTTP/2", "HTTP/1.1", "HTTP/2,HTTP/1.1" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xhttpModeOptions()
|
||||
@@ -515,6 +523,11 @@ QStringList XrayConfigModel::xhttpModeOptions()
|
||||
return { "Auto", "Packet-up", "Stream-up", "Stream-one" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xhttpHeadersTemplateOptions()
|
||||
{
|
||||
return { "HTTP", "None" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xhttpUplinkMethodOptions()
|
||||
{
|
||||
return { "POST", "PUT", "PATCH" };
|
||||
@@ -522,12 +535,17 @@ QStringList XrayConfigModel::xhttpUplinkMethodOptions()
|
||||
|
||||
QStringList XrayConfigModel::xhttpSessionPlacementOptions()
|
||||
{
|
||||
return { "Path", "Header", "Cookie", "Query", "None" };
|
||||
return { "Path", "Header", "Cookie", "None" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xhttpSessionKeyOptions()
|
||||
{
|
||||
return { "Path", "Header", "None" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xhttpSeqPlacementOptions()
|
||||
{
|
||||
return { "Path", "Header", "Cookie", "Query", "None" };
|
||||
return { "Path", "Header", "Cookie", "None" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xhttpUplinkDataPlacementOptions()
|
||||
@@ -572,76 +590,6 @@ QString XrayConfigModel::mkcpDefaultWriteBufferSize()
|
||||
return QString::fromLatin1(protocols::xray::defaultMkcpWriteBufferSize);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::portDefault()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultPort);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::sniDefault()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultSni);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::xhttpHostDefault()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultXhttpHost);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::xhttpUplinkChunkSizeDefault()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultXhttpUplinkChunkSize);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::scMaxEachPostBytesMinDefault()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultXhttpScMaxEachPostBytesMin);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::scMaxEachPostBytesMaxDefault()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultXhttpScMaxEachPostBytesMax);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::scMinPostsIntervalMsMinDefault()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultXhttpScMinPostsIntervalMsMin);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::scMinPostsIntervalMsMaxDefault()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultXhttpScMinPostsIntervalMsMax);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::scStreamUpServerSecsMinDefault()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultXhttpScStreamUpServerSecsMin);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::scStreamUpServerSecsMaxDefault()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultXhttpScStreamUpServerSecsMax);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::xPaddingKeyDefault()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultXPaddingKey);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::xPaddingHeaderDefault()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultXPaddingHeader);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::xPaddingBytesMinDefault()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultXPaddingBytesMin);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::xPaddingBytesMaxDefault()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultXPaddingBytesMax);
|
||||
}
|
||||
|
||||
namespace {
|
||||
bool isValidSingleHost(const QString &t)
|
||||
{
|
||||
|
||||
@@ -33,6 +33,7 @@ public:
|
||||
XhttpModeRole,
|
||||
XhttpHostRole,
|
||||
XhttpPathRole,
|
||||
XhttpHeadersTemplateRole,
|
||||
XhttpUplinkMethodRole,
|
||||
XhttpDisableGrpcRole,
|
||||
XhttpDisableSseRole,
|
||||
@@ -101,8 +102,10 @@ public:
|
||||
Q_INVOKABLE static QStringList fingerprintOptions();
|
||||
Q_INVOKABLE static QStringList alpnOptions();
|
||||
Q_INVOKABLE static QStringList xhttpModeOptions();
|
||||
Q_INVOKABLE static QStringList xhttpHeadersTemplateOptions();
|
||||
Q_INVOKABLE static QStringList xhttpUplinkMethodOptions();
|
||||
Q_INVOKABLE static QStringList xhttpSessionPlacementOptions();
|
||||
Q_INVOKABLE static QStringList xhttpSessionKeyOptions();
|
||||
Q_INVOKABLE static QStringList xhttpSeqPlacementOptions();
|
||||
Q_INVOKABLE static QStringList xhttpUplinkDataPlacementOptions();
|
||||
Q_INVOKABLE static QStringList xPaddingPlacementOptions();
|
||||
@@ -115,21 +118,6 @@ public:
|
||||
Q_INVOKABLE static QString mkcpDefaultReadBufferSize();
|
||||
Q_INVOKABLE static QString mkcpDefaultWriteBufferSize();
|
||||
|
||||
Q_INVOKABLE static QString portDefault();
|
||||
Q_INVOKABLE static QString sniDefault();
|
||||
Q_INVOKABLE static QString xhttpHostDefault();
|
||||
Q_INVOKABLE static QString xhttpUplinkChunkSizeDefault();
|
||||
Q_INVOKABLE static QString scMaxEachPostBytesMinDefault();
|
||||
Q_INVOKABLE static QString scMaxEachPostBytesMaxDefault();
|
||||
Q_INVOKABLE static QString scMinPostsIntervalMsMinDefault();
|
||||
Q_INVOKABLE static QString scMinPostsIntervalMsMaxDefault();
|
||||
Q_INVOKABLE static QString scStreamUpServerSecsMinDefault();
|
||||
Q_INVOKABLE static QString scStreamUpServerSecsMaxDefault();
|
||||
Q_INVOKABLE static QString xPaddingKeyDefault();
|
||||
Q_INVOKABLE static QString xPaddingHeaderDefault();
|
||||
Q_INVOKABLE static QString xPaddingBytesMinDefault();
|
||||
Q_INVOKABLE static QString xPaddingBytesMaxDefault();
|
||||
|
||||
Q_INVOKABLE static bool isValidHost(const QString &host);
|
||||
Q_INVOKABLE static bool isValidSni(const QString &sni);
|
||||
Q_INVOKABLE static bool isValidPath(const QString &path);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user