From 01ec79b7d570a1289f91e4772d8612d981397b43 Mon Sep 17 00:00:00 2001 From: vkamn Date: Tue, 18 Nov 2025 00:21:02 +0800 Subject: [PATCH] fix: news fetch (#1994) * fix: fixed news nested qml call * feat: async proxy bypass --- client/core/controllers/coreController.cpp | 2 +- client/core/controllers/gatewayController.cpp | 225 ++++++++++++++++-- client/core/controllers/gatewayController.h | 14 +- .../ui/controllers/api/apiNewsController.cpp | 13 +- client/ui/controllers/api/apiNewsController.h | 4 +- client/ui/qml/Pages2/PageSettings.qml | 12 +- 6 files changed, 228 insertions(+), 42 deletions(-) diff --git a/client/core/controllers/coreController.cpp b/client/core/controllers/coreController.cpp index 22ab164f..51006696 100644 --- a/client/core/controllers/coreController.cpp +++ b/client/core/controllers/coreController.cpp @@ -324,7 +324,7 @@ void CoreController::initContainerModelUpdateHandler() &ContainersModel::updateModel); connect(m_serversModel.get(), &ServersModel::gatewayStacksExpanded, this, [this]() { if (m_serversModel->hasServersFromGatewayApi()) { - m_apiNewsController->fetchNews(); + m_apiNewsController->fetchNews(false); } }); m_serversModel->resetModel(); diff --git a/client/core/controllers/gatewayController.cpp b/client/core/controllers/gatewayController.cpp index b0be8d31..175e002c 100644 --- a/client/core/controllers/gatewayController.cpp +++ b/client/core/controllers/gatewayController.cpp @@ -1,8 +1,10 @@ #include "gatewayController.h" #include +#include #include +#include #include #include #include @@ -107,7 +109,8 @@ GatewayController::EncryptedRequestData GatewayController::prepareRequest(const encryptedKeyPayload = rsa.encrypt(QJsonDocument(keyPayload).toJson(), publicKey, RSA_PKCS1_PADDING); EVP_PKEY_free(publicKey); - encryptedApiPayload = blockCipher.encryptAesBlockCipher(QJsonDocument(apiPayload).toJson(), encRequestData.key, encRequestData.iv, "", encRequestData.salt); + encryptedApiPayload = blockCipher.encryptAesBlockCipher(QJsonDocument(apiPayload).toJson(), encRequestData.key, encRequestData.iv, + "", encRequestData.salt); } catch (...) { Utils::logException(); qCritical() << "error when encrypting the request body"; @@ -146,19 +149,21 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api reply->deleteLater(); - if (sslErrors.isEmpty() && shouldBypassProxy(replyError, encryptedResponseBody, true, encRequestData.key, encRequestData.iv, encRequestData.salt)) { + if (sslErrors.isEmpty() + && shouldBypassProxy(replyError, encryptedResponseBody, true, encRequestData.key, encRequestData.iv, encRequestData.salt)) { 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 &nestedSslErrors) { + auto replyProcessingFunction = [&encryptedResponseBody, &replyErrorString, &replyError, &httpStatusCode, &sslErrors, + &encRequestData, this](QNetworkReply *reply, const QList &nestedSslErrors) { encryptedResponseBody = reply->readAll(); replyErrorString = reply->errorString(); replyError = reply->error(); httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - if (!sslErrors.isEmpty() || shouldBypassProxy(replyError, encryptedResponseBody, true, encRequestData.key, encRequestData.iv, encRequestData.salt)) { + if (!sslErrors.isEmpty() + || shouldBypassProxy(replyError, encryptedResponseBody, true, encRequestData.key, encRequestData.iv, encRequestData.salt)) { sslErrors = nestedSslErrors; return false; } @@ -177,7 +182,8 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api try { QSimpleCrypto::QBlockCipher blockCipher; - responseBody = blockCipher.decryptAesBlockCipher(encryptedResponseBody, encRequestData.key, encRequestData.iv, "", encRequestData.salt); + responseBody = + blockCipher.decryptAesBlockCipher(encryptedResponseBody, encRequestData.key, encRequestData.iv, "", encRequestData.salt); return ErrorCode::NoError; } catch (...) { // todo change error handling in QSimpleCrypto? Utils::logException(); @@ -202,11 +208,9 @@ QFuture> GatewayController::postAsync(const QString auto sslErrors = QSharedPointer>::create(); - connect(reply, &QNetworkReply::sslErrors, [sslErrors](const QList &errors) { - *sslErrors = errors; - }); + connect(reply, &QNetworkReply::sslErrors, [sslErrors](const QList &errors) { *sslErrors = errors; }); - connect(reply, &QNetworkReply::finished, reply, [=]() { + connect(reply, &QNetworkReply::finished, reply, [promise, sslErrors, encRequestData, endpoint, apiPayload, reply, this]() mutable { QByteArray encryptedResponseBody = reply->readAll(); QString replyErrorString = reply->errorString(); auto replyError = reply->error(); @@ -214,23 +218,61 @@ QFuture> GatewayController::postAsync(const QString reply->deleteLater(); - auto errorCode = apiUtils::checkNetworkReplyErrors(*sslErrors, replyErrorString, replyError, httpStatusCode, encryptedResponseBody); - if (errorCode) { - promise->addResult(qMakePair(errorCode, QByteArray())); - promise->finish(); - return; - } + auto processResponse = [promise, encRequestData](const QByteArray &ecryptedResponseBody, const QList &sslErrors, + QNetworkReply::NetworkError replyError, const QString &replyErrorString, + int httpStatusCode) { + auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, ecryptedResponseBody); + if (errorCode) { + promise->addResult(qMakePair(errorCode, QByteArray())); + promise->finish(); + return; + } - QSimpleCrypto::QBlockCipher blockCipher; - try { - QByteArray responseBody = blockCipher.decryptAesBlockCipher(encryptedResponseBody, encRequestData.key, encRequestData.iv, "", encRequestData.salt); - promise->addResult(qMakePair(ErrorCode::NoError, responseBody)); - promise->finish(); - } catch (...) { - Utils::logException(); - qCritical() << "error when decrypting the request body"; - promise->addResult(qMakePair(ErrorCode::ApiConfigDecryptionError, QByteArray())); - promise->finish(); + QSimpleCrypto::QBlockCipher blockCipher; + try { + QByteArray responseBody = blockCipher.decryptAesBlockCipher(ecryptedResponseBody, encRequestData.key, encRequestData.iv, "", + encRequestData.salt); + promise->addResult(qMakePair(ErrorCode::NoError, responseBody)); + promise->finish(); + } catch (...) { + Utils::logException(); + qCritical() << "error when decrypting the request body"; + promise->addResult(qMakePair(ErrorCode::ApiConfigDecryptionError, QByteArray())); + promise->finish(); + } + }; + + if (sslErrors->isEmpty() + && shouldBypassProxy(replyError, encryptedResponseBody, true, encRequestData.key, encRequestData.iv, encRequestData.salt)) { + auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString(""); + auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString(""); + + QStringList baseUrls; + if (m_isDevEnvironment) { + baseUrls = QString(DEV_S3_ENDPOINT).split(", "); + } else { + baseUrls = QString(PROD_S3_ENDPOINT).split(", "); + } + + QStringList proxyStorageUrls; + if (!serviceType.isEmpty()) { + for (const auto &baseUrl : baseUrls) { + QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8(); + proxyStorageUrls.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) + + ".json"); + } + } + for (const auto &baseUrl : baseUrls) + 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); + }); + }); + + } else { + processResponse(encryptedResponseBody, *sslErrors, replyError, replyErrorString, httpStatusCode); } }); @@ -435,3 +477,134 @@ void GatewayController::bypassProxy(const QString &endpoint, const QString &serv } } } + +void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex, + std::function onComplete) +{ + if (currentProxyStorageIndex >= proxyStorageUrls.size()) { + onComplete({}); + return; + } + + QNetworkRequest request; + request.setTransferTimeout(m_requestTimeoutMsecs); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setUrl(proxyStorageUrls[currentProxyStorageIndex]); + + QNetworkReply *reply = amnApp->networkManager()->get(request); + + // connect(reply, &QNetworkReply::sslErrors, this, [state](const QList &e) { *(state->sslErrors) = e; }); + + connect(reply, &QNetworkReply::finished, this, [this, proxyStorageUrls, currentProxyStorageIndex, onComplete, reply]() { + if (reply->error() == QNetworkReply::NoError) { + QByteArray encrypted = reply->readAll(); + reply->deleteLater(); + + QByteArray responseBody; + try { + QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY; + if (!m_isDevEnvironment) { + QCryptographicHash hash(QCryptographicHash::Sha512); + hash.addData(key); + QByteArray h = hash.result().toHex(); + + QByteArray decKey = QByteArray::fromHex(h.left(64)); + QByteArray iv = QByteArray::fromHex(h.mid(64, 32)); + QByteArray ba = QByteArray::fromBase64(encrypted); + + QSimpleCrypto::QBlockCipher cipher; + responseBody = cipher.decryptAesBlockCipher(ba, decKey, iv); + } else { + responseBody = encrypted; + } + } catch (...) { + Utils::logException(); + qCritical() << "error decrypting payload"; + QMetaObject::invokeMethod( + this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection); + return; + } + + QJsonArray endpointsArray = QJsonDocument::fromJson(responseBody).array(); + QStringList endpoints; + for (const QJsonValue &endpoint : endpointsArray) + endpoints.push_back(endpoint.toString()); + + QStringList shuffled = endpoints; + std::random_device randomDevice; + std::mt19937 generator(randomDevice()); + std::shuffle(shuffled.begin(), shuffled.end(), generator); + + onComplete(shuffled); + return; + } + + int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + qDebug() << httpStatusCode; + qDebug() << "go to the next storage endpoint"; + reply->deleteLater(); + QMetaObject::invokeMethod( + this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection); + }); +} + +void GatewayController::getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex, std::function onComplete) +{ + if (currentProxyIndex >= proxyUrls.size()) { + onComplete(""); + return; + } + + QNetworkRequest request; + request.setTransferTimeout(1000); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setUrl(proxyUrls[currentProxyIndex] + "lmbd-health"); + + QNetworkReply *reply = amnApp->networkManager()->get(request); + + // connect(reply, &QNetworkReply::sslErrors, this, [state](const QList &e) { + // *(state->sslErrors) = e; + // }); + + connect(reply, &QNetworkReply::finished, this, [this, proxyUrls, currentProxyIndex, onComplete, reply]() { + reply->deleteLater(); + + if (reply->error() == QNetworkReply::NoError) { + m_proxyUrl = proxyUrls[currentProxyIndex]; + onComplete(m_proxyUrl); + return; + } + + qDebug() << "go to the next proxy endpoint"; + QMetaObject::invokeMethod(this, [=]() { getProxyUrlAsync(proxyUrls, currentProxyIndex + 1, onComplete); }, Qt::QueuedConnection); + }); +} + +void GatewayController::bypassProxyAsync( + const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData, + std::function &, QNetworkReply::NetworkError, const QString &, int)> onComplete) +{ + auto sslErrors = QSharedPointer>::create(); + if (proxyUrl.isEmpty()) { + onComplete(QByteArray(), *sslErrors, QNetworkReply::InternalServerError, "empty proxy url", 0); + return; + } + + QNetworkRequest request = encRequestData.request; + request.setUrl(endpoint.arg(proxyUrl)); + + QNetworkReply *reply = amnApp->networkManager()->post(request, encRequestData.requestBody); + + connect(reply, &QNetworkReply::sslErrors, this, [sslErrors](const QList &errors) { *sslErrors = errors; }); + + connect(reply, &QNetworkReply::finished, this, [sslErrors, onComplete, reply]() { + QByteArray encryptedResponseBody = reply->readAll(); + QString replyErrorString = reply->errorString(); + auto replyError = reply->error(); + int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + reply->deleteLater(); + + onComplete(encryptedResponseBody, *sslErrors, replyError, replyErrorString, httpStatusCode); + }); +} diff --git a/client/core/controllers/gatewayController.h b/client/core/controllers/gatewayController.h index e13da1ed..f6b05336 100644 --- a/client/core/controllers/gatewayController.h +++ b/client/core/controllers/gatewayController.h @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include "core/defs.h" @@ -24,7 +26,8 @@ public: QFuture> postAsync(const QString &endpoint, const QJsonObject apiPayload); private: - struct EncryptedRequestData { + struct EncryptedRequestData + { QNetworkRequest request; QByteArray requestBody; QByteArray key; @@ -34,7 +37,7 @@ private: }; EncryptedRequestData prepareRequest(const QString &endpoint, const QJsonObject &apiPayload); - + 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 = ""); @@ -42,6 +45,13 @@ private: std::function requestFunction, std::function &sslErrors)> replyProcessingFunction); + void getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex, + std::function onComplete); + void getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex, std::function onComplete); + void bypassProxyAsync( + const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData, + std::function &, QNetworkReply::NetworkError, const QString &, int)> onComplete); + int m_requestTimeoutMsecs; QString m_gatewayEndpoint; bool m_isDevEnvironment = false; diff --git a/client/ui/controllers/api/apiNewsController.cpp b/client/ui/controllers/api/apiNewsController.cpp index a6525c04..9e294f11 100644 --- a/client/ui/controllers/api/apiNewsController.cpp +++ b/client/ui/controllers/api/apiNewsController.cpp @@ -19,7 +19,7 @@ ApiNewsController::ApiNewsController(const QSharedPointer &newsModel, { } -void ApiNewsController::fetchNews() +void ApiNewsController::fetchNews(bool showError) { if (m_serversModel.isNull()) { qWarning() << "ServersModel is null, skip fetchNews"; @@ -30,8 +30,9 @@ void ApiNewsController::fetchNews() qDebug() << "No Gateway stacks, skip fetchNews"; return; } - GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs, - m_settings->isStrictKillSwitchEnabled()); + + auto gatewayController = QSharedPointer::create(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), + apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled()); QJsonObject payload; payload.insert("locale", m_settings->getAppLanguage().name().split("_").first()); @@ -43,11 +44,11 @@ void ApiNewsController::fetchNews() payload.insert(configKey::serviceType, stacksJson.value(configKey::serviceType)); } - auto future = gatewayController.postAsync(QString("%1v1/news"), payload); - future.then(this, [this](QPair result) { + auto future = gatewayController->postAsync(QString("%1v1/news"), payload); + future.then(this, [this, showError, gatewayController](QPair result) { auto [errorCode, responseBody] = result; if (errorCode != ErrorCode::NoError) { - emit errorOccurred(errorCode); + emit errorOccurred(errorCode, showError); return; } diff --git a/client/ui/controllers/api/apiNewsController.h b/client/ui/controllers/api/apiNewsController.h index 17e744ae..43008af7 100644 --- a/client/ui/controllers/api/apiNewsController.h +++ b/client/ui/controllers/api/apiNewsController.h @@ -19,10 +19,10 @@ public: explicit ApiNewsController(const QSharedPointer &newsModel, const std::shared_ptr &settings, const QSharedPointer &serversModel, QObject *parent = nullptr); - Q_INVOKABLE void fetchNews(); + Q_INVOKABLE void fetchNews(bool showError); signals: - void errorOccurred(ErrorCode errorCode); + void errorOccurred(ErrorCode errorCode, bool showError); void fetchNewsFinished(); private: diff --git a/client/ui/qml/Pages2/PageSettings.qml b/client/ui/qml/Pages2/PageSettings.qml index 51e8e4c8..3ebbc798 100644 --- a/client/ui/qml/Pages2/PageSettings.qml +++ b/client/ui/qml/Pages2/PageSettings.qml @@ -20,10 +20,12 @@ PageType { PageController.showBusyIndicator(false) } - function onErrorOccurred(errorCode) { - PageController.showErrorMessage(errorCode) - PageController.closePage() - PageController.showBusyIndicator(false) + function onErrorOccurred(errorCode, showError) { + if (showError) { + PageController.showErrorMessage(errorCode) + PageController.closePage() + PageController.showBusyIndicator(false) + } } } @@ -153,7 +155,7 @@ PageType { return; } PageController.showBusyIndicator(true) - ApiNewsController.fetchNews() + ApiNewsController.fetchNews(true) PageController.goToPage(PageEnum.PageSettingsNewsNotifications) } }