fixed crash app & up vercion & fix qml captha

This commit is contained in:
dranik
2026-05-05 16:45:26 +03:00
parent 55572eddd1
commit d3c1f0a6f8
12 changed files with 159 additions and 27 deletions

View File

@@ -4,7 +4,7 @@ set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(PROJECT AmneziaVPN)
set(AMNEZIAVPN_VERSION 4.8.15.4)
set(AMNEZIAVPN_VERSION 4.9.1.1)
set(QT_CREATOR_SKIP_PACKAGE_MANAGER_SETUP ON CACHE BOOL "" FORCE)
set(CMAKE_PROJECT_TOP_LEVEL_INCLUDES

View File

@@ -5,6 +5,7 @@
#include <QEventLoop>
#include <QFutureWatcher>
#include <QJsonDocument>
#include <QJsonObject>
#include <QPromise>
#include <QSet>
#include <QSysInfo>
@@ -209,11 +210,19 @@ void SubscriptionController::updateApiConfigInJson(QJsonObject &serverConfigJson
serverConfigJson[apiDefs::key::apiConfig] = apiConfig;
}
ErrorCode SubscriptionController::executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody, bool isTestPurchase)
ErrorCode SubscriptionController::executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody,
bool isTestPurchase, QString *outEffectiveRequestBase,
const QString &reuseRequestBase)
{
GatewayController gatewayController(m_appSettingsRepository->getGatewayEndpoint(isTestPurchase), m_appSettingsRepository->isDevGatewayEnv(isTestPurchase), apiDefs::requestTimeoutMsecs,
m_appSettingsRepository->isStrictKillSwitchEnabled());
return gatewayController.post(endpoint, apiPayload, responseBody);
GatewayController gatewayController(m_appSettingsRepository->getGatewayEndpoint(isTestPurchase),
m_appSettingsRepository->isDevGatewayEnv(isTestPurchase), apiDefs::requestTimeoutMsecs,
m_appSettingsRepository->isStrictKillSwitchEnabled(), nullptr, reuseRequestBase);
return gatewayController.post(endpoint, apiPayload, responseBody, outEffectiveRequestBase);
}
void SubscriptionController::clearGatewayCaptchaSticky()
{
m_gatewayCaptchaStickyBase.clear();
}
ErrorCode SubscriptionController::importServiceFromGateway(const QString &userCountryCode, const QString &serviceType,
@@ -235,9 +244,12 @@ ErrorCode SubscriptionController::importServiceFromGateway(const QString &userCo
appendProtocolDataToApiPayload(serviceProtocol, protocolData, apiPayload);
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody);
QString effectiveRequestBase;
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody, false, &effectiveRequestBase,
m_gatewayCaptchaStickyBase);
if (errorCode == ErrorCode::ApiCaptchaRequiredError) {
m_gatewayCaptchaStickyBase = effectiveRequestBase;
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
if (jsonDoc.isObject()) {
QJsonObject jsonObj = jsonDoc.object();
@@ -249,6 +261,8 @@ ErrorCode SubscriptionController::importServiceFromGateway(const QString &userCo
return errorCode;
}
m_gatewayCaptchaStickyBase.clear();
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
@@ -1110,7 +1124,8 @@ ErrorCode SubscriptionController::resolveImportServiceCaptcha(const QString &use
const ProtocolData &protocolData,
const QString &captchaId,
const QString &captchaSolution,
ServerConfig &serverConfig) {
ServerConfig &serverConfig,
CaptchaInfo *retryCaptchaOut) {
GatewayRequestData gatewayRequestData{QSysInfo::productType(),
QString(APP_VERSION),
m_appSettingsRepository->getAppLanguage().name().split("_").first(),
@@ -1125,14 +1140,42 @@ ErrorCode SubscriptionController::resolveImportServiceCaptcha(const QString &use
appendProtocolDataToApiPayload(serviceProtocol, protocolData, apiPayload);
apiPayload["captcha_id"] = captchaId;
apiPayload["captcha_solution"] = captchaSolution;
QString normalizedSolution;
normalizedSolution.reserve(captchaSolution.size());
for (const QChar &ch : captchaSolution) {
const ushort u = ch.unicode();
if (u >= '0' && u <= '9') {
normalizedSolution += ch;
} else if (u >= 0xFF10 && u <= 0xFF19) {
normalizedSolution += QChar(static_cast<char16_t>(u - 0xFF10 + '0'));
}
}
apiPayload["captcha_solution"] = normalizedSolution.isEmpty() ? captchaSolution.trimmed() : normalizedSolution;
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody);
QString effectiveRequestBase;
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody, false, &effectiveRequestBase,
m_gatewayCaptchaStickyBase);
if (errorCode != ErrorCode::NoError) {
m_gatewayCaptchaStickyBase = effectiveRequestBase;
if (retryCaptchaOut
&& (errorCode == ErrorCode::ApiCaptchaInvalidError || errorCode == ErrorCode::ApiCaptchaRefreshError)) {
const QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
if (jsonDoc.isObject()) {
const QJsonObject jsonObj = jsonDoc.object();
if (jsonObj.contains(QStringLiteral("captcha_id")) && jsonObj.contains(QStringLiteral("captcha_image"))) {
retryCaptchaOut->captchaId = jsonObj.value(QStringLiteral("captcha_id")).toString();
retryCaptchaOut->captchaImageBase64 = jsonObj.value(QStringLiteral("captcha_image")).toString();
retryCaptchaOut->hint = jsonObj.value(QStringLiteral("hint")).toString();
retryCaptchaOut->isRequired = true;
}
}
}
return errorCode;
}
m_gatewayCaptchaStickyBase.clear();
QJsonObject serverConfigJson;
errorCode = extractServerConfigJsonFromResponse(responseBody, serviceProtocol, protocolData, serverConfigJson);
if (errorCode != ErrorCode::NoError) {

View File

@@ -115,10 +115,14 @@ public:
ErrorCode resolveImportServiceCaptcha(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol, const ProtocolData &protocolData,
const QString &captchaId, const QString &captchaSolution,
ServerConfig &serverConfig);
ServerConfig &serverConfig, CaptchaInfo *retryCaptchaOut = nullptr);
void clearGatewayCaptchaSticky();
private:
ErrorCode executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody, bool isTestPurchase = false);
ErrorCode executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody,
bool isTestPurchase = false, QString *outEffectiveRequestBase = nullptr,
const QString &reuseRequestBase = QString());
bool isApiKeyExpired(int serverIndex) const;
ErrorCode extractServerConfigJsonFromResponse(const QByteArray &apiResponseBody, const QString &protocol,
@@ -129,6 +133,8 @@ private:
SecureServersRepository* m_serversRepository;
SecureAppSettingsRepository* m_appSettingsRepository;
QString m_gatewayCaptchaStickyBase;
};
#endif // SUBSCRIPTIONCONTROLLER_H

View File

@@ -56,13 +56,23 @@ namespace
}
GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
const bool isStrictKillSwitchEnabled, QObject *parent)
const bool isStrictKillSwitchEnabled, QObject *parent, const QString &reuseAgwRequestBase)
: QObject(parent),
m_gatewayEndpoint(gatewayEndpoint),
m_isDevEnvironment(isDevEnvironment),
m_requestTimeoutMsecs(requestTimeoutMsecs),
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled)
{
if (!reuseAgwRequestBase.isEmpty()) {
m_proxyUrl = reuseAgwRequestBase;
}
}
void GatewayController::writeEffectiveRequestBase(QString *outEffectiveRequestBase) const
{
if (outEffectiveRequestBase) {
*outEffectiveRequestBase = m_proxyUrl.isEmpty() ? m_gatewayEndpoint : m_proxyUrl;
}
}
GatewayController::EncryptedRequestData GatewayController::prepareRequest(const QString &endpoint, const QJsonObject &apiPayload)
@@ -172,10 +182,12 @@ GatewayController::DecryptionResult GatewayController::tryDecryptResponseBody(co
return result;
}
ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody)
ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody,
QString *outEffectiveRequestBase)
{
EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload);
if (encRequestData.errorCode != ErrorCode::NoError) {
writeEffectiveRequestBase(outEffectiveRequestBase);
return encRequestData.errorCode;
}
@@ -206,7 +218,8 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
#endif
if (!plaintextMock && sslErrors.isEmpty() && shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
if (!plaintextMock && sslErrors.isEmpty()
&& shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful, httpStatusCode)) {
auto requestFunction = [&encRequestData, &encryptedResponseBody](const QString &url) {
encRequestData.request.setUrl(url);
return amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody);
@@ -223,7 +236,7 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
if (!sslErrors.isEmpty()
|| shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
|| shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful, httpStatusCode)) {
sslErrors = nestedSslErrors;
return false;
}
@@ -239,14 +252,17 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
const auto errorCode =
apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, responseBody);
if (errorCode) {
writeEffectiveRequestBase(outEffectiveRequestBase);
return errorCode;
}
if (!decryptionResult.isDecryptionSuccessful) {
qCritical() << "error when decrypting the request body";
writeEffectiveRequestBase(outEffectiveRequestBase);
return ErrorCode::ApiConfigDecryptionError;
}
writeEffectiveRequestBase(outEffectiveRequestBase);
return ErrorCode::NoError;
}
@@ -312,7 +328,8 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
promise->finish();
};
if (!plaintextMock && sslErrors->isEmpty() && shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
if (!plaintextMock && sslErrors->isEmpty()
&& shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful, httpStatusCode)) {
auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString("");
auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString("");
@@ -471,7 +488,7 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
}
bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody,
bool isDecryptionSuccessful)
bool isDecryptionSuccessful, int httpStatusCode)
{
const QByteArray &responseBody = decryptedResponseBody;
@@ -497,6 +514,10 @@ bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &rep
return false;
}
}
// Reverse proxy or unknown route returns plaintext (e.g. "404 page not found") — not a proxy/CDN issue.
if (httpStatusCode == httpStatusCodeNotFound || replyError == QNetworkReply::ContentNotFoundError) {
return false;
}
qDebug() << "failed to decrypt the data";
return true;
}
@@ -562,6 +583,10 @@ void GatewayController::bypassProxy(const QString &endpoint, const QString &serv
qDebug() << "go to the next proxy endpoint";
QNetworkReply *reply = requestFunction(endpoint.arg(proxyUrl));
if (!reply) {
qWarning() << "GatewayController::bypassProxy: requestFunction returned null";
return false;
}
QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
@@ -584,6 +609,10 @@ void GatewayController::bypassProxy(const QString &endpoint, const QString &serv
for (const QString &proxyUrl : proxyUrls) {
request.setUrl(proxyUrl + "lmbd-health");
reply = amnApp->networkManager()->get(request);
if (!reply) {
qWarning() << "GatewayController::bypassProxy: health check get() returned null";
continue;
}
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });

View File

@@ -22,9 +22,11 @@ class GatewayController : public QObject
public:
explicit GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
const bool isStrictKillSwitchEnabled, QObject *parent = nullptr);
const bool isStrictKillSwitchEnabled, QObject *parent = nullptr,
const QString &reuseAgwRequestBase = QString());
amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody);
amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody,
QString *outEffectiveRequestBase = nullptr);
QFuture<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject apiPayload);
private:
@@ -49,7 +51,8 @@ private:
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 &decryptedResponseBody, bool isDecryptionSuccessful);
bool shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody,
bool isDecryptionSuccessful, int httpStatusCode);
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);
@@ -61,12 +64,14 @@ private:
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
std::function<void(const QByteArray &, bool, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> onComplete);
void writeEffectiveRequestBase(QString *outEffectiveRequestBase) const;
int m_requestTimeoutMsecs;
QString m_gatewayEndpoint;
bool m_isDevEnvironment = false;
bool m_isStrictKillSwitchEnabled = false;
inline static QString m_proxyUrl;
QString m_proxyUrl;
};
#endif // GATEWAYCONTROLLER_H

