diff --git a/.gitignore b/.gitignore index cae7d9c8..50d648be 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ deploy/build_32/* deploy/build_64/* winbuild*.bat .cache/ +.vscode/ # Qt-es diff --git a/CMakeLists.txt b/CMakeLists.txt index 74eb8053..5bb72fd1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) set(PROJECT DefaultVPN) -set(DEFAULTVPN_VERSION 1.0.6.0) +set(DEFAULTVPN_VERSION 1.0.6.1) project(${PROJECT} VERSION ${DEFAULTVPN_VERSION} DESCRIPTION "DefaultVPN" @@ -12,7 +12,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d") set(RELEASE_DATE "${CURRENT_DATE}") set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) -set(APP_ANDROID_VERSION_CODE 2094) +set(APP_ANDROID_VERSION_CODE 2096) if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(MZ_PLATFORM_NAME "linux") diff --git a/client/amnezia_application.cpp b/client/amnezia_application.cpp index 2143721e..2bd950b4 100644 --- a/client/amnezia_application.cpp +++ b/client/amnezia_application.cpp @@ -25,7 +25,9 @@ #include // for QQuickWindow #include // for qobject_cast -AmneziaApplication::AmneziaApplication(int &argc, char *argv[]) : AMNEZIA_BASE_CLASS(argc, argv) +AmneziaApplication::AmneziaApplication(int &argc, char *argv[]) : AMNEZIA_BASE_CLASS(argc, argv), + m_optAutostart({QStringLiteral("a"), QStringLiteral("autostart")}, QStringLiteral("System autostart")), + m_optCleanup ({QStringLiteral("c"), QStringLiteral("cleanup")}, QStringLiteral("Cleanup logs")) { setQuitOnLastWindowClosed(false); @@ -51,18 +53,8 @@ AmneziaApplication::AmneziaApplication(int &argc, char *argv[]) : AMNEZIA_BASE_C AmneziaApplication::~AmneziaApplication() { - if (m_vpnConnection) { - QMetaObject::invokeMethod(m_vpnConnection.get(), "disconnectFromVpn", Qt::QueuedConnection); - QMetaObject::invokeMethod(m_vpnConnection.get(), "deleteLater", Qt::QueuedConnection); - } - m_vpnConnectionThread.quit(); - if (!m_vpnConnectionThread.wait(5000)) { - m_vpnConnectionThread.terminate(); - m_vpnConnectionThread.wait(); - } - if (m_engine) { QObject::disconnect(m_engine, 0, 0, 0); delete m_engine; @@ -119,7 +111,7 @@ void AmneziaApplication::init() Logger::setServiceLogsEnabled(enabled); #ifdef Q_OS_WIN //TODO - if (m_parser.isSet("a")) + if (m_parser.isSet(m_optAutostart)) m_coreController->pageController()->showOnStartup(); else emit m_coreController->pageController()->raiseMainWindow(); @@ -187,15 +179,12 @@ bool AmneziaApplication::parseCommands() m_parser.addHelpOption(); m_parser.addVersionOption(); - QCommandLineOption c_autostart { { "a", "autostart" }, "System autostart" }; - m_parser.addOption(c_autostart); - - QCommandLineOption c_cleanup { { "c", "cleanup" }, "Cleanup logs" }; - m_parser.addOption(c_cleanup); + m_parser.addOption(m_optAutostart); + m_parser.addOption(m_optCleanup); m_parser.process(*this); - if (m_parser.isSet(c_cleanup)) { + if (m_parser.isSet(m_optCleanup)) { Logger::cleanUp(); QTimer::singleShot(100, this, [this] { quit(); }); exec(); diff --git a/client/amnezia_application.h b/client/amnezia_application.h index 28aefab0..9926254a 100644 --- a/client/amnezia_application.h +++ b/client/amnezia_application.h @@ -56,6 +56,9 @@ private: QCommandLineParser m_parser; + QCommandLineOption m_optAutostart; + QCommandLineOption m_optCleanup; + QSharedPointer m_vpnConnection; QThread m_vpnConnectionThread; diff --git a/client/cmake/ios.cmake b/client/cmake/ios.cmake index 4f0c0372..09f0cf1d 100644 --- a/client/cmake/ios.cmake +++ b/client/cmake/ios.cmake @@ -46,6 +46,7 @@ set(SOURCES ${SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.mm + ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/AmneziaSceneDelegateHooks.mm ) @@ -135,10 +136,21 @@ set_property(TARGET ${PROJECT} APPEND PROPERTY RESOURCE add_subdirectory(ios/networkextension) add_dependencies(${PROJECT} networkextension) -set_property(TARGET ${PROJECT} PROPERTY XCODE_EMBED_FRAMEWORKS - "${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-ios/OpenVPNAdapter.framework" +set(OPENVPN_FRAMEWORK_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-ios") +set(OPENVPN_EMBEDDED_FRAMEWORKS + "${OPENVPN_FRAMEWORK_DIR}/OpenVPNAdapter.framework" + "${OPENVPN_FRAMEWORK_DIR}/OpenVPNClient.framework" + "${OPENVPN_FRAMEWORK_DIR}/mbedTLS.framework" + "${OPENVPN_FRAMEWORK_DIR}/LZ4.framework" ) -set(CMAKE_XCODE_ATTRIBUTE_FRAMEWORK_SEARCH_PATHS ${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-ios/) -target_link_libraries("networkextension" PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-ios/OpenVPNAdapter.framework") +set_property(TARGET ${PROJECT} PROPERTY XCODE_EMBED_FRAMEWORKS "${OPENVPN_EMBEDDED_FRAMEWORKS}") +set(CMAKE_XCODE_ATTRIBUTE_FRAMEWORK_SEARCH_PATHS "$(inherited) ${OPENVPN_FRAMEWORK_DIR}") +foreach(_framework ${OPENVPN_EMBEDDED_FRAMEWORKS}) + target_link_libraries(networkextension PRIVATE "${_framework}") +endforeach() + +set_property(TARGET networkextension PROPERTY XCODE_EMBED_FRAMEWORKS "${OPENVPN_EMBEDDED_FRAMEWORKS}") +set_property(TARGET networkextension PROPERTY XCODE_EMBED_FRAMEWORKS_CODE_SIGN_ON_COPY ON) +set_property(TARGET networkextension PROPERTY XCODE_ATTRIBUTE_FRAMEWORK_SEARCH_PATHS "$(inherited) ${OPENVPN_FRAMEWORK_DIR}") diff --git a/client/configurators/openvpn_configurator.cpp b/client/configurators/openvpn_configurator.cpp index bb3dbd4c..75c611d7 100644 --- a/client/configurators/openvpn_configurator.cpp +++ b/client/configurators/openvpn_configurator.cpp @@ -83,12 +83,30 @@ QString OpenVpnConfigurator::createConfig(const ServerCredentials &credentials, return ""; } + auto sanitizeStaticKey = [](const QString &key) { + QStringList lines = key.split('\n'); + QStringList filtered; + filtered.reserve(lines.size()); + for (const QString &line : lines) { + const QString trimmed = line.trimmed(); + if (trimmed.startsWith('#')) { + continue; + } + filtered.append(line); + } + QString result = filtered.join('\n'); + if (!result.endsWith('\n')) { + result.append('\n'); + } + return result; + }; + config.replace("$OPENVPN_CA_CERT", connData.caCert); config.replace("$OPENVPN_CLIENT_CERT", connData.clientCert); config.replace("$OPENVPN_PRIV_KEY", connData.privKey); if (config.contains("$OPENVPN_TA_KEY")) { - config.replace("$OPENVPN_TA_KEY", connData.taKey); + config.replace("$OPENVPN_TA_KEY", sanitizeStaticKey(connData.taKey)); } else { config.replace("", ""); config.replace("", ""); @@ -117,7 +135,7 @@ QString OpenVpnConfigurator::processConfigWithLocalSettings(const QPair(serverConfigObject.value(apiDefs::key::configVersion).toInt()); } -amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList &sslErrors, QNetworkReply *reply) +amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList &sslErrors, const QString &replyErrorString, + const QNetworkReply::NetworkError &replyError, const int httpStatusCode, + const QByteArray &responseBody) { const int httpStatusCodeConflict = 409; const int httpStatusCodeNotFound = 404; @@ -90,21 +92,19 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList &ssl if (!sslErrors.empty()) { qDebug().noquote() << sslErrors; return amnezia::ErrorCode::ApiConfigSslError; - } else if (reply->error() == QNetworkReply::NoError) { + } else if (replyError == QNetworkReply::NoError) { return amnezia::ErrorCode::NoError; - } else if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError - || reply->error() == QNetworkReply::NetworkError::TimeoutError) { - qDebug() << reply->error(); + } else if (replyError == QNetworkReply::NetworkError::OperationCanceledError + || replyError == QNetworkReply::NetworkError::TimeoutError) { + qDebug() << replyError; return amnezia::ErrorCode::ApiConfigTimeoutError; - } else if (reply->error() == QNetworkReply::NetworkError::OperationNotImplementedError) { - qDebug() << reply->error(); + } else if (replyError == QNetworkReply::NetworkError::OperationNotImplementedError) { + qDebug() << replyError; return amnezia::ErrorCode::ApiUpdateRequestError; } else { - QString err = reply->errorString(); - int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - qDebug() << QString::fromUtf8(reply->readAll()); - qDebug() << reply->error(); - qDebug() << err; + qDebug() << QString::fromUtf8(responseBody); + qDebug() << replyError; + qDebug() << replyErrorString; qDebug() << httpStatusCode; if (httpStatusCode == httpStatusCodeConflict) { return amnezia::ErrorCode::ApiConfigLimitError; @@ -162,3 +162,51 @@ QString apiUtils::getPremiumV1VpnKey(const QJsonObject &serverConfigObject) return QString("vpn://%1").arg(QString(signedData.toBase64(QByteArray::Base64UrlEncoding))); } + +QString apiUtils::getPremiumV2VpnKey(const QJsonObject &serverConfigObject) +{ + if (apiUtils::getConfigType(serverConfigObject) != apiDefs::ConfigType::AmneziaPremiumV2) { + return {}; + } + + QString vpnKeyText = ""; + + auto apiConfig = serverConfigObject.value(apiDefs::key::apiConfig).toObject(); + auto authData = serverConfigObject.value(QLatin1String("auth_data")).toObject(); + + const QString name = serverConfigObject.value(apiDefs::key::name).toString(); + const QString description = serverConfigObject.value(apiDefs::key::description).toString(); + const double configVersion = serverConfigObject.value(apiDefs::key::configVersion).toDouble(); + + const QString serviceType = apiConfig.value(apiDefs::key::serviceType).toString(); + const QString serviceProtocol = apiConfig.value(QLatin1String("service_protocol")).toString(); + const QString userCountryCode = apiConfig.value(QLatin1String("user_country_code")).toString(); + + const QString apiKey = authData.value(apiDefs::key::apiKey).toString(); + + QString vpnKeyStr = "{"; + vpnKeyStr += "\"" + QString(apiDefs::key::name) + "\": \"" + name + "\", "; + vpnKeyStr += "\"" + QString(apiDefs::key::description) + "\": \"" + description + "\", "; + vpnKeyStr += "\"" + QString(apiDefs::key::configVersion) + "\": " + QString::number(static_cast(configVersion)) + ", "; + + vpnKeyStr += "\"" + QString(apiDefs::key::apiConfig) + "\": {"; + vpnKeyStr += "\"" + QString(apiDefs::key::serviceType) + "\": \"" + serviceType + "\", "; + vpnKeyStr += "\"service_protocol\": \"" + serviceProtocol + "\", "; + vpnKeyStr += "\"user_country_code\": \"" + userCountryCode + "\""; + vpnKeyStr += "}, "; + + vpnKeyStr += "\"auth_data\": {"; + vpnKeyStr += "\"" + QString(apiDefs::key::apiKey) + "\": \"" + apiKey + "\""; + vpnKeyStr += "}"; + + vpnKeyStr += "}"; + + QByteArray vpnKeyCompressed = escapeUnicode(vpnKeyStr).toUtf8(); + vpnKeyCompressed = qCompress(vpnKeyCompressed, 6); + vpnKeyCompressed = vpnKeyCompressed.mid(4); + + QByteArray signedData = AMNEZIA_CONFIG_SIGNATURE + vpnKeyCompressed; + vpnKeyText = QString("vpn://%1").arg(QString(signedData.toBase64(QByteArray::Base64UrlEncoding))); + + return vpnKeyText; +} diff --git a/client/core/api/apiUtils.h b/client/core/api/apiUtils.h index 45eaf2de..d4e1d9ce 100644 --- a/client/core/api/apiUtils.h +++ b/client/core/api/apiUtils.h @@ -18,9 +18,12 @@ namespace apiUtils apiDefs::ConfigType getConfigType(const QJsonObject &serverConfigObject); apiDefs::ConfigSource getConfigSource(const QJsonObject &serverConfigObject); - amnezia::ErrorCode checkNetworkReplyErrors(const QList &sslErrors, QNetworkReply *reply); + amnezia::ErrorCode checkNetworkReplyErrors(const QList &sslErrors, const QString &replyErrorString, + const QNetworkReply::NetworkError &replyError, const int httpStatusCode, + const QByteArray &responseBody); QString getPremiumV1VpnKey(const QJsonObject &serverConfigObject); + QString getPremiumV2VpnKey(const QJsonObject &serverConfigObject); } #endif // APIUTILS_H diff --git a/client/core/controllers/coreController.cpp b/client/core/controllers/coreController.cpp index 0a6570d1..a44543bb 100644 --- a/client/core/controllers/coreController.cpp +++ b/client/core/controllers/coreController.cpp @@ -26,9 +26,8 @@ CoreController::CoreController(const QSharedPointer &vpnConnectio initNotificationHandler(); - auto locale = m_settings->getAppLanguage(); m_translator.reset(new QTranslator()); - updateTranslator(locale); + updateTranslator(m_settings->getAppLanguage()); } void CoreController::initModels() @@ -100,6 +99,9 @@ void CoreController::initModels() m_apiDevicesModel.reset(new ApiDevicesModel(m_settings, this)); m_engine->rootContext()->setContextProperty("ApiDevicesModel", m_apiDevicesModel.get()); + + m_newsModel.reset(new NewsModel(m_settings, this)); + m_engine->rootContext()->setContextProperty("NewsModel", m_newsModel.get()); } void CoreController::initControllers() @@ -154,6 +156,9 @@ void CoreController::initControllers() m_apiPremV1MigrationController.reset(new ApiPremV1MigrationController(m_serversModel, m_settings, this)); m_engine->rootContext()->setContextProperty("ApiPremV1MigrationController", m_apiPremV1MigrationController.get()); + + m_apiNewsController.reset(new ApiNewsController(m_newsModel, m_settings, m_serversModel, this)); + m_engine->rootContext()->setContextProperty("ApiNewsController", m_apiNewsController.get()); } void CoreController::initAndroidController() @@ -317,6 +322,11 @@ void CoreController::initContainerModelUpdateHandler() connect(m_serversModel.get(), &ServersModel::containersUpdated, m_containersModel.get(), &ContainersModel::updateModel); connect(m_serversModel.get(), &ServersModel::defaultServerContainersUpdated, m_defaultServerContainersModel.get(), &ContainersModel::updateModel); + connect(m_serversModel.get(), &ServersModel::gatewayStacksExpanded, this, [this]() { + if (m_serversModel->hasServersFromGatewayApi()) { + m_apiNewsController->fetchNews(); + } + }); m_serversModel->resetModel(); } diff --git a/client/core/controllers/coreController.h b/client/core/controllers/coreController.h index 66ddb72f..404a1782 100644 --- a/client/core/controllers/coreController.h +++ b/client/core/controllers/coreController.h @@ -12,6 +12,7 @@ #include "ui/controllers/api/apiConfigsController.h" #include "ui/controllers/api/apiSettingsController.h" #include "ui/controllers/api/apiPremV1MigrationController.h" +#include "ui/controllers/api/apiNewsController.h" #include "ui/controllers/appSplitTunnelingController.h" #include "ui/controllers/allowedDnsController.h" #include "ui/controllers/connectionController.h" @@ -47,6 +48,7 @@ #include "ui/models/services/sftpConfigModel.h" #include "ui/models/services/socks5ProxyConfigModel.h" #include "ui/models/sites_model.h" +#include "ui/models/newsModel.h" #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) #include "ui/notificationhandler.h" @@ -118,6 +120,7 @@ private: QScopedPointer m_apiSettingsController; QScopedPointer m_apiConfigsController; QScopedPointer m_apiPremV1MigrationController; + QScopedPointer m_apiNewsController; QSharedPointer m_containersModel; QSharedPointer m_defaultServerContainersModel; @@ -125,6 +128,7 @@ private: QSharedPointer m_languageModel; QSharedPointer m_protocolsModel; QSharedPointer m_sitesModel; + QSharedPointer m_newsModel; QSharedPointer m_allowedDnsModel; QSharedPointer m_appSplitTunnelingModel; QSharedPointer m_clientManagementModel; diff --git a/client/core/controllers/gatewayController.cpp b/client/core/controllers/gatewayController.cpp index 54954063..8bc0939c 100644 --- a/client/core/controllers/gatewayController.cpp +++ b/client/core/controllers/gatewayController.cpp @@ -50,69 +50,6 @@ GatewayController::GatewayController(const QString &gatewayEndpoint, const bool { } -ErrorCode GatewayController::get(const QString &endpoint, QByteArray &responseBody) -{ -#ifdef Q_OS_IOS - IosController::Instance()->requestInetAccess(); - QThread::msleep(10); -#endif - - QNetworkRequest request; - request.setTransferTimeout(m_requestTimeoutMsecs); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - request.setRawHeader(QString("X-Client-Request-ID").toUtf8(), QUuid::createUuid().toString(QUuid::WithoutBraces).toUtf8()); - - request.setUrl(QString(endpoint).arg(m_proxyUrl.isEmpty() ? m_gatewayEndpoint : m_proxyUrl)); - - // bypass killSwitch exceptions for API-gateway -#ifdef AMNEZIA_DESKTOP - if (m_isStrictKillSwitchEnabled) { - QString host = QUrl(request.url()).host(); - QString ip = NetworkUtilities::getIPAddress(host); - if (!ip.isEmpty()) { - IpcClient::Interface()->addKillSwitchAllowedRange(QStringList { ip }); - } - } -#endif - - QNetworkReply *reply; - reply = amnApp->networkManager()->get(request); - - QEventLoop wait; - QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); - - QList sslErrors; - connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); - wait.exec(); - - responseBody = reply->readAll(); - - if (sslErrors.isEmpty() && shouldBypassProxy(reply, responseBody, false)) { - auto requestFunction = [&request, &responseBody](const QString &url) { - request.setUrl(url); - return amnApp->networkManager()->get(request); - }; - - auto replyProcessingFunction = [&responseBody, &reply, &sslErrors, this](QNetworkReply *nestedReply, - const QList &nestedSslErrors) { - responseBody = nestedReply->readAll(); - if (!sslErrors.isEmpty() || !shouldBypassProxy(nestedReply, responseBody, false)) { - sslErrors = nestedSslErrors; - reply = nestedReply; - return true; - } - return false; - }; - - bypassProxy(endpoint, reply, requestFunction, replyProcessingFunction); - } - - auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, reply); - reply->deleteLater(); - - return errorCode; -} - ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody) { #ifdef Q_OS_IOS @@ -188,29 +125,37 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api wait.exec(); QByteArray encryptedResponseBody = reply->readAll(); + QString replyErrorString = reply->errorString(); + auto replyError = reply->error(); + int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - if (sslErrors.isEmpty() && shouldBypassProxy(reply, encryptedResponseBody, true, key, iv, salt)) { + reply->deleteLater(); + + if (sslErrors.isEmpty() && shouldBypassProxy(replyError, encryptedResponseBody, true, key, iv, salt)) { auto requestFunction = [&request, &encryptedResponseBody, &requestBody](const QString &url) { request.setUrl(url); return amnApp->networkManager()->post(request, QJsonDocument(requestBody).toJson()); }; - auto replyProcessingFunction = [&encryptedResponseBody, &reply, &sslErrors, &key, &iv, &salt, - this](QNetworkReply *nestedReply, const QList &nestedSslErrors) { - encryptedResponseBody = nestedReply->readAll(); - reply = nestedReply; - if (!sslErrors.isEmpty() || shouldBypassProxy(nestedReply, encryptedResponseBody, true, key, iv, salt)) { + auto replyProcessingFunction = [&encryptedResponseBody, &replyErrorString, &replyError, &httpStatusCode, &sslErrors, &key, &iv, + &salt, this](QNetworkReply *reply, const QList &nestedSslErrors) { + encryptedResponseBody = reply->readAll(); + replyErrorString = reply->errorString(); + replyError = reply->error(); + httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (!sslErrors.isEmpty() || shouldBypassProxy(replyError, encryptedResponseBody, true, key, iv, salt)) { sslErrors = nestedSslErrors; return false; } return true; }; - bypassProxy(endpoint, reply, requestFunction, replyProcessingFunction); + auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString(""); + auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString(""); + bypassProxy(endpoint, serviceType, userCountryCode, requestFunction, replyProcessingFunction); } - auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, reply); - reply->deleteLater(); + auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, encryptedResponseBody); if (errorCode) { return errorCode; } @@ -225,7 +170,7 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api } } -QStringList GatewayController::getProxyUrls() +QStringList GatewayController::getProxyUrls(const QString &serviceType, const QString &userCountryCode) { QNetworkRequest request; request.setTransferTimeout(m_requestTimeoutMsecs); @@ -235,15 +180,25 @@ QStringList GatewayController::getProxyUrls() QList sslErrors; QNetworkReply *reply; - QStringList proxyStorageUrls; + QStringList baseUrls; if (m_isDevEnvironment) { - proxyStorageUrls = QString(DEV_S3_ENDPOINT).split(", "); + baseUrls = QString(DEV_S3_ENDPOINT).split(", "); } else { - proxyStorageUrls = QString(PROD_S3_ENDPOINT).split(", "); + baseUrls = QString(PROD_S3_ENDPOINT).split(", "); } QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY; + QStringList proxyStorageUrls; + if (!serviceType.isEmpty()) { + for (const auto &baseUrl : baseUrls) { + proxyStorageUrls.push_back(baseUrl + "-" + serviceType + "-" + userCountryCode + ".json"); + } + } + for (const auto &baseUrl : baseUrls) { + proxyStorageUrls.push_back(baseUrl + ".json"); + } + for (const auto &proxyStorageUrl : proxyStorageUrls) { request.setUrl(proxyStorageUrl); reply = amnApp->networkManager()->get(request); @@ -288,7 +243,10 @@ QStringList GatewayController::getProxyUrls() } return endpoints; } else { - apiUtils::checkNetworkReplyErrors(sslErrors, reply); + auto replyError = reply->error(); + int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + qDebug() << replyError; + qDebug() << httpStatusCode; qDebug() << "go to the next storage endpoint"; reply->deleteLater(); @@ -297,33 +255,33 @@ QStringList GatewayController::getProxyUrls() return {}; } -bool GatewayController::shouldBypassProxy(QNetworkReply *reply, const QByteArray &responseBody, bool checkEncryption, const QByteArray &key, - const QByteArray &iv, const QByteArray &salt) +bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &responseBody, + bool checkEncryption, const QByteArray &key, const QByteArray &iv, const QByteArray &salt) { - if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError || reply->error() == QNetworkReply::NetworkError::TimeoutError) { + if (replyError == QNetworkReply::NetworkError::OperationCanceledError || replyError == QNetworkReply::NetworkError::TimeoutError) { qDebug() << "timeout occurred"; - qDebug() << reply->error(); + qDebug() << replyError; return true; } else if (responseBody.contains("html")) { qDebug() << "the response contains an html tag"; return true; - } else if (reply->error() == QNetworkReply::NetworkError::ContentNotFoundError) { + } else if (replyError == QNetworkReply::NetworkError::ContentNotFoundError) { if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2) || responseBody.contains(errorResponsePattern3)) { return false; } else { - qDebug() << reply->error(); + qDebug() << replyError; return true; } - } else if (reply->error() == QNetworkReply::NetworkError::OperationNotImplementedError) { + } else if (replyError == QNetworkReply::NetworkError::OperationNotImplementedError) { if (responseBody.contains(updateRequestResponsePattern)) { return false; } else { - qDebug() << reply->error(); + qDebug() << replyError; return true; } - } else if (reply->error() != QNetworkReply::NetworkError::NoError) { - qDebug() << reply->error(); + } else if (replyError != QNetworkReply::NetworkError::NoError) { + qDebug() << replyError; return true; } else if (checkEncryption) { try { @@ -337,35 +295,33 @@ bool GatewayController::shouldBypassProxy(QNetworkReply *reply, const QByteArray return false; } -void GatewayController::bypassProxy(const QString &endpoint, QNetworkReply *reply, +void GatewayController::bypassProxy(const QString &endpoint, const QString &serviceType, const QString &userCountryCode, std::function requestFunction, std::function &sslErrors)> replyProcessingFunction) { - QStringList proxyUrls = getProxyUrls(); + QStringList proxyUrls = getProxyUrls(serviceType, userCountryCode); std::random_device randomDevice; std::mt19937 generator(randomDevice()); std::shuffle(proxyUrls.begin(), proxyUrls.end(), generator); QByteArray responseBody; - auto bypassFunction = [this](const QString &endpoint, const QString &proxyUrl, QNetworkReply *reply, + auto bypassFunction = [this](const QString &endpoint, const QString &proxyUrl, std::function requestFunction, std::function &sslErrors)> replyProcessingFunction) { QEventLoop wait; QList sslErrors; qDebug() << "go to the next proxy endpoint"; - reply->deleteLater(); // delete the previous reply - reply = requestFunction(endpoint.arg(proxyUrl)); + QNetworkReply *reply = requestFunction(endpoint.arg(proxyUrl)); QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); wait.exec(); - if (replyProcessingFunction(reply, sslErrors)) { - return true; - } - return false; + auto result = replyProcessingFunction(reply, sslErrors); + reply->deleteLater(); + return result; }; if (m_proxyUrl.isEmpty()) { @@ -399,13 +355,13 @@ void GatewayController::bypassProxy(const QString &endpoint, QNetworkReply *repl } if (!m_proxyUrl.isEmpty()) { - if (bypassFunction(endpoint, m_proxyUrl, reply, requestFunction, replyProcessingFunction)) { + if (bypassFunction(endpoint, m_proxyUrl, requestFunction, replyProcessingFunction)) { return; } } for (const QString &proxyUrl : proxyUrls) { - if (bypassFunction(endpoint, proxyUrl, reply, requestFunction, replyProcessingFunction)) { + if (bypassFunction(endpoint, proxyUrl, requestFunction, replyProcessingFunction)) { m_proxyUrl = proxyUrl; break; } diff --git a/client/core/controllers/gatewayController.h b/client/core/controllers/gatewayController.h index 4c247fc6..d4e5061f 100644 --- a/client/core/controllers/gatewayController.h +++ b/client/core/controllers/gatewayController.h @@ -18,14 +18,14 @@ public: explicit GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs, const bool isStrictKillSwitchEnabled, QObject *parent = nullptr); - amnezia::ErrorCode get(const QString &endpoint, QByteArray &responseBody); amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody); private: - QStringList getProxyUrls(); - bool shouldBypassProxy(QNetworkReply *reply, const QByteArray &responseBody, bool checkEncryption, const QByteArray &key = "", - const QByteArray &iv = "", const QByteArray &salt = ""); - void bypassProxy(const QString &endpoint, QNetworkReply *reply, std::function requestFunction, + QStringList getProxyUrls(const QString &serviceType, const QString &userCountryCode); + bool shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &responseBody, bool checkEncryption, + const QByteArray &key = "", const QByteArray &iv = "", const QByteArray &salt = ""); + void bypassProxy(const QString &endpoint, const QString &serviceType, const QString &userCountryCode, + std::function requestFunction, std::function &sslErrors)> replyProcessingFunction); int m_requestTimeoutMsecs; diff --git a/client/images/controls/news-unread.svg b/client/images/controls/news-unread.svg new file mode 100644 index 00000000..22a7b1a0 --- /dev/null +++ b/client/images/controls/news-unread.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/client/images/controls/news.svg b/client/images/controls/news.svg new file mode 100644 index 00000000..92eff99e --- /dev/null +++ b/client/images/controls/news.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/client/images/controls/settings-news.svg b/client/images/controls/settings-news.svg new file mode 100644 index 00000000..39225d46 --- /dev/null +++ b/client/images/controls/settings-news.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/images/controls/unread-dot.svg b/client/images/controls/unread-dot.svg new file mode 100644 index 00000000..3ba4e178 --- /dev/null +++ b/client/images/controls/unread-dot.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/client/ios/app/Info.plist.in b/client/ios/app/Info.plist.in index f282b693..29f3f482 100644 --- a/client/ios/app/Info.plist.in +++ b/client/ios/app/Info.plist.in @@ -32,17 +32,41 @@ UILaunchStoryboardName DefaultVPNLaunchScreen + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + QIOSWindowSceneDelegate + + + + UIRequiredDeviceCapabilities UIRequiresFullScreen - + UISupportedInterfaceOrientations UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationPortrait UISupportedInterfaceOrientations~ipad - + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + UIUserInterfaceStyle Light com.wireguard.ios.app_group_id @@ -50,7 +74,7 @@ UIViewControllerBasedStatusBarAppearance NSCameraUsageDescription - Amnezia VPN needs access to the camera for reading QR-codes. + Default VPN needs access to the camera for reading QR-codes. NSAppTransportSecurity NSAllowsArbitraryLoads @@ -70,7 +94,7 @@ public.data UTTypeDescription - Amnezia VPN config + Default VPN config UTTypeIconFiles UTTypeIdentifier @@ -162,7 +186,7 @@ CFBundleTypeName - Amnezia VPN config + Default VPN config LSHandlerRank Alternate LSItemContentTypes diff --git a/client/mozilla/localsocketcontroller.cpp b/client/mozilla/localsocketcontroller.cpp index 22033c20..615d34ab 100644 --- a/client/mozilla/localsocketcontroller.cpp +++ b/client/mozilla/localsocketcontroller.cpp @@ -264,13 +264,13 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) { && !wgConfig.value(amnezia::config_key::junkPacketMaxSize).isUndefined() && !wgConfig.value(amnezia::config_key::initPacketJunkSize).isUndefined() && !wgConfig.value(amnezia::config_key::responsePacketJunkSize).isUndefined() - && !wgConfig.value(amnezia::config_key::cookieReplyPacketJunkSize).isUndefined() - && !wgConfig.value(amnezia::config_key::transportPacketJunkSize).isUndefined() + // && !wgConfig.value(amnezia::config_key::cookieReplyPacketJunkSize).isUndefined() + // && !wgConfig.value(amnezia::config_key::transportPacketJunkSize).isUndefined() && !wgConfig.value(amnezia::config_key::initPacketMagicHeader).isUndefined() && !wgConfig.value(amnezia::config_key::responsePacketMagicHeader).isUndefined() && !wgConfig.value(amnezia::config_key::underloadPacketMagicHeader).isUndefined() && !wgConfig.value(amnezia::config_key::transportPacketMagicHeader).isUndefined() - && !wgConfig.value(amnezia::config_key::specialJunk1).isUndefined() +/* && !wgConfig.value(amnezia::config_key::specialJunk1).isUndefined() && !wgConfig.value(amnezia::config_key::specialJunk2).isUndefined() && !wgConfig.value(amnezia::config_key::specialJunk3).isUndefined() && !wgConfig.value(amnezia::config_key::specialJunk4).isUndefined() @@ -278,27 +278,27 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) { && !wgConfig.value(amnezia::config_key::controlledJunk1).isUndefined() && !wgConfig.value(amnezia::config_key::controlledJunk2).isUndefined() && !wgConfig.value(amnezia::config_key::controlledJunk3).isUndefined() - && !wgConfig.value(amnezia::config_key::specialHandshakeTimeout).isUndefined()) { + && !wgConfig.value(amnezia::config_key::specialHandshakeTimeout).isUndefined()*/) { json.insert(amnezia::config_key::junkPacketCount, wgConfig.value(amnezia::config_key::junkPacketCount)); json.insert(amnezia::config_key::junkPacketMinSize, wgConfig.value(amnezia::config_key::junkPacketMinSize)); json.insert(amnezia::config_key::junkPacketMaxSize, wgConfig.value(amnezia::config_key::junkPacketMaxSize)); json.insert(amnezia::config_key::initPacketJunkSize, wgConfig.value(amnezia::config_key::initPacketJunkSize)); json.insert(amnezia::config_key::responsePacketJunkSize, wgConfig.value(amnezia::config_key::responsePacketJunkSize)); - json.insert(amnezia::config_key::cookieReplyPacketJunkSize, wgConfig.value(amnezia::config_key::cookieReplyPacketJunkSize)); - json.insert(amnezia::config_key::transportPacketJunkSize, wgConfig.value(amnezia::config_key::transportPacketJunkSize)); + // json.insert(amnezia::config_key::cookieReplyPacketJunkSize, wgConfig.value(amnezia::config_key::cookieReplyPacketJunkSize)); + // json.insert(amnezia::config_key::transportPacketJunkSize, wgConfig.value(amnezia::config_key::transportPacketJunkSize)); json.insert(amnezia::config_key::initPacketMagicHeader, wgConfig.value(amnezia::config_key::initPacketMagicHeader)); json.insert(amnezia::config_key::responsePacketMagicHeader, wgConfig.value(amnezia::config_key::responsePacketMagicHeader)); json.insert(amnezia::config_key::underloadPacketMagicHeader, wgConfig.value(amnezia::config_key::underloadPacketMagicHeader)); json.insert(amnezia::config_key::transportPacketMagicHeader, wgConfig.value(amnezia::config_key::transportPacketMagicHeader)); - json.insert(amnezia::config_key::specialJunk1, wgConfig.value(amnezia::config_key::specialJunk1)); - json.insert(amnezia::config_key::specialJunk2, wgConfig.value(amnezia::config_key::specialJunk2)); - json.insert(amnezia::config_key::specialJunk3, wgConfig.value(amnezia::config_key::specialJunk3)); - json.insert(amnezia::config_key::specialJunk4, wgConfig.value(amnezia::config_key::specialJunk4)); - json.insert(amnezia::config_key::specialJunk5, wgConfig.value(amnezia::config_key::specialJunk5)); - json.insert(amnezia::config_key::controlledJunk1, wgConfig.value(amnezia::config_key::controlledJunk1)); - json.insert(amnezia::config_key::controlledJunk2, wgConfig.value(amnezia::config_key::controlledJunk2)); - json.insert(amnezia::config_key::controlledJunk3, wgConfig.value(amnezia::config_key::controlledJunk3)); - json.insert(amnezia::config_key::specialHandshakeTimeout, wgConfig.value(amnezia::config_key::specialHandshakeTimeout)); + // json.insert(amnezia::config_key::specialJunk1, wgConfig.value(amnezia::config_key::specialJunk1)); + // json.insert(amnezia::config_key::specialJunk2, wgConfig.value(amnezia::config_key::specialJunk2)); + // json.insert(amnezia::config_key::specialJunk3, wgConfig.value(amnezia::config_key::specialJunk3)); + // json.insert(amnezia::config_key::specialJunk4, wgConfig.value(amnezia::config_key::specialJunk4)); + // json.insert(amnezia::config_key::specialJunk5, wgConfig.value(amnezia::config_key::specialJunk5)); + // json.insert(amnezia::config_key::controlledJunk1, wgConfig.value(amnezia::config_key::controlledJunk1)); + // json.insert(amnezia::config_key::controlledJunk2, wgConfig.value(amnezia::config_key::controlledJunk2)); + // json.insert(amnezia::config_key::controlledJunk3, wgConfig.value(amnezia::config_key::controlledJunk3)); + // json.insert(amnezia::config_key::specialHandshakeTimeout, wgConfig.value(amnezia::config_key::specialHandshakeTimeout)); } write(json); diff --git a/client/platforms/ios/AmneziaSceneDelegateHooks.mm b/client/platforms/ios/AmneziaSceneDelegateHooks.mm new file mode 100644 index 00000000..60cbbe0f --- /dev/null +++ b/client/platforms/ios/AmneziaSceneDelegateHooks.mm @@ -0,0 +1,82 @@ +#import +#import +#include + +#include +#include +#include + +#include "ios_controller.h" + +using SceneOpenURLContexts = void (*)(id, SEL, UIScene *, NSSet *); + +static SceneOpenURLContexts g_originalSceneOpenURLContexts = nullptr; + +static void amnezia_handleURL(NSURL *url) +{ + if (!url || !url.isFileURL) { + return; + } + + QString filePath(url.path.UTF8String); + if (filePath.isEmpty()) { + return; + } + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + if (filePath.contains("backup")) { + IosController::Instance()->importBackupFromOutside(filePath); + return; + } + + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) { + return; + } + + const QByteArray data = file.readAll(); + IosController::Instance()->importConfigFromOutside(QString::fromUtf8(data)); + }); +} + +static void amnezia_scene_openURLContexts(id self, SEL _cmd, UIScene *scene, NSSet *contexts) +{ + if (g_originalSceneOpenURLContexts) { + g_originalSceneOpenURLContexts(self, _cmd, scene, contexts); + } + + if (!contexts || contexts.count == 0) { + return; + } + + if (@available(iOS 13.0, *)) { + for (UIOpenURLContext *context in contexts) { + amnezia_handleURL(context.URL); + } + } +} + +@interface AmneziaSceneDelegateHooks : NSObject +@end + +@implementation AmneziaSceneDelegateHooks + ++ (void)load +{ + Class cls = objc_getClass("QIOSWindowSceneDelegate"); + if (!cls) { + return; + } + + SEL selector = @selector(scene:openURLContexts:); + Method method = class_getInstanceMethod(cls, selector); + if (method) { + g_originalSceneOpenURLContexts = reinterpret_cast(method_getImplementation(method)); + method_setImplementation(method, reinterpret_cast(amnezia_scene_openURLContexts)); + } else { + const char *types = "v@:@@"; + class_addMethod(cls, selector, reinterpret_cast(amnezia_scene_openURLContexts), types); + } +} + +@end diff --git a/client/platforms/ios/Log.swift b/client/platforms/ios/Log.swift index da07f66a..1494992b 100644 --- a/client/platforms/ios/Log.swift +++ b/client/platforms/ios/Log.swift @@ -2,7 +2,8 @@ import Foundation import os.log struct Log { - static let osLog = Logger() + private static let subsystemIdentifier = Bundle.main.bundleIdentifier ?? "org.amnezia.AmneziaVPN" + static let osLog = Logger(subsystem: subsystemIdentifier, category: "App") private static let IsLoggingEnabledKey = "IsLoggingEnabled" static var isLoggingEnabled: Bool { @@ -77,9 +78,40 @@ struct Log { static func log(_ type: OSLogType, title: String = "", message: String, url: URL = neLogURL) { NSLog("\(title) \(message)") - guard isLoggingEnabled else { return } + switch type { + case .debug: + if title.isEmpty { + osLog.debug("\(message, privacy: .public)") + } else { + osLog.debug("\(title, privacy: .public) \(message, privacy: .public)") + } + case .info: + if title.isEmpty { + osLog.info("\(message, privacy: .public)") + } else { + osLog.info("\(title, privacy: .public) \(message, privacy: .public)") + } + case .error: + if title.isEmpty { + osLog.error("\(message, privacy: .public)") + } else { + osLog.error("\(title, privacy: .public) \(message, privacy: .public)") + } + case .fault: + if title.isEmpty { + osLog.fault("\(message, privacy: .public)") + } else { + osLog.fault("\(title, privacy: .public) \(message, privacy: .public)") + } + default: + if title.isEmpty { + osLog.log("\(message, privacy: .public)") + } else { + osLog.log("\(title, privacy: .public) \(message, privacy: .public)") + } + } - osLog.log(level: type, "\(title) \(message)") + guard isLoggingEnabled else { return } let date = Date() let level = Record.Level(from: type) diff --git a/client/platforms/ios/NELogController.swift b/client/platforms/ios/NELogController.swift index 257dc087..5426b865 100644 --- a/client/platforms/ios/NELogController.swift +++ b/client/platforms/ios/NELogController.swift @@ -1,22 +1,76 @@ import Foundation import os.log +private let subsystemIdentifier = Bundle.main.bundleIdentifier ?? "org.amnezia.AmneziaVPN" +private let wireGuardSystemLogger = Logger(subsystem: subsystemIdentifier, category: "WireGuard") +private let openVPNSystemLogger = Logger(subsystem: subsystemIdentifier, category: "OpenVPN") +private let xraySystemLogger = Logger(subsystem: subsystemIdentifier, category: "Xray") +private let networkExtensionLogger = Logger(subsystem: subsystemIdentifier, category: "NetworkExtension") + +private func logToSystem(_ logger: Logger, type: OSLogType, prefix: String, title: String, message: String) { + let combinedTitle: String + if title.isEmpty { + combinedTitle = prefix + } else { + combinedTitle = "\(prefix): \(title)" + } + + switch type { + case .debug: + if combinedTitle.isEmpty { + logger.debug("\(message, privacy: .public)") + } else { + logger.debug("\(combinedTitle, privacy: .public) \(message, privacy: .public)") + } + case .info: + if combinedTitle.isEmpty { + logger.info("\(message, privacy: .public)") + } else { + logger.info("\(combinedTitle, privacy: .public) \(message, privacy: .public)") + } + case .error: + if combinedTitle.isEmpty { + logger.error("\(message, privacy: .public)") + } else { + logger.error("\(combinedTitle, privacy: .public) \(message, privacy: .public)") + } + case .fault: + if combinedTitle.isEmpty { + logger.fault("\(message, privacy: .public)") + } else { + logger.fault("\(combinedTitle, privacy: .public) \(message, privacy: .public)") + } + default: + if combinedTitle.isEmpty { + logger.log("\(message, privacy: .public)") + } else { + logger.log("\(combinedTitle, privacy: .public) \(message, privacy: .public)") + } + } +} + public func wg_log(_ type: OSLogType, title: String = "", staticMessage: StaticString) { - neLog(type, title: "WG: \(title)", message: "\(staticMessage)") + let stringMessage = String(describing: staticMessage) + logToSystem(wireGuardSystemLogger, type: type, prefix: "WG", title: title, message: stringMessage) + neLog(type, title: "WG: \(title)", message: stringMessage) } public func wg_log(_ type: OSLogType, title: String = "", message: String) { + logToSystem(wireGuardSystemLogger, type: type, prefix: "WG", title: title, message: message) neLog(type, title: "WG: \(title)", message: message) } public func ovpnLog(_ type: OSLogType, title: String = "", message: String) { + logToSystem(openVPNSystemLogger, type: type, prefix: "OVPN", title: title, message: message) neLog(type, title: "OVPN: \(title)", message: message) } public func xrayLog(_ type: OSLogType, title: String = "", message: String) { + logToSystem(xraySystemLogger, type: type, prefix: "XRAY", title: title, message: message) neLog(type, title: "XRAY: \(title)", message: message) } public func neLog(_ type: OSLogType, title: String = "", message: String) { + logToSystem(networkExtensionLogger, type: type, prefix: "NE", title: title, message: message) Log.log(type, title: "NE: \(title)", message: message) } diff --git a/client/platforms/ios/PacketTunnelProvider+OpenVPN.swift b/client/platforms/ios/PacketTunnelProvider+OpenVPN.swift index bfd1165f..831651c0 100644 --- a/client/platforms/ios/PacketTunnelProvider+OpenVPN.swift +++ b/client/platforms/ios/PacketTunnelProvider+OpenVPN.swift @@ -1,6 +1,7 @@ import Foundation import NetworkExtension import OpenVPNAdapter +import CryptoKit struct OpenVPNConfig: Decodable { let config: String @@ -27,26 +28,83 @@ extension PacketTunnelProvider { let ovpnConfiguration = Data(openVPNConfig.config.utf8) setupAndlaunchOpenVPN(withConfig: ovpnConfiguration, completionHandler: completionHandler) } catch { - ovpnLog(.error, message: "Can't parse config: \(error.localizedDescription)") - - if let underlyingError = (error as NSError).userInfo[NSUnderlyingErrorKey] as? NSError { - ovpnLog(.error, message: "Can't parse config: \(underlyingError.localizedDescription)") - } - + ovpnLog(.error, message: "Can't parse OpenVPN config: \(error.localizedDescription)") return } } + private func logOpenVPNError(_ error: NSError) { + let fatalFlag = (error.userInfo[OpenVPNAdapterErrorFatalKey] as? Bool) ?? false + var lines: [String] = [] + lines.append("domain=\(error.domain) code=\(error.code) fatal=\(fatalFlag)") + + if let adapterMessage = error.userInfo[OpenVPNAdapterErrorMessageKey] as? String, !adapterMessage.isEmpty { + lines.append("message=\(adapterMessage)") + } + + let userInfoKeys = error.userInfo.keys.map { String(describing: $0) }.sorted() + if !userInfoKeys.isEmpty { + lines.append("userInfoKeys=[\(userInfoKeys.joined(separator: ","))]") + } + + if let underlying = error.userInfo[NSUnderlyingErrorKey] as? NSError { + lines.append("underlying=\(underlying.domain)#\(underlying.code) fatal=\((underlying.userInfo[OpenVPNAdapterErrorFatalKey] as? Bool) ?? false)") + if let underlyingMessage = underlying.userInfo[OpenVPNAdapterErrorMessageKey] as? String, !underlyingMessage.isEmpty { + lines.append("underlyingMessage=\(underlyingMessage)") + } else if !underlying.localizedDescription.isEmpty { + lines.append("underlyingLocalized=\(underlying.localizedDescription)") + } + } else if let underlying = error.userInfo[NSUnderlyingErrorKey] { + lines.append("underlyingRaw=\(underlying)") + } + + let formatted = lines.joined(separator: "\n ") + ovpnLog(.error, title: "Error", message: formatted) + } + private func setupAndlaunchOpenVPN(withConfig ovpnConfiguration: Data, withShadowSocks viaSS: Bool = false, completionHandler: @escaping (Error?) -> Void) { ovpnLog(.info, message: "Setup and launch") - let str = String(decoding: ovpnConfiguration, as: UTF8.self) + var configString = String(decoding: ovpnConfiguration, as: UTF8.self) + + let digest = SHA256.hash(data: ovpnConfiguration) + let digestString = digest.map { String(format: "%02x", $0) }.joined() + ovpnLog(.info, title: "ConfigDigest", message: digestString) + + let hasTlsAuthOpen = configString.contains("") + let hasTlsAuthClose = configString.contains("") + ovpnLog(.info, title: "ConfigFlags", message: "tls-auth open=\(hasTlsAuthOpen) close=\(hasTlsAuthClose)") + + let lines = configString.split(separator: "\n") + let head = lines.prefix(10).joined(separator: "\n") + let tail = lines.suffix(10).joined(separator: "\n") + ovpnLog(.debug, title: "ConfigHead", message: head) + ovpnLog(.debug, title: "ConfigTail", message: tail) + + if let start = configString.range(of: ""), + let end = configString.range(of: "", range: start.upperBound..disableKillSwitch(); IpcClient::Interface()->StartRoutingIpv6(); + IpcClient::Interface()->restoreResolvers(); #endif qDebug() << "XrayProtocol::stop()"; m_xrayProcess.disconnect(); diff --git a/client/resources.qrc b/client/resources.qrc index 21c4c4de..a322fbfc 100644 --- a/client/resources.qrc +++ b/client/resources.qrc @@ -36,6 +36,9 @@ images/controls/mail.svg images/controls/map-pin.svg images/controls/more-vertical.svg + images/controls/news.svg + images/controls/news-unread.svg + images/controls/unread-dot.svg images/controls/plus.svg images/controls/qr-code.svg images/controls/radio-button-inner-circle-pressed.png @@ -50,6 +53,7 @@ images/controls/server.svg images/controls/settings-2.svg images/controls/settings.svg + images/controls/settings-news.svg images/controls/share-2.svg images/controls/split-tunneling.svg images/controls/tag.svg @@ -214,6 +218,8 @@ ui/qml/Pages2/PageSettingsServerServices.qml ui/qml/Pages2/PageSettingsServersList.qml ui/qml/Pages2/PageSettingsSplitTunneling.qml + ui/qml/Pages2/PageSettingsNewsNotifications.qml + ui/qml/Pages2/PageSettingsNewsDetail.qml ui/qml/Pages2/PageProtocolAwgClientSettings.qml ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml diff --git a/client/server_scripts/check_server.sh b/client/server_scripts/check_server.sh new file mode 100755 index 00000000..2db63fc5 --- /dev/null +++ b/client/server_scripts/check_server.sh @@ -0,0 +1,519 @@ +#!/bin/sh + +LOG_DATE=$(date -u +'%Y%m%d-%H%M%S') +SCRIPT_DIR=$(dirname "$0") +LOG_FILE="${SCRIPT_DIR}/server-diagnostics-${LOG_DATE}.log" + +# Logging function (sh compatible) +log_and_display() { + if [ "$1" = "-n" ]; then + shift + printf "%s" "$*" | tee -a "$LOG_FILE" + else + echo "$1" | tee -a "$LOG_FILE" + fi +} + +# Redirect stderr to stdout for logging +exec 2>&1 + +header() { + log_and_display "" + log_and_display "=== $1 ===" +} + +# Pause for cancellation +log_and_display "" +log_and_display "VPN Server Diagnostics will start in 9s. Press Ctrl+C to cancel." +sleep 9 + +log_and_display "" +header "STARTING VPN SERVER DIAGNOSTICS" +log_and_display "" + +# ------------------------------------------------------------------------------ +# 1. Basic system information +# ------------------------------------------------------------------------------ +header "System Information" + +# Uptime +UPTIME_STR=$(awk '{printf "%d:%02d:%02d", int($1/3600), int(($1%3600)/60), int($1%60)}' /proc/uptime 2>/dev/null || echo "unknown") + log_and_display "Uptime (H:M:S): $UPTIME_STR" + +# Date/time UTC +DATE_UTC=$(date -u +'%d %b %Y|%T' 2>/dev/null || echo "unknown") + log_and_display "Date|Time (UTC): $DATE_UTC" + +# Init system (PID 1) +INIT_NAME=$(cat /proc/1/status 2>/dev/null | head -1 | awk '{print $2}' 2>/dev/null || echo "unknown") + log_and_display "Init system (PID 1): $INIT_NAME" + +# Locale +if echo "$LANG" | grep -E '^(en_US.UTF-8|C.UTF-8|C)$' >/dev/null 2>&1; then + log_and_display "Locale: $LANG" +else + log_and_display "Locale: $LANG (not en_US.UTF-8, C.UTF-8 or C)" +fi + +# ------------------------------------------------------------------------------ +# 2. Package manager detection +# ------------------------------------------------------------------------------ +header "Package Manager Information" + +if command -v apt-get >/dev/null 2>&1; then + log_and_display "Package Manager: APT" + PM="apt-get" + PM_VER_OPT="--version" + DOCKER_PKG="docker.io" +elif command -v dnf >/dev/null 2>&1; then + log_and_display "Package Manager: DNF" + PM="dnf" + PM_VER_OPT="--version" + DOCKER_PKG="docker" +elif command -v yum >/dev/null 2>&1; then + log_and_display "Package Manager: YUM" + PM="yum" + PM_VER_OPT="--version" + DOCKER_PKG="docker" +elif command -v zypper >/dev/null 2>&1; then + log_and_display "Package Manager: ZYPPER" + PM="zypper" + PM_VER_OPT="--version" + DOCKER_PKG="docker" +elif command -v pacman >/dev/null 2>&1; then + log_and_display "Package Manager: PACMAN" + PM="pacman" + PM_VER_OPT="--version" + DOCKER_PKG="docker" +elif command -v opkg >/dev/null 2>&1; then + log_and_display "Package Manager: OPKG - Not supported on this platform" + PM="opkg" + PM_VER_OPT="--version" + DOCKER_PKG="docker" +else + log_and_display "Package Manager: Unknown" + # fallback + PM="uname" + PM_VER_OPT="-a" + DOCKER_PKG="docker" +fi + +# Check package versions +log_and_display "" +log_and_display "Package versions:" + +# Check sudo +if [ "$PM" = "apt-get" ]; then + sudo_version=$(dpkg -s "sudo" 2>/dev/null | grep '^Version:' | awk '{print $2}' || echo "not installed") +elif [ "$PM" = "dnf" ] || [ "$PM" = "yum" ] || [ "$PM" = "zypper" ]; then + sudo_version=$(rpm -q "sudo" 2>/dev/null || echo "not installed") +elif [ "$PM" = "pacman" ]; then + sudo_version=$(pacman -Q "sudo" 2>/dev/null || echo "not installed") +elif [ "$PM" = "opkg" ]; then + sudo_version=$(opkg info "sudo" 2>/dev/null | grep '^Version:' | awk '{print $2}' || echo "not installed") +else + sudo_version="unknown" +fi +log_and_display " sudo: $sudo_version" + +# Check Docker package +if [ "$PM" = "apt-get" ]; then + docker_pkg_version=$(dpkg -s "$DOCKER_PKG" 2>/dev/null | grep '^Version:' | awk '{print $2}' || echo "not installed") +elif [ "$PM" = "dnf" ] || [ "$PM" = "yum" ] || [ "$PM" = "zypper" ]; then + docker_pkg_version=$(rpm -q "$DOCKER_PKG" 2>/dev/null || echo "not installed") +elif [ "$PM" = "pacman" ]; then + docker_pkg_version=$(pacman -Q "$DOCKER_PKG" 2>/dev/null || echo "not installed") +elif [ "$PM" = "opkg" ]; then + docker_pkg_version=$(opkg info "$DOCKER_PKG" 2>/dev/null | grep '^Version:' | awk '{print $2}' || echo "not installed") +else + docker_pkg_version="unknown" +fi +log_and_display " $DOCKER_PKG: $docker_pkg_version" + +# Check lsof +if [ "$PM" = "apt-get" ]; then + lsof_version=$(dpkg -s "lsof" 2>/dev/null | grep '^Version:' | awk '{print $2}' || echo "not installed") +elif [ "$PM" = "dnf" ] || [ "$PM" = "yum" ] || [ "$PM" = "zypper" ]; then + lsof_version=$(rpm -q "lsof" 2>/dev/null || echo "not installed") +elif [ "$PM" = "pacman" ]; then + lsof_version=$(pacman -Q "lsof" 2>/dev/null || echo "not installed") +elif [ "$PM" = "opkg" ]; then + lsof_version=$(opkg info "lsof" 2>/dev/null | grep '^Version:' | awk '{print $2}' || echo "not installed") +else + lsof_version="unknown" +fi +log_and_display " lsof: $lsof_version" + +# ------------------------------------------------------------------------------ +# 3. Additional system information (hostnamectl / /proc/version) +# ------------------------------------------------------------------------------ +header "OS / Kernel Information" + +if command -v hostnamectl >/dev/null 2>&1; then + hostnamectl 2>/dev/null | grep -E 'Operating System:|Virtualization:|Kernel:|Architecture:' | sed 's/^[ \t]*//;s/:/: /' | while read line; do + log_and_display " $line" + done +else + log_and_display "Operating System: $(cat /proc/version 2>/dev/null || echo 'unknown')" +fi + +# CPU threads +CPU_THREADS=$(nproc 2>/dev/null || grep -c "^processor" /proc/cpuinfo 2>/dev/null || echo "unknown") +log_and_display " CPU threads: $CPU_THREADS" + +# ------------------------------------------------------------------------------ +# 4. Memory (RAM) check +# ------------------------------------------------------------------------------ +header "Memory Information" + +if command -v free >/dev/null 2>&1; then + # Remove extra spaces in header + free -h 2>/dev/null | tee -a "$LOG_FILE" || log_and_display " Error getting memory info" +elif command -v vmstat >/dev/null 2>&1; then + vmstat -S M -s 2>/dev/null | grep -iE 'total memory|total swap' | sed 's/ *//' | tee -a "$LOG_FILE" || log_and_display " Error getting memory info" +else + grep -iE 'MemTotal|SwapTotal' /proc/meminfo 2>/dev/null | sed 's/ \+/ /' | tee -a "$LOG_FILE" || log_and_display " Error getting memory info" +fi + +if command -v free >/dev/null 2>&1; then + log_and_display "" + log_and_display "Detailed Memory Info:" + free -h 2>/dev/null | awk 'NR==2{printf " Used: %s / %s (%.1f%%)\n", $3, $2, $3/$2*100}' 2>/dev/null | tee -a "$LOG_FILE" || log_and_display " Error calculating memory usage" +free -h 2>/dev/null | awk 'NR==3{printf " Swap: %s / %s (%.1f%%)\n", $3, $2, $2>0 ? $3/$2*100 : 0}' 2>/dev/null | tee -a "$LOG_FILE" || log_and_display " Error calculating swap usage" +fi + +# Disk usage +header "Disk Usage" +df -h 2>/dev/null | awk ' +BEGIN {print " Filesystem Size Used Avail Use% Mounted"} +NR>1 {printf " %-10s %5s %5s %5s %4s %s\n", $1, $2, $3, $4, $5, $6}' | tee -a "$LOG_FILE" || log_and_display " Error getting disk usage" + +# ------------------------------------------------------------------------------ +# 5. Current user and sudo check +# ------------------------------------------------------------------------------ +header "User Check" + +CUR_USER=$(whoami 2>/dev/null || echo ~ | sed 's/.*\///') +USER_GROUP=$(groups "$CUR_USER" 2>/dev/null || echo "") +USER_GOOD=0 + +log_and_display -n "Current user: $CUR_USER => " + +if [ "$CUR_USER" = "root" ]; then + log_and_display "passed.. (is root)" + USER_GOOD="r" # root +else + if echo "$USER_GROUP" | grep -qE '(^|[[:space:]])sudo($|[[:space:]])'; then + log_and_display "passed.. (in sudo group)" + USER_GOOD=1 + elif echo "$USER_GROUP" | grep -qE '(^|[[:space:]])wheel($|[[:space:]])'; then + log_and_display "passed.. (in wheel group)" + USER_GOOD=1 + elif echo "$USER_GROUP" | grep -qE '(^|[[:space:]])docker($|[[:space:]])'; then + log_and_display "failed.. (only in docker group)" + USER_GOOD="d" + else + log_and_display "failed.. (not a member of the sudo or wheel groups)" + USER_GOOD=0 + fi +fi + +# Check if password is required for sudo +if [ "$USER_GOOD" = "0" ] || [ "$USER_GOOD" = "d" ]; then + log_and_display -n "Passwd request: " + log_and_display "check skipped (not sudoer)" +else + if command -v sudo >/dev/null 2>&1; then + # Try sudo without password - more thorough check + PASSWD_REQUEST=$(sudo -K 2>&1 && sudo -nu $CUR_USER $PM $PM_VER_OPT 2>&1 >/dev/null && sudo -n $PM $PM_VER_OPT 2>&1 >/dev/null) + if [ -n "$PASSWD_REQUEST" ]; then + USER_GOOD=0 + log_and_display -n "Passwd request: " + log_and_display "failed.. ($PASSWD_REQUEST)" \ + | sed "s/$CUR_USER/User/g;s/$(hostname 2>/dev/null || echo 'Server')/Server/g;s/ user / /g" + else + log_and_display -n "Passwd request: " + log_and_display "passed.. (not required)" + fi + else + if [ "$USER_GOOD" = "r" ]; then + log_and_display -n "Passwd request: " + log_and_display "check skipped (sudo not installed, but root user)" + else + log_and_display "Warning! The sudo package must be pre-installed!" + USER_GOOD=0 + fi + fi +fi + +# Home directory check +log_and_display -n "Home dir: " +if cd ~ 2>/dev/null; then + log_and_display "passed.. (accessible)" +else + log_and_display "failed.. (not accessible)" +fi +log_and_display "Default shell: $SHELL" + +# ------------------------------------------------------------------------------ +# 6. Important components check (sudo, lsof, fuser, apparmor) +# ------------------------------------------------------------------------------ +header "Component Checks" + +log_and_display -n " sudo: " +if command -v sudo >/dev/null 2>&1; then + log_and_display "passed.. (installed)" +else + log_and_display "not installed" +fi + +log_and_display -n " lsof: " +if command -v lsof >/dev/null 2>&1; then + log_and_display "passed.. (installed)" +else + log_and_display "not installed" +fi + +log_and_display -n " fuser: " +if command -v fuser >/dev/null 2>&1; then + log_and_display "passed.. (installed)" +else + log_and_display "psmisc not installed" +fi + +log_and_display -n "apparmor: " +AA_ENABLED=$(cat /sys/module/apparmor/parameters/enabled 2>/dev/null || echo "N") +if [ "$AA_ENABLED" = "Y" ]; then + if command -v apparmor_parser >/dev/null 2>&1; then + log_and_display "passed.. (used)" + else + log_and_display "failed.. (installation required)" + fi +else + if command -v apparmor_parser >/dev/null 2>&1; then + log_and_display "passed.. (not used)" + else + log_and_display "passed.. (not required)" + fi +fi + +# ------------------------------------------------------------------------------ +# 7. SELinux check +# ------------------------------------------------------------------------------ +header "SELinux Check" + +if command -v getenforce >/dev/null 2>&1; then + SELINUX_STATUS=$(getenforce 2>/dev/null || echo "unknown") + if [ "$SELINUX_STATUS" = "Enforcing" ]; then + log_and_display "SELinux status: $SELINUX_STATUS (strict mode)" + elif [ "$SELINUX_STATUS" = "Permissive" ]; then + log_and_display "SELinux status: $SELINUX_STATUS (permissive mode)" + else + log_and_display "SELinux status: $SELINUX_STATUS (disabled)" + fi +else + log_and_display "SELinux: not found (or not applicable)" +fi + +# ------------------------------------------------------------------------------ +# 8. Docker + Docker/Podman service check +# ------------------------------------------------------------------------------ +header "Docker / Podman Status" +CHECK_CONTAINERS=0 + +if ! command -v docker >/dev/null 2>&1; then + log_and_display "Docker: $DOCKER_PKG not installed" +else + # If user is in sudoers, use sudo without password + if [ "$USER_GOOD" = "1" ]; then + SUD="sudo -n" + elif [ "$USER_GOOD" = "r" ]; then + SUD="" # root + else + SUD="" + fi + + DOCKER_VERSION=$($SUD docker -v 2>/dev/null || echo 'docker -v error') + log_and_display "Installed: $DOCKER_VERSION" + + # Check for podman + if echo "$DOCKER_VERSION" | grep -qi "podman"; then + log_and_display " WARNING: Podman detected - not supported at the moment!" + log_and_display " Podman (podman-docker) is not supported and is installed by mistake" + docker_service="podman.socket" + else + docker_service="docker.service" + fi + log_and_display " service: $docker_service" + + # Check status + if command -v systemctl >/dev/null 2>&1; then + docker_status=$(systemctl is-active "$docker_service" 2>/dev/null || echo "unknown") + docker_loading=$(systemctl is-enabled "$docker_service" 2>/dev/null || echo "unknown") + else + docker_status="unknown (systemctl not found)" + docker_loading="unknown" + fi + + if [ "$docker_status" = "active" ]; then + log_and_display " status: passed.. ($docker_status)" + CHECK_CONTAINERS=1 + else + log_and_display " status: incorrect.. ($docker_status)" + CHECK_CONTAINERS=0 + fi + + if [ "$docker_loading" = "enabled" ]; then + log_and_display " loading: good (startup $docker_loading)" + else + log_and_display " loading: bad (startup $docker_loading)" + fi +fi + +# ------------------------------------------------------------------------------ +# 9. Docker pull test + container check with improved Docker Hub verification +# ------------------------------------------------------------------------------ +header "Docker Hub: pull hello-world test" + +if [ "$CHECK_CONTAINERS" = "1" ] && [ "$USER_GOOD" != "0" ]; then + # First check Docker Hub availability + log_and_display "Checking Docker Hub connectivity..." + + # Try to execute docker pull with timeout + if timeout 30 $SUD docker pull docker.io/library/hello-world >/dev/null 2>&1; then + log_and_display "Docker Hub: available" + + # Start container for testing + if $SUD docker run --rm docker.io/library/hello-world >/dev/null 2>&1; then + log_and_display "Hello-world container: successfully started and completed" + else + log_and_display "Hello-world container: startup error" + fi + else + log_and_display "Docker Hub: unavailable or blocked (possibly exceeded download limit)" + log_and_display "Docker Hub has download limits, try again later" + fi + + log_and_display "" + total_cont=$($SUD docker ps -aq 2>/dev/null | wc -l || echo "0") + active_cont=$($SUD docker ps -q 2>/dev/null | wc -l || echo "0") + amnezia_cont=$($SUD docker ps -a 2>/dev/null | grep -c amnezia || echo "0") + + log_and_display "Containers check: Total $total_cont / Active $active_cont / Amnezia $amnezia_cont" + $SUD docker ps -a --format "{{.Names}} ({{.Image}}) ({{.Status}}) ({{.Ports}})" 2>/dev/null | grep amnezia || true + + # Peers check + if $SUD docker ps 2>/dev/null | grep -qE '\<(amnezia-awg|amnezia-wireguard)\>'; then + log_and_display "" + log_and_display "Peers check (beta):" + if $SUD docker ps 2>/dev/null | grep -q amnezia-awg; then + AMNEZIA_WG_CONTAINER=$($SUD docker ps 2>/dev/null | grep amnezia-awg | awk '{print $1}' | head -1) + if [ -n "$AMNEZIA_WG_CONTAINER" ]; then + WG_PEERS=$($SUD docker exec -it "$AMNEZIA_WG_CONTAINER" wg show 2>/dev/null | grep -c 'peer' || echo "0") + log_and_display "AmneziaWG peers: $WG_PEERS" + fi + fi + if $SUD docker ps 2>/dev/null | grep -q amnezia-wireguard; then + WIREGUARD_CONTAINER=$($SUD docker ps 2>/dev/null | grep amnezia-wireguard | awk '{print $1}' | head -1) + if [ -n "$WIREGUARD_CONTAINER" ]; then + WG_PEERS=$($SUD docker exec -it "$WIREGUARD_CONTAINER" wg show 2>/dev/null | grep -c 'peer' || echo "0") + log_and_display "WireGuard peers: $WG_PEERS" + fi + fi + fi +else + log_and_display "skipped.." +fi + +# ------------------------------------------------------------------------------ +# 10. Additional improvements +# ------------------------------------------------------------------------------ +# +# 10.1. CPU and memory load check (Load average, top processes) +# +header "CPU & Memory usage (top)" + +# Load average (last 1,5,15 minutes) +LOAD_AVG=$(uptime 2>/dev/null | awk -F'load average:' '{print $2}' || echo "unknown") +log_and_display "Load average: $LOAD_AVG" + +log_and_display "" +log_and_display "Top 5 processes by CPU:" +ps aux 2>/dev/null | sort -k3 -nr | head -n 6 | awk '{printf "%s %s %s %s %s\n", $1,$2,$3"%",$4"%",$11}' | column -t 2>/dev/null | tee -a "$LOG_FILE" || log_and_display " Error getting CPU processes" + +log_and_display "" +log_and_display "Top 5 processes by MEM:" +ps aux 2>/dev/null | sort -k4 -nr | head -n 6 | awk '{printf "%s %s %s %s %s\n", $1,$2,$3"%",$4"%",$11}' | column -t 2>/dev/null | tee -a "$LOG_FILE" || log_and_display " Error getting MEM processes" + +# 10.2. System logs check (latest critical messages) +header "Last 10 critical/error messages (journalctl)" + +if command -v journalctl >/dev/null 2>&1; then + journalctl -p 3 -n 10 --no-pager 2>/dev/null | tee -a "$LOG_FILE" || log_and_display " Error getting system logs" +else + log_and_display "journalctl not found (non-systemd system?)" +fi + +# 10.3. System package versions check (examples) + +# Open ports check +header "Network Ports Check" +if command -v netstat >/dev/null 2>&1; then + log_and_display "Listening ports:" + netstat -tlnp 2>/dev/null | grep LISTEN | head -10 | while read line; do + log_and_display " $line" + done +elif command -v ss >/dev/null 2>&1; then + log_and_display "Listening ports:" + ss -tlnp 2>/dev/null | head -10 | while read line; do + log_and_display " $line" + done +else + log_and_display "netstat/ss not found" +fi + +# SSH check +header "SSH Service Check" +if command -v systemctl >/dev/null 2>&1; then + ssh_status=$(systemctl is-active ssh 2>/dev/null || systemctl is-active sshd 2>/dev/null || echo "not found") + if [ "$ssh_status" = "active" ]; then + log_and_display "SSH service: $ssh_status" + else + log_and_display "SSH service: $ssh_status" + fi +else + log_and_display "systemctl not found" +fi + +# Time check +header "Time Synchronization" +if command -v timedatectl >/dev/null 2>&1; then + timedatectl status 2>/dev/null | grep -E "System clock|NTP service" | while read line; do + log_and_display " $line" + done +else + log_and_display " System time: $(date 2>/dev/null || echo 'unknown')" +fi + +# Kernel check +header "Kernel Information" +log_and_display "Kernel version: $(uname -r 2>/dev/null || echo 'unknown')" +log_and_display "Kernel architecture: $(uname -m 2>/dev/null || echo 'unknown')" +if [ -f /proc/cmdline ]; then + log_and_display "Kernel parameters:" + cat /proc/cmdline 2>/dev/null | tr ' ' '\n' | head -5 | while read param; do + log_and_display " $param" + done +fi + +# ------------------------------------------------------------------------------ +# Completion +# ------------------------------------------------------------------------------ +log_and_display "" +header "FINISH" +log_and_display "" +log_and_display "Diagnostics completed. Log saved to: $LOG_FILE" +log_and_display "" + +# Variable cleanup +pm="" && opt="" && docker_pkg="" && CUR_USER="" && USER_GOOD="" && USER_GROUP="" && PASSWD_REQUEST="" && CHECK_CONTAINERS="" && SUD="" && docker_service="" && docker_status="" && docker_loading="" \ No newline at end of file diff --git a/client/settings.cpp b/client/settings.cpp index bceb524c..883afa7e 100644 --- a/client/settings.cpp +++ b/client/settings.cpp @@ -541,12 +541,12 @@ QString Settings::getGatewayEndpoint() bool Settings::isDevGatewayEnv() { - return m_isDevGatewayEnv; + return value("Conf/devGatewayEnv", false).toBool(); } void Settings::toggleDevGatewayEnv(bool enabled) { - m_isDevGatewayEnv = enabled; + setValue("Conf/devGatewayEnv", enabled); } bool Settings::isHomeAdLabelVisible() @@ -578,3 +578,13 @@ void Settings::setAllowedDnsServers(const QStringList &servers) { setValue("Conf/allowedDnsServers", servers); } + +QStringList Settings::readNewsIds() const +{ + return value("News/readIds").toStringList(); +} + +void Settings::setReadNewsIds(const QStringList &ids) +{ + setValue("News/readIds", ids); +} diff --git a/client/settings.h b/client/settings.h index 7c244cd4..24950627 100644 --- a/client/settings.h +++ b/client/settings.h @@ -174,7 +174,7 @@ public: QLocale getAppLanguage() { - QString localeStr = m_settings.value("Conf/appLanguage").toString(); + QString localeStr = m_settings.value("Conf/appLanguage", QLocale::system().name()).toString(); return QLocale(localeStr); }; void setAppLanguage(QLocale locale) @@ -236,6 +236,9 @@ public: QStringList allowedDnsServers() const; void setAllowedDnsServers(const QStringList &servers); + QStringList readNewsIds() const; + void setReadNewsIds(const QStringList &ids); + signals: void saveLogsChanged(bool enabled); void screenshotsEnabledChanged(bool enabled); @@ -251,7 +254,6 @@ private: mutable SecureQSettings m_settings; QString m_gatewayEndpoint; - bool m_isDevGatewayEnv = false; }; #endif // SETTINGS_H diff --git a/client/translations/defaultvpn_ru_RU.ts b/client/translations/defaultvpn_ru_RU.ts index 89967a79..a4ab06d3 100644 --- a/client/translations/defaultvpn_ru_RU.ts +++ b/client/translations/defaultvpn_ru_RU.ts @@ -123,8 +123,8 @@ - <li>13 locations (with more coming soon)</li> - <li>13 локаций (их число будет расти)</li> + <li>20 locations (with more coming soon)</li> + <li>20 локаций (их число будет расти)</li> @@ -2067,7 +2067,7 @@ Thank you for staying with us! Сетевые адреса одного или нескольких серверов были обновлены. Пожалуйста, удалите старые конфигурацию и загрузите новые файлы - + Manage configuration files Управление файлами конфигурации @@ -2104,44 +2104,44 @@ Thank you for staying with us! Ключ для подключения - + Configuration Files Файлы конфигурации - + Active Devices Активные устройства - + Manage currently connected devices Управление подключенными устройствами - + Support Поддержка - + How to connect on another device Как подключить другие устройства - + Reload API config Перезагрузить конфигурацию API - + Reload API config? Перезагрузить конфигурацию API? - - - + + + Continue Продолжить @@ -2160,32 +2160,32 @@ Thank you for staying with us! Невозможно перзагрузить API конфигурацию при активном соединении - + Unlink this device Отвязать это устройство - + Are you sure you want to unlink this device? Вы уверены, что хотите отвязать это устройство? - + This will unlink the device from your subscription. You can reconnect it anytime by pressing "Reload API config" in subscription settings on device. Это отключит устройство от вашей подписки. Вы можете повторно подключить его в любое время, нажав "Перезагрузить конфигурацию API" в настройках подписки на устройстве. - + Cannot unlink device during active connection Невозможно отвязать устройство во время активного соединения - + Remove from application Удалить из приложения - + Remove from application? Удалить из приложения? @@ -2321,32 +2321,37 @@ Thank you for staying with us! Режим - + + Only "Apps from the list should not have access via VPN" mode is available on Windows + На Windows доступен только режим "Приложения из списка не должны работать через VPN" + + + Remove Удалить - + Continue Продолжить - + Cancel Отменить - + application name название приложения - + Open executable file Открыть исполняемый файл - + Executable files (*.*) Исполняемые файлы (*.*) @@ -3233,19 +3238,19 @@ Thank you for staying with us! Режим - + Remove Удалить - - + + Continue Продолжить - - + + Cancel Отменить @@ -3260,70 +3265,70 @@ Thank you for staying with us! Невозможно изменить настройки раздельного туннелирования во время активного соединения - + website or IP веб-сайт или IP - + Additional options Дополнительные настройки - + Import Импорт - + Save site list Сохранить список сайтов - + Save sites Сохранить сайты - - - + + + Sites files (*.json) Файлы сайтов (*.json) - + Clear site list Очистить список сайтов - + Clear site list? Очистить список сайтов? - + All sites will be removed from list. Все сайты будут удалены из списка. - + Import a list of sites Импортировать список с сайтами - + Replace site list Заменить список с сайтами - - + + Open sites file Открыть список с сайтами - + Add imported sites to existing ones Добавить импортированные сайты к существующим @@ -3331,32 +3336,32 @@ Thank you for staying with us! PageSetupWizardApiServiceInfo - + For the region Для региона - + Price Цена - + Work period Период работы - + Speed Скорость - + Features Особенности - + Connect Подключиться @@ -3364,12 +3369,12 @@ Thank you for staying with us! PageSetupWizardApiServicesList - + VPN by Amnezia VPN от Amnezia - + Choose a VPN service that suits your needs. Выберите VPN-сервис, который подходит именно вам. @@ -3646,38 +3651,38 @@ Thank you for staying with us! PageSetupWizardInstalling - + The server has already been added to the application Сервер уже был добавлен в приложение - + Amnezia has detected that your server is currently Amnezia обнаружила, что ваш сервер в настоящее время - + busy installing other software. Amnezia installation занят установкой других протоколов или сервисов. Установка Amnezia - + will pause until the server finishes installing other software будет приостановлена до тех пор, пока сервер не завершит установку другого ПО - + Installing Установка - + Cancel installation Отменить установку - + Usually it takes no more than 5 minutes Обычно это занимает не более 5 минут @@ -3813,28 +3818,28 @@ Thank you for staying with us! PageShare - + OpenVPN native format Оригинальный формат OpenVPN - + WireGuard native format Оригинальный формат WireGuard - + Connection Соединение - - + + Server Сервер - + Config revoked Конфигурация отозвана @@ -3884,141 +3889,141 @@ Thank you for staying with us! Для приложения DefaultVPN - + AmneziaWG native format Оригинальный формат AmneziaWG - + Shadowsocks native format Оригинальный формат Shadowsocks - + Cloak native format Оригинальный формат Cloak - + XRay native format Оригинальный формат XRay - + Share VPN Access Поделиться VPN - + Share full access to the server and VPN Поделиться полным доступом к серверу и VPN - + Use for your own devices, or share with those you trust to manage the server. Используйте для собственных устройств или передайте управление сервером тем, кому вы доверяете. - - + + Users Пользователи - + User name Имя пользователя - + Search Поиск - + Creation date: %1 Дата создания: %1 - + Latest handshake: %1 Последнее рукопожатие: %1 - + Data received: %1 Получено данных: %1 - + Data sent: %1 Отправлено данных: %1 - + Allowed IPs: %1 Разрешенные подсети: %1 - + Rename Переименовать - + Client name Имя клиента - + Save Сохранить - + Revoke Отозвать - + Revoke the config for a user - %1? Отозвать конфигурацию для пользователя - %1? - + The user will no longer be able to connect to your server. Пользователь больше не сможет подключаться к вашему серверу. - + Continue Продолжить - + Cancel Отменить - + Share VPN access without the ability to manage the server Поделиться доступом к VPN без возможности управления сервером - - + + Protocol Протокол - - + + Connection format Формат подключения - - + + Share Поделиться @@ -4026,51 +4031,38 @@ Thank you for staying with us! PageShareConnection - - - Connection to - Подключение к - - - - - File with connection settings to - Файл с настройками подключения к - - - + Share Поделиться - + Copy Скопировать - - + Save AmneziaVPN config Сохранить конфигурацию AmneziaVPN - + Copy config string Скопировать строку конфигурации - + Show connection settings Показать настройки подключения - - + + Copied Скопировано - + To read the QR code in the Amnezia app, select "Add server" → "I have data to connect" → "QR code, key or settings file" Для считывания QR-кода в приложении Amnezia выберите "Добавить сервер" → "У меня есть данные для подключения" → "Открыть файл конфигурации, ключ или QR-код" @@ -4078,45 +4070,45 @@ Thank you for staying with us! PageShareFullAccess - + Full access to the server and VPN Полный доступ к серверу и VPN - + We recommend that you use full access to the server only for your own additional devices. Мы рекомендуем использовать полный доступ к серверу только для собственных устройств. - + If you share full access with other people, they can remove and add protocols and services to the server, which will cause the VPN to work incorrectly for all users. Если вы поделитесь полным доступом с другими людьми, то они смогут удалять и добавлять протоколы и сервисы на сервер, что приведет к некорректной работе VPN для всех пользователей. - - + + Server Сервер - + Accessing Доступ - + File with accessing settings to Файл с настройками доступа к - + Share Поделиться - + Access error! Ошибка доступа! @@ -4124,17 +4116,17 @@ Thank you for staying with us! PageStart - + Logging was disabled after 14 days, log files were deleted Логирование было отключено по прошествии 14 дней, файлы логов были удалены. - + Settings restored from backup file Настройки восстановлены из бэкап файла - + Logging is enabled. Note that logs will be automaticallydisabled after 14 days, and all log files will be deleted. Логирование включено. Обратите внимание, что через 14 дней оно будет автоматически отключено, а все файлы логов будут удалены. @@ -5264,12 +5256,12 @@ For more detailed information, you can SettingsController - + All settings have been reset to default values Все настройки сброшены до значений по умолчанию - + Backup file is corrupted Файл резервной копии поврежден @@ -5398,7 +5390,7 @@ For more detailed information, you can TextFieldWithHeaderType - + The field can't be empty Поле не может быть пустым diff --git a/client/ui/controllers/api/apiConfigsController.cpp b/client/ui/controllers/api/apiConfigsController.cpp index ed467df0..314bc9dd 100644 --- a/client/ui/controllers/api/apiConfigsController.cpp +++ b/client/ui/controllers/api/apiConfigsController.cpp @@ -47,6 +47,8 @@ namespace constexpr char subscription[] = "subscription"; constexpr char endDate[] = "end_date"; + + constexpr char isConnectEvent[] = "is_connect_event"; } struct ProtocolData @@ -254,6 +256,23 @@ ApiConfigsController::ApiConfigsController(const QSharedPointer &s { } +bool ApiConfigsController::exportVpnKey(const QString &fileName) +{ + if (fileName.isEmpty()) { + emit errorOccurred(ErrorCode::PermissionsError); + return false; + } + + prepareVpnKeyExport(); + if (m_vpnKey.isEmpty()) { + emit errorOccurred(ErrorCode::ApiConfigEmptyError); + return false; + } + + SystemController::saveFile(fileName, m_vpnKey); + return true; +} + bool ApiConfigsController::exportNativeConfig(const QString &serverCountryCode, const QString &fileName) { if (fileName.isEmpty()) { @@ -335,6 +354,13 @@ void ApiConfigsController::prepareVpnKeyExport() auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject(); auto vpnKey = apiConfigObject.value(apiDefs::key::vpnKey).toString(); + if (vpnKey.isEmpty()) { + vpnKey = apiUtils::getPremiumV2VpnKey(serverConfigObject); + apiConfigObject.insert(apiDefs::key::vpnKey, vpnKey); + serverConfigObject.insert(configKey::apiConfig, apiConfigObject); + m_serversModel->editServer(serverConfigObject, m_serversModel->getProcessedServerIndex()); + } + m_vpnKey = vpnKey; vpnKey.replace("vpn://", ""); @@ -450,6 +476,10 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const QJsonObject apiPayload = gatewayRequestData.toJsonObject(); appendProtocolDataToApiPayload(gatewayRequestData.serviceProtocol, protocolData, apiPayload); + if (newCountryCode.isEmpty() && newCountryName.isEmpty() && !reloadServiceConfig) { + apiPayload.insert(configKey::isConnectEvent, true); + } + QByteArray responseBody; ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody); @@ -469,6 +499,7 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const newServerConfig.insert(configKey::apiConfig, newApiConfig); newServerConfig.insert(configKey::authData, gatewayRequestData.authData); + newServerConfig.insert(config_key::crc, serverConfig.value(config_key::crc)); if (serverConfig.value(config_key::nameOverriddenByUser).toBool()) { newServerConfig.insert(config_key::name, serverConfig.value(config_key::name)); @@ -532,7 +563,7 @@ bool ApiConfigsController::updateServiceFromTelegram(const int serverIndex) } } -bool ApiConfigsController::deactivateDevice() +bool ApiConfigsController::deactivateDevice(const bool isRemoveEvent) { auto serverIndex = m_serversModel->getProcessedServerIndex(); auto serverConfigObject = m_serversModel->getServerConfig(serverIndex); @@ -543,8 +574,12 @@ bool ApiConfigsController::deactivateDevice() } if (isSubscriptionExpired(apiConfigObject)) { - emit errorOccurred(ErrorCode::ApiSubscriptionExpiredError); - return false; + if (isRemoveEvent) { + return true; + } else { + emit errorOccurred(ErrorCode::ApiSubscriptionExpiredError); + return false; + } } GatewayRequestData gatewayRequestData { QSysInfo::productType(), diff --git a/client/ui/controllers/api/apiConfigsController.h b/client/ui/controllers/api/apiConfigsController.h index a04a142c..2a4cd400 100644 --- a/client/ui/controllers/api/apiConfigsController.h +++ b/client/ui/controllers/api/apiConfigsController.h @@ -21,7 +21,7 @@ public: public slots: bool exportNativeConfig(const QString &serverCountryCode, const QString &fileName); bool revokeNativeConfig(const QString &serverCountryCode); - // bool exportVpnKey(const QString &fileName); + bool exportVpnKey(const QString &fileName); void prepareVpnKeyExport(); void copyVpnKeyToClipboard(); @@ -30,7 +30,7 @@ public slots: bool updateServiceFromGateway(const int serverIndex, const QString &newCountryCode, const QString &newCountryName, bool reloadServiceConfig = false); bool updateServiceFromTelegram(const int serverIndex); - bool deactivateDevice(); + bool deactivateDevice(const bool isRemoveEvent); bool deactivateExternalDevice(const QString &uuid, const QString &serverCountryCode); bool isConfigValid(); diff --git a/client/ui/controllers/api/apiNewsController.cpp b/client/ui/controllers/api/apiNewsController.cpp new file mode 100644 index 00000000..45afacb1 --- /dev/null +++ b/client/ui/controllers/api/apiNewsController.cpp @@ -0,0 +1,65 @@ +#include "apiNewsController.h" + +#include "core/api/apiUtils.h" +#include +#include + +namespace +{ + namespace configKey + { + constexpr char userCountryCode[] = "user_country_code"; + constexpr char serviceType[] = "service_type"; + } +} + +ApiNewsController::ApiNewsController(const QSharedPointer &newsModel, const std::shared_ptr &settings, + const QSharedPointer &serversModel, QObject *parent) + : QObject(parent), m_newsModel(newsModel), m_settings(settings), m_serversModel(serversModel) +{ +} + +void ApiNewsController::fetchNews() +{ + if (m_serversModel.isNull()) { + qWarning() << "ServersModel is null, skip fetchNews"; + return; + } + const auto stacks = m_serversModel->gatewayStacks(); + if (stacks.isEmpty()) { + qDebug() << "No Gateway stacks, skip fetchNews"; + return; + } + GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs, + m_settings->isStrictKillSwitchEnabled()); + QByteArray responseBody; + QJsonObject payload; + payload.insert("locale", m_settings->getAppLanguage().name().split("_").first()); + + const QJsonObject stacksJson = stacks.toJson(); + if (stacksJson.contains(configKey::userCountryCode)) { + payload.insert(configKey::userCountryCode, stacksJson.value(configKey::userCountryCode)); + } + if (stacksJson.contains(configKey::serviceType)) { + payload.insert(configKey::serviceType, stacksJson.value(configKey::serviceType)); + } + + ErrorCode errorCode = gatewayController.post(QString("%1v1/news"), payload, responseBody); + if (errorCode != ErrorCode::NoError) { + emit errorOccurred(errorCode); + return; + } + + QJsonDocument doc = QJsonDocument::fromJson(responseBody); + QJsonArray newsArray; + if (doc.isArray()) { + newsArray = doc.array(); + } else if (doc.isObject()) { + QJsonObject obj = doc.object(); + if (obj.value("news").isArray()) { + newsArray = obj.value("news").toArray(); + } + } + + m_newsModel->updateModel(newsArray); +} diff --git a/client/ui/controllers/api/apiNewsController.h b/client/ui/controllers/api/apiNewsController.h new file mode 100644 index 00000000..e830c682 --- /dev/null +++ b/client/ui/controllers/api/apiNewsController.h @@ -0,0 +1,33 @@ +#ifndef APINEWSCONTROLLER_H +#define APINEWSCONTROLLER_H + +#include +#include +#include +#include + +#include "core/api/apiDefs.h" +#include "core/controllers/gatewayController.h" +#include "settings.h" +#include "ui/models/newsModel.h" +#include "ui/models/servers_model.h" + +class ApiNewsController : public QObject +{ + Q_OBJECT +public: + explicit ApiNewsController(const QSharedPointer &newsModel, const std::shared_ptr &settings, + const QSharedPointer &serversModel, QObject *parent = nullptr); + + Q_INVOKABLE void fetchNews(); + +signals: + void errorOccurred(ErrorCode errorCode); + +private: + QSharedPointer m_newsModel; + std::shared_ptr m_settings; + QSharedPointer m_serversModel; +}; + +#endif // APINEWSCONTROLLER_H diff --git a/client/ui/controllers/exportController.cpp b/client/ui/controllers/exportController.cpp index c487e3b2..853aacbb 100644 --- a/client/ui/controllers/exportController.cpp +++ b/client/ui/controllers/exportController.cpp @@ -297,10 +297,11 @@ void ExportController::revokeConfig(const int row, const DockerContainer contain { QSharedPointer serverController(new ServerController(m_settings)); ErrorCode errorCode = - m_clientManagementModel->revokeClient(row, container, credentials, m_serversModel->getProcessedServerIndex(), serverController); + m_clientManagementModel->revokeClient(row, container, credentials, m_serversModel->getProcessedServerIndex(), serverController); if (errorCode != ErrorCode::NoError) { emit exportErrorOccurred(errorCode); } + emit revokeConfigCompleted(); } void ExportController::renameClient(const int row, const QString &clientName, const DockerContainer container, ServerCredentials credentials) diff --git a/client/ui/controllers/exportController.h b/client/ui/controllers/exportController.h index 5fb3e6b3..72f08b3e 100644 --- a/client/ui/controllers/exportController.h +++ b/client/ui/controllers/exportController.h @@ -42,6 +42,7 @@ public slots: signals: void generateConfig(int type); + void revokeConfigCompleted(); void exportErrorOccurred(const QString &errorMessage); void exportErrorOccurred(ErrorCode errorCode); diff --git a/client/ui/controllers/importController.cpp b/client/ui/controllers/importController.cpp index 33d95e8c..473ca27a 100644 --- a/client/ui/controllers/importController.cpp +++ b/client/ui/controllers/importController.cpp @@ -274,7 +274,7 @@ void ImportController::processNativeWireGuardConfig() auto serverProtocolConfig = container.value(ContainerProps::containerTypeToString(DockerContainer::WireGuard)).toObject(); auto clientProtocolConfig = QJsonDocument::fromJson(serverProtocolConfig.value(config_key::last_config).toString().toUtf8()).object(); - QString junkPacketCount = QString::number(QRandomGenerator::global()->bounded(2, 5)); + QString junkPacketCount = QString::number(QRandomGenerator::global()->bounded(4, 7)); QString junkPacketMinSize = QString::number(10); QString junkPacketMaxSize = QString::number(50); clientProtocolConfig[config_key::junkPacketCount] = junkPacketCount; diff --git a/client/ui/controllers/installController.cpp b/client/ui/controllers/installController.cpp index ec22fc12..7d2699e1 100755 --- a/client/ui/controllers/installController.cpp +++ b/client/ui/controllers/installController.cpp @@ -73,7 +73,7 @@ void InstallController::install(DockerContainer container, int port, TransportPr containerConfig.insert(config_key::transport_proto, ProtocolProps::transportProtoToString(transportProto, protocol)); if (container == DockerContainer::Awg) { - QString junkPacketCount = QString::number(QRandomGenerator::global()->bounded(2, 5)); + QString junkPacketCount = QString::number(QRandomGenerator::global()->bounded(4, 7)); QString junkPacketMinSize = QString::number(10); QString junkPacketMaxSize = QString::number(50); diff --git a/client/ui/controllers/pageController.cpp b/client/ui/controllers/pageController.cpp index 1b6634ac..23a76bf9 100644 --- a/client/ui/controllers/pageController.cpp +++ b/client/ui/controllers/pageController.cpp @@ -169,7 +169,7 @@ void PageController::onShowErrorMessage(ErrorCode errorCode) { const auto fullErrorMessage = errorString(errorCode); const auto errorMessage = fullErrorMessage.mid(fullErrorMessage.indexOf(". ") + 1); // remove ErrorCode %1. - const auto errorUrl = QStringLiteral("https://docs.amnezia.org/troubleshooting/error-codes/#error-%1-%2").arg(static_cast(errorCode)).arg(utils::enumToString(errorCode).toLower()); + const auto errorUrl = QStringLiteral("troubleshooting/error-codes/#error-%1-%2").arg(static_cast(errorCode)).arg(utils::enumToString(errorCode).toLower()); const auto fullMessage = QStringLiteral("ErrorCode: %2. %3").arg(errorUrl).arg(static_cast(errorCode)).arg(errorMessage); emit showErrorMessage(fullMessage); diff --git a/client/ui/controllers/pageController.h b/client/ui/controllers/pageController.h index 4f85f925..25e0020a 100644 --- a/client/ui/controllers/pageController.h +++ b/client/ui/controllers/pageController.h @@ -27,6 +27,8 @@ namespace PageLoader PageSettingsConnection, PageSettingsDns, PageSettingsApplication, + PageSettingsNewsNotifications, + PageSettingsNewsDetail, PageSettingsBackup, PageSettingsAbout, PageSettingsLogging, diff --git a/client/ui/controllers/settingsController.cpp b/client/ui/controllers/settingsController.cpp index 5536151e..7f53fde2 100644 --- a/client/ui/controllers/settingsController.cpp +++ b/client/ui/controllers/settingsController.cpp @@ -5,6 +5,7 @@ #include "logger.h" #include "systemController.h" #include "ui/qautostart.h" +#include "amnezia_application.h" #include "version.h" #ifdef Q_OS_ANDROID #include "platforms/android/android_controller.h" @@ -33,6 +34,9 @@ SettingsController::SettingsController(const QSharedPointer &serve #ifdef Q_OS_ANDROID connect(AndroidController::instance(), &AndroidController::notificationStateChanged, this, &SettingsController::onNotificationStateChanged); #endif + + m_isDevModeEnabled = m_settings->isDevGatewayEnv(); + toggleDevGatewayEnv(m_isDevModeEnabled); } QString getPlatformName() @@ -139,6 +143,10 @@ void SettingsController::clearLogs() Logger::clearLogs(false); Logger::clearServiceLogs(); #endif + + qInfo().noquote() << QString("Started %1 version %2 %3").arg(APPLICATION_NAME, APP_VERSION, GIT_COMMIT_HASH); + qInfo().noquote() << QString("%1 (%2)").arg(QSysInfo::prettyProductName(), QSysInfo::currentCpuArchitecture()); + qInfo().noquote() << QString("SSL backend: %1").arg(QSslSocket::sslLibraryVersionString()); } void SettingsController::backupAppConfig(const QString &fileName) @@ -151,6 +159,7 @@ void SettingsController::backupAppConfig(const QString &fileName) config["Conf/autoStart"] = Autostart::isAutostart(); config["Conf/killSwitchEnabled"] = isKillSwitchEnabled(); config["Conf/strictKillSwitchEnabled"] = isStrictKillSwitchEnabled(); + config["Conf/useAmneziaDns"] = isAmneziaDnsEnabled(); SystemController::saveFile(fileName, QJsonDocument(config).toJson()); } @@ -186,7 +195,8 @@ void SettingsController::restoreAppConfigFromData(const QByteArray &data) #if defined(Q_OS_WINDOWS) || defined(Q_OS_ANDROID) int appSplitTunnelingRouteMode = newConfigData.value("Conf/appsRouteMode").toInt(); - bool appSplittunnelingEnabled = newConfigData.value("Conf/appsSplitTunnelingEnabled").toString().toLower() == "true"; + bool appSplittunnelingEnabled = + newConfigData.value("Conf/appsSplitTunnelingEnabled").toVariant().toString().toLower() == "true"; m_appSplitTunnelingModel->setRouteMode(appSplitTunnelingRouteMode); #if defined(Q_OS_WINDOWS) @@ -198,12 +208,13 @@ void SettingsController::restoreAppConfigFromData(const QByteArray &data) m_appSplitTunnelingModel->clearAppsList(); } } - + m_appSplitTunnelingModel->toggleSplitTunneling(appSplittunnelingEnabled); #endif int siteSplitTunnelingRouteMode = newConfigData.value("Conf/routeMode").toInt(); - bool siteSplittunnelingEnabled = newConfigData.value("Conf/sitesSplitTunnelingEnabled").toString().toLower() == "true"; + bool siteSplittunnelingEnabled = + newConfigData.value("Conf/sitesSplitTunnelingEnabled").toVariant().toString().toLower() == "true"; m_sitesModel->setRouteMode(siteSplitTunnelingRouteMode); m_sitesModel->toggleSplitTunneling(siteSplittunnelingEnabled); @@ -214,6 +225,11 @@ void SettingsController::restoreAppConfigFromData(const QByteArray &data) m_settings->setStrictKillSwitchEnabled(false); #endif + bool amneziaDnsEnabled = newConfigData.contains("Conf/useAmneziaDns") + ? newConfigData.value("Conf/useAmneziaDns").toBool() + : m_settings->useAmneziaDns(); + emit amneziaDnsToggled(amneziaDnsEnabled); + emit restoreBackupFinished(); } else { emit changeSettingsErrorOccurred(tr("Backup file is corrupted")); @@ -264,6 +280,9 @@ bool SettingsController::isAutoStartEnabled() void SettingsController::toggleAutoStart(bool enable) { Autostart::setAutostart(enable); + if (!enable) { + toggleStartMinimized(false); + } } bool SettingsController::isStartMinimizedEnabled() @@ -274,6 +293,7 @@ bool SettingsController::isStartMinimizedEnabled() void SettingsController::toggleStartMinimized(bool enable) { m_settings->setStartMinimized(enable); + emit startMinimizedChanged(); } bool SettingsController::isScreenshotsEnabled() diff --git a/client/ui/controllers/settingsController.h b/client/ui/controllers/settingsController.h index a9e686e6..a5c65642 100644 --- a/client/ui/controllers/settingsController.h +++ b/client/ui/controllers/settingsController.h @@ -32,6 +32,7 @@ public: Q_PROPERTY(bool isDevGatewayEnv READ isDevGatewayEnv WRITE toggleDevGatewayEnv NOTIFY devGatewayEnvChanged) Q_PROPERTY(bool isHomeAdLabelVisible READ isHomeAdLabelVisible NOTIFY isHomeAdLabelVisibleChanged) + Q_PROPERTY(bool startMinimized READ isStartMinimizedEnabled NOTIFY startMinimizedChanged) public slots: void toggleAmneziaDns(bool enable); @@ -125,6 +126,7 @@ signals: void devGatewayEnvChanged(bool enabled); void isHomeAdLabelVisibleChanged(bool visible); + void startMinimizedChanged(); private: QSharedPointer m_serversModel; diff --git a/client/ui/models/clientManagementModel.cpp b/client/ui/models/clientManagementModel.cpp index f07eae71..ca43e1a9 100644 --- a/client/ui/models/clientManagementModel.cpp +++ b/client/ui/models/clientManagementModel.cpp @@ -497,7 +497,8 @@ ErrorCode ClientManagementModel::appendClient(const QString &clientId, const QSt return error; } -ErrorCode ClientManagementModel::renameClient(const int row, const QString &clientName, const DockerContainer container, +ErrorCode ClientManagementModel::renameClient(const int row, const QString &clientName, + const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController, bool addTimeStamp) { @@ -529,7 +530,8 @@ ErrorCode ClientManagementModel::renameClient(const int row, const QString &clie return error; } -ErrorCode ClientManagementModel::revokeClient(const int row, const DockerContainer container, const ServerCredentials &credentials, +ErrorCode ClientManagementModel::revokeClient(const int row, const DockerContainer container, + const ServerCredentials &credentials, const int serverIndex, const QSharedPointer &serverController) { ErrorCode errorCode = ErrorCode::NoError; diff --git a/client/ui/models/clientManagementModel.h b/client/ui/models/clientManagementModel.h index 989120a9..2880a6d1 100644 --- a/client/ui/models/clientManagementModel.h +++ b/client/ui/models/clientManagementModel.h @@ -44,10 +44,10 @@ public slots: const ServerCredentials &credentials, const QSharedPointer &serverController); ErrorCode appendClient(const QString &clientId, const QString &clientName, const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController); - ErrorCode renameClient(const int row, const QString &userName, const DockerContainer container, const ServerCredentials &credentials, - const QSharedPointer &serverController, bool addTimeStamp = false); - ErrorCode revokeClient(const int index, const DockerContainer container, const ServerCredentials &credentials, const int serverIndex, - const QSharedPointer &serverController); + ErrorCode renameClient(const int row, const QString &userName, const DockerContainer container, + const ServerCredentials &credentials, const QSharedPointer &serverController, bool addTimeStamp = false); + ErrorCode revokeClient(const int index, const DockerContainer container, const ServerCredentials &credentials, + const int serverIndex, const QSharedPointer &serverController); ErrorCode revokeClient(const QJsonObject &containerConfig, const DockerContainer container, const ServerCredentials &credentials, const int serverIndex, const QSharedPointer &serverController); @@ -60,6 +60,8 @@ signals: private: bool isClientExists(const QString &clientId); + int clientIndexById(const QString &clientId); + void migration(const QByteArray &clientsTableString); ErrorCode revokeOpenVpn(const int row, const DockerContainer container, const ServerCredentials &credentials, const int serverIndex, diff --git a/client/ui/models/newsModel.cpp b/client/ui/models/newsModel.cpp new file mode 100644 index 00000000..408c1312 --- /dev/null +++ b/client/ui/models/newsModel.cpp @@ -0,0 +1,130 @@ +#include "ui/models/newsModel.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +NewsModel::NewsModel(const std::shared_ptr &settings, QObject *parent) : QAbstractListModel(parent), m_settings(settings) +{ + loadReadIds(); +} + +int NewsModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return m_items.size(); +} + +QVariant NewsModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= m_items.size()) + return QVariant(); + + const NewsItem &item = m_items.at(index.row()); + switch (role) { + case IdRole: return item.id; + case TitleRole: return item.title; + case ContentRole: return item.content; + case TimestampRole: return item.timestamp.toString(Qt::ISODate); + case IsReadRole: return item.read; + case IsProcessedRole: return index.row() == m_processedIndex; + default: return QVariant(); + } +} + +QHash NewsModel::roleNames() const +{ + QHash roles; + roles[IdRole] = "id"; + roles[TitleRole] = "title"; + roles[ContentRole] = "content"; + roles[TimestampRole] = "timestamp"; + roles[IsReadRole] = "read"; + roles[IsProcessedRole] = "isProcessed"; + return roles; +} + +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(); + } +} + +int NewsModel::processedIndex() const +{ + return m_processedIndex; +} + +void NewsModel::setProcessedIndex(int index) +{ + if (index < 0 || index >= m_items.size() || m_processedIndex == index) + return; + m_processedIndex = index; + emit processedIndexChanged(index); +} + +void NewsModel::updateModel(const QJsonArray &serverItems) +{ + QSet existingIds; + for (const NewsItem &item : m_items) { + existingIds.insert(item.id); + } + + QList newItems; + for (const QJsonValue &value : serverItems) { + if (!value.isObject()) + continue; + const QJsonObject obj = value.toObject(); + QString id = obj.value("id").toString(); + + if (!existingIds.contains(id)) { + NewsItem item; + item.id = id; + item.title = obj.value("title").toString(); + item.content = obj.value("content").toString(); + item.timestamp = QDateTime::fromString(obj.value("timestamp").toString(), Qt::ISODate); + item.read = m_readIds.contains(id); + newItems.append(item); + existingIds.insert(id); + } + } + + beginResetModel(); + m_items.append(newItems); + std::sort(m_items.begin(), m_items.end(), [](const NewsItem &a, const NewsItem &b) { return a.timestamp > b.timestamp; }); + endResetModel(); + emit hasUnreadChanged(); +} + +bool NewsModel::hasUnread() const +{ + for (const NewsItem &item : m_items) { + if (!item.read) + return true; + } + return false; +} + +void NewsModel::loadReadIds() +{ + QStringList ids = m_settings->readNewsIds(); + m_readIds = QSet(ids.begin(), ids.end()); +} + +void NewsModel::saveReadIds() const +{ + m_settings->setReadNewsIds(QStringList(m_readIds.begin(), m_readIds.end())); +} diff --git a/client/ui/models/newsModel.h b/client/ui/models/newsModel.h new file mode 100644 index 00000000..6188a981 --- /dev/null +++ b/client/ui/models/newsModel.h @@ -0,0 +1,62 @@ +#ifndef NEWSMODEL_H +#define NEWSMODEL_H + +#include "settings.h" +#include +#include +#include +#include +#include +#include +#include + +struct NewsItem +{ + QString id; + QString title; + QString content; + QDateTime timestamp; + bool read; +}; + +class NewsModel : public QAbstractListModel +{ + Q_OBJECT +public: + enum Roles { + IdRole = Qt::UserRole + 1, + TitleRole, + ContentRole, + TimestampRole, + IsReadRole, + IsProcessedRole + }; + explicit NewsModel(const std::shared_ptr &settings, QObject *parent = nullptr); + Q_INVOKABLE void markAsRead(int index); + + 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); + bool hasUnread() const; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash roleNames() const override; + +signals: + void processedIndexChanged(int index); + void hasUnreadChanged(); + +private: + QVector m_items; + int m_processedIndex = -1; + std::shared_ptr m_settings; + QSet m_readIds; + void loadReadIds(); + void saveReadIds() const; +}; + +#endif // NEWSMODEL_H diff --git a/client/ui/models/servers_model.cpp b/client/ui/models/servers_model.cpp index 3926f8ed..39749e87 100644 --- a/client/ui/models/servers_model.cpp +++ b/client/ui/models/servers_model.cpp @@ -44,6 +44,8 @@ ServersModel::ServersModel(std::shared_ptr settings, QObject *parent) connect(this, &ServersModel::processedServerIndexChanged, this, &ServersModel::processedServerChanged); connect(this, &ServersModel::dataChanged, this, &ServersModel::processedServerChanged); + + connect(this, &QAbstractItemModel::modelReset, this, &ServersModel::recomputeGatewayStacks); } int ServersModel::rowCount(const QModelIndex &parent) const @@ -173,6 +175,7 @@ void ServersModel::resetModel() m_servers = m_settings->serversArray(); m_defaultServerIndex = m_settings->defaultServerIndex(); m_processedServerIndex = m_defaultServerIndex; + m_isAmneziaDnsEnabled = m_settings->useAmneziaDns(); endResetModel(); emit defaultServerIndexChanged(m_defaultServerIndex); } @@ -374,7 +377,6 @@ QHash ServersModel::roleNames() const { QHash roles; - roles[NameRole] = "serverName"; roles[NameRole] = "name"; roles[ServerDescriptionRole] = "serverDescription"; roles[CollapsedServerDescriptionRole] = "collapsedServerDescription"; @@ -755,6 +757,68 @@ bool ServersModel::isServerFromApi(const int serverIndex) return data(serverIndex, IsServerFromTelegramApiRole).toBool() || data(serverIndex, IsServerFromGatewayApiRole).toBool(); } +bool ServersModel::hasServersFromGatewayApi() +{ + return !m_gatewayStacks.isEmpty(); +} + +bool ServersModel::GatewayStacks::operator==(const GatewayStacks &other) const +{ + return userCountryCodes == other.userCountryCodes && serviceTypes == other.serviceTypes; +} + +QJsonObject ServersModel::GatewayStacks::toJson() const +{ + QJsonObject obj; + if (!userCountryCodes.isEmpty()) { + obj.insert(configKey::userCountryCode, QJsonArray::fromStringList(userCountryCodes.values())); + } + if (!serviceTypes.isEmpty()) { + obj.insert(configKey::serviceType, QJsonArray::fromStringList(serviceTypes.values())); + } + return obj; +} + +void ServersModel::recomputeGatewayStacks() +{ + const bool wasEmpty = m_gatewayStacks.isEmpty(); + GatewayStacks computed; + bool hasNewTags = false; + + for (int i = 0; i < m_servers.count(); ++i) { + if (data(i, IsServerFromGatewayApiRole).toBool()) { + const QJsonObject server = m_servers.at(i).toObject(); + const QJsonObject apiConfig = server.value(configKey::apiConfig).toObject(); + + const QString userCountryCode = apiConfig.value(configKey::userCountryCode).toString(); + const QString serviceType = apiConfig.value(configKey::serviceType).toString(); + + if (!userCountryCode.isEmpty()) { + if (!m_gatewayStacks.userCountryCodes.contains(userCountryCode)) { + hasNewTags = true; + } + computed.userCountryCodes.insert(userCountryCode); + } + + if (!serviceType.isEmpty()) { + if (!m_gatewayStacks.serviceTypes.contains(serviceType)) { + hasNewTags = true; + } + computed.serviceTypes.insert(serviceType); + } + } + } + + m_gatewayStacks = std::move(computed); + if (hasNewTags) { + emit gatewayStacksExpanded(); + } + + if (wasEmpty != m_gatewayStacks.isEmpty()) { + emit hasServersFromGatewayApiChanged(); + } +} + bool ServersModel::isApiKeyExpired(const int serverIndex) { auto serverConfig = m_servers.at(serverIndex).toObject(); diff --git a/client/ui/models/servers_model.h b/client/ui/models/servers_model.h index c36b6534..973b5418 100644 --- a/client/ui/models/servers_model.h +++ b/client/ui/models/servers_model.h @@ -10,6 +10,16 @@ class ServersModel : public QAbstractListModel { Q_OBJECT public: + struct GatewayStacks + { + QSet userCountryCodes; + QSet serviceTypes; + + bool isEmpty() const { return userCountryCodes.isEmpty() && serviceTypes.isEmpty(); } + bool operator==(const GatewayStacks &other) const; + QJsonObject toJson() const; + }; + enum Roles { NameRole = Qt::UserRole + 1, ServerDescriptionRole, @@ -52,6 +62,8 @@ public: void resetModel(); + GatewayStacks gatewayStacks() const { return m_gatewayStacks; } + Q_PROPERTY(int defaultIndex READ getDefaultServerIndex WRITE setDefaultServerIndex NOTIFY defaultServerIndexChanged) Q_PROPERTY(QString defaultServerName READ getDefaultServerName NOTIFY defaultServerNameChanged) Q_PROPERTY(QString defaultServerDefaultContainerName READ getDefaultServerDefaultContainerName NOTIFY defaultServerDefaultContainerChanged) @@ -62,6 +74,8 @@ public: defaultServerDefaultContainerChanged) Q_PROPERTY(bool isDefaultServerFromApi READ isDefaultServerFromApi NOTIFY defaultServerIndexChanged) + Q_PROPERTY(bool hasServersFromGatewayApi READ hasServersFromGatewayApi NOTIFY hasServersFromGatewayApiChanged) + Q_PROPERTY(int processedIndex READ getProcessedServerIndex WRITE setProcessedServerIndex NOTIFY processedServerIndexChanged) Q_PROPERTY(bool processedServerIsPremium READ processedServerIsPremium NOTIFY processedServerChanged) @@ -82,6 +96,8 @@ public slots: bool isDefaultServerHasWriteAccess(); bool hasServerWithWriteAccess(); + bool hasServersFromGatewayApi(); + const int getServersCount(); void setProcessedServerIndex(const int index); @@ -147,6 +163,9 @@ signals: void updateApiCountryModel(); void updateApiServicesModel(); + void hasServersFromGatewayApiChanged(); + void gatewayStacksExpanded(); + private: ServerCredentials serverCredentials(int index) const; @@ -167,6 +186,9 @@ private: int m_processedServerIndex; bool m_isAmneziaDnsEnabled = m_settings->useAmneziaDns(); + + GatewayStacks m_gatewayStacks; + void recomputeGatewayStacks(); }; #endif // SERVERSMODEL_H diff --git a/client/ui/qml/Components/ApiPremV1MigrationDrawer.qml b/client/ui/qml/Components/ApiPremV1MigrationDrawer.qml index 21aa78a0..3877600f 100644 --- a/client/ui/qml/Components/ApiPremV1MigrationDrawer.qml +++ b/client/ui/qml/Components/ApiPremV1MigrationDrawer.qml @@ -73,7 +73,7 @@ DrawerType2 { var str = qsTr("We'll preserve all remaining days of your current subscription and give you an extra month as a thank you. ") str += qsTr("This new subscription type will be actively developed with more locations and features added regularly. Currently available:") str += "
    " - str += qsTr("
  • 13 locations (with more coming soon)
  • ") + str += qsTr("
  • 20 locations (with more coming soon)
  • ") str += qsTr("
  • Easier switching between countries in the app
  • ") str += qsTr("
  • Personal dashboard to manage your subscription
  • ") str += "
" diff --git a/client/ui/qml/Controls2/ContextMenuType.qml b/client/ui/qml/Controls2/ContextMenuType.qml index cb32e311..4f7936af 100644 --- a/client/ui/qml/Controls2/ContextMenuType.qml +++ b/client/ui/qml/Controls2/ContextMenuType.qml @@ -18,8 +18,8 @@ Menu { } MenuItem { text: qsTr("&Paste") - // Fix calling paste from clipboard when launching app on android - enabled: Qt.platform.os === "android" ? true : textObj.canPaste + // Fix calling paste from clipboard when launching app on android/ios + enabled: (Qt.platform.os === "android" || Qt.platform.os === "ios") ? true : textObj.canPaste onTriggered: textObj.paste() } diff --git a/client/ui/qml/Controls2/PopupType.qml b/client/ui/qml/Controls2/PopupType.qml index dfb6f273..d8b79bca 100644 --- a/client/ui/qml/Controls2/PopupType.qml +++ b/client/ui/qml/Controls2/PopupType.qml @@ -72,7 +72,7 @@ Popup { Layout.fillWidth: true onLinkActivated: function(link) { - Qt.openUrlExternally(link) + Qt.openUrlExternally(LanguageModel.getCurrentDocsUrl(link)) } text: root.text diff --git a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml index 2eef235f..fdb7a3ae 100644 --- a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml +++ b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml @@ -1,220 +1,236 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts - -import Style 1.0 - -import "TextTypes" - -Item { - id: root - - property string headerText - property string headerTextDisabledColor: AmneziaStyle.color.charcoalGray - property string headerTextColor: AmneziaStyle.color.mutedGray - - property alias errorText: errorField.text - property bool checkEmptyText: false - property bool rightButtonClickedOnEnter: false - - property string buttonText - property string buttonImageSource - property var clickedFunc - - property alias textField: textField - property string textFieldTextColor: AmneziaStyle.color.paleGray - property string textFieldTextDisabledColor: AmneziaStyle.color.mutedGray - - property bool textFieldEditable: true - - property string borderColor: AmneziaStyle.color.slateGray - property string borderFocusedColor: AmneziaStyle.color.paleGray - - property string backgroundColor: AmneziaStyle.color.onyxBlack - property string backgroundDisabledColor: AmneziaStyle.color.transparent - property string bgBorderHoveredColor: AmneziaStyle.color.charcoalGray - - implicitWidth: content.implicitWidth - implicitHeight: content.implicitHeight - - ColumnLayout { - id: content - anchors.fill: parent - - Rectangle { - id: backgroud - Layout.fillWidth: true - Layout.preferredHeight: input.implicitHeight - color: root.enabled ? root.backgroundColor : root.backgroundDisabledColor - radius: 16 - border.color: getBackgroundBorderColor(root.borderColor) - border.width: 1 - - Behavior on border.color { - PropertyAnimation { duration: 200 } - } - - RowLayout { - id: input - anchors.fill: backgroud - ColumnLayout { - Layout.margins: 16 - LabelTextType { - text: root.headerText - color: root.enabled ? root.headerTextColor : root.headerTextDisabledColor - - visible: text !== "" - - Layout.fillWidth: true - } - - TextField { - id: textField - - property bool isFocusable: true - - Keys.onTabPressed: { - FocusController.nextKeyTabItem() - } - - Keys.onBacktabPressed: { - FocusController.previousKeyTabItem() - } - - enabled: root.textFieldEditable - color: root.enabled ? root.textFieldTextColor : root.textFieldTextDisabledColor - - inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhSensitiveData | Qt.ImhNoPredictiveText - - placeholderTextColor: AmneziaStyle.color.charcoalGray - - selectionColor: AmneziaStyle.color.richBrown - selectedTextColor: AmneziaStyle.color.paleGray - - font.pixelSize: 16 - font.weight: 400 - font.family: "PT Root UI VF" - - height: 24 - Layout.fillWidth: true - - topPadding: 0 - rightPadding: 0 - leftPadding: 0 - bottomPadding: 0 - - background: Rectangle { - anchors.fill: parent - color: root.enabled ? root.backgroundColor : root.backgroundDisabledColor - } - - onTextChanged: { - root.errorText = "" - } - - onActiveFocusChanged: { - if (root.checkEmptyText && text === "") { - root.errorText = qsTr("The field can't be empty") - } - } - - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.RightButton - onClicked: contextMenu.open() - enabled: true - } - - ContextMenuType { - id: contextMenu - textObj: textField - } - - onFocusChanged: { - backgroud.border.color = getBackgroundBorderColor(root.borderColor) - } - } - } - } - } - - SmallTextType { - id: errorField - - text: root.errorText - visible: root.errorText !== "" - color: AmneziaStyle.color.vibrantRed - - Layout.fillWidth: true - } - } - - MouseArea { - anchors.fill: root - cursorShape: Qt.IBeamCursor - - hoverEnabled: true - - onPressed: function(mouse) { - textField.forceActiveFocus() - mouse.accepted = false - - backgroud.border.color = getBackgroundBorderColor(root.borderColor) - } - - onEntered: { - backgroud.border.color = getBackgroundBorderColor(bgBorderHoveredColor) - } - - - onExited: { - backgroud.border.color = getBackgroundBorderColor(root.borderColor) - } - } - - BasicButtonType { - visible: (root.buttonText !== "") || (root.buttonImageSource !== "") - - focusPolicy: Qt.NoFocus - text: root.buttonText - leftImageSource: root.buttonImageSource - - anchors.top: content.top - anchors.bottom: content.bottom - anchors.right: content.right - - height: content.implicitHeight - width: content.implicitHeight - squareLeftSide: true - - clickedFunc: function() { - if (root.clickedFunc && typeof root.clickedFunc === "function") { - root.clickedFunc() - } - } - } - - function getBackgroundBorderColor(noneFocusedColor) { - return textField.focus ? root.borderFocusedColor : noneFocusedColor - } - - Keys.onEnterPressed: { - if (root.rightButtonClickedOnEnter && root.clickedFunc && typeof root.clickedFunc === "function") { - clickedFunc() - } - - // if (KeyNavigation.tab) { - // KeyNavigation.tab.forceActiveFocus(); - // } - } - - Keys.onReturnPressed: { - if (root.rightButtonClickedOnEnter &&root.clickedFunc && typeof root.clickedFunc === "function") { - clickedFunc() - } - - // if (KeyNavigation.tab) { - // KeyNavigation.tab.forceActiveFocus(); - // } - } -} +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import Style 1.0 + +import "TextTypes" + +Item { + id: root + + property string headerText + property string headerTextDisabledColor: AmneziaStyle.color.charcoalGray + property string headerTextColor: AmneziaStyle.color.mutedGray + + property alias errorText: errorField.text + property bool checkEmptyText: false + property bool rightButtonClickedOnEnter: false + + property string buttonText + property string buttonImageSource + property var clickedFunc + + property alias textField: textField + property string textFieldTextColor: AmneziaStyle.color.paleGray + property string textFieldTextDisabledColor: AmneziaStyle.color.mutedGray + + property bool textFieldEditable: true + + property string borderColor: AmneziaStyle.color.slateGray + property string borderFocusedColor: AmneziaStyle.color.paleGray + + property string backgroundColor: AmneziaStyle.color.onyxBlack + property string backgroundDisabledColor: AmneziaStyle.color.transparent + property string bgBorderHoveredColor: AmneziaStyle.color.charcoalGray + + implicitWidth: content.implicitWidth + implicitHeight: content.implicitHeight + + Keys.onTabPressed: { + FocusController.nextKeyTabItem() + } + + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } + + Keys.onUpPressed: { + FocusController.nextKeyUpItem() + } + + Keys.onDownPressed: { + FocusController.nextKeyDownItem() + } + + ColumnLayout { + id: content + anchors.fill: parent + + Rectangle { + id: backgroud + Layout.fillWidth: true + Layout.preferredHeight: input.implicitHeight + color: root.enabled ? root.backgroundColor : root.backgroundDisabledColor + radius: 16 + border.color: getBackgroundBorderColor(root.borderColor) + border.width: 1 + + Behavior on border.color { + PropertyAnimation { duration: 200 } + } + + RowLayout { + id: input + anchors.fill: backgroud + ColumnLayout { + Layout.margins: 16 + LabelTextType { + text: root.headerText + color: root.enabled ? root.headerTextColor : root.headerTextDisabledColor + + visible: text !== "" + + Layout.fillWidth: true + } + + TextField { + id: textField + + property bool isFocusable: true + + Keys.onTabPressed: { + FocusController.nextKeyTabItem() + } + + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } + + enabled: root.textFieldEditable + color: root.enabled ? root.textFieldTextColor : root.textFieldTextDisabledColor + + inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhSensitiveData | Qt.ImhNoPredictiveText + + placeholderTextColor: AmneziaStyle.color.charcoalGray + + selectionColor: AmneziaStyle.color.richBrown + selectedTextColor: AmneziaStyle.color.paleGray + + font.pixelSize: 16 + font.weight: 400 + font.family: "PT Root UI VF" + + height: 24 + Layout.fillWidth: true + + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + bottomPadding: 0 + + background: Rectangle { + anchors.fill: parent + color: root.enabled ? root.backgroundColor : root.backgroundDisabledColor + } + + onTextChanged: { + root.errorText = "" + } + + onActiveFocusChanged: { + if (root.checkEmptyText && text === "") { + root.errorText = qsTr("The field can't be empty") + } + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.RightButton + onClicked: contextMenu.open() + enabled: true + } + + ContextMenuType { + id: contextMenu + textObj: textField + } + + onFocusChanged: { + backgroud.border.color = getBackgroundBorderColor(root.borderColor) + } + } + } + } + } + + SmallTextType { + id: errorField + + text: root.errorText + visible: root.errorText !== "" + color: AmneziaStyle.color.vibrantRed + + Layout.fillWidth: true + } + } + + MouseArea { + anchors.fill: root + cursorShape: Qt.IBeamCursor + + hoverEnabled: true + + onPressed: function(mouse) { + textField.forceActiveFocus() + mouse.accepted = false + + backgroud.border.color = getBackgroundBorderColor(root.borderColor) + } + + onEntered: { + backgroud.border.color = getBackgroundBorderColor(bgBorderHoveredColor) + } + + + onExited: { + backgroud.border.color = getBackgroundBorderColor(root.borderColor) + } + } + + BasicButtonType { + visible: (root.buttonText !== "") || (root.buttonImageSource !== "") + + focusPolicy: Qt.NoFocus + text: root.buttonText + leftImageSource: root.buttonImageSource + + anchors.top: content.top + anchors.bottom: content.bottom + anchors.right: content.right + + height: content.implicitHeight + width: content.implicitHeight + squareLeftSide: true + + clickedFunc: function() { + if (root.clickedFunc && typeof root.clickedFunc === "function") { + root.clickedFunc() + } + } + } + + function getBackgroundBorderColor(noneFocusedColor) { + return textField.focus ? root.borderFocusedColor : noneFocusedColor + } + + Keys.onEnterPressed: { + if (root.rightButtonClickedOnEnter && root.clickedFunc && typeof root.clickedFunc === "function") { + clickedFunc() + } + + // if (KeyNavigation.tab) { + // KeyNavigation.tab.forceActiveFocus(); + // } + } + + Keys.onReturnPressed: { + if (root.rightButtonClickedOnEnter &&root.clickedFunc && typeof root.clickedFunc === "function") { + clickedFunc() + } + + // if (KeyNavigation.tab) { + // KeyNavigation.tab.forceActiveFocus(); + // } + } +} diff --git a/client/ui/qml/DefaultVpn/Controls/ContextMenuType.qml b/client/ui/qml/DefaultVpn/Controls/ContextMenuType.qml index cb32e311..4f7936af 100644 --- a/client/ui/qml/DefaultVpn/Controls/ContextMenuType.qml +++ b/client/ui/qml/DefaultVpn/Controls/ContextMenuType.qml @@ -18,8 +18,8 @@ Menu { } MenuItem { text: qsTr("&Paste") - // Fix calling paste from clipboard when launching app on android - enabled: Qt.platform.os === "android" ? true : textObj.canPaste + // Fix calling paste from clipboard when launching app on android/ios + enabled: (Qt.platform.os === "android" || Qt.platform.os === "ios") ? true : textObj.canPaste onTriggered: textObj.paste() } diff --git a/client/ui/qml/Pages2/PageHome.qml b/client/ui/qml/Pages2/PageHome.qml index 43a9df32..d2289ab0 100644 --- a/client/ui/qml/Pages2/PageHome.qml +++ b/client/ui/qml/Pages2/PageHome.qml @@ -109,6 +109,34 @@ PageType { } } + BasicButtonType { + id: devGatewayButton + objectName: "devGatewayButton" + + property bool isDevGatewayEnabled: SettingsController.isDevGatewayEnv + + Layout.alignment: Qt.AlignHCenter + + implicitHeight: 36 + + defaultColor: AmneziaStyle.color.transparent + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite + disabledColor: AmneziaStyle.color.mutedGray + textColor: AmneziaStyle.color.mutedGray + borderWidth: 0 + + visible: SettingsController.isDevModeEnabled && isDevGatewayEnabled + text: qsTr("Dev gateway enabled") + + Keys.onEnterPressed: this.clicked() + Keys.onReturnPressed: this.clicked() + + onClicked: { + PageController.goToPage(PageEnum.PageDevMenu) + } + } + ConnectButton { id: connectButton objectName: "connectButton" diff --git a/client/ui/qml/Pages2/PageSettings.qml b/client/ui/qml/Pages2/PageSettings.qml index 81c5dc42..94f2f6c5 100644 --- a/client/ui/qml/Pages2/PageSettings.qml +++ b/client/ui/qml/Pages2/PageSettings.qml @@ -1,163 +1,177 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import QtQuick.Dialogs - -import PageEnum 1.0 -import Style 1.0 - -import "./" -import "../Controls2" -import "../Controls2/TextTypes" -import "../Config" - -PageType { - id: root - - ListViewType { - id: listView - - anchors.fill: parent - - header: ColumnLayout { - width: listView.width - - BaseHeaderType { - id: header - Layout.fillWidth: true - Layout.topMargin: 24 - Layout.bottomMargin: 16 - Layout.rightMargin: 16 - Layout.leftMargin: 16 - - headerText: qsTr("Settings") - } - } - - model: settingsEntries - - delegate: ColumnLayout { - width: listView.width - - spacing: 0 - - LabelWithButtonType { - Layout.fillWidth: true - - visible: isVisible - - text: title - rightImageSource: "qrc:/images/controls/chevron-right.svg" - leftImageSource: leftImagePath - - clickedFunction: clickedHandler - } - - DividerType { - visible: isVisible - } - } - - footer: ColumnLayout { - width: listView.width - - LabelWithButtonType { - id: close - - visible: GC.isDesktop() - Layout.fillWidth: true - - text: qsTr("Close application") - leftImageSource: "qrc:/images/controls/x-circle.svg" - isLeftImageHoverEnabled: false - - clickedFunction: function() { - PageController.closeApplication() - } - } - - DividerType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - - visible: GC.isDesktop() - } - } - } - - property list settingsEntries: [ - servers, - connection, - application, - backup, - about, - devConsole - ] - - QtObject { - id: servers - - property string title: qsTr("Servers") - readonly property string leftImagePath: "qrc:/images/controls/server.svg" - property bool isVisible: true - readonly property var clickedHandler: function() { - PageController.goToPage(PageEnum.PageSettingsServersList) - } - } - - QtObject { - id: connection - - property string title: qsTr("Connection") - readonly property string leftImagePath: "qrc:/images/controls/radio.svg" - property bool isVisible: true - readonly property var clickedHandler: function() { - PageController.goToPage(PageEnum.PageSettingsConnection) - } - } - - QtObject { - id: application - - property string title: qsTr("Application") - readonly property string leftImagePath: "qrc:/images/controls/app.svg" - property bool isVisible: true - readonly property var clickedHandler: function() { - PageController.goToPage(PageEnum.PageSettingsApplication) - } - } - - QtObject { - id: backup - - property string title: qsTr("Backup") - readonly property string leftImagePath: "qrc:/images/controls/save.svg" - property bool isVisible: true - readonly property var clickedHandler: function() { - PageController.goToPage(PageEnum.PageSettingsBackup) - } - } - - QtObject { - id: about - - property string title: qsTr("About DefaultVPN") - readonly property string leftImagePath: "qrc:/images/controls/amnezia.svg" - property bool isVisible: true - readonly property var clickedHandler: function() { - PageController.goToPage(PageEnum.PageSettingsAbout) - } - } - - QtObject { - id: devConsole - - property string title: qsTr("Dev console") - readonly property string leftImagePath: "qrc:/images/controls/bug.svg" - property bool isVisible: SettingsController.isDevModeEnabled - readonly property var clickedHandler: function() { - PageController.goToPage(PageEnum.PageDevMenu) - } - } -} +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Dialogs + +import PageEnum 1.0 +import Style 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" + +PageType { + id: root + + ListViewType { + id: listView + + anchors.fill: parent + + header: ColumnLayout { + width: listView.width + + BaseHeaderType { + id: header + Layout.fillWidth: true + Layout.topMargin: 24 + Layout.bottomMargin: 16 + Layout.rightMargin: 16 + Layout.leftMargin: 16 + + headerText: qsTr("Settings") + } + } + + model: settingsEntries + + delegate: ColumnLayout { + width: listView.width + + spacing: 0 + + LabelWithButtonType { + Layout.fillWidth: true + + visible: isVisible + + text: title + rightImageSource: "qrc:/images/controls/chevron-right.svg" + leftImageSource: leftImagePath + + clickedFunction: clickedHandler + } + + DividerType { + visible: isVisible + } + } + + footer: ColumnLayout { + width: listView.width + + LabelWithButtonType { + id: close + + visible: GC.isDesktop() + Layout.fillWidth: true + + text: qsTr("Close application") + leftImageSource: "qrc:/images/controls/x-circle.svg" + isLeftImageHoverEnabled: false + + clickedFunction: function() { + PageController.closeApplication() + } + } + + DividerType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + + visible: GC.isDesktop() + } + } + } + + property list settingsEntries: [ + servers, + connection, + application, + news, + backup, + about, + devConsole + ] + + QtObject { + id: servers + + property string title: qsTr("Servers") + readonly property string leftImagePath: "qrc:/images/controls/server.svg" + property bool isVisible: true + readonly property var clickedHandler: function() { + PageController.goToPage(PageEnum.PageSettingsServersList) + } + } + + QtObject { + id: connection + + property string title: qsTr("Connection") + readonly property string leftImagePath: "qrc:/images/controls/radio.svg" + property bool isVisible: true + readonly property var clickedHandler: function() { + PageController.goToPage(PageEnum.PageSettingsConnection) + } + } + + QtObject { + id: application + + property string title: qsTr("Application") + readonly property string leftImagePath: "qrc:/images/controls/app.svg" + property bool isVisible: true + readonly property var clickedHandler: function() { + PageController.goToPage(PageEnum.PageSettingsApplication) + } + } + + QtObject { + id: news + + property string title: qsTr("News & Notifications") + readonly property string leftImagePath: NewsModel.hasUnread ? "qrc:/images/controls/news-unread.svg" : "qrc:/images/controls/news.svg" + property bool isVisible: ServersModel.hasServersFromGatewayApi + readonly property var clickedHandler: function() { + if (!ServersModel.hasServersFromGatewayApi) return; + ApiNewsController.fetchNews(); + PageController.goToPage(PageEnum.PageSettingsNewsNotifications) + } + } + + QtObject { + id: backup + + property string title: qsTr("Backup") + readonly property string leftImagePath: "qrc:/images/controls/save.svg" + property bool isVisible: true + readonly property var clickedHandler: function() { + PageController.goToPage(PageEnum.PageSettingsBackup) + } + } + + QtObject { + id: about + + property string title: qsTr("About DefaultVPN") + readonly property string leftImagePath: "qrc:/images/controls/amnezia.svg" + property bool isVisible: true + readonly property var clickedHandler: function() { + PageController.goToPage(PageEnum.PageSettingsAbout) + } + } + + QtObject { + id: devConsole + + property string title: qsTr("Dev console") + readonly property string leftImagePath: "qrc:/images/controls/bug.svg" + property bool isVisible: SettingsController.isDevModeEnabled + readonly property var clickedHandler: function() { + PageController.goToPage(PageEnum.PageDevMenu) + } + } +} diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml index f1290948..5a36daa8 100644 --- a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml @@ -359,7 +359,7 @@ PageType { PageController.showNotificationMessage(qsTr("Cannot unlink device during active connection")) } else { PageController.showBusyIndicator(true) - if (ApiConfigsController.deactivateDevice()) { + if (ApiConfigsController.deactivateDevice(false)) { ApiSettingsController.getAccountInfo(true) } PageController.showBusyIndicator(false) @@ -396,7 +396,7 @@ PageType { PageController.showNotificationMessage(qsTr("Cannot remove server during active connection")) } else { PageController.showBusyIndicator(true) - if (ApiConfigsController.deactivateDevice()) { + if (ApiConfigsController.deactivateDevice(true)) { InstallController.removeProcessedServer() } PageController.showBusyIndicator(false) diff --git a/client/ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml b/client/ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml index d31d87fe..8760fdd6 100644 --- a/client/ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml +++ b/client/ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml @@ -6,6 +6,8 @@ import Qt.labs.platform 1.1 import QtCore +import SortFilterProxyModel 0.2 + import PageEnum 1.0 import Style 1.0 @@ -17,6 +19,33 @@ import "../Components" PageType { id: root + property var processedServer + + Connections { + target: ServersModel + + function onProcessedServerChanged() { + root.processedServer = proxyServersModel.get(0) + } + } + + SortFilterProxyModel { + id: proxyServersModel + objectName: "proxyServersModel" + + sourceModel: ServersModel + filters: [ + ValueFilter { + roleName: "isCurrentlyProcessed" + value: true + } + ] + + Component.onCompleted: { + root.processedServer = proxyServersModel.get(0) + } + } + Component.onCompleted: { PageController.showBusyIndicator(true) ApiConfigsController.prepareVpnKeyExport() @@ -40,7 +69,7 @@ PageType { Layout.leftMargin: 16 Layout.rightMargin: 16 Layout.topMargin: 16 - text: qsTr("Amnezia Premium\nsubscription key") + text: qsTr(root.processedServer.name + "\nsubscription key") font.pixelSize: 32 font.bold: true color: AmneziaStyle.color.paleGray @@ -56,7 +85,7 @@ PageType { text: qsTr("Copy key") leftImageSource: "qrc:/images/controls/copy.svg" - onClicked: { + clickedFunc: function() { ApiConfigsController.copyVpnKeyToClipboard() PageController.showNotificationMessage(qsTr("Copied")) } @@ -77,20 +106,20 @@ PageType { text: qsTr("Save key as a file") leftImageSource: "qrc:/images/controls/share-2.svg" - onClicked: { + clickedFunc: function() { var fileName = GC.isMobile() - ? "amnezia_vpn_key.vpn" + ? root.processedServer.name.toLowerCase().replace(/\s+/g, "_") + "_key.vpn" : SystemController.getFileName( qsTr("Save DefaultVPN config"), qsTr("Config files (*.vpn)"), - StandardPaths.standardLocations(StandardPaths.DocumentsLocation) + "/amnezia_vpn_key", + StandardPaths.standardLocations(StandardPaths.DocumentsLocation) + "/" + root.processedServer.name.toLowerCase().replace(/\s+/g, "_") + "_key", true, ".vpn" ) if (fileName !== "") { PageController.showBusyIndicator(true) - ExportController.exportConfig(fileName) + ApiConfigsController.exportVpnKey(fileName) PageController.showBusyIndicator(false) } } @@ -110,7 +139,7 @@ PageType { text: qsTr("Show key text") leftImageSource: "qrc:/images/controls/eye.svg" - onClicked: { + clickedFunc: function() { PageController.showBusyIndicator(true) ApiConfigsController.prepareVpnKeyExport() PageController.showBusyIndicator(false) @@ -119,8 +148,9 @@ PageType { } Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: width + Layout.preferredWidth: Math.min(Math.min(root.width - (Layout.leftMargin + Layout.rightMargin), root.height * 0.5), 360) + Layout.preferredHeight: Layout.preferredWidth + Layout.alignment: Qt.AlignHCenter Layout.topMargin: 20 Layout.leftMargin: 16 Layout.rightMargin: 16 @@ -132,6 +162,9 @@ PageType { Image { anchors.fill: parent smooth: false + fillMode: Image.PreserveAspectFit + sourceSize.width: parent.width + sourceSize.height: parent.height source: ApiConfigsController.qrCodesCount > 0 && ApiConfigsController.qrCodes[0] ? ApiConfigsController.qrCodes[0] : "" } } @@ -173,7 +206,7 @@ PageType { Header2Type { Layout.fillWidth: true - headerText: qsTr("Amnezia Premium Subscription key") + headerText: qsTr(root.processedServer.name + " Subscription key") } TextArea { diff --git a/client/ui/qml/Pages2/PageSettingsAppSplitTunneling.qml b/client/ui/qml/Pages2/PageSettingsAppSplitTunneling.qml index 60b59f45..d83994bf 100644 --- a/client/ui/qml/Pages2/PageSettingsAppSplitTunneling.qml +++ b/client/ui/qml/Pages2/PageSettingsAppSplitTunneling.qml @@ -145,11 +145,25 @@ PageType { } } } + + WarningType { + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + + textString: qsTr("Only \"Apps from the list should not have access via VPN\" mode is available on Windows") + iconPath: "qrc:/images/controls/alert-circle.svg" + + visible: (Qt.platform.os === "windows") && root.pageEnabled + } } ListViewType { id: listView + ScrollBar.vertical: ScrollBarType { policy: ScrollBar.AlwaysOn } + anchors.top: header.bottom anchors.bottom: addAppButton.top anchors.left: parent.left diff --git a/client/ui/qml/Pages2/PageSettingsApplication.qml b/client/ui/qml/Pages2/PageSettingsApplication.qml index 648a3077..4025e0b4 100644 --- a/client/ui/qml/Pages2/PageSettingsApplication.qml +++ b/client/ui/qml/Pages2/PageSettingsApplication.qml @@ -157,9 +157,9 @@ PageType { enabled: switcherAutoStart.checked opacity: enabled ? 1.0 : 0.5 - checked: SettingsController.isStartMinimizedEnabled() + checked: SettingsController.startMinimized onToggled: function() { - if (checked !== SettingsController.isStartMinimizedEnabled()) { + if (checked !== SettingsController.startMinimized) { SettingsController.toggleStartMinimized(checked) } } diff --git a/client/ui/qml/Pages2/PageSettingsNewsDetail.qml b/client/ui/qml/Pages2/PageSettingsNewsDetail.qml new file mode 100644 index 00000000..bda9543c --- /dev/null +++ b/client/ui/qml/Pages2/PageSettingsNewsDetail.qml @@ -0,0 +1,69 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import PageEnum 1.0 +import Style 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" +import SortFilterProxyModel 0.2 + +PageType { + id: root + property var newsItem + + SortFilterProxyModel { + id: proxyNews + sourceModel: NewsModel + filters: [ ValueFilter { roleName: "isProcessed"; value: true } ] + Component.onCompleted: root.newsItem = proxyNews.get(0) + } + + Connections { + target: NewsModel + function onProcessedIndexChanged() { + root.newsItem = proxyNews.get(0) + } + } + + BackButtonType { + id: backButton + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 20 + } + + FlickableType { + id: fl + anchors.top: backButton.bottom + anchors.bottom: parent.bottom + contentHeight: content.height + + ColumnLayout { + id: content + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + spacing: 0 + + BaseHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + headerText: newsItem.title + } + + ParagraphTextType { + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: newsItem.content + } + } + } +} diff --git a/client/ui/qml/Pages2/PageSettingsNewsNotifications.qml b/client/ui/qml/Pages2/PageSettingsNewsNotifications.qml new file mode 100644 index 00000000..cd4a1b00 --- /dev/null +++ b/client/ui/qml/Pages2/PageSettingsNewsNotifications.qml @@ -0,0 +1,81 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import PageEnum 1.0 +import Style 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" + +PageType { + id: root + + ColumnLayout { + id: header + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + anchors.topMargin: 20 + + BackButtonType { + id: backButton + } + + BaseHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + + headerText: qsTr("News & Notifications") + } + } + + ListView { + id: newsList + width: parent.width + anchors.top: header.bottom + anchors.topMargin: 16 + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + + property bool isFocusable: true + + model: NewsModel + + clip: true + reuseItems: true + + delegate: Item { + implicitWidth: newsList.width + implicitHeight: content.implicitHeight + + ColumnLayout { + id: content + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + LabelWithButtonType { + Layout.fillWidth: true + leftImageSource: read ? "" : "qrc:/images/controls/unread-dot.svg" + isSmallLeftImage: !read + text: title + rightImageSource: "qrc:/images/controls/chevron-right.svg" + + clickedFunction: function() { + NewsModel.markAsRead(index) + NewsModel.processedIndex = index + PageController.goToPage(PageEnum.PageSettingsNewsDetail) + } + } + + DividerType {} + } + } + } +} diff --git a/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml b/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml index 7d0ba599..ba020b42 100644 --- a/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml +++ b/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml @@ -164,6 +164,8 @@ PageType { ListViewType { id: listView + ScrollBar.vertical: ScrollBarType { policy: ScrollBar.AlwaysOn } + anchors.top: header.bottom anchors.topMargin: 16 anchors.bottom: addSiteButton.top @@ -348,7 +350,6 @@ PageType { Layout.fillWidth: true text: qsTr("Clear site list") - rightImageSource: "qrc:/images/controls/trash.svg" clickedFunction: function() { var headerText = qsTr("Clear site list?") diff --git a/client/ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml b/client/ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml index 8668a1e2..f20886a5 100644 --- a/client/ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml +++ b/client/ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml @@ -87,7 +87,7 @@ PageType { textFormat: Text.RichText text: { var text = ApiServicesModel.getSelectedServiceData("features") - return text.replace("%1", LanguageModel.getCurrentSiteUrl("free")) + return text.replace("%1", LanguageModel.getCurrentSiteUrl("free")).replace("/free", "") // todo link should come from gateway } MouseArea { diff --git a/client/ui/qml/Pages2/PageSetupWizardInstalling.qml b/client/ui/qml/Pages2/PageSetupWizardInstalling.qml index efabeb9f..2e069cb7 100644 --- a/client/ui/qml/Pages2/PageSetupWizardInstalling.qml +++ b/client/ui/qml/Pages2/PageSetupWizardInstalling.qml @@ -30,7 +30,7 @@ PageType { if (!ConnectionController.isConnected && !ContainersModel.isServiceContainer(containerIndex)) { ServersModel.setDefaultContainer(ServersModel.processedIndex, containerIndex) } - + PageController.closePage() // close installing page PageController.closePage() // close protocol settings page @@ -38,6 +38,10 @@ PageType { PageController.restorePageHomeState(true) } + if (stackView.currentItem.objectName === PageController.getPagePath(PageEnum.PageSetupWizardProtocols)) { + PageController.goToPage(PageEnum.PageHome) + } + PageController.showNotificationMessage(finishedMessage) } diff --git a/client/ui/qml/Pages2/PageShare.qml b/client/ui/qml/Pages2/PageShare.qml index 042e6fba..04583923 100644 --- a/client/ui/qml/Pages2/PageShare.qml +++ b/client/ui/qml/Pages2/PageShare.qml @@ -29,19 +29,14 @@ PageType { Xray } - signal revokeConfig(int index) - onRevokeConfig: function(index) { - PageController.showBusyIndicator(true) - ExportController.revokeConfig(index, - ContainersModel.getProcessedContainerIndex(), - ServersModel.getProcessedServerCredentials()) - PageController.showBusyIndicator(false) - PageController.showNotificationMessage(qsTr("Config revoked")) - } - Connections { target: ExportController + function onRevokeConfigCompleted() { + PageController.showBusyIndicator(false) + PageController.showNotificationMessage(qsTr("Config revoked")) + } + function onGenerateConfig(type) { PageController.showBusyIndicator(true) @@ -608,12 +603,14 @@ PageType { visible: accessTypeSelector.currentIndex === 1 property bool isFocusable: true + property bool freezeFilter: false model: SortFilterProxyModel { id: proxyClientManagementModel sourceModel: ClientManagementModel filters: RegExpFilter { roleName: "clientName" + enabled: !clientsListView.freezeFilter pattern: ".*" + searchTextField.textField.text + ".*" caseSensitivity: Qt.CaseInsensitive } @@ -796,12 +793,14 @@ PageType { } if (clientNameEditor.textField.text !== clientName) { + clientsListView.freezeFilter = true PageController.showBusyIndicator(true) - ExportController.renameClient(index, + ExportController.renameClient(proxyClientManagementModel.mapToSource(index), clientNameEditor.textField.text, ContainersModel.getProcessedContainerIndex(), ServersModel.getProcessedServerCredentials()) PageController.showBusyIndicator(false) + Qt.callLater(function(){ clientsListView.freezeFilter = false }) clientNameEditDrawer.closeTriggered() } } @@ -832,7 +831,10 @@ PageType { var yesButtonFunction = function() { clientInfoDrawer.closeTriggered() - root.revokeConfig(index) + PageController.showBusyIndicator(true) + ExportController.revokeConfig(proxyClientManagementModel.mapToSource(index), + ContainersModel.getProcessedContainerIndex(), + ServersModel.getProcessedServerCredentials()) } var noButtonFunction = function() { } diff --git a/client/ui/qml/Pages2/PageShareConnection.qml b/client/ui/qml/Pages2/PageShareConnection.qml index 262d8734..28fdf158 100644 --- a/client/ui/qml/Pages2/PageShareConnection.qml +++ b/client/ui/qml/Pages2/PageShareConnection.qml @@ -269,8 +269,9 @@ PageType { Rectangle { id: qrCodeContainer - Layout.fillWidth: true - Layout.preferredHeight: width + Layout.preferredWidth: Math.min(Math.min(listView.width - (Layout.leftMargin + Layout.rightMargin), pageShareConnection.height * 0.5), 360) + Layout.preferredHeight: Layout.preferredWidth + Layout.alignment: Qt.AlignHCenter Layout.topMargin: 20 Layout.leftMargin: 16 Layout.rightMargin: 16 @@ -280,6 +281,9 @@ PageType { Image { anchors.fill: parent smooth: false + fillMode: Image.PreserveAspectFit + sourceSize.width: parent.width + sourceSize.height: parent.height source: pageShareConnection.isSelfHostedConfig ? (isQrCodeVisible ? ExportController.qrCodes[0] : "") : (isQrCodeVisible ? ApiConfigsController.qrCodes[0] : "") property bool isFocusable: true Keys.onTabPressed: FocusController.nextKeyTabItem() diff --git a/client/ui/qml/Pages2/PageShareFullAccess.qml b/client/ui/qml/Pages2/PageShareFullAccess.qml index 37f89f1c..d79abcc3 100644 --- a/client/ui/qml/Pages2/PageShareFullAccess.qml +++ b/client/ui/qml/Pages2/PageShareFullAccess.qml @@ -160,7 +160,7 @@ PageType { PageController.showBusyIndicator(false) - PageController.goToShareConnectionPage(listView.headerText, listView.configContentHeaderText, "", "", "") + PageController.goToShareConnectionPage(listView.headerText, listView.configContentHeaderText, "", ".vpn", "amnezia_config") } } } diff --git a/client/ui/qml/Pages2/PageStart.qml b/client/ui/qml/Pages2/PageStart.qml index 570c51f3..65cc9a3d 100644 --- a/client/ui/qml/Pages2/PageStart.qml +++ b/client/ui/qml/Pages2/PageStart.qml @@ -380,7 +380,13 @@ PageType { objectName: "settingsTabButton" isSelected: tabBar.currentIndex === 2 - image: "qrc:/images/controls/settings.svg" + image: (ServersModel.hasServersFromGatewayApi && NewsModel.hasUnread) ? "qrc:/images/controls/settings-news.svg" : "qrc:/images/controls/settings.svg" + Binding { + target: settingsTabButton + property: "defaultColor" + value: "transparent" + when: (ServersModel.hasServersFromGatewayApi && NewsModel.hasUnread) + } clickedFunc: function () { tabBarStackView.goToTabBarPage(PageEnum.PageSettings) tabBar.currentIndex = 2 diff --git a/deploy/data/linux/post_uninstall.sh b/deploy/data/linux/post_uninstall.sh index b8e71c69..08509604 100755 --- a/deploy/data/linux/post_uninstall.sh +++ b/deploy/data/linux/post_uninstall.sh @@ -1,6 +1,7 @@ #!/bin/bash APP_NAME=DefaultVPN +ORG_NAME=DefaultVPN.ORG LOG_FOLDER=/var/log/$APP_NAME LOG_FILE="$LOG_FOLDER/post-uninstall.log" APP_PATH=/opt/$APP_NAME @@ -64,6 +65,24 @@ if test -f /usr/share/pixmaps/$APP_NAME.png; then fi +### Remove the service log file (keep post-uninstall.log) +if test -f "$LOG_FOLDER/DefaultVPN-service.log"; then + sudo rm -f "$LOG_FOLDER/DefaultVPN-service.log" >> $LOG_FILE 2>&1 +fi + +### Remove user logs for current user only +TARGET_HOME="$HOME" +if [ -n "$SUDO_USER" ] && [ "$SUDO_USER" != "root" ]; then + TARGET_HOME=$(getent passwd "$SUDO_USER" | cut -d: -f6) +fi +if test -d "$TARGET_HOME/.local/share/$ORG_NAME/$APP_NAME/log"; then + rm -rf "$TARGET_HOME/.local/share/$ORG_NAME/$APP_NAME/log" >> $LOG_FILE 2>&1 +fi + +# Try to remove empty app and organization directories under user share +if rmdir "$TARGET_HOME/.local/share/$ORG_NAME/$APP_NAME" 2>/dev/null; then :; fi +if rmdir "$TARGET_HOME/.local/share/$ORG_NAME" 2>/dev/null; then :; fi + if command -v steamos-readonly &> /dev/null; then sudo steamos-readonly enable >> $LOG_FILE echo "steamos-readonly enabled" >> $LOG_FILE diff --git a/deploy/data/windows/x32/post_uninstall.cmd b/deploy/data/windows/x32/post_uninstall.cmd index d68ff52d..79e8e5b8 100644 --- a/deploy/data/windows/x32/post_uninstall.cmd +++ b/deploy/data/windows/x32/post_uninstall.cmd @@ -1,7 +1,14 @@ set AmneziaPath=%~dp0 echo %AmneziaPath% -"%AmneziaPath%\DefaultVPN.exe" -c +rem Define directories for logs +set "ORG_DIR=%AppData%\DefaultVPN.ORG" +set "USER_APP_DIR=%ORG_DIR%\DefaultVPN" +set "USER_LOG_DIR=%USER_APP_DIR%\log" +set "SYS_APP_DIR=%ProgramData%\DefaultVPN" +set "SYS_LOG_DIR=%SYS_APP_DIR%\log" +set "SYS_LOG_FILE=%SYS_LOG_DIR%\DefaultVPN-service.log" + timeout /t 1 sc stop DefaultVPN-service sc delete DefaultVPN-service @@ -9,4 +16,17 @@ sc stop AmneziaWGTunnel$DefaultVPN sc delete AmneziaWGTunnel$DefaultVPN taskkill /IM "DefaultVPN-service.exe" /F taskkill /IM "DefaultVPN.exe" /F + +rem Delete the service log file under ProgramData +if exist "%SYS_LOG_FILE%" del /F /Q "%SYS_LOG_FILE%" +if exist "%SYS_LOG_DIR%" rmdir /S /Q "%SYS_LOG_DIR%" +rem Try to remove application dir if empty +rd "%SYS_APP_DIR%" 2>nul + +rem Delete client logs under current user's AppData\Roaming (Organization\Application) +if exist "%USER_LOG_DIR%" rmdir /S /Q "%USER_LOG_DIR%" +rem Try to remove app and org directories if empty +rd "%USER_APP_DIR%" 2>nul +rd "%ORG_DIR%" 2>nul + exit /b 0 diff --git a/deploy/data/windows/x64/post_uninstall.cmd b/deploy/data/windows/x64/post_uninstall.cmd index d68ff52d..79e8e5b8 100644 --- a/deploy/data/windows/x64/post_uninstall.cmd +++ b/deploy/data/windows/x64/post_uninstall.cmd @@ -1,7 +1,14 @@ set AmneziaPath=%~dp0 echo %AmneziaPath% -"%AmneziaPath%\DefaultVPN.exe" -c +rem Define directories for logs +set "ORG_DIR=%AppData%\DefaultVPN.ORG" +set "USER_APP_DIR=%ORG_DIR%\DefaultVPN" +set "USER_LOG_DIR=%USER_APP_DIR%\log" +set "SYS_APP_DIR=%ProgramData%\DefaultVPN" +set "SYS_LOG_DIR=%SYS_APP_DIR%\log" +set "SYS_LOG_FILE=%SYS_LOG_DIR%\DefaultVPN-service.log" + timeout /t 1 sc stop DefaultVPN-service sc delete DefaultVPN-service @@ -9,4 +16,17 @@ sc stop AmneziaWGTunnel$DefaultVPN sc delete AmneziaWGTunnel$DefaultVPN taskkill /IM "DefaultVPN-service.exe" /F taskkill /IM "DefaultVPN.exe" /F + +rem Delete the service log file under ProgramData +if exist "%SYS_LOG_FILE%" del /F /Q "%SYS_LOG_FILE%" +if exist "%SYS_LOG_DIR%" rmdir /S /Q "%SYS_LOG_DIR%" +rem Try to remove application dir if empty +rd "%SYS_APP_DIR%" 2>nul + +rem Delete client logs under current user's AppData\Roaming (Organization\Application) +if exist "%USER_LOG_DIR%" rmdir /S /Q "%USER_LOG_DIR%" +rem Try to remove app and org directories if empty +rd "%USER_APP_DIR%" 2>nul +rd "%ORG_DIR%" 2>nul + exit /b 0 diff --git a/ipc/ipc_interface.rep b/ipc/ipc_interface.rep index 4ecae9bc..d9eb3e0f 100644 --- a/ipc/ipc_interface.rep +++ b/ipc/ipc_interface.rep @@ -36,5 +36,6 @@ class IpcInterface SLOT( bool enablePeerTraffic( const QJsonObject &configStr) ); SLOT( bool enableKillSwitch( const QJsonObject &excludeAddr, int vpnAdapterIndex) ); SLOT( bool updateResolvers(const QString& ifname, const QList& resolvers) ); + SLOT( bool restoreResolvers() ); }; diff --git a/ipc/ipcserver.cpp b/ipc/ipcserver.cpp index 0c7f5295..cb669c68 100644 --- a/ipc/ipcserver.cpp +++ b/ipc/ipcserver.cpp @@ -157,6 +157,10 @@ bool IpcServer::updateResolvers(const QString &ifname, const QList return Router::updateResolvers(ifname, resolvers); } +bool IpcServer::restoreResolvers() { + return Router::restoreResolvers(); +} + void IpcServer::StartRoutingIpv6() { Router::StartRoutingIpv6(); diff --git a/ipc/ipcserver.h b/ipc/ipcserver.h index 00d36354..bcc5733a 100644 --- a/ipc/ipcserver.h +++ b/ipc/ipcserver.h @@ -42,6 +42,7 @@ public: virtual bool disableKillSwitch() override; virtual bool refreshKillSwitch( bool enabled ) override; virtual bool updateResolvers(const QString& ifname, const QList& resolvers) override; + virtual bool restoreResolvers() override; private: int m_localpid = 0; diff --git a/service/server/router.cpp b/service/server/router.cpp index fdf03232..be422f04 100644 --- a/service/server/router.cpp +++ b/service/server/router.cpp @@ -99,6 +99,17 @@ bool Router::updateResolvers(const QString& ifname, const QList& r #endif } +bool Router::restoreResolvers() { +#ifdef Q_OS_LINUX + return RouterLinux::Instance().restoreResolvers(); +#endif +#ifdef Q_OS_MACOS + return RouterMac::Instance().restoreResolvers(); +#endif +#ifdef Q_OS_WIN + return RouterWin::Instance().restoreResolvers(); +#endif +} void Router::StopRoutingIpv6() { diff --git a/service/server/router.h b/service/server/router.h index dfaf9021..2285cdce 100644 --- a/service/server/router.h +++ b/service/server/router.h @@ -26,6 +26,7 @@ public: static void StartRoutingIpv6(); static void StopRoutingIpv6(); static bool updateResolvers(const QString& ifname, const QList& resolvers); + static bool restoreResolvers(); }; #endif // ROUTER_H diff --git a/service/server/router_linux.cpp b/service/server/router_linux.cpp index 852c878f..7601265b 100644 --- a/service/server/router_linux.cpp +++ b/service/server/router_linux.cpp @@ -279,6 +279,10 @@ bool RouterLinux::updateResolvers(const QString& ifname, const QListupdateResolvers(ifname, resolvers); } +bool RouterLinux::restoreResolvers() { + return m_dnsUtil->restoreResolvers(); +} + void RouterLinux::StartRoutingIpv6() { QProcess process; diff --git a/service/server/router_linux.h b/service/server/router_linux.h index 2094f596..a7bf534d 100644 --- a/service/server/router_linux.h +++ b/service/server/router_linux.h @@ -36,6 +36,7 @@ public: void StartRoutingIpv6(); void StopRoutingIpv6(); bool updateResolvers(const QString& ifname, const QList& resolvers); + bool restoreResolvers(); public slots: private: diff --git a/service/server/router_mac.cpp b/service/server/router_mac.cpp index d1afb68f..056517b5 100644 --- a/service/server/router_mac.cpp +++ b/service/server/router_mac.cpp @@ -158,6 +158,9 @@ bool RouterMac::updateResolvers(const QString& ifname, const QList return m_dnsUtil->updateResolvers(ifname, resolvers); } +bool RouterMac::restoreResolvers() { + return m_dnsUtil->restoreResolvers(); +} bool RouterMac::deleteTun(const QString &dev) { diff --git a/service/server/router_mac.h b/service/server/router_mac.h index d23e40fa..9289ce56 100644 --- a/service/server/router_mac.h +++ b/service/server/router_mac.h @@ -33,6 +33,7 @@ public: bool createTun(const QString &dev, const QString &subnet); bool deleteTun(const QString &dev); bool updateResolvers(const QString& ifname, const QList& resolvers); + bool restoreResolvers(); public slots: diff --git a/service/server/router_win.cpp b/service/server/router_win.cpp index 998f4193..849b8059 100644 --- a/service/server/router_win.cpp +++ b/service/server/router_win.cpp @@ -443,6 +443,9 @@ bool RouterWin::updateResolvers(const QString& ifname, const QList return m_dnsUtil->updateResolvers(ifname, resolvers); } +bool RouterWin::restoreResolvers() { + return m_dnsUtil->restoreResolvers(); +} void RouterWin::StopRoutingIpv6() { diff --git a/service/server/router_win.h b/service/server/router_win.h index 07b5be35..a137487e 100644 --- a/service/server/router_win.h +++ b/service/server/router_win.h @@ -47,6 +47,7 @@ public: void suspendWcmSvc(bool suspend); bool updateResolvers(const QString& ifname, const QList& resolvers); + bool restoreResolvers(); private: RouterWin(RouterWin const &) = delete;