From 871037f887dae54493be6bf73b0c14c70a6f7ac7 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Sat, 25 May 2024 10:04:41 +0200 Subject: [PATCH 01/31] added changelog drawer --- client/amnezia_application.cpp | 8 + client/amnezia_application.h | 2 + client/core/controllers/apiController.cpp | 2 +- client/resources.qrc | 3 +- client/ui/controllers/pageController.h | 2 + client/ui/controllers/systemController.h | 2 +- client/ui/controllers/updateController.cpp | 149 +++++++++++++++++++ client/ui/controllers/updateController.h | 34 +++++ client/ui/qml/Components/ChangelogDrawer.qml | 119 +++++++++++++++ client/ui/qml/main2.qml | 14 ++ ipc/ipc_interface.rep | 2 + ipc/ipcserver.cpp | 97 ++++++------ ipc/ipcserver.h | 1 + 13 files changed, 384 insertions(+), 51 deletions(-) create mode 100644 client/ui/controllers/updateController.cpp create mode 100644 client/ui/controllers/updateController.h create mode 100644 client/ui/qml/Components/ChangelogDrawer.qml diff --git a/client/amnezia_application.cpp b/client/amnezia_application.cpp index 06d2f9acb..1bd196fd0 100644 --- a/client/amnezia_application.cpp +++ b/client/amnezia_application.cpp @@ -406,4 +406,12 @@ void AmneziaApplication::initControllers() m_systemController.reset(new SystemController(m_settings)); m_engine->rootContext()->setContextProperty("SystemController", m_systemController.get()); + + m_updateController.reset(new UpdateController(m_settings)); + m_engine->rootContext()->setContextProperty("UpdateController", m_updateController.get()); + m_updateController->checkForUpdates(); + + connect(m_updateController.get(), &UpdateController::updateFound, this, [this]() { + QTimer::singleShot(1000, this, [this]() { m_pageController->showChangelogDrawer(); }); + }); } diff --git a/client/amnezia_application.h b/client/amnezia_application.h index 5561d7c7e..395ed2378 100644 --- a/client/amnezia_application.h +++ b/client/amnezia_application.h @@ -24,6 +24,7 @@ #include "ui/controllers/sitesController.h" #include "ui/controllers/systemController.h" #include "ui/controllers/appSplitTunnelingController.h" +#include "ui/controllers/updateController.h" #include "ui/models/containers_model.h" #include "ui/models/languageModel.h" #include "ui/models/protocols/cloakConfigModel.h" @@ -130,6 +131,7 @@ class AmneziaApplication : public AMNEZIA_BASE_CLASS QScopedPointer m_sitesController; QScopedPointer m_systemController; QScopedPointer m_appSplitTunnelingController; + QScopedPointer m_updateController; QNetworkAccessManager *m_nam; }; diff --git a/client/core/controllers/apiController.cpp b/client/core/controllers/apiController.cpp index fa0fcaec3..ab8fd5d36 100644 --- a/client/core/controllers/apiController.cpp +++ b/client/core/controllers/apiController.cpp @@ -99,7 +99,7 @@ void ApiController::updateServerConfigFromApi(const QString &installationUuid, c QByteArray requestBody = QJsonDocument(apiPayload).toJson(); - QNetworkReply *reply = amnApp->manager()->post(request, requestBody); // ?? + QNetworkReply *reply = amnApp->manager()->post(request, requestBody); QObject::connect(reply, &QNetworkReply::finished, [this, reply, protocol, apiPayloadData, serverIndex, serverConfig]() mutable { if (reply->error() == QNetworkReply::NoError) { diff --git a/client/resources.qrc b/client/resources.qrc index 49fd66d35..8a42e5645 100644 --- a/client/resources.qrc +++ b/client/resources.qrc @@ -198,7 +198,7 @@ ui/qml/Pages2/PageProtocolOpenVpnSettings.qml ui/qml/Pages2/PageProtocolShadowSocksSettings.qml ui/qml/Pages2/PageProtocolCloakSettings.qml - ui/qml/Pages2/PageProtocolXraySettings.qml + ui/qml/Pages2/PageProtocolXraySettings.qml ui/qml/Pages2/PageProtocolRaw.qml ui/qml/Pages2/PageSettingsLogging.qml ui/qml/Pages2/PageServiceSftpSettings.qml @@ -239,5 +239,6 @@ images/controls/alert-circle.svg images/controls/file-check-2.svg ui/qml/Controls2/WarningType.qml + ui/qml/Components/ChangelogDrawer.qml diff --git a/client/ui/controllers/pageController.h b/client/ui/controllers/pageController.h index b286b1b1a..58454ef69 100644 --- a/client/ui/controllers/pageController.h +++ b/client/ui/controllers/pageController.h @@ -126,6 +126,8 @@ public slots: void forceTabBarActiveFocus(); void forceStackActiveFocus(); + void showChangelogDrawer(); + private: QSharedPointer m_serversModel; diff --git a/client/ui/controllers/systemController.h b/client/ui/controllers/systemController.h index 274df2349..7dbf89471 100644 --- a/client/ui/controllers/systemController.h +++ b/client/ui/controllers/systemController.h @@ -9,7 +9,7 @@ class SystemController : public QObject { Q_OBJECT public: - explicit SystemController(const std::shared_ptr &setting, QObject *parent = nullptr); + explicit SystemController(const std::shared_ptr &settings, QObject *parent = nullptr); static void saveFile(QString fileName, const QString &data); diff --git a/client/ui/controllers/updateController.cpp b/client/ui/controllers/updateController.cpp new file mode 100644 index 000000000..6bf6f9fd2 --- /dev/null +++ b/client/ui/controllers/updateController.cpp @@ -0,0 +1,149 @@ +#include "updateController.h" + +#include +#include +#include +#include + +#include "amnezia_application.h" +#include "core/errorstrings.h" +#include "version.h" + +namespace { +#ifdef Q_OS_MACOS + const QString installerPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN.dmg"; +#elif defined Q_OS_WINDOWS + const QString installerPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN.exe"; +#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) + const QString installerPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN.tar.zip"; +#endif +} + +UpdateController::UpdateController(const std::shared_ptr &settings, QObject *parent) : QObject(parent), m_settings(settings) +{ +} + +QString UpdateController::getHeaderText() +{ + return tr("New version released: %1 (%2)").arg(m_version, m_releaseDate); +} + +QString UpdateController::getChangelogText() +{ + return m_changelogText; +} + +void UpdateController::checkForUpdates() +{ + QNetworkRequest request; + request.setTransferTimeout(7000); + QString endpoint = "https://api.github.com/repos/amnezia-vpn/amnezia-client/releases/latest"; + request.setUrl(endpoint); + + QNetworkReply *reply = amnApp->manager()->get(request); + + QObject::connect(reply, &QNetworkReply::finished, [this, reply]() { + if (reply->error() == QNetworkReply::NoError) { + QString contents = QString::fromUtf8(reply->readAll()); + QJsonObject data = QJsonDocument::fromJson(contents.toUtf8()).object(); + m_version = data.value("tag_name").toString(); + + auto currentVersion = QVersionNumber::fromString(QString(APP_VERSION)); + qDebug() << currentVersion; + auto newVersion = QVersionNumber::fromString(m_version); + if (newVersion > currentVersion) { + m_changelogText = data.value("body").toString(); + + QString dateString = data.value("published_at").toString(); + QDateTime dateTime = QDateTime::fromString(dateString, "yyyy-MM-ddTHH:mm:ssZ"); + m_releaseDate = dateTime.toString("MMM dd yyyy"); + + QJsonArray assets = data.value("assets").toArray(); + + for (auto asset : assets) { + QJsonObject assetObject = asset.toObject(); + if (assetObject.value("name").toString().contains(".dmg")) { + m_downloadUrl = assetObject.value("browser_download_url").toString(); + } + } + + emit updateFound(); + } + } else { + if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError + || reply->error() == QNetworkReply::NetworkError::TimeoutError) { + qDebug() << errorString(ErrorCode::ApiConfigTimeoutError); + } else { + QString err = reply->errorString(); + qDebug() << QString::fromUtf8(reply->readAll()); + qDebug() << reply->error(); + qDebug() << err; + qDebug() << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); + qDebug() << errorString(ErrorCode::ApiConfigDownloadError); + } + } + + reply->deleteLater(); + }); + + QObject::connect(reply, &QNetworkReply::errorOccurred, + [this, reply](QNetworkReply::NetworkError error) { qDebug() << reply->errorString() << error; }); + connect(reply, &QNetworkReply::sslErrors, [this, reply](const QList &errors) { + qDebug().noquote() << errors; + qDebug() << errorString(ErrorCode::ApiConfigSslError); + }); +} + +void UpdateController::runInstaller() +{ + QNetworkRequest request; + request.setTransferTimeout(7000); + request.setUrl(m_downloadUrl); + + QNetworkReply *reply = amnApp->manager()->get(request); + + QObject::connect(reply, &QNetworkReply::finished, [this, reply]() { + if (reply->error() == QNetworkReply::NoError) { + QFile file(installerPath); + if (file.open(QIODevice::WriteOnly)) { + file.write(reply->readAll()); + file.close(); + + QFutureWatcher watcher; + QFuture future = QtConcurrent::run([this]() { + QString t = installerPath; + QRemoteObjectPendingReply ipcReply = IpcClient::Interface()->mountDmg(t, true); + ipcReply.waitForFinished(); + QProcess::execute("/Volumes/AmneziaVPN/AmneziaVPN.app/Contents/MacOS/AmneziaVPN"); + ipcReply = IpcClient::Interface()->mountDmg(t, false); + ipcReply.waitForFinished(); + return ipcReply.returnValue(); + }); + + QEventLoop wait; + connect(&watcher, &QFutureWatcher::finished, &wait, &QEventLoop::quit); + watcher.setFuture(future); + wait.exec(); + + qDebug() << future.result(); + +// emit errorOccured(""); + } + } else { + if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError + || reply->error() == QNetworkReply::NetworkError::TimeoutError) { + qDebug() << errorString(ErrorCode::ApiConfigTimeoutError); + } else { + QString err = reply->errorString(); + qDebug() << QString::fromUtf8(reply->readAll()); + qDebug() << reply->error(); + qDebug() << err; + qDebug() << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); + qDebug() << errorString(ErrorCode::ApiConfigDownloadError); + } + } + + reply->deleteLater(); + }); + +} diff --git a/client/ui/controllers/updateController.h b/client/ui/controllers/updateController.h new file mode 100644 index 000000000..986174acf --- /dev/null +++ b/client/ui/controllers/updateController.h @@ -0,0 +1,34 @@ +#ifndef UPDATECONTROLLER_H +#define UPDATECONTROLLER_H + +#include + +#include "settings.h" + +class UpdateController : public QObject +{ + Q_OBJECT +public: + explicit UpdateController(const std::shared_ptr &settings, QObject *parent = nullptr); + + Q_PROPERTY(QString changelogText READ getChangelogText NOTIFY updateFound) + Q_PROPERTY(QString headerText READ getHeaderText NOTIFY updateFound) +public slots: + QString getHeaderText(); + QString getChangelogText(); + + void checkForUpdates(); + void runInstaller(); +signals: + void updateFound(); + void errorOccured(const QString &errorMessage); +private: + std::shared_ptr m_settings; + + QString m_changelogText; + QString m_version; + QString m_releaseDate; + QString m_downloadUrl; +}; + +#endif // UPDATECONTROLLER_H diff --git a/client/ui/qml/Components/ChangelogDrawer.qml b/client/ui/qml/Components/ChangelogDrawer.qml new file mode 100644 index 000000000..c2eae80e5 --- /dev/null +++ b/client/ui/qml/Components/ChangelogDrawer.qml @@ -0,0 +1,119 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import "../Controls2" +import "../Controls2/TextTypes" + +import "../Config" + +DrawerType2 { + id: root + + anchors.fill: parent + expandedHeight: parent.height * 0.9 + + expandedContent: Item { + implicitHeight: root.expandedHeight + + Connections { + target: root + enabled: !GC.isMobile() + function onOpened() { + focusItem.forceActiveFocus() + } + } + + Header2TextType { + id: header + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 16 + anchors.rightMargin: 16 + anchors.leftMargin: 16 + + text: UpdateController.headerText + } + + FlickableType { + anchors.top: header.bottom + anchors.bottom: updateButton.top + contentHeight: changelog.height + 32 + + ParagraphTextType { + id: changelog + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 48 + anchors.rightMargin: 16 + anchors.leftMargin: 16 + + HoverHandler { + enabled: parent.hoveredLink + cursorShape: Qt.PointingHandCursor + } + + onLinkActivated: function(link) { + Qt.openUrlExternally(link) + } + + text: UpdateController.changelogText + textFormat: Text.MarkdownText + } + } + + Item { + id: focusItem + KeyNavigation.tab: updateButton + } + + BasicButtonType { + id: updateButton + anchors.bottom: skipButton.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 16 + anchors.bottomMargin: 8 + anchors.rightMargin: 16 + anchors.leftMargin: 16 + + text: qsTr("Update") + + clickedFunc: function() { + PageController.showBusyIndicator(true) + UpdateController.runInstaller() + PageController.showBusyIndicator(false) + root.close() + } + + KeyNavigation.tab: skipButton + } + + BasicButtonType { + id: skipButton + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottomMargin: 16 + anchors.rightMargin: 16 + anchors.leftMargin: 16 + + defaultColor: "transparent" + hoveredColor: Qt.rgba(1, 1, 1, 0.08) + pressedColor: Qt.rgba(1, 1, 1, 0.12) + disabledColor: "#878B91" + textColor: "#D7D8DB" + borderWidth: 1 + + text: qsTr("Skip this version") + + clickedFunc: function() { + root.close() + } + + KeyNavigation.tab: focusItem + } + } +} diff --git a/client/ui/qml/main2.qml b/client/ui/qml/main2.qml index 7e31bb09d..a366fd2db 100644 --- a/client/ui/qml/main2.qml +++ b/client/ui/qml/main2.qml @@ -92,6 +92,10 @@ Window { busyIndicator.visible = visible PageController.disableControls(visible) } + + function onShowChangelogDrawer() { + changelogDrawer.open() + } } Connections { @@ -264,4 +268,14 @@ Window { onAccepted: SystemController.fileDialogClosed(true) onRejected: SystemController.fileDialogClosed(false) } + + Item { + anchors.fill: parent + + ChangelogDrawer { + id: changelogDrawer + + anchors.fill: parent + } + } } diff --git a/ipc/ipc_interface.rep b/ipc/ipc_interface.rep index 79f2d0422..7b49b8b79 100644 --- a/ipc/ipc_interface.rep +++ b/ipc/ipc_interface.rep @@ -32,5 +32,7 @@ class IpcInterface SLOT( bool enablePeerTraffic( const QJsonObject &configStr) ); SLOT( bool enableKillSwitch( const QJsonObject &excludeAddr, int vpnAdapterIndex) ); SLOT( bool updateResolvers(const QString& ifname, const QList& resolvers) ); + + SLOT( int mountDmg(const QString &path, bool mount) ); }; diff --git a/ipc/ipcserver.cpp b/ipc/ipcserver.cpp index c734912b1..9b72a5534 100644 --- a/ipc/ipcserver.cpp +++ b/ipc/ipcserver.cpp @@ -1,32 +1,33 @@ #include "ipcserver.h" -#include #include -#include #include +#include +#include +#include -#include "router.h" #include "logger.h" +#include "router.h" #include "../client/protocols/protocols_defs.h" #ifdef Q_OS_WIN -#include "tapcontroller_win.h" -#include "../client/platforms/windows/daemon/windowsfirewall.h" -#include "../client/platforms/windows/daemon/windowsdaemon.h" + #include "../client/platforms/windows/daemon/windowsdaemon.h" + #include "../client/platforms/windows/daemon/windowsfirewall.h" + #include "tapcontroller_win.h" #endif #ifdef Q_OS_LINUX -#include "../client/platforms/linux/daemon/linuxfirewall.h" + #include "../client/platforms/linux/daemon/linuxfirewall.h" #endif #ifdef Q_OS_MACOS -#include "../client/platforms/macos/daemon/macosfirewall.h" + #include "../client/platforms/macos/daemon/macosfirewall.h" #endif -IpcServer::IpcServer(QObject *parent): - IpcInterfaceSource(parent) +IpcServer::IpcServer(QObject *parent) : IpcInterfaceSource(parent) -{} +{ +} int IpcServer::createPrivilegedProcess() { @@ -58,23 +59,20 @@ int IpcServer::createPrivilegedProcess() } }); - QObject::connect(pd.serverNode.data(), &QRemoteObjectHost::error, this, [pd](QRemoteObjectNode::ErrorCode errorCode) { - qDebug() << "QRemoteObjectHost::error" << errorCode; - }); + QObject::connect(pd.serverNode.data(), &QRemoteObjectHost::error, this, + [pd](QRemoteObjectNode::ErrorCode errorCode) { qDebug() << "QRemoteObjectHost::error" << errorCode; }); - QObject::connect(pd.serverNode.data(), &QRemoteObjectHost::destroyed, this, [pd]() { - qDebug() << "QRemoteObjectHost::destroyed"; - }); + QObject::connect(pd.serverNode.data(), &QRemoteObjectHost::destroyed, this, [pd]() { qDebug() << "QRemoteObjectHost::destroyed"; }); -// connect(pd.ipcProcess.data(), &IpcServerProcess::finished, this, [this, pid=m_localpid](int exitCode, QProcess::ExitStatus exitStatus){ -// qDebug() << "IpcServerProcess finished" << exitCode << exitStatus; -//// if (m_processes.contains(pid)) { -//// m_processes[pid].ipcProcess.reset(); -//// m_processes[pid].serverNode.reset(); -//// m_processes[pid].localServer.reset(); -//// m_processes.remove(pid); -//// } -// }); + // connect(pd.ipcProcess.data(), &IpcServerProcess::finished, this, [this, pid=m_localpid](int exitCode, QProcess::ExitStatus exitStatus){ + // qDebug() << "IpcServerProcess finished" << exitCode << exitStatus; + //// if (m_processes.contains(pid)) { + //// m_processes[pid].ipcProcess.reset(); + //// m_processes[pid].serverNode.reset(); + //// m_processes[pid].localServer.reset(); + //// m_processes.remove(pid); + //// } + // }); m_processes.insert(m_localpid, pd); @@ -105,7 +103,7 @@ bool IpcServer::routeDeleteList(const QString &gw, const QStringList &ips) qDebug() << "IpcServer::routeDeleteList"; #endif - return Router::routeDeleteList(gw ,ips); + return Router::routeDeleteList(gw, ips); } void IpcServer::flushDns() @@ -172,7 +170,7 @@ bool IpcServer::deleteTun(const QString &dev) return Router::deleteTun(dev); } -bool IpcServer::updateResolvers(const QString& ifname, const QList& resolvers) +bool IpcServer::updateResolvers(const QString &ifname, const QList &resolvers) { return Router::updateResolvers(ifname, resolvers); } @@ -194,13 +192,11 @@ void IpcServer::setLogsEnabled(bool enabled) if (enabled) { Logger::init(); - } - else { + } else { Logger::deinit(); } } - bool IpcServer::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterIndex) { #ifdef Q_OS_WIN @@ -216,13 +212,11 @@ bool IpcServer::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterInd QStringList allownets; QStringList blocknets; - if (splitTunnelType == 0) - { + if (splitTunnelType == 0) { blockAll = true; allowNets = true; allownets.append(configStr.value(amnezia::config_key::hostName).toString()); - } else if (splitTunnelType == 1) - { + } else if (splitTunnelType == 1) { blockNets = true; for (auto v : splitTunnelSites) { blocknets.append(v.toString()); @@ -264,18 +258,17 @@ bool IpcServer::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterInd // double-check + ensure our firewall is installed and enabled. This is necessary as // other software may disable pfctl before re-enabling with their own rules (e.g other VPNs) - if (!MacOSFirewall::isInstalled()) MacOSFirewall::install(); + if (!MacOSFirewall::isInstalled()) + MacOSFirewall::install(); MacOSFirewall::ensureRootAnchorPriority(); MacOSFirewall::setAnchorEnabled(QStringLiteral("000.allowLoopback"), true); MacOSFirewall::setAnchorEnabled(QStringLiteral("100.blockAll"), blockAll); MacOSFirewall::setAnchorEnabled(QStringLiteral("110.allowNets"), allowNets); - MacOSFirewall::setAnchorTable(QStringLiteral("110.allowNets"), allowNets, - QStringLiteral("allownets"), allownets); + MacOSFirewall::setAnchorTable(QStringLiteral("110.allowNets"), allowNets, QStringLiteral("allownets"), allownets); MacOSFirewall::setAnchorEnabled(QStringLiteral("120.blockNets"), blockNets); - MacOSFirewall::setAnchorTable(QStringLiteral("120.blockNets"), blockNets, - QStringLiteral("blocknets"), blocknets); + MacOSFirewall::setAnchorTable(QStringLiteral("120.blockNets"), blockNets, QStringLiteral("blocknets"), blocknets); MacOSFirewall::setAnchorEnabled(QStringLiteral("200.allowVPN"), true); MacOSFirewall::setAnchorEnabled(QStringLiteral("250.blockIPv6"), true); MacOSFirewall::setAnchorEnabled(QStringLiteral("290.allowDHCP"), true); @@ -326,10 +319,8 @@ bool IpcServer::enablePeerTraffic(const QJsonObject &configStr) // Use APP split tunnel if (splitTunnelType == 0 || splitTunnelType == 2) { - config.m_allowedIPAddressRanges.append( - IPAddress(QHostAddress("0.0.0.0"), 0)); - config.m_allowedIPAddressRanges.append( - IPAddress(QHostAddress("::"), 0)); + config.m_allowedIPAddressRanges.append(IPAddress(QHostAddress("0.0.0.0"), 0)); + config.m_allowedIPAddressRanges.append(IPAddress(QHostAddress("::"), 0)); } if (splitTunnelType == 1) { @@ -337,10 +328,9 @@ bool IpcServer::enablePeerTraffic(const QJsonObject &configStr) QString ipRange = v.toString(); if (ipRange.split('/').size() > 1) { config.m_allowedIPAddressRanges.append( - IPAddress(QHostAddress(ipRange.split('/')[0]), atoi(ipRange.split('/')[1].toLocal8Bit()))); + IPAddress(QHostAddress(ipRange.split('/')[0]), atoi(ipRange.split('/')[1].toLocal8Bit()))); } else { - config.m_allowedIPAddressRanges.append( - IPAddress(QHostAddress(ipRange), 32)); + config.m_allowedIPAddressRanges.append(IPAddress(QHostAddress(ipRange), 32)); } } } @@ -353,7 +343,7 @@ bool IpcServer::enablePeerTraffic(const QJsonObject &configStr) } } - for (const QJsonValue& i : configStr.value(amnezia::config_key::splitTunnelApps).toArray()) { + for (const QJsonValue &i : configStr.value(amnezia::config_key::splitTunnelApps).toArray()) { if (!i.isString()) { break; } @@ -371,3 +361,14 @@ bool IpcServer::enablePeerTraffic(const QJsonObject &configStr) #endif return true; } + +int IpcServer::mountDmg(const QString &path, bool mount) +{ +#ifdef Q_OS_MACOS + qDebug() << path; + auto res = QProcess::execute(QString("sudo hdiutil %1 %2").arg(mount ? "attach" : "unmount", path)); + qDebug() << res; + return res; +#endif + return 0; +} diff --git a/ipc/ipcserver.h b/ipc/ipcserver.h index bd474481c..21e2a591c 100644 --- a/ipc/ipcserver.h +++ b/ipc/ipcserver.h @@ -35,6 +35,7 @@ class IpcServer : public IpcInterfaceSource virtual bool enableKillSwitch(const QJsonObject &excludeAddr, int vpnAdapterIndex) override; virtual bool disableKillSwitch() override; virtual bool updateResolvers(const QString& ifname, const QList& resolvers) override; + virtual int mountDmg(const QString &path, bool mount) override; private: int m_localpid = 0; From efdd47a63da92d19bcfee0f781a90e5df750eddc Mon Sep 17 00:00:00 2001 From: aiamnezia Date: Thu, 28 Nov 2024 11:36:50 +0400 Subject: [PATCH 02/31] Created a scaffold for Linux installation --- client/ui/controllers/updateController.cpp | 11 ++++++----- ipc/ipc_interface.rep | 1 + ipc/ipcserver.cpp | 8 ++++++++ ipc/ipcserver.h | 1 + 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/client/ui/controllers/updateController.cpp b/client/ui/controllers/updateController.cpp index 6bf6f9fd2..dfabd7cdf 100644 --- a/client/ui/controllers/updateController.cpp +++ b/client/ui/controllers/updateController.cpp @@ -62,7 +62,7 @@ void UpdateController::checkForUpdates() for (auto asset : assets) { QJsonObject assetObject = asset.toObject(); - if (assetObject.value("name").toString().contains(".dmg")) { + if (assetObject.value("name").toString().contains(".tar.gz")) { m_downloadUrl = assetObject.value("browser_download_url").toString(); } } @@ -112,10 +112,11 @@ void UpdateController::runInstaller() QFutureWatcher watcher; QFuture future = QtConcurrent::run([this]() { QString t = installerPath; - QRemoteObjectPendingReply ipcReply = IpcClient::Interface()->mountDmg(t, true); - ipcReply.waitForFinished(); - QProcess::execute("/Volumes/AmneziaVPN/AmneziaVPN.app/Contents/MacOS/AmneziaVPN"); - ipcReply = IpcClient::Interface()->mountDmg(t, false); + QRemoteObjectPendingReply ipcReply = IpcClient::Interface()->installApp(t); + // QRemoteObjectPendingReply ipcReply = IpcClient::Interface()->mountDmg(t, true); + // ipcReply.waitForFinished(); + // QProcess::execute("/Volumes/AmneziaVPN/AmneziaVPN.app/Contents/MacOS/AmneziaVPN"); + // ipcReply = IpcClient::Interface()->mountDmg(t, false); ipcReply.waitForFinished(); return ipcReply.returnValue(); }); diff --git a/ipc/ipc_interface.rep b/ipc/ipc_interface.rep index 1647ea190..7dad63bd4 100644 --- a/ipc/ipc_interface.rep +++ b/ipc/ipc_interface.rep @@ -34,5 +34,6 @@ class IpcInterface SLOT( bool updateResolvers(const QString& ifname, const QList& resolvers) ); SLOT( int mountDmg(const QString &path, bool mount) ); + SLOT (int installApp(const QString &path)); }; diff --git a/ipc/ipcserver.cpp b/ipc/ipcserver.cpp index 2565fc996..c4fe804ea 100644 --- a/ipc/ipcserver.cpp +++ b/ipc/ipcserver.cpp @@ -377,3 +377,11 @@ int IpcServer::mountDmg(const QString &path, bool mount) #endif return 0; } + +int IpcServer::installApp(const QString &path) +{ +#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) + return QProcess::execute(QString("sudo dpkg -i %1").arg(path)); +#endif + return 0; +} diff --git a/ipc/ipcserver.h b/ipc/ipcserver.h index 5d8b61a25..7e5b21d11 100644 --- a/ipc/ipcserver.h +++ b/ipc/ipcserver.h @@ -39,6 +39,7 @@ class IpcServer : public IpcInterfaceSource virtual bool disableKillSwitch() override; virtual bool updateResolvers(const QString& ifname, const QList& resolvers) override; virtual int mountDmg(const QString &path, bool mount) override; + virtual int installApp(const QString &path) override; private: int m_localpid = 0; From 99f610edf91eca0579c83968ab46ea776f67b89a Mon Sep 17 00:00:00 2001 From: aiamnezia Date: Fri, 29 Nov 2024 19:20:15 +0400 Subject: [PATCH 03/31] implement Linux updating --- client/ui/controllers/updateController.cpp | 27 ++------- ipc/ipcserver.cpp | 65 +++++++++++++++++++++- 2 files changed, 70 insertions(+), 22 deletions(-) diff --git a/client/ui/controllers/updateController.cpp b/client/ui/controllers/updateController.cpp index dfabd7cdf..32aed926a 100644 --- a/client/ui/controllers/updateController.cpp +++ b/client/ui/controllers/updateController.cpp @@ -62,7 +62,7 @@ void UpdateController::checkForUpdates() for (auto asset : assets) { QJsonObject assetObject = asset.toObject(); - if (assetObject.value("name").toString().contains(".tar.gz")) { + if (assetObject.value("name").toString().contains(".tar.zip")) { m_downloadUrl = assetObject.value("browser_download_url").toString(); } } @@ -108,27 +108,12 @@ void UpdateController::runInstaller() if (file.open(QIODevice::WriteOnly)) { file.write(reply->readAll()); file.close(); + QString t = installerPath; + auto ipcReply = IpcClient::Interface()->installApp(t); + ipcReply.waitForFinished(); + int result = ipcReply.returnValue(); - QFutureWatcher watcher; - QFuture future = QtConcurrent::run([this]() { - QString t = installerPath; - QRemoteObjectPendingReply ipcReply = IpcClient::Interface()->installApp(t); - // QRemoteObjectPendingReply ipcReply = IpcClient::Interface()->mountDmg(t, true); - // ipcReply.waitForFinished(); - // QProcess::execute("/Volumes/AmneziaVPN/AmneziaVPN.app/Contents/MacOS/AmneziaVPN"); - // ipcReply = IpcClient::Interface()->mountDmg(t, false); - ipcReply.waitForFinished(); - return ipcReply.returnValue(); - }); - - QEventLoop wait; - connect(&watcher, &QFutureWatcher::finished, &wait, &QEventLoop::quit); - watcher.setFuture(future); - wait.exec(); - - qDebug() << future.result(); - -// emit errorOccured(""); + // emit errorOccured(""); } } else { if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError diff --git a/ipc/ipcserver.cpp b/ipc/ipcserver.cpp index c4fe804ea..c6ca5f52d 100644 --- a/ipc/ipcserver.cpp +++ b/ipc/ipcserver.cpp @@ -381,7 +381,70 @@ int IpcServer::mountDmg(const QString &path, bool mount) int IpcServer::installApp(const QString &path) { #if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) - return QProcess::execute(QString("sudo dpkg -i %1").arg(path)); + QProcess process; + QString tempDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation); + QString extractDir = tempDir + "/amnezia_update"; + + qDebug() << "Installing app from:" << path; + qDebug() << "Using temp directory:" << extractDir; + + // Create extraction directory if it doesn't exist + QDir dir(extractDir); + if (!dir.exists()) { + dir.mkpath("."); + qDebug() << "Created extraction directory"; + } + + // First, extract the zip archive + qDebug() << "Extracting ZIP archive..."; + process.start("unzip", QStringList() << path << "-d" << extractDir); + process.waitForFinished(); + if (process.exitCode() != 0) { + qDebug() << "ZIP extraction error:" << process.readAllStandardError(); + return process.exitCode(); + } + qDebug() << "ZIP archive extracted successfully"; + + // Look for tar file in extracted files + qDebug() << "Looking for TAR file..."; + QDirIterator tarIt(extractDir, QStringList() << "*.tar", QDir::Files); + if (!tarIt.hasNext()) { + qDebug() << "TAR file not found in the extracted archive"; + return -1; + } + + // Extract found tar archive + QString tarPath = tarIt.next(); + qDebug() << "Found TAR file:" << tarPath; + qDebug() << "Extracting TAR archive..."; + + process.start("tar", QStringList() << "-xf" << tarPath << "-C" << extractDir); + process.waitForFinished(); + if (process.exitCode() != 0) { + qDebug() << "TAR extraction error:" << process.readAllStandardError(); + return process.exitCode(); + } + qDebug() << "TAR archive extracted successfully"; + + // Remove tar file as it's no longer needed + QFile::remove(tarPath); + qDebug() << "Removed temporary TAR file"; + + // Find executable file and run it + qDebug() << "Looking for executable file..."; + QDirIterator it(extractDir, QDir::Files | QDir::Executable, QDirIterator::Subdirectories); + if (it.hasNext()) { + QString execPath = it.next(); + qDebug() << "Found executable:" << execPath; + qDebug() << "Launching installer..."; + process.start("sudo", QStringList() << execPath); + process.waitForFinished(); + qDebug() << "Installer finished with exit code:" << process.exitCode(); + return process.exitCode(); + } + + qDebug() << "No executable file found"; + return -1; // Executable not found #endif return 0; } From 42e47684839eedd7774b06cdffc61c6c13ccdb6f Mon Sep 17 00:00:00 2001 From: aiamnezia Date: Wed, 4 Dec 2024 15:38:55 +0400 Subject: [PATCH 04/31] Add debug logs about installer in service --- ipc/ipcserver.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ipc/ipcserver.cpp b/ipc/ipcserver.cpp index c6ca5f52d..67650221c 100644 --- a/ipc/ipcserver.cpp +++ b/ipc/ipcserver.cpp @@ -439,6 +439,8 @@ int IpcServer::installApp(const QString &path) qDebug() << "Launching installer..."; process.start("sudo", QStringList() << execPath); process.waitForFinished(); + qDebug() << "Installer stdout:" << process.readAllStandardOutput(); + qDebug() << "Installer stderr:" << process.readAllStandardError(); qDebug() << "Installer finished with exit code:" << process.exitCode(); return process.exitCode(); } From 506f96c5d0405b35a0186dfdb4de3cbf8331a977 Mon Sep 17 00:00:00 2001 From: aiamnezia Date: Tue, 10 Dec 2024 17:43:25 +0400 Subject: [PATCH 05/31] Add client side of installation logic for Windows and MacOS --- client/ui/controllers/updateController.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/client/ui/controllers/updateController.cpp b/client/ui/controllers/updateController.cpp index 32aed926a..45acf1908 100644 --- a/client/ui/controllers/updateController.cpp +++ b/client/ui/controllers/updateController.cpp @@ -62,9 +62,19 @@ void UpdateController::checkForUpdates() for (auto asset : assets) { QJsonObject assetObject = asset.toObject(); + #ifdef Q_OS_WINDOWS + if (assetObject.value("name").toString().endsWith(".exe")) { + m_downloadUrl = assetObject.value("browser_download_url").toString(); + } + #elif defined(Q_OS_MACOS) + if (assetObject.value("name").toString().endsWith(".dmg")) { + m_downloadUrl = assetObject.value("browser_download_url").toString(); + } + #elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) if (assetObject.value("name").toString().contains(".tar.zip")) { m_downloadUrl = assetObject.value("browser_download_url").toString(); } + #endif } emit updateFound(); From e748ac35c9cf8c7aafe77eb4a1b093e110f83f96 Mon Sep 17 00:00:00 2001 From: aiamnezia Date: Tue, 10 Dec 2024 18:14:34 +0400 Subject: [PATCH 06/31] Add service side of installation logic for Windows --- ipc/ipcserver.cpp | 82 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/ipc/ipcserver.cpp b/ipc/ipcserver.cpp index 67650221c..d02fe56aa 100644 --- a/ipc/ipcserver.cpp +++ b/ipc/ipcserver.cpp @@ -380,7 +380,87 @@ int IpcServer::mountDmg(const QString &path, bool mount) int IpcServer::installApp(const QString &path) { -#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) + qDebug() << "Installing app from:" << path; + +#ifdef Q_OS_WINDOWS + // On Windows, simply run the .exe file with administrator privileges + QProcess process; + process.setProgram("powershell.exe"); + process.setArguments(QStringList() + << "Start-Process" + << path + << "-Verb" + << "RunAs" + << "-Wait"); + + qDebug() << "Launching installer with elevated privileges..."; + process.start(); + process.waitForFinished(); + + if (process.exitCode() != 0) { + qDebug() << "Installation error:" << process.readAllStandardError(); + } + return process.exitCode(); + +#elif defined(Q_OS_MACOS) + // DRAFT + + QProcess process; + QString tempDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation); + QString mountPoint = tempDir + "/AmneziaVPN_mount"; + + // Create mount point + QDir dir(mountPoint); + if (!dir.exists()) { + dir.mkpath("."); + } + + // Mount DMG image + qDebug() << "Mounting DMG image..."; + process.start("hdiutil", QStringList() + << "attach" + << path + << "-mountpoint" + << mountPoint + << "-nobrowse"); + process.waitForFinished(); + + if (process.exitCode() != 0) { + qDebug() << "Failed to mount DMG:" << process.readAllStandardError(); + return process.exitCode(); + } + + // Look for .app bundle in mounted image + QDirIterator it(mountPoint, QStringList() << "*.app", QDir::Dirs); + if (!it.hasNext()) { + qDebug() << "No .app bundle found in DMG"; + return -1; + } + + QString appPath = it.next(); + QString targetPath = "/Applications/" + QFileInfo(appPath).fileName(); + + // Copy application to /Applications + qDebug() << "Copying app to Applications folder..."; + process.start("cp", QStringList() + << "-R" + << appPath + << targetPath); + process.waitForFinished(); + + // Unmount DMG + qDebug() << "Unmounting DMG..."; + process.start("hdiutil", QStringList() + << "detach" + << mountPoint); + process.waitForFinished(); + + if (process.exitCode() != 0) { + qDebug() << "Installation error:" << process.readAllStandardError(); + } + return process.exitCode(); + +#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) QProcess process; QString tempDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation); QString extractDir = tempDir + "/amnezia_update"; From 9aef463b603cf9e531ad9c208669bdf18a8f9d8a Mon Sep 17 00:00:00 2001 From: Pokamest Nikak Date: Fri, 6 Dec 2024 12:40:04 +0000 Subject: [PATCH 07/31] ru readme --- README_RU.md | 191 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 README_RU.md diff --git a/README_RU.md b/README_RU.md new file mode 100644 index 000000000..8b4539075 --- /dev/null +++ b/README_RU.md @@ -0,0 +1,191 @@ +# Amnezia VPN +## _The best client for self-hosted VPN_ + +[![Build Status](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml/badge.svg?branch=dev)](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev) +[![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/amnezia-vpn/amnezia-client) + +[Amnezia](https://amnezia.org) is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server. + +[![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png)](https://amnezia.org) + +### [Website](https://amnezia.org) | [Alt website link](https://storage.googleapis.com/kldscp/amnezia.org) | [Documentation](https://docs.amnezia.org) | [Troubleshooting](https://docs.amnezia.org/troubleshooting) + +> [!TIP] +> If the [Amnezia website](https://amnezia.org) is blocked in your region, you can use an [Alternative website link](https://storage.googleapis.com/kldscp/amnezia.org). + + + + +[All releases](https://github.com/amnezia-vpn/amnezia-client/releases) + +
+ + + +## Features + +- Very easy to use - enter your IP address, SSH login, password and Amnezia will automatically install VPN docker containers to your server and connect to the VPN. +- Classic VPN-protocols: OpenVPN, WireGuard and IKEv2 protocols. +- Protocols with traffic Masking (Obfuscation): OpenVPN over [Cloak](https://github.com/cbeuw/Cloak) plugin, Shadowsocks (OpenVPN over Shadowsocks), [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) and XRay. +- Split tunneling support - add any sites to the client to enable VPN only for them or add Apps (only for Android and Desktop). +- Windows, MacOS, Linux, Android, iOS releases. +- Support for AmneziaWG protocol configuration on [Keenetic beta firmware](https://docs.keenetic.com/ua/air/kn-1611/en/6319-latest-development-release.html#UUID-186c4108-5afd-c10b-f38a-cdff6c17fab3_section-idm33192196168192-improved). + +## Links + +- [https://amnezia.org](https://amnezia.org) - Project website | [Alternative link (mirror)](https://storage.googleapis.com/kldscp/amnezia.org) +- [https://docs.amnezia.org](https://docs.amnezia.org) - Documentation +- [https://www.reddit.com/r/AmneziaVPN](https://www.reddit.com/r/AmneziaVPN) - Reddit +- [https://t.me/amnezia_vpn_en](https://t.me/amnezia_vpn_en) - Telegram support channel (English) +- [https://t.me/amnezia_vpn_ir](https://t.me/amnezia_vpn_ir) - Telegram support channel (Farsi) +- [https://t.me/amnezia_vpn_mm](https://t.me/amnezia_vpn_mm) - Telegram support channel (Myanmar) +- [https://t.me/amnezia_vpn](https://t.me/amnezia_vpn) - Telegram support channel (Russian) +- [https://vpnpay.io/en/amnezia-premium/](https://vpnpay.io/en/amnezia-premium/) - Amnezia Premium + +## Tech + +AmneziaVPN uses several open-source projects to work: + +- [OpenSSL](https://www.openssl.org/) +- [OpenVPN](https://openvpn.net/) +- [Shadowsocks](https://shadowsocks.org/) +- [Qt](https://www.qt.io/) +- [LibSsh](https://libssh.org) - forked from Qt Creator +- and more... + +## Checking out the source code + +Make sure to pull all submodules after checking out the repo. + +```bash +git submodule update --init --recursive +``` + +## Development + +Want to contribute? Welcome! + +### Help with translations + +Download the most actual translation files. + +Go to ["Actions" tab](https://github.com/amnezia-vpn/amnezia-client/actions?query=is%3Asuccess+branch%3Adev), click on the first line. +Then scroll down to the "Artifacts" section and download "AmneziaVPN_translations". + +Unzip this file. +Each *.ts file contains strings for one corresponding language. + +Translate or correct some strings in one or multiple *.ts files and commit them back to this repository into the ``client/translations`` folder. +You can do it via a web-interface or any other method you're familiar with. + +### Building sources and deployment + +Check deploy folder for build scripts. + +### How to build an iOS app from source code on MacOS + +1. First, make sure you have [XCode](https://developer.apple.com/xcode/) installed, at least version 14 or higher. + +2. We use QT to generate the XCode project. We need QT version 6.6.2. Install QT for MacOS [here](https://doc.qt.io/qt-6/macos.html) or [QT Online Installer](https://www.qt.io/download-open-source). Required modules: + - MacOS + - iOS + - Qt 5 Compatibility Module + - Qt Shader Tools + - Additional Libraries: + - Qt Image Formats + - Qt Multimedia + - Qt Remote Objects + +3. Install CMake if required. We recommend CMake version 3.25. You can install CMake [here](https://cmake.org/download/) + +4. You also need to install go >= v1.16. If you don't have it installed already, +download go from the [official website](https://golang.org/dl/) or use Homebrew. +The latest version is recommended. Install gomobile +```bash +export PATH=$PATH:~/go/bin +go install golang.org/x/mobile/cmd/gomobile@latest +gomobile init +``` + +5. Build the project +```bash +export QT_BIN_DIR="/Qt//ios/bin" +export QT_MACOS_ROOT_DIR="/Qt//macos" +export QT_IOS_BIN=$QT_BIN_DIR +export PATH=$PATH:~/go/bin +mkdir build-ios +$QT_IOS_BIN/qt-cmake . -B build-ios -GXcode -DQT_HOST_PATH=$QT_MACOS_ROOT_DIR +``` +Replace PATH-TO-QT-FOLDER and QT-VERSION to your environment + + +If you get `gomobile: command not found` make sure to set PATH to the location +of the bin folder where gomobile was installed. Usually, it's in `GOPATH`. +```bash +export PATH=$(PATH):/path/to/GOPATH/bin +``` + +6. Open the XCode project. You can then run /test/archive/ship the app. + +If the build fails with the following error +``` +make: *** +[$(PROJECTDIR)/client/build/AmneziaVPN.build/Debug-iphoneos/wireguard-go-bridge/goroot/.prepared] +Error 1 +``` +Add a user-defined variable to both AmneziaVPN and WireGuardNetworkExtension targets' build settings with +key `PATH` and value `${PATH}/path/to/bin/folder/with/go/executable`, e.g. `${PATH}:/usr/local/go/bin`. + +if the above error persists on your M1 Mac, then most probably you need to install arch based CMake +``` +arch -arm64 brew install cmake +``` + +Build might fail with the "source files not found" error the first time you try it, because the modern XCode build system compiles dependencies in parallel, and some dependencies end up being built after the ones that +require them. In this case, simply restart the build. + +## How to build the Android app + +_Tested on Mac OS_ + +The Android app has the following requirements: +* JDK 11 +* Android platform SDK 33 +* CMake 3.25.0 + +After you have installed QT, QT Creator, and Android Studio, you need to configure QT Creator correctly. + +- Click in the top menu bar on `QT Creator` -> `Preferences` -> `Devices` and select the tab `Android`. +- Set path to JDK 11 +- Set path to Android SDK (`$ANDROID_HOME`) + +In case you get errors regarding missing SDK or 'SDK manager not running', you cannot fix them by correcting the paths. If you have some spare GBs on your disk, you can let QT Creator install all requirements by choosing an empty folder for `Android SDK location` and clicking on `Set Up SDK`. Be aware: This will install a second Android SDK and NDK on your machine!  +Double-check that the right CMake version is configured:  Click on `QT Creator` -> `Preferences` and click on the side menu on `Kits`. Under the center content view's `Kits` tab, you'll find an entry for `CMake Tool`. If the default selected CMake version is lower than 3.25.0, install on your system CMake >= 3.25.0 and choose `System CMake at ` from the drop-down list. If this entry is missing, you either have not installed CMake yet or QT Creator hasn't found the path to it. In that case, click in the preferences window on the side menu item `CMake`, then on the tab `Tools` in the center content view, and finally on the button `Add` to set the path to your installed CMake.  +Please make sure that you have selected Android Platform SDK 33 for your project: click in the main view's side menu on `Projects`, and on the left, you'll see a section `Build & Run` showing different Android build targets. You can select any of them, Amnezia VPN's project setup is designed in a way that all Android targets will be built. Click on the targets submenu item `Build` and scroll in the center content view to `Build Steps`. Click on `Details` at the end of the headline `Build Android APK` (the `Details` button might be hidden in case the QT Creator Window is not running in full screen!). Here we are: Choose `android-33` as `Android Build Platform SDK`. + +That's it! You should be ready to compile the project from QT Creator! + +### Development flow + +After you've hit the build button, QT-Creator copies the whole project to a folder in the repository parent directory. The folder should look something like `build-amnezia-client-Android_Qt__Clang_-`. +If you want to develop Amnezia VPNs Android components written in Kotlin, such as components using system APIs, you need to import the generated project in Android Studio with `build-amnezia-client-Android_Qt__Clang_-/client/android-build` as the projects root directory. While you should be able to compile the generated project from Android Studio, you cannot work directly in the repository's Android project. So whenever you are confident with your work in the generated project, you'll need to copy and paste the affected files to the corresponding path in the repository's Android project so that you can add and commit your changes! + +You may face compiling issues in QT Creator after you've worked in Android Studio on the generated project. Just do a `./gradlew clean` in the generated project's root directory (`/client/android-build/.`) and you should be good to go. + +## License + +GPL v3.0 + +## Donate + +Patreon: [https://www.patreon.com/amneziavpn](https://www.patreon.com/amneziavpn) + +Bitcoin: bc1q26eevjcg9j0wuyywd2e3uc9cs2w58lpkpjxq6p
+USDT BEP20: 0x6abD576765a826f87D1D95183438f9408C901bE4
+USDT TRC20: TELAitazF1MZGmiNjTcnxDjEiH5oe7LC9d
+XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3
+TON: UQDpU1CyKRmg7L8mNScKk9FRc2SlESuI7N-Hby4nX-CcVmns +## Acknowledgments + +This project is tested with BrowserStack. +We express our gratitude to [BrowserStack](https://www.browserstack.com) for supporting our project. From 086d6c4ae389c6b5ecbe9a4bfd30787b20837bff Mon Sep 17 00:00:00 2001 From: KsZnak Date: Fri, 6 Dec 2024 22:15:01 +0200 Subject: [PATCH 08/31] Update README_RU.md --- README_RU.md | 181 +++++++++------------------------------------------ 1 file changed, 30 insertions(+), 151 deletions(-) diff --git a/README_RU.md b/README_RU.md index 8b4539075..6ebdb97f7 100644 --- a/README_RU.md +++ b/README_RU.md @@ -1,182 +1,60 @@ # Amnezia VPN -## _The best client for self-hosted VPN_ +## _Лучший клиент для создания VPN на собственном сервере_ -[![Build Status](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml/badge.svg?branch=dev)](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev) -[![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/amnezia-vpn/amnezia-client) - -[Amnezia](https://amnezia.org) is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server. +[AmneziaVPN](https://amnezia.org) — это open sourse VPN-клиент, ключевая особенность которого заключается в возможности развернуть собственный VPN на вашем сервере. [![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png)](https://amnezia.org) -### [Website](https://amnezia.org) | [Alt website link](https://storage.googleapis.com/kldscp/amnezia.org) | [Documentation](https://docs.amnezia.org) | [Troubleshooting](https://docs.amnezia.org/troubleshooting) +### [Сайт](https://amnezia.org) | [Зеркало на сайт](https://storage.googleapis.com/kldscp/amnezia.org) | [Документация](https://docs.amnezia.org) | [Решение проблем](https://docs.amnezia.org/troubleshooting) > [!TIP] -> If the [Amnezia website](https://amnezia.org) is blocked in your region, you can use an [Alternative website link](https://storage.googleapis.com/kldscp/amnezia.org). +> Если [сайт Amnezia](https://amnezia.org) заблокирован в вашем регионе, вы можете воспользоваться [ссылкой на зеркало](https://storage.googleapis.com/kldscp/amnezia.org). -[All releases](https://github.com/amnezia-vpn/amnezia-client/releases) +[Все релизы](https://github.com/amnezia-vpn/amnezia-client/releases)
-## Features +## Особенности -- Very easy to use - enter your IP address, SSH login, password and Amnezia will automatically install VPN docker containers to your server and connect to the VPN. -- Classic VPN-protocols: OpenVPN, WireGuard and IKEv2 protocols. -- Protocols with traffic Masking (Obfuscation): OpenVPN over [Cloak](https://github.com/cbeuw/Cloak) plugin, Shadowsocks (OpenVPN over Shadowsocks), [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) and XRay. -- Split tunneling support - add any sites to the client to enable VPN only for them or add Apps (only for Android and Desktop). -- Windows, MacOS, Linux, Android, iOS releases. -- Support for AmneziaWG protocol configuration on [Keenetic beta firmware](https://docs.keenetic.com/ua/air/kn-1611/en/6319-latest-development-release.html#UUID-186c4108-5afd-c10b-f38a-cdff6c17fab3_section-idm33192196168192-improved). +- Простой в использовании — введите IP-адрес, SSH-логин и пароль, и Amnezia автоматически установит VPN-контейнеры Docker на ваш сервер и подключится к VPN. +- Классические VPN-протоколы: OpenVPN, WireGuard и IKEv2. +- Протоколы с маскировкой трафика (обфускацией): OpenVPN с плагином [Cloak](https://github.com/cbeuw/Cloak), Shadowsocks (OpenVPN over Shadowsocks), [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) and XRay. +- Поддержка Split Tunneling — добавляйте любые сайты или приложения в список, чтобы включить VPN только для них. +- Поддерживает платформы: Windows, MacOS, Linux, Android, iOS. +- Поддержка конфигурации протокола AmneziaWG на [бета-прошивке Keenetic](https://docs.keenetic.com/ua/air/kn-1611/en/6319-latest-development-release.html#UUID-186c4108-5afd-c10b-f38a-cdff6c17fab3_section-idm33192196168192-improved). -## Links +## Ссылки -- [https://amnezia.org](https://amnezia.org) - Project website | [Alternative link (mirror)](https://storage.googleapis.com/kldscp/amnezia.org) -- [https://docs.amnezia.org](https://docs.amnezia.org) - Documentation +- [https://amnezia.org](https://amnezia.org) - Веб-сайт проекта | [Альтернативная ссылка (зеркало)](https://storage.googleapis.com/kldscp/amnezia.org) +- [https://docs.amnezia.org](https://docs.amnezia.org) - Документация - [https://www.reddit.com/r/AmneziaVPN](https://www.reddit.com/r/AmneziaVPN) - Reddit -- [https://t.me/amnezia_vpn_en](https://t.me/amnezia_vpn_en) - Telegram support channel (English) -- [https://t.me/amnezia_vpn_ir](https://t.me/amnezia_vpn_ir) - Telegram support channel (Farsi) -- [https://t.me/amnezia_vpn_mm](https://t.me/amnezia_vpn_mm) - Telegram support channel (Myanmar) -- [https://t.me/amnezia_vpn](https://t.me/amnezia_vpn) - Telegram support channel (Russian) -- [https://vpnpay.io/en/amnezia-premium/](https://vpnpay.io/en/amnezia-premium/) - Amnezia Premium +- [https://t.me/amnezia_vpn_en](https://t.me/amnezia_vpn_en) - Канал поддржки в Telegram (Английский) +- [https://t.me/amnezia_vpn_ir](https://t.me/amnezia_vpn_ir) - Канал поддржки в Telegram (Фарси) +- [https://t.me/amnezia_vpn_mm](https://t.me/amnezia_vpn_mm) - Канал поддржки в Telegram (Мьянма) +- [https://t.me/amnezia_vpn](https://t.me/amnezia_vpn) - Канал поддржки в Telegram (Русский) +- [https://vpnpay.io/en/amnezia-premium/](https://vpnpay.io/en/amnezia-premium/) - Amnezia Premium | [Зеркало](https://storage.googleapis.com/kldscp/vpnpay.io/ru/amnezia-premium\) -## Tech +## Технологии -AmneziaVPN uses several open-source projects to work: +AmneziaVPN использует несколько проектов с открытым исходным кодом: - [OpenSSL](https://www.openssl.org/) - [OpenVPN](https://openvpn.net/) - [Shadowsocks](https://shadowsocks.org/) - [Qt](https://www.qt.io/) -- [LibSsh](https://libssh.org) - forked from Qt Creator -- and more... - -## Checking out the source code - -Make sure to pull all submodules after checking out the repo. - -```bash -git submodule update --init --recursive -``` - -## Development - -Want to contribute? Welcome! - -### Help with translations - -Download the most actual translation files. - -Go to ["Actions" tab](https://github.com/amnezia-vpn/amnezia-client/actions?query=is%3Asuccess+branch%3Adev), click on the first line. -Then scroll down to the "Artifacts" section and download "AmneziaVPN_translations". - -Unzip this file. -Each *.ts file contains strings for one corresponding language. - -Translate or correct some strings in one or multiple *.ts files and commit them back to this repository into the ``client/translations`` folder. -You can do it via a web-interface or any other method you're familiar with. - -### Building sources and deployment - -Check deploy folder for build scripts. - -### How to build an iOS app from source code on MacOS - -1. First, make sure you have [XCode](https://developer.apple.com/xcode/) installed, at least version 14 or higher. - -2. We use QT to generate the XCode project. We need QT version 6.6.2. Install QT for MacOS [here](https://doc.qt.io/qt-6/macos.html) or [QT Online Installer](https://www.qt.io/download-open-source). Required modules: - - MacOS - - iOS - - Qt 5 Compatibility Module - - Qt Shader Tools - - Additional Libraries: - - Qt Image Formats - - Qt Multimedia - - Qt Remote Objects - -3. Install CMake if required. We recommend CMake version 3.25. You can install CMake [here](https://cmake.org/download/) - -4. You also need to install go >= v1.16. If you don't have it installed already, -download go from the [official website](https://golang.org/dl/) or use Homebrew. -The latest version is recommended. Install gomobile -```bash -export PATH=$PATH:~/go/bin -go install golang.org/x/mobile/cmd/gomobile@latest -gomobile init -``` +- [LibSsh](https://libssh.org) +- и другие... -5. Build the project -```bash -export QT_BIN_DIR="/Qt//ios/bin" -export QT_MACOS_ROOT_DIR="/Qt//macos" -export QT_IOS_BIN=$QT_BIN_DIR -export PATH=$PATH:~/go/bin -mkdir build-ios -$QT_IOS_BIN/qt-cmake . -B build-ios -GXcode -DQT_HOST_PATH=$QT_MACOS_ROOT_DIR -``` -Replace PATH-TO-QT-FOLDER and QT-VERSION to your environment - - -If you get `gomobile: command not found` make sure to set PATH to the location -of the bin folder where gomobile was installed. Usually, it's in `GOPATH`. -```bash -export PATH=$(PATH):/path/to/GOPATH/bin -``` - -6. Open the XCode project. You can then run /test/archive/ship the app. - -If the build fails with the following error -``` -make: *** -[$(PROJECTDIR)/client/build/AmneziaVPN.build/Debug-iphoneos/wireguard-go-bridge/goroot/.prepared] -Error 1 -``` -Add a user-defined variable to both AmneziaVPN and WireGuardNetworkExtension targets' build settings with -key `PATH` and value `${PATH}/path/to/bin/folder/with/go/executable`, e.g. `${PATH}:/usr/local/go/bin`. - -if the above error persists on your M1 Mac, then most probably you need to install arch based CMake -``` -arch -arm64 brew install cmake -``` - -Build might fail with the "source files not found" error the first time you try it, because the modern XCode build system compiles dependencies in parallel, and some dependencies end up being built after the ones that -require them. In this case, simply restart the build. - -## How to build the Android app - -_Tested on Mac OS_ - -The Android app has the following requirements: -* JDK 11 -* Android platform SDK 33 -* CMake 3.25.0 - -After you have installed QT, QT Creator, and Android Studio, you need to configure QT Creator correctly. - -- Click in the top menu bar on `QT Creator` -> `Preferences` -> `Devices` and select the tab `Android`. -- Set path to JDK 11 -- Set path to Android SDK (`$ANDROID_HOME`) - -In case you get errors regarding missing SDK or 'SDK manager not running', you cannot fix them by correcting the paths. If you have some spare GBs on your disk, you can let QT Creator install all requirements by choosing an empty folder for `Android SDK location` and clicking on `Set Up SDK`. Be aware: This will install a second Android SDK and NDK on your machine!  -Double-check that the right CMake version is configured:  Click on `QT Creator` -> `Preferences` and click on the side menu on `Kits`. Under the center content view's `Kits` tab, you'll find an entry for `CMake Tool`. If the default selected CMake version is lower than 3.25.0, install on your system CMake >= 3.25.0 and choose `System CMake at ` from the drop-down list. If this entry is missing, you either have not installed CMake yet or QT Creator hasn't found the path to it. In that case, click in the preferences window on the side menu item `CMake`, then on the tab `Tools` in the center content view, and finally on the button `Add` to set the path to your installed CMake.  -Please make sure that you have selected Android Platform SDK 33 for your project: click in the main view's side menu on `Projects`, and on the left, you'll see a section `Build & Run` showing different Android build targets. You can select any of them, Amnezia VPN's project setup is designed in a way that all Android targets will be built. Click on the targets submenu item `Build` and scroll in the center content view to `Build Steps`. Click on `Details` at the end of the headline `Build Android APK` (the `Details` button might be hidden in case the QT Creator Window is not running in full screen!). Here we are: Choose `android-33` as `Android Build Platform SDK`. - -That's it! You should be ready to compile the project from QT Creator! - -### Development flow - -After you've hit the build button, QT-Creator copies the whole project to a folder in the repository parent directory. The folder should look something like `build-amnezia-client-Android_Qt__Clang_-`. -If you want to develop Amnezia VPNs Android components written in Kotlin, such as components using system APIs, you need to import the generated project in Android Studio with `build-amnezia-client-Android_Qt__Clang_-/client/android-build` as the projects root directory. While you should be able to compile the generated project from Android Studio, you cannot work directly in the repository's Android project. So whenever you are confident with your work in the generated project, you'll need to copy and paste the affected files to the corresponding path in the repository's Android project so that you can add and commit your changes! - -You may face compiling issues in QT Creator after you've worked in Android Studio on the generated project. Just do a `./gradlew clean` in the generated project's root directory (`/client/android-build/.`) and you should be good to go. - -## License +## Лицензия GPL v3.0 -## Donate +## Донаты Patreon: [https://www.patreon.com/amneziavpn](https://www.patreon.com/amneziavpn) @@ -185,7 +63,8 @@ USDT BEP20: 0x6abD576765a826f87D1D95183438f9408C901bE4
USDT TRC20: TELAitazF1MZGmiNjTcnxDjEiH5oe7LC9d
XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3
TON: UQDpU1CyKRmg7L8mNScKk9FRc2SlESuI7N-Hby4nX-CcVmns -## Acknowledgments -This project is tested with BrowserStack. -We express our gratitude to [BrowserStack](https://www.browserstack.com) for supporting our project. +## Благодарности + +Этот проект тестируется с помощью BrowserStack. +Мы выражаем благодарность [BrowserStack](https://www.browserstack.com) за поддержку нашего проекта. From 061c63d5bd8dd1ea275e70c28c6b45f61dd667e4 Mon Sep 17 00:00:00 2001 From: KsZnak Date: Sat, 7 Dec 2024 15:53:40 +0200 Subject: [PATCH 09/31] Add files via upload --- metadata/img-readme/download-website-ru.svg | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 metadata/img-readme/download-website-ru.svg diff --git a/metadata/img-readme/download-website-ru.svg b/metadata/img-readme/download-website-ru.svg new file mode 100644 index 000000000..386ae4fe5 --- /dev/null +++ b/metadata/img-readme/download-website-ru.svg @@ -0,0 +1,8 @@ + + + + + + + + From e20f8bead29b6199fa31bb9707f7fa4548da267f Mon Sep 17 00:00:00 2001 From: Nethius Date: Sun, 8 Dec 2024 08:14:22 +0300 Subject: [PATCH 10/31] chore: added clang-format config files (#1293) --- .clang-format | 39 +++++++++++++++++++++++++++++++++++++++ .clang-format-ignore | 20 ++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 .clang-format create mode 100644 .clang-format-ignore diff --git a/.clang-format b/.clang-format new file mode 100644 index 000000000..5c459fd2f --- /dev/null +++ b/.clang-format @@ -0,0 +1,39 @@ +BasedOnStyle: WebKit +AccessModifierOffset: '-4' +AlignAfterOpenBracket: Align +AlignConsecutiveMacros: 'true' +AlignTrailingComments: 'true' +AllowAllArgumentsOnNextLine: 'true' +AllowAllParametersOfDeclarationOnNextLine: 'true' +AllowShortBlocksOnASingleLine: 'false' +AllowShortCaseLabelsOnASingleLine: 'true' +AllowShortEnumsOnASingleLine: 'false' +AllowShortFunctionsOnASingleLine: None +AlwaysBreakTemplateDeclarations: 'No' +BreakBeforeBinaryOperators: NonAssignment +BreakBeforeBraces: Custom +BraceWrapping: + AfterClass: true + AfterControlStatement: false + AfterEnum: false + AfterFunction: true + AfterNamespace: true + AfterObjCDeclaration: false + AfterStruct: true + AfterUnion: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false +BreakConstructorInitializers: BeforeColon +ColumnLimit: '120' +CommentPragmas: '"^!|^:"' +ConstructorInitializerAllOnOneLineOrOnePerLine: 'true' +ConstructorInitializerIndentWidth: '4' +ContinuationIndentWidth: '8' +IndentPPDirectives: BeforeHash +NamespaceIndentation: All +PenaltyExcessCharacter: '10' +PointerAlignment: Right +SortIncludes: 'true' +SpaceAfterTemplateKeyword: 'false' +Standard: Auto diff --git a/.clang-format-ignore b/.clang-format-ignore new file mode 100644 index 000000000..4019357f0 --- /dev/null +++ b/.clang-format-ignore @@ -0,0 +1,20 @@ +/client/3rd +/client/3rd-prebuild +/client/android +/client/cmake +/client/core/serialization +/client/daemon +/client/fonts +/client/images +/client/ios +/client/mozilla +/client/platforms/dummy +/client/platforms/linux +/client/platforms/macos +/client/platforms/windows +/client/server_scripts +/client/translations +/deploy +/docs +/metadata +/service/src From 1858bb9f8522f393b0abc83b3da5f807fdff5fbb Mon Sep 17 00:00:00 2001 From: KsZnak Date: Sun, 8 Dec 2024 05:49:26 +0200 Subject: [PATCH 11/31] Update README_RU.md --- README_RU.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README_RU.md b/README_RU.md index 6ebdb97f7..fe9dd2860 100644 --- a/README_RU.md +++ b/README_RU.md @@ -1,6 +1,11 @@ # Amnezia VPN -## _Лучший клиент для создания VPN на собственном сервере_ +### _Лучший клиент для создания VPN на собственном сервере_ + +[![Build Status](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml/badge.svg?branch=dev)](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev) +[![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/amnezia-vpn/amnezia-client) + +### [English](https://github.com/amnezia-vpn/amnezia-client/blob/dev/README.md) | Русский [AmneziaVPN](https://amnezia.org) — это open sourse VPN-клиент, ключевая особенность которого заключается в возможности развернуть собственный VPN на вашем сервере. [![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png)](https://amnezia.org) @@ -10,8 +15,8 @@ > [!TIP] > Если [сайт Amnezia](https://amnezia.org) заблокирован в вашем регионе, вы можете воспользоваться [ссылкой на зеркало](https://storage.googleapis.com/kldscp/amnezia.org). - - + + [Все релизы](https://github.com/amnezia-vpn/amnezia-client/releases) From 8d2fe39ea3859acc3338657c81ad213e07632e4a Mon Sep 17 00:00:00 2001 From: KsZnak Date: Sun, 8 Dec 2024 05:34:18 +0200 Subject: [PATCH 12/31] Update README.md --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8b4539075..8f8878081 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,14 @@ # Amnezia VPN -## _The best client for self-hosted VPN_ + +### _The best client for self-hosted VPN_ + [![Build Status](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml/badge.svg?branch=dev)](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev) [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/amnezia-vpn/amnezia-client) +### [English]([https://github.com/amnezia-vpn/amnezia-client/blob/dev/README_RU.md](https://github.com/amnezia-vpn/amnezia-client/tree/dev?tab=readme-ov-file#)) | [Русский](https://github.com/amnezia-vpn/amnezia-client/blob/dev/README_RU.md) + + [Amnezia](https://amnezia.org) is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server. [![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png)](https://amnezia.org) From 321f0727d251896500425a2c3b6aae93972a291b Mon Sep 17 00:00:00 2001 From: Nethius Date: Mon, 9 Dec 2024 09:32:49 +0300 Subject: [PATCH 13/31] feature: added subscription expiration date for premium v2 (#1261) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feature: added subscription expiration date for premium v2 * feature: added a check for the presence of the “services” field in the response body of the getServicesList() function * feature: added prohibition to change location when connection is active * bugfix: renamed public_key->end_date to public_key->expires_at according to the changes on the backend --- client/core/controllers/apiController.cpp | 7 + client/core/defs.h | 1 + client/core/errorstrings.cpp | 3 +- .../ui/controllers/connectionController.cpp | 2 +- client/ui/models/apiServicesModel.cpp | 112 ++++++--- client/ui/models/apiServicesModel.h | 40 +++- client/ui/models/servers_model.cpp | 33 ++- client/ui/models/servers_model.h | 5 + .../Pages2/PageSettingsApiLanguageList.qml | 6 + .../qml/Pages2/PageSettingsApiServerInfo.qml | 7 +- .../ui/qml/Pages2/PageSettingsServerInfo.qml | 219 +++++++++--------- 11 files changed, 280 insertions(+), 155 deletions(-) diff --git a/client/core/controllers/apiController.cpp b/client/core/controllers/apiController.cpp index c50165e7e..6562632a9 100644 --- a/client/core/controllers/apiController.cpp +++ b/client/core/controllers/apiController.cpp @@ -379,6 +379,13 @@ ErrorCode ApiController::getServicesList(QByteArray &responseBody) auto errorCode = checkErrors(sslErrors, reply); reply->deleteLater(); + + if (errorCode == ErrorCode::NoError) { + if (!responseBody.contains("services")) { + return ErrorCode::ApiServicesMissingError; + } + } + return errorCode; } diff --git a/client/core/defs.h b/client/core/defs.h index d00d347b1..c0db2e127 100644 --- a/client/core/defs.h +++ b/client/core/defs.h @@ -109,6 +109,7 @@ namespace amnezia ApiConfigSslError = 1104, ApiMissingAgwPublicKey = 1105, ApiConfigDecryptionError = 1106, + ApiServicesMissingError = 1107, // QFile errors OpenError = 1200, diff --git a/client/core/errorstrings.cpp b/client/core/errorstrings.cpp index 495346063..70f433c62 100644 --- a/client/core/errorstrings.cpp +++ b/client/core/errorstrings.cpp @@ -63,7 +63,8 @@ QString errorString(ErrorCode code) { case (ErrorCode::ApiConfigTimeoutError): errorMessage = QObject::tr("Server response timeout on api request"); break; case (ErrorCode::ApiMissingAgwPublicKey): errorMessage = QObject::tr("Missing AGW public key"); break; case (ErrorCode::ApiConfigDecryptionError): errorMessage = QObject::tr("Failed to decrypt response payload"); break; - + case (ErrorCode::ApiServicesMissingError): errorMessage = QObject::tr("Missing list of available services"); break; + // QFile errors case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break; case(ErrorCode::ReadError): errorMessage = QObject::tr("QFile error: An error occurred when reading from the file"); break; diff --git a/client/ui/controllers/connectionController.cpp b/client/ui/controllers/connectionController.cpp index f8516f6e3..f9491d4e6 100644 --- a/client/ui/controllers/connectionController.cpp +++ b/client/ui/controllers/connectionController.cpp @@ -55,7 +55,7 @@ void ConnectionController::openConnection() && !m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) { emit updateApiConfigFromGateway(); } else if (configVersion && m_serversModel->isApiKeyExpired(serverIndex)) { - qDebug() << "attempt to update api config by end_date event"; + qDebug() << "attempt to update api config by expires_at event"; if (configVersion == ApiConfigSources::Telegram) { emit updateApiConfigFromTelegram(); } else { diff --git a/client/ui/models/apiServicesModel.cpp b/client/ui/models/apiServicesModel.cpp index 2a87bde3f..81a10f873 100644 --- a/client/ui/models/apiServicesModel.cpp +++ b/client/ui/models/apiServicesModel.cpp @@ -27,6 +27,9 @@ namespace constexpr char storeEndpoint[] = "store_endpoint"; constexpr char isAvailable[] = "is_available"; + + constexpr char subscription[] = "subscription"; + constexpr char endDate[] = "end_date"; } namespace serviceType @@ -51,23 +54,23 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const if (!index.isValid() || index.row() < 0 || index.row() >= static_cast(rowCount())) return QVariant(); - QJsonObject service = m_services.at(index.row()).toObject(); - QJsonObject serviceInfo = service.value(configKey::serviceInfo).toObject(); - auto serviceType = service.value(configKey::serviceType).toString(); + auto apiServiceData = m_services.at(index.row()); + auto serviceType = apiServiceData.type; + auto isServiceAvailable = apiServiceData.isServiceAvailable; switch (role) { case NameRole: { - return serviceInfo.value(configKey::name).toString(); + return apiServiceData.serviceInfo.name; } case CardDescriptionRole: { - auto speed = serviceInfo.value(configKey::speed).toString(); + auto speed = apiServiceData.serviceInfo.speed; if (serviceType == serviceType::amneziaPremium) { return tr("Classic VPN for comfortable work, downloading large files and watching videos. " "Works for any sites. Speed up to %1 MBit/s") .arg(speed); } else if (serviceType == serviceType::amneziaFree){ QString description = tr("VPN to access blocked sites in regions with high levels of Internet censorship. "); - if (service.value(configKey::isAvailable).isBool() && !service.value(configKey::isAvailable).toBool()) { + if (isServiceAvailable) { description += tr("

