From 06372c8fd7f735158b335198c02da9be46152116 Mon Sep 17 00:00:00 2001 From: vkamn Date: Fri, 15 May 2026 12:33:36 +0800 Subject: [PATCH 01/14] refactor: remove serverConfig struct (#2595) * refactor: remove serverConfig struct * refactor: add warnings for api v1 configs * refactor: moved the server type definition to a separate namespace * refactor: simplified gateway stacks * fix: fixed server description * fix: fixed postAsync reply usage * fix: fixed validateConfig call * fix: fixed server name in notifications * fix: fixed initPrepareConfigHandler for lagacy configs --- client/cmake/sources.cmake | 2 +- .../core/controllers/api/newsController.cpp | 83 ++- client/core/controllers/api/newsController.h | 10 +- .../api/servicesCatalogController.cpp | 2 +- .../api/subscriptionController.cpp | 376 ++++--------- .../controllers/api/subscriptionController.h | 38 +- .../core/controllers/connectionController.cpp | 105 +++- .../core/controllers/connectionController.h | 11 +- client/core/controllers/coreController.cpp | 17 +- client/core/controllers/coreController.h | 2 - .../core/controllers/coreSignalHandlers.cpp | 130 +++-- client/core/controllers/coreSignalHandlers.h | 2 +- client/core/controllers/gatewayController.cpp | 2 +- .../selfhosted/exportController.cpp | 150 +++--- .../controllers/selfhosted/exportController.h | 28 +- .../selfhosted/importController.cpp | 32 +- .../selfhosted/installController.cpp | 225 +++++--- .../selfhosted/installController.h | 29 +- .../selfhosted/usersController.cpp | 79 ++- .../controllers/selfhosted/usersController.h | 15 +- client/core/controllers/serversController.cpp | 489 ++++++++++++----- client/core/controllers/serversController.h | 68 +-- .../core/controllers/settingsController.cpp | 7 +- client/core/models/api/apiConfig.h | 2 +- client/core/models/api/apiV1ServerConfig.cpp | 140 ----- client/core/models/api/apiV1ServerConfig.h | 47 -- client/core/models/api/apiV2ServerConfig.cpp | 8 + client/core/models/api/apiV2ServerConfig.h | 3 +- client/core/models/api/authData.h | 2 +- .../core/models/api/legacyApiServerConfig.cpp | 43 ++ .../core/models/api/legacyApiServerConfig.h | 38 ++ .../models/selfhosted/nativeServerConfig.cpp | 8 + .../models/selfhosted/nativeServerConfig.h | 1 + .../selfHostedAdminServerConfig.cpp | 170 ++++++ .../selfhosted/selfHostedAdminServerConfig.h | 53 ++ ...fig.cpp => selfHostedUserServerConfig.cpp} | 100 ++-- ...rConfig.h => selfHostedUserServerConfig.h} | 20 +- client/core/models/serverConfig.cpp | 234 -------- client/core/models/serverConfig.h | 92 ---- client/core/models/serverDescription.cpp | 187 +++++++ client/core/models/serverDescription.h | 64 +++ .../secureAppSettingsRepository.cpp | 5 +- .../repositories/secureServersRepository.cpp | 466 ++++++++++------ .../repositories/secureServersRepository.h | 78 +-- client/core/utils/api/apiEnums.h | 25 - client/core/utils/api/apiUtils.cpp | 71 +-- client/core/utils/api/apiUtils.h | 7 +- client/core/utils/constants/apiConstants.h | 8 +- client/core/utils/constants/apiKeys.h | 5 +- client/core/utils/constants/configKeys.h | 3 + client/core/utils/errorCodes.h | 5 +- client/core/utils/errorStrings.cpp | 1 + client/core/utils/serverConfigUtils.cpp | 122 +++++ client/core/utils/serverConfigUtils.h | 40 ++ client/tests/CMakeLists.txt | 10 - client/tests/testAdminSelfHostedExport.cpp | 6 +- client/tests/testComplexOperations.cpp | 31 +- client/tests/testDefaultServerChange.cpp | 31 +- client/tests/testGatewayStacks.cpp | 75 --- client/tests/testMultipleImports.cpp | 37 +- client/tests/testSelfHostedServerSetup.cpp | 84 +-- client/tests/testServerEdgeCases.cpp | 38 +- client/tests/testServerEdit.cpp | 38 +- client/tests/testServerRepositoryHelpers.h | 93 ++++ client/tests/testServersModelSync.cpp | 17 +- client/tests/testSettingsSignals.cpp | 1 - client/tests/testSignalOrder.cpp | 9 +- .../tests/testUiServersModelAndController.cpp | 42 +- .../api/subscriptionUiController.cpp | 135 +++-- .../api/subscriptionUiController.h | 29 +- .../ui/controllers/connectionUiController.cpp | 17 +- .../ui/controllers/connectionUiController.h | 2 - client/ui/controllers/qml/pageController.cpp | 12 +- client/ui/controllers/qml/pageController.h | 5 +- .../selfhosted/exportUiController.cpp | 36 +- .../selfhosted/exportUiController.h | 22 +- .../selfhosted/installUiController.cpp | 164 +++--- .../selfhosted/installUiController.h | 34 +- client/ui/controllers/serversUiController.cpp | 499 ++++++++++-------- client/ui/controllers/serversUiController.h | 70 ++- client/ui/models/api/apiAccountInfoModel.cpp | 18 +- client/ui/models/api/apiAccountInfoModel.h | 4 +- client/ui/models/api/apiCountryModel.cpp | 2 +- client/ui/models/api/apiDevicesModel.cpp | 2 +- client/ui/models/serversModel.cpp | 301 +++-------- client/ui/models/serversModel.h | 17 +- client/ui/qml/Components/ConnectButton.qml | 2 +- client/ui/qml/Components/GamepadLoader.qml | 2 +- .../qml/Components/HomeContainersListView.qml | 2 +- .../ui/qml/Components/RenameServerDrawer.qml | 2 +- client/ui/qml/Components/ServersListView.qml | 12 +- .../Components/SettingsContainersListView.qml | 6 +- .../Components/SubscriptionExpiredDrawer.qml | 4 +- client/ui/qml/Pages2/PageHome.qml | 4 +- .../Pages2/PageProtocolAwgClientSettings.qml | 2 +- .../ui/qml/Pages2/PageProtocolAwgSettings.qml | 2 +- .../Pages2/PageProtocolOpenVpnSettings.qml | 2 +- client/ui/qml/Pages2/PageProtocolRaw.qml | 2 +- .../PageProtocolWireGuardClientSettings.qml | 2 +- .../Pages2/PageProtocolWireGuardSettings.qml | 2 +- .../qml/Pages2/PageProtocolXraySettings.qml | 2 +- .../ui/qml/Pages2/PageServiceDnsSettings.qml | 2 +- .../ui/qml/Pages2/PageServiceSftpSettings.qml | 2 +- .../Pages2/PageServiceSocksProxySettings.qml | 2 +- .../PageSettingsApiAvailableCountries.qml | 6 +- .../ui/qml/Pages2/PageSettingsApiDevices.qml | 10 +- .../Pages2/PageSettingsApiNativeConfigs.qml | 486 ++++++++--------- .../qml/Pages2/PageSettingsApiServerInfo.qml | 20 +- .../Pages2/PageSettingsApiSubscriptionKey.qml | 6 +- client/ui/qml/Pages2/PageSettingsDns.qml | 2 +- .../ui/qml/Pages2/PageSettingsServerData.qml | 10 +- .../qml/Pages2/PageSettingsServerProtocol.qml | 8 +- .../ui/qml/Pages2/PageSettingsServersList.qml | 4 +- .../qml/Pages2/PageSetupWizardCredentials.qml | 2 +- client/ui/qml/Pages2/PageSetupWizardEasy.qml | 6 +- .../qml/Pages2/PageSetupWizardInstalling.qml | 8 +- .../PageSetupWizardProtocolSettings.qml | 2 +- client/ui/qml/Pages2/PageShare.qml | 30 +- client/ui/qml/Pages2/PageShareFullAccess.qml | 6 +- client/ui/qml/Pages2/PageStart.qml | 12 +- client/ui/qml/main2.qml | 30 ++ client/vpnConnection.cpp | 49 +- client/vpnConnection.h | 2 +- 123 files changed, 3558 insertions(+), 3026 deletions(-) delete mode 100644 client/core/models/api/apiV1ServerConfig.cpp delete mode 100644 client/core/models/api/apiV1ServerConfig.h create mode 100644 client/core/models/api/legacyApiServerConfig.cpp create mode 100644 client/core/models/api/legacyApiServerConfig.h create mode 100644 client/core/models/selfhosted/selfHostedAdminServerConfig.cpp create mode 100644 client/core/models/selfhosted/selfHostedAdminServerConfig.h rename client/core/models/selfhosted/{selfHostedServerConfig.cpp => selfHostedUserServerConfig.cpp} (53%) rename client/core/models/selfhosted/{selfHostedServerConfig.h => selfHostedUserServerConfig.h} (66%) delete mode 100644 client/core/models/serverConfig.cpp delete mode 100644 client/core/models/serverConfig.h create mode 100644 client/core/models/serverDescription.cpp create mode 100644 client/core/models/serverDescription.h delete mode 100644 client/core/utils/api/apiEnums.h create mode 100644 client/core/utils/serverConfigUtils.cpp create mode 100644 client/core/utils/serverConfigUtils.h delete mode 100644 client/tests/testGatewayStacks.cpp create mode 100644 client/tests/testServerRepositoryHelpers.h diff --git a/client/cmake/sources.cmake b/client/cmake/sources.cmake index 497757757..7c5767549 100644 --- a/client/cmake/sources.cmake +++ b/client/cmake/sources.cmake @@ -15,7 +15,6 @@ set(HEADERS ${HEADERS} ${CLIENT_ROOT_DIR}/core/utils/constants/protocolConstants.h ${CLIENT_ROOT_DIR}/core/utils/constants/apiKeys.h ${CLIENT_ROOT_DIR}/core/utils/constants/apiConstants.h - ${CLIENT_ROOT_DIR}/core/utils/api/apiEnums.h ${CLIENT_ROOT_DIR}/core/utils/errorStrings.h ${CLIENT_ROOT_DIR}/core/utils/selfhosted/scriptsRegistry.h ${CLIENT_ROOT_DIR}/core/utils/qrCodeUtils.h @@ -138,6 +137,7 @@ set(SOURCES ${SOURCES} ${CLIENT_ROOT_DIR}/../common/logger/logger.cpp ${CLIENT_ROOT_DIR}/ui/utils/qmlUtils.cpp ${CLIENT_ROOT_DIR}/core/utils/api/apiUtils.cpp + ${CLIENT_ROOT_DIR}/core/utils/serverConfigUtils.cpp ${CLIENT_ROOT_DIR}/core/utils/osSignalHandler.cpp ${CLIENT_ROOT_DIR}/core/utils/utilities.cpp ${CLIENT_ROOT_DIR}/core/utils/managementServer.cpp diff --git a/client/core/controllers/api/newsController.cpp b/client/core/controllers/api/newsController.cpp index abc045947..4529f29f9 100644 --- a/client/core/controllers/api/newsController.cpp +++ b/client/core/controllers/api/newsController.cpp @@ -1,51 +1,93 @@ #include "newsController.h" #include "core/controllers/gatewayController.h" -#include "core/utils/api/apiEnums.h" +#include "core/repositories/secureServersRepository.h" #include "core/utils/constants/apiKeys.h" #include "core/utils/constants/apiConstants.h" -#include "core/utils/constants/configKeys.h" #include +#include #include #include +#include #include using namespace amnezia; -NewsController::NewsController(SecureAppSettingsRepository* appSettingsRepository, - ServersController* serversController) - : m_appSettingsRepository(appSettingsRepository), m_serversController(serversController) +NewsController::NewsController(SecureAppSettingsRepository *appSettingsRepository, + SecureServersRepository *serversRepository) + : m_appSettingsRepository(appSettingsRepository), + m_serversRepository(serversRepository) { } +QJsonObject NewsController::getServicesList() const +{ + if (!m_serversRepository) { + return {}; + } + QSet userCountryCodes; + QSet serviceTypes; + const QVector ids = m_serversRepository->orderedServerIds(); + for (const QString &id : ids) { + const auto apiV2 = m_serversRepository->apiV2Config(id); + if (!apiV2.has_value()) { + continue; + } + if (!apiV2->apiConfig.userCountryCode.isEmpty()) { + userCountryCodes.insert(apiV2->apiConfig.userCountryCode); + } + const QString serviceType = apiV2->serviceType(); + if (!serviceType.isEmpty()) { + serviceTypes.insert(serviceType); + } + } + if (userCountryCodes.isEmpty() && serviceTypes.isEmpty()) { + return {}; + } + QJsonObject json; + + QJsonArray userCountryCodesArray; + for (const QString &code : userCountryCodes) { + userCountryCodesArray.append(code); + } + json[apiDefs::key::userCountryCode] = userCountryCodesArray; + + QJsonArray serviceTypesArray; + for (const QString &type : serviceTypes) { + serviceTypesArray.append(type); + } + json[apiDefs::key::serviceType] = serviceTypesArray; + + return json; +} + QFuture> NewsController::fetchNews() { - if (!m_serversController) { - qWarning() << "ServersController is null, skip fetchNews"; + if (!m_serversRepository) { + qWarning() << "SecureServersRepository is null, skip fetchNews"; return QtFuture::makeReadyFuture(qMakePair(ErrorCode::InternalError, QJsonArray())); } - - const auto stacks = m_serversController->gatewayStacks(); - if (stacks.isEmpty()) { + + const QJsonObject services = getServicesList(); + if (services.isEmpty()) { qDebug() << "No Gateway stacks, skip fetchNews"; return QtFuture::makeReadyFuture(qMakePair(ErrorCode::NoError, QJsonArray())); } auto gatewayController = QSharedPointer::create( - m_appSettingsRepository->getGatewayEndpoint(), - m_appSettingsRepository->isDevGatewayEnv(), - apiDefs::requestTimeoutMsecs, - m_appSettingsRepository->isStrictKillSwitchEnabled()); - + m_appSettingsRepository->getGatewayEndpoint(), + m_appSettingsRepository->isDevGatewayEnv(), + apiDefs::requestTimeoutMsecs, + m_appSettingsRepository->isStrictKillSwitchEnabled()); + QJsonObject payload; payload.insert("locale", m_appSettingsRepository->getAppLanguage().name().split("_").first()); - const QJsonObject stacksJson = stacks.toJson(); - if (stacksJson.contains(apiDefs::key::userCountryCode)) { - payload.insert(apiDefs::key::userCountryCode, stacksJson.value(apiDefs::key::userCountryCode)); + if (services.contains(apiDefs::key::userCountryCode)) { + payload.insert(apiDefs::key::userCountryCode, services.value(apiDefs::key::userCountryCode)); } - if (stacksJson.contains(apiDefs::key::serviceType)) { - payload.insert(apiDefs::key::serviceType, stacksJson.value(apiDefs::key::serviceType)); + if (services.contains(apiDefs::key::serviceType)) { + payload.insert(apiDefs::key::serviceType, services.value(apiDefs::key::serviceType)); } auto future = gatewayController->postAsync(QString("%1v1/news"), payload); @@ -69,4 +111,3 @@ QFuture> NewsController::fetchNews() return qMakePair(ErrorCode::NoError, newsArray); }); } - diff --git a/client/core/controllers/api/newsController.h b/client/core/controllers/api/newsController.h index 15ffd67b1..42f249276 100644 --- a/client/core/controllers/api/newsController.h +++ b/client/core/controllers/api/newsController.h @@ -3,26 +3,28 @@ #include #include +#include #include #include "core/utils/errorCodes.h" #include "core/utils/routeModes.h" #include "core/utils/commonStructs.h" #include "core/repositories/secureAppSettingsRepository.h" -#include "core/controllers/serversController.h" +#include "core/repositories/secureServersRepository.h" class NewsController { public: explicit NewsController(SecureAppSettingsRepository* appSettingsRepository, - ServersController* serversController); + SecureServersRepository* serversRepository); QFuture> fetchNews(); private: + QJsonObject getServicesList() const; + SecureAppSettingsRepository* m_appSettingsRepository; - ServersController* m_serversController; + SecureServersRepository* m_serversRepository; }; #endif // NEWSCONTROLLER_H - diff --git a/client/core/controllers/api/servicesCatalogController.cpp b/client/core/controllers/api/servicesCatalogController.cpp index afdcfac6d..0d767d8de 100644 --- a/client/core/controllers/api/servicesCatalogController.cpp +++ b/client/core/controllers/api/servicesCatalogController.cpp @@ -11,7 +11,7 @@ #include #include "core/controllers/gatewayController.h" -#include "core/utils/api/apiEnums.h" +#include "core/utils/serverConfigUtils.h" #include "core/utils/constants/apiKeys.h" #include "core/utils/constants/apiConstants.h" #include "version.h" diff --git a/client/core/controllers/api/subscriptionController.cpp b/client/core/controllers/api/subscriptionController.cpp index ce34d4690..d7f149f43 100644 --- a/client/core/controllers/api/subscriptionController.cpp +++ b/client/core/controllers/api/subscriptionController.cpp @@ -16,7 +16,7 @@ #include "core/utils/containerEnum.h" #include "core/utils/containers/containerUtils.h" #include "core/utils/protocolEnum.h" -#include "core/utils/api/apiEnums.h" +#include "core/utils/serverConfigUtils.h" #include "core/utils/constants/apiKeys.h" #include "core/utils/constants/apiConstants.h" #include "core/utils/api/apiUtils.h" @@ -26,7 +26,6 @@ #include "core/utils/constants/configKeys.h" #include "core/utils/constants/protocolConstants.h" #include "version.h" -#include "core/models/serverConfig.h" #include "core/models/containerConfig.h" #include "core/models/api/apiConfig.h" @@ -196,7 +195,7 @@ void SubscriptionController::updateApiConfigInJson(QJsonObject &serverConfigJson apiConfig[apiDefs::key::serviceProtocol] = serviceProtocol; apiConfig[apiDefs::key::userCountryCode] = userCountryCode; - if (serverConfigJson.value(configKey::configVersion).toInt() == apiDefs::ConfigSource::AmneziaGateway) { + if (serverConfigJson.value(configKey::configVersion).toInt() == serverConfigUtils::ConfigSource::AmneziaGateway) { QJsonObject responseObj = QJsonDocument::fromJson(apiResponseBody).object(); if (responseObj.contains(apiDefs::key::supportedProtocols)) { apiConfig.insert(apiDefs::key::supportedProtocols, responseObj.value(apiDefs::key::supportedProtocols).toArray()); @@ -217,8 +216,7 @@ ErrorCode SubscriptionController::executeRequest(const QString &endpoint, const } ErrorCode SubscriptionController::importServiceFromGateway(const QString &userCountryCode, const QString &serviceType, - const QString &serviceProtocol, const ProtocolData &protocolData, - ServerConfig &serverConfig) + const QString &serviceProtocol, const ProtocolData &protocolData) { GatewayRequestData gatewayRequestData { QSysInfo::productType(), QString(APP_VERSION), @@ -247,20 +245,18 @@ ErrorCode SubscriptionController::importServiceFromGateway(const QString &userCo updateApiConfigInJson(serverConfigJson, serviceType, serviceProtocol, userCountryCode, responseBody); - ServerConfig serverConfigModel = ServerConfig::fromJson(serverConfigJson); - - if (!serverConfigModel.isApiV2()) { + if (serverConfigJson.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) { return ErrorCode::InternalError; } - m_serversRepository->addServer(serverConfigModel); - serverConfig = serverConfigModel; + ApiV2ServerConfig apiV2ServerConfig = ApiV2ServerConfig::fromJson(serverConfigJson); + m_serversRepository->addServer(QString(), apiV2ServerConfig.toJson(), + serverConfigUtils::configTypeFromJson(apiV2ServerConfig.toJson())); return ErrorCode::NoError; } ErrorCode SubscriptionController::importTrialFromGateway(const QString &userCountryCode, const QString &serviceType, - const QString &serviceProtocol, const QString &email, - ServerConfig &serverConfig) + const QString &serviceProtocol, const QString &email) { const QString trimmedEmail = email.trimmed(); if (trimmedEmail.isEmpty()) { @@ -306,16 +302,19 @@ ErrorCode SubscriptionController::importTrialFromGateway(const QString &userCoun } QJsonObject configObject = QJsonDocument::fromJson(configBytes).object(); - ServerConfig serverConfigModel = ServerConfig::fromJson(configObject); - m_serversRepository->addServer(serverConfigModel); - serverConfig = serverConfigModel; + if (configObject.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) { + return ErrorCode::InternalError; + } + + ApiV2ServerConfig apiV2ServerConfig = ApiV2ServerConfig::fromJson(configObject); + m_serversRepository->addServer(QString(), apiV2ServerConfig.toJson(), + serverConfigUtils::configTypeFromJson(apiV2ServerConfig.toJson())); return ErrorCode::NoError; } ErrorCode SubscriptionController::importServiceFromAppStore(const QString &userCountryCode, const QString &serviceType, const QString &serviceProtocol, const ProtocolData &protocolData, const QString &transactionId, bool isTestPurchase, - ServerConfig &serverConfig, int *duplicateServerIndex) { GatewayRequestData gatewayRequestData { QSysInfo::productType(), @@ -351,15 +350,8 @@ ErrorCode SubscriptionController::importServiceFromAppStore(const QString &userC // Check if server with this VPN key already exists for (int i = 0; i < m_serversRepository->serversCount(); ++i) { - ServerConfig existingServerConfig = m_serversRepository->server(i); - QString existingVpnKey; - if (existingServerConfig.isApiV1()) { - const ApiV1ServerConfig* apiV1 = existingServerConfig.as(); - existingVpnKey = apiV1 ? apiV1->vpnKey() : QString(); - } else if (existingServerConfig.isApiV2()) { - const ApiV2ServerConfig* apiV2 = existingServerConfig.as(); - existingVpnKey = apiV2 ? apiV2->vpnKey() : QString(); - } + const auto apiV2 = m_serversRepository->apiV2Config(m_serversRepository->serverIdAt(i)); + QString existingVpnKey = apiV2.has_value() ? apiV2->vpnKey() : QString(); existingVpnKey.replace(QStringLiteral("vpn://"), QString()); if (!existingVpnKey.isEmpty() && existingVpnKey == normalizedKey) { if (duplicateServerIndex) { @@ -385,38 +377,28 @@ ErrorCode SubscriptionController::importServiceFromAppStore(const QString &userC quint16 crc = qChecksum(QJsonDocument(configObject).toJson()); - ServerConfig serverConfigModel = ServerConfig::fromJson(configObject); - - if (!serverConfigModel.isApiV2()) { + if (configObject.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) { return ErrorCode::InternalError; } - ApiV2ServerConfig* apiV2 = serverConfigModel.as(); - if (!apiV2) { - return ErrorCode::InternalError; - } + ApiV2ServerConfig apiV2ServerConfig = ApiV2ServerConfig::fromJson(configObject); + ApiV2ServerConfig* apiV2 = &apiV2ServerConfig; apiV2->apiConfig.vpnKey = normalizedKey; apiV2->apiConfig.isTestPurchase = isTestPurchase; apiV2->apiConfig.isInAppPurchase = true; apiV2->apiConfig.subscriptionExpiredByServer = false; apiV2->crc = crc; - m_serversRepository->addServer(serverConfigModel); - serverConfig = serverConfigModel; + m_serversRepository->addServer(QString(), apiV2ServerConfig.toJson(), + serverConfigUtils::configTypeFromJson(apiV2ServerConfig.toJson())); return ErrorCode::NoError; } -ErrorCode SubscriptionController::updateServiceFromGateway(int serverIndex, const QString &newCountryCode, bool isConnectEvent) +ErrorCode SubscriptionController::updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, bool isConnectEvent) { - ServerConfig serverConfigModel = m_serversRepository->server(serverIndex); - - if (!serverConfigModel.isApiV2()) { - return ErrorCode::InternalError; - } - - const ApiV2ServerConfig* apiV2 = serverConfigModel.as(); - if (!apiV2) { + auto apiV2 = m_serversRepository->apiV2Config(serverId); + if (!apiV2.has_value()) { return ErrorCode::InternalError; } const bool isTestPurchase = apiV2->apiConfig.isTestPurchase; @@ -445,12 +427,10 @@ ErrorCode SubscriptionController::updateServiceFromGateway(int serverIndex, cons ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody, isTestPurchase); if (errorCode != ErrorCode::NoError) { if (errorCode == ErrorCode::ApiSubscriptionExpiredError && !apiV2->apiConfig.isInAppPurchase) { - ServerConfig expiredServerConfig = serverConfigModel; - ApiV2ServerConfig *expiredApiV2 = expiredServerConfig.as(); - if (expiredApiV2) { - expiredApiV2->apiConfig.subscriptionExpiredByServer = true; - m_serversRepository->editServer(serverIndex, expiredServerConfig); - } + ApiV2ServerConfig expiredApiV2 = *apiV2; + expiredApiV2.apiConfig.subscriptionExpiredByServer = true; + m_serversRepository->editServer(serverId, expiredApiV2.toJson(), + serverConfigUtils::configTypeFromJson(expiredApiV2.toJson())); } return errorCode; } @@ -463,16 +443,12 @@ ErrorCode SubscriptionController::updateServiceFromGateway(int serverIndex, cons updateApiConfigInJson(serverConfigJson, apiV2->apiConfig.serviceType, serviceProtocol, apiV2->apiConfig.userCountryCode, responseBody); - ServerConfig newServerConfigModel = ServerConfig::fromJson(serverConfigJson); - - if (!newServerConfigModel.isApiV2()) { + if (serverConfigJson.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) { return ErrorCode::InternalError; } - ApiV2ServerConfig* newApiV2 = newServerConfigModel.as(); - if (!newApiV2) { - return ErrorCode::InternalError; - } + ApiV2ServerConfig newApiV2Config = ApiV2ServerConfig::fromJson(serverConfigJson); + ApiV2ServerConfig* newApiV2 = &newApiV2Config; newApiV2->apiConfig.vpnKey = apiV2->apiConfig.vpnKey; newApiV2->apiConfig.isTestPurchase = apiV2->apiConfig.isTestPurchase; @@ -487,20 +463,15 @@ ErrorCode SubscriptionController::updateServiceFromGateway(int serverIndex, cons newApiV2->nameOverriddenByUser = true; } - m_serversRepository->editServer(serverIndex, newServerConfigModel); + m_serversRepository->editServer(serverId, newApiV2Config.toJson(), + serverConfigUtils::configTypeFromJson(newApiV2Config.toJson())); return ErrorCode::NoError; } -ErrorCode SubscriptionController::deactivateDevice(int serverIndex) +ErrorCode SubscriptionController::deactivateDevice(const QString &serverId) { - ServerConfig serverConfigModel = m_serversRepository->server(serverIndex); - - if (!serverConfigModel.isApiV2()) { - return ErrorCode::NoError; - } - - const ApiV2ServerConfig* apiV2 = serverConfigModel.as(); - if (!apiV2) { + auto apiV2 = m_serversRepository->apiV2Config(serverId); + if (!apiV2.has_value()) { return ErrorCode::NoError; } @@ -528,23 +499,16 @@ ErrorCode SubscriptionController::deactivateDevice(int serverIndex) return errorCode; } - serverConfigModel.visit([](auto& arg) { - arg.containers.clear(); - }); - m_serversRepository->editServer(serverIndex, serverConfigModel); + apiV2->containers.clear(); + m_serversRepository->editServer(serverId, apiV2->toJson(), + serverConfigUtils::configTypeFromJson(apiV2->toJson())); return ErrorCode::NoError; } -ErrorCode SubscriptionController::deactivateExternalDevice(int serverIndex, const QString &uuid, const QString &serverCountryCode) +ErrorCode SubscriptionController::deactivateExternalDevice(const QString &serverId, const QString &uuid, const QString &serverCountryCode) { - ServerConfig serverConfigModel = m_serversRepository->server(serverIndex); - - if (!serverConfigModel.isApiV2()) { - return ErrorCode::NoError; - } - - const ApiV2ServerConfig* apiV2 = serverConfigModel.as(); - if (!apiV2) { + auto apiV2 = m_serversRepository->apiV2Config(serverId); + if (!apiV2.has_value()) { return ErrorCode::NoError; } @@ -573,25 +537,18 @@ ErrorCode SubscriptionController::deactivateExternalDevice(int serverIndex, cons } if (uuid == m_appSettingsRepository->getInstallationUuid(true)) { - serverConfigModel.visit([](auto& arg) { - arg.containers.clear(); - }); - m_serversRepository->editServer(serverIndex, serverConfigModel); + apiV2->containers.clear(); + m_serversRepository->editServer(serverId, apiV2->toJson(), + serverConfigUtils::configTypeFromJson(apiV2->toJson())); } return ErrorCode::NoError; } -ErrorCode SubscriptionController::exportNativeConfig(int serverIndex, const QString &serverCountryCode, QString &nativeConfig) +ErrorCode SubscriptionController::exportNativeConfig(const QString &serverId, const QString &serverCountryCode, QString &nativeConfig) { - ServerConfig serverConfigModel = m_serversRepository->server(serverIndex); - - if (!serverConfigModel.isApiV2()) { - return ErrorCode::InternalError; - } - - const ApiV2ServerConfig* apiV2 = serverConfigModel.as(); - if (!apiV2) { + auto apiV2 = m_serversRepository->apiV2Config(serverId); + if (!apiV2.has_value()) { return ErrorCode::InternalError; } const bool isTestPurchase = apiV2->apiConfig.isTestPurchase; @@ -624,16 +581,10 @@ ErrorCode SubscriptionController::exportNativeConfig(int serverIndex, const QStr return ErrorCode::NoError; } -ErrorCode SubscriptionController::revokeNativeConfig(int serverIndex, const QString &serverCountryCode) +ErrorCode SubscriptionController::revokeNativeConfig(const QString &serverId, const QString &serverCountryCode) { - ServerConfig serverConfigModel = m_serversRepository->server(serverIndex); - - if (!serverConfigModel.isApiV2()) { - return ErrorCode::InternalError; - } - - const ApiV2ServerConfig* apiV2 = serverConfigModel.as(); - if (!apiV2) { + auto apiV2 = m_serversRepository->apiV2Config(serverId); + if (!apiV2.has_value()) { return ErrorCode::InternalError; } const bool isTestPurchase = apiV2->apiConfig.isTestPurchase; @@ -661,126 +612,54 @@ ErrorCode SubscriptionController::revokeNativeConfig(int serverIndex, const QStr return ErrorCode::NoError; } -ErrorCode SubscriptionController::updateServiceFromTelegram(int serverIndex) +ErrorCode SubscriptionController::prepareVpnKeyExport(const QString &serverId, QString &vpnKey) { - ServerConfig serverConfigModel = m_serversRepository->server(serverIndex); - - if (!serverConfigModel.isApiV1()) { - return ErrorCode::InternalError; - } - - const ApiV1ServerConfig* apiV1 = serverConfigModel.as(); - if (!apiV1) { - return ErrorCode::InternalError; - } - QString serviceProtocol = apiV1->protocol; - ProtocolData protocolData = generateProtocolData(serviceProtocol); - QString installationUuid = m_appSettingsRepository->getInstallationUuid(true); - - GatewayController gatewayController(m_appSettingsRepository->getGatewayEndpoint(), m_appSettingsRepository->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs, - m_appSettingsRepository->isStrictKillSwitchEnabled()); - - QJsonObject apiPayload; - appendProtocolDataToApiPayload(serviceProtocol, protocolData, apiPayload); - apiPayload[apiDefs::key::uuid] = installationUuid; - apiPayload[apiDefs::key::osVersion] = QSysInfo::productType(); - apiPayload[apiDefs::key::appVersion] = QString(APP_VERSION); - apiPayload[configKey::accessToken] = apiV1->apiKey; - apiPayload[apiDefs::key::apiEndpoint] = apiV1->apiEndpoint; - - QByteArray responseBody; - ErrorCode errorCode = gatewayController.post(QString("%1v1/proxy_config"), apiPayload, responseBody); - if (errorCode != ErrorCode::NoError) { - return errorCode; - } - - QJsonObject serverConfigJson; - errorCode = extractServerConfigJsonFromResponse(responseBody, serviceProtocol, protocolData, serverConfigJson); - if (errorCode != ErrorCode::NoError) { - return errorCode; - } - - ServerConfig newServerConfigModel = ServerConfig::fromJson(serverConfigJson); - - if (!newServerConfigModel.isApiV1()) { - return ErrorCode::InternalError; - } - - ApiV1ServerConfig* newApiV1 = newServerConfigModel.as(); - if (!newApiV1) { - return ErrorCode::InternalError; - } - newApiV1->apiKey = apiV1->apiKey; - newApiV1->apiEndpoint = apiV1->apiEndpoint; - newApiV1->crc = apiV1->crc; - - m_serversRepository->editServer(serverIndex, newServerConfigModel); - return ErrorCode::NoError; -} - -ErrorCode SubscriptionController::prepareVpnKeyExport(int serverIndex, QString &vpnKey) -{ - ServerConfig serverConfigModel = m_serversRepository->server(serverIndex); - - if (serverConfigModel.isApiV1()) { - const ApiV1ServerConfig* apiV1 = serverConfigModel.as(); - vpnKey = apiV1 ? apiV1->vpnKey() : QString(); - } else if (serverConfigModel.isApiV2()) { - ApiV2ServerConfig* apiV2 = serverConfigModel.as(); - vpnKey = apiV2 ? apiV2->vpnKey() : QString(); - if (vpnKey.isEmpty()) { - QJsonObject serverJson = serverConfigModel.toJson(); - vpnKey = apiUtils::getPremiumV2VpnKey(serverJson); - if (vpnKey.isEmpty()) { - return ErrorCode::ApiConfigEmptyError; - } - apiV2->apiConfig.vpnKey = vpnKey; - m_serversRepository->editServer(serverIndex, serverConfigModel); - } - } else { + auto apiV2 = m_serversRepository->apiV2Config(serverId); + if (!apiV2.has_value()) { return ErrorCode::ApiConfigEmptyError; } + vpnKey = apiV2->vpnKey(); + if (vpnKey.isEmpty()) { + vpnKey = apiUtils::getPremiumV2VpnKey(apiV2->toJson()); + if (vpnKey.isEmpty()) { + return ErrorCode::ApiConfigEmptyError; + } + apiV2->apiConfig.vpnKey = vpnKey; + m_serversRepository->editServer(serverId, apiV2->toJson(), + serverConfigUtils::configTypeFromJson(apiV2->toJson())); + } return ErrorCode::NoError; } -ErrorCode SubscriptionController::validateAndUpdateConfig(int serverIndex, bool hasInstalledContainers) +ErrorCode SubscriptionController::validateAndUpdateConfig(const QString &serverId, bool hasInstalledContainers) { - ServerConfig serverConfigModel = m_serversRepository->server(serverIndex); - - apiDefs::ConfigSource configSource; - if (serverConfigModel.isApiV1()) { - configSource = apiDefs::ConfigSource::Telegram; - } else if (serverConfigModel.isApiV2()) { - configSource = apiDefs::ConfigSource::AmneziaGateway; - } else { + if (!m_serversRepository->apiV2Config(serverId).has_value()) { return ErrorCode::NoError; } - if (configSource == apiDefs::ConfigSource::Telegram && !hasInstalledContainers) { - removeApiConfig(serverIndex); - return updateServiceFromTelegram(serverIndex); - } else if (configSource == apiDefs::ConfigSource::AmneziaGateway && !hasInstalledContainers) { - return updateServiceFromGateway(serverIndex, "", true); - } else if (configSource && isApiKeyExpired(serverIndex)) { - qDebug() << "attempt to update api config by expires_at event"; - if (configSource == apiDefs::ConfigSource::AmneziaGateway) { - return updateServiceFromGateway(serverIndex, "", true); - } else { - removeApiConfig(serverIndex); - return updateServiceFromTelegram(serverIndex); - } + if (!hasInstalledContainers) { + return updateServiceFromGateway(serverId, "", true); } + + if (isApiKeyExpired(serverId)) { + qDebug() << "attempt to update api config by expires_at event"; + return updateServiceFromGateway(serverId, "", true); + } + return ErrorCode::NoError; } -void SubscriptionController::removeApiConfig(int serverIndex) +void SubscriptionController::removeApiConfig(const QString &serverId) { - ServerConfig serverConfigModel = m_serversRepository->server(serverIndex); + auto apiV2 = m_serversRepository->apiV2Config(serverId); + if (!apiV2.has_value()) { + return; + } #if defined(Q_OS_IOS) || defined(MACOS_NE) - QString description = serverConfigModel.description(); - QString hostName = serverConfigModel.hostName(); + QString description = apiV2->description; + QString hostName = apiV2->hostName; QString vpncName = QString("%1 (%2) %3") .arg(description) .arg(hostName) @@ -789,34 +668,21 @@ void SubscriptionController::removeApiConfig(int serverIndex) AmneziaVPN::removeVPNC(vpncName.toStdString()); #endif - serverConfigModel.visit([](auto& arg) { - arg.dns1.clear(); - arg.dns2.clear(); - arg.containers.clear(); - arg.hostName.clear(); - arg.defaultContainer = DockerContainer::None; - }); + apiV2->dns1.clear(); + apiV2->dns2.clear(); + apiV2->containers.clear(); + apiV2->hostName.clear(); + apiV2->defaultContainer = DockerContainer::None; + apiV2->apiConfig.publicKey = ApiConfig::PublicKeyInfo{}; - if (serverConfigModel.isApiV2()) { - ApiV2ServerConfig* apiV2 = serverConfigModel.as(); - if (apiV2) { - apiV2->apiConfig.publicKey = ApiConfig::PublicKeyInfo{}; - } - } - - m_serversRepository->editServer(serverIndex, serverConfigModel); + m_serversRepository->editServer(serverId, apiV2->toJson(), + serverConfigUtils::configTypeFromJson(apiV2->toJson())); } -bool SubscriptionController::isApiKeyExpired(int serverIndex) const +bool SubscriptionController::isApiKeyExpired(const QString &serverId) const { - ServerConfig serverConfigModel = m_serversRepository->server(serverIndex); - - if (!serverConfigModel.isApiV2()) { - return false; - } - - const ApiV2ServerConfig* apiV2 = serverConfigModel.as(); - if (!apiV2) { + auto apiV2 = m_serversRepository->apiV2Config(serverId); + if (!apiV2.has_value()) { return false; } const QString expiresAt = apiV2->apiConfig.publicKey.expiresAt; @@ -833,31 +699,24 @@ bool SubscriptionController::isApiKeyExpired(int serverIndex) const return false; } -void SubscriptionController::setCurrentProtocol(int serverIndex, const QString &protocolName) +void SubscriptionController::setCurrentProtocol(const QString &serverId, const QString &protocolName) { - ServerConfig serverConfigModel = m_serversRepository->server(serverIndex); - if (serverConfigModel.isApiV2()) { - ApiV2ServerConfig* apiV2 = serverConfigModel.as(); - if (apiV2) { - apiV2->apiConfig.serviceProtocol = protocolName; - } - m_serversRepository->editServer(serverIndex, serverConfigModel); + auto apiV2 = m_serversRepository->apiV2Config(serverId); + if (apiV2.has_value()) { + apiV2->apiConfig.serviceProtocol = protocolName; + m_serversRepository->editServer(serverId, apiV2->toJson(), + serverConfigUtils::configTypeFromJson(apiV2->toJson())); } } -bool SubscriptionController::isVlessProtocol(int serverIndex) const +bool SubscriptionController::isVlessProtocol(const QString &serverId) const { - ServerConfig serverConfigModel = m_serversRepository->server(serverIndex); - if (serverConfigModel.isApiV2()) { - const ApiV2ServerConfig* apiV2 = serverConfigModel.as(); - return apiV2 && apiV2->serviceProtocol() == "vless"; - } - return false; + auto apiV2 = m_serversRepository->apiV2Config(serverId); + return apiV2.has_value() && apiV2->serviceProtocol() == "vless"; } ErrorCode SubscriptionController::processAppStorePurchase(const QString &userCountryCode, const QString &serviceType, const QString &serviceProtocol, const QString &productId, - ServerConfig &serverConfig, int *duplicateServerIndex) { #if defined(Q_OS_IOS) || defined(MACOS_NE) @@ -891,13 +750,12 @@ ErrorCode SubscriptionController::processAppStorePurchase(const QString &userCou ProtocolData protocolData = generateProtocolData(serviceProtocol); return importServiceFromAppStore(userCountryCode, serviceType, serviceProtocol, protocolData, - originalTransactionId, isTestPurchase, serverConfig, duplicateServerIndex); + originalTransactionId, isTestPurchase, duplicateServerIndex); #else Q_UNUSED(userCountryCode); Q_UNUSED(serviceType); Q_UNUSED(serviceProtocol); Q_UNUSED(productId); - Q_UNUSED(serverConfig); return ErrorCode::ApiPurchaseError; #endif } @@ -956,10 +814,9 @@ SubscriptionController::AppStoreRestoreResult SubscriptionController::processApp << "originalTransactionId =" << originalTransactionId << "productId =" << transactionProductId; ProtocolData protocolData = generateProtocolData(serviceProtocol); - ServerConfig serverConfig; int currentDuplicateServerIndex = -1; ErrorCode errorCode = importServiceFromAppStore(userCountryCode, serviceType, serviceProtocol, protocolData, - originalTransactionId, isTestPurchase, serverConfig, + originalTransactionId, isTestPurchase, ¤tDuplicateServerIndex); if (errorCode == ErrorCode::ApiConfigAlreadyAdded) { @@ -991,16 +848,10 @@ SubscriptionController::AppStoreRestoreResult SubscriptionController::processApp #endif } -ErrorCode SubscriptionController::getAccountInfo(int serverIndex, QJsonObject &accountInfo) +ErrorCode SubscriptionController::getAccountInfo(const QString &serverId, QJsonObject &accountInfo) { - ServerConfig serverConfigModel = m_serversRepository->server(serverIndex); - - if (!serverConfigModel.isApiV2()) { - return ErrorCode::InternalError; - } - - const ApiV2ServerConfig* apiV2 = serverConfigModel.as(); - if (!apiV2) { + auto apiV2 = m_serversRepository->apiV2Config(serverId); + if (!apiV2.has_value()) { return ErrorCode::InternalError; } bool isTestPurchase = apiV2->apiConfig.isTestPurchase; @@ -1030,20 +881,13 @@ ErrorCode SubscriptionController::getAccountInfo(int serverIndex, QJsonObject &a return ErrorCode::NoError; } -QFuture> SubscriptionController::getRenewalLink(int serverIndex) +QFuture> SubscriptionController::getRenewalLink(const QString &serverId) { auto promise = QSharedPointer>>::create(); promise->start(); - ServerConfig serverConfigModel = m_serversRepository->server(serverIndex); - if (!serverConfigModel.isApiV2()) { - promise->addResult(qMakePair(ErrorCode::InternalError, QString())); - promise->finish(); - return promise->future(); - } - - const ApiV2ServerConfig *apiV2 = serverConfigModel.as(); - if (!apiV2) { + auto apiV2 = m_serversRepository->apiV2Config(serverId); + if (!apiV2.has_value()) { promise->addResult(qMakePair(ErrorCode::InternalError, QString())); promise->finish(); return promise->future(); diff --git a/client/core/controllers/api/subscriptionController.h b/client/core/controllers/api/subscriptionController.h index 2780ab1da..5b0f65da6 100644 --- a/client/core/controllers/api/subscriptionController.h +++ b/client/core/controllers/api/subscriptionController.h @@ -12,7 +12,6 @@ #include "core/utils/commonStructs.h" #include "core/repositories/secureServersRepository.h" #include "core/repositories/secureAppSettingsRepository.h" -#include "core/models/serverConfig.h" class ServersController; @@ -48,44 +47,38 @@ public: ProtocolData generateProtocolData(const QString &protocol); void appendProtocolDataToApiPayload(const QString &protocol, const ProtocolData &protocolData, QJsonObject &apiPayload); - ErrorCode fillServerConfig(const QJsonObject &serverConfigJson, ServerConfig &serverConfig); ErrorCode importServiceFromGateway(const QString &userCountryCode, const QString &serviceType, - const QString &serviceProtocol, const ProtocolData &protocolData, - ServerConfig &serverConfig); + const QString &serviceProtocol, const ProtocolData &protocolData); ErrorCode importTrialFromGateway(const QString &userCountryCode, const QString &serviceType, - const QString &serviceProtocol, const QString &email, - ServerConfig &serverConfig); + const QString &serviceProtocol, const QString &email); ErrorCode importServiceFromAppStore(const QString &userCountryCode, const QString &serviceType, const QString &serviceProtocol, const ProtocolData &protocolData, const QString &transactionId, bool isTestPurchase, - ServerConfig &serverConfig, int *duplicateServerIndex = nullptr); - ErrorCode updateServiceFromGateway(int serverIndex, const QString &newCountryCode, bool isConnectEvent); + ErrorCode updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, bool isConnectEvent); - ErrorCode deactivateDevice(int serverIndex); + ErrorCode deactivateDevice(const QString &serverId); - ErrorCode deactivateExternalDevice(int serverIndex, const QString &uuid, const QString &serverCountryCode); + ErrorCode deactivateExternalDevice(const QString &serverId, const QString &uuid, const QString &serverCountryCode); - ErrorCode exportNativeConfig(int serverIndex, const QString &serverCountryCode, QString &nativeConfig); + ErrorCode exportNativeConfig(const QString &serverId, const QString &serverCountryCode, QString &nativeConfig); - ErrorCode revokeNativeConfig(int serverIndex, const QString &serverCountryCode); + ErrorCode revokeNativeConfig(const QString &serverId, const QString &serverCountryCode); - ErrorCode updateServiceFromTelegram(int serverIndex); + ErrorCode prepareVpnKeyExport(const QString &serverId, QString &vpnKey); - ErrorCode prepareVpnKeyExport(int serverIndex, QString &vpnKey); + ErrorCode validateAndUpdateConfig(const QString &serverId, bool hasInstalledContainers); - ErrorCode validateAndUpdateConfig(int serverIndex, bool hasInstalledContainers); + void removeApiConfig(const QString &serverId); - void removeApiConfig(int serverIndex); + void setCurrentProtocol(const QString &serverId, const QString &protocolName); + bool isVlessProtocol(const QString &serverId) const; - void setCurrentProtocol(int serverIndex, const QString &protocolName); - bool isVlessProtocol(int serverIndex) const; - - ErrorCode getAccountInfo(int serverIndex, QJsonObject &accountInfo); - QFuture> getRenewalLink(int serverIndex); + ErrorCode getAccountInfo(const QString &serverId, QJsonObject &accountInfo); + QFuture> getRenewalLink(const QString &serverId); struct AppStoreRestoreResult { @@ -98,7 +91,6 @@ public: ErrorCode processAppStorePurchase(const QString &userCountryCode, const QString &serviceType, const QString &serviceProtocol, const QString &productId, - ServerConfig &serverConfig, int *duplicateServerIndex = nullptr); AppStoreRestoreResult processAppStoreRestore(const QString &userCountryCode, const QString &serviceType, @@ -106,7 +98,7 @@ public: private: ErrorCode executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody, bool isTestPurchase = false); - bool isApiKeyExpired(int serverIndex) const; + bool isApiKeyExpired(const QString &serverId) const; ErrorCode extractServerConfigJsonFromResponse(const QByteArray &apiResponseBody, const QString &protocol, const ProtocolData &protocolData, QJsonObject &serverConfigJson); diff --git a/client/core/controllers/connectionController.cpp b/client/core/controllers/connectionController.cpp index 122727e76..f360cf2f4 100644 --- a/client/core/controllers/connectionController.cpp +++ b/client/core/controllers/connectionController.cpp @@ -9,11 +9,11 @@ #include "core/utils/constants/protocolConstants.h" #include "core/utils/utilities.h" #include "core/utils/networkUtilities.h" +#include "core/utils/serverConfigUtils.h" #include "version.h" #include "core/utils/containerEnum.h" #include "core/utils/containers/containerUtils.h" #include "core/utils/protocolEnum.h" -#include "core/models/serverConfig.h" #include "core/models/containerConfig.h" #include "core/models/protocolConfig.h" @@ -51,7 +51,7 @@ void ConnectionController::setConnectionState(Vpn::ConnectionState state) } } -ErrorCode ConnectionController::prepareConnection(int serverIndex, +ErrorCode ConnectionController::prepareConnection(const QString &serverId, QJsonObject& vpnConfiguration, DockerContainer& container) { @@ -59,35 +59,98 @@ ErrorCode ConnectionController::prepareConnection(int serverIndex, return ErrorCode::AmneziaServiceNotRunning; } - ServerConfig serverConfigModel = m_serversRepository->server(serverIndex); - container = serverConfigModel.defaultContainer(); + ContainerConfig containerConfigModel; + QPair dns; + QString hostName; + QString description; + int configVersion = 0; + bool isApiConfig = false; + + const auto kind = m_serversRepository->serverKind(serverId); + switch (kind) { + case serverConfigUtils::ConfigType::SelfHostedAdmin: { + const auto cfg = m_serversRepository->selfHostedAdminConfig(serverId); + if (!cfg.has_value()) return ErrorCode::InternalError; + container = cfg->defaultContainer; + containerConfigModel = cfg->containerConfig(container); + dns = { cfg->dns1, cfg->dns2 }; + hostName = cfg->hostName; + description = cfg->description; + break; + } + case serverConfigUtils::ConfigType::SelfHostedUser: { + const auto cfg = m_serversRepository->selfHostedUserConfig(serverId); + if (!cfg.has_value()) return ErrorCode::InternalError; + container = cfg->defaultContainer; + containerConfigModel = cfg->containerConfig(container); + dns = { cfg->dns1, cfg->dns2 }; + hostName = cfg->hostName; + description = cfg->description; + break; + } + case serverConfigUtils::ConfigType::Native: { + const auto cfg = m_serversRepository->nativeConfig(serverId); + if (!cfg.has_value()) return ErrorCode::InternalError; + container = cfg->defaultContainer; + containerConfigModel = cfg->containerConfig(container); + dns = { cfg->dns1, cfg->dns2 }; + hostName = cfg->hostName; + description = cfg->description; + break; + } + case serverConfigUtils::ConfigType::AmneziaPremiumV2: + case serverConfigUtils::ConfigType::AmneziaFreeV3: + case serverConfigUtils::ConfigType::ExternalPremium: { + const auto cfg = m_serversRepository->apiV2Config(serverId); + if (!cfg.has_value()) return ErrorCode::InternalError; + container = cfg->defaultContainer; + containerConfigModel = cfg->containerConfig(container); + dns = { cfg->dns1, cfg->dns2 }; + hostName = cfg->hostName; + description = cfg->description; + configVersion = serverConfigUtils::ConfigSource::AmneziaGateway; + isApiConfig = true; + break; + } + case serverConfigUtils::ConfigType::AmneziaPremiumV1: + case serverConfigUtils::ConfigType::AmneziaFreeV2: + return ErrorCode::InternalError; + case serverConfigUtils::ConfigType::Invalid: + default: + return ErrorCode::InternalError; + } if (!isContainerSupported(container)) { return ErrorCode::NotSupportedOnThisPlatform; } + if (dns.first.isEmpty() || !NetworkUtilities::checkIPv4Format(dns.first)) { + if (m_appSettingsRepository->useAmneziaDns()) { + dns.first = protocols::dns::amneziaDnsIp; + } else { + dns.first = m_appSettingsRepository->primaryDns(); + } + } + if (dns.second.isEmpty() || !NetworkUtilities::checkIPv4Format(dns.second)) { + dns.second = m_appSettingsRepository->secondaryDns(); + } - ContainerConfig containerConfigModel = m_serversRepository->containerConfig(serverIndex, container); - - auto dns = serverConfigModel.getDnsPair(m_appSettingsRepository->useAmneziaDns(), - m_appSettingsRepository->primaryDns(), - m_appSettingsRepository->secondaryDns()); - - vpnConfiguration = createConnectionConfiguration(dns, serverConfigModel, containerConfigModel, container); + vpnConfiguration = createConnectionConfiguration(dns, isApiConfig, hostName, description, configVersion, + containerConfigModel, container); return ErrorCode::NoError; } -ErrorCode ConnectionController::openConnection(int serverIndex) +ErrorCode ConnectionController::openConnection(const QString &serverId) { QJsonObject vpnConfiguration; DockerContainer container; - ErrorCode errorCode = prepareConnection(serverIndex, vpnConfiguration, container); + ErrorCode errorCode = prepareConnection(serverId, vpnConfiguration, container); if (errorCode != ErrorCode::NoError) { return errorCode; } - emit openConnectionRequested(serverIndex, container, vpnConfiguration); + emit openConnectionRequested(serverId, container, vpnConfiguration); return ErrorCode::NoError; } @@ -120,7 +183,10 @@ ErrorCode ConnectionController::lastConnectionError() const } QJsonObject ConnectionController::createConnectionConfiguration(const QPair &dns, - const ServerConfig &serverConfig, + bool isApiConfig, + const QString &hostName, + const QString &description, + int configVersion, const ContainerConfig &containerConfig, DockerContainer container) { @@ -134,7 +200,7 @@ QJsonObject ConnectionController::createConnectionConfiguration(const QPairisSitesSplitTunnelingEnabled(), m_appSettingsRepository->routeMode() @@ -160,10 +226,9 @@ QJsonObject ConnectionController::createConnectionConfiguration(const QPair &dns, - const ServerConfig &serverConfig, + bool isApiConfig, + const QString &hostName, + const QString &description, + int configVersion, const ContainerConfig &containerConfig, DockerContainer container); @@ -60,7 +63,7 @@ public: signals: void connectionStateChanged(Vpn::ConnectionState state); - void openConnectionRequested(int serverIndex, DockerContainer container, const QJsonObject &vpnConfiguration); + void openConnectionRequested(const QString &serverId, DockerContainer container, const QJsonObject &vpnConfiguration); void closeConnectionRequested(); void setConnectionStateRequested(Vpn::ConnectionState state); void killSwitchModeChangedRequested(bool enabled); diff --git a/client/core/controllers/coreController.cpp b/client/core/controllers/coreController.cpp index 227850b6d..b23f98180 100644 --- a/client/core/controllers/coreController.cpp +++ b/client/core/controllers/coreController.cpp @@ -8,7 +8,6 @@ #include "core/controllers/selfhosted/installController.h" #include "core/controllers/selfhosted/importController.h" #include "core/controllers/coreSignalHandlers.h" -#include "core/models/serverConfig.h" #include "logger.h" #include "secureQSettings.h" @@ -145,7 +144,7 @@ void CoreController::initCoreControllers() m_allowedDnsController = new AllowedDnsController(m_appSettingsRepository); m_servicesCatalogController = new ServicesCatalogController(m_appSettingsRepository); m_subscriptionController = new SubscriptionController(m_serversRepository, m_appSettingsRepository); - m_newsController = new NewsController(m_appSettingsRepository, m_serversController); + m_newsController = new NewsController(m_appSettingsRepository, m_serversRepository); m_updateController = new UpdateController(m_appSettingsRepository, this); m_installController = new InstallController(m_serversRepository, m_appSettingsRepository, this); @@ -165,7 +164,7 @@ void CoreController::initControllers() setQmlContextProperty("FocusController", m_focusController); } - m_installUiController = new InstallUiController(m_installController, m_serversController, m_settingsController, m_protocolsModel, m_usersController, + m_installUiController = new InstallUiController(m_installController, m_serversController, m_settingsController, m_protocolsModel, m_usersController, m_awgConfigModel, m_wireGuardConfigModel, m_openVpnConfigModel, m_xrayConfigModel, m_torConfigModel, #ifdef Q_OS_WINDOWS m_ikev2ConfigModel, @@ -262,9 +261,12 @@ void CoreController::initSignalHandlers() { m_signalHandlers = new CoreSignalHandlers(this, this); m_signalHandlers->initAllHandlers(); - + // Trigger initial update after handlers are connected m_serversUiController->updateModel(); + if (m_serversUiController->hasServersFromGatewayApi()) { + m_apiNewsUiController->fetchNews(false); + } } void CoreController::updateTranslator(const QLocale &locale) @@ -322,11 +324,16 @@ PageController* CoreController::pageController() const void CoreController::openConnectionByIndex(int serverIndex) { + const QString serverId = + m_serversUiController ? m_serversUiController->getServerId(serverIndex) : QString(); + if (serverId.isEmpty()) { + return; + } if (m_serversModel) { m_serversModel->setProcessedServerIndex(serverIndex); } if (m_serversController) { - m_serversController->setDefaultServerIndex(serverIndex); + m_serversController->setDefaultServer(serverId); } m_connectionUiController->toggleConnection(); } diff --git a/client/core/controllers/coreController.h b/client/core/controllers/coreController.h index 0d77c5167..4b3ed2831 100644 --- a/client/core/controllers/coreController.h +++ b/client/core/controllers/coreController.h @@ -84,7 +84,6 @@ class TestDefaultServerChange; class TestServerEdgeCases; class TestSignalOrder; class TestServersModelSync; -class TestGatewayStacks; class TestComplexOperations; class TestSettingsSignals; class TestUiServersModelAndController; @@ -101,7 +100,6 @@ class CoreController : public QObject friend class TestServerEdgeCases; friend class TestSignalOrder; friend class TestServersModelSync; - friend class TestGatewayStacks; friend class TestComplexOperations; friend class TestSettingsSignals; friend class TestUiServersModelAndController; diff --git a/client/core/controllers/coreSignalHandlers.cpp b/client/core/controllers/coreSignalHandlers.cpp index 449a8af1d..934f20f6a 100644 --- a/client/core/controllers/coreSignalHandlers.cpp +++ b/client/core/controllers/coreSignalHandlers.cpp @@ -7,6 +7,7 @@ #include "core/utils/routeModes.h" #include "core/controllers/coreController.h" #include "core/repositories/secureServersRepository.h" +#include "core/utils/serverConfigUtils.h" #include "core/repositories/secureAppSettingsRepository.h" #include "vpnConnection.h" #include "ui/controllers/qml/pageController.h" @@ -65,7 +66,6 @@ void CoreSignalHandlers::initAllHandlers() initImportControllerHandler(); initApiCountryModelUpdateHandler(); initSubscriptionRefreshHandler(); - initContainerModelUpdateHandler(); initAdminConfigRevokedHandler(); initPassphraseRequestHandler(); initTranslationsUpdatedHandler(); @@ -78,6 +78,7 @@ void CoreSignalHandlers::initAllHandlers() initAllowedDnsModelUpdateHandler(); initAppSplitTunnelingModelUpdateHandler(); initPrepareConfigHandler(); + initUnsupportedConnectDrawerHandler(); initStrictKillSwitchHandler(); initAndroidSettingsHandler(); initAndroidConnectionHandler(); @@ -124,11 +125,9 @@ void CoreSignalHandlers::initInstallControllerHandler() { connect(m_coreController->m_installController, &InstallController::serverIsBusy, m_coreController->m_installUiController, &InstallUiController::serverIsBusy); connect(m_coreController->m_installUiController, &InstallUiController::cancelInstallation, m_coreController->m_installController, &InstallController::cancelInstallation); - connect(m_coreController->m_installUiController, &InstallUiController::currentContainerUpdated, m_coreController->m_connectionUiController, - &ConnectionUiController::onCurrentContainerUpdated); connect(m_coreController->m_serversUiController, &ServersUiController::processedServerIndexChanged, - m_coreController->m_installUiController, [this](int index) { - if (index >= 0) { + m_coreController->m_installUiController, [this](int serverIndex) { + if (serverIndex >= 0) { m_coreController->m_installUiController->clearProcessedServerCredentials(); } }); @@ -137,20 +136,20 @@ void CoreSignalHandlers::initInstallControllerHandler() void CoreSignalHandlers::initExportControllerHandler() { connect(m_coreController->m_exportController, &ExportController::appendClientRequested, this, - [this](int serverIndex, const QString &clientId, const QString &clientName, DockerContainer container) { - m_coreController->m_usersController->appendClient(serverIndex, clientId, clientName, container); + [this](const QString &serverId, const QString &clientId, const QString &clientName, DockerContainer container) { + m_coreController->m_usersController->appendClient(serverId, clientId, clientName, container); }); connect(m_coreController->m_exportController, &ExportController::updateClientsRequested, this, - [this](int serverIndex, DockerContainer container) { - m_coreController->m_usersController->updateClients(serverIndex, container); + [this](const QString &serverId, DockerContainer container) { + m_coreController->m_usersController->updateClients(serverId, container); }); connect(m_coreController->m_exportController, &ExportController::revokeClientRequested, this, - [this](int serverIndex, int row, DockerContainer container) { - m_coreController->m_usersController->revokeClient(serverIndex, row, container); + [this](const QString &serverId, int row, DockerContainer container) { + m_coreController->m_usersController->revokeClient(serverId, row, container); }); connect(m_coreController->m_exportController, &ExportController::renameClientRequested, this, - [this](int serverIndex, int row, const QString &clientName, DockerContainer container) { - m_coreController->m_usersController->renameClient(serverIndex, row, clientName, container); + [this](const QString &serverId, int row, const QString &clientName, DockerContainer container) { + m_coreController->m_usersController->renameClient(serverId, row, clientName, container); }); } @@ -159,9 +158,12 @@ void CoreSignalHandlers::initImportControllerHandler() connect(m_coreController->m_importCoreController, &ImportController::importFinished, this, [this]() { if (!m_coreController->m_connectionController->isConnected()) { int newServerIndex = m_coreController->m_serversController->getServersCount() - 1; - m_coreController->m_serversController->setDefaultServerIndex(newServerIndex); + const QString serverId = m_coreController->m_serversController->getServerId(newServerIndex); + if (!serverId.isEmpty()) { + m_coreController->m_serversController->setDefaultServer(serverId); + } if (m_coreController->m_serversUiController) { - m_coreController->m_serversUiController->setProcessedServerIndex(newServerIndex); + m_coreController->m_serversUiController->setProcessedServerId(serverId); } } }); @@ -170,21 +172,18 @@ void CoreSignalHandlers::initImportControllerHandler() void CoreSignalHandlers::initApiCountryModelUpdateHandler() { connect(m_coreController->m_serversUiController, &ServersUiController::updateApiCountryModel, this, [this]() { - int processedIndex = m_coreController->m_serversUiController->getProcessedServerIndex(); - if (processedIndex < 0 || processedIndex >= m_coreController->m_serversRepository->serversCount()) { + const QString processedServerId = m_coreController->m_serversUiController->getProcessedServerId(); + if (processedServerId.isEmpty()) { return; } - ServerConfig server = m_coreController->m_serversRepository->server(processedIndex); QJsonArray availableCountries; QString serverCountryCode; - - if (server.isApiV2()) { - const ApiV2ServerConfig* apiV2 = server.as(); - if (apiV2) { - availableCountries = apiV2->apiConfig.availableCountries; - serverCountryCode = apiV2->apiConfig.serverCountryCode; - } + + const auto apiV2 = m_coreController->m_serversRepository->apiV2Config(processedServerId); + if (apiV2.has_value()) { + availableCountries = apiV2->apiConfig.availableCountries; + serverCountryCode = apiV2->apiConfig.serverCountryCode; } m_coreController->m_apiCountryModel->updateModel(availableCountries, serverCountryCode); @@ -194,18 +193,9 @@ void CoreSignalHandlers::initApiCountryModelUpdateHandler() void CoreSignalHandlers::initSubscriptionRefreshHandler() { connect(m_coreController->m_subscriptionUiController, &SubscriptionUiController::subscriptionRefreshNeeded, this, [this]() { - const int defaultServerIndex = m_coreController->m_serversController->getDefaultServerIndex(); - if (defaultServerIndex >= 0) { - m_coreController->m_subscriptionUiController->getAccountInfo(defaultServerIndex, false); - } - }); -} - -void CoreSignalHandlers::initContainerModelUpdateHandler() -{ - connect(m_coreController->m_serversController, &ServersController::gatewayStacksExpanded, this, [this]() { - if (m_coreController->m_serversUiController->hasServersFromGatewayApi()) { - m_coreController->m_apiNewsUiController->fetchNews(false); + const QString defaultServerId = m_coreController->m_serversController->getDefaultServerId(); + if (!defaultServerId.isEmpty()) { + m_coreController->m_subscriptionUiController->getAccountInfo(defaultServerId, false); } }); } @@ -213,17 +203,17 @@ void CoreSignalHandlers::initContainerModelUpdateHandler() void CoreSignalHandlers::initAdminConfigRevokedHandler() { connect(m_coreController->m_installController, &InstallController::clientRevocationRequested, this, - [this](int serverIndex, const ContainerConfig &containerConfig, DockerContainer container) { - m_coreController->m_usersController->revokeClient(serverIndex, containerConfig, container); + [this](const QString &serverId, const ContainerConfig &containerConfig, DockerContainer container) { + m_coreController->m_usersController->revokeClient(serverId, containerConfig, container); }); connect(m_coreController->m_installController, &InstallController::clientAppendRequested, this, - [this](int serverIndex, const QString &clientId, const QString &clientName, DockerContainer container) { - m_coreController->m_usersController->appendClient(serverIndex, clientId, clientName, container); + [this](const QString &serverId, const QString &clientId, const QString &clientName, DockerContainer container) { + m_coreController->m_usersController->appendClient(serverId, clientId, clientName, container); }); - connect(m_coreController->m_usersController, &UsersController::adminConfigRevoked, m_coreController->m_serversController, - &ServersController::clearCachedProfile); + connect(m_coreController->m_usersController, &UsersController::adminConfigRevoked, m_coreController->m_installController, + &InstallController::clearCachedProfile); } void CoreSignalHandlers::initPassphraseRequestHandler() @@ -251,7 +241,8 @@ void CoreSignalHandlers::initLanguageHandler() void CoreSignalHandlers::initAutoConnectHandler() { - if (m_coreController->m_settingsUiController->isAutoConnectEnabled() && m_coreController->m_serversController->getDefaultServerIndex() >= 0) { + if (m_coreController->m_settingsUiController->isAutoConnectEnabled() + && !m_coreController->m_serversController->getDefaultServerId().isEmpty()) { QTimer::singleShot(1000, this, [this]() { m_coreController->m_connectionUiController->openConnection(); }); } } @@ -271,16 +262,20 @@ void CoreSignalHandlers::initServersModelUpdateHandler() m_coreController->m_serversUiController, &ServersUiController::updateModel); connect(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged, m_coreController->m_serversUiController, &ServersUiController::onDefaultServerChanged); - - connect(m_coreController->m_serversRepository, &SecureServersRepository::serverAdded, - m_coreController->m_serversController, &ServersController::recomputeGatewayStacks); - connect(m_coreController->m_serversRepository, &SecureServersRepository::serverEdited, - m_coreController->m_serversController, &ServersController::recomputeGatewayStacks); - connect(m_coreController->m_serversRepository, &SecureServersRepository::serverRemoved, - m_coreController->m_serversController, &ServersController::recomputeGatewayStacks); - - connect(m_coreController->m_settingsUiController, &SettingsUiController::restoreBackupFinished, - m_coreController->m_serversUiController, &ServersUiController::updateModel); + + connect(m_coreController->m_serversRepository, &SecureServersRepository::serverAdded, this, + [this](const QString &serverId) { + if (m_coreController->m_serversRepository->apiV2Config(serverId).has_value()) { + m_coreController->m_apiNewsUiController->fetchNews(false); + } + }); + + connect(m_coreController->m_settingsUiController, &SettingsUiController::restoreBackupFinished, this, [this]() { + m_coreController->m_serversUiController->updateModel(); + if (m_coreController->m_serversUiController->hasServersFromGatewayApi()) { + m_coreController->m_apiNewsUiController->fetchNews(false); + } + }); } void CoreSignalHandlers::initClientManagementModelUpdateHandler() @@ -315,7 +310,19 @@ void CoreSignalHandlers::initPrepareConfigHandler() connect(m_coreController->m_connectionUiController, &ConnectionUiController::prepareConfig, this, [this]() { m_coreController->m_connectionController->setConnectionState(Vpn::ConnectionState::Preparing); - m_coreController->m_subscriptionUiController->validateConfig(); + const QString serverId = m_coreController->m_serversController->getDefaultServerId(); + if (serverId.isEmpty()) { + m_coreController->m_connectionController->setConnectionState(Vpn::ConnectionState::Disconnected); + return; + } + + const serverConfigUtils::ConfigType kind = m_coreController->m_serversRepository->serverKind(serverId); + + if (serverConfigUtils::isApiV2Subscription(kind) || serverConfigUtils::isLegacyApiSubscription(kind)) { + m_coreController->m_subscriptionUiController->validateConfig(); + } else { + m_coreController->m_installUiController->validateConfig(); + } }); connect(m_coreController->m_subscriptionUiController, &SubscriptionUiController::configValidated, this, [this](bool isValid) { @@ -324,7 +331,7 @@ void CoreSignalHandlers::initPrepareConfigHandler() return; } - m_coreController->m_installUiController->validateConfig(); + m_coreController->m_connectionUiController->openConnection(); }); connect(m_coreController->m_installUiController, &InstallUiController::configValidated, this, [this](bool isValid) { @@ -337,6 +344,12 @@ void CoreSignalHandlers::initPrepareConfigHandler() }); } +void CoreSignalHandlers::initUnsupportedConnectDrawerHandler() +{ + connect(m_coreController->m_subscriptionUiController, &SubscriptionUiController::unsupportedConnectDrawerRequested, + m_coreController->m_pageController, &PageController::unsupportedConnectDrawerRequested); +} + void CoreSignalHandlers::initStrictKillSwitchHandler() { connect(m_coreController->m_settingsUiController, &SettingsUiController::strictKillSwitchEnabledChanged, m_coreController->m_connectionController, @@ -348,7 +361,10 @@ void CoreSignalHandlers::initAndroidSettingsHandler() #ifdef Q_OS_ANDROID connect(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::saveLogsChanged, AndroidController::instance(), &AndroidController::setSaveLogs); connect(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::screenshotsEnabledChanged, AndroidController::instance(), &AndroidController::setScreenshotsEnabled); - connect(m_coreController->m_serversRepository, &SecureServersRepository::serverRemoved, AndroidController::instance(), &AndroidController::resetLastServer); + connect(m_coreController->m_serversRepository, &SecureServersRepository::serverRemoved, this, + [](const QString &/*serverId*/, int removedIndex) { + AndroidController::instance()->resetLastServer(removedIndex); + }); connect(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::settingsCleared, []() { AndroidController::instance()->resetLastServer(-1); }); #endif } diff --git a/client/core/controllers/coreSignalHandlers.h b/client/core/controllers/coreSignalHandlers.h index 51f1d2f1d..2f5d59976 100644 --- a/client/core/controllers/coreSignalHandlers.h +++ b/client/core/controllers/coreSignalHandlers.h @@ -21,7 +21,6 @@ private: void initImportControllerHandler(); void initApiCountryModelUpdateHandler(); void initSubscriptionRefreshHandler(); - void initContainerModelUpdateHandler(); void initAdminConfigRevokedHandler(); void initPassphraseRequestHandler(); void initTranslationsUpdatedHandler(); @@ -34,6 +33,7 @@ private: void initAllowedDnsModelUpdateHandler(); void initAppSplitTunnelingModelUpdateHandler(); void initPrepareConfigHandler(); + void initUnsupportedConnectDrawerHandler(); void initStrictKillSwitchHandler(); void initAndroidSettingsHandler(); void initAndroidConnectionHandler(); diff --git a/client/core/controllers/gatewayController.cpp b/client/core/controllers/gatewayController.cpp index 23ced44f3..9b22ad6ad 100644 --- a/client/core/controllers/gatewayController.cpp +++ b/client/core/controllers/gatewayController.cpp @@ -239,7 +239,7 @@ QFuture> GatewayController::postAsync(const QString connect(reply, &QNetworkReply::sslErrors, [sslErrors](const QList &errors) { *sslErrors = errors; }); - connect(reply, &QNetworkReply::finished, reply, [promise, sslErrors, encRequestData, endpoint, apiPayload, reply, this]() mutable { + connect(reply, &QNetworkReply::finished, this, [promise, sslErrors, encRequestData, endpoint, apiPayload, reply, this]() mutable { QByteArray encryptedResponseBody = reply->readAll(); QString replyErrorString = reply->errorString(); auto replyError = reply->error(); diff --git a/client/core/controllers/selfhosted/exportController.cpp b/client/core/controllers/selfhosted/exportController.cpp index 917304c49..095b57353 100644 --- a/client/core/controllers/selfhosted/exportController.cpp +++ b/client/core/controllers/selfhosted/exportController.cpp @@ -5,14 +5,13 @@ #include "core/configurators/configuratorBase.h" #include "core/utils/selfhosted/sshSession.h" -#include "core/utils/networkUtilities.h" #include "core/utils/qrCodeUtils.h" #include "core/utils/serialization/serialization.h" #include "core/utils/protocolEnum.h" #include "core/protocols/protocolUtils.h" #include "core/utils/constants/configKeys.h" #include "core/utils/constants/protocolConstants.h" -#include "core/models/serverConfig.h" +#include "core/models/selfhosted/selfHostedAdminServerConfig.h" #include "core/models/containerConfig.h" #include "core/models/protocolConfig.h" @@ -27,18 +26,20 @@ ExportController::ExportController(SecureServersRepository* serversRepository, { } -ExportController::ExportResult ExportController::generateFullAccessConfig(int serverIndex) +ExportController::ExportResult ExportController::generateFullAccessConfig(const QString &serverId) { ExportResult result; - ServerConfig serverConfig = m_serversRepository->server(serverIndex); - serverConfig.visit([](auto& arg) { - for (auto it = arg.containers.begin(); it != arg.containers.end(); ++it) { - it.value().protocolConfig.clearClientConfig(); - } - }); + auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId); + if (!adminConfig.has_value()) { + result.errorCode = ErrorCode::InternalError; + return result; + } + for (auto it = adminConfig->containers.begin(); it != adminConfig->containers.end(); ++it) { + it.value().protocolConfig.clearClientConfig(); + } - QJsonObject serverJson = serverConfig.toJson(); + QJsonObject serverJson = adminConfig->toJson(); QByteArray compressedConfig = QJsonDocument(serverJson).toJson(); compressedConfig = qCompress(compressedConfig, 8); result.config = generateVpnUrl(compressedConfig); @@ -47,13 +48,22 @@ ExportController::ExportResult ExportController::generateFullAccessConfig(int se return result; } -ExportController::ExportResult ExportController::generateConnectionConfig(int serverIndex, int containerIndex, const QString &clientName) +ExportController::ExportResult ExportController::generateConnectionConfig(const QString &serverId, int containerIndex, const QString &clientName) { ExportResult result; DockerContainer container = static_cast(containerIndex); - ServerCredentials credentials = m_serversRepository->serverCredentials(serverIndex); - ContainerConfig containerConfig = m_serversRepository->containerConfig(serverIndex, container); + auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId); + if (!adminConfig.has_value()) { + result.errorCode = ErrorCode::InternalError; + return result; + } + const ServerCredentials credentials = adminConfig->credentials(); + if (!credentials.isValid()) { + result.errorCode = ErrorCode::InternalError; + return result; + } + ContainerConfig containerConfig = adminConfig->containerConfig(container); if (ContainerUtils::containerService(container) != ServiceType::Other) { SshSession sshSession; @@ -74,35 +84,25 @@ ExportController::ExportResult ExportController::generateConnectionConfig(int se QString clientId = newProtocolConfig.clientId(); if (!clientId.isEmpty()) { - emit appendClientRequested(serverIndex, clientId, clientName, container); + emit appendClientRequested(serverId, clientId, clientName, container); } } - ServerConfig serverConfig = m_serversRepository->server(serverIndex); - serverConfig.visit([container, containerConfig](auto& arg) { - arg.containers.clear(); - arg.containers[container] = containerConfig; - arg.defaultContainer = container; - }); + const QPair dns = adminConfig->getDnsPair(m_appSettingsRepository->useAmneziaDns(), + m_appSettingsRepository->primaryDns(), + m_appSettingsRepository->secondaryDns()); - if (serverConfig.isSelfHosted()) { - SelfHostedServerConfig* selfHosted = serverConfig.as(); - if (selfHosted) { - selfHosted->userName.reset(); - selfHosted->password.reset(); - selfHosted->port.reset(); - } - } + adminConfig->containers.clear(); + adminConfig->containers[container] = containerConfig; + adminConfig->defaultContainer = container; + adminConfig->userName.clear(); + adminConfig->password.clear(); + adminConfig->port = 0; - auto dns = serverConfig.getDnsPair(m_appSettingsRepository->useAmneziaDns(), - m_appSettingsRepository->primaryDns(), - m_appSettingsRepository->secondaryDns()); - serverConfig.visit([&dns](auto& arg) { - arg.dns1 = dns.first; - arg.dns2 = dns.second; - }); + adminConfig->dns1 = dns.first; + adminConfig->dns2 = dns.second; - QJsonObject serverJson = serverConfig.toJson(); + QJsonObject serverJson = adminConfig->toJson(); QByteArray compressedConfig = QJsonDocument(serverJson).toJson(); compressedConfig = qCompress(compressedConfig, 8); result.config = generateVpnUrl(compressedConfig); @@ -111,7 +111,7 @@ ExportController::ExportResult ExportController::generateConnectionConfig(int se return result; } -ExportController::NativeConfigResult ExportController::generateNativeConfig(int serverIndex, DockerContainer container, +ExportController::NativeConfigResult ExportController::generateNativeConfig(const QString &serverId, DockerContainer container, const ContainerConfig &containerConfig, const QString &clientName) { @@ -123,11 +123,19 @@ ExportController::NativeConfigResult ExportController::generateNativeConfig(int Proto protocol = ContainerUtils::defaultProtocol(container); - ServerCredentials credentials = m_serversRepository->serverCredentials(serverIndex); - ServerConfig serverConfig = m_serversRepository->server(serverIndex); - auto dns = serverConfig.getDnsPair(m_appSettingsRepository->useAmneziaDns(), - m_appSettingsRepository->primaryDns(), - m_appSettingsRepository->secondaryDns()); + auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId); + if (!adminConfig.has_value()) { + result.errorCode = ErrorCode::InternalError; + return result; + } + const ServerCredentials credentials = adminConfig->credentials(); + if (!credentials.isValid()) { + result.errorCode = ErrorCode::InternalError; + return result; + } + const QPair dns = adminConfig->getDnsPair(m_appSettingsRepository->useAmneziaDns(), + m_appSettingsRepository->primaryDns(), + m_appSettingsRepository->secondaryDns()); ContainerConfig modifiedContainerConfig = containerConfig; modifiedContainerConfig.container = container; @@ -157,20 +165,25 @@ ExportController::NativeConfigResult ExportController::generateNativeConfig(int if (protocol == Proto::OpenVpn || protocol == Proto::WireGuard || protocol == Proto::Awg || protocol == Proto::Xray) { QString clientId = newProtocolConfig.clientId(); if (!clientId.isEmpty()) { - emit appendClientRequested(serverIndex, clientId, clientName, container); + emit appendClientRequested(serverId, clientId, clientName, container); } } return result; } -ExportController::ExportResult ExportController::generateOpenVpnConfig(int serverIndex, const QString &clientName) +ExportController::ExportResult ExportController::generateOpenVpnConfig(const QString &serverId, const QString &clientName) { ExportResult result; DockerContainer container = DockerContainer::OpenVpn; - ContainerConfig containerConfig = m_serversRepository->containerConfig(serverIndex, container); + auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId); + if (!adminConfig.has_value()) { + result.errorCode = ErrorCode::InternalError; + return result; + } + ContainerConfig containerConfig = adminConfig->containerConfig(container); - auto nativeResult = generateNativeConfig(serverIndex, container, containerConfig, clientName); + auto nativeResult = generateNativeConfig(serverId, container, containerConfig, clientName); if (nativeResult.errorCode != ErrorCode::NoError) { result.errorCode = nativeResult.errorCode; return result; @@ -185,13 +198,18 @@ ExportController::ExportResult ExportController::generateOpenVpnConfig(int serve return result; } -ExportController::ExportResult ExportController::generateWireGuardConfig(int serverIndex, const QString &clientName) +ExportController::ExportResult ExportController::generateWireGuardConfig(const QString &serverId, const QString &clientName) { ExportResult result; - ContainerConfig containerConfig = m_serversRepository->containerConfig(serverIndex, DockerContainer::WireGuard); + auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId); + if (!adminConfig.has_value()) { + result.errorCode = ErrorCode::InternalError; + return result; + } + ContainerConfig containerConfig = adminConfig->containerConfig(DockerContainer::WireGuard); - auto nativeResult = generateNativeConfig(serverIndex, DockerContainer::WireGuard, containerConfig, clientName); + auto nativeResult = generateNativeConfig(serverId, DockerContainer::WireGuard, containerConfig, clientName); if (nativeResult.errorCode != ErrorCode::NoError) { result.errorCode = nativeResult.errorCode; return result; @@ -206,7 +224,7 @@ ExportController::ExportResult ExportController::generateWireGuardConfig(int ser return result; } -ExportController::ExportResult ExportController::generateAwgConfig(int serverIndex, int containerIndex, const QString &clientName) +ExportController::ExportResult ExportController::generateAwgConfig(const QString &serverId, int containerIndex, const QString &clientName) { ExportResult result; @@ -215,9 +233,14 @@ ExportController::ExportResult ExportController::generateAwgConfig(int serverInd result.errorCode = ErrorCode::InternalError; return result; } - ContainerConfig containerConfig = m_serversRepository->containerConfig(serverIndex, container); + auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId); + if (!adminConfig.has_value()) { + result.errorCode = ErrorCode::InternalError; + return result; + } + ContainerConfig containerConfig = adminConfig->containerConfig(container); - auto nativeResult = generateNativeConfig(serverIndex, container, containerConfig, clientName); + auto nativeResult = generateNativeConfig(serverId, container, containerConfig, clientName); if (nativeResult.errorCode != ErrorCode::NoError) { result.errorCode = nativeResult.errorCode; return result; @@ -233,13 +256,18 @@ ExportController::ExportResult ExportController::generateAwgConfig(int serverInd } -ExportController::ExportResult ExportController::generateXrayConfig(int serverIndex, const QString &clientName) +ExportController::ExportResult ExportController::generateXrayConfig(const QString &serverId, const QString &clientName) { ExportResult result; - ContainerConfig containerConfig = m_serversRepository->containerConfig(serverIndex, DockerContainer::Xray); + auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId); + if (!adminConfig.has_value()) { + result.errorCode = ErrorCode::InternalError; + return result; + } + ContainerConfig containerConfig = adminConfig->containerConfig(DockerContainer::Xray); - auto nativeResult = generateNativeConfig(serverIndex, DockerContainer::Xray, containerConfig, clientName); + auto nativeResult = generateNativeConfig(serverId, DockerContainer::Xray, containerConfig, clientName); if (nativeResult.errorCode != ErrorCode::NoError) { result.errorCode = nativeResult.errorCode; return result; @@ -302,22 +330,22 @@ ExportController::ExportResult ExportController::generateXrayConfig(int serverIn return result; } -void ExportController::updateClientManagementModel(int serverIndex, int containerIndex) +void ExportController::updateClientManagementModel(const QString &serverId, int containerIndex) { DockerContainer container = static_cast(containerIndex); - emit updateClientsRequested(serverIndex, container); + emit updateClientsRequested(serverId, container); } -void ExportController::revokeConfig(int row, int serverIndex, int containerIndex) +void ExportController::revokeConfig(int row, const QString &serverId, int containerIndex) { DockerContainer container = static_cast(containerIndex); - emit revokeClientRequested(serverIndex, row, container); + emit revokeClientRequested(serverId, row, container); } -void ExportController::renameClient(int row, const QString &clientName, int serverIndex, int containerIndex) +void ExportController::renameClient(int row, const QString &clientName, const QString &serverId, int containerIndex) { DockerContainer container = static_cast(containerIndex); - emit renameClientRequested(serverIndex, row, clientName, container); + emit renameClientRequested(serverId, row, clientName, container); } QString ExportController::generateVpnUrl(const QByteArray &compressedConfig) diff --git a/client/core/controllers/selfhosted/exportController.h b/client/core/controllers/selfhosted/exportController.h index 2f2648c1c..e89ef7336 100644 --- a/client/core/controllers/selfhosted/exportController.h +++ b/client/core/controllers/selfhosted/exportController.h @@ -37,23 +37,23 @@ public: SecureAppSettingsRepository* appSettingsRepository, QObject *parent = nullptr); - ExportResult generateFullAccessConfig(int serverIndex); - ExportResult generateConnectionConfig(int serverIndex, int containerIndex, const QString &clientName); - ExportResult generateOpenVpnConfig(int serverIndex, const QString &clientName); - ExportResult generateWireGuardConfig(int serverIndex, const QString &clientName); - ExportResult generateAwgConfig(int serverIndex, int containerIndex, const QString &clientName); - ExportResult generateXrayConfig(int serverIndex, const QString &clientName); + ExportResult generateFullAccessConfig(const QString &serverId); + ExportResult generateConnectionConfig(const QString &serverId, int containerIndex, const QString &clientName); + ExportResult generateOpenVpnConfig(const QString &serverId, const QString &clientName); + ExportResult generateWireGuardConfig(const QString &serverId, const QString &clientName); + ExportResult generateAwgConfig(const QString &serverId, int containerIndex, const QString &clientName); + ExportResult generateXrayConfig(const QString &serverId, const QString &clientName); signals: - void appendClientRequested(int serverIndex, const QString &clientId, const QString &clientName, DockerContainer container); - void updateClientsRequested(int serverIndex, DockerContainer container); - void revokeClientRequested(int serverIndex, int row, DockerContainer container); - void renameClientRequested(int serverIndex, int row, const QString &clientName, DockerContainer container); + void appendClientRequested(const QString &serverId, const QString &clientId, const QString &clientName, DockerContainer container); + void updateClientsRequested(const QString &serverId, DockerContainer container); + void revokeClientRequested(const QString &serverId, int row, DockerContainer container); + void renameClientRequested(const QString &serverId, int row, const QString &clientName, DockerContainer container); public slots: - void updateClientManagementModel(int serverIndex, int containerIndex); - void revokeConfig(int row, int serverIndex, int containerIndex); - void renameClient(int row, const QString &clientName, int serverIndex, int containerIndex); + void updateClientManagementModel(const QString &serverId, int containerIndex); + void revokeConfig(int row, const QString &serverId, int containerIndex); + void renameClient(int row, const QString &clientName, const QString &serverId, int containerIndex); private: struct NativeConfigResult @@ -62,7 +62,7 @@ private: QJsonObject jsonNativeConfig; }; - NativeConfigResult generateNativeConfig(int serverIndex, DockerContainer container, + NativeConfigResult generateNativeConfig(const QString &serverId, DockerContainer container, const ContainerConfig &containerConfig, const QString &clientName); diff --git a/client/core/controllers/selfhosted/importController.cpp b/client/core/controllers/selfhosted/importController.cpp index c1c7503eb..bf31e1312 100644 --- a/client/core/controllers/selfhosted/importController.cpp +++ b/client/core/controllers/selfhosted/importController.cpp @@ -16,7 +16,7 @@ #include "core/utils/containerEnum.h" #include "core/utils/containers/containerUtils.h" #include "core/utils/protocolEnum.h" -#include "core/utils/api/apiEnums.h" +#include "core/utils/serverConfigUtils.h" #include "core/utils/constants/apiKeys.h" #include "core/utils/constants/apiConstants.h" #include "core/utils/api/apiUtils.h" @@ -27,7 +27,6 @@ #include "core/utils/constants/configKeys.h" #include "core/utils/constants/protocolConstants.h" #include "core/utils/qrCodeUtils.h" -#include "core/models/serverConfig.h" using namespace amnezia; using namespace ProtocolUtils; @@ -208,12 +207,18 @@ ImportController::ImportResult ImportController::extractConfigFromData(const QSt case ConfigTypes::Amnezia: { result.config = QJsonDocument::fromJson(config.toUtf8()).object(); - if (apiUtils::isServerFromApi(result.config)) { + if (serverConfigUtils::isServerFromApi(result.config)) { auto apiConfig = result.config.value(apiDefs::key::apiConfig).toObject(); apiConfig[apiDefs::key::vpnKey] = data; result.config[apiDefs::key::apiConfig] = apiConfig; } + if (serverConfigUtils::isLegacyApiSubscription(serverConfigUtils::configTypeFromJson(result.config))) { + result.errorCode = ErrorCode::LegacyApiV1NotSupportedError; + result.config = {}; + return result; + } + processAmneziaConfig(result.config); if (!result.config.empty()) { checkForMaliciousStrings(result.config, result.maliciousWarningText); @@ -381,18 +386,29 @@ void ImportController::importConfig(const QJsonObject &config) credentials.secretData = config.value(configKey::password).toString(); if (credentials.isValid() || config.contains(configKey::containers)) { - ServerConfig serverConfig = ServerConfig::fromJson(config); - m_serversRepository->addServer(serverConfig); + m_serversRepository->addServer(QString(), config, serverConfigUtils::configTypeFromJson(config)); emit importFinished(); } else if (config.contains(configKey::configVersion)) { quint16 crc = qChecksum(QJsonDocument(config).toJson()); - if (m_serversRepository->hasServerWithCrc(crc)) { + bool hasServerWithCrc = false; + const QVector ids = m_serversRepository->orderedServerIds(); + for (const QString &id : ids) { + const auto apiV2 = m_serversRepository->apiV2Config(id); + if (!apiV2.has_value()) { + continue; + } + if (static_cast(apiV2->crc) == crc) { + hasServerWithCrc = true; + break; + } + } + + if (hasServerWithCrc) { emit importErrorOccurred(ErrorCode::ApiConfigAlreadyAdded, true); } else { QJsonObject configWithCrc = config; configWithCrc.insert(configKey::crc, crc); - ServerConfig serverConfig = ServerConfig::fromJson(configWithCrc); - m_serversRepository->addServer(serverConfig); + m_serversRepository->addServer(QString(), configWithCrc, serverConfigUtils::configTypeFromJson(configWithCrc)); emit importFinished(); } } else { diff --git a/client/core/controllers/selfhosted/installController.cpp b/client/core/controllers/selfhosted/installController.cpp index c862f4723..2a45ba789 100644 --- a/client/core/controllers/selfhosted/installController.cpp +++ b/client/core/controllers/selfhosted/installController.cpp @@ -33,7 +33,6 @@ #include "core/protocols/protocolUtils.h" #include "core/utils/constants/configKeys.h" #include "core/utils/constants/protocolConstants.h" -#include "core/models/serverConfig.h" #include "core/models/containerConfig.h" #include "core/models/protocols/awgProtocolConfig.h" #include "ui/models/protocols/wireguardConfigModel.h" @@ -129,15 +128,27 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials return startupContainerWorker(credentials, container, config, sshSession); } -ErrorCode InstallController::updateContainer(int serverIndex, DockerContainer container, const ContainerConfig &oldConfig, +ErrorCode InstallController::updateContainer(const QString &serverId, DockerContainer container, const ContainerConfig &oldConfig, ContainerConfig &newConfig) { if (!isUpdateDockerContainerRequired(container, oldConfig, newConfig)) { - m_serversRepository->setContainerConfig(serverIndex, container, newConfig); + auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId); + if (!adminConfig.has_value()) { + return ErrorCode::InternalError; + } + adminConfig->updateContainerConfig(container, newConfig); + m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin); return ErrorCode::NoError; } - ServerCredentials credentials = m_serversRepository->serverCredentials(serverIndex); + auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId); + if (!adminConfig.has_value()) { + return ErrorCode::InternalError; + } + ServerCredentials credentials = adminConfig->credentials(); + if (!credentials.isValid()) { + return ErrorCode::InternalError; + } SshSession sshSession(this); bool reinstallRequired = isReinstallContainerRequired(container, oldConfig, newConfig); @@ -154,42 +165,51 @@ ErrorCode InstallController::updateContainer(int serverIndex, DockerContainer co } if (errorCode == ErrorCode::NoError) { - clearCachedProfile(serverIndex, container); - m_serversRepository->setContainerConfig(serverIndex, container, newConfig); + clearCachedProfile(serverId, container); + adminConfig->updateContainerConfig(container, newConfig); + m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin); } return errorCode; } -void InstallController::clearCachedProfile(int serverIndex, DockerContainer container) +void InstallController::clearCachedProfile(const QString &serverId, DockerContainer container) { if (ContainerUtils::containerService(container) == ServiceType::Other) { return; } - ContainerConfig containerConfigModel = m_serversRepository->containerConfig(serverIndex, container); - - m_serversRepository->clearLastConnectionConfig(serverIndex, container); - - emit clientRevocationRequested(serverIndex, containerConfigModel, container); -} - -ErrorCode InstallController::validateAndPrepareConfig(int serverIndex) -{ - ServerConfig serverConfigModel = m_serversRepository->server(serverIndex); - - if (serverConfigModel.isApiConfig()) { - return ErrorCode::NoError; + auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId); + if (!adminConfig.has_value()) { + return; } - DockerContainer container = serverConfigModel.defaultContainer(); + adminConfig->clearCachedClientProfile(container); + const ContainerConfig containerConfigModel = adminConfig->containerConfig(container); + + m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin); + + emit clientRevocationRequested(serverId, containerConfigModel, container); +} + +ErrorCode InstallController::validateAndPrepareConfig(const QString &serverId) +{ + auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId); + if (!adminConfig.has_value()) { + return ErrorCode::InternalError; + } + + DockerContainer container = adminConfig->defaultContainer; if (container == DockerContainer::None) { return ErrorCode::NoInstalledContainersError; } - ContainerConfig containerConfig = m_serversRepository->containerConfig(serverIndex, container); - ServerCredentials credentials = m_serversRepository->serverCredentials(serverIndex); + ContainerConfig containerConfig = adminConfig->containerConfig(container); + ServerCredentials credentials = adminConfig->credentials(); + if (!credentials.isValid()) { + return ErrorCode::InternalError; + } SshSession sshSession; auto isProtocolConfigExists = [](const ContainerConfig &cfg) { @@ -198,20 +218,21 @@ ErrorCode InstallController::validateAndPrepareConfig(int serverIndex) if (!isProtocolConfigExists(containerConfig)) { QString clientName = QString("Admin [%1]").arg(QSysInfo::prettyProductName()); - ErrorCode errorCode = processContainerForAdmin(container, containerConfig, credentials, sshSession, serverIndex, clientName); + ErrorCode errorCode = processContainerForAdmin(container, containerConfig, credentials, sshSession, serverId, clientName); if (errorCode != ErrorCode::NoError) { return errorCode; } - m_serversRepository->setContainerConfig(serverIndex, container, containerConfig); + adminConfig->updateContainerConfig(container, containerConfig); + m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin); } return ErrorCode::NoError; } -void InstallController::validateConfig(int serverIndex) +void InstallController::validateConfig(const QString &serverId) { - QFuture future = QtConcurrent::run([this, serverIndex]() { - return validateAndPrepareConfig(serverIndex); + QFuture future = QtConcurrent::run([this, serverId]() { + return validateAndPrepareConfig(serverId); }); auto *watcher = new QFutureWatcher(this); @@ -230,6 +251,21 @@ void InstallController::validateConfig(int serverIndex) watcher->setFuture(future); } +void InstallController::addEmptyServer(const ServerCredentials &credentials) +{ + SelfHostedAdminServerConfig serverConfig; + serverConfig.hostName = credentials.hostName; + serverConfig.userName = credentials.userName; + serverConfig.password = credentials.secretData; + serverConfig.port = credentials.port; + serverConfig.description = m_appSettingsRepository->nextAvailableServerName(); + serverConfig.displayName = serverConfig.description.isEmpty() ? serverConfig.hostName : serverConfig.description; + serverConfig.defaultContainer = DockerContainer::None; + + m_serversRepository->addServer(QString(), serverConfig.toJson(), + serverConfigUtils::ConfigType::SelfHostedAdmin); +} + ErrorCode InstallController::prepareContainerConfig(DockerContainer container, const ServerCredentials &credentials, ContainerConfig &containerConfig, SshSession &sshSession) { if (!ContainerUtils::isSupportedByCurrentPlatform(container)) { @@ -257,7 +293,7 @@ ErrorCode InstallController::prepareContainerConfig(DockerContainer container, c return ErrorCode::NoError; } -void InstallController::adminAppendRequested(int serverIndex, DockerContainer container, +void InstallController::adminAppendRequested(const QString &serverId, DockerContainer container, const ContainerConfig &containerConfig, const QString &clientName) { if (ContainerUtils::containerService(container) == ServiceType::Other @@ -266,13 +302,13 @@ void InstallController::adminAppendRequested(int serverIndex, DockerContainer co } QString clientId = containerConfig.protocolConfig.clientId(); if (!clientId.isEmpty()) { - emit clientAppendRequested(serverIndex, clientId, clientName, container); + emit clientAppendRequested(serverId, clientId, clientName, container); } } ErrorCode InstallController::processContainerForAdmin(DockerContainer container, ContainerConfig &containerConfig, const ServerCredentials &credentials, SshSession &sshSession, - int serverIndex, const QString &clientName) + const QString &serverId, const QString &clientName) { if (ContainerUtils::isSupportedByCurrentPlatform(container)) { ErrorCode errorCode = prepareContainerConfig(container, credentials, containerConfig, sshSession); @@ -280,7 +316,7 @@ ErrorCode InstallController::processContainerForAdmin(DockerContainer container, return errorCode; } } - adminAppendRequested(serverIndex, container, containerConfig, clientName); + adminAppendRequested(serverId, container, containerConfig, clientName); return ErrorCode::NoError; } @@ -688,9 +724,16 @@ ErrorCode InstallController::setupServerFirewall(const ServerCredentials &creden amnezia::genBaseVars(credentials, DockerContainer::None, QString(), QString()))); } -ErrorCode InstallController::rebootServer(int serverIndex) +ErrorCode InstallController::rebootServer(const QString &serverId) { - auto credentials = m_serversRepository->serverCredentials(serverIndex); + const auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId); + if (!adminConfig.has_value()) { + return ErrorCode::InternalError; + } + ServerCredentials credentials = adminConfig->credentials(); + if (!credentials.isValid()) { + return ErrorCode::InternalError; + } SshSession sshSession(this); QString script = QString("sudo reboot"); @@ -709,27 +752,38 @@ ErrorCode InstallController::rebootServer(int serverIndex) return sshSession.runScript(credentials, script, cbReadStdOut, cbReadStdErr); } -ErrorCode InstallController::removeAllContainers(int serverIndex) +ErrorCode InstallController::removeAllContainers(const QString &serverId) { - auto credentials = m_serversRepository->serverCredentials(serverIndex); + auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId); + if (!adminConfig.has_value()) { + return ErrorCode::InternalError; + } + ServerCredentials credentials = adminConfig->credentials(); + if (!credentials.isValid()) { + return ErrorCode::InternalError; + } SshSession sshSession(this); ErrorCode errorCode = sshSession.runScript(credentials, amnezia::scriptData(SharedScriptType::remove_all_containers)); if (errorCode == ErrorCode::NoError) { - ServerConfig serverConfigModel = m_serversRepository->server(serverIndex); - serverConfigModel.visit([](auto& arg) { - arg.containers.clear(); - arg.defaultContainer = DockerContainer::None; - }); - m_serversRepository->editServer(serverIndex, serverConfigModel); + adminConfig->containers.clear(); + adminConfig->defaultContainer = DockerContainer::None; + m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin); } return errorCode; } -ErrorCode InstallController::removeContainer(int serverIndex, DockerContainer container) +ErrorCode InstallController::removeContainer(const QString &serverId, DockerContainer container) { - auto credentials = m_serversRepository->serverCredentials(serverIndex); + auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId); + if (!adminConfig.has_value()) { + return ErrorCode::InternalError; + } + ServerCredentials credentials = adminConfig->credentials(); + if (!credentials.isValid()) { + return ErrorCode::InternalError; + } SshSession sshSession(this); ErrorCode errorCode = sshSession.runScript( credentials, @@ -737,11 +791,10 @@ ErrorCode InstallController::removeContainer(int serverIndex, DockerContainer co amnezia::genBaseVars(credentials, container, QString(), QString()))); if (errorCode == ErrorCode::NoError) { - ServerConfig serverConfigModel = m_serversRepository->server(serverIndex); - QMap containers = serverConfigModel.containers(); + QMap containers = adminConfig->containers; containers.remove(container); - - DockerContainer defaultContainer = serverConfigModel.defaultContainer(); + + DockerContainer defaultContainer = adminConfig->defaultContainer; if (defaultContainer == container) { if (containers.isEmpty()) { defaultContainer = DockerContainer::None; @@ -749,12 +802,10 @@ ErrorCode InstallController::removeContainer(int serverIndex, DockerContainer co defaultContainer = containers.begin().key(); } } - - serverConfigModel.visit([&containers, defaultContainer](auto& arg) { - arg.containers = containers; - arg.defaultContainer = defaultContainer; - }); - m_serversRepository->editServer(serverIndex, serverConfigModel); + + adminConfig->containers = containers; + adminConfig->defaultContainer = defaultContainer; + m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin); } return errorCode; @@ -815,9 +866,16 @@ bool InstallController::isUpdateDockerContainerRequired(DockerContainer containe return true; } -ErrorCode InstallController::scanServerForInstalledContainers(int serverIndex) +ErrorCode InstallController::scanServerForInstalledContainers(const QString &serverId) { - ServerCredentials credentials = m_serversRepository->serverCredentials(serverIndex); + auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId); + if (!adminConfig.has_value()) { + return ErrorCode::InternalError; + } + ServerCredentials credentials = adminConfig->credentials(); + if (!credentials.isValid()) { + return ErrorCode::InternalError; + } SshSession sshSession(this); QMap installedContainers; @@ -826,8 +884,7 @@ ErrorCode InstallController::scanServerForInstalledContainers(int serverIndex) return errorCode; } - ServerConfig serverConfigModel = m_serversRepository->server(serverIndex); - QMap containers = serverConfigModel.containers(); + QMap containers = adminConfig->containers; bool hasNewContainers = false; QString clientName = QString("Admin [%1]").arg(QSysInfo::prettyProductName()); @@ -835,29 +892,25 @@ ErrorCode InstallController::scanServerForInstalledContainers(int serverIndex) if (!containers.contains(iterator.key())) { ContainerConfig containerConfig = iterator.value(); errorCode = processContainerForAdmin(iterator.key(), containerConfig, credentials, sshSession, - serverIndex, clientName); + serverId, clientName); if (errorCode != ErrorCode::NoError) { return errorCode; } containers.insert(iterator.key(), containerConfig); hasNewContainers = true; - DockerContainer defaultContainer = serverConfigModel.defaultContainer(); + DockerContainer defaultContainer = adminConfig->defaultContainer; if (defaultContainer == DockerContainer::None && ContainerUtils::containerService(iterator.key()) != ServiceType::Other && ContainerUtils::isSupportedByCurrentPlatform(iterator.key())) { - serverConfigModel.visit([iterator](auto& arg) { - arg.defaultContainer = iterator.key(); - }); + adminConfig->defaultContainer = iterator.key(); } } } if (hasNewContainers) { - serverConfigModel.visit([&containers](auto& arg) { - arg.containers = containers; - }); - m_serversRepository->editServer(serverIndex, serverConfigModel); + adminConfig->containers = containers; + m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin); } return ErrorCode::NoError; @@ -899,7 +952,7 @@ ErrorCode InstallController::installServer(const ServerCredentials &credentials, preparedContainers.insert(container, containerConfig); } - SelfHostedServerConfig serverConfig; + SelfHostedAdminServerConfig serverConfig; serverConfig.hostName = credentials.hostName; serverConfig.userName = credentials.userName; serverConfig.password = credentials.secretData; @@ -912,21 +965,29 @@ ErrorCode InstallController::installServer(const ServerCredentials &credentials, serverConfig.defaultContainer = container; - m_serversRepository->addServer(ServerConfig(serverConfig)); + serverConfig.displayName = serverConfig.description.isEmpty() ? serverConfig.hostName : serverConfig.description; - int serverIndex = m_serversRepository->serversCount() - 1; + const QString newServerId = m_serversRepository->addServer(QString(), serverConfig.toJson(), + serverConfigUtils::ConfigType::SelfHostedAdmin); QString clientName = QString("Admin [%1]").arg(QSysInfo::prettyProductName()); for (auto iterator = preparedContainers.begin(); iterator != preparedContainers.end(); iterator++) { - adminAppendRequested(serverIndex, iterator.key(), iterator.value(), clientName); + adminAppendRequested(newServerId, iterator.key(), iterator.value(), clientName); } return ErrorCode::NoError; } -ErrorCode InstallController::installContainer(int serverIndex, DockerContainer container, int port, +ErrorCode InstallController::installContainer(const QString &serverId, DockerContainer container, int port, TransportProto transportProto, bool &wasContainerInstalled) { - ServerCredentials credentials = m_serversRepository->serverCredentials(serverIndex); + auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId); + if (!adminConfig.has_value()) { + return ErrorCode::InternalError; + } + ServerCredentials credentials = adminConfig->credentials(); + if (!credentials.isValid()) { + return ErrorCode::InternalError; + } SshSession sshSession(this); QMap installedContainers; @@ -949,15 +1010,17 @@ ErrorCode InstallController::installContainer(int serverIndex, DockerContainer c QString clientName = QString("Admin [%1]").arg(QSysInfo::prettyProductName()); for (auto iterator = installedContainers.begin(); iterator != installedContainers.end(); iterator++) { - ContainerConfig existingConfigModel = m_serversRepository->containerConfig(serverIndex, iterator.key()); + ContainerConfig existingConfigModel = adminConfig->containerConfig(iterator.key()); if (existingConfigModel.container == DockerContainer::None) { ContainerConfig containerConfig = iterator.value(); errorCode = processContainerForAdmin(iterator.key(), containerConfig, credentials, sshSession, - serverIndex, clientName); + serverId, clientName); if (errorCode != ErrorCode::NoError) { return errorCode; } - m_serversRepository->setContainerConfig(serverIndex, iterator.key(), containerConfig); + adminConfig->updateContainerConfig(iterator.key(), containerConfig); + m_serversRepository->editServer(serverId, adminConfig->toJson(), + serverConfigUtils::ConfigType::SelfHostedAdmin); } } @@ -993,7 +1056,15 @@ bool InstallController::isServerAlreadyExists(const ServerCredentials &credentia { int serversCount = m_serversRepository->serversCount(); for (int i = 0; i < serversCount; i++) { - const ServerCredentials existingCredentials = m_serversRepository->serverCredentials(i); + const QString existingServerId = m_serversRepository->serverIdAt(i); + const auto adminConfig = m_serversRepository->selfHostedAdminConfig(existingServerId); + if (!adminConfig.has_value()) { + continue; + } + const ServerCredentials existingCredentials = adminConfig->credentials(); + if (!existingCredentials.isValid()) { + continue; + } if (credentials.hostName == existingCredentials.hostName && credentials.port == existingCredentials.port) { existingServerIndex = i; return true; diff --git a/client/core/controllers/selfhosted/installController.h b/client/core/controllers/selfhosted/installController.h index bf5701e27..acb61ca41 100644 --- a/client/core/controllers/selfhosted/installController.h +++ b/client/core/controllers/selfhosted/installController.h @@ -33,22 +33,22 @@ public: ~InstallController(); ErrorCode setupContainer(const ServerCredentials &credentials, DockerContainer container, ContainerConfig &config, bool isUpdate = false); - ErrorCode updateContainer(int serverIndex, DockerContainer container, const ContainerConfig &oldConfig, ContainerConfig &newConfig); + ErrorCode updateContainer(const QString &serverId, DockerContainer container, const ContainerConfig &oldConfig, ContainerConfig &newConfig); - ErrorCode rebootServer(int serverIndex); - ErrorCode removeAllContainers(int serverIndex); - ErrorCode removeContainer(int serverIndex, DockerContainer container); + ErrorCode rebootServer(const QString &serverId); + ErrorCode removeAllContainers(const QString &serverId); + ErrorCode removeContainer(const QString &serverId, DockerContainer container); ContainerConfig generateConfig(DockerContainer container, int port, TransportProto transportProto); ErrorCode getAlreadyInstalledContainers(const ServerCredentials &credentials, QMap &installedContainers, SshSession &sshSession); - ErrorCode scanServerForInstalledContainers(int serverIndex); + ErrorCode scanServerForInstalledContainers(const QString &serverId); ErrorCode installContainer(const ServerCredentials &credentials, DockerContainer container, int port, TransportProto transportProto, ContainerConfig &config); ErrorCode installServer(const ServerCredentials &credentials, DockerContainer container, int port, TransportProto transportProto, bool &wasContainerInstalled); - ErrorCode installContainer(int serverIndex, DockerContainer container, int port, TransportProto transportProto, + ErrorCode installContainer(const QString &serverId, DockerContainer container, int port, TransportProto transportProto, bool &wasContainerInstalled); bool isUpdateDockerContainerRequired(DockerContainer container, const ContainerConfig &oldConfig, const ContainerConfig &newConfig); @@ -62,11 +62,13 @@ public: void cancelInstallation(); - void clearCachedProfile(int serverIndex, DockerContainer container); + void clearCachedProfile(const QString &serverId, DockerContainer container); - ErrorCode validateAndPrepareConfig(int serverIndex); + ErrorCode validateAndPrepareConfig(const QString &serverId); - void validateConfig(int serverIndex); + void validateConfig(const QString &serverId); + + void addEmptyServer(const ServerCredentials &credentials); signals: void configValidated(bool isValid); @@ -74,8 +76,8 @@ signals: void serverIsBusy(const bool isBusy); void cancelInstallationRequested(); - void clientRevocationRequested(int serverIndex, const ContainerConfig &containerConfig, DockerContainer container); - void clientAppendRequested(int serverIndex, const QString &clientId, const QString &clientName, DockerContainer container); + void clientRevocationRequested(const QString &serverId, const ContainerConfig &containerConfig, DockerContainer container); + void clientAppendRequested(const QString &serverId, const QString &clientId, const QString &clientName, DockerContainer container); private: ErrorCode installDockerWorker(const ServerCredentials &credentials, DockerContainer container, SshSession &sshSession); @@ -95,9 +97,9 @@ private: ErrorCode processContainerForAdmin(DockerContainer container, ContainerConfig &containerConfig, const ServerCredentials &credentials, SshSession &sshSession, - int serverIndex, const QString &clientName); + const QString &serverId, const QString &clientName); - void adminAppendRequested(int serverIndex, DockerContainer container, + void adminAppendRequested(const QString &serverId, DockerContainer container, const ContainerConfig &containerConfig, const QString &clientName); static void updateContainerConfigAfterInstallation(DockerContainer container, ContainerConfig &containerConfig, const QString &stdOut); @@ -114,4 +116,3 @@ private: }; #endif // INSTALLCONTROLLER_H - diff --git a/client/core/controllers/selfhosted/usersController.cpp b/client/core/controllers/selfhosted/usersController.cpp index 3999f66d7..7180cbb8f 100644 --- a/client/core/controllers/selfhosted/usersController.cpp +++ b/client/core/controllers/selfhosted/usersController.cpp @@ -14,7 +14,6 @@ #include "core/protocols/protocolUtils.h" #include "core/utils/constants/configKeys.h" #include "core/utils/constants/protocolConstants.h" -#include "core/models/serverConfig.h" #include "core/models/containerConfig.h" using namespace amnezia; @@ -292,11 +291,18 @@ ErrorCode UsersController::getXrayClients(const DockerContainer container, const return error; } -ErrorCode UsersController::updateClients(int serverIndex, const DockerContainer container) +ErrorCode UsersController::updateClients(const QString &serverId, const DockerContainer container) { ErrorCode error = ErrorCode::NoError; SshSession sshSession; - ServerCredentials credentials = m_serversRepository->serverCredentials(serverIndex); + auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId); + if (!adminConfig.has_value()) { + return ErrorCode::InternalError; + } + ServerCredentials credentials = adminConfig->credentials(); + if (!credentials.isValid()) { + return ErrorCode::InternalError; + } QString clientsTableFile = QString("/opt/amnezia/%1/clientsTable"); if (container == DockerContainer::OpenVpn) { @@ -381,20 +387,27 @@ ErrorCode UsersController::updateClients(int serverIndex, const DockerContainer } -ErrorCode UsersController::appendClient(int serverIndex, const QString &clientId, const QString &clientName, const DockerContainer container) +ErrorCode UsersController::appendClient(const QString &serverId, const QString &clientId, const QString &clientName, const DockerContainer container) { ErrorCode error = ErrorCode::NoError; SshSession sshSession; - ServerCredentials credentials = m_serversRepository->serverCredentials(serverIndex); + auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId); + if (!adminConfig.has_value()) { + return ErrorCode::InternalError; + } + ServerCredentials credentials = adminConfig->credentials(); + if (!credentials.isValid()) { + return ErrorCode::InternalError; + } - error = updateClients(serverIndex, container); + error = updateClients(serverId, container); if (error != ErrorCode::NoError) { return error; } int existingIndex = clientIndexById(clientId, m_clientsTable); if (existingIndex >= 0) { - return renameClient(serverIndex, existingIndex, clientName, container, true); + return renameClient(serverId, existingIndex, clientName, container, true); } QJsonObject client; @@ -426,7 +439,7 @@ ErrorCode UsersController::appendClient(int serverIndex, const QString &clientId return error; } -ErrorCode UsersController::renameClient(int serverIndex, const int row, const QString &clientName, +ErrorCode UsersController::renameClient(const QString &serverId, const int row, const QString &clientName, const DockerContainer container, bool addTimeStamp) { if (row < 0 || row >= m_clientsTable.size()) { @@ -434,7 +447,14 @@ ErrorCode UsersController::renameClient(int serverIndex, const int row, const QS } SshSession sshSession; - ServerCredentials credentials = m_serversRepository->serverCredentials(serverIndex); + auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId); + if (!adminConfig.has_value()) { + return ErrorCode::InternalError; + } + ServerCredentials credentials = adminConfig->credentials(); + if (!credentials.isValid()) { + return ErrorCode::InternalError; + } auto client = m_clientsTable.at(row).toObject(); auto userData = client[configKey::userData].toObject(); @@ -470,7 +490,7 @@ ErrorCode UsersController::renameClient(int serverIndex, const int row, const QS } ErrorCode UsersController::revokeOpenVpn(const int row, const DockerContainer container, const ServerCredentials &credentials, - const int serverIndex, SshSession* sshSession, QJsonArray &clientsTable) + SshSession* sshSession, QJsonArray &clientsTable) { if (row < 0 || row >= clientsTable.size()) { return ErrorCode::InternalError; @@ -689,14 +709,21 @@ ErrorCode UsersController::revokeXray(const int row, return error; } -ErrorCode UsersController::revokeClient(int serverIndex, const int index, const DockerContainer container) +ErrorCode UsersController::revokeClient(const QString &serverId, const int index, const DockerContainer container) { if (index < 0 || index >= m_clientsTable.size()) { return ErrorCode::InternalError; } SshSession sshSession; - ServerCredentials credentials = m_serversRepository->serverCredentials(serverIndex); + auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId); + if (!adminConfig.has_value()) { + return ErrorCode::InternalError; + } + ServerCredentials credentials = adminConfig->credentials(); + if (!credentials.isValid()) { + return ErrorCode::InternalError; + } QString clientId = m_clientsTable.at(index).toObject().value(configKey::clientId).toString(); ErrorCode errorCode = ErrorCode::NoError; @@ -704,7 +731,7 @@ ErrorCode UsersController::revokeClient(int serverIndex, const int index, const switch(container) { case DockerContainer::OpenVpn: { - errorCode = revokeOpenVpn(index, container, credentials, serverIndex, &sshSession, m_clientsTable); + errorCode = revokeOpenVpn(index, container, credentials, &sshSession, m_clientsTable); break; } case DockerContainer::WireGuard: @@ -724,12 +751,15 @@ ErrorCode UsersController::revokeClient(int serverIndex, const int index, const } if (errorCode == ErrorCode::NoError) { - ServerConfig serverConfig = m_serversRepository->server(serverIndex); - ContainerConfig containerCfg = m_serversRepository->containerConfig(serverIndex, container); + auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId); + if (!adminConfig.has_value()) { + return ErrorCode::InternalError; + } + ContainerConfig containerCfg = adminConfig->containerConfig(container); QString containerClientId = containerCfg.protocolConfig.clientId(); if (!clientId.isEmpty() && !containerClientId.isEmpty() && containerClientId.contains(clientId)) { - emit adminConfigRevoked(serverIndex, container); + emit adminConfigRevoked(serverId, container); } emit clientRevoked(index); @@ -739,13 +769,20 @@ ErrorCode UsersController::revokeClient(int serverIndex, const int index, const return errorCode; } -ErrorCode UsersController::revokeClient(int serverIndex, const ContainerConfig &containerConfig, const DockerContainer container) +ErrorCode UsersController::revokeClient(const QString &serverId, const ContainerConfig &containerConfig, const DockerContainer container) { SshSession sshSession; - ServerCredentials credentials = m_serversRepository->serverCredentials(serverIndex); + auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId); + if (!adminConfig.has_value()) { + return ErrorCode::InternalError; + } + ServerCredentials credentials = adminConfig->credentials(); + if (!credentials.isValid()) { + return ErrorCode::InternalError; + } ErrorCode errorCode = ErrorCode::NoError; - errorCode = updateClients(serverIndex, container); + errorCode = updateClients(serverId, container); if (errorCode != ErrorCode::NoError) { return errorCode; } @@ -778,7 +815,7 @@ ErrorCode UsersController::revokeClient(int serverIndex, const ContainerConfig & switch (container) { case DockerContainer::OpenVpn: { - errorCode = revokeOpenVpn(row, container, credentials, serverIndex, &sshSession, m_clientsTable); + errorCode = revokeOpenVpn(row, container, credentials, &sshSession, m_clientsTable); break; } case DockerContainer::WireGuard: @@ -797,7 +834,7 @@ ErrorCode UsersController::revokeClient(int serverIndex, const ContainerConfig & } if (errorCode == ErrorCode::NoError) { - emit adminConfigRevoked(serverIndex, container); + emit adminConfigRevoked(serverId, container); emit clientRevoked(row); emit clientsUpdated(m_clientsTable); } diff --git a/client/core/controllers/selfhosted/usersController.h b/client/core/controllers/selfhosted/usersController.h index 89bc94b90..b52a3f5e5 100644 --- a/client/core/controllers/selfhosted/usersController.h +++ b/client/core/controllers/selfhosted/usersController.h @@ -37,21 +37,21 @@ signals: void clientAdded(const QJsonObject &client); void clientRenamed(int row, const QString &newName); void clientRevoked(int row); - void adminConfigRevoked(int serverIndex, DockerContainer container); + void adminConfigRevoked(const QString &serverId, DockerContainer container); public slots: - ErrorCode updateClients(int serverIndex, const DockerContainer container); - ErrorCode appendClient(int serverIndex, const QString &clientId, const QString &clientName, const DockerContainer container); - ErrorCode renameClient(int serverIndex, const int row, const QString &userName, const DockerContainer container, bool addTimeStamp = false); - ErrorCode revokeClient(int serverIndex, const int index, const DockerContainer container); - ErrorCode revokeClient(int serverIndex, const ContainerConfig &containerConfig, const DockerContainer container); + ErrorCode updateClients(const QString &serverId, const DockerContainer container); + ErrorCode appendClient(const QString &serverId, const QString &clientId, const QString &clientName, const DockerContainer container); + ErrorCode renameClient(const QString &serverId, const int row, const QString &userName, const DockerContainer container, bool addTimeStamp = false); + ErrorCode revokeClient(const QString &serverId, const int index, const DockerContainer container); + ErrorCode revokeClient(const QString &serverId, const ContainerConfig &containerConfig, const DockerContainer container); private: bool isClientExists(const QString &clientId, const QJsonArray &clientsTable); int clientIndexById(const QString &clientId, const QJsonArray &clientsTable); void migration(const QByteArray &clientsTableString, QJsonArray &clientsTable); - ErrorCode revokeOpenVpn(const int row, const DockerContainer container, const ServerCredentials &credentials, const int serverIndex, + ErrorCode revokeOpenVpn(const int row, const DockerContainer container, const ServerCredentials &credentials, SshSession* sshSession, QJsonArray &clientsTable); ErrorCode revokeWireGuard(const int row, const DockerContainer container, const ServerCredentials &credentials, SshSession* sshSession, QJsonArray &clientsTable); @@ -73,4 +73,3 @@ private: }; #endif // USERSCONTROLLER_H - diff --git a/client/core/controllers/serversController.cpp b/client/core/controllers/serversController.cpp index 1842775b8..7b406ae06 100644 --- a/client/core/controllers/serversController.cpp +++ b/client/core/controllers/serversController.cpp @@ -1,81 +1,268 @@ #include "serversController.h" -#include "core/utils/networkUtilities.h" -#include "core/utils/api/apiEnums.h" -#include "core/utils/constants/apiKeys.h" -#include "core/utils/constants/apiConstants.h" +#include "core/utils/serverConfigUtils.h" #include "core/utils/protocolEnum.h" #include "core/protocols/protocolUtils.h" #include "core/utils/constants/configKeys.h" -#include "core/utils/constants/protocolConstants.h" -#include "core/models/serverConfig.h" #include "core/models/containerConfig.h" +#include "core/models/serverDescription.h" + #if defined(Q_OS_IOS) || defined(MACOS_NE) #include #endif -ServersController::ServersController(SecureServersRepository* serversRepository, - SecureAppSettingsRepository* appSettingsRepository, - QObject *parent) +ServersController::ServersController(SecureServersRepository *serversRepository, + SecureAppSettingsRepository *appSettingsRepository, QObject *parent) : QObject(parent), m_serversRepository(serversRepository), m_appSettingsRepository(appSettingsRepository) { - recomputeGatewayStacks(); + ensureDefaultServerValid(); } -void ServersController::addServer(const ServerConfig &server) +void ServersController::ensureDefaultServerValid() { - m_serversRepository->addServer(server); -} - -void ServersController::editServer(int index, const ServerConfig &server) -{ - m_serversRepository->editServer(index, server); -} - -void ServersController::removeServer(int index) -{ - m_serversRepository->removeServer(index); -} - -void ServersController::setDefaultServerIndex(int index) -{ - m_serversRepository->setDefaultServer(index); -} - -void ServersController::setDefaultContainer(int serverIndex, DockerContainer container) -{ - m_serversRepository->setDefaultContainer(serverIndex, container); -} - -void ServersController::updateContainerConfig(int serverIndex, DockerContainer container, const ContainerConfig &config) -{ - m_serversRepository->setContainerConfig(serverIndex, container, config); -} - -void ServersController::clearCachedProfile(int serverIndex, DockerContainer container) -{ - m_serversRepository->clearLastConnectionConfig(serverIndex, container); -} - -QJsonArray ServersController::getServersArray() const -{ - QJsonArray result; - QVector servers = m_serversRepository->servers(); - for (const ServerConfig& server : servers) { - result.append(server.toJson()); + if (!getServersCount()) { + return; + } + + const QString defaultId = getDefaultServerId(); + if (!defaultId.isEmpty() && indexOfServerId(defaultId) >= 0) { + return; + } + + const QString firstId = getServerId(0); + if (!firstId.isEmpty()) { + setDefaultServer(firstId); } - return result; } -QVector ServersController::getServers() const +bool ServersController::renameServer(const QString &serverId, const QString &name) { - return m_serversRepository->servers(); + const serverConfigUtils::ConfigType kind = m_serversRepository->serverKind(serverId); + switch (kind) { + case serverConfigUtils::ConfigType::SelfHostedAdmin: { + auto cfg = m_serversRepository->selfHostedAdminConfig(serverId); + if (!cfg.has_value()) return false; + cfg->description = name; + m_serversRepository->editServer(serverId, cfg->toJson(), kind); + return true; + } + case serverConfigUtils::ConfigType::SelfHostedUser: { + auto cfg = m_serversRepository->selfHostedUserConfig(serverId); + if (!cfg.has_value()) return false; + cfg->description = name; + m_serversRepository->editServer(serverId, cfg->toJson(), kind); + return true; + } + case serverConfigUtils::ConfigType::Native: { + auto cfg = m_serversRepository->nativeConfig(serverId); + if (!cfg.has_value()) return false; + cfg->description = name; + m_serversRepository->editServer(serverId, cfg->toJson(), kind); + return true; + } + case serverConfigUtils::ConfigType::AmneziaPremiumV2: + case serverConfigUtils::ConfigType::AmneziaFreeV3: + case serverConfigUtils::ConfigType::ExternalPremium: { + auto cfg = m_serversRepository->apiV2Config(serverId); + if (!cfg.has_value()) return false; + cfg->name = name; + cfg->nameOverriddenByUser = true; + m_serversRepository->editServer(serverId, cfg->toJson(), kind); + return true; + } + case serverConfigUtils::ConfigType::AmneziaPremiumV1: + case serverConfigUtils::ConfigType::AmneziaFreeV2: + case serverConfigUtils::ConfigType::Invalid: + default: + return false; + } } -ContainerConfig ServersController::getContainerConfig(int serverIndex, DockerContainer container) const +void ServersController::removeServer(const QString &serverId) { - return m_serversRepository->containerConfig(serverIndex, container); + m_serversRepository->removeServer(serverId); +} + +void ServersController::setDefaultServer(const QString &serverId) +{ + m_serversRepository->setDefaultServer(serverId); +} + +void ServersController::setDefaultContainer(const QString &serverId, DockerContainer container) +{ + const serverConfigUtils::ConfigType kind = m_serversRepository->serverKind(serverId); + switch (kind) { + case serverConfigUtils::ConfigType::SelfHostedAdmin: { + auto cfg = m_serversRepository->selfHostedAdminConfig(serverId); + if (!cfg.has_value()) return; + cfg->defaultContainer = container; + m_serversRepository->editServer(serverId, cfg->toJson(), kind); + return; + } + case serverConfigUtils::ConfigType::SelfHostedUser: { + auto cfg = m_serversRepository->selfHostedUserConfig(serverId); + if (!cfg.has_value()) return; + cfg->defaultContainer = container; + m_serversRepository->editServer(serverId, cfg->toJson(), kind); + return; + } + case serverConfigUtils::ConfigType::Native: { + auto cfg = m_serversRepository->nativeConfig(serverId); + if (!cfg.has_value()) return; + cfg->defaultContainer = container; + m_serversRepository->editServer(serverId, cfg->toJson(), kind); + return; + } + case serverConfigUtils::ConfigType::AmneziaPremiumV2: + case serverConfigUtils::ConfigType::AmneziaFreeV3: + case serverConfigUtils::ConfigType::ExternalPremium: { + auto cfg = m_serversRepository->apiV2Config(serverId); + if (!cfg.has_value()) return; + cfg->defaultContainer = container; + m_serversRepository->editServer(serverId, cfg->toJson(), kind); + return; + } + case serverConfigUtils::ConfigType::AmneziaPremiumV1: + case serverConfigUtils::ConfigType::AmneziaFreeV2: + case serverConfigUtils::ConfigType::Invalid: + default: + return; + } +} + +QVector ServersController::buildServerDescriptions(bool isAmneziaDnsEnabled) const +{ + QVector out; + const QVector ids = m_serversRepository->orderedServerIds(); + out.reserve(ids.size()); + + for (const QString &id : ids) { + ServerDescription d; + using Kind = serverConfigUtils::ConfigType; + const Kind kind = m_serversRepository->serverKind(id); + switch (kind) { + case Kind::SelfHostedAdmin: { + const auto cfg = m_serversRepository->selfHostedAdminConfig(id); + if (!cfg) { + continue; + } + d = buildServerDescription(*cfg, isAmneziaDnsEnabled); + break; + } + case Kind::SelfHostedUser: { + const auto cfg = m_serversRepository->selfHostedUserConfig(id); + if (!cfg) { + continue; + } + d = buildServerDescription(*cfg, isAmneziaDnsEnabled); + break; + } + case Kind::Native: { + const auto cfg = m_serversRepository->nativeConfig(id); + if (!cfg) { + continue; + } + d = buildServerDescription(*cfg, isAmneziaDnsEnabled); + break; + } + case Kind::AmneziaPremiumV2: + case Kind::AmneziaFreeV3: + case Kind::ExternalPremium: { + const auto cfg = m_serversRepository->apiV2Config(id); + if (!cfg) { + continue; + } + d = buildServerDescription(*cfg, isAmneziaDnsEnabled); + break; + } + case Kind::AmneziaPremiumV1: + case Kind::AmneziaFreeV2: { + const auto cfg = m_serversRepository->legacyApiConfig(id); + if (!cfg) { + continue; + } + d = buildServerDescription(*cfg, isAmneziaDnsEnabled); + break; + } + case Kind::Invalid: + default: + continue; + } + + d.serverId = id; + out.append(d); + } + return out; +} + +QMap ServersController::getServerContainersMap(const QString &serverId) const +{ + switch (m_serversRepository->serverKind(serverId)) { + case serverConfigUtils::ConfigType::SelfHostedAdmin: { + const auto cfg = m_serversRepository->selfHostedAdminConfig(serverId); + return cfg.has_value() ? cfg->containers : QMap{}; + } + case serverConfigUtils::ConfigType::SelfHostedUser: { + const auto cfg = m_serversRepository->selfHostedUserConfig(serverId); + return cfg.has_value() ? cfg->containers : QMap{}; + } + case serverConfigUtils::ConfigType::Native: { + const auto cfg = m_serversRepository->nativeConfig(serverId); + return cfg.has_value() ? cfg->containers : QMap{}; + } + case serverConfigUtils::ConfigType::AmneziaPremiumV2: + case serverConfigUtils::ConfigType::AmneziaFreeV3: + case serverConfigUtils::ConfigType::ExternalPremium: { + const auto cfg = m_serversRepository->apiV2Config(serverId); + return cfg.has_value() ? cfg->containers : QMap{}; + } + case serverConfigUtils::ConfigType::AmneziaPremiumV1: + case serverConfigUtils::ConfigType::AmneziaFreeV2: { + const auto cfg = m_serversRepository->legacyApiConfig(serverId); + return cfg.has_value() ? cfg->containers : QMap{}; + } + case serverConfigUtils::ConfigType::Invalid: + default: + return {}; + } +} + +DockerContainer ServersController::getDefaultContainer(const QString &serverId) const +{ + switch (m_serversRepository->serverKind(serverId)) { + case serverConfigUtils::ConfigType::SelfHostedAdmin: { + const auto cfg = m_serversRepository->selfHostedAdminConfig(serverId); + return cfg.has_value() ? cfg->defaultContainer : DockerContainer::None; + } + case serverConfigUtils::ConfigType::SelfHostedUser: { + const auto cfg = m_serversRepository->selfHostedUserConfig(serverId); + return cfg.has_value() ? cfg->defaultContainer : DockerContainer::None; + } + case serverConfigUtils::ConfigType::Native: { + const auto cfg = m_serversRepository->nativeConfig(serverId); + return cfg.has_value() ? cfg->defaultContainer : DockerContainer::None; + } + case serverConfigUtils::ConfigType::AmneziaPremiumV2: + case serverConfigUtils::ConfigType::AmneziaFreeV3: + case serverConfigUtils::ConfigType::ExternalPremium: { + const auto cfg = m_serversRepository->apiV2Config(serverId); + return cfg.has_value() ? cfg->defaultContainer : DockerContainer::None; + } + case serverConfigUtils::ConfigType::AmneziaPremiumV1: + case serverConfigUtils::ConfigType::AmneziaFreeV2: { + const auto cfg = m_serversRepository->legacyApiConfig(serverId); + return cfg.has_value() ? cfg->defaultContainer : DockerContainer::None; + } + case serverConfigUtils::ConfigType::Invalid: + default: + return DockerContainer::None; + } +} + +ContainerConfig ServersController::getContainerConfig(const QString &serverId, DockerContainer container) const +{ + return getServerContainersMap(serverId).value(container); } int ServersController::getDefaultServerIndex() const @@ -83,114 +270,131 @@ int ServersController::getDefaultServerIndex() const return m_serversRepository->defaultServerIndex(); } +QString ServersController::getDefaultServerId() const +{ + return m_serversRepository->defaultServerId(); +} + int ServersController::getServersCount() const { return m_serversRepository->serversCount(); } -ServerConfig ServersController::getServerConfig(int serverIndex) const +QString ServersController::getServerId(int serverIndex) const { - return m_serversRepository->server(serverIndex); + return m_serversRepository->serverIdAt(serverIndex); } -ServerCredentials ServersController::getServerCredentials(int serverIndex) const +int ServersController::indexOfServerId(const QString &serverId) const { - return m_serversRepository->serverCredentials(serverIndex); + return m_serversRepository->indexOfServerId(serverId); } -QPair ServersController::getDnsPair(int serverIndex, bool isAmneziaDnsEnabled) const +QString ServersController::notificationDisplayName(const QString &serverId) const { - ServerConfig serverConfig = m_serversRepository->server(serverIndex); - return serverConfig.getDnsPair(isAmneziaDnsEnabled, - m_appSettingsRepository->primaryDns(), - m_appSettingsRepository->secondaryDns()); -} + if (serverId.isEmpty()) { + return {}; + } -ServersController::GatewayStacksData ServersController::gatewayStacks() const -{ - return m_gatewayStacks; -} - -void ServersController::recomputeGatewayStacks() -{ - GatewayStacksData computed; - bool hasNewTags = false; - QVector servers = m_serversRepository->servers(); - - for (const ServerConfig& serverConfig : servers) { - if (serverConfig.isApiV2()) { - const ApiV2ServerConfig* apiV2 = serverConfig.as(); - if (!apiV2) continue; - const QString userCountryCode = apiV2->apiConfig.userCountryCode; - const QString serviceType = apiV2->serviceType(); - - if (!userCountryCode.isEmpty()) { - if (!m_gatewayStacks.userCountryCodes.contains(userCountryCode)) { - hasNewTags = true; - } - computed.userCountryCodes.insert(userCountryCode); - } - - if (!serviceType.isEmpty()) { - if (!m_gatewayStacks.serviceTypes.contains(serviceType)) { - hasNewTags = true; - } - computed.serviceTypes.insert(serviceType); + using Kind = serverConfigUtils::ConfigType; + switch (m_serversRepository->serverKind(serverId)) { + case Kind::SelfHostedAdmin: { + if (const auto cfg = m_serversRepository->selfHostedAdminConfig(serverId)) { + if (!cfg->displayName.isEmpty()) { + return cfg->displayName; } } + break; } - - m_gatewayStacks = std::move(computed); - if (hasNewTags) { - emit gatewayStacksExpanded(); - } -} - -bool ServersController::GatewayStacksData::operator==(const GatewayStacksData &other) const -{ - return userCountryCodes == other.userCountryCodes && serviceTypes == other.serviceTypes; -} - -QJsonObject ServersController::GatewayStacksData::toJson() const -{ - QJsonObject json; - - QJsonArray userCountryCodesArray; - for (const QString &code : userCountryCodes) { - userCountryCodesArray.append(code); - } - json[apiDefs::key::userCountryCode] = userCountryCodesArray; - - QJsonArray serviceTypesArray; - for (const QString &type : serviceTypes) { - serviceTypesArray.append(type); - } - json[apiDefs::key::serviceType] = serviceTypesArray; - - return json; -} - -bool ServersController::isServerFromApiAlreadyExists(const QString &userCountryCode, const QString &serviceType, const QString &serviceProtocol) const -{ - QVector servers = m_serversRepository->servers(); - for (const ServerConfig& serverConfig : servers) { - if (serverConfig.isApiV2()) { - const ApiV2ServerConfig* apiV2 = serverConfig.as(); - if (!apiV2) return false; - if (apiV2->apiConfig.userCountryCode == userCountryCode - && apiV2->serviceType() == serviceType - && apiV2->serviceProtocol() == serviceProtocol) { - return true; + case Kind::SelfHostedUser: { + if (const auto cfg = m_serversRepository->selfHostedUserConfig(serverId)) { + if (!cfg->displayName.isEmpty()) { + return cfg->displayName; } } + break; + } + case Kind::Native: { + if (const auto cfg = m_serversRepository->nativeConfig(serverId)) { + if (!cfg->displayName.isEmpty()) { + return cfg->displayName; + } + } + break; + } + case Kind::AmneziaPremiumV2: + case Kind::AmneziaFreeV3: + case Kind::ExternalPremium: { + if (const auto cfg = m_serversRepository->apiV2Config(serverId)) { + if (!cfg->displayName.isEmpty()) { + return cfg->displayName; + } + } + break; + } + case Kind::AmneziaPremiumV1: + case Kind::AmneziaFreeV2: { + if (const auto cfg = m_serversRepository->legacyApiConfig(serverId)) { + if (!cfg->displayName.isEmpty()) { + return cfg->displayName; + } + } + break; + } + default: + break; + } + + const int idx = indexOfServerId(serverId); + if (idx >= 0) { + return QString::number(idx + 1); + } + return serverId; +} + +std::optional ServersController::apiV2Config(const QString &serverId) const +{ + return m_serversRepository->apiV2Config(serverId); +} + +std::optional ServersController::selfHostedAdminConfig(const QString &serverId) const +{ + return m_serversRepository->selfHostedAdminConfig(serverId); +} + +ServerCredentials ServersController::getServerCredentials(const QString &serverId) const +{ + const auto cfg = m_serversRepository->selfHostedAdminConfig(serverId); + if (cfg.has_value()) { + const ServerCredentials creds = cfg->credentials(); + if (creds.isValid()) { + return creds; + } + } + return ServerCredentials {}; +} + +bool ServersController::isServerFromApiAlreadyExists(const QString &userCountryCode, const QString &serviceType, + const QString &serviceProtocol) const +{ + const QVector ids = m_serversRepository->orderedServerIds(); + for (const QString &id : ids) { + const auto apiV2 = m_serversRepository->apiV2Config(id); + if (!apiV2.has_value()) { + continue; + } + if (apiV2->apiConfig.userCountryCode == userCountryCode && apiV2->serviceType() == serviceType + && apiV2->serviceProtocol() == serviceProtocol) { + return true; + } } return false; } -bool ServersController::hasInstalledContainers(int serverIndex) const +bool ServersController::hasInstalledContainers(const QString &serverId) const { - ServerConfig serverConfig = m_serversRepository->server(serverIndex); - QMap containers = serverConfig.containers(); + const QMap containers = getServerContainersMap(serverId); + for (auto it = containers.begin(); it != containers.end(); ++it) { DockerContainer container = it.key(); if (ContainerUtils::containerService(container) == ServiceType::Vpn) { @@ -203,3 +407,8 @@ bool ServersController::hasInstalledContainers(int serverIndex) const return false; } +bool ServersController::isLegacyApiV1Server(const QString &serverId) const +{ + return !serverId.isEmpty() + && serverConfigUtils::isLegacyApiSubscription(m_serversRepository->serverKind(serverId)); +} diff --git a/client/core/controllers/serversController.h b/client/core/controllers/serversController.h index 3b91e5558..e8286ed4c 100644 --- a/client/core/controllers/serversController.h +++ b/client/core/controllers/serversController.h @@ -1,11 +1,11 @@ #ifndef SERVERSCONTROLLER_H #define SERVERSCONTROLLER_H +#include + #include -#include -#include -#include #include +#include #include @@ -17,34 +17,18 @@ #include "core/utils/commonStructs.h" #include "core/repositories/secureServersRepository.h" #include "core/repositories/secureAppSettingsRepository.h" -#include "core/models/serverConfig.h" #include "core/models/containerConfig.h" +#include "core/models/serverDescription.h" class SshSession; class InstallController; using namespace amnezia; -/** - * @brief Core business logic controller for server operations - * - * This controller contains pure business logic for managing servers. - */ class ServersController : public QObject { Q_OBJECT - -public: - struct GatewayStacksData - { - QSet userCountryCodes; - QSet serviceTypes; - bool isEmpty() const { return userCountryCodes.isEmpty() && serviceTypes.isEmpty(); } - bool operator==(const GatewayStacksData &other) const; - QJsonObject toJson() const; - }; - public: explicit ServersController(SecureServersRepository* serversRepository, SecureAppSettingsRepository* appSettingsRepository = nullptr, @@ -52,44 +36,38 @@ public: ~ServersController() = default; // Server management - void addServer(const ServerConfig &server); - void editServer(int index, const ServerConfig &server); - void removeServer(int index); - void setDefaultServerIndex(int index); + bool renameServer(const QString &serverId, const QString &name); + void removeServer(const QString &serverId); + void setDefaultServer(const QString &serverId); // Container management - void setDefaultContainer(int serverIndex, DockerContainer container); - void updateContainerConfig(int serverIndex, DockerContainer container, const ContainerConfig &config); - - // Cache management - void clearCachedProfile(int serverIndex, DockerContainer container); + void setDefaultContainer(const QString &serverId, DockerContainer container); // Getters - QJsonArray getServersArray() const; - QVector getServers() const; + QVector buildServerDescriptions(bool isAmneziaDnsEnabled) const; int getDefaultServerIndex() const; + QString getDefaultServerId() const; int getServersCount() const; - ServerConfig getServerConfig(int serverIndex) const; - ServerCredentials getServerCredentials(int serverIndex) const; - ContainerConfig getContainerConfig(int serverIndex, DockerContainer container) const; - QPair getDnsPair(int serverIndex, bool isAmneziaDnsEnabled) const; - - GatewayStacksData gatewayStacks() const; + QString getServerId(int serverIndex) const; + int indexOfServerId(const QString &serverId) const; + QString notificationDisplayName(const QString &serverId) const; + std::optional apiV2Config(const QString &serverId) const; + std::optional selfHostedAdminConfig(const QString &serverId) const; + ServerCredentials getServerCredentials(const QString &serverId) const; + QMap getServerContainersMap(const QString &serverId) const; + DockerContainer getDefaultContainer(const QString &serverId) const; + ContainerConfig getContainerConfig(const QString &serverId, DockerContainer container) const; // Validation bool isServerFromApiAlreadyExists(const QString &userCountryCode, const QString &serviceType, const QString &serviceProtocol) const; - bool hasInstalledContainers(int serverIndex) const; - -signals: - void gatewayStacksExpanded(); - -public slots: - void recomputeGatewayStacks(); + bool hasInstalledContainers(const QString &serverId) const; + bool isLegacyApiV1Server(const QString &serverId) const; private: + void ensureDefaultServerValid(); + SecureServersRepository* m_serversRepository; SecureAppSettingsRepository* m_appSettingsRepository; - GatewayStacksData m_gatewayStacks; }; #endif // SERVERSCONTROLLER_H diff --git a/client/core/controllers/settingsController.cpp b/client/core/controllers/settingsController.cpp index f4f2e4327..3e0aae76a 100644 --- a/client/core/controllers/settingsController.cpp +++ b/client/core/controllers/settingsController.cpp @@ -179,12 +179,9 @@ QString SettingsController::getAppVersion() const void SettingsController::clearSettings() { - int serverCount = m_serversRepository->serversCount(); - m_appSettingsRepository->clearSettings(); - - m_serversRepository->setServersArray(QJsonArray()); - m_serversRepository->setDefaultServer(0); + + m_serversRepository->clearServers(); emit siteSplitTunnelingRouteModeChanged(RouteMode::VpnOnlyForwardSites); emit siteSplitTunnelingToggled(false); diff --git a/client/core/models/api/apiConfig.h b/client/core/models/api/apiConfig.h index 05ecda478..3af65cf47 100644 --- a/client/core/models/api/apiConfig.h +++ b/client/core/models/api/apiConfig.h @@ -6,7 +6,7 @@ #include #include -#include "core/utils/api/apiEnums.h" +#include "core/utils/serverConfigUtils.h" #include "core/utils/constants/apiKeys.h" #include "core/utils/constants/apiConstants.h" diff --git a/client/core/models/api/apiV1ServerConfig.cpp b/client/core/models/api/apiV1ServerConfig.cpp deleted file mode 100644 index 9e4c0e6d2..000000000 --- a/client/core/models/api/apiV1ServerConfig.cpp +++ /dev/null @@ -1,140 +0,0 @@ -#include "apiV1ServerConfig.h" - -#include -#include - -#include "core/utils/containerEnum.h" -#include "core/utils/containers/containerUtils.h" -#include "core/utils/protocolEnum.h" -#include "core/utils/protocolEnum.h" -#include "core/protocols/protocolUtils.h" -#include "core/utils/constants/apiKeys.h" -#include "core/utils/constants/configKeys.h" -#include "core/utils/constants/protocolConstants.h" -#include "core/utils/api/apiUtils.h" - -namespace amnezia -{ - -using namespace ContainerEnumNS; - -bool ApiV1ServerConfig::isPremium() const -{ - constexpr QLatin1String premiumV1Endpoint(PREM_V1_ENDPOINT); - return apiEndpoint.contains(premiumV1Endpoint); -} - -bool ApiV1ServerConfig::isFree() const -{ - constexpr QLatin1String freeV2Endpoint(FREE_V2_ENDPOINT); - return apiEndpoint.contains(freeV2Endpoint); -} - -QString ApiV1ServerConfig::vpnKey() const -{ - QJsonObject json = toJson(); - return apiUtils::getPremiumV1VpnKey(json); -} - -bool ApiV1ServerConfig::hasContainers() const -{ - return !containers.isEmpty(); -} - -ContainerConfig ApiV1ServerConfig::containerConfig(DockerContainer container) const -{ - if (!containers.contains(container)) { - return ContainerConfig{}; - } - return containers.value(container); -} - -QJsonObject ApiV1ServerConfig::toJson() const -{ - QJsonObject obj; - - if (!name.isEmpty()) { - obj[configKey::name] = name; - } - if (!description.isEmpty()) { - obj[configKey::description] = description; - } - if (!protocol.isEmpty()) { - obj[apiDefs::key::protocol] = protocol; - } - if (!apiEndpoint.isEmpty()) { - obj[apiDefs::key::apiEndpoint] = apiEndpoint; - } - if (!apiKey.isEmpty()) { - obj[apiDefs::key::apiKey] = apiKey; - } - - obj[configKey::configVersion] = configVersion; - - if (!hostName.isEmpty()) { - obj[configKey::hostName] = hostName; - } - - QJsonArray containersArray; - for (auto it = containers.begin(); it != containers.end(); ++it) { - QJsonObject containerObj = it.value().toJson(); - containersArray.append(containerObj); - } - if (!containersArray.isEmpty()) { - obj[configKey::containers] = containersArray; - } - - if (defaultContainer != DockerContainer::None) { - obj[configKey::defaultContainer] = ContainerUtils::containerToString(defaultContainer); - } - - if (!dns1.isEmpty()) { - obj[configKey::dns1] = dns1; - } - if (!dns2.isEmpty()) { - obj[configKey::dns2] = dns2; - } - - if (crc > 0) { - obj[configKey::crc] = crc; - } - - return obj; -} - -ApiV1ServerConfig ApiV1ServerConfig::fromJson(const QJsonObject& json) -{ - ApiV1ServerConfig config; - - config.name = json.value(configKey::name).toString(); - config.description = json.value(configKey::description).toString(); - config.protocol = json.value(apiDefs::key::protocol).toString(); - config.apiEndpoint = json.value(apiDefs::key::apiEndpoint).toString(); - config.apiKey = json.value(apiDefs::key::apiKey).toString(); - config.configVersion = json.value(configKey::configVersion).toInt(1); - config.hostName = json.value(configKey::hostName).toString(); - - QJsonArray containersArray = json.value(configKey::containers).toArray(); - for (const QJsonValue& val : containersArray) { - QJsonObject containerObj = val.toObject(); - ContainerConfig containerConfig = ContainerConfig::fromJson(containerObj); - - QString containerStr = containerObj.value(configKey::container).toString(); - DockerContainer container = ContainerUtils::containerFromString(containerStr); - - config.containers.insert(container, containerConfig); - } - - QString defaultContainerStr = json.value(configKey::defaultContainer).toString(); - config.defaultContainer = ContainerUtils::containerFromString(defaultContainerStr); - - config.dns1 = json.value(configKey::dns1).toString(); - config.dns2 = json.value(configKey::dns2).toString(); - - config.crc = json.value(configKey::crc).toInt(0); - - return config; -} - -} // namespace amnezia - diff --git a/client/core/models/api/apiV1ServerConfig.h b/client/core/models/api/apiV1ServerConfig.h deleted file mode 100644 index be414cef5..000000000 --- a/client/core/models/api/apiV1ServerConfig.h +++ /dev/null @@ -1,47 +0,0 @@ -#ifndef APIV1SERVERCONFIG_H -#define APIV1SERVERCONFIG_H - -#include -#include - -#include "core/utils/containerEnum.h" -#include "core/utils/containers/containerUtils.h" -#include "core/utils/protocolEnum.h" -#include "core/models/containerConfig.h" -#include "core/utils/api/apiEnums.h" -#include "core/utils/constants/apiKeys.h" -#include "core/utils/constants/apiConstants.h" - -namespace amnezia -{ - -using namespace ContainerEnumNS; - -struct ApiV1ServerConfig { - QString description; - QString hostName; - QMap containers; - DockerContainer defaultContainer; - QString dns1; - QString dns2; - - QString name; - QString protocol; - QString apiEndpoint; - QString apiKey; - int crc; - int configVersion; - - bool isPremium() const; - bool isFree() const; - QString vpnKey() const; - bool hasContainers() const; - ContainerConfig containerConfig(DockerContainer container) const; - QJsonObject toJson() const; - static ApiV1ServerConfig fromJson(const QJsonObject& json); -}; - -} // namespace amnezia - -#endif // APIV1SERVERCONFIG_H - diff --git a/client/core/models/api/apiV2ServerConfig.cpp b/client/core/models/api/apiV2ServerConfig.cpp index 08d6323f8..2809675e5 100644 --- a/client/core/models/api/apiV2ServerConfig.cpp +++ b/client/core/models/api/apiV2ServerConfig.cpp @@ -80,6 +80,9 @@ QJsonObject ApiV2ServerConfig::toJson() const if (!description.isEmpty()) { obj[configKey::description] = description; } + if (!displayName.isEmpty()) { + obj[configKey::displayName] = displayName; + } obj[configKey::configVersion] = configVersion; @@ -131,6 +134,7 @@ ApiV2ServerConfig ApiV2ServerConfig::fromJson(const QJsonObject& json) config.name = json.value(configKey::name).toString(); config.nameOverriddenByUser = json.value(configKey::nameOverriddenByUser).toBool(false); config.description = json.value(configKey::description).toString(); + config.displayName = json.value(configKey::displayName).toString(); config.configVersion = json.value(configKey::configVersion).toInt(2); config.hostName = json.value(configKey::hostName).toString(); @@ -163,6 +167,10 @@ ApiV2ServerConfig ApiV2ServerConfig::fromJson(const QJsonObject& json) config.authData = AuthData::fromJson(authDataObj); } + if (config.displayName.isEmpty()) { + config.displayName = config.name.isEmpty() ? config.description : config.name; + } + return config; } diff --git a/client/core/models/api/apiV2ServerConfig.h b/client/core/models/api/apiV2ServerConfig.h index e3625ae96..c2f9b8762 100644 --- a/client/core/models/api/apiV2ServerConfig.h +++ b/client/core/models/api/apiV2ServerConfig.h @@ -10,7 +10,7 @@ #include "core/models/containerConfig.h" #include "core/models/api/apiConfig.h" #include "core/models/api/authData.h" -#include "core/utils/api/apiEnums.h" +#include "core/utils/serverConfigUtils.h" #include "core/utils/constants/apiKeys.h" #include "core/utils/constants/apiConstants.h" @@ -21,6 +21,7 @@ using namespace ContainerEnumNS; struct ApiV2ServerConfig { QString description; + QString displayName; QString hostName; QMap containers; DockerContainer defaultContainer; diff --git a/client/core/models/api/authData.h b/client/core/models/api/authData.h index 1d2061ac9..b92ba1b86 100644 --- a/client/core/models/api/authData.h +++ b/client/core/models/api/authData.h @@ -4,7 +4,7 @@ #include #include -#include "core/utils/api/apiEnums.h" +#include "core/utils/serverConfigUtils.h" #include "core/utils/constants/apiKeys.h" #include "core/utils/constants/apiConstants.h" diff --git a/client/core/models/api/legacyApiServerConfig.cpp b/client/core/models/api/legacyApiServerConfig.cpp new file mode 100644 index 000000000..9d6bafff4 --- /dev/null +++ b/client/core/models/api/legacyApiServerConfig.cpp @@ -0,0 +1,43 @@ +#include "legacyApiServerConfig.h" + +#include "core/utils/constants/apiKeys.h" +#include "core/utils/constants/configKeys.h" + +namespace amnezia +{ + +bool LegacyApiServerConfig::hasContainers() const +{ + return !containers.isEmpty(); +} + +ContainerConfig LegacyApiServerConfig::containerConfig(DockerContainer container) const +{ + if (!containers.contains(container)) { + return ContainerConfig{}; + } + return containers.value(container); +} + +LegacyApiServerConfig LegacyApiServerConfig::fromJson(const QJsonObject &json) +{ + LegacyApiServerConfig config; + + config.name = json.value(configKey::name).toString(); + config.description = json.value(configKey::description).toString(); + config.displayName = json.value(configKey::displayName).toString(); + config.hostName = json.value(configKey::hostName).toString(); + + config.crc = json.value(configKey::crc).toInt(0); + + config.configVersion = json.value(configKey::configVersion).toInt(1); + config.apiEndpoint = json.value(apiDefs::key::apiEndpoint).toString(); + + if (config.displayName.isEmpty()) { + config.displayName = config.name.isEmpty() ? config.description : config.name; + } + + return config; +} + +} // namespace amnezia diff --git a/client/core/models/api/legacyApiServerConfig.h b/client/core/models/api/legacyApiServerConfig.h new file mode 100644 index 000000000..94f904981 --- /dev/null +++ b/client/core/models/api/legacyApiServerConfig.h @@ -0,0 +1,38 @@ +#ifndef LEGACYAPISERVERCONFIG_H +#define LEGACYAPISERVERCONFIG_H + +#include +#include + +#include "core/utils/containerEnum.h" +#include "core/utils/protocolEnum.h" +#include "core/models/containerConfig.h" + +namespace amnezia +{ + +using namespace ContainerEnumNS; + +struct LegacyApiServerConfig { + QString description; + QString displayName; + QString hostName; + QMap containers; + DockerContainer defaultContainer = DockerContainer::None; + QString dns1; + QString dns2; + + QString name; + int crc = 0; + + int configVersion = 0; + QString apiEndpoint; + + bool hasContainers() const; + ContainerConfig containerConfig(DockerContainer container) const; + static LegacyApiServerConfig fromJson(const QJsonObject &json); +}; + +} // namespace amnezia + +#endif // LEGACYAPISERVERCONFIG_H diff --git a/client/core/models/selfhosted/nativeServerConfig.cpp b/client/core/models/selfhosted/nativeServerConfig.cpp index 34577ac29..d8f0d80be 100644 --- a/client/core/models/selfhosted/nativeServerConfig.cpp +++ b/client/core/models/selfhosted/nativeServerConfig.cpp @@ -35,6 +35,9 @@ QJsonObject NativeServerConfig::toJson() const if (!description.isEmpty()) { obj[configKey::description] = this->description; } + if (!displayName.isEmpty()) { + obj[configKey::displayName] = displayName; + } if (!hostName.isEmpty()) { obj[configKey::hostName] = hostName; } @@ -67,6 +70,7 @@ NativeServerConfig NativeServerConfig::fromJson(const QJsonObject& json) NativeServerConfig config; config.description = json.value(configKey::description).toString(); + config.displayName = json.value(configKey::displayName).toString(); config.hostName = json.value(configKey::hostName).toString(); QJsonArray containersArray = json.value(configKey::containers).toArray(); @@ -86,6 +90,10 @@ NativeServerConfig NativeServerConfig::fromJson(const QJsonObject& json) config.dns1 = json.value(configKey::dns1).toString(); config.dns2 = json.value(configKey::dns2).toString(); + if (config.displayName.isEmpty()) { + config.displayName = config.description.isEmpty() ? config.hostName : config.description; + } + return config; } diff --git a/client/core/models/selfhosted/nativeServerConfig.h b/client/core/models/selfhosted/nativeServerConfig.h index 13982eb8b..87d11077a 100644 --- a/client/core/models/selfhosted/nativeServerConfig.h +++ b/client/core/models/selfhosted/nativeServerConfig.h @@ -16,6 +16,7 @@ using namespace ContainerEnumNS; struct NativeServerConfig { QString description; + QString displayName; QString hostName; QMap containers; DockerContainer defaultContainer; diff --git a/client/core/models/selfhosted/selfHostedAdminServerConfig.cpp b/client/core/models/selfhosted/selfHostedAdminServerConfig.cpp new file mode 100644 index 000000000..7729a3e46 --- /dev/null +++ b/client/core/models/selfhosted/selfHostedAdminServerConfig.cpp @@ -0,0 +1,170 @@ +#include "selfHostedAdminServerConfig.h" + +#include + +#include "core/protocols/protocolUtils.h" +#include "core/utils/constants/configKeys.h" +#include "core/utils/constants/protocolConstants.h" +#include "core/utils/containerEnum.h" +#include "core/utils/containers/containerUtils.h" +#include "core/utils/protocolEnum.h" +#include "core/utils/networkUtilities.h" + +namespace amnezia +{ + +using namespace ContainerEnumNS; + +bool SelfHostedAdminServerConfig::hasCredentials() const +{ + return !userName.isEmpty() && !password.isEmpty() && port > 0; +} + +bool SelfHostedAdminServerConfig::isReadOnly() const +{ + return !hasCredentials(); +} + +ServerCredentials SelfHostedAdminServerConfig::credentials() const +{ + ServerCredentials creds; + creds.hostName = hostName; + creds.userName = userName; + creds.secretData = password; + creds.port = port; + return creds; +} + +bool SelfHostedAdminServerConfig::hasContainers() const +{ + return !containers.isEmpty(); +} + +ContainerConfig SelfHostedAdminServerConfig::containerConfig(DockerContainer container) const +{ + if (!containers.contains(container)) { + return ContainerConfig{}; + } + return containers.value(container); +} + +void SelfHostedAdminServerConfig::updateContainerConfig(DockerContainer container, const ContainerConfig &config) +{ + containers[container] = config; +} + +void SelfHostedAdminServerConfig::clearCachedClientProfile(DockerContainer container) +{ + if (ContainerUtils::containerService(container) == ServiceType::Other) { + return; + } + + ContainerConfig cleared = containerConfig(container); + cleared.protocolConfig.clearClientConfig(); + containers[container] = cleared; +} + +QPair SelfHostedAdminServerConfig::getDnsPair(bool isAmneziaDnsEnabled, const QString &primaryDns, + const QString &secondaryDns) const +{ + QString d1 = dns1; + QString d2 = dns2; + const bool dnsOnServer = containers.contains(DockerContainer::Dns); + + if (d1.isEmpty() || !NetworkUtilities::checkIPv4Format(d1)) { + d1 = (isAmneziaDnsEnabled && dnsOnServer) ? protocols::dns::amneziaDnsIp : primaryDns; + } + if (d2.isEmpty() || !NetworkUtilities::checkIPv4Format(d2)) { + d2 = secondaryDns; + } + return { d1, d2 }; +} + +QJsonObject SelfHostedAdminServerConfig::toJson() const +{ + QJsonObject obj; + + if (!description.isEmpty()) { + obj[configKey::description] = this->description; + } + if (!displayName.isEmpty()) { + obj[configKey::displayName] = displayName; + } + if (!hostName.isEmpty()) { + obj[configKey::hostName] = hostName; + } + + QJsonArray containersArray; + for (auto it = containers.begin(); it != containers.end(); ++it) { + QJsonObject containerObj = it.value().toJson(); + containersArray.append(containerObj); + } + if (!containersArray.isEmpty()) { + obj[configKey::containers] = containersArray; + } + + if (defaultContainer != DockerContainer::None) { + obj[configKey::defaultContainer] = ContainerUtils::containerToString(defaultContainer); + } + + if (!dns1.isEmpty()) { + obj[configKey::dns1] = dns1; + } + if (!dns2.isEmpty()) { + obj[configKey::dns2] = dns2; + } + + if (!userName.isEmpty()) { + obj[configKey::userName] = userName; + } + if (!password.isEmpty()) { + obj[configKey::password] = password; + } + if (port > 0) { + obj[configKey::port] = port; + } + + return obj; +} + +SelfHostedAdminServerConfig SelfHostedAdminServerConfig::fromJson(const QJsonObject &json) +{ + SelfHostedAdminServerConfig config; + + config.description = json.value(configKey::description).toString(); + config.displayName = json.value(configKey::displayName).toString(); + config.hostName = json.value(configKey::hostName).toString(); + + QJsonArray containersArray = json.value(configKey::containers).toArray(); + for (const QJsonValue &val : containersArray) { + QJsonObject containerObj = val.toObject(); + ContainerConfig cc = ContainerConfig::fromJson(containerObj); + + QString containerStr = containerObj.value(configKey::container).toString(); + DockerContainer container = ContainerUtils::containerFromString(containerStr); + + config.containers.insert(container, cc); + } + + QString defaultContainerStr = json.value(configKey::defaultContainer).toString(); + config.defaultContainer = ContainerUtils::containerFromString(defaultContainerStr); + + config.dns1 = json.value(configKey::dns1).toString(); + config.dns2 = json.value(configKey::dns2).toString(); + + config.userName = json.value(configKey::userName).toString(); + config.password = json.value(configKey::password).toString(); + if (json.contains(configKey::port)) { + config.port = json.value(configKey::port).toInt(); + } else { + config.port = 0; + } + + if (config.displayName.isEmpty()) { + config.displayName = config.description.isEmpty() ? config.hostName : config.description; + } + + return config; +} + +} // namespace amnezia diff --git a/client/core/models/selfhosted/selfHostedAdminServerConfig.h b/client/core/models/selfhosted/selfHostedAdminServerConfig.h new file mode 100644 index 000000000..bd4a04716 --- /dev/null +++ b/client/core/models/selfhosted/selfHostedAdminServerConfig.h @@ -0,0 +1,53 @@ +#ifndef SELFHOSTEDADMINSERVERCONFIG_H +#define SELFHOSTEDADMINSERVERCONFIG_H + +#include +#include +#include + +#include "core/utils/containerEnum.h" +#include "core/utils/containers/containerUtils.h" +#include "core/utils/protocolEnum.h" +#include "core/models/containerConfig.h" +#include "core/utils/errorCodes.h" +#include "core/utils/routeModes.h" +#include "core/utils/commonStructs.h" + +namespace amnezia +{ + +using namespace ContainerEnumNS; + +struct SelfHostedAdminServerConfig { + QString description; + QString displayName; + QString hostName; + QMap containers; + DockerContainer defaultContainer; + QString dns1; + QString dns2; + + QString userName; + QString password; + int port = 0; + + bool hasCredentials() const; + bool isReadOnly() const; + ServerCredentials credentials() const; + bool hasContainers() const; + ContainerConfig containerConfig(DockerContainer container) const; + + void updateContainerConfig(DockerContainer container, const ContainerConfig &config); + + void clearCachedClientProfile(DockerContainer container); + + QPair getDnsPair(bool isAmneziaDnsEnabled, const QString &primaryDns, + const QString &secondaryDns) const; + + QJsonObject toJson() const; + static SelfHostedAdminServerConfig fromJson(const QJsonObject &json); +}; + +} // namespace amnezia + +#endif // SELFHOSTEDADMINSERVERCONFIG_H diff --git a/client/core/models/selfhosted/selfHostedServerConfig.cpp b/client/core/models/selfhosted/selfHostedUserServerConfig.cpp similarity index 53% rename from client/core/models/selfhosted/selfHostedServerConfig.cpp rename to client/core/models/selfhosted/selfHostedUserServerConfig.cpp index 535350936..982456ed2 100644 --- a/client/core/models/selfhosted/selfHostedServerConfig.cpp +++ b/client/core/models/selfhosted/selfHostedUserServerConfig.cpp @@ -1,53 +1,40 @@ -#include "selfHostedServerConfig.h" +#include "selfHostedUserServerConfig.h" #include -#include -#include -#include "core/utils/containerEnum.h" -#include "core/utils/containers/containerUtils.h" -#include "core/utils/protocolEnum.h" -#include "core/utils/protocolEnum.h" #include "core/protocols/protocolUtils.h" #include "core/utils/constants/configKeys.h" #include "core/utils/constants/protocolConstants.h" +#include "core/utils/containerEnum.h" +#include "core/utils/containers/containerUtils.h" +#include "core/utils/protocolEnum.h" namespace amnezia { using namespace ContainerEnumNS; -bool SelfHostedServerConfig::hasCredentials() const +bool SelfHostedUserServerConfig::hasCredentials() const { - return userName.has_value() && password.has_value() && port.has_value(); + return false; } -bool SelfHostedServerConfig::isReadOnly() const +bool SelfHostedUserServerConfig::isReadOnly() const { - return !hasCredentials(); + return true; } -std::optional SelfHostedServerConfig::credentials() const +std::optional SelfHostedUserServerConfig::credentials() const { - if (!hasCredentials()) { - return std::nullopt; - } - - ServerCredentials creds; - creds.hostName = hostName; - creds.userName = userName.value(); - creds.secretData = password.value(); - creds.port = port.value(); - - return creds; + return std::nullopt; } -bool SelfHostedServerConfig::hasContainers() const +bool SelfHostedUserServerConfig::hasContainers() const { return !containers.isEmpty(); } -ContainerConfig SelfHostedServerConfig::containerConfig(DockerContainer container) const +ContainerConfig SelfHostedUserServerConfig::containerConfig(DockerContainer container) const { if (!containers.contains(container)) { return ContainerConfig{}; @@ -55,17 +42,20 @@ ContainerConfig SelfHostedServerConfig::containerConfig(DockerContainer containe return containers.value(container); } -QJsonObject SelfHostedServerConfig::toJson() const +QJsonObject SelfHostedUserServerConfig::toJson() const { QJsonObject obj; - + if (!description.isEmpty()) { obj[configKey::description] = this->description; } + if (!displayName.isEmpty()) { + obj[configKey::displayName] = displayName; + } if (!hostName.isEmpty()) { obj[configKey::hostName] = hostName; } - + QJsonArray containersArray; for (auto it = containers.begin(); it != containers.end(); ++it) { QJsonObject containerObj = it.value().toJson(); @@ -74,67 +64,51 @@ QJsonObject SelfHostedServerConfig::toJson() const if (!containersArray.isEmpty()) { obj[configKey::containers] = containersArray; } - + if (defaultContainer != DockerContainer::None) { obj[configKey::defaultContainer] = ContainerUtils::containerToString(defaultContainer); } - + if (!dns1.isEmpty()) { obj[configKey::dns1] = dns1; } if (!dns2.isEmpty()) { obj[configKey::dns2] = dns2; } - - if (userName.has_value()) { - obj[configKey::userName] = userName.value(); - } - if (password.has_value()) { - obj[configKey::password] = password.value(); - } - if (port.has_value()) { - obj[configKey::port] = port.value(); - } - + return obj; } -SelfHostedServerConfig SelfHostedServerConfig::fromJson(const QJsonObject& json) +SelfHostedUserServerConfig SelfHostedUserServerConfig::fromJson(const QJsonObject &json) { - SelfHostedServerConfig config; - + SelfHostedUserServerConfig config; + config.description = json.value(configKey::description).toString(); + config.displayName = json.value(configKey::displayName).toString(); config.hostName = json.value(configKey::hostName).toString(); - + QJsonArray containersArray = json.value(configKey::containers).toArray(); - for (const QJsonValue& val : containersArray) { + for (const QJsonValue &val : containersArray) { QJsonObject containerObj = val.toObject(); - ContainerConfig containerConfig = ContainerConfig::fromJson(containerObj); - + ContainerConfig cc = ContainerConfig::fromJson(containerObj); + QString containerStr = containerObj.value(configKey::container).toString(); DockerContainer container = ContainerUtils::containerFromString(containerStr); - - config.containers.insert(container, containerConfig); + + config.containers.insert(container, cc); } - + QString defaultContainerStr = json.value(configKey::defaultContainer).toString(); config.defaultContainer = ContainerUtils::containerFromString(defaultContainerStr); - + config.dns1 = json.value(configKey::dns1).toString(); config.dns2 = json.value(configKey::dns2).toString(); - - if (json.contains(configKey::userName)) { - config.userName = json.value(configKey::userName).toString(); + + if (config.displayName.isEmpty()) { + config.displayName = config.description.isEmpty() ? config.hostName : config.description; } - if (json.contains(configKey::password)) { - config.password = json.value(configKey::password).toString(); - } - if (json.contains(configKey::port)) { - config.port = json.value(configKey::port).toInt(); - } - + return config; } } // namespace amnezia - diff --git a/client/core/models/selfhosted/selfHostedServerConfig.h b/client/core/models/selfhosted/selfHostedUserServerConfig.h similarity index 66% rename from client/core/models/selfhosted/selfHostedServerConfig.h rename to client/core/models/selfhosted/selfHostedUserServerConfig.h index 39dc88540..67d053a29 100644 --- a/client/core/models/selfhosted/selfHostedServerConfig.h +++ b/client/core/models/selfhosted/selfHostedUserServerConfig.h @@ -1,5 +1,5 @@ -#ifndef SELFHOSTEDSERVERCONFIG_H -#define SELFHOSTEDSERVERCONFIG_H +#ifndef SELFHOSTEDUSERSERVERCONFIG_H +#define SELFHOSTEDUSERSERVERCONFIG_H #include #include @@ -9,8 +9,6 @@ #include "core/utils/containers/containerUtils.h" #include "core/utils/protocolEnum.h" #include "core/models/containerConfig.h" -#include "core/utils/errorCodes.h" -#include "core/utils/routeModes.h" #include "core/utils/commonStructs.h" namespace amnezia @@ -18,28 +16,24 @@ namespace amnezia using namespace ContainerEnumNS; -struct SelfHostedServerConfig { +struct SelfHostedUserServerConfig { QString description; + QString displayName; QString hostName; QMap containers; DockerContainer defaultContainer; QString dns1; QString dns2; - - std::optional userName; - std::optional password; - std::optional port; - + bool hasCredentials() const; bool isReadOnly() const; std::optional credentials() const; bool hasContainers() const; ContainerConfig containerConfig(DockerContainer container) const; QJsonObject toJson() const; - static SelfHostedServerConfig fromJson(const QJsonObject& json); + static SelfHostedUserServerConfig fromJson(const QJsonObject &json); }; } // namespace amnezia -#endif // SELFHOSTEDSERVERCONFIG_H - +#endif // SELFHOSTEDUSERSERVERCONFIG_H diff --git a/client/core/models/serverConfig.cpp b/client/core/models/serverConfig.cpp deleted file mode 100644 index 7006fb07b..000000000 --- a/client/core/models/serverConfig.cpp +++ /dev/null @@ -1,234 +0,0 @@ -#include "serverConfig.h" - -#include "core/utils/api/apiUtils.h" -#include "core/utils/networkUtilities.h" -#include "core/models/selfhosted/selfHostedServerConfig.h" -#include "core/models/selfhosted/nativeServerConfig.h" -#include "core/models/api/apiV1ServerConfig.h" -#include "core/models/api/apiV2ServerConfig.h" -#include "core/utils/protocolEnum.h" -#include "core/protocols/protocolUtils.h" -#include "core/utils/constants/configKeys.h" -#include "core/utils/constants/protocolConstants.h" - -namespace amnezia -{ - -using namespace ContainerEnumNS; - -QString ServerConfig::description() const -{ - return std::visit([](const auto& v) { return v.description; }, data); -} - -QString ServerConfig::hostName() const -{ - return std::visit([](const auto& v) { return v.hostName; }, data); -} - -QString ServerConfig::displayName() const -{ - if (isApiV1()) { - const auto *apiV1 = as(); - return apiV1 ? apiV1->name : description(); - } - if (isApiV2()) { - const auto *apiV2 = as(); - return apiV2 ? apiV2->name : description(); - } - QString name = description(); - return name.isEmpty() ? hostName() : name; -} - -QMap ServerConfig::containers() const -{ - return std::visit([](const auto& v) { return v.containers; }, data); -} - -DockerContainer ServerConfig::defaultContainer() const -{ - return std::visit([](const auto& v) { return v.defaultContainer; }, data); -} - -QString ServerConfig::dns1() const -{ - return std::visit([](const auto& v) { return v.dns1; }, data); -} - -QString ServerConfig::dns2() const -{ - return std::visit([](const auto& v) { return v.dns2; }, data); -} - -bool ServerConfig::hasContainers() const -{ - return std::visit([](const auto& v) { return v.hasContainers(); }, data); -} - -ContainerConfig ServerConfig::containerConfig(DockerContainer container) const -{ - return std::visit([container](const auto& v) { return v.containerConfig(container); }, data); -} - -int ServerConfig::crc() const -{ - return std::visit([](const auto& v) -> int { - using T = std::decay_t; - if constexpr (std::is_same_v || - std::is_same_v) { - return v.crc; - } - return 0; - }, data); -} - -int ServerConfig::configVersion() const -{ - return std::visit([](const auto& v) -> int { - using T = std::decay_t; - if constexpr (std::is_same_v) { - return apiDefs::ConfigSource::Telegram; - } else if constexpr (std::is_same_v) { - return apiDefs::ConfigSource::AmneziaGateway; - } - return 0; // SelfHostedServerConfig or NativeServerConfig - }, data); -} - -bool ServerConfig::isSelfHosted() const -{ - return std::holds_alternative(data); -} - -bool ServerConfig::isNative() const -{ - return std::holds_alternative(data); -} - -bool ServerConfig::isApiV1() const -{ - return std::holds_alternative(data); -} - -bool ServerConfig::isApiV2() const -{ - return std::holds_alternative(data); -} - -bool ServerConfig::isApiConfig() const -{ - return isApiV1() || isApiV2(); -} - -QJsonObject ServerConfig::toJson() const -{ - return std::visit([](const auto& v) { return v.toJson(); }, data); -} - -ServerConfig ServerConfig::fromJson(const QJsonObject& json) -{ - apiDefs::ConfigType configType = apiUtils::getConfigType(json); - - switch (configType) { - case apiDefs::ConfigType::SelfHosted: { - bool hasThirdPartyConfig = false; - QJsonArray containersArray = json.value(configKey::containers).toArray(); - for (const QJsonValue& val : containersArray) { - QJsonObject containerObj = val.toObject(); - for (auto it = containerObj.begin(); it != containerObj.end(); ++it) { - QString key = it.key(); - if (key != configKey::container) { - QJsonObject protocolObj = it.value().toObject(); - if (protocolObj.contains(configKey::isThirdPartyConfig) && - protocolObj.value(configKey::isThirdPartyConfig).toBool()) { - hasThirdPartyConfig = true; - break; - } - } - } - if (hasThirdPartyConfig) { - break; - } - } - - if (hasThirdPartyConfig) { - return ServerConfig{NativeServerConfig::fromJson(json)}; - } else { - return ServerConfig{SelfHostedServerConfig::fromJson(json)}; - } - } - case apiDefs::ConfigType::AmneziaPremiumV1: - case apiDefs::ConfigType::AmneziaFreeV2: - return ServerConfig{ApiV1ServerConfig::fromJson(json)}; - case apiDefs::ConfigType::AmneziaPremiumV2: - case apiDefs::ConfigType::AmneziaFreeV3: - case apiDefs::ConfigType::ExternalPremium: - return ServerConfig{ApiV2ServerConfig::fromJson(json)}; - default: { - // Check if any container has isThirdPartyConfig - bool hasThirdPartyConfig = false; - QJsonArray containersArray = json.value(configKey::containers).toArray(); - for (const QJsonValue& val : containersArray) { - QJsonObject containerObj = val.toObject(); - // Check all protocol keys in the container object - for (auto it = containerObj.begin(); it != containerObj.end(); ++it) { - QString key = it.key(); - if (key != configKey::container) { - QJsonObject protocolObj = it.value().toObject(); - if (protocolObj.contains(configKey::isThirdPartyConfig) && - protocolObj.value(configKey::isThirdPartyConfig).toBool()) { - hasThirdPartyConfig = true; - break; - } - } - } - if (hasThirdPartyConfig) { - break; - } - } - - if (hasThirdPartyConfig) { - return ServerConfig{NativeServerConfig::fromJson(json)}; - } else { - return ServerConfig{SelfHostedServerConfig::fromJson(json)}; - } - } - } -} - -QPair ServerConfig::getDnsPair(bool isAmneziaDnsEnabled, - const QString &primaryDns, - const QString &secondaryDns) const -{ - QPair dns; - - QMap serverContainers = containers(); - - bool isDnsContainerInstalled = false; - for (auto it = serverContainers.begin(); it != serverContainers.end(); ++it) { - if (it.key() == DockerContainer::Dns) { - isDnsContainerInstalled = true; - break; - } - } - - dns.first = dns1(); - dns.second = dns2(); - - if (dns.first.isEmpty() || !NetworkUtilities::checkIPv4Format(dns.first)) { - if (isAmneziaDnsEnabled && isDnsContainerInstalled) { - dns.first = protocols::dns::amneziaDnsIp; - } else { - dns.first = primaryDns; - } - } - - if (dns.second.isEmpty() || !NetworkUtilities::checkIPv4Format(dns.second)) { - dns.second = secondaryDns; - } - - return dns; -} - -} // namespace amnezia - diff --git a/client/core/models/serverConfig.h b/client/core/models/serverConfig.h deleted file mode 100644 index 93ef1842d..000000000 --- a/client/core/models/serverConfig.h +++ /dev/null @@ -1,92 +0,0 @@ -#ifndef SERVERCONFIG_H -#define SERVERCONFIG_H - -#include -#include -#include - -#include "core/utils/containerEnum.h" -#include "core/utils/containers/containerUtils.h" -#include "core/utils/protocolEnum.h" -#include "core/models/selfhosted/selfHostedServerConfig.h" -#include "core/models/selfhosted/nativeServerConfig.h" -#include "core/models/api/apiV1ServerConfig.h" -#include "core/models/api/apiV2ServerConfig.h" -#include "core/models/containerConfig.h" - -namespace amnezia -{ - -using namespace ContainerEnumNS; - -struct ServerConfig { - using Variant = std::variant< - SelfHostedServerConfig, - NativeServerConfig, - ApiV1ServerConfig, - ApiV2ServerConfig - >; - - Variant data; - - ServerConfig() = default; - ServerConfig(const Variant& v) : data(v) {} - ServerConfig(Variant&& v) : data(std::move(v)) {} - - template>, ServerConfig>::value>> - ServerConfig(const T& v) : data(v) {} - - template>, ServerConfig>::value>> - ServerConfig(T&& v) : data(std::forward(v)) {} - - QString description() const; - QString hostName() const; - QString displayName() const; - QMap containers() const; - DockerContainer defaultContainer() const; - QString dns1() const; - QString dns2() const; - bool hasContainers() const; - ContainerConfig containerConfig(DockerContainer container) const; - - int crc() const; - int configVersion() const; - - bool isSelfHosted() const; - bool isNative() const; - bool isApiV1() const; - bool isApiV2() const; - bool isApiConfig() const; - - template - T* as() { - return std::get_if(&data); - } - - template - const T* as() const { - return std::get_if(&data); - } - - QJsonObject toJson() const; - static ServerConfig fromJson(const QJsonObject& json); - - template - auto visit(Visitor&& visitor) { - return std::visit(std::forward(visitor), data); - } - - template - auto visit(Visitor&& visitor) const { - return std::visit(std::forward(visitor), data); - } - - QPair getDnsPair(bool isAmneziaDnsEnabled, - const QString &primaryDns, - const QString &secondaryDns) const; -}; - -} // namespace amnezia - -#endif // SERVERCONFIG_H - diff --git a/client/core/models/serverDescription.cpp b/client/core/models/serverDescription.cpp new file mode 100644 index 000000000..c533a094b --- /dev/null +++ b/client/core/models/serverDescription.cpp @@ -0,0 +1,187 @@ +#include "serverDescription.h" + +#include + +#include "core/utils/serverConfigUtils.h" +#include "core/utils/constants/apiKeys.h" +#include "core/utils/constants/apiConstants.h" +#include "core/utils/constants/protocolConstants.h" +#include "core/utils/api/apiUtils.h" +#include "core/utils/containers/containerUtils.h" +#include "core/protocols/protocolUtils.h" +#include "core/models/protocols/awgProtocolConfig.h" + +using namespace amnezia; + +namespace +{ + +bool computeHasInstalledVpnContainers(const QMap &containers) +{ + for (auto it = containers.begin(); it != containers.end(); ++it) { + const DockerContainer container = it.key(); + if (ContainerUtils::containerService(container) == ServiceType::Vpn || container == DockerContainer::SSXray) { + return true; + } + } + return false; +} + +template +ServerDescription buildBaseDescription(const T &server) +{ + ServerDescription row; + row.hostName = server.hostName; + row.defaultContainer = server.defaultContainer; + row.primaryDnsIsAmnezia = (server.dns1 == protocols::dns::amneziaDnsIp); + row.hasInstalledVpnContainers = computeHasInstalledVpnContainers(server.containers); + return row; +} + +QString getBaseDescription(const QMap &containers, + bool isAmneziaDnsEnabled, + bool hasWriteAccess, + bool primaryDnsIsAmnezia) +{ + QString description; + if (hasWriteAccess) { + const bool isDnsInstalled = containers.contains(DockerContainer::Dns); + if (isAmneziaDnsEnabled && isDnsInstalled) { + description += QStringLiteral("Amnezia DNS | "); + } + } else if (primaryDnsIsAmnezia) { + description += QStringLiteral("Amnezia DNS | "); + } + return description; +} + +QString getProtocolName(DockerContainer defaultContainer, const QMap &containers) +{ + QString containerName = ContainerUtils::containerHumanNames().value(defaultContainer); + QString protocolVersion; + + if (ContainerUtils::isAwgContainer(defaultContainer)) { + const auto it = containers.constFind(defaultContainer); + if (it != containers.cend()) { + if (const AwgProtocolConfig *awg = it->getAwgProtocolConfig()) { + protocolVersion = ProtocolUtils::getProtocolVersionString(awg->toJson()); + if (defaultContainer == DockerContainer::Awg && !awg->serverConfig.isThirdPartyConfig) { + containerName = QStringLiteral("AmneziaWG Legacy"); + } + } + } + } + + return containerName + protocolVersion + QStringLiteral(" | "); +} + +} // namespace + +namespace amnezia +{ + +ServerDescription buildServerDescription(const SelfHostedAdminServerConfig &server, bool isAmneziaDnsEnabled) +{ + ServerDescription row = buildBaseDescription(server); + row.selfHostedSshCredentials.hostName = server.hostName; + row.selfHostedSshCredentials.userName = server.userName; + row.selfHostedSshCredentials.secretData = server.password; + row.selfHostedSshCredentials.port = server.port > 0 ? server.port : 22; + + row.hasWriteAccess = !row.selfHostedSshCredentials.userName.isEmpty() + && !row.selfHostedSshCredentials.secretData.isEmpty(); + + row.serverName = server.displayName; + row.baseDescription = getBaseDescription(server.containers, isAmneziaDnsEnabled, row.hasWriteAccess, row.primaryDnsIsAmnezia); + + const QString protocolName = getProtocolName(server.defaultContainer, server.containers); + row.expandedServerDescription = row.baseDescription + row.hostName; + row.collapsedServerDescription = row.baseDescription + protocolName + row.hostName; + return row; +} + +ServerDescription buildServerDescription(const SelfHostedUserServerConfig &server, bool isAmneziaDnsEnabled) +{ + ServerDescription row = buildBaseDescription(server); + row.selfHostedSshCredentials.hostName = server.hostName; + row.selfHostedSshCredentials.port = 22; + row.hasWriteAccess = false; + + row.serverName = server.displayName; + row.baseDescription = getBaseDescription(server.containers, isAmneziaDnsEnabled, row.hasWriteAccess, row.primaryDnsIsAmnezia); + + const QString protocolName = getProtocolName(server.defaultContainer, server.containers); + row.expandedServerDescription = row.baseDescription + row.hostName; + row.collapsedServerDescription = row.baseDescription + protocolName + row.hostName; + return row; +} + +ServerDescription buildServerDescription(const NativeServerConfig &server, bool isAmneziaDnsEnabled) +{ + ServerDescription row = buildBaseDescription(server); + row.hasWriteAccess = false; + + row.serverName = server.displayName; + row.baseDescription = getBaseDescription(server.containers, isAmneziaDnsEnabled, row.hasWriteAccess, row.primaryDnsIsAmnezia); + + const QString protocolName = getProtocolName(server.defaultContainer, server.containers); + row.expandedServerDescription = row.baseDescription + row.hostName; + row.collapsedServerDescription = row.baseDescription + protocolName + row.hostName; + return row; +} + +ServerDescription buildServerDescription(const LegacyApiServerConfig &server, bool /*isAmneziaDnsEnabled*/) +{ + ServerDescription row = buildBaseDescription(server); + row.configVersion = serverConfigUtils::ConfigSource::Telegram; + row.isApiV1 = true; + row.isServerFromGatewayApi = false; + row.hasWriteAccess = false; + + row.serverName = server.displayName; + row.baseDescription = server.description; + + const QString fullDescriptionForCollapsed = row.baseDescription; + row.collapsedServerDescription = fullDescriptionForCollapsed; + row.expandedServerDescription = fullDescriptionForCollapsed; + return row; +} + +ServerDescription buildServerDescription(const ApiV2ServerConfig &server, bool /*isAmneziaDnsEnabled*/) +{ + ServerDescription row = buildBaseDescription(server); + row.configVersion = serverConfigUtils::ConfigSource::AmneziaGateway; + row.isApiV2 = true; + row.isServerFromGatewayApi = true; + row.isPremium = server.isPremium() || server.isExternalPremium(); + row.hasWriteAccess = false; + + row.serverName = server.displayName; + row.baseDescription = server.apiConfig.serverCountryCode.isEmpty() ? server.description : server.apiConfig.serverCountryName; + + row.isCountrySelectionAvailable = !server.apiConfig.availableCountries.isEmpty(); + row.apiAvailableCountries = server.apiConfig.availableCountries; + row.apiServerCountryCode = server.apiConfig.serverCountryCode; + + row.isAdVisible = server.apiConfig.serviceInfo.isAdVisible; + row.adHeader = server.apiConfig.serviceInfo.adHeader; + row.adDescription = server.apiConfig.serviceInfo.adDescription; + row.adEndpoint = server.apiConfig.serviceInfo.adEndpoint; + row.isRenewalAvailable = server.apiConfig.serviceInfo.isRenewalAvailable; + + if (!server.apiConfig.isInAppPurchase) { + if (server.apiConfig.subscriptionExpiredByServer) { + row.isSubscriptionExpired = true; + } else if (!server.apiConfig.subscription.endDate.isEmpty()) { + row.isSubscriptionExpired = apiUtils::isSubscriptionExpired(server.apiConfig.subscription.endDate); + row.isSubscriptionExpiringSoon = apiUtils::isSubscriptionExpiringSoon(server.apiConfig.subscription.endDate); + } + } + + const QString fullDescriptionForCollapsed = row.baseDescription; + row.collapsedServerDescription = fullDescriptionForCollapsed; + row.expandedServerDescription = fullDescriptionForCollapsed; + return row; +} + +} // namespace amnezia diff --git a/client/core/models/serverDescription.h b/client/core/models/serverDescription.h new file mode 100644 index 000000000..22a0ddb72 --- /dev/null +++ b/client/core/models/serverDescription.h @@ -0,0 +1,64 @@ +#ifndef SERVERDESCRIPTION_H +#define SERVERDESCRIPTION_H + +#include +#include + +#include "core/utils/containerEnum.h" +#include "core/utils/selfhosted/sshSession.h" +#include "core/models/selfhosted/selfHostedAdminServerConfig.h" +#include "core/models/selfhosted/selfHostedUserServerConfig.h" +#include "core/models/selfhosted/nativeServerConfig.h" +#include "core/models/api/legacyApiServerConfig.h" +#include "core/models/api/apiV2ServerConfig.h" + +namespace amnezia +{ + +struct ServerDescription +{ + QString serverId; + + QString serverName; + QString baseDescription; + QString hostName; + + int configVersion = 0; + + ServerCredentials selfHostedSshCredentials; + bool hasWriteAccess = false; + + bool primaryDnsIsAmnezia = false; + DockerContainer defaultContainer = DockerContainer::None; + bool hasInstalledVpnContainers = false; + + bool isApiV1 = false; + bool isApiV2 = false; + bool isServerFromGatewayApi = false; + bool isPremium = false; + + bool isCountrySelectionAvailable = false; + QJsonArray apiAvailableCountries; + QString apiServerCountryCode; + + bool isAdVisible = false; + QString adHeader; + QString adDescription; + QString adEndpoint; + bool isRenewalAvailable = false; + bool isSubscriptionExpired = false; + bool isSubscriptionExpiringSoon = false; + + QString collapsedServerDescription; + QString expandedServerDescription; +}; + +ServerDescription buildServerDescription(const SelfHostedAdminServerConfig &server, bool isAmneziaDnsEnabled); +ServerDescription buildServerDescription(const SelfHostedUserServerConfig &server, bool isAmneziaDnsEnabled); +ServerDescription buildServerDescription(const NativeServerConfig &server, bool isAmneziaDnsEnabled); +ServerDescription buildServerDescription(const LegacyApiServerConfig &server, bool isAmneziaDnsEnabled); +ServerDescription buildServerDescription(const ApiV2ServerConfig &server, bool isAmneziaDnsEnabled); + +} // namespace amnezia + +#endif diff --git a/client/core/repositories/secureAppSettingsRepository.cpp b/client/core/repositories/secureAppSettingsRepository.cpp index 3bc579c74..01f313b08 100644 --- a/client/core/repositories/secureAppSettingsRepository.cpp +++ b/client/core/repositories/secureAppSettingsRepository.cpp @@ -2,15 +2,16 @@ #include #include +#include #include #include "core/utils/errorCodes.h" #include "core/utils/routeModes.h" #include "core/utils/commonStructs.h" -#include "core/utils/api/apiEnums.h" +#include "core/utils/serverConfigUtils.h" #include "core/utils/constants/apiKeys.h" #include "core/utils/constants/apiConstants.h" -#include "core/models/serverConfig.h" +#include "core/utils/constants/configKeys.h" #include "core/utils/networkUtilities.h" using namespace amnezia; diff --git a/client/core/repositories/secureServersRepository.cpp b/client/core/repositories/secureServersRepository.cpp index 7107b8aa5..d59dfc7b7 100644 --- a/client/core/repositories/secureServersRepository.cpp +++ b/client/core/repositories/secureServersRepository.cpp @@ -1,26 +1,44 @@ #include "secureServersRepository.h" -#include #include +#include +#include +#include -#include "core/utils/api/apiEnums.h" +#include "core/utils/serverConfigUtils.h" #include "core/utils/constants/apiKeys.h" -#include "core/utils/constants/apiConstants.h" -#include "core/models/serverConfig.h" -#include "core/models/containerConfig.h" -#include "core/utils/protocolEnum.h" -#include "core/protocols/protocolUtils.h" #include "core/utils/constants/configKeys.h" -#include "core/utils/constants/protocolConstants.h" -SecureServersRepository::SecureServersRepository(SecureQSettings* settings, QObject *parent) +using namespace amnezia; + +namespace { + +QString readStorageServerId(const QJsonObject &json) +{ + return json.value(QString(configKey::storageServerId)).toString().trimmed(); +} + +QJsonObject withoutStorageServerId(const QJsonObject &json) +{ + QJsonObject o = json; + o.remove(QString(configKey::storageServerId)); + return o; +} + +QJsonObject embedStorageServerId(const QString &serverId, const QJsonObject &payloadSansId) +{ + QJsonObject o = payloadSansId; + o.insert(QString(configKey::storageServerId), serverId); + return o; +} + +} // namespace + +SecureServersRepository::SecureServersRepository(SecureQSettings *settings, QObject *parent) : QObject(parent), m_settings(settings) { - QJsonArray arr = QJsonDocument::fromJson(value("Servers/serversList").toByteArray()).array(); - for (const QJsonValue &val : arr) { - m_servers.append(ServerConfig::fromJson(val.toObject())); - } - m_defaultServerIndex = value("Servers/defaultServerIndex", 0).toInt(); + loadFromStorage(); + persistDefaultServerFields(); } QVariant SecureServersRepository::value(const QString &key, const QVariant &defaultValue) const @@ -33,216 +51,322 @@ void SecureServersRepository::setValue(const QString &key, const QVariant &value m_settings->setValue(key, value); } +void SecureServersRepository::clearServerStateMaps() +{ + m_serverJsonById.clear(); + m_orderedServerIds.clear(); +} + +QString SecureServersRepository::normalizedOrGeneratedServerId(const QString &candidateId) const +{ + const QString trimmed = candidateId.trimmed(); + if (!trimmed.isEmpty() && !m_serverJsonById.contains(trimmed)) { + return trimmed; + } + return QUuid::createUuid().toString(QUuid::WithoutBraces); +} + +void SecureServersRepository::updateDefaultServerFromStorage() +{ + const QString storedDefaultId = value(QStringLiteral("Servers/defaultServerId"), QString()).toString(); + if (!storedDefaultId.isEmpty() && m_serverJsonById.contains(storedDefaultId)) { + m_defaultServerId = storedDefaultId; + return; + } + + const int storedDefaultIndex = value("Servers/defaultServerIndex", 0).toInt(); + if (storedDefaultIndex >= 0 && storedDefaultIndex < m_orderedServerIds.size()) { + m_defaultServerId = m_orderedServerIds.at(storedDefaultIndex); + return; + } + + if (!m_orderedServerIds.isEmpty()) { + m_defaultServerId = m_orderedServerIds.first(); + return; + } + + m_defaultServerId.clear(); +} + +void SecureServersRepository::persistDefaultServerFields() +{ + if (m_orderedServerIds.isEmpty()) { + m_defaultServerId.clear(); + } else if (!m_orderedServerIds.contains(m_defaultServerId)) { + m_defaultServerId = m_orderedServerIds.first(); + } + + setValue("Servers/defaultServerId", m_defaultServerId); +} + +void SecureServersRepository::loadFromStorage() +{ + clearServerStateMaps(); + + const QJsonArray serversArray = + QJsonDocument::fromJson(value(QStringLiteral("Servers/serversList"), QByteArray()).toByteArray()) + .array(); + + for (int i = 0; i < serversArray.size(); ++i) { + const QJsonObject json = serversArray.at(i).toObject(); + const QString candidateId = readStorageServerId(json); + const QString serverId = normalizedOrGeneratedServerId(candidateId); + const QJsonObject strippedJson = withoutStorageServerId(json); + const serverConfigUtils::ConfigType kind = serverConfigUtils::configTypeFromJson(strippedJson); + + if (m_serverJsonById.contains(serverId) || kind == serverConfigUtils::ConfigType::Invalid) { + continue; + } + m_serverJsonById.insert(serverId, embedStorageServerId(serverId, strippedJson)); + m_orderedServerIds.append(serverId); + } + + updateDefaultServerFromStorage(); +} + void SecureServersRepository::syncToStorage() { - QJsonArray arr; - for (const ServerConfig &cfg : m_servers) { - arr.append(cfg.toJson()); + QJsonArray serversArray; + + for (const QString &serverId : m_orderedServerIds) { + if (!m_serverJsonById.contains(serverId)) { + continue; + } + serversArray.append(m_serverJsonById.value(serverId)); } - setValue("Servers/serversList", QJsonDocument(arr).toJson()); + + setValue("Servers/serversList", QJsonDocument(serversArray).toJson()); + persistDefaultServerFields(); } void SecureServersRepository::invalidateCache() { - m_servers.clear(); - QJsonArray arr = QJsonDocument::fromJson(value("Servers/serversList").toByteArray()).array(); - for (const QJsonValue &val : arr) { - m_servers.append(ServerConfig::fromJson(val.toObject())); - } - m_defaultServerIndex = value("Servers/defaultServerIndex", 0).toInt(); + loadFromStorage(); } -void SecureServersRepository::setServersArray(const QJsonArray &servers) +void SecureServersRepository::clearServers() { - m_servers.clear(); - for (const QJsonValue &val : servers) { - m_servers.append(ServerConfig::fromJson(val.toObject())); - } + clearServerStateMaps(); + + m_defaultServerId.clear(); + syncToStorage(); } -void SecureServersRepository::addServer(const ServerConfig &server) +QString SecureServersRepository::addServer(const QString &serverId, const QJsonObject &serverJson, serverConfigUtils::ConfigType kind) { - m_servers.append(server); + const QString id = normalizedOrGeneratedServerId(serverId); + if (m_serverJsonById.contains(id) || kind == serverConfigUtils::ConfigType::Invalid) { + return id; + } + const QJsonObject strippedJson = withoutStorageServerId(serverJson); + if (serverConfigUtils::configTypeFromJson(strippedJson) != kind) { + return id; + } + m_serverJsonById.insert(id, embedStorageServerId(id, strippedJson)); + + m_orderedServerIds.append(id); + + if (m_defaultServerId.isEmpty()) { + m_defaultServerId = id; + } + syncToStorage(); - emit serverAdded(server); + emit serverAdded(id); + return id; } -void SecureServersRepository::editServer(int index, const ServerConfig &server) +void SecureServersRepository::editServer(const QString &serverId, const QJsonObject &serverJson, serverConfigUtils::ConfigType kind) { - if (index < 0 || index >= m_servers.size()) { + if (indexOfServerId(serverId) < 0 || kind == serverConfigUtils::ConfigType::Invalid) { return; } - m_servers.replace(index, server); - syncToStorage(); - emit serverEdited(index, server); -} - -void SecureServersRepository::removeServer(int index) -{ - if (index < 0 || index >= m_servers.size()) { + if (!m_serverJsonById.contains(serverId)) { return; } - int defaultIndex = m_defaultServerIndex; - m_servers.removeAt(index); - if (defaultIndex == index) { - setDefaultServer(0); - } else if (defaultIndex > index) { - setDefaultServer(defaultIndex - 1); + const QJsonObject oldJson = m_serverJsonById.value(serverId); + const serverConfigUtils::ConfigType oldKind = serverConfigUtils::configTypeFromJson(withoutStorageServerId(oldJson)); + + m_serverJsonById.remove(serverId); + + const QJsonObject strippedNew = withoutStorageServerId(serverJson); + if (serverConfigUtils::configTypeFromJson(strippedNew) != kind) { + const QJsonObject strippedOld = withoutStorageServerId(oldJson); + if (oldKind != serverConfigUtils::ConfigType::Invalid && serverConfigUtils::configTypeFromJson(strippedOld) == oldKind) { + m_serverJsonById.insert(serverId, embedStorageServerId(serverId, strippedOld)); + } + return; + } + m_serverJsonById.insert(serverId, embedStorageServerId(serverId, strippedNew)); + + syncToStorage(); + emit serverEdited(serverId); +} + +void SecureServersRepository::removeServer(const QString &serverId) +{ + const int removedIndex = indexOfServerId(serverId); + if (removedIndex < 0) { + return; + } + if (!m_serverJsonById.contains(serverId)) { + return; } - if (m_servers.isEmpty()) { - setDefaultServer(0); + const QString previousDefaultId = m_defaultServerId; + const int previousDefaultIndex = defaultServerIndex(); + + m_serverJsonById.remove(serverId); + m_orderedServerIds.removeAt(removedIndex); + + if (m_orderedServerIds.isEmpty()) { + m_defaultServerId.clear(); + } else if (m_defaultServerId == serverId) { + const int fallbackIndex = qMin(removedIndex, m_orderedServerIds.size() - 1); + m_defaultServerId = m_orderedServerIds.at(fallbackIndex); + } else if (!m_orderedServerIds.contains(m_defaultServerId)) { + m_defaultServerId = m_orderedServerIds.first(); + } + + const int newDefaultIndex = defaultServerIndex(); + if (previousDefaultId != m_defaultServerId || previousDefaultIndex != newDefaultIndex) { + emit defaultServerChanged(m_defaultServerId); } syncToStorage(); - emit serverRemoved(index); + emit serverRemoved(serverId, removedIndex); } -ServerConfig SecureServersRepository::server(int index) const +serverConfigUtils::ConfigType SecureServersRepository::serverKind(const QString &serverId) const { - if (index < 0 || index >= m_servers.size()) { - return SelfHostedServerConfig{}; + const auto it = m_serverJsonById.constFind(serverId); + if (it == m_serverJsonById.constEnd()) { + return serverConfigUtils::ConfigType::Invalid; } - return m_servers.at(index); + return serverConfigUtils::configTypeFromJson(withoutStorageServerId(it.value())); } -QVector SecureServersRepository::servers() const +std::optional SecureServersRepository::selfHostedAdminConfig(const QString &serverId) const { - return m_servers; + const auto it = m_serverJsonById.constFind(serverId); + if (it == m_serverJsonById.constEnd()) { + return std::nullopt; + } + const QJsonObject strippedJson = withoutStorageServerId(it.value()); + if (serverConfigUtils::configTypeFromJson(strippedJson) != serverConfigUtils::ConfigType::SelfHostedAdmin) { + return std::nullopt; + } + return SelfHostedAdminServerConfig::fromJson(strippedJson); +} + +std::optional SecureServersRepository::selfHostedUserConfig(const QString &serverId) const +{ + const auto it = m_serverJsonById.constFind(serverId); + if (it == m_serverJsonById.constEnd()) { + return std::nullopt; + } + const QJsonObject strippedJson = withoutStorageServerId(it.value()); + if (serverConfigUtils::configTypeFromJson(strippedJson) != serverConfigUtils::ConfigType::SelfHostedUser) { + return std::nullopt; + } + return SelfHostedUserServerConfig::fromJson(strippedJson); +} + +std::optional SecureServersRepository::nativeConfig(const QString &serverId) const +{ + const auto it = m_serverJsonById.constFind(serverId); + if (it == m_serverJsonById.constEnd()) { + return std::nullopt; + } + const QJsonObject strippedJson = withoutStorageServerId(it.value()); + if (serverConfigUtils::configTypeFromJson(strippedJson) != serverConfigUtils::ConfigType::Native) { + return std::nullopt; + } + return NativeServerConfig::fromJson(strippedJson); +} + +std::optional SecureServersRepository::apiV2Config(const QString &serverId) const +{ + const auto it = m_serverJsonById.constFind(serverId); + if (it == m_serverJsonById.constEnd()) { + return std::nullopt; + } + const QJsonObject strippedJson = withoutStorageServerId(it.value()); + if (!serverConfigUtils::isApiV2Subscription(serverConfigUtils::configTypeFromJson(strippedJson))) { + return std::nullopt; + } + return ApiV2ServerConfig::fromJson(strippedJson); +} + +std::optional SecureServersRepository::legacyApiConfig(const QString &serverId) const +{ + const auto it = m_serverJsonById.constFind(serverId); + if (it == m_serverJsonById.constEnd()) { + return std::nullopt; + } + const QJsonObject strippedJson = withoutStorageServerId(it.value()); + if (!serverConfigUtils::isLegacyApiSubscription(serverConfigUtils::configTypeFromJson(strippedJson))) { + return std::nullopt; + } + return LegacyApiServerConfig::fromJson(strippedJson); } int SecureServersRepository::serversCount() const { - return m_servers.size(); + return m_orderedServerIds.size(); +} + +QString SecureServersRepository::serverIdAt(int index) const +{ + if (index < 0 || index >= m_orderedServerIds.size()) { + return QString(); + } + return m_orderedServerIds.at(index); +} + +QVector SecureServersRepository::orderedServerIds() const +{ + return m_orderedServerIds; +} + +int SecureServersRepository::indexOfServerId(const QString &serverId) const +{ + return m_orderedServerIds.indexOf(serverId); } int SecureServersRepository::defaultServerIndex() const { - return m_defaultServerIndex; + if (m_orderedServerIds.isEmpty()) { + return 0; + } + const int idx = m_orderedServerIds.indexOf(m_defaultServerId); + return idx >= 0 ? idx : 0; } -void SecureServersRepository::setDefaultServer(int index) +QString SecureServersRepository::defaultServerId() const { - if (index < 0) { + return m_defaultServerId; +} + +void SecureServersRepository::setDefaultServer(const QString &serverId) +{ + if (m_orderedServerIds.isEmpty()) { return; } - if (m_servers.size() > 0 && index >= m_servers.size()) { + if (!m_serverJsonById.contains(serverId)) { return; } - if (m_servers.isEmpty() && index != 0) { + + if (indexOfServerId(serverId) < 0) { return; } - if (m_defaultServerIndex == index) { + + if (m_defaultServerId == serverId) { return; } - m_defaultServerIndex = index; - setValue("Servers/defaultServerIndex", index); - emit defaultServerChanged(index); -} -void SecureServersRepository::setDefaultContainer(int serverIndex, DockerContainer container) -{ - ServerConfig config = server(serverIndex); - config.visit([container](auto& arg) { - arg.defaultContainer = container; - }); - editServer(serverIndex, config); -} - -ContainerConfig SecureServersRepository::containerConfig(int serverIndex, DockerContainer container) const -{ - ServerConfig config = server(serverIndex); - return config.containerConfig(container); -} - -void SecureServersRepository::setContainerConfig(int serverIndex, DockerContainer container, const ContainerConfig &config) -{ - ServerConfig serverConfig = server(serverIndex); - serverConfig.visit([container, &config](auto& arg) { - arg.containers[container] = config; - }); - editServer(serverIndex, serverConfig); -} - -void SecureServersRepository::clearLastConnectionConfig(int serverIndex, DockerContainer container) -{ - ServerConfig serverConfig = server(serverIndex); - ContainerConfig containerCfg = serverConfig.containerConfig(container); - - containerCfg.protocolConfig.clearClientConfig(); - - setContainerConfig(serverIndex, container, containerCfg); -} - -ServerCredentials SecureServersRepository::serverCredentials(int index) const -{ - ServerConfig config = server(index); - - if (config.isSelfHosted()) { - const SelfHostedServerConfig* selfHosted = config.as(); - if (!selfHosted) return ServerCredentials(); - auto creds = selfHosted->credentials(); - if (creds.has_value()) { - return creds.value(); - } - } - - return ServerCredentials{}; -} - -bool SecureServersRepository::hasServerWithVpnKey(const QString &vpnKey) const -{ - QString normalizedInput = vpnKey.trimmed(); - if (normalizedInput.startsWith(QStringLiteral("vpn://"), Qt::CaseInsensitive)) { - normalizedInput = normalizedInput.mid(QStringLiteral("vpn://").size()); - } - if (normalizedInput.isEmpty()) { - return false; - } - - QVector serversList = servers(); - for (const ServerConfig& serverConfig : serversList) { - if (serverConfig.isApiV1()) { - const ApiV1ServerConfig* apiV1 = serverConfig.as(); - if (!apiV1) continue; - QString storedKey = apiV1->vpnKey(); - if (storedKey.isEmpty()) { - continue; - } - QString normalizedStored = storedKey.trimmed(); - if (normalizedStored.startsWith(QStringLiteral("vpn://"), Qt::CaseInsensitive)) { - normalizedStored = normalizedStored.mid(QStringLiteral("vpn://").size()); - } - if (normalizedInput == normalizedStored) { - return true; - } - } else if (serverConfig.isApiV2()) { - const ApiV2ServerConfig* apiV2 = serverConfig.as(); - if (!apiV2) continue; - QString storedKey = apiV2->vpnKey(); - if (storedKey.isEmpty()) { - continue; - } - QString normalizedStored = storedKey.trimmed(); - if (normalizedStored.startsWith(QStringLiteral("vpn://"), Qt::CaseInsensitive)) { - normalizedStored = normalizedStored.mid(QStringLiteral("vpn://").size()); - } - if (normalizedInput == normalizedStored) { - return true; - } - } - } - return false; -} - -bool SecureServersRepository::hasServerWithCrc(quint16 crc) const -{ - for (const ServerConfig& serverConfig : m_servers) { - if (static_cast(serverConfig.crc()) == crc) { - return true; - } - } - return false; + m_defaultServerId = serverId; + persistDefaultServerFields(); + emit defaultServerChanged(m_defaultServerId); } diff --git a/client/core/repositories/secureServersRepository.h b/client/core/repositories/secureServersRepository.h index 03c876a71..a0542a62f 100644 --- a/client/core/repositories/secureServersRepository.h +++ b/client/core/repositories/secureServersRepository.h @@ -1,14 +1,20 @@ #ifndef SECURESERVERSREPOSITORY_H #define SECURESERVERSREPOSITORY_H +#include +#include #include #include -#include -#include #include +#include -#include "core/models/serverConfig.h" +#include "core/models/selfhosted/selfHostedAdminServerConfig.h" +#include "core/models/selfhosted/selfHostedUserServerConfig.h" +#include "core/models/selfhosted/nativeServerConfig.h" +#include "core/models/api/apiV2ServerConfig.h" +#include "core/models/api/legacyApiServerConfig.h" #include "core/models/containerConfig.h" +#include "core/utils/serverConfigUtils.h" #include "secureQSettings.h" using namespace amnezia; @@ -18,47 +24,57 @@ class SecureServersRepository : public QObject Q_OBJECT public: - explicit SecureServersRepository(SecureQSettings* settings, QObject *parent = nullptr); + explicit SecureServersRepository(SecureQSettings *settings, QObject *parent = nullptr); + + QString addServer(const QString &serverId, const QJsonObject &serverJson, serverConfigUtils::ConfigType kind); + void editServer(const QString &serverId, const QJsonObject &serverJson, serverConfigUtils::ConfigType kind); + void removeServer(const QString &serverId); + serverConfigUtils::ConfigType serverKind(const QString &serverId) const; + + std::optional selfHostedAdminConfig(const QString &serverId) const; + std::optional selfHostedUserConfig(const QString &serverId) const; + std::optional nativeConfig(const QString &serverId) const; + std::optional apiV2Config(const QString &serverId) const; + std::optional legacyApiConfig(const QString &serverId) const; - void addServer(const ServerConfig &server); - void editServer(int index, const ServerConfig &server); - void removeServer(int index); - ServerConfig server(int index) const; - QVector servers() const; int serversCount() const; + int indexOfServerId(const QString &serverId) const; + QString serverIdAt(int index) const; + QVector orderedServerIds() const; int defaultServerIndex() const; - void setDefaultServer(int index); + QString defaultServerId() const; + void setDefaultServer(const QString &serverId); - void setDefaultContainer(int serverIndex, DockerContainer container); - ContainerConfig containerConfig(int serverIndex, DockerContainer container) const; - void setContainerConfig(int serverIndex, DockerContainer container, const ContainerConfig &config); - void clearLastConnectionConfig(int serverIndex, DockerContainer container); - - ServerCredentials serverCredentials(int index) const; - bool hasServerWithVpnKey(const QString &vpnKey) const; - bool hasServerWithCrc(quint16 crc) const; - - void setServersArray(const QJsonArray &servers); + void clearServers(); void invalidateCache(); signals: - void serverAdded(ServerConfig config); - void serverEdited(int index, ServerConfig config); - void serverRemoved(int index); - void defaultServerChanged(int index); + void serverAdded(const QString &serverId); + void serverEdited(const QString &serverId); + void serverRemoved(const QString &serverId, int removedIndex); + void defaultServerChanged(const QString &defaultServerId); private: + void loadFromStorage(); + void updateDefaultServerFromStorage(); + void persistDefaultServerFields(); + + QString normalizedOrGeneratedServerId(const QString &candidateId) const; + void syncToStorage(); - QVariant value(const QString &key, const QVariant &defaultValue = QVariant()) const; + QVariant value(const QString &key, const QVariant &defaultValue) const; void setValue(const QString &key, const QVariant &value); - - SecureQSettings* m_settings; - - QVector m_servers; - int m_defaultServerIndex = 0; + + void clearServerStateMaps(); + + SecureQSettings *m_settings; + + QHash m_serverJsonById; + QVector m_orderedServerIds; + + QString m_defaultServerId; }; #endif // SECURESERVERSREPOSITORY_H - diff --git a/client/core/utils/api/apiEnums.h b/client/core/utils/api/apiEnums.h deleted file mode 100644 index c52ce98a0..000000000 --- a/client/core/utils/api/apiEnums.h +++ /dev/null @@ -1,25 +0,0 @@ -#ifndef APIENUMS_H -#define APIENUMS_H - -namespace apiDefs -{ - enum ConfigType { - AmneziaFreeV2 = 0, - AmneziaFreeV3, - AmneziaPremiumV1, - AmneziaPremiumV2, - AmneziaTrialV2, - SelfHosted, - ExternalPremium, - ExternalTrial - }; - - enum ConfigSource { - Telegram = 1, - AmneziaGateway - }; -} - -#endif // APIENUMS_H - - diff --git a/client/core/utils/api/apiUtils.cpp b/client/core/utils/api/apiUtils.cpp index eca4689fb..60b78d565 100644 --- a/client/core/utils/api/apiUtils.cpp +++ b/client/core/utils/api/apiUtils.cpp @@ -1,5 +1,6 @@ #include "apiUtils.h" +#include "core/utils/serverConfigUtils.h" #include "core/utils/constants/configKeys.h" #include #include @@ -75,63 +76,6 @@ bool apiUtils::isSubscriptionExpiringSoon(const QString &subscriptionEndDate, in return endDate <= nowUtc.addDays(withinDays); } -bool apiUtils::isServerFromApi(const QJsonObject &serverConfigObject) -{ - auto configVersion = serverConfigObject.value(configKey::configVersion).toInt(); - switch (configVersion) { - case apiDefs::ConfigSource::Telegram: return true; - case apiDefs::ConfigSource::AmneziaGateway: return true; - default: return false; - } -} - -apiDefs::ConfigType apiUtils::getConfigType(const QJsonObject &serverConfigObject) -{ - auto configVersion = serverConfigObject.value(configKey::configVersion).toInt(); - - switch (configVersion) { - case apiDefs::ConfigSource::Telegram: { - constexpr QLatin1String freeV2Endpoint(FREE_V2_ENDPOINT); - constexpr QLatin1String premiumV1Endpoint(PREM_V1_ENDPOINT); - - auto apiEndpoint = serverConfigObject.value(apiDefs::key::apiEndpoint).toString(); - - if (apiEndpoint.contains(premiumV1Endpoint)) { - return apiDefs::ConfigType::AmneziaPremiumV1; - } else if (apiEndpoint.contains(freeV2Endpoint)) { - return apiDefs::ConfigType::AmneziaFreeV2; - } - }; - case apiDefs::ConfigSource::AmneziaGateway: { - constexpr QLatin1String servicePremium("amnezia-premium"); - constexpr QLatin1String serviceFree("amnezia-free"); - constexpr QLatin1String serviceExternalPremium("external-premium"); - constexpr QLatin1String serviceExternalTrial("external-trial"); - - auto apiConfigObject = serverConfigObject.value(apiDefs::key::apiConfig).toObject(); - auto serviceType = apiConfigObject.value(apiDefs::key::serviceType).toString(); - - if (serviceType == servicePremium) { - return apiDefs::ConfigType::AmneziaPremiumV2; - } else if (serviceType == serviceFree) { - return apiDefs::ConfigType::AmneziaFreeV3; - } else if (serviceType == serviceExternalPremium) { - return apiDefs::ConfigType::ExternalPremium; - } else if (serviceType == serviceExternalTrial) { - return apiDefs::ConfigType::ExternalTrial; - } - } - default: { - return apiDefs::ConfigType::SelfHosted; - } - }; -} - -apiDefs::ConfigSource apiUtils::getConfigSource(const QJsonObject &serverConfigObject) -{ - return static_cast(serverConfigObject.value(configKey::configVersion).toInt()); -} - amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList &sslErrors, const QString &replyErrorString, const QNetworkReply::NetworkError &replyError, const int httpStatusCode, const QByteArray &responseBody) @@ -197,14 +141,14 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList &ssl bool apiUtils::isPremiumServer(const QJsonObject &serverConfigObject) { - static const QSet premiumTypes = { apiDefs::ConfigType::AmneziaPremiumV1, apiDefs::ConfigType::AmneziaPremiumV2, - apiDefs::ConfigType::ExternalPremium, apiDefs::ConfigType::ExternalTrial }; - return premiumTypes.contains(getConfigType(serverConfigObject)); + static const QSet premiumTypes = { serverConfigUtils::ConfigType::AmneziaPremiumV1, serverConfigUtils::ConfigType::AmneziaPremiumV2, + serverConfigUtils::ConfigType::ExternalPremium }; + return premiumTypes.contains(serverConfigUtils::configTypeFromJson(serverConfigObject)); } QString apiUtils::getPremiumV1VpnKey(const QJsonObject &serverConfigObject) { - if (apiUtils::getConfigType(serverConfigObject) != apiDefs::ConfigType::AmneziaPremiumV1) { + if (serverConfigUtils::configTypeFromJson(serverConfigObject) != serverConfigUtils::ConfigType::AmneziaPremiumV1) { return {}; } @@ -242,9 +186,8 @@ QString apiUtils::getPremiumV1VpnKey(const QJsonObject &serverConfigObject) QString apiUtils::getPremiumV2VpnKey(const QJsonObject &serverConfigObject) { - auto configType = apiUtils::getConfigType(serverConfigObject); - if (configType != apiDefs::ConfigType::AmneziaPremiumV2 && configType != apiDefs::ConfigType::ExternalPremium - && configType != apiDefs::ConfigType::ExternalTrial) { + auto configType = serverConfigUtils::configTypeFromJson(serverConfigObject); + if (configType != serverConfigUtils::ConfigType::AmneziaPremiumV2 && configType != serverConfigUtils::ConfigType::ExternalPremium) { return {}; } diff --git a/client/core/utils/api/apiUtils.h b/client/core/utils/api/apiUtils.h index be770defa..e1ada61ae 100644 --- a/client/core/utils/api/apiUtils.h +++ b/client/core/utils/api/apiUtils.h @@ -4,7 +4,7 @@ #include #include -#include "core/utils/api/apiEnums.h" +#include "core/utils/serverConfigUtils.h" #include "core/utils/constants/apiKeys.h" #include "core/utils/constants/apiConstants.h" #include "core/utils/errorCodes.h" @@ -13,17 +13,12 @@ namespace apiUtils { - bool isServerFromApi(const QJsonObject &serverConfigObject); - bool isSubscriptionExpired(const QString &subscriptionEndDate); bool isSubscriptionExpiringSoon(const QString &subscriptionEndDate, int withinDays = 30); bool isPremiumServer(const QJsonObject &serverConfigObject); - apiDefs::ConfigType getConfigType(const QJsonObject &serverConfigObject); - apiDefs::ConfigSource getConfigSource(const QJsonObject &serverConfigObject); - amnezia::ErrorCode checkNetworkReplyErrors(const QList &sslErrors, const QString &replyErrorString, const QNetworkReply::NetworkError &replyError, const int httpStatusCode, const QByteArray &responseBody); diff --git a/client/core/utils/constants/apiConstants.h b/client/core/utils/constants/apiConstants.h index b9895bc82..148ae2e36 100644 --- a/client/core/utils/constants/apiConstants.h +++ b/client/core/utils/constants/apiConstants.h @@ -3,9 +3,9 @@ namespace apiDefs { - const int requestTimeoutMsecs = 12 * 1000; // 12 secs -} + +constexpr int requestTimeoutMsecs = 12 * 1000; // 12 secs + +} // namespace apiDefs #endif // APICONSTANTS_H - - diff --git a/client/core/utils/constants/apiKeys.h b/client/core/utils/constants/apiKeys.h index 2e037bca6..46833706c 100644 --- a/client/core/utils/constants/apiKeys.h +++ b/client/core/utils/constants/apiKeys.h @@ -2,7 +2,6 @@ #define APIKEYS_H #include -#include "core/utils/api/apiEnums.h" namespace apiDefs { @@ -82,7 +81,7 @@ namespace apiDefs constexpr QLatin1String expiresAt("expires_at"); constexpr QLatin1String isConnectEvent("is_connect_event"); constexpr QLatin1String certificate("certificate"); - } -} + } // namespace key +} // namespace apiDefs #endif // APIKEYS_H diff --git a/client/core/utils/constants/configKeys.h b/client/core/utils/constants/configKeys.h index 40bc842b1..3272eb2bb 100644 --- a/client/core/utils/constants/configKeys.h +++ b/client/core/utils/constants/configKeys.h @@ -18,6 +18,7 @@ namespace amnezia constexpr QLatin1String serverIndex("serverIndex"); constexpr QLatin1String description("description"); + constexpr QLatin1String displayName("displayName"); constexpr QLatin1String name("name"); constexpr QLatin1String cert("cert"); constexpr QLatin1String accessToken("api_key"); @@ -121,6 +122,8 @@ namespace amnezia constexpr QLatin1String latestHandshake("latestHandshake"); constexpr QLatin1String dataReceived("dataReceived"); constexpr QLatin1String dataSent("dataSent"); + + constexpr QLatin1String storageServerId("storageServerId"); } } diff --git a/client/core/utils/errorCodes.h b/client/core/utils/errorCodes.h index 00e8c6b20..0750553be 100644 --- a/client/core/utils/errorCodes.h +++ b/client/core/utils/errorCodes.h @@ -71,10 +71,11 @@ namespace amnezia // import and install errors ImportInvalidConfigError = 900, - ImportBackupFileUseRestoreInstead = 903, - RestoreBackupInvalidError = 904, ImportOpenConfigError = 901, NoInstalledContainersError = 902, + ImportBackupFileUseRestoreInstead = 903, + RestoreBackupInvalidError = 904, + LegacyApiV1NotSupportedError = 905, // Android errors AndroidError = 1000, diff --git a/client/core/utils/errorStrings.cpp b/client/core/utils/errorStrings.cpp index 591716ec9..05e481303 100644 --- a/client/core/utils/errorStrings.cpp +++ b/client/core/utils/errorStrings.cpp @@ -59,6 +59,7 @@ QString errorString(ErrorCode code) { case (ErrorCode::ImportInvalidConfigError): errorMessage = QObject::tr("The config does not contain any containers and credentials for connecting to the server"); break; case (ErrorCode::ImportBackupFileUseRestoreInstead): errorMessage = QObject::tr("Backup files cannot be imported here. Use 'Restore from backup' instead."); break; case (ErrorCode::RestoreBackupInvalidError): errorMessage = QObject::tr("Backup file is corrupted or has invalid format"); break; + case (ErrorCode::LegacyApiV1NotSupportedError): errorMessage = QObject::tr("This legacy Amnezia subscription format is no longer supported"); break; case (ErrorCode::ImportOpenConfigError): errorMessage = QObject::tr("Unable to open config file"); break; case (ErrorCode::NoInstalledContainersError): errorMessage = QObject::tr("VPN Protocols is not installed.\n Please install VPN container at first"); break; diff --git a/client/core/utils/serverConfigUtils.cpp b/client/core/utils/serverConfigUtils.cpp new file mode 100644 index 000000000..c6ec2a0fe --- /dev/null +++ b/client/core/utils/serverConfigUtils.cpp @@ -0,0 +1,122 @@ +#include "serverConfigUtils.h" + +#include +#include + +#include "core/models/selfhosted/selfHostedAdminServerConfig.h" +#include "core/utils/constants/apiKeys.h" +#include "core/utils/constants/configKeys.h" + +namespace +{ + +bool hasThirdPartyConfig(const QJsonObject &json) +{ + const QJsonArray containersArray = json.value(amnezia::configKey::containers).toArray(); + for (const QJsonValue &val : containersArray) { + const QJsonObject containerObj = val.toObject(); + for (auto it = containerObj.begin(); it != containerObj.end(); ++it) { + if (it.key() == amnezia::configKey::container) { + continue; + } + const QJsonObject protocolObj = it.value().toObject(); + if (protocolObj.contains(amnezia::configKey::isThirdPartyConfig) + && protocolObj.value(amnezia::configKey::isThirdPartyConfig).toBool()) { + return true; + } + } + } + return false; +} + +} // namespace + +namespace serverConfigUtils +{ + +bool isServerFromApi(const QJsonObject &serverConfigObject) +{ + const int configVersion = serverConfigObject.value(amnezia::configKey::configVersion).toInt(); + switch (configVersion) { + case ConfigSource::Telegram: + case ConfigSource::AmneziaGateway: + return true; + default: + return false; + } +} + +ConfigSource getConfigSource(const QJsonObject &serverConfigObject) +{ + return static_cast(serverConfigObject.value(amnezia::configKey::configVersion).toInt()); +} + +ConfigType configTypeFromJson(const QJsonObject &serverConfigObject) +{ + const int configVersion = serverConfigObject.value(amnezia::configKey::configVersion).toInt(); + + switch (configVersion) { + case ConfigSource::Telegram: { + constexpr QLatin1String freeV2Endpoint(FREE_V2_ENDPOINT); + constexpr QLatin1String premiumV1Endpoint(PREM_V1_ENDPOINT); + + const QString apiEndpointValue = serverConfigObject.value(apiDefs::key::apiEndpoint).toString(); + + if (apiEndpointValue.contains(premiumV1Endpoint)) { + return ConfigType::AmneziaPremiumV1; + } + if (apiEndpointValue.contains(freeV2Endpoint)) { + return ConfigType::AmneziaFreeV2; + } + } + [[fallthrough]]; + case ConfigSource::AmneziaGateway: { + constexpr QLatin1String servicePremium("amnezia-premium"); + constexpr QLatin1String serviceFree("amnezia-free"); + constexpr QLatin1String serviceExternalPremium("external-premium"); + + const QJsonObject apiConfigObject = serverConfigObject.value(apiDefs::key::apiConfig).toObject(); + const QString serviceTypeStr = apiConfigObject.value(apiDefs::key::serviceType).toString(); + + if (serviceTypeStr == servicePremium) { + return ConfigType::AmneziaPremiumV2; + } + if (serviceTypeStr == serviceFree) { + return ConfigType::AmneziaFreeV3; + } + if (serviceTypeStr == serviceExternalPremium) { + return ConfigType::ExternalPremium; + } + break; + } + default: + break; + } + + if (hasThirdPartyConfig(serverConfigObject)) { + return ConfigType::Native; + } + + const amnezia::SelfHostedAdminServerConfig adminProbe = + amnezia::SelfHostedAdminServerConfig::fromJson(serverConfigObject); + return adminProbe.hasCredentials() ? ConfigType::SelfHostedAdmin : ConfigType::SelfHostedUser; +} + +bool isLegacyApiSubscription(ConfigType configType) +{ + return configType == ConfigType::AmneziaPremiumV1 || configType == ConfigType::AmneziaFreeV2; +} + +bool isApiV2Subscription(ConfigType configType) +{ + switch (configType) { + case ConfigType::AmneziaPremiumV2: + case ConfigType::AmneziaFreeV3: + case ConfigType::ExternalPremium: + return true; + default: + return false; + } +} + +} // namespace serverConfigUtils diff --git a/client/core/utils/serverConfigUtils.h b/client/core/utils/serverConfigUtils.h new file mode 100644 index 000000000..9657c0da9 --- /dev/null +++ b/client/core/utils/serverConfigUtils.h @@ -0,0 +1,40 @@ +#ifndef SERVERCONFIGUTILS_H +#define SERVERCONFIGUTILS_H + +#include + +namespace serverConfigUtils +{ + +enum ConfigType { + AmneziaFreeV2 = 0, + AmneziaFreeV3, + AmneziaPremiumV1, + AmneziaPremiumV2, + SelfHosted, + ExternalPremium, + + SelfHostedAdmin = 8, + SelfHostedUser, + Native, + Invalid +}; + +enum ConfigSource { + Telegram = 1, + AmneziaGateway +}; + +bool isServerFromApi(const QJsonObject &serverConfigObject); + +ConfigSource getConfigSource(const QJsonObject &serverConfigObject); + +ConfigType configTypeFromJson(const QJsonObject &serverConfigObject); + +bool isLegacyApiSubscription(ConfigType configType); + +bool isApiV2Subscription(ConfigType configType); + +} // namespace serverConfigUtils + +#endif // SERVERCONFIGUTILS_H diff --git a/client/tests/CMakeLists.txt b/client/tests/CMakeLists.txt index e541a4e6f..a8ba12246 100644 --- a/client/tests/CMakeLists.txt +++ b/client/tests/CMakeLists.txt @@ -95,15 +95,6 @@ target_link_libraries(test_servers_model_sync PRIVATE test_common ) -add_executable(test_gateway_stacks - testGatewayStacks.cpp -) - -target_link_libraries(test_gateway_stacks PRIVATE - Qt6::Test - test_common -) - add_executable(test_complex_operations testComplexOperations.cpp ) @@ -148,7 +139,6 @@ add_test(NAME DefaultServerChangeTest COMMAND test_default_server_change) add_test(NAME ServerEdgeCasesTest COMMAND test_server_edge_cases) add_test(NAME SignalOrderTest COMMAND test_signal_order) add_test(NAME ServersModelSyncTest COMMAND test_servers_model_sync) -add_test(NAME GatewayStacksTest COMMAND test_gateway_stacks) add_test(NAME ComplexOperationsTest COMMAND test_complex_operations) add_test(NAME SettingsSignalsTest COMMAND test_settings_signals) add_test(NAME UiServersModelAndControllerTest COMMAND test_ui_servers_model_and_controller) diff --git a/client/tests/testAdminSelfHostedExport.cpp b/client/tests/testAdminSelfHostedExport.cpp index 9cd8a6976..c2a4065f8 100644 --- a/client/tests/testAdminSelfHostedExport.cpp +++ b/client/tests/testAdminSelfHostedExport.cpp @@ -8,7 +8,7 @@ #include #include "core/controllers/coreController.h" -#include "core/models/serverConfig.h" +#include "core/utils/constants/configKeys.h" #include "vpnConnection.h" #include "secureQSettings.h" @@ -117,8 +117,8 @@ private slots: QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged signal should NOT be emitted (default is already 0)"); QVERIFY2(m_coreController->m_serversRepository->serversCount() > 0, "Server should be added"); - int serverIndex = m_coreController->m_serversRepository->defaultServerIndex(); - auto exportResult = m_coreController->m_exportController->generateFullAccessConfig(serverIndex); + const QString serverId = m_coreController->m_serversRepository->defaultServerId(); + auto exportResult = m_coreController->m_exportController->generateFullAccessConfig(serverId); QVERIFY2(exportResult.errorCode == ErrorCode::NoError, "Export should succeed"); QVERIFY2(!exportResult.config.isEmpty(), "Exported config should not be empty"); diff --git a/client/tests/testComplexOperations.cpp b/client/tests/testComplexOperations.cpp index 878a12510..1117b5e13 100644 --- a/client/tests/testComplexOperations.cpp +++ b/client/tests/testComplexOperations.cpp @@ -5,7 +5,8 @@ #include #include "core/controllers/coreController.h" -#include "core/models/serverConfig.h" +#include "core/models/serverDescription.h" +#include "tests/testServerRepositoryHelpers.h" #include "vpnConnection.h" #include "secureQSettings.h" @@ -37,7 +38,7 @@ private slots: void init() { m_settings->clearSettings(); if (m_coreController->m_serversModel) { - m_coreController->m_serversModel->updateModel(QVector(), -1, false); + m_coreController->m_serversModel->updateModel(QVector(), -1); } } @@ -65,35 +66,33 @@ private slots: QVERIFY2(m_coreController->m_serversRepository->serversCount() == 3, "Should have 3 servers"); QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 2, "Default should be index 2"); - ServerConfig server0 = m_coreController->m_serversController->getServerConfig(0); - server0.visit([](auto& arg) { - arg.description = "Edited First Server"; - }); - m_coreController->m_serversController->editServer(0, server0); + amnezia::test::setServerDescription(m_coreController->m_serversRepository, + m_coreController->m_serversController->getServerId(0), + QStringLiteral("Edited First Server")); QVERIFY2(serverEditedSpy.count() == 1, "serverEdited should be emitted"); - QString editedDesc0 = m_coreController->m_serversRepository->server(0).description(); + QString editedDesc0 = amnezia::test::serverDescription(m_coreController->m_serversRepository, + m_coreController->m_serversRepository->serverIdAt(0)); QVERIFY2(editedDesc0 == "Edited First Server", "First server should be edited"); - m_coreController->m_serversController->removeServer(1); + m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(1)); QVERIFY2(serverRemovedSpy.count() == 1, "serverRemoved should be emitted"); QVERIFY2(m_coreController->m_serversRepository->serversCount() == 2, "Should have 2 servers"); QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Default should be index 1 (was 2, removed 1)"); - m_coreController->m_serversController->setDefaultServerIndex(0); + m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(0)); QVERIFY2(defaultServerChangedSpy.count() == 4, "defaultServerChanged should be emitted again"); QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "Default should be index 0"); - ServerConfig server0After = m_coreController->m_serversController->getServerConfig(0); - server0After.visit([](auto& arg) { - arg.description = "Final Edited Server"; - }); - m_coreController->m_serversController->editServer(0, server0After); + amnezia::test::setServerDescription(m_coreController->m_serversRepository, + m_coreController->m_serversController->getServerId(0), + QStringLiteral("Final Edited Server")); QVERIFY2(serverEditedSpy.count() == 2, "serverEdited should be emitted again"); - QString finalDesc0 = m_coreController->m_serversRepository->server(0).description(); + QString finalDesc0 = amnezia::test::serverDescription(m_coreController->m_serversRepository, + m_coreController->m_serversRepository->serverIdAt(0)); QVERIFY2(finalDesc0 == "Final Edited Server", "First server should be edited again"); QVERIFY2(m_coreController->m_serversRepository->serversCount() == 2, "Final servers count should be 2"); diff --git a/client/tests/testDefaultServerChange.cpp b/client/tests/testDefaultServerChange.cpp index adffde62f..f0fb6163b 100644 --- a/client/tests/testDefaultServerChange.cpp +++ b/client/tests/testDefaultServerChange.cpp @@ -5,7 +5,8 @@ #include #include "core/controllers/coreController.h" -#include "core/models/serverConfig.h" +#include "core/models/serverDescription.h" +#include "tests/testServerRepositoryHelpers.h" #include "ui/models/serversModel.h" #include "vpnConnection.h" #include "secureQSettings.h" @@ -39,7 +40,7 @@ private slots: m_settings->clearSettings(); m_coreController->m_serversRepository->invalidateCache(); if (m_coreController->m_serversModel) { - m_coreController->m_serversModel->updateModel(QVector(), -1, false); + m_coreController->m_serversModel->updateModel(QVector(), -1); } } @@ -60,9 +61,10 @@ private slots: QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged); - m_coreController->m_serversController->setDefaultServerIndex(0); + m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(0)); QVERIFY2(defaultServerChangedSpy.count() == 1, "defaultServerChanged signal should be emitted"); - QVERIFY2(defaultServerChangedSpy.at(0).at(0).toInt() == 0, "defaultServerChanged should emit index 0"); + QVERIFY2(defaultServerChangedSpy.at(0).at(0).toString() == m_coreController->m_serversController->getServerId(0), + "defaultServerChanged should emit new default server id"); QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "Default server index should be 0"); if (m_coreController->m_serversModel) { @@ -70,9 +72,10 @@ private slots: QVERIFY2(modelDefaultIndex == 0, "Model should reflect default server"); } - m_coreController->m_serversController->setDefaultServerIndex(2); + m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(2)); QVERIFY2(defaultServerChangedSpy.count() == 2, "defaultServerChanged signal should be emitted again"); - QVERIFY2(defaultServerChangedSpy.at(1).at(0).toInt() == 2, "defaultServerChanged should emit index 2"); + QVERIFY2(defaultServerChangedSpy.at(1).at(0).toString() == m_coreController->m_serversController->getServerId(2), + "defaultServerChanged should emit new default server id"); QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 2, "Default server index should be 2"); } @@ -94,28 +97,28 @@ private slots: QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged); QSignalSpy serverRemovedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverRemoved); - m_coreController->m_serversController->removeServer(0); + m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(0)); QVERIFY2(serverRemovedSpy.count() == 1, "serverRemoved signal should be emitted"); QVERIFY2(m_coreController->m_serversRepository->serversCount() == 2, "Should have 2 servers"); QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Default should be index 1 (was 2, removed 0)"); - ServerConfig remainingServer1 = m_coreController->m_serversRepository->server(0); - ServerConfig remainingServer2 = m_coreController->m_serversRepository->server(1); - QString desc1 = remainingServer1.description(); - QString desc2 = remainingServer2.description(); + QString desc1 = amnezia::test::serverDescription(m_coreController->m_serversRepository, + m_coreController->m_serversRepository->serverIdAt(0)); + QString desc2 = amnezia::test::serverDescription(m_coreController->m_serversRepository, + m_coreController->m_serversRepository->serverIdAt(1)); QVERIFY2(desc1 == "Xray Server", "First remaining server should be Xray"); QVERIFY2(desc2 == "WireGuard Server", "Second remaining server should be WireGuard"); defaultServerChangedSpy.clear(); serverRemovedSpy.clear(); - m_coreController->m_serversController->removeServer(0); + m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(0)); QVERIFY2(serverRemovedSpy.count() == 1, "serverRemoved signal should be emitted"); QVERIFY2(m_coreController->m_serversRepository->serversCount() == 1, "Should have 1 server"); QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "Default should be index 0 (was 1, removed 0)"); - ServerConfig lastServer = m_coreController->m_serversRepository->server(0); - QString lastDesc = lastServer.description(); + QString lastDesc = amnezia::test::serverDescription(m_coreController->m_serversRepository, + m_coreController->m_serversRepository->serverIdAt(0)); QVERIFY2(lastDesc == "WireGuard Server", "Last server should be WireGuard"); } }; diff --git a/client/tests/testGatewayStacks.cpp b/client/tests/testGatewayStacks.cpp deleted file mode 100644 index acffe39bb..000000000 --- a/client/tests/testGatewayStacks.cpp +++ /dev/null @@ -1,75 +0,0 @@ -#include -#include -#include -#include -#include - -#include "core/controllers/coreController.h" -#include "core/models/serverConfig.h" -#include "vpnConnection.h" -#include "secureQSettings.h" - -using namespace amnezia; - -class TestGatewayStacks : public QObject -{ - Q_OBJECT - -private: - CoreController* m_coreController; - SecureQSettings* m_settings; - -private slots: - void initTestCase() { - QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString(); - m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false); - - auto vpnConnection = QSharedPointer::create(nullptr, nullptr); - m_coreController = new CoreController(vpnConnection, m_settings, nullptr, this); - } - - void cleanupTestCase() { - m_settings->clearSettings(); - delete m_coreController; - delete m_settings; - } - - void init() { - m_settings->clearSettings(); - if (m_coreController->m_serversModel) { - m_coreController->m_serversModel->updateModel(QVector(), -1, false); - } - } - - void testGatewayStacksRecomputeOnServerOperations() { - QString awgKey = "vpn://AAABFHjadZBBT4QwEIX_ipkzS2wBJdyMB1cPXvbgwRgyQnclgZa0RTYS_rszXRa52Mt77TfzOu0EldEeG62sg-J9AhxPUEywF1CAuF3WTl4dRLCXhJIVpVuUEMpWdLdFKaH7FeUb9Mx3scpFk0XTRbOLvlSkKZsOz-Gi4BsdRiV_EGEydhwlg0tWynEZmd5Yz1bkoaK3xpvKtOU3_UFjOE3SsRs-tfIl1rVVzoWQOI9FzC3eonYcU4ZmgkPdwxz9fSYdYafVT4M7-lEJ80cEtTri0PrH_2q4wlW26f1lioe3p5uDsjQWoS_j_Ct2ipvGU6zO2PWtiivT8RPQudHYmqBXzl-3Yn2slBEMTtklgYt4C_Mv3ROMwA"; - - QSignalSpy gatewayStacksExpandedSpy(m_coreController->m_serversController, &ServersController::gatewayStacksExpanded); - QSignalSpy serverAddedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverAdded); - QSignalSpy serverEditedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverEdited); - QSignalSpy serverRemovedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverRemoved); - - auto importResult = m_coreController->m_importCoreController->extractConfigFromData(awgKey); - m_coreController->m_importCoreController->importConfig(importResult.config); - - QVERIFY2(serverAddedSpy.count() == 1, "serverAdded signal should be emitted"); - QVERIFY2(m_coreController->m_serversController->gatewayStacks().isEmpty(), "Gateway stacks should be empty for self-hosted servers"); - - ServerConfig serverConfig = m_coreController->m_serversController->getServerConfig(0); - serverConfig.visit([](auto& arg) { - arg.description = "Edited Server"; - }); - m_coreController->m_serversController->editServer(0, serverConfig); - - QVERIFY2(serverEditedSpy.count() == 1, "serverEdited signal should be emitted"); - - m_coreController->m_serversController->removeServer(0); - - QVERIFY2(serverRemovedSpy.count() == 1, "serverRemoved signal should be emitted"); - QVERIFY2(m_coreController->m_serversController->gatewayStacks().isEmpty(), "Gateway stacks should remain empty"); - } -}; - -QTEST_MAIN(TestGatewayStacks) -#include "testGatewayStacks.moc" - diff --git a/client/tests/testMultipleImports.cpp b/client/tests/testMultipleImports.cpp index 44a0acd3c..932c0c902 100644 --- a/client/tests/testMultipleImports.cpp +++ b/client/tests/testMultipleImports.cpp @@ -6,7 +6,8 @@ #include #include "core/controllers/coreController.h" -#include "core/models/serverConfig.h" +#include "core/models/serverDescription.h" +#include "tests/testServerRepositoryHelpers.h" #include "vpnConnection.h" #include "secureQSettings.h" @@ -40,7 +41,7 @@ private slots: m_settings->clearSettings(); m_coreController->m_serversRepository->invalidateCache(); if (m_coreController->m_serversModel) { - m_coreController->m_serversModel->updateModel(QVector(), -1, false); + m_coreController->m_serversModel->updateModel(QVector(), -1); } } @@ -70,8 +71,8 @@ private slots: } QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "First server should be default"); - ServerConfig server1 = m_coreController->m_serversRepository->server(0); - QString desc1 = server1.description(); + QString desc1 = amnezia::test::serverDescription(m_coreController->m_serversRepository, + m_coreController->m_serversRepository->serverIdAt(0)); QVERIFY2(desc1 == "AWG Server", "First server description should match"); if (m_coreController->m_serversModel) { @@ -92,8 +93,8 @@ private slots: } QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Second server should be default"); - ServerConfig server2 = m_coreController->m_serversRepository->server(1); - QString desc2 = server2.description(); + QString desc2 = amnezia::test::serverDescription(m_coreController->m_serversRepository, + m_coreController->m_serversRepository->serverIdAt(1)); QVERIFY2(desc2 == "Xray Server", "Second server description should match"); if (m_coreController->m_serversModel) { @@ -114,8 +115,8 @@ private slots: } QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 2, "Third server should be default"); - ServerConfig server3 = m_coreController->m_serversRepository->server(2); - QString desc3 = server3.description(); + QString desc3 = amnezia::test::serverDescription(m_coreController->m_serversRepository, + m_coreController->m_serversRepository->serverIdAt(2)); QVERIFY2(desc3 == "WireGuard Server", "Third server description should match"); if (m_coreController->m_serversModel) { @@ -147,25 +148,25 @@ private slots: QVERIFY2(m_coreController->m_serversRepository->serversCount() == 2, "After two imports servers count should be 2"); QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Second server should be default"); - ServerConfig server0 = m_coreController->m_serversRepository->server(0); - ServerConfig server1 = m_coreController->m_serversRepository->server(1); - QString desc0 = server0.description(); - QString desc1 = server1.description(); + QString desc0 = amnezia::test::serverDescription(m_coreController->m_serversRepository, + m_coreController->m_serversRepository->serverIdAt(0)); + QString desc1 = amnezia::test::serverDescription(m_coreController->m_serversRepository, + m_coreController->m_serversRepository->serverIdAt(1)); QVERIFY2(desc0 == "AWG Server", "First server description should match"); QVERIFY2(desc1 == "Xray Server", "Second server description should match"); defaultServerChangedSpy.clear(); serverRemovedSpy.clear(); - m_coreController->m_serversController->removeServer(0); + m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(0)); QVERIFY2(serverRemovedSpy.count() == 1, "serverRemoved signal should be emitted"); - QVERIFY2(serverRemovedSpy.at(0).at(0).toInt() == 0, "serverRemoved should emit index 0"); + QVERIFY2(serverRemovedSpy.at(0).at(1).toInt() == 0, "serverRemoved should emit removed index 0"); QVERIFY2(m_coreController->m_serversRepository->serversCount() == 1, "After removing first server, servers count should be 1"); QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "After removing first server, default index should be 0"); - ServerConfig remainingServer = m_coreController->m_serversRepository->server(0); - QString remainingDesc = remainingServer.description(); + QString remainingDesc = amnezia::test::serverDescription(m_coreController->m_serversRepository, + m_coreController->m_serversRepository->serverIdAt(0)); QVERIFY2(remainingDesc == "Xray Server", "Remaining server should be Xray Server"); if (m_coreController->m_serversModel) { @@ -177,10 +178,10 @@ private slots: defaultServerChangedSpy.clear(); serverRemovedSpy.clear(); - m_coreController->m_serversController->removeServer(0); + m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(0)); QVERIFY2(serverRemovedSpy.count() == 1, "serverRemoved signal should be emitted"); - QVERIFY2(serverRemovedSpy.at(0).at(0).toInt() == 0, "serverRemoved should emit index 0"); + QVERIFY2(serverRemovedSpy.at(0).at(1).toInt() == 0, "serverRemoved should emit removed index 0"); QVERIFY2(m_coreController->m_serversRepository->serversCount() == 0, "After removing last server, servers count should be 0"); QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "After removing last server, default index should be 0"); diff --git a/client/tests/testSelfHostedServerSetup.cpp b/client/tests/testSelfHostedServerSetup.cpp index a44725556..fa1a783fb 100644 --- a/client/tests/testSelfHostedServerSetup.cpp +++ b/client/tests/testSelfHostedServerSetup.cpp @@ -7,8 +7,8 @@ #include #include "core/controllers/coreController.h" -#include "core/models/serverConfig.h" -#include "core/models/selfhosted/selfHostedServerConfig.h" +#include "core/models/serverDescription.h" +#include "core/models/selfhosted/selfHostedAdminServerConfig.h" #include "core/models/containerConfig.h" #include "core/models/protocols/awgProtocolConfig.h" #include "core/models/protocols/dnsProtocolConfig.h" @@ -60,21 +60,24 @@ private: qDebug() << "SSH connection successful. Output:" << sshOutput; } - void verifyAdminAccess(int serverIndex) { - ServerConfig server = m_coreController->m_serversRepository->server(serverIndex); - const SelfHostedServerConfig* selfHosted = server.as(); - QVERIFY2(selfHosted != nullptr, "Server config should be SelfHostedServerConfig"); + void verifyAdminAccess(int serverIndex) + { + const QString serverId = m_coreController->m_serversRepository->serverIdAt(serverIndex); + const auto adminCfg = m_coreController->m_serversRepository->selfHostedAdminConfig(serverId); + QVERIFY2(adminCfg.has_value(), "Server config should be SelfHostedAdminServerConfig"); + + const SelfHostedAdminServerConfig &selfHosted = *adminCfg; - QVERIFY2(selfHosted->hasCredentials(), + QVERIFY2(selfHosted.hasCredentials(), "Server should have credentials (admin access)"); - QVERIFY2(selfHosted->userName.has_value() && !selfHosted->userName.value().isEmpty(), + QVERIFY2(!selfHosted.userName.isEmpty(), "Server should have userName for admin access"); - QVERIFY2(selfHosted->password.has_value() && !selfHosted->password.value().isEmpty(), + QVERIFY2(!selfHosted.password.isEmpty(), "Server should have password for admin access"); - QVERIFY2(!selfHosted->isReadOnly(), + QVERIFY2(!selfHosted.isReadOnly(), "Server should not be read-only (should have admin access)"); if (m_coreController->m_serversModel) { @@ -143,7 +146,7 @@ private slots: void init() { m_settings->clearSettings(); if (m_coreController->m_serversModel) { - m_coreController->m_serversModel->updateModel(QVector(), -1, false); + m_coreController->m_serversModel->updateModel(QVector(), -1); } } @@ -177,10 +180,10 @@ private slots: int serverIndex = m_coreController->m_serversRepository->serversCount() - 1; qDebug() << "Server with Awg container added at index:" << serverIndex; - ServerConfig serverAfterAwg = m_coreController->m_serversRepository->server(serverIndex); - QVERIFY2(serverAfterAwg.isSelfHosted(), "Server should be self-hosted"); - const SelfHostedServerConfig* selfHostedAfterAwg = serverAfterAwg.as(); - QVERIFY2(selfHostedAfterAwg != nullptr, "Server config should be SelfHostedServerConfig"); + const auto adminAfterAwg = m_coreController->m_serversRepository->selfHostedAdminConfig( + m_coreController->m_serversRepository->serverIdAt(serverIndex)); + QVERIFY2(adminAfterAwg.has_value(), "Server should be self-hosted (admin)"); + const SelfHostedAdminServerConfig *selfHostedAfterAwg = &(*adminAfterAwg); QVERIFY2(selfHostedAfterAwg->defaultContainer == DockerContainer::Awg, "Default container should be Awg"); QVERIFY2(selfHostedAfterAwg->containers.contains(DockerContainer::Awg), "Server should have Awg container"); @@ -198,8 +201,9 @@ private slots: TransportProto dnsTransportProto = TransportProto::Udp; bool wasDnsInstalled = false; + const QString serverIdForOps = m_coreController->m_serversRepository->serverIdAt(serverIndex); ErrorCode installContainerError = m_coreController->m_installController->installContainer( - serverIndex, DockerContainer::Dns, dnsPort, dnsTransportProto, wasDnsInstalled); + serverIdForOps, DockerContainer::Dns, dnsPort, dnsTransportProto, wasDnsInstalled); QVERIFY2(installContainerError == ErrorCode::NoError, QString("installContainer for Dns should succeed. Error: %1") @@ -207,9 +211,10 @@ private slots: .toUtf8().constData()); qDebug() << "Dns container installed:" << wasDnsInstalled; - ServerConfig serverAfterDns = m_coreController->m_serversRepository->server(serverIndex); - const SelfHostedServerConfig* selfHostedAfterDns = serverAfterDns.as(); - QVERIFY2(selfHostedAfterDns != nullptr, "Server config should be SelfHostedServerConfig"); + const auto adminAfterDns = m_coreController->m_serversRepository->selfHostedAdminConfig( + m_coreController->m_serversRepository->serverIdAt(serverIndex)); + QVERIFY2(adminAfterDns.has_value(), "Server config should be SelfHostedAdminServerConfig"); + const SelfHostedAdminServerConfig *selfHostedAfterDns = &(*adminAfterDns); QVERIFY2(selfHostedAfterDns->containers.contains(DockerContainer::Awg), "Server should still have Awg container"); QVERIFY2(selfHostedAfterDns->containers.contains(DockerContainer::Dns), "Server should have Dns container"); QVERIFY2(selfHostedAfterDns->containers.size() == 2, @@ -242,16 +247,18 @@ private slots: verifySshConnection(credentials); - SelfHostedServerConfig serverConfig; + SelfHostedAdminServerConfig serverConfig; serverConfig.hostName = credentials.hostName; serverConfig.userName = credentials.userName; serverConfig.password = credentials.secretData; serverConfig.port = credentials.port; serverConfig.description = m_coreController->m_appSettingsRepository->nextAvailableServerName(); + serverConfig.displayName = serverConfig.description.isEmpty() ? serverConfig.hostName : serverConfig.description; serverConfig.defaultContainer = DockerContainer::None; QSignalSpy serverAddedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverAdded); - m_coreController->m_serversController->addServer(ServerConfig(serverConfig)); + m_coreController->m_serversRepository->addServer(QString(), serverConfig.toJson(), + serverConfigUtils::ConfigType::SelfHostedAdmin); QVERIFY2(serverAddedSpy.count() == 1, "serverAdded signal should be emitted"); QVERIFY2(m_coreController->m_serversRepository->serversCount() > 0, "Server should be added"); @@ -259,23 +266,25 @@ private slots: int serverIndex = m_coreController->m_serversRepository->serversCount() - 1; qDebug() << "Empty server added at index:" << serverIndex; - ServerConfig addedServer = m_coreController->m_serversRepository->server(serverIndex); - QVERIFY2(addedServer.isSelfHosted(), "Added server should be self-hosted"); - const SelfHostedServerConfig* selfHosted = addedServer.as(); - QVERIFY2(selfHosted != nullptr, "Server config should be SelfHostedServerConfig"); + const auto addedAdmin = m_coreController->m_serversRepository->selfHostedAdminConfig( + m_coreController->m_serversRepository->serverIdAt(serverIndex)); + QVERIFY2(addedAdmin.has_value(), "Added server should be self-hosted admin"); + const SelfHostedAdminServerConfig *selfHosted = &(*addedAdmin); QVERIFY2(selfHosted->containers.isEmpty(), "Server should have no containers initially"); QVERIFY2(selfHosted->defaultContainer == DockerContainer::None, "Default container should be None"); - ErrorCode scanError = m_coreController->m_installController->scanServerForInstalledContainers(serverIndex); + const QString scanServerId = m_coreController->m_serversRepository->serverIdAt(serverIndex); + ErrorCode scanError = m_coreController->m_installController->scanServerForInstalledContainers(scanServerId); QVERIFY2(scanError == ErrorCode::NoError, QString("Server scan should succeed. Error: %1") .arg(static_cast(scanError)) .toUtf8().constData()); qDebug() << "Server scan completed successfully"; - ServerConfig scannedServer = m_coreController->m_serversRepository->server(serverIndex); - const SelfHostedServerConfig* scannedSelfHosted = scannedServer.as(); - QVERIFY2(scannedSelfHosted != nullptr, "Scanned server config should be SelfHostedServerConfig"); + const auto scannedAdmin = m_coreController->m_serversRepository->selfHostedAdminConfig( + m_coreController->m_serversRepository->serverIdAt(serverIndex)); + QVERIFY2(scannedAdmin.has_value(), "Scanned server config should be SelfHostedAdminServerConfig"); + const SelfHostedAdminServerConfig *scannedSelfHosted = &(*scannedAdmin); QMap containers = scannedSelfHosted->containers; int containersCount = containers.size(); @@ -336,24 +345,27 @@ private slots: int serverIndex = m_coreController->m_serversRepository->serversCount() - 1; qDebug() << "Server with Awg container added at index:" << serverIndex; - ServerConfig serverBeforeRemoval = m_coreController->m_serversRepository->server(serverIndex); - const SelfHostedServerConfig* selfHostedBeforeRemoval = serverBeforeRemoval.as(); - QVERIFY2(selfHostedBeforeRemoval != nullptr, "Server config should be SelfHostedServerConfig"); + const auto adminBeforeRemoval = m_coreController->m_serversRepository->selfHostedAdminConfig( + m_coreController->m_serversRepository->serverIdAt(serverIndex)); + QVERIFY2(adminBeforeRemoval.has_value(), "Server config should be SelfHostedAdminServerConfig"); + const SelfHostedAdminServerConfig *selfHostedBeforeRemoval = &(*adminBeforeRemoval); QVERIFY2(!selfHostedBeforeRemoval->containers.isEmpty(), "Server should have containers before removal"); QVERIFY2(selfHostedBeforeRemoval->defaultContainer != DockerContainer::None, "Server should have default container before removal"); qDebug() << "Containers before removal:" << selfHostedBeforeRemoval->containers.size(); - ErrorCode removeError = m_coreController->m_installController->removeAllContainers(serverIndex); + const QString removeServerId = m_coreController->m_serversRepository->serverIdAt(serverIndex); + ErrorCode removeError = m_coreController->m_installController->removeAllContainers(removeServerId); QVERIFY2(removeError == ErrorCode::NoError, QString("removeAllContainers should succeed. Error: %1") .arg(static_cast(removeError)) .toUtf8().constData()); qDebug() << "All containers removed successfully"; - ServerConfig serverAfterRemoval = m_coreController->m_serversRepository->server(serverIndex); - const SelfHostedServerConfig* selfHostedAfterRemoval = serverAfterRemoval.as(); - QVERIFY2(selfHostedAfterRemoval != nullptr, "Server config should be SelfHostedServerConfig"); + const auto adminAfterRemoval = m_coreController->m_serversRepository->selfHostedAdminConfig( + m_coreController->m_serversRepository->serverIdAt(serverIndex)); + QVERIFY2(adminAfterRemoval.has_value(), "Server config should be SelfHostedAdminServerConfig"); + const SelfHostedAdminServerConfig *selfHostedAfterRemoval = &(*adminAfterRemoval); QVERIFY2(selfHostedAfterRemoval->containers.isEmpty(), "Server should have no containers after removal"); diff --git a/client/tests/testServerEdgeCases.cpp b/client/tests/testServerEdgeCases.cpp index 53c358d20..91dba71fd 100644 --- a/client/tests/testServerEdgeCases.cpp +++ b/client/tests/testServerEdgeCases.cpp @@ -1,13 +1,15 @@ #include -#include #include #include #include #include "core/controllers/coreController.h" -#include "core/models/serverConfig.h" +#include "core/repositories/secureServersRepository.h" +#include "core/models/serverDescription.h" +#include "core/models/selfhosted/selfHostedAdminServerConfig.h" #include "vpnConnection.h" #include "secureQSettings.h" +#include "core/utils/serverConfigUtils.h" using namespace amnezia; @@ -38,7 +40,7 @@ private slots: m_settings->clearSettings(); m_coreController->m_serversRepository->invalidateCache(); if (m_coreController->m_serversModel) { - m_coreController->m_serversModel->updateModel(QVector(), -1, false); + m_coreController->m_serversModel->updateModel(QVector(), -1); } } @@ -54,27 +56,32 @@ private slots: QSignalSpy serverEditedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverEdited); QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged); - m_coreController->m_serversController->removeServer(-1); + m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(-1)); QVERIFY2(serverRemovedSpy.count() == 0, "serverRemoved should NOT be emitted for invalid index"); - m_coreController->m_serversController->removeServer(10); + m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(10)); QVERIFY2(serverRemovedSpy.count() == 0, "serverRemoved should NOT be emitted for invalid index"); - m_coreController->m_serversController->removeServer(100); + m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(100)); QVERIFY2(serverRemovedSpy.count() == 0, "serverRemoved should NOT be emitted for invalid index"); QVERIFY2(m_coreController->m_serversRepository->serversCount() == 1, "Server count should remain 1"); - ServerConfig serverConfig = m_coreController->m_serversController->getServerConfig(0); - m_coreController->m_serversController->editServer(-1, serverConfig); + const QString validServerId = m_coreController->m_serversController->getServerId(0); + const serverConfigUtils::ConfigType editKind = + m_coreController->m_serversRepository->serverKind(validServerId); + + m_coreController->m_serversRepository->editServer(m_coreController->m_serversController->getServerId(-1), + QJsonObject(), editKind); QVERIFY2(serverEditedSpy.count() == 0, "serverEdited should NOT be emitted for invalid index"); - m_coreController->m_serversController->editServer(10, serverConfig); + m_coreController->m_serversRepository->editServer(m_coreController->m_serversController->getServerId(10), + QJsonObject(), editKind); QVERIFY2(serverEditedSpy.count() == 0, "serverEdited should NOT be emitted for invalid index"); - m_coreController->m_serversController->setDefaultServerIndex(-1); + m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(-1)); QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged should NOT be emitted for invalid index"); - m_coreController->m_serversController->setDefaultServerIndex(10); + m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(10)); QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged should NOT be emitted for invalid index"); QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "Default server index should remain 0"); } @@ -86,14 +93,15 @@ private slots: QVERIFY2(m_coreController->m_serversRepository->serversCount() == 0, "Should start with 0 servers"); - ServerConfig emptyConfig = SelfHostedServerConfig{}; - m_coreController->m_serversController->removeServer(0); + m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(0)); QVERIFY2(serverRemovedSpy.count() == 0, "serverRemoved should NOT be emitted for empty repository"); - m_coreController->m_serversController->editServer(0, emptyConfig); + m_coreController->m_serversRepository->editServer(m_coreController->m_serversController->getServerId(0), + SelfHostedAdminServerConfig {}.toJson(), + serverConfigUtils::ConfigType::SelfHostedAdmin); QVERIFY2(serverEditedSpy.count() == 0, "serverEdited should NOT be emitted for empty repository"); - m_coreController->m_serversController->setDefaultServerIndex(0); + m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(0)); QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged should NOT be emitted for empty repository"); QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "Default server index should be 0 for empty repository"); diff --git a/client/tests/testServerEdit.cpp b/client/tests/testServerEdit.cpp index 57f18ef12..4e101eb6e 100644 --- a/client/tests/testServerEdit.cpp +++ b/client/tests/testServerEdit.cpp @@ -5,7 +5,8 @@ #include #include "core/controllers/coreController.h" -#include "core/models/serverConfig.h" +#include "core/models/serverDescription.h" +#include "tests/testServerRepositoryHelpers.h" #include "ui/models/serversModel.h" #include "vpnConnection.h" #include "secureQSettings.h" @@ -39,7 +40,7 @@ private slots: m_settings->clearSettings(); m_coreController->m_serversRepository->invalidateCache(); if (m_coreController->m_serversModel) { - m_coreController->m_serversModel->updateModel(QVector(), -1, false); + m_coreController->m_serversModel->updateModel(QVector(), -1); } } @@ -52,20 +53,17 @@ private slots: QVERIFY2(importFinishedSpy.count() == 1, "Import should succeed"); QSignalSpy serverEditedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverEdited); - QSignalSpy gatewayStacksExpandedSpy(m_coreController->m_serversController, &ServersController::gatewayStacksExpanded); - ServerConfig serverConfig = m_coreController->m_serversController->getServerConfig(0); - serverConfig.visit([](auto& arg) { - arg.description = "Edited AWG Server"; - }); - - m_coreController->m_serversController->editServer(0, serverConfig); + amnezia::test::setServerDescription(m_coreController->m_serversRepository, + m_coreController->m_serversController->getServerId(0), + QStringLiteral("Edited AWG Server")); QVERIFY2(serverEditedSpy.count() == 1, "serverEdited signal should be emitted"); - QVERIFY2(serverEditedSpy.at(0).at(0).toInt() == 0, "serverEdited should emit index 0"); + QVERIFY2(serverEditedSpy.at(0).at(0).toString() == m_coreController->m_serversRepository->serverIdAt(0), + "serverEdited should emit edited server id"); - ServerConfig editedServer = m_coreController->m_serversRepository->server(0); - QString editedDesc = editedServer.description(); + const QString editedDesc = amnezia::test::serverDescription(m_coreController->m_serversRepository, + m_coreController->m_serversRepository->serverIdAt(0)); QVERIFY2(editedDesc == "Edited AWG Server", "Server description should be updated"); if (m_coreController->m_serversModel) { @@ -87,20 +85,16 @@ private slots: QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged); - ServerConfig defaultServerConfig = m_coreController->m_serversController->getServerConfig(1); - defaultServerConfig.visit([](auto& arg) { - arg.description = "Edited Default Server"; - }); - m_coreController->m_serversController->editServer(1, defaultServerConfig); + amnezia::test::setServerDescription(m_coreController->m_serversRepository, + m_coreController->m_serversController->getServerId(1), + QStringLiteral("Edited Default Server")); QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged should NOT be emitted when editing default server"); QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Default server index should remain 1"); - ServerConfig nonDefaultServerConfig = m_coreController->m_serversController->getServerConfig(0); - nonDefaultServerConfig.visit([](auto& arg) { - arg.description = "Edited Non-Default Server"; - }); - m_coreController->m_serversController->editServer(0, nonDefaultServerConfig); + amnezia::test::setServerDescription(m_coreController->m_serversRepository, + m_coreController->m_serversController->getServerId(0), + QStringLiteral("Edited Non-Default Server")); QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged should NOT be emitted when editing non-default server"); QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Default server index should remain 1"); diff --git a/client/tests/testServerRepositoryHelpers.h b/client/tests/testServerRepositoryHelpers.h new file mode 100644 index 000000000..13c63b47c --- /dev/null +++ b/client/tests/testServerRepositoryHelpers.h @@ -0,0 +1,93 @@ +#ifndef TESTSERVERREPOSITORYHELPERS_H +#define TESTSERVERREPOSITORYHELPERS_H + +#include +#include + +#include "core/repositories/secureServersRepository.h" +#include "core/utils/serverConfigUtils.h" + +namespace amnezia::test +{ + +inline QString serverDescription(SecureServersRepository *repo, const QString &serverId) +{ + switch (repo->serverKind(serverId)) { + case serverConfigUtils::ConfigType::SelfHostedAdmin: { + const auto cfg = repo->selfHostedAdminConfig(serverId); + return cfg.has_value() ? cfg->description : QString(); + } + case serverConfigUtils::ConfigType::SelfHostedUser: { + const auto cfg = repo->selfHostedUserConfig(serverId); + return cfg.has_value() ? cfg->description : QString(); + } + case serverConfigUtils::ConfigType::Native: { + const auto cfg = repo->nativeConfig(serverId); + return cfg.has_value() ? cfg->description : QString(); + } + case serverConfigUtils::ConfigType::AmneziaPremiumV2: + case serverConfigUtils::ConfigType::AmneziaFreeV3: + case serverConfigUtils::ConfigType::ExternalPremium: { + const auto cfg = repo->apiV2Config(serverId); + return cfg.has_value() ? cfg->description : QString(); + } + case serverConfigUtils::ConfigType::AmneziaPremiumV1: + case serverConfigUtils::ConfigType::AmneziaFreeV2: { + const auto cfg = repo->legacyApiConfig(serverId); + return cfg.has_value() ? cfg->description : QString(); + } + case serverConfigUtils::ConfigType::Invalid: + default: + return {}; + } +} + +inline void setServerDescription(SecureServersRepository *repo, const QString &serverId, const QString &description) +{ + const serverConfigUtils::ConfigType kind = repo->serverKind(serverId); + switch (kind) { + case serverConfigUtils::ConfigType::SelfHostedAdmin: { + auto cfg = repo->selfHostedAdminConfig(serverId); + if (!cfg.has_value()) return; + cfg->description = description; + cfg->displayName = description; + repo->editServer(serverId, cfg->toJson(), kind); + return; + } + case serverConfigUtils::ConfigType::SelfHostedUser: { + auto cfg = repo->selfHostedUserConfig(serverId); + if (!cfg.has_value()) return; + cfg->description = description; + cfg->displayName = description; + repo->editServer(serverId, cfg->toJson(), kind); + return; + } + case serverConfigUtils::ConfigType::Native: { + auto cfg = repo->nativeConfig(serverId); + if (!cfg.has_value()) return; + cfg->description = description; + cfg->displayName = description; + repo->editServer(serverId, cfg->toJson(), kind); + return; + } + case serverConfigUtils::ConfigType::AmneziaPremiumV2: + case serverConfigUtils::ConfigType::AmneziaFreeV3: + case serverConfigUtils::ConfigType::ExternalPremium: { + auto cfg = repo->apiV2Config(serverId); + if (!cfg.has_value()) return; + cfg->description = description; + cfg->displayName = description; + repo->editServer(serverId, cfg->toJson(), kind); + return; + } + case serverConfigUtils::ConfigType::AmneziaPremiumV1: + case serverConfigUtils::ConfigType::AmneziaFreeV2: + case serverConfigUtils::ConfigType::Invalid: + default: + return; + } +} + +} // namespace amnezia::test + +#endif diff --git a/client/tests/testServersModelSync.cpp b/client/tests/testServersModelSync.cpp index a1e63ccce..12b1288fe 100644 --- a/client/tests/testServersModelSync.cpp +++ b/client/tests/testServersModelSync.cpp @@ -5,7 +5,8 @@ #include #include "core/controllers/coreController.h" -#include "core/models/serverConfig.h" +#include "core/models/serverDescription.h" +#include "tests/testServerRepositoryHelpers.h" #include "ui/models/serversModel.h" #include "vpnConnection.h" #include "secureQSettings.h" @@ -38,7 +39,7 @@ private slots: void init() { m_settings->clearSettings(); if (m_coreController->m_serversModel) { - m_coreController->m_serversModel->updateModel(QVector(), -1, false); + m_coreController->m_serversModel->updateModel(QVector(), -1); } } @@ -58,16 +59,14 @@ private slots: QString modelDesc1 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::NameRole).toString(); QVERIFY2(modelDesc1 == "AWG Server", "Model should have correct server name"); - ServerConfig serverConfig = m_coreController->m_serversController->getServerConfig(0); - serverConfig.visit([](auto& arg) { - arg.description = "Edited AWG Server"; - }); - m_coreController->m_serversController->editServer(0, serverConfig); + amnezia::test::setServerDescription(m_coreController->m_serversRepository, + m_coreController->m_serversController->getServerId(0), + QStringLiteral("Edited AWG Server")); QString modelDesc2 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::NameRole).toString(); QVERIFY2(modelDesc2 == "Edited AWG Server", "Model should be updated after edit"); - m_coreController->m_serversController->removeServer(0); + m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(0)); QVERIFY2(m_coreController->m_serversModel->rowCount() == 0, "Model should have 0 rows after removal"); } @@ -98,7 +97,7 @@ private slots: QVERIFY2(!isDefault1, "Server 1 should not be default"); QVERIFY2(isDefault2, "Server 2 should be default"); - m_coreController->m_serversController->setDefaultServerIndex(0); + m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(0)); isDefault0 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::IsDefaultRole).toBool(); isDefault2 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(2, 0), ServersModel::IsDefaultRole).toBool(); diff --git a/client/tests/testSettingsSignals.cpp b/client/tests/testSettingsSignals.cpp index e0308a56c..22c5c0677 100644 --- a/client/tests/testSettingsSignals.cpp +++ b/client/tests/testSettingsSignals.cpp @@ -6,7 +6,6 @@ #include #include "core/controllers/coreController.h" -#include "core/models/serverConfig.h" #include "ui/controllers/settingsUiController.h" #include "ui/controllers/languageUiController.h" #include "ui/models/allowedDnsModel.h" diff --git a/client/tests/testSignalOrder.cpp b/client/tests/testSignalOrder.cpp index 97b7cb29e..8c6cf0d65 100644 --- a/client/tests/testSignalOrder.cpp +++ b/client/tests/testSignalOrder.cpp @@ -5,7 +5,7 @@ #include #include "core/controllers/coreController.h" -#include "core/models/serverConfig.h" +#include "core/models/serverDescription.h" #include "vpnConnection.h" #include "secureQSettings.h" @@ -38,7 +38,7 @@ private slots: m_settings->clearSettings(); m_coreController->m_serversRepository->invalidateCache(); if (m_coreController->m_serversModel) { - m_coreController->m_serversModel->updateModel(QVector(), -1, false); + m_coreController->m_serversModel->updateModel(QVector(), -1); } } @@ -73,11 +73,12 @@ private slots: QSignalSpy serverRemovedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverRemoved); QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged); - m_coreController->m_serversController->removeServer(1); + m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(1)); QVERIFY2(serverRemovedSpy.count() == 1, "serverRemoved signal should be emitted"); QVERIFY2(defaultServerChangedSpy.count() == 1, "defaultServerChanged signal should be emitted when removing default server"); - QVERIFY2(defaultServerChangedSpy.at(0).at(0).toInt() == 0, "defaultServerChanged should emit new default index 0"); + QVERIFY2(defaultServerChangedSpy.at(0).at(0).toString() == m_coreController->m_serversRepository->defaultServerId(), + "defaultServerChanged should emit new default server id"); QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "Default server index should be 0"); } }; diff --git a/client/tests/testUiServersModelAndController.cpp b/client/tests/testUiServersModelAndController.cpp index 16039e988..ca67ab612 100644 --- a/client/tests/testUiServersModelAndController.cpp +++ b/client/tests/testUiServersModelAndController.cpp @@ -8,7 +8,7 @@ #include #include "core/controllers/coreController.h" -#include "core/models/serverConfig.h" +#include "core/models/serverDescription.h" #include "core/controllers/selfhosted/importController.h" #include "ui/models/serversModel.h" #include "ui/models/containersModel.h" @@ -23,6 +23,18 @@ using namespace amnezia; using namespace amnezia; +namespace { +int defaultServerRow(const QVector &descriptions, const QString &defaultServerId) +{ + for (int i = 0; i < descriptions.size(); ++i) { + if (descriptions.at(i).serverId == defaultServerId) { + return i; + } + } + return -1; +} +} // namespace + class TestUiServersModelAndController : public QObject { Q_OBJECT @@ -119,7 +131,7 @@ private slots: void init() { m_settings->clearSettings(); if (m_coreController->m_serversModel) { - m_coreController->m_serversModel->updateModel(QVector(), -1, false); + m_coreController->m_serversModel->updateModel(QVector(), -1); } } @@ -166,11 +178,9 @@ private slots: } if (m_coreController->m_serversUiController) { - m_coreController->m_serversUiController->setProcessedServerIndex(serverIndex); - - ServerConfig serverConfig = m_coreController->m_serversRepository->server(serverIndex); - QString actualServerName = serverConfig.description(); - QString containerName = ContainerUtils::containerHumanNames().value(DockerContainer::Awg); + m_coreController->m_serversUiController->setProcessedServerId( + m_coreController->m_serversUiController->getServerId(0)); + QString hostName = "test.example.com"; QString collapsedDescription = m_coreController->m_serversUiController->getDefaultServerDescriptionCollapsed(); @@ -261,27 +271,29 @@ private slots: m_coreController->m_importCoreController->importConfig(configNoDns); QVERIFY2(importFinishedSpy.count() == 1, "importFinished should be emitted"); m_coreController->m_appSettingsRepository->setUseAmneziaDns(false); - m_coreController->m_serversModel->updateModel( - m_coreController->m_serversRepository->servers(), - m_coreController->m_serversRepository->defaultServerIndex(), + QVector descriptionsNoDns = m_coreController->m_serversController->buildServerDescriptions( m_coreController->m_appSettingsRepository->useAmneziaDns()); + const QString defIdNoDns = m_coreController->m_serversRepository->defaultServerId(); + m_coreController->m_serversModel->updateModel(descriptionsNoDns, defaultServerRow(descriptionsNoDns, defIdNoDns)); QString descNoDns = m_coreController->m_serversModel->data( m_coreController->m_serversModel->index(0, 0), ServersModel::ServerDescriptionRole).toString(); QVERIFY2(descNoDns == "test.example.com", QString("Without Amnezia DNS expected 'test.example.com', got '%1'").arg(descNoDns).toUtf8().constData()); - m_coreController->m_serversRepository->setServersArray(QJsonArray()); - m_coreController->m_serversRepository->setDefaultServer(0); + m_coreController->m_serversRepository->clearServers(); + if (m_coreController->m_serversRepository->serversCount() > 0) { + m_coreController->m_serversRepository->setDefaultServer(m_coreController->m_serversRepository->serverIdAt(0)); + } QJsonObject configWithDns = createServerDescriptionTestConfig(true); m_coreController->m_importCoreController->importConfig(configWithDns); QVERIFY2(m_coreController->m_serversRepository->serversCount() == 1, "Server should be imported"); m_coreController->m_appSettingsRepository->setUseAmneziaDns(true); - m_coreController->m_serversModel->updateModel( - m_coreController->m_serversRepository->servers(), - m_coreController->m_serversRepository->defaultServerIndex(), + QVector descriptionsWithDns = m_coreController->m_serversController->buildServerDescriptions( m_coreController->m_appSettingsRepository->useAmneziaDns()); + const QString defIdWithDns = m_coreController->m_serversRepository->defaultServerId(); + m_coreController->m_serversModel->updateModel(descriptionsWithDns, defaultServerRow(descriptionsWithDns, defIdWithDns)); QString descWithDns = m_coreController->m_serversModel->data( m_coreController->m_serversModel->index(0, 0), ServersModel::ServerDescriptionRole).toString(); diff --git a/client/ui/controllers/api/subscriptionUiController.cpp b/client/ui/controllers/api/subscriptionUiController.cpp index 75d96553e..5dee205dc 100644 --- a/client/ui/controllers/api/subscriptionUiController.cpp +++ b/client/ui/controllers/api/subscriptionUiController.cpp @@ -2,14 +2,13 @@ #include "amneziaApplication.h" #include "core/configurators/wireguardConfigurator.h" -#include "core/utils/api/apiEnums.h" +#include "core/utils/serverConfigUtils.h" #include "core/utils/constants/apiKeys.h" #include "core/utils/constants/apiConstants.h" #include "core/utils/api/apiUtils.h" #include "core/utils/qrCodeUtils.h" #include "ui/controllers/systemController.h" #include "version.h" -#include "core/models/serverConfig.h" #include #include #include @@ -67,7 +66,17 @@ SubscriptionUiController::SubscriptionUiController(ServersController* serversCon ApiDevicesModel* apiDevicesModel, SettingsController* settingsController, QObject *parent) - : QObject(parent), m_serversController(serversController), m_apiServicesModel(apiServicesModel), m_servicesCatalogController(servicesCatalogController), m_subscriptionController(subscriptionController), m_apiSubscriptionPlansModel(apiSubscriptionPlansModel), m_apiBenefitsModel(apiBenefitsModel), m_apiAccountInfoModel(apiAccountInfoModel), m_apiCountryModel(apiCountryModel), m_apiDevicesModel(apiDevicesModel), m_settingsController(settingsController) + : QObject(parent), + m_serversController(serversController), + m_apiServicesModel(apiServicesModel), + m_servicesCatalogController(servicesCatalogController), + m_subscriptionController(subscriptionController), + m_apiSubscriptionPlansModel(apiSubscriptionPlansModel), + m_apiBenefitsModel(apiBenefitsModel), + m_apiAccountInfoModel(apiAccountInfoModel), + m_apiCountryModel(apiCountryModel), + m_apiDevicesModel(apiDevicesModel), + m_settingsController(settingsController) { connect(m_apiServicesModel, &ApiServicesModel::serviceSelectionChanged, this, [this]() { ApiServicesModel::ApiServicesData selectedServiceData = m_apiServicesModel->selectedServiceData(); @@ -76,14 +85,14 @@ SubscriptionUiController::SubscriptionUiController(ServersController* serversCon }); } -bool SubscriptionUiController::exportVpnKey(int serverIndex, const QString &fileName) +bool SubscriptionUiController::exportVpnKey(const QString &serverId, const QString &fileName) { if (fileName.isEmpty()) { emit errorOccurred(ErrorCode::PermissionsError); return false; } - prepareVpnKeyExport(serverIndex); + prepareVpnKeyExport(serverId); if (m_vpnKey.isEmpty()) { emit errorOccurred(ErrorCode::ApiConfigEmptyError); return false; @@ -93,7 +102,8 @@ bool SubscriptionUiController::exportVpnKey(int serverIndex, const QString &file return true; } -bool SubscriptionUiController::exportNativeConfig(int serverIndex, const QString &serverCountryCode, const QString &fileName) + +bool SubscriptionUiController::exportNativeConfig(const QString &serverId, const QString &serverCountryCode, const QString &fileName) { if (fileName.isEmpty()) { emit errorOccurred(ErrorCode::PermissionsError); @@ -101,7 +111,7 @@ bool SubscriptionUiController::exportNativeConfig(int serverIndex, const QString } QString nativeConfig; - ErrorCode errorCode = m_subscriptionController->exportNativeConfig(serverIndex, serverCountryCode, nativeConfig); + ErrorCode errorCode = m_subscriptionController->exportNativeConfig(serverId, serverCountryCode, nativeConfig); if (errorCode != ErrorCode::NoError) { emit errorOccurred(errorCode); return false; @@ -111,9 +121,10 @@ bool SubscriptionUiController::exportNativeConfig(int serverIndex, const QString return true; } -bool SubscriptionUiController::revokeNativeConfig(int serverIndex, const QString &serverCountryCode) + +bool SubscriptionUiController::revokeNativeConfig(const QString &serverId, const QString &serverCountryCode) { - ErrorCode errorCode = m_subscriptionController->revokeNativeConfig(serverIndex, serverCountryCode); + ErrorCode errorCode = m_subscriptionController->revokeNativeConfig(serverId, serverCountryCode); if (errorCode != ErrorCode::NoError) { emit errorOccurred(errorCode); return false; @@ -121,10 +132,11 @@ bool SubscriptionUiController::revokeNativeConfig(int serverIndex, const QString return true; } -void SubscriptionUiController::prepareVpnKeyExport(int serverIndex) + +void SubscriptionUiController::prepareVpnKeyExport(const QString &serverId) { QString vpnKey; - ErrorCode errorCode = m_subscriptionController->prepareVpnKeyExport(serverIndex, vpnKey); + ErrorCode errorCode = m_subscriptionController->prepareVpnKeyExport(serverId, vpnKey); if (errorCode != ErrorCode::NoError) { emit errorOccurred(errorCode); return; @@ -140,6 +152,7 @@ void SubscriptionUiController::prepareVpnKeyExport(int serverIndex) emit vpnKeyExportReady(); } + void SubscriptionUiController::copyVpnKeyToClipboard() { auto clipboard = amnApp->getClipboard(); @@ -170,14 +183,12 @@ bool SubscriptionUiController::importPremiumFromAppStore(const QString &storePro productId = QStringLiteral("amnezia_premium_6_month"); } - ServerConfig serverConfig; int duplicateServerIndex = -1; ErrorCode errorCode = m_subscriptionController->processAppStorePurchase( m_apiServicesModel->getCountryCode(), m_apiServicesModel->getSelectedServiceType(), m_apiServicesModel->getSelectedServiceProtocol(), productId, - serverConfig, &duplicateServerIndex); if (errorCode != ErrorCode::NoError) { @@ -260,11 +271,8 @@ bool SubscriptionUiController::importFreeFromGateway() } SubscriptionController::ProtocolData protocolData = m_subscriptionController->generateProtocolData(serviceProtocol); - - ServerConfig serverConfig; ErrorCode errorCode = m_subscriptionController->importServiceFromGateway(userCountryCode, serviceType, - serviceProtocol, protocolData, - serverConfig); + serviceProtocol, protocolData); if (errorCode == ErrorCode::NoError) { emit installServerFromApiFinished(tr("%1 installed successfully.").arg(m_apiServicesModel->getSelectedServiceName())); @@ -278,12 +286,10 @@ bool SubscriptionUiController::importFreeFromGateway() bool SubscriptionUiController::importTrialFromGateway(const QString &email) { emit trialEmailError(QString()); - ServerConfig serverConfig; ErrorCode errorCode = m_subscriptionController->importTrialFromGateway(m_apiServicesModel->getCountryCode(), m_apiServicesModel->getSelectedServiceType(), m_apiServicesModel->getSelectedServiceProtocol(), - email, - serverConfig); + email); if (errorCode != ErrorCode::NoError) { if (errorCode == ErrorCode::ApiTrialAlreadyUsedError) { emit trialEmailError( @@ -298,21 +304,17 @@ bool SubscriptionUiController::importTrialFromGateway(const QString &email) return true; } -bool SubscriptionUiController::updateServiceFromGateway(const int serverIndex, const QString &newCountryCode, const QString &newCountryName, +bool SubscriptionUiController::updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, const QString &newCountryName, bool reloadServiceConfig) { bool isConnectEvent = newCountryCode.isEmpty() && newCountryName.isEmpty() && !reloadServiceConfig; bool wasSubscriptionExpired = false; - ServerConfig oldServerConfig = m_serversController->getServerConfig(serverIndex); - if (oldServerConfig.isApiV2()) { - const ApiV2ServerConfig *oldApiV2 = oldServerConfig.as(); - if (oldApiV2) { - wasSubscriptionExpired = oldApiV2->apiConfig.subscriptionExpiredByServer - || oldApiV2->apiConfig.isSubscriptionExpired(); - } + if (const auto oldApiV2 = m_serversController->apiV2Config(serverId)) { + wasSubscriptionExpired = oldApiV2->apiConfig.subscriptionExpiredByServer + || oldApiV2->apiConfig.isSubscriptionExpired(); } - ErrorCode errorCode = m_subscriptionController->updateServiceFromGateway(serverIndex, newCountryCode, isConnectEvent); + ErrorCode errorCode = m_subscriptionController->updateServiceFromGateway(serverId, newCountryCode, isConnectEvent); if (errorCode == ErrorCode::NoError) { if (wasSubscriptionExpired) { @@ -336,27 +338,10 @@ bool SubscriptionUiController::updateServiceFromGateway(const int serverIndex, c } } -bool SubscriptionUiController::updateServiceFromTelegram(const int serverIndex) + +bool SubscriptionUiController::deactivateDevice(const QString &serverId) { -#ifdef Q_OS_IOS - IosController::Instance()->requestInetAccess(); - QThread::msleep(10); -#endif - - ErrorCode errorCode = m_subscriptionController->updateServiceFromTelegram(serverIndex); - - if (errorCode == ErrorCode::NoError) { - emit updateServerFromApiFinished(); - return true; - } else { - emit errorOccurred(errorCode); - return false; - } -} - -bool SubscriptionUiController::deactivateDevice(int serverIndex) -{ - ErrorCode errorCode = m_subscriptionController->deactivateDevice(serverIndex); + ErrorCode errorCode = m_subscriptionController->deactivateDevice(serverId); if (errorCode != ErrorCode::NoError) { emit errorOccurred(errorCode); return false; @@ -365,9 +350,10 @@ bool SubscriptionUiController::deactivateDevice(int serverIndex) return true; } -bool SubscriptionUiController::deactivateExternalDevice(int serverIndex, const QString &uuid, const QString &serverCountryCode) + +bool SubscriptionUiController::deactivateExternalDevice(const QString &serverId, const QString &uuid, const QString &serverCountryCode) { - ErrorCode errorCode = m_subscriptionController->deactivateExternalDevice(serverIndex, uuid, serverCountryCode); + ErrorCode errorCode = m_subscriptionController->deactivateExternalDevice(serverId, uuid, serverCountryCode); if (errorCode != ErrorCode::NoError) { emit errorOccurred(errorCode); return false; @@ -376,12 +362,19 @@ bool SubscriptionUiController::deactivateExternalDevice(int serverIndex, const Q return true; } + void SubscriptionUiController::validateConfig() { - int serverIndex = m_serversController->getDefaultServerIndex(); - bool hasInstalledContainers = m_serversController->hasInstalledContainers(serverIndex); + const QString serverId = m_serversController->getDefaultServerId(); + if (!serverId.isEmpty() && m_serversController->isLegacyApiV1Server(serverId)) { + emit unsupportedConnectDrawerRequested(); + emit configValidated(false); + return; + } - ErrorCode errorCode = m_subscriptionController->validateAndUpdateConfig(serverIndex, hasInstalledContainers); + bool hasInstalledContainers = m_serversController->hasInstalledContainers(serverId); + + ErrorCode errorCode = m_subscriptionController->validateAndUpdateConfig(serverId, hasInstalledContainers); if (errorCode != ErrorCode::NoError) { if (errorCode == ErrorCode::ApiSubscriptionExpiredError) { @@ -395,22 +388,25 @@ void SubscriptionUiController::validateConfig() emit configValidated(true); } -void SubscriptionUiController::setCurrentProtocol(int serverIndex, const QString &protocolName) +void SubscriptionUiController::setCurrentProtocol(const QString &serverId, const QString &protocolName) { - m_subscriptionController->setCurrentProtocol(serverIndex, protocolName); + m_subscriptionController->setCurrentProtocol(serverId, protocolName); } -bool SubscriptionUiController::isVlessProtocol(int serverIndex) + +bool SubscriptionUiController::isVlessProtocol(const QString &serverId) { - return m_subscriptionController->isVlessProtocol(serverIndex); + return m_subscriptionController->isVlessProtocol(serverId); } -void SubscriptionUiController::removeApiConfig(int serverIndex) + +void SubscriptionUiController::removeApiConfig(const QString &serverId) { - m_subscriptionController->removeApiConfig(serverIndex); + m_subscriptionController->removeApiConfig(serverId); emit apiConfigRemoved(tr("Api config removed")); } + QList SubscriptionUiController::getQrCodes() { return m_qrCodes; @@ -426,7 +422,7 @@ QString SubscriptionUiController::getVpnKey() return m_vpnKey; } -bool SubscriptionUiController::getAccountInfo(int serverIndex, bool reload) +bool SubscriptionUiController::getAccountInfo(const QString &serverId, bool reload) { if (reload) { QEventLoop wait; @@ -434,15 +430,18 @@ bool SubscriptionUiController::getAccountInfo(int serverIndex, bool reload) wait.exec(QEventLoop::ExcludeUserInputEvents); } QJsonObject accountInfo; - ErrorCode errorCode = m_subscriptionController->getAccountInfo(serverIndex, accountInfo); + ErrorCode errorCode = m_subscriptionController->getAccountInfo(serverId, accountInfo); if (errorCode != ErrorCode::NoError) { emit errorOccurred(errorCode); return false; } - ServerConfig serverConfig = m_serversController->getServerConfig(serverIndex); - QJsonObject serverConfigJson = serverConfig.toJson(); - m_apiAccountInfoModel->updateModel(accountInfo, serverConfigJson); + const auto apiV2 = m_serversController->apiV2Config(serverId); + if (!apiV2.has_value()) { + emit errorOccurred(ErrorCode::InternalError); + return false; + } + m_apiAccountInfoModel->updateModel(accountInfo, apiV2->toJson()); if (reload) { updateApiCountryModel(); @@ -463,9 +462,9 @@ void SubscriptionUiController::updateApiDevicesModel() m_apiDevicesModel->updateModel(m_apiAccountInfoModel->getIssuedConfigsInfo(), m_settingsController->getInstallationUuid(false)); } -void SubscriptionUiController::getRenewalLink(int serverIndex) +void SubscriptionUiController::getRenewalLink(const QString &serverId) { - if (serverIndex < 0) { + if (serverId.isEmpty()) { emit errorOccurred(ErrorCode::InternalError); return; } @@ -483,6 +482,6 @@ void SubscriptionUiController::getRenewalLink(int serverIndex) } emit renewalLinkReceived(url); }); - watcher->setFuture(m_subscriptionController->getRenewalLink(serverIndex)); + watcher->setFuture(m_subscriptionController->getRenewalLink(serverId)); } diff --git a/client/ui/controllers/api/subscriptionUiController.h b/client/ui/controllers/api/subscriptionUiController.h index d64a2eb2d..e4be939a3 100644 --- a/client/ui/controllers/api/subscriptionUiController.h +++ b/client/ui/controllers/api/subscriptionUiController.h @@ -13,6 +13,7 @@ #include "ui/models/api/apiAccountInfoModel.h" #include "ui/models/api/apiCountryModel.h" #include "ui/models/api/apiDevicesModel.h" + class SubscriptionUiController : public QObject { Q_OBJECT @@ -34,10 +35,10 @@ public: Q_PROPERTY(QString vpnKey READ getVpnKey NOTIFY vpnKeyExportReady) public slots: - bool exportNativeConfig(int serverIndex, const QString &serverCountryCode, const QString &fileName); - bool revokeNativeConfig(int serverIndex, const QString &serverCountryCode); - bool exportVpnKey(int serverIndex, const QString &fileName); - void prepareVpnKeyExport(int serverIndex); + bool exportNativeConfig(const QString &serverId, const QString &serverCountryCode, const QString &fileName); + bool revokeNativeConfig(const QString &serverId, const QString &serverCountryCode); + bool exportVpnKey(const QString &serverId, const QString &fileName); + void prepareVpnKeyExport(const QString &serverId); void copyVpnKeyToClipboard(); bool fillAvailableServices(); @@ -45,21 +46,21 @@ public slots: bool importFreeFromGateway(); bool restoreServiceFromAppStore(); bool importTrialFromGateway(const QString &email); - bool updateServiceFromGateway(const int serverIndex, const QString &newCountryCode, const QString &newCountryName, + bool updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, const QString &newCountryName, bool reloadServiceConfig = false); - bool updateServiceFromTelegram(const int serverIndex); - bool deactivateDevice(int serverIndex); - bool deactivateExternalDevice(int serverIndex, const QString &uuid, const QString &serverCountryCode); + bool deactivateDevice(const QString &serverId); + bool deactivateExternalDevice(const QString &serverId, const QString &uuid, const QString &serverCountryCode); void validateConfig(); - void setCurrentProtocol(int serverIndex, const QString &protocolName); - bool isVlessProtocol(int serverIndex); + void setCurrentProtocol(const QString &serverId, const QString &protocolName); + bool isVlessProtocol(const QString &serverId); - void removeApiConfig(int serverIndex); + void removeApiConfig(const QString &serverId); + + bool getAccountInfo(const QString &serverId, bool reload); + void getRenewalLink(const QString &serverId); - bool getAccountInfo(int serverIndex, bool reload); - void getRenewalLink(int serverIndex); void updateApiCountryModel(); void updateApiDevicesModel(); @@ -80,6 +81,8 @@ signals: void vpnKeyExportReady(); + void unsupportedConnectDrawerRequested(); + private: QList getQrCodes(); int getQrCodesCount(); diff --git a/client/ui/controllers/connectionUiController.cpp b/client/ui/controllers/connectionUiController.cpp index a69c0bf3b..488d71804 100644 --- a/client/ui/controllers/connectionUiController.cpp +++ b/client/ui/controllers/connectionUiController.cpp @@ -25,9 +25,12 @@ ConnectionUiController::ConnectionUiController(ConnectionController* connectionC void ConnectionUiController::openConnection() { - int serverIndex = m_serversController->getDefaultServerIndex(); + const QString serverId = m_serversController->getDefaultServerId(); + if (serverId.isEmpty()) { + return; + } - ErrorCode errorCode = m_connectionController->openConnection(serverIndex); + ErrorCode errorCode = m_connectionController->openConnection(serverId); if (errorCode != ErrorCode::NoError) { emit connectionErrorOccurred(errorCode); @@ -100,16 +103,6 @@ void ConnectionUiController::onConnectionStateChanged(Vpn::ConnectionState state emit connectionStateChanged(); } -void ConnectionUiController::onCurrentContainerUpdated() -{ - if (m_isConnected || m_isConnectionInProgress) { - emit reconnectWithUpdatedContainer(tr("Settings updated successfully, reconnection...")); - openConnection(); - } else { - emit reconnectWithUpdatedContainer(tr("Settings updated successfully")); - } -} - void ConnectionUiController::onTranslationsUpdated() { onConnectionStateChanged(getCurrentConnectionState()); diff --git a/client/ui/controllers/connectionUiController.h b/client/ui/controllers/connectionUiController.h index e09f2977e..14f3927ca 100644 --- a/client/ui/controllers/connectionUiController.h +++ b/client/ui/controllers/connectionUiController.h @@ -38,8 +38,6 @@ public slots: ErrorCode getLastConnectionError(); void onConnectionStateChanged(Vpn::ConnectionState state); - void onCurrentContainerUpdated(); - void onTranslationsUpdated(); signals: diff --git a/client/ui/controllers/qml/pageController.cpp b/client/ui/controllers/qml/pageController.cpp index 67b9b64c1..b51ece70b 100644 --- a/client/ui/controllers/qml/pageController.cpp +++ b/client/ui/controllers/qml/pageController.cpp @@ -19,8 +19,7 @@ #include "ui/utils/macosUtil.h" #endif -PageController::PageController(ServersController* serversController, - SettingsController* settingsController, +PageController::PageController(ServersController* serversController, SettingsController* settingsController, QObject *parent) : QObject(parent), m_serversController(serversController), m_settingsController(settingsController) { @@ -57,14 +56,7 @@ PageController::PageController(ServersController* serversController, bool PageController::isStartPageVisible() { - if (m_serversController->getServersCount()) { - if (m_serversController->getDefaultServerIndex() < 0) { - m_serversController->setDefaultServerIndex(0); - } - return false; - } else { - return true; - } + return m_serversController->getServersCount() == 0; } QString PageController::getPagePath(PageLoader::PageEnum page) diff --git a/client/ui/controllers/qml/pageController.h b/client/ui/controllers/qml/pageController.h index 603e8a8f4..f98c1c130 100644 --- a/client/ui/controllers/qml/pageController.h +++ b/client/ui/controllers/qml/pageController.h @@ -94,8 +94,7 @@ class PageController : public QObject { Q_OBJECT public: - explicit PageController(ServersController* serversController, - SettingsController* settingsController, + explicit PageController(ServersController* serversController, SettingsController* settingsController, QObject *parent = nullptr); Q_PROPERTY(int safeAreaTopMargin READ getSafeAreaTopMargin NOTIFY safeAreaTopMarginChanged) @@ -164,6 +163,8 @@ signals: void showPassphraseRequestDrawer(); void passphraseRequestDrawerClosed(QString passphrase); + void unsupportedConnectDrawerRequested(); + void escapePressed(); void closeTopDrawer(); diff --git a/client/ui/controllers/selfhosted/exportUiController.cpp b/client/ui/controllers/selfhosted/exportUiController.cpp index 4a9f8bfc4..2c76fccb6 100644 --- a/client/ui/controllers/selfhosted/exportUiController.cpp +++ b/client/ui/controllers/selfhosted/exportUiController.cpp @@ -8,46 +8,46 @@ ExportUiController::ExportUiController(ExportController* exportController, QObje { } -void ExportUiController::generateFullAccessConfig(int serverIndex) +void ExportUiController::generateFullAccessConfig(const QString &serverId) { clearPreviousConfig(); - auto result = m_exportController->generateFullAccessConfig(serverIndex); + auto result = m_exportController->generateFullAccessConfig(serverId); applyExportResult(result); } -void ExportUiController::generateConnectionConfig(int serverIndex, int containerIndex, const QString &clientName) +void ExportUiController::generateConnectionConfig(const QString &serverId, int containerIndex, const QString &clientName) { clearPreviousConfig(); - auto result = m_exportController->generateConnectionConfig(serverIndex, containerIndex, clientName); + auto result = m_exportController->generateConnectionConfig(serverId, containerIndex, clientName); applyExportResult(result); } -void ExportUiController::generateOpenVpnConfig(int serverIndex, const QString &clientName) +void ExportUiController::generateOpenVpnConfig(const QString &serverId, const QString &clientName) { clearPreviousConfig(); - auto result = m_exportController->generateOpenVpnConfig(serverIndex, clientName); + auto result = m_exportController->generateOpenVpnConfig(serverId, clientName); applyExportResult(result); } -void ExportUiController::generateWireGuardConfig(int serverIndex, const QString &clientName) +void ExportUiController::generateWireGuardConfig(const QString &serverId, const QString &clientName) { clearPreviousConfig(); - auto result = m_exportController->generateWireGuardConfig(serverIndex, clientName); + auto result = m_exportController->generateWireGuardConfig(serverId, clientName); applyExportResult(result); } -void ExportUiController::generateAwgConfig(int serverIndex, int containerIndex, const QString &clientName) +void ExportUiController::generateAwgConfig(const QString &serverId, int containerIndex, const QString &clientName) { clearPreviousConfig(); - auto result = m_exportController->generateAwgConfig(serverIndex, containerIndex, clientName); + auto result = m_exportController->generateAwgConfig(serverId, containerIndex, clientName); applyExportResult(result); } -void ExportUiController::generateXrayConfig(int serverIndex, const QString &clientName) +void ExportUiController::generateXrayConfig(const QString &serverId, const QString &clientName) { clearPreviousConfig(); - auto result = m_exportController->generateXrayConfig(serverIndex, clientName); + auto result = m_exportController->generateXrayConfig(serverId, clientName); applyExportResult(result); } @@ -71,20 +71,20 @@ void ExportUiController::exportConfig(const QString &fileName) SystemController::saveFile(fileName, m_config); } -void ExportUiController::updateClientManagementModel(int serverIndex, int containerIndex) +void ExportUiController::updateClientManagementModel(const QString &serverId, int containerIndex) { - m_exportController->updateClientManagementModel(serverIndex, containerIndex); + m_exportController->updateClientManagementModel(serverId, containerIndex); } -void ExportUiController::revokeConfig(int row, int serverIndex, int containerIndex) +void ExportUiController::revokeConfig(int row, const QString &serverId, int containerIndex) { - m_exportController->revokeConfig(row, serverIndex, containerIndex); + m_exportController->revokeConfig(row, serverId, containerIndex); emit revokeConfigFinished(); } -void ExportUiController::renameClient(int row, const QString &clientName, int serverIndex, int containerIndex) +void ExportUiController::renameClient(int row, const QString &clientName, const QString &serverId, int containerIndex) { - m_exportController->renameClient(row, clientName, serverIndex, containerIndex); + m_exportController->renameClient(row, clientName, serverId, containerIndex); } int ExportUiController::getQrCodesCount() diff --git a/client/ui/controllers/selfhosted/exportUiController.h b/client/ui/controllers/selfhosted/exportUiController.h index bfc000b0e..5bcac90bd 100644 --- a/client/ui/controllers/selfhosted/exportUiController.h +++ b/client/ui/controllers/selfhosted/exportUiController.h @@ -4,6 +4,7 @@ #include #include "core/controllers/selfhosted/exportController.h" +#include "core/utils/errorCodes.h" class ExportUiController : public QObject { @@ -17,12 +18,13 @@ public: Q_PROPERTY(QString nativeConfigString READ getNativeConfigString NOTIFY exportConfigChanged) public slots: - void generateFullAccessConfig(int serverIndex); - void generateConnectionConfig(int serverIndex, int containerIndex, const QString &clientName); - void generateOpenVpnConfig(int serverIndex, const QString &clientName); - void generateWireGuardConfig(int serverIndex, const QString &clientName); - void generateAwgConfig(int serverIndex, int containerIndex, const QString &clientName); - void generateXrayConfig(int serverIndex, const QString &clientName); + void generateFullAccessConfig(const QString &serverId); + + void generateConnectionConfig(const QString &serverId, int containerIndex, const QString &clientName); + void generateOpenVpnConfig(const QString &serverId, const QString &clientName); + void generateWireGuardConfig(const QString &serverId, const QString &clientName); + void generateAwgConfig(const QString &serverId, int containerIndex, const QString &clientName); + void generateXrayConfig(const QString &serverId, const QString &clientName); QString getConfig(); QString getNativeConfigString(); @@ -30,9 +32,11 @@ public slots: void exportConfig(const QString &fileName); - void updateClientManagementModel(int serverIndex, int containerIndex); - void revokeConfig(int row, int serverIndex, int containerIndex); - void renameClient(int row, const QString &clientName, int serverIndex, int containerIndex); + void updateClientManagementModel(const QString &serverId, int containerIndex); + + void revokeConfig(int row, const QString &serverId, int containerIndex); + + void renameClient(int row, const QString &clientName, const QString &serverId, int containerIndex); signals: void generateConfig(int type); diff --git a/client/ui/controllers/selfhosted/installUiController.cpp b/client/ui/controllers/selfhosted/installUiController.cpp index b867a0f5d..32178e7a6 100755 --- a/client/ui/controllers/selfhosted/installUiController.cpp +++ b/client/ui/controllers/selfhosted/installUiController.cpp @@ -11,7 +11,6 @@ #include "core/controllers/selfhosted/installController.h" #include "core/utils/selfhosted/sshSession.h" #include "core/utils/networkUtilities.h" -#include "logger.h" #include "core/utils/protocolEnum.h" #include "core/protocols/protocolUtils.h" #include "core/utils/constants/configKeys.h" @@ -27,33 +26,12 @@ #include "ui/models/services/socks5ProxyConfigModel.h" #include "ui/models/services/torConfigModel.h" #include "core/utils/utilities.h" -#include "core/models/serverConfig.h" #include "core/models/containerConfig.h" #include "core/models/protocols/awgProtocolConfig.h" #include "core/models/protocols/wireGuardProtocolConfig.h" #include "core/models/protocols/openVpnProtocolConfig.h" #include "core/models/protocols/xrayProtocolConfig.h" -namespace -{ - Logger logger("InstallUiController"); - - namespace configKey - { - constexpr char serviceInfo[] = "service_info"; - constexpr char serviceType[] = "service_type"; - constexpr char serviceProtocol[] = "service_protocol"; - constexpr char userCountryCode[] = "user_country_code"; - - constexpr char serverCountryCode[] = "server_country_code"; - constexpr char serverCountryName[] = "server_country_name"; - constexpr char availableCountries[] = "available_countries"; - - constexpr char apiConfig[] = "api_config"; - constexpr char authData[] = "auth_data"; - } -} - InstallUiController::InstallUiController(InstallController *installController, ServersController *serversController, SettingsController *settingsController, @@ -101,19 +79,18 @@ InstallUiController::~InstallUiController() { } -void InstallUiController::install(DockerContainer container, int port, TransportProto transportProto, int serverIndex) +void InstallUiController::install(DockerContainer container, int port, TransportProto transportProto, const QString &serverId) { - const bool isNewServer = serverIndex < 0; + const bool isNewServer = serverId.isEmpty(); ServerCredentials serverCredentials; if (isNewServer) { serverCredentials = m_processedServerCredentials; } else { - serverCredentials = m_serversController->getServerCredentials(serverIndex); + serverCredentials = m_serversController->getServerCredentials(serverId); m_processedServerCredentials = ServerCredentials(); } - QMap preparedContainers; QString finishMessage; ErrorCode errorCode; @@ -131,9 +108,13 @@ void InstallUiController::install(DockerContainer container, int port, Transport return; } - int serverIndex = m_serversController->getServersCount() - 1; - ServerConfig serverConfig = m_serversController->getServerConfig(serverIndex); - QMap containers = serverConfig.containers(); + const QString newServerId = m_serversController->getServerId(m_serversController->getServersCount() - 1); + const auto admin = m_serversController->selfHostedAdminConfig(newServerId); + if (!admin.has_value()) { + emit installationErrorOccurred(ErrorCode::InternalError); + return; + } + QMap containers = admin->containers; int containersCount = containers.size(); if (wasContainerInstalled) { @@ -148,20 +129,28 @@ void InstallUiController::install(DockerContainer container, int port, Transport emit installServerFinished(finishMessage); } else { - ServerConfig serverConfig = m_serversController->getServerConfig(serverIndex); - QMap containers = serverConfig.containers(); + const auto adminBefore = m_serversController->selfHostedAdminConfig(serverId); + if (!adminBefore.has_value()) { + emit installationErrorOccurred(ErrorCode::InternalError); + return; + } + QMap containers = adminBefore->containers; int containersCount = containers.size(); bool wasContainerInstalled = false; - errorCode = m_installController->installContainer(serverIndex, container, port, transportProto, + errorCode = m_installController->installContainer(serverId, container, port, transportProto, wasContainerInstalled); if (errorCode) { emit installationErrorOccurred(errorCode); return; } - ServerConfig newServerConfig = m_serversController->getServerConfig(serverIndex); - QMap newContainers = newServerConfig.containers(); + const auto adminAfter = m_serversController->selfHostedAdminConfig(serverId); + if (!adminAfter.has_value()) { + emit installationErrorOccurred(ErrorCode::InternalError); + return; + } + QMap newContainers = adminAfter->containers; int newContainersCount = newContainers.size(); bool hasNewContainers = (newContainersCount - containersCount) > (wasContainerInstalled ? 1 : 0); @@ -181,17 +170,25 @@ void InstallUiController::install(DockerContainer container, int port, Transport } } -void InstallUiController::scanServerForInstalledContainers(int serverIndex) +void InstallUiController::scanServerForInstalledContainers(const QString &serverId) { - ServerConfig serverBefore = m_serversController->getServerConfig(serverIndex); - QMap containersBefore = serverBefore.containers(); + const auto serverBefore = m_serversController->selfHostedAdminConfig(serverId); + if (!serverBefore.has_value()) { + emit installationErrorOccurred(ErrorCode::InternalError); + return; + } + QMap containersBefore = serverBefore->containers; int containersCountBefore = containersBefore.size(); - ErrorCode errorCode = m_installController->scanServerForInstalledContainers(serverIndex); + ErrorCode errorCode = m_installController->scanServerForInstalledContainers(serverId); if (errorCode == ErrorCode::NoError) { - ServerConfig serverAfter = m_serversController->getServerConfig(serverIndex); - QMap containersAfter = serverAfter.containers(); + const auto serverAfter = m_serversController->selfHostedAdminConfig(serverId); + if (!serverAfter.has_value()) { + emit installationErrorOccurred(ErrorCode::InternalError); + return; + } + QMap containersAfter = serverAfter->containers; int containersCountAfter = containersAfter.size(); bool isInstalledContainerAdded = containersCountAfter > containersCountBefore; @@ -202,7 +199,7 @@ void InstallUiController::scanServerForInstalledContainers(int serverIndex) emit installationErrorOccurred(errorCode); } -void InstallUiController::updateContainer(int serverIndex, int containerIndex, int protocolIndex) +void InstallUiController::updateContainer(const QString &serverId, int containerIndex, int protocolIndex) { DockerContainer container = static_cast(containerIndex); @@ -250,32 +247,26 @@ void InstallUiController::updateContainer(int serverIndex, int containerIndex, i default: return; } - ContainerConfig oldContainerConfig = m_serversController->getContainerConfig(serverIndex, container); + ContainerConfig oldContainerConfig = m_serversController->getContainerConfig(serverId, container); - ErrorCode errorCode = m_installController->updateContainer(serverIndex, container, oldContainerConfig, containerConfig); + ErrorCode errorCode = m_installController->updateContainer(serverId, container, oldContainerConfig, containerConfig); if (errorCode == ErrorCode::NoError) { - ContainerConfig updatedConfig = m_serversController->getContainerConfig(serverIndex, container); + ContainerConfig updatedConfig = m_serversController->getContainerConfig(serverId, container); m_protocolModel->updateModel(updatedConfig); - auto defaultContainer = m_serversController->getServerConfig(serverIndex).defaultContainer(); - if ((serverIndex == m_serversController->getDefaultServerIndex()) && (container == defaultContainer)) { - emit currentContainerUpdated(); - } else { - emit updateContainerFinished(tr("Settings updated successfully")); - } - + emit updateContainerFinished(tr("Settings updated successfully")); return; } emit installationErrorOccurred(errorCode); } -void InstallUiController::rebootServer(int serverIndex) +void InstallUiController::rebootServer(const QString &serverId) { - QString serverName = m_serversController->getServerConfig(serverIndex).displayName(); + const QString serverName = m_serversController->notificationDisplayName(serverId); - const auto errorCode = m_installController->rebootServer(serverIndex); + const auto errorCode = m_installController->rebootServer(serverId); if (errorCode == ErrorCode::NoError) { emit rebootServerFinished(tr("Server '%1' was rebooted").arg(serverName)); } else { @@ -283,19 +274,22 @@ void InstallUiController::rebootServer(int serverIndex) } } -void InstallUiController::removeServer(int serverIndex) +void InstallUiController::removeServer(const QString &serverId) { - QString serverName = m_serversController->getServerConfig(serverIndex).displayName(); + if (serverId.isEmpty()) { + return; + } + const QString serverName = m_serversController->notificationDisplayName(serverId); - m_serversController->removeServer(serverIndex); + m_serversController->removeServer(serverId); emit removeServerFinished(tr("Server '%1' was removed").arg(serverName)); } -void InstallUiController::removeAllContainers(int serverIndex) +void InstallUiController::removeAllContainers(const QString &serverId) { - QString serverName = m_serversController->getServerConfig(serverIndex).displayName(); + const QString serverName = m_serversController->notificationDisplayName(serverId); - ErrorCode errorCode = m_installController->removeAllContainers(serverIndex); + ErrorCode errorCode = m_installController->removeAllContainers(serverId); if (errorCode == ErrorCode::NoError) { emit removeAllContainersFinished(tr("All containers from server '%1' have been removed").arg(serverName)); return; @@ -303,14 +297,14 @@ void InstallUiController::removeAllContainers(int serverIndex) emit installationErrorOccurred(errorCode); } -void InstallUiController::removeContainer(int serverIndex, int containerIndex) +void InstallUiController::removeContainer(const QString &serverId, int containerIndex) { - QString serverName = m_serversController->getServerConfig(serverIndex).displayName(); + const QString serverName = m_serversController->notificationDisplayName(serverId); DockerContainer container = static_cast(containerIndex); QString containerName = ContainerUtils::containerHumanNames().value(container); - ErrorCode errorCode = m_installController->removeContainer(serverIndex, container); + ErrorCode errorCode = m_installController->removeContainer(serverId, container); if (errorCode == ErrorCode::NoError) { emit removeContainerFinished(tr("%1 has been removed from the server '%2'").arg(containerName, serverName)); @@ -319,17 +313,17 @@ void InstallUiController::removeContainer(int serverIndex, int containerIndex) emit installationErrorOccurred(errorCode); } -void InstallUiController::clearCachedProfile(int serverIndex, int containerIndex) +void InstallUiController::clearCachedProfile(const QString &serverId, int containerIndex) { DockerContainer container = static_cast(containerIndex); if (ContainerUtils::containerService(container) == ServiceType::Other) { return; } - m_installController->clearCachedProfile(serverIndex, container); + m_installController->clearCachedProfile(serverId, container); emit cachedProfileCleared(tr("%1 cached profile cleared").arg(ContainerUtils::containerHumanNames().value(container))); - ContainerConfig updatedConfig = m_serversController->getContainerConfig(serverIndex, container); + ContainerConfig updatedConfig = m_serversController->getContainerConfig(serverId, container); m_protocolModel->updateModel(updatedConfig); } @@ -354,9 +348,9 @@ void InstallUiController::setProcessedServerCredentials(const QString &hostName, m_processedServerCredentials.secretData = secretData; } -void InstallUiController::mountSftpDrive(int serverIndex, const QString &port, const QString &password, const QString &username) +void InstallUiController::mountSftpDrive(const QString &serverId, const QString &port, const QString &password, const QString &username) { - ServerCredentials serverCredentials = m_serversController->getServerCredentials(serverIndex); + ServerCredentials serverCredentials = m_serversController->getServerCredentials(serverId); ErrorCode errorCode = m_installController->mountSftpDrive(serverCredentials, port, password, username); if (errorCode != ErrorCode::NoError) { emit installationErrorOccurred(errorCode); @@ -399,40 +393,35 @@ void InstallUiController::setEncryptedPassphrase(QString passphrase) void InstallUiController::addEmptyServer() { - SelfHostedServerConfig serverConfig; - serverConfig.hostName = m_processedServerCredentials.hostName; - serverConfig.userName = m_processedServerCredentials.userName; - serverConfig.password = m_processedServerCredentials.secretData; - serverConfig.port = m_processedServerCredentials.port; - serverConfig.description = m_settingsController->nextAvailableServerName(); - serverConfig.defaultContainer = DockerContainer::None; - - m_serversController->addServer(ServerConfig(serverConfig)); + m_installController->addEmptyServer(m_processedServerCredentials); emit installServerFinished(tr("Server added successfully")); } void InstallUiController::validateConfig() { - int serverIndex = m_serversController->getDefaultServerIndex(); - m_installController->validateConfig(serverIndex); + const QString serverId = m_serversController->getDefaultServerId(); + if (serverId.isEmpty()) { + return; + } + m_installController->validateConfig(serverId); } -void InstallUiController::updateProtocols(int serverIndex, int containerIndex) +void InstallUiController::updateProtocols(const QString &serverId, int containerIndex) { DockerContainer container = static_cast(containerIndex); - ContainerConfig containerConfig = m_serversController->getContainerConfig(serverIndex, container); + ContainerConfig containerConfig = m_serversController->getContainerConfig(serverId, container); containerConfig.container = container; m_protocolModel->updateModel(containerConfig); } -void InstallUiController::openServerSettings(int serverIndex, int containerIndex, int protocolIndex) +void InstallUiController::openServerSettings(const QString &serverId, int containerIndex, int protocolIndex) { - updateProtocolConfigModel(serverIndex, containerIndex, protocolIndex); + updateProtocolConfigModel(serverId, containerIndex, protocolIndex); } -void InstallUiController::openClientSettings(int serverIndex, int containerIndex, int protocolIndex) +void InstallUiController::openClientSettings(const QString &serverId, int containerIndex, int protocolIndex) { - updateProtocolConfigModel(serverIndex, containerIndex, protocolIndex); + updateProtocolConfigModel(serverId, containerIndex, protocolIndex); } int InstallUiController::defaultPort(int protocolIndex) @@ -465,10 +454,10 @@ bool InstallUiController::defaultTransportProtoChangeable(int protocolIndex) return ProtocolUtils::defaultTransportProtoChangeable(proto); } -void InstallUiController::updateProtocolConfigModel(int serverIndex, int containerIndex, int protocolIndex) +void InstallUiController::updateProtocolConfigModel(const QString &serverId, int containerIndex, int protocolIndex) { DockerContainer container = static_cast(containerIndex); - ContainerConfig containerConfig = m_serversController->getContainerConfig(serverIndex, container); + ContainerConfig containerConfig = m_serversController->getContainerConfig(serverId, container); containerConfig.container = container; Proto protocolType = static_cast(protocolIndex); @@ -490,4 +479,3 @@ void InstallUiController::updateProtocolConfigModel(int serverIndex, int contain default: break; } } - diff --git a/client/ui/controllers/selfhosted/installUiController.h b/client/ui/controllers/selfhosted/installUiController.h index 89e0291cf..beec0a3b8 100644 --- a/client/ui/controllers/selfhosted/installUiController.h +++ b/client/ui/controllers/selfhosted/installUiController.h @@ -52,24 +52,24 @@ public: ~InstallUiController(); public slots: - void install(DockerContainer container, int port, TransportProto transportProto, int serverIndex); + void install(DockerContainer container, int port, TransportProto transportProto, const QString &serverId); void setProcessedServerCredentials(const QString &hostName, const QString &userName, const QString &secretData); void clearProcessedServerCredentials(); - void scanServerForInstalledContainers(int serverIndex); + void scanServerForInstalledContainers(const QString &serverId); - void updateContainer(int serverIndex, int containerIndex, int protocolIndex); + void updateContainer(const QString &serverId, int containerIndex, int protocolIndex); - void removeServer(int serverIndex); - void rebootServer(int serverIndex); - void removeAllContainers(int serverIndex); - void removeContainer(int serverIndex, int containerIndex); + void removeServer(const QString &serverId); + void rebootServer(const QString &serverId); + void removeAllContainers(const QString &serverId); + void removeContainer(const QString &serverId, int containerIndex); - void clearCachedProfile(int serverIndex, int containerIndex); + void clearCachedProfile(const QString &serverId, int containerIndex); QRegularExpression ipAddressRegExp(); - void mountSftpDrive(int serverIndex, const QString &port, const QString &password, const QString &username); + void mountSftpDrive(const QString &serverId, const QString &port, const QString &password, const QString &username); bool checkSshConnection(); @@ -78,12 +78,12 @@ public slots: void addEmptyServer(); void validateConfig(); - - Q_INVOKABLE void updateProtocols(int serverIndex, int containerIndex); - - void openServerSettings(int serverIndex, int containerIndex, int protocolIndex); - void openClientSettings(int serverIndex, int containerIndex, int protocolIndex); - + + Q_INVOKABLE void updateProtocols(const QString &serverId, int containerIndex); + + void openServerSettings(const QString &serverId, int containerIndex, int protocolIndex); + void openClientSettings(const QString &serverId, int containerIndex, int protocolIndex); + int defaultPort(int protocolIndex); int getPortForInstall(int protocolIndex); int defaultTransportProto(int protocolIndex); @@ -114,8 +114,6 @@ signals: void serverIsBusy(const bool isBusy); void cancelInstallation(); - void currentContainerUpdated(); - void cachedProfileCleared(const QString &message); void apiConfigRemoved(const QString &message); @@ -145,7 +143,7 @@ private: QString m_privateKeyPassphrase; - void updateProtocolConfigModel(int serverIndex, int containerIndex, int protocolIndex); + void updateProtocolConfigModel(const QString &serverId, int containerIndex, int protocolIndex); }; #endif // INSTALLUICONTROLLER_H diff --git a/client/ui/controllers/serversUiController.cpp b/client/ui/controllers/serversUiController.cpp index ccafe0f4b..b9c2a9bf9 100644 --- a/client/ui/controllers/serversUiController.cpp +++ b/client/ui/controllers/serversUiController.cpp @@ -1,37 +1,37 @@ #include "serversUiController.h" -#include "core/utils/api/apiEnums.h" -#include "core/utils/constants/apiKeys.h" -#include "core/utils/constants/apiConstants.h" -#include "core/utils/api/apiUtils.h" #include "core/utils/containerEnum.h" #include "core/utils/containers/containerUtils.h" #include "core/utils/protocolEnum.h" -#include "core/utils/protocolEnum.h" -#include "core/protocols/protocolUtils.h" -#include "core/utils/constants/configKeys.h" -#include "core/utils/constants/protocolConstants.h" -#include -#include -#include "core/models/serverConfig.h" #include "core/models/protocolConfig.h" #include "core/models/containerConfig.h" -#include "core/models/protocols/awgProtocolConfig.h" using namespace amnezia; -namespace +namespace { +int rowForServerId(const QVector &list, const QString &serverId) { - namespace configKey - { - constexpr char apiConfig[] = "api_config"; - constexpr char serverCountryCode[] = "server_country_code"; - constexpr char serverCountryName[] = "server_country_name"; - constexpr char userCountryCode[] = "user_country_code"; - constexpr char serviceType[] = "service_type"; + if (serverId.isEmpty()) { + return -1; } + for (int i = 0; i < list.size(); ++i) { + if (list.at(i).serverId == serverId) { + return i; + } + } + return -1; } +bool descriptionsHaveGatewayServers(const QVector &list) +{ + for (const auto &d : list) { + if (d.isServerFromGatewayApi) { + return true; + } + } + return false; +} +} // namespace ServersUiController::ServersUiController(ServersController* serversController, SettingsController* settingsController, ServersModel* serversModel, @@ -47,48 +47,70 @@ ServersUiController::ServersUiController(ServersController* serversController, { } -void ServersUiController::removeServer(int index) +void ServersUiController::removeServer(const QString &serverId) { - m_serversController->removeServer(index); - updateModel(); -} - -void ServersUiController::editServerName(int index, const QString &name) -{ - ServerConfig serverConfig = m_serversController->getServerConfig(index); - - if (serverConfig.isApiV1()) { - ApiV1ServerConfig* apiV1 = serverConfig.as(); - if (apiV1) { - apiV1->name = name; - } - } else if (serverConfig.isApiV2()) { - ApiV2ServerConfig* apiV2 = serverConfig.as(); - if (apiV2) { - apiV2->name = name; - apiV2->nameOverriddenByUser = true; - } - } else { - serverConfig.visit([&name](auto& arg) { - arg.description = name; - }); + if (serverId.isEmpty()) { + return; } - - m_serversController->editServer(index, serverConfig); + m_serversController->removeServer(serverId); updateModel(); } -void ServersUiController::setDefaultServerIndex(int index) +void ServersUiController::removeServerAtIndex(int index) { - m_serversController->setDefaultServerIndex(index); - updateModel(); - emit defaultServerIndexChanged(index); + const QString serverId = getServerId(index); + if (!serverId.isEmpty()) { + removeServer(serverId); + } } -void ServersUiController::setDefaultContainer(int serverIndex, int containerIndex) +void ServersUiController::setDefaultServerAtIndex(int index) { + const QString serverId = getServerId(index); + if (!serverId.isEmpty()) { + setDefaultServer(serverId); + } +} + +void ServersUiController::setDefaultContainerAtIndex(int index, int containerIndex) +{ + const QString serverId = getServerId(index); + if (!serverId.isEmpty()) { + setDefaultContainer(serverId, containerIndex); + } +} + +void ServersUiController::editServerName(const QString &serverId, const QString &name) +{ + if (serverId.isEmpty()) { + return; + } + + if (!m_serversController->renameServer(serverId, name)) { + emit errorOccurred(tr("Legacy API v1 configs are no longer supported. Remove this server to continue.")); + emit finished(tr("Use the remove action to delete this legacy config.")); + return; + } + updateModel(); +} + +void ServersUiController::setDefaultServer(const QString &serverId) +{ + if (serverId.isEmpty()) { + return; + } + m_serversController->setDefaultServer(serverId); + updateModel(); + emit defaultServerIdChanged(serverId); +} + +void ServersUiController::setDefaultContainer(const QString &serverId, int containerIndex) +{ + if (serverId.isEmpty()) { + return; + } auto container = static_cast(containerIndex); - m_serversController->setDefaultContainer(serverIndex, container); + m_serversController->setDefaultContainer(serverId, container); updateModel(); } @@ -98,129 +120,123 @@ void ServersUiController::toggleAmneziaDns(bool enabled) updateModel(); } -void ServersUiController::onDefaultServerChanged(int index) +void ServersUiController::onDefaultServerChanged(const QString &/*defaultServerId*/) { - setProcessedServerIndex(index); updateModel(); + setProcessedServerId(m_serversController->getDefaultServerId()); updateDefaultServerContainersModel(); - emit defaultServerIndexChanged(index); + emit defaultServerIdChanged(m_serversController->getDefaultServerId()); } void ServersUiController::updateModel() { - int defaultIndex = m_serversController->getDefaultServerIndex(); - bool wasEmpty = !hasServersFromGatewayApi(); - int serversCount = m_serversController->getServersCount(); + QVector descriptions = + m_serversController->buildServerDescriptions(m_settingsController->isAmneziaDnsEnabled()); - if (m_processedServerIndex >= serversCount) { - setProcessedServerIndex(defaultIndex); - } else if (m_processedServerIndex >= 0) { - setProcessedServerIndex(m_processedServerIndex); + const QString defaultServerId = m_serversController->getDefaultServerId(); + const bool hadServersFromGatewayBefore = descriptionsHaveGatewayServers(m_orderedServerDescriptions); + const bool hasServersFromGatewayNow = descriptionsHaveGatewayServers(descriptions); + const int listCount = descriptions.size(); + const int defaultRowInDescriptions = rowForServerId(descriptions, defaultServerId); + + m_orderedServerDescriptions = descriptions; + + if (listCount == 0) { + setProcessedServerId(QString()); + } else if (m_processedServerIndex >= listCount) { + setProcessedServerId(defaultServerId); + } else if (!m_processedServerId.isEmpty()) { + const int row = rowForServerId(m_orderedServerDescriptions, m_processedServerId); + if (row < 0) { + setProcessedServerId(defaultServerId); + } else { + setProcessedServerId(m_processedServerId); + } + } else if (defaultRowInDescriptions >= 0) { + setProcessedServerId(defaultServerId); } - - m_serversModel->updateModel(m_serversController->getServers(), defaultIndex, m_settingsController->isAmneziaDnsEnabled()); - - + + m_serversModel->updateModel(m_orderedServerDescriptions, defaultRowInDescriptions); + updateContainersModel(); updateDefaultServerContainersModel(); - - bool isEmpty = !hasServersFromGatewayApi(); - if (wasEmpty != isEmpty) { + + if (hadServersFromGatewayBefore != hasServersFromGatewayNow) { emit hasServersFromGatewayApiChanged(); } - - emit defaultServerIndexChanged(defaultIndex); + + emit defaultServerIdChanged(defaultServerId); + emit defaultServerIndexChanged(defaultServerIndex()); } -int ServersUiController::getDefaultServerIndex() const +QString ServersUiController::getDefaultServerId() const { - return m_serversController->getDefaultServerIndex(); + return m_serversController->getDefaultServerId(); } QString ServersUiController::getDefaultServerName() const { - int defaultIndex = getDefaultServerIndex(); - return m_serversController->getServerConfig(defaultIndex).displayName(); + const QString defaultServerId = m_serversController->getDefaultServerId(); + for (const auto &description : m_orderedServerDescriptions) { + if (description.serverId == defaultServerId) { + return description.serverName; + } + } + return QString(); } QString ServersUiController::getDefaultServerDefaultContainerName() const { - int defaultIndex = getDefaultServerIndex(); - const ServerConfig server = m_serversController->getServerConfig(defaultIndex); - return ContainerUtils::containerHumanNames().value(server.defaultContainer()); + const QString defaultServerId = m_serversController->getDefaultServerId(); + for (const auto &description : m_orderedServerDescriptions) { + if (description.serverId == defaultServerId) { + return ContainerUtils::containerHumanNames().value(description.defaultContainer); + } + } + return QString(); } QString ServersUiController::getDefaultServerDescriptionCollapsed() const { - int defaultIndex = getDefaultServerIndex(); - const ServerConfig server = m_serversController->getServerConfig(defaultIndex); - QString description = getDefaultServerDescription(server, defaultIndex); - - if (server.isApiConfig()) { - return description; - } - - DockerContainer container = server.defaultContainer(); - QString containerName = ContainerUtils::containerHumanNames().value(container); - QString protocolVersion; - QString hostName = server.hostName(); - - if (ContainerUtils::isAwgContainer(container)) { - ContainerConfig containerConfig = server.containerConfig(container); - if (auto* awgProtocolConfig = containerConfig.getAwgProtocolConfig()) { - QString version = awgProtocolConfig->serverConfig.protocolVersion; - if (version == protocols::awg::awgV2) { - protocolVersion = QObject::tr(" (version 2)"); - } else if (version == protocols::awg::awgV1_5) { - protocolVersion = QObject::tr(" (version 1.5)"); - } - - if (container == DockerContainer::Awg && !awgProtocolConfig->serverConfig.isThirdPartyConfig) { - containerName = "AmneziaWG Legacy"; - } + const QString defaultServerId = m_serversController->getDefaultServerId(); + for (const auto &description : m_orderedServerDescriptions) { + if (description.serverId == defaultServerId) { + return description.collapsedServerDescription; } } - - return description + containerName + protocolVersion + " | " + hostName; + return QString(); } QString ServersUiController::getDefaultServerImagePathCollapsed() const { - int defaultIndex = getDefaultServerIndex(); - const ServerConfig server = m_serversController->getServerConfig(defaultIndex); - - if (server.isApiV2()) { - const ApiV2ServerConfig* apiV2 = server.as(); - if (!apiV2) return QString(); - const QString countryCode = apiV2->apiConfig.serverCountryCode; - if (countryCode.isEmpty()) { - return ""; + const QString defaultServerId = m_serversController->getDefaultServerId(); + for (const auto &description : m_orderedServerDescriptions) { + if (description.serverId == defaultServerId) { + if (!description.isApiV2 || description.apiServerCountryCode.isEmpty()) { + return ""; + } + return QString("qrc:/countriesFlags/images/flagKit/%1.svg").arg(description.apiServerCountryCode.toUpper()); } - return QString("qrc:/countriesFlags/images/flagKit/%1.svg").arg(countryCode.toUpper()); } return ""; } QString ServersUiController::getDefaultServerDescriptionExpanded() const { - int defaultIndex = getDefaultServerIndex(); - const ServerConfig server = m_serversController->getServerConfig(defaultIndex); - QString description = getDefaultServerDescription(server, defaultIndex); - - if (server.isApiConfig()) { - return description; + const QString defaultServerId = m_serversController->getDefaultServerId(); + for (const auto &description : m_orderedServerDescriptions) { + if (description.serverId == defaultServerId) { + return description.expandedServerDescription; + } } - - return description + server.hostName(); + return QString(); } bool ServersUiController::isDefaultServerDefaultContainerHasSplitTunneling() const { - int defaultIndex = getDefaultServerIndex(); - const ServerConfig server = m_serversController->getServerConfig(defaultIndex); - DockerContainer defaultContainer = server.defaultContainer(); - - ContainerConfig containerConfig = server.containerConfig(defaultContainer); + const QString defaultServerId = m_serversController->getDefaultServerId(); + const DockerContainer defaultContainer = m_serversController->getDefaultContainer(defaultServerId); + const ContainerConfig containerConfig = m_serversController->getContainerConfig(defaultServerId, defaultContainer); if (defaultContainer == DockerContainer::Awg || defaultContainer == DockerContainer::WireGuard) { auto hasSplitTunnelingFromAllowedIps = [](const QStringList& allowedIps, const QString& nativeConfig) -> bool { @@ -265,16 +281,13 @@ bool ServersUiController::isDefaultServerDefaultContainerHasSplitTunneling() con bool ServersUiController::isDefaultServerFromApi() const { - int defaultIndex = getDefaultServerIndex(); - const ServerConfig server = m_serversController->getServerConfig(defaultIndex); - const int configVersion = server.configVersion(); - return configVersion == apiDefs::ConfigSource::Telegram - || configVersion == apiDefs::ConfigSource::AmneziaGateway; -} - -int ServersUiController::getProcessedServerIndex() const -{ - return m_processedServerIndex; + const QString defaultServerId = m_serversController->getDefaultServerId(); + for (const auto &description : m_orderedServerDescriptions) { + if (description.serverId == defaultServerId) { + return description.isApiV2; + } + } + return false; } int ServersUiController::getProcessedContainerIndex() const @@ -291,186 +304,196 @@ void ServersUiController::setProcessedContainerIndex(int index) } } -void ServersUiController::setProcessedServerIndex(int index) +QString ServersUiController::getProcessedServerId() const { - if (index >= m_serversController->getServersCount()) { + return m_processedServerId; +} + +void ServersUiController::setProcessedServerId(const QString &serverId) +{ + const int index = serverId.isEmpty() ? -1 : serverIndexForId(serverId); + if (!serverId.isEmpty() && index < 0) { return; } - if (m_processedServerIndex != index) { + if (m_processedServerIndex != index || m_processedServerId != serverId) { m_processedServerIndex = index; + m_processedServerId = serverId; m_serversModel->setProcessedServerIndex(index); if (index >= 0) { updateContainersModel(); - - ServerConfig server = m_serversController->getServerConfig(index); - setProcessedContainerIndex(static_cast(server.defaultContainer())); - - if (server.isApiV2()) { - const ApiV2ServerConfig* apiV2 = server.as(); - if (apiV2 && !apiV2->apiConfig.availableCountries.isEmpty()) { - emit updateApiCountryModel(); + for (const auto &description : m_orderedServerDescriptions) { + if (description.serverId == serverId) { + setProcessedContainerIndex(static_cast(description.defaultContainer)); + break; } - emit updateApiServicesModel(); + } + + for (const auto &description : m_orderedServerDescriptions) { + if (description.serverId != serverId) { + continue; + } + if (description.isApiV2) { + if (description.isCountrySelectionAvailable && !description.apiAvailableCountries.isEmpty()) { + emit updateApiCountryModel(); + } + emit updateApiServicesModel(); + } + break; } } + emit processedServerIdChanged(m_processedServerId); emit processedServerIndexChanged(m_processedServerIndex); } } +int ServersUiController::getProcessedServerIndex() const +{ + return m_processedServerIndex; +} + +void ServersUiController::setProcessedServerIndex(int index) +{ + if (index < 0) { + setProcessedServerId(QString()); + return; + } + const QString id = getServerId(index); + if (!id.isEmpty()) { + setProcessedServerId(id); + } +} + +int ServersUiController::defaultServerIndex() const +{ + return rowForServerId(m_orderedServerDescriptions, getDefaultServerId()); +} + bool ServersUiController::processedServerIsPremium() const { - ServerConfig server = m_serversController->getServerConfig(m_processedServerIndex); - if (server.isApiV1()) { - const ApiV1ServerConfig* apiV1 = server.as(); - return apiV1 ? apiV1->isPremium() : false; - } else if (server.isApiV2()) { - const ApiV2ServerConfig* apiV2 = server.as(); - return apiV2 ? (apiV2->isPremium() || apiV2->isExternalPremium()) : false; + for (const auto &description : m_orderedServerDescriptions) { + if (description.serverId == m_processedServerId) { + return description.isPremium; + } } return false; } const ServerCredentials ServersUiController::getProcessedServerCredentials() const { - return m_serversController->getServerCredentials(m_processedServerIndex); + return m_serversController->getServerCredentials(m_processedServerId); } bool ServersUiController::isDefaultServerCurrentlyProcessed() const { - return m_serversController->getDefaultServerIndex() == m_processedServerIndex; + return m_serversController->getDefaultServerId() == m_processedServerId; } bool ServersUiController::isProcessedServerHasWriteAccess() const { - ServerCredentials credentials = m_serversController->getServerCredentials(m_processedServerIndex); + ServerCredentials credentials = m_serversController->getServerCredentials(m_processedServerId); return (!credentials.userName.isEmpty() && !credentials.secretData.isEmpty()); } - -QString ServersUiController::getDefaultServerDescription(const ServerConfig& server, int index) const +QString ServersUiController::getDefaultServerDescription(const QString &serverId) const { - QString description; - - if (server.isApiV2()) { - const ApiV2ServerConfig* apiV2 = server.as(); - if (!apiV2) return QString(); - if (!apiV2->apiConfig.serverCountryCode.isEmpty()) { - return apiV2->apiConfig.serverCountryName; + for (const auto &description : m_orderedServerDescriptions) { + if (description.serverId == serverId) { + return description.baseDescription; } - return apiV2->description; - } else if (server.isApiV1()) { - const ApiV1ServerConfig* apiV1 = server.as(); - return apiV1 ? apiV1->description : QString(); - } else { - ServerCredentials credentials = m_serversController->getServerCredentials(index); - if (!credentials.userName.isEmpty() && !credentials.secretData.isEmpty()) { - bool isAmneziaDnsEnabled = m_settingsController->isAmneziaDnsEnabled(); - if (isAmneziaDnsEnabled && isAmneziaDnsContainerInstalled(index)) { - description += "Amnezia DNS | "; - } - } else { - if (server.dns1() == protocols::dns::amneziaDnsIp) { - description += "Amnezia DNS | "; - } - } - return description; } -} - -bool ServersUiController::isAmneziaDnsContainerInstalled(int serverIndex) const -{ - const ServerConfig server = m_serversController->getServerConfig(serverIndex); - QMap containers = server.containers(); - - return containers.contains(DockerContainer::Dns); + return QString(); } bool ServersUiController::hasServersFromGatewayApi() const { - QVector servers = m_serversController->getServers(); - for (const ServerConfig &server : servers) { - if (server.isApiV2()) { - return true; - } - } - return false; + return listHasServersFromGatewayApi(); } bool ServersUiController::isAdVisible() const { - int defaultIndex = getDefaultServerIndex(); - if (defaultIndex < 0) { + const QString defaultServerId = m_serversController->getDefaultServerId(); + if (defaultServerId.isEmpty()) { return false; } - ServerConfig server = m_serversController->getServerConfig(defaultIndex); - if (server.isApiV2()) { - const ApiV2ServerConfig* apiV2 = server.as(); - if (!apiV2) return false; - return apiV2->apiConfig.serviceInfo.isAdVisible; + for (const auto &description : m_orderedServerDescriptions) { + if (description.serverId == defaultServerId) { + return description.isAdVisible; + } } return false; } QString ServersUiController::adHeader() const { - int defaultIndex = getDefaultServerIndex(); - if (defaultIndex < 0) { + const QString defaultServerId = m_serversController->getDefaultServerId(); + if (defaultServerId.isEmpty()) { return QString(); } - ServerConfig server = m_serversController->getServerConfig(defaultIndex); - if (server.isApiV2()) { - const ApiV2ServerConfig* apiV2 = server.as(); - if (!apiV2) return QString(); - return apiV2->apiConfig.serviceInfo.adHeader; + for (const auto &description : m_orderedServerDescriptions) { + if (description.serverId == defaultServerId) { + return description.adHeader; + } } return QString(); } QString ServersUiController::adDescription() const { - int defaultIndex = getDefaultServerIndex(); - if (defaultIndex < 0) { + const QString defaultServerId = m_serversController->getDefaultServerId(); + if (defaultServerId.isEmpty()) { return QString(); } - ServerConfig server = m_serversController->getServerConfig(defaultIndex); - if (server.isApiV2()) { - const ApiV2ServerConfig* apiV2 = server.as(); - if (!apiV2) return QString(); - return apiV2->apiConfig.serviceInfo.adDescription; + for (const auto &description : m_orderedServerDescriptions) { + if (description.serverId == defaultServerId) { + return description.adDescription; + } } return QString(); } +QString ServersUiController::getServerId(int index) const +{ + if (index < 0 || index >= m_orderedServerDescriptions.size()) { + return QString(); + } + return m_orderedServerDescriptions.at(index).serverId; +} + +int ServersUiController::getServerIndexById(const QString &serverId) const +{ + return rowForServerId(m_orderedServerDescriptions, serverId); +} + void ServersUiController::updateContainersModel() { - if (m_processedServerIndex < 0 || m_processedServerIndex >= m_serversController->getServersCount()) { + if (m_processedServerId.isEmpty()) { return; } - ServerConfig server = m_serversController->getServerConfig(m_processedServerIndex); - QMap containers = server.containers(); + const QMap containers = + m_serversController->getServerContainersMap(m_processedServerId); m_containersModel->updateModel(containers); } void ServersUiController::updateDefaultServerContainersModel() { - int defaultIndex = m_serversController->getDefaultServerIndex(); - if (defaultIndex < 0 || defaultIndex >= m_serversController->getServersCount()) { + const QString defaultServerId = m_serversController->getDefaultServerId(); + if (defaultServerId.isEmpty()) { return; } - ServerConfig server = m_serversController->getServerConfig(defaultIndex); - QMap containers = server.containers(); + const QMap containers = + m_serversController->getServerContainersMap(defaultServerId); m_defaultServerContainersModel->updateModel(containers); } QStringList ServersUiController::getAllInstalledServicesName(int serverIndex) const { QStringList servicesName; - ServerConfig server = m_serversController->getServerConfig(serverIndex); - QMap containers = server.containers(); - + const QString serverId = getServerId(serverIndex); + const QMap containers = m_serversController->getServerContainersMap(serverId); + for (auto it = containers.begin(); it != containers.end(); ++it) { DockerContainer container = it.key(); if (ContainerUtils::containerService(container) == ServiceType::Other) { @@ -489,3 +512,13 @@ QStringList ServersUiController::getAllInstalledServicesName(int serverIndex) co return servicesName; } +int ServersUiController::serverIndexForId(const QString &serverId) const +{ + return rowForServerId(m_orderedServerDescriptions, serverId); +} + +bool ServersUiController::listHasServersFromGatewayApi() const +{ + return descriptionsHaveGatewayServers(m_orderedServerDescriptions); +} + diff --git a/client/ui/controllers/serversUiController.h b/client/ui/controllers/serversUiController.h index 7d16362af..6342faf22 100644 --- a/client/ui/controllers/serversUiController.h +++ b/client/ui/controllers/serversUiController.h @@ -6,35 +6,39 @@ #include #include #include +#include #include "core/controllers/serversController.h" +#include "core/models/serverDescription.h" #include "core/controllers/settingsController.h" #include "ui/models/serversModel.h" #include "ui/models/containersModel.h" -#include "core/models/serverConfig.h" class ServersUiController : public QObject { Q_OBJECT - Q_PROPERTY(int defaultIndex READ getDefaultServerIndex NOTIFY defaultServerIndexChanged) - Q_PROPERTY(QString defaultServerName READ getDefaultServerName NOTIFY defaultServerIndexChanged) - Q_PROPERTY(QString defaultServerDefaultContainerName READ getDefaultServerDefaultContainerName NOTIFY defaultServerIndexChanged) - Q_PROPERTY(QString defaultServerDescriptionCollapsed READ getDefaultServerDescriptionCollapsed NOTIFY defaultServerIndexChanged) - Q_PROPERTY(QString defaultServerImagePathCollapsed READ getDefaultServerImagePathCollapsed NOTIFY defaultServerIndexChanged) - Q_PROPERTY(QString defaultServerDescriptionExpanded READ getDefaultServerDescriptionExpanded NOTIFY defaultServerIndexChanged) - Q_PROPERTY(bool isDefaultServerDefaultContainerHasSplitTunneling READ isDefaultServerDefaultContainerHasSplitTunneling NOTIFY defaultServerIndexChanged) - Q_PROPERTY(bool isDefaultServerFromApi READ isDefaultServerFromApi NOTIFY defaultServerIndexChanged) + Q_PROPERTY(QString defaultServerId READ getDefaultServerId NOTIFY defaultServerIdChanged) + Q_PROPERTY(int defaultServerIndex READ defaultServerIndex NOTIFY defaultServerIndexChanged) + + Q_PROPERTY(QString defaultServerName READ getDefaultServerName NOTIFY defaultServerIdChanged) + Q_PROPERTY(QString defaultServerDefaultContainerName READ getDefaultServerDefaultContainerName NOTIFY defaultServerIdChanged) + Q_PROPERTY(QString defaultServerDescriptionCollapsed READ getDefaultServerDescriptionCollapsed NOTIFY defaultServerIdChanged) + Q_PROPERTY(QString defaultServerImagePathCollapsed READ getDefaultServerImagePathCollapsed NOTIFY defaultServerIdChanged) + Q_PROPERTY(QString defaultServerDescriptionExpanded READ getDefaultServerDescriptionExpanded NOTIFY defaultServerIdChanged) + Q_PROPERTY(bool isDefaultServerDefaultContainerHasSplitTunneling READ isDefaultServerDefaultContainerHasSplitTunneling NOTIFY defaultServerIdChanged) + Q_PROPERTY(bool isDefaultServerFromApi READ isDefaultServerFromApi NOTIFY defaultServerIdChanged) - Q_PROPERTY(int processedIndex READ getProcessedServerIndex WRITE setProcessedServerIndex NOTIFY processedServerIndexChanged) + Q_PROPERTY(QString processedServerId READ getProcessedServerId WRITE setProcessedServerId NOTIFY processedServerIdChanged) + Q_PROPERTY(int processedServerIndex READ getProcessedServerIndex WRITE setProcessedServerIndex NOTIFY processedServerIndexChanged) Q_PROPERTY(int processedContainerIndex READ getProcessedContainerIndex WRITE setProcessedContainerIndex NOTIFY processedContainerIndexChanged) Q_PROPERTY(bool processedServerIsPremium READ processedServerIsPremium NOTIFY processedServerIndexChanged) Q_PROPERTY(bool hasServersFromGatewayApi READ hasServersFromGatewayApi NOTIFY hasServersFromGatewayApiChanged) - Q_PROPERTY(bool isAdVisible READ isAdVisible NOTIFY defaultServerIndexChanged) - Q_PROPERTY(QString adHeader READ adHeader NOTIFY defaultServerIndexChanged) - Q_PROPERTY(QString adDescription READ adDescription NOTIFY defaultServerIndexChanged) + Q_PROPERTY(bool isAdVisible READ isAdVisible NOTIFY defaultServerIdChanged) + Q_PROPERTY(QString adHeader READ adHeader NOTIFY defaultServerIdChanged) + Q_PROPERTY(QString adDescription READ adDescription NOTIFY defaultServerIdChanged) public: explicit ServersUiController(ServersController* serversController, @@ -45,15 +49,22 @@ public: QObject *parent = nullptr); public slots: - void removeServer(int index); - void editServerName(int index, const QString &name); - void setDefaultServerIndex(int index); - void setDefaultContainer(int serverIndex, int containerIndex); + void removeServer(const QString &serverId); + void removeServerAtIndex(int index); + + void editServerName(const QString &serverId, const QString &name); + + void setDefaultServer(const QString &serverId); + void setDefaultServerAtIndex(int index); + + void setDefaultContainer(const QString &serverId, int containerIndex); + void setDefaultContainerAtIndex(int index, int containerIndex); + void toggleAmneziaDns(bool enabled); - void onDefaultServerChanged(int index); + void onDefaultServerChanged(const QString &defaultServerId); // Getters for properties - int getDefaultServerIndex() const; + QString getDefaultServerId() const; QString getDefaultServerName() const; QString getDefaultServerDefaultContainerName() const; QString getDefaultServerDescriptionCollapsed() const; @@ -62,8 +73,14 @@ public slots: bool isDefaultServerDefaultContainerHasSplitTunneling() const; bool isDefaultServerFromApi() const; + QString getProcessedServerId() const; + void setProcessedServerId(const QString &serverId); + int getProcessedServerIndex() const; void setProcessedServerIndex(int index); + + int defaultServerIndex() const; + int getProcessedContainerIndex() const; void setProcessedContainerIndex(int index); bool processedServerIsPremium() const; @@ -78,12 +95,16 @@ public slots: QString adHeader() const; QString adDescription() const; + QString getServerId(int index) const; + int getServerIndexById(const QString &serverId) const; QStringList getAllInstalledServicesName(int serverIndex) const; signals: void errorOccurred(const QString &errorMessage); void finished(const QString &message); + void defaultServerIdChanged(const QString &serverId); void defaultServerIndexChanged(int index); + void processedServerIdChanged(const QString &serverId); void processedServerIndexChanged(int index); void processedContainerIndexChanged(int index); void hasServersFromGatewayApiChanged(); @@ -94,20 +115,23 @@ public: void updateModel(); private: - QString getDefaultServerDescription(const ServerConfig& server, int index) const; - bool isAmneziaDnsContainerInstalled(int serverIndex) const; + QString getDefaultServerDescription(const QString &serverId) const; + int serverIndexForId(const QString &serverId) const; + bool listHasServersFromGatewayApi() const; void updateContainersModel(); void updateDefaultServerContainersModel(); - void updateApiModelsForProcessedServer(); - + ServersController* m_serversController; SettingsController* m_settingsController; ServersModel* m_serversModel; ContainersModel* m_containersModel; ContainersModel* m_defaultServerContainersModel; + + QVector m_orderedServerDescriptions; int m_processedServerIndex = -1; + QString m_processedServerId; int m_processedContainerIndex = -1; }; diff --git a/client/ui/models/api/apiAccountInfoModel.cpp b/client/ui/models/api/apiAccountInfoModel.cpp index b9380ed51..85b1226bb 100644 --- a/client/ui/models/api/apiAccountInfoModel.cpp +++ b/client/ui/models/api/apiAccountInfoModel.cpp @@ -4,6 +4,7 @@ #include #include "core/utils/api/apiUtils.h" +#include "core/utils/serverConfigUtils.h" #include "logger.h" namespace @@ -28,7 +29,7 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const switch (role) { case SubscriptionStatusRole: { - if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) { + if (m_accountInfoData.configType == serverConfigUtils::ConfigType::AmneziaFreeV3) { return tr("Active"); } @@ -37,14 +38,14 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const : QStringLiteral("

%1").arg(tr("Active")); } case EndDateRole: { - if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) { + if (m_accountInfoData.configType == serverConfigUtils::ConfigType::AmneziaFreeV3) { return ""; } return QDateTime::fromString(m_accountInfoData.subscriptionEndDate, Qt::ISODate).toLocalTime().toString("d MMM yyyy"); } case ConnectedDevicesRole: { - if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) { + if (m_accountInfoData.configType == serverConfigUtils::ConfigType::AmneziaFreeV3) { return ""; } return tr("%1 out of %2").arg(m_accountInfoData.activeDeviceCount).arg(m_accountInfoData.maxDeviceCount); @@ -53,9 +54,8 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const return m_accountInfoData.subscriptionDescription; } case IsComponentVisibleRole: { - return m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2 - || m_accountInfoData.configType == apiDefs::ConfigType::ExternalPremium - || m_accountInfoData.configType == apiDefs::ConfigType::ExternalTrial; + return m_accountInfoData.configType == serverConfigUtils::ConfigType::AmneziaPremiumV2 + || m_accountInfoData.configType == serverConfigUtils::ConfigType::ExternalPremium; } case IsSubscriptionRenewalAvailableRole: { return m_accountInfoData.isRenewalAvailable; @@ -80,7 +80,7 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const return false; } case IsSubscriptionExpiredRole: { - if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) { + if (m_accountInfoData.configType == serverConfigUtils::ConfigType::AmneziaFreeV3) { return false; } if (m_accountInfoData.isInAppPurchase) { @@ -92,7 +92,7 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const return apiUtils::isSubscriptionExpired(m_accountInfoData.subscriptionEndDate); } case IsSubscriptionExpiringSoonRole: { - if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) { + if (m_accountInfoData.configType == serverConfigUtils::ConfigType::AmneziaFreeV3) { return false; } if (m_accountInfoData.isInAppPurchase) { @@ -124,7 +124,7 @@ void ApiAccountInfoModel::updateModel(const QJsonObject &accountInfoObject, cons accountInfoData.maxDeviceCount = accountInfoObject.value(apiDefs::key::maxDeviceCount).toInt(); accountInfoData.subscriptionEndDate = accountInfoObject.value(apiDefs::key::subscriptionEndDate).toString(); - accountInfoData.configType = apiUtils::getConfigType(serverConfig); + accountInfoData.configType = serverConfigUtils::configTypeFromJson(serverConfig); const QJsonObject apiConfig = serverConfig.value(apiDefs::key::apiConfig).toObject(); accountInfoData.isInAppPurchase = apiConfig.value(apiDefs::key::isInAppPurchase).toBool(false); diff --git a/client/ui/models/api/apiAccountInfoModel.h b/client/ui/models/api/apiAccountInfoModel.h index 379575dcb..b0c559a4e 100644 --- a/client/ui/models/api/apiAccountInfoModel.h +++ b/client/ui/models/api/apiAccountInfoModel.h @@ -5,7 +5,7 @@ #include #include -#include "core/utils/api/apiEnums.h" +#include "core/utils/serverConfigUtils.h" #include "core/utils/constants/apiKeys.h" #include "core/utils/constants/apiConstants.h" @@ -56,7 +56,7 @@ private: int activeDeviceCount; int maxDeviceCount; - apiDefs::ConfigType configType; + serverConfigUtils::ConfigType configType; QStringList supportedProtocols; diff --git a/client/ui/models/api/apiCountryModel.cpp b/client/ui/models/api/apiCountryModel.cpp index 4aec43366..b0315f346 100644 --- a/client/ui/models/api/apiCountryModel.cpp +++ b/client/ui/models/api/apiCountryModel.cpp @@ -2,7 +2,7 @@ #include -#include "core/utils/api/apiEnums.h" +#include "core/utils/serverConfigUtils.h" #include "core/utils/constants/apiKeys.h" #include "core/utils/constants/apiConstants.h" #include "logger.h" diff --git a/client/ui/models/api/apiDevicesModel.cpp b/client/ui/models/api/apiDevicesModel.cpp index 9d69c7b0b..8e205ccf2 100644 --- a/client/ui/models/api/apiDevicesModel.cpp +++ b/client/ui/models/api/apiDevicesModel.cpp @@ -2,7 +2,7 @@ #include -#include "core/utils/api/apiEnums.h" +#include "core/utils/serverConfigUtils.h" #include "core/utils/constants/apiKeys.h" #include "core/utils/constants/apiConstants.h" #include "logger.h" diff --git a/client/ui/models/serversModel.cpp b/client/ui/models/serversModel.cpp index 131d241f8..018146f99 100644 --- a/client/ui/models/serversModel.cpp +++ b/client/ui/models/serversModel.cpp @@ -1,11 +1,12 @@ #include "serversModel.h" +#include "core/models/serverDescription.h" + #include #include #include -#include "core/models/serverConfig.h" -#include "core/utils/api/apiEnums.h" +#include "core/utils/serverConfigUtils.h" #include "core/utils/constants/apiKeys.h" #include "core/utils/constants/apiConstants.h" #include "core/utils/selfhosted/sshSession.h" @@ -19,42 +20,15 @@ using namespace amnezia; -namespace -{ - namespace configKey - { - constexpr char apiConfig[] = "api_config"; - constexpr char serviceInfo[] = "service_info"; - constexpr char availableCountries[] = "available_countries"; - constexpr char serverCountryCode[] = "server_country_code"; - constexpr char serverCountryName[] = "server_country_name"; - constexpr char userCountryCode[] = "user_country_code"; - constexpr char serviceType[] = "service_type"; - constexpr char serviceProtocol[] = "service_protocol"; - - constexpr char publicKeyInfo[] = "public_key"; - constexpr char expiresAt[] = "expires_at"; - } - - QString normalizeVpnKey(const QString &vpnKey) - { - QString normalized = vpnKey.trimmed(); - if (normalized.startsWith(QStringLiteral("vpn://"), Qt::CaseInsensitive)) { - normalized = normalized.mid(QStringLiteral("vpn://").size()); - } - return normalized; - } -} - ServersModel::ServersModel(QObject *parent) : QAbstractListModel(parent) { connect(this, &ServersModel::defaultServerIndexChanged, this, &ServersModel::defaultServerNameChanged); connect(this, &ServersModel::defaultServerIndexChanged, this, [this](const int serverIndex) { - if (serverIndex < 0 || serverIndex >= m_servers.size()) { + if (serverIndex < 0 || serverIndex >= m_descriptions.size()) { return; } - auto defaultContainer = m_servers.at(serverIndex).defaultContainer(); + auto defaultContainer = m_descriptions.at(serverIndex).defaultContainer; emit ServersModel::defaultServerDefaultContainerChanged(defaultContainer); emit ServersModel::defaultServerNameChanged(); }); @@ -65,148 +39,73 @@ ServersModel::ServersModel(QObject *parent) : QAbstractListModel(parent) int ServersModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); - return static_cast(m_servers.size()); + return static_cast(m_descriptions.size()); } QVariant ServersModel::data(const QModelIndex &index, int role) const { - if (!index.isValid() || index.row() < 0 || index.row() >= static_cast(m_servers.size())) { + if (!index.isValid() || index.row() < 0 || index.row() >= static_cast(m_descriptions.size())) { return QVariant(); } - const ServerConfig &server = m_servers.at(index.row()); - const int configVersion = server.configVersion(); - + const ServerDescription &row = m_descriptions.at(index.row()); + const int configVersion = row.configVersion; + switch (role) { - case NameRole: { - if (configVersion) { - if (server.isApiV1()) { - return server.as()->name; - } else if (server.isApiV2()) { - return server.as()->name; - } - } - QString name = server.description(); - if (name.isEmpty()) { - return server.hostName(); - } - return name; - } - case ServerDescriptionRole: { - auto description = getServerDescription(server, index.row()); - return configVersion ? description : description + server.hostName(); - } - case HostNameRole: return server.hostName(); - case CredentialsRole: return QVariant::fromValue(serverCredentials(index.row())); - case CredentialsLoginRole: return serverCredentials(index.row()).userName; - case IsDefaultRole: return index.row() == m_defaultServerIndex; - case IsCurrentlyProcessedRole: return index.row() == m_processedServerIndex; - case HasWriteAccessRole: { - auto credentials = serverCredentials(index.row()); - return (!credentials.userName.isEmpty() && !credentials.secretData.isEmpty()); - } - case ContainsAmneziaDnsRole: { - QString primaryDns = server.dns1(); - return primaryDns == protocols::dns::amneziaDnsIp; - } - case DefaultContainerRole: { - return server.defaultContainer(); - } - case HasInstalledContainers: { - return serverHasInstalledContainers(index.row()); - } - case IsServerFromTelegramApiRole: { - return configVersion == apiDefs::ConfigSource::Telegram; - } - case IsServerFromGatewayApiRole: { - return configVersion == apiDefs::ConfigSource::AmneziaGateway; - } - case ApiConfigRole: { + case NameRole: + return row.serverName; + case ServerDescriptionRole: + return configVersion ? row.baseDescription : (row.baseDescription + row.hostName); + case CollapsedServerDescriptionRole: + return row.collapsedServerDescription; + case ExpandedServerDescriptionRole: + return row.expandedServerDescription; + case HostNameRole: + return row.hostName; + case CredentialsRole: + return QVariant::fromValue(serverCredentials(index.row())); + case CredentialsLoginRole: + return serverCredentials(index.row()).userName; + case IsDefaultRole: + return index.row() == m_defaultServerIndex; + case IsCurrentlyProcessedRole: + return index.row() == m_processedServerIndex; + case HasWriteAccessRole: + return row.hasWriteAccess; + case ContainsAmneziaDnsRole: + return row.primaryDnsIsAmnezia; + case DefaultContainerRole: + return QVariant::fromValue(row.defaultContainer); + case HasInstalledContainers: + return row.hasInstalledVpnContainers; + case IsServerFromTelegramApiRole: + return false; + case IsServerFromGatewayApiRole: + return row.isServerFromGatewayApi; + case ApiConfigRole: return QVariant(); - } - case IsCountrySelectionAvailableRole: { - if (server.isApiV2()) { - return !server.as()->apiConfig.availableCountries.isEmpty(); - } - return false; - } - case ApiAvailableCountriesRole: { - if (server.isApiV2()) { - return server.as()->apiConfig.availableCountries; - } - return QJsonArray(); - } - case ApiServerCountryCodeRole: { - if (server.isApiV2()) { - return server.as()->apiConfig.serverCountryCode; - } - return QString(); - } - case HasAmneziaDns: { - QString primaryDns = server.dns1(); - return primaryDns == protocols::dns::amneziaDnsIp; - } - case IsAdVisibleRole: { - if (server.isApiV2()) { - return server.as()->apiConfig.serviceInfo.isAdVisible; - } - return false; - } - case AdHeaderRole: { - if (server.isApiV2()) { - return server.as()->apiConfig.serviceInfo.adHeader; - } - return QString(); - } - case AdDescriptionRole: { - if (server.isApiV2()) { - return server.as()->apiConfig.serviceInfo.adDescription; - } - return QString(); - } - case AdEndpointRole: { - if (server.isApiV2()) { - return server.as()->apiConfig.serviceInfo.adEndpoint; - } - return QString(); - } - case IsRenewalAvailableRole: { - if (server.isApiV2()) { - return server.as()->apiConfig.serviceInfo.isRenewalAvailable; - } - return false; - } - case IsSubscriptionExpiredRole: { - if (!server.isApiV2()) { - return false; - } - - const ApiConfig &apiConfig = server.as()->apiConfig; - if (apiConfig.isInAppPurchase) { - return false; - } - if (apiConfig.subscriptionExpiredByServer) { - return true; - } - if (apiConfig.subscription.endDate.isEmpty()) { - return false; - } - return apiUtils::isSubscriptionExpired(apiConfig.subscription.endDate); - } - case IsSubscriptionExpiringSoonRole: { - if (!server.isApiV2()) { - return false; - } - - const ApiConfig &apiConfig = server.as()->apiConfig; - if (apiConfig.isInAppPurchase) { - return false; - } - if (apiConfig.subscription.endDate.isEmpty()) { - return false; - } - return apiUtils::isSubscriptionExpiringSoon(apiConfig.subscription.endDate); - } + case IsCountrySelectionAvailableRole: + return row.isCountrySelectionAvailable; + case ApiAvailableCountriesRole: + return row.apiAvailableCountries; + case ApiServerCountryCodeRole: + return row.apiServerCountryCode; + case HasAmneziaDns: + return row.primaryDnsIsAmnezia; + case IsAdVisibleRole: + return row.isAdVisible; + case AdHeaderRole: + return row.adHeader; + case AdDescriptionRole: + return row.adDescription; + case AdEndpointRole: + return row.adEndpoint; + case IsRenewalAvailableRole: + return row.isRenewalAvailable; + case IsSubscriptionExpiredRole: + return row.isSubscriptionExpired; + case IsSubscriptionExpiringSoonRole: + return row.isSubscriptionExpiringSoon; } return QVariant(); @@ -218,12 +117,11 @@ QVariant ServersModel::data(const int index, int role) const return data(modelIndex, role); } -void ServersModel::updateModel(const QVector &servers, int defaultServerIndex, bool isAmneziaDnsEnabled) +void ServersModel::updateModel(const QVector &descriptions, int defaultServerIndex) { beginResetModel(); - m_servers = servers; + m_descriptions = descriptions; m_defaultServerIndex = defaultServerIndex; - m_isAmneziaDnsEnabled = isAmneziaDnsEnabled; endResetModel(); emit defaultServerIndexChanged(m_defaultServerIndex); emit processedServerChanged(); @@ -234,43 +132,15 @@ const int ServersModel::getDefaultServerIndex() return m_defaultServerIndex; } -QString ServersModel::getServerDescription(const ServerConfig &server, const int index) const -{ - const int configVersion = server.configVersion(); - QString description; - - if (server.isApiV2()) { - const ApiV2ServerConfig *apiV2 = server.as(); - if (apiV2 && !apiV2->apiConfig.serverCountryCode.isEmpty()) { - return apiV2->apiConfig.serverCountryName; - } - return apiV2 ? apiV2->description : server.description(); - } else if (server.isApiV1()) { - const ApiV1ServerConfig *apiV1 = server.as(); - return apiV1 ? apiV1->description : server.description(); - } else if (data(index, HasWriteAccessRole).toBool()) { - QMap containers = server.containers(); - bool isDnsInstalled = containers.contains(DockerContainer::Dns); - if (m_isAmneziaDnsEnabled && isDnsInstalled) { - description += "Amnezia DNS | "; - } - } else { - if (data(index, HasAmneziaDns).toBool()) { - description += "Amnezia DNS | "; - } - } - return description; -} - const int ServersModel::getServersCount() { - return m_servers.size(); + return m_descriptions.size(); } bool ServersModel::hasServerWithWriteAccess() { for (size_t i = 0; i < getServersCount(); i++) { - if (qvariant_cast(data(i, HasWriteAccessRole))) { + if (qvariant_cast(data(static_cast(i), HasWriteAccessRole))) { return true; } } @@ -350,29 +220,17 @@ QHash ServersModel::roleNames() const roles[IsSubscriptionExpiredRole] = "isSubscriptionExpired"; roles[IsSubscriptionExpiringSoonRole] = "isSubscriptionExpiringSoon"; + roles[HasAmneziaDns] = "hasAmneziaDns"; + return roles; } ServerCredentials ServersModel::serverCredentials(int index) const { - if (index < 0 || index >= m_servers.size()) { + if (index < 0 || index >= m_descriptions.size()) { return ServerCredentials(); } - const ServerConfig &server = m_servers.at(index); - - if (server.isSelfHosted()) { - const SelfHostedServerConfig *selfHosted = server.as(); - if (selfHosted) { - ServerCredentials credentials; - credentials.hostName = selfHosted->hostName; - credentials.userName = selfHosted->userName.value_or(""); - credentials.secretData = selfHosted->password.value_or(""); - credentials.port = selfHosted->port.value_or(22); - return credentials; - } - } - - return ServerCredentials(); + return m_descriptions.at(index).selfHostedSshCredentials; } bool ServersModel::isServerFromApi(const int serverIndex) @@ -405,21 +263,10 @@ QVariant ServersModel::getProcessedServerData(const QString &roleString) return {}; } - bool ServersModel::serverHasInstalledContainers(const int serverIndex) const { - const ServerConfig &server = m_servers.at(serverIndex); - QMap containers = server.containers(); - - for (auto it = containers.begin(); it != containers.end(); ++it) { - DockerContainer container = it.key(); - if (ContainerUtils::containerService(container) == ServiceType::Vpn) { - return true; - } - if (container == DockerContainer::SSXray) { - return true; - } + if (serverIndex < 0 || serverIndex >= m_descriptions.size()) { + return false; } - return false; + return m_descriptions.at(serverIndex).hasInstalledVpnContainers; } - diff --git a/client/ui/models/serversModel.h b/client/ui/models/serversModel.h index 9bff2eadb..90637e18b 100644 --- a/client/ui/models/serversModel.h +++ b/client/ui/models/serversModel.h @@ -5,7 +5,7 @@ #include #include "core/utils/selfhosted/sshSession.h" -#include "core/models/serverConfig.h" +#include "core/models/serverDescription.h" class ServersModel : public QAbstractListModel { @@ -75,14 +75,13 @@ public slots: bool isServerFromApi(const int serverIndex); - void updateModel(const QVector &servers, int defaultServerIndex, bool isAmneziaDnsEnabled = false); - + void updateModel(const QVector &descriptions, int defaultServerIndex); + protected: QHash roleNames() const override; signals: void processedServerIndexChanged(const int index); - // emitted when the processed server index or processed server data is changed void processedServerChanged(); void defaultServerIndexChanged(const int index); @@ -97,16 +96,12 @@ signals: private: ServerCredentials serverCredentials(int index) const; - QString getServerDescription(const ServerConfig &server, const int index) const; - bool serverHasInstalledContainers(const int serverIndex) const; - QVector m_servers; + QVector m_descriptions; - int m_defaultServerIndex; - int m_processedServerIndex; - - bool m_isAmneziaDnsEnabled = false; + int m_defaultServerIndex = -1; + int m_processedServerIndex = -1; }; #endif // SERVERSMODEL_H diff --git a/client/ui/qml/Components/ConnectButton.qml b/client/ui/qml/Components/ConnectButton.qml index 153aef122..2f17f4b60 100644 --- a/client/ui/qml/Components/ConnectButton.qml +++ b/client/ui/qml/Components/ConnectButton.qml @@ -182,7 +182,7 @@ Button { } onClicked: { - ServersUiController.setProcessedServerIndex(ServersUiController.defaultIndex) + ServersUiController.setProcessedServerIndex(ServersUiController.defaultServerIndex) ConnectionController.connectButtonClicked() } diff --git a/client/ui/qml/Components/GamepadLoader.qml b/client/ui/qml/Components/GamepadLoader.qml index d7853582c..5dc39a96c 100644 --- a/client/ui/qml/Components/GamepadLoader.qml +++ b/client/ui/qml/Components/GamepadLoader.qml @@ -13,7 +13,7 @@ Item { onButtonStartChanged: { if (buttonStart) { - ServersUiController.setProcessedServerIndex(ServersUiController.defaultIndex) + ServersUiController.setProcessedServerIndex(ServersUiController.defaultServerIndex) ConnectionController.connectButtonClicked() } } diff --git a/client/ui/qml/Components/HomeContainersListView.qml b/client/ui/qml/Components/HomeContainersListView.qml index bd24f9f3b..c1e8ac489 100644 --- a/client/ui/qml/Components/HomeContainersListView.qml +++ b/client/ui/qml/Components/HomeContainersListView.qml @@ -58,7 +58,7 @@ ListViewType { if (checked) { containersDropDown.closeTriggered() - ServersUiController.setDefaultContainer(ServersUiController.defaultIndex, proxyDefaultServerContainersModel.mapToSource(index)) + ServersUiController.setDefaultContainer(ServersUiController.getServerId(ServersUiController.defaultServerIndex), proxyDefaultServerContainersModel.mapToSource(index)) } else { ServersUiController.processedContainerIndex = proxyDefaultServerContainersModel.mapToSource(index) PageController.goToPage(PageEnum.PageSetupWizardProtocolSettings) diff --git a/client/ui/qml/Components/RenameServerDrawer.qml b/client/ui/qml/Components/RenameServerDrawer.qml index a067835e5..328c8f537 100644 --- a/client/ui/qml/Components/RenameServerDrawer.qml +++ b/client/ui/qml/Components/RenameServerDrawer.qml @@ -46,7 +46,7 @@ DrawerType2 { } if (serverName.textField.text !== root.serverNameText) { - ServersUiController.editServerName(ServersUiController.processedIndex, serverName.textField.text); + ServersUiController.editServerName(ServersUiController.getServerId(ServersUiController.processedServerIndex), serverName.textField.text); } root.closeTriggered() } diff --git a/client/ui/qml/Components/ServersListView.qml b/client/ui/qml/Components/ServersListView.qml index b96dfb1d2..6e92a1fd1 100644 --- a/client/ui/qml/Components/ServersListView.qml +++ b/client/ui/qml/Components/ServersListView.qml @@ -17,7 +17,7 @@ import "../Config" ListViewType { id: root - property int selectedIndex: ServersUiController.defaultIndex + property int selectedIndex: ServersUiController.defaultServerIndex anchors.top: serversMenuHeader.bottom anchors.right: parent.right @@ -29,8 +29,8 @@ ListViewType { Connections { target: ServersUiController - function onDefaultServerIndexChanged(serverIndex) { - root.selectedIndex = serverIndex + function onDefaultServerIndexChanged() { + root.selectedIndex = ServersUiController.defaultServerIndex } } @@ -86,7 +86,7 @@ ListViewType { root.selectedIndex = index - ServersUiController.setDefaultServerIndex(index) + ServersUiController.setDefaultServerAtIndex(index) } Keys.onEnterPressed: serverRadioButton.clicked() @@ -106,14 +106,14 @@ ListViewType { z: 1 onClicked: function() { - ServersUiController.processedIndex = index + ServersUiController.processedServerIndex = index if (ServersModel.getProcessedServerData("isServerFromGatewayApi")) { if (ServersModel.getProcessedServerData("isCountrySelectionAvailable")) { PageController.goToPage(PageEnum.PageSettingsApiAvailableCountries) } else { PageController.showBusyIndicator(true) - let result = SubscriptionUiController.getAccountInfo(ServersUiController.getProcessedServerIndex(), false) + let result = SubscriptionUiController.getAccountInfo(ServersUiController.getServerId(ServersUiController.processedServerIndex), false) PageController.showBusyIndicator(false) if (!result) { return diff --git a/client/ui/qml/Components/SettingsContainersListView.qml b/client/ui/qml/Components/SettingsContainersListView.qml index c5dd130e1..209c56438 100644 --- a/client/ui/qml/Components/SettingsContainersListView.qml +++ b/client/ui/qml/Components/SettingsContainersListView.qml @@ -34,19 +34,19 @@ ListViewType { if (isVpnContainer) { // var isThirdPartyConfig = root.model.data(index, ContainersModel.IsThirdPartyConfigRole) if (isThirdPartyConfig) { - InstallController.updateProtocols(ServersUiController.processedIndex, containerIndex) + InstallController.updateProtocols(ServersUiController.getServerId(ServersUiController.processedServerIndex), containerIndex) PageController.goToPage(PageEnum.PageProtocolRaw) return } } if (isIpsec) { - InstallController.updateProtocols(ServersUiController.processedIndex, containerIndex) + InstallController.updateProtocols(ServersUiController.getServerId(ServersUiController.processedServerIndex), containerIndex) PageController.goToPage(PageEnum.PageProtocolRaw) } else if (isDns) { PageController.goToPage(PageEnum.PageServiceDnsSettings) } else { - InstallController.updateProtocols(ServersUiController.processedIndex, containerIndex) + InstallController.updateProtocols(ServersUiController.getServerId(ServersUiController.processedServerIndex), containerIndex) PageController.goToPage(PageEnum.PageSettingsServerProtocol) } diff --git a/client/ui/qml/Components/SubscriptionExpiredDrawer.qml b/client/ui/qml/Components/SubscriptionExpiredDrawer.qml index 24fe66087..b1b27f16b 100644 --- a/client/ui/qml/Components/SubscriptionExpiredDrawer.qml +++ b/client/ui/qml/Components/SubscriptionExpiredDrawer.qml @@ -76,7 +76,7 @@ DrawerType2 { textColor: AmneziaStyle.color.midnightBlack clickedFunc: function() { - SubscriptionUiController.getRenewalLink(ServersUiController.defaultIndex) + SubscriptionUiController.getRenewalLink(ServersUiController.getServerId(ServersUiController.defaultServerIndex)) } } @@ -96,7 +96,7 @@ DrawerType2 { clickedFunc: function() { PageController.showBusyIndicator(true) - let result = SubscriptionUiController.getAccountInfo(ServersUiController.defaultIndex, false) + let result = SubscriptionUiController.getAccountInfo(ServersUiController.getServerId(ServersUiController.defaultServerIndex), false) PageController.showBusyIndicator(false) if (result) { root.closeTriggered() diff --git a/client/ui/qml/Pages2/PageHome.qml b/client/ui/qml/Pages2/PageHome.qml index 25794211c..12b811652 100644 --- a/client/ui/qml/Pages2/PageHome.qml +++ b/client/ui/qml/Pages2/PageHome.qml @@ -344,14 +344,14 @@ PageType { Keys.onReturnPressed: this.clicked() onClicked: { - ServersUiController.processedIndex = ServersUiController.defaultIndex + ServersUiController.setProcessedServerIndex(ServersUiController.defaultServerIndex) if (ServersModel.getProcessedServerData("isServerFromGatewayApi")) { if (ServersModel.getProcessedServerData("isCountrySelectionAvailable")) { PageController.goToPage(PageEnum.PageSettingsApiAvailableCountries) } else { PageController.showBusyIndicator(true) - let result = SubscriptionUiController.getAccountInfo(ServersUiController.getProcessedServerIndex(), false) + let result = SubscriptionUiController.getAccountInfo(ServersUiController.getServerId(ServersUiController.processedServerIndex), false) PageController.showBusyIndicator(false) if (!result) { return diff --git a/client/ui/qml/Pages2/PageProtocolAwgClientSettings.qml b/client/ui/qml/Pages2/PageProtocolAwgClientSettings.qml index e5ae84aae..12b08fc20 100644 --- a/client/ui/qml/Pages2/PageProtocolAwgClientSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolAwgClientSettings.qml @@ -441,7 +441,7 @@ PageType { } PageController.goToPage(PageEnum.PageSetupWizardInstalling); - InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Awg) + InstallController.updateContainer(ServersUiController.getServerId(ServersUiController.processedServerIndex), ServersUiController.processedContainerIndex, ProtocolEnum.Awg) } var noButtonFunction = function() {} diff --git a/client/ui/qml/Pages2/PageProtocolAwgSettings.qml b/client/ui/qml/Pages2/PageProtocolAwgSettings.qml index c8c8ab0ed..74f998ae0 100644 --- a/client/ui/qml/Pages2/PageProtocolAwgSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolAwgSettings.qml @@ -561,7 +561,7 @@ PageType { } PageController.goToPage(PageEnum.PageSetupWizardInstalling); - InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Awg) + InstallController.updateContainer(ServersUiController.getServerId(ServersUiController.processedServerIndex), ServersUiController.processedContainerIndex, ProtocolEnum.Awg) } var noButtonFunction = function() {} diff --git a/client/ui/qml/Pages2/PageProtocolOpenVpnSettings.qml b/client/ui/qml/Pages2/PageProtocolOpenVpnSettings.qml index 1e00ac8fb..1e216e2fd 100644 --- a/client/ui/qml/Pages2/PageProtocolOpenVpnSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolOpenVpnSettings.qml @@ -434,7 +434,7 @@ PageType { } PageController.goToPage(PageEnum.PageSetupWizardInstalling); - InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.OpenVpn) + InstallController.updateContainer(ServersUiController.getServerId(ServersUiController.processedServerIndex), ServersUiController.processedContainerIndex, ProtocolEnum.OpenVpn) } var noButtonFunction = function() { if (!GC.isMobile()) { diff --git a/client/ui/qml/Pages2/PageProtocolRaw.qml b/client/ui/qml/Pages2/PageProtocolRaw.qml index 57dc7d9fc..881c20bac 100644 --- a/client/ui/qml/Pages2/PageProtocolRaw.qml +++ b/client/ui/qml/Pages2/PageProtocolRaw.qml @@ -184,7 +184,7 @@ PageType { var yesButtonFunction = function() { PageController.goToPage(PageEnum.PageDeinstalling) - InstallController.removeContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex) + InstallController.removeContainer(ServersUiController.getServerId(ServersUiController.processedServerIndex), ServersUiController.processedContainerIndex) } var noButtonFunction = function() {} diff --git a/client/ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml b/client/ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml index f572d9c5c..730750f33 100644 --- a/client/ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml @@ -129,7 +129,7 @@ PageType { } PageController.goToPage(PageEnum.PageSetupWizardInstalling); - InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.WireGuard) + InstallController.updateContainer(ServersUiController.getServerId(ServersUiController.processedServerIndex), ServersUiController.processedContainerIndex, ProtocolEnum.WireGuard) } var noButtonFunction = function() {} showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) diff --git a/client/ui/qml/Pages2/PageProtocolWireGuardSettings.qml b/client/ui/qml/Pages2/PageProtocolWireGuardSettings.qml index 899def535..72257cb2f 100644 --- a/client/ui/qml/Pages2/PageProtocolWireGuardSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolWireGuardSettings.qml @@ -129,7 +129,7 @@ PageType { } PageController.goToPage(PageEnum.PageSetupWizardInstalling); - InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.WireGuard) + InstallController.updateContainer(ServersUiController.getServerId(ServersUiController.processedServerIndex), ServersUiController.processedContainerIndex, ProtocolEnum.WireGuard) } var noButtonFunction = function() { if (!GC.isMobile()) { diff --git a/client/ui/qml/Pages2/PageProtocolXraySettings.qml b/client/ui/qml/Pages2/PageProtocolXraySettings.qml index 552ec3794..43c57caff 100644 --- a/client/ui/qml/Pages2/PageProtocolXraySettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXraySettings.qml @@ -139,7 +139,7 @@ PageType { } PageController.goToPage(PageEnum.PageSetupWizardInstalling); - InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray) + InstallController.updateContainer(ServersUiController.getServerId(ServersUiController.processedServerIndex), ServersUiController.processedContainerIndex, ProtocolEnum.Xray) } var noButtonFunction = function() { if (!GC.isMobile()) { diff --git a/client/ui/qml/Pages2/PageServiceDnsSettings.qml b/client/ui/qml/Pages2/PageServiceDnsSettings.qml index 6a2f6a07d..5dac3959e 100644 --- a/client/ui/qml/Pages2/PageServiceDnsSettings.qml +++ b/client/ui/qml/Pages2/PageServiceDnsSettings.qml @@ -79,7 +79,7 @@ PageType { PageController.showNotificationMessage(qsTr("Cannot remove AmneziaDNS from running server")) } else { PageController.goToPage(PageEnum.PageDeinstalling) - InstallController.removeContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex) + InstallController.removeContainer(ServersUiController.getServerId(ServersUiController.processedServerIndex), ServersUiController.processedContainerIndex) } } var noButtonFunction = function() {} diff --git a/client/ui/qml/Pages2/PageServiceSftpSettings.qml b/client/ui/qml/Pages2/PageServiceSftpSettings.qml index c47f84672..2f6bbbb30 100644 --- a/client/ui/qml/Pages2/PageServiceSftpSettings.qml +++ b/client/ui/qml/Pages2/PageServiceSftpSettings.qml @@ -173,7 +173,7 @@ PageType { clickedFunc: function() { PageController.showBusyIndicator(true) - InstallController.mountSftpDrive(ServersUiController.processedIndex, port, password, username) + InstallController.mountSftpDrive(ServersUiController.getServerId(ServersUiController.processedServerIndex), port, password, username) PageController.showBusyIndicator(false) } } diff --git a/client/ui/qml/Pages2/PageServiceSocksProxySettings.qml b/client/ui/qml/Pages2/PageServiceSocksProxySettings.qml index b52d0b69e..0e86ddc6a 100644 --- a/client/ui/qml/Pages2/PageServiceSocksProxySettings.qml +++ b/client/ui/qml/Pages2/PageServiceSocksProxySettings.qml @@ -285,7 +285,7 @@ PageType { } PageController.goToPage(PageEnum.PageSetupWizardInstalling) - InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Socks5Proxy) + InstallController.updateContainer(ServersUiController.getServerId(ServersUiController.processedServerIndex), ServersUiController.processedContainerIndex, ProtocolEnum.Socks5Proxy) tempPort = portTextField.textField.text tempUsername = usernameTextField.textField.text tempPassword = passwordTextField.textField.text diff --git a/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml b/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml index e54508096..4e5d62a41 100644 --- a/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml +++ b/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml @@ -108,7 +108,7 @@ PageType { actionButtonFunction: function() { PageController.showBusyIndicator(true) - let result = SubscriptionUiController.getAccountInfo(ServersUiController.getProcessedServerIndex(), false) + let result = SubscriptionUiController.getAccountInfo(ServersUiController.getServerId(ServersUiController.processedServerIndex), false) PageController.showBusyIndicator(false) if (!result) { return @@ -148,7 +148,7 @@ PageType { text: qsTr("Renew subscription") clickedFunc: function() { - SubscriptionUiController.getRenewalLink(ServersUiController.getProcessedServerIndex()) + SubscriptionUiController.getRenewalLink(ServersUiController.getServerId(ServersUiController.processedServerIndex)) } } @@ -200,7 +200,7 @@ PageType { PageController.showBusyIndicator(true) var prevIndex = ApiCountryModel.currentIndex ApiCountryModel.currentIndex = index - if (!SubscriptionUiController.updateServiceFromGateway(ServersUiController.getProcessedServerIndex(), countryCode, countryName)) { + if (!SubscriptionUiController.updateServiceFromGateway(ServersUiController.getServerId(ServersUiController.processedServerIndex), countryCode, countryName)) { ApiCountryModel.currentIndex = prevIndex } PageController.showBusyIndicator(false) diff --git a/client/ui/qml/Pages2/PageSettingsApiDevices.qml b/client/ui/qml/Pages2/PageSettingsApiDevices.qml index 6dd7b59d7..6aecdc634 100644 --- a/client/ui/qml/Pages2/PageSettingsApiDevices.qml +++ b/client/ui/qml/Pages2/PageSettingsApiDevices.qml @@ -82,8 +82,8 @@ PageType { var noButtonText = qsTr("Cancel") var yesButtonFunction = function() { - var serverIndex = ServersUiController.getProcessedServerIndex() - Qt.callLater(deactivateExternalDevice, serverIndex, supportTag, countryCode) + var serverId = ServersUiController.getServerId(ServersUiController.processedServerIndex) + Qt.callLater(deactivateExternalDevice, serverId, supportTag, countryCode) } var noButtonFunction = function() { } @@ -96,10 +96,10 @@ PageType { } } - function deactivateExternalDevice(serverIndex, supportTag, countryCode) { + function deactivateExternalDevice(serverId, supportTag, countryCode) { PageController.showBusyIndicator(true) - if (SubscriptionUiController.deactivateExternalDevice(serverIndex, supportTag, countryCode)) { - SubscriptionUiController.getAccountInfo(serverIndex, true) + if (SubscriptionUiController.deactivateExternalDevice(serverId, supportTag, countryCode)) { + SubscriptionUiController.getAccountInfo(serverId, true) } PageController.showBusyIndicator(false) } diff --git a/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml b/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml index b55e9f46c..4c09122f3 100644 --- a/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml +++ b/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml @@ -1,243 +1,243 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import QtQuick.Dialogs - -import QtCore - -import SortFilterProxyModel 0.2 - -import PageEnum 1.0 -import Style 1.0 - -import "./" -import "../Controls2" -import "../Controls2/TextTypes" -import "../Config" -import "../Components" - -PageType { - id: root - - property string configExtension: ".conf" - property string configCaption: qsTr("Save AmneziaVPN config") - - BackButtonType { - id: backButton - - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.topMargin: 20 + PageController.safeAreaTopMargin - - onActiveFocusChanged: { - if(backButton.enabled && backButton.activeFocus) { - listView.positionViewAtBeginning() - } - } - } - - ListViewType { - id: listView - - anchors.top: backButton.bottom - anchors.bottom: parent.bottom - anchors.right: parent.right - anchors.left: parent.left - - model: ApiCountryModel - - header: ColumnLayout { - width: listView.width - - BaseHeaderType { - id: header - - Layout.fillWidth: true - Layout.rightMargin: 16 - Layout.leftMargin: 16 - - headerText: qsTr("Configuration Files") - descriptionText: qsTr("For router setup or the AmneziaWG app") - } - } - - delegate: ColumnLayout { - width: listView.width - - LabelWithButtonType { - Layout.fillWidth: true - Layout.topMargin: 6 - - text: countryName - descriptionText: isWorkerExpired ? qsTr("The configuration needs to be reissued") : "" - hideDescription: isWorkerExpired ? false : true - descriptionColor: AmneziaStyle.color.vibrantRed - - leftImageSource: "qrc:/countriesFlags/images/flagKit/" + countryImageCode + ".svg" - rightImageSource: isIssued ? "qrc:/images/controls/more-vertical.svg" : "qrc:/images/controls/download.svg" - - clickedFunction: function() { - if (isIssued) { - moreOptionsDrawer.countryName = countryName - moreOptionsDrawer.countryCode = countryCode - moreOptionsDrawer.openTriggered() - } else { - issueConfig(countryCode) - } - } - } - - DividerType {} - } - } - - DrawerType2 { - id: moreOptionsDrawer - - property string countryName - property string countryCode - - anchors.fill: parent - expandedHeight: parent.height * 0.4375 - - expandedStateContent: Item { - implicitHeight: moreOptionsDrawer.expandedHeight - - BackButtonType { - id: moreOptionsDrawerBackButton - - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.topMargin: 16 - - backButtonFunction: function() { - moreOptionsDrawer.closeTriggered() - } - } - - ListViewType { - id: drawerListView - - anchors.top: moreOptionsDrawerBackButton.bottom - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.right: parent.right - - header: ColumnLayout { - width: drawerListView.width - - Header2Type { - Layout.fillWidth: true - Layout.margins: 16 - - headerText: moreOptionsDrawer.countryName + qsTr(" configuration file") - } - } - - model: 1 // fake model to force the ListView to be created without a model - - delegate: ColumnLayout { - width: drawerListView.width - - LabelWithButtonType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - - text: qsTr("Generate a new configuration file") - descriptionText: qsTr("The previously created one will stop working") - - clickedFunction: function() { - showQuestion(true, moreOptionsDrawer.countryCode, moreOptionsDrawer.countryName) - } - } - - DividerType {} - } - - footer: ColumnLayout { - width: drawerListView.width - - LabelWithButtonType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - - text: qsTr("Revoke the current configuration file") - - clickedFunction: function() { - showQuestion(false, moreOptionsDrawer.countryCode, moreOptionsDrawer.countryName) - } - } - - DividerType {} - } - } - } - } - - function issueConfig(countryCode) { - var fileName = "" - if (GC.isMobile()) { - fileName = countryCode + configExtension - } else { - fileName = SystemController.getFileName(configCaption, - qsTr("Config files (*" + configExtension + ")"), - StandardPaths.standardLocations(StandardPaths.DocumentsLocation) + "/" + countryCode, - true, - configExtension) - } - if (fileName !== "") { - PageController.showBusyIndicator(true) - let result = SubscriptionUiController.exportNativeConfig(ServersUiController.getProcessedServerIndex(), countryCode, fileName) - if (result) { - SubscriptionUiController.getAccountInfo(ServersUiController.getProcessedServerIndex(), true) - } - - PageController.showBusyIndicator(false) - if (result) { - PageController.showNotificationMessage(qsTr("Config file saved")) - } - } - } - - function revokeConfig(countryCode) { - PageController.showBusyIndicator(true) - let result = SubscriptionUiController.revokeNativeConfig(ServersUiController.getProcessedServerIndex(), countryCode) - if (result) { - SubscriptionUiController.getAccountInfo(ServersUiController.getProcessedServerIndex(), true) - } - PageController.showBusyIndicator(false) - - if (result) { - PageController.showNotificationMessage(qsTr("The config has been revoked")) - } - } - - function showQuestion(isConfigIssue, countryCode, countryName) { - var headerText - if (isConfigIssue) { - headerText = qsTr("Generate a new %1 configuration file?").arg(countryName) - } else { - headerText = qsTr("Revoke the current %1 configuration file?").arg(countryName) - } - - var descriptionText = qsTr("Your previous configuration file will no longer work, and it will not be possible to connect using it") - var yesButtonText = isConfigIssue ? qsTr("Download") : qsTr("Continue") - var noButtonText = qsTr("Cancel") - - var yesButtonFunction = function() { - if (isConfigIssue) { - issueConfig(countryCode) - } else { - revokeConfig(countryCode) - } - moreOptionsDrawer.closeTriggered() - } - var noButtonFunction = function() {} - - showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) - } -} +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Dialogs + +import QtCore + +import SortFilterProxyModel 0.2 + +import PageEnum 1.0 +import Style 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" +import "../Components" + +PageType { + id: root + + property string configExtension: ".conf" + property string configCaption: qsTr("Save AmneziaVPN config") + + BackButtonType { + id: backButton + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 20 + PageController.safeAreaTopMargin + + onActiveFocusChanged: { + if(backButton.enabled && backButton.activeFocus) { + listView.positionViewAtBeginning() + } + } + } + + ListViewType { + id: listView + + anchors.top: backButton.bottom + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.left: parent.left + + model: ApiCountryModel + + header: ColumnLayout { + width: listView.width + + BaseHeaderType { + id: header + + Layout.fillWidth: true + Layout.rightMargin: 16 + Layout.leftMargin: 16 + + headerText: qsTr("Configuration Files") + descriptionText: qsTr("For router setup or the AmneziaWG app") + } + } + + delegate: ColumnLayout { + width: listView.width + + LabelWithButtonType { + Layout.fillWidth: true + Layout.topMargin: 6 + + text: countryName + descriptionText: isWorkerExpired ? qsTr("The configuration needs to be reissued") : "" + hideDescription: isWorkerExpired ? false : true + descriptionColor: AmneziaStyle.color.vibrantRed + + leftImageSource: "qrc:/countriesFlags/images/flagKit/" + countryImageCode + ".svg" + rightImageSource: isIssued ? "qrc:/images/controls/more-vertical.svg" : "qrc:/images/controls/download.svg" + + clickedFunction: function() { + if (isIssued) { + moreOptionsDrawer.countryName = countryName + moreOptionsDrawer.countryCode = countryCode + moreOptionsDrawer.openTriggered() + } else { + issueConfig(countryCode) + } + } + } + + DividerType {} + } + } + + DrawerType2 { + id: moreOptionsDrawer + + property string countryName + property string countryCode + + anchors.fill: parent + expandedHeight: parent.height * 0.4375 + + expandedStateContent: Item { + implicitHeight: moreOptionsDrawer.expandedHeight + + BackButtonType { + id: moreOptionsDrawerBackButton + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 16 + + backButtonFunction: function() { + moreOptionsDrawer.closeTriggered() + } + } + + ListViewType { + id: drawerListView + + anchors.top: moreOptionsDrawerBackButton.bottom + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + + header: ColumnLayout { + width: drawerListView.width + + Header2Type { + Layout.fillWidth: true + Layout.margins: 16 + + headerText: moreOptionsDrawer.countryName + qsTr(" configuration file") + } + } + + model: 1 // fake model to force the ListView to be created without a model + + delegate: ColumnLayout { + width: drawerListView.width + + LabelWithButtonType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + + text: qsTr("Generate a new configuration file") + descriptionText: qsTr("The previously created one will stop working") + + clickedFunction: function() { + showQuestion(true, moreOptionsDrawer.countryCode, moreOptionsDrawer.countryName) + } + } + + DividerType {} + } + + footer: ColumnLayout { + width: drawerListView.width + + LabelWithButtonType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + + text: qsTr("Revoke the current configuration file") + + clickedFunction: function() { + showQuestion(false, moreOptionsDrawer.countryCode, moreOptionsDrawer.countryName) + } + } + + DividerType {} + } + } + } + } + + function issueConfig(countryCode) { + var fileName = "" + if (GC.isMobile()) { + fileName = countryCode + configExtension + } else { + fileName = SystemController.getFileName(configCaption, + qsTr("Config files (*" + configExtension + ")"), + StandardPaths.standardLocations(StandardPaths.DocumentsLocation) + "/" + countryCode, + true, + configExtension) + } + if (fileName !== "") { + PageController.showBusyIndicator(true) + let result = SubscriptionUiController.exportNativeConfig(ServersUiController.getServerId(ServersUiController.processedServerIndex), countryCode, fileName) + if (result) { + SubscriptionUiController.getAccountInfo(ServersUiController.getServerId(ServersUiController.processedServerIndex), true) + } + + PageController.showBusyIndicator(false) + if (result) { + PageController.showNotificationMessage(qsTr("Config file saved")) + } + } + } + + function revokeConfig(countryCode) { + PageController.showBusyIndicator(true) + let result = SubscriptionUiController.revokeNativeConfig(ServersUiController.getServerId(ServersUiController.processedServerIndex), countryCode) + if (result) { + SubscriptionUiController.getAccountInfo(ServersUiController.getServerId(ServersUiController.processedServerIndex), true) + } + PageController.showBusyIndicator(false) + + if (result) { + PageController.showNotificationMessage(qsTr("The config has been revoked")) + } + } + + function showQuestion(isConfigIssue, countryCode, countryName) { + var headerText + if (isConfigIssue) { + headerText = qsTr("Generate a new %1 configuration file?").arg(countryName) + } else { + headerText = qsTr("Revoke the current %1 configuration file?").arg(countryName) + } + + var descriptionText = qsTr("Your previous configuration file will no longer work, and it will not be possible to connect using it") + var yesButtonText = isConfigIssue ? qsTr("Download") : qsTr("Continue") + var noButtonText = qsTr("Cancel") + + var yesButtonFunction = function() { + if (isConfigIssue) { + issueConfig(countryCode) + } else { + revokeConfig(countryCode) + } + moreOptionsDrawer.closeTriggered() + } + var noButtonFunction = function() {} + + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + } +} diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml index 7eb119244..07fb3089d 100644 --- a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml @@ -186,7 +186,7 @@ PageType { textColor: AmneziaStyle.color.midnightBlack clickedFunc: function() { - SubscriptionUiController.getRenewalLink(ServersUiController.getProcessedServerIndex()) + SubscriptionUiController.getRenewalLink(ServersUiController.getServerId(ServersUiController.processedServerIndex)) } } } @@ -246,7 +246,7 @@ PageType { text: qsTr("Renew subscription") clickedFunc: function() { - SubscriptionUiController.getRenewalLink(ServersUiController.getProcessedServerIndex()) + SubscriptionUiController.getRenewalLink(ServersUiController.getServerId(ServersUiController.processedServerIndex)) } } @@ -258,7 +258,7 @@ PageType { SwitcherType { id: switcher - readonly property bool isVlessProtocol: SubscriptionUiController.isVlessProtocol(ServersUiController.getProcessedServerIndex()) + readonly property bool isVlessProtocol: SubscriptionUiController.isVlessProtocol(ServersUiController.getServerId(ServersUiController.processedServerIndex)) readonly property bool isProtocolSwitchBlocked: ServersModel.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected Layout.fillWidth: true @@ -276,8 +276,8 @@ PageType { PageController.showNotificationMessage(qsTr("Cannot change protocol during active connection")) } else { PageController.showBusyIndicator(true) - SubscriptionUiController.setCurrentProtocol(ServersUiController.getProcessedServerIndex(), switcher.isVlessProtocol ? "awg" : "vless") - SubscriptionUiController.updateServiceFromGateway(ServersUiController.processedIndex, "", "", true) + SubscriptionUiController.setCurrentProtocol(ServersUiController.getServerId(ServersUiController.processedServerIndex), switcher.isVlessProtocol ? "awg" : "vless") + SubscriptionUiController.updateServiceFromGateway(ServersUiController.getServerId(ServersUiController.processedServerIndex), "", "", true) PageController.showBusyIndicator(false) } } @@ -325,7 +325,7 @@ PageType { PageController.goToPage(PageEnum.PageSettingsApiSubscriptionKey) PageController.showBusyIndicator(true) - SubscriptionUiController.prepareVpnKeyExport(ServersUiController.getProcessedServerIndex()) + SubscriptionUiController.prepareVpnKeyExport(ServersUiController.getServerId(ServersUiController.processedServerIndex)) PageController.showBusyIndicator(false) } @@ -431,7 +431,7 @@ PageType { PageController.showNotificationMessage(qsTr("Cannot reload API config during active connection")) } else { PageController.showBusyIndicator(true) - SubscriptionUiController.updateServiceFromGateway(ServersUiController.processedIndex, "", "", true) + SubscriptionUiController.updateServiceFromGateway(ServersUiController.getServerId(ServersUiController.processedServerIndex), "", "", true) PageController.showBusyIndicator(false) } } @@ -469,8 +469,8 @@ PageType { PageController.showNotificationMessage(qsTr("Cannot unlink device during active connection")) } else { PageController.showBusyIndicator(true) - if (SubscriptionUiController.deactivateDevice(ServersUiController.getProcessedServerIndex())) { - SubscriptionUiController.getAccountInfo(ServersUiController.getProcessedServerIndex(), true) + if (SubscriptionUiController.deactivateDevice(ServersUiController.getServerId(ServersUiController.processedServerIndex))) { + SubscriptionUiController.getAccountInfo(ServersUiController.getServerId(ServersUiController.processedServerIndex), true) } PageController.showBusyIndicator(false) } @@ -506,7 +506,7 @@ PageType { PageController.showNotificationMessage(qsTr("Cannot remove server during active connection")) } else { PageController.showBusyIndicator(true) - InstallController.removeServer(ServersUiController.getProcessedServerIndex()) + InstallController.removeServer(ServersUiController.getServerId(ServersUiController.processedServerIndex)) PageController.showBusyIndicator(false) } } diff --git a/client/ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml b/client/ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml index ac0c9f71e..442bf2ba2 100644 --- a/client/ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml +++ b/client/ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml @@ -48,7 +48,7 @@ PageType { Component.onCompleted: { PageController.showBusyIndicator(true) - SubscriptionUiController.prepareVpnKeyExport(ServersUiController.getProcessedServerIndex()) + SubscriptionUiController.prepareVpnKeyExport(ServersUiController.getServerId(ServersUiController.processedServerIndex)) PageController.showBusyIndicator(false) } @@ -119,7 +119,7 @@ PageType { if (fileName !== "") { PageController.showBusyIndicator(true) - SubscriptionUiController.exportVpnKey(ServersUiController.getProcessedServerIndex(), fileName) + SubscriptionUiController.exportVpnKey(ServersUiController.getServerId(ServersUiController.processedServerIndex), fileName) PageController.showBusyIndicator(false) } } @@ -141,7 +141,7 @@ PageType { clickedFunc: function() { PageController.showBusyIndicator(true) - SubscriptionUiController.prepareVpnKeyExport(ServersUiController.getProcessedServerIndex()) + SubscriptionUiController.prepareVpnKeyExport(ServersUiController.getServerId(ServersUiController.processedServerIndex)) PageController.showBusyIndicator(false) vpnKeyDrawer.openTriggered() } diff --git a/client/ui/qml/Pages2/PageSettingsDns.qml b/client/ui/qml/Pages2/PageSettingsDns.qml index a96d45a1b..ba68c20c5 100644 --- a/client/ui/qml/Pages2/PageSettingsDns.qml +++ b/client/ui/qml/Pages2/PageSettingsDns.qml @@ -37,7 +37,7 @@ PageType { anchors.right: parent.right anchors.left: parent.left - property var isServerFromApi: ServersModel.isServerFromApi(ServersUiController.defaultIndex) + property var isServerFromApi: ServersModel.isServerFromApi(ServersUiController.defaultServerIndex) enabled: !isServerFromApi diff --git a/client/ui/qml/Pages2/PageSettingsServerData.qml b/client/ui/qml/Pages2/PageSettingsServerData.qml index 8ae736ae0..f34d2d522 100644 --- a/client/ui/qml/Pages2/PageSettingsServerData.qml +++ b/client/ui/qml/Pages2/PageSettingsServerData.qml @@ -111,7 +111,7 @@ PageType { readonly property var tColor: AmneziaStyle.color.paleGray readonly property var clickedHandler: function() { PageController.showBusyIndicator(true) - InstallController.scanServerForInstalledContainers(ServersUiController.processedIndex) + InstallController.scanServerForInstalledContainers(ServersUiController.getServerId(ServersUiController.processedServerIndex)) PageController.showBusyIndicator(false) } } @@ -134,7 +134,7 @@ PageType { PageController.showNotificationMessage(qsTr("Cannot reboot server during active connection")) } else { PageController.showBusyIndicator(true) - InstallController.rebootServer(ServersUiController.processedIndex) + InstallController.rebootServer(ServersUiController.getServerId(ServersUiController.processedServerIndex)) PageController.showBusyIndicator(false) } } @@ -164,7 +164,7 @@ PageType { PageController.showNotificationMessage(qsTr("Cannot remove server during active connection")) } else { PageController.showBusyIndicator(true) - InstallController.removeServer(ServersUiController.processedIndex) + InstallController.removeServer(ServersUiController.getServerId(ServersUiController.processedServerIndex)) PageController.showBusyIndicator(false) } } @@ -194,7 +194,7 @@ PageType { PageController.showNotificationMessage(qsTr("Cannot clear server from Amnezia software during active connection")) } else { PageController.goToPage(PageEnum.PageDeinstalling) - InstallController.removeAllContainers(ServersUiController.processedIndex) + InstallController.removeAllContainers(ServersUiController.getServerId(ServersUiController.processedServerIndex)) } } var noButtonFunction = function() { @@ -223,7 +223,7 @@ PageType { PageController.showNotificationMessage(qsTr("Cannot reset API config during active connection")) } else { PageController.showBusyIndicator(true) - SubscriptionUiController.removeApiConfig(ServersUiController.processedIndex) + SubscriptionUiController.removeApiConfig(ServersUiController.getServerId(ServersUiController.processedServerIndex)) PageController.showBusyIndicator(false) } } diff --git a/client/ui/qml/Pages2/PageSettingsServerProtocol.qml b/client/ui/qml/Pages2/PageSettingsServerProtocol.qml index 13d77aa5b..e7ffe90db 100644 --- a/client/ui/qml/Pages2/PageSettingsServerProtocol.qml +++ b/client/ui/qml/Pages2/PageSettingsServerProtocol.qml @@ -76,7 +76,7 @@ PageType { clickedFunction: function() { if (isClientProtocolExists) { - InstallController.openClientSettings(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, protocolIndex) + InstallController.openClientSettings(ServersUiController.getServerId(ServersUiController.processedServerIndex), ServersUiController.processedContainerIndex, protocolIndex) PageController.goToPage(clientProtocolPage); } else { PageController.showNotificationMessage(qsTr("Click the \"connect\" button to create a connection configuration")) @@ -104,7 +104,7 @@ PageType { visible: delegateContent.isServerSettingsVisible clickedFunction: function() { - InstallController.openServerSettings(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, protocolIndex) + InstallController.openServerSettings(ServersUiController.getServerId(ServersUiController.processedServerIndex), ServersUiController.processedContainerIndex, protocolIndex) PageController.goToPage(serverProtocolPage); } @@ -147,7 +147,7 @@ PageType { } PageController.showBusyIndicator(true) - InstallController.clearCachedProfile(ServersUiController.processedIndex, ServersUiController.processedContainerIndex) + InstallController.clearCachedProfile(ServersUiController.getServerId(ServersUiController.processedServerIndex), ServersUiController.processedContainerIndex) PageController.showBusyIndicator(false) } @@ -191,7 +191,7 @@ PageType { } else { PageController.goToPage(PageEnum.PageDeinstalling) - InstallController.removeContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex) + InstallController.removeContainer(ServersUiController.getServerId(ServersUiController.processedServerIndex), ServersUiController.processedContainerIndex) } } var noButtonFunction = function() { diff --git a/client/ui/qml/Pages2/PageSettingsServersList.qml b/client/ui/qml/Pages2/PageSettingsServersList.qml index c163b1eb9..478900d4e 100644 --- a/client/ui/qml/Pages2/PageSettingsServersList.qml +++ b/client/ui/qml/Pages2/PageSettingsServersList.qml @@ -86,11 +86,11 @@ PageType { rightImageSource: "qrc:/images/controls/chevron-right.svg" clickedFunction: function() { - ServersUiController.processedIndex = index + ServersUiController.setProcessedServerIndex(index) if (ServersModel.getProcessedServerData("isServerFromGatewayApi")) { PageController.showBusyIndicator(true) - let result = SubscriptionUiController.getAccountInfo(ServersUiController.getProcessedServerIndex(), false) + let result = SubscriptionUiController.getAccountInfo(ServersUiController.getServerId(ServersUiController.processedServerIndex), false) PageController.showBusyIndicator(false) if (!result) { return diff --git a/client/ui/qml/Pages2/PageSetupWizardCredentials.qml b/client/ui/qml/Pages2/PageSetupWizardCredentials.qml index 216d8aa32..24436cc38 100644 --- a/client/ui/qml/Pages2/PageSetupWizardCredentials.qml +++ b/client/ui/qml/Pages2/PageSetupWizardCredentials.qml @@ -121,7 +121,7 @@ PageType { var _secretData = listView.itemAtIndex(vars.secretDataIndex).children[0].textField.text InstallController.setProcessedServerCredentials(_hostname, _username, _secretData) - ServersUiController.processedIndex = -1 + ServersUiController.setProcessedServerIndex(-1) PageController.showBusyIndicator(true) var isConnectionOpened = InstallController.checkSshConnection() diff --git a/client/ui/qml/Pages2/PageSetupWizardEasy.qml b/client/ui/qml/Pages2/PageSetupWizardEasy.qml index 27d955812..faefd1e25 100644 --- a/client/ui/qml/Pages2/PageSetupWizardEasy.qml +++ b/client/ui/qml/Pages2/PageSetupWizardEasy.qml @@ -163,9 +163,9 @@ PageType { ServersUiController.processedContainerIndex = listView.dockerContainer PageController.goToPage(PageEnum.PageSetupWizardInstalling) InstallController.install(listView.dockerContainer, - listView.containerDefaultPort, - listView.containerDefaultTransportProto, - ServersUiController.processedIndex) + listView.containerDefaultPort, + listView.containerDefaultTransportProto, + ServersUiController.getServerId(ServersUiController.processedServerIndex)) } else { PageController.goToPage(PageEnum.PageSetupWizardProtocols) } diff --git a/client/ui/qml/Pages2/PageSetupWizardInstalling.qml b/client/ui/qml/Pages2/PageSetupWizardInstalling.qml index 4759eb3d7..2d7841496 100644 --- a/client/ui/qml/Pages2/PageSetupWizardInstalling.qml +++ b/client/ui/qml/Pages2/PageSetupWizardInstalling.qml @@ -28,7 +28,7 @@ PageType { function onInstallContainerFinished(finishedMessage, isServiceInstall) { var containerIndex = ServersUiController.processedContainerIndex if (!ConnectionController.isConnected && !ContainersModel.isServiceContainer(containerIndex)) { - ServersUiController.setDefaultContainer(ServersUiController.processedIndex, containerIndex) + ServersUiController.setDefaultContainer(ServersUiController.getServerId(ServersUiController.processedServerIndex), containerIndex) } PageController.closePage() // close installing page @@ -47,8 +47,8 @@ PageType { function onInstallServerFinished(finishedMessage) { if (!ConnectionController.isConnected) { - ServersUiController.setDefaultServerIndex(ServersModel.getServersCount() - 1); - ServersUiController.processedIndex = ServersUiController.defaultIndex + ServersUiController.setDefaultServerAtIndex(ServersModel.getServersCount() - 1); + ServersUiController.setProcessedServerIndex(ServersUiController.defaultServerIndex) } PageController.goToPageHome() @@ -57,7 +57,7 @@ PageType { function onServerAlreadyExists(serverIndex) { PageController.goToStartPage() - ServersUiController.processedIndex = serverIndex + ServersUiController.setProcessedServerIndex(serverIndex) PageController.goToPage(PageEnum.PageSettingsServerInfo, false) PageController.showErrorMessage(qsTr("The server has already been added to the application")) diff --git a/client/ui/qml/Pages2/PageSetupWizardProtocolSettings.qml b/client/ui/qml/Pages2/PageSetupWizardProtocolSettings.qml index 7d7526592..bd51a3ced 100644 --- a/client/ui/qml/Pages2/PageSetupWizardProtocolSettings.qml +++ b/client/ui/qml/Pages2/PageSetupWizardProtocolSettings.qml @@ -243,7 +243,7 @@ PageType { } PageController.goToPage(PageEnum.PageSetupWizardInstalling); - InstallController.install(dockerContainer, port.textField.text, transportProtoSelector.currentIndex, ServersUiController.processedIndex) + InstallController.install(dockerContainer, port.textField.text, transportProtoSelector.currentIndex, ServersUiController.getServerId(ServersUiController.processedServerIndex)) } } diff --git a/client/ui/qml/Pages2/PageShare.qml b/client/ui/qml/Pages2/PageShare.qml index 6c2c81e4e..9dab7a5fa 100644 --- a/client/ui/qml/Pages2/PageShare.qml +++ b/client/ui/qml/Pages2/PageShare.qml @@ -42,40 +42,40 @@ PageType { var configExtension var configFileName - var serverIndex = ServersUiController.processedIndex var containerIndex = ServersUiController.processedContainerIndex + var serverId = ServersUiController.getServerId(ServersUiController.processedServerIndex) switch (type) { case PageShare.ConfigType.AmneziaConnection: { - ExportController.generateConnectionConfig(serverIndex, containerIndex, clientNameTextField.textField.text); + ExportController.generateConnectionConfig(serverId, containerIndex, clientNameTextField.textField.text); configCaption = qsTr("Save AmneziaVPN config") configExtension = ".vpn" configFileName = "amnezia_config" break; } case PageShare.ConfigType.OpenVpn: { - ExportController.generateOpenVpnConfig(serverIndex, clientNameTextField.textField.text) + ExportController.generateOpenVpnConfig(serverId, clientNameTextField.textField.text) configCaption = qsTr("Save OpenVPN config") configExtension = ".ovpn" configFileName = "amnezia_for_openvpn" break } case PageShare.ConfigType.WireGuard: { - ExportController.generateWireGuardConfig(serverIndex, clientNameTextField.textField.text) + ExportController.generateWireGuardConfig(serverId, clientNameTextField.textField.text) configCaption = qsTr("Save WireGuard config") configExtension = ".conf" configFileName = "amnezia_for_wireguard" break } case PageShare.ConfigType.Awg: { - ExportController.generateAwgConfig(serverIndex, containerIndex, clientNameTextField.textField.text) + ExportController.generateAwgConfig(serverId, containerIndex, clientNameTextField.textField.text) configCaption = qsTr("Save AmneziaWG config") configExtension = ".conf" configFileName = "amnezia_for_awg" break } case PageShare.ConfigType.Xray: { - ExportController.generateXrayConfig(serverIndex, clientNameTextField.textField.text) + ExportController.generateXrayConfig(serverId, clientNameTextField.textField.text) configCaption = qsTr("Save XRay config") configExtension = ".json" configFileName = "amnezia_for_xray" @@ -249,7 +249,7 @@ PageType { onClicked: { accessTypeSelector.currentIndex = 1 PageController.showBusyIndicator(true) - ExportController.updateClientManagementModel(ServersUiController.processedIndex, + ExportController.updateClientManagementModel(ServersUiController.getServerId(ServersUiController.processedServerIndex), ServersUiController.processedContainerIndex) PageController.showBusyIndicator(false) } @@ -333,7 +333,7 @@ PageType { Component.onCompleted: { if (ServersModel.isDefaultServerHasWriteAccess() && ServersModel.getDefaultServerData("hasInstalledContainers")) { - serverSelectorListView.selectedIndex = proxyServersModel.mapFromSource(ServersUiController.defaultIndex) + serverSelectorListView.selectedIndex = proxyServersModel.mapFromSource(ServersUiController.defaultServerIndex) } else { serverSelectorListView.selectedIndex = 0 } @@ -344,7 +344,7 @@ PageType { function handler() { serverSelector.text = selectedText - ServersUiController.processedIndex = proxyServersModel.mapToSource(selectedIndex) + ServersUiController.setProcessedServerIndex(proxyServersModel.mapToSource(selectedIndex)) } } } @@ -417,7 +417,7 @@ PageType { if (accessTypeSelector.currentIndex === 1) { PageController.showBusyIndicator(true) - ExportController.updateClientManagementModel(ServersUiController.processedIndex, + ExportController.updateClientManagementModel(ServersUiController.getServerId(ServersUiController.processedServerIndex), ServersUiController.processedContainerIndex) PageController.showBusyIndicator(false) } @@ -792,9 +792,9 @@ PageType { clientsListView.freezeFilter = true PageController.showBusyIndicator(true) ExportController.renameClient(proxyClientManagementModel.mapToSource(index), - clientNameEditor.textField.text, - ServersUiController.processedIndex, - ServersUiController.processedContainerIndex) + clientNameEditor.textField.text, + ServersUiController.getServerId(ServersUiController.processedServerIndex), + ServersUiController.processedContainerIndex) PageController.showBusyIndicator(false) Qt.callLater(function(){ clientsListView.freezeFilter = false }) clientNameEditDrawer.closeTriggered() @@ -829,8 +829,8 @@ PageType { clientInfoDrawer.closeTriggered() PageController.showBusyIndicator(true) ExportController.revokeConfig(proxyClientManagementModel.mapToSource(index), - ServersUiController.processedIndex, - ServersUiController.processedContainerIndex) + ServersUiController.getServerId(ServersUiController.processedServerIndex), + ServersUiController.processedContainerIndex) } var noButtonFunction = function() { } diff --git a/client/ui/qml/Pages2/PageShareFullAccess.qml b/client/ui/qml/Pages2/PageShareFullAccess.qml index 8b162899c..7fb0dd61d 100644 --- a/client/ui/qml/Pages2/PageShareFullAccess.qml +++ b/client/ui/qml/Pages2/PageShareFullAccess.qml @@ -119,13 +119,13 @@ PageType { Component.onCompleted: { serverSelectorListView.currentIndex = ServersModel.isDefaultServerHasWriteAccess() ? - proxyServersModel.mapFromSource(ServersUiController.defaultIndex) : 0 + proxyServersModel.mapFromSource(ServersUiController.defaultServerIndex) : 0 serverSelectorListView.triggerCurrentItem() } function handler() { serverSelector.text = selectedText - ServersUiController.processedIndex = proxyServersModel.mapToSource(selectedIndex) + ServersUiController.setProcessedServerIndex(proxyServersModel.mapToSource(selectedIndex)) } } } @@ -155,7 +155,7 @@ PageType { ExportController.exportErrorOccurred(qsTr("Access error!")) return } else { - ExportController.generateFullAccessConfig(ServersUiController.processedIndex) + ExportController.generateFullAccessConfig(ServersUiController.getServerId(ServersUiController.processedServerIndex)) } PageController.showBusyIndicator(false) diff --git a/client/ui/qml/Pages2/PageStart.qml b/client/ui/qml/Pages2/PageStart.qml index d39baa9f8..00411b5e1 100644 --- a/client/ui/qml/Pages2/PageStart.qml +++ b/client/ui/qml/Pages2/PageStart.qml @@ -154,7 +154,7 @@ PageType { function onNoInstalledContainers() { PageController.setTriggeredByConnectButton(true) - ServersUiController.processedIndex = ServersUiController.defaultIndex + ServersUiController.setProcessedServerIndex(ServersUiController.defaultServerIndex) PageController.goToPage(PageEnum.PageSetupWizardEasy) } } @@ -227,11 +227,11 @@ PageType { function onInstallServerFromApiFinished(message, preferredDefaultIndex) { if (!ConnectionController.isConnected) { if (preferredDefaultIndex !== undefined && preferredDefaultIndex >= 0) { - ServersUiController.setDefaultServerIndex(preferredDefaultIndex) + ServersUiController.setDefaultServerAtIndex(preferredDefaultIndex) } else { - ServersUiController.setDefaultServerIndex(ServersModel.getServersCount() - 1); + ServersUiController.setDefaultServerAtIndex(ServersModel.getServersCount() - 1); } - ServersUiController.processedIndex = ServersUiController.defaultIndex + ServersUiController.setProcessedServerIndex(ServersUiController.defaultServerIndex) } PageController.goToPageHome() @@ -274,7 +274,7 @@ PageType { } else { tabBar.visible = true pagePath = PageController.getPagePath(PageEnum.PageHome) - ServersUiController.processedIndex = ServersUiController.defaultIndex + ServersUiController.setProcessedServerIndex(ServersUiController.defaultServerIndex) } tabBarStackView.push(pagePath, { "objectName" : pagePath }) @@ -348,7 +348,7 @@ PageType { image: "qrc:/images/controls/home.svg" clickedFunc: function () { tabBarStackView.goToTabBarPage(PageEnum.PageHome) - ServersUiController.processedIndex = ServersUiController.defaultIndex + ServersUiController.setProcessedServerIndex(ServersUiController.defaultServerIndex) tabBar.currentIndex = 0 } } diff --git a/client/ui/qml/main2.qml b/client/ui/qml/main2.qml index bf3ab0518..18c59b264 100644 --- a/client/ui/qml/main2.qml +++ b/client/ui/qml/main2.qml @@ -304,6 +304,14 @@ Window { } } + Connections { + target: PageController + + function onUnsupportedConnectDrawerRequested() { + root.showUnsupportedConnectDrawer() + } + } + Connections { target: SubscriptionUiController @@ -332,6 +340,28 @@ Window { } } + function showUnsupportedConnectDrawer() { + let headerText = qsTr("This subscription format is no longer supported") + let descriptionText = qsTr("This legacy Amnezia subscription type can no longer be used to connect in this application version.\nRemove the server from the app to continue.") + let yesButtonText = qsTr("Continue") + let noButtonText = qsTr("Cancel") + + let yesButtonFunction = function() { + if (ConnectionController.isConnected) { + PageController.showNotificationMessage(qsTr("Cannot remove server during active connection")) + return + } + + PageController.showBusyIndicator(true) + InstallController.removeServer(ServersUiController.defaultServerId) + PageController.showBusyIndicator(false) + } + let noButtonFunction = function() { + } + + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + } + function showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) { questionDrawer.headerText = headerText questionDrawer.descriptionText = descriptionText diff --git a/client/vpnConnection.cpp b/client/vpnConnection.cpp index ad8ce917d..5329f2dc5 100644 --- a/client/vpnConnection.cpp +++ b/client/vpnConnection.cpp @@ -30,6 +30,7 @@ #endif #include "core/utils/networkUtilities.h" +#include "core/utils/serverConfigUtils.h" #include "vpnConnection.h" using namespace ProtocolUtils; @@ -74,8 +75,46 @@ void VpnConnection::onConnectionStateChanged(Vpn::ConnectionState state) return; } - ServerConfig defaultServer = m_serversRepository->server(m_serversRepository->defaultServerIndex()); - DockerContainer container = defaultServer.defaultContainer(); + const QString defaultServerId = m_serversRepository->defaultServerId(); + DockerContainer container = DockerContainer::None; + switch (m_serversRepository->serverKind(defaultServerId)) { + case serverConfigUtils::ConfigType::SelfHostedAdmin: { + const auto cfg = m_serversRepository->selfHostedAdminConfig(defaultServerId); + if (cfg.has_value()) { + container = cfg->defaultContainer; + } + break; + } + case serverConfigUtils::ConfigType::SelfHostedUser: { + const auto cfg = m_serversRepository->selfHostedUserConfig(defaultServerId); + if (cfg.has_value()) { + container = cfg->defaultContainer; + } + break; + } + case serverConfigUtils::ConfigType::Native: { + const auto cfg = m_serversRepository->nativeConfig(defaultServerId); + if (cfg.has_value()) { + container = cfg->defaultContainer; + } + break; + } + case serverConfigUtils::ConfigType::AmneziaPremiumV2: + case serverConfigUtils::ConfigType::AmneziaFreeV3: + case serverConfigUtils::ConfigType::ExternalPremium: { + const auto cfg = m_serversRepository->apiV2Config(defaultServerId); + if (cfg.has_value()) { + container = cfg->defaultContainer; + } + break; + } + case serverConfigUtils::ConfigType::AmneziaPremiumV1: + case serverConfigUtils::ConfigType::AmneziaFreeV2: + break; + case serverConfigUtils::ConfigType::Invalid: + default: + break; + } IpcClient::withInterface([&](QSharedPointer iface) { switch (state) { @@ -247,7 +286,7 @@ Vpn::ConnectionState VpnConnection::connectionState() const return m_connectionState; } -void VpnConnection::connectToVpn(int serverIndex, DockerContainer container, const QJsonObject &vpnConfiguration) +void VpnConnection::connectToVpn(const QString &serverId, DockerContainer container, const QJsonObject &vpnConfiguration) { if (!m_appSettingsRepository || !m_serversRepository) { qCritical() << "VpnConnection::connectToVpn: repositories not initialized"; @@ -255,8 +294,8 @@ void VpnConnection::connectToVpn(int serverIndex, DockerContainer container, con return; } - qDebug() << QString("Trying to connect to VPN, server index is %1, container is %2, route mode is") - .arg(serverIndex) + qDebug() << QString("Trying to connect to VPN, server id is %1, container is %2, route mode is") + .arg(serverId) .arg(ContainerUtils::containerToString(container)) << m_appSettingsRepository->routeMode(); diff --git a/client/vpnConnection.h b/client/vpnConnection.h index 0a6857447..9e5a9aa55 100644 --- a/client/vpnConnection.h +++ b/client/vpnConnection.h @@ -49,7 +49,7 @@ public: public slots: void setRepositories(SecureServersRepository* serversRepository, SecureAppSettingsRepository* appSettingsRepository); - void connectToVpn(int serverIndex, DockerContainer container, const QJsonObject &vpnConfiguration); + void connectToVpn(const QString &serverId, DockerContainer container, const QJsonObject &vpnConfiguration); void reconnectToVpn(); void disconnectFromVpn(); From fd0c773918eb66fe2db3bc2c30a07c6d41d941bb Mon Sep 17 00:00:00 2001 From: Yaroslav Gurov <31506978+ygurov@users.noreply.github.com> Date: Fri, 15 May 2026 06:36:38 +0200 Subject: [PATCH 02/14] fix: change artifact names (#2589) --- client/client_scripts/clientScripts.qrc | 1 - client/client_scripts/linux_installer.sh | 29 -------------- client/core/controllers/updateController.cpp | 40 +++---------------- .../core/utils/selfhosted/scriptsRegistry.cpp | 1 - .../core/utils/selfhosted/scriptsRegistry.h | 1 - 5 files changed, 6 insertions(+), 66 deletions(-) delete mode 100644 client/client_scripts/linux_installer.sh diff --git a/client/client_scripts/clientScripts.qrc b/client/client_scripts/clientScripts.qrc index 1c0ba9909..5e561a011 100644 --- a/client/client_scripts/clientScripts.qrc +++ b/client/client_scripts/clientScripts.qrc @@ -1,6 +1,5 @@ - linux_installer.sh mac_installer.sh diff --git a/client/client_scripts/linux_installer.sh b/client/client_scripts/linux_installer.sh deleted file mode 100644 index f2232bc4c..000000000 --- a/client/client_scripts/linux_installer.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash - -EXTRACT_DIR="$1" -INSTALLER_PATH="$2" - -# Create and clean extract directory -rm -rf "$EXTRACT_DIR" -mkdir -p "$EXTRACT_DIR" - -# Extract TAR archive -tar -xf "$INSTALLER_PATH" -C "$EXTRACT_DIR" -if [ $? -ne 0 ]; then - echo 'Failed to extract TAR archive' - exit 1 -fi - -# Find and run installer -INSTALLER=$(find "$EXTRACT_DIR" -type f -executable) -if [ -z "$INSTALLER" ]; then - echo 'Installer not found' - exit 1 -fi - -"$INSTALLER" -EXIT_CODE=$? - -# Cleanup -rm -rf "$EXTRACT_DIR" -exit $EXIT_CODE \ No newline at end of file diff --git a/client/core/controllers/updateController.cpp b/client/core/controllers/updateController.cpp index 24009705e..de7106c00 100644 --- a/client/core/controllers/updateController.cpp +++ b/client/core/controllers/updateController.cpp @@ -21,14 +21,14 @@ namespace Logger logger("UpdateController"); #if defined(Q_OS_WINDOWS) - const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN_%1_x64.exe"); + const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN-%1-win64.exe"); const QString kInstallerLocalPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN_installer.exe"; #elif defined(Q_OS_MACOS) - const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN_%1_macos.pkg"); + const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN-%1-Darwin.pkg"); const QString kInstallerLocalPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN.pkg"; #elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) - const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN_%1_linux_x64.tar"); - const QString kInstallerLocalPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN.tar"; + const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN-%1-Linux.run"); + const QString kInstallerLocalPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN.run"; #endif } @@ -346,36 +346,10 @@ int UpdateController::runMacInstaller(const QString &installerPath) #if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) int UpdateController::runLinuxInstaller(const QString &installerPath) { - // Create temporary directory for extraction - QTemporaryDir extractDir; - extractDir.setAutoRemove(false); - if (!extractDir.isValid()) { - logger.error() << "Failed to create temporary directory"; - return -1; - } - logger.info() << "Temporary directory created:" << extractDir.path(); + QFile::setPermissions(installerPath, QFile::permissions(installerPath) | QFile::ExeUser); - // Create script file in the temporary directory - QString scriptPath = extractDir.path() + "/installer.sh"; - QFile scriptFile(scriptPath); - if (!scriptFile.open(QIODevice::WriteOnly)) { - logger.error() << "Failed to create script file"; - return -1; - } - - // Get script content from registry - QString scriptContent = amnezia::scriptData(amnezia::ClientScriptType::linux_installer); - scriptFile.write(scriptContent.toUtf8()); - scriptFile.close(); - logger.info() << "Script file created:" << scriptPath; - - // Make script executable - QFile::setPermissions(scriptPath, QFile::permissions(scriptPath) | QFile::ExeUser); - - // Start detached process qint64 pid; - bool success = - QProcess::startDetached("/bin/bash", QStringList() << scriptPath << extractDir.path() << installerPath, extractDir.path(), &pid); + bool success = QProcess::startDetached(installerPath, QStringList(), QString(), &pid); if (success) { logger.info() << "Installation process started with PID:" << pid; @@ -387,5 +361,3 @@ int UpdateController::runLinuxInstaller(const QString &installerPath) return 0; } #endif - - diff --git a/client/core/utils/selfhosted/scriptsRegistry.cpp b/client/core/utils/selfhosted/scriptsRegistry.cpp index 3ff409499..26bf73de6 100644 --- a/client/core/utils/selfhosted/scriptsRegistry.cpp +++ b/client/core/utils/selfhosted/scriptsRegistry.cpp @@ -76,7 +76,6 @@ QString amnezia::scriptName(ProtocolScriptType type) QString amnezia::scriptName(ClientScriptType type) { switch (type) { - case ClientScriptType::linux_installer: return QLatin1String("linux_installer.sh"); case ClientScriptType::mac_installer: return QLatin1String("mac_installer.sh"); default: return QString(); } diff --git a/client/core/utils/selfhosted/scriptsRegistry.h b/client/core/utils/selfhosted/scriptsRegistry.h index 26bb2f0e9..b9d320455 100644 --- a/client/core/utils/selfhosted/scriptsRegistry.h +++ b/client/core/utils/selfhosted/scriptsRegistry.h @@ -43,7 +43,6 @@ enum ProtocolScriptType { enum ClientScriptType { // Client-side scripts - linux_installer, mac_installer }; From d0a1af03815f3dfca8ac9edd109d378fb3f88348 Mon Sep 17 00:00:00 2001 From: yp Date: Fri, 15 May 2026 09:56:09 +0300 Subject: [PATCH 03/14] refactor: deactivate api config before remove (#2569) Co-authored-by: vkamn --- .../api/subscriptionController.cpp | 21 +++++++++++++++++++ .../controllers/api/subscriptionController.h | 2 ++ .../api/subscriptionUiController.cpp | 9 ++++++++ .../api/subscriptionUiController.h | 3 +++ .../qml/Pages2/PageSettingsApiServerInfo.qml | 2 +- client/ui/qml/Pages2/PageStart.qml | 10 +++++++++ 6 files changed, 46 insertions(+), 1 deletion(-) diff --git a/client/core/controllers/api/subscriptionController.cpp b/client/core/controllers/api/subscriptionController.cpp index d7f149f43..8efc14558 100644 --- a/client/core/controllers/api/subscriptionController.cpp +++ b/client/core/controllers/api/subscriptionController.cpp @@ -679,6 +679,27 @@ void SubscriptionController::removeApiConfig(const QString &serverId) serverConfigUtils::configTypeFromJson(apiV2->toJson())); } +bool SubscriptionController::removeServer(const QString &serverId) +{ + if (serverId.isEmpty()) { + return false; + } + + if (!m_serversRepository->apiV2Config(serverId).has_value()) { + qWarning().noquote() << "SubscriptionController::removeServer: not an Api V2 server, id" << serverId; + return false; + } + + const ErrorCode revokeError = deactivateDevice(serverId); + if (revokeError != ErrorCode::NoError && revokeError != ErrorCode::ApiNotFoundError) { + qWarning().noquote() << "SubscriptionController::removeServer: deactivateDevice failed (error" + << static_cast(revokeError) << "); removing locally anyway."; + } + + m_serversRepository->removeServer(serverId); + return true; +} + bool SubscriptionController::isApiKeyExpired(const QString &serverId) const { auto apiV2 = m_serversRepository->apiV2Config(serverId); diff --git a/client/core/controllers/api/subscriptionController.h b/client/core/controllers/api/subscriptionController.h index 5b0f65da6..a0ac5d24b 100644 --- a/client/core/controllers/api/subscriptionController.h +++ b/client/core/controllers/api/subscriptionController.h @@ -74,6 +74,8 @@ public: void removeApiConfig(const QString &serverId); + bool removeServer(const QString &serverId); + void setCurrentProtocol(const QString &serverId, const QString &protocolName); bool isVlessProtocol(const QString &serverId) const; diff --git a/client/ui/controllers/api/subscriptionUiController.cpp b/client/ui/controllers/api/subscriptionUiController.cpp index 5dee205dc..550334c3a 100644 --- a/client/ui/controllers/api/subscriptionUiController.cpp +++ b/client/ui/controllers/api/subscriptionUiController.cpp @@ -406,6 +406,15 @@ void SubscriptionUiController::removeApiConfig(const QString &serverId) emit apiConfigRemoved(tr("Api config removed")); } +void SubscriptionUiController::removeServer(const QString &serverId) +{ + const QString serverName = m_serversController->notificationDisplayName(serverId); + if (!m_subscriptionController->removeServer(serverId)) { + return; + } + emit apiServerRemoved(tr("Server '%1' was removed").arg(serverName)); +} + QList SubscriptionUiController::getQrCodes() { diff --git a/client/ui/controllers/api/subscriptionUiController.h b/client/ui/controllers/api/subscriptionUiController.h index e4be939a3..8d28b52c9 100644 --- a/client/ui/controllers/api/subscriptionUiController.h +++ b/client/ui/controllers/api/subscriptionUiController.h @@ -58,6 +58,8 @@ public slots: void removeApiConfig(const QString &serverId); + void removeServer(const QString &serverId); + bool getAccountInfo(const QString &serverId, bool reload); void getRenewalLink(const QString &serverId); @@ -78,6 +80,7 @@ signals: void subscriptionRefreshNeeded(); void apiConfigRemoved(const QString &message); + void apiServerRemoved(const QString &message); void vpnKeyExportReady(); diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml index 07fb3089d..e1b8c7664 100644 --- a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml @@ -506,7 +506,7 @@ PageType { PageController.showNotificationMessage(qsTr("Cannot remove server during active connection")) } else { PageController.showBusyIndicator(true) - InstallController.removeServer(ServersUiController.getServerId(ServersUiController.processedServerIndex)) + SubscriptionUiController.removeServer(ServersUiController.getServerId(ServersUiController.processedServerIndex)) PageController.showBusyIndicator(false) } } diff --git a/client/ui/qml/Pages2/PageStart.qml b/client/ui/qml/Pages2/PageStart.qml index 00411b5e1..950a5a7d8 100644 --- a/client/ui/qml/Pages2/PageStart.qml +++ b/client/ui/qml/Pages2/PageStart.qml @@ -224,6 +224,16 @@ PageType { PageController.showNotificationMessage(message) } + function onApiServerRemoved(message) { + if (!ServersModel.getServersCount()) { + PageController.goToPageHome() + } else { + PageController.goToStartPage() + PageController.goToPage(PageEnum.PageSettingsServersList) + } + PageController.showNotificationMessage(message) + } + function onInstallServerFromApiFinished(message, preferredDefaultIndex) { if (!ConnectionController.isConnected) { if (preferredDefaultIndex !== undefined && preferredDefaultIndex >= 0) { From cb48667b9104ca3d4b2cf3cbdaf52c089705f972 Mon Sep 17 00:00:00 2001 From: yp Date: Fri, 15 May 2026 09:57:44 +0300 Subject: [PATCH 04/14] fix: bug when saving after canceling the save action (#2568) Co-authored-by: vkamn --- client/platforms/ios/ios_controller.mm | 4 ++- .../ui/controllers/allowedDnsUiController.cpp | 6 +++- .../api/subscriptionUiController.cpp | 8 +++--- .../ipSplitTunnelingUiController.cpp | 7 ++++- .../selfhosted/exportUiController.cpp | 6 +++- .../ui/controllers/settingsUiController.cpp | 12 ++++++-- client/ui/controllers/systemController.cpp | 28 ++++++++++++++----- client/ui/controllers/systemController.h | 4 ++- .../Pages2/PageSettingsApiNativeConfigs.qml | 3 -- .../Pages2/PageSettingsApiSubscriptionKey.qml | 5 +++- 10 files changed, 60 insertions(+), 23 deletions(-) diff --git a/client/platforms/ios/ios_controller.mm b/client/platforms/ios/ios_controller.mm index 73aa02484..c93dac460 100644 --- a/client/platforms/ios/ios_controller.mm +++ b/client/platforms/ios/ios_controller.mm @@ -977,7 +977,9 @@ bool IosController::shareText(const QStringList& filesToSend) { } #if !MACOS_NE UIViewController *qtController = getViewController(); - if (!qtController) return; + if (!qtController) { + return false; + } UIActivityViewController *activityController = [[UIActivityViewController alloc] initWithActivityItems:sharingItems applicationActivities:nil]; #endif diff --git a/client/ui/controllers/allowedDnsUiController.cpp b/client/ui/controllers/allowedDnsUiController.cpp index f9d62a5a6..e71b8dd5c 100644 --- a/client/ui/controllers/allowedDnsUiController.cpp +++ b/client/ui/controllers/allowedDnsUiController.cpp @@ -1,5 +1,6 @@ #include "allowedDnsUiController.h" +#include #include #include #include @@ -98,7 +99,10 @@ void AllowedDnsUiController::exportDns(const QString &fileName) QJsonDocument jsonDocument(jsonArray); QByteArray jsonData = jsonDocument.toJson(); - SystemController::saveFile(fileName, jsonData); + if (!SystemController::saveFile(fileName, jsonData)) { + qInfo() << "AllowedDnsUiController::exportDns: save or share was cancelled or failed"; + return; + } emit finished(tr("Export completed")); } diff --git a/client/ui/controllers/api/subscriptionUiController.cpp b/client/ui/controllers/api/subscriptionUiController.cpp index 550334c3a..05369d34d 100644 --- a/client/ui/controllers/api/subscriptionUiController.cpp +++ b/client/ui/controllers/api/subscriptionUiController.cpp @@ -98,8 +98,7 @@ bool SubscriptionUiController::exportVpnKey(const QString &serverId, const QStri return false; } - SystemController::saveFile(fileName, m_vpnKey); - return true; + return SystemController::saveFile(fileName, m_vpnKey); } @@ -117,8 +116,9 @@ bool SubscriptionUiController::exportNativeConfig(const QString &serverId, const return false; } - SystemController::saveFile(fileName, nativeConfig); - return true; + const bool saved = SystemController::saveFile(fileName, nativeConfig); + getAccountInfo(serverIndex, true); + return saved; } diff --git a/client/ui/controllers/ipSplitTunnelingUiController.cpp b/client/ui/controllers/ipSplitTunnelingUiController.cpp index c9081bd83..449a7ccc7 100644 --- a/client/ui/controllers/ipSplitTunnelingUiController.cpp +++ b/client/ui/controllers/ipSplitTunnelingUiController.cpp @@ -1,5 +1,7 @@ #include "ipSplitTunnelingUiController.h" +#include + #include "systemController.h" #include "core/utils/errorCodes.h" #include "core/utils/routeModes.h" @@ -55,7 +57,10 @@ void IpSplitTunnelingUiController::importSites(const QString &fileName, bool rep void IpSplitTunnelingUiController::exportSites(const QString &fileName) { QByteArray jsonData = m_ipSplitTunnelingController->exportSitesToJson(); - SystemController::saveFile(fileName, QString::fromUtf8(jsonData)); + if (!SystemController::saveFile(fileName, jsonData)) { + qInfo() << "IpSplitTunnelingUiController::exportSites: save or share was cancelled or failed"; + return; + } emit finished(tr("Export completed")); } diff --git a/client/ui/controllers/selfhosted/exportUiController.cpp b/client/ui/controllers/selfhosted/exportUiController.cpp index 2c76fccb6..3e7984668 100644 --- a/client/ui/controllers/selfhosted/exportUiController.cpp +++ b/client/ui/controllers/selfhosted/exportUiController.cpp @@ -1,5 +1,7 @@ #include "exportUiController.h" +#include + #include "../systemController.h" ExportUiController::ExportUiController(ExportController* exportController, QObject *parent) @@ -68,7 +70,9 @@ QList ExportUiController::getQrCodes() void ExportUiController::exportConfig(const QString &fileName) { - SystemController::saveFile(fileName, m_config); + if (!SystemController::saveFile(fileName, m_config)) { + qInfo() << "ExportUiController::exportConfig: save or share was cancelled or failed"; + } } void ExportUiController::updateClientManagementModel(const QString &serverId, int containerIndex) diff --git a/client/ui/controllers/settingsUiController.cpp b/client/ui/controllers/settingsUiController.cpp index a577d3cbc..6037aed76 100644 --- a/client/ui/controllers/settingsUiController.cpp +++ b/client/ui/controllers/settingsUiController.cpp @@ -107,7 +107,9 @@ void SettingsUiController::exportLogsFile(const QString &fileName) #ifdef Q_OS_ANDROID AndroidController::instance()->exportLogsFile(fileName); #else - SystemController::saveFile(fileName, Logger::getLogFile()); + if (!SystemController::saveFile(fileName, Logger::getLogFile())) { + qInfo() << "SettingsUiController::exportLogsFile: save or share was cancelled or failed"; + } #endif } @@ -116,7 +118,9 @@ void SettingsUiController::exportServiceLogsFile(const QString &fileName) #ifdef Q_OS_ANDROID AndroidController::instance()->exportLogsFile(fileName); #else - SystemController::saveFile(fileName, Logger::getServiceLogFile()); + if (!SystemController::saveFile(fileName, Logger::getServiceLogFile())) { + qInfo() << "SettingsUiController::exportServiceLogsFile: save or share was cancelled or failed"; + } #endif } @@ -132,7 +136,9 @@ void SettingsUiController::clearLogs() void SettingsUiController::backupAppConfig(const QString &fileName) { QByteArray data = m_settingsController->backupAppConfig(); - SystemController::saveFile(fileName, data); + if (!SystemController::saveFile(fileName, data)) { + qInfo() << "SettingsUiController::backupAppConfig: save or share was cancelled or failed"; + } } void SettingsUiController::restoreAppConfig(const QString &fileName) diff --git a/client/ui/controllers/systemController.cpp b/client/ui/controllers/systemController.cpp index 236643315..57409dfeb 100644 --- a/client/ui/controllers/systemController.cpp +++ b/client/ui/controllers/systemController.cpp @@ -1,5 +1,6 @@ #include "systemController.h" +#include #include #include #include @@ -24,11 +25,20 @@ SystemController::SystemController(QObject *parent) { } -void SystemController::saveFile(const QString &fileName, const QString &data) +bool SystemController::saveFile(const QString &fileName, const QString &data) { #if defined Q_OS_ANDROID AndroidController::instance()->saveFile(fileName, data); - return; + return true; +#endif + return saveFile(fileName, data.toUtf8()); +} + +bool SystemController::saveFile(const QString &fileName, const QByteArray &data) +{ +#if defined Q_OS_ANDROID + AndroidController::instance()->saveFile(fileName, QString::fromUtf8(data)); + return true; #endif #ifdef Q_OS_IOS @@ -39,17 +49,20 @@ void SystemController::saveFile(const QString &fileName, const QString &data) #endif if (!file.open(QIODevice::WriteOnly)) { - return; + qWarning() << "SystemController::saveFile: cannot open" << fileName; + return false; + } + if (file.write(data) != data.size()) { + qWarning() << "SystemController::saveFile: write failed" << fileName; + file.close(); + return false; } - file.write(data.toUtf8()); file.close(); #ifdef Q_OS_IOS QStringList filesToSend; filesToSend.append(fileUrl.toString()); - // todo check if save successful - IosController::Instance()->shareText(filesToSend); - return; + return IosController::Instance()->shareText(filesToSend); #else QFileInfo fi(fileName); @@ -62,6 +75,7 @@ void SystemController::saveFile(const QString &fileName, const QString &data) #ifndef MACOS_NE QDesktopServices::openUrl(url); #endif + return true; #endif } diff --git a/client/ui/controllers/systemController.h b/client/ui/controllers/systemController.h index 8f62ef6ee..83f81fbb5 100644 --- a/client/ui/controllers/systemController.h +++ b/client/ui/controllers/systemController.h @@ -1,6 +1,7 @@ #ifndef SYSTEMCONTROLLER_H #define SYSTEMCONTROLLER_H +#include #include class SystemController : public QObject @@ -9,7 +10,8 @@ class SystemController : public QObject public: explicit SystemController(QObject *parent = nullptr); - static void saveFile(const QString &fileName, const QString &data); + static bool saveFile(const QString &fileName, const QString &data); + static bool saveFile(const QString &fileName, const QByteArray &data); static bool readFile(const QString &fileName, QByteArray &data); static bool readFile(const QString &fileName, QString &data); diff --git a/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml b/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml index 4c09122f3..51b12bd2d 100644 --- a/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml +++ b/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml @@ -192,9 +192,6 @@ PageType { if (fileName !== "") { PageController.showBusyIndicator(true) let result = SubscriptionUiController.exportNativeConfig(ServersUiController.getServerId(ServersUiController.processedServerIndex), countryCode, fileName) - if (result) { - SubscriptionUiController.getAccountInfo(ServersUiController.getServerId(ServersUiController.processedServerIndex), true) - } PageController.showBusyIndicator(false) if (result) { diff --git a/client/ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml b/client/ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml index 442bf2ba2..848884475 100644 --- a/client/ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml +++ b/client/ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml @@ -119,8 +119,11 @@ PageType { if (fileName !== "") { PageController.showBusyIndicator(true) - SubscriptionUiController.exportVpnKey(ServersUiController.getServerId(ServersUiController.processedServerIndex), fileName) + let ok = SubscriptionUiController.exportVpnKey(ServersUiController.getServerId(ServersUiController.processedServerIndex), fileName) PageController.showBusyIndicator(false) + if (ok) { + PageController.showNotificationMessage(qsTr("Config file saved")) + } } } } From 0433e03bdcd7481cd9d725729f4388ca3f2bf506 Mon Sep 17 00:00:00 2001 From: MrMirDan <58086007+MrMirDan@users.noreply.github.com> Date: Fri, 15 May 2026 09:58:11 +0300 Subject: [PATCH 05/14] fix: amnezia free card button hovers when card enabled (#2602) --- client/ui/qml/Controls2/CardWithIconsType.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ui/qml/Controls2/CardWithIconsType.qml b/client/ui/qml/Controls2/CardWithIconsType.qml index 827a3950c..95f462302 100644 --- a/client/ui/qml/Controls2/CardWithIconsType.qml +++ b/client/ui/qml/Controls2/CardWithIconsType.qml @@ -188,7 +188,7 @@ Button { anchors.fill: parent radius: 12 - color: root.pressed ? rightImage.pressedColor : root.hovered ? rightImage.hoveredColor : rightImage.defaultColor + color: root.pressed ? rightImage.pressedColor : root.hovered && root.enabled ? rightImage.hoveredColor : rightImage.defaultColor Behavior on color { PropertyAnimation { duration: 200 } From 98771027b77951d9ab1243b6b71a83f524ad4ee3 Mon Sep 17 00:00:00 2001 From: MrMirDan <58086007+MrMirDan@users.noreply.github.com> Date: Fri, 15 May 2026 09:58:23 +0300 Subject: [PATCH 06/14] fix: vless switch between dividers (#2600) --- client/ui/qml/Pages2/PageSettingsApiServerInfo.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml index e1b8c7664..2dd6439c0 100644 --- a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml @@ -265,6 +265,7 @@ PageType { Layout.topMargin: 24 Layout.rightMargin: 16 Layout.leftMargin: 16 + Layout.bottomMargin: 24 visible: ApiAccountInfoModel.data("isProtocolSelectionSupported") enabled: !switcher.isProtocolSwitchBlocked From 2a3e3126ac2262cb5fab20d113c6d76b4af60092 Mon Sep 17 00:00:00 2001 From: yp Date: Fri, 15 May 2026 10:44:58 +0300 Subject: [PATCH 07/14] feat: regional country codes (#2567) Co-authored-by: vkamn --- client/core/utils/api/apiUtils.cpp | 16 ++++++++++++++++ client/core/utils/api/apiUtils.h | 3 +++ .../controllers/api/subscriptionUiController.cpp | 2 +- client/ui/controllers/serversUiController.cpp | 7 ++++++- client/ui/models/api/apiCountryModel.cpp | 3 ++- 5 files changed, 28 insertions(+), 3 deletions(-) diff --git a/client/core/utils/api/apiUtils.cpp b/client/core/utils/api/apiUtils.cpp index 60b78d565..4555d80dc 100644 --- a/client/core/utils/api/apiUtils.cpp +++ b/client/core/utils/api/apiUtils.cpp @@ -2,6 +2,7 @@ #include "core/utils/serverConfigUtils.h" #include "core/utils/constants/configKeys.h" +#include #include #include #include @@ -232,3 +233,18 @@ QString apiUtils::getPremiumV2VpnKey(const QJsonObject &serverConfigObject) return vpnKeyText; } + +QString apiUtils::countryCodeBaseForFlag(const QString &fullCountryCode) +{ + const QString trimmed = fullCountryCode.trimmed(); + if (trimmed.isEmpty()) { + return QString(); + } + const int dashIdx = trimmed.indexOf(QLatin1Char('-')); + const QString base = dashIdx < 0 ? trimmed : trimmed.left(dashIdx); + const QString normalized = base.trimmed(); + if (normalized.isEmpty()) { + return QString(); + } + return normalized.toUpper(); +} diff --git a/client/core/utils/api/apiUtils.h b/client/core/utils/api/apiUtils.h index e1ada61ae..c601ec895 100644 --- a/client/core/utils/api/apiUtils.h +++ b/client/core/utils/api/apiUtils.h @@ -25,6 +25,9 @@ namespace apiUtils QString getPremiumV1VpnKey(const QJsonObject &serverConfigObject); QString getPremiumV2VpnKey(const QJsonObject &serverConfigObject); + + // ISO2-style segment for flagKit assets (e.g. US-WEST -> US). Do not use in API request bodies. + QString countryCodeBaseForFlag(const QString &fullCountryCode); } #endif // APIUTILS_H diff --git a/client/ui/controllers/api/subscriptionUiController.cpp b/client/ui/controllers/api/subscriptionUiController.cpp index 05369d34d..581a10a2d 100644 --- a/client/ui/controllers/api/subscriptionUiController.cpp +++ b/client/ui/controllers/api/subscriptionUiController.cpp @@ -117,7 +117,7 @@ bool SubscriptionUiController::exportNativeConfig(const QString &serverId, const } const bool saved = SystemController::saveFile(fileName, nativeConfig); - getAccountInfo(serverIndex, true); + getAccountInfo(serverId, true); return saved; } diff --git a/client/ui/controllers/serversUiController.cpp b/client/ui/controllers/serversUiController.cpp index b9c2a9bf9..a5c0741f5 100644 --- a/client/ui/controllers/serversUiController.cpp +++ b/client/ui/controllers/serversUiController.cpp @@ -1,5 +1,6 @@ #include "serversUiController.h" +#include "core/utils/api/apiUtils.h" #include "core/utils/containerEnum.h" #include "core/utils/containers/containerUtils.h" #include "core/utils/protocolEnum.h" @@ -215,7 +216,11 @@ QString ServersUiController::getDefaultServerImagePathCollapsed() const if (!description.isApiV2 || description.apiServerCountryCode.isEmpty()) { return ""; } - return QString("qrc:/countriesFlags/images/flagKit/%1.svg").arg(description.apiServerCountryCode.toUpper()); + const QString imageCode = apiUtils::countryCodeBaseForFlag(description.apiServerCountryCode.toUpper()); + if (imageCode.isEmpty()) { + return QString(); + } + return QString("qrc:/countriesFlags/images/flagKit/%1.svg").arg(imageCode); } } return ""; diff --git a/client/ui/models/api/apiCountryModel.cpp b/client/ui/models/api/apiCountryModel.cpp index b0315f346..b1154e434 100644 --- a/client/ui/models/api/apiCountryModel.cpp +++ b/client/ui/models/api/apiCountryModel.cpp @@ -5,6 +5,7 @@ #include "core/utils/serverConfigUtils.h" #include "core/utils/constants/apiKeys.h" #include "core/utils/constants/apiConstants.h" +#include "core/utils/api/apiUtils.h" #include "logger.h" namespace @@ -41,7 +42,7 @@ QVariant ApiCountryModel::data(const QModelIndex &index, int role) const return countryInfo.countryName; } case CountryImageCodeRole: { - return countryInfo.countryCode.toUpper(); + return apiUtils::countryCodeBaseForFlag(countryInfo.countryCode); } case IsIssuedRole: { return isIssued; From c9ed0baf3b0b98d2884337ee2c40d1a4fe0eb0b3 Mon Sep 17 00:00:00 2001 From: MrMirDan <58086007+MrMirDan@users.noreply.github.com> Date: Fri, 15 May 2026 16:01:39 +0300 Subject: [PATCH 08/14] fix: app freezes when revoke awg/wg client during active connection (#2211) * block configs revoke during connection * update: check that current config is active * update: notification text --- client/ui/qml/Pages2/PageShare.qml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/client/ui/qml/Pages2/PageShare.qml b/client/ui/qml/Pages2/PageShare.qml index 9dab7a5fa..0ea626ef0 100644 --- a/client/ui/qml/Pages2/PageShare.qml +++ b/client/ui/qml/Pages2/PageShare.qml @@ -835,7 +835,15 @@ PageType { var noButtonFunction = function() { } - showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + var isActiveConfigForCurrentClient = ServersModel.isDefaultServerCurrentlyProcessed() + && ServersModel.getDefaultServerData("defaultContainer") === ContainersModel.getProcessedContainerIndex() + + if ((ConnectionController.isConnectionInProgress || ConnectionController.isConnected) + && isActiveConfigForCurrentClient) { + PageController.showNotificationMessage("Unable to revoke current config during active connection") + } else { + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + } } } } From c7b1c2809fb1c555587190dacff225b9fb538e48 Mon Sep 17 00:00:00 2001 From: MrMirDan <58086007+MrMirDan@users.noreply.github.com> Date: Fri, 15 May 2026 16:02:09 +0300 Subject: [PATCH 09/14] fix: app buttons clicked instead of buttons in context menu (#2200) * fix: app buttons clicked instead of buttons in context menu * update: using MouseArea instead of changing popupType * fix(cursor): fixed cursor type at opened context menu --------- Co-authored-by: Mitternacht822 --- client/ui/qml/Controls2/ContextMenuType.qml | 10 ++++++++++ client/ui/qml/Controls2/TextAreaType.qml | 3 ++- client/ui/qml/Controls2/TextAreaWithFooterType.qml | 3 ++- client/ui/qml/Controls2/TextFieldWithHeaderType.qml | 3 ++- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/client/ui/qml/Controls2/ContextMenuType.qml b/client/ui/qml/Controls2/ContextMenuType.qml index cb32e3118..6b3c3ae1c 100644 --- a/client/ui/qml/Controls2/ContextMenuType.qml +++ b/client/ui/qml/Controls2/ContextMenuType.qml @@ -6,6 +6,9 @@ Menu { popupType: Popup.Native + onAboutToShow: blocker.enabled = true + onClosed: blocker.enabled = false + MenuItem { text: qsTr("C&ut") enabled: textObj.selectedText @@ -28,4 +31,11 @@ Menu { enabled: textObj.length > 0 onTriggered: textObj.selectAll() } + + MouseArea { + id: blocker + z: 2 + enabled: false + preventStealing: true + } } diff --git a/client/ui/qml/Controls2/TextAreaType.qml b/client/ui/qml/Controls2/TextAreaType.qml index 3200e1f35..92d104f4e 100644 --- a/client/ui/qml/Controls2/TextAreaType.qml +++ b/client/ui/qml/Controls2/TextAreaType.qml @@ -24,7 +24,7 @@ Rectangle { MouseArea { id: parentMouse anchors.fill: parent - cursorShape: Qt.IBeamCursor + cursorShape: contextMenu.opened ? Qt.ArrowCursor : Qt.IBeamCursor onClicked: textArea.forceActiveFocus() hoverEnabled: true @@ -94,6 +94,7 @@ Rectangle { wrapMode: Text.Wrap ContextMenu.menu: ContextMenuType { + id: contextMenu textObj: textArea } diff --git a/client/ui/qml/Controls2/TextAreaWithFooterType.qml b/client/ui/qml/Controls2/TextAreaWithFooterType.qml index f54a0688e..a46e3f117 100644 --- a/client/ui/qml/Controls2/TextAreaWithFooterType.qml +++ b/client/ui/qml/Controls2/TextAreaWithFooterType.qml @@ -34,7 +34,7 @@ Rectangle { MouseArea { id: parentMouse anchors.fill: parent - cursorShape: Qt.IBeamCursor + cursorShape: contextMenu.opened ? Qt.ArrowCursor : Qt.IBeamCursor onClicked: textArea.forceActiveFocus() hoverEnabled: true @@ -80,6 +80,7 @@ Rectangle { wrapMode: Text.Wrap ContextMenu.menu: ContextMenuType { + id: contextMenu textObj: textArea } diff --git a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml index 897584303..e192535ac 100644 --- a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml +++ b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml @@ -135,6 +135,7 @@ Item { } ContextMenu.menu: ContextMenuType { + id: contextMenu textObj: textField } @@ -159,7 +160,7 @@ Item { MouseArea { anchors.fill: root - cursorShape: Qt.IBeamCursor + cursorShape: contextMenu.opened ? Qt.ArrowCursor : Qt.IBeamCursor hoverEnabled: true From f0299ca9fee65252cb5971eab79128c47291a2da Mon Sep 17 00:00:00 2001 From: lunardunno <126363523+lunardunno@users.noreply.github.com> Date: Mon, 18 May 2026 07:55:07 +0400 Subject: [PATCH 10/14] chore: authentication prompt in Ubuntu 26.04 (#2603) Handling the password prompt in Ubuntu 26.04 --- client/core/controllers/selfhosted/installController.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/core/controllers/selfhosted/installController.cpp b/client/core/controllers/selfhosted/installController.cpp index 2a45ba789..f4c318161 100644 --- a/client/core/controllers/selfhosted/installController.cpp +++ b/client/core/controllers/selfhosted/installController.cpp @@ -654,7 +654,7 @@ ErrorCode InstallController::isUserInSudo(const ServerCredentials &credentials, return ErrorCode::ServerUserDirectoryNotAccessible; if (stdOut.contains("sudoers") || stdOut.contains("is not allowed to run sudo on")) return ErrorCode::ServerUserNotAllowedInSudoers; - if (stdOut.contains("password is required")) + if (stdOut.contains("password is required") || stdOut.contains("authentication is required")) return ErrorCode::ServerUserPasswordRequired; return error; From 8c33779fc3a588eef85b37b01242709a9d39122c Mon Sep 17 00:00:00 2001 From: lunardunno <126363523+lunardunno@users.noreply.github.com> Date: Mon, 18 May 2026 09:56:57 +0400 Subject: [PATCH 11/14] chore: Install recommends for apt (#2596) --- client/server_scripts/install_docker.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/server_scripts/install_docker.sh b/client/server_scripts/install_docker.sh index 3bd94c004..1133aee87 100644 --- a/client/server_scripts/install_docker.sh +++ b/client/server_scripts/install_docker.sh @@ -1,4 +1,4 @@ -if which apt-get > /dev/null 2>&1; then pm=$(which apt-get); silent_inst="-yq install"; check_pkgs="-yq update"; docker_pkg="docker.io"; dist="debian";\ +if which apt-get > /dev/null 2>&1; then pm=$(which apt-get); silent_inst="-yq install --install-recommends"; check_pkgs="-yq update"; docker_pkg="docker.io"; dist="debian";\ elif which dnf > /dev/null 2>&1; then pm=$(which dnf); silent_inst="-yq install"; check_pkgs="-yq check-update"; docker_pkg="docker"; dist="fedora";\ elif which yum > /dev/null 2>&1; then pm=$(which yum); silent_inst="-y -q install"; check_pkgs="-y -q check-update"; docker_pkg="docker"; dist="centos";\ elif which zypper > /dev/null 2>&1; then pm=$(which zypper); silent_inst="-nq install"; check_pkgs="-nq refresh"; docker_pkg="docker"; dist="opensuse";\ From 277b295fd863822ee1548747ba6ea61afd983e7b Mon Sep 17 00:00:00 2001 From: yp Date: Mon, 18 May 2026 14:52:58 +0300 Subject: [PATCH 12/14] feat: add mtproxy(#2370) * Feat: Add MtProxy (Telegram) * add path files * refactor: move logic from ui to core --------- Co-authored-by: vkamn --- client/cmake/sources.cmake | 4 + client/core/controllers/coreController.cpp | 9 +- client/core/controllers/coreController.h | 5 + .../selfhosted/installController.cpp | 209 +- .../selfhosted/installController.h | 11 + .../core/diagnostics/containerDiagnostics.h | 16 + client/core/diagnostics/mtProxyDiagnostics.h | 18 + client/core/installers/installerBase.cpp | 7 + client/core/installers/mtProxyInstaller.cpp | 130 ++ client/core/installers/mtProxyInstaller.h | 34 + client/core/models/containerConfig.cpp | 10 + client/core/models/containerConfig.h | 3 + client/core/models/protocolConfig.cpp | 9 + client/core/models/protocolConfig.h | 2 + .../protocols/mtProxyProtocolConfig.cpp | 147 ++ .../models/protocols/mtProxyProtocolConfig.h | 38 + client/core/protocols/protocolUtils.cpp | 11 +- client/core/utils/constants/configKeys.h | 1 + .../core/utils/constants/protocolConstants.h | 33 +- client/core/utils/containerEnum.h | 3 +- .../core/utils/containers/containerUtils.cpp | 25 +- client/core/utils/protocolEnum.h | 3 +- .../core/utils/selfhosted/scriptsRegistry.cpp | 55 +- .../core/utils/selfhosted/scriptsRegistry.h | 1 + client/core/utils/selfhosted/sshClient.cpp | 6 +- client/core/utils/selfhosted/sshSession.cpp | 4 +- client/server_scripts/mtproxy/Dockerfile | 9 + .../mtproxy/configure_container.sh | 60 + .../server_scripts/mtproxy/run_container.sh | 9 + client/server_scripts/mtproxy/start.sh | 71 + client/server_scripts/serverScripts.qrc | 5 +- .../networkReachabilityController.cpp | 46 + .../networkReachabilityController.h | 30 + client/ui/controllers/qml/pageController.h | 1 + .../selfhosted/exportUiController.cpp | 9 + .../selfhosted/exportUiController.h | 1 + .../selfhosted/installUiController.cpp | 111 +- .../selfhosted/installUiController.h | 18 +- client/ui/controllers/serversUiController.cpp | 2 + client/ui/models/containersModel.cpp | 2 + client/ui/models/containersModel.h | 3 +- client/ui/models/protocolsModel.cpp | 3 + client/ui/models/protocolsModel.h | 3 +- .../ui/models/services/mtProxyConfigModel.cpp | 714 +++++++ .../ui/models/services/mtProxyConfigModel.h | 156 ++ .../utils/mtproxy_public_host_input.cpp | 127 ++ .../models/utils/mtproxy_public_host_input.h | 20 + .../Components/SettingsContainersListView.qml | 3 + .../Controls2/ListViewWithRadioButtonType.qml | 3 + .../qml/Pages2/PageServiceMtProxySettings.qml | 1885 +++++++++++++++++ client/ui/qml/Pages2/PageStart.qml | 6 +- client/ui/qml/qml.qrc | 1 + 52 files changed, 4062 insertions(+), 30 deletions(-) create mode 100644 client/core/diagnostics/containerDiagnostics.h create mode 100644 client/core/diagnostics/mtProxyDiagnostics.h create mode 100644 client/core/installers/mtProxyInstaller.cpp create mode 100644 client/core/installers/mtProxyInstaller.h create mode 100644 client/core/models/protocols/mtProxyProtocolConfig.cpp create mode 100644 client/core/models/protocols/mtProxyProtocolConfig.h create mode 100644 client/server_scripts/mtproxy/Dockerfile create mode 100644 client/server_scripts/mtproxy/configure_container.sh create mode 100644 client/server_scripts/mtproxy/run_container.sh create mode 100644 client/server_scripts/mtproxy/start.sh create mode 100644 client/ui/controllers/networkReachabilityController.cpp create mode 100644 client/ui/controllers/networkReachabilityController.h create mode 100644 client/ui/models/services/mtProxyConfigModel.cpp create mode 100644 client/ui/models/services/mtProxyConfigModel.h create mode 100644 client/ui/models/utils/mtproxy_public_host_input.cpp create mode 100644 client/ui/models/utils/mtproxy_public_host_input.h create mode 100644 client/ui/qml/Pages2/PageServiceMtProxySettings.qml diff --git a/client/cmake/sources.cmake b/client/cmake/sources.cmake index 7c5767549..8afd4a5fd 100644 --- a/client/cmake/sources.cmake +++ b/client/cmake/sources.cmake @@ -35,6 +35,7 @@ set(HEADERS ${HEADERS} ${CLIENT_ROOT_DIR}/core/installers/torInstaller.h ${CLIENT_ROOT_DIR}/core/installers/sftpInstaller.h ${CLIENT_ROOT_DIR}/core/installers/socks5Installer.h + ${CLIENT_ROOT_DIR}/core/installers/mtProxyInstaller.h ${CLIENT_ROOT_DIR}/core/controllers/appSplitTunnelingController.h ${CLIENT_ROOT_DIR}/core/controllers/ipSplitTunnelingController.h ${CLIENT_ROOT_DIR}/core/controllers/allowedDnsController.h @@ -110,6 +111,7 @@ set(SOURCES ${SOURCES} ${CLIENT_ROOT_DIR}/core/installers/torInstaller.cpp ${CLIENT_ROOT_DIR}/core/installers/sftpInstaller.cpp ${CLIENT_ROOT_DIR}/core/installers/socks5Installer.cpp + ${CLIENT_ROOT_DIR}/core/installers/mtProxyInstaller.cpp ${CLIENT_ROOT_DIR}/core/controllers/appSplitTunnelingController.cpp ${CLIENT_ROOT_DIR}/core/controllers/ipSplitTunnelingController.cpp ${CLIENT_ROOT_DIR}/core/controllers/allowedDnsController.cpp @@ -201,12 +203,14 @@ file(GLOB UI_MODELS_H CONFIGURE_DEPENDS ${CLIENT_ROOT_DIR}/ui/models/*.h ${CLIENT_ROOT_DIR}/ui/models/protocols/*.h ${CLIENT_ROOT_DIR}/ui/models/services/*.h + ${CLIENT_ROOT_DIR}/ui/models/utils/*.h ${CLIENT_ROOT_DIR}/ui/models/api/*.h ) file(GLOB UI_MODELS_CPP CONFIGURE_DEPENDS ${CLIENT_ROOT_DIR}/ui/models/*.cpp ${CLIENT_ROOT_DIR}/ui/models/protocols/*.cpp ${CLIENT_ROOT_DIR}/ui/models/services/*.cpp + ${CLIENT_ROOT_DIR}/ui/models/utils/*.cpp ${CLIENT_ROOT_DIR}/ui/models/api/*.cpp ) diff --git a/client/core/controllers/coreController.cpp b/client/core/controllers/coreController.cpp index b23f98180..3240f6b5c 100644 --- a/client/core/controllers/coreController.cpp +++ b/client/core/controllers/coreController.cpp @@ -100,6 +100,9 @@ void CoreController::initModels() m_socks5ConfigModel = new Socks5ProxyConfigModel(this); setQmlContextProperty("Socks5ProxyConfigModel", m_socks5ConfigModel); + m_mtProxyConfigModel = new MtProxyConfigModel(this); + setQmlContextProperty("MtProxyConfigModel", m_mtProxyConfigModel); + m_clientManagementModel = new ClientManagementModel(this); setQmlContextProperty("ClientManagementModel", m_clientManagementModel); @@ -169,7 +172,7 @@ void CoreController::initControllers() #ifdef Q_OS_WINDOWS m_ikev2ConfigModel, #endif - m_sftpConfigModel, m_socks5ConfigModel, this); + m_sftpConfigModel, m_socks5ConfigModel, m_mtProxyConfigModel, this); setQmlContextProperty("InstallController", m_installUiController); m_importController = new ImportUiController(m_importCoreController, this); @@ -202,6 +205,10 @@ void CoreController::initControllers() m_systemController = new SystemController(this); setQmlContextProperty("SystemController", m_systemController); + m_networkReachabilityController = new NetworkReachabilityController(this); + m_engine->rootContext()->setContextProperty("NetworkReachabilityController", m_networkReachabilityController); + m_engine->rootContext()->setContextProperty("NetworkReachability", m_networkReachabilityController); + m_servicesCatalogUiController = new ServicesCatalogUiController(m_servicesCatalogController, m_apiServicesModel, this); setQmlContextProperty("ServicesCatalogUiController", m_servicesCatalogUiController); diff --git a/client/core/controllers/coreController.h b/client/core/controllers/coreController.h index 4b3ed2831..64645ab02 100644 --- a/client/core/controllers/coreController.h +++ b/client/core/controllers/coreController.h @@ -28,6 +28,7 @@ #include "ui/controllers/languageUiController.h" #include "ui/controllers/updateUiController.h" #include "ui/controllers/api/servicesCatalogUiController.h" +#include "ui/controllers/networkReachabilityController.h" #include "core/controllers/serversController.h" #include "core/controllers/selfhosted/usersController.h" @@ -69,6 +70,8 @@ #include "ui/models/serversModel.h" #include "ui/models/services/sftpConfigModel.h" #include "ui/models/services/socks5ProxyConfigModel.h" +#include "ui/models/services/mtProxyConfigModel.h" + #include "ui/models/ipSplitTunnelingModel.h" #include "ui/models/newsModel.h" @@ -156,6 +159,7 @@ private: ServersUiController* m_serversUiController; IpSplitTunnelingUiController* m_ipSplitTunnelingUiController; SystemController* m_systemController; + NetworkReachabilityController* m_networkReachabilityController; AppSplitTunnelingUiController* m_appSplitTunnelingUiController; AllowedDnsUiController* m_allowedDnsUiController; LanguageUiController* m_languageUiController; @@ -208,6 +212,7 @@ private: #endif SftpConfigModel* m_sftpConfigModel; Socks5ProxyConfigModel* m_socks5ConfigModel; + MtProxyConfigModel* m_mtProxyConfigModel; CoreSignalHandlers* m_signalHandlers; }; diff --git a/client/core/controllers/selfhosted/installController.cpp b/client/core/controllers/selfhosted/installController.cpp index f4c318161..a9035b59f 100644 --- a/client/core/controllers/selfhosted/installController.cpp +++ b/client/core/controllers/selfhosted/installController.cpp @@ -19,6 +19,7 @@ #include "core/installers/openvpnInstaller.h" #include "core/installers/sftpInstaller.h" #include "core/installers/socks5Installer.h" +#include "core/installers/mtProxyInstaller.h" #include "core/installers/torInstaller.h" #include "core/installers/wireguardInstaller.h" #include "core/installers/xrayInstaller.h" @@ -34,6 +35,7 @@ #include "core/utils/constants/configKeys.h" #include "core/utils/constants/protocolConstants.h" #include "core/models/containerConfig.h" +#include "core/models/protocols/mtProxyProtocolConfig.h" #include "core/models/protocols/awgProtocolConfig.h" #include "ui/models/protocols/wireguardConfigModel.h" #include "core/utils/utilities.h" @@ -53,6 +55,21 @@ using namespace ProtocolUtils; namespace { Logger logger("InstallController"); + + bool dockerDaemonContainerMissing(const QString &out, const QString &containerDockerName) + { + if (!out.contains(QLatin1String("Error response from daemon"), Qt::CaseInsensitive)) { + return false; + } + if (out.contains(QLatin1String("No such container"), Qt::CaseInsensitive) + && out.contains(containerDockerName, Qt::CaseInsensitive)) { + return true; + } + if (out.size() < 700 && out.contains(QLatin1String("is not running"), Qt::CaseInsensitive)) { + return true; + } + return false; + } } InstallController::InstallController(SecureServersRepository *serversRepository, @@ -136,6 +153,11 @@ ErrorCode InstallController::updateContainer(const QString &serverId, DockerCont if (!adminConfig.has_value()) { return ErrorCode::InternalError; } + if (container == DockerContainer::MtProxy) { + ServerCredentials credentials = adminConfig->credentials(); + SshSession sshSession(this); + MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig); + } adminConfig->updateContainerConfig(container, newConfig); m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin); return ErrorCode::NoError; @@ -165,6 +187,9 @@ ErrorCode InstallController::updateContainer(const QString &serverId, DockerCont } if (errorCode == ErrorCode::NoError) { + if (container == DockerContainer::MtProxy) { + MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig); + } clearCachedProfile(serverId, container); adminConfig->updateContainerConfig(container, newConfig); m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin); @@ -408,9 +433,22 @@ ErrorCode InstallController::configureContainerWorker(const ServerCredentials &c sshSession.replaceVars(amnezia::scriptData(ProtocolScriptType::configure_container, container), baseVars), cbReadStdOut, cbReadStdErr); + if (e != ErrorCode::NoError) { + return e; + } + + if (dockerDaemonContainerMissing(stdOut, ContainerUtils::containerToString(container))) { + qDebug() << "configureContainerWorker: Docker daemon reports container missing/stopped, output:" << stdOut; + return ErrorCode::ServerContainerMissingError; + } + updateContainerConfigAfterInstallation(container, config, stdOut); - return e; + if (container == DockerContainer::MtProxy) { + MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, config); + } + + return ErrorCode::NoError; } ErrorCode InstallController::startupContainerWorker(const ServerCredentials &credentials, DockerContainer container, const ContainerConfig &config, SshSession &sshSession) @@ -563,6 +601,32 @@ bool InstallController::isReinstallContainerRequired(DockerContainer container, } } + if (container == DockerContainer::MtProxy) { + const auto *oldMt = oldConfig.getMtProxyProtocolConfig(); + const auto *newMt = newConfig.getMtProxyProtocolConfig(); + if (oldMt && newMt) { + const QString oldPort = + oldMt->port.isEmpty() ? QString(protocols::mtProxy::defaultPort) : oldMt->port; + const QString newPort = + newMt->port.isEmpty() ? QString(protocols::mtProxy::defaultPort) : newMt->port; + if (oldPort != newPort) { + return true; + } + const QString oldTransport = oldMt->transportMode.isEmpty() ? QString( + protocols::mtProxy::transportModeStandard) + : oldMt->transportMode; + const QString newTransport = newMt->transportMode.isEmpty() ? QString( + protocols::mtProxy::transportModeStandard) + : newMt->transportMode; + if (oldTransport != newTransport) { + return true; + } + if (oldMt->tlsDomain != newMt->tlsDomain) { + return true; + } + } + } + if (container == DockerContainer::Socks5Proxy) { return true; } @@ -823,6 +887,7 @@ QScopedPointer InstallController::createInstaller(DockerContainer case DockerContainer::TorWebSite: return QScopedPointer(new TorInstaller(this)); case DockerContainer::Sftp: return QScopedPointer(new SftpInstaller(this)); case DockerContainer::Socks5Proxy: return QScopedPointer(new Socks5Installer(this)); + case DockerContainer::MtProxy: return QScopedPointer(new MtProxyInstaller(this)); default: return QScopedPointer(new InstallerBase(this)); } } @@ -861,6 +926,13 @@ bool InstallController::isUpdateDockerContainerRequired(DockerContainer containe return false; } } + } else if (container == DockerContainer::MtProxy) { + const auto *oldMt = oldConfig.getMtProxyProtocolConfig(); + const auto *newMt = newConfig.getMtProxyProtocolConfig(); + if (!oldMt || !newMt) { + return true; + } + return !oldMt->equalsDockerDeploymentSettings(*newMt); } return true; @@ -1164,6 +1236,31 @@ void InstallController::updateContainerConfigAfterInstallation(DockerContainer c onion.replace("\n", ""); torProtocolConfig->serverConfig.site = onion; } + } else if (container == DockerContainer::MtProxy) { + if (auto* mtProxyConfig = containerConfig.getMtProxyProtocolConfig()) { + qDebug() << "amnezia mtproxy" << stdOut; + + static const QRegularExpression reSecret( + QStringLiteral(R"(\[\*\]\s+Secret:\s+([0-9a-fA-F]{32}))"), + QRegularExpression::CaseInsensitiveOption); + static const QRegularExpression reTgLink(QStringLiteral(R"(\[\*\]\s+tg://\s+link:\s+(tg://proxy\?[^\s]+))")); + static const QRegularExpression reTmeLink( + QStringLiteral(R"(\[\*\]\s+t\.me\s+link:\s+(https://t\.me/proxy\?[^\s]+))")); + + const QRegularExpressionMatch mSecret = reSecret.match(stdOut); + const QRegularExpressionMatch mTgLink = reTgLink.match(stdOut); + const QRegularExpressionMatch mTmeLink = reTmeLink.match(stdOut); + + if (mSecret.hasMatch()) { + mtProxyConfig->secret = mSecret.captured(1); + } + if (mTgLink.hasMatch()) { + mtProxyConfig->tgLink = mTgLink.captured(1); + } + if (mTmeLink.hasMatch()) { + mtProxyConfig->tmeLink = mTmeLink.captured(1); + } + } } } @@ -1248,3 +1345,113 @@ ErrorCode InstallController::getAlreadyInstalledContainers(const ServerCredentia return ErrorCode::NoError; } + +ErrorCode InstallController::setDockerContainerEnabledState(const QString &serverId, DockerContainer container, bool enabled) +{ + auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId); + if (!adminConfig.has_value()) { + return ErrorCode::InternalError; + } + ServerCredentials credentials = adminConfig->credentials(); + if (!credentials.isValid()) { + return ErrorCode::InternalError; + } + const QString containerName = ContainerUtils::containerToString(container); + SshSession sshSession(this); + const QString script = enabled ? QStringLiteral("sudo docker start %1").arg(containerName) + : QStringLiteral("sudo docker stop %1").arg(containerName); + const ErrorCode runError = sshSession.runScript(credentials, script); + if (runError != ErrorCode::NoError) { + return runError; + } + ContainerConfig currentConfig = adminConfig->containerConfig(container); + if (auto *mtConfig = currentConfig.getMtProxyProtocolConfig()) { + mtConfig->isEnabled = enabled; + adminConfig->updateContainerConfig(container, currentConfig); + m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin); + } + return ErrorCode::NoError; +} + +ErrorCode InstallController::queryDockerContainerStatus(const QString &serverId, DockerContainer container, int &statusOut) +{ + statusOut = 3; + auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId); + if (!adminConfig.has_value()) { + return ErrorCode::InternalError; + } + ServerCredentials credentials = adminConfig->credentials(); + if (!credentials.isValid()) { + return ErrorCode::InternalError; + } + const QString containerName = ContainerUtils::containerToString(container); + QString stdOut; + auto cbReadStdOut = [&](const QString &data, libssh::Client &) { + stdOut += data; + return ErrorCode::NoError; + }; + SshSession sshSession(this); + const QString script = QStringLiteral( + "sudo docker inspect --format '{{.State.Status}}' %1 2>/dev/null || echo 'not_found'") + .arg(containerName); + const ErrorCode errorCode = sshSession.runScript(credentials, script, cbReadStdOut); + if (errorCode != ErrorCode::NoError) { + return errorCode; + } + const QString status = stdOut.trimmed(); + if (status == QLatin1String("running")) { + statusOut = 1; + } else if (status == QLatin1String("not_found") || status.isEmpty()) { + statusOut = 0; + } else if (status == QLatin1String("exited") || status == QLatin1String("created") + || status == QLatin1String("paused")) { + statusOut = 2; + } else { + statusOut = 3; + } + return ErrorCode::NoError; +} + +ErrorCode InstallController::queryMtProxyDiagnostics(const QString &serverId, DockerContainer container, int listenPort, + MtProxyContainerDiagnostics &out) +{ + out = {}; + auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId); + if (!adminConfig.has_value()) { + return ErrorCode::InternalError; + } + ServerCredentials credentials = adminConfig->credentials(); + if (!credentials.isValid()) { + return ErrorCode::InternalError; + } + SshSession sshSession(this); + return MtProxyInstaller::queryDiagnostics(sshSession, credentials, container, listenPort, out); +} + +QString InstallController::fetchDockerContainerSecret(const QString &serverId, DockerContainer container) +{ + auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId); + if (!adminConfig.has_value()) { + return {}; + } + ServerCredentials credentials = adminConfig->credentials(); + if (!credentials.isValid()) { + return {}; + } + const QString containerName = ContainerUtils::containerToString(container); + QString stdOut; + auto cbReadStdOut = [&](const QString &data, libssh::Client &) { + stdOut += data; + return ErrorCode::NoError; + }; + SshSession sshSession(this); + const QString path = QStringLiteral("/data/secret"); + const QString cmd = QStringLiteral("sudo docker exec %1 cat %2").arg(containerName, path); + const ErrorCode errorCode = sshSession.runScript(credentials, cmd, cbReadStdOut); + if (errorCode != ErrorCode::NoError) { + return {}; + } + const QString secret = stdOut.trimmed(); + static const QRegularExpression hex32(QStringLiteral("^[0-9a-fA-F]{32}$")); + return hex32.match(secret).hasMatch() ? secret : QString(); +} diff --git a/client/core/controllers/selfhosted/installController.h b/client/core/controllers/selfhosted/installController.h index acb61ca41..25c041273 100644 --- a/client/core/controllers/selfhosted/installController.h +++ b/client/core/controllers/selfhosted/installController.h @@ -16,6 +16,7 @@ #include "core/models/containerConfig.h" #include "core/repositories/secureServersRepository.h" #include "core/repositories/secureAppSettingsRepository.h" +#include "core/installers/mtProxyInstaller.h" class SshSession; class InstallerBase; @@ -39,6 +40,16 @@ public: ErrorCode removeAllContainers(const QString &serverId); ErrorCode removeContainer(const QString &serverId, DockerContainer container); + ErrorCode setDockerContainerEnabledState(const QString &serverId, DockerContainer container, bool enabled); + + /// statusOut: 0 = not deployed, 1 = running, 2 = stopped, 3 = error + ErrorCode queryDockerContainerStatus(const QString &serverId, DockerContainer container, int &statusOut); + + ErrorCode queryMtProxyDiagnostics(const QString &serverId, DockerContainer container, int listenPort, + MtProxyContainerDiagnostics &out); + + QString fetchDockerContainerSecret(const QString &serverId, DockerContainer container); + ContainerConfig generateConfig(DockerContainer container, int port, TransportProto transportProto); ErrorCode getAlreadyInstalledContainers(const ServerCredentials &credentials, QMap &installedContainers, SshSession &sshSession); diff --git a/client/core/diagnostics/containerDiagnostics.h b/client/core/diagnostics/containerDiagnostics.h new file mode 100644 index 000000000..7833cc332 --- /dev/null +++ b/client/core/diagnostics/containerDiagnostics.h @@ -0,0 +1,16 @@ +#ifndef CONTAINERDIAGNOSTICS_H +#define CONTAINERDIAGNOSTICS_H + +namespace amnezia +{ + struct ContainerDiagnostics + { + bool available = false; + bool portReachable = false; + + virtual ~ContainerDiagnostics() = default; + }; + +} // namespace amnezia + +#endif // CONTAINERDIAGNOSTICS_H diff --git a/client/core/diagnostics/mtProxyDiagnostics.h b/client/core/diagnostics/mtProxyDiagnostics.h new file mode 100644 index 000000000..d738a2274 --- /dev/null +++ b/client/core/diagnostics/mtProxyDiagnostics.h @@ -0,0 +1,18 @@ +#ifndef MTPROXYDIAGNOSTICS_H +#define MTPROXYDIAGNOSTICS_H + +#include "containerDiagnostics.h" + +#include + +namespace amnezia { + struct MtProxyDiagnostics : ContainerDiagnostics { + bool upstreamReachable = false; + int clientsConnected = -1; + QString lastConfigRefresh; + QString statsEndpoint; + }; + +} // namespace amnezia + +#endif // MTPROXYDIAGNOSTICS_H diff --git a/client/core/installers/installerBase.cpp b/client/core/installers/installerBase.cpp index 5bf9caf05..d4243a5f4 100644 --- a/client/core/installers/installerBase.cpp +++ b/client/core/installers/installerBase.cpp @@ -14,6 +14,7 @@ #include "core/models/protocols/xrayProtocolConfig.h" #include "core/models/protocols/sftpProtocolConfig.h" #include "core/models/protocols/socks5ProxyProtocolConfig.h" +#include "core/models/protocols/mtProxyProtocolConfig.h" #include "core/models/protocols/ikev2ProtocolConfig.h" #include "core/models/protocols/torProtocolConfig.h" @@ -91,6 +92,12 @@ ContainerConfig InstallerBase::createBaseConfig(DockerContainer container, int p config.protocolConfig = socks5Config; break; } + case Proto::MtProxy: { + MtProxyProtocolConfig mtConfig; + mtConfig.port = portStr; + config.protocolConfig = mtConfig; + break; + } case Proto::Ikev2: { Ikev2ProtocolConfig ikev2Config; config.protocolConfig = ikev2Config; diff --git a/client/core/installers/mtProxyInstaller.cpp b/client/core/installers/mtProxyInstaller.cpp new file mode 100644 index 000000000..21ac5bd4a --- /dev/null +++ b/client/core/installers/mtProxyInstaller.cpp @@ -0,0 +1,130 @@ +#include "mtProxyInstaller.h" + +#include "core/utils/containerEnum.h" +#include "core/utils/containers/containerUtils.h" +#include "core/utils/protocolEnum.h" +#include "core/utils/selfhosted/sshSession.h" +#include "core/models/containerConfig.h" +#include "core/models/protocols/mtProxyProtocolConfig.h" + +#include +#include +#include +#include + +#include + +using namespace amnezia; + +namespace { + constexpr QLatin1String kMtProxyClientJsonPath("/data/amnezia-mtproxy-client.json"); + constexpr QLatin1String kMtProxyClientJsonUploadPath("data/amnezia-mtproxy-client.json"); + constexpr QLatin1String kMtProxySecretPath("/data/secret"); +} + +MtProxyInstaller::MtProxyInstaller(QObject *parent) + : InstallerBase(parent) { +} + +ErrorCode MtProxyInstaller::extractConfigFromContainer(DockerContainer container, const ServerCredentials &credentials, + SshSession *sshSession, ContainerConfig &config) { + if (container != DockerContainer::MtProxy || !sshSession) { + return ErrorCode::NoError; + } + + MtProxyProtocolConfig *mt = config.getMtProxyProtocolConfig(); + if (!mt) { + return ErrorCode::NoError; + } + + ErrorCode jsonErr = ErrorCode::NoError; + const QByteArray jsonRaw = + sshSession->getTextFileFromContainer(container, credentials, QString(kMtProxyClientJsonPath), jsonErr); + if (jsonErr == ErrorCode::NoError && !jsonRaw.trimmed().isEmpty()) { + QJsonParseError parseError; + const QJsonDocument doc = QJsonDocument::fromJson(jsonRaw.trimmed(), &parseError); + if (parseError.error == QJsonParseError::NoError && doc.isObject()) { + QJsonObject merged = mt->toJson(); + const QJsonObject snap = doc.object(); + for (auto it = snap.constBegin(); it != snap.constEnd(); ++it) { + merged.insert(it.key(), it.value()); + } + *mt = MtProxyProtocolConfig::fromJson(merged); + } + } + + ErrorCode secretErr = ErrorCode::NoError; + const QByteArray secretRaw = + sshSession->getTextFileFromContainer(container, credentials, QString(kMtProxySecretPath), secretErr); + const QString sec = QString::fromUtf8(secretRaw).trimmed(); + if (sec.length() == 32) { + static const QRegularExpression hex32(QStringLiteral("^[0-9a-fA-F]{32}$")); + if (hex32.match(sec).hasMatch()) { + mt->secret = sec; + } + } + + return ErrorCode::NoError; +} + +ErrorCode MtProxyInstaller::queryDiagnostics(SshSession &sshSession, const ServerCredentials &credentials, + DockerContainer container, int listenPort, + MtProxyContainerDiagnostics &out) +{ + out = {}; + if (container != DockerContainer::MtProxy) { + return ErrorCode::InternalError; + } + const QString containerName = ContainerUtils::containerToString(container); + const QString script = + QStringLiteral( + "PORT_OK=$(sudo docker exec %1 sh -c 'ss -tlnp 2>/dev/null | grep -q :%2 && echo yes || echo no' 2>/dev/null || echo no); " + "TG_OK=$(curl -s --max-time 5 -o /dev/null -w '%%{http_code}' https://core.telegram.org/getProxySecret 2>/dev/null | grep -q '200' && echo yes || echo no); " + "CLIENTS=$(sudo docker exec amnezia-mtproxy sh -c 'curl -s --max-time 3 http://localhost:2398/stats 2>/dev/null | grep -o \"total_special_connections:[0-9]*\" | cut -d: -f2' 2>/dev/null); " + "CONF_TIME=$(sudo docker exec amnezia-mtproxy sh -c 'stat -c \"%%y\" /data/proxy-multi.conf 2>/dev/null | cut -d. -f1' 2>/dev/null || echo unknown); " + "echo \"PORT_OK=${PORT_OK}\"; " + "echo \"TG_OK=${TG_OK}\"; " + "echo \"CLIENTS=${CLIENTS:-0}\"; " + "echo \"CONF_TIME=${CONF_TIME}\"; " + "echo \"STATS=http://localhost:2398/stats\";") + .arg(containerName) + .arg(listenPort); + + QString stdOut; + auto cbReadStdOut = [&](const QString &data, libssh::Client &) { + stdOut += data; + return ErrorCode::NoError; + }; + const ErrorCode errorCode = sshSession.runScript(credentials, script, cbReadStdOut); + if (errorCode != ErrorCode::NoError) { + return errorCode; + } + for (const QString &line : stdOut.split('\n', Qt::SkipEmptyParts)) { + if (line.startsWith(QLatin1String("PORT_OK="))) { + out.portReachable = line.mid(8).trimmed() == QLatin1String("yes"); + } else if (line.startsWith(QLatin1String("TG_OK="))) { + out.upstreamReachable = line.mid(6).trimmed() == QLatin1String("yes"); + } else if (line.startsWith(QLatin1String("CLIENTS="))) { + out.clientsConnected = line.mid(8).trimmed().toInt(); + } else if (line.startsWith(QLatin1String("CONF_TIME="))) { + out.lastConfigRefresh = line.mid(10).trimmed(); + } else if (line.startsWith(QLatin1String("STATS="))) { + out.statsEndpoint = line.mid(6).trimmed(); + } + } + return ErrorCode::NoError; +} + +void MtProxyInstaller::uploadClientSettingsSnapshot(SshSession &sshSession, const ServerCredentials &credentials, + DockerContainer container, const ContainerConfig &config) { + const MtProxyProtocolConfig *mt = config.getMtProxyProtocolConfig(); + if (!mt) { + return; + } + const QByteArray payload = QJsonDocument(mt->toJson()).toJson(QJsonDocument::Compact); + const ErrorCode err = sshSession.uploadTextFileToContainer(container, credentials, QString::fromUtf8(payload), + QString(kMtProxyClientJsonUploadPath)); + if (err != ErrorCode::NoError) { + qWarning() << "MtProxyInstaller::uploadClientSettingsSnapshot failed" << err; + } +} diff --git a/client/core/installers/mtProxyInstaller.h b/client/core/installers/mtProxyInstaller.h new file mode 100644 index 000000000..2487c9b56 --- /dev/null +++ b/client/core/installers/mtProxyInstaller.h @@ -0,0 +1,34 @@ +#ifndef MTPROXYINSTALLER_H +#define MTPROXYINSTALLER_H + +#include "installerBase.h" + +#include + +struct MtProxyContainerDiagnostics { + bool portReachable = false; + bool upstreamReachable = false; + int clientsConnected = -1; + QString lastConfigRefresh; + QString statsEndpoint; +}; + +class MtProxyInstaller : public InstallerBase { +Q_OBJECT +public: + explicit MtProxyInstaller(QObject *parent = nullptr); + + amnezia::ErrorCode + extractConfigFromContainer(amnezia::DockerContainer container, const amnezia::ServerCredentials &credentials, + SshSession *sshSession, amnezia::ContainerConfig &config) override; + + static void uploadClientSettingsSnapshot(SshSession &sshSession, const amnezia::ServerCredentials &credentials, + amnezia::DockerContainer container, + const amnezia::ContainerConfig &config); + + static amnezia::ErrorCode queryDiagnostics(SshSession &sshSession, const amnezia::ServerCredentials &credentials, + amnezia::DockerContainer container, int listenPort, + MtProxyContainerDiagnostics &out); +}; + +#endif // MTPROXYINSTALLER_H diff --git a/client/core/models/containerConfig.cpp b/client/core/models/containerConfig.cpp index 834f1b540..deb123d45 100644 --- a/client/core/models/containerConfig.cpp +++ b/client/core/models/containerConfig.cpp @@ -113,6 +113,16 @@ const Socks5ProxyProtocolConfig* ContainerConfig::getSocks5ProxyProtocolConfig() return protocolConfig.as(); } +MtProxyProtocolConfig* ContainerConfig::getMtProxyProtocolConfig() +{ + return protocolConfig.as(); +} + +const MtProxyProtocolConfig* ContainerConfig::getMtProxyProtocolConfig() const +{ + return protocolConfig.as(); +} + Ikev2ProtocolConfig* ContainerConfig::getIkev2ProtocolConfig() { return protocolConfig.as(); diff --git a/client/core/models/containerConfig.h b/client/core/models/containerConfig.h index 7f94cce76..9f116fc08 100644 --- a/client/core/models/containerConfig.h +++ b/client/core/models/containerConfig.h @@ -57,6 +57,9 @@ struct ContainerConfig { Socks5ProxyProtocolConfig* getSocks5ProxyProtocolConfig(); const Socks5ProxyProtocolConfig* getSocks5ProxyProtocolConfig() const; + MtProxyProtocolConfig* getMtProxyProtocolConfig(); + const MtProxyProtocolConfig* getMtProxyProtocolConfig() const; + Ikev2ProtocolConfig* getIkev2ProtocolConfig(); const Ikev2ProtocolConfig* getIkev2ProtocolConfig() const; diff --git a/client/core/models/protocolConfig.cpp b/client/core/models/protocolConfig.cpp index ed91f7beb..2b3d60864 100644 --- a/client/core/models/protocolConfig.cpp +++ b/client/core/models/protocolConfig.cpp @@ -9,6 +9,7 @@ #include "core/utils/protocolEnum.h" #include "core/models/protocols/ikev2ProtocolConfig.h" #include "core/models/protocols/dnsProtocolConfig.h" +#include "core/models/protocols/mtProxyProtocolConfig.h" namespace amnezia { @@ -38,6 +39,8 @@ Proto ProtocolConfig::type() const return Proto::TorWebSite; } else if constexpr (std::is_same_v) { return Proto::Dns; + } else if constexpr (std::is_same_v) { + return Proto::MtProxy; } return Proto::Unknown; }, data); @@ -65,6 +68,8 @@ QString ProtocolConfig::port() const return QString(); } else if constexpr (std::is_same_v) { return QString(); + } else if constexpr (std::is_same_v) { + return arg.port.isEmpty() ? QString(protocols::mtProxy::defaultPort) : arg.port; } return QString(); }, data); @@ -88,6 +93,8 @@ QString ProtocolConfig::transportProto() const return QString(); } else if constexpr (std::is_same_v) { return QString(); + } else if constexpr (std::is_same_v) { + return QStringLiteral("tcp"); } return QString(); }, data); @@ -299,6 +306,8 @@ ProtocolConfig ProtocolConfig::fromJson(const QJsonObject& json, Proto type) return ProtocolConfig{TorProtocolConfig::fromJson(json)}; case Proto::Dns: return ProtocolConfig{DnsProtocolConfig::fromJson(json)}; + case Proto::MtProxy: + return ProtocolConfig{MtProxyProtocolConfig::fromJson(json)}; default: return ProtocolConfig{AwgProtocolConfig{}}; } diff --git a/client/core/models/protocolConfig.h b/client/core/models/protocolConfig.h index 325f52ab9..8a8cf91f2 100644 --- a/client/core/models/protocolConfig.h +++ b/client/core/models/protocolConfig.h @@ -22,6 +22,7 @@ #include "core/models/protocols/ikev2ProtocolConfig.h" #include "core/models/protocols/torProtocolConfig.h" #include "core/models/protocols/dnsProtocolConfig.h" +#include "core/models/protocols/mtProxyProtocolConfig.h" namespace amnezia { @@ -36,6 +37,7 @@ struct ProtocolConfig { XrayProtocolConfig, SftpProtocolConfig, Socks5ProxyProtocolConfig, + MtProxyProtocolConfig, Ikev2ProtocolConfig, TorProtocolConfig, DnsProtocolConfig diff --git a/client/core/models/protocols/mtProxyProtocolConfig.cpp b/client/core/models/protocols/mtProxyProtocolConfig.cpp new file mode 100644 index 000000000..d6e0ce1be --- /dev/null +++ b/client/core/models/protocols/mtProxyProtocolConfig.cpp @@ -0,0 +1,147 @@ +#include "mtProxyProtocolConfig.h" + +#include "../../../core/utils/protocolEnum.h" +#include "../../../core/protocols/protocolUtils.h" +#include "../../../core/utils/constants/configKeys.h" +#include "../../../core/utils/constants/protocolConstants.h" +#include + +#include + +using namespace amnezia; + +namespace amnezia { + + QJsonObject MtProxyProtocolConfig::toJson() const { + QJsonObject obj; + + if (!port.isEmpty()) { + obj[configKey::port] = port; + } + if (!secret.isEmpty()) { + obj[protocols::mtProxy::secretKey] = secret; + } + if (!tag.isEmpty()) { + obj[protocols::mtProxy::tagKey] = tag; + } + if (!tgLink.isEmpty()) { + obj[protocols::mtProxy::tgLinkKey] = tgLink; + } + if (!tmeLink.isEmpty()) { + obj[protocols::mtProxy::tmeLinkKey] = tmeLink; + } + obj[protocols::mtProxy::isEnabledKey] = isEnabled; + if (!publicHost.isEmpty()) { + obj[protocols::mtProxy::publicHostKey] = publicHost; + } + if (!transportMode.isEmpty()) { + obj[protocols::mtProxy::transportModeKey] = transportMode; + } + if (!tlsDomain.isEmpty()) { + obj[protocols::mtProxy::tlsDomainKey] = tlsDomain; + } + if (!additionalSecrets.isEmpty()) { + obj[protocols::mtProxy::additionalSecretsKey] = QJsonArray::fromStringList(additionalSecrets); + } + if (!workersMode.isEmpty()) { + obj[protocols::mtProxy::workersModeKey] = workersMode; + } + if (!workers.isEmpty()) { + obj[protocols::mtProxy::workersKey] = workers; + } + obj[protocols::mtProxy::natEnabledKey] = natEnabled; + if (!natInternalIp.isEmpty()) { + obj[protocols::mtProxy::natInternalIpKey] = natInternalIp; + } + if (!natExternalIp.isEmpty()) { + obj[protocols::mtProxy::natExternalIpKey] = natExternalIp; + } + + return obj; + } + + MtProxyProtocolConfig MtProxyProtocolConfig::fromJson(const QJsonObject &json) { + MtProxyProtocolConfig config; + + config.port = json.value(configKey::port).toString(); + config.secret = json.value(protocols::mtProxy::secretKey).toString(); + config.tag = json.value(protocols::mtProxy::tagKey).toString(); + config.tgLink = json.value(protocols::mtProxy::tgLinkKey).toString(); + config.tmeLink = json.value(protocols::mtProxy::tmeLinkKey).toString(); + config.isEnabled = json.value(protocols::mtProxy::isEnabledKey).toBool(true); + config.publicHost = json.value(protocols::mtProxy::publicHostKey).toString(); + config.transportMode = json.value(protocols::mtProxy::transportModeKey).toString(); + config.tlsDomain = json.value(protocols::mtProxy::tlsDomainKey).toString(); + for (const auto &v: json.value(protocols::mtProxy::additionalSecretsKey).toArray()) { + const QString s = v.toString(); + if (!s.isEmpty()) { + config.additionalSecrets.append(s); + } + } + config.workersMode = json.value(protocols::mtProxy::workersModeKey).toString(); + config.workers = json.value(protocols::mtProxy::workersKey).toString(); + config.natEnabled = json.value(protocols::mtProxy::natEnabledKey).toBool(false); + config.natInternalIp = json.value(protocols::mtProxy::natInternalIpKey).toString(); + config.natExternalIp = json.value(protocols::mtProxy::natExternalIpKey).toString(); + + return config; + } + + bool MtProxyProtocolConfig::equalsDockerDeploymentSettings(const MtProxyProtocolConfig &other) const { + const auto normPort = [](const QString &p) { + return p.isEmpty() ? QString(protocols::mtProxy::defaultPort) : p; + }; + const auto normTransport = [](const QString &t) { + return t.isEmpty() ? QString(protocols::mtProxy::transportModeStandard) : t; + }; + const auto normWorkersMode = [](const QString &m) { + return m.isEmpty() ? QString(protocols::mtProxy::workersModeAuto) : m; + }; + + if (normPort(port) != normPort(other.port)) { + return false; + } + if (normTransport(transportMode) != normTransport(other.transportMode)) { + return false; + } + if (tlsDomain != other.tlsDomain) { + return false; + } + if (secret != other.secret) { + return false; + } + if (tag != other.tag) { + return false; + } + if (publicHost != other.publicHost) { + return false; + } + if (normWorkersMode(workersMode) != normWorkersMode(other.workersMode)) { + return false; + } + if (workers != other.workers) { + return false; + } + if (natEnabled != other.natEnabled) { + return false; + } + if (natInternalIp != other.natInternalIp) { + return false; + } + if (natExternalIp != other.natExternalIp) { + return false; + } + if (isEnabled != other.isEnabled) { + return false; + } + + QStringList aa = additionalSecrets; + QStringList bb = other.additionalSecrets; + aa.removeAll(QString()); + bb.removeAll(QString()); + std::sort(aa.begin(), aa.end()); + std::sort(bb.begin(), bb.end()); + return aa == bb; + } + +} // namespace amnezia diff --git a/client/core/models/protocols/mtProxyProtocolConfig.h b/client/core/models/protocols/mtProxyProtocolConfig.h new file mode 100644 index 000000000..b4f532608 --- /dev/null +++ b/client/core/models/protocols/mtProxyProtocolConfig.h @@ -0,0 +1,38 @@ +#ifndef MTPROXYPROTOCOLCONFIG_H +#define MTPROXYPROTOCOLCONFIG_H + +#include +#include +#include + +namespace amnezia { + + struct MtProxyProtocolConfig { + QString port; + QString secret; + QString tag; + QString tgLink; + QString tmeLink; + bool isEnabled = true; + QString publicHost; + QString transportMode; + QString tlsDomain; + QStringList additionalSecrets; + QString workersMode; + QString workers; + bool natEnabled = false; + QString natInternalIp; + QString natExternalIp; + + QJsonObject toJson() const; + + static MtProxyProtocolConfig fromJson(const QJsonObject &json); + + // Port, transport, TLS, secrets, NAT, workers, isEnabled, additionalSecrets (order-independent). + // Ignores tgLink / tmeLink (derived / display). + bool equalsDockerDeploymentSettings(const MtProxyProtocolConfig &other) const; + }; + +} // namespace amnezia + +#endif // MTPROXYPROTOCOLCONFIG_H diff --git a/client/core/protocols/protocolUtils.cpp b/client/core/protocols/protocolUtils.cpp index 65446e0ec..2f8d10c2b 100644 --- a/client/core/protocols/protocolUtils.cpp +++ b/client/core/protocols/protocolUtils.cpp @@ -68,7 +68,9 @@ QMap ProtocolUtils::protocolHumanNames() { Proto::TorWebSite, "Website in Tor network" }, { Proto::Dns, "DNS Service" }, { Proto::Sftp, QObject::tr("SFTP service") }, - { Proto::Socks5Proxy, QObject::tr("SOCKS5 proxy server") } }; + { Proto::Socks5Proxy, QObject::tr("SOCKS5 proxy server") }, + { Proto::MtProxy, QObject::tr("MTProxy (Telegram)") }, + }; } QMap ProtocolUtils::protocolDescriptions() @@ -92,6 +94,7 @@ ServiceType ProtocolUtils::protocolService(Proto p) case Proto::Dns: return ServiceType::Other; case Proto::Sftp: return ServiceType::Other; case Proto::Socks5Proxy: return ServiceType::Other; + case Proto::MtProxy: return ServiceType::Other; default: return ServiceType::Other; } } @@ -104,6 +107,7 @@ int ProtocolUtils::getPortForInstall(Proto p) case OpenVpn: case Socks5Proxy: return QRandomGenerator::global()->bounded(30000, 50000); + case MtProxy: default: return defaultPort(p); } @@ -123,6 +127,7 @@ int ProtocolUtils::defaultPort(Proto p) case Proto::Dns: return 53; case Proto::Sftp: return 222; case Proto::Socks5Proxy: return 38080; + case Proto::MtProxy: return QString(protocols::mtProxy::defaultPort).toInt(); default: return -1; } } @@ -141,6 +146,7 @@ bool ProtocolUtils::defaultPortChangeable(Proto p) case Proto::Dns: return false; case Proto::Sftp: return true; case Proto::Socks5Proxy: return true; + case Proto::MtProxy: return true; default: return false; } } @@ -161,6 +167,7 @@ TransportProto ProtocolUtils::defaultTransportProto(Proto p) case Proto::Dns: return TransportProto::Udp; case Proto::Sftp: return TransportProto::Tcp; case Proto::Socks5Proxy: return TransportProto::Tcp; + case Proto::MtProxy: return TransportProto::Tcp; default: return TransportProto::Udp; } } @@ -180,6 +187,7 @@ bool ProtocolUtils::defaultTransportProtoChangeable(Proto p) case Proto::Dns: return false; case Proto::Sftp: return false; case Proto::Socks5Proxy: return false; + case Proto::MtProxy: return false; default: return false; } return false; @@ -208,4 +216,3 @@ QString ProtocolUtils::getProtocolVersionString(const QJsonObject &protocolConfi if (version == protocols::awg::awgV1_5) return QObject::tr(" (version 1.5)"); return ""; } - diff --git a/client/core/utils/constants/configKeys.h b/client/core/utils/constants/configKeys.h index 3272eb2bb..71d63674a 100644 --- a/client/core/utils/constants/configKeys.h +++ b/client/core/utils/constants/configKeys.h @@ -93,6 +93,7 @@ namespace amnezia constexpr QLatin1String xray("xray"); constexpr QLatin1String ssxray("ssxray"); constexpr QLatin1String socks5proxy("socks5proxy"); + constexpr QLatin1String mtproxy("mtproxy"); constexpr QLatin1String splitTunnelSites("splitTunnelSites"); constexpr QLatin1String splitTunnelType("splitTunnelType"); diff --git a/client/core/utils/constants/protocolConstants.h b/client/core/utils/constants/protocolConstants.h index 01e2a151a..0cb471d61 100644 --- a/client/core/utils/constants/protocolConstants.h +++ b/client/core/utils/constants/protocolConstants.h @@ -3,6 +3,7 @@ namespace amnezia { + namespace protocols { @@ -174,9 +175,37 @@ namespace amnezia constexpr char proxyConfigPath[] = "/usr/local/3proxy/conf/3proxy.cfg"; } + namespace mtProxy + { + constexpr char secretKey[] = "mtproxy_secret"; + constexpr char tagKey[] = "mtproxy_tag"; + constexpr char tgLinkKey[] = "mtproxy_tg_link"; + constexpr char tmeLinkKey[] = "mtproxy_tme_link"; + constexpr char isEnabledKey[] = "mtproxy_is_enabled"; + constexpr char publicHostKey[] = "mtproxy_public_host"; + constexpr char transportModeKey[] = "mtproxy_transport_mode"; + constexpr char tlsDomainKey[] = "mtproxy_tls_domain"; + constexpr char additionalSecretsKey[] = "mtproxy_additional_secrets"; + constexpr char workersKey[] = "mtproxy_workers"; + constexpr char workersModeKey[] = "mtproxy_workers_mode"; + constexpr char natEnabledKey[] = "mtproxy_nat_enabled"; + constexpr char natInternalIpKey[] = "mtproxy_nat_internal_ip"; + constexpr char natExternalIpKey[] = "mtproxy_nat_external_ip"; + + constexpr char transportModeStandard[] = "standard"; + constexpr char transportModeFakeTLS[] = "faketls"; + + constexpr char workersModeAuto[] = "auto"; + constexpr char workersModeManual[] = "manual"; + + constexpr char defaultPort[] = "443"; + constexpr char defaultWorkers[] = "2"; + constexpr int maxWorkers = 32; + constexpr int botTagHexLength = 32; + constexpr char defaultTlsDomain[] = "googletagmanager.com"; + } + } // namespace protocols } #endif // PROTOCOLCONSTANTS_H - - diff --git a/client/core/utils/containerEnum.h b/client/core/utils/containerEnum.h index 97398b783..8e4fc33f8 100644 --- a/client/core/utils/containerEnum.h +++ b/client/core/utils/containerEnum.h @@ -23,7 +23,8 @@ namespace amnezia TorWebSite, Dns, Sftp, - Socks5Proxy + Socks5Proxy, + MtProxy, }; Q_ENUM_NS(DockerContainer) } // namespace ContainerEnumNS diff --git a/client/core/utils/containers/containerUtils.cpp b/client/core/utils/containers/containerUtils.cpp index b028dcf5f..cda3353d5 100644 --- a/client/core/utils/containers/containerUtils.cpp +++ b/client/core/utils/containers/containerUtils.cpp @@ -72,7 +72,9 @@ QMap ContainerUtils::containerHumanNames() { DockerContainer::TorWebSite, QObject::tr("Website in Tor network") }, { DockerContainer::Dns, QObject::tr("AmneziaDNS") }, { DockerContainer::Sftp, QObject::tr("SFTP file sharing service") }, - { DockerContainer::Socks5Proxy, QObject::tr("SOCKS5 proxy server") } }; + { DockerContainer::Socks5Proxy, QObject::tr("SOCKS5 proxy server") }, + { DockerContainer::MtProxy, QObject::tr("MTProxy (Telegram)") }, + }; } QMap ContainerUtils::containerDescriptions() @@ -102,7 +104,10 @@ QMap ContainerUtils::containerDescriptions() { DockerContainer::Sftp, QObject::tr("Create a file vault on your server to securely store and transfer files.") }, { DockerContainer::Socks5Proxy, - QObject::tr("") } }; + QObject::tr("") }, + { DockerContainer::MtProxy, + QObject::tr("Telegram MTProto proxy server") }, + }; } QMap ContainerUtils::containerDetailedDescriptions() @@ -172,7 +177,12 @@ QMap ContainerUtils::containerDetailedDescriptions() "You will be able to access it using\n FileZilla or other SFTP clients, " "as well as mount the disk on your device to access\n it directly from your device.\n\n" "For more detailed information, you can\n find it in the support section under \"Create SFTP file storage.\" ") }, - { DockerContainer::Socks5Proxy, QObject::tr("SOCKS5 proxy server") } + { DockerContainer::Socks5Proxy, QObject::tr("SOCKS5 proxy server") }, + { DockerContainer::MtProxy, + QObject::tr("Telegram MTProto proxy server. " + "Allows Telegram clients to connect through your server " + "using the MTProto protocol. Supports FakeTLS mode for " + "bypassing DPI-based blocking.") }, }; } @@ -197,6 +207,7 @@ Proto ContainerUtils::defaultProtocol(DockerContainer c) case DockerContainer::Dns: return Proto::Dns; case DockerContainer::Sftp: return Proto::Sftp; case DockerContainer::Socks5Proxy: return Proto::Socks5Proxy; + case DockerContainer::MtProxy: return Proto::MtProxy; default: return Proto::Unknown; } } @@ -224,6 +235,7 @@ bool ContainerUtils::isSupportedByCurrentPlatform(DockerContainer c) case DockerContainer::Awg: return true; case DockerContainer::Xray: return true; case DockerContainer::SSXray: return true; + case DockerContainer::MtProxy: return true; default: return false; } @@ -237,7 +249,7 @@ bool ContainerUtils::isSupportedByCurrentPlatform(DockerContainer c) case DockerContainer::Awg: return true; case DockerContainer::Xray: return true; case DockerContainer::SSXray: return true; - return false; + case DockerContainer::MtProxy: return true; default: return false; } @@ -256,6 +268,7 @@ bool ContainerUtils::isSupportedByCurrentPlatform(DockerContainer c) case DockerContainer::Awg: return true; case DockerContainer::Xray: return true; case DockerContainer::SSXray: return true; + case DockerContainer::MtProxy: return true; default: return false; } @@ -318,6 +331,7 @@ bool ContainerUtils::isShareable(DockerContainer container) case DockerContainer::Dns: return false; case DockerContainer::Sftp: return false; case DockerContainer::Socks5Proxy: return false; + case DockerContainer::MtProxy: return false; default: return true; } } @@ -346,8 +360,9 @@ int ContainerUtils::installPageOrder(DockerContainer container) case DockerContainer::Xray: return 3; case DockerContainer::Ipsec: return 7; case DockerContainer::SSXray: return 8; + case DockerContainer::MtProxy: + return 20; default: return 0; } } - diff --git a/client/core/utils/protocolEnum.h b/client/core/utils/protocolEnum.h index 2c5d9663b..5293d3fe2 100644 --- a/client/core/utils/protocolEnum.h +++ b/client/core/utils/protocolEnum.h @@ -30,7 +30,8 @@ namespace amnezia TorWebSite, Dns, Sftp, - Socks5Proxy + Socks5Proxy, + MtProxy, }; Q_ENUM_NS(Proto) diff --git a/client/core/utils/selfhosted/scriptsRegistry.cpp b/client/core/utils/selfhosted/scriptsRegistry.cpp index 26bf73de6..e0e49c26b 100644 --- a/client/core/utils/selfhosted/scriptsRegistry.cpp +++ b/client/core/utils/selfhosted/scriptsRegistry.cpp @@ -9,7 +9,6 @@ #include "core/utils/containerEnum.h" #include "core/utils/containers/containerUtils.h" #include "core/utils/protocolEnum.h" -#include "core/utils/protocolEnum.h" #include "core/protocols/protocolUtils.h" #include "core/utils/constants/configKeys.h" #include "core/utils/constants/protocolConstants.h" @@ -20,6 +19,7 @@ #include "core/models/protocols/xrayProtocolConfig.h" #include "core/models/protocols/sftpProtocolConfig.h" #include "core/models/protocols/socks5ProxyProtocolConfig.h" +#include "core/models/protocols/mtProxyProtocolConfig.h" using namespace amnezia; using namespace ProtocolUtils; @@ -38,6 +38,7 @@ QString amnezia::scriptFolder(amnezia::DockerContainer container) case DockerContainer::Dns: return QLatin1String("dns"); case DockerContainer::Sftp: return QLatin1String("sftp"); case DockerContainer::Socks5Proxy: return QLatin1String("socks5_proxy"); + case DockerContainer::MtProxy: return QLatin1String("mtproxy"); default: return QString(); } } @@ -284,6 +285,55 @@ amnezia::ScriptVars amnezia::genSocks5ProxyVars(const ContainerConfig &container return vars; } +amnezia::ScriptVars amnezia::genMtProxyVars(const ContainerConfig &containerConfig) { + ScriptVars vars; + + if (auto *mtProxyProtocolConfig = containerConfig.getMtProxyProtocolConfig()) { + const MtProxyProtocolConfig &c = *mtProxyProtocolConfig; + + vars.append({{"$MTPROXY_PORT", c.port.isEmpty() ? QString(protocols::mtProxy::defaultPort) : c.port}}); + vars.append({{"$MTPROXY_SECRET", c.secret}}); + vars.append({{"$MTPROXY_TAG", c.tag}}); + vars.append({{"$MTPROXY_TRANSPORT_MODE", + c.transportMode.isEmpty() ? QString(protocols::mtProxy::transportModeStandard) + : c.transportMode}}); + + QString tlsDomain = c.tlsDomain; + if (tlsDomain.isEmpty()) { + tlsDomain = QString(protocols::mtProxy::defaultTlsDomain); + } + vars.append({{"$MTPROXY_TLS_DOMAIN", tlsDomain}}); + vars.append({{"$MTPROXY_PUBLIC_HOST", c.publicHost}}); + + QStringList additionalList; + for (const QString &s: c.additionalSecrets) { + if (!s.isEmpty()) { + additionalList << s; + } + } + vars.append({{"$MTPROXY_ADDITIONAL_SECRETS", additionalList.join(QLatin1Char(','))}}); + + const QString workersMode = c.workersMode.isEmpty() ? QString(protocols::mtProxy::workersModeAuto) + : c.workersMode; + QString workers; + if (workersMode == QLatin1String(protocols::mtProxy::workersModeManual)) { + workers = c.workers.isEmpty() ? QString(protocols::mtProxy::defaultWorkers) : c.workers; + } else { + const QString transportMode = + c.transportMode.isEmpty() ? QString(protocols::mtProxy::transportModeStandard) : c.transportMode; + workers = (transportMode == QLatin1String(protocols::mtProxy::transportModeFakeTLS)) ? QStringLiteral("0") + : QStringLiteral("2"); + } + vars.append({{"$MTPROXY_WORKERS", workers}}); + + vars.append({{"$MTPROXY_NAT_ENABLED", c.natEnabled ? QStringLiteral("1") : QStringLiteral("0")}}); + vars.append({{"$MTPROXY_NAT_INTERNAL_IP", c.natInternalIp}}); + vars.append({{"$MTPROXY_NAT_EXTERNAL_IP", c.natExternalIp}}); + } + + return vars; +} + amnezia::ScriptVars amnezia::genProtocolVarsForContainer(DockerContainer container, const ContainerConfig &containerConfig) { ScriptVars vars; @@ -308,6 +358,9 @@ amnezia::ScriptVars amnezia::genProtocolVarsForContainer(DockerContainer contain case Proto::Socks5Proxy: vars.append(genSocks5ProxyVars(containerConfig)); break; + case Proto::MtProxy: + vars.append(genMtProxyVars(containerConfig)); + break; default: break; } diff --git a/client/core/utils/selfhosted/scriptsRegistry.h b/client/core/utils/selfhosted/scriptsRegistry.h index b9d320455..cf3c39394 100644 --- a/client/core/utils/selfhosted/scriptsRegistry.h +++ b/client/core/utils/selfhosted/scriptsRegistry.h @@ -67,6 +67,7 @@ ScriptVars genWireGuardVars(const ContainerConfig &containerConfig); ScriptVars genAwgVars(const ContainerConfig &containerConfig); ScriptVars genSftpVars(const ContainerConfig &containerConfig); ScriptVars genSocks5ProxyVars(const ContainerConfig &containerConfig); +ScriptVars genMtProxyVars(const ContainerConfig &containerConfig); ScriptVars genProtocolVarsForContainer(DockerContainer container, const ContainerConfig &containerConfig); } diff --git a/client/core/utils/selfhosted/sshClient.cpp b/client/core/utils/selfhosted/sshClient.cpp index e8847a484..5e854ab4b 100644 --- a/client/core/utils/selfhosted/sshClient.cpp +++ b/client/core/utils/selfhosted/sshClient.cpp @@ -56,7 +56,7 @@ namespace libssh { QEventLoop wait; connect(&watcher, &QFutureWatcher::finished, &wait, &QEventLoop::quit); watcher.setFuture(future); - wait.exec(); + wait.exec(QEventLoop::ExcludeUserInputEvents); int connectionResult = watcher.result(); @@ -189,7 +189,7 @@ namespace libssh { QEventLoop wait; QObject::connect(this, &Client::writeToChannelFinished, &wait, &QEventLoop::quit); - wait.exec(); + wait.exec(QEventLoop::ExcludeUserInputEvents); return watcher.result(); } @@ -284,7 +284,7 @@ namespace libssh { QEventLoop wait; QObject::connect(this, &Client::scpFileCopyFinished, &wait, &QEventLoop::quit); - wait.exec(); + wait.exec(QEventLoop::ExcludeUserInputEvents); closeScpSession(); return watcher.result(); diff --git a/client/core/utils/selfhosted/sshSession.cpp b/client/core/utils/selfhosted/sshSession.cpp index 5821ad232..c2360c6d1 100644 --- a/client/core/utils/selfhosted/sshSession.cpp +++ b/client/core/utils/selfhosted/sshSession.cpp @@ -103,8 +103,8 @@ ErrorCode SshSession::runContainerScript(const ServerCredentials &credentials, D if (e) return e; - QString runner = - QString("sudo docker exec -i $CONTAINER_NAME %2 %1 ").arg(fileName, (container == DockerContainer::Socks5Proxy ? "sh" : "bash")); + const bool useSh = container == DockerContainer::Socks5Proxy || container == DockerContainer::MtProxy; + QString runner = QString("sudo docker exec -i $CONTAINER_NAME %2 %1 ").arg(fileName, useSh ? "sh" : "bash"); e = runScript(credentials, replaceVars(runner, amnezia::genBaseVars(credentials, container, QString(), QString())), cbReadStdOut, cbReadStdErr); QString remover = QString("sudo docker exec -i $CONTAINER_NAME rm %1 ").arg(fileName); diff --git a/client/server_scripts/mtproxy/Dockerfile b/client/server_scripts/mtproxy/Dockerfile new file mode 100644 index 000000000..64ace34d3 --- /dev/null +++ b/client/server_scripts/mtproxy/Dockerfile @@ -0,0 +1,9 @@ +FROM amneziavpn/mtproxy:latest + +RUN mkdir -p /opt/amnezia /data +RUN printf '#!/bin/sh\ntail -f /dev/null\n' > /opt/amnezia/start.sh && \ + chmod a+x /opt/amnezia/start.sh + +VOLUME /data +ENTRYPOINT ["/bin/sh", "/opt/amnezia/start.sh"] +CMD [""] diff --git a/client/server_scripts/mtproxy/configure_container.sh b/client/server_scripts/mtproxy/configure_container.sh new file mode 100644 index 000000000..5ba6da11b --- /dev/null +++ b/client/server_scripts/mtproxy/configure_container.sh @@ -0,0 +1,60 @@ +#!/bin/sh + +# Download Telegram config files +curl -s https://core.telegram.org/getProxySecret -o /data/proxy-secret +curl -s https://core.telegram.org/getProxyConfig -o /data/proxy-multi.conf + +# Determine secret: env var -> saved file -> generate new +if [ -n "$MTPROXY_SECRET" ]; then + SECRET="$MTPROXY_SECRET" +elif [ -f /data/secret ]; then + SECRET=$(cat /data/secret) +else + SECRET=$(openssl rand -hex 16) +fi + +# Validate: must be exactly 32 hex chars +echo "$SECRET" | grep -qE '^[0-9a-fA-F]{32}$' || SECRET=$(openssl rand -hex 16) + +# Persist secret for start.sh restarts +echo "$SECRET" > /data/secret + +# Detect external IP +IP=$(curl -s --max-time 5 https://api.ipify.org 2>/dev/null) +[ -z "$IP" ] && IP=$(curl -s --max-time 5 https://ifconfig.me 2>/dev/null) +[ -z "$IP" ] && IP=$(curl -s --max-time 5 https://icanhazip.com 2>/dev/null) + +# Use custom public host/domain if provided, otherwise fall back to detected IP +if [ -n "$MTPROXY_PUBLIC_HOST" ]; then + LINK_HOST="$MTPROXY_PUBLIC_HOST" +else + LINK_HOST="$IP" +fi + +PORT=$MTPROXY_PORT + +# Transport mode is substituted by replaceVars — plain variable, no curly braces +TRANSPORT_MODE=$MTPROXY_TRANSPORT_MODE + +PADDED_SECRET="dd${SECRET}" + +if [ "$TRANSPORT_MODE" = "faketls" ] && [ -n "$MTPROXY_TLS_DOMAIN" ]; then + DOMAIN_HEX=$(echo -n "$MTPROXY_TLS_DOMAIN" | od -A n -t x1 | tr -d ' \n') + FAKETLS_SECRET="ee${SECRET}${DOMAIN_HEX}" +else + FAKETLS_SECRET="" +fi + +# Active link secret depends on transport mode +if [ "$TRANSPORT_MODE" = "faketls" ] && [ -n "$FAKETLS_SECRET" ]; then + LINK_SECRET="$FAKETLS_SECRET" +else + LINK_SECRET="$PADDED_SECRET" +fi + +# Output stable markers — parsed by updateContainerConfigAfterInstallation() +echo "[*] MTProxy configuration" +echo "[*] Secret: ${SECRET}" +echo "[*] FakeTLS: ${FAKETLS_SECRET}" +echo "[*] tg:// link: tg://proxy?server=${LINK_HOST}&port=${PORT}&secret=${LINK_SECRET}" +echo "[*] t.me link: https://t.me/proxy?server=${LINK_HOST}&port=${PORT}&secret=${LINK_SECRET}" diff --git a/client/server_scripts/mtproxy/run_container.sh b/client/server_scripts/mtproxy/run_container.sh new file mode 100644 index 000000000..9039ced75 --- /dev/null +++ b/client/server_scripts/mtproxy/run_container.sh @@ -0,0 +1,9 @@ +# Run container +sudo docker run -d \ + --log-driver none \ + --restart always \ + -p $MTPROXY_PORT:$MTPROXY_PORT/tcp \ + -v amnezia-mtproxy-data:/data \ + --name $CONTAINER_NAME \ + $CONTAINER_NAME + diff --git a/client/server_scripts/mtproxy/start.sh b/client/server_scripts/mtproxy/start.sh new file mode 100644 index 000000000..4b8248e7e --- /dev/null +++ b/client/server_scripts/mtproxy/start.sh @@ -0,0 +1,71 @@ +#!/bin/sh + +echo "Container startup" + +# Read persisted secret +SECRET="" +if [ -f /data/secret ]; then + SECRET=$(cat /data/secret) +fi + +if [ -z "$SECRET" ]; then + echo "ERROR: /data/secret not found — run configure_container first" + tail -f /dev/null + exit 1 +fi + +# Build tag argument +TAG_ARG="" +if [ -n "$MTPROXY_TAG" ]; then + TAG_ARG="-P $MTPROXY_TAG" +fi + +# Build domain argument for FakeTLS mode +DOMAIN_ARG="" +if [ "$MTPROXY_TRANSPORT_MODE" = "faketls" ] && [ -n "$MTPROXY_TLS_DOMAIN" ]; then + DOMAIN_ARG="--domain $MTPROXY_TLS_DOMAIN" +fi + +WORKERS=$MTPROXY_WORKERS +STATS_PORT=2398 +LISTEN_PORT=$MTPROXY_PORT + +NAT_FLAG="" +NAT_VALUE="" +if [ "$MTPROXY_NAT_ENABLED" = "1" ] && [ -n "$MTPROXY_NAT_INTERNAL_IP" ] && [ -n "$MTPROXY_NAT_EXTERNAL_IP" ]; then + NAT_FLAG="--nat-info" + NAT_VALUE="$MTPROXY_NAT_INTERNAL_IP:$MTPROXY_NAT_EXTERNAL_IP" +else + INTERNAL_IP=$(hostname -i 2>/dev/null | awk '{print $1}') + EXTERNAL_IP=$(curl -s --max-time 5 https://api.ipify.org 2>/dev/null) + [ -z "$EXTERNAL_IP" ] && EXTERNAL_IP=$(curl -s --max-time 5 https://ifconfig.me 2>/dev/null) + + if [ -n "$INTERNAL_IP" ] && [ -n "$EXTERNAL_IP" ] && [ "$INTERNAL_IP" != "$EXTERNAL_IP" ]; then + NAT_FLAG="--nat-info" + NAT_VALUE="${INTERNAL_IP}:${EXTERNAL_IP}" + fi +fi + +# Build additional secrets arguments +ADDITIONAL_SECRETS_ARG="" +if [ -n "$MTPROXY_ADDITIONAL_SECRETS" ]; then + for S in $(echo "$MTPROXY_ADDITIONAL_SECRETS" | tr ',' ' '); do + ADDITIONAL_SECRETS_ARG="$ADDITIONAL_SECRETS_ARG -S $S" + done +fi + +# Start proxy (foreground) +exec mtproto-proxy \ + -u root \ + -p ${STATS_PORT} \ + -H ${LISTEN_PORT} \ + -S ${SECRET} \ + ${ADDITIONAL_SECRETS_ARG} \ + --aes-pwd /data/proxy-secret \ + -M ${WORKERS} \ + -C 60000 \ + --allow-skip-dh \ + ${NAT_FLAG:+${NAT_FLAG} ${NAT_VALUE}} \ + ${TAG_ARG} \ + ${DOMAIN_ARG} \ + /data/proxy-multi.conf diff --git a/client/server_scripts/serverScripts.qrc b/client/server_scripts/serverScripts.qrc index 5ed57b01e..35a2be2a3 100644 --- a/client/server_scripts/serverScripts.qrc +++ b/client/server_scripts/serverScripts.qrc @@ -24,6 +24,10 @@ ipsec/run_container.sh ipsec/start.sh ipsec/strongswan.profile + mtproxy/configure_container.sh + mtproxy/Dockerfile + mtproxy/run_container.sh + mtproxy/start.sh openvpn/configure_container.sh openvpn/Dockerfile openvpn/run_container.sh @@ -55,4 +59,3 @@ xray/template.json - diff --git a/client/ui/controllers/networkReachabilityController.cpp b/client/ui/controllers/networkReachabilityController.cpp new file mode 100644 index 000000000..390b506c7 --- /dev/null +++ b/client/ui/controllers/networkReachabilityController.cpp @@ -0,0 +1,46 @@ +#include "networkReachabilityController.h" + +#include + +namespace { + + bool reachabilityAllowsRemoteOperations(QNetworkInformation::Reachability r) { + using R = QNetworkInformation::Reachability; + // Unknown: no backend or not yet determined — do not block UI. + return r == R::Online || r == R::Unknown; + } + +} // namespace + +NetworkReachabilityController::NetworkReachabilityController(QObject *parent) : QObject(parent) { + attachToNetworkInformation(); +} + +bool NetworkReachabilityController::hasInternetAccess() const { + return m_hasInternetAccess; +} + +void NetworkReachabilityController::attachToNetworkInformation() { + if (!QNetworkInformation::loadDefaultBackend()) { + return; + } + QNetworkInformation *ni = QNetworkInformation::instance(); + if (!ni) { + return; + } + const bool initial = reachabilityAllowsRemoteOperations(ni->reachability()); + const bool previous = m_hasInternetAccess; + m_hasInternetAccess = initial; + if (previous != m_hasInternetAccess) { + emit hasInternetAccessChanged(); + } + connect(ni, &QNetworkInformation::reachabilityChanged, this, + [this](QNetworkInformation::Reachability r) { + const bool ok = reachabilityAllowsRemoteOperations(r); + if (ok == m_hasInternetAccess) { + return; + } + m_hasInternetAccess = ok; + emit hasInternetAccessChanged(); + }); +} diff --git a/client/ui/controllers/networkReachabilityController.h b/client/ui/controllers/networkReachabilityController.h new file mode 100644 index 000000000..effa2a88b --- /dev/null +++ b/client/ui/controllers/networkReachabilityController.h @@ -0,0 +1,30 @@ +#ifndef NETWORKREACHABILITYCONTROLLER_H +#define NETWORKREACHABILITYCONTROLLER_H + +#include + +// Exposes QNetworkInformation to QML for UI that must not run remote operations offline. +// Note: mozilla/networkwatcher.h has NetworkWatcher::getReachability() using the same API, +// but networkwatcher.cpp is not linked into the desktop client (only the service process). + +class NetworkReachabilityController final : public QObject { +Q_OBJECT + + Q_PROPERTY(bool hasInternetAccess READ hasInternetAccess NOTIFY hasInternetAccessChanged) + +public: + explicit NetworkReachabilityController(QObject *parent = nullptr); + + bool hasInternetAccess() const; + +signals: + + void hasInternetAccessChanged(); + +private: + void attachToNetworkInformation(); + + bool m_hasInternetAccess = true; +}; + +#endif // NETWORKREACHABILITYCONTROLLER_H diff --git a/client/ui/controllers/qml/pageController.h b/client/ui/controllers/qml/pageController.h index f98c1c130..b5e6007b7 100644 --- a/client/ui/controllers/qml/pageController.h +++ b/client/ui/controllers/qml/pageController.h @@ -50,6 +50,7 @@ namespace PageLoader PageServiceTorWebsiteSettings, PageServiceDnsSettings, PageServiceSocksProxySettings, + PageServiceMtProxySettings, PageSetupWizardStart, PageSetupWizardCredentials, diff --git a/client/ui/controllers/selfhosted/exportUiController.cpp b/client/ui/controllers/selfhosted/exportUiController.cpp index 3e7984668..3be7ef981 100644 --- a/client/ui/controllers/selfhosted/exportUiController.cpp +++ b/client/ui/controllers/selfhosted/exportUiController.cpp @@ -3,6 +3,7 @@ #include #include "../systemController.h" +#include "core/utils/qrCodeUtils.h" ExportUiController::ExportUiController(ExportController* exportController, QObject *parent) : QObject(parent), @@ -53,6 +54,14 @@ void ExportUiController::generateXrayConfig(const QString &serverId, const QStri applyExportResult(result); } +void ExportUiController::generateQrFromString(const QString &text) +{ + clearPreviousConfig(); + m_config = text; + m_qrCodes = qrCodeUtils::generateQrCodeImageSeries(text.toUtf8()); + emit exportConfigChanged(); +} + QString ExportUiController::getConfig() { return m_config; diff --git a/client/ui/controllers/selfhosted/exportUiController.h b/client/ui/controllers/selfhosted/exportUiController.h index 5bcac90bd..20f7a2282 100644 --- a/client/ui/controllers/selfhosted/exportUiController.h +++ b/client/ui/controllers/selfhosted/exportUiController.h @@ -25,6 +25,7 @@ public slots: void generateWireGuardConfig(const QString &serverId, const QString &clientName); void generateAwgConfig(const QString &serverId, int containerIndex, const QString &clientName); void generateXrayConfig(const QString &serverId, const QString &clientName); + void generateQrFromString(const QString &text); QString getConfig(); QString getNativeConfigString(); diff --git a/client/ui/controllers/selfhosted/installUiController.cpp b/client/ui/controllers/selfhosted/installUiController.cpp index 32178e7a6..6e674f452 100755 --- a/client/ui/controllers/selfhosted/installUiController.cpp +++ b/client/ui/controllers/selfhosted/installUiController.cpp @@ -5,11 +5,13 @@ #include #include #include +#include #include +#include +#include #include "core/utils/api/apiUtils.h" #include "core/controllers/selfhosted/installController.h" -#include "core/utils/selfhosted/sshSession.h" #include "core/utils/networkUtilities.h" #include "core/utils/protocolEnum.h" #include "core/protocols/protocolUtils.h" @@ -47,6 +49,7 @@ InstallUiController::InstallUiController(InstallController *installController, #endif SftpConfigModel *sftpConfigModel, Socks5ProxyConfigModel *socks5ConfigModel, + MtProxyConfigModel* mtConfigModel, QObject *parent) : QObject(parent), m_installController(installController), @@ -63,7 +66,8 @@ InstallUiController::InstallUiController(InstallController *installController, m_ikev2ConfigModel(ikev2ConfigModel), #endif m_sftpConfigModel(sftpConfigModel), - m_socks5ConfigModel(socks5ConfigModel) + m_socks5ConfigModel(socks5ConfigModel), + m_mtProxyConfigModel(mtConfigModel) { connect(m_installController, &InstallController::configValidated, this, &InstallUiController::configValidated); connect(m_installController, &InstallController::validationErrorOccurred, this, [this](ErrorCode errorCode) { @@ -199,7 +203,7 @@ void InstallUiController::scanServerForInstalledContainers(const QString &server emit installationErrorOccurred(errorCode); } -void InstallUiController::updateContainer(const QString &serverId, int containerIndex, int protocolIndex) +void InstallUiController::updateContainer(const QString &serverId, int containerIndex, int protocolIndex, bool closePage) { DockerContainer container = static_cast(containerIndex); @@ -238,6 +242,10 @@ void InstallUiController::updateContainer(const QString &serverId, int container containerConfig.protocolConfig = m_socks5ConfigModel->getProtocolConfig(); break; } + case Proto::MtProxy: { + containerConfig.protocolConfig = m_mtProxyConfigModel->getProtocolConfig(); + break; + } #ifdef Q_OS_WINDOWS case Proto::Ikev2: { containerConfig.protocolConfig = m_ikev2ConfigModel->getProtocolConfig(); @@ -249,19 +257,113 @@ void InstallUiController::updateContainer(const QString &serverId, int container } ContainerConfig oldContainerConfig = m_serversController->getContainerConfig(serverId, container); + if (container == DockerContainer::MtProxy) { + emit serverIsBusy(true); + auto *watcher = new QFutureWatcher(this); + QObject::connect(watcher, &QFutureWatcher::finished, this, + [this, watcher, serverId, container, closePage]() { + const ErrorCode errorCode = watcher->result(); + watcher->deleteLater(); + emit serverIsBusy(false); + + if (errorCode == ErrorCode::NoError) { + const ContainerConfig updatedConfig = + m_serversController->getContainerConfig(serverId, container); + m_protocolModel->updateModel(updatedConfig); + + const auto defaultContainer = + m_serversController->getDefaultContainer(serverId); + if ((serverId == m_serversController->getDefaultServerId()) + && (container == defaultContainer)) { + emit currentContainerUpdated(); + } else { + emit updateContainerFinished(tr("Settings updated successfully"), closePage); + } + } else { + emit installationErrorOccurred(errorCode); + } + }); + + ContainerConfig newConfigCopy = containerConfig; + ContainerConfig oldConfigCopy = oldContainerConfig; + InstallController *installController = m_installController; + QFuture future = + QtConcurrent::run([installController, serverId, container, oldConfigCopy, + newConfigCopy]() mutable -> ErrorCode { + return installController->updateContainer(serverId, container, oldConfigCopy, newConfigCopy); + }); + watcher->setFuture(future); + return; + } + ErrorCode errorCode = m_installController->updateContainer(serverId, container, oldContainerConfig, containerConfig); if (errorCode == ErrorCode::NoError) { ContainerConfig updatedConfig = m_serversController->getContainerConfig(serverId, container); m_protocolModel->updateModel(updatedConfig); - emit updateContainerFinished(tr("Settings updated successfully")); + const auto defaultContainer = m_serversController->getDefaultContainer(serverId); + if ((serverId == m_serversController->getDefaultServerId()) && (container == defaultContainer)) { + emit currentContainerUpdated(); + } else { + emit updateContainerFinished(tr("Settings updated successfully"), closePage); + } return; } emit installationErrorOccurred(errorCode); } +void InstallUiController::setContainerEnabled(const QString &serverId, int containerIndex, bool enabled) +{ + const DockerContainer container = static_cast(containerIndex); + + emit serverIsBusy(true); + const ErrorCode errorCode = m_installController->setDockerContainerEnabledState(serverId, container, enabled); + emit serverIsBusy(false); + + if (errorCode == ErrorCode::NoError) { + const ContainerConfig currentConfig = m_serversController->getContainerConfig(serverId, container); + m_protocolModel->updateModel(currentConfig); + emit setContainerEnabledFinished(enabled); + return; + } + + emit installationErrorOccurred(errorCode); +} + +void InstallUiController::refreshContainerStatus(const QString &serverId, int containerIndex) +{ + const DockerContainer container = static_cast(containerIndex); + int status = 3; + const ErrorCode errorCode = m_installController->queryDockerContainerStatus(serverId, container, status); + if (errorCode != ErrorCode::NoError) { + emit containerStatusRefreshed(3); + return; + } + emit containerStatusRefreshed(status); +} + +void InstallUiController::refreshContainerDiagnostics(const QString &serverId, int containerIndex, int port) +{ + const DockerContainer container = static_cast(containerIndex); + MtProxyContainerDiagnostics diag; + const ErrorCode errorCode = m_installController->queryMtProxyDiagnostics(serverId, container, port, diag); + if (errorCode != ErrorCode::NoError) { + emit containerDiagnosticsRefreshed(false, false, -1, QString(), QString()); + return; + } + emit containerDiagnosticsRefreshed(diag.portReachable, diag.upstreamReachable, diag.clientsConnected, + diag.lastConfigRefresh, diag.statsEndpoint); +} + +void InstallUiController::fetchContainerSecret(const QString &serverId, int containerIndex) +{ + const DockerContainer container = static_cast(containerIndex); + const QString secret = m_installController->fetchDockerContainerSecret(serverId, container); + emit containerSecretFetched(secret); +} + void InstallUiController::rebootServer(const QString &serverId) { const QString serverName = m_serversController->notificationDisplayName(serverId); @@ -473,6 +575,7 @@ void InstallUiController::updateProtocolConfigModel(const QString &serverId, int case Proto::TorWebSite: updateIfPresent(m_torConfigModel, containerConfig.getTorProtocolConfig()); break; case Proto::Sftp: updateIfPresent(m_sftpConfigModel, containerConfig.getSftpProtocolConfig()); break; case Proto::Socks5Proxy: updateIfPresent(m_socks5ConfigModel, containerConfig.getSocks5ProxyProtocolConfig()); break; + case Proto::MtProxy: updateIfPresent(m_mtProxyConfigModel, containerConfig.getMtProxyProtocolConfig()); break; #ifdef Q_OS_WINDOWS case Proto::Ikev2: updateIfPresent(m_ikev2ConfigModel, containerConfig.getIkev2ProtocolConfig()); break; #endif diff --git a/client/ui/controllers/selfhosted/installUiController.h b/client/ui/controllers/selfhosted/installUiController.h index beec0a3b8..c8332e555 100644 --- a/client/ui/controllers/selfhosted/installUiController.h +++ b/client/ui/controllers/selfhosted/installUiController.h @@ -28,6 +28,7 @@ #include "ui/models/services/torConfigModel.h" #include "core/models/protocols/sftpProtocolConfig.h" #include "core/models/protocols/socks5ProxyProtocolConfig.h" +#include "ui/models/services/mtProxyConfigModel.h" class InstallUiController : public QObject { @@ -48,6 +49,7 @@ public: #endif SftpConfigModel* sftpConfigModel, Socks5ProxyConfigModel* socks5ConfigModel, + MtProxyConfigModel* mtConfigModel, QObject *parent = nullptr); ~InstallUiController(); @@ -58,12 +60,16 @@ public slots: void scanServerForInstalledContainers(const QString &serverId); - void updateContainer(const QString &serverId, int containerIndex, int protocolIndex); + void updateContainer(const QString &serverId, int containerIndex, int protocolIndex, bool closePage = true); void removeServer(const QString &serverId); void rebootServer(const QString &serverId); void removeAllContainers(const QString &serverId); void removeContainer(const QString &serverId, int containerIndex); + void setContainerEnabled(const QString &serverId, int containerIndex, bool enabled); + void refreshContainerStatus(const QString &serverId, int containerIndex); + void refreshContainerDiagnostics(const QString &serverId, int containerIndex, int port); + void fetchContainerSecret(const QString &serverId, int containerIndex); void clearCachedProfile(const QString &serverId, int containerIndex); @@ -94,7 +100,7 @@ signals: void installContainerFinished(const QString &finishMessage, bool isServiceInstall); void installServerFinished(const QString &finishMessage); - void updateContainerFinished(const QString &message); + void updateContainerFinished(const QString &message, bool closePage); void scanServerFinished(bool isInstalledContainerFound); @@ -102,6 +108,11 @@ signals: void removeServerFinished(const QString &finishedMessage); void removeAllContainersFinished(const QString &finishedMessage); void removeContainerFinished(const QString &finishedMessage); + void setContainerEnabledFinished(bool enabled); + void containerStatusRefreshed(int status); + void containerDiagnosticsRefreshed(bool portReachable, bool upstreamReachable, int clientsConnected, + const QString &lastConfigRefresh, const QString &statsEndpoint); + void containerSecretFetched(const QString &secret); void installationErrorOccurred(ErrorCode errorCode); void wrongInstallationUser(const QString &message); @@ -114,6 +125,8 @@ signals: void serverIsBusy(const bool isBusy); void cancelInstallation(); + void currentContainerUpdated(); + void cachedProfileCleared(const QString &message); void apiConfigRemoved(const QString &message); @@ -138,6 +151,7 @@ private: #endif SftpConfigModel* m_sftpConfigModel; Socks5ProxyConfigModel* m_socks5ConfigModel; + MtProxyConfigModel* m_mtProxyConfigModel; ServerCredentials m_processedServerCredentials; diff --git a/client/ui/controllers/serversUiController.cpp b/client/ui/controllers/serversUiController.cpp index a5c0741f5..c2e3955b6 100644 --- a/client/ui/controllers/serversUiController.cpp +++ b/client/ui/controllers/serversUiController.cpp @@ -510,6 +510,8 @@ QStringList ServersUiController::getAllInstalledServicesName(int serverIndex) co servicesName.append("TOR"); } else if (container == DockerContainer::Socks5Proxy) { servicesName.append("SOCKS5"); + } else if (container == DockerContainer::MtProxy) { + servicesName.append("MTProxy"); } } } diff --git a/client/ui/models/containersModel.cpp b/client/ui/models/containersModel.cpp index 335ddbe7c..ade74f984 100644 --- a/client/ui/models/containersModel.cpp +++ b/client/ui/models/containersModel.cpp @@ -74,6 +74,7 @@ QVariant ContainersModel::data(const QModelIndex &index, int role) const case IsSftpRole: return container == DockerContainer::Sftp; case IsTorWebsiteRole: return container == DockerContainer::TorWebSite; case IsSocks5ProxyRole: return container == DockerContainer::Socks5Proxy; + case IsMtProxyRole: return container == DockerContainer::MtProxy; case InstallPageOrderRole: return ContainerUtils::installPageOrder(container); } @@ -184,5 +185,6 @@ QHash ContainersModel::roleNames() const roles[IsSftpRole] = "isSftp"; roles[IsTorWebsiteRole] = "isTorWebsite"; roles[IsSocks5ProxyRole] = "isSocks5Proxy"; + roles[IsMtProxyRole] = "isMtProxy"; return roles; } diff --git a/client/ui/models/containersModel.h b/client/ui/models/containersModel.h index e5f71b01d..d88628d91 100644 --- a/client/ui/models/containersModel.h +++ b/client/ui/models/containersModel.h @@ -48,7 +48,8 @@ public: IsDnsRole, IsSftpRole, IsTorWebsiteRole, - IsSocks5ProxyRole + IsSocks5ProxyRole, + IsMtProxyRole, }; Q_INVOKABLE void openContainerSettings(int containerIndex); diff --git a/client/ui/models/protocolsModel.cpp b/client/ui/models/protocolsModel.cpp index 773a92344..c0cbe99ce 100644 --- a/client/ui/models/protocolsModel.cpp +++ b/client/ui/models/protocolsModel.cpp @@ -42,6 +42,7 @@ QHash ProtocolsModel::roleNames() const roles[IsSftpRole] = "isSftp"; roles[IsIpsecRole] = "isIpsec"; roles[IsSocks5ProxyRole] = "isSocks5Proxy"; + roles[IsMtProxyRole] = "isMtProxy"; return roles; } @@ -71,6 +72,7 @@ QVariant ProtocolsModel::data(const QModelIndex &index, int role) const case IsSftpRole: return proto == Proto::Sftp; case IsIpsecRole: return proto == Proto::Ikev2; case IsSocks5ProxyRole: return proto == Proto::Socks5Proxy; + case IsMtProxyRole: return proto == Proto::MtProxy; case RawConfigRole: return getRawConfig(); case IsClientProtocolExistsRole: @@ -124,6 +126,7 @@ PageLoader::PageEnum ProtocolsModel::serverProtocolPage(Proto protocol) const case Proto::Dns: return PageLoader::PageEnum::PageServiceDnsSettings; case Proto::Sftp: return PageLoader::PageEnum::PageServiceSftpSettings; case Proto::Socks5Proxy: return PageLoader::PageEnum::PageServiceSocksProxySettings; + case Proto::MtProxy: return PageLoader::PageEnum::PageServiceMtProxySettings; default: return PageLoader::PageEnum::PageProtocolOpenVpnSettings; } } diff --git a/client/ui/models/protocolsModel.h b/client/ui/models/protocolsModel.h index a10ab8731..01f9b9cd4 100644 --- a/client/ui/models/protocolsModel.h +++ b/client/ui/models/protocolsModel.h @@ -25,7 +25,8 @@ public: IsXrayRole, IsSftpRole, IsIpsecRole, - IsSocks5ProxyRole + IsSocks5ProxyRole, + IsMtProxyRole, }; explicit ProtocolsModel(QObject *parent = nullptr); diff --git a/client/ui/models/services/mtProxyConfigModel.cpp b/client/ui/models/services/mtProxyConfigModel.cpp new file mode 100644 index 000000000..5e68d786a --- /dev/null +++ b/client/ui/models/services/mtProxyConfigModel.cpp @@ -0,0 +1,714 @@ +#include "mtProxyConfigModel.h" + +#include "ui/models/utils/mtproxy_public_host_input.h" + +#include "core/utils/networkUtilities.h" +#include "core/utils/qrCodeUtils.h" +#include "core/utils/constants/protocolConstants.h" +#include "core/utils/constants/configKeys.h" +#include "qrcodegen.hpp" + +#include +#include +#include +#include +#include +#include +#include + +using namespace amnezia; + +MtProxyConfigModel::MtProxyConfigModel(QObject *parent) : QAbstractListModel(parent) { + qmlRegisterType("MtProxyConfig", 1, 0, "PublicHostInputValidator"); +} + +int MtProxyConfigModel::rowCount(const QModelIndex &parent) const { + Q_UNUSED(parent); + return 1; +} + +bool MtProxyConfigModel::setData(const QModelIndex &index, const QVariant &value, int role) { + if (!index.isValid() || index.row() != 0) { + return false; + } + + switch (role) { + case Roles::PortRole: { + m_protocolConfig.port = value.toString(); + break; + } + case Roles::SecretRole: { + m_protocolConfig.secret = value.toString(); + break; + } + case Roles::TagRole: { + const QString tag = sanitizeMtProxyTagFieldText(value.toString()); + if (!isValidMtProxyTag(tag)) { + return false; + } + m_protocolConfig.tag = tag; + break; + } + case Roles::IsEnabledRole: { + m_protocolConfig.isEnabled = value.toBool(); + break; + } + case Roles::PublicHostRole: { + const QString h = value.toString().trimmed(); + if (!isValidPublicHost(h)) { + return false; + } + m_protocolConfig.publicHost = h; + break; + } + case Roles::TransportModeRole: { + m_protocolConfig.transportMode = value.toString(); + break; + } + case Roles::TlsDomainRole: { + const QString d = value.toString().trimmed(); + if (!isValidFakeTlsDomain(d)) { + return false; + } + m_protocolConfig.tlsDomain = d; + break; + } + case Roles::AdditionalSecretsRole: { + m_protocolConfig.additionalSecrets = value.toStringList(); + break; + } + case Roles::WorkersModeRole: { + m_protocolConfig.workersMode = value.toString(); + break; + } + case Roles::WorkersRole: { + m_protocolConfig.workers = value.toString(); + break; + } + case Roles::NatEnabledRole: { + m_protocolConfig.natEnabled = value.toBool(); + break; + } + case Roles::NatInternalIpRole: { + const QString ip = value.toString().trimmed(); + if (!isValidOptionalIpv4(ip)) { + return false; + } + m_protocolConfig.natInternalIp = ip; + break; + } + case Roles::NatExternalIpRole: { + const QString ip = value.toString().trimmed(); + if (!isValidOptionalIpv4(ip)) { + return false; + } + m_protocolConfig.natExternalIp = ip; + break; + } + default: { + return false; + } + } + + emit dataChanged(index, index, QList{role}); + return true; +} + +QVariant MtProxyConfigModel::data(const QModelIndex &index, int role) const { + if (!index.isValid() || index.row() != 0) { + return QVariant(); + } + + switch (role) { + case Roles::PortRole: { + return m_protocolConfig.port.isEmpty() ? QString(protocols::mtProxy::defaultPort) : m_protocolConfig.port; + } + case Roles::SecretRole: { + return m_protocolConfig.secret; + } + case Roles::TagRole: { + return m_protocolConfig.tag; + } + case Roles::TgLinkRole: { + return m_protocolConfig.tgLink; + } + case Roles::TmeLinkRole: { + return m_protocolConfig.tmeLink; + } + case Roles::IsEnabledRole: { + return m_protocolConfig.isEnabled; + } + case Roles::PublicHostRole: { + return m_protocolConfig.publicHost.isEmpty() + ? m_fullConfig.value(configKey::hostName).toString() + : m_protocolConfig.publicHost; + } + case Roles::TransportModeRole: { + return m_protocolConfig.transportMode.isEmpty() + ? QString(protocols::mtProxy::transportModeStandard) + : m_protocolConfig.transportMode; + } + case Roles::TlsDomainRole: { + return m_protocolConfig.tlsDomain; + } + case Roles::AdditionalSecretsRole: { + return m_protocolConfig.additionalSecrets; + } + case Roles::WorkersModeRole: { + return m_protocolConfig.workersMode.isEmpty() + ? QString(protocols::mtProxy::workersModeAuto) + : m_protocolConfig.workersMode; + } + case Roles::WorkersRole: { + return m_protocolConfig.workers.isEmpty() ? QString(protocols::mtProxy::defaultWorkers) + : m_protocolConfig.workers; + } + case Roles::NatEnabledRole: { + return m_protocolConfig.natEnabled; + } + case Roles::NatInternalIpRole: { + return m_protocolConfig.natInternalIp; + } + case Roles::NatExternalIpRole: { + return m_protocolConfig.natExternalIp; + } + } + + + return QVariant(); +} + +void MtProxyConfigModel::updateModel(amnezia::DockerContainer container, + const amnezia::MtProxyProtocolConfig &protocolConfig) { + beginResetModel(); + m_container = container; + m_protocolConfig = protocolConfig; + endResetModel(); +} + +void MtProxyConfigModel::updateModel(const QJsonObject &config) { + beginResetModel(); + + m_fullConfig = config; + m_protocolConfig = MtProxyProtocolConfig::fromJson(config.value(configKey::mtproxy).toObject()); + if (m_protocolConfig.port.isEmpty()) m_protocolConfig.port = protocols::mtProxy::defaultPort; + if (m_protocolConfig.transportMode.isEmpty()) m_protocolConfig.transportMode = protocols::mtProxy::transportModeStandard; + if (m_protocolConfig.workersMode.isEmpty()) m_protocolConfig.workersMode = protocols::mtProxy::workersModeAuto; + if (m_protocolConfig.workers.isEmpty()) m_protocolConfig.workers = protocols::mtProxy::defaultWorkers; + { + QString tagIn = sanitizeMtProxyTagFieldText(m_protocolConfig.tag); + if (!isValidMtProxyTag(tagIn)) { + tagIn.clear(); + } + m_protocolConfig.tag = tagIn; + } + + endResetModel(); +} + +QJsonObject MtProxyConfigModel::getConfig() { + m_fullConfig.insert(configKey::mtproxy, m_protocolConfig.toJson()); + return m_fullConfig; +} + +void MtProxyConfigModel::generateSecret() { + // Generate 16 random bytes = 32 hex chars + QString secret; + for (int i = 0; i < 16; ++i) { + quint32 byte = QRandomGenerator::global()->bounded(256); + secret += QString("%1").arg(byte, 2, 16, QChar('0')); + } + + m_protocolConfig.secret = secret; + emit dataChanged(index(0), index(0), QList{SecretRole}); +} + +void MtProxyConfigModel::setSecret(const QString &secret) { + if (secret.isEmpty()) { + return; + } + setData(index(0), secret, SecretRole); +} + +bool MtProxyConfigModel::validateAndSetSecret(const QString &rawSecret) { + if (!QRegularExpression("^[0-9a-fA-F]{32}$").match(rawSecret).hasMatch()) { + return false; + } + setData(index(0), rawSecret, SecretRole); + return true; +} + +void MtProxyConfigModel::setPort(const QString &port) { + setData(index(0), port, PortRole); +} + +void MtProxyConfigModel::setTag(const QString &tag) { + setData(index(0), tag, TagRole); +} + +void MtProxyConfigModel::setPublicHost(const QString &host) { + const QString t = host.trimmed(); + if (!isValidPublicHost(t)) { + return; + } + setData(index(0), t, PublicHostRole); +} + +void MtProxyConfigModel::setTransportMode(const QString &mode) { + setData(index(0), mode, TransportModeRole); +} + +QString MtProxyConfigModel::getTransportMode() const { + return m_protocolConfig.transportMode.isEmpty() + ? QString(protocols::mtProxy::transportModeStandard) + : m_protocolConfig.transportMode; +} + +QString MtProxyConfigModel::getTlsDomain() const { + return m_protocolConfig.tlsDomain.isEmpty() + ? QString(protocols::mtProxy::defaultTlsDomain) + : m_protocolConfig.tlsDomain; +} + +QString MtProxyConfigModel::getPublicHost() const { + return m_protocolConfig.publicHost; +} + +void MtProxyConfigModel::setTlsDomain(const QString &domain) { + const QString t = domain.trimmed(); + if (!isValidFakeTlsDomain(t)) { + return; + } + setData(index(0), t, TlsDomainRole); +} + +void MtProxyConfigModel::setWorkersMode(const QString &mode) { + setData(index(0), mode, WorkersModeRole); +} + +void MtProxyConfigModel::setWorkers(const QString &workers) { + setData(index(0), workers, WorkersRole); +} + +void MtProxyConfigModel::setNatEnabled(bool enabled) { + setData(index(0), enabled, NatEnabledRole); +} + +void MtProxyConfigModel::setNatInternalIp(const QString &ip) { + const QString t = ip.trimmed(); + if (!isValidOptionalIpv4(t)) { + return; + } + setData(index(0), t, NatInternalIpRole); +} + +void MtProxyConfigModel::setNatExternalIp(const QString &ip) { + const QString t = ip.trimmed(); + if (!isValidOptionalIpv4(t)) { + return; + } + setData(index(0), t, NatExternalIpRole); +} + +void MtProxyConfigModel::addAdditionalSecret() { + QString newSecret; + for (int i = 0; i < 16; ++i) { + quint32 byte = QRandomGenerator::global()->bounded(256); + newSecret += QString("%1").arg(byte, 2, 16, QChar('0')); + } + + m_protocolConfig.additionalSecrets.append(newSecret); + emit dataChanged(index(0), index(0), QList{AdditionalSecretsRole}); +} + +void MtProxyConfigModel::removeAdditionalSecret(int idx) { + if (idx < 0 || idx >= m_protocolConfig.additionalSecrets.size()) { + return; + } + m_protocolConfig.additionalSecrets.removeAt(idx); + emit dataChanged(index(0), index(0), QList{AdditionalSecretsRole}); +} + +QVariantList MtProxyConfigModel::additionalSecretsList() const { + QVariantList out; + out.reserve(m_protocolConfig.additionalSecrets.size()); + for (const auto &s: m_protocolConfig.additionalSecrets) { + if (!s.isEmpty()) { + out.append(s); + } + } + return out; +} + +void MtProxyConfigModel::setEnabled(bool enabled) { + m_protocolConfig.isEnabled = enabled; + emit dataChanged(index(0), index(0), QList{IsEnabledRole}); +} + +QString MtProxyConfigModel::generateQrCode(const QString &text) { + if (text.isEmpty()) { + return ""; + } + auto qr = qrCodeUtils::generateQrCode(text.toUtf8()); + return qrCodeUtils::svgToBase64(QString::fromStdString(toSvgString(qr, 1))); +} + +QString MtProxyConfigModel::defaultTlsDomain() const { + return protocols::mtProxy::defaultTlsDomain; +} + +QString MtProxyConfigModel::defaultPort() const { + return protocols::mtProxy::defaultPort; +} + +QString MtProxyConfigModel::defaultWorkers() const { + return protocols::mtProxy::defaultWorkers; +} + +int MtProxyConfigModel::maxWorkers() const { + return protocols::mtProxy::maxWorkers; +} + +QString MtProxyConfigModel::transportModeStandard() const { + return protocols::mtProxy::transportModeStandard; +} + +QString MtProxyConfigModel::transportModeFakeTLS() const { + return protocols::mtProxy::transportModeFakeTLS; +} + +QString MtProxyConfigModel::workersModeAuto() const { + return protocols::mtProxy::workersModeAuto; +} + +QString MtProxyConfigModel::workersModeManual() const { + return protocols::mtProxy::workersModeManual; +} + +bool MtProxyConfigModel::isValidPublicHost(const QString &host) const { + const QString t = host.trimmed(); + if (t.isEmpty()) { + return true; + } + if (t.length() > 253) { + return false; + } + QHostAddress a(t); + if (a.protocol() == QHostAddress::IPv4Protocol) { + return NetworkUtilities::checkIPv4Format(t); + } + if (a.protocol() == QHostAddress::IPv6Protocol) { + return true; + } + static const QRegularExpression onlyAsciiDigits(QStringLiteral(R"(^\d+$)")); + if (onlyAsciiDigits.match(t).hasMatch()) { + return false; + } + return NetworkUtilities::domainRegExp().exactMatch(t); +} + +bool MtProxyConfigModel::isPublicHostInputAllowed(const QString &text) const { + return mtproxyPublicHostInputAllowed(text); +} + +bool MtProxyConfigModel::isPublicHostTypingIncomplete(const QString &text) const { + const QString t = text.trimmed(); + if (isValidPublicHost(t)) { + return false; + } + + static const QRegularExpression onlyDigitDot(QStringLiteral(R"(^[0-9.]+$)")); + if (onlyDigitDot.match(t).hasMatch()) { + if (t.endsWith(QLatin1Char('.'))) { + return true; + } + const QStringList parts = t.split(QLatin1Char('.'), Qt::KeepEmptyParts); + if (parts.size() < 4) { + return true; + } + for (const QString &part: parts) { + if (part.isEmpty()) { + return true; + } + } + return false; + } + + if (t.contains(QLatin1Char(':'))) { + if (t.contains(QLatin1String(":::"))) { + return false; + } + if (t.endsWith(QLatin1Char(':'))) { + return true; + } + QHostAddress a(t); + if (a.protocol() == QHostAddress::IPv6Protocol) { + return false; + } + if (!t.contains(QLatin1String("::")) && t.count(QLatin1Char(':')) < 7 && !t.contains(QLatin1Char('.'))) { + return true; + } + return false; + } + + if (!t.contains(QLatin1Char('.'))) { + return true; + } + + return false; +} + +bool MtProxyConfigModel::isValidMtProxyTag(const QString &tag) const { + if (tag.isEmpty()) { + return true; + } + static const QRegularExpression re( + QStringLiteral("^([0-9a-fA-F]{%1})$").arg(protocols::mtProxy::botTagHexLength)); + return re.match(tag).hasMatch(); +} + +bool MtProxyConfigModel::isMtProxyTagTypingIncomplete(const QString &text) const { + const QString t = text.trimmed(); + if (t.isEmpty()) { + return true; + } + static const QRegularExpression hexOnly(QStringLiteral(R"(^[0-9a-fA-F]*$)")); + if (!hexOnly.match(t).hasMatch()) { + return false; + } + return t.size() < protocols::mtProxy::botTagHexLength; +} + +int MtProxyConfigModel::mtProxyBotTagHexLength() const { + return protocols::mtProxy::botTagHexLength; +} + +bool MtProxyConfigModel::isValidFakeTlsDomain(const QString &domain) const { + const QString t = domain.trimmed(); + if (t.isEmpty()) { + return true; + } + if (t.length() > 253) { + return false; + } + QHostAddress addr; + if (addr.setAddress(t)) { + return false; + } + static const QRegularExpression onlyAsciiDigits(QStringLiteral(R"(^\d+$)")); + if (onlyAsciiDigits.match(t).hasMatch()) { + return false; + } + QRegExp re(NetworkUtilities::domainRegExp()); + re.setCaseSensitivity(Qt::CaseInsensitive); + if (!re.exactMatch(t)) { + return false; + } + // ee + 32 hex (base secret) + hex(UTF-8 domain); keep headroom under typical client limits. + if (t.toUtf8().size() > 111) { + return false; + } + return true; +} + +QString MtProxyConfigModel::clipboardText() const { + if (QClipboard *c = QGuiApplication::clipboard()) { + return c->text(); + } + return QString(); +} + +QString MtProxyConfigModel::sanitizeFakeTlsDomainFieldText(const QString &input) const { + const QString t = normalizeFakeTlsDomainInput(input); + QString out; + out.reserve(t.size()); + for (const QChar &c: t) { + const ushort u = c.unicode(); + const bool letter = (u >= 'a' && u <= 'z') || (u >= 'A' && u <= 'Z'); + const bool digit = (u >= '0' && u <= '9'); + if (letter || digit || u == '.' || u == '-') { + out.append(c); + } + } + if (out.size() > 253) { + out.truncate(253); + } + return out; +} + +bool MtProxyConfigModel::isFakeTlsDomainInputAllowed(const QString &text) const { + if (text.length() > 253) { + return false; + } + static const QRegularExpression re(QStringLiteral(R"(^[a-zA-Z0-9.-]*$)")); + return re.match(text).hasMatch(); +} + +QString MtProxyConfigModel::sanitizePublicHostFieldText(const QString &input) const { + QString out; + const int cap = qMin(input.size(), 253); + out.reserve(cap); + for (const QChar &c: input) { + if (out.size() >= 253) { + break; + } + const ushort u = c.unicode(); + if ((u >= 'a' && u <= 'z') || (u >= 'A' && u <= 'Z') || (u >= '0' && u <= '9') || u == '.' || u == ':' || + u == '-') { + out.append(c); + } + } + return out; +} + +QString MtProxyConfigModel::sanitizePortFieldText(const QString &input) const { + QString out; + out.reserve(qMin(input.size(), 5)); + for (const QChar &c: input) { + const ushort u = c.unicode(); + if (u >= '0' && u <= '9' && out.size() < 5) { + out.append(c); + } + } + return out; +} + +QString MtProxyConfigModel::sanitizeMtProxyTagFieldText(const QString &input) const { + QString trimmed = input.trimmed(); + if (trimmed.startsWith(QLatin1String("0x"), Qt::CaseInsensitive)) { + trimmed = trimmed.mid(2).trimmed(); + } + // Prefer a contiguous 32-hex run (paste from bot message with extra text). + static const QRegularExpression runHex(QStringLiteral(R"(([0-9a-fA-F]{32}))")); + const QRegularExpressionMatch m = runHex.match(trimmed); + if (m.hasMatch()) { + return m.captured(1); + } + const int cap = protocols::mtProxy::botTagHexLength; + QString out; + out.reserve(qMin(trimmed.size(), cap)); + for (const QChar &c: trimmed) { + if (out.size() >= cap) { + break; + } + const ushort u = c.unicode(); + if ((u >= '0' && u <= '9') || (u >= 'a' && u <= 'f') || (u >= 'A' && u <= 'F')) { + out.append(c); + } + } + return out; +} + +QString MtProxyConfigModel::sanitizeWorkersFieldText(const QString &input) const { + QString out; + out.reserve(qMin(input.size(), 3)); + for (const QChar &c: input) { + const ushort u = c.unicode(); + if (u >= '0' && u <= '9' && out.size() < 3) { + out.append(c); + } + } + return out; +} + +QString MtProxyConfigModel::sanitizeOptionalIpv4FieldText(const QString &input) const { + QString out; + out.reserve(qMin(input.size(), 15)); + for (const QChar &c: input) { + if (out.size() >= 15) { + break; + } + const ushort u = c.unicode(); + if ((u >= '0' && u <= '9') || u == '.') { + out.append(c); + } + } + return out; +} + +QString MtProxyConfigModel::normalizeFakeTlsDomainInput(const QString &input) const { + QString t = input.trimmed(); + if (t.startsWith(QLatin1String("https://"), Qt::CaseInsensitive)) { + t = t.mid(8); + } else if (t.startsWith(QLatin1String("http://"), Qt::CaseInsensitive)) { + t = t.mid(7); + } + if (const int slash = t.indexOf(QLatin1Char('/')); slash >= 0) { + t = t.left(slash); + } + if (const int at = t.indexOf(QLatin1Char('@')); at >= 0) { + t = t.mid(at + 1); + } + if (const int colon = t.indexOf(QLatin1Char(':')); colon >= 0) { + t = t.left(colon); + } + if (t.startsWith(QLatin1String("www."), Qt::CaseInsensitive)) { + const QString rest = t.mid(4); + if (rest.contains(QLatin1Char('.'))) { + t = rest; + } + } + return t.trimmed(); +} + +bool MtProxyConfigModel::isFakeTlsDomainTypingIncomplete(const QString &text) const { + const QString t = text.trimmed(); + if (t.isEmpty()) { + return true; + } + if (isValidFakeTlsDomain(t)) { + return false; + } + if (t.contains(QLatin1Char('/')) || t.contains(QLatin1Char(':')) || t.contains(QLatin1Char('@')) + || t.contains(QLatin1Char(' '))) { + return false; + } + if (t.contains(QLatin1String(".."))) { + return false; + } + if (!t.contains(QLatin1Char('.'))) { + return true; + } + if (t.endsWith(QLatin1Char('.'))) { + return true; + } + static const QRegularExpression legalPartial(QStringLiteral(R"(^[a-zA-Z0-9.-]*$)")); + if (!legalPartial.match(t).hasMatch()) { + return false; + } + return true; +} + +bool MtProxyConfigModel::isValidOptionalIpv4(const QString &ip) const { + const QString t = ip.trimmed(); + if (t.isEmpty()) { + return true; + } + return NetworkUtilities::checkIPv4Format(t); +} + +QHash MtProxyConfigModel::roleNames() const { + QHash roles; + + roles[PortRole] = "port"; + roles[SecretRole] = "secret"; + roles[TagRole] = "tag"; + roles[TgLinkRole] = "tgLink"; + roles[TmeLinkRole] = "tmeLink"; + roles[IsEnabledRole] = "isEnabled"; + roles[PublicHostRole] = "publicHost"; + roles[TransportModeRole] = "transportMode"; + roles[TlsDomainRole] = "tlsDomain"; + roles[AdditionalSecretsRole] = "additionalSecrets"; + roles[WorkersModeRole] = "workersMode"; + roles[WorkersRole] = "workers"; + roles[NatEnabledRole] = "natEnabled"; + roles[NatInternalIpRole] = "natInternalIp"; + roles[NatExternalIpRole] = "natExternalIp"; + + return roles; +} + +amnezia::MtProxyProtocolConfig MtProxyConfigModel::getProtocolConfig() { + return m_protocolConfig; +} diff --git a/client/ui/models/services/mtProxyConfigModel.h b/client/ui/models/services/mtProxyConfigModel.h new file mode 100644 index 000000000..b67969ed4 --- /dev/null +++ b/client/ui/models/services/mtProxyConfigModel.h @@ -0,0 +1,156 @@ +#ifndef MTPROXYCONFIGMODEL_H +#define MTPROXYCONFIGMODEL_H + +#include +#include +#include +#include +#include "core/utils/containerEnum.h" +#include "core/utils/containers/containerUtils.h" +#include "core/utils/protocolEnum.h" +#include "core/models/protocols/mtProxyProtocolConfig.h" + +class MtProxyConfigModel : public QAbstractListModel { +Q_OBJECT + +public: + enum Roles { + PortRole = Qt::UserRole + 1, + SecretRole, + TagRole, + TgLinkRole, + TmeLinkRole, + IsEnabledRole, + PublicHostRole, + TransportModeRole, + TlsDomainRole, + AdditionalSecretsRole, + WorkersModeRole, + WorkersRole, + NatEnabledRole, + NatInternalIpRole, + NatExternalIpRole + }; + + explicit MtProxyConfigModel(QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + +public slots: + + void updateModel(amnezia::DockerContainer container, const amnezia::MtProxyProtocolConfig &protocolConfig); + + void updateModel(const QJsonObject &config); + + QJsonObject getConfig(); + + amnezia::MtProxyProtocolConfig getProtocolConfig(); + + Q_INVOKABLE void generateSecret(); + + Q_INVOKABLE void setSecret(const QString &secret); + + Q_INVOKABLE bool validateAndSetSecret(const QString &rawSecret); + + Q_INVOKABLE void setPort(const QString &port); + + Q_INVOKABLE void setTag(const QString &tag); + + Q_INVOKABLE void setPublicHost(const QString &host); + + Q_INVOKABLE void setTransportMode(const QString &mode); + + Q_INVOKABLE QString getTransportMode() const; + + Q_INVOKABLE QString getTlsDomain() const; + + Q_INVOKABLE QString getPublicHost() const; + + Q_INVOKABLE void setTlsDomain(const QString &domain); + + Q_INVOKABLE void setWorkersMode(const QString &mode); + + Q_INVOKABLE void setWorkers(const QString &workers); + + Q_INVOKABLE void setNatEnabled(bool enabled); + + Q_INVOKABLE void setNatInternalIp(const QString &ip); + + Q_INVOKABLE void setNatExternalIp(const QString &ip); + + Q_INVOKABLE void addAdditionalSecret(); + + Q_INVOKABLE void removeAdditionalSecret(int idx); + /// Current `mtproxy_additional_secrets` list from in-memory config (for QML snapshot vs. unsaved adds). + Q_INVOKABLE QVariantList additionalSecretsList() const; + + Q_INVOKABLE QString generateQrCode(const QString &text); + + Q_INVOKABLE void setEnabled(bool enabled); + + Q_INVOKABLE QString defaultTlsDomain() const; + + Q_INVOKABLE QString defaultPort() const; + + Q_INVOKABLE QString defaultWorkers() const; + + Q_INVOKABLE int maxWorkers() const; + + Q_INVOKABLE QString transportModeStandard() const; + + Q_INVOKABLE QString transportModeFakeTLS() const; + + Q_INVOKABLE QString workersModeAuto() const; + + Q_INVOKABLE QString workersModeManual() const; + + Q_INVOKABLE bool isValidPublicHost(const QString &host) const; + + Q_INVOKABLE bool isPublicHostInputAllowed(const QString &text) const; + + Q_INVOKABLE bool isPublicHostTypingIncomplete(const QString &text) const; + + Q_INVOKABLE bool isValidMtProxyTag(const QString &tag) const; + + Q_INVOKABLE bool isMtProxyTagTypingIncomplete(const QString &text) const; + + Q_INVOKABLE int mtProxyBotTagHexLength() const; + + Q_INVOKABLE bool isValidFakeTlsDomain(const QString &domain) const; + + Q_INVOKABLE QString normalizeFakeTlsDomainInput(const QString &input) const; + + Q_INVOKABLE QString sanitizeFakeTlsDomainFieldText(const QString &input) const; + + Q_INVOKABLE bool isFakeTlsDomainInputAllowed(const QString &text) const; + + Q_INVOKABLE QString clipboardText() const; + + Q_INVOKABLE QString sanitizePublicHostFieldText(const QString &input) const; + + Q_INVOKABLE QString sanitizePortFieldText(const QString &input) const; + + Q_INVOKABLE QString sanitizeMtProxyTagFieldText(const QString &input) const; + + Q_INVOKABLE QString sanitizeWorkersFieldText(const QString &input) const; + + Q_INVOKABLE QString sanitizeOptionalIpv4FieldText(const QString &input) const; + + Q_INVOKABLE bool isFakeTlsDomainTypingIncomplete(const QString &text) const; + + Q_INVOKABLE bool isValidOptionalIpv4(const QString &ip) const; + +protected: + QHash roleNames() const override; + +private: + amnezia::DockerContainer m_container; + QJsonObject m_fullConfig; + amnezia::MtProxyProtocolConfig m_protocolConfig; +}; + +#endif // MTPROXYCONFIGMODEL_H diff --git a/client/ui/models/utils/mtproxy_public_host_input.cpp b/client/ui/models/utils/mtproxy_public_host_input.cpp new file mode 100644 index 000000000..2cbf0b2f7 --- /dev/null +++ b/client/ui/models/utils/mtproxy_public_host_input.cpp @@ -0,0 +1,127 @@ +#include "mtproxy_public_host_input.h" + +#include + +namespace { + + bool ipv4OctetTokenOk(const QString &s) { + static const QRegularExpression re(QStringLiteral(R"(^\d{1,3}$)")); + if (!re.match(s).hasMatch()) { + return false; + } + bool ok = false; + const int n = s.toInt(&ok); + return ok && n >= 0 && n <= 255; + } + +// Reject labels like "312edweqwe" (digits >255 then letters). + bool labelHasInvalidOctetLikePrefixBeforeLetters(const QString &label) { + static const QRegularExpression re(QStringLiteral(R"(^(\d+)([a-zA-Z].*)$)")); + const QRegularExpressionMatch m = re.match(label); + if (!m.hasMatch()) { + return false; + } + const QString digits = m.captured(1); + if (digits.length() > 3) { + return true; + } + bool ok = false; + const int n = digits.toInt(&ok); + if (!ok) { + return true; + } + if (n > 255) { + return true; + } + // Do not restrict n≤255 + letters here (e.g. "123mlkjh.example.com"); four-segment IPv4+junk is handled below. + return false; + } + +// "123.123wqqweqweqweqwe" — first label is a real octet, second looks like an octet glued to letters (not "123.45"). + bool looksLikeTwoSegmentOctetThenDigitLetterGlue(const QString &text) { + const QStringList parts = text.split(QLatin1Char('.'), Qt::KeepEmptyParts); + if (parts.size() != 2) { + return false; + } + if (!ipv4OctetTokenOk(parts.at(0))) { + return false; + } + const QString &p1 = parts.at(1); + static const QRegularExpression digitThenLetter(QStringLiteral(R"(^\d+[a-zA-Z])")); + if (!digitThenLetter.match(p1).hasMatch()) { + return false; + } + return !ipv4OctetTokenOk(p1); + } + +// "a.b.c.djunk" where first three parts are pure octets and last part has digits then letters (e.g. "123wdqweqweqwe"). + bool looksLikeFourOctetIpv4WithGarbageInLastSegment(const QString &text) { + const QStringList parts = text.split(QLatin1Char('.'), Qt::KeepEmptyParts); + if (parts.size() != 4) { + return false; + } + for (int i = 0; i < 3; ++i) { + if (!ipv4OctetTokenOk(parts.at(i))) { + return false; + } + } + static const QRegularExpression digitThenLetter(QStringLiteral(R"(^\d+[a-zA-Z])")); + return digitThenLetter.match(parts.at(3)).hasMatch(); + } + + bool hostLabelsRejectBrokenDigitLetterMix(const QString &text) { + if (looksLikeTwoSegmentOctetThenDigitLetterGlue(text)) { + return false; + } + if (looksLikeFourOctetIpv4WithGarbageInLastSegment(text)) { + return false; + } + const QStringList parts = text.split(QLatin1Char('.'), Qt::KeepEmptyParts); + for (const QString &part: parts) { + if (labelHasInvalidOctetLikePrefixBeforeLetters(part)) { + return false; + } + } + return true; + } + +} // namespace + +bool mtproxyPublicHostInputAllowed(const QString &text) { + if (text.length() > 253) { + return false; + } + static const QRegularExpression allowed(QStringLiteral(R"(^[a-zA-Z0-9.:\-]*$)")); + if (!allowed.match(text).hasMatch()) { + return false; + } + static const QRegularExpression onlyDigits(QStringLiteral(R"(^\d+$)")); + if (onlyDigits.match(text).hasMatch() && text.length() > 3) { + return false; + } + static const QRegularExpression onlyDigitDot(QStringLiteral(R"(^[0-9.]+$)")); + if (!text.isEmpty() && onlyDigitDot.match(text).hasMatch()) { + static const QRegularExpression ipv4Partial(QStringLiteral(R"(^(\d{1,3}\.){0,3}\d{0,3}$)")); + return ipv4Partial.match(text).hasMatch(); + } + if (text.contains(QLatin1Char(':'))) { + static const QRegularExpression ipv6Chars(QStringLiteral(R"(^[0-9a-fA-F:.]*$)")); + if (!ipv6Chars.match(text).hasMatch()) { + return false; + } + if (text.size() > 45) { + return false; + } + } + if (!hostLabelsRejectBrokenDigitLetterMix(text)) { + return false; + } + return true; +} + +PublicHostInputValidator::PublicHostInputValidator(QObject *parent) : QValidator(parent) {} + +QValidator::State PublicHostInputValidator::validate(QString &input, int &pos) const { + Q_UNUSED(pos) + return mtproxyPublicHostInputAllowed(input) ? Acceptable : Invalid; +} diff --git a/client/ui/models/utils/mtproxy_public_host_input.h b/client/ui/models/utils/mtproxy_public_host_input.h new file mode 100644 index 000000000..9f3cffed4 --- /dev/null +++ b/client/ui/models/utils/mtproxy_public_host_input.h @@ -0,0 +1,20 @@ +#ifndef MTPROXY_PUBLIC_HOST_INPUT_H +#define MTPROXY_PUBLIC_HOST_INPUT_H + +#include + +#include + +/// Shared rules for public host field (IPv4 dotted partial, IPv6 hex, FQDN ASCII). +bool mtproxyPublicHostInputAllowed(const QString &text); + +class PublicHostInputValidator : public QValidator { +Q_OBJECT + +public: + explicit PublicHostInputValidator(QObject *parent = nullptr); + + QValidator::State validate(QString &input, int &pos) const override; +}; + +#endif diff --git a/client/ui/qml/Components/SettingsContainersListView.qml b/client/ui/qml/Components/SettingsContainersListView.qml index 209c56438..e560a2c8d 100644 --- a/client/ui/qml/Components/SettingsContainersListView.qml +++ b/client/ui/qml/Components/SettingsContainersListView.qml @@ -45,6 +45,9 @@ ListViewType { PageController.goToPage(PageEnum.PageProtocolRaw) } else if (isDns) { PageController.goToPage(PageEnum.PageServiceDnsSettings) + } else if (isMtProxy) { + MtProxyConfigModel.updateModel(config) + PageController.goToPage(PageEnum.PageServiceMtProxySettings) } else { InstallController.updateProtocols(ServersUiController.getServerId(ServersUiController.processedServerIndex), containerIndex) PageController.goToPage(PageEnum.PageSettingsServerProtocol) diff --git a/client/ui/qml/Controls2/ListViewWithRadioButtonType.qml b/client/ui/qml/Controls2/ListViewWithRadioButtonType.qml index abd4da3e2..9454caec5 100644 --- a/client/ui/qml/Controls2/ListViewWithRadioButtonType.qml +++ b/client/ui/qml/Controls2/ListViewWithRadioButtonType.qml @@ -31,6 +31,9 @@ ListViewType { function triggerCurrentItem() { var item = root.itemAtIndex(selectedIndex) + if (!item) { + return + } item.selectable.clicked() } diff --git a/client/ui/qml/Pages2/PageServiceMtProxySettings.qml b/client/ui/qml/Pages2/PageServiceMtProxySettings.qml new file mode 100644 index 000000000..a1c185104 --- /dev/null +++ b/client/ui/qml/Pages2/PageServiceMtProxySettings.qml @@ -0,0 +1,1885 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import SortFilterProxyModel 0.2 + +import PageEnum 1.0 +import ContainerProps 1.0 +import ProtocolEnum 1.0 +import Style 1.0 +import MtProxyConfig 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" +import "../Components" + + +PageType { + id: root + + Rectangle { + anchors.fill: parent + z: -1 + color: AmneziaStyle.color.onyxBlack + } + + property int containerStatus: 1 + property bool isUpdating: false + property bool isCheckingStatus: false + property bool previousEnabled: true + property int previousContainerStatus: 1 + + property string previousPort: "" + property string previousTag: "" + property string previousPublicHost: "" + property string previousTransportMode: MtProxyConfigModel.transportModeStandard() + property string previousTlsDomain: MtProxyConfigModel.defaultTlsDomain() + property string previousWorkersMode: MtProxyConfigModel.workersModeAuto() + property string previousWorkers: MtProxyConfigModel.defaultWorkers() + property bool previousNatEnabled: false + property string previousNatInternalIp: "" + property string previousNatExternalIp: "" + property string previousSecret: "" + + property string savedTransportMode: "" + property string savedTlsDomain: "" + property string savedPublicHost: "" + + onSavedTransportModeChanged: { + if (savedTransportMode === "faketls") { + root.syncedSecretTabIndex = 2 + } else if (savedTransportMode !== "") { + root.syncedSecretTabIndex = 0 + } + } + + property bool diagLoading: false + property int syncedSecretTabIndex: 0 + property bool pendingEnableAfterRestart: false + property bool pendingUpdateAfterEnable: false + property bool diagPortReachable: false + property bool diagTelegramReachable: false + property int diagClientsConnected: -1 + property string diagLastConfigRefresh: "" + property string diagStatsEndpoint: "" + + readonly property bool mtProxyNetworkBlocked: !NetworkReachabilityController.hasInternetAccess + + readonly property bool navigationBlockedWhileBusy: isUpdating || diagLoading + + // Hex values that exist in last loaded / last successfully saved config — show link panel only for these. + property var mtProxyPersistedAdditionalHex: [] + + function mtProxyRefreshPersistedAdditionalSecrets() { + var list = MtProxyConfigModel.additionalSecretsList() + var a = [] + for (var i = 0; i < list.length; ++i) { + a.push(String(list[i])) + } + root.mtProxyPersistedAdditionalHex = a + } + + function mtProxyIsPersistedAdditionalHex(hex) { + var h = String(hex) + for (var j = 0; j < root.mtProxyPersistedAdditionalHex.length; ++j) { + if (String(root.mtProxyPersistedAdditionalHex[j]) === h) { + return true + } + } + return false + } + + // Rejects garbage like "123123123123"; only dotted IPv4 shape (≤3 digits per octet, ≤4 octets). + readonly property var natIpv4InputFormat: /^(\d{1,3}\.){0,3}\d{0,3}$/ + + // Defer SSH/updateContainer so QML control handlers return before nested event loops run; + // avoids "Object destroyed while one of its QML signal handlers is in progress". + function mtProxyScheduleUpdate(closePage) { + var cp = closePage === undefined ? false : closePage + Qt.callLater(function () { + InstallController.updateContainer(ServersUiController.getServerId(ServersUiController.processedServerIndex), ServersUiController.processedContainerIndex, ProtocolEnum.MtProxy, cp) + }) + } + + // Optional IPv4: show invalid while typing only when the string looks complete (four octets), so partial entry is not nagged. + function natIpv4FieldShowInvalidError(text) { + var t = text ? String(text).replace(/^\s+|\s+$/g, '') : "" + if (t === "") + return false + if (MtProxyConfigModel.isValidOptionalIpv4(t)) + return false + var parts = t.split('.') + var j + for (j = 0; j < parts.length; j++) { + if (parts[j].length > 3) + return true + } + if (parts.length > 4) + return true + if (t.indexOf('.') < 0 && t.length > 3) + return true + if (t.endsWith('.')) + return false + if (parts.length < 4) + return false + for (var i = 0; i < parts.length; i++) { + if (parts[i] === "") + return true + } + return true + } + + function statusText() { + if (isCheckingStatus) { + return qsTr("Checking...") + } + if (isUpdating) { + return qsTr("Updating") + } + switch (containerStatus) { + case 0: { + return qsTr("Not deployed") + } + case 1: { + return qsTr("Running") + } + case 2: { + return qsTr("Stopped") + } + case 3: { + return qsTr("Error") + } + default: { + return qsTr("Unknown") + } + } + } + + Component.onCompleted: { + root.savedTransportMode = MtProxyConfigModel.getTransportMode() + root.savedTlsDomain = MtProxyConfigModel.getTlsDomain() + root.savedPublicHost = MtProxyConfigModel.getPublicHost() + + Qt.callLater(function () { + root.mtProxyRefreshPersistedAdditionalSecrets() + }) + + if (!NetworkReachabilityController.hasInternetAccess) { + isCheckingStatus = false + return + } + isCheckingStatus = true + InstallController.refreshContainerStatus(ServersUiController.getServerId(ServersUiController.processedServerIndex), ServersUiController.processedContainerIndex) + } + + // Block back navigation and Escape (via PageStart.isControlsDisabled) while SSH/update or diagnostics refresh runs. + onNavigationBlockedWhileBusyChanged: { + if (root.visible) { + PageController.disableControls(navigationBlockedWhileBusy) + } + } + + onVisibleChanged: { + if (!visible) { + PageController.disableControls(false) + diagLoading = false + } else { + PageController.disableControls(navigationBlockedWhileBusy) + } + } + + Connections { + target: NetworkReachabilityController + + function onHasInternetAccessChanged() { + if (!root.visible) { + return + } + if (NetworkReachabilityController.hasInternetAccess) { + isCheckingStatus = true + InstallController.refreshContainerStatus(ServersUiController.getServerId(ServersUiController.processedServerIndex), ServersUiController.processedContainerIndex) + } + } + } + + Connections { + target: InstallController + + function onUpdateContainerFinished(message, closePage) { + if (!root.visible) { + isUpdating = false + isCheckingStatus = false + return + } + isUpdating = false + containerStatus = 1 + root.savedTransportMode = MtProxyConfigModel.getTransportMode() + root.savedTlsDomain = MtProxyConfigModel.getTlsDomain() + root.savedPublicHost = MtProxyConfigModel.getPublicHost() + root.mtProxyRefreshPersistedAdditionalSecrets() + PageController.showNotificationMessage(message) + } + + function onInstallationErrorOccurred() { + if (!root.visible) { + isUpdating = false + isCheckingStatus = false + return + } + isUpdating = false + containerStatus = previousContainerStatus + MtProxyConfigModel.setEnabled(previousEnabled) + MtProxyConfigModel.setPort(previousPort) + MtProxyConfigModel.setTag(previousTag) + MtProxyConfigModel.setPublicHost(previousPublicHost) + MtProxyConfigModel.setTransportMode(previousTransportMode) + MtProxyConfigModel.setTlsDomain(previousTlsDomain) + MtProxyConfigModel.setWorkersMode(previousWorkersMode) + MtProxyConfigModel.setWorkers(previousWorkers) + MtProxyConfigModel.setNatEnabled(previousNatEnabled) + MtProxyConfigModel.setNatInternalIp(previousNatInternalIp) + MtProxyConfigModel.setNatExternalIp(previousNatExternalIp) + if (previousSecret !== "") { + MtProxyConfigModel.setSecret(previousSecret) + } + } + + function onSetContainerEnabledFinished(enabled) { + if (!root.visible) { + isUpdating = false + return + } + if (enabled && pendingUpdateAfterEnable) { + pendingUpdateAfterEnable = false + root.mtProxyScheduleUpdate(false) + return + } + isUpdating = false + containerStatus = enabled ? 1 : 2 + PageController.showNotificationMessage( + enabled ? qsTr("MTProxy started") : qsTr("MTProxy stopped")) + } + + function onContainerStatusRefreshed(status) { + if (!root.visible) { + isCheckingStatus = false + return + } + isCheckingStatus = false + containerStatus = status + + root.savedTransportMode = MtProxyConfigModel.getTransportMode() + root.savedTlsDomain = MtProxyConfigModel.getTlsDomain() + root.savedPublicHost = MtProxyConfigModel.getPublicHost() + if (status === 1) { + MtProxyConfigModel.setEnabled(true) + InstallController.fetchContainerSecret(ServersUiController.getServerId(ServersUiController.processedServerIndex), ServersUiController.processedContainerIndex) + } else if (status === 2) { + MtProxyConfigModel.setEnabled(false) + } + } + + function onContainerDiagnosticsRefreshed(portReachable, upstreamReachable, clientsConnected, lastConfigRefresh, statsEndpoint) { + if (!root.visible) { + return + } + diagLoading = false + diagPortReachable = portReachable + diagTelegramReachable = upstreamReachable + diagClientsConnected = clientsConnected + diagLastConfigRefresh = lastConfigRefresh + diagStatsEndpoint = statsEndpoint + } + + function onContainerSecretFetched(secret) { + if (!root.visible) { + return + } + MtProxyConfigModel.validateAndSetSecret(secret) + } + } + + BackButtonType { + id: backButton + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 20 + SettingsController.safeAreaTopMargin + onFocusChanged: { + if (this.activeFocus) connectionListView.positionViewAtBeginning() + } + } + + ColumnLayout { + id: pageHeader + anchors.top: backButton.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 8 + spacing: 0 + + BaseHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + headerText: qsTr("MTProxy settings") + } + + LabelWithButtonType { + Layout.fillWidth: true + Layout.leftMargin: 0 + Layout.rightMargin: 16 + text: qsTr("Read more about this settings") + textColor: AmneziaStyle.color.goldenApricot + clickedFunction: function () { + Qt.openUrlExternally("https://core.telegram.org/proxy") + } + } + + TabBar { + id: mainTabBar + Layout.fillWidth: true + Layout.topMargin: 4 + + background: Rectangle { + color: AmneziaStyle.color.transparent + Rectangle { + width: parent.width + height: 1 + anchors.bottom: parent.bottom + color: AmneziaStyle.color.slateGray + } + } + + TabButtonType { + text: qsTr("Connection") + isSelected: mainTabBar.currentIndex === 0 + } + TabButtonType { + text: qsTr("Settings") + isSelected: mainTabBar.currentIndex === 1 + } + } + } + + StackLayout { + id: tabContent + anchors.top: pageHeader.bottom + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + currentIndex: mainTabBar.currentIndex + + ListViewType { + id: connectionListView + model: MtProxyConfigModel + + delegate: ColumnLayout { + width: connectionListView.width + spacing: 0 + + function domainToHex(domain) { + var hex = "" + for (var i = 0; i < domain.length; i++) { + var code = domain.charCodeAt(i).toString(16) + hex += (code.length < 2 ? "0" : "") + code + } + return hex + } + + function secretForMode(mode) { + if (mode === "faketls") { + var domain = root.savedTlsDomain !== "" ? root.savedTlsDomain : MtProxyConfigModel.defaultTlsDomain() + return "ee" + secret + domainToHex(domain) + } else if (mode === "padded") { + return "dd" + secret + } + return secret + } + + property int secretTabIndex: root.syncedSecretTabIndex + + function activeSecret() { + if (root.syncedSecretTabIndex === 0) { + return secretForMode("standard") + } + if (root.syncedSecretTabIndex === 1) { + return secretForMode("padded") + } + return secretForMode("faketls") + } + + function effectiveSecret() { + return activeSecret() + } + + function effectiveHost() { + return root.savedPublicHost !== "" ? root.savedPublicHost : ServersModel.getProcessedServerData("hostName") + } + + function tmeLink() { + return "https://t.me/proxy?server=" + effectiveHost() + "&port=" + port + "&secret=" + activeSecret() + } + + function tgLink() { + return "tg://proxy?server=" + effectiveHost() + "&port=" + port + "&secret=" + activeSecret() + } + + CaptionTextType { + Layout.fillWidth: true + Layout.topMargin: 24 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + text: qsTr("Use Telegram connection link") + color: AmneziaStyle.color.mutedGray + } + + Rectangle { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + implicitHeight: linkRow.implicitHeight + 16 + color: AmneziaStyle.color.onyxBlack + radius: 8 + border.color: AmneziaStyle.color.slateGray + border.width: 1 + + RowLayout { + id: linkRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 + anchors.rightMargin: 8 + spacing: 4 + + CaptionTextType { + Layout.fillWidth: true + text: secret !== "" ? tmeLink() : qsTr("Deploy MTProxy first") + color: secret !== "" ? AmneziaStyle.color.goldenApricot : AmneziaStyle.color.mutedGray + elide: Text.ElideRight + maximumLineCount: 1 + font.pixelSize: 13 + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/qr-code.svg" + imageColor: AmneziaStyle.color.paleGray + visible: secret !== "" + onClicked: { + ExportController.generateQrFromString(tmeLink()) + PageController.goToShareConnectionPage( + qsTr("Telegram connection link"), + qsTr("MTProxy connection link"), + "", "", "") + } + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + visible: secret !== "" + onClicked: { + GC.copyToClipBoard(tmeLink()) + PageController.showNotificationMessage(qsTr("Copied")) + } + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + implicitHeight: tgLinkRow.implicitHeight + 16 + color: AmneziaStyle.color.onyxBlack + radius: 8 + border.color: AmneziaStyle.color.slateGray + border.width: 1 + visible: secret !== "" + + RowLayout { + id: tgLinkRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 + anchors.rightMargin: 8 + spacing: 4 + + CaptionTextType { + Layout.fillWidth: true + text: tgLink() + color: AmneziaStyle.color.goldenApricot + elide: Text.ElideRight + maximumLineCount: 1 + font.pixelSize: 13 + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/qr-code.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: { + ExportController.generateQrFromString(tgLink()) + PageController.goToShareConnectionPage( + qsTr("Telegram connection link"), + qsTr("MTProxy connection link"), + "", "", "") + } + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: { + GC.copyToClipBoard(tgLink()) + PageController.showNotificationMessage(qsTr("Copied")) + } + } + } + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + spacing: 4 + + CaptionTextType { + text: qsTr("Or enter the proxy details manually.") + color: AmneziaStyle.color.mutedGray + } + + CaptionTextType { + Layout.fillWidth: true + text: qsTr("How to do it") + color: AmneziaStyle.color.goldenApricot + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: Qt.openUrlExternally("https://core.telegram.org/proxy") + } + } + + Item { + Layout.fillWidth: true + } + } + + Rectangle { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 32 + implicitHeight: manualCol.implicitHeight + 8 + color: AmneziaStyle.color.onyxBlack + radius: 8 + border.color: AmneziaStyle.color.slateGray + border.width: 1 + + ColumnLayout { + id: manualCol + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.topMargin: 8 + spacing: 0 + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 12 + Layout.rightMargin: 8 + Layout.bottomMargin: 8 + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + CaptionTextType { + text: qsTr("Host") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + } + CaptionTextType { + Layout.fillWidth: true + text: effectiveHost() + color: AmneziaStyle.color.paleGray + elide: Text.ElideRight + } + } + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: { GC.copyToClipBoard(effectiveHost()) + PageController.showNotificationMessage(qsTr("Copied")) } + } + } + + DividerType { + Layout.fillWidth: true + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 12 + Layout.rightMargin: 8 + Layout.topMargin: 8 + Layout.bottomMargin: 8 + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + CaptionTextType { + text: qsTr("Port") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + } + CaptionTextType { + Layout.fillWidth: true + text: port + color: AmneziaStyle.color.paleGray + } + } + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: { GC.copyToClipBoard(port) + PageController.showNotificationMessage(qsTr("Copied")) } + } + } + + DividerType { + Layout.fillWidth: true + } + + ButtonGroup { + id: secretTabGroup + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 12 + Layout.rightMargin: 8 + Layout.topMargin: 4 + Layout.bottomMargin: 8 + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + CaptionTextType { + text: qsTr("Secret") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + } + CaptionTextType { + Layout.fillWidth: true + text: activeSecret() + color: AmneziaStyle.color.paleGray + wrapMode: Text.WrapAnywhere + font.pixelSize: 13 + } + } + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: { GC.copyToClipBoard(activeSecret()) + PageController.showNotificationMessage(qsTr("Copied")) } + } + } + } + } + + LabelWithButtonType { + id: removeButton + Layout.fillWidth: true + Layout.bottomMargin: 24 + Layout.leftMargin: 0 + Layout.rightMargin: 16 + visible: ServersModel.isProcessedServerHasWriteAccess() + text: qsTr("Delete MTProxy") + textColor: AmneziaStyle.color.vibrantRed + clickedFunction: function () { + var headerText = qsTr("Remove %1 from server?").arg(ContainersModel.getProcessedContainerName()) + var descriptionText = qsTr("The proxy will be stopped and all users will lose access.") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + var yesButtonFunction = function () { + PageController.goToPage(PageEnum.PageDeinstalling) + InstallController.removeContainer(ServersUiController.getServerId(ServersUiController.processedServerIndex), ServersUiController.processedContainerIndex) + } + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, function () { + }) + } + MouseArea { + anchors.fill: removeButton + cursorShape: Qt.PointingHandCursor + enabled: false + } + } + } + } + + ListViewType { + id: settingsListView + model: MtProxyConfigModel + reuseItems: false + + delegate: ColumnLayout { + id: settingsRoot + width: settingsListView.width + spacing: 0 + + function mtProxyDomainToHex(domain) { + var hex = "" + for (var i = 0; i < domain.length; i++) { + var code = domain.charCodeAt(i).toString(16) + hex += (code.length < 2 ? "0" : "") + code + } + return hex + } + + function mtProxySecretForBaseHex(baseHex, mode) { + if (mode === "faketls") { + var domain = root.savedTlsDomain !== "" ? root.savedTlsDomain : MtProxyConfigModel.defaultTlsDomain() + return "ee" + baseHex + mtProxyDomainToHex(domain) + } else if (mode === "padded") { + return "dd" + baseHex + } + return baseHex + } + + function mtProxyActiveSecretForBaseHex(baseHex) { + if (root.syncedSecretTabIndex === 0) { + return mtProxySecretForBaseHex(baseHex, "standard") + } + if (root.syncedSecretTabIndex === 1) { + return mtProxySecretForBaseHex(baseHex, "padded") + } + return mtProxySecretForBaseHex(baseHex, "faketls") + } + + function mtProxyEffectiveHostForLinks() { + return root.savedPublicHost !== "" ? root.savedPublicHost : ServersModel.getProcessedServerData("hostName") + } + + function mtProxyTmeLinkForAdditional(baseHex) { + return "https://t.me/proxy?server=" + mtProxyEffectiveHostForLinks() + "&port=" + port + "&secret=" + mtProxyActiveSecretForBaseHex(baseHex) + } + + function mtProxyTgLinkForAdditional(baseHex) { + return "tg://proxy?server=" + mtProxyEffectiveHostForLinks() + "&port=" + port + "&secret=" + mtProxyActiveSecretForBaseHex(baseHex) + } + + SwitcherType { + id: enableMtProxySwitch + Layout.fillWidth: true + Layout.topMargin: 24 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + text: qsTr("Enable MTProxy") + checked: isEnabled + enabled: !isCheckingStatus && containerStatus !== 0 && containerStatus !== 3 && !isUpdating + && !root.mtProxyNetworkBlocked + onToggled: function () { + if (checked !== isEnabled) { + previousEnabled = isEnabled + previousContainerStatus = containerStatus + root.previousSecret = secret + isEnabled = checked + isUpdating = true + if (checked) { + root.pendingUpdateAfterEnable = true + InstallController.setContainerEnabled(ServersUiController.getServerId(ServersUiController.processedServerIndex), ServersUiController.processedContainerIndex, true) + } else { + InstallController.setContainerEnabled(ServersUiController.getServerId(ServersUiController.processedServerIndex), ServersUiController.processedContainerIndex, false) + } + } + } + } + + ColumnLayout { + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 * 2 + spacing: 4 + + CaptionTextType { + text: qsTr("Base secret") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + } + + RowLayout { + Layout.fillWidth: true + spacing: 8 + + CaptionTextType { + Layout.fillWidth: true + text: secret !== "" ? secret : qsTr("Not generated") + color: secret !== "" ? AmneziaStyle.color.paleGray : AmneziaStyle.color.mutedGray + elide: Text.ElideMiddle + font.pixelSize: 14 + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/refresh-cw.svg" + imageColor: AmneziaStyle.color.paleGray + visible: ServersModel.isProcessedServerHasWriteAccess() + onClicked: { + var secretSnapshot = secret + showQuestionDrawer( + qsTr("Generate new secret?"), + qsTr("All existing connection links will stop working. Users will need new links."), + qsTr("Generate"), + qsTr("Cancel"), + function () { + root.previousSecret = secretSnapshot + if (containerStatus === 1) { + isUpdating = true + MtProxyConfigModel.generateSecret() + root.mtProxyScheduleUpdate(false) + } else { + MtProxyConfigModel.generateSecret() + PageController.showNotificationMessage(qsTr("New secret saved. It will be applied when MTProxy is started.")) + } + }, + function () { + } + ) + } + } + } + } + + TextFieldWithHeaderType { + id: publicHostTextField + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 4 + headerText: qsTr("Public host / IP") + textField.placeholderText: ServersModel.getProcessedServerData("hostName") + textField.text: publicHost + textField.maximumLength: 253 + textField.validator: PublicHostInputValidator { + } + textField.onTextChanged: { + var t = publicHostTextField.textField.text + if (MtProxyConfigModel.isPublicHostTypingIncomplete(t)) { + publicHostTextField.errorText = "" + } else if (!MtProxyConfigModel.isValidPublicHost(t)) { + publicHostTextField.errorText = qsTr("Enter a valid IP address or domain name") + } else { + publicHostTextField.errorText = "" + } + } + textField.onEditingFinished: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + if (!MtProxyConfigModel.isValidPublicHost(textField.text)) { + publicHostTextField.errorText = qsTr("Enter a valid IP address or domain name") + return + } + publicHostTextField.errorText = "" + if (textField.text !== publicHost) { + publicHost = textField.text + MtProxyConfigModel.setPublicHost(publicHost) + } + } + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 4 + visible: publicHostTextField.textField.text === "" + text: qsTr("Leave empty to use server IP automatically") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + wrapMode: Text.WordWrap + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 12 + visible: publicHostTextField.textField.text !== "" && + publicHostTextField.textField.text !== ServersModel.getProcessedServerData("hostName") + text: qsTr("⚠ This overrides the server IP in connection links. Make sure this host/domain points to your server.") + color: AmneziaStyle.color.goldenApricot + font.pixelSize: 12 + wrapMode: Text.WordWrap + } + + TextFieldWithHeaderType { + id: portTextField + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + headerText: qsTr("Server port") + textField.placeholderText: MtProxyConfigModel.defaultPort() + textField.maximumLength: 5 + textField.validator: IntValidator { + bottom: 1 + top: 65535 + } + Component.onCompleted: { + var savedPort = port + textField.text = (savedPort === MtProxyConfigModel.defaultPort()) ? "" : savedPort + } + textField.onEditingFinished: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + } + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 12 + visible: transportMode === "faketls" && portTextField.textField.text !== "443" && portTextField.textField.text !== "" + text: qsTr("FakeTLS may not work on ports other than 443") + color: AmneziaStyle.color.goldenApricot + font.pixelSize: 12 + wrapMode: Text.WordWrap + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + text: qsTr("The promoted channel is set in @MTProxyBot. Paste the proxy tag here: exactly 32 hexadecimal characters (0-9, A-F), as in the bot message — or leave empty.") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + wrapMode: Text.WordWrap + } + + TextFieldWithHeaderType { + id: tagTextField + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + headerText: qsTr("MTProxy bot tag (optional)") + textField.placeholderText: qsTr("32 hex chars from @MTProxyBot (e.g. 3b7b2fa9…)") + textField.text: tag + textField.maximumLength: MtProxyConfigModel.mtProxyBotTagHexLength() + textField.onTextChanged: { + var cur = tagTextField.textField.text + var clean = MtProxyConfigModel.sanitizeMtProxyTagFieldText(cur) + if (clean !== cur) { + textField.text = clean + textField.cursorPosition = clean.length + return + } + var tt = tagTextField.textField.text + if (tt === "") { + tagTextField.errorText = "" + return + } + if (MtProxyConfigModel.isMtProxyTagTypingIncomplete(tt)) { + tagTextField.errorText = "" + return + } + if (!MtProxyConfigModel.isValidMtProxyTag(tt)) { + tagTextField.errorText = qsTr("Proxy tag must be exactly 32 hexadecimal characters (0-9, A-F).") + return + } + tagTextField.errorText = "" + } + textField.onEditingFinished: { + var raw = textField.text.replace(/^\s+|\s+$/g, '') + var normalized = MtProxyConfigModel.sanitizeMtProxyTagFieldText(raw) + textField.text = normalized + if (!MtProxyConfigModel.isValidMtProxyTag(normalized)) { + tagTextField.errorText = qsTr("Proxy tag must be exactly 32 hexadecimal characters (0-9, A-F). Leave empty if unused.") + return + } + tagTextField.errorText = "" + if (normalized !== tag) { + tag = normalized + MtProxyConfigModel.setTag(tag) + } + } + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + spacing: 4 + + CaptionTextType { + text: qsTr("Get a tag from") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + } + CaptionTextType { + text: "@MTProxyBot" + color: AmneziaStyle.color.goldenApricot + font.pixelSize: 12 + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: Qt.openUrlExternally("https://t.me/MTProxyBot") + } + } + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 16 * 2 + text: qsTr("Transport mode") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + } + + DropDownType { + id: transportModeDropDown + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + + drawerParent: root + drawerHeight: 0.4 + headerText: qsTr("Transport mode") + text: transportMode === "faketls" ? qsTr("FakeTLS") : qsTr("Standard MTProto") + + listView: Component { + ListViewType { + model: [qsTr("Standard MTProto"), qsTr("FakeTLS")] + delegate: LabelWithButtonType { + Layout.fillWidth: true + text: modelData + rightImageSource: { + var isCurrent = (index === 0 && transportMode === "standard") || + (index === 1 && transportMode === "faketls") + return isCurrent ? "qrc:/images/controls/check.svg" : "" + } + rightImageColor: AmneziaStyle.color.goldenApricot + clickedFunction: function () { + transportMode = (index === 0) ? "standard" : "faketls" + MtProxyConfigModel.setTransportMode(transportMode) + transportModeDropDown.closeTriggered() + } + } + } + } + } + + TextFieldWithHeaderType { + id: tlsDomainTextField + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + visible: transportMode === "faketls" + headerText: qsTr("FakeTLS domain") + textField.placeholderText: root.previousTlsDomain + Component.onCompleted: { + var savedDomain = tlsDomain + textField.text = (savedDomain === MtProxyConfigModel.defaultTlsDomain() || savedDomain === "") ? "" : savedDomain + } + textField.onEditingFinished: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + var domainValue = textField.text === "" ? MtProxyConfigModel.defaultTlsDomain() : textField.text + if (!MtProxyConfigModel.isValidFakeTlsDomain(domainValue)) { + tlsDomainTextField.errorText = qsTr("Enter a valid domain name") + return + } + tlsDomainTextField.errorText = "" + if (domainValue !== tlsDomain) { + tlsDomain = domainValue + MtProxyConfigModel.setTlsDomain(tlsDomain) + } + } + } + + ColumnLayout { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + spacing: 4 + visible: transportMode === "faketls" + + CaptionTextType { + Layout.fillWidth: true + text: qsTr("The domain is encoded into the FakeTLS client secret (ee + base_secret + hex(domain)). It must support HTTPS / TLS 1.3.") + color: AmneziaStyle.color.mutedGray + wrapMode: Text.WordWrap + font.pixelSize: 12 + } + CaptionTextType { + Layout.fillWidth: true + text: qsTr("\u26a0 Changing the domain will invalidate all previously issued FakeTLS connection links.") + color: AmneziaStyle.color.goldenApricot + wrapMode: Text.WordWrap + font.pixelSize: 12 + } + } + + LabelWithButtonType { + id: advancedHeader + Layout.fillWidth: true + Layout.leftMargin: 0 + Layout.rightMargin: 16 + property bool expanded: false + text: qsTr("Advanced") + rightImageSource: expanded + ? "qrc:/images/controls/chevron-up.svg" + : "qrc:/images/controls/chevron-down.svg" + rightImageColor: AmneziaStyle.color.mutedGray + clickedFunction: function () { + expanded = !expanded + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + visible: advancedHeader.expanded + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + Layout.bottomMargin: 4 + text: qsTr("Additional secrets") + color: AmneziaStyle.color.mutedGray + } + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + text: qsTr("Add extra secrets to allow gradual migration without disconnecting existing users.") + color: AmneziaStyle.color.charcoalGray + wrapMode: Text.WordWrap + font.pixelSize: 12 + } + + Repeater { + model: additionalSecrets + delegate: ColumnLayout { + id: addSecretDelegate + property bool linksExpanded: false + readonly property bool linksPanelAllowed: root.mtProxyIsPersistedAdditionalHex(modelData) + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + spacing: 0 + + onLinksPanelAllowedChanged: { + if (!linksPanelAllowed) { + linksExpanded = false + } + } + + Rectangle { + Layout.fillWidth: true + implicitHeight: collapsedBar.implicitHeight + 16 + color: AmneziaStyle.color.onyxBlack + radius: 8 + border.color: AmneziaStyle.color.slateGray + border.width: 1 + + RowLayout { + id: collapsedBar + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 + anchors.rightMargin: 8 + spacing: 8 + + Item { + Layout.fillWidth: true + implicitHeight: Math.max(hexCaption.implicitHeight, 24) + + RowLayout { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: 8 + + CaptionTextType { + id: hexCaption + Layout.fillWidth: true + text: modelData + color: AmneziaStyle.color.paleGray + elide: Text.ElideMiddle + font.pixelSize: 13 + } + + Image { + width: 24 + height: 24 + visible: addSecretDelegate.linksPanelAllowed + source: "qrc:/images/controls/chevron-down.svg" + sourceSize.width: 24 + sourceSize.height: 24 + rotation: addSecretDelegate.linksExpanded ? 180 : 0 + Behavior on rotation { + NumberAnimation { + duration: 150 + } + } + } + } + + MouseArea { + anchors.fill: parent + visible: addSecretDelegate.linksPanelAllowed + enabled: addSecretDelegate.linksPanelAllowed + cursorShape: Qt.PointingHandCursor + onClicked: addSecretDelegate.linksExpanded = !addSecretDelegate.linksExpanded + } + } + + ImageButtonType { + implicitWidth: 32 + implicitHeight: 32 + hoverEnabled: true + visible: ServersModel.isProcessedServerHasWriteAccess() + image: "qrc:/images/controls/trash.svg" + imageColor: AmneziaStyle.color.vibrantRed + onClicked: { + MtProxyConfigModel.removeAdditionalSecret(index) + } + } + } + } + + ColumnLayout { + Layout.fillWidth: true + Layout.topMargin: 8 + spacing: 8 + visible: addSecretDelegate.linksPanelAllowed && addSecretDelegate.linksExpanded + + CaptionTextType { + Layout.fillWidth: true + text: qsTr("Use Telegram connection link") + color: AmneziaStyle.color.mutedGray + } + + Rectangle { + Layout.fillWidth: true + implicitHeight: expTmeRow.implicitHeight + 16 + color: AmneziaStyle.color.onyxBlack + radius: 8 + border.color: AmneziaStyle.color.slateGray + border.width: 1 + + RowLayout { + id: expTmeRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 + anchors.rightMargin: 8 + spacing: 4 + + CaptionTextType { + Layout.fillWidth: true + text: settingsRoot.mtProxyTmeLinkForAdditional(modelData) + color: AmneziaStyle.color.goldenApricot + elide: Text.ElideRight + maximumLineCount: 1 + font.pixelSize: 13 + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/qr-code.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: { + ExportController.generateQrFromString(settingsRoot.mtProxyTmeLinkForAdditional(modelData)) + PageController.goToShareConnectionPage( + qsTr("Telegram connection link"), + qsTr("MTProxy connection link"), + "", "", "") + } + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: { + GC.copyToClipBoard(settingsRoot.mtProxyTmeLinkForAdditional(modelData)) + PageController.showNotificationMessage(qsTr("Copied")) + } + } + } + } + + Rectangle { + Layout.fillWidth: true + implicitHeight: expTgRow.implicitHeight + 16 + color: AmneziaStyle.color.onyxBlack + radius: 8 + border.color: AmneziaStyle.color.slateGray + border.width: 1 + + RowLayout { + id: expTgRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 + anchors.rightMargin: 8 + spacing: 4 + + CaptionTextType { + Layout.fillWidth: true + text: settingsRoot.mtProxyTgLinkForAdditional(modelData) + color: AmneziaStyle.color.goldenApricot + elide: Text.ElideRight + maximumLineCount: 1 + font.pixelSize: 13 + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/qr-code.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: { + ExportController.generateQrFromString(settingsRoot.mtProxyTgLinkForAdditional(modelData)) + PageController.goToShareConnectionPage( + qsTr("Telegram connection link"), + qsTr("MTProxy connection link"), + "", "", "") + } + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: { + GC.copyToClipBoard(settingsRoot.mtProxyTgLinkForAdditional(modelData)) + PageController.showNotificationMessage(qsTr("Copied")) + } + } + } + } + } + } + } + + BasicButtonType { + Layout.fillWidth: true + Layout.topMargin: 8 + + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + text: qsTr("Add additional secret") + clickedFunc: function () { + MtProxyConfigModel.addAdditionalSecret() + } + } + + DividerType { + Layout.fillWidth: true + Layout.bottomMargin: 8 + } + + LabelTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.bottomMargin: 4 + text: qsTr("Worker mode") + } + + ButtonGroup { + id: workerModeGroup + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 4 + spacing: 0 + visible: transportMode !== "faketls" + + HorizontalRadioButton { + Layout.fillWidth: true + text: qsTr("Auto") + ButtonGroup.group: workerModeGroup + checked: workersMode === "auto" + onClicked: { workersMode = "auto"; MtProxyConfigModel.setWorkersMode("auto") } + } + HorizontalRadioButton { + Layout.fillWidth: true + text: qsTr("Manual") + ButtonGroup.group: workerModeGroup + checked: workersMode === "manual" + onClicked: { workersMode = "manual"; MtProxyConfigModel.setWorkersMode("manual") } + } + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + visible: transportMode === "faketls" + text: qsTr("Workers are set to 0 automatically for FakeTLS mode.") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + wrapMode: Text.WordWrap + } + + TextFieldWithHeaderType { + id: workersTextField + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + visible: workersMode === "manual" && transportMode !== "faketls" + headerText: qsTr("Workers count") + textField.placeholderText: "2" + textField.text: workers + textField.maximumLength: 3 + textField.validator: IntValidator { + bottom: 1 + top: MtProxyConfigModel.maxWorkers() + } + textField.onEditingFinished: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + if (textField.text !== workers) { + workers = textField.text + MtProxyConfigModel.setWorkers(workers) + } + } + } + + DividerType { + Layout.fillWidth: true + Layout.bottomMargin: 8 + } + + SwitcherType { + Layout.fillWidth: true + Layout.rightMargin: 16 + Layout.leftMargin: 16 + Layout.bottomMargin: 4 + text: qsTr("Server is behind NAT / Docker bridge") + descriptionText: qsTr("Enable if your server is not directly accessible from the internet, e.g. Docker or private network") + checked: natEnabled + onToggled: function () { + if (checked !== natEnabled) { + natEnabled = checked + MtProxyConfigModel.setNatEnabled(natEnabled) + } + } + } + + TextFieldWithHeaderType { + id: natInternalIpTextField + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + visible: natEnabled + headerText: qsTr("Internal IP") + textField.placeholderText: "172.17.0.2" + textField.text: natInternalIp + textField.maximumLength: 15 + textField.validator: RegularExpressionValidator { + regularExpression: root.natIpv4InputFormat + } + textField.onTextChanged: { + if (root.natIpv4FieldShowInvalidError(textField.text)) { + natInternalIpTextField.errorText = qsTr("Enter a valid IPv4 address") + } else { + natInternalIpTextField.errorText = "" + } + } + textField.onEditingFinished: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + if (!MtProxyConfigModel.isValidOptionalIpv4(textField.text)) { + natInternalIpTextField.errorText = qsTr("Enter a valid IPv4 address") + return + } + natInternalIpTextField.errorText = "" + if (textField.text !== natInternalIp) { + natInternalIp = textField.text + MtProxyConfigModel.setNatInternalIp(natInternalIp) + } + } + } + + TextFieldWithHeaderType { + id: natExternalIpTextField + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + visible: natEnabled + headerText: qsTr("External IP") + textField.placeholderText: "1.2.3.4" + textField.text: natExternalIp + textField.maximumLength: 15 + textField.validator: RegularExpressionValidator { + regularExpression: root.natIpv4InputFormat + } + textField.onTextChanged: { + if (root.natIpv4FieldShowInvalidError(textField.text)) { + natExternalIpTextField.errorText = qsTr("Enter a valid IPv4 address") + } else { + natExternalIpTextField.errorText = "" + } + } + textField.onEditingFinished: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + if (!MtProxyConfigModel.isValidOptionalIpv4(textField.text)) { + natExternalIpTextField.errorText = qsTr("Enter a valid IPv4 address") + return + } + natExternalIpTextField.errorText = "" + if (textField.text !== natExternalIp) { + natExternalIp = textField.text + MtProxyConfigModel.setNatExternalIp(natExternalIp) + } + } + } + } + + DividerType { + Layout.fillWidth: true + Layout.topMargin: 8 + } + + ColumnLayout { + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + spacing: 8 + visible: containerStatus === 1 + + RowLayout { + Layout.fillWidth: true + + Header2Type { + Layout.fillWidth: true + headerText: qsTr("Diagnostics") + } + + ImageButtonType { + implicitWidth: 32 + implicitHeight: 32 + image: "qrc:/images/controls/refresh-cw.svg" + imageColor: diagLoading ? AmneziaStyle.color.mutedGray : AmneziaStyle.color.paleGray + hoverEnabled: !diagLoading + enabled: !diagLoading + onClicked: { + diagLoading = true + InstallController.refreshContainerDiagnostics(ServersUiController.getServerId(ServersUiController.processedServerIndex), ServersUiController.processedContainerIndex, parseInt(port)) + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 8 + Rectangle { + width: 8 + height: 8 + radius: 4 + color: diagClientsConnected >= 0 ? (diagPortReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray + } + CaptionTextType { + Layout.fillWidth: true + text: qsTr("Public port reachable") + color: AmneziaStyle.color.paleGray + } + CaptionTextType { + text: diagClientsConnected < 0 ? qsTr("—") : (diagPortReachable ? qsTr("Yes") : qsTr("No")) + color: diagClientsConnected >= 0 ? (diagPortReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 8 + Rectangle { + width: 8 + height: 8 + radius: 4 + color: diagClientsConnected >= 0 ? (diagTelegramReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray + } + CaptionTextType { + Layout.fillWidth: true + text: qsTr("Telegram upstream reachable") + color: AmneziaStyle.color.paleGray + } + CaptionTextType { + text: diagClientsConnected < 0 ? qsTr("—") : (diagTelegramReachable ? qsTr("Yes") : qsTr("No")) + color: diagClientsConnected >= 0 ? (diagTelegramReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 8 + Rectangle { + width: 8 + height: 8 + radius: 4 + color: diagClientsConnected >= 0 ? AmneziaStyle.color.goldenApricot : AmneziaStyle.color.mutedGray + } + CaptionTextType { + Layout.fillWidth: true + text: qsTr("Clients connected") + color: AmneziaStyle.color.paleGray + } + CaptionTextType { + text: diagClientsConnected < 0 ? qsTr("—") : diagClientsConnected.toString() + color: AmneziaStyle.color.paleGray + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 8 + Rectangle { + width: 8 + height: 8 + radius: 4 + color: AmneziaStyle.color.mutedGray + } + CaptionTextType { + Layout.fillWidth: true + text: qsTr("Last config refresh") + color: AmneziaStyle.color.paleGray + } + CaptionTextType { + text: diagLastConfigRefresh !== "" ? diagLastConfigRefresh : qsTr("—") + color: AmneziaStyle.color.mutedGray + } + } + + LabelWithButtonType { + Layout.fillWidth: true + Layout.leftMargin: -16 + visible: diagStatsEndpoint !== "" + text: qsTr("Stats endpoint") + descriptionText: diagStatsEndpoint + descriptionOnTop: true + rightImageSource: "qrc:/images/controls/copy.svg" + rightImageColor: AmneziaStyle.color.paleGray + clickedFunction: function () { + GC.copyToClipBoard(diagStatsEndpoint) + PageController.showNotificationMessage(qsTr("Copied")) + } + } + + CaptionTextType { + Layout.fillWidth: true + text: diagLoading ? qsTr("Refreshing…") : qsTr("Tap ↻ to refresh diagnostics") + color: AmneziaStyle.color.mutedGray + visible: diagClientsConnected < 0 + } + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 16 * 2 + Layout.bottomMargin: 24 + text: qsTr("If you change the settings, the proxy connection link will change. The old link will stop working.") + color: AmneziaStyle.color.mutedGray + wrapMode: Text.WordWrap + font.pixelSize: 12 + } + + BasicButtonType { + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.bottomMargin: 32 + Layout.rightMargin: 16 + Layout.leftMargin: 16 + visible: ServersModel.isProcessedServerHasWriteAccess() + enabled: !root.mtProxyNetworkBlocked + text: qsTr("Save") + clickedFunc: function () { + if (root.mtProxyNetworkBlocked) { + PageController.showErrorMessage(qsTr("No internet connection. Connect to the internet to change MTProxy settings.")) + return + } + publicHostTextField.errorText = "" + tagTextField.errorText = "" + tlsDomainTextField.errorText = "" + natInternalIpTextField.errorText = "" + natExternalIpTextField.errorText = "" + portTextField.errorText = "" + + var portValue = portTextField.textField.text === "" + ? MtProxyConfigModel.defaultPort() + : portTextField.textField.text + + var errorLines = [] + var bullet = "- " + if (!portTextField.textField.acceptableInput && portTextField.textField.text !== "") { + var portErr = qsTr("The port must be in the range of 1 to 65535") + portTextField.errorText = portErr + errorLines.push(bullet + portErr) + } + if (!MtProxyConfigModel.isValidPublicHost(publicHostTextField.textField.text)) { + var hostErr = qsTr("Enter a valid IP address or domain name") + publicHostTextField.errorText = hostErr + errorLines.push(bullet + hostErr) + } + var tagNormalized = MtProxyConfigModel.sanitizeMtProxyTagFieldText(tagTextField.textField.text) + tagTextField.textField.text = tagNormalized + if (!MtProxyConfigModel.isValidMtProxyTag(tagNormalized)) { + var tagErr = qsTr("Proxy tag must be exactly 32 hexadecimal characters (0-9, A-F), or leave empty.") + tagTextField.errorText = tagErr + errorLines.push(bullet + tagErr) + } + var domainValueForSave = tlsDomainTextField.textField.text === "" + ? MtProxyConfigModel.defaultTlsDomain() + : tlsDomainTextField.textField.text + if (!MtProxyConfigModel.isValidFakeTlsDomain(domainValueForSave)) { + var tlsErr = qsTr("Enter a valid domain name") + tlsDomainTextField.errorText = tlsErr + errorLines.push(bullet + tlsErr) + } + var natIpErr = qsTr("Enter a valid IPv4 address") + if (!MtProxyConfigModel.isValidOptionalIpv4(natInternalIpTextField.textField.text)) { + natInternalIpTextField.errorText = natIpErr + errorLines.push(bullet + qsTr("NAT internal IP: enter a valid IPv4 address")) + } + if (!MtProxyConfigModel.isValidOptionalIpv4(natExternalIpTextField.textField.text)) { + natExternalIpTextField.errorText = natIpErr + errorLines.push(bullet + qsTr("NAT external IP: enter a valid IPv4 address")) + } + if (errorLines.length > 0) { + PageController.showErrorMessage(errorLines.join("\n")) + return + } + MtProxyConfigModel.setPort(portValue) + MtProxyConfigModel.setTag(tagNormalized) + MtProxyConfigModel.setPublicHost(publicHostTextField.textField.text) + MtProxyConfigModel.setTransportMode(transportMode) + var domainValue = tlsDomainTextField.textField.text === "" + ? MtProxyConfigModel.defaultTlsDomain() + : tlsDomainTextField.textField.text + MtProxyConfigModel.setTlsDomain(domainValue) + + if (transportMode === "faketls") { + workers = "0" + MtProxyConfigModel.setWorkers("0") + } else { + MtProxyConfigModel.setWorkersMode(workersMode) + MtProxyConfigModel.setWorkers(workers) + } + MtProxyConfigModel.setNatEnabled(natEnabled) + MtProxyConfigModel.setNatInternalIp(natInternalIpTextField.textField.text) + MtProxyConfigModel.setNatExternalIp(natExternalIpTextField.textField.text) + + previousPort = port + previousTag = tag + previousPublicHost = publicHost + previousTransportMode = transportMode + previousTlsDomain = tlsDomain + previousWorkersMode = workersMode + previousWorkers = workers + previousNatEnabled = natEnabled + previousNatInternalIp = natInternalIp + previousNatExternalIp = natExternalIp + root.previousSecret = secret + isUpdating = true + root.mtProxyScheduleUpdate(false) + } + } + } + } + } + + Rectangle { + anchors.fill: parent + visible: isCheckingStatus || isUpdating || root.mtProxyNetworkBlocked + color: AmneziaStyle.color.midnightBlack + opacity: 0.6 + z: 1 + MouseArea { + anchors.fill: parent + } + BusyIndicator { + anchors.centerIn: parent + visible: isCheckingStatus || isUpdating + running: isCheckingStatus || isUpdating + width: 48 + height: 48 + } + CaptionTextType { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 24 + anchors.rightMargin: 24 + visible: root.mtProxyNetworkBlocked && !isCheckingStatus && !isUpdating + horizontalAlignment: Text.AlignHCenter + text: qsTr("No internet connection. Connect to the internet to change MTProxy settings.") + color: AmneziaStyle.color.paleGray + wrapMode: Text.WordWrap + font.pixelSize: 14 + } + } +} diff --git a/client/ui/qml/Pages2/PageStart.qml b/client/ui/qml/Pages2/PageStart.qml index 950a5a7d8..5096be733 100644 --- a/client/ui/qml/Pages2/PageStart.qml +++ b/client/ui/qml/Pages2/PageStart.qml @@ -132,9 +132,11 @@ PageType { onInstallationErrorOccurred(message) } - function onUpdateContainerFinished(message) { + function onUpdateContainerFinished(message, closePage) { PageController.showNotificationMessage(message) - PageController.closePage() + if (closePage) { + PageController.closePage() + } } function onCachedProfileCleared(message) { diff --git a/client/ui/qml/qml.qrc b/client/ui/qml/qml.qrc index f2a462630..dd4a5041b 100644 --- a/client/ui/qml/qml.qrc +++ b/client/ui/qml/qml.qrc @@ -78,6 +78,7 @@ Pages2/PageProtocolWireGuardSettings.qml Pages2/PageProtocolXraySettings.qml Pages2/PageServiceDnsSettings.qml + Pages2/PageServiceMtProxySettings.qml Pages2/PageServiceSftpSettings.qml Pages2/PageServiceSocksProxySettings.qml Pages2/PageServiceTorWebsiteSettings.qml From a49892c7e7cb316276878a1e41d704093fd2acaf Mon Sep 17 00:00:00 2001 From: yp Date: Mon, 18 May 2026 15:01:09 +0300 Subject: [PATCH 13/14] feat: add telemt container (#2435) * Feat: Add MtProxy (Telegram) * add path files * Feat: Add Telemt (MtProxy) * fixed secret & enum * remove old path * refactor: move logic from ui to core --------- Co-authored-by: vkamn --- client/cmake/sources.cmake | 2 + client/core/controllers/coreController.cpp | 5 +- client/core/controllers/coreController.h | 2 + .../selfhosted/installController.cpp | 102 ++ client/core/diagnostics/telemtDiagnostics.h | 20 + client/core/installers/installerBase.cpp | 7 + client/core/installers/mtProxyInstaller.cpp | 6 +- client/core/installers/telemtInstaller.cpp | 79 + client/core/installers/telemtInstaller.h | 20 + client/core/models/containerConfig.cpp | 10 + client/core/models/containerConfig.h | 3 + client/core/models/protocolConfig.cpp | 9 + client/core/models/protocolConfig.h | 2 + .../models/protocols/telemtProtocolConfig.cpp | 162 ++ .../models/protocols/telemtProtocolConfig.h | 38 + client/core/protocols/protocolUtils.cpp | 8 +- client/core/utils/constants/configKeys.h | 1 + .../core/utils/constants/protocolConstants.h | 34 + client/core/utils/containerEnum.h | 1 + .../core/utils/containers/containerUtils.cpp | 12 + client/core/utils/protocolEnum.h | 1 + .../core/utils/selfhosted/scriptsRegistry.cpp | 36 + .../core/utils/selfhosted/scriptsRegistry.h | 1 + client/core/utils/selfhosted/sshSession.cpp | 2 +- client/server_scripts/serverScripts.qrc | 4 + client/server_scripts/telemt/Dockerfile | 42 + .../telemt/configure_container.sh | 73 + client/server_scripts/telemt/run_container.sh | 9 + client/server_scripts/telemt/start.sh | 12 + client/ui/controllers/qml/pageController.h | 1 + .../selfhosted/installUiController.cpp | 26 +- .../selfhosted/installUiController.h | 3 + client/ui/controllers/serversUiController.cpp | 2 + client/ui/models/containersModel.cpp | 2 + client/ui/models/containersModel.h | 1 + client/ui/models/protocolsModel.cpp | 3 + client/ui/models/protocolsModel.h | 1 + .../ui/models/services/telemtConfigModel.cpp | 406 +++++ client/ui/models/services/telemtConfigModel.h | 130 ++ .../Components/SettingsContainersListView.qml | 3 + .../qml/Pages2/PageServiceTelemtSettings.qml | 1447 +++++++++++++++++ client/ui/qml/qml.qrc | 1 + 42 files changed, 2721 insertions(+), 8 deletions(-) create mode 100644 client/core/diagnostics/telemtDiagnostics.h create mode 100644 client/core/installers/telemtInstaller.cpp create mode 100644 client/core/installers/telemtInstaller.h create mode 100644 client/core/models/protocols/telemtProtocolConfig.cpp create mode 100644 client/core/models/protocols/telemtProtocolConfig.h create mode 100644 client/server_scripts/telemt/Dockerfile create mode 100644 client/server_scripts/telemt/configure_container.sh create mode 100644 client/server_scripts/telemt/run_container.sh create mode 100644 client/server_scripts/telemt/start.sh create mode 100644 client/ui/models/services/telemtConfigModel.cpp create mode 100644 client/ui/models/services/telemtConfigModel.h create mode 100644 client/ui/qml/Pages2/PageServiceTelemtSettings.qml diff --git a/client/cmake/sources.cmake b/client/cmake/sources.cmake index 8afd4a5fd..ddc44d47b 100644 --- a/client/cmake/sources.cmake +++ b/client/cmake/sources.cmake @@ -36,6 +36,7 @@ set(HEADERS ${HEADERS} ${CLIENT_ROOT_DIR}/core/installers/sftpInstaller.h ${CLIENT_ROOT_DIR}/core/installers/socks5Installer.h ${CLIENT_ROOT_DIR}/core/installers/mtProxyInstaller.h + ${CLIENT_ROOT_DIR}/core/installers/telemtInstaller.h ${CLIENT_ROOT_DIR}/core/controllers/appSplitTunnelingController.h ${CLIENT_ROOT_DIR}/core/controllers/ipSplitTunnelingController.h ${CLIENT_ROOT_DIR}/core/controllers/allowedDnsController.h @@ -112,6 +113,7 @@ set(SOURCES ${SOURCES} ${CLIENT_ROOT_DIR}/core/installers/sftpInstaller.cpp ${CLIENT_ROOT_DIR}/core/installers/socks5Installer.cpp ${CLIENT_ROOT_DIR}/core/installers/mtProxyInstaller.cpp + ${CLIENT_ROOT_DIR}/core/installers/telemtInstaller.cpp ${CLIENT_ROOT_DIR}/core/controllers/appSplitTunnelingController.cpp ${CLIENT_ROOT_DIR}/core/controllers/ipSplitTunnelingController.cpp ${CLIENT_ROOT_DIR}/core/controllers/allowedDnsController.cpp diff --git a/client/core/controllers/coreController.cpp b/client/core/controllers/coreController.cpp index 3240f6b5c..d9bf0ea9b 100644 --- a/client/core/controllers/coreController.cpp +++ b/client/core/controllers/coreController.cpp @@ -103,6 +103,9 @@ void CoreController::initModels() m_mtProxyConfigModel = new MtProxyConfigModel(this); setQmlContextProperty("MtProxyConfigModel", m_mtProxyConfigModel); + m_telemtConfigModel = new TelemtConfigModel(this); + setQmlContextProperty("TelemtConfigModel", m_telemtConfigModel); + m_clientManagementModel = new ClientManagementModel(this); setQmlContextProperty("ClientManagementModel", m_clientManagementModel); @@ -172,7 +175,7 @@ void CoreController::initControllers() #ifdef Q_OS_WINDOWS m_ikev2ConfigModel, #endif - m_sftpConfigModel, m_socks5ConfigModel, m_mtProxyConfigModel, this); + m_sftpConfigModel, m_socks5ConfigModel, m_mtProxyConfigModel, m_telemtConfigModel, this); setQmlContextProperty("InstallController", m_installUiController); m_importController = new ImportUiController(m_importCoreController, this); diff --git a/client/core/controllers/coreController.h b/client/core/controllers/coreController.h index 64645ab02..acf347f5d 100644 --- a/client/core/controllers/coreController.h +++ b/client/core/controllers/coreController.h @@ -71,6 +71,7 @@ #include "ui/models/services/sftpConfigModel.h" #include "ui/models/services/socks5ProxyConfigModel.h" #include "ui/models/services/mtProxyConfigModel.h" +#include "ui/models/services/telemtConfigModel.h" #include "ui/models/ipSplitTunnelingModel.h" #include "ui/models/newsModel.h" @@ -213,6 +214,7 @@ private: SftpConfigModel* m_sftpConfigModel; Socks5ProxyConfigModel* m_socks5ConfigModel; MtProxyConfigModel* m_mtProxyConfigModel; + TelemtConfigModel* m_telemtConfigModel; CoreSignalHandlers* m_signalHandlers; }; diff --git a/client/core/controllers/selfhosted/installController.cpp b/client/core/controllers/selfhosted/installController.cpp index a9035b59f..fa02fa016 100644 --- a/client/core/controllers/selfhosted/installController.cpp +++ b/client/core/controllers/selfhosted/installController.cpp @@ -20,6 +20,7 @@ #include "core/installers/sftpInstaller.h" #include "core/installers/socks5Installer.h" #include "core/installers/mtProxyInstaller.h" +#include "core/installers/telemtInstaller.h" #include "core/installers/torInstaller.h" #include "core/installers/wireguardInstaller.h" #include "core/installers/xrayInstaller.h" @@ -157,6 +158,10 @@ ErrorCode InstallController::updateContainer(const QString &serverId, DockerCont ServerCredentials credentials = adminConfig->credentials(); SshSession sshSession(this); MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig); + } else if (container == DockerContainer::Telemt) { + ServerCredentials credentials = adminConfig->credentials(); + SshSession sshSession(this); + TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig); } adminConfig->updateContainerConfig(container, newConfig); m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin); @@ -189,6 +194,8 @@ ErrorCode InstallController::updateContainer(const QString &serverId, DockerCont if (errorCode == ErrorCode::NoError) { if (container == DockerContainer::MtProxy) { MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig); + } else if (container == DockerContainer::Telemt) { + TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig); } clearCachedProfile(serverId, container); adminConfig->updateContainerConfig(container, newConfig); @@ -446,6 +453,8 @@ ErrorCode InstallController::configureContainerWorker(const ServerCredentials &c if (container == DockerContainer::MtProxy) { MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, config); + } else if (container == DockerContainer::Telemt) { + TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, config); } return ErrorCode::NoError; @@ -627,6 +636,53 @@ bool InstallController::isReinstallContainerRequired(DockerContainer container, } } + if (container == DockerContainer::Telemt) { + const auto *oldT = oldConfig.getTelemtProtocolConfig(); + const auto *newT = newConfig.getTelemtProtocolConfig(); + if (oldT && newT) { + const QString oldPort = + oldT->port.isEmpty() ? QString(protocols::telemt::defaultPort) : oldT->port; + const QString newPort = + newT->port.isEmpty() ? QString(protocols::telemt::defaultPort) : newT->port; + if (oldPort != newPort) { + return true; + } + const QString oldTransport = oldT->transportMode.isEmpty() + ? QString(protocols::telemt::transportModeStandard) + : oldT->transportMode; + const QString newTransport = newT->transportMode.isEmpty() + ? QString(protocols::telemt::transportModeStandard) + : newT->transportMode; + if (oldTransport != newTransport) { + return true; + } + if (oldT->tlsDomain != newT->tlsDomain) { + return true; + } + if (oldT->maskEnabled != newT->maskEnabled) { + return true; + } + if (oldT->tlsEmulation != newT->tlsEmulation) { + return true; + } + if (oldT->useMiddleProxy != newT->useMiddleProxy) { + return true; + } + if (oldT->tag != newT->tag) { + return true; + } + const QString oldUser = oldT->userName.isEmpty() + ? QString::fromUtf8(protocols::telemt::defaultUserName) + : oldT->userName; + const QString newUser = newT->userName.isEmpty() + ? QString::fromUtf8(protocols::telemt::defaultUserName) + : newT->userName; + if (oldUser != newUser) { + return true; + } + } + } + if (container == DockerContainer::Socks5Proxy) { return true; } @@ -888,6 +944,7 @@ QScopedPointer InstallController::createInstaller(DockerContainer case DockerContainer::Sftp: return QScopedPointer(new SftpInstaller(this)); case DockerContainer::Socks5Proxy: return QScopedPointer(new Socks5Installer(this)); case DockerContainer::MtProxy: return QScopedPointer(new MtProxyInstaller(this)); + case DockerContainer::Telemt: return QScopedPointer(new TelemtInstaller(this)); default: return QScopedPointer(new InstallerBase(this)); } } @@ -933,6 +990,13 @@ bool InstallController::isUpdateDockerContainerRequired(DockerContainer containe return true; } return !oldMt->equalsDockerDeploymentSettings(*newMt); + } else if (container == DockerContainer::Telemt) { + const auto *oldT = oldConfig.getTelemtProtocolConfig(); + const auto *newT = newConfig.getTelemtProtocolConfig(); + if (!oldT || !newT) { + return true; + } + return !oldT->equalsDockerDeploymentSettings(*newT); } return true; @@ -1261,6 +1325,31 @@ void InstallController::updateContainerConfigAfterInstallation(DockerContainer c mtProxyConfig->tmeLink = mTmeLink.captured(1); } } + } else if (container == DockerContainer::Telemt) { + if (auto *telemtConfig = containerConfig.getTelemtProtocolConfig()) { + qDebug() << "amnezia-telemt configure stdout" << stdOut; + + static const QRegularExpression reSecret( + QStringLiteral(R"(\[\*\]\s+Secret:\s+([0-9a-fA-F]{32}))"), + QRegularExpression::CaseInsensitiveOption); + static const QRegularExpression reTgLink(QStringLiteral(R"(\[\*\]\s+tg://\s+link:\s+(tg://proxy\?[^\s]+))")); + static const QRegularExpression reTmeLink( + QStringLiteral(R"(\[\*\]\s+t\.me\s+link:\s+(https://t\.me/proxy\?[^\s]+))")); + + const QRegularExpressionMatch mSecret = reSecret.match(stdOut); + const QRegularExpressionMatch mTgLink = reTgLink.match(stdOut); + const QRegularExpressionMatch mTmeLink = reTmeLink.match(stdOut); + + if (mSecret.hasMatch()) { + telemtConfig->secret = mSecret.captured(1); + } + if (mTgLink.hasMatch()) { + telemtConfig->tgLink = mTgLink.captured(1); + } + if (mTmeLink.hasMatch()) { + telemtConfig->tmeLink = mTmeLink.captured(1); + } + } } } @@ -1348,6 +1437,9 @@ ErrorCode InstallController::getAlreadyInstalledContainers(const ServerCredentia ErrorCode InstallController::setDockerContainerEnabledState(const QString &serverId, DockerContainer container, bool enabled) { + if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) { + return ErrorCode::InternalError; + } auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId); if (!adminConfig.has_value()) { return ErrorCode::InternalError; @@ -1365,8 +1457,15 @@ ErrorCode InstallController::setDockerContainerEnabledState(const QString &serve return runError; } ContainerConfig currentConfig = adminConfig->containerConfig(container); + bool persist = false; if (auto *mtConfig = currentConfig.getMtProxyProtocolConfig()) { mtConfig->isEnabled = enabled; + persist = true; + } else if (auto *telemtConfig = currentConfig.getTelemtProtocolConfig()) { + telemtConfig->isEnabled = enabled; + persist = true; + } + if (persist) { adminConfig->updateContainerConfig(container, currentConfig); m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin); } @@ -1430,6 +1529,9 @@ ErrorCode InstallController::queryMtProxyDiagnostics(const QString &serverId, Do QString InstallController::fetchDockerContainerSecret(const QString &serverId, DockerContainer container) { + if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) { + return {}; + } auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId); if (!adminConfig.has_value()) { return {}; diff --git a/client/core/diagnostics/telemtDiagnostics.h b/client/core/diagnostics/telemtDiagnostics.h new file mode 100644 index 000000000..d2860d299 --- /dev/null +++ b/client/core/diagnostics/telemtDiagnostics.h @@ -0,0 +1,20 @@ +#ifndef TELEMTDIAGNOSTICS_H +#define TELEMTDIAGNOSTICS_H + +#include "containerDiagnostics.h" + +#include + +namespace amnezia +{ + struct TelemtDiagnostics : ContainerDiagnostics + { + bool upstreamReachable = false; + int clientsConnected = -1; + QString lastConfigRefresh; + QString statsEndpoint; + }; + +} // namespace amnezia + +#endif // TELEMTDIAGNOSTICS_H diff --git a/client/core/installers/installerBase.cpp b/client/core/installers/installerBase.cpp index d4243a5f4..2dc08e85a 100644 --- a/client/core/installers/installerBase.cpp +++ b/client/core/installers/installerBase.cpp @@ -15,6 +15,7 @@ #include "core/models/protocols/sftpProtocolConfig.h" #include "core/models/protocols/socks5ProxyProtocolConfig.h" #include "core/models/protocols/mtProxyProtocolConfig.h" +#include "core/models/protocols/telemtProtocolConfig.h" #include "core/models/protocols/ikev2ProtocolConfig.h" #include "core/models/protocols/torProtocolConfig.h" @@ -98,6 +99,12 @@ ContainerConfig InstallerBase::createBaseConfig(DockerContainer container, int p config.protocolConfig = mtConfig; break; } + case Proto::Telemt: { + TelemtProtocolConfig telemtConfig; + telemtConfig.port = portStr; + config.protocolConfig = telemtConfig; + break; + } case Proto::Ikev2: { Ikev2ProtocolConfig ikev2Config; config.protocolConfig = ikev2Config; diff --git a/client/core/installers/mtProxyInstaller.cpp b/client/core/installers/mtProxyInstaller.cpp index 21ac5bd4a..937dab0db 100644 --- a/client/core/installers/mtProxyInstaller.cpp +++ b/client/core/installers/mtProxyInstaller.cpp @@ -68,11 +68,11 @@ ErrorCode MtProxyInstaller::extractConfigFromContainer(DockerContainer container } ErrorCode MtProxyInstaller::queryDiagnostics(SshSession &sshSession, const ServerCredentials &credentials, - DockerContainer container, int listenPort, - MtProxyContainerDiagnostics &out) + DockerContainer container, int listenPort, + MtProxyContainerDiagnostics &out) { out = {}; - if (container != DockerContainer::MtProxy) { + if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) { return ErrorCode::InternalError; } const QString containerName = ContainerUtils::containerToString(container); diff --git a/client/core/installers/telemtInstaller.cpp b/client/core/installers/telemtInstaller.cpp new file mode 100644 index 000000000..ff0d595c7 --- /dev/null +++ b/client/core/installers/telemtInstaller.cpp @@ -0,0 +1,79 @@ +#include "telemtInstaller.h" + +#include "core/utils/containerEnum.h" +#include "core/utils/containers/containerUtils.h" +#include "core/utils/selfhosted/sshSession.h" +#include "core/models/containerConfig.h" +#include "core/models/protocols/telemtProtocolConfig.h" + +#include +#include +#include +#include + +#include + +using namespace amnezia; + +namespace { + constexpr QLatin1String kTelemtClientJsonPath("/data/amnezia-telemt-client.json"); + constexpr QLatin1String kTelemtClientJsonUploadPath("data/amnezia-telemt-client.json"); + constexpr QLatin1String kTelemtSecretPath("/data/secret"); +} + +TelemtInstaller::TelemtInstaller(QObject *parent) : InstallerBase(parent) {} + +ErrorCode TelemtInstaller::extractConfigFromContainer(DockerContainer container, const ServerCredentials &credentials, + SshSession *sshSession, ContainerConfig &config) { + if (container != DockerContainer::Telemt || !sshSession) { + return ErrorCode::NoError; + } + + TelemtProtocolConfig *tc = config.getTelemtProtocolConfig(); + if (!tc) { + return ErrorCode::NoError; + } + + ErrorCode jsonErr = ErrorCode::NoError; + const QByteArray jsonRaw = + sshSession->getTextFileFromContainer(container, credentials, QString(kTelemtClientJsonPath), jsonErr); + if (jsonErr == ErrorCode::NoError && !jsonRaw.trimmed().isEmpty()) { + QJsonParseError parseError; + const QJsonDocument doc = QJsonDocument::fromJson(jsonRaw.trimmed(), &parseError); + if (parseError.error == QJsonParseError::NoError && doc.isObject()) { + QJsonObject merged = tc->toJson(); + const QJsonObject snap = doc.object(); + for (auto it = snap.constBegin(); it != snap.constEnd(); ++it) { + merged.insert(it.key(), it.value()); + } + *tc = TelemtProtocolConfig::fromJson(merged); + } + } + + ErrorCode secretErr = ErrorCode::NoError; + const QByteArray secretRaw = + sshSession->getTextFileFromContainer(container, credentials, QString(kTelemtSecretPath), secretErr); + const QString sec = QString::fromUtf8(secretRaw).trimmed(); + if (sec.length() == 32) { + static const QRegularExpression hex32(QStringLiteral("^[0-9a-fA-F]{32}$")); + if (hex32.match(sec).hasMatch()) { + tc->secret = sec; + } + } + + return ErrorCode::NoError; +} + +void TelemtInstaller::uploadClientSettingsSnapshot(SshSession &sshSession, const ServerCredentials &credentials, + DockerContainer container, const ContainerConfig &config) { + const TelemtProtocolConfig *tc = config.getTelemtProtocolConfig(); + if (!tc) { + return; + } + const QByteArray payload = QJsonDocument(tc->toJson()).toJson(QJsonDocument::Compact); + const ErrorCode err = sshSession.uploadTextFileToContainer(container, credentials, QString::fromUtf8(payload), + QString(kTelemtClientJsonUploadPath)); + if (err != ErrorCode::NoError) { + qWarning() << "TelemtInstaller::uploadClientSettingsSnapshot failed" << err; + } +} diff --git a/client/core/installers/telemtInstaller.h b/client/core/installers/telemtInstaller.h new file mode 100644 index 000000000..13323e8a7 --- /dev/null +++ b/client/core/installers/telemtInstaller.h @@ -0,0 +1,20 @@ +#ifndef TELEMTINSTALLER_H +#define TELEMTINSTALLER_H + +#include "installerBase.h" + +class TelemtInstaller : public InstallerBase { +Q_OBJECT +public: + explicit TelemtInstaller(QObject *parent = nullptr); + + amnezia::ErrorCode + extractConfigFromContainer(amnezia::DockerContainer container, const amnezia::ServerCredentials &credentials, + SshSession *sshSession, amnezia::ContainerConfig &config) override; + + static void uploadClientSettingsSnapshot(SshSession &sshSession, const amnezia::ServerCredentials &credentials, + amnezia::DockerContainer container, + const amnezia::ContainerConfig &config); +}; + +#endif // TELEMTINSTALLER_H diff --git a/client/core/models/containerConfig.cpp b/client/core/models/containerConfig.cpp index deb123d45..6a008a13d 100644 --- a/client/core/models/containerConfig.cpp +++ b/client/core/models/containerConfig.cpp @@ -123,6 +123,16 @@ const MtProxyProtocolConfig* ContainerConfig::getMtProxyProtocolConfig() const return protocolConfig.as(); } +TelemtProtocolConfig* ContainerConfig::getTelemtProtocolConfig() +{ + return protocolConfig.as(); +} + +const TelemtProtocolConfig* ContainerConfig::getTelemtProtocolConfig() const +{ + return protocolConfig.as(); +} + Ikev2ProtocolConfig* ContainerConfig::getIkev2ProtocolConfig() { return protocolConfig.as(); diff --git a/client/core/models/containerConfig.h b/client/core/models/containerConfig.h index 9f116fc08..b07ff6dff 100644 --- a/client/core/models/containerConfig.h +++ b/client/core/models/containerConfig.h @@ -60,6 +60,9 @@ struct ContainerConfig { MtProxyProtocolConfig* getMtProxyProtocolConfig(); const MtProxyProtocolConfig* getMtProxyProtocolConfig() const; + TelemtProtocolConfig* getTelemtProtocolConfig(); + const TelemtProtocolConfig* getTelemtProtocolConfig() const; + Ikev2ProtocolConfig* getIkev2ProtocolConfig(); const Ikev2ProtocolConfig* getIkev2ProtocolConfig() const; diff --git a/client/core/models/protocolConfig.cpp b/client/core/models/protocolConfig.cpp index 2b3d60864..24e879f18 100644 --- a/client/core/models/protocolConfig.cpp +++ b/client/core/models/protocolConfig.cpp @@ -10,6 +10,7 @@ #include "core/models/protocols/ikev2ProtocolConfig.h" #include "core/models/protocols/dnsProtocolConfig.h" #include "core/models/protocols/mtProxyProtocolConfig.h" +#include "core/models/protocols/telemtProtocolConfig.h" namespace amnezia { @@ -41,6 +42,8 @@ Proto ProtocolConfig::type() const return Proto::Dns; } else if constexpr (std::is_same_v) { return Proto::MtProxy; + } else if constexpr (std::is_same_v) { + return Proto::Telemt; } return Proto::Unknown; }, data); @@ -70,6 +73,8 @@ QString ProtocolConfig::port() const return QString(); } else if constexpr (std::is_same_v) { return arg.port.isEmpty() ? QString(protocols::mtProxy::defaultPort) : arg.port; + } else if constexpr (std::is_same_v) { + return arg.port.isEmpty() ? QString(protocols::telemt::defaultPort) : arg.port; } return QString(); }, data); @@ -95,6 +100,8 @@ QString ProtocolConfig::transportProto() const return QString(); } else if constexpr (std::is_same_v) { return QStringLiteral("tcp"); + } else if constexpr (std::is_same_v) { + return QStringLiteral("tcp"); } return QString(); }, data); @@ -308,6 +315,8 @@ ProtocolConfig ProtocolConfig::fromJson(const QJsonObject& json, Proto type) return ProtocolConfig{DnsProtocolConfig::fromJson(json)}; case Proto::MtProxy: return ProtocolConfig{MtProxyProtocolConfig::fromJson(json)}; + case Proto::Telemt: + return ProtocolConfig{TelemtProtocolConfig::fromJson(json)}; default: return ProtocolConfig{AwgProtocolConfig{}}; } diff --git a/client/core/models/protocolConfig.h b/client/core/models/protocolConfig.h index 8a8cf91f2..324530087 100644 --- a/client/core/models/protocolConfig.h +++ b/client/core/models/protocolConfig.h @@ -23,6 +23,7 @@ #include "core/models/protocols/torProtocolConfig.h" #include "core/models/protocols/dnsProtocolConfig.h" #include "core/models/protocols/mtProxyProtocolConfig.h" +#include "core/models/protocols/telemtProtocolConfig.h" namespace amnezia { @@ -38,6 +39,7 @@ struct ProtocolConfig { SftpProtocolConfig, Socks5ProxyProtocolConfig, MtProxyProtocolConfig, + TelemtProtocolConfig, Ikev2ProtocolConfig, TorProtocolConfig, DnsProtocolConfig diff --git a/client/core/models/protocols/telemtProtocolConfig.cpp b/client/core/models/protocols/telemtProtocolConfig.cpp new file mode 100644 index 000000000..5f55d0e10 --- /dev/null +++ b/client/core/models/protocols/telemtProtocolConfig.cpp @@ -0,0 +1,162 @@ +#include "telemtProtocolConfig.h" + +#include "core/utils/constants/configKeys.h" +#include "core/utils/constants/protocolConstants.h" + +#include +#include + +using namespace amnezia; + +QJsonObject TelemtProtocolConfig::toJson() const +{ + QJsonObject obj; + if (!port.isEmpty()) { + obj[QString(configKey::port)] = port; + } + if (!secret.isEmpty()) { + obj[protocols::telemt::secretKey] = secret; + } + if (!tag.isEmpty()) { + obj[protocols::telemt::tagKey] = tag; + } + if (!tgLink.isEmpty()) { + obj[protocols::telemt::tgLinkKey] = tgLink; + } + if (!tmeLink.isEmpty()) { + obj[protocols::telemt::tmeLinkKey] = tmeLink; + } + obj[protocols::telemt::isEnabledKey] = isEnabled; + if (!publicHost.isEmpty()) { + obj[protocols::telemt::publicHostKey] = publicHost; + } + if (!transportMode.isEmpty()) { + obj[protocols::telemt::transportModeKey] = transportMode; + } + if (!tlsDomain.isEmpty()) { + obj[protocols::telemt::tlsDomainKey] = tlsDomain; + } + obj[protocols::telemt::maskEnabledKey] = maskEnabled; + obj[protocols::telemt::tlsEmulationKey] = tlsEmulation; + obj[protocols::telemt::useMiddleProxyKey] = useMiddleProxy; + if (!userName.isEmpty()) { + obj[protocols::telemt::userNameKey] = userName; + } + if (!additionalSecrets.isEmpty()) { + obj[protocols::telemt::additionalSecretsKey] = QJsonArray::fromStringList(additionalSecrets); + } + if (!workersMode.isEmpty()) { + obj[protocols::telemt::workersModeKey] = workersMode; + } + if (!workers.isEmpty()) { + obj[protocols::telemt::workersKey] = workers; + } + obj[protocols::telemt::natEnabledKey] = natEnabled; + if (!natInternalIp.isEmpty()) { + obj[protocols::telemt::natInternalIpKey] = natInternalIp; + } + if (!natExternalIp.isEmpty()) { + obj[protocols::telemt::natExternalIpKey] = natExternalIp; + } + return obj; +} + +TelemtProtocolConfig TelemtProtocolConfig::fromJson(const QJsonObject &json) +{ + TelemtProtocolConfig c; + c.port = json.value(QString(configKey::port)).toString(); + c.secret = json.value(protocols::telemt::secretKey).toString(); + c.tag = json.value(protocols::telemt::tagKey).toString(); + c.tgLink = json.value(protocols::telemt::tgLinkKey).toString(); + c.tmeLink = json.value(protocols::telemt::tmeLinkKey).toString(); + c.isEnabled = json.value(protocols::telemt::isEnabledKey).toBool(true); + c.publicHost = json.value(protocols::telemt::publicHostKey).toString(); + c.transportMode = json.value(protocols::telemt::transportModeKey).toString(); + c.tlsDomain = json.value(protocols::telemt::tlsDomainKey).toString(); + c.maskEnabled = json.value(protocols::telemt::maskEnabledKey).toBool(true); + c.tlsEmulation = json.value(protocols::telemt::tlsEmulationKey).toBool(false); + c.useMiddleProxy = json.value(protocols::telemt::useMiddleProxyKey).toBool(true); + c.userName = json.value(protocols::telemt::userNameKey).toString(); + for (const auto &v : json.value(protocols::telemt::additionalSecretsKey).toArray()) { + const QString s = v.toString(); + if (!s.isEmpty()) { + c.additionalSecrets.append(s); + } + } + c.workersMode = json.value(protocols::telemt::workersModeKey).toString(); + c.workers = json.value(protocols::telemt::workersKey).toString(); + c.natEnabled = json.value(protocols::telemt::natEnabledKey).toBool(false); + c.natInternalIp = json.value(protocols::telemt::natInternalIpKey).toString(); + c.natExternalIp = json.value(protocols::telemt::natExternalIpKey).toString(); + return c; +} + +bool TelemtProtocolConfig::equalsDockerDeploymentSettings(const TelemtProtocolConfig &other) const +{ + const auto normPort = [](const QString &p) { + return p.isEmpty() ? QString(protocols::telemt::defaultPort) : p; + }; + const auto normTransport = [](const QString &t) { + return t.isEmpty() ? QString(protocols::telemt::transportModeStandard) : t; + }; + const auto normWorkersMode = [](const QString &m) { + return m.isEmpty() ? QString(protocols::telemt::workersModeAuto) : m; + }; + + if (normPort(port) != normPort(other.port)) { + return false; + } + if (normTransport(transportMode) != normTransport(other.transportMode)) { + return false; + } + if (tlsDomain != other.tlsDomain) { + return false; + } + if (secret != other.secret) { + return false; + } + if (tag != other.tag) { + return false; + } + if (publicHost != other.publicHost) { + return false; + } + if (maskEnabled != other.maskEnabled) { + return false; + } + if (tlsEmulation != other.tlsEmulation) { + return false; + } + if (useMiddleProxy != other.useMiddleProxy) { + return false; + } + if (userName != other.userName) { + return false; + } + if (normWorkersMode(workersMode) != normWorkersMode(other.workersMode)) { + return false; + } + if (workers != other.workers) { + return false; + } + if (natEnabled != other.natEnabled) { + return false; + } + if (natInternalIp != other.natInternalIp) { + return false; + } + if (natExternalIp != other.natExternalIp) { + return false; + } + if (isEnabled != other.isEnabled) { + return false; + } + + QStringList aa = additionalSecrets; + QStringList bb = other.additionalSecrets; + aa.removeAll(QString()); + bb.removeAll(QString()); + std::sort(aa.begin(), aa.end()); + std::sort(bb.begin(), bb.end()); + return aa == bb; +} diff --git a/client/core/models/protocols/telemtProtocolConfig.h b/client/core/models/protocols/telemtProtocolConfig.h new file mode 100644 index 000000000..0a8830e60 --- /dev/null +++ b/client/core/models/protocols/telemtProtocolConfig.h @@ -0,0 +1,38 @@ +#ifndef TELEMTPROTOCOLCONFIG_H +#define TELEMTPROTOCOLCONFIG_H + +#include +#include +#include + +namespace amnezia { + +struct TelemtProtocolConfig { + QString port; + QString secret; + QString tag; + QString tgLink; + QString tmeLink; + bool isEnabled = true; + QString publicHost; + QString transportMode; + QString tlsDomain; + bool maskEnabled = true; + bool tlsEmulation = false; + bool useMiddleProxy = true; + QString userName; + QStringList additionalSecrets; + QString workersMode; + QString workers; + bool natEnabled = false; + QString natInternalIp; + QString natExternalIp; + + QJsonObject toJson() const; + static TelemtProtocolConfig fromJson(const QJsonObject &json); + bool equalsDockerDeploymentSettings(const TelemtProtocolConfig &other) const; +}; + +} // namespace amnezia + +#endif // TELEMTPROTOCOLCONFIG_H diff --git a/client/core/protocols/protocolUtils.cpp b/client/core/protocols/protocolUtils.cpp index 2f8d10c2b..fe8a1454b 100644 --- a/client/core/protocols/protocolUtils.cpp +++ b/client/core/protocols/protocolUtils.cpp @@ -70,6 +70,7 @@ QMap ProtocolUtils::protocolHumanNames() { Proto::Sftp, QObject::tr("SFTP service") }, { Proto::Socks5Proxy, QObject::tr("SOCKS5 proxy server") }, { Proto::MtProxy, QObject::tr("MTProxy (Telegram)") }, + { Proto::Telemt, QObject::tr("Telemt (Telegram)") }, }; } @@ -95,6 +96,7 @@ ServiceType ProtocolUtils::protocolService(Proto p) case Proto::Sftp: return ServiceType::Other; case Proto::Socks5Proxy: return ServiceType::Other; case Proto::MtProxy: return ServiceType::Other; + case Proto::Telemt: return ServiceType::Other; default: return ServiceType::Other; } } @@ -108,6 +110,7 @@ int ProtocolUtils::getPortForInstall(Proto p) case Socks5Proxy: return QRandomGenerator::global()->bounded(30000, 50000); case MtProxy: + case Telemt: default: return defaultPort(p); } @@ -128,6 +131,7 @@ int ProtocolUtils::defaultPort(Proto p) case Proto::Sftp: return 222; case Proto::Socks5Proxy: return 38080; case Proto::MtProxy: return QString(protocols::mtProxy::defaultPort).toInt(); + case Proto::Telemt: return QString(protocols::telemt::defaultPort).toInt(); default: return -1; } } @@ -147,6 +151,7 @@ bool ProtocolUtils::defaultPortChangeable(Proto p) case Proto::Sftp: return true; case Proto::Socks5Proxy: return true; case Proto::MtProxy: return true; + case Proto::Telemt: return true; default: return false; } } @@ -168,6 +173,7 @@ TransportProto ProtocolUtils::defaultTransportProto(Proto p) case Proto::Sftp: return TransportProto::Tcp; case Proto::Socks5Proxy: return TransportProto::Tcp; case Proto::MtProxy: return TransportProto::Tcp; + case Proto::Telemt: return TransportProto::Tcp; default: return TransportProto::Udp; } } @@ -188,9 +194,9 @@ bool ProtocolUtils::defaultTransportProtoChangeable(Proto p) case Proto::Sftp: return false; case Proto::Socks5Proxy: return false; case Proto::MtProxy: return false; + case Proto::Telemt: return false; default: return false; } - return false; } QString ProtocolUtils::key_proto_config_data(Proto p) diff --git a/client/core/utils/constants/configKeys.h b/client/core/utils/constants/configKeys.h index 71d63674a..62d9577e4 100644 --- a/client/core/utils/constants/configKeys.h +++ b/client/core/utils/constants/configKeys.h @@ -94,6 +94,7 @@ namespace amnezia constexpr QLatin1String ssxray("ssxray"); constexpr QLatin1String socks5proxy("socks5proxy"); constexpr QLatin1String mtproxy("mtproxy"); + constexpr QLatin1String telemt("telemt"); constexpr QLatin1String splitTunnelSites("splitTunnelSites"); constexpr QLatin1String splitTunnelType("splitTunnelType"); diff --git a/client/core/utils/constants/protocolConstants.h b/client/core/utils/constants/protocolConstants.h index 0cb471d61..ec502669d 100644 --- a/client/core/utils/constants/protocolConstants.h +++ b/client/core/utils/constants/protocolConstants.h @@ -205,6 +205,40 @@ namespace amnezia constexpr char defaultTlsDomain[] = "googletagmanager.com"; } + namespace telemt + { + constexpr char secretKey[] = "telemt_secret"; + constexpr char tagKey[] = "telemt_tag"; + constexpr char tgLinkKey[] = "telemt_tg_link"; + constexpr char tmeLinkKey[] = "telemt_tme_link"; + constexpr char isEnabledKey[] = "telemt_is_enabled"; + constexpr char publicHostKey[] = "telemt_public_host"; + constexpr char transportModeKey[] = "telemt_transport_mode"; + constexpr char tlsDomainKey[] = "telemt_tls_domain"; + constexpr char maskEnabledKey[] = "telemt_mask_enabled"; + constexpr char tlsEmulationKey[] = "telemt_tls_emulation"; + constexpr char useMiddleProxyKey[] = "telemt_use_middle_proxy"; + constexpr char userNameKey[] = "telemt_user_name"; + // Stored for UI only (Telemt server ignores these; same controls as MTProxy page) + constexpr char additionalSecretsKey[] = "telemt_additional_secrets"; + constexpr char workersKey[] = "telemt_workers"; + constexpr char workersModeKey[] = "telemt_workers_mode"; + constexpr char natEnabledKey[] = "telemt_nat_enabled"; + constexpr char natInternalIpKey[] = "telemt_nat_internal_ip"; + constexpr char natExternalIpKey[] = "telemt_nat_external_ip"; + + constexpr char transportModeStandard[] = "standard"; + constexpr char transportModeFakeTLS[] = "faketls"; + + constexpr char defaultPort[] = "443"; + constexpr char defaultTlsDomain[] = "googletagmanager.com"; + constexpr char defaultUserName[] = "amnezia"; + constexpr char defaultWorkers[] = "2"; + constexpr char workersModeAuto[] = "auto"; + constexpr char workersModeManual[] = "manual"; + constexpr int maxWorkers = 32; + } + } // namespace protocols } diff --git a/client/core/utils/containerEnum.h b/client/core/utils/containerEnum.h index 8e4fc33f8..986aff92f 100644 --- a/client/core/utils/containerEnum.h +++ b/client/core/utils/containerEnum.h @@ -25,6 +25,7 @@ namespace amnezia Sftp, Socks5Proxy, MtProxy, + Telemt, }; Q_ENUM_NS(DockerContainer) } // namespace ContainerEnumNS diff --git a/client/core/utils/containers/containerUtils.cpp b/client/core/utils/containers/containerUtils.cpp index cda3353d5..29664408f 100644 --- a/client/core/utils/containers/containerUtils.cpp +++ b/client/core/utils/containers/containerUtils.cpp @@ -74,6 +74,7 @@ QMap ContainerUtils::containerHumanNames() { DockerContainer::Sftp, QObject::tr("SFTP file sharing service") }, { DockerContainer::Socks5Proxy, QObject::tr("SOCKS5 proxy server") }, { DockerContainer::MtProxy, QObject::tr("MTProxy (Telegram)") }, + { DockerContainer::Telemt, QObject::tr("Telemt (Telegram)") }, }; } @@ -107,6 +108,8 @@ QMap ContainerUtils::containerDescriptions() QObject::tr("") }, { DockerContainer::MtProxy, QObject::tr("Telegram MTProto proxy server") }, + { DockerContainer::Telemt, + QObject::tr("Telegram MTProto proxy (Telemt, Rust)") }, }; } @@ -183,6 +186,9 @@ QMap ContainerUtils::containerDetailedDescriptions() "Allows Telegram clients to connect through your server " "using the MTProto protocol. Supports FakeTLS mode for " "bypassing DPI-based blocking.") }, + { DockerContainer::Telemt, + QObject::tr("Telegram MTProto proxy powered by Telemt (Rust). " + "Supports secure and TLS fronting modes with optional traffic masking.") }, }; } @@ -208,6 +214,7 @@ Proto ContainerUtils::defaultProtocol(DockerContainer c) case DockerContainer::Sftp: return Proto::Sftp; case DockerContainer::Socks5Proxy: return Proto::Socks5Proxy; case DockerContainer::MtProxy: return Proto::MtProxy; + case DockerContainer::Telemt: return Proto::Telemt; default: return Proto::Unknown; } } @@ -236,6 +243,7 @@ bool ContainerUtils::isSupportedByCurrentPlatform(DockerContainer c) case DockerContainer::Xray: return true; case DockerContainer::SSXray: return true; case DockerContainer::MtProxy: return true; + case DockerContainer::Telemt: return true; default: return false; } @@ -250,6 +258,7 @@ bool ContainerUtils::isSupportedByCurrentPlatform(DockerContainer c) case DockerContainer::Xray: return true; case DockerContainer::SSXray: return true; case DockerContainer::MtProxy: return true; + case DockerContainer::Telemt: return true; default: return false; } @@ -269,6 +278,7 @@ bool ContainerUtils::isSupportedByCurrentPlatform(DockerContainer c) case DockerContainer::Xray: return true; case DockerContainer::SSXray: return true; case DockerContainer::MtProxy: return true; + case DockerContainer::Telemt: return true; default: return false; } @@ -332,6 +342,7 @@ bool ContainerUtils::isShareable(DockerContainer container) case DockerContainer::Sftp: return false; case DockerContainer::Socks5Proxy: return false; case DockerContainer::MtProxy: return false; + case DockerContainer::Telemt: return false; default: return true; } } @@ -361,6 +372,7 @@ int ContainerUtils::installPageOrder(DockerContainer container) case DockerContainer::Ipsec: return 7; case DockerContainer::SSXray: return 8; case DockerContainer::MtProxy: + case DockerContainer::Telemt: return 20; default: return 0; } diff --git a/client/core/utils/protocolEnum.h b/client/core/utils/protocolEnum.h index 5293d3fe2..19fdc67dc 100644 --- a/client/core/utils/protocolEnum.h +++ b/client/core/utils/protocolEnum.h @@ -32,6 +32,7 @@ namespace amnezia Sftp, Socks5Proxy, MtProxy, + Telemt, }; Q_ENUM_NS(Proto) diff --git a/client/core/utils/selfhosted/scriptsRegistry.cpp b/client/core/utils/selfhosted/scriptsRegistry.cpp index e0e49c26b..14c43eaab 100644 --- a/client/core/utils/selfhosted/scriptsRegistry.cpp +++ b/client/core/utils/selfhosted/scriptsRegistry.cpp @@ -20,6 +20,7 @@ #include "core/models/protocols/sftpProtocolConfig.h" #include "core/models/protocols/socks5ProxyProtocolConfig.h" #include "core/models/protocols/mtProxyProtocolConfig.h" +#include "core/models/protocols/telemtProtocolConfig.h" using namespace amnezia; using namespace ProtocolUtils; @@ -39,6 +40,7 @@ QString amnezia::scriptFolder(amnezia::DockerContainer container) case DockerContainer::Sftp: return QLatin1String("sftp"); case DockerContainer::Socks5Proxy: return QLatin1String("socks5_proxy"); case DockerContainer::MtProxy: return QLatin1String("mtproxy"); + case DockerContainer::Telemt: return QLatin1String("telemt"); default: return QString(); } } @@ -334,6 +336,37 @@ amnezia::ScriptVars amnezia::genMtProxyVars(const ContainerConfig &containerConf return vars; } +amnezia::ScriptVars amnezia::genTelemtVars(const ContainerConfig &containerConfig) +{ + ScriptVars vars; + + if (auto *telemtProtocolConfig = containerConfig.getTelemtProtocolConfig()) { + const TelemtProtocolConfig &c = *telemtProtocolConfig; + + const QString transport = c.transportMode.isEmpty() ? QString(protocols::telemt::transportModeStandard) + : c.transportMode; + const bool faketls = (transport == QLatin1String(protocols::telemt::transportModeFakeTLS)); + vars.append({ { "$TELEMT_TOML_SECURE", faketls ? QLatin1String("false") : QLatin1String("true") } }); + vars.append({ { "$TELEMT_TOML_TLS", faketls ? QLatin1String("true") : QLatin1String("false") } }); + vars.append({ { "$TELEMT_PORT", c.port.isEmpty() ? QString(protocols::telemt::defaultPort) : c.port } }); + vars.append({ { "$TELEMT_SECRET", c.secret } }); + vars.append({ { "$TELEMT_TAG", c.tag } }); + QString tlsDomain = c.tlsDomain; + if (tlsDomain.isEmpty()) { + tlsDomain = QString(protocols::telemt::defaultTlsDomain); + } + vars.append({ { "$TELEMT_TLS_DOMAIN", tlsDomain } }); + vars.append({ { "$TELEMT_PUBLIC_HOST", c.publicHost } }); + vars.append({ { "$TELEMT_USER_NAME", + c.userName.isEmpty() ? QString::fromUtf8(protocols::telemt::defaultUserName) : c.userName } }); + vars.append({ { "$TELEMT_USE_MIDDLE_PROXY", c.useMiddleProxy ? QLatin1String("true") : QLatin1String("false") } }); + vars.append({ { "$TELEMT_MASK", c.maskEnabled ? QLatin1String("true") : QLatin1String("false") } }); + vars.append({ { "$TELEMT_TLS_EMULATION", c.tlsEmulation ? QLatin1String("true") : QLatin1String("false") } }); + } + + return vars; +} + amnezia::ScriptVars amnezia::genProtocolVarsForContainer(DockerContainer container, const ContainerConfig &containerConfig) { ScriptVars vars; @@ -361,6 +394,9 @@ amnezia::ScriptVars amnezia::genProtocolVarsForContainer(DockerContainer contain case Proto::MtProxy: vars.append(genMtProxyVars(containerConfig)); break; + case Proto::Telemt: + vars.append(genTelemtVars(containerConfig)); + break; default: break; } diff --git a/client/core/utils/selfhosted/scriptsRegistry.h b/client/core/utils/selfhosted/scriptsRegistry.h index cf3c39394..f63b850a6 100644 --- a/client/core/utils/selfhosted/scriptsRegistry.h +++ b/client/core/utils/selfhosted/scriptsRegistry.h @@ -68,6 +68,7 @@ ScriptVars genAwgVars(const ContainerConfig &containerConfig); ScriptVars genSftpVars(const ContainerConfig &containerConfig); ScriptVars genSocks5ProxyVars(const ContainerConfig &containerConfig); ScriptVars genMtProxyVars(const ContainerConfig &containerConfig); +ScriptVars genTelemtVars(const ContainerConfig &containerConfig); ScriptVars genProtocolVarsForContainer(DockerContainer container, const ContainerConfig &containerConfig); } diff --git a/client/core/utils/selfhosted/sshSession.cpp b/client/core/utils/selfhosted/sshSession.cpp index c2360c6d1..363745fd5 100644 --- a/client/core/utils/selfhosted/sshSession.cpp +++ b/client/core/utils/selfhosted/sshSession.cpp @@ -103,7 +103,7 @@ ErrorCode SshSession::runContainerScript(const ServerCredentials &credentials, D if (e) return e; - const bool useSh = container == DockerContainer::Socks5Proxy || container == DockerContainer::MtProxy; + const bool useSh = container == DockerContainer::Socks5Proxy || container == DockerContainer::MtProxy || container == DockerContainer::Telemt; QString runner = QString("sudo docker exec -i $CONTAINER_NAME %2 %1 ").arg(fileName, useSh ? "sh" : "bash"); e = runScript(credentials, replaceVars(runner, amnezia::genBaseVars(credentials, container, QString(), QString())), cbReadStdOut, cbReadStdErr); diff --git a/client/server_scripts/serverScripts.qrc b/client/server_scripts/serverScripts.qrc index 35a2be2a3..278e16953 100644 --- a/client/server_scripts/serverScripts.qrc +++ b/client/server_scripts/serverScripts.qrc @@ -28,6 +28,10 @@ mtproxy/Dockerfile mtproxy/run_container.sh mtproxy/start.sh + telemt/configure_container.sh + telemt/Dockerfile + telemt/run_container.sh + telemt/start.sh openvpn/configure_container.sh openvpn/Dockerfile openvpn/run_container.sh diff --git a/client/server_scripts/telemt/Dockerfile b/client/server_scripts/telemt/Dockerfile new file mode 100644 index 000000000..ad3f27365 --- /dev/null +++ b/client/server_scripts/telemt/Dockerfile @@ -0,0 +1,42 @@ +# syntax=docker/dockerfile:1 +# Debian-based image with Telemt binary (shell + jq for Amnezia configure scripts). +# Binary from https://github.com/telemt/telemt releases (same pattern as upstream Dockerfile minimal stage). + +FROM debian:12-slim + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + binutils \ + ca-certificates \ + curl \ + jq \ + openssl \ + tar \ + && rm -rf /var/lib/apt/lists/* + +# Use machine arch (works with classic `docker build`; TARGETARCH is only set with BuildKit). +RUN set -eux; \ + ARCH="$(uname -m)"; \ + case "$ARCH" in \ + x86_64) ASSET="telemt-x86_64-linux-musl.tar.gz" ;; \ + aarch64|arm64) ASSET="telemt-aarch64-linux-musl.tar.gz" ;; \ + *) echo "Unsupported architecture: $ARCH" >&2; exit 1 ;; \ + esac; \ + curl -fL --retry 5 --retry-delay 3 --connect-timeout 10 --max-time 120 \ + -o "/tmp/${ASSET}" "https://github.com/telemt/telemt/releases/latest/download/${ASSET}"; \ + curl -fL --retry 5 --retry-delay 3 --connect-timeout 10 --max-time 120 \ + -o "/tmp/${ASSET}.sha256" "https://github.com/telemt/telemt/releases/latest/download/${ASSET}.sha256"; \ + cd /tmp && sha256sum -c "${ASSET}.sha256"; \ + tar -xzf "${ASSET}" -C /tmp; \ + test -f /tmp/telemt; \ + install -m 0755 /tmp/telemt /usr/local/bin/telemt; \ + strip --strip-unneeded /usr/local/bin/telemt || true; \ + rm -f "/tmp/${ASSET}" "/tmp/${ASSET}.sha256" /tmp/telemt + +RUN mkdir -p /opt/amnezia /data +RUN printf '#!/bin/sh\ntail -f /dev/null\n' > /opt/amnezia/start.sh && \ + chmod a+x /opt/amnezia/start.sh + +VOLUME /data +ENTRYPOINT ["/bin/sh", "/opt/amnezia/start.sh"] +CMD [""] diff --git a/client/server_scripts/telemt/configure_container.sh b/client/server_scripts/telemt/configure_container.sh new file mode 100644 index 000000000..6cd9d31db --- /dev/null +++ b/client/server_scripts/telemt/configure_container.sh @@ -0,0 +1,73 @@ +#!/bin/sh +# Do not use set -e: Telemt / curl / kill edge cases should not abort the whole configure step. + +echo "[*] Amnezia Telemt: configure script start" +mkdir -p /data/tlsfront + +# Secret: substituted $TELEMT_SECRET -> saved file -> openssl (same rules as MTProxy configure) +if [ -n "$TELEMT_SECRET" ]; then + SECRET="$TELEMT_SECRET" +elif [ -f /data/secret ]; then + SECRET=$(cat /data/secret) +else + SECRET=$(openssl rand -hex 16) +fi +# Must be exactly 32 hex chars +echo "$SECRET" | grep -qE '^[0-9a-fA-F]{32}$' || SECRET=$(openssl rand -hex 16) + +# Build config.toml (other variables substituted on the host by Amnezia before upload) +rm -f /data/config.toml + +{ + echo "### Amnezia Telemt — generated" + echo "[general]" + echo "use_middle_proxy = $TELEMT_USE_MIDDLE_PROXY" + echo "log_level = \"normal\"" + if [ -n "$TELEMT_TAG" ]; then + echo "ad_tag = \"$TELEMT_TAG\"" + fi + echo "" + echo "[general.modes]" + echo "classic = false" + echo "secure = $TELEMT_TOML_SECURE" + echo "tls = $TELEMT_TOML_TLS" + echo "" + echo "[general.links]" + echo "show = \"*\"" + if [ -n "$TELEMT_PUBLIC_HOST" ]; then + echo "public_host = \"$TELEMT_PUBLIC_HOST\"" + fi + echo "public_port = $TELEMT_PORT" + echo "" + echo "[server]" + echo "port = $TELEMT_PORT" + echo "" + echo "[server.api]" + echo "enabled = true" + echo "listen = \"0.0.0.0:9091\"" + # Match upstream Telemt default: localhost API only (curl in this script uses 127.0.0.1). + echo "whitelist = [\"127.0.0.0/8\"]" + echo "" + echo "[[server.listeners]]" + echo "ip = \"0.0.0.0\"" + echo "" + echo "[censorship]" + echo "tls_domain = \"$TELEMT_TLS_DOMAIN\"" + echo "mask = $TELEMT_MASK" + echo "tls_emulation = $TELEMT_TLS_EMULATION" + echo "tls_front_dir = \"/data/tlsfront\"" + echo "" + echo "[access.users]" + echo "$TELEMT_USER_NAME = \"$SECRET\"" +} > /data/config.toml + +echo "$SECRET" > /data/secret +chmod 600 /data/secret 2>/dev/null || true + +# Do not start telemt here: a long-lived process + curl loop inside `docker exec` can confuse SSH/Docker +# timing and is unnecessary — start.sh runs telemt after configure. Links can be empty until the service +# is up; the client still parses Secret below. +echo "[*] Telemt configuration" +echo "[*] Secret: $SECRET" +echo "[*] tg:// link: " +echo "[*] t.me link: " diff --git a/client/server_scripts/telemt/run_container.sh b/client/server_scripts/telemt/run_container.sh new file mode 100644 index 000000000..24d3516e3 --- /dev/null +++ b/client/server_scripts/telemt/run_container.sh @@ -0,0 +1,9 @@ +# Run container (ulimit per Telemt docs — avoids "Too many open files" under load) +sudo docker run -d \ + --log-driver none \ + --restart always \ + --ulimit nofile=65536:65536 \ + -p $TELEMT_PORT:$TELEMT_PORT/tcp \ + -v amnezia-telemt-data:/data \ + --name $CONTAINER_NAME \ + $CONTAINER_NAME diff --git a/client/server_scripts/telemt/start.sh b/client/server_scripts/telemt/start.sh new file mode 100644 index 000000000..c7799aa4d --- /dev/null +++ b/client/server_scripts/telemt/start.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +echo "Container startup (Telemt)" + +if [ ! -f /data/config.toml ]; then + echo "ERROR: /data/config.toml not found — run configure_container first" + tail -f /dev/null + exit 1 +fi + +mkdir -p /data/tlsfront +exec /usr/local/bin/telemt /data/config.toml diff --git a/client/ui/controllers/qml/pageController.h b/client/ui/controllers/qml/pageController.h index b5e6007b7..714333a5a 100644 --- a/client/ui/controllers/qml/pageController.h +++ b/client/ui/controllers/qml/pageController.h @@ -51,6 +51,7 @@ namespace PageLoader PageServiceDnsSettings, PageServiceSocksProxySettings, PageServiceMtProxySettings, + PageServiceTelemtSettings, PageSetupWizardStart, PageSetupWizardCredentials, diff --git a/client/ui/controllers/selfhosted/installUiController.cpp b/client/ui/controllers/selfhosted/installUiController.cpp index 6e674f452..ad42f0b86 100755 --- a/client/ui/controllers/selfhosted/installUiController.cpp +++ b/client/ui/controllers/selfhosted/installUiController.cpp @@ -50,6 +50,7 @@ InstallUiController::InstallUiController(InstallController *installController, SftpConfigModel *sftpConfigModel, Socks5ProxyConfigModel *socks5ConfigModel, MtProxyConfigModel* mtConfigModel, + TelemtConfigModel *telemtConfigModel, QObject *parent) : QObject(parent), m_installController(installController), @@ -67,7 +68,8 @@ InstallUiController::InstallUiController(InstallController *installController, #endif m_sftpConfigModel(sftpConfigModel), m_socks5ConfigModel(socks5ConfigModel), - m_mtProxyConfigModel(mtConfigModel) + m_mtProxyConfigModel(mtConfigModel), + m_telemtConfigModel(telemtConfigModel) { connect(m_installController, &InstallController::configValidated, this, &InstallUiController::configValidated); connect(m_installController, &InstallController::validationErrorOccurred, this, [this](ErrorCode errorCode) { @@ -246,6 +248,10 @@ void InstallUiController::updateContainer(const QString &serverId, int container containerConfig.protocolConfig = m_mtProxyConfigModel->getProtocolConfig(); break; } + case Proto::Telemt: { + containerConfig.protocolConfig = m_telemtConfigModel->getProtocolConfig(); + break; + } #ifdef Q_OS_WINDOWS case Proto::Ikev2: { containerConfig.protocolConfig = m_ikev2ConfigModel->getProtocolConfig(); @@ -257,7 +263,7 @@ void InstallUiController::updateContainer(const QString &serverId, int container } ContainerConfig oldContainerConfig = m_serversController->getContainerConfig(serverId, container); - if (container == DockerContainer::MtProxy) { + if (container == DockerContainer::MtProxy || container == DockerContainer::Telemt) { emit serverIsBusy(true); auto *watcher = new QFutureWatcher(this); QObject::connect(watcher, &QFutureWatcher::finished, this, @@ -317,6 +323,9 @@ void InstallUiController::updateContainer(const QString &serverId, int container void InstallUiController::setContainerEnabled(const QString &serverId, int containerIndex, bool enabled) { const DockerContainer container = static_cast(containerIndex); + if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) { + return; + } emit serverIsBusy(true); const ErrorCode errorCode = m_installController->setDockerContainerEnabledState(serverId, container, enabled); @@ -335,6 +344,10 @@ void InstallUiController::setContainerEnabled(const QString &serverId, int conta void InstallUiController::refreshContainerStatus(const QString &serverId, int containerIndex) { const DockerContainer container = static_cast(containerIndex); + if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) { + return; + } + int status = 3; const ErrorCode errorCode = m_installController->queryDockerContainerStatus(serverId, container, status); if (errorCode != ErrorCode::NoError) { @@ -347,6 +360,10 @@ void InstallUiController::refreshContainerStatus(const QString &serverId, int co void InstallUiController::refreshContainerDiagnostics(const QString &serverId, int containerIndex, int port) { const DockerContainer container = static_cast(containerIndex); + if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) { + return; + } + MtProxyContainerDiagnostics diag; const ErrorCode errorCode = m_installController->queryMtProxyDiagnostics(serverId, container, port, diag); if (errorCode != ErrorCode::NoError) { @@ -360,6 +377,10 @@ void InstallUiController::refreshContainerDiagnostics(const QString &serverId, i void InstallUiController::fetchContainerSecret(const QString &serverId, int containerIndex) { const DockerContainer container = static_cast(containerIndex); + if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) { + return; + } + const QString secret = m_installController->fetchDockerContainerSecret(serverId, container); emit containerSecretFetched(secret); } @@ -576,6 +597,7 @@ void InstallUiController::updateProtocolConfigModel(const QString &serverId, int case Proto::Sftp: updateIfPresent(m_sftpConfigModel, containerConfig.getSftpProtocolConfig()); break; case Proto::Socks5Proxy: updateIfPresent(m_socks5ConfigModel, containerConfig.getSocks5ProxyProtocolConfig()); break; case Proto::MtProxy: updateIfPresent(m_mtProxyConfigModel, containerConfig.getMtProxyProtocolConfig()); break; + case Proto::Telemt: updateIfPresent(m_telemtConfigModel, containerConfig.getTelemtProtocolConfig()); break; #ifdef Q_OS_WINDOWS case Proto::Ikev2: updateIfPresent(m_ikev2ConfigModel, containerConfig.getIkev2ProtocolConfig()); break; #endif diff --git a/client/ui/controllers/selfhosted/installUiController.h b/client/ui/controllers/selfhosted/installUiController.h index c8332e555..b0683552a 100644 --- a/client/ui/controllers/selfhosted/installUiController.h +++ b/client/ui/controllers/selfhosted/installUiController.h @@ -29,6 +29,7 @@ #include "core/models/protocols/sftpProtocolConfig.h" #include "core/models/protocols/socks5ProxyProtocolConfig.h" #include "ui/models/services/mtProxyConfigModel.h" +#include "ui/models/services/telemtConfigModel.h" class InstallUiController : public QObject { @@ -50,6 +51,7 @@ public: SftpConfigModel* sftpConfigModel, Socks5ProxyConfigModel* socks5ConfigModel, MtProxyConfigModel* mtConfigModel, + TelemtConfigModel* telemtConfigModel, QObject *parent = nullptr); ~InstallUiController(); @@ -152,6 +154,7 @@ private: SftpConfigModel* m_sftpConfigModel; Socks5ProxyConfigModel* m_socks5ConfigModel; MtProxyConfigModel* m_mtProxyConfigModel; + TelemtConfigModel* m_telemtConfigModel; ServerCredentials m_processedServerCredentials; diff --git a/client/ui/controllers/serversUiController.cpp b/client/ui/controllers/serversUiController.cpp index c2e3955b6..48fc15540 100644 --- a/client/ui/controllers/serversUiController.cpp +++ b/client/ui/controllers/serversUiController.cpp @@ -512,6 +512,8 @@ QStringList ServersUiController::getAllInstalledServicesName(int serverIndex) co servicesName.append("SOCKS5"); } else if (container == DockerContainer::MtProxy) { servicesName.append("MTProxy"); + } else if (container == DockerContainer::Telemt) { + servicesName.append("Telemt"); } } } diff --git a/client/ui/models/containersModel.cpp b/client/ui/models/containersModel.cpp index ade74f984..e176ac167 100644 --- a/client/ui/models/containersModel.cpp +++ b/client/ui/models/containersModel.cpp @@ -75,6 +75,7 @@ QVariant ContainersModel::data(const QModelIndex &index, int role) const case IsTorWebsiteRole: return container == DockerContainer::TorWebSite; case IsSocks5ProxyRole: return container == DockerContainer::Socks5Proxy; case IsMtProxyRole: return container == DockerContainer::MtProxy; + case IsTelemtRole: return container == DockerContainer::Telemt; case InstallPageOrderRole: return ContainerUtils::installPageOrder(container); } @@ -186,5 +187,6 @@ QHash ContainersModel::roleNames() const roles[IsTorWebsiteRole] = "isTorWebsite"; roles[IsSocks5ProxyRole] = "isSocks5Proxy"; roles[IsMtProxyRole] = "isMtProxy"; + roles[IsTelemtRole] = "isTelemt"; return roles; } diff --git a/client/ui/models/containersModel.h b/client/ui/models/containersModel.h index d88628d91..eec1be794 100644 --- a/client/ui/models/containersModel.h +++ b/client/ui/models/containersModel.h @@ -50,6 +50,7 @@ public: IsTorWebsiteRole, IsSocks5ProxyRole, IsMtProxyRole, + IsTelemtRole, }; Q_INVOKABLE void openContainerSettings(int containerIndex); diff --git a/client/ui/models/protocolsModel.cpp b/client/ui/models/protocolsModel.cpp index c0cbe99ce..b5d07e3c4 100644 --- a/client/ui/models/protocolsModel.cpp +++ b/client/ui/models/protocolsModel.cpp @@ -43,6 +43,7 @@ QHash ProtocolsModel::roleNames() const roles[IsIpsecRole] = "isIpsec"; roles[IsSocks5ProxyRole] = "isSocks5Proxy"; roles[IsMtProxyRole] = "isMtProxy"; + roles[IsTelemtRole] = "isTelemt"; return roles; } @@ -73,6 +74,7 @@ QVariant ProtocolsModel::data(const QModelIndex &index, int role) const case IsIpsecRole: return proto == Proto::Ikev2; case IsSocks5ProxyRole: return proto == Proto::Socks5Proxy; case IsMtProxyRole: return proto == Proto::MtProxy; + case IsTelemtRole: return proto == Proto::Telemt; case RawConfigRole: return getRawConfig(); case IsClientProtocolExistsRole: @@ -127,6 +129,7 @@ PageLoader::PageEnum ProtocolsModel::serverProtocolPage(Proto protocol) const case Proto::Sftp: return PageLoader::PageEnum::PageServiceSftpSettings; case Proto::Socks5Proxy: return PageLoader::PageEnum::PageServiceSocksProxySettings; case Proto::MtProxy: return PageLoader::PageEnum::PageServiceMtProxySettings; + case Proto::Telemt: return PageLoader::PageEnum::PageServiceTelemtSettings; default: return PageLoader::PageEnum::PageProtocolOpenVpnSettings; } } diff --git a/client/ui/models/protocolsModel.h b/client/ui/models/protocolsModel.h index 01f9b9cd4..e5ed345a8 100644 --- a/client/ui/models/protocolsModel.h +++ b/client/ui/models/protocolsModel.h @@ -27,6 +27,7 @@ public: IsIpsecRole, IsSocks5ProxyRole, IsMtProxyRole, + IsTelemtRole, }; explicit ProtocolsModel(QObject *parent = nullptr); diff --git a/client/ui/models/services/telemtConfigModel.cpp b/client/ui/models/services/telemtConfigModel.cpp new file mode 100644 index 000000000..6a3fd9eb1 --- /dev/null +++ b/client/ui/models/services/telemtConfigModel.cpp @@ -0,0 +1,406 @@ +#include "telemtConfigModel.h" + +#include + +#include "core/utils/qrCodeUtils.h" +#include "core/utils/constants/configKeys.h" +#include "core/utils/constants/protocolConstants.h" +#include "qrcodegen.hpp" + +using namespace amnezia; + +TelemtConfigModel::TelemtConfigModel(QObject *parent) : QAbstractListModel(parent) {} + +void TelemtConfigModel::applyDefaults(TelemtProtocolConfig &c) { + if (c.port.isEmpty()) { + c.port = QString::fromUtf8(protocols::telemt::defaultPort); + } + if (c.transportMode.isEmpty()) { + c.transportMode = QString::fromUtf8(protocols::telemt::transportModeStandard); + } + if (c.workersMode.isEmpty()) { + c.workersMode = QString::fromUtf8(protocols::telemt::workersModeAuto); + } + if (c.workers.isEmpty()) { + c.workers = QString::fromUtf8(protocols::telemt::defaultWorkers); + } + if (c.userName.isEmpty()) { + c.userName = QString::fromUtf8(protocols::telemt::defaultUserName); + } +} + +int TelemtConfigModel::rowCount(const QModelIndex &parent) const { + Q_UNUSED(parent); + return 1; +} + +bool TelemtConfigModel::setData(const QModelIndex &index, const QVariant &value, int role) { + if (!index.isValid() || index.row() != 0) { + return false; + } + + switch (role) { + case Roles::PortRole: { + m_protocolConfig.port = value.toString(); + break; + } + case Roles::SecretRole: { + m_protocolConfig.secret = value.toString(); + break; + } + case Roles::TagRole: { + m_protocolConfig.tag = value.toString(); + break; + } + case Roles::IsEnabledRole: { + m_protocolConfig.isEnabled = value.toBool(); + break; + } + case Roles::PublicHostRole: { + m_protocolConfig.publicHost = value.toString(); + break; + } + case Roles::TransportModeRole: { + m_protocolConfig.transportMode = value.toString(); + break; + } + case Roles::TlsDomainRole: { + m_protocolConfig.tlsDomain = value.toString(); + break; + } + case Roles::AdditionalSecretsRole: { + m_protocolConfig.additionalSecrets = value.toStringList(); + break; + } + case Roles::WorkersModeRole: { + m_protocolConfig.workersMode = value.toString(); + break; + } + case Roles::WorkersRole: { + m_protocolConfig.workers = value.toString(); + break; + } + case Roles::NatEnabledRole: { + m_protocolConfig.natEnabled = value.toBool(); + break; + } + case Roles::NatInternalIpRole: { + m_protocolConfig.natInternalIp = value.toString(); + break; + } + case Roles::NatExternalIpRole: { + m_protocolConfig.natExternalIp = value.toString(); + break; + } + case Roles::MaskEnabledRole: { + m_protocolConfig.maskEnabled = value.toBool(); + break; + } + case Roles::UseMiddleProxyRole: { + m_protocolConfig.useMiddleProxy = value.toBool(); + break; + } + case Roles::TlsEmulationRole: { + m_protocolConfig.tlsEmulation = value.toBool(); + break; + } + case Roles::UserNameRole: { + m_protocolConfig.userName = value.toString(); + break; + } + default: { + return false; + } + } + + emit dataChanged(index, index, QList{role}); + return true; +} + +QVariant TelemtConfigModel::data(const QModelIndex &index, int role) const { + if (!index.isValid() || index.row() != 0) { + return QVariant(); + } + + switch (role) { + case Roles::PortRole: { + return m_protocolConfig.port.isEmpty() ? QString::fromUtf8(protocols::telemt::defaultPort) + : m_protocolConfig.port; + } + case Roles::SecretRole: { + return m_protocolConfig.secret; + } + case Roles::TagRole: { + return m_protocolConfig.tag; + } + case Roles::TgLinkRole: { + return m_protocolConfig.tgLink; + } + case Roles::TmeLinkRole: { + return m_protocolConfig.tmeLink; + } + case Roles::IsEnabledRole: { + return m_protocolConfig.isEnabled; + } + case Roles::PublicHostRole: { + return m_protocolConfig.publicHost.isEmpty() ? m_fullConfig.value(QString(configKey::hostName)).toString() + : m_protocolConfig.publicHost; + } + case Roles::TransportModeRole: { + return m_protocolConfig.transportMode.isEmpty() ? QString::fromUtf8( + protocols::telemt::transportModeStandard) + : m_protocolConfig.transportMode; + } + case Roles::TlsDomainRole: { + return m_protocolConfig.tlsDomain; + } + case Roles::AdditionalSecretsRole: { + return m_protocolConfig.additionalSecrets; + } + case Roles::WorkersModeRole: { + return m_protocolConfig.workersMode.isEmpty() ? QString::fromUtf8(protocols::telemt::workersModeAuto) + : m_protocolConfig.workersMode; + } + case Roles::WorkersRole: { + return m_protocolConfig.workers.isEmpty() ? QString::fromUtf8(protocols::telemt::defaultWorkers) + : m_protocolConfig.workers; + } + case Roles::NatEnabledRole: { + return m_protocolConfig.natEnabled; + } + case Roles::NatInternalIpRole: { + return m_protocolConfig.natInternalIp; + } + case Roles::NatExternalIpRole: { + return m_protocolConfig.natExternalIp; + } + case Roles::MaskEnabledRole: { + return m_protocolConfig.maskEnabled; + } + case Roles::UseMiddleProxyRole: { + return m_protocolConfig.useMiddleProxy; + } + case Roles::TlsEmulationRole: { + return m_protocolConfig.tlsEmulation; + } + case Roles::UserNameRole: { + return m_protocolConfig.userName.isEmpty() ? QString::fromUtf8(protocols::telemt::defaultUserName) + : m_protocolConfig.userName; + } + } + + return QVariant(); +} + +void TelemtConfigModel::updateModel(DockerContainer container, const TelemtProtocolConfig &protocolConfig) { + beginResetModel(); + m_container = container; + m_protocolConfig = protocolConfig; + applyDefaults(m_protocolConfig); + endResetModel(); +} + +void TelemtConfigModel::updateModel(const QJsonObject &config) { + beginResetModel(); + + m_fullConfig = config; + m_protocolConfig = TelemtProtocolConfig::fromJson(config.value(QString(configKey::telemt)).toObject()); + applyDefaults(m_protocolConfig); + + endResetModel(); +} + +QJsonObject TelemtConfigModel::getConfig() { + m_fullConfig.insert(QString(configKey::telemt), m_protocolConfig.toJson()); + return m_fullConfig; +} + +TelemtProtocolConfig TelemtConfigModel::getProtocolConfig() { + return m_protocolConfig; +} + +void TelemtConfigModel::generateSecret() { + QString secret; + for (int i = 0; i < 16; ++i) { + quint32 byte = QRandomGenerator::global()->bounded(256); + secret += QString("%1").arg(byte, 2, 16, QChar('0')); + } + + m_protocolConfig.secret = secret; + emit dataChanged(index(0), index(0), QList{SecretRole}); +} + +void TelemtConfigModel::setSecret(const QString &secret) { + if (secret.isEmpty()) { + return; + } + setData(index(0), secret, SecretRole); +} + +bool TelemtConfigModel::validateAndSetSecret(const QString &rawSecret) { + if (!QRegularExpression(QStringLiteral("^[0-9a-fA-F]{32}$")).match(rawSecret).hasMatch()) { + return false; + } + setData(index(0), rawSecret, SecretRole); + return true; +} + +void TelemtConfigModel::setPort(const QString &port) { + setData(index(0), port, PortRole); +} + +void TelemtConfigModel::setTag(const QString &tag) { + setData(index(0), tag, TagRole); +} + +void TelemtConfigModel::setPublicHost(const QString &host) { + setData(index(0), host, PublicHostRole); +} + +void TelemtConfigModel::setTransportMode(const QString &mode) { + setData(index(0), mode, TransportModeRole); +} + +QString TelemtConfigModel::getTransportMode() const { + return m_protocolConfig.transportMode.isEmpty() ? QString::fromUtf8(protocols::telemt::transportModeStandard) + : m_protocolConfig.transportMode; +} + +QString TelemtConfigModel::getTlsDomain() const { + return m_protocolConfig.tlsDomain.isEmpty() ? QString::fromUtf8(protocols::telemt::defaultTlsDomain) + : m_protocolConfig.tlsDomain; +} + +QString TelemtConfigModel::getPublicHost() const { + return m_protocolConfig.publicHost; +} + +void TelemtConfigModel::setTlsDomain(const QString &domain) { + setData(index(0), domain, TlsDomainRole); +} + +void TelemtConfigModel::setWorkersMode(const QString &mode) { + setData(index(0), mode, WorkersModeRole); +} + +void TelemtConfigModel::setWorkers(const QString &workers) { + setData(index(0), workers, WorkersRole); +} + +void TelemtConfigModel::setNatEnabled(bool enabled) { + setData(index(0), enabled, NatEnabledRole); +} + +void TelemtConfigModel::setNatInternalIp(const QString &ip) { + setData(index(0), ip, NatInternalIpRole); +} + +void TelemtConfigModel::setNatExternalIp(const QString &ip) { + setData(index(0), ip, NatExternalIpRole); +} + +void TelemtConfigModel::setMaskEnabled(bool enabled) { + setData(index(0), enabled, MaskEnabledRole); +} + +void TelemtConfigModel::setUseMiddleProxy(bool enabled) { + setData(index(0), enabled, UseMiddleProxyRole); +} + +void TelemtConfigModel::setTlsEmulation(bool enabled) { + setData(index(0), enabled, TlsEmulationRole); +} + +void TelemtConfigModel::setUserName(const QString &name) { + setData(index(0), name, UserNameRole); +} + +void TelemtConfigModel::addAdditionalSecret() { + QString newSecret; + for (int i = 0; i < 16; ++i) { + quint32 byte = QRandomGenerator::global()->bounded(256); + newSecret += QString("%1").arg(byte, 2, 16, QChar('0')); + } + + m_protocolConfig.additionalSecrets.append(newSecret); + emit dataChanged(index(0), index(0), QList{AdditionalSecretsRole}); +} + +void TelemtConfigModel::removeAdditionalSecret(int idx) { + if (idx < 0 || idx >= m_protocolConfig.additionalSecrets.size()) { + return; + } + m_protocolConfig.additionalSecrets.removeAt(idx); + emit dataChanged(index(0), index(0), QList{AdditionalSecretsRole}); +} + +void TelemtConfigModel::setEnabled(bool enabled) { + m_protocolConfig.isEnabled = enabled; + emit dataChanged(index(0), index(0), QList{IsEnabledRole}); +} + +QString TelemtConfigModel::generateQrCode(const QString &text) { + if (text.isEmpty()) { + return ""; + } + auto qr = qrCodeUtils::generateQrCode(text.toUtf8()); + return qrCodeUtils::svgToBase64(QString::fromStdString(toSvgString(qr, 1))); +} + +QString TelemtConfigModel::defaultTlsDomain() const { + return QString::fromUtf8(protocols::telemt::defaultTlsDomain); +} + +QString TelemtConfigModel::defaultPort() const { + return QString::fromUtf8(protocols::telemt::defaultPort); +} + +QString TelemtConfigModel::defaultWorkers() const { + return QString::fromUtf8(protocols::telemt::defaultWorkers); +} + +int TelemtConfigModel::maxWorkers() const { + return protocols::telemt::maxWorkers; +} + +QString TelemtConfigModel::transportModeStandard() const { + return QString::fromUtf8(protocols::telemt::transportModeStandard); +} + +QString TelemtConfigModel::transportModeFakeTLS() const { + return QString::fromUtf8(protocols::telemt::transportModeFakeTLS); +} + +QString TelemtConfigModel::workersModeAuto() const { + return QString::fromUtf8(protocols::telemt::workersModeAuto); +} + +QString TelemtConfigModel::workersModeManual() const { + return QString::fromUtf8(protocols::telemt::workersModeManual); +} + +QHash TelemtConfigModel::roleNames() const { + QHash roles; + + roles[PortRole] = "port"; + roles[SecretRole] = "secret"; + roles[TagRole] = "tag"; + roles[TgLinkRole] = "tgLink"; + roles[TmeLinkRole] = "tmeLink"; + roles[IsEnabledRole] = "isEnabled"; + roles[PublicHostRole] = "publicHost"; + roles[TransportModeRole] = "transportMode"; + roles[TlsDomainRole] = "tlsDomain"; + roles[AdditionalSecretsRole] = "additionalSecrets"; + roles[WorkersModeRole] = "workersMode"; + roles[WorkersRole] = "workers"; + roles[NatEnabledRole] = "natEnabled"; + roles[NatInternalIpRole] = "natInternalIp"; + roles[NatExternalIpRole] = "natExternalIp"; + roles[MaskEnabledRole] = "maskEnabled"; + roles[UseMiddleProxyRole] = "useMiddleProxy"; + roles[TlsEmulationRole] = "tlsEmulation"; + roles[UserNameRole] = "userName"; + + return roles; +} diff --git a/client/ui/models/services/telemtConfigModel.h b/client/ui/models/services/telemtConfigModel.h new file mode 100644 index 000000000..c386d210e --- /dev/null +++ b/client/ui/models/services/telemtConfigModel.h @@ -0,0 +1,130 @@ +#ifndef TELEMTCONFIGMODEL_H +#define TELEMTCONFIGMODEL_H + +#include +#include +#include + +#include "core/models/protocols/telemtProtocolConfig.h" +#include "core/utils/containerEnum.h" + +class TelemtConfigModel : public QAbstractListModel { +Q_OBJECT + +public: + enum Roles { + PortRole = Qt::UserRole + 1, + SecretRole, + TagRole, + TgLinkRole, + TmeLinkRole, + IsEnabledRole, + PublicHostRole, + TransportModeRole, + TlsDomainRole, + AdditionalSecretsRole, + WorkersModeRole, + WorkersRole, + NatEnabledRole, + NatInternalIpRole, + NatExternalIpRole, + MaskEnabledRole, + UseMiddleProxyRole, + TlsEmulationRole, + UserNameRole + }; + + explicit TelemtConfigModel(QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + +public slots: + + void updateModel(amnezia::DockerContainer container, const amnezia::TelemtProtocolConfig &protocolConfig); + + void updateModel(const QJsonObject &config); + + QJsonObject getConfig(); + + amnezia::TelemtProtocolConfig getProtocolConfig(); + + Q_INVOKABLE void generateSecret(); + + Q_INVOKABLE void setSecret(const QString &secret); + + Q_INVOKABLE bool validateAndSetSecret(const QString &rawSecret); + + Q_INVOKABLE void setPort(const QString &port); + + Q_INVOKABLE void setTag(const QString &tag); + + Q_INVOKABLE void setPublicHost(const QString &host); + + Q_INVOKABLE void setTransportMode(const QString &mode); + + Q_INVOKABLE QString getTransportMode() const; + + Q_INVOKABLE QString getTlsDomain() const; + + Q_INVOKABLE QString getPublicHost() const; + + Q_INVOKABLE void setTlsDomain(const QString &domain); + + Q_INVOKABLE void setWorkersMode(const QString &mode); + + Q_INVOKABLE void setWorkers(const QString &workers); + + Q_INVOKABLE void setNatEnabled(bool enabled); + + Q_INVOKABLE void setNatInternalIp(const QString &ip); + + Q_INVOKABLE void setNatExternalIp(const QString &ip); + + Q_INVOKABLE void addAdditionalSecret(); + + Q_INVOKABLE void removeAdditionalSecret(int idx); + + Q_INVOKABLE QString generateQrCode(const QString &text); + + Q_INVOKABLE void setEnabled(bool enabled); + + Q_INVOKABLE void setMaskEnabled(bool enabled); + + Q_INVOKABLE void setUseMiddleProxy(bool enabled); + + Q_INVOKABLE void setTlsEmulation(bool enabled); + + Q_INVOKABLE void setUserName(const QString &name); + + Q_INVOKABLE QString defaultTlsDomain() const; + + Q_INVOKABLE QString defaultPort() const; + + Q_INVOKABLE QString defaultWorkers() const; + + Q_INVOKABLE int maxWorkers() const; + + Q_INVOKABLE QString transportModeStandard() const; + + Q_INVOKABLE QString transportModeFakeTLS() const; + + Q_INVOKABLE QString workersModeAuto() const; + + Q_INVOKABLE QString workersModeManual() const; + +protected: + QHash roleNames() const override; + +private: + static void applyDefaults(amnezia::TelemtProtocolConfig &c); + + amnezia::DockerContainer m_container = amnezia::DockerContainer::None; + QJsonObject m_fullConfig; + amnezia::TelemtProtocolConfig m_protocolConfig; +}; + +#endif // TELEMTCONFIGMODEL_H diff --git a/client/ui/qml/Components/SettingsContainersListView.qml b/client/ui/qml/Components/SettingsContainersListView.qml index e560a2c8d..9e51ae8d8 100644 --- a/client/ui/qml/Components/SettingsContainersListView.qml +++ b/client/ui/qml/Components/SettingsContainersListView.qml @@ -48,6 +48,9 @@ ListViewType { } else if (isMtProxy) { MtProxyConfigModel.updateModel(config) PageController.goToPage(PageEnum.PageServiceMtProxySettings) + } else if (isTelemt) { + TelemtConfigModel.updateModel(config) + PageController.goToPage(PageEnum.PageServiceTelemtSettings) } else { InstallController.updateProtocols(ServersUiController.getServerId(ServersUiController.processedServerIndex), containerIndex) PageController.goToPage(PageEnum.PageSettingsServerProtocol) diff --git a/client/ui/qml/Pages2/PageServiceTelemtSettings.qml b/client/ui/qml/Pages2/PageServiceTelemtSettings.qml new file mode 100644 index 000000000..320300f98 --- /dev/null +++ b/client/ui/qml/Pages2/PageServiceTelemtSettings.qml @@ -0,0 +1,1447 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import SortFilterProxyModel 0.2 + +import PageEnum 1.0 +import ContainerProps 1.0 +import ProtocolEnum 1.0 +import Style 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" +import "../Components" + +PageType { + id: root + + Rectangle { + anchors.fill: parent + z: -1 + color: AmneziaStyle.color.onyxBlack + } + + property int containerStatus: 1 + property bool isUpdating: false + property bool isCheckingStatus: false + property bool previousEnabled: true + property int previousContainerStatus: 1 + + property string previousPort: "" + property string previousTag: "" + property string previousPublicHost: "" + property string previousTransportMode: TelemtConfigModel.transportModeStandard() + property string previousTlsDomain: TelemtConfigModel.defaultTlsDomain() + property string previousWorkersMode: TelemtConfigModel.workersModeAuto() + property string previousWorkers: TelemtConfigModel.defaultWorkers() + property bool previousNatEnabled: false + property string previousNatInternalIp: "" + property string previousNatExternalIp: "" + + property string savedTransportMode: "" + property string savedTlsDomain: "" + property string savedPublicHost: "" + + onSavedTransportModeChanged: { + if (savedTransportMode === "faketls") { + root.syncedSecretTabIndex = 2 + } else if (savedTransportMode !== "") { + root.syncedSecretTabIndex = 0 + } + } + + property bool diagLoading: false + property int syncedSecretTabIndex: 0 + property bool pendingEnableAfterRestart: false + property bool pendingUpdateAfterEnable: false + property bool diagPortReachable: false + property bool diagTelegramReachable: false + property int diagClientsConnected: -1 + property string diagLastConfigRefresh: "" + property string diagStatsEndpoint: "" + + readonly property bool telemtNetworkBlocked: !NetworkReachabilityController.hasInternetAccess + readonly property bool navigationBlockedWhileBusy: isUpdating || diagLoading + + // Defer SSH/updateContainer so QML control handlers return before nested event loops run. + function telemtScheduleUpdate(closePage) { + var cp = closePage === undefined ? false : closePage + Qt.callLater(function () { + InstallController.updateContainer(ServersUiController.getServerId(ServersUiController.processedServerIndex), ServersUiController.processedContainerIndex, ProtocolEnum.Telemt, cp) + }) + } + + function statusText() { + if (isCheckingStatus) { + return qsTr("Checking...") + } + if (isUpdating) { + return qsTr("Updating") + } + switch (containerStatus) { + case 0: { + return qsTr("Not deployed") + } + case 1: { + return qsTr("Running") + } + case 2: { + return qsTr("Stopped") + } + case 3: { + return qsTr("Error") + } + default: { + return qsTr("Unknown") + } + } + } + + Component.onCompleted: { + root.savedTransportMode = TelemtConfigModel.getTransportMode() + root.savedTlsDomain = TelemtConfigModel.getTlsDomain() + root.savedPublicHost = TelemtConfigModel.getPublicHost() + + if (!NetworkReachabilityController.hasInternetAccess) { + isCheckingStatus = false + return + } + isCheckingStatus = true + InstallController.refreshContainerStatus(ServersUiController.getServerId(ServersUiController.processedServerIndex), ServersUiController.processedContainerIndex) + } + + onNavigationBlockedWhileBusyChanged: { + if (root.visible) { + PageController.disableControls(navigationBlockedWhileBusy) + } + } + + onVisibleChanged: { + if (!visible) { + PageController.disableControls(false) + diagLoading = false + } else { + PageController.disableControls(navigationBlockedWhileBusy) + } + } + + Connections { + target: NetworkReachabilityController + + function onHasInternetAccessChanged() { + if (!root.visible) { + return + } + if (NetworkReachabilityController.hasInternetAccess) { + isCheckingStatus = true + InstallController.refreshContainerStatus(ServersUiController.getServerId(ServersUiController.processedServerIndex), ServersUiController.processedContainerIndex) + } + } + } + + Connections { + target: InstallController + + function onUpdateContainerFinished(message, closePage) { + if (!root.visible) { + isUpdating = false + isCheckingStatus = false + return + } + isUpdating = false + containerStatus = 1 + root.savedTransportMode = TelemtConfigModel.getTransportMode() + root.savedTlsDomain = TelemtConfigModel.getTlsDomain() + root.savedPublicHost = TelemtConfigModel.getPublicHost() + PageController.showNotificationMessage(message) + if (closePage) { + PageController.closePage() + } + } + + function onInstallationErrorOccurred() { + if (!root.visible) { + isUpdating = false + isCheckingStatus = false + return + } + isUpdating = false + containerStatus = previousContainerStatus + TelemtConfigModel.setEnabled(previousEnabled) + TelemtConfigModel.setPort(previousPort) + TelemtConfigModel.setTag(previousTag) + TelemtConfigModel.setPublicHost(previousPublicHost) + TelemtConfigModel.setTransportMode(previousTransportMode) + TelemtConfigModel.setTlsDomain(previousTlsDomain) + TelemtConfigModel.setWorkersMode(previousWorkersMode) + TelemtConfigModel.setWorkers(previousWorkers) + TelemtConfigModel.setNatEnabled(previousNatEnabled) + TelemtConfigModel.setNatInternalIp(previousNatInternalIp) + TelemtConfigModel.setNatExternalIp(previousNatExternalIp) + } + + function onSetContainerEnabledFinished(enabled) { + if (!root.visible) { + isUpdating = false + return + } + if (enabled && pendingUpdateAfterEnable) { + pendingUpdateAfterEnable = false + root.telemtScheduleUpdate(false) + return + } + isUpdating = false + containerStatus = enabled ? 1 : 2 + PageController.showNotificationMessage( + enabled ? qsTr("Telemt started") : qsTr("Telemt stopped")) + } + + function onContainerStatusRefreshed(status) { + if (!root.visible) { + isCheckingStatus = false + return + } + isCheckingStatus = false + containerStatus = status + + root.savedTransportMode = TelemtConfigModel.getTransportMode() + root.savedTlsDomain = TelemtConfigModel.getTlsDomain() + root.savedPublicHost = TelemtConfigModel.getPublicHost() + if (status === 1) { + TelemtConfigModel.setEnabled(true) + InstallController.fetchContainerSecret(ServersUiController.getServerId(ServersUiController.processedServerIndex), ServersUiController.processedContainerIndex) + } else if (status === 2) { + TelemtConfigModel.setEnabled(false) + } + } + + function onContainerDiagnosticsRefreshed(portReachable, upstreamReachable, clientsConnected, lastConfigRefresh, statsEndpoint) { + if (!root.visible) { + return + } + diagLoading = false + diagPortReachable = portReachable + diagTelegramReachable = upstreamReachable + diagClientsConnected = clientsConnected + diagLastConfigRefresh = lastConfigRefresh + diagStatsEndpoint = statsEndpoint + } + + function onContainerSecretFetched(secret) { + if (!root.visible) { + return + } + TelemtConfigModel.validateAndSetSecret(secret) + } + } + + BackButtonType { + id: backButton + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 20 + SettingsController.safeAreaTopMargin + onFocusChanged: { + if (this.activeFocus) connectionListView.positionViewAtBeginning() + } + } + + ColumnLayout { + id: pageHeader + anchors.top: backButton.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 8 + spacing: 0 + + BaseHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + headerText: qsTr("Telemt settings") + } + + LabelWithButtonType { + Layout.fillWidth: true + Layout.leftMargin: 0 + Layout.rightMargin: 16 + text: qsTr("Read more about this settings") + textColor: AmneziaStyle.color.goldenApricot + clickedFunction: function () { + Qt.openUrlExternally("https://github.com/telemt/telemt") + } + } + + TabBar { + id: mainTabBar + Layout.fillWidth: true + Layout.topMargin: 4 + + background: Rectangle { + color: AmneziaStyle.color.transparent + Rectangle { + width: parent.width + height: 1 + anchors.bottom: parent.bottom + color: AmneziaStyle.color.slateGray + } + } + + TabButtonType { + text: qsTr("Connection") + isSelected: mainTabBar.currentIndex === 0 + } + TabButtonType { + text: qsTr("Settings") + isSelected: mainTabBar.currentIndex === 1 + } + } + } + + StackLayout { + id: tabContent + anchors.top: pageHeader.bottom + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + currentIndex: mainTabBar.currentIndex + + ListViewType { + id: connectionListView + model: TelemtConfigModel + + delegate: ColumnLayout { + width: connectionListView.width + spacing: 0 + + function domainToHex(domain) { + var hex = "" + for (var i = 0; i < domain.length; i++) { + var code = domain.charCodeAt(i).toString(16) + hex += (code.length < 2 ? "0" : "") + code + } + return hex + } + + function secretForMode(mode) { + if (mode === "faketls") { + var domain = root.savedTlsDomain !== "" ? root.savedTlsDomain : TelemtConfigModel.defaultTlsDomain() + return "ee" + secret + domainToHex(domain) + } else if (mode === "padded") { + return "dd" + secret + } + // Telemt default (secure MTProto, not FakeTLS): Telegram proxy links require dd + hex secret + return "dd" + secret + } + + property int secretTabIndex: root.syncedSecretTabIndex + + function activeSecret() { + if (root.syncedSecretTabIndex === 0) { + return secretForMode("standard") + } + if (root.syncedSecretTabIndex === 1) { + return secretForMode("padded") + } + return secretForMode("faketls") + } + + function effectiveSecret() { + return activeSecret() + } + + function effectiveHost() { + return root.savedPublicHost !== "" ? root.savedPublicHost : ServersModel.getProcessedServerData("hostName") + } + + function tmeLink() { + return "https://t.me/proxy?server=" + effectiveHost() + "&port=" + port + "&secret=" + activeSecret() + } + + function tgLink() { + return "tg://proxy?server=" + effectiveHost() + "&port=" + port + "&secret=" + activeSecret() + } + + CaptionTextType { + Layout.fillWidth: true + Layout.topMargin: 24 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + text: qsTr("Use Telegram connection link") + color: AmneziaStyle.color.mutedGray + } + + Rectangle { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + implicitHeight: linkRow.implicitHeight + 16 + color: AmneziaStyle.color.onyxBlack + radius: 8 + border.color: AmneziaStyle.color.slateGray + border.width: 1 + + RowLayout { + id: linkRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 + anchors.rightMargin: 8 + spacing: 4 + + CaptionTextType { + Layout.fillWidth: true + text: secret !== "" ? tmeLink() : qsTr("Deploy Telemt first") + color: secret !== "" ? AmneziaStyle.color.goldenApricot : AmneziaStyle.color.mutedGray + elide: Text.ElideRight + maximumLineCount: 1 + font.pixelSize: 13 + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/qr-code.svg" + imageColor: AmneziaStyle.color.paleGray + visible: secret !== "" + onClicked: { + ExportController.generateQrFromString(tmeLink()) + PageController.goToShareConnectionPage( + qsTr("Telegram connection link"), + qsTr("Telemt connection link"), + "", "", "") + } + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + visible: secret !== "" + onClicked: { + GC.copyToClipBoard(tmeLink()) + PageController.showNotificationMessage(qsTr("Copied")) + } + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + implicitHeight: tgLinkRow.implicitHeight + 16 + color: AmneziaStyle.color.onyxBlack + radius: 8 + border.color: AmneziaStyle.color.slateGray + border.width: 1 + visible: secret !== "" + + RowLayout { + id: tgLinkRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 + anchors.rightMargin: 8 + spacing: 4 + + CaptionTextType { + Layout.fillWidth: true + text: tgLink() + color: AmneziaStyle.color.goldenApricot + elide: Text.ElideRight + maximumLineCount: 1 + font.pixelSize: 13 + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/qr-code.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: { + ExportController.generateQrFromString(tgLink()) + PageController.goToShareConnectionPage( + qsTr("Telegram connection link"), + qsTr("Telemt connection link"), + "", "", "") + } + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: { + GC.copyToClipBoard(tgLink()) + PageController.showNotificationMessage(qsTr("Copied")) + } + } + } + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + spacing: 4 + + CaptionTextType { + text: qsTr("Or enter the proxy details manually.") + color: AmneziaStyle.color.mutedGray + } + + CaptionTextType { + Layout.fillWidth: true + text: qsTr("How to do it") + color: AmneziaStyle.color.goldenApricot + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: Qt.openUrlExternally("https://core.telegram.org/proxy") + } + } + + Item { + Layout.fillWidth: true + } + } + + Rectangle { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 32 + implicitHeight: manualCol.implicitHeight + 8 + color: AmneziaStyle.color.onyxBlack + radius: 8 + border.color: AmneziaStyle.color.slateGray + border.width: 1 + + ColumnLayout { + id: manualCol + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.topMargin: 8 + spacing: 0 + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 12 + Layout.rightMargin: 8 + Layout.bottomMargin: 8 + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + CaptionTextType { + text: qsTr("Host") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + } + CaptionTextType { + Layout.fillWidth: true + text: effectiveHost() + color: AmneziaStyle.color.paleGray + elide: Text.ElideRight + } + } + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: { GC.copyToClipBoard(effectiveHost()) + PageController.showNotificationMessage(qsTr("Copied")) } + } + } + + DividerType { + Layout.fillWidth: true + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 12 + Layout.rightMargin: 8 + Layout.topMargin: 8 + Layout.bottomMargin: 8 + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + CaptionTextType { + text: qsTr("Port") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + } + CaptionTextType { + Layout.fillWidth: true + text: port + color: AmneziaStyle.color.paleGray + } + } + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: { GC.copyToClipBoard(port) + PageController.showNotificationMessage(qsTr("Copied")) } + } + } + + DividerType { + Layout.fillWidth: true + } + + ButtonGroup { + id: secretTabGroup + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 12 + Layout.rightMargin: 8 + Layout.topMargin: 4 + Layout.bottomMargin: 8 + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + CaptionTextType { + text: qsTr("Secret") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + } + CaptionTextType { + Layout.fillWidth: true + text: activeSecret() + color: AmneziaStyle.color.paleGray + wrapMode: Text.WrapAnywhere + font.pixelSize: 13 + } + } + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: { GC.copyToClipBoard(activeSecret()) + PageController.showNotificationMessage(qsTr("Copied")) } + } + } + } + } + + LabelWithButtonType { + id: removeButton + Layout.fillWidth: true + Layout.bottomMargin: 24 + Layout.leftMargin: 0 + Layout.rightMargin: 16 + visible: ServersModel.isProcessedServerHasWriteAccess() + text: qsTr("Delete Telemt") + textColor: AmneziaStyle.color.vibrantRed + clickedFunction: function () { + var headerText = qsTr("Remove %1 from server?").arg(ContainersModel.getProcessedContainerName()) + var descriptionText = qsTr("The proxy will be stopped and all users will lose access.") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + var yesButtonFunction = function () { + PageController.goToPage(PageEnum.PageDeinstalling) + InstallController.removeContainer(ServersUiController.getServerId(ServersUiController.processedServerIndex), ServersUiController.processedContainerIndex) + } + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, function () { + }) + } + MouseArea { + anchors.fill: removeButton + cursorShape: Qt.PointingHandCursor + enabled: false + } + } + } + } + + ListViewType { + id: settingsListView + model: TelemtConfigModel + reuseItems: false + + delegate: ColumnLayout { + width: settingsListView.width + spacing: 0 + + SwitcherType { + id: enableTelemtSwitch + Layout.fillWidth: true + Layout.topMargin: 24 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + text: qsTr("Enable Telemt") + checked: isEnabled + enabled: !isCheckingStatus && containerStatus !== 0 && containerStatus !== 3 && !isUpdating + onToggled: function () { + if (checked !== isEnabled) { + previousEnabled = isEnabled + previousContainerStatus = containerStatus + isEnabled = checked + isUpdating = true + if (checked) { + root.pendingUpdateAfterEnable = true + InstallController.setContainerEnabled(ServersUiController.getServerId(ServersUiController.processedServerIndex), ServersUiController.processedContainerIndex, true) + } else { + InstallController.setContainerEnabled(ServersUiController.getServerId(ServersUiController.processedServerIndex), ServersUiController.processedContainerIndex, false) + } + } + } + } + + ColumnLayout { + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 * 2 + spacing: 4 + + CaptionTextType { + text: qsTr("Base secret") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + } + + RowLayout { + Layout.fillWidth: true + spacing: 8 + + CaptionTextType { + Layout.fillWidth: true + text: secret !== "" ? secret : qsTr("Not generated") + color: secret !== "" ? AmneziaStyle.color.paleGray : AmneziaStyle.color.mutedGray + elide: Text.ElideMiddle + font.pixelSize: 14 + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/refresh-cw.svg" + imageColor: AmneziaStyle.color.paleGray + visible: ServersModel.isProcessedServerHasWriteAccess() + onClicked: { + showQuestionDrawer( + qsTr("Generate new secret?"), + qsTr("All existing connection links will stop working. Users will need new links."), + qsTr("Generate"), + qsTr("Cancel"), + function () { + if (containerStatus === 1) { + isUpdating = true + TelemtConfigModel.generateSecret() + root.telemtScheduleUpdate(false) + } else { + TelemtConfigModel.generateSecret() + PageController.showNotificationMessage(qsTr("New secret saved. It will be applied when Telemt is started.")) + } + }, + function () { + } + ) + } + } + } + } + + TextFieldWithHeaderType { + id: publicHostTextField + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 4 + headerText: qsTr("Public host / IP") + textField.placeholderText: ServersModel.getProcessedServerData("hostName") + textField.text: publicHost + textField.onEditingFinished: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + if (textField.text !== publicHost) { + publicHost = textField.text + TelemtConfigModel.setPublicHost(publicHost) + } + } + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 4 + visible: publicHostTextField.textField.text === "" + text: qsTr("Leave empty to use server IP automatically") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + wrapMode: Text.WordWrap + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 12 + visible: publicHostTextField.textField.text !== "" && + publicHostTextField.textField.text !== ServersModel.getProcessedServerData("hostName") + text: qsTr("⚠ This overrides the server IP in connection links. Make sure this host/domain points to your server.") + color: AmneziaStyle.color.goldenApricot + font.pixelSize: 12 + wrapMode: Text.WordWrap + } + + TextFieldWithHeaderType { + id: portTextField + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + headerText: qsTr("Server port") + textField.placeholderText: TelemtConfigModel.defaultPort() + textField.maximumLength: 5 + textField.validator: IntValidator { + bottom: 1 + top: 65535 + } + Component.onCompleted: { + var savedPort = port + textField.text = (savedPort === TelemtConfigModel.defaultPort()) ? "" : savedPort + } + textField.onEditingFinished: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + var portValue = textField.text === "" ? TelemtConfigModel.defaultPort() : textField.text + if (portValue !== port) { + port = portValue + TelemtConfigModel.setPort(port) + } + } + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 12 + visible: transportMode === "faketls" && portTextField.textField.text !== "443" && portTextField.textField.text !== "" + text: qsTr("FakeTLS may not work on ports other than 443") + color: AmneziaStyle.color.goldenApricot + font.pixelSize: 12 + wrapMode: Text.WordWrap + } + + TextFieldWithHeaderType { + id: tagTextField + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + headerText: qsTr("Promoted channel tag (optional)") + textField.placeholderText: qsTr("leave empty if not needed") + textField.text: tag + textField.maximumLength: 64 + textField.onEditingFinished: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + if (textField.text !== tag) { + tag = textField.text + TelemtConfigModel.setTag(tag) + } + } + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + spacing: 4 + + CaptionTextType { + text: qsTr("Get a tag from") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + } + CaptionTextType { + text: "@MTProxyBot" + color: AmneziaStyle.color.goldenApricot + font.pixelSize: 12 + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: Qt.openUrlExternally("https://t.me/MTProxyBot") + } + } + } + + DropDownType { + id: transportModeDropDown + Layout.fillWidth: true + Layout.topMargin: 16 * 2 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + + drawerParent: root + drawerHeight: 0.35 + descriptionText: qsTr("Transport mode") + text: transportMode === "faketls" ? qsTr("FakeTLS") : qsTr("Standard MTProto") + + listView: Component { + ListViewType { + model: [qsTr("Standard MTProto"), qsTr("FakeTLS")] + delegate: LabelWithButtonType { + Layout.fillWidth: true + text: modelData + rightImageSource: { + var isCurrent = (index === 0 && transportMode === "standard") || + (index === 1 && transportMode === "faketls") + return isCurrent ? "qrc:/images/controls/check.svg" : "" + } + rightImageColor: AmneziaStyle.color.goldenApricot + clickedFunction: function () { + transportMode = (index === 0) ? "standard" : "faketls" + TelemtConfigModel.setTransportMode(transportMode) + transportModeDropDown.closeTriggered() + } + } + } + } + } + + TextFieldWithHeaderType { + id: tlsDomainTextField + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + visible: transportMode === "faketls" + headerText: qsTr("FakeTLS domain") + textField.placeholderText: root.previousTlsDomain + Component.onCompleted: { + var savedDomain = tlsDomain + textField.text = (savedDomain === TelemtConfigModel.defaultTlsDomain() || savedDomain === "") ? "" : savedDomain + } + textField.onEditingFinished: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + var domainValue = textField.text === "" ? TelemtConfigModel.defaultTlsDomain() : textField.text + if (domainValue !== tlsDomain) { + tlsDomain = domainValue + TelemtConfigModel.setTlsDomain(tlsDomain) + } + } + } + + ColumnLayout { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + spacing: 4 + visible: transportMode === "faketls" + + CaptionTextType { + Layout.fillWidth: true + text: qsTr("The domain is encoded into the FakeTLS client secret (ee + base_secret + hex(domain)). It must support HTTPS / TLS 1.3.") + color: AmneziaStyle.color.mutedGray + wrapMode: Text.WordWrap + font.pixelSize: 12 + } + CaptionTextType { + Layout.fillWidth: true + text: qsTr("\u26a0 Changing the domain will invalidate all previously issued FakeTLS connection links.") + color: AmneziaStyle.color.goldenApricot + wrapMode: Text.WordWrap + font.pixelSize: 12 + } + } + + LabelWithButtonType { + id: advancedHeader + Layout.fillWidth: true + Layout.leftMargin: 0 + Layout.rightMargin: 16 + property bool expanded: false + text: qsTr("Advanced") + rightImageSource: expanded + ? "qrc:/images/controls/chevron-up.svg" + : "qrc:/images/controls/chevron-down.svg" + rightImageColor: AmneziaStyle.color.mutedGray + clickedFunction: function () { + expanded = !expanded + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + visible: advancedHeader.expanded + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + Layout.bottomMargin: 4 + text: qsTr("Additional secrets") + color: AmneziaStyle.color.mutedGray + } + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + text: qsTr("Add extra secrets to allow gradual migration without disconnecting existing users.") + color: AmneziaStyle.color.charcoalGray + wrapMode: Text.WordWrap + font.pixelSize: 12 + } + + Repeater { + model: additionalSecrets + delegate: RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 4 + spacing: 8 + CaptionTextType { + Layout.fillWidth: true + text: modelData + color: AmneziaStyle.color.paleGray + elide: Text.ElideMiddle + font.pixelSize: 13 + } + ImageButtonType { + implicitWidth: 32 + implicitHeight: 32 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.mutedGray + onClicked: { GC.copyToClipBoard(modelData) + PageController.showNotificationMessage(qsTr("Copied")) } + } + ImageButtonType { + implicitWidth: 32 + implicitHeight: 32 + hoverEnabled: true + image: "qrc:/images/controls/trash.svg" + imageColor: AmneziaStyle.color.vibrantRed + onClicked: { + TelemtConfigModel.removeAdditionalSecret(index) + root.telemtScheduleUpdate(false) + } + } + } + } + + BasicButtonType { + Layout.fillWidth: true + Layout.topMargin: 8 + + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + text: qsTr("Add additional secret") + clickedFunc: function () { + TelemtConfigModel.addAdditionalSecret() + root.telemtScheduleUpdate(false) + } + } + + DividerType { + Layout.fillWidth: true + Layout.bottomMargin: 8 + } + + LabelTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.bottomMargin: 4 + text: qsTr("Worker mode") + } + + ButtonGroup { + id: workerModeGroup + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 4 + spacing: 0 + visible: transportMode !== "faketls" + + HorizontalRadioButton { + Layout.fillWidth: true + text: qsTr("Auto") + ButtonGroup.group: workerModeGroup + checked: workersMode === "auto" + onClicked: { workersMode = "auto"; TelemtConfigModel.setWorkersMode("auto") } + } + HorizontalRadioButton { + Layout.fillWidth: true + text: qsTr("Manual") + ButtonGroup.group: workerModeGroup + checked: workersMode === "manual" + onClicked: { workersMode = "manual"; TelemtConfigModel.setWorkersMode("manual") } + } + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + visible: transportMode === "faketls" + text: qsTr("Workers are set to 0 automatically for FakeTLS mode.") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + wrapMode: Text.WordWrap + } + + TextFieldWithHeaderType { + id: workersTextField + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + visible: workersMode === "manual" && transportMode !== "faketls" + headerText: qsTr("Workers count") + textField.placeholderText: "2" + textField.text: workers + textField.maximumLength: 3 + textField.validator: IntValidator { + bottom: 1 + top: TelemtConfigModel.maxWorkers() + } + textField.onEditingFinished: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + if (textField.text !== workers) { + workers = textField.text + TelemtConfigModel.setWorkers(workers) + } + } + } + + DividerType { + Layout.fillWidth: true + Layout.bottomMargin: 8 + } + + SwitcherType { + Layout.fillWidth: true + Layout.rightMargin: 16 + Layout.leftMargin: 16 + Layout.bottomMargin: 4 + text: qsTr("Server is behind NAT / Docker bridge") + descriptionText: qsTr("Enable if your server is not directly accessible from the internet, e.g. Docker or private network") + checked: natEnabled + onToggled: function () { + if (checked !== natEnabled) { + natEnabled = checked + TelemtConfigModel.setNatEnabled(natEnabled) + } + } + } + + TextFieldWithHeaderType { + id: natInternalIpTextField + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + visible: natEnabled + headerText: qsTr("Internal IP") + textField.placeholderText: "172.17.0.2" + textField.text: natInternalIp + textField.onEditingFinished: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + if (textField.text !== natInternalIp) { + natInternalIp = textField.text + TelemtConfigModel.setNatInternalIp(natInternalIp) + } + } + } + + TextFieldWithHeaderType { + id: natExternalIpTextField + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + visible: natEnabled + headerText: qsTr("External IP") + textField.placeholderText: "1.2.3.4" + textField.text: natExternalIp + textField.onEditingFinished: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + if (textField.text !== natExternalIp) { + natExternalIp = textField.text + TelemtConfigModel.setNatExternalIp(natExternalIp) + } + } + } + } + + DividerType { + Layout.fillWidth: true + Layout.topMargin: 8 + } + + ColumnLayout { + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + spacing: 8 + visible: containerStatus === 1 + + RowLayout { + Layout.fillWidth: true + + Header2Type { + Layout.fillWidth: true + headerText: qsTr("Diagnostics") + } + + ImageButtonType { + implicitWidth: 32 + implicitHeight: 32 + image: "qrc:/images/controls/refresh-cw.svg" + imageColor: diagLoading ? AmneziaStyle.color.mutedGray : AmneziaStyle.color.paleGray + hoverEnabled: !diagLoading + enabled: !diagLoading + onClicked: { + diagLoading = true + InstallController.refreshContainerDiagnostics(ServersUiController.getServerId(ServersUiController.processedServerIndex), ServersUiController.processedContainerIndex, parseInt(port)) + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 8 + Rectangle { + width: 8 + height: 8 + radius: 4 + color: diagClientsConnected >= 0 ? (diagPortReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray + } + CaptionTextType { + Layout.fillWidth: true + text: qsTr("Public port reachable") + color: AmneziaStyle.color.paleGray + } + CaptionTextType { + text: diagClientsConnected < 0 ? qsTr("—") : (diagPortReachable ? qsTr("Yes") : qsTr("No")) + color: diagClientsConnected >= 0 ? (diagPortReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 8 + Rectangle { + width: 8 + height: 8 + radius: 4 + color: diagClientsConnected >= 0 ? (diagTelegramReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray + } + CaptionTextType { + Layout.fillWidth: true + text: qsTr("Telegram upstream reachable") + color: AmneziaStyle.color.paleGray + } + CaptionTextType { + text: diagClientsConnected < 0 ? qsTr("—") : (diagTelegramReachable ? qsTr("Yes") : qsTr("No")) + color: diagClientsConnected >= 0 ? (diagTelegramReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 8 + Rectangle { + width: 8 + height: 8 + radius: 4 + color: diagClientsConnected >= 0 ? AmneziaStyle.color.goldenApricot : AmneziaStyle.color.mutedGray + } + CaptionTextType { + Layout.fillWidth: true + text: qsTr("Clients connected") + color: AmneziaStyle.color.paleGray + } + CaptionTextType { + text: diagClientsConnected < 0 ? qsTr("—") : diagClientsConnected.toString() + color: AmneziaStyle.color.paleGray + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 8 + Rectangle { + width: 8 + height: 8 + radius: 4 + color: AmneziaStyle.color.mutedGray + } + CaptionTextType { + Layout.fillWidth: true + text: qsTr("Last config refresh") + color: AmneziaStyle.color.paleGray + } + CaptionTextType { + text: diagLastConfigRefresh !== "" ? diagLastConfigRefresh : qsTr("—") + color: AmneziaStyle.color.mutedGray + } + } + + LabelWithButtonType { + Layout.fillWidth: true + Layout.leftMargin: -16 + visible: diagStatsEndpoint !== "" + text: qsTr("Stats endpoint") + descriptionText: diagStatsEndpoint + descriptionOnTop: true + rightImageSource: "qrc:/images/controls/copy.svg" + rightImageColor: AmneziaStyle.color.paleGray + clickedFunction: function () { + GC.copyToClipBoard(diagStatsEndpoint) + PageController.showNotificationMessage(qsTr("Copied")) + } + } + + CaptionTextType { + Layout.fillWidth: true + text: diagLoading ? qsTr("Refreshing…") : qsTr("Tap ↻ to refresh diagnostics") + color: AmneziaStyle.color.mutedGray + visible: diagClientsConnected < 0 + } + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 16 * 2 + Layout.bottomMargin: 24 + text: qsTr("If you change the settings, the proxy connection link will change. The old link will stop working.") + color: AmneziaStyle.color.mutedGray + wrapMode: Text.WordWrap + font.pixelSize: 12 + } + + BasicButtonType { + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.bottomMargin: 32 + Layout.rightMargin: 16 + Layout.leftMargin: 16 + visible: ServersModel.isProcessedServerHasWriteAccess() + text: qsTr("Save") + clickedFunc: function () { + var portValue = portTextField.textField.text === "" + ? TelemtConfigModel.defaultPort() + : portTextField.textField.text + if (!portTextField.textField.acceptableInput && portTextField.textField.text !== "") { + portTextField.errorText = qsTr("The port must be in the range of 1 to 65535") + return + } + TelemtConfigModel.setPort(portValue) + TelemtConfigModel.setTag(tagTextField.textField.text) + TelemtConfigModel.setPublicHost(publicHostTextField.textField.text) + TelemtConfigModel.setTransportMode(transportMode) + var domainValue = tlsDomainTextField.textField.text === "" + ? TelemtConfigModel.defaultTlsDomain() + : tlsDomainTextField.textField.text + TelemtConfigModel.setTlsDomain(domainValue) + + if (transportMode === "faketls") { + workers = "0" + TelemtConfigModel.setWorkers("0") + } else { + TelemtConfigModel.setWorkersMode(workersMode) + TelemtConfigModel.setWorkers(workers) + } + TelemtConfigModel.setNatEnabled(natEnabled) + TelemtConfigModel.setNatInternalIp(natInternalIpTextField.textField.text) + TelemtConfigModel.setNatExternalIp(natExternalIpTextField.textField.text) + + previousPort = port + previousTag = tag + previousPublicHost = publicHost + previousTransportMode = transportMode + previousTlsDomain = tlsDomain + previousWorkersMode = workersMode + previousWorkers = workers + previousNatEnabled = natEnabled + previousNatInternalIp = natInternalIp + previousNatExternalIp = natExternalIp + isUpdating = true + root.telemtScheduleUpdate(false) + } + } + } + } + } + + Rectangle { + anchors.fill: parent + visible: isCheckingStatus || isUpdating || root.telemtNetworkBlocked + color: AmneziaStyle.color.midnightBlack + opacity: 0.6 + z: 1 + MouseArea { + anchors.fill: parent + } + BusyIndicator { + anchors.centerIn: parent + visible: isCheckingStatus || isUpdating + running: isCheckingStatus || isUpdating + width: 48 + height: 48 + } + CaptionTextType { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 24 + anchors.rightMargin: 24 + visible: root.telemtNetworkBlocked && !isCheckingStatus && !isUpdating + horizontalAlignment: Text.AlignHCenter + text: qsTr("No internet connection. Connect to the internet to change Telemt settings.") + color: AmneziaStyle.color.paleGray + wrapMode: Text.WordWrap + font.pixelSize: 14 + } + } +} diff --git a/client/ui/qml/qml.qrc b/client/ui/qml/qml.qrc index dd4a5041b..64e60c201 100644 --- a/client/ui/qml/qml.qrc +++ b/client/ui/qml/qml.qrc @@ -79,6 +79,7 @@ Pages2/PageProtocolXraySettings.qml Pages2/PageServiceDnsSettings.qml Pages2/PageServiceMtProxySettings.qml + Pages2/PageServiceTelemtSettings.qml Pages2/PageServiceSftpSettings.qml Pages2/PageServiceSocksProxySettings.qml Pages2/PageServiceTorWebsiteSettings.qml From fb5666057b9e77b566bf6a3a1f971a6558fb1c6f Mon Sep 17 00:00:00 2001 From: yp Date: Mon, 18 May 2026 17:35:01 +0300 Subject: [PATCH 14/14] feat: add extended vless configuration (#2566) * update UI XRay, add new page PageProtocolXrayTransportSettings.qml PageProtocolXrayXmuxSettings.qml PageProtocolXrayXPaddingSettings.qml * add UI PageProtocolXrayConfigsSettings, PageProtocolXrayFlowSettings, PageProtocolXraySecuritySettings * add Xray-specific keys * add vars xray model * add new qml padding, update model * update model and export * rename file & update name class & update list xray * fixed ui * add save file in temp * remove debug macros * fixed build windows * fix path Windows * remove save config * fixed changes * fixed conf * fixed UI * fixed size & button save * fixed build iOS * fix: fixed headers base control --------- Co-authored-by: vkamn --- .../core/configurators/xrayConfigurator.cpp | 465 +++++++++-- client/core/configurators/xrayConfigurator.h | 9 + client/core/controllers/coreController.cpp | 3 + client/core/controllers/coreController.h | 2 + .../selfhosted/exportController.cpp | 12 + client/core/installers/xrayInstaller.cpp | 259 +++++- .../models/protocols/xrayProtocolConfig.cpp | 301 +++++-- .../models/protocols/xrayProtocolConfig.h | 120 ++- client/core/protocols/xrayProtocol.cpp | 48 +- client/core/protocols/xrayProtocol.h | 3 + .../secureAppSettingsRepository.cpp | 8 + .../secureAppSettingsRepository.h | 3 + client/core/utils/constants/configKeys.h | 70 ++ .../core/utils/constants/protocolConstants.h | 34 + client/platforms/ios/ios_controller.mm | 2 +- client/translations/amneziavpn_ru_RU.ts | 15 + client/ui/controllers/importUiController.cpp | 9 + client/ui/controllers/importUiController.h | 1 + client/ui/controllers/qml/pageController.h | 10 +- .../selfhosted/exportUiController.cpp | 10 + .../selfhosted/exportUiController.h | 1 + .../selfhosted/installUiController.cpp | 3 +- .../ui/models/protocols/xrayConfigModel.cpp | 525 +++++++++++- client/ui/models/protocols/xrayConfigModel.h | 118 ++- .../protocols/xrayConfigSnapshotsModel.cpp | 216 +++++ .../protocols/xrayConfigSnapshotsModel.h | 76 ++ client/ui/qml/Controls2/MinMaxRowType.qml | 61 ++ .../qml/Controls2/TextFieldWithHeaderType.qml | 10 + .../Pages2/PageProtocolXrayFlowSettings.qml | 125 +++ .../PageProtocolXraySecuritySettings.qml | 292 +++++++ .../qml/Pages2/PageProtocolXraySettings.qml | 177 ++-- .../qml/Pages2/PageProtocolXraySnapshots.qml | 291 +++++++ .../PageProtocolXrayTransportSettings.qml | 755 ++++++++++++++++++ .../PageProtocolXrayXPaddingBytesSettings.qml | 108 +++ .../PageProtocolXrayXPaddingSettings.qml | 224 ++++++ .../Pages2/PageProtocolXrayXmuxSettings.qml | 222 +++++ client/ui/qml/qml.qrc | 10 + 37 files changed, 4364 insertions(+), 234 deletions(-) mode change 100755 => 100644 client/core/protocols/xrayProtocol.cpp mode change 100755 => 100644 client/ui/controllers/selfhosted/installUiController.cpp create mode 100644 client/ui/models/protocols/xrayConfigSnapshotsModel.cpp create mode 100644 client/ui/models/protocols/xrayConfigSnapshotsModel.h create mode 100644 client/ui/qml/Controls2/MinMaxRowType.qml create mode 100644 client/ui/qml/Pages2/PageProtocolXrayFlowSettings.qml create mode 100644 client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml create mode 100644 client/ui/qml/Pages2/PageProtocolXraySnapshots.qml create mode 100644 client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml create mode 100644 client/ui/qml/Pages2/PageProtocolXrayXPaddingBytesSettings.qml create mode 100644 client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml create mode 100644 client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml diff --git a/client/core/configurators/xrayConfigurator.cpp b/client/core/configurators/xrayConfigurator.cpp index b525c8991..1d6e2240a 100644 --- a/client/core/configurators/xrayConfigurator.cpp +++ b/client/core/configurators/xrayConfigurator.cpp @@ -20,14 +20,123 @@ #include "core/models/protocols/xrayProtocolConfig.h" namespace { -Logger logger("XrayConfigurator"); -} + Logger logger("XrayConfigurator"); + + QString normalizeXhttpMode(const QString &m) { + const QString t = m.trimmed(); + if (t.isEmpty() || t.compare(QLatin1String("Auto"), Qt::CaseInsensitive) == 0) { + return QStringLiteral("auto"); + } + if (t.compare(QLatin1String("Packet-up"), Qt::CaseInsensitive) == 0) + return QStringLiteral("packet-up"); + if (t.compare(QLatin1String("Stream-up"), Qt::CaseInsensitive) == 0) + return QStringLiteral("stream-up"); + if (t.compare(QLatin1String("Stream-one"), Qt::CaseInsensitive) == 0) + return QStringLiteral("stream-one"); + return t.toLower(); + } + + // Xray-core: empty → path; "None" in UI → omit (core default path) + QString normalizeSessionSeqPlacement(const QString &p) + { + if (p.isEmpty() || p.compare(QLatin1String("None"), Qt::CaseInsensitive) == 0) + return {}; + return p.toLower(); + } + + QString normalizeUplinkDataPlacement(const QString &p) + { + if (p.isEmpty() || p.compare(QLatin1String("Body"), Qt::CaseInsensitive) == 0) + return QStringLiteral("body"); + if (p.compare(QLatin1String("Auto"), Qt::CaseInsensitive) == 0) + return QStringLiteral("auto"); + if (p.compare(QLatin1String("Query"), Qt::CaseInsensitive) == 0) + // "Query" is not valid for uplink payload in splithttp; closest documented mode + return QStringLiteral("header"); + return p.toLower(); + } + + // splithttp: cookie | header | query | queryInHeader (not "body") + QString normalizeXPaddingPlacement(const QString &p) + { + QString t = p.trimmed(); + if (t.isEmpty()) + return QString::fromLatin1(amnezia::protocols::xray::defaultXPaddingPlacement).toLower(); + if (t.compare(QLatin1String("Body"), Qt::CaseInsensitive) == 0) + return QStringLiteral("queryInHeader"); + if (t.contains(QLatin1String("queryInHeader"), Qt::CaseInsensitive) + || t.compare(QLatin1String("Query in header"), Qt::CaseInsensitive) == 0) + return QStringLiteral("queryInHeader"); + return t.toLower(); + } + + // splithttp: repeat-x | tokenish + QString normalizeXPaddingMethod(const QString &m) + { + QString t = m.trimmed(); + if (t.isEmpty() || t.compare(QLatin1String("Repeat-x"), Qt::CaseInsensitive) == 0) + return QStringLiteral("repeat-x"); + if (t.compare(QLatin1String("Tokenish"), Qt::CaseInsensitive) == 0) + return QStringLiteral("tokenish"); + if (t.compare(QLatin1String("Random"), Qt::CaseInsensitive) == 0 + || t.compare(QLatin1String("Zero"), Qt::CaseInsensitive) == 0) + return QStringLiteral("repeat-x"); + return t.toLower(); + } + + void putIntRangeIfAny(QJsonObject &obj, const char *key, QString minV, QString maxV, const char *fallbackMin, + const char *fallbackMax) + { + if (minV.isEmpty() && maxV.isEmpty()) + return; + if (minV.isEmpty()) + minV = QString::fromLatin1(fallbackMin); + if (maxV.isEmpty()) + maxV = QString::fromLatin1(fallbackMax); + QJsonObject r; + r[QStringLiteral("from")] = minV.toInt(); + r[QStringLiteral("to")] = maxV.toInt(); + obj[QString::fromUtf8(key)] = r; + } + + // Desktop applies this in XrayProtocol::start(); iOS/Android pass JSON straight to libxray — same fixes here. + void sanitizeXrayNativeConfig(amnezia::ProtocolConfig &pc) + { + QString c = pc.nativeConfig(); + if (c.isEmpty()) { + return; + } + bool changed = false; + if (c.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) { + c.replace(QLatin1String("Mozilla/5.0"), QString::fromLatin1(amnezia::protocols::xray::defaultFingerprint), + Qt::CaseInsensitive); + changed = true; + } + const QString legacyListen = QString::fromLatin1(amnezia::protocols::xray::defaultLocalAddr); + const QString listenOk = QString::fromLatin1(amnezia::protocols::xray::defaultLocalListenAddr); + if (c.contains(legacyListen)) { + c.replace(legacyListen, listenOk); + changed = true; + } + if (changed) { + pc.setNativeConfig(c); + } + } +} // namespace XrayConfigurator::XrayConfigurator(SshSession* sshSession, QObject *parent) : ConfiguratorBase(sshSession, parent) { } +amnezia::ProtocolConfig XrayConfigurator::processConfigWithLocalSettings(const amnezia::ConnectionSettings &settings, + amnezia::ProtocolConfig protocolConfig) +{ + applyDnsToNativeConfig(settings.dns, protocolConfig); + sanitizeXrayNativeConfig(protocolConfig); + return protocolConfig; +} + QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentials, DockerContainer container, const ContainerConfig &containerConfig, const DnsSettings &dnsSettings, @@ -35,11 +144,19 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia { // Generate new UUID for client QString clientId = QUuid::createUuid().toString(QUuid::WithoutBraces); - + + // Get flow value from settings (default xtls-rprx-vision) + QString flowValue = "xtls-rprx-vision"; + if (const auto *xrayCfg = containerConfig.protocolConfig.as()) { + if (!xrayCfg->serverConfig.flow.isEmpty()) { + flowValue = xrayCfg->serverConfig.flow; + } + } + // Get current server config QString currentConfig = m_sshSession->getTextFileFromContainer( container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode); - + if (errorCode != ErrorCode::NoError) { logger.error() << "Failed to get server config file"; return ""; @@ -54,7 +171,7 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia } QJsonObject serverConfig = doc.object(); - + // Validate server config structure if (!serverConfig.contains(amnezia::protocols::xray::inbounds)) { logger.error() << "Server config missing 'inbounds' field"; @@ -68,7 +185,7 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia errorCode = ErrorCode::InternalError; return ""; } - + QJsonObject inbound = inbounds[0].toObject(); if (!inbound.contains(amnezia::protocols::xray::settings)) { logger.error() << "Inbound missing 'settings' field"; @@ -84,26 +201,29 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia } QJsonArray clients = settings[amnezia::protocols::xray::clients].toArray(); - + // Create configuration for new client QJsonObject clientConfig { {amnezia::protocols::xray::id, clientId}, - {amnezia::protocols::xray::flow, "xtls-rprx-vision"} }; - + clientConfig[amnezia::protocols::xray::id] = clientId; + if (!flowValue.isEmpty()) { + clientConfig[amnezia::protocols::xray::flow] = flowValue; + } + clients.append(clientConfig); - + // Update config settings[amnezia::protocols::xray::clients] = clients; inbound[amnezia::protocols::xray::settings] = settings; inbounds[0] = inbound; serverConfig[amnezia::protocols::xray::inbounds] = inbounds; - + // Save updated config to server QString updatedConfig = QJsonDocument(serverConfig).toJson(); errorCode = m_sshSession->uploadTextFileToContainer( - container, - credentials, + container, + credentials, updatedConfig, amnezia::protocols::xray::serverConfigPath, libssh::ScpOverwriteMode::ScpOverwriteExisting @@ -116,7 +236,7 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia // Restart container QString restartScript = QString("sudo docker restart $CONTAINER_NAME"); errorCode = m_sshSession->runScript( - credentials, + credentials, m_sshSession->replaceVars(restartScript, amnezia::genBaseVars(credentials, container, dnsSettings.primaryDns, dnsSettings.secondaryDns)) ); @@ -128,75 +248,286 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia return clientId; } -ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container, - const ContainerConfig &containerConfig, - const DnsSettings &dnsSettings, - ErrorCode &errorCode) +QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, const QString &clientId) const { - const XrayServerConfig* serverConfig = nullptr; - if (auto* xrayConfig = containerConfig.protocolConfig.as()) { - serverConfig = &xrayConfig->serverConfig; + QJsonObject streamSettings; + const auto &xhttp = srv.xhttp; + const auto &mkcp = srv.mkcp; + namespace px = amnezia::protocols::xray; + + QString networkValue = QStringLiteral("tcp"); + if (srv.transport == QLatin1String("xhttp")) + networkValue = QStringLiteral("xhttp"); + else if (srv.transport == QLatin1String("mkcp")) + networkValue = QStringLiteral("kcp"); + streamSettings[px::network] = networkValue; + + streamSettings[px::security] = srv.security; + + if (srv.security == QLatin1String("tls")) { + QJsonObject tlsSettings; + const QString sniEff = srv.sni.isEmpty() ? QString::fromLatin1(px::defaultSni) : srv.sni; + tlsSettings[px::serverName] = sniEff; + const QString alpnEff = srv.alpn.isEmpty() ? QString::fromLatin1(px::defaultAlpn) : srv.alpn; + QJsonArray alpnArray; + for (const QString &a : alpnEff.split(QLatin1Char(','))) { + const QString t = a.trimmed(); + if (!t.isEmpty()) + alpnArray.append(t); + } + if (!alpnArray.isEmpty()) + tlsSettings[QStringLiteral("alpn")] = alpnArray; + const QString fpEff = srv.fingerprint.isEmpty() ? QString::fromLatin1(px::defaultFingerprint) : srv.fingerprint; + tlsSettings[px::fingerprint] = fpEff; + streamSettings[QStringLiteral("tlsSettings")] = tlsSettings; } - + + if (srv.security == QLatin1String("reality")) { + QJsonObject realSettings; + const QString fpEff = srv.fingerprint.isEmpty() ? QString::fromLatin1(px::defaultFingerprint) : srv.fingerprint; + realSettings[px::fingerprint] = fpEff; + const QString sniEff = srv.sni.isEmpty() ? QString::fromLatin1(px::defaultSni) : srv.sni; + realSettings[px::serverName] = sniEff; + streamSettings[px::realitySettings] = realSettings; + } + + // XHTTP — JSON must match Xray-core SplitHTTPConfig (flat xPadding fields, see transport_internet.go) + if (srv.transport == QLatin1String("xhttp")) { + QJsonObject xo; + const QString hostEff = xhttp.host.isEmpty() ? QString::fromLatin1(px::defaultXhttpHost) : xhttp.host; + xo[QStringLiteral("host")] = hostEff; + if (!xhttp.path.isEmpty()) + xo[QStringLiteral("path")] = xhttp.path; + xo[QStringLiteral("mode")] = normalizeXhttpMode(xhttp.mode); + + if (xhttp.headersTemplate.compare(QLatin1String("HTTP"), Qt::CaseInsensitive) == 0) { + QJsonObject headers; + headers[QStringLiteral("Host")] = hostEff; + xo[QStringLiteral("headers")] = headers; + } + + const QString methodEff = + xhttp.uplinkMethod.isEmpty() ? QString::fromLatin1(px::defaultXhttpUplinkMethod) : xhttp.uplinkMethod; + xo[QStringLiteral("uplinkHTTPMethod")] = methodEff.toUpper(); + + xo[QStringLiteral("noGRPCHeader")] = xhttp.disableGrpc; + xo[QStringLiteral("noSSEHeader")] = xhttp.disableSse; + + const QString sessPl = normalizeSessionSeqPlacement(xhttp.sessionPlacement); + if (!sessPl.isEmpty()) + xo[QStringLiteral("sessionPlacement")] = sessPl; + const QString seqPl = normalizeSessionSeqPlacement(xhttp.seqPlacement); + if (!seqPl.isEmpty()) + xo[QStringLiteral("seqPlacement")] = seqPl; + if (!xhttp.sessionKey.isEmpty()) + xo[QStringLiteral("sessionKey")] = xhttp.sessionKey; + if (!xhttp.seqKey.isEmpty()) + xo[QStringLiteral("seqKey")] = xhttp.seqKey; + + xo[QStringLiteral("uplinkDataPlacement")] = normalizeUplinkDataPlacement(xhttp.uplinkDataPlacement); + if (!xhttp.uplinkDataKey.isEmpty()) + xo[QStringLiteral("uplinkDataKey")] = xhttp.uplinkDataKey; + + const QString ucs = xhttp.uplinkChunkSize.isEmpty() ? QString::fromLatin1(px::defaultXhttpUplinkChunkSize) + : xhttp.uplinkChunkSize; + if (!ucs.isEmpty() && ucs != QLatin1String("0")) { + const int v = ucs.toInt(); + QJsonObject chunkR; + chunkR[QStringLiteral("from")] = v; + chunkR[QStringLiteral("to")] = v; + xo[QStringLiteral("uplinkChunkSize")] = chunkR; + } + + if (!xhttp.scMaxBufferedPosts.isEmpty()) + xo[QStringLiteral("scMaxBufferedPosts")] = xhttp.scMaxBufferedPosts.toLongLong(); + + putIntRangeIfAny(xo, "scMaxEachPostBytes", xhttp.scMaxEachPostBytesMin, xhttp.scMaxEachPostBytesMax, + px::defaultXhttpScMaxEachPostBytesMin, px::defaultXhttpScMaxEachPostBytesMax); + putIntRangeIfAny(xo, "scMinPostsIntervalMs", xhttp.scMinPostsIntervalMsMin, xhttp.scMinPostsIntervalMsMax, + px::defaultXhttpScMinPostsIntervalMsMin, px::defaultXhttpScMinPostsIntervalMsMax); + putIntRangeIfAny(xo, "scStreamUpServerSecs", xhttp.scStreamUpServerSecsMin, xhttp.scStreamUpServerSecsMax, + px::defaultXhttpScStreamUpServerSecsMin, px::defaultXhttpScStreamUpServerSecsMax); + + const auto &pad = xhttp.xPadding; + xo[QStringLiteral("xPaddingObfsMode")] = pad.obfsMode; + if (pad.obfsMode) { + if (!pad.bytesMin.isEmpty() || !pad.bytesMax.isEmpty()) { + QJsonObject br; + br[QStringLiteral("from")] = pad.bytesMin.isEmpty() ? 1 : pad.bytesMin.toInt(); + br[QStringLiteral("to")] = pad.bytesMax.isEmpty() ? (pad.bytesMin.isEmpty() ? 256 : pad.bytesMin.toInt()) + : pad.bytesMax.toInt(); + xo[QStringLiteral("xPaddingBytes")] = br; + } + xo[QStringLiteral("xPaddingKey")] = pad.key.isEmpty() ? QStringLiteral("x_padding") : pad.key; + xo[QStringLiteral("xPaddingHeader")] = pad.header.isEmpty() ? QStringLiteral("X-Padding") : pad.header; + xo[QStringLiteral("xPaddingPlacement")] = normalizeXPaddingPlacement( + pad.placement.isEmpty() ? QString::fromLatin1(px::defaultXPaddingPlacement) : pad.placement); + xo[QStringLiteral("xPaddingMethod")] = normalizeXPaddingMethod( + pad.method.isEmpty() ? QString::fromLatin1(px::defaultXPaddingMethod) : pad.method); + } + + // xmux: Xray has no "enabled" flag; omit object when UI disables multiplex tuning. + if (xhttp.xmux.enabled) { + QJsonObject mux; + auto addMuxRange = [&](const char *key, const QString &a, const QString &b) { + if (a.isEmpty() && b.isEmpty()) + return; + QJsonObject r; + r[QStringLiteral("from")] = a.isEmpty() ? 0 : a.toInt(); + r[QStringLiteral("to")] = b.isEmpty() ? 0 : b.toInt(); + mux[QString::fromUtf8(key)] = r; + }; + addMuxRange("maxConcurrency", xhttp.xmux.maxConcurrencyMin, xhttp.xmux.maxConcurrencyMax); + addMuxRange("maxConnections", xhttp.xmux.maxConnectionsMin, xhttp.xmux.maxConnectionsMax); + addMuxRange("cMaxReuseTimes", xhttp.xmux.cMaxReuseTimesMin, xhttp.xmux.cMaxReuseTimesMax); + addMuxRange("hMaxRequestTimes", xhttp.xmux.hMaxRequestTimesMin, xhttp.xmux.hMaxRequestTimesMax); + addMuxRange("hMaxReusableSecs", xhttp.xmux.hMaxReusableSecsMin, xhttp.xmux.hMaxReusableSecsMax); + if (!xhttp.xmux.hKeepAlivePeriod.isEmpty()) + mux[QStringLiteral("hKeepAlivePeriod")] = xhttp.xmux.hKeepAlivePeriod.toLongLong(); + if (!mux.isEmpty()) + xo[QStringLiteral("xmux")] = mux; + } + + streamSettings[QStringLiteral("xhttpSettings")] = xo; + } + + if (srv.transport == QLatin1String("mkcp")) { + QJsonObject kcpObj; + const QString ttiEff = mkcp.tti.isEmpty() ? QString::fromLatin1(px::defaultMkcpTti) : mkcp.tti; + const QString upEff = mkcp.uplinkCapacity.isEmpty() ? QString::fromLatin1(px::defaultMkcpUplinkCapacity) + : mkcp.uplinkCapacity; + const QString downEff = mkcp.downlinkCapacity.isEmpty() ? QString::fromLatin1(px::defaultMkcpDownlinkCapacity) + : mkcp.downlinkCapacity; + const QString rbufEff = mkcp.readBufferSize.isEmpty() ? QString::fromLatin1(px::defaultMkcpReadBufferSize) + : mkcp.readBufferSize; + const QString wbufEff = mkcp.writeBufferSize.isEmpty() ? QString::fromLatin1(px::defaultMkcpWriteBufferSize) + : mkcp.writeBufferSize; + kcpObj[QStringLiteral("tti")] = ttiEff.toInt(); + kcpObj[QStringLiteral("uplinkCapacity")] = upEff.toInt(); + kcpObj[QStringLiteral("downlinkCapacity")] = downEff.toInt(); + kcpObj[QStringLiteral("readBufferSize")] = rbufEff.toInt(); + kcpObj[QStringLiteral("writeBufferSize")] = wbufEff.toInt(); + kcpObj[QStringLiteral("congestion")] = mkcp.congestion; + streamSettings[QStringLiteral("kcpSettings")] = kcpObj; + } + + return streamSettings; +} + +ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container, + const ContainerConfig &containerConfig, + const DnsSettings &dnsSettings, + ErrorCode &errorCode) +{ + const XrayServerConfig *serverConfig = nullptr; + if (const auto *xrayCfg = containerConfig.protocolConfig.as()) { + serverConfig = &xrayCfg->serverConfig; + } + + if (!serverConfig) { + logger.error() << "No XrayProtocolConfig found"; + errorCode = ErrorCode::InternalError; + return XrayProtocolConfig{}; + } + + const XrayServerConfig &srv = *serverConfig; + QString xrayClientId = prepareServerConfig(credentials, container, containerConfig, dnsSettings, errorCode); if (errorCode != ErrorCode::NoError || xrayClientId.isEmpty()) { logger.error() << "Failed to prepare server config"; - errorCode = ErrorCode::InternalError; + if (errorCode == ErrorCode::NoError) { + errorCode = ErrorCode::InternalError; + } return XrayProtocolConfig{}; } - amnezia::ScriptVars vars = amnezia::genBaseVars(credentials, container, dnsSettings.primaryDns, dnsSettings.secondaryDns); - vars.append(amnezia::genProtocolVarsForContainer(container, containerConfig)); - QString config = m_sshSession->replaceVars(amnezia::scriptData(ProtocolScriptType::xray_template, container), vars); - - if (config.isEmpty()) { - logger.error() << "Failed to get config template"; - errorCode = ErrorCode::InternalError; - return XrayProtocolConfig{}; + // Fetch server keys (Reality only) + QString xrayPublicKey; + QString xrayShortId; + + if (srv.security == "reality") { + xrayPublicKey = m_sshSession->getTextFileFromContainer(container, credentials, + amnezia::protocols::xray::PublicKeyPath, errorCode); + if (errorCode != ErrorCode::NoError || xrayPublicKey.isEmpty()) { + logger.error() << "Failed to get public key"; + if (errorCode == ErrorCode::NoError) { + errorCode = ErrorCode::InternalError; + } + return XrayProtocolConfig{}; + } + xrayPublicKey.replace("\n", ""); + + xrayShortId = m_sshSession->getTextFileFromContainer(container, credentials, + amnezia::protocols::xray::shortidPath, errorCode); + if (errorCode != ErrorCode::NoError || xrayShortId.isEmpty()) { + logger.error() << "Failed to get short ID"; + if (errorCode == ErrorCode::NoError) { + errorCode = ErrorCode::InternalError; + } + return XrayProtocolConfig{}; + } + xrayShortId.replace("\n", ""); } - QString xrayPublicKey = - m_sshSession->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::PublicKeyPath, errorCode); - if (errorCode != ErrorCode::NoError || xrayPublicKey.isEmpty()) { - logger.error() << "Failed to get public key"; - errorCode = ErrorCode::InternalError; - return XrayProtocolConfig{}; - } - xrayPublicKey.replace("\n", ""); - - QString xrayShortId = - m_sshSession->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::shortidPath, errorCode); - if (errorCode != ErrorCode::NoError || xrayShortId.isEmpty()) { - logger.error() << "Failed to get short ID"; - errorCode = ErrorCode::InternalError; - return XrayProtocolConfig{}; - } - xrayShortId.replace("\n", ""); - - if (!config.contains("$XRAY_CLIENT_ID") || !config.contains("$XRAY_PUBLIC_KEY") || !config.contains("$XRAY_SHORT_ID")) { - logger.error() << "Config template missing required variables:" - << "XRAY_CLIENT_ID:" << !config.contains("$XRAY_CLIENT_ID") - << "XRAY_PUBLIC_KEY:" << !config.contains("$XRAY_PUBLIC_KEY") - << "XRAY_SHORT_ID:" << !config.contains("$XRAY_SHORT_ID"); - errorCode = ErrorCode::InternalError; - return XrayProtocolConfig{}; + // Build outbound + QJsonObject userObj; + userObj[amnezia::protocols::xray::id] = xrayClientId; + userObj[amnezia::protocols::xray::encryption] = "none"; + if (!srv.flow.isEmpty()) { + userObj[amnezia::protocols::xray::flow] = srv.flow; } - config.replace("$XRAY_CLIENT_ID", xrayClientId); - config.replace("$XRAY_PUBLIC_KEY", xrayPublicKey); - config.replace("$XRAY_SHORT_ID", xrayShortId); + QJsonObject vnextEntry; + vnextEntry[amnezia::protocols::xray::address] = credentials.hostName; + vnextEntry[amnezia::protocols::xray::port] = srv.port.toInt(); + vnextEntry[amnezia::protocols::xray::users] = QJsonArray { userObj }; + QJsonObject outboundSettings; + outboundSettings[amnezia::protocols::xray::vnext] = QJsonArray { vnextEntry }; + + QJsonObject outbound; + outbound["protocol"] = "vless"; + outbound[amnezia::protocols::xray::settings] = outboundSettings; + + // Build streamSettings + QJsonObject streamObj = buildStreamSettings(srv, xrayClientId); + + // Inject Reality keys + if (srv.security == "reality") { + QJsonObject rs = streamObj[amnezia::protocols::xray::realitySettings].toObject(); + rs[amnezia::protocols::xray::publicKey] = xrayPublicKey; + rs[amnezia::protocols::xray::shortId] = xrayShortId; + rs[amnezia::protocols::xray::spiderX] = ""; + streamObj[amnezia::protocols::xray::realitySettings] = rs; + } + + outbound[amnezia::protocols::xray::streamSettings] = streamObj; + + // Build full client config + QJsonObject inboundObj; + inboundObj["listen"] = amnezia::protocols::xray::defaultLocalListenAddr; + inboundObj[amnezia::protocols::xray::port] = amnezia::protocols::xray::defaultLocalProxyPort; + inboundObj["protocol"] = "socks"; + inboundObj[amnezia::protocols::xray::settings] = QJsonObject { { "udp", true } }; + + QJsonObject clientJson; + clientJson["log"] = QJsonObject { { "loglevel", "error" } }; + clientJson[amnezia::protocols::xray::inbounds] = QJsonArray { inboundObj }; + clientJson[amnezia::protocols::xray::outbounds] = QJsonArray { outbound }; + + QString config = QString::fromUtf8(QJsonDocument(clientJson).toJson(QJsonDocument::Compact)); + + // Return XrayProtocolConfig protocolConfig; - if (serverConfig) { - protocolConfig.serverConfig = *serverConfig; - } - + protocolConfig.serverConfig = srv; + XrayClientConfig clientConfig; clientConfig.nativeConfig = config; - clientConfig.localPort = ""; + qDebug() << "config:" << config; + clientConfig.localPort = QString(amnezia::protocols::xray::defaultLocalProxyPort); clientConfig.id = xrayClientId; - + protocolConfig.setClientConfig(clientConfig); - + return protocolConfig; -} +} \ No newline at end of file diff --git a/client/core/configurators/xrayConfigurator.h b/client/core/configurators/xrayConfigurator.h index 74a0ea006..968e85b44 100644 --- a/client/core/configurators/xrayConfigurator.h +++ b/client/core/configurators/xrayConfigurator.h @@ -2,11 +2,13 @@ #define XRAY_CONFIGURATOR_H #include +#include #include "configuratorBase.h" #include "core/utils/errorCodes.h" #include "core/utils/routeModes.h" #include "core/utils/commonStructs.h" +#include "core/models/protocols/xrayProtocolConfig.h" class XrayConfigurator : public ConfiguratorBase { @@ -18,10 +20,17 @@ public: const amnezia::DnsSettings &dnsSettings, amnezia::ErrorCode &errorCode) override; + amnezia::ProtocolConfig processConfigWithLocalSettings(const amnezia::ConnectionSettings &settings, + amnezia::ProtocolConfig protocolConfig) override; + private: QString prepareServerConfig(const amnezia::ServerCredentials &credentials, amnezia::DockerContainer container, const amnezia::ContainerConfig &containerConfig, const amnezia::DnsSettings &dnsSettings, amnezia::ErrorCode &errorCode); + + // Builds the native xray "streamSettings" JSON object from XrayServerConfig + QJsonObject buildStreamSettings(const amnezia::XrayServerConfig &srv, + const QString &clientId) const; }; #endif // XRAY_CONFIGURATOR_H diff --git a/client/core/controllers/coreController.cpp b/client/core/controllers/coreController.cpp index d9bf0ea9b..77b951f9e 100644 --- a/client/core/controllers/coreController.cpp +++ b/client/core/controllers/coreController.cpp @@ -86,6 +86,9 @@ void CoreController::initModels() m_xrayConfigModel = new XrayConfigModel(this); setQmlContextProperty("XrayConfigModel", m_xrayConfigModel); + m_xrayConfigSnapshotsModel = new XrayConfigSnapshotsModel(m_appSettingsRepository, m_xrayConfigModel, this); + setQmlContextProperty("XrayConfigSnapshotsModel", m_xrayConfigSnapshotsModel); + m_torConfigModel = new TorConfigModel(this); setQmlContextProperty("TorConfigModel", m_torConfigModel); diff --git a/client/core/controllers/coreController.h b/client/core/controllers/coreController.h index acf347f5d..70033d61b 100644 --- a/client/core/controllers/coreController.h +++ b/client/core/controllers/coreController.h @@ -65,6 +65,7 @@ #include "ui/models/protocols/openvpnConfigModel.h" #include "ui/models/protocols/wireguardConfigModel.h" #include "ui/models/protocols/xrayConfigModel.h" +#include "ui/models/protocols/xrayConfigSnapshotsModel.h" #include "ui/models/protocolsModel.h" #include "ui/models/services/torConfigModel.h" #include "ui/models/serversModel.h" @@ -205,6 +206,7 @@ private: OpenVpnConfigModel* m_openVpnConfigModel; XrayConfigModel* m_xrayConfigModel; + XrayConfigSnapshotsModel* m_xrayConfigSnapshotsModel; TorConfigModel* m_torConfigModel; WireGuardConfigModel* m_wireGuardConfigModel; AwgConfigModel* m_awgConfigModel; diff --git a/client/core/controllers/selfhosted/exportController.cpp b/client/core/controllers/selfhosted/exportController.cpp index 095b57353..75faccf20 100644 --- a/client/core/controllers/selfhosted/exportController.cpp +++ b/client/core/controllers/selfhosted/exportController.cpp @@ -323,6 +323,18 @@ ExportController::ExportResult ExportController::generateXrayConfig(const QStrin vlessServer.shortId = realitySettings.value(amnezia::protocols::xray::shortId).toString(); vlessServer.fingerprint = realitySettings.value(amnezia::protocols::xray::fingerprint).toString("chrome"); vlessServer.spiderX = realitySettings.value(amnezia::protocols::xray::spiderX).toString(""); + } else if (vlessServer.security == "tls") { + QJsonObject tlsSettings = streamSettings.value("tlsSettings").toObject(); + vlessServer.serverName = tlsSettings.value(amnezia::protocols::xray::serverName).toString(); + vlessServer.fingerprint = tlsSettings.value(amnezia::protocols::xray::fingerprint).toString(); + // alpn: serialize array back to comma-separated for VLESS URI + QJsonArray alpnArr = tlsSettings.value("alpn").toArray(); + QStringList alpnList; + for (const QJsonValue &v : alpnArr) { + alpnList << v.toString(); + } + // alpn goes into vless URI query param — handled by Serialize via serverName/alpn fields + // VlessServerObject doesn't have alpn field, so we embed in serverName if needed } result.nativeConfigString = amnezia::serialization::vless::Serialize(vlessServer, "AmneziaVPN"); diff --git a/client/core/installers/xrayInstaller.cpp b/client/core/installers/xrayInstaller.cpp index 12a4b9833..30e61cc2a 100644 --- a/client/core/installers/xrayInstaller.cpp +++ b/client/core/installers/xrayInstaller.cpp @@ -14,8 +14,18 @@ #include "core/models/protocols/xrayProtocolConfig.h" #include "logger.h" -namespace { +namespace +{ Logger logger("XrayInstaller"); + + // Xray expects uTLS preset names (chrome, firefox, …). Old Amnezia/server templates used "Mozilla/5.0". + QString normalizeXrayFingerprint(const QString &fp) + { + if (fp.isEmpty() || fp.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) { + return QString::fromLatin1(protocols::xray::defaultFingerprint); + } + return fp; + } } using namespace amnezia; @@ -63,18 +73,251 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c } QJsonObject streamSettings = inbound[protocols::xray::streamSettings].toObject(); - QJsonObject realitySettings = streamSettings[protocols::xray::realitySettings].toObject(); - if (!realitySettings.contains(protocols::xray::serverNames)) { - logger.error() << "Settings missing 'serverNames' field"; + auto *xrayConfig = config.getXrayProtocolConfig(); + if (!xrayConfig) { + logger.error() << "No XrayProtocolConfig in ContainerConfig"; return ErrorCode::InternalError; } - QString siteName = realitySettings[protocols::xray::serverNames][0].toString(); + XrayServerConfig &srv = xrayConfig->serverConfig; - if (auto* xrayConfig = config.getXrayProtocolConfig()) { - xrayConfig->serverConfig.site = siteName; + // ── Port ───────────────────────────────────────────────────────── + if (inbound.contains(protocols::xray::port)) { + srv.port = QString::number(inbound[protocols::xray::port].toInt()); } - + + // ── Network (transport) ─────────────────────────────────────────── + QString networkVal = streamSettings.value(protocols::xray::network).toString("tcp"); + if (networkVal == "xhttp") { + srv.transport = "xhttp"; + } else if (networkVal == "kcp") { + srv.transport = "mkcp"; + } else { + srv.transport = "raw"; + } + + // ── Security ────────────────────────────────────────────────────── + srv.security = streamSettings.value(protocols::xray::security).toString("reality"); + + // ── Reality settings ────────────────────────────────────────────── + if (srv.security == "reality") { + QJsonObject rs = streamSettings.value(protocols::xray::realitySettings).toObject(); + + // serverNames array → site + sni + if (rs.contains(protocols::xray::serverNames)) { + QString sniVal = rs[protocols::xray::serverNames].toArray().first().toString(); + srv.sni = sniVal; + srv.site = sniVal; + } else if (rs.contains(protocols::xray::serverName)) { + srv.sni = rs[protocols::xray::serverName].toString(); + srv.site = srv.sni; + } + + srv.fingerprint = normalizeXrayFingerprint(rs.value(protocols::xray::fingerprint).toString()); + } + + // ── TLS settings ────────────────────────────────────────────────── + if (srv.security == "tls") { + QJsonObject tls = streamSettings.value("tlsSettings").toObject(); + srv.sni = tls.value(protocols::xray::serverName).toString(); + srv.fingerprint = normalizeXrayFingerprint(tls.value(protocols::xray::fingerprint).toString()); + + QJsonArray alpnArr = tls.value("alpn").toArray(); + QStringList alpnList; + for (const QJsonValue &v : alpnArr) { + alpnList << v.toString(); + } + srv.alpn = alpnList.join(","); + } + + // ── Flow (from users array) ─────────────────────────────────────── + if (inbound.contains(protocols::xray::settings)) { + QJsonObject s = inbound[protocols::xray::settings].toObject(); + QJsonArray clientsArr = s.value(protocols::xray::clients).toArray(); + if (!clientsArr.isEmpty()) { + srv.flow = clientsArr[0].toObject().value(protocols::xray::flow).toString(); + } + } + + // ── XHTTP settings (Xray-core SplitHTTPConfig + legacy Amnezia keys) ── + if (srv.transport == "xhttp") { + QJsonObject xhttpObj = streamSettings.value("xhttpSettings").toObject(); + { + const QString m = xhttpObj.value("mode").toString(); + if (m.isEmpty() || m == QLatin1String("auto")) + srv.xhttp.mode = QStringLiteral("Auto"); + else if (m == QLatin1String("packet-up")) + srv.xhttp.mode = QStringLiteral("Packet-up"); + else if (m == QLatin1String("stream-up")) + srv.xhttp.mode = QStringLiteral("Stream-up"); + else if (m == QLatin1String("stream-one")) + srv.xhttp.mode = QStringLiteral("Stream-one"); + else + srv.xhttp.mode = m; + } + + srv.xhttp.host = xhttpObj.value("host").toString(); + srv.xhttp.path = xhttpObj.value("path").toString(); + + { + const QJsonObject hdrs = xhttpObj.value("headers").toObject(); + if (hdrs.contains(QLatin1String("Host")) || !hdrs.isEmpty()) + srv.xhttp.headersTemplate = QStringLiteral("HTTP"); + } + + if (xhttpObj.contains(QLatin1String("uplinkHTTPMethod"))) + srv.xhttp.uplinkMethod = xhttpObj.value("uplinkHTTPMethod").toString(); + else + srv.xhttp.uplinkMethod = xhttpObj.value("method").toString(); + + srv.xhttp.disableGrpc = xhttpObj.value("noGRPCHeader").toBool(true); + srv.xhttp.disableSse = xhttpObj.value("noSSEHeader").toBool(true); + + auto sessionSeqUi = [](const QString &core) -> QString { + if (core.isEmpty() || core == QLatin1String("path")) + return QStringLiteral("Path"); + if (core == QLatin1String("cookie")) + return QStringLiteral("Cookie"); + if (core == QLatin1String("header")) + return QStringLiteral("Header"); + if (core == QLatin1String("query")) + return QStringLiteral("Query"); + return core; + }; + QString sess = xhttpObj.value("sessionPlacement").toString(); + if (sess.isEmpty()) + sess = xhttpObj.value("scSessionPlacement").toString(); + srv.xhttp.sessionPlacement = sessionSeqUi(sess); + + QString seq = xhttpObj.value("seqPlacement").toString(); + if (seq.isEmpty()) + seq = xhttpObj.value("scSeqPlacement").toString(); + srv.xhttp.seqPlacement = sessionSeqUi(seq); + + auto uplinkDataUi = [](const QString &core) -> QString { + if (core.isEmpty() || core == QLatin1String("body")) + return QStringLiteral("Body"); + if (core == QLatin1String("auto")) + return QStringLiteral("Auto"); + if (core == QLatin1String("header")) + return QStringLiteral("Header"); + if (core == QLatin1String("cookie")) + return QStringLiteral("Cookie"); + return core; + }; + QString udata = xhttpObj.value("uplinkDataPlacement").toString(); + if (udata.isEmpty()) + udata = xhttpObj.value("scUplinkDataPlacement").toString(); + srv.xhttp.uplinkDataPlacement = uplinkDataUi(udata); + + srv.xhttp.sessionKey = xhttpObj.value("sessionKey").toString(); + srv.xhttp.seqKey = xhttpObj.value("seqKey").toString(); + srv.xhttp.uplinkDataKey = xhttpObj.value("uplinkDataKey").toString(); + + if (xhttpObj.contains(QLatin1String("uplinkChunkSize"))) { + QJsonObject uc = xhttpObj.value("uplinkChunkSize").toObject(); + if (!uc.isEmpty()) + srv.xhttp.uplinkChunkSize = QString::number(uc.value("from").toInt()); + } else if (xhttpObj.contains(QLatin1String("xhttpUplinkChunkSize"))) { + srv.xhttp.uplinkChunkSize = QString::number(xhttpObj.value("xhttpUplinkChunkSize").toInt()); + } + if (xhttpObj.contains(QLatin1String("scMaxBufferedPosts"))) { + srv.xhttp.scMaxBufferedPosts = QString::number(xhttpObj.value("scMaxBufferedPosts").toVariant().toLongLong()); + } + + auto readRange = [&](const char *key, QString &minOut, QString &maxOut) { + QJsonObject r = xhttpObj.value(QLatin1String(key)).toObject(); + if (!r.isEmpty()) { + minOut = QString::number(r.value("from").toInt()); + maxOut = QString::number(r.value("to").toInt()); + } + }; + readRange("scMaxEachPostBytes", srv.xhttp.scMaxEachPostBytesMin, srv.xhttp.scMaxEachPostBytesMax); + readRange("scMinPostsIntervalMs", srv.xhttp.scMinPostsIntervalMsMin, srv.xhttp.scMinPostsIntervalMsMax); + readRange("scStreamUpServerSecs", srv.xhttp.scStreamUpServerSecsMin, srv.xhttp.scStreamUpServerSecsMax); + + auto loadPaddingFromObject = [&](const QJsonObject &pad) { + if (pad.contains(QLatin1String("xPaddingObfsMode"))) + srv.xhttp.xPadding.obfsMode = pad.value("xPaddingObfsMode").toBool(true); + srv.xhttp.xPadding.key = pad.value("xPaddingKey").toString(); + srv.xhttp.xPadding.header = pad.value("xPaddingHeader").toString(); + srv.xhttp.xPadding.placement = pad.value("xPaddingPlacement").toString(); + srv.xhttp.xPadding.method = pad.value("xPaddingMethod").toString(); + QJsonObject bytesRange = pad.value("xPaddingBytes").toObject(); + if (!bytesRange.isEmpty()) { + srv.xhttp.xPadding.bytesMin = QString::number(bytesRange.value("from").toInt()); + srv.xhttp.xPadding.bytesMax = QString::number(bytesRange.value("to").toInt()); + } + QString pl = srv.xhttp.xPadding.placement.toLower(); + if (pl == QLatin1String("cookie")) + srv.xhttp.xPadding.placement = QStringLiteral("Cookie"); + else if (pl == QLatin1String("header")) + srv.xhttp.xPadding.placement = QStringLiteral("Header"); + else if (pl == QLatin1String("query")) + srv.xhttp.xPadding.placement = QStringLiteral("Query"); + else if (pl == QLatin1String("queryinheader")) + srv.xhttp.xPadding.placement = QStringLiteral("Query in header"); + QString met = srv.xhttp.xPadding.method.toLower(); + if (met == QLatin1String("repeat-x")) + srv.xhttp.xPadding.method = QStringLiteral("Repeat-x"); + else if (met == QLatin1String("tokenish")) + srv.xhttp.xPadding.method = QStringLiteral("Tokenish"); + }; + if (xhttpObj.contains(QLatin1String("xPaddingObfsMode")) || xhttpObj.contains(QLatin1String("xPaddingKey")) + || !xhttpObj.value("xPaddingBytes").toObject().isEmpty()) { + loadPaddingFromObject(xhttpObj); + } else if (xhttpObj.contains(QLatin1String("xPadding")) && xhttpObj.value("xPadding").isObject()) { + const QJsonObject nested = xhttpObj.value("xPadding").toObject(); + if (!nested.isEmpty()) { + loadPaddingFromObject(nested); + if (!nested.contains(QLatin1String("xPaddingObfsMode"))) + srv.xhttp.xPadding.obfsMode = true; + } + } + + if (xhttpObj.contains(QLatin1String("xmux"))) { + QJsonObject mux = xhttpObj.value("xmux").toObject(); + srv.xhttp.xmux.enabled = true; + + auto readMuxRange = [&](const char *key, QString &minOut, QString &maxOut) { + QJsonObject r = mux.value(QLatin1String(key)).toObject(); + if (!r.isEmpty()) { + minOut = QString::number(r.value("from").toInt()); + maxOut = QString::number(r.value("to").toInt()); + } + }; + readMuxRange("maxConcurrency", srv.xhttp.xmux.maxConcurrencyMin, srv.xhttp.xmux.maxConcurrencyMax); + readMuxRange("maxConnections", srv.xhttp.xmux.maxConnectionsMin, srv.xhttp.xmux.maxConnectionsMax); + readMuxRange("cMaxReuseTimes", srv.xhttp.xmux.cMaxReuseTimesMin, srv.xhttp.xmux.cMaxReuseTimesMax); + readMuxRange("hMaxRequestTimes", srv.xhttp.xmux.hMaxRequestTimesMin, srv.xhttp.xmux.hMaxRequestTimesMax); + readMuxRange("hMaxReusableSecs", srv.xhttp.xmux.hMaxReusableSecsMin, srv.xhttp.xmux.hMaxReusableSecsMax); + + if (mux.contains(QLatin1String("hKeepAlivePeriod"))) + srv.xhttp.xmux.hKeepAlivePeriod = QString::number(mux.value("hKeepAlivePeriod").toVariant().toLongLong()); + } + } + + // ── mKCP settings ───────────────────────────────────────────────── + if (srv.transport == "mkcp") { + QJsonObject kcp = streamSettings.value("kcpSettings").toObject(); + if (kcp.contains("tti")) { + srv.mkcp.tti = QString::number(kcp["tti"].toInt()); + } + if (kcp.contains("uplinkCapacity")) { + srv.mkcp.uplinkCapacity = QString::number(kcp["uplinkCapacity"].toInt()); + } + if (kcp.contains("downlinkCapacity")) { + srv.mkcp.downlinkCapacity = QString::number(kcp["downlinkCapacity"].toInt()); + } + if (kcp.contains("readBufferSize")) { + srv.mkcp.readBufferSize = QString::number(kcp["readBufferSize"].toInt()); + } + if (kcp.contains("writeBufferSize")) { + srv.mkcp.writeBufferSize = QString::number(kcp["writeBufferSize"].toInt()); + } + srv.mkcp.congestion = kcp.value("congestion").toBool(true); + } + return ErrorCode::NoError; } diff --git a/client/core/models/protocols/xrayProtocolConfig.cpp b/client/core/models/protocols/xrayProtocolConfig.cpp index bb4e61457..a6c0043bc 100644 --- a/client/core/models/protocols/xrayProtocolConfig.cpp +++ b/client/core/models/protocols/xrayProtocolConfig.cpp @@ -3,20 +3,173 @@ #include #include -#include "../../../core/utils/protocolEnum.h" -#include "../../../core/protocols/protocolUtils.h" -#include "../../../core/utils/constants/configKeys.h" -#include "../../../core/utils/constants/protocolConstants.h" +#include "core/utils/protocolEnum.h" +#include "core/protocols/protocolUtils.h" +#include "core/utils/constants/configKeys.h" +#include "core/utils/constants/protocolConstants.h" using namespace amnezia; using namespace ProtocolUtils; + namespace amnezia { +QJsonObject XrayXPaddingConfig::toJson() const +{ + QJsonObject obj; + if (!bytesMin.isEmpty()) obj[configKey::xPaddingBytesMin] = bytesMin; + if (!bytesMax.isEmpty()) obj[configKey::xPaddingBytesMax] = bytesMax; + obj[configKey::xPaddingObfsMode] = obfsMode; + if (!key.isEmpty()) obj[configKey::xPaddingKey] = key; + if (!header.isEmpty()) obj[configKey::xPaddingHeader] = header; + if (!placement.isEmpty()) obj[configKey::xPaddingPlacement] = placement; + if (!method.isEmpty()) obj[configKey::xPaddingMethod] = method; + return obj; +} + +XrayXPaddingConfig XrayXPaddingConfig::fromJson(const QJsonObject &json) +{ + XrayXPaddingConfig c; + c.bytesMin = json.value(configKey::xPaddingBytesMin).toString(); + c.bytesMax = json.value(configKey::xPaddingBytesMax).toString(); + c.obfsMode = json.value(configKey::xPaddingObfsMode).toBool(true); + c.key = json.value(configKey::xPaddingKey).toString(protocols::xray::defaultSite); + c.header = json.value(configKey::xPaddingHeader).toString(); + c.placement = json.value(configKey::xPaddingPlacement).toString(protocols::xray::defaultXPaddingPlacement); + c.method = json.value(configKey::xPaddingMethod).toString(protocols::xray::defaultXPaddingMethod); + return c; +} + +QJsonObject XrayXmuxConfig::toJson() const +{ + QJsonObject obj; + obj[configKey::xmuxEnabled] = enabled; + if (!maxConcurrencyMin.isEmpty()) obj[configKey::xmuxMaxConcurrencyMin] = maxConcurrencyMin; + if (!maxConcurrencyMax.isEmpty()) obj[configKey::xmuxMaxConcurrencyMax] = maxConcurrencyMax; + if (!maxConnectionsMin.isEmpty()) obj[configKey::xmuxMaxConnectionsMin] = maxConnectionsMin; + if (!maxConnectionsMax.isEmpty()) obj[configKey::xmuxMaxConnectionsMax] = maxConnectionsMax; + if (!cMaxReuseTimesMin.isEmpty()) obj[configKey::xmuxCMaxReuseTimesMin] = cMaxReuseTimesMin; + if (!cMaxReuseTimesMax.isEmpty()) obj[configKey::xmuxCMaxReuseTimesMax] = cMaxReuseTimesMax; + if (!hMaxRequestTimesMin.isEmpty()) obj[configKey::xmuxHMaxRequestTimesMin] = hMaxRequestTimesMin; + if (!hMaxRequestTimesMax.isEmpty()) obj[configKey::xmuxHMaxRequestTimesMax] = hMaxRequestTimesMax; + if (!hMaxReusableSecsMin.isEmpty()) obj[configKey::xmuxHMaxReusableSecsMin] = hMaxReusableSecsMin; + if (!hMaxReusableSecsMax.isEmpty()) obj[configKey::xmuxHMaxReusableSecsMax] = hMaxReusableSecsMax; + if (!hKeepAlivePeriod.isEmpty()) obj[configKey::xmuxHKeepAlivePeriod] = hKeepAlivePeriod; + return obj; +} + +XrayXmuxConfig XrayXmuxConfig::fromJson(const QJsonObject &json) +{ + XrayXmuxConfig c; + c.enabled = json.value(configKey::xmuxEnabled).toBool(true); + c.maxConcurrencyMin = json.value(configKey::xmuxMaxConcurrencyMin).toString("0"); + c.maxConcurrencyMax = json.value(configKey::xmuxMaxConcurrencyMax).toString("0"); + c.maxConnectionsMin = json.value(configKey::xmuxMaxConnectionsMin).toString("0"); + c.maxConnectionsMax = json.value(configKey::xmuxMaxConnectionsMax).toString("0"); + c.cMaxReuseTimesMin = json.value(configKey::xmuxCMaxReuseTimesMin).toString("0"); + c.cMaxReuseTimesMax = json.value(configKey::xmuxCMaxReuseTimesMax).toString("0"); + c.hMaxRequestTimesMin = json.value(configKey::xmuxHMaxRequestTimesMin).toString("0"); + c.hMaxRequestTimesMax = json.value(configKey::xmuxHMaxRequestTimesMax).toString("0"); + c.hMaxReusableSecsMin = json.value(configKey::xmuxHMaxReusableSecsMin).toString("0"); + c.hMaxReusableSecsMax = json.value(configKey::xmuxHMaxReusableSecsMax).toString("0"); + c.hKeepAlivePeriod = json.value(configKey::xmuxHKeepAlivePeriod).toString(); + return c; +} + +QJsonObject XrayXhttpConfig::toJson() const +{ + QJsonObject obj; + if (!mode.isEmpty()) obj[configKey::xhttpMode] = mode; + if (!host.isEmpty()) obj[configKey::xhttpHost] = host; + if (!path.isEmpty()) obj[configKey::xhttpPath] = path; + if (!headersTemplate.isEmpty()) obj[configKey::xhttpHeadersTemplate] = headersTemplate; + if (!uplinkMethod.isEmpty()) obj[configKey::xhttpUplinkMethod] = uplinkMethod; + obj[configKey::xhttpDisableGrpc] = disableGrpc; + obj[configKey::xhttpDisableSse] = disableSse; + + if (!sessionPlacement.isEmpty()) obj[configKey::xhttpSessionPlacement] = sessionPlacement; + if (!sessionKey.isEmpty()) obj[configKey::xhttpSessionKey] = sessionKey; + if (!seqPlacement.isEmpty()) obj[configKey::xhttpSeqPlacement] = seqPlacement; + if (!seqKey.isEmpty()) obj[configKey::xhttpSeqKey] = seqKey; + if (!uplinkDataPlacement.isEmpty()) obj[configKey::xhttpUplinkDataPlacement] = uplinkDataPlacement; + if (!uplinkDataKey.isEmpty()) obj[configKey::xhttpUplinkDataKey] = uplinkDataKey; + + if (!uplinkChunkSize.isEmpty()) obj[configKey::xhttpUplinkChunkSize] = uplinkChunkSize; + if (!scMaxBufferedPosts.isEmpty()) obj[configKey::xhttpScMaxBufferedPosts] = scMaxBufferedPosts; + if (!scMaxEachPostBytesMin.isEmpty()) obj[configKey::xhttpScMaxEachPostBytesMin] = scMaxEachPostBytesMin; + if (!scMaxEachPostBytesMax.isEmpty()) obj[configKey::xhttpScMaxEachPostBytesMax] = scMaxEachPostBytesMax; + if (!scMinPostsIntervalMsMin.isEmpty()) obj[configKey::xhttpScMinPostsIntervalMsMin] = scMinPostsIntervalMsMin; + if (!scMinPostsIntervalMsMax.isEmpty()) obj[configKey::xhttpScMinPostsIntervalMsMax] = scMinPostsIntervalMsMax; + if (!scStreamUpServerSecsMin.isEmpty()) obj[configKey::xhttpScStreamUpServerSecsMin] = scStreamUpServerSecsMin; + if (!scStreamUpServerSecsMax.isEmpty()) obj[configKey::xhttpScStreamUpServerSecsMax] = scStreamUpServerSecsMax; + + obj["xPadding"] = xPadding.toJson(); + obj["xmux"] = xmux.toJson(); + + return obj; +} + +XrayXhttpConfig XrayXhttpConfig::fromJson(const QJsonObject &json) +{ + XrayXhttpConfig c; + c.mode = json.value(configKey::xhttpMode).toString(protocols::xray::defaultXhttpMode); + c.host = json.value(configKey::xhttpHost).toString(protocols::xray::defaultSite); + c.path = json.value(configKey::xhttpPath).toString(); + c.headersTemplate = json.value(configKey::xhttpHeadersTemplate).toString(protocols::xray::defaultXhttpHeadersTemplate); + c.uplinkMethod = json.value(configKey::xhttpUplinkMethod).toString(protocols::xray::defaultXhttpUplinkMethod); + c.disableGrpc = json.value(configKey::xhttpDisableGrpc).toBool(true); + c.disableSse = json.value(configKey::xhttpDisableSse).toBool(true); + + c.sessionPlacement = json.value(configKey::xhttpSessionPlacement).toString(protocols::xray::defaultXhttpSessionPlacement); + c.sessionKey = json.value(configKey::xhttpSessionKey).toString(); + c.seqPlacement = json.value(configKey::xhttpSeqPlacement).toString(protocols::xray::defaultXhttpSessionPlacement); + c.seqKey = json.value(configKey::xhttpSeqKey).toString(); + c.uplinkDataPlacement = json.value(configKey::xhttpUplinkDataPlacement).toString(protocols::xray::defaultXhttpUplinkDataPlacement); + c.uplinkDataKey = json.value(configKey::xhttpUplinkDataKey).toString(); + + c.uplinkChunkSize = json.value(configKey::xhttpUplinkChunkSize).toString("0"); + c.scMaxBufferedPosts = json.value(configKey::xhttpScMaxBufferedPosts).toString(); + c.scMaxEachPostBytesMin = json.value(configKey::xhttpScMaxEachPostBytesMin).toString("1"); + c.scMaxEachPostBytesMax = json.value(configKey::xhttpScMaxEachPostBytesMax).toString("100"); + c.scMinPostsIntervalMsMin = json.value(configKey::xhttpScMinPostsIntervalMsMin).toString("100"); + c.scMinPostsIntervalMsMax = json.value(configKey::xhttpScMinPostsIntervalMsMax).toString("800"); + c.scStreamUpServerSecsMin = json.value(configKey::xhttpScStreamUpServerSecsMin).toString("1"); + c.scStreamUpServerSecsMax = json.value(configKey::xhttpScStreamUpServerSecsMax).toString("100"); + + c.xPadding = XrayXPaddingConfig::fromJson(json.value("xPadding").toObject()); + c.xmux = XrayXmuxConfig::fromJson(json.value("xmux").toObject()); + + return c; +} + +QJsonObject XrayMkcpConfig::toJson() const +{ + QJsonObject obj; + if (!tti.isEmpty()) obj[configKey::mkcpTti] = tti; + if (!uplinkCapacity.isEmpty()) obj[configKey::mkcpUplinkCapacity] = uplinkCapacity; + if (!downlinkCapacity.isEmpty()) obj[configKey::mkcpDownlinkCapacity] = downlinkCapacity; + if (!readBufferSize.isEmpty()) obj[configKey::mkcpReadBufferSize] = readBufferSize; + if (!writeBufferSize.isEmpty()) obj[configKey::mkcpWriteBufferSize] = writeBufferSize; + obj[configKey::mkcpCongestion] = congestion; + return obj; +} + +XrayMkcpConfig XrayMkcpConfig::fromJson(const QJsonObject &json) +{ + XrayMkcpConfig c; + c.tti = json.value(configKey::mkcpTti).toString(); + c.uplinkCapacity = json.value(configKey::mkcpUplinkCapacity).toString(); + c.downlinkCapacity = json.value(configKey::mkcpDownlinkCapacity).toString(); + c.readBufferSize = json.value(configKey::mkcpReadBufferSize).toString(); + c.writeBufferSize = json.value(configKey::mkcpWriteBufferSize).toString(); + c.congestion = json.value(configKey::mkcpCongestion).toBool(true); + return c; +} QJsonObject XrayServerConfig::toJson() const { QJsonObject obj; - + + // Existing fields if (!port.isEmpty()) { obj[configKey::port] = port; } @@ -29,60 +182,96 @@ QJsonObject XrayServerConfig::toJson() const if (!site.isEmpty()) { obj[configKey::site] = site; } - + if (isThirdPartyConfig) { obj[configKey::isThirdPartyConfig] = isThirdPartyConfig; } - + + // New: Security + if (!security.isEmpty()) { + obj[configKey::xraySecurity] = security; + } + if (!flow.isEmpty()) { + obj[configKey::xrayFlow] = flow; + } + if (!fingerprint.isEmpty()) { + obj[configKey::xrayFingerprint] = fingerprint; + } + if (!sni.isEmpty()) { + obj[configKey::xraySni] = sni; + } + if (!alpn.isEmpty()) { + obj[configKey::xrayAlpn] = alpn; + } + + // New: Transport + if (!transport.isEmpty()) { + obj[configKey::xrayTransport] = transport; + } + obj["xhttp"] = xhttp.toJson(); + obj["mkcp"] = mkcp.toJson(); + return obj; } -XrayServerConfig XrayServerConfig::fromJson(const QJsonObject& json) +XrayServerConfig XrayServerConfig::fromJson(const QJsonObject &json) { - XrayServerConfig config; - - config.port = json.value(configKey::port).toString(); - config.transportProto = json.value(configKey::transportProto).toString(); - config.subnetAddress = json.value(configKey::subnetAddress).toString(); - config.site = json.value(configKey::site).toString(); - - config.isThirdPartyConfig = json.value(configKey::isThirdPartyConfig).toBool(false); - - return config; + XrayServerConfig c; + + // Existing fields + c.port = json.value(configKey::port).toString(); + c.transportProto = json.value(configKey::transportProto).toString(); + c.subnetAddress = json.value(configKey::subnetAddress).toString(); + c.site = json.value(configKey::site).toString(); + c.isThirdPartyConfig = json.value(configKey::isThirdPartyConfig).toBool(false); + + // New: Security + c.security = json.value(configKey::xraySecurity).toString(protocols::xray::defaultSecurity); + c.flow = json.value(configKey::xrayFlow).toString(protocols::xray::defaultFlow); + c.fingerprint = json.value(configKey::xrayFingerprint).toString(protocols::xray::defaultFingerprint); + if (c.fingerprint.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) { + c.fingerprint = QString::fromLatin1(protocols::xray::defaultFingerprint); + } + c.sni = json.value(configKey::xraySni).toString(protocols::xray::defaultSni); + c.alpn = json.value(configKey::xrayAlpn).toString(protocols::xray::defaultAlpn); + + // New: Transport + c.transport = json.value(configKey::xrayTransport).toString(protocols::xray::defaultTransport); + c.xhttp = XrayXhttpConfig::fromJson(json.value("xhttp").toObject()); + c.mkcp = XrayMkcpConfig::fromJson(json.value("mkcp").toObject()); + + return c; } -bool XrayServerConfig::hasEqualServerSettings(const XrayServerConfig& other) const +bool XrayServerConfig::hasEqualServerSettings(const XrayServerConfig &other) const { - return port == other.port && site == other.site; + return port == other.port + && site == other.site + && security == other.security + && flow == other.flow + && transport == other.transport + && fingerprint == other.fingerprint + && sni == other.sni; } QJsonObject XrayClientConfig::toJson() const { QJsonObject obj; - - if (!nativeConfig.isEmpty()) { - obj[configKey::config] = nativeConfig; - } - if (!localPort.isEmpty()) { - obj[configKey::localPort] = localPort; - } - if (!id.isEmpty()) { - obj[configKey::clientId] = id; - } - + if (!nativeConfig.isEmpty()) obj[configKey::config] = nativeConfig; + if (!localPort.isEmpty()) obj[configKey::localPort] = localPort; + if (!id.isEmpty()) obj[configKey::clientId] = id; return obj; } -XrayClientConfig XrayClientConfig::fromJson(const QJsonObject& json) +XrayClientConfig XrayClientConfig::fromJson(const QJsonObject &json) { - XrayClientConfig config; - - config.nativeConfig = json.value(configKey::config).toString(); - config.localPort = json.value(configKey::localPort).toString(); - config.id = json.value(configKey::clientId).toString(); - - if (config.id.isEmpty() && !config.nativeConfig.isEmpty()) { - QJsonDocument doc = QJsonDocument::fromJson(config.nativeConfig.toUtf8()); + XrayClientConfig c; + c.nativeConfig = json.value(configKey::config).toString(); + c.localPort = json.value(configKey::localPort).toString(); + c.id = json.value(configKey::clientId).toString(); + + if (c.id.isEmpty() && !c.nativeConfig.isEmpty()) { + QJsonDocument doc = QJsonDocument::fromJson(c.nativeConfig.toUtf8()); if (!doc.isNull() && doc.isObject()) { QJsonObject configObj = doc.object(); if (configObj.contains(protocols::xray::outbounds)) { @@ -100,7 +289,7 @@ XrayClientConfig XrayClientConfig::fromJson(const QJsonObject& json) if (!users.isEmpty()) { QJsonObject user = users[0].toObject(); if (user.contains(protocols::xray::id)) { - config.id = user[protocols::xray::id].toString(); + c.id = user[protocols::xray::id].toString(); } } } @@ -111,16 +300,15 @@ XrayClientConfig XrayClientConfig::fromJson(const QJsonObject& json) } } } - - return config; + + return c; } QJsonObject XrayProtocolConfig::toJson() const { QJsonObject obj = serverConfig.toJson(); - + if (clientConfig.has_value()) { - // Third-party import: nativeConfig is raw Xray JSON (inbounds/outbounds) QJsonDocument doc = QJsonDocument::fromJson(clientConfig->nativeConfig.toUtf8()); if (!doc.isNull() && doc.isObject() && doc.object().contains(protocols::xray::outbounds) && !doc.object().contains(configKey::config)) { @@ -130,22 +318,20 @@ QJsonObject XrayProtocolConfig::toJson() const obj[configKey::lastConfig] = QString::fromUtf8(QJsonDocument(clientJson).toJson(QJsonDocument::Compact)); } } - + return obj; } -XrayProtocolConfig XrayProtocolConfig::fromJson(const QJsonObject& json) +XrayProtocolConfig XrayProtocolConfig::fromJson(const QJsonObject &json) { - XrayProtocolConfig config; - - config.serverConfig = XrayServerConfig::fromJson(json); - + XrayProtocolConfig c; + c.serverConfig = XrayServerConfig::fromJson(json); + QString lastConfigStr = json.value(configKey::lastConfig).toString(); if (!lastConfigStr.isEmpty()) { QJsonDocument doc = QJsonDocument::fromJson(lastConfigStr.toUtf8()); if (doc.isObject()) { QJsonObject parsed = doc.object(); - // Third-party import stores raw Xray config (inbounds/outbounds) directly if (parsed.contains(protocols::xray::outbounds) && !parsed.contains(configKey::config)) { XrayClientConfig clientCfg; clientCfg.nativeConfig = lastConfigStr; @@ -158,14 +344,14 @@ XrayProtocolConfig XrayProtocolConfig::fromJson(const QJsonObject& json) } } } - config.clientConfig = clientCfg; + c.clientConfig = clientCfg; } else { - config.clientConfig = XrayClientConfig::fromJson(parsed); + c.clientConfig = XrayClientConfig::fromJson(parsed); } } } - - return config; + + return c; } bool XrayProtocolConfig::hasClientConfig() const @@ -173,7 +359,7 @@ bool XrayProtocolConfig::hasClientConfig() const return clientConfig.has_value(); } -void XrayProtocolConfig::setClientConfig(const XrayClientConfig& config) +void XrayProtocolConfig::setClientConfig(const XrayClientConfig &config) { clientConfig = config; } @@ -184,4 +370,3 @@ void XrayProtocolConfig::clearClientConfig() } } // namespace amnezia - diff --git a/client/core/models/protocols/xrayProtocolConfig.h b/client/core/models/protocols/xrayProtocolConfig.h index fc52a81b3..eaf9abfd2 100644 --- a/client/core/models/protocols/xrayProtocolConfig.h +++ b/client/core/models/protocols/xrayProtocolConfig.h @@ -2,47 +2,145 @@ #define XRAYPROTOCOLCONFIG_H #include +#include "core/utils/constants/protocolConstants.h" #include #include namespace amnezia { +// ── xPadding ───────────────────────────────────────────────────────────────── +struct XrayXPaddingConfig { + QString bytesMin; // xPaddingBytes min + QString bytesMax; // xPaddingBytes max + bool obfsMode = true; // xPaddingObfsMode + QString key; // xPaddingKey + QString header; // xPaddingHeader + QString placement = protocols::xray::defaultXPaddingPlacement; // xPaddingPlacement: Cookie|Header|Query|Body + QString method = protocols::xray::defaultXPaddingMethod; // xPaddingMethod: Repeat-x|Random|Zero + + QJsonObject toJson() const; + static XrayXPaddingConfig fromJson(const QJsonObject &json); +}; + +// ── xmux ───────────────────────────────────────────────────────────────────── +struct XrayXmuxConfig { + bool enabled = true; + + QString maxConcurrencyMin = "0"; + QString maxConcurrencyMax = "0"; + QString maxConnectionsMin = "0"; + QString maxConnectionsMax = "0"; + QString cMaxReuseTimesMin = "0"; + QString cMaxReuseTimesMax = "0"; + QString hMaxRequestTimesMin = "0"; + QString hMaxRequestTimesMax = "0"; + QString hMaxReusableSecsMin = "0"; + QString hMaxReusableSecsMax = "0"; + QString hKeepAlivePeriod; + + QJsonObject toJson() const; + static XrayXmuxConfig fromJson(const QJsonObject &json); +}; + +// ── XHTTP transport ─────────────────────────────────────────────────────────── +struct XrayXhttpConfig { + QString mode = protocols::xray::defaultXhttpMode; // Auto|Packet-up|Stream-up|Stream-one + QString host = protocols::xray::defaultXhttpHost; + QString path; + QString headersTemplate = protocols::xray::defaultXhttpHeadersTemplate; // HTTP|None + QString uplinkMethod = protocols::xray::defaultXhttpUplinkMethod; // POST|PUT|PATCH + bool disableGrpc = true; + bool disableSse = true; + + // Session & Sequence + QString sessionPlacement = protocols::xray::defaultXhttpSessionPlacement; + QString sessionKey = protocols::xray::defaultXhttpSessionKey; + QString seqPlacement = protocols::xray::defaultXhttpSeqPlacement; + QString seqKey; + QString uplinkDataPlacement = protocols::xray::defaultXhttpUplinkDataPlacement; + QString uplinkDataKey; + + // Traffic Shaping + QString uplinkChunkSize = protocols::xray::defaultXhttpUplinkChunkSize; + QString scMaxBufferedPosts; + QString scMaxEachPostBytesMin = protocols::xray::defaultXhttpScMaxEachPostBytesMin; + QString scMaxEachPostBytesMax = protocols::xray::defaultXhttpScMaxEachPostBytesMax; + QString scMinPostsIntervalMsMin = protocols::xray::defaultXhttpScMinPostsIntervalMsMin; + QString scMinPostsIntervalMsMax = protocols::xray::defaultXhttpScMinPostsIntervalMsMax; + QString scStreamUpServerSecsMin = protocols::xray::defaultXhttpScStreamUpServerSecsMin; + QString scStreamUpServerSecsMax = protocols::xray::defaultXhttpScStreamUpServerSecsMax; + + XrayXPaddingConfig xPadding; + XrayXmuxConfig xmux; + + QJsonObject toJson() const; + static XrayXhttpConfig fromJson(const QJsonObject &json); +}; + +// ── mKCP transport ──────────────────────────────────────────────────────────── +struct XrayMkcpConfig { + QString tti; + QString uplinkCapacity; + QString downlinkCapacity; + QString readBufferSize; + QString writeBufferSize; + bool congestion = true; + + QJsonObject toJson() const; + static XrayMkcpConfig fromJson(const QJsonObject &json); +}; + +// ── Server config (settings editable by user) ───────────────────────────────── struct XrayServerConfig { QString port; QString transportProto; QString subnetAddress; QString site; bool isThirdPartyConfig = false; - + + // New: Security + QString security = protocols::xray::defaultSecurity; + QString flow = protocols::xray::defaultFlow; + QString fingerprint = protocols::xray::defaultFingerprint; + QString sni = protocols::xray::defaultSni; + QString alpn = protocols::xray::defaultAlpn; + + // New: Transport + QString transport = protocols::xray::defaultTransport; + XrayXhttpConfig xhttp; + XrayMkcpConfig mkcp; + QJsonObject toJson() const; - static XrayServerConfig fromJson(const QJsonObject& json); - - bool hasEqualServerSettings(const XrayServerConfig& other) const; + + static XrayServerConfig fromJson(const QJsonObject &json); + + bool hasEqualServerSettings(const XrayServerConfig &other) const; }; +// ── Client config (generated, not edited by user) ───────────────────────────── struct XrayClientConfig { QString nativeConfig; QString localPort; QString id; - + QJsonObject toJson() const; - static XrayClientConfig fromJson(const QJsonObject& json); + static XrayClientConfig fromJson(const QJsonObject &json); }; +// ── Top-level protocol config ────────────────────────────────────────────────── struct XrayProtocolConfig { XrayServerConfig serverConfig; std::optional clientConfig; - + QJsonObject toJson() const; - static XrayProtocolConfig fromJson(const QJsonObject& json); - + static XrayProtocolConfig fromJson(const QJsonObject &json); + bool hasClientConfig() const; - void setClientConfig(const XrayClientConfig& config); + void setClientConfig(const XrayClientConfig &config); void clearClientConfig(); }; } // namespace amnezia #endif // XRAYPROTOCOLCONFIG_H - diff --git a/client/core/protocols/xrayProtocol.cpp b/client/core/protocols/xrayProtocol.cpp old mode 100755 new mode 100644 index cceaddc28..9b9b6e41e --- a/client/core/protocols/xrayProtocol.cpp +++ b/client/core/protocols/xrayProtocol.cpp @@ -2,6 +2,7 @@ #include "core/protocols/protocolUtils.h" #include "core/utils/constants/configKeys.h" +#include "core/utils/constants/protocolConstants.h" #include "core/utils/ipcClient.h" #include "core/utils/networkUtilities.h" #include "core/utils/serialization/serialization.h" @@ -9,6 +10,7 @@ #include #include +#include #include #include #include @@ -79,12 +81,29 @@ ErrorCode XrayProtocol::start() m_socksPassword = creds.password; m_socksPort = creds.port; - const QString xrayConfigStr = QJsonDocument(m_xrayConfig).toJson(QJsonDocument::Compact); + QString xrayConfigStr = QJsonDocument(m_xrayConfig).toJson(QJsonDocument::Compact); if (xrayConfigStr.isEmpty()) { qCritical() << "Xray config is empty"; return ErrorCode::XrayExecutableCrashed; } + // Fix fingerprint: old configs may contain "Mozilla/5.0" which xray-core rejects. + // Replace with the correct default at runtime so stale stored configs still work. + if (xrayConfigStr.contains("Mozilla/5.0", Qt::CaseInsensitive)) { + xrayConfigStr.replace("Mozilla/5.0", amnezia::protocols::xray::defaultFingerprint, + Qt::CaseInsensitive); + qDebug() << "XrayProtocol: patched legacy fingerprint to" + << amnezia::protocols::xray::defaultFingerprint; + } + + // Fix inbound listen address: old configs may use "10.33.0.2" which doesn't exist + // until TUN is created. xray must listen on 127.0.0.1 so tun2socks can connect. + if (xrayConfigStr.contains(amnezia::protocols::xray::defaultLocalAddr)) { + xrayConfigStr.replace(amnezia::protocols::xray::defaultLocalAddr, + amnezia::protocols::xray::defaultLocalListenAddr); + qDebug() << "XrayProtocol: patched legacy inbound listen address to 127.0.0.1"; + } + return IpcClient::withInterface( [&](QSharedPointer iface) { auto xrayStart = iface->xrayStart(xrayConfigStr); @@ -188,6 +207,33 @@ ErrorCode XrayProtocol::startTun2Socks() connect( m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::finished, this, [this](int exitCode, QProcess::ExitStatus exitStatus) { + // Check stdout for "resource busy" — the TUN device was not yet released + // by the previous tun2socks instance. Retry after a short delay. + bool resourceBusy = false; + if (m_tun2socksProcess) { + auto readOut = m_tun2socksProcess->readAllStandardOutput(); + if (readOut.waitForFinished()) { + resourceBusy = readOut.returnValue().contains("resource busy"); + } + } + + if (resourceBusy && m_tun2socksRetryCount < maxTun2SocksRetries) { + m_tun2socksRetryCount++; + qWarning() << QString("Tun2socks: TUN resource busy, retrying (%1/%2) in %3ms...") + .arg(m_tun2socksRetryCount) + .arg(maxTun2SocksRetries) + .arg(tun2socksRetryDelayMs); + QTimer::singleShot(tun2socksRetryDelayMs, this, [this]() { + if (ErrorCode err = startTun2Socks(); err != ErrorCode::NoError) { + stop(); + setLastError(err); + } + }); + return; + } + + m_tun2socksRetryCount = 0; + if (exitStatus == QProcess::ExitStatus::CrashExit) { qCritical() << "Tun2socks process crashed!"; } else { diff --git a/client/core/protocols/xrayProtocol.h b/client/core/protocols/xrayProtocol.h index e831ab2f4..55b6d1d5c 100644 --- a/client/core/protocols/xrayProtocol.h +++ b/client/core/protocols/xrayProtocol.h @@ -35,6 +35,9 @@ private: int m_socksPort = 10808; QSharedPointer m_tun2socksProcess; + int m_tun2socksRetryCount = 0; + static constexpr int maxTun2SocksRetries = 5; + static constexpr int tun2socksRetryDelayMs = 400; }; #endif // XRAYPROTOCOL_H diff --git a/client/core/repositories/secureAppSettingsRepository.cpp b/client/core/repositories/secureAppSettingsRepository.cpp index 01f313b08..441e53bbc 100644 --- a/client/core/repositories/secureAppSettingsRepository.cpp +++ b/client/core/repositories/secureAppSettingsRepository.cpp @@ -451,4 +451,12 @@ void SecureAppSettingsRepository::setInstallationUuid(const QString &uuid) m_settings->setValue("Conf/installationUuid", uuid); } +QByteArray SecureAppSettingsRepository::xraySavedConfigs() const +{ + return value("Xray/savedConfigs").toByteArray(); +} +void SecureAppSettingsRepository::setXraySavedConfigs(const QByteArray &data) +{ + setValue("Xray/savedConfigs", data); +} diff --git a/client/core/repositories/secureAppSettingsRepository.h b/client/core/repositories/secureAppSettingsRepository.h index 54ee0cd07..f95174dd4 100644 --- a/client/core/repositories/secureAppSettingsRepository.h +++ b/client/core/repositories/secureAppSettingsRepository.h @@ -92,6 +92,9 @@ public: QString nextAvailableServerName() const; + QByteArray xraySavedConfigs() const; + void setXraySavedConfigs(const QByteArray &data); + signals: void appLanguageChanged(QLocale locale); void allowedDnsServersChanged(const QStringList &servers); diff --git a/client/core/utils/constants/configKeys.h b/client/core/utils/constants/configKeys.h index 62d9577e4..b896cdc37 100644 --- a/client/core/utils/constants/configKeys.h +++ b/client/core/utils/constants/configKeys.h @@ -126,6 +126,76 @@ namespace amnezia constexpr QLatin1String dataSent("dataSent"); constexpr QLatin1String storageServerId("storageServerId"); + + // ── Xray-specific keys ──────────────────────────────────────── + + // Security + constexpr QLatin1String xraySecurity("xray_security"); // none | tls | reality + constexpr QLatin1String xrayFlow("xray_flow"); // "" | xtls-rprx-vision | xtls-rprx-vision-udp443 + constexpr QLatin1String xrayFingerprint("xray_fingerprint"); // Mozilla/5.0 | chrome | firefox | ... + constexpr QLatin1String xraySni("xray_sni"); // Server Name (SNI) + constexpr QLatin1String xrayAlpn("xray_alpn"); // HTTP/2 | HTTP/1.1 | HTTP/2,HTTP/1.1 + + // Transport — common + constexpr QLatin1String xrayTransport("xray_transport"); // raw | xhttp | mkcp + + // Transport — XHTTP + constexpr QLatin1String xhttpMode("xhttp_mode"); // Auto | Packet-up | Stream-up | Stream-one + constexpr QLatin1String xhttpHost("xhttp_host"); + constexpr QLatin1String xhttpPath("xhttp_path"); + constexpr QLatin1String xhttpHeadersTemplate("xhttp_headers_template"); // HTTP | None + constexpr QLatin1String xhttpUplinkMethod("xhttp_uplink_method"); // POST | PUT | PATCH + constexpr QLatin1String xhttpDisableGrpc("xhttp_disable_grpc"); // bool + constexpr QLatin1String xhttpDisableSse("xhttp_disable_sse"); // bool + + // Transport — XHTTP Session & Sequence + constexpr QLatin1String xhttpSessionPlacement("xhttp_session_placement"); // Path | Header | Cookie | None + constexpr QLatin1String xhttpSessionKey("xhttp_session_key"); + constexpr QLatin1String xhttpSeqPlacement("xhttp_seq_placement"); + constexpr QLatin1String xhttpSeqKey("xhttp_seq_key"); + constexpr QLatin1String xhttpUplinkDataPlacement("xhttp_uplink_data_placement"); // Body | Query + constexpr QLatin1String xhttpUplinkDataKey("xhttp_uplink_data_key"); + + // Transport — XHTTP Traffic Shaping + constexpr QLatin1String xhttpUplinkChunkSize("xhttp_uplink_chunk_size"); + constexpr QLatin1String xhttpScMaxBufferedPosts("xhttp_sc_max_buffered_posts"); + constexpr QLatin1String xhttpScMaxEachPostBytesMin("xhttp_sc_max_each_post_bytes_min"); + constexpr QLatin1String xhttpScMaxEachPostBytesMax("xhttp_sc_max_each_post_bytes_max"); + constexpr QLatin1String xhttpScMinPostsIntervalMsMin("xhttp_sc_min_posts_interval_ms_min"); + constexpr QLatin1String xhttpScMinPostsIntervalMsMax("xhttp_sc_min_posts_interval_ms_max"); + constexpr QLatin1String xhttpScStreamUpServerSecsMin("xhttp_sc_stream_up_server_secs_min"); + constexpr QLatin1String xhttpScStreamUpServerSecsMax("xhttp_sc_stream_up_server_secs_max"); + + // Transport — mKCP + constexpr QLatin1String mkcpTti("mkcp_tti"); + constexpr QLatin1String mkcpUplinkCapacity("mkcp_uplink_capacity"); + constexpr QLatin1String mkcpDownlinkCapacity("mkcp_downlink_capacity"); + constexpr QLatin1String mkcpReadBufferSize("mkcp_read_buffer_size"); + constexpr QLatin1String mkcpWriteBufferSize("mkcp_write_buffer_size"); + constexpr QLatin1String mkcpCongestion("mkcp_congestion"); // bool + + // xPadding + constexpr QLatin1String xPaddingBytesMin("xpadding_bytes_min"); + constexpr QLatin1String xPaddingBytesMax("xpadding_bytes_max"); + constexpr QLatin1String xPaddingObfsMode("xpadding_obfs_mode"); // bool + constexpr QLatin1String xPaddingKey("xpadding_key"); + constexpr QLatin1String xPaddingHeader("xpadding_header"); + constexpr QLatin1String xPaddingPlacement("xpadding_placement"); // Cookie | Header | Query | Body + constexpr QLatin1String xPaddingMethod("xpadding_method"); // Repeat-x | Random | Zero + + // xmux + constexpr QLatin1String xmuxEnabled("xmux_enabled"); // bool + constexpr QLatin1String xmuxMaxConcurrencyMin("xmux_max_concurrency_min"); + constexpr QLatin1String xmuxMaxConcurrencyMax("xmux_max_concurrency_max"); + constexpr QLatin1String xmuxMaxConnectionsMin("xmux_max_connections_min"); + constexpr QLatin1String xmuxMaxConnectionsMax("xmux_max_connections_max"); + constexpr QLatin1String xmuxCMaxReuseTimesMin("xmux_c_max_reuse_times_min"); + constexpr QLatin1String xmuxCMaxReuseTimesMax("xmux_c_max_reuse_times_max"); + constexpr QLatin1String xmuxHMaxRequestTimesMin("xmux_h_max_request_times_min"); + constexpr QLatin1String xmuxHMaxRequestTimesMax("xmux_h_max_request_times_max"); + constexpr QLatin1String xmuxHMaxReusableSecsMin("xmux_h_max_reusable_secs_min"); + constexpr QLatin1String xmuxHMaxReusableSecsMax("xmux_h_max_reusable_secs_max"); + constexpr QLatin1String xmuxHKeepAlivePeriod("xmux_h_keep_alive_period"); } } diff --git a/client/core/utils/constants/protocolConstants.h b/client/core/utils/constants/protocolConstants.h index ec502669d..5c65d881e 100644 --- a/client/core/utils/constants/protocolConstants.h +++ b/client/core/utils/constants/protocolConstants.h @@ -58,6 +58,40 @@ namespace amnezia constexpr char defaultPort[] = "443"; constexpr char defaultLocalProxyPort[] = "10808"; constexpr char defaultLocalAddr[] = "10.33.0.2"; + constexpr char defaultLocalListenAddr[] = "127.0.0.1"; + + constexpr char defaultSecurity[] = "reality"; + constexpr char defaultFlow[] = "xtls-rprx-vision"; + constexpr char defaultTransport[] = "raw"; + constexpr char defaultFingerprint[] = "chrome"; + constexpr char defaultSni[] = "cdn.example.com"; + constexpr char defaultAlpn[] = "HTTP/2"; + + constexpr char defaultXhttpMode[] = "Auto"; + constexpr char defaultXhttpHeadersTemplate[] = "HTTP"; + constexpr char defaultXhttpUplinkMethod[] = "POST"; + constexpr char defaultXhttpSessionPlacement[] = "Path"; + constexpr char defaultXhttpSessionKey[] = "Path"; + constexpr char defaultXhttpSeqPlacement[] = "Path"; + constexpr char defaultXhttpUplinkDataPlacement[] = "Body"; + + constexpr char defaultXhttpHost[] = "www.googletagmanager.com"; + constexpr char defaultXhttpUplinkChunkSize[] = "0"; + constexpr char defaultXhttpScMaxEachPostBytesMin[] = "1"; + constexpr char defaultXhttpScMaxEachPostBytesMax[] = "100"; + constexpr char defaultXhttpScMinPostsIntervalMsMin[] = "100"; + constexpr char defaultXhttpScMinPostsIntervalMsMax[] = "800"; + constexpr char defaultXhttpScStreamUpServerSecsMin[] = "1"; + constexpr char defaultXhttpScStreamUpServerSecsMax[] = "100"; + + constexpr char defaultXPaddingPlacement[] = "Cookie"; + constexpr char defaultXPaddingMethod[] = "Repeat-x"; + + constexpr char defaultMkcpTti[] = "50"; + constexpr char defaultMkcpUplinkCapacity[] = "5"; + constexpr char defaultMkcpDownlinkCapacity[] = "20"; + constexpr char defaultMkcpReadBufferSize[] = "2"; + constexpr char defaultMkcpWriteBufferSize[] = "2"; constexpr char outbounds[] = "outbounds"; constexpr char inbounds[] = "inbounds"; diff --git a/client/platforms/ios/ios_controller.mm b/client/platforms/ios/ios_controller.mm index c93dac460..e988ae43e 100644 --- a/client/platforms/ios/ios_controller.mm +++ b/client/platforms/ios/ios_controller.mm @@ -220,7 +220,7 @@ bool IosController::connectVpn(amnezia::Proto proto, const QJsonObject& configur m_rawConfig = configuration; m_serverAddress = configuration.value(configKey::hostName).toString().toNSString(); - const QString serverDescription = configuration.value(config_key::description).toString().trimmed(); + const QString serverDescription = configuration.value(configKey::description).toString().trimmed(); QString tunnelName; if (serverDescription.isEmpty()) { tunnelName = ProtocolUtils::protoToString(proto); diff --git a/client/translations/amneziavpn_ru_RU.ts b/client/translations/amneziavpn_ru_RU.ts index e813b40f1..b1f95a688 100644 --- a/client/translations/amneziavpn_ru_RU.ts +++ b/client/translations/amneziavpn_ru_RU.ts @@ -1312,6 +1312,21 @@ Thank you for staying with us! PageProtocolXraySettings + + + XRay VLESS settings + Настройки XRay VLESS + + + + More about settings + Подробнее о настройках + + + + Reset settings + Сбросить настройки + XRay settings diff --git a/client/ui/controllers/importUiController.cpp b/client/ui/controllers/importUiController.cpp index ce9b952c1..db81c608c 100644 --- a/client/ui/controllers/importUiController.cpp +++ b/client/ui/controllers/importUiController.cpp @@ -201,3 +201,12 @@ bool ImportUiController::decodeQrCode(const QString &code) return mInstance->parseQrCodeChunk(code); } #endif + +QString ImportUiController::readTextFile(const QString &fileName) +{ + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + return {}; + } + return QString::fromUtf8(file.readAll()); +} diff --git a/client/ui/controllers/importUiController.h b/client/ui/controllers/importUiController.h index 853539d05..c527e93c2 100644 --- a/client/ui/controllers/importUiController.h +++ b/client/ui/controllers/importUiController.h @@ -28,6 +28,7 @@ public slots: QString getMaliciousWarningText(); bool isNativeWireGuardConfig(); void processNativeWireGuardConfig(); + QString readTextFile(const QString &fileName); #if defined Q_OS_ANDROID || defined Q_OS_IOS void startDecodingQr(); diff --git a/client/ui/controllers/qml/pageController.h b/client/ui/controllers/qml/pageController.h index 714333a5a..216328daf 100644 --- a/client/ui/controllers/qml/pageController.h +++ b/client/ui/controllers/qml/pageController.h @@ -82,7 +82,15 @@ namespace PageLoader PageSetupWizardApiPremiumInfo, PageSetupWizardApiTrialEmail, - PageDevMenu + PageDevMenu, + + PageProtocolXraySnapshots, + PageProtocolXrayTransportSettings, + PageProtocolXrayXmuxSettings, + PageProtocolXrayXPaddingSettings, + PageProtocolXrayFlowSettings, + PageProtocolXraySecuritySettings, + PageProtocolXrayXPaddingBytesSettings, }; Q_ENUM_NS(PageEnum) diff --git a/client/ui/controllers/selfhosted/exportUiController.cpp b/client/ui/controllers/selfhosted/exportUiController.cpp index 3be7ef981..bbe1da570 100644 --- a/client/ui/controllers/selfhosted/exportUiController.cpp +++ b/client/ui/controllers/selfhosted/exportUiController.cpp @@ -127,3 +127,13 @@ void ExportUiController::applyExportResult(const ExportController::ExportResult emit exportConfigChanged(); } + +void ExportUiController::setConfigFromString(const QString &config, const QString &fileName) +{ + clearPreviousConfig(); + m_config = config; + emit exportConfigChanged(); + if (!fileName.isEmpty()) { + SystemController::saveFile(fileName, m_config); + } +} diff --git a/client/ui/controllers/selfhosted/exportUiController.h b/client/ui/controllers/selfhosted/exportUiController.h index 20f7a2282..970ce7c08 100644 --- a/client/ui/controllers/selfhosted/exportUiController.h +++ b/client/ui/controllers/selfhosted/exportUiController.h @@ -32,6 +32,7 @@ public slots: QList getQrCodes(); void exportConfig(const QString &fileName); + void setConfigFromString(const QString &config, const QString &fileName); void updateClientManagementModel(const QString &serverId, int containerIndex); diff --git a/client/ui/controllers/selfhosted/installUiController.cpp b/client/ui/controllers/selfhosted/installUiController.cpp old mode 100755 new mode 100644 index ad42f0b86..f413aac19 --- a/client/ui/controllers/selfhosted/installUiController.cpp +++ b/client/ui/controllers/selfhosted/installUiController.cpp @@ -592,7 +592,8 @@ void InstallUiController::updateProtocolConfigModel(const QString &serverId, int case Proto::Awg: updateIfPresent(m_awgConfigModel, containerConfig.getAwgProtocolConfig()); break; case Proto::WireGuard: updateIfPresent(m_wireGuardConfigModel, containerConfig.getWireGuardProtocolConfig()); break; case Proto::OpenVpn: updateIfPresent(m_openVpnConfigModel, containerConfig.getOpenVpnProtocolConfig()); break; - case Proto::Xray: updateIfPresent(m_xrayConfigModel, containerConfig.getXrayProtocolConfig()); break; + case Proto::Xray: + case Proto::SSXray: updateIfPresent(m_xrayConfigModel, containerConfig.getXrayProtocolConfig()); break; case Proto::TorWebSite: updateIfPresent(m_torConfigModel, containerConfig.getTorProtocolConfig()); break; case Proto::Sftp: updateIfPresent(m_sftpConfigModel, containerConfig.getSftpProtocolConfig()); break; case Proto::Socks5Proxy: updateIfPresent(m_socks5ConfigModel, containerConfig.getSocks5ProxyProtocolConfig()); break; diff --git a/client/ui/models/protocols/xrayConfigModel.cpp b/client/ui/models/protocols/xrayConfigModel.cpp index 462982073..9a24759c9 100644 --- a/client/ui/models/protocols/xrayConfigModel.cpp +++ b/client/ui/models/protocols/xrayConfigModel.cpp @@ -8,94 +8,575 @@ using namespace amnezia; using namespace ProtocolUtils; -XrayConfigModel::XrayConfigModel(QObject *parent) : QAbstractListModel(parent) +XrayConfigModel::XrayConfigModel(QObject* parent) : QAbstractListModel(parent) { } -int XrayConfigModel::rowCount(const QModelIndex &parent) const +int XrayConfigModel::rowCount(const QModelIndex& parent) const { Q_UNUSED(parent); return 1; } -bool XrayConfigModel::setData(const QModelIndex &index, const QVariant &value, int role) +bool XrayConfigModel::setData(const QModelIndex& index, const QVariant& value, int role) { - if (!index.isValid() || index.row() < 0 || index.row() >= ContainerUtils::allContainers().size()) { + // This model always has a single row (row 0). Using rowCount() avoids + // coupling editing ability to global container list size. + if (!index.isValid() || index.row() < 0 || index.row() >= rowCount()) + { return false; } - QString strValue = value.toString(); + const bool wasUnsavedChanges = hasUnsavedChanges(); + + auto& srv = m_protocolConfig.serverConfig; + auto& xhttp = srv.xhttp; + auto& mkcp = srv.mkcp; + auto& pad = xhttp.xPadding; + auto& mux = xhttp.xmux; + + QString str = value.toString(); + + switch (role) + { + // ── Main ────────────────────────────────────────────────────────── + case Roles::SiteRole: srv.site = str; + break; + case Roles::PortRole: srv.port = str; + break; + case Roles::TransportRole: srv.transport = str; + break; + case Roles::SecurityRole: srv.security = str; + break; + case Roles::FlowRole: srv.flow = str; + break; + + // ── Security ────────────────────────────────────────────────────── + case Roles::FingerprintRole: srv.fingerprint = str; + break; + case Roles::SniRole: srv.sni = str; + break; + case Roles::AlpnRole: srv.alpn = str; + break; + + // ── XHTTP ───────────────────────────────────────────────────────── + case Roles::XhttpModeRole: xhttp.mode = str; + break; + case Roles::XhttpHostRole: xhttp.host = str; + break; + case Roles::XhttpPathRole: xhttp.path = str; + break; + case Roles::XhttpHeadersTemplateRole: xhttp.headersTemplate = str; + break; + case Roles::XhttpUplinkMethodRole: xhttp.uplinkMethod = str; + break; + case Roles::XhttpDisableGrpcRole: xhttp.disableGrpc = value.toBool(); + break; + case Roles::XhttpDisableSseRole: xhttp.disableSse = value.toBool(); + break; + + case Roles::XhttpSessionPlacementRole: xhttp.sessionPlacement = str; + break; + case Roles::XhttpSessionKeyRole: xhttp.sessionKey = str; + break; + case Roles::XhttpSeqPlacementRole: xhttp.seqPlacement = str; + break; + case Roles::XhttpSeqKeyRole: xhttp.seqKey = str; + break; + case Roles::XhttpUplinkDataPlacementRole: xhttp.uplinkDataPlacement = str; + break; + case Roles::XhttpUplinkDataKeyRole: xhttp.uplinkDataKey = str; + break; + + case Roles::XhttpUplinkChunkSizeRole: xhttp.uplinkChunkSize = str; + break; + case Roles::XhttpScMaxBufferedPostsRole: xhttp.scMaxBufferedPosts = str; + break; + case Roles::XhttpScMaxEachPostBytesMinRole: xhttp.scMaxEachPostBytesMin = str; + break; + case Roles::XhttpScMaxEachPostBytesMaxRole: xhttp.scMaxEachPostBytesMax = str; + break; + case Roles::XhttpScMinPostsIntervalMsMinRole: xhttp.scMinPostsIntervalMsMin = str; + break; + case Roles::XhttpScMinPostsIntervalMsMaxRole: xhttp.scMinPostsIntervalMsMax = str; + break; + case Roles::XhttpScStreamUpServerSecsMinRole: xhttp.scStreamUpServerSecsMin = str; + break; + case Roles::XhttpScStreamUpServerSecsMaxRole: xhttp.scStreamUpServerSecsMax = str; + break; + + // ── mKCP ────────────────────────────────────────────────────────── + case Roles::MkcpTtiRole: mkcp.tti = str; + break; + case Roles::MkcpUplinkCapacityRole: mkcp.uplinkCapacity = str; + break; + case Roles::MkcpDownlinkCapacityRole: mkcp.downlinkCapacity = str; + break; + case Roles::MkcpReadBufferSizeRole: mkcp.readBufferSize = str; + break; + case Roles::MkcpWriteBufferSizeRole: mkcp.writeBufferSize = str; + break; + case Roles::MkcpCongestionRole: mkcp.congestion = value.toBool(); + break; + + // ── xPadding ────────────────────────────────────────────────────── + case Roles::XPaddingBytesMinRole: pad.bytesMin = str; + break; + case Roles::XPaddingBytesMaxRole: pad.bytesMax = str; + break; + case Roles::XPaddingObfsModeRole: pad.obfsMode = value.toBool(); + break; + case Roles::XPaddingKeyRole: pad.key = str; + break; + case Roles::XPaddingHeaderRole: pad.header = str; + break; + case Roles::XPaddingPlacementRole: pad.placement = str; + break; + case Roles::XPaddingMethodRole: pad.method = str; + break; + + // ── xmux ────────────────────────────────────────────────────────── + case Roles::XmuxEnabledRole: mux.enabled = value.toBool(); + break; + case Roles::XmuxMaxConcurrencyMinRole: mux.maxConcurrencyMin = str; + break; + case Roles::XmuxMaxConcurrencyMaxRole: mux.maxConcurrencyMax = str; + break; + case Roles::XmuxMaxConnectionsMinRole: mux.maxConnectionsMin = str; + break; + case Roles::XmuxMaxConnectionsMaxRole: mux.maxConnectionsMax = str; + break; + case Roles::XmuxCMaxReuseTimesMinRole: mux.cMaxReuseTimesMin = str; + break; + case Roles::XmuxCMaxReuseTimesMaxRole: mux.cMaxReuseTimesMax = str; + break; + case Roles::XmuxHMaxRequestTimesMinRole: mux.hMaxRequestTimesMin = str; + break; + case Roles::XmuxHMaxRequestTimesMaxRole: mux.hMaxRequestTimesMax = str; + break; + case Roles::XmuxHMaxReusableSecsMinRole: mux.hMaxReusableSecsMin = str; + break; + case Roles::XmuxHMaxReusableSecsMaxRole: mux.hMaxReusableSecsMax = str; + break; + case Roles::XmuxHKeepAlivePeriodRole: mux.hKeepAlivePeriod = str; + break; - switch (role) { - case Roles::SiteRole: m_protocolConfig.serverConfig.site = strValue; break; - case Roles::PortRole: m_protocolConfig.serverConfig.port = strValue; break; default: return false; } - emit dataChanged(index, index, QList { role }); + emit dataChanged(index, index, QList{role}); + if (wasUnsavedChanges != hasUnsavedChanges()) { + emit hasUnsavedChangesChanged(); + } return true; } -QVariant XrayConfigModel::data(const QModelIndex &index, int role) const +QVariant XrayConfigModel::data(const QModelIndex& index, int role) const { if (!index.isValid() || index.row() < 0 || index.row() >= rowCount()) { return QVariant(); } - switch (role) { - case Roles::SiteRole: return m_protocolConfig.serverConfig.site; - case Roles::PortRole: return m_protocolConfig.serverConfig.port; + const auto& srv = m_protocolConfig.serverConfig; + const auto& xhttp = srv.xhttp; + const auto& mkcp = srv.mkcp; + const auto& pad = xhttp.xPadding; + const auto& mux = xhttp.xmux; + + switch (role) + { + // ── Main ────────────────────────────────────────────────────────── + case Roles::SiteRole: return srv.site; + case Roles::PortRole: return srv.port; + case Roles::TransportRole: return srv.transport; + case Roles::SecurityRole: return srv.security; + case Roles::FlowRole: return srv.flow; + + // ── Security ────────────────────────────────────────────────────── + case Roles::FingerprintRole: return srv.fingerprint; + case Roles::SniRole: return srv.sni; + case Roles::AlpnRole: return srv.alpn; + + // ── XHTTP ───────────────────────────────────────────────────────── + case Roles::XhttpModeRole: return xhttp.mode; + case Roles::XhttpHostRole: return xhttp.host; + case Roles::XhttpPathRole: return xhttp.path; + case Roles::XhttpHeadersTemplateRole: return xhttp.headersTemplate; + case Roles::XhttpUplinkMethodRole: return xhttp.uplinkMethod; + case Roles::XhttpDisableGrpcRole: return xhttp.disableGrpc; + case Roles::XhttpDisableSseRole: return xhttp.disableSse; + + case Roles::XhttpSessionPlacementRole: return xhttp.sessionPlacement; + case Roles::XhttpSessionKeyRole: return xhttp.sessionKey; + case Roles::XhttpSeqPlacementRole: return xhttp.seqPlacement; + case Roles::XhttpSeqKeyRole: return xhttp.seqKey; + case Roles::XhttpUplinkDataPlacementRole: return xhttp.uplinkDataPlacement; + case Roles::XhttpUplinkDataKeyRole: return xhttp.uplinkDataKey; + + case Roles::XhttpUplinkChunkSizeRole: return xhttp.uplinkChunkSize; + case Roles::XhttpScMaxBufferedPostsRole: return xhttp.scMaxBufferedPosts; + case Roles::XhttpScMaxEachPostBytesMinRole: return xhttp.scMaxEachPostBytesMin; + case Roles::XhttpScMaxEachPostBytesMaxRole: return xhttp.scMaxEachPostBytesMax; + case Roles::XhttpScMinPostsIntervalMsMinRole: return xhttp.scMinPostsIntervalMsMin; + case Roles::XhttpScMinPostsIntervalMsMaxRole: return xhttp.scMinPostsIntervalMsMax; + case Roles::XhttpScStreamUpServerSecsMinRole: return xhttp.scStreamUpServerSecsMin; + case Roles::XhttpScStreamUpServerSecsMaxRole: return xhttp.scStreamUpServerSecsMax; + + // ── mKCP ────────────────────────────────────────────────────────── + case Roles::MkcpTtiRole: return mkcp.tti; + case Roles::MkcpUplinkCapacityRole: return mkcp.uplinkCapacity; + case Roles::MkcpDownlinkCapacityRole: return mkcp.downlinkCapacity; + case Roles::MkcpReadBufferSizeRole: return mkcp.readBufferSize; + case Roles::MkcpWriteBufferSizeRole: return mkcp.writeBufferSize; + case Roles::MkcpCongestionRole: return mkcp.congestion; + + // ── xPadding ────────────────────────────────────────────────────── + case Roles::XPaddingBytesMinRole: return pad.bytesMin; + case Roles::XPaddingBytesMaxRole: return pad.bytesMax; + case Roles::XPaddingObfsModeRole: return pad.obfsMode; + case Roles::XPaddingKeyRole: return pad.key; + case Roles::XPaddingHeaderRole: return pad.header; + case Roles::XPaddingPlacementRole: return pad.placement; + case Roles::XPaddingMethodRole: return pad.method; + + // ── xmux ────────────────────────────────────────────────────────── + case Roles::XmuxEnabledRole: return mux.enabled; + case Roles::XmuxMaxConcurrencyMinRole: return mux.maxConcurrencyMin; + case Roles::XmuxMaxConcurrencyMaxRole: return mux.maxConcurrencyMax; + case Roles::XmuxMaxConnectionsMinRole: return mux.maxConnectionsMin; + case Roles::XmuxMaxConnectionsMaxRole: return mux.maxConnectionsMax; + case Roles::XmuxCMaxReuseTimesMinRole: return mux.cMaxReuseTimesMin; + case Roles::XmuxCMaxReuseTimesMaxRole: return mux.cMaxReuseTimesMax; + case Roles::XmuxHMaxRequestTimesMinRole: return mux.hMaxRequestTimesMin; + case Roles::XmuxHMaxRequestTimesMaxRole: return mux.hMaxRequestTimesMax; + case Roles::XmuxHMaxReusableSecsMinRole: return mux.hMaxReusableSecsMin; + case Roles::XmuxHMaxReusableSecsMaxRole: return mux.hMaxReusableSecsMax; + case Roles::XmuxHKeepAlivePeriodRole: return mux.hKeepAlivePeriod; } return QVariant(); } -void XrayConfigModel::updateModel(amnezia::DockerContainer container, const amnezia::XrayProtocolConfig &protocolConfig) +void XrayConfigModel::updateModel(amnezia::DockerContainer container, const amnezia::XrayProtocolConfig& protocolConfig) { + const bool wasUnsavedChanges = hasUnsavedChanges(); + beginResetModel(); + m_container = container; - + m_protocolConfig = protocolConfig; - + applyDefaultsToServerConfig(m_protocolConfig.serverConfig); - + m_originalProtocolConfig = m_protocolConfig; - + endResetModel(); + if (wasUnsavedChanges != hasUnsavedChanges()) { + emit hasUnsavedChangesChanged(); + } } -void XrayConfigModel::applyDefaultsToServerConfig(amnezia::XrayServerConfig& config) +void XrayConfigModel::applyDefaultsToServerConfig(amnezia::XrayServerConfig &config) { if (config.port.isEmpty()) { config.port = protocols::xray::defaultPort; } + if (config.transportProto.isEmpty()) { config.transportProto = ProtocolUtils::transportProtoToString( ProtocolUtils::defaultTransportProto(amnezia::Proto::Xray), amnezia::Proto::Xray); } + if (config.site.isEmpty()) { config.site = protocols::xray::defaultSite; } + + if (config.transport.isEmpty()) { + config.transport = protocols::xray::defaultTransport; + } + + if (config.security.isEmpty()) { + config.security = protocols::xray::defaultSecurity; + } + + if (config.flow.isEmpty()) { + config.flow = protocols::xray::defaultFlow; + } + + if (config.fingerprint.isEmpty()) { + config.fingerprint = protocols::xray::defaultFingerprint; + } else if (config.fingerprint.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) { + config.fingerprint = QString::fromLatin1(protocols::xray::defaultFingerprint); + } + + if (config.sni.isEmpty()) { + config.sni = protocols::xray::defaultSni; + } + + if (config.alpn.isEmpty()) { + config.alpn = protocols::xray::defaultAlpn; + } + + // XHTTP transport defaults + if (config.xhttp.host.isEmpty()) { + config.xhttp.host = protocols::xray::defaultXhttpHost; + } + if (config.xhttp.mode.isEmpty()) { + config.xhttp.mode = protocols::xray::defaultXhttpMode; + } + if (config.xhttp.headersTemplate.isEmpty()) { + config.xhttp.headersTemplate = protocols::xray::defaultXhttpHeadersTemplate; + } + if (config.xhttp.uplinkMethod.isEmpty()) { + config.xhttp.uplinkMethod = protocols::xray::defaultXhttpUplinkMethod; + } + if (config.xhttp.sessionPlacement.isEmpty()) { + config.xhttp.sessionPlacement = protocols::xray::defaultXhttpSessionPlacement; + } + if (config.xhttp.sessionKey.isEmpty()) { + config.xhttp.sessionKey = protocols::xray::defaultXhttpSessionKey; + } + if (config.xhttp.seqPlacement.isEmpty()) { + config.xhttp.seqPlacement = protocols::xray::defaultXhttpSeqPlacement; + } + if (config.xhttp.uplinkDataPlacement.isEmpty()) { + config.xhttp.uplinkDataPlacement = protocols::xray::defaultXhttpUplinkDataPlacement; + } + + // xPadding defaults + if (config.xhttp.xPadding.placement.isEmpty()) { + config.xhttp.xPadding.placement = protocols::xray::defaultXPaddingPlacement; + } + if (config.xhttp.xPadding.method.isEmpty()) { + config.xhttp.xPadding.method = protocols::xray::defaultXPaddingMethod; + } } amnezia::XrayProtocolConfig XrayConfigModel::getProtocolConfig() { - bool serverSettingsChanged = !m_protocolConfig.serverConfig.hasEqualServerSettings(m_originalProtocolConfig.serverConfig); - + const bool serverSettingsChanged = + !m_protocolConfig.serverConfig.hasEqualServerSettings(m_originalProtocolConfig.serverConfig); + if (serverSettingsChanged) { m_protocolConfig.clearClientConfig(); } - return m_protocolConfig; } +bool XrayConfigModel::isServerSettingsEqual() const +{ + return m_protocolConfig.serverConfig.hasEqualServerSettings(m_originalProtocolConfig.serverConfig); +} + +bool XrayConfigModel::hasUnsavedChanges() const +{ + return !isServerSettingsEqual(); +} + QHash XrayConfigModel::roleNames() const { QHash roles; + // Main roles[SiteRole] = "site"; roles[PortRole] = "port"; + roles[TransportRole] = "transport"; + roles[SecurityRole] = "security"; + roles[FlowRole] = "flow"; + + // Security + roles[FingerprintRole] = "fingerprint"; + roles[SniRole] = "sni"; + roles[AlpnRole] = "alpn"; + + // XHTTP + roles[XhttpModeRole] = "xhttpMode"; + roles[XhttpHostRole] = "xhttpHost"; + roles[XhttpPathRole] = "xhttpPath"; + roles[XhttpHeadersTemplateRole] = "xhttpHeadersTemplate"; + roles[XhttpUplinkMethodRole] = "xhttpUplinkMethod"; + roles[XhttpDisableGrpcRole] = "xhttpDisableGrpc"; + roles[XhttpDisableSseRole] = "xhttpDisableSse"; + + roles[XhttpSessionPlacementRole] = "xhttpSessionPlacement"; + roles[XhttpSessionKeyRole] = "xhttpSessionKey"; + roles[XhttpSeqPlacementRole] = "xhttpSeqPlacement"; + roles[XhttpSeqKeyRole] = "xhttpSeqKey"; + roles[XhttpUplinkDataPlacementRole] = "xhttpUplinkDataPlacement"; + roles[XhttpUplinkDataKeyRole] = "xhttpUplinkDataKey"; + + roles[XhttpUplinkChunkSizeRole] = "xhttpUplinkChunkSize"; + roles[XhttpScMaxBufferedPostsRole] = "xhttpScMaxBufferedPosts"; + roles[XhttpScMaxEachPostBytesMinRole] = "xhttpScMaxEachPostBytesMin"; + roles[XhttpScMaxEachPostBytesMaxRole] = "xhttpScMaxEachPostBytesMax"; + roles[XhttpScMinPostsIntervalMsMinRole] = "xhttpScMinPostsIntervalMsMin"; + roles[XhttpScMinPostsIntervalMsMaxRole] = "xhttpScMinPostsIntervalMsMax"; + roles[XhttpScStreamUpServerSecsMinRole] = "xhttpScStreamUpServerSecsMin"; + roles[XhttpScStreamUpServerSecsMaxRole] = "xhttpScStreamUpServerSecsMax"; + + // mKCP + roles[MkcpTtiRole] = "mkcpTti"; + roles[MkcpUplinkCapacityRole] = "mkcpUplinkCapacity"; + roles[MkcpDownlinkCapacityRole] = "mkcpDownlinkCapacity"; + roles[MkcpReadBufferSizeRole] = "mkcpReadBufferSize"; + roles[MkcpWriteBufferSizeRole] = "mkcpWriteBufferSize"; + roles[MkcpCongestionRole] = "mkcpCongestion"; + + // xPadding + roles[XPaddingBytesMinRole] = "xPaddingBytesMin"; + roles[XPaddingBytesMaxRole] = "xPaddingBytesMax"; + roles[XPaddingObfsModeRole] = "xPaddingObfsMode"; + roles[XPaddingKeyRole] = "xPaddingKey"; + roles[XPaddingHeaderRole] = "xPaddingHeader"; + roles[XPaddingPlacementRole] = "xPaddingPlacement"; + roles[XPaddingMethodRole] = "xPaddingMethod"; + + // xmux + roles[XmuxEnabledRole] = "xmuxEnabled"; + roles[XmuxMaxConcurrencyMinRole] = "xmuxMaxConcurrencyMin"; + roles[XmuxMaxConcurrencyMaxRole] = "xmuxMaxConcurrencyMax"; + roles[XmuxMaxConnectionsMinRole] = "xmuxMaxConnectionsMin"; + roles[XmuxMaxConnectionsMaxRole] = "xmuxMaxConnectionsMax"; + roles[XmuxCMaxReuseTimesMinRole] = "xmuxCMaxReuseTimesMin"; + roles[XmuxCMaxReuseTimesMaxRole] = "xmuxCMaxReuseTimesMax"; + roles[XmuxHMaxRequestTimesMinRole] = "xmuxHMaxRequestTimesMin"; + roles[XmuxHMaxRequestTimesMaxRole] = "xmuxHMaxRequestTimesMax"; + roles[XmuxHMaxReusableSecsMinRole] = "xmuxHMaxReusableSecsMin"; + roles[XmuxHMaxReusableSecsMaxRole] = "xmuxHMaxReusableSecsMax"; + roles[XmuxHKeepAlivePeriodRole] = "xmuxHKeepAlivePeriod"; return roles; } + +void XrayConfigModel::resetToDefaults() +{ + const bool wasUnsavedChanges = hasUnsavedChanges(); + + beginResetModel(); + m_protocolConfig.serverConfig = amnezia::XrayServerConfig{}; + applyDefaultsToServerConfig(m_protocolConfig.serverConfig); + endResetModel(); + + if (wasUnsavedChanges != hasUnsavedChanges()) { + emit hasUnsavedChangesChanged(); + } +} + +void XrayConfigModel::applyServerConfig(const amnezia::XrayServerConfig &serverConfig) +{ + const bool wasUnsavedChanges = hasUnsavedChanges(); + + beginResetModel(); + m_protocolConfig.serverConfig = serverConfig; + // Clear client config since server settings changed + m_protocolConfig.clearClientConfig(); + m_originalProtocolConfig = m_protocolConfig; + endResetModel(); + + if (wasUnsavedChanges != hasUnsavedChanges()) { + emit hasUnsavedChangesChanged(); + } +} + +QStringList XrayConfigModel::flowOptions() +{ + return { + "", // Empty (no flow) + "xtls-rprx-vision", + "xtls-rprx-vision-udp443" + }; +} + +QStringList XrayConfigModel::securityOptions() +{ + return { "none", "tls", "reality" }; +} + +QStringList XrayConfigModel::transportOptions() +{ + return { "raw", "xhttp", "mkcp" }; +} + +QStringList XrayConfigModel::fingerprintOptions() +{ + return { "chrome", "firefox", "safari", "ios", "android", "edge", "360", "qq", "random" }; +} + +QStringList XrayConfigModel::alpnOptions() +{ + return { "HTTP/2", "HTTP/1.1", "HTTP/2,HTTP/1.1" }; +} + +QStringList XrayConfigModel::xhttpModeOptions() +{ + return { "Auto", "Packet-up", "Stream-up", "Stream-one" }; +} + +QStringList XrayConfigModel::xhttpHeadersTemplateOptions() +{ + return { "HTTP", "None" }; +} + +QStringList XrayConfigModel::xhttpUplinkMethodOptions() +{ + return { "POST", "PUT", "PATCH" }; +} + +QStringList XrayConfigModel::xhttpSessionPlacementOptions() +{ + return { "Path", "Header", "Cookie", "None" }; +} + +QStringList XrayConfigModel::xhttpSessionKeyOptions() +{ + return { "Path", "Header", "None" }; +} + +QStringList XrayConfigModel::xhttpSeqPlacementOptions() +{ + return { "Path", "Header", "Cookie", "None" }; +} + +QStringList XrayConfigModel::xhttpUplinkDataPlacementOptions() +{ + // Matches splithttp uplink payload placement (packet-up / advanced) + return { "Body", "Auto", "Header", "Cookie" }; +} + +QStringList XrayConfigModel::xPaddingPlacementOptions() +{ + // Xray-core: cookie | header | query | queryInHeader (not "body") + return { "Cookie", "Header", "Query", "Query in header" }; +} + +QStringList XrayConfigModel::xPaddingMethodOptions() +{ + return { "Repeat-x", "Tokenish" }; +} + +QString XrayConfigModel::mkcpDefaultTti() +{ + return QString::fromLatin1(protocols::xray::defaultMkcpTti); +} + +QString XrayConfigModel::mkcpDefaultUplinkCapacity() +{ + return QString::fromLatin1(protocols::xray::defaultMkcpUplinkCapacity); +} + +QString XrayConfigModel::mkcpDefaultDownlinkCapacity() +{ + return QString::fromLatin1(protocols::xray::defaultMkcpDownlinkCapacity); +} + +QString XrayConfigModel::mkcpDefaultReadBufferSize() +{ + return QString::fromLatin1(protocols::xray::defaultMkcpReadBufferSize); +} + +QString XrayConfigModel::mkcpDefaultWriteBufferSize() +{ + return QString::fromLatin1(protocols::xray::defaultMkcpWriteBufferSize); +} diff --git a/client/ui/models/protocols/xrayConfigModel.h b/client/ui/models/protocols/xrayConfigModel.h index 5549cf446..fee066107 100644 --- a/client/ui/models/protocols/xrayConfigModel.h +++ b/client/ui/models/protocols/xrayConfigModel.h @@ -2,6 +2,7 @@ #define XRAYCONFIGMODEL_H #include +#include #include "core/utils/containerEnum.h" #include "core/utils/containers/containerUtils.h" @@ -11,23 +12,122 @@ class XrayConfigModel : public QAbstractListModel { Q_OBJECT + Q_PROPERTY(bool hasUnsavedChanges READ hasUnsavedChanges NOTIFY hasUnsavedChangesChanged) public: - enum Roles { - SiteRole, - PortRole + enum Roles + { + // ── Main page ───────────────────────────────────────────────── + SiteRole = Qt::UserRole + 1, + PortRole, + TransportRole, // "raw" | "xhttp" | "mkcp" (display in main page row) + SecurityRole, // "none" | "tls" | "reality" (display in main page row) + FlowRole, // "" | "xtls-rprx-vision" | "xtls-rprx-vision-udp443" + + // ── Security ────────────────────────────────────────────────── + FingerprintRole, + SniRole, + AlpnRole, + + // ── Transport — XHTTP ───────────────────────────────────────── + XhttpModeRole, + XhttpHostRole, + XhttpPathRole, + XhttpHeadersTemplateRole, + XhttpUplinkMethodRole, + XhttpDisableGrpcRole, + XhttpDisableSseRole, + + // Session & Sequence + XhttpSessionPlacementRole, + XhttpSessionKeyRole, + XhttpSeqPlacementRole, + XhttpSeqKeyRole, + XhttpUplinkDataPlacementRole, + XhttpUplinkDataKeyRole, + + // Traffic Shaping + XhttpUplinkChunkSizeRole, + XhttpScMaxBufferedPostsRole, + XhttpScMaxEachPostBytesMinRole, + XhttpScMaxEachPostBytesMaxRole, + XhttpScMinPostsIntervalMsMinRole, + XhttpScMinPostsIntervalMsMaxRole, + XhttpScStreamUpServerSecsMinRole, + XhttpScStreamUpServerSecsMaxRole, + + // ── Transport — mKCP ────────────────────────────────────────── + MkcpTtiRole, + MkcpUplinkCapacityRole, + MkcpDownlinkCapacityRole, + MkcpReadBufferSizeRole, + MkcpWriteBufferSizeRole, + MkcpCongestionRole, + + // ── xPadding ────────────────────────────────────────────────── + XPaddingBytesMinRole, + XPaddingBytesMaxRole, + XPaddingObfsModeRole, + XPaddingKeyRole, + XPaddingHeaderRole, + XPaddingPlacementRole, + XPaddingMethodRole, + + // ── xmux ────────────────────────────────────────────────────── + XmuxEnabledRole, + XmuxMaxConcurrencyMinRole, + XmuxMaxConcurrencyMaxRole, + XmuxMaxConnectionsMinRole, + XmuxMaxConnectionsMaxRole, + XmuxCMaxReuseTimesMinRole, + XmuxCMaxReuseTimesMaxRole, + XmuxHMaxRequestTimesMinRole, + XmuxHMaxRequestTimesMaxRole, + XmuxHMaxReusableSecsMinRole, + XmuxHMaxReusableSecsMaxRole, + XmuxHKeepAlivePeriodRole, }; - explicit XrayConfigModel(QObject *parent = nullptr); + explicit XrayConfigModel(QObject* parent = nullptr); - int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int rowCount(const QModelIndex& parent = QModelIndex()) const override; - bool setData(const QModelIndex &index, const QVariant &value, int role) override; - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + bool setData(const QModelIndex& index, const QVariant& value, int role) override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + + // ── Static option lists (for QML DropDown models) ───────────────── + Q_INVOKABLE static QStringList flowOptions(); + Q_INVOKABLE static QStringList securityOptions(); + Q_INVOKABLE static QStringList transportOptions(); + Q_INVOKABLE static QStringList fingerprintOptions(); + Q_INVOKABLE static QStringList alpnOptions(); + Q_INVOKABLE static QStringList xhttpModeOptions(); + Q_INVOKABLE static QStringList xhttpHeadersTemplateOptions(); + Q_INVOKABLE static QStringList xhttpUplinkMethodOptions(); + Q_INVOKABLE static QStringList xhttpSessionPlacementOptions(); + Q_INVOKABLE static QStringList xhttpSessionKeyOptions(); + Q_INVOKABLE static QStringList xhttpSeqPlacementOptions(); + Q_INVOKABLE static QStringList xhttpUplinkDataPlacementOptions(); + Q_INVOKABLE static QStringList xPaddingPlacementOptions(); + Q_INVOKABLE static QStringList xPaddingMethodOptions(); + + // mKCP display defaults (protocolConstants.h — must match xrayConfigurator empty-field behavior) + Q_INVOKABLE static QString mkcpDefaultTti(); + Q_INVOKABLE static QString mkcpDefaultUplinkCapacity(); + Q_INVOKABLE static QString mkcpDefaultDownlinkCapacity(); + Q_INVOKABLE static QString mkcpDefaultReadBufferSize(); + Q_INVOKABLE static QString mkcpDefaultWriteBufferSize(); public slots: - void updateModel(amnezia::DockerContainer container, const amnezia::XrayProtocolConfig &protocolConfig); + void updateModel(amnezia::DockerContainer container, const amnezia::XrayProtocolConfig& protocolConfig); amnezia::XrayProtocolConfig getProtocolConfig(); + bool isServerSettingsEqual() const; + bool hasUnsavedChanges() const; + void resetToDefaults(); + void applyServerConfig(const amnezia::XrayServerConfig &serverConfig); + +signals: + void hasUnsavedChangesChanged(); protected: QHash roleNames() const override; @@ -36,7 +136,7 @@ private: amnezia::DockerContainer m_container; amnezia::XrayProtocolConfig m_protocolConfig; amnezia::XrayProtocolConfig m_originalProtocolConfig; - + void applyDefaultsToServerConfig(amnezia::XrayServerConfig& config); }; diff --git a/client/ui/models/protocols/xrayConfigSnapshotsModel.cpp b/client/ui/models/protocols/xrayConfigSnapshotsModel.cpp new file mode 100644 index 000000000..8a023212f --- /dev/null +++ b/client/ui/models/protocols/xrayConfigSnapshotsModel.cpp @@ -0,0 +1,216 @@ +#include "xrayConfigSnapshotsModel.h" + +#include +#include + +#include "core/repositories/secureAppSettingsRepository.h" +#include "core/utils/constants/configKeys.h" + +QJsonObject XrayConfigSnapshot::toJson() const +{ + QJsonObject obj; + obj["id"] = id; + obj["displayName"] = displayName; + obj["createdAt"] = createdAt.toString(Qt::ISODate); + obj["serverConfig"] = serverConfig.toJson(); + return obj; +} + +XrayConfigSnapshot XrayConfigSnapshot::fromJson(const QJsonObject &json) +{ + XrayConfigSnapshot s; + s.id = json.value("id").toString(); + s.displayName = json.value("displayName").toString(); + s.createdAt = QDateTime::fromString(json.value("createdAt").toString(), Qt::ISODate); + s.serverConfig = amnezia::XrayServerConfig::fromJson(json.value("serverConfig").toObject()); + return s; +} + +XrayConfigSnapshotsModel::XrayConfigSnapshotsModel(SecureAppSettingsRepository *appSettings, + XrayConfigModel *xrayConfigModel, QObject *parent) + : QAbstractListModel(parent), m_appSettings(appSettings), m_xrayConfigModel(xrayConfigModel) +{ + loadAll(); +} + +void XrayConfigSnapshotsModel::loadAll() +{ + m_configs.clear(); + QByteArray raw = m_appSettings->xraySavedConfigs(); + if (raw.isEmpty()) { + return; + } + + QJsonArray arr = QJsonDocument::fromJson(raw).array(); + for (const QJsonValue &v : arr) { + m_configs.append(XrayConfigSnapshot::fromJson(v.toObject())); + } +} + +void XrayConfigSnapshotsModel::persistAll() +{ + QJsonArray arr; + for (const XrayConfigSnapshot &s : m_configs) { + arr.append(s.toJson()); + } + m_appSettings->setXraySavedConfigs(QJsonDocument(arr).toJson(QJsonDocument::Compact)); +} + +int XrayConfigSnapshotsModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return m_configs.size(); +} + +QVariant XrayConfigSnapshotsModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= m_configs.size()) { + return QVariant(); + } + + const XrayConfigSnapshot &s = m_configs.at(index.row()); + + switch (role) { + case IdRole: { + return s.id; + } + case DisplayNameRole: { + return s.displayName; + } + case CreatedAtRole: { + return s.createdAt.toString("dd.MM.yyyy HH:mm"); + } + } + return QVariant(); +} + +QHash XrayConfigSnapshotsModel::roleNames() const +{ + QHash roles; + roles[IdRole] = "configId"; + roles[DisplayNameRole] = "configName"; + roles[CreatedAtRole] = "configDate"; + return roles; +} + +void XrayConfigSnapshotsModel::reload() +{ + beginResetModel(); + loadAll(); + endResetModel(); +} + +void XrayConfigSnapshotsModel::createFromCurrent(const amnezia::XrayServerConfig &serverConfig) +{ + XrayConfigSnapshot snapshot; + snapshot.id = QUuid::createUuid().toString(QUuid::WithoutBraces); + snapshot.displayName = buildDisplayName(serverConfig); + snapshot.createdAt = QDateTime::currentDateTime(); + snapshot.serverConfig = serverConfig; + + beginInsertRows(QModelIndex(), m_configs.size(), m_configs.size()); + m_configs.append(snapshot); + endInsertRows(); + + persistAll(); +} + +amnezia::XrayServerConfig XrayConfigSnapshotsModel::applyConfig(int index) const +{ + if (index < 0 || index >= m_configs.size()) { + return amnezia::XrayServerConfig {}; + } + + return m_configs.at(index).serverConfig; +} + +void XrayConfigSnapshotsModel::removeConfig(int index) +{ + if (index < 0 || index >= m_configs.size()) { + return; + } + + beginRemoveRows(QModelIndex(), index, index); + m_configs.removeAt(index); + endRemoveRows(); + + persistAll(); + emit configRemoved(index); +} + +QString XrayConfigSnapshotsModel::exportToJson(int index) const +{ + if (index < 0 || index >= m_configs.size()) { + return {}; + } + return QString::fromUtf8(QJsonDocument(m_configs.at(index).toJson()).toJson(QJsonDocument::Indented)); +} + +bool XrayConfigSnapshotsModel::importFromJson(const QString &jsonString) +{ + QJsonDocument doc = QJsonDocument::fromJson(jsonString.toUtf8()); + if (!doc.isObject()) { + emit importFailed(tr("Invalid JSON format")); + return false; + } + + XrayConfigSnapshot snapshot = XrayConfigSnapshot::fromJson(doc.object()); + if (snapshot.id.isEmpty()) { + snapshot.id = QUuid::createUuid().toString(QUuid::WithoutBraces); + } + if (snapshot.displayName.isEmpty()) { + snapshot.displayName = buildDisplayName(snapshot.serverConfig); + } + snapshot.createdAt = QDateTime::currentDateTime(); + + beginInsertRows(QModelIndex(), m_configs.size(), m_configs.size()); + m_configs.append(snapshot); + endInsertRows(); + + persistAll(); + return true; +} + +QString XrayConfigSnapshotsModel::buildDisplayName(const amnezia::XrayServerConfig &cfg) +{ + // Build a human-readable name: "XHTTP TLS Reality", "RAW Reality", etc. + QString transport; + if (cfg.transport == "xhttp") { + transport = "XHTTP"; + } else if (cfg.transport == "mkcp") { + transport = "mKCP"; + } else { + transport = "RAW (TCP)"; + } + + QString security; + if (cfg.security == "tls") { + security = "TLS"; + } else if (cfg.security == "reality") { + security = "Reality"; + } else { + security = "None"; + } + + return QString("%1 %2").arg(transport, security).trimmed(); +} + +void XrayConfigSnapshotsModel::createFromCurrentModel() +{ + if (!m_xrayConfigModel) { + return; + } + createFromCurrent(m_xrayConfigModel->getProtocolConfig().serverConfig); +} + +void XrayConfigSnapshotsModel::applyConfigToCurrentModel(int index) +{ + if (!m_xrayConfigModel) { + return; + } + amnezia::XrayServerConfig cfg = applyConfig(index); + if (cfg.port.isEmpty()) { + return; // guard against invalid index + } + m_xrayConfigModel->applyServerConfig(cfg); +} diff --git a/client/ui/models/protocols/xrayConfigSnapshotsModel.h b/client/ui/models/protocols/xrayConfigSnapshotsModel.h new file mode 100644 index 000000000..9688cd863 --- /dev/null +++ b/client/ui/models/protocols/xrayConfigSnapshotsModel.h @@ -0,0 +1,76 @@ +#ifndef XRAYCONFIGSMODEL_H +#define XRAYCONFIGSMODEL_H + +#include +#include +#include +#include +#include +#include + +#include "core/models/protocols/xrayProtocolConfig.h" +#include "ui/models/protocols/xrayConfigModel.h" + +class SecureAppSettingsRepository; + +struct XrayConfigSnapshot +{ + QString id; + QString displayName; // auto-generated: "XHTTP TLS Reality", "RAW Reality", etc. + QDateTime createdAt; + amnezia::XrayServerConfig serverConfig; + + QJsonObject toJson() const; + static XrayConfigSnapshot fromJson(const QJsonObject &json); +}; + +class XrayConfigSnapshotsModel : public QAbstractListModel +{ + Q_OBJECT + +public: + enum Roles { + IdRole = Qt::UserRole + 1, + DisplayNameRole, + CreatedAtRole, // "dd.MM.yyyy HH:mm" + }; + + explicit XrayConfigSnapshotsModel(SecureAppSettingsRepository *appSettings, XrayConfigModel *xrayConfigModel, + QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + +public slots: + void reload(); + + Q_INVOKABLE void createFromCurrent(const amnezia::XrayServerConfig &serverConfig); + Q_INVOKABLE amnezia::XrayServerConfig applyConfig(int index) const; + Q_INVOKABLE void removeConfig(int index); + + Q_INVOKABLE QString exportToJson(int index) const; + Q_INVOKABLE bool importFromJson(const QString &jsonString); + + // Convenience: create snapshot from live model, apply snapshot back to model + Q_INVOKABLE void createFromCurrentModel(); + Q_INVOKABLE void applyConfigToCurrentModel(int index); + +signals: + void configApplied(int index); + void configRemoved(int index); + void importFailed(const QString &errorMessage); + +protected: + QHash roleNames() const override; + +private: + SecureAppSettingsRepository *m_appSettings; + XrayConfigModel *m_xrayConfigModel; + QVector m_configs; + + void persistAll(); + void loadAll(); + static QString buildDisplayName(const amnezia::XrayServerConfig &cfg); +}; + +#endif // XRAYCONFIGSMODEL_H diff --git a/client/ui/qml/Controls2/MinMaxRowType.qml b/client/ui/qml/Controls2/MinMaxRowType.qml new file mode 100644 index 000000000..90a7fae7b --- /dev/null +++ b/client/ui/qml/Controls2/MinMaxRowType.qml @@ -0,0 +1,61 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import Style 1.0 + +import "../Controls2" +import "../Controls2/TextTypes" + +// MinMaxRowType — two side-by-side labeled text fields: Min / Max +// Usage: +// MinMaxRowType { +// minValue: "0" +// maxValue: "0" +// onMinChanged: someProperty = val +// onMaxChanged: someProperty = val +// } +Item { + id: root + + property string minValue: "0" + property string maxValue: "0" + + signal minChanged(string val) + signal maxChanged(string val) + + implicitHeight: row.implicitHeight + implicitWidth: row.implicitWidth + + RowLayout { + id: row + anchors.fill: parent + spacing: 10 + + // Min field + TextFieldWithHeaderType { + Layout.fillWidth: true + headerText: qsTr("Min") + textField.text: root.minValue + textField.validator: IntValidator { bottom: 0 } + textField.onEditingFinished: { + if (textField.text !== root.minValue) { + root.minChanged(textField.text) + } + } + } + + // Max field + TextFieldWithHeaderType { + Layout.fillWidth: true + headerText: qsTr("Max") + textField.text: root.maxValue + textField.validator: IntValidator { bottom: 0 } + textField.onEditingFinished: { + if (textField.text !== root.maxValue) { + root.maxChanged(textField.text) + } + } + } + } +} diff --git a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml index e192535ac..903b94163 100644 --- a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml +++ b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml @@ -10,6 +10,7 @@ Item { id: root property string headerText + property string subtitleText // optional line under header (e.g. default value hint) property string headerTextDisabledColor: AmneziaStyle.color.charcoalGray property string headerTextColor: AmneziaStyle.color.mutedGray @@ -84,6 +85,15 @@ Item { Layout.fillWidth: true } + SmallTextType { + text: root.subtitleText + visible: root.subtitleText !== "" + color: AmneziaStyle.color.charcoalGray + font.pixelSize: 13 + Layout.fillWidth: true + Layout.topMargin: visible ? 2 : 0 + } + TextField { id: textField diff --git a/client/ui/qml/Pages2/PageProtocolXrayFlowSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayFlowSettings.qml new file mode 100644 index 000000000..afde16088 --- /dev/null +++ b/client/ui/qml/Pages2/PageProtocolXrayFlowSettings.qml @@ -0,0 +1,125 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import PageEnum 1.0 +import ProtocolEnum 1.0 +import Style 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" +import "../Components" + +PageType { + id: root + + BackButtonType { + id: backButton + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 20 + PageController.safeAreaTopMargin + } + + ListViewType { + id: listView + anchors.top: backButton.bottom + anchors.bottom: saveButton.visible ? saveButton.top : parent.bottom + anchors.left: parent.left + anchors.right: parent.right + + enabled: ServersUiController.isProcessedServerHasWriteAccess() + model: XrayConfigModel + + delegate: ColumnLayout { + width: listView.width + spacing: 0 + + BaseHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 0 + Layout.bottomMargin: 24 + headerText: qsTr("Flow") + } + + VerticalRadioButton { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: qsTr("Empty") + checked: flow === "" + onClicked: flow = "" + } + + DividerType { + } + + VerticalRadioButton { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: "xtls-rprx-vision" + checked: flow === "xtls-rprx-vision" + onClicked: flow = "xtls-rprx-vision" + } + + DividerType { + } + + VerticalRadioButton { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: "xtls-rprx-vision-udp443" + checked: flow === "xtls-rprx-vision-udp443" + onClicked: flow = "xtls-rprx-vision-udp443" + } + + DividerType { + } + + Item { + Layout.preferredHeight: 16 + } + } + } + + BasicButtonType { + id: saveButton + + anchors.left: root.left + anchors.right: root.right + anchors.bottom: root.bottom + anchors.leftMargin: 16 + anchors.rightMargin: 16 + anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin + + visible: listView.enabled && XrayConfigModel.hasUnsavedChanges + enabled: visible + text: qsTr("Save") + clickedFunc: function () { + var headerText = qsTr("Save settings?") + var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + var yesButtonFunction = function () { + if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) { + PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection")) + return + } + PageController.goToPage(PageEnum.PageSetupWizardInstalling) + InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray) + } + var noButtonFunction = function () { + if (typeof GC !== "undefined" && !GC.isMobile()) { + saveButton.forceActiveFocus() + } + } + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + } + } +} diff --git a/client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml b/client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml new file mode 100644 index 000000000..fc1a58de1 --- /dev/null +++ b/client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml @@ -0,0 +1,292 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import PageEnum 1.0 +import ProtocolEnum 1.0 +import Style 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" +import "../Components" + +PageType { + id: root + + BackButtonType { + id: backButton + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 20 + PageController.safeAreaTopMargin + } + + ListViewType { + id: listView + anchors.top: backButton.bottom + anchors.bottom: saveButton.visible ? saveButton.top : parent.bottom + anchors.left: parent.left + anchors.right: parent.right + + enabled: ServersUiController.isProcessedServerHasWriteAccess() + model: XrayConfigModel + + delegate: ColumnLayout { + width: listView.width + spacing: 0 + + BaseHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 0 + Layout.bottomMargin: 24 + headerText: qsTr("Security") + } + + VerticalRadioButton { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: qsTr("None") + checked: security === "none" + onClicked: security = "none" + } + + DividerType { + } + + VerticalRadioButton { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: qsTr("TLS") + checked: security === "tls" + onClicked: security = "tls" + } + + DividerType { + } + + VerticalRadioButton { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: qsTr("Reality") + checked: security === "reality" + onClicked: security = "reality" + } + + DividerType { + } + + // ── TLS fields ──────────────────────────────────────────── + ColumnLayout { + visible: security === "tls" + Layout.fillWidth: true + spacing: 0 + + DropDownType { + id: tlsAlpnDropDown + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: alpn + descriptionText: qsTr("ALPN") + headerText: qsTr("ALPN") + drawerParent: root + listView: ListViewWithRadioButtonType { + rootWidth: root.width + model: ListModel { + Component.onCompleted: { + var opts = XrayConfigModel.alpnOptions() + for (var i = 0; i < opts.length; i++) { + append({name: opts[i]}) + } + } + } + clickedFunction: function () { + alpn = selectedText + tlsAlpnDropDown.text = selectedText + tlsAlpnDropDown.closeTriggered() + } + Component.onCompleted: { + for (var i = 0; i < model.count; i++) { + if (model.get(i).name === alpn) { + selectedIndex = i; + break + } + } + } + } + Connections { + target: XrayConfigModel + + function onDataChanged() { + tlsAlpnDropDown.text = alpn + } + } + } + + DropDownType { + id: tlsFingerprintDropDown + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: fingerprint + descriptionText: qsTr("Fingerprint") + headerText: qsTr("Fingerprint") + drawerParent: root + listView: ListViewWithRadioButtonType { + rootWidth: root.width + model: ListModel { + Component.onCompleted: { + var opts = XrayConfigModel.fingerprintOptions() + for (var i = 0; i < opts.length; i++) { + append({name: opts[i]}) + } + } + } + clickedFunction: function () { + fingerprint = selectedText + tlsFingerprintDropDown.text = selectedText + tlsFingerprintDropDown.closeTriggered() + } + Component.onCompleted: { + for (var i = 0; i < model.count; i++) { + if (model.get(i).name === fingerprint) { + selectedIndex = i; + break + } + } + } + } + Connections { + target: XrayConfigModel + + function onDataChanged() { + tlsFingerprintDropDown.text = fingerprint + } + } + } + + TextFieldWithHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + headerText: qsTr("Server Name (SNI)") + textField.text: sni + textField.onEditingFinished: { + if (textField.text !== sni) sni = textField.text + } + } + } + + // ── Reality fields ──────────────────────────────────────── + ColumnLayout { + visible: security === "reality" + Layout.fillWidth: true + spacing: 0 + + DropDownType { + id: realityFingerprintDropDown + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: fingerprint + descriptionText: qsTr("Fingerprint") + headerText: qsTr("Fingerprint") + drawerParent: root + listView: ListViewWithRadioButtonType { + rootWidth: root.width + model: ListModel { + Component.onCompleted: { + var opts = XrayConfigModel.fingerprintOptions() + for (var i = 0; i < opts.length; i++) { + append({name: opts[i]}) + } + } + } + clickedFunction: function () { + fingerprint = selectedText + realityFingerprintDropDown.text = selectedText + realityFingerprintDropDown.closeTriggered() + } + Component.onCompleted: { + for (var i = 0; i < model.count; i++) { + if (model.get(i).name === fingerprint) { + selectedIndex = i; + break + } + } + } + } + Connections { + target: XrayConfigModel + + function onDataChanged() { + realityFingerprintDropDown.text = fingerprint + } + } + } + + TextFieldWithHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + headerText: qsTr("Server Name (SNI)") + textField.text: sni + textField.onEditingFinished: { + if (textField.text !== sni) sni = textField.text + } + } + } + + Item { + Layout.preferredHeight: 16 + } + } + } + + BasicButtonType { + id: saveButton + + anchors.left: root.left + anchors.right: root.right + anchors.bottom: root.bottom + anchors.leftMargin: 16 + anchors.rightMargin: 16 + anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin + + visible: listView.enabled && XrayConfigModel.hasUnsavedChanges + enabled: visible + text: qsTr("Save") + clickedFunc: function () { + var headerText = qsTr("Save settings?") + var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + var yesButtonFunction = function () { + if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) { + PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection")) + return + } + PageController.goToPage(PageEnum.PageSetupWizardInstalling) + InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray) + } + var noButtonFunction = function () { + if (typeof GC !== "undefined" && !GC.isMobile()) { + saveButton.forceActiveFocus() + } + } + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + } + } +} diff --git a/client/ui/qml/Pages2/PageProtocolXraySettings.qml b/client/ui/qml/Pages2/PageProtocolXraySettings.qml index 43c57caff..c63d58eca 100644 --- a/client/ui/qml/Pages2/PageProtocolXraySettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXraySettings.qml @@ -17,6 +17,20 @@ import "../Components" PageType { id: root + function formatTransport(value) { + if (value === "raw") return "RAW (TCP)" + if (value === "xhttp") return "XHTTP" + if (value === "mkcp") return "mKCP" + return value + } + + function formatSecurity(value) { + if (value === "none") return "None" + if (value === "tls") return "TLS" + if (value === "reality") return "Reality" + return value + } + BackButtonType { id: backButton @@ -50,88 +64,125 @@ PageType { spacing: 0 - BaseHeaderType { + RowLayout { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 - headerText: qsTr("XRay settings") + Layout.topMargin: 0 + + BaseHeaderType { + Layout.fillWidth: true + headerText: qsTr("XRay VLESS settings") + } + + ImageButtonType { + Layout.alignment: Qt.AlignTop | Qt.AlignRight + implicitWidth: 40 + implicitHeight: 40 + image: "qrc:/images/controls/more-vertical.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: PageController.goToPage(PageEnum.PageProtocolXraySnapshots) + } + } + + LabelTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 4 + text: qsTr("More about settings") + color: AmneziaStyle.color.goldenApricot + font.pixelSize: 16 + lineHeight: 24 + LanguageUiController.getLineHeightAppend() + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: Qt.openUrlExternally("https://docs.amnezia.org") + } } TextFieldWithHeaderType { id: textFieldWithHeaderType - Layout.fillWidth: true Layout.topMargin: 32 Layout.leftMargin: 16 Layout.rightMargin: 16 - enabled: listView.enabled - - headerText: qsTr("Disguised as traffic from") - textField.text: site - - textField.onEditingFinished: { - if (textField.text !== site) { - var tmpText = textField.text - tmpText = tmpText.toLocaleLowerCase() - - if (tmpText.startsWith("https://")) { - tmpText = textField.text.substring(8) - site = tmpText - } else { - site = textField.text - } - } - } - - checkEmptyText: true - } - - TextFieldWithHeaderType { - id: portTextField - - Layout.fillWidth: true - Layout.topMargin: 16 - Layout.leftMargin: 16 - Layout.rightMargin: 16 - - enabled: listView.enabled - headerText: qsTr("Port") textField.text: port textField.maximumLength: 5 - textField.validator: IntValidator { bottom: 1; top: 65535 } - - textField.onEditingFinished: { - if (textField.text !== port) { - port = textField.text - } + textField.validator: IntValidator { + bottom: 1; top: 65535 + } + textField.onEditingFinished: { + if (textField.text !== port) port = textField.text } - checkEmptyText: true } + LabelWithButtonType { + Layout.fillWidth: true + Layout.topMargin: 16 + text: qsTr("Transport") + descriptionText: root.formatTransport(transport) + rightImageSource: "qrc:/images/controls/chevron-right.svg" + enabled: listView.enabled + clickedFunction: function() { + PageController.goToPage(PageEnum.PageProtocolXrayTransportSettings) + } + } + + DividerType { + } + + LabelWithButtonType { + Layout.fillWidth: true + text: qsTr("Security") + descriptionText: root.formatSecurity(security) + rightImageSource: "qrc:/images/controls/chevron-right.svg" + enabled: listView.enabled + clickedFunction: function() { + PageController.goToPage(PageEnum.PageProtocolXraySecuritySettings) + } + } + + DividerType { + } + + LabelWithButtonType { + Layout.fillWidth: true + text: qsTr("Flow") + descriptionText: flow + rightImageSource: "qrc:/images/controls/chevron-right.svg" + enabled: listView.enabled + clickedFunction: function() { + PageController.goToPage(PageEnum.PageProtocolXrayFlowSettings) + } + } + + DividerType { + } + + Item { + Layout.fillWidth: true; Layout.preferredHeight: 24 + } + BasicButtonType { id: saveButton - Layout.fillWidth: true - Layout.topMargin: 24 - Layout.bottomMargin: 24 + Layout.bottomMargin: 8 Layout.leftMargin: 16 Layout.rightMargin: 16 - - enabled: portTextField.errorText === "" - + // Show Save immediately while user edits port, even before focus loss. + visible: listView.enabled && (XrayConfigModel.hasUnsavedChanges || textFieldWithHeaderType.textField.text !== port) + enabled: visible && textFieldWithHeaderType.errorText === "" text: qsTr("Save") - onClicked: function() { forceActiveFocus() - var headerText = qsTr("Save settings?") var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.") var yesButtonText = qsTr("Continue") var noButtonText = qsTr("Cancel") - var yesButtonFunction = function() { if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) { PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection")) @@ -142,16 +193,32 @@ PageType { InstallController.updateContainer(ServersUiController.getServerId(ServersUiController.processedServerIndex), ServersUiController.processedContainerIndex, ProtocolEnum.Xray) } var noButtonFunction = function() { - if (!GC.isMobile()) { - saveButton.forceActiveFocus() - } + if (!GC.isMobile()) saveButton.forceActiveFocus() } showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) } - Keys.onEnterPressed: saveButton.clicked() Keys.onReturnPressed: saveButton.clicked() } + + LabelWithButtonType { + Layout.fillWidth: true + text: qsTr("Reset settings") + textColor: AmneziaStyle.color.vibrantRed + visible: listView.enabled + clickedFunction: function() { + var yesButtonFunction = function() { + XrayConfigModel.resetToDefaults() + } + showQuestionDrawer(qsTr("Reset settings?"), qsTr("All XRay settings will be restored to defaults."), + qsTr("Reset"), qsTr("Cancel"), yesButtonFunction, function() { + }) + } + } + + Item { + Layout.fillWidth: true; Layout.preferredHeight: 32 + } } } } diff --git a/client/ui/qml/Pages2/PageProtocolXraySnapshots.qml b/client/ui/qml/Pages2/PageProtocolXraySnapshots.qml new file mode 100644 index 000000000..446ad468a --- /dev/null +++ b/client/ui/qml/Pages2/PageProtocolXraySnapshots.qml @@ -0,0 +1,291 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import PageEnum 1.0 +import Style 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" +import "../Components" +import Qt.labs.platform 1.1 + +PageType { + id: root + + property string selectedConfigName: "" + property int selectedConfigIndex: -1 + + // Reload the list every time we open this page + Component.onCompleted: XrayConfigSnapshotsModel.reload() + + // ── Save xray config snapshot to file ──────────────────────────── + function saveConfigToFile(json) { + var fileName = "" + if (GC.isMobile()) { + fileName = "amnezia_xray_config.json" + } else { + fileName = SystemController.getFileName( + qsTr("Save XRay configuration"), + qsTr("JSON files (*.json)"), + StandardPaths.standardLocations(StandardPaths.DocumentsLocation) + "/amnezia_xray_config", + true, + ".json") + } + if (fileName !== "") { + PageController.showBusyIndicator(true) + ExportController.setConfigFromString(json, fileName) + PageController.showBusyIndicator(false) + PageController.showNotificationMessage(qsTr("Configuration saved")) + } + } + + + BackButtonType { + id: backButton + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 20 + PageController.safeAreaTopMargin + } + + ListViewType { + id: listView + anchors.top: backButton.bottom + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + + model: XrayConfigSnapshotsModel + + header: ColumnLayout { + width: listView.width + spacing: 0 + + BaseHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 0 + Layout.bottomMargin: 24 + headerText: qsTr("XRay Configurations") + } + + // ── Create from current settings ────────────────────────── + LabelWithButtonType { + Layout.fillWidth: true + text: qsTr("Create configuration based on current settings") + textMaximumLineCount: 2 + rightImageSource: "qrc:/images/controls/chevron-right.svg" + clickedFunction: function () { + XrayConfigSnapshotsModel.createFromCurrentModel() + } + } + + DividerType { + } + + // ── Export ──────────────────────────────────────────────── + LabelWithButtonType { + Layout.fillWidth: true + text: qsTr("Export settings") + rightImageSource: "qrc:/images/controls/chevron-right.svg" + clickedFunction: function () { + var idx = root.selectedConfigIndex >= 0 ? root.selectedConfigIndex : 0 + if (listView.count > 0) { + var json = XrayConfigSnapshotsModel.exportToJson(idx) + saveConfigToFile(json) + } + } + } + + DividerType { + } + + // ── Import ──────────────────────────────────────────────── + LabelWithButtonType { + Layout.fillWidth: true + text: qsTr("Import settings") + descriptionText: qsTr("In JSON format") + rightImageSource: "qrc:/images/controls/chevron-right.svg" + clickedFunction: function () { + var filePath = SystemController.getFileName( + qsTr("Open XRay configuration"), + qsTr("JSON files (*.json)")) + if (filePath !== "") { + var jsonContent = ImportController.readTextFile(filePath) + if (jsonContent !== "") { + if (!XrayConfigSnapshotsModel.importFromJson(jsonContent)) { + PageController.showNotificationMessage(qsTr("Failed to import configuration")) + } else { + PageController.showNotificationMessage(qsTr("Configuration imported successfully")) + } + } + } + } + } + + DividerType { + } + + // ── Section label ───────────────────────────────────────── + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 24 + Layout.bottomMargin: 8 + text: qsTr("Configurations") + color: AmneziaStyle.color.mutedGray + visible: listView.count > 0 + } + } + + // ── Empty state ─────────────────────────────────────────────── + footer: ColumnLayout { + width: listView.width + visible: listView.count === 0 + spacing: 0 + + Item { + Layout.preferredHeight: 32 + } + + ParagraphTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: qsTr("No saved configurations yet.\nCreate one from the current settings.") + color: AmneziaStyle.color.mutedGray + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + } + } + + // ── Config list items ───────────────────────────────────────── + delegate: ColumnLayout { + width: listView.width + spacing: 0 + + LabelWithButtonType { + Layout.fillWidth: true + text: configName + descriptionText: configDate + rightImageSource: "qrc:/images/controls/more-vertical.svg" + clickedFunction: function () { + root.selectedConfigName = configName + root.selectedConfigIndex = index + configActionsDrawer.openTriggered() + } + } + + DividerType { + } + } + } + + // ── Import result handler ───────────────────────────────────────── + Connections { + target: XrayConfigSnapshotsModel + + function onImportFailed(errorMessage) { + PageController.showNotificationMessage(errorMessage) + } + } + + // ── Per-config actions drawer ───────────────────────────────────── + DrawerType2 { + id: configActionsDrawer + parent: root + anchors.fill: parent + expandedHeight: root.height * 0.35 + + expandedStateContent: ColumnLayout { + id: drawerContent + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + spacing: 0 + + onImplicitHeightChanged: { + configActionsDrawer.expandedHeight = drawerContent.implicitHeight + 32 + } + + BackButtonType { + Layout.fillWidth: true + Layout.topMargin: 16 + backButtonFunction: function () { + configActionsDrawer.closeTriggered() + } + } + + BaseHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + Layout.bottomMargin: 16 + headerText: root.selectedConfigName + } + + // Apply + LabelWithButtonType { + Layout.fillWidth: true + text: qsTr("Apply configuration") + rightImageSource: "qrc:/images/controls/chevron-right.svg" + clickedFunction: function () { + configActionsDrawer.closeTriggered() + XrayConfigSnapshotsModel.applyConfigToCurrentModel(root.selectedConfigIndex) + PageController.closePage() + } + } + + DividerType { + } + + // Export this config + LabelWithButtonType { + Layout.fillWidth: true + text: qsTr("Export configuration") + rightImageSource: "qrc:/images/controls/chevron-right.svg" + clickedFunction: function () { + configActionsDrawer.closeTriggered() + var json = XrayConfigSnapshotsModel.exportToJson(root.selectedConfigIndex) + saveConfigToFile(json) + } + } + + DividerType { + } + + // Delete + LabelWithButtonType { + Layout.fillWidth: true + text: qsTr("Delete configuration") + textColor: AmneziaStyle.color.vibrantRed + clickedFunction: function () { + configActionsDrawer.closeTriggered() + var yesButtonFunction = function () { + XrayConfigSnapshotsModel.removeConfig(root.selectedConfigIndex) + root.selectedConfigIndex = -1 + root.selectedConfigName = "" + } + showQuestionDrawer( + qsTr("Delete configuration?"), + qsTr("This action cannot be undone."), + qsTr("Delete"), qsTr("Cancel"), + yesButtonFunction, function () { + }) + } + } + + DividerType { + } + Item { + Layout.preferredHeight: 16 + } + } + } +} diff --git a/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml new file mode 100644 index 000000000..bbda7eb87 --- /dev/null +++ b/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml @@ -0,0 +1,755 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import PageEnum 1.0 +import ProtocolEnum 1.0 +import Style 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" +import "../Components" + +PageType { + id: root + + BackButtonType { + id: backButton + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 20 + PageController.safeAreaTopMargin + } + + ListViewType { + id: listView + anchors.top: backButton.bottom + anchors.bottom: saveButton.visible ? saveButton.top : parent.bottom + anchors.left: parent.left + anchors.right: parent.right + + enabled: ServersUiController.isProcessedServerHasWriteAccess() + model: XrayConfigModel + + delegate: ColumnLayout { + width: listView.width + spacing: 0 + + BaseHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 0 + Layout.bottomMargin: 24 + headerText: qsTr("Transport") + } + + // ── Radio buttons ───────────────────────────────────────── + VerticalRadioButton { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: qsTr("RAW (TCP)") + checked: transport === "raw" + onToggled: if (checked && transport !== "raw") transport = "raw" + } + + DividerType { + } + + VerticalRadioButton { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: qsTr("XHTTP") + descriptionText: qsTr("Advanced users") + checked: transport === "xhttp" + onToggled: if (checked && transport !== "xhttp") transport = "xhttp" + } + + DividerType { + } + + VerticalRadioButton { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: qsTr("mKCP") + checked: transport === "mkcp" + onToggled: if (checked && transport !== "mkcp") transport = "mkcp" + } + + DividerType { + } + + // ══════════════════════════════════════════════════════════ + // mKCP Settings + // ══════════════════════════════════════════════════════════ + ColumnLayout { + visible: transport === "mkcp" + Layout.fillWidth: true + spacing: 0 + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 24 + Layout.bottomMargin: 8 + text: qsTr("mKCP Settings") + color: AmneziaStyle.color.mutedGray + } + + TextFieldWithHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + headerText: qsTr("TTI") + subtitleText: qsTr("Default: %1 ms", "mKCP TTI").arg(XrayConfigModel.mkcpDefaultTti()) + textField.text: mkcpTti + textField.onEditingFinished: { + if (textField.text !== mkcpTti) mkcpTti = textField.text + } + } + + TextFieldWithHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + headerText: qsTr("uplinkCapacity") + subtitleText: qsTr("Default: %1 Mbit/s", "mKCP uplink").arg(XrayConfigModel.mkcpDefaultUplinkCapacity()) + textField.text: mkcpUplinkCapacity + textField.onEditingFinished: { + if (textField.text !== mkcpUplinkCapacity) mkcpUplinkCapacity = textField.text + } + } + + TextFieldWithHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + headerText: qsTr("downlinkCapacity") + subtitleText: qsTr("Default: %1 Mbit/s", "mKCP downlink").arg(XrayConfigModel.mkcpDefaultDownlinkCapacity()) + textField.text: mkcpDownlinkCapacity + textField.onEditingFinished: { + if (textField.text !== mkcpDownlinkCapacity) mkcpDownlinkCapacity = textField.text + } + } + + TextFieldWithHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + headerText: qsTr("readBufferSize") + subtitleText: qsTr("Default: %1 MiB").arg(XrayConfigModel.mkcpDefaultReadBufferSize()) + textField.text: mkcpReadBufferSize + textField.onEditingFinished: { + if (textField.text !== mkcpReadBufferSize) mkcpReadBufferSize = textField.text + } + } + + TextFieldWithHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + headerText: qsTr("writeBufferSize") + subtitleText: qsTr("Default: %1 MiB").arg(XrayConfigModel.mkcpDefaultWriteBufferSize()) + textField.text: mkcpWriteBufferSize + textField.onEditingFinished: { + if (textField.text !== mkcpWriteBufferSize) mkcpWriteBufferSize = textField.text + } + } + + SwitcherType { + Layout.fillWidth: true + Layout.margins: 16 + Layout.topMargin: 8 + text: qsTr("Congestion") + checked: mkcpCongestion + onToggled: mkcpCongestion = checked + } + } + + // ══════════════════════════════════════════════════════════ + // XHTTP Settings + // ══════════════════════════════════════════════════════════ + ColumnLayout { + visible: transport === "xhttp" + Layout.fillWidth: true + spacing: 0 + + DropDownType { + id: modeDropDown + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: xhttpMode + descriptionText: qsTr("Mode") + headerText: qsTr("Mode") + drawerParent: root + listView: ListViewWithRadioButtonType { + rootWidth: root.width + model: ListModel { + Component.onCompleted: { + var opts = XrayConfigModel.xhttpModeOptions() + for (var i = 0; i < opts.length; i++) { + append({name: opts[i]}) + } + } + } + clickedFunction: function () { + xhttpMode = selectedText + modeDropDown.text = selectedText + modeDropDown.closeTriggered() + } + Component.onCompleted: { + for (var i = 0; i < model.count; i++) { + if (model.get(i).name === xhttpMode) { + selectedIndex = i; + break + } + } + } + } + Connections { + target: XrayConfigModel + + function onDataChanged() { + modeDropDown.text = xhttpMode + } + } + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 24 + Layout.bottomMargin: 8 + text: qsTr("HTTP Profile") + color: AmneziaStyle.color.mutedGray + } + + TextFieldWithHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + headerText: qsTr("Host") + textField.text: xhttpHost + textField.onEditingFinished: { + if (textField.text !== xhttpHost) xhttpHost = textField.text + } + } + + TextFieldWithHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + headerText: qsTr("Path") + textField.text: xhttpPath + textField.onEditingFinished: { + if (textField.text !== xhttpPath) xhttpPath = textField.text + } + } + + DropDownType { + id: headersDropDown + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: xhttpHeadersTemplate + descriptionText: qsTr("Headers template") + headerText: qsTr("Headers template") + drawerParent: root + listView: ListViewWithRadioButtonType { + rootWidth: root.width + model: ListModel { + Component.onCompleted: { + var opts = XrayConfigModel.xhttpHeadersTemplateOptions() + for (var i = 0; i < opts.length; i++) { + append({name: opts[i]}) + } + } + } + clickedFunction: function () { + xhttpHeadersTemplate = selectedText + headersDropDown.text = selectedText + headersDropDown.closeTriggered() + } + Component.onCompleted: { + for (var i = 0; i < model.count; i++) { + if (model.get(i).name === xhttpHeadersTemplate) { + selectedIndex = i; + break + } + } + } + } + Connections { + target: XrayConfigModel + + function onDataChanged() { + headersDropDown.text = xhttpHeadersTemplate + } + } + } + + DropDownType { + id: uplinkMethodDropDown + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: xhttpUplinkMethod + descriptionText: qsTr("UplinkHTTPMethod") + headerText: qsTr("UplinkHTTPMethod") + drawerParent: root + listView: ListViewWithRadioButtonType { + rootWidth: root.width + model: ListModel { + Component.onCompleted: { + var opts = XrayConfigModel.xhttpUplinkMethodOptions() + for (var i = 0; i < opts.length; i++) { + append({name: opts[i]}) + } + } + } + clickedFunction: function () { + xhttpUplinkMethod = selectedText + uplinkMethodDropDown.text = selectedText + uplinkMethodDropDown.closeTriggered() + } + Component.onCompleted: { + for (var i = 0; i < model.count; i++) { + if (model.get(i).name === xhttpUplinkMethod) { + selectedIndex = i; + break + } + } + } + } + Connections { + target: XrayConfigModel + + function onDataChanged() { + uplinkMethodDropDown.text = xhttpUplinkMethod + } + } + } + + SwitcherType { + Layout.fillWidth: true + Layout.margins: 16 + Layout.topMargin: 16 + text: qsTr("Disable gRPC Header") + descriptionText: qsTr("noGRPCHeader") + checked: xhttpDisableGrpc + onToggled: xhttpDisableGrpc = checked + } + + DividerType { + } + + SwitcherType { + Layout.fillWidth: true + Layout.margins: 16 + text: qsTr("Disable SSE Header") + descriptionText: qsTr("noSSEHeader") + checked: xhttpDisableSse + onToggled: xhttpDisableSse = checked + } + + DividerType { + } + + // ── Session & Sequence ──────────────────────────────── + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 24 + Layout.bottomMargin: 8 + text: qsTr("Session & Sequence") + color: AmneziaStyle.color.mutedGray + } + + DropDownType { + id: sessionPlacementDropDown + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: xhttpSessionPlacement + descriptionText: qsTr("SessionPlacement") + headerText: qsTr("SessionPlacement") + drawerParent: root + listView: ListViewWithRadioButtonType { + rootWidth: root.width + model: ListModel { + Component.onCompleted: { + var opts = XrayConfigModel.xhttpSessionPlacementOptions() + for (var i = 0; i < opts.length; i++) { + append({name: opts[i]}) + } + } + } + clickedFunction: function () { + xhttpSessionPlacement = selectedText + sessionPlacementDropDown.text = selectedText + sessionPlacementDropDown.closeTriggered() + } + Component.onCompleted: { + for (var i = 0; i < model.count; i++) { + if (model.get(i).name === xhttpSessionPlacement) { + selectedIndex = i; + break + } + } + } + } + Connections { + target: XrayConfigModel + + function onDataChanged() { + sessionPlacementDropDown.text = xhttpSessionPlacement + } + } + } + + DropDownType { + id: sessionKeyDropDown + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: xhttpSessionKey + descriptionText: qsTr("SessionKey") + headerText: qsTr("SessionKey") + drawerParent: root + listView: ListViewWithRadioButtonType { + rootWidth: root.width + model: ListModel { + Component.onCompleted: { + var opts = XrayConfigModel.xhttpSessionKeyOptions() + for (var i = 0; i < opts.length; i++) { + append({name: opts[i]}) + } + } + } + clickedFunction: function () { + xhttpSessionKey = selectedText + sessionKeyDropDown.text = selectedText + sessionKeyDropDown.closeTriggered() + } + Component.onCompleted: { + for (var i = 0; i < model.count; i++) { + if (model.get(i).name === xhttpSessionKey) { + selectedIndex = i; + break + } + } + } + } + Connections { + target: XrayConfigModel + + function onDataChanged() { + sessionKeyDropDown.text = xhttpSessionKey + } + } + } + + DropDownType { + id: seqPlacementDropDown + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: xhttpSeqPlacement + descriptionText: qsTr("SeqPlacement") + headerText: qsTr("SeqPlacement") + drawerParent: root + listView: ListViewWithRadioButtonType { + rootWidth: root.width + model: ListModel { + Component.onCompleted: { + var opts = XrayConfigModel.xhttpSeqPlacementOptions() + for (var i = 0; i < opts.length; i++) { + append({name: opts[i]}) + } + } + } + clickedFunction: function () { + xhttpSeqPlacement = selectedText + seqPlacementDropDown.text = selectedText + seqPlacementDropDown.closeTriggered() + } + Component.onCompleted: { + for (var i = 0; i < model.count; i++) { + if (model.get(i).name === xhttpSeqPlacement) { + selectedIndex = i; + break + } + } + } + } + Connections { + target: XrayConfigModel + + function onDataChanged() { + seqPlacementDropDown.text = xhttpSeqPlacement + } + } + } + + TextFieldWithHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + headerText: qsTr("SeqKey") + textField.text: xhttpSeqKey + textField.onEditingFinished: { + if (textField.text !== xhttpSeqKey) xhttpSeqKey = textField.text + } + } + + DropDownType { + id: uplinkDataPlacementDropDown + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: xhttpUplinkDataPlacement + descriptionText: qsTr("UplinkDataPlacement") + headerText: qsTr("UplinkDataPlacement") + drawerParent: root + listView: ListViewWithRadioButtonType { + rootWidth: root.width + model: ListModel { + Component.onCompleted: { + var opts = XrayConfigModel.xhttpUplinkDataPlacementOptions() + for (var i = 0; i < opts.length; i++) { + append({name: opts[i]}) + } + } + } + clickedFunction: function () { + xhttpUplinkDataPlacement = selectedText + uplinkDataPlacementDropDown.text = selectedText + uplinkDataPlacementDropDown.closeTriggered() + } + Component.onCompleted: { + for (var i = 0; i < model.count; i++) { + if (model.get(i).name === xhttpUplinkDataPlacement) { + selectedIndex = i; + break + } + } + } + } + Connections { + target: XrayConfigModel + + function onDataChanged() { + uplinkDataPlacementDropDown.text = xhttpUplinkDataPlacement + } + } + } + + TextFieldWithHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + headerText: qsTr("UplinkDataKey") + textField.text: xhttpUplinkDataKey + textField.onEditingFinished: { + if (textField.text !== xhttpUplinkDataKey) xhttpUplinkDataKey = textField.text + } + } + + // ── Traffic Shaping ─────────────────────────────────── + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 24 + Layout.bottomMargin: 8 + text: qsTr("Traffic Shaping") + color: AmneziaStyle.color.mutedGray + } + + TextFieldWithHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + headerText: qsTr("UplinkChunkSize") + textField.text: xhttpUplinkChunkSize + textField.validator: IntValidator { + bottom: 0 + } + textField.onEditingFinished: { + if (textField.text !== xhttpUplinkChunkSize) xhttpUplinkChunkSize = textField.text + } + } + + TextFieldWithHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + headerText: qsTr("scMaxBufferedPosts") + textField.text: xhttpScMaxBufferedPosts + textField.onEditingFinished: { + if (textField.text !== xhttpScMaxBufferedPosts) xhttpScMaxBufferedPosts = textField.text + } + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 16 + Layout.bottomMargin: 8 + text: qsTr("scMaxEachPostBytes") + color: AmneziaStyle.color.mutedGray + } + MinMaxRowType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + minValue: xhttpScMaxEachPostBytesMin + maxValue: xhttpScMaxEachPostBytesMax + onMinChanged: xhttpScMaxEachPostBytesMin = val + onMaxChanged: xhttpScMaxEachPostBytesMax = val + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 16 + Layout.bottomMargin: 8 + text: qsTr("scStreamUpServerSecs") + color: AmneziaStyle.color.mutedGray + } + MinMaxRowType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + minValue: xhttpScStreamUpServerSecsMin + maxValue: xhttpScStreamUpServerSecsMax + onMinChanged: xhttpScStreamUpServerSecsMin = val + onMaxChanged: xhttpScStreamUpServerSecsMax = val + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 16 + Layout.bottomMargin: 8 + text: qsTr("scMinPostsIntervalMs") + color: AmneziaStyle.color.mutedGray + } + MinMaxRowType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + minValue: xhttpScMinPostsIntervalMsMin + maxValue: xhttpScMinPostsIntervalMsMax + onMinChanged: xhttpScMinPostsIntervalMsMin = val + onMaxChanged: xhttpScMinPostsIntervalMsMax = val + } + + // ── Padding and multiplexing ────────────────────────── + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 24 + Layout.bottomMargin: 8 + text: qsTr("Padding and multiplexing") + color: AmneziaStyle.color.mutedGray + } + + LabelWithButtonType { + Layout.fillWidth: true + text: qsTr("xPadding") + rightImageSource: "qrc:/images/controls/chevron-right.svg" + clickedFunction: function () { + PageController.goToPage(PageEnum.PageProtocolXrayXPaddingSettings) + } + } + + DividerType { + } + + LabelWithButtonType { + Layout.fillWidth: true + text: qsTr("XMux") + descriptionText: xmuxEnabled ? qsTr("On") : qsTr("Off") + rightImageSource: "qrc:/images/controls/chevron-right.svg" + clickedFunction: function () { + PageController.goToPage(PageEnum.PageProtocolXrayXmuxSettings) + } + } + + DividerType { + } + } + + Item { + Layout.preferredHeight: 16 + } + } + } + + BasicButtonType { + id: saveButton + + anchors.left: root.left + anchors.right: root.right + anchors.bottom: root.bottom + anchors.leftMargin: 16 + anchors.rightMargin: 16 + anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin + + visible: listView.enabled && XrayConfigModel.hasUnsavedChanges + enabled: visible + text: qsTr("Save") + clickedFunc: function () { + var headerText = qsTr("Save settings?") + var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + var yesButtonFunction = function () { + if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) { + PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection")) + return + } + PageController.goToPage(PageEnum.PageSetupWizardInstalling) + InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray) + } + var noButtonFunction = function () { + if (typeof GC !== "undefined" && !GC.isMobile()) { + saveButton.forceActiveFocus() + } + } + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + } + } +} diff --git a/client/ui/qml/Pages2/PageProtocolXrayXPaddingBytesSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayXPaddingBytesSettings.qml new file mode 100644 index 000000000..026c8cfa7 --- /dev/null +++ b/client/ui/qml/Pages2/PageProtocolXrayXPaddingBytesSettings.qml @@ -0,0 +1,108 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import PageEnum 1.0 +import ProtocolEnum 1.0 +import Style 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" +import "../Components" + +PageType { + id: root + + BackButtonType { + id: backButton + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 20 + PageController.safeAreaTopMargin + } + + ListViewType { + id: listView + anchors.top: backButton.bottom + anchors.bottom: saveButton.visible ? saveButton.top : parent.bottom + anchors.left: parent.left + anchors.right: parent.right + + enabled: ServersUiController.isProcessedServerHasWriteAccess() + model: XrayConfigModel + + delegate: ColumnLayout { + width: listView.width + spacing: 0 + + BaseHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 0 + Layout.bottomMargin: 24 + headerText: qsTr("xPaddingBytes") + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + text: qsTr("Range") + color: AmneziaStyle.color.mutedGray + } + + MinMaxRowType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + minValue: xPaddingBytesMin + maxValue: xPaddingBytesMax + onMinChanged: xPaddingBytesMin = val + onMaxChanged: xPaddingBytesMax = val + } + + Item { + Layout.preferredHeight: 16 + } + } + } + + BasicButtonType { + id: saveButton + + anchors.left: root.left + anchors.right: root.right + anchors.bottom: root.bottom + anchors.leftMargin: 16 + anchors.rightMargin: 16 + anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin + + visible: listView.enabled && XrayConfigModel.hasUnsavedChanges + enabled: visible + text: qsTr("Save") + clickedFunc: function () { + var headerText = qsTr("Save settings?") + var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + var yesButtonFunction = function () { + if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) { + PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection")) + return + } + PageController.goToPage(PageEnum.PageSetupWizardInstalling) + InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray) + } + var noButtonFunction = function () { + if (typeof GC !== "undefined" && !GC.isMobile()) { + saveButton.forceActiveFocus() + } + } + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + } + } +} diff --git a/client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml new file mode 100644 index 000000000..b06b745f2 --- /dev/null +++ b/client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml @@ -0,0 +1,224 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import PageEnum 1.0 +import ProtocolEnum 1.0 +import Style 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" +import "../Components" + +PageType { + id: root + + BackButtonType { + id: backButton + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 20 + PageController.safeAreaTopMargin + } + + ListViewType { + id: listView + anchors.top: backButton.bottom + anchors.bottom: saveButton.visible ? saveButton.top : parent.bottom + anchors.left: parent.left + anchors.right: parent.right + + enabled: ServersUiController.isProcessedServerHasWriteAccess() + model: XrayConfigModel + + delegate: ColumnLayout { + width: listView.width + spacing: 0 + + BaseHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 0 + Layout.bottomMargin: 24 + headerText: qsTr("xPadding") + } + + // xPaddingBytes — min/max display row + LabelWithButtonType { + Layout.fillWidth: true + text: qsTr("xPaddingBytes") + descriptionText: (xPaddingBytesMin !== "" ? xPaddingBytesMin : "0") + "—" + (xPaddingBytesMax !== "" ? xPaddingBytesMax : "0") + rightImageSource: "qrc:/images/controls/chevron-right.svg" + clickedFunction: function () { + PageController.goToPage(PageEnum.PageProtocolXrayXPaddingBytesSettings) + } + } + + DividerType { + } + + SwitcherType { + Layout.fillWidth: true + Layout.margins: 16 + text: qsTr("xPaddingObfsMode") + checked: xPaddingObfsMode + onToggled: xPaddingObfsMode = checked + } + + DividerType { + } + + TextFieldWithHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 16 + headerText: qsTr("xPaddingKey") + textField.text: xPaddingKey + textField.onEditingFinished: { + if (textField.text !== xPaddingKey) xPaddingKey = textField.text + } + } + + TextFieldWithHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + headerText: qsTr("xPaddingHeader") + textField.text: xPaddingHeader + textField.onEditingFinished: { + if (textField.text !== xPaddingHeader) xPaddingHeader = textField.text + } + } + + DropDownType { + id: placementDropDown + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: xPaddingPlacement + descriptionText: qsTr("xPaddingPlacement") + headerText: qsTr("xPaddingPlacement") + drawerParent: root + listView: ListViewWithRadioButtonType { + rootWidth: root.width + model: ListModel { + Component.onCompleted: { + var opts = XrayConfigModel.xPaddingPlacementOptions() + for (var i = 0; i < opts.length; i++) { + append({name: opts[i]}) + } + } + } + clickedFunction: function () { + xPaddingPlacement = selectedText + placementDropDown.text = selectedText + placementDropDown.closeTriggered() + } + Component.onCompleted: { + for (var i = 0; i < model.count; i++) { + if (model.get(i).name === xPaddingPlacement) { + selectedIndex = i; + break + } + } + } + } + Connections { + target: XrayConfigModel + + function onDataChanged() { + placementDropDown.text = xPaddingPlacement + } + } + } + + DropDownType { + id: methodDropDown + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: xPaddingMethod + descriptionText: qsTr("xPaddingMethod") + headerText: qsTr("xPaddingMethod") + drawerParent: root + listView: ListViewWithRadioButtonType { + rootWidth: root.width + model: ListModel { + Component.onCompleted: { + var opts = XrayConfigModel.xPaddingMethodOptions() + for (var i = 0; i < opts.length; i++) { + append({name: opts[i]}) + } + } + } + clickedFunction: function () { + xPaddingMethod = selectedText + methodDropDown.text = selectedText + methodDropDown.closeTriggered() + } + Component.onCompleted: { + for (var i = 0; i < model.count; i++) { + if (model.get(i).name === xPaddingMethod) { + selectedIndex = i; + break + } + } + } + } + Connections { + target: XrayConfigModel + + function onDataChanged() { + methodDropDown.text = xPaddingMethod + } + } + } + + Item { + Layout.preferredHeight: 16 + } + } + } + + BasicButtonType { + id: saveButton + + anchors.left: root.left + anchors.right: root.right + anchors.bottom: root.bottom + anchors.leftMargin: 16 + anchors.rightMargin: 16 + anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin + + visible: listView.enabled && XrayConfigModel.hasUnsavedChanges + enabled: visible + text: qsTr("Save") + clickedFunc: function () { + var headerText = qsTr("Save settings?") + var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + var yesButtonFunction = function () { + if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) { + PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection")) + return + } + PageController.goToPage(PageEnum.PageSetupWizardInstalling) + InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray) + } + var noButtonFunction = function () { + if (typeof GC !== "undefined" && !GC.isMobile()) { + saveButton.forceActiveFocus() + } + } + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + } + } +} diff --git a/client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml new file mode 100644 index 000000000..dff46b2dd --- /dev/null +++ b/client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml @@ -0,0 +1,222 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import PageEnum 1.0 +import ProtocolEnum 1.0 +import Style 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" +import "../Components" + +PageType { + id: root + + BackButtonType { + id: backButton + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 20 + PageController.safeAreaTopMargin + } + + ListViewType { + id: listView + anchors.top: backButton.bottom + anchors.bottom: saveButton.visible ? saveButton.top : parent.bottom + anchors.left: parent.left + anchors.right: parent.right + + enabled: ServersUiController.isProcessedServerHasWriteAccess() + model: XrayConfigModel + + delegate: ColumnLayout { + width: listView.width + spacing: 0 + + BaseHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 0 + Layout.bottomMargin: 24 + headerText: qsTr("xmux") + } + + SwitcherType { + Layout.fillWidth: true + Layout.margins: 16 + text: qsTr("xmux") + checked: xmuxEnabled + onToggled: xmuxEnabled = checked + } + + DividerType { + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + enabled: xmuxEnabled + + // maxConcurrency + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 24 + Layout.bottomMargin: 8 + text: qsTr("maxConcurrency") + color: AmneziaStyle.color.mutedGray + } + MinMaxRowType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + minValue: xmuxMaxConcurrencyMin + maxValue: xmuxMaxConcurrencyMax + onMinChanged: xmuxMaxConcurrencyMin = val + onMaxChanged: xmuxMaxConcurrencyMax = val + } + + // maxConnections + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 16 + Layout.bottomMargin: 8 + text: qsTr("maxConnections") + color: AmneziaStyle.color.mutedGray + } + MinMaxRowType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + minValue: xmuxMaxConnectionsMin + maxValue: xmuxMaxConnectionsMax + onMinChanged: xmuxMaxConnectionsMin = val + onMaxChanged: xmuxMaxConnectionsMax = val + } + + // cMaxReuseTimes + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 16 + Layout.bottomMargin: 8 + text: qsTr("cMaxReuseTimes") + color: AmneziaStyle.color.mutedGray + } + MinMaxRowType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + minValue: xmuxCMaxReuseTimesMin + maxValue: xmuxCMaxReuseTimesMax + onMinChanged: xmuxCMaxReuseTimesMin = val + onMaxChanged: xmuxCMaxReuseTimesMax = val + } + + // hMaxRequestTimes + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 16 + Layout.bottomMargin: 8 + text: qsTr("hMaxRequestTimes") + color: AmneziaStyle.color.mutedGray + } + MinMaxRowType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + minValue: xmuxHMaxRequestTimesMin + maxValue: xmuxHMaxRequestTimesMax + onMinChanged: xmuxHMaxRequestTimesMin = val + onMaxChanged: xmuxHMaxRequestTimesMax = val + } + + // hMaxReusableSecs + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 16 + Layout.bottomMargin: 8 + text: qsTr("hMaxReusableSecs") + color: AmneziaStyle.color.mutedGray + } + MinMaxRowType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + minValue: xmuxHMaxReusableSecsMin + maxValue: xmuxHMaxReusableSecsMax + onMinChanged: xmuxHMaxReusableSecsMin = val + onMaxChanged: xmuxHMaxReusableSecsMax = val + } + + TextFieldWithHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 16 + headerText: qsTr("hKeepAlivePeriod") + textField.text: xmuxHKeepAlivePeriod + textField.validator: IntValidator { + bottom: 0 + } + textField.onEditingFinished: { + if (textField.text !== xmuxHKeepAlivePeriod) xmuxHKeepAlivePeriod = textField.text + } + } + } + + Item { + Layout.preferredHeight: 16 + } + } + } + + BasicButtonType { + id: saveButton + + anchors.left: root.left + anchors.right: root.right + anchors.bottom: root.bottom + anchors.leftMargin: 16 + anchors.rightMargin: 16 + anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin + + visible: listView.enabled && XrayConfigModel.hasUnsavedChanges + enabled: visible + text: qsTr("Save") + clickedFunc: function () { + var headerText = qsTr("Save settings?") + var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + var yesButtonFunction = function () { + if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) { + PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection")) + return + } + PageController.goToPage(PageEnum.PageSetupWizardInstalling) + InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray) + } + var noButtonFunction = function () { + if (typeof GC !== "undefined" && !GC.isMobile()) { + saveButton.forceActiveFocus() + } + } + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + } + } +} + diff --git a/client/ui/qml/qml.qrc b/client/ui/qml/qml.qrc index 64e60c201..5785b4c78 100644 --- a/client/ui/qml/qml.qrc +++ b/client/ui/qml/qml.qrc @@ -77,6 +77,16 @@ Pages2/PageProtocolRaw.qml Pages2/PageProtocolWireGuardSettings.qml Pages2/PageProtocolXraySettings.qml + + Pages2/PageProtocolXraySnapshots.qml + Pages2/PageProtocolXrayFlowSettings.qml + Pages2/PageProtocolXraySecuritySettings.qml + Pages2/PageProtocolXrayTransportSettings.qml + Pages2/PageProtocolXrayXmuxSettings.qml + Pages2/PageProtocolXrayXPaddingSettings.qml + Pages2/PageProtocolXrayXPaddingBytesSettings.qml + Controls2/MinMaxRowType.qml + Pages2/PageServiceDnsSettings.qml Pages2/PageServiceMtProxySettings.qml Pages2/PageServiceTelemtSettings.qml