#include "updateController.h" #include #include #include #include #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 { 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, 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 &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(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(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; }