From 871037f887dae54493be6bf73b0c14c70a6f7ac7 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Sat, 25 May 2024 10:04:41 +0200 Subject: [PATCH 01/37] 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 06d2f9ac..1bd196fd 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 5561d7c7..395ed237 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 @@ private: 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 fa0fcaec..ab8fd5d3 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 49fd66d3..8a42e564 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 b286b1b1..58454ef6 100644 --- a/client/ui/controllers/pageController.h +++ b/client/ui/controllers/pageController.h @@ -126,6 +126,8 @@ signals: 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 274df234..7dbf8947 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 00000000..6bf6f9fd --- /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 00000000..986174ac --- /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 00000000..c2eae80e --- /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 7e31bb09..a366fd2d 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 79f2d042..7b49b8b7 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 c734912b..9b72a553 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 bd474481..21e2a591 100644 --- a/ipc/ipcserver.h +++ b/ipc/ipcserver.h @@ -35,6 +35,7 @@ public: 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/37] 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 6bf6f9fd..dfabd7cd 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 1647ea19..7dad63bd 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 2565fc99..c4fe804e 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 5d8b61a2..7e5b21d1 100644 --- a/ipc/ipcserver.h +++ b/ipc/ipcserver.h @@ -39,6 +39,7 @@ public: 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/37] 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 dfabd7cd..32aed926 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 c4fe804e..c6ca5f52 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/37] 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 c6ca5f52..67650221 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/37] 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 32aed926..45acf190 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/37] 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 67650221..d02fe56a 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/37] 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 00000000..8b453907 --- /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/37] 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 8b453907..6ebdb97f 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... +- [LibSsh](https://libssh.org) +- и другие... -## 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) @@ -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/37] 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 00000000..386ae4fe --- /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/37] 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 00000000..5c459fd2 --- /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 00000000..4019357f --- /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/37] 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 6ebdb97f..fe9dd286 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/37] Update README.md --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8b453907..8f887808 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/37] 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 | 114 ++++++--- 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 | 227 +++++++++--------- 11 files changed, 285 insertions(+), 160 deletions(-) diff --git a/client/core/controllers/apiController.cpp b/client/core/controllers/apiController.cpp index c50165e7..6562632a 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 d00d347b..c0db2e12 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 49534606..70f433c6 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 f8516f6e..f9491d4e 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 2a87bde3..81a10f87 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_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)); + m_services.clear(); - m_services.push_back(service); + m_countryCode = data.value(configKey::userCountryCode).toString(); + auto services = data.value(configKey::services).toArray(); + + 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 49918940..c96a49ab 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 @@ public: ServiceDescriptionRole, IsServiceAvailableRole, SpeedRole, - WorkPeriodRole, + TimeLimitRole, RegionRole, FeaturesRole, - PriceRole + PriceRole, + EndDateRole }; explicit ApiServicesModel(QObject *parent = nullptr); @@ -48,8 +50,40 @@ protected: 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 c87499a7..b72b10c3 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 0f18ea30..78bc22cc 100644 --- a/client/ui/models/servers_model.h +++ b/client/ui/models/servers_model.h @@ -46,6 +46,7 @@ public: 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 @@ protected: 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 120313cd..600db85d 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 2d6c1d9b..167e56e5 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 95ae5c8a..ffcfb441 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 + spacing: 4 - Repeater { - id: header - model: proxyServersModel + BackButtonType { + id: backButton - activeFocusOnTab: true - onFocusChanged: { - header.itemAt(0).focusItem.forceActiveFocus() + Layout.topMargin: 20 + KeyNavigation.tab: headerContent.actionButton + + backButtonFunction: function() { + if (nestedStackView.currentIndex === root.pageSettingsApiServerInfo && + root.processedServer.isCountrySelectionAvailable) { + nestedStackView.currentIndex = root.pageSettingsApiLanguageList + } else { + PageController.closePage() + } + } + } + + 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: 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 + } } - delegate: ColumnLayout { + KeyNavigation.tab: tabBar - property alias focusItem: backButton + actionButtonFunction: function() { + if (nestedStackView.currentIndex === root.pageSettingsApiLanguageList) { + nestedStackView.currentIndex = root.pageSettingsApiServerInfo + } else { + serverNameEditDrawer.open() + } + } + } - id: content + DrawerType2 { + id: serverNameEditDrawer - Layout.topMargin: 20 + parent: root - BackButtonType { - id: backButton - KeyNavigation.tab: headerContent.actionButton + anchors.fill: parent + expandedHeight: root.height * 0.35 - backButtonFunction: function() { - if (nestedStackView.currentIndex === root.pageSettingsApiServerInfo && - ServersModel.getProcessedServerData("isCountrySelectionAvailable")) { - nestedStackView.currentIndex = root.pageSettingsApiLanguageList - } else { - PageController.closePage() - } + 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() } } - HeaderType { - id: headerContent + Item { + id: focusItem1 + KeyNavigation.tab: serverName.textField + } + + TextFieldWithHeaderType { + id: serverName + Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 + headerText: qsTr("Server name") + textFieldText: root.processedServer.name + textField.maximumLength: 30 + checkEmptyText: true - 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 - - actionButtonFunction: function() { - if (nestedStackView.currentIndex === root.pageSettingsApiLanguageList) { - nestedStackView.currentIndex = root.pageSettingsApiServerInfo - } else { - serverNameEditDrawer.open() - } - } + KeyNavigation.tab: saveButton } - DrawerType2 { - id: serverNameEditDrawer + BasicButtonType { + id: saveButton - parent: root + Layout.fillWidth: true - anchors.fill: parent - expandedHeight: root.height * 0.35 + text: qsTr("Save") + KeyNavigation.tab: focusItem1 - 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() - } + clickedFunc: function() { + if (serverName.textFieldText === "") { + return } - Item { - id: focusItem1 - KeyNavigation.tab: serverName.textField - } - - TextFieldWithHeaderType { - id: serverName - - Layout.fillWidth: true - headerText: qsTr("Server name") - textFieldText: name - textField.maximumLength: 30 - checkEmptyText: true - - KeyNavigation.tab: saveButton - } - - BasicButtonType { - id: saveButton - - Layout.fillWidth: true - - text: qsTr("Save") - KeyNavigation.tab: focusItem1 - - 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/37] 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 | 165 +++++++++- client/configurators/xray_configurator.h | 4 + client/ui/controllers/exportController.cpp | 9 +- client/ui/controllers/exportController.h | 2 +- client/ui/models/clientManagementModel.cpp | 353 +++++++++++++++++++-- client/ui/models/clientManagementModel.h | 6 + client/ui/qml/Pages2/PageShare.qml | 2 +- 7 files changed, 495 insertions(+), 46 deletions(-) diff --git a/client/configurators/xray_configurator.cpp b/client/configurators/xray_configurator.cpp index 786da47c..514aa821 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) { - QString config = m_serverController->replaceVars(amnezia::scriptData(ProtocolScriptType::xray_template, container), - m_serverController->genVarsForScript(credentials, container, containerConfig)); - - QString xrayPublicKey = - m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::PublicKeyPath, errorCode); - 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); - xrayShortId.replace("\n", ""); - + // 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 ""; } - config.replace("$XRAY_CLIENT_ID", xrayUuid); + // 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 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", ""); + + // 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", 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 2acfdf71..8ed4e775 100644 --- a/client/configurators/xray_configurator.h +++ b/client/configurators/xray_configurator.h @@ -14,6 +14,10 @@ public: 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 2690b5b1..8681406e 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 b031ea39..a2c9fcfa 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 7445d60f..f07eae71 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) { - errorCode = revokeWireGuard(row, container, credentials, 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; + } + 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 60132abe..989120a9 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 @@ private: 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 995fa3e7..d6ce7848 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/37] 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 45acf190..80d04d6a 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 986174ac..ea5c22fa 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 d02fe56a..46c96074 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 7e5b21d1..c3aaaf4e 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 @@ public: 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/37] 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 46c96074..1d0182b8 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/37] 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 1d0182b8..f9519a49 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..."; + logger.info() << "Launching installer with elevated privileges..."; 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("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/37] 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 f9519a49..55a6e6ab 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/37] Optimized code --- ipc/ipcserver.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/ipc/ipcserver.cpp b/ipc/ipcserver.cpp index 55a6e6ab..64d046f3 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/37] 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 | 70 +++++++++++--------- client/ui/controllers/updateController.cpp | 75 ++++++++++++++++++++-- client/ui/controllers/updateController.h | 8 +++ 5 files changed, 186 insertions(+), 35 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 00000000..82987535 --- /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 95b5df4a..9b02fba9 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 d952dafb..2b4bf087 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 { +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 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); + enum ClientScriptType { + // Client-side scripts + linux_installer + }; -QString scriptName(SharedScriptType type); -QString scriptName(ProtocolScriptType type); + QString scriptFolder(DockerContainer container); -QString scriptData(SharedScriptType type); -QString scriptData(ProtocolScriptType type, 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 80d04d6a..2888ec2d 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 ea5c22fa..85b7c48d 100644 --- a/client/ui/controllers/updateController.h +++ b/client/ui/controllers/updateController.h @@ -30,6 +30,14 @@ private: 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/37] 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 2888ec2d..34ad35a9 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/37] 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 7dad63bd..1647ea19 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 64d046f3..b73ae407 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 c3aaaf4e..0f0153aa 100644 --- a/ipc/ipcserver.h +++ b/ipc/ipcserver.h @@ -39,7 +39,6 @@ public: 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/37] 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 5447fe71..ae015b9f 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/37] 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 34ad35a9..7eb5c63f 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 85b7c48d..1f667c04 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/37] 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 00000000..a572be8e --- /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 9b02fba9..d2b17cb9 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 2b4bf087..87fddbb5 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 ae015b9f..4b6689e5 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 7eb5c63f..e62ee02f 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/37] 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 3c78717c..71d84066 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/37] 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 a572be8e..186f1502 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 e62ee02f..41b19bc1 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 c2eae80e..0a919287 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 1647ea19..c0f031fe 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 b73ae407..648fe540 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 0f0153aa..f66dae90 100644 --- a/ipc/ipcserver.h +++ b/ipc/ipcserver.h @@ -38,7 +38,6 @@ public: 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/37] chore: remove duplicate lines --- client/resources.qrc | 5 ----- 1 file changed, 5 deletions(-) diff --git a/client/resources.qrc b/client/resources.qrc index f057fda9..06fb6329 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/37] 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 0a919287..1bb767be 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 c2ed5197..d7f3317f 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 a2b64d32..c57bbd0a 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/37] 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 41b19bc1..770ca75c 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/37] decrease version for testing --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 98f3be14..b1ed9839 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/" ) From 449a8070c1eeb232af16fd0f2edd02ae4ba7785b Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Wed, 5 Mar 2025 13:14:07 +0700 Subject: [PATCH 32/37] chore: added changelog text processing depend on OS --- client/core/controllers/coreController.cpp | 14 +++++ client/core/controllers/coreController.h | 3 ++ client/ui/controllers/updateController.cpp | 61 +++++++++++++++------- ipc/ipcserver.cpp | 21 ++------ ipc/ipcserver.h | 12 ++--- 5 files changed, 68 insertions(+), 43 deletions(-) diff --git a/client/core/controllers/coreController.cpp b/client/core/controllers/coreController.cpp index 82232c99..b58113e5 100644 --- a/client/core/controllers/coreController.cpp +++ b/client/core/controllers/coreController.cpp @@ -141,6 +141,9 @@ void CoreController::initControllers() m_apiConfigsController.reset(new ApiConfigsController(m_serversModel, m_apiServicesModel, m_settings)); m_engine->rootContext()->setContextProperty("ApiConfigsController", m_apiConfigsController.get()); + + m_updateController.reset(new UpdateController(m_settings)); + m_engine->rootContext()->setContextProperty("UpdateController", m_updateController.get()); } void CoreController::initAndroidController() @@ -213,6 +216,7 @@ void CoreController::initSignalHandlers() initAutoConnectHandler(); initAmneziaDnsToggledHandler(); initPrepareConfigHandler(); + initUpdateFoundHandler(); } void CoreController::initNotificationHandler() @@ -339,6 +343,16 @@ void CoreController::initPrepareConfigHandler() }); } +void CoreController::initUpdateFoundHandler() +{ +#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 +} + QSharedPointer CoreController::pageController() const { return m_pageController; diff --git a/client/core/controllers/coreController.h b/client/core/controllers/coreController.h index 700504af..769c5e77 100644 --- a/client/core/controllers/coreController.h +++ b/client/core/controllers/coreController.h @@ -17,6 +17,7 @@ #include "ui/controllers/settingsController.h" #include "ui/controllers/sitesController.h" #include "ui/controllers/systemController.h" +#include "ui/controllers/updateController.h" #include "ui/models/containers_model.h" #include "ui/models/languageModel.h" @@ -80,6 +81,7 @@ private: void initAutoConnectHandler(); void initAmneziaDnsToggledHandler(); void initPrepareConfigHandler(); + void initUpdateFoundHandler(); QQmlApplicationEngine *m_engine {}; // TODO use parent child system here? std::shared_ptr m_settings; @@ -102,6 +104,7 @@ private: QScopedPointer m_sitesController; QScopedPointer m_systemController; QScopedPointer m_appSplitTunnelingController; + QScopedPointer m_updateController; QScopedPointer m_apiSettingsController; QScopedPointer m_apiConfigsController; diff --git a/client/ui/controllers/updateController.cpp b/client/ui/controllers/updateController.cpp index 770ca75c..b7c5cdd5 100644 --- a/client/ui/controllers/updateController.cpp +++ b/client/ui/controllers/updateController.cpp @@ -18,15 +18,13 @@ 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_installer.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 } -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) { } @@ -37,7 +35,34 @@ QString UpdateController::getHeaderText() QString UpdateController::getChangelogText() { - return m_changelogText; + QStringList lines = m_changelogText.split("\n"); + QStringList filteredChangeLogText; + bool add = false; + QString osSection; + +#ifdef Q_OS_WINDOWS + osSection = "### Windows"; +#elif defined(Q_OS_MACOS) + osSection = "### macOS"; +#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) + osSection = "### Linux"; +#endif + + for (const QString &line : lines) { + if (line.startsWith("### General")) { + add = true; + } else if (line.startsWith("### ") && line != osSection) { + add = false; + } else if (line == osSection) { + add = true; + } + + if (add) { + filteredChangeLogText.append(line); + } + } + + return filteredChangeLogText.join("\n"); } void UpdateController::checkForUpdates() @@ -47,7 +72,7 @@ void UpdateController::checkForUpdates() QString endpoint = "https://api.github.com/repos/amnezia-vpn/amnezia-client/releases/latest"; request.setUrl(endpoint); - QNetworkReply *reply = amnApp->manager()->get(request); + QNetworkReply *reply = amnApp->networkManager()->get(request); QObject::connect(reply, &QNetworkReply::finished, [this, reply]() { if (reply->error() == QNetworkReply::NoError) { @@ -127,21 +152,19 @@ void UpdateController::runInstaller() request.setTransferTimeout(7000); request.setUrl(m_downloadUrl); - QNetworkReply *reply = amnApp->manager()->get(request); + QNetworkReply *reply = amnApp->networkManager()->get(request); QObject::connect(reply, &QNetworkReply::finished, [this, reply]() { if (reply->error() == QNetworkReply::NoError) { QFile file(installerPath); if (!file.open(QIODevice::WriteOnly)) { - logger.error() << "Failed to open installer file for writing:" << installerPath - << "Error:" << file.errorString(); + 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(); + logger.error() << "Failed to write installer data to file:" << installerPath << "Error:" << file.errorString(); file.close(); reply->deleteLater(); return; @@ -149,13 +172,13 @@ void UpdateController::runInstaller() file.close(); -#if defined(Q_OS_WINDOWS) + #if defined(Q_OS_WINDOWS) runWindowsInstaller(installerPath); -#elif defined(Q_OS_MACOS) + #elif defined(Q_OS_MACOS) runMacInstaller(installerPath); -#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) + #elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) runLinuxInstaller(installerPath); -#endif + #endif } else { if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError || reply->error() == QNetworkReply::NetworkError::TimeoutError) { @@ -228,8 +251,8 @@ int UpdateController::runMacInstaller(const QString &installerPath) // Start detached process qint64 pid; - bool success = QProcess::startDetached( - "/bin/bash", QStringList() << scriptPath << extractDir.path() << installerPath, extractDir.path(), &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; @@ -273,8 +296,8 @@ int UpdateController::runLinuxInstaller(const QString &installerPath) // Start detached process qint64 pid; - bool success = QProcess::startDetached( - "/bin/bash", QStringList() << scriptPath << extractDir.path() << installerPath, extractDir.path(), &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; diff --git a/ipc/ipcserver.cpp b/ipc/ipcserver.cpp index 0d6be471..17f34499 100644 --- a/ipc/ipcserver.cpp +++ b/ipc/ipcserver.cpp @@ -8,8 +8,8 @@ #include "logger.h" #include "router.h" -#include "../client/protocols/protocols_defs.h" #include "../core/networkUtilities.h" +#include "../client/protocols/protocols_defs.h" #ifdef Q_OS_WIN #include "../client/platforms/windows/daemon/windowsdaemon.h" #include "../client/platforms/windows/daemon/windowsfirewall.h" @@ -55,23 +55,10 @@ 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"; }); - - // 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); - //// } - // }); + QObject::connect(pd.serverNode.data(), &QRemoteObjectHost::destroyed, this, [pd]() { qDebug() << "QRemoteObjectHost::destroyed"; }); m_processes.insert(m_localpid, pd); diff --git a/ipc/ipcserver.h b/ipc/ipcserver.h index f66dae90..9810046b 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,13 @@ public: 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; 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 cb6a2c9195524e76ce8ff40b86500523b898fc8e Mon Sep 17 00:00:00 2001 From: aiamnezia Date: Thu, 22 May 2025 14:05:44 +0400 Subject: [PATCH 33/37] add .vscode to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 5b90fd55..48f50891 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ deploy/build_32/* deploy/build_64/* winbuild*.bat .cache/ +.vscode/ # Qt-es From 68708114d5332a16f82474853443b96790f282e7 Mon Sep 17 00:00:00 2001 From: aiamnezia Date: Thu, 22 May 2025 16:50:16 +0400 Subject: [PATCH 34/37] Change updater downloading method to retrieving link from the gateway --- client/ui/controllers/updateController.cpp | 140 +++++++++++++-------- 1 file changed, 88 insertions(+), 52 deletions(-) diff --git a/client/ui/controllers/updateController.cpp b/client/ui/controllers/updateController.cpp index b7c5cdd5..b1705ec7 100644 --- a/client/ui/controllers/updateController.cpp +++ b/client/ui/controllers/updateController.cpp @@ -4,12 +4,14 @@ #include #include #include +#include #include "amnezia_application.h" #include "core/errorstrings.h" #include "core/scripts_registry.h" #include "logger.h" #include "version.h" +#include "core/controllers/gatewayController.h" namespace { @@ -67,76 +69,110 @@ QString UpdateController::getChangelogText() void UpdateController::checkForUpdates() { - QNetworkRequest request; - request.setTransferTimeout(7000); - QString endpoint = "https://api.github.com/repos/amnezia-vpn/amnezia-client/releases/latest"; - request.setUrl(endpoint); + qDebug() << "checkForUpdates"; + GatewayController gatewayController(m_settings->getGatewayEndpoint(), + m_settings->isDevGatewayEnv(), + 7000, + m_settings->isStrictKillSwitchEnabled()); - QNetworkReply *reply = amnApp->networkManager()->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(); + QByteArray gatewayResponse; + auto err = gatewayController.get(QStringLiteral("%1v1/updater_endpoint"), gatewayResponse); + if (err != ErrorCode::NoError) { + logger.error() << errorString(err); + return; + } + QJsonObject gatewayData = QJsonDocument::fromJson(gatewayResponse).object(); + qDebug() << "gatewayData:" << gatewayData; + QString baseUrl = gatewayData.value("url").toString(); + if (baseUrl.endsWith('/')) { + baseUrl.chop(1); + } + // Fetch version file + QNetworkRequest versionReq; + versionReq.setTransferTimeout(7000); + versionReq.setUrl(QUrl(baseUrl + "/VERSION")); + QNetworkReply* versionReply = amnApp->networkManager()->get(versionReq); + // Handle network and SSL errors for VERSION fetch + QObject::connect(versionReply, &QNetworkReply::errorOccurred, [this, versionReply](QNetworkReply::NetworkError error) { + logger.error() << "Network error occurred while fetching VERSION:" << versionReply->errorString() << error; + }); + QObject::connect(versionReply, &QNetworkReply::sslErrors, [this, versionReply](const QList &errors) { + QStringList errorStrings; + for (const QSslError &err : errors) errorStrings << err.errorString(); + logger.error() << "SSL errors while fetching VERSION:" << errorStrings; + }); + QObject::connect(versionReply, &QNetworkReply::finished, [this, versionReply, baseUrl]() { + if (versionReply->error() == QNetworkReply::NoError) { + QByteArray versionData = versionReply->readAll(); + qDebug() << "versionReply data:" << QString::fromUtf8(versionData); + m_version = QString::fromUtf8(versionData).trimmed(); auto currentVersion = QVersionNumber::fromString(QString(APP_VERSION)); 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(); -#ifdef Q_OS_WINDOWS - if (assetObject.value("name").toString().endsWith(".exe")) { - m_downloadUrl = assetObject.value("browser_download_url").toString(); + // Fetch changelog file + QNetworkRequest changelogReq; + changelogReq.setTransferTimeout(7000); + changelogReq.setUrl(QUrl(baseUrl + "/CHANGELOG")); + QNetworkReply* changelogReply = amnApp->networkManager()->get(changelogReq); + // Handle network and SSL errors for CHANGELOG fetch + QObject::connect(changelogReply, &QNetworkReply::errorOccurred, [this, changelogReply](QNetworkReply::NetworkError error) { + logger.error() << "Network error occurred while fetching CHANGELOG:" << changelogReply->errorString() << error; + }); + QObject::connect(changelogReply, &QNetworkReply::sslErrors, [this, changelogReply](const QList &errors) { + QStringList errorStrings; + for (const QSslError &err : errors) errorStrings << err.errorString(); + logger.error() << "SSL errors while fetching CHANGELOG:" << errorStrings; + }); + QObject::connect(changelogReply, &QNetworkReply::finished, [this, changelogReply, baseUrl]() { + if (changelogReply->error() == QNetworkReply::NoError) { + m_changelogText = QString::fromUtf8(changelogReply->readAll()); + } else { + if (changelogReply->error() == QNetworkReply::NetworkError::OperationCanceledError + || changelogReply->error() == QNetworkReply::NetworkError::TimeoutError) { + logger.error() << errorString(ErrorCode::ApiConfigTimeoutError); + } else { + QString err = changelogReply->errorString(); + logger.error() << QString::fromUtf8(changelogReply->readAll()); + logger.error() << "Network error code:" << QString::number(static_cast(changelogReply->error())); + logger.error() << "Error message:" << err; + logger.error() << "HTTP status:" << changelogReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + logger.error() << errorString(ErrorCode::ApiConfigDownloadError); + } + m_changelogText = tr("Failed to load changelog text"); } + changelogReply->deleteLater(); + m_releaseDate = QStringLiteral("TBD"); + + QString fileName; +#if defined(Q_OS_WINDOWS) + fileName = QString("AmneziaVPN_%1_x64.exe").arg(m_version); #elif defined(Q_OS_MACOS) - if (assetObject.value("name").toString().endsWith(".dmg")) { - m_downloadUrl = assetObject.value("browser_download_url").toString(); - } + fileName = QString("AmneziaVPN_%1_macos.dmg").arg(m_version); #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(); - } + fileName = QString("AmneziaVPN_%1_linux.tar.zip").arg(m_version); #endif - } + m_downloadUrl = baseUrl + "/" + fileName; + qDebug() << "m_downloadUrl:" << m_downloadUrl; - emit updateFound(); + emit updateFound(); + }); } } else { - if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError - || reply->error() == QNetworkReply::NetworkError::TimeoutError) { + // Detailed error logging for VERSION fetch + if (versionReply->error() == QNetworkReply::NetworkError::OperationCanceledError + || versionReply->error() == QNetworkReply::NetworkError::TimeoutError) { logger.error() << errorString(ErrorCode::ApiConfigTimeoutError); } else { - QString err = reply->errorString(); - logger.error() << QString::fromUtf8(reply->readAll()); - logger.error() << "Network error code:" << QString::number(static_cast(reply->error())); + QString err = versionReply->errorString(); + logger.error() << QString::fromUtf8(versionReply->readAll()); + logger.error() << "Network error code:" << QString::number(static_cast(versionReply->error())); logger.error() << "Error message:" << err; - logger.error() << "HTTP status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + logger.error() << "HTTP status:" << versionReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); logger.error() << errorString(ErrorCode::ApiConfigDownloadError); } } - - reply->deleteLater(); - }); - - 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) { - QStringList errorStrings; - for (const QSslError &error : errors) { - errorStrings << error.errorString(); - } - logger.error() << "SSL errors:" << errorStrings; - logger.error() << errorString(ErrorCode::ApiConfigSslError); + versionReply->deleteLater(); }); } From 7023b270292ec90d561862f91419f2b479967c25 Mon Sep 17 00:00:00 2001 From: aiamnezia Date: Sat, 24 May 2025 13:38:00 +0400 Subject: [PATCH 35/37] add Release date file creation to s3 deploy script --- deploy/deploy_s3.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/deploy/deploy_s3.sh b/deploy/deploy_s3.sh index c109a286..862cf9fc 100755 --- a/deploy/deploy_s3.sh +++ b/deploy/deploy_s3.sh @@ -12,7 +12,8 @@ mkdir -p dist cd dist -echo $VERSION >> VERSION +echo $VERSION > VERSION +curl -s https://api.github.com/repos/amnezia-vpn/amnezia-client/releases/tags/$VERSION | jq -r .published_at > RELEASE_DATE curl -s https://api.github.com/repos/amnezia-vpn/amnezia-client/releases/tags/$VERSION | jq -r .body | tr -d '\r' > CHANGELOG if [[ $(cat CHANGELOG) = null ]]; then From 943e76043af2bf19217ebbcf2c0a4d15268c6861 Mon Sep 17 00:00:00 2001 From: aiamnezia Date: Sat, 24 May 2025 16:54:58 +0400 Subject: [PATCH 36/37] Add release date downloading from endpoint --- client/ui/controllers/updateController.cpp | 51 ++++++++++++++++++---- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/client/ui/controllers/updateController.cpp b/client/ui/controllers/updateController.cpp index b1705ec7..bef1a7ac 100644 --- a/client/ui/controllers/updateController.cpp +++ b/client/ui/controllers/updateController.cpp @@ -142,20 +142,55 @@ void UpdateController::checkForUpdates() m_changelogText = tr("Failed to load changelog text"); } changelogReply->deleteLater(); - m_releaseDate = QStringLiteral("TBD"); - QString fileName; + QNetworkRequest dateReq; + dateReq.setTransferTimeout(7000); + dateReq.setUrl(QUrl(baseUrl + "/RELEASE_DATE")); + QNetworkReply* dateReply = amnApp->networkManager()->get(dateReq); + + QObject::connect(dateReply, &QNetworkReply::errorOccurred, [this, dateReply](QNetworkReply::NetworkError error) { + logger.error() << "Network error occurred while fetching RELEASE_DATE:" << dateReply->errorString() << error; + }); + QObject::connect(dateReply, &QNetworkReply::sslErrors, [this, dateReply](const QList &errors) { + QStringList errorStrings; + for (const QSslError &err : errors) errorStrings << err.errorString(); + logger.error() << "SSL errors while fetching RELEASE_DATE:" << errorStrings; + }); + + QObject::connect(dateReply, &QNetworkReply::finished, [this, dateReply, baseUrl]() { + if (dateReply->error() == QNetworkReply::NoError) { + m_releaseDate = QString::fromUtf8(dateReply->readAll()).trimmed(); + } else { + // Detailed error logging for RELEASE_DATE fetch + if (dateReply->error() == QNetworkReply::NetworkError::OperationCanceledError + || dateReply->error() == QNetworkReply::NetworkError::TimeoutError) { + logger.error() << errorString(ErrorCode::ApiConfigTimeoutError); + } else { + QString err = dateReply->errorString(); + logger.error() << QString::fromUtf8(dateReply->readAll()); + logger.error() << "Network error code:" << QString::number(static_cast(dateReply->error())); + logger.error() << "Error message:" << err; + logger.error() << "HTTP status:" << dateReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + logger.error() << errorString(ErrorCode::ApiConfigDownloadError); + } + m_releaseDate = QStringLiteral("Failed to load release date"); + } + dateReply->deleteLater(); + + // Compose installer link and notify + QString fileName; #if defined(Q_OS_WINDOWS) - fileName = QString("AmneziaVPN_%1_x64.exe").arg(m_version); + fileName = QString("AmneziaVPN_%1_x64.exe").arg(m_version); #elif defined(Q_OS_MACOS) - fileName = QString("AmneziaVPN_%1_macos.dmg").arg(m_version); + fileName = QString("AmneziaVPN_%1_macos.dmg").arg(m_version); #elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) - fileName = QString("AmneziaVPN_%1_linux.tar.zip").arg(m_version); + fileName = QString("AmneziaVPN_%1_linux.tar.zip").arg(m_version); #endif - m_downloadUrl = baseUrl + "/" + fileName; - qDebug() << "m_downloadUrl:" << m_downloadUrl; + m_downloadUrl = baseUrl + "/" + fileName; + qDebug() << "m_downloadUrl:" << m_downloadUrl; - emit updateFound(); + emit updateFound(); + }); }); } } else { From 6e06b86cb28de3af6d42a5633aa4d76c25a8bb40 Mon Sep 17 00:00:00 2001 From: aiamnezia Date: Tue, 17 Jun 2025 00:02:52 +0400 Subject: [PATCH 37/37] update check refactoring --- client/ui/controllers/updateController.cpp | 239 +++++++++++---------- client/ui/controllers/updateController.h | 12 ++ 2 files changed, 133 insertions(+), 118 deletions(-) diff --git a/client/ui/controllers/updateController.cpp b/client/ui/controllers/updateController.cpp index bef1a7ac..c98d7ccc 100644 --- a/client/ui/controllers/updateController.cpp +++ b/client/ui/controllers/updateController.cpp @@ -5,6 +5,9 @@ #include #include #include +#include +#include +#include #include "amnezia_application.h" #include "core/errorstrings.h" @@ -70,6 +73,18 @@ QString UpdateController::getChangelogText() void UpdateController::checkForUpdates() { qDebug() << "checkForUpdates"; + if (!fetchGatewayUrl()) return; + if (!fetchVersionInfo()) return; + if (!isNewVersionAvailable()) return; + if (!fetchChangelog()) return; + if (!fetchReleaseDate()) return; + + m_downloadUrl = composeDownloadUrl(); + emit updateFound(); +} + +bool UpdateController::fetchGatewayUrl() +{ GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), 7000, @@ -79,138 +94,103 @@ void UpdateController::checkForUpdates() auto err = gatewayController.get(QStringLiteral("%1v1/updater_endpoint"), gatewayResponse); if (err != ErrorCode::NoError) { logger.error() << errorString(err); - return; + return false; } + QJsonObject gatewayData = QJsonDocument::fromJson(gatewayResponse).object(); qDebug() << "gatewayData:" << gatewayData; + QString baseUrl = gatewayData.value("url").toString(); if (baseUrl.endsWith('/')) { baseUrl.chop(1); } + + m_baseUrl = baseUrl; + return true; +} - // Fetch version file - QNetworkRequest versionReq; - versionReq.setTransferTimeout(7000); - versionReq.setUrl(QUrl(baseUrl + "/VERSION")); - QNetworkReply* versionReply = amnApp->networkManager()->get(versionReq); - // Handle network and SSL errors for VERSION fetch - QObject::connect(versionReply, &QNetworkReply::errorOccurred, [this, versionReply](QNetworkReply::NetworkError error) { - logger.error() << "Network error occurred while fetching VERSION:" << versionReply->errorString() << error; +bool UpdateController::fetchVersionInfo() +{ + QByteArray data; + if (!doSyncGet("/VERSION", data)) { + return false; + } + m_version = QString::fromUtf8(data).trimmed(); + return true; +} + +bool UpdateController::isNewVersionAvailable() +{ + auto currentVersion = QVersionNumber::fromString(QString(APP_VERSION)); + auto newVersion = QVersionNumber::fromString(m_version); + return newVersion > currentVersion; +} + +bool UpdateController::fetchChangelog() +{ + QByteArray data; + if (!doSyncGet("/CHANGELOG", data)) { + m_changelogText = tr("Failed to load changelog text"); + } else { + m_changelogText = QString::fromUtf8(data); + } + return true; +} + +bool UpdateController::fetchReleaseDate() +{ + QByteArray data; + if (!doSyncGet("/RELEASE_DATE", data)) { + m_releaseDate = QStringLiteral("Failed to load release date"); + } else { + m_releaseDate = QString::fromUtf8(data).trimmed(); + } + return true; +} + +void UpdateController::setupNetworkErrorHandling(QNetworkReply* reply, const QString& operation) +{ + QObject::connect(reply, &QNetworkReply::errorOccurred, [this, reply, operation](QNetworkReply::NetworkError error) { + logger.error() << QString("Network error occurred while fetching %1: %2 %3") + .arg(operation, reply->errorString(), QString::number(error)); }); - QObject::connect(versionReply, &QNetworkReply::sslErrors, [this, versionReply](const QList &errors) { + + QObject::connect(reply, &QNetworkReply::sslErrors, [this, reply, operation](const QList &errors) { QStringList errorStrings; - for (const QSslError &err : errors) errorStrings << err.errorString(); - logger.error() << "SSL errors while fetching VERSION:" << errorStrings; - }); - QObject::connect(versionReply, &QNetworkReply::finished, [this, versionReply, baseUrl]() { - if (versionReply->error() == QNetworkReply::NoError) { - QByteArray versionData = versionReply->readAll(); - qDebug() << "versionReply data:" << QString::fromUtf8(versionData); - m_version = QString::fromUtf8(versionData).trimmed(); - auto currentVersion = QVersionNumber::fromString(QString(APP_VERSION)); - auto newVersion = QVersionNumber::fromString(m_version); - if (newVersion > currentVersion) { - // Fetch changelog file - QNetworkRequest changelogReq; - changelogReq.setTransferTimeout(7000); - changelogReq.setUrl(QUrl(baseUrl + "/CHANGELOG")); - QNetworkReply* changelogReply = amnApp->networkManager()->get(changelogReq); - // Handle network and SSL errors for CHANGELOG fetch - QObject::connect(changelogReply, &QNetworkReply::errorOccurred, [this, changelogReply](QNetworkReply::NetworkError error) { - logger.error() << "Network error occurred while fetching CHANGELOG:" << changelogReply->errorString() << error; - }); - QObject::connect(changelogReply, &QNetworkReply::sslErrors, [this, changelogReply](const QList &errors) { - QStringList errorStrings; - for (const QSslError &err : errors) errorStrings << err.errorString(); - logger.error() << "SSL errors while fetching CHANGELOG:" << errorStrings; - }); - QObject::connect(changelogReply, &QNetworkReply::finished, [this, changelogReply, baseUrl]() { - if (changelogReply->error() == QNetworkReply::NoError) { - m_changelogText = QString::fromUtf8(changelogReply->readAll()); - } else { - if (changelogReply->error() == QNetworkReply::NetworkError::OperationCanceledError - || changelogReply->error() == QNetworkReply::NetworkError::TimeoutError) { - logger.error() << errorString(ErrorCode::ApiConfigTimeoutError); - } else { - QString err = changelogReply->errorString(); - logger.error() << QString::fromUtf8(changelogReply->readAll()); - logger.error() << "Network error code:" << QString::number(static_cast(changelogReply->error())); - logger.error() << "Error message:" << err; - logger.error() << "HTTP status:" << changelogReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - logger.error() << errorString(ErrorCode::ApiConfigDownloadError); - } - m_changelogText = tr("Failed to load changelog text"); - } - changelogReply->deleteLater(); - - QNetworkRequest dateReq; - dateReq.setTransferTimeout(7000); - dateReq.setUrl(QUrl(baseUrl + "/RELEASE_DATE")); - QNetworkReply* dateReply = amnApp->networkManager()->get(dateReq); - - QObject::connect(dateReply, &QNetworkReply::errorOccurred, [this, dateReply](QNetworkReply::NetworkError error) { - logger.error() << "Network error occurred while fetching RELEASE_DATE:" << dateReply->errorString() << error; - }); - QObject::connect(dateReply, &QNetworkReply::sslErrors, [this, dateReply](const QList &errors) { - QStringList errorStrings; - for (const QSslError &err : errors) errorStrings << err.errorString(); - logger.error() << "SSL errors while fetching RELEASE_DATE:" << errorStrings; - }); - - QObject::connect(dateReply, &QNetworkReply::finished, [this, dateReply, baseUrl]() { - if (dateReply->error() == QNetworkReply::NoError) { - m_releaseDate = QString::fromUtf8(dateReply->readAll()).trimmed(); - } else { - // Detailed error logging for RELEASE_DATE fetch - if (dateReply->error() == QNetworkReply::NetworkError::OperationCanceledError - || dateReply->error() == QNetworkReply::NetworkError::TimeoutError) { - logger.error() << errorString(ErrorCode::ApiConfigTimeoutError); - } else { - QString err = dateReply->errorString(); - logger.error() << QString::fromUtf8(dateReply->readAll()); - logger.error() << "Network error code:" << QString::number(static_cast(dateReply->error())); - logger.error() << "Error message:" << err; - logger.error() << "HTTP status:" << dateReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - logger.error() << errorString(ErrorCode::ApiConfigDownloadError); - } - m_releaseDate = QStringLiteral("Failed to load release date"); - } - dateReply->deleteLater(); - - // Compose installer link and notify - QString fileName; -#if defined(Q_OS_WINDOWS) - fileName = QString("AmneziaVPN_%1_x64.exe").arg(m_version); -#elif defined(Q_OS_MACOS) - fileName = QString("AmneziaVPN_%1_macos.dmg").arg(m_version); -#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) - fileName = QString("AmneziaVPN_%1_linux.tar.zip").arg(m_version); -#endif - m_downloadUrl = baseUrl + "/" + fileName; - qDebug() << "m_downloadUrl:" << m_downloadUrl; - - emit updateFound(); - }); - }); - } - } else { - // Detailed error logging for VERSION fetch - if (versionReply->error() == QNetworkReply::NetworkError::OperationCanceledError - || versionReply->error() == QNetworkReply::NetworkError::TimeoutError) { - logger.error() << errorString(ErrorCode::ApiConfigTimeoutError); - } else { - QString err = versionReply->errorString(); - logger.error() << QString::fromUtf8(versionReply->readAll()); - logger.error() << "Network error code:" << QString::number(static_cast(versionReply->error())); - logger.error() << "Error message:" << err; - logger.error() << "HTTP status:" << versionReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - logger.error() << errorString(ErrorCode::ApiConfigDownloadError); - } + for (const QSslError &err : errors) { + errorStrings << err.errorString(); } - versionReply->deleteLater(); + logger.error() << QString("SSL errors while fetching %1: %2").arg(operation, errorStrings.join("; ")); }); } +void UpdateController::handleNetworkError(QNetworkReply* reply, const QString& operation) +{ + if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError + || reply->error() == QNetworkReply::NetworkError::TimeoutError) { + logger.error() << errorString(ErrorCode::ApiConfigTimeoutError); + } else { + QString err = reply->errorString(); + 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); + } +} + +QString UpdateController::composeDownloadUrl() +{ + QString fileName; +#if defined(Q_OS_WINDOWS) + fileName = QString("AmneziaVPN_%1_x64.exe").arg(m_version); +#elif defined(Q_OS_MACOS) + fileName = QString("AmneziaVPN_%1_macos.dmg").arg(m_version); +#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) + fileName = QString("AmneziaVPN_%1_linux.tar.zip").arg(m_version); +#endif + return m_baseUrl + "/" + fileName; +} + void UpdateController::runInstaller() { #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) @@ -380,3 +360,26 @@ int UpdateController::runLinuxInstaller(const QString &installerPath) return 0; } #endif + +bool UpdateController::doSyncGet(const QString& endpoint, QByteArray& outData) +{ + QNetworkRequest req; + req.setTransferTimeout(7000); + req.setUrl(QUrl(m_baseUrl + endpoint)); + + QNetworkReply* reply = amnApp->networkManager()->get(req); + setupNetworkErrorHandling(reply, endpoint); + + QEventLoop loop; + QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + loop.exec(); + + bool ok = (reply->error() == QNetworkReply::NoError); + if (ok) { + outData = reply->readAll(); + } else { + handleNetworkError(reply, endpoint); + } + reply->deleteLater(); + return ok; +} diff --git a/client/ui/controllers/updateController.h b/client/ui/controllers/updateController.h index 1f667c04..4dfcf5d8 100644 --- a/client/ui/controllers/updateController.h +++ b/client/ui/controllers/updateController.h @@ -2,6 +2,7 @@ #define UPDATECONTROLLER_H #include +#include #include "settings.h" @@ -23,8 +24,19 @@ signals: void updateFound(); private: + bool fetchGatewayUrl(); + bool fetchVersionInfo(); + bool fetchChangelog(); + bool fetchReleaseDate(); + bool isNewVersionAvailable(); + bool doSyncGet(const QString& endpoint, QByteArray& outData); + void setupNetworkErrorHandling(QNetworkReply* reply, const QString& operation); + void handleNetworkError(QNetworkReply* reply, const QString& operation); + QString composeDownloadUrl(); + std::shared_ptr m_settings; + QString m_baseUrl; QString m_changelogText; QString m_version; QString m_releaseDate;