Compare commits

..

12 Commits

Author SHA1 Message Date
NickVs2015
c5979faba0 fix: translation 2026-05-30 13:57:21 +03:00
NickVs2015
303cd58354 fix: replace ServersModel.getProcessedServerData with ServersUiController API in backup/restore pages 2026-05-30 13:57:21 +03:00
NickVs2015
ffe07ff860 fix: adapt ServersBackupController to new server ID-based API 2026-05-30 13:57:21 +03:00
NickVs2015
471842b363 fix: add translations self-hosted backup 2026-05-30 13:57:21 +03:00
NickVs2015
40abac8725 - SshSession::resetConnection(): force-disconnect before each new top-level
backup operation so ssh_scp_new() doesn't fail on a reused stale session
  (reproduces as 'Failed to open channel for scp' right after restore)
- Call resetConnection() at entry of createBackup() and uploadBackup()
- Replace recursive findStackView() in PageSetupWizardEasy with upward
  parent traversal (depth+push check) to avoid JS stack overflow on iOS
  when component tree is large (71 VPN managers in test)
2026-05-30 13:57:21 +03:00
NickVs2015
23b7c26609 fix: adapt ServersBackupController and wizard restore to new codebase after rebase
- Fix includes in ServersBackupController (remove core/defs.h, containers_defs.h;
  add core/utils/containerEnum.h, commonStructs.h, errorCodes.h)
- Inject ServersUiController and ServersController into ServersBackupController
  to replace removed ServersModel API (getProcessedServerIndex, getServerCredentials,
  setDefaultServerIndex, getServerConfig, setDefaultContainer)
- Replace ContainerProps::containerFromString/toString with ContainerUtils equivalents
- Fix SCP permission denied: upload to /tmp first, then sudo mv to backup dir
- Fix double restore: use m_autoRestoreAfterUpload=true always (C++ handles both flows);
  remove redundant onBackupUploaded handler from PageSettingsServerData
- Remove duplicate ServersBackupController Connections from PageSettingsServerData
  (now exclusively handled by PageSettingsServerBackup + PageSettingsServerRestoreMode)
- Remove onBackupRestored from PageSettingsServerBackup (handled by PageSettingsServerRestoreMode)
- Fix wizard restore: remove non-existent setShouldCreateServer calls
- Fix wizard restore: use InstallController.getPortForInstall/defaultTransportProto
  instead of ProtocolProps; pass serverId as 4th arg to InstallController.install()
- Fix QML: replace ServersModel.getServerCredentials with
  ServersUiController.getProcessedServerCredentials()
2026-05-30 13:57:21 +03:00
NickVs2015
0ca5279e02 fix: adapt ServersBackupController to new codebase after rebase
- Remove deleted ServerController dependency; implement runHostScript,
  downloadFileFromHost, uploadFileToHostPublic directly using SshSession
  and libssh::Client which are the current SSH abstractions
- Change constructor from std::shared_ptr<Settings> to QPointer<SecureQSettings>
  to match CoreController's settings type
- Register PageSettingsServerBackup, PageSettingsServerBackupRestored,
  PageSettingsServerRestoreMode in qml.qrc (were in resources.qrc which
  was removed from the codebase)
2026-05-30 13:57:21 +03:00
NickVs2015
7112ab98c0 fix: ios compile 2026-05-30 13:57:21 +03:00
NickVs2015
6801c00aeb fix: add container copy optimization 2026-05-30 13:57:21 +03:00
NickVs2015
c83e748def fix: add install container and copy config 2026-05-30 13:57:21 +03:00
NickVs2015
2c85e99129 fix: restore configs 2026-05-30 13:57:21 +03:00
NickVs2015
637ea758d7 feat: add seft-hosted server backup 2026-05-30 13:57:21 +03:00
92 changed files with 3408 additions and 643 deletions

View File

@@ -157,7 +157,7 @@ jobs:
run: pip install "conan==2.28.0"
- name: 'Build dependencies'
run: cmake -S . -B build -DPREBUILTS_ONLY=1
run: cmake -S . -B build -G "Visual Studio 17 2022" -DPREBUILTS_ONLY=1
- name: 'Authorize in remote'
if: github.ref == 'refs/heads/dev'

View File

@@ -4,7 +4,7 @@ set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(PROJECT AmneziaVPN)
set(AMNEZIAVPN_VERSION 4.9.0.2)
set(AMNEZIAVPN_VERSION 4.9.0.1)
set(QT_CREATOR_SKIP_PACKAGE_MANAGER_SETUP ON CACHE BOOL "" FORCE)
set(CMAKE_PROJECT_TOP_LEVEL_INCLUDES
@@ -28,7 +28,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
set(RELEASE_DATE "${CURRENT_DATE}")
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
set(APP_ANDROID_VERSION_CODE 2123)
set(APP_ANDROID_VERSION_CODE 2122)
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(MZ_PLATFORM_NAME "linux")

View File

@@ -49,92 +49,14 @@ void ConnectionController::setConnectionState(Vpn::ConnectionState state)
}
}
ErrorCode ConnectionController::defaultContainerForServer(const QString &serverId, DockerContainer &container) const
{
const auto kind = m_serversRepository->serverKind(serverId);
switch (kind) {
case serverConfigUtils::ConfigType::SelfHostedAdmin: {
const auto cfg = m_serversRepository->selfHostedAdminConfig(serverId);
if (!cfg.has_value()) {
return ErrorCode::InternalError;
}
container = cfg->defaultContainer;
return ErrorCode::NoError;
}
case serverConfigUtils::ConfigType::SelfHostedUser: {
const auto cfg = m_serversRepository->selfHostedUserConfig(serverId);
if (!cfg.has_value()) {
return ErrorCode::InternalError;
}
container = cfg->defaultContainer;
return ErrorCode::NoError;
}
case serverConfigUtils::ConfigType::Native: {
const auto cfg = m_serversRepository->nativeConfig(serverId);
if (!cfg.has_value()) {
return ErrorCode::InternalError;
}
container = cfg->defaultContainer;
return ErrorCode::NoError;
}
case serverConfigUtils::ConfigType::AmneziaPremiumV2:
case serverConfigUtils::ConfigType::AmneziaFreeV3:
case serverConfigUtils::ConfigType::ExternalPremium: {
const auto cfg = m_serversRepository->apiV2Config(serverId);
if (!cfg.has_value()) {
return ErrorCode::InternalError;
}
container = cfg->defaultContainer;
return ErrorCode::NoError;
}
case serverConfigUtils::ConfigType::AmneziaPremiumV1:
case serverConfigUtils::ConfigType::AmneziaFreeV2:
return ErrorCode::LegacyApiV1NotSupportedError;
case serverConfigUtils::ConfigType::Invalid:
default:
return ErrorCode::InternalError;
}
}
ErrorCode ConnectionController::isConnectionSupported(const QString &serverId) const
{
if (serverId.isEmpty()) {
return ErrorCode::InternalError;
}
if (!isServiceReady()) {
return ErrorCode::AmneziaServiceNotRunning;
}
if (serverConfigUtils::isLegacyApiSubscription(m_serversRepository->serverKind(serverId))) {
return ErrorCode::LegacyApiV1NotSupportedError;
}
DockerContainer container = DockerContainer::None;
const ErrorCode errorCode = defaultContainerForServer(serverId, container);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
if (container == DockerContainer::None) {
return ErrorCode::NoInstalledContainersError;
}
if (ContainerUtils::isUnsupportedContainer(container)) {
return ErrorCode::LegacyContainerNotSupportedError;
}
if (!isContainerSupported(container)) {
return ErrorCode::NotSupportedOnThisPlatform;
}
return ErrorCode::NoError;
}
ErrorCode ConnectionController::prepareConnection(const QString &serverId,
QJsonObject& vpnConfiguration,
DockerContainer& container)
{
if (!isServiceReady()) {
return ErrorCode::AmneziaServiceNotRunning;
}
ContainerConfig containerConfigModel;
QPair<QString, QString> dns;
QString hostName;
@@ -198,6 +120,10 @@ ErrorCode ConnectionController::prepareConnection(const QString &serverId,
return ErrorCode::InternalError;
}
if (!isContainerSupported(container)) {
return ErrorCode::NotSupportedOnThisPlatform;
}
vpnConfiguration = createConnectionConfiguration(dns, isApiConfig, hostName, description, configVersion,
containerConfigModel, container);

View File

@@ -34,8 +34,6 @@ public:
QJsonObject& vpnConfiguration,
DockerContainer& container);
ErrorCode isConnectionSupported(const QString &serverId) const;
ErrorCode openConnection(const QString &serverId);
void closeConnection();
@@ -75,8 +73,6 @@ signals:
#endif
private:
ErrorCode defaultContainerForServer(const QString &serverId, DockerContainer &container) const;
SecureServersRepository* m_serversRepository;
SecureAppSettingsRepository* m_appSettingsRepository;
VpnConnection* m_vpnConnection;

View File

@@ -191,7 +191,7 @@ void CoreController::initControllers()
m_languageUiController = new LanguageUiController(m_settingsController, m_languageModel, this);
setQmlContextProperty("LanguageUiController", m_languageUiController);
m_settingsUiController = new SettingsUiController(m_settingsController, m_serversController, this);
m_settingsUiController = new SettingsUiController(m_settingsController, m_serversController, m_languageUiController, this);
setQmlContextProperty("SettingsController", m_settingsUiController);
m_pageController = new PageController(m_serversController, m_settingsController, this);
@@ -203,6 +203,9 @@ void CoreController::initControllers()
m_ipSplitTunnelingUiController = new IpSplitTunnelingUiController(m_ipSplitTunnelingController, m_ipSplitTunnelingModel, this);
setQmlContextProperty("IpSplitTunnelingController", m_ipSplitTunnelingUiController);
m_serversBackupController = new ServersBackupController(m_settings, m_serversModel, m_serversUiController, m_serversController);
setQmlContextProperty("ServersBackupController", m_serversBackupController);
m_allowedDnsUiController = new AllowedDnsUiController(m_allowedDnsController, m_allowedDnsModel, this);
setQmlContextProperty("AllowedDnsController", m_allowedDnsUiController);

View File

@@ -24,6 +24,7 @@
#include "ui/controllers/settingsUiController.h"
#include "ui/controllers/serversUiController.h"
#include "ui/controllers/ipSplitTunnelingUiController.h"
#include "ui/controllers/serversBackupController.h"
#include "ui/controllers/systemController.h"
#include "ui/controllers/languageUiController.h"
#include "ui/controllers/updateUiController.h"
@@ -174,6 +175,7 @@ private:
AllowedDnsUiController* m_allowedDnsUiController;
LanguageUiController* m_languageUiController;
UpdateUiController* m_updateUiController;
ServersBackupController* m_serversBackupController;
SubscriptionUiController* m_subscriptionUiController;
ApiNewsUiController* m_apiNewsUiController;

View File

@@ -33,6 +33,7 @@
#include "core/controllers/connectionController.h"
#include "ui/models/clientManagementModel.h"
#include "ui/controllers/api/apiNewsUiController.h"
#include "ui/models/api/apiCountryModel.h"
#include "ui/models/containersModel.h"
#include "core/utils/containerEnum.h"
@@ -155,17 +156,15 @@ void CoreSignalHandlers::initExportControllerHandler()
void CoreSignalHandlers::initImportControllerHandler()
{
connect(m_coreController->m_importCoreController, &ImportController::importFinished, this, [this]() {
if (m_coreController->m_connectionUiController->isConnected()) {
return;
}
const int newServerIndex = m_coreController->m_serversController->getServersCount() - 1;
const QString serverId = m_coreController->m_serversController->getServerId(newServerIndex);
if (!serverId.isEmpty()) {
m_coreController->m_serversController->setDefaultServer(serverId);
}
if (m_coreController->m_serversUiController) {
m_coreController->m_serversUiController->setProcessedServerId(serverId);
if (!m_coreController->m_connectionController->isConnected()) {
int newServerIndex = m_coreController->m_serversController->getServersCount() - 1;
const QString serverId = m_coreController->m_serversController->getServerId(newServerIndex);
if (!serverId.isEmpty()) {
m_coreController->m_serversController->setDefaultServer(serverId);
}
if (m_coreController->m_serversUiController) {
m_coreController->m_serversUiController->setProcessedServerId(serverId);
}
}
});
}
@@ -177,14 +176,17 @@ void CoreSignalHandlers::initApiCountryModelUpdateHandler()
if (processedServerId.isEmpty()) {
return;
}
QJsonArray availableCountries;
QString serverCountryCode;
const auto apiV2 = m_coreController->m_serversRepository->apiV2Config(processedServerId);
if (!apiV2.has_value()) {
return;
if (apiV2.has_value()) {
availableCountries = apiV2->apiConfig.availableCountries;
serverCountryCode = apiV2->apiConfig.serverCountryCode;
}
m_coreController->m_apiCountryModel->updateModel(apiV2->apiConfig.availableCountries,
apiV2->apiConfig.serverCountryCode);
m_coreController->m_apiCountryModel->updateModel(availableCountries, serverCountryCode);
});
}
@@ -235,16 +237,13 @@ void CoreSignalHandlers::initLanguageHandler()
connect(m_coreController->m_settingsUiController, &SettingsUiController::resetLanguageToSystem, m_coreController->m_languageUiController, [this]() {
m_coreController->m_languageUiController->changeLanguage(m_coreController->m_languageUiController->getSystemLanguageEnum());
});
connect(m_coreController->m_settingsUiController, &SettingsUiController::appLanguageChanged, m_coreController->m_languageUiController, [this]() {
m_coreController->m_languageUiController->onAppLanguageChanged(m_coreController->m_settingsController->getAppLanguage());
});
}
void CoreSignalHandlers::initAutoConnectHandler()
{
if (m_coreController->m_settingsUiController->isAutoConnectEnabled()
&& !m_coreController->m_serversController->getDefaultServerId().isEmpty()) {
QTimer::singleShot(1000, this, [this]() { m_coreController->m_connectionUiController->toggleConnection(); });
QTimer::singleShot(1000, this, [this]() { m_coreController->m_connectionUiController->openConnection(); });
}
}
@@ -349,9 +348,6 @@ void CoreSignalHandlers::initUnsupportedConnectDrawerHandler()
{
connect(m_coreController->m_subscriptionUiController, &SubscriptionUiController::unsupportedConnectDrawerRequested,
m_coreController->m_pageController, &PageController::unsupportedConnectDrawerRequested);
connect(m_coreController->m_connectionUiController, &ConnectionUiController::unsupportedConnectDrawerRequested,
m_coreController->m_pageController, &PageController::unsupportedConnectDrawerRequested);
}
void CoreSignalHandlers::initStrictKillSwitchHandler()

View File

