Merge branch 'dev' into feature/local-proxy-integration

This commit is contained in:
aiamnezia
2026-01-16 17:48:48 +04:00
35 changed files with 849 additions and 747 deletions

View File

@@ -10,10 +10,10 @@ env:
jobs:
Build-Linux-Ubuntu:
runs-on: ubuntu-22.04
runs-on: 4-core
env:
QT_VERSION: 6.6.2
QT_VERSION: 6.8.3
QIF_VERSION: 4.7
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
@@ -30,13 +30,15 @@ jobs:
version: ${{ env.QT_VERSION }}
host: 'linux'
target: 'desktop'
arch: 'gcc_64'
arch: 'linux_gcc_64'
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
dir: ${{ runner.temp }}
setup-python: 'true'
tools: 'tools_ifw'
set-env: 'true'
extra: '--external 7z --base ${{ env.QT_MIRROR }}'
aqtversion: '==3.3.0'
py7zrversion: '==0.22.*'
extra: '--base ${{ env.QT_MIRROR }}'
- name: 'Get sources'
uses: actions/checkout@v4
@@ -51,8 +53,8 @@ jobs:
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "Version: $VERSION"
- name: 'Setup ccache'
uses: hendrikmuhs/ccache-action@v1.2
# - name: 'Setup ccache'
# uses: hendrikmuhs/ccache-action@v1.2
- name: 'Build project'
run: |
@@ -91,7 +93,7 @@ jobs:
runs-on: windows-latest
env:
QT_VERSION: 6.6.2
QT_VERSION: 6.10.1
QIF_VERSION: 4.7
BUILD_ARCH: 64
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
@@ -117,8 +119,8 @@ jobs:
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "Version: $VERSION"
- name: 'Setup ccache'
uses: hendrikmuhs/ccache-action@v1.2
# - name: 'Setup ccache'
# uses: hendrikmuhs/ccache-action@v1.2
- name: 'Install Qt'
uses: jurplel/install-qt-action@v3
@@ -126,13 +128,15 @@ jobs:
version: ${{ env.QT_VERSION }}
host: 'windows'
target: 'desktop'
arch: 'win64_msvc2019_64'
arch: 'win64_msvc2022_64'
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
dir: ${{ runner.temp }}
setup-python: 'true'
tools: 'tools_ifw'
set-env: 'true'
extra: '--external 7z --base ${{ env.QT_MIRROR }}'
aqtversion: '==3.3.0'
py7zrversion: '==0.22.*'
extra: '--base ${{ env.QT_MIRROR }}'
- name: 'Setup mvsc'
uses: ilammy/msvc-dev-cmd@v1
@@ -158,7 +162,7 @@ jobs:
shell: cmd
run: |
set BUILD_ARCH=${{ env.BUILD_ARCH }}
set QT_BIN_DIR="${{ runner.temp }}\\Qt\\${{ env.QT_VERSION }}\\msvc2019_64\\bin"
set QT_BIN_DIR="${{ runner.temp }}\\Qt\\${{ env.QT_VERSION }}\\msvc2022_64\\bin"
set QIF_BIN_DIR="${{ runner.temp }}\\Qt\\Tools\\QtInstallerFramework\\${{ env.QIF_VERSION }}\\bin"
set WIX_BIN_DIR=%USERPROFILE%\.dotnet\tools
call deploy\\build_windows.bat
@@ -195,7 +199,7 @@ jobs:
runs-on: macos-latest
env:
QT_VERSION: 6.9.2
QT_VERSION: 6.10.1
CC: cc
CXX: c++
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
@@ -254,8 +258,8 @@ jobs:
submodules: 'true'
fetch-depth: 10
- name: 'Setup ccache'
uses: hendrikmuhs/ccache-action@v1.2
# - name: 'Setup ccache'
# uses: hendrikmuhs/ccache-action@v1.2
- name: 'Install dependencies'
run: pip install jsonschema jinja2
@@ -346,8 +350,8 @@ jobs:
submodules: 'true'
fetch-depth: 10
- name: 'Setup ccache'
uses: hendrikmuhs/ccache-action@v1.2
# - name: 'Setup ccache'
# uses: hendrikmuhs/ccache-action@v1.2
- name: 'Build project'
run: |
@@ -374,7 +378,7 @@ jobs:
runs-on: macos-latest
env:
QT_VERSION: 6.8.3
QT_VERSION: 6.10.1
MAC_TEAM_ID: ${{ secrets.MAC_TEAM_ID }}
@@ -412,15 +416,11 @@ jobs:
arch: 'clang_64'
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
dir: ${{ runner.temp }}
#setup-python: 'true'
#set-env: 'true'
#extra: '--external 7z --base ${{ env.QT_MIRROR }}'
setup-python: 'true'
set-env: 'true'
aqtversion: '==3.3.0'
py7zrversion: '==0.22.*'
extra: '--base ${{ env.QT_MIRROR }}'
cache: 'true'
- name: 'Get sources'
uses: actions/checkout@v4
@@ -435,8 +435,8 @@ jobs:
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "Version: $VERSION"
- name: 'Setup ccache'
uses: hendrikmuhs/ccache-action@v1.2
# - name: 'Setup ccache'
# uses: hendrikmuhs/ccache-action@v1.2
- name: 'Build project'
run: |
@@ -467,7 +467,7 @@ jobs:
runs-on: macos-latest
env:
QT_VERSION: 6.9.2
QT_VERSION: 6.10.1
MAC_TEAM_ID: ${{ secrets.MAC_TEAM_ID }}
@@ -519,8 +519,8 @@ jobs:
submodules: 'true'
fetch-depth: 10
- name: 'Setup ccache'
uses: hendrikmuhs/ccache-action@v1.2
# - name: 'Setup ccache'
# uses: hendrikmuhs/ccache-action@v1.2
- name: 'Build project'
run: |
@@ -537,7 +537,7 @@ jobs:
# ------------------------------------------------------
Build-Android:
runs-on: ubuntu-latest
runs-on: 4-core
env:
ANDROID_BUILD_PLATFORM: android-36
@@ -629,15 +629,15 @@ jobs:
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "Version: $VERSION"
- name: 'Setup ccache'
uses: hendrikmuhs/ccache-action@v1.2
# - name: 'Setup ccache'
# uses: hendrikmuhs/ccache-action@v1.2
- name: 'Setup Java'
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
cache: 'gradle'
# cache: 'gradle'
- name: 'Setup Android NDK'
id: setup-ndk

View File

@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
set(PROJECT AmneziaVPN)
set(AMNEZIAVPN_VERSION 4.8.12.0)
set(AMNEZIAVPN_VERSION 4.8.12.7)
project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
DESCRIPTION "AmneziaVPN"
@@ -12,7 +12,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
set(RELEASE_DATE "${CURRENT_DATE}")
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
set(APP_ANDROID_VERSION_CODE 2101)
set(APP_ANDROID_VERSION_CODE 2103)
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(MZ_PLATFORM_NAME "linux")

View File

@@ -35,6 +35,7 @@ AmneziaApplication::AmneziaApplication(int &argc, char *argv[]) : AMNEZIA_BASE_C
m_optConnect ({QStringLiteral("connect")}, QStringLiteral("Connect to server by index on startup"), QStringLiteral("index")),
m_optImport ({QStringLiteral("import")}, QStringLiteral("Import configuration from data string"), QStringLiteral("data"))
{
setDesktopFileName(QStringLiteral(APPLICATION_NAME));
setQuitOnLastWindowClosed(false);
// Fix config file permissions
@@ -59,11 +60,13 @@ AmneziaApplication::AmneziaApplication(int &argc, char *argv[]) : AMNEZIA_BASE_C
AmneziaApplication::~AmneziaApplication()
{
if (m_vpnConnection) {
QMetaObject::invokeMethod(m_vpnConnection.get(), "disconnectSlots", Qt::QueuedConnection);
QMetaObject::invokeMethod(m_vpnConnection.get(), "disconnectFromVpn", Qt::QueuedConnection);
QThread::msleep(2000);
#ifdef AMNEZIA_DESKTOP
if (m_vpnConnection && m_vpnConnectionThread.isRunning()) {
QMetaObject::invokeMethod(m_vpnConnection.get(), "disconnectSlots", Qt::BlockingQueuedConnection);
QMetaObject::invokeMethod(m_vpnConnection.get(), "disconnectFromVpn", Qt::BlockingQueuedConnection);
}
#endif
m_vpnConnectionThread.requestInterruption();
m_vpnConnectionThread.quit();
@@ -74,7 +77,6 @@ AmneziaApplication::~AmneziaApplication()
}
if (m_engine) {
QObject::disconnect(m_engine, 0, 0, 0);
delete m_engine;
}
}

View File

@@ -127,7 +127,8 @@ QMap<DockerContainer, QString> ContainerProps::containerDescriptions()
QObject::tr("WireGuard - popular VPN protocol with high performance, high speed and low power "
"consumption.") },
{ DockerContainer::Awg,
QObject::tr("AmneziaWG Legacy is a outdated version of AmneziaWG protocol. To upgrade, install AmneziaWG and recreate users.") },
QObject::tr("AmneziaWG is a special protocol from Amnezia based on WireGuard. "
"It provides high connection speed and ensures stable operation even in the most challenging network conditions.") },
{ DockerContainer::Awg2,
QObject::tr("AmneziaWG is a special protocol from Amnezia based on WireGuard. "
"It provides high connection speed and ensures stable operation even in the most challenging network conditions.") },

View File

@@ -68,6 +68,7 @@ namespace apiDefs
constexpr QLatin1String migrationCode("migration_code");
constexpr QLatin1String transactionId("transaction_id");
constexpr QLatin1String isTestPurchase("is_test_purchase");
constexpr QLatin1String userCountryCode("user_country_code");

View File

@@ -1,6 +1,7 @@
#include "apiUtils.h"
#include <QDateTime>
#include <QJsonDocument>
#include <QJsonObject>
namespace
@@ -88,6 +89,7 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
{
const int httpStatusCodeConflict = 409;
const int httpStatusCodeNotFound = 404;
const int httpStatusCodeNotImplemented = 501;
if (!sslErrors.empty()) {
qDebug().noquote() << sslErrors;
@@ -106,10 +108,20 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
qDebug() << replyError;
qDebug() << replyErrorString;
qDebug() << httpStatusCode;
if (httpStatusCode == httpStatusCodeConflict) {
int httpStatusFromBody = -1;
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
if (jsonDoc.isObject()) {
QJsonObject jsonObj = jsonDoc.object();
httpStatusFromBody = jsonObj.value("http_status").toInt(-1);
}
if (httpStatusFromBody == httpStatusCodeConflict) {
return amnezia::ErrorCode::ApiConfigLimitError;
} else if (httpStatusCode == httpStatusCodeNotFound) {
} else if (httpStatusFromBody == httpStatusCodeNotFound) {
return amnezia::ErrorCode::ApiNotFoundError;
} else if (httpStatusFromBody == httpStatusCodeNotImplemented) {
return amnezia::ErrorCode::ApiUpdateRequestError;
}
return amnezia::ErrorCode::ApiConfigDownloadError;
}

View File

@@ -41,6 +41,11 @@ namespace
constexpr QLatin1String errorResponsePattern3("Account not found.");
constexpr QLatin1String updateRequestResponsePattern("client version update is required");
constexpr int httpStatusCodeNotFound = 404;
constexpr int httpStatusCodeConflict = 409;
constexpr int httpStatusCodeNotImplemented = 501;
}
GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
@@ -130,6 +135,26 @@ GatewayController::EncryptedRequestData GatewayController::prepareRequest(const
return encRequestData;
}
GatewayController::DecryptionResult GatewayController::tryDecryptResponseBody(const QByteArray &encryptedResponseBody,
QNetworkReply::NetworkError replyError, const QByteArray &key,
const QByteArray &iv, const QByteArray &salt)
{
DecryptionResult result;
result.decryptedBody = encryptedResponseBody;
result.isDecryptionSuccessful = false;
try {
QSimpleCrypto::QBlockCipher blockCipher;
result.decryptedBody = blockCipher.decryptAesBlockCipher(encryptedResponseBody, key, iv, "", salt);
result.isDecryptionSuccessful = true;
} catch (...) {
result.decryptedBody = encryptedResponseBody;
result.isDecryptionSuccessful = false;
}
return result;
}
ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody)
{
EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload);
@@ -153,21 +178,27 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
reply->deleteLater();
if (sslErrors.isEmpty()
&& shouldBypassProxy(replyError, encryptedResponseBody, true, encRequestData.key, encRequestData.iv, encRequestData.salt)) {
auto decryptionResult =
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
if (sslErrors.isEmpty() && shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
auto requestFunction = [&encRequestData, &encryptedResponseBody](const QString &url) {
encRequestData.request.setUrl(url);
return amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody);
};
auto replyProcessingFunction = [&encryptedResponseBody, &replyErrorString, &replyError, &httpStatusCode, &sslErrors,
&encRequestData, this](QNetworkReply *reply, const QList<QSslError> &nestedSslErrors) {
auto replyProcessingFunction = [&encryptedResponseBody, &replyErrorString, &replyError, &httpStatusCode, &sslErrors, &encRequestData,
&decryptionResult, this](QNetworkReply *reply, const QList<QSslError> &nestedSslErrors) {
encryptedResponseBody = reply->readAll();
replyErrorString = reply->errorString();
replyError = reply->error();
httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
decryptionResult =
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
if (!sslErrors.isEmpty()
|| shouldBypassProxy(replyError, encryptedResponseBody, true, encRequestData.key, encRequestData.iv, encRequestData.salt)) {
|| shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
sslErrors = nestedSslErrors;
return false;
}
@@ -179,21 +210,19 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
bypassProxy(endpoint, serviceType, userCountryCode, requestFunction, replyProcessingFunction);
}
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, encryptedResponseBody);
auto errorCode =
apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, decryptionResult.decryptedBody);
if (errorCode) {
return errorCode;
}
try {
QSimpleCrypto::QBlockCipher blockCipher;
responseBody =
blockCipher.decryptAesBlockCipher(encryptedResponseBody, encRequestData.key, encRequestData.iv, "", encRequestData.salt);
return ErrorCode::NoError;
} catch (...) { // todo change error handling in QSimpleCrypto?
Utils::logException();
if (!decryptionResult.isDecryptionSuccessful) {
qCritical() << "error when decrypting the request body";
return ErrorCode::ApiConfigDecryptionError;
}
responseBody = decryptionResult.decryptedBody;
return ErrorCode::NoError;
}
QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString &endpoint, const QJsonObject apiPayload)
@@ -222,32 +251,33 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
reply->deleteLater();
auto processResponse = [promise, encRequestData](const QByteArray &ecryptedResponseBody, const QList<QSslError> &sslErrors,
QNetworkReply::NetworkError replyError, const QString &replyErrorString,
int httpStatusCode) {
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, ecryptedResponseBody);
auto decryptionResult =
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
auto processResponse = [promise, encRequestData](const GatewayController::DecryptionResult &decryptionResult,
const QList<QSslError> &sslErrors, QNetworkReply::NetworkError replyError,
const QString &replyErrorString, int httpStatusCode) {
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode,
decryptionResult.decryptedBody);
if (errorCode) {
promise->addResult(qMakePair(errorCode, QByteArray()));
promise->finish();
return;
}
QSimpleCrypto::QBlockCipher blockCipher;
try {
QByteArray responseBody = blockCipher.decryptAesBlockCipher(ecryptedResponseBody, encRequestData.key, encRequestData.iv, "",
encRequestData.salt);
promise->addResult(qMakePair(ErrorCode::NoError, responseBody));
promise->finish();
} catch (...) {
if (!decryptionResult.isDecryptionSuccessful) {
Utils::logException();
qCritical() << "error when decrypting the request body";
promise->addResult(qMakePair(ErrorCode::ApiConfigDecryptionError, QByteArray()));
promise->finish();
return;
}
promise->addResult(qMakePair(ErrorCode::NoError, decryptionResult.decryptedBody));
promise->finish();
};
if (sslErrors->isEmpty()
&& shouldBypassProxy(replyError, encryptedResponseBody, true, encRequestData.key, encRequestData.iv, encRequestData.salt)) {
if (sslErrors->isEmpty() && shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString("");
auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString("");
@@ -270,13 +300,21 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
proxyStorageUrls.push_back(baseUrl + "endpoints.json");
getProxyUrlsAsync(proxyStorageUrls, 0, [this, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) {
getProxyUrlAsync(proxyUrls, 0, [this, encRequestData, endpoint, processResponse](const QString &proxyUrls) {
bypassProxyAsync(endpoint, proxyUrls, encRequestData, processResponse);
getProxyUrlAsync(proxyUrls, 0, [this, encRequestData, endpoint, processResponse](const QString &proxyUrl) {
bypassProxyAsync(endpoint, proxyUrl, encRequestData,
[processResponse, this](const QByteArray &decryptedBody, bool isDecryptionSuccessful,
const QList<QSslError> &sslErrors, QNetworkReply::NetworkError replyError,
const QString &replyErrorString, int httpStatusCode) {
GatewayController::DecryptionResult result;
result.decryptedBody = decryptedBody;
result.isDecryptionSuccessful = isDecryptionSuccessful;
processResponse(result, sslErrors, replyError, replyErrorString, httpStatusCode);
});
});
});
} else {
processResponse(encryptedResponseBody, *sslErrors, replyError, replyErrorString, httpStatusCode);
processResponse(decryptionResult, *sslErrors, replyError, replyErrorString, httpStatusCode);
}
});
@@ -369,9 +407,23 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
return {};
}
bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &responseBody,
bool checkEncryption, const QByteArray &key, const QByteArray &iv, const QByteArray &salt)
bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody,
bool isDecryptionSuccessful)
{
const QByteArray &responseBody = decryptedResponseBody;
int httpStatus = -1;
if (isDecryptionSuccessful) {
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
if (jsonDoc.isObject()) {
QJsonObject jsonObj = jsonDoc.object();
httpStatus = jsonObj.value("http_status").toInt(-1);
}
} else {
qDebug() << "failed to decrypt the data";
return true;
}
if (replyError == QNetworkReply::NetworkError::OperationCanceledError || replyError == QNetworkReply::NetworkError::TimeoutError) {
qDebug() << "timeout occurred";
qDebug() << replyError;
@@ -379,7 +431,7 @@ bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &rep
} else if (responseBody.contains("html")) {
qDebug() << "the response contains an html tag";
return true;
} else if (replyError == QNetworkReply::NetworkError::ContentNotFoundError) {
} else if (httpStatus == httpStatusCodeNotFound) {
if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2)
|| responseBody.contains(errorResponsePattern3)) {
return false;
@@ -387,24 +439,18 @@ bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &rep
qDebug() << replyError;
return true;
}
} else if (replyError == QNetworkReply::NetworkError::OperationNotImplementedError) {
} else if (httpStatus == httpStatusCodeNotImplemented) {
if (responseBody.contains(updateRequestResponsePattern)) {
return false;
} else {
qDebug() << replyError;
return true;
}
} else if (httpStatus == httpStatusCodeConflict) {
return false;
} else if (replyError != QNetworkReply::NetworkError::NoError) {
qDebug() << replyError;
return true;
} else if (checkEncryption) {
try {
QSimpleCrypto::QBlockCipher blockCipher;
static_cast<void>(blockCipher.decryptAesBlockCipher(responseBody, key, iv, "", salt));
} catch (...) {
qDebug() << "failed to decrypt the data";
return true;
}
}
return false;
}
@@ -552,7 +598,8 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co
});
}
void GatewayController::getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex, std::function<void(const QString &)> onComplete)
void GatewayController::getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex,
std::function<void(const QString &)> onComplete)
{
if (currentProxyIndex >= proxyUrls.size()) {
onComplete("");
@@ -586,11 +633,11 @@ void GatewayController::getProxyUrlAsync(const QStringList proxyUrls, const int
void GatewayController::bypassProxyAsync(
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
std::function<void(const QByteArray &, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> onComplete)
std::function<void(const QByteArray &, bool, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> onComplete)
{
auto sslErrors = QSharedPointer<QList<QSslError>>::create();
if (proxyUrl.isEmpty()) {
onComplete(QByteArray(), *sslErrors, QNetworkReply::InternalServerError, "empty proxy url", 0);
onComplete(QByteArray(), false, *sslErrors, QNetworkReply::InternalServerError, "empty proxy url", 0);
return;
}
@@ -601,7 +648,7 @@ void GatewayController::bypassProxyAsync(
connect(reply, &QNetworkReply::sslErrors, this, [sslErrors](const QList<QSslError> &errors) { *sslErrors = errors; });
connect(reply, &QNetworkReply::finished, this, [sslErrors, onComplete, reply]() {
connect(reply, &QNetworkReply::finished, this, [sslErrors, onComplete, encRequestData, reply, this]() {
QByteArray encryptedResponseBody = reply->readAll();
QString replyErrorString = reply->errorString();
auto replyError = reply->error();
@@ -609,6 +656,10 @@ void GatewayController::bypassProxyAsync(
reply->deleteLater();
onComplete(encryptedResponseBody, *sslErrors, replyError, replyErrorString, httpStatusCode);
auto decryptionResult =
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
onComplete(decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful, *sslErrors, replyError, replyErrorString,
httpStatusCode);
});
}

View File

@@ -36,11 +36,18 @@ private:
amnezia::ErrorCode errorCode;
};
struct DecryptionResult
{
QByteArray decryptedBody;
bool isDecryptionSuccessful;
};
EncryptedRequestData prepareRequest(const QString &endpoint, const QJsonObject &apiPayload);
DecryptionResult tryDecryptResponseBody(const QByteArray &encryptedResponseBody, QNetworkReply::NetworkError replyError,
const QByteArray &key, const QByteArray &iv, const QByteArray &salt);
QStringList getProxyUrls(const QString &serviceType, const QString &userCountryCode);
bool shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &responseBody, bool checkEncryption,
const QByteArray &key = "", const QByteArray &iv = "", const QByteArray &salt = "");
bool shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody, bool isDecryptionSuccessful);
void bypassProxy(const QString &endpoint, const QString &serviceType, const QString &userCountryCode,
std::function<QNetworkReply *(const QString &url)> requestFunction,
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction);
@@ -50,7 +57,7 @@ private:
void getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex, std::function<void(const QString &)> onComplete);
void bypassProxyAsync(
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
std::function<void(const QByteArray &, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> onComplete);
std::function<void(const QByteArray &, bool, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> onComplete);
int m_requestTimeoutMsecs;
QString m_gatewayEndpoint;

View File

@@ -3,41 +3,22 @@
#include <QRemoteObjectNode>
#include <QtNetwork/qlocalsocket.h>
namespace
{
thread_local IpcClient ipcClient;
}
IpcClient::IpcClient(QObject *parent) : QObject(parent)
{
connect(&m_localSocket, &QLocalSocket::connected, this, [this]() {
m_ClientNode.reset(new QRemoteObjectNode);
m_ClientNode->addClientSideConnection(&m_localSocket);
m_ipcClient.reset(m_ClientNode->acquire<IpcInterfaceReplica>());
m_Tun2SocksClient.reset(m_ClientNode->acquire<IpcProcessTun2SocksReplica>());
m_isSocketConnected = true;
});
connect(&m_localSocket, &QLocalSocket::disconnected, this, [this]() {
m_ClientNode.clear();
m_ipcClient.clear();
m_Tun2SocksClient.clear();
m_isSocketConnected = false;
});
m_node.connectToNode(QUrl("local:" + amnezia::getIpcServiceUrl()));
m_interface.reset(m_node.acquire<IpcInterfaceReplica>());
m_tun2socks.reset(m_node.acquire<IpcProcessTun2SocksReplica>());
}
IpcClient *IpcClient::Instance()
IpcClient& IpcClient::Instance()
{
if (!ipcClient.m_isSocketConnected) {
ipcClient.establishConnection();
}
return &ipcClient;
thread_local IpcClient ipcClient;
return ipcClient;
}
QSharedPointer<IpcInterfaceReplica> IpcClient::Interface()
{
QSharedPointer<IpcInterfaceReplica> rep = Instance()->m_ipcClient;
QSharedPointer<IpcInterfaceReplica> rep = Instance().m_interface;
if (rep.isNull()) {
qCritical() << "IpcClient::Interface(): Failed to acquire replica";
return nullptr;
@@ -54,7 +35,7 @@ QSharedPointer<IpcInterfaceReplica> IpcClient::Interface()
QSharedPointer<IpcProcessTun2SocksReplica> IpcClient::InterfaceTun2Socks()
{
QSharedPointer<IpcProcessTun2SocksReplica> rep = Instance()->m_Tun2SocksClient;
QSharedPointer<IpcProcessTun2SocksReplica> rep = Instance().m_tun2socks;
if (rep.isNull()) {
qCritical() << "IpcClient::InterfaceTun2Socks: Replica is undefined";
return nullptr;
@@ -69,12 +50,6 @@ QSharedPointer<IpcProcessTun2SocksReplica> IpcClient::InterfaceTun2Socks()
return rep;
}
bool IpcClient::establishConnection()
{
m_localSocket.connectToServer(amnezia::getIpcServiceUrl());
return m_localSocket.waitForConnected();
}
QSharedPointer<PrivilegedProcess> IpcClient::CreatePrivilegedProcess()
{
QSharedPointer<IpcInterfaceReplica> rep = Interface();

View File

@@ -15,7 +15,7 @@ class IpcClient : public QObject
public:
explicit IpcClient(QObject *parent = nullptr);
static IpcClient *Instance();
static IpcClient& Instance();
static QSharedPointer<IpcInterfaceReplica> Interface();
static QSharedPointer<IpcProcessTun2SocksReplica> InterfaceTun2Socks();
@@ -24,10 +24,10 @@ public:
template <typename Func>
static auto withInterface(Func func)
{
QSharedPointer<IpcInterfaceReplica> iface = Instance()->m_ipcClient;
QSharedPointer<IpcInterfaceReplica> iface = Instance().m_interface;
using ReturnType = decltype(func(std::declval<QSharedPointer<IpcInterfaceReplica>>()));
if (iface.isNull() || !iface->isReplicaValid()) {
if (iface.isNull() || !iface->waitForSource(1000) || !iface->isReplicaValid()) {
qWarning() << "IpcClient::withInterface(): Service is not running";
if constexpr (std::is_void_v<ReturnType>)
@@ -42,25 +42,19 @@ public:
template <typename OnSuccess, typename OnFailure>
static auto withInterface(OnSuccess onSuccess, OnFailure onFailure)
{
QSharedPointer<IpcInterfaceReplica> iface = Instance()->m_ipcClient;
if (iface.isNull() || !iface->isReplicaValid()) {
QSharedPointer<IpcInterfaceReplica> iface = Instance().m_interface;
if (iface.isNull() || !iface->waitForSource(1000) || !iface->isReplicaValid()) {
return onFailure();
}
return onSuccess(iface);
}
bool isSocketConnected() const;
signals:
private:
bool establishConnection();
QLocalSocket m_localSocket;
QSharedPointer<QRemoteObjectNode> m_ClientNode;
QSharedPointer<IpcInterfaceReplica> m_ipcClient;
QSharedPointer<IpcProcessTun2SocksReplica> m_Tun2SocksClient;
QRemoteObjectNode m_node;
QSharedPointer<IpcInterfaceReplica> m_interface;
QSharedPointer<IpcProcessTun2SocksReplica> m_tun2socks;
struct ProcessDescriptor {
ProcessDescriptor () {
@@ -72,8 +66,6 @@ private:
QSharedPointer<QRemoteObjectNode> replicaNode;
QSharedPointer<QLocalSocket> localSocket;
};
bool m_isSocketConnected {false};
};
#endif // IPCCLIENT_H

View File

@@ -94,15 +94,24 @@ extension PacketTunnelProvider {
}
} catch {
wg_log(.error, message: "Can't parse WG config: \(error.localizedDescription)")
completionHandler(nil)
errorNotifier.notify(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid)
completionHandler(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid)
return
}
}
func handleWireguardStatusMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
guard let completionHandler = completionHandler else { return }
wgAdapter?.getRuntimeConfiguration { settings in
let components = settings!.components(separatedBy: "\n")
guard let wgAdapter = wgAdapter else {
completionHandler(nil)
return
}
wgAdapter.getRuntimeConfiguration { settings in
guard let settings = settings else {
completionHandler(nil)
return
}
let components = settings.components(separatedBy: "\n")
var settingsDictionary: [String: String] = [:]
for component in components {
@@ -131,7 +140,7 @@ extension PacketTunnelProvider {
}
}
private func handleWireguardAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
func handleWireguardAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
guard let completionHandler = completionHandler else { return }
if messageData.count == 1 && messageData[0] == 0 {
wgAdapter?.getRuntimeConfiguration { settings in

View File

@@ -76,7 +76,12 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
return
}
guard hasMeaningfulChange, self.protoType != nil else { return }
guard hasMeaningfulChange, let proto = self.protoType else { return }
// WireGuard/AWG manages network changes internally; avoid restarting the tunnel here.
if proto == .wireguard {
return
}
DispatchQueue.main.async {
self.handle(networkChange: path) { _ in }
@@ -123,6 +128,16 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
}
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
if messageData.count == 1 && messageData[0] == 0 {
guard let completionHandler else { return }
if protoType == .wireguard {
handleWireguardAppMessage(messageData, completionHandler: completionHandler)
} else {
completionHandler(nil)
}
return
}
guard let message = String(data: messageData, encoding: .utf8) else {
if let completionHandler {
completionHandler(nil)
@@ -133,6 +148,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
neLog(.info, title: "App said: ", message: message)
guard let message = try? JSONSerialization.jsonObject(with: messageData, options: []) as? [String: Any] else {
if protoType == .wireguard {
handleWireguardAppMessage(messageData, completionHandler: completionHandler)
return
}
neLog(.error, message: "Failed to serialize message from app")
return
}

View File

@@ -42,46 +42,64 @@ struct WGConfig: Decodable {
}
var settings: String {
guard junkPacketCount != nil else { return "" }
func trimmed(_ value: String?) -> String? {
guard let value = value?.trimmingCharacters(in: .whitespacesAndNewlines),
!value.isEmpty else {
return nil
}
return value
}
guard
let junkPacketCount = trimmed(junkPacketCount),
let junkPacketMinSize = trimmed(junkPacketMinSize),
let junkPacketMaxSize = trimmed(junkPacketMaxSize),
let initPacketJunkSize = trimmed(initPacketJunkSize),
let responsePacketJunkSize = trimmed(responsePacketJunkSize),
let initPacketMagicHeader = trimmed(initPacketMagicHeader),
let responsePacketMagicHeader = trimmed(responsePacketMagicHeader),
let underloadPacketMagicHeader = trimmed(underloadPacketMagicHeader),
let transportPacketMagicHeader = trimmed(transportPacketMagicHeader)
else { return "" }
var settingsLines: [String] = []
// Required parameters when junkPacketCount is present
settingsLines.append("Jc = \(junkPacketCount!)")
settingsLines.append("Jmin = \(junkPacketMinSize!)")
settingsLines.append("Jmax = \(junkPacketMaxSize!)")
settingsLines.append("S1 = \(initPacketJunkSize!)")
settingsLines.append("S2 = \(responsePacketJunkSize!)")
settingsLines.append("H1 = \(initPacketMagicHeader!)")
settingsLines.append("H2 = \(responsePacketMagicHeader!)")
settingsLines.append("H3 = \(underloadPacketMagicHeader!)")
settingsLines.append("H4 = \(transportPacketMagicHeader!)")
settingsLines.append("Jc = \(junkPacketCount)")
settingsLines.append("Jmin = \(junkPacketMinSize)")
settingsLines.append("Jmax = \(junkPacketMaxSize)")
settingsLines.append("S1 = \(initPacketJunkSize)")
settingsLines.append("S2 = \(responsePacketJunkSize)")
settingsLines.append("H1 = \(initPacketMagicHeader)")
settingsLines.append("H2 = \(responsePacketMagicHeader)")
settingsLines.append("H3 = \(underloadPacketMagicHeader)")
settingsLines.append("H4 = \(transportPacketMagicHeader)")
// Optional parameters - only add if not nil and not empty
if let s3 = cookieReplyPacketJunkSize, !s3.isEmpty {
if let s3 = trimmed(cookieReplyPacketJunkSize) {
settingsLines.append("S3 = \(s3)")
}
if let s4 = transportPacketJunkSize, !s4.isEmpty {
if let s4 = trimmed(transportPacketJunkSize) {
settingsLines.append("S4 = \(s4)")
}
if let i1 = specialJunk1, !i1.isEmpty {
if let i1 = trimmed(specialJunk1) {
settingsLines.append("I1 = \(i1)")
}
if let i2 = specialJunk2, !i2.isEmpty {
if let i2 = trimmed(specialJunk2) {
settingsLines.append("I2 = \(i2)")
}
if let i3 = specialJunk3, !i3.isEmpty {
if let i3 = trimmed(specialJunk3) {
settingsLines.append("I3 = \(i3)")
}
if let i4 = specialJunk4, !i4.isEmpty {
if let i4 = trimmed(specialJunk4) {
settingsLines.append("I4 = \(i4)")
}
if let i5 = specialJunk5, !i5.isEmpty {
if let i5 = trimmed(specialJunk5) {
settingsLines.append("I5 = \(i5)")
}
return settingsLines.joined(separator: "\n")
}

View File

@@ -7,6 +7,8 @@
#include <QVariantMap>
#include <QStringList>
#include <QList>
#include <QElapsedTimer>
#include <atomic>
#ifdef __OBJC__
#import <Foundation/Foundation.h>
@@ -77,6 +79,7 @@ public:
const QString &errorString)> &&callback);
void requestInetAccess();
bool isTestFlight();
signals:
void connectionStateChanged(Vpn::ConnectionState state);
void bytesChanged(quint64 receivedBytes, quint64 sentBytes);
@@ -102,6 +105,7 @@ private:
bool startXray(const QString &jsonConfig);
void startTunnel();
void emitConnectionStateIfChanged(Vpn::ConnectionState state);
private:
void *m_iosControllerWrapper {};
@@ -115,8 +119,13 @@ private:
amnezia::Proto m_proto;
QJsonObject m_rawConfig;
QString m_tunnelId;
uint64_t m_txBytes;
uint64_t m_rxBytes;
uint64_t m_txBytes = 0;
uint64_t m_rxBytes = 0;
bool m_handshakeAwaiting = false;
bool m_handshakeConfirmed = false;
QElapsedTimer m_handshakeTimer;
Vpn::ConnectionState m_lastEmittedState = Vpn::ConnectionState::Unknown;
std::atomic_bool m_statusRequestInFlight { false };
};
#endif // IOS_CONTROLLER_H

View File

@@ -93,6 +93,48 @@ Vpn::ConnectionState iosStatusToState(NEVPNStatus status) {
}
}
namespace {
constexpr int kHandshakeTimeoutMs = 12000;
constexpr uint64_t kHandshakeRxThreshold = 4096;
bool isWireGuardBasedProto(amnezia::Proto proto) {
return proto == amnezia::Proto::WireGuard || proto == amnezia::Proto::Awg;
}
uint64_t uint64FromResponse(NSDictionary *response, NSString *key, uint64_t fallback = 0) {
id value = response[key];
if (!value || value == [NSNull null]) {
return fallback;
}
if ([value isKindOfClass:[NSNumber class]]) {
return [(NSNumber *)value unsignedLongLongValue];
}
if ([value isKindOfClass:[NSString class]]) {
const char *str = [(NSString *)value UTF8String];
if (str && *str) {
return strtoull(str, nullptr, 10);
}
}
return fallback;
}
long long int64FromResponse(NSDictionary *response, NSString *key, long long fallback = 0) {
id value = response[key];
if (!value || value == [NSNull null]) {
return fallback;
}
if ([value isKindOfClass:[NSNumber class]]) {
return [(NSNumber *)value longLongValue];
}
if ([value isKindOfClass:[NSString class]]) {
const char *str = [(NSString *)value UTF8String];
if (str && *str) {
return strtoll(str, nullptr, 10);
}
}
return fallback;
}
}
namespace {
IosController* s_instance = nullptr;
}
@@ -114,6 +156,15 @@ IosController::IosController() : QObject()
}
void IosController::emitConnectionStateIfChanged(Vpn::ConnectionState state)
{
if (m_lastEmittedState == state) {
return;
}
m_lastEmittedState = state;
emit connectionStateChanged(state);
}
IosController* IosController::Instance() {
if (!s_instance) {
s_instance = new IosController();
@@ -280,33 +331,65 @@ void IosController::disconnectVpn()
void IosController::checkStatus()
{
if (!m_currentTunnel) {
return;
}
if (m_currentTunnel.connection.status != NEVPNStatusConnected) {
return;
}
if (m_statusRequestInFlight.exchange(true)) {
return;
}
NSString *actionKey = [NSString stringWithUTF8String:MessageKey::action];
NSString *actionValue = [NSString stringWithUTF8String:Action::getStatus];
NSString *tunnelIdKey = [NSString stringWithUTF8String:MessageKey::tunnelId];
NSString *tunnelIdValue = !m_tunnelId.isEmpty() ? m_tunnelId.toNSString() : @"";
NSDictionary* message = @{actionKey: actionValue, tunnelIdKey: tunnelIdValue};
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sendVpnExtensionMessage(message, [&](NSDictionary* response){
uint64_t txBytes = [response[@"tx_bytes"] intValue];
uint64_t rxBytes = [response[@"rx_bytes"] intValue];
uint64_t last_handshake_time_sec = 0;
#if !MACOS_NE
if (response[@"last_handshake_time_sec"] && ![response[@"last_handshake_time_sec"] isKindOfClass:[NSNull class]]) {
last_handshake_time_sec = [response[@"last_handshake_time_sec"] intValue];
} else {
qDebug() << "Key last_handshake_time_sec is missing or null";
if (!response) {
QMetaObject::invokeMethod(this, [this]() {
m_statusRequestInFlight = false;
}, Qt::QueuedConnection);
return;
}
if (last_handshake_time_sec < 0) {
disconnectVpn();
qDebug() << "Invalid handshake time, disconnecting VPN.";
}
#endif
const uint64_t txBytes = uint64FromResponse(response, @"tx_bytes");
const uint64_t rxBytes = uint64FromResponse(response, @"rx_bytes");
const long long last_handshake_time_sec = int64FromResponse(response, @"last_handshake_time_sec");
emit bytesChanged(rxBytes - m_rxBytes, txBytes - m_txBytes);
m_rxBytes = rxBytes;
m_txBytes = txBytes;
QMetaObject::invokeMethod(this, [this, txBytes, rxBytes, last_handshake_time_sec]() {
if (isWireGuardBasedProto(m_proto) && m_handshakeAwaiting) {
const bool hasHandshakeData = (last_handshake_time_sec >= 0);
const bool hasFreshHandshake = hasHandshakeData &&
((last_handshake_time_sec > 0) ||
(rxBytes >= kHandshakeRxThreshold) ||
(txBytes >= kHandshakeRxThreshold));
if (hasFreshHandshake) {
m_handshakeConfirmed = true;
m_handshakeAwaiting = false;
m_handshakeTimer.invalidate();
qDebug() << "IosController::checkStatus : handshake confirmed";
emitConnectionStateIfChanged(Vpn::ConnectionState::Connected);
} else if (m_handshakeTimer.isValid() &&
m_handshakeTimer.elapsed() > kHandshakeTimeoutMs) {
m_handshakeTimer.restart();
qDebug() << "IosController::checkStatus : handshake timed out, keeping tunnel alive";
emitConnectionStateIfChanged(Vpn::ConnectionState::Reconnecting);
}
}
emit bytesChanged(rxBytes - m_rxBytes, txBytes - m_txBytes);
m_rxBytes = rxBytes;
m_txBytes = txBytes;
m_statusRequestInFlight = false;
}, Qt::QueuedConnection);
});
});
}
@@ -413,7 +496,22 @@ void IosController::vpnStatusDidChange(void *pNotification)
}
}
emit connectionStateChanged(iosStatusToState(session.status));
Vpn::ConnectionState nextState = iosStatusToState(session.status);
if (session.status == NEVPNStatusConnected && isWireGuardBasedProto(m_proto)) {
if (!m_handshakeConfirmed) {
nextState = Vpn::ConnectionState::Connecting;
if (!m_handshakeAwaiting) {
m_handshakeAwaiting = true;
m_handshakeTimer.restart();
}
}
} else if (session.status != NEVPNStatusConnected) {
m_handshakeAwaiting = false;
m_handshakeConfirmed = false;
m_handshakeTimer.invalidate();
m_statusRequestInFlight = false;
}
emitConnectionStateIfChanged(nextState);
}
}
@@ -799,6 +897,9 @@ void IosController::sendVpnExtensionMessage(NSDictionary* message, std::function
{
if (!m_currentTunnel) {
qDebug() << "Cannot set an extension callback without a tunnel manager";
if (callback) {
callback(nil);
}
return;
}
@@ -808,6 +909,9 @@ void IosController::sendVpnExtensionMessage(NSDictionary* message, std::function
if (!data || error) {
qDebug() << "Failed to serialize message to VpnExtension as JSON. Error:"
<< [error.localizedDescription UTF8String];
if (callback) {
callback(nil);
}
return;
}
@@ -838,11 +942,18 @@ void IosController::sendVpnExtensionMessage(NSDictionary* message, std::function
[session sendProviderMessage:data returnError:&sendError responseHandler:completionHandler];
} else {
qDebug() << "Method sendProviderMessage:responseHandler:error: does not exist";
if (callback) {
callback(nil);
}
return;
}
if (sendError) {
qDebug() << "Failed to send message to VpnExtension. Error:"
<< [sendError.localizedDescription UTF8String];
if (callback) {
callback(nil);
}
}
}
@@ -1060,3 +1171,8 @@ void IosController::requestInetAccess() {
}];
[task resume];
}
bool IosController::isTestFlight() {
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
return receiptURL && [[receiptURL lastPathComponent] isEqualToString:@"sandboxReceipt"];
}

View File

@@ -1,5 +1,5 @@
sudo docker ps -a | grep amnezia | awk '{print $1}' | xargs sudo docker stop;\
sudo docker ps -a | grep amnezia | awk '{print $1}' | xargs sudo docker rm -fv;\
sudo docker images -a | grep amnezia | awk '{print $3}' | xargs sudo docker rmi;\
sudo docker images -a --format table | grep amnezia | awk '{print $3}' | xargs sudo docker rmi;\
sudo docker network ls | grep amnezia-dns-net | awk '{print $1}' | xargs sudo docker network rm;\
sudo rm -frd /opt/amnezia

View File

@@ -570,14 +570,14 @@ void Settings::setDevGatewayEndpoint()
m_gatewayEndpoint = DEV_AGW_ENDPOINT;
}
QString Settings::getGatewayEndpoint()
QString Settings::getGatewayEndpoint(bool isTestPurchase)
{
return m_gatewayEndpoint;
return isTestPurchase ? DEV_AGW_ENDPOINT : m_gatewayEndpoint;
}
bool Settings::isDevGatewayEnv()
bool Settings::isDevGatewayEnv(bool isTestPurchase)
{
return value("Conf/devGatewayEnv", false).toBool();
return isTestPurchase ? true : value("Conf/devGatewayEnv", false).toBool();
}
void Settings::toggleDevGatewayEnv(bool enabled)

View File

@@ -223,8 +223,8 @@ public:
void resetGatewayEndpoint();
void setGatewayEndpoint(const QString &endpoint);
void setDevGatewayEndpoint();
QString getGatewayEndpoint();
bool isDevGatewayEnv();
QString getGatewayEndpoint(bool isTestPurchase = false);
bool isDevGatewayEnv(bool isTestPurchase = false);
void toggleDevGatewayEnv(bool enabled);
bool isHomeAdLabelVisible();

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,5 @@
#include "apiConfigsController.h"
#include <QClipboard>
#include <QDebug>
#include <QEventLoop>
#include <QSet>
#include "amnezia_application.h"
#include "configurators/wireguard_configurator.h"
#include "core/api/apiDefs.h"
@@ -12,6 +8,10 @@
#include "core/qrCodeUtils.h"
#include "ui/controllers/systemController.h"
#include "version.h"
#include <QClipboard>
#include <QDebug>
#include <QEventLoop>
#include <QSet>
#include "platforms/ios/ios_controller.h"
@@ -53,6 +53,12 @@ namespace
constexpr char isConnectEvent[] = "is_connect_event";
}
namespace serviceType
{
constexpr char amneziaFree[] = "amnezia-free";
constexpr char amneziaPremium[] = "amnezia-premium";
}
struct ProtocolData
{
OpenVpnConfigurator::ConnectionData certRequest;
@@ -176,7 +182,7 @@ namespace
auto clientProtocolConfig =
QJsonDocument::fromJson(serverProtocolConfig.value(config_key::last_config).toString().toUtf8()).object();
//TODO looks like this block can be removed after v1 configs EOL
// TODO looks like this block can be removed after v1 configs EOL
serverProtocolConfig[config_key::junkPacketCount] = clientProtocolConfig.value(config_key::junkPacketCount);
serverProtocolConfig[config_key::junkPacketMinSize] = clientProtocolConfig.value(config_key::junkPacketMinSize);
@@ -235,19 +241,6 @@ namespace
return ErrorCode::NoError;
}
bool isSubscriptionExpired(const QJsonObject &apiConfig)
{
auto subscription = apiConfig.value(configKey::subscription).toObject();
if (subscription.isEmpty()) {
return false;
}
auto subscriptionEndDate = subscription.value(configKey::endDate).toString();
if (apiUtils::isSubscriptionExpired(subscriptionEndDate)) {
return true;
}
return false;
}
}
ApiConfigsController::ApiConfigsController(const QSharedPointer<ServersModel> &serversModel,
@@ -284,11 +277,6 @@ bool ApiConfigsController::exportNativeConfig(const QString &serverCountryCode,
auto serverConfigObject = m_serversModel->getServerConfig(m_serversModel->getProcessedServerIndex());
auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject();
if (isSubscriptionExpired(apiConfigObject)) {
emit errorOccurred(ErrorCode::ApiSubscriptionExpiredError);
return false;
}
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
@@ -304,9 +292,9 @@ bool ApiConfigsController::exportNativeConfig(const QString &serverCountryCode,
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
appendProtocolDataToApiPayload(gatewayRequestData.serviceProtocol, protocolData, apiPayload);
bool isTestPurchase = apiConfigObject.value(apiDefs::key::isTestPurchase).toBool(false);
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/native_config"), apiPayload, responseBody);
ErrorCode errorCode = executeRequest(QString("%1v1/native_config"), apiPayload, responseBody, isTestPurchase);
if (errorCode != ErrorCode::NoError) {
emit errorOccurred(errorCode);
return false;
@@ -325,11 +313,6 @@ bool ApiConfigsController::revokeNativeConfig(const QString &serverCountryCode)
auto serverConfigObject = m_serversModel->getServerConfig(m_serversModel->getProcessedServerIndex());
auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject();
if (isSubscriptionExpired(apiConfigObject)) {
emit errorOccurred(ErrorCode::ApiSubscriptionExpiredError);
return false;
}
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
@@ -341,9 +324,9 @@ bool ApiConfigsController::revokeNativeConfig(const QString &serverCountryCode)
serverConfigObject.value(configKey::authData).toObject() };
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
bool isTestPurchase = apiConfigObject.value(apiDefs::key::isTestPurchase).toBool(false);
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/revoke_native_config"), apiPayload, responseBody);
ErrorCode errorCode = executeRequest(QString("%1v1/revoke_native_config"), apiPayload, responseBody, isTestPurchase);
if (errorCode != ErrorCode::NoError && errorCode != ErrorCode::ApiNotFoundError) {
emit errorOccurred(errorCode);
return false;
@@ -406,48 +389,38 @@ bool ApiConfigsController::fillAvailableServices()
return true;
}
bool ApiConfigsController::importService()
{
#if defined(Q_OS_IOS) || defined(MACOS_NE)
bool isIosOrMacOsNe = true;
#else
bool isIosOrMacOsNe = false;
#endif
if (m_apiServicesModel->getSelectedServiceType() == serviceType::amneziaPremium) {
if (isIosOrMacOsNe) {
importSerivceFromAppStore();
return true;
}
} else {
importServiceFromGateway();
return true;
}
return false;
}
bool ApiConfigsController::importSerivceFromAppStore()
{
#if defined(Q_OS_IOS) || defined(MACOS_NE)
QString chosenProductId;
{
const QStringList productIds = { QStringLiteral("com.amnezia.amneziavpn.1_month"), QStringLiteral("com.amnezia.AmneziaVPN.6_month") };
qInfo().noquote() << "[IAP] Fetching products" << productIds;
QList<QVariantMap> products;
QString fetchError;
QEventLoop waitFetch;
IosController::Instance()->fetchProducts(productIds,
[&](const QList<QVariantMap> &prods, const QStringList &invalid, const QString &err) {
products = prods;
fetchError = err;
qInfo().noquote() << "[IAP] Fetch callback" << "invalid=" << invalid
<< "error=" << err;
waitFetch.quit();
});
waitFetch.exec();
qInfo().noquote() << "[IAP] Product fetch completed; success =" << fetchError.isEmpty()
<< "returned =" << products.size() << "invalid =" << !fetchError.isEmpty();
if (fetchError.isEmpty() && !products.isEmpty()) {
chosenProductId = products.first().value("productId").toString();
}
if (chosenProductId.isEmpty() && !productIds.isEmpty()) {
chosenProductId = productIds.first();
}
qInfo().noquote() << "[IAP] Chosen product =" << chosenProductId;
}
bool purchaseOk = false;
QString originalTransactionId;
QString storeTransactionId;
QString storeProductId;
QString purchaseError;
QEventLoop waitPurchase;
IosController::Instance()->purchaseProduct(chosenProductId,
[&](bool success, const QString &txId, const QString &purchasedProductId,
const QString &originalTxId, const QString &errorString) {
IosController::Instance()->purchaseProduct(QStringLiteral("amnezia_premium_6_month"),
[&](bool success, const QString &txId, const QString &purchasedProductId,
const QString &originalTxId, const QString &errorString) {
purchaseOk = success;
originalTransactionId = originalTxId;
storeTransactionId = txId;
@@ -463,11 +436,11 @@ bool ApiConfigsController::importSerivceFromAppStore()
return false;
}
qInfo().noquote() << "[IAP] Purchase success. transactionId =" << storeTransactionId
<< "originalTransactionId =" << originalTransactionId
<< "productId =" << storeProductId;
<< "originalTransactionId =" << originalTransactionId << "productId =" << storeProductId;
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
m_settings->getInstallationUuid(true),
m_apiServicesModel->getCountryCode(),
"",
@@ -477,25 +450,22 @@ bool ApiConfigsController::importSerivceFromAppStore()
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
apiPayload[apiDefs::key::transactionId] = originalTransactionId;
qInfo().noquote() << "[IAP] Sending subscription request. Payload:"
<< QJsonDocument(apiPayload).toJson(QJsonDocument::Compact);
auto isTestPurchase = IosController::Instance()->isTestFlight();
ErrorCode errorCode;
QByteArray responseBody;
errorCode = executeRequest(QString("%1v1/subscriptions"), apiPayload, responseBody);
errorCode = executeRequest(QString("%1v1/subscriptions"), apiPayload, responseBody, isTestPurchase);
if (errorCode != ErrorCode::NoError) {
emit errorOccurred(errorCode);
return false;
}
ErrorCode installError = ErrorCode::NoError;
if (!installServerFromSubscriptionResponse(responseBody, &installError)) {
const ErrorCode errorToEmit = installError == ErrorCode::NoError ? ErrorCode::ApiPurchaseError : installError;
emit errorOccurred(errorToEmit);
errorCode = importServiceFromBilling(responseBody, isTestPurchase);
if (errorCode != ErrorCode::NoError) {
emit errorOccurred(errorCode);
return false;
}
qInfo().noquote() << "[IAP] Subscription config installed after purchase";
emit installServerFromApiFinished(tr("%1 installed successfully.").arg(m_apiServicesModel->getSelectedServiceName()));
#endif
return true;
@@ -505,22 +475,18 @@ bool ApiConfigsController::restoreSerivceFromAppStore()
{
#if defined(Q_OS_IOS) || defined(MACOS_NE)
const QString premiumServiceType = QStringLiteral("amnezia-premium");
const QString originalServiceType = m_apiServicesModel->rowCount() > 0 ? m_apiServicesModel->getSelectedServiceType() : QString();
if (m_apiServicesModel->rowCount() <= 0) {
qInfo().noquote() << "[IAP] Services model is empty before restore, requesting available services";
if (!fillAvailableServices()) {
qWarning().noquote() << "[IAP] Unable to fetch services list before restore";
emit errorOccurred(ErrorCode::ApiServicesMissingError);
return false;
}
}
if (m_apiServicesModel->rowCount() <= 0) {
qWarning().noquote() << "[IAP] Restore aborted: services list is still empty";
if (!fillAvailableServices()) {
qWarning().noquote() << "[IAP] Unable to fetch services list before restore";
emit errorOccurred(ErrorCode::ApiServicesMissingError);
return false;
}
if (m_apiServicesModel->rowCount() <= 0) {
emit errorOccurred(ErrorCode::ApiServicesMissingError);
return false;
}
// Ensure we have a valid premium selection for gateway requests
bool premiumSelected = false;
for (int i = 0; i < m_apiServicesModel->rowCount(); ++i) {
@@ -530,8 +496,10 @@ bool ApiConfigsController::restoreSerivceFromAppStore()
break;
}
}
if (!premiumSelected) {
m_apiServicesModel->setServiceIndex(0);
emit errorOccurred(ErrorCode::ApiServicesMissingError);
return false;
}
bool restoreSuccess = false;
@@ -539,9 +507,7 @@ bool ApiConfigsController::restoreSerivceFromAppStore()
QString restoreError;
QEventLoop waitRestore;
IosController::Instance()->restorePurchases([&](bool success,
const QList<QVariantMap> &transactions,
const QString &errorString) {
IosController::Instance()->restorePurchases([&](bool success, const QList<QVariantMap> &transactions, const QString &errorString) {
restoreSuccess = success;
restoredTransactions = transactions;
restoreError = errorString;
@@ -571,8 +537,7 @@ bool ApiConfigsController::restoreSerivceFromAppStore()
const QString productId = transaction.value(QStringLiteral("productId")).toString();
if (originalTransactionId.isEmpty()) {
qWarning().noquote() << "[IAP] Skipping restored transaction without originalTransactionId"
<< transactionId;
qWarning().noquote() << "[IAP] Skipping restored transaction without originalTransactionId" << transactionId;
continue;
}
@@ -583,11 +548,11 @@ bool ApiConfigsController::restoreSerivceFromAppStore()
processedTransactions.insert(originalTransactionId);
qInfo().noquote() << "[IAP] Restoring subscription. transactionId =" << transactionId
<< "originalTransactionId =" << originalTransactionId
<< "productId =" << productId;
<< "originalTransactionId =" << originalTransactionId << "productId =" << productId;
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
m_settings->getInstallationUuid(true),
m_apiServicesModel->getCountryCode(),
"",
@@ -597,43 +562,30 @@ bool ApiConfigsController::restoreSerivceFromAppStore()
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
apiPayload[apiDefs::key::transactionId] = originalTransactionId;
auto isTestPurchase = IosController::Instance()->isTestFlight();
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/subscriptions"), apiPayload, responseBody);
ErrorCode errorCode = executeRequest(QString("%1v1/subscriptions"), apiPayload, responseBody, isTestPurchase);
if (errorCode != ErrorCode::NoError) {
qWarning().noquote() << "[IAP] Failed to restore transaction" << originalTransactionId
<< "errorCode =" << static_cast<int>(errorCode);
continue;
}
ErrorCode installError = ErrorCode::NoError;
if (!installServerFromSubscriptionResponse(responseBody, &installError)) {
if (installError == ErrorCode::ApiConfigAlreadyAdded) {
duplicateConfigAlreadyPresent = true;
qInfo().noquote() << "[IAP] Skipping restored transaction" << originalTransactionId
<< "because subscription config with the same vpn_key already exists";
} else {
qWarning().noquote() << "[IAP] Failed to process restored subscription response for transaction"
<< originalTransactionId;
}
continue;
ErrorCode installError = importServiceFromBilling(responseBody, isTestPurchase);
if (errorCode == ErrorCode::ApiConfigAlreadyAdded) {
duplicateConfigAlreadyPresent = true;
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;
} else {
hasInstalledConfig = true;
}
hasInstalledConfig = true;
}
if (!hasInstalledConfig) {
const ErrorCode restoreError = duplicateConfigAlreadyPresent ? ErrorCode::ApiConfigAlreadyAdded : ErrorCode::ApiPurchaseError;
emit errorOccurred(restoreError);
// Restore previous selection so that start page state is unchanged.
if (!originalServiceType.isEmpty()) {
for (int i = 0; i < m_apiServicesModel->rowCount(); ++i) {
m_apiServicesModel->setServiceIndex(i);
if (m_apiServicesModel->getSelectedServiceType() == originalServiceType) {
break;
}
}
}
return false;
}
@@ -642,16 +594,6 @@ bool ApiConfigsController::restoreSerivceFromAppStore()
qInfo().noquote() << "[IAP] Skipped" << duplicateCount
<< "duplicate restored transactions for original transaction IDs already processed";
}
// Restore previous selection if it differs from premium
if (!originalServiceType.isEmpty() && originalServiceType != premiumServiceType) {
for (int i = 0; i < m_apiServicesModel->rowCount(); ++i) {
m_apiServicesModel->setServiceIndex(i);
if (m_apiServicesModel->getSelectedServiceType() == originalServiceType) {
break;
}
}
}
#endif
return true;
}
@@ -714,11 +656,6 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
auto serverConfig = m_serversModel->getServerConfig(serverIndex);
auto apiConfig = serverConfig.value(configKey::apiConfig).toObject();
if (isSubscriptionExpired(apiConfig)) {
emit errorOccurred(ErrorCode::ApiSubscriptionExpiredError);
return false;
}
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
@@ -738,8 +675,9 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
apiPayload.insert(configKey::isConnectEvent, true);
}
bool isTestPurchase = apiConfig.value(apiDefs::key::isTestPurchase).toBool(false);
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody);
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody, isTestPurchase);
QJsonObject newServerConfig;
if (errorCode == ErrorCode::NoError) {
@@ -831,15 +769,6 @@ bool ApiConfigsController::deactivateDevice(const bool isRemoveEvent)
return true;
}
if (isSubscriptionExpired(apiConfigObject)) {
if (isRemoveEvent) {
return true;
} else {
emit errorOccurred(ErrorCode::ApiSubscriptionExpiredError);
return false;
}
}
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
@@ -851,9 +780,10 @@ bool ApiConfigsController::deactivateDevice(const bool isRemoveEvent)
serverConfigObject.value(configKey::authData).toObject() };
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
bool isTestPurchase = apiConfigObject.value(apiDefs::key::isTestPurchase).toBool(false);
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/revoke_config"), apiPayload, responseBody);
ErrorCode errorCode = executeRequest(QString("%1v1/revoke_config"), apiPayload, responseBody, isTestPurchase);
if (errorCode != ErrorCode::NoError && errorCode != ErrorCode::ApiNotFoundError) {
emit errorOccurred(errorCode);
return false;
@@ -875,11 +805,6 @@ bool ApiConfigsController::deactivateExternalDevice(const QString &uuid, const Q
return true;
}
if (isSubscriptionExpired(apiConfigObject)) {
emit errorOccurred(ErrorCode::ApiSubscriptionExpiredError);
return false;
}
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
@@ -891,9 +816,9 @@ bool ApiConfigsController::deactivateExternalDevice(const QString &uuid, const Q
serverConfigObject.value(configKey::authData).toObject() };
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
bool isTestPurchase = apiConfigObject.value(apiDefs::key::isTestPurchase).toBool(false);
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/revoke_config"), apiPayload, responseBody);
ErrorCode errorCode = executeRequest(QString("%1v1/revoke_config"), apiPayload, responseBody, isTestPurchase);
if (errorCode != ErrorCode::NoError && errorCode != ErrorCode::ApiNotFoundError) {
emit errorOccurred(errorCode);
return false;
@@ -972,95 +897,58 @@ QString ApiConfigsController::getVpnKey()
return m_vpnKey;
}
bool ApiConfigsController::installServerFromSubscriptionResponse(const QByteArray &responseBody, ErrorCode *errorOut)
ErrorCode ApiConfigsController::importServiceFromBilling(const QByteArray &responseBody, const bool isTestPurchase)
{
#ifdef Q_OS_IOS
if (errorOut) {
*errorOut = ErrorCode::NoError;
}
QJsonParseError parseError {};
QJsonDocument responseDoc = QJsonDocument::fromJson(responseBody, &parseError);
if (parseError.error == QJsonParseError::NoError) {
qInfo().noquote() << "[IAP] Subscription raw response" << responseDoc.toJson(QJsonDocument::Compact);
} else {
qWarning().noquote() << "[IAP] Subscription raw response parse error:" << parseError.errorString()
<< "raw=" << QString::fromUtf8(responseBody);
}
const QJsonObject responseObject = responseDoc.object();
QJsonObject responseObject = QJsonDocument::fromJson(responseBody).object();
QString key = responseObject.value(QStringLiteral("key")).toString();
if (key.isEmpty()) {
qWarning().noquote() << "[IAP] Subscription response does not contain a key field";
if (errorOut) {
*errorOut = ErrorCode::ApiPurchaseError;
}
return false;
return ErrorCode::ApiPurchaseError;
}
if (m_serversModel->hasServerWithVpnKey(key)) {
qInfo().noquote() << "[IAP] Subscription config with the same vpn_key already exists";
if (errorOut) {
*errorOut = ErrorCode::ApiConfigAlreadyAdded;
}
return false;
return ErrorCode::ApiConfigAlreadyAdded;
}
QString normalizedKey = key;
normalizedKey.replace(QStringLiteral("vpn://"), QString());
QByteArray config = QByteArray::fromBase64(normalizedKey.toUtf8(),
QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
QByteArray configUncompressed = qUncompress(config);
QByteArray configString = QByteArray::fromBase64(normalizedKey.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
QByteArray configUncompressed = qUncompress(configString);
if (!configUncompressed.isEmpty()) {
config = configUncompressed;
configString = configUncompressed;
}
if (config.isEmpty()) {
if (configString.isEmpty()) {
qWarning().noquote() << "[IAP] Subscription response config payload is empty";
if (errorOut) {
*errorOut = ErrorCode::ApiPurchaseError;
}
return false;
return ErrorCode::ApiPurchaseError;
}
QJsonParseError configParseError {};
QJsonDocument configDoc = QJsonDocument::fromJson(config, &configParseError);
if (configParseError.error != QJsonParseError::NoError) {
qWarning().noquote() << "[IAP] Failed to parse subscription config:" << configParseError.errorString();
if (errorOut) {
*errorOut = ErrorCode::ApiPurchaseError;
}
return false;
}
QJsonObject configObject = QJsonDocument::fromJson(configString).object();
QJsonObject configJson = configDoc.object();
quint16 crc = qChecksum(QJsonDocument(configJson).toJson());
auto apiConfig = configJson.value(apiDefs::key::apiConfig).toObject();
quint16 crc = qChecksum(QJsonDocument(configObject).toJson());
auto apiConfig = configObject.value(apiDefs::key::apiConfig).toObject();
apiConfig[apiDefs::key::vpnKey] = normalizedKey;
auto subscriptionObject = apiConfig.value(configKey::subscription).toObject();
qInfo().noquote() << "[IAP] Subscription payload details" << "serviceType="
<< apiConfig.value(configKey::serviceType).toString()
<< "serviceProtocol=" << apiConfig.value(configKey::serviceProtocol).toString()
<< "subscriptionEnd=" << subscriptionObject.value(apiDefs::key::subscriptionEndDate).toString()
<< "subscriptionType=" << subscriptionObject.value(QStringLiteral("type")).toString();
configJson.insert(apiDefs::key::apiConfig, apiConfig);
configJson.insert(config_key::crc, crc);
m_serversModel->addServer(configJson);
apiConfig[apiDefs::key::isTestPurchase] = isTestPurchase;
qDebug() << configJson;
return true;
configObject.insert(apiDefs::key::apiConfig, apiConfig);
configObject.insert(config_key::crc, crc);
m_serversModel->addServer(configObject);
return ErrorCode::NoError;
#else
Q_UNUSED(responseBody)
if (errorOut) {
*errorOut = ErrorCode::ApiPurchaseError;
}
return false;
Q_UNUSED(isTestPurchase)
return ErrorCode::NoError;
#endif
}
ErrorCode ApiConfigsController::executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody)
ErrorCode ApiConfigsController::executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody,
bool isTestPurchase)
{
GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled());
GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase), m_settings->isDevGatewayEnv(isTestPurchase),
apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled());
return gatewayController.post(endpoint, apiPayload, responseBody);
}

View File

@@ -26,6 +26,7 @@ public slots:
void copyVpnKeyToClipboard();
bool fillAvailableServices();
bool importService();
bool importSerivceFromAppStore();
bool restoreSerivceFromAppStore();
bool importServiceFromGateway();
@@ -55,8 +56,8 @@ private:
int getQrCodesCount();
QString getVpnKey();
ErrorCode executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody);
bool installServerFromSubscriptionResponse(const QByteArray &responseBody, ErrorCode *errorOut = nullptr);
ErrorCode executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody, bool isTestPurchase = false);
ErrorCode importServiceFromBilling(const QByteArray &responseBody, const bool isTestPurchase);
QList<QString> m_qrCodes;
QString m_vpnKey;

View File

@@ -5,6 +5,7 @@
#include "core/api/apiUtils.h"
#include "core/controllers/gatewayController.h"
#include "platforms/ios/ios_controller.h"
#include "version.h"
namespace
@@ -49,14 +50,15 @@ bool ApiSettingsController::getAccountInfo(bool reload)
wait.exec(QEventLoop::ExcludeUserInputEvents);
}
GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled());
auto processedIndex = m_serversModel->getProcessedServerIndex();
auto serverConfig = m_serversModel->getServerConfig(processedIndex);
auto apiConfig = serverConfig.value(configKey::apiConfig).toObject();
auto authData = serverConfig.value(configKey::authData).toObject();
bool isTestPurchase = apiConfig.value(apiDefs::key::isTestPurchase).toBool(false);
GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase), m_settings->isDevGatewayEnv(isTestPurchase),
requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled());
QJsonObject apiPayload;
apiPayload[configKey::userCountryCode] = apiConfig.value(configKey::userCountryCode).toString();
apiPayload[configKey::serviceType] = apiConfig.value(configKey::serviceType).toString();

View File

@@ -84,7 +84,7 @@ void InstallController::install(DockerContainer container, int port, TransportPr
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, 32);
int s4 = QRandomGenerator::global()->bounded(0, 20);
// Ensure all values are unique and don't create equal packet sizes
QSet<int> usedValues;
@@ -102,7 +102,7 @@ void InstallController::install(DockerContainer container, int port, TransportPr
usedValues.insert(s3);
while (usedValues.contains(s4)) {
s4 = QRandomGenerator::global()->bounded(0, 32);
s4 = QRandomGenerator::global()->bounded(0, 20);
}
QString initPacketJunkSize = QString::number(s1);

View File

@@ -9,7 +9,5 @@ TextFieldWithHeaderType {
Layout.fillWidth: true
Layout.topMargin: 16
textField.validator: IntValidator { bottom: 0 }
checkEmptyText: true
}

View File

@@ -1,33 +1,30 @@
import QtQuick
import QtQuick.Controls
import Qt.labs.platform
Menu {
property var textObj
popupType: Popup.Native
MenuItem {
text: qsTr("C&ut")
shortcut: StandardKey.Cut
enabled: textObj.selectedText
onTriggered: textObj.cut()
}
MenuItem {
text: qsTr("&Copy")
shortcut: StandardKey.Copy
enabled: textObj.selectedText
onTriggered: textObj.copy()
}
MenuItem {
text: qsTr("&Paste")
shortcut: StandardKey.Paste
// Fix calling paste from clipboard when launching app on android/ios
enabled: (Qt.platform.os === "android" || Qt.platform.os === "ios") ? true : textObj.canPaste
// Fix calling paste from clipboard when launching app on android
enabled: Qt.platform.os === "android" ? true : textObj.canPaste
onTriggered: textObj.paste()
}
MenuItem {
text: qsTr("&SelectAll")
shortcut: StandardKey.SelectAll
enabled: textObj.length > 0
onTriggered: textObj.selectAll()
}

View File

@@ -93,25 +93,13 @@ Rectangle {
wrapMode: Text.Wrap
MouseArea {
id: textAreaMouse
anchors.fill: parent
acceptedButtons: Qt.RightButton
hoverEnabled: true
onClicked: {
fl.interactive = true
contextMenu.open()
}
ContextMenu.menu: ContextMenuType {
textObj: textArea
}
onFocusChanged: {
root.border.color = getBorderColor(borderNormalColor)
}
ContextMenuType {
id: contextMenu
textObj: textArea
}
}
}

View File

@@ -79,25 +79,13 @@ Rectangle {
wrapMode: Text.Wrap
MouseArea {
id: textAreaMouse
anchors.fill: parent
acceptedButtons: Qt.RightButton
hoverEnabled: true
onClicked: {
fl.interactive = true
contextMenu.open()
}
ContextMenu.menu: ContextMenuType {
textObj: textArea
}
onFocusChanged: {
root.border.color = getBorderColor(borderNormalColor)
}
ContextMenuType {
id: contextMenu
textObj: textArea
}
}
RowLayout {

View File

@@ -137,15 +137,7 @@ Item {
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: contextMenu.open()
enabled: true
}
ContextMenuType {
id: contextMenu
ContextMenu.menu: ContextMenuType {
textObj: textField
}

View File

@@ -390,6 +390,7 @@ PageType {
headerText: qsTr("I1 - Special junk 1")
textField.text: serverSpecialJunk1
checkEmptyText: false
textField.onEditingFinished: {
if (textField.text !== serverSpecialJunk1) {
@@ -412,6 +413,7 @@ PageType {
headerText: qsTr("I2 - Special junk 2")
textField.text: serverSpecialJunk2
checkEmptyText: false
textField.onEditingFinished: {
if (textField.text !== serverSpecialJunk2) {
@@ -434,6 +436,7 @@ PageType {
headerText: qsTr("I3 - Special junk 3")
textField.text: serverSpecialJunk3
checkEmptyText: false
textField.onEditingFinished: {
if (textField.text !== serverSpecialJunk3) {
@@ -456,6 +459,7 @@ PageType {
headerText: qsTr("I4 - Special junk 4")
textField.text: serverSpecialJunk4
checkEmptyText: false
textField.onEditingFinished: {
if (textField.text !== serverSpecialJunk4) {
@@ -524,6 +528,7 @@ PageType {
}
clickedFunc: function() {
forceActiveFocus()
if (delegateItem.isEnabled) {
if (AwgConfigModel.isHeadersEqual(underloadPacketMagicHeaderTextField.textField.text,
transportPacketMagicHeaderTextField.textField.text,

View File

@@ -106,22 +106,18 @@ PageType {
Layout.leftMargin: 16
Layout.rightMargin: 16
text: qsTr("Connect")
clickedFunc: function() {
var endpoint = ApiServicesModel.getStoreEndpoint()
if (endpoint !== undefined && endpoint !== "" && Qt.platform.os !== "ios" && !IsMacOsNeBuild) {
Qt.openUrlExternally(endpoint)
PageController.closePage()
PageController.closePage()
} else if (Qt.platform.os === "ios" || IsMacOsNeBuild) {
PageController.showBusyIndicator(true)
ApiConfigsController.importSerivceFromAppStore()
PageController.showBusyIndicator(false)
} else {
PageController.showBusyIndicator(true)
ApiConfigsController.importServiceFromGateway()
PageController.showBusyIndicator(false)
text: qsTr("Connect")
clickedFunc: function() {
PageController.showBusyIndicator(true)
var result = ApiConfigsController.importService()
PageController.showBusyIndicator(false)
if (!result) {
var endpoint = ApiServicesModel.getStoreEndpoint()
Qt.openUrlExternally(endpoint)
PageController.closePage()
PageController.closePage()
}
}
}

View File

@@ -175,7 +175,7 @@ PageType {
id: configContentDrawer
parent: pageShareConnection.parent
anchors.fill: parent
expandedHeight: parent.height * 0.9
expandedHeight: parent ? parent.height * 0.9 : 0
expandedStateContent: Item {
id: configContentContainer
implicitHeight: configContentDrawer.expandedHeight

View File

@@ -109,7 +109,7 @@ PageType {
if (serverSelector.currentIndex !== serverSelectorListView.selectedIndex) {
serverSelector.currentIndex = serverSelectorListView.selectedIndex
serverSelector.severSelectorIndexChanged()
serverSelector.serverSelectorIndexChanged()
}
listView.headerText = qsTr("Accessing ") + serverSelector.text

View File

@@ -132,7 +132,9 @@ void VpnConnection::onConnectionStateChanged(Vpn::ConnectionState state)
#endif
#if defined(Q_OS_IOS) || defined(MACOS_NE)
if (state == Vpn::ConnectionState::Connected) {
if (state == Vpn::ConnectionState::Connected ||
state == Vpn::ConnectionState::Connecting ||
state == Vpn::ConnectionState::Reconnecting) {
m_checkTimer.start();
} else {
m_checkTimer.stop();
@@ -537,6 +539,12 @@ QString VpnConnection::bytesPerSecToText(quint64 bytes)
void VpnConnection::disconnectFromVpn()
{
#if defined(Q_OS_IOS) || defined(MACOS_NE)
// iOS/macOS NE use IosController directly; m_vpnProtocol is not set there.
IosController::Instance()->disconnectVpn();
disconnect(&m_checkTimer, &QTimer::timeout, IosController::Instance(), &IosController::checkStatus);
#endif
if (m_vpnProtocol.isNull()) {
emit connectionStateChanged(Vpn::ConnectionState::Disconnected);
return;
@@ -573,11 +581,6 @@ void VpnConnection::disconnectFromVpn()
m_vpnProtocol->stop();
#endif
#if defined(Q_OS_IOS) || defined(MACOS_NE)
IosController::Instance()->disconnectVpn();
disconnect(&m_checkTimer, &QTimer::timeout, IosController::Instance(), &IosController::checkStatus);
#endif
#if !defined(Q_OS_ANDROID) && !defined(AMNEZIA_DESKTOP)
m_vpnProtocol->deleteLater();
#endif