Compare commits

...

91 Commits

Author SHA1 Message Date
aiamnezia f7e99d1d5d fix: teardown xray fast fix 2026-06-30 16:37:32 +04:00
aiamnezia 661fca64dd Merge branch 'dev' into feature/local-proxy-integration 2026-06-30 15:37:39 +04:00
vkamn 0f6847219b chore: bump version (#2772) 2026-06-26 13:04:36 +08:00
yp 5d16645b84 fix: android icons (#2725)
* update icons android

* remove comment
2026-06-24 00:08:21 +08:00
Yaroslav Gurov 203a092dc9 fix: blobs for macos-ne and codesigning of qt blobs (#2754) 2026-06-24 00:07:42 +08:00
yp d8b8590bc4 fix: XRay validation audit (#2749)
* flow default and config fixes

* host/SNI/path validation backend + flow-default flag

* input validation, numeric limits and live Save on settings pages

* MinMaxRowType clamping + DropDownType fit-content drawer
2026-06-24 00:07:26 +08:00
yp 9b8bfaa6f8 fix: regression testing vs 4.8.15.4 (#2730)
* fixed revoke

* fixed async update xray/mtproxy/telemt

* fixed connect premium config

* fixed autostart app hide

* fixed clear profile

* (6) fixed xtls-rprx-vision→empty

* (7) fixed appendClient abort & fix restore admin

* (8) fixed async|clientsUpdated

* fixed increment name server N

* remove comment & reset file

* chore: add tr to nextAvailableServerName

---------

Co-authored-by: vkamn <vk@amnezia.org>
2026-06-23 23:05:58 +08:00
Yaroslav Gurov 890103a16a fix: update amneziawg (#2743)
* chore(conan): update amneziawg

* fix(conan): use cmake 4.2+ to support MSVC26

* fix(ci/cd): use the latest cmake generator available on windows
2026-06-17 19:56:53 +07:00
yp 56ab82f87f fix: Use shared OpenSSL on Android (#2736) 2026-06-16 10:57:32 +07:00
lunardunno 3984acbb44 feat: updating install_docker.sh script (#2661)
* Updating install_docker.sh script

Implementing a Docker service status check.
The Docker reinstall step has been removed due to the implementation of Docker service checking.
Implementing locale checking and assignment.
Implementation of execution of some actions through commands with sudo, to reduce delays caused by differences in the values ​​of the PATH variable for the root user and the user included in the sudo group.
Implementation of a verification step for the install containerization app to avoid installing unsupported podman-docker applications.

* adding message handling to install controller

Adding handling for "Containerization app is not supported" and "Service status not active" messages to the controller.

* Error Codes added

Error Codes added for ServerContainerizationNotSupported & DockerServiceNotActive

* Adding extended descriptions of new errors

* fix last line in errorCodes.h

* fix last line in errorStrings.cpp

* Changing the names of errors

* various changes in the script

The messages output for processing by the server controller have been changed: "Container runtime is not supported" and "Container runtime service is not running."
The redundant check and output of the "Packet manager not found" message, as well as the interruption of script execution, have been eliminated, as this situation is handled by the server controller at an earlier stage (check_server_is_busy.sh) and only there.
Added installation of the whish package if it is missing from the OS, for subsequent re-execution of the install_docker.sh and check_server_is_busy.sh scripts.
Implemented an alternative method for detecting the package manager if the whish package is initially missing from the OS.
The algorithm for setting the $pm variable (package manager) has been changed.

* processed phrases have been changed

The phrases processed by the server controller have been changed.

* Attempting to use "command -v"

Switching to using "command -v" instead of "which".

* "which" as main, "command" as backup.

* "which" as main, "command" as backup for check user

* which  LOCK_CMD with sudo

Run the "which" with sudo to check the $LOCK_CMD variable in case the user's PATH variable has incorrect values ​​if the user is not root and is only a member of the sudo group.

* suppressing sudo password prompt

* suppressing sudo password prompt

* suppressing sudo password prompt install_docker.sh

* Changing the phrase for check stdout

"sudo:" with "not found" instead of "command not found"

* Changing phrases for check stdout check_user_in_sudo.sh‎

* sudo|docker and not found, in one line

* check only sudoers
2026-06-15 22:28:38 +07:00
yp cc404378f9 fix: remove only amnezia- prefixed docker volumes (#2728) 2026-06-15 13:12:19 +07:00
yp 594635e5cf fix: script remove docker volume (#2686)
* move sudo docker volume rm -f

* fix: remove unnecessary function

---------

Co-authored-by: vkamn <vk@amnezia.org>
2026-06-04 22:58:39 +08:00
vkamn f9b106cf5b fix: various fixes (#2693)
* fix: fixed country model update

* fix: fixed context menu crush on ios

* fix: fixed passphrase dialog freeze

* fix: fixed country switch

* fix: fixed start minimized

* fix: fixed black screen after remove container

* refactor: return cloak and ss only for view

* fix: fixed default server change after improt while connected

* fix: divider visibility

* fix: fixed revoke admin user

* fix: fixed language restore after backup

* fix: link hover for tor settings page

* fix: fixed openvpn connecntion status

* fix: fixed free color status

* fix: fixed client config update

* chore: bump version
2026-06-04 22:45:53 +08:00
yp a9861d18b7 fix: wrong index on xray pages (#2669)
* test crash xray

* fixed save config xray

* reset file

* fixed text port & reset file

* fixed textFieldWithHeaderType.textField
2026-06-01 12:22:54 +08:00
lunardunno c14138f031 fix: deleting volumes when cleaning the server (#2673)
* Deleting volumes when cleaning the server

* force the remove volumes
2026-06-01 11:54:34 +08:00
yyy-amnezia 60686fde24 fix: link OpenVPNAdapter statically (#2645)
* fix(ios): link OpenVPNAdapter statically

* chore(conan): simplify openvpnadapter merge process and make everything via XCrun

---------

Co-authored-by: Yaroslav Gurov <ygurov@proton.me>
2026-05-30 13:59:35 +08:00
aiamnezia be4df3594b Merge branch 'dev' into feature/local-proxy-integration 2026-05-29 05:44:48 +04:00
aiamnezia 5c8c143833 Merge branch 'dev' into feature/local-proxy-integration 2026-05-29 05:15:31 +04:00
Yaroslav Gurov bd0747296e fix: networkextension proper framework linking (#2668) 2026-05-28 23:09:49 +08:00
Yaroslav Gurov ba61019a50 fix: enable bundled openssl for every platform except NE-based ones (#2660)
* fix: enable bundled openssl for every platform except NE-based ones

* fix(conan): trigger CI/CD on crutial cmake changes

* fix: install dylibs/dlls from conan and use proper RPATH

* fix: adjust windows runtime deps
2026-05-28 19:17:27 +08:00
vkamn 113f967006 fix: various fixes (#2664)
* fix: fixed nextAvailableServerName

* fix: fixed password request for ssh key
2026-05-28 15:14:04 +08:00
yp bcee58b08b feat: add captcha (#2508)
* test capcha

* add test AMNEZIA_GATEWAY_PLAINTEXT_MOCK

* ref

* remove first QNetworkReply::NoError

* fixed macros

* fixed http code

* add test server

* fix cmake

* add CAPTCHA refreshed

* fixed captcha

* update QML Captha

* fixed crash app & up vercion & fix qml captha

* ver 4.9.0.1

* remove m_gatewayCaptchaStickyBase & outEffectiveRequestBase

* reset code PR

* remove mock & temp var AMNEZIA_LOCAL_GATEWAY

* ref code & remove AMNEZIA_LOCAL_GATEWAY

* remove check httpStatusCode & error

* add 408 status code

* fix update captca

* remove fallback на transport

* chore: add loader after captcha solved

* chore: remove logs from api utils

* chore: minor fixes

---------

Co-authored-by: vkamn <vk@amnezia.org>
2026-05-28 13:51:26 +08:00
MrMirDan 52de1acebf refactoring: move tests to separate repo (#2550)
* fix: AUTOMOC and AUTOUIC added

* update: native exports test

* update: export test

* update: vless serialization/deserialization test added

* update: language model and controller test

* update: sites UiController and Model test

* update: sites test

* update: app ui model and controller test

* update: allowed dns ui model and controller test

* update: env vars and removed some lines

* update: news ui model and controller test (incomplete)

* update: api services ui model and controller test (incompleted)

* update: job for tests (Linux)

* update: job for tests (Windows)

* update: proper artifact names

* update: added envs

* update: added 'get sources' and changed steps order

* update: tests jobs remake

* update: 'get sources' step and windows shell

* update: using ctest

* search for exe files

* changed path to run tests

* update: 'Build' step

* update: changed path to deploy qt dependencies

* update: dependencies only for tests executables

* update: ctest dir

* update: include ctest

* update: set dir for tests exe

* update: qt path

* update: serialization test

* update: removed api tests from cmake

* update: changed tests dir

* added ctest to client cmake

* update: installing msvc and additional checks

* removed mcvs install

* update: path to ssh.dll

* fixed issue with ssh path

* update: removed unneccessary step and line

* update: linux job step 'Run tests'

* update: linux 'Install dependencies'

* update: modified qtest include

* update: changed QVERIFY to QVERIFY2

* update: some qverify2 messages

* update: linux additional dependencies

* update: offscreen for linux tests

* update: MacOS tests job

* update: Android tests job

* update: rewrited env's, qtest include and clear clients in some tests

* update: added local vars file for tests

* proper path for some vars

* some fixes due merge

* update: windows tests deploy

* python and conan installation to tests jobs

* chore: minor fixes after merge with dev

* chore: move selfhosted admin tests to separate folder

* refactor: some rename

* chore: fixes after merge

* refactor: moved tests to separate repo

* refactor: remove tests from core controller

* chore: add more protected getters to core controller

* chore: add more protected getters

* chore: remove ctest

* chore: return xray model default values

---------

Co-authored-by: vkamn <vk@amnezia.org>
2026-05-28 12:31:27 +08:00
yp 027a12a1df fix: extended VLESS configuration (#2643)
* fixed vless

* fixed default var

* fixed save button

* remove comment

* fix: fixed header link in xray settings page

---------

Co-authored-by: vkamn <vk@amnezia.org>
2026-05-28 12:21:46 +08:00
yp 0a659a2d74 fix: various fixes for MTProxy & Telemt (#2653)
* fix color & fix enabled

* fixed remove base secret

* fix mtproxy/telemt 'base secret'

* fixed button back

* fixed loader

* fixed reload loader

* fixed dd secret

* fixed qml

* fix: fixed header link in mtproxy/telemt page

---------

Co-authored-by: vkamn <vk@amnezia.org>
2026-05-28 11:46:26 +08:00
vkamn 6f119cd083 fix: various fixes (#2662)
* fix: fixed dns processing

* fix: fixed proceesed index/id selection

* refactor: stop using the server index as state

* fix: fixed autostart and start minimized

* fix: fixed typo

* fix: add socks5 extractConfigFromContainer

* fix: remove unused currentContainerUpdated

* fix: fixed clear cached profile order
2026-05-28 10:57:08 +08:00
Yaroslav Gurov 1753aed3fc fix: use shared openssl on Android (#2657)
* feat(conan): clone openssl and patch it for Android

* fix(conan): build shared libssl for Android
2026-05-26 21:59:47 +08:00
Yaroslav Gurov c714d98bd1 chore: extend prebuilts support for MacOS NE by clang 21+ (#2633)
* chore: extend prebuilts support for MacOS NE by clang 21+

* chore: trigger prebuilts on workflow file changes
2026-05-25 22:05:56 +08:00
vkamn 4787f3915b chore: defines for update controller (#2634) 2026-05-21 20:40:04 +08:00
Yaroslav Gurov 7a383116b2 fix: force os.sdk to empty on Darwin (#2632) 2026-05-21 18:56:53 +08:00
Yaroslav Gurov d3de5f0f48 fix(conan): openvpn support in MSVC+Ninja setup (#2616) 2026-05-21 18:09:52 +08:00
vkamn 8749d683e3 chore: minor fixes (#2630) 2026-05-21 11:38:41 +08:00
vkamn 9de9d082bc chore: bump version (#2629) 2026-05-21 10:41:25 +08:00
Yaroslav Gurov a4233fef41 fix: add ssh init and finalize for statically-linked libssh (#2627) 2026-05-21 10:19:08 +08:00
Yaroslav Gurov 4890dd1d74 chore: compare changes against base branch of the PR (#2626) 2026-05-20 21:26:41 +08:00
Yaroslav Gurov 564630827e сhore: apple ci cd macos versions (#2625)
* chore(ci/cd): use macos-26 for xcode>26.4

* chore(ci/cd): bump conan version

* chore(conan): remove redundant VirtualBuildEnv
2026-05-20 21:19:56 +08:00
Yaroslav Gurov fbe15d965b chore: bump apple hev-socks5-tunnel dep (#2624)
* chore(conan): bump hev-socks5-tunnel

* chore(conan): bump xcode-versions

* chore(conan): upload prebuilts only in case of pushing to dev
2026-05-20 20:31:35 +08:00
vkamn b29515c380 chore: rename artifacts (#2622) 2026-05-20 19:07:30 +08:00
vkamn 0658a8f565 revert: regional country codes UI (#2567) (#2621) 2026-05-20 13:55:28 +08:00
vkamn 482ec04b4a chore: bump version (#2620)
* chore: bump version

* chore: bump android qt version
2026-05-20 12:37:38 +08:00
vkamn d40d24fcf9 fix: fixed validateAndPrepareConfig for non admin configs (#2617)
* fix: fixed validateAndPrepareConfig for non admin configs

* fix: fix fetchGatewayUrl lambda context
2026-05-20 12:37:22 +08:00
yp fb5666057b feat: add extended vless configuration (#2566)
* update UI XRay, add new page PageProtocolXrayTransportSettings.qml PageProtocolXrayXmuxSettings.qml PageProtocolXrayXPaddingSettings.qml

* add UI PageProtocolXrayConfigsSettings, PageProtocolXrayFlowSettings, PageProtocolXraySecuritySettings

* add Xray-specific keys

* add vars xray model

* add new qml padding, update model

* update model and export

* rename file & update name class & update list xray

* fixed ui

* add save file in temp

* remove debug macros

* fixed build windows

* fix path Windows

* remove save config

* fixed changes

* fixed conf

* fixed UI

* fixed size & button save

* fixed build iOS

* fix: fixed headers base control

---------

Co-authored-by: vkamn <vk@amnezia.org>
2026-05-18 22:35:01 +08:00
yp a49892c7e7 feat: add telemt container (#2435)
* Feat: Add MtProxy (Telegram)

* add path files

* Feat: Add Telemt (MtProxy)

* fixed secret & enum

* remove old path

* refactor: move logic from ui to core

---------

Co-authored-by: vkamn <vk@amnezia.org>
2026-05-18 20:01:09 +08:00
yp 277b295fd8 feat: add mtproxy(#2370)
* Feat: Add MtProxy (Telegram)

* add path files

* refactor: move logic from ui to core

---------

Co-authored-by: vkamn <vk@amnezia.org>
2026-05-18 19:52:58 +08:00
lunardunno 8c33779fc3 chore: Install recommends for apt (#2596) 2026-05-18 13:56:57 +08:00
lunardunno f0299ca9fe chore: authentication prompt in Ubuntu 26.04 (#2603)
Handling the password prompt in Ubuntu 26.04
2026-05-18 11:55:07 +08:00
MrMirDan c7b1c2809f fix: app buttons clicked instead of buttons in context menu (#2200)
* fix: app buttons clicked instead of buttons in context menu

* update: using MouseArea instead of changing popupType

* fix(cursor): fixed cursor type at opened context menu

---------

Co-authored-by: Mitternacht822 <sb@amnezia.org>
2026-05-15 21:02:09 +08:00
MrMirDan c9ed0baf3b fix: app freezes when revoke awg/wg client during active connection (#2211)
* block configs revoke during connection

* update: check that current config is active

* update: notification text
2026-05-15 21:01:39 +08:00
yp 2a3e3126ac feat: regional country codes (#2567)
Co-authored-by: vkamn <vk@amnezia.org>
2026-05-15 15:44:58 +08:00
MrMirDan 98771027b7 fix: vless switch between dividers (#2600) 2026-05-15 14:58:23 +08:00
MrMirDan 0433e03bdc fix: amnezia free card button hovers when card enabled (#2602) 2026-05-15 14:58:11 +08:00
yp cb48667b91 fix: bug when saving after canceling the save action (#2568)
Co-authored-by: vkamn <vk@amnezia.org>
2026-05-15 14:57:44 +08:00
yp d0a1af0381 refactor: deactivate api config before remove (#2569)
Co-authored-by: vkamn <vk@amnezia.org>
2026-05-15 14:56:09 +08:00
Yaroslav Gurov fd0c773918 fix: change artifact names (#2589) 2026-05-15 12:36:38 +08:00
vkamn 06372c8fd7 refactor: remove serverConfig struct (#2595)
* refactor: remove serverConfig struct

* refactor: add warnings for api v1 configs

* refactor: moved the server type definition to a separate namespace

* refactor: simplified gateway stacks

* fix: fixed server description

* fix: fixed postAsync reply usage

* fix: fixed validateConfig call

* fix: fixed server name in notifications

* fix: fixed initPrepareConfigHandler for lagacy configs
2026-05-15 12:33:36 +08:00
aiamnezia 24f545d001 Merge branch 'dev' into feature/local-proxy-integration 2026-04-21 16:14:49 +04:00
aiamnezia 2b0d86c626 fix: waiting starting xray before setting running flag 2026-04-13 11:04:19 +04:00
aiamnezia fe773a108e fix: waiting VPN disconnect before proxy turning on 2026-04-13 10:19:01 +04:00
aiamnezia a0997156f6 fix: restoring server_uuid within config update 2026-04-13 09:40:23 +04:00
aiamnezia 9a6e975622 fix: stop xray before closing the app 2026-04-13 09:24:55 +04:00
aiamnezia 78f09634a4 fix: prevent stopping Xray due to changing local proxy port
- Added a check in XrayController::stop() to skip the stop operation if Xray is not currently running, with a debug log for clarity.
2026-04-13 07:31:26 +04:00
aiamnezia be692001b0 fix: fix local proxy settings and restart logic
- Added local proxy restart token management in Settings.
- Implemented logic to handle local proxy state on application quit.
- Updated ProxyServer to restart based on configuration changes.
- Enhanced API configuration updates to bump restart token when necessary.
- Improved UI components to reflect local proxy availability and state.
- Added new error handling and notifications for local proxy operations.
2026-04-13 07:14:42 +04:00
aiamnezia 850b8ea03b Merge branch 'dev' into feature/local-proxy-integration 2026-04-13 03:07:45 +04:00
aiamnezia c645d07a87 chore: fix build 2026-03-24 16:39:59 +04:00
aiamnezia 7f8786720e Merge branch 'dev' into feature/local-proxy-integration 2026-03-24 15:48:49 +04:00
aiamnezia 25c70b5bf6 fix: fix android build 2026-02-13 17:21:15 +04:00
aiamnezia 7bf16f075c feat: add mutual exclusion between local proxy and vpn 2026-02-13 17:07:35 +04:00
aiamnezia 6518d4866e fix: Fix local proxy UI 2026-02-13 16:46:49 +04:00
aiamnezia 4c2010244b feat: Update local proxy settings page 2026-01-27 15:09:02 +04:00
aiamnezia e946ee2430 feat: enhance ConfigManager with dynamic proxy port resolution and availability check
- Added functionality to resolve and validate the local proxy port within a specified range.
- Implemented a method to check if a port is available before applying it to the configuration.
- Updated ProxyService to handle the new port resolution logic and cache the parsed configuration.
2026-01-27 12:45:38 +04:00
aiamnezia 5fab8363e7 feat: made API port static and proxy port configurable 2026-01-27 11:09:27 +04:00
aiamnezia 35c2e1564b chore: add debug 2026-01-23 15:33:25 +04:00
aiamnezia ca5bca085b feat: remove logs for local proxy 2026-01-19 16:25:59 +04:00
aiamnezia 33b2a3d2fc Merge branch 'dev' into feature/local-proxy-integration 2026-01-16 17:48:48 +04:00
aiamnezia 917f5858a8 fix: update ProxyService to use new configuration fetching method 2026-01-15 23:35:39 +04:00
aiamnezia e98c11079a feat: enhance ConfigManager with fetch capabilities for Xray configuration 2026-01-15 23:05:36 +04:00
aiamnezia 84d908d4d8 feat: enhance local proxy error handling and configuration management
- Added signal for local proxy start failure to Settings.
- Updated CoreController to emit failure signals with descriptive messages when local proxy fails to start.
- Refactored XrayController to accept JSON configuration directly, improving configuration handling.
- Removed unused config file loading logic to streamline the XrayController's functionality.
2026-01-15 21:04:10 +04:00
aiamnezia 412e69af9b feat: update ConfigManager and ProxyServer to utilize Settings
- Modified ConfigManager to accept a Settings object for improved configuration management.
- Updated ProxyServer to initialize with Settings, enhancing dependency injection.
2025-12-31 21:16:45 +04:00
aiamnezia 4492b0af7e fix: fix local proxy page 2025-12-31 20:15:40 +04:00
aiamnezia 806b1d75af refactor: streamline HTTP API for Xray control
- Removed outdated config management routes and consolidated Xray control endpoints.
- Updated response structures to ensure consistent error handling across API calls.
2025-12-31 19:30:29 +04:00
aiamnezia 9740e7557a feat: add dynamic local proxy lifecycle in depence of settings 2025-12-31 18:48:57 +04:00
aiamnezia 5ab82e2196 chore: fix android build 2025-12-30 16:59:15 +04:00
aiamnezia ae44de7101 chore: fix win build 2025-12-30 16:05:05 +04:00
aiamnezia 2be6079e21 refactor: enhance XrayController to use IPC for process management
- Replaced direct process management with IPC calls for starting and stopping the Xray process.
- Improved error handling for IPC communication and config file loading.
- Removed unused methods and variables related to direct process handling.
- Updated logging to reflect changes in process management.
2025-12-30 15:33:59 +04:00
aiamnezia 2a653f8876 feat: implement local proxy settings UI and functionality
- Added new QML pages for managing local proxy settings and connection types.
- Updated SettingsController to handle local proxy enablement and port configuration.
- Enhanced server model to include processed server UUID for local proxy management.
2025-12-30 14:19:32 +04:00
aiamnezia 41ab51a5ef feat: add server UUID management and local proxy settings
- Implemented UUID migration for servers to ensure each server has a unique identifier.
- Added methods for managing local proxy settings, including owner UUID, port, and HTTP enablement.
- Updated server model to include server UUID role for better data handling.
2025-12-30 11:02:59 +04:00
aiamnezia 300558c33c Merge branch 'dev' into feature/local-proxy-integration 2025-12-19 14:18:26 +04:00
aiamnezia 774deb87f5 fix build scripts 2025-08-08 09:44:50 +04:00
aiamnezia bae7fbd222 Add qtwebsockets in deploy.yml 2025-08-08 09:14:35 +04:00
aiamnezia 97e4b95673 Add qthttpserver deploy.yml 2025-08-08 09:07:28 +04:00
aiamnezia 2ae97c5cda feat: Added local proxy server 2025-08-08 06:44:18 +04:00
294 changed files with 20614 additions and 6014 deletions
+60 -33
View File
@@ -18,11 +18,14 @@ jobs:
- uses: dorny/paths-filter@v3
id: filter
with:
base: ${{ github.event.before }}
filters: |
recipes:
- 'recipes/**'
- 'conanfile.py'
- '.github/workflows/deploy.yml'
- 'cmake/conan_provider.cmake'
- 'cmake/platform_settings.cmake'
- 'cmake/recipes_bootstrap.cmake'
Bake-Prebuilts-Linux:
runs-on: ubuntu-latest
@@ -40,7 +43,7 @@ jobs:
python-version: 3.14
- name: 'Install conan'
run: pip install "conan==2.26.2"
run: pip install "conan==2.28.0"
- name: 'Build dependencies'
shell: bash
@@ -50,9 +53,11 @@ jobs:
done
- name: 'Authorize in remote'
if: github.ref == 'refs/heads/dev'
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
- name: 'Upload baked prebuilts'
if: github.ref == 'refs/heads/dev'
run: conan upload -r amnezia "*" -c
# ------------------------------------------------------
@@ -83,7 +88,7 @@ jobs:
host: 'linux'
target: 'desktop'
arch: 'linux_gcc_64'
modules: 'qtremoteobjects qt5compat qtshadertools'
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
dir: ${{ runner.temp }}
setup-python: 'true'
tools: 'tools_ifw'
@@ -98,7 +103,7 @@ jobs:
python-version: 3.14
- name: 'Install conan'
run: pip install "conan==2.26.2"
run: pip install "conan==2.28.0"
- name: 'Install system packages'
run: sudo apt-get install libxkbcommon-x11-0 libsecret-1-dev
@@ -118,7 +123,7 @@ jobs:
- name: 'Upload installer artifact'
uses: actions/upload-artifact@v7
with:
path: deploy/build/AmneziaVPN-*-Linux.run
path: deploy/build/AmneziaVPN_*_linux_x64.run
archive: false
retention-days: 7
@@ -149,15 +154,17 @@ jobs:
- uses: ilammy/msvc-dev-cmd@v1
- name: 'Install conan'
run: pip install "conan==2.26.2"
run: pip install "conan==2.28.0"
- name: 'Build dependencies'
run: cmake -S . -B build -G "Visual Studio 17 2022" -DPREBUILTS_ONLY=1
run: cmake -S . -B build -DPREBUILTS_ONLY=1
- name: 'Authorize in remote'
if: github.ref == 'refs/heads/dev'
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
- name: 'Upload baked prebuilts'
if: github.ref == 'refs/heads/dev'
run: conan upload -r amnezia "*" -c
# ------------------------------------------------------
@@ -194,7 +201,7 @@ jobs:
host: 'windows'
target: 'desktop'
arch: 'win64_msvc2022_64'
modules: 'qtremoteobjects qt5compat qtshadertools'
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
dir: ${{ runner.temp }}
setup-python: 'true'
tools: 'tools_ifw'
@@ -229,7 +236,7 @@ jobs:
python-version: 3.14
- name: 'Install conan'
run: pip install "conan==2.26.2"
run: pip install "conan==2.28.0"
- name: 'Build project'
shell: cmd
@@ -242,27 +249,31 @@ jobs:
- name: 'Upload WIX installer artifact'
uses: actions/upload-artifact@v7
with:
path: deploy/build/AmneziaVPN-*-win64.msi
path: deploy/build/AmneziaVPN_*_windows_x64.msi
archive: false
retention-days: 7
- name: 'Upload IFW installer artifact'
uses: actions/upload-artifact@v7
with:
path: deploy/build/AmneziaVPN-*-win64.exe
path: deploy/build/AmneziaVPN_*_windows_x64.exe
archive: false
retention-days: 7
# ------------------------------------------------------
Bake-Prebuilts-iOS:
runs-on: macos-latest
needs: Detect-Changes
if: needs.Detect-Changes.outputs.recipes_changed == 'true'
strategy:
matrix:
xcode-version: [26.0]
xcode-version: [26.0, 26.4]
include:
- xcode-version: 26.4
os: macos-26
runs-on: ${{ matrix.os || 'macos-latest' }}
steps:
- uses: actions/checkout@v4
@@ -279,15 +290,17 @@ jobs:
xcode-version: ${{ matrix.xcode-version }}
- name: 'Install conan'
run: pip install "conan==2.26.2"
run: pip install "conan==2.28.0"
- name: 'Build dependencies'
run: cmake -S . -B build -G Xcode -DPREBUILTS_ONLY=1 -DCMAKE_SYSTEM_NAME=iOS -DCMAKE_OSX_SYSROOT=iphoneos
- name: 'Authorize in remote'
if: github.ref == 'refs/heads/dev'
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
- name: 'Upload baked prebuilts'
if: github.ref == 'refs/heads/dev'
run: conan upload -r amnezia "*" -c
# ------------------------------------------------------
@@ -344,7 +357,7 @@ jobs:
- name: 'Setup xcode'
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '26.1'
xcode-version: '26.0'
- name: 'Install desktop Qt'
uses: jurplel/install-qt-action@v3
@@ -352,7 +365,7 @@ jobs:
version: ${{ env.QT_VERSION }}
host: 'mac'
target: 'desktop'
modules: 'qtremoteobjects qt5compat qtshadertools qtmultimedia'
modules: 'qtremoteobjects qt5compat qtshadertools qtmultimedia qthttpserver qtwebsockets'
arch: 'clang_64'
dir: ${{ runner.temp }}
set-env: 'true'
@@ -364,7 +377,7 @@ jobs:
version: ${{ env.QT_VERSION }}
host: 'mac'
target: 'ios'
modules: 'qtremoteobjects qt5compat qtshadertools qtmultimedia'
modules: 'qtremoteobjects qt5compat qtshadertools qtmultimedia qthttpserver qtwebsockets'
dir: ${{ runner.temp }}
setup-python: 'true'
set-env: 'true'
@@ -376,7 +389,7 @@ jobs:
python-version: 3.14
- name: 'Install deps'
run: pip install "conan==2.26.2" jsonschema jinja2
run: pip install "conan==2.28.0" jsonschema jinja2
- name: 'Build project'
env:
@@ -394,14 +407,17 @@ jobs:
# ------------------------------------------------------
Bake-Prebuilts-MacOS:
runs-on: macos-latest
needs: Detect-Changes
if: needs.Detect-Changes.outputs.recipes_changed == 'true'
strategy:
matrix:
xcode-version: [16.2, 16.4]
xcode-version: [16.2, 16.4, 26.4]
include:
- xcode-version: 26.4
os: macos-26
runs-on: ${{ matrix.os || 'macos-latest' }}
steps:
- uses: actions/checkout@v4
@@ -418,15 +434,17 @@ jobs:
xcode-version: ${{ matrix.xcode-version }}
- name: 'Install conan'
run: pip install "conan==2.26.2"
run: pip install "conan==2.28.0"
- name: 'Build dependencies'
run: cmake -S . -B build -G Xcode -DPREBUILTS_ONLY=1
- name: 'Authorize in remote'
if: github.ref == 'refs/heads/dev'
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
- name: 'Upload baked prebuilts'
if: github.ref == 'refs/heads/dev'
run: conan upload -r amnezia "*" -c
# ------------------------------------------------------
@@ -488,7 +506,7 @@ jobs:
host: 'mac'
target: 'desktop'
arch: 'clang_64'
modules: 'qtremoteobjects qt5compat qtshadertools'
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
dir: ${{ runner.temp }}
setup-python: 'true'
set-env: 'true'
@@ -502,7 +520,7 @@ jobs:
python-version: 3.14
- name: 'Install conan'
run: pip install "conan==2.26.2"
run: pip install "conan==2.28.0"
- name: 'Build project'
env:
@@ -518,20 +536,24 @@ jobs:
- name: 'Upload installer artifact'
uses: actions/upload-artifact@v7
with:
path: deploy/build/AmneziaVPN-*-Darwin.pkg
path: deploy/build/AmneziaVPN_*_macos_x64.pkg
archive: false
retention-days: 7
# ------------------------------------------------------
Bake-Prebuilts-MacOS-NE:
runs-on: macos-latest
needs: Detect-Changes
if: needs.Detect-Changes.outputs.recipes_changed == 'true'
strategy:
matrix:
xcode-version: [16.2, 16.4]
xcode-version: [16.2, 16.4, 26.4]
include:
- xcode-version: 26.4
os: macos-26
runs-on: ${{ matrix.os || 'macos-latest' }}
steps:
- uses: actions/checkout@v4
@@ -548,15 +570,17 @@ jobs:
xcode-version: ${{ matrix.xcode-version }}
- name: 'Install conan'
run: pip install "conan==2.26.2"
run: pip install "conan==2.28.0"
- name: 'Build dependencies'
run: cmake -S . -B build -G Xcode -DPREBUILTS_ONLY=1 -DMACOS_NE=TRUE
- name: 'Authorize in remote'
if: github.ref == 'refs/heads/dev'
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
- name: 'Upload baked prebuilts'
if: github.ref == 'refs/heads/dev'
run: conan upload -r amnezia "*" -c
# ------------------------------------------------------
@@ -635,7 +659,7 @@ jobs:
python-version: 3.14
- name: 'Install conan'
run: pip install "conan==2.26.2"
run: pip install "conan==2.28.0"
- name: 'Build project'
run: |
@@ -671,7 +695,7 @@ jobs:
python-version: 3.14
- name: 'Install conan'
run: pip install "conan==2.26.2"
run: pip install "conan==2.28.0"
- name: 'Setup Android SDK'
uses: android-actions/setup-android@v4
@@ -696,9 +720,11 @@ jobs:
done
- name: 'Authorize in remote'
if: github.ref == 'refs/heads/dev'
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
- name: 'Upload baked prebuilts'
if: github.ref == 'refs/heads/dev'
run: conan upload -r amnezia "*" -c
# ------------------------------------------------------
@@ -712,8 +738,8 @@ jobs:
env:
ANDROID_PLATFORM: android-28
NDK_VERSION: 27.0.11718014
QT_VERSION: 6.10.1
QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools'
QT_VERSION: 6.10.3
QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools qthttpserver qtwebsockets'
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
@@ -806,7 +832,7 @@ jobs:
python-version: 3.14
- name: 'Install conan'
run: pip install "conan==2.26.2"
run: pip install "conan==2.28.0"
- name: 'Decode keystore secret to file'
env:
@@ -894,3 +920,4 @@ jobs:
run: |
echo "Pull request:" >> $GITHUB_STEP_SUMMARY
echo "[[#${{ fromJSON(steps.pull_request.outputs.data)[0].number }}] ${{ fromJSON(steps.pull_request.outputs.data)[0].title }}](${{ fromJSON(steps.pull_request.outputs.data)[0].html_url }})" >> $GITHUB_STEP_SUMMARY
+3 -1
View File
@@ -10,7 +10,9 @@ deploy/build_64/*
winbuild*.bat
.cache/
.vscode/
.cursorignore
.cursor/
.venv/
# Qt-es
/.qmake.cache
+4 -4
View File
@@ -4,7 +4,7 @@ set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(PROJECT AmneziaVPN)
set(AMNEZIAVPN_VERSION 4.8.15.4)
set(AMNEZIAVPN_VERSION 4.9.0.3)
set(QT_CREATOR_SKIP_PACKAGE_MANAGER_SETUP ON CACHE BOOL "" FORCE)
set(CMAKE_PROJECT_TOP_LEVEL_INCLUDES
@@ -18,9 +18,9 @@ project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
HOMEPAGE_URL "https://amnezia.org/"
)
# trigger conan to kick off `conan install` globally
find_package(OpenSSL REQUIRED)
if (PREBUILTS_ONLY)
# trigger conan to kick off `conan install`
find_package(OpenSSL REQUIRED)
return()
endif()
@@ -28,7 +28,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 2120)
set(APP_ANDROID_VERSION_CODE 2126)
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(MZ_PLATFORM_NAME "linux")
+31 -6
View File
@@ -14,6 +14,10 @@ set(PACKAGES
Core5Compat Concurrent LinguistTools
)
if(NOT ANDROID AND NOT IOS)
list(APPEND PACKAGES HttpServer)
endif()
execute_process(
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
COMMAND git rev-parse --short HEAD
@@ -47,6 +51,10 @@ set(LIBS ${LIBS}
Qt6::Core5Compat Qt6::Concurrent
)
if(NOT ANDROID AND NOT IOS)
list(APPEND LIBS Qt6::HttpServer)
endif()
if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
set(LIBS ${LIBS} Qt6::Widgets)
endif()
@@ -193,10 +201,6 @@ elseif(APPLE)
include(cmake/macos.cmake)
endif()
if(NOT IOS AND NOT ANDROID AND NOT MACOS_NE)
add_subdirectory(tests)
endif()
list(APPEND SOURCES ${CMAKE_CURRENT_LIST_DIR}/main.cpp)
target_link_libraries(${PROJECT} PRIVATE ${LIBS})
@@ -216,11 +220,32 @@ endif()
install(TARGETS ${PROJECT}
DESTINATION ${CMAKE_INSTALL_BINDIR}
RUNTIME_DEPENDENCY_SET client_deps
COMPONENT AmneziaVPN
)
install(FILES $<TARGET_RUNTIME_DLLS:${PROJECT}>
DESTINATION ${CMAKE_INSTALL_BINDIR}
if(APPLE)
set(RUNTIME_DEPS_DIR ${CMAKE_INSTALL_BINDIR}/AmneziaVPN.app/Contents/Frameworks)
else()
set(RUNTIME_DEPS_DIR ${CMAKE_INSTALL_BINDIR})
endif()
install(RUNTIME_DEPENDENCY_SET client_deps
PRE_EXCLUDE_REGEXES
[[api-ms-win-.*]]
[[ext-ms-.*]]
[[kernel32\.dll]]
[[hvsifiletrust\.dll]]
[[libc\.so\..*]] [[libgcc_s\.so\..*]] [[libm\.so\..*]] [[libstdc\+\+\.so\..*]]
[[.*\.framework]]
[[^[Qq]t.*]]
POST_EXCLUDE_REGEXES
[[^.*[\\/]system32[\\/].*\.dll$]]
[[^/lib.*]]
[[^/usr/lib.*]]
DIRECTORIES ${CONAN_RUNTIME_LIB_DIRS}
COMPONENT AmneziaVPN
DESTINATION "${RUNTIME_DEPS_DIR}"
)
set(deploy_tool_options "")
+6
View File
@@ -119,7 +119,13 @@ void AmneziaApplication::init()
win->setPersistentSceneGraph(true);
win->setPersistentGraphics(true);
#endif
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
win->show();
#else
if (!m_coreController || !m_coreController->pageController()->shouldStartMinimized()) {
win->show();
}
#endif
}
},
Qt::QueuedConnection);
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:type="linear"
android:angle="135"
android:startColor="#2A2A2E"
android:centerColor="#17171A"
android:endColor="#0E0E11" />
</shape>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<inset xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/ic_amnezia_round"
android:insetLeft="19.5%"
android:insetTop="19.5%"
android:insetRight="19.5%"
android:insetBottom="19.5%" />
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="NoActionBar">
<item name="android:windowBackground">@color/black</item>
<item name="android:colorBackground">@color/black</item>
<item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:enforceNavigationBarContrast">false</item>
<item name="android:enforceStatusBarContrast">false</item>
<item name="android:windowSplashScreenBackground">@color/ic_launcher_background</item>
<item name="android:windowSplashScreenIconBackgroundColor">@color/ic_launcher_background</item>
<item name="android:windowSplashScreenAnimatedIcon">@mipmap/icon</item>
</style>
</resources>
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#0E0E11</color>
</resources>
-1
View File
@@ -1,6 +1,5 @@
<RCC>
<qresource prefix="/client_scripts">
<file>linux_installer.sh</file>
<file>mac_installer.sh</file>
</qresource>
</RCC>
-29
View File
@@ -1,29 +0,0 @@
#!/bin/bash
EXTRACT_DIR="$1"
INSTALLER_PATH="$2"
# Create and clean extract directory
rm -rf "$EXTRACT_DIR"
mkdir -p "$EXTRACT_DIR"
# Extract TAR archive
tar -xf "$INSTALLER_PATH" -C "$EXTRACT_DIR"
if [ $? -ne 0 ]; then
echo 'Failed to extract TAR archive'
exit 1
fi
# Find and run installer
INSTALLER=$(find "$EXTRACT_DIR" -type f -executable)
if [ -z "$INSTALLER" ]; then
echo 'Installer not found'
exit 1
fi
"$INSTALLER"
EXIT_CODE=$?
# Cleanup
rm -rf "$EXTRACT_DIR"
exit $EXIT_CODE
-1
View File
@@ -54,7 +54,6 @@ target_include_directories(${PROJECT} PRIVATE ${Qt6Gui_PRIVATE_INCLUDE_DIRS})
set_target_properties(${PROJECT} PROPERTIES
XCODE_LINK_BUILD_PHASE_MODE KNOWN_LOCATION
MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Info.plist.in
MACOSX_BUNDLE_ICON_FILE "AppIcon"
MACOSX_BUNDLE_INFO_STRING "AmneziaVPN"
+1 -1
View File
@@ -152,5 +152,5 @@ message(${QtCore_location})
get_filename_component(QT_BIN_DIR_DETECTED "${QtCore_location}/../../../../../bin" ABSOLUTE)
add_custom_command(TARGET ${PROJECT} POST_BUILD
COMMAND ${QT_BIN_DIR_DETECTED}/macdeployqt $<TARGET_BUNDLE_DIR:AmneziaVPN> -appstore-compliant -qmldir=${CMAKE_CURRENT_SOURCE_DIR}
COMMAND ${QT_BIN_DIR_DETECTED}/macdeployqt $<TARGET_BUNDLE_DIR:AmneziaVPN> -appstore-compliant -qmldir=${CMAKE_CURRENT_SOURCE_DIR} -no-codesign
)
+17 -1
View File
@@ -15,7 +15,6 @@ set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/core/utils/constants/protocolConstants.h
${CLIENT_ROOT_DIR}/core/utils/constants/apiKeys.h
${CLIENT_ROOT_DIR}/core/utils/constants/apiConstants.h
${CLIENT_ROOT_DIR}/core/utils/api/apiEnums.h
${CLIENT_ROOT_DIR}/core/utils/errorStrings.h
${CLIENT_ROOT_DIR}/core/utils/selfhosted/scriptsRegistry.h
${CLIENT_ROOT_DIR}/core/utils/qrCodeUtils.h
@@ -36,6 +35,8 @@ set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/core/installers/torInstaller.h
${CLIENT_ROOT_DIR}/core/installers/sftpInstaller.h
${CLIENT_ROOT_DIR}/core/installers/socks5Installer.h
${CLIENT_ROOT_DIR}/core/installers/mtProxyInstaller.h
${CLIENT_ROOT_DIR}/core/installers/telemtInstaller.h
${CLIENT_ROOT_DIR}/core/controllers/appSplitTunnelingController.h
${CLIENT_ROOT_DIR}/core/controllers/ipSplitTunnelingController.h
${CLIENT_ROOT_DIR}/core/controllers/allowedDnsController.h
@@ -111,6 +112,8 @@ set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/core/installers/torInstaller.cpp
${CLIENT_ROOT_DIR}/core/installers/sftpInstaller.cpp
${CLIENT_ROOT_DIR}/core/installers/socks5Installer.cpp
${CLIENT_ROOT_DIR}/core/installers/mtProxyInstaller.cpp
${CLIENT_ROOT_DIR}/core/installers/telemtInstaller.cpp
${CLIENT_ROOT_DIR}/core/controllers/appSplitTunnelingController.cpp
${CLIENT_ROOT_DIR}/core/controllers/ipSplitTunnelingController.cpp
${CLIENT_ROOT_DIR}/core/controllers/allowedDnsController.cpp
@@ -138,6 +141,7 @@ set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/../common/logger/logger.cpp
${CLIENT_ROOT_DIR}/ui/utils/qmlUtils.cpp
${CLIENT_ROOT_DIR}/core/utils/api/apiUtils.cpp
${CLIENT_ROOT_DIR}/core/utils/serverConfigUtils.cpp
${CLIENT_ROOT_DIR}/core/utils/osSignalHandler.cpp
${CLIENT_ROOT_DIR}/core/utils/utilities.cpp
${CLIENT_ROOT_DIR}/core/utils/managementServer.cpp
@@ -197,16 +201,23 @@ file(GLOB CONFIGURATORS_CPP CONFIGURE_DEPENDS ${CLIENT_ROOT_DIR}/core/configurat
file(GLOB_RECURSE CORE_MODELS_H CONFIGURE_DEPENDS ${CLIENT_ROOT_DIR}/core/models/*.h)
file(GLOB_RECURSE CORE_MODELS_CPP CONFIGURE_DEPENDS ${CLIENT_ROOT_DIR}/core/models/*.cpp)
if(NOT ANDROID AND NOT IOS)
file(GLOB LOCAL_PROXY_H CONFIGURE_DEPENDS ${CLIENT_ROOT_DIR}/core/local-proxy/*.h)
file(GLOB LOCAL_PROXY_CPP CONFIGURE_DEPENDS ${CLIENT_ROOT_DIR}/core/local-proxy/*.cpp)
endif()
file(GLOB UI_MODELS_H CONFIGURE_DEPENDS
${CLIENT_ROOT_DIR}/ui/models/*.h
${CLIENT_ROOT_DIR}/ui/models/protocols/*.h
${CLIENT_ROOT_DIR}/ui/models/services/*.h
${CLIENT_ROOT_DIR}/ui/models/utils/*.h
${CLIENT_ROOT_DIR}/ui/models/api/*.h
)
file(GLOB UI_MODELS_CPP CONFIGURE_DEPENDS
${CLIENT_ROOT_DIR}/ui/models/*.cpp
${CLIENT_ROOT_DIR}/ui/models/protocols/*.cpp
${CLIENT_ROOT_DIR}/ui/models/services/*.cpp
${CLIENT_ROOT_DIR}/ui/models/utils/*.cpp
${CLIENT_ROOT_DIR}/ui/models/api/*.cpp
)
@@ -240,6 +251,11 @@ set(SOURCES ${SOURCES}
${UI_CONTROLLERS_CPP}
)
if(NOT ANDROID AND NOT IOS)
list(APPEND HEADERS ${LOCAL_PROXY_H})
list(APPEND SOURCES ${LOCAL_PROXY_CPP})
endif()
if(WIN32)
set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/core/protocols/ikev2VpnProtocolWindows.h
+616 -157
View File
@@ -4,6 +4,7 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QThread>
#include <QUuid>
#include "logger.h"
@@ -20,183 +21,641 @@
#include "core/models/protocols/xrayProtocolConfig.h"
namespace {
Logger logger("XrayConfigurator");
}
Logger logger("XrayConfigurator");
QString normalizeXhttpMode(const QString &m) {
const QString t = m.trimmed();
if (t.isEmpty() || t.compare(QLatin1String("Auto"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("auto");
}
if (t.compare(QLatin1String("Packet-up"), Qt::CaseInsensitive) == 0)
return QStringLiteral("packet-up");
if (t.compare(QLatin1String("Stream-up"), Qt::CaseInsensitive) == 0)
return QStringLiteral("stream-up");
if (t.compare(QLatin1String("Stream-one"), Qt::CaseInsensitive) == 0)
return QStringLiteral("stream-one");
return t.toLower();
}
// Xray-core: empty → path; "None" in UI → omit (core default path)
QString normalizeSessionSeqPlacement(const QString &p)
{
if (p.isEmpty() || p.compare(QLatin1String("None"), Qt::CaseInsensitive) == 0)
return {};
return p.toLower();
}
QString normalizeUplinkDataPlacement(const QString &p)
{
if (p.isEmpty() || p.compare(QLatin1String("Body"), Qt::CaseInsensitive) == 0)
return QStringLiteral("body");
if (p.compare(QLatin1String("Auto"), Qt::CaseInsensitive) == 0)
return QStringLiteral("auto");
if (p.compare(QLatin1String("Query"), Qt::CaseInsensitive) == 0)
// "Query" is not valid for uplink payload in splithttp; closest documented mode
return QStringLiteral("header");
return p.toLower();
}
// splithttp: cookie | header | query | queryInHeader (not "body")
QString normalizeXPaddingPlacement(const QString &p)
{
QString t = p.trimmed();
if (t.isEmpty())
return QString::fromLatin1(amnezia::protocols::xray::defaultXPaddingPlacement).toLower();
if (t.compare(QLatin1String("Body"), Qt::CaseInsensitive) == 0)
return QStringLiteral("queryInHeader");
if (t.contains(QLatin1String("queryInHeader"), Qt::CaseInsensitive)
|| t.compare(QLatin1String("Query in header"), Qt::CaseInsensitive) == 0)
return QStringLiteral("queryInHeader");
return t.toLower();
}
// splithttp: repeat-x | tokenish
QString normalizeXPaddingMethod(const QString &m)
{
QString t = m.trimmed();
if (t.isEmpty() || t.compare(QLatin1String("Repeat-x"), Qt::CaseInsensitive) == 0)
return QStringLiteral("repeat-x");
if (t.compare(QLatin1String("Tokenish"), Qt::CaseInsensitive) == 0)
return QStringLiteral("tokenish");
if (t.compare(QLatin1String("Random"), Qt::CaseInsensitive) == 0
|| t.compare(QLatin1String("Zero"), Qt::CaseInsensitive) == 0)
return QStringLiteral("repeat-x");
return t.toLower();
}
void putIntRangeIfAny(QJsonObject &obj, const char *key, QString minV, QString maxV, const char *fallbackMin,
const char *fallbackMax)
{
if (minV.isEmpty() && maxV.isEmpty())
return;
if (minV.isEmpty())
minV = QString::fromLatin1(fallbackMin);
if (maxV.isEmpty())
maxV = QString::fromLatin1(fallbackMax);
QJsonObject r;
r[QStringLiteral("from")] = minV.toInt();
r[QStringLiteral("to")] = maxV.toInt();
obj[QString::fromUtf8(key)] = r;
}
// Desktop applies this in XrayProtocol::start(); iOS/Android pass JSON straight to libxray — same fixes here.
void sanitizeXrayNativeConfig(amnezia::ProtocolConfig &pc)
{
QString c = pc.nativeConfig();
if (c.isEmpty()) {
return;
}
bool changed = false;
if (c.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) {
c.replace(QLatin1String("Mozilla/5.0"), QString::fromLatin1(amnezia::protocols::xray::defaultFingerprint),
Qt::CaseInsensitive);
changed = true;
}
const QString legacyListen = QString::fromLatin1(amnezia::protocols::xray::defaultLocalAddr);
const QString listenOk = QString::fromLatin1(amnezia::protocols::xray::defaultLocalListenAddr);
if (c.contains(legacyListen)) {
c.replace(legacyListen, listenOk);
changed = true;
}
if (changed) {
pc.setNativeConfig(c);
}
}
} // namespace
XrayConfigurator::XrayConfigurator(SshSession* sshSession, QObject *parent)
: ConfiguratorBase(sshSession, parent)
{
}
amnezia::ProtocolConfig XrayConfigurator::processConfigWithLocalSettings(const amnezia::ConnectionSettings &settings,
amnezia::ProtocolConfig protocolConfig)
{
applyDnsToNativeConfig(settings.dns, protocolConfig);
sanitizeXrayNativeConfig(protocolConfig);
return protocolConfig;
}
ErrorCode XrayConfigurator::uploadServerConfigJson(const ServerCredentials &credentials, DockerContainer container,
const DnsSettings &dnsSettings, const QJsonObject &serverConfig) const
{
const QString updatedConfig = QJsonDocument(serverConfig).toJson();
ErrorCode errorCode = m_sshSession->uploadTextFileToContainer(
container, credentials, updatedConfig, amnezia::protocols::xray::serverConfigPath,
libssh::ScpOverwriteMode::ScpOverwriteExisting);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Failed to upload updated config";
return errorCode;
}
const QString restartScript = QStringLiteral("sudo docker restart $CONTAINER_NAME");
errorCode = m_sshSession->runScript(
credentials,
m_sshSession->replaceVars(restartScript,
amnezia::genBaseVars(credentials, container, dnsSettings.primaryDns,
dnsSettings.secondaryDns)));
if (errorCode != ErrorCode::NoError) {
logger.error() << "Failed to restart container";
}
return errorCode;
}
ErrorCode XrayConfigurator::readRealityKeyFiles(const DockerContainer container, const ServerCredentials &credentials,
QString &outPublicKey, QString &outShortId) const
{
outPublicKey.clear();
outShortId.clear();
auto readKeyFile = [&](const QString &path, QString &out) -> ErrorCode {
for (int attempt = 0; attempt < 3; ++attempt) {
ErrorCode fileError = ErrorCode::NoError;
out = QString::fromUtf8(m_sshSession->getTextFileFromContainer(container, credentials, path, fileError));
out.replace(QLatin1Char('\n'), QString());
out.replace(QLatin1Char('\r'), QString());
if (fileError == ErrorCode::NoError && !out.isEmpty()) {
return ErrorCode::NoError;
}
if (attempt < 2) {
QThread::msleep(500);
}
}
logger.error() << "Xray readRealityKeyFiles: failed path=" << path;
return ErrorCode::XrayRealityKeysReadFailed;
};
ErrorCode errorCode = readKeyFile(QString::fromLatin1(amnezia::protocols::xray::PublicKeyPath), outPublicKey);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
return readKeyFile(QString::fromLatin1(amnezia::protocols::xray::shortidPath), outShortId);
}
QJsonObject XrayConfigurator::mergeStreamSettingsForServerInbound(const XrayServerConfig &srv,
const QJsonObject &existingStreamSettings) const
{
QJsonObject streamSettings = buildStreamSettings(srv, QString());
if (srv.security != QLatin1String("reality")) {
return streamSettings;
}
const QJsonObject newRs = streamSettings[amnezia::protocols::xray::realitySettings].toObject();
QJsonObject oldRs = existingStreamSettings[amnezia::protocols::xray::realitySettings].toObject();
QJsonObject merged = oldRs.isEmpty() ? newRs : oldRs;
const QString siteEff = srv.site.isEmpty() ? QString::fromLatin1(amnezia::protocols::xray::defaultSite) : srv.site;
const QString sniEff = srv.sni.isEmpty() ? siteEff : srv.sni;
if (newRs.contains(amnezia::protocols::xray::fingerprint)) {
merged[amnezia::protocols::xray::fingerprint] = newRs[amnezia::protocols::xray::fingerprint];
}
merged[amnezia::protocols::xray::serverNames] = QJsonArray { sniEff };
if (!merged.contains(QStringLiteral("dest"))) {
merged[QStringLiteral("dest")] = siteEff + QStringLiteral(":443");
}
streamSettings[amnezia::protocols::xray::realitySettings] = merged;
return streamSettings;
}
ErrorCode XrayConfigurator::applyServerSettingsToRemote(const ServerCredentials &credentials, DockerContainer container,
ContainerConfig &containerConfig, const DnsSettings &dnsSettings,
bool appendNewClient, QString *outClientId)
{
ErrorCode errorCode = ErrorCode::NoError;
const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>();
if (!xrayCfg) {
logger.error() << "Xray applyServerSettings: missing XrayProtocolConfig";
return ErrorCode::InternalError;
}
const XrayServerConfig &srv = xrayCfg->serverConfig;
if (srv.isThirdPartyConfig) {
logger.info() << "Xray applyServerSettings: skipped (third-party/native profile)";
if (outClientId && xrayCfg->hasClientConfig()) {
*outClientId = xrayCfg->clientConfig->id;
}
return ErrorCode::NoError;
}
logger.info() << "Xray applyServerSettings: start"
<< "container=" << static_cast<int>(container) << "host=" << credentials.hostName
<< "transport=" << srv.transport << "security=" << srv.security << "port=" << srv.port
<< "appendClient=" << appendNewClient;
const QString flowValue = srv.flow;
QString realityPublicKey;
QString realityShortId;
if (srv.security == QLatin1String("reality")) {
errorCode = readRealityKeyFiles(container, credentials, realityPublicKey, realityShortId);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Xray applyServerSettings: readRealityKeyFiles failed, error="
<< static_cast<int>(errorCode);
return errorCode;
}
}
QString currentConfig = m_sshSession->getTextFileFromContainer(
container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Xray applyServerSettings: getTextFileFromContainer failed, error="
<< static_cast<int>(errorCode) << "path=" << amnezia::protocols::xray::serverConfigPath;
return errorCode;
}
logger.info() << "Xray applyServerSettings: read server config, bytes=" << currentConfig.size();
QJsonDocument doc = QJsonDocument::fromJson(currentConfig.toUtf8());
if (doc.isNull() || !doc.isObject()) {
logger.error() << "Failed to parse server config JSON";
return ErrorCode::XrayServerConfigInvalid;
}
QJsonObject serverConfig = doc.object();
if (!serverConfig.contains(amnezia::protocols::xray::inbounds)) {
logger.error() << "Server config missing 'inbounds' field";
return ErrorCode::XrayServerConfigInvalid;
}
QJsonArray inbounds = serverConfig[amnezia::protocols::xray::inbounds].toArray();
if (inbounds.isEmpty()) {
logger.error() << "Server config has empty 'inbounds' array";
return ErrorCode::XrayServerConfigInvalid;
}
QJsonObject inbound = inbounds[0].toObject();
if (!inbound.contains(amnezia::protocols::xray::settings)) {
logger.error() << "Inbound missing 'settings' field";
return ErrorCode::XrayServerConfigInvalid;
}
const QJsonObject existingStream = inbound[amnezia::protocols::xray::streamSettings].toObject();
inbound[amnezia::protocols::xray::streamSettings] = mergeStreamSettingsForServerInbound(srv, existingStream);
if (!srv.port.isEmpty()) {
inbound[amnezia::protocols::xray::port] = srv.port.toInt();
}
QJsonObject settings = inbound[amnezia::protocols::xray::settings].toObject();
if (!settings.contains(amnezia::protocols::xray::clients)) {
settings[amnezia::protocols::xray::clients] = QJsonArray {};
}
QJsonArray clients = settings[amnezia::protocols::xray::clients].toArray();
QString clientId;
if (appendNewClient) {
clientId = QUuid::createUuid().toString(QUuid::WithoutBraces);
QJsonObject clientEntry;
clientEntry[amnezia::protocols::xray::id] = clientId;
if (!flowValue.isEmpty()) {
clientEntry[amnezia::protocols::xray::flow] = flowValue;
}
clients.append(clientEntry);
} else {
if (clients.isEmpty()) {
logger.error() << "Server config has no VLESS clients";
return ErrorCode::XrayServerNoVlessClients;
}
clientId = clients[0].toObject()[amnezia::protocols::xray::id].toString();
if (clientId.isEmpty()) {
logger.error() << "Server config VLESS client has empty id";
return ErrorCode::XrayServerNoVlessClients;
}
QJsonArray updatedClients;
for (const QJsonValue &v : clients) {
QJsonObject c = v.toObject();
if (flowValue.isEmpty()) {
c.remove(amnezia::protocols::xray::flow);
} else {
c[amnezia::protocols::xray::flow] = flowValue;
}
updatedClients.append(c);
}
clients = updatedClients;
}
settings[amnezia::protocols::xray::clients] = clients;
inbound[amnezia::protocols::xray::settings] = settings;
inbounds[0] = inbound;
serverConfig[amnezia::protocols::xray::inbounds] = inbounds;
errorCode = uploadServerConfigJson(credentials, container, dnsSettings, serverConfig);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Xray applyServerSettings: upload/restart failed, error=" << static_cast<int>(errorCode);
return errorCode;
}
logger.info() << "Xray applyServerSettings: server config uploaded and container restarted";
if (outClientId) {
*outClientId = clientId;
}
XrayProtocolConfig updated =
buildClientProtocolConfig(credentials, container, srv, clientId, errorCode, realityPublicKey, realityShortId);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Xray applyServerSettings: buildClientProtocolConfig failed, error="
<< static_cast<int>(errorCode);
return errorCode;
}
containerConfig.protocolConfig = updated;
logger.info() << "Xray applyServerSettings: done, clientId=" << clientId;
return ErrorCode::NoError;
}
QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentials, DockerContainer container,
const ContainerConfig &containerConfig,
const DnsSettings &dnsSettings,
ErrorCode &errorCode)
{
// Generate new UUID for client
QString clientId = QUuid::createUuid().toString(QUuid::WithoutBraces);
// Get current server config
QString currentConfig = m_sshSession->getTextFileFromContainer(
container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Failed to get server config file";
return "";
ContainerConfig mutableConfig = containerConfig;
QString clientId;
const ErrorCode applyError =
applyServerSettingsToRemote(credentials, container, mutableConfig, dnsSettings, true, &clientId);
errorCode = applyError;
if (applyError != ErrorCode::NoError || clientId.isEmpty()) {
return QString();
}
// 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(amnezia::protocols::xray::inbounds)) {
logger.error() << "Server config missing 'inbounds' field";
errorCode = ErrorCode::InternalError;
return "";
}
QJsonArray inbounds = serverConfig[amnezia::protocols::xray::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(amnezia::protocols::xray::settings)) {
logger.error() << "Inbound missing 'settings' field";
errorCode = ErrorCode::InternalError;
return "";
}
QJsonObject settings = inbound[amnezia::protocols::xray::settings].toObject();
if (!settings.contains(amnezia::protocols::xray::clients)) {
logger.error() << "Settings missing 'clients' field";
errorCode = ErrorCode::InternalError;
return "";
}
QJsonArray clients = settings[amnezia::protocols::xray::clients].toArray();
// Create configuration for new client
QJsonObject clientConfig {
{amnezia::protocols::xray::id, clientId},
{amnezia::protocols::xray::flow, "xtls-rprx-vision"}
};
clients.append(clientConfig);
// Update config
settings[amnezia::protocols::xray::clients] = clients;
inbound[amnezia::protocols::xray::settings] = settings;
inbounds[0] = inbound;
serverConfig[amnezia::protocols::xray::inbounds] = inbounds;
// Save updated config to server
QString updatedConfig = QJsonDocument(serverConfig).toJson();
errorCode = m_sshSession->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_sshSession->runScript(
credentials,
m_sshSession->replaceVars(restartScript, amnezia::genBaseVars(credentials, container, dnsSettings.primaryDns, dnsSettings.secondaryDns))
);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Failed to restart container";
return "";
}
return clientId;
}
ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container,
const ContainerConfig &containerConfig,
const DnsSettings &dnsSettings,
ErrorCode &errorCode)
XrayProtocolConfig XrayConfigurator::buildClientProtocolConfig(const ServerCredentials &credentials,
DockerContainer container,
const XrayServerConfig &srv, const QString &clientId,
ErrorCode &errorCode,
const QString &prefetchedRealityPublicKey,
const QString &prefetchedRealityShortId) const
{
const XrayServerConfig* serverConfig = nullptr;
if (auto* xrayConfig = containerConfig.protocolConfig.as<XrayProtocolConfig>()) {
serverConfig = &xrayConfig->serverConfig;
QString xrayPublicKey = prefetchedRealityPublicKey;
QString xrayShortId = prefetchedRealityShortId;
if (srv.security == QLatin1String("reality")) {
if (xrayPublicKey.isEmpty() || xrayShortId.isEmpty()) {
errorCode = readRealityKeyFiles(container, credentials, xrayPublicKey, xrayShortId);
if (errorCode != ErrorCode::NoError) {
return {};
}
}
}
QJsonObject userObj;
userObj[amnezia::protocols::xray::id] = clientId;
userObj[amnezia::protocols::xray::encryption] = QStringLiteral("none");
if (!srv.flow.isEmpty()) {
userObj[amnezia::protocols::xray::flow] = srv.flow;
}
QJsonObject vnextEntry;
vnextEntry[amnezia::protocols::xray::address] = credentials.hostName;
vnextEntry[amnezia::protocols::xray::port] =
srv.port.isEmpty() ? QString(amnezia::protocols::xray::defaultPort).toInt() : srv.port.toInt();
vnextEntry[amnezia::protocols::xray::users] = QJsonArray { userObj };
QJsonObject outboundSettings;
outboundSettings[amnezia::protocols::xray::vnext] = QJsonArray { vnextEntry };
QJsonObject outbound;
outbound[QStringLiteral("protocol")] = QStringLiteral("vless");
outbound[amnezia::protocols::xray::settings] = outboundSettings;
QJsonObject streamObj = buildStreamSettings(srv, clientId);
if (srv.security == QLatin1String("reality")) {
QJsonObject rs = streamObj[amnezia::protocols::xray::realitySettings].toObject();
rs[amnezia::protocols::xray::publicKey] = xrayPublicKey;
rs[amnezia::protocols::xray::shortId] = xrayShortId;
rs[amnezia::protocols::xray::spiderX] = QString();
streamObj[amnezia::protocols::xray::realitySettings] = rs;
}
outbound[amnezia::protocols::xray::streamSettings] = streamObj;
QJsonObject inboundObj;
inboundObj[QStringLiteral("listen")] = amnezia::protocols::xray::defaultLocalListenAddr;
inboundObj[amnezia::protocols::xray::port] = amnezia::protocols::xray::defaultLocalProxyPort;
inboundObj[QStringLiteral("protocol")] = QStringLiteral("socks");
inboundObj[amnezia::protocols::xray::settings] = QJsonObject { { QStringLiteral("udp"), true } };
QJsonObject clientJson;
clientJson[QStringLiteral("log")] = QJsonObject { { QStringLiteral("loglevel"), QStringLiteral("error") } };
clientJson[amnezia::protocols::xray::inbounds] = QJsonArray { inboundObj };
clientJson[amnezia::protocols::xray::outbounds] = QJsonArray { outbound };
const QString config = QString::fromUtf8(QJsonDocument(clientJson).toJson(QJsonDocument::Compact));
XrayProtocolConfig protocolConfig;
protocolConfig.serverConfig = srv;
XrayClientConfig clientConfig;
clientConfig.nativeConfig = config;
clientConfig.localPort = QString(amnezia::protocols::xray::defaultLocalProxyPort);
clientConfig.id = clientId;
protocolConfig.setClientConfig(clientConfig);
return protocolConfig;
}
QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, const QString &clientId) const
{
QJsonObject streamSettings;
const auto &xhttp = srv.xhttp;
const auto &mkcp = srv.mkcp;
namespace px = amnezia::protocols::xray;
QString networkValue = QStringLiteral("tcp");
if (srv.transport == QLatin1String("xhttp"))
networkValue = QStringLiteral("xhttp");
else if (srv.transport == QLatin1String("mkcp"))
networkValue = QStringLiteral("kcp");
streamSettings[px::network] = networkValue;
streamSettings[px::security] = srv.security;
if (srv.security == QLatin1String("tls")) {
QJsonObject tlsSettings;
const QString sniEff = srv.sni.isEmpty() ? QString::fromLatin1(px::defaultSni) : srv.sni;
tlsSettings[px::serverName] = sniEff;
const QString alpnEff = srv.alpn.isEmpty() ? QString::fromLatin1(px::defaultAlpn) : srv.alpn;
QJsonArray alpnArray;
for (const QString &a : alpnEff.split(QLatin1Char(','))) {
const QString t = a.trimmed();
if (!t.isEmpty())
alpnArray.append(t);
}
if (!alpnArray.isEmpty())
tlsSettings[QStringLiteral("alpn")] = alpnArray;
const QString fpEff = srv.fingerprint.isEmpty() ? QString::fromLatin1(px::defaultFingerprint) : srv.fingerprint;
tlsSettings[px::fingerprint] = fpEff;
streamSettings[QStringLiteral("tlsSettings")] = tlsSettings;
}
if (srv.security == QLatin1String("reality")) {
QJsonObject realSettings;
const QString fpEff = srv.fingerprint.isEmpty() ? QString::fromLatin1(px::defaultFingerprint) : srv.fingerprint;
realSettings[px::fingerprint] = fpEff;
const QString sniEff = srv.sni.isEmpty() ? QString::fromLatin1(px::defaultSni) : srv.sni;
realSettings[px::serverName] = sniEff;
streamSettings[px::realitySettings] = realSettings;
}
// XHTTP — JSON must match Xray-core SplitHTTPConfig (flat xPadding fields, see transport_internet.go)
if (srv.transport == QLatin1String("xhttp")) {
QJsonObject xo;
const QString hostEff = xhttp.host.isEmpty() ? QString::fromLatin1(px::defaultXhttpHost) : xhttp.host;
xo[QStringLiteral("host")] = hostEff;
if (!xhttp.path.isEmpty())
xo[QStringLiteral("path")] = xhttp.path;
xo[QStringLiteral("mode")] = normalizeXhttpMode(xhttp.mode);
if (xhttp.headersTemplate.compare(QLatin1String("HTTP"), Qt::CaseInsensitive) == 0) {
QJsonObject headers;
headers[QStringLiteral("Host")] = hostEff;
xo[QStringLiteral("headers")] = headers;
}
const QString methodEff =
xhttp.uplinkMethod.isEmpty() ? QString::fromLatin1(px::defaultXhttpUplinkMethod) : xhttp.uplinkMethod;
xo[QStringLiteral("uplinkHTTPMethod")] = methodEff.toUpper();
xo[QStringLiteral("noGRPCHeader")] = xhttp.disableGrpc;
xo[QStringLiteral("noSSEHeader")] = xhttp.disableSse;
const QString sessPl = normalizeSessionSeqPlacement(xhttp.sessionPlacement);
if (!sessPl.isEmpty())
xo[QStringLiteral("sessionPlacement")] = sessPl;
const QString seqPl = normalizeSessionSeqPlacement(xhttp.seqPlacement);
if (!seqPl.isEmpty())
xo[QStringLiteral("seqPlacement")] = seqPl;
if (!xhttp.sessionKey.isEmpty())
xo[QStringLiteral("sessionKey")] = xhttp.sessionKey;
if (!xhttp.seqKey.isEmpty())
xo[QStringLiteral("seqKey")] = xhttp.seqKey;
xo[QStringLiteral("uplinkDataPlacement")] = normalizeUplinkDataPlacement(xhttp.uplinkDataPlacement);
if (!xhttp.uplinkDataKey.isEmpty())
xo[QStringLiteral("uplinkDataKey")] = xhttp.uplinkDataKey;
const QString ucs = xhttp.uplinkChunkSize.isEmpty() ? QString::fromLatin1(px::defaultXhttpUplinkChunkSize)
: xhttp.uplinkChunkSize;
if (!ucs.isEmpty() && ucs != QLatin1String("0")) {
const int v = ucs.toInt();
QJsonObject chunkR;
chunkR[QStringLiteral("from")] = v;
chunkR[QStringLiteral("to")] = v;
xo[QStringLiteral("uplinkChunkSize")] = chunkR;
}
if (!xhttp.scMaxBufferedPosts.isEmpty())
xo[QStringLiteral("scMaxBufferedPosts")] = xhttp.scMaxBufferedPosts.toLongLong();
putIntRangeIfAny(xo, "scMaxEachPostBytes", xhttp.scMaxEachPostBytesMin, xhttp.scMaxEachPostBytesMax,
px::defaultXhttpScMaxEachPostBytesMin, px::defaultXhttpScMaxEachPostBytesMax);
putIntRangeIfAny(xo, "scMinPostsIntervalMs", xhttp.scMinPostsIntervalMsMin, xhttp.scMinPostsIntervalMsMax,
px::defaultXhttpScMinPostsIntervalMsMin, px::defaultXhttpScMinPostsIntervalMsMax);
putIntRangeIfAny(xo, "scStreamUpServerSecs", xhttp.scStreamUpServerSecsMin, xhttp.scStreamUpServerSecsMax,
px::defaultXhttpScStreamUpServerSecsMin, px::defaultXhttpScStreamUpServerSecsMax);
const auto &pad = xhttp.xPadding;
xo[QStringLiteral("xPaddingObfsMode")] = pad.obfsMode;
if (pad.obfsMode) {
if (!pad.bytesMin.isEmpty() || !pad.bytesMax.isEmpty()) {
QJsonObject br;
const int fromV = pad.bytesMin.isEmpty() ? 1 : pad.bytesMin.toInt();
int toV = pad.bytesMax.isEmpty() ? 256 : pad.bytesMax.toInt();
if (toV < fromV)
toV = fromV;
br[QStringLiteral("from")] = fromV;
br[QStringLiteral("to")] = toV;
xo[QStringLiteral("xPaddingBytes")] = br;
}
xo[QStringLiteral("xPaddingKey")] = pad.key.isEmpty() ? QStringLiteral("x_padding") : pad.key;
xo[QStringLiteral("xPaddingHeader")] = pad.header.isEmpty() ? QStringLiteral("X-Padding") : pad.header;
xo[QStringLiteral("xPaddingPlacement")] = normalizeXPaddingPlacement(
pad.placement.isEmpty() ? QString::fromLatin1(px::defaultXPaddingPlacement) : pad.placement);
xo[QStringLiteral("xPaddingMethod")] = normalizeXPaddingMethod(
pad.method.isEmpty() ? QString::fromLatin1(px::defaultXPaddingMethod) : pad.method);
}
// xmux: Xray has no "enabled" flag; omit object when UI disables multiplex tuning.
if (xhttp.xmux.enabled) {
QJsonObject mux;
auto addMuxRange = [&](const char *key, const QString &a, const QString &b) {
if (a.isEmpty() && b.isEmpty())
return;
QJsonObject r;
r[QStringLiteral("from")] = a.isEmpty() ? 0 : a.toInt();
r[QStringLiteral("to")] = b.isEmpty() ? 0 : b.toInt();
mux[QString::fromUtf8(key)] = r;
};
addMuxRange("maxConcurrency", xhttp.xmux.maxConcurrencyMin, xhttp.xmux.maxConcurrencyMax);
addMuxRange("maxConnections", xhttp.xmux.maxConnectionsMin, xhttp.xmux.maxConnectionsMax);
addMuxRange("cMaxReuseTimes", xhttp.xmux.cMaxReuseTimesMin, xhttp.xmux.cMaxReuseTimesMax);
addMuxRange("hMaxRequestTimes", xhttp.xmux.hMaxRequestTimesMin, xhttp.xmux.hMaxRequestTimesMax);
addMuxRange("hMaxReusableSecs", xhttp.xmux.hMaxReusableSecsMin, xhttp.xmux.hMaxReusableSecsMax);
if (!xhttp.xmux.hKeepAlivePeriod.isEmpty())
mux[QStringLiteral("hKeepAlivePeriod")] = xhttp.xmux.hKeepAlivePeriod.toLongLong();
if (!mux.isEmpty())
xo[QStringLiteral("xmux")] = mux;
}
streamSettings[QStringLiteral("xhttpSettings")] = xo;
}
if (srv.transport == QLatin1String("mkcp")) {
QJsonObject kcpObj;
const QString ttiEff = mkcp.tti.isEmpty() ? QString::fromLatin1(px::defaultMkcpTti) : mkcp.tti;
const QString upEff = mkcp.uplinkCapacity.isEmpty() ? QString::fromLatin1(px::defaultMkcpUplinkCapacity)
: mkcp.uplinkCapacity;
const QString downEff = mkcp.downlinkCapacity.isEmpty() ? QString::fromLatin1(px::defaultMkcpDownlinkCapacity)
: mkcp.downlinkCapacity;
const QString rbufEff = mkcp.readBufferSize.isEmpty() ? QString::fromLatin1(px::defaultMkcpReadBufferSize)
: mkcp.readBufferSize;
const QString wbufEff = mkcp.writeBufferSize.isEmpty() ? QString::fromLatin1(px::defaultMkcpWriteBufferSize)
: mkcp.writeBufferSize;
kcpObj[QStringLiteral("tti")] = ttiEff.toInt();
kcpObj[QStringLiteral("uplinkCapacity")] = upEff.toInt();
kcpObj[QStringLiteral("downlinkCapacity")] = downEff.toInt();
kcpObj[QStringLiteral("readBufferSize")] = rbufEff.toInt();
kcpObj[QStringLiteral("writeBufferSize")] = wbufEff.toInt();
kcpObj[QStringLiteral("congestion")] = mkcp.congestion;
streamSettings[QStringLiteral("kcpSettings")] = kcpObj;
}
return streamSettings;
}
ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container,
const ContainerConfig &containerConfig,
const DnsSettings &dnsSettings,
ErrorCode &errorCode)
{
if (const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>()) {
if (xrayCfg->serverConfig.isThirdPartyConfig && xrayCfg->hasClientConfig()) {
logger.info() << "Xray createConfig: returning existing third-party client config without server SSH";
return *xrayCfg;
}
}
const XrayServerConfig *serverConfig = nullptr;
if (const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>()) {
serverConfig = &xrayCfg->serverConfig;
}
if (!serverConfig) {
logger.error() << "No XrayProtocolConfig found";
errorCode = ErrorCode::InternalError;
return XrayProtocolConfig{};
}
const XrayServerConfig &srv = *serverConfig;
QString xrayClientId = prepareServerConfig(credentials, container, containerConfig, dnsSettings, errorCode);
if (errorCode != ErrorCode::NoError || xrayClientId.isEmpty()) {
logger.error() << "Failed to prepare server config";
errorCode = ErrorCode::InternalError;
if (errorCode == ErrorCode::NoError) {
errorCode = ErrorCode::InternalError;
}
return XrayProtocolConfig{};
}
amnezia::ScriptVars vars = amnezia::genBaseVars(credentials, container, dnsSettings.primaryDns, dnsSettings.secondaryDns);
vars.append(amnezia::genProtocolVarsForContainer(container, containerConfig));
QString config = m_sshSession->replaceVars(amnezia::scriptData(ProtocolScriptType::xray_template, container), vars);
if (config.isEmpty()) {
logger.error() << "Failed to get config template";
errorCode = ErrorCode::InternalError;
return XrayProtocolConfig{};
}
QString xrayPublicKey =
m_sshSession->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 XrayProtocolConfig{};
}
xrayPublicKey.replace("\n", "");
QString xrayShortId =
m_sshSession->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 XrayProtocolConfig{};
}
xrayShortId.replace("\n", "");
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 XrayProtocolConfig{};
}
config.replace("$XRAY_CLIENT_ID", xrayClientId);
config.replace("$XRAY_PUBLIC_KEY", xrayPublicKey);
config.replace("$XRAY_SHORT_ID", xrayShortId);
XrayProtocolConfig protocolConfig;
if (serverConfig) {
protocolConfig.serverConfig = *serverConfig;
}
XrayClientConfig clientConfig;
clientConfig.nativeConfig = config;
clientConfig.localPort = "";
clientConfig.id = xrayClientId;
protocolConfig.setClientConfig(clientConfig);
return protocolConfig;
}
return buildClientProtocolConfig(credentials, container, srv, xrayClientId, errorCode);
}
@@ -2,11 +2,13 @@
#define XRAY_CONFIGURATOR_H
#include <QObject>
#include <QJsonObject>
#include "configuratorBase.h"
#include "core/utils/errorCodes.h"
#include "core/utils/routeModes.h"
#include "core/utils/commonStructs.h"
#include "core/models/protocols/xrayProtocolConfig.h"
class XrayConfigurator : public ConfiguratorBase
{
@@ -18,10 +20,42 @@ public:
const amnezia::DnsSettings &dnsSettings,
amnezia::ErrorCode &errorCode) override;
amnezia::ProtocolConfig processConfigWithLocalSettings(const amnezia::ConnectionSettings &settings,
amnezia::ProtocolConfig protocolConfig) override;
amnezia::ErrorCode applyServerSettingsToRemote(const amnezia::ServerCredentials &credentials,
amnezia::DockerContainer container,
amnezia::ContainerConfig &containerConfig,
const amnezia::DnsSettings &dnsSettings,
bool appendNewClient,
QString *outClientId = nullptr);
private:
QString prepareServerConfig(const amnezia::ServerCredentials &credentials, amnezia::DockerContainer container, const amnezia::ContainerConfig &containerConfig,
const amnezia::DnsSettings &dnsSettings,
amnezia::ErrorCode &errorCode);
amnezia::ErrorCode uploadServerConfigJson(const amnezia::ServerCredentials &credentials, amnezia::DockerContainer container,
const amnezia::DnsSettings &dnsSettings, const QJsonObject &serverConfig) const;
amnezia::XrayProtocolConfig buildClientProtocolConfig(const amnezia::ServerCredentials &credentials,
amnezia::DockerContainer container,
const amnezia::XrayServerConfig &srv,
const QString &clientId,
amnezia::ErrorCode &errorCode,
const QString &prefetchedRealityPublicKey = {},
const QString &prefetchedRealityShortId = {}) const;
amnezia::ErrorCode readRealityKeyFiles(amnezia::DockerContainer container,
const amnezia::ServerCredentials &credentials,
QString &outPublicKey,
QString &outShortId) const;
QJsonObject mergeStreamSettingsForServerInbound(const amnezia::XrayServerConfig &srv,
const QJsonObject &existingStreamSettings) const;
QJsonObject buildStreamSettings(const amnezia::XrayServerConfig &srv,
const QString &clientId) const;
};
#endif // XRAY_CONFIGURATOR_H
+62 -21
View File
@@ -1,51 +1,93 @@
#include "newsController.h"
#include "core/controllers/gatewayController.h"
#include "core/utils/api/apiEnums.h"
#include "core/repositories/secureServersRepository.h"
#include "core/utils/constants/apiKeys.h"
#include "core/utils/constants/apiConstants.h"
#include "core/utils/constants/configKeys.h"
#include <QtConcurrent/QtConcurrent>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QSet>
#include <QSharedPointer>
using namespace amnezia;
NewsController::NewsController(SecureAppSettingsRepository* appSettingsRepository,
ServersController* serversController)
: m_appSettingsRepository(appSettingsRepository), m_serversController(serversController)
NewsController::NewsController(SecureAppSettingsRepository *appSettingsRepository,
SecureServersRepository *serversRepository)
: m_appSettingsRepository(appSettingsRepository),
m_serversRepository(serversRepository)
{
}
QJsonObject NewsController::getServicesList() const
{
if (!m_serversRepository) {
return {};
}
QSet<QString> userCountryCodes;
QSet<QString> serviceTypes;
const QVector<QString> ids = m_serversRepository->orderedServerIds();
for (const QString &id : ids) {
const auto apiV2 = m_serversRepository->apiV2Config(id);
if (!apiV2.has_value()) {
continue;
}
if (!apiV2->apiConfig.userCountryCode.isEmpty()) {
userCountryCodes.insert(apiV2->apiConfig.userCountryCode);
}
const QString serviceType = apiV2->serviceType();
if (!serviceType.isEmpty()) {
serviceTypes.insert(serviceType);
}
}
if (userCountryCodes.isEmpty() && serviceTypes.isEmpty()) {
return {};
}
QJsonObject json;
QJsonArray userCountryCodesArray;
for (const QString &code : userCountryCodes) {
userCountryCodesArray.append(code);
}
json[apiDefs::key::userCountryCode] = userCountryCodesArray;
QJsonArray serviceTypesArray;
for (const QString &type : serviceTypes) {
serviceTypesArray.append(type);
}
json[apiDefs::key::serviceType] = serviceTypesArray;
return json;
}
QFuture<QPair<ErrorCode, QJsonArray>> NewsController::fetchNews()
{
if (!m_serversController) {
qWarning() << "ServersController is null, skip fetchNews";
if (!m_serversRepository) {
qWarning() << "SecureServersRepository is null, skip fetchNews";
return QtFuture::makeReadyFuture(qMakePair(ErrorCode::InternalError, QJsonArray()));
}
const auto stacks = m_serversController->gatewayStacks();
if (stacks.isEmpty()) {
const QJsonObject services = getServicesList();
if (services.isEmpty()) {
qDebug() << "No Gateway stacks, skip fetchNews";
return QtFuture::makeReadyFuture(qMakePair(ErrorCode::NoError, QJsonArray()));
}
auto gatewayController = QSharedPointer<GatewayController>::create(
m_appSettingsRepository->getGatewayEndpoint(),
m_appSettingsRepository->isDevGatewayEnv(),
apiDefs::requestTimeoutMsecs,
m_appSettingsRepository->isStrictKillSwitchEnabled());
m_appSettingsRepository->getGatewayEndpoint(),
m_appSettingsRepository->isDevGatewayEnv(),
apiDefs::requestTimeoutMsecs,
m_appSettingsRepository->isStrictKillSwitchEnabled());
QJsonObject payload;
payload.insert("locale", m_appSettingsRepository->getAppLanguage().name().split("_").first());
const QJsonObject stacksJson = stacks.toJson();
if (stacksJson.contains(apiDefs::key::userCountryCode)) {
payload.insert(apiDefs::key::userCountryCode, stacksJson.value(apiDefs::key::userCountryCode));
if (services.contains(apiDefs::key::userCountryCode)) {
payload.insert(apiDefs::key::userCountryCode, services.value(apiDefs::key::userCountryCode));
}
if (stacksJson.contains(apiDefs::key::serviceType)) {
payload.insert(apiDefs::key::serviceType, stacksJson.value(apiDefs::key::serviceType));
if (services.contains(apiDefs::key::serviceType)) {
payload.insert(apiDefs::key::serviceType, services.value(apiDefs::key::serviceType));
}
auto future = gatewayController->postAsync(QString("%1v1/news"), payload);
@@ -69,4 +111,3 @@ QFuture<QPair<ErrorCode, QJsonArray>> NewsController::fetchNews()
return qMakePair(ErrorCode::NoError, newsArray);
});
}
+6 -4
View File
@@ -3,26 +3,28 @@
#include <QFuture>
#include <QJsonArray>
#include <QJsonObject>
#include <QPair>
#include "core/utils/errorCodes.h"
#include "core/utils/routeModes.h"
#include "core/utils/commonStructs.h"
#include "core/repositories/secureAppSettingsRepository.h"
#include "core/controllers/serversController.h"
#include "core/repositories/secureServersRepository.h"
class NewsController
{
public:
explicit NewsController(SecureAppSettingsRepository* appSettingsRepository,
ServersController* serversController);
SecureServersRepository* serversRepository);
QFuture<QPair<ErrorCode, QJsonArray>> fetchNews();
private:
QJsonObject getServicesList() const;
SecureAppSettingsRepository* m_appSettingsRepository;
ServersController* m_serversController;
SecureServersRepository* m_serversRepository;
};
#endif // NEWSCONTROLLER_H
@@ -11,7 +11,7 @@
#include <limits>
#include "core/controllers/gatewayController.h"
#include "core/utils/api/apiEnums.h"
#include "core/utils/serverConfigUtils.h"
#include "core/utils/constants/apiKeys.h"
#include "core/utils/constants/apiConstants.h"
#include "version.h"
@@ -5,6 +5,7 @@
#include <QEventLoop>
#include <QFutureWatcher>
#include <QJsonDocument>
#include <QJsonObject>
#include <QPromise>
#include <QSet>
#include <QSysInfo>
@@ -16,7 +17,7 @@
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
#include "core/utils/api/apiEnums.h"
#include "core/utils/serverConfigUtils.h"
#include "core/utils/constants/apiKeys.h"
#include "core/utils/constants/apiConstants.h"
#include "core/utils/api/apiUtils.h"
@@ -26,7 +27,6 @@
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
#include "version.h"
#include "core/models/serverConfig.h"
#include "core/models/containerConfig.h"
#include "core/models/api/apiConfig.h"
@@ -196,7 +196,7 @@ void SubscriptionController::updateApiConfigInJson(QJsonObject &serverConfigJson
apiConfig[apiDefs::key::serviceProtocol] = serviceProtocol;
apiConfig[apiDefs::key::userCountryCode] = userCountryCode;
if (serverConfigJson.value(configKey::configVersion).toInt() == apiDefs::ConfigSource::AmneziaGateway) {
if (serverConfigJson.value(configKey::configVersion).toInt() == serverConfigUtils::ConfigSource::AmneziaGateway) {
QJsonObject responseObj = QJsonDocument::fromJson(apiResponseBody).object();
if (responseObj.contains(apiDefs::key::supportedProtocols)) {
apiConfig.insert(apiDefs::key::supportedProtocols, responseObj.value(apiDefs::key::supportedProtocols).toArray());
@@ -218,7 +218,7 @@ ErrorCode SubscriptionController::executeRequest(const QString &endpoint, const
ErrorCode SubscriptionController::importServiceFromGateway(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol, const ProtocolData &protocolData,
ServerConfig &serverConfig)
CaptchaInfo &captchaInfo)
{
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
@@ -235,6 +235,19 @@ ErrorCode SubscriptionController::importServiceFromGateway(const QString &userCo
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody);
if (errorCode == ErrorCode::ApiCaptchaRequiredError) {
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
if (jsonDoc.isObject()) {
QJsonObject jsonObj = jsonDoc.object();
captchaInfo.captchaId = jsonObj.value("captcha_id").toString();
captchaInfo.captchaImageBase64 = jsonObj.value("captcha_image").toString();
captchaInfo.hint = jsonObj.value("hint").toString();
captchaInfo.isRequired = true;
}
return errorCode;
}
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
@@ -244,23 +257,21 @@ ErrorCode SubscriptionController::importServiceFromGateway(const QString &userCo
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
updateApiConfigInJson(serverConfigJson, serviceType, serviceProtocol, userCountryCode, responseBody);
ServerConfig serverConfigModel = ServerConfig::fromJson(serverConfigJson);
if (!serverConfigModel.isApiV2()) {
if (serverConfigJson.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
return ErrorCode::InternalError;
}
m_serversRepository->addServer(serverConfigModel);
serverConfig = serverConfigModel;
ApiV2ServerConfig apiV2ServerConfig = ApiV2ServerConfig::fromJson(serverConfigJson);
m_serversRepository->addServer(QString(), apiV2ServerConfig.toJson(),
serverConfigUtils::configTypeFromJson(apiV2ServerConfig.toJson()));
return ErrorCode::NoError;
}
ErrorCode SubscriptionController::importTrialFromGateway(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol, const QString &email,
ServerConfig &serverConfig)
const QString &serviceProtocol, const QString &email)
{
const QString trimmedEmail = email.trimmed();
if (trimmedEmail.isEmpty()) {
@@ -306,16 +317,19 @@ ErrorCode SubscriptionController::importTrialFromGateway(const QString &userCoun
}
QJsonObject configObject = QJsonDocument::fromJson(configBytes).object();
ServerConfig serverConfigModel = ServerConfig::fromJson(configObject);
m_serversRepository->addServer(serverConfigModel);
serverConfig = serverConfigModel;
if (configObject.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
return ErrorCode::InternalError;
}
ApiV2ServerConfig apiV2ServerConfig = ApiV2ServerConfig::fromJson(configObject);
m_serversRepository->addServer(QString(), apiV2ServerConfig.toJson(),
serverConfigUtils::configTypeFromJson(apiV2ServerConfig.toJson()));
return ErrorCode::NoError;
}
ErrorCode SubscriptionController::importServiceFromAppStore(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol, const ProtocolData &protocolData,
const QString &transactionId, bool isTestPurchase,
ServerConfig &serverConfig,
int *duplicateServerIndex)
{
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
@@ -351,15 +365,8 @@ ErrorCode SubscriptionController::importServiceFromAppStore(const QString &userC
// Check if server with this VPN key already exists
for (int i = 0; i < m_serversRepository->serversCount(); ++i) {
ServerConfig existingServerConfig = m_serversRepository->server(i);
QString existingVpnKey;
if (existingServerConfig.isApiV1()) {
const ApiV1ServerConfig* apiV1 = existingServerConfig.as<ApiV1ServerConfig>();
existingVpnKey = apiV1 ? apiV1->vpnKey() : QString();
} else if (existingServerConfig.isApiV2()) {
const ApiV2ServerConfig* apiV2 = existingServerConfig.as<ApiV2ServerConfig>();
existingVpnKey = apiV2 ? apiV2->vpnKey() : QString();
}
const auto apiV2 = m_serversRepository->apiV2Config(m_serversRepository->serverIdAt(i));
QString existingVpnKey = apiV2.has_value() ? apiV2->vpnKey() : QString();
existingVpnKey.replace(QStringLiteral("vpn://"), QString());
if (!existingVpnKey.isEmpty() && existingVpnKey == normalizedKey) {
if (duplicateServerIndex) {
@@ -385,38 +392,28 @@ ErrorCode SubscriptionController::importServiceFromAppStore(const QString &userC
quint16 crc = qChecksum(QJsonDocument(configObject).toJson());
ServerConfig serverConfigModel = ServerConfig::fromJson(configObject);
if (!serverConfigModel.isApiV2()) {
if (configObject.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
return ErrorCode::InternalError;
}
ApiV2ServerConfig* apiV2 = serverConfigModel.as<ApiV2ServerConfig>();
if (!apiV2) {
return ErrorCode::InternalError;
}
ApiV2ServerConfig apiV2ServerConfig = ApiV2ServerConfig::fromJson(configObject);
ApiV2ServerConfig* apiV2 = &apiV2ServerConfig;
apiV2->apiConfig.vpnKey = normalizedKey;
apiV2->apiConfig.isTestPurchase = isTestPurchase;
apiV2->apiConfig.isInAppPurchase = true;
apiV2->apiConfig.subscriptionExpiredByServer = false;
apiV2->crc = crc;
m_serversRepository->addServer(serverConfigModel);
serverConfig = serverConfigModel;
m_serversRepository->addServer(QString(), apiV2ServerConfig.toJson(),
serverConfigUtils::configTypeFromJson(apiV2ServerConfig.toJson()));
return ErrorCode::NoError;
}
ErrorCode SubscriptionController::updateServiceFromGateway(int serverIndex, const QString &newCountryCode, bool isConnectEvent)
ErrorCode SubscriptionController::updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, bool isConnectEvent)
{
ServerConfig serverConfigModel = m_serversRepository->server(serverIndex);
if (!serverConfigModel.isApiV2()) {
return ErrorCode::InternalError;
}
const ApiV2ServerConfig* apiV2 = serverConfigModel.as<ApiV2ServerConfig>();
if (!apiV2) {
auto apiV2 = m_serversRepository->apiV2Config(serverId);
if (!apiV2.has_value()) {
return ErrorCode::InternalError;
}
const bool isTestPurchase = apiV2->apiConfig.isTestPurchase;
@@ -445,12 +442,10 @@ ErrorCode SubscriptionController::updateServiceFromGateway(int serverIndex, cons
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody, isTestPurchase);
if (errorCode != ErrorCode::NoError) {
if (errorCode == ErrorCode::ApiSubscriptionExpiredError && !apiV2->apiConfig.isInAppPurchase) {
ServerConfig expiredServerConfig = serverConfigModel;
ApiV2ServerConfig *expiredApiV2 = expiredServerConfig.as<ApiV2ServerConfig>();
if (expiredApiV2) {
expiredApiV2->apiConfig.subscriptionExpiredByServer = true;
m_serversRepository->editServer(serverIndex, expiredServerConfig);
}
ApiV2ServerConfig expiredApiV2 = *apiV2;
expiredApiV2.apiConfig.subscriptionExpiredByServer = true;
m_serversRepository->editServer(serverId, expiredApiV2.toJson(),
serverConfigUtils::configTypeFromJson(expiredApiV2.toJson()));
}
return errorCode;
}
@@ -463,16 +458,12 @@ ErrorCode SubscriptionController::updateServiceFromGateway(int serverIndex, cons
updateApiConfigInJson(serverConfigJson, apiV2->apiConfig.serviceType, serviceProtocol, apiV2->apiConfig.userCountryCode, responseBody);
ServerConfig newServerConfigModel = ServerConfig::fromJson(serverConfigJson);
if (!newServerConfigModel.isApiV2()) {
if (serverConfigJson.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
return ErrorCode::InternalError;
}
ApiV2ServerConfig* newApiV2 = newServerConfigModel.as<ApiV2ServerConfig>();
if (!newApiV2) {
return ErrorCode::InternalError;
}
ApiV2ServerConfig newApiV2Config = ApiV2ServerConfig::fromJson(serverConfigJson);
ApiV2ServerConfig* newApiV2 = &newApiV2Config;
newApiV2->apiConfig.vpnKey = apiV2->apiConfig.vpnKey;
newApiV2->apiConfig.isTestPurchase = apiV2->apiConfig.isTestPurchase;
@@ -484,23 +475,19 @@ ErrorCode SubscriptionController::updateServiceFromGateway(int serverIndex, cons
if (apiV2->nameOverriddenByUser) {
newApiV2->name = apiV2->name;
newApiV2->displayName = apiV2->displayName;
newApiV2->nameOverriddenByUser = true;
}
m_serversRepository->editServer(serverIndex, newServerConfigModel);
m_serversRepository->editServer(serverId, newApiV2Config.toJson(),
serverConfigUtils::configTypeFromJson(newApiV2Config.toJson()));
return ErrorCode::NoError;
}
ErrorCode SubscriptionController::deactivateDevice(int serverIndex)
ErrorCode SubscriptionController::deactivateDevice(const QString &serverId)
{
ServerConfig serverConfigModel = m_serversRepository->server(serverIndex);
if (!serverConfigModel.isApiV2()) {
return ErrorCode::NoError;
}
const ApiV2ServerConfig* apiV2 = serverConfigModel.as<ApiV2ServerConfig>();
if (!apiV2) {
auto apiV2 = m_serversRepository->apiV2Config(serverId);
if (!apiV2.has_value()) {
return ErrorCode::NoError;
}
@@ -528,23 +515,16 @@ ErrorCode SubscriptionController::deactivateDevice(int serverIndex)
return errorCode;
}
serverConfigModel.visit([](auto& arg) {
arg.containers.clear();
});
m_serversRepository->editServer(serverIndex, serverConfigModel);
apiV2->containers.clear();
m_serversRepository->editServer(serverId, apiV2->toJson(),
serverConfigUtils::configTypeFromJson(apiV2->toJson()));
return ErrorCode::NoError;
}
ErrorCode SubscriptionController::deactivateExternalDevice(int serverIndex, const QString &uuid, const QString &serverCountryCode)
ErrorCode SubscriptionController::deactivateExternalDevice(const QString &serverId, const QString &uuid, const QString &serverCountryCode)
{
ServerConfig serverConfigModel = m_serversRepository->server(serverIndex);
if (!serverConfigModel.isApiV2()) {
return ErrorCode::NoError;
}
const ApiV2ServerConfig* apiV2 = serverConfigModel.as<ApiV2ServerConfig>();
if (!apiV2) {
auto apiV2 = m_serversRepository->apiV2Config(serverId);
if (!apiV2.has_value()) {
return ErrorCode::NoError;
}
@@ -573,25 +553,18 @@ ErrorCode SubscriptionController::deactivateExternalDevice(int serverIndex, cons
}
if (uuid == m_appSettingsRepository->getInstallationUuid(true)) {
serverConfigModel.visit([](auto& arg) {
arg.containers.clear();
});
m_serversRepository->editServer(serverIndex, serverConfigModel);
apiV2->containers.clear();
m_serversRepository->editServer(serverId, apiV2->toJson(),
serverConfigUtils::configTypeFromJson(apiV2->toJson()));
}
return ErrorCode::NoError;
}
ErrorCode SubscriptionController::exportNativeConfig(int serverIndex, const QString &serverCountryCode, QString &nativeConfig)
ErrorCode SubscriptionController::exportNativeConfig(const QString &serverId, const QString &serverCountryCode, QString &nativeConfig)
{
ServerConfig serverConfigModel = m_serversRepository->server(serverIndex);
if (!serverConfigModel.isApiV2()) {
return ErrorCode::InternalError;
}
const ApiV2ServerConfig* apiV2 = serverConfigModel.as<ApiV2ServerConfig>();
if (!apiV2) {
auto apiV2 = m_serversRepository->apiV2Config(serverId);
if (!apiV2.has_value()) {
return ErrorCode::InternalError;
}
const bool isTestPurchase = apiV2->apiConfig.isTestPurchase;
@@ -624,16 +597,10 @@ ErrorCode SubscriptionController::exportNativeConfig(int serverIndex, const QStr
return ErrorCode::NoError;
}
ErrorCode SubscriptionController::revokeNativeConfig(int serverIndex, const QString &serverCountryCode)
ErrorCode SubscriptionController::revokeNativeConfig(const QString &serverId, const QString &serverCountryCode)
{
ServerConfig serverConfigModel = m_serversRepository->server(serverIndex);
if (!serverConfigModel.isApiV2()) {
return ErrorCode::InternalError;
}
const ApiV2ServerConfig* apiV2 = serverConfigModel.as<ApiV2ServerConfig>();
if (!apiV2) {
auto apiV2 = m_serversRepository->apiV2Config(serverId);
if (!apiV2.has_value()) {
return ErrorCode::InternalError;
}
const bool isTestPurchase = apiV2->apiConfig.isTestPurchase;
@@ -661,126 +628,54 @@ ErrorCode SubscriptionController::revokeNativeConfig(int serverIndex, const QStr
return ErrorCode::NoError;
}
ErrorCode SubscriptionController::updateServiceFromTelegram(int serverIndex)
ErrorCode SubscriptionController::prepareVpnKeyExport(const QString &serverId, QString &vpnKey)
{
ServerConfig serverConfigModel = m_serversRepository->server(serverIndex);
if (!serverConfigModel.isApiV1()) {
return ErrorCode::InternalError;
}
const ApiV1ServerConfig* apiV1 = serverConfigModel.as<ApiV1ServerConfig>();
if (!apiV1) {
return ErrorCode::InternalError;
}
QString serviceProtocol = apiV1->protocol;
ProtocolData protocolData = generateProtocolData(serviceProtocol);
QString installationUuid = m_appSettingsRepository->getInstallationUuid(true);
GatewayController gatewayController(m_appSettingsRepository->getGatewayEndpoint(), m_appSettingsRepository->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs,
m_appSettingsRepository->isStrictKillSwitchEnabled());
QJsonObject apiPayload;
appendProtocolDataToApiPayload(serviceProtocol, protocolData, apiPayload);
apiPayload[apiDefs::key::uuid] = installationUuid;
apiPayload[apiDefs::key::osVersion] = QSysInfo::productType();
apiPayload[apiDefs::key::appVersion] = QString(APP_VERSION);
apiPayload[configKey::accessToken] = apiV1->apiKey;
apiPayload[apiDefs::key::apiEndpoint] = apiV1->apiEndpoint;
QByteArray responseBody;
ErrorCode errorCode = gatewayController.post(QString("%1v1/proxy_config"), apiPayload, responseBody);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
QJsonObject serverConfigJson;
errorCode = extractServerConfigJsonFromResponse(responseBody, serviceProtocol, protocolData, serverConfigJson);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
ServerConfig newServerConfigModel = ServerConfig::fromJson(serverConfigJson);
if (!newServerConfigModel.isApiV1()) {
return ErrorCode::InternalError;
}
ApiV1ServerConfig* newApiV1 = newServerConfigModel.as<ApiV1ServerConfig>();
if (!newApiV1) {
return ErrorCode::InternalError;
}
newApiV1->apiKey = apiV1->apiKey;
newApiV1->apiEndpoint = apiV1->apiEndpoint;
newApiV1->crc = apiV1->crc;
m_serversRepository->editServer(serverIndex, newServerConfigModel);
return ErrorCode::NoError;
}
ErrorCode SubscriptionController::prepareVpnKeyExport(int serverIndex, QString &vpnKey)
{
ServerConfig serverConfigModel = m_serversRepository->server(serverIndex);
if (serverConfigModel.isApiV1()) {
const ApiV1ServerConfig* apiV1 = serverConfigModel.as<ApiV1ServerConfig>();
vpnKey = apiV1 ? apiV1->vpnKey() : QString();
} else if (serverConfigModel.isApiV2()) {
ApiV2ServerConfig* apiV2 = serverConfigModel.as<ApiV2ServerConfig>();
vpnKey = apiV2 ? apiV2->vpnKey() : QString();
if (vpnKey.isEmpty()) {
QJsonObject serverJson = serverConfigModel.toJson();
vpnKey = apiUtils::getPremiumV2VpnKey(serverJson);
if (vpnKey.isEmpty()) {
return ErrorCode::ApiConfigEmptyError;
}
apiV2->apiConfig.vpnKey = vpnKey;
m_serversRepository->editServer(serverIndex, serverConfigModel);
}
} else {
auto apiV2 = m_serversRepository->apiV2Config(serverId);
if (!apiV2.has_value()) {
return ErrorCode::ApiConfigEmptyError;
}
vpnKey = apiV2->vpnKey();
if (vpnKey.isEmpty()) {
vpnKey = apiUtils::getPremiumV2VpnKey(apiV2->toJson());
if (vpnKey.isEmpty()) {
return ErrorCode::ApiConfigEmptyError;
}
apiV2->apiConfig.vpnKey = vpnKey;
m_serversRepository->editServer(serverId, apiV2->toJson(),
serverConfigUtils::configTypeFromJson(apiV2->toJson()));
}
return ErrorCode::NoError;
}
ErrorCode SubscriptionController::validateAndUpdateConfig(int serverIndex, bool hasInstalledContainers)
ErrorCode SubscriptionController::validateAndUpdateConfig(const QString &serverId, bool hasInstalledContainers)
{
ServerConfig serverConfigModel = m_serversRepository->server(serverIndex);
apiDefs::ConfigSource configSource;
if (serverConfigModel.isApiV1()) {
configSource = apiDefs::ConfigSource::Telegram;
} else if (serverConfigModel.isApiV2()) {
configSource = apiDefs::ConfigSource::AmneziaGateway;
} else {
if (!m_serversRepository->apiV2Config(serverId).has_value()) {
return ErrorCode::NoError;
}
if (configSource == apiDefs::ConfigSource::Telegram && !hasInstalledContainers) {
removeApiConfig(serverIndex);
return updateServiceFromTelegram(serverIndex);
} else if (configSource == apiDefs::ConfigSource::AmneziaGateway && !hasInstalledContainers) {
return updateServiceFromGateway(serverIndex, "", true);
} else if (configSource && isApiKeyExpired(serverIndex)) {
qDebug() << "attempt to update api config by expires_at event";
if (configSource == apiDefs::ConfigSource::AmneziaGateway) {
return updateServiceFromGateway(serverIndex, "", true);
} else {
removeApiConfig(serverIndex);
return updateServiceFromTelegram(serverIndex);
}
if (!hasInstalledContainers) {
return updateServiceFromGateway(serverId, "", true);
}
if (isApiKeyExpired(serverId)) {
qDebug() << "attempt to update api config by expires_at event";
return updateServiceFromGateway(serverId, "", true);
}
return ErrorCode::NoError;
}
void SubscriptionController::removeApiConfig(int serverIndex)
void SubscriptionController::removeApiConfig(const QString &serverId)
{
ServerConfig serverConfigModel = m_serversRepository->server(serverIndex);
auto apiV2 = m_serversRepository->apiV2Config(serverId);
if (!apiV2.has_value()) {
return;
}
#if defined(Q_OS_IOS) || defined(MACOS_NE)
QString description = serverConfigModel.description();
QString hostName = serverConfigModel.hostName();
QString description = apiV2->description;
QString hostName = apiV2->hostName;
QString vpncName = QString("%1 (%2) %3")
.arg(description)
.arg(hostName)
@@ -789,34 +684,42 @@ void SubscriptionController::removeApiConfig(int serverIndex)
AmneziaVPN::removeVPNC(vpncName.toStdString());
#endif
serverConfigModel.visit([](auto& arg) {
arg.dns1.clear();
arg.dns2.clear();
arg.containers.clear();
arg.hostName.clear();
arg.defaultContainer = DockerContainer::None;
});
apiV2->dns1.clear();
apiV2->dns2.clear();
apiV2->containers.clear();
apiV2->hostName.clear();
apiV2->defaultContainer = DockerContainer::None;
apiV2->apiConfig.publicKey = ApiConfig::PublicKeyInfo{};
if (serverConfigModel.isApiV2()) {
ApiV2ServerConfig* apiV2 = serverConfigModel.as<ApiV2ServerConfig>();
if (apiV2) {
apiV2->apiConfig.publicKey = ApiConfig::PublicKeyInfo{};
}
}
m_serversRepository->editServer(serverIndex, serverConfigModel);
m_serversRepository->editServer(serverId, apiV2->toJson(),
serverConfigUtils::configTypeFromJson(apiV2->toJson()));
}
bool SubscriptionController::isApiKeyExpired(int serverIndex) const
bool SubscriptionController::removeServer(const QString &serverId)
{
ServerConfig serverConfigModel = m_serversRepository->server(serverIndex);
if (!serverConfigModel.isApiV2()) {
if (serverId.isEmpty()) {
return false;
}
const ApiV2ServerConfig* apiV2 = serverConfigModel.as<ApiV2ServerConfig>();
if (!apiV2) {
if (!m_serversRepository->apiV2Config(serverId).has_value()) {
qWarning().noquote() << "SubscriptionController::removeServer: not an Api V2 server, id" << serverId;
return false;
}
const ErrorCode revokeError = deactivateDevice(serverId);
if (revokeError != ErrorCode::NoError && revokeError != ErrorCode::ApiNotFoundError) {
qWarning().noquote() << "SubscriptionController::removeServer: deactivateDevice failed (error"
<< static_cast<int>(revokeError) << "); removing locally anyway.";
}
m_serversRepository->removeServer(serverId);
return true;
}
bool SubscriptionController::isApiKeyExpired(const QString &serverId) const
{
auto apiV2 = m_serversRepository->apiV2Config(serverId);
if (!apiV2.has_value()) {
return false;
}
const QString expiresAt = apiV2->apiConfig.publicKey.expiresAt;
@@ -833,31 +736,24 @@ bool SubscriptionController::isApiKeyExpired(int serverIndex) const
return false;
}
void SubscriptionController::setCurrentProtocol(int serverIndex, const QString &protocolName)
void SubscriptionController::setCurrentProtocol(const QString &serverId, const QString &protocolName)
{
ServerConfig serverConfigModel = m_serversRepository->server(serverIndex);
if (serverConfigModel.isApiV2()) {
ApiV2ServerConfig* apiV2 = serverConfigModel.as<ApiV2ServerConfig>();
if (apiV2) {
apiV2->apiConfig.serviceProtocol = protocolName;
}
m_serversRepository->editServer(serverIndex, serverConfigModel);
auto apiV2 = m_serversRepository->apiV2Config(serverId);
if (apiV2.has_value()) {
apiV2->apiConfig.serviceProtocol = protocolName;
m_serversRepository->editServer(serverId, apiV2->toJson(),
serverConfigUtils::configTypeFromJson(apiV2->toJson()));
}
}
bool SubscriptionController::isVlessProtocol(int serverIndex) const
bool SubscriptionController::isVlessProtocol(const QString &serverId) const
{
ServerConfig serverConfigModel = m_serversRepository->server(serverIndex);
if (serverConfigModel.isApiV2()) {
const ApiV2ServerConfig* apiV2 = serverConfigModel.as<ApiV2ServerConfig>();
return apiV2 && apiV2->serviceProtocol() == "vless";
}
return false;
auto apiV2 = m_serversRepository->apiV2Config(serverId);
return apiV2.has_value() && apiV2->serviceProtocol() == "vless";
}
ErrorCode SubscriptionController::processAppStorePurchase(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol, const QString &productId,
ServerConfig &serverConfig,
int *duplicateServerIndex)
{
#if defined(Q_OS_IOS) || defined(MACOS_NE)
@@ -891,13 +787,12 @@ ErrorCode SubscriptionController::processAppStorePurchase(const QString &userCou
ProtocolData protocolData = generateProtocolData(serviceProtocol);
return importServiceFromAppStore(userCountryCode, serviceType, serviceProtocol, protocolData,
originalTransactionId, isTestPurchase, serverConfig, duplicateServerIndex);
originalTransactionId, isTestPurchase, duplicateServerIndex);
#else
Q_UNUSED(userCountryCode);
Q_UNUSED(serviceType);
Q_UNUSED(serviceProtocol);
Q_UNUSED(productId);
Q_UNUSED(serverConfig);
return ErrorCode::ApiPurchaseError;
#endif
}
@@ -956,10 +851,9 @@ SubscriptionController::AppStoreRestoreResult SubscriptionController::processApp
<< "originalTransactionId =" << originalTransactionId << "productId =" << transactionProductId;
ProtocolData protocolData = generateProtocolData(serviceProtocol);
ServerConfig serverConfig;
int currentDuplicateServerIndex = -1;
ErrorCode errorCode = importServiceFromAppStore(userCountryCode, serviceType, serviceProtocol, protocolData,
originalTransactionId, isTestPurchase, serverConfig,
originalTransactionId, isTestPurchase,
&currentDuplicateServerIndex);
if (errorCode == ErrorCode::ApiConfigAlreadyAdded) {
@@ -991,16 +885,10 @@ SubscriptionController::AppStoreRestoreResult SubscriptionController::processApp
#endif
}
ErrorCode SubscriptionController::getAccountInfo(int serverIndex, QJsonObject &accountInfo)
ErrorCode SubscriptionController::getAccountInfo(const QString &serverId, QJsonObject &accountInfo)
{
ServerConfig serverConfigModel = m_serversRepository->server(serverIndex);
if (!serverConfigModel.isApiV2()) {
return ErrorCode::InternalError;
}
const ApiV2ServerConfig* apiV2 = serverConfigModel.as<ApiV2ServerConfig>();
if (!apiV2) {
auto apiV2 = m_serversRepository->apiV2Config(serverId);
if (!apiV2.has_value()) {
return ErrorCode::InternalError;
}
bool isTestPurchase = apiV2->apiConfig.isTestPurchase;
@@ -1030,20 +918,13 @@ ErrorCode SubscriptionController::getAccountInfo(int serverIndex, QJsonObject &a
return ErrorCode::NoError;
}
QFuture<QPair<ErrorCode, QString>> SubscriptionController::getRenewalLink(int serverIndex)
QFuture<QPair<ErrorCode, QString>> SubscriptionController::getRenewalLink(const QString &serverId)
{
auto promise = QSharedPointer<QPromise<QPair<ErrorCode, QString>>>::create();
promise->start();
ServerConfig serverConfigModel = m_serversRepository->server(serverIndex);
if (!serverConfigModel.isApiV2()) {
promise->addResult(qMakePair(ErrorCode::InternalError, QString()));
promise->finish();
return promise->future();
}
const ApiV2ServerConfig *apiV2 = serverConfigModel.as<ApiV2ServerConfig>();
if (!apiV2) {
auto apiV2 = m_serversRepository->apiV2Config(serverId);
if (!apiV2.has_value()) {
promise->addResult(qMakePair(ErrorCode::InternalError, QString()));
promise->finish();
return promise->future();
@@ -1090,3 +971,74 @@ QFuture<QPair<ErrorCode, QString>> SubscriptionController::getRenewalLink(int se
return promise->future();
}
ErrorCode SubscriptionController::resolveImportServiceCaptcha(const QString &userCountryCode,
const QString &serviceType,
const QString &serviceProtocol,
const ProtocolData &protocolData,
const QString &captchaId,
const QString &captchaSolution,
CaptchaInfo *retryCaptchaOut)
{
GatewayRequestData gatewayRequestData{QSysInfo::productType(),
QString(APP_VERSION),
m_appSettingsRepository->getAppLanguage().name().split("_").first(),
m_appSettingsRepository->getInstallationUuid(true),
userCountryCode,
"",
serviceType,
serviceProtocol,
QJsonObject()};
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
appendProtocolDataToApiPayload(serviceProtocol, protocolData, apiPayload);
apiPayload["captcha_id"] = captchaId;
QString normalizedSolution;
normalizedSolution.reserve(captchaSolution.size());
for (const QChar &ch : captchaSolution) {
const ushort u = ch.unicode();
if (u >= '0' && u <= '9') {
normalizedSolution += ch;
} else if (u >= 0xFF10 && u <= 0xFF19) {
normalizedSolution += QChar(static_cast<char16_t>(u - 0xFF10 + '0'));
}
}
apiPayload["captcha_solution"] = normalizedSolution.isEmpty() ? captchaSolution.trimmed() : normalizedSolution;
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody);
if (errorCode != ErrorCode::NoError) {
if (retryCaptchaOut
&& (errorCode == ErrorCode::ApiCaptchaInvalidError || errorCode == ErrorCode::ApiCaptchaRefreshError
|| errorCode == ErrorCode::ApiCaptchaRequiredError)) {
const QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
if (jsonDoc.isObject()) {
const QJsonObject jsonObj = jsonDoc.object();
if (jsonObj.contains(QStringLiteral("captcha_id")) && jsonObj.contains(QStringLiteral("captcha_image"))) {
retryCaptchaOut->captchaId = jsonObj.value(QStringLiteral("captcha_id")).toString();
retryCaptchaOut->captchaImageBase64 = jsonObj.value(QStringLiteral("captcha_image")).toString();
retryCaptchaOut->hint = jsonObj.value(QStringLiteral("hint")).toString();
retryCaptchaOut->isRequired = true;
}
}
}
return errorCode;
}
QJsonObject serverConfigJson;
errorCode = extractServerConfigJsonFromResponse(responseBody, serviceProtocol, protocolData, serverConfigJson);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
updateApiConfigInJson(serverConfigJson, serviceType, serviceProtocol, userCountryCode, responseBody);
if (serverConfigJson.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
return ErrorCode::InternalError;
}
ApiV2ServerConfig apiV2ServerConfig = ApiV2ServerConfig::fromJson(serverConfigJson);
m_serversRepository->addServer(QString(), apiV2ServerConfig.toJson(),
serverConfigUtils::configTypeFromJson(apiV2ServerConfig.toJson()));
return ErrorCode::NoError;
}
@@ -12,7 +12,6 @@
#include "core/utils/commonStructs.h"
#include "core/repositories/secureServersRepository.h"
#include "core/repositories/secureAppSettingsRepository.h"
#include "core/models/serverConfig.h"
class ServersController;
@@ -43,49 +42,53 @@ public:
QJsonObject toJsonObject() const;
};
struct CaptchaInfo {
QString captchaId;
QString captchaImageBase64;
QString hint;
bool isRequired = false;
};
explicit SubscriptionController(SecureServersRepository* serversRepository,
SecureAppSettingsRepository* appSettingsRepository);
ProtocolData generateProtocolData(const QString &protocol);
void appendProtocolDataToApiPayload(const QString &protocol, const ProtocolData &protocolData, QJsonObject &apiPayload);
ErrorCode fillServerConfig(const QJsonObject &serverConfigJson, ServerConfig &serverConfig);
ErrorCode importServiceFromGateway(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol, const ProtocolData &protocolData,
ServerConfig &serverConfig);
CaptchaInfo &captchaInfo);
ErrorCode importTrialFromGateway(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol, const QString &email,
ServerConfig &serverConfig);
const QString &serviceProtocol, const QString &email);
ErrorCode importServiceFromAppStore(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol, const ProtocolData &protocolData,
const QString &transactionId, bool isTestPurchase,
ServerConfig &serverConfig,
int *duplicateServerIndex = nullptr);
ErrorCode updateServiceFromGateway(int serverIndex, const QString &newCountryCode, bool isConnectEvent);
ErrorCode updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, bool isConnectEvent);
ErrorCode deactivateDevice(int serverIndex);
ErrorCode deactivateDevice(const QString &serverId);
ErrorCode deactivateExternalDevice(int serverIndex, const QString &uuid, const QString &serverCountryCode);
ErrorCode deactivateExternalDevice(const QString &serverId, const QString &uuid, const QString &serverCountryCode);
ErrorCode exportNativeConfig(int serverIndex, const QString &serverCountryCode, QString &nativeConfig);
ErrorCode exportNativeConfig(const QString &serverId, const QString &serverCountryCode, QString &nativeConfig);
ErrorCode revokeNativeConfig(int serverIndex, const QString &serverCountryCode);
ErrorCode revokeNativeConfig(const QString &serverId, const QString &serverCountryCode);
ErrorCode updateServiceFromTelegram(int serverIndex);
ErrorCode prepareVpnKeyExport(const QString &serverId, QString &vpnKey);
ErrorCode prepareVpnKeyExport(int serverIndex, QString &vpnKey);
ErrorCode validateAndUpdateConfig(const QString &serverId, bool hasInstalledContainers);
ErrorCode validateAndUpdateConfig(int serverIndex, bool hasInstalledContainers);
void removeApiConfig(const QString &serverId);
void removeApiConfig(int serverIndex);
bool removeServer(const QString &serverId);
void setCurrentProtocol(int serverIndex, const QString &protocolName);
bool isVlessProtocol(int serverIndex) const;
void setCurrentProtocol(const QString &serverId, const QString &protocolName);
bool isVlessProtocol(const QString &serverId) const;
ErrorCode getAccountInfo(int serverIndex, QJsonObject &accountInfo);
QFuture<QPair<ErrorCode, QString>> getRenewalLink(int serverIndex);
ErrorCode getAccountInfo(const QString &serverId, QJsonObject &accountInfo);
QFuture<QPair<ErrorCode, QString>> getRenewalLink(const QString &serverId);
struct AppStoreRestoreResult
{
@@ -98,15 +101,19 @@ public:
ErrorCode processAppStorePurchase(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol, const QString &productId,
ServerConfig &serverConfig,
int *duplicateServerIndex = nullptr);
AppStoreRestoreResult processAppStoreRestore(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol);
ErrorCode resolveImportServiceCaptcha(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol, const ProtocolData &protocolData,
const QString &captchaId, const QString &captchaSolution,
CaptchaInfo *retryCaptchaOut = nullptr);
private:
ErrorCode executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody, bool isTestPurchase = false);
bool isApiKeyExpired(int serverIndex) const;
bool isApiKeyExpired(const QString &serverId) const;
ErrorCode extractServerConfigJsonFromResponse(const QByteArray &apiResponseBody, const QString &protocol,
const ProtocolData &protocolData, QJsonObject &serverConfigJson);
+162 -22
View File
@@ -6,14 +6,12 @@
#include "core/utils/protocolEnum.h"
#include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/utilities.h"
#include "core/utils/networkUtilities.h"
#include "core/utils/serverConfigUtils.h"
#include "version.h"
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
#include "core/models/serverConfig.h"
#include "core/models/containerConfig.h"
#include "core/models/protocolConfig.h"
@@ -51,43 +49,183 @@ void ConnectionController::setConnectionState(Vpn::ConnectionState state)
}
}
ErrorCode ConnectionController::prepareConnection(int serverIndex,
QJsonObject& vpnConfiguration,
DockerContainer& container)
ErrorCode ConnectionController::defaultContainerForServer(const QString &serverId, DockerContainer &container) const
{
const auto kind = m_serversRepository->serverKind(serverId);
switch (kind) {
case serverConfigUtils::ConfigType::SelfHostedAdmin: {
const auto cfg = m_serversRepository->selfHostedAdminConfig(serverId);
if (!cfg.has_value()) {
return ErrorCode::InternalError;
}
container = cfg->defaultContainer;
return ErrorCode::NoError;
}
case serverConfigUtils::ConfigType::SelfHostedUser: {
const auto cfg = m_serversRepository->selfHostedUserConfig(serverId);
if (!cfg.has_value()) {
return ErrorCode::InternalError;
}
container = cfg->defaultContainer;
return ErrorCode::NoError;
}
case serverConfigUtils::ConfigType::Native: {
const auto cfg = m_serversRepository->nativeConfig(serverId);
if (!cfg.has_value()) {
return ErrorCode::InternalError;
}
container = cfg->defaultContainer;
return ErrorCode::NoError;
}
case serverConfigUtils::ConfigType::AmneziaPremiumV2:
case serverConfigUtils::ConfigType::AmneziaFreeV3:
case serverConfigUtils::ConfigType::ExternalPremium: {
const auto cfg = m_serversRepository->apiV2Config(serverId);
if (!cfg.has_value()) {
return ErrorCode::InternalError;
}
container = cfg->defaultContainer;
return ErrorCode::NoError;
}
case serverConfigUtils::ConfigType::AmneziaPremiumV1:
case serverConfigUtils::ConfigType::AmneziaFreeV2:
return ErrorCode::LegacyApiV1NotSupportedError;
case serverConfigUtils::ConfigType::Invalid:
default:
return ErrorCode::InternalError;
}
}
ErrorCode ConnectionController::isConnectionSupported(const QString &serverId) const
{
if (serverId.isEmpty()) {
return ErrorCode::InternalError;
}
if (!isServiceReady()) {
return ErrorCode::AmneziaServiceNotRunning;
}
ServerConfig serverConfigModel = m_serversRepository->server(serverIndex);
container = serverConfigModel.defaultContainer();
const serverConfigUtils::ConfigType kind = m_serversRepository->serverKind(serverId);
if (serverConfigUtils::isLegacyApiSubscription(kind)) {
return ErrorCode::LegacyApiV1NotSupportedError;
}
DockerContainer container = DockerContainer::None;
const ErrorCode errorCode = defaultContainerForServer(serverId, container);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
if (container == DockerContainer::None) {
if (serverConfigUtils::isApiV2Subscription(kind)) {
return ErrorCode::NoError;
}
return ErrorCode::NoInstalledContainersError;
}
if (ContainerUtils::isUnsupportedContainer(container)) {
return ErrorCode::LegacyContainerNotSupportedError;
}
if (!isContainerSupported(container)) {
return ErrorCode::NotSupportedOnThisPlatform;
}
ContainerConfig containerConfigModel = m_serversRepository->containerConfig(serverIndex, container);
return ErrorCode::NoError;
}
auto dns = serverConfigModel.getDnsPair(m_appSettingsRepository->useAmneziaDns(),
m_appSettingsRepository->primaryDns(),
m_appSettingsRepository->secondaryDns());
ErrorCode ConnectionController::prepareConnection(const QString &serverId,
QJsonObject& vpnConfiguration,
DockerContainer& container)
{
ContainerConfig containerConfigModel;
QPair<QString, QString> dns;
QString hostName;
QString description;
int configVersion = 0;
bool isApiConfig = false;
vpnConfiguration = createConnectionConfiguration(dns, serverConfigModel, containerConfigModel, container);
const auto kind = m_serversRepository->serverKind(serverId);
const QString primaryDns = m_appSettingsRepository->primaryDns();
const QString secondaryDns = m_appSettingsRepository->secondaryDns();
switch (kind) {
case serverConfigUtils::ConfigType::SelfHostedAdmin: {
const auto cfg = m_serversRepository->selfHostedAdminConfig(serverId);
if (!cfg.has_value()) return ErrorCode::InternalError;
container = cfg->defaultContainer;
containerConfigModel = cfg->containerConfig(container);
dns = cfg->getDnsPair(m_appSettingsRepository->useAmneziaDns(), primaryDns, secondaryDns);
hostName = cfg->hostName;
description = cfg->description;
break;
}
case serverConfigUtils::ConfigType::SelfHostedUser: {
const auto cfg = m_serversRepository->selfHostedUserConfig(serverId);
if (!cfg.has_value()) return ErrorCode::InternalError;
container = cfg->defaultContainer;
containerConfigModel = cfg->containerConfig(container);
dns = cfg->getDnsPair(primaryDns, secondaryDns);
hostName = cfg->hostName;
description = cfg->description;
break;
}
case serverConfigUtils::ConfigType::Native: {
const auto cfg = m_serversRepository->nativeConfig(serverId);
if (!cfg.has_value()) return ErrorCode::InternalError;
container = cfg->defaultContainer;
containerConfigModel = cfg->containerConfig(container);
dns = cfg->getDnsPair(primaryDns, secondaryDns);
hostName = cfg->hostName;
description = cfg->description;
break;
}
case serverConfigUtils::ConfigType::AmneziaPremiumV2:
case serverConfigUtils::ConfigType::AmneziaFreeV3:
case serverConfigUtils::ConfigType::ExternalPremium: {
const auto cfg = m_serversRepository->apiV2Config(serverId);
if (!cfg.has_value()) return ErrorCode::InternalError;
container = cfg->defaultContainer;
containerConfigModel = cfg->containerConfig(container);
dns = cfg->getDnsPair(primaryDns, secondaryDns);
hostName = cfg->hostName;
description = cfg->description;
configVersion = serverConfigUtils::ConfigSource::AmneziaGateway;
isApiConfig = true;
break;
}
case serverConfigUtils::ConfigType::AmneziaPremiumV1:
case serverConfigUtils::ConfigType::AmneziaFreeV2:
return ErrorCode::InternalError;
case serverConfigUtils::ConfigType::Invalid:
default:
return ErrorCode::InternalError;
}
vpnConfiguration = createConnectionConfiguration(dns, isApiConfig, hostName, description, configVersion,
containerConfigModel, container);
return ErrorCode::NoError;
}
ErrorCode ConnectionController::openConnection(int serverIndex)
ErrorCode ConnectionController::openConnection(const QString &serverId)
{
QJsonObject vpnConfiguration;
DockerContainer container;
ErrorCode errorCode = prepareConnection(serverIndex, vpnConfiguration, container);
ErrorCode errorCode = prepareConnection(serverId, vpnConfiguration, container);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
emit openConnectionRequested(serverIndex, container, vpnConfiguration);
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
if (m_appSettingsRepository && m_appSettingsRepository->isLocalProxyHttpEnabled()) {
m_appSettingsRepository->setLocalProxyHttpEnabled(false);
emit localProxyStoppedBecauseVpnTurnedOn(tr("Local proxy stopped because VPN was turned on"));
}
#endif
emit openConnectionRequested(serverId, container, vpnConfiguration);
return ErrorCode::NoError;
}
@@ -120,7 +258,10 @@ ErrorCode ConnectionController::lastConnectionError() const
}
QJsonObject ConnectionController::createConnectionConfiguration(const QPair<QString, QString> &dns,
const ServerConfig &serverConfig,
bool isApiConfig,
const QString &hostName,
const QString &description,
int configVersion,
const ContainerConfig &containerConfig,
DockerContainer container)
{
@@ -134,7 +275,7 @@ QJsonObject ConnectionController::createConnectionConfiguration(const QPair<QStr
ConnectionSettings connectionSettings = {
{ dns.first, dns.second },
serverConfig.isApiConfig(),
isApiConfig,
{
m_appSettingsRepository->isSitesSplitTunnelingEnabled(),
m_appSettingsRepository->routeMode()
@@ -160,10 +301,9 @@ QJsonObject ConnectionController::createConnectionConfiguration(const QPair<QStr
vpnConfiguration[configKey::dns1] = dns.first;
vpnConfiguration[configKey::dns2] = dns.second;
vpnConfiguration[configKey::hostName] = serverConfig.hostName();
vpnConfiguration[configKey::description] = serverConfig.description();
vpnConfiguration[configKey::configVersion] = serverConfig.configVersion();
vpnConfiguration[configKey::hostName] = hostName;
vpnConfiguration[configKey::description] = description;
vpnConfiguration[configKey::configVersion] = configVersion;
return vpnConfiguration;
}
+12 -4
View File
@@ -30,11 +30,13 @@ public:
QObject* parent = nullptr);
~ConnectionController() = default;
ErrorCode prepareConnection(int serverIndex,
ErrorCode prepareConnection(const QString &serverId,
QJsonObject& vpnConfiguration,
DockerContainer& container);
ErrorCode openConnection(int serverIndex);
ErrorCode isConnectionSupported(const QString &serverId) const;
ErrorCode openConnection(const QString &serverId);
void closeConnection();
@@ -50,7 +52,10 @@ public:
void setConnectionState(Vpn::ConnectionState state);
QJsonObject createConnectionConfiguration(const QPair<QString, QString> &dns,
const ServerConfig &serverConfig,
bool isApiConfig,
const QString &hostName,
const QString &description,
int configVersion,
const ContainerConfig &containerConfig,
DockerContainer container);
@@ -60,16 +65,19 @@ public:
signals:
void connectionStateChanged(Vpn::ConnectionState state);
void openConnectionRequested(int serverIndex, DockerContainer container, const QJsonObject &vpnConfiguration);
void openConnectionRequested(const QString &serverId, DockerContainer container, const QJsonObject &vpnConfiguration);
void closeConnectionRequested();
void setConnectionStateRequested(Vpn::ConnectionState state);
void killSwitchModeChangedRequested(bool enabled);
void localProxyStoppedBecauseVpnTurnedOn(const QString &message);
#ifdef Q_OS_ANDROID
void restoreConnectionRequested();
#endif
private:
ErrorCode defaultContainerForServer(const QString &serverId, DockerContainer &container) const;
SecureServersRepository* m_serversRepository;
SecureAppSettingsRepository* m_appSettingsRepository;
VpnConnection* m_vpnConnection;
+104 -10
View File
@@ -1,14 +1,16 @@
#include "coreController.h"
#include <QCoreApplication>
#include <QDirIterator>
#include <QDebug>
#include <QTranslator>
#include <QStandardPaths>
#include <QTimer>
#include "core/utils/selfhosted/sshSession.h"
#include "core/controllers/selfhosted/installController.h"
#include "core/controllers/selfhosted/importController.h"
#include "core/controllers/coreSignalHandlers.h"
#include "core/models/serverConfig.h"
#include "logger.h"
#include "secureQSettings.h"
@@ -36,6 +38,10 @@ CoreController::CoreController(const QSharedPointer<VpnConnection> &vpnConnectio
initAppleController();
initLogging();
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
initLocalProxy();
#endif
m_translator = new QTranslator(this);
if (m_appSettingsRepository) {
updateTranslator(m_appSettingsRepository->getAppLanguage());
@@ -49,6 +55,69 @@ void CoreController::setQmlContextProperty(const QString &name, QObject *value)
}
}
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
void CoreController::initLocalProxy()
{
constexpr quint16 kLocalProxyApiPort = 49490;
m_proxyServer.reset(new ProxyServer(m_serversRepository, m_appSettingsRepository, this));
QObject::connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() {
if (m_appSettingsRepository && m_appSettingsRepository->isLocalProxyHttpEnabled()) {
m_appSettingsRepository->setLocalProxyHttpEnabled(false);
}
});
auto syncLocalProxy = [this]() {
if (!m_proxyServer) {
return;
}
const bool httpEnabled = m_appSettingsRepository->isLocalProxyHttpEnabled();
if (!httpEnabled) {
qInfo() << "Local proxy: HTTP API disabled";
m_proxyServer->stop();
return;
}
if (!m_proxyServer->start(kLocalProxyApiPort)) {
qWarning() << "Local proxy: failed to start on port" << kLocalProxyApiPort;
m_appSettingsRepository->setLocalProxyHttpEnabled(false);
emit m_appSettingsRepository->localProxyStartFailed(tr("Local proxy failed to start. Check if the port is available."));
return;
}
if (!m_proxyServer->syncSettings()) {
qWarning() << "Local proxy: failed to start proxy core (Xray)";
m_appSettingsRepository->setLocalProxyHttpEnabled(false);
emit m_appSettingsRepository->localProxyStartFailed(tr("Couldn’t start the proxy due to an internal error. Try restarting the app."));
return;
}
qInfo() << "Local proxy: running on 127.0.0.1:" << kLocalProxyApiPort;
};
syncLocalProxy();
connect(m_appSettingsRepository, &SecureAppSettingsRepository::localProxySettingsChanged, this, syncLocalProxy);
connect(m_serversRepository, &SecureServersRepository::serverEdited, this, [this](const QString &serverId) {
if (m_appSettingsRepository && m_appSettingsRepository->isLocalProxyHttpEnabled()
&& m_appSettingsRepository->localProxyOwnerId() == serverId) {
m_appSettingsRepository->bumpLocalProxyRestartToken();
}
});
connect(m_serversRepository, &SecureServersRepository::serverRemoved, this, [this](const QString &serverId, int) {
if (m_appSettingsRepository && m_appSettingsRepository->localProxyOwnerId() == serverId) {
m_appSettingsRepository->setLocalProxyOwnerId(QString());
m_appSettingsRepository->setLocalProxyHttpEnabled(false);
}
});
}
#endif
void CoreController::initModels()
{
m_containersModel = new ContainersModel(this);
@@ -87,6 +156,9 @@ void CoreController::initModels()
m_xrayConfigModel = new XrayConfigModel(this);
setQmlContextProperty("XrayConfigModel", m_xrayConfigModel);
m_xrayConfigSnapshotsModel = new XrayConfigSnapshotsModel(m_appSettingsRepository, m_xrayConfigModel, this);
setQmlContextProperty("XrayConfigSnapshotsModel", m_xrayConfigSnapshotsModel);
m_torConfigModel = new TorConfigModel(this);
setQmlContextProperty("TorConfigModel", m_torConfigModel);
@@ -101,6 +173,12 @@ void CoreController::initModels()
m_socks5ConfigModel = new Socks5ProxyConfigModel(this);
setQmlContextProperty("Socks5ProxyConfigModel", m_socks5ConfigModel);
m_mtProxyConfigModel = new MtProxyConfigModel(this);
setQmlContextProperty("MtProxyConfigModel", m_mtProxyConfigModel);
m_telemtConfigModel = new TelemtConfigModel(this);
setQmlContextProperty("TelemtConfigModel", m_telemtConfigModel);
m_clientManagementModel = new ClientManagementModel(this);
setQmlContextProperty("ClientManagementModel", m_clientManagementModel);
@@ -145,7 +223,7 @@ void CoreController::initCoreControllers()
m_allowedDnsController = new AllowedDnsController(m_appSettingsRepository);
m_servicesCatalogController = new ServicesCatalogController(m_appSettingsRepository);
m_subscriptionController = new SubscriptionController(m_serversRepository, m_appSettingsRepository);
m_newsController = new NewsController(m_appSettingsRepository, m_serversController);
m_newsController = new NewsController(m_appSettingsRepository, m_serversRepository);
m_updateController = new UpdateController(m_appSettingsRepository, this);
m_installController = new InstallController(m_serversRepository, m_appSettingsRepository, this);
@@ -165,12 +243,13 @@ void CoreController::initControllers()
setQmlContextProperty("FocusController", m_focusController);
}
m_installUiController = new InstallUiController(m_installController, m_serversController, m_settingsController, m_protocolsModel, m_usersController,
m_installUiController = new InstallUiController(m_installController, m_serversController, m_settingsController, m_protocolsModel, m_usersController,
m_awgConfigModel, m_wireGuardConfigModel, m_openVpnConfigModel, m_xrayConfigModel, m_torConfigModel,
#ifdef Q_OS_WINDOWS
m_ikev2ConfigModel,
#endif
m_sftpConfigModel, m_socks5ConfigModel, this);
m_sftpConfigModel, m_socks5ConfigModel, m_mtProxyConfigModel, m_telemtConfigModel,
m_connectionController, this);
setQmlContextProperty("InstallController", m_installUiController);
m_importController = new ImportUiController(m_importCoreController, this);
@@ -182,12 +261,17 @@ void CoreController::initControllers()
m_languageUiController = new LanguageUiController(m_settingsController, m_languageModel, this);
setQmlContextProperty("LanguageUiController", m_languageUiController);
m_settingsUiController = new SettingsUiController(m_settingsController, m_serversController, m_languageUiController, this);
m_settingsUiController = new SettingsUiController(m_settingsController, m_serversController, this);
setQmlContextProperty("SettingsController", m_settingsUiController);
m_pageController = new PageController(m_serversController, m_settingsController, this);
setQmlContextProperty("PageController", m_pageController);
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
connect(m_connectionController, &ConnectionController::localProxyStoppedBecauseVpnTurnedOn, m_pageController,
&PageController::showNotificationMessage);
#endif
m_serversUiController = new ServersUiController(m_serversController, m_settingsController, m_serversModel, m_containersModel, m_defaultServerContainersModel, this);
setQmlContextProperty("ServersUiController", m_serversUiController);
@@ -203,12 +287,17 @@ void CoreController::initControllers()
m_systemController = new SystemController(this);
setQmlContextProperty("SystemController", m_systemController);
m_networkReachabilityController = new NetworkReachabilityController(this);
setQmlContextProperty("NetworkReachabilityController", m_networkReachabilityController);
setQmlContextProperty("NetworkReachability", m_networkReachabilityController);
m_servicesCatalogUiController = new ServicesCatalogUiController(m_servicesCatalogController, m_apiServicesModel, this);
setQmlContextProperty("ServicesCatalogUiController", m_servicesCatalogUiController);
m_subscriptionUiController = new SubscriptionUiController(m_serversController, m_apiServicesModel, m_servicesCatalogController, m_subscriptionController,
m_apiSubscriptionPlansModel, m_apiBenefitsModel, m_apiAccountInfoModel,
m_apiCountryModel, m_apiDevicesModel, m_settingsController, this);
m_apiCountryModel, m_apiDevicesModel, m_settingsController,
m_connectionController, this);
setQmlContextProperty("SubscriptionUiController", m_subscriptionUiController);
m_apiNewsUiController = new ApiNewsUiController(m_newsModel, m_newsController, this);
@@ -262,9 +351,12 @@ void CoreController::initSignalHandlers()
{
m_signalHandlers = new CoreSignalHandlers(this, this);
m_signalHandlers->initAllHandlers();
// Trigger initial update after handlers are connected
m_serversUiController->updateModel();
if (m_serversUiController->hasServersFromGatewayApi()) {
m_apiNewsUiController->fetchNews(false);
}
}
void CoreController::updateTranslator(const QLocale &locale)
@@ -322,11 +414,13 @@ PageController* CoreController::pageController() const
void CoreController::openConnectionByIndex(int serverIndex)
{
if (m_serversModel) {
m_serversModel->setProcessedServerIndex(serverIndex);
const QString serverId =
m_serversUiController ? m_serversUiController->getServerId(serverIndex) : QString();
if (serverId.isEmpty()) {
return;
}
if (m_serversController) {
m_serversController->setDefaultServerIndex(serverIndex);
m_serversController->setDefaultServer(serverId);
}
m_connectionUiController->toggleConnection();
}
+47 -24
View File
@@ -28,6 +28,7 @@
#include "ui/controllers/languageUiController.h"
#include "ui/controllers/updateUiController.h"
#include "ui/controllers/api/servicesCatalogUiController.h"
#include "ui/controllers/networkReachabilityController.h"
#include "core/controllers/serversController.h"
#include "core/controllers/selfhosted/usersController.h"
@@ -64,48 +65,29 @@
#include "ui/models/protocols/openvpnConfigModel.h"
#include "ui/models/protocols/wireguardConfigModel.h"
#include "ui/models/protocols/xrayConfigModel.h"
#include "ui/models/protocols/xrayConfigSnapshotsModel.h"
#include "ui/models/protocolsModel.h"
#include "ui/models/services/torConfigModel.h"
#include "ui/models/serversModel.h"
#include "ui/models/services/sftpConfigModel.h"
#include "ui/models/services/socks5ProxyConfigModel.h"
#include "ui/models/services/mtProxyConfigModel.h"
#include "ui/models/services/telemtConfigModel.h"
#include "ui/models/ipSplitTunnelingModel.h"
#include "ui/models/newsModel.h"
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
#include "core/local-proxy/proxyserver.h"
#include "ui/utils/notificationHandler.h"
#endif
class CoreSignalHandlers;
class TestMultipleImports;
class TestAdminSelfHostedExport;
class TestServerEdit;
class TestDefaultServerChange;
class TestServerEdgeCases;
class TestSignalOrder;
class TestServersModelSync;
class TestGatewayStacks;
class TestComplexOperations;
class TestSettingsSignals;
class TestUiServersModelAndController;
class TestSelfHostedServerSetup;
class CoreController : public QObject
{
Q_OBJECT
friend class CoreSignalHandlers;
friend class TestMultipleImports;
friend class TestAdminSelfHostedExport;
friend class TestServerEdit;
friend class TestDefaultServerChange;
friend class TestServerEdgeCases;
friend class TestSignalOrder;
friend class TestServersModelSync;
friend class TestGatewayStacks;
friend class TestComplexOperations;
friend class TestSettingsSignals;
friend class TestUiServersModelAndController;
friend class TestSelfHostedServerSetup;
public:
explicit CoreController(const QSharedPointer<VpnConnection> &vpnConnection, SecureQSettings* settings,
@@ -122,6 +104,36 @@ signals:
void translationsUpdated();
void websiteUrlChanged(const QString &newUrl);
protected:
SecureServersRepository* serversRepositoryProtected() const { return m_serversRepository; }
SecureAppSettingsRepository* appSettingsRepositoryProtected() const { return m_appSettingsRepository; }
ServersModel* serversModelProtected() const { return m_serversModel; }
ContainersModel* containersModelProtected() const { return m_containersModel; }
ApiServicesModel* apiServicesModelProtected() const { return m_apiServicesModel; }
NewsModel* newsModelProtected() const { return m_newsModel; }
AllowedDnsModel* allowedDnsModelProtected() const { return m_allowedDnsModel; }
AppSplitTunnelingModel* appSplitTunnelingModelProtected() const { return m_appSplitTunnelingModel; }
IpSplitTunnelingModel* ipSplitTunnelingModelProtected() const { return m_ipSplitTunnelingModel; }
LanguageModel* languageModelProtected() const { return m_languageModel; }
InstallUiController* installUiControllerProtected() const { return m_installUiController; }
ImportController* importCoreControllerProtected() const { return m_importCoreController; }
ExportController* exportControllerProtected() const { return m_exportController; }
InstallController* installControllerProtected() const { return m_installController; }
ServersController* serversControllerProtected() const { return m_serversController; }
SettingsUiController* settingsUiControllerProtected() const { return m_settingsUiController; }
SettingsController* settingsControllerProtected() const { return m_settingsController; }
AllowedDnsUiController* allowedDnsUiControllerProtected() const { return m_allowedDnsUiController; }
AllowedDnsController* allowedDnsControllerProtected() const { return m_allowedDnsController; }
LanguageUiController* languageUiControllerProtected() const { return m_languageUiController; }
IpSplitTunnelingController* ipSplitTunnelingControllerProtected() const { return m_ipSplitTunnelingController; }
IpSplitTunnelingUiController* ipSplitTunnelingUiControllerProtected() const { return m_ipSplitTunnelingUiController; }
AppSplitTunnelingController* appSplitTunnelingControllerProtected() const { return m_appSplitTunnelingController; }
AppSplitTunnelingUiController* appSplitTunnelingUiControllerProtected() const { return m_appSplitTunnelingUiController; }
ServersUiController* serversUiControllerProtected() const { return m_serversUiController; }
ServicesCatalogUiController* servicesCatalogUiControllerProtected() const { return m_servicesCatalogUiController; }
ApiNewsUiController* apiNewsUiControllerProtected() const { return m_apiNewsUiController; }
private:
void initRepositories();
void initCoreControllers();
@@ -131,6 +143,9 @@ private:
void initAppleController();
void initLogging();
void initSignalHandlers();
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
void initLocalProxy();
#endif
void setQmlContextProperty(const QString &name, QObject *value);
QQmlApplicationEngine *m_engine {}; // TODO use parent child system here?
@@ -158,6 +173,7 @@ private:
ServersUiController* m_serversUiController;
IpSplitTunnelingUiController* m_ipSplitTunnelingUiController;
SystemController* m_systemController;
NetworkReachabilityController* m_networkReachabilityController;
AppSplitTunnelingUiController* m_appSplitTunnelingUiController;
AllowedDnsUiController* m_allowedDnsUiController;
LanguageUiController* m_languageUiController;
@@ -202,6 +218,7 @@ private:
OpenVpnConfigModel* m_openVpnConfigModel;
XrayConfigModel* m_xrayConfigModel;
XrayConfigSnapshotsModel* m_xrayConfigSnapshotsModel;
TorConfigModel* m_torConfigModel;
WireGuardConfigModel* m_wireGuardConfigModel;
AwgConfigModel* m_awgConfigModel;
@@ -210,8 +227,14 @@ private:
#endif
SftpConfigModel* m_sftpConfigModel;
Socks5ProxyConfigModel* m_socks5ConfigModel;
MtProxyConfigModel* m_mtProxyConfigModel;
TelemtConfigModel* m_telemtConfigModel;
CoreSignalHandlers* m_signalHandlers;
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
QScopedPointer<ProxyServer> m_proxyServer;
#endif
};
#endif // CORECONTROLLER_H
+97 -70
View File
@@ -1,12 +1,14 @@
#include "coreSignalHandlers.h"
#include <QTimer>
#include <QtConcurrent>
#include "core/utils/selfhosted/sshSession.h"
#include "core/utils/errorCodes.h"
#include "core/utils/routeModes.h"
#include "core/controllers/coreController.h"
#include "core/repositories/secureServersRepository.h"
#include "core/utils/serverConfigUtils.h"
#include "core/repositories/secureAppSettingsRepository.h"
#include "vpnConnection.h"
#include "ui/controllers/qml/pageController.h"
@@ -32,7 +34,6 @@
#include "core/controllers/connectionController.h"
#include "ui/models/clientManagementModel.h"
#include "ui/controllers/api/apiNewsUiController.h"
#include "ui/models/api/apiCountryModel.h"
#include "ui/models/containersModel.h"
#include "core/utils/containerEnum.h"
@@ -65,7 +66,6 @@ void CoreSignalHandlers::initAllHandlers()
initImportControllerHandler();
initApiCountryModelUpdateHandler();
initSubscriptionRefreshHandler();
initContainerModelUpdateHandler();
initAdminConfigRevokedHandler();
initPassphraseRequestHandler();
initTranslationsUpdatedHandler();
@@ -78,6 +78,7 @@ void CoreSignalHandlers::initAllHandlers()
initAllowedDnsModelUpdateHandler();
initAppSplitTunnelingModelUpdateHandler();
initPrepareConfigHandler();
initUnsupportedConnectDrawerHandler();
initStrictKillSwitchHandler();
initAndroidSettingsHandler();
initAndroidConnectionHandler();
@@ -124,11 +125,9 @@ void CoreSignalHandlers::initInstallControllerHandler()
{
connect(m_coreController->m_installController, &InstallController::serverIsBusy, m_coreController->m_installUiController, &InstallUiController::serverIsBusy);
connect(m_coreController->m_installUiController, &InstallUiController::cancelInstallation, m_coreController->m_installController, &InstallController::cancelInstallation);
connect(m_coreController->m_installUiController, &InstallUiController::currentContainerUpdated, m_coreController->m_connectionUiController,
&ConnectionUiController::onCurrentContainerUpdated);
connect(m_coreController->m_serversUiController, &ServersUiController::processedServerIndexChanged,
m_coreController->m_installUiController, [this](int index) {
if (index >= 0) {
connect(m_coreController->m_serversUiController, &ServersUiController::processedServerIdChanged,
m_coreController->m_installUiController, [this](const QString &serverId) {
if (!serverId.isEmpty()) {
m_coreController->m_installUiController->clearProcessedServerCredentials();
}
});
@@ -137,32 +136,39 @@ void CoreSignalHandlers::initInstallControllerHandler()
void CoreSignalHandlers::initExportControllerHandler()
{
connect(m_coreController->m_exportController, &ExportController::appendClientRequested, this,
[this](int serverIndex, const QString &clientId, const QString &clientName, DockerContainer container) {
m_coreController->m_usersController->appendClient(serverIndex, clientId, clientName, container);
[this](const QString &serverId, const QString &clientId, const QString &clientName, DockerContainer container) {
m_coreController->m_usersController->appendClient(serverId, clientId, clientName, container);
});
connect(m_coreController->m_exportController, &ExportController::updateClientsRequested, this,
[this](int serverIndex, DockerContainer container) {
m_coreController->m_usersController->updateClients(serverIndex, container);
[this](const QString &serverId, DockerContainer container) {
m_coreController->m_usersController->updateClients(serverId, container);
});
connect(m_coreController->m_exportController, &ExportController::revokeClientRequested, this,
[this](int serverIndex, int row, DockerContainer container) {
m_coreController->m_usersController->revokeClient(serverIndex, row, container);
[this](const QString &serverId, int row, DockerContainer container) {
QtConcurrent::run([this, serverId, row, container]() {
m_coreController->m_usersController->revokeClient(serverId, row, container);
});
});
connect(m_coreController->m_exportController, &ExportController::renameClientRequested, this,
[this](int serverIndex, int row, const QString &clientName, DockerContainer container) {
m_coreController->m_usersController->renameClient(serverIndex, row, clientName, container);
[this](const QString &serverId, int row, const QString &clientName, DockerContainer container) {
m_coreController->m_usersController->renameClient(serverId, row, clientName, container);
});
}
void CoreSignalHandlers::initImportControllerHandler()
{
connect(m_coreController->m_importCoreController, &ImportController::importFinished, this, [this]() {
if (!m_coreController->m_connectionController->isConnected()) {
int newServerIndex = m_coreController->m_serversController->getServersCount() - 1;
m_coreController->m_serversController->setDefaultServerIndex(newServerIndex);
if (m_coreController->m_serversUiController) {
m_coreController->m_serversUiController->setProcessedServerIndex(newServerIndex);
}
if (m_coreController->m_connectionUiController->isConnected()) {
return;
}
const int newServerIndex = m_coreController->m_serversController->getServersCount() - 1;
const QString serverId = m_coreController->m_serversController->getServerId(newServerIndex);
if (!serverId.isEmpty()) {
m_coreController->m_serversController->setDefaultServer(serverId);
}
if (m_coreController->m_serversUiController) {
m_coreController->m_serversUiController->setProcessedServerId(serverId);
}
});
}
@@ -170,42 +176,27 @@ void CoreSignalHandlers::initImportControllerHandler()
void CoreSignalHandlers::initApiCountryModelUpdateHandler()
{
connect(m_coreController->m_serversUiController, &ServersUiController::updateApiCountryModel, this, [this]() {
int processedIndex = m_coreController->m_serversUiController->getProcessedServerIndex();
if (processedIndex < 0 || processedIndex >= m_coreController->m_serversRepository->serversCount()) {
const QString processedServerId = m_coreController->m_serversUiController->getProcessedServerId();
if (processedServerId.isEmpty()) {
return;
}
ServerConfig server = m_coreController->m_serversRepository->server(processedIndex);
QJsonArray availableCountries;
QString serverCountryCode;
if (server.isApiV2()) {
const ApiV2ServerConfig* apiV2 = server.as<ApiV2ServerConfig>();
if (apiV2) {
availableCountries = apiV2->apiConfig.availableCountries;
serverCountryCode = apiV2->apiConfig.serverCountryCode;
}
const auto apiV2 = m_coreController->m_serversRepository->apiV2Config(processedServerId);
if (!apiV2.has_value()) {
return;
}
m_coreController->m_apiCountryModel->updateModel(availableCountries, serverCountryCode);
m_coreController->m_apiCountryModel->updateModel(apiV2->apiConfig.availableCountries,
apiV2->apiConfig.serverCountryCode);
});
}
void CoreSignalHandlers::initSubscriptionRefreshHandler()
{
connect(m_coreController->m_subscriptionUiController, &SubscriptionUiController::subscriptionRefreshNeeded, this, [this]() {
const int defaultServerIndex = m_coreController->m_serversController->getDefaultServerIndex();
if (defaultServerIndex >= 0) {
m_coreController->m_subscriptionUiController->getAccountInfo(defaultServerIndex, false);
}
});
}
void CoreSignalHandlers::initContainerModelUpdateHandler()
{
connect(m_coreController->m_serversController, &ServersController::gatewayStacksExpanded, this, [this]() {
if (m_coreController->m_serversUiController->hasServersFromGatewayApi()) {
m_coreController->m_apiNewsUiController->fetchNews(false);
const QString defaultServerId = m_coreController->m_serversController->getDefaultServerId();
if (!defaultServerId.isEmpty()) {
m_coreController->m_subscriptionUiController->getAccountInfo(defaultServerId, false);
}
});
}
@@ -213,17 +204,19 @@ void CoreSignalHandlers::initContainerModelUpdateHandler()
void CoreSignalHandlers::initAdminConfigRevokedHandler()
{
connect(m_coreController->m_installController, &InstallController::clientRevocationRequested, this,
[this](int serverIndex, const ContainerConfig &containerConfig, DockerContainer container) {
m_coreController->m_usersController->revokeClient(serverIndex, containerConfig, container);
[this](const QString &serverId, const ContainerConfig &containerConfig, DockerContainer container) {
QtConcurrent::run([this, serverId, containerConfig, container]() {
m_coreController->m_usersController->revokeClient(serverId, containerConfig, container);
});
});
connect(m_coreController->m_installController, &InstallController::clientAppendRequested, this,
[this](int serverIndex, const QString &clientId, const QString &clientName, DockerContainer container) {
m_coreController->m_usersController->appendClient(serverIndex, clientId, clientName, container);
});
[this](const QString &serverId, const QString &clientId, const QString &clientName, DockerContainer container) {
m_coreController->m_usersController->appendClient(serverId, clientId, clientName, container);
}, Qt::DirectConnection);
connect(m_coreController->m_usersController, &UsersController::adminConfigRevoked, m_coreController->m_serversController,
&ServersController::clearCachedProfile);
connect(m_coreController->m_usersController, &UsersController::adminConfigRevoked, m_coreController->m_installController,
&InstallController::clearCachedProfile);
}
void CoreSignalHandlers::initPassphraseRequestHandler()
@@ -247,12 +240,16 @@ void CoreSignalHandlers::initLanguageHandler()
connect(m_coreController->m_settingsUiController, &SettingsUiController::resetLanguageToSystem, m_coreController->m_languageUiController, [this]() {
m_coreController->m_languageUiController->changeLanguage(m_coreController->m_languageUiController->getSystemLanguageEnum());
});
connect(m_coreController->m_settingsUiController, &SettingsUiController::appLanguageChanged, m_coreController->m_languageUiController, [this]() {
m_coreController->m_languageUiController->onAppLanguageChanged(m_coreController->m_settingsController->getAppLanguage());
});
}
void CoreSignalHandlers::initAutoConnectHandler()
{
if (m_coreController->m_settingsUiController->isAutoConnectEnabled() && m_coreController->m_serversController->getDefaultServerIndex() >= 0) {
QTimer::singleShot(1000, this, [this]() { m_coreController->m_connectionUiController->openConnection(); });
if (m_coreController->m_settingsUiController->isAutoConnectEnabled()
&& !m_coreController->m_serversController->getDefaultServerId().isEmpty()) {
QTimer::singleShot(1000, this, [this]() { m_coreController->m_connectionUiController->toggleConnection(); });
}
}
@@ -271,16 +268,20 @@ void CoreSignalHandlers::initServersModelUpdateHandler()
m_coreController->m_serversUiController, &ServersUiController::updateModel);
connect(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged,
m_coreController->m_serversUiController, &ServersUiController::onDefaultServerChanged);
connect(m_coreController->m_serversRepository, &SecureServersRepository::serverAdded,
m_coreController->m_serversController, &ServersController::recomputeGatewayStacks);
connect(m_coreController->m_serversRepository, &SecureServersRepository::serverEdited,
m_coreController->m_serversController, &ServersController::recomputeGatewayStacks);
connect(m_coreController->m_serversRepository, &SecureServersRepository::serverRemoved,
m_coreController->m_serversController, &ServersController::recomputeGatewayStacks);
connect(m_coreController->m_settingsUiController, &SettingsUiController::restoreBackupFinished,
m_coreController->m_serversUiController, &ServersUiController::updateModel);
connect(m_coreController->m_serversRepository, &SecureServersRepository::serverAdded, this,
[this](const QString &serverId) {
if (m_coreController->m_serversRepository->apiV2Config(serverId).has_value()) {
m_coreController->m_apiNewsUiController->fetchNews(false);
}
});
connect(m_coreController->m_settingsUiController, &SettingsUiController::restoreBackupFinished, this, [this]() {
m_coreController->m_serversUiController->updateModel();
if (m_coreController->m_serversUiController->hasServersFromGatewayApi()) {
m_coreController->m_apiNewsUiController->fetchNews(false);
}
});
}
void CoreSignalHandlers::initClientManagementModelUpdateHandler()
@@ -289,6 +290,8 @@ void CoreSignalHandlers::initClientManagementModelUpdateHandler()
m_coreController->m_clientManagementModel, &ClientManagementModel::updateModel);
connect(m_coreController->m_usersController, &UsersController::clientRenamed,
m_coreController->m_clientManagementModel, &ClientManagementModel::updateClientName);
connect(m_coreController->m_usersController, &UsersController::revokeFinished,
m_coreController->m_exportController, &ExportController::revokeFinished);
}
void CoreSignalHandlers::initSitesModelUpdateHandler()
@@ -315,7 +318,19 @@ void CoreSignalHandlers::initPrepareConfigHandler()
connect(m_coreController->m_connectionUiController, &ConnectionUiController::prepareConfig, this, [this]() {
m_coreController->m_connectionController->setConnectionState(Vpn::ConnectionState::Preparing);
m_coreController->m_subscriptionUiController->validateConfig();
const QString serverId = m_coreController->m_serversController->getDefaultServerId();
if (serverId.isEmpty()) {
m_coreController->m_connectionController->setConnectionState(Vpn::ConnectionState::Disconnected);
return;
}
const serverConfigUtils::ConfigType kind = m_coreController->m_serversRepository->serverKind(serverId);
if (serverConfigUtils::isApiV2Subscription(kind) || serverConfigUtils::isLegacyApiSubscription(kind)) {
m_coreController->m_subscriptionUiController->validateConfig();
} else {
m_coreController->m_installUiController->validateConfig();
}
});
connect(m_coreController->m_subscriptionUiController, &SubscriptionUiController::configValidated, this, [this](bool isValid) {
@@ -324,7 +339,7 @@ void CoreSignalHandlers::initPrepareConfigHandler()
return;
}
m_coreController->m_installUiController->validateConfig();
m_coreController->m_connectionUiController->openConnection();
});
connect(m_coreController->m_installUiController, &InstallUiController::configValidated, this, [this](bool isValid) {
@@ -337,6 +352,15 @@ void CoreSignalHandlers::initPrepareConfigHandler()
});
}
void CoreSignalHandlers::initUnsupportedConnectDrawerHandler()
{
connect(m_coreController->m_subscriptionUiController, &SubscriptionUiController::unsupportedConnectDrawerRequested,
m_coreController->m_pageController, &PageController::unsupportedConnectDrawerRequested);
connect(m_coreController->m_connectionUiController, &ConnectionUiController::unsupportedConnectDrawerRequested,
m_coreController->m_pageController, &PageController::unsupportedConnectDrawerRequested);
}
void CoreSignalHandlers::initStrictKillSwitchHandler()
{
connect(m_coreController->m_settingsUiController, &SettingsUiController::strictKillSwitchEnabledChanged, m_coreController->m_connectionController,
@@ -348,7 +372,10 @@ void CoreSignalHandlers::initAndroidSettingsHandler()
#ifdef Q_OS_ANDROID
connect(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::saveLogsChanged, AndroidController::instance(), &AndroidController::setSaveLogs);
connect(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::screenshotsEnabledChanged, AndroidController::instance(), &AndroidController::setScreenshotsEnabled);
connect(m_coreController->m_serversRepository, &SecureServersRepository::serverRemoved, AndroidController::instance(), &AndroidController::resetLastServer);
connect(m_coreController->m_serversRepository, &SecureServersRepository::serverRemoved, this,
[](const QString &/*serverId*/, int removedIndex) {
AndroidController::instance()->resetLastServer(removedIndex);
});
connect(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::settingsCleared, []() { AndroidController::instance()->resetLastServer(-1); });
#endif
}
+1 -1
View File
@@ -21,7 +21,6 @@ private:
void initImportControllerHandler();
void initApiCountryModelUpdateHandler();
void initSubscriptionRefreshHandler();
void initContainerModelUpdateHandler();
void initAdminConfigRevokedHandler();
void initPassphraseRequestHandler();
void initTranslationsUpdatedHandler();
@@ -34,6 +33,7 @@ private:
void initAllowedDnsModelUpdateHandler();
void initAppSplitTunnelingModelUpdateHandler();
void initPrepareConfigHandler();
void initUnsupportedConnectDrawerHandler();
void initStrictKillSwitchHandler();
void initAndroidSettingsHandler();
void initAndroidConnectionHandler();
+14 -7
View File
@@ -30,6 +30,8 @@ namespace
constexpr QLatin1String errorResponsePattern1("No active configuration found for");
constexpr QLatin1String errorResponsePattern2("No non-revoked public key found for");
constexpr QLatin1String errorResponsePattern3("Account not found.");
constexpr QLatin1String errorResponsePatternQrSessionNotFound("QR session not found");
constexpr QLatin1String errorResponsePatternSessionNotFound("Session not found");
constexpr QLatin1String updateRequestResponsePattern("client version update is required");
@@ -37,6 +39,7 @@ namespace
constexpr int httpStatusCodeConflict = 409;
constexpr int httpStatusCodeNotImplemented = 501;
constexpr int httpStatusCodePaymentRequired = 402;
constexpr int httpStatusCodeRequestTimeout = 408;
constexpr int httpStatusCodeUnprocessableEntity = 422;
constexpr QLatin1String unprocessableSubscriptionMessage("Failed to retrieve subscription information. Is it activated?");
@@ -206,8 +209,9 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
bypassProxy(endpoint, serviceType, userCountryCode, requestFunction, replyProcessingFunction);
}
auto errorCode =
apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, decryptionResult.decryptedBody);
responseBody = decryptionResult.decryptedBody;
const auto errorCode =
apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, responseBody);
if (errorCode) {
return errorCode;
}
@@ -217,7 +221,6 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
return ErrorCode::ApiConfigDecryptionError;
}
responseBody = decryptionResult.decryptedBody;
return ErrorCode::NoError;
}
@@ -239,7 +242,7 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
connect(reply, &QNetworkReply::sslErrors, [sslErrors](const QList<QSslError> &errors) { *sslErrors = errors; });
connect(reply, &QNetworkReply::finished, reply, [promise, sslErrors, encRequestData, endpoint, apiPayload, reply, this]() mutable {
connect(reply, &QNetworkReply::finished, this, [promise, sslErrors, encRequestData, endpoint, apiPayload, reply, this]() mutable {
QByteArray encryptedResponseBody = reply->readAll();
QString replyErrorString = reply->errorString();
auto replyError = reply->error();
@@ -256,7 +259,7 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode,
decryptionResult.decryptedBody);
if (errorCode) {
promise->addResult(qMakePair(errorCode, QByteArray()));
promise->addResult(qMakePair(errorCode, decryptionResult.decryptedBody));
promise->finish();
return;
}
@@ -459,15 +462,19 @@ bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &rep
qDebug() << "the response contains an html tag";
return true;
}
if (apiHttpStatus == httpStatusCodeRequestTimeout) {
return false;
}
if (apiHttpStatus == httpStatusCodeNotFound) {
if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2)
|| responseBody.contains(errorResponsePattern3)) {
|| responseBody.contains(errorResponsePattern3) || responseBody.contains(errorResponsePatternQrSessionNotFound)
|| responseBody.contains(errorResponsePatternSessionNotFound)) {
return false;
} else {
qDebug() << replyError;
return true;
}
}
}
if (apiHttpStatus == httpStatusCodeNotImplemented) {
if (responseBody.contains(updateRequestResponsePattern)) {
return false;
@@ -5,14 +5,13 @@
#include "core/configurators/configuratorBase.h"
#include "core/utils/selfhosted/sshSession.h"
#include "core/utils/networkUtilities.h"
#include "core/utils/qrCodeUtils.h"
#include "core/utils/serialization/serialization.h"
#include "core/utils/protocolEnum.h"
#include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/models/serverConfig.h"
#include "core/models/selfhosted/selfHostedAdminServerConfig.h"
#include "core/models/containerConfig.h"
#include "core/models/protocolConfig.h"
@@ -27,18 +26,20 @@ ExportController::ExportController(SecureServersRepository* serversRepository,
{
}
ExportController::ExportResult ExportController::generateFullAccessConfig(int serverIndex)
ExportController::ExportResult ExportController::generateFullAccessConfig(const QString &serverId)
{
ExportResult result;
ServerConfig serverConfig = m_serversRepository->server(serverIndex);
serverConfig.visit([](auto& arg) {
for (auto it = arg.containers.begin(); it != arg.containers.end(); ++it) {
it.value().protocolConfig.clearClientConfig();
}
});
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
if (!adminConfig.has_value()) {
result.errorCode = ErrorCode::InternalError;
return result;
}
for (auto it = adminConfig->containers.begin(); it != adminConfig->containers.end(); ++it) {
it.value().protocolConfig.clearClientConfig();
}
QJsonObject serverJson = serverConfig.toJson();
QJsonObject serverJson = adminConfig->toJson();
QByteArray compressedConfig = QJsonDocument(serverJson).toJson();
compressedConfig = qCompress(compressedConfig, 8);
result.config = generateVpnUrl(compressedConfig);
@@ -47,13 +48,22 @@ ExportController::ExportResult ExportController::generateFullAccessConfig(int se
return result;
}
ExportController::ExportResult ExportController::generateConnectionConfig(int serverIndex, int containerIndex, const QString &clientName)
ExportController::ExportResult ExportController::generateConnectionConfig(const QString &serverId, int containerIndex, const QString &clientName)
{
ExportResult result;
DockerContainer container = static_cast<DockerContainer>(containerIndex);
ServerCredentials credentials = m_serversRepository->serverCredentials(serverIndex);
ContainerConfig containerConfig = m_serversRepository->containerConfig(serverIndex, container);
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
if (!adminConfig.has_value()) {
result.errorCode = ErrorCode::InternalError;
return result;
}
const ServerCredentials credentials = adminConfig->credentials();
if (!credentials.isValid()) {
result.errorCode = ErrorCode::InternalError;
return result;
}
ContainerConfig containerConfig = adminConfig->containerConfig(container);
if (ContainerUtils::containerService(container) != ServiceType::Other) {
SshSession sshSession;
@@ -74,35 +84,25 @@ ExportController::ExportResult ExportController::generateConnectionConfig(int se
QString clientId = newProtocolConfig.clientId();
if (!clientId.isEmpty()) {
emit appendClientRequested(serverIndex, clientId, clientName, container);
emit appendClientRequested(serverId, clientId, clientName, container);
}
}
ServerConfig serverConfig = m_serversRepository->server(serverIndex);
serverConfig.visit([container, containerConfig](auto& arg) {
arg.containers.clear();
arg.containers[container] = containerConfig;
arg.defaultContainer = container;
});
const QPair<QString, QString> dns = adminConfig->getDnsPair(m_appSettingsRepository->useAmneziaDns(),
m_appSettingsRepository->primaryDns(),
m_appSettingsRepository->secondaryDns());
if (serverConfig.isSelfHosted()) {
SelfHostedServerConfig* selfHosted = serverConfig.as<SelfHostedServerConfig>();
if (selfHosted) {
selfHosted->userName.reset();
selfHosted->password.reset();
selfHosted->port.reset();
}
}
adminConfig->containers.clear();
adminConfig->containers[container] = containerConfig;
adminConfig->defaultContainer = container;
adminConfig->userName.clear();
adminConfig->password.clear();
adminConfig->port = 0;
auto dns = serverConfig.getDnsPair(m_appSettingsRepository->useAmneziaDns(),
m_appSettingsRepository->primaryDns(),
m_appSettingsRepository->secondaryDns());
serverConfig.visit([&dns](auto& arg) {
arg.dns1 = dns.first;
arg.dns2 = dns.second;
});
adminConfig->dns1 = dns.first;
adminConfig->dns2 = dns.second;
QJsonObject serverJson = serverConfig.toJson();
QJsonObject serverJson = adminConfig->toJson();
QByteArray compressedConfig = QJsonDocument(serverJson).toJson();
compressedConfig = qCompress(compressedConfig, 8);
result.config = generateVpnUrl(compressedConfig);
@@ -111,7 +111,7 @@ ExportController::ExportResult ExportController::generateConnectionConfig(int se
return result;
}
ExportController::NativeConfigResult ExportController::generateNativeConfig(int serverIndex, DockerContainer container,
ExportController::NativeConfigResult ExportController::generateNativeConfig(const QString &serverId, DockerContainer container,
const ContainerConfig &containerConfig,
const QString &clientName)
{
@@ -123,11 +123,19 @@ ExportController::NativeConfigResult ExportController::generateNativeConfig(int
Proto protocol = ContainerUtils::defaultProtocol(container);
ServerCredentials credentials = m_serversRepository->serverCredentials(serverIndex);
ServerConfig serverConfig = m_serversRepository->server(serverIndex);
auto dns = serverConfig.getDnsPair(m_appSettingsRepository->useAmneziaDns(),
m_appSettingsRepository->primaryDns(),
m_appSettingsRepository->secondaryDns());
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
if (!adminConfig.has_value()) {
result.errorCode = ErrorCode::InternalError;
return result;
}
const ServerCredentials credentials = adminConfig->credentials();
if (!credentials.isValid()) {
result.errorCode = ErrorCode::InternalError;
return result;
}
const QPair<QString, QString> dns = adminConfig->getDnsPair(m_appSettingsRepository->useAmneziaDns(),
m_appSettingsRepository->primaryDns(),
m_appSettingsRepository->secondaryDns());
ContainerConfig modifiedContainerConfig = containerConfig;
modifiedContainerConfig.container = container;
@@ -157,20 +165,25 @@ ExportController::NativeConfigResult ExportController::generateNativeConfig(int
if (protocol == Proto::OpenVpn || protocol == Proto::WireGuard || protocol == Proto::Awg || protocol == Proto::Xray) {
QString clientId = newProtocolConfig.clientId();
if (!clientId.isEmpty()) {
emit appendClientRequested(serverIndex, clientId, clientName, container);
emit appendClientRequested(serverId, clientId, clientName, container);
}
}
return result;
}
ExportController::ExportResult ExportController::generateOpenVpnConfig(int serverIndex, const QString &clientName)
ExportController::ExportResult ExportController::generateOpenVpnConfig(const QString &serverId, const QString &clientName)
{
ExportResult result;
DockerContainer container = DockerContainer::OpenVpn;
ContainerConfig containerConfig = m_serversRepository->containerConfig(serverIndex, container);
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
if (!adminConfig.has_value()) {
result.errorCode = ErrorCode::InternalError;
return result;
}
ContainerConfig containerConfig = adminConfig->containerConfig(container);
auto nativeResult = generateNativeConfig(serverIndex, container, containerConfig, clientName);
auto nativeResult = generateNativeConfig(serverId, container, containerConfig, clientName);
if (nativeResult.errorCode != ErrorCode::NoError) {
result.errorCode = nativeResult.errorCode;
return result;
@@ -185,13 +198,18 @@ ExportController::ExportResult ExportController::generateOpenVpnConfig(int serve
return result;
}
ExportController::ExportResult ExportController::generateWireGuardConfig(int serverIndex, const QString &clientName)
ExportController::ExportResult ExportController::generateWireGuardConfig(const QString &serverId, const QString &clientName)
{
ExportResult result;
ContainerConfig containerConfig = m_serversRepository->containerConfig(serverIndex, DockerContainer::WireGuard);
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
if (!adminConfig.has_value()) {
result.errorCode = ErrorCode::InternalError;
return result;
}
ContainerConfig containerConfig = adminConfig->containerConfig(DockerContainer::WireGuard);
auto nativeResult = generateNativeConfig(serverIndex, DockerContainer::WireGuard, containerConfig, clientName);
auto nativeResult = generateNativeConfig(serverId, DockerContainer::WireGuard, containerConfig, clientName);
if (nativeResult.errorCode != ErrorCode::NoError) {
result.errorCode = nativeResult.errorCode;
return result;
@@ -206,7 +224,7 @@ ExportController::ExportResult ExportController::generateWireGuardConfig(int ser
return result;
}
ExportController::ExportResult ExportController::generateAwgConfig(int serverIndex, int containerIndex, const QString &clientName)
ExportController::ExportResult ExportController::generateAwgConfig(const QString &serverId, int containerIndex, const QString &clientName)
{
ExportResult result;
@@ -215,9 +233,14 @@ ExportController::ExportResult ExportController::generateAwgConfig(int serverInd
result.errorCode = ErrorCode::InternalError;
return result;
}
ContainerConfig containerConfig = m_serversRepository->containerConfig(serverIndex, container);
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
if (!adminConfig.has_value()) {
result.errorCode = ErrorCode::InternalError;
return result;
}
ContainerConfig containerConfig = adminConfig->containerConfig(container);
auto nativeResult = generateNativeConfig(serverIndex, container, containerConfig, clientName);
auto nativeResult = generateNativeConfig(serverId, container, containerConfig, clientName);
if (nativeResult.errorCode != ErrorCode::NoError) {
result.errorCode = nativeResult.errorCode;
return result;
@@ -233,13 +256,18 @@ ExportController::ExportResult ExportController::generateAwgConfig(int serverInd
}
ExportController::ExportResult ExportController::generateXrayConfig(int serverIndex, const QString &clientName)
ExportController::ExportResult ExportController::generateXrayConfig(const QString &serverId, const QString &clientName)
{
ExportResult result;
ContainerConfig containerConfig = m_serversRepository->containerConfig(serverIndex, DockerContainer::Xray);
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
if (!adminConfig.has_value()) {
result.errorCode = ErrorCode::InternalError;
return result;
}
ContainerConfig containerConfig = adminConfig->containerConfig(DockerContainer::Xray);
auto nativeResult = generateNativeConfig(serverIndex, DockerContainer::Xray, containerConfig, clientName);
auto nativeResult = generateNativeConfig(serverId, DockerContainer::Xray, containerConfig, clientName);
if (nativeResult.errorCode != ErrorCode::NoError) {
result.errorCode = nativeResult.errorCode;
return result;
@@ -295,6 +323,18 @@ ExportController::ExportResult ExportController::generateXrayConfig(int serverIn
vlessServer.shortId = realitySettings.value(amnezia::protocols::xray::shortId).toString();
vlessServer.fingerprint = realitySettings.value(amnezia::protocols::xray::fingerprint).toString("chrome");
vlessServer.spiderX = realitySettings.value(amnezia::protocols::xray::spiderX).toString("");
} else if (vlessServer.security == "tls") {
QJsonObject tlsSettings = streamSettings.value("tlsSettings").toObject();
vlessServer.serverName = tlsSettings.value(amnezia::protocols::xray::serverName).toString();
vlessServer.fingerprint = tlsSettings.value(amnezia::protocols::xray::fingerprint).toString();
// alpn: serialize array back to comma-separated for VLESS URI
QJsonArray alpnArr = tlsSettings.value("alpn").toArray();
QStringList alpnList;
for (const QJsonValue &v : alpnArr) {
alpnList << v.toString();
}
// alpn goes into vless URI query param — handled by Serialize via serverName/alpn fields
// VlessServerObject doesn't have alpn field, so we embed in serverName if needed
}
result.nativeConfigString = amnezia::serialization::vless::Serialize(vlessServer, "AmneziaVPN");
@@ -302,22 +342,22 @@ ExportController::ExportResult ExportController::generateXrayConfig(int serverIn
return result;
}
void ExportController::updateClientManagementModel(int serverIndex, int containerIndex)
void ExportController::updateClientManagementModel(const QString &serverId, int containerIndex)
{
DockerContainer container = static_cast<DockerContainer>(containerIndex);
emit updateClientsRequested(serverIndex, container);
emit updateClientsRequested(serverId, container);
}
void ExportController::revokeConfig(int row, int serverIndex, int containerIndex)
void ExportController::revokeConfig(int row, const QString &serverId, int containerIndex)
{
DockerContainer container = static_cast<DockerContainer>(containerIndex);
emit revokeClientRequested(serverIndex, row, container);
emit revokeClientRequested(serverId, row, container);
}
void ExportController::renameClient(int row, const QString &clientName, int serverIndex, int containerIndex)
void ExportController::renameClient(int row, const QString &clientName, const QString &serverId, int containerIndex)
{
DockerContainer container = static_cast<DockerContainer>(containerIndex);
emit renameClientRequested(serverIndex, row, clientName, container);
emit renameClientRequested(serverId, row, clientName, container);
}
QString ExportController::generateVpnUrl(const QByteArray &compressedConfig)
@@ -37,23 +37,24 @@ public:
SecureAppSettingsRepository* appSettingsRepository,
QObject *parent = nullptr);
ExportResult generateFullAccessConfig(int serverIndex);
ExportResult generateConnectionConfig(int serverIndex, int containerIndex, const QString &clientName);
ExportResult generateOpenVpnConfig(int serverIndex, const QString &clientName);
ExportResult generateWireGuardConfig(int serverIndex, const QString &clientName);
ExportResult generateAwgConfig(int serverIndex, int containerIndex, const QString &clientName);
ExportResult generateXrayConfig(int serverIndex, const QString &clientName);
ExportResult generateFullAccessConfig(const QString &serverId);
ExportResult generateConnectionConfig(const QString &serverId, int containerIndex, const QString &clientName);
ExportResult generateOpenVpnConfig(const QString &serverId, const QString &clientName);
ExportResult generateWireGuardConfig(const QString &serverId, const QString &clientName);
ExportResult generateAwgConfig(const QString &serverId, int containerIndex, const QString &clientName);
ExportResult generateXrayConfig(const QString &serverId, const QString &clientName);
signals:
void appendClientRequested(int serverIndex, const QString &clientId, const QString &clientName, DockerContainer container);
void updateClientsRequested(int serverIndex, DockerContainer container);
void revokeClientRequested(int serverIndex, int row, DockerContainer container);
void renameClientRequested(int serverIndex, int row, const QString &clientName, DockerContainer container);
void appendClientRequested(const QString &serverId, const QString &clientId, const QString &clientName, DockerContainer container);
void updateClientsRequested(const QString &serverId, DockerContainer container);
void revokeClientRequested(const QString &serverId, int row, DockerContainer container);
void revokeFinished(ErrorCode errorCode);
void renameClientRequested(const QString &serverId, int row, const QString &clientName, DockerContainer container);
public slots:
void updateClientManagementModel(int serverIndex, int containerIndex);
void revokeConfig(int row, int serverIndex, int containerIndex);
void renameClient(int row, const QString &clientName, int serverIndex, int containerIndex);
void updateClientManagementModel(const QString &serverId, int containerIndex);
void revokeConfig(int row, const QString &serverId, int containerIndex);
void renameClient(int row, const QString &clientName, const QString &serverId, int containerIndex);
private:
struct NativeConfigResult
@@ -62,7 +63,7 @@ private:
QJsonObject jsonNativeConfig;
};
NativeConfigResult generateNativeConfig(int serverIndex, DockerContainer container,
NativeConfigResult generateNativeConfig(const QString &serverId, DockerContainer container,
const ContainerConfig &containerConfig,
const QString &clientName);
@@ -16,7 +16,7 @@
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
#include "core/utils/api/apiEnums.h"
#include "core/utils/serverConfigUtils.h"
#include "core/utils/constants/apiKeys.h"
#include "core/utils/constants/apiConstants.h"
#include "core/utils/api/apiUtils.h"
@@ -27,7 +27,6 @@
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/qrCodeUtils.h"
#include "core/models/serverConfig.h"
using namespace amnezia;
using namespace ProtocolUtils;
@@ -208,12 +207,18 @@ ImportController::ImportResult ImportController::extractConfigFromData(const QSt
case ConfigTypes::Amnezia: {
result.config = QJsonDocument::fromJson(config.toUtf8()).object();
if (apiUtils::isServerFromApi(result.config)) {
if (serverConfigUtils::isServerFromApi(result.config)) {
auto apiConfig = result.config.value(apiDefs::key::apiConfig).toObject();
apiConfig[apiDefs::key::vpnKey] = data;
result.config[apiDefs::key::apiConfig] = apiConfig;
}
if (serverConfigUtils::isLegacyApiSubscription(serverConfigUtils::configTypeFromJson(result.config))) {
result.errorCode = ErrorCode::LegacyApiV1NotSupportedError;
result.config = {};
return result;
}
processAmneziaConfig(result.config);
if (!result.config.empty()) {
checkForMaliciousStrings(result.config, result.maliciousWarningText);
@@ -381,18 +386,29 @@ void ImportController::importConfig(const QJsonObject &config)
credentials.secretData = config.value(configKey::password).toString();
if (credentials.isValid() || config.contains(configKey::containers)) {
ServerConfig serverConfig = ServerConfig::fromJson(config);
m_serversRepository->addServer(serverConfig);
m_serversRepository->addServer(QString(), config, serverConfigUtils::configTypeFromJson(config));
emit importFinished();
} else if (config.contains(configKey::configVersion)) {
quint16 crc = qChecksum(QJsonDocument(config).toJson());
if (m_serversRepository->hasServerWithCrc(crc)) {
bool hasServerWithCrc = false;
const QVector<QString> ids = m_serversRepository->orderedServerIds();
for (const QString &id : ids) {
const auto apiV2 = m_serversRepository->apiV2Config(id);
if (!apiV2.has_value()) {
continue;
}
if (static_cast<quint16>(apiV2->crc) == crc) {
hasServerWithCrc = true;
break;
}
}
if (hasServerWithCrc) {
emit importErrorOccurred(ErrorCode::ApiConfigAlreadyAdded, true);
} else {
QJsonObject configWithCrc = config;
configWithCrc.insert(configKey::crc, crc);
ServerConfig serverConfig = ServerConfig::fromJson(configWithCrc);
m_serversRepository->addServer(serverConfig);
m_serversRepository->addServer(QString(), configWithCrc, serverConfigUtils::configTypeFromJson(configWithCrc));
emit importFinished();
}
} else {
@@ -470,7 +486,7 @@ QJsonObject ImportController::extractOpenVpnConfig(const QString &data) const
QJsonObject config;
config[configKey::containers] = arr;
config[configKey::defaultContainer] = configKey::amneziaOpenvpn;
config[configKey::description] = m_appSettingsRepository->nextAvailableServerName();
config[configKey::description] = m_serversRepository->nextAvailableServerName();
const static QRegularExpression dnsRegExp("dhcp-option DNS (\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b)");
QRegularExpressionMatchIterator dnsMatch = dnsRegExp.globalMatch(data);
@@ -629,7 +645,7 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data, Config
QJsonObject config;
config[configKey::containers] = arr;
config[configKey::defaultContainer] = containerName;
config[configKey::description] = m_appSettingsRepository->nextAvailableServerName();
config[configKey::description] = m_serversRepository->nextAvailableServerName();
const static QRegularExpression dnsRegExp(
"DNS = "
@@ -683,7 +699,7 @@ QJsonObject ImportController::extractXrayConfig(const QString &data, ConfigTypes
? configKey::amneziaSsxray
: configKey::amneziaXray;
if (description.isEmpty()) {
config[configKey::description] = m_appSettingsRepository->nextAvailableServerName();
config[configKey::description] = m_serversRepository->nextAvailableServerName();
} else {
config[configKey::description] = description;
}
File diff suppressed because it is too large Load Diff
@@ -16,6 +16,7 @@
#include "core/models/containerConfig.h"
#include "core/repositories/secureServersRepository.h"
#include "core/repositories/secureAppSettingsRepository.h"
#include "core/installers/mtProxyInstaller.h"
class SshSession;
class InstallerBase;
@@ -33,27 +34,43 @@ public:
~InstallController();
ErrorCode setupContainer(const ServerCredentials &credentials, DockerContainer container, ContainerConfig &config, bool isUpdate = false);
ErrorCode updateContainer(int serverIndex, DockerContainer container, const ContainerConfig &oldConfig, ContainerConfig &newConfig);
ErrorCode rebootServer(int serverIndex);
ErrorCode removeAllContainers(int serverIndex);
ErrorCode removeContainer(int serverIndex, DockerContainer container);
// Updates server-side container settings (admin self-hosted only): reconfigures the container over SSH.
ErrorCode updateServerConfig(const QString &serverId, DockerContainer container, const ContainerConfig &oldConfig, ContainerConfig &newConfig);
// Updates client-local settings only: rewrites the stored container config for any self-hosted/native server. No SSH.
ErrorCode updateClientConfig(const QString &serverId, DockerContainer container, ContainerConfig &newConfig);
ErrorCode rebootServer(const QString &serverId);
ErrorCode removeAllContainers(const QString &serverId);
ErrorCode removeContainer(const QString &serverId, DockerContainer container);
ErrorCode setDockerContainerEnabledState(const QString &serverId, DockerContainer container, bool enabled);
/// statusOut: 0 = not deployed, 1 = running, 2 = stopped, 3 = error
ErrorCode queryDockerContainerStatus(const QString &serverId, DockerContainer container, int &statusOut);
ErrorCode queryMtProxyDiagnostics(const QString &serverId, DockerContainer container, int listenPort,
MtProxyContainerDiagnostics &out);
QString fetchDockerContainerSecret(const QString &serverId, DockerContainer container);
ContainerConfig generateConfig(DockerContainer container, int port, TransportProto transportProto);
ErrorCode getAlreadyInstalledContainers(const ServerCredentials &credentials, QMap<DockerContainer, ContainerConfig> &installedContainers, SshSession &sshSession);
ErrorCode scanServerForInstalledContainers(int serverIndex);
ErrorCode scanServerForInstalledContainers(const QString &serverId);
ErrorCode installContainer(const ServerCredentials &credentials, DockerContainer container, int port, TransportProto transportProto, ContainerConfig &config);
ErrorCode installServer(const ServerCredentials &credentials, DockerContainer container, int port, TransportProto transportProto,
bool &wasContainerInstalled);
ErrorCode installContainer(int serverIndex, DockerContainer container, int port, TransportProto transportProto,
ErrorCode installContainer(const QString &serverId, DockerContainer container, int port, TransportProto transportProto,
bool &wasContainerInstalled);
bool isUpdateDockerContainerRequired(DockerContainer container, const ContainerConfig &oldConfig, const ContainerConfig &newConfig);
ErrorCode checkSshConnection(const ServerCredentials &credentials, QString &output, std::function<QString()> passphraseCallback = nullptr);
ErrorCode checkSshConnection(ServerCredentials &credentials, QString &output,
std::function<QString()> passphraseCallback = nullptr);
bool isServerAlreadyExists(const ServerCredentials &credentials, int &existingServerIndex);
@@ -62,11 +79,13 @@ public:
void cancelInstallation();
void clearCachedProfile(int serverIndex, DockerContainer container);
void clearCachedProfile(const QString &serverId, DockerContainer container);
ErrorCode validateAndPrepareConfig(int serverIndex);
ErrorCode validateAndPrepareConfig(const QString &serverId);
void validateConfig(int serverIndex);
void validateConfig(const QString &serverId);
void addEmptyServer(const ServerCredentials &credentials);
signals:
void configValidated(bool isValid);
@@ -74,8 +93,8 @@ signals:
void serverIsBusy(const bool isBusy);
void cancelInstallationRequested();
void clientRevocationRequested(int serverIndex, const ContainerConfig &containerConfig, DockerContainer container);
void clientAppendRequested(int serverIndex, const QString &clientId, const QString &clientName, DockerContainer container);
void clientRevocationRequested(const QString &serverId, const ContainerConfig &containerConfig, DockerContainer container);
void clientAppendRequested(const QString &serverId, const QString &clientId, const QString &clientName, DockerContainer container);
private:
ErrorCode installDockerWorker(const ServerCredentials &credentials, DockerContainer container, SshSession &sshSession);
@@ -95,9 +114,9 @@ private:
ErrorCode processContainerForAdmin(DockerContainer container, ContainerConfig &containerConfig,
const ServerCredentials &credentials, SshSession &sshSession,
int serverIndex, const QString &clientName);
const QString &serverId, const QString &clientName);
void adminAppendRequested(int serverIndex, DockerContainer container,
void adminAppendRequested(const QString &serverId, DockerContainer container,
const ContainerConfig &containerConfig, const QString &clientName);
static void updateContainerConfigAfterInstallation(DockerContainer container, ContainerConfig &containerConfig, const QString &stdOut);
@@ -114,4 +133,3 @@ private:
};
#endif // INSTALLCONTROLLER_H
@@ -14,7 +14,6 @@
#include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/models/serverConfig.h"
#include "core/models/containerConfig.h"
using namespace amnezia;
@@ -292,11 +291,18 @@ ErrorCode UsersController::getXrayClients(const DockerContainer container, const
return error;
}
ErrorCode UsersController::updateClients(int serverIndex, const DockerContainer container)
ErrorCode UsersController::updateClients(const QString &serverId, const DockerContainer container)
{
ErrorCode error = ErrorCode::NoError;
SshSession sshSession;
ServerCredentials credentials = m_serversRepository->serverCredentials(serverIndex);
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
if (!adminConfig.has_value()) {
return ErrorCode::InternalError;
}
ServerCredentials credentials = adminConfig->credentials();
if (!credentials.isValid()) {
return ErrorCode::InternalError;
}
QString clientsTableFile = QString("/opt/amnezia/%1/clientsTable");
if (container == DockerContainer::OpenVpn) {
@@ -381,20 +387,27 @@ ErrorCode UsersController::updateClients(int serverIndex, const DockerContainer
}
ErrorCode UsersController::appendClient(int serverIndex, const QString &clientId, const QString &clientName, const DockerContainer container)
ErrorCode UsersController::appendClient(const QString &serverId, const QString &clientId, const QString &clientName, const DockerContainer container)
{
ErrorCode error = ErrorCode::NoError;
SshSession sshSession;
ServerCredentials credentials = m_serversRepository->serverCredentials(serverIndex);
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
if (!adminConfig.has_value()) {
return ErrorCode::InternalError;
}
ServerCredentials credentials = adminConfig->credentials();
if (!credentials.isValid()) {
return ErrorCode::InternalError;
}
error = updateClients(serverIndex, container);
error = updateClients(serverId, container);
if (error != ErrorCode::NoError) {
return error;
}
int existingIndex = clientIndexById(clientId, m_clientsTable);
if (existingIndex >= 0) {
return renameClient(serverIndex, existingIndex, clientName, container, true);
return renameClient(serverId, existingIndex, clientName, container, true);
}
QJsonObject client;
@@ -426,7 +439,7 @@ ErrorCode UsersController::appendClient(int serverIndex, const QString &clientId
return error;
}
ErrorCode UsersController::renameClient(int serverIndex, const int row, const QString &clientName,
ErrorCode UsersController::renameClient(const QString &serverId, const int row, const QString &clientName,
const DockerContainer container, bool addTimeStamp)
{
if (row < 0 || row >= m_clientsTable.size()) {
@@ -434,7 +447,14 @@ ErrorCode UsersController::renameClient(int serverIndex, const int row, const QS
}
SshSession sshSession;
ServerCredentials credentials = m_serversRepository->serverCredentials(serverIndex);
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
if (!adminConfig.has_value()) {
return ErrorCode::InternalError;
}
ServerCredentials credentials = adminConfig->credentials();
if (!credentials.isValid()) {
return ErrorCode::InternalError;
}
auto client = m_clientsTable.at(row).toObject();
auto userData = client[configKey::userData].toObject();
@@ -470,7 +490,7 @@ ErrorCode UsersController::renameClient(int serverIndex, const int row, const QS
}
ErrorCode UsersController::revokeOpenVpn(const int row, const DockerContainer container, const ServerCredentials &credentials,
const int serverIndex, SshSession* sshSession, QJsonArray &clientsTable)
SshSession* sshSession, QJsonArray &clientsTable)
{
if (row < 0 || row >= clientsTable.size()) {
return ErrorCode::InternalError;
@@ -678,7 +698,7 @@ ErrorCode UsersController::revokeXray(const int row,
QString restartScript = QString("sudo docker restart $CONTAINER_NAME");
error = sshSession->runScript(
credentials,
credentials,
sshSession->replaceVars(restartScript, amnezia::genBaseVars(credentials, container, QString(), QString()))
);
if (error != ErrorCode::NoError) {
@@ -689,14 +709,21 @@ ErrorCode UsersController::revokeXray(const int row,
return error;
}
ErrorCode UsersController::revokeClient(int serverIndex, const int index, const DockerContainer container)
ErrorCode UsersController::revokeClient(const QString &serverId, const int index, const DockerContainer container)
{
if (index < 0 || index >= m_clientsTable.size()) {
return ErrorCode::InternalError;
}
SshSession sshSession;
ServerCredentials credentials = m_serversRepository->serverCredentials(serverIndex);
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
if (!adminConfig.has_value()) {
return ErrorCode::InternalError;
}
ServerCredentials credentials = adminConfig->credentials();
if (!credentials.isValid()) {
return ErrorCode::InternalError;
}
QString clientId = m_clientsTable.at(index).toObject().value(configKey::clientId).toString();
ErrorCode errorCode = ErrorCode::NoError;
@@ -704,7 +731,7 @@ ErrorCode UsersController::revokeClient(int serverIndex, const int index, const
switch(container)
{
case DockerContainer::OpenVpn: {
errorCode = revokeOpenVpn(index, container, credentials, serverIndex, &sshSession, m_clientsTable);
errorCode = revokeOpenVpn(index, container, credentials, &sshSession, m_clientsTable);
break;
}
case DockerContainer::WireGuard:
@@ -724,28 +751,41 @@ ErrorCode UsersController::revokeClient(int serverIndex, const int index, const
}
if (errorCode == ErrorCode::NoError) {
ServerConfig serverConfig = m_serversRepository->server(serverIndex);
ContainerConfig containerCfg = m_serversRepository->containerConfig(serverIndex, container);
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
if (!adminConfig.has_value()) {
return ErrorCode::InternalError;
}
ContainerConfig containerCfg = adminConfig->containerConfig(container);
QString containerClientId = containerCfg.protocolConfig.clientId();
if (!clientId.isEmpty() && !containerClientId.isEmpty() && containerClientId.contains(clientId)) {
emit adminConfigRevoked(serverIndex, container);
const bool isAdminMatch = !clientId.isEmpty() && !containerClientId.isEmpty() && containerClientId.contains(clientId);
if (isAdminMatch) {
emit adminConfigRevoked(serverId, container);
}
emit clientRevoked(index);
emit clientsUpdated(m_clientsTable);
}
emit clientsUpdated(m_clientsTable);
emit revokeFinished(errorCode);
return errorCode;
}
ErrorCode UsersController::revokeClient(int serverIndex, const ContainerConfig &containerConfig, const DockerContainer container)
ErrorCode UsersController::revokeClient(const QString &serverId, const ContainerConfig &containerConfig, const DockerContainer container)
{
SshSession sshSession;
ServerCredentials credentials = m_serversRepository->serverCredentials(serverIndex);
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
if (!adminConfig.has_value()) {
return ErrorCode::InternalError;
}
ServerCredentials credentials = adminConfig->credentials();
if (!credentials.isValid()) {
return ErrorCode::InternalError;
}
ErrorCode errorCode = ErrorCode::NoError;
errorCode = updateClients(serverIndex, container);
errorCode = updateClients(serverId, container);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
@@ -778,7 +818,7 @@ ErrorCode UsersController::revokeClient(int serverIndex, const ContainerConfig &
switch (container)
{
case DockerContainer::OpenVpn: {
errorCode = revokeOpenVpn(row, container, credentials, serverIndex, &sshSession, m_clientsTable);
errorCode = revokeOpenVpn(row, container, credentials, &sshSession, m_clientsTable);
break;
}
case DockerContainer::WireGuard:
@@ -797,7 +837,7 @@ ErrorCode UsersController::revokeClient(int serverIndex, const ContainerConfig &
}
if (errorCode == ErrorCode::NoError) {
emit adminConfigRevoked(serverIndex, container);
emit adminConfigRevoked(serverId, container);
emit clientRevoked(row);
emit clientsUpdated(m_clientsTable);
}
@@ -37,21 +37,22 @@ signals:
void clientAdded(const QJsonObject &client);
void clientRenamed(int row, const QString &newName);
void clientRevoked(int row);
void adminConfigRevoked(int serverIndex, DockerContainer container);
void revokeFinished(ErrorCode errorCode);
void adminConfigRevoked(const QString &serverId, DockerContainer container);
public slots:
ErrorCode updateClients(int serverIndex, const DockerContainer container);
ErrorCode appendClient(int serverIndex, const QString &clientId, const QString &clientName, const DockerContainer container);
ErrorCode renameClient(int serverIndex, const int row, const QString &userName, const DockerContainer container, bool addTimeStamp = false);
ErrorCode revokeClient(int serverIndex, const int index, const DockerContainer container);
ErrorCode revokeClient(int serverIndex, const ContainerConfig &containerConfig, const DockerContainer container);
ErrorCode updateClients(const QString &serverId, const DockerContainer container);
ErrorCode appendClient(const QString &serverId, const QString &clientId, const QString &clientName, const DockerContainer container);
ErrorCode renameClient(const QString &serverId, const int row, const QString &userName, const DockerContainer container, bool addTimeStamp = false);
ErrorCode revokeClient(const QString &serverId, const int index, const DockerContainer container);
ErrorCode revokeClient(const QString &serverId, const ContainerConfig &containerConfig, const DockerContainer container);
private:
bool isClientExists(const QString &clientId, const QJsonArray &clientsTable);
int clientIndexById(const QString &clientId, const QJsonArray &clientsTable);
void migration(const QByteArray &clientsTableString, QJsonArray &clientsTable);
ErrorCode revokeOpenVpn(const int row, const DockerContainer container, const ServerCredentials &credentials, const int serverIndex,
ErrorCode revokeOpenVpn(const int row, const DockerContainer container, const ServerCredentials &credentials,
SshSession* sshSession, QJsonArray &clientsTable);
ErrorCode revokeWireGuard(const int row, const DockerContainer container, const ServerCredentials &credentials,
SshSession* sshSession, QJsonArray &clientsTable);
@@ -73,4 +74,3 @@ private:
};
#endif // USERSCONTROLLER_H
+353 -140
View File
@@ -1,81 +1,272 @@
#include "serversController.h"
#include "core/utils/networkUtilities.h"
#include "core/utils/api/apiEnums.h"
#include "core/utils/constants/apiKeys.h"
#include "core/utils/constants/apiConstants.h"
#include "core/utils/serverConfigUtils.h"
#include "core/utils/protocolEnum.h"
#include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/models/serverConfig.h"
#include "core/models/containerConfig.h"
#include "core/models/serverDescription.h"
#if defined(Q_OS_IOS) || defined(MACOS_NE)
#include <AmneziaVPN-Swift.h>
#endif
ServersController::ServersController(SecureServersRepository* serversRepository,
SecureAppSettingsRepository* appSettingsRepository,
QObject *parent)
ServersController::ServersController(SecureServersRepository *serversRepository,
SecureAppSettingsRepository *appSettingsRepository, QObject *parent)
: QObject(parent), m_serversRepository(serversRepository), m_appSettingsRepository(appSettingsRepository)
{
recomputeGatewayStacks();
ensureDefaultServerValid();
}
void ServersController::addServer(const ServerConfig &server)
void ServersController::ensureDefaultServerValid()
{
m_serversRepository->addServer(server);
}
void ServersController::editServer(int index, const ServerConfig &server)
{
m_serversRepository->editServer(index, server);
}
void ServersController::removeServer(int index)
{
m_serversRepository->removeServer(index);
}
void ServersController::setDefaultServerIndex(int index)
{
m_serversRepository->setDefaultServer(index);
}
void ServersController::setDefaultContainer(int serverIndex, DockerContainer container)
{
m_serversRepository->setDefaultContainer(serverIndex, container);
}
void ServersController::updateContainerConfig(int serverIndex, DockerContainer container, const ContainerConfig &config)
{
m_serversRepository->setContainerConfig(serverIndex, container, config);
}
void ServersController::clearCachedProfile(int serverIndex, DockerContainer container)
{
m_serversRepository->clearLastConnectionConfig(serverIndex, container);
}
QJsonArray ServersController::getServersArray() const
{
QJsonArray result;
QVector<ServerConfig> servers = m_serversRepository->servers();
for (const ServerConfig& server : servers) {
result.append(server.toJson());
if (!getServersCount()) {
return;
}
const QString defaultId = getDefaultServerId();
if (!defaultId.isEmpty() && indexOfServerId(defaultId) >= 0) {
return;
}
const QString firstId = getServerId(0);
if (!firstId.isEmpty()) {
setDefaultServer(firstId);
}
return result;
}
QVector<ServerConfig> ServersController::getServers() const
bool ServersController::renameServer(const QString &serverId, const QString &name)
{
return m_serversRepository->servers();
const serverConfigUtils::ConfigType kind = m_serversRepository->serverKind(serverId);
switch (kind) {
case serverConfigUtils::ConfigType::SelfHostedAdmin: {
auto cfg = m_serversRepository->selfHostedAdminConfig(serverId);
if (!cfg.has_value()) return false;
cfg->description = name;
cfg->displayName = name;
m_serversRepository->editServer(serverId, cfg->toJson(), kind);
return true;
}
case serverConfigUtils::ConfigType::SelfHostedUser: {
auto cfg = m_serversRepository->selfHostedUserConfig(serverId);
if (!cfg.has_value()) return false;
cfg->description = name;
cfg->displayName = name;
m_serversRepository->editServer(serverId, cfg->toJson(), kind);
return true;
}
case serverConfigUtils::ConfigType::Native: {
auto cfg = m_serversRepository->nativeConfig(serverId);
if (!cfg.has_value()) return false;
cfg->description = name;
cfg->displayName = name;
m_serversRepository->editServer(serverId, cfg->toJson(), kind);
return true;
}
case serverConfigUtils::ConfigType::AmneziaPremiumV2:
case serverConfigUtils::ConfigType::AmneziaFreeV3:
case serverConfigUtils::ConfigType::ExternalPremium: {
auto cfg = m_serversRepository->apiV2Config(serverId);
if (!cfg.has_value()) return false;
cfg->name = name;
cfg->displayName = name;
cfg->nameOverriddenByUser = true;
m_serversRepository->editServer(serverId, cfg->toJson(), kind);
return true;
}
case serverConfigUtils::ConfigType::AmneziaPremiumV1:
case serverConfigUtils::ConfigType::AmneziaFreeV2:
case serverConfigUtils::ConfigType::Invalid:
default:
return false;
}
}
ContainerConfig ServersController::getContainerConfig(int serverIndex, DockerContainer container) const
void ServersController::removeServer(const QString &serverId)
{
return m_serversRepository->containerConfig(serverIndex, container);
m_serversRepository->removeServer(serverId);
}
void ServersController::setDefaultServer(const QString &serverId)
{
m_serversRepository->setDefaultServer(serverId);
}
void ServersController::setDefaultContainer(const QString &serverId, DockerContainer container)
{
const serverConfigUtils::ConfigType kind = m_serversRepository->serverKind(serverId);
switch (kind) {
case serverConfigUtils::ConfigType::SelfHostedAdmin: {
auto cfg = m_serversRepository->selfHostedAdminConfig(serverId);
if (!cfg.has_value()) return;
cfg->defaultContainer = container;
m_serversRepository->editServer(serverId, cfg->toJson(), kind);
return;
}
case serverConfigUtils::ConfigType::SelfHostedUser: {
auto cfg = m_serversRepository->selfHostedUserConfig(serverId);
if (!cfg.has_value()) return;
cfg->defaultContainer = container;
m_serversRepository->editServer(serverId, cfg->toJson(), kind);
return;
}
case serverConfigUtils::ConfigType::Native: {
auto cfg = m_serversRepository->nativeConfig(serverId);
if (!cfg.has_value()) return;
cfg->defaultContainer = container;
m_serversRepository->editServer(serverId, cfg->toJson(), kind);
return;
}
case serverConfigUtils::ConfigType::AmneziaPremiumV2:
case serverConfigUtils::ConfigType::AmneziaFreeV3:
case serverConfigUtils::ConfigType::ExternalPremium: {
auto cfg = m_serversRepository->apiV2Config(serverId);
if (!cfg.has_value()) return;
cfg->defaultContainer = container;
m_serversRepository->editServer(serverId, cfg->toJson(), kind);
return;
}
case serverConfigUtils::ConfigType::AmneziaPremiumV1:
case serverConfigUtils::ConfigType::AmneziaFreeV2:
case serverConfigUtils::ConfigType::Invalid:
default:
return;
}
}
QVector<ServerDescription> ServersController::buildServerDescriptions(bool isAmneziaDnsEnabled) const
{
QVector<ServerDescription> out;
const QVector<QString> ids = m_serversRepository->orderedServerIds();
out.reserve(ids.size());
for (const QString &id : ids) {
ServerDescription d;
using Kind = serverConfigUtils::ConfigType;
const Kind kind = m_serversRepository->serverKind(id);
switch (kind) {
case Kind::SelfHostedAdmin: {
const auto cfg = m_serversRepository->selfHostedAdminConfig(id);
if (!cfg) {
continue;
}
d = buildServerDescription(*cfg, isAmneziaDnsEnabled);
break;
}
case Kind::SelfHostedUser: {
const auto cfg = m_serversRepository->selfHostedUserConfig(id);
if (!cfg) {
continue;
}
d = buildServerDescription(*cfg, isAmneziaDnsEnabled);
break;
}
case Kind::Native: {
const auto cfg = m_serversRepository->nativeConfig(id);
if (!cfg) {
continue;
}
d = buildServerDescription(*cfg, isAmneziaDnsEnabled);
break;
}
case Kind::AmneziaPremiumV2:
case Kind::AmneziaFreeV3:
case Kind::ExternalPremium: {
const auto cfg = m_serversRepository->apiV2Config(id);
if (!cfg) {
continue;
}
d = buildServerDescription(*cfg, isAmneziaDnsEnabled);
break;
}
case Kind::AmneziaPremiumV1:
case Kind::AmneziaFreeV2: {
const auto cfg = m_serversRepository->legacyApiConfig(id);
if (!cfg) {
continue;
}
d = buildServerDescription(*cfg, isAmneziaDnsEnabled);
break;
}
case Kind::Invalid:
default:
continue;
}
d.serverId = id;
out.append(d);
}
return out;
}
QMap<DockerContainer, ContainerConfig> ServersController::getServerContainersMap(const QString &serverId) const
{
switch (m_serversRepository->serverKind(serverId)) {
case serverConfigUtils::ConfigType::SelfHostedAdmin: {
const auto cfg = m_serversRepository->selfHostedAdminConfig(serverId);
return cfg.has_value() ? cfg->containers : QMap<DockerContainer, ContainerConfig>{};
}
case serverConfigUtils::ConfigType::SelfHostedUser: {
const auto cfg = m_serversRepository->selfHostedUserConfig(serverId);
return cfg.has_value() ? cfg->containers : QMap<DockerContainer, ContainerConfig>{};
}
case serverConfigUtils::ConfigType::Native: {
const auto cfg = m_serversRepository->nativeConfig(serverId);
return cfg.has_value() ? cfg->containers : QMap<DockerContainer, ContainerConfig>{};
}
case serverConfigUtils::ConfigType::AmneziaPremiumV2:
case serverConfigUtils::ConfigType::AmneziaFreeV3:
case serverConfigUtils::ConfigType::ExternalPremium: {
const auto cfg = m_serversRepository->apiV2Config(serverId);
return cfg.has_value() ? cfg->containers : QMap<DockerContainer, ContainerConfig>{};
}
case serverConfigUtils::ConfigType::AmneziaPremiumV1:
case serverConfigUtils::ConfigType::AmneziaFreeV2: {
const auto cfg = m_serversRepository->legacyApiConfig(serverId);
return cfg.has_value() ? cfg->containers : QMap<DockerContainer, ContainerConfig>{};
}
case serverConfigUtils::ConfigType::Invalid:
default:
return {};
}
}
DockerContainer ServersController::getDefaultContainer(const QString &serverId) const
{
switch (m_serversRepository->serverKind(serverId)) {
case serverConfigUtils::ConfigType::SelfHostedAdmin: {
const auto cfg = m_serversRepository->selfHostedAdminConfig(serverId);
return cfg.has_value() ? cfg->defaultContainer : DockerContainer::None;
}
case serverConfigUtils::ConfigType::SelfHostedUser: {
const auto cfg = m_serversRepository->selfHostedUserConfig(serverId);
return cfg.has_value() ? cfg->defaultContainer : DockerContainer::None;
}
case serverConfigUtils::ConfigType::Native: {
const auto cfg = m_serversRepository->nativeConfig(serverId);
return cfg.has_value() ? cfg->defaultContainer : DockerContainer::None;
}
case serverConfigUtils::ConfigType::AmneziaPremiumV2:
case serverConfigUtils::ConfigType::AmneziaFreeV3:
case serverConfigUtils::ConfigType::ExternalPremium: {
const auto cfg = m_serversRepository->apiV2Config(serverId);
return cfg.has_value() ? cfg->defaultContainer : DockerContainer::None;
}
case serverConfigUtils::ConfigType::AmneziaPremiumV1:
case serverConfigUtils::ConfigType::AmneziaFreeV2: {
const auto cfg = m_serversRepository->legacyApiConfig(serverId);
return cfg.has_value() ? cfg->defaultContainer : DockerContainer::None;
}
case serverConfigUtils::ConfigType::Invalid:
default:
return DockerContainer::None;
}
}
ContainerConfig ServersController::getContainerConfig(const QString &serverId, DockerContainer container) const
{
return getServerContainersMap(serverId).value(container);
}
int ServersController::getDefaultServerIndex() const
@@ -83,114 +274,131 @@ int ServersController::getDefaultServerIndex() const
return m_serversRepository->defaultServerIndex();
}
QString ServersController::getDefaultServerId() const
{
return m_serversRepository->defaultServerId();
}
int ServersController::getServersCount() const
{
return m_serversRepository->serversCount();
}
ServerConfig ServersController::getServerConfig(int serverIndex) const
QString ServersController::getServerId(int serverIndex) const
{
return m_serversRepository->server(serverIndex);
return m_serversRepository->serverIdAt(serverIndex);
}
ServerCredentials ServersController::getServerCredentials(int serverIndex) const
int ServersController::indexOfServerId(const QString &serverId) const
{
return m_serversRepository->serverCredentials(serverIndex);
return m_serversRepository->indexOfServerId(serverId);
}
QPair<QString, QString> ServersController::getDnsPair(int serverIndex, bool isAmneziaDnsEnabled) const
QString ServersController::notificationDisplayName(const QString &serverId) const
{
ServerConfig serverConfig = m_serversRepository->server(serverIndex);
return serverConfig.getDnsPair(isAmneziaDnsEnabled,
m_appSettingsRepository->primaryDns(),
m_appSettingsRepository->secondaryDns());
}
if (serverId.isEmpty()) {
return {};
}
ServersController::GatewayStacksData ServersController::gatewayStacks() const
{
return m_gatewayStacks;
}
void ServersController::recomputeGatewayStacks()
{
GatewayStacksData computed;
bool hasNewTags = false;
QVector<ServerConfig> servers = m_serversRepository->servers();
for (const ServerConfig& serverConfig : servers) {
if (serverConfig.isApiV2()) {
const ApiV2ServerConfig* apiV2 = serverConfig.as<ApiV2ServerConfig>();
if (!apiV2) continue;
const QString userCountryCode = apiV2->apiConfig.userCountryCode;
const QString serviceType = apiV2->serviceType();
if (!userCountryCode.isEmpty()) {
if (!m_gatewayStacks.userCountryCodes.contains(userCountryCode)) {
hasNewTags = true;
}
computed.userCountryCodes.insert(userCountryCode);
}
if (!serviceType.isEmpty()) {
if (!m_gatewayStacks.serviceTypes.contains(serviceType)) {
hasNewTags = true;
}
computed.serviceTypes.insert(serviceType);
using Kind = serverConfigUtils::ConfigType;
switch (m_serversRepository->serverKind(serverId)) {
case Kind::SelfHostedAdmin: {
if (const auto cfg = m_serversRepository->selfHostedAdminConfig(serverId)) {
if (!cfg->displayName.isEmpty()) {
return cfg->displayName;
}
}
break;
}
m_gatewayStacks = std::move(computed);
if (hasNewTags) {
emit gatewayStacksExpanded();
}
}
bool ServersController::GatewayStacksData::operator==(const GatewayStacksData &other) const
{
return userCountryCodes == other.userCountryCodes && serviceTypes == other.serviceTypes;
}
QJsonObject ServersController::GatewayStacksData::toJson() const
{
QJsonObject json;
QJsonArray userCountryCodesArray;
for (const QString &code : userCountryCodes) {
userCountryCodesArray.append(code);
}
json[apiDefs::key::userCountryCode] = userCountryCodesArray;
QJsonArray serviceTypesArray;
for (const QString &type : serviceTypes) {
serviceTypesArray.append(type);
}
json[apiDefs::key::serviceType] = serviceTypesArray;
return json;
}
bool ServersController::isServerFromApiAlreadyExists(const QString &userCountryCode, const QString &serviceType, const QString &serviceProtocol) const
{
QVector<ServerConfig> servers = m_serversRepository->servers();
for (const ServerConfig& serverConfig : servers) {
if (serverConfig.isApiV2()) {
const ApiV2ServerConfig* apiV2 = serverConfig.as<ApiV2ServerConfig>();
if (!apiV2) return false;
if (apiV2->apiConfig.userCountryCode == userCountryCode
&& apiV2->serviceType() == serviceType
&& apiV2->serviceProtocol() == serviceProtocol) {
return true;
case Kind::SelfHostedUser: {
if (const auto cfg = m_serversRepository->selfHostedUserConfig(serverId)) {
if (!cfg->displayName.isEmpty()) {
return cfg->displayName;
}
}
break;
}
case Kind::Native: {
if (const auto cfg = m_serversRepository->nativeConfig(serverId)) {
if (!cfg->displayName.isEmpty()) {
return cfg->displayName;
}
}
break;
}
case Kind::AmneziaPremiumV2:
case Kind::AmneziaFreeV3:
case Kind::ExternalPremium: {
if (const auto cfg = m_serversRepository->apiV2Config(serverId)) {
if (!cfg->displayName.isEmpty()) {
return cfg->displayName;
}
}
break;
}
case Kind::AmneziaPremiumV1:
case Kind::AmneziaFreeV2: {
if (const auto cfg = m_serversRepository->legacyApiConfig(serverId)) {
if (!cfg->displayName.isEmpty()) {
return cfg->displayName;
}
}
break;
}
default:
break;
}
const int idx = indexOfServerId(serverId);
if (idx >= 0) {
return QString::number(idx + 1);
}
return serverId;
}
std::optional<ApiV2ServerConfig> ServersController::apiV2Config(const QString &serverId) const
{
return m_serversRepository->apiV2Config(serverId);
}
std::optional<SelfHostedAdminServerConfig> ServersController::selfHostedAdminConfig(const QString &serverId) const
{
return m_serversRepository->selfHostedAdminConfig(serverId);
}
ServerCredentials ServersController::getServerCredentials(const QString &serverId) const
{
const auto cfg = m_serversRepository->selfHostedAdminConfig(serverId);
if (cfg.has_value()) {
const ServerCredentials creds = cfg->credentials();
if (creds.isValid()) {
return creds;
}
}
return ServerCredentials {};
}
bool ServersController::isServerFromApiAlreadyExists(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol) const
{
const QVector<QString> ids = m_serversRepository->orderedServerIds();
for (const QString &id : ids) {
const auto apiV2 = m_serversRepository->apiV2Config(id);
if (!apiV2.has_value()) {
continue;
}
if (apiV2->apiConfig.userCountryCode == userCountryCode && apiV2->serviceType() == serviceType
&& apiV2->serviceProtocol() == serviceProtocol) {
return true;
}
}
return false;
}
bool ServersController::hasInstalledContainers(int serverIndex) const
bool ServersController::hasInstalledContainers(const QString &serverId) const
{
ServerConfig serverConfig = m_serversRepository->server(serverIndex);
QMap<DockerContainer, ContainerConfig> containers = serverConfig.containers();
const QMap<DockerContainer, ContainerConfig> containers = getServerContainersMap(serverId);
for (auto it = containers.begin(); it != containers.end(); ++it) {
DockerContainer container = it.key();
if (ContainerUtils::containerService(container) == ServiceType::Vpn) {
@@ -203,3 +411,8 @@ bool ServersController::hasInstalledContainers(int serverIndex) const
return false;
}
bool ServersController::isLegacyApiV1Server(const QString &serverId) const
{
return !serverId.isEmpty()
&& serverConfigUtils::isLegacyApiSubscription(m_serversRepository->serverKind(serverId));
}
+23 -45
View File
@@ -1,11 +1,11 @@
#ifndef SERVERSCONTROLLER_H
#define SERVERSCONTROLLER_H
#include <optional>
#include <QObject>
#include <QJsonObject>
#include <QJsonArray>
#include <QSet>
#include <QVector>
#include <QMap>
#include <QPair>
@@ -17,34 +17,18 @@
#include "core/utils/commonStructs.h"
#include "core/repositories/secureServersRepository.h"
#include "core/repositories/secureAppSettingsRepository.h"
#include "core/models/serverConfig.h"
#include "core/models/containerConfig.h"
#include "core/models/serverDescription.h"
class SshSession;
class InstallController;
using namespace amnezia;
/**
* @brief Core business logic controller for server operations
*
* This controller contains pure business logic for managing servers.
*/
class ServersController : public QObject
{
Q_OBJECT
public:
struct GatewayStacksData
{
QSet<QString> userCountryCodes;
QSet<QString> serviceTypes;
bool isEmpty() const { return userCountryCodes.isEmpty() && serviceTypes.isEmpty(); }
bool operator==(const GatewayStacksData &other) const;
QJsonObject toJson() const;
};
public:
explicit ServersController(SecureServersRepository* serversRepository,
SecureAppSettingsRepository* appSettingsRepository = nullptr,
@@ -52,44 +36,38 @@ public:
~ServersController() = default;
// Server management
void addServer(const ServerConfig &server);
void editServer(int index, const ServerConfig &server);
void removeServer(int index);
void setDefaultServerIndex(int index);
bool renameServer(const QString &serverId, const QString &name);
void removeServer(const QString &serverId);
void setDefaultServer(const QString &serverId);
// Container management
void setDefaultContainer(int serverIndex, DockerContainer container);
void updateContainerConfig(int serverIndex, DockerContainer container, const ContainerConfig &config);
// Cache management
void clearCachedProfile(int serverIndex, DockerContainer container);
void setDefaultContainer(const QString &serverId, DockerContainer container);
// Getters
QJsonArray getServersArray() const;
QVector<ServerConfig> getServers() const;
QVector<ServerDescription> buildServerDescriptions(bool isAmneziaDnsEnabled) const;
int getDefaultServerIndex() const;
QString getDefaultServerId() const;
int getServersCount() const;
ServerConfig getServerConfig(int serverIndex) const;
ServerCredentials getServerCredentials(int serverIndex) const;
ContainerConfig getContainerConfig(int serverIndex, DockerContainer container) const;
QPair<QString, QString> getDnsPair(int serverIndex, bool isAmneziaDnsEnabled) const;
GatewayStacksData gatewayStacks() const;
QString getServerId(int serverIndex) const;
int indexOfServerId(const QString &serverId) const;
QString notificationDisplayName(const QString &serverId) const;
std::optional<ApiV2ServerConfig> apiV2Config(const QString &serverId) const;
std::optional<SelfHostedAdminServerConfig> selfHostedAdminConfig(const QString &serverId) const;
ServerCredentials getServerCredentials(const QString &serverId) const;
QMap<DockerContainer, ContainerConfig> getServerContainersMap(const QString &serverId) const;
DockerContainer getDefaultContainer(const QString &serverId) const;
ContainerConfig getContainerConfig(const QString &serverId, DockerContainer container) const;
// Validation
bool isServerFromApiAlreadyExists(const QString &userCountryCode, const QString &serviceType, const QString &serviceProtocol) const;
bool hasInstalledContainers(int serverIndex) const;
signals:
void gatewayStacksExpanded();
public slots:
void recomputeGatewayStacks();
bool hasInstalledContainers(const QString &serverId) const;
bool isLegacyApiV1Server(const QString &serverId) const;
private:
void ensureDefaultServerValid();
SecureServersRepository* m_serversRepository;
SecureAppSettingsRepository* m_appSettingsRepository;
GatewayStacksData m_gatewayStacks;
};
#endif // SERVERSCONTROLLER_H
+133 -6
View File
@@ -8,10 +8,19 @@
#include "version.h"
#include "ui/utils/qAutoStart.h"
#include "logger.h"
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
#include "core/local-proxy/portavailabilityhelper.h"
#endif
#ifdef Q_OS_ANDROID
#include "platforms/android/android_controller.h"
#endif
namespace {
constexpr int kDefaultProxyPort = 10808;
constexpr int kLocalProxyPortMin = 1024;
constexpr int kLocalProxyPortMax = 65535;
}
QString getPlatformName()
{
#if defined(Q_OS_WINDOWS)
@@ -38,6 +47,11 @@ SettingsController::SettingsController(SecureServersRepository* serversRepositor
{
m_appVersion = QString("%1 (%2, %3)").arg(QString(APP_VERSION), __DATE__, GIT_COMMIT_HASH);
m_isDevModeEnabled = m_appSettingsRepository->isDevGatewayEnv();
connect(m_appSettingsRepository, &SecureAppSettingsRepository::localProxySettingsChanged, this,
&SettingsController::localProxySettingsUpdated);
connect(m_appSettingsRepository, &SecureAppSettingsRepository::localProxyStartFailed, this,
&SettingsController::localProxyStartFailed);
}
void SettingsController::toggleAmneziaDns(bool enable)
@@ -179,12 +193,9 @@ QString SettingsController::getAppVersion() const
void SettingsController::clearSettings()
{
int serverCount = m_serversRepository->serversCount();
m_appSettingsRepository->clearSettings();
m_serversRepository->setServersArray(QJsonArray());
m_serversRepository->setDefaultServer(0);
m_serversRepository->clearServers();
emit siteSplitTunnelingRouteModeChanged(RouteMode::VpnOnlyForwardSites);
emit siteSplitTunnelingToggled(false);
@@ -220,6 +231,11 @@ void SettingsController::toggleAutoStart(bool enable)
bool SettingsController::isStartMinimizedEnabled() const
{
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
if (!isAutoStartEnabled()) {
return false;
}
#endif
return m_appSettingsRepository->isStartMinimized();
}
@@ -361,6 +377,117 @@ void SettingsController::disablePremV1MigrationReminder()
QString SettingsController::nextAvailableServerName() const
{
return m_appSettingsRepository->nextAvailableServerName();
return m_serversRepository->nextAvailableServerName();
}
bool SettingsController::isLocalProxySupported() const
{
#ifdef AMNEZIA_DESKTOP
return true;
#else
return false;
#endif
}
bool SettingsController::isLocalProxyHttpEnabled() const
{
return m_appSettingsRepository->isLocalProxyHttpEnabled();
}
int SettingsController::localProxyPort() const
{
return static_cast<int>(m_appSettingsRepository->localProxyPort());
}
QString SettingsController::localProxyOwnerId() const
{
return m_appSettingsRepository->localProxyOwnerId();
}
bool SettingsController::isLocalProxyPortUserDefined() const
{
return m_appSettingsRepository->isLocalProxyPortUserDefined();
}
bool SettingsController::setLocalProxyPort(int port)
{
if (port < kLocalProxyPortMin || port > kLocalProxyPortMax) {
return false;
}
if (m_appSettingsRepository->localProxyPort() == static_cast<quint16>(port)) {
m_appSettingsRepository->setLocalProxyPortUserDefined(true);
return true;
}
m_appSettingsRepository->setLocalProxyPort(static_cast<quint16>(port));
m_appSettingsRepository->setLocalProxyPortUserDefined(true);
return true;
}
bool SettingsController::isLocalProxyPortBusy(int port) const
{
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
return !PortAvailabilityHelper::isPortAvailable(port);
#else
Q_UNUSED(port);
return false;
#endif
}
int SettingsController::findFirstAvailableLocalProxyPort(int startPort) const
{
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
const auto port = PortAvailabilityHelper::findFirstAvailablePort(startPort, kLocalProxyPortMax);
return port ? *port : -1;
#else
Q_UNUSED(startPort);
return -1;
#endif
}
bool SettingsController::enableLocalProxy(const QString &ownerId, int port)
{
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
Q_UNUSED(ownerId);
Q_UNUSED(port);
return false;
#else
if (port < kLocalProxyPortMin || port > kLocalProxyPortMax || ownerId.isEmpty()) {
return false;
}
if (m_appSettingsRepository->isLocalProxyHttpEnabled() && m_appSettingsRepository->localProxyOwnerId() != ownerId) {
return false;
}
int selectedPort = port;
const bool isUserDefinedPort = m_appSettingsRepository->isLocalProxyPortUserDefined();
if (isUserDefinedPort) {
if (!PortAvailabilityHelper::isPortAvailable(selectedPort)) {
return false;
}
} else if (selectedPort != kDefaultProxyPort && !PortAvailabilityHelper::isPortAvailable(selectedPort)) {
return false;
}
if (m_appSettingsRepository->localProxyPort() != static_cast<quint16>(selectedPort)) {
m_appSettingsRepository->setLocalProxyPort(static_cast<quint16>(selectedPort));
}
m_appSettingsRepository->setLocalProxyPortUserDefined(isUserDefinedPort);
m_appSettingsRepository->setLocalProxyOwnerId(ownerId);
m_appSettingsRepository->setLocalProxyHttpEnabled(true);
return true;
#endif
}
void SettingsController::disableLocalProxy()
{
if (m_appSettingsRepository->isLocalProxyHttpEnabled()) {
m_appSettingsRepository->setLocalProxyHttpEnabled(false);
}
}
@@ -89,6 +89,18 @@ public:
QLocale getAppLanguage() const;
void setAppLanguage(const QLocale &locale);
// Local proxy
bool isLocalProxySupported() const;
bool isLocalProxyHttpEnabled() const;
int localProxyPort() const;
QString localProxyOwnerId() const;
bool isLocalProxyPortUserDefined() const;
bool setLocalProxyPort(int port);
bool isLocalProxyPortBusy(int port) const;
int findFirstAvailableLocalProxyPort(int startPort) const;
bool enableLocalProxy(const QString &ownerId, int port);
void disableLocalProxy();
signals:
void siteSplitTunnelingRouteModeChanged(RouteMode mode);
void siteSplitTunnelingToggled(bool enabled);
@@ -96,6 +108,9 @@ signals:
void appSplitTunnelingToggled(bool enabled);
void appSplitTunnelingClearAppsList();
void localProxySettingsUpdated();
void localProxyStartFailed(const QString &message);
private:
QString getPlatform() const;
+19 -64
View File
@@ -13,7 +13,6 @@
#include "version.h"
#include "core/controllers/gatewayController.h"
#include "core/utils/constants/apiKeys.h"
#include "core/utils/errorStrings.h"
#include "core/utils/selfhosted/scriptsRegistry.h"
namespace
@@ -21,14 +20,14 @@ namespace
Logger logger("UpdateController");
#if defined(Q_OS_WINDOWS)
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN_%1_x64.exe");
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN_%1_windows_x64.exe");
const QString kInstallerLocalPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN_installer.exe";
#elif defined(Q_OS_MACOS)
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN_%1_macos.pkg");
#elif defined(Q_OS_MACOS) && !defined(MACOS_NE)
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN_%1_macos_x64.pkg");
const QString kInstallerLocalPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN.pkg";
#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN_%1_linux_x64.tar");
const QString kInstallerLocalPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN.tar";
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN_%1_linux_x64.run");
const QString kInstallerLocalPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN.run";
#endif
}
@@ -106,10 +105,10 @@ void UpdateController::fetchGatewayUrl()
// Workaround: wait before contacting gateway to avoid rate limit triggered by other requests (news etc.)
QTimer::singleShot(1000, this, [this, gatewayController, apiPayload]() {
gatewayController->postAsync(QStringLiteral("%1v1/updater_endpoint"), apiPayload)
.then(this, [this](QPair<ErrorCode, QByteArray> result) {
.then(this, [this, gatewayController](QPair<ErrorCode, QByteArray> result) {
auto [err, gatewayResponse] = result;
if (err != ErrorCode::NoError) {
logger.error() << errorString(err);
logger.error() << "Gateway request failed, error code:" << static_cast<int>(err);
finishUpdateCheck();
return;
}
@@ -184,7 +183,7 @@ void UpdateController::setupNetworkErrorHandling(QNetworkReply* reply, const QSt
logger.error() << QString("Network error occurred while fetching %1: %2 %3")
.arg(operation, reply->errorString(), QString::number(error));
});
QObject::connect(reply, &QNetworkReply::sslErrors, [operation](const QList<QSslError> &errors) {
QStringList errorStrings;
for (const QSslError &err : errors) {
@@ -196,21 +195,13 @@ void UpdateController::setupNetworkErrorHandling(QNetworkReply* reply, const QSt
void UpdateController::handleNetworkError(QNetworkReply* reply, const QString& operation)
{
if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError
|| reply->error() == QNetworkReply::NetworkError::TimeoutError) {
logger.error() << errorString(ErrorCode::ApiConfigTimeoutError);
} else {
QString err = reply->errorString();
logger.error() << "Network error code:" << QString::number(static_cast<int>(reply->error()));
logger.error() << "Error message:" << err;
logger.error() << "HTTP status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
logger.error() << errorString(ErrorCode::ApiConfigDownloadError);
}
logger.error() << "Network error code:" << QString::number(static_cast<int>(reply->error()));
logger.error() << "HTTP status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
}
QString UpdateController::composeDownloadUrl() const
{
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
const QString fileName = QString(kInstallerRemoteFileNamePattern).arg(m_version);
return m_baseUrl + "/" + fileName;
#else
@@ -220,7 +211,7 @@ QString UpdateController::composeDownloadUrl() const
void UpdateController::runInstaller()
{
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
if (m_downloadUrl.isEmpty()) {
logger.error() << "Download URL is empty";
return;
@@ -252,23 +243,15 @@ void UpdateController::runInstaller()
#if defined(Q_OS_WINDOWS)
runWindowsInstaller(kInstallerLocalPath);
#elif defined(Q_OS_MACOS)
#elif defined(Q_OS_MACOS) && !defined(MACOS_NE)
runMacInstaller(kInstallerLocalPath);
#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
runLinuxInstaller(kInstallerLocalPath);
#endif
} else {
if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError
|| reply->error() == QNetworkReply::NetworkError::TimeoutError) {
logger.error() << errorString(ErrorCode::ApiConfigTimeoutError);
} else {
QString err = reply->errorString();
logger.error() << QString::fromUtf8(reply->readAll());
logger.error() << "Network error code:" << QString::number(static_cast<int>(reply->error()));
logger.error() << "Error message:" << err;
logger.error() << "HTTP status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
logger.error() << errorString(ErrorCode::ApiConfigDownloadError);
}
logger.error() << "Installer download failed, network error:" << static_cast<int>(reply->error())
<< reply->errorString();
logger.error() << "HTTP status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
}
reply->deleteLater();
});
@@ -292,7 +275,7 @@ int UpdateController::runWindowsInstaller(const QString &installerPath)
}
#endif
#if defined(Q_OS_MACOS)
#if defined(Q_OS_MACOS) && !defined(MACOS_NE)
int UpdateController::runMacInstaller(const QString &installerPath)
{
// Create temporary directory for extraction
@@ -346,36 +329,10 @@ int UpdateController::runMacInstaller(const QString &installerPath)
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
int UpdateController::runLinuxInstaller(const QString &installerPath)
{
// Create temporary directory for extraction
QTemporaryDir extractDir;
extractDir.setAutoRemove(false);
if (!extractDir.isValid()) {
logger.error() << "Failed to create temporary directory";
return -1;
}
logger.info() << "Temporary directory created:" << extractDir.path();
QFile::setPermissions(installerPath, QFile::permissions(installerPath) | QFile::ExeUser);
// Create script file in the temporary directory
QString scriptPath = extractDir.path() + "/installer.sh";
QFile scriptFile(scriptPath);
if (!scriptFile.open(QIODevice::WriteOnly)) {
logger.error() << "Failed to create script file";
return -1;
}
// Get script content from registry
QString scriptContent = amnezia::scriptData(amnezia::ClientScriptType::linux_installer);
scriptFile.write(scriptContent.toUtf8());
scriptFile.close();
logger.info() << "Script file created:" << scriptPath;
// Make script executable
QFile::setPermissions(scriptPath, QFile::permissions(scriptPath) | QFile::ExeUser);
// Start detached process
qint64 pid;
bool success =
QProcess::startDetached("/bin/bash", QStringList() << scriptPath << extractDir.path() << installerPath, extractDir.path(), &pid);
bool success = QProcess::startDetached(installerPath, QStringList(), QString(), &pid);
if (success) {
logger.info() << "Installation process started with PID:" << pid;
@@ -387,5 +344,3 @@ int UpdateController::runLinuxInstaller(const QString &installerPath)
return 0;
}
#endif
@@ -0,0 +1,16 @@
#ifndef CONTAINERDIAGNOSTICS_H
#define CONTAINERDIAGNOSTICS_H
namespace amnezia
{
struct ContainerDiagnostics
{
bool available = false;
bool portReachable = false;
virtual ~ContainerDiagnostics() = default;
};
} // namespace amnezia
#endif // CONTAINERDIAGNOSTICS_H
@@ -0,0 +1,18 @@
#ifndef MTPROXYDIAGNOSTICS_H
#define MTPROXYDIAGNOSTICS_H
#include "containerDiagnostics.h"
#include <QString>
namespace amnezia {
struct MtProxyDiagnostics : ContainerDiagnostics {
bool upstreamReachable = false;
int clientsConnected = -1;
QString lastConfigRefresh;
QString statsEndpoint;
};
} // namespace amnezia
#endif // MTPROXYDIAGNOSTICS_H
@@ -0,0 +1,20 @@
#ifndef TELEMTDIAGNOSTICS_H
#define TELEMTDIAGNOSTICS_H
#include "containerDiagnostics.h"
#include <QString>
namespace amnezia
{
struct TelemtDiagnostics : ContainerDiagnostics
{
bool upstreamReachable = false;
int clientsConnected = -1;
QString lastConfigRefresh;
QString statsEndpoint;
};
} // namespace amnezia
#endif // TELEMTDIAGNOSTICS_H
+14
View File
@@ -14,6 +14,8 @@
#include "core/models/protocols/xrayProtocolConfig.h"
#include "core/models/protocols/sftpProtocolConfig.h"
#include "core/models/protocols/socks5ProxyProtocolConfig.h"
#include "core/models/protocols/mtProxyProtocolConfig.h"
#include "core/models/protocols/telemtProtocolConfig.h"
#include "core/models/protocols/ikev2ProtocolConfig.h"
#include "core/models/protocols/torProtocolConfig.h"
@@ -91,6 +93,18 @@ ContainerConfig InstallerBase::createBaseConfig(DockerContainer container, int p
config.protocolConfig = socks5Config;
break;
}
case Proto::MtProxy: {
MtProxyProtocolConfig mtConfig;
mtConfig.port = portStr;
config.protocolConfig = mtConfig;
break;
}
case Proto::Telemt: {
TelemtProtocolConfig telemtConfig;
telemtConfig.port = portStr;
config.protocolConfig = telemtConfig;
break;
}
case Proto::Ikev2: {
Ikev2ProtocolConfig ikev2Config;
config.protocolConfig = ikev2Config;
+130
View File
@@ -0,0 +1,130 @@
#include "mtProxyInstaller.h"
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
#include "core/utils/selfhosted/sshSession.h"
#include "core/models/containerConfig.h"
#include "core/models/protocols/mtProxyProtocolConfig.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QRegularExpression>
#include <QtGlobal>
using namespace amnezia;
namespace {
constexpr QLatin1String kMtProxyClientJsonPath("/data/amnezia-mtproxy-client.json");
constexpr QLatin1String kMtProxyClientJsonUploadPath("data/amnezia-mtproxy-client.json");
constexpr QLatin1String kMtProxySecretPath("/data/secret");
}
MtProxyInstaller::MtProxyInstaller(QObject *parent)
: InstallerBase(parent) {
}
ErrorCode MtProxyInstaller::extractConfigFromContainer(DockerContainer container, const ServerCredentials &credentials,
SshSession *sshSession, ContainerConfig &config) {
if (container != DockerContainer::MtProxy || !sshSession) {
return ErrorCode::NoError;
}
MtProxyProtocolConfig *mt = config.getMtProxyProtocolConfig();
if (!mt) {
return ErrorCode::NoError;
}
ErrorCode jsonErr = ErrorCode::NoError;
const QByteArray jsonRaw =
sshSession->getTextFileFromContainer(container, credentials, QString(kMtProxyClientJsonPath), jsonErr);
if (jsonErr == ErrorCode::NoError && !jsonRaw.trimmed().isEmpty()) {
QJsonParseError parseError;
const QJsonDocument doc = QJsonDocument::fromJson(jsonRaw.trimmed(), &parseError);
if (parseError.error == QJsonParseError::NoError && doc.isObject()) {
QJsonObject merged = mt->toJson();
const QJsonObject snap = doc.object();
for (auto it = snap.constBegin(); it != snap.constEnd(); ++it) {
merged.insert(it.key(), it.value());
}
*mt = MtProxyProtocolConfig::fromJson(merged);
}
}
ErrorCode secretErr = ErrorCode::NoError;
const QByteArray secretRaw =
sshSession->getTextFileFromContainer(container, credentials, QString(kMtProxySecretPath), secretErr);
const QString sec = QString::fromUtf8(secretRaw).trimmed();
if (sec.length() == 32) {
static const QRegularExpression hex32(QStringLiteral("^[0-9a-fA-F]{32}$"));
if (hex32.match(sec).hasMatch()) {
mt->secret = sec;
}
}
return ErrorCode::NoError;
}
ErrorCode MtProxyInstaller::queryDiagnostics(SshSession &sshSession, const ServerCredentials &credentials,
DockerContainer container, int listenPort,
MtProxyContainerDiagnostics &out)
{
out = {};
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) {
return ErrorCode::InternalError;
}
const QString containerName = ContainerUtils::containerToString(container);
const QString script =
QStringLiteral(
"PORT_OK=$(sudo docker exec %1 sh -c 'ss -tlnp 2>/dev/null | grep -q :%2 && echo yes || echo no' 2>/dev/null || echo no); "
"TG_OK=$(curl -s --max-time 5 -o /dev/null -w '%%{http_code}' https://core.telegram.org/getProxySecret 2>/dev/null | grep -q '200' && echo yes || echo no); "
"CLIENTS=$(sudo docker exec amnezia-mtproxy sh -c 'curl -s --max-time 3 http://localhost:2398/stats 2>/dev/null | grep -o \"total_special_connections:[0-9]*\" | cut -d: -f2' 2>/dev/null); "
"CONF_TIME=$(sudo docker exec amnezia-mtproxy sh -c 'stat -c \"%%y\" /data/proxy-multi.conf 2>/dev/null | cut -d. -f1' 2>/dev/null || echo unknown); "
"echo \"PORT_OK=${PORT_OK}\"; "
"echo \"TG_OK=${TG_OK}\"; "
"echo \"CLIENTS=${CLIENTS:-0}\"; "
"echo \"CONF_TIME=${CONF_TIME}\"; "
"echo \"STATS=http://localhost:2398/stats\";")
.arg(containerName)
.arg(listenPort);
QString stdOut;
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
stdOut += data;
return ErrorCode::NoError;
};
const ErrorCode errorCode = sshSession.runScript(credentials, script, cbReadStdOut);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
for (const QString &line : stdOut.split('\n', Qt::SkipEmptyParts)) {
if (line.startsWith(QLatin1String("PORT_OK="))) {
out.portReachable = line.mid(8).trimmed() == QLatin1String("yes");
} else if (line.startsWith(QLatin1String("TG_OK="))) {
out.upstreamReachable = line.mid(6).trimmed() == QLatin1String("yes");
} else if (line.startsWith(QLatin1String("CLIENTS="))) {
out.clientsConnected = line.mid(8).trimmed().toInt();
} else if (line.startsWith(QLatin1String("CONF_TIME="))) {
out.lastConfigRefresh = line.mid(10).trimmed();
} else if (line.startsWith(QLatin1String("STATS="))) {
out.statsEndpoint = line.mid(6).trimmed();
}
}
return ErrorCode::NoError;
}
void MtProxyInstaller::uploadClientSettingsSnapshot(SshSession &sshSession, const ServerCredentials &credentials,
DockerContainer container, const ContainerConfig &config) {
const MtProxyProtocolConfig *mt = config.getMtProxyProtocolConfig();
if (!mt) {
return;
}
const QByteArray payload = QJsonDocument(mt->toJson()).toJson(QJsonDocument::Compact);
const ErrorCode err = sshSession.uploadTextFileToContainer(container, credentials, QString::fromUtf8(payload),
QString(kMtProxyClientJsonUploadPath));
if (err != ErrorCode::NoError) {
qWarning() << "MtProxyInstaller::uploadClientSettingsSnapshot failed" << err;
}
}
+34
View File
@@ -0,0 +1,34 @@
#ifndef MTPROXYINSTALLER_H
#define MTPROXYINSTALLER_H
#include "installerBase.h"
#include <QString>
struct MtProxyContainerDiagnostics {
bool portReachable = false;
bool upstreamReachable = false;
int clientsConnected = -1;
QString lastConfigRefresh;
QString statsEndpoint;
};
class MtProxyInstaller : public InstallerBase {
Q_OBJECT
public:
explicit MtProxyInstaller(QObject *parent = nullptr);
amnezia::ErrorCode
extractConfigFromContainer(amnezia::DockerContainer container, const amnezia::ServerCredentials &credentials,
SshSession *sshSession, amnezia::ContainerConfig &config) override;
static void uploadClientSettingsSnapshot(SshSession &sshSession, const amnezia::ServerCredentials &credentials,
amnezia::DockerContainer container,
const amnezia::ContainerConfig &config);
static amnezia::ErrorCode queryDiagnostics(SshSession &sshSession, const amnezia::ServerCredentials &credentials,
amnezia::DockerContainer container, int listenPort,
MtProxyContainerDiagnostics &out);
};
#endif // MTPROXYINSTALLER_H
+27 -6
View File
@@ -1,15 +1,17 @@
#include "socks5Installer.h"
#include "core/models/protocols/socks5ProxyProtocolConfig.h"
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
#include "core/utils/protocolEnum.h"
#include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/selfhosted/sshSession.h"
#include "core/utils/utilities.h"
#include <QRegularExpression>
using namespace amnezia;
using namespace ProtocolUtils;
@@ -33,10 +35,29 @@ ContainerConfig Socks5Installer::generateConfig(DockerContainer container, int p
ErrorCode Socks5Installer::extractConfigFromContainer(DockerContainer container, const ServerCredentials &credentials,
SshSession* sshSession, ContainerConfig &config)
{
Q_UNUSED(container);
Q_UNUSED(credentials);
Q_UNUSED(sshSession);
Q_UNUSED(config);
if (container != DockerContainer::Socks5Proxy || !sshSession) {
return ErrorCode::NoError;
}
Socks5ProxyProtocolConfig *socks5Config = config.getSocks5ProxyProtocolConfig();
if (!socks5Config) {
return ErrorCode::NoError;
}
ErrorCode readError = ErrorCode::NoError;
const QByteArray configRaw = sshSession->getTextFileFromContainer(
container, credentials, QString::fromUtf8(protocols::socks5Proxy::proxyConfigPath), readError);
if (readError != ErrorCode::NoError || configRaw.trimmed().isEmpty()) {
return ErrorCode::NoError;
}
const QString proxyConfig = QString::fromUtf8(configRaw);
static const QRegularExpression usernameAndPasswordRegExp(QStringLiteral("users (\\w+):CL:(\\w+)"));
const QRegularExpressionMatch usernameAndPasswordMatch = usernameAndPasswordRegExp.match(proxyConfig);
if (usernameAndPasswordMatch.hasMatch()) {
socks5Config->userName = usernameAndPasswordMatch.captured(1);
socks5Config->password = usernameAndPasswordMatch.captured(2);
}
return ErrorCode::NoError;
}
@@ -0,0 +1,79 @@
#include "telemtInstaller.h"
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/selfhosted/sshSession.h"
#include "core/models/containerConfig.h"
#include "core/models/protocols/telemtProtocolConfig.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QRegularExpression>
#include <QtGlobal>
using namespace amnezia;
namespace {
constexpr QLatin1String kTelemtClientJsonPath("/data/amnezia-telemt-client.json");
constexpr QLatin1String kTelemtClientJsonUploadPath("data/amnezia-telemt-client.json");
constexpr QLatin1String kTelemtSecretPath("/data/secret");
}
TelemtInstaller::TelemtInstaller(QObject *parent) : InstallerBase(parent) {}
ErrorCode TelemtInstaller::extractConfigFromContainer(DockerContainer container, const ServerCredentials &credentials,
SshSession *sshSession, ContainerConfig &config) {
if (container != DockerContainer::Telemt || !sshSession) {
return ErrorCode::NoError;
}
TelemtProtocolConfig *tc = config.getTelemtProtocolConfig();
if (!tc) {
return ErrorCode::NoError;
}
ErrorCode jsonErr = ErrorCode::NoError;
const QByteArray jsonRaw =
sshSession->getTextFileFromContainer(container, credentials, QString(kTelemtClientJsonPath), jsonErr);
if (jsonErr == ErrorCode::NoError && !jsonRaw.trimmed().isEmpty()) {
QJsonParseError parseError;
const QJsonDocument doc = QJsonDocument::fromJson(jsonRaw.trimmed(), &parseError);
if (parseError.error == QJsonParseError::NoError && doc.isObject()) {
QJsonObject merged = tc->toJson();
const QJsonObject snap = doc.object();
for (auto it = snap.constBegin(); it != snap.constEnd(); ++it) {
merged.insert(it.key(), it.value());
}
*tc = TelemtProtocolConfig::fromJson(merged);
}
}
ErrorCode secretErr = ErrorCode::NoError;
const QByteArray secretRaw =
sshSession->getTextFileFromContainer(container, credentials, QString(kTelemtSecretPath), secretErr);
const QString sec = QString::fromUtf8(secretRaw).trimmed();
if (sec.length() == 32) {
static const QRegularExpression hex32(QStringLiteral("^[0-9a-fA-F]{32}$"));
if (hex32.match(sec).hasMatch()) {
tc->secret = sec;
}
}
return ErrorCode::NoError;
}
void TelemtInstaller::uploadClientSettingsSnapshot(SshSession &sshSession, const ServerCredentials &credentials,
DockerContainer container, const ContainerConfig &config) {
const TelemtProtocolConfig *tc = config.getTelemtProtocolConfig();
if (!tc) {
return;
}
const QByteArray payload = QJsonDocument(tc->toJson()).toJson(QJsonDocument::Compact);
const ErrorCode err = sshSession.uploadTextFileToContainer(container, credentials, QString::fromUtf8(payload),
QString(kTelemtClientJsonUploadPath));
if (err != ErrorCode::NoError) {
qWarning() << "TelemtInstaller::uploadClientSettingsSnapshot failed" << err;
}
}
+20
View File
@@ -0,0 +1,20 @@
#ifndef TELEMTINSTALLER_H
#define TELEMTINSTALLER_H
#include "installerBase.h"
class TelemtInstaller : public InstallerBase {
Q_OBJECT
public:
explicit TelemtInstaller(QObject *parent = nullptr);
amnezia::ErrorCode
extractConfigFromContainer(amnezia::DockerContainer container, const amnezia::ServerCredentials &credentials,
SshSession *sshSession, amnezia::ContainerConfig &config) override;
static void uploadClientSettingsSnapshot(SshSession &sshSession, const amnezia::ServerCredentials &credentials,
amnezia::DockerContainer container,
const amnezia::ContainerConfig &config);
};
#endif // TELEMTINSTALLER_H
+251 -8
View File
@@ -14,8 +14,18 @@
#include "core/models/protocols/xrayProtocolConfig.h"
#include "logger.h"
namespace {
namespace
{
Logger logger("XrayInstaller");
// Xray expects uTLS preset names (chrome, firefox, …). Old Amnezia/server templates used "Mozilla/5.0".
QString normalizeXrayFingerprint(const QString &fp)
{
if (fp.isEmpty() || fp.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) {
return QString::fromLatin1(protocols::xray::defaultFingerprint);
}
return fp;
}
}
using namespace amnezia;
@@ -63,18 +73,251 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c
}
QJsonObject streamSettings = inbound[protocols::xray::streamSettings].toObject();
QJsonObject realitySettings = streamSettings[protocols::xray::realitySettings].toObject();
if (!realitySettings.contains(protocols::xray::serverNames)) {
logger.error() << "Settings missing 'serverNames' field";
auto *xrayConfig = config.getXrayProtocolConfig();
if (!xrayConfig) {
logger.error() << "No XrayProtocolConfig in ContainerConfig";
return ErrorCode::InternalError;
}
QString siteName = realitySettings[protocols::xray::serverNames][0].toString();
XrayServerConfig &srv = xrayConfig->serverConfig;
if (auto* xrayConfig = config.getXrayProtocolConfig()) {
xrayConfig->serverConfig.site = siteName;
// ── Port ─────────────────────────────────────────────────────────
if (inbound.contains(protocols::xray::port)) {
srv.port = QString::number(inbound[protocols::xray::port].toInt());
}
// ── Network (transport) ───────────────────────────────────────────
QString networkVal = streamSettings.value(protocols::xray::network).toString("tcp");
if (networkVal == "xhttp") {
srv.transport = "xhttp";
} else if (networkVal == "kcp") {
srv.transport = "mkcp";
} else {
srv.transport = "raw";
}
// ── Security ──────────────────────────────────────────────────────
srv.security = streamSettings.value(protocols::xray::security).toString("reality");
// ── Reality settings ──────────────────────────────────────────────
if (srv.security == "reality") {
QJsonObject rs = streamSettings.value(protocols::xray::realitySettings).toObject();
// serverNames array → site + sni
if (rs.contains(protocols::xray::serverNames)) {
QString sniVal = rs[protocols::xray::serverNames].toArray().first().toString();
srv.sni = sniVal;
srv.site = sniVal;
} else if (rs.contains(protocols::xray::serverName)) {
srv.sni = rs[protocols::xray::serverName].toString();
srv.site = srv.sni;
}
srv.fingerprint = normalizeXrayFingerprint(rs.value(protocols::xray::fingerprint).toString());
}
// ── TLS settings ──────────────────────────────────────────────────
if (srv.security == "tls") {
QJsonObject tls = streamSettings.value("tlsSettings").toObject();
srv.sni = tls.value(protocols::xray::serverName).toString();
srv.fingerprint = normalizeXrayFingerprint(tls.value(protocols::xray::fingerprint).toString());
QJsonArray alpnArr = tls.value("alpn").toArray();
QStringList alpnList;
for (const QJsonValue &v : alpnArr) {
alpnList << v.toString();
}
srv.alpn = alpnList.join(",");
}
// ── Flow (from users array) ───────────────────────────────────────
if (inbound.contains(protocols::xray::settings)) {
QJsonObject s = inbound[protocols::xray::settings].toObject();
QJsonArray clientsArr = s.value(protocols::xray::clients).toArray();
if (!clientsArr.isEmpty()) {
srv.flow = clientsArr[0].toObject().value(protocols::xray::flow).toString();
}
}
// ── XHTTP settings (Xray-core SplitHTTPConfig + legacy Amnezia keys) ──
if (srv.transport == "xhttp") {
QJsonObject xhttpObj = streamSettings.value("xhttpSettings").toObject();
{
const QString m = xhttpObj.value("mode").toString();
if (m.isEmpty() || m == QLatin1String("auto"))
srv.xhttp.mode = QStringLiteral("Auto");
else if (m == QLatin1String("packet-up"))
srv.xhttp.mode = QStringLiteral("Packet-up");
else if (m == QLatin1String("stream-up"))
srv.xhttp.mode = QStringLiteral("Stream-up");
else if (m == QLatin1String("stream-one"))
srv.xhttp.mode = QStringLiteral("Stream-one");
else
srv.xhttp.mode = m;
}
srv.xhttp.host = xhttpObj.value("host").toString();
srv.xhttp.path = xhttpObj.value("path").toString();
{
const QJsonObject hdrs = xhttpObj.value("headers").toObject();
if (hdrs.contains(QLatin1String("Host")) || !hdrs.isEmpty())
srv.xhttp.headersTemplate = QStringLiteral("HTTP");
}
if (xhttpObj.contains(QLatin1String("uplinkHTTPMethod")))
srv.xhttp.uplinkMethod = xhttpObj.value("uplinkHTTPMethod").toString();
else
srv.xhttp.uplinkMethod = xhttpObj.value("method").toString();
srv.xhttp.disableGrpc = xhttpObj.value("noGRPCHeader").toBool(true);
srv.xhttp.disableSse = xhttpObj.value("noSSEHeader").toBool(true);
auto sessionSeqUi = [](const QString &core) -> QString {
if (core.isEmpty() || core == QLatin1String("path"))
return QStringLiteral("Path");
if (core == QLatin1String("cookie"))
return QStringLiteral("Cookie");
if (core == QLatin1String("header"))
return QStringLiteral("Header");
if (core == QLatin1String("query"))
return QStringLiteral("Query");
return core;
};
QString sess = xhttpObj.value("sessionPlacement").toString();
if (sess.isEmpty())
sess = xhttpObj.value("scSessionPlacement").toString();
srv.xhttp.sessionPlacement = sessionSeqUi(sess);
QString seq = xhttpObj.value("seqPlacement").toString();
if (seq.isEmpty())
seq = xhttpObj.value("scSeqPlacement").toString();
srv.xhttp.seqPlacement = sessionSeqUi(seq);
auto uplinkDataUi = [](const QString &core) -> QString {
if (core.isEmpty() || core == QLatin1String("body"))
return QStringLiteral("Body");
if (core == QLatin1String("auto"))
return QStringLiteral("Auto");
if (core == QLatin1String("header"))
return QStringLiteral("Header");
if (core == QLatin1String("cookie"))
return QStringLiteral("Cookie");
return core;
};
QString udata = xhttpObj.value("uplinkDataPlacement").toString();
if (udata.isEmpty())
udata = xhttpObj.value("scUplinkDataPlacement").toString();
srv.xhttp.uplinkDataPlacement = uplinkDataUi(udata);
srv.xhttp.sessionKey = xhttpObj.value("sessionKey").toString();
srv.xhttp.seqKey = xhttpObj.value("seqKey").toString();
srv.xhttp.uplinkDataKey = xhttpObj.value("uplinkDataKey").toString();
if (xhttpObj.contains(QLatin1String("uplinkChunkSize"))) {
QJsonObject uc = xhttpObj.value("uplinkChunkSize").toObject();
if (!uc.isEmpty())
srv.xhttp.uplinkChunkSize = QString::number(uc.value("from").toInt());
} else if (xhttpObj.contains(QLatin1String("xhttpUplinkChunkSize"))) {
srv.xhttp.uplinkChunkSize = QString::number(xhttpObj.value("xhttpUplinkChunkSize").toInt());
}
if (xhttpObj.contains(QLatin1String("scMaxBufferedPosts"))) {
srv.xhttp.scMaxBufferedPosts = QString::number(xhttpObj.value("scMaxBufferedPosts").toVariant().toLongLong());
}
auto readRange = [&](const char *key, QString &minOut, QString &maxOut) {
QJsonObject r = xhttpObj.value(QLatin1String(key)).toObject();
if (!r.isEmpty()) {
minOut = QString::number(r.value("from").toInt());
maxOut = QString::number(r.value("to").toInt());
}
};
readRange("scMaxEachPostBytes", srv.xhttp.scMaxEachPostBytesMin, srv.xhttp.scMaxEachPostBytesMax);
readRange("scMinPostsIntervalMs", srv.xhttp.scMinPostsIntervalMsMin, srv.xhttp.scMinPostsIntervalMsMax);
readRange("scStreamUpServerSecs", srv.xhttp.scStreamUpServerSecsMin, srv.xhttp.scStreamUpServerSecsMax);
auto loadPaddingFromObject = [&](const QJsonObject &pad) {
if (pad.contains(QLatin1String("xPaddingObfsMode")))
srv.xhttp.xPadding.obfsMode = pad.value("xPaddingObfsMode").toBool(true);
srv.xhttp.xPadding.key = pad.value("xPaddingKey").toString();
srv.xhttp.xPadding.header = pad.value("xPaddingHeader").toString();
srv.xhttp.xPadding.placement = pad.value("xPaddingPlacement").toString();
srv.xhttp.xPadding.method = pad.value("xPaddingMethod").toString();
QJsonObject bytesRange = pad.value("xPaddingBytes").toObject();
if (!bytesRange.isEmpty()) {
srv.xhttp.xPadding.bytesMin = QString::number(bytesRange.value("from").toInt());
srv.xhttp.xPadding.bytesMax = QString::number(bytesRange.value("to").toInt());
}
QString pl = srv.xhttp.xPadding.placement.toLower();
if (pl == QLatin1String("cookie"))
srv.xhttp.xPadding.placement = QStringLiteral("Cookie");
else if (pl == QLatin1String("header"))
srv.xhttp.xPadding.placement = QStringLiteral("Header");
else if (pl == QLatin1String("query"))
srv.xhttp.xPadding.placement = QStringLiteral("Query");
else if (pl == QLatin1String("queryinheader"))
srv.xhttp.xPadding.placement = QStringLiteral("Query in header");
QString met = srv.xhttp.xPadding.method.toLower();
if (met == QLatin1String("repeat-x"))
srv.xhttp.xPadding.method = QStringLiteral("Repeat-x");
else if (met == QLatin1String("tokenish"))
srv.xhttp.xPadding.method = QStringLiteral("Tokenish");
};
if (xhttpObj.contains(QLatin1String("xPaddingObfsMode")) || xhttpObj.contains(QLatin1String("xPaddingKey"))
|| !xhttpObj.value("xPaddingBytes").toObject().isEmpty()) {
loadPaddingFromObject(xhttpObj);
} else if (xhttpObj.contains(QLatin1String("xPadding")) && xhttpObj.value("xPadding").isObject()) {
const QJsonObject nested = xhttpObj.value("xPadding").toObject();
if (!nested.isEmpty()) {
loadPaddingFromObject(nested);
if (!nested.contains(QLatin1String("xPaddingObfsMode")))
srv.xhttp.xPadding.obfsMode = true;
}
}
if (xhttpObj.contains(QLatin1String("xmux"))) {
QJsonObject mux = xhttpObj.value("xmux").toObject();
srv.xhttp.xmux.enabled = true;
auto readMuxRange = [&](const char *key, QString &minOut, QString &maxOut) {
QJsonObject r = mux.value(QLatin1String(key)).toObject();
if (!r.isEmpty()) {
minOut = QString::number(r.value("from").toInt());
maxOut = QString::number(r.value("to").toInt());
}
};
readMuxRange("maxConcurrency", srv.xhttp.xmux.maxConcurrencyMin, srv.xhttp.xmux.maxConcurrencyMax);
readMuxRange("maxConnections", srv.xhttp.xmux.maxConnectionsMin, srv.xhttp.xmux.maxConnectionsMax);
readMuxRange("cMaxReuseTimes", srv.xhttp.xmux.cMaxReuseTimesMin, srv.xhttp.xmux.cMaxReuseTimesMax);
readMuxRange("hMaxRequestTimes", srv.xhttp.xmux.hMaxRequestTimesMin, srv.xhttp.xmux.hMaxRequestTimesMax);
readMuxRange("hMaxReusableSecs", srv.xhttp.xmux.hMaxReusableSecsMin, srv.xhttp.xmux.hMaxReusableSecsMax);
if (mux.contains(QLatin1String("hKeepAlivePeriod")))
srv.xhttp.xmux.hKeepAlivePeriod = QString::number(mux.value("hKeepAlivePeriod").toVariant().toLongLong());
}
}
// ── mKCP settings ─────────────────────────────────────────────────
if (srv.transport == "mkcp") {
QJsonObject kcp = streamSettings.value("kcpSettings").toObject();
if (kcp.contains("tti")) {
srv.mkcp.tti = QString::number(kcp["tti"].toInt());
}
if (kcp.contains("uplinkCapacity")) {
srv.mkcp.uplinkCapacity = QString::number(kcp["uplinkCapacity"].toInt());
}
if (kcp.contains("downlinkCapacity")) {
srv.mkcp.downlinkCapacity = QString::number(kcp["downlinkCapacity"].toInt());
}
if (kcp.contains("readBufferSize")) {
srv.mkcp.readBufferSize = QString::number(kcp["readBufferSize"].toInt());
}
if (kcp.contains("writeBufferSize")) {
srv.mkcp.writeBufferSize = QString::number(kcp["writeBufferSize"].toInt());
}
srv.mkcp.congestion = kcp.value("congestion").toBool(true);
}
return ErrorCode::NoError;
}
+444
View File
@@ -0,0 +1,444 @@
#include "configmanager.h"
#include "core/controllers/gatewayController.h"
#include "core/repositories/secureAppSettingsRepository.h"
#include "core/repositories/secureServersRepository.h"
#include "core/utils/api/apiUtils.h"
#include "core/utils/constants/apiConstants.h"
#include "core/utils/constants/apiKeys.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/containers/containerUtils.h"
#include "portavailabilityhelper.h"
#include "proxylogger.h"
#include "version.h"
#include <QDir>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonParseError>
#include <QSaveFile>
#include <QSysInfo>
#include <QStandardPaths>
#include <QUuid>
using namespace amnezia;
ConfigManager::ConfigManager(SecureServersRepository *serversRepository, SecureAppSettingsRepository *appSettingsRepository)
: m_serversRepository(serversRepository), m_appSettingsRepository(appSettingsRepository)
{
ProxyLogger::getInstance().debug("ConfigManager initialized (repository-backed)");
}
namespace {
namespace gateway_key {
constexpr char vless[] = "vless";
} // namespace gateway_key
constexpr quint16 kDefaultProxyPort = 10808;
constexpr int kProxyPortMin = 1024;
constexpr int kProxyPortMax = 65535;
int resolveProxyPort(SecureAppSettingsRepository *appSettings)
{
if (!appSettings) {
return kDefaultProxyPort;
}
const quint16 port = appSettings->localProxyPort();
if (port < kProxyPortMin || port > kProxyPortMax) {
return kDefaultProxyPort;
}
return static_cast<int>(port);
}
} // namespace
bool ConfigManager::applyProxyPortToConfig(QJsonObject &config, int port) const
{
if (!config.contains("inbounds") || !config.value("inbounds").isArray()) {
return false;
}
QJsonArray inbounds = config.value("inbounds").toArray();
if (inbounds.isEmpty() || !inbounds.at(0).isObject()) {
return false;
}
QJsonObject firstInbound = inbounds.at(0).toObject();
firstInbound.insert("port", port);
inbounds[0] = firstInbound;
config.insert("inbounds", inbounds);
return true;
}
QString ConfigManager::serializeConfig(const QJsonObject &config) const
{
return QString::fromUtf8(QJsonDocument(config).toJson(QJsonDocument::Compact));
}
std::optional<ConfigManager::ConfigData> ConfigManager::buildConfig(QString &errorDescription) const
{
errorDescription.clear();
if (!m_serversRepository || !m_appSettingsRepository) {
const QString message = QStringLiteral("Local proxy repositories are not available");
ProxyLogger::getInstance().error(message);
errorDescription = message;
return std::nullopt;
}
const QString ownerId = m_appSettingsRepository->localProxyOwnerId();
if (ownerId.isEmpty()) {
const QString message = QStringLiteral("Local proxy owner server id is not configured");
ProxyLogger::getInstance().warning(message);
errorDescription = message;
return std::nullopt;
}
const auto ownerServer = m_serversRepository->serverJsonById(ownerId);
if (!ownerServer) {
const QString message = QStringLiteral("Owner server with id %1 not found").arg(ownerId);
ProxyLogger::getInstance().error(message);
errorDescription = message;
return std::nullopt;
}
if (!apiUtils::isPremiumServer(*ownerServer)) {
const QString message = QStringLiteral("Server %1 is not premium, local proxy is unavailable")
.arg(ownerServer->value(configKey::name).toString());
ProxyLogger::getInstance().warning(message);
errorDescription = message;
return std::nullopt;
}
const auto serializedConfig = extractSerializedXrayConfig(*ownerServer);
if (!serializedConfig || serializedConfig->isEmpty()) {
const QString message = QStringLiteral("Server %1 lacks Xray last_config payload")
.arg(ownerServer->value(configKey::name).toString());
ProxyLogger::getInstance().error(message);
errorDescription = message;
return std::nullopt;
}
QJsonParseError parseError;
const QJsonDocument doc = QJsonDocument::fromJson(serializedConfig->toUtf8(), &parseError);
if (parseError.error != QJsonParseError::NoError || !doc.isObject()) {
const QString message = QStringLiteral("Failed to parse Xray config JSON: %1").arg(parseError.errorString());
ProxyLogger::getInstance().error(message);
errorDescription = message;
return std::nullopt;
}
ConfigData data;
data.ownerId = ownerId;
data.serverName = ownerServer->value(configKey::name).toString();
data.parsedConfig = doc.object();
const int proxyPort = resolveProxyPort(m_appSettingsRepository);
if (applyProxyPortToConfig(data.parsedConfig, proxyPort)) {
data.serializedConfig = serializeConfig(data.parsedConfig);
} else {
ProxyLogger::getInstance().warning(QStringLiteral("Failed to override local proxy inbound port; using original config"));
data.serializedConfig = *serializedConfig;
}
return data;
}
std::optional<ConfigManager::ConfigData> ConfigManager::buildConfigWithFetch(QString &errorDescription) const
{
errorDescription.clear();
if (!m_serversRepository || !m_appSettingsRepository) {
const QString message = QStringLiteral("Local proxy repositories are not available");
ProxyLogger::getInstance().error(message);
errorDescription = message;
return std::nullopt;
}
const QString ownerId = m_appSettingsRepository->localProxyOwnerId();
if (ownerId.isEmpty()) {
const QString message = QStringLiteral("Local proxy owner server id is not configured");
ProxyLogger::getInstance().warning(message);
errorDescription = message;
return std::nullopt;
}
const auto ownerServer = m_serversRepository->serverJsonById(ownerId);
if (!ownerServer) {
const QString message = QStringLiteral("Owner server with id %1 not found").arg(ownerId);
ProxyLogger::getInstance().error(message);
errorDescription = message;
return std::nullopt;
}
if (!apiUtils::isPremiumServer(*ownerServer)) {
const QString message = QStringLiteral("Server %1 is not premium, local proxy is unavailable")
.arg(ownerServer->value(configKey::name).toString());
ProxyLogger::getInstance().warning(message);
errorDescription = message;
return std::nullopt;
}
auto serializedConfig = extractSerializedXrayConfig(*ownerServer);
if (!serializedConfig || serializedConfig->isEmpty()) {
auto fetchedConfig = fetchSerializedXrayConfigFromGateway(*ownerServer, errorDescription);
if (!fetchedConfig || fetchedConfig->isEmpty()) {
return std::nullopt;
}
serializedConfig = fetchedConfig;
}
QJsonParseError parseError;
const QJsonDocument doc = QJsonDocument::fromJson(serializedConfig->toUtf8(), &parseError);
if (parseError.error != QJsonParseError::NoError || !doc.isObject()) {
const QString message = QStringLiteral("Failed to parse Xray config JSON: %1").arg(parseError.errorString());
ProxyLogger::getInstance().error(message);
errorDescription = message;
return std::nullopt;
}
ConfigData data;
data.ownerId = ownerId;
data.serverName = ownerServer->value(configKey::name).toString();
data.parsedConfig = doc.object();
int selectedPort = resolveProxyPort(m_appSettingsRepository);
const bool isUserDefinedPort = m_appSettingsRepository->isLocalProxyPortUserDefined();
if (!PortAvailabilityHelper::isPortAvailable(selectedPort)) {
const bool canAutoSelect = !isUserDefinedPort && selectedPort == kDefaultProxyPort;
if (canAutoSelect) {
const auto freePort = PortAvailabilityHelper::findFirstAvailablePort(kDefaultProxyPort + 1, kProxyPortMax);
if (!freePort) {
errorDescription = QStringLiteral("No available local proxy port in range %1-%2")
.arg(kDefaultProxyPort + 1)
.arg(kProxyPortMax);
ProxyLogger::getInstance().error(errorDescription);
return std::nullopt;
}
selectedPort = *freePort;
} else {
errorDescription = QStringLiteral("Local proxy port %1 is already in use")
.arg(selectedPort);
ProxyLogger::getInstance().error(errorDescription);
return std::nullopt;
}
}
if (applyProxyPortToConfig(data.parsedConfig, selectedPort)) {
data.serializedConfig = serializeConfig(data.parsedConfig);
} else {
ProxyLogger::getInstance().warning(QStringLiteral("Failed to override local proxy inbound port; using original config"));
data.serializedConfig = *serializedConfig;
}
return data;
}
bool ConfigManager::writeTempConfig(const QString &serializedConfig, QString &configPath, QString &errorDescription) const
{
errorDescription.clear();
configPath.clear();
const QString directory = tempDirectory();
if (!QDir().mkpath(directory)) {
const QString message = QStringLiteral("Failed to create temp config directory: %1").arg(directory);
ProxyLogger::getInstance().error(message);
errorDescription = message;
return false;
}
const QString path = tempConfigPath();
QSaveFile file(path);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
const QString message = QStringLiteral("Failed to open temp config file %1: %2").arg(path, file.errorString());
ProxyLogger::getInstance().error(message);
errorDescription = message;
return false;
}
if (file.write(serializedConfig.toUtf8()) == -1) {
const QString message = QStringLiteral("Failed to write temp config file %1: %2").arg(path, file.errorString());
ProxyLogger::getInstance().error(message);
errorDescription = message;
return false;
}
if (!file.commit()) {
const QString message = QStringLiteral("Failed to commit temp config file %1").arg(path);
ProxyLogger::getInstance().error(message);
errorDescription = message;
return false;
}
ProxyLogger::getInstance().info(QStringLiteral("Xray config saved to %1").arg(path));
configPath = path;
return true;
}
bool ConfigManager::removeTempConfig() const
{
const QString path = tempConfigPath();
QFile file(path);
if (!file.exists()) {
return true;
}
if (!file.remove()) {
ProxyLogger::getInstance().warning(QStringLiteral("Failed to remove temp config file %1: %2").arg(path, file.errorString()));
return false;
}
ProxyLogger::getInstance().debug(QStringLiteral("Removed temp config file %1").arg(path));
return true;
}
QString ConfigManager::tempConfigPath() const
{
return QDir(tempDirectory()).filePath(QStringLiteral("xray_active.json"));
}
std::optional<QString> ConfigManager::extractSerializedXrayConfig(const QJsonObject &server) const
{
const QJsonArray containers = server.value(configKey::containers).toArray();
const QString targetContainer = ContainerUtils::containerToString(DockerContainer::Xray);
const QString protoKey = QString(configKey::xray);
for (const QJsonValue &value : containers) {
const QJsonObject container = value.toObject();
if (container.value(configKey::container).toString() != targetContainer) {
continue;
}
const QJsonObject proto = container.value(protoKey).toObject();
const QString serialized = proto.value(configKey::lastConfig).toString();
if (!serialized.isEmpty()) {
return serialized;
}
}
return std::nullopt;
}
std::optional<QString> ConfigManager::fetchSerializedXrayConfigFromGateway(const QJsonObject &server, QString &errorDescription) const
{
errorDescription.clear();
if (!m_appSettingsRepository) {
const QString message = QStringLiteral("App settings repository is not available");
ProxyLogger::getInstance().error(message);
errorDescription = message;
return std::nullopt;
}
const QJsonObject apiConfig = server.value(apiDefs::key::apiConfig).toObject();
if (apiConfig.isEmpty()) {
const QString message = QStringLiteral("Server API config is missing");
ProxyLogger::getInstance().warning(message);
errorDescription = message;
return std::nullopt;
}
const QString userCountryCode = apiConfig.value(apiDefs::key::userCountryCode).toString();
const QString serviceType = apiConfig.value(apiDefs::key::serviceType).toString();
if (userCountryCode.isEmpty() || serviceType.isEmpty()) {
const QString message = QStringLiteral("Server API config lacks service identifiers");
ProxyLogger::getInstance().warning(message);
errorDescription = message;
return std::nullopt;
}
QJsonObject apiPayload;
apiPayload[apiDefs::key::osVersion] = QSysInfo::productType();
apiPayload[apiDefs::key::appVersion] = QString(APP_VERSION);
const QString appLanguage = m_appSettingsRepository->getAppLanguage().name().split("_").first();
if (!appLanguage.isEmpty()) {
apiPayload[apiDefs::key::appLanguage] = appLanguage;
}
apiPayload[apiDefs::key::uuid] = m_appSettingsRepository->getInstallationUuid(true);
apiPayload[apiDefs::key::userCountryCode] = userCountryCode;
apiPayload[apiDefs::key::serviceType] = serviceType;
apiPayload[apiDefs::key::serviceProtocol] = QString(gateway_key::vless);
apiPayload[apiDefs::key::publicKey] = QUuid::createUuid().toString(QUuid::WithoutBraces);
const QJsonObject authData = server.value(apiDefs::key::authData).toObject();
if (!authData.isEmpty()) {
apiPayload[apiDefs::key::authData] = authData;
}
GatewayController gatewayController(m_appSettingsRepository->getGatewayEndpoint(), m_appSettingsRepository->isDevGatewayEnv(),
apiDefs::requestTimeoutMsecs, m_appSettingsRepository->isStrictKillSwitchEnabled());
QByteArray responseBody;
const amnezia::ErrorCode errorCode = gatewayController.post(QString("%1v1/config"), apiPayload, responseBody);
if (errorCode != amnezia::ErrorCode::NoError) {
const QString message = QStringLiteral("Gateway request failed with error code %1").arg(static_cast<int>(errorCode));
ProxyLogger::getInstance().error(message);
errorDescription = message;
return std::nullopt;
}
QJsonParseError responseError;
const QJsonDocument responseDoc = QJsonDocument::fromJson(responseBody, &responseError);
if (responseError.error != QJsonParseError::NoError || !responseDoc.isObject()) {
const QString message = QStringLiteral("Failed to parse gateway response: %1").arg(responseError.errorString());
ProxyLogger::getInstance().error(message);
errorDescription = message;
return std::nullopt;
}
QString data = responseDoc.object().value(configKey::config).toString();
if (data.isEmpty()) {
const QString message = QStringLiteral("Gateway response lacks config payload");
ProxyLogger::getInstance().error(message);
errorDescription = message;
return std::nullopt;
}
data.replace("vpn://", "");
QByteArray decoded = QByteArray::fromBase64(data.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
if (decoded.isEmpty()) {
const QString message = QStringLiteral("Gateway config payload is empty");
ProxyLogger::getInstance().error(message);
errorDescription = message;
return std::nullopt;
}
const QByteArray uncompressed = qUncompress(decoded);
if (!uncompressed.isEmpty()) {
decoded = uncompressed;
}
QJsonParseError configError;
const QJsonDocument configDoc = QJsonDocument::fromJson(decoded, &configError);
if (configError.error != QJsonParseError::NoError || !configDoc.isObject()) {
const QString message = QStringLiteral("Failed to parse gateway config JSON: %1").arg(configError.errorString());
ProxyLogger::getInstance().error(message);
errorDescription = message;
return std::nullopt;
}
const auto serializedConfig = extractSerializedXrayConfig(configDoc.object());
if (!serializedConfig || serializedConfig->isEmpty()) {
const QString message = QStringLiteral("Gateway response lacks Xray last_config payload");
ProxyLogger::getInstance().error(message);
errorDescription = message;
return std::nullopt;
}
ProxyLogger::getInstance().info("Fetched Xray config from gateway");
return serializedConfig;
}
QString ConfigManager::tempDirectory() const
{
const QString baseDir = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation);
if (baseDir.isEmpty()) {
return QDir::temp().filePath(QStringLiteral("amnezia_local_proxy"));
}
return QDir(baseDir).filePath(QStringLiteral("local_proxy"));
}
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include <optional>
#include <QJsonObject>
#include <QString>
class SecureServersRepository;
class SecureAppSettingsRepository;
class ConfigManager {
public:
struct ConfigData {
QString ownerId;
QString serverName;
QString serializedConfig;
QJsonObject parsedConfig;
};
ConfigManager(SecureServersRepository *serversRepository, SecureAppSettingsRepository *appSettingsRepository);
std::optional<ConfigData> buildConfig(QString &errorDescription) const;
std::optional<ConfigData> buildConfigWithFetch(QString &errorDescription) const;
bool writeTempConfig(const QString &serializedConfig, QString &configPath, QString &errorDescription) const;
bool removeTempConfig() const;
QString tempConfigPath() const;
private:
std::optional<QString> extractSerializedXrayConfig(const QJsonObject &server) const;
std::optional<QString> fetchSerializedXrayConfigFromGateway(const QJsonObject &server, QString &errorDescription) const;
QString tempDirectory() const;
bool applyProxyPortToConfig(QJsonObject &config, int port) const;
QString serializeConfig(const QJsonObject &config) const;
SecureServersRepository *m_serversRepository;
SecureAppSettingsRepository *m_appSettingsRepository;
};
+190
View File
@@ -0,0 +1,190 @@
#include "httpapi.h"
#include "proxylogger.h"
#include <QJsonObject>
#include <QJsonArray>
#include <QHostAddress>
#include <optional>
namespace {
std::optional<int> extractInboundPort(const QJsonObject &config)
{
if (!config.contains("inbounds") || !config["inbounds"].isArray()) {
return std::nullopt;
}
const QJsonArray inbounds = config["inbounds"].toArray();
if (inbounds.isEmpty() || !inbounds.at(0).isObject()) {
return std::nullopt;
}
const QJsonObject firstInbound = inbounds.at(0).toObject();
if (!firstInbound.contains("port")) {
return std::nullopt;
}
return firstInbound.value("port").toInt();
}
QJsonValue proxyPortValue(const std::optional<int> &port)
{
if (port.has_value()) {
return QJsonValue(*port);
}
return QJsonValue::Null;
}
QHttpServerResponse makeServiceUnavailablePingResponse()
{
QJsonObject payload{
{"status", "error"},
{"proxyPort", QJsonValue::Null}
};
return QHttpServerResponse(payload, QHttpServerResponse::StatusCode::ServiceUnavailable);
}
QHttpServerResponse makeServiceUnavailableStatusResponse()
{
QJsonObject payload{
{"status", "error"}
};
return QHttpServerResponse(payload, QHttpServerResponse::StatusCode::ServiceUnavailable);
}
} // namespace
HttpApi::HttpApi(QWeakPointer<IProxyService> service, QObject* parent)
: QObject(parent)
, m_tcpServer(new QTcpServer(this))
, m_service(service)
{
ProxyLogger::getInstance().debug("HttpApi initialized");
}
HttpApi::~HttpApi()
{
stop();
}
bool HttpApi::start(quint16 port)
{
ProxyLogger::getInstance().info(QString("Starting HTTP API server on port %1").arg(port));
if (!m_tcpServer->listen(QHostAddress::LocalHost, port)) {
ProxyLogger::getInstance().error(QString("Failed to start HTTP API server on port %1").arg(port));
return false;
}
setupRoutes();
m_server.bind(m_tcpServer.data());
ProxyLogger::getInstance().info(QString("HTTP API server is running on localhost:%1").arg(m_tcpServer->serverPort()));
ProxyLogger::getInstance().debug("Available endpoints:\n"
" POST /api/v1/up\n"
" POST /api/v1/down\n"
" GET /api/v1/ping");
return true;
}
void HttpApi::stop()
{
ProxyLogger::getInstance().info("Stopping HTTP API server");
if (m_tcpServer) {
m_tcpServer->close();
}
}
void HttpApi::setupRoutes()
{
ProxyLogger::getInstance().debug("Setting up HTTP API routes");
m_server.route("/api/v1/up", QHttpServerRequest::Method::Post,
[this] {
ProxyLogger::getInstance().debug("Handling POST /api/v1/up request");
return handlePostUp();
});
m_server.route("/api/v1/down", QHttpServerRequest::Method::Post,
[this] {
ProxyLogger::getInstance().debug("Handling POST /api/v1/down request");
return handlePostDown();
});
m_server.route("/api/v1/ping", QHttpServerRequest::Method::Get,
[this] {
ProxyLogger::getInstance().debug("Handling GET /api/v1/ping request");
return handleGetPing();
});
}
QHttpServerResponse HttpApi::handlePostUp()
{
if (auto service = m_service.lock()) {
const bool started = service->startXray();
QJsonObject response;
response["status"] = started ? "ok" : "error";
const auto port = started ? extractInboundPort(service->getConfig()) : std::optional<int>{};
response["proxyPort"] = proxyPortValue(port);
if (started) {
if (port.has_value()) {
ProxyLogger::getInstance().info(QString("Xray process started on port %1").arg(*port));
} else {
ProxyLogger::getInstance().warning("Xray started but inbound port is unknown (local proxy owner may be missing)");
}
} else {
ProxyLogger::getInstance().warning("Failed to start Xray process via HTTP API");
}
return QHttpServerResponse(response);
}
ProxyLogger::getInstance().error("Service unavailable: proxy backend is not initialized");
return makeServiceUnavailablePingResponse();
}
QHttpServerResponse HttpApi::handlePostDown()
{
if (auto service = m_service.lock()) {
const bool stopped = service->stopXray();
QJsonObject response;
response["status"] = stopped ? "ok" : "error";
if (!stopped) {
ProxyLogger::getInstance().warning("Failed to stop Xray process via HTTP API");
} else {
ProxyLogger::getInstance().info("Xray process stopped via HTTP API");
}
return QHttpServerResponse(response);
}
ProxyLogger::getInstance().error("Service unavailable: proxy backend is not initialized");
return makeServiceUnavailableStatusResponse();
}
QHttpServerResponse HttpApi::handleGetPing() const
{
if (auto service = m_service.lock()) {
QJsonObject response;
response["status"] = "ok";
const bool isRunning = service->isXrayRunning();
if (isRunning) {
const auto port = extractInboundPort(service->getConfig());
response["proxyPort"] = proxyPortValue(port);
if (port.has_value()) {
ProxyLogger::getInstance().debug(QString("Xray port: %1").arg(*port));
} else {
ProxyLogger::getInstance().warning("Unable to detect inbound port while Xray is running");
}
} else {
response["proxyPort"] = QJsonValue::Null;
ProxyLogger::getInstance().debug("Xray is not running");
}
return QHttpServerResponse(response);
}
ProxyLogger::getInstance().error("Service unavailable: proxy backend is not initialized");
return makeServiceUnavailablePingResponse();
}
+32
View File
@@ -0,0 +1,32 @@
#pragma once
#include <QObject>
#include <QScopedPointer>
#include <QHttpServer>
#include <QHttpServerRequest>
#include <QHttpServerResponse>
#include <QTcpServer>
#include <QWeakPointer>
#include "iproxyservice.h"
class HttpApi : public QObject {
Q_OBJECT
public:
explicit HttpApi(QWeakPointer<IProxyService> service, QObject* parent = nullptr);
~HttpApi();
bool start(quint16 port);
void stop();
private:
void setupRoutes();
QHttpServerResponse handlePostUp();
QHttpServerResponse handlePostDown();
QHttpServerResponse handleGetPing() const;
QHttpServer m_server;
QScopedPointer<QTcpServer> m_tcpServer;
QWeakPointer<IProxyService> m_service;
};
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include <QJsonObject>
#include <QString>
class IProxyService {
public:
virtual ~IProxyService() = default;
virtual QJsonObject getConfig() = 0;
virtual bool startXray() = 0;
virtual bool stopXray() = 0;
virtual bool isXrayRunning() const = 0;
virtual qint64 getXrayProcessId() const = 0;
virtual QString getXrayError() const = 0;
};
@@ -0,0 +1,43 @@
#include "portavailabilityhelper.h"
#include <QHostAddress>
#include <QTcpServer>
namespace {
constexpr int kProxyPortMin = 1024;
constexpr int kProxyPortMax = 65535;
}
bool PortAvailabilityHelper::isPortAvailable(int port)
{
if (port < kProxyPortMin || port > kProxyPortMax) {
return false;
}
QTcpServer server;
const bool success = server.listen(QHostAddress::LocalHost, static_cast<quint16>(port));
server.close();
return success;
}
std::optional<int> PortAvailabilityHelper::findFirstAvailablePort(int startPort, int endPort)
{
if (startPort < kProxyPortMin) {
startPort = kProxyPortMin;
}
if (endPort > kProxyPortMax) {
endPort = kProxyPortMax;
}
if (startPort > endPort) {
return std::nullopt;
}
for (int port = startPort; port <= endPort; ++port) {
if (isPortAvailable(port)) {
return port;
}
}
return std::nullopt;
}
@@ -0,0 +1,11 @@
#pragma once
#include <optional>
class PortAvailabilityHelper
{
public:
static bool isPortAvailable(int port);
static std::optional<int> findFirstAvailablePort(int startPort, int endPort);
};
+133
View File
@@ -0,0 +1,133 @@
#include "proxylogger.h"
#include <QDir>
#include <QTextStream>
ProxyLogger::ProxyLogger() : m_maxFileSize(0), m_currentLevel(LogLevel::Info)
{
}
ProxyLogger::~ProxyLogger()
{
}
ProxyLogger& ProxyLogger::getInstance()
{
static ProxyLogger instance;
return instance;
}
void ProxyLogger::init(const QString& logPath, qint64 maxFileSize)
{
QMutexLocker locker(&m_mutex);
m_logPath = logPath;
m_maxFileSize = maxFileSize;
// Create logs directory if it doesn't exist
QDir dir = QFileInfo(m_logPath).dir();
if (!dir.exists()) {
dir.mkpath(".");
}
}
void ProxyLogger::setLogLevel(LogLevel level)
{
m_currentLevel = level;
}
void ProxyLogger::log(LogLevel level, const QString& message)
{
logInternal(level, message);
}
void ProxyLogger::debug(const QString& message)
{
logInternal(LogLevel::Debug, message);
}
void ProxyLogger::info(const QString& message)
{
logInternal(LogLevel::Info, message);
}
void ProxyLogger::warning(const QString& message)
{
logInternal(LogLevel::Warning, message);
}
void ProxyLogger::error(const QString& message)
{
logInternal(LogLevel::Error, message);
}
void ProxyLogger::logInternal(LogLevel level, const QString& message)
{
if (m_logPath.isEmpty()) {
return;
}
if (level < m_currentLevel) {
return;
}
QMutexLocker locker(&m_mutex);
checkRotation();
QFile file(m_logPath);
if (!openLogFile(file)) {
return;
}
QTextStream stream(&file);
QString timestamp = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss.zzz");
stream << QString("[%1] [%2] %3\n").arg(timestamp, levelToString(level), message);
stream.flush();
file.close();
}
QString ProxyLogger::levelToString(LogLevel level)
{
switch (level) {
case LogLevel::Debug: return "DEBUG";
case LogLevel::Info: return "INFO";
case LogLevel::Warning: return "WARNING";
case LogLevel::Error: return "ERROR";
default: return "UNKNOWN";
}
}
qint64 ProxyLogger::getCurrentFileSize() const
{
QFile file(m_logPath);
if (file.exists()) {
return file.size();
}
return -1;
}
void ProxyLogger::checkRotation()
{
if (m_maxFileSize > 0 && getCurrentFileSize() >= m_maxFileSize) {
// Delete the oldest file
QFile::remove(QString("%1.%2").arg(m_logPath).arg(MAX_BACKUP_FILES));
// Shift existing files
for (int i = MAX_BACKUP_FILES - 1; i >= 1; --i) {
QString oldName = QString("%1.%2").arg(m_logPath).arg(i);
QString newName = QString("%1.%2").arg(m_logPath).arg(i + 1);
QFile::rename(oldName, newName);
}
// Rename current file
QFile::rename(m_logPath, m_logPath + ".1");
}
}
bool ProxyLogger::openLogFile(QFile& file)
{
if (!file.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) {
qDebug() << "Failed to open log file:" << m_logPath;
return false;
}
return true;
}
+54
View File
@@ -0,0 +1,54 @@
#ifndef LOCAL_PROXY_LOGGER_H
#define LOCAL_PROXY_LOGGER_H
#include <QObject>
#include <QFile>
#include <QDateTime>
#include <QMutex>
#include <QString>
class ProxyLogger
{
public:
enum class LogLevel {
Debug,
Info,
Warning,
Error
};
static ProxyLogger& getInstance();
void init(const QString& logPath, qint64 maxFileSize = 1024 * 1024 * 10); // 10MB by default
void setLogLevel(LogLevel level);
// Main logging method
void log(LogLevel level, const QString& message);
// Helper methods for convenience
void debug(const QString& message);
void info(const QString& message);
void warning(const QString& message);
void error(const QString& message);
private:
ProxyLogger();
~ProxyLogger();
ProxyLogger(const ProxyLogger&) = delete;
ProxyLogger& operator=(const ProxyLogger&) = delete;
void logInternal(LogLevel level, const QString& message);
void checkRotation();
QString levelToString(LogLevel level);
bool openLogFile(QFile& file);
qint64 getCurrentFileSize() const;
QString m_logPath;
qint64 m_maxFileSize;
LogLevel m_currentLevel;
QMutex m_mutex;
static const int MAX_BACKUP_FILES = 3;
};
#endif // LOCAL_PROXY_LOGGER_H
+114
View File
@@ -0,0 +1,114 @@
#include "proxyserver.h"
#include "core/repositories/secureAppSettingsRepository.h"
#include "core/repositories/secureServersRepository.h"
#include <QDebug>
ProxyServer::ProxyServer(SecureServersRepository *serversRepository, SecureAppSettingsRepository *appSettingsRepository,
QObject *parent)
: QObject(parent)
, m_appSettingsRepository(appSettingsRepository)
, m_service(new ProxyService(serversRepository, appSettingsRepository, this))
{
m_lastRestartToken = m_appSettingsRepository ? m_appSettingsRepository->localProxyRestartToken() : 0;
}
ProxyServer::~ProxyServer()
{
stop();
}
bool ProxyServer::start(quint16 port)
{
if (m_isRunning) {
if (m_currentApiPort == port) {
qInfo() << "Local proxy: already running on port" << port;
return true;
}
qInfo() << "Local proxy: restarting on new port" << port;
stop();
}
m_api.reset(new HttpApi(m_service.toWeakRef()));
const bool apiStarted = m_api->start(port);
if (!apiStarted) {
qWarning() << "Local proxy: port is busy:" << port;
m_api.reset();
m_isRunning = false;
m_currentApiPort = 0;
return false;
}
m_isRunning = true;
m_currentApiPort = port;
return true;
}
void ProxyServer::stop()
{
stopXrayProcess();
if (m_api) {
m_api->stop();
m_api.reset();
}
m_isRunning = false;
m_currentApiPort = 0;
m_currentProxyPort = 0;
}
bool ProxyServer::startXrayProcess()
{
return m_service->startXray();
}
void ProxyServer::stopXrayProcess()
{
m_service->stopXray();
}
bool ProxyServer::syncSettings()
{
if (!m_isRunning) {
qDebug() << "Local proxy: syncSettings called but server is not running";
return false;
}
const quint16 newProxyPort = m_appSettingsRepository ? m_appSettingsRepository->localProxyPort() : 0;
const int restartToken = m_appSettingsRepository ? m_appSettingsRepository->localProxyRestartToken() : 0;
const bool xrayRunning = m_service->isXrayRunning();
if (!xrayRunning) {
qInfo() << "Local proxy: starting Xray on port" << newProxyPort;
const bool started = startXrayProcess();
if (started) {
m_currentProxyPort = newProxyPort;
m_lastRestartToken = restartToken;
}
return started;
}
if (m_lastRestartToken != restartToken) {
qInfo() << "Local proxy: restarting Xray due to config change token";
const bool restarted = m_service->restartXray();
if (restarted) {
m_currentProxyPort = newProxyPort;
m_lastRestartToken = restartToken;
}
return restarted;
}
if (m_currentProxyPort != newProxyPort) {
qInfo() << "Local proxy: proxy port changed from" << m_currentProxyPort << "to" << newProxyPort;
const bool restarted = m_service->restartXray();
if (restarted) {
m_currentProxyPort = newProxyPort;
m_lastRestartToken = restartToken;
}
return restarted;
}
return true;
}
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include <QObject>
#include <QScopedPointer>
#include <QSharedPointer>
#include "httpapi.h"
#include "proxyservice.h"
class SecureServersRepository;
class SecureAppSettingsRepository;
class ProxyServer : public QObject
{
Q_OBJECT
public:
ProxyServer(SecureServersRepository *serversRepository, SecureAppSettingsRepository *appSettingsRepository,
QObject *parent = nullptr);
~ProxyServer();
bool start(quint16 port = 49490);
void stop();
bool syncSettings();
private:
bool startXrayProcess();
void stopXrayProcess();
SecureAppSettingsRepository *m_appSettingsRepository;
QScopedPointer<HttpApi> m_api;
QSharedPointer<ProxyService> m_service;
bool m_isRunning {false};
quint16 m_currentApiPort {0};
quint16 m_currentProxyPort {0};
int m_lastRestartToken {0};
};
+118
View File
@@ -0,0 +1,118 @@
#include "proxyservice.h"
#include "proxylogger.h"
namespace {
void logConfigError(const QString &errorMessage)
{
if (!errorMessage.isEmpty()) {
ProxyLogger::getInstance().error(errorMessage);
}
}
} // namespace
ProxyService::ProxyService(SecureServersRepository *serversRepository, SecureAppSettingsRepository *appSettingsRepository,
QObject *parent)
: QObject(parent)
, m_configManager(new ConfigManager(serversRepository, appSettingsRepository))
, m_xrayController(new XrayController())
{
ProxyLogger::getInstance().debug("ProxyService initialized");
}
QJsonObject ProxyService::getConfig()
{
if (!m_cachedConfig.isEmpty()) {
return m_cachedConfig;
}
QString error;
const auto configData = m_configManager->buildConfigWithFetch(error);
if (!configData) {
logConfigError(error);
return {};
}
m_cachedConfig = configData->parsedConfig;
return m_cachedConfig;
}
bool ProxyService::startXray()
{
ProxyLogger::getInstance().info("Starting Xray");
if (m_xrayController->isXrayRunning()) {
ProxyLogger::getInstance().info("Xray is already running");
return true;
}
QString error;
const auto configData = m_configManager->buildConfigWithFetch(error);
if (!configData) {
logConfigError(error);
return false;
}
const bool success = m_xrayController->start(configData->serializedConfig);
if (success) {
m_cachedConfig = configData->parsedConfig;
ProxyLogger::getInstance().info("Xray started successfully");
emit xrayStatusChanged(true);
return true;
}
ProxyLogger::getInstance().error(QStringLiteral("Failed to start Xray: %1").arg(m_xrayController->getError()));
return false;
}
bool ProxyService::stopXray()
{
ProxyLogger::getInstance().info("Stopping Xray");
const bool stopped = m_xrayController->stop();
if (stopped) {
ProxyLogger::getInstance().info("Xray stopped");
emit xrayStatusChanged(false);
return true;
}
ProxyLogger::getInstance().warning(QStringLiteral("Failed to stop Xray: %1").arg(m_xrayController->getError()));
return false;
}
bool ProxyService::isXrayRunning() const
{
return m_xrayController->isXrayRunning();
}
qint64 ProxyService::getXrayProcessId() const
{
return m_xrayController->getProcessId();
}
QString ProxyService::getXrayError() const
{
return m_xrayController->getError();
}
void ProxyService::clearCache()
{
m_cachedConfig = QJsonObject();
ProxyLogger::getInstance().debug("ProxyService cache cleared");
}
bool ProxyService::restartXray()
{
ProxyLogger::getInstance().info("Restarting Xray with updated config");
clearCache();
if (m_xrayController->isXrayRunning()) {
if (!stopXray()) {
ProxyLogger::getInstance().error("Failed to stop Xray during restart, aborting");
return false;
}
}
return startXray();
}
+39
View File
@@ -0,0 +1,39 @@
#pragma once
#include "configmanager.h"
#include "iproxyservice.h"
#include "xraycontroller.h"
#include <QObject>
#include <QScopedPointer>
#include <QJsonObject>
class SecureServersRepository;
class SecureAppSettingsRepository;
class ProxyService : public QObject, public IProxyService {
Q_OBJECT
public:
ProxyService(SecureServersRepository *serversRepository, SecureAppSettingsRepository *appSettingsRepository,
QObject *parent = nullptr);
~ProxyService() = default;
QJsonObject getConfig() override;
bool startXray() override;
bool stopXray() override;
bool isXrayRunning() const override;
qint64 getXrayProcessId() const override;
QString getXrayError() const override;
void clearCache();
bool restartXray();
signals:
void xrayStatusChanged(bool running);
private:
QScopedPointer<ConfigManager> m_configManager;
QScopedPointer<XrayController> m_xrayController;
QJsonObject m_cachedConfig;
};
+105
View File
@@ -0,0 +1,105 @@
#include "xraycontroller.h"
#include "proxylogger.h"
#include "core/utils/ipcClient.h"
namespace {
const QString kIpcUnavailableError = QStringLiteral("Failed to communicate with IPC service");
}
XrayController::XrayController(QObject *parent)
: QObject(parent)
{
ProxyLogger::getInstance().debug("XrayController initialized");
}
XrayController::~XrayController()
{
stop();
}
bool XrayController::start(const QString &configJson)
{
if (m_isRunning) {
ProxyLogger::getInstance().info("Xray is already running");
return true;
}
ProxyLogger::getInstance().info("Request to start Xray via IPC");
m_lastError.clear();
if (configJson.trimmed().isEmpty()) {
m_lastError = QStringLiteral("Config content is empty");
ProxyLogger::getInstance().error(m_lastError);
return false;
}
const bool ipcResult = IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
auto xrayStart = iface->xrayStart(configJson);
if (!xrayStart.waitForFinished() || !xrayStart.returnValue()) {
ProxyLogger::getInstance().warning("Failed to start Xray via IPC");
return false;
}
return true;
}, []() {
return false;
});
if (!ipcResult) {
m_lastError = kIpcUnavailableError;
ProxyLogger::getInstance().error(m_lastError);
return false;
}
ProxyLogger::getInstance().info("Xray start command sent to IPC service");
m_isRunning = true;
return true;
}
bool XrayController::stop()
{
if (!m_isRunning) {
ProxyLogger::getInstance().debug("Skipping Xray stop via IPC: local proxy Xray is not running");
return true;
}
ProxyLogger::getInstance().info("Stopping Xray via IPC");
const bool ipcResult = IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
auto xrayStop = iface->xrayStop();
if (!xrayStop.waitForFinished() || !xrayStop.returnValue()) {
ProxyLogger::getInstance().warning("Failed to stop Xray via IPC");
return false;
}
return true;
}, []() {
return false;
});
if (!ipcResult) {
m_lastError = kIpcUnavailableError;
ProxyLogger::getInstance().warning(m_lastError);
return false;
}
m_isRunning = false;
return true;
}
bool XrayController::isXrayRunning() const
{
return m_isRunning;
}
qint64 XrayController::getProcessId() const
{
return -1;
}
QString XrayController::getError() const
{
return m_lastError;
}
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include <QObject>
#include <QString>
class XrayController : public QObject {
Q_OBJECT
public:
explicit XrayController(QObject* parent = nullptr);
~XrayController();
bool start(const QString& configJson);
bool stop();
bool isXrayRunning() const;
qint64 getProcessId() const;
QString getError() const;
private:
bool m_isRunning {false};
QString m_lastError;
};
+1 -1
View File
@@ -6,7 +6,7 @@
#include <QString>
#include <QDateTime>
#include "core/utils/api/apiEnums.h"
#include "core/utils/serverConfigUtils.h"
#include "core/utils/constants/apiKeys.h"
#include "core/utils/constants/apiConstants.h"
@@ -1,140 +0,0 @@
#include "apiV1ServerConfig.h"
#include <QJsonArray>
#include <QJsonDocument>
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
#include "core/utils/protocolEnum.h"
#include "core/protocols/protocolUtils.h"
#include "core/utils/constants/apiKeys.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/api/apiUtils.h"
namespace amnezia
{
using namespace ContainerEnumNS;
bool ApiV1ServerConfig::isPremium() const
{
constexpr QLatin1String premiumV1Endpoint(PREM_V1_ENDPOINT);
return apiEndpoint.contains(premiumV1Endpoint);
}
bool ApiV1ServerConfig::isFree() const
{
constexpr QLatin1String freeV2Endpoint(FREE_V2_ENDPOINT);
return apiEndpoint.contains(freeV2Endpoint);
}
QString ApiV1ServerConfig::vpnKey() const
{
QJsonObject json = toJson();
return apiUtils::getPremiumV1VpnKey(json);
}
bool ApiV1ServerConfig::hasContainers() const
{
return !containers.isEmpty();
}
ContainerConfig ApiV1ServerConfig::containerConfig(DockerContainer container) const
{
if (!containers.contains(container)) {
return ContainerConfig{};
}
return containers.value(container);
}
QJsonObject ApiV1ServerConfig::toJson() const
{
QJsonObject obj;
if (!name.isEmpty()) {
obj[configKey::name] = name;
}
if (!description.isEmpty()) {
obj[configKey::description] = description;
}
if (!protocol.isEmpty()) {
obj[apiDefs::key::protocol] = protocol;
}
if (!apiEndpoint.isEmpty()) {
obj[apiDefs::key::apiEndpoint] = apiEndpoint;
}
if (!apiKey.isEmpty()) {
obj[apiDefs::key::apiKey] = apiKey;
}
obj[configKey::configVersion] = configVersion;
if (!hostName.isEmpty()) {
obj[configKey::hostName] = hostName;
}
QJsonArray containersArray;
for (auto it = containers.begin(); it != containers.end(); ++it) {
QJsonObject containerObj = it.value().toJson();
containersArray.append(containerObj);
}
if (!containersArray.isEmpty()) {
obj[configKey::containers] = containersArray;
}
if (defaultContainer != DockerContainer::None) {
obj[configKey::defaultContainer] = ContainerUtils::containerToString(defaultContainer);
}
if (!dns1.isEmpty()) {
obj[configKey::dns1] = dns1;
}
if (!dns2.isEmpty()) {
obj[configKey::dns2] = dns2;
}
if (crc > 0) {
obj[configKey::crc] = crc;
}
return obj;
}
ApiV1ServerConfig ApiV1ServerConfig::fromJson(const QJsonObject& json)
{
ApiV1ServerConfig config;
config.name = json.value(configKey::name).toString();
config.description = json.value(configKey::description).toString();
config.protocol = json.value(apiDefs::key::protocol).toString();
config.apiEndpoint = json.value(apiDefs::key::apiEndpoint).toString();
config.apiKey = json.value(apiDefs::key::apiKey).toString();
config.configVersion = json.value(configKey::configVersion).toInt(1);
config.hostName = json.value(configKey::hostName).toString();
QJsonArray containersArray = json.value(configKey::containers).toArray();
for (const QJsonValue& val : containersArray) {
QJsonObject containerObj = val.toObject();
ContainerConfig containerConfig = ContainerConfig::fromJson(containerObj);
QString containerStr = containerObj.value(configKey::container).toString();
DockerContainer container = ContainerUtils::containerFromString(containerStr);
config.containers.insert(container, containerConfig);
}
QString defaultContainerStr = json.value(configKey::defaultContainer).toString();
config.defaultContainer = ContainerUtils::containerFromString(defaultContainerStr);
config.dns1 = json.value(configKey::dns1).toString();
config.dns2 = json.value(configKey::dns2).toString();
config.crc = json.value(configKey::crc).toInt(0);
return config;
}
} // namespace amnezia
@@ -1,47 +0,0 @@
#ifndef APIV1SERVERCONFIG_H
#define APIV1SERVERCONFIG_H
#include <QJsonObject>
#include <QMap>
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
#include "core/models/containerConfig.h"
#include "core/utils/api/apiEnums.h"
#include "core/utils/constants/apiKeys.h"
#include "core/utils/constants/apiConstants.h"
namespace amnezia
{
using namespace ContainerEnumNS;
struct ApiV1ServerConfig {
QString description;
QString hostName;
QMap<DockerContainer, ContainerConfig> containers;
DockerContainer defaultContainer;
QString dns1;
QString dns2;
QString name;
QString protocol;
QString apiEndpoint;
QString apiKey;
int crc;
int configVersion;
bool isPremium() const;
bool isFree() const;
QString vpnKey() const;
bool hasContainers() const;
ContainerConfig containerConfig(DockerContainer container) const;
QJsonObject toJson() const;
static ApiV1ServerConfig fromJson(const QJsonObject& json);
};
} // namespace amnezia
#endif // APIV1SERVERCONFIG_H
@@ -13,6 +13,7 @@
#include "core/utils/api/apiUtils.h"
#include "core/models/api/apiConfig.h"
#include "core/models/api/authData.h"
#include "core/utils/networkUtilities.h"
namespace amnezia
{
@@ -67,6 +68,20 @@ ContainerConfig ApiV2ServerConfig::containerConfig(DockerContainer container) co
return containers.value(container);
}
QPair<QString, QString> ApiV2ServerConfig::getDnsPair(const QString &primaryDns, const QString &secondaryDns) const
{
QString d1 = dns1;
QString d2 = dns2;
if (d1.isEmpty() || !NetworkUtilities::checkIPv4Format(d1)) {
d1 = primaryDns;
}
if (d2.isEmpty() || !NetworkUtilities::checkIPv4Format(d2)) {
d2 = secondaryDns;
}
return { d1, d2 };
}
QJsonObject ApiV2ServerConfig::toJson() const
{
QJsonObject obj;
@@ -163,6 +178,10 @@ ApiV2ServerConfig ApiV2ServerConfig::fromJson(const QJsonObject& json)
config.authData = AuthData::fromJson(authDataObj);
}
if (config.displayName.isEmpty()) {
config.displayName = config.name.isEmpty() ? config.description : config.name;
}
return config;
}
+6 -1
View File
@@ -3,6 +3,7 @@
#include <QJsonObject>
#include <QMap>
#include <QPair>
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
@@ -10,7 +11,7 @@
#include "core/models/containerConfig.h"
#include "core/models/api/apiConfig.h"
#include "core/models/api/authData.h"
#include "core/utils/api/apiEnums.h"
#include "core/utils/serverConfigUtils.h"
#include "core/utils/constants/apiKeys.h"
#include "core/utils/constants/apiConstants.h"
@@ -21,6 +22,7 @@ using namespace ContainerEnumNS;
struct ApiV2ServerConfig {
QString description;
QString displayName;
QString hostName;
QMap<DockerContainer, ContainerConfig> containers;
DockerContainer defaultContainer;
@@ -42,6 +44,9 @@ struct ApiV2ServerConfig {
bool isExternalPremium() const;
bool hasContainers() const;
ContainerConfig containerConfig(DockerContainer container) const;
QPair<QString, QString> getDnsPair(const QString &primaryDns, const QString &secondaryDns) const;
QJsonObject toJson() const;
static ApiV2ServerConfig fromJson(const QJsonObject& json);
};
+1 -1
View File
@@ -4,7 +4,7 @@
#include <QJsonObject>
#include <QString>
#include "core/utils/api/apiEnums.h"
#include "core/utils/serverConfigUtils.h"
#include "core/utils/constants/apiKeys.h"
#include "core/utils/constants/apiConstants.h"
@@ -0,0 +1,41 @@
#include "legacyApiServerConfig.h"
#include "core/utils/constants/apiKeys.h"
#include "core/utils/constants/configKeys.h"
namespace amnezia
{
bool LegacyApiServerConfig::hasContainers() const
{
return !containers.isEmpty();
}
ContainerConfig LegacyApiServerConfig::containerConfig(DockerContainer container) const
{
if (!containers.contains(container)) {
return ContainerConfig{};
}
return containers.value(container);
}
LegacyApiServerConfig LegacyApiServerConfig::fromJson(const QJsonObject &json)
{
LegacyApiServerConfig config;
config.description = json.value(configKey::description).toString();
config.hostName = json.value(configKey::hostName).toString();
config.crc = json.value(configKey::crc).toInt(0);
config.configVersion = json.value(configKey::configVersion).toInt(1);
config.apiEndpoint = json.value(apiDefs::key::apiEndpoint).toString();
if (config.displayName.isEmpty()) {
config.displayName = config.name.isEmpty() ? config.description : config.name;
}
return config;
}
} // namespace amnezia
@@ -0,0 +1,38 @@
#ifndef LEGACYAPISERVERCONFIG_H
#define LEGACYAPISERVERCONFIG_H
#include <QJsonObject>
#include <QMap>
#include "core/utils/containerEnum.h"
#include "core/utils/protocolEnum.h"
#include "core/models/containerConfig.h"
namespace amnezia
{
using namespace ContainerEnumNS;
struct LegacyApiServerConfig {
QString description;
QString displayName;
QString hostName;
QMap<DockerContainer, ContainerConfig> containers;
DockerContainer defaultContainer = DockerContainer::None;
QString dns1;
QString dns2;
QString name;
int crc = 0;
int configVersion = 0;
QString apiEndpoint;
bool hasContainers() const;
ContainerConfig containerConfig(DockerContainer container) const;
static LegacyApiServerConfig fromJson(const QJsonObject &json);
};
} // namespace amnezia
#endif // LEGACYAPISERVERCONFIG_H
+20
View File
@@ -113,6 +113,26 @@ const Socks5ProxyProtocolConfig* ContainerConfig::getSocks5ProxyProtocolConfig()
return protocolConfig.as<Socks5ProxyProtocolConfig>();
}
MtProxyProtocolConfig* ContainerConfig::getMtProxyProtocolConfig()
{
return protocolConfig.as<MtProxyProtocolConfig>();
}
const MtProxyProtocolConfig* ContainerConfig::getMtProxyProtocolConfig() const
{
return protocolConfig.as<MtProxyProtocolConfig>();
}
TelemtProtocolConfig* ContainerConfig::getTelemtProtocolConfig()
{
return protocolConfig.as<TelemtProtocolConfig>();
}
const TelemtProtocolConfig* ContainerConfig::getTelemtProtocolConfig() const
{
return protocolConfig.as<TelemtProtocolConfig>();
}
Ikev2ProtocolConfig* ContainerConfig::getIkev2ProtocolConfig()
{
return protocolConfig.as<Ikev2ProtocolConfig>();
+6
View File
@@ -57,6 +57,12 @@ struct ContainerConfig {
Socks5ProxyProtocolConfig* getSocks5ProxyProtocolConfig();
const Socks5ProxyProtocolConfig* getSocks5ProxyProtocolConfig() const;
MtProxyProtocolConfig* getMtProxyProtocolConfig();
const MtProxyProtocolConfig* getMtProxyProtocolConfig() const;
TelemtProtocolConfig* getTelemtProtocolConfig();
const TelemtProtocolConfig* getTelemtProtocolConfig() const;
Ikev2ProtocolConfig* getIkev2ProtocolConfig();
const Ikev2ProtocolConfig* getIkev2ProtocolConfig() const;
+18
View File
@@ -9,6 +9,8 @@
#include "core/utils/protocolEnum.h"
#include "core/models/protocols/ikev2ProtocolConfig.h"
#include "core/models/protocols/dnsProtocolConfig.h"
#include "core/models/protocols/mtProxyProtocolConfig.h"
#include "core/models/protocols/telemtProtocolConfig.h"
namespace amnezia
{
@@ -38,6 +40,10 @@ Proto ProtocolConfig::type() const
return Proto::TorWebSite;
} else if constexpr (std::is_same_v<T, DnsProtocolConfig>) {
return Proto::Dns;
} else if constexpr (std::is_same_v<T, MtProxyProtocolConfig>) {
return Proto::MtProxy;
} else if constexpr (std::is_same_v<T, TelemtProtocolConfig>) {
return Proto::Telemt;
}
return Proto::Unknown;
}, data);
@@ -65,6 +71,10 @@ QString ProtocolConfig::port() const
return QString();
} else if constexpr (std::is_same_v<T, DnsProtocolConfig>) {
return QString();
} else if constexpr (std::is_same_v<T, MtProxyProtocolConfig>) {
return arg.port.isEmpty() ? QString(protocols::mtProxy::defaultPort) : arg.port;
} else if constexpr (std::is_same_v<T, TelemtProtocolConfig>) {
return arg.port.isEmpty() ? QString(protocols::telemt::defaultPort) : arg.port;
}
return QString();
}, data);
@@ -88,6 +98,10 @@ QString ProtocolConfig::transportProto() const
return QString();
} else if constexpr (std::is_same_v<T, DnsProtocolConfig>) {
return QString();
} else if constexpr (std::is_same_v<T, MtProxyProtocolConfig>) {
return QStringLiteral("tcp");
} else if constexpr (std::is_same_v<T, TelemtProtocolConfig>) {
return QStringLiteral("tcp");
}
return QString();
}, data);
@@ -299,6 +313,10 @@ ProtocolConfig ProtocolConfig::fromJson(const QJsonObject& json, Proto type)
return ProtocolConfig{TorProtocolConfig::fromJson(json)};
case Proto::Dns:
return ProtocolConfig{DnsProtocolConfig::fromJson(json)};
case Proto::MtProxy:
return ProtocolConfig{MtProxyProtocolConfig::fromJson(json)};
case Proto::Telemt:
return ProtocolConfig{TelemtProtocolConfig::fromJson(json)};
default:
return ProtocolConfig{AwgProtocolConfig{}};
}
+4
View File
@@ -22,6 +22,8 @@
#include "core/models/protocols/ikev2ProtocolConfig.h"
#include "core/models/protocols/torProtocolConfig.h"
#include "core/models/protocols/dnsProtocolConfig.h"
#include "core/models/protocols/mtProxyProtocolConfig.h"
#include "core/models/protocols/telemtProtocolConfig.h"
namespace amnezia
{
@@ -36,6 +38,8 @@ struct ProtocolConfig {
XrayProtocolConfig,
SftpProtocolConfig,
Socks5ProxyProtocolConfig,
MtProxyProtocolConfig,
TelemtProtocolConfig,
Ikev2ProtocolConfig,
TorProtocolConfig,
DnsProtocolConfig
@@ -0,0 +1,147 @@
#include "mtProxyProtocolConfig.h"
#include "../../../core/utils/protocolEnum.h"
#include "../../../core/protocols/protocolUtils.h"
#include "../../../core/utils/constants/configKeys.h"
#include "../../../core/utils/constants/protocolConstants.h"
#include <QJsonArray>
#include <algorithm>
using namespace amnezia;
namespace amnezia {
QJsonObject MtProxyProtocolConfig::toJson() const {
QJsonObject obj;
if (!port.isEmpty()) {
obj[configKey::port] = port;
}
if (!secret.isEmpty()) {
obj[protocols::mtProxy::secretKey] = secret;
}
if (!tag.isEmpty()) {
obj[protocols::mtProxy::tagKey] = tag;
}
if (!tgLink.isEmpty()) {
obj[protocols::mtProxy::tgLinkKey] = tgLink;
}
if (!tmeLink.isEmpty()) {
obj[protocols::mtProxy::tmeLinkKey] = tmeLink;
}
obj[protocols::mtProxy::isEnabledKey] = isEnabled;
if (!publicHost.isEmpty()) {
obj[protocols::mtProxy::publicHostKey] = publicHost;
}
if (!transportMode.isEmpty()) {
obj[protocols::mtProxy::transportModeKey] = transportMode;
}
if (!tlsDomain.isEmpty()) {
obj[protocols::mtProxy::tlsDomainKey] = tlsDomain;
}
if (!additionalSecrets.isEmpty()) {
obj[protocols::mtProxy::additionalSecretsKey] = QJsonArray::fromStringList(additionalSecrets);
}
if (!workersMode.isEmpty()) {
obj[protocols::mtProxy::workersModeKey] = workersMode;
}
if (!workers.isEmpty()) {
obj[protocols::mtProxy::workersKey] = workers;
}
obj[protocols::mtProxy::natEnabledKey] = natEnabled;
if (!natInternalIp.isEmpty()) {
obj[protocols::mtProxy::natInternalIpKey] = natInternalIp;
}
if (!natExternalIp.isEmpty()) {
obj[protocols::mtProxy::natExternalIpKey] = natExternalIp;
}
return obj;
}
MtProxyProtocolConfig MtProxyProtocolConfig::fromJson(const QJsonObject &json) {
MtProxyProtocolConfig config;
config.port = json.value(configKey::port).toString();
config.secret = json.value(protocols::mtProxy::secretKey).toString();
config.tag = json.value(protocols::mtProxy::tagKey).toString();
config.tgLink = json.value(protocols::mtProxy::tgLinkKey).toString();
config.tmeLink = json.value(protocols::mtProxy::tmeLinkKey).toString();
config.isEnabled = json.value(protocols::mtProxy::isEnabledKey).toBool(true);
config.publicHost = json.value(protocols::mtProxy::publicHostKey).toString();
config.transportMode = json.value(protocols::mtProxy::transportModeKey).toString();
config.tlsDomain = json.value(protocols::mtProxy::tlsDomainKey).toString();
for (const auto &v: json.value(protocols::mtProxy::additionalSecretsKey).toArray()) {
const QString s = v.toString();
if (!s.isEmpty()) {
config.additionalSecrets.append(s);
}
}
config.workersMode = json.value(protocols::mtProxy::workersModeKey).toString();
config.workers = json.value(protocols::mtProxy::workersKey).toString();
config.natEnabled = json.value(protocols::mtProxy::natEnabledKey).toBool(false);
config.natInternalIp = json.value(protocols::mtProxy::natInternalIpKey).toString();
config.natExternalIp = json.value(protocols::mtProxy::natExternalIpKey).toString();
return config;
}
bool MtProxyProtocolConfig::equalsDockerDeploymentSettings(const MtProxyProtocolConfig &other) const {
const auto normPort = [](const QString &p) {
return p.isEmpty() ? QString(protocols::mtProxy::defaultPort) : p;
};
const auto normTransport = [](const QString &t) {
return t.isEmpty() ? QString(protocols::mtProxy::transportModeStandard) : t;
};
const auto normWorkersMode = [](const QString &m) {
return m.isEmpty() ? QString(protocols::mtProxy::workersModeAuto) : m;
};
if (normPort(port) != normPort(other.port)) {
return false;
}
if (normTransport(transportMode) != normTransport(other.transportMode)) {
return false;
}
if (tlsDomain != other.tlsDomain) {
return false;
}
if (secret != other.secret) {
return false;
}
if (tag != other.tag) {
return false;
}
if (publicHost != other.publicHost) {
return false;
}
if (normWorkersMode(workersMode) != normWorkersMode(other.workersMode)) {
return false;
}
if (workers != other.workers) {
return false;
}
if (natEnabled != other.natEnabled) {
return false;
}
if (natInternalIp != other.natInternalIp) {
return false;
}
if (natExternalIp != other.natExternalIp) {
return false;
}
if (isEnabled != other.isEnabled) {
return false;
}
QStringList aa = additionalSecrets;
QStringList bb = other.additionalSecrets;
aa.removeAll(QString());
bb.removeAll(QString());
std::sort(aa.begin(), aa.end());
std::sort(bb.begin(), bb.end());
return aa == bb;
}
} // namespace amnezia
@@ -0,0 +1,38 @@
#ifndef MTPROXYPROTOCOLCONFIG_H
#define MTPROXYPROTOCOLCONFIG_H
#include <QJsonObject>
#include <QString>
#include <QStringList>
namespace amnezia {
struct MtProxyProtocolConfig {
QString port;
QString secret;
QString tag;
QString tgLink;
QString tmeLink;
bool isEnabled = true;
QString publicHost;
QString transportMode;
QString tlsDomain;
QStringList additionalSecrets;
QString workersMode;
QString workers;
bool natEnabled = false;
QString natInternalIp;
QString natExternalIp;
QJsonObject toJson() const;
static MtProxyProtocolConfig fromJson(const QJsonObject &json);
// Port, transport, TLS, secrets, NAT, workers, isEnabled, additionalSecrets (order-independent).
// Ignores tgLink / tmeLink (derived / display).
bool equalsDockerDeploymentSettings(const MtProxyProtocolConfig &other) const;
};
} // namespace amnezia
#endif // MTPROXYPROTOCOLCONFIG_H
@@ -0,0 +1,162 @@
#include "telemtProtocolConfig.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
#include <QJsonArray>
#include <algorithm>
using namespace amnezia;
QJsonObject TelemtProtocolConfig::toJson() const
{
QJsonObject obj;
if (!port.isEmpty()) {
obj[QString(configKey::port)] = port;
}
if (!secret.isEmpty()) {
obj[protocols::telemt::secretKey] = secret;
}
if (!tag.isEmpty()) {
obj[protocols::telemt::tagKey] = tag;
}
if (!tgLink.isEmpty()) {
obj[protocols::telemt::tgLinkKey] = tgLink;
}
if (!tmeLink.isEmpty()) {
obj[protocols::telemt::tmeLinkKey] = tmeLink;
}
obj[protocols::telemt::isEnabledKey] = isEnabled;
if (!publicHost.isEmpty()) {
obj[protocols::telemt::publicHostKey] = publicHost;
}
if (!transportMode.isEmpty()) {
obj[protocols::telemt::transportModeKey] = transportMode;
}
if (!tlsDomain.isEmpty()) {
obj[protocols::telemt::tlsDomainKey] = tlsDomain;
}
obj[protocols::telemt::maskEnabledKey] = maskEnabled;
obj[protocols::telemt::tlsEmulationKey] = tlsEmulation;
obj[protocols::telemt::useMiddleProxyKey] = useMiddleProxy;
if (!userName.isEmpty()) {
obj[protocols::telemt::userNameKey] = userName;
}
if (!additionalSecrets.isEmpty()) {
obj[protocols::telemt::additionalSecretsKey] = QJsonArray::fromStringList(additionalSecrets);
}
if (!workersMode.isEmpty()) {
obj[protocols::telemt::workersModeKey] = workersMode;
}
if (!workers.isEmpty()) {
obj[protocols::telemt::workersKey] = workers;
}
obj[protocols::telemt::natEnabledKey] = natEnabled;
if (!natInternalIp.isEmpty()) {
obj[protocols::telemt::natInternalIpKey] = natInternalIp;
}
if (!natExternalIp.isEmpty()) {
obj[protocols::telemt::natExternalIpKey] = natExternalIp;
}
return obj;
}
TelemtProtocolConfig TelemtProtocolConfig::fromJson(const QJsonObject &json)
{
TelemtProtocolConfig c;
c.port = json.value(QString(configKey::port)).toString();
c.secret = json.value(protocols::telemt::secretKey).toString();
c.tag = json.value(protocols::telemt::tagKey).toString();
c.tgLink = json.value(protocols::telemt::tgLinkKey).toString();
c.tmeLink = json.value(protocols::telemt::tmeLinkKey).toString();
c.isEnabled = json.value(protocols::telemt::isEnabledKey).toBool(true);
c.publicHost = json.value(protocols::telemt::publicHostKey).toString();
c.transportMode = json.value(protocols::telemt::transportModeKey).toString();
c.tlsDomain = json.value(protocols::telemt::tlsDomainKey).toString();
c.maskEnabled = json.value(protocols::telemt::maskEnabledKey).toBool(true);
c.tlsEmulation = json.value(protocols::telemt::tlsEmulationKey).toBool(false);
c.useMiddleProxy = json.value(protocols::telemt::useMiddleProxyKey).toBool(true);
c.userName = json.value(protocols::telemt::userNameKey).toString();
for (const auto &v : json.value(protocols::telemt::additionalSecretsKey).toArray()) {
const QString s = v.toString();
if (!s.isEmpty()) {
c.additionalSecrets.append(s);
}
}
c.workersMode = json.value(protocols::telemt::workersModeKey).toString();
c.workers = json.value(protocols::telemt::workersKey).toString();
c.natEnabled = json.value(protocols::telemt::natEnabledKey).toBool(false);
c.natInternalIp = json.value(protocols::telemt::natInternalIpKey).toString();
c.natExternalIp = json.value(protocols::telemt::natExternalIpKey).toString();
return c;
}
bool TelemtProtocolConfig::equalsDockerDeploymentSettings(const TelemtProtocolConfig &other) const
{
const auto normPort = [](const QString &p) {
return p.isEmpty() ? QString(protocols::telemt::defaultPort) : p;
};
const auto normTransport = [](const QString &t) {
return t.isEmpty() ? QString(protocols::telemt::transportModeStandard) : t;
};
const auto normWorkersMode = [](const QString &m) {
return m.isEmpty() ? QString(protocols::telemt::workersModeAuto) : m;
};
if (normPort(port) != normPort(other.port)) {
return false;
}
if (normTransport(transportMode) != normTransport(other.transportMode)) {
return false;
}
if (tlsDomain != other.tlsDomain) {
return false;
}
if (secret != other.secret) {
return false;
}
if (tag != other.tag) {
return false;
}
if (publicHost != other.publicHost) {
return false;
}
if (maskEnabled != other.maskEnabled) {
return false;
}
if (tlsEmulation != other.tlsEmulation) {
return false;
}
if (useMiddleProxy != other.useMiddleProxy) {
return false;
}
if (userName != other.userName) {
return false;
}
if (normWorkersMode(workersMode) != normWorkersMode(other.workersMode)) {
return false;
}
if (workers != other.workers) {
return false;
}
if (natEnabled != other.natEnabled) {
return false;
}
if (natInternalIp != other.natInternalIp) {
return false;
}
if (natExternalIp != other.natExternalIp) {
return false;
}
if (isEnabled != other.isEnabled) {
return false;
}
QStringList aa = additionalSecrets;
QStringList bb = other.additionalSecrets;
aa.removeAll(QString());
bb.removeAll(QString());
std::sort(aa.begin(), aa.end());
std::sort(bb.begin(), bb.end());
return aa == bb;
}
@@ -0,0 +1,38 @@
#ifndef TELEMTPROTOCOLCONFIG_H
#define TELEMTPROTOCOLCONFIG_H
#include <QJsonObject>
#include <QString>
#include <QStringList>
namespace amnezia {
struct TelemtProtocolConfig {
QString port;
QString secret;
QString tag;
QString tgLink;
QString tmeLink;
bool isEnabled = true;
QString publicHost;
QString transportMode;
QString tlsDomain;
bool maskEnabled = true;
bool tlsEmulation = false;
bool useMiddleProxy = true;
QString userName;
QStringList additionalSecrets;
QString workersMode;
QString workers;
bool natEnabled = false;
QString natInternalIp;
QString natExternalIp;
QJsonObject toJson() const;
static TelemtProtocolConfig fromJson(const QJsonObject &json);
bool equalsDockerDeploymentSettings(const TelemtProtocolConfig &other) const;
};
} // namespace amnezia
#endif // TELEMTPROTOCOLCONFIG_H
@@ -3,20 +3,267 @@
#include <QJsonDocument>
#include <QJsonArray>
#include "../../../core/utils/protocolEnum.h"
#include "../../../core/protocols/protocolUtils.h"
#include "../../../core/utils/constants/configKeys.h"
#include "../../../core/utils/constants/protocolConstants.h"
#include "core/utils/protocolEnum.h"
#include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
using namespace amnezia;
using namespace ProtocolUtils;
namespace amnezia
{
QJsonObject XrayXPaddingConfig::toJson() const
{
QJsonObject obj;
if (!bytesMin.isEmpty()) obj[configKey::xPaddingBytesMin] = bytesMin;
if (!bytesMax.isEmpty()) obj[configKey::xPaddingBytesMax] = bytesMax;
obj[configKey::xPaddingObfsMode] = obfsMode;
if (!key.isEmpty()) obj[configKey::xPaddingKey] = key;
if (!header.isEmpty()) obj[configKey::xPaddingHeader] = header;
if (!placement.isEmpty()) obj[configKey::xPaddingPlacement] = placement;
if (!method.isEmpty()) obj[configKey::xPaddingMethod] = method;
return obj;
}
XrayXPaddingConfig XrayXPaddingConfig::fromJson(const QJsonObject &json)
{
XrayXPaddingConfig c;
c.bytesMin = json.value(configKey::xPaddingBytesMin).toString();
c.bytesMax = json.value(configKey::xPaddingBytesMax).toString();
c.obfsMode = json.value(configKey::xPaddingObfsMode).toBool(true);
c.key = json.value(configKey::xPaddingKey).toString();
c.header = json.value(configKey::xPaddingHeader).toString();
c.placement = json.value(configKey::xPaddingPlacement).toString(protocols::xray::defaultXPaddingPlacement);
c.method = json.value(configKey::xPaddingMethod).toString(protocols::xray::defaultXPaddingMethod);
return c;
}
QJsonObject XrayXmuxConfig::toJson() const
{
QJsonObject obj;
obj[configKey::xmuxEnabled] = enabled;
if (!maxConcurrencyMin.isEmpty()) obj[configKey::xmuxMaxConcurrencyMin] = maxConcurrencyMin;
if (!maxConcurrencyMax.isEmpty()) obj[configKey::xmuxMaxConcurrencyMax] = maxConcurrencyMax;
if (!maxConnectionsMin.isEmpty()) obj[configKey::xmuxMaxConnectionsMin] = maxConnectionsMin;
if (!maxConnectionsMax.isEmpty()) obj[configKey::xmuxMaxConnectionsMax] = maxConnectionsMax;
if (!cMaxReuseTimesMin.isEmpty()) obj[configKey::xmuxCMaxReuseTimesMin] = cMaxReuseTimesMin;
if (!cMaxReuseTimesMax.isEmpty()) obj[configKey::xmuxCMaxReuseTimesMax] = cMaxReuseTimesMax;
if (!hMaxRequestTimesMin.isEmpty()) obj[configKey::xmuxHMaxRequestTimesMin] = hMaxRequestTimesMin;
if (!hMaxRequestTimesMax.isEmpty()) obj[configKey::xmuxHMaxRequestTimesMax] = hMaxRequestTimesMax;
if (!hMaxReusableSecsMin.isEmpty()) obj[configKey::xmuxHMaxReusableSecsMin] = hMaxReusableSecsMin;
if (!hMaxReusableSecsMax.isEmpty()) obj[configKey::xmuxHMaxReusableSecsMax] = hMaxReusableSecsMax;
if (!hKeepAlivePeriod.isEmpty()) obj[configKey::xmuxHKeepAlivePeriod] = hKeepAlivePeriod;
return obj;
}
XrayXmuxConfig XrayXmuxConfig::fromJson(const QJsonObject &json)
{
XrayXmuxConfig c;
c.enabled = json.value(configKey::xmuxEnabled).toBool(true);
c.maxConcurrencyMin = json.value(configKey::xmuxMaxConcurrencyMin).toString("0");
c.maxConcurrencyMax = json.value(configKey::xmuxMaxConcurrencyMax).toString("0");
c.maxConnectionsMin = json.value(configKey::xmuxMaxConnectionsMin).toString("0");
c.maxConnectionsMax = json.value(configKey::xmuxMaxConnectionsMax).toString("0");
c.cMaxReuseTimesMin = json.value(configKey::xmuxCMaxReuseTimesMin).toString("0");
c.cMaxReuseTimesMax = json.value(configKey::xmuxCMaxReuseTimesMax).toString("0");
c.hMaxRequestTimesMin = json.value(configKey::xmuxHMaxRequestTimesMin).toString("0");
c.hMaxRequestTimesMax = json.value(configKey::xmuxHMaxRequestTimesMax).toString("0");
c.hMaxReusableSecsMin = json.value(configKey::xmuxHMaxReusableSecsMin).toString("0");
c.hMaxReusableSecsMax = json.value(configKey::xmuxHMaxReusableSecsMax).toString("0");
c.hKeepAlivePeriod = json.value(configKey::xmuxHKeepAlivePeriod).toString();
return c;
}
QJsonObject XrayXhttpConfig::toJson() const
{
QJsonObject obj;
if (!mode.isEmpty()) obj[configKey::xhttpMode] = mode;
if (!host.isEmpty()) obj[configKey::xhttpHost] = host;
if (!path.isEmpty()) obj[configKey::xhttpPath] = path;
if (!headersTemplate.isEmpty()) obj[configKey::xhttpHeadersTemplate] = headersTemplate;
if (!uplinkMethod.isEmpty()) obj[configKey::xhttpUplinkMethod] = uplinkMethod;
obj[configKey::xhttpDisableGrpc] = disableGrpc;
obj[configKey::xhttpDisableSse] = disableSse;
if (!sessionPlacement.isEmpty()) obj[configKey::xhttpSessionPlacement] = sessionPlacement;
if (!sessionKey.isEmpty()) obj[configKey::xhttpSessionKey] = sessionKey;
if (!seqPlacement.isEmpty()) obj[configKey::xhttpSeqPlacement] = seqPlacement;
if (!seqKey.isEmpty()) obj[configKey::xhttpSeqKey] = seqKey;
if (!uplinkDataPlacement.isEmpty()) obj[configKey::xhttpUplinkDataPlacement] = uplinkDataPlacement;
if (!uplinkDataKey.isEmpty()) obj[configKey::xhttpUplinkDataKey] = uplinkDataKey;
if (!uplinkChunkSize.isEmpty()) obj[configKey::xhttpUplinkChunkSize] = uplinkChunkSize;
if (!scMaxBufferedPosts.isEmpty()) obj[configKey::xhttpScMaxBufferedPosts] = scMaxBufferedPosts;
if (!scMaxEachPostBytesMin.isEmpty()) obj[configKey::xhttpScMaxEachPostBytesMin] = scMaxEachPostBytesMin;
if (!scMaxEachPostBytesMax.isEmpty()) obj[configKey::xhttpScMaxEachPostBytesMax] = scMaxEachPostBytesMax;
if (!scMinPostsIntervalMsMin.isEmpty()) obj[configKey::xhttpScMinPostsIntervalMsMin] = scMinPostsIntervalMsMin;
if (!scMinPostsIntervalMsMax.isEmpty()) obj[configKey::xhttpScMinPostsIntervalMsMax] = scMinPostsIntervalMsMax;
if (!scStreamUpServerSecsMin.isEmpty()) obj[configKey::xhttpScStreamUpServerSecsMin] = scStreamUpServerSecsMin;
if (!scStreamUpServerSecsMax.isEmpty()) obj[configKey::xhttpScStreamUpServerSecsMax] = scStreamUpServerSecsMax;
obj["xPadding"] = xPadding.toJson();
obj["xmux"] = xmux.toJson();
return obj;
}
namespace
{
XrayXhttpConfig clearedXhttpConfig()
{
XrayXhttpConfig c;
c.mode = QString();
c.host = QString();
c.path = QString();
c.headersTemplate = QString();
c.uplinkMethod = QString();
c.disableGrpc = false;
c.disableSse = false;
c.sessionPlacement = QString();
c.sessionKey = QString();
c.seqPlacement = QString();
c.seqKey = QString();
c.uplinkDataPlacement = QString();
c.uplinkDataKey = QString();
c.uplinkChunkSize = QString();
c.scMaxBufferedPosts = QString();
c.scMaxEachPostBytesMin = QString();
c.scMaxEachPostBytesMax = QString();
c.scMinPostsIntervalMsMin = QString();
c.scMinPostsIntervalMsMax = QString();
c.scStreamUpServerSecsMin = QString();
c.scStreamUpServerSecsMax = QString();
return c;
}
} // namespace
XrayXhttpConfig XrayXhttpConfig::fromJson(const QJsonObject &json)
{
if (json.isEmpty()) {
return clearedXhttpConfig();
}
XrayXhttpConfig c = clearedXhttpConfig();
if (json.contains(configKey::xhttpMode)) {
c.mode = json.value(configKey::xhttpMode).toString();
}
if (json.contains(configKey::xhttpHost)) {
c.host = json.value(configKey::xhttpHost).toString();
}
if (json.contains(configKey::xhttpPath)) {
c.path = json.value(configKey::xhttpPath).toString();
}
if (json.contains(configKey::xhttpHeadersTemplate)) {
c.headersTemplate = json.value(configKey::xhttpHeadersTemplate).toString();
}
if (json.contains(configKey::xhttpUplinkMethod)) {
c.uplinkMethod = json.value(configKey::xhttpUplinkMethod).toString();
}
if (json.contains(configKey::xhttpDisableGrpc)) {
c.disableGrpc = json.value(configKey::xhttpDisableGrpc).toBool();
}
if (json.contains(configKey::xhttpDisableSse)) {
c.disableSse = json.value(configKey::xhttpDisableSse).toBool();
}
if (json.contains(configKey::xhttpSessionPlacement)) {
c.sessionPlacement = json.value(configKey::xhttpSessionPlacement).toString();
}
if (json.contains(configKey::xhttpSessionKey)) {
c.sessionKey = json.value(configKey::xhttpSessionKey).toString();
}
if (json.contains(configKey::xhttpSeqPlacement)) {
c.seqPlacement = json.value(configKey::xhttpSeqPlacement).toString();
}
if (json.contains(configKey::xhttpSeqKey)) {
c.seqKey = json.value(configKey::xhttpSeqKey).toString();
}
if (json.contains(configKey::xhttpUplinkDataPlacement)) {
c.uplinkDataPlacement = json.value(configKey::xhttpUplinkDataPlacement).toString();
}
if (json.contains(configKey::xhttpUplinkDataKey)) {
c.uplinkDataKey = json.value(configKey::xhttpUplinkDataKey).toString();
}
if (json.contains(configKey::xhttpUplinkChunkSize)) {
c.uplinkChunkSize = json.value(configKey::xhttpUplinkChunkSize).toString();
}
if (json.contains(configKey::xhttpScMaxBufferedPosts)) {
c.scMaxBufferedPosts = json.value(configKey::xhttpScMaxBufferedPosts).toString();
}
if (json.contains(configKey::xhttpScMaxEachPostBytesMin)) {
c.scMaxEachPostBytesMin = json.value(configKey::xhttpScMaxEachPostBytesMin).toString();
}
if (json.contains(configKey::xhttpScMaxEachPostBytesMax)) {
c.scMaxEachPostBytesMax = json.value(configKey::xhttpScMaxEachPostBytesMax).toString();
}
if (json.contains(configKey::xhttpScMinPostsIntervalMsMin)) {
c.scMinPostsIntervalMsMin = json.value(configKey::xhttpScMinPostsIntervalMsMin).toString();
}
if (json.contains(configKey::xhttpScMinPostsIntervalMsMax)) {
c.scMinPostsIntervalMsMax = json.value(configKey::xhttpScMinPostsIntervalMsMax).toString();
}
if (json.contains(configKey::xhttpScStreamUpServerSecsMin)) {
c.scStreamUpServerSecsMin = json.value(configKey::xhttpScStreamUpServerSecsMin).toString();
}
if (json.contains(configKey::xhttpScStreamUpServerSecsMax)) {
c.scStreamUpServerSecsMax = json.value(configKey::xhttpScStreamUpServerSecsMax).toString();
}
if (json.contains(QLatin1String("xPadding"))) {
c.xPadding = XrayXPaddingConfig::fromJson(json.value(QLatin1String("xPadding")).toObject());
}
if (json.contains(QLatin1String("xmux"))) {
c.xmux = XrayXmuxConfig::fromJson(json.value(QLatin1String("xmux")).toObject());
}
return c;
}
QJsonObject XrayMkcpConfig::toJson() const
{
QJsonObject obj;
if (!tti.isEmpty()) obj[configKey::mkcpTti] = tti;
if (!uplinkCapacity.isEmpty()) obj[configKey::mkcpUplinkCapacity] = uplinkCapacity;
if (!downlinkCapacity.isEmpty()) obj[configKey::mkcpDownlinkCapacity] = downlinkCapacity;
if (!readBufferSize.isEmpty()) obj[configKey::mkcpReadBufferSize] = readBufferSize;
if (!writeBufferSize.isEmpty()) obj[configKey::mkcpWriteBufferSize] = writeBufferSize;
obj[configKey::mkcpCongestion] = congestion;
return obj;
}
XrayMkcpConfig XrayMkcpConfig::fromJson(const QJsonObject &json)
{
XrayMkcpConfig c;
if (json.isEmpty()) {
return c;
}
if (json.contains(configKey::mkcpTti)) {
c.tti = json.value(configKey::mkcpTti).toString();
}
if (json.contains(configKey::mkcpUplinkCapacity)) {
c.uplinkCapacity = json.value(configKey::mkcpUplinkCapacity).toString();
}
if (json.contains(configKey::mkcpDownlinkCapacity)) {
c.downlinkCapacity = json.value(configKey::mkcpDownlinkCapacity).toString();
}
if (json.contains(configKey::mkcpReadBufferSize)) {
c.readBufferSize = json.value(configKey::mkcpReadBufferSize).toString();
}
if (json.contains(configKey::mkcpWriteBufferSize)) {
c.writeBufferSize = json.value(configKey::mkcpWriteBufferSize).toString();
}
if (json.contains(configKey::mkcpCongestion)) {
c.congestion = json.value(configKey::mkcpCongestion).toBool();
}
return c;
}
QJsonObject XrayServerConfig::toJson() const
{
QJsonObject obj;
// Existing fields
if (!port.isEmpty()) {
obj[configKey::port] = port;
}
@@ -29,60 +276,126 @@ QJsonObject XrayServerConfig::toJson() const
if (!site.isEmpty()) {
obj[configKey::site] = site;
}
if (isThirdPartyConfig) {
obj[configKey::isThirdPartyConfig] = isThirdPartyConfig;
}
// New: Security
if (!security.isEmpty()) {
obj[configKey::xraySecurity] = security;
}
if (!flow.isEmpty()) {
obj[configKey::xrayFlow] = flow;
}
if (!fingerprint.isEmpty()) {
obj[configKey::xrayFingerprint] = fingerprint;
}
if (!sni.isEmpty()) {
obj[configKey::xraySni] = sni;
}
if (!alpn.isEmpty()) {
obj[configKey::xrayAlpn] = alpn;
}
// New: Transport
if (!transport.isEmpty()) {
obj[configKey::xrayTransport] = transport;
}
const QJsonObject xhttpObj = xhttp.toJson();
if (!xhttpObj.isEmpty()) {
obj[QStringLiteral("xhttp")] = xhttpObj;
}
const QJsonObject mkcpObj = mkcp.toJson();
if (!mkcpObj.isEmpty()) {
obj[QStringLiteral("mkcp")] = mkcpObj;
}
return obj;
}
XrayServerConfig XrayServerConfig::fromJson(const QJsonObject& json)
XrayServerConfig XrayServerConfig::fromJson(const QJsonObject &json)
{
XrayServerConfig config;
config.port = json.value(configKey::port).toString();
config.transportProto = json.value(configKey::transportProto).toString();
config.subnetAddress = json.value(configKey::subnetAddress).toString();
config.site = json.value(configKey::site).toString();
config.isThirdPartyConfig = json.value(configKey::isThirdPartyConfig).toBool(false);
return config;
XrayServerConfig c;
// Existing fields
c.port = json.value(configKey::port).toString();
c.transportProto = json.value(configKey::transportProto).toString();
c.subnetAddress = json.value(configKey::subnetAddress).toString();
c.site = json.value(configKey::site).toString();
c.isThirdPartyConfig = json.value(configKey::isThirdPartyConfig).toBool(false);
if (json.contains(configKey::xraySecurity)) {
c.security = json.value(configKey::xraySecurity).toString();
}
if (json.contains(configKey::xrayFlow)) {
c.flow = json.value(configKey::xrayFlow).toString();
}
if (json.contains(configKey::xrayFingerprint)) {
c.fingerprint = json.value(configKey::xrayFingerprint).toString();
if (c.fingerprint.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) {
c.fingerprint = QString::fromLatin1(protocols::xray::defaultFingerprint);
}
}
if (json.contains(configKey::xraySni)) {
c.sni = json.value(configKey::xraySni).toString();
}
if (json.contains(configKey::xrayAlpn)) {
c.alpn = json.value(configKey::xrayAlpn).toString();
}
if (json.contains(configKey::xrayTransport)) {
c.transport = json.value(configKey::xrayTransport).toString();
}
if (json.contains(QLatin1String("xhttp"))) {
const QJsonObject xhttpJson = json.value(QLatin1String("xhttp")).toObject();
if (!xhttpJson.isEmpty()) {
c.xhttp = XrayXhttpConfig::fromJson(xhttpJson);
}
}
if (json.contains(QLatin1String("mkcp"))) {
const QJsonObject mkcpJson = json.value(QLatin1String("mkcp")).toObject();
if (!mkcpJson.isEmpty()) {
c.mkcp = XrayMkcpConfig::fromJson(mkcpJson);
}
}
return c;
}
bool XrayServerConfig::hasEqualServerSettings(const XrayServerConfig& other) const
bool XrayServerConfig::hasEqualServerSettings(const XrayServerConfig &other) const
{
return port == other.port && site == other.site;
return port == other.port
&& transportProto == other.transportProto
&& subnetAddress == other.subnetAddress
&& site == other.site
&& security == other.security
&& flow == other.flow
&& transport == other.transport
&& fingerprint == other.fingerprint
&& sni == other.sni
&& alpn == other.alpn
&& xhttp.toJson() == other.xhttp.toJson()
&& mkcp.toJson() == other.mkcp.toJson();
}
QJsonObject XrayClientConfig::toJson() const
{
QJsonObject obj;
if (!nativeConfig.isEmpty()) {
obj[configKey::config] = nativeConfig;
}
if (!localPort.isEmpty()) {
obj[configKey::localPort] = localPort;
}
if (!id.isEmpty()) {
obj[configKey::clientId] = id;
}
if (!nativeConfig.isEmpty()) obj[configKey::config] = nativeConfig;
if (!localPort.isEmpty()) obj[configKey::localPort] = localPort;
if (!id.isEmpty()) obj[configKey::clientId] = id;
return obj;
}
XrayClientConfig XrayClientConfig::fromJson(const QJsonObject& json)
XrayClientConfig XrayClientConfig::fromJson(const QJsonObject &json)
{
XrayClientConfig config;
config.nativeConfig = json.value(configKey::config).toString();
config.localPort = json.value(configKey::localPort).toString();
config.id = json.value(configKey::clientId).toString();
if (config.id.isEmpty() && !config.nativeConfig.isEmpty()) {
QJsonDocument doc = QJsonDocument::fromJson(config.nativeConfig.toUtf8());
XrayClientConfig c;
c.nativeConfig = json.value(configKey::config).toString();
c.localPort = json.value(configKey::localPort).toString();
c.id = json.value(configKey::clientId).toString();
if (c.id.isEmpty() && !c.nativeConfig.isEmpty()) {
QJsonDocument doc = QJsonDocument::fromJson(c.nativeConfig.toUtf8());
if (!doc.isNull() && doc.isObject()) {
QJsonObject configObj = doc.object();
if (configObj.contains(protocols::xray::outbounds)) {
@@ -100,7 +413,7 @@ XrayClientConfig XrayClientConfig::fromJson(const QJsonObject& json)
if (!users.isEmpty()) {
QJsonObject user = users[0].toObject();
if (user.contains(protocols::xray::id)) {
config.id = user[protocols::xray::id].toString();
c.id = user[protocols::xray::id].toString();
}
}
}
@@ -111,16 +424,15 @@ XrayClientConfig XrayClientConfig::fromJson(const QJsonObject& json)
}
}
}
return config;
return c;
}
QJsonObject XrayProtocolConfig::toJson() const
{
QJsonObject obj = serverConfig.toJson();
if (clientConfig.has_value()) {
// Third-party import: nativeConfig is raw Xray JSON (inbounds/outbounds)
QJsonDocument doc = QJsonDocument::fromJson(clientConfig->nativeConfig.toUtf8());
if (!doc.isNull() && doc.isObject() && doc.object().contains(protocols::xray::outbounds)
&& !doc.object().contains(configKey::config)) {
@@ -130,22 +442,20 @@ QJsonObject XrayProtocolConfig::toJson() const
obj[configKey::lastConfig] = QString::fromUtf8(QJsonDocument(clientJson).toJson(QJsonDocument::Compact));
}
}
return obj;
}
XrayProtocolConfig XrayProtocolConfig::fromJson(const QJsonObject& json)
XrayProtocolConfig XrayProtocolConfig::fromJson(const QJsonObject &json)
{
XrayProtocolConfig config;
config.serverConfig = XrayServerConfig::fromJson(json);
XrayProtocolConfig c;
c.serverConfig = XrayServerConfig::fromJson(json);
QString lastConfigStr = json.value(configKey::lastConfig).toString();
if (!lastConfigStr.isEmpty()) {
QJsonDocument doc = QJsonDocument::fromJson(lastConfigStr.toUtf8());
if (doc.isObject()) {
QJsonObject parsed = doc.object();
// Third-party import stores raw Xray config (inbounds/outbounds) directly
if (parsed.contains(protocols::xray::outbounds) && !parsed.contains(configKey::config)) {
XrayClientConfig clientCfg;
clientCfg.nativeConfig = lastConfigStr;
@@ -158,14 +468,170 @@ XrayProtocolConfig XrayProtocolConfig::fromJson(const QJsonObject& json)
}
}
}
config.clientConfig = clientCfg;
const QJsonArray outbounds = parsed.value(protocols::xray::outbounds).toArray();
if (!outbounds.isEmpty()) {
const QJsonObject settings = outbounds[0].toObject().value(protocols::xray::settings).toObject();
const QJsonArray vnext = settings.value(protocols::xray::vnext).toArray();
if (!vnext.isEmpty()) {
const QJsonArray users = vnext[0].toObject().value(protocols::xray::users).toArray();
if (!users.isEmpty()) {
clientCfg.id = users[0].toObject().value(protocols::xray::id).toString();
}
}
}
c.clientConfig = clientCfg;
} else {
config.clientConfig = XrayClientConfig::fromJson(parsed);
c.clientConfig = XrayClientConfig::fromJson(parsed);
}
}
}
return config;
c.needsClientHydration =
c.hasClientConfig()
&& (!json.contains(configKey::xrayTransport) || c.serverConfig.isThirdPartyConfig);
if (c.needsClientHydration) {
c.hydrateServerConfigFromClientNative();
}
return c;
}
bool XrayProtocolConfig::hydrateServerConfigFromClientNative()
{
if (!clientConfig.has_value() || clientConfig->nativeConfig.isEmpty()) {
return false;
}
QJsonDocument doc = QJsonDocument::fromJson(clientConfig->nativeConfig.toUtf8());
if (doc.isNull() || !doc.isObject()) {
return false;
}
const QJsonObject root = doc.object();
const QJsonArray outbounds = root.value(protocols::xray::outbounds).toArray();
if (outbounds.isEmpty()) {
return false;
}
const QJsonObject outbound = outbounds[0].toObject();
const QJsonObject streamSettings = outbound.value(protocols::xray::streamSettings).toObject();
if (streamSettings.isEmpty()) {
return false;
}
XrayServerConfig &srv = serverConfig;
const QJsonObject settings = outbound.value(protocols::xray::settings).toObject();
const QJsonArray vnext = settings.value(protocols::xray::vnext).toArray();
if (!vnext.isEmpty()) {
const QJsonObject vnextEntry = vnext[0].toObject();
if (vnextEntry.contains(protocols::xray::port)) {
srv.port = QString::number(vnextEntry.value(protocols::xray::port).toInt());
}
const QJsonArray users = vnextEntry.value(protocols::xray::users).toArray();
if (!users.isEmpty()) {
srv.flow = users[0].toObject().value(protocols::xray::flow).toString();
}
}
const QString networkVal = streamSettings.value(protocols::xray::network).toString(QStringLiteral("tcp"));
if (networkVal == QLatin1String("xhttp")) {
srv.transport = QStringLiteral("xhttp");
} else if (networkVal == QLatin1String("kcp")) {
srv.transport = QStringLiteral("mkcp");
} else {
srv.transport = QStringLiteral("raw");
}
if (streamSettings.contains(protocols::xray::security)) {
srv.security = streamSettings.value(protocols::xray::security).toString();
}
if (srv.security == QLatin1String("reality")) {
const QJsonObject rs = streamSettings.value(protocols::xray::realitySettings).toObject();
srv.sni = rs.value(protocols::xray::serverName).toString();
srv.site = srv.sni.isEmpty() ? srv.site : srv.sni;
const QString fp = rs.value(protocols::xray::fingerprint).toString();
if (!fp.isEmpty()) {
srv.fingerprint = fp.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)
? QString::fromLatin1(protocols::xray::defaultFingerprint)
: fp;
}
}
if (srv.security == QLatin1String("tls")) {
const QJsonObject tls = streamSettings.value(QStringLiteral("tlsSettings")).toObject();
srv.sni = tls.value(protocols::xray::serverName).toString();
const QString fp = tls.value(protocols::xray::fingerprint).toString();
if (!fp.isEmpty()) {
srv.fingerprint = fp;
}
QStringList alpnList;
for (const QJsonValue &v : tls.value(QStringLiteral("alpn")).toArray()) {
alpnList << v.toString();
}
if (!alpnList.isEmpty()) {
srv.alpn = alpnList.join(QLatin1Char(','));
}
}
if (srv.transport == QLatin1String("xhttp")) {
const QJsonObject xhttpObj = streamSettings.value(QStringLiteral("xhttpSettings")).toObject();
QJsonObject xhttpJson;
const QString mode = xhttpObj.value(QStringLiteral("mode")).toString();
if (!mode.isEmpty()) {
if (mode == QLatin1String("auto")) {
xhttpJson[configKey::xhttpMode] = QStringLiteral("Auto");
} else if (mode == QLatin1String("packet-up")) {
xhttpJson[configKey::xhttpMode] = QStringLiteral("Packet-up");
} else if (mode == QLatin1String("stream-up")) {
xhttpJson[configKey::xhttpMode] = QStringLiteral("Stream-up");
} else if (mode == QLatin1String("stream-one")) {
xhttpJson[configKey::xhttpMode] = QStringLiteral("Stream-one");
} else {
xhttpJson[configKey::xhttpMode] = mode;
}
}
if (xhttpObj.contains(QStringLiteral("host"))) {
xhttpJson[configKey::xhttpHost] = xhttpObj.value(QStringLiteral("host")).toString();
}
if (xhttpObj.contains(QStringLiteral("path"))) {
xhttpJson[configKey::xhttpPath] = xhttpObj.value(QStringLiteral("path")).toString();
}
if (xhttpObj.contains(QStringLiteral("uplinkHTTPMethod"))) {
xhttpJson[configKey::xhttpUplinkMethod] = xhttpObj.value(QStringLiteral("uplinkHTTPMethod")).toString();
}
xhttpJson[configKey::xhttpDisableGrpc] = xhttpObj.value(QStringLiteral("noGRPCHeader")).toBool(true);
xhttpJson[configKey::xhttpDisableSse] = xhttpObj.value(QStringLiteral("noSSEHeader")).toBool(true);
srv.xhttp = XrayXhttpConfig::fromJson(xhttpJson);
}
if (srv.transport == QLatin1String("mkcp")) {
const QJsonObject kcpObj = streamSettings.value(QStringLiteral("kcpSettings")).toObject();
XrayMkcpConfig mk;
if (kcpObj.contains(QStringLiteral("tti"))) {
mk.tti = QString::number(kcpObj.value(QStringLiteral("tti")).toInt());
}
if (kcpObj.contains(QStringLiteral("uplinkCapacity"))) {
mk.uplinkCapacity = QString::number(kcpObj.value(QStringLiteral("uplinkCapacity")).toInt());
}
if (kcpObj.contains(QStringLiteral("downlinkCapacity"))) {
mk.downlinkCapacity = QString::number(kcpObj.value(QStringLiteral("downlinkCapacity")).toInt());
}
if (kcpObj.contains(QStringLiteral("readBufferSize"))) {
mk.readBufferSize = QString::number(kcpObj.value(QStringLiteral("readBufferSize")).toInt());
}
if (kcpObj.contains(QStringLiteral("writeBufferSize"))) {
mk.writeBufferSize = QString::number(kcpObj.value(QStringLiteral("writeBufferSize")).toInt());
}
if (kcpObj.contains(QStringLiteral("congestion"))) {
mk.congestion = kcpObj.value(QStringLiteral("congestion")).toBool(true);
}
srv.mkcp = mk;
}
needsClientHydration = false;
return true;
}
bool XrayProtocolConfig::hasClientConfig() const
@@ -173,7 +639,7 @@ bool XrayProtocolConfig::hasClientConfig() const
return clientConfig.has_value();
}
void XrayProtocolConfig::setClientConfig(const XrayClientConfig& config)
void XrayProtocolConfig::setClientConfig(const XrayClientConfig &config)
{
clientConfig = config;
}
@@ -184,4 +650,3 @@ void XrayProtocolConfig::clearClientConfig()
}
} // namespace amnezia
+112 -11
View File
@@ -2,47 +2,148 @@
#define XRAYPROTOCOLCONFIG_H
#include <QJsonObject>
#include "core/utils/constants/protocolConstants.h"
#include <QString>
#include <optional>
namespace amnezia
{
// ── xPadding ─────────────────────────────────────────────────────────────────
struct XrayXPaddingConfig {
QString bytesMin; // xPaddingBytes min
QString bytesMax; // xPaddingBytes max
bool obfsMode = true; // xPaddingObfsMode
QString key; // xPaddingKey
QString header; // xPaddingHeader
QString placement = protocols::xray::defaultXPaddingPlacement; // xPaddingPlacement: Cookie|Header|Query|Body
QString method = protocols::xray::defaultXPaddingMethod; // xPaddingMethod: Repeat-x|Random|Zero
QJsonObject toJson() const;
static XrayXPaddingConfig fromJson(const QJsonObject &json);
};
// ── xmux ─────────────────────────────────────────────────────────────────────
struct XrayXmuxConfig {
bool enabled = true;
QString maxConcurrencyMin = "0";
QString maxConcurrencyMax = "0";
QString maxConnectionsMin = "0";
QString maxConnectionsMax = "0";
QString cMaxReuseTimesMin = "0";
QString cMaxReuseTimesMax = "0";
QString hMaxRequestTimesMin = "0";
QString hMaxRequestTimesMax = "0";
QString hMaxReusableSecsMin = "0";
QString hMaxReusableSecsMax = "0";
QString hKeepAlivePeriod;
QJsonObject toJson() const;
static XrayXmuxConfig fromJson(const QJsonObject &json);
};
// ── XHTTP transport ───────────────────────────────────────────────────────────
struct XrayXhttpConfig {
QString mode = protocols::xray::defaultXhttpMode; // Auto|Packet-up|Stream-up|Stream-one
QString host = protocols::xray::defaultXhttpHost;
QString path;
QString headersTemplate = protocols::xray::defaultXhttpHeadersTemplate; // HTTP|None
QString uplinkMethod = protocols::xray::defaultXhttpUplinkMethod; // POST|PUT|PATCH
bool disableGrpc = true;
bool disableSse = true;
// Session & Sequence
QString sessionPlacement = protocols::xray::defaultXhttpSessionPlacement;
QString sessionKey = protocols::xray::defaultXhttpSessionKey;
QString seqPlacement = protocols::xray::defaultXhttpSeqPlacement;
QString seqKey;
QString uplinkDataPlacement = protocols::xray::defaultXhttpUplinkDataPlacement;
QString uplinkDataKey;
// Traffic Shaping
QString uplinkChunkSize = protocols::xray::defaultXhttpUplinkChunkSize;
QString scMaxBufferedPosts;
QString scMaxEachPostBytesMin = protocols::xray::defaultXhttpScMaxEachPostBytesMin;
QString scMaxEachPostBytesMax = protocols::xray::defaultXhttpScMaxEachPostBytesMax;
QString scMinPostsIntervalMsMin = protocols::xray::defaultXhttpScMinPostsIntervalMsMin;
QString scMinPostsIntervalMsMax = protocols::xray::defaultXhttpScMinPostsIntervalMsMax;
QString scStreamUpServerSecsMin = protocols::xray::defaultXhttpScStreamUpServerSecsMin;
QString scStreamUpServerSecsMax = protocols::xray::defaultXhttpScStreamUpServerSecsMax;
XrayXPaddingConfig xPadding;
XrayXmuxConfig xmux;
QJsonObject toJson() const;
/// Reads only keys present in JSON (no Amnezia UI defaults). Use XrayConfigModel::applyDefaultsToServerConfig for UI.
static XrayXhttpConfig fromJson(const QJsonObject &json);
};
// ── mKCP transport ────────────────────────────────────────────────────────────
struct XrayMkcpConfig {
QString tti;
QString uplinkCapacity;
QString downlinkCapacity;
QString readBufferSize;
QString writeBufferSize;
bool congestion = true;
QJsonObject toJson() const;
static XrayMkcpConfig fromJson(const QJsonObject &json);
};
// ── Server config (settings editable by user) ─────────────────────────────────
struct XrayServerConfig {
QString port;
QString transportProto;
QString subnetAddress;
QString site;
bool isThirdPartyConfig = false;
QString security;
QString flow;
QString fingerprint;
QString sni;
QString alpn;
QString transport;
XrayXhttpConfig xhttp;
XrayMkcpConfig mkcp;
QJsonObject toJson() const;
static XrayServerConfig fromJson(const QJsonObject& json);
bool hasEqualServerSettings(const XrayServerConfig& other) const;
static XrayServerConfig fromJson(const QJsonObject &json);
bool hasEqualServerSettings(const XrayServerConfig &other) const;
};
// ── Client config (generated, not edited by user) ─────────────────────────────
struct XrayClientConfig {
QString nativeConfig;
QString localPort;
QString id;
QJsonObject toJson() const;
static XrayClientConfig fromJson(const QJsonObject& json);
static XrayClientConfig fromJson(const QJsonObject &json);
};
// ── Top-level protocol config ──────────────────────────────────────────────────
struct XrayProtocolConfig {
XrayServerConfig serverConfig;
std::optional<XrayClientConfig> clientConfig;
QJsonObject toJson() const;
static XrayProtocolConfig fromJson(const QJsonObject& json);
static XrayProtocolConfig fromJson(const QJsonObject &json);
bool hasClientConfig() const;
void setClientConfig(const XrayClientConfig& config);
void setClientConfig(const XrayClientConfig &config);
void clearClientConfig();
bool needsClientHydration = false;
bool hydrateServerConfigFromClientNative();
};
} // namespace amnezia
#endif // XRAYPROTOCOLCONFIG_H
@@ -9,6 +9,7 @@
#include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/networkUtilities.h"
namespace amnezia
{
@@ -28,6 +29,25 @@ ContainerConfig NativeServerConfig::containerConfig(DockerContainer container) c
return containers.value(container);
}
void NativeServerConfig::updateContainerConfig(DockerContainer container, const ContainerConfig &config)
{
containers[container] = config;
}
QPair<QString, QString> NativeServerConfig::getDnsPair(const QString &primaryDns, const QString &secondaryDns) const
{
QString d1 = dns1;
QString d2 = dns2;
if (d1.isEmpty() || !NetworkUtilities::checkIPv4Format(d1)) {
d1 = primaryDns;
}
if (d2.isEmpty() || !NetworkUtilities::checkIPv4Format(d2)) {
d2 = secondaryDns;
}
return { d1, d2 };
}
QJsonObject NativeServerConfig::toJson() const
{
QJsonObject obj;
@@ -86,6 +106,10 @@ NativeServerConfig NativeServerConfig::fromJson(const QJsonObject& json)
config.dns1 = json.value(configKey::dns1).toString();
config.dns2 = json.value(configKey::dns2).toString();
if (config.displayName.isEmpty()) {
config.displayName = config.description.isEmpty() ? config.hostName : config.description;
}
return config;
}
@@ -3,6 +3,7 @@
#include <QJsonObject>
#include <QMap>
#include <QPair>
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
@@ -16,6 +17,7 @@ using namespace ContainerEnumNS;
struct NativeServerConfig {
QString description;
QString displayName;
QString hostName;
QMap<DockerContainer, ContainerConfig> containers;
DockerContainer defaultContainer;
@@ -24,6 +26,11 @@ struct NativeServerConfig {
bool hasContainers() const;
ContainerConfig containerConfig(DockerContainer container) const;
void updateContainerConfig(DockerContainer container, const ContainerConfig &config);
QPair<QString, QString> getDnsPair(const QString &primaryDns, const QString &secondaryDns) const;
QJsonObject toJson() const;
static NativeServerConfig fromJson(const QJsonObject& json);
};
@@ -0,0 +1,166 @@
#include "selfHostedAdminServerConfig.h"
#include <QJsonArray>
#include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
#include "core/utils/networkUtilities.h"
namespace amnezia
{
using namespace ContainerEnumNS;
bool SelfHostedAdminServerConfig::hasCredentials() const
{
return !userName.isEmpty() && !password.isEmpty() && port > 0;
}
bool SelfHostedAdminServerConfig::isReadOnly() const
{
return !hasCredentials();
}
ServerCredentials SelfHostedAdminServerConfig::credentials() const
{
ServerCredentials creds;
creds.hostName = hostName;
creds.userName = userName;
creds.secretData = password;
creds.port = port;
return creds;
}
bool SelfHostedAdminServerConfig::hasContainers() const
{
return !containers.isEmpty();
}
ContainerConfig SelfHostedAdminServerConfig::containerConfig(DockerContainer container) const
{
if (!containers.contains(container)) {
return ContainerConfig{};
}
return containers.value(container);
}
void SelfHostedAdminServerConfig::updateContainerConfig(DockerContainer container, const ContainerConfig &config)
{
containers[container] = config;
}
void SelfHostedAdminServerConfig::clearCachedClientProfile(DockerContainer container)
{
if (ContainerUtils::containerService(container) == ServiceType::Other) {
return;
}
ContainerConfig cleared = containerConfig(container);
cleared.protocolConfig.clearClientConfig();
containers[container] = cleared;
}
QPair<QString, QString> SelfHostedAdminServerConfig::getDnsPair(bool isAmneziaDnsEnabled, const QString &primaryDns,
const QString &secondaryDns) const
{
QString d1 = dns1;
QString d2 = dns2;
const bool dnsOnServer = containers.contains(DockerContainer::Dns);
if (d1.isEmpty() || !NetworkUtilities::checkIPv4Format(d1)) {
d1 = (isAmneziaDnsEnabled && dnsOnServer) ? protocols::dns::amneziaDnsIp : primaryDns;
}
if (d2.isEmpty() || !NetworkUtilities::checkIPv4Format(d2)) {
d2 = secondaryDns;
}
return { d1, d2 };
}
QJsonObject SelfHostedAdminServerConfig::toJson() const
{
QJsonObject obj;
if (!description.isEmpty()) {
obj[configKey::description] = this->description;
}
if (!hostName.isEmpty()) {
obj[configKey::hostName] = hostName;
}
QJsonArray containersArray;
for (auto it = containers.begin(); it != containers.end(); ++it) {
QJsonObject containerObj = it.value().toJson();
containersArray.append(containerObj);
}
if (!containersArray.isEmpty()) {
obj[configKey::containers] = containersArray;
}
if (defaultContainer != DockerContainer::None) {
obj[configKey::defaultContainer] = ContainerUtils::containerToString(defaultContainer);
}
if (!dns1.isEmpty()) {
obj[configKey::dns1] = dns1;
}
if (!dns2.isEmpty()) {
obj[configKey::dns2] = dns2;
}
if (!userName.isEmpty()) {
obj[configKey::userName] = userName;
}
if (!password.isEmpty()) {
obj[configKey::password] = password;
}
if (port > 0) {
obj[configKey::port] = port;
}
return obj;
}
SelfHostedAdminServerConfig SelfHostedAdminServerConfig::fromJson(const QJsonObject &json)
{
SelfHostedAdminServerConfig config;
config.description = json.value(configKey::description).toString();
config.hostName = json.value(configKey::hostName).toString();
QJsonArray containersArray = json.value(configKey::containers).toArray();
for (const QJsonValue &val : containersArray) {
QJsonObject containerObj = val.toObject();
ContainerConfig cc = ContainerConfig::fromJson(containerObj);
QString containerStr = containerObj.value(configKey::container).toString();
DockerContainer container = ContainerUtils::containerFromString(containerStr);
config.containers.insert(container, cc);
}
QString defaultContainerStr = json.value(configKey::defaultContainer).toString();
config.defaultContainer = ContainerUtils::containerFromString(defaultContainerStr);
config.dns1 = json.value(configKey::dns1).toString();
config.dns2 = json.value(configKey::dns2).toString();
config.userName = json.value(configKey::userName).toString();
config.password = json.value(configKey::password).toString();
if (json.contains(configKey::port)) {
config.port = json.value(configKey::port).toInt();
} else {
config.port = 0;
}
if (config.displayName.isEmpty()) {
config.displayName = config.description.isEmpty() ? config.hostName : config.description;
}
return config;
}
} // namespace amnezia
@@ -0,0 +1,53 @@
#ifndef SELFHOSTEDADMINSERVERCONFIG_H
#define SELFHOSTEDADMINSERVERCONFIG_H
#include <QJsonObject>
#include <QMap>
#include <QPair>
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
#include "core/models/containerConfig.h"
#include "core/utils/errorCodes.h"
#include "core/utils/routeModes.h"
#include "core/utils/commonStructs.h"
namespace amnezia
{
using namespace ContainerEnumNS;
struct SelfHostedAdminServerConfig {
QString description;
QString displayName;
QString hostName;
QMap<DockerContainer, ContainerConfig> containers;
DockerContainer defaultContainer;
QString dns1;
QString dns2;
QString userName;
QString password;
int port = 0;
bool hasCredentials() const;
bool isReadOnly() const;
ServerCredentials credentials() const;
bool hasContainers() const;
ContainerConfig containerConfig(DockerContainer container) const;
void updateContainerConfig(DockerContainer container, const ContainerConfig &config);
void clearCachedClientProfile(DockerContainer container);
QPair<QString, QString> getDnsPair(bool isAmneziaDnsEnabled, const QString &primaryDns,
const QString &secondaryDns) const;
QJsonObject toJson() const;
static SelfHostedAdminServerConfig fromJson(const QJsonObject &json);
};
} // namespace amnezia
#endif // SELFHOSTEDADMINSERVERCONFIG_H
@@ -1,53 +1,41 @@
#include "selfHostedServerConfig.h"
#include "selfHostedUserServerConfig.h"
#include <QJsonArray>
#include <QJsonDocument>
#include <stdexcept>
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
#include "core/utils/protocolEnum.h"
#include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
#include "core/utils/networkUtilities.h"
namespace amnezia
{
using namespace ContainerEnumNS;
bool SelfHostedServerConfig::hasCredentials() const
bool SelfHostedUserServerConfig::hasCredentials() const
{
return userName.has_value() && password.has_value() && port.has_value();
return false;
}
bool SelfHostedServerConfig::isReadOnly() const
bool SelfHostedUserServerConfig::isReadOnly() const
{
return !hasCredentials();
return true;
}
std::optional<ServerCredentials> SelfHostedServerConfig::credentials() const
std::optional<ServerCredentials> SelfHostedUserServerConfig::credentials() const
{
if (!hasCredentials()) {
return std::nullopt;
}
ServerCredentials creds;
creds.hostName = hostName;
creds.userName = userName.value();
creds.secretData = password.value();
creds.port = port.value();
return creds;
return std::nullopt;
}
bool SelfHostedServerConfig::hasContainers() const
bool SelfHostedUserServerConfig::hasContainers() const
{
return !containers.isEmpty();
}
ContainerConfig SelfHostedServerConfig::containerConfig(DockerContainer container) const
ContainerConfig SelfHostedUserServerConfig::containerConfig(DockerContainer container) const
{
if (!containers.contains(container)) {
return ContainerConfig{};
@@ -55,17 +43,37 @@ ContainerConfig SelfHostedServerConfig::containerConfig(DockerContainer containe
return containers.value(container);
}
QJsonObject SelfHostedServerConfig::toJson() const
void SelfHostedUserServerConfig::updateContainerConfig(DockerContainer container, const ContainerConfig &config)
{
containers[container] = config;
}
QPair<QString, QString> SelfHostedUserServerConfig::getDnsPair(const QString &primaryDns,
const QString &secondaryDns) const
{
QString d1 = dns1;
QString d2 = dns2;
if (d1.isEmpty() || !NetworkUtilities::checkIPv4Format(d1)) {
d1 = primaryDns;
}
if (d2.isEmpty() || !NetworkUtilities::checkIPv4Format(d2)) {
d2 = secondaryDns;
}
return { d1, d2 };
}
QJsonObject SelfHostedUserServerConfig::toJson() const
{
QJsonObject obj;
if (!description.isEmpty()) {
obj[configKey::description] = this->description;
}
if (!hostName.isEmpty()) {
obj[configKey::hostName] = hostName;
}
QJsonArray containersArray;
for (auto it = containers.begin(); it != containers.end(); ++it) {
QJsonObject containerObj = it.value().toJson();
@@ -74,67 +82,50 @@ QJsonObject SelfHostedServerConfig::toJson() const
if (!containersArray.isEmpty()) {
obj[configKey::containers] = containersArray;
}
if (defaultContainer != DockerContainer::None) {
obj[configKey::defaultContainer] = ContainerUtils::containerToString(defaultContainer);
}
if (!dns1.isEmpty()) {
obj[configKey::dns1] = dns1;
}
if (!dns2.isEmpty()) {
obj[configKey::dns2] = dns2;
}
if (userName.has_value()) {
obj[configKey::userName] = userName.value();
}
if (password.has_value()) {
obj[configKey::password] = password.value();
}
if (port.has_value()) {
obj[configKey::port] = port.value();
}
return obj;
}
SelfHostedServerConfig SelfHostedServerConfig::fromJson(const QJsonObject& json)
SelfHostedUserServerConfig SelfHostedUserServerConfig::fromJson(const QJsonObject &json)
{
SelfHostedServerConfig config;
SelfHostedUserServerConfig config;
config.description = json.value(configKey::description).toString();
config.hostName = json.value(configKey::hostName).toString();
QJsonArray containersArray = json.value(configKey::containers).toArray();
for (const QJsonValue& val : containersArray) {
for (const QJsonValue &val : containersArray) {
QJsonObject containerObj = val.toObject();
ContainerConfig containerConfig = ContainerConfig::fromJson(containerObj);
ContainerConfig cc = ContainerConfig::fromJson(containerObj);
QString containerStr = containerObj.value(configKey::container).toString();
DockerContainer container = ContainerUtils::containerFromString(containerStr);
config.containers.insert(container, containerConfig);
config.containers.insert(container, cc);
}
QString defaultContainerStr = json.value(configKey::defaultContainer).toString();
config.defaultContainer = ContainerUtils::containerFromString(defaultContainerStr);
config.dns1 = json.value(configKey::dns1).toString();
config.dns2 = json.value(configKey::dns2).toString();
if (json.contains(configKey::userName)) {
config.userName = json.value(configKey::userName).toString();
if (config.displayName.isEmpty()) {
config.displayName = config.description.isEmpty() ? config.hostName : config.description;
}
if (json.contains(configKey::password)) {
config.password = json.value(configKey::password).toString();
}
if (json.contains(configKey::port)) {
config.port = json.value(configKey::port).toInt();
}
return config;
}
} // namespace amnezia
@@ -1,16 +1,15 @@
#ifndef SELFHOSTEDSERVERCONFIG_H
#define SELFHOSTEDSERVERCONFIG_H
#ifndef SELFHOSTEDUSERSERVERCONFIG_H
#define SELFHOSTEDUSERSERVERCONFIG_H
#include <QJsonObject>
#include <QMap>
#include <QPair>
#include <optional>
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
#include "core/models/containerConfig.h"
#include "core/utils/errorCodes.h"
#include "core/utils/routeModes.h"
#include "core/utils/commonStructs.h"
namespace amnezia
@@ -18,28 +17,29 @@ namespace amnezia
using namespace ContainerEnumNS;
struct SelfHostedServerConfig {
struct SelfHostedUserServerConfig {
QString description;
QString displayName;
QString hostName;
QMap<DockerContainer, ContainerConfig> containers;
DockerContainer defaultContainer;
QString dns1;
QString dns2;
std::optional<QString> userName;
std::optional<QString> password;
std::optional<int> port;
bool hasCredentials() const;
bool isReadOnly() const;
std::optional<ServerCredentials> credentials() const;
bool hasContainers() const;
ContainerConfig containerConfig(DockerContainer container) const;
void updateContainerConfig(DockerContainer container, const ContainerConfig &config);
QPair<QString, QString> getDnsPair(const QString &primaryDns, const QString &secondaryDns) const;
QJsonObject toJson() const;
static SelfHostedServerConfig fromJson(const QJsonObject& json);
static SelfHostedUserServerConfig fromJson(const QJsonObject &json);
};
} // namespace amnezia
#endif // SELFHOSTEDSERVERCONFIG_H
#endif // SELFHOSTEDUSERSERVERCONFIG_H
-234
View File
@@ -1,234 +0,0 @@
#include "serverConfig.h"
#include "core/utils/api/apiUtils.h"
#include "core/utils/networkUtilities.h"
#include "core/models/selfhosted/selfHostedServerConfig.h"
#include "core/models/selfhosted/nativeServerConfig.h"
#include "core/models/api/apiV1ServerConfig.h"
#include "core/models/api/apiV2ServerConfig.h"
#include "core/utils/protocolEnum.h"
#include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
namespace amnezia
{
using namespace ContainerEnumNS;
QString ServerConfig::description() const
{
return std::visit([](const auto& v) { return v.description; }, data);
}
QString ServerConfig::hostName() const
{
return std::visit([](const auto& v) { return v.hostName; }, data);
}
QString ServerConfig::displayName() const
{
if (isApiV1()) {
const auto *apiV1 = as<ApiV1ServerConfig>();
return apiV1 ? apiV1->name : description();
}
if (isApiV2()) {
const auto *apiV2 = as<ApiV2ServerConfig>();
return apiV2 ? apiV2->name : description();
}
QString name = description();
return name.isEmpty() ? hostName() : name;
}
QMap<DockerContainer, ContainerConfig> ServerConfig::containers() const
{
return std::visit([](const auto& v) { return v.containers; }, data);
}
DockerContainer ServerConfig::defaultContainer() const
{
return std::visit([](const auto& v) { return v.defaultContainer; }, data);
}
QString ServerConfig::dns1() const
{
return std::visit([](const auto& v) { return v.dns1; }, data);
}
QString ServerConfig::dns2() const
{
return std::visit([](const auto& v) { return v.dns2; }, data);
}
bool ServerConfig::hasContainers() const
{
return std::visit([](const auto& v) { return v.hasContainers(); }, data);
}
ContainerConfig ServerConfig::containerConfig(DockerContainer container) const
{
return std::visit([container](const auto& v) { return v.containerConfig(container); }, data);
}
int ServerConfig::crc() const
{
return std::visit([](const auto& v) -> int {
using T = std::decay_t<decltype(v)>;
if constexpr (std::is_same_v<T, ApiV1ServerConfig> ||
std::is_same_v<T, ApiV2ServerConfig>) {
return v.crc;
}
return 0;
}, data);
}
int ServerConfig::configVersion() const
{
return std::visit([](const auto& v) -> int {
using T = std::decay_t<decltype(v)>;
if constexpr (std::is_same_v<T, ApiV1ServerConfig>) {
return apiDefs::ConfigSource::Telegram;
} else if constexpr (std::is_same_v<T, ApiV2ServerConfig>) {
return apiDefs::ConfigSource::AmneziaGateway;
}
return 0; // SelfHostedServerConfig or NativeServerConfig
}, data);
}
bool ServerConfig::isSelfHosted() const
{
return std::holds_alternative<SelfHostedServerConfig>(data);
}
bool ServerConfig::isNative() const
{
return std::holds_alternative<NativeServerConfig>(data);
}
bool ServerConfig::isApiV1() const
{
return std::holds_alternative<ApiV1ServerConfig>(data);
}
bool ServerConfig::isApiV2() const
{
return std::holds_alternative<ApiV2ServerConfig>(data);
}
bool ServerConfig::isApiConfig() const
{
return isApiV1() || isApiV2();
}
QJsonObject ServerConfig::toJson() const
{
return std::visit([](const auto& v) { return v.toJson(); }, data);
}
ServerConfig ServerConfig::fromJson(const QJsonObject& json)
{
apiDefs::ConfigType configType = apiUtils::getConfigType(json);
switch (configType) {
case apiDefs::ConfigType::SelfHosted: {
bool hasThirdPartyConfig = false;
QJsonArray containersArray = json.value(configKey::containers).toArray();
for (const QJsonValue& val : containersArray) {
QJsonObject containerObj = val.toObject();
for (auto it = containerObj.begin(); it != containerObj.end(); ++it) {
QString key = it.key();
if (key != configKey::container) {
QJsonObject protocolObj = it.value().toObject();
if (protocolObj.contains(configKey::isThirdPartyConfig) &&
protocolObj.value(configKey::isThirdPartyConfig).toBool()) {
hasThirdPartyConfig = true;
break;
}
}
}
if (hasThirdPartyConfig) {
break;
}
}
if (hasThirdPartyConfig) {
return ServerConfig{NativeServerConfig::fromJson(json)};
} else {
return ServerConfig{SelfHostedServerConfig::fromJson(json)};
}
}
case apiDefs::ConfigType::AmneziaPremiumV1:
case apiDefs::ConfigType::AmneziaFreeV2:
return ServerConfig{ApiV1ServerConfig::fromJson(json)};
case apiDefs::ConfigType::AmneziaPremiumV2:
case apiDefs::ConfigType::AmneziaFreeV3:
case apiDefs::ConfigType::ExternalPremium:
return ServerConfig{ApiV2ServerConfig::fromJson(json)};
default: {
// Check if any container has isThirdPartyConfig
bool hasThirdPartyConfig = false;
QJsonArray containersArray = json.value(configKey::containers).toArray();
for (const QJsonValue& val : containersArray) {
QJsonObject containerObj = val.toObject();
// Check all protocol keys in the container object
for (auto it = containerObj.begin(); it != containerObj.end(); ++it) {
QString key = it.key();
if (key != configKey::container) {
QJsonObject protocolObj = it.value().toObject();
if (protocolObj.contains(configKey::isThirdPartyConfig) &&
protocolObj.value(configKey::isThirdPartyConfig).toBool()) {
hasThirdPartyConfig = true;
break;
}
}
}
if (hasThirdPartyConfig) {
break;
}
}
if (hasThirdPartyConfig) {
return ServerConfig{NativeServerConfig::fromJson(json)};
} else {
return ServerConfig{SelfHostedServerConfig::fromJson(json)};
}
}
}
}
QPair<QString, QString> ServerConfig::getDnsPair(bool isAmneziaDnsEnabled,
const QString &primaryDns,
const QString &secondaryDns) const
{
QPair<QString, QString> dns;
QMap<DockerContainer, ContainerConfig> serverContainers = containers();
bool isDnsContainerInstalled = false;
for (auto it = serverContainers.begin(); it != serverContainers.end(); ++it) {
if (it.key() == DockerContainer::Dns) {
isDnsContainerInstalled = true;
break;
}
}
dns.first = dns1();
dns.second = dns2();
if (dns.first.isEmpty() || !NetworkUtilities::checkIPv4Format(dns.first)) {
if (isAmneziaDnsEnabled && isDnsContainerInstalled) {
dns.first = protocols::dns::amneziaDnsIp;
} else {
dns.first = primaryDns;
}
}
if (dns.second.isEmpty() || !NetworkUtilities::checkIPv4Format(dns.second)) {
dns.second = secondaryDns;
}
return dns;
}
} // namespace amnezia
-92
View File
@@ -1,92 +0,0 @@
#ifndef SERVERCONFIG_H
#define SERVERCONFIG_H
#include <variant>
#include <QJsonObject>
#include <QMap>
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
#include "core/models/selfhosted/selfHostedServerConfig.h"
#include "core/models/selfhosted/nativeServerConfig.h"
#include "core/models/api/apiV1ServerConfig.h"
#include "core/models/api/apiV2ServerConfig.h"
#include "core/models/containerConfig.h"
namespace amnezia
{
using namespace ContainerEnumNS;
struct ServerConfig {
using Variant = std::variant<
SelfHostedServerConfig,
NativeServerConfig,
ApiV1ServerConfig,
ApiV2ServerConfig
>;
Variant data;
ServerConfig() = default;
ServerConfig(const Variant& v) : data(v) {}
ServerConfig(Variant&& v) : data(std::move(v)) {}
template<typename T, typename = std::enable_if_t<!std::is_same<std::remove_cv_t<std::remove_reference_t<T>>, ServerConfig>::value>>
ServerConfig(const T& v) : data(v) {}
template<typename T, typename = std::enable_if_t<!std::is_same<std::remove_cv_t<std::remove_reference_t<T>>, ServerConfig>::value>>
ServerConfig(T&& v) : data(std::forward<T>(v)) {}
QString description() const;
QString hostName() const;
QString displayName() const;
QMap<DockerContainer, ContainerConfig> containers() const;
DockerContainer defaultContainer() const;
QString dns1() const;
QString dns2() const;
bool hasContainers() const;
ContainerConfig containerConfig(DockerContainer container) const;
int crc() const;
int configVersion() const;
bool isSelfHosted() const;
bool isNative() const;
bool isApiV1() const;
bool isApiV2() const;
bool isApiConfig() const;
template<typename T>
T* as() {
return std::get_if<T>(&data);
}
template<typename T>
const T* as() const {
return std::get_if<T>(&data);
}
QJsonObject toJson() const;
static ServerConfig fromJson(const QJsonObject& json);
template<typename Visitor>
auto visit(Visitor&& visitor) {
return std::visit(std::forward<Visitor>(visitor), data);
}
template<typename Visitor>
auto visit(Visitor&& visitor) const {
return std::visit(std::forward<Visitor>(visitor), data);
}
QPair<QString, QString> getDnsPair(bool isAmneziaDnsEnabled,
const QString &primaryDns,
const QString &secondaryDns) const;
};
} // namespace amnezia
#endif // SERVERCONFIG_H
+187
View File
@@ -0,0 +1,187 @@
#include "serverDescription.h"
#include <QMap>
#include "core/utils/serverConfigUtils.h"
#include "core/utils/constants/apiKeys.h"
#include "core/utils/constants/apiConstants.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/api/apiUtils.h"
#include "core/utils/containers/containerUtils.h"
#include "core/protocols/protocolUtils.h"
#include "core/models/protocols/awgProtocolConfig.h"
using namespace amnezia;
namespace
{
bool computeHasInstalledVpnContainers(const QMap<DockerContainer, ContainerConfig> &containers)
{
for (auto it = containers.begin(); it != containers.end(); ++it) {
const DockerContainer container = it.key();
if (ContainerUtils::containerService(container) == ServiceType::Vpn || container == DockerContainer::SSXray) {
return true;
}
}
return false;
}
template <typename T>
ServerDescription buildBaseDescription(const T &server)
{
ServerDescription row;
row.hostName = server.hostName;
row.defaultContainer = server.defaultContainer;
row.primaryDnsIsAmnezia = (server.dns1 == protocols::dns::amneziaDnsIp);
row.hasInstalledVpnContainers = computeHasInstalledVpnContainers(server.containers);
return row;
}
QString getBaseDescription(const QMap<DockerContainer, ContainerConfig> &containers,
bool isAmneziaDnsEnabled,
bool hasWriteAccess,
bool primaryDnsIsAmnezia)
{
QString description;
if (hasWriteAccess) {
const bool isDnsInstalled = containers.contains(DockerContainer::Dns);
if (isAmneziaDnsEnabled && isDnsInstalled) {
description += QStringLiteral("Amnezia DNS | ");
}
} else if (primaryDnsIsAmnezia) {
description += QStringLiteral("Amnezia DNS | ");
}
return description;
}
QString getProtocolName(DockerContainer defaultContainer, const QMap<DockerContainer, ContainerConfig> &containers)
{
QString containerName = ContainerUtils::containerHumanNames().value(defaultContainer);
QString protocolVersion;
if (ContainerUtils::isAwgContainer(defaultContainer)) {
const auto it = containers.constFind(defaultContainer);
if (it != containers.cend()) {
if (const AwgProtocolConfig *awg = it->getAwgProtocolConfig()) {
protocolVersion = ProtocolUtils::getProtocolVersionString(awg->toJson());
if (defaultContainer == DockerContainer::Awg && !awg->serverConfig.isThirdPartyConfig) {
containerName = QStringLiteral("AmneziaWG Legacy");
}
}
}
}
return containerName + protocolVersion + QStringLiteral(" | ");
}
} // namespace
namespace amnezia
{
ServerDescription buildServerDescription(const SelfHostedAdminServerConfig &server, bool isAmneziaDnsEnabled)
{
ServerDescription row = buildBaseDescription(server);
row.selfHostedSshCredentials.hostName = server.hostName;
row.selfHostedSshCredentials.userName = server.userName;
row.selfHostedSshCredentials.secretData = server.password;
row.selfHostedSshCredentials.port = server.port > 0 ? server.port : 22;
row.hasWriteAccess = !row.selfHostedSshCredentials.userName.isEmpty()
&& !row.selfHostedSshCredentials.secretData.isEmpty();
row.serverName = server.displayName;
row.baseDescription = getBaseDescription(server.containers, isAmneziaDnsEnabled, row.hasWriteAccess, row.primaryDnsIsAmnezia);
const QString protocolName = getProtocolName(server.defaultContainer, server.containers);
row.expandedServerDescription = row.baseDescription + row.hostName;
row.collapsedServerDescription = row.baseDescription + protocolName + row.hostName;
return row;
}
ServerDescription buildServerDescription(const SelfHostedUserServerConfig &server, bool isAmneziaDnsEnabled)
{
ServerDescription row = buildBaseDescription(server);
row.selfHostedSshCredentials.hostName = server.hostName;
row.selfHostedSshCredentials.port = 22;
row.hasWriteAccess = false;
row.serverName = server.displayName;
row.baseDescription = getBaseDescription(server.containers, isAmneziaDnsEnabled, row.hasWriteAccess, row.primaryDnsIsAmnezia);
const QString protocolName = getProtocolName(server.defaultContainer, server.containers);
row.expandedServerDescription = row.baseDescription + row.hostName;
row.collapsedServerDescription = row.baseDescription + protocolName + row.hostName;
return row;
}
ServerDescription buildServerDescription(const NativeServerConfig &server, bool isAmneziaDnsEnabled)
{
ServerDescription row = buildBaseDescription(server);
row.hasWriteAccess = false;
row.serverName = server.displayName;
row.baseDescription = getBaseDescription(server.containers, isAmneziaDnsEnabled, row.hasWriteAccess, row.primaryDnsIsAmnezia);
const QString protocolName = getProtocolName(server.defaultContainer, server.containers);
row.expandedServerDescription = row.baseDescription + row.hostName;
row.collapsedServerDescription = row.baseDescription + protocolName + row.hostName;
return row;
}
ServerDescription buildServerDescription(const LegacyApiServerConfig &server, bool /*isAmneziaDnsEnabled*/)
{
ServerDescription row = buildBaseDescription(server);
row.configVersion = serverConfigUtils::ConfigSource::Telegram;
row.isApiV1 = true;
row.isServerFromGatewayApi = false;
row.hasWriteAccess = false;
row.serverName = server.displayName;
row.baseDescription = server.description;
const QString fullDescriptionForCollapsed = row.baseDescription;
row.collapsedServerDescription = fullDescriptionForCollapsed;
row.expandedServerDescription = fullDescriptionForCollapsed;
return row;
}
ServerDescription buildServerDescription(const ApiV2ServerConfig &server, bool /*isAmneziaDnsEnabled*/)
{
ServerDescription row = buildBaseDescription(server);
row.configVersion = serverConfigUtils::ConfigSource::AmneziaGateway;
row.isApiV2 = true;
row.isServerFromGatewayApi = true;
row.isPremium = server.isPremium() || server.isExternalPremium();
row.hasWriteAccess = false;
row.serverName = server.displayName;
row.baseDescription = server.apiConfig.serverCountryCode.isEmpty() ? server.description : server.apiConfig.serverCountryName;
row.isCountrySelectionAvailable = !server.apiConfig.availableCountries.isEmpty();
row.apiAvailableCountries = server.apiConfig.availableCountries;
row.apiServerCountryCode = server.apiConfig.serverCountryCode;
row.isAdVisible = server.apiConfig.serviceInfo.isAdVisible;
row.adHeader = server.apiConfig.serviceInfo.adHeader;
row.adDescription = server.apiConfig.serviceInfo.adDescription;
row.adEndpoint = server.apiConfig.serviceInfo.adEndpoint;
row.isRenewalAvailable = server.apiConfig.serviceInfo.isRenewalAvailable;
if (!server.apiConfig.isInAppPurchase) {
if (server.apiConfig.subscriptionExpiredByServer) {
row.isSubscriptionExpired = true;
} else if (!server.apiConfig.subscription.endDate.isEmpty()) {
row.isSubscriptionExpired = apiUtils::isSubscriptionExpired(server.apiConfig.subscription.endDate);
row.isSubscriptionExpiringSoon = apiUtils::isSubscriptionExpiringSoon(server.apiConfig.subscription.endDate);
}
}
const QString fullDescriptionForCollapsed = row.baseDescription;
row.collapsedServerDescription = fullDescriptionForCollapsed;
row.expandedServerDescription = fullDescriptionForCollapsed;
return row;
}
} // namespace amnezia
+64
View File
@@ -0,0 +1,64 @@
#ifndef SERVERDESCRIPTION_H
#define SERVERDESCRIPTION_H
#include <QString>
#include <QJsonArray>
#include "core/utils/containerEnum.h"
#include "core/utils/selfhosted/sshSession.h"
#include "core/models/selfhosted/selfHostedAdminServerConfig.h"
#include "core/models/selfhosted/selfHostedUserServerConfig.h"
#include "core/models/selfhosted/nativeServerConfig.h"
#include "core/models/api/legacyApiServerConfig.h"
#include "core/models/api/apiV2ServerConfig.h"
namespace amnezia
{
struct ServerDescription
{
QString serverId;
QString serverName;
QString baseDescription;
QString hostName;
int configVersion = 0;
ServerCredentials selfHostedSshCredentials;
bool hasWriteAccess = false;
bool primaryDnsIsAmnezia = false;
DockerContainer defaultContainer = DockerContainer::None;
bool hasInstalledVpnContainers = false;
bool isApiV1 = false;
bool isApiV2 = false;
bool isServerFromGatewayApi = false;
bool isPremium = false;
bool isCountrySelectionAvailable = false;
QJsonArray apiAvailableCountries;
QString apiServerCountryCode;
bool isAdVisible = false;
QString adHeader;
QString adDescription;
QString adEndpoint;
bool isRenewalAvailable = false;
bool isSubscriptionExpired = false;
bool isSubscriptionExpiringSoon = false;
QString collapsedServerDescription;
QString expandedServerDescription;
};
ServerDescription buildServerDescription(const SelfHostedAdminServerConfig &server, bool isAmneziaDnsEnabled);
ServerDescription buildServerDescription(const SelfHostedUserServerConfig &server, bool isAmneziaDnsEnabled);
ServerDescription buildServerDescription(const NativeServerConfig &server, bool isAmneziaDnsEnabled);
ServerDescription buildServerDescription(const LegacyApiServerConfig &server, bool isAmneziaDnsEnabled);
ServerDescription buildServerDescription(const ApiV2ServerConfig &server, bool isAmneziaDnsEnabled);
} // namespace amnezia
#endif

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