Compare commits

...

13 Commits

Author SHA1 Message Date
NickVs2015 787089f4ae fix: deploy downgrade 2026-06-17 11:47:52 +03:00
NickVs2015 ebffda68c2 fix: double call filepicker 2026-06-17 11:35:37 +03:00
NickVs2015 8ce5237a5f fix: support filepicker android 9 2026-06-16 17:43:20 +03:00
NickVs2015 129ae44edc fix: backup file and sigfall when app start (#2732)
* fix: backup loading

* fix: qt 6.11 sig fall
2026-06-15 12:56:58 +07:00
vkamn 16fc44f989 chore: bump version 2026-06-14 13:09:02 +07:00
vkamn ef909d3605 chore: fix typo 2026-06-14 13:05:19 +07:00
Yaroslav Gurov b9ca3315c6 fix: handle undefined values properly 2026-06-14 13:00:44 +07:00
vkamn e9ed5b59a4 chore: bump version 2026-06-11 11:42:39 +07:00
vkamn 047dbb2677 Merge branch 'feat/proxy-storage-cache' of github-amnezia:amnezia-vpn/amnezia-client into feat/proxy-storage-cache 2026-06-09 23:02:46 +07:00
vkamn e9efe32f9b chore: bump version 2026-06-09 23:01:49 +07:00
Yaroslav Gurov 2dd3531e78 chore: bump awg dependency 2026-06-09 10:51:58 +02:00
vkamn 129f79ca2c chore: bump version 2026-05-29 22:51:23 +08:00
vkamn 50769f231d feat: add proxy storage cache 2026-05-25 19:17:17 +08:00
15 changed files with 333 additions and 124 deletions
+1 -1
View File
@@ -547,7 +547,7 @@ jobs:
env: env:
ANDROID_BUILD_PLATFORM: android-36 ANDROID_BUILD_PLATFORM: android-36
QT_VERSION: 6.10.1 QT_VERSION: 6.10.3
QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools' QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools'
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
+2 -2
View File
@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
set(PROJECT AmneziaVPN) set(PROJECT AmneziaVPN)
set(AMNEZIAVPN_VERSION 4.8.15.4) set(AMNEZIAVPN_VERSION 4.8.19.0)
project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION} project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
DESCRIPTION "AmneziaVPN" DESCRIPTION "AmneziaVPN"
@@ -12,7 +12,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
set(RELEASE_DATE "${CURRENT_DATE}") set(RELEASE_DATE "${CURRENT_DATE}")
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
set(APP_ANDROID_VERSION_CODE 2120) set(APP_ANDROID_VERSION_CODE 2129)
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(MZ_PLATFORM_NAME "linux") set(MZ_PLATFORM_NAME "linux")
@@ -42,6 +42,7 @@ import androidx.core.view.OnApplyWindowInsetsListener
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.WindowInsetsControllerCompat
import java.io.File
import java.io.IOException import java.io.IOException
import kotlin.LazyThreadSafetyMode.NONE import kotlin.LazyThreadSafetyMode.NONE
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@@ -766,7 +767,13 @@ class AmneziaActivity : QtActivity() {
fun openFile(filter: String?) { fun openFile(filter: String?) {
Log.v(TAG, "Open file with filter: $filter") Log.v(TAG, "Open file with filter: $filter")
mainScope.launch { mainScope.launch {
val intent = if (!isOnTv()) { val systemPickerPackage = listOf("com.google.android.documentsui", "com.android.documentsui")
.firstOrNull { pkg ->
try { packageManager.getPackageInfo(pkg, 0); true }
catch (_: PackageManager.NameNotFoundException) { false }
}
val intent = if (!isOnTv() && systemPickerPackage != null) {
val mimeTypes = if (!filter.isNullOrEmpty()) { val mimeTypes = if (!filter.isNullOrEmpty()) {
val extensionRegex = "\\*\\.([a-z0-9]+)".toRegex(IGNORE_CASE) val extensionRegex = "\\*\\.([a-z0-9]+)".toRegex(IGNORE_CASE)
val mime = MimeTypeMap.getSingleton() val mime = MimeTypeMap.getSingleton()
@@ -792,6 +799,7 @@ class AmneziaActivity : QtActivity() {
else -> type = "*/*" else -> type = "*/*"
} }
} }
`package` = systemPickerPackage
} }
} else { } else {
Intent(this@AmneziaActivity, TvFilePicker::class.java) Intent(this@AmneziaActivity, TvFilePicker::class.java)
@@ -803,8 +811,11 @@ class AmneziaActivity : QtActivity() {
if (isOnTv() && it?.hasExtra("activityNotFound") == true) { if (isOnTv() && it?.hasExtra("activityNotFound") == true) {
showNoFileBrowserAlertDialog() showNoFileBrowserAlertDialog()
} }
val uri = it?.data?.apply { val uri = it?.data?.let { u ->
grantUriPermission(packageName, this, Intent.FLAG_GRANT_READ_URI_PERMISSION) if (u.scheme == "content") {
try { grantUriPermission(packageName, u, Intent.FLAG_GRANT_READ_URI_PERMISSION) } catch (_: Exception) {}
}
u
}?.toString() ?: "" }?.toString() ?: ""
Log.v(TAG, "Open file: $uri") Log.v(TAG, "Open file: $uri")
if (uri.isNotEmpty()) { if (uri.isNotEmpty()) {
@@ -844,7 +855,12 @@ class AmneziaActivity : QtActivity() {
Log.v(TAG, "Get fd for $fileName") Log.v(TAG, "Get fd for $fileName")
return blockingCall(Dispatchers.IO) { return blockingCall(Dispatchers.IO) {
try { try {
pfd = contentResolver.openFileDescriptor(Uri.parse(fileName), "r") val uri = Uri.parse(fileName)
pfd = if (uri.scheme == "file") {
ParcelFileDescriptor.open(File(uri.path!!), ParcelFileDescriptor.MODE_READ_ONLY)
} else {
contentResolver.openFileDescriptor(uri, "r")
}
pfd?.fd ?: -1 pfd?.fd ?: -1
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to get fd: $e") Log.e(TAG, "Failed to get fd: $e")
@@ -1064,13 +1080,11 @@ class AmneziaActivity : QtActivity() {
@Suppress("unused") @Suppress("unused")
fun sendTouch(x: Float, y: Float) { fun sendTouch(x: Float, y: Float) {
Log.v(TAG, "Send touch: $x, $y") Log.v(TAG, "Send touch: $x, $y")
blockingCall {
findQtWindow(window.decorView)?.let { findQtWindow(window.decorView)?.let {
Log.v(TAG, "Send touch to $it") Log.v(TAG, "Send touch to $it")
it.dispatchTouchEvent(createEvent(x, y, SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN)) it.dispatchTouchEvent(createEvent(x, y, SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN))
it.dispatchTouchEvent(createEvent(x, y, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP)) it.dispatchTouchEvent(createEvent(x, y, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP))
} }
}
} }
private fun findQtWindow(view: View): View? { private fun findQtWindow(view: View): View? {
@@ -1,30 +1,36 @@
package org.amnezia.vpn package org.amnezia.vpn
import android.Manifest
import android.app.AlertDialog
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Environment
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import org.amnezia.vpn.util.Log import org.amnezia.vpn.util.Log
import java.io.File
private const val TAG = "TvFilePicker" private const val TAG = "TvFilePicker"
private const val READ_STORAGE_REQUEST_CODE = 1001
class TvFilePicker : ComponentActivity() { class TvFilePicker : ComponentActivity() {
private val fileChooseResultLauncher = registerForActivityResult(object : ActivityResultContracts.OpenDocument() { // SAF launcher for Android 10+ where File API is blocked by scoped storage
private val safLauncher = registerForActivityResult(object : ActivityResultContracts.OpenDocument() {
override fun createIntent(context: Context, input: Array<String>): Intent { override fun createIntent(context: Context, input: Array<String>): Intent {
val intent = super.createIntent(context, input) val intent = super.createIntent(context, input)
val activities = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val activitiesToResolveIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.packageManager.queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong())) context.packageManager.queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()))
} else { } else {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
context.packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY) context.packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
} }
if (activitiesToResolveIntent.all { if (activities.all {
val name = it.activityInfo.packageName val name = it.activityInfo.packageName
name.startsWith("com.google.android.tv.frameworkpackagestubs") || name.startsWith("com.android.tv.frameworkpackagestubs") name.startsWith("com.google.android.tv.frameworkpackagestubs") || name.startsWith("com.android.tv.frameworkpackagestubs")
}) { }) {
@@ -32,38 +38,140 @@ class TvFilePicker : ComponentActivity() {
} }
return intent return intent
} }
}) { }) { uri ->
setResult(RESULT_OK, Intent().apply { setResult(RESULT_OK, Intent().apply {
data = it data = uri
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}) })
finish() finish()
} }
private val directoryStack = ArrayDeque<File>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
Log.v(TAG, "onCreate") Log.v(TAG, "onCreate")
getFile() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
launchSaf()
} else {
checkPermissionAndBrowse()
}
} }
override fun onNewIntent(intent: Intent) { @Deprecated("Deprecated in Java")
super.onNewIntent(intent) override fun onBackPressed() {
Log.v(TAG, "onNewIntent") navigateBack()
getFile()
} }
private fun getFile() { private fun launchSaf() {
try { try {
Log.v(TAG, "getFile") safLauncher.launch(arrayOf("*/*"))
fileChooseResultLauncher.launch(arrayOf("*/*"))
} catch (_: ActivityNotFoundException) { } catch (_: ActivityNotFoundException) {
Log.w(TAG, "Activity not found") Log.w(TAG, "No SAF activity found")
setResult(RESULT_CANCELED, Intent().apply { putExtra("activityNotFound", true) }) setResult(RESULT_CANCELED, Intent().apply { putExtra("activityNotFound", true) })
finish() finish()
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to get file: $e") Log.e(TAG, "SAF launch failed: $e")
setResult(RESULT_CANCELED) setResult(RESULT_CANCELED)
finish() finish()
} }
} }
private fun checkPermissionAndBrowse() {
if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), READ_STORAGE_REQUEST_CODE)
} else {
showRootDirectory()
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == READ_STORAGE_REQUEST_CODE &&
grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
showRootDirectory()
} else {
setResult(RESULT_CANCELED)
finish()
}
}
private fun showRootDirectory() {
@Suppress("DEPRECATION")
val primaryExternal = Environment.getExternalStorageDirectory()
val storageDir = File("/storage")
// Pre-seed stack with /storage so Back from primary storage goes there (USB drives etc.)
if (storageDir.exists() && storageDir.canonicalPath != primaryExternal.canonicalPath) {
directoryStack.addLast(storageDir)
}
showDirectory(primaryExternal)
}
private fun navigateBack() {
if (directoryStack.size > 1) {
directoryStack.removeLast()
val parent = directoryStack.removeLast()
showDirectory(parent)
} else {
setResult(RESULT_CANCELED)
finish()
}
}
private fun showDirectory(dir: File) {
directoryStack.addLast(dir)
Log.v(TAG, "Showing directory: ${dir.absolutePath}")
val entries = try {
dir.listFiles()
?.sortedWith(compareBy({ !it.isDirectory }, { it.name.lowercase() }))
?: emptyList()
} catch (e: Exception) {
Log.e(TAG, "Failed to list directory: $e")
emptyList()
}
val names = entries.map { if (it.isDirectory) "[${it.name}]" else it.name }.toTypedArray()
val builder = AlertDialog.Builder(this)
.setTitle(dir.absolutePath)
if (entries.isEmpty()) {
builder.setMessage("No files available")
} else {
builder.setItems(names) { dialog, which ->
dialog.dismiss()
val selected = entries[which]
if (selected.isDirectory) {
showDirectory(selected)
} else {
Log.v(TAG, "Selected file: ${selected.absolutePath}")
setResult(RESULT_OK, Intent().apply {
data = Uri.fromFile(selected)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
})
finish()
}
}
}
if (directoryStack.size > 1) {
builder.setNegativeButton("↑ Back") { dialog, _ ->
dialog.dismiss()
navigateBack()
}
} else {
builder.setNegativeButton(android.R.string.cancel) { _, _ ->
setResult(RESULT_CANCELED)
finish()
}
}
builder.setOnCancelListener {
setResult(RESULT_CANCELED)
finish()
}
builder.show()
}
} }
+86 -55
View File
@@ -18,6 +18,7 @@
#include "amnezia_application.h" #include "amnezia_application.h"
#include "core/api/apiUtils.h" #include "core/api/apiUtils.h"
#include "core/networkUtilities.h" #include "core/networkUtilities.h"
#include "settings.h"
#include "utilities.h" #include "utilities.h"
#ifdef AMNEZIA_DESKTOP #ifdef AMNEZIA_DESKTOP
@@ -51,15 +52,78 @@ namespace
constexpr QLatin1String unprocessableSubscriptionMessage("Failed to retrieve subscription information. Is it activated?"); constexpr QLatin1String unprocessableSubscriptionMessage("Failed to retrieve subscription information. Is it activated?");
constexpr int proxyStorageRequestTimeoutMsecs = 3000; constexpr int proxyStorageRequestTimeoutMsecs = 3000;
QStringList shuffledProxyUrls(const QStringList &proxyUrls)
{
QStringList shuffled = proxyUrls;
std::random_device randomDevice;
std::mt19937 generator(randomDevice());
std::shuffle(shuffled.begin(), shuffled.end(), generator);
return shuffled;
}
QString getProxyUrlsCacheKey(const QString &serviceType, const QString &userCountryCode)
{
return QStringLiteral("service_%1_country_%2").arg(serviceType, userCountryCode);
}
bool decryptProxyUrlsPayload(const QByteArray &encryptedPayload, bool isDevEnvironment, QByteArray &decryptedPayload)
{
try {
QByteArray key = isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
if (!isDevEnvironment) {
QCryptographicHash hash(QCryptographicHash::Sha512);
hash.addData(key);
QByteArray h = hash.result().toHex();
QByteArray decKey = QByteArray::fromHex(h.left(64));
QByteArray iv = QByteArray::fromHex(h.mid(64, 32));
QByteArray ba = QByteArray::fromBase64(encryptedPayload);
QSimpleCrypto::QBlockCipher cipher;
decryptedPayload = cipher.decryptAesBlockCipher(ba, decKey, iv);
} else {
decryptedPayload = encryptedPayload;
}
return true;
} catch (...) {
Utils::logException();
return false;
}
}
QStringList readCachedProxyUrls(const QByteArray &cachedProxyUrlsEncrypted, bool isDevEnvironment)
{
if (cachedProxyUrlsEncrypted.isEmpty()) {
return {};
}
QByteArray cachedProxyUrlsDecrypted;
if (!decryptProxyUrlsPayload(cachedProxyUrlsEncrypted, isDevEnvironment, cachedProxyUrlsDecrypted)) {
qCritical() << "error decrypting cached proxy urls payload";
return {};
}
QJsonArray endpointsArray = QJsonDocument::fromJson(cachedProxyUrlsDecrypted).array();
QStringList endpoints;
endpoints.reserve(endpointsArray.size());
for (const QJsonValue &endpoint : endpointsArray) {
endpoints.push_back(endpoint.toString());
}
return endpoints;
}
} }
GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs, GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
const bool isStrictKillSwitchEnabled, QObject *parent) const bool isStrictKillSwitchEnabled, const std::shared_ptr<Settings> &settings,
QObject *parent)
: QObject(parent), : QObject(parent),
m_gatewayEndpoint(gatewayEndpoint), m_gatewayEndpoint(gatewayEndpoint),
m_isDevEnvironment(isDevEnvironment), m_isDevEnvironment(isDevEnvironment),
m_requestTimeoutMsecs(requestTimeoutMsecs), m_requestTimeoutMsecs(requestTimeoutMsecs),
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled) m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled),
m_settings(settings)
{ {
} }
@@ -310,8 +374,9 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
QStringList proxyStorageUrls; QStringList proxyStorageUrls;
appendStorageUrls(primaryBaseUrls, proxyStorageUrls); appendStorageUrls(primaryBaseUrls, proxyStorageUrls);
appendStorageUrls(fallbackBaseUrls, proxyStorageUrls); appendStorageUrls(fallbackBaseUrls, proxyStorageUrls);
const QString proxyUrlsCacheKey = getProxyUrlsCacheKey(serviceType, userCountryCode);
getProxyUrlsAsync(proxyStorageUrls, 0, [this, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) { getProxyUrlsAsync(proxyStorageUrls, 0, proxyUrlsCacheKey, [this, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) {
getProxyUrlAsync(proxyUrls, 0, [this, encRequestData, endpoint, processResponse](const QString &proxyUrl) { getProxyUrlAsync(proxyUrls, 0, [this, encRequestData, endpoint, processResponse](const QString &proxyUrl) {
bypassProxyAsync(endpoint, proxyUrl, encRequestData, bypassProxyAsync(endpoint, proxyUrl, encRequestData,
[processResponse, this](const QByteArray &decryptedBody, bool isDecryptionSuccessful, [processResponse, this](const QByteArray &decryptedBody, bool isDecryptionSuccessful,
@@ -357,8 +422,6 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
std::shuffle(primaryBaseUrls.begin(), primaryBaseUrls.end(), generator); std::shuffle(primaryBaseUrls.begin(), primaryBaseUrls.end(), generator);
std::shuffle(fallbackBaseUrls.begin(), fallbackBaseUrls.end(), generator); std::shuffle(fallbackBaseUrls.begin(), fallbackBaseUrls.end(), generator);
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
auto appendStorageUrls = [&serviceType, &userCountryCode](const QStringList &baseUrls, QStringList &target) { auto appendStorageUrls = [&serviceType, &userCountryCode](const QStringList &baseUrls, QStringList &target) {
if (!serviceType.isEmpty()) { if (!serviceType.isEmpty()) {
for (const auto &baseUrl : baseUrls) { for (const auto &baseUrl : baseUrls) {
@@ -374,10 +437,12 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
QStringList proxyStorageUrls; QStringList proxyStorageUrls;
appendStorageUrls(primaryBaseUrls, proxyStorageUrls); appendStorageUrls(primaryBaseUrls, proxyStorageUrls);
appendStorageUrls(fallbackBaseUrls, proxyStorageUrls); appendStorageUrls(fallbackBaseUrls, proxyStorageUrls);
const QString proxyUrlsCacheKey = getProxyUrlsCacheKey(serviceType, userCountryCode);
const QByteArray cachedProxyUrlsEncrypted = m_settings->readGatewayProxyUrls(proxyUrlsCacheKey);
if (proxyStorageUrls.empty()) { if (proxyStorageUrls.empty()) {
qDebug() << "empty storage endpoint list"; qDebug() << "empty storage endpoint list";
return {}; return readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment);
} }
for (const auto &proxyStorageUrl : proxyStorageUrls) { for (const auto &proxyStorageUrl : proxyStorageUrls) {
@@ -392,26 +457,8 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
auto encryptedResponseBody = reply->readAll(); auto encryptedResponseBody = reply->readAll();
reply->deleteLater(); reply->deleteLater();
EVP_PKEY *privateKey = nullptr;
QByteArray responseBody; QByteArray responseBody;
try { if (!decryptProxyUrlsPayload(encryptedResponseBody, m_isDevEnvironment, responseBody)) {
if (!m_isDevEnvironment) {
QCryptographicHash hash(QCryptographicHash::Sha512);
hash.addData(key);
QByteArray hashResult = hash.result().toHex();
QByteArray key = QByteArray::fromHex(hashResult.left(64));
QByteArray iv = QByteArray::fromHex(hashResult.mid(64, 32));
QByteArray ba = QByteArray::fromBase64(encryptedResponseBody);
QSimpleCrypto::QBlockCipher blockCipher;
responseBody = blockCipher.decryptAesBlockCipher(ba, key, iv);
} else {
responseBody = encryptedResponseBody;
}
} catch (...) {
Utils::logException();
qCritical() << "error loading private key from environment variables or decrypting payload" << encryptedResponseBody; qCritical() << "error loading private key from environment variables or decrypting payload" << encryptedResponseBody;
continue; continue;
} }
@@ -422,6 +469,8 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
for (const auto &endpoint : endpointsArray) { for (const auto &endpoint : endpointsArray) {
endpoints.push_back(endpoint.toString()); endpoints.push_back(endpoint.toString());
} }
m_settings->writeGatewayProxyUrls(proxyUrlsCacheKey, encryptedResponseBody);
return endpoints; return endpoints;
} else { } else {
auto replyError = reply->error(); auto replyError = reply->error();
@@ -433,7 +482,7 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
reply->deleteLater(); reply->deleteLater();
} }
} }
return {}; return readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment);
} }
bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody, bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody,
@@ -571,10 +620,12 @@ void GatewayController::bypassProxy(const QString &endpoint, const QString &serv
} }
void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex, void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
std::function<void(const QStringList &)> onComplete) const QString &proxyUrlsCacheKey, std::function<void(const QStringList &)> onComplete)
{ {
const QByteArray cachedProxyUrlsEncrypted = m_settings->readGatewayProxyUrls(proxyUrlsCacheKey);
if (currentProxyStorageIndex >= proxyStorageUrls.size()) { if (currentProxyStorageIndex >= proxyStorageUrls.size()) {
onComplete({}); onComplete(shuffledProxyUrls(readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment)));
return; return;
} }
@@ -587,33 +638,17 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co
// connect(reply, &QNetworkReply::sslErrors, this, [state](const QList<QSslError> &e) { *(state->sslErrors) = e; }); // connect(reply, &QNetworkReply::sslErrors, this, [state](const QList<QSslError> &e) { *(state->sslErrors) = e; });
connect(reply, &QNetworkReply::finished, this, [this, proxyStorageUrls, currentProxyStorageIndex, onComplete, reply]() { connect(reply, &QNetworkReply::finished, this,
[this, proxyStorageUrls, currentProxyStorageIndex, proxyUrlsCacheKey, onComplete, reply]() {
if (reply->error() == QNetworkReply::NoError) { if (reply->error() == QNetworkReply::NoError) {
QByteArray encrypted = reply->readAll(); QByteArray encrypted = reply->readAll();
reply->deleteLater(); reply->deleteLater();
QByteArray responseBody; QByteArray responseBody;
try { if (!decryptProxyUrlsPayload(encrypted, m_isDevEnvironment, responseBody)) {
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
if (!m_isDevEnvironment) {
QCryptographicHash hash(QCryptographicHash::Sha512);
hash.addData(key);
QByteArray h = hash.result().toHex();
QByteArray decKey = QByteArray::fromHex(h.left(64));
QByteArray iv = QByteArray::fromHex(h.mid(64, 32));
QByteArray ba = QByteArray::fromBase64(encrypted);
QSimpleCrypto::QBlockCipher cipher;
responseBody = cipher.decryptAesBlockCipher(ba, decKey, iv);
} else {
responseBody = encrypted;
}
} catch (...) {
Utils::logException();
qCritical() << "error decrypting payload"; qCritical() << "error decrypting payload";
QMetaObject::invokeMethod( QMetaObject::invokeMethod(
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection); this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, proxyUrlsCacheKey, onComplete); }, Qt::QueuedConnection);
return; return;
} }
@@ -621,13 +656,9 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co
QStringList endpoints; QStringList endpoints;
for (const QJsonValue &endpoint : endpointsArray) for (const QJsonValue &endpoint : endpointsArray)
endpoints.push_back(endpoint.toString()); endpoints.push_back(endpoint.toString());
m_settings->writeGatewayProxyUrls(proxyUrlsCacheKey, encrypted);
QStringList shuffled = endpoints; onComplete(shuffledProxyUrls(endpoints));
std::random_device randomDevice;
std::mt19937 generator(randomDevice());
std::shuffle(shuffled.begin(), shuffled.end(), generator);
onComplete(shuffled);
return; return;
} }
@@ -636,7 +667,7 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co
qDebug() << "go to the next storage endpoint"; qDebug() << "go to the next storage endpoint";
reply->deleteLater(); reply->deleteLater();
QMetaObject::invokeMethod( QMetaObject::invokeMethod(
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection); this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, proxyUrlsCacheKey, onComplete); }, Qt::QueuedConnection);
}); });
} }
+9 -2
View File
@@ -7,6 +7,9 @@
#include <QPair> #include <QPair>
#include <QPromise> #include <QPromise>
#include <QSharedPointer> #include <QSharedPointer>
#include <QString>
#include <QStringList>
#include <memory>
#include "core/defs.h" #include "core/defs.h"
@@ -14,13 +17,16 @@
#include "platforms/ios/ios_controller.h" #include "platforms/ios/ios_controller.h"
#endif #endif
class Settings;
class GatewayController : public QObject class GatewayController : public QObject
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs, explicit GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
const bool isStrictKillSwitchEnabled, QObject *parent = nullptr); const bool isStrictKillSwitchEnabled, const std::shared_ptr<Settings> &settings,
QObject *parent = nullptr);
amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody); amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody);
QFuture<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject apiPayload); QFuture<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject apiPayload);
@@ -53,7 +59,7 @@ private:
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction); std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction);
void getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex, void getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
std::function<void(const QStringList &)> onComplete); const QString &proxyUrlsCacheKey, std::function<void(const QStringList &)> onComplete);
void getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex, std::function<void(const QString &)> onComplete); void getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex, std::function<void(const QString &)> onComplete);
void bypassProxyAsync( void bypassProxyAsync(
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData, const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
@@ -63,6 +69,7 @@ private:
QString m_gatewayEndpoint; QString m_gatewayEndpoint;
bool m_isDevEnvironment = false; bool m_isDevEnvironment = false;
bool m_isStrictKillSwitchEnabled = false; bool m_isStrictKillSwitchEnabled = false;
std::shared_ptr<Settings> m_settings;
inline static QString m_proxyUrl; inline static QString m_proxyUrl;
}; };
+32 -32
View File
@@ -390,55 +390,55 @@ bool Daemon::parseConfig(const QJsonObject& obj, InterfaceConfig& config) {
config.m_killSwitchEnabled = QVariant(obj.value("killSwitchOption").toString()).toBool(); config.m_killSwitchEnabled = QVariant(obj.value("killSwitchOption").toString()).toBool();
if (!obj.value("Jc").isNull()) { if (const auto jc = obj.value("Jc"); !jc.isUndefined()) {
config.m_junkPacketCount = obj.value("Jc").toString(); config.m_junkPacketCount = jc.toString();
} }
if (!obj.value("Jmin").isNull()) { if (const auto jmin = obj.value("Jmin"); !jmin.isUndefined()) {
config.m_junkPacketMinSize = obj.value("Jmin").toString(); config.m_junkPacketMinSize = jmin.toString();
} }
if (!obj.value("Jmax").isNull()) { if (const auto jmax = obj.value("Jmax"); !jmax.isUndefined()) {
config.m_junkPacketMaxSize = obj.value("Jmax").toString(); config.m_junkPacketMaxSize = jmax.toString();
} }
if (!obj.value("S1").isNull()) { if (const auto s1 = obj.value("S1"); !s1.isUndefined()) {
config.m_initPacketJunkSize = obj.value("S1").toString(); config.m_initPacketJunkSize = s1.toString();
} }
if (!obj.value("S2").isNull()) { if (const auto s2 = obj.value("S2"); !s2.isUndefined()) {
config.m_responsePacketJunkSize = obj.value("S2").toString(); config.m_responsePacketJunkSize = s2.toString();
} }
if (!obj.value("S3").isNull()) { if (const auto s3 = obj.value("S3"); !s3.isUndefined()) {
config.m_cookieReplyPacketJunkSize = obj.value("S3").toString(); config.m_cookieReplyPacketJunkSize = s3.toString();
} }
if (!obj.value("S4").isNull()) { if (const auto s4 = obj.value("S4"); !s4.isUndefined()) {
config.m_transportPacketJunkSize = obj.value("S4").toString(); config.m_transportPacketJunkSize = s4.toString();
} }
if (!obj.value("H1").isNull()) { if (const auto h1 = obj.value("H1"); !h1.isUndefined()) {
config.m_initPacketMagicHeader = obj.value("H1").toString(); config.m_initPacketMagicHeader = h1.toString();
} }
if (!obj.value("H2").isNull()) { if (const auto h2 = obj.value("H2"); !h2.isUndefined()) {
config.m_responsePacketMagicHeader = obj.value("H2").toString(); config.m_responsePacketMagicHeader = h2.toString();
} }
if (!obj.value("H3").isNull()) { if (const auto h3 = obj.value("H3"); !h3.isUndefined()) {
config.m_underloadPacketMagicHeader = obj.value("H3").toString(); config.m_underloadPacketMagicHeader = h3.toString();
} }
if (!obj.value("H4").isNull()) { if (const auto h4 = obj.value("H4"); !h4.isUndefined()) {
config.m_transportPacketMagicHeader = obj.value("H4").toString(); config.m_transportPacketMagicHeader = h4.toString();
} }
if (!obj.value("I1").isNull()) { if (const auto i1 = obj.value("I1"); !i1.isUndefined()) {
config.m_specialJunk["I1"] = obj.value("I1").toString(); config.m_specialJunk["I1"] = i1.toString();
} }
if (!obj.value("I2").isNull()) { if (const auto i2 = obj.value("I2"); !i2.isUndefined()) {
config.m_specialJunk["I2"] = obj.value("I2").toString(); config.m_specialJunk["I2"] = i2.toString();
} }
if (!obj.value("I3").isNull()) { if (const auto i3 = obj.value("I3"); !i3.isUndefined()) {
config.m_specialJunk["I3"] = obj.value("I3").toString(); config.m_specialJunk["I3"] = i3.toString();
} }
if (!obj.value("I4").isNull()) { if (const auto i4 = obj.value("I4"); !i4.isUndefined()) {
config.m_specialJunk["I4"] = obj.value("I4").toString(); config.m_specialJunk["I4"] = i4.toString();
} }
if (!obj.value("I5").isNull()) { if (const auto i5 = obj.value("I5"); !i5.isUndefined()) {
config.m_specialJunk["I5"] = obj.value("I5").toString(); config.m_specialJunk["I5"] = i5.toString();
} }
return true; return true;
+19
View File
@@ -15,6 +15,7 @@ namespace
const char cloudFlareNs2[] = "1.0.0.1"; const char cloudFlareNs2[] = "1.0.0.1";
constexpr char gatewayEndpoint[] = "http://gw.amnezia.org:80/"; constexpr char gatewayEndpoint[] = "http://gw.amnezia.org:80/";
constexpr char proxyUrlsKey[] = "Conf/proxyUrls/";
} }
Settings::Settings(QObject *parent) : QObject(parent), m_settings(ORGANIZATION_NAME, APPLICATION_NAME, this) Settings::Settings(QObject *parent) : QObject(parent), m_settings(ORGANIZATION_NAME, APPLICATION_NAME, this)
@@ -526,6 +527,24 @@ void Settings::toggleDevGatewayEnv(bool enabled)
m_settings.setValue("Conf/devGatewayEnv", enabled); m_settings.setValue("Conf/devGatewayEnv", enabled);
} }
QByteArray Settings::readGatewayProxyUrls(const QString &cacheKey) const
{
if (cacheKey.isEmpty()) {
return {};
}
return m_settings.value(QString(proxyUrlsKey) + cacheKey).toByteArray();
}
void Settings::writeGatewayProxyUrls(const QString &cacheKey, const QByteArray &proxyUrlsEncrypted)
{
if (cacheKey.isEmpty()) {
return;
}
m_settings.setValue(QString(proxyUrlsKey) + cacheKey, proxyUrlsEncrypted);
}
bool Settings::isHomeAdLabelVisible() bool Settings::isHomeAdLabelVisible()
{ {
return m_settings.value("Conf/homeAdLabelVisible", true).toBool(); return m_settings.value("Conf/homeAdLabelVisible", true).toBool();
+3
View File
@@ -4,6 +4,7 @@
#include <QObject> #include <QObject>
#include <QSettings> #include <QSettings>
#include <QString> #include <QString>
#include <QByteArray>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
@@ -234,6 +235,8 @@ public:
QString getGatewayEndpoint(bool isTestPurchase = false); QString getGatewayEndpoint(bool isTestPurchase = false);
bool isDevGatewayEnv(bool isTestPurchase = false); bool isDevGatewayEnv(bool isTestPurchase = false);
void toggleDevGatewayEnv(bool enabled); void toggleDevGatewayEnv(bool enabled);
QByteArray readGatewayProxyUrls(const QString &cacheKey) const;
void writeGatewayProxyUrls(const QString &cacheKey, const QByteArray &proxyUrlsEncrypted);
bool isHomeAdLabelVisible(); bool isHomeAdLabelVisible();
void disableHomeAdLabel(); void disableHomeAdLabel();
@@ -1027,7 +1027,7 @@ bool ApiConfigsController::updateServiceFromTelegram(const int serverIndex)
#endif #endif
GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs, GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled()); m_settings->isStrictKillSwitchEnabled(), m_settings);
auto serverConfig = m_serversModel->getServerConfig(serverIndex); auto serverConfig = m_serversModel->getServerConfig(serverIndex);
auto installationUuid = m_settings->getInstallationUuid(true); auto installationUuid = m_settings->getInstallationUuid(true);
@@ -1273,6 +1273,6 @@ ErrorCode ApiConfigsController::executeRequest(const QString &endpoint, const QJ
bool isTestPurchase) bool isTestPurchase)
{ {
GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase), m_settings->isDevGatewayEnv(isTestPurchase), GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase), m_settings->isDevGatewayEnv(isTestPurchase),
apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled()); apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled(), m_settings);
return gatewayController.post(endpoint, apiPayload, responseBody); return gatewayController.post(endpoint, apiPayload, responseBody);
} }
@@ -32,7 +32,8 @@ void ApiNewsController::fetchNews(bool showError)
} }
auto gatewayController = QSharedPointer<GatewayController>::create(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), auto gatewayController = QSharedPointer<GatewayController>::create(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(),
apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled()); apiDefs::requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled(), m_settings);
QJsonObject payload; QJsonObject payload;
payload.insert("locale", m_settings->getAppLanguage().name().split("_").first()); payload.insert("locale", m_settings->getAppLanguage().name().split("_").first());
@@ -71,7 +71,7 @@ bool ApiSettingsController::getAccountInfo(bool reload)
bool isTestPurchase = apiConfig.value(apiDefs::key::isTestPurchase).toBool(false); bool isTestPurchase = apiConfig.value(apiDefs::key::isTestPurchase).toBool(false);
GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase), m_settings->isDevGatewayEnv(isTestPurchase), GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase), m_settings->isDevGatewayEnv(isTestPurchase),
requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled()); requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled(), m_settings);
QJsonObject apiPayload; QJsonObject apiPayload;
apiPayload[configKey::userCountryCode] = apiConfig.value(configKey::userCountryCode).toString(); apiPayload[configKey::userCountryCode] = apiConfig.value(configKey::userCountryCode).toString();
@@ -110,7 +110,7 @@ void ApiSettingsController::getRenewalLink()
auto gatewayController = QSharedPointer<GatewayController>::create(m_settings->getGatewayEndpoint(isTestPurchase), auto gatewayController = QSharedPointer<GatewayController>::create(m_settings->getGatewayEndpoint(isTestPurchase),
m_settings->isDevGatewayEnv(isTestPurchase), m_settings->isDevGatewayEnv(isTestPurchase),
requestTimeoutMsecs, requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled()); m_settings->isStrictKillSwitchEnabled(), m_settings);
QJsonObject apiPayload; QJsonObject apiPayload;
apiPayload[configKey::userCountryCode] = apiConfig.value(configKey::userCountryCode).toString(); apiPayload[configKey::userCountryCode] = apiConfig.value(configKey::userCountryCode).toString();
+17 -2
View File
@@ -17,6 +17,8 @@ import "../Controls2/TextTypes"
PageType { PageType {
id: root id: root
property bool isRestoringBackup: false
Connections { Connections {
target: SettingsController target: SettingsController
@@ -138,9 +140,14 @@ PageType {
textColor: AmneziaStyle.color.paleGray textColor: AmneziaStyle.color.paleGray
borderWidth: 1 borderWidth: 1
enabled: !root.isRestoringBackup
text: qsTr("Restore from backup") text: qsTr("Restore from backup")
clickedFunc: function() { clickedFunc: function() {
if (root.isRestoringBackup) {
return
}
var filePath = SystemController.getFileName(qsTr("Open backup file"), var filePath = SystemController.getFileName(qsTr("Open backup file"),
qsTr("Backup files (*.backup)")) qsTr("Backup files (*.backup)"))
if (filePath !== "") { if (filePath !== "") {
@@ -152,6 +159,10 @@ PageType {
} }
function restoreBackup(filePath) { function restoreBackup(filePath) {
if (root.isRestoringBackup) {
return
}
var headerText = qsTr("Import settings from a backup file?") var headerText = qsTr("Import settings from a backup file?")
var descriptionText = qsTr("All current settings will be reset"); var descriptionText = qsTr("All current settings will be reset");
var yesButtonText = qsTr("Continue") var yesButtonText = qsTr("Continue")
@@ -161,9 +172,13 @@ PageType {
if (ConnectionController.isConnected) { if (ConnectionController.isConnected) {
PageController.showNotificationMessage(qsTr("Cannot restore backup settings during active connection")) PageController.showNotificationMessage(qsTr("Cannot restore backup settings during active connection"))
} else { } else {
root.isRestoringBackup = true
PageController.showBusyIndicator(true) PageController.showBusyIndicator(true)
SettingsController.restoreAppConfig(filePath) Qt.callLater(function() {
PageController.showBusyIndicator(false) SettingsController.restoreAppConfig(filePath)
PageController.showBusyIndicator(false)
root.isRestoringBackup = false
})
} }
} }
var noButtonFunction = function() { var noButtonFunction = function() {
@@ -16,6 +16,8 @@ import "../Config"
PageType { PageType {
id: root id: root
property bool isRestoringBackup: false
Connections { Connections {
target: ImportController target: ImportController
@@ -228,6 +230,8 @@ PageType {
rightImageSource: "qrc:/images/controls/chevron-right.svg" rightImageSource: "qrc:/images/controls/chevron-right.svg"
leftImageSource: imageSource leftImageSource: imageSource
enabled: !root.isRestoringBackup
onClicked: { handler() } onClicked: { handler() }
Keys.onEnterPressed: this.clicked() Keys.onEnterPressed: this.clicked()
@@ -314,12 +318,19 @@ PageType {
property string imageSource: "qrc:/images/controls/archive-restore.svg" property string imageSource: "qrc:/images/controls/archive-restore.svg"
property bool isVisible: PageController.isStartPageVisible() property bool isVisible: PageController.isStartPageVisible()
property var handler: function() { property var handler: function() {
if (root.isRestoringBackup) {
return
}
var filePath = SystemController.getFileName(qsTr("Open backup file"), var filePath = SystemController.getFileName(qsTr("Open backup file"),
qsTr("Backup files (*.backup)")) qsTr("Backup files (*.backup)"))
if (filePath !== "") { if (filePath !== "") {
root.isRestoringBackup = true
PageController.showBusyIndicator(true) PageController.showBusyIndicator(true)
SettingsController.restoreAppConfig(filePath) Qt.callLater(function() {
PageController.showBusyIndicator(false) SettingsController.restoreAppConfig(filePath)
PageController.showBusyIndicator(false)
root.isRestoringBackup = false
})
} }
} }
} }