Not available in your region. If you have VPN enabled, disable it, return to the previous screen, and try again."); } return description; @@ -83,25 +86,24 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const } case IsServiceAvailableRole: { if (serviceType == serviceType::amneziaFree) { - if (service.value(configKey::isAvailable).isBool() && !service.value(configKey::isAvailable).toBool()) { + if (isServiceAvailable) { return false; } } return true; } case SpeedRole: { - auto speed = serviceInfo.value(configKey::speed).toString(); - return tr("%1 MBit/s").arg(speed); + return tr("%1 MBit/s").arg(apiServiceData.serviceInfo.speed); } - case WorkPeriodRole: { - auto timelimit = serviceInfo.value(configKey::timelimit).toString(); - if (timelimit == "0") { + case TimeLimitRole: { + auto timeLimit = apiServiceData.serviceInfo.timeLimit; + if (timeLimit == "0") { return ""; } - return tr("%1 days").arg(timelimit); + return tr("%1 days").arg(timeLimit); } case RegionRole: { - return serviceInfo.value(configKey::region).toString(); + return apiServiceData.serviceInfo.region; } case FeaturesRole: { if (serviceType == serviceType::amneziaPremium) { @@ -113,12 +115,15 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const } } case PriceRole: { - auto price = serviceInfo.value(configKey::price).toString(); + auto price = apiServiceData.serviceInfo.price; if (price == "free") { return tr("Free"); } return tr("%1 $/month").arg(price); } + case EndDateRole: { + return QDateTime::fromString(apiServiceData.subscription.endDate, Qt::ISODate).toLocalTime().toString("d MMM yyyy"); + } } return QVariant(); @@ -128,15 +133,18 @@ void ApiServicesModel::updateModel(const QJsonObject &data) { beginResetModel(); + m_services.clear(); + m_countryCode = data.value(configKey::userCountryCode).toString(); - m_services = data.value(configKey::services).toArray(); - if (m_services.isEmpty()) { - QJsonObject service; - service.insert(configKey::serviceInfo, data.value(configKey::serviceInfo)); - service.insert(configKey::serviceType, data.value(configKey::serviceType)); + auto services = data.value(configKey::services).toArray(); - m_services.push_back(service); + if (services.isEmpty()) { + m_services.push_back(getApiServicesData(data)); m_selectedServiceIndex = 0; + } else { + for (const auto &service : services) { + m_services.push_back(getApiServicesData(service.toObject())); + } } endResetModel(); @@ -149,32 +157,32 @@ void ApiServicesModel::setServiceIndex(const int index) QJsonObject ApiServicesModel::getSelectedServiceInfo() { - QJsonObject service = m_services.at(m_selectedServiceIndex).toObject(); - return service.value(configKey::serviceInfo).toObject(); + auto service = m_services.at(m_selectedServiceIndex); + return service.serviceInfo.object; } QString ApiServicesModel::getSelectedServiceType() { - QJsonObject service = m_services.at(m_selectedServiceIndex).toObject(); - return service.value(configKey::serviceType).toString(); + auto service = m_services.at(m_selectedServiceIndex); + return service.type; } QString ApiServicesModel::getSelectedServiceProtocol() { - QJsonObject service = m_services.at(m_selectedServiceIndex).toObject(); - return service.value(configKey::serviceProtocol).toString(); + auto service = m_services.at(m_selectedServiceIndex); + return service.protocol; } QString ApiServicesModel::getSelectedServiceName() { - auto modelIndex = index(m_selectedServiceIndex, 0); - return data(modelIndex, ApiServicesModel::Roles::NameRole).toString(); + auto service = m_services.at(m_selectedServiceIndex); + return service.serviceInfo.name; } QJsonArray ApiServicesModel::getSelectedServiceCountries() { - QJsonObject service = m_services.at(m_selectedServiceIndex).toObject(); - return service.value(configKey::availableCountries).toArray(); + auto service = m_services.at(m_selectedServiceIndex); + return service.availableCountries; } QString ApiServicesModel::getCountryCode() @@ -184,8 +192,8 @@ QString ApiServicesModel::getCountryCode() QString ApiServicesModel::getStoreEndpoint() { - QJsonObject service = m_services.at(m_selectedServiceIndex).toObject(); - return service.value(configKey::storeEndpoint).toString(); + auto service = m_services.at(m_selectedServiceIndex); + return service.storeEndpoint; } QVariant ApiServicesModel::getSelectedServiceData(const QString roleString) @@ -209,10 +217,46 @@ QHash ApiServicesModel::roleNames() const roles[ServiceDescriptionRole] = "serviceDescription"; roles[IsServiceAvailableRole] = "isServiceAvailable"; roles[SpeedRole] = "speed"; - roles[WorkPeriodRole] = "workPeriod"; + roles[TimeLimitRole] = "timeLimit"; roles[RegionRole] = "region"; roles[FeaturesRole] = "features"; roles[PriceRole] = "price"; + roles[EndDateRole] = "endDate"; return roles; } + +ApiServicesModel::ApiServicesData ApiServicesModel::getApiServicesData(const QJsonObject &data) +{ + auto serviceInfo = data.value(configKey::serviceInfo).toObject(); + auto serviceType = data.value(configKey::serviceType).toString(); + auto serviceProtocol = data.value(configKey::serviceProtocol).toString(); + auto availableCountries = data.value(configKey::availableCountries).toArray(); + + auto subscriptionObject = data.value(configKey::subscription).toObject(); + + ApiServicesData serviceData; + serviceData.serviceInfo.name = serviceInfo.value(configKey::name).toString(); + serviceData.serviceInfo.price = serviceInfo.value(configKey::price).toString(); + serviceData.serviceInfo.region = serviceInfo.value(configKey::region).toString(); + serviceData.serviceInfo.speed = serviceInfo.value(configKey::speed).toString(); + serviceData.serviceInfo.timeLimit = serviceInfo.value(configKey::timelimit).toString(); + + serviceData.type = serviceType; + serviceData.protocol = serviceProtocol; + + serviceData.storeEndpoint = serviceInfo.value(configKey::storeEndpoint).toString(); + + if (serviceInfo.value(configKey::isAvailable).isBool()) { + serviceData.isServiceAvailable = data.value(configKey::isAvailable).toBool(); + } else { + serviceData.isServiceAvailable = true; + } + + serviceData.serviceInfo.object = serviceInfo; + serviceData.availableCountries = availableCountries; + + serviceData.subscription.endDate = subscriptionObject.value(configKey::endDate).toString(); + + return serviceData; +} diff --git a/client/ui/models/apiServicesModel.h b/client/ui/models/apiServicesModel.h index 499189401..c96a49abc 100644 --- a/client/ui/models/apiServicesModel.h +++ b/client/ui/models/apiServicesModel.h @@ -3,6 +3,7 @@ #include #include +#include class ApiServicesModel : public QAbstractListModel { @@ -15,10 +16,11 @@ class ApiServicesModel : public QAbstractListModel ServiceDescriptionRole, IsServiceAvailableRole, SpeedRole, - WorkPeriodRole, + TimeLimitRole, RegionRole, FeaturesRole, - PriceRole + PriceRole, + EndDateRole }; explicit ApiServicesModel(QObject *parent = nullptr); @@ -48,8 +50,40 @@ public slots: QHash roleNames() const override; private: + struct ServiceInfo + { + QString name; + QString speed; + QString timeLimit; + QString region; + QString price; + + QJsonObject object; + }; + + struct Subscription + { + QString endDate; + }; + + struct ApiServicesData + { + bool isServiceAvailable; + + QString type; + QString protocol; + QString storeEndpoint; + + ServiceInfo serviceInfo; + Subscription subscription; + + QJsonArray availableCountries; + }; + + ApiServicesData getApiServicesData(const QJsonObject &data); + QString m_countryCode; - QJsonArray m_services; + QVector m_services; int m_selectedServiceIndex; }; diff --git a/client/ui/models/servers_model.cpp b/client/ui/models/servers_model.cpp index c87499a73..b72b10c31 100644 --- a/client/ui/models/servers_model.cpp +++ b/client/ui/models/servers_model.cpp @@ -22,7 +22,7 @@ namespace constexpr char serviceProtocol[] = "service_protocol"; constexpr char publicKeyInfo[] = "public_key"; - constexpr char endDate[] = "end_date"; + constexpr char expiresAt[] = "expires_at"; } } @@ -39,6 +39,9 @@ ServersModel::ServersModel(std::shared_ptr settings, QObject *parent) emit ServersModel::defaultServerNameChanged(); updateDefaultServerContainersModel(); }); + + connect(this, &ServersModel::processedServerIndexChanged, this, &ServersModel::processedServerChanged); + connect(this, &ServersModel::dataChanged, this, &ServersModel::processedServerChanged); } int ServersModel::rowCount(const QModelIndex &parent) const @@ -79,6 +82,12 @@ bool ServersModel::setData(const QModelIndex &index, const QVariant &value, int return true; } +bool ServersModel::setData(const int index, const QVariant &value, int role) +{ + QModelIndex modelIndex = this->index(index); + return setData(modelIndex, value, role); +} + QVariant ServersModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() < 0 || index.row() >= static_cast(m_servers.size())) { @@ -679,6 +688,18 @@ QVariant ServersModel::getProcessedServerData(const QString roleString) return {}; } +bool ServersModel::setProcessedServerData(const QString &roleString, const QVariant &value) +{ + const auto roles = roleNames(); + for (auto it = roles.begin(); it != roles.end(); it++) { + if (QString(it.value()) == roleString) { + return setData(m_processedServerIndex, value, it.key()); + } + } + + return false; +} + bool ServersModel::isDefaultServerDefaultContainerHasSplitTunneling() { auto server = m_servers.at(m_defaultServerIndex).toObject(); @@ -718,9 +739,9 @@ bool ServersModel::isApiKeyExpired(const int serverIndex) auto apiConfig = serverConfig.value(configKey::apiConfig).toObject(); auto publicKeyInfo = apiConfig.value(configKey::publicKeyInfo).toObject(); - const QString endDate = publicKeyInfo.value(configKey::endDate).toString(); - if (endDate.isEmpty()) { - publicKeyInfo.insert(configKey::endDate, QDateTime::currentDateTimeUtc().addDays(1).toString(Qt::ISODate)); + const QString expiresAt = publicKeyInfo.value(configKey::expiresAt).toString(); + if (expiresAt.isEmpty()) { + publicKeyInfo.insert(configKey::expiresAt, QDateTime::currentDateTimeUtc().addDays(1).toString(Qt::ISODate)); apiConfig.insert(configKey::publicKeyInfo, publicKeyInfo); serverConfig.insert(configKey::apiConfig, apiConfig); editServer(serverConfig, serverIndex); @@ -728,8 +749,8 @@ bool ServersModel::isApiKeyExpired(const int serverIndex) return false; } - auto endDateDateTime = QDateTime::fromString(endDate, Qt::ISODate).toUTC(); - if (endDateDateTime < QDateTime::currentDateTimeUtc()) { + auto expiresAtDateTime = QDateTime::fromString(expiresAt, Qt::ISODate).toUTC(); + if (expiresAtDateTime < QDateTime::currentDateTimeUtc()) { return true; } return false; diff --git a/client/ui/models/servers_model.h b/client/ui/models/servers_model.h index 0f18ea301..78bc22cc0 100644 --- a/client/ui/models/servers_model.h +++ b/client/ui/models/servers_model.h @@ -46,6 +46,7 @@ class ServersModel : public QAbstractListModel int rowCount(const QModelIndex &parent = QModelIndex()) const override; bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + bool setData(const int index, const QVariant &value, int role = Qt::EditRole); QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QVariant data(const int index, int role = Qt::DisplayRole) const; @@ -115,6 +116,7 @@ public slots: QVariant getDefaultServerData(const QString roleString); QVariant getProcessedServerData(const QString roleString); + bool setProcessedServerData(const QString &roleString, const QVariant &value); bool isDefaultServerDefaultContainerHasSplitTunneling(); @@ -127,6 +129,9 @@ public slots: signals: void processedServerIndexChanged(const int index); + // emitted when the processed server index or processed server data is changed + void processedServerChanged(); + void defaultServerIndexChanged(const int index); void defaultServerNameChanged(); void defaultServerDescriptionChanged(); diff --git a/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml b/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml index 120313cd1..600db85d1 100644 --- a/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml +++ b/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml @@ -54,8 +54,14 @@ PageType { imageSource: "qrc:/images/controls/download.svg" checked: index === ApiCountryModel.currentIndex + checkable: !ConnectionController.isConnected onClicked: { + if (ConnectionController.isConnected) { + PageController.showNotificationMessage(qsTr("Unable change server location while there is an active connection")) + return + } + if (index !== ApiCountryModel.currentIndex) { PageController.showBusyIndicator(true) var prevIndex = ApiCountryModel.currentIndex diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml index 2d6c1d9be..167e56e5a 100644 --- a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml @@ -56,12 +56,15 @@ PageType { } LabelWithImageType { + property bool showSubscriptionEndDate: ServersModel.getProcessedServerData("isCountrySelectionAvailable") + Layout.fillWidth: true Layout.margins: 16 imageSource: "qrc:/images/controls/history.svg" - leftText: qsTr("Work period") - rightText: ApiServicesModel.getSelectedServiceData("workPeriod") + leftText: showSubscriptionEndDate ? qsTr("Valid until") : qsTr("Work period") + rightText: showSubscriptionEndDate ? ApiServicesModel.getSelectedServiceData("endDate") + : ApiServicesModel.getSelectedServiceData("workPeriod") visible: rightText !== "" } diff --git a/client/ui/qml/Pages2/PageSettingsServerInfo.qml b/client/ui/qml/Pages2/PageSettingsServerInfo.qml index 95ae5c8a6..ffcfb4411 100644 --- a/client/ui/qml/Pages2/PageSettingsServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsServerInfo.qml @@ -25,6 +25,8 @@ PageType { property int pageSettingsApiServerInfo: 3 property int pageSettingsApiLanguageList: 4 + property var processedServer + defaultActiveFocusItem: focusItem Connections { @@ -35,8 +37,18 @@ PageType { } } + Connections { + target: ServersModel + + function onProcessedServerChanged() { + root.processedServer = proxyServersModel.get(0) + } + } + SortFilterProxyModel { id: proxyServersModel + objectName: "proxyServersModel" + sourceModel: ServersModel filters: [ ValueFilter { @@ -44,147 +56,139 @@ PageType { value: true } ] + + Component.onCompleted: { + root.processedServer = proxyServersModel.get(0) + } } Item { id: focusItem - KeyNavigation.tab: header + //KeyNavigation.tab: header } ColumnLayout { anchors.fill: parent - spacing: 16 - - Repeater { - id: header - model: proxyServersModel - - activeFocusOnTab: true - onFocusChanged: { - header.itemAt(0).focusItem.forceActiveFocus() - } + spacing: 4 - delegate: ColumnLayout { + BackButtonType { + id: backButton - property alias focusItem: backButton + Layout.topMargin: 20 + KeyNavigation.tab: headerContent.actionButton - id: content + backButtonFunction: function() { + if (nestedStackView.currentIndex === root.pageSettingsApiServerInfo && + root.processedServer.isCountrySelectionAvailable) { + nestedStackView.currentIndex = root.pageSettingsApiLanguageList + } else { + PageController.closePage() + } + } + } - Layout.topMargin: 20 + HeaderType { + id: headerContent + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 - BackButtonType { - id: backButton - KeyNavigation.tab: headerContent.actionButton + actionButtonImage: nestedStackView.currentIndex === root.pageSettingsApiLanguageList ? "qrc:/images/controls/settings.svg" + : "qrc:/images/controls/edit-3.svg" - backButtonFunction: function() { - if (nestedStackView.currentIndex === root.pageSettingsApiServerInfo && - ServersModel.getProcessedServerData("isCountrySelectionAvailable")) { - nestedStackView.currentIndex = root.pageSettingsApiLanguageList - } else { - PageController.closePage() - } + headerText: root.processedServer.name + descriptionText: { + if (root.processedServer.isServerFromGatewayApi) { + if (nestedStackView.currentIndex === root.pageSettingsApiLanguageList) { + return qsTr("Subscription is valid until ") + ApiServicesModel.getSelectedServiceData("endDate") + } else { + return ApiServicesModel.getSelectedServiceData("serviceDescription") } + } else if (root.processedServer.isServerFromTelegramApi) { + return root.processedServer.serverDescription + } else if (root.processedServer.hasWriteAccess) { + return root.processedServer.credentialsLogin + " · " + root.processedServer.hostName + } else { + return root.processedServer.hostName } + } - HeaderType { - id: headerContent - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - - actionButtonImage: nestedStackView.currentIndex === root.pageSettingsApiLanguageList ? "qrc:/images/controls/settings.svg" : "qrc:/images/controls/edit-3.svg" - - headerText: name - descriptionText: { - if (ServersModel.getProcessedServerData("isServerFromGatewayApi")) { - return ApiServicesModel.getSelectedServiceData("serviceDescription") - } else if (ServersModel.getProcessedServerData("isServerFromTelegramApi")) { - return serverDescription - } else if (ServersModel.isProcessedServerHasWriteAccess()) { - return credentialsLogin + " · " + hostName - } else { - return hostName - } - } - - KeyNavigation.tab: tabBar + KeyNavigation.tab: tabBar - actionButtonFunction: function() { - if (nestedStackView.currentIndex === root.pageSettingsApiLanguageList) { - nestedStackView.currentIndex = root.pageSettingsApiServerInfo - } else { - serverNameEditDrawer.open() - } - } + actionButtonFunction: function() { + if (nestedStackView.currentIndex === root.pageSettingsApiLanguageList) { + nestedStackView.currentIndex = root.pageSettingsApiServerInfo + } else { + serverNameEditDrawer.open() } + } + } - DrawerType2 { - id: serverNameEditDrawer + DrawerType2 { + id: serverNameEditDrawer - parent: root + parent: root - anchors.fill: parent - expandedHeight: root.height * 0.35 + anchors.fill: parent + expandedHeight: root.height * 0.35 - onClosed: { - if (!GC.isMobile()) { - headerContent.actionButton.forceActiveFocus() - } - } + onClosed: { + if (!GC.isMobile()) { + headerContent.actionButton.forceActiveFocus() + } + } - expandedContent: ColumnLayout { - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.topMargin: 32 - anchors.leftMargin: 16 - anchors.rightMargin: 16 - - Connections { - target: serverNameEditDrawer - enabled: !GC.isMobile() - function onOpened() { - serverName.textField.forceActiveFocus() - } - } + expandedContent: ColumnLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 32 + anchors.leftMargin: 16 + anchors.rightMargin: 16 + + Connections { + target: serverNameEditDrawer + enabled: !GC.isMobile() + function onOpened() { + serverName.textField.forceActiveFocus() + } + } - Item { - id: focusItem1 - KeyNavigation.tab: serverName.textField - } + Item { + id: focusItem1 + KeyNavigation.tab: serverName.textField + } - TextFieldWithHeaderType { - id: serverName + TextFieldWithHeaderType { + id: serverName - Layout.fillWidth: true - headerText: qsTr("Server name") - textFieldText: name - textField.maximumLength: 30 - checkEmptyText: true + Layout.fillWidth: true + headerText: qsTr("Server name") + textFieldText: root.processedServer.name + textField.maximumLength: 30 + checkEmptyText: true - KeyNavigation.tab: saveButton - } + KeyNavigation.tab: saveButton + } - BasicButtonType { - id: saveButton + BasicButtonType { + id: saveButton - Layout.fillWidth: true + Layout.fillWidth: true - text: qsTr("Save") - KeyNavigation.tab: focusItem1 + text: qsTr("Save") + KeyNavigation.tab: focusItem1 - clickedFunc: function() { - if (serverName.textFieldText === "") { - return - } + clickedFunc: function() { + if (serverName.textFieldText === "") { + return + } - if (serverName.textFieldText !== name) { - name = serverName.textFieldText - } - serverNameEditDrawer.close() - } + if (serverName.textFieldText !== root.processedServer.name) { + ServersModel.setProcessedServerData("name", serverName.textFieldText); } + serverNameEditDrawer.close() } } } @@ -257,8 +261,7 @@ PageType { StackLayout { id: nestedStackView - Layout.preferredWidth: root.width - Layout.preferredHeight: root.height - tabBar.implicitHeight - header.implicitHeight + Layout.fillWidth: true currentIndex: ServersModel.getProcessedServerData("isServerFromGatewayApi") ? (ServersModel.getProcessedServerData("isCountrySelectionAvailable") ? From 9e7cf7fa1f5ee298f97d057f3daf990a5c6920a5 Mon Sep 17 00:00:00 2001 From: Cyril Anisimov Date: Tue, 10 Dec 2024 03:17:16 +0100 Subject: [PATCH 14/31] feature/xray user management (#972) * feature: implement client management functionality for Xray --------- Co-authored-by: aiamnezia Co-authored-by: vladimir.kuznetsov --- client/configurators/xray_configurator.cpp | 147 ++++++++- client/configurators/xray_configurator.h | 4 + client/ui/controllers/exportController.cpp | 9 +- client/ui/controllers/exportController.h | 2 +- client/ui/models/clientManagementModel.cpp | 351 +++++++++++++++++++-- client/ui/models/clientManagementModel.h | 6 + client/ui/qml/Pages2/PageShare.qml | 2 +- 7 files changed, 485 insertions(+), 36 deletions(-) diff --git a/client/configurators/xray_configurator.cpp b/client/configurators/xray_configurator.cpp index 786da47c5..514aa8211 100644 --- a/client/configurators/xray_configurator.cpp +++ b/client/configurators/xray_configurator.cpp @@ -3,38 +3,169 @@ #include #include #include +#include +#include "logger.h" #include "containers/containers_defs.h" #include "core/controllers/serverController.h" #include "core/scripts_registry.h" +namespace { +Logger logger("XrayConfigurator"); +} + XrayConfigurator::XrayConfigurator(std::shared_ptr settings, const QSharedPointer &serverController, QObject *parent) : ConfiguratorBase(settings, serverController, parent) { } -QString XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig, - ErrorCode &errorCode) +QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentials, DockerContainer container, + const QJsonObject &containerConfig, ErrorCode &errorCode) +{ + // Generate new UUID for client + QString clientId = QUuid::createUuid().toString(QUuid::WithoutBraces); + + // Get current server config + QString currentConfig = m_serverController->getTextFileFromContainer( + container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode); + + if (errorCode != ErrorCode::NoError) { + logger.error() << "Failed to get server config file"; + return ""; + } + + // Parse current config as JSON + QJsonDocument doc = QJsonDocument::fromJson(currentConfig.toUtf8()); + if (doc.isNull() || !doc.isObject()) { + logger.error() << "Failed to parse server config JSON"; + errorCode = ErrorCode::InternalError; + return ""; + } + + QJsonObject serverConfig = doc.object(); + + // Validate server config structure + if (!serverConfig.contains("inbounds")) { + logger.error() << "Server config missing 'inbounds' field"; + errorCode = ErrorCode::InternalError; + return ""; + } + + QJsonArray inbounds = serverConfig["inbounds"].toArray(); + if (inbounds.isEmpty()) { + logger.error() << "Server config has empty 'inbounds' array"; + errorCode = ErrorCode::InternalError; + return ""; + } + + QJsonObject inbound = inbounds[0].toObject(); + if (!inbound.contains("settings")) { + logger.error() << "Inbound missing 'settings' field"; + errorCode = ErrorCode::InternalError; + return ""; + } + + QJsonObject settings = inbound["settings"].toObject(); + if (!settings.contains("clients")) { + logger.error() << "Settings missing 'clients' field"; + errorCode = ErrorCode::InternalError; + return ""; + } + + QJsonArray clients = settings["clients"].toArray(); + + // Create configuration for new client + QJsonObject clientConfig { + {"id", clientId}, + {"flow", "xtls-rprx-vision"} + }; + + clients.append(clientConfig); + + // Update config + settings["clients"] = clients; + inbound["settings"] = settings; + inbounds[0] = inbound; + serverConfig["inbounds"] = inbounds; + + // Save updated config to server + QString updatedConfig = QJsonDocument(serverConfig).toJson(); + errorCode = m_serverController->uploadTextFileToContainer( + container, + credentials, + updatedConfig, + amnezia::protocols::xray::serverConfigPath, + libssh::ScpOverwriteMode::ScpOverwriteExisting + ); + if (errorCode != ErrorCode::NoError) { + logger.error() << "Failed to upload updated config"; + return ""; + } + + // Restart container + QString restartScript = QString("sudo docker restart $CONTAINER_NAME"); + errorCode = m_serverController->runScript( + credentials, + m_serverController->replaceVars(restartScript, m_serverController->genVarsForScript(credentials, container)) + ); + + if (errorCode != ErrorCode::NoError) { + logger.error() << "Failed to restart container"; + return ""; + } + + return clientId; +} + +QString XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container, + const QJsonObject &containerConfig, ErrorCode &errorCode) { + // Get client ID from prepareServerConfig + QString xrayClientId = prepareServerConfig(credentials, container, containerConfig, errorCode); + if (errorCode != ErrorCode::NoError || xrayClientId.isEmpty()) { + logger.error() << "Failed to prepare server config"; + errorCode = ErrorCode::InternalError; + return ""; + } + QString config = m_serverController->replaceVars(amnezia::scriptData(ProtocolScriptType::xray_template, container), m_serverController->genVarsForScript(credentials, container, containerConfig)); + + if (config.isEmpty()) { + logger.error() << "Failed to get config template"; + errorCode = ErrorCode::InternalError; + return ""; + } QString xrayPublicKey = m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::PublicKeyPath, errorCode); + if (errorCode != ErrorCode::NoError || xrayPublicKey.isEmpty()) { + logger.error() << "Failed to get public key"; + errorCode = ErrorCode::InternalError; + return ""; + } xrayPublicKey.replace("\n", ""); - - QString xrayUuid = m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::uuidPath, errorCode); - xrayUuid.replace("\n", ""); - + QString xrayShortId = m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::shortidPath, errorCode); + if (errorCode != ErrorCode::NoError || xrayShortId.isEmpty()) { + logger.error() << "Failed to get short ID"; + errorCode = ErrorCode::InternalError; + return ""; + } xrayShortId.replace("\n", ""); - if (errorCode != ErrorCode::NoError) { + // Validate all required variables are present + if (!config.contains("$XRAY_CLIENT_ID") || !config.contains("$XRAY_PUBLIC_KEY") || !config.contains("$XRAY_SHORT_ID")) { + logger.error() << "Config template missing required variables:" + << "XRAY_CLIENT_ID:" << !config.contains("$XRAY_CLIENT_ID") + << "XRAY_PUBLIC_KEY:" << !config.contains("$XRAY_PUBLIC_KEY") + << "XRAY_SHORT_ID:" << !config.contains("$XRAY_SHORT_ID"); + errorCode = ErrorCode::InternalError; return ""; } - config.replace("$XRAY_CLIENT_ID", xrayUuid); + config.replace("$XRAY_CLIENT_ID", xrayClientId); config.replace("$XRAY_PUBLIC_KEY", xrayPublicKey); config.replace("$XRAY_SHORT_ID", xrayShortId); diff --git a/client/configurators/xray_configurator.h b/client/configurators/xray_configurator.h index 2acfdf71d..8ed4e7752 100644 --- a/client/configurators/xray_configurator.h +++ b/client/configurators/xray_configurator.h @@ -14,6 +14,10 @@ class XrayConfigurator : public ConfiguratorBase QString createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig, ErrorCode &errorCode); + +private: + QString prepareServerConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig, + ErrorCode &errorCode); }; #endif // XRAY_CONFIGURATOR_H diff --git a/client/ui/controllers/exportController.cpp b/client/ui/controllers/exportController.cpp index 2690b5b1e..8681406e9 100644 --- a/client/ui/controllers/exportController.cpp +++ b/client/ui/controllers/exportController.cpp @@ -121,9 +121,8 @@ ErrorCode ExportController::generateNativeConfig(const DockerContainer container jsonNativeConfig = QJsonDocument::fromJson(protocolConfigString.toUtf8()).object(); - if (protocol == Proto::OpenVpn || protocol == Proto::WireGuard || protocol == Proto::Awg) { - auto clientId = jsonNativeConfig.value(config_key::clientId).toString(); - errorCode = m_clientManagementModel->appendClient(clientId, clientName, container, credentials, serverController); + if (protocol == Proto::OpenVpn || protocol == Proto::WireGuard || protocol == Proto::Awg || protocol == Proto::Xray) { + errorCode = m_clientManagementModel->appendClient(jsonNativeConfig, clientName, container, credentials, serverController); } return errorCode; } @@ -248,10 +247,10 @@ void ExportController::generateCloakConfig() emit exportConfigChanged(); } -void ExportController::generateXrayConfig() +void ExportController::generateXrayConfig(const QString &clientName) { QJsonObject nativeConfig; - ErrorCode errorCode = generateNativeConfig(DockerContainer::Xray, "", Proto::Xray, nativeConfig); + ErrorCode errorCode = generateNativeConfig(DockerContainer::Xray, clientName, Proto::Xray, nativeConfig); if (errorCode) { emit exportErrorOccurred(errorCode); return; diff --git a/client/ui/controllers/exportController.h b/client/ui/controllers/exportController.h index b031ea393..a2c9fcfaf 100644 --- a/client/ui/controllers/exportController.h +++ b/client/ui/controllers/exportController.h @@ -28,7 +28,7 @@ public slots: void generateAwgConfig(const QString &clientName); void generateShadowSocksConfig(); void generateCloakConfig(); - void generateXrayConfig(); + void generateXrayConfig(const QString &clientName); QString getConfig(); QString getNativeConfigString(); diff --git a/client/ui/models/clientManagementModel.cpp b/client/ui/models/clientManagementModel.cpp index 7445d60fb..f07eae712 100644 --- a/client/ui/models/clientManagementModel.cpp +++ b/client/ui/models/clientManagementModel.cpp @@ -106,6 +106,8 @@ ErrorCode ClientManagementModel::updateModel(const DockerContainer container, co error = getOpenVpnClients(container, credentials, serverController, count); } else if (container == DockerContainer::WireGuard || container == DockerContainer::Awg) { error = getWireGuardClients(container, credentials, serverController, count); + } else if (container == DockerContainer::Xray) { + error = getXrayClients(container, credentials, serverController, count); } if (error != ErrorCode::NoError) { endResetModel(); @@ -239,6 +241,68 @@ ErrorCode ClientManagementModel::getWireGuardClients(const DockerContainer conta } return error; } +ErrorCode ClientManagementModel::getXrayClients(const DockerContainer container, const ServerCredentials& credentials, + const QSharedPointer &serverController, int &count) +{ + ErrorCode error = ErrorCode::NoError; + + const QString serverConfigPath = amnezia::protocols::xray::serverConfigPath; + const QString configString = serverController->getTextFileFromContainer(container, credentials, serverConfigPath, error); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to get the xray server config file from the server"; + return error; + } + + QJsonDocument serverConfig = QJsonDocument::fromJson(configString.toUtf8()); + if (serverConfig.isNull()) { + logger.error() << "Failed to parse xray server config JSON"; + return ErrorCode::InternalError; + } + + if (!serverConfig.object().contains("inbounds") || serverConfig.object()["inbounds"].toArray().isEmpty()) { + logger.error() << "Invalid xray server config structure"; + return ErrorCode::InternalError; + } + + const QJsonObject inbound = serverConfig.object()["inbounds"].toArray()[0].toObject(); + if (!inbound.contains("settings")) { + logger.error() << "Missing settings in xray inbound config"; + return ErrorCode::InternalError; + } + + const QJsonObject settings = inbound["settings"].toObject(); + if (!settings.contains("clients")) { + logger.error() << "Missing clients in xray settings config"; + return ErrorCode::InternalError; + } + + const QJsonArray clients = settings["clients"].toArray(); + for (const auto &clientValue : clients) { + const QJsonObject clientObj = clientValue.toObject(); + if (!clientObj.contains("id")) { + logger.error() << "Missing id in xray client config"; + continue; + } + QString clientId = clientObj["id"].toString(); + + QString xrayDefaultUuid = serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::uuidPath, error); + xrayDefaultUuid.replace("\n", ""); + + if (!isClientExists(clientId) && clientId != xrayDefaultUuid) { + QJsonObject client; + client[configKey::clientId] = clientId; + + QJsonObject userData; + userData[configKey::clientName] = QString("Client %1").arg(count); + client[configKey::userData] = userData; + + m_clientsTable.push_back(client); + count++; + } + } + + return error; +} ErrorCode ClientManagementModel::wgShow(const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController, std::vector &data) @@ -326,17 +390,67 @@ ErrorCode ClientManagementModel::appendClient(const DockerContainer container, c const QSharedPointer &serverController) { Proto protocol; - if (container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) { - protocol = Proto::OpenVpn; - } else if (container == DockerContainer::OpenVpn || container == DockerContainer::WireGuard || container == DockerContainer::Awg) { - protocol = ContainerProps::defaultProtocol(container); - } else { - return ErrorCode::NoError; + switch (container) { + case DockerContainer::ShadowSocks: + case DockerContainer::Cloak: + protocol = Proto::OpenVpn; + break; + case DockerContainer::OpenVpn: + case DockerContainer::WireGuard: + case DockerContainer::Awg: + case DockerContainer::Xray: + protocol = ContainerProps::defaultProtocol(container); + break; + default: + return ErrorCode::NoError; } auto protocolConfig = ContainerProps::getProtocolConfigFromContainer(protocol, containerConfig); + return appendClient(protocolConfig, clientName, container, credentials, serverController); +} - return appendClient(protocolConfig.value(config_key::clientId).toString(), clientName, container, credentials, serverController); +ErrorCode ClientManagementModel::appendClient(QJsonObject &protocolConfig, const QString &clientName, const DockerContainer container, + const ServerCredentials &credentials, const QSharedPointer &serverController) +{ + QString clientId; + if (container == DockerContainer::Xray) { + if (!protocolConfig.contains("outbounds")) { + return ErrorCode::InternalError; + } + QJsonArray outbounds = protocolConfig.value("outbounds").toArray(); + if (outbounds.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject outbound = outbounds[0].toObject(); + if (!outbound.contains("settings")) { + return ErrorCode::InternalError; + } + QJsonObject settings = outbound["settings"].toObject(); + if (!settings.contains("vnext")) { + return ErrorCode::InternalError; + } + QJsonArray vnext = settings["vnext"].toArray(); + if (vnext.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject vnextObj = vnext[0].toObject(); + if (!vnextObj.contains("users")) { + return ErrorCode::InternalError; + } + QJsonArray users = vnextObj["users"].toArray(); + if (users.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject user = users[0].toObject(); + if (!user.contains("id")) { + return ErrorCode::InternalError; + } + clientId = user["id"].toString(); + } else { + clientId = protocolConfig.value(config_key::clientId).toString(); + } + + return appendClient(clientId, clientName, container, credentials, serverController); } ErrorCode ClientManagementModel::appendClient(const QString &clientId, const QString &clientName, const DockerContainer container, @@ -422,10 +536,27 @@ ErrorCode ClientManagementModel::revokeClient(const int row, const DockerContain auto client = m_clientsTable.at(row).toObject(); QString clientId = client.value(configKey::clientId).toString(); - if (container == DockerContainer::OpenVpn || container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) { - errorCode = revokeOpenVpn(row, container, credentials, serverIndex, serverController); - } else if (container == DockerContainer::WireGuard || container == DockerContainer::Awg) { - errorCode = revokeWireGuard(row, container, credentials, serverController); + switch(container) + { + case DockerContainer::OpenVpn: + case DockerContainer::ShadowSocks: + case DockerContainer::Cloak: { + errorCode = revokeOpenVpn(row, container, credentials, serverIndex, serverController); + break; + } + case DockerContainer::WireGuard: + case DockerContainer::Awg: { + errorCode = revokeWireGuard(row, container, credentials, serverController); + break; + } + case DockerContainer::Xray: { + errorCode = revokeXray(row, container, credentials, serverController); + break; + } + default: { + logger.error() << "Internal error: received unexpected container type"; + return ErrorCode::InternalError; + } } if (errorCode == ErrorCode::NoError) { @@ -463,19 +594,69 @@ ErrorCode ClientManagementModel::revokeClient(const QJsonObject &containerConfig } Proto protocol; - if (container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) { - protocol = Proto::OpenVpn; - } else if (container == DockerContainer::OpenVpn || container == DockerContainer::WireGuard || container == DockerContainer::Awg) { - protocol = ContainerProps::defaultProtocol(container); - } else { - return ErrorCode::NoError; + + switch(container) + { + case DockerContainer::ShadowSocks: + case DockerContainer::Cloak: { + protocol = Proto::OpenVpn; + break; + } + case DockerContainer::OpenVpn: + case DockerContainer::WireGuard: + case DockerContainer::Awg: + case DockerContainer::Xray: { + protocol = ContainerProps::defaultProtocol(container); + break; + } + default: { + logger.error() << "Internal error: received unexpected container type"; + return ErrorCode::InternalError; + } } auto protocolConfig = ContainerProps::getProtocolConfigFromContainer(protocol, containerConfig); + QString clientId; + if (container == DockerContainer::Xray) { + if (!protocolConfig.contains("outbounds")) { + return ErrorCode::InternalError; + } + QJsonArray outbounds = protocolConfig.value("outbounds").toArray(); + if (outbounds.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject outbound = outbounds[0].toObject(); + if (!outbound.contains("settings")) { + return ErrorCode::InternalError; + } + QJsonObject settings = outbound["settings"].toObject(); + if (!settings.contains("vnext")) { + return ErrorCode::InternalError; + } + QJsonArray vnext = settings["vnext"].toArray(); + if (vnext.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject vnextObj = vnext[0].toObject(); + if (!vnextObj.contains("users")) { + return ErrorCode::InternalError; + } + QJsonArray users = vnextObj["users"].toArray(); + if (users.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject user = users[0].toObject(); + if (!user.contains("id")) { + return ErrorCode::InternalError; + } + clientId = user["id"].toString(); + } else { + clientId = protocolConfig.value(config_key::clientId).toString(); + } + int row; bool clientExists = false; - QString clientId = protocolConfig.value(config_key::clientId).toString(); for (row = 0; row < rowCount(); row++) { auto client = m_clientsTable.at(row).toObject(); if (clientId == client.value(configKey::clientId).toString()) { @@ -487,11 +668,28 @@ ErrorCode ClientManagementModel::revokeClient(const QJsonObject &containerConfig return errorCode; } - if (container == DockerContainer::OpenVpn || container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) { + switch (container) + { + case DockerContainer::OpenVpn: + case DockerContainer::ShadowSocks: + case DockerContainer::Cloak: { errorCode = revokeOpenVpn(row, container, credentials, serverIndex, serverController); - } else if (container == DockerContainer::WireGuard || container == DockerContainer::Awg) { + break; + } + case DockerContainer::WireGuard: + case DockerContainer::Awg: { errorCode = revokeWireGuard(row, container, credentials, serverController); + break; + } + case DockerContainer::Xray: { + errorCode = revokeXray(row, container, credentials, serverController); + break; } + default: + logger.error() << "Internal error: received unexpected container type"; + return ErrorCode::InternalError; + } + return errorCode; } @@ -594,6 +792,117 @@ ErrorCode ClientManagementModel::revokeWireGuard(const int row, const DockerCont return ErrorCode::NoError; } +ErrorCode ClientManagementModel::revokeXray(const int row, + const DockerContainer container, + const ServerCredentials &credentials, + const QSharedPointer &serverController) +{ + ErrorCode error = ErrorCode::NoError; + + // Get server config + const QString serverConfigPath = amnezia::protocols::xray::serverConfigPath; + const QString configString = serverController->getTextFileFromContainer(container, credentials, serverConfigPath, error); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to get the xray server config file"; + return error; + } + + QJsonDocument serverConfig = QJsonDocument::fromJson(configString.toUtf8()); + if (serverConfig.isNull()) { + logger.error() << "Failed to parse xray server config JSON"; + return ErrorCode::InternalError; + } + + // Get client ID to remove + auto client = m_clientsTable.at(row).toObject(); + QString clientId = client.value(configKey::clientId).toString(); + + // Remove client from server config + QJsonObject configObj = serverConfig.object(); + if (!configObj.contains("inbounds")) { + logger.error() << "Missing inbounds in xray config"; + return ErrorCode::InternalError; + } + + QJsonArray inbounds = configObj["inbounds"].toArray(); + if (inbounds.isEmpty()) { + logger.error() << "Empty inbounds array in xray config"; + return ErrorCode::InternalError; + } + + QJsonObject inbound = inbounds[0].toObject(); + if (!inbound.contains("settings")) { + logger.error() << "Missing settings in xray inbound config"; + return ErrorCode::InternalError; + } + + QJsonObject settings = inbound["settings"].toObject(); + if (!settings.contains("clients")) { + logger.error() << "Missing clients in xray settings"; + return ErrorCode::InternalError; + } + + QJsonArray clients = settings["clients"].toArray(); + if (clients.isEmpty()) { + logger.error() << "Empty clients array in xray config"; + return ErrorCode::InternalError; + } + + for (int i = 0; i < clients.size(); ++i) { + QJsonObject clientObj = clients[i].toObject(); + if (clientObj.contains("id") && clientObj["id"].toString() == clientId) { + clients.removeAt(i); + break; + } + } + + // Update server config + settings["clients"] = clients; + inbound["settings"] = settings; + inbounds[0] = inbound; + configObj["inbounds"] = inbounds; + + // Upload updated config + error = serverController->uploadTextFileToContainer( + container, + credentials, + QJsonDocument(configObj).toJson(), + serverConfigPath + ); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to upload updated xray config"; + return error; + } + + // Remove from local table + beginRemoveRows(QModelIndex(), row, row); + m_clientsTable.removeAt(row); + endRemoveRows(); + + // Update clients table file on server + const QByteArray clientsTableString = QJsonDocument(m_clientsTable).toJson(); + QString clientsTableFile = QString("/opt/amnezia/%1/clientsTable") + .arg(ContainerProps::containerTypeToString(container)); + + error = serverController->uploadTextFileToContainer(container, credentials, clientsTableString, clientsTableFile); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to upload the clientsTable file"; + } + + // Restart container + QString restartScript = QString("sudo docker restart $CONTAINER_NAME"); + error = serverController->runScript( + credentials, + serverController->replaceVars(restartScript, serverController->genVarsForScript(credentials, container)) + ); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to restart xray container"; + return error; + } + + return error; +} + QHash ClientManagementModel::roleNames() const { QHash roles; @@ -604,4 +913,4 @@ QHash ClientManagementModel::roleNames() const roles[DataSentRole] = "dataSent"; roles[AllowedIpsRole] = "allowedIps"; return roles; -} +} \ No newline at end of file diff --git a/client/ui/models/clientManagementModel.h b/client/ui/models/clientManagementModel.h index 60132abe2..989120a9e 100644 --- a/client/ui/models/clientManagementModel.h +++ b/client/ui/models/clientManagementModel.h @@ -40,6 +40,8 @@ public slots: const QSharedPointer &serverController); ErrorCode appendClient(const DockerContainer container, const ServerCredentials &credentials, const QJsonObject &containerConfig, const QString &clientName, const QSharedPointer &serverController); + ErrorCode appendClient(QJsonObject &protocolConfig, const QString &clientName,const DockerContainer container, + const ServerCredentials &credentials, const QSharedPointer &serverController); ErrorCode appendClient(const QString &clientId, const QString &clientName, const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController); ErrorCode renameClient(const int row, const QString &userName, const DockerContainer container, const ServerCredentials &credentials, @@ -64,11 +66,15 @@ public slots: const QSharedPointer &serverController); ErrorCode revokeWireGuard(const int row, const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController); + ErrorCode revokeXray(const int row, const DockerContainer container, const ServerCredentials &credentials, + const QSharedPointer &serverController); ErrorCode getOpenVpnClients(const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController, int &count); ErrorCode getWireGuardClients(const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController, int &count); + ErrorCode getXrayClients(const DockerContainer container, const ServerCredentials& credentials, + const QSharedPointer &serverController, int &count); ErrorCode wgShow(const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController, std::vector &data); diff --git a/client/ui/qml/Pages2/PageShare.qml b/client/ui/qml/Pages2/PageShare.qml index 995fa3e76..d6ce78488 100644 --- a/client/ui/qml/Pages2/PageShare.qml +++ b/client/ui/qml/Pages2/PageShare.qml @@ -92,7 +92,7 @@ PageType { break } case PageShare.ConfigType.Xray: { - ExportController.generateXrayConfig() + ExportController.generateXrayConfig(clientNameTextField.textFieldText) shareConnectionDrawer.configCaption = qsTr("Save XRay config") shareConnectionDrawer.configExtension = ".json" shareConnectionDrawer.configFileName = "amnezia_for_xray" From 6a21994736ae1cad4c377a6d27e88eaf2b764482 Mon Sep 17 00:00:00 2001 From: aiamnezia Date: Tue, 10 Dec 2024 19:04:11 +0400 Subject: [PATCH 15/31] Fix formatting --- client/ui/controllers/updateController.cpp | 15 +++-- client/ui/controllers/updateController.h | 1 + ipc/ipcserver.cpp | 78 ++++++++++------------ ipc/ipcserver.h | 12 ++-- 4 files changed, 50 insertions(+), 56 deletions(-) diff --git a/client/ui/controllers/updateController.cpp b/client/ui/controllers/updateController.cpp index 45acf1908..80d04d6a5 100644 --- a/client/ui/controllers/updateController.cpp +++ b/client/ui/controllers/updateController.cpp @@ -9,7 +9,8 @@ #include "core/errorstrings.h" #include "version.h" -namespace { +namespace +{ #ifdef Q_OS_MACOS const QString installerPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN.dmg"; #elif defined Q_OS_WINDOWS @@ -19,7 +20,8 @@ namespace { #endif } -UpdateController::UpdateController(const std::shared_ptr &settings, QObject *parent) : QObject(parent), m_settings(settings) +UpdateController::UpdateController(const std::shared_ptr &settings, QObject *parent) + : QObject(parent), m_settings(settings) { } @@ -62,19 +64,19 @@ void UpdateController::checkForUpdates() for (auto asset : assets) { QJsonObject assetObject = asset.toObject(); - #ifdef Q_OS_WINDOWS +#ifdef Q_OS_WINDOWS if (assetObject.value("name").toString().endsWith(".exe")) { m_downloadUrl = assetObject.value("browser_download_url").toString(); } - #elif defined(Q_OS_MACOS) +#elif defined(Q_OS_MACOS) if (assetObject.value("name").toString().endsWith(".dmg")) { m_downloadUrl = assetObject.value("browser_download_url").toString(); } - #elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) +#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) if (assetObject.value("name").toString().contains(".tar.zip")) { m_downloadUrl = assetObject.value("browser_download_url").toString(); } - #endif +#endif } emit updateFound(); @@ -141,5 +143,4 @@ void UpdateController::runInstaller() reply->deleteLater(); }); - } diff --git a/client/ui/controllers/updateController.h b/client/ui/controllers/updateController.h index 986174acf..ea5c22fae 100644 --- a/client/ui/controllers/updateController.h +++ b/client/ui/controllers/updateController.h @@ -22,6 +22,7 @@ public slots: signals: void updateFound(); void errorOccured(const QString &errorMessage); + private: std::shared_ptr m_settings; diff --git a/ipc/ipcserver.cpp b/ipc/ipcserver.cpp index d02fe56aa..46c960741 100644 --- a/ipc/ipcserver.cpp +++ b/ipc/ipcserver.cpp @@ -9,8 +9,8 @@ #include "logger.h" #include "router.h" -#include "../core/networkUtilities.h" #include "../client/protocols/protocols_defs.h" +#include "../core/networkUtilities.h" #ifdef Q_OS_WIN #include "../client/platforms/windows/daemon/windowsdaemon.h" #include "../client/platforms/windows/daemon/windowsfirewall.h" @@ -60,12 +60,15 @@ int IpcServer::createPrivilegedProcess() } }); - QObject::connect(pd.serverNode.data(), &QRemoteObjectHost::error, this, - [pd](QRemoteObjectNode::ErrorCode errorCode) { qDebug() << "QRemoteObjectHost::error" << errorCode; }); + QObject::connect(pd.serverNode.data(), &QRemoteObjectHost::error, this, [pd](QRemoteObjectNode::ErrorCode errorCode) { + qDebug() << "QRemoteObjectHost::error" << errorCode; + }); - QObject::connect(pd.serverNode.data(), &QRemoteObjectHost::destroyed, this, [pd]() { qDebug() << "QRemoteObjectHost::destroyed"; }); + QObject::connect(pd.serverNode.data(), &QRemoteObjectHost::destroyed, this, + [pd]() { qDebug() << "QRemoteObjectHost::destroyed"; }); - // connect(pd.ipcProcess.data(), &IpcServerProcess::finished, this, [this, pid=m_localpid](int exitCode, QProcess::ExitStatus exitStatus){ + // connect(pd.ipcProcess.data(), &IpcServerProcess::finished, this, [this, pid=m_localpid](int exitCode, + // QProcess::ExitStatus exitStatus){ // qDebug() << "IpcServerProcess finished" << exitCode << exitStatus; //// if (m_processes.contains(pid)) { //// m_processes[pid].ipcProcess.reset(); @@ -386,17 +389,14 @@ int IpcServer::installApp(const QString &path) // On Windows, simply run the .exe file with administrator privileges QProcess process; process.setProgram("powershell.exe"); - process.setArguments(QStringList() - << "Start-Process" - << path - << "-Verb" - << "RunAs" - << "-Wait"); - + process.setArguments(QStringList() << "Start-Process" << path << "-Verb" + << "RunAs" + << "-Wait"); + qDebug() << "Launching installer with elevated privileges..."; process.start(); process.waitForFinished(); - + if (process.exitCode() != 0) { qDebug() << "Installation error:" << process.readAllStandardError(); } @@ -404,57 +404,47 @@ int IpcServer::installApp(const QString &path) #elif defined(Q_OS_MACOS) // DRAFT - + QProcess process; QString tempDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation); QString mountPoint = tempDir + "/AmneziaVPN_mount"; - + // Create mount point QDir dir(mountPoint); if (!dir.exists()) { dir.mkpath("."); } - + // Mount DMG image qDebug() << "Mounting DMG image..."; - process.start("hdiutil", QStringList() - << "attach" - << path - << "-mountpoint" - << mountPoint - << "-nobrowse"); + process.start("hdiutil", QStringList() << "attach" << path << "-mountpoint" << mountPoint << "-nobrowse"); process.waitForFinished(); - + if (process.exitCode() != 0) { qDebug() << "Failed to mount DMG:" << process.readAllStandardError(); return process.exitCode(); } - + // Look for .app bundle in mounted image QDirIterator it(mountPoint, QStringList() << "*.app", QDir::Dirs); if (!it.hasNext()) { qDebug() << "No .app bundle found in DMG"; return -1; } - + QString appPath = it.next(); QString targetPath = "/Applications/" + QFileInfo(appPath).fileName(); - + // Copy application to /Applications qDebug() << "Copying app to Applications folder..."; - process.start("cp", QStringList() - << "-R" - << appPath - << targetPath); + process.start("cp", QStringList() << "-R" << appPath << targetPath); process.waitForFinished(); - + // Unmount DMG qDebug() << "Unmounting DMG..."; - process.start("hdiutil", QStringList() - << "detach" - << mountPoint); + process.start("hdiutil", QStringList() << "detach" << mountPoint); process.waitForFinished(); - + if (process.exitCode() != 0) { qDebug() << "Installation error:" << process.readAllStandardError(); } @@ -464,17 +454,17 @@ int IpcServer::installApp(const QString &path) QProcess process; QString tempDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation); QString extractDir = tempDir + "/amnezia_update"; - + qDebug() << "Installing app from:" << path; qDebug() << "Using temp directory:" << extractDir; - + // Create extraction directory if it doesn't exist QDir dir(extractDir); if (!dir.exists()) { dir.mkpath("."); qDebug() << "Created extraction directory"; } - + // First, extract the zip archive qDebug() << "Extracting ZIP archive..."; process.start("unzip", QStringList() << path << "-d" << extractDir); @@ -484,7 +474,7 @@ int IpcServer::installApp(const QString &path) return process.exitCode(); } qDebug() << "ZIP archive extracted successfully"; - + // Look for tar file in extracted files qDebug() << "Looking for TAR file..."; QDirIterator tarIt(extractDir, QStringList() << "*.tar", QDir::Files); @@ -492,12 +482,12 @@ int IpcServer::installApp(const QString &path) qDebug() << "TAR file not found in the extracted archive"; return -1; } - + // Extract found tar archive QString tarPath = tarIt.next(); qDebug() << "Found TAR file:" << tarPath; qDebug() << "Extracting TAR archive..."; - + process.start("tar", QStringList() << "-xf" << tarPath << "-C" << extractDir); process.waitForFinished(); if (process.exitCode() != 0) { @@ -505,11 +495,11 @@ int IpcServer::installApp(const QString &path) return process.exitCode(); } qDebug() << "TAR archive extracted successfully"; - + // Remove tar file as it's no longer needed QFile::remove(tarPath); qDebug() << "Removed temporary TAR file"; - + // Find executable file and run it qDebug() << "Looking for executable file..."; QDirIterator it(extractDir, QDir::Files | QDir::Executable, QDirIterator::Subdirectories); @@ -524,7 +514,7 @@ int IpcServer::installApp(const QString &path) qDebug() << "Installer finished with exit code:" << process.exitCode(); return process.exitCode(); } - + qDebug() << "No executable file found"; return -1; // Executable not found #endif diff --git a/ipc/ipcserver.h b/ipc/ipcserver.h index 7e5b21d11..c3aaaf4e0 100644 --- a/ipc/ipcserver.h +++ b/ipc/ipcserver.h @@ -1,11 +1,11 @@ #ifndef IPCSERVER_H #define IPCSERVER_H +#include "../client/daemon/interfaceconfig.h" +#include #include #include #include -#include -#include "../client/daemon/interfaceconfig.h" #include "ipc.h" #include "ipcserverprocess.h" @@ -37,15 +37,17 @@ class IpcServer : public IpcInterfaceSource virtual bool enablePeerTraffic(const QJsonObject &configStr) override; virtual bool enableKillSwitch(const QJsonObject &excludeAddr, int vpnAdapterIndex) override; virtual bool disableKillSwitch() override; - virtual bool updateResolvers(const QString& ifname, const QList& resolvers) override; + virtual bool updateResolvers(const QString &ifname, const QList &resolvers) override; virtual int mountDmg(const QString &path, bool mount) override; virtual int installApp(const QString &path) override; private: int m_localpid = 0; - struct ProcessDescriptor { - ProcessDescriptor (QObject *parent = nullptr) { + struct ProcessDescriptor + { + ProcessDescriptor(QObject *parent = nullptr) + { serverNode = QSharedPointer(new QRemoteObjectHost(parent)); ipcProcess = QSharedPointer(new IpcServerProcess(parent)); tun2socksProcess = QSharedPointer(new IpcProcessTun2Socks(parent)); From 3b300a203f9c9ecdd772a9d985b43f646ff8c106 Mon Sep 17 00:00:00 2001 From: aiamnezia Date: Wed, 11 Dec 2024 20:24:59 +0400 Subject: [PATCH 16/31] Fix installation for Windows and MacOS --- ipc/ipcserver.cpp | 88 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 75 insertions(+), 13 deletions(-) diff --git a/ipc/ipcserver.cpp b/ipc/ipcserver.cpp index 46c960741..1d0182b8e 100644 --- a/ipc/ipcserver.cpp +++ b/ipc/ipcserver.cpp @@ -386,15 +386,46 @@ int IpcServer::installApp(const QString &path) qDebug() << "Installing app from:" << path; #ifdef Q_OS_WINDOWS - // On Windows, simply run the .exe file with administrator privileges QProcess process; - process.setProgram("powershell.exe"); - process.setArguments(QStringList() << "Start-Process" << path << "-Verb" - << "RunAs" - << "-Wait"); + QString tempDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation); + QString extractDir = tempDir + "/amnezia_update"; + // Create extraction directory if it doesn't exist + QDir dir(extractDir); + if (!dir.exists()) { + dir.mkpath("."); + qDebug() << "Created extraction directory"; + } + + // Extract ZIP archive + qDebug() << "Extracting ZIP archive..."; + process.start("powershell.exe", + QStringList() << "Expand-Archive" + << "-Path" << path << "-DestinationPath" << extractDir << "-Force"); + process.waitForFinished(); + + if (process.exitCode() != 0) { + qDebug() << "ZIP extraction error:" << process.readAllStandardError(); + return process.exitCode(); + } + qDebug() << "ZIP archive extracted successfully"; + + // Find .exe file in extracted directory + QDirIterator it(extractDir, QStringList() << "*.exe", QDir::Files, QDirIterator::Subdirectories); + if (!it.hasNext()) { + qDebug() << "No .exe file found in the extracted archive"; + return -1; + } + + QString installerPath = it.next(); + qDebug() << "Found installer:" << installerPath; + + // Run installer with elevated privileges qDebug() << "Launching installer with elevated privileges..."; - process.start(); + process.start("powershell.exe", + QStringList() << "Start-Process" << installerPath << "-Verb" + << "RunAs" + << "-Wait"); process.waitForFinished(); if (process.exitCode() != 0) { @@ -403,21 +434,48 @@ int IpcServer::installApp(const QString &path) return process.exitCode(); #elif defined(Q_OS_MACOS) - // DRAFT - QProcess process; QString tempDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation); + QString extractDir = tempDir + "/amnezia_update"; + + // Create extraction directory + QDir dir(extractDir); + if (!dir.exists()) { + dir.mkpath("."); + qDebug() << "Created extraction directory"; + } + + // Extract ZIP archive using unzip command + qDebug() << "Extracting ZIP archive..."; + process.start("unzip", QStringList() << path << "-d" << extractDir); + process.waitForFinished(); + + if (process.exitCode() != 0) { + qDebug() << "ZIP extraction error:" << process.readAllStandardError(); + return process.exitCode(); + } + qDebug() << "ZIP archive extracted successfully"; + + // Find .dmg file in extracted directory + QDirIterator it(extractDir, QStringList() << "*.dmg", QDir::Files, QDirIterator::Subdirectories); + if (!it.hasNext()) { + qDebug() << "No .dmg file found in the extracted archive"; + return -1; + } + + QString dmgPath = it.next(); + qDebug() << "Found DMG file:" << dmgPath; QString mountPoint = tempDir + "/AmneziaVPN_mount"; // Create mount point - QDir dir(mountPoint); + dir = QDir(mountPoint); if (!dir.exists()) { dir.mkpath("."); } // Mount DMG image qDebug() << "Mounting DMG image..."; - process.start("hdiutil", QStringList() << "attach" << path << "-mountpoint" << mountPoint << "-nobrowse"); + process.start("hdiutil", QStringList() << "attach" << dmgPath << "-mountpoint" << mountPoint << "-nobrowse"); process.waitForFinished(); if (process.exitCode() != 0) { @@ -426,13 +484,13 @@ int IpcServer::installApp(const QString &path) } // Look for .app bundle in mounted image - QDirIterator it(mountPoint, QStringList() << "*.app", QDir::Dirs); - if (!it.hasNext()) { + QDirIterator appIt(mountPoint, QStringList() << "*.app", QDir::Dirs); + if (!appIt.hasNext()) { qDebug() << "No .app bundle found in DMG"; return -1; } - QString appPath = it.next(); + QString appPath = appIt.next(); QString targetPath = "/Applications/" + QFileInfo(appPath).fileName(); // Copy application to /Applications @@ -448,6 +506,10 @@ int IpcServer::installApp(const QString &path) if (process.exitCode() != 0) { qDebug() << "Installation error:" << process.readAllStandardError(); } + + // Clean up + QDir(extractDir).removeRecursively(); + return process.exitCode(); #elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) From a73234ec2afbc2bf00f81fad5ac404a44832d3fc Mon Sep 17 00:00:00 2001 From: aiamnezia Date: Wed, 11 Dec 2024 18:11:46 +0400 Subject: [PATCH 17/31] Add some logs --- ipc/ipcserver.cpp | 135 ++++++++++++---------------------------------- 1 file changed, 33 insertions(+), 102 deletions(-) diff --git a/ipc/ipcserver.cpp b/ipc/ipcserver.cpp index 1d0182b8e..f9519a491 100644 --- a/ipc/ipcserver.cpp +++ b/ipc/ipcserver.cpp @@ -383,133 +383,67 @@ int IpcServer::mountDmg(const QString &path, bool mount) int IpcServer::installApp(const QString &path) { - qDebug() << "Installing app from:" << path; + Logger logger("IpcServer"); + logger.info() << "Installing app from:" << path; #ifdef Q_OS_WINDOWS QProcess process; - QString tempDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation); - QString extractDir = tempDir + "/amnezia_update"; - - // Create extraction directory if it doesn't exist - QDir dir(extractDir); - if (!dir.exists()) { - dir.mkpath("."); - qDebug() << "Created extraction directory"; - } - - // Extract ZIP archive - qDebug() << "Extracting ZIP archive..."; - process.start("powershell.exe", - QStringList() << "Expand-Archive" - << "-Path" << path << "-DestinationPath" << extractDir << "-Force"); - process.waitForFinished(); - - if (process.exitCode() != 0) { - qDebug() << "ZIP extraction error:" << process.readAllStandardError(); - return process.exitCode(); - } - qDebug() << "ZIP archive extracted successfully"; - - // Find .exe file in extracted directory - QDirIterator it(extractDir, QStringList() << "*.exe", QDir::Files, QDirIterator::Subdirectories); - if (!it.hasNext()) { - qDebug() << "No .exe file found in the extracted archive"; - return -1; - } - - QString installerPath = it.next(); - qDebug() << "Found installer:" << installerPath; - - // Run installer with elevated privileges - qDebug() << "Launching installer with elevated privileges..."; + logger.info() << "Launching installer with elevated privileges..."; process.start("powershell.exe", - QStringList() << "Start-Process" << installerPath << "-Verb" + QStringList() << "Start-Process" << path << "-Verb" << "RunAs" << "-Wait"); process.waitForFinished(); if (process.exitCode() != 0) { - qDebug() << "Installation error:" << process.readAllStandardError(); + logger.error() << "Installation error:" << process.readAllStandardError(); } return process.exitCode(); #elif defined(Q_OS_MACOS) QProcess process; QString tempDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation); - QString extractDir = tempDir + "/amnezia_update"; - - // Create extraction directory - QDir dir(extractDir); - if (!dir.exists()) { - dir.mkpath("."); - qDebug() << "Created extraction directory"; - } - - // Extract ZIP archive using unzip command - qDebug() << "Extracting ZIP archive..."; - process.start("unzip", QStringList() << path << "-d" << extractDir); - process.waitForFinished(); - - if (process.exitCode() != 0) { - qDebug() << "ZIP extraction error:" << process.readAllStandardError(); - return process.exitCode(); - } - qDebug() << "ZIP archive extracted successfully"; - - // Find .dmg file in extracted directory - QDirIterator it(extractDir, QStringList() << "*.dmg", QDir::Files, QDirIterator::Subdirectories); - if (!it.hasNext()) { - qDebug() << "No .dmg file found in the extracted archive"; - return -1; - } - - QString dmgPath = it.next(); - qDebug() << "Found DMG file:" << dmgPath; QString mountPoint = tempDir + "/AmneziaVPN_mount"; // Create mount point - dir = QDir(mountPoint); + QDir dir(mountPoint); if (!dir.exists()) { dir.mkpath("."); } // Mount DMG image - qDebug() << "Mounting DMG image..."; - process.start("hdiutil", QStringList() << "attach" << dmgPath << "-mountpoint" << mountPoint << "-nobrowse"); + logger.info() << "Mounting DMG image..."; + process.start("hdiutil", QStringList() << "attach" << path << "-mountpoint" << mountPoint << "-nobrowse"); process.waitForFinished(); if (process.exitCode() != 0) { - qDebug() << "Failed to mount DMG:" << process.readAllStandardError(); + logger.error() << "Failed to mount DMG:" << process.readAllStandardError(); return process.exitCode(); } // Look for .app bundle in mounted image - QDirIterator appIt(mountPoint, QStringList() << "*.app", QDir::Dirs); - if (!appIt.hasNext()) { - qDebug() << "No .app bundle found in DMG"; + QDirIterator it(mountPoint, QStringList() << "*.app", QDir::Dirs); + if (!it.hasNext()) { + logger.error() << "No .app bundle found in DMG"; return -1; } - QString appPath = appIt.next(); + QString appPath = it.next(); QString targetPath = "/Applications/" + QFileInfo(appPath).fileName(); // Copy application to /Applications - qDebug() << "Copying app to Applications folder..."; + logger.info() << "Copying app to Applications folder..."; process.start("cp", QStringList() << "-R" << appPath << targetPath); process.waitForFinished(); // Unmount DMG - qDebug() << "Unmounting DMG..."; + logger.info() << "Unmounting DMG..."; process.start("hdiutil", QStringList() << "detach" << mountPoint); process.waitForFinished(); if (process.exitCode() != 0) { - qDebug() << "Installation error:" << process.readAllStandardError(); + logger.error() << "Installation error:" << process.readAllStandardError(); } - - // Clean up - QDir(extractDir).removeRecursively(); - return process.exitCode(); #elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) @@ -517,67 +451,64 @@ int IpcServer::installApp(const QString &path) QString tempDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation); QString extractDir = tempDir + "/amnezia_update"; - qDebug() << "Installing app from:" << path; - qDebug() << "Using temp directory:" << extractDir; + logger.info() << "Using temp directory:" << extractDir; // Create extraction directory if it doesn't exist QDir dir(extractDir); if (!dir.exists()) { dir.mkpath("."); - qDebug() << "Created extraction directory"; + logger.info() << "Created extraction directory"; } // First, extract the zip archive - qDebug() << "Extracting ZIP archive..."; + logger.info() << "Extracting ZIP archive..."; process.start("unzip", QStringList() << path << "-d" << extractDir); process.waitForFinished(); if (process.exitCode() != 0) { - qDebug() << "ZIP extraction error:" << process.readAllStandardError(); + logger.error() << "ZIP extraction error:" << process.readAllStandardError(); return process.exitCode(); } - qDebug() << "ZIP archive extracted successfully"; + logger.info() << "ZIP archive extracted successfully"; // Look for tar file in extracted files - qDebug() << "Looking for TAR file..."; + logger.info() << "Looking for TAR file..."; QDirIterator tarIt(extractDir, QStringList() << "*.tar", QDir::Files); if (!tarIt.hasNext()) { - qDebug() << "TAR file not found in the extracted archive"; + logger.error() << "TAR file not found in the extracted archive"; return -1; } // Extract found tar archive QString tarPath = tarIt.next(); - qDebug() << "Found TAR file:" << tarPath; - qDebug() << "Extracting TAR archive..."; + logger.info() << "Found TAR file:" << tarPath; + logger.info() << "Extracting TAR archive..."; process.start("tar", QStringList() << "-xf" << tarPath << "-C" << extractDir); process.waitForFinished(); if (process.exitCode() != 0) { - qDebug() << "TAR extraction error:" << process.readAllStandardError(); + logger.error() << "TAR extraction error:" << process.readAllStandardError(); return process.exitCode(); } - qDebug() << "TAR archive extracted successfully"; + logger.info() << "TAR archive extracted successfully"; // Remove tar file as it's no longer needed QFile::remove(tarPath); - qDebug() << "Removed temporary TAR file"; + logger.info() << "Removed temporary TAR file"; // Find executable file and run it - qDebug() << "Looking for executable file..."; + logger.info() << "Looking for executable file..."; QDirIterator it(extractDir, QDir::Files | QDir::Executable, QDirIterator::Subdirectories); if (it.hasNext()) { QString execPath = it.next(); - qDebug() << "Found executable:" << execPath; - qDebug() << "Launching installer..."; + logger.info() << "Found executable:" << execPath; + logger.info() << "Launching installer..."; process.start("sudo", QStringList() << execPath); process.waitForFinished(); - qDebug() << "Installer stdout:" << process.readAllStandardOutput(); - qDebug() << "Installer stderr:" << process.readAllStandardError(); - qDebug() << "Installer finished with exit code:" << process.exitCode(); + logger.info() << "Installer finished with exit code:" << process.exitCode(); return process.exitCode(); } - qDebug() << "No executable file found"; + logger.error() << "No executable file found"; return -1; // Executable not found #endif return 0; From bac71ed3e756ab7eb3207bd90f3ec98c496f84a6 Mon Sep 17 00:00:00 2001 From: aiamnezia Date: Wed, 11 Dec 2024 18:34:16 +0400 Subject: [PATCH 18/31] Add logs from installattion shell on Windows --- ipc/ipcserver.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ipc/ipcserver.cpp b/ipc/ipcserver.cpp index f9519a491..55a6e6abb 100644 --- a/ipc/ipcserver.cpp +++ b/ipc/ipcserver.cpp @@ -393,6 +393,8 @@ int IpcServer::installApp(const QString &path) QStringList() << "Start-Process" << path << "-Verb" << "RunAs" << "-Wait"); + logger.info() << "Installer stdout:" << process.readAllStandardOutput(); + logger.info() << "Installer stderr:" << process.readAllStandardError(); process.waitForFinished(); if (process.exitCode() != 0) { From 2029c108e56ad18879ede667bfa46ab8ae559d74 Mon Sep 17 00:00:00 2001 From: aiamnezia Date: Thu, 12 Dec 2024 06:54:10 +0400 Subject: [PATCH 19/31] Optimized code --- ipc/ipcserver.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/ipc/ipcserver.cpp b/ipc/ipcserver.cpp index 55a6e6abb..64d046f35 100644 --- a/ipc/ipcserver.cpp +++ b/ipc/ipcserver.cpp @@ -389,13 +389,10 @@ int IpcServer::installApp(const QString &path) #ifdef Q_OS_WINDOWS QProcess process; logger.info() << "Launching installer with elevated privileges..."; - process.start("powershell.exe", - QStringList() << "Start-Process" << path << "-Verb" - << "RunAs" - << "-Wait"); + process.start(path); + process.waitForFinished(); logger.info() << "Installer stdout:" << process.readAllStandardOutput(); logger.info() << "Installer stderr:" << process.readAllStandardError(); - process.waitForFinished(); if (process.exitCode() != 0) { logger.error() << "Installation error:" << process.readAllStandardError(); From 8de7ad6b41ca83f0b350f7f035384493a847c481 Mon Sep 17 00:00:00 2001 From: aiamnezia Date: Thu, 19 Dec 2024 18:10:46 +0400 Subject: [PATCH 20/31] Move installer running to client side for Ubuntu --- client/client_scripts/linux_installer.sh | 44 ++++++++++++ client/core/scripts_registry.cpp | 24 +++++++ client/core/scripts_registry.h | 80 ++++++++++++---------- client/ui/controllers/updateController.cpp | 75 ++++++++++++++++++-- client/ui/controllers/updateController.h | 8 +++ 5 files changed, 191 insertions(+), 40 deletions(-) create mode 100644 client/client_scripts/linux_installer.sh diff --git a/client/client_scripts/linux_installer.sh b/client/client_scripts/linux_installer.sh new file mode 100644 index 000000000..829875357 --- /dev/null +++ b/client/client_scripts/linux_installer.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +EXTRACT_DIR="$1" +INSTALLER_PATH="$2" + +# Create and clean extract directory +rm -rf "$EXTRACT_DIR" +mkdir -p "$EXTRACT_DIR" + +# Extract ZIP archive +unzip "$INSTALLER_PATH" -d "$EXTRACT_DIR" +if [ $? -ne 0 ]; then + echo 'Failed to extract ZIP archive' + exit 1 +fi + +# Find and extract TAR archive +TAR_FILE=$(find "$EXTRACT_DIR" -name '*.tar' -type f) +if [ -z "$TAR_FILE" ]; then + echo 'TAR file not found' + exit 1 +fi + +tar -xf "$TAR_FILE" -C "$EXTRACT_DIR" +if [ $? -ne 0 ]; then + echo 'Failed to extract TAR archive' + exit 1 +fi + +rm -f "$TAR_FILE" + +# Find and run installer +INSTALLER=$(find "$EXTRACT_DIR" -type f -executable) +if [ -z "$INSTALLER" ]; then + echo 'Installer not found' + exit 1 +fi + +"$INSTALLER" +EXIT_CODE=$? + +# Cleanup +rm -rf "$EXTRACT_DIR" +exit $EXIT_CODE \ No newline at end of file diff --git a/client/core/scripts_registry.cpp b/client/core/scripts_registry.cpp index 95b5df4ad..9b02fba9d 100644 --- a/client/core/scripts_registry.cpp +++ b/client/core/scripts_registry.cpp @@ -54,6 +54,14 @@ QString amnezia::scriptName(ProtocolScriptType type) } } +QString amnezia::scriptName(ClientScriptType type) +{ + switch (type) { + case ClientScriptType::linux_installer: return QLatin1String("linux_installer.sh"); + default: return QString(); + } +} + QString amnezia::scriptData(amnezia::SharedScriptType type) { QString fileName = QString(":/server_scripts/%1").arg(amnezia::scriptName(type)); @@ -81,3 +89,19 @@ QString amnezia::scriptData(amnezia::ProtocolScriptType type, DockerContainer co data.replace("\r", ""); return data; } + +QString amnezia::scriptData(ClientScriptType type) +{ + QString fileName = QString(":/client_scripts/%1").arg(amnezia::scriptName(type)); + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly)) { + qDebug() << "Warning: script missing" << fileName; + return ""; + } + QByteArray data = file.readAll(); + if (data.isEmpty()) { + qDebug() << "Warning: script is empty" << fileName; + } + data.replace("\r", ""); + return data; +} diff --git a/client/core/scripts_registry.h b/client/core/scripts_registry.h index d952dafb0..2b4bf087d 100644 --- a/client/core/scripts_registry.h +++ b/client/core/scripts_registry.h @@ -1,44 +1,52 @@ #ifndef SCRIPTS_REGISTRY_H #define SCRIPTS_REGISTRY_H -#include -#include "core/defs.h" #include "containers/containers_defs.h" +#include "core/defs.h" +#include -namespace amnezia { - -enum SharedScriptType { - // General scripts - prepare_host, - install_docker, - build_container, - remove_container, - remove_all_containers, - setup_host_firewall, - check_connection, - check_server_is_busy, - check_user_in_sudo -}; -enum ProtocolScriptType { - // Protocol scripts - dockerfile, - run_container, - configure_container, - container_startup, - openvpn_template, - wireguard_template, - awg_template, - xray_template -}; - - -QString scriptFolder(DockerContainer container); - -QString scriptName(SharedScriptType type); -QString scriptName(ProtocolScriptType type); - -QString scriptData(SharedScriptType type); -QString scriptData(ProtocolScriptType type, DockerContainer container); +namespace amnezia +{ + + enum SharedScriptType { + // General scripts + prepare_host, + install_docker, + build_container, + remove_container, + remove_all_containers, + setup_host_firewall, + check_connection, + check_server_is_busy, + check_user_in_sudo + }; + + enum ProtocolScriptType { + // Protocol scripts + dockerfile, + run_container, + configure_container, + container_startup, + openvpn_template, + wireguard_template, + awg_template, + xray_template + }; + + enum ClientScriptType { + // Client-side scripts + linux_installer + }; + + QString scriptFolder(DockerContainer container); + + QString scriptName(SharedScriptType type); + QString scriptName(ProtocolScriptType type); + QString scriptName(ClientScriptType type); + + QString scriptData(SharedScriptType type); + QString scriptData(ProtocolScriptType type, DockerContainer container); + QString scriptData(ClientScriptType type); } #endif // SCRIPTS_REGISTRY_H diff --git a/client/ui/controllers/updateController.cpp b/client/ui/controllers/updateController.cpp index 80d04d6a5..2888ec2dc 100644 --- a/client/ui/controllers/updateController.cpp +++ b/client/ui/controllers/updateController.cpp @@ -7,6 +7,7 @@ #include "amnezia_application.h" #include "core/errorstrings.h" +#include "core/scripts_registry.h" #include "version.h" namespace @@ -121,10 +122,14 @@ void UpdateController::runInstaller() file.write(reply->readAll()); file.close(); QString t = installerPath; - auto ipcReply = IpcClient::Interface()->installApp(t); - ipcReply.waitForFinished(); - int result = ipcReply.returnValue(); +#if defined(Q_OS_WINDOWS) + runWindowsInstaller(t); +#elif defined(Q_OS_MACOS) + runMacInstaller(t); +#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) + runLinuxInstaller(t); +#endif // emit errorOccured(""); } } else { @@ -140,7 +145,69 @@ void UpdateController::runInstaller() qDebug() << errorString(ErrorCode::ApiConfigDownloadError); } } - reply->deleteLater(); }); } + +#if defined(Q_OS_WINDOWS) +int UpdateController::runWindowsInstaller(const QString &installerPath) +{ + qDebug() << "Windows installer path:" << installerPath; + // TODO: Implement Windows installation logic + return -1; +} +#endif + +#if defined(Q_OS_MACOS) +int UpdateController::runMacInstaller(const QString &installerPath) +{ + qDebug() << "macOS installer path:" << installerPath; + // TODO: Implement macOS installation logic + return -1; +} +#endif + +#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) +int UpdateController::runLinuxInstaller(const QString &installerPath) +{ + // Create temporary directory for extraction + QTemporaryDir extractDir; + extractDir.setAutoRemove(false); + if (!extractDir.isValid()) { + qDebug() << "Failed to create temporary directory"; + return -1; + } + qDebug() << "Temporary directory created:" << extractDir.path(); + + // Create script file in the temporary directory + QString scriptPath = extractDir.path() + "/installer.sh"; + QFile scriptFile(scriptPath); + if (!scriptFile.open(QIODevice::WriteOnly)) { + qDebug() << "Failed to create script file"; + return -1; + } + + // Get script content from registry + QString scriptContent = amnezia::scriptData(amnezia::ClientScriptType::linux_installer); + scriptFile.write(scriptContent.toUtf8()); + scriptFile.close(); + qDebug() << "Script file created:" << scriptPath; + + // Make script executable + QFile::setPermissions(scriptPath, QFile::permissions(scriptPath) | QFile::ExeUser); + + // Start detached process + qint64 pid; + bool success = QProcess::startDetached( + "/bin/bash", QStringList() << scriptPath << extractDir.path() << installerPath, extractDir.path(), &pid); + + if (success) { + qDebug() << "Installation process started with PID:" << pid; + } else { + qDebug() << "Failed to start installation process"; + return -1; + } + + return 0; +} +#endif diff --git a/client/ui/controllers/updateController.h b/client/ui/controllers/updateController.h index ea5c22fae..85b7c48d5 100644 --- a/client/ui/controllers/updateController.h +++ b/client/ui/controllers/updateController.h @@ -30,6 +30,14 @@ public slots: QString m_version; QString m_releaseDate; QString m_downloadUrl; + +#if defined(Q_OS_WINDOWS) + int runWindowsInstaller(const QString &installerPath); +#elif defined(Q_OS_MACOS) + int runMacInstaller(const QString &installerPath); +#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) + int runLinuxInstaller(const QString &installerPath); +#endif }; #endif // UPDATECONTROLLER_H From 11f9c7bc7cb287eef7393b536b74e1371e6e8d8f Mon Sep 17 00:00:00 2001 From: aiamnezia Date: Thu, 19 Dec 2024 19:10:40 +0400 Subject: [PATCH 21/31] Move installer launch logic to client side for Windows --- client/ui/controllers/updateController.cpp | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/client/ui/controllers/updateController.cpp b/client/ui/controllers/updateController.cpp index 2888ec2dc..34ad35a91 100644 --- a/client/ui/controllers/updateController.cpp +++ b/client/ui/controllers/updateController.cpp @@ -15,7 +15,8 @@ namespace #ifdef Q_OS_MACOS const QString installerPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN.dmg"; #elif defined Q_OS_WINDOWS - const QString installerPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN.exe"; + const QString installerPath = + QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN_installer.exe"; #elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) const QString installerPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN.tar.zip"; #endif @@ -152,9 +153,18 @@ void UpdateController::runInstaller() #if defined(Q_OS_WINDOWS) int UpdateController::runWindowsInstaller(const QString &installerPath) { - qDebug() << "Windows installer path:" << installerPath; - // TODO: Implement Windows installation logic - return -1; + // Start the installer process + qint64 pid; + bool success = QProcess::startDetached(installerPath, QStringList(), QString(), &pid); + + if (success) { + qDebug() << "Installation process started with PID:" << pid; + } else { + qDebug() << "Failed to start installation process"; + return -1; + } + + return 0; } #endif From fe9be2353689fec5a91b0744f140fb0ff8925481 Mon Sep 17 00:00:00 2001 From: aiamnezia Date: Thu, 19 Dec 2024 19:20:31 +0400 Subject: [PATCH 22/31] Clean service code --- ipc/ipc_interface.rep | 1 - ipc/ipcserver.cpp | 135 +----------------------------------------- ipc/ipcserver.h | 1 - 3 files changed, 1 insertion(+), 136 deletions(-) diff --git a/ipc/ipc_interface.rep b/ipc/ipc_interface.rep index 7dad63bd4..1647ea190 100644 --- a/ipc/ipc_interface.rep +++ b/ipc/ipc_interface.rep @@ -34,6 +34,5 @@ class IpcInterface SLOT( bool updateResolvers(const QString& ifname, const QList& resolvers) ); SLOT( int mountDmg(const QString &path, bool mount) ); - SLOT (int installApp(const QString &path)); }; diff --git a/ipc/ipcserver.cpp b/ipc/ipcserver.cpp index 64d046f35..b73ae4077 100644 --- a/ipc/ipcserver.cpp +++ b/ipc/ipcserver.cpp @@ -4,7 +4,6 @@ #include #include #include -#include #include "logger.h" #include "router.h" @@ -379,136 +378,4 @@ int IpcServer::mountDmg(const QString &path, bool mount) return res; #endif return 0; -} - -int IpcServer::installApp(const QString &path) -{ - Logger logger("IpcServer"); - logger.info() << "Installing app from:" << path; - -#ifdef Q_OS_WINDOWS - QProcess process; - logger.info() << "Launching installer with elevated privileges..."; - process.start(path); - process.waitForFinished(); - logger.info() << "Installer stdout:" << process.readAllStandardOutput(); - logger.info() << "Installer stderr:" << process.readAllStandardError(); - - if (process.exitCode() != 0) { - logger.error() << "Installation error:" << process.readAllStandardError(); - } - return process.exitCode(); - -#elif defined(Q_OS_MACOS) - QProcess process; - QString tempDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation); - QString mountPoint = tempDir + "/AmneziaVPN_mount"; - - // Create mount point - QDir dir(mountPoint); - if (!dir.exists()) { - dir.mkpath("."); - } - - // Mount DMG image - logger.info() << "Mounting DMG image..."; - process.start("hdiutil", QStringList() << "attach" << path << "-mountpoint" << mountPoint << "-nobrowse"); - process.waitForFinished(); - - if (process.exitCode() != 0) { - logger.error() << "Failed to mount DMG:" << process.readAllStandardError(); - return process.exitCode(); - } - - // Look for .app bundle in mounted image - QDirIterator it(mountPoint, QStringList() << "*.app", QDir::Dirs); - if (!it.hasNext()) { - logger.error() << "No .app bundle found in DMG"; - return -1; - } - - QString appPath = it.next(); - QString targetPath = "/Applications/" + QFileInfo(appPath).fileName(); - - // Copy application to /Applications - logger.info() << "Copying app to Applications folder..."; - process.start("cp", QStringList() << "-R" << appPath << targetPath); - process.waitForFinished(); - - // Unmount DMG - logger.info() << "Unmounting DMG..."; - process.start("hdiutil", QStringList() << "detach" << mountPoint); - process.waitForFinished(); - - if (process.exitCode() != 0) { - logger.error() << "Installation error:" << process.readAllStandardError(); - } - return process.exitCode(); - -#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) - QProcess process; - QString tempDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation); - QString extractDir = tempDir + "/amnezia_update"; - - logger.info() << "Using temp directory:" << extractDir; - - // Create extraction directory if it doesn't exist - QDir dir(extractDir); - if (!dir.exists()) { - dir.mkpath("."); - logger.info() << "Created extraction directory"; - } - - // First, extract the zip archive - logger.info() << "Extracting ZIP archive..."; - process.start("unzip", QStringList() << path << "-d" << extractDir); - process.waitForFinished(); - if (process.exitCode() != 0) { - logger.error() << "ZIP extraction error:" << process.readAllStandardError(); - return process.exitCode(); - } - logger.info() << "ZIP archive extracted successfully"; - - // Look for tar file in extracted files - logger.info() << "Looking for TAR file..."; - QDirIterator tarIt(extractDir, QStringList() << "*.tar", QDir::Files); - if (!tarIt.hasNext()) { - logger.error() << "TAR file not found in the extracted archive"; - return -1; - } - - // Extract found tar archive - QString tarPath = tarIt.next(); - logger.info() << "Found TAR file:" << tarPath; - logger.info() << "Extracting TAR archive..."; - - process.start("tar", QStringList() << "-xf" << tarPath << "-C" << extractDir); - process.waitForFinished(); - if (process.exitCode() != 0) { - logger.error() << "TAR extraction error:" << process.readAllStandardError(); - return process.exitCode(); - } - logger.info() << "TAR archive extracted successfully"; - - // Remove tar file as it's no longer needed - QFile::remove(tarPath); - logger.info() << "Removed temporary TAR file"; - - // Find executable file and run it - logger.info() << "Looking for executable file..."; - QDirIterator it(extractDir, QDir::Files | QDir::Executable, QDirIterator::Subdirectories); - if (it.hasNext()) { - QString execPath = it.next(); - logger.info() << "Found executable:" << execPath; - logger.info() << "Launching installer..."; - process.start("sudo", QStringList() << execPath); - process.waitForFinished(); - logger.info() << "Installer finished with exit code:" << process.exitCode(); - return process.exitCode(); - } - - logger.error() << "No executable file found"; - return -1; // Executable not found -#endif - return 0; -} +} \ No newline at end of file diff --git a/ipc/ipcserver.h b/ipc/ipcserver.h index c3aaaf4e0..0f0153aa5 100644 --- a/ipc/ipcserver.h +++ b/ipc/ipcserver.h @@ -39,7 +39,6 @@ class IpcServer : public IpcInterfaceSource virtual bool disableKillSwitch() override; virtual bool updateResolvers(const QString &ifname, const QList &resolvers) override; virtual int mountDmg(const QString &path, bool mount) override; - virtual int installApp(const QString &path) override; private: int m_localpid = 0; From 44376847e2c0d5f0969d5365598f8a07ff38b8bd Mon Sep 17 00:00:00 2001 From: aiamnezia Date: Tue, 24 Dec 2024 17:07:55 +0400 Subject: [PATCH 23/31] Add linux_install script to resources --- client/resources.qrc | 1 + 1 file changed, 1 insertion(+) diff --git a/client/resources.qrc b/client/resources.qrc index 5447fe714..ae015b9fc 100644 --- a/client/resources.qrc +++ b/client/resources.qrc @@ -3,6 +3,7 @@ images/tray/active.png images/tray/default.png images/tray/error.png + client_scripts/linux_installer.sh images/AmneziaVPN.png server_scripts/remove_container.sh server_scripts/setup_host_firewall.sh From 89df1df886efce66f54fe8bca666dd64d8af43f1 Mon Sep 17 00:00:00 2001 From: aiamnezia Date: Tue, 24 Dec 2024 18:53:35 +0400 Subject: [PATCH 24/31] Add logs for UpdateController --- client/ui/controllers/updateController.cpp | 92 ++++++++++++++-------- client/ui/controllers/updateController.h | 1 - 2 files changed, 57 insertions(+), 36 deletions(-) diff --git a/client/ui/controllers/updateController.cpp b/client/ui/controllers/updateController.cpp index 34ad35a91..7eb5c63fd 100644 --- a/client/ui/controllers/updateController.cpp +++ b/client/ui/controllers/updateController.cpp @@ -8,10 +8,13 @@ #include "amnezia_application.h" #include "core/errorstrings.h" #include "core/scripts_registry.h" +#include "logger.h" #include "version.h" namespace { + Logger logger("UpdateController"); + #ifdef Q_OS_MACOS const QString installerPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN.dmg"; #elif defined Q_OS_WINDOWS @@ -53,7 +56,6 @@ void UpdateController::checkForUpdates() m_version = data.value("tag_name").toString(); auto currentVersion = QVersionNumber::fromString(QString(APP_VERSION)); - qDebug() << currentVersion; auto newVersion = QVersionNumber::fromString(m_version); if (newVersion > currentVersion) { m_changelogText = data.value("body").toString(); @@ -86,30 +88,40 @@ void UpdateController::checkForUpdates() } else { if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError || reply->error() == QNetworkReply::NetworkError::TimeoutError) { - qDebug() << errorString(ErrorCode::ApiConfigTimeoutError); + logger.error() << errorString(ErrorCode::ApiConfigTimeoutError); } else { QString err = reply->errorString(); - qDebug() << QString::fromUtf8(reply->readAll()); - qDebug() << reply->error(); - qDebug() << err; - qDebug() << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); - qDebug() << errorString(ErrorCode::ApiConfigDownloadError); + logger.error() << QString::fromUtf8(reply->readAll()); + logger.error() << "Network error code:" << QString::number(static_cast(reply->error())); + logger.error() << "Error message:" << err; + logger.error() << "HTTP status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + logger.error() << errorString(ErrorCode::ApiConfigDownloadError); } } reply->deleteLater(); }); - QObject::connect(reply, &QNetworkReply::errorOccurred, - [this, reply](QNetworkReply::NetworkError error) { qDebug() << reply->errorString() << error; }); + QObject::connect(reply, &QNetworkReply::errorOccurred, [this, reply](QNetworkReply::NetworkError error) { + logger.error() << "Network error occurred:" << reply->errorString() << error; + }); connect(reply, &QNetworkReply::sslErrors, [this, reply](const QList &errors) { - qDebug().noquote() << errors; - qDebug() << errorString(ErrorCode::ApiConfigSslError); + QStringList errorStrings; + for (const QSslError &error : errors) { + errorStrings << error.errorString(); + } + logger.error() << "SSL errors:" << errorStrings; + logger.error() << errorString(ErrorCode::ApiConfigSslError); }); } void UpdateController::runInstaller() { + if (m_downloadUrl.isEmpty()) { + logger.error() << "Download URL is empty"; + return; + } + QNetworkRequest request; request.setTransferTimeout(7000); request.setUrl(m_downloadUrl); @@ -119,31 +131,42 @@ void UpdateController::runInstaller() QObject::connect(reply, &QNetworkReply::finished, [this, reply]() { if (reply->error() == QNetworkReply::NoError) { QFile file(installerPath); - if (file.open(QIODevice::WriteOnly)) { - file.write(reply->readAll()); + if (!file.open(QIODevice::WriteOnly)) { + logger.error() << "Failed to open installer file for writing:" << installerPath + << "Error:" << file.errorString(); + reply->deleteLater(); + return; + } + + if (file.write(reply->readAll()) == -1) { + logger.error() << "Failed to write installer data to file:" << installerPath + << "Error:" << file.errorString(); file.close(); - QString t = installerPath; + reply->deleteLater(); + return; + } + + file.close(); + QString t = installerPath; #if defined(Q_OS_WINDOWS) - runWindowsInstaller(t); + runWindowsInstaller(t); #elif defined(Q_OS_MACOS) - runMacInstaller(t); + runMacInstaller(t); #elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) - runLinuxInstaller(t); + runLinuxInstaller(t); #endif - // emit errorOccured(""); - } } else { if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError || reply->error() == QNetworkReply::NetworkError::TimeoutError) { - qDebug() << errorString(ErrorCode::ApiConfigTimeoutError); + logger.error() << errorString(ErrorCode::ApiConfigTimeoutError); } else { QString err = reply->errorString(); - qDebug() << QString::fromUtf8(reply->readAll()); - qDebug() << reply->error(); - qDebug() << err; - qDebug() << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); - qDebug() << errorString(ErrorCode::ApiConfigDownloadError); + logger.error() << QString::fromUtf8(reply->readAll()); + logger.error() << "Network error code:" << QString::number(static_cast(reply->error())); + logger.error() << "Error message:" << err; + logger.error() << "HTTP status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + logger.error() << errorString(ErrorCode::ApiConfigDownloadError); } } reply->deleteLater(); @@ -153,14 +176,13 @@ void UpdateController::runInstaller() #if defined(Q_OS_WINDOWS) int UpdateController::runWindowsInstaller(const QString &installerPath) { - // Start the installer process qint64 pid; bool success = QProcess::startDetached(installerPath, QStringList(), QString(), &pid); if (success) { - qDebug() << "Installation process started with PID:" << pid; + logger.info() << "Installation process started with PID:" << pid; } else { - qDebug() << "Failed to start installation process"; + logger.error() << "Failed to start installation process"; return -1; } @@ -171,7 +193,7 @@ int UpdateController::runWindowsInstaller(const QString &installerPath) #if defined(Q_OS_MACOS) int UpdateController::runMacInstaller(const QString &installerPath) { - qDebug() << "macOS installer path:" << installerPath; + logger.info() << "macOS installer path:" << installerPath; // TODO: Implement macOS installation logic return -1; } @@ -184,16 +206,16 @@ int UpdateController::runLinuxInstaller(const QString &installerPath) QTemporaryDir extractDir; extractDir.setAutoRemove(false); if (!extractDir.isValid()) { - qDebug() << "Failed to create temporary directory"; + logger.error() << "Failed to create temporary directory"; return -1; } - qDebug() << "Temporary directory created:" << extractDir.path(); + logger.info() << "Temporary directory created:" << extractDir.path(); // Create script file in the temporary directory QString scriptPath = extractDir.path() + "/installer.sh"; QFile scriptFile(scriptPath); if (!scriptFile.open(QIODevice::WriteOnly)) { - qDebug() << "Failed to create script file"; + logger.error() << "Failed to create script file"; return -1; } @@ -201,7 +223,7 @@ int UpdateController::runLinuxInstaller(const QString &installerPath) QString scriptContent = amnezia::scriptData(amnezia::ClientScriptType::linux_installer); scriptFile.write(scriptContent.toUtf8()); scriptFile.close(); - qDebug() << "Script file created:" << scriptPath; + logger.info() << "Script file created:" << scriptPath; // Make script executable QFile::setPermissions(scriptPath, QFile::permissions(scriptPath) | QFile::ExeUser); @@ -212,9 +234,9 @@ int UpdateController::runLinuxInstaller(const QString &installerPath) "/bin/bash", QStringList() << scriptPath << extractDir.path() << installerPath, extractDir.path(), &pid); if (success) { - qDebug() << "Installation process started with PID:" << pid; + logger.info() << "Installation process started with PID:" << pid; } else { - qDebug() << "Failed to start installation process"; + logger.error() << "Failed to start installation process"; return -1; } diff --git a/client/ui/controllers/updateController.h b/client/ui/controllers/updateController.h index 85b7c48d5..1f667c045 100644 --- a/client/ui/controllers/updateController.h +++ b/client/ui/controllers/updateController.h @@ -21,7 +21,6 @@ public slots: void runInstaller(); signals: void updateFound(); - void errorOccured(const QString &errorMessage); private: std::shared_ptr m_settings; From 5d334e365cc9af2d9c226cab9b079a0e9a046f50 Mon Sep 17 00:00:00 2001 From: aiamnezia Date: Tue, 24 Dec 2024 19:33:26 +0400 Subject: [PATCH 25/31] Add draft for MacOS installation --- client/client_scripts/mac_installer.sh | 36 ++++++++++++++++ client/core/scripts_registry.cpp | 1 + client/core/scripts_registry.h | 3 +- client/resources.qrc | 1 + client/ui/controllers/updateController.cpp | 48 ++++++++++++++++++++-- 5 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 client/client_scripts/mac_installer.sh diff --git a/client/client_scripts/mac_installer.sh b/client/client_scripts/mac_installer.sh new file mode 100644 index 000000000..a572be8e0 --- /dev/null +++ b/client/client_scripts/mac_installer.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +EXTRACT_DIR="$1" +INSTALLER_PATH="$2" + +# Create and clean extract directory +rm -rf "$EXTRACT_DIR" +mkdir -p "$EXTRACT_DIR" + +# Mount the DMG +hdiutil attach "$INSTALLER_PATH" -mountpoint "$EXTRACT_DIR/mounted_dmg" -nobrowse -quiet +if [ $? -ne 0 ]; then + echo "Failed to mount DMG" + exit 1 +fi + +# Copy the app to /Applications +cp -R "$EXTRACT_DIR/mounted_dmg/AmneziaVPN.app" /Applications/ +if [ $? -ne 0 ]; then + echo "Failed to copy AmneziaVPN.app to /Applications" + hdiutil detach "$EXTRACT_DIR/mounted_dmg" -quiet + exit 1 +fi + +# Unmount the DMG +hdiutil detach "$EXTRACT_DIR/mounted_dmg" -quiet +if [ $? -ne 0 ]; then + echo "Failed to unmount DMG" + exit 1 +fi + +# Optional: Remove the DMG file +rm "$INSTALLER_PATH" + +echo "Installation completed successfully" +exit 0 \ No newline at end of file diff --git a/client/core/scripts_registry.cpp b/client/core/scripts_registry.cpp index 9b02fba9d..d2b17cb93 100644 --- a/client/core/scripts_registry.cpp +++ b/client/core/scripts_registry.cpp @@ -58,6 +58,7 @@ QString amnezia::scriptName(ClientScriptType type) { switch (type) { case ClientScriptType::linux_installer: return QLatin1String("linux_installer.sh"); + case ClientScriptType::mac_installer: return QLatin1String("mac_installer.sh"); default: return QString(); } } diff --git a/client/core/scripts_registry.h b/client/core/scripts_registry.h index 2b4bf087d..87fddbb56 100644 --- a/client/core/scripts_registry.h +++ b/client/core/scripts_registry.h @@ -35,7 +35,8 @@ namespace amnezia enum ClientScriptType { // Client-side scripts - linux_installer + linux_installer, + mac_installer }; QString scriptFolder(DockerContainer container); diff --git a/client/resources.qrc b/client/resources.qrc index ae015b9fc..4b6689e58 100644 --- a/client/resources.qrc +++ b/client/resources.qrc @@ -4,6 +4,7 @@ images/tray/default.png images/tray/error.png client_scripts/linux_installer.sh + client_scripts/mac_installer.sh images/AmneziaVPN.png server_scripts/remove_container.sh server_scripts/setup_host_firewall.sh diff --git a/client/ui/controllers/updateController.cpp b/client/ui/controllers/updateController.cpp index 7eb5c63fd..e62ee02f4 100644 --- a/client/ui/controllers/updateController.cpp +++ b/client/ui/controllers/updateController.cpp @@ -193,9 +193,51 @@ int UpdateController::runWindowsInstaller(const QString &installerPath) #if defined(Q_OS_MACOS) int UpdateController::runMacInstaller(const QString &installerPath) { - logger.info() << "macOS installer path:" << installerPath; - // TODO: Implement macOS installation logic - return -1; + // Create temporary directory for extraction + QTemporaryDir extractDir; + extractDir.setAutoRemove(false); + if (!extractDir.isValid()) { + logger.error() << "Failed to create temporary directory"; + return -1; + } + logger.info() << "Temporary directory created:" << extractDir.path(); + + // Create script file in the temporary directory + QString scriptPath = extractDir.path() + "/mac_installer.sh"; + QFile scriptFile(scriptPath); + if (!scriptFile.open(QIODevice::WriteOnly)) { + logger.error() << "Failed to create script file"; + return -1; + } + + // Get script content from registry + QString scriptContent = amnezia::scriptData(amnezia::ClientScriptType::mac_installer); + if (scriptContent.isEmpty()) { + logger.error() << "macOS installer script content is empty"; + scriptFile.close(); + return -1; + } + + scriptFile.write(scriptContent.toUtf8()); + scriptFile.close(); + logger.info() << "Script file created:" << scriptPath; + + // Make script executable + QFile::setPermissions(scriptPath, QFile::permissions(scriptPath) | QFile::ExeUser); + + // Start detached process + qint64 pid; + bool success = QProcess::startDetached( + "/bin/bash", QStringList() << scriptPath << extractDir.path() << installerPath, extractDir.path(), &pid); + + if (success) { + logger.info() << "Installation process started with PID:" << pid; + } else { + logger.error() << "Failed to start installation process"; + return -1; + } + + return 0; } #endif From eb6c40f92a1428e134b7b07961c39721ad73cd5d Mon Sep 17 00:00:00 2001 From: aiamnezia Date: Wed, 25 Dec 2024 18:17:00 +0400 Subject: [PATCH 26/31] Disable updates checking for Android and iOS --- client/amnezia_application.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/amnezia_application.cpp b/client/amnezia_application.cpp index 3c78717cf..71d840665 100644 --- a/client/amnezia_application.cpp +++ b/client/amnezia_application.cpp @@ -463,9 +463,12 @@ void AmneziaApplication::initControllers() m_updateController.reset(new UpdateController(m_settings)); m_engine->rootContext()->setContextProperty("UpdateController", m_updateController.get()); - m_updateController->checkForUpdates(); +#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) connect(m_updateController.get(), &UpdateController::updateFound, this, [this]() { QTimer::singleShot(1000, this, [this]() { m_pageController->showChangelogDrawer(); }); }); + + m_updateController->checkForUpdates(); +#endif } From 44082462b715d7a22af5003d4eabc5afe1df1046 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Thu, 2 Jan 2025 13:40:55 +0700 Subject: [PATCH 27/31] chore: fixed macos update script --- client/client_scripts/mac_installer.sh | 36 +++++++++++++++----- client/ui/controllers/updateController.cpp | 7 ++-- client/ui/qml/Components/ChangelogDrawer.qml | 24 +++---------- ipc/ipc_interface.rep | 2 -- ipc/ipcserver.cpp | 11 ------ ipc/ipcserver.h | 1 - 6 files changed, 35 insertions(+), 46 deletions(-) diff --git a/client/client_scripts/mac_installer.sh b/client/client_scripts/mac_installer.sh index a572be8e0..186f15024 100644 --- a/client/client_scripts/mac_installer.sh +++ b/client/client_scripts/mac_installer.sh @@ -8,22 +8,42 @@ rm -rf "$EXTRACT_DIR" mkdir -p "$EXTRACT_DIR" # Mount the DMG -hdiutil attach "$INSTALLER_PATH" -mountpoint "$EXTRACT_DIR/mounted_dmg" -nobrowse -quiet +MOUNT_POINT="$EXTRACT_DIR/mounted_dmg" +hdiutil attach "$INSTALLER_PATH" -mountpoint "$MOUNT_POINT" if [ $? -ne 0 ]; then echo "Failed to mount DMG" exit 1 fi -# Copy the app to /Applications -cp -R "$EXTRACT_DIR/mounted_dmg/AmneziaVPN.app" /Applications/ -if [ $? -ne 0 ]; then - echo "Failed to copy AmneziaVPN.app to /Applications" - hdiutil detach "$EXTRACT_DIR/mounted_dmg" -quiet +# Check if the application exists in the mounted DMG +if [ ! -d "$MOUNT_POINT/AmneziaVPN.app" ]; then + echo "Error: AmneziaVPN.app not found in the mounted DMG." + hdiutil detach "$MOUNT_POINT" #-quiet exit 1 fi +# Run the application +echo "Running AmneziaVPN.app from the mounted DMG..." +open "$MOUNT_POINT/AmneziaVPN.app" + +# Get the PID of the app launched from the DMG +APP_PATH="$MOUNT_POINT/AmneziaVPN.app" +PID=$(pgrep -f "$APP_PATH") + +if [ -z "$PID" ]; then + echo "Failed to retrieve PID for AmneziaVPN.app" + hdiutil detach "$MOUNT_POINT" + exit 1 +fi + +# Wait for the specific PID to exit +echo "Waiting for AmneziaVPN.app to exit..." +while kill -0 "$PID" 2>/dev/null; do + sleep 1 +done + # Unmount the DMG -hdiutil detach "$EXTRACT_DIR/mounted_dmg" -quiet +hdiutil detach "$EXTRACT_DIR/mounted_dmg" if [ $? -ne 0 ]; then echo "Failed to unmount DMG" exit 1 @@ -33,4 +53,4 @@ fi rm "$INSTALLER_PATH" echo "Installation completed successfully" -exit 0 \ No newline at end of file +exit 0 diff --git a/client/ui/controllers/updateController.cpp b/client/ui/controllers/updateController.cpp index e62ee02f4..41b19bc1b 100644 --- a/client/ui/controllers/updateController.cpp +++ b/client/ui/controllers/updateController.cpp @@ -147,14 +147,13 @@ void UpdateController::runInstaller() } file.close(); - QString t = installerPath; #if defined(Q_OS_WINDOWS) - runWindowsInstaller(t); + runWindowsInstaller(installerPath); #elif defined(Q_OS_MACOS) - runMacInstaller(t); + runMacInstaller(installerPath); #elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) - runLinuxInstaller(t); + runLinuxInstaller(installerPath); #endif } else { if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError diff --git a/client/ui/qml/Components/ChangelogDrawer.qml b/client/ui/qml/Components/ChangelogDrawer.qml index c2eae80e5..0a919287b 100644 --- a/client/ui/qml/Components/ChangelogDrawer.qml +++ b/client/ui/qml/Components/ChangelogDrawer.qml @@ -16,14 +16,6 @@ DrawerType2 { expandedContent: Item { implicitHeight: root.expandedHeight - Connections { - target: root - enabled: !GC.isMobile() - function onOpened() { - focusItem.forceActiveFocus() - } - } - Header2TextType { id: header anchors.top: parent.top @@ -32,6 +24,7 @@ DrawerType2 { anchors.topMargin: 16 anchors.rightMargin: 16 anchors.leftMargin: 16 + anchors.bottomMargin: 16 text: UpdateController.headerText } @@ -46,9 +39,10 @@ DrawerType2 { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - anchors.topMargin: 48 + anchors.topMargin: 16 anchors.rightMargin: 16 anchors.leftMargin: 16 + anchors.bottomMargin: 16 HoverHandler { enabled: parent.hoveredLink @@ -64,17 +58,11 @@ DrawerType2 { } } - Item { - id: focusItem - KeyNavigation.tab: updateButton - } - BasicButtonType { id: updateButton anchors.bottom: skipButton.top anchors.left: parent.left anchors.right: parent.right - anchors.topMargin: 16 anchors.bottomMargin: 8 anchors.rightMargin: 16 anchors.leftMargin: 16 @@ -87,8 +75,6 @@ DrawerType2 { PageController.showBusyIndicator(false) root.close() } - - KeyNavigation.tab: skipButton } BasicButtonType { @@ -107,13 +93,11 @@ DrawerType2 { textColor: "#D7D8DB" borderWidth: 1 - text: qsTr("Skip this version") + text: qsTr("Skip") clickedFunc: function() { root.close() } - - KeyNavigation.tab: focusItem } } } diff --git a/ipc/ipc_interface.rep b/ipc/ipc_interface.rep index 1647ea190..c0f031fe5 100644 --- a/ipc/ipc_interface.rep +++ b/ipc/ipc_interface.rep @@ -32,7 +32,5 @@ class IpcInterface SLOT( bool enablePeerTraffic( const QJsonObject &configStr) ); SLOT( bool enableKillSwitch( const QJsonObject &excludeAddr, int vpnAdapterIndex) ); SLOT( bool updateResolvers(const QString& ifname, const QList& resolvers) ); - - SLOT( int mountDmg(const QString &path, bool mount) ); }; diff --git a/ipc/ipcserver.cpp b/ipc/ipcserver.cpp index b73ae4077..648fe5404 100644 --- a/ipc/ipcserver.cpp +++ b/ipc/ipcserver.cpp @@ -368,14 +368,3 @@ bool IpcServer::enablePeerTraffic(const QJsonObject &configStr) #endif return true; } - -int IpcServer::mountDmg(const QString &path, bool mount) -{ -#ifdef Q_OS_MACOS - qDebug() << path; - auto res = QProcess::execute(QString("sudo hdiutil %1 %2").arg(mount ? "attach" : "unmount", path)); - qDebug() << res; - return res; -#endif - return 0; -} \ No newline at end of file diff --git a/ipc/ipcserver.h b/ipc/ipcserver.h index 0f0153aa5..f66dae90a 100644 --- a/ipc/ipcserver.h +++ b/ipc/ipcserver.h @@ -38,7 +38,6 @@ class IpcServer : public IpcInterfaceSource virtual bool enableKillSwitch(const QJsonObject &excludeAddr, int vpnAdapterIndex) override; virtual bool disableKillSwitch() override; virtual bool updateResolvers(const QString &ifname, const QList &resolvers) override; - virtual int mountDmg(const QString &path, bool mount) override; private: int m_localpid = 0; From cda9b5d496eac7b2f78c122c7df558897a47ff4d Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Thu, 2 Jan 2025 13:56:11 +0700 Subject: [PATCH 28/31] chore: remove duplicate lines --- client/resources.qrc | 5 ----- 1 file changed, 5 deletions(-) diff --git a/client/resources.qrc b/client/resources.qrc index f057fda98..06fb63293 100644 --- a/client/resources.qrc +++ b/client/resources.qrc @@ -60,9 +60,6 @@ images/tray/error.png client_scripts/linux_installer.sh client_scripts/mac_installer.sh - images/AmneziaVPN.png - server_scripts/remove_container.sh - server_scripts/setup_host_firewall.sh server_scripts/openvpn_cloak/Dockerfile server_scripts/awg/configure_container.sh server_scripts/awg/Dockerfile @@ -177,12 +174,10 @@ ui/qml/Controls2/VerticalRadioButton.qml ui/qml/Controls2/WarningType.qml ui/qml/Components/ChangelogDrawer.qml - fonts/pt-root-ui_vf.ttf ui/qml/Modules/Style/qmldir ui/qml/Filters/ContainersModelFilters.qml ui/qml/main2.qml ui/qml/Modules/Style/AmneziaStyle.qml - ui/qml/Modules/Style/qmldir ui/qml/Pages2/PageDeinstalling.qml ui/qml/Pages2/PageDevMenu.qml ui/qml/Pages2/PageHome.qml From 694b7896e5d0f8a02386d9788aa0af9aaaac215e Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Thu, 2 Jan 2025 14:05:25 +0700 Subject: [PATCH 29/31] chore: post merge fixes --- client/ui/qml/Components/ChangelogDrawer.qml | 6 +++--- client/ui/qml/Controls2/PageType.qml | 1 - client/ui/qml/main2.qml | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/client/ui/qml/Components/ChangelogDrawer.qml b/client/ui/qml/Components/ChangelogDrawer.qml index 0a919287b..1bb767be4 100644 --- a/client/ui/qml/Components/ChangelogDrawer.qml +++ b/client/ui/qml/Components/ChangelogDrawer.qml @@ -13,7 +13,7 @@ DrawerType2 { anchors.fill: parent expandedHeight: parent.height * 0.9 - expandedContent: Item { + expandedStateContent: Item { implicitHeight: root.expandedHeight Header2TextType { @@ -73,7 +73,7 @@ DrawerType2 { PageController.showBusyIndicator(true) UpdateController.runInstaller() PageController.showBusyIndicator(false) - root.close() + root.closeTriggered() } } @@ -96,7 +96,7 @@ DrawerType2 { text: qsTr("Skip") clickedFunc: function() { - root.close() + root.closeTriggered() } } } diff --git a/client/ui/qml/Controls2/PageType.qml b/client/ui/qml/Controls2/PageType.qml index c2ed51975..d7f3317f3 100644 --- a/client/ui/qml/Controls2/PageType.qml +++ b/client/ui/qml/Controls2/PageType.qml @@ -20,7 +20,6 @@ Item { id: timer interval: 200 // Milliseconds onTriggered: { - console.debug(">>> PageType timer triggered") FocusController.resetRootObject() FocusController.setFocusOnDefaultItem() } diff --git a/client/ui/qml/main2.qml b/client/ui/qml/main2.qml index a2b64d326..c57bbd0ae 100644 --- a/client/ui/qml/main2.qml +++ b/client/ui/qml/main2.qml @@ -98,7 +98,7 @@ Window { } function onShowChangelogDrawer() { - changelogDrawer.open() + changelogDrawer.openTriggered() } } From 574773fa7c2e947e927b5f182a0810c9d521a9f1 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Thu, 9 Jan 2025 15:06:25 +0700 Subject: [PATCH 30/31] chore: add missing ifdef --- client/ui/controllers/updateController.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ui/controllers/updateController.cpp b/client/ui/controllers/updateController.cpp index 41b19bc1b..770ca75cc 100644 --- a/client/ui/controllers/updateController.cpp +++ b/client/ui/controllers/updateController.cpp @@ -117,6 +117,7 @@ void UpdateController::checkForUpdates() void UpdateController::runInstaller() { +#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) if (m_downloadUrl.isEmpty()) { logger.error() << "Download URL is empty"; return; @@ -170,6 +171,7 @@ void UpdateController::runInstaller() } reply->deleteLater(); }); +#endif } #if defined(Q_OS_WINDOWS) From 49990f012244444cbea0d2d0bd9520723f3a3b2f Mon Sep 17 00:00:00 2001 From: aiamnezia Date: Fri, 10 Jan 2025 09:42:35 +0400 Subject: [PATCH 31/31] decrease version for testing --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 98f3be14a..b1ed98399 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) set(PROJECT AmneziaVPN) -project(${PROJECT} VERSION 4.8.3.0 +project(${PROJECT} VERSION 4.8.2.0 DESCRIPTION "AmneziaVPN" HOMEPAGE_URL "https://amnezia.org/" )