Compare commits

..

89 Commits

Author SHA1 Message Date
albexk 29d98846d3 Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
2025-10-10 13:33:28 +03:00
vkamn f6e7d3ccf1 fix: minor ui fixes (#1917)
* feat: improve storage processing

* fix: minor ui fixes
2025-10-09 23:22:58 +08:00
Mitternacht822 a754a11913 fix: added displaying vpn_key field added in older version of the app (#1873)
* fix(api_key): added displaying vpn_key field added in older version of the app

* revert changes

* fix: implemented generation of api key text for PremiumV2

* fix: deleted unnecessary code

* saving apikey text when generating

* added method for vpn key export, fixed wrong saving file
2025-10-07 23:16:28 +08:00
vkamn 4d25e3b6f6 chore: minor bugfixes (#1915) 2025-10-07 23:15:06 +08:00
MrMirDan 1fac280497 fix: main app info added after clearing logs (#1913) 2025-10-06 21:07:04 +08:00
Yaroslav c886c5e6a7 feat: enhance OpenVPN configuration handling and logging for iOS plat… (#1910)
* feat: enhance OpenVPN configuration handling and logging for iOS platform

* refactor: remove $OPENVPN_TA_KEY_SANITIZED and use $OPENVPN_TA_KEY instead
2025-10-06 21:04:49 +08:00
aiamnezia cd7f78b9ca feat: news and notifications page (#1660)
* Add news and notifications

* Add localization for news and notifications

* Remove news caching

* Add fetching news befor openning news page

* Fix not updating news page

* Delete debug output

* Remove news and notificztions with only self-hosted servers

* Add stack filters to fetching news request

* Add fetching news with changing stack in the client

* small refactoring

* polishing

* Rename newsModel files and fix naming in code

* fix: remove custom signals; fetch news only on stack expansion

* chore: delete unnecessary code

* chore: code style fixes

* fix: fixed memory leak in gateway controller

---------

Co-authored-by: vkamn <vk@amnezia.org>
2025-10-06 12:06:36 +08:00
vkamn a587d3230f fix: again fixed site link for features field (#1908) 2025-10-06 11:38:57 +08:00
MrMirDan 93e7b45136 fix: removed 'clear site list' button icon (#1909) 2025-10-06 11:37:42 +08:00
vkamn e024f71ce1 fix: allow remove expired api configs (#1907) 2025-10-03 14:45:12 +08:00
MrMirDan 50d1be7b4a chore: update for RU translation (#1893) 2025-10-02 20:59:45 +08:00
MrMirDan 3ec6d8973b fix: warning visible only on windows (#1900) 2025-10-02 20:59:23 +08:00
Yaroslav Gurov 3ea47d31a9 fix: restore dns after using xray (#1902) 2025-10-02 20:58:53 +08:00
vkamn 30c8cc4548 feat: add isConnectEvent field to api request (#1896) 2025-09-30 12:10:27 +08:00
vkamn 98586d2dd9 fix: fixed site link (#1897) 2025-09-30 12:07:27 +08:00
vkamn c66d8ecca0 chore: bump version (#1892) 2025-09-29 11:07:27 +08:00
vkamn db535f7e7d chore: increase default values (#1891) 2025-09-29 11:05:30 +08:00
vkamn 89f30d8c31 fix: fixed native wg obfuscation (#1890) 2025-09-29 10:58:44 +08:00
Yaroslav 8bce432824 fix: enable paste from clipboard on ios in addition to android (#1868) 2025-09-29 10:56:41 +08:00
MrMirDan f3539b2632 fix: proper wl name on connection key page (#1867)
* fix: proper wl name on connection key page

* some changings

* little change

* added missing import

* fix: proper wl default filename
2025-09-29 10:55:53 +08:00
MrMirDan 7a96c212f3 fix: rename user in search (#1847) 2025-09-29 10:51:52 +08:00
MrMirDan 2d5dc54e0f fix: keyboard navigation for text fields (#1879) 2025-09-29 10:50:57 +08:00
MrMirDan cef4c262e9 fix: keyboard fix for api 'connection key' buttons (#1872) 2025-09-29 10:50:18 +08:00
MrMirDan 34309261a8 fix: scrollbar always visible (#1877)
* fix: scrollbar always visible

* fix: scrollbar always visible on app split tunneling page
2025-09-29 10:49:19 +08:00
MrMirDan 657eeb40c7 fix: mirror error code link (#1863)
* fix: mirror error code link

* remake
2025-09-29 10:48:36 +08:00
MrMirDan b4938c2cc9 fix: default lang matching between app and OS (#1855)
* fix: default lang matching between app and OS

* remake

* fix: set default lang value
2025-09-29 10:47:54 +08:00
MrMirDan 524fefc5cb feat: warning on app split tunneling for windows (#1880) 2025-09-29 10:45:14 +08:00
Yaroslav 73f13404bb feat: add support for multiple scenes and handle URL contexts in iOS 13+ (#1889) 2025-09-29 10:40:58 +08:00
MrMirDan 5fc68cca83 fix: split tunneling restoration from backup (#1835) 2025-09-15 10:55:18 +08:00
Mitternacht822 fcb7b8fa8d fix: save/restore AmneziaDNS state (#1833) 2025-09-15 10:54:34 +08:00
aiamnezia a81e32ff95 fix: clean service/client logs in uninstall scripts (#1846)
- Windows (x64/x86):
  - Remove delegation to `AmneziaVPN.exe -c`
  - Delete `%ProgramData%\AmneziaVPN\log\AmneziaVPN-service.log`
  - Delete current user logs at `%AppData%\AmneziaVPN.ORG\AmneziaVPN\log`
  - Remove empty parent dirs (app/org, log)

- Linux:
  - Delete only `/var/log/AmneziaVPN/AmneziaVPN-service.log` (preserve `post-uninstall.log`)
  - Delete current user logs at `$HOME/.local/share/AmneziaVPN.ORG/AmneziaVPN/log`
2025-09-15 10:53:51 +08:00
albexk 4177c8e780 Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
2025-09-10 14:48:44 +03:00
albexk c897052107 chore: bump version (#1850) 2025-09-10 19:28:36 +08:00
vkamn 4d0efc7ea5 fix: remove duplicate m_vpnConnection delete from AmneziaApplication destructor (#1848) 2025-09-10 15:01:52 +08:00
Ivan a77842c9e3 feat: add server diagnostics script (#1837)
Co-authored-by: Ivan Istomin <istomin-ms@yandex.ru>
2025-09-09 19:33:35 +08:00
Mitternacht822 0ded9db780 refactor: use QCommandLineOption members for autostart/cleanup (#1820)
* refactor(app options): use QCommandLineOption members for autostart/cleanup

* fix(app): initialize QCommandLineOption members in ctor/field to avoid no-default-ctor build failures
2025-09-03 12:03:45 +08:00
Mitternacht822 58d480fcb5 fix: moved startMinimized to Q_Property (#1819) 2025-09-03 12:03:10 +08:00
aiamnezia 7154428d26 fix: sharing QR code size (#1830) 2025-09-03 11:58:36 +08:00
MrMirDan 02a52d0169 fix: full config default filename (#1831) 2025-09-03 11:57:30 +08:00
MrMirDan ec60764072 fix: rename/revoke user while in search on share page (#1787)
* fix: revoke user config

* fix: user renaming

* fix: revoke signal

* some fixes

* remaded fix
2025-09-03 11:56:08 +08:00
MrMirDan 17d2fa5532 fix: premium key duplication (#1818)
* ru translation fix

* crc saving

* little fix

* updated crc saving

* fix: added comparison by key

* remaded fix
2025-09-03 11:54:11 +08:00
MrMirDan 3ca8b534e8 fix: go to home page after first protocol manual installation (#1829) 2025-09-03 11:52:45 +08:00
MrMirDan e88f7c5e46 fix: index assignment (#1821) 2025-09-02 13:03:05 +08:00
albexk adcb90a7ac fix: fix import TimeZone 2025-08-27 19:48:51 +03:00
albexk 880057641e Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
#	client/android/utils/src/main/kotlin/Log.kt
2025-08-27 18:34:58 +03:00
MrMirDan 3ac5d7bd1f chore: ru translation update (#1815) 2025-08-27 18:37:43 +08:00
vkamn 19cad00a00 fix: minor ui fixes (#1817)
* fix: minor ui fixes with services list

* fix: fix page share connection headers and config description
2025-08-27 16:42:28 +08:00
vkamn 1ea716a163 fix: fix page share connection headers and config description 2025-08-27 16:41:20 +08:00
vkamn 4551659c2a fix: minor ui fixes with services list 2025-08-27 15:15:53 +08:00
MrMirDan c568bf8c24 chore: ru translation update (#1812)
* ru translation update

* fixes
2025-08-26 20:32:00 +08:00
vkamn a412d91105 feat: subscription expiration processing (#1814) 2025-08-26 20:31:41 +08:00
vkamn ad01f23bbe feat: add service description customization (#1811) 2025-08-26 12:17:37 +08:00
vkamn 656070b132 feat: add request id (#1809) 2025-08-25 22:05:00 +08:00
MrMirDan c907f5ca36 fix: removed service logs section for mobile platforms (#1810) 2025-08-25 22:04:48 +08:00
Mykola Baibuz 94a13b2b54 fix: set guid for windows tun2socks tun interface (#1808) 2025-08-25 11:03:42 +08:00
MrMirDan 169f11d9c7 chore: added trimming I's and J's params on save (#1774)
* trimming params on save

* removed unused code
2025-08-21 12:29:22 +08:00
vkamn 816dc3af95 feat: add ping before request to proxy (#1805) 2025-08-21 12:28:03 +08:00
Mykola Baibuz b802863de5 fix: check for empty secondary DNS (#1799) 2025-08-20 14:19:22 +08:00
vkamn 8dc2a4b76c fix: fixed switcher behavior (#1801) 2025-08-20 13:01:09 +08:00
vkamn beb1c6dbf2 feat: added cache for proxy bypass (#1797) 2025-08-20 13:00:35 +08:00
vkamn 3eb06916c7 chore: bump version (#1802)
* chore: bump version

* fix: fixed ios build
2025-08-20 13:00:20 +08:00
Cyril Anisimov 30d0f84a4f fix: fixed focus view and reverse focus change in headers (#1791)
* fix: add view movement on changing the focus in backwards direction

* fix: return value in isFirstFocusItemInHeader function
2025-08-20 12:59:57 +08:00
Mykola Baibuz 251f2aa5db fix: remove double disconnect for Win IPSec (#1800) 2025-08-20 12:58:39 +08:00
albexk 765964bdb2 Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
2025-07-31 13:25:03 +03:00
albexk 584ead4a33 Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
2025-07-25 12:36:35 +03:00
albexk 5f86957556 Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
2025-07-10 16:04:34 +03:00
albexk 215b417da3 Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
2025-07-07 16:01:29 +03:00
albexk 97dd76ea7b Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
2025-05-26 12:33:40 +03:00
albexk 35d762ccf9 Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
2025-05-20 11:42:18 +03:00
albexk d0086de333 Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
2025-03-21 16:11:42 +03:00
albexk 9eef389cdb Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
2025-03-10 19:20:13 +03:00
albexk b9a0364b3b Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
2025-03-04 12:15:37 +03:00
albexk 0b63efcd67 Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
2025-02-25 18:33:47 +03:00
albexk b3060187ef Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
2025-02-24 17:43:02 +03:00
albexk b6118e4c9f Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
2025-02-13 20:34:31 +03:00
pokamest 4135eb0110 Merge pull request #1389 from amnezia-vpn/android7-pull
pull fixes
2025-01-31 23:02:15 +01:00
pokamest 936adcafa6 Merge branch 'android-7' into android7-pull 2025-01-31 23:00:45 +01:00
albexk ad62fc4aca Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
2025-01-15 17:33:36 +03:00
albexk aaa12e51f0 Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
#	client/android/src/org/amnezia/vpn/AmneziaActivity.kt
2025-01-13 19:14:03 +03:00
albexk a440ddd7e7 Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
2024-11-08 11:54:44 +03:00
albexk 0e571af728 Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
2024-10-25 19:24:54 +03:00
albexk e46e983bb8 Merge branch 'dev' into android-7
# Conflicts:
#	CMakeLists.txt
2024-10-24 19:39:00 +03:00
albexk 1ae9a57b57 Fix merge 2024-10-22 14:19:44 +03:00
albexk 5e80223e7a Merge branch 'dev' into android-7
# Conflicts:
#	.github/workflows/deploy.yml
#	CMakeLists.txt
#	client/android/src/org/amnezia/vpn/AmneziaActivity.kt
#	client/android/src/org/amnezia/vpn/AmneziaVpnService.kt
#	client/android/src/org/amnezia/vpn/ServiceNotification.kt
#	client/android/utils/src/main/kotlin/Log.kt
#	client/android/utils/src/main/kotlin/net/NetworkState.kt
2024-10-22 12:51:10 +03:00
albexk 4d6174f5d8 Fix GA 2024-10-02 15:25:25 +03:00
albexk ce9a062bea Update version code to separate versions for new and old Androids 2024-10-02 15:20:55 +03:00
albexk 4910dcfa96 Set the maximum version of Androids to 7.1 (API 25) 2024-10-02 15:18:05 +03:00
albexk 744b45476c Bump version to 4.8.2.0 2024-10-01 20:23:06 +03:00
albexk ca43c6e69e Up Qt to 6.7.3 2024-10-01 20:21:56 +03:00
124 changed files with 3331 additions and 2665 deletions
+2 -2
View File
@@ -470,7 +470,7 @@ jobs:
env: env:
ANDROID_BUILD_PLATFORM: android-34 ANDROID_BUILD_PLATFORM: android-34
QT_VERSION: 6.7.3 QT_VERSION: 6.6.3
QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools' QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools'
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
@@ -487,7 +487,7 @@ jobs:
version: ${{ env.QT_VERSION }} version: ${{ env.QT_VERSION }}
host: 'linux' host: 'linux'
target: 'desktop' target: 'desktop'
arch: 'linux_gcc_64' arch: 'gcc_64'
modules: ${{ env.QT_MODULES }} modules: ${{ env.QT_MODULES }}
dir: ${{ runner.temp }} dir: ${{ runner.temp }}
py7zrversion: '==0.22.*' py7zrversion: '==0.22.*'
+1
View File
@@ -9,6 +9,7 @@ deploy/build_32/*
deploy/build_64/* deploy/build_64/*
winbuild*.bat winbuild*.bat
.cache/ .cache/
.vscode/
# Qt-es # Qt-es
+2 -2
View File
@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
set(PROJECT AmneziaVPN) set(PROJECT AmneziaVPN)
set(AMNEZIAVPN_VERSION 4.8.9.2) set(AMNEZIAVPN_VERSION 4.8.11.0)
project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION} project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
DESCRIPTION "AmneziaVPN" DESCRIPTION "AmneziaVPN"
@@ -12,7 +12,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
set(RELEASE_DATE "${CURRENT_DATE}") set(RELEASE_DATE "${CURRENT_DATE}")
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
set(APP_ANDROID_VERSION_CODE 2092) set(APP_ANDROID_VERSION_CODE 1095)
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(MZ_PLATFORM_NAME "linux") set(MZ_PLATFORM_NAME "linux")
+7 -18
View File
@@ -25,7 +25,9 @@
#include <QtQuick/QQuickWindow> // for QQuickWindow #include <QtQuick/QQuickWindow> // for QQuickWindow
#include <QWindow> // for qobject_cast<QWindow*> #include <QWindow> // for qobject_cast<QWindow*>
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); setQuitOnLastWindowClosed(false);
@@ -51,18 +53,8 @@ AmneziaApplication::AmneziaApplication(int &argc, char *argv[]) : AMNEZIA_BASE_C
AmneziaApplication::~AmneziaApplication() AmneziaApplication::~AmneziaApplication()
{ {
if (m_vpnConnection) {
QMetaObject::invokeMethod(m_vpnConnection.get(), "disconnectFromVpn", Qt::QueuedConnection);
QMetaObject::invokeMethod(m_vpnConnection.get(), "deleteLater", Qt::QueuedConnection);
}
m_vpnConnectionThread.quit(); m_vpnConnectionThread.quit();
if (!m_vpnConnectionThread.wait(5000)) {
m_vpnConnectionThread.terminate();
m_vpnConnectionThread.wait();
}
if (m_engine) { if (m_engine) {
QObject::disconnect(m_engine, 0, 0, 0); QObject::disconnect(m_engine, 0, 0, 0);
delete m_engine; delete m_engine;
@@ -119,7 +111,7 @@ void AmneziaApplication::init()
Logger::setServiceLogsEnabled(enabled); Logger::setServiceLogsEnabled(enabled);
#ifdef Q_OS_WIN //TODO #ifdef Q_OS_WIN //TODO
if (m_parser.isSet("a")) if (m_parser.isSet(m_optAutostart))
m_coreController->pageController()->showOnStartup(); m_coreController->pageController()->showOnStartup();
else else
emit m_coreController->pageController()->raiseMainWindow(); emit m_coreController->pageController()->raiseMainWindow();
@@ -187,15 +179,12 @@ bool AmneziaApplication::parseCommands()
m_parser.addHelpOption(); m_parser.addHelpOption();
m_parser.addVersionOption(); m_parser.addVersionOption();
QCommandLineOption c_autostart { { "a", "autostart" }, "System autostart" }; m_parser.addOption(m_optAutostart);
m_parser.addOption(c_autostart); m_parser.addOption(m_optCleanup);
QCommandLineOption c_cleanup { { "c", "cleanup" }, "Cleanup logs" };
m_parser.addOption(c_cleanup);
m_parser.process(*this); m_parser.process(*this);
if (m_parser.isSet(c_cleanup)) { if (m_parser.isSet(m_optCleanup)) {
Logger::cleanUp(); Logger::cleanUp();
QTimer::singleShot(100, this, [this] { quit(); }); QTimer::singleShot(100, this, [this] { quit(); });
exec(); exec();
+3
View File
@@ -56,6 +56,9 @@ private:
QCommandLineParser m_parser; QCommandLineParser m_parser;
QCommandLineOption m_optAutostart;
QCommandLineOption m_optCleanup;
QSharedPointer<VpnConnection> m_vpnConnection; QSharedPointer<VpnConnection> m_vpnConnection;
QThread m_vpnConnectionThread; QThread m_vpnConnectionThread;
+6
View File
@@ -3,10 +3,13 @@
<manifest <manifest
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="org.amnezia.vpn"
android:versionName="-- %%INSERT_VERSION_NAME%% --" android:versionName="-- %%INSERT_VERSION_NAME%% --"
android:versionCode="-- %%INSERT_VERSION_CODE%% --" android:versionCode="-- %%INSERT_VERSION_CODE%% --"
android:installLocation="auto"> android:installLocation="auto">
<uses-sdk android:maxSdkVersion="25" />
<uses-feature android:name="android.hardware.camera" android:required="false" /> <uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.any" android:required="false" /> <uses-feature android:name="android.hardware.camera.any" android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" /> <uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
@@ -67,6 +70,9 @@
android:name="android.app.lib_name" android:name="android.app.lib_name"
android:value="-- %%INSERT_APP_LIB_NAME%% --" /> android:value="-- %%INSERT_APP_LIB_NAME%% --" />
<meta-data
android:name="android.app.extract_android_style"
android:value="minimal" />
</activity> </activity>
<activity <activity
+1 -1
View File
@@ -33,7 +33,7 @@ android.library.defaults.buildfeatures.androidresources=false
# For development copy and set local values for these parameters in local.properties # For development copy and set local values for these parameters in local.properties
#androidCompileSdkVersion=android-34 #androidCompileSdkVersion=android-34
#androidBuildToolsVersion=34.0.0 #androidBuildToolsVersion=34.0.0
#qtMinSdkVersion=26 #qtMinSdkVersion=24
#qtTargetSdkVersion=34 #qtTargetSdkVersion=34
#androidNdkVersion=26.1.10909125 #androidNdkVersion=26.1.10909125
#qtTargetAbiList=x86_64 #qtTargetAbiList=x86_64
@@ -183,14 +183,6 @@ class OpenVpnClient(
// Never called more than once per tun_builder session. // Never called more than once per tun_builder session.
override fun tun_builder_set_proxy_http(host: String, port: Int): Boolean { override fun tun_builder_set_proxy_http(host: String, port: Int): Boolean {
Log.d(TAG, "tun_builder_set_proxy_http: $host, $port") Log.d(TAG, "tun_builder_set_proxy_http: $host, $port")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
try {
configBuilder.setHttpProxy(ProxyInfo.buildDirectProxy(host, port))
} catch (e: Exception) {
Log.e(TAG, "Could not set proxy: ${e.message}")
return false
}
}
return true return true
} }
@@ -113,12 +113,7 @@ abstract class Protocol {
Log.d(TAG, "addRoute: $inetNetwork") Log.d(TAG, "addRoute: $inetNetwork")
vpnBuilder.addRoute(inetNetwork) vpnBuilder.addRoute(inetNetwork)
} else { } else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { Log.e(TAG, "Trying to exclude route $inetNetwork on old Android")
Log.d(TAG, "excludeRoute: $inetNetwork")
vpnBuilder.excludeRoute(inetNetwork)
} else {
Log.e(TAG, "Trying to exclude route $inetNetwork on old Android")
}
} }
} }
@@ -135,13 +130,6 @@ abstract class Protocol {
Log.d(TAG, "setMtu: ${config.mtu}") Log.d(TAG, "setMtu: ${config.mtu}")
vpnBuilder.setMtu(config.mtu) vpnBuilder.setMtu(config.mtu)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
config.httpProxy?.let {
Log.d(TAG, "setHttpProxy: $it")
vpnBuilder.setHttpProxy(it)
}
}
if (config.allowAllAF) { if (config.allowAllAF) {
Log.d(TAG, "allowFamily") Log.d(TAG, "allowFamily")
vpnBuilder.allowFamily(OsConstants.AF_INET) vpnBuilder.allowFamily(OsConstants.AF_INET)
@@ -151,8 +139,6 @@ abstract class Protocol {
Log.d(TAG, "setBlocking: ${config.blockingMode}") Log.d(TAG, "setBlocking: ${config.blockingMode}")
vpnBuilder.setBlocking(config.blockingMode) vpnBuilder.setBlocking(config.blockingMode)
vpnBuilder.setUnderlyingNetworks(null) vpnBuilder.setUnderlyingNetworks(null)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
vpnBuilder.setMetered(false)
} }
} }
@@ -145,7 +145,7 @@ open class ProtocolConfig protected constructor(
} }
// for older versions of Android, build a list of subnets without excluded routes // for older versions of Android, build a list of subnets without excluded routes
// and add them to routes // and add them to routes
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU && routes.any { !it.include }) { if (routes.any { !it.include }) {
val ipRangeSet = IpRangeSet() val ipRangeSet = IpRangeSet()
routes.forEach { routes.forEach {
if (it.include) ipRangeSet.add(IpRange(it.inetNetwork)) if (it.include) ipRangeSet.add(IpRange(it.inetNetwork))
+1 -1
View File
@@ -21,5 +21,5 @@ android {
} }
dependencies { dependencies {
api(fileTree(mapOf("dir" to "../libs", "include" to listOf("*.jar")))) implementation(fileTree(mapOf("dir" to "../libs", "include" to listOf("*.jar"))))
} }
@@ -3,9 +3,7 @@ package org.amnezia.vpn
import android.Manifest import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog import android.app.AlertDialog
import android.app.NotificationManager
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.BroadcastReceiver
import android.content.ComponentName import android.content.ComponentName
import android.content.Intent import android.content.Intent
import android.content.Intent.EXTRA_MIME_TYPES import android.content.Intent.EXTRA_MIME_TYPES
@@ -77,7 +75,6 @@ class AmneziaActivity : QtActivity() {
private var isWaitingStatus = true private var isWaitingStatus = true
private var isServiceConnected = false private var isServiceConnected = false
private var isInBoundState = false private var isInBoundState = false
private var notificationStateReceiver: BroadcastReceiver? = null
private lateinit var vpnServiceMessenger: IpcMessenger private lateinit var vpnServiceMessenger: IpcMessenger
private var pfd: ParcelFileDescriptor? = null private var pfd: ParcelFileDescriptor? = null
@@ -186,7 +183,6 @@ class AmneziaActivity : QtActivity() {
doBindService() doBindService()
} }
) )
registerBroadcastReceivers()
intent?.let(::processIntent) intent?.let(::processIntent)
runBlocking { vpnProto = proto.await() } runBlocking { vpnProto = proto.await() }
} }
@@ -202,26 +198,6 @@ class AmneziaActivity : QtActivity() {
} }
} }
private fun registerBroadcastReceivers() {
notificationStateReceiver = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
registerBroadcastReceiver(
arrayOf(
NotificationManager.ACTION_NOTIFICATION_CHANNEL_BLOCK_STATE_CHANGED,
NotificationManager.ACTION_APP_BLOCK_STATE_CHANGED
)
) {
Log.v(
TAG, "Notification state changed: ${it?.action}, blocked = " +
"${it?.getBooleanExtra(NotificationManager.EXTRA_BLOCKED_STATE, false)}"
)
mainScope.launch {
qtInitialized.await()
QtAndroidController.onNotificationStateChanged()
}
}
} else null
}
override fun onNewIntent(intent: Intent?) { override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent) super.onNewIntent(intent)
Log.v(TAG, "onNewIntent: $intent") Log.v(TAG, "onNewIntent: $intent")
@@ -267,8 +243,6 @@ class AmneziaActivity : QtActivity() {
override fun onDestroy() { override fun onDestroy() {
Log.d(TAG, "Destroy Amnezia activity") Log.d(TAG, "Destroy Amnezia activity")
unregisterBroadcastReceiver(notificationStateReceiver)
notificationStateReceiver = null
mainScope.cancel() mainScope.cancel()
super.onDestroy() super.onDestroy()
} }
@@ -747,7 +721,7 @@ class AmneziaActivity : QtActivity() {
} }
@Suppress("unused") @Suppress("unused")
fun isNotificationPermissionGranted(): Boolean = applicationContext.isNotificationPermissionGranted() fun isNotificationPermissionGranted(): Boolean = true
@Suppress("unused") @Suppress("unused")
fun requestNotificationPermission() { fun requestNotificationPermission() {
@@ -847,67 +821,6 @@ class AmneziaActivity : QtActivity() {
0, 0, 1.0f, 1.0f, 0, 0, 0,0 0, 0, 1.0f, 1.0f, 0, 0, 0,0
) )
// workaround for a bug in Qt that causes the mouse click event not to be handled
// also disable right-click, as it causes the application to crash
private var lastButtonState = 0
private fun MotionEvent.fixCopy(): MotionEvent = MotionEvent.obtain(
downTime,
eventTime,
action,
pointerCount,
(0 until pointerCount).map { i ->
MotionEvent.PointerProperties().apply {
getPointerProperties(i, this)
}
}.toTypedArray(),
(0 until pointerCount).map { i ->
MotionEvent.PointerCoords().apply {
getPointerCoords(i, this)
}
}.toTypedArray(),
metaState,
MotionEvent.BUTTON_PRIMARY,
xPrecision,
yPrecision,
deviceId,
edgeFlags,
source,
flags
)
private fun handleMouseEvent(ev: MotionEvent, superDispatch: (MotionEvent?) -> Boolean): Boolean {
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
lastButtonState = ev.buttonState
if (ev.buttonState == MotionEvent.BUTTON_SECONDARY) return true
}
MotionEvent.ACTION_UP -> {
when (lastButtonState) {
MotionEvent.BUTTON_SECONDARY -> return true
MotionEvent.BUTTON_PRIMARY -> {
val modEvent = ev.fixCopy()
return superDispatch(modEvent).apply { modEvent.recycle() }
}
}
}
}
return superDispatch(ev)
}
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
Log.v(TAG, "dispatchTouch: $ev")
if (ev != null && ev.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) {
return handleMouseEvent(ev) { super.dispatchTouchEvent(it) }
}
return super.dispatchTouchEvent(ev)
}
override fun dispatchTrackballEvent(ev: MotionEvent?): Boolean {
ev?.let { return handleMouseEvent(ev) { super.dispatchTrackballEvent(it) }}
return super.dispatchTrackballEvent(ev)
}
/** /**
* Utils methods * Utils methods
*/ */
@@ -1,12 +1,9 @@
package org.amnezia.vpn package org.amnezia.vpn
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.ComponentName import android.content.ComponentName
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection import android.content.ServiceConnection
import android.net.VpnService import android.net.VpnService
import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.os.Messenger import android.os.Messenger
import android.service.quicksettings.Tile import android.service.quicksettings.Tile
@@ -148,7 +145,8 @@ class AmneziaTileService : TileService() {
Intent(this, AmneziaActivity::class.java).apply { Intent(this, AmneziaActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}.also { }.also {
startActivityAndCollapseCompat(it) @Suppress("DEPRECATION")
startActivityAndCollapse(it)
} }
} }
} }
@@ -192,7 +190,8 @@ class AmneziaTileService : TileService() {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
putExtra(EXTRA_PROTOCOL, vpnProto) putExtra(EXTRA_PROTOCOL, vpnProto)
}.also { }.also {
startActivityAndCollapseCompat(it) @Suppress("DEPRECATION")
startActivityAndCollapse(it)
} }
false false
} else { } else {
@@ -216,23 +215,6 @@ class AmneziaTileService : TileService() {
private fun stopVpn() = vpnServiceMessenger.send(Action.DISCONNECT) private fun stopVpn() = vpnServiceMessenger.send(Action.DISCONNECT)
@SuppressLint("StartActivityAndCollapseDeprecated")
private fun startActivityAndCollapseCompat(intent: Intent) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startActivityAndCollapse(
PendingIntent.getActivity(
applicationContext,
0,
intent,
PendingIntent.FLAG_IMMUTABLE
)
)
} else {
@Suppress("DEPRECATION")
startActivityAndCollapse(intent)
}
}
private fun updateVpnState(state: ProtocolState) = private fun updateVpnState(state: ProtocolState) =
scope.launch { VpnStateStore.store { it.copy(protocolState = state) } } scope.launch { VpnStateStore.store { it.copy(protocolState = state) } }
@@ -249,17 +231,14 @@ class AmneziaTileService : TileService() {
when (val protocolState = vpnState.protocolState) { when (val protocolState = vpnState.protocolState) {
CONNECTED -> { CONNECTED -> {
state = Tile.STATE_ACTIVE state = Tile.STATE_ACTIVE
subtitleCompat = null
} }
DISCONNECTED, UNKNOWN -> { DISCONNECTED, UNKNOWN -> {
state = Tile.STATE_INACTIVE state = Tile.STATE_INACTIVE
subtitleCompat = null
} }
CONNECTING, DISCONNECTING, RECONNECTING -> { CONNECTING, DISCONNECTING, RECONNECTING -> {
state = Tile.STATE_UNAVAILABLE state = Tile.STATE_UNAVAILABLE
subtitleCompat = getString(protocolState)
} }
} }
updateTile() updateTile()
@@ -267,17 +246,4 @@ class AmneziaTileService : TileService() {
// double update to fix weird visual glitches // double update to fix weird visual glitches
tile.updateTile() tile.updateTile()
} }
private var Tile.subtitleCompat: CharSequence?
set(value) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
this.subtitle = value
}
}
get() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
return this.subtitle
}
return null
}
} }
@@ -3,14 +3,10 @@ package org.amnezia.vpn
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.ActivityManager import android.app.ActivityManager
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE
import android.app.NotificationManager
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED
import android.net.VpnService import android.net.VpnService
import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.IBinder import android.os.IBinder
import android.os.Looper import android.os.Looper
@@ -104,7 +100,6 @@ open class AmneziaVpnService : VpnService() {
private lateinit var networkState: NetworkState private lateinit var networkState: NetworkState
private lateinit var trafficStats: TrafficStats private lateinit var trafficStats: TrafficStats
private var controlReceiver: BroadcastReceiver? = null private var controlReceiver: BroadcastReceiver? = null
private var notificationStateReceiver: BroadcastReceiver? = null
private var screenOnReceiver: BroadcastReceiver? = null private var screenOnReceiver: BroadcastReceiver? = null
private var screenOffReceiver: BroadcastReceiver? = null private var screenOffReceiver: BroadcastReceiver? = null
private val clientMessengers = ConcurrentHashMap<Messenger, IpcMessenger>() private val clientMessengers = ConcurrentHashMap<Messenger, IpcMessenger>()
@@ -189,16 +184,6 @@ open class AmneziaVpnService : VpnService() {
Messenger(actionMessageHandler) Messenger(actionMessageHandler)
} }
/**
* Notification setup
*/
private val foregroundServiceTypeCompat
get() = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE -> FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> FOREGROUND_SERVICE_TYPE_MANIFEST
else -> 0
}
private val serviceNotification: ServiceNotification by lazy(NONE) { ServiceNotification(this) } private val serviceNotification: ServiceNotification by lazy(NONE) { ServiceNotification(this) }
/** /**
@@ -232,7 +217,7 @@ open class AmneziaVpnService : VpnService() {
ServiceCompat.startForeground( ServiceCompat.startForeground(
this, NOTIFICATION_ID, this, NOTIFICATION_ID,
serviceNotification.buildNotification(serverName, vpnProto?.label, protocolState.value), serviceNotification.buildNotification(serverName, vpnProto?.label, protocolState.value),
foregroundServiceTypeCompat 0
) )
return START_REDELIVER_INTENT return START_REDELIVER_INTENT
} }
@@ -309,23 +294,6 @@ open class AmneziaVpnService : VpnService() {
} }
} }
notificationStateReceiver = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
registerBroadcastReceiver(
arrayOf(
NotificationManager.ACTION_NOTIFICATION_CHANNEL_BLOCK_STATE_CHANGED,
NotificationManager.ACTION_APP_BLOCK_STATE_CHANGED
)
) {
val state = it?.getBooleanExtra(NotificationManager.EXTRA_BLOCKED_STATE, false)
Log.v(TAG, "Notification state changed: ${it?.action}, blocked = $state")
if (state == false) {
enableNotification()
} else {
disableNotification()
}
}
} else null
registerScreenStateBroadcastReceivers() registerScreenStateBroadcastReceivers()
} }
@@ -353,10 +321,8 @@ open class AmneziaVpnService : VpnService() {
private fun unregisterBroadcastReceivers() { private fun unregisterBroadcastReceivers() {
Log.d(TAG, "Unregister broadcast receivers") Log.d(TAG, "Unregister broadcast receivers")
unregisterBroadcastReceiver(controlReceiver) unregisterBroadcastReceiver(controlReceiver)
unregisterBroadcastReceiver(notificationStateReceiver)
unregisterScreenStateBroadcastReceivers() unregisterScreenStateBroadcastReceivers()
controlReceiver = null controlReceiver = null
notificationStateReceiver = null
} }
/** /**
@@ -1,19 +1,15 @@
package org.amnezia.vpn package org.amnezia.vpn
import android.Manifest.permission
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Notification import android.app.Notification
import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import androidx.core.app.NotificationChannelCompat.Builder import androidx.core.app.NotificationChannelCompat.Builder
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.Action import androidx.core.app.NotificationCompat.Action
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import org.amnezia.vpn.protocol.ProtocolState import org.amnezia.vpn.protocol.ProtocolState
import org.amnezia.vpn.protocol.ProtocolState.CONNECTED import org.amnezia.vpn.protocol.ProtocolState.CONNECTED
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
@@ -85,27 +81,17 @@ class ServiceNotification(private val context: Context) {
.setSubText(getSpeedString(speed)) .setSubText(getSpeedString(speed))
.build() .build()
fun isNotificationEnabled(): Boolean { fun isNotificationEnabled(): Boolean = notificationManager.areNotificationsEnabled()
if (!context.isNotificationPermissionGranted()) return false
if (!notificationManager.areNotificationsEnabled()) return false
return notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID)?.let {
it.importance != NotificationManager.IMPORTANCE_NONE
} ?: true
}
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
fun updateNotification(serverName: String?, protocol: String?, state: ProtocolState) { fun updateNotification(serverName: String?, protocol: String?, state: ProtocolState) {
if (context.isNotificationPermissionGranted()) { Log.v(TAG, "Update notification: $serverName, $state")
Log.v(TAG, "Update notification: $serverName, $state") notificationManager.notify(NOTIFICATION_ID, buildNotification(serverName, protocol, state))
notificationManager.notify(NOTIFICATION_ID, buildNotification(serverName, protocol, state))
}
} }
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
fun updateSpeed(speed: TrafficData) { fun updateSpeed(speed: TrafficData) {
if (context.isNotificationPermissionGranted()) { notificationManager.notify(NOTIFICATION_ID, buildNotification(speed))
notificationManager.notify(NOTIFICATION_ID, buildNotification(speed))
}
} }
private fun getSpeedString(traffic: TrafficData) = private fun getSpeedString(traffic: TrafficData) =
@@ -166,8 +152,3 @@ class ServiceNotification(private val context: Context) {
} }
} }
} }
fun Context.isNotificationPermissionGranted(): Boolean =
Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU ||
ContextCompat.checkSelfPermission(this, permission.POST_NOTIFICATIONS) ==
PackageManager.PERMISSION_GRANTED
@@ -7,7 +7,6 @@ import android.content.Intent
import android.content.res.Configuration.UI_MODE_NIGHT_MASK import android.content.res.Configuration.UI_MODE_NIGHT_MASK
import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.net.VpnService import android.net.VpnService
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.Settings import android.provider.Settings
import android.widget.Toast import android.widget.Toast
@@ -31,12 +30,9 @@ class VpnRequestActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
Log.d(TAG, "Start request activity") Log.d(TAG, "Start request activity")
vpnProto = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { @Suppress("DEPRECATION")
intent.extras?.getSerializable(EXTRA_PROTOCOL, VpnProto::class.java) vpnProto = intent.extras?.getSerializable(EXTRA_PROTOCOL) as VpnProto
} else {
@Suppress("DEPRECATION")
intent.extras?.getSerializable(EXTRA_PROTOCOL) as VpnProto
}
val requestIntent = VpnService.prepare(applicationContext) val requestIntent = VpnService.prepare(applicationContext)
if (requestIntent != null) { if (requestIntent != null) {
if (getSystemService<KeyguardManager>()!!.isKeyguardLocked) { if (getSystemService<KeyguardManager>()!!.isKeyguardLocked) {
+11 -6
View File
@@ -1,6 +1,9 @@
package org.amnezia.vpn.util package org.amnezia.vpn.util
import android.content.Context import android.content.Context
import android.icu.text.DateFormat
import android.icu.text.SimpleDateFormat
import android.icu.util.TimeZone
import android.os.Build import android.os.Build
import android.os.Process import android.os.Process
import java.io.File import java.io.File
@@ -8,10 +11,8 @@ import java.io.IOException
import java.io.RandomAccessFile import java.io.RandomAccessFile
import java.nio.channels.FileChannel import java.nio.channels.FileChannel
import java.nio.channels.FileLock import java.nio.channels.FileLock
import java.time.LocalDateTime import java.util.Date
import java.time.format.DateTimeFormatter import java.util.Locale
import java.time.ZonedDateTime
import java.time.ZoneOffset
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import org.amnezia.vpn.util.Log.Priority.D import org.amnezia.vpn.util.Log.Priority.D
import org.amnezia.vpn.util.Log.Priority.E import org.amnezia.vpn.util.Log.Priority.E
@@ -39,7 +40,11 @@ private const val LOG_MAX_FILE_SIZE = 1024 * 1024
* | | | create a report and/or terminate the process | * | | | create a report and/or terminate the process |
*/ */
object Log { object Log {
private val dateTimeFormat: DateTimeFormatter = DateTimeFormatter.ofPattern(DATE_TIME_PATTERN) private val dateTimeFormat = object : ThreadLocal<DateFormat>() {
override fun initialValue(): DateFormat = SimpleDateFormat(DATE_TIME_PATTERN, Locale.US).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
}
private lateinit var logDir: File private lateinit var logDir: File
private val logFile: File by lazy { File(logDir, LOG_FILE_NAME) } private val logFile: File by lazy { File(logDir, LOG_FILE_NAME) }
@@ -137,7 +142,7 @@ object Log {
} }
private fun formatLogMsg(tag: String, msg: String, priority: Priority): String { private fun formatLogMsg(tag: String, msg: String, priority: Priority): String {
val utcDate = ZonedDateTime.now(ZoneOffset.UTC).format(dateTimeFormat) val utcDate = dateTimeFormat.get()?.format(Date())
return "${utcDate}Z ${Process.myPid()} ${Process.myTid()} $priority [${Thread.currentThread().name}] " + return "${utcDate}Z ${Process.myPid()} ${Process.myTid()} $priority [${Thread.currentThread().name}] " +
"$tag: $msg\n" "$tag: $msg\n"
} }
@@ -8,11 +8,9 @@ import android.net.NetworkCapabilities
import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
import android.net.NetworkRequest import android.net.NetworkRequest
import android.os.Build
import android.os.Handler import android.os.Handler
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import kotlin.LazyThreadSafetyMode.NONE import kotlin.LazyThreadSafetyMode.NONE
import kotlinx.coroutines.delay
import org.amnezia.vpn.util.Log import org.amnezia.vpn.util.Log
private const val TAG = "NetworkState" private const val TAG = "NetworkState"
@@ -47,7 +45,9 @@ class NetworkState(
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
Log.v(TAG, "onCapabilitiesChanged: $network, $networkCapabilities") Log.v(TAG, "onCapabilitiesChanged: $network, $networkCapabilities")
checkNetworkState(network, networkCapabilities) handler.post {
checkNetworkState(network, networkCapabilities)
}
} }
private fun checkNetworkState(network: Network, networkCapabilities: NetworkCapabilities) { private fun checkNetworkState(network: Network, networkCapabilities: NetworkCapabilities) {
@@ -76,33 +76,10 @@ class NetworkState(
} }
} }
suspend fun bindNetworkListener() { fun bindNetworkListener() {
if (isListenerBound) return if (isListenerBound) return
Log.d(TAG, "Bind network listener") Log.d(TAG, "Bind network listener")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { connectivityManager.requestNetwork(networkRequest, networkCallback)
connectivityManager.registerBestMatchingNetworkCallback(networkRequest, networkCallback, handler)
} else {
val numberAttempts = 300
var attemptCount = 0
while(true) {
try {
connectivityManager.requestNetwork(networkRequest, networkCallback, handler)
break
} catch (e: SecurityException) {
Log.e(TAG, "Failed to bind network listener: $e")
// Android 11 bug: https://issuetracker.google.com/issues/175055271
if (e.message?.startsWith("Package android does not belong to") == true) {
if (++attemptCount > numberAttempts) {
throw e
}
delay(1000)
continue
} else {
throw e
}
}
}
}
isListenerBound = true isListenerBound = true
} }
@@ -1,7 +1,6 @@
package org.amnezia.vpn.util.net package org.amnezia.vpn.util.net
import android.net.TrafficStats import android.net.TrafficStats
import android.os.Build
import android.os.Process import android.os.Process
import android.os.SystemClock import android.os.SystemClock
import kotlin.math.roundToLong import kotlin.math.roundToLong
@@ -17,18 +16,12 @@ class TrafficStats {
private var lastTrafficData = TrafficData.ZERO private var lastTrafficData = TrafficData.ZERO
private var lastTimestamp = 0L private var lastTimestamp = 0L
private val getTrafficDataCompat: () -> TrafficData = private val getTrafficDataCompat: () -> TrafficData = run {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val uid = Process.myUid()
val iface = "tun0" fun(): TrafficData {
fun(): TrafficData { return TrafficData(TrafficStats.getUidRxBytes(uid), TrafficStats.getUidTxBytes(uid))
return TrafficData(TrafficStats.getRxBytes(iface), TrafficStats.getTxBytes(iface))
}
} else {
val uid = Process.myUid()
fun(): TrafficData {
return TrafficData(TrafficStats.getUidRxBytes(uid), TrafficStats.getUidTxBytes(uid))
}
} }
}
fun reset() { fun reset() {
lastTrafficData = getTrafficDataCompat() lastTrafficData = getTrafficDataCompat()
+1 -1
View File
@@ -1,6 +1,6 @@
message("Client android ${CMAKE_ANDROID_ARCH_ABI} build") message("Client android ${CMAKE_ANDROID_ARCH_ABI} build")
set(APP_ANDROID_MIN_SDK 26) set(APP_ANDROID_MIN_SDK 24)
set(ANDROID_PLATFORM "android-${APP_ANDROID_MIN_SDK}" CACHE STRING set(ANDROID_PLATFORM "android-${APP_ANDROID_MIN_SDK}" CACHE STRING
"The minimum API level supported by the application or library" FORCE) "The minimum API level supported by the application or library" FORCE)
+16 -4
View File
@@ -46,6 +46,7 @@ set(SOURCES ${SOURCES}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.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_subdirectory(ios/networkextension)
add_dependencies(${PROJECT} networkextension) add_dependencies(${PROJECT} networkextension)
set_property(TARGET ${PROJECT} PROPERTY XCODE_EMBED_FRAMEWORKS set(OPENVPN_FRAMEWORK_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-ios")
"${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-ios/OpenVPNAdapter.framework" 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/) set_property(TARGET ${PROJECT} PROPERTY XCODE_EMBED_FRAMEWORKS "${OPENVPN_EMBEDDED_FRAMEWORKS}")
target_link_libraries("networkextension" PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-ios/OpenVPNAdapter.framework") 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}")
+20 -2
View File
@@ -83,12 +83,30 @@ QString OpenVpnConfigurator::createConfig(const ServerCredentials &credentials,
return ""; 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_CA_CERT", connData.caCert);
config.replace("$OPENVPN_CLIENT_CERT", connData.clientCert); config.replace("$OPENVPN_CLIENT_CERT", connData.clientCert);
config.replace("$OPENVPN_PRIV_KEY", connData.privKey); config.replace("$OPENVPN_PRIV_KEY", connData.privKey);
if (config.contains("$OPENVPN_TA_KEY")) { if (config.contains("$OPENVPN_TA_KEY")) {
config.replace("$OPENVPN_TA_KEY", connData.taKey); config.replace("$OPENVPN_TA_KEY", sanitizeStaticKey(connData.taKey));
} else { } else {
config.replace("<tls-auth>", ""); config.replace("<tls-auth>", "");
config.replace("</tls-auth>", ""); config.replace("</tls-auth>", "");
@@ -117,7 +135,7 @@ QString OpenVpnConfigurator::processConfigWithLocalSettings(const QPair<QString,
if (!isApiConfig) { if (!isApiConfig) {
QRegularExpression regex("redirect-gateway.*"); QRegularExpression regex("redirect-gateway.*");
config.replace(regex, ""); config.replace(regex, "");
// We don't use secondary DNS if primary DNS is AmneziaDNS // We don't use secondary DNS if primary DNS is AmneziaDNS
if (dns.first.contains(protocols::dns::amneziaDnsIp)) { if (dns.first.contains(protocols::dns::amneziaDnsIp)) {
QRegularExpression dnsRegex("dhcp-option DNS " + dns.second); QRegularExpression dnsRegex("dhcp-option DNS " + dns.second);
+4
View File
@@ -64,6 +64,10 @@ namespace apiDefs
constexpr QLatin1String id("id"); constexpr QLatin1String id("id");
constexpr QLatin1String orderId("order_id"); constexpr QLatin1String orderId("order_id");
constexpr QLatin1String migrationCode("migration_code"); constexpr QLatin1String migrationCode("migration_code");
constexpr QLatin1String transactionId("transaction_id");
constexpr QLatin1String userCountryCode("user_country_code");
} }
const int requestTimeoutMsecs = 12 * 1000; // 12 secs const int requestTimeoutMsecs = 12 * 1000; // 12 secs
+61 -13
View File
@@ -23,7 +23,7 @@ namespace
bool apiUtils::isSubscriptionExpired(const QString &subscriptionEndDate) bool apiUtils::isSubscriptionExpired(const QString &subscriptionEndDate)
{ {
QDateTime now = QDateTime::currentDateTime(); QDateTime now = QDateTime::currentDateTimeUtc();
QDateTime endDate = QDateTime::fromString(subscriptionEndDate, Qt::ISODateWithMs); QDateTime endDate = QDateTime::fromString(subscriptionEndDate, Qt::ISODateWithMs);
return endDate < now; return endDate < now;
} }
@@ -82,7 +82,9 @@ apiDefs::ConfigSource apiUtils::getConfigSource(const QJsonObject &serverConfigO
return static_cast<apiDefs::ConfigSource>(serverConfigObject.value(apiDefs::key::configVersion).toInt()); return static_cast<apiDefs::ConfigSource>(serverConfigObject.value(apiDefs::key::configVersion).toInt());
} }
amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &sslErrors, QNetworkReply *reply) amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &sslErrors, const QString &replyErrorString,
const QNetworkReply::NetworkError &replyError, const int httpStatusCode,
const QByteArray &responseBody)
{ {
const int httpStatusCodeConflict = 409; const int httpStatusCodeConflict = 409;
const int httpStatusCodeNotFound = 404; const int httpStatusCodeNotFound = 404;
@@ -90,21 +92,19 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
if (!sslErrors.empty()) { if (!sslErrors.empty()) {
qDebug().noquote() << sslErrors; qDebug().noquote() << sslErrors;
return amnezia::ErrorCode::ApiConfigSslError; return amnezia::ErrorCode::ApiConfigSslError;
} else if (reply->error() == QNetworkReply::NoError) { } else if (replyError == QNetworkReply::NoError) {
return amnezia::ErrorCode::NoError; return amnezia::ErrorCode::NoError;
} else if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError } else if (replyError == QNetworkReply::NetworkError::OperationCanceledError
|| reply->error() == QNetworkReply::NetworkError::TimeoutError) { || replyError == QNetworkReply::NetworkError::TimeoutError) {
qDebug() << reply->error(); qDebug() << replyError;
return amnezia::ErrorCode::ApiConfigTimeoutError; return amnezia::ErrorCode::ApiConfigTimeoutError;
} else if (reply->error() == QNetworkReply::NetworkError::OperationNotImplementedError) { } else if (replyError == QNetworkReply::NetworkError::OperationNotImplementedError) {
qDebug() << reply->error(); qDebug() << replyError;
return amnezia::ErrorCode::ApiUpdateRequestError; return amnezia::ErrorCode::ApiUpdateRequestError;
} else { } else {
QString err = reply->errorString(); qDebug() << QString::fromUtf8(responseBody);
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); qDebug() << replyError;
qDebug() << QString::fromUtf8(reply->readAll()); qDebug() << replyErrorString;
qDebug() << reply->error();
qDebug() << err;
qDebug() << httpStatusCode; qDebug() << httpStatusCode;
if (httpStatusCode == httpStatusCodeConflict) { if (httpStatusCode == httpStatusCodeConflict) {
return amnezia::ErrorCode::ApiConfigLimitError; return amnezia::ErrorCode::ApiConfigLimitError;
@@ -162,3 +162,51 @@ QString apiUtils::getPremiumV1VpnKey(const QJsonObject &serverConfigObject)
return QString("vpn://%1").arg(QString(signedData.toBase64(QByteArray::Base64UrlEncoding))); 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<int>(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;
}
+4 -1
View File
@@ -18,9 +18,12 @@ namespace apiUtils
apiDefs::ConfigType getConfigType(const QJsonObject &serverConfigObject); apiDefs::ConfigType getConfigType(const QJsonObject &serverConfigObject);
apiDefs::ConfigSource getConfigSource(const QJsonObject &serverConfigObject); apiDefs::ConfigSource getConfigSource(const QJsonObject &serverConfigObject);
amnezia::ErrorCode checkNetworkReplyErrors(const QList<QSslError> &sslErrors, QNetworkReply *reply); amnezia::ErrorCode checkNetworkReplyErrors(const QList<QSslError> &sslErrors, const QString &replyErrorString,
const QNetworkReply::NetworkError &replyError, const int httpStatusCode,
const QByteArray &responseBody);
QString getPremiumV1VpnKey(const QJsonObject &serverConfigObject); QString getPremiumV1VpnKey(const QJsonObject &serverConfigObject);
QString getPremiumV2VpnKey(const QJsonObject &serverConfigObject);
} }
#endif // APIUTILS_H #endif // APIUTILS_H
+12 -5
View File
@@ -26,9 +26,8 @@ CoreController::CoreController(const QSharedPointer<VpnConnection> &vpnConnectio
initNotificationHandler(); initNotificationHandler();
auto locale = m_settings->getAppLanguage();
m_translator.reset(new QTranslator()); m_translator.reset(new QTranslator());
updateTranslator(locale); updateTranslator(m_settings->getAppLanguage());
} }
void CoreController::initModels() void CoreController::initModels()
@@ -100,6 +99,9 @@ void CoreController::initModels()
m_apiDevicesModel.reset(new ApiDevicesModel(m_settings, this)); m_apiDevicesModel.reset(new ApiDevicesModel(m_settings, this));
m_engine->rootContext()->setContextProperty("ApiDevicesModel", m_apiDevicesModel.get()); 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() void CoreController::initControllers()
@@ -155,8 +157,8 @@ void CoreController::initControllers()
m_apiPremV1MigrationController.reset(new ApiPremV1MigrationController(m_serversModel, m_settings, this)); m_apiPremV1MigrationController.reset(new ApiPremV1MigrationController(m_serversModel, m_settings, this));
m_engine->rootContext()->setContextProperty("ApiPremV1MigrationController", m_apiPremV1MigrationController.get()); m_engine->rootContext()->setContextProperty("ApiPremV1MigrationController", m_apiPremV1MigrationController.get());
m_transferController.reset(new TransferController(m_settings, m_serversModel, m_exportController.get())); m_apiNewsController.reset(new ApiNewsController(m_newsModel, m_settings, m_serversModel, this));
m_engine->rootContext()->setContextProperty("TransferController", m_transferController.get()); m_engine->rootContext()->setContextProperty("ApiNewsController", m_apiNewsController.get());
} }
void CoreController::initAndroidController() void CoreController::initAndroidController()
@@ -236,7 +238,7 @@ void CoreController::initSignalHandlers()
void CoreController::initNotificationHandler() void CoreController::initNotificationHandler()
{ {
#ifndef Q_OS_ANDROID #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
m_notificationHandler.reset(NotificationHandler::create(nullptr)); m_notificationHandler.reset(NotificationHandler::create(nullptr));
connect(m_vpnConnection.get(), &VpnConnection::connectionStateChanged, m_notificationHandler.get(), connect(m_vpnConnection.get(), &VpnConnection::connectionStateChanged, m_notificationHandler.get(),
@@ -320,6 +322,11 @@ void CoreController::initContainerModelUpdateHandler()
connect(m_serversModel.get(), &ServersModel::containersUpdated, m_containersModel.get(), &ContainersModel::updateModel); connect(m_serversModel.get(), &ServersModel::containersUpdated, m_containersModel.get(), &ContainersModel::updateModel);
connect(m_serversModel.get(), &ServersModel::defaultServerContainersUpdated, m_defaultServerContainersModel.get(), connect(m_serversModel.get(), &ServersModel::defaultServerContainersUpdated, m_defaultServerContainersModel.get(),
&ContainersModel::updateModel); &ContainersModel::updateModel);
connect(m_serversModel.get(), &ServersModel::gatewayStacksExpanded, this, [this]() {
if (m_serversModel->hasServersFromGatewayApi()) {
m_apiNewsController->fetchNews();
}
});
m_serversModel->resetModel(); m_serversModel->resetModel();
} }
+7 -5
View File
@@ -5,13 +5,14 @@
#include <QQmlContext> #include <QQmlContext>
#include <QThread> #include <QThread>
#ifndef Q_OS_ANDROID #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
#include "ui/systemtray_notificationhandler.h" #include "ui/systemtray_notificationhandler.h"
#endif #endif
#include "ui/controllers/api/apiConfigsController.h" #include "ui/controllers/api/apiConfigsController.h"
#include "ui/controllers/api/apiSettingsController.h" #include "ui/controllers/api/apiSettingsController.h"
#include "ui/controllers/api/apiPremV1MigrationController.h" #include "ui/controllers/api/apiPremV1MigrationController.h"
#include "ui/controllers/api/apiNewsController.h"
#include "ui/controllers/appSplitTunnelingController.h" #include "ui/controllers/appSplitTunnelingController.h"
#include "ui/controllers/allowedDnsController.h" #include "ui/controllers/allowedDnsController.h"
#include "ui/controllers/connectionController.h" #include "ui/controllers/connectionController.h"
@@ -23,7 +24,6 @@
#include "ui/controllers/settingsController.h" #include "ui/controllers/settingsController.h"
#include "ui/controllers/sitesController.h" #include "ui/controllers/sitesController.h"
#include "ui/controllers/systemController.h" #include "ui/controllers/systemController.h"
#include "ui/controllers/transferController.h"
#include "ui/models/allowed_dns_model.h" #include "ui/models/allowed_dns_model.h"
#include "ui/models/containers_model.h" #include "ui/models/containers_model.h"
@@ -48,8 +48,9 @@
#include "ui/models/services/sftpConfigModel.h" #include "ui/models/services/sftpConfigModel.h"
#include "ui/models/services/socks5ProxyConfigModel.h" #include "ui/models/services/socks5ProxyConfigModel.h"
#include "ui/models/sites_model.h" #include "ui/models/sites_model.h"
#include "ui/models/newsModel.h"
#ifndef Q_OS_ANDROID #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
#include "ui/notificationhandler.h" #include "ui/notificationhandler.h"
#endif #endif
@@ -98,7 +99,7 @@ private:
QSharedPointer<VpnConnection> m_vpnConnection; QSharedPointer<VpnConnection> m_vpnConnection;
QSharedPointer<QTranslator> m_translator; QSharedPointer<QTranslator> m_translator;
#ifndef Q_OS_ANDROID #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
QScopedPointer<NotificationHandler> m_notificationHandler; QScopedPointer<NotificationHandler> m_notificationHandler;
#endif #endif
@@ -119,6 +120,7 @@ private:
QScopedPointer<ApiSettingsController> m_apiSettingsController; QScopedPointer<ApiSettingsController> m_apiSettingsController;
QScopedPointer<ApiConfigsController> m_apiConfigsController; QScopedPointer<ApiConfigsController> m_apiConfigsController;
QScopedPointer<ApiPremV1MigrationController> m_apiPremV1MigrationController; QScopedPointer<ApiPremV1MigrationController> m_apiPremV1MigrationController;
QScopedPointer<ApiNewsController> m_apiNewsController;
QSharedPointer<ContainersModel> m_containersModel; QSharedPointer<ContainersModel> m_containersModel;
QSharedPointer<ContainersModel> m_defaultServerContainersModel; QSharedPointer<ContainersModel> m_defaultServerContainersModel;
@@ -126,6 +128,7 @@ private:
QSharedPointer<LanguageModel> m_languageModel; QSharedPointer<LanguageModel> m_languageModel;
QSharedPointer<ProtocolsModel> m_protocolsModel; QSharedPointer<ProtocolsModel> m_protocolsModel;
QSharedPointer<SitesModel> m_sitesModel; QSharedPointer<SitesModel> m_sitesModel;
QSharedPointer<NewsModel> m_newsModel;
QSharedPointer<AllowedDnsModel> m_allowedDnsModel; QSharedPointer<AllowedDnsModel> m_allowedDnsModel;
QSharedPointer<AppSplitTunnelingModel> m_appSplitTunnelingModel; QSharedPointer<AppSplitTunnelingModel> m_appSplitTunnelingModel;
QSharedPointer<ClientManagementModel> m_clientManagementModel; QSharedPointer<ClientManagementModel> m_clientManagementModel;
@@ -146,7 +149,6 @@ private:
#endif #endif
QScopedPointer<SftpConfigModel> m_sftpConfigModel; QScopedPointer<SftpConfigModel> m_sftpConfigModel;
QScopedPointer<Socks5ProxyConfigModel> m_socks5ConfigModel; QScopedPointer<Socks5ProxyConfigModel> m_socks5ConfigModel;
QScopedPointer<TransferController> m_transferController;
}; };
#endif // CORECONTROLLER_H #endif // CORECONTROLLER_H
+101 -96
View File
@@ -8,7 +8,6 @@
#include <QJsonObject> #include <QJsonObject>
#include <QNetworkReply> #include <QNetworkReply>
#include <QUrl> #include <QUrl>
#include <QRegularExpression>
#include "QBlockCipher.h" #include "QBlockCipher.h"
#include "QRsa.h" #include "QRsa.h"
@@ -51,68 +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.setUrl(QString(endpoint).arg(m_gatewayEndpoint));
// 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<QSslError> sslErrors;
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &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<QSslError> &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) ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody)
{ {
#ifdef Q_OS_IOS #ifdef Q_OS_IOS
@@ -123,8 +60,9 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
QNetworkRequest request; QNetworkRequest request;
request.setTransferTimeout(m_requestTimeoutMsecs); request.setTransferTimeout(m_requestTimeoutMsecs);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader(QString("X-Client-Request-ID").toUtf8(), QUuid::createUuid().toString(QUuid::WithoutBraces).toUtf8());
request.setUrl(endpoint.arg(m_gatewayEndpoint)); request.setUrl(endpoint.arg(m_proxyUrl.isEmpty() ? m_gatewayEndpoint : m_proxyUrl));
// bypass killSwitch exceptions for API-gateway // bypass killSwitch exceptions for API-gateway
#ifdef AMNEZIA_DESKTOP #ifdef AMNEZIA_DESKTOP
@@ -187,29 +125,37 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
wait.exec(); wait.exec();
QByteArray encryptedResponseBody = reply->readAll(); 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) { auto requestFunction = [&request, &encryptedResponseBody, &requestBody](const QString &url) {
request.setUrl(url); request.setUrl(url);
return amnApp->networkManager()->post(request, QJsonDocument(requestBody).toJson()); return amnApp->networkManager()->post(request, QJsonDocument(requestBody).toJson());
}; };
auto replyProcessingFunction = [&encryptedResponseBody, &reply, &sslErrors, &key, &iv, &salt, auto replyProcessingFunction = [&encryptedResponseBody, &replyErrorString, &replyError, &httpStatusCode, &sslErrors, &key, &iv,
this](QNetworkReply *nestedReply, const QList<QSslError> &nestedSslErrors) { &salt, this](QNetworkReply *reply, const QList<QSslError> &nestedSslErrors) {
encryptedResponseBody = nestedReply->readAll(); encryptedResponseBody = reply->readAll();
reply = nestedReply; replyErrorString = reply->errorString();
if (!sslErrors.isEmpty() || shouldBypassProxy(nestedReply, encryptedResponseBody, true, key, iv, salt)) { replyError = reply->error();
httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (!sslErrors.isEmpty() || shouldBypassProxy(replyError, encryptedResponseBody, true, key, iv, salt)) {
sslErrors = nestedSslErrors; sslErrors = nestedSslErrors;
return false; return false;
} }
return true; 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); auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, encryptedResponseBody);
reply->deleteLater();
if (errorCode) { if (errorCode) {
return errorCode; return errorCode;
} }
@@ -224,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; QNetworkRequest request;
request.setTransferTimeout(m_requestTimeoutMsecs); request.setTransferTimeout(m_requestTimeoutMsecs);
@@ -234,15 +180,26 @@ QStringList GatewayController::getProxyUrls()
QList<QSslError> sslErrors; QList<QSslError> sslErrors;
QNetworkReply *reply; QNetworkReply *reply;
QStringList proxyStorageUrls; QStringList baseUrls;
if (m_isDevEnvironment) { if (m_isDevEnvironment) {
proxyStorageUrls = QString(DEV_S3_ENDPOINT).split(", "); baseUrls = QString(DEV_S3_ENDPOINT).split(", ");
} else { } 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; QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
QStringList proxyStorageUrls;
if (!serviceType.isEmpty()) {
for (const auto &baseUrl : baseUrls) {
QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8();
proxyStorageUrls.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) + ".json");
}
}
for (const auto &baseUrl : baseUrls) {
proxyStorageUrls.push_back(baseUrl + "endpoints.json");
}
for (const auto &proxyStorageUrl : proxyStorageUrls) { for (const auto &proxyStorageUrl : proxyStorageUrls) {
request.setUrl(proxyStorageUrl); request.setUrl(proxyStorageUrl);
reply = amnApp->networkManager()->get(request); reply = amnApp->networkManager()->get(request);
@@ -287,7 +244,10 @@ QStringList GatewayController::getProxyUrls()
} }
return endpoints; return endpoints;
} else { } 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"; qDebug() << "go to the next storage endpoint";
reply->deleteLater(); reply->deleteLater();
@@ -296,33 +256,33 @@ QStringList GatewayController::getProxyUrls()
return {}; return {};
} }
bool GatewayController::shouldBypassProxy(QNetworkReply *reply, const QByteArray &responseBody, bool checkEncryption, const QByteArray &key, bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &responseBody,
const QByteArray &iv, const QByteArray &salt) 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() << "timeout occurred";
qDebug() << reply->error(); qDebug() << replyError;
return true; return true;
} else if (responseBody.contains("html")) { } else if (responseBody.contains("html")) {
qDebug() << "the response contains an html tag"; qDebug() << "the response contains an html tag";
return true; return true;
} else if (reply->error() == QNetworkReply::NetworkError::ContentNotFoundError) { } else if (replyError == QNetworkReply::NetworkError::ContentNotFoundError) {
if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2) if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2)
|| responseBody.contains(errorResponsePattern3)) { || responseBody.contains(errorResponsePattern3)) {
return false; return false;
} else { } else {
qDebug() << reply->error(); qDebug() << replyError;
return true; return true;
} }
} else if (reply->error() == QNetworkReply::NetworkError::OperationNotImplementedError) { } else if (replyError == QNetworkReply::NetworkError::OperationNotImplementedError) {
if (responseBody.contains(updateRequestResponsePattern)) { if (responseBody.contains(updateRequestResponsePattern)) {
return false; return false;
} else { } else {
qDebug() << reply->error(); qDebug() << replyError;
return true; return true;
} }
} else if (reply->error() != QNetworkReply::NetworkError::NoError) { } else if (replyError != QNetworkReply::NetworkError::NoError) {
qDebug() << reply->error(); qDebug() << replyError;
return true; return true;
} else if (checkEncryption) { } else if (checkEncryption) {
try { try {
@@ -336,29 +296,74 @@ bool GatewayController::shouldBypassProxy(QNetworkReply *reply, const QByteArray
return false; return false;
} }
void GatewayController::bypassProxy(const QString &endpoint, QNetworkReply *reply, void GatewayController::bypassProxy(const QString &endpoint, const QString &serviceType, const QString &userCountryCode,
std::function<QNetworkReply *(const QString &url)> requestFunction, std::function<QNetworkReply *(const QString &url)> requestFunction,
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction) std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction)
{ {
QStringList proxyUrls = getProxyUrls(); QStringList proxyUrls = getProxyUrls(serviceType, userCountryCode);
std::random_device randomDevice; std::random_device randomDevice;
std::mt19937 generator(randomDevice()); std::mt19937 generator(randomDevice());
std::shuffle(proxyUrls.begin(), proxyUrls.end(), generator); std::shuffle(proxyUrls.begin(), proxyUrls.end(), generator);
QEventLoop wait;
QList<QSslError> sslErrors;
QByteArray responseBody; QByteArray responseBody;
for (const QString &proxyUrl : proxyUrls) { auto bypassFunction = [this](const QString &endpoint, const QString &proxyUrl,
std::function<QNetworkReply *(const QString &url)> requestFunction,
std::function<bool(QNetworkReply * reply, const QList<QSslError> &sslErrors)> replyProcessingFunction) {
QEventLoop wait;
QList<QSslError> sslErrors;
qDebug() << "go to the next proxy endpoint"; qDebug() << "go to the next proxy endpoint";
reply->deleteLater(); // delete the previous reply QNetworkReply *reply = requestFunction(endpoint.arg(proxyUrl));
reply = requestFunction(endpoint.arg(proxyUrl));
QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; }); connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec(); wait.exec();
if (replyProcessingFunction(reply, sslErrors)) { auto result = replyProcessingFunction(reply, sslErrors);
reply->deleteLater();
return result;
};
if (m_proxyUrl.isEmpty()) {
QNetworkRequest request;
request.setTransferTimeout(1000);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QEventLoop wait;
QList<QSslError> sslErrors;
QNetworkReply *reply;
for (const QString &proxyUrl : proxyUrls) {
request.setUrl(proxyUrl + "lmbd-health");
reply = amnApp->networkManager()->get(request);
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec();
if (reply->error() == QNetworkReply::NetworkError::NoError) {
reply->deleteLater();
m_proxyUrl = proxyUrl;
if (!m_proxyUrl.isEmpty()) {
break;
}
} else {
reply->deleteLater();
}
}
}
if (!m_proxyUrl.isEmpty()) {
if (bypassFunction(endpoint, m_proxyUrl, requestFunction, replyProcessingFunction)) {
return;
}
}
for (const QString &proxyUrl : proxyUrls) {
if (bypassFunction(endpoint, proxyUrl, requestFunction, replyProcessingFunction)) {
m_proxyUrl = proxyUrl;
break; break;
} }
} }
+7 -5
View File
@@ -18,20 +18,22 @@ public:
explicit GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs, explicit GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
const bool isStrictKillSwitchEnabled, QObject *parent = nullptr); 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); amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody);
private: private:
QStringList getProxyUrls(); QStringList getProxyUrls(const QString &serviceType, const QString &userCountryCode);
bool shouldBypassProxy(QNetworkReply *reply, const QByteArray &responseBody, bool checkEncryption, const QByteArray &key = "", bool shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &responseBody, bool checkEncryption,
const QByteArray &iv = "", const QByteArray &salt = ""); const QByteArray &key = "", const QByteArray &iv = "", const QByteArray &salt = "");
void bypassProxy(const QString &endpoint, QNetworkReply *reply, std::function<QNetworkReply *(const QString &url)> requestFunction, void bypassProxy(const QString &endpoint, const QString &serviceType, const QString &userCountryCode,
std::function<QNetworkReply *(const QString &url)> requestFunction,
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction); std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction);
int m_requestTimeoutMsecs; int m_requestTimeoutMsecs;
QString m_gatewayEndpoint; QString m_gatewayEndpoint;
bool m_isDevEnvironment = false; bool m_isDevEnvironment = false;
bool m_isStrictKillSwitchEnabled = false; bool m_isStrictKillSwitchEnabled = false;
inline static QString m_proxyUrl;
}; };
#endif // GATEWAYCONTROLLER_H #endif // GATEWAYCONTROLLER_H
+1
View File
@@ -120,6 +120,7 @@ namespace amnezia
ApiNotFoundError = 1109, ApiNotFoundError = 1109,
ApiMigrationError = 1110, ApiMigrationError = 1110,
ApiUpdateRequestError = 1111, ApiUpdateRequestError = 1111,
ApiSubscriptionExpiredError = 1112,
// QFile errors // QFile errors
OpenError = 1200, OpenError = 1200,
+1
View File
@@ -77,6 +77,7 @@ QString errorString(ErrorCode code) {
case (ErrorCode::ApiNotFoundError): errorMessage = QObject::tr("Error when retrieving configuration from API"); break; case (ErrorCode::ApiNotFoundError): errorMessage = QObject::tr("Error when retrieving configuration from API"); break;
case (ErrorCode::ApiMigrationError): errorMessage = QObject::tr("A migration error has occurred. Please contact our technical support"); break; case (ErrorCode::ApiMigrationError): errorMessage = QObject::tr("A migration error has occurred. Please contact our technical support"); break;
case (ErrorCode::ApiUpdateRequestError): errorMessage = QObject::tr("Please update the application to use this feature"); break; case (ErrorCode::ApiUpdateRequestError): errorMessage = QObject::tr("Please update the application to use this feature"); break;
case (ErrorCode::ApiSubscriptionExpiredError): errorMessage = QObject::tr("Your Amnezia Premium subscription has expired.\n Please check your email for renewal instructions.\n If you haven't received an email, please contact our support."); break;
// QFile errors // QFile errors
case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break; case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break;
+2 -2
View File
@@ -101,10 +101,10 @@ QString InterfaceConfig::toWgConf(const QMap<QString, QString>& extra) const {
out << "MTU = " << m_deviceMTU << "\n"; out << "MTU = " << m_deviceMTU << "\n";
} }
if (!m_primaryDnsServer.isNull()) { if (!m_primaryDnsServer.isEmpty()) {
QStringList dnsServers; QStringList dnsServers;
dnsServers.append(m_primaryDnsServer); dnsServers.append(m_primaryDnsServer);
if (!m_secondaryDnsServer.isNull()) { if (!m_secondaryDnsServer.isEmpty()) {
dnsServers.append(m_secondaryDnsServer); dnsServers.append(m_secondaryDnsServer);
} }
// If the DNS is not the Gateway, it's a user defined DNS // If the DNS is not the Gateway, it's a user defined DNS
@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<!-- Monitor -->
<rect x="2.5" y="3.5" width="14" height="10" rx="2" ry="2" stroke="#D7D8DB" stroke-width="1.8"/>
<line x1="9.5" y1="13.5" x2="9.5" y2="16.5" stroke="#D7D8DB" stroke-width="1.8" stroke-linecap="round"/>
<rect x="6" y="16.5" width="7" height="1.8" rx="0.9" ry="0.9" fill="#D7D8DB"/>
<!-- Phone -->
<rect x="17.5" y="6" width="4.5" height="9.5" rx="1.2" ry="1.2" stroke="#D7D8DB" stroke-width="1.8"/>
<circle cx="19.75" cy="14.2" r="0.6" fill="#D7D8DB"/>
</svg>

Before

Width:  |  Height:  |  Size: 628 B

+14
View File
@@ -0,0 +1,14 @@
<svg width="24" height="24" viewBox="0 0 74 74" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_4_34)">
<path d="M55.5 12.3333H18.5C15.0942 12.3333 12.3333 15.0943 12.3333 18.5V55.5C12.3333 58.9058 15.0942 61.6667 18.5 61.6667H55.5C58.9057 61.6667 61.6666 58.9058 61.6666 55.5V18.5C61.6666 15.0943 58.9057 12.3333 55.5 12.3333Z" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21.5833 24.6667H52.4167" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21.5833 37H52.4167" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21.5833 49.3333H40.0833" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="61.5" cy="12.5" r="15" fill="#FBB36B" stroke="#1C1D21" stroke-width="5"/>
</g>
<defs>
<clipPath id="clip0_4_34">
<rect width="74" height="74" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 982 B

+8
View File
@@ -0,0 +1,8 @@
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#CBCAC8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<!-- Основа газеты -->
<rect x="4" y="4" width="16" height="16" rx="2"/>
<!-- Линии текста -->
<line x1="7" y1="8" x2="17" y2="8"/>
<line x1="7" y1="12" x2="17" y2="12"/>
<line x1="7" y1="16" x2="13" y2="16"/>
</svg>

After

Width:  |  Height:  |  Size: 410 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.9 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 35 35" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="17.5" cy="17.5" r="15" fill="#FBB36B" stroke="#1C1D21" stroke-width="5"/>
</svg>

After

Width:  |  Height:  |  Size: 188 B

+26 -2
View File
@@ -32,17 +32,41 @@
<false/> <false/>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>AmneziaVPNLaunchScreen</string> <string>AmneziaVPNLaunchScreen</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>QIOSWindowSceneDelegate</string>
</dict>
</array>
</dict>
</dict>
<key>UIRequiredDeviceCapabilities</key> <key>UIRequiredDeviceCapabilities</key>
<array/> <array/>
<key>UIRequiresFullScreen</key> <key>UIRequiresFullScreen</key>
<true/> <false/>
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array> <array>
<string>UIInterfaceOrientationPortraitUpsideDown</string> <string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
</array> </array>
<key>UISupportedInterfaceOrientations~ipad</key> <key>UISupportedInterfaceOrientations~ipad</key>
<array/> <array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIUserInterfaceStyle</key> <key>UIUserInterfaceStyle</key>
<string>Light</string> <string>Light</string>
<key>com.wireguard.ios.app_group_id</key> <key>com.wireguard.ios.app_group_id</key>
+15 -15
View File
@@ -264,13 +264,13 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
&& !wgConfig.value(amnezia::config_key::junkPacketMaxSize).isUndefined() && !wgConfig.value(amnezia::config_key::junkPacketMaxSize).isUndefined()
&& !wgConfig.value(amnezia::config_key::initPacketJunkSize).isUndefined() && !wgConfig.value(amnezia::config_key::initPacketJunkSize).isUndefined()
&& !wgConfig.value(amnezia::config_key::responsePacketJunkSize).isUndefined() && !wgConfig.value(amnezia::config_key::responsePacketJunkSize).isUndefined()
&& !wgConfig.value(amnezia::config_key::cookieReplyPacketJunkSize).isUndefined() // && !wgConfig.value(amnezia::config_key::cookieReplyPacketJunkSize).isUndefined()
&& !wgConfig.value(amnezia::config_key::transportPacketJunkSize).isUndefined() // && !wgConfig.value(amnezia::config_key::transportPacketJunkSize).isUndefined()
&& !wgConfig.value(amnezia::config_key::initPacketMagicHeader).isUndefined() && !wgConfig.value(amnezia::config_key::initPacketMagicHeader).isUndefined()
&& !wgConfig.value(amnezia::config_key::responsePacketMagicHeader).isUndefined() && !wgConfig.value(amnezia::config_key::responsePacketMagicHeader).isUndefined()
&& !wgConfig.value(amnezia::config_key::underloadPacketMagicHeader).isUndefined() && !wgConfig.value(amnezia::config_key::underloadPacketMagicHeader).isUndefined()
&& !wgConfig.value(amnezia::config_key::transportPacketMagicHeader).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::specialJunk2).isUndefined()
&& !wgConfig.value(amnezia::config_key::specialJunk3).isUndefined() && !wgConfig.value(amnezia::config_key::specialJunk3).isUndefined()
&& !wgConfig.value(amnezia::config_key::specialJunk4).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::controlledJunk1).isUndefined()
&& !wgConfig.value(amnezia::config_key::controlledJunk2).isUndefined() && !wgConfig.value(amnezia::config_key::controlledJunk2).isUndefined()
&& !wgConfig.value(amnezia::config_key::controlledJunk3).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::junkPacketCount, wgConfig.value(amnezia::config_key::junkPacketCount));
json.insert(amnezia::config_key::junkPacketMinSize, wgConfig.value(amnezia::config_key::junkPacketMinSize)); 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::junkPacketMaxSize, wgConfig.value(amnezia::config_key::junkPacketMaxSize));
json.insert(amnezia::config_key::initPacketJunkSize, wgConfig.value(amnezia::config_key::initPacketJunkSize)); 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::responsePacketJunkSize, wgConfig.value(amnezia::config_key::responsePacketJunkSize));
json.insert(amnezia::config_key::cookieReplyPacketJunkSize, wgConfig.value(amnezia::config_key::cookieReplyPacketJunkSize)); // 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::transportPacketJunkSize, wgConfig.value(amnezia::config_key::transportPacketJunkSize));
json.insert(amnezia::config_key::initPacketMagicHeader, wgConfig.value(amnezia::config_key::initPacketMagicHeader)); 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::responsePacketMagicHeader, wgConfig.value(amnezia::config_key::responsePacketMagicHeader));
json.insert(amnezia::config_key::underloadPacketMagicHeader, wgConfig.value(amnezia::config_key::underloadPacketMagicHeader)); 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::transportPacketMagicHeader, wgConfig.value(amnezia::config_key::transportPacketMagicHeader));
json.insert(amnezia::config_key::specialJunk1, wgConfig.value(amnezia::config_key::specialJunk1)); // 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::specialJunk2, wgConfig.value(amnezia::config_key::specialJunk2));
json.insert(amnezia::config_key::specialJunk3, wgConfig.value(amnezia::config_key::specialJunk3)); // 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::specialJunk4, wgConfig.value(amnezia::config_key::specialJunk4));
json.insert(amnezia::config_key::specialJunk5, wgConfig.value(amnezia::config_key::specialJunk5)); // 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::controlledJunk1, wgConfig.value(amnezia::config_key::controlledJunk1));
json.insert(amnezia::config_key::controlledJunk2, wgConfig.value(amnezia::config_key::controlledJunk2)); // 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::controlledJunk3, wgConfig.value(amnezia::config_key::controlledJunk3));
json.insert(amnezia::config_key::specialHandshakeTimeout, wgConfig.value(amnezia::config_key::specialHandshakeTimeout)); // json.insert(amnezia::config_key::specialHandshakeTimeout, wgConfig.value(amnezia::config_key::specialHandshakeTimeout));
} }
write(json); write(json);
@@ -0,0 +1,82 @@
#import <UIKit/UIKit.h>
#import <objc/runtime.h>
#include <dispatch/dispatch.h>
#include <QByteArray>
#include <QFile>
#include <QString>
#include "ios_controller.h"
using SceneOpenURLContexts = void (*)(id, SEL, UIScene *, NSSet<UIOpenURLContext *> *);
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<UIOpenURLContext *> *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<SceneOpenURLContexts>(method_getImplementation(method));
method_setImplementation(method, reinterpret_cast<IMP>(amnezia_scene_openURLContexts));
} else {
const char *types = "v@:@@";
class_addMethod(cls, selector, reinterpret_cast<IMP>(amnezia_scene_openURLContexts), types);
}
}
@end
+35 -3
View File
@@ -2,7 +2,8 @@ import Foundation
import os.log import os.log
struct 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" private static let IsLoggingEnabledKey = "IsLoggingEnabled"
static var isLoggingEnabled: Bool { static var isLoggingEnabled: Bool {
@@ -77,9 +78,40 @@ struct Log {
static func log(_ type: OSLogType, title: String = "", message: String, url: URL = neLogURL) { static func log(_ type: OSLogType, title: String = "", message: String, url: URL = neLogURL) {
NSLog("\(title) \(message)") 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 date = Date()
let level = Record.Level(from: type) let level = Record.Level(from: type)
+55 -1
View File
@@ -1,22 +1,76 @@
import Foundation import Foundation
import os.log 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) { 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) { 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) neLog(type, title: "WG: \(title)", message: message)
} }
public func ovpnLog(_ type: OSLogType, title: String = "", message: String) { 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) neLog(type, title: "OVPN: \(title)", message: message)
} }
public func xrayLog(_ type: OSLogType, title: String = "", message: String) { 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) neLog(type, title: "XRAY: \(title)", message: message)
} }
public func neLog(_ type: OSLogType, title: String = "", message: String) { 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) Log.log(type, title: "NE: \(title)", message: message)
} }
@@ -1,6 +1,7 @@
import Foundation import Foundation
import NetworkExtension import NetworkExtension
import OpenVPNAdapter import OpenVPNAdapter
import CryptoKit
struct OpenVPNConfig: Decodable { struct OpenVPNConfig: Decodable {
let config: String let config: String
@@ -27,26 +28,83 @@ extension PacketTunnelProvider {
let ovpnConfiguration = Data(openVPNConfig.config.utf8) let ovpnConfiguration = Data(openVPNConfig.config.utf8)
setupAndlaunchOpenVPN(withConfig: ovpnConfiguration, completionHandler: completionHandler) setupAndlaunchOpenVPN(withConfig: ovpnConfiguration, completionHandler: completionHandler)
} catch { } catch {
ovpnLog(.error, message: "Can't parse config: \(error.localizedDescription)") ovpnLog(.error, message: "Can't parse OpenVPN config: \(error.localizedDescription)")
if let underlyingError = (error as NSError).userInfo[NSUnderlyingErrorKey] as? NSError {
ovpnLog(.error, message: "Can't parse config: \(underlyingError.localizedDescription)")
}
return 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, private func setupAndlaunchOpenVPN(withConfig ovpnConfiguration: Data,
withShadowSocks viaSS: Bool = false, withShadowSocks viaSS: Bool = false,
completionHandler: @escaping (Error?) -> Void) { completionHandler: @escaping (Error?) -> Void) {
ovpnLog(.info, message: "Setup and launch") 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("<tls-auth>")
let hasTlsAuthClose = configString.contains("</tls-auth>")
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: "<tls-auth>"),
let end = configString.range(of: "</tls-auth>", range: start.upperBound..<configString.endIndex) {
let keyBody = String(configString[start.upperBound..<end.lowerBound])
ovpnLog(.debug, title: "TLSAuthInline", message: keyBody)
let sanitizedLines = keyBody
.split(whereSeparator: { $0.isNewline })
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
.filter { !$0.hasPrefix("#") }
let sanitizedKey = sanitizedLines.joined(separator: "\n")
ovpnLog(.debug, title: "TLSAuthSanitized", message: sanitizedKey)
let sanitizedBlock = "<tls-auth>\n\(sanitizedKey)\n</tls-auth>"
configString.replaceSubrange(start.lowerBound..<end.upperBound, with: sanitizedBlock)
}
let normalizedConfig = configString.replacingOccurrences(of: "\r\n", with: "\n")
let sanitizedData = Data(normalizedConfig.utf8)
let configuration = OpenVPNConfiguration() let configuration = OpenVPNConfiguration()
configuration.fileContent = ovpnConfiguration configuration.fileContent = sanitizedData
if str.contains("cloak") { if configString.contains("cloak") {
configuration.setPTCloak() configuration.setPTCloak()
} }
@@ -57,6 +115,8 @@ extension PacketTunnelProvider {
evaluation = try ovpnAdapter?.apply(configuration: configuration) evaluation = try ovpnAdapter?.apply(configuration: configuration)
} catch { } catch {
let nsError = error as NSError
ovpnLog(.error, title: "ApplyConfig", message: "domain=\(nsError.domain) code=\(nsError.code) info=\(nsError.userInfo)")
completionHandler(error) completionHandler(error)
return return
} }
@@ -208,8 +268,11 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
// Handle errors thrown by the OpenVPN library // Handle errors thrown by the OpenVPN library
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleError error: Error) { func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleError error: Error) {
let nsError = error as NSError
logOpenVPNError(nsError)
// Handle only fatal errors // Handle only fatal errors
guard let fatal = (error as NSError).userInfo[OpenVPNAdapterErrorFatalKey] as? Bool, guard let fatal = nsError.userInfo[OpenVPNAdapterErrorFatalKey] as? Bool,
fatal == true else { return } fatal == true else { return }
if vpnReachability.isTracking { if vpnReachability.isTracking {
+37 -3
View File
@@ -29,12 +29,46 @@ const char* MessageKey::SplitTunnelSites = "SplitTunnelSites";
#if !MACOS_NE #if !MACOS_NE
static UIViewController* getViewController() { static UIViewController* getViewController() {
NSArray *windows = [[UIApplication sharedApplication]windows]; UIApplication *application = [UIApplication sharedApplication];
for (UIWindow *window in windows) {
if (window.isKeyWindow) { if (@available(iOS 13.0, *)) {
for (UIScene *scene in application.connectedScenes) {
if (scene.activationState != UISceneActivationStateForegroundActive) {
continue;
}
if (![scene isKindOfClass:[UIWindowScene class]]) {
continue;
}
UIWindowScene *windowScene = (UIWindowScene *)scene;
for (UIWindow *window in windowScene.windows) {
if (window.isKeyWindow && window.rootViewController) {
return window.rootViewController;
}
}
for (UIWindow *window in windowScene.windows) {
if (!window.isHidden && window.rootViewController) {
return window.rootViewController;
}
}
}
}
for (UIWindow *window in application.windows) {
if (window.isKeyWindow && window.rootViewController) {
return window.rootViewController; return window.rootViewController;
} }
} }
for (UIWindow *window in application.windows) {
if (window.rootViewController) {
return window.rootViewController;
}
}
return nil; return nil;
} }
#endif #endif
@@ -30,7 +30,6 @@ Ikev2Protocol::Ikev2Protocol(const QJsonObject &configuration, QObject* parent)
Ikev2Protocol::~Ikev2Protocol() Ikev2Protocol::~Ikev2Protocol()
{ {
qDebug() << "IpsecProtocol::~IpsecProtocol()"; qDebug() << "IpsecProtocol::~IpsecProtocol()";
disconnect_vpn();
Ikev2Protocol::stop(); Ikev2Protocol::stop();
} }
@@ -38,7 +37,7 @@ void Ikev2Protocol::stop()
{ {
setConnectionState(Vpn::ConnectionState::Disconnecting); setConnectionState(Vpn::ConnectionState::Disconnecting);
{ {
if (! disconnect_vpn() ){ if (!disconnect_vpn()){
qDebug()<<"We don't disconnect"; qDebug()<<"We don't disconnect";
setConnectionState(Vpn::ConnectionState::Error); setConnectionState(Vpn::ConnectionState::Error);
} }
@@ -311,7 +310,9 @@ bool Ikev2Protocol::connect_to_vpn(const QString & vpn_name){
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
bool Ikev2Protocol::disconnect_vpn(){ bool Ikev2Protocol::disconnect_vpn(){
if ( hRasConn != nullptr ){ if ( hRasConn != nullptr ){
if ( RasHangUp(hRasConn) != ERROR_SUCCESS) auto ret = RasHangUp(hRasConn);
qDebug() << "RasHangUp " << ret;
if (ret != ERROR_SUCCESS)
return false; return false;
} }
QThread::msleep(3000); QThread::msleep(3000);
+1
View File
@@ -169,6 +169,7 @@ void XrayProtocol::stop()
#if defined(Q_OS_WIN) || defined(Q_OS_LINUX) || defined(Q_OS_MACOS) #if defined(Q_OS_WIN) || defined(Q_OS_LINUX) || defined(Q_OS_MACOS)
IpcClient::Interface()->disableKillSwitch(); IpcClient::Interface()->disableKillSwitch();
IpcClient::Interface()->StartRoutingIpv6(); IpcClient::Interface()->StartRoutingIpv6();
IpcClient::Interface()->restoreResolvers();
#endif #endif
qDebug() << "XrayProtocol::stop()"; qDebug() << "XrayProtocol::stop()";
m_xrayProcess.disconnect(); m_xrayProcess.disconnect();
+6 -5
View File
@@ -35,6 +35,9 @@
<file>images/controls/mail.svg</file> <file>images/controls/mail.svg</file>
<file>images/controls/map-pin.svg</file> <file>images/controls/map-pin.svg</file>
<file>images/controls/more-vertical.svg</file> <file>images/controls/more-vertical.svg</file>
<file>images/controls/news.svg</file>
<file>images/controls/news-unread.svg</file>
<file>images/controls/unread-dot.svg</file>
<file>images/controls/plus.svg</file> <file>images/controls/plus.svg</file>
<file>images/controls/qr-code.svg</file> <file>images/controls/qr-code.svg</file>
<file>images/controls/radio-button-inner-circle-pressed.png</file> <file>images/controls/radio-button-inner-circle-pressed.png</file>
@@ -49,6 +52,7 @@
<file>images/controls/server.svg</file> <file>images/controls/server.svg</file>
<file>images/controls/settings-2.svg</file> <file>images/controls/settings-2.svg</file>
<file>images/controls/settings.svg</file> <file>images/controls/settings.svg</file>
<file>images/controls/settings-news.svg</file>
<file>images/controls/share-2.svg</file> <file>images/controls/share-2.svg</file>
<file>images/controls/split-tunneling.svg</file> <file>images/controls/split-tunneling.svg</file>
<file>images/controls/tag.svg</file> <file>images/controls/tag.svg</file>
@@ -212,6 +216,8 @@
<file>ui/qml/Pages2/PageSettingsServerServices.qml</file> <file>ui/qml/Pages2/PageSettingsServerServices.qml</file>
<file>ui/qml/Pages2/PageSettingsServersList.qml</file> <file>ui/qml/Pages2/PageSettingsServersList.qml</file>
<file>ui/qml/Pages2/PageSettingsSplitTunneling.qml</file> <file>ui/qml/Pages2/PageSettingsSplitTunneling.qml</file>
<file>ui/qml/Pages2/PageSettingsNewsNotifications.qml</file>
<file>ui/qml/Pages2/PageSettingsNewsDetail.qml</file>
<file>ui/qml/Pages2/PageProtocolAwgClientSettings.qml</file> <file>ui/qml/Pages2/PageProtocolAwgClientSettings.qml</file>
<file>ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml</file> <file>ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml</file>
<file>ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml</file> <file>ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml</file>
@@ -229,7 +235,6 @@
<file>ui/qml/Pages2/PageShare.qml</file> <file>ui/qml/Pages2/PageShare.qml</file>
<file>ui/qml/Pages2/PageShareFullAccess.qml</file> <file>ui/qml/Pages2/PageShareFullAccess.qml</file>
<file>ui/qml/Pages2/PageShareConnection.qml</file> <file>ui/qml/Pages2/PageShareConnection.qml</file>
<file>ui/qml/Pages2/PageTransferConfigViaQR.qml</file>
<file>ui/qml/Pages2/PageStart.qml</file> <file>ui/qml/Pages2/PageStart.qml</file>
<file>ui/qml/Components/RenameServerDrawer.qml</file> <file>ui/qml/Components/RenameServerDrawer.qml</file>
<file>ui/qml/Controls2/ListViewType.qml</file> <file>ui/qml/Controls2/ListViewType.qml</file>
@@ -237,11 +242,7 @@
<file>ui/qml/Pages2/PageSettingsApiInstructions.qml</file> <file>ui/qml/Pages2/PageSettingsApiInstructions.qml</file>
<file>ui/qml/Pages2/PageSettingsApiNativeConfigs.qml</file> <file>ui/qml/Pages2/PageSettingsApiNativeConfigs.qml</file>
<file>ui/qml/Pages2/PageSettingsApiDevices.qml</file> <file>ui/qml/Pages2/PageSettingsApiDevices.qml</file>
<file>ui/qml/Pages2/PageSettingsApiDevicesLimit.qml</file>
<file>ui/qml/Pages2/PageSettingsApiAddDeviceScan.qml</file>
<file>ui/qml/Pages2/PageSettingsApiAddDeviceConfirm.qml</file>
<file>images/controls/monitor.svg</file> <file>images/controls/monitor.svg</file>
<file>images/controls/monitor-with-phone.svg</file>
<file>ui/qml/Components/ApiPremV1MigrationDrawer.qml</file> <file>ui/qml/Components/ApiPremV1MigrationDrawer.qml</file>
<file>ui/qml/Components/ApiPremV1SubListDrawer.qml</file> <file>ui/qml/Components/ApiPremV1SubListDrawer.qml</file>
<file>ui/qml/Components/OtpCodeDrawer.qml</file> <file>ui/qml/Components/OtpCodeDrawer.qml</file>
+519
View File
@@ -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=""
+12 -2
View File
@@ -541,12 +541,12 @@ QString Settings::getGatewayEndpoint()
bool Settings::isDevGatewayEnv() bool Settings::isDevGatewayEnv()
{ {
return m_isDevGatewayEnv; return value("Conf/devGatewayEnv", false).toBool();
} }
void Settings::toggleDevGatewayEnv(bool enabled) void Settings::toggleDevGatewayEnv(bool enabled)
{ {
m_isDevGatewayEnv = enabled; setValue("Conf/devGatewayEnv", enabled);
} }
bool Settings::isHomeAdLabelVisible() bool Settings::isHomeAdLabelVisible()
@@ -578,3 +578,13 @@ void Settings::setAllowedDnsServers(const QStringList &servers)
{ {
setValue("Conf/allowedDnsServers", servers); setValue("Conf/allowedDnsServers", servers);
} }
QStringList Settings::readNewsIds() const
{
return value("News/readIds").toStringList();
}
void Settings::setReadNewsIds(const QStringList &ids)
{
setValue("News/readIds", ids);
}
+4 -2
View File
@@ -174,7 +174,7 @@ public:
QLocale getAppLanguage() QLocale getAppLanguage()
{ {
QString localeStr = m_settings.value("Conf/appLanguage").toString(); QString localeStr = m_settings.value("Conf/appLanguage", QLocale::system().name()).toString();
return QLocale(localeStr); return QLocale(localeStr);
}; };
void setAppLanguage(QLocale locale) void setAppLanguage(QLocale locale)
@@ -236,6 +236,9 @@ public:
QStringList allowedDnsServers() const; QStringList allowedDnsServers() const;
void setAllowedDnsServers(const QStringList &servers); void setAllowedDnsServers(const QStringList &servers);
QStringList readNewsIds() const;
void setReadNewsIds(const QStringList &ids);
signals: signals:
void saveLogsChanged(bool enabled); void saveLogsChanged(bool enabled);
void screenshotsEnabledChanged(bool enabled); void screenshotsEnabledChanged(bool enabled);
@@ -251,7 +254,6 @@ private:
mutable SecureQSettings m_settings; mutable SecureQSettings m_settings;
QString m_gatewayEndpoint; QString m_gatewayEndpoint;
bool m_isDevGatewayEnv = false;
}; };
#endif // SETTINGS_H #endif // SETTINGS_H
File diff suppressed because it is too large Load Diff
@@ -29,6 +29,7 @@ namespace
constexpr char uuid[] = "installation_uuid"; constexpr char uuid[] = "installation_uuid";
constexpr char osVersion[] = "os_version"; constexpr char osVersion[] = "os_version";
constexpr char appVersion[] = "app_version"; constexpr char appVersion[] = "app_version";
constexpr char appLanguage[] = "app_language";
constexpr char userCountryCode[] = "user_country_code"; constexpr char userCountryCode[] = "user_country_code";
constexpr char serverCountryCode[] = "server_country_code"; constexpr char serverCountryCode[] = "server_country_code";
@@ -43,6 +44,11 @@ namespace
constexpr char authData[] = "auth_data"; constexpr char authData[] = "auth_data";
constexpr char config[] = "config"; constexpr char config[] = "config";
constexpr char subscription[] = "subscription";
constexpr char endDate[] = "end_date";
constexpr char isConnectEvent[] = "is_connect_event";
} }
struct ProtocolData struct ProtocolData
@@ -163,7 +169,7 @@ namespace
auto clientProtocolConfig = auto clientProtocolConfig =
QJsonDocument::fromJson(serverProtocolConfig.value(config_key::last_config).toString().toUtf8()).object(); QJsonDocument::fromJson(serverProtocolConfig.value(config_key::last_config).toString().toUtf8()).object();
//TODO looks like this block can be removed after v1 configs EOL // TODO looks like this block can be removed after v1 configs EOL
serverProtocolConfig[config_key::junkPacketCount] = clientProtocolConfig.value(config_key::junkPacketCount); serverProtocolConfig[config_key::junkPacketCount] = clientProtocolConfig.value(config_key::junkPacketCount);
serverProtocolConfig[config_key::junkPacketMinSize] = clientProtocolConfig.value(config_key::junkPacketMinSize); serverProtocolConfig[config_key::junkPacketMinSize] = clientProtocolConfig.value(config_key::junkPacketMinSize);
@@ -223,6 +229,19 @@ namespace
return ErrorCode::NoError; return ErrorCode::NoError;
} }
bool isSubscriptionExpired(const QJsonObject &apiConfig)
{
auto subscription = apiConfig.value(configKey::subscription).toObject();
if (subscription.isEmpty()) {
return false;
}
auto subscriptionEndDate = subscription.value(configKey::endDate).toString();
if (apiUtils::isSubscriptionExpired(subscriptionEndDate)) {
return true;
}
return false;
}
} }
ApiConfigsController::ApiConfigsController(const QSharedPointer<ServersModel> &serversModel, ApiConfigsController::ApiConfigsController(const QSharedPointer<ServersModel> &serversModel,
@@ -232,6 +251,23 @@ ApiConfigsController::ApiConfigsController(const QSharedPointer<ServersModel> &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) bool ApiConfigsController::exportNativeConfig(const QString &serverCountryCode, const QString &fileName)
{ {
if (fileName.isEmpty()) { if (fileName.isEmpty()) {
@@ -242,6 +278,11 @@ bool ApiConfigsController::exportNativeConfig(const QString &serverCountryCode,
auto serverConfigObject = m_serversModel->getServerConfig(m_serversModel->getProcessedServerIndex()); auto serverConfigObject = m_serversModel->getServerConfig(m_serversModel->getProcessedServerIndex());
auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject(); auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject();
if (isSubscriptionExpired(apiConfigObject)) {
emit errorOccurred(ErrorCode::ApiSubscriptionExpiredError);
return false;
}
GatewayRequestData gatewayRequestData { QSysInfo::productType(), GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION), QString(APP_VERSION),
m_settings->getInstallationUuid(true), m_settings->getInstallationUuid(true),
@@ -277,6 +318,11 @@ bool ApiConfigsController::revokeNativeConfig(const QString &serverCountryCode)
auto serverConfigObject = m_serversModel->getServerConfig(m_serversModel->getProcessedServerIndex()); auto serverConfigObject = m_serversModel->getServerConfig(m_serversModel->getProcessedServerIndex());
auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject(); auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject();
if (isSubscriptionExpired(apiConfigObject)) {
emit errorOccurred(ErrorCode::ApiSubscriptionExpiredError);
return false;
}
GatewayRequestData gatewayRequestData { QSysInfo::productType(), GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION), QString(APP_VERSION),
m_settings->getInstallationUuid(true), m_settings->getInstallationUuid(true),
@@ -303,6 +349,13 @@ void ApiConfigsController::prepareVpnKeyExport()
auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject(); auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject();
auto vpnKey = apiConfigObject.value(apiDefs::key::vpnKey).toString(); 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; m_vpnKey = vpnKey;
vpnKey.replace("vpn://", ""); vpnKey.replace("vpn://", "");
@@ -322,6 +375,7 @@ bool ApiConfigsController::fillAvailableServices()
{ {
QJsonObject apiPayload; QJsonObject apiPayload;
apiPayload[configKey::osVersion] = QSysInfo::productType(); apiPayload[configKey::osVersion] = QSysInfo::productType();
apiPayload[configKey::appLanguage] = m_settings->getAppLanguage().name().split("_").first();
QByteArray responseBody; QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/services"), apiPayload, responseBody); ErrorCode errorCode = executeRequest(QString("%1v1/services"), apiPayload, responseBody);
@@ -396,6 +450,11 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
auto serverConfig = m_serversModel->getServerConfig(serverIndex); auto serverConfig = m_serversModel->getServerConfig(serverIndex);
auto apiConfig = serverConfig.value(configKey::apiConfig).toObject(); auto apiConfig = serverConfig.value(configKey::apiConfig).toObject();
if (isSubscriptionExpired(apiConfig)) {
emit errorOccurred(ErrorCode::ApiSubscriptionExpiredError);
return false;
}
GatewayRequestData gatewayRequestData { QSysInfo::productType(), GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION), QString(APP_VERSION),
m_settings->getInstallationUuid(true), m_settings->getInstallationUuid(true),
@@ -410,6 +469,10 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
QJsonObject apiPayload = gatewayRequestData.toJsonObject(); QJsonObject apiPayload = gatewayRequestData.toJsonObject();
appendProtocolDataToApiPayload(gatewayRequestData.serviceProtocol, protocolData, apiPayload); appendProtocolDataToApiPayload(gatewayRequestData.serviceProtocol, protocolData, apiPayload);
if (newCountryCode.isEmpty() && newCountryName.isEmpty() && !reloadServiceConfig) {
apiPayload.insert(configKey::isConnectEvent, true);
}
QByteArray responseBody; QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody); ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody);
@@ -429,6 +492,7 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
newServerConfig.insert(configKey::apiConfig, newApiConfig); newServerConfig.insert(configKey::apiConfig, newApiConfig);
newServerConfig.insert(configKey::authData, gatewayRequestData.authData); newServerConfig.insert(configKey::authData, gatewayRequestData.authData);
newServerConfig.insert(config_key::crc, serverConfig.value(config_key::crc));
if (serverConfig.value(config_key::nameOverriddenByUser).toBool()) { if (serverConfig.value(config_key::nameOverriddenByUser).toBool()) {
newServerConfig.insert(config_key::name, serverConfig.value(config_key::name)); newServerConfig.insert(config_key::name, serverConfig.value(config_key::name));
@@ -492,7 +556,7 @@ bool ApiConfigsController::updateServiceFromTelegram(const int serverIndex)
} }
} }
bool ApiConfigsController::deactivateDevice() bool ApiConfigsController::deactivateDevice(const bool isRemoveEvent)
{ {
auto serverIndex = m_serversModel->getProcessedServerIndex(); auto serverIndex = m_serversModel->getProcessedServerIndex();
auto serverConfigObject = m_serversModel->getServerConfig(serverIndex); auto serverConfigObject = m_serversModel->getServerConfig(serverIndex);
@@ -502,6 +566,15 @@ bool ApiConfigsController::deactivateDevice()
return true; return true;
} }
if (isSubscriptionExpired(apiConfigObject)) {
if (isRemoveEvent) {
return true;
} else {
emit errorOccurred(ErrorCode::ApiSubscriptionExpiredError);
return false;
}
}
GatewayRequestData gatewayRequestData { QSysInfo::productType(), GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION), QString(APP_VERSION),
m_settings->getInstallationUuid(true), m_settings->getInstallationUuid(true),
@@ -536,6 +609,11 @@ bool ApiConfigsController::deactivateExternalDevice(const QString &uuid, const Q
return true; return true;
} }
if (isSubscriptionExpired(apiConfigObject)) {
emit errorOccurred(ErrorCode::ApiSubscriptionExpiredError);
return false;
}
GatewayRequestData gatewayRequestData { QSysInfo::productType(), GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION), QString(APP_VERSION),
uuid, uuid,
@@ -21,7 +21,7 @@ public:
public slots: public slots:
bool exportNativeConfig(const QString &serverCountryCode, const QString &fileName); bool exportNativeConfig(const QString &serverCountryCode, const QString &fileName);
bool revokeNativeConfig(const QString &serverCountryCode); bool revokeNativeConfig(const QString &serverCountryCode);
// bool exportVpnKey(const QString &fileName); bool exportVpnKey(const QString &fileName);
void prepareVpnKeyExport(); void prepareVpnKeyExport();
void copyVpnKeyToClipboard(); void copyVpnKeyToClipboard();
@@ -30,7 +30,7 @@ public slots:
bool updateServiceFromGateway(const int serverIndex, const QString &newCountryCode, const QString &newCountryName, bool updateServiceFromGateway(const int serverIndex, const QString &newCountryCode, const QString &newCountryName,
bool reloadServiceConfig = false); bool reloadServiceConfig = false);
bool updateServiceFromTelegram(const int serverIndex); bool updateServiceFromTelegram(const int serverIndex);
bool deactivateDevice(); bool deactivateDevice(const bool isRemoveEvent);
bool deactivateExternalDevice(const QString &uuid, const QString &serverCountryCode); bool deactivateExternalDevice(const QString &uuid, const QString &serverCountryCode);
bool isConfigValid(); bool isConfigValid();
@@ -0,0 +1,65 @@
#include "apiNewsController.h"
#include "core/api/apiUtils.h"
#include <QJsonDocument>
#include <QJsonObject>
namespace
{
namespace configKey
{
constexpr char userCountryCode[] = "user_country_code";
constexpr char serviceType[] = "service_type";
}
}
ApiNewsController::ApiNewsController(const QSharedPointer<NewsModel> &newsModel, const std::shared_ptr<Settings> &settings,
const QSharedPointer<ServersModel> &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);
}
@@ -0,0 +1,33 @@
#ifndef APINEWSCONTROLLER_H
#define APINEWSCONTROLLER_H
#include <QJsonArray>
#include <QObject>
#include <QSharedPointer>
#include <memory>
#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> &newsModel, const std::shared_ptr<Settings> &settings,
const QSharedPointer<ServersModel> &serversModel, QObject *parent = nullptr);
Q_INVOKABLE void fetchNews();
signals:
void errorOccurred(ErrorCode errorCode);
private:
QSharedPointer<NewsModel> m_newsModel;
std::shared_ptr<Settings> m_settings;
QSharedPointer<ServersModel> m_serversModel;
};
#endif // APINEWSCONTROLLER_H
+2 -1
View File
@@ -297,10 +297,11 @@ void ExportController::revokeConfig(const int row, const DockerContainer contain
{ {
QSharedPointer<ServerController> serverController(new ServerController(m_settings)); QSharedPointer<ServerController> serverController(new ServerController(m_settings));
ErrorCode errorCode = 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) { if (errorCode != ErrorCode::NoError) {
emit exportErrorOccurred(errorCode); emit exportErrorOccurred(errorCode);
} }
emit revokeConfigCompleted();
} }
void ExportController::renameClient(const int row, const QString &clientName, const DockerContainer container, ServerCredentials credentials) void ExportController::renameClient(const int row, const QString &clientName, const DockerContainer container, ServerCredentials credentials)
+1
View File
@@ -42,6 +42,7 @@ public slots:
signals: signals:
void generateConfig(int type); void generateConfig(int type);
void revokeConfigCompleted();
void exportErrorOccurred(const QString &errorMessage); void exportErrorOccurred(const QString &errorMessage);
void exportErrorOccurred(ErrorCode errorCode); void exportErrorOccurred(ErrorCode errorCode);
+2 -21
View File
@@ -274,7 +274,7 @@ void ImportController::processNativeWireGuardConfig()
auto serverProtocolConfig = container.value(ContainerProps::containerTypeToString(DockerContainer::WireGuard)).toObject(); auto serverProtocolConfig = container.value(ContainerProps::containerTypeToString(DockerContainer::WireGuard)).toObject();
auto clientProtocolConfig = QJsonDocument::fromJson(serverProtocolConfig.value(config_key::last_config).toString().toUtf8()).object(); 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 junkPacketMinSize = QString::number(10);
QString junkPacketMaxSize = QString::number(50); QString junkPacketMaxSize = QString::number(50);
clientProtocolConfig[config_key::junkPacketCount] = junkPacketCount; clientProtocolConfig[config_key::junkPacketCount] = junkPacketCount;
@@ -584,26 +584,7 @@ bool ImportController::decodeQrCode(const QString &code)
mInstance->m_totalQrCodeChunksCount = 0; mInstance->m_totalQrCodeChunksCount = 0;
mInstance->m_receivedQrCodeChunksCount = 0; mInstance->m_receivedQrCodeChunksCount = 0;
} }
return mInstance->parseQrCodeChunk(code);
// First, try transferController QR (plain JSON payload).
QJsonParseError err;
const QJsonDocument doc = QJsonDocument::fromJson(code.toUtf8(), &err);
if (err.error == QJsonParseError::NoError && doc.isObject()) {
const QJsonObject obj = doc.object();
if (obj.contains(QStringLiteral("gw"))
&& (obj.contains(QStringLiteral("uuid")) || obj.contains(QStringLiteral("u")))) {
// это наш QR для передачи устройства
mInstance->m_isQrCodeProcessed = false;
emit mInstance->transferQrDecoded(code);
return true;
}
}
if (mInstance->parseQrCodeChunk(code)) {
return true;
}
return false;
} }
#endif #endif
-1
View File
@@ -57,7 +57,6 @@ signals:
void importErrorOccurred(ErrorCode errorCode, bool goToPageHome); void importErrorOccurred(ErrorCode errorCode, bool goToPageHome);
void qrDecodingFinished(); void qrDecodingFinished();
void transferQrDecoded(const QString &code);
void restoreAppConfig(const QByteArray &data); void restoreAppConfig(const QByteArray &data);
+1 -1
View File
@@ -73,7 +73,7 @@ void InstallController::install(DockerContainer container, int port, TransportPr
containerConfig.insert(config_key::transport_proto, ProtocolProps::transportProtoToString(transportProto, protocol)); containerConfig.insert(config_key::transport_proto, ProtocolProps::transportProtoToString(transportProto, protocol));
if (container == DockerContainer::Awg) { 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 junkPacketMinSize = QString::number(10);
QString junkPacketMaxSize = QString::number(50); QString junkPacketMaxSize = QString::number(50);
@@ -112,6 +112,7 @@ void ListViewFocusController::previousDelegate()
case Section::Default: { case Section::Default: {
if (hasFooter()) { if (hasFooter()) {
m_currentSection = Section::Footer; m_currentSection = Section::Footer;
viewAtCurrentIndex();
break; break;
} }
[[fallthrough]]; [[fallthrough]];
@@ -127,9 +128,11 @@ void ListViewFocusController::previousDelegate()
case Section::Delegate: { case Section::Delegate: {
if (m_delegateIndex > 0) { if (m_delegateIndex > 0) {
setDelegateIndex(m_delegateIndex - 1); setDelegateIndex(m_delegateIndex - 1);
viewAtCurrentIndex();
break; break;
} else if (hasHeader()) { } else if (hasHeader()) {
m_currentSection = Section::Header; m_currentSection = Section::Header;
viewAtCurrentIndex();
break; break;
} }
[[fallthrough]]; [[fallthrough]];
@@ -137,6 +140,7 @@ void ListViewFocusController::previousDelegate()
case Section::Header: { case Section::Header: {
m_isReturnNeeded = true; m_isReturnNeeded = true;
m_currentSection = Section::Default; m_currentSection = Section::Default;
viewAtCurrentIndex();
break; break;
} }
default: { default: {
@@ -275,7 +279,7 @@ bool ListViewFocusController::isFirstFocusItemInListView() const
return isFirstFocusItemInDelegate() && (m_delegateIndex == 0) && !hasHeader(); return isFirstFocusItemInDelegate() && (m_delegateIndex == 0) && !hasHeader();
} }
case Section::Header: { case Section::Header: {
isFirstFocusItemInDelegate(); return isFirstFocusItemInDelegate();
} }
case Section::Default: { case Section::Default: {
return true; return true;
+1 -1
View File
@@ -169,7 +169,7 @@ void PageController::onShowErrorMessage(ErrorCode errorCode)
{ {
const auto fullErrorMessage = errorString(errorCode); const auto fullErrorMessage = errorString(errorCode);
const auto errorMessage = fullErrorMessage.mid(fullErrorMessage.indexOf(". ") + 1); // remove ErrorCode %1. 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<int>(errorCode)).arg(utils::enumToString(errorCode).toLower()); const auto errorUrl = QStringLiteral("troubleshooting/error-codes/#error-%1-%2").arg(static_cast<int>(errorCode)).arg(utils::enumToString(errorCode).toLower());
const auto fullMessage = QStringLiteral("<a href=\"%1\" style=\"color: #FBB26A;\">ErrorCode: %2</a>. %3").arg(errorUrl).arg(static_cast<int>(errorCode)).arg(errorMessage); const auto fullMessage = QStringLiteral("<a href=\"%1\" style=\"color: #FBB26A;\">ErrorCode: %2</a>. %3").arg(errorUrl).arg(static_cast<int>(errorCode)).arg(errorMessage);
emit showErrorMessage(fullMessage); emit showErrorMessage(fullMessage);
+5 -5
View File
@@ -26,6 +26,8 @@ namespace PageLoader
PageSettingsConnection, PageSettingsConnection,
PageSettingsDns, PageSettingsDns,
PageSettingsApplication, PageSettingsApplication,
PageSettingsNewsNotifications,
PageSettingsNewsDetail,
PageSettingsBackup, PageSettingsBackup,
PageSettingsAbout, PageSettingsAbout,
PageSettingsLogging, PageSettingsLogging,
@@ -38,12 +40,9 @@ namespace PageLoader
PageSettingsApiInstructions, PageSettingsApiInstructions,
PageSettingsApiNativeConfigs, PageSettingsApiNativeConfigs,
PageSettingsApiDevices, PageSettingsApiDevices,
PageSettingsApiDevicesLimit,
PageSettingsApiAddDeviceConfirm,
PageSettingsApiAddDeviceScan,
PageSettingsApiSubscriptionKey, PageSettingsApiSubscriptionKey,
PageSettingsKillSwitchExceptions, PageSettingsKillSwitchExceptions,
PageServiceSftpSettings, PageServiceSftpSettings,
PageServiceTorWebsiteSettings, PageServiceTorWebsiteSettings,
PageServiceDnsSettings, PageServiceDnsSettings,
@@ -76,7 +75,6 @@ namespace PageLoader
PageShareFullAccess, PageShareFullAccess,
PageShareConnection, PageShareConnection,
PageTransferConfigViaQR,
PageDevMenu PageDevMenu
}; };
@@ -129,6 +127,8 @@ signals:
void goToPageViewConfig(); void goToPageViewConfig();
void goToPageSettingsServerServices(); void goToPageSettingsServerServices();
void goToPageSettingsBackup(); void goToPageSettingsBackup();
void goToShareConnectionPage(QString headerText, QString configContentHeaderText, QString configCaption, QString configExtension,
QString configFileName);
void closePage(); void closePage();
+23 -3
View File
@@ -5,6 +5,7 @@
#include "logger.h" #include "logger.h"
#include "systemController.h" #include "systemController.h"
#include "ui/qautostart.h" #include "ui/qautostart.h"
#include "amnezia_application.h"
#include "version.h" #include "version.h"
#ifdef Q_OS_ANDROID #ifdef Q_OS_ANDROID
#include "platforms/android/android_controller.h" #include "platforms/android/android_controller.h"
@@ -33,6 +34,9 @@ SettingsController::SettingsController(const QSharedPointer<ServersModel> &serve
#ifdef Q_OS_ANDROID #ifdef Q_OS_ANDROID
connect(AndroidController::instance(), &AndroidController::notificationStateChanged, this, &SettingsController::onNotificationStateChanged); connect(AndroidController::instance(), &AndroidController::notificationStateChanged, this, &SettingsController::onNotificationStateChanged);
#endif #endif
m_isDevModeEnabled = m_settings->isDevGatewayEnv();
toggleDevGatewayEnv(m_isDevModeEnabled);
} }
QString getPlatformName() QString getPlatformName()
@@ -139,6 +143,10 @@ void SettingsController::clearLogs()
Logger::clearLogs(false); Logger::clearLogs(false);
Logger::clearServiceLogs(); Logger::clearServiceLogs();
#endif #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) void SettingsController::backupAppConfig(const QString &fileName)
@@ -151,6 +159,7 @@ void SettingsController::backupAppConfig(const QString &fileName)
config["Conf/autoStart"] = Autostart::isAutostart(); config["Conf/autoStart"] = Autostart::isAutostart();
config["Conf/killSwitchEnabled"] = isKillSwitchEnabled(); config["Conf/killSwitchEnabled"] = isKillSwitchEnabled();
config["Conf/strictKillSwitchEnabled"] = isStrictKillSwitchEnabled(); config["Conf/strictKillSwitchEnabled"] = isStrictKillSwitchEnabled();
config["Conf/useAmneziaDns"] = isAmneziaDnsEnabled();
SystemController::saveFile(fileName, QJsonDocument(config).toJson()); 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) #if defined(Q_OS_WINDOWS) || defined(Q_OS_ANDROID)
int appSplitTunnelingRouteMode = newConfigData.value("Conf/appsRouteMode").toInt(); 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); m_appSplitTunnelingModel->setRouteMode(appSplitTunnelingRouteMode);
#if defined(Q_OS_WINDOWS) #if defined(Q_OS_WINDOWS)
@@ -198,12 +208,13 @@ void SettingsController::restoreAppConfigFromData(const QByteArray &data)
m_appSplitTunnelingModel->clearAppsList(); m_appSplitTunnelingModel->clearAppsList();
} }
} }
m_appSplitTunnelingModel->toggleSplitTunneling(appSplittunnelingEnabled); m_appSplitTunnelingModel->toggleSplitTunneling(appSplittunnelingEnabled);
#endif #endif
int siteSplitTunnelingRouteMode = newConfigData.value("Conf/routeMode").toInt(); 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->setRouteMode(siteSplitTunnelingRouteMode);
m_sitesModel->toggleSplitTunneling(siteSplittunnelingEnabled); m_sitesModel->toggleSplitTunneling(siteSplittunnelingEnabled);
@@ -214,6 +225,11 @@ void SettingsController::restoreAppConfigFromData(const QByteArray &data)
m_settings->setStrictKillSwitchEnabled(false); m_settings->setStrictKillSwitchEnabled(false);
#endif #endif
bool amneziaDnsEnabled = newConfigData.contains("Conf/useAmneziaDns")
? newConfigData.value("Conf/useAmneziaDns").toBool()
: m_settings->useAmneziaDns();
emit amneziaDnsToggled(amneziaDnsEnabled);
emit restoreBackupFinished(); emit restoreBackupFinished();
} else { } else {
emit changeSettingsErrorOccurred(tr("Backup file is corrupted")); emit changeSettingsErrorOccurred(tr("Backup file is corrupted"));
@@ -264,6 +280,9 @@ bool SettingsController::isAutoStartEnabled()
void SettingsController::toggleAutoStart(bool enable) void SettingsController::toggleAutoStart(bool enable)
{ {
Autostart::setAutostart(enable); Autostart::setAutostart(enable);
if (!enable) {
toggleStartMinimized(false);
}
} }
bool SettingsController::isStartMinimizedEnabled() bool SettingsController::isStartMinimizedEnabled()
@@ -274,6 +293,7 @@ bool SettingsController::isStartMinimizedEnabled()
void SettingsController::toggleStartMinimized(bool enable) void SettingsController::toggleStartMinimized(bool enable)
{ {
m_settings->setStartMinimized(enable); m_settings->setStartMinimized(enable);
emit startMinimizedChanged();
} }
bool SettingsController::isScreenshotsEnabled() bool SettingsController::isScreenshotsEnabled()
@@ -32,6 +32,7 @@ public:
Q_PROPERTY(bool isDevGatewayEnv READ isDevGatewayEnv WRITE toggleDevGatewayEnv NOTIFY devGatewayEnvChanged) Q_PROPERTY(bool isDevGatewayEnv READ isDevGatewayEnv WRITE toggleDevGatewayEnv NOTIFY devGatewayEnvChanged)
Q_PROPERTY(bool isHomeAdLabelVisible READ isHomeAdLabelVisible NOTIFY isHomeAdLabelVisibleChanged) Q_PROPERTY(bool isHomeAdLabelVisible READ isHomeAdLabelVisible NOTIFY isHomeAdLabelVisibleChanged)
Q_PROPERTY(bool startMinimized READ isStartMinimizedEnabled NOTIFY startMinimizedChanged)
public slots: public slots:
void toggleAmneziaDns(bool enable); void toggleAmneziaDns(bool enable);
@@ -125,6 +126,7 @@ signals:
void devGatewayEnvChanged(bool enabled); void devGatewayEnvChanged(bool enabled);
void isHomeAdLabelVisibleChanged(bool visible); void isHomeAdLabelVisibleChanged(bool visible);
void startMinimizedChanged();
private: private:
QSharedPointer<ServersModel> m_serversModel; QSharedPointer<ServersModel> m_serversModel;
@@ -1,344 +0,0 @@
#include "transferController.h"
#include <QVariant>
#include <QJsonParseError>
#include <QDebug>
#include <qeventloop.h>
#include <QNetworkProxyFactory>
#include <QNetworkProxyQuery>
#include <QUrl>
#include "core/api/apiUtils.h"
#include "core/qrCodeUtils.h"
#include "amnezia_application.h"
#include "settings.h"
#include "ui/models/servers_model.h"
#include "ui/controllers/exportController.h"
#include "ui/controllers/importController.h"
#include "core/api/apiDefs.h"
#include "core/controllers/gatewayController.h"
#include "core/errorstrings.h"
namespace {
void logSystemProxiesForUrl(const QString &urlStr)
{
const QUrl url(urlStr);
const QList<QNetworkProxy> proxies = QNetworkProxyFactory::systemProxyForQuery(QNetworkProxyQuery(url));
QStringList proxyDesc;
proxyDesc.reserve(proxies.size());
for (const auto &p : proxies) {
proxyDesc << QStringLiteral("%1 %2:%3")
.arg(p.type() == QNetworkProxy::NoProxy ? QStringLiteral("NoProxy")
: p.type() == QNetworkProxy::HttpProxy ? QStringLiteral("HttpProxy")
: p.type() == QNetworkProxy::Socks5Proxy ? QStringLiteral("Socks5Proxy")
: QStringLiteral("Proxy"))
.arg(p.hostName())
.arg(p.port());
}
}
}
TransferController::TransferController(const std::shared_ptr<Settings> &settings,
const QSharedPointer<ServersModel> &serversModel,
ExportController *exportController,
QObject *parent)
: QObject(parent), m_settings(settings), m_serversModel(serversModel), m_exportController(exportController)
{
}
void TransferController::handleImportControllerDestroyed()
{
m_importController = nullptr;
stopWaitForConfig();
}
TransferController::~TransferController() {
}
QString TransferController::buildQrPayloadJson(const QString &gatewayUrl, const QString &uuid) const
{
QJsonObject obj;
obj["gw"] = gatewayUrl;
obj["uuid"] = uuid;
// Used on the sender side for human-friendly notifications (same style as "Active Devices" list).
#if defined(Q_OS_ANDROID)
obj["name"] = QStringLiteral("Android");
#elif defined(Q_OS_IOS)
obj["name"] = QStringLiteral("iOS");
#elif defined(Q_OS_WIN)
obj["name"] = QStringLiteral("Windows");
#elif defined(Q_OS_MACOS)
obj["name"] = QStringLiteral("macOS");
#elif defined(Q_OS_LINUX)
obj["name"] = QStringLiteral("Linux");
#else
obj["name"] = QStringLiteral("Device");
#endif
return QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Compact));
}
void TransferController::generateNewQrCode()
{
QString gw = m_settings->getGatewayEndpoint();
if (!gw.endsWith('/')) {
gw.append('/');
}
m_currentUuid = QUuid::createUuid().toString(QUuid::WithoutBraces);
m_currentPayload = buildQrPayloadJson(gw, m_currentUuid);
auto qr = qrCodeUtils::generateQrCode(m_currentPayload.toUtf8());
const QString svg = QString::fromStdString(toSvgString(qr, 1));
m_qrCodeUrl = qrCodeUtils::svgToBase64(svg);
emit qrCodeUpdated();
emit currentUuidChanged();
emit currentPayloadChanged();
}
void TransferController::stopScanner()
{
emit scannerShouldStop();
}
QString TransferController::getCurrentApiKey(QString *vpnKeyOut) const
{
const int idx = m_serversModel ? m_serversModel->getProcessedServerIndex() : -1;
if (idx < 0 || !m_serversModel) {
return QString();
}
const QJsonObject server = m_serversModel->getServerConfig(idx);
const QJsonObject apiConfig = server.value(apiDefs::key::apiConfig).toObject();
const QJsonObject authData = server.value(QStringLiteral("auth_data")).toObject();
const QString apiKey = authData.value(QStringLiteral("api_key")).toString();
if (vpnKeyOut) {
QString vpnKey = apiConfig.value(apiDefs::key::vpnKey).toString();
if (vpnKey.isEmpty()) {
vpnKey = apiUtils::getPremiumV1VpnKey(server);
}
*vpnKeyOut = vpnKey;
}
return apiKey;
}
void TransferController::onTransferQrScanned(const QString &code)
{
QJsonParseError err;
const QJsonDocument doc = QJsonDocument::fromJson(code.toUtf8(), &err);
if (err.error != QJsonParseError::NoError || !doc.isObject()) {
qWarning() << "TransferController::onTransferQrScanned: invalid QR JSON " << err.errorString();
emit postFailed(QStringLiteral("Invalid QR JSON"));
return;
}
const QJsonObject obj = doc.object();
QString gw = obj.value("gw").toString();
const QString uuid = obj.value("uuid").toString();
if (gw.isEmpty() || uuid.isEmpty()) {
qWarning() << "TransferController::onTransferQrScanned: QR missing gw or uuid";
emit postFailed(QStringLiteral("QR missing gw or uuid"));
return;
}
if (!gw.endsWith('/')) {
gw.append('/');
}
int chosenServerIdx = -1;
QString apiKey;
QString vpnKey;
auto tryServerIndex = [&](int idx) -> bool {
if (!m_serversModel || idx < 0 || idx >= m_serversModel->getServersCount()) {
return false;
}
const QJsonObject server = m_serversModel->getServerConfig(idx);
const QJsonObject apiConfig = server.value(apiDefs::key::apiConfig).toObject();
const QJsonObject authData = server.value(QStringLiteral("auth_data")).toObject();
const QString candidateApiKey = authData.value(QStringLiteral("api_key")).toString();
QString candidateVpnKey = apiConfig.value(apiDefs::key::vpnKey).toString();
if (candidateVpnKey.isEmpty()) {
// Fallback for older Premium V1 configs where vpn_key may be derived.
candidateVpnKey = apiUtils::getPremiumV1VpnKey(server);
}
const bool candidateIsPremium = apiUtils::isPremiumServer(server);
const bool candidateIsFromGatewayApi = m_serversModel->data(idx, ServersModel::IsServerFromGatewayApiRole).toBool();
if (candidateApiKey.isEmpty() || candidateVpnKey.isEmpty()) {
return false;
}
if (!candidateIsPremium && !candidateIsFromGatewayApi) {
return false;
}
chosenServerIdx = idx;
apiKey = candidateApiKey;
vpnKey = candidateVpnKey;
return true;
};
if (m_serversModel) {
tryServerIndex(m_serversModel->getProcessedServerIndex());
if (chosenServerIdx < 0) {
tryServerIndex(m_serversModel->getDefaultServerIndex());
}
if (chosenServerIdx < 0) {
const int n = m_serversModel->getServersCount();
for (int i = 0; i < n; i++) {
if (tryServerIndex(i)) {
break;
}
}
}
}
if (chosenServerIdx < 0) {
qWarning() << "TransferController::onTransferQrScanned: no suitable subscription key/config found to send";
emit postFailed(QStringLiteral("No subscription key or config to send"));
return;
}
emit postStarted();
const int sendTimeoutMs = 60000;
GatewayController gatewayController(gw,
m_settings->isDevGatewayEnv(),
sendTimeoutMs,
m_settings->isStrictKillSwitchEnabled());
QJsonObject payload;
payload.insert(QStringLiteral("uuid"), uuid);
payload.insert(QStringLiteral("api_key"), apiKey);
payload.insert(QStringLiteral("config"), vpnKey);
const QString endpoint = QStringLiteral("%1v1/sendConfig");
QByteArray responseBody;
const QString fullUrl = endpoint.arg(gw);
qDebug() << "TransferController::onTransferQrScanned: POST" << fullUrl
<< "uuid:" << uuid;
logSystemProxiesForUrl(fullUrl);
const auto errorCode = gatewayController.post(endpoint, payload, responseBody);
qDebug() << "TransferController::onTransferQrScanned: sendConfig finished with code"
<< static_cast<int>(errorCode)
<< "response size:" << responseBody.size();
if (errorCode != ErrorCode::NoError) {
qWarning() << "TransferController::onTransferQrScanned: sendConfig failed with code" << static_cast<int>(errorCode);
emit postFailed(QStringLiteral("sendConfig failed: %1").arg(errorString(errorCode)));
return;
}
QJsonParseError parseErr;
const QJsonDocument respDoc = QJsonDocument::fromJson(responseBody, &parseErr);
if (parseErr.error == QJsonParseError::NoError && respDoc.isObject()
&& respDoc.object().value(QStringLiteral("status")).toString() == QStringLiteral("success")) {
emit postSucceeded();
stopScanner();
return;
}
qWarning() << "TransferController::onTransferQrScanned: unexpected gateway response:" << responseBody;
emit postFailed(QStringLiteral("Gateway response error"));
}
QString TransferController::qrCodeUrl() const
{
return m_qrCodeUrl;
}
void TransferController::startWaitForConfig(ImportController *importController)
{
QString gw = m_settings->getGatewayEndpoint();
if (!gw.endsWith(QLatin1Char('/'))) {
gw.append(QLatin1Char('/'));
}
const QString uuid = m_currentUuid;
if (uuid.isEmpty()) {
qWarning() << "TransferController::startWaitForConfig: no uuid";
emit waitError(QStringLiteral("No UUID"));
return;
}
m_importController = importController;
if (m_importController) {
connect(m_importController, &ImportController::destroyed,
this,
&TransferController::handleImportControllerDestroyed,
Qt::UniqueConnection);
}
const int waitTimeoutMs = 60000;
QJsonObject payload;
payload.insert(QStringLiteral("uuid"), uuid);
GatewayController gatewayController(gw,
m_settings->isDevGatewayEnv(),
waitTimeoutMs,
m_settings->isStrictKillSwitchEnabled());
const QString endpoint = QStringLiteral("%1v1/waitConfig");
QByteArray responseBody;
const QString fullUrl = endpoint.arg(gw);
logSystemProxiesForUrl(fullUrl);
const auto errorCode = gatewayController.post(endpoint, payload, responseBody);
if (errorCode != ErrorCode::NoError) {
qWarning() << "TransferController::startWaitForConfig: waitConfig failed with code" << static_cast<int>(errorCode);
emit waitError(QStringLiteral("waitConfig failed (%1)").arg(static_cast<int>(errorCode)));
return;
}
if (!m_importController) {
qWarning() << "TransferController::startWaitForConfig: import controller is null";
emit waitError(QStringLiteral("Import Controller destroyed"));
return;
}
QJsonParseError parseErr;
const QJsonDocument respDoc = QJsonDocument::fromJson(responseBody, &parseErr);
if (parseErr.error != QJsonParseError::NoError || !respDoc.isObject()) {
qWarning() << "TransferController::startWaitForConfig: invalid JSON response:" << responseBody;
emit waitError(QStringLiteral("Invalid gateway response"));
return;
}
const QJsonObject respObj = respDoc.object();
const QString status = respObj.value(QStringLiteral("status")).toString();
const QString configStr = respObj.value(QStringLiteral("config")).toString();
if (status != QStringLiteral("success")) {
qWarning() << "TransferController::startWaitForConfig: gateway status not success:" << status;
emit waitError(QStringLiteral("Gateway error"));
return;
}
if (configStr.isEmpty()) {
emit waitError(QStringLiteral("Empty config"));
return;
}
if (configStr == QStringLiteral("timeout")) {
emit waitError(QStringLiteral("Timeout"));
return;
}
if (!m_importController->extractConfigFromData(configStr)) {
qWarning() << "TransferController::startWaitForConfig: failed to parse config string";
emit waitError(QStringLiteral("Invalid config payload"));
return;
}
m_importController->importConfig();
emit configApplied();
}
void TransferController::stopWaitForConfig()
{
qDebug() << "TransferController::stopWaitForConfig: stop flag set";
}
@@ -1,77 +0,0 @@
#ifndef TRANSFERCONTROLLER_H
#define TRANSFERCONTROLLER_H
#include <QObject>
#include <QScopedPointer>
#include <QJsonObject>
#include <QJsonDocument>
#include <QUuid>
class Settings;
class ServersModel;
class ExportController;
class ImportController;
class TransferController : public QObject
{
Q_OBJECT
Q_PROPERTY(QString qrCodeUrl READ qrCodeUrl NOTIFY qrCodeUpdated)
Q_PROPERTY(QString pendingQrCode READ pendingQrCode WRITE setPendingQrCode NOTIFY pendingQrCodeChanged)
Q_PROPERTY(QString currentUuid READ currentUuid NOTIFY currentUuidChanged)
Q_PROPERTY(QString currentPayload READ currentPayload NOTIFY currentPayloadChanged)
public:
explicit TransferController(const std::shared_ptr<Settings> &settings,
const QSharedPointer<ServersModel> &serversModel,
ExportController *exportController,
QObject *parent = nullptr);
~TransferController() override;
Q_INVOKABLE void generateNewQrCode();
Q_INVOKABLE void stopScanner();
Q_INVOKABLE void onTransferQrScanned(const QString &code);
Q_INVOKABLE void setPendingQrCode(const QString &code) { m_pendingQrCode = code; emit pendingQrCodeChanged(); }
QString pendingQrCode() const { return m_pendingQrCode; }
Q_INVOKABLE void startWaitForConfig(ImportController *importController);
Q_INVOKABLE void stopWaitForConfig();
QString qrCodeUrl() const;
QString currentUuid() const { return m_currentUuid; }
QString currentPayload() const { return m_currentPayload; }
signals:
void qrCodeUpdated();
void scannerShouldStop();
void pendingQrCodeChanged();
void currentUuidChanged();
void currentPayloadChanged();
void waitError(const QString &message);
void configApplied();
void postStarted();
void postSucceeded();
void postFailed(const QString &message);
private slots:
void handleImportControllerDestroyed();
private:
QString buildQrPayloadJson(const QString &gatewayUrl, const QString &uuid) const;
QString m_pendingQrCode;
QString getCurrentApiKey(QString *vpnKeyOut = nullptr) const;
std::shared_ptr<Settings> m_settings;
QSharedPointer<ServersModel> m_serversModel;
ExportController *m_exportController { nullptr };
ImportController *m_importController { nullptr };
QString m_qrCodeUrl;
QString m_currentUuid;
QString m_currentPayload;
};
#endif // TRANSFERCONTROLLER_H
+1 -1
View File
@@ -31,7 +31,7 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
return tr("Active"); return tr("Active");
} }
return apiUtils::isSubscriptionExpired(m_accountInfoData.subscriptionEndDate) ? tr("Inactive") : tr("Active"); return apiUtils::isSubscriptionExpired(m_accountInfoData.subscriptionEndDate) ? tr("<p><a style=\"color: #EB5757;\">Inactive</a>") : tr("Active");
} }
case EndDateRole: { case EndDateRole: {
if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) { if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) {
+22 -17
View File
@@ -15,6 +15,7 @@ namespace
constexpr char serviceInfo[] = "service_info"; constexpr char serviceInfo[] = "service_info";
constexpr char serviceType[] = "service_type"; constexpr char serviceType[] = "service_type";
constexpr char serviceProtocol[] = "service_protocol"; constexpr char serviceProtocol[] = "service_protocol";
constexpr char serviceDescription[] = "service_description";
constexpr char name[] = "name"; constexpr char name[] = "name";
constexpr char price[] = "price"; constexpr char price[] = "price";
@@ -22,6 +23,10 @@ namespace
constexpr char timelimit[] = "timelimit"; constexpr char timelimit[] = "timelimit";
constexpr char region[] = "region"; constexpr char region[] = "region";
constexpr char description[] = "description";
constexpr char cardDescription[] = "card_description";
constexpr char features[] = "features";
constexpr char availableCountries[] = "available_countries"; constexpr char availableCountries[] = "available_countries";
constexpr char storeEndpoint[] = "store_endpoint"; constexpr char storeEndpoint[] = "store_endpoint";
@@ -65,11 +70,9 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
case CardDescriptionRole: { case CardDescriptionRole: {
auto speed = apiServiceData.serviceInfo.speed; auto speed = apiServiceData.serviceInfo.speed;
if (serviceType == serviceType::amneziaPremium) { if (serviceType == serviceType::amneziaPremium) {
return tr("Amnezia Premium is classic VPN for seamless work, downloading large files, and watching videos. " return apiServiceData.serviceInfo.cardDescription.arg(speed);
"Access all websites and online resources. Speeds up to %1 Mbps.")
.arg(speed);
} else if (serviceType == serviceType::amneziaFree) { } else if (serviceType == serviceType::amneziaFree) {
QString description = tr("Amnezia Free provides unlimited, free access to a basic set of websites and apps, including Facebook, Instagram, Twitter (X), Discord, Telegram, and more. YouTube is not included in the free plan."); QString description = apiServiceData.serviceInfo.cardDescription;
if (!isServiceAvailable) { if (!isServiceAvailable) {
description += tr("<p><a style=\"color: #EB5757;\">Not available in your region. If you have VPN enabled, disable it, " description += tr("<p><a style=\"color: #EB5757;\">Not available in your region. If you have VPN enabled, disable it, "
"return to the previous screen, and try again.</a>"); "return to the previous screen, and try again.</a>");
@@ -78,12 +81,7 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
} }
} }
case ServiceDescriptionRole: { case ServiceDescriptionRole: {
if (serviceType == serviceType::amneziaPremium) { return apiServiceData.serviceInfo.description;
return tr("Amnezia Premium is classic VPN for for seamless work, downloading large files, and watching videos. "
"Access all websites and online resources.");
} else {
return tr("Amnezia Free provides unlimited, free access to a basic set of websites and apps, including Facebook, Instagram, Twitter (X), Discord, Telegram, and more. YouTube is not included in the free plan.");
}
} }
case IsServiceAvailableRole: { case IsServiceAvailableRole: {
if (serviceType == serviceType::amneziaFree) { if (serviceType == serviceType::amneziaFree) {
@@ -107,13 +105,7 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
return apiServiceData.serviceInfo.region; return apiServiceData.serviceInfo.region;
} }
case FeaturesRole: { case FeaturesRole: {
if (serviceType == serviceType::amneziaPremium) { return apiServiceData.serviceInfo.features;
return tr("");
} else {
return tr("VPN will open only popular sites blocked in your region, such as Instagram, Facebook, Twitter and others. "
"Other sites will be opened from your real IP address, "
"<a href=\"%1\" style=\"color: #FBB26A;\">more details on the website.</a>");
}
} }
case PriceRole: { case PriceRole: {
auto price = apiServiceData.serviceInfo.price; auto price = apiServiceData.serviceInfo.price;
@@ -125,6 +117,13 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
case EndDateRole: { case EndDateRole: {
return QDateTime::fromString(apiServiceData.subscription.endDate, Qt::ISODate).toLocalTime().toString("d MMM yyyy"); return QDateTime::fromString(apiServiceData.subscription.endDate, Qt::ISODate).toLocalTime().toString("d MMM yyyy");
} }
case OrderRole: {
if (serviceType == serviceType::amneziaPremium) {
return 0;
} else if (serviceType == serviceType::amneziaFree) {
return 1;
}
}
} }
return QVariant(); return QVariant();
@@ -224,6 +223,7 @@ QHash<int, QByteArray> ApiServicesModel::roleNames() const
roles[FeaturesRole] = "features"; roles[FeaturesRole] = "features";
roles[PriceRole] = "price"; roles[PriceRole] = "price";
roles[EndDateRole] = "endDate"; roles[EndDateRole] = "endDate";
roles[OrderRole] = "order";
return roles; return roles;
} }
@@ -234,6 +234,7 @@ ApiServicesModel::ApiServicesData ApiServicesModel::getApiServicesData(const QJs
auto serviceType = data.value(configKey::serviceType).toString(); auto serviceType = data.value(configKey::serviceType).toString();
auto serviceProtocol = data.value(configKey::serviceProtocol).toString(); auto serviceProtocol = data.value(configKey::serviceProtocol).toString();
auto availableCountries = data.value(configKey::availableCountries).toArray(); auto availableCountries = data.value(configKey::availableCountries).toArray();
auto serviceDescription = data.value(configKey::serviceDescription).toObject();
auto subscriptionObject = data.value(configKey::subscription).toObject(); auto subscriptionObject = data.value(configKey::subscription).toObject();
@@ -244,6 +245,10 @@ ApiServicesModel::ApiServicesData ApiServicesModel::getApiServicesData(const QJs
serviceData.serviceInfo.speed = serviceInfo.value(configKey::speed).toString(); serviceData.serviceInfo.speed = serviceInfo.value(configKey::speed).toString();
serviceData.serviceInfo.timeLimit = serviceInfo.value(configKey::timelimit).toString(); serviceData.serviceInfo.timeLimit = serviceInfo.value(configKey::timelimit).toString();
serviceData.serviceInfo.cardDescription = serviceDescription.value(configKey::cardDescription).toString();
serviceData.serviceInfo.description = serviceDescription.value(configKey::description).toString();
serviceData.serviceInfo.features = serviceDescription.value(configKey::features).toString();
serviceData.type = serviceType; serviceData.type = serviceType;
serviceData.protocol = serviceProtocol; serviceData.protocol = serviceProtocol;
+6 -1
View File
@@ -20,7 +20,8 @@ public:
RegionRole, RegionRole,
FeaturesRole, FeaturesRole,
PriceRole, PriceRole,
EndDateRole EndDateRole,
OrderRole
}; };
explicit ApiServicesModel(QObject *parent = nullptr); explicit ApiServicesModel(QObject *parent = nullptr);
@@ -58,6 +59,10 @@ private:
QString region; QString region;
QString price; QString price;
QString description;
QString features;
QString cardDescription;
QJsonObject object; QJsonObject object;
}; };
+4 -2
View File
@@ -497,7 +497,8 @@ ErrorCode ClientManagementModel::appendClient(const QString &clientId, const QSt
return error; 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 ServerCredentials &credentials,
const QSharedPointer<ServerController> &serverController, bool addTimeStamp) const QSharedPointer<ServerController> &serverController, bool addTimeStamp)
{ {
@@ -529,7 +530,8 @@ ErrorCode ClientManagementModel::renameClient(const int row, const QString &clie
return error; 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> &serverController) const int serverIndex, const QSharedPointer<ServerController> &serverController)
{ {
ErrorCode errorCode = ErrorCode::NoError; ErrorCode errorCode = ErrorCode::NoError;
+6 -4
View File
@@ -44,10 +44,10 @@ public slots:
const ServerCredentials &credentials, const QSharedPointer<ServerController> &serverController); const ServerCredentials &credentials, const QSharedPointer<ServerController> &serverController);
ErrorCode appendClient(const QString &clientId, const QString &clientName, const DockerContainer container, ErrorCode appendClient(const QString &clientId, const QString &clientName, const DockerContainer container,
const ServerCredentials &credentials, const QSharedPointer<ServerController> &serverController); const ServerCredentials &credentials, const QSharedPointer<ServerController> &serverController);
ErrorCode renameClient(const int row, const QString &userName, const DockerContainer container, const ServerCredentials &credentials, ErrorCode renameClient(const int row, const QString &userName, const DockerContainer container,
const QSharedPointer<ServerController> &serverController, bool addTimeStamp = false); const ServerCredentials &credentials, const QSharedPointer<ServerController> &serverController, bool addTimeStamp = false);
ErrorCode revokeClient(const int index, const DockerContainer container, const ServerCredentials &credentials, const int serverIndex, ErrorCode revokeClient(const int index, const DockerContainer container, const ServerCredentials &credentials,
const QSharedPointer<ServerController> &serverController); const int serverIndex, const QSharedPointer<ServerController> &serverController);
ErrorCode revokeClient(const QJsonObject &containerConfig, const DockerContainer container, const ServerCredentials &credentials, ErrorCode revokeClient(const QJsonObject &containerConfig, const DockerContainer container, const ServerCredentials &credentials,
const int serverIndex, const QSharedPointer<ServerController> &serverController); const int serverIndex, const QSharedPointer<ServerController> &serverController);
@@ -60,6 +60,8 @@ signals:
private: private:
bool isClientExists(const QString &clientId); bool isClientExists(const QString &clientId);
int clientIndexById(const QString &clientId);
void migration(const QByteArray &clientsTableString); void migration(const QByteArray &clientsTableString);
ErrorCode revokeOpenVpn(const int row, const DockerContainer container, const ServerCredentials &credentials, const int serverIndex, ErrorCode revokeOpenVpn(const int row, const DockerContainer container, const ServerCredentials &credentials, const int serverIndex,
+130
View File
@@ -0,0 +1,130 @@
#include "ui/models/newsModel.h"
#include <QDir>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QQmlEngine>
#include <QStandardPaths>
#include <algorithm>
NewsModel::NewsModel(const std::shared_ptr<Settings> &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<int, QByteArray> NewsModel::roleNames() const
{
QHash<int, QByteArray> 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<QString> existingIds;
for (const NewsItem &item : m_items) {
existingIds.insert(item.id);
}
QList<NewsItem> 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<QString>(ids.begin(), ids.end());
}
void NewsModel::saveReadIds() const
{
m_settings->setReadNewsIds(QStringList(m_readIds.begin(), m_readIds.end()));
}
+62
View File
@@ -0,0 +1,62 @@
#ifndef NEWSMODEL_H
#define NEWSMODEL_H
#include "settings.h"
#include <QAbstractListModel>
#include <QDateTime>
#include <QJsonArray>
#include <QSet>
#include <QString>
#include <QVector>
#include <memory>
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> &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<int, QByteArray> roleNames() const override;
signals:
void processedIndexChanged(int index);
void hasUnreadChanged();
private:
QVector<NewsItem> m_items;
int m_processedIndex = -1;
std::shared_ptr<Settings> m_settings;
QSet<QString> m_readIds;
void loadReadIds();
void saveReadIds() const;
};
#endif // NEWSMODEL_H
@@ -191,14 +191,14 @@ QJsonObject AwgConfigModel::getConfig()
jsonConfig[config_key::junkPacketCount] = m_clientProtocolConfig[config_key::junkPacketCount]; jsonConfig[config_key::junkPacketCount] = m_clientProtocolConfig[config_key::junkPacketCount];
jsonConfig[config_key::junkPacketMinSize] = m_clientProtocolConfig[config_key::junkPacketMinSize]; jsonConfig[config_key::junkPacketMinSize] = m_clientProtocolConfig[config_key::junkPacketMinSize];
jsonConfig[config_key::junkPacketMaxSize] = m_clientProtocolConfig[config_key::junkPacketMaxSize]; jsonConfig[config_key::junkPacketMaxSize] = m_clientProtocolConfig[config_key::junkPacketMaxSize];
jsonConfig[config_key::specialJunk1] = m_clientProtocolConfig[config_key::specialJunk1]; jsonConfig[config_key::specialJunk1] = m_clientProtocolConfig[config_key::specialJunk1].toString().trimmed();
jsonConfig[config_key::specialJunk2] = m_clientProtocolConfig[config_key::specialJunk2]; jsonConfig[config_key::specialJunk2] = m_clientProtocolConfig[config_key::specialJunk2].toString().trimmed();
jsonConfig[config_key::specialJunk3] = m_clientProtocolConfig[config_key::specialJunk3]; jsonConfig[config_key::specialJunk3] = m_clientProtocolConfig[config_key::specialJunk3].toString().trimmed();
jsonConfig[config_key::specialJunk4] = m_clientProtocolConfig[config_key::specialJunk4]; jsonConfig[config_key::specialJunk4] = m_clientProtocolConfig[config_key::specialJunk4].toString().trimmed();
jsonConfig[config_key::specialJunk5] = m_clientProtocolConfig[config_key::specialJunk5]; jsonConfig[config_key::specialJunk5] = m_clientProtocolConfig[config_key::specialJunk5].toString().trimmed();
jsonConfig[config_key::controlledJunk1] = m_clientProtocolConfig[config_key::controlledJunk1]; jsonConfig[config_key::controlledJunk1] = m_clientProtocolConfig[config_key::controlledJunk1].toString().trimmed();
jsonConfig[config_key::controlledJunk2] = m_clientProtocolConfig[config_key::controlledJunk2]; jsonConfig[config_key::controlledJunk2] = m_clientProtocolConfig[config_key::controlledJunk2].toString().trimmed();
jsonConfig[config_key::controlledJunk3] = m_clientProtocolConfig[config_key::controlledJunk3]; jsonConfig[config_key::controlledJunk3] = m_clientProtocolConfig[config_key::controlledJunk3].toString().trimmed();
jsonConfig[config_key::specialHandshakeTimeout] = m_clientProtocolConfig[config_key::specialHandshakeTimeout]; jsonConfig[config_key::specialHandshakeTimeout] = m_clientProtocolConfig[config_key::specialHandshakeTimeout];
m_serverProtocolConfig[config_key::last_config] = QString(QJsonDocument(jsonConfig).toJson()); m_serverProtocolConfig[config_key::last_config] = QString(QJsonDocument(jsonConfig).toJson());
+65 -1
View File
@@ -44,6 +44,8 @@ ServersModel::ServersModel(std::shared_ptr<Settings> settings, QObject *parent)
connect(this, &ServersModel::processedServerIndexChanged, this, &ServersModel::processedServerChanged); connect(this, &ServersModel::processedServerIndexChanged, this, &ServersModel::processedServerChanged);
connect(this, &ServersModel::dataChanged, this, &ServersModel::processedServerChanged); connect(this, &ServersModel::dataChanged, this, &ServersModel::processedServerChanged);
connect(this, &QAbstractItemModel::modelReset, this, &ServersModel::recomputeGatewayStacks);
} }
int ServersModel::rowCount(const QModelIndex &parent) const int ServersModel::rowCount(const QModelIndex &parent) const
@@ -173,6 +175,7 @@ void ServersModel::resetModel()
m_servers = m_settings->serversArray(); m_servers = m_settings->serversArray();
m_defaultServerIndex = m_settings->defaultServerIndex(); m_defaultServerIndex = m_settings->defaultServerIndex();
m_processedServerIndex = m_defaultServerIndex; m_processedServerIndex = m_defaultServerIndex;
m_isAmneziaDnsEnabled = m_settings->useAmneziaDns();
endResetModel(); endResetModel();
emit defaultServerIndexChanged(m_defaultServerIndex); emit defaultServerIndexChanged(m_defaultServerIndex);
} }
@@ -374,7 +377,6 @@ QHash<int, QByteArray> ServersModel::roleNames() const
{ {
QHash<int, QByteArray> roles; QHash<int, QByteArray> roles;
roles[NameRole] = "serverName";
roles[NameRole] = "name"; roles[NameRole] = "name";
roles[ServerDescriptionRole] = "serverDescription"; roles[ServerDescriptionRole] = "serverDescription";
roles[CollapsedServerDescriptionRole] = "collapsedServerDescription"; roles[CollapsedServerDescriptionRole] = "collapsedServerDescription";
@@ -755,6 +757,68 @@ bool ServersModel::isServerFromApi(const int serverIndex)
return data(serverIndex, IsServerFromTelegramApiRole).toBool() || data(serverIndex, IsServerFromGatewayApiRole).toBool(); 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) bool ServersModel::isApiKeyExpired(const int serverIndex)
{ {
auto serverConfig = m_servers.at(serverIndex).toObject(); auto serverConfig = m_servers.at(serverIndex).toObject();
+22
View File
@@ -10,6 +10,16 @@ class ServersModel : public QAbstractListModel
{ {
Q_OBJECT Q_OBJECT
public: public:
struct GatewayStacks
{
QSet<QString> userCountryCodes;
QSet<QString> serviceTypes;
bool isEmpty() const { return userCountryCodes.isEmpty() && serviceTypes.isEmpty(); }
bool operator==(const GatewayStacks &other) const;
QJsonObject toJson() const;
};
enum Roles { enum Roles {
NameRole = Qt::UserRole + 1, NameRole = Qt::UserRole + 1,
ServerDescriptionRole, ServerDescriptionRole,
@@ -52,6 +62,8 @@ public:
void resetModel(); void resetModel();
GatewayStacks gatewayStacks() const { return m_gatewayStacks; }
Q_PROPERTY(int defaultIndex READ getDefaultServerIndex WRITE setDefaultServerIndex NOTIFY defaultServerIndexChanged) Q_PROPERTY(int defaultIndex READ getDefaultServerIndex WRITE setDefaultServerIndex NOTIFY defaultServerIndexChanged)
Q_PROPERTY(QString defaultServerName READ getDefaultServerName NOTIFY defaultServerNameChanged) Q_PROPERTY(QString defaultServerName READ getDefaultServerName NOTIFY defaultServerNameChanged)
Q_PROPERTY(QString defaultServerDefaultContainerName READ getDefaultServerDefaultContainerName NOTIFY defaultServerDefaultContainerChanged) Q_PROPERTY(QString defaultServerDefaultContainerName READ getDefaultServerDefaultContainerName NOTIFY defaultServerDefaultContainerChanged)
@@ -62,6 +74,8 @@ public:
defaultServerDefaultContainerChanged) defaultServerDefaultContainerChanged)
Q_PROPERTY(bool isDefaultServerFromApi READ isDefaultServerFromApi NOTIFY defaultServerIndexChanged) 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(int processedIndex READ getProcessedServerIndex WRITE setProcessedServerIndex NOTIFY processedServerIndexChanged)
Q_PROPERTY(bool processedServerIsPremium READ processedServerIsPremium NOTIFY processedServerChanged) Q_PROPERTY(bool processedServerIsPremium READ processedServerIsPremium NOTIFY processedServerChanged)
@@ -82,6 +96,8 @@ public slots:
bool isDefaultServerHasWriteAccess(); bool isDefaultServerHasWriteAccess();
bool hasServerWithWriteAccess(); bool hasServerWithWriteAccess();
bool hasServersFromGatewayApi();
const int getServersCount(); const int getServersCount();
void setProcessedServerIndex(const int index); void setProcessedServerIndex(const int index);
@@ -147,6 +163,9 @@ signals:
void updateApiCountryModel(); void updateApiCountryModel();
void updateApiServicesModel(); void updateApiServicesModel();
void hasServersFromGatewayApiChanged();
void gatewayStacksExpanded();
private: private:
ServerCredentials serverCredentials(int index) const; ServerCredentials serverCredentials(int index) const;
@@ -167,6 +186,9 @@ private:
int m_processedServerIndex; int m_processedServerIndex;
bool m_isAmneziaDnsEnabled = m_settings->useAmneziaDns(); bool m_isAmneziaDnsEnabled = m_settings->useAmneziaDns();
GatewayStacks m_gatewayStacks;
void recomputeGatewayStacks();
}; };
#endif // SERVERSMODEL_H #endif // SERVERSMODEL_H
@@ -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. ") 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 += qsTr("This new subscription type will be actively developed with more locations and features added regularly. Currently available:")
str += "<ul style='margin-left: -16px;'>" str += "<ul style='margin-left: -16px;'>"
str += qsTr("<li>13 locations (with more coming soon)</li>") str += qsTr("<li>20 locations (with more coming soon)</li>")
str += qsTr("<li>Easier switching between countries in the app</li>") str += qsTr("<li>Easier switching between countries in the app</li>")
str += qsTr("<li>Personal dashboard to manage your subscription</li>") str += qsTr("<li>Personal dashboard to manage your subscription</li>")
str += "</ul>" str += "</ul>"
+2 -2
View File
@@ -20,8 +20,8 @@ Menu {
MenuItem { MenuItem {
text: qsTr("&Paste") text: qsTr("&Paste")
shortcut: StandardKey.Paste shortcut: StandardKey.Paste
// Fix calling paste from clipboard when launching app on android // Fix calling paste from clipboard when launching app on android/ios
enabled: Qt.platform.os === "android" ? true : textObj.canPaste enabled: (Qt.platform.os === "android" || Qt.platform.os === "ios") ? true : textObj.canPaste
onTriggered: textObj.paste() onTriggered: textObj.paste()
} }
@@ -7,17 +7,20 @@ import Style 1.0
import "TextTypes" import "TextTypes"
RowLayout { RowLayout {
id: root
property string imageSource property string imageSource
property string leftText property string leftText
property var rightText property var rightText
property bool isRightTextUndefined: rightText === undefined property bool isRightTextUndefined: rightText === undefined
property int rightTextFormat: Text.PlainText
visible: !isRightTextUndefined visible: !isRightTextUndefined
Image { Image {
Layout.preferredHeight: 18 Layout.preferredHeight: 18
Layout.preferredWidth: 18 Layout.preferredWidth: 18
source: imageSource source: root.imageSource
} }
ListItemTitleType { ListItemTitleType {
@@ -25,14 +28,15 @@ RowLayout {
Layout.rightMargin: 10 Layout.rightMargin: 10
Layout.alignment: Qt.AlignRight Layout.alignment: Qt.AlignRight
text: leftText text: root.leftText
} }
ParagraphTextType { ParagraphTextType {
visible: rightText !== "" visible: root.rightText !== ""
Layout.alignment: Qt.AlignLeft Layout.alignment: Qt.AlignLeft
text: isRightTextUndefined ? "" : rightText text: root.isRightTextUndefined ? "" : root.rightText
textFormat: root.rightTextFormat
} }
} }
+1 -1
View File
@@ -72,7 +72,7 @@ Popup {
Layout.fillWidth: true Layout.fillWidth: true
onLinkActivated: function(link) { onLinkActivated: function(link) {
Qt.openUrlExternally(link) Qt.openUrlExternally(LanguageModel.getCurrentDocsUrl(link))
} }
text: root.text text: root.text
+1 -1
View File
@@ -155,7 +155,7 @@ Switch {
function handleSwitch(event) { function handleSwitch(event) {
if (!event.isAutoRepeat) { if (!event.isAutoRepeat) {
root.checked = !root.checked root.checked = !root.checked
root.checkedChanged() root.toggled()
} }
event.accepted = true event.accepted = true
} }
@@ -37,6 +37,22 @@ Item {
implicitWidth: content.implicitWidth implicitWidth: content.implicitWidth
implicitHeight: content.implicitHeight implicitHeight: content.implicitHeight
Keys.onTabPressed: {
FocusController.nextKeyTabItem()
}
Keys.onBacktabPressed: {
FocusController.previousKeyTabItem()
}
Keys.onUpPressed: {
FocusController.nextKeyUpItem()
}
Keys.onDownPressed: {
FocusController.nextKeyDownItem()
}
ColumnLayout { ColumnLayout {
id: content id: content
anchors.fill: parent anchors.fill: parent
+28
View File
@@ -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 { ConnectButton {
id: connectButton id: connectButton
objectName: "connectButton" objectName: "connectButton"
@@ -153,7 +153,7 @@ PageType {
text: qsTr("Auto-negotiate encryption") text: qsTr("Auto-negotiate encryption")
checked: autoNegotiateEncryprion checked: autoNegotiateEncryprion
onCheckedChanged: { onToggled: function() {
if (checked !== autoNegotiateEncryprion) { if (checked !== autoNegotiateEncryprion) {
autoNegotiateEncryprion = checked autoNegotiateEncryprion = checked
} }
@@ -320,7 +320,7 @@ PageType {
text: qsTr("Additional client configuration commands") text: qsTr("Additional client configuration commands")
onCheckedChanged: { onToggled: function() {
if (!checked) { if (!checked) {
additionalClientCommands = "" additionalClientCommands = ""
} }
@@ -357,7 +357,7 @@ PageType {
text: qsTr("Additional server configuration commands") text: qsTr("Additional server configuration commands")
onCheckedChanged: { onToggled: function() {
if (!checked) { if (!checked) {
additionalServerCommands = "" additionalServerCommands = ""
} }
+181 -163
View File
@@ -1,163 +1,181 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Dialogs import QtQuick.Dialogs
import PageEnum 1.0 import PageEnum 1.0
import Style 1.0 import Style 1.0
import "./" import "./"
import "../Controls2" import "../Controls2"
import "../Controls2/TextTypes" import "../Controls2/TextTypes"
import "../Config" import "../Config"
PageType { PageType {
id: root id: root
ListViewType { ListViewType {
id: listView id: listView
anchors.fill: parent anchors.fill: parent
header: ColumnLayout { header: ColumnLayout {
width: listView.width width: listView.width
BaseHeaderType { BaseHeaderType {
id: header id: header
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 24 Layout.topMargin: 24
Layout.bottomMargin: 16 Layout.bottomMargin: 16
Layout.rightMargin: 16 Layout.rightMargin: 16
Layout.leftMargin: 16 Layout.leftMargin: 16
headerText: qsTr("Settings") headerText: qsTr("Settings")
} }
} }
model: settingsEntries model: settingsEntries
delegate: ColumnLayout { delegate: ColumnLayout {
width: listView.width width: listView.width
spacing: 0 spacing: 0
LabelWithButtonType { LabelWithButtonType {
Layout.fillWidth: true Layout.fillWidth: true
visible: isVisible visible: isVisible
text: title text: title
rightImageSource: "qrc:/images/controls/chevron-right.svg" rightImageSource: "qrc:/images/controls/chevron-right.svg"
leftImageSource: leftImagePath leftImageSource: leftImagePath
clickedFunction: clickedHandler clickedFunction: clickedHandler
} }
DividerType { DividerType {
visible: isVisible visible: isVisible
} }
} }
footer: ColumnLayout { footer: ColumnLayout {
width: listView.width width: listView.width
LabelWithButtonType { LabelWithButtonType {
id: close id: close
visible: GC.isDesktop() visible: GC.isDesktop()
Layout.fillWidth: true Layout.fillWidth: true
text: qsTr("Close application") text: qsTr("Close application")
leftImageSource: "qrc:/images/controls/x-circle.svg" leftImageSource: "qrc:/images/controls/x-circle.svg"
isLeftImageHoverEnabled: false isLeftImageHoverEnabled: false
clickedFunction: function() { clickedFunction: function() {
PageController.closeApplication() PageController.closeApplication()
} }
} }
DividerType { DividerType {
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: 16 Layout.leftMargin: 16
Layout.rightMargin: 16 Layout.rightMargin: 16
visible: GC.isDesktop() visible: GC.isDesktop()
} }
} }
} }
property list<QtObject> settingsEntries: [ property list<QtObject> settingsEntries: [
servers, servers,
connection, connection,
application, application,
backup, news,
about, backup,
devConsole about,
] devConsole
]
QtObject {
id: servers QtObject {
id: servers
property string title: qsTr("Servers")
readonly property string leftImagePath: "qrc:/images/controls/server.svg" property string title: qsTr("Servers")
property bool isVisible: true readonly property string leftImagePath: "qrc:/images/controls/server.svg"
readonly property var clickedHandler: function() { property bool isVisible: true
PageController.goToPage(PageEnum.PageSettingsServersList) readonly property var clickedHandler: function() {
} PageController.goToPage(PageEnum.PageSettingsServersList)
} }
}
QtObject {
id: connection QtObject {
id: connection
property string title: qsTr("Connection")
readonly property string leftImagePath: "qrc:/images/controls/radio.svg" property string title: qsTr("Connection")
property bool isVisible: true readonly property string leftImagePath: "qrc:/images/controls/radio.svg"
readonly property var clickedHandler: function() { property bool isVisible: true
PageController.goToPage(PageEnum.PageSettingsConnection) readonly property var clickedHandler: function() {
} PageController.goToPage(PageEnum.PageSettingsConnection)
} }
}
QtObject {
id: application QtObject {
id: application
property string title: qsTr("Application")
readonly property string leftImagePath: "qrc:/images/controls/app.svg" property string title: qsTr("Application")
property bool isVisible: true readonly property string leftImagePath: "qrc:/images/controls/app.svg"
readonly property var clickedHandler: function() { property bool isVisible: true
PageController.goToPage(PageEnum.PageSettingsApplication) readonly property var clickedHandler: function() {
} PageController.goToPage(PageEnum.PageSettingsApplication)
} }
}
QtObject {
id: backup QtObject {
id: news
property string title: qsTr("Backup")
readonly property string leftImagePath: "qrc:/images/controls/save.svg" property string title: qsTr("News & Notifications")
property bool isVisible: true readonly property string leftImagePath: NewsModel.hasUnread ? "qrc:/images/controls/news-unread.svg" : "qrc:/images/controls/news.svg"
readonly property var clickedHandler: function() { property bool isVisible: ServersModel.hasServersFromGatewayApi
PageController.goToPage(PageEnum.PageSettingsBackup) readonly property var clickedHandler: function() {
} if (!ServersModel.hasServersFromGatewayApi) {
} return;
}
QtObject { PageController.showBusyIndicator(true)
id: about ApiNewsController.fetchNews();
PageController.goToPage(PageEnum.PageSettingsNewsNotifications)
property string title: qsTr("About AmneziaVPN") PageController.showBusyIndicator(false)
readonly property string leftImagePath: "qrc:/images/controls/amnezia.svg" }
property bool isVisible: true }
readonly property var clickedHandler: function() {
PageController.goToPage(PageEnum.PageSettingsAbout) QtObject {
} id: backup
}
property string title: qsTr("Backup")
QtObject { readonly property string leftImagePath: "qrc:/images/controls/save.svg"
id: devConsole property bool isVisible: true
readonly property var clickedHandler: function() {
property string title: qsTr("Dev console") PageController.goToPage(PageEnum.PageSettingsBackup)
readonly property string leftImagePath: "qrc:/images/controls/bug.svg" }
property bool isVisible: SettingsController.isDevModeEnabled }
readonly property var clickedHandler: function() {
PageController.goToPage(PageEnum.PageDevMenu) QtObject {
} id: about
}
} property string title: qsTr("About AmneziaVPN")
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)
}
}
}
@@ -1,126 +0,0 @@
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
property bool isSending: false
function getAddedDeviceName() {
try {
var obj = JSON.parse(TransferController.pendingQrCode)
if (obj && obj.name && obj.name.length > 0) {
return obj.name
}
} catch (e) {}
return qsTr("Device")
}
function getAvailableCount() {
var max = ApiAccountInfoModel.data("maxDeviceCount")
var active = ApiAccountInfoModel.data("activeDeviceCount")
if (!max || max <= 0) max = 7
if (!active || active < 0) active = 0
var remain = max - active
return remain > 0 ? remain : 0
}
ListViewType {
id: listView
anchors.fill: parent
anchors.topMargin: 20
header: ColumnLayout {
width: listView.width
BackButtonType {
backButtonFunction: function() {
if (root.isSending) {
TransferController.stopWaitForConfig()
}
PageController.closePage()
}
}
BaseHeaderType {
Layout.fillWidth: true
Layout.rightMargin: 16
Layout.leftMargin: 16
headerText: qsTr("Add a new device to the subscription?")
descriptionText: qsTr("Devices available with Amnezia Premium: (%1)").arg(getAvailableCount())
}
BasicButtonType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 16
text: qsTr("Add Device")
enabled: !root.isSending && root.getAvailableCount() > 0 && TransferController.pendingQrCode !== ""
clickedFunc: function() {
if (TransferController.pendingQrCode !== "") {
root.isSending = true
TransferController.onTransferQrScanned(TransferController.pendingQrCode)
}
}
}
BasicButtonType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
defaultColor: AmneziaStyle.color.transparent
hoveredColor: AmneziaStyle.color.translucentWhite
pressedColor: AmneziaStyle.color.sheerWhite
textColor: AmneziaStyle.color.paleGray
borderColor: AmneziaStyle.color.paleGray
borderWidth: 1
text: qsTr("Cancel")
enabled: !root.isSending
clickedFunc: function() {
PageController.closePage()
}
}
}
}
Connections {
target: TransferController
function onPostStarted() {
PageController.showNotificationMessage(qsTr("Sending configuration..."))
}
function onPostSucceeded() {
root.isSending = false
PageController.showNotificationMessage(qsTr("%1 has been added to your subscription").arg(root.getAddedDeviceName()))
PageController.closePage()
PageController.closePage()
}
function onPostFailed(message) {
root.isSending = false
PageController.showErrorMessage(message)
}
}
}
@@ -1,140 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import PageEnum 1.0
import QRCodeReader 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
PageType {
id: root
property bool scanCompleted: false
Item {
id: cameraArea
anchors.fill: parent
}
Loader {
id: iosQrLoader
anchors.fill: cameraArea
active: Qt.platform.os === "ios"
sourceComponent: Component {
QRCodeReader {
id: qrCodeReader
function updateCameraSize() {
qrCodeReader.setCameraSize(Qt.rect(cameraArea.x,
cameraArea.y,
cameraArea.width,
cameraArea.height))
}
onCodeReaded: function(code) {
if (!code || code.length === 0) {
return
}
var obj = null
try {
obj = JSON.parse(code)
} catch (e) {
obj = null
}
if (!obj || !obj.gw || !(obj.uuid || obj.u)) {
return
}
var normalizedObj = { gw: obj.gw, uuid: (obj.uuid ? obj.uuid : obj.u) }
if (obj.name && obj.name.length > 0) {
normalizedObj.name = obj.name
}
var normalized = JSON.stringify(normalizedObj)
TransferController.setPendingQrCode(normalized)
qrCodeReader.stopReading()
PageController.goToPage(PageEnum.PageSettingsApiAddDeviceConfirm)
}
Component.onCompleted: {
updateCameraSize()
qrCodeReader.startReading()
}
Component.onDestruction: qrCodeReader.stopReading()
}
}
}
onWidthChanged: {
if (iosQrLoader.item && iosQrLoader.item.updateCameraSize) {
iosQrLoader.item.updateCameraSize()
}
}
onHeightChanged: {
if (iosQrLoader.item && iosQrLoader.item.updateCameraSize) {
iosQrLoader.item.updateCameraSize()
}
}
Connections {
target: TransferController
function onScannerShouldStop() {
if (iosQrLoader.item && iosQrLoader.item.stopReading) {
iosQrLoader.item.stopReading()
}
}
}
Connections {
target: ImportController
function onTransferQrDecoded(code) {
if (!code || code.length === 0) {
return
}
var obj = null
try {
obj = JSON.parse(code)
} catch (e) {
obj = null
}
if (obj && obj.gw && (obj.uuid || obj.u)) {
var normalizedObj = { gw: obj.gw, uuid: (obj.uuid ? obj.uuid : obj.u) }
if (obj.name && obj.name.length > 0) {
normalizedObj.name = obj.name
}
code = JSON.stringify(normalizedObj)
}
root.scanCompleted = true
TransferController.setPendingQrCode(code)
Qt.callLater(function() {
PageController.goToPage(PageEnum.PageSettingsApiAddDeviceConfirm)
})
}
function onQrDecodingFinished() {
if (Qt.platform.os === "android" && !root.scanCompleted) {
PageController.closePage()
}
}
}
Component.onCompleted: {
TransferController.setPendingQrCode("")
root.scanCompleted = false
if (Qt.platform.os === "android") {
ImportController.startDecodingQr()
}
}
}
+5 -106
View File
@@ -4,7 +4,6 @@ import QtQuick.Layouts
import QtQuick.Dialogs import QtQuick.Dialogs
import QtCore import QtCore
import QRCodeReader 1.0
import SortFilterProxyModel 0.2 import SortFilterProxyModel 0.2
@@ -20,29 +19,6 @@ import "../Components"
PageType { PageType {
id: root id: root
function isAtDeviceLimit() {
var maxDeviceCount = ApiAccountInfoModel.data("maxDeviceCount")
return listView.count >= maxDeviceCount
}
function getConfigFilesCount() {
try {
var arr = ApiAccountInfoModel.getIssuedConfigsInfo()
if (!arr) return 0
var count = 0
for (var i = 0; i < arr.length; i++) {
var item = arr[i]
if (item && item["source_type"] === "country_config") {
++count
}
}
return count
} catch (e) {
return 0
}
}
ListViewType { ListViewType {
id: listView id: listView
@@ -70,41 +46,16 @@ PageType {
descriptionText: qsTr("Manage currently connected devices") descriptionText: qsTr("Manage currently connected devices")
} }
BasicButtonType { WarningType {
id: addDeviceQrButton
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 16 Layout.topMargin: 16
visible: GC.isMobile()
defaultColor: AmneziaStyle.color.transparent
hoveredColor: AmneziaStyle.color.translucentWhite
pressedColor: AmneziaStyle.color.sheerWhite
textColor: AmneziaStyle.color.paleGray
borderColor: AmneziaStyle.color.paleGray
borderWidth: 1
text: qsTr("Add device by QR code")
clickedFunc: function() {
if (root.isAtDeviceLimit()) {
PageController.goToPage(PageEnum.PageSettingsApiDevicesLimit)
} else {
PageController.goToPage(PageEnum.PageSettingsApiAddDeviceScan)
}
}
}
SmallTextType {
Layout.topMargin: 8
Layout.rightMargin: 16 Layout.rightMargin: 16
Layout.leftMargin: 16 Layout.leftMargin: 16
Layout.fillWidth: true Layout.fillWidth: true
text: qsTr("On the other device, tap + at the bottom → Connect to Amnezia Premium") textString: qsTr("You can find the identifier on the Support tab or, for older versions of the app, "
+ "by tapping '+' and then the three dots at the top of the page.")
iconPath: "qrc:/images/controls/alert-circle.svg"
} }
} }
@@ -142,58 +93,6 @@ PageType {
DividerType {} DividerType {}
} }
footer: ColumnLayout {
width: listView.width
LabelWithButtonType {
Layout.fillWidth: true
Layout.topMargin: 6
text: qsTr("Configuration Files: {%1}").arg(root.getConfigFilesCount())
descriptionText: qsTr("Generated configuration files also count towards the device limit")
rightImageSource: "qrc:/images/controls/chevron-right.svg"
clickedFunction: function() {
ApiSettingsController.updateApiCountryModel()
PageController.goToPage(PageEnum.PageSettingsApiNativeConfigs)
}
}
DividerType {}
WarningType {
Layout.topMargin: 16
Layout.rightMargin: 16
Layout.leftMargin: 16
Layout.bottomMargin: 8
Layout.fillWidth: true
textString: qsTr("The Support tag is available on the Support page. In older versions: tap + in the bottom bar, then More (...) in the top-right.")
iconPath: "qrc:/images/controls/alert-circle.svg"
}
}
Connections {
target: TransferController
function onPostStarted() {
PageController.showBusyIndicator(true)
}
function onPostSucceeded() {
PageController.showBusyIndicator(false)
ApiSettingsController.getAccountInfo(true)
PageController.showNotificationMessage(qsTr("New device added to subscription"))
}
function onPostFailed(message) {
PageController.showBusyIndicator(false)
PageController.showErrorMessage(message)
}
function onScannerShouldStop() {}
}
} }
function deactivateExternalDevice(supportTag, countryCode) { function deactivateExternalDevice(supportTag, countryCode) {
@@ -1,53 +0,0 @@
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
anchors.topMargin: 20
header: ColumnLayout {
width: listView.width
BackButtonType {}
BaseHeaderType {
Layout.fillWidth: true
Layout.rightMargin: 16
Layout.leftMargin: 16
headerText: qsTr("Your Amnezia Premium subscription already has the maximum number of devices — ") + ApiAccountInfoModel.data("connectedDevices")
descriptionText: qsTr("Remove one of the previously connected devices to add a new one")
}
BasicButtonType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 16
text: qsTr("Show all devices")
clickedFunc: function() {
PageController.goToPage(PageEnum.PageSettingsApiDevices)
}
}
}
}
}
@@ -29,6 +29,7 @@ PageType {
readonly property string title: qsTr("Subscription Status") readonly property string title: qsTr("Subscription Status")
readonly property string contentKey: "subscriptionStatus" readonly property string contentKey: "subscriptionStatus"
readonly property string objectImageSource: "qrc:/images/controls/info.svg" readonly property string objectImageSource: "qrc:/images/controls/info.svg"
readonly property bool isRichText: true
} }
QtObject { QtObject {
@@ -37,6 +38,7 @@ PageType {
readonly property string title: qsTr("Valid Until") readonly property string title: qsTr("Valid Until")
readonly property string contentKey: "endDate" readonly property string contentKey: "endDate"
readonly property string objectImageSource: "qrc:/images/controls/history.svg" readonly property string objectImageSource: "qrc:/images/controls/history.svg"
readonly property bool isRichText: false
} }
QtObject { QtObject {
@@ -45,6 +47,7 @@ PageType {
readonly property string title: qsTr("Active Connections") readonly property string title: qsTr("Active Connections")
readonly property string contentKey: "connectedDevices" readonly property string contentKey: "connectedDevices"
readonly property string objectImageSource: "qrc:/images/controls/monitor.svg" readonly property string objectImageSource: "qrc:/images/controls/monitor.svg"
readonly property bool isRichText: false
} }
property var processedServer property var processedServer
@@ -104,7 +107,7 @@ PageType {
actionButtonImage: "qrc:/images/controls/edit-3.svg" actionButtonImage: "qrc:/images/controls/edit-3.svg"
headerText: root.processedServer && root.processedServer.name ? root.processedServer.name : "" headerText: root.processedServer.name
descriptionText: ApiAccountInfoModel.data("serviceDescription") descriptionText: ApiAccountInfoModel.data("serviceDescription")
actionButtonFunction: function() { actionButtonFunction: function() {
@@ -134,6 +137,7 @@ PageType {
imageSource: objectImageSource imageSource: objectImageSource
leftText: title leftText: title
rightText: ApiAccountInfoModel.data(contentKey) rightText: ApiAccountInfoModel.data(contentKey)
rightTextFormat: isRichText ? Text.RichText : Text.PlainText
visible: rightText !== "" visible: rightText !== ""
} }
@@ -355,7 +359,7 @@ PageType {
PageController.showNotificationMessage(qsTr("Cannot unlink device during active connection")) PageController.showNotificationMessage(qsTr("Cannot unlink device during active connection"))
} else { } else {
PageController.showBusyIndicator(true) PageController.showBusyIndicator(true)
if (ApiConfigsController.deactivateDevice()) { if (ApiConfigsController.deactivateDevice(false)) {
ApiSettingsController.getAccountInfo(true) ApiSettingsController.getAccountInfo(true)
} }
PageController.showBusyIndicator(false) PageController.showBusyIndicator(false)
@@ -392,7 +396,7 @@ PageType {
PageController.showNotificationMessage(qsTr("Cannot remove server during active connection")) PageController.showNotificationMessage(qsTr("Cannot remove server during active connection"))
} else { } else {
PageController.showBusyIndicator(true) PageController.showBusyIndicator(true)
if (ApiConfigsController.deactivateDevice()) { if (ApiConfigsController.deactivateDevice(true)) {
InstallController.removeProcessedServer() InstallController.removeProcessedServer()
} }
PageController.showBusyIndicator(false) PageController.showBusyIndicator(false)
@@ -413,6 +417,6 @@ PageType {
anchors.fill: parent anchors.fill: parent
expandedHeight: parent.height * 0.35 expandedHeight: parent.height * 0.35
serverNameText: root.processedServer && root.processedServer.name ? root.processedServer.name : "" serverNameText: root.processedServer.name
} }
} }
@@ -6,6 +6,8 @@ import Qt.labs.platform 1.1
import QtCore import QtCore
import SortFilterProxyModel 0.2
import PageEnum 1.0 import PageEnum 1.0
import Style 1.0 import Style 1.0
@@ -17,6 +19,33 @@ import "../Components"
PageType { PageType {
id: root 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: { Component.onCompleted: {
PageController.showBusyIndicator(true) PageController.showBusyIndicator(true)
ApiConfigsController.prepareVpnKeyExport() ApiConfigsController.prepareVpnKeyExport()
@@ -40,7 +69,7 @@ PageType {
Layout.leftMargin: 16 Layout.leftMargin: 16
Layout.rightMargin: 16 Layout.rightMargin: 16
Layout.topMargin: 16 Layout.topMargin: 16
text: qsTr("Amnezia Premium\nsubscription key") text: qsTr(root.processedServer.name + "\nsubscription key")
font.pixelSize: 32 font.pixelSize: 32
font.bold: true font.bold: true
color: AmneziaStyle.color.paleGray color: AmneziaStyle.color.paleGray
@@ -53,18 +82,10 @@ PageType {
Layout.leftMargin: 16 Layout.leftMargin: 16
Layout.rightMargin: 16 Layout.rightMargin: 16
defaultColor: AmneziaStyle.color.paleGray
hoveredColor: AmneziaStyle.color.sheerWhite
pressedColor: AmneziaStyle.color.translucentWhite
disabledColor: AmneziaStyle.color.mutedGray
textColor: AmneziaStyle.color.black
leftImageColor: "black"
borderWidth: 1
text: qsTr("Copy key") text: qsTr("Copy key")
leftImageSource: "qrc:/images/controls/copy.svg" leftImageSource: "qrc:/images/controls/copy.svg"
onClicked: { clickedFunc: function() {
ApiConfigsController.copyVpnKeyToClipboard() ApiConfigsController.copyVpnKeyToClipboard()
PageController.showNotificationMessage(qsTr("Copied")) PageController.showNotificationMessage(qsTr("Copied"))
} }
@@ -85,20 +106,20 @@ PageType {
text: qsTr("Save key as a file") text: qsTr("Save key as a file")
leftImageSource: "qrc:/images/controls/share-2.svg" leftImageSource: "qrc:/images/controls/share-2.svg"
onClicked: { clickedFunc: function() {
var fileName = GC.isMobile() var fileName = GC.isMobile()
? "amnezia_vpn_key.vpn" ? root.processedServer.name.toLowerCase().replace(/\s+/g, "_") + "_key.vpn"
: SystemController.getFileName( : SystemController.getFileName(
qsTr("Save AmneziaVPN config"), qsTr("Save AmneziaVPN config"),
qsTr("Config files (*.vpn)"), qsTr("Config files (*.vpn)"),
StandardPaths.standardLocations(StandardPaths.DocumentsLocation) + "/amnezia_vpn_key", StandardPaths.standardLocations(StandardPaths.DocumentsLocation) + "/" + root.processedServer.name.toLowerCase().replace(/\s+/g, "_") + "_key",
true, true,
".vpn" ".vpn"
) )
if (fileName !== "") { if (fileName !== "") {
PageController.showBusyIndicator(true) PageController.showBusyIndicator(true)
ExportController.exportConfig(fileName) ApiConfigsController.exportVpnKey(fileName)
PageController.showBusyIndicator(false) PageController.showBusyIndicator(false)
} }
} }
@@ -118,7 +139,7 @@ PageType {
text: qsTr("Show key text") text: qsTr("Show key text")
leftImageSource: "qrc:/images/controls/eye.svg" leftImageSource: "qrc:/images/controls/eye.svg"
onClicked: { clickedFunc: function() {
PageController.showBusyIndicator(true) PageController.showBusyIndicator(true)
ApiConfigsController.prepareVpnKeyExport() ApiConfigsController.prepareVpnKeyExport()
PageController.showBusyIndicator(false) PageController.showBusyIndicator(false)
@@ -127,8 +148,9 @@ PageType {
} }
Rectangle { Rectangle {
Layout.fillWidth: true Layout.preferredWidth: Math.min(Math.min(root.width - (Layout.leftMargin + Layout.rightMargin), root.height * 0.5), 360)
Layout.preferredHeight: width Layout.preferredHeight: Layout.preferredWidth
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 20 Layout.topMargin: 20
Layout.leftMargin: 16 Layout.leftMargin: 16
Layout.rightMargin: 16 Layout.rightMargin: 16
@@ -140,6 +162,9 @@ PageType {
Image { Image {
anchors.fill: parent anchors.fill: parent
smooth: false smooth: false
fillMode: Image.PreserveAspectFit
sourceSize.width: parent.width
sourceSize.height: parent.height
source: ApiConfigsController.qrCodesCount > 0 && ApiConfigsController.qrCodes[0] ? ApiConfigsController.qrCodes[0] : "" source: ApiConfigsController.qrCodesCount > 0 && ApiConfigsController.qrCodes[0] ? ApiConfigsController.qrCodes[0] : ""
} }
} }
@@ -181,7 +206,7 @@ PageType {
Header2Type { Header2Type {
Layout.fillWidth: true Layout.fillWidth: true
headerText: qsTr("Amnezia Premium Subscription key") headerText: qsTr(root.processedServer.name + " Subscription key")
} }
TextArea { TextArea {
@@ -194,7 +219,7 @@ PageType {
font.pixelSize: 16 font.pixelSize: 16
font.weight: Font.Medium font.weight: Font.Medium
font.family: "PT Root UI VF" font.family: "PT Root UI VF"
text: ApiConfigsController.vpnKey //|| "" text: ApiConfigsController.vpnKey
wrapMode: Text.Wrap wrapMode: Text.Wrap
background: Rectangle { color: AmneziaStyle.color.transparent } background: Rectangle { color: AmneziaStyle.color.transparent }
} }
@@ -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 { ListViewType {
id: listView id: listView
ScrollBar.vertical: ScrollBarType { policy: ScrollBar.AlwaysOn }
anchors.top: header.bottom anchors.top: header.bottom
anchors.bottom: addAppButton.top anchors.bottom: addAppButton.top
anchors.left: parent.left anchors.left: parent.left
@@ -66,7 +66,7 @@ PageType {
text: qsTr("Allow application screenshots") text: qsTr("Allow application screenshots")
checked: SettingsController.isScreenshotsEnabled() checked: SettingsController.isScreenshotsEnabled()
onCheckedChanged: { onToggled: function() {
if (checked !== SettingsController.isScreenshotsEnabled()) { if (checked !== SettingsController.isScreenshotsEnabled()) {
SettingsController.toggleScreenshotsEnabled(checked) SettingsController.toggleScreenshotsEnabled(checked)
} }
@@ -109,7 +109,7 @@ PageType {
descriptionText: qsTr("Launch the application every time the device is starts") descriptionText: qsTr("Launch the application every time the device is starts")
checked: SettingsController.isAutoStartEnabled() checked: SettingsController.isAutoStartEnabled()
onCheckedChanged: { onToggled: function() {
if (checked !== SettingsController.isAutoStartEnabled()) { if (checked !== SettingsController.isAutoStartEnabled()) {
SettingsController.toggleAutoStart(checked) SettingsController.toggleAutoStart(checked)
} }
@@ -132,7 +132,7 @@ PageType {
descriptionText: qsTr("Connect to VPN on app start") descriptionText: qsTr("Connect to VPN on app start")
checked: SettingsController.isAutoConnectEnabled() checked: SettingsController.isAutoConnectEnabled()
onCheckedChanged: { onToggled: function() {
if (checked !== SettingsController.isAutoConnectEnabled()) { if (checked !== SettingsController.isAutoConnectEnabled()) {
SettingsController.toggleAutoConnect(checked) SettingsController.toggleAutoConnect(checked)
} }
@@ -157,9 +157,9 @@ PageType {
enabled: switcherAutoStart.checked enabled: switcherAutoStart.checked
opacity: enabled ? 1.0 : 0.5 opacity: enabled ? 1.0 : 0.5
checked: SettingsController.isStartMinimizedEnabled() checked: SettingsController.startMinimized
onCheckedChanged: { onToggled: function() {
if (checked !== SettingsController.isStartMinimizedEnabled()) { if (checked !== SettingsController.startMinimized) {
SettingsController.toggleStartMinimized(checked) SettingsController.toggleStartMinimized(checked)
} }
} }
@@ -66,7 +66,7 @@ PageType {
descriptionText: qsTr("If AmneziaDNS is installed on the server") descriptionText: qsTr("If AmneziaDNS is installed on the server")
checked: SettingsController.isAmneziaDnsEnabled() checked: SettingsController.isAmneziaDnsEnabled()
onCheckedChanged: { onToggled: function() {
if (checked !== SettingsController.isAmneziaDnsEnabled()) { if (checked !== SettingsController.isAmneziaDnsEnabled()) {
SettingsController.toggleAmneziaDns(checked) SettingsController.toggleAmneziaDns(checked)
} }
+8 -11
View File
@@ -64,7 +64,7 @@ PageType {
checked: SettingsController.isLoggingEnabled checked: SettingsController.isLoggingEnabled
onCheckedChanged: { onToggled: function() {
if (checked !== SettingsController.isLoggingEnabled) { if (checked !== SettingsController.isLoggingEnabled) {
SettingsController.isLoggingEnabled = checked SettingsController.isLoggingEnabled = checked
} }
@@ -167,7 +167,8 @@ PageType {
// Show service logs only if this is NOT a macOS build with // Show service logs only if this is NOT a macOS build with
// Network-Extension (IsMacOsNeBuild is injected from C++ at run-time) // Network-Extension (IsMacOsNeBuild is injected from C++ at run-time)
property list<QtObject> logTypes: IsMacOsNeBuild ? [ // or if this is NOT a mobile build
property list<QtObject> logTypes: (IsMacOsNeBuild || GC.isMobile()) ? [
clientLogs clientLogs
] : [ ] : [
clientLogs, clientLogs,
@@ -214,15 +215,11 @@ PageType {
} }
readonly property var exportLogsHandler: function() { readonly property var exportLogsHandler: function() {
var fileName = "" var fileName = ""
if (GC.isMobile()) { fileName = SystemController.getFileName(qsTr("Save"),
fileName = "AmneziaVPN-service.log" qsTr("Logs files (*.log)"),
} else { StandardPaths.standardLocations(StandardPaths.DocumentsLocation) + "/AmneziaVPN-service",
fileName = SystemController.getFileName(qsTr("Save"), true,
qsTr("Logs files (*.log)"), ".log")
StandardPaths.standardLocations(StandardPaths.DocumentsLocation) + "/AmneziaVPN-service",
true,
".log")
}
if (fileName !== "") { if (fileName !== "") {
PageController.showBusyIndicator(true) PageController.showBusyIndicator(true)
SettingsController.exportServiceLogsFile(fileName) SettingsController.exportServiceLogsFile(fileName)
@@ -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
}
}
}
}
@@ -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 {}
}
}
}
}
@@ -164,6 +164,8 @@ PageType {
ListViewType { ListViewType {
id: listView id: listView
ScrollBar.vertical: ScrollBarType { policy: ScrollBar.AlwaysOn }
anchors.top: header.bottom anchors.top: header.bottom
anchors.topMargin: 16 anchors.topMargin: 16
anchors.bottom: addSiteButton.top anchors.bottom: addSiteButton.top
@@ -348,7 +350,6 @@ PageType {
Layout.fillWidth: true Layout.fillWidth: true
text: qsTr("Clear site list") text: qsTr("Clear site list")
rightImageSource: "qrc:/images/controls/trash.svg"
clickedFunction: function() { clickedFunction: function() {
var headerText = qsTr("Clear site list?") var headerText = qsTr("Clear site list?")
@@ -66,6 +66,8 @@ PageType {
imageSource: imagePath imageSource: imagePath
leftText: lText leftText: lText
rightText: rText rightText: rText
visible: isVisible
} }
} }
@@ -85,7 +87,7 @@ PageType {
textFormat: Text.RichText textFormat: Text.RichText
text: { text: {
var text = ApiServicesModel.getSelectedServiceData("features") 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 { MouseArea {

Some files were not shown because too many files have changed in this diff Show More