diff --git a/client/core/controllers/gatewayController.cpp b/client/core/controllers/gatewayController.cpp index 4631eac80..9c9565857 100644 --- a/client/core/controllers/gatewayController.cpp +++ b/client/core/controllers/gatewayController.cpp @@ -18,6 +18,7 @@ #include "amnezia_application.h" #include "core/api/apiUtils.h" #include "core/networkUtilities.h" +#include "settings.h" #include "utilities.h" #ifdef AMNEZIA_DESKTOP @@ -51,15 +52,78 @@ namespace constexpr QLatin1String unprocessableSubscriptionMessage("Failed to retrieve subscription information. Is it activated?"); constexpr int proxyStorageRequestTimeoutMsecs = 3000; + + QStringList shuffledProxyUrls(const QStringList &proxyUrls) + { + QStringList shuffled = proxyUrls; + std::random_device randomDevice; + std::mt19937 generator(randomDevice()); + std::shuffle(shuffled.begin(), shuffled.end(), generator); + return shuffled; + } + + QString getProxyUrlsCacheKey(const QString &serviceType, const QString &userCountryCode) + { + return QStringLiteral("service_%1_country_%2").arg(serviceType, userCountryCode); + } + + bool decryptProxyUrlsPayload(const QByteArray &encryptedPayload, bool isDevEnvironment, QByteArray &decryptedPayload) + { + try { + QByteArray key = isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY; + if (!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(encryptedPayload); + + QSimpleCrypto::QBlockCipher cipher; + decryptedPayload = cipher.decryptAesBlockCipher(ba, decKey, iv); + } else { + decryptedPayload = encryptedPayload; + } + return true; + } catch (...) { + Utils::logException(); + return false; + } + } + + QStringList readCachedProxyUrls(const QByteArray &cachedProxyUrlsEncrypted, bool isDevEnvironment) + { + if (cachedProxyUrlsEncrypted.isEmpty()) { + return {}; + } + + QByteArray cachedProxyUrlsDecrypted; + if (!decryptProxyUrlsPayload(cachedProxyUrlsEncrypted, isDevEnvironment, cachedProxyUrlsDecrypted)) { + qCritical() << "error decrypting cached proxy urls payload"; + return {}; + } + + QJsonArray endpointsArray = QJsonDocument::fromJson(cachedProxyUrlsDecrypted).array(); + QStringList endpoints; + endpoints.reserve(endpointsArray.size()); + for (const QJsonValue &endpoint : endpointsArray) { + endpoints.push_back(endpoint.toString()); + } + + return endpoints; + } } GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs, - const bool isStrictKillSwitchEnabled, QObject *parent) + const bool isStrictKillSwitchEnabled, const std::shared_ptr &settings, + QObject *parent) : QObject(parent), m_gatewayEndpoint(gatewayEndpoint), m_isDevEnvironment(isDevEnvironment), m_requestTimeoutMsecs(requestTimeoutMsecs), - m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled) + m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled), + m_settings(settings) { } @@ -310,8 +374,9 @@ QFuture> GatewayController::postAsync(const QString QStringList proxyStorageUrls; appendStorageUrls(primaryBaseUrls, proxyStorageUrls); appendStorageUrls(fallbackBaseUrls, proxyStorageUrls); + const QString proxyUrlsCacheKey = getProxyUrlsCacheKey(serviceType, userCountryCode); - getProxyUrlsAsync(proxyStorageUrls, 0, [this, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) { + getProxyUrlsAsync(proxyStorageUrls, 0, proxyUrlsCacheKey, [this, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) { getProxyUrlAsync(proxyUrls, 0, [this, encRequestData, endpoint, processResponse](const QString &proxyUrl) { bypassProxyAsync(endpoint, proxyUrl, encRequestData, [processResponse, this](const QByteArray &decryptedBody, bool isDecryptionSuccessful, @@ -357,8 +422,6 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS std::shuffle(primaryBaseUrls.begin(), primaryBaseUrls.end(), generator); std::shuffle(fallbackBaseUrls.begin(), fallbackBaseUrls.end(), generator); - QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY; - auto appendStorageUrls = [&serviceType, &userCountryCode](const QStringList &baseUrls, QStringList &target) { if (!serviceType.isEmpty()) { for (const auto &baseUrl : baseUrls) { @@ -374,10 +437,12 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS QStringList proxyStorageUrls; appendStorageUrls(primaryBaseUrls, proxyStorageUrls); appendStorageUrls(fallbackBaseUrls, proxyStorageUrls); + const QString proxyUrlsCacheKey = getProxyUrlsCacheKey(serviceType, userCountryCode); + const QByteArray cachedProxyUrlsEncrypted = m_settings->readGatewayProxyUrls(proxyUrlsCacheKey); if (proxyStorageUrls.empty()) { qDebug() << "empty storage endpoint list"; - return {}; + return readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment); } for (const auto &proxyStorageUrl : proxyStorageUrls) { @@ -392,26 +457,8 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS auto encryptedResponseBody = reply->readAll(); reply->deleteLater(); - EVP_PKEY *privateKey = nullptr; QByteArray responseBody; - try { - if (!m_isDevEnvironment) { - QCryptographicHash hash(QCryptographicHash::Sha512); - hash.addData(key); - QByteArray hashResult = hash.result().toHex(); - - QByteArray key = QByteArray::fromHex(hashResult.left(64)); - QByteArray iv = QByteArray::fromHex(hashResult.mid(64, 32)); - - QByteArray ba = QByteArray::fromBase64(encryptedResponseBody); - - QSimpleCrypto::QBlockCipher blockCipher; - responseBody = blockCipher.decryptAesBlockCipher(ba, key, iv); - } else { - responseBody = encryptedResponseBody; - } - } catch (...) { - Utils::logException(); + if (!decryptProxyUrlsPayload(encryptedResponseBody, m_isDevEnvironment, responseBody)) { qCritical() << "error loading private key from environment variables or decrypting payload" << encryptedResponseBody; continue; } @@ -422,6 +469,8 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS for (const auto &endpoint : endpointsArray) { endpoints.push_back(endpoint.toString()); } + m_settings->writeGatewayProxyUrls(proxyUrlsCacheKey, encryptedResponseBody); + return endpoints; } else { auto replyError = reply->error(); @@ -433,7 +482,7 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS reply->deleteLater(); } } - return {}; + return readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment); } bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody, @@ -571,10 +620,12 @@ void GatewayController::bypassProxy(const QString &endpoint, const QString &serv } void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex, - std::function onComplete) + const QString &proxyUrlsCacheKey, std::function onComplete) { + const QByteArray cachedProxyUrlsEncrypted = m_settings->readGatewayProxyUrls(proxyUrlsCacheKey); + if (currentProxyStorageIndex >= proxyStorageUrls.size()) { - onComplete({}); + onComplete(shuffledProxyUrls(readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment))); return; } @@ -587,33 +638,17 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co // connect(reply, &QNetworkReply::sslErrors, this, [state](const QList &e) { *(state->sslErrors) = e; }); - connect(reply, &QNetworkReply::finished, this, [this, proxyStorageUrls, currentProxyStorageIndex, onComplete, reply]() { + connect(reply, &QNetworkReply::finished, this, + [this, proxyStorageUrls, currentProxyStorageIndex, proxyUrlsCacheKey, 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(); + if (!decryptProxyUrlsPayload(encrypted, m_isDevEnvironment, responseBody)) { qCritical() << "error decrypting payload"; QMetaObject::invokeMethod( - this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection); + this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, proxyUrlsCacheKey, onComplete); }, Qt::QueuedConnection); return; } @@ -621,13 +656,9 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co QStringList endpoints; for (const QJsonValue &endpoint : endpointsArray) endpoints.push_back(endpoint.toString()); + m_settings->writeGatewayProxyUrls(proxyUrlsCacheKey, encrypted); - QStringList shuffled = endpoints; - std::random_device randomDevice; - std::mt19937 generator(randomDevice()); - std::shuffle(shuffled.begin(), shuffled.end(), generator); - - onComplete(shuffled); + onComplete(shuffledProxyUrls(endpoints)); return; } @@ -636,7 +667,7 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co qDebug() << "go to the next storage endpoint"; reply->deleteLater(); QMetaObject::invokeMethod( - this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection); + this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, proxyUrlsCacheKey, onComplete); }, Qt::QueuedConnection); }); } diff --git a/client/core/controllers/gatewayController.h b/client/core/controllers/gatewayController.h index 96e842535..fe9d6f3a5 100644 --- a/client/core/controllers/gatewayController.h +++ b/client/core/controllers/gatewayController.h @@ -7,6 +7,9 @@ #include #include #include +#include +#include +#include #include "core/defs.h" @@ -14,13 +17,16 @@ #include "platforms/ios/ios_controller.h" #endif +class Settings; + class GatewayController : public QObject { Q_OBJECT public: explicit GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs, - const bool isStrictKillSwitchEnabled, QObject *parent = nullptr); + const bool isStrictKillSwitchEnabled, const std::shared_ptr &settings, + QObject *parent = nullptr); amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody); QFuture> postAsync(const QString &endpoint, const QJsonObject apiPayload); @@ -53,7 +59,7 @@ private: std::function &sslErrors)> replyProcessingFunction); void getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex, - std::function onComplete); + const QString &proxyUrlsCacheKey, std::function onComplete); void getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex, std::function onComplete); void bypassProxyAsync( const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData, @@ -63,6 +69,7 @@ private: QString m_gatewayEndpoint; bool m_isDevEnvironment = false; bool m_isStrictKillSwitchEnabled = false; + std::shared_ptr m_settings; inline static QString m_proxyUrl; }; diff --git a/client/settings.cpp b/client/settings.cpp index 2f7b24cbe..635e929a5 100644 --- a/client/settings.cpp +++ b/client/settings.cpp @@ -15,6 +15,7 @@ namespace const char cloudFlareNs2[] = "1.0.0.1"; constexpr char gatewayEndpoint[] = "http://gw.amnezia.org:80/"; + constexpr char proxyUrlsKey[] = "Conf/proxyUrls/"; } Settings::Settings(QObject *parent) : QObject(parent), m_settings(ORGANIZATION_NAME, APPLICATION_NAME, this) @@ -526,6 +527,24 @@ void Settings::toggleDevGatewayEnv(bool enabled) m_settings.setValue("Conf/devGatewayEnv", enabled); } +QByteArray Settings::readGatewayProxyUrls(const QString &cacheKey) const +{ + if (cacheKey.isEmpty()) { + return {}; + } + + return m_settings.value(QString(proxyUrlsKey) + cacheKey).toByteArray(); +} + +void Settings::writeGatewayProxyUrls(const QString &cacheKey, const QByteArray &proxyUrlsEncrypted) +{ + if (cacheKey.isEmpty()) { + return; + } + + m_settings.setValue(QString(proxyUrlsKey) + cacheKey, proxyUrlsEncrypted); +} + bool Settings::isHomeAdLabelVisible() { return m_settings.value("Conf/homeAdLabelVisible", true).toBool(); diff --git a/client/settings.h b/client/settings.h index 45fe39e10..f53f2e4b9 100644 --- a/client/settings.h +++ b/client/settings.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -234,6 +235,8 @@ public: QString getGatewayEndpoint(bool isTestPurchase = false); bool isDevGatewayEnv(bool isTestPurchase = false); void toggleDevGatewayEnv(bool enabled); + QByteArray readGatewayProxyUrls(const QString &cacheKey) const; + void writeGatewayProxyUrls(const QString &cacheKey, const QByteArray &proxyUrlsEncrypted); bool isHomeAdLabelVisible(); void disableHomeAdLabel(); diff --git a/client/ui/controllers/api/apiConfigsController.cpp b/client/ui/controllers/api/apiConfigsController.cpp index d55a74dbd..8e23bb187 100644 --- a/client/ui/controllers/api/apiConfigsController.cpp +++ b/client/ui/controllers/api/apiConfigsController.cpp @@ -1027,7 +1027,7 @@ bool ApiConfigsController::updateServiceFromTelegram(const int serverIndex) #endif GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs, - m_settings->isStrictKillSwitchEnabled()); + m_settings->isStrictKillSwitchEnabled(), m_settings); auto serverConfig = m_serversModel->getServerConfig(serverIndex); auto installationUuid = m_settings->getInstallationUuid(true); @@ -1273,6 +1273,6 @@ ErrorCode ApiConfigsController::executeRequest(const QString &endpoint, const QJ bool isTestPurchase) { GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase), m_settings->isDevGatewayEnv(isTestPurchase), - apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled()); + apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled(), m_settings); return gatewayController.post(endpoint, apiPayload, responseBody); } diff --git a/client/ui/controllers/api/apiNewsController.cpp b/client/ui/controllers/api/apiNewsController.cpp index 9e294f11f..03d64b590 100644 --- a/client/ui/controllers/api/apiNewsController.cpp +++ b/client/ui/controllers/api/apiNewsController.cpp @@ -32,7 +32,8 @@ void ApiNewsController::fetchNews(bool showError) } auto gatewayController = QSharedPointer::create(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), - apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled()); + apiDefs::requestTimeoutMsecs, + m_settings->isStrictKillSwitchEnabled(), m_settings); QJsonObject payload; payload.insert("locale", m_settings->getAppLanguage().name().split("_").first()); diff --git a/client/ui/controllers/api/apiSettingsController.cpp b/client/ui/controllers/api/apiSettingsController.cpp index 9ba2262fc..c8b9bdf8d 100644 --- a/client/ui/controllers/api/apiSettingsController.cpp +++ b/client/ui/controllers/api/apiSettingsController.cpp @@ -71,7 +71,7 @@ bool ApiSettingsController::getAccountInfo(bool reload) bool isTestPurchase = apiConfig.value(apiDefs::key::isTestPurchase).toBool(false); GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase), m_settings->isDevGatewayEnv(isTestPurchase), - requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled()); + requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled(), m_settings); QJsonObject apiPayload; apiPayload[configKey::userCountryCode] = apiConfig.value(configKey::userCountryCode).toString(); @@ -110,7 +110,7 @@ void ApiSettingsController::getRenewalLink() auto gatewayController = QSharedPointer::create(m_settings->getGatewayEndpoint(isTestPurchase), m_settings->isDevGatewayEnv(isTestPurchase), requestTimeoutMsecs, - m_settings->isStrictKillSwitchEnabled()); + m_settings->isStrictKillSwitchEnabled(), m_settings); QJsonObject apiPayload; apiPayload[configKey::userCountryCode] = apiConfig.value(configKey::userCountryCode).toString();