amnezia-client/client/ui/controllers/updateController.cpp
2025-06-17 00:50:00 +04:00

385 lines
12 KiB
C++

#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;
}