chore: is-test-flight processing (#2093)

* fix: context menu fixes for qt6.9

* chore: is-test-flight porcessing

* chore: bump version and minor build fixes

* refactor: moved test purchase processing on client side

* fix: fixed free import on ios

* chore: bump qt version in deploy.yml

* fix: minor fixes
This commit is contained in:
vkamn
2025-12-29 19:18:03 +08:00
committed by GitHub
parent 6bac948633
commit d78202c612
19 changed files with 272 additions and 341 deletions
+3 -3
View File
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
env: env:
QT_VERSION: 6.6.2 QT_VERSION: 6.9.2
QIF_VERSION: 4.7 QIF_VERSION: 4.7
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 }}
@@ -91,7 +91,7 @@ jobs:
runs-on: windows-latest runs-on: windows-latest
env: env:
QT_VERSION: 6.6.2 QT_VERSION: 6.9.2
QIF_VERSION: 4.7 QIF_VERSION: 4.7
BUILD_ARCH: 64 BUILD_ARCH: 64
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
@@ -374,7 +374,7 @@ jobs:
runs-on: macos-latest runs-on: macos-latest
env: env:
QT_VERSION: 6.8.3 QT_VERSION: 6.9.2
MAC_TEAM_ID: ${{ secrets.MAC_TEAM_ID }} MAC_TEAM_ID: ${{ secrets.MAC_TEAM_ID }}
+1 -1
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.12.0) set(AMNEZIAVPN_VERSION 4.8.12.5)
project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION} project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
DESCRIPTION "AmneziaVPN" DESCRIPTION "AmneziaVPN"
+2
View File
@@ -59,11 +59,13 @@ AmneziaApplication::AmneziaApplication(int &argc, char *argv[]) : AMNEZIA_BASE_C
AmneziaApplication::~AmneziaApplication() AmneziaApplication::~AmneziaApplication()
{ {
#ifdef AMNEZIA_DESKTOP
if (m_vpnConnection) { if (m_vpnConnection) {
QMetaObject::invokeMethod(m_vpnConnection.get(), "disconnectSlots", Qt::QueuedConnection); QMetaObject::invokeMethod(m_vpnConnection.get(), "disconnectSlots", Qt::QueuedConnection);
QMetaObject::invokeMethod(m_vpnConnection.get(), "disconnectFromVpn", Qt::QueuedConnection); QMetaObject::invokeMethod(m_vpnConnection.get(), "disconnectFromVpn", Qt::QueuedConnection);
QThread::msleep(2000); QThread::msleep(2000);
} }
#endif
m_vpnConnectionThread.requestInterruption(); m_vpnConnectionThread.requestInterruption();
m_vpnConnectionThread.quit(); m_vpnConnectionThread.quit();
+1
View File
@@ -68,6 +68,7 @@ namespace apiDefs
constexpr QLatin1String migrationCode("migration_code"); constexpr QLatin1String migrationCode("migration_code");
constexpr QLatin1String transactionId("transaction_id"); constexpr QLatin1String transactionId("transaction_id");
constexpr QLatin1String isTestPurchase("is_test_purchase");
constexpr QLatin1String userCountryCode("user_country_code"); constexpr QLatin1String userCountryCode("user_country_code");
+14 -2
View File
@@ -1,6 +1,7 @@
#include "apiUtils.h" #include "apiUtils.h"
#include <QDateTime> #include <QDateTime>
#include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
namespace namespace
@@ -88,6 +89,7 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
{ {
const int httpStatusCodeConflict = 409; const int httpStatusCodeConflict = 409;
const int httpStatusCodeNotFound = 404; const int httpStatusCodeNotFound = 404;
const int httpStatusCodeNotImplemented = 501;
if (!sslErrors.empty()) { if (!sslErrors.empty()) {
qDebug().noquote() << sslErrors; qDebug().noquote() << sslErrors;
@@ -106,10 +108,20 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
qDebug() << replyError; qDebug() << replyError;
qDebug() << replyErrorString; qDebug() << replyErrorString;
qDebug() << httpStatusCode; 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; return amnezia::ErrorCode::ApiConfigLimitError;
} else if (httpStatusCode == httpStatusCodeNotFound) { } else if (httpStatusFromBody == httpStatusCodeNotFound) {
return amnezia::ErrorCode::ApiNotFoundError; return amnezia::ErrorCode::ApiNotFoundError;
} else if (httpStatusFromBody == httpStatusCodeNotImplemented) {
return amnezia::ErrorCode::ApiUpdateRequestError;
} }
return amnezia::ErrorCode::ApiConfigDownloadError; return amnezia::ErrorCode::ApiConfigDownloadError;
} }
+97 -46
View File
@@ -41,6 +41,11 @@ namespace
constexpr QLatin1String errorResponsePattern3("Account not found."); constexpr QLatin1String errorResponsePattern3("Account not found.");
constexpr QLatin1String updateRequestResponsePattern("client version update is required"); 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, GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
@@ -130,6 +135,26 @@ GatewayController::EncryptedRequestData GatewayController::prepareRequest(const
return encRequestData; 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) ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody)
{ {
EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload); EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload);
@@ -153,21 +178,27 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
reply->deleteLater(); reply->deleteLater();
if (sslErrors.isEmpty() auto decryptionResult =
&& shouldBypassProxy(replyError, encryptedResponseBody, true, encRequestData.key, encRequestData.iv, encRequestData.salt)) { 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) { auto requestFunction = [&encRequestData, &encryptedResponseBody](const QString &url) {
encRequestData.request.setUrl(url); encRequestData.request.setUrl(url);
return amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody); return amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody);
}; };
auto replyProcessingFunction = [&encryptedResponseBody, &replyErrorString, &replyError, &httpStatusCode, &sslErrors, auto replyProcessingFunction = [&encryptedResponseBody, &replyErrorString, &replyError, &httpStatusCode, &sslErrors, &encRequestData,
&encRequestData, this](QNetworkReply *reply, const QList<QSslError> &nestedSslErrors) { &decryptionResult, this](QNetworkReply *reply, const QList<QSslError> &nestedSslErrors) {
encryptedResponseBody = reply->readAll(); encryptedResponseBody = reply->readAll();
replyErrorString = reply->errorString(); replyErrorString = reply->errorString();
replyError = reply->error(); replyError = reply->error();
httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
decryptionResult =
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
if (!sslErrors.isEmpty() if (!sslErrors.isEmpty()
|| shouldBypassProxy(replyError, encryptedResponseBody, true, encRequestData.key, encRequestData.iv, encRequestData.salt)) { || shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
sslErrors = nestedSslErrors; sslErrors = nestedSslErrors;
return false; return false;
} }
@@ -179,21 +210,19 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
bypassProxy(endpoint, serviceType, userCountryCode, requestFunction, replyProcessingFunction); 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) { if (errorCode) {
return errorCode; return errorCode;
} }
try { if (!decryptionResult.isDecryptionSuccessful) {
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();
qCritical() << "error when decrypting the request body"; qCritical() << "error when decrypting the request body";
return ErrorCode::ApiConfigDecryptionError; return ErrorCode::ApiConfigDecryptionError;
} }
responseBody = decryptionResult.decryptedBody;
return ErrorCode::NoError;
} }
QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString &endpoint, const QJsonObject apiPayload) 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(); reply->deleteLater();
auto processResponse = [promise, encRequestData](const QByteArray &ecryptedResponseBody, const QList<QSslError> &sslErrors, auto decryptionResult =
QNetworkReply::NetworkError replyError, const QString &replyErrorString, tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
int httpStatusCode) {
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, ecryptedResponseBody); 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) { if (errorCode) {
promise->addResult(qMakePair(errorCode, QByteArray())); promise->addResult(qMakePair(errorCode, QByteArray()));
promise->finish(); promise->finish();
return; return;
} }
QSimpleCrypto::QBlockCipher blockCipher; if (!decryptionResult.isDecryptionSuccessful) {
try {
QByteArray responseBody = blockCipher.decryptAesBlockCipher(ecryptedResponseBody, encRequestData.key, encRequestData.iv, "",
encRequestData.salt);
promise->addResult(qMakePair(ErrorCode::NoError, responseBody));
promise->finish();
} catch (...) {
Utils::logException(); Utils::logException();
qCritical() << "error when decrypting the request body"; qCritical() << "error when decrypting the request body";
promise->addResult(qMakePair(ErrorCode::ApiConfigDecryptionError, QByteArray())); promise->addResult(qMakePair(ErrorCode::ApiConfigDecryptionError, QByteArray()));
promise->finish(); promise->finish();
return;
} }
promise->addResult(qMakePair(ErrorCode::NoError, decryptionResult.decryptedBody));
promise->finish();
}; };
if (sslErrors->isEmpty() if (sslErrors->isEmpty() && shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
&& shouldBypassProxy(replyError, encryptedResponseBody, true, encRequestData.key, encRequestData.iv, encRequestData.salt)) {
auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString(""); auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString("");
auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).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"); proxyStorageUrls.push_back(baseUrl + "endpoints.json");
getProxyUrlsAsync(proxyStorageUrls, 0, [this, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) { getProxyUrlsAsync(proxyStorageUrls, 0, [this, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) {
getProxyUrlAsync(proxyUrls, 0, [this, encRequestData, endpoint, processResponse](const QString &proxyUrls) { getProxyUrlAsync(proxyUrls, 0, [this, encRequestData, endpoint, processResponse](const QString &proxyUrl) {
bypassProxyAsync(endpoint, proxyUrls, encRequestData, processResponse); 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 { } 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 {}; return {};
} }
bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &responseBody, bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody,
bool checkEncryption, const QByteArray &key, const QByteArray &iv, const QByteArray &salt) 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) { if (replyError == QNetworkReply::NetworkError::OperationCanceledError || replyError == QNetworkReply::NetworkError::TimeoutError) {
qDebug() << "timeout occurred"; qDebug() << "timeout occurred";
qDebug() << replyError; qDebug() << replyError;
@@ -379,7 +431,7 @@ bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &rep
} else if (responseBody.contains("html")) { } else if (responseBody.contains("html")) {
qDebug() << "the response contains an html tag"; qDebug() << "the response contains an html tag";
return true; return true;
} else if (replyError == QNetworkReply::NetworkError::ContentNotFoundError) { } else if (httpStatus == httpStatusCodeNotFound) {
if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2) if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2)
|| responseBody.contains(errorResponsePattern3)) { || responseBody.contains(errorResponsePattern3)) {
return false; return false;
@@ -387,24 +439,18 @@ bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &rep
qDebug() << replyError; qDebug() << replyError;
return true; return true;
} }
} else if (replyError == QNetworkReply::NetworkError::OperationNotImplementedError) { } else if (httpStatus == httpStatusCodeNotImplemented) {
if (responseBody.contains(updateRequestResponsePattern)) { if (responseBody.contains(updateRequestResponsePattern)) {
return false; return false;
} else { } else {
qDebug() << replyError; qDebug() << replyError;
return true; return true;
} }
} else if (httpStatus == httpStatusCodeConflict) {
return false;
} else if (replyError != QNetworkReply::NetworkError::NoError) { } else if (replyError != QNetworkReply::NetworkError::NoError) {
qDebug() << replyError; qDebug() << replyError;
return true; 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; 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()) { if (currentProxyIndex >= proxyUrls.size()) {
onComplete(""); onComplete("");
@@ -586,11 +633,11 @@ void GatewayController::getProxyUrlAsync(const QStringList proxyUrls, const int
void GatewayController::bypassProxyAsync( void GatewayController::bypassProxyAsync(
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData, 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(); auto sslErrors = QSharedPointer<QList<QSslError>>::create();
if (proxyUrl.isEmpty()) { if (proxyUrl.isEmpty()) {
onComplete(QByteArray(), *sslErrors, QNetworkReply::InternalServerError, "empty proxy url", 0); onComplete(QByteArray(), false, *sslErrors, QNetworkReply::InternalServerError, "empty proxy url", 0);
return; return;
} }
@@ -601,7 +648,7 @@ void GatewayController::bypassProxyAsync(
connect(reply, &QNetworkReply::sslErrors, this, [sslErrors](const QList<QSslError> &errors) { *sslErrors = errors; }); 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(); QByteArray encryptedResponseBody = reply->readAll();
QString replyErrorString = reply->errorString(); QString replyErrorString = reply->errorString();
auto replyError = reply->error(); auto replyError = reply->error();
@@ -609,6 +656,10 @@ void GatewayController::bypassProxyAsync(
reply->deleteLater(); 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);
}); });
} }
+10 -3
View File
@@ -36,11 +36,18 @@ private:
amnezia::ErrorCode errorCode; amnezia::ErrorCode errorCode;
}; };
struct DecryptionResult
{
QByteArray decryptedBody;
bool isDecryptionSuccessful;
};
EncryptedRequestData prepareRequest(const QString &endpoint, const QJsonObject &apiPayload); 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); QStringList getProxyUrls(const QString &serviceType, const QString &userCountryCode);
bool shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &responseBody, bool checkEncryption, bool shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody, bool isDecryptionSuccessful);
const QByteArray &key = "", const QByteArray &iv = "", const QByteArray &salt = "");
void bypassProxy(const QString &endpoint, const QString &serviceType, const QString &userCountryCode, void bypassProxy(const QString &endpoint, const QString &serviceType, const QString &userCountryCode,
std::function<QNetworkReply *(const QString &url)> requestFunction, std::function<QNetworkReply *(const QString &url)> requestFunction,
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction); 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 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,
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; int m_requestTimeoutMsecs;
QString m_gatewayEndpoint; QString m_gatewayEndpoint;
+1
View File
@@ -77,6 +77,7 @@ public:
const QString &errorString)> &&callback); const QString &errorString)> &&callback);
void requestInetAccess(); void requestInetAccess();
bool isTestFlight();
signals: signals:
void connectionStateChanged(Vpn::ConnectionState state); void connectionStateChanged(Vpn::ConnectionState state);
void bytesChanged(quint64 receivedBytes, quint64 sentBytes); void bytesChanged(quint64 receivedBytes, quint64 sentBytes);
+5
View File
@@ -1060,3 +1060,8 @@ void IosController::requestInetAccess() {
}]; }];
[task resume]; [task resume];
} }
bool IosController::isTestFlight() {
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
return receiptURL && [[receiptURL lastPathComponent] isEqualToString:@"sandboxReceipt"];
}
+4 -4
View File
@@ -534,14 +534,14 @@ void Settings::setDevGatewayEndpoint()
m_gatewayEndpoint = DEV_AGW_ENDPOINT; 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) void Settings::toggleDevGatewayEnv(bool enabled)
+2 -2
View File
@@ -223,8 +223,8 @@ public:
void resetGatewayEndpoint(); void resetGatewayEndpoint();
void setGatewayEndpoint(const QString &endpoint); void setGatewayEndpoint(const QString &endpoint);
void setDevGatewayEndpoint(); void setDevGatewayEndpoint();
QString getGatewayEndpoint(); QString getGatewayEndpoint(bool isTestPurchase = false);
bool isDevGatewayEnv(); bool isDevGatewayEnv(bool isTestPurchase = false);
void toggleDevGatewayEnv(bool enabled); void toggleDevGatewayEnv(bool enabled);
bool isHomeAdLabelVisible(); bool isHomeAdLabelVisible();
+103 -215
View File
@@ -1,9 +1,5 @@
#include "apiConfigsController.h" #include "apiConfigsController.h"
#include <QClipboard>
#include <QDebug>
#include <QEventLoop>
#include <QSet>
#include "amnezia_application.h" #include "amnezia_application.h"
#include "configurators/wireguard_configurator.h" #include "configurators/wireguard_configurator.h"
#include "core/api/apiDefs.h" #include "core/api/apiDefs.h"
@@ -12,6 +8,10 @@
#include "core/qrCodeUtils.h" #include "core/qrCodeUtils.h"
#include "ui/controllers/systemController.h" #include "ui/controllers/systemController.h"
#include "version.h" #include "version.h"
#include <QClipboard>
#include <QDebug>
#include <QEventLoop>
#include <QSet>
#include "platforms/ios/ios_controller.h" #include "platforms/ios/ios_controller.h"
@@ -53,6 +53,12 @@ namespace
constexpr char isConnectEvent[] = "is_connect_event"; constexpr char isConnectEvent[] = "is_connect_event";
} }
namespace serviceType
{
constexpr char amneziaFree[] = "amnezia-free";
constexpr char amneziaPremium[] = "amnezia-premium";
}
struct ProtocolData struct ProtocolData
{ {
OpenVpnConfigurator::ConnectionData certRequest; OpenVpnConfigurator::ConnectionData certRequest;
@@ -176,7 +182,7 @@ namespace
auto clientProtocolConfig = auto clientProtocolConfig =
QJsonDocument::fromJson(serverProtocolConfig.value(config_key::last_config).toString().toUtf8()).object(); 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::junkPacketCount] = clientProtocolConfig.value(config_key::junkPacketCount);
serverProtocolConfig[config_key::junkPacketMinSize] = clientProtocolConfig.value(config_key::junkPacketMinSize); serverProtocolConfig[config_key::junkPacketMinSize] = clientProtocolConfig.value(config_key::junkPacketMinSize);
@@ -235,19 +241,6 @@ namespace
return ErrorCode::NoError; 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, 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 serverConfigObject = m_serversModel->getServerConfig(m_serversModel->getProcessedServerIndex());
auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject(); auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject();
if (isSubscriptionExpired(apiConfigObject)) {
emit errorOccurred(ErrorCode::ApiSubscriptionExpiredError);
return false;
}
GatewayRequestData gatewayRequestData { QSysInfo::productType(), GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION), QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(), m_settings->getAppLanguage().name().split("_").first(),
@@ -304,9 +292,9 @@ bool ApiConfigsController::exportNativeConfig(const QString &serverCountryCode,
QJsonObject apiPayload = gatewayRequestData.toJsonObject(); QJsonObject apiPayload = gatewayRequestData.toJsonObject();
appendProtocolDataToApiPayload(gatewayRequestData.serviceProtocol, protocolData, apiPayload); appendProtocolDataToApiPayload(gatewayRequestData.serviceProtocol, protocolData, apiPayload);
bool isTestPurchase = apiConfigObject.value(apiDefs::key::isTestPurchase).toBool(false);
QByteArray responseBody; 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) { if (errorCode != ErrorCode::NoError) {
emit errorOccurred(errorCode); emit errorOccurred(errorCode);
return false; return false;
@@ -325,11 +313,6 @@ bool ApiConfigsController::revokeNativeConfig(const QString &serverCountryCode)
auto serverConfigObject = m_serversModel->getServerConfig(m_serversModel->getProcessedServerIndex()); auto serverConfigObject = m_serversModel->getServerConfig(m_serversModel->getProcessedServerIndex());
auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject(); auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject();
if (isSubscriptionExpired(apiConfigObject)) {
emit errorOccurred(ErrorCode::ApiSubscriptionExpiredError);
return false;
}
GatewayRequestData gatewayRequestData { QSysInfo::productType(), GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION), QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(), m_settings->getAppLanguage().name().split("_").first(),
@@ -341,9 +324,9 @@ bool ApiConfigsController::revokeNativeConfig(const QString &serverCountryCode)
serverConfigObject.value(configKey::authData).toObject() }; serverConfigObject.value(configKey::authData).toObject() };
QJsonObject apiPayload = gatewayRequestData.toJsonObject(); QJsonObject apiPayload = gatewayRequestData.toJsonObject();
bool isTestPurchase = apiConfigObject.value(apiDefs::key::isTestPurchase).toBool(false);
QByteArray responseBody; 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) { if (errorCode != ErrorCode::NoError && errorCode != ErrorCode::ApiNotFoundError) {
emit errorOccurred(errorCode); emit errorOccurred(errorCode);
return false; return false;
@@ -406,48 +389,38 @@ bool ApiConfigsController::fillAvailableServices()
return true; 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() bool ApiConfigsController::importSerivceFromAppStore()
{ {
#if defined(Q_OS_IOS) || defined(MACOS_NE) #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; bool purchaseOk = false;
QString originalTransactionId; QString originalTransactionId;
QString storeTransactionId; QString storeTransactionId;
QString storeProductId; QString storeProductId;
QString purchaseError; QString purchaseError;
QEventLoop waitPurchase; QEventLoop waitPurchase;
IosController::Instance()->purchaseProduct(chosenProductId, IosController::Instance()->purchaseProduct(QStringLiteral("amnezia_premium_6_month"),
[&](bool success, const QString &txId, const QString &purchasedProductId, [&](bool success, const QString &txId, const QString &purchasedProductId,
const QString &originalTxId, const QString &errorString) { const QString &originalTxId, const QString &errorString) {
purchaseOk = success; purchaseOk = success;
originalTransactionId = originalTxId; originalTransactionId = originalTxId;
storeTransactionId = txId; storeTransactionId = txId;
@@ -463,11 +436,11 @@ bool ApiConfigsController::importSerivceFromAppStore()
return false; return false;
} }
qInfo().noquote() << "[IAP] Purchase success. transactionId =" << storeTransactionId qInfo().noquote() << "[IAP] Purchase success. transactionId =" << storeTransactionId
<< "originalTransactionId =" << originalTransactionId << "originalTransactionId =" << originalTransactionId << "productId =" << storeProductId;
<< "productId =" << storeProductId;
GatewayRequestData gatewayRequestData { QSysInfo::productType(), GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION), QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
m_settings->getInstallationUuid(true), m_settings->getInstallationUuid(true),
m_apiServicesModel->getCountryCode(), m_apiServicesModel->getCountryCode(),
"", "",
@@ -477,25 +450,22 @@ bool ApiConfigsController::importSerivceFromAppStore()
QJsonObject apiPayload = gatewayRequestData.toJsonObject(); QJsonObject apiPayload = gatewayRequestData.toJsonObject();
apiPayload[apiDefs::key::transactionId] = originalTransactionId; apiPayload[apiDefs::key::transactionId] = originalTransactionId;
qInfo().noquote() << "[IAP] Sending subscription request. Payload:" auto isTestPurchase = IosController::Instance()->isTestFlight();
<< QJsonDocument(apiPayload).toJson(QJsonDocument::Compact);
ErrorCode errorCode; ErrorCode errorCode;
QByteArray responseBody; QByteArray responseBody;
errorCode = executeRequest(QString("%1v1/subscriptions"), apiPayload, responseBody); errorCode = executeRequest(QString("%1v1/subscriptions"), apiPayload, responseBody, isTestPurchase);
if (errorCode != ErrorCode::NoError) { if (errorCode != ErrorCode::NoError) {
emit errorOccurred(errorCode); emit errorOccurred(errorCode);
return false; return false;
} }
ErrorCode installError = ErrorCode::NoError; errorCode = importServiceFromBilling(responseBody, isTestPurchase);
if (!installServerFromSubscriptionResponse(responseBody, &installError)) { if (errorCode != ErrorCode::NoError) {
const ErrorCode errorToEmit = installError == ErrorCode::NoError ? ErrorCode::ApiPurchaseError : installError; emit errorOccurred(errorCode);
emit errorOccurred(errorToEmit);
return false; return false;
} }
qInfo().noquote() << "[IAP] Subscription config installed after purchase";
emit installServerFromApiFinished(tr("%1 installed successfully.").arg(m_apiServicesModel->getSelectedServiceName())); emit installServerFromApiFinished(tr("%1 installed successfully.").arg(m_apiServicesModel->getSelectedServiceName()));
#endif #endif
return true; return true;
@@ -505,22 +475,18 @@ bool ApiConfigsController::restoreSerivceFromAppStore()
{ {
#if defined(Q_OS_IOS) || defined(MACOS_NE) #if defined(Q_OS_IOS) || defined(MACOS_NE)
const QString premiumServiceType = QStringLiteral("amnezia-premium"); const QString premiumServiceType = QStringLiteral("amnezia-premium");
const QString originalServiceType = m_apiServicesModel->rowCount() > 0 ? m_apiServicesModel->getSelectedServiceType() : QString();
if (m_apiServicesModel->rowCount() <= 0) { if (!fillAvailableServices()) {
qInfo().noquote() << "[IAP] Services model is empty before restore, requesting available services"; qWarning().noquote() << "[IAP] Unable to fetch services list before restore";
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";
emit errorOccurred(ErrorCode::ApiServicesMissingError); emit errorOccurred(ErrorCode::ApiServicesMissingError);
return false; return false;
} }
if (m_apiServicesModel->rowCount() <= 0) {
emit errorOccurred(ErrorCode::ApiServicesMissingError);
return false;
}
// Ensure we have a valid premium selection for gateway requests // Ensure we have a valid premium selection for gateway requests
bool premiumSelected = false; bool premiumSelected = false;
for (int i = 0; i < m_apiServicesModel->rowCount(); ++i) { for (int i = 0; i < m_apiServicesModel->rowCount(); ++i) {
@@ -530,8 +496,10 @@ bool ApiConfigsController::restoreSerivceFromAppStore()
break; break;
} }
} }
if (!premiumSelected) { if (!premiumSelected) {
m_apiServicesModel->setServiceIndex(0); emit errorOccurred(ErrorCode::ApiServicesMissingError);
return false;
} }
bool restoreSuccess = false; bool restoreSuccess = false;
@@ -539,9 +507,7 @@ bool ApiConfigsController::restoreSerivceFromAppStore()
QString restoreError; QString restoreError;
QEventLoop waitRestore; QEventLoop waitRestore;
IosController::Instance()->restorePurchases([&](bool success, IosController::Instance()->restorePurchases([&](bool success, const QList<QVariantMap> &transactions, const QString &errorString) {
const QList<QVariantMap> &transactions,
const QString &errorString) {
restoreSuccess = success; restoreSuccess = success;
restoredTransactions = transactions; restoredTransactions = transactions;
restoreError = errorString; restoreError = errorString;
@@ -571,8 +537,7 @@ bool ApiConfigsController::restoreSerivceFromAppStore()
const QString productId = transaction.value(QStringLiteral("productId")).toString(); const QString productId = transaction.value(QStringLiteral("productId")).toString();
if (originalTransactionId.isEmpty()) { if (originalTransactionId.isEmpty()) {
qWarning().noquote() << "[IAP] Skipping restored transaction without originalTransactionId" qWarning().noquote() << "[IAP] Skipping restored transaction without originalTransactionId" << transactionId;
<< transactionId;
continue; continue;
} }
@@ -583,11 +548,11 @@ bool ApiConfigsController::restoreSerivceFromAppStore()
processedTransactions.insert(originalTransactionId); processedTransactions.insert(originalTransactionId);
qInfo().noquote() << "[IAP] Restoring subscription. transactionId =" << transactionId qInfo().noquote() << "[IAP] Restoring subscription. transactionId =" << transactionId
<< "originalTransactionId =" << originalTransactionId << "originalTransactionId =" << originalTransactionId << "productId =" << productId;
<< "productId =" << productId;
GatewayRequestData gatewayRequestData { QSysInfo::productType(), GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION), QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
m_settings->getInstallationUuid(true), m_settings->getInstallationUuid(true),
m_apiServicesModel->getCountryCode(), m_apiServicesModel->getCountryCode(),
"", "",
@@ -597,43 +562,30 @@ bool ApiConfigsController::restoreSerivceFromAppStore()
QJsonObject apiPayload = gatewayRequestData.toJsonObject(); QJsonObject apiPayload = gatewayRequestData.toJsonObject();
apiPayload[apiDefs::key::transactionId] = originalTransactionId; apiPayload[apiDefs::key::transactionId] = originalTransactionId;
auto isTestPurchase = IosController::Instance()->isTestFlight();
QByteArray responseBody; QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/subscriptions"), apiPayload, responseBody); ErrorCode errorCode = executeRequest(QString("%1v1/subscriptions"), apiPayload, responseBody, isTestPurchase);
if (errorCode != ErrorCode::NoError) { if (errorCode != ErrorCode::NoError) {
qWarning().noquote() << "[IAP] Failed to restore transaction" << originalTransactionId qWarning().noquote() << "[IAP] Failed to restore transaction" << originalTransactionId
<< "errorCode =" << static_cast<int>(errorCode); << "errorCode =" << static_cast<int>(errorCode);
continue; continue;
} }
ErrorCode installError = ErrorCode::NoError; ErrorCode installError = importServiceFromBilling(responseBody, isTestPurchase);
if (!installServerFromSubscriptionResponse(responseBody, &installError)) { if (errorCode == ErrorCode::ApiConfigAlreadyAdded) {
if (installError == ErrorCode::ApiConfigAlreadyAdded) { duplicateConfigAlreadyPresent = true;
duplicateConfigAlreadyPresent = true; qInfo().noquote() << "[IAP] Skipping restored transaction" << originalTransactionId
qInfo().noquote() << "[IAP] Skipping restored transaction" << originalTransactionId << "because subscription config with the same vpn_key already exists";
<< "because subscription config with the same vpn_key already exists"; } else if (errorCode != ErrorCode::NoError) {
} else { qWarning().noquote() << "[IAP] Failed to process restored subscription response for transaction" << originalTransactionId;
qWarning().noquote() << "[IAP] Failed to process restored subscription response for transaction" } else {
<< originalTransactionId; hasInstalledConfig = true;
}
continue;
} }
hasInstalledConfig = true;
} }
if (!hasInstalledConfig) { if (!hasInstalledConfig) {
const ErrorCode restoreError = duplicateConfigAlreadyPresent ? ErrorCode::ApiConfigAlreadyAdded : ErrorCode::ApiPurchaseError; const ErrorCode restoreError = duplicateConfigAlreadyPresent ? ErrorCode::ApiConfigAlreadyAdded : ErrorCode::ApiPurchaseError;
emit errorOccurred(restoreError); 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; return false;
} }
@@ -642,16 +594,6 @@ bool ApiConfigsController::restoreSerivceFromAppStore()
qInfo().noquote() << "[IAP] Skipped" << duplicateCount qInfo().noquote() << "[IAP] Skipped" << duplicateCount
<< "duplicate restored transactions for original transaction IDs already processed"; << "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 #endif
return true; return true;
} }
@@ -714,11 +656,6 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
auto serverConfig = m_serversModel->getServerConfig(serverIndex); auto serverConfig = m_serversModel->getServerConfig(serverIndex);
auto apiConfig = serverConfig.value(configKey::apiConfig).toObject(); auto apiConfig = serverConfig.value(configKey::apiConfig).toObject();
if (isSubscriptionExpired(apiConfig)) {
emit errorOccurred(ErrorCode::ApiSubscriptionExpiredError);
return false;
}
GatewayRequestData gatewayRequestData { QSysInfo::productType(), GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION), QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(), m_settings->getAppLanguage().name().split("_").first(),
@@ -738,8 +675,9 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
apiPayload.insert(configKey::isConnectEvent, true); apiPayload.insert(configKey::isConnectEvent, true);
} }
bool isTestPurchase = apiConfig.value(apiDefs::key::isTestPurchase).toBool(false);
QByteArray responseBody; QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody); ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody, isTestPurchase);
QJsonObject newServerConfig; QJsonObject newServerConfig;
if (errorCode == ErrorCode::NoError) { if (errorCode == ErrorCode::NoError) {
@@ -831,15 +769,6 @@ bool ApiConfigsController::deactivateDevice(const bool isRemoveEvent)
return true; return true;
} }
if (isSubscriptionExpired(apiConfigObject)) {
if (isRemoveEvent) {
return true;
} else {
emit errorOccurred(ErrorCode::ApiSubscriptionExpiredError);
return false;
}
}
GatewayRequestData gatewayRequestData { QSysInfo::productType(), GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION), QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(), m_settings->getAppLanguage().name().split("_").first(),
@@ -851,9 +780,10 @@ bool ApiConfigsController::deactivateDevice(const bool isRemoveEvent)
serverConfigObject.value(configKey::authData).toObject() }; serverConfigObject.value(configKey::authData).toObject() };
QJsonObject apiPayload = gatewayRequestData.toJsonObject(); QJsonObject apiPayload = gatewayRequestData.toJsonObject();
bool isTestPurchase = apiConfigObject.value(apiDefs::key::isTestPurchase).toBool(false);
QByteArray responseBody; 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) { if (errorCode != ErrorCode::NoError && errorCode != ErrorCode::ApiNotFoundError) {
emit errorOccurred(errorCode); emit errorOccurred(errorCode);
return false; return false;
@@ -875,11 +805,6 @@ bool ApiConfigsController::deactivateExternalDevice(const QString &uuid, const Q
return true; return true;
} }
if (isSubscriptionExpired(apiConfigObject)) {
emit errorOccurred(ErrorCode::ApiSubscriptionExpiredError);
return false;
}
GatewayRequestData gatewayRequestData { QSysInfo::productType(), GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION), QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(), m_settings->getAppLanguage().name().split("_").first(),
@@ -891,9 +816,9 @@ bool ApiConfigsController::deactivateExternalDevice(const QString &uuid, const Q
serverConfigObject.value(configKey::authData).toObject() }; serverConfigObject.value(configKey::authData).toObject() };
QJsonObject apiPayload = gatewayRequestData.toJsonObject(); QJsonObject apiPayload = gatewayRequestData.toJsonObject();
bool isTestPurchase = apiConfigObject.value(apiDefs::key::isTestPurchase).toBool(false);
QByteArray responseBody; 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) { if (errorCode != ErrorCode::NoError && errorCode != ErrorCode::ApiNotFoundError) {
emit errorOccurred(errorCode); emit errorOccurred(errorCode);
return false; return false;
@@ -972,95 +897,58 @@ QString ApiConfigsController::getVpnKey()
return m_vpnKey; return m_vpnKey;
} }
bool ApiConfigsController::installServerFromSubscriptionResponse(const QByteArray &responseBody, ErrorCode *errorOut) ErrorCode ApiConfigsController::importServiceFromBilling(const QByteArray &responseBody, const bool isTestPurchase)
{ {
#ifdef Q_OS_IOS #ifdef Q_OS_IOS
if (errorOut) { QJsonObject responseObject = QJsonDocument::fromJson(responseBody).object();
*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();
QString key = responseObject.value(QStringLiteral("key")).toString(); QString key = responseObject.value(QStringLiteral("key")).toString();
if (key.isEmpty()) { if (key.isEmpty()) {
qWarning().noquote() << "[IAP] Subscription response does not contain a key field"; qWarning().noquote() << "[IAP] Subscription response does not contain a key field";
if (errorOut) { return ErrorCode::ApiPurchaseError;
*errorOut = ErrorCode::ApiPurchaseError;
}
return false;
} }
if (m_serversModel->hasServerWithVpnKey(key)) { if (m_serversModel->hasServerWithVpnKey(key)) {
qInfo().noquote() << "[IAP] Subscription config with the same vpn_key already exists"; qInfo().noquote() << "[IAP] Subscription config with the same vpn_key already exists";
if (errorOut) { return ErrorCode::ApiConfigAlreadyAdded;
*errorOut = ErrorCode::ApiConfigAlreadyAdded;
}
return false;
} }
QString normalizedKey = key; QString normalizedKey = key;
normalizedKey.replace(QStringLiteral("vpn://"), QString()); normalizedKey.replace(QStringLiteral("vpn://"), QString());
QByteArray config = QByteArray::fromBase64(normalizedKey.toUtf8(), QByteArray configString = QByteArray::fromBase64(normalizedKey.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); QByteArray configUncompressed = qUncompress(configString);
QByteArray configUncompressed = qUncompress(config);
if (!configUncompressed.isEmpty()) { if (!configUncompressed.isEmpty()) {
config = configUncompressed; configString = configUncompressed;
} }
if (config.isEmpty()) {
if (configString.isEmpty()) {
qWarning().noquote() << "[IAP] Subscription response config payload is empty"; qWarning().noquote() << "[IAP] Subscription response config payload is empty";
if (errorOut) { return ErrorCode::ApiPurchaseError;
*errorOut = ErrorCode::ApiPurchaseError;
}
return false;
} }
QJsonParseError configParseError {}; QJsonObject configObject = QJsonDocument::fromJson(configString).object();
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 configJson = configDoc.object(); quint16 crc = qChecksum(QJsonDocument(configObject).toJson());
auto apiConfig = configObject.value(apiDefs::key::apiConfig).toObject();
quint16 crc = qChecksum(QJsonDocument(configJson).toJson());
auto apiConfig = configJson.value(apiDefs::key::apiConfig).toObject();
apiConfig[apiDefs::key::vpnKey] = normalizedKey; apiConfig[apiDefs::key::vpnKey] = normalizedKey;
auto subscriptionObject = apiConfig.value(configKey::subscription).toObject(); apiConfig[apiDefs::key::isTestPurchase] = isTestPurchase;
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);
qDebug() << configJson; configObject.insert(apiDefs::key::apiConfig, apiConfig);
return true; configObject.insert(config_key::crc, crc);
m_serversModel->addServer(configObject);
return ErrorCode::NoError;
#else #else
Q_UNUSED(responseBody) Q_UNUSED(responseBody)
if (errorOut) { Q_UNUSED(isTestPurchase)
*errorOut = ErrorCode::ApiPurchaseError; return ErrorCode::NoError;
}
return false;
#endif #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, GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase), m_settings->isDevGatewayEnv(isTestPurchase),
m_settings->isStrictKillSwitchEnabled()); apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled());
return gatewayController.post(endpoint, apiPayload, responseBody); return gatewayController.post(endpoint, apiPayload, responseBody);
} }
@@ -26,6 +26,7 @@ public slots:
void copyVpnKeyToClipboard(); void copyVpnKeyToClipboard();
bool fillAvailableServices(); bool fillAvailableServices();
bool importService();
bool importSerivceFromAppStore(); bool importSerivceFromAppStore();
bool restoreSerivceFromAppStore(); bool restoreSerivceFromAppStore();
bool importServiceFromGateway(); bool importServiceFromGateway();
@@ -55,8 +56,8 @@ private:
int getQrCodesCount(); int getQrCodesCount();
QString getVpnKey(); QString getVpnKey();
ErrorCode executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody); ErrorCode executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody, bool isTestPurchase = false);
bool installServerFromSubscriptionResponse(const QByteArray &responseBody, ErrorCode *errorOut = nullptr); ErrorCode importServiceFromBilling(const QByteArray &responseBody, const bool isTestPurchase);
QList<QString> m_qrCodes; QList<QString> m_qrCodes;
QString m_vpnKey; QString m_vpnKey;
@@ -5,6 +5,7 @@
#include "core/api/apiUtils.h" #include "core/api/apiUtils.h"
#include "core/controllers/gatewayController.h" #include "core/controllers/gatewayController.h"
#include "platforms/ios/ios_controller.h"
#include "version.h" #include "version.h"
namespace namespace
@@ -49,14 +50,15 @@ bool ApiSettingsController::getAccountInfo(bool reload)
wait.exec(QEventLoop::ExcludeUserInputEvents); wait.exec(QEventLoop::ExcludeUserInputEvents);
} }
GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled());
auto processedIndex = m_serversModel->getProcessedServerIndex(); auto processedIndex = m_serversModel->getProcessedServerIndex();
auto serverConfig = m_serversModel->getServerConfig(processedIndex); auto serverConfig = m_serversModel->getServerConfig(processedIndex);
auto apiConfig = serverConfig.value(configKey::apiConfig).toObject(); auto apiConfig = serverConfig.value(configKey::apiConfig).toObject();
auto authData = serverConfig.value(configKey::authData).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; QJsonObject apiPayload;
apiPayload[configKey::userCountryCode] = apiConfig.value(configKey::userCountryCode).toString(); apiPayload[configKey::userCountryCode] = apiConfig.value(configKey::userCountryCode).toString();
apiPayload[configKey::serviceType] = apiConfig.value(configKey::serviceType).toString(); apiPayload[configKey::serviceType] = apiConfig.value(configKey::serviceType).toString();
+4 -7
View File
@@ -1,33 +1,30 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import Qt.labs.platform
Menu { Menu {
property var textObj property var textObj
popupType: Popup.Native
MenuItem { MenuItem {
text: qsTr("C&ut") text: qsTr("C&ut")
shortcut: StandardKey.Cut
enabled: textObj.selectedText enabled: textObj.selectedText
onTriggered: textObj.cut() onTriggered: textObj.cut()
} }
MenuItem { MenuItem {
text: qsTr("&Copy") text: qsTr("&Copy")
shortcut: StandardKey.Copy
enabled: textObj.selectedText enabled: textObj.selectedText
onTriggered: textObj.copy() onTriggered: textObj.copy()
} }
MenuItem { MenuItem {
text: qsTr("&Paste") text: qsTr("&Paste")
shortcut: StandardKey.Paste // Fix calling paste from clipboard when launching app on android
// Fix calling paste from clipboard when launching app on android/ios enabled: Qt.platform.os === "android" ? true : textObj.canPaste
enabled: (Qt.platform.os === "android" || Qt.platform.os === "ios") ? true : textObj.canPaste
onTriggered: textObj.paste() onTriggered: textObj.paste()
} }
MenuItem { MenuItem {
text: qsTr("&SelectAll") text: qsTr("&SelectAll")
shortcut: StandardKey.SelectAll
enabled: textObj.length > 0 enabled: textObj.length > 0
onTriggered: textObj.selectAll() onTriggered: textObj.selectAll()
} }
+2 -14
View File
@@ -93,25 +93,13 @@ Rectangle {
wrapMode: Text.Wrap wrapMode: Text.Wrap
MouseArea { ContextMenu.menu: ContextMenuType {
id: textAreaMouse textObj: textArea
anchors.fill: parent
acceptedButtons: Qt.RightButton
hoverEnabled: true
onClicked: {
fl.interactive = true
contextMenu.open()
}
} }
onFocusChanged: { onFocusChanged: {
root.border.color = getBorderColor(borderNormalColor) root.border.color = getBorderColor(borderNormalColor)
} }
ContextMenuType {
id: contextMenu
textObj: textArea
}
} }
} }
@@ -79,25 +79,13 @@ Rectangle {
wrapMode: Text.Wrap wrapMode: Text.Wrap
MouseArea { ContextMenu.menu: ContextMenuType {
id: textAreaMouse textObj: textArea
anchors.fill: parent
acceptedButtons: Qt.RightButton
hoverEnabled: true
onClicked: {
fl.interactive = true
contextMenu.open()
}
} }
onFocusChanged: { onFocusChanged: {
root.border.color = getBorderColor(borderNormalColor) root.border.color = getBorderColor(borderNormalColor)
} }
ContextMenuType {
id: contextMenu
textObj: textArea
}
} }
RowLayout { RowLayout {
@@ -137,15 +137,7 @@ Item {
} }
} }
MouseArea { ContextMenu.menu: ContextMenuType {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: contextMenu.open()
enabled: true
}
ContextMenuType {
id: contextMenu
textObj: textField textObj: textField
} }
@@ -106,22 +106,18 @@ PageType {
Layout.leftMargin: 16 Layout.leftMargin: 16
Layout.rightMargin: 16 Layout.rightMargin: 16
text: qsTr("Connect") text: qsTr("Connect")
clickedFunc: function() { clickedFunc: function() {
var endpoint = ApiServicesModel.getStoreEndpoint() PageController.showBusyIndicator(true)
if (endpoint !== undefined && endpoint !== "" && Qt.platform.os !== "ios" && !IsMacOsNeBuild) { var result = ApiConfigsController.importService()
Qt.openUrlExternally(endpoint) PageController.showBusyIndicator(false)
PageController.closePage()
PageController.closePage() if (!result) {
} else if (Qt.platform.os === "ios" || IsMacOsNeBuild) { var endpoint = ApiServicesModel.getStoreEndpoint()
PageController.showBusyIndicator(true) Qt.openUrlExternally(endpoint)
ApiConfigsController.importSerivceFromAppStore() PageController.closePage()
PageController.showBusyIndicator(false) PageController.closePage()
} else {
PageController.showBusyIndicator(true)
ApiConfigsController.importServiceFromGateway()
PageController.showBusyIndicator(false)
} }
} }
} }