@@ -72,16 +72,6 @@ namespace
}
return false;
}
QString buildRemoveContainerScript(const amnezia::ScriptVars &vars, bool removeDataVolume)
{
QString script = SshSession::replaceVars(amnezia::scriptData(SharedScriptType::remove_container), vars);
if (removeDataVolume) {
script += QLatin1String("\nsudo docker volume rm -f $CONTAINER_NAME-data 2>/dev/null || true");
script = SshSession::replaceVars(script, vars);
}
return script;
}
}
InstallController::InstallController(SecureServersRepository *serversRepository,
@@ -130,10 +120,14 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
return e;
qDebug().noquote() << "InstallController::setupContainer prepareHostWorker finished";
const amnezia::ScriptVars removeContainerVars =
amnezia::ScriptVars removeContainerVars =
amnezia::genBaseVars(credentials, container, QString(), QString());
const bool removeDataVolume = !isUpdate && (container == DockerContainer::MtProxy || container == DockerContainer::Telemt);
sshSession.runScript(credentials, buildRemoveContainerScript(removeContainerVars, removeDataVolume));
if (!isUpdate) {
removeContainerVars.append({ { "$REMOVE_CONTAINER_DATA", QStringLiteral("1") } });
}
sshSession.runScript(credentials,
sshSession.replaceVars(amnezia::scriptData(SharedScriptType::remove_container),
removeContainerVars));
qDebug().noquote() << "InstallController::setupContainer removeContainer finished";
qDebug().noquote() << "buildContainerWorker start";
@@ -158,8 +152,8 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
return startupContainerWorker(credentials, container, config, sshSession);
}
ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerContainer container, const ContainerConfig &oldConfig,
ContainerConfig &newConfig)
ErrorCode InstallController::updateContainer(const QString &serverId, DockerContainer container, const ContainerConfig &oldConfig,
ContainerConfig &newConfig)
{
if (!isUpdateDockerContainerRequired(container, oldConfig, newConfig)) {
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
@@ -191,7 +185,7 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
SshSession sshSession(this);
bool reinstallRequired = isReinstallContainerRequired(container, oldConfig, newConfig);
qDebug() << "InstallController::updateServerConfig for container" << container << "reinstall required is" << reinstallRequired;
qDebug() << "InstallController::updateContainer for container" << container << "reinstall required is" << reinstallRequired;
bool xrayServerSettingsChanged = false;
if (container == DockerContainer::Xray || container == DockerContainer::SSXray) {
@@ -219,11 +213,11 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
if (errorCode == ErrorCode::NoError && xrayServerSettingsChanged && !skipXrayInboundSync) {
DnsSettings dnsSettings = { m_appSettingsRepository->primaryDns(), m_appSettingsRepository->secondaryDns() };
XrayConfigurator xrayConfigurator(&sshSession);
qDebug() << "InstallController::updateServerConfig applying Xray server inbound sync, reinstall="
qDebug() << "InstallController::updateContainer applying Xray server inbound sync, reinstall="
<< reinstallRequired;
errorCode = xrayConfigurator.applyServerSettingsToRemote(credentials, container, newConfig, dnsSettings, false);
if (errorCode != ErrorCode::NoError) {
qDebug() << "InstallController::updateServerConfig Xray inbound sync failed, error="
qDebug() << "InstallController::updateContainer Xray inbound sync failed, error="
<< static_cast<int>(errorCode);
}
}
@@ -242,41 +236,6 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
return errorCode;
}
ErrorCode InstallController::updateClientConfig(const QString &serverId, DockerContainer container, ContainerConfig &newConfig)
{
switch (m_serversRepository->serverKind(serverId)) {
case serverConfigUtils::ConfigType::SelfHostedAdmin: {
auto config = m_serversRepository->selfHostedAdminConfig(serverId);
if (!config.has_value()) {
return ErrorCode::InternalError;
}
config->updateContainerConfig(container, newConfig);
m_serversRepository->editServer(serverId, config->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
return ErrorCode::NoError;
}
case serverConfigUtils::ConfigType::SelfHostedUser: {
auto config = m_serversRepository->selfHostedUserConfig(serverId);
if (!config.has_value()) {
return ErrorCode::InternalError;
}
config->updateContainerConfig(container, newConfig);
m_serversRepository->editServer(serverId, config->toJson(), serverConfigUtils::ConfigType::SelfHostedUser);
return ErrorCode::NoError;
}
case serverConfigUtils::ConfigType::Native: {
auto config = m_serversRepository->nativeConfig(serverId);
if (!config.has_value()) {
return ErrorCode::InternalError;
}
config->updateContainerConfig(container, newConfig);
m_serversRepository->editServer(serverId, config->toJson(), serverConfigUtils::ConfigType::Native);
return ErrorCode::NoError;
}
default:
return ErrorCode::InternalError;
}
}
void InstallController::clearCachedProfile(const QString &serverId, DockerContainer container)
{
if (ContainerUtils::containerService(container) == ServiceType::Other) {
@@ -836,8 +795,8 @@ ErrorCode InstallController::installDockerWorker(const ServerCredentials &creden
qDebug().noquote() << "InstallController::installDockerWorker" << stdOut;
if (container == DockerContainer::Awg2) {
QRegularExpression kernelVersionRegex(R"(Linux\s+(\d+)\.(\d+)[^\d]*)");
QRegularExpressionMatch match = kernelVersionRegex.match(stdOut);
QRegularExpression regex(R"(Linux\s+(\d+)\.(\d+)[^\d]*)");
QRegularExpressionMatch match = regex.match(stdOut);
if (match.hasMatch()) {
int majorVersion = match.captured(1).toInt();
int minorVersion = match.captured(2).toInt();
@@ -850,19 +809,8 @@ ErrorCode InstallController::installDockerWorker(const ServerCredentials &creden
if (stdOut.contains("lock"))
return ErrorCode::ServerPacketManagerError;
if (stdOut.contains("Container runtime is not supported"))
return ErrorCode::ServerContainerRuntimeNotSupported;
QRegularExpression notFoundRegex(
R"(^.*(?:sudo:|docker:).*not found.*$)",
QRegularExpression::MultilineOption);
if (notFoundRegex.match(stdOut).hasMatch()) {
if (stdOut.contains("command not found"))
return ErrorCode::ServerDockerFailedError;
}
if (stdOut.contains("Container runtime service not running"))
return ErrorCode::ContainerRuntimeServiceNotRunning;
return error;
}
@@ -899,7 +847,7 @@ ErrorCode InstallController::isUserInSudo(const ServerCredentials &credentials,
return ErrorCode::ServerUserNotInSudo;
if (stdOut.contains("can't cd to") || stdOut.contains("Permission denied") || stdOut.contains("No such file or directory"))
return ErrorCode::ServerUserDirectoryNotAccessible;
if (stdOut.contains(QRegularExpression(R"(\bsudoers\b)")) || stdOut.contains("is not allowed to") || stdOut.contains("can't do that"))
if (stdOut.contains("sudoers") || stdOut.contains("is not allowed to run sudo on"))
return ErrorCode::ServerUserNotAllowedInSudoers;
if (stdOut.contains("password is required") || stdOut.contains("authentication is required"))
return ErrorCode::ServerUserPasswordRequired;
@@ -1032,11 +980,12 @@ ErrorCode InstallController::removeContainer(const QString &serverId, DockerCont
return ErrorCode::InternalError;
}
SshSession sshSession(this);
const amnezia::ScriptVars removeContainerVars =
amnezia::ScriptVars removeContainerVars =
amnezia::genBaseVars(credentials, container, QString(), QString());
const bool removeDataVolume = (container == DockerContainer::MtProxy || container == DockerContainer::Telemt);
ErrorCode errorCode =
sshSession.runScript(credentials, buildRemoveContainerScript(removeContainerVars, removeDataVolume));
removeContainerVars.append({ { "$REMOVE_CONTAINER_DATA", QStringLiteral("1") } });
ErrorCode errorCode = sshSession.runScript(
credentials,
sshSession.replaceVars(amnezia::scriptData(SharedScriptType::remove_container), removeContainerVars));
if (errorCode == ErrorCode::NoError) {
QMap<DockerContainer, ContainerConfig> containers = adminConfig->containers;
@@ -1514,7 +1463,7 @@ ErrorCode InstallController::getAlreadyInstalledContainers(const ServerCredentia
QString transportProtoStr = containerAndPortMatch.captured(3);
DockerContainer container = ContainerUtils::containerFromString(name);
if (container == DockerContainer::None || ContainerUtils::isUnsupportedContainer(container)) {
if (container == DockerContainer::None) {
continue;
}
@@ -1539,7 +1488,7 @@ ErrorCode InstallController::getAlreadyInstalledContainers(const ServerCredentia
QString transportProtoStr = torOrDnsRegMatch.captured(3);
DockerContainer container = ContainerUtils::containerFromString(name);
if (container == DockerContainer::None || ContainerUtils::isUnsupportedContainer(container)) {
if (container == DockerContainer::None) {
continue;
}

View File

@@ -34,12 +34,7 @@ public:
~InstallController();
ErrorCode setupContainer(const ServerCredentials &credentials, DockerContainer container, ContainerConfig &config, bool isUpdate = false);
// Updates server-side container settings (admin self-hosted only): reconfigures the container over SSH.
ErrorCode updateServerConfig(const QString &serverId, DockerContainer container, const ContainerConfig &oldConfig, ContainerConfig &newConfig);
// Updates client-local settings only: rewrites the stored container config for any self-hosted/native server. No SSH.
ErrorCode updateClientConfig(const QString &serverId, DockerContainer container, ContainerConfig &newConfig);
ErrorCode updateContainer(const QString &serverId, DockerContainer container, const ContainerConfig &oldConfig, ContainerConfig &newConfig);
ErrorCode rebootServer(const QString &serverId);
ErrorCode removeAllContainers(const QString &serverId);

View File

@@ -29,11 +29,6 @@ ContainerConfig NativeServerConfig::containerConfig(DockerContainer container) c
return containers.value(container);
}
void NativeServerConfig::updateContainerConfig(DockerContainer container, const ContainerConfig &config)
{
containers[container] = config;
}
QPair<QString, QString> NativeServerConfig::getDnsPair(const QString &primaryDns, const QString &secondaryDns) const
{
QString d1 = dns1;

View File

@@ -27,8 +27,6 @@ struct NativeServerConfig {
bool hasContainers() const;
ContainerConfig containerConfig(DockerContainer container) const;
void updateContainerConfig(DockerContainer container, const ContainerConfig &config);
QPair<QString, QString> getDnsPair(const QString &primaryDns, const QString &secondaryDns) const;
QJsonObject toJson() const;

View File

@@ -43,11 +43,6 @@ ContainerConfig SelfHostedUserServerConfig::containerConfig(DockerContainer cont
return containers.value(container);
}
void SelfHostedUserServerConfig::updateContainerConfig(DockerContainer container, const ContainerConfig &config)
{
containers[container] = config;
}
QPair<QString, QString> SelfHostedUserServerConfig::getDnsPair(const QString &primaryDns,
const QString &secondaryDns) const
{

View File

@@ -32,8 +32,6 @@ struct SelfHostedUserServerConfig {
bool hasContainers() const;
ContainerConfig containerConfig(DockerContainer container) const;
void updateContainerConfig(DockerContainer container, const ContainerConfig &config);
QPair<QString, QString> getDnsPair(const QString &primaryDns, const QString &secondaryDns) const;
QJsonObject toJson() const;

View File

@@ -39,44 +39,33 @@ QString OpenVpnProtocol::defaultConfigPath()
return p;
}
void OpenVpnProtocol::cleanupResources()
void OpenVpnProtocol::stop()
{
if (m_openVpnProcess || openVpnProcessIsRunning()) {
qDebug() << "OpenVpnProtocol::stop()";
setConnectionState(Vpn::ConnectionState::Disconnecting);
// TODO: need refactoring
// sendTermSignal() will even return true while server connected ???
if ((m_connectionState == Vpn::ConnectionState::Preparing) || (m_connectionState == Vpn::ConnectionState::Connecting)
|| (m_connectionState == Vpn::ConnectionState::Connected)
|| (m_connectionState == Vpn::ConnectionState::Reconnecting)) {
if (!sendTermSignal()) {
killOpenVpnProcess();
}
QThread::msleep(10);
m_managementServer.stop();
}
m_managementServer.stop();
#if defined(Q_OS_WIN) || defined(Q_OS_LINUX) || defined(Q_OS_MACOS)
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
QRemoteObjectPendingReply<bool> reply = iface->disableKillSwitch();
if (!reply.waitForFinished(1000) && !reply.returnValue()) {
qWarning() << "OpenVpnProtocol::cleanupResources(): Failed to disable killswitch";
qWarning() << "OpenVpnProtocol::stop(): Failed to disable killswitch";
}
});
#endif
}
void OpenVpnProtocol::stop()
{
qDebug() << "OpenVpnProtocol::stop()";
const bool wasActive = m_connectionState == Vpn::ConnectionState::Preparing
|| m_connectionState == Vpn::ConnectionState::Connecting
|| m_connectionState == Vpn::ConnectionState::Connected
|| m_connectionState == Vpn::ConnectionState::Reconnecting;
if (wasActive) {
setConnectionState(Vpn::ConnectionState::Disconnecting);
}
cleanupResources();
if (wasActive || m_connectionState == Vpn::ConnectionState::Disconnecting) {
setConnectionState(Vpn::ConnectionState::Disconnected);
}
setConnectionState(Vpn::ConnectionState::Disconnected);
}
ErrorCode OpenVpnProtocol::prepare()
@@ -179,7 +168,7 @@ void OpenVpnProtocol::updateRouteGateway(QString line)
ErrorCode OpenVpnProtocol::start()
{
cleanupResources();
OpenVpnProtocol::stop();
if (!QFileInfo::exists(configPath())) {
setLastError(ErrorCode::OpenVpnConfigMissing);

View File

@@ -29,7 +29,6 @@ protected slots:
void onReadyReadDataFromManagementServer();
private:
void cleanupResources();
QString configPath() const;
bool openVpnProcessIsRunning() const;
bool sendTermSignal();

View File

@@ -15,8 +15,6 @@ namespace amnezia
Awg2,
WireGuard,
OpenVpn,
Cloak,
ShadowSocks,
Ipsec,
Xray,
SSXray,

View File

@@ -21,8 +21,6 @@ QString ContainerUtils::containerToString(DockerContainer c)
{
if (c == DockerContainer::None)
return "none";
if (c == DockerContainer::Cloak)
return "amnezia-openvpn-cloak";
if (c == DockerContainer::Awg)
return "amnezia-awg";
if (c == DockerContainer::Awg2)
@@ -64,8 +62,6 @@ QMap<DockerContainer, QString> ContainerUtils::containerHumanNames()
{
return { { DockerContainer::None, "Not installed" },
{ DockerContainer::OpenVpn, "OpenVPN" },
{ DockerContainer::ShadowSocks, "OpenVPN over SS" },
{ DockerContainer::Cloak, "OpenVPN over Cloak" },
{ DockerContainer::WireGuard, "WireGuard" },
{ DockerContainer::Awg, "AmneziaWG" },
{ DockerContainer::Awg2, "AmneziaWG" },
@@ -87,10 +83,6 @@ QMap<DockerContainer, QString> ContainerUtils::containerDescriptions()
return { { DockerContainer::OpenVpn,
QObject::tr("OpenVPN is the most popular VPN protocol, with flexible configuration options. It uses its "
"own security protocol with SSL/TLS for key exchange.") },
{ DockerContainer::ShadowSocks,
QObject::tr("This protocol is no longer supported.") },
{ DockerContainer::Cloak,
QObject::tr("This protocol is no longer supported.") },
{ DockerContainer::WireGuard,
QObject::tr("WireGuard - popular VPN protocol with high performance, high speed and low power "
"consumption.") },
@@ -202,9 +194,6 @@ QMap<DockerContainer, QString> ContainerUtils::containerDetailedDescriptions()
ServiceType ContainerUtils::containerService(DockerContainer c)
{
if (isUnsupportedContainer(c)) {
return ServiceType::Vpn;
}
return ProtocolUtils::protocolService(defaultProtocol(c));
}
@@ -213,8 +202,6 @@ Proto ContainerUtils::defaultProtocol(DockerContainer c)
switch (c) {
case DockerContainer::None: return Proto::Unknown;
case DockerContainer::OpenVpn: return Proto::OpenVpn;
case DockerContainer::Cloak:
case DockerContainer::ShadowSocks: return Proto::Unknown;
case DockerContainer::WireGuard: return Proto::WireGuard;
case DockerContainer::Awg2: return Proto::Awg;
case DockerContainer::Awg: return Proto::Awg;
@@ -265,8 +252,6 @@ bool ContainerUtils::isSupportedByCurrentPlatform(DockerContainer c)
// macOS build using Network Extension allow OpenVPN for parity with iOS.
switch (c) {
case DockerContainer::OpenVpn: return true;
case DockerContainer::Cloak: return false;
case DockerContainer::ShadowSocks: return false;
case DockerContainer::WireGuard: return true;
case DockerContainer::Awg2: return true;
case DockerContainer::Awg: return true;
@@ -351,10 +336,6 @@ int ContainerUtils::easySetupOrder(DockerContainer container)
bool ContainerUtils::isShareable(DockerContainer container)
{
if (isUnsupportedContainer(container)) {
return false;
}
switch (container) {
case DockerContainer::TorWebSite: return false;
case DockerContainer::Dns: return false;
@@ -371,11 +352,6 @@ bool ContainerUtils::isAwgContainer(DockerContainer container)
return container == DockerContainer::Awg || container == DockerContainer::Awg2;
}
bool ContainerUtils::isUnsupportedContainer(DockerContainer container)
{
return container == DockerContainer::Cloak || container == DockerContainer::ShadowSocks;
}
QJsonObject ContainerUtils::getProtocolConfigFromContainer(const Proto protocol, const QJsonObject &containerConfig)
{
QString protocolConfigString = containerConfig.value(ProtocolUtils::protoToString(protocol))

View File

@@ -45,8 +45,6 @@ namespace amnezia
bool isAwgContainer(DockerContainer container);
bool isUnsupportedContainer(DockerContainer container);
QJsonObject getProtocolConfigFromContainer(const Proto protocol, const QJsonObject &containerConfig);
int installPageOrder(DockerContainer container);

View File

@@ -38,8 +38,6 @@ namespace amnezia
XrayServerConfigInvalid = 215,
XrayServerNoVlessClients = 216,
XrayRealityKeysReadFailed = 217,
ServerContainerRuntimeNotSupported = 218,
ContainerRuntimeServiceNotRunning = 219,
// Ssh connection errors
SshRequestDeniedError = 300,
@@ -81,7 +79,6 @@ namespace amnezia
ImportBackupFileUseRestoreInstead = 903,
RestoreBackupInvalidError = 904,
LegacyApiV1NotSupportedError = 905,
LegacyContainerNotSupportedError = 906,
// Android errors
AndroidError = 1000,
@@ -126,3 +123,5 @@ namespace amnezia
Q_DECLARE_METATYPE(amnezia::ErrorCode)
#endif // ERRORCODES_H

View File

@@ -39,8 +39,6 @@ QString errorString(ErrorCode code) {
case(ErrorCode::XrayRealityKeysReadFailed):
errorMessage = QObject::tr("Server error: failed to read XRay Reality keys from the server");
break;
case(ErrorCode::ServerContainerRuntimeNotSupported): errorMessage = QObject::tr("Server error: The default container runtime available for installation on this server is not supported.\n Install Docker Engine on the server manually and try again."); break;
case(ErrorCode::ContainerRuntimeServiceNotRunning): errorMessage = QObject::tr("Container runtime error: The container runtime service is not running.\n Check the container runtime service on the server, or wait about a minute and try again."); break;
// Libssh errors
case(ErrorCode::SshRequestDeniedError): errorMessage = QObject::tr("SSH request was denied"); break;
@@ -71,7 +69,6 @@ QString errorString(ErrorCode code) {
case (ErrorCode::ImportBackupFileUseRestoreInstead): errorMessage = QObject::tr("Backup files cannot be imported here. Use 'Restore from backup' instead."); break;
case (ErrorCode::RestoreBackupInvalidError): errorMessage = QObject::tr("Backup file is corrupted or has invalid format"); break;
case (ErrorCode::LegacyApiV1NotSupportedError): errorMessage = QObject::tr("This legacy Amnezia subscription format is no longer supported"); break;
case (ErrorCode::LegacyContainerNotSupportedError): errorMessage = QObject::tr("This protocol is no longer supported. Please select another protocol or remove this container from the server settings."); break;
case (ErrorCode::ImportOpenConfigError): errorMessage = QObject::tr("Unable to open config file"); break;
case (ErrorCode::NoInstalledContainersError): errorMessage = QObject::tr("VPN Protocols is not installed.\n Please install VPN container at first"); break;

View File

@@ -290,6 +290,86 @@ namespace libssh {
return watcher.result();
}
ErrorCode Client::scpFileDownload(const QString& remotePath, const QString& localPath)
{
// Use full path for SCP download
m_scpSession = ssh_scp_new(m_session, SSH_SCP_READ, remotePath.toStdString().c_str());
if (m_scpSession == nullptr) {
return fromLibsshErrorCode();
}
if (ssh_scp_init(m_scpSession) != SSH_OK) {
auto errorCode = fromLibsshErrorCode();
closeScpSession();
return errorCode;
}
QFutureWatcher<ErrorCode> watcher;
connect(&watcher, &QFutureWatcher<ErrorCode>::finished, this, &Client::scpFileDownloadFinished);
QFuture<ErrorCode> future = QtConcurrent::run([this, &remotePath, &localPath]() {
// Pull request - this gets file info
int result = ssh_scp_pull_request(m_scpSession);
if (result != SSH_SCP_REQUEST_NEWFILE) {
return fromLibsshErrorCode();
}
// Accept the request
ssh_scp_accept_request(m_scpSession);
// Get file size
int fileSize = ssh_scp_request_get_size(m_scpSession);
if (fileSize <= 0) {
return ErrorCode::InternalError;
}
// Open local file for writing
QFile fout(localPath);
if (!fout.open(QIODevice::WriteOnly)) {
return fromFileErrorCode(fout.error());
}
// Read file data in chunks
constexpr size_t bufferSize = 16384;
int transferred = 0;
while (transferred < fileSize) {
int chunkSize = qMin(bufferSize, static_cast<size_t>(fileSize - transferred));
QByteArray buffer(chunkSize, 0);
int bytesRead = ssh_scp_read(m_scpSession, buffer.data(), chunkSize);
if (bytesRead == SSH_ERROR) {
fout.close();
return fromLibsshErrorCode();
}
if (bytesRead != chunkSize) {
fout.close();
return ErrorCode::InternalError;
}
qint64 bytesWritten = fout.write(buffer);
if (bytesWritten != chunkSize) {
fout.close();
return fromFileErrorCode(fout.error());
}
transferred += bytesRead;
}
fout.close();
return ErrorCode::NoError;
});
watcher.setFuture(future);
QEventLoop wait;
QObject::connect(this, &Client::scpFileDownloadFinished, &wait, &QEventLoop::quit);
wait.exec();
closeScpSession();
return watcher.result();
}
void Client::closeScpSession()
{
if (m_scpSession != nullptr) {

View File

@@ -38,6 +38,8 @@ namespace libssh {
const QString &localPath,
const QString &remotePath,
const QString &fileDesc);
ErrorCode scpFileDownload(const QString &remotePath,
const QString &localPath);
ErrorCode getDecryptedPrivateKey(const ServerCredentials &credentials, QString &decryptedPrivateKey, const std::function<QString()> &passphraseCallback);
private:
ErrorCode closeChannel();
@@ -54,6 +56,7 @@ namespace libssh {
signals:
void writeToChannelFinished();
void scpFileCopyFinished();
void scpFileDownloadFinished();
};
}

View File

@@ -44,6 +44,11 @@ SshSession::~SshSession()
m_sshClient.disconnectFromHost();
}
void SshSession::resetConnection()
{
m_sshClient.disconnectFromHost();
}
ErrorCode SshSession::runScript(const ServerCredentials &credentials, QString script,
const std::function<ErrorCode(const QString &, libssh::Client &)> &cbReadStdOut,
const std::function<ErrorCode(const QString &, libssh::Client &)> &cbReadStdErr)

View File

@@ -47,6 +47,9 @@ public:
ErrorCode uploadFileToHost(const ServerCredentials &credentials, const QByteArray &data, const QString &remotePath,
libssh::ScpOverwriteMode overwriteMode = libssh::ScpOverwriteMode::ScpOverwriteExisting);
/** Force-close the current SSH connection so the next operation starts fresh. */
void resetConnection();
private:
libssh::Client m_sshClient;
};

View File

@@ -1,8 +1,7 @@
if which apt-get > /dev/null 2>&1 || command -v apt-get > /dev/null 2>&1; then LOCK_CMD="fuser"; LOCK_FILE="/var/lib/dpkg/lock-frontend";\
elif which dnf > /dev/null 2>&1 || command -v dnf > /dev/null 2>&1; then LOCK_CMD="fuser"; LOCK_FILE="/var/cache/dnf/* /var/run/dnf/* /var/lib/dnf/* /var/lib/rpm/*";\
elif which yum > /dev/null 2>&1 || command -v yum > /dev/null 2>&1; then LOCK_CMD="cat"; LOCK_FILE="/var/run/yum.pid";\
elif which zypper > /dev/null 2>&1 || command -v zypper > /dev/null 2>&1; then LOCK_CMD="cat"; LOCK_FILE="/var/run/zypp.pid";\
elif which pacman > /dev/null 2>&1 || command -v pacman > /dev/null 2>&1; then LOCK_CMD="fuser"; LOCK_FILE="/var/lib/pacman/db.lck";\
else echo "Packet manager not found"; echo "Internal error"; exit 1;\
fi;\
if sudo -n which $LOCK_CMD > /dev/null 2>&1 || command -v $LOCK_CMD > /dev/null 2>&1; then sudo -n $LOCK_CMD $LOCK_FILE 2>/dev/null; else echo "$LOCK_CMD not installed"; fi
if which apt-get > /dev/null 2>&1; then LOCK_CMD="fuser"; LOCK_FILE="/var/lib/dpkg/lock-frontend";\
elif which dnf > /dev/null 2>&1; then LOCK_CMD="fuser"; LOCK_FILE="/var/cache/dnf/* /var/run/dnf/* /var/lib/dnf/* /var/lib/rpm/*";\
elif which yum > /dev/null 2>&1; then LOCK_CMD="cat"; LOCK_FILE="/var/run/yum.pid";\
elif which zypper > /dev/null 2>&1; then LOCK_CMD="cat"; LOCK_FILE="/var/run/zypp.pid";\
elif which pacman > /dev/null 2>&1; then LOCK_CMD="fuser"; LOCK_FILE="/var/lib/pacman/db.lck";\
else echo "Packet manager not found"; echo "Internal error"; exit 1; fi;\
if command -v $LOCK_CMD > /dev/null 2>&1; then sudo $LOCK_CMD $LOCK_FILE 2>/dev/null; else echo "$LOCK_CMD not installed"; fi

View File

@@ -1,8 +1,8 @@
if pm=$(which apt-get 2>/dev/null || command -v apt-get 2>/dev/null); then opt="--version";\
elif pm=$(which dnf 2>/dev/null || command -v dnf 2>/dev/null); then opt="--version";\
elif pm=$(which yum 2>/dev/null || command -v yum 2>/dev/null); then opt="--version";\
elif pm=$(which zypper 2>/dev/null || command -v zypper 2>/dev/null); then opt="--version";\
elif pm=$(which pacman 2>/dev/null || command -v pacman 2>/dev/null); then opt="--version";\
if which apt-get > /dev/null 2>&1; then pm=$(which apt-get); opt="--version";\
elif which dnf > /dev/null 2>&1; then pm=$(which dnf); opt="--version";\
elif which yum > /dev/null 2>&1; then pm=$(which yum); opt="--version";\
elif which zypper > /dev/null 2>&1; then pm=$(which zypper); opt="--version";\
elif which pacman > /dev/null 2>&1; then pm=$(which pacman); opt="--version";\
else pm="uname"; opt="-a";\
fi;\
CUR_USER=$(whoami 2>/dev/null || echo $HOME | sed 's/.*\///');\

View File

@@ -1,34 +1,25 @@
if pm=$(which apt-get 2>/dev/null || command -v apt-get 2>/dev/null); then silent_inst="-yq install --install-recommends"; what_pkg="-s install"; check_pkgs="-yq update"; docker_pkg="docker.io"; dist="debian";\
elif pm=$(which dnf 2>/dev/null || command -v dnf 2>/dev/null); then silent_inst="-yq install"; what_pkg="--assumeno install --setopt=tsflags=test"; check_pkgs="-yq check-update"; docker_pkg="docker"; dist="fedora";\
elif pm=$(which yum 2>/dev/null || command -v yum 2>/dev/null); then silent_inst="-y -q install"; what_pkg="--assumeno install --setopt=tsflags=test"; check_pkgs="-y -q check-update"; docker_pkg="docker"; dist="centos";\
elif pm=$(which zypper 2>/dev/null || command -v zypper 2>/dev/null); then silent_inst="-nq install"; what_pkg="--dry-run install"; check_pkgs="-nq refresh"; docker_pkg="docker"; dist="suse";\
elif pm=$(which pacman 2>/dev/null || command -v pacman 2>/dev/null); then silent_inst="-S --noconfirm --noprogressbar --quiet"; what_pkg="-Sp"; check_pkgs="-Sup"; docker_pkg="docker"; dist="archlinux";\
fi;\
echo "Dist: $dist, Packet manager: $pm, Install command: $silent_inst, What pkg command: $what_pkg, Check pkgs command: $check_pkgs, Docker pkg: $docker_pkg, Language: $LANG";\
echo $LANG | grep -qE '^(en_US.UTF-8|C.UTF-8|C)$' || export LC_ALL=C;\
if which apt-get > /dev/null 2>&1; then pm=$(which apt-get); silent_inst="-yq install --install-recommends"; check_pkgs="-yq update"; docker_pkg="docker.io"; dist="debian";\
elif which dnf > /dev/null 2>&1; then pm=$(which dnf); silent_inst="-yq install"; check_pkgs="-yq check-update"; docker_pkg="docker"; dist="fedora";\
elif which yum > /dev/null 2>&1; then pm=$(which yum); silent_inst="-y -q install"; check_pkgs="-y -q check-update"; docker_pkg="docker"; dist="centos";\
elif which zypper > /dev/null 2>&1; then pm=$(which zypper); silent_inst="-nq install"; check_pkgs="-nq refresh"; docker_pkg="docker"; dist="opensuse";\
elif which pacman > /dev/null 2>&1; then pm=$(which pacman); silent_inst="-S --noconfirm --noprogressbar --quiet"; check_pkgs="-Sup"; docker_pkg="docker"; dist="archlinux";\
else echo "Packet manager not found"; exit 1; fi;\
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 $check_pkgs; $pm $silent_inst sudo; fi;\
if ! sudo -n sh -c 'command -v which > /dev/null 2>&1'; then sudo -n $pm $check_pkgs; sudo -n $pm $silent_inst which; fi;\
if ! sudo -n sh -c 'command -v fuser > /dev/null 2>&1'; then sudo -n $pm $check_pkgs; sudo -n $pm $silent_inst psmisc; fi;\
if ! sudo -n sh -c 'command -v lsof > /dev/null 2>&1'; then sudo -n $pm $check_pkgs; sudo -n $pm $silent_inst lsof; fi;\
if ! sudo -n sh -c 'command -v docker > /dev/null 2>&1'; then \
sudo -n $pm $check_pkgs;\
if ! sudo -n $pm $what_pkg $docker_pkg 2>/dev/null | grep -qi podman; then \
sudo -n $pm $silent_inst $docker_pkg;\
sleep 5; sudo -n systemctl enable --now docker; sleep 5;\
else \
echo "Container runtime is not supported";\
exit 1;\
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 [ "$(sudo -n cat /sys/module/apparmor/parameters/enabled 2>/dev/null)" = "Y" ]; then \
if ! sudo -n sh -c 'command -v apparmor_parser > /dev/null 2>&1'; then \
sudo -n $pm $check_pkgs; sudo -n $pm $silent_inst apparmor;\
fi;\
if [ "$(cat /sys/module/apparmor/parameters/enabled 2>/dev/null)" = "Y" ]; then \
if ! command -v apparmor_parser > /dev/null 2>&1; then sudo $pm $check_pkgs; sudo $pm $silent_inst apparmor; fi;\
fi;\
if [ "$(sudo -n systemctl is-active docker)" != "active" ]; then \
sleep 5; sudo -n systemctl start docker; sleep 5;\
if [ "$(sudo -n systemctl is-active docker)" != "active" ]; then echo "Container runtime service not running"; fi;\
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;\
sudo -n docker --version || docker --version;\
if ! command -v sudo > /dev/null 2>&1; then echo "Failed to install sudo, command not found"; exit 1; fi;\
docker --version;\
uname -sr

View File

@@ -1,6 +1,5 @@
sudo docker ps -a | grep amnezia | awk '{print $1}' | xargs sudo docker stop;\
sudo docker ps -a | grep amnezia | awk '{print $1}' | xargs sudo docker rm -fv;\
sudo docker images -a --format table | grep amnezia | awk '{print $3, $1 ":" $2}' | xargs sudo docker rmi;\
sudo docker volume ls --format '{{.Name}}' | grep '^amnezia-' | xargs -r sudo docker volume rm -f;\
sudo docker network ls | grep amnezia-dns-net | awk '{print $1}' | xargs sudo docker network rm;\
sudo rm -frd /opt/amnezia

View File

@@ -1,3 +1,4 @@
sudo docker stop $CONTAINER_NAME;\
sudo docker rm -fv $CONTAINER_NAME;\
sudo docker rmi $CONTAINER_NAME;
sudo docker rmi $CONTAINER_NAME;\
test "$REMOVE_CONTAINER_DATA" = "1" && sudo docker volume rm -f ${CONTAINER_NAME}-data 2>/dev/null || true

View File

@@ -2880,6 +2880,16 @@ Thank you for staying with us!</source>
<source>Add them to the application if they were not displayed</source>
<translation>Добавить их в приложение, если они не отображаются</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="126"/>
<source>Backup</source>
<translation>Резервное копирование</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="127"/>
<source>Local copy of VPN protocols, services, all server settings and users</source>
<translation>Локальная копия VPN-протоколов, сервисов, всех настроек сервера и пользователей</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="124"/>
<source>Reboot server</source>
@@ -3569,6 +3579,34 @@ Thank you for staying with us!</source>
<source>Continue</source>
<translation>Продолжить</translation>
</message>
<message>
<source>No containers found in backup file</source>
<translation>В файле резервной копии не найдено контейнеров</translation>
</message>
<message>
<source>Installing %1 (%2/%3)...</source>
<translation>Устанавливается %1 (%2/%3)...</translation>
</message>
<message>
<source>backup.tgz</source>
<translation>backup.tgz</translation>
</message>
<message>
<source>RestoredServer</source>
<translation>Восстановленный сервер</translation>
</message>
<message>
<source>Restore from backup</source>
<translation>Восстановить из резервной копии</translation>
</message>
<message>
<source>Restoration of VPN protocols, services, all server settings and users</source>
<translation>Восстановление VPN-протоколов, сервисов, всех настроек сервера и пользователей</translation>
</message>
<message>
<source>Select Backup to Restore</source>
<translation>Выберите файл резервной копии</translation>
</message>
</context>
<context>
<name>PageSetupWizardInstalling</name>
@@ -5386,6 +5424,131 @@ FileZilla или другие SFTP-клиенты, а также смонтир
<translation>Будет установлен протокол AmneziaWG. Он обеспечивает высокую скорость соединения и гарантирует стабильную работу даже в самых сложных условиях.</translation>
</message>
</context>
<context>
<name>PageSettingsServerBackup</name>
<message>
<source>Backup</source>
<translation>Резервное копирование</translation>
</message>
<message>
<source>Local copy of VPN protocols, services, all server settings and users.</source>
<translation>Локальная копия VPN-протоколов, сервисов, всех настроек сервера и пользователей.</translation>
</message>
<message>
<source>More about backups</source>
<translation>Подробнее о резервных копиях</translation>
</message>
<message>
<source>Create backup</source>
<translation>Создать резервную копию</translation>
</message>
<message>
<source>Restore from backup</source>
<translation>Восстановить из резервной копии</translation>
</message>
<message>
<source>Create backup and download to device?</source>
<translation>Создать резервную копию и скачать на устройство?</translation>
</message>
<message>
<source>Backup will be created on server and automatically downloaded to your device</source>
<translation>Резервная копия будет создана на сервере и автоматически скачана на ваше устройство</translation>
</message>
<message>
<source>Create server configuration backup?</source>
<translation>Создать резервную копию конфигурации сервера?</translation>
</message>
<message>
<source>This will create a backup of your server containers configuration on the server</source>
<translation>На сервере будет создана резервная копия конфигурации контейнеров</translation>
</message>
<message>
<source>Create</source>
<translation>Создать</translation>
</message>
<message>
<source>Cancel</source>
<translation>Отмена</translation>
</message>
<message>
<source>Select Backup to Restore</source>
<translation>Выберите файл резервной копии</translation>
</message>
<message>
<source>Backup files (*.tar.gz *.backup *.tgz *.gz)</source>
<translation>Файлы резервных копий (*.tar.gz *.backup *.tgz *.gz)</translation>
</message>
<message>
<source>Server</source>
<translation>Сервер</translation>
</message>
<message>
<source>Backup created successfully: %1</source>
<translation>Резервная копия успешно создана: %1</translation>
</message>
<message>
<source>Backup downloaded successfully!\n\nSaved to:\n%1</source>
<translation>Резервная копия успешно скачана!\n\nСохранена в:\n%1</translation>
</message>
<message>
<source>Backup error: %1</source>
<translation>Ошибка резервного копирования: %1</translation>
</message>
</context>
<context>
<name>PageSettingsServerRestoreMode</name>
<message>
<source>%1 on %2</source>
<translation>%1 на %2</translation>
</message>
<message>
<source>Restore from backup</source>
<translation>Восстановление из резервной копии</translation>
</message>
<message>
<source>Add data from backup</source>
<translation>Добавить данные из резервной копии</translation>
</message>
<message>
<source>If the same protocols are already installed on the server, they will be updated. Created users and access will be saved</source>
<translation>Если на сервере уже установлены такие же протоколы, они будут обновлены. Созданные пользователи и доступы сохранятся</translation>
</message>
<message>
<source>Replace</source>
<translation>Заменить</translation>
</message>
<message>
<source>All installed protocols, users and their access will not be saved</source>
<translation>Все установленные протоколы, пользователи и их доступы не будут сохранены</translation>
</message>
<message>
<source>Backup restore error: %1</source>
<translation>Ошибка восстановления из резервной копии: %1</translation>
</message>
<message>
<source>Server</source>
<translation>Сервер</translation>
</message>
</context>
<context>
<name>PageSettingsServerBackupRestored</name>
<message>
<source>%1 on &quot;%2&quot;</source>
<translation>%1 на &quot;%2&quot;</translation>
</message>
<message>
<source>Backup restored</source>
<translation>Резервная копия восстановлена</translation>
</message>
<message>
<source>To home</source>
<translation>На главную</translation>
</message>
<message>
<source>To server settings</source>
<translation>К настройкам сервера</translation>
</message>
</context>
<context>
<name>main2</name>
<message>

View File

@@ -475,7 +475,8 @@ bool SubscriptionUiController::deactivateExternalDevice(const QString &serverId,
void SubscriptionUiController::validateConfig()
{
const QString serverId = m_serversController->getDefaultServerId();
if (serverId.isEmpty()) {
if (!serverId.isEmpty() && m_serversController->isLegacyApiV1Server(serverId)) {
emit unsupportedConnectDrawerRequested();
emit configValidated(false);
return;
}

View File

@@ -8,8 +8,6 @@
#include "amneziaApplication.h"
#include "core/controllers/serversController.h"
#include "core/models/containerConfig.h"
#include "core/utils/containerEnum.h"
ConnectionUiController::ConnectionUiController(ConnectionController* connectionController,
ServersController* serversController,
@@ -35,7 +33,7 @@ void ConnectionUiController::openConnection()
ErrorCode errorCode = m_connectionController->openConnection(serverId);
if (errorCode != ErrorCode::NoError) {
notifyConnectionBlocked(errorCode);
emit connectionErrorOccurred(errorCode);
return;
}
}
@@ -132,36 +130,10 @@ void ConnectionUiController::toggleConnection()
} else if (isConnected()) {
closeConnection();
} else {
const QString serverId = m_serversController->getDefaultServerId();
if (serverId.isEmpty()) {
return;
}
const ErrorCode errorCode = m_connectionController->isConnectionSupported(serverId);
if (errorCode != ErrorCode::NoError) {
notifyConnectionBlocked(errorCode);
return;
}
emit prepareConfig();
}
}
void ConnectionUiController::notifyConnectionBlocked(ErrorCode errorCode)
{
if (errorCode == ErrorCode::LegacyApiV1NotSupportedError) {
emit unsupportedConnectDrawerRequested();
return;
}
if (errorCode == ErrorCode::NoInstalledContainersError) {
emit noInstalledContainers();
return;
}
emit connectionErrorOccurred(errorCode);
}
bool ConnectionUiController::isConnectionInProgress() const
{
return m_isConnectionInProgress;
@@ -171,32 +143,3 @@ bool ConnectionUiController::isConnected() const
{
return m_isConnected;
}
bool ConnectionUiController::isRevokeBlockedDuringActiveConnection(const QString &serverId, int containerIndex,
const QString &clientId) const
{
if (clientId.isEmpty() || (!isConnected() && !isConnectionInProgress())) {
return false;
}
if (m_serversController->getDefaultServerId() != serverId) {
return false;
}
if (static_cast<int>(m_serversController->getDefaultContainer(serverId)) != containerIndex) {
return false;
}
const auto adminConfig = m_serversController->selfHostedAdminConfig(serverId);
if (!adminConfig.has_value()) {
return false;
}
const QString connectionClientId =
adminConfig->containerConfig(static_cast<DockerContainer>(containerIndex)).protocolConfig.clientId();
if (connectionClientId.isEmpty()) {
return false;
}
return connectionClientId == clientId || connectionClientId.contains(clientId);
}

View File

@@ -35,8 +35,6 @@ public slots:
void openConnection();
void closeConnection();
bool isRevokeBlockedDuringActiveConnection(const QString &serverId, int containerIndex, const QString &clientId) const;
ErrorCode getLastConnectionError();
void onConnectionStateChanged(Vpn::ConnectionState state);
@@ -50,12 +48,9 @@ signals:
void connectButtonClicked();
void preparingConfig();
void prepareConfig();
void unsupportedConnectDrawerRequested();
void noInstalledContainers();
private:
Vpn::ConnectionState getCurrentConnectionState();
void notifyConnectionBlocked(ErrorCode errorCode);
ConnectionController* m_connectionController;
ServersController* m_serversController;

View File

@@ -32,6 +32,9 @@ namespace PageLoader
PageSettingsNewsNotifications,
PageSettingsNewsDetail,
PageSettingsBackup,
PageSettingsServerBackup,
PageSettingsServerRestoreMode,
PageSettingsServerBackupRestored,
PageSettingsAbout,
PageSettingsLogging,
PageSettingsSplitTunneling,
@@ -151,6 +154,7 @@ signals:
void goToPageSettings();
void goToPageViewConfig();
void goToPageSettingsServerServices();
void goToPageSettingsServerManagement();
void goToPageSettingsBackup();
void goToShareConnectionPage(QString headerText, QString configContentHeaderText, QString configCaption, QString configExtension,
QString configFileName);

View File

@@ -75,7 +75,13 @@ InstallUiController::InstallUiController(InstallController *installController,
m_connectionController(connectionController)
{
connect(m_installController, &InstallController::configValidated, this, &InstallUiController::configValidated);
connect(m_installController, &InstallController::validationErrorOccurred, this, &InstallUiController::installationErrorOccurred);
connect(m_installController, &InstallController::validationErrorOccurred, this, [this](ErrorCode errorCode) {
if (errorCode == ErrorCode::NoInstalledContainersError) {
emit noInstalledContainers();
} else {
emit installationErrorOccurred(errorCode);
}
});
}
InstallUiController::~InstallUiController()
@@ -211,13 +217,15 @@ void InstallUiController::scanServerForInstalledContainers(const QString &server
emit installationErrorOccurred(errorCode);
}
bool InstallUiController::buildContainerConfigFromModel(int containerIndex, int protocolIndex, ContainerConfig &containerConfig)
void InstallUiController::updateContainer(const QString &serverId, int containerIndex, int protocolIndex, bool closePage)
{
DockerContainer container = static_cast<DockerContainer>(containerIndex);
Proto protocolType = static_cast<Proto>(protocolIndex);
ContainerConfig containerConfig;
containerConfig.container = container;
switch (protocolType) {
case Proto::Awg: {
containerConfig.protocolConfig = m_awgConfigModel->getProtocolConfig();
@@ -263,41 +271,6 @@ bool InstallUiController::buildContainerConfigFromModel(int containerIndex, int
}
#endif
default:
return false;
}
return true;
}
void InstallUiController::updateClientConfig(const QString &serverId, int containerIndex, int protocolIndex, bool closePage)
{
DockerContainer container = static_cast<DockerContainer>(containerIndex);
Proto protocolType = static_cast<Proto>(protocolIndex);
ContainerConfig containerConfig;
if (!buildContainerConfigFromModel(containerIndex, protocolIndex, containerConfig)) {
return;
}
ErrorCode errorCode = m_installController->updateClientConfig(serverId, container, containerConfig);
if (errorCode == ErrorCode::NoError) {
ContainerConfig updatedConfig = m_serversController->getContainerConfig(serverId, container);
m_protocolModel->updateModel(updatedConfig);
updateProtocolConfigModel(serverId, static_cast<int>(container), static_cast<int>(protocolType));
emit updateContainerFinished(tr("Settings updated successfully"), closePage);
return;
}
emit installationErrorOccurred(errorCode);
}
void InstallUiController::updateServerConfig(const QString &serverId, int containerIndex, int protocolIndex, bool closePage)
{
DockerContainer container = static_cast<DockerContainer>(containerIndex);
Proto protocolType = static_cast<Proto>(protocolIndex);
ContainerConfig containerConfig;
if (!buildContainerConfigFromModel(containerIndex, protocolIndex, containerConfig)) {
return;
}
ContainerConfig oldContainerConfig = m_serversController->getContainerConfig(serverId, container);
@@ -332,13 +305,13 @@ void InstallUiController::updateServerConfig(const QString &serverId, int contai
QFuture<ErrorCode> future =
QtConcurrent::run([installController, serverId, container, oldConfigCopy,
newConfigCopy]() mutable -> ErrorCode {
return installController->updateServerConfig(serverId, container, oldConfigCopy, newConfigCopy);
return installController->updateContainer(serverId, container, oldConfigCopy, newConfigCopy);
});
watcher->setFuture(future);
return;
}
ErrorCode errorCode = m_installController->updateServerConfig(serverId, container, oldContainerConfig, containerConfig);
ErrorCode errorCode = m_installController->updateContainer(serverId, container, oldContainerConfig, containerConfig);
if (errorCode == ErrorCode::NoError) {
ContainerConfig updatedConfig = m_serversController->getContainerConfig(serverId, container);

View File

@@ -64,8 +64,7 @@ public slots:
void scanServerForInstalledContainers(const QString &serverId);
void updateServerConfig(const QString &serverId, int containerIndex, int protocolIndex, bool closePage = true);
void updateClientConfig(const QString &serverId, int containerIndex, int protocolIndex, bool closePage = true);
void updateContainer(const QString &serverId, int containerIndex, int protocolIndex, bool closePage = true);
void removeServer(const QString &serverId);
void rebootServer(const QString &serverId);
@@ -133,6 +132,7 @@ signals:
void cachedProfileCleared(const QString &message);
void apiConfigRemoved(const QString &message);
void noInstalledContainers();
void configValidated(bool isValid);
private:
@@ -162,8 +162,6 @@ private:
QString m_privateKeyPassphrase;
void updateProtocolConfigModel(const QString &serverId, int containerIndex, int protocolIndex);
bool buildContainerConfigFromModel(int containerIndex, int protocolIndex, ContainerConfig &containerConfig);
};
#endif // INSTALLUICONTROLLER_H

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,406 @@
#ifndef SERVERSBACKUPCONTROLLER_H
#define SERVERSBACKUPCONTROLLER_H
#include <QObject>
#include <QString>
#include <QDateTime>
#include <QJsonObject>
#include <QJsonArray>
#include <QFileInfo>
class QTemporaryFile;
class ServersModel;
class ServersUiController;
class ServersController;
class SecureQSettings;
#include <QPointer>
#include "core/utils/containerEnum.h"
#include "core/utils/commonStructs.h"
#include "core/utils/errorCodes.h"
#include "core/utils/selfhosted/sshSession.h"
using namespace amnezia;
/**
* @brief Controller for managing Amnezia VPN configuration backups
*
* Uses existing ServerController and libssh::Client from Amnezia
* Bash scripts are embedded directly in C++ code
* Supports direct container backup via docker cp
*
* Fully cross-platform: Windows, macOS, Linux, iOS, Android
*/
class ServersBackupController : public QObject
{
Q_OBJECT
public:
explicit ServersBackupController(SecureQSettings *settings, ServersModel *serversModel,
ServersUiController *serversUiController, ServersController *serversController,
QObject *parent = nullptr);
~ServersBackupController();
/**
* @brief Backup information
*/
struct BackupInfo {
QString filename;
QString fullPath;
QDateTime createdAt;
qint64 size;
bool isValid;
QStringList containers;
};
enum BackupStatus {
Idle,
InProgress,
Success,
Failed
};
Q_ENUM(BackupStatus)
public slots:
/**
* @brief Create backup on server (all containers)
* @param credentials Server credentials
*/
void createBackup(const ServerCredentials &credentials);
/**
* @brief Create backup and automatically download to device (for QML)
* @param downloadToDevice Download to device after creation?
* @param deleteFromServer Delete from server after download?
*/
Q_INVOKABLE void createBackupWithDownload(bool downloadToDevice = true,
bool deleteFromServer = true);
/**
* @brief Create backup of specific container
* @param credentials Server credentials
* @param container Container type for backup
*/
void createContainerBackup(const ServerCredentials &credentials, DockerContainer container);
/**
* @brief Create backup of specific container by name
* @param credentials Server credentials
* @param containerName Container name (e.g. "amnezia-awg")
*/
void createBackupByName(const ServerCredentials &credentials, const QString &containerName);
/**
* @brief Create backup of multiple containers
* @param credentials Server credentials
* @param containers List of containers for backup
*/
void createContainersBackup(const ServerCredentials &credentials, const QList<DockerContainer> &containers);
/**
* @brief Get list of backups from server
* @param credentials Server credentials
*/
void fetchBackupList(const ServerCredentials &credentials);
/**
* @brief Restore from backup
* @param credentials Server credentials
* @param backupFilename Backup file name
* @param containers List of containers (empty = all)
* @param replaceMode If true - clears container first, then restores. If false - adds data on top of existing
*/
void restoreBackup(const ServerCredentials &credentials,
const QString &backupFilename,
const QStringList &containers = QStringList(),
bool replaceMode = false);
/**
* @brief Check backup status on server
* @param credentials Server credentials
*/
void checkBackupStatus(const ServerCredentials &credentials);
/**
* @brief Download backup to local machine
* @param credentials Server credentials
* @param backupFilename Backup file name
* @param localPath Save path
*/
void downloadBackup(const ServerCredentials &credentials,
const QString &backupFilename,
const QString &localPath);
/**
* @brief Upload backup to server
* @param credentials Server credentials
* @param localPath Path to local file
* @param replaceMode Restore mode (true = replace, false = add). Saved for later use in restoreBackup
*/
void uploadBackup(const ServerCredentials &credentials,
const QString &localPath,
bool replaceMode = false);
// Overloaded method for setup wizard with separate credential parameters
Q_INVOKABLE void uploadBackupWithStrings(const QString &hostname,
const QString &username,
const QString &secretData,
const QString &localPath,
bool replaceMode = false);
/**
* @brief Universal method to start restore (from QML)
* Automatically selects correct path depending on parameters
* @param isFromSetupWizard Restore from setup wizard?
* @param backupFilePath Path to local backup file
* @param replaceMode Restore mode (true = replace, false = add)
* @param wizardHostname Hostname for setup wizard (optional)
* @param wizardUsername Username for setup wizard (optional)
* @param wizardSecretData Secret data for setup wizard (optional)
*/
Q_INVOKABLE void startRestore(bool isFromSetupWizard,
const QString &backupFilePath,
bool replaceMode,
const QString &wizardHostname = QString(),
const QString &wizardUsername = QString(),
const QString &wizardSecretData = QString());
/**
* @brief Prepare restore information from backup file
* Parses filename, extracts IP, prepares metadata
* @param backupFilePath Path to backup file
* @return QVariantMap with keys: fileName, serverIp
*/
Q_INVOKABLE QVariantMap getBackupFileInfo(const QString &backupFilePath);
/**
* @brief Scan backup file and determine which containers it contains
* @param localPath Path to local backup file
* @return List of container names found in backup
*/
Q_INVOKABLE QStringList scanBackupForContainers(const QString &localPath);
/**
* @brief Set default server and container after restore (for setup wizard)
* @param isFromSetupWizard Was restore called from setup wizard
* @return true if successful, false if no servers or containers
*/
Q_INVOKABLE bool setDefaultServerAfterRestore(bool isFromSetupWizard);
/**
* @brief Install containers from backup on empty server (for setup wizard)
* Scans backup, adds empty server and sends signal to install containers
* @param backupFilePath Path to local backup file
* @param hostname Server hostname
* @param username Username for SSH
* @param secretData Password/key for SSH
*/
Q_INVOKABLE void prepareRestoreFromBackup(const QString &backupFilePath,
const QString &hostname,
const QString &username,
const QString &secretData);
/**
* @brief Delete backup from server
* @param credentials Server credentials
* @param backupFilename Backup file name
*/
void deleteBackup(const ServerCredentials &credentials,
const QString &backupFilename);
/**
* @brief Set backup directory on server
*/
void setBackupDirectory(const QString &directory);
/**
* @brief Get backup directory
*/
QString backupDirectory() const { return m_backupDir; }
signals:
/**
* @brief Operation status changed
*/
void statusChanged(BackupStatus status);
/**
* @brief Operation progress (0-100)
*/
void progressChanged(int percent, const QString &message);
/**
* @brief Backup list received
*/
void backupListReceived(const QList<BackupInfo> &backups);
/**
* @brief Backup created successfully
*/
void backupCreated(const QString &backupFilename);
/**
* @brief Backup restored successfully
*/
void backupRestored();
/**
* @brief Need to set default server and container (for setup wizard)
* This signal is sent after backupRestored() if restore was from setup wizard
*/
void needSetDefaultServer();
/**
* @brief Default server and container successfully set
* Can navigate to result page
*/
void defaultServerAndContainerSet();
/**
* @brief All containers from backup installed
* Can proceed to data restore
* @param backupFilePath Path to backup file
* @param hostname Hostname
* @param username Username
* @param secretData Secret data
* @param serverIp IP address (for display)
* @param fileName File name (for display)
*/
void readyForRestore(const QString &backupFilePath,
const QString &hostname,
const QString &username,
const QString &secretData,
const QString &serverIp,
const QString &fileName);
/**
* @brief Backup downloaded
*/
void backupDownloaded(const QString &localPath);
/**
* @brief Backup uploaded to server
*/
void backupUploaded(const QString &serverPath);
/**
* @brief Backup status information received
*/
void backupStatusReceived(const QJsonObject &status);
/**
* @brief Error occurred
*/
void errorOccurred(const QString &errorMessage, ErrorCode errorCode);
private:
/**
* @brief Get bash script for creating backup of all containers
* @param ipAddress Server IP address in underscored format (e.g. "192_119_110_11")
*/
QString getBackupScript(const QString &ipAddress) const;
/**
* @brief Get bash script for creating backup of specific container
* @param container Container type
* @param ipAddress Server IP address in underscored format (e.g. "192_119_110_11")
*/
QString getContainerBackupScript(DockerContainer container, const QString &ipAddress) const;
/**
* @brief Get bash script for creating backup of multiple containers
* @param containers List of containers
* @param ipAddress Server IP address in underscored format (e.g. "192_119_110_11")
*/
QString getContainersBackupScript(const QList<DockerContainer> &containers, const QString &ipAddress) const;
/**
* @brief Get bash script for restore
* @param backupFilename Backup file name
* @param containers List of containers
* @param replaceMode If true - clears container first, then restores
*/
QString getRestoreScript(const QString &backupFilename, const QStringList &containers, bool replaceMode = false) const;
/**
* @brief Get bash script for status check
*/
QString getCheckStatusScript() const;
/**
* @brief Get bash script for backup list
*/
QString getListBackupsScript() const;
/**
* @brief Parse backup list from output
*/
QList<BackupInfo> parseBackupList(const QString &output);
/**
* @brief Parse status from output
*/
QJsonObject parseBackupStatus(const QString &output);
/**
* @brief Handle standard output
*/
ErrorCode handleStdOut(const QString &data, QString &output);
/**
* @brief Handle error output
*/
ErrorCode handleStdErr(const QString &data, QString &error);
/**
* @brief Set status
*/
void setStatus(BackupStatus status);
/**
* @brief Set progress
*/
void setProgress(int percent, const QString &message);
/**
* @brief Attempt to set default container (called from timer)
*/
void trySetDefaultContainer();
ErrorCode runHostScript(const ServerCredentials &credentials, const QString &script,
const std::function<ErrorCode(const QString &, libssh::Client &)> &cbStdOut = nullptr,
const std::function<ErrorCode(const QString &, libssh::Client &)> &cbStdErr = nullptr);
ErrorCode downloadFileFromHost(const ServerCredentials &credentials, const QString &remotePath, const QString &localPath);
ErrorCode uploadFileToHostPublic(const ServerCredentials &credentials, const QString &localPath, const QString &remotePath,
libssh::ScpOverwriteMode overwriteMode = libssh::ScpOverwriteMode::ScpOverwriteExisting);
private:
QPointer<SecureQSettings> m_settings;
ServersModel *m_serversModel;
ServersUiController *m_serversUiController;
ServersController *m_serversController;
SshSession m_sshSession;
BackupStatus m_status;
QString m_backupDir;
QString m_currentOutput;
QString m_currentError;
bool m_restoreReplaceMode; // Save restore mode for use after uploadBackup
QTemporaryFile *m_tempUploadFile; // Temp file for Android URI (to prevent deletion before upload completes)
// For setting default container
int m_containerRetryCount;
static constexpr int m_maxContainerRetries = 3;
// For automatic restore after upload
ServerCredentials m_pendingRestoreCredentials;
bool m_autoRestoreAfterUpload;
// For automatic backup download/delete
bool m_autoDownloadAfterCreate;
bool m_autoDeleteAfterDownload;
QString m_lastCreatedBackupFilename;
};
#endif // SERVERSBACKUPCONTROLLER_H

View File

@@ -156,17 +156,7 @@ void ServersUiController::updateModel()
m_serversModel->updateModel(m_orderedServerDescriptions, defaultServerId);
if (!m_processedServerId.isEmpty()) {
if (isServerFromApi(m_processedServerId)) {
const auto &description = serverDescriptionById(m_processedServerId);
if (description.isApiV2 && description.isCountrySelectionAvailable
&& !description.apiAvailableCountries.isEmpty()) {
emit updateApiCountryModel();
}
} else {
updateContainersModel();
}
}
updateContainersModel();
updateDefaultServerContainersModel();
if (hadServersFromGatewayBefore != hasServersFromGatewayNow) {
@@ -360,14 +350,19 @@ void ServersUiController::setProcessedServerId(const QString &serverId)
m_processedServerId = normalizedServerId;
if (newIndex >= 0) {
if (isServerFromApi(m_processedServerId)) {
const auto &description = serverDescriptionById(m_processedServerId);
if (description.isApiV2 && description.isCountrySelectionAvailable
&& !description.apiAvailableCountries.isEmpty()) {
emit updateApiCountryModel();
updateContainersModel();
for (const auto &description : m_orderedServerDescriptions) {
if (description.serverId != normalizedServerId) {
continue;
}
} else {
updateContainersModel();
if (description.isApiV2) {
if (description.isCountrySelectionAvailable && !description.apiAvailableCountries.isEmpty()) {
emit updateApiCountryModel();
}
emit updateApiServicesModel();
}
break;
}
}

View File

@@ -113,6 +113,7 @@ signals:
void processedContainerIndexChanged(int index);
void hasServersFromGatewayApiChanged();
void updateApiCountryModel();
void updateApiServicesModel();
public:
void updateModel();

View File

@@ -22,10 +22,12 @@
SettingsUiController::SettingsUiController(SettingsController* settingsController,
ServersController* serversController,
LanguageUiController* languageUiController,
QObject *parent)
: QObject(parent),
m_settingsController(settingsController),
m_serversController(serversController)
m_serversController(serversController),
m_languageUiController(languageUiController)
{
#ifdef Q_OS_ANDROID
connect(AndroidController::instance(), &AndroidController::notificationStateChanged, this, &SettingsUiController::onNotificationStateChanged);
@@ -155,13 +157,13 @@ void SettingsUiController::restoreAppConfigFromData(const QByteArray &data)
{
ErrorCode errorCode = m_settingsController->restoreAppConfigFromData(data);
if (errorCode == ErrorCode::NoError) {
emit appLanguageChanged();
emit appLanguageChanged(
static_cast<LanguageSettings::AvailableLanguageEnum>(m_languageUiController->getCurrentLanguageIndex()));
bool amneziaDnsEnabled = m_settingsController->isAmneziaDnsEnabled();
emit amneziaDnsToggled(amneziaDnsEnabled);
emit restoreBackupFinished();
emit autoStartChanged();
emit startMinimizedChanged();
} else {
emit errorOccurred(errorCode);
@@ -176,7 +178,6 @@ QString SettingsUiController::getAppVersion()
void SettingsUiController::clearSettings()
{
m_settingsController->clearSettings();
emit autoStartChanged();
emit startMinimizedChanged();
emit resetLanguageToSystem();
@@ -205,8 +206,9 @@ bool SettingsUiController::isAutoStartEnabled()
void SettingsUiController::toggleAutoStart(bool enable)
{
m_settingsController->toggleAutoStart(enable);
emit autoStartChanged();
emit startMinimizedChanged();
if (!enable) {
emit startMinimizedChanged();
}
}
bool SettingsUiController::isStartMinimizedEnabled()

View File

@@ -5,6 +5,8 @@
#include "core/controllers/settingsController.h"
#include "core/controllers/serversController.h"
#include "ui/controllers/languageUiController.h"
#include "ui/models/languageModel.h"
#include "core/utils/errorCodes.h"
#include "core/utils/routeModes.h"
#include "core/utils/commonStructs.h"
@@ -15,6 +17,7 @@ class SettingsUiController : public QObject
public:
explicit SettingsUiController(SettingsController* settingsController,
ServersController* serversController,
LanguageUiController* languageUiController,
QObject *parent = nullptr);
Q_PROPERTY(QString primaryDns READ getPrimaryDns WRITE setPrimaryDns NOTIFY primaryDnsChanged)
@@ -29,7 +32,6 @@ public:
Q_PROPERTY(bool isDevGatewayEnv READ isDevGatewayEnv WRITE toggleDevGatewayEnv NOTIFY devGatewayEnvChanged)
Q_PROPERTY(bool isHomeAdLabelVisible READ isHomeAdLabelVisible NOTIFY isHomeAdLabelVisibleChanged)
Q_PROPERTY(bool autoStartEnabled READ isAutoStartEnabled NOTIFY autoStartChanged)
Q_PROPERTY(bool startMinimized READ isStartMinimizedEnabled NOTIFY startMinimizedChanged)
public slots:
@@ -120,7 +122,7 @@ signals:
void loggingDisableByWatcher();
void appLanguageChanged();
void appLanguageChanged(const LanguageSettings::AvailableLanguageEnum language);
void resetLanguageToSystem();
void onNotificationStateChanged();
@@ -133,12 +135,12 @@ signals:
void activityResumed();
void isHomeAdLabelVisibleChanged(bool visible);
void autoStartChanged();
void startMinimizedChanged();
private:
SettingsController* m_settingsController;
ServersController* m_serversController;
LanguageUiController* m_languageUiController;
};
#endif

View File

@@ -168,6 +168,42 @@ void SystemController::setQmlRoot(QObject *qmlRoot)
m_qmlRoot = qmlRoot;
}
QString SystemController::getFileNameFromPath(const QString &filePath)
{
if (filePath.isEmpty()) {
return "";
}
#ifdef Q_OS_ANDROID
// Для Android URI используем специальный метод для получения имени файла
if (filePath.startsWith("content://")) {
QString fileName = AndroidController::instance()->getFileName(filePath);
if (!fileName.isEmpty()) {
return fileName;
}
// Если не удалось получить имя через ContentResolver, пытаемся извлечь из URI
}
#endif
// Для обычных путей или если Android метод не сработал
QFileInfo fileInfo(filePath);
QString fileName = fileInfo.fileName();
// Если имя файла пустое, пытаемся извлечь из пути
if (fileName.isEmpty()) {
QStringList parts = filePath.split('/');
if (!parts.isEmpty()) {
fileName = parts.last();
// Декодируем URL-кодированные символы
if (fileName.contains('%')) {
fileName = QUrl::fromPercentEncoding(fileName.toUtf8());
}
}
}
return fileName;
}
bool SystemController::isAuthenticated()
{
#ifdef Q_OS_ANDROID

View File

@@ -18,6 +18,13 @@ public:
public slots:
QString getFileName(const QString &acceptLabel, const QString &nameFilter, const QString &selectedFile = "",
const bool isSaveMode = false, const QString &defaultSuffix = "");
/**
* @brief Получить имя файла из пути или URI (для Android)
* @param filePath Путь к файлу или URI
* @return Имя файла
*/
Q_INVOKABLE QString getFileNameFromPath(const QString &filePath);
void setQmlRoot(QObject *qmlRoot);

View File

@@ -30,7 +30,7 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
switch (role) {
case SubscriptionStatusRole: {
if (m_accountInfoData.configType == serverConfigUtils::ConfigType::AmneziaFreeV3) {
return QStringLiteral("<p><a style=\"color: #28c840;\">%1</a>").arg(tr("Active"));
return tr("Active");
}
return apiUtils::isSubscriptionExpired(m_accountInfoData.subscriptionEndDate)

View File

@@ -27,7 +27,6 @@ QVariant ClientManagementModel::data(const QModelIndex &index, int role) const
auto userData = client.value(configKey::userData).toObject();
switch (role) {
case ClientIdRole: return client.value(configKey::clientId).toString();
case ClientNameRole: return userData.value(configKey::clientName).toString();
case CreationDateRole: return userData.value(configKey::creationDate).toString();
case LatestHandshakeRole: return userData.value(configKey::latestHandshake).toString();
@@ -63,7 +62,6 @@ void ClientManagementModel::updateClientName(int row, const QString &newName)
QHash<int, QByteArray> ClientManagementModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[ClientIdRole] = "clientId";
roles[ClientNameRole] = "clientName";
roles[CreationDateRole] = "creationDate";
roles[LatestHandshakeRole] = "latestHandshake";

View File

@@ -10,8 +10,7 @@ class ClientManagementModel : public QAbstractListModel
public:
enum Roles {
ClientIdRole = Qt::UserRole + 1,
ClientNameRole,
ClientNameRole = Qt::UserRole + 1,
CreationDateRole,
LatestHandshakeRole,
DataReceivedRole,

View File

@@ -23,10 +23,6 @@ public:
Q_INVOKABLE int containerFromString(const QString &container) const {
return static_cast<int>(amnezia::ContainerUtils::containerFromString(container));
}
Q_INVOKABLE bool isUnsupportedContainer(int containerIndex) const {
return amnezia::ContainerUtils::isUnsupportedContainer(static_cast<amnezia::DockerContainer>(containerIndex));
}
};
#endif // CONTAINERPROPS_H

View File

@@ -67,7 +67,6 @@ QVariant ContainersModel::data(const QModelIndex &index, int role) const
case IsCurrentlyProcessedRole: return container == static_cast<DockerContainer>(m_processedContainerIndex);
case IsSupportedRole: return ContainerUtils::isSupportedByCurrentPlatform(container);
case IsShareableRole: return ContainerUtils::isShareable(container);
case IsUnsupportedContainerRole: return ContainerUtils::isUnsupportedContainer(container);
case IsVpnContainerRole: return ContainerUtils::containerService(container) == ServiceType::Vpn;
case IsServiceContainerRole: return ContainerUtils::containerService(container) == ServiceType::Other;
case IsIpsecRole: return container == DockerContainer::Ipsec;
@@ -143,8 +142,7 @@ bool ContainersModel::hasInstalledProtocols()
bool ContainersModel::isInstallationAllowed(DockerContainer container)
{
return container != DockerContainer::Awg
&& !ContainerUtils::isUnsupportedContainer(container);
return container != DockerContainer::Awg;
}
void ContainersModel::openContainerSettings(int containerIndex)
@@ -178,7 +176,6 @@ QHash<int, QByteArray> ContainersModel::roleNames() const
roles[IsCurrentlyProcessedRole] = "isCurrentlyProcessed";
roles[IsSupportedRole] = "isSupported";
roles[IsShareableRole] = "isShareable";
roles[IsUnsupportedContainerRole] = "isUnsupportedContainer";
roles[IsInstallationAllowedRole] = "isInstallationAllowed";
roles[InstallPageOrderRole] = "installPageOrder";

View File

@@ -39,8 +39,6 @@ public:
IsSupportedRole,
IsShareableRole,
IsUnsupportedContainerRole,
InstallPageOrderRole,
// Container type check roles

View File

@@ -56,17 +56,14 @@ ListViewType {
return
}
var containerIndex = proxyDefaultServerContainersModel.mapToSource(index)
if (!isInstalled) {
ServersUiController.processedContainerIndex = containerIndex
if (checked) {
containersDropDown.closeTriggered()
ServersUiController.setDefaultContainer(ServersUiController.defaultServerId, proxyDefaultServerContainersModel.mapToSource(index))
} else {
ServersUiController.processedContainerIndex = proxyDefaultServerContainersModel.mapToSource(index)
PageController.goToPage(PageEnum.PageSetupWizardProtocolSettings)
containersDropDown.closeTriggered()
return
}
containersDropDown.closeTriggered()
ServersUiController.setDefaultContainer(ServersUiController.defaultServerId, containerIndex)
}
MouseArea {

View File

@@ -5,6 +5,7 @@ import QtQuick.Layouts
import SortFilterProxyModel 0.2
import PageEnum 1.0
import ContainerProps 1.0
import "../Controls2"
import "../Controls2/TextTypes"

View File

@@ -6,36 +6,8 @@ Menu {
popupType: Popup.Native
property Item inputBlocker: null
Component {
id: inputBlockerComponent
MouseArea {
anchors.fill: parent
preventStealing: true
}
}
onAboutToShow: {
if (!textObj || !textObj.window) {
return
}
const contentItem = textObj.window.contentItem
if (!inputBlocker) {
inputBlocker = inputBlockerComponent.createObject(contentItem)
} else {
inputBlocker.parent = contentItem
}
}
onClosed: {
if (inputBlocker) {
inputBlocker.destroy()
inputBlocker = null
}
}
onAboutToShow: blocker.enabled = true
onClosed: blocker.enabled = false
MenuItem {
text: qsTr("C&ut")
@@ -59,4 +31,11 @@ Menu {
enabled: textObj.length > 0
onTriggered: textObj.selectAll()
}
MouseArea {
id: blocker
z: 2
enabled: false
preventStealing: true
}
}

View File

@@ -25,8 +25,8 @@ PageType {
filters: [
ValueFilter {
roleName: "serverId"
value: ServersUiController.processedServerId
roleName: "isCurrentlyProcessed"
value: true
}
]
}

View File

@@ -440,7 +440,8 @@ PageType {
return
}
InstallController.updateClientConfig(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, ProtocolEnum.Awg)
PageController.goToPage(PageEnum.PageSetupWizardInstalling);
InstallController.updateContainer(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, ProtocolEnum.Awg)
}
var noButtonFunction = function() {}

View File

@@ -561,7 +561,7 @@ PageType {
}
PageController.goToPage(PageEnum.PageSetupWizardInstalling);
InstallController.updateServerConfig(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, ProtocolEnum.Awg)
InstallController.updateContainer(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, ProtocolEnum.Awg)
}
var noButtonFunction = function() {}

View File

@@ -434,7 +434,7 @@ PageType {
}
PageController.goToPage(PageEnum.PageSetupWizardInstalling);
InstallController.updateServerConfig(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, ProtocolEnum.OpenVpn)
InstallController.updateContainer(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, ProtocolEnum.OpenVpn)
}
var noButtonFunction = function() {
if (!GC.isMobile()) {

View File

@@ -128,7 +128,8 @@ PageType {
return
}
InstallController.updateClientConfig(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, ProtocolEnum.WireGuard)
PageController.goToPage(PageEnum.PageSetupWizardInstalling);
InstallController.updateContainer(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, ProtocolEnum.WireGuard)
}
var noButtonFunction = function() {}
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)

View File

@@ -129,7 +129,7 @@ PageType {
}
PageController.goToPage(PageEnum.PageSetupWizardInstalling);
InstallController.updateServerConfig(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, ProtocolEnum.WireGuard)
InstallController.updateContainer(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, ProtocolEnum.WireGuard)
}
var noButtonFunction = function() {
if (!GC.isMobile()) {

View File

@@ -112,7 +112,7 @@ PageType {
return
}
PageController.goToPage(PageEnum.PageSetupWizardInstalling)
InstallController.updateServerConfig(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
}
var noButtonFunction = function () {
if (typeof GC !== "undefined" && !GC.isMobile()) {

View File

@@ -279,7 +279,7 @@ PageType {
return
}
PageController.goToPage(PageEnum.PageSetupWizardInstalling)
InstallController.updateServerConfig(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
}
var noButtonFunction = function () {
if (typeof GC !== "undefined" && !GC.isMobile()) {

View File

@@ -17,10 +17,6 @@ import "../Components"
PageType {
id: root
enableTimer: false
property bool portDirty: false
function formatTransport(value) {
if (value === "raw") return "RAW (TCP)"
if (value === "xhttp") return "XHTTP"
@@ -43,8 +39,8 @@ PageType {
anchors.right: parent.right
anchors.topMargin: 20 + PageController.safeAreaTopMargin
onActiveFocusChanged: {
if (backButton.enabled && backButton.activeFocus) {
onFocusChanged: {
if (this.activeFocus) {
listView.positionViewAtBeginning()
}
}
@@ -64,6 +60,8 @@ PageType {
delegate: ColumnLayout {
width: listView.width
property alias focusItemId: textFieldWithHeaderType.textField
spacing: 0
Text {
@@ -109,32 +107,13 @@ PageType {
Layout.rightMargin: 16
enabled: listView.enabled
headerText: qsTr("Port")
Binding {
target: textFieldWithHeaderType.textField
property: "text"
value: port
when: !textFieldWithHeaderType.textField.activeFocus
restoreMode: Binding.RestoreNone
}
textField.text: port
textField.maximumLength: 5
textField.validator: IntValidator {
bottom: 1; top: 65535
}
textField.onActiveFocusChanged: {
if (textField.activeFocus && textField.text === "" && port !== "") {
textField.text = port
}
}
textField.onTextChanged: {
root.portDirty = (textField.text !== port)
}
textField.onEditingFinished: {
if (textField.text !== port) {
port = textField.text
}
root.portDirty = false
if (textField.text !== port) port = textField.text
}
checkEmptyText: true
}
@@ -193,8 +172,9 @@ PageType {
Layout.leftMargin: 16
Layout.rightMargin: 16
visible: listView.enabled
&& (XrayConfigModel.hasUnsavedChanges || root.portDirty)
enabled: visible && textFieldWithHeaderType.textField.text !== ""
&& (XrayConfigModel.hasUnsavedChanges
|| textFieldWithHeaderType.textField.text !== port)
enabled: visible && textFieldWithHeaderType.errorText === ""
text: qsTr("Save")
onClicked: function() {
forceActiveFocus()
@@ -213,7 +193,7 @@ PageType {
}
PageController.goToPage(PageEnum.PageSetupWizardInstalling);
InstallController.updateServerConfig(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
InstallController.updateContainer(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
}
var noButtonFunction = function() {
if (!GC.isMobile()) saveButton.forceActiveFocus()

View File

@@ -742,7 +742,7 @@ PageType {
return
}
PageController.goToPage(PageEnum.PageSetupWizardInstalling)
InstallController.updateServerConfig(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
}
var noButtonFunction = function () {
if (typeof GC !== "undefined" && !GC.isMobile()) {

View File

@@ -95,7 +95,7 @@ PageType {
return
}
PageController.goToPage(PageEnum.PageSetupWizardInstalling)
InstallController.updateServerConfig(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
}
var noButtonFunction = function () {
if (typeof GC !== "undefined" && !GC.isMobile()) {

View File

@@ -211,7 +211,7 @@ PageType {
return
}
PageController.goToPage(PageEnum.PageSetupWizardInstalling)
InstallController.updateServerConfig(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
}
var noButtonFunction = function () {
if (typeof GC !== "undefined" && !GC.isMobile()) {

View File

@@ -208,7 +208,7 @@ PageType {
return
}
PageController.goToPage(PageEnum.PageSetupWizardInstalling)
InstallController.updateServerConfig(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
}
var noButtonFunction = function () {
if (typeof GC !== "undefined" && !GC.isMobile()) {

View File

@@ -179,7 +179,7 @@ PageType {
function mtProxyScheduleUpdate(closePage) {
var cp = closePage === undefined ? false : closePage
Qt.callLater(function () {
InstallController.updateServerConfig(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, ProtocolEnum.MtProxy, cp)
InstallController.updateContainer(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, ProtocolEnum.MtProxy, cp)
})
}

View File

@@ -285,7 +285,7 @@ PageType {
}
PageController.goToPage(PageEnum.PageSetupWizardInstalling)
InstallController.updateServerConfig(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, ProtocolEnum.Socks5Proxy)
InstallController.updateContainer(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, ProtocolEnum.Socks5Proxy)
tempPort = portTextField.textField.text
tempUsername = usernameTextField.textField.text
tempPassword = passwordTextField.textField.text

View File

@@ -154,7 +154,7 @@ PageType {
function telemtScheduleUpdate(closePage) {
var cp = closePage === undefined ? false : closePage
Qt.callLater(function () {
InstallController.updateServerConfig(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, ProtocolEnum.Telemt, cp)
InstallController.updateContainer(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, ProtocolEnum.Telemt, cp)
})
}

View File

@@ -100,12 +100,6 @@ PageType {
onLinkActivated: Qt.openUrlExternally(link)
textFormat: Text.RichText
text: qsTr("Use <a href=\"https://www.torproject.org/download/\" style=\"color: #FBB26A;\">Tor Browser</a> to open this URL.")
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
}
}
ParagraphTextType {

View File

@@ -30,16 +30,6 @@ PageType {
root.isInAppPurchase = ApiAccountInfoModel.data("isInAppPurchase")
}
function selectConnectionCountry(countryIndex, countryCode, countryName) {
if (countryIndex === ApiCountryModel.currentIndex) {
return
}
PageController.showBusyIndicator(true)
SubscriptionUiController.updateServiceFromGateway(ServersUiController.processedServerId, countryCode, countryName)
PageController.showBusyIndicator(false)
}
Component.onCompleted: {
root.updateSubscriptionState()
}
@@ -93,7 +83,7 @@ PageType {
model: ApiCountryModel
currentIndex: ApiCountryModel.currentIndex
currentIndex: 0
ButtonGroup {
id: containersRadioButtonGroup
@@ -214,7 +204,15 @@ PageType {
return
}
root.selectConnectionCountry(index, countryCode, countryName)
if (index !== ApiCountryModel.currentIndex) {
PageController.showBusyIndicator(true)
var prevIndex = ApiCountryModel.currentIndex
ApiCountryModel.currentIndex = index
if (!SubscriptionUiController.updateServiceFromGateway(ServersUiController.processedServerId, countryCode, countryName)) {
ApiCountryModel.currentIndex = prevIndex
}
PageController.showBusyIndicator(false)
}
}
Keys.onEnterPressed: {

View File

@@ -108,9 +108,9 @@ PageType {
text: qsTr("Auto start")
descriptionText: qsTr("Launch the application every time the device is starts")
checked: SettingsController.autoStartEnabled
checked: SettingsController.isAutoStartEnabled()
onToggled: function() {
if (checked !== SettingsController.autoStartEnabled) {
if (checked !== SettingsController.isAutoStartEnabled()) {
SettingsController.toggleAutoStart(checked)
}
}
@@ -154,10 +154,10 @@ PageType {
text: qsTr("Start minimized")
descriptionText: qsTr("Launch application minimized (works with autostart option turned on)")
enabled: SettingsController.autoStartEnabled
enabled: SettingsController.isAutoStartEnabled()
opacity: enabled ? 1.0 : 0.5
checked: SettingsController.autoStartEnabled && SettingsController.startMinimized
checked: SettingsController.isAutoStartEnabled() && SettingsController.startMinimized
onToggled: function() {
if (checked !== SettingsController.startMinimized) {
SettingsController.toggleStartMinimized(checked)
@@ -166,7 +166,7 @@ PageType {
}
DividerType {
visible: !GC.isMobile() && ServersUiController.hasServersFromGatewayApi
visible: !GC.isMobile()
}
SwitcherType {

View File

@@ -0,0 +1,212 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import PageEnum 1.0
import ProtocolEnum 1.0
import Style 1.0
import "../Controls2"
import "../Controls2/TextTypes"
import "../Components"
import "../Config"
PageType {
id: root
BackButtonType {
id: backButton
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onActiveFocusChanged: {
if(backButton.enabled && backButton.activeFocus) {
flickable.contentY = 0
}
}
}
FlickableType {
id: flickable
anchors.top: backButton.bottom
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
contentHeight: contentColumn.implicitHeight
ColumnLayout {
id: contentColumn
width: flickable.width
spacing: 16
BaseHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
headerText: qsTr("Backup")
}
ParagraphTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
text: qsTr("Local copy of VPN protocols, services, all server settings and users.")
color: AmneziaStyle.color.mutedGray
}
Text {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: -8
text: qsTr("More about backups")
color: AmneziaStyle.color.goldenApricot
font.pixelSize: 14
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
// TODO: Open help page or show more info
}
}
}
ColumnLayout {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 16
spacing: 12
BasicButtonType {
Layout.fillWidth: true
text: qsTr("Create backup")
clickedFunc: function() {
createBackup(true)
}
}
BasicButtonType {
Layout.fillWidth: true
text: qsTr("Restore from backup")
defaultColor: AmneziaStyle.color.transparent
hoveredColor: Qt.rgba(1, 1, 1, 0.08)
pressedColor: Qt.rgba(1, 1, 1, 0.12)
disabledColor: AmneziaStyle.color.mutedGray
textColor: AmneziaStyle.color.goldenApricot
borderWidth: 1
clickedFunc: function() {
restoreBackup()
}
}
}
}
}
function createBackup(shouldDownload) {
// Default shouldDownload = true
var downloadAfterCreate = (shouldDownload !== undefined) ? shouldDownload : true
var headerText = downloadAfterCreate ?
qsTr("Create backup and download to device?") :
qsTr("Create server configuration backup?")
var descriptionText = downloadAfterCreate ?
qsTr("Backup will be created on server and automatically downloaded to your device") :
qsTr("This will create a backup of your server containers configuration on the server")
var yesButtonText = qsTr("Create")
var noButtonText = qsTr("Cancel")
var yesButtonFunction = function() {
PageController.showBusyIndicator(true)
// Call C++ method that manages download and delete automatically
ServersBackupController.createBackupWithDownload(downloadAfterCreate, true)
}
var noButtonFunction = function() {}
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
}
function restoreBackup() {
var filter = GC.isMobile() ? "*.gz *.tgz *.tar.gz" : "Backup files (*.tar.gz *.backup *.tgz *.gz)"
var localPath = SystemController.getFileName(
qsTr("Select Backup to Restore"),
filter,
"",
false,
""
)
if (!localPath || localPath.length === 0) {
return
}
// Get file information via C++
var fileInfo = ServersBackupController.getBackupFileInfo(localPath)
var fileName = fileInfo.fileName || "backup.tgz"
var serverIp = fileInfo.serverIp || ""
// If IP not found in filename, use current server
if (!serverIp || serverIp.length === 0) {
serverIp = ServersUiController.serverHostName(ServersUiController.processedServerId) || ""
}
var serverName = ServersUiController.serverName(ServersUiController.processedServerId) || qsTr("Server")
// Open restore mode selection page
var parentItem = root.parent
while (parentItem && parentItem.objectName !== "tabBarStackView") {
parentItem = parentItem.parent
}
if (parentItem && typeof parentItem.push === "function") {
parentItem.push(PageController.getPagePath(PageEnum.PageSettingsServerRestoreMode), {
"backupFilePath": localPath,
"backupFileName": fileName,
"serverName": serverName,
"serverIp": serverIp
})
} else {
console.warn("Could not find StackView to navigate to restore mode page")
}
}
// ============ Backup Controller Connections ============
Connections {
target: ServersBackupController
function onBackupCreated(backupFilename) {
// If auto-download is not enabled, show success message
PageController.showBusyIndicator(false)
PageController.showNotificationMessage(qsTr("Backup created successfully: %1").arg(backupFilename))
}
function onBackupDownloaded(localPath) {
PageController.showBusyIndicator(false)
console.log("Backup downloaded to:", localPath)
PageController.showNotificationMessage(qsTr("Backup downloaded successfully!\n\nSaved to:\n%1").arg(localPath))
}
function onProgressChanged(percent, message) {
console.log("Backup progress:", percent, "%", message)
}
function onErrorOccurred(errorMessage, errorCode) {
PageController.showBusyIndicator(false)
PageController.showErrorMessage(qsTr("Backup error: %1").arg(errorMessage))
}
}
}

View File

@@ -0,0 +1,130 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import PageEnum 1.0
import ProtocolEnum 1.0
import Style 1.0
import "../Controls2"
import "../Controls2/TextTypes"
import "../Components"
import "../Config"
PageType {
id: root
property string backupFileName: ""
property string serverName: ""
property string serverIp: ""
property bool isFromSetupWizard: false
Component.onCompleted: {
// Убеждаемся, что все свойства инициализированы
if (!backupFileName) backupFileName = ""
if (!serverName) serverName = ""
if (!serverIp) serverIp = ""
}
BackButtonType {
id: backButton
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
backButtonFunction: function() {
// После успешного restore всегда идем на главную страницу
PageController.goToPageHome()
}
onActiveFocusChanged: {
if(backButton.enabled && backButton.activeFocus) {
flickable.contentY = 0
}
}
}
FlickableType {
id: flickable
anchors.top: backButton.bottom
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
contentHeight: contentColumn.implicitHeight
ColumnLayout {
id: contentColumn
width: flickable.width
spacing: 16
BaseHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
headerText: qsTr("Backup restored")
}
ParagraphTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
text: {
var baseText = qsTr("%1 on \"%2\"").arg(backupFileName).arg(serverName)
if (serverIp && serverIp.length > 0) {
return baseText + ", " + serverIp
}
return baseText
}
color: AmneziaStyle.color.mutedGray
}
ColumnLayout {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 24
spacing: 12
BasicButtonType {
Layout.fillWidth: true
text: qsTr("To home")
implicitHeight: 56
clickedFunc: function() {
// Переход на главную страницу (PageHome)
PageController.goToPageHome()
}
}
BasicButtonType {
Layout.fillWidth: true
text: qsTr("To server settings")
implicitHeight: 56
defaultColor: AmneziaStyle.color.transparent
hoveredColor: AmneziaStyle.color.transparent
pressedColor: AmneziaStyle.color.transparent
borderWidth: 1
borderColor: "#FFFFFF"
textColor: "#FFFFFF"
clickedFunc: function() {
// Открываем страницу настроек сервера с активной вкладкой "Управление"
PageController.goToPage(PageEnum.PageSettingsServerInfo)
// Устанавливаем активной вкладку "Управление" через сигнал
PageController.goToPageSettingsServerManagement()
}
}
}
}
}
}

View File

@@ -36,6 +36,17 @@ PageType {
function onRebootServerFinished(finishedMessage) {
PageController.showNotificationMessage(finishedMessage)
}
function onRemoveAllContainersFinished(finishedMessage) {
PageController.closePage() // close deInstalling page
PageController.showNotificationMessage(finishedMessage)
}
function onRemoveContainerFinished(finishedMessage) {
PageController.closePage() // close deInstalling page
PageController.closePage() // close page with remove button
PageController.showNotificationMessage(finishedMessage)
}
}
Connections {
@@ -85,6 +96,7 @@ PageType {
property list<QtObject> serverActions: [
check,
backupSection,
reboot,
remove,
clear,
@@ -95,6 +107,7 @@ PageType {
id: check
property bool isVisible: root.isServerWithWriteAccess
readonly property bool isBackupSection: false
readonly property string title: qsTr("Check the server for previously installed Amnezia services")
readonly property string description: qsTr("Add them to the application if they were not displayed")
readonly property var tColor: AmneziaStyle.color.paleGray
@@ -105,10 +118,25 @@ PageType {
}
}
QtObject {
id: backupSection
property bool isVisible: root.isServerWithWriteAccess
readonly property bool isBackupSection: false
readonly property string title: qsTr("Backup")
readonly property string description: qsTr("Local copy of VPN protocols, services, all server settings and users")
readonly property var tColor: AmneziaStyle.color.paleGray
readonly property var clickedHandler: function() {
// Navigate to server backup page using PageController
PageController.goToPage(PageEnum.PageSettingsServerBackup)
}
}
QtObject {
id: reboot
property bool isVisible: root.isServerWithWriteAccess
readonly property bool isBackupSection: false
readonly property string title: qsTr("Reboot server")
readonly property string description: ""
readonly property var tColor: AmneziaStyle.color.vibrantRed
@@ -139,6 +167,7 @@ PageType {
id: remove
property bool isVisible: true
readonly property bool isBackupSection: false
readonly property string title: qsTr("Remove server from application")
readonly property string description: ""
readonly property var tColor: AmneziaStyle.color.vibrantRed
@@ -169,6 +198,7 @@ PageType {
id: clear
property bool isVisible: root.isServerWithWriteAccess
readonly property bool isBackupSection: false
readonly property string title: qsTr("Clear server from Amnezia software")
readonly property string description: ""
readonly property var tColor: AmneziaStyle.color.vibrantRed
@@ -198,6 +228,7 @@ PageType {
id: reset
property bool isVisible: ServersUiController.isServerFromApi(ServersUiController.processedServerId)
readonly property bool isBackupSection: false
readonly property string title: qsTr("Reset API config")
readonly property string description: ""
readonly property var tColor: AmneziaStyle.color.vibrantRed
@@ -224,4 +255,5 @@ PageType {
}
}
}

View File

@@ -29,6 +29,10 @@ PageType {
function onGoToPageSettingsServerServices() {
tabBar.setCurrentIndex(root.pageSettingsServerServices)
}
function onGoToPageSettingsServerManagement() {
tabBar.setCurrentIndex(root.pageSettingsServerData)
}
}
Connections {

View File

@@ -17,8 +17,7 @@ import "../Components"
PageType {
id: root
property bool isUnsupportedContainer: ContainerProps.isUnsupportedContainer(ServersUiController.processedContainerIndex)
property bool isClearCacheVisible: !isUnsupportedContainer && ServersUiController.isProcessedServerHasWriteAccess() && !ContainersModel.isServiceContainer(ServersUiController.processedContainerIndex)
property bool isClearCacheVisible: ServersUiController.isProcessedServerHasWriteAccess() && !ContainersModel.isServiceContainer(ServersUiController.processedContainerIndex)
BackButtonType {
id: backButton
@@ -53,11 +52,10 @@ PageType {
Layout.bottomMargin: 32
headerText: ContainersModel.getProcessedContainerName() + qsTr(" settings")
descriptionText: root.isUnsupportedContainer ? qsTr("This protocol is no longer supported.") : ""
}
}
model: root.isUnsupportedContainer ? null : ProtocolsModel
model: ProtocolsModel
delegate: ColumnLayout {
id: delegateContent

View File

@@ -0,0 +1,212 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import PageEnum 1.0
import ProtocolEnum 1.0
import Style 1.0
import "../Controls2"
import "../Controls2/TextTypes"
import "../Components"
import "../Config"
PageType {
id: root
property string backupFilePath: ""
property string backupFileName: ""
property string serverName: ""
property string serverIp: ""
property bool isFromSetupWizard: false
// Credentials for setup wizard (when server is not yet added to ServersModel)
property string wizardHostname: ""
property string wizardUsername: ""
property string wizardSecretData: ""
BackButtonType {
id: backButton
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onActiveFocusChanged: {
if(backButton.enabled && backButton.activeFocus) {
flickable.contentY = 0
}
}
}
FlickableType {
id: flickable
anchors.top: backButton.bottom
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
contentHeight: contentColumn.implicitHeight
ColumnLayout {
id: contentColumn
width: flickable.width
spacing: 16
BaseHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
headerText: qsTr("Restore from backup")
}
ParagraphTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
text: {
// Show only filename and IP address, without server name
if (serverIp && serverIp.length > 0) {
return qsTr("%1 on %2").arg(backupFileName).arg(serverIp)
}
return backupFileName
}
color: AmneziaStyle.color.mutedGray
}
ColumnLayout {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 16
spacing: 0
LabelWithButtonType {
Layout.fillWidth: true
text: qsTr("Add data from backup")
descriptionText: qsTr("If the same protocols are already installed on the server, they will be updated. Created users and access will be saved")
rightImageSource: "qrc:/images/controls/chevron-right.svg"
clickedFunction: function() {
startRestore(false) // false = add mode
}
}
DividerType {}
LabelWithButtonType {
Layout.fillWidth: true
text: qsTr("Replace")
descriptionText: qsTr("All installed protocols, users and their access will not be saved")
rightImageSource: "qrc:/images/controls/chevron-right.svg"
textColor: AmneziaStyle.color.vibrantRed
clickedFunction: function() {
startRestore(true) // true = replace mode
}
}
}
}
}
property bool restoreReplaceMode: false
function startRestore(replaceMode) {
restoreReplaceMode = replaceMode
PageController.showBusyIndicator(true)
// Call universal C++ method that will determine how to perform restore
ServersBackupController.startRestore(
isFromSetupWizard,
backupFilePath,
replaceMode,
wizardHostname || "",
wizardUsername || "",
wizardSecretData || ""
)
}
property string lastUploadedBackupFilename: ""
Connections {
target: ServersBackupController
function onBackupRestored() {
console.log(" onBackupRestored, isFromSetupWizard:", isFromSetupWizard)
// For setup wizard, call C++ method to set default server and container
if (isFromSetupWizard) {
ServersBackupController.setDefaultServerAfterRestore(true)
} else {
// For regular mode, navigate directly
PageController.showBusyIndicator(false)
navigateToRestoredPage()
}
}
function onDefaultServerAndContainerSet() {
console.log(" onDefaultServerAndContainerSet - navigating to restored page")
// C++ has set default server and container, navigate to result page
PageController.showBusyIndicator(false)
navigateToRestoredPage()
}
function onErrorOccurred(errorMessage, errorCode) {
PageController.showBusyIndicator(false)
PageController.showErrorMessage(qsTr("Backup restore error: %1").arg(errorMessage))
}
}
function navigateToRestoredPage() {
// Navigate to successful restore page
// Get actual server name from model
var actualServerName = serverName
if (root.isFromSetupWizard && ServersUiController.getServersCount() > 0) {
var lastServerId = ServersUiController.getServerId(ServersUiController.getServersCount() - 1)
actualServerName = ServersUiController.serverName(lastServerId) || qsTr("Server")
} else if (!serverName || serverName.length === 0) {
actualServerName = ServersUiController.serverName(ServersUiController.processedServerId) || qsTr("Server")
}
var parentItem = root.parent
// For setup wizard use regular StackView
if (root.isFromSetupWizard) {
while (parentItem && typeof parentItem.push !== "function") {
parentItem = parentItem.parent
}
if (parentItem && typeof parentItem.push === "function") {
parentItem.push(PageController.getPagePath(PageEnum.PageSettingsServerBackupRestored), {
"backupFileName": backupFileName,
"serverName": actualServerName,
"serverIp": serverIp,
"isFromSetupWizard": true
})
}
} else {
// For management menu, find tabBarStackView
while (parentItem && parentItem.objectName !== "tabBarStackView") {
parentItem = parentItem.parent
}
if (parentItem && typeof parentItem.push === "function") {
parentItem.push(PageController.getPagePath(PageEnum.PageSettingsServerBackupRestored), {
"backupFileName": backupFileName,
"serverName": actualServerName,
"serverIp": serverIp,
"isFromSetupWizard": false
})
} else {
console.warn("Could not find StackView to navigate to restored page")
}
}
}
}

View File

@@ -13,6 +13,12 @@ import "../Controls2/TextTypes"
PageType {
id: root
property var setupWizardEasy: null
property string savedHostname: ""
property string savedUsername: ""
property string savedSecretData: ""
BackButtonType {
id: backButton
@@ -130,6 +136,11 @@ PageType {
return
}
root.savedHostname = _hostname
root.savedUsername = _username
root.savedSecretData = _secretData
console.log("Saved credentials in PageSetupWizardCredentials:", _hostname, _username)
PageController.goToPage(PageEnum.PageSetupWizardEasy)
}
}

View File

@@ -15,8 +15,216 @@ import "../Config"
PageType {
id: root
objectName: "pageSetupWizardEasy"
property bool isEasySetup: true
property bool isRestoreFromBackup: false
property string backupFilePath: ""
property string restoreHostname: ""
property string restoreUsername: ""
property string restoreSecretData: ""
property bool waitingForServerToAdd: false
// For installing containers from backup
property var containersToInstall: []
property int currentContainerIndex: 0
property bool isInstallingContainers: false
// Connections for ServersBackupController
Connections {
target: ServersBackupController
function onReadyForRestore(backupFilePath, hostname, username, secretData, serverIp, fileName) {
console.log("onReadyForRestore received from C++")
console.log(" backupFilePath:", backupFilePath)
console.log(" hostname:", hostname)
console.log(" serverIp:", serverIp)
console.log(" fileName:", fileName)
// Scan backup to determine containers (C++ already did this, but needed for QML)
var foundContainers = ServersBackupController.scanBackupForContainers(backupFilePath)
console.log("Found containers:", foundContainers)
if (foundContainers.length === 0) {
PageController.showErrorMessage(qsTr("No containers found in backup file"))
root.isRestoreFromBackup = false
return
}
root.containersToInstall = foundContainers
root.currentContainerIndex = 0
// Now add empty server with these credentials
InstallController.setProcessedServerCredentials(hostname, username, secretData)
// Set waiting flag
root.waitingForServerToAdd = true
console.log("Backup scanned, adding server...")
// Add server (asynchronously)
InstallController.addEmptyServer()
// Further execution will happen in onInstallServerFinished
}
}
// Connections for tracking server addition
Connections {
target: InstallController
function onInstallServerFinished(finishedMessage) {
if (root.waitingForServerToAdd && root.isRestoreFromBackup && root.backupFilePath.length > 0) {
console.log("Server added successfully, now installing containers from backup...")
root.waitingForServerToAdd = false
// Start installing containers
root.isInstallingContainers = true
installNextContainer()
}
}
function onInstallContainerFinished(finishedMessage, isServiceInstall) {
if (root.isInstallingContainers) {
console.log("Container installed:", finishedMessage)
// Move to next container
root.currentContainerIndex++
if (root.currentContainerIndex < root.containersToInstall.length) {
// Install next container
installNextContainer()
} else {
// All containers installed, now do restore
console.log("All containers installed, starting restore...")
root.isInstallingContainers = false
// IMPORTANT: Turn off busy indicator before navigation
PageController.showBusyIndicator(false)
// Start navigation to restore mode selection page
navigationTimer.start()
}
}
}
}
// Function to install next container from list
function installNextContainer() {
if (root.currentContainerIndex >= root.containersToInstall.length) {
return
}
var containerName = root.containersToInstall[root.currentContainerIndex]
console.log("Installing container:", containerName, "(", root.currentContainerIndex + 1, "/", root.containersToInstall.length, ")")
// Convert container name to DockerContainer enum
var dockerContainer = ContainerProps.containerFromString(containerName)
if (dockerContainer === 0) { // None
console.log("Unknown container:", containerName, "skipping...")
root.currentContainerIndex++
installNextContainer()
return
}
// Get default settings for container
var defaultProtocol = ContainerProps.defaultProtocol(dockerContainer)
var defaultPort = InstallController.getPortForInstall(defaultProtocol)
var defaultTransport = InstallController.defaultTransportProto(defaultProtocol)
// Set server index
var serverIdx = ServersModel.getServersCount() - 1
ServersModel.processedIndex = serverIdx
var serverId = ServersUiController.getServerId(serverIdx)
// Show loading indicator with message
PageController.showBusyIndicator(true)
PageController.showNotificationMessage(qsTr("Installing %1 (%2/%3)...")
.arg(containerName)
.arg(root.currentContainerIndex + 1)
.arg(root.containersToInstall.length))
// Ensure credentials are set
InstallController.setProcessedServerCredentials(root.restoreHostname, root.restoreUsername, root.restoreSecretData)
// Install container
console.log("Installing container:", containerName, "serverId:", serverId)
ContainersModel.setProcessedContainerIndex(dockerContainer)
InstallController.install(dockerContainer, defaultPort, defaultTransport, serverId)
}
// Timer for navigating to restore mode selection page after file selection
Timer {
id: navigationTimer
interval: 500
repeat: false
onTriggered: {
if (root.backupFilePath.length > 0 && root.isRestoreFromBackup) {
console.log("Navigation timer triggered, going to restore mode page")
console.log("Credentials available:", root.restoreHostname, root.restoreUsername, root.restoreSecretData.length > 0 ? "***" : "EMPTY")
// Get filename
var fileName = SystemController.getFileNameFromPath(root.backupFilePath)
if (!fileName || fileName === undefined || fileName.length === 0) {
var fallbackName = root.backupFilePath.split('/').pop()
fileName = (fallbackName && fallbackName.length > 0) ? fallbackName : qsTr("backup.tgz")
}
fileName = String(fileName)
// Extract IP address from filename
var serverIp = ""
var ipMatch = fileName.match(/^([\d_]+)\s*-/)
if (ipMatch && ipMatch.length > 1) {
serverIp = ipMatch[1].replace(/_/g, ".")
}
if (!serverIp || serverIp.length === 0) {
serverIp = root.restoreHostname
}
var serverName = root.restoreHostname
if (!serverName || serverName.length === 0) {
serverName = qsTr("RestoredServer")
}
// Navigate to installation page
PageController.goToPage(PageEnum.PageSetupWizardInstalling)
// Immediately find StackView and navigate to restore page
// Server already added, as we waited for onInstallServerFinished
Qt.callLater(function() {
var pagePath = "qrc:/ui/qml/Pages2/PageSettingsServerRestoreMode.qml"
// Traverse upward from root to find the containing StackView.
// StackView has both `push` function and `depth` property.
// This avoids a recursive downward search that causes stack overflow
// on iOS when the component tree is large (many VPN managers).
var stackView = root.parent
while (stackView) {
if (typeof stackView.push === "function" && stackView.hasOwnProperty("depth")) {
break
}
stackView = stackView.parent
}
if (stackView) {
console.log("Found StackView, pushing restore mode page")
stackView.push(pagePath, {
"backupFilePath": root.backupFilePath,
"backupFileName": fileName,
"serverName": "",
"serverIp": serverIp,
"isFromSetupWizard": true,
"wizardHostname": root.restoreHostname,
"wizardUsername": root.restoreUsername,
"wizardSecretData": root.restoreSecretData
})
} else {
console.error("Could not find StackView")
}
})
}
}
}
SortFilterProxyModel {
id: proxyContainersModel
@@ -149,6 +357,83 @@ PageType {
Keys.onReturnPressed: this.clicked()
}
DividerType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
}
CardType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: qsTr("Restore from backup")
bodyText: qsTr("Restoration of VPN protocols, services, all server settings and users")
ButtonGroup.group: buttonGroup
onClicked: function() {
var filter = GC.isMobile() ? "*.gz *.tgz *.tar.gz" : "Backup files (*.tar.gz *.backup *.tgz *.gz)"
var localPath = SystemController.getFileName(
qsTr("Select Backup to Restore"),
filter,
"",
false,
""
)
console.log("Selected file path:", localPath)
if (!localPath || localPath.length === 0) {
console.log("No file selected")
return
}
// Save backup file path
root.backupFilePath = localPath
root.isRestoreFromBackup = true
// Get credentials from PageSetupWizardCredentials via StackView search
var credentialsPage = null
var item = root
// Find StackView
while (item && !item.hasOwnProperty("depth")) {
item = item.parent
}
// If found StackView, search for PageSetupWizardCredentials in its history
if (item && item.depth > 0) {
for (var i = 0; i < item.depth; i++) {
var page = item.get(i)
if (page && page.hasOwnProperty("savedHostname")) {
credentialsPage = page
break
}
}
}
if (credentialsPage && credentialsPage.savedHostname.length > 0) {
root.restoreHostname = credentialsPage.savedHostname
root.restoreUsername = credentialsPage.savedUsername
root.restoreSecretData = credentialsPage.savedSecretData
console.log("Got credentials from PageSetupWizardCredentials:", root.restoreHostname, root.restoreUsername)
// Call C++ method to prepare restore
// It will scan backup and send readyForRestore signal
ServersBackupController.prepareRestoreFromBackup(localPath, root.restoreHostname, root.restoreUsername, root.restoreSecretData)
} else {
console.log("WARNING: No credentials found")
return
}
}
Keys.onEnterPressed: this.clicked()
Keys.onReturnPressed: this.clicked()
}
BasicButtonType {
id: continueButton

View File

@@ -29,10 +29,6 @@ PageType {
ValueFilter {
roleName: "isInstallationAllowed"
value: true
},
ValueFilter {
roleName: "isUnsupportedContainer"
value: false
}
]
sorters: RoleSorter {

View File

@@ -382,10 +382,6 @@ PageType {
ValueFilter {
roleName: "isShareable"
value: true
},
ValueFilter {
roleName: "isUnsupportedContainer"
value: false
}
]
}
@@ -400,19 +396,9 @@ PageType {
target: serverSelector
function onServerSelectorIndexChanged() {
if (!proxyContainersModel.count) {
root.shareButtonEnabled = false
return
}
var defaultContainer = proxyContainersModel.mapFromSource(
ServersUiController.serverDefaultContainer(ServersUiController.processedServerId))
if (defaultContainer < 0) {
defaultContainer = 0
}
var defaultContainer = proxyContainersModel.mapFromSource(ServersUiController.serverDefaultContainer(ServersUiController.processedServerId))
containerSelectorListView.selectedIndex = defaultContainer
containerSelectorListView.positionViewAtIndex(defaultContainer, ListView.Beginning)
containerSelectorListView.positionViewAtIndex(selectedIndex, ListView.Beginning)
containerSelectorListView.triggerCurrentItem()
}
}
@@ -851,10 +837,11 @@ PageType {
var noButtonFunction = function() {
}
if (ConnectionController.isRevokeBlockedDuringActiveConnection(
ServersUiController.processedServerId,
ServersUiController.processedContainerIndex,
clientId)) {
var isActiveConfigForCurrentClient = ServersUiController.isDefaultServerCurrentlyProcessed()
&& ServersUiController.serverDefaultContainer(ServersUiController.defaultServerId) === ServersUiController.processedContainerIndex
if ((ConnectionController.isConnectionInProgress || ConnectionController.isConnected)
&& isActiveConfigForCurrentClient) {
PageController.showNotificationMessage("Unable to revoke current config during active connection")
} else {
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)

View File

@@ -105,19 +105,6 @@ PageType {
}
}
Connections {
objectName: "connectionControllerConnections"
target: ConnectionController
function onNoInstalledContainers() {
PageController.setTriggeredByConnectButton(true)
ServersUiController.setProcessedServerId(ServersUiController.defaultServerId)
PageController.goToPage(PageEnum.PageSetupWizardEasy)
}
}
Connections {
objectName: "installControllerConnections"
@@ -166,19 +153,11 @@ PageType {
PageController.showNotificationMessage(finishedMessage)
}
function onRemoveAllContainersFinished(finishedMessage) {
if (tabBarStackView.currentItem.objectName === PageController.getPagePath(PageEnum.PageDeinstalling)) {
PageController.closePage()
}
PageController.showNotificationMessage(finishedMessage)
}
function onNoInstalledContainers() {
PageController.setTriggeredByConnectButton(true)
function onRemoveContainerFinished(finishedMessage) {
if (tabBarStackView.currentItem.objectName === PageController.getPagePath(PageEnum.PageDeinstalling)) {
PageController.closePage()
}
PageController.closePage()
PageController.showNotificationMessage(finishedMessage)
ServersUiController.setProcessedServerId(ServersUiController.defaultServerId)
PageController.goToPage(PageEnum.PageSetupWizardEasy)
}
}

View File

@@ -234,8 +234,6 @@ Window {
DrawerType2 {
id: privateKeyPassphraseDrawer
property bool isCloseByUser: false
anchors.fill: parent
expandedHeight: root.height * 0.35 + PageController.safeAreaBottomMargin + PageController.imeHeight
@@ -255,11 +253,6 @@ Window {
}
function onAboutToHide() {
if (privateKeyPassphraseDrawer.isCloseByUser === false) {
privateKeyPassphraseDrawer.isCloseByUser = true
PageController.passphraseRequestDrawerClosed("")
}
if (passphrase.textField.text !== "") {
PageController.showBusyIndicator(true)
}
@@ -300,7 +293,6 @@ Window {
text: qsTr("Save")
clickedFunc: function() {
privateKeyPassphraseDrawer.isCloseByUser = true
privateKeyPassphraseDrawer.closeTriggered()
PageController.passphraseRequestDrawerClosed(passphrase.textField.text)
}

View File

@@ -106,8 +106,11 @@
<file>Pages2/PageSettingsKillSwitch.qml</file>
<file>Pages2/PageSettingsKillSwitchExceptions.qml</file>
<file>Pages2/PageSettingsLogging.qml</file>
<file>Pages2/PageSettingsServerBackup.qml</file>
<file>Pages2/PageSettingsServerBackupRestored.qml</file>
<file>Pages2/PageSettingsServerData.qml</file>
<file>Pages2/PageSettingsServerInfo.qml</file>
<file>Pages2/PageSettingsServerRestoreMode.qml</file>
<file>Pages2/PageSettingsServerProtocol.qml</file>
<file>Pages2/PageSettingsServerProtocols.qml</file>
<file>Pages2/PageSettingsServerServices.qml</file>

View File

@@ -19,12 +19,12 @@ class AmneziaVPN(ConanFile):
if has_service:
if os == "Windows":
self.requires("awg-windows/0.1.9")
self.requires("awg-windows/0.1.8")
self.requires("tap-windows6/9.27.0")
self.requires("win-split-tunnel/1.2.5.0")
self.requires("wintun/0.14.1")
else:
self.requires("awg-go/0.2.18")
self.requires("awg-go/0.2.16")
self.requires("amnezia-xray-bindings/1.1.0")
self.requires("tun2socks/2.6.0")
@@ -32,13 +32,13 @@ class AmneziaVPN(ConanFile):
self.requires("v2ray-rules-dat/202603162227")
if has_ne:
self.requires("awg-apple/2.0.2")
self.requires("awg-apple/2.0.1")
self.requires("hev-socks5-tunnel/2.15.0", options={"as_framework": True})
self.requires("openvpnadapter/1.0.0")
if os == "Android":
self.requires("amnezia-libxray/1.0.0")
self.requires("awg-android/2.0.1")
self.requires("awg-android/1.1.7")
self.requires("openvpn-pt-android/1.0.0")
# expicitly use libssh@amnezia to prevent it from being downloaded from conan-center

View File

@@ -9,7 +9,7 @@ import platform
class AwgAndroid(ConanFile):
name = "awg-android"
version = "2.0.1"
version = "1.1.7"
settings = "os", "arch", "build_type", "compiler"
def configure(self):

View File

@@ -9,7 +9,7 @@ import os
class AwgApple(ConanFile):
name = "awg-apple"
version = "2.0.2"
version = "2.0.1"
settings = "os", "arch", "compiler"
@property
@@ -39,7 +39,7 @@ class AwgApple(ConanFile):
def source(self):
get(self, f"https://github.com/amnezia-vpn/amneziawg-apple/archive/refs/tags/v{self.version}.zip",
sha256="a04f49eac9f82bbf5dd9031bab188d44de2b3482efde1b6e970821de1d5a3c5d", strip_root=True
sha256="9fe4f8cfbb6a751558b54b7979db3a5ea46e49731912aae99f093e84a1433e97", strip_root=True
)
def generate(self):

View File

@@ -8,7 +8,7 @@ import os
class AwgGo(ConanFile):
name = "awg-go"
version = "0.2.18"
version = "0.2.16"
package_type = "application"
settings = "os", "arch"
@@ -42,7 +42,7 @@ class AwgGo(ConanFile):
def source(self):
get(self, f"https://github.com/amnezia-vpn/amneziawg-go/archive/refs/tags/v{self.version}.zip",
sha256="58eefbd012e79bd1525f0e02d748979e9480acc1a339df8ceb3b9ffafcedb1ba", strip_root=True
sha256="34da7d4189f215f3930de441548bc2a0c89d54d347a4fb85cb9c715fce6413aa", strip_root=True
)
def generate(self):

View File

@@ -8,7 +8,7 @@ import os
class AwgWindows(ConanFile):
name = "awg-windows"
version = "0.1.9"
version = "0.1.8"
settings = "os", "arch"
@property
@@ -63,7 +63,7 @@ class AwgWindows(ConanFile):
def source(self):
get(self, f"https://github.com/amnezia-vpn/amneziawg-windows/archive/refs/tags/v{self.version}.zip",
sha256="5c29a75cb2beae291cc51b64840a39f838da5f300b9e956f7964813a687ec74c", strip_root=True)
sha256="1de472832b332515c96cdf14ea887edde42ed7ad173675280c51baa9a3ef62f2", strip_root=True)
def generate(self):
tc = AutotoolsToolchain(self)

View File

@@ -650,9 +650,6 @@ class OpenSSLConan(ConanFile):
if self._use_nmake:
self.cpp_info.components["ssl"].libs = ["libssl"]
self.cpp_info.components["crypto"].libs = ["libcrypto"]
elif self.settings.os == "Android" and self.options.shared:
self.cpp_info.components["ssl"].libs = ["ssl_3"]
self.cpp_info.components["crypto"].libs = ["crypto_3"]
else:
self.cpp_info.components["ssl"].libs = ["ssl"]
self.cpp_info.components["crypto"].libs = ["crypto"]

View File

@@ -28,7 +28,7 @@ class Openvpn(ConanFile):
def build_requirements(self):
if self._is_windows:
self.tool_requires("cmake/[>=4.2]")
self.tool_requires("cmake/[>=3.14 <4]")
else:
self.tool_requires("libtool/2.4.7")
self.tool_requires("automake/1.16.5")