diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 6c4f1ae4..0bc4e89c 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -56,6 +56,7 @@ set(CMAKE_AUTORCC ON) set(AMNEZIAVPN_TS_FILES ${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_ru.ts ${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_zh_CN.ts + ${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_fa_IR.ts ) file(GLOB_RECURSE AMNEZIAVPN_TS_SOURCES *.qrc *.cpp *.h *.ui) @@ -107,7 +108,7 @@ set(HEADERS ${HEADERS} ${CMAKE_CURRENT_LIST_DIR}/core/errorstrings.h ${CMAKE_CURRENT_LIST_DIR}/core/scripts_registry.h ${CMAKE_CURRENT_LIST_DIR}/core/server_defs.h - ${CMAKE_CURRENT_LIST_DIR}/core/servercontroller.h + ${CMAKE_CURRENT_LIST_DIR}/core/controllers/serverController.h ${CMAKE_CURRENT_LIST_DIR}/protocols/protocols_defs.h ${CMAKE_CURRENT_LIST_DIR}/protocols/qml_register_protocols.h ${CMAKE_CURRENT_LIST_DIR}/ui/notificationhandler.h @@ -146,7 +147,7 @@ set(SOURCES ${SOURCES} ${CMAKE_CURRENT_LIST_DIR}/core/errorstrings.cpp ${CMAKE_CURRENT_LIST_DIR}/core/scripts_registry.cpp ${CMAKE_CURRENT_LIST_DIR}/core/server_defs.cpp - ${CMAKE_CURRENT_LIST_DIR}/core/servercontroller.cpp + ${CMAKE_CURRENT_LIST_DIR}/core/controllers/serverController.cpp ${CMAKE_CURRENT_LIST_DIR}/protocols/protocols_defs.cpp ${CMAKE_CURRENT_LIST_DIR}/ui/notificationhandler.cpp ${CMAKE_CURRENT_LIST_DIR}/ui/qautostart.cpp diff --git a/client/amnezia_application.cpp b/client/amnezia_application.cpp index a3050071..4d372226 100644 --- a/client/amnezia_application.cpp +++ b/client/amnezia_application.cpp @@ -275,19 +275,16 @@ QQmlApplicationEngine *AmneziaApplication::qmlEngine() const void AmneziaApplication::initModels() { - m_containersModel.reset(new ContainersModel(m_settings, this)); + m_containersModel.reset(new ContainersModel(this)); m_engine->rootContext()->setContextProperty("ContainersModel", m_containersModel.get()); - connect(m_vpnConnection.get(), &VpnConnection::newVpnConfigurationCreated, m_containersModel.get(), - &ContainersModel::updateContainersConfig); m_serversModel.reset(new ServersModel(m_settings, this)); m_engine->rootContext()->setContextProperty("ServersModel", m_serversModel.get()); - connect(m_serversModel.get(), &ServersModel::currentlyProcessedServerIndexChanged, m_containersModel.get(), - &ContainersModel::setCurrentlyProcessedServerIndex); - connect(m_serversModel.get(), &ServersModel::defaultServerIndexChanged, m_containersModel.get(), - &ContainersModel::setCurrentlyProcessedServerIndex); - connect(m_containersModel.get(), &ContainersModel::containersModelUpdated, m_serversModel.get(), - &ServersModel::updateContainersConfig); + connect(m_serversModel.get(), &ServersModel::containersUpdated, m_containersModel.get(), + &ContainersModel::updateModel); + connect(m_serversModel.get(), &ServersModel::defaultContainerChanged, m_containersModel.get(), + &ContainersModel::setDefaultContainer); + m_containersModel->setDefaultContainer(m_serversModel->getDefaultContainer()); // make better? m_languageModel.reset(new LanguageModel(m_settings, this)); m_engine->rootContext()->setContextProperty("LanguageModel", m_languageModel.get()); @@ -296,7 +293,7 @@ void AmneziaApplication::initModels() m_sitesModel.reset(new SitesModel(m_settings, this)); m_engine->rootContext()->setContextProperty("SitesModel", m_sitesModel.get()); - + m_protocolsModel.reset(new ProtocolsModel(m_settings, this)); m_engine->rootContext()->setContextProperty("ProtocolsModel", m_protocolsModel.get()); @@ -322,6 +319,16 @@ void AmneziaApplication::initModels() m_sftpConfigModel.reset(new SftpConfigModel(this)); m_engine->rootContext()->setContextProperty("SftpConfigModel", m_sftpConfigModel.get()); + + m_clientManagementModel.reset(new ClientManagementModel(m_settings, this)); + m_engine->rootContext()->setContextProperty("ClientManagementModel", m_clientManagementModel.get()); + + connect(m_configurator.get(), &VpnConfigurator::newVpnConfigCreated, this, + [this](const QString &clientId, const QString &clientName, const DockerContainer container, + ServerCredentials credentials) { + m_serversModel->reloadContainerConfig(); + m_clientManagementModel->appendClient(clientId, clientName, container, credentials); + }); } void AmneziaApplication::initControllers() @@ -347,18 +354,24 @@ void AmneziaApplication::initControllers() m_importController.reset(new ImportController(m_serversModel, m_containersModel, m_settings)); m_engine->rootContext()->setContextProperty("ImportController", m_importController.get()); - m_exportController.reset(new ExportController(m_serversModel, m_containersModel, m_settings, m_configurator)); + m_exportController.reset(new ExportController(m_serversModel, m_containersModel, m_clientManagementModel, + m_settings, m_configurator)); m_engine->rootContext()->setContextProperty("ExportController", m_exportController.get()); m_settingsController.reset(new SettingsController(m_serversModel, m_containersModel, m_languageModel, m_settings)); m_engine->rootContext()->setContextProperty("SettingsController", m_settingsController.get()); - if (m_settingsController->isAutoStartEnabled() && m_serversModel->getDefaultServerIndex() >= 0) { + if (m_settingsController->isAutoConnectEnabled() && m_serversModel->getDefaultServerIndex() >= 0) { QTimer::singleShot(1000, this, [this]() { m_connectionController->openConnection(); }); } + connect(m_settingsController.get(), &SettingsController::amneziaDnsToggled , m_serversModel.get(), + &ServersModel::toggleAmneziaDns); m_sitesController.reset(new SitesController(m_settings, m_vpnConnection, m_sitesModel)); m_engine->rootContext()->setContextProperty("SitesController", m_sitesController.get()); m_systemController.reset(new SystemController(m_settings)); m_engine->rootContext()->setContextProperty("SystemController", m_systemController.get()); + + m_cloudController.reset(new ApiController(m_serversModel, m_containersModel)); + m_engine->rootContext()->setContextProperty("ApiController", m_cloudController.get()); } diff --git a/client/amnezia_application.h b/client/amnezia_application.h index 32300421..aff853a6 100644 --- a/client/amnezia_application.h +++ b/client/amnezia_application.h @@ -24,6 +24,7 @@ #include "ui/controllers/settingsController.h" #include "ui/controllers/sitesController.h" #include "ui/controllers/systemController.h" +#include "ui/controllers/apiController.h" #include "ui/models/containers_model.h" #include "ui/models/languageModel.h" #include "ui/models/protocols/cloakConfigModel.h" @@ -39,6 +40,7 @@ #include "ui/models/servers_model.h" #include "ui/models/services/sftpConfigModel.h" #include "ui/models/sites_model.h" +#include "ui/models/clientManagementModel.h" #define amnApp (static_cast(QCoreApplication::instance())) @@ -94,6 +96,7 @@ private: QSharedPointer m_languageModel; QSharedPointer m_protocolsModel; QSharedPointer m_sitesModel; + QSharedPointer m_clientManagementModel; QScopedPointer m_openVpnConfigModel; QScopedPointer m_shadowSocksConfigModel; @@ -118,6 +121,7 @@ private: QScopedPointer m_settingsController; QScopedPointer m_sitesController; QScopedPointer m_systemController; + QScopedPointer m_cloudController; }; #endif // AMNEZIA_APPLICATION_H diff --git a/client/configurators/awg_configurator.cpp b/client/configurators/awg_configurator.cpp index c3e42258..5b452755 100644 --- a/client/configurators/awg_configurator.cpp +++ b/client/configurators/awg_configurator.cpp @@ -3,18 +3,17 @@ #include #include -#include "core/servercontroller.h" +#include "core/controllers/serverController.h" AwgConfigurator::AwgConfigurator(std::shared_ptr settings, QObject *parent) : WireguardConfigurator(settings, true, parent) { } -QString AwgConfigurator::genAwgConfig(const ServerCredentials &credentials, - DockerContainer container, - const QJsonObject &containerConfig, ErrorCode *errorCode) +QString AwgConfigurator::genAwgConfig(const ServerCredentials &credentials, DockerContainer container, + const QJsonObject &containerConfig, QString &clientId, ErrorCode *errorCode) { - QString config = WireguardConfigurator::genWireguardConfig(credentials, container, containerConfig, errorCode); + QString config = WireguardConfigurator::genWireguardConfig(credentials, container, containerConfig, clientId, errorCode); QJsonObject jsonConfig = QJsonDocument::fromJson(config.toUtf8()).object(); QString awgConfig = jsonConfig.value(config_key::config).toString(); diff --git a/client/configurators/awg_configurator.h b/client/configurators/awg_configurator.h index cf0f2cae..ef40804c 100644 --- a/client/configurators/awg_configurator.h +++ b/client/configurators/awg_configurator.h @@ -12,7 +12,7 @@ public: AwgConfigurator(std::shared_ptr settings, QObject *parent = nullptr); QString genAwgConfig(const ServerCredentials &credentials, DockerContainer container, - const QJsonObject &containerConfig, ErrorCode *errorCode = nullptr); + const QJsonObject &containerConfig, QString &clientId, ErrorCode *errorCode = nullptr); }; #endif // AWGCONFIGURATOR_H diff --git a/client/configurators/cloak_configurator.cpp b/client/configurators/cloak_configurator.cpp index fab378e2..9c540967 100644 --- a/client/configurators/cloak_configurator.cpp +++ b/client/configurators/cloak_configurator.cpp @@ -4,7 +4,7 @@ #include #include -#include "core/servercontroller.h" +#include "core/controllers/serverController.h" #include "containers/containers_defs.h" CloakConfigurator::CloakConfigurator(std::shared_ptr settings, QObject *parent): diff --git a/client/configurators/ikev2_configurator.cpp b/client/configurators/ikev2_configurator.cpp index 4ca0e5da..752d1750 100644 --- a/client/configurators/ikev2_configurator.cpp +++ b/client/configurators/ikev2_configurator.cpp @@ -11,7 +11,7 @@ #include "containers/containers_defs.h" #include "core/scripts_registry.h" #include "core/server_defs.h" -#include "core/servercontroller.h" +#include "core/controllers/serverController.h" #include "utilities.h" Ikev2Configurator::Ikev2Configurator(std::shared_ptr settings, QObject *parent) diff --git a/client/configurators/openvpn_configurator.cpp b/client/configurators/openvpn_configurator.cpp index 8c58a376..e3362236 100644 --- a/client/configurators/openvpn_configurator.cpp +++ b/client/configurators/openvpn_configurator.cpp @@ -16,7 +16,7 @@ #include "containers/containers_defs.h" #include "core/scripts_registry.h" #include "core/server_defs.h" -#include "core/servercontroller.h" +#include "core/controllers/serverController.h" #include "settings.h" #include "utilities.h" @@ -83,7 +83,7 @@ OpenVpnConfigurator::ConnectionData OpenVpnConfigurator::prepareOpenVpnConfig(co } QString OpenVpnConfigurator::genOpenVpnConfig(const ServerCredentials &credentials, DockerContainer container, - const QJsonObject &containerConfig, ErrorCode *errorCode) + const QJsonObject &containerConfig, QString &clientId, ErrorCode *errorCode) { ServerController serverController(m_settings); QString config = @@ -113,6 +113,8 @@ QString OpenVpnConfigurator::genOpenVpnConfig(const ServerCredentials &credentia QJsonObject jConfig; jConfig[config_key::config] = config; + clientId = connData.clientId; + return QJsonDocument(jConfig).toJson(); } @@ -131,13 +133,13 @@ QString OpenVpnConfigurator::processConfigWithLocalSettings(QString jsonConfig) config.append("block-ipv6\n"); } if (m_settings->routeMode() == Settings::VpnOnlyForwardSites) { - + // no redirect-gateway } if (m_settings->routeMode() == Settings::VpnAllExceptSites) { -#ifndef Q_OS_ANDROID +#ifndef Q_OS_ANDROID config.append("\nredirect-gateway ipv6 !ipv4 bypass-dhcp\n"); -#endif +#endif // Prevent ipv6 leak config.append("ifconfig-ipv6 fd15:53b6:dead::2/64 fd15:53b6:dead::1\n"); config.append("block-ipv6\n"); diff --git a/client/configurators/openvpn_configurator.h b/client/configurators/openvpn_configurator.h index 3b84e0a0..cc66d13f 100644 --- a/client/configurators/openvpn_configurator.h +++ b/client/configurators/openvpn_configurator.h @@ -24,7 +24,7 @@ public: }; QString genOpenVpnConfig(const ServerCredentials &credentials, DockerContainer container, - const QJsonObject &containerConfig, ErrorCode *errorCode = nullptr); + const QJsonObject &containerConfig, QString &clientId, ErrorCode *errorCode = nullptr); QString processConfigWithLocalSettings(QString jsonConfig); QString processConfigWithExportSettings(QString jsonConfig); @@ -32,9 +32,9 @@ public: ErrorCode signCert(DockerContainer container, const ServerCredentials &credentials, QString clientId); -private: - ConnectionData createCertRequest(); + static ConnectionData createCertRequest(); +private: ConnectionData prepareOpenVpnConfig(const ServerCredentials &credentials, DockerContainer container, ErrorCode *errorCode = nullptr); diff --git a/client/configurators/shadowsocks_configurator.cpp b/client/configurators/shadowsocks_configurator.cpp index a71064c8..99e4158c 100644 --- a/client/configurators/shadowsocks_configurator.cpp +++ b/client/configurators/shadowsocks_configurator.cpp @@ -5,7 +5,7 @@ #include #include "containers/containers_defs.h" -#include "core/servercontroller.h" +#include "core/controllers/serverController.h" ShadowSocksConfigurator::ShadowSocksConfigurator(std::shared_ptr settings, QObject *parent): ConfiguratorBase(settings, parent) diff --git a/client/configurators/vpn_configurator.cpp b/client/configurators/vpn_configurator.cpp index 6c5286c2..3018b52f 100644 --- a/client/configurators/vpn_configurator.cpp +++ b/client/configurators/vpn_configurator.cpp @@ -28,11 +28,11 @@ VpnConfigurator::VpnConfigurator(std::shared_ptr settings, QObject *pa } QString VpnConfigurator::genVpnProtocolConfig(const ServerCredentials &credentials, DockerContainer container, - const QJsonObject &containerConfig, Proto proto, ErrorCode *errorCode) + const QJsonObject &containerConfig, Proto proto, QString &clientId, ErrorCode *errorCode) { switch (proto) { case Proto::OpenVpn: - return openVpnConfigurator->genOpenVpnConfig(credentials, container, containerConfig, errorCode); + return openVpnConfigurator->genOpenVpnConfig(credentials, container, containerConfig, clientId, errorCode); case Proto::ShadowSocks: return shadowSocksConfigurator->genShadowSocksConfig(credentials, container, containerConfig, errorCode); @@ -40,10 +40,10 @@ QString VpnConfigurator::genVpnProtocolConfig(const ServerCredentials &credentia case Proto::Cloak: return cloakConfigurator->genCloakConfig(credentials, container, containerConfig, errorCode); case Proto::WireGuard: - return wireguardConfigurator->genWireguardConfig(credentials, container, containerConfig, errorCode); + return wireguardConfigurator->genWireguardConfig(credentials, container, containerConfig, clientId, errorCode); case Proto::Awg: - return awgConfigurator->genAwgConfig(credentials, container, containerConfig, errorCode); + return awgConfigurator->genAwgConfig(credentials, container, containerConfig, clientId, errorCode); case Proto::Ikev2: return ikev2Configurator->genIkev2Config(credentials, container, containerConfig, errorCode); diff --git a/client/configurators/vpn_configurator.h b/client/configurators/vpn_configurator.h index ac89b0e4..61dc2ac6 100644 --- a/client/configurators/vpn_configurator.h +++ b/client/configurators/vpn_configurator.h @@ -6,7 +6,6 @@ #include "configurator_base.h" #include "core/defs.h" - class OpenVpnConfigurator; class ShadowSocksConfigurator; class CloakConfigurator; @@ -16,14 +15,15 @@ class SshConfigurator; class AwgConfigurator; // Retrieve connection settings from server -class VpnConfigurator : ConfiguratorBase +class VpnConfigurator : public ConfiguratorBase { Q_OBJECT public: explicit VpnConfigurator(std::shared_ptr settings, QObject *parent = nullptr); QString genVpnProtocolConfig(const ServerCredentials &credentials, DockerContainer container, - const QJsonObject &containerConfig, Proto proto, ErrorCode *errorCode = nullptr); + const QJsonObject &containerConfig, Proto proto, QString &clientId, + ErrorCode *errorCode = nullptr); QPair getDnsForConfig(int serverIndex); QString &processConfigWithDnsSettings(int serverIndex, DockerContainer container, Proto proto, QString &config); @@ -32,8 +32,8 @@ public: QString &processConfigWithExportSettings(int serverIndex, DockerContainer container, Proto proto, QString &config); // workaround for containers which is not support normal configuration - void updateContainerConfigAfterInstallation(DockerContainer container, - QJsonObject &containerConfig, const QString &stdOut); + void updateContainerConfigAfterInstallation(DockerContainer container, QJsonObject &containerConfig, + const QString &stdOut); std::shared_ptr openVpnConfigurator; std::shared_ptr shadowSocksConfigurator; @@ -42,6 +42,10 @@ public: std::shared_ptr ikev2Configurator; std::shared_ptr sshConfigurator; std::shared_ptr awgConfigurator; + +signals: + void newVpnConfigCreated(const QString &clientId, const QString &clientName, const DockerContainer container, + ServerCredentials credentials); }; #endif // VPN_CONFIGURATOR_H diff --git a/client/configurators/wireguard_configurator.cpp b/client/configurators/wireguard_configurator.cpp index e22c8282..8bfd5e75 100644 --- a/client/configurators/wireguard_configurator.cpp +++ b/client/configurators/wireguard_configurator.cpp @@ -15,7 +15,7 @@ #include "containers/containers_defs.h" #include "core/scripts_registry.h" #include "core/server_defs.h" -#include "core/servercontroller.h" +#include "core/controllers/serverController.h" #include "settings.h" #include "utilities.h" @@ -177,7 +177,7 @@ WireguardConfigurator::ConnectionData WireguardConfigurator::prepareWireguardCon } QString WireguardConfigurator::genWireguardConfig(const ServerCredentials &credentials, DockerContainer container, - const QJsonObject &containerConfig, ErrorCode *errorCode) + const QJsonObject &containerConfig, QString &clientId, ErrorCode *errorCode) { ServerController serverController(m_settings); QString scriptData = amnezia::scriptData(m_configTemplate, container); @@ -205,6 +205,8 @@ QString WireguardConfigurator::genWireguardConfig(const ServerCredentials &crede jConfig[config_key::psk_key] = connData.pskKey; jConfig[config_key::server_pub_key] = connData.serverPubKey; + clientId = connData.clientPubKey; + return QJsonDocument(jConfig).toJson(); } diff --git a/client/configurators/wireguard_configurator.h b/client/configurators/wireguard_configurator.h index 7f8e1587..c1b4aa3c 100644 --- a/client/configurators/wireguard_configurator.h +++ b/client/configurators/wireguard_configurator.h @@ -26,7 +26,7 @@ public: }; QString genWireguardConfig(const ServerCredentials &credentials, DockerContainer container, - const QJsonObject &containerConfig, ErrorCode *errorCode = nullptr); + const QJsonObject &containerConfig, QString &clientId, ErrorCode *errorCode = nullptr); QString processConfigWithLocalSettings(QString config); QString processConfigWithExportSettings(QString config); diff --git a/client/containers/containers_defs.cpp b/client/containers/containers_defs.cpp index 495443ce..e3348fba 100644 --- a/client/containers/containers_defs.cpp +++ b/client/containers/containers_defs.cpp @@ -54,11 +54,11 @@ QVector ContainerProps::protocolsForContainer(amnezia::DockerCon case DockerContainer::ShadowSocks: return { Proto::OpenVpn, Proto::ShadowSocks }; - case DockerContainer::Cloak: return { Proto::OpenVpn, Proto::ShadowSocks, Proto::Cloak }; + case DockerContainer::Cloak: return { Proto::OpenVpn, /*Proto::ShadowSocks,*/ Proto::Cloak }; case DockerContainer::Ipsec: return { Proto::Ikev2 /*, Protocol::L2tp */ }; - case DockerContainer::Dns: return {}; + case DockerContainer::Dns: return { Proto::Dns }; case DockerContainer::Sftp: return { Proto::Sftp }; diff --git a/client/core/servercontroller.cpp b/client/core/controllers/serverController.cpp similarity index 91% rename from client/core/servercontroller.cpp rename to client/core/controllers/serverController.cpp index 398b46b3..36560e10 100644 --- a/client/core/servercontroller.cpp +++ b/client/core/controllers/serverController.cpp @@ -1,4 +1,4 @@ -#include "servercontroller.h" +#include "serverController.h" #include #include @@ -24,13 +24,18 @@ #include "containers/containers_defs.h" #include "logger.h" -#include "scripts_registry.h" -#include "server_defs.h" +#include "core/scripts_registry.h" +#include "core/server_defs.h" #include "settings.h" #include "utilities.h" #include +namespace +{ + Logger logger("ServerController"); +} + ServerController::ServerController(std::shared_ptr settings, QObject *parent) : m_settings(settings) { } @@ -634,9 +639,9 @@ QString ServerController::checkSshConnection(const ServerCredentials &credential return stdOut; } -void ServerController::setCancelInstallation(const bool cancel) +void ServerController::cancelInstallation() { - m_cancelInstallation = cancel; + m_cancelInstallation = true; } ErrorCode ServerController::setupServerFirewall(const ServerCredentials &credentials) @@ -737,6 +742,7 @@ ErrorCode ServerController::isUserInSudo(const ServerCredentials &credentials, D ErrorCode ServerController::isServerDpkgBusy(const ServerCredentials &credentials, DockerContainer container) { + m_cancelInstallation = false; QString stdOut; auto cbReadStdOut = [&](const QString &data, libssh::Client &) { stdOut += data + "\n"; @@ -784,7 +790,6 @@ ErrorCode ServerController::isServerDpkgBusy(const ServerCredentials &credential watcher.setFuture(future); wait.exec(); - m_cancelInstallation = false; emit serverIsBusy(false); return future.result(); @@ -857,6 +862,67 @@ ErrorCode ServerController::getAlreadyInstalledContainers(const ServerCredential containerConfig[config_key::responsePacketMagicHeader] = serverConfigMap.value(config_key::responsePacketMagicHeader); containerConfig[config_key::underloadPacketMagicHeader] = serverConfigMap.value(config_key::underloadPacketMagicHeader); containerConfig[config_key::transportPacketMagicHeader] = serverConfigMap.value(config_key::transportPacketMagicHeader); + } else if (protocol == Proto::Sftp) { + stdOut.clear(); + script = QString("sudo docker inspect --format '{{.Config.Cmd}}' %1").arg(name); + + ErrorCode errorCode = runScript(credentials, script, cbReadStdOut, cbReadStdErr); + if (errorCode != ErrorCode::NoError) { + return errorCode; + } + + auto sftpInfo = stdOut.split(":"); + if (sftpInfo.size() < 2) { + logger.error() << "Key parameters for the sftp container are missing"; + continue; + } + auto userName = sftpInfo.at(0); + userName = userName.remove(0, 1); + auto password = sftpInfo.at(1); + + containerConfig.insert(config_key::userName, userName); + containerConfig.insert(config_key::password, password); + } + + config.insert(config_key::container, ContainerProps::containerToString(container)); + } + config.insert(ProtocolProps::protoToString(protocol), containerConfig); + } + installedContainers.insert(container, config); + } + const static QRegularExpression torOrDnsRegExp("(amnezia-(?:torwebsite|dns)).*?([0-9]*)/(udp|tcp).*"); + QRegularExpressionMatch torOrDnsRegMatch = torOrDnsRegExp.match(containerInfo); + if (torOrDnsRegMatch.hasMatch()) { + QString name = torOrDnsRegMatch.captured(1); + QString port = torOrDnsRegMatch.captured(2); + QString transportProto = torOrDnsRegMatch.captured(3); + DockerContainer container = ContainerProps::containerFromString(name); + + QJsonObject config; + Proto mainProto = ContainerProps::defaultProtocol(container); + for (auto protocol : ContainerProps::protocolsForContainer(container)) { + QJsonObject containerConfig; + if (protocol == mainProto) { + containerConfig.insert(config_key::port, port); + containerConfig.insert(config_key::transport_proto, transportProto); + + if (protocol == Proto::TorWebSite) { + stdOut.clear(); + script = QString("sudo docker exec -i %1 sh -c 'cat /var/lib/tor/hidden_service/hostname'").arg(name); + + ErrorCode errorCode = runScript(credentials, script, cbReadStdOut, cbReadStdErr); + if (errorCode != ErrorCode::NoError) { + return errorCode; + } + + if (stdOut.isEmpty()) { + logger.error() << "Key parameters for the tor container are missing"; + continue; + } + + QString onion = stdOut; + onion.replace("\n", ""); + containerConfig.insert(config_key::site, onion); } config.insert(config_key::container, ContainerProps::containerToString(container)); diff --git a/client/core/servercontroller.h b/client/core/controllers/serverController.h similarity index 98% rename from client/core/servercontroller.h rename to client/core/controllers/serverController.h index 3191386c..175d96da 100644 --- a/client/core/servercontroller.h +++ b/client/core/controllers/serverController.h @@ -5,8 +5,8 @@ #include #include "containers/containers_defs.h" -#include "defs.h" -#include "sshclient.h" +#include "core/defs.h" +#include "core/sshclient.h" class Settings; class VpnConfigurator; @@ -56,7 +56,7 @@ public: QString checkSshConnection(const ServerCredentials &credentials, ErrorCode *errorCode = nullptr); - void setCancelInstallation(const bool cancel); + void cancelInstallation(); ErrorCode getDecryptedPrivateKey(const ServerCredentials &credentials, QString &decryptedPrivateKey, const std::function &callback); diff --git a/client/core/defs.h b/client/core/defs.h index 35515103..7de55286 100644 --- a/client/core/defs.h +++ b/client/core/defs.h @@ -36,7 +36,7 @@ enum ErrorCode ServerPacketManagerError, // Ssh connection errors - SshRequsetDeniedError, SshInterruptedError, SshInternalError, + SshRequestDeniedError, SshInterruptedError, SshInternalError, SshPrivateKeyError, SshPrivateKeyFormatError, SshTimeoutError, // Ssh sftp errors @@ -47,7 +47,6 @@ enum ErrorCode SshSftpNoMediaError, // Local errors - FailedToSaveConfigData, OpenVpnConfigMissing, OpenVpnManagementServerError, ConfigMissing, @@ -67,7 +66,6 @@ enum ErrorCode // 3rd party utils errors OpenSslFailed, - OpenVpnExecutableCrashed, ShadowSocksExecutableCrashed, CloakExecutableCrashed, diff --git a/client/core/errorstrings.cpp b/client/core/errorstrings.cpp index cd66186d..ab23a089 100644 --- a/client/core/errorstrings.cpp +++ b/client/core/errorstrings.cpp @@ -19,7 +19,7 @@ QString errorString(ErrorCode code){ case(ServerUserNotInSudo): return QObject::tr("The user does not have permission to use sudo"); // Libssh errors - case(SshRequsetDeniedError): return QObject::tr("Ssh request was denied"); + case(SshRequestDeniedError): return QObject::tr("Ssh request was denied"); case(SshInterruptedError): return QObject::tr("Ssh request was interrupted"); case(SshInternalError): return QObject::tr("Ssh internal error"); case(SshPrivateKeyError): return QObject::tr("Invalid private key or invalid passphrase entered"); @@ -42,7 +42,6 @@ QString errorString(ErrorCode code){ case(SshSftpNoMediaError): return QObject::tr("Sftp error: No media was in remote drive"); // Local errors - case (FailedToSaveConfigData): return QObject::tr("Failed to save config to disk"); case (OpenVpnConfigMissing): return QObject::tr("OpenVPN config missing"); case (OpenVpnManagementServerError): return QObject::tr("OpenVPN management server error"); @@ -58,7 +57,7 @@ QString errorString(ErrorCode code){ case (OpenVpnTapAdapterError): return QObject::tr("Can't setup OpenVPN TAP network adapter"); case (AddressPoolError): return QObject::tr("VPN pool error: no available addresses"); - case (ImportInvalidConfigError): return QObject::tr("The config does not contain any containers and credentiaks for connecting to the server"); + case (ImportInvalidConfigError): return QObject::tr("The config does not contain any containers and credentials for connecting to the server"); case(InternalError): default: diff --git a/client/core/sshclient.cpp b/client/core/sshclient.cpp index 797bdc6f..0ac95662 100644 --- a/client/core/sshclient.cpp +++ b/client/core/sshclient.cpp @@ -333,7 +333,7 @@ namespace libssh { switch (errorCode) { case(SSH_NO_ERROR): return ErrorCode::NoError; - case(SSH_REQUEST_DENIED): return ErrorCode::SshRequsetDeniedError; + case(SSH_REQUEST_DENIED): return ErrorCode::SshRequestDeniedError; case(SSH_EINTR): return ErrorCode::SshInterruptedError; case(SSH_FATAL): return ErrorCode::SshInternalError; default: return ErrorCode::SshInternalError; diff --git a/client/images/controls/close.svg b/client/images/controls/close.svg new file mode 100644 index 00000000..0643cdc8 --- /dev/null +++ b/client/images/controls/close.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/client/images/controls/search.svg b/client/images/controls/search.svg new file mode 100644 index 00000000..56fa50e1 --- /dev/null +++ b/client/images/controls/search.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/client/protocols/protocols_defs.h b/client/protocols/protocols_defs.h index 4d81e952..8d7d9bfb 100644 --- a/client/protocols/protocols_defs.h +++ b/client/protocols/protocols_defs.h @@ -21,6 +21,7 @@ namespace amnezia constexpr char dns2[] = "dns2"; constexpr char description[] = "description"; + constexpr char name[] = "name"; constexpr char cert[] = "cert"; constexpr char config[] = "config"; @@ -79,6 +80,8 @@ namespace amnezia constexpr char sftp[] = "sftp"; constexpr char awg[] = "awg"; + constexpr char configVersion[] = "config_version"; + constexpr char splitTunnelSites[] = "splitTunnelSites"; constexpr char splitTunnelType[] = "splitTunnelType"; diff --git a/client/resources.qrc b/client/resources.qrc index 4c63383c..dfadcb20 100644 --- a/client/resources.qrc +++ b/client/resources.qrc @@ -222,5 +222,8 @@ server_scripts/awg/configure_container.sh server_scripts/awg/run_container.sh server_scripts/awg/Dockerfile + ui/qml/Pages2/PageShareFullAccess.qml + images/controls/close.svg + images/controls/search.svg diff --git a/client/server_scripts/install_docker.sh b/client/server_scripts/install_docker.sh index 58f92540..6f19e090 100644 --- a/client/server_scripts/install_docker.sh +++ b/client/server_scripts/install_docker.sh @@ -1,19 +1,19 @@ -if which apt-get > /dev/null 2>&1; then pm=$(which apt-get); docker_pkg="docker.io"; dist="debian";\ -elif which dnf > /dev/null 2>&1; then pm=$(which dnf); docker_pkg="docker"; dist="fedora";\ -elif which yum > /dev/null 2>&1; then pm=$(which yum); docker_pkg="docker"; dist="centos";\ +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";\ +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";\ else echo "Packet manager not found"; exit 1; fi;\ -echo "Dist: $dist, Packet manager: $pm, Docker pkg: $docker_pkg";\ +echo "Dist: $dist, Packet manager: $pm, Install command: $silent_inst, Check pkgs command: $check_pkgs, Docker pkg: $docker_pkg";\ if [ "$dist" = "debian" ]; then export DEBIAN_FRONTEND=noninteractive; fi;\ -if ! command -v sudo > /dev/null 2>&1; then $pm update -yq; $pm install -yq sudo; fi;\ -if ! command -v fuser > /dev/null 2>&1; then sudo $pm install -yq psmisc; fi;\ -if ! command -v lsof > /dev/null 2>&1; then sudo $pm install -yq lsof; fi;\ -if ! command -v docker > /dev/null 2>&1; then sudo $pm update -yq; sudo $pm install -yq $docker_pkg;\ - if [ "$dist" = "fedora" ] || [ "$dist" = "centos" ] || [ "$dist" = "debian" ]; then sudo systemctl enable docker && sudo systemctl start docker; fi;\ +if ! command -v sudo > /dev/null 2>&1; then $pm $check_pkgs; $pm $silent_inst sudo; fi;\ +if ! command -v fuser > /dev/null 2>&1; then sudo $pm $check_pkgs; sudo $pm $silent_inst psmisc; fi;\ +if ! command -v lsof > /dev/null 2>&1; then sudo $pm $check_pkgs; sudo $pm $silent_inst lsof; fi;\ +if ! command -v docker > /dev/null 2>&1; then \ + sudo $pm $check_pkgs; sudo $pm $silent_inst $docker_pkg;\ + sleep 5; sudo systemctl enable --now docker; sleep 5;\ fi;\ -if [ "$dist" = "debian" ]; then \ - docker_service=$(systemctl list-units --full --all | grep docker.service | grep -v inactive | grep -v dead | grep -v failed);\ - if [ -z "$docker_service" ]; then sudo $pm update -yq; sudo $pm install -yq curl $docker_pkg; fi;\ - sleep 3 && sudo systemctl start docker && sleep 3;\ +if [ "$(systemctl is-active docker)" != "active" ]; then \ + sudo $pm $check_pkgs; sudo $pm $silent_inst $docker_pkg;\ + sleep 5; sudo systemctl start docker; sleep 5;\ fi;\ -if ! command -v sudo > /dev/null 2>&1; then echo "Failed to install Docker";exit 1;fi;\ +if ! command -v sudo > /dev/null 2>&1; then echo "Failed to install sudo, command not found"; exit 1; fi;\ docker --version diff --git a/client/translations/amneziavpn_fa_IR.ts b/client/translations/amneziavpn_fa_IR.ts new file mode 100644 index 00000000..7471208b --- /dev/null +++ b/client/translations/amneziavpn_fa_IR.ts @@ -0,0 +1,3185 @@ + + + + + AmneziaApplication + + Split tunneling for WireGuard is not implemented, the option was disabled + Раздельное туннелирование для "Wireguard" не реализовано,опция отключена + + + Split tunneling for %1 is not implemented, the option was disabled + جداسازی ترافیک برای %1 پیاده سازی نشده، این گزینه غیرفعال است + + + + AndroidController + + + AmneziaVPN + AmneziaVPN + + + + VPN Connected + Refers to the app - which is currently running the background and waiting + وی‎پی‎ان متصل است + + + + ApiController + + + Error when retrieving configuration from cloud server + + + + + ConnectionController + + + VPN Protocols is not installed. + Please install VPN container at first + پروتکل وی‎پی‎ان نصب نشده است +لطفا کانتینر وی‎پی‎ان را نصب کنید + + + + Connection... + ارتباط + + + + Connected + متصل + + + + Settings updated successfully, Reconnnection... + تنظیمات به روز رسانی شد +در حال اتصال دوباره... + + + + Settings updated successfully + + + + + Reconnection... + در حال اتصال دوباره... + + + + + + + Connect + اتصال + + + + Disconnection... + قطع ارتباط... + + + + ConnectionTypeSelectionDrawer + + + Add new connection + ایجاد ارتباط جدید + + + + Configure your server + تنظیم سرور + + + + Open config file, key or QR code + بارگذاری فایل تنظیمات، کلید یا QR Code + + + + ContextMenuType + + + C&ut + &بریدن + + + + &Copy + &کپی + + + + &Paste + &پیوست + + + + &SelectAll + &انتخاب همه + + + + ExportController + + + Access error! + خطای دسترسی! + + + + HomeContainersListView + + + Unable change protocol while there is an active connection + امکان تغییر پروتکل در هنگام متصل بودن وجود ندارد + + + + The selected protocol is not supported on the current platform + پروتکل انتخاب شده بر روی این پلتفرم پشتیبانی نمی‎‎شود + + + Reconnect via VPN Procotol: + Переподключение через VPN протокол: + + + + ImportController + + + Scanned %1 of %2. + ارزیابی %1 از %2. + + + + InstallController + + + + %1 installed successfully. + %1 با موفقیت نصب شد + + + + + %1 is already installed on the server. + %1 در حال حاضر بر روی سرور نصب شده است + + + + +Added containers that were already installed on the server + +کانتینرهایی که بر روی سرور موجود بودند اضافه شدند + + + + +Already installed containers were found on the server. All installed containers have been added to the application + +کانتینرهای نصب شده بر روی سرور شناسایی شدند. تمام کانتینترهای نصب شده به نرم افزار اضافه شدند + + + + Settings updated successfully + تنظیمات با موفقیت به‎روز‎رسانی شدند + + + + Server '%1' was removed + سرور %1 حذف شد + + + + All containers from server '%1' have been removed + تمام کانتینترها از سرور %1 حذف شدند + + + + %1 has been removed from the server '%2' + %1 از سرور %2 حذف شد + + + + Please login as the user + لطفا به عنوان کاربر وارد شوید + + + + Server added successfully + سرور با موفقیت اضافه شد + + + + KeyChainClass + + + Read key failed: %1 + خواندن کلید انجام نشد: %1 + + + + Write key failed: %1 + نوشتن کلید انجام نشد: %1 + + + + Delete key failed: %1 + حذف کلید انجام نشد: %1 + + + + NotificationHandler + + + + AmneziaVPN + AmneziaVPN + + + + VPN Connected + وی‎پی‎ان متصل است + + + + VPN Disconnected + وی‎پی‎ان قطع است + + + + AmneziaVPN notification + اخطار AmneziaVPN + + + + Unsecured network detected: + شبکه ناامن شناسایی شد: + + + + PageDeinstalling + + + Removing services from %1 + حذف سرویس‎ها از %1 + + + + Usually it takes no more than 5 minutes + معمولا بیش از 5 دقیقه طول نمی‎کشد + + + + PageHome + + + VPN protocol + پروتکل وی‎پی‎ان + + + + Servers + سرورها + + + + Unable change server while there is an active connection + امکان تغییر سرور در هنگام متصل بودن وجود ندارد + + + + PageProtocolAwgSettings + + + AmneziaWG settings + تنظیمات AmneziaWG + + + + Port + پورت + + + + Junk packet count + تعداد بسته‎های ناخواسته + + + + Junk packet minimum size + Junk packet minimum size + + + + Junk packet maximum size + Junk packet maximum size + + + + Init packet junk size + Init packet junk size + + + + Response packet junk size + Response packet junk size + + + + Init packet magic header + Init packet magic header + + + + Response packet magic header + Response packet magic header + + + + Transport packet magic header + Transport packet magic header + + + + Underload packet magic header + Underload packet magic header + + + + Remove AmneziaWG + حذف AmneziaWG + + + + Remove AmneziaWG from server? + آیا میخواهید AmneziaWG از سرور حذف شود؟ + + + + All users with whom you shared a connection will no longer be able to connect to it. + تمام کاربرانی که این اتصال را با آن‎ها با اشتراک گذاشته‎اید دیگر نمی‎توانند به آن متصل شوند. + + + All users who you shared a connection with will no longer be able to connect to it. + Все пользователи, с которыми вы поделились этим VPN-протоколом, больше не смогут к нему подключаться. + + + + Continue + ادامه + + + + Cancel + کنسل + + + + Save and Restart Amnezia + ذخیره و راه اندازی مجدد Amnezia + + + + PageProtocolCloakSettings + + + Cloak settings + تنظیمات Cloak + + + + Disguised as traffic from + پنهان کردن به عنوان ترافیک از + + + + Port + پورت + + + + + Cipher + رمزگذاری + + + + Save and Restart Amnezia + ذخیره و راه اندازی دوباره Amnezia + + + + PageProtocolOpenVpnSettings + + + OpenVPN settings + تنظیمات OpenVPN + + + + VPN Addresses Subnet + آدرس زیرشبکه وی‎پی‎ان + + + + Network protocol + پروتکل شبکه + + + + Port + پورت + + + + Auto-negotiate encryption + رمزگذاری خودکار + + + + + Hash + هش + + + + SHA512 + SHA512 + + + + SHA384 + SHA384 + + + + SHA256 + SHA256 + + + + SHA3-512 + SHA3-512 + + + + SHA3-384 + SHA3-384 + + + + SHA3-256 + SHA3-256 + + + + whirlpool + whirlpool + + + + BLAKE2b512 + BLAKE2b512 + + + + BLAKE2s256 + BLAKE2s256 + + + + SHA1 + SHA1 + + + + + Cipher + رمزگذاری + + + + AES-256-GCM + AES-256-GCM + + + + AES-192-GCM + AES-192-GCM + + + + AES-128-GCM + AES-128-GCM + + + + AES-256-CBC + AES-256-CBC + + + + AES-192-CBC + AES-192-CBC + + + + AES-128-CBC + AES-128-CBC + + + + ChaCha20-Poly1305 + ChaCha20-Poly1305 + + + + ARIA-256-CBC + ARIA-256-CBC + + + + CAMELLIA-256-CBC + CAMELLIA-256-CBC + + + + none + none + + + + TLS auth + اعتبار TLS + + + + Block DNS requests outside of VPN + مسدود کردن درخواست‎های DNS خارج از وی‎پی‎ان + + + + Additional client configuration commands + تنظیمات و دستورات اضافه برنامه متصل شونده + + + + + Commands: + دستورات: + + + + Additional server configuration commands + تنظیمات و دستورات اضافه سرور + + + + Remove OpenVPN + حذف OpenVPN + + + + Remove OpenVpn from server? + آیا میخواهید OpenVPN از سرور حذف شود؟ + + + + All users with whom you shared a connection will no longer be able to connect to it. + تمام کاربرانی که این اتصال را با آن‎ها با اشتراک گذاشته‎اید دیگر نمی‎توانند به آن متصل شوند. + + + All users who you shared a connection with will no longer be able to connect to it. + Все пользователи, с которыми вы поделились этим VPN-протоколом, больше не смогут к нему подключаться. + + + + Continue + ادامه + + + + Cancel + کنسل + + + + Save and Restart Amnezia + ذخیره و راه اندازی دوباره Amnezia + + + + PageProtocolRaw + + + settings + تنظیمات + + + + Show connection options + نمایش تنظیمات اتصال + + + + Connection options %1 + تنظیمات اتصال %1 + + + + Remove + حذف + + + + Remove %1 from server? + %1 از سرور حذف شود؟ + + + + All users with whom you shared a connection will no longer be able to connect to it. + تمام کاربرانی که این اتصال را با آن‎ها با اشتراک گذاشته‎اید دیگر نمی‎توانند به آن متصل شوند. + + + All users who you shared a connection with will no longer be able to connect to it. + Все пользователи, с которыми вы поделились этим VPN-протоколом, больше не смогут к нему подключаться. + + + + Continue + ادامه + + + + Cancel + کنسل + + + + PageProtocolShadowSocksSettings + + + ShadowSocks settings + تنظیمات ShadowSocks + + + + Port + پورت + + + + + Cipher + رمزگذاری + + + + Save and Restart Amnezia + ذخیره و راه اندازی دوباره Amnezia + + + + PageServerContainers + + Continue + Продолжить + + + + PageServiceDnsSettings + + + A DNS service is installed on your server, and it is only accessible via VPN. + + یک سرویس DSN بر روی سرور شما نصب شده و فقط از طریق وی‎پی‎ان قابل دسترسی می‎باشد + + + + + The DNS address is the same as the address of your server. You can configure DNS in the settings, under the connections tab. + آدرس DSN همان آدرس سرور شماست. میتوانید از قسمت تنظیمات و تب اتصالات DSN خود را تنظیم کنید + + + + Remove + جذف + + + + Remove %1 from server? + %1 از سرور حذف شود؟ + + + + Continue + ادامه + + + + Cancel + کنسل + + + + PageServiceSftpSettings + + + Settings updated successfully + تنظیمات با موفقیت به‎روز‎رسانی شد + + + + SFTP settings + تنظیمات SFTP + + + + Host + هاست + + + + + + + Copied + کپی شد + + + + Port + پورت + + + + Login + ورود + + + + Password + رمز عبور + + + + Mount folder on device + بارگذاری پوشه بر روی دستگاه + + + + In order to mount remote SFTP folder as local drive, perform following steps: <br> + برای بارگذاری پوشه SFTP بر روی درایو محلی قدم‎های زیر را انجام دهید: <br> + + + + + <br>1. Install the latest version of + <br> 1. آخرین نسخه را نصب کنید: + + + + + <br>2. Install the latest version of + <br> 2. آخرین نسخه را نصب کنید: + + + + Detailed instructions + جزییات دستورالعمل‎ها + + + + Remove SFTP and all data stored there + حذف SFTP و تمام داده‎های ذخیره شده در آن + + + + Remove SFTP and all data stored there? + پوشه SFTP و تمام داده‎های آن حذف شوند؟ + + + + Continue + ادامه + + + + Cancel + کنسل + + + + PageServiceTorWebsiteSettings + + + Settings updated successfully + تنظیمات با موفقیت به‎روز‎‌رسانی شد + + + + Tor website settings + تنظیمات وب‎سایت Tor + + + + Website address + آدرس وب‎سایت + + + + Copied + کپی شد + + + + Use <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> to open this url. + از <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> برای باز کردن این url استفاده کنید. + + + + After installation it takes several minutes while your onion site will become available in the Tor Network. + بعد از نصب چند دقیقه طول میکشد که سایت پیازی شما در شبکه Tor در دسترس قرار گیرد. + + + + When configuring WordPress set the this onion address as domain. + زمانی که سایت وردپرس را تنظیم میکنید این آدرس پیازی را به عنوان دامنه قرار دهید. + + + When configuring WordPress set the this address as domain. + При настройке WordPress укажите этот onion адрес в качестве домена. + + + + Remove website + حذف وب سایت + + + + The site with all data will be removed from the tor network. + سایت با تمام داده‎ها از شبکه Tor حذف خواهد شد. + + + + Continue + ادامه + + + + Cancel + کنسل + + + + PageSettings + + + Settings + تنظیمات + + + + Servers + سرورها + + + + Connection + ارتباط + + + + Application + نرم‎افزار + + + + Backup + بک‎آپ + + + + About AmneziaVPN + درباره Amnezia + + + + Close application + بستن نرم‎افزار + + + + PageSettingsAbout + + + Support the project with a donation + حمایت از پروژه با کمک‎های مالی + + + + This is a free and open source application. If you like it, support the developers with a donation. + این نرم‎افزار یک پروژه رایگان است. اگر آن را دوست دارید با کمک‎های مالی از توسعه‎دهندگان آن حمایت کنید. + + + + And if you don’t like the application, all the more reason to support it - the donation will be used for the improving the application. + و اگر آن‎را دوست ندارید دلایل بیشتری برای کمک به نرم‎افزار است، کمک‎های مالی شما برای بهبود نرم‎افزار استفاده میشود. + + + + Card on Patreon + کارت روی Patreon + + + + https://www.patreon.com/amneziavpn + https://www.patreon.com/amneziavpn + + + + Show other methods on Github + نمایش متد‎های دیگر در گیت هاب + + + + Contacts + مخاطب + + + + Telegram group + گروه تلگرام + + + + To discuss features + برای گفتگو در مورد ویژگی‎ها + + + + https://t.me/amnezia_vpn_en + https://t.me/amnezia_vpn + + + + Mail + ایمیل + + + + For reviews and bug reports + برای ارائه نظرات و گزارشات باگ + + + + Github + Github + + + + https://github.com/amnezia-vpn/amnezia-client + https://github.com/amnezia-vpn/amnezia-client + + + + Website + وب سایت + + + + https://amnezia.org + https://amnezia.org + + + + Check for updates + بررسی بروز‎رسانی + + + + PageSettingsApplication + + + Application + نرم افزار + + + + Allow application screenshots + مجوز اسکرین‎شات در برنامه + + + + Auto start + شروع خودکار + + + + Launch the application every time the device is starts + راه‎اندازی نرم‎افزار با هر بار روشن شدن دستگاه + + + + Start minimized + شروع به صورت کوچک + + + + Launch application minimized + راه‎اندازی برنامه به صورت کوچک + + + + Language + زبان + + + + Logging + گزارشات + + + + Enabled + فعال + + + + Disabled + غیر فعال + + + + Reset settings and remove all data from the application + ریست کردن تنظیمات و حذف تمام داده‎ها از نرم‎افزار + + + + Reset settings and remove all data from the application? + ریست کردن تنظیمات و حذف تمام داده‎ها از نرم‎افزار؟ + + + + All settings will be reset to default. All installed AmneziaVPN services will still remain on the server. + تمام تنظیمات به حالت پیش‎فرض ریست می‎شوند. تمام سرویس‎های Amnezia بر روی سرور باقی می‎مانند. + + + + Continue + ادامه + + + + Cancel + کنسل + + + + PageSettingsBackup + + + Backup + پشتیبان‎گیری + + + + Settings restored from backup file + تنظیمات از فایل پشتیبان بازیابی شد + + + + Configuration backup + پشتیبان‎گیری از پیکربندی + + + + You can save your settings to a backup file to restore them the next time you install the application. + می‎توانید تنظیمات را در یک فایل پشتیبان ذخیره کرده و دفعه بعد که نرم‎افزار را نصب کردید آن‎ها را بازیابی کنید. + + + + Make a backup + ایجاد یک پشتیبان + + + + Save backup file + ذخیره فایل پشتیبان + + + + + Backup files (*.backup) + Backup files (*.backup) + + + + Backup file saved + فایل پشتیبان ذخیره شد + + + + Restore from backup + بازیابی از پشتیبان + + + + Open backup file + باز کردن فایل پشتیبان + + + + Import settings from a backup file? + ورود تنظیمات از فایل پشتیبان؟ + + + + All current settings will be reset + تمام تنظیمات جاری ریست خواهد شد + + + + Continue + ادامه + + + + Cancel + کنسل + + + + PageSettingsConnection + + + Connection + ارتباط + + + + Auto connect + اتصال خودکار + + + + Connect to VPN on app start + اتصال به وی‎‎پی‎ان با شروع نرم‎افزار + + + + Use AmneziaDNS + استفاده از AmneziaDNS + + + + If AmneziaDNS is installed on the server + اگر AmneziaDNS بر روی سرور نصب شده باشد + + + + DNS servers + سرورهای DNS + + + + If AmneziaDNS is not used or installed + اگر AmneziaDNS نصب نشده یا استفاده نشود + + + + Site-based split tunneling + جداسازی ترافیک بر اساس سایت + + + + Allows you to select which sites you want to access through the VPN + میتوانید مشخص کنید که چه سایت‎هایی از وی‎پی‎ان استفاده کنند + + + + App-based split tunneling + جداسازی ترافیک بر اساس نرم‎افزار + + + + Allows you to use the VPN only for certain applications + میتوانید مشخص کنید که چه نرم‎افزارهایی از وی‎پی‎ان استفاده کنند + + + + PageSettingsDns + + + DNS servers + سرورهای DNS + + + + If AmneziaDNS is not used or installed + اگر AmneziaDNS نصب نباشد یا استفاده نشود + + + + Primary DNS + DNS اصلی + + + + Secondary DNS + DNS ثانویه + + + + Restore default + بازگشت به پیش‎فرض + + + + Restore default DNS settings? + بازگشت به تنظیمات پیش‎فرض DNS؟ + + + + Continue + ادامه + + + + Cancel + کنسل + + + + Settings have been reset + تنظیمات ریست شد + + + + Save + ذخیره + + + + Settings saved + ذخیره تنظیمات + + + + PageSettingsLogging + + + Logging + گزارشات + + + + Save logs + ذخیره گزارشات + + + + Open folder with logs + باز کردن پوشه گزارشات + + + + Save + ذخیره + + + + Logs files (*.log) + Logs files (*.log) + + + + Logs file saved + فایل گزارشات ذخیره شد + + + + Save logs to file + ذخیره گزارشات در فایل + + + + Clear logs? + پاک کردن گزارشات؟ + + + + Continue + ادامه + + + + Cancel + کنسل + + + + Logs have been cleaned up + گزارشات پاک شدند + + + + Clear logs + پاک کردن گزارشات + + + + PageSettingsServerData + + + All installed containers have been added to the application + تمام کانتینرهای نصب شده به نرم‎افزار اضافه شدند + + + + Clear Amnezia cache + پاک کردن حافظه داخلی Amnezia + + + + May be needed when changing other settings + وقتی تنظیمات دیگر را تغییر دهید ممکن است نیاز باشد + + + + Clear cached profiles? + پاک کردن پروفایل ذخیره شده؟ + + + + No new installed containers found + کانتینر نصب شده جدیدی پیدا نشد + + + + + + + + + + + Continue + ادامه + + + + + + Cancel + کنسل + + + + Check the server for previously installed Amnezia services + چک کردن سرویس‎های نصب شده Amnezia بر روی سرور + + + + Add them to the application if they were not displayed + اضافه کردن آنها به نرم‎افزار اگر نمایش داده نشده‎اند + + + + Remove server from application + حذف کردن سرور از نرم‎افزار + + + + Remove server? + حذف سرور؟ + + + + All installed AmneziaVPN services will still remain on the server. + تمام سرویس‎های نصب‎شده Amnezia همچنان بر روی سرور باقی خواهند ماند. + + + + Clear server from Amnezia software + پاک کردن سرور از نرم‎افزار Amnezia + + + + Clear server from Amnezia software? + سرور از نرم‎افزار Amnezia پاک شود؟ + + + + All containers will be deleted on the server. This means that configuration files, keys and certificates will be deleted. + تمام کانتینرها از سرور پاک شوند، به این معنی که تمام فایل‎های پیکربندی، کلیدها و مجوزها حذف خواهند شد. + + + + PageSettingsServerInfo + + + Server name + نام سرور + + + + Save + ذخیره + + + + Protocols + پروتکل‎ها + + + + Services + سرویس‎ها + + + + Data + داده + + + + PageSettingsServerProtocol + + + settings + تنظیمات + + + + Remove + حذف + + + + Remove %1 from server? + حذف %1 از سرور؟ + + + + All users with whom you shared a connection will no longer be able to connect to it. + تمام کاربرانی که این ارتباط را با آنها به اشتراک گذاشته‎اید دیگر نمی‎توانند به آن متصل شوند. + + + All users who you shared a connection with will no longer be able to connect to it. + Все пользователи, которым вы поделились VPN, больше не смогут к нему подключаться. + + + + Continue + ادامه + + + + Cancel + کنسل + + + + PageSettingsServersList + + + Servers + سرورها + + + + PageSettingsSplitTunneling + + + Addresses from the list should be accessed via VPN + دسترسی به آدرس‎های لیست از طریق وی‎پی‎ان + + + + Addresses from the list should not be accessed via VPN + دسترسی به آدرس‎های لیست بدون وی‎پی‎ان + + + + Split tunneling + جداسازی ترافیک + + + + Mode + حالت + + + + Remove + حذف + + + + Continue + ادامه + + + + Cancel + کنسل + + + + Site or IP + سایت یا آی‎پی + + + + Import/Export Sites + بارگذاری / خروجی‎گرفتن از سایت‎ها + + + + Import + بارگذاری + + + + Save site list + ذخیره لیست سایت‎ها + + + + Save sites + ذخیره سایت‎ها + + + + + + Sites files (*.json) + Sites files (*.json) + + + + Import a list of sites + بارگذاری لیست سایت‎ها + + + + Replace site list + جایگزین کردن لیست سایت + + + + + Open sites file + باز کردن فایل سایت‎ها + + + + Add imported sites to existing ones + اضافه کردن سایت‎های بارگذاری شده به سایت‎های موجود + + + + PageSetupWizardConfigSource + + + Server connection + ارتباط سرور + + + + Do not use connection code from public sources. It may have been created to intercept your data. + +It's okay as long as it's from someone you trust. + از کد اتصالاتی که در منابع عمومی هستند استفاده نکنید. ممکن است برای شنود اطلاعات شما ایجاد شده باشند. + +ایرادی ندارد که از طرف کسی باشد که به او اعتماد دارید. + + + + What do you have? + چی داری؟ + + + + File with connection settings + فایل شامل تنظیمات اتصال + + + + File with connection settings or backup + فایل شامل تنظیمات اتصال یا بک‎آپ + + + + Open config file + باز کردن فایل تنظیمات + + + + QR-code + QR-Code + + + + Key as text + متن شامل کلید + + + + PageSetupWizardCredentials + + Server connection + Подключение к серверу + + + + Server IP address [:port] + آدرس آی‎پی سرور (:پورت) + + + + 255.255.255.255:88 + 255.255.255.255:88 + + + + Password / SSH private key + Password / SSH private key + + + + Continue + ادامه + + + All data you enter will remain strictly confidential +and will not be shared or disclosed to the Amnezia or any third parties + تمام داده‎هایی که شما وارد می‎کنید به شدت محرمانه‎ است و با Amnezia یا هر شخص ثالث دیگری به اشتراک گذاشته نمی‎شود + + + + Enter the address in the format 255.255.255.255:88 + آدرس را با فرمت 255.255.255.255:88 وارد کنید + + + + Login to connect via SSH + ورود و اتصال با استفاده از SSH + + + + Configure your server + سرور خود را پیکربندی کنید + + + + All data you enter will remain strictly confidential and will not be shared or disclosed to the Amnezia or any third parties + + + + + Ip address cannot be empty + آدرس آی‎پی نمی‎تواند خالی باشد + + + + Login cannot be empty + نام‎کاربری نمی‎تواند خالی باشد + + + + Password/private key cannot be empty + پسورد یا کلید خصوصی نمی‎تواند خالی باشد + + + + PageSetupWizardEasy + + + What is the level of internet control in your region? + سطح کنترل اینترنت در منطقه شما چگونه است؟ + + + + Set up a VPN yourself + یک وی‎پی‎ان برای خودتان بسازید + + + + I want to choose a VPN protocol + می‎خواهم پروتکل وی‎پی‎ان را انتخاب کنم + + + + Continue + ادامه + + + + Set up later + بعدا تنظیم شود + + + + PageSetupWizardInstalling + + + The server has already been added to the application + سرور در حال حاضر به نرم‎افزار اضافه شده است + + + Amnesia has detected that your server is currently + Amnesia обнаружила, что ваш сервер в настоящее время + + + busy installing other software. Amnesia installation + занят установкой других протоколов или сервисов. Установка Amnesia + + + + Amnezia has detected that your server is currently + برنامه Amnezia تشخیص داده است که سرور در حال حاضر + + + + busy installing other software. Amnezia installation + مشغول نصب نرم‎افزار دیگری است. نصب Amnezia + + + + will pause until the server finishes installing other software + متوقف شده تا زمانی که سرور نصب نرم‎افزار دیگر را تمام کند + + + + Installing + در حال نصب + + + + Cancel installation + + + + + + Usually it takes no more than 5 minutes + معمولا بیش از 5 دقیقه طول نمی‎کشد + + + + PageSetupWizardProtocolSettings + + + Installing %1 + در حال نصب %1 + + + + More detailed + جزییات بیشتر + + + + Close + بستن + + + + Network protocol + پروتکل شبکه + + + + Port + پورت + + + + Install + نصب + + + + PageSetupWizardProtocols + + + VPN protocol + پروتکل وی‎پی‎ان + + + + Choose the one with the highest priority for you. Later, you can install other protocols and additional services, such as DNS proxy and SFTP. + پروتکلی که بیشترین اولویت را برای شما دارد انتخاب کنید. بعدا، میتوانید پروتکل‎ها و سرویس‎های اضافه مانند پروکسی DNS و SFTP را هم نصب کنید + + + + PageSetupWizardQrReader + + + Point the camera at the QR code and hold for a couple of seconds. + دوربین را روی QR Code بگیرید و برای چند ثانیه آن را نگه دارید. + + + + PageSetupWizardStart + + + Settings restored from backup file + تنظیمات از فایل بک‎آپ بازیابی شدند + + + + Free service for creating a personal VPN on your server. + سرویس رایگان برای ایجاد وی‎پی‎ان شخصی بر روی سرور خودتان. + + + + Helps you access blocked content without revealing your privacy, even to VPN providers. + به شما کمک می‎کند که بدون فاش کردن حریم شخصی خودتان حتی برای ارائه دهنده وی‎پی‎ان به محتوای مسدود شده دسترسی پیدا کنید. + + + + I have the data to connect + من داده برای اتصال دارم + + + + I have nothing + من هیچی ندارم + + + + PageSetupWizardTextKey + + + Connection key + کلید ارتباط + + + + A line that starts with vpn://... + یک کلید متنی که با vpn:// شروع می‎شود + + + + Key + کلید + + + + Insert + وارد کردن + + + + Continue + ادامه + + + + PageSetupWizardViewConfig + + + New connection + ارتباط جدید + + + + Do not use connection code from public sources. It could be created to intercept your data. + از کد اتصالی که در منابع عمومی هست استفاده نکنید. ممکن است برای شنود اطلاعات شما ایجاد شده باشد. + + + + Collapse content + جمع کردن محتوا + + + + Show content + نمایش محتوا + + + + Connect + اتصال + + + + PageShare + + + OpenVpn native format + فرمت محلی OpenVPN + + + + WireGuard native format + فرمت محلی WireGuard + + + VPN Access + VPN-Доступ + + + + Connection + ارتباط + + + VPN access without the ability to manage the server + Доступ к VPN, без возможности управления сервером + + + Access to server management. The user with whom you share full access to the connection will be able to add and remove your protocols and services to the server, as well as change settings. + Доступ к управлению сервером. Пользователь, с которым вы делитесь полным доступом к соединению, сможет добавлять и удалять ваши протоколы и службы на сервере, а также изменять настройки. + + + + + Server + سرور + + + Accessing + در حال دسترسی به + + + File with accessing settings to + فایل شامل تنظیمات دسترسی به + + + + Config revoked + + + + + Connection to + ارتباط با + + + + File with connection settings to + فایل شامل تنظیمات ارتباط با + + + + Save OpenVPN config + ذخیره تنظیمات OpenVPN + + + + Save WireGuard config + ذخیره تنظیمات WireGuard + + + + Save ShadowSocks config + + + + + Save Cloak config + + + + + For the AmneziaVPN app + برای نرم‎افزار AmneziaVPN + + + + ShadowSocks native format + + + + + Cloak native format + + + + + Share VPN Access + به اشتراک گذاشتن دسترسی وی‎پی‎ان + + + + Share full access to the server and VPN + + + + + Use for your own devices, or share with those you trust to manage the server. + + + + + + Users + + + + + User name + + + + + Search + + + + + Rename + + + + + Client name + + + + + Save + ذخیره + + + + Revoke + + + + + Revoke the config for a user - + + + + + The user will no longer be able to connect to your server. + + + + + Continue + + + + + Cancel + کنسل + + + Full access + دسترسی کامل + + + + Share VPN access without the ability to manage the server + به اشتراک گذاشتن دسترسی وی‎پی‎ان بدون امکان مدیریت سرور + + + Share access to server management. The user with whom you share full access to the server will be able to add and remove any protocols and services to the server, as well as change settings. + به اشتراک گذاری دسترسی به مدیریت سرور. کاربری که دسترسی کامل سرور با او به اشتراک گذاشته می‎شود می‎تواند پروتکل‌‎ها و سرویس‎ها را در سرور حذف یا اضافه کند و یا تنظیمات سرور را تغییر دهد. + + + + + Protocol + پروتکل + + + + + Connection format + فرمت ارتباط + + + + + Share + اشتراک‎گذاری + + + + PageShareFullAccess + + + Full access to the server and VPN + + + + + We recommend that you use full access to the server only for your own additional devices. + + + + + + If you share full access with other people, they can remove and add protocols and services to the server, which will cause the VPN to work incorrectly for all users. + + + + + + Server + + + + + Accessing + در حال دسترسی به + + + + File with accessing settings to + فایل شامل تنظیمات دسترسی به + + + + Share + اشتراک‎گذاری + + + + Connection to + ارتباط با + + + + File with connection settings to + فایل شامل تنظیمات ارتباط با + + + + PopupType + + + Close + بستن + + + + QKeychain::DeletePasswordJobPrivate + + + Password entry not found + Password entry not found + + + + Could not decrypt data + Could not decrypt data + + + + + Unknown error + Unknown error + + + + Could not open wallet: %1; %2 + Could not open wallet: %1; %2 + + + + Password not found + Password not found + + + + Could not open keystore + Could not open keystore + + + + Could not remove private key from keystore + Could not remove private key from keystore + + + + QKeychain::JobPrivate + + + Unknown error + Unknown error + + + + Access to keychain denied + Access to keychain denied + + + + QKeychain::PlainTextStore + + + Could not store data in settings: access error + Could not store data in settings: access error + + + + Could not store data in settings: format error + Could not store data in settings: format error + + + + Could not delete data from settings: access error + Could not delete data from settings: access error + + + + Could not delete data from settings: format error + Could not delete data from settings: format error + + + + Entry not found + Entry not found + + + + QKeychain::ReadPasswordJobPrivate + + + Password entry not found + Password entry not found + + + + + Could not decrypt data + Could not decrypt data + + + + D-Bus is not running + D-Bus is not running + + + + + Unknown error + Unknown error + + + + No keychain service available + No keychain service available + + + + Could not open wallet: %1; %2 + Could not open wallet: %1; %2 + + + + Access to keychain denied + Access to keychain denied + + + + Could not determine data type: %1; %2 + Could not determine data type: %1; %2 + + + + + Entry not found + Entry not found + + + + Unsupported entry type 'Map' + Unsupported entry type 'Map' + + + + Unknown kwallet entry type '%1' + Unknown kwallet entry type '%1' + + + + Password not found + Password not found + + + + Could not open keystore + Could not open keystore + + + + Could not retrieve private key from keystore + Could not retrieve private key from keystore + + + + Could not create decryption cipher + Could not create decryption cipher + + + + QKeychain::WritePasswordJobPrivate + + + Credential size exceeds maximum size of %1 + Credential size exceeds maximum size of %1 + + + + Credential key exceeds maximum size of %1 + Credential key exceeds maximum size of %1 + + + + Writing credentials failed: Win32 error code %1 + Writing credentials failed: Win32 error code %1 + + + + Encryption failed + Encryption failed + + + + D-Bus is not running + D-Bus is not running + + + + + Unknown error + Unknown error + + + + Could not open wallet: %1; %2 + Could not open wallet: %1; %2 + + + + Password not found + Password not found + + + + Could not open keystore + Could not open keystore + + + + Could not create private key generator + Could not create private key generator + + + + Could not generate new private key + Could not generate new private key + + + + Could not retrieve private key from keystore + Could not retrieve private key from keystore + + + + Could not create encryption cipher + Could not create encryption cipher + + + + Could not encrypt data + Could not encrypt data + + + + QObject + + + No error + No error + + + + Unknown Error + Unknown Error + + + + Function not implemented + Function not implemented + + + + Server check failed + Server check failed + + + + Server port already used. Check for another software + Server port already used. Check for another software + + + + Server error: Docker container missing + Server error: Docker container missing + + + + Server error: Docker failed + Server error: Docker failed + + + + Installation canceled by user + Installation canceled by user + + + + The user does not have permission to use sudo + The user does not have permission to use sudo + + + + Ssh request was denied + Ssh request was denied + + + + Ssh request was interrupted + Ssh request was interrupted + + + + Ssh internal error + Ssh internal error + + + + Invalid private key or invalid passphrase entered + Invalid private key or invalid passphrase entered + + + + The selected private key format is not supported, use openssh ED25519 key types or PEM key types + The selected private key format is not supported, use openssh ED25519 key types or PEM key types + + + + Timeout connecting to server + Timeout connecting to server + + + + Sftp error: End-of-file encountered + Sftp error: End-of-file encountered + + + + Sftp error: File does not exist + Sftp error: File does not exist + + + + Sftp error: Permission denied + Sftp error: Permission denied + + + + Sftp error: Generic failure + Sftp error: Generic failure + + + + Sftp error: Garbage received from server + Sftp error: Garbage received from server + + + + Sftp error: No connection has been set up + Sftp error: No connection has been set up + + + + Sftp error: There was a connection, but we lost it + Sftp error: There was a connection, but we lost it + + + + Sftp error: Operation not supported by libssh yet + Sftp error: Operation not supported by libssh yet + + + + Sftp error: Invalid file handle + Sftp error: Invalid file handle + + + + Sftp error: No such file or directory path exists + Sftp error: No such file or directory path exists + + + + Sftp error: An attempt to create an already existing file or directory has been made + Sftp error: An attempt to create an already existing file or directory has been made + + + + Sftp error: Write-protected filesystem + Sftp error: Write-protected filesystem + + + + Sftp error: No media was in remote drive + Sftp error: No media was in remote drive + + + + The config does not contain any containers and credentials for connecting to the server + + + + Failed to save config to disk + Failed to save config to disk + + + + OpenVPN config missing + OpenVPN config missing + + + + OpenVPN management server error + OpenVPN management server error + + + + OpenVPN executable missing + OpenVPN executable missing + + + + ShadowSocks (ss-local) executable missing + ShadowSocks (ss-local) executable missing + + + + Cloak (ck-client) executable missing + Cloak (ck-client) executable missing + + + + Amnezia helper service error + Amnezia helper service error + + + + OpenSSL failed + OpenSSL failed + + + + Can't connect: another VPN connection is active + Can't connect: another VPN connection is active + + + + Can't setup OpenVPN TAP network adapter + Can't setup OpenVPN TAP network adapter + + + + VPN pool error: no available addresses + VPN pool error: no available addresses + + + The config does not contain any containers and credentiaks for connecting to the server + The config does not contain any containers and credentiaks for connecting to the server + + + + Internal error + Internal error + + + + IPsec + IPsec + + + + IKEv2, paired with the IPSec encryption layer, stands as a modern and stable VPN protocol. +One of its distinguishing features is its ability to swiftly switch between networks and devices, making it particularly adaptive in dynamic network environments. +While it offers a blend of security, stability, and speed, it's essential to note that IKEv2 can be easily detected and is susceptible to blocking. + +* Available in the AmneziaVPN only on Windows +* Low power consumption, on mobile devices +* Minimal configuration +* Recognised by DPI analysis systems +* Works over UDP network protocol, ports 500 and 4500. + IKEv2 в сочетании с уровнем шифрования IPSec это современный и стабильный протокол VPN. +Он может быстро переключаться между сетями и устройствами, что делает его особенно адаптивным в динамичных сетевых средах. +Несмотря на сочетание безопасности, стабильности и скорости, необходимо отметить, что IKEv2 легко обнаруживается и подвержен блокировке. + +* Доступно в AmneziaVPN только для Windows. +* Низкое энергопотребление, на мобильных устройствах +* Минимальная конфигурация +* Распознается системами DPI-анализа +* Работает по сетевому протоколу UDP, порты 500 и 4500. + + + + DNS Service + DNS Сервис + + + + Sftp file sharing service + Сервис обмена файлами Sftp + + + + + Website in Tor network + Веб-сайт в сети Tor + + + + Amnezia DNS + Amnezia DNS + + + + OpenVPN is the most popular VPN protocol, with flexible configuration options. It uses its own security protocol with SSL/TLS for key exchange. + OpenVPN - популярный VPN-протокол, с гибкой настройкой. Имеет собственный протокол безопасности с SSL/TLS для обмена ключами. + + + + ShadowSocks - masks VPN traffic, making it similar to normal web traffic, but is recognised by analysis systems in some highly censored regions. + ShadowSocks - маскирует VPN-трафик под обычный веб-трафик, но распознается системами анализа в некоторых регионах с высоким уровнем цензуры. + + + + OpenVPN over Cloak - OpenVPN with VPN masquerading as web traffic and protection against active-probbing detection. Ideal for bypassing blocking in regions with the highest levels of censorship. + OpenVPN over Cloak - OpenVPN с маскировкой VPN под web-трафик и защитой от обнаружения active-probbing. Подходит для регионов с самым высоким уровнем цензуры. + + + + WireGuard - New popular VPN protocol with high performance, high speed and low power consumption. Recommended for regions with low levels of censorship. + WireGuard - Популярный VPN-протокол с высокой производительностью, высокой скоростью и низким энергопотреблением. Для регионов с низким уровнем цензуры. + + + + AmneziaWG - Special protocol from Amnezia, based on WireGuard. It's fast like WireGuard, but very resistant to blockages. Recommended for regions with high levels of censorship. + AmneziaWG - Специальный протокол от Amnezia, основанный на протоколе WireGuard. Он такой же быстрый, как WireGuard, но очень устойчив к блокировкам. Рекомендуется для регионов с высоким уровнем цензуры. + + + + IKEv2 - Modern stable protocol, a bit faster than others, restores connection after signal loss. It has native support on the latest versions of Android and iOS. + IKEv2 Современный стабильный протокол, немного быстрее других восстанавливает соединение после потери сигнала. Имеет нативную поддержку последних версиий Android и iOS. + + + + Deploy a WordPress site on the Tor network in two clicks. + Разверните сайт на WordPress в сети Tor в два клика. + + + + Replace the current DNS server with your own. This will increase your privacy level. + Замените DNS-сервер на Amnezia DNS. Это повысит уровень конфиденциальности. + + + + Creates a file vault on your server to securely store and transfer files. + Создайте на сервере файловое хранилище для безопасного хранения и передачи файлов. + + + + OpenVPN stands as one of the most popular and time-tested VPN protocols available. +It employs its unique security protocol, leveraging the strength of SSL/TLS for encryption and key exchange. Furthermore, OpenVPN's support for a multitude of authentication methods makes it versatile and adaptable, catering to a wide range of devices and operating systems. Due to its open-source nature, OpenVPN benefits from extensive scrutiny by the global community, which continually reinforces its security. With a strong balance of performance, security, and compatibility, OpenVPN remains a top choice for privacy-conscious individuals and businesses alike. + +* Available in the AmneziaVPN across all platforms +* Normal power consumption on mobile devices +* Flexible customisation to suit user needs to work with different operating systems and devices +* Recognised by DPI analysis systems and therefore susceptible to blocking +* Can operate over both TCP and UDP network protocols. + OpenVPN однин из самых популярных и проверенных временем VPN-протоколов. +В нем используется уникальный протокол безопасности, опирающийся на протокол SSL/TLS для шифрования и обмена ключами. Кроме того, поддержка OpenVPN множества методов аутентификации делает его универсальным и адаптируемым к широкому спектру устройств и операционных систем. Благодаря открытому исходному коду OpenVPN подвергается тщательному анализу со стороны мирового сообщества, что постоянно повышает его безопасность. Благодаря оптимальному соотношению производительности, безопасности и совместимости OpenVPN остается лучшим выбором как для частных лиц, так и для компаний, заботящихся о конфиденциальности. + +* Доступность AmneziaVPN для всех платформ +* Нормальное энергопотребление на мобильных устройствах +* Гибкая настройка под нужды пользователя для работы с различными операционными системами и устройствами +* Распознается системами DPI-анализа и поэтому подвержен блокировке +* Может работать по сетевым протоколам TCP и UDP. + + + + Shadowsocks, inspired by the SOCKS5 protocol, safeguards the connection using the AEAD cipher. Although Shadowsocks is designed to be discreet and challenging to identify, it isn't identical to a standard HTTPS connection.However, certain traffic analysis systems might still detect a Shadowsocks connection. Due to limited support in Amnezia, it's recommended to use AmneziaWG protocol. + +* Available in the AmneziaVPN only on desktop platforms +* Normal power consumption on mobile devices + +* Configurable encryption protocol +* Detectable by some DPI systems +* Works over TCP network protocol. + Shadowsocks, создан на основе протокола SOCKS5, защищает соединение с помощью шифра AEAD. Несмотря на то, что протокол Shadowsocks разработан таким образом, чтобы быть незаметным и сложным для идентификации, он не идентичен стандартному HTTPS-соединению. Однако некоторые системы анализа трафика все же могут обнаружить соединение Shadowsocks. В связи с ограниченной поддержкой в Amnezia рекомендуется использовать протокол AmneziaWG, или OpenVPN over Cloak. + +* Доступен в AmneziaVPN только на ПК ноутбуках. +* Настраиваемый протокол шифрования +* Обнаруживается некоторыми DPI-системами +* Работает по сетевому протоколу TCP. + + + + This is a combination of the OpenVPN protocol and the Cloak plugin designed specifically for blocking protection. + +OpenVPN provides a secure VPN connection by encrypting all Internet traffic between the client and the server. + +Cloak protects OpenVPN from detection and blocking. + +Cloak can modify packet metadata so that it completely masks VPN traffic as normal web traffic, and also protects the VPN from detection by Active Probing. This makes it very resistant to being detected + +Immediately after receiving the first data packet, Cloak authenticates the incoming connection. If authentication fails, the plugin masks the server as a fake website and your VPN becomes invisible to analysis systems. + +If there is a extreme level of Internet censorship in your region, we advise you to use only OpenVPN over Cloak from the first connection + +* Available in the AmneziaVPN across all platforms +* High power consumption on mobile devices +* Flexible settings +* Not recognised by DPI analysis systems +* Works over TCP network protocol, 443 port. + + OpenVPN over Cloak - это комбинация протокола OpenVPN и плагина Cloak, разработанного специально для защиты от блокировок. + +OpenVPN обеспечивает безопасное VPN-соединение за счет шифрования всего интернет-трафика между клиентом и сервером. + +Cloak защищает OpenVPN от обнаружения и блокировок. + +Cloak может изменять метаданные пакетов. Он полностью маскирует VPN-трафик под обычный веб-трафик, а также защищает VPN от обнаружения с помощью Active Probing. Это делает его очень устойчивым к обнаружению + +Сразу же после получения первого пакета данных Cloak проверяет подлинность входящего соединения. Если аутентификация не проходит, плагин маскирует сервер под поддельный сайт, и ваш VPN становится невидимым для аналитических систем. + +Если в вашем регионе существует экстремальный уровень цензуры в Интернете, мы советуем вам при первом подключении использовать только OpenVPN через Cloak + +* Доступность AmneziaVPN на всех платформах +* Высокое энергопотребление на мобильных устройствах +* Гибкие настройки +* Не распознается системами DPI-анализа +* Работает по сетевому протоколу TCP, 443 порт. + + + + + A relatively new popular VPN protocol with a simplified architecture. +Provides stable VPN connection, high performance on all devices. Uses hard-coded encryption settings. WireGuard compared to OpenVPN has lower latency and better data transfer throughput. +WireGuard is very susceptible to blocking due to its distinct packet signatures. Unlike some other VPN protocols that employ obfuscation techniques, the consistent signature patterns of WireGuard packets can be more easily identified and thus blocked by advanced Deep Packet Inspection (DPI) systems and other network monitoring tools. + +* Available in the AmneziaVPN across all platforms +* Low power consumption +* Minimum number of settings +* Easily recognised by DPI analysis systems, susceptible to blocking +* Works over UDP network protocol. + WireGuard - относительно новый популярный VPN-протокол с упрощенной архитектурой. +Обеспечивает стабильное VPN-соединение, высокую производительность на всех устройствах. Использует жестко заданные настройки шифрования. WireGuard по сравнению с OpenVPN имеет меньшую задержку и лучшую пропускную способность при передаче данных. +WireGuard очень восприимчив к блокированию из-за особенностей сигнатур пакетов. В отличие от некоторых других VPN-протоколов, использующих методы обфускации, последовательные сигнатуры пакетов WireGuard легче выявляются и, соответственно, блокируются современными системами глубокой проверки пакетов (DPI) и другими средствами сетевого мониторинга. + +* Доступность AmneziaVPN для всех платформ +* Низкое энергопотребление +* Минимальное количество настроек +* Легко распознается системами DPI-анализа, подвержен блокировке +* Работает по сетевому протоколу UDP. + + + + A modern iteration of the popular VPN protocol, AmneziaWG builds upon the foundation set by WireGuard, retaining its simplified architecture and high-performance capabilities across devices. +While WireGuard is known for its efficiency, it had issues with being easily detected due to its distinct packet signatures. AmneziaWG solves this problem by using better obfuscation methods, making its traffic blend in with regular internet traffic. +This means that AmneziaWG keeps the fast performance of the original while adding an extra layer of stealth, making it a great choice for those wanting a fast and discreet VPN connection. + +* Available in the AmneziaVPN across all platforms +* Low power consumption +* Minimum number of settings +* Not recognised by DPI analysis systems, resistant to blocking +* Works over UDP network protocol. + AmneziaWG - усовершенствованная версия популярного VPN-протокола Wireguard. AmneziaWG опирается на фундамент, заложенный WireGuard, сохраняя упрощенную архитектуру и высокопроизводительные возможности работы на разных устройствах. +Хотя WireGuard известен своей эффективностью, у него были проблемы с обнаружением из-за характерных сигнатур пакетов. AmneziaWG решает эту проблему за счет использования более совершенных методов обфускации, благодаря чему его трафик сливается с обычным интернет-трафиком. +Таким образом, AmneziaWG сохраняет высокую производительность оригинала, добавляя при этом дополнительный уровень скрытности, что делает его отличным выбором для тех, кому нужно быстрое и незаметное VPN-соединение. + +* Доступность AmneziaVPN на всех платформах +* Низкое энергопотребление +* Минимальное количество настроек +* Не распознается системами DPI-анализа, устойчив к блокировке +* Работает по сетевому протоколу UDP. + + + AmneziaWG container + AmneziaWG протокол + + + + Sftp file sharing service - is secure FTP service + Сервис обмена файлами Sftp - безопасный FTP-сервис + + + + Sftp service + Сервис SFTP + + + + Entry not found + Entry not found + + + + Access to keychain denied + Access to keychain denied + + + + No keyring daemon + No keyring daemon + + + + Already unlocked + Already unlocked + + + + No such keyring + No such keyring + + + + Bad arguments + Bad arguments + + + + I/O error + I/O error + + + + Cancelled + Cancelled + + + + Keyring already exists + Keyring already exists + + + + No match + No match + + + + Unknown error + Unknown error + + + + error 0x%1: %2 + error 0x%1: %2 + + + + SelectLanguageDrawer + + + Choose language + انتخاب زبان + + + + Settings + + + Server #1 + Server #1 + + + + + Server + Server + + + + SettingsController + + + Software version + نسخه نرم‎افزار + + + + All settings have been reset to default values + تمام تنظیمات به مقادیر پیش فرض ریست شد + + + + Cached profiles cleared + پروفایل ذخیره شده پاک شد + + + + Backup file is corrupted + فایل بک‎آپ خراب شده است + + + + ShareConnectionDrawer + + + + Save AmneziaVPN config + ذخیره تنظیمات AmneziaVPN + + + + Share + اشتراک‎گذاری + + + + Copy + کپی + + + + + Copied + کپی شد + + + + Copy config string + + + + + Show connection settings + نمایش تنظیمات ارتباط + + + + To read the QR code in the Amnezia app, select "Add server" → "I have data to connect" → "QR code, key or settings file" + برای خواندن QR Code در نرم‎افزار AmneziaVPN "اضافه کردن سرور" -> "من داده برای اتصال دارم" -> "QR Code، کلید یا فایل تنظیمات" + + + + SitesController + + + Hostname not look like ip adress or domain name + فرمت هاست شبیه آدرس آی‎پی یا نام دامنه نیست + + + + New site added: %1 + سایت جدید اضافه‎شد: %1 + + + + Site removed: %1 + سایت حذف شد: %1 + + + + Can't open file: %1 + فایل باز نشد: %1 + + + + Failed to parse JSON data from file: %1 + مشکل در تحلیل داده‎های JSON در فایل: %1 + + + + The JSON data is not an array in file: %1 + داده‎های JSON در فایل به صورت آرایه نیستند: %1 + + + + Import completed + بارگذاری کامل شد + + + + Export completed + خروجی گرفتن کامل شد + + + + SystemTrayNotificationHandler + + + + Show + نمایش + + + + + Connect + اتصال + + + + + Disconnect + قطع ارتباط + + + + + Visit Website + بازدید وب‎سایت + + + + + Quit + خروج + + + + TextFieldWithHeaderType + + + The field can't be empty + Поле не может быть пустым + + + + VpnConnection + + + Mbps + Mbps + + + + VpnProtocol + + + Unknown + ناشناخته + + + + Disconnected + قطع شده + + + + Preparing + درحال آماده‎سازی + + + + Connecting... + برقراری ارتباط... + + + + Connected + وصل شد + + + + Disconnecting... + در حال قطع شدن... + + + + Reconnecting... + برقراری ارتباط دوباره... + + + + Error + خطا + + + + amnezia::ContainerProps + + + Low + پایین + + + + Medium or High + متوسط یا بالا + + + + Extreme + شدید + + + + I just want to increase the level of my privacy. + من فقط میخواهم سطح حریم شخصی خودم را بالا ببرم + + + + I want to bypass censorship. This option recommended in most cases. + من میخواهم از سانسور عبور کنم. این گزینه در اکثر موارد توصیه می‎‌شود + + + + Most VPN protocols are blocked. Recommended if other options are not working. + اکثر پروتکل‎های وی‎پی‎ان مسدود شده‎اند. در مواردی که بقیه گزینه‎ها کار نمی‎کنند توصی می‎شود. + + + High + Высокий + + + Medium + Средний + + + Many foreign websites and VPN providers are blocked + Многие иностранные сайты и VPN-провайдеры заблокированы + + + Some foreign sites are blocked, but VPN providers are not blocked + Некоторые иностранные сайты заблокированы, но VPN-провайдеры не блокируются + + + I just want to increase the level of privacy + Хочу просто повысить уровень приватности + + + + main2 + + + Private key passphrase + عبارت کلید خصوصی + + + + Save + ذخیره + + + diff --git a/client/translations/amneziavpn_ru.ts b/client/translations/amneziavpn_ru.ts index 861beec2..4f24e540 100644 --- a/client/translations/amneziavpn_ru.ts +++ b/client/translations/amneziavpn_ru.ts @@ -8,9 +8,8 @@ Раздельное туннелирование для "Wireguard" не реализовано,опция отключена - Split tunneling for %1 is not implemented, the option was disabled - Раздельное туннелирование для %1 не реализовано, опция отключена + Раздельное туннелирование для %1 не реализовано, опция отключена @@ -27,45 +26,58 @@ VPN Подключен + + ApiController + + + Error when retrieving configuration from cloud server + + + ConnectionController - + VPN Protocols is not installed. Please install VPN container at first VPN протоколы не установлены. Пожалуйста, установите протокол - + Connection... Подключение... - + Connected Подключено - + Settings updated successfully, Reconnnection... Настройки успешно обновлены. Подключение... - + + Settings updated successfully + Настройки успешно обновлены + + + Reconnection... Переподключение... - - - + + + Connect Подключиться - + Disconnection... Отключение... @@ -114,7 +126,7 @@ ExportController - + Access error! Ошибка доступа! @@ -127,7 +139,7 @@ Невозможно изменить протокол при активном соединении - + The selected protocol is not supported on the current platform Выбранный протокол не поддерживается на данном устройстве @@ -139,7 +151,7 @@ ImportController - + Scanned %1 of %2. Отсканировано %1 из%2. @@ -147,58 +159,57 @@ InstallController - - + + %1 installed successfully. %1 успешно установлен. - - + + %1 is already installed on the server. %1 уже установлен на сервер. - + Added containers that were already installed on the server - -В приложение добавлены обнаруженные на сервере протоклы и сервисы + - + Already installed containers were found on the server. All installed containers have been added to the application На сервере обнаружены установленные протоколы и сервисы, все они добавлены в приложение - + Settings updated successfully Настройки успешно обновлены - + Server '%1' was removed Сервер '%1' был удален - + All containers from server '%1' have been removed Все протоклы и сервисы были удалены с сервера '%1' - + %1 has been removed from the server '%2' %1 был удален с сервера '%2' - + Please login as the user Пожалуйста, войдите в систему от имени пользователя - + Server added successfully Сервер успешно добавлен @@ -266,17 +277,17 @@ Already installed containers were found on the server. All installed containers PageHome - + VPN protocol VPN протокол - + Servers Серверы - + Unable change server while there is an active connection Невозможно изменить сервер при активном соединении @@ -376,28 +387,28 @@ Already installed containers were found on the server. All installed containers PageProtocolCloakSettings - + Cloak settings Настройки Cloak - + Disguised as traffic from Замаскировать трафик под - + Port Порт - - + + Cipher Шифрование - + Save and Restart Amnezia Сохранить и перезагрузить Amnezia @@ -652,23 +663,23 @@ Already installed containers were found on the server. All installed containers PageProtocolShadowSocksSettings - + ShadowSocks settings Настройки ShadowSocks - + Port Порт - - + + Cipher Шифрование - + Save and Restart Amnezia Сохранить и перезагрузить Amnezia @@ -1001,17 +1012,17 @@ Already installed containers were found on the server. All installed containers Allow application screenshots - Разрешить скриншоты + Auto start - Авто-запуск + Launch the application every time the device is starts - Запускать приложение при каждом включении + @@ -1642,18 +1653,17 @@ It's okay as long as it's from someone you trust. Password / SSH private key - + Continue Продолжить - All data you enter will remain strictly confidential and will not be shared or disclosed to the Amnezia or any third parties - Все данные, которые вы вводите, останутся строго конфиденциальными и не будут переданы или раскрыты Amnezia или каким-либо третьим сторонам + Все данные, которые вы вводите, останутся строго конфиденциальными и не будут переданы или раскрыты Amnezia или каким-либо третьим сторонам - + Enter the address in the format 255.255.255.255:88 Введите адрес в формате 255.255.255.255:88 @@ -1668,17 +1678,22 @@ and will not be shared or disclosed to the Amnezia or any third parties Настроить ваш сервер - + + All data you enter will remain strictly confidential and will not be shared or disclosed to the Amnezia or any third parties + + + + Ip address cannot be empty Поле Ip address не может быть пустым - + Login cannot be empty Поле Login не может быть пустым - + Password/private key cannot be empty Поле Password/private key не может быть пустым @@ -1714,7 +1729,7 @@ and will not be shared or disclosed to the Amnezia or any third parties PageSetupWizardInstalling - + The server has already been added to the application Сервер уже был добавлен в приложение @@ -1727,28 +1742,33 @@ and will not be shared or disclosed to the Amnezia or any third parties занят установкой других протоколов или сервисов. Установка Amnesia - + Amnezia has detected that your server is currently Amnezia обнаружила, что ваш сервер в настоящее время - + busy installing other software. Amnezia installation занят установкой другого программного обеспечения. Установка Amnezia - + will pause until the server finishes installing other software будет приостановлена до тех пор, пока сервер не завершит установку - + Installing Установка + + + Cancel installation + + - + Usually it takes no more than 5 minutes Обычно это занимает не более 5 минут @@ -1815,22 +1835,22 @@ and will not be shared or disclosed to the Amnezia or any third parties Восстановление настроек из бэкап файла - + Free service for creating a personal VPN on your server. Простое и бесплатное приложение для запуска self-hosted VPN с высокими требованиями к приватности. - + Helps you access blocked content without revealing your privacy, even to VPN providers. Помогает получить доступ к заблокированному контенту, не раскрывая вашу конфиденциальность даже провайдерам VPN. - + I have the data to connect У меня есть данные для подключения - + I have nothing У меня ничего нет @@ -1894,12 +1914,12 @@ and will not be shared or disclosed to the Amnezia or any third parties PageShare - + OpenVpn native format OpenVpn нативный формат - + WireGuard native format WireGuard нативный формат @@ -1908,7 +1928,7 @@ and will not be shared or disclosed to the Amnezia or any third parties VPN-Доступ - + Connection Соединение @@ -1921,84 +1941,214 @@ and will not be shared or disclosed to the Amnezia or any third parties Доступ к управлению сервером. Пользователь, с которым вы делитесь полным доступом к соединению, сможет добавлять и удалять ваши протоколы и службы на сервере, а также изменять настройки. - - + + Server Сервер - Accessing - Доступ + Доступ - - File with accessing settings to - Файл с настройками доступа к + + Config revoked + - + Connection to Подключение к - + File with connection settings to Файл с настройками доступа к - + Save OpenVPN config Сохранить OpenVPN config - + Save WireGuard config Сохранить WireGuard config - + + Save ShadowSocks config + + + + + Save Cloak config + + + + For the AmneziaVPN app Для AmneziaVPN - + + ShadowSocks native format + + + + + Cloak native format + + + + Share VPN Access Поделиться VPN - - Full access - Полный доступ + + Share full access to the server and VPN + - + + Use for your own devices, or share with those you trust to manage the server. + + + + + + Users + + + + + User name + + + + + Search + + + + + Rename + + + + + Client name + + + + + Save + Сохранить + + + + Revoke + + + + + Revoke the config for a user - + + + + + The user will no longer be able to connect to your server. + + + + + Continue + Продолжить + + + + Cancel + Отменить + + + Full access + Полный доступ + + + Share VPN access without the ability to manage the server Поделиться доступом к VPN, без возможности управления сервером - - Share access to server management. The user with whom you share full access to the server will be able to add and remove any protocols and services to the server, as well as change settings. - Поделиться доступом к управлению сервером. Пользователь, с которым вы делитесь полным доступом к серверу, сможет добавлять и удалять любые протоколы и службы на сервере, а также изменять настройки. - - - - + + Protocol Протокол - - + + Connection format Формат подключения - + + Share Поделиться + + PageShareFullAccess + + + Full access to the server and VPN + + + + + We recommend that you use full access to the server only for your own additional devices. + + + + + + If you share full access with other people, they can remove and add protocols and services to the server, which will cause the VPN to work incorrectly for all users. + + + + + + Server + + + + + Accessing + Доступ + + + + File with accessing settings to + + + + + Share + Поделиться + + + + Connection to + Подключение к + + + + File with connection settings to + Файл с настройками доступа к + + PopupType @@ -2385,67 +2535,66 @@ and will not be shared or disclosed to the Amnezia or any third parties Sftp error: No media was in remote drive - - Failed to save config to disk - Failed to save config to disk + + The config does not contain any containers and credentials for connecting to the server + - + Failed to save config to disk + Failed to save config to disk + + + OpenVPN config missing OpenVPN config missing - + OpenVPN management server error OpenVPN management server error - + OpenVPN executable missing OpenVPN executable missing - + ShadowSocks (ss-local) executable missing ShadowSocks (ss-local) executable missing - + Cloak (ck-client) executable missing Cloak (ck-client) executable missing - + Amnezia helper service error Amnezia helper service error - + OpenSSL failed OpenSSL failed - + Can't connect: another VPN connection is active Can't connect: another VPN connection is active - + Can't setup OpenVPN TAP network adapter Can't setup OpenVPN TAP network adapter - + VPN pool error: no available addresses VPN pool error: no available addresses - - The config does not contain any containers and credentiaks for connecting to the server - The config does not contain any containers and credentiaks for connecting to the server - - - + Internal error Internal error @@ -2732,16 +2881,6 @@ This means that AmneziaWG keeps the fast performance of the original while addin error 0x%1: %2 error 0x%1: %2 - - - WireGuard Configuration Highlighter - - - - - &Randomize colors - - SelectLanguageDrawer @@ -2773,17 +2912,17 @@ This means that AmneziaWG keeps the fast performance of the original while addin Версия ПО - + All settings have been reset to default values Все настройки были сброшены к значению "По умолчанию" - + Cached profiles cleared Кэш профиля очищен - + Backup file is corrupted Backup файл поврежден @@ -2808,16 +2947,22 @@ This means that AmneziaWG keeps the fast performance of the original while addin + Copied Скопировано - - Show connection settings - Показать настройки подключения + + Copy config string + - + + Show connection settings + + + + To read the QR code in the Amnezia app, select "Add server" → "I have data to connect" → "QR code, key or settings file" Для считывания QR-кода в приложении Amnezia выберите "Добавить сервер" → "У меня есть данные для подключения" → "QR-код, ключ или файл настроек" @@ -2909,7 +3054,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin VpnConnection - + Mbps Mbps diff --git a/client/translations/amneziavpn_zh_CN.ts b/client/translations/amneziavpn_zh_CN.ts index 8833d5c6..e63344d8 100644 --- a/client/translations/amneziavpn_zh_CN.ts +++ b/client/translations/amneziavpn_zh_CN.ts @@ -7,11 +7,6 @@ Split tunneling for WireGuard is not implemented, the option was disabled 未启用选项,还未实现基于WireGuard协议的VPN分离 - - - Split tunneling for %1 is not implemented, the option was disabled - - AndroidController @@ -27,47 +22,60 @@ VPN已连接 + + ApiController + + + Error when retrieving configuration from cloud server + + + ConnectionController - - - + + + Connect 连接 - + VPN Protocols is not installed. Please install VPN container at first 请先安装VPN协议 - + Connection... 连接中 - + Connected 已连接 - + Reconnection... 重连中 - + Disconnection... 断开中 - + Settings updated successfully, Reconnnection... 配置已更新,重连中 + + + Settings updated successfully + 配置更新成功 + ConnectionTypeSelectionDrawer @@ -125,7 +133,7 @@ ExportController - + Access error! 访问错误 @@ -138,7 +146,7 @@ 已建立连接时无法更改服务器配置 - + The selected protocol is not supported on the current platform 当前平台不支持所选协议 @@ -150,7 +158,7 @@ ImportController - + Scanned %1 of %2. 扫描 %1 of %2. @@ -166,47 +174,47 @@ 已安装在服务器上 - - + + %1 installed successfully. %1 安装成功。 - - + + %1 is already installed on the server. 服务器上已经安装 %1。 - + Added containers that were already installed on the server 添加已安装在服务器上的容器 - + Already installed containers were found on the server. All installed containers have been added to the application 在服务上发现已经安装协议并添加至应用 - + Settings updated successfully 配置更新成功 - + Server '%1' was removed 已移除服务器 '%1' - + All containers from server '%1' have been removed 服务器 '%1' 的所有容器已移除 - + %1 has been removed from the server '%2' %1 已从服务器 '%2' 上移除 @@ -227,12 +235,12 @@ Already installed containers were found on the server. All installed containers 协议已从 - + Please login as the user 请以用户身份登录 - + Server added successfully 增加服务器成功 @@ -300,17 +308,17 @@ Already installed containers were found on the server. All installed containers PageHome - + VPN protocol VPN协议 - + Servers 服务器 - + Unable change server while there is an active connection 已建立连接时无法更改服务器配置 @@ -410,28 +418,28 @@ Already installed containers were found on the server. All installed containers PageProtocolCloakSettings - + Cloak settings Cloak 配置 - + Disguised as traffic from 伪装流量为 - + Port 端口 - - + + Cipher 加密算法 - + Save and Restart Amnezia 保存并重启Amnezia @@ -702,23 +710,23 @@ Already installed containers were found on the server. All installed containers PageProtocolShadowSocksSettings - + ShadowSocks settings ShadowSocks 配置 - + Port 端口 - - + + Cipher 加密算法 - + Save and Restart Amnezia 保存并重启Amnezia @@ -1757,34 +1765,38 @@ It's okay as long as it's from someone you trust. 密码 或 私钥 - + Continue 继续 - + + All data you enter will remain strictly confidential and will not be shared or disclosed to the Amnezia or any third parties + + + All data you enter will remain strictly confidential and will not be shared or disclosed to the Amnezia or any third parties - 您输入的所有数据将严格保密 + 您输入的所有数据将严格保密 不会向 Amnezia 或任何第三方分享或披露 - + Ip address cannot be empty IP不能为空 - + Enter the address in the format 255.255.255.255:88 按照这种格式输入 255.255.255.255:88 - + Login cannot be empty 账号不能为空 - + Password/private key cannot be empty 密码或私钥不能为空 @@ -1821,25 +1833,30 @@ and will not be shared or disclosed to the Amnezia or any third parties PageSetupWizardInstalling - + Usually it takes no more than 5 minutes 通常不超过5分钟 - + The server has already been added to the application 服务器已添加到应用软件中 - + Amnezia has detected that your server is currently Amnezia 检测到您的服务器当前 - + busy installing other software. Amnezia installation 正安装其他软件。Amnezia安装 + + + Cancel installation + + Amnesia has detected that your server is currently Amnezia 检测到您的服务器当前 @@ -1849,12 +1866,12 @@ and will not be shared or disclosed to the Amnezia or any third parties 正安装其他软件。Amnezia安装 - + will pause until the server finishes installing other software 将暂停,直到其他软件安装完成。 - + Installing 安装中 @@ -1921,22 +1938,22 @@ and will not be shared or disclosed to the Amnezia or any third parties 从备份文件还原配置 - + Free service for creating a personal VPN on your server. 在您的服务器上架设私人免费VPN服务。 - + Helps you access blocked content without revealing your privacy, even to VPN providers. 帮助您访问受限内容,保护您的隐私,即使是VPN提供商也无法获取。 - + I have the data to connect 我有连接配置 - + I have nothing 我没有 @@ -2000,58 +2017,137 @@ and will not be shared or disclosed to the Amnezia or any third parties PageShare - + Save OpenVPN config 保存OpenVPN配置 - + Save WireGuard config 保存WireGuard配置 - + + Save ShadowSocks config + + + + + Save Cloak config + + + + For the AmneziaVPN app AmneziaVPN 应用 - + OpenVpn native format OpenVPN原生格式 - + WireGuard native format WireGuard原生格式 - + + ShadowSocks native format + + + + + Cloak native format + + + + Share VPN Access 共享 VPN 访问 - + + Share full access to the server and VPN + + + + + Use for your own devices, or share with those you trust to manage the server. + + + + + + Users + + + + Share VPN access without the ability to manage the server 共享 VPN 访问,无需管理服务器 - + + Search + + + + + Rename + + + + + Client name + + + + + Save + 保存 + + + + Revoke + + + + + Revoke the config for a user - + + + + + The user will no longer be able to connect to your server. + + + + + Continue + 继续 + + + + Cancel + 取消 + + Share access to server management. The user with whom you share full access to the server will be able to add and remove any protocols and services to the server, as well as change settings. - 共享服务器管理访问权限。与您共享服务器全部访问权限的用户将可以添加和删除服务器上的任何协议和服务,以及更改设置。 + 共享服务器管理访问权限。与您共享服务器全部访问权限的用户将可以添加和删除服务器上的任何协议和服务,以及更改设置。 VPN Access 访问VPN - + Connection 连接 - Full access - 完全访问 + 完全访问 VPN access without the ability to manage the server @@ -2074,23 +2170,21 @@ and will not be shared or disclosed to the Amnezia or any third parties 服务器 - - + + Server 服务器 - Accessing - 访问 + 访问 - File with accessing settings to - 访问配置文件的内容为: + 访问配置文件的内容为: - + File with connection settings to 连接配置文件的内容为: @@ -2099,28 +2193,89 @@ and will not be shared or disclosed to the Amnezia or any third parties 协议 - - + + Protocol 协议 - + Connection to 连接到 - - + + Config revoked + + + + + User name + + + + + Connection format 连接格式 - + + Share 共享 + + PageShareFullAccess + + + Full access to the server and VPN + + + + + We recommend that you use full access to the server only for your own additional devices. + + + + + + If you share full access with other people, they can remove and add protocols and services to the server, which will cause the VPN to work incorrectly for all users. + + + + + + Server + 服务器 + + + + Accessing + 访问 + + + + File with accessing settings to + 访问配置文件的内容为: + + + + Share + 共享 + + + + Connection to + 连接到 + + + + File with connection settings to + 连接配置文件的内容为: + + PopupType @@ -2512,67 +2667,70 @@ and will not be shared or disclosed to the Amnezia or any third parties Sftp 错误: 远程驱动器中没有媒介 - Failed to save config to disk - 配置保存到磁盘失败 + 配置保存到磁盘失败 - + OpenVPN config missing OpenVPN配置丢失 - + OpenVPN management server error OpenVPN 管理服务器错误 - + OpenVPN executable missing OpenVPN 可执行文件丢失 - + ShadowSocks (ss-local) executable missing ShadowSocks (ss-local) 执行文件丢失 - + Cloak (ck-client) executable missing Cloak (ck-client) 执行文件丢失 - + Amnezia helper service error Amnezia 服务连接失败 - + OpenSSL failed OpenSSL错误 - + Can't connect: another VPN connection is active 无法连接:另一个VPN连接处于活跃状态 - + Can't setup OpenVPN TAP network adapter 无法设置 OpenVPN TAP 网络适配器 - + VPN pool error: no available addresses VPN 池错误:没有可用地址 - - The config does not contain any containers and credentiaks for connecting to the server - 该配置不包含任何用于连接到服务器的容器和凭据。 + + The config does not contain any containers and credentials for connecting to the server + - + The config does not contain any containers and credentiaks for connecting to the server + 该配置不包含任何用于连接到服务器的容器和凭据。 + + + Internal error 内部错误 @@ -2871,16 +3029,6 @@ While it offers a blend of security, stability, and speed, it's essential t error 0x%1: %2 错误 0x%1: %2 - - - WireGuard Configuration Highlighter - - - - - &Randomize colors - - SelectLanguageDrawer @@ -2912,17 +3060,17 @@ While it offers a blend of security, stability, and speed, it's essential t 软件版本 - + Backup file is corrupted 备份文件已损坏 - + All settings have been reset to default values 所配置恢复为默认值 - + Cached profiles cleared 缓存的配置文件已清除 @@ -2947,11 +3095,17 @@ While it offers a blend of security, stability, and speed, it's essential t + Copied 已拷贝 - + + Copy config string + + + + Show connection settings 显示连接配置 @@ -2960,7 +3114,7 @@ While it offers a blend of security, stability, and speed, it's essential t 展示内容 - + To read the QR code in the Amnezia app, select "Add server" → "I have data to connect" → "QR code, key or settings file" 要应用二维码到 Amnezia,请底部工具栏点击“+”→“连接方式”→“二维码、授权码或配置文件” @@ -3052,7 +3206,7 @@ While it offers a blend of security, stability, and speed, it's essential t VpnConnection - + Mbps diff --git a/client/ui/controllers/apiController.cpp b/client/ui/controllers/apiController.cpp new file mode 100644 index 00000000..14a05410 --- /dev/null +++ b/client/ui/controllers/apiController.cpp @@ -0,0 +1,129 @@ +#include "apiController.h" + +#include +#include +#include + +#include "configurators/openvpn_configurator.h" + +namespace +{ + namespace configKey + { + constexpr char cloak[] = "cloak"; + + constexpr char apiEdnpoint[] = "api_endpoint"; + constexpr char accessToken[] = "api_key"; + constexpr char certificate[] = "certificate"; + constexpr char publicKey[] = "public_key"; + constexpr char protocol[] = "protocol"; + } +} + +ApiController::ApiController(const QSharedPointer &serversModel, + const QSharedPointer &containersModel, QObject *parent) + : QObject(parent), m_serversModel(serversModel), m_containersModel(containersModel) +{ +} + +QString ApiController::genPublicKey(const QString &protocol) +{ + if (protocol == configKey::cloak) { + return "."; + } + return QString(); +} + +QString ApiController::genCertificateRequest(const QString &protocol) +{ + if (protocol == configKey::cloak) { + m_certRequest = OpenVpnConfigurator::createCertRequest(); + return m_certRequest.request; + } + return QString(); +} + +void ApiController::processCloudConfig(const QString &protocol, QString &config) +{ + if (protocol == configKey::cloak) { + config.replace("", "\n"); + config.replace("$OPENVPN_PRIV_KEY", m_certRequest.privKey); + return; + } + return; +} + +bool ApiController::updateServerConfigFromApi() +{ + auto serverConfig = m_serversModel->getDefaultServerConfig(); + + auto containerConfig = serverConfig.value(config_key::containers).toArray(); + + if (serverConfig.value(config_key::configVersion).toInt() && containerConfig.isEmpty()) { + QNetworkAccessManager manager; + + QNetworkRequest request; + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Authorization", + "Api-Key " + serverConfig.value(configKey::accessToken).toString().toUtf8()); + QString endpoint = serverConfig.value(configKey::apiEdnpoint).toString(); + request.setUrl(endpoint.replace("https", "http")); // todo remove + + QString protocol = serverConfig.value(configKey::protocol).toString(); + + QJsonObject obj; + + obj[configKey::publicKey] = genPublicKey(protocol); + obj[configKey::certificate] = genCertificateRequest(protocol); + + QByteArray requestBody = QJsonDocument(obj).toJson(); + qDebug() << requestBody; + + QScopedPointer reply; + reply.reset(manager.post(request, requestBody)); + + QEventLoop wait; + QObject::connect(reply.get(), &QNetworkReply::finished, &wait, &QEventLoop::quit); + wait.exec(); + + if (reply->error() == QNetworkReply::NoError) { + QString contents = QString::fromUtf8(reply->readAll()); + auto data = QJsonDocument::fromJson(contents.toUtf8()).object().value(config_key::config).toString(); + + data.replace("vpn://", ""); + QByteArray ba = QByteArray::fromBase64(data.toUtf8(), + QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); + + QByteArray ba_uncompressed = qUncompress(ba); + if (!ba_uncompressed.isEmpty()) { + ba = ba_uncompressed; + } + + QString configStr = ba; + processCloudConfig(protocol, configStr); + + QJsonObject cloudConfig = QJsonDocument::fromJson(configStr.toUtf8()).object(); + + serverConfig.insert("cloudConfig", cloudConfig); + serverConfig.insert(config_key::dns1, cloudConfig.value(config_key::dns1)); + serverConfig.insert(config_key::dns2, cloudConfig.value(config_key::dns2)); + serverConfig.insert(config_key::containers, cloudConfig.value(config_key::containers)); + serverConfig.insert(config_key::hostName, cloudConfig.value(config_key::hostName)); + + auto defaultContainer = cloudConfig.value(config_key::defaultContainer).toString(); + serverConfig.insert(config_key::defaultContainer, defaultContainer); + m_serversModel->editServer(serverConfig); + emit m_serversModel->defaultContainerChanged(ContainerProps::containerFromString(defaultContainer)); + } else { + QString err = reply->errorString(); + qDebug() << QString::fromUtf8(reply->readAll()); //todo remove debug output + qDebug() << reply->error(); + qDebug() << err; + qDebug() << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); + emit errorOccurred(tr("Error when retrieving configuration from cloud server")); + return false; + } + } + + return true; +} diff --git a/client/ui/controllers/apiController.h b/client/ui/controllers/apiController.h new file mode 100644 index 00000000..1ce933c6 --- /dev/null +++ b/client/ui/controllers/apiController.h @@ -0,0 +1,36 @@ +#ifndef APICONTROLLER_H +#define APICONTROLLER_H + +#include + +#include "configurators/openvpn_configurator.h" +#include "ui/models/containers_model.h" +#include "ui/models/servers_model.h" + +class ApiController : public QObject +{ + Q_OBJECT + +public: + explicit ApiController(const QSharedPointer &serversModel, + const QSharedPointer &containersModel, QObject *parent = nullptr); + +public slots: + bool updateServerConfigFromApi(); + +signals: + void errorOccurred(const QString &errorMessage); + +private: + QString genPublicKey(const QString &protocol); + QString genCertificateRequest(const QString &protocol); + + void processCloudConfig(const QString &protocol, QString &config); + + QSharedPointer m_serversModel; + QSharedPointer m_containersModel; + + OpenVpnConfigurator::ConnectionData m_certRequest; +}; + +#endif // APICONTROLLER_H diff --git a/client/ui/controllers/connectionController.cpp b/client/ui/controllers/connectionController.cpp index 74438dcc..25bebfb1 100644 --- a/client/ui/controllers/connectionController.cpp +++ b/client/ui/controllers/connectionController.cpp @@ -26,13 +26,10 @@ ConnectionController::ConnectionController(const QSharedPointer &s void ConnectionController::openConnection() { int serverIndex = m_serversModel->getDefaultServerIndex(); - ServerCredentials credentials = - qvariant_cast(m_serversModel->data(serverIndex, ServersModel::Roles::CredentialsRole)); + ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex); DockerContainer container = m_containersModel->getDefaultContainer(); - QModelIndex containerModelIndex = m_containersModel->index(container); - const QJsonObject &containerConfig = - qvariant_cast(m_containersModel->data(containerModelIndex, ContainersModel::Roles::ConfigRole)); + const QJsonObject &containerConfig = m_containersModel->getContainerConfig(container); if (container == DockerContainer::None) { emit connectionErrorOccurred(tr("VPN Protocols is not installed.\n Please install VPN container at first")); @@ -40,6 +37,7 @@ void ConnectionController::openConnection() } qApp->processEvents(); + emit connectToVpn(serverIndex, credentials, container, containerConfig); } @@ -110,6 +108,8 @@ void ConnectionController::onCurrentContainerUpdated() if (m_isConnected || m_isConnectionInProgress) { emit reconnectWithUpdatedContainer(tr("Settings updated successfully, Reconnnection...")); openConnection(); + } else { + emit reconnectWithUpdatedContainer(tr("Settings updated successfully")); } } diff --git a/client/ui/controllers/exportController.cpp b/client/ui/controllers/exportController.cpp index ef5cc4e3..9930926f 100644 --- a/client/ui/controllers/exportController.cpp +++ b/client/ui/controllers/exportController.cpp @@ -8,7 +8,9 @@ #include #include +#include "configurators/cloak_configurator.h" #include "configurators/openvpn_configurator.h" +#include "configurators/shadowsocks_configurator.h" #include "configurators/wireguard_configurator.h" #include "core/errorstrings.h" #include "systemController.h" @@ -19,11 +21,13 @@ ExportController::ExportController(const QSharedPointer &serversModel, const QSharedPointer &containersModel, + const QSharedPointer &clientManagementModel, const std::shared_ptr &settings, const std::shared_ptr &configurator, QObject *parent) : QObject(parent), m_serversModel(serversModel), m_containersModel(containersModel), + m_clientManagementModel(clientManagementModel), m_settings(settings), m_configurator(configurator) { @@ -75,35 +79,40 @@ void ExportController::generateFullAccessConfigAndroid() } #endif -void ExportController::generateConnectionConfig() +void ExportController::generateConnectionConfig(const QString &clientName) { clearPreviousConfig(); int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex(); - ServerCredentials credentials = - qvariant_cast(m_serversModel->data(serverIndex, ServersModel::Roles::CredentialsRole)); + ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex); DockerContainer container = static_cast(m_containersModel->getCurrentlyProcessedContainerIndex()); - QModelIndex containerModelIndex = m_containersModel->index(container); - QJsonObject containerConfig = - qvariant_cast(m_containersModel->data(containerModelIndex, ContainersModel::Roles::ConfigRole)); + QJsonObject containerConfig = m_containersModel->getContainerConfig(container); containerConfig.insert(config_key::container, ContainerProps::containerToString(container)); ErrorCode errorCode = ErrorCode::NoError; for (Proto protocol : ContainerProps::protocolsForContainer(container)) { QJsonObject protocolConfig = m_settings->protocolConfig(serverIndex, container, protocol); - QString vpnConfig = - m_configurator->genVpnProtocolConfig(credentials, container, containerConfig, protocol, &errorCode); + QString clientId; + QString vpnConfig = m_configurator->genVpnProtocolConfig(credentials, container, containerConfig, protocol, + clientId, &errorCode); if (errorCode) { emit exportErrorOccurred(errorString(errorCode)); return; } protocolConfig.insert(config_key::last_config, vpnConfig); containerConfig.insert(ProtocolProps::protoToString(protocol), protocolConfig); + if (protocol == Proto::OpenVpn || protocol == Proto::Awg || protocol == Proto::WireGuard) { + errorCode = m_clientManagementModel->appendClient(clientId, clientName, container, credentials); + if (errorCode) { + emit exportErrorOccurred(errorString(errorCode)); + return; + } + } } - QJsonObject config = m_settings->server(serverIndex); + QJsonObject config = m_settings->server(serverIndex); // todo change to servers_model if (!errorCode) { config.remove(config_key::userName); config.remove(config_key::password); @@ -126,23 +135,21 @@ void ExportController::generateConnectionConfig() emit exportConfigChanged(); } -void ExportController::generateOpenVpnConfig() +void ExportController::generateOpenVpnConfig(const QString &clientName) { clearPreviousConfig(); int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex(); - ServerCredentials credentials = - qvariant_cast(m_serversModel->data(serverIndex, ServersModel::Roles::CredentialsRole)); + ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex); DockerContainer container = static_cast(m_containersModel->getCurrentlyProcessedContainerIndex()); - QModelIndex containerModelIndex = m_containersModel->index(container); - QJsonObject containerConfig = - qvariant_cast(m_containersModel->data(containerModelIndex, ContainersModel::Roles::ConfigRole)); + QJsonObject containerConfig = m_containersModel->getContainerConfig(container); containerConfig.insert(config_key::container, ContainerProps::containerToString(container)); ErrorCode errorCode = ErrorCode::NoError; - QString config = - m_configurator->openVpnConfigurator->genOpenVpnConfig(credentials, container, containerConfig, &errorCode); + QString clientId; + QString config = m_configurator->openVpnConfigurator->genOpenVpnConfig(credentials, container, containerConfig, + clientId, &errorCode); if (errorCode) { emit exportErrorOccurred(errorString(errorCode)); return; @@ -155,26 +162,32 @@ void ExportController::generateOpenVpnConfig() m_config.append(line + "\n"); } + m_qrCodes = generateQrCodeImageSeries(m_config.toUtf8()); + + errorCode = m_clientManagementModel->appendClient(clientId, clientName, container, credentials); + if (errorCode) { + emit exportErrorOccurred(errorString(errorCode)); + return; + } + emit exportConfigChanged(); } -void ExportController::generateWireGuardConfig() +void ExportController::generateWireGuardConfig(const QString &clientName) { clearPreviousConfig(); int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex(); - ServerCredentials credentials = - qvariant_cast(m_serversModel->data(serverIndex, ServersModel::Roles::CredentialsRole)); + ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex); DockerContainer container = static_cast(m_containersModel->getCurrentlyProcessedContainerIndex()); - QModelIndex containerModelIndex = m_containersModel->index(container); - QJsonObject containerConfig = - qvariant_cast(m_containersModel->data(containerModelIndex, ContainersModel::Roles::ConfigRole)); + QJsonObject containerConfig = m_containersModel->getContainerConfig(container); containerConfig.insert(config_key::container, ContainerProps::containerToString(container)); + QString clientId; ErrorCode errorCode = ErrorCode::NoError; QString config = m_configurator->wireguardConfigurator->genWireguardConfig(credentials, container, containerConfig, - &errorCode); + clientId, &errorCode); if (errorCode) { emit exportErrorOccurred(errorString(errorCode)); return; @@ -187,6 +200,84 @@ void ExportController::generateWireGuardConfig() m_config.append(line + "\n"); } + qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(m_config.toUtf8(), qrcodegen::QrCode::Ecc::LOW); + m_qrCodes << svgToBase64(QString::fromStdString(toSvgString(qr, 1))); + + errorCode = m_clientManagementModel->appendClient(clientId, clientName, container, credentials); + if (errorCode) { + emit exportErrorOccurred(errorString(errorCode)); + return; + } + + emit exportConfigChanged(); +} + +void ExportController::generateShadowSocksConfig() +{ + clearPreviousConfig(); + + int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex(); + ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex); + + DockerContainer container = static_cast(m_containersModel->getCurrentlyProcessedContainerIndex()); + QJsonObject containerConfig = m_containersModel->getContainerConfig(container); + containerConfig.insert(config_key::container, ContainerProps::containerToString(container)); + + ErrorCode errorCode = ErrorCode::NoError; + QString config = m_configurator->shadowSocksConfigurator->genShadowSocksConfig(credentials, container, + containerConfig, &errorCode); + + config = m_configurator->processConfigWithExportSettings(serverIndex, container, Proto::ShadowSocks, config); + QJsonObject configJson = QJsonDocument::fromJson(config.toUtf8()).object(); + + QStringList lines = QString(QJsonDocument(configJson).toJson()).replace("\r", "").split("\n"); + for (const QString &line : lines) { + m_config.append(line + "\n"); + } + + m_nativeConfigString = + QString("%1:%2@%3:%4") + .arg(configJson.value("method").toString(), configJson.value("password").toString(), + configJson.value("server").toString(), configJson.value("server_port").toString()); + + m_nativeConfigString = "ss://" + m_nativeConfigString.toUtf8().toBase64(); + + qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(m_nativeConfigString.toUtf8(), qrcodegen::QrCode::Ecc::LOW); + m_qrCodes << svgToBase64(QString::fromStdString(toSvgString(qr, 1))); + + emit exportConfigChanged(); +} + +void ExportController::generateCloakConfig() +{ + clearPreviousConfig(); + + int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex(); + ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex); + + DockerContainer container = static_cast(m_containersModel->getCurrentlyProcessedContainerIndex()); + QJsonObject containerConfig = m_containersModel->getContainerConfig(container); + containerConfig.insert(config_key::container, ContainerProps::containerToString(container)); + + ErrorCode errorCode = ErrorCode::NoError; + QString config = + m_configurator->cloakConfigurator->genCloakConfig(credentials, container, containerConfig, &errorCode); + + if (errorCode) { + emit exportErrorOccurred(errorString(errorCode)); + return; + } + config = m_configurator->processConfigWithExportSettings(serverIndex, container, Proto::Cloak, config); + QJsonObject configJson = QJsonDocument::fromJson(config.toUtf8()).object(); + + configJson.remove(config_key::transport_proto); + configJson.insert("ProxyMethod", "shadowsocks"); + + QStringList lines = QString(QJsonDocument(configJson).toJson()).replace("\r", "").split("\n"); + for (const QString &line : lines) { + m_config.append(line + "\n"); + } + emit exportConfigChanged(); } @@ -195,6 +286,11 @@ QString ExportController::getConfig() return m_config; } +QString ExportController::getNativeConfigString() +{ + return m_nativeConfigString; +} + QList ExportController::getQrCodes() { return m_qrCodes; @@ -205,6 +301,30 @@ void ExportController::exportConfig(const QString &fileName) SystemController::saveFile(fileName, m_config); } +void ExportController::updateClientManagementModel(const DockerContainer container, ServerCredentials credentials) +{ + ErrorCode errorCode = m_clientManagementModel->updateModel(container, credentials); + if (errorCode != ErrorCode::NoError) { + emit exportErrorOccurred(errorString(errorCode)); + } +} + +void ExportController::revokeConfig(const int row, const DockerContainer container, ServerCredentials credentials) +{ + ErrorCode errorCode = m_clientManagementModel->revokeClient(row, container, credentials); + if (errorCode != ErrorCode::NoError) { + emit exportErrorOccurred(errorString(errorCode)); + } +} + +void ExportController::renameClient(const int row, const QString &clientName, const DockerContainer container, ServerCredentials credentials) +{ + ErrorCode errorCode = m_clientManagementModel->renameClient(row, clientName, container, credentials); + if (errorCode != ErrorCode::NoError) { + emit exportErrorOccurred(errorString(errorCode)); + } +} + QList ExportController::generateQrCodeImageSeries(const QByteArray &data) { double k = 850; @@ -219,7 +339,7 @@ QList ExportController::generateQrCodeImageSeries(const QByteArray &dat QByteArray ba = chunk.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(ba, qrcodegen::QrCode::Ecc::LOW); - QString svg = QString::fromStdString(toSvgString(qr, 0)); + QString svg = QString::fromStdString(toSvgString(qr, 1)); chunks.append(svgToBase64(svg)); } @@ -239,5 +359,6 @@ int ExportController::getQrCodesCount() void ExportController::clearPreviousConfig() { m_config.clear(); + m_nativeConfigString.clear(); m_qrCodes.clear(); } diff --git a/client/ui/controllers/exportController.h b/client/ui/controllers/exportController.h index 24eaa5c8..a6dc468b 100644 --- a/client/ui/controllers/exportController.h +++ b/client/ui/controllers/exportController.h @@ -6,6 +6,7 @@ #include "configurators/vpn_configurator.h" #include "ui/models/containers_model.h" #include "ui/models/servers_model.h" +#include "ui/models/clientManagementModel.h" #ifdef Q_OS_ANDROID #include "platforms/android/authResultReceiver.h" #endif @@ -16,27 +17,36 @@ class ExportController : public QObject public: explicit ExportController(const QSharedPointer &serversModel, const QSharedPointer &containersModel, + const QSharedPointer &clientManagementModel, const std::shared_ptr &settings, const std::shared_ptr &configurator, QObject *parent = nullptr); Q_PROPERTY(QList qrCodes READ getQrCodes NOTIFY exportConfigChanged) Q_PROPERTY(int qrCodesCount READ getQrCodesCount NOTIFY exportConfigChanged) Q_PROPERTY(QString config READ getConfig NOTIFY exportConfigChanged) + Q_PROPERTY(QString nativeConfigString READ getNativeConfigString NOTIFY exportConfigChanged) public slots: void generateFullAccessConfig(); #if defined(Q_OS_ANDROID) void generateFullAccessConfigAndroid(); #endif - void generateConnectionConfig(); - void generateOpenVpnConfig(); - void generateWireGuardConfig(); + void generateConnectionConfig(const QString &clientName); + void generateOpenVpnConfig(const QString &clientName); + void generateWireGuardConfig(const QString &clientName); + void generateShadowSocksConfig(); + void generateCloakConfig(); QString getConfig(); + QString getNativeConfigString(); QList getQrCodes(); void exportConfig(const QString &fileName); + void updateClientManagementModel(const DockerContainer container, ServerCredentials credentials); + void revokeConfig(const int row, const DockerContainer container, ServerCredentials credentials); + void renameClient(const int row, const QString &clientName, const DockerContainer container, ServerCredentials credentials); + signals: void generateConfig(int type); void exportErrorOccurred(const QString &errorMessage); @@ -55,10 +65,12 @@ private: QSharedPointer m_serversModel; QSharedPointer m_containersModel; + QSharedPointer m_clientManagementModel; std::shared_ptr m_settings; std::shared_ptr m_configurator; QString m_config; + QString m_nativeConfigString; QList m_qrCodes; #ifdef Q_OS_ANDROID diff --git a/client/ui/controllers/importController.cpp b/client/ui/controllers/importController.cpp index b9ee0b68..44fc1cb9 100644 --- a/client/ui/controllers/importController.cpp +++ b/client/ui/controllers/importController.cpp @@ -123,7 +123,9 @@ void ImportController::importConfig() credentials.userName = m_config.value(config_key::userName).toString(); credentials.secretData = m_config.value(config_key::password).toString(); - if (credentials.isValid() || m_config.contains(config_key::containers)) { + if (credentials.isValid() + || m_config.contains(config_key::containers) + || m_config.contains(config_key::configVersion)) { // todo m_serversModel->addServer(m_config); emit importFinished(); @@ -147,7 +149,9 @@ QJsonObject ImportController::extractAmneziaConfig(QString &data) ba = ba_uncompressed; } - return QJsonDocument::fromJson(ba).object(); + QJsonObject config = QJsonDocument::fromJson(ba).object(); + + return config; } QJsonObject ImportController::extractOpenVpnConfig(const QString &data) diff --git a/client/ui/controllers/installController.cpp b/client/ui/controllers/installController.cpp index 80e74764..d4582429 100644 --- a/client/ui/controllers/installController.cpp +++ b/client/ui/controllers/installController.cpp @@ -8,7 +8,7 @@ #include #include "core/errorstrings.h" -#include "core/servercontroller.h" +#include "core/controllers/serverController.h" #include "utilities.h" namespace @@ -130,6 +130,7 @@ void InstallController::installServer(DockerContainer container, QJsonObject &co { ServerController serverController(m_settings); connect(&serverController, &ServerController::serverIsBusy, this, &InstallController::serverIsBusy); + connect(this, &InstallController::cancelInstallation, &serverController, &ServerController::cancelInstallation); QMap installedContainers; ErrorCode errorCode = @@ -181,6 +182,7 @@ void InstallController::installContainer(DockerContainer container, QJsonObject ServerController serverController(m_settings); connect(&serverController, &ServerController::serverIsBusy, this, &InstallController::serverIsBusy); + connect(this, &InstallController::cancelInstallation, &serverController, &ServerController::cancelInstallation); QMap installedContainers; ErrorCode errorCode = serverController.getAlreadyInstalledContainers(serverCredentials, installedContainers); @@ -199,12 +201,9 @@ void InstallController::installContainer(DockerContainer container, QJsonObject if (errorCode == ErrorCode::NoError) { for (auto iterator = installedContainers.begin(); iterator != installedContainers.end(); iterator++) { - auto modelIndex = m_containersModel->index(iterator.key()); - QJsonObject containerConfig = - qvariant_cast(m_containersModel->data(modelIndex, ContainersModel::Roles::ConfigRole)); + QJsonObject containerConfig = m_containersModel->getContainerConfig(iterator.key()); if (containerConfig.isEmpty()) { - m_containersModel->setData(m_containersModel->index(iterator.key()), iterator.value(), - ContainersModel::Roles::ConfigRole); + m_serversModel->addContainerConfig(iterator.key(), iterator.value()); if (container != iterator.key()) { // skip the newly installed container isInstalledContainerAddedToGui = true; } @@ -252,12 +251,9 @@ void InstallController::scanServerForInstalledContainers() bool isInstalledContainerAddedToGui = false; for (auto iterator = installedContainers.begin(); iterator != installedContainers.end(); iterator++) { - auto modelIndex = m_containersModel->index(iterator.key()); - QJsonObject containerConfig = - qvariant_cast(m_containersModel->data(modelIndex, ContainersModel::Roles::ConfigRole)); + QJsonObject containerConfig = m_containersModel->getContainerConfig(iterator.key()); if (containerConfig.isEmpty()) { - m_containersModel->setData(m_containersModel->index(iterator.key()), iterator.value(), - ContainersModel::Roles::ConfigRole); + m_serversModel->addContainerConfig(iterator.key(), iterator.value()); isInstalledContainerAddedToGui = true; } } @@ -276,16 +272,15 @@ void InstallController::updateContainer(QJsonObject config) qvariant_cast(m_serversModel->data(serverIndex, ServersModel::Roles::CredentialsRole)); const DockerContainer container = ContainerProps::containerFromString(config.value(config_key::container).toString()); - auto modelIndex = m_containersModel->index(container); - QJsonObject oldContainerConfig = - qvariant_cast(m_containersModel->data(modelIndex, ContainersModel::Roles::ConfigRole)); + QJsonObject oldContainerConfig = m_containersModel->getContainerConfig(container); ServerController serverController(m_settings); connect(&serverController, &ServerController::serverIsBusy, this, &InstallController::serverIsBusy); + connect(this, &InstallController::cancelInstallation, &serverController, &ServerController::cancelInstallation); auto errorCode = serverController.updateContainer(serverCredentials, container, oldContainerConfig, config); if (errorCode == ErrorCode::NoError) { - m_containersModel->setData(modelIndex, config, ContainersModel::Roles::ConfigRole); + m_serversModel->updateContainerConfig(container, config); m_protocolModel->updateModel(config); if ((serverIndex == m_serversModel->getDefaultServerIndex()) @@ -315,7 +310,7 @@ void InstallController::removeAllContainers() int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex(); QString serverName = m_serversModel->data(serverIndex, ServersModel::Roles::NameRole).toString(); - ErrorCode errorCode = m_containersModel->removeAllContainers(); + ErrorCode errorCode = m_serversModel->removeAllContainers(); if (errorCode == ErrorCode::NoError) { emit removeAllContainersFinished(tr("All containers from server '%1' have been removed").arg(serverName)); return; @@ -329,12 +324,12 @@ void InstallController::removeCurrentlyProcessedContainer() QString serverName = m_serversModel->data(serverIndex, ServersModel::Roles::NameRole).toString(); int container = m_containersModel->getCurrentlyProcessedContainerIndex(); - QString containerName = m_containersModel->data(container, ContainersModel::Roles::NameRole).toString(); + QString containerName = m_containersModel->getCurrentlyProcessedContainerName(); - ErrorCode errorCode = m_containersModel->removeCurrentlyProcessedContainer(); + ErrorCode errorCode = m_serversModel->removeContainer(container); if (errorCode == ErrorCode::NoError) { - emit removeCurrentlyProcessedContainerFinished(tr("%1 has been removed from the server '%2'").arg(containerName).arg(serverName)); + emit removeCurrentlyProcessedContainerFinished(tr("%1 has been removed from the server '%2'").arg(containerName, serverName)); return; } emit installationErrorOccurred(errorString(errorCode)); diff --git a/client/ui/controllers/installController.h b/client/ui/controllers/installController.h index a5fd2875..cd0e6b22 100644 --- a/client/ui/controllers/installController.h +++ b/client/ui/controllers/installController.h @@ -65,6 +65,7 @@ signals: void passphraseRequestFinished(); void serverIsBusy(const bool isBusy); + void cancelInstallation(); void currentContainerUpdated(); diff --git a/client/ui/controllers/pageController.h b/client/ui/controllers/pageController.h index 20c3bbed..f7e697fb 100644 --- a/client/ui/controllers/pageController.h +++ b/client/ui/controllers/pageController.h @@ -51,7 +51,9 @@ namespace PageLoader PageProtocolWireGuardSettings, PageProtocolAwgSettings, PageProtocolIKev2Settings, - PageProtocolRaw + PageProtocolRaw, + + PageShareFullAccess }; Q_ENUM_NS(PageEnum) diff --git a/client/ui/controllers/settingsController.cpp b/client/ui/controllers/settingsController.cpp index 78d0dd67..9fa4d76b 100644 --- a/client/ui/controllers/settingsController.cpp +++ b/client/ui/controllers/settingsController.cpp @@ -42,6 +42,7 @@ SettingsController::SettingsController(const QSharedPointer &serve void SettingsController::toggleAmneziaDns(bool enable) { m_settings->setUseAmneziaDns(enable); + emit amneziaDnsToggled(enable); } bool SettingsController::isAmneziaDnsEnabled() @@ -138,7 +139,7 @@ void SettingsController::clearSettings() void SettingsController::clearCachedProfiles() { - m_containersModel->clearCachedProfiles(); + m_serversModel->clearCachedProfiles(); emit changeSettingsFinished(tr("Cached profiles cleared")); } diff --git a/client/ui/controllers/settingsController.h b/client/ui/controllers/settingsController.h index be041f3e..710d255f 100644 --- a/client/ui/controllers/settingsController.h +++ b/client/ui/controllers/settingsController.h @@ -70,6 +70,8 @@ signals: void importBackupFromOutside(QString filePath); + void amneziaDnsToggled(bool enable); + private: QSharedPointer m_serversModel; QSharedPointer m_containersModel; diff --git a/client/ui/models/clientManagementModel.cpp b/client/ui/models/clientManagementModel.cpp index 87652ff2..8ec31d02 100644 --- a/client/ui/models/clientManagementModel.cpp +++ b/client/ui/models/clientManagementModel.cpp @@ -1,104 +1,373 @@ #include "clientManagementModel.h" +#include #include -ClientManagementModel::ClientManagementModel(QObject *parent) : QAbstractListModel(parent) -{ +#include "core/controllers/serverController.h" +#include "logger.h" -} - -void ClientManagementModel::clearData() +namespace { - beginResetModel(); - m_content.clear(); - endResetModel(); -} + Logger logger("ClientManagementModel"); -void ClientManagementModel::setContent(const QVector &data) -{ - beginResetModel(); - m_content = data; - endResetModel(); -} - -QJsonObject ClientManagementModel::getContent(amnezia::Proto protocol) -{ - QJsonObject clientsTable; - for (const auto &item : m_content) { - if (protocol == amnezia::Proto::OpenVpn) { - clientsTable[item.toJsonObject()["openvpnCertId"].toString()] = item.toJsonObject(); - } else if (protocol == amnezia::Proto::WireGuard) { - clientsTable[item.toJsonObject()["wireguardPublicKey"].toString()] = item.toJsonObject(); - } + namespace configKey { + constexpr char clientId[] = "clientId"; + constexpr char clientName[] = "clientName"; + constexpr char container[] = "container"; + constexpr char userData[] = "userData"; } - return clientsTable; +} + +ClientManagementModel::ClientManagementModel(std::shared_ptr settings, QObject *parent) + : m_settings(settings), QAbstractListModel(parent) +{ } int ClientManagementModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); - return static_cast(m_content.size()); + return static_cast(m_clientsTable.size()); } QVariant ClientManagementModel::data(const QModelIndex &index, int role) const { - if (!index.isValid() || index.row() < 0 - || index.row() >= static_cast(m_content.size())) { + if (!index.isValid() || index.row() < 0 || index.row() >= static_cast(m_clientsTable.size())) { return QVariant(); } - if (role == NameRole) { - return m_content[index.row()].toJsonObject()["clientName"].toString(); - } else if (role == OpenVpnCertIdRole) { - return m_content[index.row()].toJsonObject()["openvpnCertId"].toString(); - } else if (role == OpenVpnCertDataRole) { - return m_content[index.row()].toJsonObject()["openvpnCertData"].toString(); - } else if (role == WireGuardPublicKey) { - return m_content[index.row()].toJsonObject()["wireguardPublicKey"].toString(); + auto client = m_clientsTable.at(index.row()).toObject(); + auto userData = client.value(configKey::userData).toObject(); + + switch (role) { + case ClientNameRole: return userData.value(configKey::clientName).toString(); } return QVariant(); } -void ClientManagementModel::setData(const QModelIndex &index, QVariant data, int role) +ErrorCode ClientManagementModel::updateModel(DockerContainer container, ServerCredentials credentials) { - if (!index.isValid() || index.row() < 0 - || index.row() >= static_cast(m_content.size())) { - return; + beginResetModel(); + m_clientsTable = QJsonArray(); + + ServerController serverController(m_settings); + + ErrorCode error = ErrorCode::NoError; + + const QString clientsTableFile = + QString("/opt/amnezia/%1/clientsTable").arg(ContainerProps::containerTypeToString(container)); + const QByteArray clientsTableString = + serverController.getTextFileFromContainer(container, credentials, clientsTableFile, &error); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to get the clientsTable file from the server"; + endResetModel(); + return error; } - auto client = m_content[index.row()].toJsonObject(); - if (role == NameRole) { - client["clientName"] = data.toString(); - } else if (role == OpenVpnCertIdRole) { - client["openvpnCertId"] = data.toString(); - } else if (role == OpenVpnCertDataRole) { - client["openvpnCertData"] = data.toString(); - } else if (role == WireGuardPublicKey) { - client["wireguardPublicKey"] = data.toString(); - } else { - return; - } - if (m_content[index.row()] != client) { - m_content[index.row()] = client; - emit dataChanged(index, index); + m_clientsTable = QJsonDocument::fromJson(clientsTableString).array(); + + if (m_clientsTable.isEmpty()) { + int count = 0; + + if (container == DockerContainer::OpenVpn || container == DockerContainer::ShadowSocks + || container == DockerContainer::Cloak) { + error = getOpenVpnClients(serverController, container, credentials, count); + } else if (container == DockerContainer::WireGuard || container == DockerContainer::Awg) { + error = getWireGuardClients(serverController, container, credentials, count); + } + if (error != ErrorCode::NoError) { + endResetModel(); + return error; + } + + const QByteArray newClientsTableString = QJsonDocument(m_clientsTable).toJson(); + if (clientsTableString != newClientsTableString) { + error = serverController.uploadTextFileToContainer(container, credentials, newClientsTableString, + clientsTableFile); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to upload the clientsTable file to the server"; + } + } } + + endResetModel(); + return error; } -bool ClientManagementModel::removeRows(int row) +ErrorCode ClientManagementModel::getOpenVpnClients(ServerController &serverController, DockerContainer container, ServerCredentials credentials, int &count) { + ErrorCode error = ErrorCode::NoError; + QString stdOut; + auto cbReadStdOut = [&](const QString &data, libssh::Client &) { + stdOut += data + "\n"; + return ErrorCode::NoError; + }; + + const QString getOpenVpnClientsList = + "sudo docker exec -i $CONTAINER_NAME bash -c 'ls /opt/amnezia/openvpn/pki/issued'"; + QString script = serverController.replaceVars(getOpenVpnClientsList, + serverController.genVarsForScript(credentials, container)); + error = serverController.runScript(credentials, script, cbReadStdOut); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to retrieve the list of issued certificates on the server"; + return error; + } + + if (!stdOut.isEmpty()) { + QStringList certsIds = stdOut.split("\n", Qt::SkipEmptyParts); + certsIds.removeAll("AmneziaReq.crt"); + + for (auto &openvpnCertId : certsIds) { + openvpnCertId.replace(".crt", ""); + if (!isClientExists(openvpnCertId)) { + QJsonObject client; + client[configKey::clientId] = openvpnCertId; + + QJsonObject userData; + userData[configKey::clientName] = QString("Client %1").arg(count); + client[configKey::userData] = userData; + + m_clientsTable.push_back(client); + + count++; + } + } + } + return error; +} + +ErrorCode ClientManagementModel::getWireGuardClients(ServerController &serverController, DockerContainer container, ServerCredentials credentials, int &count) +{ + ErrorCode error = ErrorCode::NoError; + + const QString wireGuardConfigFile = + QString("opt/amnezia/%1/wg0.conf").arg(container == DockerContainer::WireGuard ? "wireguard" : "awg"); + const QString wireguardConfigString = + serverController.getTextFileFromContainer(container, credentials, wireGuardConfigFile, &error); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to get the wg conf file from the server"; + return error; + } + + auto configLines = wireguardConfigString.split("\n", Qt::SkipEmptyParts); + QStringList wireguardKeys; + for (const auto &line : configLines) { + auto configPair = line.split(" = ", Qt::SkipEmptyParts); + if (configPair.front() == "PublicKey") { + wireguardKeys.push_back(configPair.back()); + } + } + + for (auto &wireguardKey : wireguardKeys) { + if (!isClientExists(wireguardKey)) { + QJsonObject client; + client[configKey::clientId] = wireguardKey; + + QJsonObject userData; + userData[configKey::clientName] = QString("Client %1").arg(count); + client[configKey::userData] = userData; + + m_clientsTable.push_back(client); + + count++; + } + } + return error; +} + +bool ClientManagementModel::isClientExists(const QString &clientId) +{ + for (const QJsonValue &value : qAsConst(m_clientsTable)) { + if (value.isObject()) { + QJsonObject obj = value.toObject(); + if (obj.contains(configKey::clientId) && obj[configKey::clientId].toString() == clientId) { + return true; + } + } + } + return false; +} + +ErrorCode ClientManagementModel::appendClient(const QString &clientId, const QString &clientName, + const DockerContainer container, ServerCredentials credentials) +{ + ErrorCode error; + + error = updateModel(container, credentials); + if (error != ErrorCode::NoError) { + return error; + } + + for (int i = 0; i < m_clientsTable.size(); i++) { + if (m_clientsTable.at(i).toObject().value(configKey::clientId) == clientId) { + return renameClient(i, clientName, container, credentials); + } + } + + beginResetModel(); + QJsonObject client; + client[configKey::clientId] = clientId; + + QJsonObject userData; + userData[configKey::clientName] = clientName; + client[configKey::userData] = userData; + m_clientsTable.push_back(client); + endResetModel(); + + const QByteArray clientsTableString = QJsonDocument(m_clientsTable).toJson(); + + ServerController serverController(m_settings); + const QString clientsTableFile = + QString("/opt/amnezia/%1/clientsTable").arg(ContainerProps::containerTypeToString(container)); + + error = serverController.uploadTextFileToContainer(container, credentials, clientsTableString, clientsTableFile); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to upload the clientsTable file to the server"; + } + + return error; +} + +ErrorCode ClientManagementModel::renameClient(const int row, const QString &clientName, const DockerContainer container, + ServerCredentials credentials) +{ + auto client = m_clientsTable.at(row).toObject(); + auto userData = client[configKey::userData].toObject(); + userData[configKey::clientName] = clientName; + client[configKey::userData] = userData; + + m_clientsTable.replace(row, client); + emit dataChanged(index(row, 0), index(row, 0)); + + const QByteArray clientsTableString = QJsonDocument(m_clientsTable).toJson(); + + ServerController serverController(m_settings); + const QString clientsTableFile = + QString("/opt/amnezia/%1/clientsTable").arg(ContainerProps::containerTypeToString(container)); + + ErrorCode error = + serverController.uploadTextFileToContainer(container, credentials, clientsTableString, clientsTableFile); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to upload the clientsTable file to the server"; + } + + return error; +} + +ErrorCode ClientManagementModel::revokeClient(const int row, const DockerContainer container, + ServerCredentials credentials) +{ + if (container == DockerContainer::OpenVpn || container == DockerContainer::ShadowSocks + || container == DockerContainer::Cloak) { + return revokeOpenVpn(row, container, credentials); + } else if (container == DockerContainer::WireGuard || container == DockerContainer::Awg) { + return revokeWireGuard(row, container, credentials); + } + return ErrorCode::NoError; +} + +ErrorCode ClientManagementModel::revokeOpenVpn(const int row, const DockerContainer container, + ServerCredentials credentials) +{ + auto client = m_clientsTable.at(row).toObject(); + QString clientId = client.value(configKey::clientId).toString(); + + const QString getOpenVpnCertData = QString("sudo docker exec -i $CONTAINER_NAME bash -c '" + "cd /opt/amnezia/openvpn ;\\" + "easyrsa revoke %1 ;\\" + "easyrsa gen-crl ;\\" + "cp pki/crl.pem .'") + .arg(clientId); + + ServerController serverController(m_settings); + const QString script = + serverController.replaceVars(getOpenVpnCertData, serverController.genVarsForScript(credentials, container)); + ErrorCode error = serverController.runScript(credentials, script); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to revoke the certificate"; + return error; + } + beginRemoveRows(QModelIndex(), row, row); - m_content.removeAt(row); + m_clientsTable.removeAt(row); endRemoveRows(); - return true; + + const QByteArray clientsTableString = QJsonDocument(m_clientsTable).toJson(); + + const QString clientsTableFile = + QString("/opt/amnezia/%1/clientsTable").arg(ContainerProps::containerTypeToString(container)); + error = serverController.uploadTextFileToContainer(container, credentials, clientsTableString, clientsTableFile); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to upload the clientsTable file to the server"; + return error; + } + + return ErrorCode::NoError; +} + +ErrorCode ClientManagementModel::revokeWireGuard(const int row, const DockerContainer container, + ServerCredentials credentials) +{ + ErrorCode error; + ServerController serverController(m_settings); + + const QString wireGuardConfigFile = + QString("/opt/amnezia/%1/wg0.conf").arg(container == DockerContainer::WireGuard ? "wireguard" : "awg"); + const QString wireguardConfigString = + serverController.getTextFileFromContainer(container, credentials, wireGuardConfigFile, &error); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to get the wg conf file from the server"; + return error; + } + + auto client = m_clientsTable.at(row).toObject(); + QString clientId = client.value(configKey::clientId).toString(); + + auto configSections = wireguardConfigString.split("[", Qt::SkipEmptyParts); + for (auto §ion : configSections) { + if (section.contains(clientId)) { + configSections.removeOne(section); + break; + } + } + QString newWireGuardConfig = configSections.join("["); + newWireGuardConfig.insert(0, "["); + error = serverController.uploadTextFileToContainer(container, credentials, newWireGuardConfig, wireGuardConfigFile); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to upload the wg conf file to the server"; + return error; + } + + beginRemoveRows(QModelIndex(), row, row); + m_clientsTable.removeAt(row); + endRemoveRows(); + + const QByteArray clientsTableString = QJsonDocument(m_clientsTable).toJson(); + + const QString clientsTableFile = + QString("/opt/amnezia/%1/clientsTable").arg(ContainerProps::containerTypeToString(container)); + error = serverController.uploadTextFileToContainer(container, credentials, clientsTableString, clientsTableFile); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to upload the clientsTable file to the server"; + return error; + } + + const QString script = "sudo docker exec -i $CONTAINER_NAME bash -c 'wg syncconf wg0 <(wg-quick strip %1)'"; + error = serverController.runScript( + credentials, + serverController.replaceVars(script.arg(wireGuardConfigFile), + serverController.genVarsForScript(credentials, container))); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to execute the command 'wg syncconf' on the server"; + return error; + } + + return ErrorCode::NoError; } QHash ClientManagementModel::roleNames() const { QHash roles; - roles[NameRole] = "clientName"; - roles[OpenVpnCertIdRole] = "openvpnCertId"; - roles[OpenVpnCertDataRole] = "openvpnCertData"; - roles[WireGuardPublicKey] = "wireguardPublicKey"; + roles[ClientNameRole] = "clientName"; return roles; } diff --git a/client/ui/models/clientManagementModel.h b/client/ui/models/clientManagementModel.h index 5230c337..6b6adf68 100644 --- a/client/ui/models/clientManagementModel.h +++ b/client/ui/models/clientManagementModel.h @@ -2,36 +2,48 @@ #define CLIENTMANAGEMENTMODEL_H #include +#include -#include "protocols/protocols_defs.h" +#include "core/controllers/serverController.h" +#include "settings.h" class ClientManagementModel : public QAbstractListModel { Q_OBJECT public: - enum ClientRoles { - NameRole = Qt::UserRole + 1, - OpenVpnCertIdRole, - OpenVpnCertDataRole, - WireGuardPublicKey, + enum Roles { + ClientNameRole = Qt::UserRole + 1, }; - ClientManagementModel(QObject *parent = nullptr); + ClientManagementModel(std::shared_ptr settings, QObject *parent = nullptr); - void clearData(); - void setContent(const QVector &data); - QJsonObject getContent(amnezia::Proto protocol); int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - void setData(const QModelIndex &index, QVariant data, int role = Qt::DisplayRole); - bool removeRows(int row); + +public slots: + ErrorCode updateModel(DockerContainer container, ServerCredentials credentials); + ErrorCode appendClient(const QString &clientId, const QString &clientName, const DockerContainer container, + ServerCredentials credentials); + ErrorCode renameClient(const int row, const QString &userName, const DockerContainer container, + ServerCredentials credentials); + ErrorCode revokeClient(const int index, const DockerContainer container, ServerCredentials credentials); protected: QHash roleNames() const override; private: - QVector m_content; + bool isClientExists(const QString &clientId); + + ErrorCode revokeOpenVpn(const int row, const DockerContainer container, ServerCredentials credentials); + ErrorCode revokeWireGuard(const int row, const DockerContainer container, ServerCredentials credentials); + + ErrorCode getOpenVpnClients(ServerController &serverController, DockerContainer container, ServerCredentials credentials, int &count); + ErrorCode getWireGuardClients(ServerController &serverController, DockerContainer container, ServerCredentials credentials, int &count); + + QJsonArray m_clientsTable; + + std::shared_ptr m_settings; }; #endif // CLIENTMANAGEMENTMODEL_H diff --git a/client/ui/models/containers_model.cpp b/client/ui/models/containers_model.cpp index 3fff22d4..7e73b2e9 100644 --- a/client/ui/models/containers_model.cpp +++ b/client/ui/models/containers_model.cpp @@ -1,9 +1,9 @@ #include "containers_model.h" -#include "core/servercontroller.h" +#include -ContainersModel::ContainersModel(std::shared_ptr settings, QObject *parent) - : m_settings(settings), QAbstractListModel(parent) +ContainersModel::ContainersModel(QObject *parent) + : QAbstractListModel(parent) { } @@ -13,37 +13,6 @@ int ContainersModel::rowCount(const QModelIndex &parent) const return ContainerProps::allContainers().size(); } -bool ContainersModel::setData(const QModelIndex &index, const QVariant &value, int role) -{ - if (!index.isValid() || index.row() < 0 || index.row() >= ContainerProps::allContainers().size()) { - return false; - } - - DockerContainer container = ContainerProps::allContainers().at(index.row()); - - switch (role) { - case ConfigRole: { - m_settings->setContainerConfig(m_currentlyProcessedServerIndex, container, value.toJsonObject()); - m_containers = m_settings->containers(m_currentlyProcessedServerIndex); - if (m_defaultContainerIndex != DockerContainer::None) { - break; - } else if (ContainerProps::containerService(container) == ServiceType::Other) { - break; - } - } - case IsDefaultRole: { //todo remove - m_settings->setDefaultContainer(m_currentlyProcessedServerIndex, container); - m_defaultContainerIndex = container; - emit defaultContainerChanged(); - } - default: break; - } - - emit containersModelUpdated(); - emit dataChanged(index, index); - return true; -} - QVariant ContainersModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() < 0 || index.row() >= ContainerProps::allContainers().size()) { @@ -84,37 +53,32 @@ QVariant ContainersModel::data(const int index, int role) const return data(modelIndex, role); } -void ContainersModel::setCurrentlyProcessedServerIndex(const int index) +void ContainersModel::updateModel(QJsonArray &containers) { beginResetModel(); - m_currentlyProcessedServerIndex = index; - m_containers = m_settings->containers(m_currentlyProcessedServerIndex); - m_defaultContainerIndex = m_settings->defaultContainer(m_currentlyProcessedServerIndex); + m_containers.clear(); + for (const QJsonValue &val : containers) { + m_containers.insert(ContainerProps::containerFromString(val.toObject().value(config_key::container).toString()), + val.toObject()); + } endResetModel(); - emit defaultContainerChanged(); } -void ContainersModel::setCurrentlyProcessedContainerIndex(int index) +void ContainersModel::setDefaultContainer(const int containerIndex) { - m_currentlyProcessedContainerIndex = index; + m_defaultContainerIndex = static_cast(containerIndex); + emit dataChanged(index(containerIndex, 0), index(containerIndex, 0)); } + DockerContainer ContainersModel::getDefaultContainer() { return m_defaultContainerIndex; } -QString ContainersModel::getDefaultContainerName() +void ContainersModel::setCurrentlyProcessedContainerIndex(int index) { - return ContainerProps::containerHumanNames().value(m_defaultContainerIndex); -} - -void ContainersModel::setDefaultContainer(int index) -{ - auto container = static_cast(index); - m_settings->setDefaultContainer(m_currentlyProcessedServerIndex, container); - m_defaultContainerIndex = container; - emit defaultContainerChanged(); + m_currentlyProcessedContainerIndex = index; } int ContainersModel::getCurrentlyProcessedContainerIndex() @@ -127,91 +91,9 @@ QString ContainersModel::getCurrentlyProcessedContainerName() return ContainerProps::containerHumanNames().value(static_cast(m_currentlyProcessedContainerIndex)); } -QJsonObject ContainersModel::getCurrentlyProcessedContainerConfig() +QJsonObject ContainersModel::getContainerConfig(const int containerIndex) { - return qvariant_cast(data(index(m_currentlyProcessedContainerIndex), ConfigRole)); -} - -QStringList ContainersModel::getAllInstalledServicesName(const int serverIndex) -{ - QStringList servicesName; - const auto &containers = m_settings->containers(serverIndex); - for (const DockerContainer &container : containers.keys()) { - if (ContainerProps::containerService(container) == ServiceType::Other && m_containers.contains(container)) { - if (container == DockerContainer::Dns) { - servicesName.append("DNS"); - } else if (container == DockerContainer::Sftp) { - servicesName.append("SFTP"); - } else if (container == DockerContainer::TorWebSite) { - servicesName.append("TOR"); - } - } - } - servicesName.sort(); - return servicesName; -} - -ErrorCode ContainersModel::removeAllContainers() -{ - - ServerController serverController(m_settings); - ErrorCode errorCode = - serverController.removeAllContainers(m_settings->serverCredentials(m_currentlyProcessedServerIndex)); - - if (errorCode == ErrorCode::NoError) { - beginResetModel(); - - m_settings->setContainers(m_currentlyProcessedServerIndex, {}); - m_containers = m_settings->containers(m_currentlyProcessedServerIndex); - - setData(index(DockerContainer::None, 0), true, IsDefaultRole); - endResetModel(); - } - return errorCode; -} - -ErrorCode ContainersModel::removeCurrentlyProcessedContainer() -{ - ServerController serverController(m_settings); - auto credentials = m_settings->serverCredentials(m_currentlyProcessedServerIndex); - auto dockerContainer = static_cast(m_currentlyProcessedContainerIndex); - - ErrorCode errorCode = serverController.removeContainer(credentials, dockerContainer); - - if (errorCode == ErrorCode::NoError) { - beginResetModel(); - m_settings->removeContainerConfig(m_currentlyProcessedServerIndex, dockerContainer); - m_containers = m_settings->containers(m_currentlyProcessedServerIndex); - - if (m_defaultContainerIndex == m_currentlyProcessedContainerIndex) { - if (m_containers.isEmpty()) { - setData(index(DockerContainer::None, 0), true, IsDefaultRole); - } else { - setData(index(m_containers.begin().key(), 0), true, IsDefaultRole); - } - } - endResetModel(); - } - return errorCode; -} - -void ContainersModel::clearCachedProfiles() -{ - const auto &containers = m_settings->containers(m_currentlyProcessedServerIndex); - for (DockerContainer container : containers.keys()) { - m_settings->clearLastConnectionConfig(m_currentlyProcessedServerIndex, container); - } -} - -bool ContainersModel::isAmneziaDnsContainerInstalled() -{ - return m_containers.contains(DockerContainer::Dns); -} - -bool ContainersModel::isAmneziaDnsContainerInstalled(const int serverIndex) -{ - QMap containers = m_settings->containers(serverIndex); - return containers.contains(DockerContainer::Dns); + return qvariant_cast(data(index(containerIndex), ConfigRole)); } bool ContainersModel::isAnyContainerInstalled() @@ -228,11 +110,6 @@ bool ContainersModel::isAnyContainerInstalled() return false; } -void ContainersModel::updateContainersConfig() -{ - m_containers = m_settings->containers(m_currentlyProcessedServerIndex); -} - QHash ContainersModel::roleNames() const { QHash roles; diff --git a/client/ui/models/containers_model.h b/client/ui/models/containers_model.h index 8f087d87..23a91ea5 100644 --- a/client/ui/models/containers_model.h +++ b/client/ui/models/containers_model.h @@ -7,13 +7,12 @@ #include #include "containers/containers_defs.h" -#include "settings.h" class ContainersModel : public QAbstractListModel { Q_OBJECT public: - ContainersModel(std::shared_ptr settings, QObject *parent = nullptr); + ContainersModel(QObject *parent = nullptr); enum Roles { NameRole = Qt::UserRole + 1, @@ -37,37 +36,24 @@ public: int rowCount(const QModelIndex &parent = QModelIndex()) const override; - bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QVariant data(const int index, int role) const; - Q_PROPERTY(QString defaultContainerName READ getDefaultContainerName NOTIFY defaultContainerChanged) - public slots: + void updateModel(QJsonArray &containers); + DockerContainer getDefaultContainer(); - QString getDefaultContainerName(); - void setDefaultContainer(int index); + void setDefaultContainer(const int containerIndex); - void setCurrentlyProcessedServerIndex(const int index); - - void setCurrentlyProcessedContainerIndex(int index); + void setCurrentlyProcessedContainerIndex(int containerIndex); int getCurrentlyProcessedContainerIndex(); QString getCurrentlyProcessedContainerName(); - QJsonObject getCurrentlyProcessedContainerConfig(); - QStringList getAllInstalledServicesName(const int serverIndex); - ErrorCode removeAllContainers(); - ErrorCode removeCurrentlyProcessedContainer(); - void clearCachedProfiles(); - - bool isAmneziaDnsContainerInstalled(); - bool isAmneziaDnsContainerInstalled(const int serverIndex); + QJsonObject getContainerConfig(const int containerIndex); bool isAnyContainerInstalled(); - void updateContainersConfig(); - protected: QHash roleNames() const override; @@ -78,11 +64,8 @@ signals: private: QMap m_containers; - int m_currentlyProcessedServerIndex; int m_currentlyProcessedContainerIndex; DockerContainer m_defaultContainerIndex; - - std::shared_ptr m_settings; }; #endif // CONTAINERS_MODEL_H diff --git a/client/ui/models/languageModel.cpp b/client/ui/models/languageModel.cpp index b860b9da..c3552ea7 100644 --- a/client/ui/models/languageModel.cpp +++ b/client/ui/models/languageModel.cpp @@ -44,6 +44,7 @@ QString LanguageModel::getLocalLanguageName(const LanguageSettings::AvailableLan case LanguageSettings::AvailableLanguageEnum::English: strLanguage = "English"; break; case LanguageSettings::AvailableLanguageEnum::Russian: strLanguage = "Русский"; break; case LanguageSettings::AvailableLanguageEnum::China_cn: strLanguage = "\347\256\200\344\275\223\344\270\255\346\226\207"; break; + case LanguageSettings::AvailableLanguageEnum::Persian: strLanguage = "فارسی"; break; default: break; } @@ -57,6 +58,7 @@ void LanguageModel::changeLanguage(const LanguageSettings::AvailableLanguageEnum case LanguageSettings::AvailableLanguageEnum::English: emit updateTranslations(QLocale::English); break; case LanguageSettings::AvailableLanguageEnum::Russian: emit updateTranslations(QLocale::Russian); break; case LanguageSettings::AvailableLanguageEnum::China_cn: emit updateTranslations(QLocale::Chinese); break; + case LanguageSettings::AvailableLanguageEnum::Persian: emit updateTranslations(QLocale::Persian); break; default: emit updateTranslations(QLocale::English); break; } } @@ -68,6 +70,7 @@ int LanguageModel::getCurrentLanguageIndex() case QLocale::English: return static_cast(LanguageSettings::AvailableLanguageEnum::English); break; case QLocale::Russian: return static_cast(LanguageSettings::AvailableLanguageEnum::Russian); break; case QLocale::Chinese: return static_cast(LanguageSettings::AvailableLanguageEnum::China_cn); break; + case QLocale::Persian: return static_cast(LanguageSettings::AvailableLanguageEnum::Persian); break; default: return static_cast(LanguageSettings::AvailableLanguageEnum::English); break; } } diff --git a/client/ui/models/languageModel.h b/client/ui/models/languageModel.h index c8879a34..8a55263c 100644 --- a/client/ui/models/languageModel.h +++ b/client/ui/models/languageModel.h @@ -12,7 +12,8 @@ namespace LanguageSettings enum class AvailableLanguageEnum { English, Russian, - China_cn + China_cn, + Persian }; Q_ENUM_NS(AvailableLanguageEnum) diff --git a/client/ui/models/servers_model.cpp b/client/ui/models/servers_model.cpp index f3b2337f..ad927fce 100644 --- a/client/ui/models/servers_model.cpp +++ b/client/ui/models/servers_model.cpp @@ -1,5 +1,7 @@ #include "servers_model.h" +#include "core/controllers/serverController.h" + ServersModel::ServersModel(std::shared_ptr settings, QObject *parent) : m_settings(settings), QAbstractListModel(parent) { @@ -8,6 +10,11 @@ ServersModel::ServersModel(std::shared_ptr settings, QObject *parent) m_currentlyProcessedServerIndex = m_defaultServerIndex; connect(this, &ServersModel::defaultServerIndexChanged, this, &ServersModel::defaultServerNameChanged); + connect(this, &ServersModel::defaultContainerChanged, this, &ServersModel::defaultServerDescriptionChanged); + connect(this, &ServersModel::defaultServerIndexChanged, this, [this](const int serverIndex) { + auto defaultContainer = ContainerProps::containerFromString(m_servers.at(serverIndex).toObject().value(config_key::defaultContainer).toString()); + emit ServersModel::defaultContainerChanged(defaultContainer); + }); } int ServersModel::rowCount(const QModelIndex &parent) const @@ -50,14 +57,23 @@ QVariant ServersModel::data(const QModelIndex &index, int role) const } const QJsonObject server = m_servers.at(index.row()).toObject(); - + const auto configVersion = server.value(config_key::configVersion).toInt(); switch (role) { case NameRole: { - auto description = server.value(config_key::description).toString(); - if (description.isEmpty()) { + if (configVersion) { + return server.value(config_key::name).toString(); + } + auto name = server.value(config_key::description).toString(); + if (name.isEmpty()) { return server.value(config_key::hostName).toString(); } - return description; + return name; + } + case ServerDescriptionRole: { + if (configVersion) { + return server.value(config_key::description).toString(); + } + return server.value(config_key::hostName).toString(); } case HostNameRole: return server.value(config_key::hostName).toString(); case CredentialsRole: return QVariant::fromValue(serverCredentials(index.row())); @@ -72,6 +88,9 @@ QVariant ServersModel::data(const QModelIndex &index, int role) const QString primaryDns = server.value(config_key::dns1).toString(); return primaryDns == protocols::dns::amneziaDnsIp; } + case DefaultContainerRole: { + return ContainerProps::containerFromString(server.value(config_key::defaultContainer).toString()); + } } return QVariant(); @@ -114,6 +133,53 @@ const QString ServersModel::getDefaultServerHostName() return qvariant_cast(data(m_defaultServerIndex, HostNameRole)); } +QString ServersModel::getDefaultServerDescription(const QJsonObject &server) +{ + const auto configVersion = server.value(config_key::configVersion).toInt(); + + QString description; + + if (configVersion) { + return server.value(config_key::description).toString(); + } else if (isDefaultServerHasWriteAccess()) { + if (m_isAmneziaDnsEnabled + && isAmneziaDnsContainerInstalled(m_defaultServerIndex)) { + description += "Amnezia DNS | "; + } + } else { + if (isDefaultServerConfigContainsAmneziaDns()) { + description += "Amnezia DNS | "; + } + } + return description; +} + +const QString ServersModel::getDefaultServerDescriptionCollapsed() +{ + const QJsonObject server = m_servers.at(m_defaultServerIndex).toObject(); + const auto configVersion = server.value(config_key::configVersion).toInt(); + auto description = getDefaultServerDescription(server); + if (configVersion) { + return description; + } + + auto container = ContainerProps::containerFromString(server.value(config_key::defaultContainer).toString()); + + return description += ContainerProps::containerHumanNames().value(container) + " | " + server.value(config_key::hostName).toString(); +} + +const QString ServersModel::getDefaultServerDescriptionExpanded() +{ + const QJsonObject server = m_servers.at(m_defaultServerIndex).toObject(); + const auto configVersion = server.value(config_key::configVersion).toInt(); + auto description = getDefaultServerDescription(server); + if (configVersion) { + return description; + } + + return description += server.value(config_key::hostName).toString(); +} + const int ServersModel::getServersCount() { return m_servers.count(); @@ -132,6 +198,7 @@ bool ServersModel::hasServerWithWriteAccess() void ServersModel::setCurrentlyProcessedServerIndex(const int index) { m_currentlyProcessedServerIndex = index; + updateContainersModel(); emit currentlyProcessedServerIndexChanged(m_currentlyProcessedServerIndex); } @@ -145,6 +212,16 @@ QString ServersModel::getCurrentlyProcessedServerHostName() return qvariant_cast(data(m_currentlyProcessedServerIndex, HostNameRole)); } +const ServerCredentials ServersModel::getCurrentlyProcessedServerCredentials() +{ + return serverCredentials(m_currentlyProcessedServerIndex); +} + +const ServerCredentials ServersModel::getServerCredentials(const int index) +{ + return serverCredentials(index); +} + bool ServersModel::isDefaultServerCurrentlyProcessed() { return m_defaultServerIndex == m_currentlyProcessedServerIndex; @@ -168,6 +245,15 @@ void ServersModel::addServer(const QJsonObject &server) endResetModel(); } +void ServersModel::editServer(const QJsonObject &server) +{ + beginResetModel(); + m_settings->editServer(m_currentlyProcessedServerIndex, server); + m_servers = m_settings->serversArray(); + endResetModel(); + updateContainersModel(); +} + void ServersModel::removeServer() { beginResetModel(); @@ -193,23 +279,27 @@ bool ServersModel::isDefaultServerConfigContainsAmneziaDns() return primaryDns == protocols::dns::amneziaDnsIp; } -void ServersModel::updateContainersConfig() -{ - auto server = m_settings->server(m_currentlyProcessedServerIndex); - m_servers.replace(m_currentlyProcessedServerIndex, server); -} - QHash ServersModel::roleNames() const { QHash roles; + + roles[NameRole] = "serverName"; roles[NameRole] = "name"; + roles[ServerDescriptionRole] = "serverDescription"; + roles[HostNameRole] = "hostName"; + roles[CredentialsRole] = "credentials"; roles[CredentialsLoginRole] = "credentialsLogin"; + roles[IsDefaultRole] = "isDefault"; roles[IsCurrentlyProcessedRole] = "isCurrentlyProcessed"; + roles[HasWriteAccessRole] = "hasWriteAccess"; + roles[ContainsAmneziaDnsRole] = "containsAmneziaDns"; + + roles[DefaultContainerRole] = "defaultContainer"; return roles; } @@ -225,3 +315,206 @@ ServerCredentials ServersModel::serverCredentials(int index) const return credentials; } + +void ServersModel::updateContainersModel() +{ + auto containers = m_servers.at(m_currentlyProcessedServerIndex).toObject().value(config_key::containers).toArray(); + emit containersUpdated(containers); +} + +QJsonObject ServersModel::getDefaultServerConfig() +{ + return m_servers.at(m_defaultServerIndex).toObject(); +} + +void ServersModel::reloadContainerConfig() +{ + QJsonObject server = m_servers.at(m_currentlyProcessedServerIndex).toObject(); + auto container = ContainerProps::containerFromString(server.value(config_key::defaultContainer).toString()); + + auto containers = server.value(config_key::containers).toArray(); + + auto config = m_settings->containerConfig(m_currentlyProcessedServerIndex, container); + for (auto i = 0; i < containers.size(); i++) { + auto c = ContainerProps::containerFromString(containers.at(i).toObject().value(config_key::container).toString()); + if (c == container) { + containers.replace(i, config); + break; + } + } + + server.insert(config_key::containers, containers); + editServer(server); +} + +void ServersModel::updateContainerConfig(const int containerIndex, const QJsonObject config) +{ + auto container = static_cast(containerIndex); + QJsonObject server = m_servers.at(m_currentlyProcessedServerIndex).toObject(); + + auto containers = server.value(config_key::containers).toArray(); + for (auto i = 0; i < containers.size(); i++) { + auto c = ContainerProps::containerFromString(containers.at(i).toObject().value(config_key::container).toString()); + if (c == container) { + containers.replace(i, config); + break; + } + } + + server.insert(config_key::containers, containers); + + auto defaultContainer = server.value(config_key::defaultContainer).toString(); + if ((ContainerProps::containerFromString(defaultContainer) == DockerContainer::None || ContainerProps::containerService(container) != ServiceType::Other)) { + server.insert(config_key::defaultContainer, ContainerProps::containerToString(container)); + } + + editServer(server); +} + +void ServersModel::addContainerConfig(const int containerIndex, const QJsonObject config) +{ + auto container = static_cast(containerIndex); + QJsonObject server = m_servers.at(m_currentlyProcessedServerIndex).toObject(); + + auto containers = server.value(config_key::containers).toArray(); + containers.push_back(config); + + server.insert(config_key::containers, containers); + + bool isDefaultContainerChanged = false; + auto defaultContainer = server.value(config_key::defaultContainer).toString(); + if ((ContainerProps::containerFromString(defaultContainer) == DockerContainer::None || ContainerProps::containerService(container) != ServiceType::Other)) { + server.insert(config_key::defaultContainer, ContainerProps::containerToString(container)); + isDefaultContainerChanged = true; + } + + editServer(server); + if (isDefaultContainerChanged) { + emit defaultContainerChanged(container); + } +} + +void ServersModel::setDefaultContainer(const int containerIndex) +{ + auto container = static_cast(containerIndex); + QJsonObject s = m_servers.at(m_currentlyProcessedServerIndex).toObject(); + s.insert(config_key::defaultContainer, ContainerProps::containerToString(container)); + editServer(s); //check + emit defaultContainerChanged(container); +} + +DockerContainer ServersModel::getDefaultContainer() +{ + return qvariant_cast(data(m_currentlyProcessedServerIndex, DefaultContainerRole)); +} + +const QString ServersModel::getDefaultContainerName() +{ + auto defaultContainer = getDefaultContainer(); + return ContainerProps::containerHumanNames().value(defaultContainer); +} + +ErrorCode ServersModel::removeAllContainers() +{ + ServerController serverController(m_settings); + ErrorCode errorCode = + serverController.removeAllContainers(m_settings->serverCredentials(m_currentlyProcessedServerIndex)); + + if (errorCode == ErrorCode::NoError) { + QJsonObject s = m_servers.at(m_currentlyProcessedServerIndex).toObject(); + s.insert(config_key::containers, {}); + s.insert(config_key::defaultContainer, ContainerProps::containerToString(DockerContainer::None)); + + editServer(s); + emit defaultContainerChanged(DockerContainer::None); + } + return errorCode; +} + +ErrorCode ServersModel::removeContainer(const int containerIndex) +{ + ServerController serverController(m_settings); + auto credentials = m_settings->serverCredentials(m_currentlyProcessedServerIndex); + auto dockerContainer = static_cast(containerIndex); + + ErrorCode errorCode = serverController.removeContainer(credentials, dockerContainer); + + if (errorCode == ErrorCode::NoError) { + QJsonObject server = m_servers.at(m_currentlyProcessedServerIndex).toObject(); + + auto containers = server.value(config_key::containers).toArray(); + for (auto it = containers.begin(); it != containers.end(); it++) { + if (it->toObject().value(config_key::container).toString() == ContainerProps::containerToString(dockerContainer)) { + containers.erase(it); + break; + } + } + + server.insert(config_key::containers, containers); + + auto defaultContainer = ContainerProps::containerFromString(server.value(config_key::defaultContainer).toString()); + if (defaultContainer == containerIndex) { + if (containers.empty()) { + defaultContainer = DockerContainer::None; + } else { + defaultContainer = ContainerProps::containerFromString(containers.begin()->toObject().value(config_key::container).toString()); + } + server.insert(config_key::defaultContainer, ContainerProps::containerToString(defaultContainer)); + } + + editServer(server); + emit defaultContainerChanged(defaultContainer); + } + return errorCode; +} + +void ServersModel::clearCachedProfiles() +{ + const auto &containers = m_settings->containers(m_currentlyProcessedServerIndex); + for (DockerContainer container : containers.keys()) { + m_settings->clearLastConnectionConfig(m_currentlyProcessedServerIndex, container); + } + + m_servers.replace(m_currentlyProcessedServerIndex, m_settings->server(m_currentlyProcessedServerIndex)); + updateContainersModel(); +} + +bool ServersModel::isAmneziaDnsContainerInstalled(const int serverIndex) +{ + QJsonObject server = m_servers.at(serverIndex).toObject(); + auto containers = server.value(config_key::containers).toArray(); + for (auto it = containers.begin(); it != containers.end(); it++) { + if (it->toObject().value(config_key::container).toString() == ContainerProps::containerToString(DockerContainer::Dns)) { + return true; + } + } + return false; +} + +QStringList ServersModel::getAllInstalledServicesName(const int serverIndex) +{ + QStringList servicesName; + QJsonObject server = m_servers.at(serverIndex).toObject(); + const auto containers = server.value(config_key::containers).toArray(); + for (auto it = containers.begin(); it != containers.end(); it++) { + auto container = ContainerProps::containerFromString(it->toObject().value(config_key::container).toString()); + if (ContainerProps::containerService(container) == ServiceType::Other) { + if (container == DockerContainer::Dns) { + servicesName.append("DNS"); + } else if (container == DockerContainer::Sftp) { + servicesName.append("SFTP"); + } else if (container == DockerContainer::TorWebSite) { + servicesName.append("TOR"); + } + } + } + servicesName.sort(); + return servicesName; +} + +void ServersModel::toggleAmneziaDns(bool enabled) +{ + m_isAmneziaDnsEnabled = enabled; + emit defaultServerDescriptionChanged(); +} + diff --git a/client/ui/models/servers_model.h b/client/ui/models/servers_model.h index 97d8015f..901605e2 100644 --- a/client/ui/models/servers_model.h +++ b/client/ui/models/servers_model.h @@ -11,13 +11,21 @@ class ServersModel : public QAbstractListModel public: enum Roles { NameRole = Qt::UserRole + 1, + ServerDescriptionRole, + HostNameRole, + CredentialsRole, CredentialsLoginRole, + IsDefaultRole, IsCurrentlyProcessedRole, + HasWriteAccessRole, - ContainsAmneziaDnsRole + + ContainsAmneziaDnsRole, + + DefaultContainerRole }; ServersModel(std::shared_ptr settings, QObject *parent = nullptr); @@ -33,6 +41,10 @@ public: Q_PROPERTY(int defaultIndex READ getDefaultServerIndex WRITE setDefaultServerIndex NOTIFY defaultServerIndexChanged) Q_PROPERTY(QString defaultServerName READ getDefaultServerName NOTIFY defaultServerNameChanged) Q_PROPERTY(QString defaultServerHostName READ getDefaultServerHostName NOTIFY defaultServerIndexChanged) + Q_PROPERTY(QString defaultContainerName READ getDefaultContainerName NOTIFY defaultContainerChanged) + Q_PROPERTY(QString defaultServerDescriptionCollapsed READ getDefaultServerDescriptionCollapsed NOTIFY defaultServerDescriptionChanged) + Q_PROPERTY(QString defaultServerDescriptionExpanded READ getDefaultServerDescriptionExpanded NOTIFY defaultServerDescriptionChanged) + Q_PROPERTY(int currentlyProcessedIndex READ getCurrentlyProcessedServerIndex WRITE setCurrentlyProcessedServerIndex NOTIFY currentlyProcessedServerIndexChanged) @@ -41,6 +53,8 @@ public slots: const int getDefaultServerIndex(); const QString getDefaultServerName(); const QString getDefaultServerHostName(); + const QString getDefaultServerDescriptionCollapsed(); + const QString getDefaultServerDescriptionExpanded(); bool isDefaultServerCurrentlyProcessed(); bool isCurrentlyProcessedServerHasWriteAccess(); @@ -53,13 +67,34 @@ public slots: int getCurrentlyProcessedServerIndex(); QString getCurrentlyProcessedServerHostName(); + const ServerCredentials getCurrentlyProcessedServerCredentials(); + const ServerCredentials getServerCredentials(const int index); void addServer(const QJsonObject &server); + void editServer(const QJsonObject &server); void removeServer(); bool isDefaultServerConfigContainsAmneziaDns(); + bool isAmneziaDnsContainerInstalled(const int serverIndex); - void updateContainersConfig(); + QJsonObject getDefaultServerConfig(); + + void reloadContainerConfig(); + void updateContainerConfig(const int containerIndex, const QJsonObject config); + void addContainerConfig(const int containerIndex, const QJsonObject config); + + void clearCachedProfiles(); + + ErrorCode removeContainer(const int containerIndex); + ErrorCode removeAllContainers(); + + void setDefaultContainer(const int containerIndex); + DockerContainer getDefaultContainer(); + const QString getDefaultContainerName(); + + QStringList getAllInstalledServicesName(const int serverIndex); + + void toggleAmneziaDns(bool enabled); protected: QHash roleNames() const override; @@ -68,9 +103,16 @@ signals: void currentlyProcessedServerIndexChanged(const int index); void defaultServerIndexChanged(const int index); void defaultServerNameChanged(); + void defaultServerDescriptionChanged(); + + void containersUpdated(QJsonArray &containers); + void defaultContainerChanged(const int containerIndex); private: ServerCredentials serverCredentials(int index) const; + void updateContainersModel(); + + QString getDefaultServerDescription(const QJsonObject &server); QJsonArray m_servers; @@ -78,6 +120,8 @@ private: int m_defaultServerIndex; int m_currentlyProcessedServerIndex; + + bool m_isAmneziaDnsEnabled = m_settings->useAmneziaDns(); }; #endif // SERVERSMODEL_H diff --git a/client/ui/qml/Components/ConnectButton.qml b/client/ui/qml/Components/ConnectButton.qml index c2ec186d..a0a57e6a 100644 --- a/client/ui/qml/Components/ConnectButton.qml +++ b/client/ui/qml/Components/ConnectButton.qml @@ -138,6 +138,10 @@ Button { } onClicked: { + if (!ApiController.updateServerConfigFromApi()) { + return + } + if (!ContainersModel.isAnyContainerInstalled()) { PageController.setTriggeredBtConnectButton(true) diff --git a/client/ui/qml/Components/HomeContainersListView.qml b/client/ui/qml/Components/HomeContainersListView.qml index f05b90d6..78ea9330 100644 --- a/client/ui/qml/Components/HomeContainersListView.qml +++ b/client/ui/qml/Components/HomeContainersListView.qml @@ -60,9 +60,8 @@ ListView { } if (checked) { - isDefault = true + ServersModel.setDefaultContainer(proxyContainersModel.mapToSource(index)) - menuContent.currentIndex = index containersDropDown.menuVisible = false } else { if (!isSupported && isInstalled) { diff --git a/client/ui/qml/Components/ShareConnectionDrawer.qml b/client/ui/qml/Components/ShareConnectionDrawer.qml index 1158dadc..e354e951 100644 --- a/client/ui/qml/Components/ShareConnectionDrawer.qml +++ b/client/ui/qml/Components/ShareConnectionDrawer.qml @@ -112,6 +112,30 @@ DrawerType { } } + BasicButtonType { + Layout.fillWidth: true + Layout.topMargin: 8 + + visible: nativeConfigString.text !== "" + + defaultColor: "transparent" + hoveredColor: Qt.rgba(1, 1, 1, 0.08) + pressedColor: Qt.rgba(1, 1, 1, 0.12) + disabledColor: "#878B91" + textColor: "#D7D8DB" + borderWidth: 1 + + text: qsTr("Copy config string") + imageSource: "qrc:/images/controls/copy.svg" + + onClicked: { + nativeConfigString.selectAll() + nativeConfigString.copy() + nativeConfigString.select(0, 0) + PageController.showNotificationMessage(qsTr("Copied")) + } + } + BasicButtonType { Layout.fillWidth: true Layout.topMargin: 24 @@ -170,6 +194,12 @@ DrawerType { } TextField { + id: nativeConfigString + visible: false + text: ExportController.nativeConfigString + } + + TextArea { id: configText Layout.fillWidth: true @@ -213,7 +243,6 @@ DrawerType { Image { anchors.fill: parent - anchors.margins: 2 smooth: false source: ExportController.qrCodesCount ? ExportController.qrCodes[0] : "" diff --git a/client/ui/qml/Controls2/SwitcherType.qml b/client/ui/qml/Controls2/SwitcherType.qml index 1dbd0e84..37024872 100644 --- a/client/ui/qml/Controls2/SwitcherType.qml +++ b/client/ui/qml/Controls2/SwitcherType.qml @@ -87,6 +87,7 @@ Switch { id: content anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left ListItemTitleType { Layout.fillWidth: true diff --git a/client/ui/qml/Pages2/PageHome.qml b/client/ui/qml/Pages2/PageHome.qml index 222f7764..8374dbc3 100644 --- a/client/ui/qml/Pages2/PageHome.qml +++ b/client/ui/qml/Pages2/PageHome.qml @@ -22,10 +22,6 @@ PageType { property string borderColor: "#2C2D30" - property string defaultServerName: ServersModel.defaultServerName - property string defaultServerHostName: ServersModel.defaultServerHostName - property string defaultContainerName: ContainersModel.defaultContainerName - Connections { target: PageController @@ -40,41 +36,6 @@ PageType { } } - Connections { - target: ServersModel - - function onDefaultServerIndexChanged() { - updateDescriptions() - } - } - - Connections { - target: ContainersModel - - function onDefaultContainerChanged() { - updateDescriptions() - } - } - - function updateDescriptions() { - var description = "" - if (ServersModel.isDefaultServerHasWriteAccess()) { - if (SettingsController.isAmneziaDnsEnabled() - && ContainersModel.isAmneziaDnsContainerInstalled(ServersModel.getDefaultServerIndex())) { - description += "Amnezia DNS | " - } - } else { - if (ServersModel.isDefaultServerConfigContainsAmneziaDns()) { - description += "Amnezia DNS | " - } - } - - collapsedServerMenuDescription.text = description + root.defaultContainerName + " | " + root.defaultServerHostName - expandedServersMenuDescription.text = description + root.defaultServerHostName - } - - Component.onCompleted: updateDescriptions() - MouseArea { anchors.fill: parent enabled: buttonContent.state === "expanded" @@ -267,7 +228,7 @@ PageType { maximumLineCount: 2 elide: Qt.ElideRight - text: root.defaultServerName + text: ServersModel.defaultServerName horizontalAlignment: Qt.AlignHCenter Behavior on opacity { @@ -304,6 +265,7 @@ PageType { Layout.bottomMargin: 44 Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter visible: buttonContent.collapsedVisibility + text: ServersModel.defaultServerDescriptionCollapsed } ColumnLayout { @@ -319,7 +281,7 @@ PageType { Layout.leftMargin: 16 Layout.rightMargin: 16 - text: root.defaultServerName + text: ServersModel.defaultServerName horizontalAlignment: Qt.AlignHCenter maximumLineCount: 2 elide: Qt.ElideRight @@ -331,6 +293,7 @@ PageType { Layout.fillWidth: true horizontalAlignment: Qt.AlignHCenter verticalAlignment: Qt.AlignVCenter + text: ServersModel.defaultServerDescriptionExpanded } RowLayout { @@ -349,7 +312,7 @@ PageType { rootButtonTextTopMargin: 8 rootButtonTextBottomMargin: 8 - text: root.defaultContainerName + text: ServersModel.defaultContainerName textColor: "#0E0E11" headerText: qsTr("VPN protocol") headerBackButtonImage: "qrc:/images/controls/arrow-left.svg" @@ -468,7 +431,7 @@ PageType { var description = "" if (hasWriteAccess) { if (SettingsController.isAmneziaDnsEnabled() - && ContainersModel.isAmneziaDnsContainerInstalled(index)) { + && ServersModel.isAmneziaDnsContainerInstalled(index)) { description += "Amnezia DNS | " } } else { diff --git a/client/ui/qml/Pages2/PageProtocolAwgSettings.qml b/client/ui/qml/Pages2/PageProtocolAwgSettings.qml index 237a8b46..a4f5abe3 100644 --- a/client/ui/qml/Pages2/PageProtocolAwgSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolAwgSettings.qml @@ -312,9 +312,8 @@ PageType { onClicked: { forceActiveFocus() - PageController.showBusyIndicator(true) + PageController.goToPage(PageEnum.PageSetupWizardInstalling); InstallController.updateContainer(AwgConfigModel.getConfig()) - PageController.showBusyIndicator(false) } } } diff --git a/client/ui/qml/Pages2/PageProtocolCloakSettings.qml b/client/ui/qml/Pages2/PageProtocolCloakSettings.qml index 78e666a7..98e9c28f 100644 --- a/client/ui/qml/Pages2/PageProtocolCloakSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolCloakSettings.qml @@ -4,6 +4,8 @@ import QtQuick.Layouts import SortFilterProxyModel 0.2 +import PageEnum 1.0 + import "./" import "../Controls2" import "../Controls2/TextTypes" @@ -160,9 +162,8 @@ PageType { onClicked: { forceActiveFocus() - PageController.showBusyIndicator(true) + PageController.goToPage(PageEnum.PageSetupWizardInstalling); InstallController.updateContainer(CloakConfigModel.getConfig()) - PageController.showBusyIndicator(false) } } } diff --git a/client/ui/qml/Pages2/PageProtocolOpenVpnSettings.qml b/client/ui/qml/Pages2/PageProtocolOpenVpnSettings.qml index 55cdcf04..971f9f39 100644 --- a/client/ui/qml/Pages2/PageProtocolOpenVpnSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolOpenVpnSettings.qml @@ -390,9 +390,8 @@ PageType { onClicked: { forceActiveFocus() - PageController.showBusyIndicator(true) + PageController.goToPage(PageEnum.PageSetupWizardInstalling); InstallController.updateContainer(OpenVpnConfigModel.getConfig()) - PageController.showBusyIndicator(false) } } } diff --git a/client/ui/qml/Pages2/PageProtocolShadowSocksSettings.qml b/client/ui/qml/Pages2/PageProtocolShadowSocksSettings.qml index 2453281f..573aca06 100644 --- a/client/ui/qml/Pages2/PageProtocolShadowSocksSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolShadowSocksSettings.qml @@ -4,6 +4,8 @@ import QtQuick.Layouts import SortFilterProxyModel 0.2 +import PageEnum 1.0 + import "./" import "../Controls2" import "../Controls2/TextTypes" @@ -138,9 +140,8 @@ PageType { onClicked: { forceActiveFocus() - PageController.showBusyIndicator(true) + PageController.goToPage(PageEnum.PageSetupWizardInstalling); InstallController.updateContainer(ShadowSocksConfigModel.getConfig()) - PageController.showBusyIndicator(false) } } } diff --git a/client/ui/qml/Pages2/PageServiceTorWebsiteSettings.qml b/client/ui/qml/Pages2/PageServiceTorWebsiteSettings.qml index 3bfa5bb0..aca4575c 100644 --- a/client/ui/qml/Pages2/PageServiceTorWebsiteSettings.qml +++ b/client/ui/qml/Pages2/PageServiceTorWebsiteSettings.qml @@ -66,8 +66,8 @@ PageType { text: qsTr("Website address") descriptionText: { - var config = ContainersModel.getCurrentlyProcessedContainerConfig() var containerIndex = ContainersModel.getCurrentlyProcessedContainerIndex() + var config = ContainersModel.getContainerConfig(containerIndex) return config[ContainerProps.containerTypeToString(containerIndex)]["site"] } diff --git a/client/ui/qml/Pages2/PageSettingsServersList.qml b/client/ui/qml/Pages2/PageSettingsServersList.qml index 040aafc3..dca904ae 100644 --- a/client/ui/qml/Pages2/PageSettingsServersList.qml +++ b/client/ui/qml/Pages2/PageSettingsServersList.qml @@ -77,7 +77,7 @@ PageType { text: name descriptionText: { var servicesNameString = "" - var servicesName = ContainersModel.getAllInstalledServicesName(index) + var servicesName = ServersModel.getAllInstalledServicesName(index) for (var i = 0; i < servicesName.length; i++) { servicesNameString += servicesName[i] + " · " } diff --git a/client/ui/qml/Pages2/PageSetupWizardCredentials.qml b/client/ui/qml/Pages2/PageSetupWizardCredentials.qml index 5c32b0c5..3eadb647 100644 --- a/client/ui/qml/Pages2/PageSetupWizardCredentials.qml +++ b/client/ui/qml/Pages2/PageSetupWizardCredentials.qml @@ -54,7 +54,7 @@ PageType { regularExpression: InstallController.ipAddressPortRegExp() } - onTextFieldTextChanged: { + onFocusChanged: { textField.text = textField.text.replace(/^\s+|\s+$/g, ''); } } @@ -81,6 +81,10 @@ PageType { clickedFunc: function() { hidePassword = !hidePassword } + + onFocusChanged: { + textField.text = textField.text.replace(/^\s+|\s+$/g, ''); + } } BasicButtonType { @@ -90,6 +94,7 @@ PageType { text: qsTr("Continue") onClicked: function() { + forceActiveFocus() if (!isCredentialsFilled()) { return } @@ -112,8 +117,7 @@ PageType { Layout.fillWidth: true Layout.topMargin: 12 - text: qsTr("All data you enter will remain strictly confidential -and will not be shared or disclosed to the Amnezia or any third parties") + text: qsTr("All data you enter will remain strictly confidential and will not be shared or disclosed to the Amnezia or any third parties") } } } diff --git a/client/ui/qml/Pages2/PageSetupWizardInstalling.qml b/client/ui/qml/Pages2/PageSetupWizardInstalling.qml index c82ce855..9811d87d 100644 --- a/client/ui/qml/Pages2/PageSetupWizardInstalling.qml +++ b/client/ui/qml/Pages2/PageSetupWizardInstalling.qml @@ -19,13 +19,14 @@ PageType { property bool isTimerRunning: true property string progressBarText: qsTr("Usually it takes no more than 5 minutes") + property bool isCancelButtonVisible: false Connections { target: InstallController function onInstallContainerFinished(finishedMessage, isServiceInstall) { if (!ConnectionController.isConnected && !isServiceInstall) { - ContainersModel.setDefaultContainer(ContainersModel.getCurrentlyProcessedContainerIndex()) + ServersModel.setDefaultContainer(ContainersModel.getCurrentlyProcessedContainerIndex()) } PageController.closePage() // close installing page @@ -61,11 +62,13 @@ PageType { function onServerIsBusy(isBusy) { if (isBusy) { + root.isCancelButtonVisible = true root.progressBarText = qsTr("Amnezia has detected that your server is currently ") + qsTr("busy installing other software. Amnezia installation ") + qsTr("will pause until the server finishes installing other software") root.isTimerRunning = false } else { + root.isCancelButtonVisible = false root.progressBarText = qsTr("Usually it takes no more than 5 minutes") root.isTimerRunning = true } @@ -150,6 +153,22 @@ PageType { text: root.progressBarText } + + BasicButtonType { + id: cancelIntallationButton + + Layout.fillWidth: true + Layout.topMargin: 24 + + visible: root.isCancelButtonVisible + + text: qsTr("Cancel installation") + + onClicked: { + InstallController.cancelInstallation() + PageController.showBusyIndicator(true) + } + } } } } diff --git a/client/ui/qml/Pages2/PageSetupWizardStart.qml b/client/ui/qml/Pages2/PageSetupWizardStart.qml index 994ec200..2f89bc57 100644 --- a/client/ui/qml/Pages2/PageSetupWizardStart.qml +++ b/client/ui/qml/Pages2/PageSetupWizardStart.qml @@ -60,6 +60,7 @@ PageType { target: InstallController function onInstallationErrorOccurred(errorMessage) { + PageController.showBusyIndicator(false) PageController.showErrorMessage(errorMessage) var currentPageName = stackView.currentItem.objectName diff --git a/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml b/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml index ac35651f..65a6f319 100644 --- a/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml +++ b/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml @@ -24,7 +24,7 @@ PageType { } function onImportFinished() { - if (ConnectionController.isConnected) { + if (!ConnectionController.isConnected) { ServersModel.setDefaultServerIndex(ServersModel.getServersCount() - 1); } diff --git a/client/ui/qml/Pages2/PageShare.qml b/client/ui/qml/Pages2/PageShare.qml index ced7a5ff..38010b8f 100644 --- a/client/ui/qml/Pages2/PageShare.qml +++ b/client/ui/qml/Pages2/PageShare.qml @@ -18,15 +18,29 @@ PageType { enum ConfigType { AmneziaConnection, - AmneziaFullAccess, OpenVpn, - WireGuard + WireGuard, + ShadowSocks, + Cloak + } + + signal revokeConfig(int index) + onRevokeConfig: function(index) { + PageController.showBusyIndicator(true) + ExportController.revokeConfig(index, + ContainersModel.getCurrentlyProcessedContainerIndex(), + ServersModel.getCurrentlyProcessedServerCredentials()) + PageController.showBusyIndicator(false) + PageController.showNotificationMessage(qsTr("Config revoked")) } Connections { target: ExportController function onGenerateConfig(type) { + shareConnectionDrawer.headerText = qsTr("Connection to ") + serverSelector.text + shareConnectionDrawer.configContentHeaderText = qsTr("File with connection settings to ") + serverSelector.text + shareConnectionDrawer.needCloseButton = false shareConnectionDrawer.open() @@ -34,28 +48,34 @@ PageType { PageController.showBusyIndicator(true) switch (type) { - case PageShare.ConfigType.AmneziaConnection: ExportController.generateConnectionConfig(); break; - case PageShare.ConfigType.AmneziaFullAccess: { - if (Qt.platform.os === "android") { - ExportController.generateFullAccessConfigAndroid(); - } else { - ExportController.generateFullAccessConfig(); - } - break; - } + case PageShare.ConfigType.AmneziaConnection: ExportController.generateConnectionConfig(clientNameTextField.textFieldText); break; case PageShare.ConfigType.OpenVpn: { - ExportController.generateOpenVpnConfig(); + ExportController.generateOpenVpnConfig(clientNameTextField.textFieldText) shareConnectionDrawer.configCaption = qsTr("Save OpenVPN config") shareConnectionDrawer.configExtension = ".ovpn" shareConnectionDrawer.configFileName = "amnezia_for_openvpn" - break; + break } case PageShare.ConfigType.WireGuard: { - ExportController.generateWireGuardConfig(); + ExportController.generateWireGuardConfig(clientNameTextField.textFieldText) shareConnectionDrawer.configCaption = qsTr("Save WireGuard config") shareConnectionDrawer.configExtension = ".conf" shareConnectionDrawer.configFileName = "amnezia_for_wireguard" - break; + break + } + case PageShare.ConfigType.ShadowSocks: { + ExportController.generateShadowSocksConfig() + shareConnectionDrawer.configCaption = qsTr("Save ShadowSocks config") + shareConnectionDrawer.configExtension = ".json" + shareConnectionDrawer.configFileName = "amnezia_for_shadowsocks" + break + } + case PageShare.ConfigType.Cloak: { + ExportController.generateCloakConfig() + shareConnectionDrawer.configCaption = qsTr("Save Cloak config") + shareConnectionDrawer.configExtension = ".json" + shareConnectionDrawer.configFileName = "amnezia_for_cloak" + break } } @@ -73,8 +93,7 @@ PageType { } } - property string fullConfigServerSelectorText - property string connectionServerSelectorText + property bool isSearchBarVisible: false property bool showContent: false property bool shareButtonEnabled: true property list connectionTypesModel: [ @@ -96,6 +115,16 @@ PageType { property string name: qsTr("WireGuard native format") property var type: PageShare.ConfigType.WireGuard } + QtObject { + id: shadowSocksConnectionFormat + property string name: qsTr("ShadowSocks native format") + property var type: PageShare.ConfigType.ShadowSocks + } + QtObject { + id: cloakConnectionFormat + property string name: qsTr("Cloak native format") + property var type: PageShare.ConfigType.Cloak + } FlickableType { anchors.top: parent.top @@ -119,6 +148,51 @@ PageType { Layout.topMargin: 24 headerText: qsTr("Share VPN Access") + + actionButtonImage: "qrc:/images/controls/more-vertical.svg" + actionButtonFunction: function() { + shareFullAccessDrawer.open() + } + + DrawerType { + id: shareFullAccessDrawer + + width: root.width + height: root.height * 0.45 + + + ColumnLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 16 + + spacing: 0 + + Header2Type { + Layout.fillWidth: true + Layout.bottomMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + + headerText: qsTr("Share full access to the server and VPN") + descriptionText: qsTr("Use for your own devices, or share with those you trust to manage the server.") + } + + + LabelWithButtonType { + Layout.fillWidth: true + + text: qsTr("Share") + rightImageSource: "qrc:/images/controls/chevron-right.svg" + + clickedFunction: function() { + PageController.goToPage(PageEnum.PageShareFullAccess) + shareFullAccessDrawer.close() + } + } + } + } } Rectangle { @@ -147,20 +221,21 @@ PageType { onClicked: { accessTypeSelector.currentIndex = 0 - serverSelector.text = root.connectionServerSelectorText } } HorizontalRadioButton { - checked: root.currentIndex === 1 + checked: accessTypeSelector.currentIndex === 1 implicitWidth: (root.width - 32) / 2 - text: qsTr("Full access") + text: qsTr("Users") onClicked: { accessTypeSelector.currentIndex = 1 - serverSelector.text = root.fullConfigServerSelectorText - root.shareButtonEnabled = true + PageController.showBusyIndicator(true) + ExportController.updateClientManagementModel(ContainersModel.getCurrentlyProcessedContainerIndex(), + ServersModel.getCurrentlyProcessedServerCredentials()) + PageController.showBusyIndicator(false) } } } @@ -171,16 +246,30 @@ PageType { Layout.topMargin: 24 Layout.bottomMargin: 24 - text: accessTypeSelector.currentIndex === 0 ? qsTr("Share VPN access without the ability to manage the server") : - qsTr("Share access to server management. The user with whom you share full access to the server will be able to add and remove any protocols and services to the server, as well as change settings.") + visible: accessTypeSelector.currentIndex === 0 + + text: qsTr("Share VPN access without the ability to manage the server") color: "#878B91" } + TextFieldWithHeaderType { + id: clientNameTextField + Layout.fillWidth: true + Layout.topMargin: 16 + + visible: accessTypeSelector.currentIndex === 0 + + headerText: qsTr("User name") + textFieldText: "New client" + + checkEmptyText: true + } + DropDownType { id: serverSelector signal severSelectorIndexChanged - property int currentIndex: 0 + property int currentIndex: -1 Layout.fillWidth: true Layout.topMargin: 16 @@ -207,8 +296,6 @@ PageType { ] } - currentIndex: 0 - clickedFunction: function() { handler() @@ -217,22 +304,17 @@ PageType { serverSelector.severSelectorIndexChanged() } - if (accessTypeSelector.currentIndex !== 0) { - shareConnectionDrawer.headerText = qsTr("Accessing ") + serverSelector.text - shareConnectionDrawer.configContentHeaderText = qsTr("File with accessing settings to ") + serverSelector.text - } serverSelector.menuVisible = false } Component.onCompleted: { - handler() - serverSelector.severSelectorIndexChanged() + serverSelectorListView.currentIndex = ServersModel.isDefaultServerHasWriteAccess() ? + proxyServersModel.mapFromSource(ServersModel.defaultIndex) : 0 + serverSelectorListView.triggerCurrentItem() } function handler() { serverSelector.text = selectedText - root.fullConfigServerSelectorText = selectedText - root.connectionServerSelectorText = selectedText ServersModel.currentlyProcessedIndex = proxyServersModel.mapToSource(currentIndex) } } @@ -241,8 +323,6 @@ PageType { DropDownType { id: protocolSelector - visible: accessTypeSelector.currentIndex === 0 - Layout.fillWidth: true Layout.topMargin: 16 @@ -280,17 +360,11 @@ PageType { protocolSelector.menuVisible = false } - Component.onCompleted: { - if (accessTypeSelector.currentIndex === 0) { - handler() - } - } - Connections { target: serverSelector function onSeverSelectorIndexChanged() { - protocolSelectorListView.currentIndex = 0 + protocolSelectorListView.currentIndex = proxyContainersModel.mapFromSource(ServersModel.getDefaultContainer()) protocolSelectorListView.triggerCurrentItem() } } @@ -304,13 +378,17 @@ PageType { } protocolSelector.text = selectedText - root.connectionServerSelectorText = serverSelector.text - shareConnectionDrawer.headerText = qsTr("Connection to ") + serverSelector.text - shareConnectionDrawer.configContentHeaderText = qsTr("File with connection settings to ") + serverSelector.text ContainersModel.setCurrentlyProcessedContainerIndex(proxyContainersModel.mapToSource(currentIndex)) fillConnectionTypeModel() + + if (accessTypeSelector.currentIndex === 1) { + PageController.showBusyIndicator(true) + ExportController.updateClientManagementModel(ContainersModel.getCurrentlyProcessedContainerIndex(), + ServersModel.getCurrentlyProcessedServerCredentials()) + PageController.showBusyIndicator(false) + } } function fillConnectionTypeModel() { @@ -322,6 +400,13 @@ PageType { root.connectionTypesModel.push(openVpnConnectionFormat) } else if (index === ContainerProps.containerFromString("amnezia-wireguard")) { root.connectionTypesModel.push(wireGuardConnectionFormat) + } else if (index === ContainerProps.containerFromString("amnezia-shadowsocks")) { + root.connectionTypesModel.push(openVpnConnectionFormat) + root.connectionTypesModel.push(shadowSocksConnectionFormat) + } else if (index === ContainerProps.containerFromString("amnezia-openvpn-cloak")) { + root.connectionTypesModel.push(openVpnConnectionFormat) + root.connectionTypesModel.push(shadowSocksConnectionFormat) + root.connectionTypesModel.push(cloakConnectionFormat) } } } @@ -378,18 +463,235 @@ PageType { Layout.topMargin: 40 enabled: shareButtonEnabled + visible: accessTypeSelector.currentIndex === 0 text: qsTr("Share") imageSource: "qrc:/images/controls/share-2.svg" onClicked: { - if (accessTypeSelector.currentIndex === 0) { - ExportController.generateConfig(root.connectionTypesModel[exportTypeSelector.currentIndex].type) - } else { - ExportController.generateConfig(PageShare.ConfigType.AmneziaFullAccess) + ExportController.generateConfig(root.connectionTypesModel[exportTypeSelector.currentIndex].type) + } + } + + Header2Type { + Layout.fillWidth: true + Layout.topMargin: 24 + Layout.bottomMargin: 16 + + visible: accessTypeSelector.currentIndex === 1 && !root.isSearchBarVisible + + headerText: qsTr("Users") + actionButtonImage: "qrc:/images/controls/search.svg" + actionButtonFunction: function() { + root.isSearchBarVisible = true + } + } + + RowLayout { + Layout.topMargin: 24 + Layout.bottomMargin: 16 + visible: accessTypeSelector.currentIndex === 1 && root.isSearchBarVisible + + TextFieldWithHeaderType { + id: searchTextField + Layout.fillWidth: true + + textFieldPlaceholderText: qsTr("Search") + } + + ImageButtonType { + image: "qrc:/images/controls/close.svg" + imageColor: "#D7D8DB" + + onClicked: function() { + root.isSearchBarVisible = false + searchTextField.textFieldText = "" } } } + + ListView { + id: clientsListView + Layout.fillWidth: true + Layout.preferredHeight: childrenRect.height + + visible: accessTypeSelector.currentIndex === 1 + + model: SortFilterProxyModel { + id: proxyClientManagementModel + sourceModel: ClientManagementModel + filters: RegExpFilter { + roleName: "clientName" + pattern: ".*" + searchTextField.textFieldText + ".*" + caseSensitivity: Qt.CaseInsensitive + } + } + + clip: true + interactive: false + + delegate: Item { + implicitWidth: clientsListView.width + implicitHeight: delegateContent.implicitHeight + + ColumnLayout { + id: delegateContent + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + anchors.rightMargin: -16 + anchors.leftMargin: -16 + + LabelWithButtonType { + Layout.fillWidth: true + + text: clientName + rightImageSource: "qrc:/images/controls/chevron-right.svg" + + clickedFunction: function() { + clientInfoDrawer.open() + } + } + + DividerType {} + + DrawerType { + id: clientInfoDrawer + + width: root.width + height: root.height * 0.5 + + ColumnLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 16 + anchors.leftMargin: 16 + anchors.rightMargin: 16 + + spacing: 8 + + Header2Type { + Layout.fillWidth: true + Layout.bottomMargin: 24 + + headerText: clientName + descriptionText: serverSelector.text + } + + BasicButtonType { + Layout.fillWidth: true + Layout.topMargin: 24 + + defaultColor: "transparent" + hoveredColor: Qt.rgba(1, 1, 1, 0.08) + pressedColor: Qt.rgba(1, 1, 1, 0.12) + disabledColor: "#878B91" + textColor: "#D7D8DB" + borderWidth: 1 + + text: qsTr("Rename") + + onClicked: function() { + clientNameEditDrawer.open() + } + + DrawerType { + id: clientNameEditDrawer + + width: root.width + height: root.height * 0.35 + + onVisibleChanged: { + if (clientNameEditDrawer.visible) { + clientNameEditor.textField.forceActiveFocus() + } + } + + ColumnLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 16 + anchors.leftMargin: 16 + anchors.rightMargin: 16 + + TextFieldWithHeaderType { + id: clientNameEditor + Layout.fillWidth: true + headerText: qsTr("Client name") + textFieldText: clientName + textField.maximumLength: 30 + } + + BasicButtonType { + Layout.fillWidth: true + + text: qsTr("Save") + + onClicked: { + if (clientNameEditor.textFieldText !== clientName) { + PageController.showBusyIndicator(true) + ExportController.renameClient(index, + clientNameEditor.textFieldText, + ContainersModel.getCurrentlyProcessedContainerIndex(), + ServersModel.getCurrentlyProcessedServerCredentials()) + PageController.showBusyIndicator(false) + clientNameEditDrawer.close() + } + } + } + } + } + } + + BasicButtonType { + Layout.fillWidth: true + + defaultColor: "transparent" + hoveredColor: Qt.rgba(1, 1, 1, 0.08) + pressedColor: Qt.rgba(1, 1, 1, 0.12) + disabledColor: "#878B91" + textColor: "#D7D8DB" + borderWidth: 1 + + text: qsTr("Revoke") + + onClicked: function() { + questionDrawer.headerText = qsTr("Revoke the config for a user - ") + clientName + "?" + questionDrawer.descriptionText = qsTr("The user will no longer be able to connect to your server.") + questionDrawer.yesButtonText = qsTr("Continue") + questionDrawer.noButtonText = qsTr("Cancel") + + questionDrawer.yesButtonFunction = function() { + questionDrawer.close() + clientInfoDrawer.close() + root.revokeConfig(index) + } + questionDrawer.noButtonFunction = function() { + questionDrawer.close() + } + questionDrawer.open() + } + } + } + } + } + } + } + + QuestionDrawer { + id: questionDrawer + } + } + } + MouseArea { + anchors.fill: parent + onPressed: function(mouse) { + forceActiveFocus() + mouse.accepted = false } } } diff --git a/client/ui/qml/Pages2/PageShareFullAccess.qml b/client/ui/qml/Pages2/PageShareFullAccess.qml new file mode 100644 index 00000000..e59c57ef --- /dev/null +++ b/client/ui/qml/Pages2/PageShareFullAccess.qml @@ -0,0 +1,155 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Dialogs + +import SortFilterProxyModel 0.2 + +import PageEnum 1.0 +import ContainerProps 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Components" + +PageType { + id: root + + BackButtonType { + id: backButton + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 20 + } + + FlickableType { + anchors.top: backButton.bottom + anchors.bottom: parent.bottom + contentHeight: content.height + + ColumnLayout { + id: content + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + anchors.rightMargin: 16 + anchors.leftMargin: 16 + + spacing: 0 + + HeaderType { + Layout.fillWidth: true + Layout.topMargin: 24 + + headerText: qsTr("Full access to the server and VPN") + } + + ParagraphTextType { + Layout.fillWidth: true + Layout.topMargin: 24 + Layout.bottomMargin: 24 + + text: qsTr("We recommend that you use full access to the server only for your own additional devices.\n") + + qsTr("If you share full access with other people, they can remove and add protocols and services to the server, which will cause the VPN to work incorrectly for all users. ") + color: "#878B91" + } + + DropDownType { + id: serverSelector + + signal severSelectorIndexChanged + property int currentIndex: 0 + + Layout.fillWidth: true + Layout.topMargin: 16 + + drawerHeight: 0.4375 + + descriptionText: qsTr("Server") + headerText: qsTr("Server") + + listView: ListViewWithRadioButtonType { + id: serverSelectorListView + + rootWidth: root.width + imageSource: "qrc:/images/controls/check.svg" + + model: SortFilterProxyModel { + id: proxyServersModel + sourceModel: ServersModel + filters: [ + ValueFilter { + roleName: "hasWriteAccess" + value: true + } + ] + } + + currentIndex: 0 + + clickedFunction: function() { + handler() + + if (serverSelector.currentIndex !== serverSelectorListView.currentIndex) { + serverSelector.currentIndex = serverSelectorListView.currentIndex + } + + shareConnectionDrawer.headerText = qsTr("Accessing ") + serverSelector.text + shareConnectionDrawer.configContentHeaderText = qsTr("File with accessing settings to ") + serverSelector.text + serverSelector.menuVisible = false + } + + Component.onCompleted: { + handler() + } + + function handler() { + serverSelector.text = selectedText + ServersModel.currentlyProcessedIndex = proxyServersModel.mapToSource(currentIndex) + } + } + } + + BasicButtonType { + Layout.fillWidth: true + Layout.topMargin: 40 + + text: qsTr("Share") + imageSource: "qrc:/images/controls/share-2.svg" + + onClicked: function() { + shareConnectionDrawer.headerText = qsTr("Connection to ") + serverSelector.text + shareConnectionDrawer.configContentHeaderText = qsTr("File with connection settings to ") + serverSelector.text + + shareConnectionDrawer.needCloseButton = false + + shareConnectionDrawer.open() + shareConnectionDrawer.contentVisible = false + PageController.showBusyIndicator(true) + + if (Qt.platform.os === "android") { + ExportController.generateFullAccessConfigAndroid(); + } else { + ExportController.generateFullAccessConfig(); + } + + PageController.showBusyIndicator(false) + + shareConnectionDrawer.needCloseButton = true + PageController.showTopCloseButton(true) + + shareConnectionDrawer.contentVisible = true + } + } + + ShareConnectionDrawer { + id: shareConnectionDrawer + } + } + } +} diff --git a/client/ui/qml/Pages2/PageStart.qml b/client/ui/qml/Pages2/PageStart.qml index ab02ace4..3afdf73a 100644 --- a/client/ui/qml/Pages2/PageStart.qml +++ b/client/ui/qml/Pages2/PageStart.qml @@ -82,6 +82,7 @@ PageType { target: InstallController function onInstallationErrorOccurred(errorMessage) { + PageController.showBusyIndicator(false) PageController.showErrorMessage(errorMessage) var needCloseCurrentPage = false @@ -99,6 +100,7 @@ PageType { function onUpdateContainerFinished(message) { PageController.showNotificationMessage(message) + PageController.closePage() } } @@ -107,6 +109,7 @@ PageType { function onReconnectWithUpdatedContainer(message) { PageController.showNotificationMessage(message) + PageController.closePage() } } diff --git a/client/vpnconnection.cpp b/client/vpnconnection.cpp index 1cf3a7e0..1f160552 100644 --- a/client/vpnconnection.cpp +++ b/client/vpnconnection.cpp @@ -10,7 +10,7 @@ #include #include #include -#include +#include "core/controllers/serverController.h" #ifdef AMNEZIA_DESKTOP #include "core/ipcclient.h" @@ -227,7 +227,8 @@ QString VpnConnection::createVpnConfigurationForProto(int serverIndex, const Ser configData = lastVpnConfig.value(proto); configData = m_configurator->processConfigWithLocalSettings(serverIndex, container, proto, configData); } else { - configData = m_configurator->genVpnProtocolConfig(credentials, container, containerConfig, proto, errorCode); + QString clientId; + configData = m_configurator->genVpnProtocolConfig(credentials, container, containerConfig, proto, clientId, errorCode); if (errorCode && *errorCode) { return ""; @@ -244,6 +245,8 @@ QString VpnConnection::createVpnConfigurationForProto(int serverIndex, const Ser protoObject.insert(config_key::last_config, configDataBeforeLocalProcessing); m_settings->setProtocolConfig(serverIndex, container, proto, protoObject); } + + emit m_configurator->newVpnConfigCreated(clientId, "unnamed client", container, credentials); } return configData; @@ -258,9 +261,7 @@ QJsonObject VpnConnection::createVpnConfiguration(int serverIndex, const ServerC for (ProtocolEnumNS::Proto proto : ContainerProps::protocolsForContainer(container)) { QJsonObject vpnConfigData = QJsonDocument::fromJson(createVpnConfigurationForProto(serverIndex, credentials, container, - containerConfig, proto, errorCode) - .toUtf8()) - .object(); + containerConfig, proto, errorCode).toUtf8()).object(); if (errorCode && *errorCode) { return {}; @@ -323,7 +324,7 @@ void VpnConnection::connectToVpn(int serverIndex, const ServerCredentials &crede ErrorCode e = ErrorCode::NoError; m_vpnConfiguration = createVpnConfiguration(serverIndex, credentials, container, containerConfig, &e); - emit newVpnConfigurationCreated(); + if (e) { emit connectionStateChanged(Vpn::ConnectionState::Error); return; diff --git a/client/vpnconnection.h b/client/vpnconnection.h index fbdb4e82..3cc670d5 100644 --- a/client/vpnconnection.h +++ b/client/vpnconnection.h @@ -79,8 +79,6 @@ signals: void serviceIsNotReady(); - void newVpnConfigurationCreated(); - protected slots: void onBytesChanged(quint64 receivedBytes, quint64 sentBytes); void onConnectionStateChanged(Vpn::ConnectionState state); diff --git a/deploy/build_macos.sh b/deploy/build_macos.sh index 13214d6d..0d64a9ec 100755 --- a/deploy/build_macos.sh +++ b/deploy/build_macos.sh @@ -146,7 +146,7 @@ if [ "${MAC_CERT_PW+x}" ]; then fi echo "Building DMG installer..." -hdiutil create -volname AmneziaVPN -srcfolder $BUILD_DIR/installer/$APP_NAME.app -ov -format UDZO $DMG_FILENAME +hdiutil create -size 120mb -volname AmneziaVPN -srcfolder $BUILD_DIR/installer/$APP_NAME.app -ov -format UDZO $DMG_FILENAME if [ "${MAC_CERT_PW+x}" ]; then echo "Signing DMG installer..." diff --git a/deploy/installer/config/controlscript.js b/deploy/installer/config/controlscript.js index 39404530..d0c82636 100644 --- a/deploy/installer/config/controlscript.js +++ b/deploy/installer/config/controlscript.js @@ -76,9 +76,7 @@ function raiseInstallerWindow() function appProcessIsRunning() { if (runningOnWindows()) { - var cmdArgs = ["/FI", "WINDOWTITLE eq " + appName()]; - var result = installer.execute("tasklist", cmdArgs); - + var result = installer.execute("tasklist"); if ( Number(result[1]) === 0 ) { if (result[0].indexOf(appExecutableFileName()) !== -1) { return true;