mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-05-08 14:33:23 +00:00
merge dev
This commit is contained in:
@@ -69,6 +69,7 @@ endif()
|
||||
qt6_add_resources(QRC ${QRC}
|
||||
${CMAKE_CURRENT_LIST_DIR}/images/images.qrc
|
||||
${CMAKE_CURRENT_LIST_DIR}/images/flagKit.qrc
|
||||
${CMAKE_CURRENT_LIST_DIR}/client_scripts/clientScripts.qrc
|
||||
${CMAKE_CURRENT_LIST_DIR}/ui/qml/qml.qrc
|
||||
${CMAKE_CURRENT_LIST_DIR}/server_scripts/serverScripts.qrc
|
||||
)
|
||||
|
||||
6
client/client_scripts/clientScripts.qrc
Normal file
6
client/client_scripts/clientScripts.qrc
Normal file
@@ -0,0 +1,6 @@
|
||||
<RCC>
|
||||
<qresource prefix="/client_scripts">
|
||||
<file>linux_installer.sh</file>
|
||||
<file>mac_installer.sh</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
29
client/client_scripts/linux_installer.sh
Normal file
29
client/client_scripts/linux_installer.sh
Normal file
@@ -0,0 +1,29 @@
|
||||
#!/bin/bash
|
||||
|
||||
EXTRACT_DIR="$1"
|
||||
INSTALLER_PATH="$2"
|
||||
|
||||
# Create and clean extract directory
|
||||
rm -rf "$EXTRACT_DIR"
|
||||
mkdir -p "$EXTRACT_DIR"
|
||||
|
||||
# Extract TAR archive
|
||||
tar -xf "$INSTALLER_PATH" -C "$EXTRACT_DIR"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo 'Failed to extract TAR archive'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 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
|
||||
42
client/client_scripts/mac_installer.sh
Normal file
42
client/client_scripts/mac_installer.sh
Normal file
@@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
|
||||
EXTRACT_DIR="$1"
|
||||
INSTALLER_PATH="$2"
|
||||
|
||||
set -e
|
||||
|
||||
echo "[AmneziaVPN] Installer package: $INSTALLER_PATH"
|
||||
|
||||
if [ ! -f "$INSTALLER_PATH" ]; then
|
||||
echo "[AmneziaVPN] ERROR: Installer package not found: $INSTALLER_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PKG_PATH="$INSTALLER_PATH"
|
||||
echo "[AmneziaVPN] Using PKG: $PKG_PATH"
|
||||
|
||||
# Optional: basic signature/gatekeeper checks (non-fatal)
|
||||
if command -v pkgutil >/dev/null 2>&1; then
|
||||
pkgutil --check-signature "$PKG_PATH" || true
|
||||
fi
|
||||
if command -v spctl >/dev/null 2>&1; then
|
||||
spctl -a -vvv -t install "$PKG_PATH" || true
|
||||
fi
|
||||
|
||||
# Run installer with admin privileges via AppleScript (prompts for password)
|
||||
echo "[AmneziaVPN] Running installer..."
|
||||
OSA_CMD='do shell script "/usr/sbin/installer -pkg '"$PKG_PATH"' -target /" with administrator privileges'
|
||||
osascript -e "$OSA_CMD"
|
||||
|
||||
STATUS=$?
|
||||
if [ $STATUS -ne 0 ]; then
|
||||
echo "[AmneziaVPN] ERROR: installer exited with status $STATUS"
|
||||
exit $STATUS
|
||||
fi
|
||||
|
||||
echo "[AmneziaVPN] Cleaning up..."
|
||||
rm -f "$INSTALLER_PATH" || true
|
||||
rm -rf "$EXTRACT_DIR" 2>/dev/null || true
|
||||
|
||||
echo "[AmneziaVPN] Installation completed successfully"
|
||||
exit 0
|
||||
@@ -45,6 +45,7 @@ set(HEADERS ${HEADERS}
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/servicesCatalogController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/subscriptionController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/newsController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/updateController.h
|
||||
${CLIENT_ROOT_DIR}/core/repositories/secureServersRepository.h
|
||||
${CLIENT_ROOT_DIR}/core/repositories/secureAppSettingsRepository.h
|
||||
${CLIENT_ROOT_DIR}/core/protocols/qmlRegisterProtocols.h
|
||||
@@ -119,6 +120,7 @@ set(SOURCES ${SOURCES}
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/servicesCatalogController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/subscriptionController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/newsController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/updateController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/repositories/secureServersRepository.cpp
|
||||
${CLIENT_ROOT_DIR}/core/repositories/secureAppSettingsRepository.cpp
|
||||
${CLIENT_ROOT_DIR}/ui/utils/qAutoStart.cpp
|
||||
|
||||
@@ -346,13 +346,8 @@ ErrorCode SubscriptionController::importServiceFromAppStore(const QString &userC
|
||||
appendProtocolDataToApiPayload(serviceProtocol, protocolData, apiPayload);
|
||||
apiPayload[apiDefs::key::transactionId] = transactionId;
|
||||
|
||||
GatewayController gatewayController(m_appSettingsRepository->getGatewayEndpoint(),
|
||||
m_appSettingsRepository->isDevGatewayEnv(),
|
||||
apiDefs::requestTimeoutMsecs,
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled());
|
||||
|
||||
QByteArray responseBody;
|
||||
ErrorCode errorCode = gatewayController.post(QString("%1v1/subscriptions"), apiPayload, responseBody);
|
||||
ErrorCode errorCode = executeRequest(QString("%1v1/subscriptions"), apiPayload, responseBody, isTestPurchase);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
@@ -365,6 +360,9 @@ ErrorCode SubscriptionController::importServiceFromAppStore(const QString &userC
|
||||
return ErrorCode::ApiPurchaseError;
|
||||
}
|
||||
|
||||
QString normalizedKey = key;
|
||||
normalizedKey.replace(QStringLiteral("vpn://"), QString());
|
||||
|
||||
// Check if server with this VPN key already exists
|
||||
for (int i = 0; i < m_serversRepository->serversCount(); ++i) {
|
||||
ServerConfig existingServerConfig = m_serversRepository->server(i);
|
||||
@@ -376,7 +374,8 @@ ErrorCode SubscriptionController::importServiceFromAppStore(const QString &userC
|
||||
const ApiV2ServerConfig* apiV2 = existingServerConfig.as<ApiV2ServerConfig>();
|
||||
existingVpnKey = apiV2 ? apiV2->vpnKey() : QString();
|
||||
}
|
||||
if (existingVpnKey == key) {
|
||||
existingVpnKey.replace(QStringLiteral("vpn://"), QString());
|
||||
if (!existingVpnKey.isEmpty() && existingVpnKey == normalizedKey) {
|
||||
if (duplicateServerIndex) {
|
||||
*duplicateServerIndex = i;
|
||||
}
|
||||
@@ -385,9 +384,6 @@ ErrorCode SubscriptionController::importServiceFromAppStore(const QString &userC
|
||||
}
|
||||
}
|
||||
|
||||
QString normalizedKey = key;
|
||||
normalizedKey.replace(QStringLiteral("vpn://"), QString());
|
||||
|
||||
QByteArray configString = QByteArray::fromBase64(normalizedKey.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
|
||||
QByteArray configUncompressed = qUncompress(configString);
|
||||
if (!configUncompressed.isEmpty()) {
|
||||
@@ -437,6 +433,7 @@ ErrorCode SubscriptionController::updateServiceFromGateway(int serverIndex, cons
|
||||
if (!apiV2) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
const bool isTestPurchase = apiV2->apiConfig.isTestPurchase;
|
||||
QString serviceProtocol = apiV2->serviceProtocol();
|
||||
ProtocolData protocolData = generateProtocolData(serviceProtocol);
|
||||
|
||||
@@ -459,7 +456,7 @@ ErrorCode SubscriptionController::updateServiceFromGateway(int serverIndex, cons
|
||||
}
|
||||
|
||||
QByteArray responseBody;
|
||||
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody);
|
||||
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody, isTestPurchase);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
if (errorCode == ErrorCode::ApiSubscriptionExpiredError && !apiV2->apiConfig.isInAppPurchase) {
|
||||
ServerConfig expiredServerConfig = serverConfigModel;
|
||||
@@ -508,7 +505,7 @@ ErrorCode SubscriptionController::updateServiceFromGateway(int serverIndex, cons
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::deactivateDevice(int serverIndex, bool isRemoveEvent)
|
||||
ErrorCode SubscriptionController::deactivateDevice(int serverIndex)
|
||||
{
|
||||
ServerConfig serverConfigModel = m_serversRepository->server(serverIndex);
|
||||
|
||||
@@ -538,8 +535,9 @@ ErrorCode SubscriptionController::deactivateDevice(int serverIndex, bool isRemov
|
||||
|
||||
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
|
||||
|
||||
const bool isTestPurchase = apiV2->apiConfig.isTestPurchase;
|
||||
QByteArray responseBody;
|
||||
ErrorCode errorCode = executeRequest(QString("%1v1/revoke_config"), apiPayload, responseBody);
|
||||
ErrorCode errorCode = executeRequest(QString("%1v1/revoke_config"), apiPayload, responseBody, isTestPurchase);
|
||||
if (errorCode != ErrorCode::NoError && errorCode != ErrorCode::ApiNotFoundError) {
|
||||
return errorCode;
|
||||
}
|
||||
@@ -581,8 +579,9 @@ ErrorCode SubscriptionController::deactivateExternalDevice(int serverIndex, cons
|
||||
|
||||
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
|
||||
|
||||
const bool isTestPurchase = apiV2->apiConfig.isTestPurchase;
|
||||
QByteArray responseBody;
|
||||
ErrorCode errorCode = executeRequest(QString("%1v1/revoke_config"), apiPayload, responseBody);
|
||||
ErrorCode errorCode = executeRequest(QString("%1v1/revoke_config"), apiPayload, responseBody, isTestPurchase);
|
||||
if (errorCode != ErrorCode::NoError && errorCode != ErrorCode::ApiNotFoundError) {
|
||||
return errorCode;
|
||||
}
|
||||
@@ -609,6 +608,7 @@ ErrorCode SubscriptionController::exportNativeConfig(int serverIndex, const QStr
|
||||
if (!apiV2) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
const bool isTestPurchase = apiV2->apiConfig.isTestPurchase;
|
||||
QString protocol = configKey::awg;
|
||||
ProtocolData protocolData = generateProtocolData(protocol);
|
||||
|
||||
@@ -627,7 +627,7 @@ ErrorCode SubscriptionController::exportNativeConfig(int serverIndex, const QStr
|
||||
appendProtocolDataToApiPayload(protocol, protocolData, apiPayload);
|
||||
|
||||
QByteArray responseBody;
|
||||
ErrorCode errorCode = executeRequest(QString("%1v1/native_config"), apiPayload, responseBody);
|
||||
ErrorCode errorCode = executeRequest(QString("%1v1/native_config"), apiPayload, responseBody, isTestPurchase);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
@@ -650,6 +650,7 @@ ErrorCode SubscriptionController::revokeNativeConfig(int serverIndex, const QStr
|
||||
if (!apiV2) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
const bool isTestPurchase = apiV2->apiConfig.isTestPurchase;
|
||||
QString protocol = configKey::awg;
|
||||
|
||||
QJsonObject authDataJson = apiV2->authData.toJson();
|
||||
@@ -666,7 +667,7 @@ ErrorCode SubscriptionController::revokeNativeConfig(int serverIndex, const QStr
|
||||
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
|
||||
|
||||
QByteArray responseBody;
|
||||
ErrorCode errorCode = executeRequest(QString("%1v1/revoke_native_config"), apiPayload, responseBody);
|
||||
ErrorCode errorCode = executeRequest(QString("%1v1/revoke_native_config"), apiPayload, responseBody, isTestPurchase);
|
||||
if (errorCode != ErrorCode::NoError && errorCode != ErrorCode::ApiNotFoundError) {
|
||||
return errorCode;
|
||||
}
|
||||
@@ -760,7 +761,7 @@ ErrorCode SubscriptionController::prepareVpnKeyExport(int serverIndex, QString &
|
||||
ErrorCode SubscriptionController::validateAndUpdateConfig(int serverIndex, bool hasInstalledContainers)
|
||||
{
|
||||
ServerConfig serverConfigModel = m_serversRepository->server(serverIndex);
|
||||
|
||||
|
||||
apiDefs::ConfigSource configSource;
|
||||
if (serverConfigModel.isApiV1()) {
|
||||
configSource = apiDefs::ConfigSource::Telegram;
|
||||
@@ -774,11 +775,11 @@ ErrorCode SubscriptionController::validateAndUpdateConfig(int serverIndex, bool
|
||||
removeApiConfig(serverIndex);
|
||||
return updateServiceFromTelegram(serverIndex);
|
||||
} else if (configSource == apiDefs::ConfigSource::AmneziaGateway && !hasInstalledContainers) {
|
||||
return updateServiceFromGateway(serverIndex, "", false);
|
||||
return updateServiceFromGateway(serverIndex, "", true);
|
||||
} else if (configSource && isApiKeyExpired(serverIndex)) {
|
||||
qDebug() << "attempt to update api config by expires_at event";
|
||||
if (configSource == apiDefs::ConfigSource::AmneziaGateway) {
|
||||
return updateServiceFromGateway(serverIndex, "", false);
|
||||
return updateServiceFromGateway(serverIndex, "", true);
|
||||
} else {
|
||||
removeApiConfig(serverIndex);
|
||||
return updateServiceFromTelegram(serverIndex);
|
||||
@@ -1076,6 +1077,7 @@ QFuture<QPair<ErrorCode, QString>> SubscriptionController::getRenewalLink(int se
|
||||
|
||||
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
|
||||
apiPayload[apiDefs::key::cliVersion] = QString(APP_VERSION);
|
||||
apiPayload[apiDefs::key::subscriptionStatus] = getSubscriptionStatusForRenewal(apiV2->apiConfig);
|
||||
|
||||
auto gatewayController = QSharedPointer<GatewayController>::create(m_appSettingsRepository->getGatewayEndpoint(isTestPurchase),
|
||||
m_appSettingsRepository->isDevGatewayEnv(isTestPurchase),
|
||||
@@ -1095,11 +1097,7 @@ QFuture<QPair<ErrorCode, QString>> SubscriptionController::getRenewalLink(int se
|
||||
|
||||
QJsonObject responseJson = QJsonDocument::fromJson(responseBody).object();
|
||||
const QString url = responseJson.value("renewal_url").toString();
|
||||
if (url.isEmpty()) {
|
||||
promise->addResult(qMakePair(ErrorCode::InternalError, QString()));
|
||||
} else {
|
||||
promise->addResult(qMakePair(ErrorCode::NoError, url));
|
||||
}
|
||||
promise->addResult(qMakePair(ErrorCode::NoError, url));
|
||||
promise->finish();
|
||||
});
|
||||
watcher->setFuture(postFuture);
|
||||
|
||||
@@ -73,7 +73,7 @@ public:
|
||||
|
||||
ErrorCode updateServiceFromGateway(int serverIndex, const QString &newCountryCode, bool isConnectEvent);
|
||||
|
||||
ErrorCode deactivateDevice(int serverIndex, bool isRemoveEvent);
|
||||
ErrorCode deactivateDevice(int serverIndex);
|
||||
|
||||
ErrorCode deactivateExternalDevice(int serverIndex, const QString &uuid, const QString &serverCountryCode);
|
||||
|
||||
|
||||
@@ -146,6 +146,7 @@ void CoreController::initCoreControllers()
|
||||
m_servicesCatalogController = new ServicesCatalogController(m_appSettingsRepository);
|
||||
m_subscriptionController = new SubscriptionController(m_serversRepository, m_appSettingsRepository);
|
||||
m_newsController = new NewsController(m_appSettingsRepository, m_serversController);
|
||||
m_updateController = new UpdateController(m_appSettingsRepository, this);
|
||||
|
||||
m_installController = new InstallController(m_serversRepository, m_appSettingsRepository, this);
|
||||
m_exportController = new ExportController(m_serversRepository, m_appSettingsRepository, this);
|
||||
@@ -212,6 +213,9 @@ void CoreController::initControllers()
|
||||
|
||||
m_apiNewsUiController = new ApiNewsUiController(m_newsModel, m_newsController, this);
|
||||
setQmlContextProperty("ApiNewsController", m_apiNewsUiController);
|
||||
|
||||
m_updateUiController = new UpdateUiController(m_updateController, this);
|
||||
setQmlContextProperty("UpdateController", m_updateUiController);
|
||||
}
|
||||
|
||||
void CoreController::initAndroidController()
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "ui/controllers/ipSplitTunnelingUiController.h"
|
||||
#include "ui/controllers/systemController.h"
|
||||
#include "ui/controllers/languageUiController.h"
|
||||
#include "ui/controllers/updateUiController.h"
|
||||
#include "ui/controllers/api/servicesCatalogUiController.h"
|
||||
|
||||
#include "core/controllers/serversController.h"
|
||||
@@ -39,6 +40,7 @@
|
||||
#include "core/controllers/selfhosted/installController.h"
|
||||
#include "core/controllers/settingsController.h"
|
||||
#include "core/controllers/connectionController.h"
|
||||
#include "core/controllers/updateController.h"
|
||||
|
||||
#include "core/repositories/secureServersRepository.h"
|
||||
#include "core/repositories/secureAppSettingsRepository.h"
|
||||
@@ -159,6 +161,7 @@ private:
|
||||
AppSplitTunnelingUiController* m_appSplitTunnelingUiController;
|
||||
AllowedDnsUiController* m_allowedDnsUiController;
|
||||
LanguageUiController* m_languageUiController;
|
||||
UpdateUiController* m_updateUiController;
|
||||
|
||||
SubscriptionUiController* m_subscriptionUiController;
|
||||
ApiNewsUiController* m_apiNewsUiController;
|
||||
@@ -173,6 +176,7 @@ private:
|
||||
ServicesCatalogController* m_servicesCatalogController;
|
||||
SubscriptionController* m_subscriptionController;
|
||||
NewsController* m_newsController;
|
||||
UpdateController* m_updateController;
|
||||
InstallController* m_installController;
|
||||
ExportController* m_exportController;
|
||||
ConnectionController* m_connectionController;
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "ui/controllers/selfhosted/installUiController.h"
|
||||
#include "ui/controllers/importUiController.h"
|
||||
#include "ui/controllers/api/subscriptionUiController.h"
|
||||
#include "ui/controllers/updateUiController.h"
|
||||
#include "ui/models/serversModel.h"
|
||||
#include "core/controllers/serversController.h"
|
||||
#include "core/controllers/ipSplitTunnelingController.h"
|
||||
@@ -83,6 +84,7 @@ void CoreSignalHandlers::initAllHandlers()
|
||||
initIosImportHandler();
|
||||
initIosSettingsHandler();
|
||||
initNotificationHandler();
|
||||
initUpdateFoundHandler();
|
||||
}
|
||||
|
||||
void CoreSignalHandlers::initErrorMessagesHandler()
|
||||
@@ -410,3 +412,19 @@ void CoreSignalHandlers::initNotificationHandler()
|
||||
#endif
|
||||
}
|
||||
|
||||
void CoreSignalHandlers::initUpdateFoundHandler()
|
||||
{
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
connect(m_coreController->m_apiNewsUiController, &ApiNewsUiController::fetchNewsFinished, m_coreController->m_updateUiController,
|
||||
&UpdateUiController::checkForUpdates);
|
||||
|
||||
connect(m_coreController->m_updateUiController, &UpdateUiController::updateFound, this, [this]() {
|
||||
const QString version = m_coreController->m_updateUiController->getVersion();
|
||||
const QString updateId = version.isEmpty() ? QStringLiteral("update") : QStringLiteral("update-%1").arg(version);
|
||||
m_coreController->m_newsModel->setUpdateNotification(
|
||||
updateId, m_coreController->m_updateUiController->getHeaderText(), m_coreController->m_updateUiController->getChangelogText());
|
||||
emit m_coreController->m_pageController->showChangelogDrawer();
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ private:
|
||||
void initIosImportHandler();
|
||||
void initIosSettingsHandler();
|
||||
void initNotificationHandler();
|
||||
void initUpdateFoundHandler();
|
||||
|
||||
CoreController* m_coreController;
|
||||
};
|
||||
|
||||
391
client/core/controllers/updateController.cpp
Normal file
391
client/core/controllers/updateController.cpp
Normal file
@@ -0,0 +1,391 @@
|
||||
#include "updateController.h"
|
||||
|
||||
#include <QNetworkReply>
|
||||
#include <QVersionNumber>
|
||||
#include <QUrl>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QSysInfo>
|
||||
#include <QTimer>
|
||||
|
||||
#include "amneziaApplication.h"
|
||||
#include "logger.h"
|
||||
#include "version.h"
|
||||
#include "core/controllers/gatewayController.h"
|
||||
#include "core/utils/constants/apiKeys.h"
|
||||
#include "core/utils/errorStrings.h"
|
||||
#include "core/utils/selfhosted/scriptsRegistry.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
Logger logger("UpdateController");
|
||||
|
||||
#if defined(Q_OS_WINDOWS)
|
||||
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN_%1_x64.exe");
|
||||
const QString kInstallerLocalPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN_installer.exe";
|
||||
#elif defined(Q_OS_MACOS)
|
||||
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN_%1_macos.pkg");
|
||||
const QString kInstallerLocalPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN.pkg";
|
||||
#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
|
||||
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN_%1_linux_x64.tar");
|
||||
const QString kInstallerLocalPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN.tar";
|
||||
#endif
|
||||
}
|
||||
|
||||
UpdateController::UpdateController(SecureAppSettingsRepository* appSettingsRepository, QObject *parent)
|
||||
: QObject(parent), m_appSettingsRepository(appSettingsRepository)
|
||||
{
|
||||
}
|
||||
|
||||
QString UpdateController::getRawChangelogText() const
|
||||
{
|
||||
return m_changelogText;
|
||||
}
|
||||
|
||||
QString UpdateController::getReleaseDate() const
|
||||
{
|
||||
return m_releaseDate;
|
||||
}
|
||||
|
||||
QString UpdateController::getVersion() const
|
||||
{
|
||||
return m_version;
|
||||
}
|
||||
|
||||
void UpdateController::checkForUpdates()
|
||||
{
|
||||
if (m_updateCheckRunning || !m_appSettingsRepository) {
|
||||
return;
|
||||
}
|
||||
m_updateCheckRunning = true;
|
||||
|
||||
fetchGatewayUrl();
|
||||
}
|
||||
|
||||
void UpdateController::finishUpdateCheck()
|
||||
{
|
||||
m_updateCheckRunning = false;
|
||||
}
|
||||
|
||||
void UpdateController::doGetAsync(const QString &endpoint, std::function<void(bool, QByteArray)> onDone)
|
||||
{
|
||||
QString fullUrl = m_baseUrl + endpoint;
|
||||
|
||||
QNetworkRequest req;
|
||||
req.setTransferTimeout(7000);
|
||||
req.setUrl(QUrl(fullUrl));
|
||||
|
||||
QNetworkReply *reply = amnApp->networkManager()->get(req);
|
||||
setupNetworkErrorHandling(reply, endpoint);
|
||||
|
||||
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, endpoint, onDone]() {
|
||||
const bool ok = (reply->error() == QNetworkReply::NoError);
|
||||
QByteArray data;
|
||||
if (ok) {
|
||||
data = reply->readAll();
|
||||
} else {
|
||||
handleNetworkError(reply, endpoint);
|
||||
}
|
||||
reply->deleteLater();
|
||||
onDone(ok, data);
|
||||
});
|
||||
}
|
||||
|
||||
void UpdateController::fetchGatewayUrl()
|
||||
{
|
||||
auto gatewayController = QSharedPointer<GatewayController>::create(m_appSettingsRepository->getGatewayEndpoint(),
|
||||
m_appSettingsRepository->isDevGatewayEnv(),
|
||||
7000,
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled());
|
||||
|
||||
QJsonObject apiPayload;
|
||||
apiPayload[apiDefs::key::cliVersion] = QString(APP_VERSION);
|
||||
apiPayload[apiDefs::key::osVersion] = QSysInfo::productType();
|
||||
apiPayload[apiDefs::key::installationUuid] = m_appSettingsRepository->getInstallationUuid(true);
|
||||
|
||||
// Workaround: wait before contacting gateway to avoid rate limit triggered by other requests (news etc.)
|
||||
QTimer::singleShot(1000, this, [this, gatewayController, apiPayload]() {
|
||||
gatewayController->postAsync(QStringLiteral("%1v1/updater_endpoint"), apiPayload)
|
||||
.then(this, [this](QPair<ErrorCode, QByteArray> result) {
|
||||
auto [err, gatewayResponse] = result;
|
||||
if (err != ErrorCode::NoError) {
|
||||
logger.error() << errorString(err);
|
||||
finishUpdateCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject gatewayData = QJsonDocument::fromJson(gatewayResponse).object();
|
||||
|
||||
QString baseUrl = gatewayData.value("url").toString();
|
||||
if (baseUrl.endsWith('/')) {
|
||||
baseUrl.chop(1);
|
||||
}
|
||||
m_baseUrl = baseUrl;
|
||||
|
||||
fetchVersionInfo();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void UpdateController::fetchVersionInfo()
|
||||
{
|
||||
doGetAsync("/VERSION", [this](bool ok, QByteArray data) {
|
||||
if (!ok) {
|
||||
finishUpdateCheck();
|
||||
return;
|
||||
}
|
||||
m_version = QString::fromUtf8(data).trimmed();
|
||||
|
||||
if (!isNewVersionAvailable()) {
|
||||
finishUpdateCheck();
|
||||
return;
|
||||
}
|
||||
fetchChangelog();
|
||||
});
|
||||
}
|
||||
|
||||
void UpdateController::fetchChangelog()
|
||||
{
|
||||
doGetAsync("/CHANGELOG", [this](bool ok, QByteArray data) {
|
||||
if (!ok) {
|
||||
m_changelogText.clear();
|
||||
} else {
|
||||
m_changelogText = QString::fromUtf8(data);
|
||||
}
|
||||
fetchReleaseDate();
|
||||
});
|
||||
}
|
||||
|
||||
void UpdateController::fetchReleaseDate()
|
||||
{
|
||||
doGetAsync("/RELEASE_DATE", [this](bool ok, QByteArray data) {
|
||||
if (ok) {
|
||||
m_releaseDate = QString::fromUtf8(data).trimmed();
|
||||
} else {
|
||||
m_releaseDate = QString();
|
||||
}
|
||||
|
||||
m_downloadUrl = composeDownloadUrl();
|
||||
emit updateFound();
|
||||
finishUpdateCheck();
|
||||
});
|
||||
}
|
||||
|
||||
bool UpdateController::isNewVersionAvailable() const
|
||||
{
|
||||
auto currentVersion = QVersionNumber::fromString(QString(APP_VERSION));
|
||||
auto newVersion = QVersionNumber::fromString(m_version);
|
||||
return newVersion > currentVersion;
|
||||
}
|
||||
|
||||
void UpdateController::setupNetworkErrorHandling(QNetworkReply* reply, const QString& operation)
|
||||
{
|
||||
QObject::connect(reply, &QNetworkReply::errorOccurred, [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, [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() const
|
||||
{
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
const QString fileName = QString(kInstallerRemoteFileNamePattern).arg(m_version);
|
||||
return m_baseUrl + "/" + fileName;
|
||||
#else
|
||||
return QString();
|
||||
#endif
|
||||
}
|
||||
|
||||
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(30000);
|
||||
request.setUrl(m_downloadUrl);
|
||||
|
||||
QNetworkReply *reply = amnApp->networkManager()->get(request);
|
||||
|
||||
QObject::connect(reply, &QNetworkReply::finished, [this, reply]() {
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
QFile file(kInstallerLocalPath);
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
logger.error() << "Failed to open installer file for writing:" << kInstallerLocalPath << "Error:" << file.errorString();
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.write(reply->readAll()) == -1) {
|
||||
logger.error() << "Failed to write installer data to file:" << kInstallerLocalPath << "Error:" << file.errorString();
|
||||
file.close();
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
file.close();
|
||||
|
||||
#if defined(Q_OS_WINDOWS)
|
||||
runWindowsInstaller(kInstallerLocalPath);
|
||||
#elif defined(Q_OS_MACOS)
|
||||
runMacInstaller(kInstallerLocalPath);
|
||||
#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
|
||||
runLinuxInstaller(kInstallerLocalPath);
|
||||
#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
|
||||
|
||||
|
||||
57
client/core/controllers/updateController.h
Normal file
57
client/core/controllers/updateController.h
Normal file
@@ -0,0 +1,57 @@
|
||||
#ifndef UPDATECONTROLLER_H
|
||||
#define UPDATECONTROLLER_H
|
||||
|
||||
#include <functional>
|
||||
#include <QObject>
|
||||
#include <QNetworkReply>
|
||||
|
||||
#include "core/repositories/secureAppSettingsRepository.h"
|
||||
|
||||
class UpdateController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit UpdateController(SecureAppSettingsRepository* appSettingsRepository, QObject *parent = nullptr);
|
||||
|
||||
QString getRawChangelogText() const;
|
||||
QString getReleaseDate() const;
|
||||
QString getVersion() const;
|
||||
|
||||
public slots:
|
||||
void checkForUpdates();
|
||||
void runInstaller();
|
||||
|
||||
signals:
|
||||
void updateFound();
|
||||
|
||||
private:
|
||||
void finishUpdateCheck();
|
||||
void fetchGatewayUrl();
|
||||
void fetchVersionInfo();
|
||||
void fetchChangelog();
|
||||
void fetchReleaseDate();
|
||||
void doGetAsync(const QString &endpoint, std::function<void(bool, QByteArray)> onDone);
|
||||
bool isNewVersionAvailable() const;
|
||||
void setupNetworkErrorHandling(QNetworkReply* reply, const QString& operation);
|
||||
void handleNetworkError(QNetworkReply* reply, const QString& operation);
|
||||
QString composeDownloadUrl() const;
|
||||
|
||||
SecureAppSettingsRepository* m_appSettingsRepository;
|
||||
|
||||
QString m_baseUrl;
|
||||
QString m_changelogText;
|
||||
QString m_version;
|
||||
QString m_releaseDate;
|
||||
QString m_downloadUrl;
|
||||
bool m_updateCheckRunning = false;
|
||||
|
||||
#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
|
||||
@@ -58,7 +58,7 @@ struct ApiConfig
|
||||
|
||||
QString stackType;
|
||||
QString cliVersion;
|
||||
bool isTestPurchase;
|
||||
bool isTestPurchase = false;
|
||||
bool isInAppPurchase = false;
|
||||
bool subscriptionExpiredByServer = false;
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
#include "xrayProtocol.h"
|
||||
|
||||
#include "core/protocols/protocolUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/ipcClient.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
#include "core/protocols/protocolUtils.h"
|
||||
#include "core/utils/serialization/serialization.h"
|
||||
#include "ipc.h"
|
||||
|
||||
#include <QCryptographicHash>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkInterface>
|
||||
#include <QJsonDocument>
|
||||
#include <QtCore/qlogging.h>
|
||||
#include <QtCore/qobjectdefs.h>
|
||||
#include <QtCore/qprocess.h>
|
||||
@@ -43,7 +43,17 @@ XrayProtocol::XrayProtocol(const QJsonObject &configuration, QObject *parent) :
|
||||
if (xrayConfiguration.isEmpty()) {
|
||||
xrayConfiguration = configuration.value(ProtocolUtils::key_proto_config_data(Proto::SSXray)).toObject();
|
||||
}
|
||||
m_xrayConfig = xrayConfiguration;
|
||||
|
||||
if (xrayConfiguration.isEmpty()) {
|
||||
qWarning() << "Xray config wrapper is empty";
|
||||
m_xrayConfig = {};
|
||||
}
|
||||
|
||||
m_xrayConfig = QJsonDocument::fromJson(xrayConfiguration.value(amnezia::configKey::config).toString().toUtf8()).object();
|
||||
if (m_xrayConfig.isEmpty()) {
|
||||
qWarning() << "Xray config string is not a valid JSON object";
|
||||
m_xrayConfig = {};
|
||||
}
|
||||
}
|
||||
|
||||
XrayProtocol::~XrayProtocol()
|
||||
@@ -65,9 +75,9 @@ ErrorCode XrayProtocol::start()
|
||||
qCritical() << "EnsureInboundAuth failed:" << e.what();
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
m_socksUser = creds.username;
|
||||
m_socksUser = creds.username;
|
||||
m_socksPassword = creds.password;
|
||||
m_socksPort = creds.port;
|
||||
m_socksPort = creds.port;
|
||||
|
||||
const QString xrayConfigStr = QJsonDocument(m_xrayConfig).toJson(QJsonDocument::Compact);
|
||||
if (xrayConfigStr.isEmpty()) {
|
||||
@@ -75,16 +85,16 @@ ErrorCode XrayProtocol::start()
|
||||
return ErrorCode::XrayExecutableCrashed;
|
||||
}
|
||||
|
||||
return IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
auto xrayStart = iface->xrayStart(xrayConfigStr);
|
||||
if (!xrayStart.waitForFinished() || !xrayStart.returnValue()) {
|
||||
qCritical() << "Failed to start xray";
|
||||
return ErrorCode::XrayExecutableCrashed;
|
||||
}
|
||||
return startTun2Socks();
|
||||
}, [] () {
|
||||
return ErrorCode::AmneziaServiceConnectionFailed;
|
||||
});
|
||||
return IpcClient::withInterface(
|
||||
[&](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
auto xrayStart = iface->xrayStart(xrayConfigStr);
|
||||
if (!xrayStart.waitForFinished() || !xrayStart.returnValue()) {
|
||||
qCritical() << "Failed to start xray";
|
||||
return ErrorCode::XrayExecutableCrashed;
|
||||
}
|
||||
return startTun2Socks();
|
||||
},
|
||||
[]() { return ErrorCode::AmneziaServiceConnectionFailed; });
|
||||
}
|
||||
|
||||
void XrayProtocol::stop()
|
||||
@@ -143,129 +153,135 @@ ErrorCode XrayProtocol::startTun2Socks()
|
||||
return ErrorCode::AmneziaServiceConnectionFailed;
|
||||
}
|
||||
|
||||
const QString proxyUrl = QString("socks5://%1:%2@127.0.0.1:%3")
|
||||
.arg(m_socksUser, m_socksPassword, QString::number(m_socksPort));
|
||||
const QString proxyUrl = QString("socks5://%1:%2@127.0.0.1:%3").arg(m_socksUser, m_socksPassword, QString::number(m_socksPort));
|
||||
|
||||
m_tun2socksProcess->setProgram(PermittedProcess::Tun2Socks);
|
||||
m_tun2socksProcess->setArguments({"-device", QString("tun://%1").arg(tunName), "-proxy", proxyUrl});
|
||||
m_tun2socksProcess->setArguments({ "-device", QString("tun://%1").arg(tunName), "-proxy", proxyUrl });
|
||||
|
||||
connect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardOutput, this, [this]() {
|
||||
auto readAllStandardOutput = m_tun2socksProcess->readAllStandardOutput();
|
||||
if (!readAllStandardOutput.waitForFinished()) {
|
||||
qWarning() << "Failed to read output from tun2socks";
|
||||
return;
|
||||
}
|
||||
connect(
|
||||
m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardOutput, this,
|
||||
[this]() {
|
||||
auto readAllStandardOutput = m_tun2socksProcess->readAllStandardOutput();
|
||||
if (!readAllStandardOutput.waitForFinished()) {
|
||||
qWarning() << "Failed to read output from tun2socks";
|
||||
return;
|
||||
}
|
||||
|
||||
const QString line = readAllStandardOutput.returnValue();
|
||||
const QString line = readAllStandardOutput.returnValue();
|
||||
|
||||
if (!line.contains("[TCP]") && !line.contains("[UDP]"))
|
||||
qDebug() << "[tun2socks]:" << line;
|
||||
|
||||
if (line.contains("[STACK] tun://") && line.contains("<-> socks5://")) {
|
||||
disconnect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardOutput, this, nullptr);
|
||||
if (!line.contains("[TCP]") && !line.contains("[UDP]"))
|
||||
qDebug() << "[tun2socks]:" << line;
|
||||
|
||||
if (ErrorCode res = setupRouting(); res != ErrorCode::NoError) {
|
||||
if (line.contains("[STACK] tun://") && line.contains("<-> socks5://")) {
|
||||
disconnect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardOutput, this, nullptr);
|
||||
|
||||
if (ErrorCode res = setupRouting(); res != ErrorCode::NoError) {
|
||||
stop();
|
||||
setLastError(res);
|
||||
} else {
|
||||
setConnectionState(Vpn::ConnectionState::Connected);
|
||||
}
|
||||
}
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
|
||||
connect(
|
||||
m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::finished, this,
|
||||
[this](int exitCode, QProcess::ExitStatus exitStatus) {
|
||||
if (exitStatus == QProcess::ExitStatus::CrashExit) {
|
||||
qCritical() << "Tun2socks process crashed!";
|
||||
} else {
|
||||
qCritical() << QString("Tun2socks process was closed with %1 exit code").arg(exitCode);
|
||||
}
|
||||
stop();
|
||||
setLastError(res);
|
||||
} else {
|
||||
setConnectionState(Vpn::ConnectionState::Connected);
|
||||
}
|
||||
}
|
||||
}, Qt::QueuedConnection);
|
||||
|
||||
connect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::finished, this, [this](int exitCode, QProcess::ExitStatus exitStatus) {
|
||||
if (exitStatus == QProcess::ExitStatus::CrashExit) {
|
||||
qCritical() << "Tun2socks process crashed!";
|
||||
} else {
|
||||
qCritical() << QString("Tun2socks process was closed with %1 exit code").arg(exitCode);
|
||||
}
|
||||
stop();
|
||||
setLastError(ErrorCode::Tun2SockExecutableCrashed);
|
||||
}, Qt::QueuedConnection);
|
||||
setLastError(ErrorCode::Tun2SockExecutableCrashed);
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
|
||||
m_tun2socksProcess->start();
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode XrayProtocol::setupRouting() {
|
||||
return IpcClient::withInterface([this](QSharedPointer<IpcInterfaceReplica> iface) -> ErrorCode {
|
||||
ErrorCode XrayProtocol::setupRouting()
|
||||
{
|
||||
return IpcClient::withInterface(
|
||||
[this](QSharedPointer<IpcInterfaceReplica> iface) -> ErrorCode {
|
||||
#ifdef Q_OS_WIN
|
||||
const int inetAdapterIndex = NetworkUtilities::AdapterIndexTo(QHostAddress(m_remoteAddress));
|
||||
const int inetAdapterIndex = NetworkUtilities::AdapterIndexTo(QHostAddress(m_remoteAddress));
|
||||
#endif
|
||||
auto createTun = iface->createTun(tunName, amnezia::protocols::xray::defaultLocalAddr);
|
||||
if (!createTun.waitForFinished() || !createTun.returnValue()) {
|
||||
qCritical() << "Failed to assign IP address for TUN";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
auto updateResolvers = iface->updateResolvers(tunName, m_dnsServers);
|
||||
if (!updateResolvers.waitForFinished() || !updateResolvers.returnValue()) {
|
||||
qCritical() << "Failed to set DNS resolvers for TUN";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
int vpnAdapterIndex = -1;
|
||||
QList<QNetworkInterface> netInterfaces = QNetworkInterface::allInterfaces();
|
||||
for (auto& netInterface : netInterfaces) {
|
||||
for (auto& address : netInterface.addressEntries()) {
|
||||
if (m_vpnLocalAddress == address.ip().toString())
|
||||
vpnAdapterIndex = netInterface.index();
|
||||
}
|
||||
}
|
||||
#else
|
||||
static const int vpnAdapterIndex = 0;
|
||||
#endif
|
||||
const bool killSwitchEnabled = QVariant(m_rawConfig.value(configKey::killSwitchOption).toString()).toBool();
|
||||
if (killSwitchEnabled) {
|
||||
if (vpnAdapterIndex != -1) {
|
||||
QJsonObject config = m_rawConfig;
|
||||
config.insert("vpnServer", m_remoteAddress);
|
||||
|
||||
auto enableKillSwitch = IpcClient::Interface()->enableKillSwitch(config, vpnAdapterIndex);
|
||||
if (!enableKillSwitch.waitForFinished() || !enableKillSwitch.returnValue()) {
|
||||
qCritical() << "Failed to enable killswitch";
|
||||
auto createTun = iface->createTun(tunName, amnezia::protocols::xray::defaultLocalAddr);
|
||||
if (!createTun.waitForFinished() || !createTun.returnValue()) {
|
||||
qCritical() << "Failed to assign IP address for TUN";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
} else
|
||||
qWarning() << "Failed to get vpnAdapterIndex. Killswitch disabled";
|
||||
}
|
||||
|
||||
if (m_routeMode == amnezia::RouteMode::VpnAllSites) {
|
||||
static const QStringList subnets = { "1.0.0.0/8", "2.0.0.0/7", "4.0.0.0/6", "8.0.0.0/5", "16.0.0.0/4", "32.0.0.0/3", "64.0.0.0/2", "128.0.0.0/1" };
|
||||
|
||||
auto routeAddList = iface->routeAddList(m_vpnGateway, subnets);
|
||||
if (!routeAddList.waitForFinished() || routeAddList.returnValue() != subnets.count()) {
|
||||
qCritical() << "Failed to set routes for TUN";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
}
|
||||
|
||||
auto StopRoutingIpv6 = iface->StopRoutingIpv6();
|
||||
if (!StopRoutingIpv6.waitForFinished() || !StopRoutingIpv6.returnValue()) {
|
||||
qCritical() << "Failed to disable IPv6 routing";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
auto updateResolvers = iface->updateResolvers(tunName, m_dnsServers);
|
||||
if (!updateResolvers.waitForFinished() || !updateResolvers.returnValue()) {
|
||||
qCritical() << "Failed to set DNS resolvers for TUN";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
if (inetAdapterIndex != -1 && vpnAdapterIndex != -1) {
|
||||
QJsonObject config = m_rawConfig;
|
||||
config.insert("inetAdapterIndex", inetAdapterIndex);
|
||||
config.insert("vpnAdapterIndex", vpnAdapterIndex);
|
||||
config.insert("vpnGateway", m_vpnGateway);
|
||||
config.insert("vpnServer", m_remoteAddress);
|
||||
|
||||
auto enablePeerTraffic = iface->enablePeerTraffic(config);
|
||||
if (!enablePeerTraffic.waitForFinished() || !enablePeerTraffic.returnValue()) {
|
||||
qCritical() << "Failed to enable peer traffic";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
} else
|
||||
qWarning() << "Failed to get adapter indexes. Split-tunneling disabled";
|
||||
int vpnAdapterIndex = -1;
|
||||
QList<QNetworkInterface> netInterfaces = QNetworkInterface::allInterfaces();
|
||||
for (auto &netInterface : netInterfaces) {
|
||||
for (auto &address : netInterface.addressEntries()) {
|
||||
if (m_vpnLocalAddress == address.ip().toString())
|
||||
vpnAdapterIndex = netInterface.index();
|
||||
}
|
||||
}
|
||||
#else
|
||||
static const int vpnAdapterIndex = 0;
|
||||
#endif
|
||||
return ErrorCode::NoError;
|
||||
},
|
||||
[] () {
|
||||
return ErrorCode::AmneziaServiceConnectionFailed;
|
||||
});
|
||||
const bool killSwitchEnabled = QVariant(m_rawConfig.value(configKey::killSwitchOption).toString()).toBool();
|
||||
if (killSwitchEnabled) {
|
||||
if (vpnAdapterIndex != -1) {
|
||||
QJsonObject config = m_rawConfig;
|
||||
config.insert("vpnServer", m_remoteAddress);
|
||||
|
||||
auto enableKillSwitch = IpcClient::Interface()->enableKillSwitch(config, vpnAdapterIndex);
|
||||
if (!enableKillSwitch.waitForFinished() || !enableKillSwitch.returnValue()) {
|
||||
qCritical() << "Failed to enable killswitch";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
} else
|
||||
qWarning() << "Failed to get vpnAdapterIndex. Killswitch disabled";
|
||||
}
|
||||
|
||||
if (m_routeMode == amnezia::RouteMode::VpnAllSites) {
|
||||
static const QStringList subnets = { "1.0.0.0/8", "2.0.0.0/7", "4.0.0.0/6", "8.0.0.0/5",
|
||||
"16.0.0.0/4", "32.0.0.0/3", "64.0.0.0/2", "128.0.0.0/1" };
|
||||
|
||||
auto routeAddList = iface->routeAddList(m_vpnGateway, subnets);
|
||||
if (!routeAddList.waitForFinished() || routeAddList.returnValue() != subnets.count()) {
|
||||
qCritical() << "Failed to set routes for TUN";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
}
|
||||
|
||||
auto StopRoutingIpv6 = iface->StopRoutingIpv6();
|
||||
if (!StopRoutingIpv6.waitForFinished() || !StopRoutingIpv6.returnValue()) {
|
||||
qCritical() << "Failed to disable IPv6 routing";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
if (inetAdapterIndex != -1 && vpnAdapterIndex != -1) {
|
||||
QJsonObject config = m_rawConfig;
|
||||
config.insert("inetAdapterIndex", inetAdapterIndex);
|
||||
config.insert("vpnAdapterIndex", vpnAdapterIndex);
|
||||
config.insert("vpnGateway", m_vpnGateway);
|
||||
config.insert("vpnServer", m_remoteAddress);
|
||||
|
||||
auto enablePeerTraffic = iface->enablePeerTraffic(config);
|
||||
if (!enablePeerTraffic.waitForFinished() || !enablePeerTraffic.returnValue()) {
|
||||
qCritical() << "Failed to enable peer traffic";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
} else
|
||||
qWarning() << "Failed to get adapter indexes. Split-tunneling disabled";
|
||||
#endif
|
||||
return ErrorCode::NoError;
|
||||
},
|
||||
[]() { return ErrorCode::AmneziaServiceConnectionFailed; });
|
||||
}
|
||||
|
||||
@@ -73,6 +73,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 fileName = QString(":/server_scripts/%1").arg(amnezia::scriptName(type));
|
||||
@@ -101,6 +110,22 @@ QString amnezia::scriptData(amnezia::ProtocolScriptType type, DockerContainer co
|
||||
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;
|
||||
}
|
||||
|
||||
amnezia::ScriptVars amnezia::genBaseVars(const ServerCredentials &credentials,
|
||||
DockerContainer container,
|
||||
const QString &primaryDns,
|
||||
|
||||
@@ -41,14 +41,21 @@ enum ProtocolScriptType {
|
||||
xray_template
|
||||
};
|
||||
|
||||
enum ClientScriptType {
|
||||
// Client-side scripts
|
||||
linux_installer,
|
||||
mac_installer
|
||||
};
|
||||
|
||||
QString scriptFolder(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);
|
||||
|
||||
ScriptVars genBaseVars(const ServerCredentials &credentials,
|
||||
DockerContainer container,
|
||||
|
||||
@@ -22,7 +22,7 @@ void ApiNewsUiController::fetchNews(bool showError)
|
||||
return;
|
||||
}
|
||||
|
||||
m_newsModel->updateModel(newsArray);
|
||||
m_newsModel->setNewsList(newsArray);
|
||||
emit fetchNewsFinished();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -23,6 +23,9 @@ bool ServicesCatalogUiController::fillAvailableServices()
|
||||
}
|
||||
|
||||
m_apiServicesModel->updateModel(servicesData);
|
||||
if (m_apiServicesModel->rowCount() > 0) {
|
||||
m_apiServicesModel->setServiceIndex(0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -390,7 +390,8 @@ bool SubscriptionUiController::updateServiceFromGateway(const int serverIndex, c
|
||||
if (oldServerConfig.isApiV2()) {
|
||||
const ApiV2ServerConfig *oldApiV2 = oldServerConfig.as<ApiV2ServerConfig>();
|
||||
if (oldApiV2) {
|
||||
wasSubscriptionExpired = oldApiV2->apiConfig.isSubscriptionExpired();
|
||||
wasSubscriptionExpired = oldApiV2->apiConfig.subscriptionExpiredByServer
|
||||
|| oldApiV2->apiConfig.isSubscriptionExpired();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,8 +412,9 @@ bool SubscriptionUiController::updateServiceFromGateway(const int serverIndex, c
|
||||
} else {
|
||||
if (errorCode == ErrorCode::ApiSubscriptionExpiredError) {
|
||||
emit subscriptionExpiredOnServer();
|
||||
} else {
|
||||
emit errorOccurred(errorCode);
|
||||
}
|
||||
emit errorOccurred(errorCode);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -435,14 +437,10 @@ bool SubscriptionUiController::updateServiceFromTelegram(const int serverIndex)
|
||||
}
|
||||
}
|
||||
|
||||
bool SubscriptionUiController::deactivateDevice(int serverIndex, const bool isRemoveEvent)
|
||||
bool SubscriptionUiController::deactivateDevice(int serverIndex)
|
||||
{
|
||||
|
||||
ErrorCode errorCode = m_subscriptionController->deactivateDevice(serverIndex, isRemoveEvent);
|
||||
ErrorCode errorCode = m_subscriptionController->deactivateDevice(serverIndex);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
if (errorCode == ErrorCode::ApiSubscriptionExpiredError && isRemoveEvent) {
|
||||
return true;
|
||||
}
|
||||
emit errorOccurred(errorCode);
|
||||
return false;
|
||||
}
|
||||
@@ -469,7 +467,11 @@ void SubscriptionUiController::validateConfig()
|
||||
ErrorCode errorCode = m_subscriptionController->validateAndUpdateConfig(serverIndex, hasInstalledContainers);
|
||||
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
emit errorOccurred(errorCode);
|
||||
if (errorCode == ErrorCode::ApiSubscriptionExpiredError) {
|
||||
emit subscriptionExpiredOnServer();
|
||||
} else {
|
||||
emit errorOccurred(errorCode);
|
||||
}
|
||||
emit configValidated(false);
|
||||
return;
|
||||
}
|
||||
@@ -559,6 +561,9 @@ void SubscriptionUiController::getRenewalLink(int serverIndex)
|
||||
emit errorOccurred(errorCode);
|
||||
return;
|
||||
}
|
||||
if (url.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
emit renewalLinkReceived(url);
|
||||
});
|
||||
watcher->setFuture(m_subscriptionController->getRenewalLink(serverIndex));
|
||||
|
||||
@@ -48,7 +48,7 @@ public slots:
|
||||
bool updateServiceFromGateway(const int serverIndex, const QString &newCountryCode, const QString &newCountryName,
|
||||
bool reloadServiceConfig = false);
|
||||
bool updateServiceFromTelegram(const int serverIndex);
|
||||
bool deactivateDevice(int serverIndex, const bool isRemoveEvent);
|
||||
bool deactivateDevice(int serverIndex);
|
||||
bool deactivateExternalDevice(int serverIndex, const QString &uuid, const QString &serverCountryCode);
|
||||
|
||||
void validateConfig();
|
||||
|
||||
@@ -167,6 +167,7 @@ signals:
|
||||
void escapePressed();
|
||||
void closeTopDrawer();
|
||||
|
||||
void showChangelogDrawer();
|
||||
void imeHeightChanged(int height);
|
||||
void safeAreaTopMarginChanged();
|
||||
void safeAreaBottomMarginChanged();
|
||||
|
||||
84
client/ui/controllers/updateUiController.cpp
Normal file
84
client/ui/controllers/updateUiController.cpp
Normal file
@@ -0,0 +1,84 @@
|
||||
#include "updateUiController.h"
|
||||
|
||||
UpdateUiController::UpdateUiController(UpdateController* updateController, QObject *parent)
|
||||
: QObject(parent), m_updateController(updateController)
|
||||
{
|
||||
if (m_updateController) {
|
||||
connect(m_updateController, &UpdateController::updateFound, this, &UpdateUiController::updateFound);
|
||||
}
|
||||
}
|
||||
|
||||
QString UpdateUiController::getHeaderText() const
|
||||
{
|
||||
if (!m_updateController) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
const QString version = m_updateController->getVersion();
|
||||
const QString releaseDate = m_updateController->getReleaseDate();
|
||||
if (releaseDate.trimmed().isEmpty()) {
|
||||
return tr("New version released: %1").arg(version);
|
||||
}
|
||||
|
||||
return tr("New version released: %1 (%2)").arg(version, releaseDate);
|
||||
}
|
||||
|
||||
QString UpdateUiController::getChangelogText() const
|
||||
{
|
||||
if (!m_updateController) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
const QString rawChangelog = m_updateController->getRawChangelogText();
|
||||
if (rawChangelog.isEmpty()) {
|
||||
return tr("Failed to load changelog text");
|
||||
}
|
||||
|
||||
QStringList lines = rawChangelog.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");
|
||||
}
|
||||
|
||||
QString UpdateUiController::getVersion() const
|
||||
{
|
||||
return m_updateController ? m_updateController->getVersion() : QString();
|
||||
}
|
||||
|
||||
void UpdateUiController::checkForUpdates()
|
||||
{
|
||||
if (m_updateController) {
|
||||
m_updateController->checkForUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateUiController::runInstaller()
|
||||
{
|
||||
if (m_updateController) {
|
||||
m_updateController->runInstaller();
|
||||
}
|
||||
}
|
||||
33
client/ui/controllers/updateUiController.h
Normal file
33
client/ui/controllers/updateUiController.h
Normal file
@@ -0,0 +1,33 @@
|
||||
#ifndef UPDATEUICONTROLLER_H
|
||||
#define UPDATEUICONTROLLER_H
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "core/controllers/updateController.h"
|
||||
|
||||
class UpdateUiController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(QString changelogText READ getChangelogText NOTIFY updateFound)
|
||||
Q_PROPERTY(QString headerText READ getHeaderText NOTIFY updateFound)
|
||||
|
||||
public:
|
||||
explicit UpdateUiController(UpdateController* updateController, QObject *parent = nullptr);
|
||||
|
||||
QString getHeaderText() const;
|
||||
QString getChangelogText() const;
|
||||
QString getVersion() const;
|
||||
|
||||
public slots:
|
||||
void checkForUpdates();
|
||||
void runInstaller();
|
||||
|
||||
signals:
|
||||
void updateFound();
|
||||
|
||||
private:
|
||||
UpdateController* m_updateController;
|
||||
};
|
||||
|
||||
#endif // UPDATEUICONTROLLER_H
|
||||
@@ -33,8 +33,9 @@ QVariant NewsModel::data(const QModelIndex &index, int role) const
|
||||
case TitleRole: return item.title;
|
||||
case ContentRole: return item.content;
|
||||
case TimestampRole: return item.timestamp.toLocalTime().toString(Qt::ISODate);
|
||||
case IsReadRole: return item.read;
|
||||
case IsReadRole: return m_readIds.contains(item.id);
|
||||
case IsProcessedRole: return index.row() == m_processedIndex;
|
||||
case IsUpdateRole: return item.isUpdate;
|
||||
default: return QVariant();
|
||||
}
|
||||
}
|
||||
@@ -48,6 +49,7 @@ QHash<int, QByteArray> NewsModel::roleNames() const
|
||||
roles[TimestampRole] = "timestamp";
|
||||
roles[IsReadRole] = "read";
|
||||
roles[IsProcessedRole] = "isProcessed";
|
||||
roles[IsUpdateRole] = "isUpdate";
|
||||
return roles;
|
||||
}
|
||||
|
||||
@@ -55,13 +57,33 @@ void NewsModel::markAsRead(int index)
|
||||
{
|
||||
if (index < 0 || index >= m_items.size())
|
||||
return;
|
||||
if (!m_items[index].read) {
|
||||
m_items[index].read = true;
|
||||
m_readIds.insert(m_items[index].id);
|
||||
saveReadIds();
|
||||
QModelIndex idx = createIndex(index, 0);
|
||||
emit dataChanged(idx, idx, { IsReadRole });
|
||||
emit hasUnreadChanged();
|
||||
|
||||
const QString &itemId = m_items.at(index).id;
|
||||
if (itemId.isEmpty() || m_readIds.contains(itemId))
|
||||
return;
|
||||
|
||||
m_readIds.insert(itemId);
|
||||
saveReadIds();
|
||||
|
||||
QModelIndex idx = createIndex(index, 0);
|
||||
emit dataChanged(idx, idx, { IsReadRole });
|
||||
emit hasUnreadChanged();
|
||||
}
|
||||
|
||||
void NewsModel::markUpdateAsSkipped()
|
||||
{
|
||||
if (!m_updateItem.has_value())
|
||||
return;
|
||||
|
||||
const QString updateId = m_updateItem->id;
|
||||
if (updateId.isEmpty())
|
||||
return;
|
||||
|
||||
for (int i = 0; i < m_items.size(); ++i) {
|
||||
if (m_items.at(i).id == updateId) {
|
||||
markAsRead(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,37 +100,65 @@ void NewsModel::setProcessedIndex(int index)
|
||||
emit processedIndexChanged(index);
|
||||
}
|
||||
|
||||
void NewsModel::updateModel(const QJsonArray &serverItems)
|
||||
void NewsModel::setNewsList(const QJsonArray &serverItems)
|
||||
{
|
||||
QList<NewsItem> updatedItems;
|
||||
QVector<NewsItem> updatedItems;
|
||||
updatedItems.reserve(serverItems.size());
|
||||
|
||||
for (const QJsonValue &value : serverItems) {
|
||||
if (!value.isObject())
|
||||
continue;
|
||||
|
||||
QJsonObject object = value.toObject();
|
||||
|
||||
const QJsonObject object = value.toObject();
|
||||
|
||||
NewsItem item;
|
||||
item.id = object.value("id").toString();
|
||||
if (item.id.isEmpty())
|
||||
continue;
|
||||
item.title = object.value("title").toString();
|
||||
item.content = object.value("content").toString();
|
||||
item.timestamp = QDateTime::fromString(object.value("timestamp").toString(), Qt::ISODate);
|
||||
item.read = m_readIds.contains(object.value("id").toString());
|
||||
item.isUpdate = false;
|
||||
|
||||
updatedItems.append(item);
|
||||
}
|
||||
|
||||
m_apiItems = updatedItems;
|
||||
updateModel();
|
||||
}
|
||||
|
||||
void NewsModel::setUpdateNotification(const QString &id, const QString &title, const QString &content)
|
||||
{
|
||||
if (id.isEmpty())
|
||||
return;
|
||||
|
||||
NewsItem updateItem;
|
||||
updateItem.id = id;
|
||||
updateItem.title = title;
|
||||
updateItem.content = content;
|
||||
updateItem.timestamp = QDateTime::currentDateTimeUtc();
|
||||
updateItem.isUpdate = true;
|
||||
|
||||
m_updateItem = updateItem;
|
||||
updateModel();
|
||||
}
|
||||
|
||||
void NewsModel::updateModel()
|
||||
{
|
||||
beginResetModel();
|
||||
m_items = updatedItems;
|
||||
m_items = m_apiItems;
|
||||
std::sort(m_items.begin(), m_items.end(), [](const NewsItem &a, const NewsItem &b) { return a.timestamp > b.timestamp; });
|
||||
if (m_updateItem.has_value()) {
|
||||
m_items.prepend(*m_updateItem);
|
||||
}
|
||||
endResetModel();
|
||||
loadReadIds();
|
||||
emit hasUnreadChanged();
|
||||
}
|
||||
|
||||
bool NewsModel::hasUnread() const
|
||||
{
|
||||
for (const NewsItem &item : m_items) {
|
||||
if (!item.read)
|
||||
if (!m_readIds.contains(item.id))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <QSet>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
#include <optional>
|
||||
|
||||
struct NewsItem
|
||||
{
|
||||
@@ -14,7 +15,7 @@ struct NewsItem
|
||||
QString title;
|
||||
QString content;
|
||||
QDateTime timestamp;
|
||||
bool read;
|
||||
bool isUpdate = false;
|
||||
};
|
||||
|
||||
class NewsModel : public QAbstractListModel
|
||||
@@ -27,17 +28,20 @@ public:
|
||||
ContentRole,
|
||||
TimestampRole,
|
||||
IsReadRole,
|
||||
IsProcessedRole
|
||||
IsProcessedRole,
|
||||
IsUpdateRole
|
||||
};
|
||||
explicit NewsModel(class SecureAppSettingsRepository* appSettingsRepository, QObject *parent = nullptr);
|
||||
Q_INVOKABLE void markAsRead(int index);
|
||||
Q_INVOKABLE void markUpdateAsSkipped();
|
||||
|
||||
Q_PROPERTY(int processedIndex READ processedIndex WRITE setProcessedIndex NOTIFY processedIndexChanged)
|
||||
Q_PROPERTY(bool hasUnread READ hasUnread NOTIFY hasUnreadChanged)
|
||||
int processedIndex() const;
|
||||
void setProcessedIndex(int index);
|
||||
|
||||
void updateModel(const QJsonArray &items);
|
||||
void setNewsList(const QJsonArray &items);
|
||||
void setUpdateNotification(const QString &id, const QString &title, const QString &content);
|
||||
bool hasUnread() const;
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
@@ -50,11 +54,14 @@ signals:
|
||||
|
||||
private:
|
||||
QVector<NewsItem> m_items;
|
||||
QVector<NewsItem> m_apiItems;
|
||||
std::optional<NewsItem> m_updateItem;
|
||||
int m_processedIndex = -1;
|
||||
class SecureAppSettingsRepository* m_appSettingsRepository;
|
||||
QSet<QString> m_readIds;
|
||||
void loadReadIds();
|
||||
void saveReadIds() const;
|
||||
void updateModel();
|
||||
};
|
||||
|
||||
#endif // NEWSMODEL_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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ DrawerType2 {
|
||||
property bool isRenewalAvailable: false
|
||||
|
||||
onOpened: {
|
||||
isRenewalAvailable = ServersModel.getProcessedServerData("isRenewalAvailable") && !ApiAccountInfoModel.data("isInAppPurchase")
|
||||
isRenewalAvailable = ServersModel.getDefaultServerData("isRenewalAvailable") && !ApiAccountInfoModel.data("isInAppPurchase")
|
||||
}
|
||||
|
||||
expandedStateContent: ColumnLayout {
|
||||
@@ -28,7 +28,7 @@ DrawerType2 {
|
||||
spacing: 0
|
||||
|
||||
onImplicitHeightChanged: {
|
||||
root.expandedHeight = content.implicitHeight + 32 + SettingsController.safeAreaBottomMargin
|
||||
root.expandedHeight = content.implicitHeight + 32 + PageController.safeAreaBottomMargin
|
||||
}
|
||||
|
||||
Item {
|
||||
@@ -43,7 +43,7 @@ DrawerType2 {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
text: ServersModel.getProcessedServerData("name") + qsTr(" subscription has expired")
|
||||
text: ServersModel.getDefaultServerData("name") + qsTr(" subscription has expired")
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,7 @@ DrawerType2 {
|
||||
textColor: AmneziaStyle.color.midnightBlack
|
||||
|
||||
clickedFunc: function() {
|
||||
SubscriptionUiController.getRenewalLink(ServersUiController.getProcessedServerIndex())
|
||||
SubscriptionUiController.getRenewalLink(ServersUiController.defaultIndex)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ DrawerType2 {
|
||||
|
||||
clickedFunc: function() {
|
||||
PageController.showBusyIndicator(true)
|
||||
let result = ApiSettingsController.getAccountInfo(false)
|
||||
let result = SubscriptionUiController.getAccountInfo(ServersUiController.defaultIndex, false)
|
||||
PageController.showBusyIndicator(false)
|
||||
if (result) {
|
||||
root.closeTriggered()
|
||||
|
||||
@@ -469,7 +469,7 @@ PageType {
|
||||
PageController.showNotificationMessage(qsTr("Cannot unlink device during active connection"))
|
||||
} else {
|
||||
PageController.showBusyIndicator(true)
|
||||
if (SubscriptionUiController.deactivateDevice(ServersUiController.getProcessedServerIndex(), false)) {
|
||||
if (SubscriptionUiController.deactivateDevice(ServersUiController.getProcessedServerIndex())) {
|
||||
SubscriptionUiController.getAccountInfo(ServersUiController.getProcessedServerIndex(), true)
|
||||
}
|
||||
PageController.showBusyIndicator(false)
|
||||
|
||||
@@ -14,6 +14,7 @@ import SortFilterProxyModel 0.2
|
||||
PageType {
|
||||
id: root
|
||||
property var newsItem
|
||||
property bool isUpdateItem: newsItem && (newsItem.isUpdate !== undefined ? newsItem.isUpdate : false)
|
||||
|
||||
SortFilterProxyModel {
|
||||
id: proxyNews
|
||||
@@ -54,7 +55,7 @@ PageType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
headerText: newsItem.title
|
||||
headerText: newsItem ? newsItem.title : ""
|
||||
}
|
||||
|
||||
ParagraphTextType {
|
||||
@@ -62,9 +63,9 @@ PageType {
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: newsItem.content
|
||||
text: newsItem ? newsItem.content : ""
|
||||
|
||||
textFormat: Text.RichText
|
||||
textFormat: root.isUpdateItem ? Text.MarkdownText : Text.RichText
|
||||
|
||||
onLinkActivated: function(link) {
|
||||
Qt.openUrlExternally(link)
|
||||
@@ -76,6 +77,47 @@ PageType {
|
||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
}
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 24
|
||||
visible: root.isUpdateItem
|
||||
text: qsTr("Update")
|
||||
|
||||
clickedFunc: function() {
|
||||
if (!root.isUpdateItem)
|
||||
return
|
||||
PageController.showBusyIndicator(true)
|
||||
UpdateController.runInstaller()
|
||||
PageController.showBusyIndicator(false)
|
||||
PageController.closePage()
|
||||
}
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
Layout.bottomMargin: 16
|
||||
visible: root.isUpdateItem
|
||||
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() {
|
||||
if (!root.isUpdateItem)
|
||||
return
|
||||
NewsModel.markUpdateAsSkipped()
|
||||
PageController.closePage()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,8 +69,10 @@ PageType {
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
|
||||
clickedFunction: function() {
|
||||
if (!isUpdate) {
|
||||
NewsModel.markAsRead(index)
|
||||
NewsModel.processedIndex = index
|
||||
}
|
||||
NewsModel.processedIndex = index
|
||||
PageController.goToPage(PageEnum.PageSettingsNewsDetail)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,6 +144,10 @@ Window {
|
||||
busyIndicator.visible = visible
|
||||
PageController.disableControls(visible)
|
||||
}
|
||||
|
||||
function onShowChangelogDrawer() {
|
||||
changelogDrawer.openTriggered()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
@@ -386,4 +390,14 @@ Window {
|
||||
onAccepted: SystemController.fileDialogClosed(true)
|
||||
onRejected: SystemController.fileDialogClosed(false)
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
ChangelogDrawer {
|
||||
id: changelogDrawer
|
||||
|
||||
anchors.fill: parent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<file>Components/HomeContainersListView.qml</file>
|
||||
<file>Components/HomeSplitTunnelingDrawer.qml</file>
|
||||
<file>Components/InstalledAppsDrawer.qml</file>
|
||||
<file>Components/ChangelogDrawer.qml</file>
|
||||
<file>Components/QuestionDrawer.qml</file>
|
||||
<file>Components/SelectLanguageDrawer.qml</file>
|
||||
<file>Components/ServersListView.qml</file>
|
||||
|
||||
@@ -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
|
||||
curl -s https://api.github.com/repos/amnezia-vpn/amnezia-client/releases/tags/$VERSION | jq -r .published_at > RELEASE_DATE
|
||||
|
||||
|
||||
Reference in New Issue
Block a user