View File

@@ -37,6 +37,8 @@ UpdateController::UpdateController(SecureAppSettingsRepository* appSettingsRepos
{
}
UpdateController::~UpdateController() = default;
QString UpdateController::getRawChangelogText() const
{
return m_changelogText;
@@ -97,6 +99,7 @@ void UpdateController::fetchGatewayUrl()
m_appSettingsRepository->isDevGatewayEnv(),
7000,
m_appSettingsRepository->isStrictKillSwitchEnabled());
m_activeGatewayController = gatewayController;
QJsonObject apiPayload;
apiPayload[apiDefs::key::cliVersion] = QString(APP_VERSION);
@@ -107,6 +110,7 @@ void UpdateController::fetchGatewayUrl()
QTimer::singleShot(1000, this, [this, gatewayController, apiPayload]() {
gatewayController->postAsync(QStringLiteral("%1v1/updater_endpoint"), apiPayload)
.then(this, [this](QPair<ErrorCode, QByteArray> result) {
m_activeGatewayController.clear();
auto [err, gatewayResponse] = result;
if (err != ErrorCode::NoError) {
logger.error() << errorString(err);

View File

@@ -7,11 +7,16 @@
#include "core/repositories/secureAppSettingsRepository.h"
#include <QSharedPointer>
class GatewayController;
class UpdateController : public QObject
{
Q_OBJECT
public:
explicit UpdateController(SecureAppSettingsRepository* appSettingsRepository, QObject *parent = nullptr);
~UpdateController() override;
QString getRawChangelogText() const;
QString getReleaseDate() const;
@@ -38,6 +43,8 @@ private:
SecureAppSettingsRepository* m_appSettingsRepository;
QSharedPointer<GatewayController> m_activeGatewayController;
QString m_baseUrl;
QString m_changelogText;
QString m_version;

View File

@@ -170,6 +170,13 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
qDebug() << replyError;
qDebug() << httpStatusCode;
if (httpStatusCode == httpStatusCodeNotFound) {
const QJsonDocument probe = QJsonDocument::fromJson(responseBody);
if (!probe.isObject()) {
return amnezia::ErrorCode::ApiNotFoundError;
}
}
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
if (jsonDoc.isObject()) {
QJsonObject jsonObj = jsonDoc.object();

View File

@@ -255,6 +255,10 @@ bool SubscriptionUiController::restoreServiceFromAppStore()
bool SubscriptionUiController::importFreeFromGateway()
{
if (!isCaptchaAwaitingUser()) {
m_subscriptionController->clearGatewayCaptchaSticky();
}
QString userCountryCode = m_apiServicesModel->getCountryCode();
QString serviceType = m_apiServicesModel->getSelectedServiceType();
QString serviceProtocol = m_apiServicesModel->getSelectedServiceProtocol();
@@ -307,6 +311,7 @@ void SubscriptionUiController::onCaptchaSolved(const QString &captchaId, const Q
protocolData.xrayUuid = m_captchaState.xrayUuid;
ServerConfig serverConfig;
SubscriptionController::CaptchaInfo retryCaptcha;
ErrorCode errorCode = m_subscriptionController->resolveImportServiceCaptcha(
m_captchaState.userCountryCode,
m_captchaState.serviceType,
@@ -314,16 +319,26 @@ void SubscriptionUiController::onCaptchaSolved(const QString &captchaId, const Q
protocolData,
captchaId,
solution,
serverConfig);
m_captchaState.isPending = false;
serverConfig,
&retryCaptcha);
if (errorCode == ErrorCode::NoError) {
m_captchaState.isPending = false;
emit captchaFlowDismissRequested();
m_serversController->addServer(serverConfig);
emit installServerFromApiFinished(tr("%1 installed successfully.").arg(m_apiServicesModel->getSelectedServiceName()));
} else {
emit errorOccurred(errorCode);
return;
}
if ((errorCode == ErrorCode::ApiCaptchaInvalidError || errorCode == ErrorCode::ApiCaptchaRefreshError)
&& retryCaptcha.isRequired) {
emit captchaRequired(retryCaptcha.captchaId, retryCaptcha.captchaImageBase64,
retryCaptcha.hint.isEmpty() ? tr("Enter the digits from the image to continue") : retryCaptcha.hint);
return;
}
m_captchaState.isPending = false;
emit errorOccurred(errorCode);
}
void SubscriptionUiController::onRefreshCaptchaRequested()

View File

@@ -84,6 +84,7 @@ signals:
void vpnKeyExportReady();
void captchaRequired(const QString &captchaId, const QString &captchaImageBase64, const QString &hint);
void captchaFlowDismissRequested();
private:
struct CaptchaState {

View File

@@ -40,6 +40,18 @@ Popup {
solutionField.textField.focus = true
}
onCaptchaIdChanged: {
if (opened) {
solutionField.textField.text = ""
}
}
onCaptchaImageBase64Changed: {
if (opened) {
solutionField.textField.text = ""
}
}
onClosed: {
FocusController.dropRootObject(root)
}

View File

@@ -214,7 +214,6 @@ Window {
id: captchaDialog
onCaptchaSolved: function(captchaId, solution) {
captchaDialog.close()
SubscriptionUiController.onCaptchaSolved(captchaId, solution)
}
@@ -336,6 +335,10 @@ Window {
captchaDialog.hint = hint
captchaDialog.open()
}
function onCaptchaFlowDismissRequested() {
captchaDialog.close()
}
}
Connections {