Merge 273eeb78c6 into 979ab42c5a
This commit is contained in:
commit
527b956fcd
15 changed files with 748 additions and 34 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -9,6 +9,7 @@ deploy/build_32/*
|
||||||
deploy/build_64/*
|
deploy/build_64/*
|
||||||
winbuild*.bat
|
winbuild*.bat
|
||||||
.cache/
|
.cache/
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
|
||||||
# Qt-es
|
# Qt-es
|
||||||
|
|
|
||||||
44
client/client_scripts/linux_installer.sh
Normal file
44
client/client_scripts/linux_installer.sh
Normal file
|
|
@ -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
|
||||||
56
client/client_scripts/mac_installer.sh
Normal file
56
client/client_scripts/mac_installer.sh
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
EXTRACT_DIR="$1"
|
||||||
|
INSTALLER_PATH="$2"
|
||||||
|
|
||||||
|
# Create and clean extract directory
|
||||||
|
rm -rf "$EXTRACT_DIR"
|
||||||
|
mkdir -p "$EXTRACT_DIR"
|
||||||
|
|
||||||
|
# Mount the DMG
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
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
|
||||||
|
|
@ -151,6 +151,9 @@ void CoreController::initControllers()
|
||||||
|
|
||||||
m_apiPremV1MigrationController.reset(new ApiPremV1MigrationController(m_serversModel, m_settings, this));
|
m_apiPremV1MigrationController.reset(new ApiPremV1MigrationController(m_serversModel, m_settings, this));
|
||||||
m_engine->rootContext()->setContextProperty("ApiPremV1MigrationController", m_apiPremV1MigrationController.get());
|
m_engine->rootContext()->setContextProperty("ApiPremV1MigrationController", m_apiPremV1MigrationController.get());
|
||||||
|
|
||||||
|
m_updateController.reset(new UpdateController(m_settings));
|
||||||
|
m_engine->rootContext()->setContextProperty("UpdateController", m_updateController.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
void CoreController::initAndroidController()
|
void CoreController::initAndroidController()
|
||||||
|
|
@ -226,6 +229,7 @@ void CoreController::initSignalHandlers()
|
||||||
initImportPremiumV2VpnKeyHandler();
|
initImportPremiumV2VpnKeyHandler();
|
||||||
initShowMigrationDrawerHandler();
|
initShowMigrationDrawerHandler();
|
||||||
initStrictKillSwitchHandler();
|
initStrictKillSwitchHandler();
|
||||||
|
initUpdateFoundHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CoreController::initNotificationHandler()
|
void CoreController::initNotificationHandler()
|
||||||
|
|
@ -393,6 +397,16 @@ void CoreController::initStrictKillSwitchHandler()
|
||||||
&VpnConnection::onKillSwitchModeChanged);
|
&VpnConnection::onKillSwitchModeChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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<PageController> CoreController::pageController() const
|
QSharedPointer<PageController> CoreController::pageController() const
|
||||||
{
|
{
|
||||||
return m_pageController;
|
return m_pageController;
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
#include "ui/controllers/settingsController.h"
|
#include "ui/controllers/settingsController.h"
|
||||||
#include "ui/controllers/sitesController.h"
|
#include "ui/controllers/sitesController.h"
|
||||||
#include "ui/controllers/systemController.h"
|
#include "ui/controllers/systemController.h"
|
||||||
|
#include "ui/controllers/updateController.h"
|
||||||
|
|
||||||
#include "ui/models/allowed_dns_model.h"
|
#include "ui/models/allowed_dns_model.h"
|
||||||
#include "ui/models/containers_model.h"
|
#include "ui/models/containers_model.h"
|
||||||
|
|
@ -86,6 +87,7 @@ private:
|
||||||
void initImportPremiumV2VpnKeyHandler();
|
void initImportPremiumV2VpnKeyHandler();
|
||||||
void initShowMigrationDrawerHandler();
|
void initShowMigrationDrawerHandler();
|
||||||
void initStrictKillSwitchHandler();
|
void initStrictKillSwitchHandler();
|
||||||
|
void initUpdateFoundHandler();
|
||||||
|
|
||||||
QQmlApplicationEngine *m_engine {}; // TODO use parent child system here?
|
QQmlApplicationEngine *m_engine {}; // TODO use parent child system here?
|
||||||
std::shared_ptr<Settings> m_settings;
|
std::shared_ptr<Settings> m_settings;
|
||||||
|
|
@ -109,6 +111,7 @@ private:
|
||||||
QScopedPointer<SystemController> m_systemController;
|
QScopedPointer<SystemController> m_systemController;
|
||||||
QScopedPointer<AppSplitTunnelingController> m_appSplitTunnelingController;
|
QScopedPointer<AppSplitTunnelingController> m_appSplitTunnelingController;
|
||||||
QScopedPointer<AllowedDnsController> m_allowedDnsController;
|
QScopedPointer<AllowedDnsController> m_allowedDnsController;
|
||||||
|
QScopedPointer<UpdateController> m_updateController;
|
||||||
|
|
||||||
QScopedPointer<ApiSettingsController> m_apiSettingsController;
|
QScopedPointer<ApiSettingsController> m_apiSettingsController;
|
||||||
QScopedPointer<ApiConfigsController> m_apiConfigsController;
|
QScopedPointer<ApiConfigsController> m_apiConfigsController;
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,15 @@ QString amnezia::scriptName(ProtocolScriptType type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString amnezia::scriptName(ClientScriptType type)
|
||||||
|
{
|
||||||
|
switch (type) {
|
||||||
|
case ClientScriptType::linux_installer: return QLatin1String("linux_installer.sh");
|
||||||
|
case ClientScriptType::mac_installer: return QLatin1String("mac_installer.sh");
|
||||||
|
default: return QString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QString amnezia::scriptData(amnezia::SharedScriptType type)
|
QString amnezia::scriptData(amnezia::SharedScriptType type)
|
||||||
{
|
{
|
||||||
QString fileName = QString(":/server_scripts/%1").arg(amnezia::scriptName(type));
|
QString fileName = QString(":/server_scripts/%1").arg(amnezia::scriptName(type));
|
||||||
|
|
@ -81,3 +90,19 @@ QString amnezia::scriptData(amnezia::ProtocolScriptType type, DockerContainer co
|
||||||
data.replace("\r", "");
|
data.replace("\r", "");
|
||||||
return data;
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,44 +1,53 @@
|
||||||
#ifndef SCRIPTS_REGISTRY_H
|
#ifndef SCRIPTS_REGISTRY_H
|
||||||
#define SCRIPTS_REGISTRY_H
|
#define SCRIPTS_REGISTRY_H
|
||||||
|
|
||||||
#include <QLatin1String>
|
|
||||||
#include "core/defs.h"
|
|
||||||
#include "containers/containers_defs.h"
|
#include "containers/containers_defs.h"
|
||||||
|
#include "core/defs.h"
|
||||||
|
#include <QLatin1String>
|
||||||
|
|
||||||
namespace amnezia {
|
namespace amnezia
|
||||||
|
{
|
||||||
|
|
||||||
enum SharedScriptType {
|
enum SharedScriptType {
|
||||||
// General scripts
|
// General scripts
|
||||||
prepare_host,
|
prepare_host,
|
||||||
install_docker,
|
install_docker,
|
||||||
build_container,
|
build_container,
|
||||||
remove_container,
|
remove_container,
|
||||||
remove_all_containers,
|
remove_all_containers,
|
||||||
setup_host_firewall,
|
setup_host_firewall,
|
||||||
check_connection,
|
check_connection,
|
||||||
check_server_is_busy,
|
check_server_is_busy,
|
||||||
check_user_in_sudo
|
check_user_in_sudo
|
||||||
};
|
};
|
||||||
enum ProtocolScriptType {
|
|
||||||
// Protocol scripts
|
|
||||||
dockerfile,
|
|
||||||
run_container,
|
|
||||||
configure_container,
|
|
||||||
container_startup,
|
|
||||||
openvpn_template,
|
|
||||||
wireguard_template,
|
|
||||||
awg_template,
|
|
||||||
xray_template
|
|
||||||
};
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
mac_installer
|
||||||
|
};
|
||||||
|
|
||||||
QString scriptName(SharedScriptType type);
|
QString scriptFolder(DockerContainer container);
|
||||||
QString scriptName(ProtocolScriptType type);
|
|
||||||
|
|
||||||
QString scriptData(SharedScriptType type);
|
QString scriptName(SharedScriptType type);
|
||||||
QString scriptData(ProtocolScriptType type, DockerContainer container);
|
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
|
#endif // SCRIPTS_REGISTRY_H
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,9 @@
|
||||||
<file>images/tray/active.png</file>
|
<file>images/tray/active.png</file>
|
||||||
<file>images/tray/default.png</file>
|
<file>images/tray/default.png</file>
|
||||||
<file>images/tray/error.png</file>
|
<file>images/tray/error.png</file>
|
||||||
|
<file>client_scripts/linux_installer.sh</file>
|
||||||
|
<file>client_scripts/mac_installer.sh</file>
|
||||||
|
<file>server_scripts/openvpn_cloak/Dockerfile</file>
|
||||||
<file>server_scripts/awg/configure_container.sh</file>
|
<file>server_scripts/awg/configure_container.sh</file>
|
||||||
<file>server_scripts/awg/Dockerfile</file>
|
<file>server_scripts/awg/Dockerfile</file>
|
||||||
<file>server_scripts/awg/run_container.sh</file>
|
<file>server_scripts/awg/run_container.sh</file>
|
||||||
|
|
@ -175,10 +178,11 @@
|
||||||
<file>ui/qml/Controls2/TopCloseButtonType.qml</file>
|
<file>ui/qml/Controls2/TopCloseButtonType.qml</file>
|
||||||
<file>ui/qml/Controls2/VerticalRadioButton.qml</file>
|
<file>ui/qml/Controls2/VerticalRadioButton.qml</file>
|
||||||
<file>ui/qml/Controls2/WarningType.qml</file>
|
<file>ui/qml/Controls2/WarningType.qml</file>
|
||||||
|
<file>ui/qml/Components/ChangelogDrawer.qml</file>
|
||||||
|
<file>ui/qml/Modules/Style/qmldir</file>
|
||||||
<file>ui/qml/Filters/ContainersModelFilters.qml</file>
|
<file>ui/qml/Filters/ContainersModelFilters.qml</file>
|
||||||
<file>ui/qml/main2.qml</file>
|
<file>ui/qml/main2.qml</file>
|
||||||
<file>ui/qml/Modules/Style/AmneziaStyle.qml</file>
|
<file>ui/qml/Modules/Style/AmneziaStyle.qml</file>
|
||||||
<file>ui/qml/Modules/Style/qmldir</file>
|
|
||||||
<file>ui/qml/Pages2/PageDeinstalling.qml</file>
|
<file>ui/qml/Pages2/PageDeinstalling.qml</file>
|
||||||
<file>ui/qml/Pages2/PageDevMenu.qml</file>
|
<file>ui/qml/Pages2/PageDevMenu.qml</file>
|
||||||
<file>ui/qml/Pages2/PageHome.qml</file>
|
<file>ui/qml/Pages2/PageHome.qml</file>
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,7 @@ signals:
|
||||||
void escapePressed();
|
void escapePressed();
|
||||||
void closeTopDrawer();
|
void closeTopDrawer();
|
||||||
|
|
||||||
|
void showChangelogDrawer();
|
||||||
private:
|
private:
|
||||||
QSharedPointer<ServersModel> m_serversModel;
|
QSharedPointer<ServersModel> m_serversModel;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ class SystemController : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit SystemController(const std::shared_ptr<Settings> &setting, QObject *parent = nullptr);
|
explicit SystemController(const std::shared_ptr<Settings> &settings, QObject *parent = nullptr);
|
||||||
|
|
||||||
static void saveFile(const QString &fileName, const QString &data);
|
static void saveFile(const QString &fileName, const QString &data);
|
||||||
static bool readFile(const QString &fileName, QByteArray &data);
|
static bool readFile(const QString &fileName, QByteArray &data);
|
||||||
|
|
|
||||||
385
client/ui/controllers/updateController.cpp
Normal file
385
client/ui/controllers/updateController.cpp
Normal file
|
|
@ -0,0 +1,385 @@
|
||||||
|
#include "updateController.h"
|
||||||
|
|
||||||
|
#include <QNetworkAccessManager>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QVersionNumber>
|
||||||
|
#include <QtConcurrent>
|
||||||
|
#include <QUrl>
|
||||||
|
#include <QEventLoop>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
|
||||||
|
#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
|
||||||
|
{
|
||||||
|
Logger logger("UpdateController");
|
||||||
|
|
||||||
|
#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";
|
||||||
|
#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> &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()
|
||||||
|
{
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
m_settings->isStrictKillSwitchEnabled());
|
||||||
|
|
||||||
|
QByteArray gatewayResponse;
|
||||||
|
auto err = gatewayController.get(QStringLiteral("%1v1/updater_endpoint"), gatewayResponse);
|
||||||
|
if (err != ErrorCode::NoError) {
|
||||||
|
logger.error() << errorString(err);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(reply, &QNetworkReply::sslErrors, [this, reply, operation](const QList<QSslError> &errors) {
|
||||||
|
QStringList errorStrings;
|
||||||
|
for (const QSslError &err : errors) {
|
||||||
|
errorStrings << err.errorString();
|
||||||
|
}
|
||||||
|
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<int>(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)
|
||||||
|
if (m_downloadUrl.isEmpty()) {
|
||||||
|
logger.error() << "Download URL is empty";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QNetworkRequest request;
|
||||||
|
request.setTransferTimeout(7000);
|
||||||
|
request.setUrl(m_downloadUrl);
|
||||||
|
|
||||||
|
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();
|
||||||
|
reply->deleteLater();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.write(reply->readAll()) == -1) {
|
||||||
|
logger.error() << "Failed to write installer data to file:" << installerPath << "Error:" << file.errorString();
|
||||||
|
file.close();
|
||||||
|
reply->deleteLater();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
#if defined(Q_OS_WINDOWS)
|
||||||
|
runWindowsInstaller(installerPath);
|
||||||
|
#elif defined(Q_OS_MACOS)
|
||||||
|
runMacInstaller(installerPath);
|
||||||
|
#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
|
||||||
|
runLinuxInstaller(installerPath);
|
||||||
|
#endif
|
||||||
|
} else {
|
||||||
|
if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError
|
||||||
|
|| reply->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<int>(reply->error()));
|
||||||
|
logger.error() << "Error message:" << err;
|
||||||
|
logger.error() << "HTTP status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||||
|
logger.error() << errorString(ErrorCode::ApiConfigDownloadError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reply->deleteLater();
|
||||||
|
});
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(Q_OS_WINDOWS)
|
||||||
|
int UpdateController::runWindowsInstaller(const QString &installerPath)
|
||||||
|
{
|
||||||
|
qint64 pid;
|
||||||
|
bool success = QProcess::startDetached(installerPath, QStringList(), QString(), &pid);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
logger.info() << "Installation process started with PID:" << pid;
|
||||||
|
} else {
|
||||||
|
logger.error() << "Failed to start installation process";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(Q_OS_MACOS)
|
||||||
|
int UpdateController::runMacInstaller(const QString &installerPath)
|
||||||
|
{
|
||||||
|
// Create temporary directory for extraction
|
||||||
|
QTemporaryDir extractDir;
|
||||||
|
extractDir.setAutoRemove(false);
|
||||||
|
if (!extractDir.isValid()) {
|
||||||
|
logger.error() << "Failed to create temporary directory";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
logger.info() << "Temporary directory created:" << extractDir.path();
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
|
||||||
|
int UpdateController::runLinuxInstaller(const QString &installerPath)
|
||||||
|
{
|
||||||
|
// Create temporary directory for extraction
|
||||||
|
QTemporaryDir extractDir;
|
||||||
|
extractDir.setAutoRemove(false);
|
||||||
|
if (!extractDir.isValid()) {
|
||||||
|
logger.error() << "Failed to create temporary directory";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
logger.info() << "Temporary directory created:" << extractDir.path();
|
||||||
|
|
||||||
|
// Create script file in the temporary directory
|
||||||
|
QString scriptPath = extractDir.path() + "/installer.sh";
|
||||||
|
QFile scriptFile(scriptPath);
|
||||||
|
if (!scriptFile.open(QIODevice::WriteOnly)) {
|
||||||
|
logger.error() << "Failed to create script file";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get script content from registry
|
||||||
|
QString scriptContent = amnezia::scriptData(amnezia::ClientScriptType::linux_installer);
|
||||||
|
scriptFile.write(scriptContent.toUtf8());
|
||||||
|
scriptFile.close();
|
||||||
|
logger.info() << "Script file created:" << scriptPath;
|
||||||
|
|
||||||
|
// Make script executable
|
||||||
|
QFile::setPermissions(scriptPath, QFile::permissions(scriptPath) | QFile::ExeUser);
|
||||||
|
|
||||||
|
// Start detached process
|
||||||
|
qint64 pid;
|
||||||
|
bool success =
|
||||||
|
QProcess::startDetached("/bin/bash", QStringList() << scriptPath << extractDir.path() << installerPath, extractDir.path(), &pid);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
logger.info() << "Installation process started with PID:" << pid;
|
||||||
|
} else {
|
||||||
|
logger.error() << "Failed to start installation process";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
54
client/ui/controllers/updateController.h
Normal file
54
client/ui/controllers/updateController.h
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
#ifndef UPDATECONTROLLER_H
|
||||||
|
#define UPDATECONTROLLER_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
|
||||||
|
#include "settings.h"
|
||||||
|
|
||||||
|
class UpdateController : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit UpdateController(const std::shared_ptr<Settings> &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();
|
||||||
|
|
||||||
|
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<Settings> m_settings;
|
||||||
|
|
||||||
|
QString m_baseUrl;
|
||||||
|
QString m_changelogText;
|
||||||
|
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
|
||||||
103
client/ui/qml/Components/ChangelogDrawer.qml
Normal file
103
client/ui/qml/Components/ChangelogDrawer.qml
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
expandedStateContent: Item {
|
||||||
|
implicitHeight: root.expandedHeight
|
||||||
|
|
||||||
|
Header2TextType {
|
||||||
|
id: header
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.topMargin: 16
|
||||||
|
anchors.rightMargin: 16
|
||||||
|
anchors.leftMargin: 16
|
||||||
|
anchors.bottomMargin: 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: 16
|
||||||
|
anchors.rightMargin: 16
|
||||||
|
anchors.leftMargin: 16
|
||||||
|
anchors.bottomMargin: 16
|
||||||
|
|
||||||
|
HoverHandler {
|
||||||
|
enabled: parent.hoveredLink
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
}
|
||||||
|
|
||||||
|
onLinkActivated: function(link) {
|
||||||
|
Qt.openUrlExternally(link)
|
||||||
|
}
|
||||||
|
|
||||||
|
text: UpdateController.changelogText
|
||||||
|
textFormat: Text.MarkdownText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BasicButtonType {
|
||||||
|
id: updateButton
|
||||||
|
anchors.bottom: skipButton.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottomMargin: 8
|
||||||
|
anchors.rightMargin: 16
|
||||||
|
anchors.leftMargin: 16
|
||||||
|
|
||||||
|
text: qsTr("Update")
|
||||||
|
|
||||||
|
clickedFunc: function() {
|
||||||
|
PageController.showBusyIndicator(true)
|
||||||
|
UpdateController.runInstaller()
|
||||||
|
PageController.showBusyIndicator(false)
|
||||||
|
root.closeTriggered()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
clickedFunc: function() {
|
||||||
|
root.closeTriggered()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -96,6 +96,10 @@ Window {
|
||||||
busyIndicator.visible = visible
|
busyIndicator.visible = visible
|
||||||
PageController.disableControls(visible)
|
PageController.disableControls(visible)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onShowChangelogDrawer() {
|
||||||
|
changelogDrawer.openTriggered()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
|
|
@ -285,4 +289,14 @@ Window {
|
||||||
onAccepted: SystemController.fileDialogClosed(true)
|
onAccepted: SystemController.fileDialogClosed(true)
|
||||||
onRejected: SystemController.fileDialogClosed(false)
|
onRejected: SystemController.fileDialogClosed(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
ChangelogDrawer {
|
||||||
|
id: changelogDrawer
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@ mkdir -p dist
|
||||||
|
|
||||||
cd 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
|
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
|
if [[ $(cat CHANGELOG) = null ]]; then
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue