From f1481b1b1fd0b2bcc2854a76014924a17af5f87c Mon Sep 17 00:00:00 2001 From: vkamn Date: Wed, 29 Oct 2025 23:24:24 +0800 Subject: [PATCH] feat: add async post in gateway controller (#1963) --- client/core/controllers/gatewayController.cpp | 121 ++++++++++++++---- client/core/controllers/gatewayController.h | 14 ++ .../ui/controllers/api/apiNewsController.cpp | 37 +++--- client/ui/controllers/api/apiNewsController.h | 1 + client/ui/qml/Pages2/PageSettings.qml | 16 ++- 5 files changed, 143 insertions(+), 46 deletions(-) diff --git a/client/core/controllers/gatewayController.cpp b/client/core/controllers/gatewayController.cpp index f626189e1..bba0ef9d9 100644 --- a/client/core/controllers/gatewayController.cpp +++ b/client/core/controllers/gatewayController.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include "QBlockCipher.h" @@ -50,24 +51,25 @@ GatewayController::GatewayController(const QString &gatewayEndpoint, const bool { } -ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody) +GatewayController::EncryptedRequestData GatewayController::prepareRequest(const QString &endpoint, const QJsonObject &apiPayload) { + EncryptedRequestData encRequestData; + encRequestData.errorCode = ErrorCode::NoError; + #ifdef Q_OS_IOS IosController::Instance()->requestInetAccess(); QThread::msleep(10); #endif - QNetworkRequest request; - request.setTransferTimeout(m_requestTimeoutMsecs); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - request.setRawHeader(QString("X-Client-Request-ID").toUtf8(), QUuid::createUuid().toString(QUuid::WithoutBraces).toUtf8()); - - request.setUrl(endpoint.arg(m_proxyUrl.isEmpty() ? m_gatewayEndpoint : m_proxyUrl)); + encRequestData.request.setTransferTimeout(m_requestTimeoutMsecs); + encRequestData.request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + encRequestData.request.setRawHeader(QString("X-Client-Request-ID").toUtf8(), QUuid::createUuid().toString(QUuid::WithoutBraces).toUtf8()); + encRequestData.request.setUrl(endpoint.arg(m_proxyUrl.isEmpty() ? m_gatewayEndpoint : m_proxyUrl)); // bypass killSwitch exceptions for API-gateway #ifdef AMNEZIA_DESKTOP if (m_isStrictKillSwitchEnabled) { - QString host = QUrl(request.url()).host(); + QString host = QUrl(encRequestData.request.url()).host(); QString ip = NetworkUtilities::getIPAddress(host); if (!ip.isEmpty()) { IpcClient::Interface()->addKillSwitchAllowedRange(QStringList { ip }); @@ -76,14 +78,14 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api #endif QSimpleCrypto::QBlockCipher blockCipher; - QByteArray key = blockCipher.generatePrivateSalt(32); - QByteArray iv = blockCipher.generatePrivateSalt(32); - QByteArray salt = blockCipher.generatePrivateSalt(8); + encRequestData.key = blockCipher.generatePrivateSalt(32); + encRequestData.iv = blockCipher.generatePrivateSalt(32); + encRequestData.salt = blockCipher.generatePrivateSalt(8); QJsonObject keyPayload; - keyPayload[configKey::aesKey] = QString(key.toBase64()); - keyPayload[configKey::aesIv] = QString(iv.toBase64()); - keyPayload[configKey::aesSalt] = QString(salt.toBase64()); + keyPayload[configKey::aesKey] = QString(encRequestData.key.toBase64()); + keyPayload[configKey::aesIv] = QString(encRequestData.iv.toBase64()); + keyPayload[configKey::aesSalt] = QString(encRequestData.salt.toBase64()); QByteArray encryptedKeyPayload; QByteArray encryptedApiPayload; @@ -98,24 +100,37 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api } catch (...) { Utils::logException(); qCritical() << "error loading public key from environment variables"; - return ErrorCode::ApiMissingAgwPublicKey; + encRequestData.errorCode = ErrorCode::ApiMissingAgwPublicKey; + return encRequestData; } encryptedKeyPayload = rsa.encrypt(QJsonDocument(keyPayload).toJson(), publicKey, RSA_PKCS1_PADDING); EVP_PKEY_free(publicKey); - encryptedApiPayload = blockCipher.encryptAesBlockCipher(QJsonDocument(apiPayload).toJson(), key, iv, "", salt); - } catch (...) { // todo change error handling in QSimpleCrypto? + encryptedApiPayload = blockCipher.encryptAesBlockCipher(QJsonDocument(apiPayload).toJson(), encRequestData.key, encRequestData.iv, "", encRequestData.salt); + } catch (...) { Utils::logException(); qCritical() << "error when encrypting the request body"; - return ErrorCode::ApiConfigDecryptionError; + encRequestData.errorCode = ErrorCode::ApiConfigDecryptionError; + return encRequestData; } QJsonObject requestBody; requestBody[configKey::keyPayload] = QString(encryptedKeyPayload.toBase64()); requestBody[configKey::apiPayload] = QString(encryptedApiPayload.toBase64()); - QNetworkReply *reply = amnApp->networkManager()->post(request, QJsonDocument(requestBody).toJson()); + encRequestData.requestBody = QJsonDocument(requestBody).toJson(); + return encRequestData; +} + +ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody) +{ + EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload); + if (encRequestData.errorCode != ErrorCode::NoError) { + return encRequestData.errorCode; + } + + QNetworkReply *reply = amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody); QEventLoop wait; connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); @@ -131,19 +146,19 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api reply->deleteLater(); - if (sslErrors.isEmpty() && shouldBypassProxy(replyError, encryptedResponseBody, true, key, iv, salt)) { - auto requestFunction = [&request, &encryptedResponseBody, &requestBody](const QString &url) { - request.setUrl(url); - return amnApp->networkManager()->post(request, QJsonDocument(requestBody).toJson()); + 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, &key, &iv, - &salt, 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, key, iv, salt)) { + if (!sslErrors.isEmpty() || shouldBypassProxy(replyError, encryptedResponseBody, true, encRequestData.key, encRequestData.iv, encRequestData.salt)) { sslErrors = nestedSslErrors; return false; } @@ -161,7 +176,8 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api } try { - responseBody = blockCipher.decryptAesBlockCipher(encryptedResponseBody, key, iv, "", salt); + 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(); @@ -170,6 +186,57 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api } } +QFuture> GatewayController::postAsync(const QString &endpoint, const QJsonObject apiPayload) +{ + auto promise = QSharedPointer>>::create(); + promise->start(); + + EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload); + if (encRequestData.errorCode != ErrorCode::NoError) { + promise->addResult(qMakePair(encRequestData.errorCode, QByteArray())); + promise->finish(); + return promise->future(); + } + + QNetworkReply *reply = amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody); + + auto sslErrors = QSharedPointer>::create(); + + connect(reply, &QNetworkReply::sslErrors, [sslErrors](const QList &errors) { + *sslErrors = errors; + }); + + connect(reply, &QNetworkReply::finished, reply, [=]() { + QByteArray encryptedResponseBody = reply->readAll(); + QString replyErrorString = reply->errorString(); + auto replyError = reply->error(); + int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + reply->deleteLater(); + + auto errorCode = apiUtils::checkNetworkReplyErrors(*sslErrors, replyErrorString, replyError, httpStatusCode, encryptedResponseBody); + 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(); + } + }); + + return promise->future(); +} + QStringList GatewayController::getProxyUrls(const QString &serviceType, const QString &userCountryCode) { QNetworkRequest request; diff --git a/client/core/controllers/gatewayController.h b/client/core/controllers/gatewayController.h index d4e5061f7..e13da1ed3 100644 --- a/client/core/controllers/gatewayController.h +++ b/client/core/controllers/gatewayController.h @@ -1,8 +1,10 @@ #ifndef GATEWAYCONTROLLER_H #define GATEWAYCONTROLLER_H +#include #include #include +#include #include "core/defs.h" @@ -19,8 +21,20 @@ public: const bool isStrictKillSwitchEnabled, QObject *parent = nullptr); amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody); + QFuture> postAsync(const QString &endpoint, const QJsonObject apiPayload); private: + struct EncryptedRequestData { + QNetworkRequest request; + QByteArray requestBody; + QByteArray key; + QByteArray iv; + QByteArray salt; + amnezia::ErrorCode errorCode; + }; + + 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 = ""); diff --git a/client/ui/controllers/api/apiNewsController.cpp b/client/ui/controllers/api/apiNewsController.cpp index 45afacb1f..a6525c043 100644 --- a/client/ui/controllers/api/apiNewsController.cpp +++ b/client/ui/controllers/api/apiNewsController.cpp @@ -32,7 +32,6 @@ void ApiNewsController::fetchNews() } GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled()); - QByteArray responseBody; QJsonObject payload; payload.insert("locale", m_settings->getAppLanguage().name().split("_").first()); @@ -44,22 +43,26 @@ void ApiNewsController::fetchNews() payload.insert(configKey::serviceType, stacksJson.value(configKey::serviceType)); } - ErrorCode errorCode = gatewayController.post(QString("%1v1/news"), payload, responseBody); - if (errorCode != ErrorCode::NoError) { - emit errorOccurred(errorCode); - return; - } - - QJsonDocument doc = QJsonDocument::fromJson(responseBody); - QJsonArray newsArray; - if (doc.isArray()) { - newsArray = doc.array(); - } else if (doc.isObject()) { - QJsonObject obj = doc.object(); - if (obj.value("news").isArray()) { - newsArray = obj.value("news").toArray(); + auto future = gatewayController.postAsync(QString("%1v1/news"), payload); + future.then(this, [this](QPair result) { + auto [errorCode, responseBody] = result; + if (errorCode != ErrorCode::NoError) { + emit errorOccurred(errorCode); + return; } - } - m_newsModel->updateModel(newsArray); + QJsonDocument doc = QJsonDocument::fromJson(responseBody); + QJsonArray newsArray; + if (doc.isArray()) { + newsArray = doc.array(); + } else if (doc.isObject()) { + QJsonObject obj = doc.object(); + if (obj.value("news").isArray()) { + newsArray = obj.value("news").toArray(); + } + } + + m_newsModel->updateModel(newsArray); + emit fetchNewsFinished(); + }); } diff --git a/client/ui/controllers/api/apiNewsController.h b/client/ui/controllers/api/apiNewsController.h index e830c6829..17e744ae1 100644 --- a/client/ui/controllers/api/apiNewsController.h +++ b/client/ui/controllers/api/apiNewsController.h @@ -23,6 +23,7 @@ public: signals: void errorOccurred(ErrorCode errorCode); + void fetchNewsFinished(); private: QSharedPointer m_newsModel; diff --git a/client/ui/qml/Pages2/PageSettings.qml b/client/ui/qml/Pages2/PageSettings.qml index f6647d321..5a6e6bd4c 100644 --- a/client/ui/qml/Pages2/PageSettings.qml +++ b/client/ui/qml/Pages2/PageSettings.qml @@ -14,6 +14,19 @@ import "../Config" PageType { id: root + Connections { + target: ApiNewsController + function onFetchNewsFinished() { + PageController.showBusyIndicator(false) + } + + function onErrorOccurred(errorCode) { + PageController.showErrorMessage(errorCode) + PageController.closePage() + PageController.showBusyIndicator(false) + } + } + ListViewType { id: listView @@ -140,9 +153,8 @@ PageType { return; } PageController.showBusyIndicator(true) - ApiNewsController.fetchNews(); + ApiNewsController.fetchNews() PageController.goToPage(PageEnum.PageSettingsNewsNotifications) - PageController.showBusyIndicator(false) } }