Compare commits

...

86 Commits

Author SHA1 Message Date
albexk ea278e458a fix: remove mandatory requirement for android.software.leanback 2025-01-09 14:32:56 +03:00
Nethius e43aa02a5b chore: changed the icon for the settings section (#1348) 2025-01-09 13:33:35 +07:00
Mikhail Kiselev c3fb62a6ab fix: rewrite linux router dns flusher (#1335)
Co-authored-by: sund3RRR <evenquantity@gamil.com>
2025-01-08 14:38:53 +07:00
Nethius 62f3a339b7 bugfix: ui fixes after merge with android tab navigation branch (#1339)
* bugfix: ui fixes after merge with android tab navigation branch

* bugfix: fix crash on quit

* chore: fix typos

* chore: remove useless comment

* bugfix: fix trigger behavior for `ListViewWithRadioButtonType`

* bugfix: fixed dropdown listview scrolling

* bugfix: fixed amfree availability display

* chore: remove item existence check in triggerCurrentItem function

---------

Co-authored-by: Cyril Anisimov <CyAn84@gmail.com>
2025-01-08 13:12:55 +07:00
Mykola Baibuz 767b14b37a Improve XRay protocol process close (#1318) 2025-01-07 21:52:10 +07:00
Nethius e7fa160c9c feature: added ad label on page home (#1316)
* feature: added ad label on page home
2025-01-07 10:38:32 +07:00
Vitaly 7350d79c50 feature: WG and AWG: Subnet IP setting change support (#1323)
feature: wg/awg subnet ip setting change support
2025-01-02 14:07:12 +07:00
Mykola Baibuz 86f08554cd fix: check for Linux firewall install before use it (#1328)
* bugfix: check for Linux firewall install before use it

* XRay Linux firewall rules
2024-12-31 10:23:53 +07:00
Andrey Alekseenko a741186c21 bugfix: Correctly use QProcess::start and QProcess::execute (#1331)
Affected functions (all on Linux/Mac):
 - `RouterLinux::flushDns` was not reloading the DNS manager.
 - `Utils::processIsRunning` was always saying that the process
   is not running when `fullFlag` was set to `false`.
 - `Utils::killProcessByName` was not killing anything.
2024-12-31 10:21:40 +07:00
Cyril Anisimov 6acaab0ffa Improve navigation cpp (#1061)
* add focusController class

* add more key handlers

* add focus navigation to qml

* fixed language selector

* add reverse focus change to FocusController

* add default focus item

* update transitions

* update pages

* add ListViewFocusController

* fix ListView navigation

* update CardType for using with focus navigation

* remove useless key navigation

* remove useless slots, logs, Drawer open and close

* fix reverse focus move on listView

* fix drawer radio buttons selection

* fix drawer layout and focus move

* fix PageSetupWizardProtocolSettings focus move

* fix back navigation on default focus item

* fix crashes after ListView navigation

* fix protocol settings focus move

* fix focus on users on page share

* clean up page share

* fix server rename

* fix page share default server selection

* refactor about page for correct focus move

* fix focus move on list views with header and-or footer

* minor fixes

* fix server list back button handler

* fix spawn signals on switch

* fix share details drawer

* fix drawer open close usage

* refactor listViewFocusController

* refactor focusController to make the logic more
straightforward

* fix focus on notification

* update config page for scrolling with tab

* fix crash on return with esc key

* fix focus navigation in dynamic delegate of list view

* fix focus move on qr code on share page

* refactor page logging settings for focus navigation

* update popup

* Bump version

* Add mandatory requirement for android.software.leanback.

* Fix importing files on TVs

* fix: add separate method for reading files to fix file reading on Android TV

* fix(android): add CHANGE_NETWORK_STATE permission for all Android versions

* Fix connection check for AWG/WG

* chore: minor fixes (#1235)

* fix: add a workaround to open files on Android TV due to lack of SAF

* fix: change the banner format for TV

* refactor: make TvFilePicker activity more sustainable

* fix: add the touch emulation method for Android TV

* fix: null uri processing

* fix: add the touch emulation method for Android TV

* fix: hide UI elements that use file saving

* chore: bump version code

* add `ScrollBarType`

* update initial config page

* refactor credentials setup page to handle the focus navigation

* add `setDelegateIndex` method to `listViewFocusController`

* fix focus behavior on new page/popup

* make minor fixes and clean up

* fix: get rid of the assign function call

* Scrollbar is on if the content is larger than a screen

* Fix selection in language change list

* Update select language list

* update logging settings page

* fix checked item in lists

* fix split tunneling settings

* make unchangable properties readonly

* refactor SwitcherType

* fix hide/unhide password

* `PageShare` readonly properties

* Fix list view focus moving on `PageShare`

* remove manual focus control on `PageShare`

* format `ListViewFocusController`

* format `FocusController`

* add `focusControl` with utility functions for
focus control

* refactor `listViewFocusController` acoording to `focusControl`

* refactor `focusConroller` according to `focusControl`

* add `printSectionName` method to `listViewController`

* remove arrow from `Close application` item

* fix focus movement in `ServersListView`

* `Restore from backup` is visible only on start screen

* `I have nothing` is visible only on start screen

* fix back button on `SelectLanguageDrawer`

* rename `focusControl` to `qmlUtils`

* fix `CMakeLists.txt`

* fix `ScrollBarType`

* fix `PageSetupWizardApiServicesList`

* fix focus movement on dynamic delegates in listView

* refactor `PageSetupWizardProtocols`

* remove comments and clean up

* fix `ListViewWithLabelsType`

* fix `PageProtocolCloakSettings`

* fix `PageSettingsAppSplitTunneling`

* fix `PageDevMenu`

* remove debug output from `FocusController`

* remove debug output from `ListViewFocusController`

* remove debug output from `focusControl`

* `focusControl` => `FocusControl`

---------

Co-authored-by: albexk <albexk@proton.me>
Co-authored-by: Nethius <nethiuswork@gmail.com>
2024-12-31 10:16:52 +07:00
Andrey Alekseenko 212e9b3a91 fix: adding second new VMess links now works (#1325) 2024-12-30 12:45:26 +07:00
Mikhail Kiselev 2bff37efae fix: segmentation violation due to missing return (#1321) 2024-12-28 12:02:14 +07:00
albexk b88ab8e432 fix(build): fix aqtinstall (#1312) 2024-12-23 08:27:09 +07:00
Nethius 48f6cf904e chore/minor UI fixes (#1308)
* chore: corrected the translation error

* bugfix: fixed basic button left iamge color
2024-12-19 14:36:20 +07:00
KsZnak 367789bda2 Update README_RU.md (#1300)
* Update README_RU.md
2024-12-14 19:29:33 +07:00
Cyril Anisimov d06924c59d feature/xray user management (#972)
* feature: implement client management functionality for Xray

---------

Co-authored-by: aiamnezia <ai@amnezia.org>
Co-authored-by: vladimir.kuznetsov <nethiuswork@gmail.com>
2024-12-10 09:17:16 +07:00
Nethius 2db99715b1 feature: added subscription expiration date for premium v2 (#1261)
* feature: added subscription expiration date for premium v2

* feature: added a check for the presence of the “services” field in the response body of the getServicesList() function

* feature: added prohibition to change location when connection is active

* bugfix: renamed public_key->end_date to public_key->expires_at according to the changes on the backend
2024-12-09 13:32:49 +07:00
pokamest 9688a8e52d Merge pull request #1292 from amnezia-vpn/Add-english-option
Update README.md
2024-12-08 10:40:20 +01:00
pokamest b2c429f74d Merge pull request #1291 from amnezia-vpn/readme_ru_update
Update README_RU.md
2024-12-08 10:39:01 +01:00
Nethius 6ea6ab1bd9 chore: added clang-format config files (#1293) 2024-12-08 12:14:22 +07:00
KsZnak c5aa070bf4 Update README_RU.md 2024-12-08 05:49:26 +02:00
KsZnak d67201ede9 Update README.md 2024-12-08 05:34:18 +02:00
pokamest 4323fb2063 Merge pull request #1290 from amnezia-vpn/Readme_ru
Add files via upload
2024-12-07 15:36:56 +01:00
pokamest 6d5452b8ee Merge pull request #1288 from amnezia-vpn/Readme-ru
Update README_RU.md
2024-12-07 15:36:08 +01:00
KsZnak 569d63ef0f Add files via upload 2024-12-07 15:53:40 +02:00
KsZnak ea910ba300 Update README_RU.md 2024-12-06 22:15:01 +02:00
Pokamest Nikak 1c1e74d06f ru readme 2024-12-06 12:40:04 +00:00
Nethius 5dc16c06f1 chore: increased the api request timeout (#1276) 2024-12-03 12:47:33 +07:00
Nethius 4efaf20a1c chore: fix deploy workflow (#1280) 2024-12-02 14:46:20 +07:00
Pokamest Nikak 9d96b1cd13 Update Readme 2024-11-29 22:10:35 +00:00
Anton Sosnin 1d721ffb9a SteamDeck/OS installation fix (#1270) 2024-11-27 09:55:23 +07:00
Nethius 2130131a9d bugfix: added scroll on page with services list (#1262)
* added scroll on page with services list

* fixed margins on PageSetupWizardApiServicesList
2024-11-26 11:41:17 +07:00
Aftershock669 e0b091b474 Update readme (#1267) 2024-11-25 23:51:46 +07:00
Nethius 8547de82ea bump xcode-version for macos build (#1249) 2024-11-14 10:58:04 +07:00
Nethius aa871bd1c9 feature: added country selection on home page drawer (#1215) 2024-11-12 13:22:34 +07:00
albexk 23806e1def chore: bump version to 4.8.2.4 (#1240) 2024-11-08 15:22:16 +07:00
Nethius 31867993ce chore: minor fixes (#1235) 2024-11-06 12:57:39 +07:00
pokamest 7b7a922d92 Merge pull request #1226 from amnezia-vpn/fix/android-awg-connection
Fix connection check for AWG/WG
2024-11-05 12:25:49 +01:00
pokamest 09bd958d8d Merge pull request #1231 from amnezia-vpn/fix/android-network-state
Add CHANGE_NETWORK_STATE permission for all Android versions
2024-11-05 12:25:11 +01:00
albexk 576e2226fe fix(android): add CHANGE_NETWORK_STATE permission for all Android versions 2024-11-03 16:11:23 +03:00
albexk 1533270e4e Fix connection check for AWG/WG 2024-11-02 00:54:24 +03:00
albexk e7b25719e4 Chore/bump version (#1204)
* chore: bump Android version code

---------

Co-authored-by: Nethius <nethiuswork@gmail.com>
2024-10-25 23:23:05 +07:00
pokamest 7183e8541c Merge pull request #1202 from Aftershock669/update-readme
Fix / Update README
2024-10-25 16:43:50 +01:00
Nethius 9e71e64cbd chore: bump version to 4.8.2.3 (#1203) 2024-10-25 22:19:28 +07:00
Aftershock669 4f3bae4a9a Fix / Update README 2024-10-25 17:00:28 +03:00
pokamest 990059f8a6 Merge pull request #1200 from amnezia-vpn/bugfix/proxy-bypass-enc-check 2024-10-25 10:50:07 +01:00
vladimir.kuznetsov af55af5e76 bugfix: fixed proxy bypass encryption check 2024-10-25 17:48:22 +08:00
pokamest 82d96a9691 Merge pull request #1197 from Aftershock669/update-readme
Update README
2024-10-24 23:57:58 +01:00
Aftershock669 9f3f215452 Update README
- add website mirror links
- remove direct platform download links
- add "Testiny" sponsored badge
2024-10-24 22:32:35 +03:00
pokamest 2dfc6a87b8 Merge pull request #1196 from amnezia-vpn/bump-version
Bump version to 4.8.2.1
2024-10-24 17:36:50 +01:00
albexk 7261a86c48 Bump version to 4.8.2.1 2024-10-24 19:25:44 +03:00
pokamest 2946dd2278 Merge pull request #1124 from amnezia-vpn/bugfix/page-share-recursive-rearrange
bugfix: fixed clientInfoDrawer.expandedHeight recursive rearrange
2024-10-24 16:39:04 +01:00
vladimir.kuznetsov 5065262aac bugfix: fixed clientInfoDrawer recursive rearrange 2024-10-24 23:37:42 +08:00
Nethius 4685d3b543 bugfix/api auth data saving (#1195)
* bugfix: fixed authData saving

* bugfix: added serviceInfo processing from api response
2024-10-24 16:12:53 +01:00
pokamest 7a389e8755 Merge pull request #1188 from amnezia-vpn/chore/global-network-manager
chore/using the global network manager
2024-10-24 16:10:57 +01:00
vladimir.kuznetsov 4e5daf22a3 Merge branch 'dev' of github.com:amnezia-vpn/amnezia-client into chore/global-network-manager 2024-10-24 22:53:56 +08:00
pokamest 3bf9c10d7d Merge pull request #1192 from amnezia-vpn/bugfix/awg-wg-routes-vpnconnection
bugfix/removed adding routes in vpnconnection class for awg and wg protocols
2024-10-24 14:11:26 +01:00
pokamest 2e175cb9fc Merge pull request #1189 from amnezia-vpn/feature/support-tag
feature/added support tag to PageSetupWizardConfigSource
2024-10-24 14:08:28 +01:00
pokamest 823c1b5d3a Merge pull request #1190 from amnezia-vpn/chore/win-routes-logger
chore/displaying route addresses when adding to split tunneling fails
2024-10-24 14:04:45 +01:00
pokamest 92bc1a6f09 Merge pull request #1194 from amnezia-vpn/feature/proxy-bypass-checks
feature/proxy bypass checks
2024-10-24 14:03:56 +01:00
vladimir.kuznetsov d511220f8b added a randomized proxy bypass 2024-10-24 10:59:50 +08:00
vladimir.kuznetsov 923e358aaa added a check to trigger proxy bypass 2024-10-24 01:02:30 +08:00
vladimir.kuznetsov 92b19eccf6 bugfix/removed adding routes in vpnconnection class for awg and wg protocols 2024-10-23 00:33:22 +08:00
vladimir.kuznetsov 5358aaeb00 chore/displaying route addresses when adding to split tunneling fails 2024-10-22 23:14:41 +08:00
vladimir.kuznetsov e31a2066c0 feature/added support tag to PageSetupWizardConfigSource 2024-10-22 23:05:58 +08:00
vladimir.kuznetsov 928c4f18c9 chore/using the global network manager 2024-10-22 22:24:23 +08:00
pokamest 628e22869d Merge pull request #1085 from amnezia-vpn/bugfix/win_check_ps
Refactoring wmic to winapi
2024-10-18 15:45:32 +01:00
Pokamest Nikak c9cd860654 Merge branch 'dev' into bugfix/win_check_ps 2024-10-18 15:42:08 +01:00
pokamest 17984adae5 Merge pull request #1181 from amnezia-vpn/chore/workflow-envs
chore: added new env for workflows
2024-10-18 15:02:36 +01:00
vladimir.kuznetsov 5601bc4fdf chore: added new env for workflows 2024-10-18 21:39:09 +08:00
pokamest e14681801e Merge pull request #1086 from amnezia-vpn/bugfix/pw_rnd_gen
Switched to secure PRNG & some pw len increased
2024-10-18 14:17:33 +01:00
pokamest f106b4d367 Merge pull request #1117 from amnezia-vpn/feature/process-auth-data
added processing of auth_data section when requesting api config
2024-10-18 10:57:57 +01:00
Nethius 74802f30ed feature/proxy storage bypass (#1179)
* feature: added proxy storage bypass
- added encryption error handling to apiController

* chore: fixed include
2024-10-18 10:57:38 +01:00
albexk d63bf15011 Android qt 6.7.3 (#1143)
* Up Qt to 6.7.3

* Bump version to 4.8.2.0

* Raise the minimum Android version to 8 (API 26)

* Update version code to separate versions for new and old Androids

* Fix mouse not working on TVs

* Refactor logging

* Bump version code
2024-10-18 10:52:24 +01:00
Nethius 60de146f03 chore/mozilla upstream (#1136)
* cherry-pick commit 5a51e292d44ec0fb07867aff0401b4c2a8fca1e8 from mozila upstream

* cherry-pick commit e8ecb857dcfb804b7766a54e725b442fc6c0e661 from mozila upstream

* cherry-pick commit 16269ffa600905b09678014f64951748fb0ff8ad from mozila upstream
2024-10-18 10:47:53 +01:00
pokamest c4f32eed31 Merge pull request #1180 from amnezia-vpn/bugfix/open-file-error-missing-text
bugfix: added missing text in the errors [no ci]
2024-10-18 10:45:10 +01:00
vladimir.kuznetsov 2c9067b0de bugfix: added missing text in the errors 2024-10-18 14:57:20 +08:00
pokamest 6844a2375b Merge pull request #1107 from amnezia-vpn/chore/fix-warnings
chore: added clear() after extractConfigFromData() on android
2024-10-13 12:18:46 +01:00
Nethius 7b838e77a0 bugfix: removed the importErrorOccurred() signal overload, since qml does not know how to handle signal overloads (#1111) 2024-10-13 12:14:43 +01:00
Nethius 694e781beb bugfix: fixed path to log folder for wireguard on windows (#1137) 2024-10-11 08:58:53 +07:00
Nethius 399a8c6d28 bugfix: fixed qml warnings when loading user list on PageShare (#1119) 2024-10-11 08:58:30 +07:00
vladimir.kuznetsov dce08b3ecc added processing of auth_data section when requesting api config
- fixed saving api_config section when processing backend response
2024-10-06 13:23:19 +08:00
vladimir.kuznetsov 2763da960f chore: added clear() after extractConfigFromData() on android 2024-10-02 13:20:16 +08:00
Pokamest Nikak 1542adba82 Switched to secure PRNG & some pw len increased 2024-09-23 00:44:25 +01:00
Pokamest Nikak 3aa8a46f6e wip 2024-09-23 01:19:46 +03:00
Pokamest Nikak 1f08d78b43 wip 2024-09-22 22:52:59 +01:00
196 changed files with 6080 additions and 4391 deletions
+39
View File
@@ -0,0 +1,39 @@
BasedOnStyle: WebKit
AccessModifierOffset: '-4'
AlignAfterOpenBracket: Align
AlignConsecutiveMacros: 'true'
AlignTrailingComments: 'true'
AllowAllArgumentsOnNextLine: 'true'
AllowAllParametersOfDeclarationOnNextLine: 'true'
AllowShortBlocksOnASingleLine: 'false'
AllowShortCaseLabelsOnASingleLine: 'true'
AllowShortEnumsOnASingleLine: 'false'
AllowShortFunctionsOnASingleLine: None
AlwaysBreakTemplateDeclarations: 'No'
BreakBeforeBinaryOperators: NonAssignment
BreakBeforeBraces: Custom
BraceWrapping:
AfterClass: true
AfterControlStatement: false
AfterEnum: false
AfterFunction: true
AfterNamespace: true
AfterObjCDeclaration: false
AfterStruct: true
AfterUnion: false
BeforeCatch: false
BeforeElse: false
IndentBraces: false
BreakConstructorInitializers: BeforeColon
ColumnLimit: '120'
CommentPragmas: '"^!|^:"'
ConstructorInitializerAllOnOneLineOrOnePerLine: 'true'
ConstructorInitializerIndentWidth: '4'
ContinuationIndentWidth: '8'
IndentPPDirectives: BeforeHash
NamespaceIndentation: All
PenaltyExcessCharacter: '10'
PointerAlignment: Right
SortIncludes: 'true'
SpaceAfterTemplateKeyword: 'false'
Standard: Auto
+20
View File
@@ -0,0 +1,20 @@
/client/3rd
/client/3rd-prebuild
/client/android
/client/cmake
/client/core/serialization
/client/daemon
/client/fonts
/client/images
/client/ios
/client/mozilla
/client/platforms/dummy
/client/platforms/linux
/client/platforms/macos
/client/platforms/windows
/client/server_scripts
/client/translations
/deploy
/docs
/metadata
/service/src
+32 -8
View File
@@ -16,7 +16,10 @@ jobs:
QT_VERSION: 6.6.2
QIF_VERSION: 4.7
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
steps:
- name: 'Install Qt'
@@ -83,7 +86,10 @@ jobs:
QIF_VERSION: 4.7
BUILD_ARCH: 64
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
steps:
- name: 'Get sources'
@@ -146,7 +152,10 @@ jobs:
CC: cc
CXX: c++
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
steps:
- name: 'Setup xcode'
@@ -208,7 +217,11 @@ jobs:
export QT_BIN_DIR="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/ios/bin"
export QT_MACOS_ROOT_DIR="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/macos"
export PATH=$PATH:~/go/bin
sh deploy/build_ios.sh
sh deploy/build_ios.sh | \
sed -e '/-Xcc -DPROD_AGW_PUBLIC_KEY/,/-Xcc/ { /-Xcc/!d; }' -e '/-Xcc -DPROD_AGW_PUBLIC_KEY/d' | \
sed -e '/-Xcc -DDEV_AGW_PUBLIC_KEY/,/-Xcc/ { /-Xcc/!d; }' -e '/-Xcc -DDEV_AGW_PUBLIC_KEY/d' | \
sed -e '/-DPROD_AGW_PUBLIC_KEY/,/-D/ { /-D/!d; }' -e '/-DPROD_AGW_PUBLIC_KEY/d' | \
sed -e '/-DDEV_AGW_PUBLIC_KEY/,/-D/ { /-D/!d; }' -e '/-DDEV_AGW_PUBLIC_KEY/d'
env:
IOS_TRUST_CERT_BASE64: ${{ secrets.IOS_TRUST_CERT_BASE64 }}
IOS_SIGNING_CERT_BASE64: ${{ secrets.IOS_SIGNING_CERT_BASE64 }}
@@ -238,13 +251,16 @@ jobs:
QT_VERSION: 6.4.3
QIF_VERSION: 4.6
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
steps:
- name: 'Setup xcode'
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '14.3.1'
xcode-version: '15.4.0'
- name: 'Install Qt'
uses: jurplel/install-qt-action@v3
@@ -301,10 +317,13 @@ jobs:
env:
ANDROID_BUILD_PLATFORM: android-34
QT_VERSION: 6.7.2
QT_VERSION: 6.7.3
QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools'
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
steps:
- name: 'Install desktop Qt'
@@ -316,7 +335,8 @@ jobs:
arch: 'linux_gcc_64'
modules: ${{ env.QT_MODULES }}
dir: ${{ runner.temp }}
extra: '--external 7z --base ${{ env.QT_MIRROR }}'
py7zrversion: '==0.22.*'
extra: '--base ${{ env.QT_MIRROR }}'
- name: 'Install android_x86_64 Qt'
uses: jurplel/install-qt-action@v4
@@ -327,7 +347,8 @@ jobs:
arch: 'android_x86_64'
modules: ${{ env.QT_MODULES }}
dir: ${{ runner.temp }}
extra: '--external 7z --base ${{ env.QT_MIRROR }}'
py7zrversion: '==0.22.*'
extra: '--base ${{ env.QT_MIRROR }}'
- name: 'Install android_x86 Qt'
uses: jurplel/install-qt-action@v4
@@ -338,7 +359,8 @@ jobs:
arch: 'android_x86'
modules: ${{ env.QT_MODULES }}
dir: ${{ runner.temp }}
extra: '--external 7z --base ${{ env.QT_MIRROR }}'
py7zrversion: '==0.22.*'
extra: '--base ${{ env.QT_MIRROR }}'
- name: 'Install android_armv7 Qt'
uses: jurplel/install-qt-action@v4
@@ -349,7 +371,8 @@ jobs:
arch: 'android_armv7'
modules: ${{ env.QT_MODULES }}
dir: ${{ runner.temp }}
extra: '--external 7z --base ${{ env.QT_MIRROR }}'
py7zrversion: '==0.22.*'
extra: '--base ${{ env.QT_MIRROR }}'
- name: 'Install android_arm64_v8a Qt'
uses: jurplel/install-qt-action@v4
@@ -360,7 +383,8 @@ jobs:
arch: 'android_arm64_v8a'
modules: ${{ env.QT_MODULES }}
dir: ${{ runner.temp }}
extra: '--external 7z --base ${{ env.QT_MIRROR }}'
py7zrversion: '==0.22.*'
extra: '--base ${{ env.QT_MIRROR }}'
- name: 'Grant execute permission for qt-cmake'
shell: bash
+3
View File
@@ -16,7 +16,10 @@ jobs:
QT_VERSION: 6.4.1
QIF_VERSION: 4.5
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
steps:
- name: 'Install desktop Qt'
+2 -2
View File
@@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
set(PROJECT AmneziaVPN)
project(${PROJECT} VERSION 4.8.1.9
project(${PROJECT} VERSION 4.8.3.0
DESCRIPTION "AmneziaVPN"
HOMEPAGE_URL "https://amnezia.org/"
)
@@ -11,7 +11,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
set(RELEASE_DATE "${CURRENT_DATE}")
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
set(APP_ANDROID_VERSION_CODE 65)
set(APP_ANDROID_VERSION_CODE 2072)
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(MZ_PLATFORM_NAME "linux")
+17 -15
View File
@@ -1,30 +1,31 @@
# Amnezia VPN
## _The best client for self-hosted VPN_
### _The best client for self-hosted VPN_
[![Build Status](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml/badge.svg?branch=dev)](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev)
[![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/amnezia-vpn/amnezia-client)
Amnezia is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server.
### [English]([https://github.com/amnezia-vpn/amnezia-client/blob/dev/README_RU.md](https://github.com/amnezia-vpn/amnezia-client/tree/dev?tab=readme-ov-file#)) | [Русский](https://github.com/amnezia-vpn/amnezia-client/blob/dev/README_RU.md)
![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png)
<br>
[Amnezia](https://amnezia.org) is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server.
<a href="https://github.com/amnezia-vpn/amnezia-client/releases/download/4.7.0.0/AmneziaVPN_4.7.0.0_x64.exe"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/win.png" width="150" style="max-width: 100%;"></a>
<a href="https://github.com/amnezia-vpn/amnezia-client/releases/download/4.7.0.0/AmneziaVPN_4.7.0.0.dmg"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/mac.png" width="150" style="max-width: 100%;"></a>
<a href="https://github.com/amnezia-vpn/amnezia-client/releases/download/4.7.0.0/AmneziaVPN_Linux_4.7.0.0.tar.zip"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/lin.png" width="150" style="max-width: 100%;"></a>
<a href="https://github.com/amnezia-vpn/amnezia-client/releases/tag/4.7.0.0"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/andr.png" width="150" style="max-width: 100%;"></a>
[![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png)](https://amnezia.org)
<br>
### [Website](https://amnezia.org) | [Alt website link](https://storage.googleapis.com/kldscp/amnezia.org) | [Documentation](https://docs.amnezia.org) | [Troubleshooting](https://docs.amnezia.org/troubleshooting)
<a href="https://play.google.com/store/search?q=amnezia+vpn&c=apps"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/play.png" width="150" style="max-width: 100%;"></a>
<a href="https://apps.apple.com/us/app/amneziavpn/id1600529900"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/apl.png" width="150" style="max-width: 100%;"></a>
> [!TIP]
> If the [Amnezia website](https://amnezia.org) is blocked in your region, you can use an [Alternative website link](https://storage.googleapis.com/kldscp/amnezia.org).
<a href="https://amnezia.org/downloads"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/download-website.svg" width="150" style="max-width: 100%; margin-right: 10px"></a>
<a href="https://storage.googleapis.com/kldscp/amnezia.org/downloads"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/download-alt.svg" width="150" style="max-width: 100%;"></a>
[All releases](https://github.com/amnezia-vpn/amnezia-client/releases)
<br>
<br/>
<a href="https://www.testiny.io"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/testiny.png" height="28px"></a>
## Features
@@ -37,7 +38,8 @@ Amnezia is an open-source VPN client, with a key feature that enables you to dep
## Links
- [https://amnezia.org](https://amnezia.org) - project website
- [https://amnezia.org](https://amnezia.org) - Project website | [Alternative link (mirror)](https://storage.googleapis.com/kldscp/amnezia.org)
- [https://docs.amnezia.org](https://docs.amnezia.org) - Documentation
- [https://www.reddit.com/r/AmneziaVPN](https://www.reddit.com/r/AmneziaVPN) - Reddit
- [https://t.me/amnezia_vpn_en](https://t.me/amnezia_vpn_en) - Telegram support channel (English)
- [https://t.me/amnezia_vpn_ir](https://t.me/amnezia_vpn_ir) - Telegram support channel (Farsi)
@@ -186,8 +188,8 @@ Patreon: [https://www.patreon.com/amneziavpn](https://www.patreon.com/amneziavpn
Bitcoin: bc1q26eevjcg9j0wuyywd2e3uc9cs2w58lpkpjxq6p <br>
USDT BEP20: 0x6abD576765a826f87D1D95183438f9408C901bE4 <br>
USDT TRC20: TELAitazF1MZGmiNjTcnxDjEiH5oe7LC9d <br>
XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3
XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3 <br>
TON: UQDpU1CyKRmg7L8mNScKk9FRc2SlESuI7N-Hby4nX-CcVmns
## Acknowledgments
This project is tested with BrowserStack.
+181
View File
@@ -0,0 +1,181 @@
# Amnezia VPN
### _Лучший клиент для создания VPN на собственном сервере_
[![Build Status](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml/badge.svg?branch=dev)](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev)
[![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/amnezia-vpn/amnezia-client)
### [English](https://github.com/amnezia-vpn/amnezia-client/blob/dev/README.md) | Русский
[AmneziaVPN](https://amnezia.org) — это open sourse VPN-клиент, ключевая особенность которого заключается в возможности развернуть собственный VPN на вашем сервере.
[![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png)](https://amnezia.org)
### [Сайт](https://amnezia.org) | [Зеркало на сайт](https://storage.googleapis.com/kldscp/amnezia.org) | [Документация](https://docs.amnezia.org) | [Решение проблем](https://docs.amnezia.org/troubleshooting)
> [!TIP]
> Если [сайт Amnezia](https://amnezia.org) заблокирован в вашем регионе, вы можете воспользоваться [ссылкой на зеркало](https://storage.googleapis.com/kldscp/amnezia.org).
<a href="https://storage.googleapis.com/kldscp/amnezia.org/downloads"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/download-website-ru.svg" width="150" style="max-width: 100%; margin-right: 10px"></a>
[Все релизы](https://github.com/amnezia-vpn/amnezia-client/releases)
<br/>
<a href="https://www.testiny.io"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/testiny.png" height="28px"></a>
## Особенности
- Простой в использовании — введите IP-адрес, SSH-логин и пароль, и Amnezia автоматически установит VPN-контейнеры Docker на ваш сервер и подключится к VPN.
- Классические VPN-протоколы: OpenVPN, WireGuard и IKEv2.
- Протоколы с маскировкой трафика (обфускацией): OpenVPN с плагином [Cloak](https://github.com/cbeuw/Cloak), Shadowsocks (OpenVPN over Shadowsocks), [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) and XRay.
- Поддержка Split Tunneling — добавляйте любые сайты или приложения в список, чтобы включить VPN только для них.
- Поддерживает платформы: Windows, MacOS, Linux, Android, iOS.
- Поддержка конфигурации протокола AmneziaWG на [бета-прошивке Keenetic](https://docs.keenetic.com/ua/air/kn-1611/en/6319-latest-development-release.html#UUID-186c4108-5afd-c10b-f38a-cdff6c17fab3_section-idm33192196168192-improved).
## Ссылки
- [https://amnezia.org](https://amnezia.org) - Веб-сайт проекта | [Альтернативная ссылка (зеркало)](https://storage.googleapis.com/kldscp/amnezia.org)
- [https://docs.amnezia.org](https://docs.amnezia.org) - Документация
- [https://www.reddit.com/r/AmneziaVPN](https://www.reddit.com/r/AmneziaVPN) - Reddit
- [https://t.me/amnezia_vpn_en](https://t.me/amnezia_vpn_en) - Канал поддржки в Telegram (Английский)
- [https://t.me/amnezia_vpn_ir](https://t.me/amnezia_vpn_ir) - Канал поддржки в Telegram (Фарси)
- [https://t.me/amnezia_vpn_mm](https://t.me/amnezia_vpn_mm) - Канал поддржки в Telegram (Мьянма)
- [https://t.me/amnezia_vpn](https://t.me/amnezia_vpn) - Канал поддржки в Telegram (Русский)
- [https://vpnpay.io/en/amnezia-premium/](https://vpnpay.io/en/amnezia-premium/) - Amnezia Premium | [Зеркало](https://storage.googleapis.com/kldscp/vpnpay.io/ru/amnezia-premium\)
## Технологии
AmneziaVPN использует несколько проектов с открытым исходным кодом:
- [OpenSSL](https://www.openssl.org/)
- [OpenVPN](https://openvpn.net/)
- [Shadowsocks](https://shadowsocks.org/)
- [Qt](https://www.qt.io/)
- [LibSsh](https://libssh.org)
- и другие...
## Проверка исходного кода
После клонирования репозитория обязательно загрузите все подмодули.
```bash
git submodule update --init --recursive
```
## Разработка
Хотите внести свой вклад? Добро пожаловать!
### Помощь с переводами
Загрузите самые актуальные файлы перевода.
Перейдите на [вкладку "Actions"](https://github.com/amnezia-vpn/amnezia-client/actions?query=is%3Asuccess+branch%3Adev), нажмите на первую строку. Затем прокрутите вниз до раздела "Artifacts" и скачайте "AmneziaVPN_translations".
Распакуйте этот файл. Каждый файл с расширением *.ts содержит строки для соответствующего языка.
Переведите или исправьте строки в одном или нескольких файлах *.ts и загрузите их обратно в этот репозиторий в папку ``client/translations``. Это можно сделать через веб-интерфейс или любым другим знакомым вам способом.
### Сборка исходного кода и деплой
Проверьте папку deploy для скриптов сборки.
### Как собрать iOS-приложение из исходного кода на MacOS
1. Убедитесь, что у вас установлен XCode версии 14 или выше.
2. Для генерации проекта XCode используется QT. Требуется версия QT 6.6.2. Установите QT для MacOS здесь или через QT Online Installer. Необходимые модули:
- MacOS
- iOS
- Модуль совместимости с Qt 5
- Qt Shader Tools
- Дополнительные библиотеки:
- Qt Image Formats
- Qt Multimedia
- Qt Remote Objects
3. Установите CMake, если это необходимо. Рекомендуемая версия — 3.25. Скачать CMake можно здесь.
4. Установите Go версии >= v1.16. Если Go ещё не установлен, скачайте его с [официального сайта](https://golang.org/dl/) или используйте Homebrew. Установите gomobile:
```bash
export PATH=$PATH:~/go/bin
go install golang.org/x/mobile/cmd/gomobile@latest
gomobile init
```
5. Соберите проект:
```bash
export QT_BIN_DIR="<PATH-TO-QT-FOLDER>/Qt/<QT-VERSION>/ios/bin"
export QT_MACOS_ROOT_DIR="<PATH-TO-QT-FOLDER>/Qt/<QT-VERSION>/macos"
export QT_IOS_BIN=$QT_BIN_DIR
export PATH=$PATH:~/go/bin
mkdir build-ios
$QT_IOS_BIN/qt-cmake . -B build-ios -GXcode -DQT_HOST_PATH=$QT_MACOS_ROOT_DIR
```
Замените <PATH-TO-QT-FOLDER> и <QT-VERSION> на ваши значения.
Если появляется ошибка gomobile: command not found, убедитесь, что PATH настроен на папку bin, где установлен gomobile:
```bash
export PATH=$(PATH):/path/to/GOPATH/bin
```
6. Откройте проект в XCode. Теперь вы можете тестировать, архивировать или публиковать приложение.
Если сборка завершится с ошибкой:
```
make: ***
[$(PROJECTDIR)/client/build/AmneziaVPN.build/Debug-iphoneos/wireguard-go-bridge/goroot/.prepared]
Error 1
```
Добавьте пользовательскую переменную PATH в настройки сборки для целей AmneziaVPN и WireGuardNetworkExtension с ключом `PATH` и значением `${PATH}/path/to/bin/folder/with/go/executable`, e.g. `${PATH}:/usr/local/go/bin`.
Если ошибка повторяется на Mac с M1, установите версию CMake для архитектуры ARM:
```
arch -arm64 brew install cmake
```
При первой попытке сборка может завершиться с ошибкой source files not found. Это происходит из-за параллельной компиляции зависимостей в XCode. Просто перезапустите сборку.
## Как собрать Android-приложение
Сборка тестировалась на MacOS. Требования:
- JDK 11
- Android SDK 33
- CMake 3.25.0
Установите QT, QT Creator и Android Studio.
Настройте QT Creator:
- В меню QT Creator перейдите в `QT Creator` -> `Preferences` -> `Devices` ->`Android`.
- Укажите путь к JDK 11.
- Укажите путь к Android SDK (`$ANDROID_HOME`)
Если вы сталкиваетесь с ошибками, связанными с отсутствием SDK или сообщением «SDK manager not running», их нельзя исправить просто корректировкой путей. Если у вас есть несколько свободных гигабайт на диске, вы можете позволить Qt Creator установить все необходимые компоненты, выбрав пустую папку для расположения Android SDK и нажав кнопку **Set Up SDK**. Учтите: это установит второй Android SDK и NDK на вашем компьютере!
Убедитесь, что настроена правильная версия CMake: перейдите в **Qt Creator -> Preferences** и в боковом меню выберите пункт **Kits**. В центральной части окна, на вкладке **Kits**, найдите запись для инструмента **CMake Tool**. Если выбранная по умолчанию версия CMake ниже 3.25.0, установите на свою систему CMake версии 3.25.0 или выше, а затем выберите опцию **System CMake at <путь>** из выпадающего списка. Если этот пункт отсутствует, это может означать, что вы еще не установили CMake, или Qt Creator не смог найти путь к нему. В таком случае в окне **Preferences** перейдите в боковое меню **CMake**, затем во вкладку **Tools** в центральной части окна и нажмите кнопку **Add**, чтобы указать путь к установленному CMake.
Убедитесь, что для вашего проекта выбрана Android Platform SDK 33: в главном окне на боковой панели выберите пункт **Projects**, и слева вы увидите раздел **Build & Run**, показывающий различные целевые Android-платформы. Вы можете выбрать любую из них, так как настройка проекта Amnezia VPN разработана таким образом, чтобы все Android-цели могли быть собраны. Перейдите в подраздел **Build** и прокрутите центральную часть окна до раздела **Build Steps**. Нажмите **Details** в заголовке **Build Android APK** (кнопка **Details** может быть скрыта, если окно Qt Creator не запущено в полноэкранном режиме!). Вот здесь выберите **android-33** в качестве Android Build Platform SDK.
### Разработка Android-компонентов
После сборки QT Creator копирует проект в отдельную папку, например, `build-amnezia-client-Android_Qt_<version>_Clang_<architecture>-<BuildType>`. Для разработки Android-компонентов откройте сгенерированный проект в Android Studio, указав папку `build-amnezia-client-Android_Qt_<version>_Clang_<architecture>-<BuildType>/client/android-build` в качестве корневой.
Изменения в сгенерированном проекте нужно вручную перенести в репозиторий. После этого можно коммитить изменения.
Если возникают проблемы со сборкой в QT Creator после работы в Android Studio, выполните команду `./gradlew clean` в корневой папке сгенерированного проекта (`<path>/client/android-build/.`).
## Лицензия
GPL v3.0
## Донаты
Patreon: [https://www.patreon.com/amneziavpn](https://www.patreon.com/amneziavpn)
Bitcoin: bc1q26eevjcg9j0wuyywd2e3uc9cs2w58lpkpjxq6p <br>
USDT BEP20: 0x6abD576765a826f87D1D95183438f9408C901bE4 <br>
USDT TRC20: TELAitazF1MZGmiNjTcnxDjEiH5oe7LC9d <br>
XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3 <br>
TON: UQDpU1CyKRmg7L8mNScKk9FRc2SlESuI7N-Hby4nX-CcVmns
## Благодарности
Этот проект тестируется с помощью BrowserStack.
Мы выражаем благодарность [BrowserStack](https://www.browserstack.com) за поддержку нашего проекта.
+4 -1
View File
@@ -25,10 +25,11 @@ execute_process(
add_definitions(-DGIT_COMMIT_HASH="${GIT_COMMIT_HASH}")
add_definitions(-DPROD_AGW_PUBLIC_KEY="$ENV{PROD_AGW_PUBLIC_KEY}")
add_definitions(-DPROD_PROXY_STORAGE_KEY="$ENV{PROD_PROXY_STORAGE_KEY}")
add_definitions(-DPROD_S3_ENDPOINT="$ENV{PROD_S3_ENDPOINT}")
add_definitions(-DDEV_AGW_PUBLIC_KEY="$ENV{DEV_AGW_PUBLIC_KEY}")
add_definitions(-DDEV_AGW_ENDPOINT="$ENV{DEV_AGW_ENDPOINT}")
add_definitions(-DDEV_S3_ENDPOINT="$ENV{DEV_S3_ENDPOINT}")
if(IOS)
set(PACKAGES ${PACKAGES} Multimedia)
@@ -145,6 +146,7 @@ set(HEADERS ${HEADERS}
${CMAKE_CURRENT_LIST_DIR}/core/serialization/transfer.h
${CMAKE_CURRENT_LIST_DIR}/core/enums/apiEnums.h
${CMAKE_CURRENT_LIST_DIR}/../common/logger/logger.h
${CMAKE_CURRENT_LIST_DIR}/utils/qmlUtils.h
)
# Mozilla headres
@@ -196,6 +198,7 @@ set(SOURCES ${SOURCES}
${CMAKE_CURRENT_LIST_DIR}/core/serialization/vmess.cpp
${CMAKE_CURRENT_LIST_DIR}/core/serialization/vmess_new.cpp
${CMAKE_CURRENT_LIST_DIR}/../common/logger/logger.cpp
${CMAKE_CURRENT_LIST_DIR}/utils/qmlUtils.cpp
)
# Mozilla sources
+13 -9
View File
@@ -111,10 +111,11 @@ void AmneziaApplication::init()
qFatal("Android controller initialization failed");
}
connect(AndroidController::instance(), &AndroidController::importConfigFromOutside, [this](QString data) {
m_pageController->goToPageHome();
connect(AndroidController::instance(), &AndroidController::importConfigFromOutside, this, [this](QString data) {
emit m_pageController->goToPageHome();
m_importController->extractConfigFromData(data);
m_pageController->goToPageViewConfig();
data.clear();
emit m_pageController->goToPageViewConfig();
});
m_engine->addImageProvider(QLatin1String("installedAppImage"), new InstalledAppsImageProvider);
@@ -122,16 +123,16 @@ void AmneziaApplication::init()
#ifdef Q_OS_IOS
IosController::Instance()->initialize();
connect(IosController::Instance(), &IosController::importConfigFromOutside, [this](QString data) {
m_pageController->goToPageHome();
connect(IosController::Instance(), &IosController::importConfigFromOutside, this, [this](QString data) {
emit m_pageController->goToPageHome();
m_importController->extractConfigFromData(data);
m_pageController->goToPageViewConfig();
emit m_pageController->goToPageViewConfig();
});
connect(IosController::Instance(), &IosController::importBackupFromOutside, [this](QString filePath) {
m_pageController->goToPageHome();
connect(IosController::Instance(), &IosController::importBackupFromOutside, this, [this](QString filePath) {
emit m_pageController->goToPageHome();
m_pageController->goToPageSettingsBackup();
m_settingsController->importBackupFromOutside(filePath);
emit m_settingsController->importBackupFromOutside(filePath);
});
QTimer::singleShot(0, this, [this]() { AmneziaVPN::toggleScreenshots(m_settings->isScreenshotsEnabled()); });
@@ -403,6 +404,9 @@ void AmneziaApplication::initControllers()
m_pageController.reset(new PageController(m_serversModel, m_settings));
m_engine->rootContext()->setContextProperty("PageController", m_pageController.get());
m_focusController.reset(new FocusController(m_engine, this));
m_engine->rootContext()->setContextProperty("FocusController", m_focusController.get());
m_installController.reset(new InstallController(m_serversModel, m_containersModel, m_protocolsModel, m_clientManagementModel,
m_apiServicesModel, m_settings));
m_engine->rootContext()->setContextProperty("InstallController", m_installController.get());
+2
View File
@@ -19,6 +19,7 @@
#include "ui/controllers/exportController.h"
#include "ui/controllers/importController.h"
#include "ui/controllers/installController.h"
#include "ui/controllers/focusController.h"
#include "ui/controllers/pageController.h"
#include "ui/controllers/settingsController.h"
#include "ui/controllers/sitesController.h"
@@ -124,6 +125,7 @@ private:
#endif
QScopedPointer<ConnectionController> m_connectionController;
QScopedPointer<FocusController> m_focusController;
QScopedPointer<PageController> m_pageController;
QScopedPointer<InstallController> m_installController;
QScopedPointer<ImportController> m_importController;
+8 -1
View File
@@ -20,7 +20,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<!-- To request network state -->
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
@@ -91,6 +91,13 @@
android:exported="false"
android:theme="@style/Translucent" />
<activity android:name=".TvFilePicker"
android:excludeFromRecents="true"
android:launchMode="singleTask"
android:taskAffinity=""
android:exported="false"
android:theme="@style/Translucent" />
<activity
android:name=".ImportConfigActivity"
android:excludeFromRecents="true"
+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
#androidCompileSdkVersion=android-34
#androidBuildToolsVersion=34.0.0
#qtMinSdkVersion=24
#qtMinSdkVersion=26
#qtTargetSdkVersion=34
#androidNdkVersion=26.1.10909125
#qtTargetAbiList=x86_64
@@ -1,6 +1,5 @@
package org.amnezia.vpn.protocol
import android.annotation.SuppressLint
import android.content.Context
import android.net.IpPrefix
import android.net.VpnService
@@ -8,9 +7,6 @@ import android.net.VpnService.Builder
import android.os.Build
import android.system.OsConstants
import androidx.annotation.RequiresApi
import java.io.File
import java.io.FileOutputStream
import java.util.zip.ZipFile
import kotlinx.coroutines.flow.MutableStateFlow
import org.amnezia.vpn.util.Log
import org.amnezia.vpn.util.net.InetNetwork
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_banner_background"/>
<foreground android:drawable="@mipmap/ic_banner_foreground"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

+2
View File
@@ -23,4 +23,6 @@
<string name="notificationSettingsDialogTitle">Настройки уведомлений</string>
<string name="notificationSettingsDialogMessage">Для показа уведомлений необходимо включить уведомления в системных настройках</string>
<string name="openNotificationSettings">Открыть настройки уведомлений</string>
<string name="tvNoFileBrowser">Пожалуйста, установите приложение для просмотра файлов</string>
</resources>
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_banner_background">#1E1E1F</color>
</resources>
+2
View File
@@ -23,4 +23,6 @@
<string name="notificationSettingsDialogTitle">Notification settings</string>
<string name="notificationSettingsDialogMessage">To show notifications, you must enable notifications in the system settings</string>
<string name="openNotificationSettings">Open notification settings</string>
<string name="tvNoFileBrowser">Please install a file management utility to browse files</string>
</resources>
@@ -4,6 +4,7 @@ import android.Manifest
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.app.NotificationManager
import android.content.ActivityNotFoundException
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Intent
@@ -12,6 +13,7 @@ import android.content.Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY
import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.net.Uri
import android.net.VpnService
import android.os.Build
import android.os.Bundle
@@ -20,7 +22,13 @@ import android.os.IBinder
import android.os.Looper
import android.os.Message
import android.os.Messenger
import android.os.ParcelFileDescriptor
import android.os.SystemClock
import android.provider.OpenableColumns
import android.provider.Settings
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager.LayoutParams
import android.webkit.MimeTypeMap
import android.widget.Toast
@@ -29,6 +37,7 @@ import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import java.io.IOException
import kotlin.LazyThreadSafetyMode.NONE
import kotlin.coroutines.CoroutineContext
import kotlin.text.RegexOption.IGNORE_CASE
import AppListProvider
import kotlinx.coroutines.CompletableDeferred
@@ -70,6 +79,7 @@ class AmneziaActivity : QtActivity() {
private var isInBoundState = false
private var notificationStateReceiver: BroadcastReceiver? = null
private lateinit var vpnServiceMessenger: IpcMessenger
private var pfd: ParcelFileDescriptor? = null
private val actionResultHandlers = mutableMapOf<Int, ActivityResultHandler>()
private val permissionRequestHandlers = mutableMapOf<Int, PermissionRequestHandler>()
@@ -158,7 +168,7 @@ class AmneziaActivity : QtActivity() {
*/
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(TAG, "Create Amnezia activity: $intent")
Log.d(TAG, "Create Amnezia activity")
loadLibs()
window.apply {
addFlags(LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
@@ -200,7 +210,7 @@ class AmneziaActivity : QtActivity() {
NotificationManager.ACTION_APP_BLOCK_STATE_CHANGED
)
) {
Log.d(
Log.v(
TAG, "Notification state changed: ${it?.action}, blocked = " +
"${it?.getBooleanExtra(NotificationManager.EXTRA_BLOCKED_STATE, false)}"
)
@@ -214,7 +224,7 @@ class AmneziaActivity : QtActivity() {
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
Log.d(TAG, "onNewIntent: $intent")
Log.v(TAG, "onNewIntent: $intent")
intent?.let(::processIntent)
}
@@ -403,7 +413,7 @@ class AmneziaActivity : QtActivity() {
@MainThread
private fun startVpn(vpnConfig: String) {
getVpnProto(vpnConfig)?.let { proto ->
Log.d(TAG, "Proto from config: $proto, current proto: $vpnProto")
Log.v(TAG, "Proto from config: $proto, current proto: $vpnProto")
if (isServiceConnected) {
if (proto.serviceClass == vpnProto?.serviceClass) {
vpnProto = proto
@@ -513,21 +523,25 @@ class AmneziaActivity : QtActivity() {
type = "text/*"
putExtra(Intent.EXTRA_TITLE, fileName)
}.also {
startActivityForResult(it, CREATE_FILE_ACTION_CODE, ActivityResultHandler(
onSuccess = {
it?.data?.let { uri ->
Log.d(TAG, "Save file to $uri")
try {
contentResolver.openOutputStream(uri)?.use { os ->
os.bufferedWriter().use { it.write(data) }
try {
startActivityForResult(it, CREATE_FILE_ACTION_CODE, ActivityResultHandler(
onSuccess = {
it?.data?.let { uri ->
Log.v(TAG, "Save file to $uri")
try {
contentResolver.openOutputStream(uri)?.use { os ->
os.bufferedWriter().use { it.write(data) }
}
} catch (e: IOException) {
Log.e(TAG, "Failed to save file $uri: $e")
// todo: send error to Qt
}
} catch (e: IOException) {
Log.e(TAG, "Failed to save file $uri: $e")
// todo: send error to Qt
}
}
}
))
))
} catch (_: ActivityNotFoundException) {
Toast.makeText(this@AmneziaActivity, "Unsupported", Toast.LENGTH_LONG).show()
}
}
}
}
@@ -536,46 +550,115 @@ class AmneziaActivity : QtActivity() {
fun openFile(filter: String?) {
Log.v(TAG, "Open file with filter: $filter")
mainScope.launch {
val mimeTypes = if (!filter.isNullOrEmpty()) {
val extensionRegex = "\\*\\.([a-z0-9]+)".toRegex(IGNORE_CASE)
val mime = MimeTypeMap.getSingleton()
extensionRegex.findAll(filter).map {
it.groups[1]?.value?.let { mime.getMimeTypeFromExtension(it) } ?: "*/*"
}.toSet()
} else emptySet()
val intent = if (!isOnTv()) {
val mimeTypes = if (!filter.isNullOrEmpty()) {
val extensionRegex = "\\*\\.([a-z0-9]+)".toRegex(IGNORE_CASE)
val mime = MimeTypeMap.getSingleton()
extensionRegex.findAll(filter).map {
it.groups[1]?.value?.let { mime.getMimeTypeFromExtension(it) } ?: "*/*"
}.toSet()
} else emptySet()
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
Log.v(TAG, "File mimyType filter: $mimeTypes")
if ("*/*" in mimeTypes) {
type = "*/*"
} else {
when (mimeTypes.size) {
1 -> type = mimeTypes.first()
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
Log.v(TAG, "File mimyType filter: $mimeTypes")
if ("*/*" in mimeTypes) {
type = "*/*"
} else {
when (mimeTypes.size) {
1 -> type = mimeTypes.first()
in 2..Int.MAX_VALUE -> {
type = "*/*"
putExtra(EXTRA_MIME_TYPES, mimeTypes.toTypedArray())
in 2..Int.MAX_VALUE -> {
type = "*/*"
putExtra(EXTRA_MIME_TYPES, mimeTypes.toTypedArray())
}
else -> type = "*/*"
}
else -> type = "*/*"
}
}
}.also {
startActivityForResult(it, OPEN_FILE_ACTION_CODE, ActivityResultHandler(
} else {
Intent(this@AmneziaActivity, TvFilePicker::class.java)
}
try {
startActivityForResult(intent, OPEN_FILE_ACTION_CODE, ActivityResultHandler(
onAny = {
val uri = it?.data?.toString() ?: ""
Log.d(TAG, "Open file: $uri")
if (isOnTv() && it?.hasExtra("activityNotFound") == true) {
showNoFileBrowserAlertDialog()
}
val uri = it?.data?.apply {
grantUriPermission(packageName, this, Intent.FLAG_GRANT_READ_URI_PERMISSION)
}?.toString() ?: ""
Log.v(TAG, "Open file: $uri")
mainScope.launch {
qtInitialized.await()
QtAndroidController.onFileOpened(uri)
}
}
))
} catch (_: ActivityNotFoundException) {
showNoFileBrowserAlertDialog()
mainScope.launch {
qtInitialized.await()
QtAndroidController.onFileOpened("")
}
}
}
}
private fun showNoFileBrowserAlertDialog() {
AlertDialog.Builder(this)
.setMessage(R.string.tvNoFileBrowser)
.setCancelable(false)
.setPositiveButton(android.R.string.ok) { _, _ ->
try {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://webstoreredirect")))
} catch (_: Throwable) {}
}
.show()
}
@Suppress("unused")
fun getFd(fileName: String): Int {
Log.v(TAG, "Get fd for $fileName")
return blockingCall {
try {
pfd = contentResolver.openFileDescriptor(Uri.parse(fileName), "r")
pfd?.fd ?: -1
} catch (e: Exception) {
Log.e(TAG, "Failed to get fd: $e")
-1
}
}
}
@Suppress("unused")
fun closeFd() {
Log.v(TAG, "Close fd")
mainScope.launch {
pfd?.close()
pfd = null
}
}
@Suppress("unused")
fun getFileName(uri: String): String {
Log.v(TAG, "Get file name for uri: $uri")
return blockingCall {
try {
contentResolver.query(Uri.parse(uri), arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)?.use { cursor ->
if (cursor.moveToFirst() && !cursor.isNull(0)) {
return@blockingCall cursor.getString(0) ?: ""
}
}
} catch (e: Exception) {
Log.e(TAG, "Failed to get file name: $e")
}
""
}
}
@Suppress("unused")
@SuppressLint("UnsupportedChromeOsCameraSystemFeature")
fun isCameraPresent(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA)
@@ -720,9 +803,121 @@ class AmneziaActivity : QtActivity() {
}
}
// method to workaround Qt's problem with calling the keyboard on TVs
@Suppress("unused")
fun sendTouch(x: Float, y: Float) {
Log.v(TAG, "Send touch: $x, $y")
blockingCall {
findQtWindow(window.decorView)?.let {
Log.v(TAG, "Send touch to $it")
it.dispatchTouchEvent(createEvent(x, y, SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN))
it.dispatchTouchEvent(createEvent(x, y, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP))
}
}
}
private fun findQtWindow(view: View): View? {
Log.v(TAG, "findQtWindow: process $view")
if (view::class.simpleName == "QtWindow") return view
else if (view is ViewGroup) {
for (i in 0 until view.childCount) {
val result = findQtWindow(view.getChildAt(i))
if (result != null) return result
}
return null
} else return null
}
private fun createEvent(x: Float, y: Float, eventTime: Long, action: Int): MotionEvent =
MotionEvent.obtain(
eventTime,
eventTime,
action,
1,
arrayOf(MotionEvent.PointerProperties().apply {
id = 0
toolType = MotionEvent.TOOL_TYPE_FINGER
}),
arrayOf(MotionEvent.PointerCoords().apply {
this.x = x
this.y = y
pressure = 1f
size = 1f
}),
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
*/
private fun <T> blockingCall(
context: CoroutineContext = Dispatchers.Main.immediate,
block: suspend () -> T
) = runBlocking {
mainScope.async(context) { block() }.await()
}
companion object {
private fun actionCodeToString(actionCode: Int): String =
when (actionCode) {
@@ -300,7 +300,7 @@ open class AmneziaVpnService : VpnService() {
arrayOf(ACTION_CONNECT, ACTION_DISCONNECT), ContextCompat.RECEIVER_NOT_EXPORTED
) {
it?.action?.let { action ->
Log.d(TAG, "Broadcast request received: $action")
Log.v(TAG, "Broadcast request received: $action")
when (action) {
ACTION_CONNECT -> connect()
ACTION_DISCONNECT -> disconnect()
@@ -317,7 +317,7 @@ open class AmneziaVpnService : VpnService() {
)
) {
val state = it?.getBooleanExtra(NotificationManager.EXTRA_BLOCKED_STATE, false)
Log.d(TAG, "Notification state changed: ${it?.action}, blocked = $state")
Log.v(TAG, "Notification state changed: ${it?.action}, blocked = $state")
if (state == false) {
enableNotification()
} else {
@@ -450,7 +450,7 @@ open class AmneziaVpnService : VpnService() {
serviceNotification.isNotificationEnabled() &&
getSystemService<PowerManager>()?.isInteractive != false
) {
Log.d(TAG, "Launch traffic stats update")
Log.v(TAG, "Launch traffic stats update")
trafficStats.reset()
startTrafficStatsUpdateJob()
}
@@ -66,7 +66,7 @@ class AuthActivity : FragmentActivity() {
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: AuthenticationResult) {
super.onAuthenticationSucceeded(result)
Log.d(TAG, "Authentication succeeded")
Log.v(TAG, "Authentication succeeded")
QtAndroidController.onAuthResult(true)
finish()
}
@@ -29,20 +29,20 @@ class ImportConfigActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(TAG, "Create Import Config Activity: $intent")
Log.v(TAG, "Create Import Config Activity: $intent")
intent?.let(::readConfig)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
Log.d(TAG, "onNewIntent: $intent")
Log.v(TAG, "onNewIntent: $intent")
intent.let(::readConfig)
}
private fun readConfig(intent: Intent) {
when (intent.action) {
ACTION_SEND -> {
Log.d(TAG, "Process SEND action, type: ${intent.type}")
Log.v(TAG, "Process SEND action, type: ${intent.type}")
when (intent.type) {
"application/octet-stream" -> {
intent.getUriCompat()?.let { uri ->
@@ -60,7 +60,7 @@ class ImportConfigActivity : ComponentActivity() {
}
ACTION_VIEW -> {
Log.d(TAG, "Process VIEW action, scheme: ${intent.scheme}")
Log.v(TAG, "Process VIEW action, scheme: ${intent.scheme}")
when (intent.scheme) {
"file", "content" -> {
intent.data?.let { uri ->
@@ -62,7 +62,7 @@ class ServiceNotification(private val context: Context) {
fun buildNotification(serverName: String?, protocol: String?, state: ProtocolState): Notification {
val speedString = if (state == CONNECTED) zeroSpeed else null
Log.d(TAG, "Build notification: $serverName, $state")
Log.v(TAG, "Build notification: $serverName, $state")
return notificationBuilder
.setSmallIcon(R.drawable.ic_amnezia_round)
@@ -88,17 +88,15 @@ class ServiceNotification(private val context: Context) {
fun isNotificationEnabled(): Boolean {
if (!context.isNotificationPermissionGranted()) return false
if (!notificationManager.areNotificationsEnabled()) return false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID)
?.let { it.importance != NotificationManager.IMPORTANCE_NONE } ?: true
}
return true
return notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID)?.let {
it.importance != NotificationManager.IMPORTANCE_NONE
} ?: true
}
@SuppressLint("MissingPermission")
fun updateNotification(serverName: String?, protocol: String?, state: ProtocolState) {
if (context.isNotificationPermissionGranted()) {
Log.d(TAG, "Update notification: $serverName, $state")
Log.v(TAG, "Update notification: $serverName, $state")
notificationManager.notify(NOTIFICATION_ID, buildNotification(serverName, protocol, state))
}
}
@@ -0,0 +1,45 @@
package org.amnezia.vpn
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import org.amnezia.vpn.util.Log
private const val TAG = "TvFilePicker"
class TvFilePicker : ComponentActivity() {
private val fileChooseResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) {
setResult(RESULT_OK, Intent().apply { data = it })
finish()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.v(TAG, "onCreate")
getFile()
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
Log.v(TAG, "onNewIntent")
getFile()
}
private fun getFile() {
try {
Log.v(TAG, "getFile")
fileChooseResultLauncher.launch("*/*")
} catch (_: ActivityNotFoundException) {
Log.w(TAG, "Activity not found")
setResult(RESULT_CANCELED, Intent().apply { putExtra("activityNotFound", true) })
finish()
} catch (e: Exception) {
Log.e(TAG, "Failed to get file: $e")
setResult(RESULT_CANCELED)
finish()
}
}
}
@@ -46,7 +46,7 @@ object LibraryLoader {
System.loadLibrary(libraryName)
return
} catch (_: UnsatisfiedLinkError) {
Log.d(TAG, "Failed to load library, try to extract it from apk")
Log.w(TAG, "Failed to load library, try to extract it from apk")
}
var tempFile: File? = null
try {
+2 -15
View File
@@ -1,8 +1,6 @@
package org.amnezia.vpn.util
import android.content.Context
import android.icu.text.DateFormat
import android.icu.text.SimpleDateFormat
import android.os.Build
import android.os.Process
import java.io.File
@@ -12,8 +10,6 @@ import java.nio.channels.FileChannel
import java.nio.channels.FileLock
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.Date
import java.util.Locale
import java.util.concurrent.locks.ReentrantLock
import org.amnezia.vpn.util.Log.Priority.D
import org.amnezia.vpn.util.Log.Priority.E
@@ -41,11 +37,7 @@ private const val LOG_MAX_FILE_SIZE = 1024 * 1024
* | | | create a report and/or terminate the process |
*/
object Log {
private val dateTimeFormat: Any =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) DateTimeFormatter.ofPattern(DATE_TIME_PATTERN)
else object : ThreadLocal<DateFormat>() {
override fun initialValue(): DateFormat = SimpleDateFormat(DATE_TIME_PATTERN, Locale.US)
}
private val dateTimeFormat: DateTimeFormatter = DateTimeFormatter.ofPattern(DATE_TIME_PATTERN)
private lateinit var logDir: File
private val logFile: File by lazy { File(logDir, LOG_FILE_NAME) }
@@ -143,12 +135,7 @@ object Log {
}
private fun formatLogMsg(tag: String, msg: String, priority: Priority): String {
val date = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
LocalDateTime.now().format(dateTimeFormat as DateTimeFormatter)
} else {
@Suppress("UNCHECKED_CAST")
(dateTimeFormat as ThreadLocal<DateFormat>).get()?.format(Date())
}
val date = LocalDateTime.now().format(dateTimeFormat)
return "$date ${Process.myPid()} ${Process.myTid()} $priority [${Thread.currentThread().name}] " +
"$tag: $msg\n"
}
@@ -42,18 +42,12 @@ class NetworkState(
private val networkCallback: NetworkCallback by lazy(NONE) {
object : NetworkCallback() {
override fun onAvailable(network: Network) {
Log.d(TAG, "onAvailable: $network")
Log.v(TAG, "onAvailable: $network")
}
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
Log.d(TAG, "onCapabilitiesChanged: $network, $networkCapabilities")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
checkNetworkState(network, networkCapabilities)
} else {
handler.post {
checkNetworkState(network, networkCapabilities)
}
}
Log.v(TAG, "onCapabilitiesChanged: $network, $networkCapabilities")
checkNetworkState(network, networkCapabilities)
}
private fun checkNetworkState(network: Network, networkCapabilities: NetworkCapabilities) {
@@ -73,11 +67,11 @@ class NetworkState(
}
override fun onBlockedStatusChanged(network: Network, blocked: Boolean) {
Log.d(TAG, "onBlockedStatusChanged: $network, $blocked")
Log.v(TAG, "onBlockedStatusChanged: $network, $blocked")
}
override fun onLost(network: Network) {
Log.d(TAG, "onLost: $network")
Log.v(TAG, "onLost: $network")
}
}
}
@@ -87,7 +81,7 @@ class NetworkState(
Log.d(TAG, "Bind network listener")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
connectivityManager.registerBestMatchingNetworkCallback(networkRequest, networkCallback, handler)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
} else {
val numberAttempts = 300
var attemptCount = 0
while(true) {
@@ -108,8 +102,6 @@ class NetworkState(
}
}
}
} else {
connectivityManager.requestNetwork(networkRequest, networkCallback)
}
isListenerBound = true
}
@@ -1,11 +1,12 @@
package org.amnezia.vpn.protocol.wireguard
import android.net.VpnService.Builder
import java.io.IOException
import java.util.Locale
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import kotlinx.coroutines.launch
import org.amnezia.awg.GoBackend
import org.amnezia.vpn.protocol.Protocol
import org.amnezia.vpn.protocol.ProtocolState.CONNECTED
@@ -27,6 +28,8 @@ open class Wireguard : Protocol() {
private var tunnelHandle: Int = -1
protected open val ifName: String = "amn0"
private lateinit var scope: CoroutineScope
private var statusJob: Job? = null
override val statistics: Statistics
get() {
@@ -49,46 +52,17 @@ open class Wireguard : Protocol() {
override fun internalInit() {
if (!isInitialized) loadSharedLibrary(context, "wg-go")
if (this::scope.isInitialized) {
scope.cancel()
}
scope = CoroutineScope(Dispatchers.IO)
}
override suspend fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) {
val wireguardConfig = parseConfig(config)
val startTime = System.currentTimeMillis()
start(wireguardConfig, vpnBuilder, protect)
waitForConnection(startTime)
state.value = CONNECTED
}
private suspend fun waitForConnection(startTime: Long) {
Log.d(TAG, "Waiting for connection")
withContext(Dispatchers.IO) {
val time = String.format(Locale.ROOT,"%.3f", startTime / 1000.0)
try {
delay(1000)
var log = getLogcat(time)
Log.d(TAG, "First waiting log: $log")
// check that there is a connection log,
// to avoid infinite connection
if (!log.contains("Attaching to interface")) {
Log.w(TAG, "Logs do not contain a connection log")
return@withContext
}
while (!log.contains("Received handshake response")) {
delay(1000)
log = getLogcat(time)
}
} catch (e: IOException) {
Log.e(TAG, "Failed to get logcat: $e")
}
}
}
private fun getLogcat(time: String): String =
ProcessBuilder("logcat", "--buffer=main", "--format=raw", "*:S AmneziaWG/awg0", "-t", time)
.redirectErrorStream(true)
.start()
.inputStream.reader().readText()
protected open fun parseConfig(config: JSONObject): WireguardConfig {
val configData = config.getJSONObject("wireguard_config_data")
return WireguardConfig.build {
@@ -178,6 +152,43 @@ open class Wireguard : Protocol() {
tunnelHandle = -1
throw VpnStartException("Protect VPN interface: permission not granted or revoked")
}
launchStatusJob()
}
private fun launchStatusJob() {
Log.d(TAG, "Launch status job")
statusJob = scope.launch {
while (true) {
val lastHandshake = getLastHandshake()
Log.v(TAG, "lastHandshake=$lastHandshake")
if (lastHandshake == 0L) {
delay(1000)
continue
}
if (lastHandshake == -2L || lastHandshake > 0L) state.value = CONNECTED
else if (lastHandshake == -1L) state.value = DISCONNECTED
statusJob = null
break
}
}
}
private fun getLastHandshake(): Long {
if (tunnelHandle == -1) {
Log.e(TAG, "Trying to get config of a non-existent tunnel")
return -1
}
val config = GoBackend.awgGetConfig(tunnelHandle)
if (config == null) {
Log.e(TAG, "Failed to get tunnel config")
return -2
}
val lastHandshake = config.lines().find { it.startsWith("last_handshake_time_sec=") }?.substring(24)?.toLong()
if (lastHandshake == null) {
Log.e(TAG, "Failed to get last_handshake_time_sec")
return -2
}
return lastHandshake
}
override fun stopVpn() {
@@ -185,6 +196,8 @@ open class Wireguard : Protocol() {
Log.w(TAG, "Tunnel already down")
return
}
statusJob?.cancel()
statusJob = null
val handleToClose = tunnelHandle
tunnelHandle = -1
GoBackend.awgTurnOff(handleToClose)
+2 -2
View File
@@ -130,8 +130,8 @@ class Xray : Protocol() {
LibXray.initXray(assetsPath)
val geoDir = File(assetsPath, "geo").absolutePath
val configPath = File(context.cacheDir, "config.json")
Log.d(TAG, "xray.location.asset: $geoDir")
Log.d(TAG, "config: $configPath")
Log.v(TAG, "xray.location.asset: $geoDir")
Log.v(TAG, "config: $configPath")
try {
configPath.writeText(configJson)
} catch (e: IOException) {
+1 -1
View File
@@ -1,6 +1,6 @@
message("Client android ${CMAKE_ANDROID_ARCH_ABI} build")
set(APP_ANDROID_MIN_SDK 24)
set(APP_ANDROID_MIN_SDK 26)
set(ANDROID_PLATFORM "android-${APP_ANDROID_MIN_SDK}" CACHE STRING
"The minimum API level supported by the application or library" FORCE)
@@ -120,7 +120,7 @@ WireguardConfigurator::ConnectionData WireguardConfigurator::prepareWireguardCon
}
}
QString subnetIp = containerConfig.value(config_key::subnet_address).toString(protocols::wireguard::defaultSubnetAddress);
QString subnetIp = containerConfig.value(m_protocolName).toObject().value(config_key::subnet_address).toString(protocols::wireguard::defaultSubnetAddress);
{
QStringList l = subnetIp.split(".", Qt::SkipEmptyParts);
if (l.isEmpty()) {
+148 -17
View File
@@ -3,38 +3,169 @@
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QUuid>
#include "logger.h"
#include "containers/containers_defs.h"
#include "core/controllers/serverController.h"
#include "core/scripts_registry.h"
namespace {
Logger logger("XrayConfigurator");
}
XrayConfigurator::XrayConfigurator(std::shared_ptr<Settings> settings, const QSharedPointer<ServerController> &serverController, QObject *parent)
: ConfiguratorBase(settings, serverController, parent)
{
}
QString XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig,
ErrorCode &errorCode)
QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentials, DockerContainer container,
const QJsonObject &containerConfig, ErrorCode &errorCode)
{
QString config = m_serverController->replaceVars(amnezia::scriptData(ProtocolScriptType::xray_template, container),
m_serverController->genVarsForScript(credentials, container, containerConfig));
QString xrayPublicKey =
m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::PublicKeyPath, errorCode);
xrayPublicKey.replace("\n", "");
QString xrayUuid = m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::uuidPath, errorCode);
xrayUuid.replace("\n", "");
QString xrayShortId =
m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::shortidPath, errorCode);
xrayShortId.replace("\n", "");
// Generate new UUID for client
QString clientId = QUuid::createUuid().toString(QUuid::WithoutBraces);
// Get current server config
QString currentConfig = m_serverController->getTextFileFromContainer(
container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Failed to get server config file";
return "";
}
config.replace("$XRAY_CLIENT_ID", xrayUuid);
// Parse current config as JSON
QJsonDocument doc = QJsonDocument::fromJson(currentConfig.toUtf8());
if (doc.isNull() || !doc.isObject()) {
logger.error() << "Failed to parse server config JSON";
errorCode = ErrorCode::InternalError;
return "";
}
QJsonObject serverConfig = doc.object();
// Validate server config structure
if (!serverConfig.contains("inbounds")) {
logger.error() << "Server config missing 'inbounds' field";
errorCode = ErrorCode::InternalError;
return "";
}
QJsonArray inbounds = serverConfig["inbounds"].toArray();
if (inbounds.isEmpty()) {
logger.error() << "Server config has empty 'inbounds' array";
errorCode = ErrorCode::InternalError;
return "";
}
QJsonObject inbound = inbounds[0].toObject();
if (!inbound.contains("settings")) {
logger.error() << "Inbound missing 'settings' field";
errorCode = ErrorCode::InternalError;
return "";
}
QJsonObject settings = inbound["settings"].toObject();
if (!settings.contains("clients")) {
logger.error() << "Settings missing 'clients' field";
errorCode = ErrorCode::InternalError;
return "";
}
QJsonArray clients = settings["clients"].toArray();
// Create configuration for new client
QJsonObject clientConfig {
{"id", clientId},
{"flow", "xtls-rprx-vision"}
};
clients.append(clientConfig);
// Update config
settings["clients"] = clients;
inbound["settings"] = settings;
inbounds[0] = inbound;
serverConfig["inbounds"] = inbounds;
// Save updated config to server
QString updatedConfig = QJsonDocument(serverConfig).toJson();
errorCode = m_serverController->uploadTextFileToContainer(
container,
credentials,
updatedConfig,
amnezia::protocols::xray::serverConfigPath,
libssh::ScpOverwriteMode::ScpOverwriteExisting
);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Failed to upload updated config";
return "";
}
// Restart container
QString restartScript = QString("sudo docker restart $CONTAINER_NAME");
errorCode = m_serverController->runScript(
credentials,
m_serverController->replaceVars(restartScript, m_serverController->genVarsForScript(credentials, container))
);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Failed to restart container";
return "";
}
return clientId;
}
QString XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container,
const QJsonObject &containerConfig, ErrorCode &errorCode)
{
// Get client ID from prepareServerConfig
QString xrayClientId = prepareServerConfig(credentials, container, containerConfig, errorCode);
if (errorCode != ErrorCode::NoError || xrayClientId.isEmpty()) {
logger.error() << "Failed to prepare server config";
errorCode = ErrorCode::InternalError;
return "";
}
QString config = m_serverController->replaceVars(amnezia::scriptData(ProtocolScriptType::xray_template, container),
m_serverController->genVarsForScript(credentials, container, containerConfig));
if (config.isEmpty()) {
logger.error() << "Failed to get config template";
errorCode = ErrorCode::InternalError;
return "";
}
QString xrayPublicKey =
m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::PublicKeyPath, errorCode);
if (errorCode != ErrorCode::NoError || xrayPublicKey.isEmpty()) {
logger.error() << "Failed to get public key";
errorCode = ErrorCode::InternalError;
return "";
}
xrayPublicKey.replace("\n", "");
QString xrayShortId =
m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::shortidPath, errorCode);
if (errorCode != ErrorCode::NoError || xrayShortId.isEmpty()) {
logger.error() << "Failed to get short ID";
errorCode = ErrorCode::InternalError;
return "";
}
xrayShortId.replace("\n", "");
// Validate all required variables are present
if (!config.contains("$XRAY_CLIENT_ID") || !config.contains("$XRAY_PUBLIC_KEY") || !config.contains("$XRAY_SHORT_ID")) {
logger.error() << "Config template missing required variables:"
<< "XRAY_CLIENT_ID:" << !config.contains("$XRAY_CLIENT_ID")
<< "XRAY_PUBLIC_KEY:" << !config.contains("$XRAY_PUBLIC_KEY")
<< "XRAY_SHORT_ID:" << !config.contains("$XRAY_SHORT_ID");
errorCode = ErrorCode::InternalError;
return "";
}
config.replace("$XRAY_CLIENT_ID", xrayClientId);
config.replace("$XRAY_PUBLIC_KEY", xrayPublicKey);
config.replace("$XRAY_SHORT_ID", xrayShortId);
+4
View File
@@ -14,6 +14,10 @@ public:
QString createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig,
ErrorCode &errorCode);
private:
QString prepareServerConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig,
ErrorCode &errorCode);
};
#endif // XRAY_CONFIGURATOR_H
+127 -41
View File
@@ -1,5 +1,8 @@
#include "apiController.h"
#include <algorithm>
#include <random>
#include <QEventLoop>
#include <QNetworkAccessManager>
#include <QNetworkReply>
@@ -11,6 +14,7 @@
#include "amnezia_application.h"
#include "configurators/wireguard_configurator.h"
#include "core/enums/apiEnums.h"
#include "utilities.h"
#include "version.h"
namespace
@@ -33,6 +37,7 @@ namespace
constexpr char userCountryCode[] = "user_country_code";
constexpr char serverCountryCode[] = "server_country_code";
constexpr char serviceType[] = "service_type";
constexpr char serviceInfo[] = "service_info";
constexpr char aesKey[] = "aes_key";
constexpr char aesIv[] = "aes_iv";
@@ -40,9 +45,12 @@ namespace
constexpr char apiPayload[] = "api_payload";
constexpr char keyPayload[] = "key_payload";
constexpr char apiConfig[] = "api_config";
constexpr char authData[] = "auth_data";
}
const QStringList proxyStorageUrl = { "" };
const int requestTimeoutMsecs = 12 * 1000; // 12 secs
ErrorCode checkErrors(const QList<QSslError> &sslErrors, QNetworkReply *reply)
{
@@ -63,6 +71,28 @@ namespace
return ErrorCode::ApiConfigDownloadError;
}
}
bool shouldBypassProxy(QNetworkReply *reply, const QByteArray &responseBody, bool checkEncryption, const QByteArray &key = "",
const QByteArray &iv = "", const QByteArray &salt = "")
{
if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError
|| reply->error() == QNetworkReply::NetworkError::TimeoutError) {
qDebug() << "Timeout occurred";
return true;
} else if (responseBody.contains("html")) {
qDebug() << "The response contains an html tag";
return true;
} else if (checkEncryption) {
try {
QSimpleCrypto::QBlockCipher blockCipher;
static_cast<void>(blockCipher.decryptAesBlockCipher(responseBody, key, iv, "", salt));
} catch (...) {
qDebug() << "Failed to decrypt the data";
return true;
}
}
return false;
}
}
ApiController::ApiController(const QString &gatewayEndpoint, bool isDevEnvironment, QObject *parent)
@@ -94,8 +124,8 @@ void ApiController::fillServerConfig(const QString &protocol, const ApiControlle
configStr.replace("$OPENVPN_PRIV_KEY", apiPayloadData.certRequest.privKey);
} else if (protocol == configKey::awg) {
configStr.replace("$WIREGUARD_CLIENT_PRIVATE_KEY", apiPayloadData.wireGuardClientPrivKey);
auto serverConfig = QJsonDocument::fromJson(configStr.toUtf8()).object();
auto containers = serverConfig.value(config_key::containers).toArray();
auto newServerConfig = QJsonDocument::fromJson(configStr.toUtf8()).object();
auto containers = newServerConfig.value(config_key::containers).toArray();
if (containers.isEmpty()) {
return; // todo process error
}
@@ -114,38 +144,57 @@ void ApiController::fillServerConfig(const QString &protocol, const ApiControlle
containerConfig[config_key::transportPacketMagicHeader] = protocolConfig.value(config_key::transportPacketMagicHeader);
container[containerName] = containerConfig;
containers.replace(0, container);
serverConfig[config_key::containers] = containers;
configStr = QString(QJsonDocument(serverConfig).toJson());
newServerConfig[config_key::containers] = containers;
configStr = QString(QJsonDocument(newServerConfig).toJson());
}
QJsonObject apiConfig = QJsonDocument::fromJson(configStr.toUtf8()).object();
serverConfig[config_key::dns1] = apiConfig.value(config_key::dns1);
serverConfig[config_key::dns2] = apiConfig.value(config_key::dns2);
serverConfig[config_key::containers] = apiConfig.value(config_key::containers);
serverConfig[config_key::hostName] = apiConfig.value(config_key::hostName);
QJsonObject newServerConfig = QJsonDocument::fromJson(configStr.toUtf8()).object();
serverConfig[config_key::dns1] = newServerConfig.value(config_key::dns1);
serverConfig[config_key::dns2] = newServerConfig.value(config_key::dns2);
serverConfig[config_key::containers] = newServerConfig.value(config_key::containers);
serverConfig[config_key::hostName] = newServerConfig.value(config_key::hostName);
if (apiConfig.value(config_key::configVersion).toInt() == ApiConfigSources::AmneziaGateway) {
serverConfig[config_key::configVersion] = apiConfig.value(config_key::configVersion);
serverConfig[config_key::description] = apiConfig.value(config_key::description);
serverConfig[config_key::name] = apiConfig.value(config_key::name);
if (newServerConfig.value(config_key::configVersion).toInt() == ApiConfigSources::AmneziaGateway) {
serverConfig[config_key::configVersion] = newServerConfig.value(config_key::configVersion);
serverConfig[config_key::description] = newServerConfig.value(config_key::description);
serverConfig[config_key::name] = newServerConfig.value(config_key::name);
}
auto defaultContainer = apiConfig.value(config_key::defaultContainer).toString();
auto defaultContainer = newServerConfig.value(config_key::defaultContainer).toString();
serverConfig[config_key::defaultContainer] = defaultContainer;
QVariantMap map = serverConfig.value(configKey::apiConfig).toObject().toVariantMap();
map.insert(newServerConfig.value(configKey::apiConfig).toObject().toVariantMap());
auto apiConfig = QJsonObject::fromVariantMap(map);
if (newServerConfig.value(config_key::configVersion).toInt() == ApiConfigSources::AmneziaGateway) {
apiConfig.insert(configKey::serviceInfo, QJsonDocument::fromJson(apiResponseBody).object().value(configKey::serviceInfo).toObject());
}
serverConfig[configKey::apiConfig] = apiConfig;
return;
}
QStringList ApiController::getProxyUrls()
{
QNetworkRequest request;
request.setTransferTimeout(7000);
request.setTransferTimeout(requestTimeoutMsecs);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QEventLoop wait;
QList<QSslError> sslErrors;
QNetworkReply *reply;
QStringList proxyStorageUrl;
if (m_isDevEnvironment) {
proxyStorageUrl = QStringList { DEV_S3_ENDPOINT };
} else {
proxyStorageUrl = QStringList { PROD_S3_ENDPOINT };
}
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
for (const auto &proxyStorageUrl : proxyStorageUrl) {
request.setUrl(proxyStorageUrl);
reply = amnApp->manager()->get(request);
@@ -166,11 +215,23 @@ QStringList ApiController::getProxyUrls()
EVP_PKEY *privateKey = nullptr;
QByteArray responseBody;
try {
QByteArray key = PROD_PROXY_STORAGE_KEY;
QSimpleCrypto::QRsa rsa;
privateKey = rsa.getPrivateKeyFromByteArray(key, "");
responseBody = rsa.decrypt(encryptedResponseBody, privateKey, RSA_PKCS1_PADDING);
if (!m_isDevEnvironment) {
QCryptographicHash hash(QCryptographicHash::Sha512);
hash.addData(key);
QByteArray hashResult = hash.result().toHex();
QByteArray key = QByteArray::fromHex(hashResult.left(64));
QByteArray iv = QByteArray::fromHex(hashResult.mid(64, 32));
QByteArray ba = QByteArray::fromBase64(encryptedResponseBody);
QSimpleCrypto::QBlockCipher blockCipher;
responseBody = blockCipher.decryptAesBlockCipher(ba, key, iv);
} else {
responseBody = encryptedResponseBody;
}
} catch (...) {
Utils::logException();
qCritical() << "error loading private key from environment variables or decrypting payload";
return {};
}
@@ -221,7 +282,7 @@ void ApiController::updateServerConfigFromApi(const QString &installationUuid, c
if (serverConfig.value(config_key::configVersion).toInt()) {
QNetworkRequest request;
request.setTransferTimeout(7000);
request.setTransferTimeout(requestTimeoutMsecs);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Authorization", "Api-Key " + serverConfig.value(configKey::accessToken).toString().toUtf8());
QString endpoint = serverConfig.value(configKey::apiEdnpoint).toString();
@@ -277,7 +338,7 @@ ErrorCode ApiController::getServicesList(QByteArray &responseBody)
#endif
QNetworkRequest request;
request.setTransferTimeout(7000);
request.setTransferTimeout(requestTimeoutMsecs);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setUrl(QString("%1v1/services").arg(m_gatewayEndpoint));
@@ -292,40 +353,53 @@ ErrorCode ApiController::getServicesList(QByteArray &responseBody)
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec();
if (reply->error() == QNetworkReply::NetworkError::TimeoutError || reply->error() == QNetworkReply::NetworkError::OperationCanceledError) {
responseBody = reply->readAll();
if (sslErrors.isEmpty() && shouldBypassProxy(reply, responseBody, false)) {
m_proxyUrls = getProxyUrls();
std::random_device randomDevice;
std::mt19937 generator(randomDevice());
std::shuffle(m_proxyUrls.begin(), m_proxyUrls.end(), generator);
for (const QString &proxyUrl : m_proxyUrls) {
qDebug() << "Go to the next endpoint";
request.setUrl(QString("%1v1/services").arg(proxyUrl));
reply->deleteLater(); // delete the previous reply
reply = amnApp->manager()->get(request);
QObject::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::TimeoutError
&& reply->error() != QNetworkReply::NetworkError::OperationCanceledError) {
responseBody = reply->readAll();
if (!sslErrors.isEmpty() || !shouldBypassProxy(reply, responseBody, false)) {
break;
}
reply->deleteLater();
}
}
responseBody = reply->readAll();
auto errorCode = checkErrors(sslErrors, reply);
reply->deleteLater();
if (errorCode == ErrorCode::NoError) {
if (!responseBody.contains("services")) {
return ErrorCode::ApiServicesMissingError;
}
}
return errorCode;
}
ErrorCode ApiController::getConfigForService(const QString &installationUuid, const QString &userCountryCode, const QString &serviceType,
const QString &protocol, const QString &serverCountryCode, QJsonObject &serverConfig)
const QString &protocol, const QString &serverCountryCode, const QJsonObject &authData,
QJsonObject &serverConfig)
{
#ifdef Q_OS_IOS
IosController::Instance()->requestInetAccess();
QThread::msleep(10);
#endif
QNetworkAccessManager manager;
QNetworkRequest request;
request.setTransferTimeout(7000);
request.setTransferTimeout(requestTimeoutMsecs);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setUrl(QString("%1v1/config").arg(m_gatewayEndpoint));
@@ -339,6 +413,9 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co
}
apiPayload[configKey::serviceType] = serviceType;
apiPayload[configKey::uuid] = installationUuid;
if (!authData.isEmpty()) {
apiPayload[configKey::authData] = authData;
}
QSimpleCrypto::QBlockCipher blockCipher;
QByteArray key = blockCipher.generatePrivateSalt(32);
@@ -361,6 +438,7 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co
QSimpleCrypto::QRsa rsa;
publicKey = rsa.getPublicKeyFromByteArray(rsaKey);
} catch (...) {
Utils::logException();
qCritical() << "error loading public key from environment variables";
return ErrorCode::ApiMissingAgwPublicKey;
}
@@ -370,14 +448,16 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co
encryptedApiPayload = blockCipher.encryptAesBlockCipher(QJsonDocument(apiPayload).toJson(), key, iv, "", salt);
} catch (...) { // todo change error handling in QSimpleCrypto?
Utils::logException();
qCritical() << "error when encrypting the request body";
return ErrorCode::ApiConfigDecryptionError;
}
QJsonObject requestBody;
requestBody[configKey::keyPayload] = QString(encryptedKeyPayload.toBase64());
requestBody[configKey::apiPayload] = QString(encryptedApiPayload.toBase64());
QNetworkReply *reply = manager.post(request, QJsonDocument(requestBody).toJson());
QNetworkReply *reply = amnApp->manager()->post(request, QJsonDocument(requestBody).toJson());
QEventLoop wait;
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
@@ -386,37 +466,43 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec();
if (reply->error() == QNetworkReply::NetworkError::TimeoutError || reply->error() == QNetworkReply::NetworkError::OperationCanceledError) {
if (m_proxyUrls.isEmpty()) {
m_proxyUrls = getProxyUrls();
}
auto encryptedResponseBody = reply->readAll();
if (sslErrors.isEmpty() && shouldBypassProxy(reply, encryptedResponseBody, true, key, iv, salt)) {
m_proxyUrls = getProxyUrls();
std::random_device randomDevice;
std::mt19937 generator(randomDevice());
std::shuffle(m_proxyUrls.begin(), m_proxyUrls.end(), generator);
for (const QString &proxyUrl : m_proxyUrls) {
qDebug() << "Go to the next endpoint";
request.setUrl(QString("%1v1/config").arg(proxyUrl));
reply = manager.post(request, QJsonDocument(requestBody).toJson());
reply->deleteLater(); // delete the previous reply
reply = amnApp->manager()->post(request, QJsonDocument(requestBody).toJson());
QObject::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::TimeoutError
&& reply->error() != QNetworkReply::NetworkError::OperationCanceledError) {
encryptedResponseBody = reply->readAll();
if (!sslErrors.isEmpty() || !shouldBypassProxy(reply, encryptedResponseBody, true, key, iv, salt)) {
break;
}
reply->deleteLater();
}
}
auto errorCode = checkErrors(sslErrors, reply);
reply->deleteLater();
if (errorCode) {
return errorCode;
}
auto encryptedResponseBody = reply->readAll();
reply->deleteLater();
try {
auto responseBody = blockCipher.decryptAesBlockCipher(encryptedResponseBody, key, iv, "", salt);
fillServerConfig(protocol, apiPayloadData, responseBody, serverConfig);
} catch (...) { // todo change error handling in QSimpleCrypto?
Utils::logException();
qCritical() << "error when decrypting the request body";
return ErrorCode::ApiConfigDecryptionError;
}
return errorCode;
+1 -1
View File
@@ -21,7 +21,7 @@ public slots:
ErrorCode getServicesList(QByteArray &responseBody);
ErrorCode getConfigForService(const QString &installationUuid, const QString &userCountryCode, const QString &serviceType,
const QString &protocol, const QString &serverCountryCode, QJsonObject &serverConfig);
const QString &protocol, const QString &serverCountryCode, const QJsonObject &authData, QJsonObject &serverConfig);
signals:
void errorOccurred(ErrorCode errorCode);
+9 -3
View File
@@ -346,7 +346,9 @@ bool ServerController::isReinstallContainerRequired(DockerContainer container, c
}
if (container == DockerContainer::Awg) {
if ((oldProtoConfig.value(config_key::port).toString(protocols::awg::defaultPort)
if ((oldProtoConfig.value(config_key::subnet_address).toString(protocols::wireguard::defaultSubnetAddress)
!= newProtoConfig.value(config_key::subnet_address).toString(protocols::wireguard::defaultSubnetAddress))
|| (oldProtoConfig.value(config_key::port).toString(protocols::awg::defaultPort)
!= newProtoConfig.value(config_key::port).toString(protocols::awg::defaultPort))
|| (oldProtoConfig.value(config_key::junkPacketCount).toString(protocols::awg::defaultJunkPacketCount)
!= newProtoConfig.value(config_key::junkPacketCount).toString(protocols::awg::defaultJunkPacketCount))
@@ -370,8 +372,10 @@ bool ServerController::isReinstallContainerRequired(DockerContainer container, c
}
if (container == DockerContainer::WireGuard) {
if (oldProtoConfig.value(config_key::port).toString(protocols::wireguard::defaultPort)
!= newProtoConfig.value(config_key::port).toString(protocols::wireguard::defaultPort))
if ((oldProtoConfig.value(config_key::subnet_address).toString(protocols::wireguard::defaultSubnetAddress)
!= newProtoConfig.value(config_key::subnet_address).toString(protocols::wireguard::defaultSubnetAddress))
|| (oldProtoConfig.value(config_key::port).toString(protocols::wireguard::defaultPort)
!= newProtoConfig.value(config_key::port).toString(protocols::wireguard::defaultPort)))
return true;
}
@@ -607,6 +611,8 @@ ServerController::Vars ServerController::genVarsForScript(const ServerCredential
vars.append({ { "$SFTP_PASSWORD", sftpConfig.value(config_key::password).toString() } });
// Amnezia wireguard vars
vars.append({ { "$AWG_SUBNET_IP",
amneziaWireguarConfig.value(config_key::subnet_address).toString(protocols::wireguard::defaultSubnetAddress) } });
vars.append({ { "$AWG_SERVER_PORT", amneziaWireguarConfig.value(config_key::port).toString(protocols::awg::defaultPort) } });
vars.append({ { "$JUNK_PACKET_COUNT", amneziaWireguarConfig.value(config_key::junkPacketCount).toString() } });
+3
View File
@@ -96,6 +96,7 @@ namespace amnezia
// import and install errors
ImportInvalidConfigError = 900,
ImportOpenConfigError = 901,
// Android errors
AndroidError = 1000,
@@ -107,6 +108,8 @@ namespace amnezia
ApiConfigTimeoutError = 1103,
ApiConfigSslError = 1104,
ApiMissingAgwPublicKey = 1105,
ApiConfigDecryptionError = 1106,
ApiServicesMissingError = 1107,
// QFile errors
OpenError = 1200,
+4 -1
View File
@@ -50,6 +50,7 @@ QString errorString(ErrorCode code) {
case (ErrorCode::AddressPoolError): errorMessage = QObject::tr("VPN pool error: no available addresses"); break;
case (ErrorCode::ImportInvalidConfigError): errorMessage = QObject::tr("The config does not contain any containers and credentials for connecting to the server"); break;
case (ErrorCode::ImportOpenConfigError): errorMessage = QObject::tr("Unable to open config file"); break;
// Android errors
case (ErrorCode::AndroidError): errorMessage = QObject::tr("VPN connection error"); break;
@@ -61,7 +62,9 @@ QString errorString(ErrorCode code) {
case (ErrorCode::ApiConfigSslError): errorMessage = QObject::tr("SSL error occurred"); break;
case (ErrorCode::ApiConfigTimeoutError): errorMessage = QObject::tr("Server response timeout on api request"); break;
case (ErrorCode::ApiMissingAgwPublicKey): errorMessage = QObject::tr("Missing AGW public key"); break;
case (ErrorCode::ApiConfigDecryptionError): errorMessage = QObject::tr("Failed to decrypt response payload"); break;
case (ErrorCode::ApiServicesMissingError): errorMessage = QObject::tr("Missing list of available services"); break;
// QFile errors
case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break;
case(ErrorCode::ReadError): errorMessage = QObject::tr("QFile error: An error occurred when reading from the file"); break;
+1 -1
View File
@@ -104,7 +104,7 @@ QJsonObject Deserialize(const QString &vmessStr, QString *alias, QString *errMes
server.users.first().security = "auto";
}
const static auto getQueryValue = [&query](const QString &key, const QString &defaultValue) {
const auto getQueryValue = [&query](const QString &key, const QString &defaultValue) {
if (query.hasQueryItem(key))
return query.queryItemValue(key, QUrl::FullyDecoded);
else
+5 -18
View File
@@ -78,7 +78,7 @@ bool Daemon::activate(const InterfaceConfig& config) {
return false;
}
if (supportDnsUtils() && !dnsutils()->restoreResolvers()) {
if (!dnsutils()->restoreResolvers()) {
return false;
}
@@ -165,10 +165,6 @@ bool Daemon::activate(const InterfaceConfig& config) {
}
bool Daemon::maybeUpdateResolvers(const InterfaceConfig& config) {
if (!supportDnsUtils()) {
return true;
}
if ((config.m_hopType == InterfaceConfig::MultiHopExit) ||
(config.m_hopType == InterfaceConfig::SingleHop)) {
QList<QHostAddress> resolvers;
@@ -423,13 +419,8 @@ bool Daemon::deactivate(bool emitSignals) {
}
// Cleanup DNS
if (supportDnsUtils() && !dnsutils()->restoreResolvers()) {
return false;
}
if (!wgutils()->interfaceExists()) {
logger.warning() << "Wireguard interface does not exist.";
return false;
if (!dnsutils()->restoreResolvers()) {
logger.warning() << "Failed to restore DNS resolvers.";
}
// Cleanup peers and routing
@@ -449,13 +440,9 @@ bool Daemon::deactivate(bool emitSignals) {
}
m_excludedAddrSet.clear();
// Delete the interface
if (!wgutils()->deleteInterface()) {
return false;
}
m_connections.clear();
return true;
// Delete the interface
return wgutils()->deleteInterface();
}
QString Daemon::logs() {
-1
View File
@@ -69,7 +69,6 @@ class Daemon : public QObject {
virtual WireguardUtils* wgutils() const = 0;
virtual bool supportIPUtils() const { return false; }
virtual IPUtils* iputils() { return nullptr; }
virtual bool supportDnsUtils() const { return false; }
virtual DnsUtils* dnsutils() { return nullptr; }
static bool parseStringList(const QJsonObject& obj, const QString& name,
+13 -4
View File
@@ -92,6 +92,17 @@ void DaemonLocalServerConnection::parseCommand(const QByteArray& data) {
logger.debug() << "Command received:" << type;
// It is expected that sometimes the client will request backend logs
// before the first authentication. In these cases we just return empty
// logs.
if (type == "logs") {
QJsonObject obj;
obj.insert("type", "logs");
obj.insert("logs", "");
write(obj);
return;
}
if (type == "activate") {
InterfaceConfig config;
if (!Daemon::parseConfig(obj, config)) {
@@ -115,8 +126,7 @@ void DaemonLocalServerConnection::parseCommand(const QByteArray& data) {
if (type == "status") {
QJsonObject obj = Daemon::instance()->getStatus();
obj.insert("type", "status");
m_socket->write(QJsonDocument(obj).toJson(QJsonDocument::Compact));
m_socket->write("\n");
write(obj);
return;
}
@@ -124,8 +134,7 @@ void DaemonLocalServerConnection::parseCommand(const QByteArray& data) {
QJsonObject obj;
obj.insert("type", "logs");
obj.insert("logs", Daemon::instance()->logs().replace("\n", "|"));
m_socket->write(QJsonDocument(obj).toJson(QJsonDocument::Compact));
m_socket->write("\n");
write(obj);
return;
}
+5
View File
@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 13V19C18 19.5304 17.7893 20.0391 17.4142 20.4142C17.0391 20.7893 16.5304 21 16 21H5C4.46957 21 3.96086 20.7893 3.58579 20.4142C3.21071 20.0391 3 19.5304 3 19V8C3 7.46957 3.21071 6.96086 3.58579 6.58579C3.96086 6.21071 4.46957 6 5 6H11" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15 3H21V9" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 14L21 3" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 644 B

+2 -2
View File
@@ -34,8 +34,8 @@ LocalSocketController::LocalSocketController() {
m_socket = new QLocalSocket(this);
connect(m_socket, &QLocalSocket::connected, this,
&LocalSocketController::daemonConnected);
connect(m_socket, &QLocalSocket::disconnected, this,
&LocalSocketController::disconnected);
connect(m_socket, &QLocalSocket::disconnected, this,
[&] { errorOccurred(QLocalSocket::PeerClosedError); });
connect(m_socket, &QLocalSocket::errorOccurred, this,
&LocalSocketController::errorOccurred);
connect(m_socket, &QLocalSocket::readyRead, this,
@@ -163,9 +163,7 @@ QString AndroidController::openFile(const QString &filter)
QString fileName;
connect(this, &AndroidController::fileOpened, this,
[&fileName, &wait](const QString &uri) {
qDebug() << "Android event: file opened; uri:" << uri;
fileName = QQmlFile::urlToLocalFileOrQrc(uri);
qDebug() << "Android opened filename:" << fileName;
fileName = uri;
wait.quit();
},
static_cast<Qt::ConnectionType>(Qt::QueuedConnection | Qt::SingleShotConnection));
@@ -175,6 +173,25 @@ QString AndroidController::openFile(const QString &filter)
return fileName;
}
int AndroidController::getFd(const QString &fileName)
{
return callActivityMethod<jint>("getFd", "(Ljava/lang/String;)I",
QJniObject::fromString(fileName).object<jstring>());
}
void AndroidController::closeFd()
{
callActivityMethod("closeFd", "()V");
}
QString AndroidController::getFileName(const QString &uri)
{
auto fileName = callActivityMethod<jstring, jstring>("getFileName", "(Ljava/lang/String;)Ljava/lang/String;",
QJniObject::fromString(uri).object<jstring>());
QJniEnvironment env;
return AndroidUtils::convertJString(env.jniEnv(), fileName.object<jstring>());
}
bool AndroidController::isCameraPresent()
{
return callActivityMethod<jboolean>("isCameraPresent", "()Z");
@@ -287,6 +304,11 @@ bool AndroidController::requestAuthentication()
return result;
}
void AndroidController::sendTouch(float x, float y)
{
callActivityMethod("sendTouch", "(FF)V", x, y);
}
// Moving log processing to the Android side
jclass AndroidController::log;
jmethodID AndroidController::logDebug;
@@ -34,6 +34,9 @@ public:
void resetLastServer(int serverIndex);
void saveFile(const QString &fileName, const QString &data);
QString openFile(const QString &filter);
int getFd(const QString &fileName);
void closeFd();
QString getFileName(const QString &uri);
bool isCameraPresent();
bool isOnTv();
void startQrReaderActivity();
@@ -48,6 +51,7 @@ public:
bool isNotificationPermissionGranted();
void requestNotificationPermission();
bool requestAuthentication();
void sendTouch(float x, float y);
static bool initLogging();
static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message);
@@ -22,7 +22,6 @@ class LinuxDaemon final : public Daemon {
protected:
WireguardUtils* wgutils() const override { return m_wgutils; }
bool supportDnsUtils() const override { return true; }
DnsUtils* dnsutils() override { return m_dnsutils; }
bool supportIPUtils() const override { return true; }
IPUtils* iputils() override { return m_iputils; }
@@ -196,6 +196,8 @@ QStringList LinuxFirewall::getDNSRules(const QStringList& servers)
result << QStringLiteral("-o amn0+ -d %1 -p tcp --dport 53 -j ACCEPT").arg(server);
result << QStringLiteral("-o tun0+ -d %1 -p udp --dport 53 -j ACCEPT").arg(server);
result << QStringLiteral("-o tun0+ -d %1 -p tcp --dport 53 -j ACCEPT").arg(server);
result << QStringLiteral("-o tun2+ -d %1 -p udp --dport 53 -j ACCEPT").arg(server);
result << QStringLiteral("-o tun2+ -d %1 -p tcp --dport 53 -j ACCEPT").arg(server);
}
return result;
}
@@ -277,6 +279,7 @@ void LinuxFirewall::install()
installAnchor(Both, QStringLiteral("200.allowVPN"), {
QStringLiteral("-o amn0+ -j ACCEPT"),
QStringLiteral("-o tun0+ -j ACCEPT"),
QStringLiteral("-o tun2+ -j ACCEPT"),
});
installAnchor(IPv4, QStringLiteral("120.blockNets"), {});
@@ -21,7 +21,6 @@ class MacOSDaemon final : public Daemon {
protected:
WireguardUtils* wgutils() const override { return m_wgutils; }
bool supportDnsUtils() const override { return true; }
DnsUtils* dnsutils() override { return m_dnsutils; }
bool supportIPUtils() const override { return true; }
IPUtils* iputils() override { return m_iputils; }
@@ -26,7 +26,6 @@ class WindowsDaemon final : public Daemon {
protected:
bool run(Op op, const InterfaceConfig& config) override;
WireguardUtils* wgutils() const override { return m_wgutils; }
bool supportDnsUtils() const override { return true; }
DnsUtils* dnsutils() override { return m_dnsutils; }
private:
@@ -502,7 +502,7 @@ QString WindowsSplitTunnel::convertPath(const QString& path) {
// device should contain : for e.g C:
return "";
}
QByteArray buffer(2048, 0xFF);
QByteArray buffer(2048, 0xFFu);
auto ok = QueryDosDeviceW(qUtf16Printable(driveLetter),
(wchar_t*)buffer.data(), buffer.size() / 2);
@@ -248,7 +248,7 @@ bool WireguardUtilsWindows::updateRoutePrefix(const IPAddress& prefix) {
}
if (result != NO_ERROR) {
logger.error() << "Failed to create route to"
<< logger.sensitive(prefix.toString())
<< prefix.toString()
<< "result:" << result;
}
return result == NO_ERROR;
@@ -265,7 +265,7 @@ bool WireguardUtilsWindows::deleteRoutePrefix(const IPAddress& prefix) {
}
if (result != NO_ERROR) {
logger.error() << "Failed to delete route to"
<< logger.sensitive(prefix.toString())
<< prefix.toString()
<< "result:" << result;
}
return result == NO_ERROR;
+1 -1
View File
@@ -21,7 +21,7 @@
#include "platforms/windows/windowsutils.h"
constexpr const char* VPN_NAME = "AmneziaVPN";
constexpr const char* WIREGUARD_DIR = "WireGuard";
constexpr const char* WIREGUARD_DIR = "AmneziaWG";
constexpr const char* DATA_DIR = "Data";
namespace {
+16 -19
View File
@@ -1,7 +1,6 @@
#include "xrayprotocol.h"
#include "utilities.h"
#include "containers/containers_defs.h"
#include "core/networkUtilities.h"
#include <QCryptographicHash>
@@ -22,9 +21,8 @@ XrayProtocol::XrayProtocol(const QJsonObject &configuration, QObject *parent):
XrayProtocol::~XrayProtocol()
{
qDebug() << "XrayProtocol::~XrayProtocol()";
XrayProtocol::stop();
QThread::msleep(200);
m_xrayProcess.close();
}
ErrorCode XrayProtocol::start()
@@ -36,10 +34,6 @@ ErrorCode XrayProtocol::start()
return lastError();
}
if (Utils::processIsRunning(Utils::executable(xrayExecPath(), true))) {
Utils::killProcessByName(Utils::executable(xrayExecPath(), true));
}
#ifdef QT_DEBUG
m_xrayCfgFile.setAutoRemove(false);
#endif
@@ -54,9 +48,16 @@ ErrorCode XrayProtocol::start()
qDebug().noquote() << "XrayProtocol::start()"
<< xrayExecPath() << args.join(" ");
m_xrayProcess.setProcessChannelMode(QProcess::MergedChannels);
m_xrayProcess.setProcessChannelMode(QProcess::MergedChannels);
m_xrayProcess.setProgram(xrayExecPath());
if (Utils::processIsRunning(Utils::executable("xray", false))) {
qDebug().noquote() << "kill previos xray";
Utils::killProcessByName(Utils::executable("xray", false));
}
m_xrayProcess.setArguments(args);
connect(&m_xrayProcess, &QProcess::readyReadStandardOutput, this, [this]() {
@@ -68,13 +69,9 @@ ErrorCode XrayProtocol::start()
connect(&m_xrayProcess, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, [this](int exitCode, QProcess::ExitStatus exitStatus) {
qDebug().noquote() << "XrayProtocol finished, exitCode, exitStatus" << exitCode << exitStatus;
setConnectionState(Vpn::ConnectionState::Disconnected);
if (exitStatus != QProcess::NormalExit) {
emit protocolError(amnezia::ErrorCode::XrayExecutableCrashed);
stop();
}
if (exitCode != 0) {
emit protocolError(amnezia::ErrorCode::InternalError);
stop();
if ((exitStatus != QProcess::NormalExit) || (exitCode != 0)) {
emit protocolError(amnezia::ErrorCode::XrayExecutableCrashed);
emit setConnectionState(Vpn::ConnectionState::Error);
}
});
@@ -177,14 +174,14 @@ void XrayProtocol::stop()
IpcClient::Interface()->StartRoutingIpv6();
#endif
qDebug() << "XrayProtocol::stop()";
m_xrayProcess.terminate();
m_xrayProcess.disconnect();
m_xrayProcess.kill();
m_xrayProcess.waitForFinished(3000);
if (m_t2sProcess) {
m_t2sProcess->stop();
}
#ifdef Q_OS_WIN
Utils::signalCtrl(m_xrayProcess.processId(), CTRL_C_EVENT);
#endif
setConnectionState(Vpn::ConnectionState::Disconnected);
}
QString XrayProtocol::xrayExecPath()
+195 -191
View File
@@ -1,225 +1,229 @@
<RCC>
<qresource prefix="/">
<file>fonts/pt-root-ui_vf.ttf</file>
<file>images/amneziaBigLogo.png</file>
<file>images/AmneziaVPN.png</file>
<file>images/controls/alert-circle.svg</file>
<file>images/controls/amnezia.svg</file>
<file>images/controls/app.svg</file>
<file>images/controls/archive-restore.svg</file>
<file>images/controls/arrow-left.svg</file>
<file>images/controls/arrow-right.svg</file>
<file>images/controls/bug.svg</file>
<file>images/controls/check.svg</file>
<file>images/controls/chevron-down.svg</file>
<file>images/controls/chevron-right.svg</file>
<file>images/controls/chevron-up.svg</file>
<file>images/controls/close.svg</file>
<file>images/controls/copy.svg</file>
<file>images/controls/delete.svg</file>
<file>images/controls/download.svg</file>
<file>images/controls/edit-3.svg</file>
<file>images/controls/eye-off.svg</file>
<file>images/controls/eye.svg</file>
<file>images/controls/external-link.svg</file>
<file>images/controls/file-check-2.svg</file>
<file>images/controls/file-cog-2.svg</file>
<file>images/controls/folder-open.svg</file>
<file>images/controls/folder-search-2.svg</file>
<file>images/controls/gauge.svg</file>
<file>images/controls/github.svg</file>
<file>images/controls/help-circle.svg</file>
<file>images/controls/history.svg</file>
<file>images/controls/home.svg</file>
<file>images/controls/info.svg</file>
<file>images/controls/mail.svg</file>
<file>images/controls/map-pin.svg</file>
<file>images/controls/more-vertical.svg</file>
<file>images/controls/plus.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.png</file>
<file>images/controls/radio-button-pressed.svg</file>
<file>images/controls/radio-button.svg</file>
<file>images/controls/radio.svg</file>
<file>images/controls/refresh-cw.svg</file>
<file>images/controls/save.svg</file>
<file>images/controls/scan-line.svg</file>
<file>images/controls/search.svg</file>
<file>images/controls/server.svg</file>
<file>images/controls/settings-2.svg</file>
<file>images/controls/settings.svg</file>
<file>images/controls/share-2.svg</file>
<file>images/controls/split-tunneling.svg</file>
<file>images/controls/tag.svg</file>
<file>images/controls/telegram.svg</file>
<file>images/controls/text-cursor.svg</file>
<file>images/controls/trash.svg</file>
<file>images/controls/x-circle.svg</file>
<file>images/tray/active.png</file>
<file>images/tray/default.png</file>
<file>images/tray/error.png</file>
<file>images/AmneziaVPN.png</file>
<file>server_scripts/remove_container.sh</file>
<file>server_scripts/setup_host_firewall.sh</file>
<file>server_scripts/openvpn_cloak/Dockerfile</file>
<file>server_scripts/awg/configure_container.sh</file>
<file>server_scripts/awg/Dockerfile</file>
<file>server_scripts/awg/run_container.sh</file>
<file>server_scripts/awg/start.sh</file>
<file>server_scripts/awg/template.conf</file>
<file>server_scripts/build_container.sh</file>
<file>server_scripts/check_connection.sh</file>
<file>server_scripts/check_server_is_busy.sh</file>
<file>server_scripts/check_user_in_sudo.sh</file>
<file>server_scripts/dns/configure_container.sh</file>
<file>server_scripts/dns/Dockerfile</file>
<file>server_scripts/dns/run_container.sh</file>
<file>server_scripts/install_docker.sh</file>
<file>server_scripts/ipsec/configure_container.sh</file>
<file>server_scripts/ipsec/Dockerfile</file>
<file>server_scripts/ipsec/mobileconfig.plist</file>
<file>server_scripts/ipsec/run_container.sh</file>
<file>server_scripts/ipsec/start.sh</file>
<file>server_scripts/ipsec/strongswan.profile</file>
<file>server_scripts/openvpn_cloak/configure_container.sh</file>
<file>server_scripts/openvpn_cloak/Dockerfile</file>
<file>server_scripts/openvpn_cloak/run_container.sh</file>
<file>server_scripts/openvpn_cloak/start.sh</file>
<file>server_scripts/openvpn_cloak/template.ovpn</file>
<file>server_scripts/install_docker.sh</file>
<file>server_scripts/build_container.sh</file>
<file>server_scripts/prepare_host.sh</file>
<file>server_scripts/check_connection.sh</file>
<file>server_scripts/remove_all_containers.sh</file>
<file>server_scripts/openvpn_cloak/run_container.sh</file>
<file>server_scripts/openvpn/configure_container.sh</file>
<file>server_scripts/openvpn/run_container.sh</file>
<file>server_scripts/openvpn/template.ovpn</file>
<file>server_scripts/openvpn/Dockerfile</file>
<file>server_scripts/openvpn/start.sh</file>
<file>server_scripts/openvpn_shadowsocks/configure_container.sh</file>
<file>server_scripts/openvpn_shadowsocks/Dockerfile</file>
<file>server_scripts/openvpn_shadowsocks/run_container.sh</file>
<file>server_scripts/openvpn_shadowsocks/start.sh</file>
<file>server_scripts/openvpn_shadowsocks/template.ovpn</file>
<file>server_scripts/openvpn/configure_container.sh</file>
<file>server_scripts/openvpn/Dockerfile</file>
<file>server_scripts/openvpn/run_container.sh</file>
<file>server_scripts/openvpn/start.sh</file>
<file>server_scripts/openvpn/template.ovpn</file>
<file>server_scripts/prepare_host.sh</file>
<file>server_scripts/remove_all_containers.sh</file>
<file>server_scripts/remove_container.sh</file>
<file>server_scripts/setup_host_firewall.sh</file>
<file>server_scripts/sftp/configure_container.sh</file>
<file>server_scripts/sftp/Dockerfile</file>
<file>server_scripts/sftp/run_container.sh</file>
<file>server_scripts/socks5_proxy/configure_container.sh</file>
<file>server_scripts/socks5_proxy/Dockerfile</file>
<file>server_scripts/socks5_proxy/run_container.sh</file>
<file>server_scripts/socks5_proxy/start.sh</file>
<file>server_scripts/website_tor/configure_container.sh</file>
<file>server_scripts/website_tor/Dockerfile</file>
<file>server_scripts/website_tor/run_container.sh</file>
<file>server_scripts/wireguard/configure_container.sh</file>
<file>server_scripts/wireguard/Dockerfile</file>
<file>server_scripts/wireguard/run_container.sh</file>
<file>server_scripts/wireguard/start.sh</file>
<file>server_scripts/wireguard/template.conf</file>
<file>server_scripts/website_tor/configure_container.sh</file>
<file>server_scripts/website_tor/run_container.sh</file>
<file>ui/qml/Config/GlobalConfig.qml</file>
<file>ui/qml/Config/qmldir</file>
<file>server_scripts/check_server_is_busy.sh</file>
<file>server_scripts/dns/configure_container.sh</file>
<file>server_scripts/dns/Dockerfile</file>
<file>server_scripts/dns/run_container.sh</file>
<file>server_scripts/sftp/configure_container.sh</file>
<file>server_scripts/sftp/Dockerfile</file>
<file>server_scripts/sftp/run_container.sh</file>
<file>server_scripts/ipsec/configure_container.sh</file>
<file>server_scripts/ipsec/Dockerfile</file>
<file>server_scripts/ipsec/run_container.sh</file>
<file>server_scripts/ipsec/start.sh</file>
<file>server_scripts/ipsec/mobileconfig.plist</file>
<file>server_scripts/ipsec/strongswan.profile</file>
<file>server_scripts/website_tor/Dockerfile</file>
<file>server_scripts/check_user_in_sudo.sh</file>
<file>ui/qml/Controls2/BasicButtonType.qml</file>
<file>ui/qml/Controls2/TextFieldWithHeaderType.qml</file>
<file>ui/qml/Controls2/LabelWithButtonType.qml</file>
<file>images/controls/arrow-right.svg</file>
<file>images/controls/chevron-right.svg</file>
<file>ui/qml/Controls2/ImageButtonType.qml</file>
<file>ui/qml/Controls2/CardType.qml</file>
<file>ui/qml/Controls2/CheckBoxType.qml</file>
<file>images/controls/check.svg</file>
<file>ui/qml/Controls2/DropDownType.qml</file>
<file>ui/qml/Pages2/PageSetupWizardStart.qml</file>
<file>ui/qml/main2.qml</file>
<file>images/amneziaBigLogo.png</file>
<file>ui/qml/Controls2/FlickableType.qml</file>
<file>ui/qml/Pages2/PageSetupWizardCredentials.qml</file>
<file>ui/qml/Controls2/HeaderType.qml</file>
<file>images/controls/arrow-left.svg</file>
<file>ui/qml/Pages2/PageSetupWizardProtocols.qml</file>
<file>ui/qml/Pages2/PageSetupWizardEasy.qml</file>
<file>images/controls/chevron-down.svg</file>
<file>images/controls/chevron-up.svg</file>
<file>ui/qml/Controls2/TextTypes/ParagraphTextType.qml</file>
<file>ui/qml/Controls2/TextTypes/Header2TextType.qml</file>
<file>ui/qml/Controls2/HorizontalRadioButton.qml</file>
<file>ui/qml/Controls2/VerticalRadioButton.qml</file>
<file>ui/qml/Controls2/SwitcherType.qml</file>
<file>ui/qml/Controls2/TabButtonType.qml</file>
<file>ui/qml/Pages2/PageSetupWizardProtocolSettings.qml</file>
<file>ui/qml/Pages2/PageSetupWizardInstalling.qml</file>
<file>ui/qml/Pages2/PageSetupWizardConfigSource.qml</file>
<file>images/controls/folder-open.svg</file>
<file>images/controls/qr-code.svg</file>
<file>images/controls/text-cursor.svg</file>
<file>ui/qml/Pages2/PageSetupWizardTextKey.qml</file>
<file>ui/qml/Pages2/PageStart.qml</file>
<file>ui/qml/Controls2/TabImageButtonType.qml</file>
<file>images/controls/home.svg</file>
<file>images/controls/settings-2.svg</file>
<file>images/controls/share-2.svg</file>
<file>ui/qml/Pages2/PageHome.qml</file>
<file>ui/qml/Pages2/PageSettingsServersList.qml</file>
<file>ui/qml/Pages2/PageShare.qml</file>
<file>ui/qml/Controls2/TextTypes/Header1TextType.qml</file>
<file>ui/qml/Controls2/TextTypes/LabelTextType.qml</file>
<file>ui/qml/Controls2/TextTypes/ButtonTextType.qml</file>
<file>ui/qml/Controls2/Header2Type.qml</file>
<file>images/controls/plus.svg</file>
<file>ui/qml/Components/ConnectButton.qml</file>
<file>images/controls/download.svg</file>
<file>ui/qml/Controls2/ProgressBarType.qml</file>
<file>ui/qml/Components/ConnectionTypeSelectionDrawer.qml</file>
<file>ui/qml/Components/HomeContainersListView.qml</file>
<file>ui/qml/Controls2/TextTypes/CaptionTextType.qml</file>
<file>images/controls/settings.svg</file>
<file>ui/qml/Pages2/PageSettingsServerInfo.qml</file>
<file>ui/qml/Controls2/PageType.qml</file>
<file>ui/qml/Controls2/PopupType.qml</file>
<file>images/controls/edit-3.svg</file>
<file>ui/qml/Pages2/PageSettingsServerData.qml</file>
<file>ui/qml/Components/SettingsContainersListView.qml</file>
<file>ui/qml/Controls2/TextTypes/ListItemTitleType.qml</file>
<file>ui/qml/Controls2/DividerType.qml</file>
<file>ui/qml/Controls2/StackViewType.qml</file>
<file>ui/qml/Pages2/PageSettings.qml</file>
<file>images/controls/amnezia.svg</file>
<file>images/controls/app.svg</file>
<file>images/controls/radio.svg</file>
<file>images/controls/save.svg</file>
<file>images/controls/server.svg</file>
<file>ui/qml/Pages2/PageSettingsServerProtocols.qml</file>
<file>ui/qml/Pages2/PageSettingsServerServices.qml</file>
<file>ui/qml/Pages2/PageSetupWizardViewConfig.qml</file>
<file>images/controls/file-cog-2.svg</file>
<file>ui/qml/Components/QuestionDrawer.qml</file>
<file>ui/qml/Pages2/PageDeinstalling.qml</file>
<file>ui/qml/Controls2/BackButtonType.qml</file>
<file>ui/qml/Pages2/PageSettingsServerProtocol.qml</file>
<file>ui/qml/Components/TransportProtoSelector.qml</file>
<file>ui/qml/Controls2/ListViewWithRadioButtonType.qml</file>
<file>images/controls/radio-button.svg</file>
<file>images/controls/radio-button-inner-circle.png</file>
<file>images/controls/radio-button-pressed.svg</file>
<file>images/controls/radio-button-inner-circle-pressed.png</file>
<file>ui/qml/Components/ShareConnectionDrawer.qml</file>
<file>ui/qml/Pages2/PageSettingsConnection.qml</file>
<file>ui/qml/Pages2/PageSettingsDns.qml</file>
<file>ui/qml/Pages2/PageSettingsApplication.qml</file>
<file>ui/qml/Pages2/PageSettingsBackup.qml</file>
<file>images/controls/delete.svg</file>
<file>ui/qml/Pages2/PageSettingsAbout.qml</file>
<file>images/controls/github.svg</file>
<file>images/controls/mail.svg</file>
<file>images/controls/telegram.svg</file>
<file>ui/qml/Controls2/TextTypes/SmallTextType.qml</file>
<file>ui/qml/Filters/ContainersModelFilters.qml</file>
<file>ui/qml/Components/SelectLanguageDrawer.qml</file>
<file>ui/qml/Controls2/BusyIndicatorType.qml</file>
<file>ui/qml/Pages2/PageProtocolOpenVpnSettings.qml</file>
<file>ui/qml/Pages2/PageProtocolShadowSocksSettings.qml</file>
<file>ui/qml/Pages2/PageProtocolCloakSettings.qml</file>
<file>ui/qml/Pages2/PageProtocolXraySettings.qml</file>
<file>ui/qml/Pages2/PageProtocolRaw.qml</file>
<file>ui/qml/Pages2/PageSettingsLogging.qml</file>
<file>ui/qml/Pages2/PageServiceSftpSettings.qml</file>
<file>images/controls/copy.svg</file>
<file>ui/qml/Pages2/PageServiceTorWebsiteSettings.qml</file>
<file>ui/qml/Pages2/PageSetupWizardQrReader.qml</file>
<file>images/controls/eye.svg</file>
<file>images/controls/eye-off.svg</file>
<file>ui/qml/Pages2/PageSettingsSplitTunneling.qml</file>
<file>ui/qml/Controls2/ContextMenuType.qml</file>
<file>ui/qml/Controls2/TextAreaType.qml</file>
<file>images/controls/trash.svg</file>
<file>images/controls/more-vertical.svg</file>
<file>ui/qml/Controls2/ListViewWithLabelsType.qml</file>
<file>ui/qml/Pages2/PageServiceDnsSettings.qml</file>
<file>ui/qml/Controls2/TopCloseButtonType.qml</file>
<file>images/controls/x-circle.svg</file>
<file>ui/qml/Pages2/PageProtocolAwgSettings.qml</file>
<file>server_scripts/awg/template.conf</file>
<file>server_scripts/awg/start.sh</file>
<file>server_scripts/awg/configure_container.sh</file>
<file>server_scripts/awg/run_container.sh</file>
<file>server_scripts/awg/Dockerfile</file>
<file>ui/qml/Pages2/PageShareFullAccess.qml</file>
<file>images/controls/close.svg</file>
<file>images/controls/search.svg</file>
<file>server_scripts/xray/configure_container.sh</file>
<file>server_scripts/xray/Dockerfile</file>
<file>server_scripts/xray/run_container.sh</file>
<file>server_scripts/xray/start.sh</file>
<file>server_scripts/xray/template.json</file>
<file>ui/qml/Pages2/PageProtocolWireGuardSettings.qml</file>
<file>ui/qml/Components/AdLabel.qml</file>
<file>ui/qml/Components/ConnectButton.qml</file>
<file>ui/qml/Components/ConnectionTypeSelectionDrawer.qml</file>
<file>ui/qml/Components/HomeContainersListView.qml</file>
<file>ui/qml/Components/HomeSplitTunnelingDrawer.qml</file>
<file>images/controls/split-tunneling.svg</file>
<file>ui/qml/Controls2/DrawerType2.qml</file>
<file>ui/qml/Pages2/PageSettingsAppSplitTunneling.qml</file>
<file>ui/qml/Components/InstalledAppsDrawer.qml</file>
<file>images/controls/alert-circle.svg</file>
<file>images/controls/file-check-2.svg</file>
<file>ui/qml/Components/QuestionDrawer.qml</file>
<file>ui/qml/Components/SelectLanguageDrawer.qml</file>
<file>ui/qml/Components/ServersListView.qml</file>
<file>ui/qml/Components/SettingsContainersListView.qml</file>
<file>ui/qml/Components/ShareConnectionDrawer.qml</file>
<file>ui/qml/Components/TransportProtoSelector.qml</file>
<file>ui/qml/Config/GlobalConfig.qml</file>
<file>ui/qml/Config/qmldir</file>
<file>ui/qml/Controls2/BackButtonType.qml</file>
<file>ui/qml/Controls2/BasicButtonType.qml</file>
<file>ui/qml/Controls2/BusyIndicatorType.qml</file>
<file>ui/qml/Controls2/CardType.qml</file>
<file>ui/qml/Controls2/CardWithIconsType.qml</file>
<file>ui/qml/Controls2/CheckBoxType.qml</file>
<file>ui/qml/Controls2/ContextMenuType.qml</file>
<file>ui/qml/Controls2/DividerType.qml</file>
<file>ui/qml/Controls2/DrawerType2.qml</file>
<file>ui/qml/Controls2/DropDownType.qml</file>
<file>ui/qml/Controls2/FlickableType.qml</file>
<file>ui/qml/Controls2/Header2Type.qml</file>
<file>ui/qml/Controls2/HeaderType.qml</file>
<file>ui/qml/Controls2/HorizontalRadioButton.qml</file>
<file>ui/qml/Controls2/ImageButtonType.qml</file>
<file>ui/qml/Controls2/LabelWithButtonType.qml</file>
<file>ui/qml/Controls2/LabelWithImageType.qml</file>
<file>ui/qml/Controls2/ListViewWithLabelsType.qml</file>
<file>ui/qml/Controls2/ListViewWithRadioButtonType.qml</file>
<file>ui/qml/Controls2/PageType.qml</file>
<file>ui/qml/Controls2/PopupType.qml</file>
<file>ui/qml/Controls2/ProgressBarType.qml</file>
<file>ui/qml/Controls2/ScrollBarType.qml</file>
<file>ui/qml/Controls2/StackViewType.qml</file>
<file>ui/qml/Controls2/SwitcherType.qml</file>
<file>ui/qml/Controls2/TabButtonType.qml</file>
<file>ui/qml/Controls2/TabImageButtonType.qml</file>
<file>ui/qml/Controls2/TextAreaType.qml</file>
<file>ui/qml/Controls2/TextAreaWithFooterType.qml</file>
<file>ui/qml/Controls2/TextFieldWithHeaderType.qml</file>
<file>ui/qml/Controls2/TextTypes/ButtonTextType.qml</file>
<file>ui/qml/Controls2/TextTypes/CaptionTextType.qml</file>
<file>ui/qml/Controls2/TextTypes/Header1TextType.qml</file>
<file>ui/qml/Controls2/TextTypes/Header2TextType.qml</file>
<file>ui/qml/Controls2/TextTypes/LabelTextType.qml</file>
<file>ui/qml/Controls2/TextTypes/ListItemTitleType.qml</file>
<file>ui/qml/Controls2/TextTypes/ParagraphTextType.qml</file>
<file>ui/qml/Controls2/TextTypes/SmallTextType.qml</file>
<file>ui/qml/Controls2/TopCloseButtonType.qml</file>
<file>ui/qml/Controls2/VerticalRadioButton.qml</file>
<file>ui/qml/Controls2/WarningType.qml</file>
<file>fonts/pt-root-ui_vf.ttf</file>
<file>ui/qml/Modules/Style/qmldir</file>
<file>ui/qml/Filters/ContainersModelFilters.qml</file>
<file>ui/qml/main2.qml</file>
<file>ui/qml/Modules/Style/AmneziaStyle.qml</file>
<file>ui/qml/Modules/Style/qmldir</file>
<file>ui/qml/Pages2/PageDeinstalling.qml</file>
<file>ui/qml/Pages2/PageDevMenu.qml</file>
<file>ui/qml/Pages2/PageHome.qml</file>
<file>ui/qml/Pages2/PageProtocolAwgSettings.qml</file>
<file>ui/qml/Pages2/PageProtocolCloakSettings.qml</file>
<file>ui/qml/Pages2/PageProtocolOpenVpnSettings.qml</file>
<file>ui/qml/Pages2/PageProtocolRaw.qml</file>
<file>ui/qml/Pages2/PageProtocolShadowSocksSettings.qml</file>
<file>ui/qml/Pages2/PageProtocolWireGuardSettings.qml</file>
<file>ui/qml/Pages2/PageProtocolXraySettings.qml</file>
<file>ui/qml/Pages2/PageServiceDnsSettings.qml</file>
<file>ui/qml/Pages2/PageServiceSftpSettings.qml</file>
<file>ui/qml/Pages2/PageServiceSocksProxySettings.qml</file>
<file>server_scripts/socks5_proxy/run_container.sh</file>
<file>server_scripts/socks5_proxy/Dockerfile</file>
<file>server_scripts/socks5_proxy/configure_container.sh</file>
<file>server_scripts/socks5_proxy/start.sh</file>
<file>ui/qml/Pages2/PageServiceTorWebsiteSettings.qml</file>
<file>ui/qml/Pages2/PageSettings.qml</file>
<file>ui/qml/Pages2/PageSettingsAbout.qml</file>
<file>ui/qml/Pages2/PageSettingsApiLanguageList.qml</file>
<file>ui/qml/Pages2/PageSettingsApiServerInfo.qml</file>
<file>ui/qml/Pages2/PageSettingsApplication.qml</file>
<file>ui/qml/Pages2/PageSettingsAppSplitTunneling.qml</file>
<file>ui/qml/Pages2/PageSettingsBackup.qml</file>
<file>ui/qml/Pages2/PageSettingsConnection.qml</file>
<file>ui/qml/Pages2/PageSettingsDns.qml</file>
<file>ui/qml/Pages2/PageSettingsLogging.qml</file>
<file>ui/qml/Pages2/PageSettingsServerData.qml</file>
<file>ui/qml/Pages2/PageSettingsServerInfo.qml</file>
<file>ui/qml/Pages2/PageSettingsServerProtocol.qml</file>
<file>ui/qml/Pages2/PageSettingsServerProtocols.qml</file>
<file>ui/qml/Pages2/PageSettingsServerServices.qml</file>
<file>ui/qml/Pages2/PageSettingsServersList.qml</file>
<file>ui/qml/Pages2/PageSettingsSplitTunneling.qml</file>
<file>ui/qml/Pages2/PageProtocolAwgClientSettings.qml</file>
<file>ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml</file>
<file>ui/qml/Pages2/PageSetupWizardApiServicesList.qml</file>
<file>ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml</file>
<file>ui/qml/Controls2/CardWithIconsType.qml</file>
<file>images/controls/tag.svg</file>
<file>images/controls/history.svg</file>
<file>images/controls/gauge.svg</file>
<file>images/controls/map-pin.svg</file>
<file>ui/qml/Controls2/LabelWithImageType.qml</file>
<file>images/controls/info.svg</file>
<file>ui/qml/Controls2/TextAreaWithFooterType.qml</file>
<file>images/controls/scan-line.svg</file>
<file>images/controls/folder-search-2.svg</file>
<file>ui/qml/Pages2/PageSettingsApiServerInfo.qml</file>
<file>images/controls/bug.svg</file>
<file>ui/qml/Pages2/PageDevMenu.qml</file>
<file>images/controls/refresh-cw.svg</file>
<file>ui/qml/Pages2/PageSettingsApiLanguageList.qml</file>
<file>images/controls/archive-restore.svg</file>
<file>images/controls/help-circle.svg</file>
<file>ui/qml/Pages2/PageSetupWizardApiServicesList.qml</file>
<file>ui/qml/Pages2/PageSetupWizardConfigSource.qml</file>
<file>ui/qml/Pages2/PageSetupWizardCredentials.qml</file>
<file>ui/qml/Pages2/PageSetupWizardEasy.qml</file>
<file>ui/qml/Pages2/PageSetupWizardInstalling.qml</file>
<file>ui/qml/Pages2/PageSetupWizardProtocols.qml</file>
<file>ui/qml/Pages2/PageSetupWizardProtocolSettings.qml</file>
<file>ui/qml/Pages2/PageSetupWizardQrReader.qml</file>
<file>ui/qml/Pages2/PageSetupWizardStart.qml</file>
<file>ui/qml/Pages2/PageSetupWizardTextKey.qml</file>
<file>ui/qml/Pages2/PageSetupWizardViewConfig.qml</file>
<file>ui/qml/Pages2/PageShare.qml</file>
<file>ui/qml/Pages2/PageShareFullAccess.qml</file>
<file>ui/qml/Pages2/PageStart.qml</file>
</qresource>
<qresource prefix="/countriesFlags">
<file>images/flagKit/ZW.svg</file>
@@ -12,7 +12,7 @@ echo $WIREGUARD_PSK > /opt/amnezia/awg/wireguard_psk.key
cat > /opt/amnezia/awg/wg0.conf <<EOF
[Interface]
PrivateKey = $WIREGUARD_SERVER_PRIVATE_KEY
Address = $WIREGUARD_SUBNET_IP/$WIREGUARD_SUBNET_CIDR
Address = $AWG_SUBNET_IP/$WIREGUARD_SUBNET_CIDR
ListenPort = $AWG_SERVER_PORT
Jc = $JUNK_PACKET_COUNT
Jmin = $JUNK_PACKET_MIN_SIZE
+4 -4
View File
@@ -17,12 +17,12 @@ iptables -A FORWARD -i wg0 -j ACCEPT
iptables -A OUTPUT -o wg0 -j ACCEPT
# Allow forwarding traffic only from the VPN.
iptables -A FORWARD -i wg0 -o eth0 -s $WIREGUARD_SUBNET_IP/$WIREGUARD_SUBNET_CIDR -j ACCEPT
iptables -A FORWARD -i wg0 -o eth1 -s $WIREGUARD_SUBNET_IP/$WIREGUARD_SUBNET_CIDR -j ACCEPT
iptables -A FORWARD -i wg0 -o eth0 -s $AWG_SUBNET_IP/$WIREGUARD_SUBNET_CIDR -j ACCEPT
iptables -A FORWARD -i wg0 -o eth1 -s $AWG_SUBNET_IP/$WIREGUARD_SUBNET_CIDR -j ACCEPT
iptables -A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -t nat -A POSTROUTING -s $WIREGUARD_SUBNET_IP/$WIREGUARD_SUBNET_CIDR -o eth0 -j MASQUERADE
iptables -t nat -A POSTROUTING -s $WIREGUARD_SUBNET_IP/$WIREGUARD_SUBNET_CIDR -o eth1 -j MASQUERADE
iptables -t nat -A POSTROUTING -s $AWG_SUBNET_IP/$WIREGUARD_SUBNET_CIDR -o eth0 -j MASQUERADE
iptables -t nat -A POSTROUTING -s $AWG_SUBNET_IP/$WIREGUARD_SUBNET_CIDR -o eth1 -j MASQUERADE
tail -f /dev/null
+10
View File
@@ -538,3 +538,13 @@ void Settings::toggleDevGatewayEnv(bool enabled)
{
m_isDevGatewayEnv = enabled;
}
bool Settings::isHomeAdLabelVisible()
{
return value("Conf/homeAdLabelVisible", true).toBool();
}
void Settings::disableHomeAdLabel()
{
setValue("Conf/homeAdLabelVisible", false);
}
+3
View File
@@ -222,6 +222,9 @@ public:
bool isDevGatewayEnv();
void toggleDevGatewayEnv(bool enabled);
bool isHomeAdLabelVisible();
void disableHomeAdLabel();
signals:
void saveLogsChanged(bool enabled);
void screenshotsEnabledChanged(bool enabled);
+1 -1
View File
@@ -2679,7 +2679,7 @@ and will not be shared or disclosed to the Amnezia or any third parties</source>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="154"/>
<source>Where to get connection data, step-by-step instructions for buying a VPS</source>
<translation>Где взять данные для подключения, пошаговые инстуркции по покупке VPS</translation>
<translation>Где взять данные для подключения, пошаговые инструкции по покупке VPS</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="170"/>
+11 -8
View File
@@ -34,13 +34,13 @@ ConnectionController::ConnectionController(const QSharedPointer<ServersModel> &s
void ConnectionController::openConnection()
{
// #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
// if (!Utils::processIsRunning(Utils::executable(SERVICE_NAME, false), true))
// {
// emit connectionErrorOccurred(ErrorCode::AmneziaServiceNotRunning);
// return;
// }
// #endif
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
if (!Utils::processIsRunning(Utils::executable(SERVICE_NAME, false), true))
{
emit connectionErrorOccurred(ErrorCode::AmneziaServiceNotRunning);
return;
}
#endif
int serverIndex = m_serversModel->getDefaultServerIndex();
QJsonObject serverConfig = m_serversModel->getServerConfig(serverIndex);
@@ -51,8 +51,11 @@ void ConnectionController::openConnection()
if (configVersion == ApiConfigSources::Telegram
&& !m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) {
emit updateApiConfigFromTelegram();
} else if (configVersion == ApiConfigSources::AmneziaGateway
&& !m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) {
emit updateApiConfigFromGateway();
} else if (configVersion && m_serversModel->isApiKeyExpired(serverIndex)) {
qDebug() << "attempt to update api config by end_date event";
qDebug() << "attempt to update api config by expires_at event";
if (configVersion == ApiConfigSources::Telegram) {
emit updateApiConfigFromTelegram();
} else {
+4 -5
View File
@@ -121,9 +121,8 @@ ErrorCode ExportController::generateNativeConfig(const DockerContainer container
jsonNativeConfig = QJsonDocument::fromJson(protocolConfigString.toUtf8()).object();
if (protocol == Proto::OpenVpn || protocol == Proto::WireGuard || protocol == Proto::Awg) {
auto clientId = jsonNativeConfig.value(config_key::clientId).toString();
errorCode = m_clientManagementModel->appendClient(clientId, clientName, container, credentials, serverController);
if (protocol == Proto::OpenVpn || protocol == Proto::WireGuard || protocol == Proto::Awg || protocol == Proto::Xray) {
errorCode = m_clientManagementModel->appendClient(jsonNativeConfig, clientName, container, credentials, serverController);
}
return errorCode;
}
@@ -248,10 +247,10 @@ void ExportController::generateCloakConfig()
emit exportConfigChanged();
}
void ExportController::generateXrayConfig()
void ExportController::generateXrayConfig(const QString &clientName)
{
QJsonObject nativeConfig;
ErrorCode errorCode = generateNativeConfig(DockerContainer::Xray, "", Proto::Xray, nativeConfig);
ErrorCode errorCode = generateNativeConfig(DockerContainer::Xray, clientName, Proto::Xray, nativeConfig);
if (errorCode) {
emit exportErrorOccurred(errorCode);
return;
+1 -1
View File
@@ -28,7 +28,7 @@ public slots:
void generateAwgConfig(const QString &clientName);
void generateShadowSocksConfig();
void generateCloakConfig();
void generateXrayConfig();
void generateXrayConfig(const QString &clientName);
QString getConfig();
QString getNativeConfigString();
+209
View File
@@ -0,0 +1,209 @@
#include "focusController.h"
#include "utils/qmlUtils.h"
#include <QQmlApplicationEngine>
#include <QQuickWindow>
FocusController::FocusController(QQmlApplicationEngine *engine, QObject *parent)
: QObject { parent },
m_engine { engine },
m_focusChain {},
m_focusedItem { nullptr },
m_rootObjects {},
m_defaultFocusItem { nullptr },
m_lvfc { nullptr }
{
QObject::connect(m_engine, &QQmlApplicationEngine::objectCreated, this, [this](QObject *object, const QUrl &url) {
QQuickItem *newDefaultFocusItem = object->findChild<QQuickItem *>("defaultFocusItem");
if (newDefaultFocusItem && m_defaultFocusItem != newDefaultFocusItem) {
m_defaultFocusItem = newDefaultFocusItem;
}
});
QObject::connect(this, &FocusController::focusedItemChanged, this,
[this]() { m_focusedItem->forceActiveFocus(Qt::TabFocusReason); });
}
void FocusController::nextKeyTabItem()
{
nextItem(Direction::Forward);
}
void FocusController::previousKeyTabItem()
{
nextItem(Direction::Backward);
}
void FocusController::nextKeyUpItem()
{
nextItem(Direction::Backward);
}
void FocusController::nextKeyDownItem()
{
nextItem(Direction::Forward);
}
void FocusController::nextKeyLeftItem()
{
nextItem(Direction::Backward);
}
void FocusController::nextKeyRightItem()
{
nextItem(Direction::Forward);
}
void FocusController::setFocusItem(QQuickItem *item)
{
if (m_focusedItem != item) {
m_focusedItem = item;
}
emit focusedItemChanged();
}
void FocusController::setFocusOnDefaultItem()
{
setFocusItem(m_defaultFocusItem);
}
void FocusController::pushRootObject(QObject *object)
{
m_rootObjects.push(object);
dropListView();
// setFocusOnDefaultItem();
}
void FocusController::dropRootObject(QObject *object)
{
if (m_rootObjects.empty()) {
return;
}
if (m_rootObjects.top() == object) {
m_rootObjects.pop();
dropListView();
setFocusOnDefaultItem();
} else {
qWarning() << "===>> TRY TO DROP WRONG ROOT OBJECT: " << m_rootObjects.top() << " SHOULD BE: " << object;
}
}
void FocusController::resetRootObject()
{
m_rootObjects.clear();
}
void FocusController::reload(Direction direction)
{
m_focusChain.clear();
QObject *rootObject = (m_rootObjects.empty() ? m_engine->rootObjects().value(0) : m_rootObjects.top());
if (!rootObject) {
qCritical() << "No ROOT OBJECT found!";
resetRootObject();
dropListView();
return;
}
m_focusChain.append(FocusControl::getSubChain(rootObject));
std::sort(m_focusChain.begin(), m_focusChain.end(),
direction == Direction::Forward ? FocusControl::isLess : FocusControl::isMore);
if (m_focusChain.empty()) {
qWarning() << "Focus chain is empty!";
resetRootObject();
dropListView();
return;
}
}
void FocusController::nextItem(Direction direction)
{
reload(direction);
if (m_lvfc && FocusControl::isListView(m_focusedItem)) {
direction == Direction::Forward ? focusNextListViewItem() : focusPreviousListViewItem();
return;
}
if (m_focusChain.empty()) {
qWarning() << "There are no items to navigate";
setFocusOnDefaultItem();
return;
}
auto focusedItemIndex = m_focusChain.indexOf(m_focusedItem);
if (focusedItemIndex == -1) {
focusedItemIndex = 0;
} else if (focusedItemIndex == (m_focusChain.size() - 1)) {
focusedItemIndex = 0;
} else {
focusedItemIndex++;
}
const auto focusedItem = qobject_cast<QQuickItem *>(m_focusChain.at(focusedItemIndex));
if (focusedItem == nullptr) {
qWarning() << "Failed to get item to focus on. Setting focus on default";
setFocusOnDefaultItem();
return;
}
if (FocusControl::isListView(focusedItem)) {
m_lvfc = new ListViewFocusController(focusedItem, this);
m_focusedItem = focusedItem;
if (direction == Direction::Forward) {
m_lvfc->nextDelegate();
focusNextListViewItem();
} else {
m_lvfc->previousDelegate();
focusPreviousListViewItem();
}
return;
}
setFocusItem(focusedItem);
}
void FocusController::focusNextListViewItem()
{
m_lvfc->reloadFocusChain();
if (m_lvfc->isLastFocusItemInListView() || m_lvfc->isReturnNeeded()) {
dropListView();
nextItem(Direction::Forward);
return;
} else if (m_lvfc->isLastFocusItemInDelegate()) {
m_lvfc->resetFocusChain();
m_lvfc->nextDelegate();
}
m_lvfc->focusNextItem();
}
void FocusController::focusPreviousListViewItem()
{
m_lvfc->reloadFocusChain();
if (m_lvfc->isFirstFocusItemInListView() || m_lvfc->isReturnNeeded()) {
dropListView();
nextItem(Direction::Backward);
return;
} else if (m_lvfc->isFirstFocusItemInDelegate()) {
m_lvfc->resetFocusChain();
m_lvfc->previousDelegate();
}
m_lvfc->focusPreviousItem();
}
void FocusController::dropListView()
{
if (m_lvfc) {
delete m_lvfc;
m_lvfc = nullptr;
}
}
+57
View File
@@ -0,0 +1,57 @@
#ifndef FOCUSCONTROLLER_H
#define FOCUSCONTROLLER_H
#include "ui/controllers/listViewFocusController.h"
#include <QQmlApplicationEngine>
/*!
* \brief The FocusController class makes focus control more straightforward
* \details Focus is handled only for visible and enabled items which have
* `isFocused` property from top left to bottom right.
* \note There are items handled differently (e.g. ListView)
*/
class FocusController : public QObject
{
Q_OBJECT
public:
explicit FocusController(QQmlApplicationEngine *engine, QObject *parent = nullptr);
~FocusController() override = default;
Q_INVOKABLE void nextKeyTabItem();
Q_INVOKABLE void previousKeyTabItem();
Q_INVOKABLE void nextKeyUpItem();
Q_INVOKABLE void nextKeyDownItem();
Q_INVOKABLE void nextKeyLeftItem();
Q_INVOKABLE void nextKeyRightItem();
Q_INVOKABLE void setFocusItem(QQuickItem *item);
Q_INVOKABLE void setFocusOnDefaultItem();
Q_INVOKABLE void pushRootObject(QObject *object);
Q_INVOKABLE void dropRootObject(QObject *object);
Q_INVOKABLE void resetRootObject();
private:
enum class Direction {
Forward,
Backward,
};
void reload(Direction direction);
void nextItem(Direction direction);
void focusNextListViewItem();
void focusPreviousListViewItem();
void dropListView();
QQmlApplicationEngine *m_engine; // Pointer to engine to get root object
QList<QObject *> m_focusChain; // List of current objects to be focused
QQuickItem *m_focusedItem; // Pointer to the active focus item
QStack<QObject *> m_rootObjects; // Pointer to stack of roots for focus chain
QQuickItem *m_defaultFocusItem;
ListViewFocusController *m_lvfc; // ListView focus manager
signals:
void focusedItemChanged();
};
#endif // FOCUSCONTROLLER_H
+16 -13
View File
@@ -9,6 +9,7 @@
#include "core/errorstrings.h"
#include "core/serialization/serialization.h"
#include "systemController.h"
#include "utilities.h"
#ifdef Q_OS_ANDROID
@@ -39,11 +40,12 @@ namespace
const QString amneziaConfigPatternUserName = "userName";
const QString amneziaConfigPatternPassword = "password";
const QString amneziaFreeConfigPattern = "api_key";
const QString amneziaPremiumConfigPattern = "auth_data";
const QString backupPattern = "Servers/serversList";
if (config.contains(backupPattern)) {
return ConfigTypes::Backup;
} else if (config.contains(amneziaConfigPattern) || config.contains(amneziaFreeConfigPattern)
} else if (config.contains(amneziaConfigPattern) || config.contains(amneziaFreeConfigPattern) || config.contains(amneziaPremiumConfigPattern)
|| (config.contains(amneziaConfigPatternHostName) && config.contains(amneziaConfigPatternUserName)
&& config.contains(amneziaConfigPatternPassword))) {
return ConfigTypes::Amnezia;
@@ -75,17 +77,18 @@ ImportController::ImportController(const QSharedPointer<ServersModel> &serversMo
bool ImportController::extractConfigFromFile(const QString &fileName)
{
QFile file(fileName);
if (file.open(QIODevice::ReadOnly)) {
QString data = file.readAll();
m_configFileName = QFileInfo(file.fileName()).fileName();
return extractConfigFromData(data);
QString data;
if (!SystemController::readFile(fileName, data)) {
emit importErrorOccurred(ErrorCode::ImportOpenConfigError, false);
return false;
}
emit importErrorOccurred(tr("Unable to open file"), false);
return false;
m_configFileName = QFileInfo(QFile(fileName).fileName()).fileName();
#ifdef Q_OS_ANDROID
if (m_configFileName.isEmpty()) {
m_configFileName = AndroidController::instance()->getFileName(fileName);
}
#endif
return extractConfigFromData(data);
}
bool ImportController::extractConfigFromData(QString data)
@@ -188,12 +191,12 @@ bool ImportController::extractConfigFromData(QString data)
if (!m_serversModel->getServersCount()) {
emit restoreAppConfig(config.toUtf8());
} else {
emit importErrorOccurred(tr("Invalid configuration file"), false);
emit importErrorOccurred(ErrorCode::ImportInvalidConfigError, false);
}
break;
}
case ConfigTypes::Invalid: {
emit importErrorOccurred(tr("Invalid configuration file"), false);
emit importErrorOccurred(ErrorCode::ImportInvalidConfigError, false);
break;
}
}
-1
View File
@@ -54,7 +54,6 @@ public slots:
signals:
void importFinished();
void importErrorOccurred(const QString &errorMessage, bool goToPageHome);
void importErrorOccurred(ErrorCode errorCode, bool goToPageHome);
void qrDecodingFinished();
+12 -35
View File
@@ -32,32 +32,8 @@ namespace
constexpr char availableCountries[] = "available_countries";
constexpr char apiConfig[] = "api_config";
constexpr char authData[] = "auth_data";
}
#ifdef Q_OS_WINDOWS
QString getNextDriverLetter()
{
QProcess drivesProc;
drivesProc.start("wmic logicaldisk get caption");
drivesProc.waitForFinished();
QString drives = drivesProc.readAll();
qDebug() << drives;
QString letters = "CFGHIJKLMNOPQRSTUVWXYZ";
QString letter;
for (int i = letters.size() - 1; i > 0; i--) {
letter = letters.at(i);
if (!drives.contains(letter + ":"))
break;
}
if (letter == "C:") {
// set err info
qDebug() << "Can't find free drive letter";
return "";
}
return letter;
}
#endif
}
InstallController::InstallController(const QSharedPointer<ServersModel> &serversModel, const QSharedPointer<ContainersModel> &containersModel,
@@ -135,10 +111,10 @@ void InstallController::install(DockerContainer container, int port, TransportPr
containerConfig[config_key::transportPacketMagicHeader] = transportPacketMagicHeader;
} else if (container == DockerContainer::Sftp) {
containerConfig.insert(config_key::userName, protocols::sftp::defaultUserName);
containerConfig.insert(config_key::password, Utils::getRandomString(10));
containerConfig.insert(config_key::password, Utils::getRandomString(16));
} else if (container == DockerContainer::Socks5Proxy) {
containerConfig.insert(config_key::userName, protocols::socks5Proxy::defaultUserName);
containerConfig.insert(config_key::password, Utils::getRandomString(10));
containerConfig.insert(config_key::password, Utils::getRandomString(16));
}
config.insert(config_key::container, ContainerProps::containerToString(container));
@@ -667,7 +643,7 @@ void InstallController::mountSftpDrive(const QString &port, const QString &passw
QString hostname = serverCredentials.hostName;
#ifdef Q_OS_WINDOWS
mountPath = getNextDriverLetter() + ":";
mountPath = Utils::getNextDriverLetter() + ":";
// QString cmd = QString("net use \\\\sshfs\\%1@x.x.x.x!%2 /USER:%1 %3")
// .arg(labelTftpUserNameText())
// .arg(labelTftpPortText())
@@ -768,7 +744,7 @@ bool InstallController::checkSshConnection(QSharedPointer<ServerController> serv
} else {
if (output.contains(tr("Please login as the user"))) {
output.replace("\n", "");
emit installationErrorOccurred(output);
emit wrongInstallationUser(output);
return false;
}
}
@@ -826,7 +802,7 @@ bool InstallController::installServiceFromApi()
ErrorCode errorCode = apiController.getConfigForService(m_settings->getInstallationUuid(true), m_apiServicesModel->getCountryCode(),
m_apiServicesModel->getSelectedServiceType(),
m_apiServicesModel->getSelectedServiceProtocol(), "", serverConfig);
m_apiServicesModel->getSelectedServiceProtocol(), "", QJsonObject(), serverConfig);
if (errorCode != ErrorCode::NoError) {
emit installationErrorOccurred(errorCode);
return false;
@@ -853,24 +829,25 @@ bool InstallController::updateServiceFromApi(const int serverIndex, const QStrin
auto serverConfig = m_serversModel->getServerConfig(serverIndex);
auto apiConfig = serverConfig.value(configKey::apiConfig).toObject();
auto authData = serverConfig.value(configKey::authData).toObject();
QJsonObject newServerConfig;
ErrorCode errorCode =
apiController.getConfigForService(m_settings->getInstallationUuid(true), apiConfig.value(configKey::userCountryCode).toString(),
apiConfig.value(configKey::serviceType).toString(),
apiConfig.value(configKey::serviceProtocol).toString(), newCountryCode, newServerConfig);
ErrorCode errorCode = apiController.getConfigForService(
m_settings->getInstallationUuid(true), apiConfig.value(configKey::userCountryCode).toString(),
apiConfig.value(configKey::serviceType).toString(), apiConfig.value(configKey::serviceProtocol).toString(), newCountryCode,
authData, newServerConfig);
if (errorCode != ErrorCode::NoError) {
emit installationErrorOccurred(errorCode);
return false;
}
QJsonObject newApiConfig = newServerConfig.value(configKey::apiConfig).toObject();
newApiConfig.insert(configKey::serviceInfo, apiConfig.value(configKey::serviceInfo));
newApiConfig.insert(configKey::userCountryCode, apiConfig.value(configKey::userCountryCode));
newApiConfig.insert(configKey::serviceType, apiConfig.value(configKey::serviceType));
newApiConfig.insert(configKey::serviceProtocol, apiConfig.value(configKey::serviceProtocol));
newServerConfig.insert(configKey::apiConfig, newApiConfig);
newServerConfig.insert(configKey::authData, authData);
m_serversModel->editServer(newServerConfig, serverIndex);
if (reloadServiceConfig) {
+1 -1
View File
@@ -75,8 +75,8 @@ signals:
void removeAllContainersFinished(const QString &finishedMessage);
void removeProcessedContainerFinished(const QString &finishedMessage);
void installationErrorOccurred(const QString &errorMessage);
void installationErrorOccurred(ErrorCode errorCode);
void wrongInstallationUser(const QString &message);
void serverAlreadyExists(int serverIndex);
@@ -0,0 +1,309 @@
#include "listViewFocusController.h"
#include "utils/qmlUtils.h"
#include <QQuickWindow>
ListViewFocusController::ListViewFocusController(QQuickItem *listView, QObject *parent)
: QObject { parent },
m_listView { listView },
m_focusChain {},
m_currentSection { Section::Default },
m_header { nullptr },
m_footer { nullptr },
m_focusedItem { nullptr },
m_focusedItemIndex { -1 },
m_delegateIndex { 0 },
m_isReturnNeeded { false },
m_currentSectionString { "Default", "Header", "Delegate", "Footer" }
{
QVariant headerItemProperty = m_listView->property("headerItem");
m_header = headerItemProperty.canConvert<QQuickItem *>() ? headerItemProperty.value<QQuickItem *>() : nullptr;
QVariant footerItemProperty = m_listView->property("footerItem");
m_footer = footerItemProperty.canConvert<QQuickItem *>() ? footerItemProperty.value<QQuickItem *>() : nullptr;
}
ListViewFocusController::~ListViewFocusController()
{
}
void ListViewFocusController::viewAtCurrentIndex() const
{
switch (m_currentSection) {
case Section::Default: [[fallthrough]];
case Section::Header: {
QMetaObject::invokeMethod(m_listView, "positionViewAtBeginning");
break;
}
case Section::Delegate: {
QMetaObject::invokeMethod(m_listView, "positionViewAtIndex", Q_ARG(int, m_delegateIndex), // Index
Q_ARG(int, 2)); // PositionMode (0 = Visible)
break;
}
case Section::Footer: {
QMetaObject::invokeMethod(m_listView, "positionViewAtEnd");
break;
}
}
}
int ListViewFocusController::size() const
{
return m_listView->property("count").toInt();
}
int ListViewFocusController::currentIndex() const
{
return m_delegateIndex;
}
void ListViewFocusController::setDelegateIndex(int index)
{
m_delegateIndex = index;
m_listView->setProperty("currentIndex", index);
}
void ListViewFocusController::nextDelegate()
{
switch (m_currentSection) {
case Section::Default: {
if (hasHeader()) {
m_currentSection = Section::Header;
viewAtCurrentIndex();
break;
}
[[fallthrough]];
}
case Section::Header: {
if (size() > 0) {
m_currentSection = Section::Delegate;
viewAtCurrentIndex();
break;
}
[[fallthrough]];
}
case Section::Delegate:
if (m_delegateIndex < (size() - 1)) {
setDelegateIndex(m_delegateIndex + 1);
viewAtCurrentIndex();
break;
} else if (hasFooter()) {
m_currentSection = Section::Footer;
viewAtCurrentIndex();
break;
}
[[fallthrough]];
case Section::Footer: {
m_isReturnNeeded = true;
m_currentSection = Section::Default;
viewAtCurrentIndex();
break;
}
default: {
qCritical() << "Current section is invalid!";
break;
}
}
}
void ListViewFocusController::previousDelegate()
{
switch (m_currentSection) {
case Section::Default: {
if (hasFooter()) {
m_currentSection = Section::Footer;
break;
}
[[fallthrough]];
}
case Section::Footer: {
if (size() > 0) {
m_currentSection = Section::Delegate;
setDelegateIndex(size() - 1);
break;
}
[[fallthrough]];
}
case Section::Delegate: {
if (m_delegateIndex > 0) {
setDelegateIndex(m_delegateIndex - 1);
break;
} else if (hasHeader()) {
m_currentSection = Section::Header;
break;
}
[[fallthrough]];
}
case Section::Header: {
m_isReturnNeeded = true;
m_currentSection = Section::Default;
break;
}
default: {
qCritical() << "Current section is invalid!";
break;
}
}
}
void ListViewFocusController::decrementIndex()
{
m_delegateIndex--;
}
QQuickItem *ListViewFocusController::itemAtIndex(const int index) const
{
QQuickItem *item { nullptr };
QMetaObject::invokeMethod(m_listView, "itemAtIndex", Q_RETURN_ARG(QQuickItem *, item), Q_ARG(int, index));
return item;
}
QQuickItem *ListViewFocusController::currentDelegate() const
{
QQuickItem *result { nullptr };
switch (m_currentSection) {
case Section::Default: {
qWarning() << "No elements...";
break;
}
case Section::Header: {
result = m_header;
break;
}
case Section::Delegate: {
result = itemAtIndex(m_delegateIndex);
break;
}
case Section::Footer: {
result = m_footer;
break;
}
}
return result;
}
QQuickItem *ListViewFocusController::focusedItem() const
{
return m_focusedItem;
}
void ListViewFocusController::focusNextItem()
{
if (m_isReturnNeeded) {
return;
}
reloadFocusChain();
if (m_focusChain.empty()) {
qWarning() << "No elements found in the delegate. Going to next delegate...";
nextDelegate();
focusNextItem();
return;
}
m_focusedItemIndex++;
m_focusedItem = qobject_cast<QQuickItem *>(m_focusChain.at(m_focusedItemIndex));
m_focusedItem->forceActiveFocus(Qt::TabFocusReason);
}
void ListViewFocusController::focusPreviousItem()
{
if (m_isReturnNeeded) {
return;
}
if (m_focusChain.empty()) {
qInfo() << "Empty focusChain with current delegate: " << currentDelegate() << "Scanning for elements...";
reloadFocusChain();
}
if (m_focusChain.empty()) {
qWarning() << "No elements found in the delegate. Going to next delegate...";
previousDelegate();
focusPreviousItem();
return;
}
if (m_focusedItemIndex == -1) {
m_focusedItemIndex = m_focusChain.size();
}
m_focusedItemIndex--;
m_focusedItem = qobject_cast<QQuickItem *>(m_focusChain.at(m_focusedItemIndex));
m_focusedItem->forceActiveFocus(Qt::TabFocusReason);
}
void ListViewFocusController::resetFocusChain()
{
m_focusChain.clear();
m_focusedItem = nullptr;
m_focusedItemIndex = -1;
}
void ListViewFocusController::reloadFocusChain()
{
m_focusChain = FocusControl::getItemsChain(currentDelegate());
}
bool ListViewFocusController::isFirstFocusItemInDelegate() const
{
return m_focusedItem && (m_focusedItem == m_focusChain.first());
}
bool ListViewFocusController::isLastFocusItemInDelegate() const
{
return m_focusedItem && (m_focusedItem == m_focusChain.last());
}
bool ListViewFocusController::hasHeader() const
{
return m_header && !FocusControl::getItemsChain(m_header).isEmpty();
}
bool ListViewFocusController::hasFooter() const
{
return m_footer && !FocusControl::getItemsChain(m_footer).isEmpty();
}
bool ListViewFocusController::isFirstFocusItemInListView() const
{
switch (m_currentSection) {
case Section::Footer: {
return isFirstFocusItemInDelegate() && !hasHeader() && (size() == 0);
}
case Section::Delegate: {
return isFirstFocusItemInDelegate() && (m_delegateIndex == 0) && !hasHeader();
}
case Section::Header: {
isFirstFocusItemInDelegate();
}
case Section::Default: {
return true;
}
default: qWarning() << "Wrong section"; return true;
}
}
bool ListViewFocusController::isLastFocusItemInListView() const
{
switch (m_currentSection) {
case Section::Default: {
return !hasHeader() && (size() == 0) && !hasFooter();
}
case Section::Header: {
return isLastFocusItemInDelegate() && (size() == 0) && !hasFooter();
}
case Section::Delegate: {
return isLastFocusItemInDelegate() && (m_delegateIndex == size() - 1) && !hasFooter();
}
case Section::Footer: {
return isLastFocusItemInDelegate();
}
default: qWarning() << "Wrong section"; return true;
}
}
bool ListViewFocusController::isReturnNeeded() const
{
return m_isReturnNeeded;
}
@@ -0,0 +1,70 @@
#ifndef LISTVIEWFOCUSCONTROLLER_H
#define LISTVIEWFOCUSCONTROLLER_H
#include <QList>
#include <QObject>
#include <QQuickItem>
#include <QSharedPointer>
#include <QStack>
/*!
* \brief The ListViewFocusController class manages the focus of elements in ListView
* \details This class object moving focus to ListView's controls since ListView stores
* it's data implicitly and it could be got one by one.
*
* This class was made to store as less as possible data getting it from QML
* when it's needed.
*/
class ListViewFocusController : public QObject
{
Q_OBJECT
public:
explicit ListViewFocusController(QQuickItem *listView, QObject *parent = nullptr);
~ListViewFocusController();
void nextDelegate();
void previousDelegate();
void decrementIndex();
void focusNextItem();
void focusPreviousItem();
void resetFocusChain();
void reloadFocusChain();
bool isFirstFocusItemInListView() const;
bool isFirstFocusItemInDelegate() const;
bool isLastFocusItemInListView() const;
bool isLastFocusItemInDelegate() const;
bool isReturnNeeded() const;
private:
enum class Section {
Default,
Header,
Delegate,
Footer,
};
int size() const;
int currentIndex() const;
void setDelegateIndex(int index);
void viewAtCurrentIndex() const;
QQuickItem *itemAtIndex(const int index) const;
QQuickItem *currentDelegate() const;
QQuickItem *focusedItem() const;
bool hasHeader() const;
bool hasFooter() const;
QQuickItem *m_listView;
QList<QObject *> m_focusChain;
Section m_currentSection;
QQuickItem *m_header;
QQuickItem *m_footer;
QQuickItem *m_focusedItem; // Pointer to focused item on Delegate
qsizetype m_focusedItemIndex;
qsizetype m_delegateIndex;
bool m_isReturnNeeded;
QList<QString> m_currentSectionString;
};
#endif // LISTVIEWFOCUSCONTROLLER_H
+16 -2
View File
@@ -81,7 +81,7 @@ void PageController::keyPressEvent(Qt::Key key)
case Qt::Key_Escape: {
if (m_drawerDepth) {
emit closeTopDrawer();
setDrawerDepth(getDrawerDepth() - 1);
decrementDrawerDepth();
} else {
emit escapePressed();
}
@@ -142,11 +142,25 @@ void PageController::setDrawerDepth(const int depth)
}
}
int PageController::getDrawerDepth()
int PageController::getDrawerDepth() const
{
return m_drawerDepth;
}
int PageController::incrementDrawerDepth()
{
return ++m_drawerDepth;
}
int PageController::decrementDrawerDepth()
{
if (m_drawerDepth == 0) {
return m_drawerDepth;
} else {
return --m_drawerDepth;
}
}
void PageController::onShowErrorMessage(ErrorCode errorCode)
{
const auto fullErrorMessage = errorString(errorCode);
+3 -4
View File
@@ -100,7 +100,9 @@ public slots:
void closeApplication();
void setDrawerDepth(const int depth);
int getDrawerDepth();
int getDrawerDepth() const;
int incrementDrawerDepth();
int decrementDrawerDepth();
private slots:
void onShowErrorMessage(amnezia::ErrorCode errorCode);
@@ -135,9 +137,6 @@ signals:
void escapePressed();
void closeTopDrawer();
void forceTabBarActiveFocus();
void forceStackActiveFocus();
private:
QSharedPointer<ServersModel> m_serversModel;
+14 -7
View File
@@ -131,12 +131,8 @@ void SettingsController::backupAppConfig(const QString &fileName)
void SettingsController::restoreAppConfig(const QString &fileName)
{
QFile file(fileName);
file.open(QIODevice::ReadOnly);
QByteArray data = file.readAll();
QByteArray data;
SystemController::readFile(fileName, data);
restoreAppConfigFromData(data);
}
@@ -324,4 +320,15 @@ bool SettingsController::isOnTv()
#else
return false;
#endif
}
}
bool SettingsController::isHomeAdLabelVisible()
{
return m_settings->isHomeAdLabelVisible();
}
void SettingsController::disableHomeAdLabel()
{
m_settings->disableHomeAdLabel();
emit isHomeAdLabelVisibleChanged(false);
}
@@ -29,6 +29,8 @@ public:
Q_PROPERTY(QString gatewayEndpoint READ getGatewayEndpoint WRITE setGatewayEndpoint NOTIFY gatewayEndpointChanged)
Q_PROPERTY(bool isDevGatewayEnv READ isDevGatewayEnv WRITE toggleDevGatewayEnv NOTIFY devGatewayEnvChanged)
Q_PROPERTY(bool isHomeAdLabelVisible READ isHomeAdLabelVisible NOTIFY isHomeAdLabelVisibleChanged)
public slots:
void toggleAmneziaDns(bool enable);
bool isAmneziaDnsEnabled();
@@ -89,6 +91,9 @@ public slots:
bool isOnTv();
bool isHomeAdLabelVisible();
void disableHomeAdLabel();
signals:
void primaryDnsChanged();
void secondaryDnsChanged();
@@ -112,6 +117,8 @@ signals:
void gatewayEndpointChanged(const QString &endpoint);
void devGatewayEnvChanged(bool enabled);
void isHomeAdLabelVisibleChanged(bool visible);
private:
QSharedPointer<ServersModel> m_serversModel;
QSharedPointer<ContainersModel> m_containersModel;
+2 -4
View File
@@ -82,14 +82,12 @@ void SitesController::removeSite(int index)
void SitesController::importSites(const QString &fileName, bool replaceExisting)
{
QFile file(fileName);
if (!file.open(QIODevice::ReadOnly)) {
QByteArray jsonData;
if (!SystemController::readFile(fileName, jsonData)) {
emit errorOccurred(tr("Can't open file: %1").arg(fileName));
return;
}
QByteArray jsonData = file.readAll();
QJsonDocument jsonDocument = QJsonDocument::fromJson(jsonData);
if (jsonDocument.isNull()) {
emit errorOccurred(tr("Failed to parse JSON data from file: %1").arg(fileName));
+33 -1
View File
@@ -24,7 +24,7 @@ SystemController::SystemController(const std::shared_ptr<Settings> &settings, QO
{
}
void SystemController::saveFile(QString fileName, const QString &data)
void SystemController::saveFile(const QString &fileName, const QString &data)
{
#if defined Q_OS_ANDROID
AndroidController::instance()->saveFile(fileName, data);
@@ -62,6 +62,31 @@ void SystemController::saveFile(QString fileName, const QString &data)
#endif
}
bool SystemController::readFile(const QString &fileName, QByteArray &data)
{
#ifdef Q_OS_ANDROID
int fd = AndroidController::instance()->getFd(fileName);
if (fd == -1) return false;
QFile file;
if(!file.open(fd, QIODevice::ReadOnly)) return false;
data = file.readAll();
AndroidController::instance()->closeFd();
#else
QFile file(fileName);
if (!file.open(QIODevice::ReadOnly)) return false;
data = file.readAll();
#endif
return true;
}
bool SystemController::readFile(const QString &fileName, QString &data)
{
QByteArray byteArray;
if(!readFile(fileName, byteArray)) return false;
data = byteArray;
return true;
}
QString SystemController::getFileName(const QString &acceptLabel, const QString &nameFilter,
const QString &selectedFile, const bool isSaveMode, const QString &defaultSuffix)
{
@@ -134,3 +159,10 @@ bool SystemController::isAuthenticated()
return true;
#endif
}
void SystemController::sendTouch(float x, float y)
{
#ifdef Q_OS_ANDROID
AndroidController::instance()->sendTouch(x, y);
#endif
}
+5 -1
View File
@@ -11,7 +11,9 @@ class SystemController : public QObject
public:
explicit SystemController(const std::shared_ptr<Settings> &setting, QObject *parent = nullptr);
static void saveFile(QString fileName, const QString &data);
static void saveFile(const QString &fileName, const QString &data);
static bool readFile(const QString &fileName, QByteArray &data);
static bool readFile(const QString &fileName, QString &data);
public slots:
QString getFileName(const QString &acceptLabel, const QString &nameFilter, const QString &selectedFile = "",
@@ -20,6 +22,8 @@ public slots:
void setQmlRoot(QObject *qmlRoot);
bool isAuthenticated();
void sendTouch(float x, float y);
signals:
void fileDialogClosed(const bool isAccepted);
+4
View File
@@ -39,6 +39,9 @@ QVariant ApiCountryModel::data(const QModelIndex &index, int role) const
case CountryNameRole: {
return countryInfo.value(configKey::serverCountryName).toString();
}
case CountryImageCodeRole: {
return countryInfo.value(configKey::serverCountryCode).toString().toUpper();
}
}
return QVariant();
@@ -76,5 +79,6 @@ QHash<int, QByteArray> ApiCountryModel::roleNames() const
QHash<int, QByteArray> roles;
roles[CountryNameRole] = "countryName";
roles[CountryCodeRole] = "countryCode";
roles[CountryImageCodeRole] = "countryImageCode";
return roles;
}
+2 -1
View File
@@ -11,7 +11,8 @@ class ApiCountryModel : public QAbstractListModel
public:
enum Roles {
CountryNameRole = Qt::UserRole + 1,
CountryCodeRole
CountryCodeRole,
CountryImageCodeRole
};
explicit ApiCountryModel(QObject *parent = nullptr);
+79 -35
View File
@@ -27,6 +27,9 @@ namespace
constexpr char storeEndpoint[] = "store_endpoint";
constexpr char isAvailable[] = "is_available";
constexpr char subscription[] = "subscription";
constexpr char endDate[] = "end_date";
}
namespace serviceType
@@ -51,23 +54,23 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
if (!index.isValid() || index.row() < 0 || index.row() >= static_cast<int>(rowCount()))
return QVariant();
QJsonObject service = m_services.at(index.row()).toObject();
QJsonObject serviceInfo = service.value(configKey::serviceInfo).toObject();
auto serviceType = service.value(configKey::serviceType).toString();
auto apiServiceData = m_services.at(index.row());
auto serviceType = apiServiceData.type;
auto isServiceAvailable = apiServiceData.isServiceAvailable;
switch (role) {
case NameRole: {
return serviceInfo.value(configKey::name).toString();
return apiServiceData.serviceInfo.name;
}
case CardDescriptionRole: {
auto speed = serviceInfo.value(configKey::speed).toString();
auto speed = apiServiceData.serviceInfo.speed;
if (serviceType == serviceType::amneziaPremium) {
return tr("Classic VPN for comfortable work, downloading large files and watching videos. "
"Works for any sites. Speed up to %1 MBit/s")
.arg(speed);
} else if (serviceType == serviceType::amneziaFree){
QString description = tr("VPN to access blocked sites in regions with high levels of Internet censorship. ");
if (service.value(configKey::isAvailable).isBool() && !service.value(configKey::isAvailable).toBool()) {
if (!isServiceAvailable) {
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 description;
@@ -83,25 +86,24 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
}
case IsServiceAvailableRole: {
if (serviceType == serviceType::amneziaFree) {
if (service.value(configKey::isAvailable).isBool() && !service.value(configKey::isAvailable).toBool()) {
if (!isServiceAvailable) {
return false;
}
}
return true;
}
case SpeedRole: {
auto speed = serviceInfo.value(configKey::speed).toString();
return tr("%1 MBit/s").arg(speed);
return tr("%1 MBit/s").arg(apiServiceData.serviceInfo.speed);
}
case WorkPeriodRole: {
auto timelimit = serviceInfo.value(configKey::timelimit).toString();
if (timelimit == "0") {
case TimeLimitRole: {
auto timeLimit = apiServiceData.serviceInfo.timeLimit;
if (timeLimit == "0") {
return "";
}
return tr("%1 days").arg(timelimit);
return tr("%1 days").arg(timeLimit);
}
case RegionRole: {
return serviceInfo.value(configKey::region).toString();
return apiServiceData.serviceInfo.region;
}
case FeaturesRole: {
if (serviceType == serviceType::amneziaPremium) {
@@ -113,12 +115,15 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
}
}
case PriceRole: {
auto price = serviceInfo.value(configKey::price).toString();
auto price = apiServiceData.serviceInfo.price;
if (price == "free") {
return tr("Free");
}
return tr("%1 $/month").arg(price);
}
case EndDateRole: {
return QDateTime::fromString(apiServiceData.subscription.endDate, Qt::ISODate).toLocalTime().toString("d MMM yyyy");
}
}
return QVariant();
@@ -128,15 +133,18 @@ void ApiServicesModel::updateModel(const QJsonObject &data)
{
beginResetModel();
m_countryCode = data.value(configKey::userCountryCode).toString();
m_services = data.value(configKey::services).toArray();
if (m_services.isEmpty()) {
QJsonObject service;
service.insert(configKey::serviceInfo, data.value(configKey::serviceInfo));
service.insert(configKey::serviceType, data.value(configKey::serviceType));
m_services.clear();
m_services.push_back(service);
m_countryCode = data.value(configKey::userCountryCode).toString();
auto services = data.value(configKey::services).toArray();
if (services.isEmpty()) {
m_services.push_back(getApiServicesData(data));
m_selectedServiceIndex = 0;
} else {
for (const auto &service : services) {
m_services.push_back(getApiServicesData(service.toObject()));
}
}
endResetModel();
@@ -149,32 +157,32 @@ void ApiServicesModel::setServiceIndex(const int index)
QJsonObject ApiServicesModel::getSelectedServiceInfo()
{
QJsonObject service = m_services.at(m_selectedServiceIndex).toObject();
return service.value(configKey::serviceInfo).toObject();
auto service = m_services.at(m_selectedServiceIndex);
return service.serviceInfo.object;
}
QString ApiServicesModel::getSelectedServiceType()
{
QJsonObject service = m_services.at(m_selectedServiceIndex).toObject();
return service.value(configKey::serviceType).toString();
auto service = m_services.at(m_selectedServiceIndex);
return service.type;
}
QString ApiServicesModel::getSelectedServiceProtocol()
{
QJsonObject service = m_services.at(m_selectedServiceIndex).toObject();
return service.value(configKey::serviceProtocol).toString();
auto service = m_services.at(m_selectedServiceIndex);
return service.protocol;
}
QString ApiServicesModel::getSelectedServiceName()
{
auto modelIndex = index(m_selectedServiceIndex, 0);
return data(modelIndex, ApiServicesModel::Roles::NameRole).toString();
auto service = m_services.at(m_selectedServiceIndex);
return service.serviceInfo.name;
}
QJsonArray ApiServicesModel::getSelectedServiceCountries()
{
QJsonObject service = m_services.at(m_selectedServiceIndex).toObject();
return service.value(configKey::availableCountries).toArray();
auto service = m_services.at(m_selectedServiceIndex);
return service.availableCountries;
}
QString ApiServicesModel::getCountryCode()
@@ -184,8 +192,8 @@ QString ApiServicesModel::getCountryCode()
QString ApiServicesModel::getStoreEndpoint()
{
QJsonObject service = m_services.at(m_selectedServiceIndex).toObject();
return service.value(configKey::storeEndpoint).toString();
auto service = m_services.at(m_selectedServiceIndex);
return service.storeEndpoint;
}
QVariant ApiServicesModel::getSelectedServiceData(const QString roleString)
@@ -209,10 +217,46 @@ QHash<int, QByteArray> ApiServicesModel::roleNames() const
roles[ServiceDescriptionRole] = "serviceDescription";
roles[IsServiceAvailableRole] = "isServiceAvailable";
roles[SpeedRole] = "speed";
roles[WorkPeriodRole] = "workPeriod";
roles[TimeLimitRole] = "timeLimit";
roles[RegionRole] = "region";
roles[FeaturesRole] = "features";
roles[PriceRole] = "price";
roles[EndDateRole] = "endDate";
return roles;
}
ApiServicesModel::ApiServicesData ApiServicesModel::getApiServicesData(const QJsonObject &data)
{
auto serviceInfo = data.value(configKey::serviceInfo).toObject();
auto serviceType = data.value(configKey::serviceType).toString();
auto serviceProtocol = data.value(configKey::serviceProtocol).toString();
auto availableCountries = data.value(configKey::availableCountries).toArray();
auto subscriptionObject = data.value(configKey::subscription).toObject();
ApiServicesData serviceData;
serviceData.serviceInfo.name = serviceInfo.value(configKey::name).toString();
serviceData.serviceInfo.price = serviceInfo.value(configKey::price).toString();
serviceData.serviceInfo.region = serviceInfo.value(configKey::region).toString();
serviceData.serviceInfo.speed = serviceInfo.value(configKey::speed).toString();
serviceData.serviceInfo.timeLimit = serviceInfo.value(configKey::timelimit).toString();
serviceData.type = serviceType;
serviceData.protocol = serviceProtocol;
serviceData.storeEndpoint = serviceInfo.value(configKey::storeEndpoint).toString();
if (serviceInfo.value(configKey::isAvailable).isBool()) {
serviceData.isServiceAvailable = data.value(configKey::isAvailable).toBool();
} else {
serviceData.isServiceAvailable = true;
}
serviceData.serviceInfo.object = serviceInfo;
serviceData.availableCountries = availableCountries;
serviceData.subscription.endDate = subscriptionObject.value(configKey::endDate).toString();
return serviceData;
}
+37 -3
View File
@@ -3,6 +3,7 @@
#include <QAbstractListModel>
#include <QJsonArray>
#include <QJsonObject>
class ApiServicesModel : public QAbstractListModel
{
@@ -15,10 +16,11 @@ public:
ServiceDescriptionRole,
IsServiceAvailableRole,
SpeedRole,
WorkPeriodRole,
TimeLimitRole,
RegionRole,
FeaturesRole,
PriceRole
PriceRole,
EndDateRole
};
explicit ApiServicesModel(QObject *parent = nullptr);
@@ -48,8 +50,40 @@ protected:
QHash<int, QByteArray> roleNames() const override;
private:
struct ServiceInfo
{
QString name;
QString speed;
QString timeLimit;
QString region;
QString price;
QJsonObject object;
};
struct Subscription
{
QString endDate;
};
struct ApiServicesData
{
bool isServiceAvailable;
QString type;
QString protocol;
QString storeEndpoint;
ServiceInfo serviceInfo;
Subscription subscription;
QJsonArray availableCountries;
};
ApiServicesData getApiServicesData(const QJsonObject &data);
QString m_countryCode;
QJsonArray m_services;
QVector<ApiServicesData> m_services;
int m_selectedServiceIndex;
};
+334 -23
View File
@@ -77,6 +77,7 @@ ErrorCode ClientManagementModel::updateModel(const DockerContainer container, co
{
beginResetModel();
m_clientsTable = QJsonArray();
endResetModel();
ErrorCode error = ErrorCode::NoError;
@@ -90,10 +91,10 @@ ErrorCode ClientManagementModel::updateModel(const DockerContainer container, co
const QByteArray clientsTableString = serverController->getTextFileFromContainer(container, credentials, clientsTableFile, error);
if (error != ErrorCode::NoError) {
logger.error() << "Failed to get the clientsTable file from the server";
endResetModel();
return error;
}
beginResetModel();
m_clientsTable = QJsonDocument::fromJson(clientsTableString).array();
if (m_clientsTable.isEmpty()) {
@@ -105,6 +106,8 @@ ErrorCode ClientManagementModel::updateModel(const DockerContainer container, co
error = getOpenVpnClients(container, credentials, serverController, count);
} else if (container == DockerContainer::WireGuard || container == DockerContainer::Awg) {
error = getWireGuardClients(container, credentials, serverController, count);
} else if (container == DockerContainer::Xray) {
error = getXrayClients(container, credentials, serverController, count);
}
if (error != ErrorCode::NoError) {
endResetModel();
@@ -238,6 +241,68 @@ ErrorCode ClientManagementModel::getWireGuardClients(const DockerContainer conta
}
return error;
}
ErrorCode ClientManagementModel::getXrayClients(const DockerContainer container, const ServerCredentials& credentials,
const QSharedPointer<ServerController> &serverController, int &count)
{
ErrorCode error = ErrorCode::NoError;
const QString serverConfigPath = amnezia::protocols::xray::serverConfigPath;
const QString configString = serverController->getTextFileFromContainer(container, credentials, serverConfigPath, error);
if (error != ErrorCode::NoError) {
logger.error() << "Failed to get the xray server config file from the server";
return error;
}
QJsonDocument serverConfig = QJsonDocument::fromJson(configString.toUtf8());
if (serverConfig.isNull()) {
logger.error() << "Failed to parse xray server config JSON";
return ErrorCode::InternalError;
}
if (!serverConfig.object().contains("inbounds") || serverConfig.object()["inbounds"].toArray().isEmpty()) {
logger.error() << "Invalid xray server config structure";
return ErrorCode::InternalError;
}
const QJsonObject inbound = serverConfig.object()["inbounds"].toArray()[0].toObject();
if (!inbound.contains("settings")) {
logger.error() << "Missing settings in xray inbound config";
return ErrorCode::InternalError;
}
const QJsonObject settings = inbound["settings"].toObject();
if (!settings.contains("clients")) {
logger.error() << "Missing clients in xray settings config";
return ErrorCode::InternalError;
}
const QJsonArray clients = settings["clients"].toArray();
for (const auto &clientValue : clients) {
const QJsonObject clientObj = clientValue.toObject();
if (!clientObj.contains("id")) {
logger.error() << "Missing id in xray client config";
continue;
}
QString clientId = clientObj["id"].toString();
QString xrayDefaultUuid = serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::uuidPath, error);
xrayDefaultUuid.replace("\n", "");
if (!isClientExists(clientId) && clientId != xrayDefaultUuid) {
QJsonObject client;
client[configKey::clientId] = clientId;
QJsonObject userData;
userData[configKey::clientName] = QString("Client %1").arg(count);
client[configKey::userData] = userData;
m_clientsTable.push_back(client);
count++;
}
}
return error;
}
ErrorCode ClientManagementModel::wgShow(const DockerContainer container, const ServerCredentials &credentials,
const QSharedPointer<ServerController> &serverController, std::vector<WgShowData> &data)
@@ -325,17 +390,67 @@ ErrorCode ClientManagementModel::appendClient(const DockerContainer container, c
const QSharedPointer<ServerController> &serverController)
{
Proto protocol;
if (container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) {
protocol = Proto::OpenVpn;
} else if (container == DockerContainer::OpenVpn || container == DockerContainer::WireGuard || container == DockerContainer::Awg) {
protocol = ContainerProps::defaultProtocol(container);
} else {
return ErrorCode::NoError;
switch (container) {
case DockerContainer::ShadowSocks:
case DockerContainer::Cloak:
protocol = Proto::OpenVpn;
break;
case DockerContainer::OpenVpn:
case DockerContainer::WireGuard:
case DockerContainer::Awg:
case DockerContainer::Xray:
protocol = ContainerProps::defaultProtocol(container);
break;
default:
return ErrorCode::NoError;
}
auto protocolConfig = ContainerProps::getProtocolConfigFromContainer(protocol, containerConfig);
return appendClient(protocolConfig, clientName, container, credentials, serverController);
}
return appendClient(protocolConfig.value(config_key::clientId).toString(), clientName, container, credentials, serverController);
ErrorCode ClientManagementModel::appendClient(QJsonObject &protocolConfig, const QString &clientName, const DockerContainer container,
const ServerCredentials &credentials, const QSharedPointer<ServerController> &serverController)
{
QString clientId;
if (container == DockerContainer::Xray) {
if (!protocolConfig.contains("outbounds")) {
return ErrorCode::InternalError;
}
QJsonArray outbounds = protocolConfig.value("outbounds").toArray();
if (outbounds.isEmpty()) {
return ErrorCode::InternalError;
}
QJsonObject outbound = outbounds[0].toObject();
if (!outbound.contains("settings")) {
return ErrorCode::InternalError;
}
QJsonObject settings = outbound["settings"].toObject();
if (!settings.contains("vnext")) {
return ErrorCode::InternalError;
}
QJsonArray vnext = settings["vnext"].toArray();
if (vnext.isEmpty()) {
return ErrorCode::InternalError;
}
QJsonObject vnextObj = vnext[0].toObject();
if (!vnextObj.contains("users")) {
return ErrorCode::InternalError;
}
QJsonArray users = vnextObj["users"].toArray();
if (users.isEmpty()) {
return ErrorCode::InternalError;
}
QJsonObject user = users[0].toObject();
if (!user.contains("id")) {
return ErrorCode::InternalError;
}
clientId = user["id"].toString();
} else {
clientId = protocolConfig.value(config_key::clientId).toString();
}
return appendClient(clientId, clientName, container, credentials, serverController);
}
ErrorCode ClientManagementModel::appendClient(const QString &clientId, const QString &clientName, const DockerContainer container,
@@ -421,10 +536,27 @@ ErrorCode ClientManagementModel::revokeClient(const int row, const DockerContain
auto client = m_clientsTable.at(row).toObject();
QString clientId = client.value(configKey::clientId).toString();
if (container == DockerContainer::OpenVpn || container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) {
errorCode = revokeOpenVpn(row, container, credentials, serverIndex, serverController);
} else if (container == DockerContainer::WireGuard || container == DockerContainer::Awg) {
errorCode = revokeWireGuard(row, container, credentials, serverController);
switch(container)
{
case DockerContainer::OpenVpn:
case DockerContainer::ShadowSocks:
case DockerContainer::Cloak: {
errorCode = revokeOpenVpn(row, container, credentials, serverIndex, serverController);
break;
}
case DockerContainer::WireGuard:
case DockerContainer::Awg: {
errorCode = revokeWireGuard(row, container, credentials, serverController);
break;
}
case DockerContainer::Xray: {
errorCode = revokeXray(row, container, credentials, serverController);
break;
}
default: {
logger.error() << "Internal error: received unexpected container type";
return ErrorCode::InternalError;
}
}
if (errorCode == ErrorCode::NoError) {
@@ -462,19 +594,69 @@ ErrorCode ClientManagementModel::revokeClient(const QJsonObject &containerConfig
}
Proto protocol;
if (container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) {
protocol = Proto::OpenVpn;
} else if (container == DockerContainer::OpenVpn || container == DockerContainer::WireGuard || container == DockerContainer::Awg) {
protocol = ContainerProps::defaultProtocol(container);
} else {
return ErrorCode::NoError;
switch(container)
{
case DockerContainer::ShadowSocks:
case DockerContainer::Cloak: {
protocol = Proto::OpenVpn;
break;
}
case DockerContainer::OpenVpn:
case DockerContainer::WireGuard:
case DockerContainer::Awg:
case DockerContainer::Xray: {
protocol = ContainerProps::defaultProtocol(container);
break;
}
default: {
logger.error() << "Internal error: received unexpected container type";
return ErrorCode::InternalError;
}
}
auto protocolConfig = ContainerProps::getProtocolConfigFromContainer(protocol, containerConfig);
QString clientId;
if (container == DockerContainer::Xray) {
if (!protocolConfig.contains("outbounds")) {
return ErrorCode::InternalError;
}
QJsonArray outbounds = protocolConfig.value("outbounds").toArray();
if (outbounds.isEmpty()) {
return ErrorCode::InternalError;
}
QJsonObject outbound = outbounds[0].toObject();
if (!outbound.contains("settings")) {
return ErrorCode::InternalError;
}
QJsonObject settings = outbound["settings"].toObject();
if (!settings.contains("vnext")) {
return ErrorCode::InternalError;
}
QJsonArray vnext = settings["vnext"].toArray();
if (vnext.isEmpty()) {
return ErrorCode::InternalError;
}
QJsonObject vnextObj = vnext[0].toObject();
if (!vnextObj.contains("users")) {
return ErrorCode::InternalError;
}
QJsonArray users = vnextObj["users"].toArray();
if (users.isEmpty()) {
return ErrorCode::InternalError;
}
QJsonObject user = users[0].toObject();
if (!user.contains("id")) {
return ErrorCode::InternalError;
}
clientId = user["id"].toString();
} else {
clientId = protocolConfig.value(config_key::clientId).toString();
}
int row;
bool clientExists = false;
QString clientId = protocolConfig.value(config_key::clientId).toString();
for (row = 0; row < rowCount(); row++) {
auto client = m_clientsTable.at(row).toObject();
if (clientId == client.value(configKey::clientId).toString()) {
@@ -486,11 +668,28 @@ ErrorCode ClientManagementModel::revokeClient(const QJsonObject &containerConfig
return errorCode;
}
if (container == DockerContainer::OpenVpn || container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) {
switch (container)
{
case DockerContainer::OpenVpn:
case DockerContainer::ShadowSocks:
case DockerContainer::Cloak: {
errorCode = revokeOpenVpn(row, container, credentials, serverIndex, serverController);
} else if (container == DockerContainer::WireGuard || container == DockerContainer::Awg) {
errorCode = revokeWireGuard(row, container, credentials, serverController);
break;
}
case DockerContainer::WireGuard:
case DockerContainer::Awg: {
errorCode = revokeWireGuard(row, container, credentials, serverController);
break;
}
case DockerContainer::Xray: {
errorCode = revokeXray(row, container, credentials, serverController);
break;
}
default:
logger.error() << "Internal error: received unexpected container type";
return ErrorCode::InternalError;
}
return errorCode;
}
@@ -593,6 +792,117 @@ ErrorCode ClientManagementModel::revokeWireGuard(const int row, const DockerCont
return ErrorCode::NoError;
}
ErrorCode ClientManagementModel::revokeXray(const int row,
const DockerContainer container,
const ServerCredentials &credentials,
const QSharedPointer<ServerController> &serverController)
{
ErrorCode error = ErrorCode::NoError;
// Get server config
const QString serverConfigPath = amnezia::protocols::xray::serverConfigPath;
const QString configString = serverController->getTextFileFromContainer(container, credentials, serverConfigPath, error);
if (error != ErrorCode::NoError) {
logger.error() << "Failed to get the xray server config file";
return error;
}
QJsonDocument serverConfig = QJsonDocument::fromJson(configString.toUtf8());
if (serverConfig.isNull()) {
logger.error() << "Failed to parse xray server config JSON";
return ErrorCode::InternalError;
}
// Get client ID to remove
auto client = m_clientsTable.at(row).toObject();
QString clientId = client.value(configKey::clientId).toString();
// Remove client from server config
QJsonObject configObj = serverConfig.object();
if (!configObj.contains("inbounds")) {
logger.error() << "Missing inbounds in xray config";
return ErrorCode::InternalError;
}
QJsonArray inbounds = configObj["inbounds"].toArray();
if (inbounds.isEmpty()) {
logger.error() << "Empty inbounds array in xray config";
return ErrorCode::InternalError;
}
QJsonObject inbound = inbounds[0].toObject();
if (!inbound.contains("settings")) {
logger.error() << "Missing settings in xray inbound config";
return ErrorCode::InternalError;
}
QJsonObject settings = inbound["settings"].toObject();
if (!settings.contains("clients")) {
logger.error() << "Missing clients in xray settings";
return ErrorCode::InternalError;
}
QJsonArray clients = settings["clients"].toArray();
if (clients.isEmpty()) {
logger.error() << "Empty clients array in xray config";
return ErrorCode::InternalError;
}
for (int i = 0; i < clients.size(); ++i) {
QJsonObject clientObj = clients[i].toObject();
if (clientObj.contains("id") && clientObj["id"].toString() == clientId) {
clients.removeAt(i);
break;
}
}
// Update server config
settings["clients"] = clients;
inbound["settings"] = settings;
inbounds[0] = inbound;
configObj["inbounds"] = inbounds;
// Upload updated config
error = serverController->uploadTextFileToContainer(
container,
credentials,
QJsonDocument(configObj).toJson(),
serverConfigPath
);
if (error != ErrorCode::NoError) {
logger.error() << "Failed to upload updated xray config";
return error;
}
// Remove from local table
beginRemoveRows(QModelIndex(), row, row);
m_clientsTable.removeAt(row);
endRemoveRows();
// Update clients table file on server
const QByteArray clientsTableString = QJsonDocument(m_clientsTable).toJson();
QString clientsTableFile = QString("/opt/amnezia/%1/clientsTable")
.arg(ContainerProps::containerTypeToString(container));
error = serverController->uploadTextFileToContainer(container, credentials, clientsTableString, clientsTableFile);
if (error != ErrorCode::NoError) {
logger.error() << "Failed to upload the clientsTable file";
}
// Restart container
QString restartScript = QString("sudo docker restart $CONTAINER_NAME");
error = serverController->runScript(
credentials,
serverController->replaceVars(restartScript, serverController->genVarsForScript(credentials, container))
);
if (error != ErrorCode::NoError) {
logger.error() << "Failed to restart xray container";
return error;
}
return error;
}
QHash<int, QByteArray> ClientManagementModel::roleNames() const
{
QHash<int, QByteArray> roles;
@@ -601,5 +911,6 @@ QHash<int, QByteArray> ClientManagementModel::roleNames() const
roles[LatestHandshakeRole] = "latestHandshake";
roles[DataReceivedRole] = "dataReceived";
roles[DataSentRole] = "dataSent";
roles[AllowedIpsRole] = "allowedIps";
return roles;
}
}
+6
View File
@@ -40,6 +40,8 @@ public slots:
const QSharedPointer<ServerController> &serverController);
ErrorCode appendClient(const DockerContainer container, const ServerCredentials &credentials, const QJsonObject &containerConfig,
const QString &clientName, const QSharedPointer<ServerController> &serverController);
ErrorCode appendClient(QJsonObject &protocolConfig, const QString &clientName,const DockerContainer container,
const ServerCredentials &credentials, const QSharedPointer<ServerController> &serverController);
ErrorCode appendClient(const QString &clientId, const QString &clientName, const DockerContainer container,
const ServerCredentials &credentials, const QSharedPointer<ServerController> &serverController);
ErrorCode renameClient(const int row, const QString &userName, const DockerContainer container, const ServerCredentials &credentials,
@@ -64,11 +66,15 @@ private:
const QSharedPointer<ServerController> &serverController);
ErrorCode revokeWireGuard(const int row, const DockerContainer container, const ServerCredentials &credentials,
const QSharedPointer<ServerController> &serverController);
ErrorCode revokeXray(const int row, const DockerContainer container, const ServerCredentials &credentials,
const QSharedPointer<ServerController> &serverController);
ErrorCode getOpenVpnClients(const DockerContainer container, const ServerCredentials &credentials,
const QSharedPointer<ServerController> &serverController, int &count);
ErrorCode getWireGuardClients(const DockerContainer container, const ServerCredentials &credentials,
const QSharedPointer<ServerController> &serverController, int &count);
ErrorCode getXrayClients(const DockerContainer container, const ServerCredentials& credentials,
const QSharedPointer<ServerController> &serverController, int &count);
ErrorCode wgShow(const DockerContainer container, const ServerCredentials &credentials,
const QSharedPointer<ServerController> &serverController, std::vector<WgShowData> &data);
@@ -21,6 +21,7 @@ bool AwgConfigModel::setData(const QModelIndex &index, const QVariant &value, in
}
switch (role) {
case Roles::SubnetAddressRole: m_serverProtocolConfig.insert(config_key::subnet_address, value.toString()); break;
case Roles::PortRole: m_serverProtocolConfig.insert(config_key::port, value.toString()); break;
case Roles::ClientMtuRole: m_clientProtocolConfig.insert(config_key::mtu, value.toString()); break;
@@ -58,6 +59,7 @@ QVariant AwgConfigModel::data(const QModelIndex &index, int role) const
}
switch (role) {
case Roles::SubnetAddressRole: return m_serverProtocolConfig.value(config_key::subnet_address).toString();
case Roles::PortRole: return m_serverProtocolConfig.value(config_key::port).toString();
case Roles::ClientMtuRole: return m_clientProtocolConfig.value(config_key::mtu);
@@ -92,6 +94,7 @@ void AwgConfigModel::updateModel(const QJsonObject &config)
m_serverProtocolConfig.insert(config_key::transport_proto,
serverProtocolConfig.value(config_key::transport_proto).toString(defaultTransportProto));
m_serverProtocolConfig[config_key::last_config] = serverProtocolConfig.value(config_key::last_config);
m_serverProtocolConfig[config_key::subnet_address] = serverProtocolConfig.value(config_key::subnet_address).toString(protocols::wireguard::defaultSubnetAddress);
m_serverProtocolConfig[config_key::port] = serverProtocolConfig.value(config_key::port).toString(protocols::awg::defaultPort);
m_serverProtocolConfig[config_key::junkPacketCount] =
serverProtocolConfig.value(config_key::junkPacketCount).toString(protocols::awg::defaultJunkPacketCount);
@@ -168,6 +171,7 @@ QHash<int, QByteArray> AwgConfigModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[SubnetAddressRole] = "subnetAddress";
roles[PortRole] = "port";
roles[ClientMtuRole] = "clientMtu";
@@ -197,6 +201,7 @@ AwgConfig::AwgConfig(const QJsonObject &serverProtocolConfig)
clientJunkPacketMinSize = clientProtocolConfig.value(config_key::junkPacketMinSize).toString(protocols::awg::defaultJunkPacketMinSize);
clientJunkPacketMaxSize = clientProtocolConfig.value(config_key::junkPacketMaxSize).toString(protocols::awg::defaultJunkPacketMaxSize);
subnetAddress = serverProtocolConfig.value(config_key::subnet_address).toString(protocols::wireguard::defaultSubnetAddress);
port = serverProtocolConfig.value(config_key::port).toString(protocols::awg::defaultPort);
serverJunkPacketCount = serverProtocolConfig.value(config_key::junkPacketCount).toString(protocols::awg::defaultJunkPacketCount);
serverJunkPacketMinSize = serverProtocolConfig.value(config_key::junkPacketMinSize).toString(protocols::awg::defaultJunkPacketMinSize);
@@ -216,7 +221,7 @@ AwgConfig::AwgConfig(const QJsonObject &serverProtocolConfig)
bool AwgConfig::hasEqualServerSettings(const AwgConfig &other) const
{
if (port != other.port || serverJunkPacketCount != other.serverJunkPacketCount
if (subnetAddress != other.subnetAddress || port != other.port || serverJunkPacketCount != other.serverJunkPacketCount
|| serverJunkPacketMinSize != other.serverJunkPacketMinSize || serverJunkPacketMaxSize != other.serverJunkPacketMaxSize
|| serverInitPacketJunkSize != other.serverInitPacketJunkSize || serverResponsePacketJunkSize != other.serverResponsePacketJunkSize
|| serverInitPacketMagicHeader != other.serverInitPacketMagicHeader
+3 -1
View File
@@ -15,6 +15,7 @@ struct AwgConfig
{
AwgConfig(const QJsonObject &jsonConfig);
QString subnetAddress;
QString port;
QString clientMtu;
@@ -43,7 +44,8 @@ class AwgConfigModel : public QAbstractListModel
public:
enum Roles {
PortRole = Qt::UserRole + 1,
SubnetAddressRole = Qt::UserRole + 1,
PortRole,
ClientMtuRole,
ClientJunkPacketCountRole,
@@ -21,6 +21,7 @@ bool WireGuardConfigModel::setData(const QModelIndex &index, const QVariant &val
}
switch (role) {
case Roles::SubnetAddressRole: m_serverProtocolConfig.insert(config_key::subnet_address, value.toString()); break;
case Roles::PortRole: m_serverProtocolConfig.insert(config_key::port, value.toString()); break;
case Roles::ClientMtuRole: m_clientProtocolConfig.insert(config_key::mtu, value.toString()); break;
}
@@ -36,6 +37,7 @@ QVariant WireGuardConfigModel::data(const QModelIndex &index, int role) const
}
switch (role) {
case Roles::SubnetAddressRole: return m_serverProtocolConfig.value(config_key::subnet_address).toString();
case Roles::PortRole: return m_serverProtocolConfig.value(config_key::port).toString();
case Roles::ClientMtuRole: return m_clientProtocolConfig.value(config_key::mtu);
}
@@ -56,6 +58,7 @@ void WireGuardConfigModel::updateModel(const QJsonObject &config)
m_serverProtocolConfig.insert(config_key::transport_proto,
serverProtocolConfig.value(config_key::transport_proto).toString(defaultTransportProto));
m_serverProtocolConfig[config_key::last_config] = serverProtocolConfig.value(config_key::last_config);
m_serverProtocolConfig[config_key::subnet_address] = serverProtocolConfig.value(config_key::subnet_address).toString(protocols::wireguard::defaultSubnetAddress);
m_serverProtocolConfig[config_key::port] = serverProtocolConfig.value(config_key::port).toString(protocols::wireguard::defaultPort);
auto lastConfig = m_serverProtocolConfig.value(config_key::last_config).toString();
@@ -96,6 +99,7 @@ QHash<int, QByteArray> WireGuardConfigModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[SubnetAddressRole] = "subnetAddress";
roles[PortRole] = "port";
roles[ClientMtuRole] = "clientMtu";
@@ -108,12 +112,13 @@ WgConfig::WgConfig(const QJsonObject &serverProtocolConfig)
QJsonObject clientProtocolConfig = QJsonDocument::fromJson(lastConfig.toUtf8()).object();
clientMtu = clientProtocolConfig[config_key::mtu].toString(protocols::wireguard::defaultMtu);
subnetAddress = serverProtocolConfig.value(config_key::subnet_address).toString(protocols::wireguard::defaultSubnetAddress);
port = serverProtocolConfig.value(config_key::port).toString(protocols::wireguard::defaultPort);
}
bool WgConfig::hasEqualServerSettings(const WgConfig &other) const
{
if (port != other.port) {
if (subnetAddress != other.subnetAddress || port != other.port) {
return false;
}
return true;
@@ -10,6 +10,7 @@ struct WgConfig
{
WgConfig(const QJsonObject &jsonConfig);
QString subnetAddress;
QString port;
QString clientMtu;
@@ -24,7 +25,8 @@ class WireGuardConfigModel : public QAbstractListModel
public:
enum Roles {
PortRole = Qt::UserRole + 1,
SubnetAddressRole = Qt::UserRole + 1,
PortRole,
ClientMtuRole
};
+28 -7
View File
@@ -22,7 +22,7 @@ namespace
constexpr char serviceProtocol[] = "service_protocol";
constexpr char publicKeyInfo[] = "public_key";
constexpr char endDate[] = "end_date";
constexpr char expiresAt[] = "expires_at";
}
}
@@ -39,6 +39,9 @@ ServersModel::ServersModel(std::shared_ptr<Settings> settings, QObject *parent)
emit ServersModel::defaultServerNameChanged();
updateDefaultServerContainersModel();
});
connect(this, &ServersModel::processedServerIndexChanged, this, &ServersModel::processedServerChanged);
connect(this, &ServersModel::dataChanged, this, &ServersModel::processedServerChanged);
}
int ServersModel::rowCount(const QModelIndex &parent) const
@@ -79,6 +82,12 @@ bool ServersModel::setData(const QModelIndex &index, const QVariant &value, int
return true;
}
bool ServersModel::setData(const int index, const QVariant &value, int role)
{
QModelIndex modelIndex = this->index(index);
return setData(modelIndex, value, role);
}
QVariant ServersModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() < 0 || index.row() >= static_cast<int>(m_servers.size())) {
@@ -679,6 +688,18 @@ QVariant ServersModel::getProcessedServerData(const QString roleString)
return {};
}
bool ServersModel::setProcessedServerData(const QString &roleString, const QVariant &value)
{
const auto roles = roleNames();
for (auto it = roles.begin(); it != roles.end(); it++) {
if (QString(it.value()) == roleString) {
return setData(m_processedServerIndex, value, it.key());
}
}
return false;
}
bool ServersModel::isDefaultServerDefaultContainerHasSplitTunneling()
{
auto server = m_servers.at(m_defaultServerIndex).toObject();
@@ -718,9 +739,9 @@ bool ServersModel::isApiKeyExpired(const int serverIndex)
auto apiConfig = serverConfig.value(configKey::apiConfig).toObject();
auto publicKeyInfo = apiConfig.value(configKey::publicKeyInfo).toObject();
const QString endDate = publicKeyInfo.value(configKey::endDate).toString();
if (endDate.isEmpty()) {
publicKeyInfo.insert(configKey::endDate, QDateTime::currentDateTimeUtc().addDays(1).toString(Qt::ISODate));
const QString expiresAt = publicKeyInfo.value(configKey::expiresAt).toString();
if (expiresAt.isEmpty()) {
publicKeyInfo.insert(configKey::expiresAt, QDateTime::currentDateTimeUtc().addDays(1).toString(Qt::ISODate));
apiConfig.insert(configKey::publicKeyInfo, publicKeyInfo);
serverConfig.insert(configKey::apiConfig, apiConfig);
editServer(serverConfig, serverIndex);
@@ -728,8 +749,8 @@ bool ServersModel::isApiKeyExpired(const int serverIndex)
return false;
}
auto endDateDateTime = QDateTime::fromString(endDate, Qt::ISODate).toUTC();
if (endDateDateTime < QDateTime::currentDateTimeUtc()) {
auto expiresAtDateTime = QDateTime::fromString(expiresAt, Qt::ISODate).toUTC();
if (expiresAtDateTime < QDateTime::currentDateTimeUtc()) {
return true;
}
return false;
@@ -771,5 +792,5 @@ const QString ServersModel::getDefaultServerImagePathCollapsed()
if (countryCode.isEmpty()) {
return "";
}
return QString("qrc:/countriesFlags/images/flagKit/%1.svg").arg(countryCode);
return QString("qrc:/countriesFlags/images/flagKit/%1.svg").arg(countryCode.toUpper());
}
+5
View File
@@ -46,6 +46,7 @@ public:
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
bool setData(const int index, const QVariant &value, int role = Qt::EditRole);
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QVariant data(const int index, int role = Qt::DisplayRole) const;
@@ -115,6 +116,7 @@ public slots:
QVariant getDefaultServerData(const QString roleString);
QVariant getProcessedServerData(const QString roleString);
bool setProcessedServerData(const QString &roleString, const QVariant &value);
bool isDefaultServerDefaultContainerHasSplitTunneling();
@@ -127,6 +129,9 @@ protected:
signals:
void processedServerIndexChanged(const int index);
// emitted when the processed server index or processed server data is changed
void processedServerChanged();
void defaultServerIndexChanged(const int index);
void defaultServerNameChanged();
void defaultServerDescriptionChanged();
+72
View File
@@ -0,0 +1,72 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Shapes
import Qt5Compat.GraphicalEffects
import Style 1.0
import "../Config"
import "../Controls2"
import "../Controls2/TextTypes"
Rectangle {
id: root
property real contentHeight: ad.implicitHeight + ad.anchors.topMargin + ad.anchors.bottomMargin
border.width: 1
border.color: AmneziaStyle.color.goldenApricot
color: AmneziaStyle.color.transparent
radius: 13
visible: GC.isDesktop() && ServersModel.isDefaultServerFromApi
&& ServersModel.isDefaultServerDefaultContainerHasSplitTunneling && SettingsController.isHomeAdLabelVisible
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: function() {
Qt.openUrlExternally(LanguageModel.getCurrentSiteUrl() + "/premium")
}
}
RowLayout {
id: ad
anchors.fill: parent
anchors.margins: 16
Image {
source: "qrc:/images/controls/amnezia.svg"
sourceSize: Qt.size(36, 36)
layer {
effect: ColorOverlay {
color: AmneziaStyle.color.paleGray
}
}
}
CaptionTextType {
Layout.fillWidth: true
Layout.rightMargin: 10
Layout.leftMargin: 10
text: qsTr("Amnezia Premium - for access to any website")
color: AmneziaStyle.color.pearlGray
lineHeight: 18
font.pixelSize: 15
}
ImageButtonType {
image: "qrc:/images/controls/close.svg"
imageColor: AmneziaStyle.color.paleGray
onClicked: function() {
SettingsController.disableHomeAdLabel()
}
}
}
}
@@ -16,6 +16,32 @@ Button {
property string connectedButtonColor: AmneziaStyle.color.goldenApricot
property bool buttonActiveFocus: activeFocus && (Qt.platform.os !== "android" || SettingsController.isOnTv())
property bool isFocusable: true
Keys.onTabPressed: {
FocusController.nextKeyTabItem()
}
Keys.onBacktabPressed: {
FocusController.previousKeyTabItem()
}
Keys.onUpPressed: {
FocusController.nextKeyUpItem()
}
Keys.onDownPressed: {
FocusController.nextKeyDownItem()
}
Keys.onLeftPressed: {
FocusController.nextKeyLeftItem()
}
Keys.onRightPressed: {
FocusController.nextKeyRightItem()
}
implicitWidth: 190
implicitHeight: 190
@@ -14,7 +14,7 @@ DrawerType2 {
width: parent.width
height: parent.height
expandedContent: ColumnLayout {
expandedStateContent: ColumnLayout {
id: content
anchors.top: parent.top
@@ -26,14 +26,6 @@ DrawerType2 {
root.expandedHeight = content.implicitHeight + 32
}
Connections {
target: root
enabled: !GC.isMobile()
function onOpened() {
focusItem.forceActiveFocus()
}
}
Header2Type {
Layout.fillWidth: true
Layout.topMargin: 24
@@ -44,11 +36,6 @@ DrawerType2 {
headerText: qsTr("Add new connection")
}
Item {
id: focusItem
KeyNavigation.tab: ip.rightButton
}
LabelWithButtonType {
id: ip
Layout.fillWidth: true
@@ -59,10 +46,8 @@ DrawerType2 {
clickedFunction: function() {
PageController.goToPage(PageEnum.PageSetupWizardCredentials)
root.close()
root.closeTriggered()
}
KeyNavigation.tab: qrCode.rightButton
}
DividerType {}
@@ -76,10 +61,8 @@ DrawerType2 {
clickedFunction: function() {
PageController.goToPage(PageEnum.PageSetupWizardConfigSource)
root.close()
root.closeTriggered()
}
KeyNavigation.tab: focusItem
}
DividerType {}
@@ -17,55 +17,15 @@ ListView {
property var rootWidth
property var selectedText
property bool a: true
width: rootWidth
height: menuContent.contentItem.height
height: contentItem.height
clip: true
interactive: false
snapMode: ListView.SnapToItem
property FlickableType parentFlickable
property var lastItemTabClicked
ScrollBar.vertical: ScrollBarType {}
property int currentFocusIndex: 0
activeFocusOnTab: true
onActiveFocusChanged: {
if (activeFocus) {
this.currentFocusIndex = 0
this.itemAtIndex(currentFocusIndex).forceActiveFocus()
}
}
Keys.onTabPressed: {
if (currentFocusIndex < this.count - 1) {
currentFocusIndex += 1
this.itemAtIndex(currentFocusIndex).forceActiveFocus()
} else {
currentFocusIndex = 0
if (lastItemTabClicked && typeof lastItemTabClicked === "function") {
lastItemTabClicked()
}
}
}
onVisibleChanged: {
if (visible) {
currentFocusIndex = 0
focusItem.forceActiveFocus()
}
}
Item {
id: focusItem
}
onCurrentFocusIndexChanged: {
if (parentFlickable) {
parentFlickable.ensureVisible(this.itemAtIndex(currentFocusIndex))
}
}
property bool isFocusable: true
ButtonGroup {
id: containersRadioButtonGroup
@@ -75,12 +35,6 @@ ListView {
implicitWidth: rootWidth
implicitHeight: content.implicitHeight
onActiveFocusChanged: {
if (activeFocus) {
containerRadioButton.forceActiveFocus()
}
}
ColumnLayout {
id: content
@@ -111,13 +65,13 @@ ListView {
}
if (checked) {
containersDropDown.close()
containersDropDown.closeTriggered()
ServersModel.setDefaultContainer(ServersModel.defaultIndex, proxyDefaultServerContainersModel.mapToSource(index))
} else {
ContainersModel.setProcessedContainerIndex(proxyDefaultServerContainersModel.mapToSource(index))
InstallController.setShouldCreateServer(false)
PageController.goToPage(PageEnum.PageSetupWizardProtocolSettings)
containersDropDown.close()
containersDropDown.closeTriggered()
}
}
@@ -16,7 +16,7 @@ DrawerType2 {
anchors.fill: parent
expandedHeight: parent.height * 0.9
expandedContent: ColumnLayout {
expandedStateContent: ColumnLayout {
id: content
anchors.top: parent.top
@@ -24,14 +24,6 @@ DrawerType2 {
anchors.right: parent.right
spacing: 0
Connections {
target: root
enabled: !GC.isMobile()
function onOpened() {
focusItem.forceActiveFocus()
}
}
Header2Type {
Layout.fillWidth: true
Layout.topMargin: 24
@@ -43,11 +35,6 @@ DrawerType2 {
descriptionText: qsTr("Allows you to connect to some sites or applications through a VPN connection and bypass others")
}
Item {
id: focusItem
KeyNavigation.tab: splitTunnelingSwitch.visible ? splitTunnelingSwitch : siteBasedSplitTunnelingSwitch.rightButton
}
LabelWithButtonType {
id: splitTunnelingSwitch
Layout.fillWidth: true
@@ -59,11 +46,9 @@ DrawerType2 {
descriptionText: qsTr("Enabled \nCan't be disabled for current server")
rightImageSource: "qrc:/images/controls/chevron-right.svg"
KeyNavigation.tab: siteBasedSplitTunnelingSwitch.visible ? siteBasedSplitTunnelingSwitch.rightButton : focusItem
clickedFunction: function() {
// PageController.goToPage(PageEnum.PageSettingsSplitTunneling)
// root.close()
PageController.goToPage(PageEnum.PageSettingsSplitTunneling)
root.closeTriggered()
}
}
@@ -80,13 +65,9 @@ DrawerType2 {
descriptionText: enabled && SitesModel.isTunnelingEnabled ? qsTr("Enabled") : qsTr("Disabled")
rightImageSource: "qrc:/images/controls/chevron-right.svg"
KeyNavigation.tab: appSplitTunnelingSwitch.visible ?
appSplitTunnelingSwitch.rightButton :
focusItem
clickedFunction: function() {
PageController.goToPage(PageEnum.PageSettingsSplitTunneling)
root.close()
root.closeTriggered()
}
}
@@ -103,11 +84,9 @@ DrawerType2 {
descriptionText: AppSplitTunnelingModel.isTunnelingEnabled ? qsTr("Enabled") : qsTr("Disabled")
rightImageSource: "qrc:/images/controls/chevron-right.svg"
KeyNavigation.tab: focusItem
clickedFunction: function() {
PageController.goToPage(PageEnum.PageSettingsAppSplitTunneling)
root.close()
root.closeTriggered()
}
}
@@ -26,7 +26,7 @@ DrawerType2 {
id: installedAppsModel
}
expandedContent: Item {
expandedStateContent: Item {
id: container
implicitHeight: expandedHeight
@@ -43,7 +43,7 @@ DrawerType2 {
BackButtonType {
backButtonImage: "qrc:/images/controls/arrow-left.svg"
backButtonFunction: function() {
root.close()
root.closeTriggered()
}
}
@@ -69,6 +69,8 @@ DrawerType2 {
clip: true
interactive: true
property bool isFocusable: true
model: SortFilterProxyModel {
id: proxyInstalledAppsModel
sourceModel: installedAppsModel
@@ -79,10 +81,7 @@ DrawerType2 {
}
}
ScrollBar.vertical: ScrollBar {
id: scrollBar
policy: ScrollBar.AlwaysOn
}
ScrollBar.vertical: ScrollBarType {}
ButtonGroup {
id: buttonGroup
@@ -155,7 +154,7 @@ DrawerType2 {
PageController.showBusyIndicator(true)
AppSplitTunnelingController.addApps(installedAppsModel.getSelectedAppsInfo())
PageController.showBusyIndicator(false)
root.close()
root.closeTriggered()
}
}
}
+1 -18
View File
@@ -20,7 +20,7 @@ DrawerType2 {
property var yesButtonFunction
property var noButtonFunction
expandedContent: ColumnLayout {
expandedStateContent: ColumnLayout {
id: content
anchors.top: parent.top
@@ -33,14 +33,6 @@ DrawerType2 {
root.expandedHeight = content.implicitHeight + 32
}
Connections {
target: root
enabled: !GC.isMobile()
function onOpened() {
focusItem.forceActiveFocus()
}
}
Header2TextType {
Layout.fillWidth: true
Layout.topMargin: 16
@@ -59,11 +51,6 @@ DrawerType2 {
text: descriptionText
}
Item {
id: focusItem
KeyNavigation.tab: yesButton
}
BasicButtonType {
id: yesButton
Layout.fillWidth: true
@@ -78,8 +65,6 @@ DrawerType2 {
yesButtonFunction()
}
}
KeyNavigation.tab: noButton
}
BasicButtonType {
@@ -102,8 +87,6 @@ DrawerType2 {
noButtonFunction()
}
}
KeyNavigation.tab: focusItem
}
}
}
+99 -131
View File
@@ -11,7 +11,7 @@ import "../Config"
DrawerType2 {
id: root
expandedContent: Item {
expandedStateContent: Item {
id: container
implicitHeight: root.height * 0.9
@@ -20,19 +20,6 @@ DrawerType2 {
root.expandedHeight = container.implicitHeight
}
Connections {
target: root
enabled: !GC.isMobile()
function onOpened() {
focusItem.forceActiveFocus()
}
}
Item {
id: focusItem
KeyNavigation.tab: backButton
}
ColumnLayout {
id: backButtonLayout
@@ -43,167 +30,148 @@ DrawerType2 {
BackButtonType {
id: backButton
Layout.fillWidth: true
backButtonImage: "qrc:/images/controls/arrow-left.svg"
backButtonFunction: function() { root.close() }
KeyNavigation.tab: listView
backButtonFunction: function() { root.closeTriggered() }
}
Header2Type {
id: header
Layout.fillWidth: true
Layout.topMargin: 16
Layout.rightMargin: 16
Layout.leftMargin: 16
headerText: qsTr("Choose language")
}
}
FlickableType {
ListView {
id: listView
anchors.top: backButtonLayout.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
contentHeight: content.implicitHeight
ColumnLayout {
id: content
property bool isFocusable: true
property int selectedIndex: LanguageModel.currentLanguageIndex
anchors.fill: parent
clip: true
reuseItems: true
Header2Type {
id: header
Layout.fillWidth: true
Layout.topMargin: 16
Layout.rightMargin: 16
Layout.leftMargin: 16
ScrollBar.vertical: ScrollBarType {}
headerText: qsTr("Choose language")
}
model: LanguageModel
ListView {
id: listView
ButtonGroup {
id: buttonGroup
}
Layout.fillWidth: true
height: listView.contentItem.height
delegate: Item {
implicitWidth: root.width
implicitHeight: delegateContent.implicitHeight
clip: true
interactive: false
ColumnLayout {
id: delegateContent
model: LanguageModel
currentIndex: LanguageModel.currentLanguageIndex
anchors.fill: parent
ButtonGroup {
id: buttonGroup
}
RadioButton {
id: radioButton
property int currentFocusIndex: 0
implicitWidth: parent.width
implicitHeight: radioButtonContent.implicitHeight
activeFocusOnTab: true
onActiveFocusChanged: {
if (activeFocus) {
this.currentFocusIndex = 0
this.itemAtIndex(currentFocusIndex).forceActiveFocus()
}
}
hoverEnabled: true
Keys.onTabPressed: {
if (currentFocusIndex < this.count - 1) {
currentFocusIndex += 1
this.itemAtIndex(currentFocusIndex).forceActiveFocus()
} else {
listViewFocusItem.forceActiveFocus()
focusItem.forceActiveFocus()
}
}
property bool isFocusable: true
Item {
id: listViewFocusItem
Keys.onTabPressed: {
root.forceActiveFocus()
FocusController.nextKeyTabItem()
}
}
onVisibleChanged: {
if (visible) {
listViewFocusItem.forceActiveFocus()
focusItem.forceActiveFocus()
Keys.onBacktabPressed: {
FocusController.previousKeyTabItem()
}
}
delegate: Item {
implicitWidth: root.width
implicitHeight: delegateContent.implicitHeight
Keys.onUpPressed: {
FocusController.nextKeyUpItem()
}
onActiveFocusChanged: {
if (activeFocus) {
radioButton.forceActiveFocus()
Keys.onDownPressed: {
FocusController.nextKeyDownItem()
}
Keys.onLeftPressed: {
FocusController.nextKeyLeftItem()
}
Keys.onRightPressed: {
FocusController.nextKeyRightItem()
}
indicator: Rectangle {
width: parent.width - 1
height: parent.height
color: radioButton.hovered ? AmneziaStyle.color.slateGray : AmneziaStyle.color.onyxBlack
border.color: radioButton.focus ? AmneziaStyle.color.paleGray : AmneziaStyle.color.transparent
border.width: radioButton.focus ? 1 : 0
Behavior on color {
PropertyAnimation { duration: 200 }
}
Behavior on border.color {
PropertyAnimation { duration: 200 }
}
}
ColumnLayout {
id: delegateContent
RowLayout {
id: radioButtonContent
anchors.fill: parent
RadioButton {
id: radioButton
anchors.rightMargin: 16
anchors.leftMargin: 16
implicitWidth: parent.width
implicitHeight: radioButtonContent.implicitHeight
spacing: 0
hoverEnabled: true
z: 1
indicator: Rectangle {
width: parent.width - 1
height: parent.height
color: radioButton.hovered ? AmneziaStyle.color.slateGray : AmneziaStyle.color.onyxBlack
border.color: radioButton.focus ? AmneziaStyle.color.paleGray : AmneziaStyle.color.transparent
border.width: radioButton.focus ? 1 : 0
ParagraphTextType {
Layout.fillWidth: true
Layout.topMargin: 20
Layout.bottomMargin: 20
Behavior on color {
PropertyAnimation { duration: 200 }
}
Behavior on border.color {
PropertyAnimation { duration: 200 }
}
}
text: languageName
}
RowLayout {
id: radioButtonContent
anchors.fill: parent
Image {
source: "qrc:/images/controls/check.svg"
visible: radioButton.checked
anchors.rightMargin: 16
anchors.leftMargin: 16
width: 24
height: 24
spacing: 0
z: 1
ParagraphTextType {
Layout.fillWidth: true
Layout.topMargin: 20
Layout.bottomMargin: 20
text: languageName
}
Image {
source: "qrc:/images/controls/check.svg"
visible: radioButton.checked
width: 24
height: 24
Layout.rightMargin: 8
}
}
ButtonGroup.group: buttonGroup
checked: listView.currentIndex === index
onClicked: {
listView.currentIndex = index
LanguageModel.changeLanguage(languageIndex)
root.close()
}
Layout.rightMargin: 8
}
}
Keys.onEnterPressed: radioButton.clicked()
Keys.onReturnPressed: radioButton.clicked()
ButtonGroup.group: buttonGroup
checked: listView.selectedIndex === index
onClicked: {
listView.selectedIndex = index
LanguageModel.changeLanguage(languageIndex)
root.closeTriggered()
}
}
}
Keys.onEnterPressed: radioButton.clicked()
Keys.onReturnPressed: radioButton.clicked()
}
}
}

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