#include "updateController.h" #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"; 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; } 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) { // 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); } } versionReply->deleteLater(); }); } 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