mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-10-24 12:06:49 +09:00
Compare commits
175
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02838a94d5 | ||
|
|
9a1200d6f3 | ||
|
|
ddde43635e | ||
|
|
ffba1cb2a4 | ||
|
|
30ff3451c7 | ||
|
|
db951e9928 | ||
|
|
a36399a4c3 | ||
|
|
8940f8e273 | ||
|
|
705a373d51 | ||
|
|
3d4a45246a | ||
|
|
11355e1de0 | ||
|
|
3e8077f2ca | ||
|
|
c52b1a50a8 | ||
|
|
2c0f169946 | ||
|
|
693d00ded5 | ||
|
|
f1af8efb4b | ||
|
|
4c2e950864 | ||
|
|
9017984b1d | ||
|
|
c5ecf6281f | ||
|
|
3e9b70a5f1 | ||
|
|
94b51df247 | ||
|
|
de93650a90 | ||
|
|
448baad3e1 | ||
|
|
aca1b7dc5c | ||
|
|
de36a81b58 | ||
|
|
76b81b43c1 | ||
|
|
bea2f5f7db | ||
|
|
a4d38b5642 | ||
|
|
b76c88bba3 | ||
|
|
eab14fd957 | ||
|
|
2a8242d306 | ||
|
|
e60842bd9d | ||
|
|
327e5985df | ||
|
|
124da5a499 | ||
|
|
2ee02bc893 | ||
|
|
398697c101 | ||
|
|
c8810dde5f | ||
|
|
9a7db3475f | ||
|
|
628dd5b6d3 | ||
|
|
71b99c799c | ||
|
|
0643a2424b | ||
|
|
dedfc08cc2 | ||
|
|
dddc181299 | ||
|
|
5689706fd9 | ||
|
|
5501fbd54e | ||
|
|
fc4bf3eb2b | ||
|
|
c8722c619a | ||
|
|
4c5bb29ab0 | ||
|
|
4976828914 | ||
|
|
5d74b2cbcf | ||
|
|
7072661244 | ||
|
|
7d4f3e0f50 | ||
|
|
b1a37b3779 | ||
|
|
86ae77e3d0 | ||
|
|
541a6e07d6 | ||
|
|
f135400d3b | ||
|
|
da184c0ddf | ||
|
|
4116c5c4bc | ||
|
|
667d98f7d7 | ||
|
|
44c10b39e3 | ||
|
|
e4b7e09f86 | ||
|
|
04c2b2af33 | ||
|
|
e643fa008c | ||
|
|
b575df05c7 | ||
|
|
85bd102efa | ||
|
|
717323f138 | ||
|
|
8ed7a5a1d3 | ||
|
|
6ece4e0ac5 | ||
|
|
9d45f3d7ad | ||
|
|
dcf53b989e | ||
|
|
e27f74e4fc | ||
|
|
dbe1a06e35 | ||
|
|
d251d5f0ac | ||
|
|
d702dc0b2a | ||
|
|
2ad6721061 | ||
|
|
0d116bfc67 | ||
|
|
25a18bdf97 | ||
|
|
f73697d3f4 | ||
|
|
53121f20a3 | ||
|
|
d6cb9b930f | ||
|
|
c4b3ccacc9 | ||
|
|
e38a233904 | ||
|
|
9b0495f5e1 | ||
|
|
30ec46a567 | ||
|
|
4055e1b05e | ||
|
|
4d28f81965 | ||
|
|
5c035bb6cf | ||
|
|
26c22327af | ||
|
|
5e9def4184 | ||
|
|
a98c60d967 | ||
|
|
0faf3e6d2f | ||
|
|
a32a26bb67 | ||
|
|
431ece7fff | ||
|
|
5e82c6a558 | ||
|
|
84a2d13b68 | ||
|
|
324b09cca3 | ||
|
|
e70563a071 | ||
|
|
bc2a0ca649 | ||
|
|
3c6ac431b4 | ||
|
|
e96a2b171c | ||
|
|
06d219b92b | ||
|
|
28c77e570b | ||
|
|
6691d2e21c | ||
|
|
c7b5ee1867 | ||
|
|
e4127f47f3 | ||
|
|
4b4563818e | ||
|
|
dbb43272e6 | ||
|
|
5e70eb20c9 | ||
|
|
15176cd3ae | ||
|
|
25df36ad73 | ||
|
|
ea8e3fda56 | ||
|
|
31ae949ba0 | ||
|
|
a17847a90b | ||
|
|
2ca497cd06 | ||
|
|
bd003b149d | ||
|
|
35c1014a44 | ||
|
|
27857cde8a | ||
|
|
b39f583097 | ||
|
|
6c7b65cac6 | ||
|
|
0f6847219b | ||
|
|
5d16645b84 | ||
|
|
203a092dc9 | ||
|
|
d8b8590bc4 | ||
|
|
9b8bfaa6f8 | ||
|
|
890103a16a | ||
|
|
56ab82f87f | ||
|
|
3984acbb44 | ||
|
|
cc404378f9 | ||
|
|
594635e5cf | ||
|
|
f9b106cf5b | ||
|
|
a9861d18b7 | ||
|
|
c14138f031 | ||
|
|
60686fde24 | ||
|
|
bd0747296e | ||
|
|
ba61019a50 | ||
|
|
113f967006 | ||
|
|
bcee58b08b | ||
|
|
52de1acebf | ||
|
|
027a12a1df | ||
|
|
0a659a2d74 | ||
|
|
6f119cd083 | ||
|
|
1753aed3fc | ||
|
|
c714d98bd1 | ||
|
|
4787f3915b | ||
|
|
7a383116b2 | ||
|
|
d3de5f0f48 | ||
|
|
8749d683e3 | ||
|
|
9de9d082bc | ||
|
|
a4233fef41 | ||
|
|
4890dd1d74 | ||
|
|
564630827e | ||
|
|
fbe15d965b | ||
|
|
b29515c380 | ||
|
|
0658a8f565 | ||
|
|
482ec04b4a | ||
|
|
d40d24fcf9 | ||
|
|
fb5666057b | ||
|
|
a49892c7e7 | ||
|
|
277b295fd8 | ||
|
|
8c33779fc3 | ||
|
|
f0299ca9fe | ||
|
|
c7b1c2809f | ||
|
|
c9ed0baf3b | ||
|
|
2a3e3126ac | ||
|
|
98771027b7 | ||
|
|
0433e03bdc | ||
|
|
cb48667b91 | ||
|
|
d0a1af0381 | ||
|
|
fd0c773918 | ||
|
|
06372c8fd7 | ||
|
|
009ca981d5 | ||
|
|
c0cae0ff01 | ||
|
|
c28452a5da | ||
|
|
396ce23228 | ||
|
|
b05ee0a654 |
No files matched your search
@@ -0,0 +1,38 @@
|
||||
# .github/actions/apple-install-cert/action.yml
|
||||
|
||||
name: Setup apple keychain
|
||||
description: Creates and configures a temporary build keychain
|
||||
|
||||
inputs:
|
||||
keychain-path:
|
||||
description: Path to the keychain
|
||||
required: true
|
||||
keychain-password:
|
||||
description: Password to the keychain
|
||||
required: true
|
||||
cert-base64:
|
||||
description: Base64-encoded certificate
|
||||
required: true
|
||||
cert-password:
|
||||
description: Certificate password
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Create keychain
|
||||
shell: bash
|
||||
env:
|
||||
KEYCHAIN_PATH: ${{ inputs.keychain-path }}
|
||||
KEYCHAIN_PASSWORD: ${{ inputs.keychain-password }}
|
||||
CERT_BASE64: ${{ inputs.cert-base64 }}
|
||||
CERT_PASSWORD: ${{ inputs.cert-password }}
|
||||
run: |
|
||||
CERT_PATH=$(mktemp /tmp/cert_XXXXXX.p12)
|
||||
trap "rm -f '$CERT_PATH'" EXIT
|
||||
|
||||
echo -n "$CERT_BASE64" | base64 --decode -o "$CERT_PATH"
|
||||
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||
security import "$CERT_PATH" -k "$KEYCHAIN_PATH" -P "$CERT_PASSWORD" -A -t cert -f pkcs12
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||
Binary file not shown.
File renamed without changes.
@@ -0,0 +1,57 @@
|
||||
# .github/actions/apple-setup-keychain/action.yml
|
||||
|
||||
name: Setup apple keychain
|
||||
description: Creates and configures a temporary build keychain
|
||||
|
||||
inputs:
|
||||
keychain-name:
|
||||
description: Name of the keychain
|
||||
required: false
|
||||
default: "ci-amnezia"
|
||||
keychain-password:
|
||||
description: The keychain password
|
||||
required: true
|
||||
lock-timeout:
|
||||
description: A timeout after exceeding which the keychain would be locked
|
||||
required: false
|
||||
default: "0"
|
||||
|
||||
outputs:
|
||||
keychain-path:
|
||||
description: "Full path to the keychain created"
|
||||
value: ${{ steps.setup.outputs.keychain-path }}
|
||||
keychain-name:
|
||||
description: "Actual name of the keychain created"
|
||||
value: ${{ steps.setup.outputs.keychain-name }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Setup keychain
|
||||
id: setup
|
||||
shell: bash
|
||||
env:
|
||||
KEYCHAIN_NAME: ${{ inputs.keychain-name }}
|
||||
KEYCHAIN_PASSWORD: ${{ inputs.keychain-password }}
|
||||
LOCK_TIMEOUT: ${{ inputs.lock-timeout }}
|
||||
run: |
|
||||
KEYCHAIN_PATH="$HOME/Library/Keychains/$KEYCHAIN_NAME.keychain-db"
|
||||
|
||||
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||
|
||||
if [[ "$LOCK_TIMEOUT" == "0" ]]; then
|
||||
security set-keychain-settings "$KEYCHAIN_PATH"
|
||||
else
|
||||
security set-keychain-settings -u -t "$LOCK_TIMEOUT" "$KEYCHAIN_PATH"
|
||||
fi
|
||||
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||
|
||||
security import "${{ github.action_path }}/DeveloperIDG2CA.cer" -k "$KEYCHAIN_PATH" -A
|
||||
security import "${{ github.action_path }}/AppleWWDRCAG3.cer" -k "$KEYCHAIN_PATH" -A
|
||||
|
||||
security list-keychains -d user -s "$KEYCHAIN_PATH"
|
||||
security default-keychain -s "$KEYCHAIN_PATH"
|
||||
|
||||
echo "keychain-name=$KEYCHAIN_NAME" >> $GITHUB_OUTPUT
|
||||
echo "keychain-path=$KEYCHAIN_PATH" >> $GITHUB_OUTPUT
|
||||
@@ -0,0 +1,31 @@
|
||||
# .github/actions/apple-setup-provisioning-profile/action.yml
|
||||
|
||||
name: Setup provisioning profiles
|
||||
description: Decodes and installs provisioning profiles
|
||||
|
||||
inputs:
|
||||
provisioning_profile_base64:
|
||||
description: Base64-encoded provisioning profile
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Setup provisioning profile
|
||||
shell: bash
|
||||
run: |
|
||||
PROFILES_DIR="$HOME/Library/MobileDevice/Provisioning Profiles"
|
||||
TEMP_FILE=$(mktemp)
|
||||
|
||||
echo "${{ inputs.provisioning_profile_base64 }}" | base64 --decode > "$TEMP_FILE"
|
||||
|
||||
PROFILE_UUID=$(grep UUID -A1 -a "$TEMP_FILE" | grep -io "[-A-F0-9]\{36\}")
|
||||
if [[ -z "$PROFILE_UUID" ]]; then
|
||||
echo "Failed to extract UUID from provisioning profile"
|
||||
rm -f "$TEMP_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$PROFILES_DIR"
|
||||
mv "$TEMP_FILE" "$PROFILES_DIR/$PROFILE_UUID.mobileprovision"
|
||||
echo "Installed profile: $PROFILE_UUID"
|
||||
+1185
-317
File diff suppressed because it is too large.
Load diff
@@ -10,6 +10,8 @@ deploy/build_64/*
|
||||
winbuild*.bat
|
||||
.cache/
|
||||
.vscode/
|
||||
.venv/
|
||||
.cursor*
|
||||
|
||||
|
||||
# Qt-es
|
||||
@@ -81,6 +83,7 @@ client/.DS_Store
|
||||
._.DS_Store
|
||||
._*
|
||||
*.dmg
|
||||
deploy/data/macos/pf/amn.400.allowPIA.conf
|
||||
|
||||
# tmp files
|
||||
*.*~
|
||||
@@ -143,3 +146,4 @@ macos-with-sign-build.sh
|
||||
DeveloperIdApplicationCertificate.p12
|
||||
DeveloperIdInstallerCertificate.p12
|
||||
|
||||
gwenv.sh
|
||||
@@ -4,16 +4,9 @@
|
||||
[submodule "client/3rd/SortFilterProxyModel"]
|
||||
path = client/3rd/SortFilterProxyModel
|
||||
url = https://github.com/mitchcurtis/SortFilterProxyModel.git
|
||||
[submodule "client/3rd-prebuilt"]
|
||||
path = client/3rd-prebuilt
|
||||
url = https://github.com/amnezia-vpn/3rd-prebuilt
|
||||
branch = feature/special-handshake
|
||||
[submodule "client/3rd/amneziawg-apple"]
|
||||
path = client/3rd/amneziawg-apple
|
||||
url = https://github.com/amnezia-vpn/amneziawg-apple
|
||||
[submodule "client/3rd/QSimpleCrypto"]
|
||||
path = client/3rd/QSimpleCrypto
|
||||
url = https://github.com/amnezia-vpn/QSimpleCrypto.git
|
||||
[submodule "client/3rd/qtgamepad"]
|
||||
path = client/3rd/qtgamepad
|
||||
url = https://github.com/amnezia-vpn/qtgamepad.git
|
||||
|
||||
+46
-46
@@ -1,18 +1,43 @@
|
||||
cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
set(PROJECT AmneziaVPN)
|
||||
set(AMNEZIAVPN_VERSION 4.8.15.4)
|
||||
if(NOT AMNEZIAVPN_VERSION)
|
||||
set(AMNEZIAVPN_VERSION 5.1.0.0 CACHE STRING "Client app version")
|
||||
endif()
|
||||
|
||||
set(QT_CREATOR_SKIP_PACKAGE_MANAGER_SETUP ON CACHE BOOL "" FORCE)
|
||||
set(CMAKE_PROJECT_TOP_LEVEL_INCLUDES
|
||||
${CMAKE_SOURCE_DIR}/cmake/platform_settings.cmake
|
||||
${CMAKE_SOURCE_DIR}/cmake/recipes_bootstrap.cmake
|
||||
${CMAKE_SOURCE_DIR}/cmake/conan_provider.cmake
|
||||
CACHE STRING "" FORCE)
|
||||
|
||||
project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
|
||||
DESCRIPTION "AmneziaVPN"
|
||||
HOMEPAGE_URL "https://amnezia.org/"
|
||||
)
|
||||
|
||||
option(AMNEZIA_BUILD_TESTS "Build test executables" OFF)
|
||||
|
||||
# trigger conan to kick off `conan install` globally
|
||||
find_package(OpenSSL REQUIRED)
|
||||
if (PREBUILTS_ONLY)
|
||||
return()
|
||||
endif()
|
||||
|
||||
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)
|
||||
|
||||
# bump by 2 on every release, because we're releasing two versions on the play store
|
||||
set(APP_ANDROID_VERSION_CODE 2173)
|
||||
if(DEFINED APP_ANDROID_VERSION_CODE_OFFSET)
|
||||
math(EXPR APP_ANDROID_VERSION_CODE "${APP_ANDROID_VERSION_CODE} + ${APP_ANDROID_VERSION_CODE_OFFSET}")
|
||||
endif()
|
||||
|
||||
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
||||
set(MZ_PLATFORM_NAME "linux")
|
||||
@@ -29,59 +54,34 @@ elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Emscripten")
|
||||
endif()
|
||||
|
||||
set(QT_BUILD_TOOLS_WHEN_CROSS_COMPILING ON)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
if(APPLE)
|
||||
if(IOS)
|
||||
set(CMAKE_OSX_ARCHITECTURES "arm64")
|
||||
elseif(MACOS_NE)
|
||||
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64")
|
||||
if(APPLE AND NOT IOS)
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
set(AMN_PF_RULE_IDENTITY "user { root }")
|
||||
else()
|
||||
set(CMAKE_OSX_ARCHITECTURES "x86_64")
|
||||
set(AMN_PF_RULE_IDENTITY "group { amnvpn }")
|
||||
endif()
|
||||
|
||||
configure_file(
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/deploy/data/pf-templates/amn.400.allowPIA.conf.in"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/amn.400.allowPIA.conf"
|
||||
@ONLY
|
||||
)
|
||||
|
||||
file(COPY_FILE
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/amn.400.allowPIA.conf"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/deploy/data/macos/pf/amn.400.allowPIA.conf"
|
||||
ONLY_IF_DIFFERENT
|
||||
)
|
||||
endif()
|
||||
|
||||
|
||||
add_subdirectory(client)
|
||||
|
||||
if(NOT IOS AND NOT ANDROID AND NOT MACOS_NE)
|
||||
add_subdirectory(service)
|
||||
|
||||
include(${CMAKE_SOURCE_DIR}/deploy/installer/config.cmake)
|
||||
endif()
|
||||
|
||||
set(AMNEZIA_STAGE_DIR "${CMAKE_BINARY_DIR}/stage")
|
||||
|
||||
if(WIN32 AND NOT IOS AND NOT ANDROID AND NOT MACOS_NE)
|
||||
file(TO_CMAKE_PATH "${AMNEZIA_STAGE_DIR}" AMNEZIA_STAGE_DIR_CMAKE)
|
||||
|
||||
set(CPACK_GENERATOR "WIX")
|
||||
set(CPACK_WIX_VERSION 4)
|
||||
set(CPACK_PACKAGE_NAME "AmneziaVPN")
|
||||
set(CPACK_PACKAGE_VENDOR "AmneziaVPN")
|
||||
set(CPACK_PACKAGE_VERSION ${AMNEZIAVPN_VERSION})
|
||||
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "AmneziaVPN client")
|
||||
set(AMNEZIA_LICENSE_TXT "${CMAKE_BINARY_DIR}/LICENSE.txt")
|
||||
configure_file("${CMAKE_SOURCE_DIR}/LICENSE" "${AMNEZIA_LICENSE_TXT}" COPYONLY)
|
||||
set(CPACK_RESOURCE_FILE_LICENSE "${AMNEZIA_LICENSE_TXT}")
|
||||
set(CPACK_PACKAGE_INSTALL_DIRECTORY "AmneziaVPN")
|
||||
set(CPACK_PACKAGE_DIRECTORY "${CMAKE_BINARY_DIR}")
|
||||
set(CPACK_PACKAGE_EXECUTABLES "AmneziaVPN" "AmneziaVPN")
|
||||
set(CPACK_WIX_UPGRADE_GUID "{2D55AC62-96D6-4692-8C05-0D85BBF95485}")
|
||||
set(CPACK_WIX_PRODUCT_ICON "${CMAKE_SOURCE_DIR}/client/images/app.ico")
|
||||
|
||||
# WiX patches
|
||||
set(_AMNEZIA_WIX_PATCH_SERVICE "${CMAKE_SOURCE_DIR}/deploy/installer/wix/service_install_patch.xml")
|
||||
set(_AMNEZIA_WIX_PATCH_CLOSE_APP "${CMAKE_SOURCE_DIR}/deploy/installer/wix/close_client_patch.xml")
|
||||
file(TO_CMAKE_PATH "${_AMNEZIA_WIX_PATCH_SERVICE}" _AMNEZIA_WIX_PATCH_SERVICE_CMAKE)
|
||||
file(TO_CMAKE_PATH "${_AMNEZIA_WIX_PATCH_CLOSE_APP}" _AMNEZIA_WIX_PATCH_CLOSE_APP_CMAKE)
|
||||
set(CPACK_WIX_PATCH_FILE "${_AMNEZIA_WIX_PATCH_SERVICE_CMAKE};${_AMNEZIA_WIX_PATCH_CLOSE_APP_CMAKE}")
|
||||
|
||||
# WiX v4 Util extension for CloseApplication + namespace for util
|
||||
set(CPACK_WIX_EXTENSIONS "${CPACK_WIX_EXTENSIONS};WixToolset.Util.wixext")
|
||||
set(CPACK_WIX_CUSTOM_XMLNS "util=http://wixtoolset.org/schemas/v4/wxs/util")
|
||||
|
||||
set(CPACK_INSTALLED_DIRECTORIES "${AMNEZIA_STAGE_DIR_CMAKE};/")
|
||||
|
||||
include(CPack)
|
||||
if ((LINUX AND NOT ANDROID) OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (WIN32))
|
||||
include(${CMAKE_SOURCE_DIR}/cmake/CPack.cmake)
|
||||
endif()
|
||||
@@ -38,14 +38,14 @@
|
||||
|
||||
## Links
|
||||
|
||||
- [https://amnezia.org](https://amnezia.org) - Project website | [Alternative link (mirror)](https://storage.googleapis.com/kldscp/amnezia.org)
|
||||
- [https://docs.amnezia.org](https://docs.amnezia.org) - Documentation
|
||||
- [https://amnezia.org](https://amnezia.org/?utm_source=github&utm_campaign=amnezia_website-read) - Project website | [Alternative link (mirror)](https://storage.googleapis.com/amnezia/amnezia.org?utm_source=github&utm_campaign=amnezia_website-read)
|
||||
- [https://docs.amnezia.org](https://docs.amnezia.org/?utm_source=github&utm_campaign=amnezia_website-read) - Documentation | [Alternative link (mirror)](https://storage.googleapis.com/amnezia/docs?utm_source=github&utm_campaign=amnezia_website-read)
|
||||
- [https://www.reddit.com/r/AmneziaVPN](https://www.reddit.com/r/AmneziaVPN) - Reddit
|
||||
- [https://t.me/amnezia_vpn_en](https://t.me/amnezia_vpn_en) - Telegram support channel (English)
|
||||
- [https://t.me/amnezia_vpn_ir](https://t.me/amnezia_vpn_ir) - Telegram support channel (Farsi)
|
||||
- [https://t.me/amnezia_vpn_mm](https://t.me/amnezia_vpn_mm) - Telegram support channel (Myanmar)
|
||||
- [https://t.me/amnezia_vpn](https://t.me/amnezia_vpn) - Telegram support channel (Russian)
|
||||
- [https://vpnpay.io/en/amnezia-premium/](https://vpnpay.io/en/amnezia-premium/) - Amnezia Premium
|
||||
- [https://telegram.me/amnezia_vpn_en](https://telegram.me/amnezia_vpn_en) - Telegram support channel (English)
|
||||
- [https://telegram.me/amnezia_vpn_ir](https://telegram.me/amnezia_vpn_ir) - Telegram support channel (Farsi)
|
||||
- [https://telegram.me/amnezia_vpn_mm](https://telegram.me/amnezia_vpn_mm) - Telegram support channel (Myanmar)
|
||||
- [https://telegram.me/amnezia_vpn](https://telegram.me/amnezia_vpn) - Telegram support channel (Russian)
|
||||
- [Get Premium for 6 or 12 months](https://storage.googleapis.com/amnezia/pay?utm_source=github&utm_campaign=ampay-read)
|
||||
|
||||
## Tech
|
||||
|
||||
@@ -53,24 +53,14 @@ AmneziaVPN uses several open-source projects to work:
|
||||
|
||||
- [OpenSSL](https://www.openssl.org/)
|
||||
- [OpenVPN](https://openvpn.net/)
|
||||
- [Shadowsocks](https://shadowsocks.org/)
|
||||
- [Qt](https://www.qt.io/)
|
||||
- [LibSsh](https://libssh.org) - forked from Qt Creator
|
||||
- [LibSsh](https://libssh.org)
|
||||
- [WireGuard](https://www.wireguard.com/)
|
||||
- [Xray-core](https://xtls.github.io/en/)
|
||||
- [Conan](https://conan.io/)
|
||||
- and more...
|
||||
|
||||
## Checking out the source code
|
||||
|
||||
Make sure to pull all submodules after checking out the repo.
|
||||
|
||||
```bash
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
Want to contribute? Welcome!
|
||||
|
||||
### Help with translations
|
||||
## Help us with translations
|
||||
|
||||
Download the most actual translation files.
|
||||
|
||||
@@ -83,99 +73,98 @@ Each *.ts file contains strings for one corresponding language.
|
||||
Translate or correct some strings in one or multiple *.ts files and commit them back to this repository into the ``client/translations`` folder.
|
||||
You can do it via a web-interface or any other method you're familiar with.
|
||||
|
||||
### Building sources and deployment
|
||||
## Checking out the source code
|
||||
|
||||
Check deploy folder for build scripts.
|
||||
Make sure to pull all submodules after checking out the repo.
|
||||
|
||||
### How to build an iOS app from source code on MacOS
|
||||
|
||||
1. First, make sure you have [XCode](https://developer.apple.com/xcode/) installed, at least version 14 or higher.
|
||||
|
||||
2. We use QT to generate the XCode project. We need QT version 6.6.2. Install QT for MacOS [here](https://doc.qt.io/qt-6/macos.html) or [QT Online Installer](https://www.qt.io/download-open-source). Required modules:
|
||||
- MacOS
|
||||
- iOS
|
||||
- Qt 5 Compatibility Module
|
||||
- Qt Shader Tools
|
||||
- Additional Libraries:
|
||||
- Qt Image Formats
|
||||
- Qt Multimedia
|
||||
- Qt Remote Objects
|
||||
|
||||
3. Install CMake if required. We recommend CMake version 3.25. You can install CMake [here](https://cmake.org/download/)
|
||||
|
||||
4. You also need to install go >= v1.16. If you don't have it installed already,
|
||||
download go from the [official website](https://golang.org/dl/) or use Homebrew.
|
||||
The latest version is recommended. Install gomobile
|
||||
```bash
|
||||
export PATH=$PATH:~/go/bin
|
||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||
gomobile init
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
5. Build the project
|
||||
## Hacking guide
|
||||
|
||||
Want to contribute? Welcome!
|
||||
|
||||
### Build requirements
|
||||
|
||||
* [`CMake`](https://cmake.org/download/)
|
||||
* Compiler and underlying build system, depending on the target:
|
||||
- [Linux] Any of `make` and `gcc`
|
||||
- [Apple] [`Xcode`](https://developer.apple.com/xcode/) or [`Xcode command line tools`](https://developer.apple.com/xcode/)
|
||||
- [Windows] [`Visual Studio 2022`](https://aka.ms/vs/17/release/vs_community.exe) or [`VS 2022 Build Tools`](https://aka.ms/vs/17/release/vs_buildtools.exe)
|
||||
- [Android] [`Android SDK`](#installing-android-sdk) and [`Ninja`](https://ninja-build.org/)
|
||||
* [`Qt 6.10+`](https://www.qt.io/download-open-source) with the following modules:
|
||||
- Core module for targeting platform (Desktop/Android/iOS)
|
||||
- Qt 5 Compatibility module
|
||||
- Qt Remote Objects
|
||||
* [`Conan`](https://conan.io/downloads) package manager
|
||||
- On MacOS is enough just to use `homebrew` or install it in `.venv` in project root
|
||||
- Other systems must have it in `PATH`
|
||||
* (Optional) Installer dependencies:
|
||||
- [Windows/Linux] [`Qt Installer Framework`](https://www.qt.io/download-open-source)
|
||||
- [Windows] [`WIX toolset`](https://github.com/wixtoolset/wix/releases)
|
||||
|
||||
### Building the project using scripts
|
||||
|
||||
* Run scripts located in `deploy` directory
|
||||
* Basically, if dependencies are located in default installation paths, the scripts will find them automatically.
|
||||
* If they differ, specify them using the following variables:
|
||||
- `QT_INSTALL_DIR` - Qt root installation folder
|
||||
- `QT_ROOT_PATH` - Qt framework root directory
|
||||
- `QIF_ROOT_PATH` - Qt Installer Framework root path
|
||||
- `ANDROID_HOME` - Path to Android SDK root folder
|
||||
- and others. Check scripts for more
|
||||
|
||||
Unix-like:
|
||||
```bash
|
||||
export QT_BIN_DIR="<PATH-TO-QT-FOLDER>/Qt/<QT-VERSION>/ios/bin"
|
||||
export QT_MACOS_ROOT_DIR="<PATH-TO-QT-FOLDER>/Qt/<QT-VERSION>/macos"
|
||||
export QT_IOS_BIN=$QT_BIN_DIR
|
||||
export PATH=$PATH:~/go/bin
|
||||
mkdir build-ios
|
||||
$QT_IOS_BIN/qt-cmake . -B build-ios -GXcode -DQT_HOST_PATH=$QT_MACOS_ROOT_DIR
|
||||
```
|
||||
Replace PATH-TO-QT-FOLDER and QT-VERSION to your environment
|
||||
# Build executables for the host platform
|
||||
deploy/build.sh
|
||||
|
||||
# Or just
|
||||
deploy/build.sh
|
||||
|
||||
If you get `gomobile: command not found` make sure to set PATH to the location
|
||||
of the bin folder where gomobile was installed. Usually, it's in `GOPATH`.
|
||||
```bash
|
||||
export PATH=$(PATH):/path/to/GOPATH/bin
|
||||
# Build executables and installers for the host platform
|
||||
deploy/build.sh --installer all
|
||||
|
||||
# Build Android APK and AAB
|
||||
deploy/build.sh -t android --aab
|
||||
|
||||
# Call for help
|
||||
deploy/build.sh -h
|
||||
```
|
||||
|
||||
6. Open the XCode project. You can then run /test/archive/ship the app.
|
||||
Windows:
|
||||
```batch
|
||||
:: Build executables for Windows
|
||||
deploy/build.bat
|
||||
|
||||
If the build fails with the following error
|
||||
```
|
||||
make: ***
|
||||
[$(PROJECTDIR)/client/build/AmneziaVPN.build/Debug-iphoneos/wireguard-go-bridge/goroot/.prepared]
|
||||
Error 1
|
||||
```
|
||||
Add a user-defined variable to both AmneziaVPN and WireGuardNetworkExtension targets' build settings with
|
||||
key `PATH` and value `${PATH}/path/to/bin/folder/with/go/executable`, e.g. `${PATH}:/usr/local/go/bin`.
|
||||
:: Build executables with IFW installer for Windows
|
||||
deploy/build.bat --installer ifw
|
||||
|
||||
if the above error persists on your M1 Mac, then most probably you need to install arch based CMake
|
||||
```
|
||||
arch -arm64 brew install cmake
|
||||
:: Build executables with IFW and WIX installer for Windows
|
||||
deploy/build.bat --installer ifw --installer wix
|
||||
|
||||
:: Or just
|
||||
deploy/build.bat --installer all
|
||||
```
|
||||
|
||||
Build might fail with the "source files not found" error the first time you try it, because the modern XCode build system compiles dependencies in parallel, and some dependencies end up being built after the ones that
|
||||
require them. In this case, simply restart the build.
|
||||
### Developing the project in IDEs
|
||||
|
||||
## How to build the Android app
|
||||
* Basically, you can use any IDE that handles CMake and Qt kits properly to run configure and build steps, and to navigate through the code nicely. For example:
|
||||
- `Qt Creator`
|
||||
- `Visual Studio Code` with `Qt Extension Pack`
|
||||
- and so on
|
||||
|
||||
_Tested on Mac OS_
|
||||
* To use `Xcode`, you have to configure project first by using `cmake`. The easiest way to do it is to use `Qt Creator` for configuration. Then open `AmneziaVPN.xcodeproj` file from the build folder by using `Xcode`. Note that none of the files changed are saved - the files actually getting changed in build directory. Copy them manually if necessary
|
||||
|
||||
The Android app has the following requirements:
|
||||
* JDK 11
|
||||
* Android platform SDK 33
|
||||
* CMake 3.25.0
|
||||
* `Android studio` could be used in the same way - just configure the project by using `cmake` manually or by using `Qt Creator`. Open `<build-dir>/client/android-build` in `Android studio` then. Do not forget to copy the changes - everything you do is saved under the build directory actually.
|
||||
|
||||
After you have installed QT, QT Creator, and Android Studio, you need to configure QT Creator correctly.
|
||||
### Installing Android SDK
|
||||
|
||||
- Click in the top menu bar on `QT Creator` -> `Preferences` -> `Devices` and select the tab `Android`.
|
||||
- Set path to JDK 11
|
||||
- Set path to Android SDK (`$ANDROID_HOME`)
|
||||
|
||||
In case you get errors regarding missing SDK or 'SDK manager not running', you cannot fix them by correcting the paths. If you have some spare GBs on your disk, you can let QT Creator install all requirements by choosing an empty folder for `Android SDK location` and clicking on `Set Up SDK`. Be aware: This will install a second Android SDK and NDK on your machine!
|
||||
Double-check that the right CMake version is configured: Click on `QT Creator` -> `Preferences` and click on the side menu on `Kits`. Under the center content view's `Kits` tab, you'll find an entry for `CMake Tool`. If the default selected CMake version is lower than 3.25.0, install on your system CMake >= 3.25.0 and choose `System CMake at <path>` from the drop-down list. If this entry is missing, you either have not installed CMake yet or QT Creator hasn't found the path to it. In that case, click in the preferences window on the side menu item `CMake`, then on the tab `Tools` in the center content view, and finally on the button `Add` to set the path to your installed CMake.
|
||||
Please make sure that you have selected Android Platform SDK 33 for your project: click in the main view's side menu on `Projects`, and on the left, you'll see a section `Build & Run` showing different Android build targets. You can select any of them, Amnezia VPN's project setup is designed in a way that all Android targets will be built. Click on the targets submenu item `Build` and scroll in the center content view to `Build Steps`. Click on `Details` at the end of the headline `Build Android APK` (the `Details` button might be hidden in case the QT Creator Window is not running in full screen!). Here we are: Choose `android-33` as `Android Build Platform SDK`.
|
||||
|
||||
That's it! You should be ready to compile the project from QT Creator!
|
||||
|
||||
### Development flow
|
||||
|
||||
After you've hit the build button, QT-Creator copies the whole project to a folder in the repository parent directory. The folder should look something like `build-amnezia-client-Android_Qt_<version>_Clang_<architecture>-<BuildType>`.
|
||||
If you want to develop Amnezia VPNs Android components written in Kotlin, such as components using system APIs, you need to import the generated project in Android Studio with `build-amnezia-client-Android_Qt_<version>_Clang_<architecture>-<BuildType>/client/android-build` as the projects root directory. While you should be able to compile the generated project from Android Studio, you cannot work directly in the repository's Android project. So whenever you are confident with your work in the generated project, you'll need to copy and paste the affected files to the corresponding path in the repository's Android project so that you can add and commit your changes!
|
||||
|
||||
You may face compiling issues in QT Creator after you've worked in Android Studio on the generated project. Just do a `./gradlew clean` in the generated project's root directory (`<path>/client/android-build/.`) and you should be good to go.
|
||||
* Android SDK could be installed using the following methods:
|
||||
- Using `Qt Creator`. Use `Preferences`->`SDKs`
|
||||
- Using `Android studio`. By default it installs necessary `SDKs` automatically during the installation
|
||||
- Manually by using `sdk-manager`. Check [this](https://developer.android.com/tools) page for details
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+87
-88
@@ -28,21 +28,21 @@
|
||||
|
||||
- Простой в использовании — введите IP-адрес, SSH-логин и пароль, и Amnezia автоматически установит VPN-контейнеры Docker на ваш сервер и подключится к VPN.
|
||||
- Классические VPN-протоколы: OpenVPN, WireGuard и IKEv2.
|
||||
- Протоколы с маскировкой трафика (обфускацией): OpenVPN с плагином [Cloak](https://github.com/cbeuw/Cloak), Shadowsocks (OpenVPN over Shadowsocks), [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) and XRay.
|
||||
- Протоколы с маскировкой трафика (обфускацией): OpenVPN с плагином [Cloak](https://github.com/cbeuw/Cloak), Shadowsocks (OpenVPN over Shadowsocks), [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) и XRay.
|
||||
- Поддержка Split Tunneling — добавляйте любые сайты или приложения в список, чтобы включить VPN только для них.
|
||||
- Поддерживает платформы: Windows, macOS, Linux, Android, iOS.
|
||||
- Поддержка конфигурации протокола AmneziaWG на [бета-прошивке Keenetic](https://docs.keenetic.com/ua/air/kn-1611/en/6319-latest-development-release.html#UUID-186c4108-5afd-c10b-f38a-cdff6c17fab3_section-idm33192196168192-improved).
|
||||
|
||||
## Ссылки
|
||||
|
||||
- [https://amnezia.org](https://amnezia.org) - Веб-сайт проекта | [Альтернативная ссылка (зеркало)](https://storage.googleapis.com/kldscp/amnezia.org)
|
||||
- [https://docs.amnezia.org](https://docs.amnezia.org) - Документация
|
||||
- [https://amnezia.org](https://amnezia.org/?utm_source=github&utm_campaign=amnezia_website-read) - Веб-сайт проекта | [Альтернативная ссылка (зеркало)](https://storage.googleapis.com/amnezia/amnezia.org?utm_source=github&utm_campaign=amnezia_website-read)
|
||||
- [https://docs.amnezia.org](https://docs.amnezia.org/?utm_source=github&utm_campaign=amnezia_website-read) - Документация | [Альтернативная ссылка (зеркало)](https://storage.googleapis.com/amnezia/docs?utm_source=github&utm_campaign=amnezia_website-read)
|
||||
- [https://www.reddit.com/r/AmneziaVPN](https://www.reddit.com/r/AmneziaVPN) - Reddit
|
||||
- [https://t.me/amnezia_vpn_en](https://t.me/amnezia_vpn_en) - Канал поддержки в Telegram (Английский)
|
||||
- [https://t.me/amnezia_vpn_ir](https://t.me/amnezia_vpn_ir) - Канал поддержки в Telegram (Фарси)
|
||||
- [https://t.me/amnezia_vpn_mm](https://t.me/amnezia_vpn_mm) - Канал поддержки в Telegram (Мьянма)
|
||||
- [https://t.me/amnezia_vpn](https://t.me/amnezia_vpn) - Канал поддержки в Telegram (Русский)
|
||||
- [https://vpnpay.io/en/amnezia-premium/](https://vpnpay.io/en/amnezia-premium/) - Amnezia Premium | [Зеркало](https://storage.googleapis.com/kldscp/vpnpay.io/ru/amnezia-premium\)
|
||||
- [https://telegram.me/amnezia_vpn_en](https://telegram.me/amnezia_vpn_en) - Канал поддержки в Telegram (Английский)
|
||||
- [https://telegram.me/amnezia_vpn_ir](https://telegram.me/amnezia_vpn_ir) - Канал поддержки в Telegram (Фарси)
|
||||
- [https://telegram.me/amnezia_vpn_mm](https://telegram.me/amnezia_vpn_mm) - Канал поддержки в Telegram (Мьянма)
|
||||
- [https://telegram.me/amnezia_vpn](https://telegram.me/amnezia_vpn) - Канал поддержки в Telegram (Русский)
|
||||
- [Оформите Premium на 6 или 12 месяцев](https://storage.googleapis.com/amnezia/pay?utm_source=github&utm_campaign=ampay-read)
|
||||
|
||||
## Технологии
|
||||
|
||||
@@ -50,23 +50,14 @@ AmneziaVPN использует несколько проектов с откр
|
||||
|
||||
- [OpenSSL](https://www.openssl.org/)
|
||||
- [OpenVPN](https://openvpn.net/)
|
||||
- [Shadowsocks](https://shadowsocks.org/)
|
||||
- [Qt](https://www.qt.io/)
|
||||
- [LibSsh](https://libssh.org)
|
||||
- [WireGuard](https://www.wireguard.com/)
|
||||
- [Xray-core](https://xtls.github.io/en/)
|
||||
- [Conan](https://conan.io/)
|
||||
- и другие...
|
||||
|
||||
## Проверка исходного кода
|
||||
После клонирования репозитория обязательно загрузите все подмодули.
|
||||
|
||||
```bash
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
|
||||
## Разработка
|
||||
Хотите внести свой вклад? Добро пожаловать!
|
||||
|
||||
### Помощь с переводами
|
||||
## Помощь с переводами
|
||||
|
||||
Загрузите самые актуальные файлы перевода.
|
||||
|
||||
@@ -76,90 +67,98 @@ git submodule update --init --recursive
|
||||
|
||||
Переведите или исправьте строки в одном или нескольких файлах *.ts и загрузите их обратно в этот репозиторий в папку ``client/translations``. Это можно сделать через веб-интерфейс или любым другим знакомым вам способом.
|
||||
|
||||
### Сборка исходного кода и деплой
|
||||
Проверьте папку deploy для скриптов сборки.
|
||||
## Проверка исходного кода
|
||||
|
||||
### Как собрать iOS-приложение из исходного кода на MacOS
|
||||
1. Убедитесь, что у вас установлен Xcode версии 14 или выше.
|
||||
2. Для генерации проекта Xcode используется QT. Требуется версия QT 6.6.2. Установите QT для MacOS здесь или через QT Online Installer. Необходимые модули:
|
||||
- MacOS
|
||||
- iOS
|
||||
- Модуль совместимости с Qt 5
|
||||
- Qt Shader Tools
|
||||
- Дополнительные библиотеки:
|
||||
- Qt Image Formats
|
||||
- Qt Multimedia
|
||||
- Qt Remote Objects
|
||||
|
||||
|
||||
3. Установите CMake, если это необходимо. Рекомендуемая версия — 3.25. Скачать CMake можно здесь.
|
||||
4. Установите Go версии >= v1.16. Если Go ещё не установлен, скачайте его с [официального сайта](https://golang.org/dl/) или используйте Homebrew. Установите gomobile:
|
||||
После клонирования репозитория обязательно загрузите все подмодули.
|
||||
|
||||
```bash
|
||||
export PATH=$PATH:~/go/bin
|
||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||
gomobile init
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
5. Соберите проект:
|
||||
## Руководство по разработке
|
||||
|
||||
Хотите внести свой вклад? Добро пожаловать!
|
||||
|
||||
### Требования для сборки
|
||||
|
||||
* [`CMake`](https://cmake.org/download/)
|
||||
* Компилятор и система сборки, в зависимости от таргета:
|
||||
- [Linux] Любые `make` и `gcc`
|
||||
- [Apple] [`Xcode`](https://developer.apple.com/xcode/) или [`Xcode command line tools`](https://developer.apple.com/xcode/)
|
||||
- [Windows] [`Visual Studio 2022`](https://aka.ms/vs/17/release/vs_community.exe) или [`VS 2022 Build Tools`](https://aka.ms/vs/17/release/vs_buildtools.exe)
|
||||
- [Android] [`Android SDK`](#установка-android-sdk) и [`Ninja`](https://ninja-build.org/)
|
||||
* [`Qt 6.10+`](https://www.qt.io/download-open-source) со следующими модулями:
|
||||
- Основные модули для таргета (Desktop/Android/iOS)
|
||||
- Qt 5 Compatibility module
|
||||
- Qt Remote Objects
|
||||
* Пакетный менеджер [`Conan`](https://conan.io/downloads)
|
||||
- На MacOS достаточно использовать `homebrew` или установить в `.venv` в корень проекта
|
||||
- Для остальных систем необходимо прописать пути в `PATH`
|
||||
* (Необязательно) Заивисимости для установщиков:
|
||||
- [Windows/Linux] [`Qt Installer Framework`](https://www.qt.io/download-open-source)
|
||||
- [Windows] [`WIX toolset`](https://github.com/wixtoolset/wix/releases)
|
||||
|
||||
### Сборка проекта через скрипты
|
||||
|
||||
* Запустите скрипты, находящиеся в папке `deploy`
|
||||
* Если все зависимости установлены в стандартных локациях, скрипт найдёт их самостоятельно
|
||||
* Если пути отличаются, их нужно явно указать используя:
|
||||
- `QT_INSTALL_DIR` - корневая папка установки Qt
|
||||
- `QT_ROOT_PATH` - корневая папка Qt Framework
|
||||
- `QIF_ROOT_PATH` - корневая папка Qt Installer Framework
|
||||
- `ANDROID_HOME` - путь к Android SDK
|
||||
- и другие. Их можно получить из вышеуказанных скриптов
|
||||
|
||||
Unix-like:
|
||||
```bash
|
||||
export QT_BIN_DIR="<PATH-TO-QT-FOLDER>/Qt/<QT-VERSION>/ios/bin"
|
||||
export QT_MACOS_ROOT_DIR="<PATH-TO-QT-FOLDER>/Qt/<QT-VERSION>/macos"
|
||||
export QT_IOS_BIN=$QT_BIN_DIR
|
||||
export PATH=$PATH:~/go/bin
|
||||
mkdir build-ios
|
||||
$QT_IOS_BIN/qt-cmake . -B build-ios -GXcode -DQT_HOST_PATH=$QT_MACOS_ROOT_DIR
|
||||
```
|
||||
Замените <PATH-TO-QT-FOLDER> и <QT-VERSION> на ваши значения.
|
||||
# Build executables for the host platform
|
||||
deploy/build.sh
|
||||
|
||||
Если появляется ошибка gomobile: command not found, убедитесь, что PATH настроен на папку bin, где установлен gomobile:
|
||||
```bash
|
||||
export PATH=$(PATH):/path/to/GOPATH/bin
|
||||
# Or just
|
||||
deploy/build.sh
|
||||
|
||||
# Build executables and installers for the host platform
|
||||
deploy/build.sh --installer all
|
||||
|
||||
# Build Android APK and AAB
|
||||
deploy/build.sh -t android --aab
|
||||
|
||||
# Call for help
|
||||
deploy/build.sh -h
|
||||
```
|
||||
|
||||
6. Откройте проект в Xcode. Теперь вы можете тестировать, архивировать или публиковать приложение.
|
||||
|
||||
Если сборка завершится с ошибкой:
|
||||
```
|
||||
make: ***
|
||||
[$(PROJECTDIR)/client/build/AmneziaVPN.build/Debug-iphoneos/wireguard-go-bridge/goroot/.prepared]
|
||||
Error 1
|
||||
```
|
||||
Добавьте пользовательскую переменную PATH в настройки сборки для целей AmneziaVPN и WireGuardNetworkExtension с ключом `PATH` и значением `${PATH}/path/to/bin/folder/with/go/executable`, e.g. `${PATH}:/usr/local/go/bin`.
|
||||
Windows:
|
||||
```batch
|
||||
:: Build executables for Windows
|
||||
deploy/build.bat
|
||||
|
||||
Если ошибка повторяется на Mac с M1, установите версию CMake для архитектуры ARM:
|
||||
```
|
||||
arch -arm64 brew install cmake
|
||||
:: Build executables with IFW installer for Windows
|
||||
deploy/build.bat --installer ifw
|
||||
|
||||
:: Build executables with IFW and WIX installer for Windows
|
||||
deploy/build.bat --installer ifw --installer wix
|
||||
|
||||
:: Or just
|
||||
deploy/build.bat --installer all
|
||||
```
|
||||
|
||||
При первой попытке сборка может завершиться с ошибкой source files not found. Это происходит из-за параллельной компиляции зависимостей в XCode. Просто перезапустите сборку.
|
||||
### Разработка в IDE
|
||||
|
||||
* Можно использовать любые IDE которые умеют работать с CMake и находить Qt Kits. Например:
|
||||
- `Qt Creator`
|
||||
- `Visual Studio Code` with `Qt Extension Pack`
|
||||
- и так далее
|
||||
|
||||
## Как собрать Android-приложение
|
||||
Сборка тестировалась на MacOS. Требования:
|
||||
- JDK 11
|
||||
- Android SDK 33
|
||||
- CMake 3.25.0
|
||||
|
||||
Установите QT, QT Creator и Android Studio.
|
||||
Настройте QT Creator:
|
||||
* Для использования `Xcode` нужно сконфигурировать проект с помощью `cmake`. Самый простой способ это сделать - использовать `Qt Creator` для конфигурации. Затем, нужно открыть файл `AmneziaVPN.xcodeproj` из папки сборки с помощью `Xcode`. Учтите, что никакие файлы фактически не сохраняются - они сохраняются в директории сборки. Если требуется, скопируйте файлы вручную
|
||||
|
||||
- В меню QT Creator перейдите в `QT Creator` -> `Preferences` -> `Devices` ->`Android`.
|
||||
- Укажите путь к JDK 11.
|
||||
- Укажите путь к Android SDK (`$ANDROID_HOME`)
|
||||
* `Android studio` может быть использована подобным вышеуказанному способу - нужно использовать `cmake` вручную или через `Qt Creator` для конфигурации. Далее, откройте `<build-dir>/client/android-build` в `Android studio`. Не забудьте скопировать изменённые файлы в папку с исходным кодом - все файлы, изменённые в IDE, сохраняются фактически в папке сборки.
|
||||
|
||||
Если вы сталкиваетесь с ошибками, связанными с отсутствием SDK или сообщением «SDK manager not running», их нельзя исправить просто корректировкой путей. Если у вас есть несколько свободных гигабайт на диске, вы можете позволить Qt Creator установить все необходимые компоненты, выбрав пустую папку для расположения Android SDK и нажав кнопку **Set Up SDK**. Учтите: это установит второй Android SDK и NDK на вашем компьютере!
|
||||
|
||||
Убедитесь, что настроена правильная версия CMake: перейдите в **Qt Creator -> Preferences** и в боковом меню выберите пункт **Kits**. В центральной части окна, на вкладке **Kits**, найдите запись для инструмента **CMake Tool**. Если выбранная по умолчанию версия CMake ниже 3.25.0, установите на свою систему CMake версии 3.25.0 или выше, а затем выберите опцию **System CMake at <путь>** из выпадающего списка. Если этот пункт отсутствует, это может означать, что вы еще не установили CMake, или Qt Creator не смог найти путь к нему. В таком случае в окне **Preferences** перейдите в боковое меню **CMake**, затем во вкладку **Tools** в центральной части окна и нажмите кнопку **Add**, чтобы указать путь к установленному CMake.
|
||||
|
||||
Убедитесь, что для вашего проекта выбрана Android Platform SDK 33: в главном окне на боковой панели выберите пункт **Projects**, и слева вы увидите раздел **Build & Run**, показывающий различные целевые Android-платформы. Вы можете выбрать любую из них, так как настройка проекта Amnezia VPN разработана таким образом, чтобы все Android-цели могли быть собраны. Перейдите в подраздел **Build** и прокрутите центральную часть окна до раздела **Build Steps**. Нажмите **Details** в заголовке **Build Android APK** (кнопка **Details** может быть скрыта, если окно Qt Creator не запущено в полноэкранном режиме!). Вот здесь выберите **android-33** в качестве Android Build Platform SDK.
|
||||
|
||||
### Разработка Android-компонентов
|
||||
|
||||
После сборки QT Creator копирует проект в отдельную папку, например, `build-amnezia-client-Android_Qt_<version>_Clang_<architecture>-<BuildType>`. Для разработки Android-компонентов откройте сгенерированный проект в Android Studio, указав папку `build-amnezia-client-Android_Qt_<version>_Clang_<architecture>-<BuildType>/client/android-build` в качестве корневой.
|
||||
Изменения в сгенерированном проекте нужно вручную перенести в репозиторий. После этого можно коммитить изменения.
|
||||
Если возникают проблемы со сборкой в QT Creator после работы в Android Studio, выполните команду `./gradlew clean` в корневой папке сгенерированного проекта (`<path>/client/android-build/.`).
|
||||
### Установка Android SDK
|
||||
|
||||
* Android SDK может быть установлен следующими способами:
|
||||
- Используя `Qt Creator`, через настройки в пунктах `Preferences`->`SDKs`
|
||||
- Используя `Android studio`. По умолчанию необходимые `SDK` устанавливаются автоматически.
|
||||
- Вручную, используя `sdk-manager`. Подробности можно найти [здесь](https://developer.android.com/tools)
|
||||
|
||||
## Лицензия
|
||||
|
||||
|
||||
Submodule client/3rd-prebuilt deleted from 51bb4703a4.
Vendored
-1
Submodule client/3rd/QSimpleCrypto deleted from c99b33f0e0.
Vendored
+1
-1
Submodule client/3rd/amneziawg-apple updated: cf63135331...bdaf0b337b.
+157
-78
@@ -1,6 +1,8 @@
|
||||
cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
|
||||
|
||||
set(PROJECT AmneziaVPN)
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/cmake/clientBrandingDefaults.cmake)
|
||||
|
||||
set(PROJECT ${CLIENT_TARGET_NAME})
|
||||
project(${PROJECT})
|
||||
|
||||
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
|
||||
@@ -35,20 +37,30 @@ add_definitions(-DFREE_V2_ENDPOINT="$ENV{FREE_V2_ENDPOINT}")
|
||||
add_definitions(-DPREM_V1_ENDPOINT="$ENV{PREM_V1_ENDPOINT}")
|
||||
|
||||
if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
|
||||
set(PACKAGES ${PACKAGES} Widgets)
|
||||
set(PACKAGES ${PACKAGES} Widgets HttpServer)
|
||||
endif()
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS ${PACKAGES})
|
||||
find_package(OpenSSL REQUIRED)
|
||||
|
||||
find_package(libagw REQUIRED)
|
||||
|
||||
set(LIBS ${LIBS}
|
||||
Qt6::Core Qt6::Gui
|
||||
Qt6::Network Qt6::Xml Qt6::RemoteObjects
|
||||
Qt6::Quick Qt6::Svg Qt6::QuickControls2
|
||||
Qt6::Core5Compat Qt6::Concurrent
|
||||
amnezia::libagw
|
||||
)
|
||||
|
||||
find_package(Qt6 QUIET OPTIONAL_COMPONENTS CorePrivate)
|
||||
if(TARGET Qt6::CorePrivate)
|
||||
set(LIBS ${LIBS} Qt6::CorePrivate)
|
||||
add_compile_definitions(AMNEZIA_HAS_QZIPWRITER)
|
||||
endif()
|
||||
|
||||
if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
|
||||
set(LIBS ${LIBS} Qt6::Widgets)
|
||||
set(LIBS ${LIBS} Qt6::Widgets Qt6::HttpServer)
|
||||
endif()
|
||||
|
||||
qt_standard_project_setup()
|
||||
@@ -56,6 +68,9 @@ qt_add_executable(${PROJECT} MANUAL_FINALIZATION)
|
||||
target_include_directories(${PROJECT} PUBLIC
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>
|
||||
)
|
||||
target_compile_definitions(${PROJECT} PRIVATE
|
||||
CLIENT_KEYCHAIN_NAME_STR=\"${CLIENT_KEYCHAIN_NAME}\"
|
||||
)
|
||||
|
||||
if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID))
|
||||
qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_interface.rep)
|
||||
@@ -65,38 +80,40 @@ endif()
|
||||
qt6_add_resources(QRC ${QRC}
|
||||
${CMAKE_CURRENT_LIST_DIR}/images/images.qrc
|
||||
${CMAKE_CURRENT_LIST_DIR}/images/flagKit.qrc
|
||||
${CMAKE_CURRENT_LIST_DIR}/ui/qml/qml.qrc
|
||||
${CMAKE_CURRENT_LIST_DIR}/resources/countries.qrc
|
||||
${CMAKE_CURRENT_LIST_DIR}/client_scripts/clientScripts.qrc
|
||||
${CLIENT_QML_QRC_FILE}
|
||||
${CMAKE_CURRENT_LIST_DIR}/server_scripts/serverScripts.qrc
|
||||
${CLIENT_EXTRA_QRC}
|
||||
)
|
||||
|
||||
# -- i18n begin
|
||||
set(CMAKE_AUTORCC ON)
|
||||
if(CLIENT_TS_FILES)
|
||||
set(AMNEZIAVPN_TS_FILES ${CLIENT_TS_FILES})
|
||||
else()
|
||||
set(AMNEZIAVPN_TS_FILES
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_ru_RU.ts
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_zh_CN.ts
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_fa_IR.ts
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_ar_EG.ts
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_my_MM.ts
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_uk_UA.ts
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_ur_PK.ts
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_hi_IN.ts
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_es_ES.ts
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_ko_KR.ts
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_fr_FR.ts
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_th_TH.ts
|
||||
)
|
||||
endif()
|
||||
|
||||
set(AMNEZIAVPN_TS_FILES
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_ru_RU.ts
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_zh_CN.ts
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_fa_IR.ts
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_ar_EG.ts
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_my_MM.ts
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_uk_UA.ts
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_ur_PK.ts
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_hi_IN.ts
|
||||
file(GLOB_RECURSE AMNEZIAVPN_QML_FILES ${CMAKE_CURRENT_LIST_DIR}/ui/qml/*.qml)
|
||||
|
||||
qt6_add_translations(${PROJECT}
|
||||
TS_FILES ${AMNEZIAVPN_TS_FILES}
|
||||
SOURCES ${AMNEZIAVPN_QML_FILES}
|
||||
RESOURCE_PREFIX "/translations"
|
||||
)
|
||||
|
||||
file(GLOB_RECURSE AMNEZIAVPN_TS_SOURCES *.qrc *.cpp *.h *.ui)
|
||||
list(FILTER AMNEZIAVPN_TS_SOURCES EXCLUDE REGEX "qtgamepad/examples")
|
||||
|
||||
qt_create_translation(AMNEZIAVPN_QM_FILES ${AMNEZIAVPN_TS_SOURCES} ${AMNEZIAVPN_TS_FILES})
|
||||
|
||||
set(QM_FILE_LIST "")
|
||||
foreach(FILE ${AMNEZIAVPN_QM_FILES})
|
||||
get_filename_component(QM_FILE_NAME ${FILE} NAME)
|
||||
list(APPEND QM_FILE_LIST "<file>${QM_FILE_NAME}</file>")
|
||||
endforeach()
|
||||
string(REPLACE ";" "" QM_FILE_LIST ${QM_FILE_LIST})
|
||||
|
||||
configure_file(${CMAKE_CURRENT_LIST_DIR}/translations/translations.qrc.in ${CMAKE_CURRENT_BINARY_DIR}/translations.qrc)
|
||||
qt6_add_resources(QRC ${I18NQRC} ${CMAKE_CURRENT_BINARY_DIR}/translations.qrc)
|
||||
# -- i18n end
|
||||
|
||||
set(IS_CI ${CI})
|
||||
@@ -111,12 +128,18 @@ endif()
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/cmake/3rdparty.cmake)
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/cmake/sources.cmake)
|
||||
|
||||
include_directories(
|
||||
set(_client_include_dirs
|
||||
${CMAKE_CURRENT_LIST_DIR}/../ipc
|
||||
${CMAKE_CURRENT_LIST_DIR}/../common/logger
|
||||
${CMAKE_CURRENT_LIST_DIR}/../common/crypto
|
||||
${CMAKE_CURRENT_LIST_DIR}
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
)
|
||||
if(CLIENT_BRANDING_INCLUDE_DIR)
|
||||
list(APPEND _client_include_dirs ${CLIENT_BRANDING_INCLUDE_DIR})
|
||||
endif()
|
||||
list(APPEND _client_include_dirs ${CMAKE_CURRENT_LIST_DIR})
|
||||
include_directories(${_client_include_dirs})
|
||||
|
||||
if(MACOS_NE)
|
||||
message("MACOS_NE is ON")
|
||||
@@ -132,6 +155,26 @@ include_directories(mozilla/shared)
|
||||
include_directories(mozilla/models)
|
||||
|
||||
configure_file(${CMAKE_CURRENT_LIST_DIR}/../version.h.in ${CMAKE_CURRENT_BINARY_DIR}/version.h)
|
||||
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/core/utils)
|
||||
configure_file(
|
||||
${CMAKE_CURRENT_LIST_DIR}/core/utils/appUiConfig.h.in
|
||||
${CMAKE_CURRENT_BINARY_DIR}/core/utils/appUiConfig.h
|
||||
@ONLY
|
||||
)
|
||||
configure_file(
|
||||
${CMAKE_CURRENT_LIST_DIR}/core/utils/swiftBridgeConfig.h.in
|
||||
${CMAKE_CURRENT_BINARY_DIR}/core/utils/swiftBridgeConfig.h
|
||||
@ONLY
|
||||
)
|
||||
# Swift can't consume a `-DGROUP_ID="value"` compile define (Swift -D flags are
|
||||
# presence-only), unlike the Objective-C++ sources that read GROUP_ID directly.
|
||||
# Generate a Swift constant instead so Log.swift can use the branded app group.
|
||||
set(CLIENT_GROUP_IDENTIFIER_SWIFT_FILE ${CMAKE_CURRENT_BINARY_DIR}/platforms/ios/GroupIdentifier.swift)
|
||||
configure_file(
|
||||
${CMAKE_CURRENT_LIST_DIR}/platforms/ios/GroupIdentifier.swift.in
|
||||
${CLIENT_GROUP_IDENTIFIER_SWIFT_FILE}
|
||||
@ONLY
|
||||
)
|
||||
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
target_compile_definitions(${PROJECT} PRIVATE "MZ_DEBUG")
|
||||
@@ -159,27 +202,20 @@ if(APPLE)
|
||||
cmake_policy(SET CMP0099 NEW)
|
||||
cmake_policy(SET CMP0114 NEW)
|
||||
|
||||
if(NOT BUILD_OSX_APP_IDENTIFIER)
|
||||
set(BUILD_OSX_APP_IDENTIFIER org.amnezia.AmneziaVPN CACHE STRING "OSX Application identifier")
|
||||
endif()
|
||||
if(NOT BUILD_IOS_APP_IDENTIFIER)
|
||||
set(BUILD_IOS_APP_IDENTIFIER org.amnezia.AmneziaVPN CACHE STRING "iOS Application identifier")
|
||||
endif()
|
||||
if(NOT BUILD_IOS_GROUP_IDENTIFIER)
|
||||
set(BUILD_IOS_GROUP_IDENTIFIER group.org.amnezia.AmneziaVPN CACHE STRING "iOS Group identifier")
|
||||
endif()
|
||||
if(NOT BUILD_VPN_DEVELOPMENT_TEAM)
|
||||
set(BUILD_VPN_DEVELOPMENT_TEAM X7UJ388FXK CACHE STRING "Amnezia VPN Development Team")
|
||||
endif()
|
||||
|
||||
set(CMAKE_XCODE_GENERATE_SCHEME FALSE)
|
||||
set(CMAKE_XCODE_ATTRIBUTE_DEVELOPMENT_TEAM ${BUILD_VPN_DEVELOPMENT_TEAM})
|
||||
set(CMAKE_XCODE_ATTRIBUTE_GROUP_ID_IOS ${BUILD_IOS_GROUP_IDENTIFIER})
|
||||
|
||||
if (BUILD_VPN_KEYCHAIN)
|
||||
set(CMAKE_XCODE_ATTRIBUTE_OTHER_CODE_SIGN_FLAGS "--keychain ${BUILD_VPN_KEYCHAIN}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(LINUX AND NOT ANDROID)
|
||||
set(LIBS ${LIBS} -static-libstdc++ -static-libgcc -ldl)
|
||||
link_directories(${CMAKE_CURRENT_LIST_DIR}/platforms/linux)
|
||||
set(LIBS ${LIBS} -Wl,--no-as-needed $<TARGET_LINKER_FILE:Qt6::RemoteObjects> -Wl,--as-needed)
|
||||
endif()
|
||||
|
||||
if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID))
|
||||
@@ -200,45 +236,11 @@ elseif(APPLE)
|
||||
include(cmake/macos.cmake)
|
||||
endif()
|
||||
|
||||
list(APPEND SOURCES ${CMAKE_CURRENT_LIST_DIR}/main.cpp)
|
||||
|
||||
target_link_libraries(${PROJECT} PRIVATE ${LIBS})
|
||||
target_compile_definitions(${PROJECT} PRIVATE "MZ_$<UPPER_CASE:${MZ_PLATFORM_NAME}>")
|
||||
|
||||
# deploy artifacts required to run the application to the debug build folder
|
||||
if(WIN32)
|
||||
if("${CMAKE_SIZEOF_VOID_P}" STREQUAL "8")
|
||||
set(DEPLOY_PLATFORM_PATH "windows/x64")
|
||||
else()
|
||||
set(DEPLOY_PLATFORM_PATH "windows/x32")
|
||||
endif()
|
||||
elseif(LINUX)
|
||||
set(DEPLOY_PLATFORM_PATH "linux/client")
|
||||
elseif(APPLE AND NOT IOS)
|
||||
set(DEPLOY_PLATFORM_PATH "macos")
|
||||
endif()
|
||||
|
||||
if(NOT IOS AND NOT ANDROID AND NOT MACOS_NE)
|
||||
add_custom_command(
|
||||
TARGET ${PROJECT} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E $<IF:$<CONFIG:Debug>,copy_directory,true>
|
||||
${CMAKE_SOURCE_DIR}/deploy/data/${DEPLOY_PLATFORM_PATH}
|
||||
$<TARGET_FILE_DIR:${PROJECT}>
|
||||
COMMAND_EXPAND_LISTS
|
||||
)
|
||||
add_custom_command(
|
||||
TARGET ${PROJECT} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E $<IF:$<CONFIG:Debug>,copy_directory,true>
|
||||
${CMAKE_SOURCE_DIR}/client/3rd-prebuilt/deploy-prebuilt/${DEPLOY_PLATFORM_PATH}
|
||||
$<TARGET_FILE_DIR:${PROJECT}>
|
||||
COMMAND_EXPAND_LISTS
|
||||
)
|
||||
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_sources(${PROJECT} PRIVATE ${SOURCES} ${HEADERS} ${RESOURCES} ${QRC} ${I18NQRC})
|
||||
|
||||
# Finalize the executable so Qt can gather/deploy QML modules and plugins correctly (Android needs this).
|
||||
@@ -250,3 +252,80 @@ if(COMMAND qt_finalize_executable)
|
||||
else()
|
||||
qt_finalize_target(${PROJECT})
|
||||
endif()
|
||||
|
||||
install(TARGETS ${PROJECT}
|
||||
DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
RUNTIME_DEPENDENCY_SET client_deps
|
||||
COMPONENT AmneziaVPN
|
||||
)
|
||||
|
||||
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]]
|
||||
[[^(lib)?[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 "")
|
||||
if(WIN32)
|
||||
set(deploy_tool_options "--force-openssl --force")
|
||||
endif()
|
||||
|
||||
qt_generate_deploy_qml_app_script(
|
||||
TARGET ${PROJECT}
|
||||
OUTPUT_SCRIPT QT_DEPLOY_SCRIPT
|
||||
NO_UNSUPPORTED_PLATFORM_ERROR
|
||||
DEPLOY_TOOL_OPTIONS ${deploy_tool_options}
|
||||
)
|
||||
install(SCRIPT ${QT_DEPLOY_SCRIPT}
|
||||
COMPONENT AmneziaVPN
|
||||
)
|
||||
|
||||
if (APPLE AND NOT IOS AND NOT MACOS_NE)
|
||||
list(APPEND OVPN_SCRIPTS "${CMAKE_SOURCE_DIR}/deploy/data/macos/update-resolv-conf.sh")
|
||||
set_target_properties(${PROJECT} PROPERTIES
|
||||
XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "NO"
|
||||
XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED "NO"
|
||||
)
|
||||
endif()
|
||||
if (LINUX AND NOT ANDROID)
|
||||
list(APPEND OVPN_SCRIPTS "${CMAKE_SOURCE_DIR}/deploy/data/linux/update-resolv-conf.sh")
|
||||
endif()
|
||||
|
||||
if(OVPN_SCRIPTS)
|
||||
add_custom_command(TARGET ${PROJECT} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${OVPN_SCRIPTS}
|
||||
"$<TARGET_FILE_DIR:${PROJECT}>"
|
||||
)
|
||||
|
||||
install(FILES ${OVPN_SCRIPTS}
|
||||
DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
COMPONENT AmneziaVPN
|
||||
PERMISSIONS
|
||||
OWNER_READ OWNER_EXECUTE
|
||||
GROUP_READ GROUP_EXECUTE
|
||||
WORLD_READ WORLD_EXECUTE
|
||||
)
|
||||
endif()
|
||||
|
||||
if(AMNEZIA_BUILD_TESTS)
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
@@ -18,13 +18,23 @@
|
||||
#include <QtQuick/QQuickWindow>
|
||||
#include <QWindow>
|
||||
|
||||
#include "core/controllers/updateController.h"
|
||||
#include "core/protocols/qmlRegisterProtocols.h"
|
||||
#include "logger.h"
|
||||
#include "ui/controllers/qml/pageController.h"
|
||||
#include "ui/models/installedAppsModel.h"
|
||||
#include "ui/utils/mtProxyPublicHostInput.h"
|
||||
#include "version.h"
|
||||
#include "core/utils/appUiConfig.h"
|
||||
|
||||
#include "platforms/ios/QRCodeReaderBase.h"
|
||||
#ifdef Q_OS_IOS
|
||||
#include "platforms/ios/ioscontextmenu.h"
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include "platforms/android/android_controller.h"
|
||||
#endif
|
||||
|
||||
|
||||
bool AmneziaApplication::m_forceQuit = false;
|
||||
@@ -98,7 +108,7 @@ void AmneziaApplication::init()
|
||||
{
|
||||
m_engine = new QQmlApplicationEngine;
|
||||
|
||||
const QUrl url(QStringLiteral("qrc:/ui/qml/main2.qml"));
|
||||
const QUrl url(QStringLiteral(APP_QML_ENTRYPOINT));
|
||||
QObject::connect(
|
||||
m_engine, &QQmlApplicationEngine::objectCreated, this,
|
||||
[this, url](QObject *obj, const QUrl &objUrl) {
|
||||
@@ -119,7 +129,18 @@ void AmneziaApplication::init()
|
||||
win->setPersistentSceneGraph(true);
|
||||
win->setPersistentGraphics(true);
|
||||
#endif
|
||||
#ifdef Q_OS_IOS
|
||||
auto pageController = m_coreController->pageController();
|
||||
pageController->setSafeAreaMargins(win->safeAreaMargins());
|
||||
QObject::connect(win, &QWindow::safeAreaMarginsChanged, pageController, &PageController::setSafeAreaMargins);
|
||||
#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);
|
||||
@@ -132,13 +153,23 @@ void AmneziaApplication::init()
|
||||
m_engine->rootContext()->setContextProperty("IsMacOsNeBuild", false);
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
m_engine->rootContext()->setContextProperty("IosContextMenu", new IosContextMenu(this));
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
m_engine->rootContext()->setContextProperty("IsPlayBuild", AndroidController::instance()->isPlay());
|
||||
#else
|
||||
m_engine->rootContext()->setContextProperty("IsPlayBuild", false);
|
||||
#endif
|
||||
|
||||
m_vpnConnection.reset(new VpnConnection(nullptr, nullptr));
|
||||
m_vpnConnection->moveToThread(&m_vpnConnectionThread);
|
||||
m_vpnConnectionThread.start();
|
||||
|
||||
m_coreController.reset(new CoreController(m_vpnConnection, m_settings, m_engine));
|
||||
|
||||
m_engine->addImportPath("qrc:/ui/qml/Modules/");
|
||||
m_engine->addImportPath(QStringLiteral(APP_QML_IMPORT_PATH));
|
||||
|
||||
if (m_parser.isSet(m_optImport)) {
|
||||
const QString data = m_parser.value(m_optImport);
|
||||
@@ -153,6 +184,10 @@ void AmneziaApplication::init()
|
||||
|
||||
m_coreController->setQmlRoot();
|
||||
|
||||
#if CLIENT_ENABLE_APP_UPDATES
|
||||
m_coreController->checkForAppUpdates();
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_WIN //TODO
|
||||
if (m_parser.isSet(m_optAutostart))
|
||||
m_coreController->pageController()->showOnStartup();
|
||||
@@ -215,16 +250,20 @@ void AmneziaApplication::registerTypes()
|
||||
|
||||
qmlRegisterType<InstalledAppsModel>("InstalledAppsModel", 1, 0, "InstalledAppsModel");
|
||||
|
||||
qmlRegisterType<PublicHostInputValidator>("MtProxyConfig", 1, 0, "PublicHostInputValidator");
|
||||
qmlRegisterType<PublicHostInputValidator>("TelemtConfig", 1, 0, "PublicHostInputValidator");
|
||||
|
||||
amnezia::declareQmlProtocolEnum();
|
||||
Vpn::declareQmlVpnConnectionStateEnum();
|
||||
PageLoader::declareQmlPageEnum();
|
||||
UpdateState::declareQmlUpdateStateEnum();
|
||||
}
|
||||
|
||||
void AmneziaApplication::loadFonts()
|
||||
{
|
||||
QQuickStyle::setStyle("Basic");
|
||||
|
||||
QFontDatabase::addApplicationFont(":/fonts/pt-root-ui_vf.ttf");
|
||||
QFontDatabase::addApplicationFont(QStringLiteral(APP_UI_FONT_RESOURCE));
|
||||
}
|
||||
|
||||
bool AmneziaApplication::parseCommands()
|
||||
@@ -251,7 +290,7 @@ bool AmneziaApplication::parseCommands()
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
||||
void AmneziaApplication::startLocalServer() {
|
||||
const QString serverName("AmneziaVPNInstance");
|
||||
const QString serverName(APP_INSTANCE_NAME);
|
||||
QLocalServer::removeServer(serverName);
|
||||
|
||||
QLocalServer *server = new QLocalServer(this);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
plugins {
|
||||
id(libs.plugins.android.library.get().pluginId)
|
||||
id(libs.plugins.kotlin.android.get().pluginId)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "org.amnezia.vpn.billing"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(project(":utils"))
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.kotlinx.coroutines)
|
||||
implementation(libs.android.billing)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import com.android.billingclient.api.BillingClient.BillingResponseCode.BILLING_UNAVAILABLE
|
||||
import com.android.billingclient.api.BillingClient.BillingResponseCode.DEVELOPER_ERROR
|
||||
import com.android.billingclient.api.BillingClient.BillingResponseCode.ERROR
|
||||
import com.android.billingclient.api.BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED
|
||||
import com.android.billingclient.api.BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED
|
||||
import com.android.billingclient.api.BillingClient.BillingResponseCode.ITEM_NOT_OWNED
|
||||
import com.android.billingclient.api.BillingClient.BillingResponseCode.ITEM_UNAVAILABLE
|
||||
import com.android.billingclient.api.BillingClient.BillingResponseCode.NETWORK_ERROR
|
||||
import com.android.billingclient.api.BillingClient.BillingResponseCode.SERVICE_DISCONNECTED
|
||||
import com.android.billingclient.api.BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE
|
||||
import com.android.billingclient.api.BillingClient.BillingResponseCode.USER_CANCELED
|
||||
import com.android.billingclient.api.BillingResult
|
||||
import org.amnezia.vpn.util.ErrorCode
|
||||
|
||||
internal class BillingException(
|
||||
billingResult: BillingResult,
|
||||
retryable: Boolean? = null
|
||||
) : Exception(billingResult.toString()) {
|
||||
|
||||
constructor(msg: String) : this(BillingResult.newBuilder()
|
||||
.setResponseCode(DEVELOPER_ERROR)
|
||||
.setDebugMessage(msg)
|
||||
.build())
|
||||
|
||||
val errorCode: Int
|
||||
val isCanceled = billingResult.responseCode == USER_CANCELED
|
||||
val isRetryable = retryable ?: (billingResult.responseCode in setOf(
|
||||
NETWORK_ERROR,
|
||||
SERVICE_DISCONNECTED,
|
||||
SERVICE_UNAVAILABLE,
|
||||
ERROR
|
||||
))
|
||||
|
||||
init {
|
||||
when (billingResult.responseCode) {
|
||||
ERROR -> {
|
||||
errorCode = ErrorCode.BillingGooglePlayError
|
||||
}
|
||||
|
||||
BILLING_UNAVAILABLE, SERVICE_DISCONNECTED, SERVICE_UNAVAILABLE -> {
|
||||
errorCode = ErrorCode.BillingUnavailable
|
||||
}
|
||||
|
||||
DEVELOPER_ERROR, FEATURE_NOT_SUPPORTED, ITEM_NOT_OWNED -> {
|
||||
errorCode = ErrorCode.BillingError
|
||||
}
|
||||
|
||||
ITEM_ALREADY_OWNED -> {
|
||||
errorCode = ErrorCode.SubscriptionAlreadyOwned
|
||||
}
|
||||
|
||||
ITEM_UNAVAILABLE -> {
|
||||
errorCode = ErrorCode.SubscriptionUnavailable
|
||||
}
|
||||
|
||||
NETWORK_ERROR -> {
|
||||
errorCode = ErrorCode.BillingNetworkError
|
||||
}
|
||||
|
||||
else -> {
|
||||
errorCode = ErrorCode.BillingError
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import com.android.billingclient.api.AcknowledgePurchaseParams
|
||||
import com.android.billingclient.api.BillingClient
|
||||
import com.android.billingclient.api.BillingClient.BillingResponseCode
|
||||
import com.android.billingclient.api.BillingClient.ProductType
|
||||
import com.android.billingclient.api.BillingClientStateListener
|
||||
import com.android.billingclient.api.BillingFlowParams
|
||||
import com.android.billingclient.api.BillingFlowParams.SubscriptionUpdateParams.ReplacementMode
|
||||
import com.android.billingclient.api.BillingResult
|
||||
import com.android.billingclient.api.GetBillingConfigParams
|
||||
import com.android.billingclient.api.PendingPurchasesParams
|
||||
import com.android.billingclient.api.ProductDetails
|
||||
import com.android.billingclient.api.Purchase
|
||||
import com.android.billingclient.api.PurchasesUpdatedListener
|
||||
import com.android.billingclient.api.QueryProductDetailsParams
|
||||
import com.android.billingclient.api.QueryProductDetailsParams.Product
|
||||
import com.android.billingclient.api.QueryPurchasesParams
|
||||
import com.android.billingclient.api.acknowledgePurchase
|
||||
import com.android.billingclient.api.queryProductDetails
|
||||
import com.android.billingclient.api.queryPurchasesAsync
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.amnezia.vpn.util.ErrorCode
|
||||
import org.amnezia.vpn.util.Log
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.text.NumberFormat
|
||||
import java.util.Currency
|
||||
|
||||
private const val TAG = "BillingProvider"
|
||||
private const val PRODUCT_ID = "premium"
|
||||
|
||||
private val billingPeriodRegex = Regex("""P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?""")
|
||||
|
||||
private fun billingPeriodToMonths(period: String): Double {
|
||||
val match = billingPeriodRegex.matchEntire(period) ?: return 0.0
|
||||
val years = match.groupValues[1].toDoubleOrNull() ?: 0.0
|
||||
val months = match.groupValues[2].toDoubleOrNull() ?: 0.0
|
||||
val weeks = match.groupValues[3].toDoubleOrNull() ?: 0.0
|
||||
val days = match.groupValues[4].toDoubleOrNull() ?: 0.0
|
||||
return years * 12.0 + months + weeks * 7.0 / 30.0 + days / 30.0
|
||||
}
|
||||
|
||||
private fun billingPeriodToDays(period: String): Int {
|
||||
val match = billingPeriodRegex.matchEntire(period) ?: return 0
|
||||
val years = match.groupValues[1].toIntOrNull() ?: 0
|
||||
val months = match.groupValues[2].toIntOrNull() ?: 0
|
||||
val weeks = match.groupValues[3].toIntOrNull() ?: 0
|
||||
val days = match.groupValues[4].toIntOrNull() ?: 0
|
||||
return years * 365 + months * 30 + weeks * 7 + days
|
||||
}
|
||||
|
||||
private fun displayPricePerMonth(priceAmountMicros: Long, currencyCode: String, billingMonths: Double): String? {
|
||||
if (billingMonths <= 1e-6 || currencyCode.isBlank()) return null
|
||||
return try {
|
||||
val amountPerMonth = (priceAmountMicros / 1_000_000.0) / billingMonths
|
||||
NumberFormat.getCurrencyInstance().apply { currency = Currency.getInstance(currencyCode) }
|
||||
.format(amountPerMonth)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log.w(TAG, "Unknown currency code from Play Billing: $currencyCode")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
class BillingProvider(context: Context) : AutoCloseable {
|
||||
|
||||
private var billingClient: BillingClient
|
||||
private var subscriptionPurchases = MutableStateFlow<Pair<BillingResult, List<Purchase>?>?>(null)
|
||||
|
||||
private val purchasesUpdatedListeners = PurchasesUpdatedListener { billingResult, purchases ->
|
||||
subscriptionPurchases.value = billingResult to purchases
|
||||
}
|
||||
|
||||
init {
|
||||
billingClient = BillingClient.newBuilder(context)
|
||||
.setListener(purchasesUpdatedListeners)
|
||||
.enablePendingPurchases(PendingPurchasesParams.newBuilder().enableOneTimeProducts().build())
|
||||
.build()
|
||||
}
|
||||
|
||||
private suspend fun connect() {
|
||||
if (billingClient.isReady) return
|
||||
|
||||
val connection = CompletableDeferred<Unit>()
|
||||
withContext(Dispatchers.IO) {
|
||||
billingClient.startConnection(object : BillingClientStateListener {
|
||||
override fun onBillingSetupFinished(billingResult: BillingResult) {
|
||||
if (billingResult.isOk) {
|
||||
connection.complete(Unit)
|
||||
} else {
|
||||
Log.e(TAG, "Billing setup failed: $billingResult")
|
||||
connection.completeExceptionally(BillingException(billingResult))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBillingServiceDisconnected() {
|
||||
Log.w(TAG, "Billing service disconnected")
|
||||
}
|
||||
})
|
||||
}
|
||||
connection.await()
|
||||
}
|
||||
|
||||
private suspend fun handleBillingApiCall(block: suspend () -> JSONObject): JSONObject {
|
||||
val numberAttempts = 3
|
||||
var attemptCount = 0
|
||||
while (true) {
|
||||
try {
|
||||
return block()
|
||||
} catch (e: BillingException) {
|
||||
if (e.isCanceled) {
|
||||
Log.w(TAG, "Billing canceled")
|
||||
return JSONObject().put("responseCode", ErrorCode.BillingCanceled)
|
||||
} else if (e.isRetryable && attemptCount < numberAttempts) {
|
||||
Log.d(TAG, "Retryable error: $e")
|
||||
++attemptCount
|
||||
delay(1000)
|
||||
} else {
|
||||
Log.e(TAG, "Billing error: $e")
|
||||
return JSONObject().put("responseCode", e.errorCode)
|
||||
}
|
||||
} catch (_: CancellationException) {
|
||||
Log.w(TAG, "Billing coroutine canceled")
|
||||
return JSONObject().put("responseCode", ErrorCode.BillingCanceled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getSubscriptionPlans(): JSONObject {
|
||||
val productDetailsList = getProductDetails()
|
||||
val resultJson = JSONObject().put("responseCode", ErrorCode.NoError)
|
||||
val productArray = JSONArray().also { resultJson.put("products", it) }
|
||||
productDetailsList?.forEach { productDetails ->
|
||||
val product = JSONObject().also { productArray.put(it) }
|
||||
.put("productId", productDetails.productId)
|
||||
.put("name", productDetails.name)
|
||||
val offers = JSONArray().also { product.put("offers", it) }
|
||||
productDetails.subscriptionOfferDetails?.forEach { offerDetails ->
|
||||
val offer = JSONObject().also { offers.put(it) }
|
||||
.put("basePlanId", offerDetails.basePlanId)
|
||||
.put("offerId", offerDetails.offerId)
|
||||
.put("offerToken", offerDetails.offerToken)
|
||||
val pricingPhases = JSONArray().also { offer.put("pricingPhases", it) }
|
||||
offerDetails.pricingPhases.pricingPhaseList.forEach { phase ->
|
||||
val billingMonths = billingPeriodToMonths(phase.billingPeriod)
|
||||
JSONObject().also { pricingPhases.put(it) }
|
||||
.put("billingCycleCount", phase.billingCycleCount)
|
||||
.put("billingPeriod", phase.billingPeriod)
|
||||
.put("formattedPrice", phase.formattedPrice)
|
||||
.put("priceAmountMicros", phase.priceAmountMicros)
|
||||
.put("recurrenceMode", phase.recurrenceMode)
|
||||
.put("subscriptionBillingMonths", billingMonths)
|
||||
.apply {
|
||||
displayPricePerMonth(phase.priceAmountMicros, phase.priceCurrencyCode, billingMonths)?.let {
|
||||
put("displayPricePerMonth", it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val trialPhase = offerDetails.pricingPhases.pricingPhaseList.firstOrNull()
|
||||
val hasFreeTrial = trialPhase != null && trialPhase.priceAmountMicros == 0L
|
||||
offer.put("hasFreeTrial", hasFreeTrial)
|
||||
if (hasFreeTrial) {
|
||||
offer.put("trialDays", billingPeriodToDays(trialPhase!!.billingPeriod))
|
||||
}
|
||||
}
|
||||
}
|
||||
return resultJson
|
||||
}
|
||||
|
||||
private suspend fun getProductDetails(): List<ProductDetails>? {
|
||||
val productDetailsParams = Product.newBuilder()
|
||||
.setProductId(PRODUCT_ID)
|
||||
.setProductType(ProductType.SUBS)
|
||||
.build()
|
||||
|
||||
val queryProductDetailsParams = QueryProductDetailsParams.newBuilder()
|
||||
.setProductList(listOf(productDetailsParams))
|
||||
.build()
|
||||
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
billingClient.queryProductDetails(queryProductDetailsParams)
|
||||
}
|
||||
|
||||
if (!result.billingResult.isOk) {
|
||||
Log.e(TAG, "Failed to get product details: ${result.billingResult}")
|
||||
throw BillingException(result.billingResult)
|
||||
}
|
||||
|
||||
return result.productDetailsList
|
||||
}
|
||||
|
||||
suspend fun getCustomerCountryCode(): JSONObject {
|
||||
val deferred = CompletableDeferred<String>()
|
||||
withContext(Dispatchers.IO) {
|
||||
billingClient.getBillingConfigAsync(GetBillingConfigParams.newBuilder().build(),
|
||||
{ billingResult, billingConfig ->
|
||||
Log.v(TAG, "Billing config: $billingResult, ${billingConfig?.countryCode}")
|
||||
if (billingResult.isOk) {
|
||||
deferred.complete(billingConfig?.countryCode ?: "")
|
||||
} else {
|
||||
deferred.completeExceptionally(BillingException(billingResult))
|
||||
}
|
||||
})
|
||||
}
|
||||
val countryCode = deferred.await()
|
||||
|
||||
return JSONObject()
|
||||
.put("responseCode", ErrorCode.NoError)
|
||||
.put("countryCode", countryCode)
|
||||
}
|
||||
|
||||
suspend fun purchaseSubscription(
|
||||
activity: Activity,
|
||||
offerToken: String,
|
||||
oldPurchaseToken: String? = null
|
||||
): JSONObject {
|
||||
if (offerToken.isBlank()) throw BillingException("offerToken can not be empty")
|
||||
|
||||
subscriptionPurchases.value = null
|
||||
|
||||
val productDetails = getProductDetails()?.firstOrNull { it.productId == PRODUCT_ID }
|
||||
?: throw BillingException("Product details not found")
|
||||
|
||||
val productDetail = BillingFlowParams.ProductDetailsParams.newBuilder()
|
||||
.setProductDetails(productDetails)
|
||||
.setOfferToken(offerToken)
|
||||
.build()
|
||||
|
||||
val subscriptionUpdateParams = oldPurchaseToken?.let {
|
||||
// Per Google Play docs, switching between auto-renewing plans within the same
|
||||
// subscription only supports CHARGE_FULL_PRICE or WITHOUT_PRORATION - any other mode
|
||||
// (e.g. CHARGE_PRORATED_PRICE) is rejected with DEVELOPER_ERROR "Requested replacement
|
||||
// mode is not supported for this request".
|
||||
BillingFlowParams.SubscriptionUpdateParams.newBuilder()
|
||||
.setOldPurchaseToken(oldPurchaseToken)
|
||||
.setSubscriptionReplacementMode(ReplacementMode.CHARGE_FULL_PRICE)
|
||||
.build()
|
||||
}
|
||||
|
||||
val billingResult = withContext(Dispatchers.Main.immediate) {
|
||||
billingClient.launchBillingFlow(activity, BillingFlowParams.newBuilder()
|
||||
.setProductDetailsParamsList(listOf(productDetail))
|
||||
.apply { subscriptionUpdateParams?.let { setSubscriptionUpdateParams(it) } }
|
||||
.build())
|
||||
}
|
||||
|
||||
if (billingResult.responseCode == BillingResponseCode.ITEM_ALREADY_OWNED) {
|
||||
Log.w(TAG, "Attempting to purchase already owned product")
|
||||
val purchases = queryPurchases()
|
||||
if (purchases.any { PRODUCT_ID in it.products }) throw BillingException(billingResult)
|
||||
else throw BillingException(billingResult, retryable = true)
|
||||
} else if (billingResult.responseCode == BillingResponseCode.ITEM_NOT_OWNED) {
|
||||
Log.w(TAG, "Attempting to replace not owned product")
|
||||
val purchases = queryPurchases()
|
||||
if (purchases.all { PRODUCT_ID !in it.products }) throw BillingException(billingResult)
|
||||
else throw BillingException(billingResult, retryable = true)
|
||||
} else if (!billingResult.isOk) throw BillingException(billingResult)
|
||||
|
||||
subscriptionPurchases.firstOrNull { it != null }?.let { (billingResult, purchases) ->
|
||||
if (!billingResult.isOk) throw BillingException(billingResult, retryable = false)
|
||||
return JSONObject()
|
||||
.put("responseCode", ErrorCode.NoError)
|
||||
.put("purchases", processPurchases(purchases))
|
||||
} ?: throw BillingException("Purchase failed")
|
||||
}
|
||||
|
||||
private fun processPurchases(purchases: List<Purchase>?): JSONArray {
|
||||
val purchaseArray = JSONArray()
|
||||
purchases?.forEach { purchase ->
|
||||
Log.v(TAG, "processPurchases: orderId=${purchase.orderId} state=${purchase.purchaseState} products=${purchase.products}")
|
||||
JSONObject().also { purchaseArray.put(it) }
|
||||
.put("purchaseToken", purchase.purchaseToken)
|
||||
.put("purchaseTime", purchase.purchaseTime)
|
||||
.put("purchaseState", purchase.purchaseState)
|
||||
.put("isAcknowledged", purchase.isAcknowledged)
|
||||
.put("isAutoRenewing", purchase.isAutoRenewing)
|
||||
.put("orderId", purchase.orderId)
|
||||
.put("productIds", JSONArray(purchase.products))
|
||||
}
|
||||
return purchaseArray
|
||||
}
|
||||
|
||||
suspend fun acknowledge(purchaseToken: String): JSONObject {
|
||||
Log.v(TAG, "Acknowledge purchase")
|
||||
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
billingClient.acknowledgePurchase(
|
||||
AcknowledgePurchaseParams.newBuilder()
|
||||
.setPurchaseToken(purchaseToken)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
Log.v(TAG, "Acknowledge purchase result: $result")
|
||||
|
||||
if (result.responseCode == BillingResponseCode.ITEM_NOT_OWNED) {
|
||||
Log.w(TAG, "Attempting to acknowledge not owned product")
|
||||
val purchases = queryPurchases()
|
||||
if (purchases.all { PRODUCT_ID !in it.products }) throw BillingException(result)
|
||||
else throw BillingException(result, retryable = true)
|
||||
} else if (!result.isOk && result.responseCode != BillingResponseCode.ITEM_ALREADY_OWNED) {
|
||||
throw BillingException(result)
|
||||
}
|
||||
|
||||
return JSONObject().put("responseCode", ErrorCode.NoError)
|
||||
}
|
||||
|
||||
suspend fun getPurchases(): JSONObject {
|
||||
val purchases = queryPurchases()
|
||||
return JSONObject()
|
||||
.put("responseCode", ErrorCode.NoError)
|
||||
.put("purchases", processPurchases(purchases))
|
||||
}
|
||||
|
||||
private suspend fun queryPurchases(): List<Purchase> {
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
billingClient.queryPurchasesAsync(
|
||||
QueryPurchasesParams.newBuilder().setProductType(ProductType.SUBS).build()
|
||||
)
|
||||
}
|
||||
Log.v(TAG, "Query purchases result: ${result.billingResult}")
|
||||
if (!result.billingResult.isOk) throw BillingException(result.billingResult)
|
||||
return result.purchasesList
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
billingClient.endConnection()
|
||||
}
|
||||
|
||||
companion object {
|
||||
suspend fun withBillingProvider(context: Context, block: suspend BillingProvider.() -> JSONObject): String =
|
||||
BillingProvider(context).use { bp ->
|
||||
bp.handleBillingApiCall {
|
||||
bp.connect()
|
||||
bp.block()
|
||||
}.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal val BillingResult.isOk: Boolean
|
||||
get() = responseCode == BillingResponseCode.OK
|
||||
@@ -20,6 +20,7 @@ android {
|
||||
namespace = "org.amnezia.vpn"
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
@@ -33,31 +34,26 @@ android {
|
||||
jniLibs.useLegacyPackaging = true
|
||||
}
|
||||
|
||||
val abiList = qtTargetAbiList.split(",")
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "org.amnezia.vpn"
|
||||
targetSdk = qtTargetSdkVersion.toInt()
|
||||
|
||||
// keeps language resources for only the locales specified below
|
||||
resourceConfigurations += listOf("en", "ru", "b+zh+Hans")
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
getByName("main") {
|
||||
manifest.srcFile("AndroidManifest.xml")
|
||||
java.setSrcDirs(listOf("src"))
|
||||
res.setSrcDirs(listOf("res"))
|
||||
// androyddeployqt creates the folders below
|
||||
assets.setSrcDirs(listOf("assets"))
|
||||
jniLibs.setSrcDirs(listOf("libs"))
|
||||
// ndk.abiFilters is only used for single-ABI builds; multi-ABI uses splits below
|
||||
if (abiList.size == 1) {
|
||||
ndk.abiFilters += abiList
|
||||
}
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
register("release") {
|
||||
storeFile = providers.environmentVariable("ANDROID_KEYSTORE_PATH").orNull?.let { file(it) }
|
||||
storePassword = providers.environmentVariable("ANDROID_KEYSTORE_KEY_PASS").orNull
|
||||
keyAlias = providers.environmentVariable("ANDROID_KEYSTORE_KEY_ALIAS").orNull
|
||||
keyPassword = providers.environmentVariable("ANDROID_KEYSTORE_KEY_PASS").orNull
|
||||
storeFile = providers.environmentVariable("QT_ANDROID_KEYSTORE_PATH").orNull?.let { file(it) }
|
||||
storePassword = providers.environmentVariable("QT_ANDROID_KEYSTORE_STORE_PASS").orNull
|
||||
keyAlias = providers.environmentVariable("QT_ANDROID_KEYSTORE_ALIAS").orNull
|
||||
keyPassword = providers.environmentVariable("QT_ANDROID_KEYSTORE_STORE_PASS").orNull
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,11 +73,44 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions += "billing"
|
||||
|
||||
productFlavors {
|
||||
create("oss") {
|
||||
dimension = "billing"
|
||||
buildConfigField("boolean", "IS_PLAY_BUILD", "false")
|
||||
}
|
||||
create("play") {
|
||||
dimension = "billing"
|
||||
buildConfigField("boolean", "IS_PLAY_BUILD", "true")
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
getByName("main") {
|
||||
manifest.srcFile("AndroidManifest.xml")
|
||||
java.setSrcDirs(listOf("src"))
|
||||
res.setSrcDirs(listOf("res"))
|
||||
// androyddeployqt creates the folders below
|
||||
assets.setSrcDirs(listOf("assets"))
|
||||
jniLibs.setSrcDirs(listOf("libs"))
|
||||
}
|
||||
|
||||
getByName("oss") {
|
||||
java.setSrcDirs(listOf("oss"))
|
||||
}
|
||||
|
||||
getByName("play") {
|
||||
java.setSrcDirs(listOf("play"))
|
||||
}
|
||||
}
|
||||
|
||||
splits {
|
||||
abi {
|
||||
isEnable = true
|
||||
// splits only make sense for multi-ABI builds; single-ABI uses ndk.abiFilters
|
||||
isEnable = abiList.size > 1
|
||||
reset()
|
||||
include(*qtTargetAbiList.split(',').toTypedArray())
|
||||
include(*abiList.toTypedArray())
|
||||
isUniversalApk = false
|
||||
}
|
||||
}
|
||||
@@ -99,6 +128,46 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
// androiddeployqt expects:
|
||||
// APK: build/outputs/apk/{base}-{buildType}[-unsigned].apk (no flavor subdir)
|
||||
// AAB: build/outputs/bundle/{buildType}/{base}-{buildType}.aab (no flavor subdir)
|
||||
// where {base} = outputBaseName (set by Qt Creator) or "android-build" (CI fallback).
|
||||
// Release APK gets -unsigned suffix (Qt cmake signs it); debug does not.
|
||||
// Copy only oss flavor to the flat output dir that androiddeployqt/Qt Creator expect.
|
||||
// Play flavor is built via android_play_apk/android_play_aab cmake targets and uses
|
||||
// its native Gradle output paths directly.
|
||||
applicationVariants.all {
|
||||
val flavorName = productFlavors.firstOrNull()?.name ?: ""
|
||||
val buildTypeName = buildType.name
|
||||
if (flavorName == "oss") {
|
||||
val base = outputBaseName.ifEmpty { "android-build" }
|
||||
val unsignedSuffix = if (buildTypeName == "release") "-unsigned" else ""
|
||||
|
||||
packageApplicationProvider.configure {
|
||||
doLast {
|
||||
val srcDir = layout.buildDirectory.dir("outputs/apk/oss/$buildTypeName").get().asFile
|
||||
val dstDir = layout.buildDirectory.dir("outputs/apk").get().asFile
|
||||
dstDir.mkdirs()
|
||||
srcDir.listFiles()?.filter { it.name.endsWith(".apk") }?.forEach { apk ->
|
||||
apk.copyTo(File(dstDir, "$base-$buildTypeName$unsignedSuffix.apk"), overwrite = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val variantName = name
|
||||
tasks.named("bundle${variantName.replaceFirstChar { it.uppercase() }}") {
|
||||
doLast {
|
||||
val srcDir = layout.buildDirectory.dir("outputs/bundle/$variantName").get().asFile
|
||||
val dstDir = layout.buildDirectory.dir("outputs/bundle/$buildTypeName").get().asFile
|
||||
dstDir.mkdirs()
|
||||
srcDir.listFiles()?.filter { it.name.endsWith(".aab") }?.forEach { aab ->
|
||||
aab.copyTo(File(dstDir, "$base-$buildTypeName.aab"), overwrite = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lint {
|
||||
disable += "InvalidFragmentVersionForActivityResult"
|
||||
}
|
||||
@@ -121,4 +190,9 @@ dependencies {
|
||||
implementation(libs.google.mlkit)
|
||||
implementation(libs.androidx.datastore)
|
||||
implementation(libs.androidx.biometric)
|
||||
|
||||
playImplementation(project(":billing"))
|
||||
}
|
||||
|
||||
fun DependencyHandler.playImplementation(dependency: Any): Dependency? =
|
||||
add("playImplementation", dependency)
|
||||
@@ -1,11 +1,12 @@
|
||||
[versions]
|
||||
agp = "8.5.2"
|
||||
kotlin = "1.9.24"
|
||||
agp = "8.6.1"
|
||||
kotlin = "2.1.0"
|
||||
android-billing = "8.0.0"
|
||||
androidx-core = "1.13.1"
|
||||
androidx-activity = "1.9.1"
|
||||
androidx-annotation = "1.8.2"
|
||||
androidx-biometric = "1.2.0-alpha05"
|
||||
androidx-camera = "1.3.4"
|
||||
androidx-camera = "1.5.3"
|
||||
androidx-fragment = "1.8.2"
|
||||
androidx-security-crypto = "1.1.0-alpha06"
|
||||
androidx-datastore = "1.1.1"
|
||||
@@ -14,6 +15,7 @@ kotlinx-serialization = "1.6.3"
|
||||
google-mlkit = "17.3.0"
|
||||
|
||||
[libraries]
|
||||
android-billing = { module = "com.android.billingclient:billing-ktx", version.ref = "android-billing" }
|
||||
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
|
||||
androidx-activity = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity" }
|
||||
androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "androidx-annotation" }
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.amnezia.vpn
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
|
||||
class BillingPaymentRepository(@Suppress("UNUSED_PARAMETER") context: Context) : BillingRepository {
|
||||
override suspend fun getCountryCode(): String = ""
|
||||
override suspend fun getSubscriptionPlans(): String = ""
|
||||
override suspend fun purchaseSubscription(activity: Activity, offerToken: String): String = ""
|
||||
override suspend fun upgradeSubscription(activity: Activity, offerToken: String, oldPurchaseToken: String): String = ""
|
||||
override suspend fun acknowledge(purchaseToken: String): String = ""
|
||||
override suspend fun queryPurchases(): String = ""
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.amnezia.vpn
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import BillingProvider.Companion.withBillingProvider
|
||||
|
||||
class BillingPaymentRepository(private val context: Context) : BillingRepository {
|
||||
|
||||
override suspend fun getCountryCode(): String = withBillingProvider(context) {
|
||||
getCustomerCountryCode()
|
||||
}
|
||||
|
||||
override suspend fun getSubscriptionPlans(): String = withBillingProvider(context) {
|
||||
getSubscriptionPlans()
|
||||
}
|
||||
|
||||
override suspend fun purchaseSubscription(activity: Activity, offerToken: String): String =
|
||||
withBillingProvider(context) {
|
||||
purchaseSubscription(activity, offerToken)
|
||||
}
|
||||
|
||||
override suspend fun upgradeSubscription(activity: Activity, offerToken: String, oldPurchaseToken: String): String =
|
||||
withBillingProvider(context) {
|
||||
purchaseSubscription(activity, offerToken, oldPurchaseToken)
|
||||
}
|
||||
|
||||
override suspend fun acknowledge(purchaseToken: String): String = withBillingProvider(context) {
|
||||
acknowledge(purchaseToken)
|
||||
}
|
||||
|
||||
override suspend fun queryPurchases(): String = withBillingProvider(context) {
|
||||
getPurchases()
|
||||
}
|
||||
}
|
||||
@@ -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: 6.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
@@ -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>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#0E0E11</color>
|
||||
</resources>
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="black">#FF0E0E11</color>
|
||||
<color name="black">#FF101012</color>
|
||||
<style name="NoActionBar">
|
||||
<item name="android:windowBackground">@color/black</item>
|
||||
<item name="android:colorBackground">@color/black</item>
|
||||
|
||||
@@ -26,11 +26,11 @@ plugins {
|
||||
id("settings-property-delegate")
|
||||
}
|
||||
|
||||
rootProject.name = "AmneziaVPN"
|
||||
rootProject.buildFileName = "build.gradle.kts"
|
||||
|
||||
include(":qt")
|
||||
include(":utils")
|
||||
include(":billing")
|
||||
include(":protocolApi")
|
||||
include(":wireguard")
|
||||
include(":awg")
|
||||
@@ -47,15 +47,7 @@ val qtMinSdkVersion: String by gradleProperties
|
||||
// set default values for all modules
|
||||
configure<SettingsExtension> {
|
||||
buildToolsVersion = androidBuildToolsVersion
|
||||
compileSdk = androidCompileSdkVersion.substringAfter('-').toInt()
|
||||
compileSdk = androidCompileSdkVersion.split('-')[1].toInt()
|
||||
minSdk = qtMinSdkVersion.toInt()
|
||||
ndkVersion = androidNdkVersion
|
||||
}
|
||||
|
||||
// stop Gradle running by androiddeployqt
|
||||
gradle.taskGraph.whenReady {
|
||||
if (providers.environmentVariable("ANDROIDDEPLOYQT_RUN").isPresent
|
||||
&& !providers.systemProperty("explicitRun").isPresent) {
|
||||
allTasks.forEach { it.enabled = false }
|
||||
}
|
||||
}
|
||||
@@ -37,12 +37,15 @@ import android.widget.Toast
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.OnApplyWindowInsetsListener
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import kotlin.LazyThreadSafetyMode.NONE
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.text.RegexOption.IGNORE_CASE
|
||||
@@ -55,7 +58,6 @@ import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.amnezia.vpn.protocol.getStatistics
|
||||
import org.amnezia.vpn.protocol.getStatus
|
||||
import org.amnezia.vpn.qt.QtAndroidController
|
||||
@@ -77,6 +79,7 @@ private const val CHECK_NOTIFICATION_PERMISSION_ACTION_CODE = 4
|
||||
private const val PREFS_NOTIFICATION_PERMISSION_ASKED = "NOTIFICATION_PERMISSION_ASKED"
|
||||
private const val OPEN_FILE_AFTER_RESUME_DELAY_MS = 400L
|
||||
private const val KEY_PENDING_OPEN_FILE_URI = "pending_open_file_uri"
|
||||
private const val FILE_PROVIDER_AUTHORITY = "org.amnezia.vpn.qtprovider"
|
||||
|
||||
class AmneziaActivity : QtActivity() {
|
||||
|
||||
@@ -89,6 +92,7 @@ class AmneziaActivity : QtActivity() {
|
||||
private var notificationStateReceiver: BroadcastReceiver? = null
|
||||
private lateinit var vpnServiceMessenger: IpcMessenger
|
||||
private var pfd: ParcelFileDescriptor? = null
|
||||
private lateinit var billingRepository: BillingRepository
|
||||
|
||||
private val actionResultHandlers = mutableMapOf<Int, ActivityResultHandler>()
|
||||
private val permissionRequestHandlers = mutableMapOf<Int, PermissionRequestHandler>()
|
||||
@@ -99,6 +103,7 @@ class AmneziaActivity : QtActivity() {
|
||||
private var pendingOpenFileUri: String? = null
|
||||
private var openFileDeliveryScheduled = false
|
||||
|
||||
|
||||
private val vpnServiceEventHandler: Handler by lazy(NONE) {
|
||||
object : Handler(Looper.getMainLooper()) {
|
||||
override fun handleMessage(msg: Message) {
|
||||
@@ -205,6 +210,7 @@ class AmneziaActivity : QtActivity() {
|
||||
registerBroadcastReceivers()
|
||||
intent?.let(::processIntent)
|
||||
runBlocking { vpnProto = proto.await() }
|
||||
billingRepository = BillingPaymentRepository(applicationContext)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
@@ -214,10 +220,7 @@ class AmneziaActivity : QtActivity() {
|
||||
|
||||
private fun loadLibs() {
|
||||
listOf(
|
||||
"rsapss",
|
||||
"crypto_3",
|
||||
"ssl_3",
|
||||
"ssh"
|
||||
"rsapss"
|
||||
).forEach {
|
||||
loadSharedLibrary(this.applicationContext, it)
|
||||
}
|
||||
@@ -733,28 +736,69 @@ class AmneziaActivity : QtActivity() {
|
||||
@Suppress("unused")
|
||||
fun saveFile(fileName: String, data: String) {
|
||||
Log.d(TAG, "Save file $fileName")
|
||||
createFile(fileName, "text/*") { os -> os.bufferedWriter().use { it.write(data) } }
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun saveFileBytes(fileName: String, data: ByteArray, mime: String) {
|
||||
Log.d(TAG, "Save file $fileName, ${data.size} bytes, $mime")
|
||||
createFile(fileName, mime) { os -> os.write(data) }
|
||||
}
|
||||
|
||||
private fun createFile(fileName: String, mime: String, write: (OutputStream) -> Unit) {
|
||||
mainScope.launch {
|
||||
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "text/*"
|
||||
type = mime
|
||||
putExtra(Intent.EXTRA_TITLE, fileName)
|
||||
}.also {
|
||||
try {
|
||||
startActivityForResult(it, CREATE_FILE_ACTION_CODE, ActivityResultHandler(
|
||||
onSuccess = {
|
||||
it?.data?.let { uri ->
|
||||
val uri = it?.data
|
||||
var saved = false
|
||||
if (uri != null) {
|
||||
Log.v(TAG, "Save file to $uri")
|
||||
try {
|
||||
contentResolver.openOutputStream(uri)?.use { os ->
|
||||
os.bufferedWriter().use { it.write(data) }
|
||||
write(os)
|
||||
saved = true
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Failed to save file $uri: $e")
|
||||
// todo: send error to Qt
|
||||
}
|
||||
}
|
||||
QtAndroidController.onFileSaved(saved, false)
|
||||
},
|
||||
onFail = {
|
||||
QtAndroidController.onFileSaved(false, true)
|
||||
}
|
||||
))
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
Toast.makeText(this@AmneziaActivity, "Unsupported", Toast.LENGTH_LONG).show()
|
||||
QtAndroidController.onFileSaved(false, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun shareFile(path: String, mime: String) {
|
||||
Log.d(TAG, "Share file $path, $mime")
|
||||
mainScope.launch {
|
||||
val uri = try {
|
||||
FileProvider.getUriForFile(this@AmneziaActivity, FILE_PROVIDER_AUTHORITY, File(path))
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log.e(TAG, "Failed to share file $path: $e")
|
||||
return@launch
|
||||
}
|
||||
Intent(Intent.ACTION_SEND).apply {
|
||||
type = mime
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}.also {
|
||||
try {
|
||||
startActivity(Intent.createChooser(it, null))
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
Toast.makeText(this@AmneziaActivity, "Unsupported", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
@@ -766,7 +810,13 @@ class AmneziaActivity : QtActivity() {
|
||||
fun openFile(filter: String?) {
|
||||
Log.v(TAG, "Open file with filter: $filter")
|
||||
mainScope.launch {
|
||||
val intent = if (!isOnTv()) {
|
||||
val systemPickerPackage = listOf("com.google.android.documentsui", "com.android.documentsui")
|
||||
.firstOrNull { pkg ->
|
||||
try { packageManager.getPackageInfo(pkg, 0); true }
|
||||
catch (_: PackageManager.NameNotFoundException) { false }
|
||||
}
|
||||
|
||||
val intent = if (!isOnTv() && systemPickerPackage != null) {
|
||||
val mimeTypes = if (!filter.isNullOrEmpty()) {
|
||||
val extensionRegex = "\\*\\.([a-z0-9]+)".toRegex(IGNORE_CASE)
|
||||
val mime = MimeTypeMap.getSingleton()
|
||||
@@ -792,32 +842,48 @@ class AmneziaActivity : QtActivity() {
|
||||
else -> type = "*/*"
|
||||
}
|
||||
}
|
||||
`package` = systemPickerPackage
|
||||
if (resolveActivity(packageManager) == null) {
|
||||
`package` = null
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Intent(this@AmneziaActivity, TvFilePicker::class.java)
|
||||
}
|
||||
|
||||
try {
|
||||
startActivityForResult(intent, OPEN_FILE_ACTION_CODE, ActivityResultHandler(
|
||||
onAny = {
|
||||
if (isOnTv() && it?.hasExtra("activityNotFound") == true) {
|
||||
showNoFileBrowserAlertDialog()
|
||||
val resultHandler = ActivityResultHandler(
|
||||
onAny = {
|
||||
if (isOnTv() && it?.hasExtra("activityNotFound") == true) {
|
||||
showNoFileBrowserAlertDialog()
|
||||
}
|
||||
val uri = it?.data?.let { u ->
|
||||
if (u.scheme == "content") {
|
||||
try { grantUriPermission(packageName, u, Intent.FLAG_GRANT_READ_URI_PERMISSION) } catch (_: Exception) {}
|
||||
}
|
||||
val uri = it?.data?.apply {
|
||||
grantUriPermission(packageName, this, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}?.toString() ?: ""
|
||||
Log.v(TAG, "Open file: $uri")
|
||||
if (uri.isNotEmpty()) {
|
||||
pendingOpenFileUri = uri
|
||||
} else {
|
||||
mainScope.launch {
|
||||
qtInitialized.await()
|
||||
QtAndroidController.onFileOpened(uri)
|
||||
}
|
||||
u
|
||||
}?.toString() ?: ""
|
||||
Log.v(TAG, "Open file: $uri")
|
||||
if (uri.isNotEmpty()) {
|
||||
pendingOpenFileUri = uri
|
||||
} else {
|
||||
mainScope.launch {
|
||||
qtInitialized.await()
|
||||
QtAndroidController.onFileOpened(uri)
|
||||
}
|
||||
}
|
||||
))
|
||||
}
|
||||
)
|
||||
|
||||
try {
|
||||
startActivityForResult(intent, OPEN_FILE_ACTION_CODE, resultHandler)
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
if (intent.`package` != null) {
|
||||
intent.`package` = null
|
||||
try {
|
||||
startActivityForResult(intent, OPEN_FILE_ACTION_CODE, resultHandler)
|
||||
return@launch
|
||||
} catch (_: ActivityNotFoundException) {}
|
||||
}
|
||||
showNoFileBrowserAlertDialog()
|
||||
mainScope.launch {
|
||||
qtInitialized.await()
|
||||
@@ -844,7 +910,12 @@ class AmneziaActivity : QtActivity() {
|
||||
Log.v(TAG, "Get fd for $fileName")
|
||||
return blockingCall(Dispatchers.IO) {
|
||||
try {
|
||||
pfd = contentResolver.openFileDescriptor(Uri.parse(fileName), "r")
|
||||
val uri = Uri.parse(fileName)
|
||||
pfd = if (uri.scheme == "file") {
|
||||
ParcelFileDescriptor.open(File(uri.path!!), ParcelFileDescriptor.MODE_READ_ONLY)
|
||||
} else {
|
||||
contentResolver.openFileDescriptor(uri, "r")
|
||||
}
|
||||
pfd?.fd ?: -1
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to get fd: $e")
|
||||
@@ -944,20 +1015,6 @@ class AmneziaActivity : QtActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun exportLogsFile(fileName: String) {
|
||||
Log.v(TAG, "Export logs file")
|
||||
saveFile(fileName, Log.getLogs())
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun clearLogs() {
|
||||
Log.v(TAG, "Clear logs")
|
||||
mainScope.launch {
|
||||
Log.clearLogs()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun setScreenshotsEnabled(enabled: Boolean) {
|
||||
Log.v(TAG, "Set screenshots enabled: $enabled")
|
||||
@@ -986,15 +1043,9 @@ class AmneziaActivity : QtActivity() {
|
||||
@Suppress("unused")
|
||||
fun getAppList(): String {
|
||||
Log.v(TAG, "Get app list")
|
||||
var appList = ""
|
||||
runBlocking {
|
||||
mainScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
appList = AppListProvider.getAppList(packageManager, packageName)
|
||||
}
|
||||
}.join()
|
||||
return blockingCall(Dispatchers.IO) {
|
||||
AppListProvider.getAppList(packageManager, packageName)
|
||||
}
|
||||
return appList
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@@ -1064,12 +1115,10 @@ class AmneziaActivity : QtActivity() {
|
||||
@Suppress("unused")
|
||||
fun sendTouch(x: Float, y: Float) {
|
||||
Log.v(TAG, "Send touch: $x, $y")
|
||||
blockingCall {
|
||||
findQtWindow(window.decorView)?.let {
|
||||
Log.v(TAG, "Send touch to $it")
|
||||
it.dispatchTouchEvent(createEvent(x, y, SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN))
|
||||
it.dispatchTouchEvent(createEvent(x, y, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP))
|
||||
}
|
||||
findQtWindow(window.decorView)?.let {
|
||||
Log.v(TAG, "Send touch to $it")
|
||||
it.dispatchTouchEvent(createEvent(x, y, SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN))
|
||||
it.dispatchTouchEvent(createEvent(x, y, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1165,11 +1214,48 @@ class AmneziaActivity : QtActivity() {
|
||||
return super.dispatchTrackballEvent(ev)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun isPlay(): Boolean = BuildConfig.FLAVOR == "play"
|
||||
|
||||
@Suppress("unused")
|
||||
fun getCountryCode(): String {
|
||||
return blockingCall { billingRepository.getCountryCode() }
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun getSubscriptionPlans(): String {
|
||||
return blockingCall { billingRepository.getSubscriptionPlans() }
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun purchaseSubscription(offerToken: String): String {
|
||||
return blockingCall { billingRepository.purchaseSubscription(this@AmneziaActivity, offerToken) }
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun upgradeSubscription(offerToken: String, oldPurchaseToken: String): String {
|
||||
Log.v(TAG, "Upgrade subscription")
|
||||
return blockingCall {
|
||||
billingRepository.upgradeSubscription(this@AmneziaActivity, offerToken, oldPurchaseToken)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun acknowledgePurchase(purchaseToken: String): String {
|
||||
Log.v(TAG, "Acknowledge purchase")
|
||||
return blockingCall { billingRepository.acknowledge(purchaseToken) }
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun queryPurchases(): String {
|
||||
return blockingCall { billingRepository.queryPurchases() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Utils methods
|
||||
*/
|
||||
private fun <T> blockingCall(
|
||||
context: CoroutineContext = Dispatchers.Main.immediate,
|
||||
context: CoroutineContext = Dispatchers.Default,
|
||||
block: suspend () -> T
|
||||
) = runBlocking {
|
||||
mainScope.async(context) { block() }.await()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.amnezia.vpn
|
||||
|
||||
import android.system.Os
|
||||
import androidx.camera.camera2.Camera2Config
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.camera.core.CameraXConfig
|
||||
@@ -12,6 +13,9 @@ private const val TAG = "AmneziaApplication"
|
||||
class AmneziaApplication : QtApplication(), CameraXConfig.Provider {
|
||||
|
||||
override fun onCreate() {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Os.setenv("QT_ANDROID_DEBUGGER_MAIN_THREAD_SLEEP_MS", "0", true)
|
||||
}
|
||||
super.onCreate()
|
||||
Prefs.init(this)
|
||||
Log.init(this)
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.amnezia.vpn
|
||||
|
||||
import android.app.Activity
|
||||
|
||||
interface BillingRepository {
|
||||
suspend fun getCountryCode(): String
|
||||
suspend fun getSubscriptionPlans(): String
|
||||
suspend fun purchaseSubscription(activity: Activity, offerToken: String): String
|
||||
suspend fun upgradeSubscription(activity: Activity, offerToken: String, oldPurchaseToken: String): String
|
||||
suspend fun acknowledge(purchaseToken: String): String
|
||||
suspend fun queryPurchases(): String
|
||||
}
|
||||
@@ -1,30 +1,36 @@
|
||||
package org.amnezia.vpn
|
||||
|
||||
import android.Manifest
|
||||
import android.app.AlertDialog
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import org.amnezia.vpn.util.Log
|
||||
import java.io.File
|
||||
|
||||
private const val TAG = "TvFilePicker"
|
||||
private const val READ_STORAGE_REQUEST_CODE = 1001
|
||||
|
||||
class TvFilePicker : ComponentActivity() {
|
||||
|
||||
private val fileChooseResultLauncher = registerForActivityResult(object : ActivityResultContracts.OpenDocument() {
|
||||
// SAF launcher for Android 10+ where File API is blocked by scoped storage
|
||||
private val safLauncher = registerForActivityResult(object : ActivityResultContracts.OpenDocument() {
|
||||
override fun createIntent(context: Context, input: Array<String>): Intent {
|
||||
val intent = super.createIntent(context, input)
|
||||
|
||||
val activitiesToResolveIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val activities = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
context.packageManager.queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()))
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
context.packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
|
||||
}
|
||||
if (activitiesToResolveIntent.all {
|
||||
if (activities.all {
|
||||
val name = it.activityInfo.packageName
|
||||
name.startsWith("com.google.android.tv.frameworkpackagestubs") || name.startsWith("com.android.tv.frameworkpackagestubs")
|
||||
}) {
|
||||
@@ -32,38 +38,140 @@ class TvFilePicker : ComponentActivity() {
|
||||
}
|
||||
return intent
|
||||
}
|
||||
}) {
|
||||
}) { uri ->
|
||||
setResult(RESULT_OK, Intent().apply {
|
||||
data = it
|
||||
data = uri
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
})
|
||||
finish()
|
||||
}
|
||||
|
||||
private val directoryStack = ArrayDeque<File>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Log.v(TAG, "onCreate")
|
||||
getFile()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
launchSaf()
|
||||
} else {
|
||||
checkPermissionAndBrowse()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
Log.v(TAG, "onNewIntent")
|
||||
getFile()
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onBackPressed() {
|
||||
navigateBack()
|
||||
}
|
||||
|
||||
private fun getFile() {
|
||||
private fun launchSaf() {
|
||||
try {
|
||||
Log.v(TAG, "getFile")
|
||||
fileChooseResultLauncher.launch(arrayOf("*/*"))
|
||||
safLauncher.launch(arrayOf("*/*"))
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
Log.w(TAG, "Activity not found")
|
||||
Log.w(TAG, "No SAF activity found")
|
||||
setResult(RESULT_CANCELED, Intent().apply { putExtra("activityNotFound", true) })
|
||||
finish()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to get file: $e")
|
||||
Log.e(TAG, "SAF launch failed: $e")
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkPermissionAndBrowse() {
|
||||
if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||
requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), READ_STORAGE_REQUEST_CODE)
|
||||
} else {
|
||||
showRootDirectory()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
if (requestCode == READ_STORAGE_REQUEST_CODE &&
|
||||
grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
|
||||
showRootDirectory()
|
||||
} else {
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showRootDirectory() {
|
||||
@Suppress("DEPRECATION")
|
||||
val primaryExternal = Environment.getExternalStorageDirectory()
|
||||
val storageDir = File("/storage")
|
||||
// Pre-seed stack with /storage so Back from primary storage goes there (USB drives etc.)
|
||||
if (storageDir.exists() && storageDir.canonicalPath != primaryExternal.canonicalPath) {
|
||||
directoryStack.addLast(storageDir)
|
||||
}
|
||||
showDirectory(primaryExternal)
|
||||
}
|
||||
|
||||
private fun navigateBack() {
|
||||
if (directoryStack.size > 1) {
|
||||
directoryStack.removeLast()
|
||||
val parent = directoryStack.removeLast()
|
||||
showDirectory(parent)
|
||||
} else {
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showDirectory(dir: File) {
|
||||
directoryStack.addLast(dir)
|
||||
Log.v(TAG, "Showing directory: ${dir.absolutePath}")
|
||||
|
||||
val entries = try {
|
||||
dir.listFiles()
|
||||
?.sortedWith(compareBy({ !it.isDirectory }, { it.name.lowercase() }))
|
||||
?: emptyList()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to list directory: $e")
|
||||
emptyList()
|
||||
}
|
||||
|
||||
val names = entries.map { if (it.isDirectory) "[${it.name}]" else it.name }.toTypedArray()
|
||||
|
||||
val builder = AlertDialog.Builder(this)
|
||||
.setTitle(dir.absolutePath)
|
||||
|
||||
if (entries.isEmpty()) {
|
||||
builder.setMessage("No files available")
|
||||
} else {
|
||||
builder.setItems(names) { dialog, which ->
|
||||
dialog.dismiss()
|
||||
val selected = entries[which]
|
||||
if (selected.isDirectory) {
|
||||
showDirectory(selected)
|
||||
} else {
|
||||
Log.v(TAG, "Selected file: ${selected.absolutePath}")
|
||||
setResult(RESULT_OK, Intent().apply {
|
||||
data = Uri.fromFile(selected)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
})
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (directoryStack.size > 1) {
|
||||
builder.setNegativeButton("↑ Back") { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
navigateBack()
|
||||
}
|
||||
} else {
|
||||
builder.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
builder.setOnCancelListener {
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
|
||||
builder.show()
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ object QtAndroidController {
|
||||
external fun onStatisticsUpdate(rxBytes: Long, txBytes: Long)
|
||||
|
||||
external fun onFileOpened(uri: String)
|
||||
external fun onFileSaved(success: Boolean, cancelled: Boolean)
|
||||
|
||||
external fun onConfigImported(data: String)
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.amnezia.vpn.util
|
||||
|
||||
// keep synchronized with client/core/utils/errorCodes.h amnezia::ErrorCode
|
||||
object ErrorCode {
|
||||
const val NoError = 0
|
||||
|
||||
const val BillingCanceled = 1300
|
||||
const val BillingError = 1301
|
||||
const val BillingGooglePlayError = 1302
|
||||
const val BillingUnavailable = 1303
|
||||
const val SubscriptionAlreadyOwned = 1304
|
||||
const val SubscriptionUnavailable = 1305
|
||||
const val BillingNetworkError = 1306
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.amnezia.vpn.util
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Process
|
||||
@@ -22,9 +23,18 @@ import org.amnezia.vpn.util.Log.Priority.W
|
||||
import android.util.Log as NativeLog
|
||||
|
||||
private const val TAG = "Log"
|
||||
private const val LOG_FILE_NAME = "amneziaVPN.log"
|
||||
private const val ROTATE_LOG_FILE_NAME = "amneziaVPN.rotate.log"
|
||||
private const val APP_LOG_FILE_NAME = "app.log"
|
||||
private const val APP_ROTATE_LOG_FILE_NAME = "app.rotate.log"
|
||||
private const val TUNNEL_LOG_FILE_NAME = "tunnel.log"
|
||||
private const val TUNNEL_ROTATE_LOG_FILE_NAME = "tunnel.rotate.log"
|
||||
private const val LEGACY_LOG_FILE_NAME = "amneziaVPN.log"
|
||||
private const val LEGACY_ROTATE_LOG_FILE_NAME = "amneziaVPN.rotate.log"
|
||||
private const val LOCK_FILE_NAME = ".lock"
|
||||
private const val TILE_SERVICE_PROCESS_SUFFIX = ":amneziaTileService"
|
||||
private const val AWG_SERVICE_PROCESS_SUFFIX = ":amneziaAwgService"
|
||||
private const val AWG_LOGCAT_TAG_PREFIX = " AmneziaWG/"
|
||||
private const val STREAM_APP = 0
|
||||
private const val STREAM_TUNNEL = 1
|
||||
private const val DATE_TIME_PATTERN = "MM-dd HH:mm:ss.SSS"
|
||||
private const val PREFS_SAVE_LOGS_KEY = "SAVE_LOGS"
|
||||
private const val LOG_MAX_FILE_SIZE = 1024 * 1024
|
||||
@@ -42,12 +52,16 @@ object Log {
|
||||
private val dateTimeFormat: DateTimeFormatter = DateTimeFormatter.ofPattern(DATE_TIME_PATTERN)
|
||||
|
||||
private lateinit var logDir: File
|
||||
private val logFile: File by lazy { File(logDir, LOG_FILE_NAME) }
|
||||
private val rotateLogFile: File by lazy { File(logDir, ROTATE_LOG_FILE_NAME) }
|
||||
private lateinit var logFile: File
|
||||
private lateinit var rotateLogFile: File
|
||||
|
||||
private val fileLock: FileChannel by lazy { RandomAccessFile(File(logDir, LOCK_FILE_NAME).path, "rw").channel }
|
||||
private val threadLock: ReentrantLock by lazy { ReentrantLock() }
|
||||
|
||||
private var isAwgProcess: Boolean = false
|
||||
private val awgLogcatLock = Any()
|
||||
private var awgLogcatReader: AwgLogcatReader? = null
|
||||
|
||||
@Volatile
|
||||
private var _saveLogs: Boolean = false
|
||||
var saveLogs: Boolean
|
||||
@@ -60,6 +74,9 @@ object Log {
|
||||
}
|
||||
_saveLogs = value
|
||||
Prefs.save(PREFS_SAVE_LOGS_KEY, value)
|
||||
if (isAwgProcess) {
|
||||
if (value) startAwgLogcatReader() else stopAwgLogcatReader()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,21 +113,60 @@ object Log {
|
||||
fun init(context: Context) {
|
||||
v(TAG, "Init Log")
|
||||
logDir = File(context.cacheDir, "logs")
|
||||
val processName = Application.getProcessName()
|
||||
val stream = if (':' in processName && !processName.endsWith(TILE_SERVICE_PROCESS_SUFFIX)) {
|
||||
STREAM_TUNNEL
|
||||
} else {
|
||||
STREAM_APP
|
||||
}
|
||||
val (rotate, current) = streamFiles(stream)
|
||||
rotateLogFile = rotate
|
||||
logFile = current
|
||||
isAwgProcess = processName.endsWith(AWG_SERVICE_PROCESS_SUFFIX)
|
||||
saveLogs = Prefs.load(PREFS_SAVE_LOGS_KEY)
|
||||
}
|
||||
|
||||
fun getLogs(): String =
|
||||
"${deviceInfo()}\n${readLogs()}\nLOGCAT:\n${getLogcat()}"
|
||||
@JvmStatic
|
||||
fun getLogFiles(stream: Int): String {
|
||||
val files = streamFiles(stream).toMutableList()
|
||||
if (stream == STREAM_APP) files.addAll(0, legacyFiles().filter { it.exists() })
|
||||
return files.joinToString("\n") { it.absolutePath }
|
||||
}
|
||||
|
||||
fun clearLogs() {
|
||||
@JvmStatic
|
||||
fun getDeviceInfo(): String {
|
||||
val sb = StringBuilder()
|
||||
sb.append("Model: ").appendLine(Build.MODEL)
|
||||
sb.append("Brand: ").appendLine(Build.BRAND)
|
||||
sb.append("Product: ").appendLine(Build.PRODUCT)
|
||||
sb.append("Device: ").appendLine(Build.DEVICE)
|
||||
sb.append("Codename: ").appendLine(Build.VERSION.CODENAME)
|
||||
sb.append("Release: ").appendLine(Build.VERSION.RELEASE)
|
||||
sb.append("SDK: ").appendLine(Build.VERSION.SDK_INT)
|
||||
sb.append("ABI: ").appendLine(Build.SUPPORTED_ABIS.joinToString())
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun clearStream(stream: Int) {
|
||||
if (logDir.exists()) {
|
||||
withLock {
|
||||
logFile.delete()
|
||||
rotateLogFile.delete()
|
||||
streamFiles(stream).forEach { it.delete() }
|
||||
if (stream == STREAM_APP) legacyFiles().forEach { it.delete() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun streamFiles(stream: Int): List<File> =
|
||||
if (stream == STREAM_TUNNEL) {
|
||||
listOf(File(logDir, TUNNEL_ROTATE_LOG_FILE_NAME), File(logDir, TUNNEL_LOG_FILE_NAME))
|
||||
} else {
|
||||
listOf(File(logDir, APP_ROTATE_LOG_FILE_NAME), File(logDir, APP_LOG_FILE_NAME))
|
||||
}
|
||||
|
||||
private fun legacyFiles(): List<File> =
|
||||
listOf(File(logDir, LEGACY_ROTATE_LOG_FILE_NAME), File(logDir, LEGACY_LOG_FILE_NAME))
|
||||
|
||||
private fun log(tag: String, msg: String, priority: Priority) {
|
||||
if (saveLogs && priority != V) saveLogMsg(formatLogMsg(tag, msg, priority))
|
||||
|
||||
@@ -136,51 +192,85 @@ object Log {
|
||||
}
|
||||
}
|
||||
|
||||
private fun startAwgLogcatReader() {
|
||||
synchronized(awgLogcatLock) {
|
||||
if (awgLogcatReader == null) {
|
||||
awgLogcatReader = AwgLogcatReader().also { it.start() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopAwgLogcatReader() {
|
||||
synchronized(awgLogcatLock) {
|
||||
awgLogcatReader?.shutdown()
|
||||
awgLogcatReader = null
|
||||
}
|
||||
}
|
||||
|
||||
private class AwgLogcatReader : Thread("AwgLogcatReader") {
|
||||
private val lock = Any()
|
||||
private var process: java.lang.Process? = null
|
||||
@Volatile
|
||||
private var stopped = false
|
||||
|
||||
init {
|
||||
isDaemon = true
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
try {
|
||||
follow()
|
||||
} finally {
|
||||
synchronized(awgLogcatLock) {
|
||||
if (awgLogcatReader === this) awgLogcatReader = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun follow() {
|
||||
val logcat = try {
|
||||
ProcessBuilder(
|
||||
"logcat", "-v", "threadtime", "-v", "UTC", "--pid=${Process.myPid()}", "-T", "1"
|
||||
).redirectErrorStream(true).start()
|
||||
} catch (e: IOException) {
|
||||
NativeLog.e(TAG, "Failed to start logcat: $e")
|
||||
return
|
||||
}
|
||||
synchronized(lock) {
|
||||
if (stopped) {
|
||||
logcat.destroy()
|
||||
return
|
||||
}
|
||||
process = logcat
|
||||
}
|
||||
try {
|
||||
logcat.inputStream.bufferedReader().useLines { lines ->
|
||||
lines.filter { it.contains(AWG_LOGCAT_TAG_PREFIX) }.forEach {
|
||||
if (!stopped) saveLogMsg("$it\n")
|
||||
}
|
||||
}
|
||||
if (!stopped) NativeLog.w(TAG, "Logcat exited unexpectedly")
|
||||
} catch (e: IOException) {
|
||||
if (!stopped) NativeLog.e(TAG, "Failed to read logcat: $e")
|
||||
} finally {
|
||||
logcat.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
fun shutdown() {
|
||||
synchronized(lock) {
|
||||
stopped = true
|
||||
process?.destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatLogMsg(tag: String, msg: String, priority: Priority): String {
|
||||
val utcDate = ZonedDateTime.now(ZoneOffset.UTC).format(dateTimeFormat)
|
||||
return "${utcDate}Z ${Process.myPid()} ${Process.myTid()} $priority [${Thread.currentThread().name}] " +
|
||||
"$tag: $msg\n"
|
||||
}
|
||||
|
||||
private fun deviceInfo(): String {
|
||||
val sb = StringBuilder()
|
||||
sb.append("Model: ").appendLine(Build.MODEL)
|
||||
sb.append("Brand: ").appendLine(Build.BRAND)
|
||||
sb.append("Product: ").appendLine(Build.PRODUCT)
|
||||
sb.append("Device: ").appendLine(Build.DEVICE)
|
||||
sb.append("Codename: ").appendLine(Build.VERSION.CODENAME)
|
||||
sb.append("Release: ").appendLine(Build.VERSION.RELEASE)
|
||||
sb.append("SDK: ").appendLine(Build.VERSION.SDK_INT)
|
||||
sb.append("ABI: ").appendLine(Build.SUPPORTED_ABIS.joinToString())
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
private fun readLogs(): String {
|
||||
var logText = ""
|
||||
withLock {
|
||||
try {
|
||||
if (rotateLogFile.exists()) logText = rotateLogFile.readText()
|
||||
if (logFile.exists()) logText += logFile.readText()
|
||||
} catch (e: IOException) {
|
||||
val errorMsg = "Failed to read log: $e"
|
||||
NativeLog.e(TAG, errorMsg)
|
||||
logText += errorMsg
|
||||
}
|
||||
}
|
||||
return logText
|
||||
}
|
||||
|
||||
private fun getLogcat(): String {
|
||||
try {
|
||||
val process = ProcessBuilder("logcat", "-d").redirectErrorStream(true).start()
|
||||
return process.inputStream.reader().readText()
|
||||
} catch (e: IOException) {
|
||||
val errorMsg = "Failed to get logcat log: $e"
|
||||
NativeLog.e(TAG, errorMsg)
|
||||
return errorMsg
|
||||
}
|
||||
}
|
||||
|
||||
private fun withLock(block: () -> Unit) {
|
||||
threadLock.lock()
|
||||
try {
|
||||
|
||||
+19
-1
@@ -111,7 +111,7 @@ open class Wireguard : Protocol() {
|
||||
configExtensionParameters(configData)
|
||||
}
|
||||
|
||||
configData.optStringOrNull("persistent_keep_alive")?.let { setPersistentKeepalive(it.toInt()) }
|
||||
configData.optStringOrNull("persistent_keep_alive")?.let { setPersistentKeepalive(it) }
|
||||
configData.getString("client_priv_key").let { setPrivateKeyHex(it.base64ToHex()) }
|
||||
configData.getString("server_pub_key").let { setPublicKeyHex(it.base64ToHex()) }
|
||||
configData.optStringOrNull("psk_key")?.let { setPreSharedKeyHex(it.base64ToHex()) }
|
||||
@@ -134,6 +134,24 @@ open class Wireguard : Protocol() {
|
||||
configData.optStringOrNull("I3")?.let { setI3(it) }
|
||||
configData.optStringOrNull("I4")?.let { setI4(it) }
|
||||
configData.optStringOrNull("I5")?.let { setI5(it) }
|
||||
configData.optStringOrNull("HeaderProtectionKey")?.trim()?.takeIf { it.isNotEmpty() }
|
||||
?.let { setHeaderProtectionKey(it.base64ToHex()) }
|
||||
configData.optStringOrNull("ContentPaddingAddition")?.trim()?.takeIf { it.isNotEmpty() }
|
||||
?.let { setContentPaddingAddition(it) }
|
||||
configData.optStringOrNull("RekeyAfterTime")?.trim()?.takeIf { it.isNotEmpty() }
|
||||
?.let { setRekeyAfterTime(it) }
|
||||
configData.optStringOrNull("RekeyTimeout")?.trim()?.takeIf { it.isNotEmpty() }
|
||||
?.let { setRekeyTimeout(it) }
|
||||
configData.optStringOrNull("RejectAfterTime")?.trim()?.takeIf { it.isNotEmpty() }
|
||||
?.let { setRejectAfterTime(it) }
|
||||
configData.optStringOrNull("KeepaliveTimeout")?.trim()?.takeIf { it.isNotEmpty() }
|
||||
?.let { setKeepaliveTimeout(it) }
|
||||
configData.optStringOrNull("MaxHandshakeAttempts")?.trim()?.takeIf { it.isNotEmpty() }
|
||||
?.let { setMaxHandshakeAttempts(it) }
|
||||
configData.optStringOrNull("RandomTrailers")?.trim()?.takeIf { it.isNotEmpty() }
|
||||
?.let { setRandomTrailers(it) }
|
||||
configData.optStringOrNull("DisableCookies")?.trim()?.takeIf { it.isNotEmpty() }
|
||||
?.let { setDisableCookies(it) }
|
||||
}
|
||||
|
||||
private fun start(
|
||||
|
||||
+55
-4
@@ -10,7 +10,7 @@ private const val WIREGUARD_DEFAULT_MTU = 1280
|
||||
open class WireguardConfig protected constructor(
|
||||
protocolConfigBuilder: ProtocolConfig.Builder,
|
||||
val endpoint: InetEndpoint,
|
||||
val persistentKeepalive: Int,
|
||||
val persistentKeepalive: String?,
|
||||
val publicKeyHex: String,
|
||||
val preSharedKeyHex: String?,
|
||||
val privateKeyHex: String,
|
||||
@@ -31,6 +31,15 @@ open class WireguardConfig protected constructor(
|
||||
var i3: String?,
|
||||
var i4: String?,
|
||||
var i5: String?,
|
||||
val headerProtectionKeyHex: String?,
|
||||
val contentPaddingAddition: String?,
|
||||
val rekeyAfterTime: String?,
|
||||
val rekeyTimeout: String?,
|
||||
val rejectAfterTime: String?,
|
||||
val keepaliveTimeout: String?,
|
||||
val maxHandshakeAttempts: String?,
|
||||
val randomTrailers: String?,
|
||||
val disableCookies: String?,
|
||||
) : ProtocolConfig(protocolConfigBuilder) {
|
||||
|
||||
protected constructor(builder: Builder) : this(
|
||||
@@ -57,6 +66,15 @@ open class WireguardConfig protected constructor(
|
||||
builder.i3,
|
||||
builder.i4,
|
||||
builder.i5,
|
||||
builder.headerProtectionKeyHex,
|
||||
builder.contentPaddingAddition,
|
||||
builder.rekeyAfterTime,
|
||||
builder.rekeyTimeout,
|
||||
builder.rejectAfterTime,
|
||||
builder.keepaliveTimeout,
|
||||
builder.maxHandshakeAttempts,
|
||||
builder.randomTrailers,
|
||||
builder.disableCookies,
|
||||
)
|
||||
|
||||
fun toWgUserspaceString(): String = with(StringBuilder()) {
|
||||
@@ -87,6 +105,15 @@ open class WireguardConfig protected constructor(
|
||||
i4?.let { appendLine("i4=$it") }
|
||||
i5?.let { appendLine("i5=$it") }
|
||||
}
|
||||
headerProtectionKeyHex?.takeIf { it.isNotEmpty() }?.let { appendLine("header_protection_key=$it") }
|
||||
contentPaddingAddition?.takeIf { it.isNotEmpty() }?.let { appendLine("content_padding_addition=$it") }
|
||||
rekeyAfterTime?.takeIf { it.isNotEmpty() }?.let { appendLine("rekey_after_time=$it") }
|
||||
rekeyTimeout?.takeIf { it.isNotEmpty() }?.let { appendLine("rekey_timeout=$it") }
|
||||
rejectAfterTime?.takeIf { it.isNotEmpty() }?.let { appendLine("reject_after_time=$it") }
|
||||
keepaliveTimeout?.takeIf { it.isNotEmpty() }?.let { appendLine("keepalive_timeout=$it") }
|
||||
maxHandshakeAttempts?.takeIf { it.isNotEmpty() }?.let { appendLine("max_handshake_attempts=$it") }
|
||||
randomTrailers?.takeIf { it.isNotEmpty() }?.let { appendLine("random_trailers=${it.toUapiBool()}") }
|
||||
disableCookies?.takeIf { it.isNotEmpty() }?.let { appendLine("disable_cookies=${it.toUapiBool()}") }
|
||||
}
|
||||
|
||||
private fun validateProtocolExtensionParameters() {
|
||||
@@ -107,7 +134,7 @@ open class WireguardConfig protected constructor(
|
||||
appendLine("allowed_ip=${route.inetNetwork}")
|
||||
}
|
||||
appendLine("endpoint=$endpoint")
|
||||
if (persistentKeepalive != 0)
|
||||
if (!persistentKeepalive.isNullOrEmpty() && persistentKeepalive != "0")
|
||||
appendLine("persistent_keepalive_interval=$persistentKeepalive")
|
||||
if (preSharedKeyHex != null)
|
||||
appendLine("preshared_key=$preSharedKeyHex")
|
||||
@@ -117,7 +144,7 @@ open class WireguardConfig protected constructor(
|
||||
internal lateinit var endpoint: InetEndpoint
|
||||
private set
|
||||
|
||||
internal var persistentKeepalive: Int = 0
|
||||
internal var persistentKeepalive: String? = null
|
||||
private set
|
||||
|
||||
internal lateinit var publicKeyHex: String
|
||||
@@ -149,10 +176,19 @@ open class WireguardConfig protected constructor(
|
||||
internal var i3: String? = null
|
||||
internal var i4: String? = null
|
||||
internal var i5: String? = null
|
||||
internal var headerProtectionKeyHex: String? = null
|
||||
internal var contentPaddingAddition: String? = null
|
||||
internal var rekeyAfterTime: String? = null
|
||||
internal var rekeyTimeout: String? = null
|
||||
internal var rejectAfterTime: String? = null
|
||||
internal var keepaliveTimeout: String? = null
|
||||
internal var maxHandshakeAttempts: String? = null
|
||||
internal var randomTrailers: String? = null
|
||||
internal var disableCookies: String? = null
|
||||
|
||||
fun setEndpoint(endpoint: InetEndpoint) = apply { this.endpoint = endpoint }
|
||||
|
||||
fun setPersistentKeepalive(persistentKeepalive: Int) = apply { this.persistentKeepalive = persistentKeepalive }
|
||||
fun setPersistentKeepalive(persistentKeepalive: String) = apply { this.persistentKeepalive = persistentKeepalive }
|
||||
|
||||
fun setPublicKeyHex(publicKeyHex: String) = apply { this.publicKeyHex = publicKeyHex }
|
||||
|
||||
@@ -178,6 +214,15 @@ open class WireguardConfig protected constructor(
|
||||
fun setI3(i3: String) = apply { this.i3 = i3 }
|
||||
fun setI4(i4: String) = apply { this.i4 = i4 }
|
||||
fun setI5(i5: String) = apply { this.i5 = i5 }
|
||||
fun setHeaderProtectionKey(headerProtectionKeyHex: String) = apply { this.headerProtectionKeyHex = headerProtectionKeyHex }
|
||||
fun setContentPaddingAddition(contentPaddingAddition: String) = apply { this.contentPaddingAddition = contentPaddingAddition }
|
||||
fun setRekeyAfterTime(rekeyAfterTime: String) = apply { this.rekeyAfterTime = rekeyAfterTime }
|
||||
fun setRekeyTimeout(rekeyTimeout: String) = apply { this.rekeyTimeout = rekeyTimeout }
|
||||
fun setRejectAfterTime(rejectAfterTime: String) = apply { this.rejectAfterTime = rejectAfterTime }
|
||||
fun setKeepaliveTimeout(keepaliveTimeout: String) = apply { this.keepaliveTimeout = keepaliveTimeout }
|
||||
fun setMaxHandshakeAttempts(maxHandshakeAttempts: String) = apply { this.maxHandshakeAttempts = maxHandshakeAttempts }
|
||||
fun setRandomTrailers(randomTrailers: String) = apply { this.randomTrailers = randomTrailers }
|
||||
fun setDisableCookies(disableCookies: String) = apply { this.disableCookies = disableCookies }
|
||||
|
||||
override fun build(): WireguardConfig = configBuild().run { WireguardConfig(this@Builder) }
|
||||
}
|
||||
@@ -189,3 +234,9 @@ open class WireguardConfig protected constructor(
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
internal fun String.base64ToHex(): String = Base64.decode(this, Base64.DEFAULT).toHexString()
|
||||
|
||||
/** Converts awg-quick on/off (and 0/1/true/false) to UAPI 1/0 for amneziawg-go ParseBool. */
|
||||
internal fun String.toUapiBool(): String = when (trim().lowercase()) {
|
||||
"on", "1", "true", "t", "yes" -> "1"
|
||||
else -> "0"
|
||||
}
|
||||
@@ -86,10 +86,6 @@ class Xray : Protocol() {
|
||||
ensureInboundAuth(xrayJsonConfig)
|
||||
val xrayConfig = parseConfig(config, xrayJsonConfig)
|
||||
|
||||
(xrayJsonConfig.optJSONObject("log") ?: JSONObject().also { xrayJsonConfig.put("log", it) })
|
||||
.put("loglevel", "warning")
|
||||
.put("access", "none") // disable access log
|
||||
|
||||
var xrayJsonConfigString = xrayJsonConfig.toString()
|
||||
config.getString("hostName").let { hostName ->
|
||||
val ipAddress = parseInetAddress(hostName).ip
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<RCC>
|
||||
<qresource prefix="/client_scripts">
|
||||
<file>mac_installer.sh</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
|
||||
EXTRACT_DIR="$1"
|
||||
INSTALLER_PATH="$2"
|
||||
|
||||
set -e
|
||||
|
||||
echo "[AmneziaVPN] Installer package: $INSTALLER_PATH"
|
||||
|
||||
if [ ! -f "$INSTALLER_PATH" ]; then
|
||||
echo "[AmneziaVPN] ERROR: Installer package not found: $INSTALLER_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PKG_PATH="$INSTALLER_PATH"
|
||||
echo "[AmneziaVPN] Using PKG: $PKG_PATH"
|
||||
|
||||
# Optional: basic signature/gatekeeper checks (non-fatal)
|
||||
if command -v pkgutil >/dev/null 2>&1; then
|
||||
pkgutil --check-signature "$PKG_PATH" || true
|
||||
fi
|
||||
if command -v spctl >/dev/null 2>&1; then
|
||||
spctl -a -vvv -t install "$PKG_PATH" || true
|
||||
fi
|
||||
|
||||
# Run installer with admin privileges via AppleScript (prompts for password)
|
||||
echo "[AmneziaVPN] Running installer..."
|
||||
OSA_CMD='do shell script "/usr/sbin/installer -pkg '"$PKG_PATH"' -target /" with administrator privileges'
|
||||
osascript -e "$OSA_CMD"
|
||||
|
||||
STATUS=$?
|
||||
if [ $STATUS -ne 0 ]; then
|
||||
echo "[AmneziaVPN] ERROR: installer exited with status $STATUS"
|
||||
exit $STATUS
|
||||
fi
|
||||
|
||||
echo "[AmneziaVPN] Cleaning up..."
|
||||
rm -f "$INSTALLER_PATH" || true
|
||||
rm -rf "$EXTRACT_DIR" 2>/dev/null || true
|
||||
|
||||
echo "[AmneziaVPN] Installation completed successfully"
|
||||
exit 0
|
||||
+15
-82
@@ -2,100 +2,33 @@ set(CLIENT_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR}/..)
|
||||
|
||||
set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/Modules;${CMAKE_MODULE_PATH}")
|
||||
|
||||
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/SortFilterProxyModel)
|
||||
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/SortFilterProxyModel ${CMAKE_BINARY_DIR}/3rd/SortFilterProxyModel)
|
||||
set(LIBS ${LIBS} SortFilterProxyModel)
|
||||
include(${CLIENT_ROOT_DIR}/cmake/QSimpleCrypto.cmake)
|
||||
|
||||
include(${CLIENT_ROOT_DIR}/3rd/qrcodegen/qrcodegen.cmake)
|
||||
|
||||
set(LIBSSH_ROOT_DIR "${CLIENT_ROOT_DIR}/3rd-prebuilt/3rd-prebuilt/libssh/")
|
||||
set(OPENSSL_ROOT_DIR "${CLIENT_ROOT_DIR}/3rd-prebuilt/3rd-prebuilt/openssl/")
|
||||
|
||||
set(OPENSSL_LIBRARIES_DIR "${OPENSSL_ROOT_DIR}/lib")
|
||||
|
||||
if(WIN32)
|
||||
set(OPENSSL_INCLUDE_DIR "${OPENSSL_ROOT_DIR}/windows/include")
|
||||
if("${CMAKE_SIZEOF_VOID_P}" STREQUAL "8")
|
||||
set(LIBSSH_LIB_PATH "${LIBSSH_ROOT_DIR}/windows/x86_64/ssh.lib")
|
||||
set(LIBSSH_INCLUDE_DIR "${LIBSSH_ROOT_DIR}/windows/x86_64")
|
||||
set(OPENSSL_LIB_SSL_PATH "${OPENSSL_ROOT_DIR}/windows/win64/libssl.lib")
|
||||
set(OPENSSL_LIB_CRYPTO_PATH "${OPENSSL_ROOT_DIR}/windows/win64/libcrypto.lib")
|
||||
else()
|
||||
set(LIBSSH_LIB_PATH "${LIBSSH_ROOT_DIR}/windows/x86/ssh.lib")
|
||||
set(LIBSSH_INCLUDE_DIR "${LIBSSH_ROOT_DIR}/windows/x86")
|
||||
set(OPENSSL_LIB_SSL_PATH "${OPENSSL_ROOT_DIR}/windows/win32/libssl.lib")
|
||||
set(OPENSSL_LIB_CRYPTO_PATH "${OPENSSL_ROOT_DIR}/windows/win32/libcrypto.lib")
|
||||
endif()
|
||||
elseif(APPLE AND NOT IOS)
|
||||
if(MACOS_NE)
|
||||
set(LIBSSH_LIB_PATH "${LIBSSH_ROOT_DIR}/macos/universal2/libssh.a")
|
||||
set(ZLIB_LIB_PATH "${LIBSSH_ROOT_DIR}/macos/universal2/libz.a")
|
||||
set(LIBSSH_INCLUDE_DIR "${LIBSSH_ROOT_DIR}/macos/universal2")
|
||||
else()
|
||||
set(LIBSSH_LIB_PATH "${LIBSSH_ROOT_DIR}/macos/x86_64/libssh.a")
|
||||
set(ZLIB_LIB_PATH "${LIBSSH_ROOT_DIR}/macos/x86_64/libz.a")
|
||||
set(LIBSSH_INCLUDE_DIR "${LIBSSH_ROOT_DIR}/macos/x86_64")
|
||||
endif()
|
||||
set(OPENSSL_INCLUDE_DIR "${OPENSSL_ROOT_DIR}/macos/include")
|
||||
set(OPENSSL_LIB_SSL_PATH "${OPENSSL_ROOT_DIR}/macos/lib/libssl.a")
|
||||
set(OPENSSL_LIB_CRYPTO_PATH "${OPENSSL_ROOT_DIR}/macos/lib/libcrypto.a")
|
||||
elseif(IOS)
|
||||
set(LIBSSH_INCLUDE_DIR "${LIBSSH_ROOT_DIR}/ios/arm64")
|
||||
set(LIBSSH_LIB_PATH "${LIBSSH_ROOT_DIR}/ios/arm64/libssh.a")
|
||||
set(ZLIB_LIB_PATH "${LIBSSH_ROOT_DIR}/ios/arm64/libz.a")
|
||||
set(OPENSSL_INCLUDE_DIR "${OPENSSL_ROOT_DIR}/ios/iphone/include")
|
||||
set(OPENSSL_LIB_SSL_PATH "${OPENSSL_ROOT_DIR}/ios/iphone/lib/libssl.a")
|
||||
set(OPENSSL_LIB_CRYPTO_PATH "${OPENSSL_ROOT_DIR}/ios/iphone/lib/libcrypto.a")
|
||||
elseif(ANDROID)
|
||||
set(abi ${CMAKE_ANDROID_ARCH_ABI})
|
||||
set(LIBSSH_INCLUDE_DIR "${LIBSSH_ROOT_DIR}/android/${abi}")
|
||||
set(LIBSSH_LIB_PATH "${LIBSSH_ROOT_DIR}/android/${abi}/libssh.so")
|
||||
set(OPENSSL_INCLUDE_DIR "${OPENSSL_ROOT_DIR}/android/include")
|
||||
set(OPENSSL_LIB_SSL_PATH "${OPENSSL_ROOT_DIR}/android/${abi}/libssl.a")
|
||||
set(OPENSSL_LIB_CRYPTO_PATH "${OPENSSL_ROOT_DIR}/android/${abi}/libcrypto.a")
|
||||
set(OPENSSL_LIBRARIES_DIR "${OPENSSL_ROOT_DIR}/android/${abi}")
|
||||
elseif(LINUX)
|
||||
set(LIBSSH_INCLUDE_DIR "${LIBSSH_ROOT_DIR}/linux/x86_64")
|
||||
set(ZLIB_LIB_PATH "${LIBSSH_ROOT_DIR}/linux/x86_64/libz.a")
|
||||
set(LIBSSH_LIB_PATH "${LIBSSH_ROOT_DIR}/linux/x86_64/libssh.a")
|
||||
set(OPENSSL_INCLUDE_DIR "${OPENSSL_ROOT_DIR}/linux/include")
|
||||
set(OPENSSL_LIB_SSL_PATH "${OPENSSL_ROOT_DIR}/linux/x86_64/libssl.a")
|
||||
set(OPENSSL_LIB_CRYPTO_PATH "${OPENSSL_ROOT_DIR}/linux/x86_64/libcrypto.a")
|
||||
endif()
|
||||
|
||||
file(COPY ${OPENSSL_LIB_SSL_PATH} ${OPENSSL_LIB_CRYPTO_PATH}
|
||||
DESTINATION ${OPENSSL_LIBRARIES_DIR})
|
||||
|
||||
set(OPENSSL_USE_STATIC_LIBS TRUE)
|
||||
|
||||
set(LIBS ${LIBS}
|
||||
${LIBSSH_LIB_PATH}
|
||||
${ZLIB_LIB_PATH}
|
||||
)
|
||||
|
||||
set(LIBS ${LIBS}
|
||||
${OPENSSL_LIB_SSL_PATH}
|
||||
${OPENSSL_LIB_CRYPTO_PATH}
|
||||
)
|
||||
|
||||
add_compile_definitions(_WINSOCKAPI_)
|
||||
|
||||
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
|
||||
set(BUILD_WITH_QT6 ON)
|
||||
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/qtkeychain)
|
||||
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/qtkeychain ${CMAKE_BINARY_DIR}/3rd/qtkeychain EXCLUDE_FROM_ALL)
|
||||
|
||||
if(ANDROID)
|
||||
# Use qtgamepad from amnezia-vpn/qtgamepad repository
|
||||
# Only if Qt6CorePrivate is available (required by qtgamepad)
|
||||
find_package(Qt6CorePrivate CONFIG QUIET)
|
||||
if(Qt6CorePrivate_FOUND)
|
||||
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/qtgamepad)
|
||||
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/qtgamepad ${CMAKE_BINARY_DIR}/3rd/qtgamepad)
|
||||
# Link both the C++ module and QML plugin
|
||||
if(TARGET GamepadLegacy)
|
||||
if(TARGET ${PROJECT})
|
||||
target_link_libraries(${PROJECT} PRIVATE GamepadLegacy)
|
||||
else()
|
||||
list(APPEND LIBS GamepadLegacy)
|
||||
endif()
|
||||
if(TARGET GamepadLegacyQuickPrivate)
|
||||
if(TARGET ${PROJECT})
|
||||
target_link_libraries(${PROJECT} PRIVATE GamepadLegacyQuickPrivate)
|
||||
else()
|
||||
list(APPEND LIBS GamepadLegacyQuickPrivate)
|
||||
endif()
|
||||
message(STATUS "Gamepad support enabled for Android")
|
||||
else()
|
||||
@@ -106,12 +39,12 @@ endif()
|
||||
set(LIBS ${LIBS} qt6keychain)
|
||||
|
||||
include_directories(
|
||||
${OPENSSL_INCLUDE_DIR}
|
||||
${LIBSSH_INCLUDE_DIR}/include
|
||||
${LIBSSH_ROOT_DIR}/include
|
||||
${CLIENT_ROOT_DIR}/3rd/libssh/include
|
||||
${CLIENT_ROOT_DIR}/3rd/QSimpleCrypto/src/include
|
||||
${CLIENT_ROOT_DIR}/3rd/qtkeychain/qtkeychain
|
||||
${CMAKE_CURRENT_BINARY_DIR}/3rd/qtkeychain
|
||||
${CMAKE_CURRENT_BINARY_DIR}/3rd/libssh/include
|
||||
)
|
||||
|
||||
find_package(OpenSSL REQUIRED)
|
||||
list(APPEND LIBS OpenSSL::SSL OpenSSL::Crypto)
|
||||
|
||||
find_package(libssh REQUIRED)
|
||||
list(APPEND LIBS ssh::ssh)
|
||||
@@ -1,21 +0,0 @@
|
||||
set(CLIENT_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR}/..)
|
||||
set(QSIMPLECRYPTO_DIR ${CLIENT_ROOT_DIR}/3rd/QSimpleCrypto/src)
|
||||
|
||||
include_directories(${QSIMPLECRYPTO_DIR})
|
||||
|
||||
set(HEADERS ${HEADERS}
|
||||
${QSIMPLECRYPTO_DIR}/include/QAead.h
|
||||
${QSIMPLECRYPTO_DIR}/include/QBlockCipher.h
|
||||
${QSIMPLECRYPTO_DIR}/include/QRsa.h
|
||||
${QSIMPLECRYPTO_DIR}/include/QSimpleCrypto_global.h
|
||||
${QSIMPLECRYPTO_DIR}/include/QX509.h
|
||||
${QSIMPLECRYPTO_DIR}/include/QX509Store.h
|
||||
)
|
||||
|
||||
set(SOURCES ${SOURCES}
|
||||
${QSIMPLECRYPTO_DIR}/sources/QAead.cpp
|
||||
${QSIMPLECRYPTO_DIR}/sources/QBlockCipher.cpp
|
||||
${QSIMPLECRYPTO_DIR}/sources/QRsa.cpp
|
||||
${QSIMPLECRYPTO_DIR}/sources/QX509.cpp
|
||||
${QSIMPLECRYPTO_DIR}/sources/QX509Store.cpp
|
||||
)
|
||||
+83
-16
@@ -1,6 +1,12 @@
|
||||
message("Client android ${CMAKE_ANDROID_ARCH_ABI} build")
|
||||
|
||||
set(APP_ANDROID_MIN_SDK 28)
|
||||
if(NOT DEFINED APP_ANDROID_MIN_SDK)
|
||||
set(APP_ANDROID_MIN_SDK 28)
|
||||
endif()
|
||||
|
||||
# Option to build Play variant (with Google Play Billing) instead of OSS
|
||||
# When ON, adds target android_play_apk: cmake --build . --target android_play_apk
|
||||
option(ANDROID_BUILD_PLAY "Add android_play_apk target for Google Play Billing build" OFF)
|
||||
set(ANDROID_PLATFORM "android-${APP_ANDROID_MIN_SDK}" CACHE STRING
|
||||
"The minimum API level supported by the application or library" FORCE)
|
||||
|
||||
@@ -13,7 +19,6 @@ set_target_properties(${PROJECT} PROPERTIES
|
||||
QT_ANDROID_MIN_SDK_VERSION ${APP_ANDROID_MIN_SDK}
|
||||
QT_ANDROID_TARGET_SDK_VERSION 36
|
||||
QT_ANDROID_SDK_BUILD_TOOLS_REVISION 36.0.0
|
||||
QT_ANDROID_PACKAGE_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/android
|
||||
)
|
||||
|
||||
set(QT_ANDROID_MULTI_ABI_FORWARD_VARS "QT_NO_GLOBAL_APK_TARGET_PART_OF_ALL;CMAKE_BUILD_TYPE")
|
||||
@@ -42,18 +47,80 @@ set(SOURCES ${SOURCES}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/core/utils/installedAppsImageProvider.cpp
|
||||
)
|
||||
|
||||
foreach(abi IN ITEMS ${QT_ANDROID_ABIS})
|
||||
set_property(TARGET ${PROJECT} PROPERTY QT_ANDROID_EXTRA_LIBS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/amneziawg/android/${abi}/libwg-go.so
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/android/${abi}/libck-ovpn-plugin.so
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/android/${abi}/libovpn3.so
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/android/${abi}/libovpnutil.so
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/android/${abi}/librsapss.so
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openssl/android/${abi}/libcrypto_3.so
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openssl/android/${abi}/libssl_3.so
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/libssh/android/${abi}/libssh.so
|
||||
)
|
||||
endforeach()
|
||||
|
||||
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/xray/android/libxray.aar
|
||||
DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/android/xray/libXray)
|
||||
find_package(awg-android REQUIRED)
|
||||
set(LIBS ${LIBS} amnezia::awg-android)
|
||||
set_property(TARGET ${PROJECT} APPEND PROPERTY QT_ANDROID_EXTRA_LIBS ${AMNEZIA_ANDROID_LIBWG_PATH} ${AMNEZIA_ANDROID_LIBWG_QUICK_PATH})
|
||||
|
||||
find_package(amnezia-libxray REQUIRED)
|
||||
file(COPY ${AMNEZIA_LIBXRAY_PATH} DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/android/xray/libXray)
|
||||
|
||||
find_package(openvpn-pt-android REQUIRED)
|
||||
set(LIBS ${LIBS} amnezia::openvpn-pt-android)
|
||||
set_property(TARGET ${PROJECT} APPEND PROPERTY QT_ANDROID_EXTRA_LIBS ${OPENVPN_PT_ANDROID_LIBCK_OVPN_PLUGIN_PATH})
|
||||
|
||||
set(APP_ANDROID_PACKAGE_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/android)
|
||||
|
||||
if(APP_ANDROID_MAX_SDK)
|
||||
set(APP_ANDROID_PACKAGE_SOURCE_DIR ${CMAKE_CURRENT_BINARY_DIR}/android-package-source)
|
||||
file(REMOVE_RECURSE ${APP_ANDROID_PACKAGE_SOURCE_DIR})
|
||||
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/android/ DESTINATION ${APP_ANDROID_PACKAGE_SOURCE_DIR})
|
||||
|
||||
set(manifest_path ${APP_ANDROID_PACKAGE_SOURCE_DIR}/AndroidManifest.xml)
|
||||
set(manifest_anchor "android:installLocation=\"auto\">")
|
||||
file(READ ${manifest_path} manifest_contents)
|
||||
string(REPLACE
|
||||
"${manifest_anchor}"
|
||||
"${manifest_anchor}\n\n <uses-sdk android:maxSdkVersion=\"${APP_ANDROID_MAX_SDK}\" />"
|
||||
patched_contents "${manifest_contents}")
|
||||
if(patched_contents STREQUAL manifest_contents)
|
||||
message(FATAL_ERROR
|
||||
"Failed to set maxSdkVersion=${APP_ANDROID_MAX_SDK}: anchor '${manifest_anchor}' "
|
||||
"not found in ${CMAKE_CURRENT_SOURCE_DIR}/android/AndroidManifest.xml")
|
||||
endif()
|
||||
file(WRITE ${manifest_path} "${patched_contents}")
|
||||
endif()
|
||||
|
||||
set_property(TARGET ${PROJECT} PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR ${APP_ANDROID_PACKAGE_SOURCE_DIR})
|
||||
|
||||
if(QT_USE_TARGET_ANDROID_BUILD_DIR)
|
||||
set(_android_build_dir "${CMAKE_CURRENT_BINARY_DIR}/android-build-${PROJECT}")
|
||||
else()
|
||||
set(_android_build_dir "${CMAKE_CURRENT_BINARY_DIR}/android-build")
|
||||
endif()
|
||||
|
||||
add_custom_target(android_gradle_clean
|
||||
COMMAND ./gradlew clean
|
||||
WORKING_DIRECTORY "${_android_build_dir}"
|
||||
COMMENT "Cleaning Android Gradle build cache"
|
||||
)
|
||||
|
||||
# Always-available debug target: build Play Debug APK and copy to standard output path
|
||||
# so Qt Creator's deploy step picks it up automatically
|
||||
add_custom_target(android_play_debug_install
|
||||
COMMAND ./gradlew assemblePlayDebug
|
||||
COMMAND sh -c "cp build/outputs/apk/play/debug/*.apk build/outputs/apk/android-build-${PROJECT}-debug.apk"
|
||||
WORKING_DIRECTORY "${_android_build_dir}"
|
||||
COMMENT "Building Android Play Debug APK and copying to deploy path"
|
||||
DEPENDS ${PROJECT}
|
||||
)
|
||||
|
||||
if(ANDROID_BUILD_PLAY)
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
set(_gradle_suffix "Debug")
|
||||
else()
|
||||
set(_gradle_suffix "Release")
|
||||
endif()
|
||||
add_custom_target(android_play_apk
|
||||
COMMAND ./gradlew assemblePlay${_gradle_suffix}
|
||||
WORKING_DIRECTORY "${_android_build_dir}"
|
||||
COMMENT "Building Android Play APK (assemblePlay${_gradle_suffix})"
|
||||
DEPENDS ${PROJECT}
|
||||
)
|
||||
add_custom_target(android_play_aab
|
||||
COMMAND ./gradlew bundlePlay${_gradle_suffix}
|
||||
WORKING_DIRECTORY "${_android_build_dir}"
|
||||
COMMENT "Building Android Play AAB (bundlePlay${_gradle_suffix})"
|
||||
DEPENDS ${PROJECT}
|
||||
)
|
||||
endif()
|
||||
@@ -0,0 +1,3 @@
|
||||
if(NOT CLIENT_ANDROID_PACKAGE)
|
||||
set(CLIENT_ANDROID_PACKAGE "org.amnezia.vpn" CACHE STRING "Android package name for Play Store version lookup")
|
||||
endif()
|
||||
@@ -0,0 +1,22 @@
|
||||
if(NOT CLIENT_SWIFT_MODULE_NAME)
|
||||
set(CLIENT_SWIFT_MODULE_NAME "${CLIENT_APPLICATION_NAME}" CACHE STRING "Swift module name for Objective-C interop header")
|
||||
endif()
|
||||
if(NOT CLIENT_SWIFT_OBJC_HEADER_NAME)
|
||||
set(CLIENT_SWIFT_OBJC_HEADER_NAME "${CLIENT_SWIFT_MODULE_NAME}-Swift.h" CACHE STRING "Generated Swift Objective-C header name")
|
||||
endif()
|
||||
if(NOT CLIENT_NETWORK_EXTENSION_NAME)
|
||||
set(CLIENT_NETWORK_EXTENSION_NAME "${CLIENT_APPLICATION_NAME}NetworkExtension" CACHE STRING "Display name for Apple network extension targets")
|
||||
endif()
|
||||
|
||||
if(NOT BUILD_OSX_APP_IDENTIFIER)
|
||||
set(BUILD_OSX_APP_IDENTIFIER org.amnezia.AmneziaVPN CACHE STRING "OSX Application identifier")
|
||||
endif()
|
||||
if(NOT BUILD_IOS_APP_IDENTIFIER)
|
||||
set(BUILD_IOS_APP_IDENTIFIER org.amnezia.AmneziaVPN CACHE STRING "iOS Application identifier")
|
||||
endif()
|
||||
if(NOT BUILD_IOS_GROUP_IDENTIFIER)
|
||||
set(BUILD_IOS_GROUP_IDENTIFIER group.org.amnezia.AmneziaVPN CACHE STRING "iOS Group identifier")
|
||||
endif()
|
||||
if(NOT BUILD_VPN_DEVELOPMENT_TEAM)
|
||||
set(BUILD_VPN_DEVELOPMENT_TEAM X7UJ388FXK CACHE STRING "Amnezia VPN Development Team")
|
||||
endif()
|
||||
@@ -0,0 +1,64 @@
|
||||
if(NOT CLIENT_TARGET_NAME)
|
||||
set(CLIENT_TARGET_NAME "AmneziaVPN" CACHE STRING "Client executable target name")
|
||||
endif()
|
||||
if(NOT CLIENT_APPLICATION_NAME)
|
||||
set(CLIENT_APPLICATION_NAME "AmneziaVPN" CACHE STRING "Application display and executable name")
|
||||
endif()
|
||||
if(NOT CLIENT_SERVICE_NAME)
|
||||
set(CLIENT_SERVICE_NAME "AmneziaVPN-service" CACHE STRING "Service executable name")
|
||||
endif()
|
||||
if(NOT CLIENT_ORGANIZATION_NAME)
|
||||
set(CLIENT_ORGANIZATION_NAME "AmneziaVPN.ORG" CACHE STRING "QSettings organization name")
|
||||
endif()
|
||||
if(NOT CLIENT_APP_INSTANCE_NAME)
|
||||
set(CLIENT_APP_INSTANCE_NAME "AmneziaVPNInstance" CACHE STRING "Single-instance local server name")
|
||||
endif()
|
||||
if(NOT CLIENT_KEYCHAIN_NAME)
|
||||
set(CLIENT_KEYCHAIN_NAME "${CLIENT_APPLICATION_NAME}-Keychain" CACHE STRING "QtKeychain service name used for encrypted settings keys")
|
||||
endif()
|
||||
|
||||
# -- QML entry points and UI resources
|
||||
if(NOT CLIENT_QML_ENTRYPOINT)
|
||||
set(CLIENT_QML_ENTRYPOINT "qrc:/ui/qml/main2.qml" CACHE STRING "Main QML entrypoint URL")
|
||||
endif()
|
||||
if(NOT CLIENT_QML_IMPORT_PATH)
|
||||
set(CLIENT_QML_IMPORT_PATH "qrc:/ui/qml/Modules/" CACHE STRING "Additional QQmlEngine import path")
|
||||
endif()
|
||||
if(NOT CLIENT_QML_PAGES_PREFIX)
|
||||
set(CLIENT_QML_PAGES_PREFIX "qrc:/ui/qml/Pages2/" CACHE STRING "PageController QML pages prefix")
|
||||
endif()
|
||||
if(NOT CLIENT_UI_FONT_RESOURCE)
|
||||
set(CLIENT_UI_FONT_RESOURCE ":/fonts/pt-root-ui_vf.ttf" CACHE STRING "UI font resource path")
|
||||
endif()
|
||||
if(NOT CLIENT_QML_QRC_FILE)
|
||||
set(CLIENT_QML_QRC_FILE "${_CLIENT_SRC_DIR}/ui/qml/qml.qrc" CACHE FILEPATH "QML resources manifest")
|
||||
endif()
|
||||
if(NOT CLIENT_EXTRA_QRC)
|
||||
set(CLIENT_EXTRA_QRC "" CACHE STRING "Optional semicolon-separated extra QRC files")
|
||||
endif()
|
||||
|
||||
# -- translations
|
||||
if(NOT CLIENT_TS_PREFIX)
|
||||
set(CLIENT_TS_PREFIX "amneziavpn" CACHE STRING "Translation filename prefix")
|
||||
endif()
|
||||
if(NOT CLIENT_TS_FILES)
|
||||
set(CLIENT_TS_FILES "" CACHE STRING "Optional semicolon-separated absolute TS file paths")
|
||||
endif()
|
||||
|
||||
# -- brand-provided headers
|
||||
if(NOT CLIENT_BRANDING_INCLUDE_DIR)
|
||||
set(CLIENT_BRANDING_INCLUDE_DIR "" CACHE PATH "Optional include root for non-generated branding headers (e.g. pageEnum.h)")
|
||||
endif()
|
||||
if(CLIENT_BRANDING_INCLUDE_DIR AND EXISTS "${CLIENT_BRANDING_INCLUDE_DIR}/ui/utils/pageEnum.h")
|
||||
set(CLIENT_PAGE_ENUM_HEADER "${CLIENT_BRANDING_INCLUDE_DIR}/ui/utils/pageEnum.h")
|
||||
else()
|
||||
set(CLIENT_PAGE_ENUM_HEADER "${_CLIENT_SRC_DIR}/ui/utils/pageEnum.h")
|
||||
endif()
|
||||
|
||||
if(NOT CLIENT_NOTIFICATION_IDENTIFIER)
|
||||
set(CLIENT_NOTIFICATION_IDENTIFIER "${CLIENT_TS_PREFIX}" CACHE STRING "Local notification request identifier")
|
||||
endif()
|
||||
|
||||
if(NOT DEFINED CLIENT_ENABLE_APP_UPDATES)
|
||||
set(CLIENT_ENABLE_APP_UPDATES 1)
|
||||
endif()
|
||||
@@ -0,0 +1,45 @@
|
||||
if(NOT CLIENT_IOS_NE_TARGET_NAME)
|
||||
set(CLIENT_IOS_NE_TARGET_NAME "networkextension" CACHE STRING "iOS network extension build target name")
|
||||
endif()
|
||||
if(NOT CLIENT_IOS_NE_ENTITLEMENTS_FILE)
|
||||
set(CLIENT_IOS_NE_ENTITLEMENTS_FILE "AmneziaVPNNetworkExtension.entitlements" CACHE STRING "iOS network extension entitlements filename")
|
||||
endif()
|
||||
if(NOT CLIENT_IOS_LAUNCHSCREEN_FILE)
|
||||
set(CLIENT_IOS_LAUNCHSCREEN_FILE "AmneziaVPNLaunchScreen.storyboard" CACHE STRING "iOS launch screen storyboard filename")
|
||||
endif()
|
||||
|
||||
get_filename_component(CLIENT_IOS_LAUNCHSCREEN_NAME "${CLIENT_IOS_LAUNCHSCREEN_FILE}" NAME_WE)
|
||||
|
||||
if(NOT CLIENT_IOS_APP_ENTITLEMENTS_PATH)
|
||||
set(CLIENT_IOS_APP_ENTITLEMENTS_PATH "${_CLIENT_SRC_DIR}/ios/app/main.entitlements" CACHE FILEPATH "iOS app entitlements file path")
|
||||
endif()
|
||||
if(NOT CLIENT_IOS_NE_ENTITLEMENTS_PATH)
|
||||
set(CLIENT_IOS_NE_ENTITLEMENTS_PATH "${_CLIENT_SRC_DIR}/ios/networkextension/${CLIENT_IOS_NE_ENTITLEMENTS_FILE}" CACHE FILEPATH "iOS network extension entitlements file path")
|
||||
endif()
|
||||
if(NOT CLIENT_IOS_LAUNCHSCREEN_PATH)
|
||||
set(CLIENT_IOS_LAUNCHSCREEN_PATH "${_CLIENT_SRC_DIR}/ios/app/${CLIENT_IOS_LAUNCHSCREEN_FILE}" CACHE FILEPATH "iOS launch screen storyboard path")
|
||||
endif()
|
||||
if(NOT CLIENT_IOS_MEDIA_ASSETS_PATH)
|
||||
set(CLIENT_IOS_MEDIA_ASSETS_PATH "${_CLIENT_SRC_DIR}/ios/app/Media.xcassets" CACHE FILEPATH "iOS app Media.xcassets path")
|
||||
endif()
|
||||
|
||||
if(NOT CLIENT_IOS_BUNDLE_ID)
|
||||
set(CLIENT_IOS_BUNDLE_ID "org.amnezia.AmneziaVPN" CACHE STRING "iOS bundle id for App Store version lookup")
|
||||
endif()
|
||||
if(NOT CLIENT_IOS_STORE_URL_FALLBACK)
|
||||
set(CLIENT_IOS_STORE_URL_FALLBACK "itms-apps://itunes.apple.com/app/id1600529900" CACHE STRING "Fallback App Store URL when lookup omits trackViewUrl")
|
||||
endif()
|
||||
|
||||
# -- signing
|
||||
if(NOT CLIENT_IOS_PROVISIONING_PROFILE_SPECIFIER)
|
||||
set(CLIENT_IOS_PROVISIONING_PROFILE_SPECIFIER "distr ios.org.amnezia.AmneziaVPN" CACHE STRING "iOS app provisioning profile specifier for release")
|
||||
endif()
|
||||
if(NOT CLIENT_IOS_PROVISIONING_PROFILE_SPECIFIER_DEBUG)
|
||||
set(CLIENT_IOS_PROVISIONING_PROFILE_SPECIFIER_DEBUG "dev ios.org.amnezia.AmneziaVPN" CACHE STRING "iOS app provisioning profile specifier for debug")
|
||||
endif()
|
||||
if(NOT CLIENT_IOS_NE_PROVISIONING_PROFILE_SPECIFIER)
|
||||
set(CLIENT_IOS_NE_PROVISIONING_PROFILE_SPECIFIER "distr ios.org.amnezia.AmneziaVPN" CACHE STRING "iOS network extension provisioning profile specifier for release")
|
||||
endif()
|
||||
if(NOT CLIENT_IOS_NE_PROVISIONING_PROFILE_SPECIFIER_DEBUG)
|
||||
set(CLIENT_IOS_NE_PROVISIONING_PROFILE_SPECIFIER_DEBUG "dev ios.org.amnezia.AmneziaVPN" CACHE STRING "iOS network extension provisioning profile specifier for debug")
|
||||
endif()
|
||||
@@ -0,0 +1,37 @@
|
||||
if(NOT CLIENT_MACOS_NE_TARGET_NAME)
|
||||
set(CLIENT_MACOS_NE_TARGET_NAME "${CLIENT_NETWORK_EXTENSION_NAME}" CACHE STRING "macOS network extension build target name")
|
||||
endif()
|
||||
if(NOT CLIENT_MACOS_NE_ENTITLEMENTS_FILE)
|
||||
set(CLIENT_MACOS_NE_ENTITLEMENTS_FILE "AmneziaVPNNetworkExtension.entitlements" CACHE STRING "macOS network extension entitlements filename")
|
||||
endif()
|
||||
|
||||
if(NOT CLIENT_MACOS_APP_ENTITLEMENTS_PATH)
|
||||
set(CLIENT_MACOS_APP_ENTITLEMENTS_PATH "${_CLIENT_SRC_DIR}/macos/app/app.entitlements" CACHE FILEPATH "macOS app entitlements file path")
|
||||
endif()
|
||||
if(NOT CLIENT_MACOS_NE_ENTITLEMENTS_PATH)
|
||||
set(CLIENT_MACOS_NE_ENTITLEMENTS_PATH "${_CLIENT_SRC_DIR}/macos/networkextension/${CLIENT_MACOS_NE_ENTITLEMENTS_FILE}" CACHE FILEPATH "macOS network extension entitlements file path")
|
||||
endif()
|
||||
if(NOT CLIENT_MACOS_MEDIA_ASSETS_PATH)
|
||||
set(CLIENT_MACOS_MEDIA_ASSETS_PATH "${_CLIENT_SRC_DIR}/macos/app/Images.xcassets" CACHE FILEPATH "macOS app Images.xcassets path")
|
||||
endif()
|
||||
if(NOT CLIENT_MACOS_APP_ICNS_PATH)
|
||||
set(CLIENT_MACOS_APP_ICNS_PATH "${_CLIENT_SRC_DIR}/images/app.icns" CACHE FILEPATH "macOS app icon (.icns) used by non-App Store builds")
|
||||
endif()
|
||||
|
||||
if(NOT CLIENT_MACOS_EXTRA_PLIST_ENTRIES)
|
||||
set(CLIENT_MACOS_EXTRA_PLIST_ENTRIES "" CACHE STRING "Extra macOS Info.plist entries appended verbatim (e.g. appearance pinning)")
|
||||
endif()
|
||||
|
||||
# -- signing
|
||||
if(NOT CLIENT_MACOS_PROVISIONING_PROFILE_SPECIFIER)
|
||||
set(CLIENT_MACOS_PROVISIONING_PROFILE_SPECIFIER "distr macos.org.amnezia.AmneziaVPN" CACHE STRING "macOS app provisioning profile specifier for release")
|
||||
endif()
|
||||
if(NOT CLIENT_MACOS_PROVISIONING_PROFILE_SPECIFIER_DEBUG)
|
||||
set(CLIENT_MACOS_PROVISIONING_PROFILE_SPECIFIER_DEBUG "dev macos.org.amnezia.AmneziaVPN" CACHE STRING "macOS app provisioning profile specifier for debug")
|
||||
endif()
|
||||
if(NOT CLIENT_MACOS_NE_PROVISIONING_PROFILE_SPECIFIER)
|
||||
set(CLIENT_MACOS_NE_PROVISIONING_PROFILE_SPECIFIER "distr macos.org.amnezia.amneziaVPN.NE" CACHE STRING "macOS network extension provisioning profile specifier for release")
|
||||
endif()
|
||||
if(NOT CLIENT_MACOS_NE_PROVISIONING_PROFILE_SPECIFIER_DEBUG)
|
||||
set(CLIENT_MACOS_NE_PROVISIONING_PROFILE_SPECIFIER_DEBUG "dev macos.org.amnezia.amneziaVPN.NE" CACHE STRING "macOS network extension provisioning profile specifier for debug")
|
||||
endif()
|
||||
@@ -0,0 +1,9 @@
|
||||
get_filename_component(_CLIENT_SRC_DIR "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
|
||||
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/branding/common.cmake)
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/branding/apple.cmake)
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/branding/ios.cmake)
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/branding/macos.cmake)
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/branding/android.cmake)
|
||||
|
||||
unset(_CLIENT_SRC_DIR)
|
||||
@@ -24,6 +24,17 @@ set(IOS_TARGET_COMPILED_TYPES
|
||||
EXECUTABLE
|
||||
)
|
||||
|
||||
## Simulator architecture: honor CMAKE_OSX_ARCHITECTURES when set, otherwise
|
||||
## match the host — arm64 Macs run arm64 simulators (Xcode 26 has no Rosetta
|
||||
## simulator destinations), Intel Macs need x86_64.
|
||||
if(CMAKE_OSX_ARCHITECTURES)
|
||||
string(REPLACE ";" " " IOS_SIMULATOR_ARCHS "${CMAKE_OSX_ARCHITECTURES}")
|
||||
elseif(CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "arm64")
|
||||
set(IOS_SIMULATOR_ARCHS "arm64")
|
||||
else()
|
||||
set(IOS_SIMULATOR_ARCHS "x86_64")
|
||||
endif()
|
||||
|
||||
## Inspect all the targets, and add extra properties if necessary.
|
||||
while(IOS_TARGETS)
|
||||
list(POP_FRONT IOS_TARGETS TARGET_NAME)
|
||||
@@ -38,6 +49,6 @@ while(IOS_TARGETS)
|
||||
message("Patching architectures for ${TARGET_NAME}")
|
||||
set_target_properties(${TARGET_NAME} PROPERTIES
|
||||
XCODE_ATTRIBUTE_ARCHS[sdk=iphoneos*] "arm64"
|
||||
XCODE_ATTRIBUTE_ARCHS[sdk=iphonesimulator*] "x86_64"
|
||||
XCODE_ATTRIBUTE_ARCHS[sdk=iphonesimulator*] "${IOS_SIMULATOR_ARCHS}"
|
||||
)
|
||||
endwhile()
|
||||
+29
-30
@@ -1,8 +1,6 @@
|
||||
message("Client iOS build")
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET 13.0)
|
||||
set(APPLE_PROJECT_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
|
||||
|
||||
|
||||
enable_language(OBJC)
|
||||
enable_language(OBJCXX)
|
||||
enable_language(Swift)
|
||||
@@ -33,8 +31,8 @@ set(HEADERS ${HEADERS}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ioscontextmenu.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate-C-Interface.h
|
||||
)
|
||||
set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.h PROPERTIES OBJECTIVE_CPP_HEADER TRUE)
|
||||
@@ -44,50 +42,54 @@ set(SOURCES ${SOURCES}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ioscontextmenu.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/AmneziaSceneDelegateHooks.mm
|
||||
)
|
||||
|
||||
# The context menu helper uses ARC-only constructs (weak references); the
|
||||
# rest of the Objective-C++ sources build with manual reference counting.
|
||||
set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ioscontextmenu.mm
|
||||
PROPERTIES COMPILE_OPTIONS "-fobjc-arc"
|
||||
)
|
||||
|
||||
|
||||
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"
|
||||
MACOSX_BUNDLE_BUNDLE_NAME "AmneziaVPN"
|
||||
MACOSX_BUNDLE_INFO_STRING "${CLIENT_APPLICATION_NAME}"
|
||||
MACOSX_BUNDLE_BUNDLE_NAME "${CLIENT_APPLICATION_NAME}"
|
||||
MACOSX_BUNDLE_GUI_IDENTIFIER "${BUILD_IOS_APP_IDENTIFIER}"
|
||||
MACOSX_BUNDLE_BUNDLE_VERSION "${CMAKE_PROJECT_VERSION_TWEAK}"
|
||||
MACOSX_BUNDLE_LONG_VERSION_STRING "${APPLE_PROJECT_VERSION}-${CMAKE_PROJECT_VERSION_TWEAK}"
|
||||
MACOSX_BUNDLE_SHORT_VERSION_STRING "${APPLE_PROJECT_VERSION}"
|
||||
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "${BUILD_IOS_APP_IDENTIFIER}"
|
||||
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS "${CMAKE_CURRENT_SOURCE_DIR}/ios/app/main.entitlements"
|
||||
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS "${CLIENT_IOS_APP_ENTITLEMENTS_PATH}"
|
||||
XCODE_ATTRIBUTE_MARKETING_VERSION "${APPLE_PROJECT_VERSION}"
|
||||
XCODE_ATTRIBUTE_CURRENT_PROJECT_VERSION "${CMAKE_PROJECT_VERSION_TWEAK}"
|
||||
XCODE_ATTRIBUTE_PRODUCT_NAME "AmneziaVPN"
|
||||
XCODE_ATTRIBUTE_BUNDLE_INFO_STRING "AmneziaVPN"
|
||||
XCODE_ATTRIBUTE_PRODUCT_NAME "${CLIENT_APPLICATION_NAME}"
|
||||
XCODE_ATTRIBUTE_BUNDLE_INFO_STRING "${CLIENT_APPLICATION_NAME}"
|
||||
XCODE_GENERATE_SCHEME TRUE
|
||||
XCODE_ATTRIBUTE_ENABLE_BITCODE "NO"
|
||||
XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME "AppIcon"
|
||||
XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2"
|
||||
XCODE_EMBED_FRAMEWORKS_CODE_SIGN_ON_COPY ON
|
||||
XCODE_LINK_BUILD_PHASE_MODE KNOWN_LOCATION
|
||||
XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/Frameworks"
|
||||
XCODE_EMBED_APP_EXTENSIONS networkextension
|
||||
XCODE_EMBED_APP_EXTENSIONS ${CLIENT_IOS_NE_TARGET_NAME}
|
||||
)
|
||||
|
||||
if(DEFINED DEPLOY)
|
||||
if(DEPLOY)
|
||||
set_target_properties(${PROJECT} PROPERTIES
|
||||
XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "Apple Distribution"
|
||||
XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY[variant=Debug] "Apple Development"
|
||||
XCODE_ATTRIBUTE_CODE_SIGN_STYLE Manual
|
||||
XCODE_ATTRIBUTE_PROVISIONING_PROFILE_SPECIFIER "distr ios.org.amnezia.AmneziaVPN"
|
||||
XCODE_ATTRIBUTE_PROVISIONING_PROFILE_SPECIFIER[variant=Debug] "dev ios.org.amnezia.AmneziaVPN"
|
||||
XCODE_ATTRIBUTE_PROVISIONING_PROFILE_SPECIFIER "${CLIENT_IOS_PROVISIONING_PROFILE_SPECIFIER}"
|
||||
XCODE_ATTRIBUTE_PROVISIONING_PROFILE_SPECIFIER[variant=Debug] "${CLIENT_IOS_PROVISIONING_PROFILE_SPECIFIER_DEBUG}"
|
||||
)
|
||||
else()
|
||||
set_target_properties(${PROJECT} PROPERTIES
|
||||
@@ -99,11 +101,11 @@ set_target_properties(${PROJECT} PROPERTIES
|
||||
XCODE_ATTRIBUTE_SWIFT_VERSION "5.0"
|
||||
XCODE_ATTRIBUTE_CLANG_ENABLE_MODULES "YES"
|
||||
XCODE_ATTRIBUTE_SWIFT_PRECOMPILE_BRIDGING_HEADER "NO"
|
||||
XCODE_ATTRIBUTE_SWIFT_OBJC_INTERFACE_HEADER_NAME "AmneziaVPN-Swift.h"
|
||||
XCODE_ATTRIBUTE_SWIFT_OBJC_INTERFACE_HEADER_NAME "${CLIENT_SWIFT_OBJC_HEADER_NAME}"
|
||||
XCODE_ATTRIBUTE_SWIFT_OBJC_INTEROP_MODE "objcxx"
|
||||
)
|
||||
set_target_properties(${PROJECT} PROPERTIES
|
||||
XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "X7UJ388FXK"
|
||||
XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "${BUILD_VPN_DEVELOPMENT_TEAM}"
|
||||
)
|
||||
target_include_directories(${PROJECT} PRIVATE ${CMAKE_CURRENT_LIST_DIR})
|
||||
target_compile_options(${PROJECT} PRIVATE
|
||||
@@ -118,31 +120,28 @@ target_sources(${PROJECT} PRIVATE
|
||||
${WG_APPLE_SOURCE_DIR}/WireGuardKitC/x25519.c
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/LogController.swift
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/Log.swift
|
||||
${CLIENT_GROUP_IDENTIFIER_SWIFT_FILE}
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/ScreenProtection.swift
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/VPNCController.swift
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/StoreKit2Helper.swift
|
||||
)
|
||||
|
||||
set_source_files_properties(
|
||||
${CLIENT_IOS_MEDIA_ASSETS_PATH}
|
||||
PROPERTIES MACOSX_PACKAGE_LOCATION Resources
|
||||
)
|
||||
|
||||
target_sources(${PROJECT} PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/AmneziaVPNLaunchScreen.storyboard
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Media.xcassets
|
||||
${CLIENT_IOS_LAUNCHSCREEN_PATH}
|
||||
${CLIENT_IOS_MEDIA_ASSETS_PATH}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy
|
||||
)
|
||||
|
||||
set_property(TARGET ${PROJECT} APPEND PROPERTY RESOURCE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/AmneziaVPNLaunchScreen.storyboard
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Media.xcassets
|
||||
${CLIENT_IOS_LAUNCHSCREEN_PATH}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy
|
||||
)
|
||||
|
||||
add_subdirectory(ios/networkextension)
|
||||
add_dependencies(${PROJECT} networkextension)
|
||||
|
||||
set_property(TARGET ${PROJECT} PROPERTY XCODE_EMBED_FRAMEWORKS
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-ios/OpenVPNAdapter.framework"
|
||||
)
|
||||
|
||||
set(CMAKE_XCODE_ATTRIBUTE_FRAMEWORK_SEARCH_PATHS ${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-ios/)
|
||||
target_link_libraries("networkextension" PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-ios/OpenVPNAdapter.framework")
|
||||
|
||||
add_dependencies(${PROJECT} ${CLIENT_IOS_NE_TARGET_NAME})
|
||||
@@ -23,9 +23,6 @@ set_target_properties(${PROJECT} PROPERTIES
|
||||
MACOSX_BUNDLE_SHORT_VERSION_STRING "${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}"
|
||||
MACOSX_BUNDLE_BUNDLE_VERSION "${CMAKE_PROJECT_VERSION_TWEAK}"
|
||||
)
|
||||
set(CMAKE_OSX_ARCHITECTURES "x86_64" CACHE INTERNAL "" FORCE)
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET 10.15)
|
||||
|
||||
|
||||
set(HEADERS ${HEADERS}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ui/utils/macosUtil.h
|
||||
@@ -37,8 +34,8 @@ set(SOURCES ${SOURCES}
|
||||
|
||||
|
||||
|
||||
set(ICON_FILE ${CMAKE_CURRENT_SOURCE_DIR}/images/app.icns)
|
||||
set(MACOSX_BUNDLE_ICON_FILE app.icns)
|
||||
set(ICON_FILE ${CLIENT_MACOS_APP_ICNS_PATH})
|
||||
get_filename_component(MACOSX_BUNDLE_ICON_FILE "${CLIENT_MACOS_APP_ICNS_PATH}" NAME)
|
||||
set_source_files_properties(${ICON_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
|
||||
set(SOURCES ${SOURCES} ${ICON_FILE})
|
||||
|
||||
|
||||
+21
-42
@@ -1,11 +1,11 @@
|
||||
message("Client ==> MacOS NE build")
|
||||
|
||||
set_target_properties(${PROJECT} PROPERTIES MACOSX_BUNDLE TRUE)
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET 10.15)
|
||||
|
||||
set(APPLE_PROJECT_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
|
||||
|
||||
enable_language(OBJC)
|
||||
enable_language(OBJCXX)
|
||||
enable_language(Swift)
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS ShaderTools Widgets)
|
||||
@@ -34,8 +34,6 @@ set(LIBS ${LIBS}
|
||||
set(HEADERS ${HEADERS}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate-C-Interface.h
|
||||
)
|
||||
@@ -45,19 +43,11 @@ set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_contro
|
||||
set(SOURCES ${SOURCES}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.mm
|
||||
)
|
||||
|
||||
set(ICON_FILE ${CMAKE_CURRENT_SOURCE_DIR}/images/app.icns)
|
||||
set(MACOSX_BUNDLE_ICON_FILE app.icns)
|
||||
set_source_files_properties(${ICON_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
|
||||
set(SOURCES ${SOURCES} ${ICON_FILE})
|
||||
|
||||
|
||||
target_include_directories(${PROJECT} PRIVATE
|
||||
${Qt6Gui_PRIVATE_INCLUDE_DIRS}
|
||||
${Qt6Widgets_PRIVATE_INCLUDE_DIRS}
|
||||
@@ -68,28 +58,25 @@ set_target_properties(${PROJECT} PROPERTIES
|
||||
XCODE_LINK_BUILD_PHASE_MODE KNOWN_LOCATION
|
||||
MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/macos/app/Info.plist.in
|
||||
MACOSX_BUNDLE_ICON_FILE "AppIcon"
|
||||
MACOSX_BUNDLE_INFO_STRING "AmneziaVPN"
|
||||
MACOSX_BUNDLE_BUNDLE_NAME "AmneziaVPN"
|
||||
MACOSX_BUNDLE_INFO_STRING "${CLIENT_APPLICATION_NAME}"
|
||||
MACOSX_BUNDLE_BUNDLE_NAME "${CLIENT_APPLICATION_NAME}"
|
||||
MACOSX_BUNDLE_GUI_IDENTIFIER "${BUILD_IOS_APP_IDENTIFIER}"
|
||||
MACOSX_BUNDLE_BUNDLE_VERSION "${CMAKE_PROJECT_VERSION_TWEAK}"
|
||||
MACOSX_BUNDLE_LONG_VERSION_STRING "${APPLE_PROJECT_VERSION}-${CMAKE_PROJECT_VERSION_TWEAK}"
|
||||
MACOSX_BUNDLE_SHORT_VERSION_STRING "${APPLE_PROJECT_VERSION}"
|
||||
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "${BUILD_IOS_APP_IDENTIFIER}"
|
||||
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS "${CMAKE_CURRENT_SOURCE_DIR}/macos/app/app.entitlements"
|
||||
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS "${CLIENT_MACOS_APP_ENTITLEMENTS_PATH}"
|
||||
XCODE_ATTRIBUTE_MARKETING_VERSION "${APPLE_PROJECT_VERSION}"
|
||||
XCODE_ATTRIBUTE_CURRENT_PROJECT_VERSION "${CMAKE_PROJECT_VERSION_TWEAK}"
|
||||
XCODE_ATTRIBUTE_PRODUCT_NAME "AmneziaVPN"
|
||||
XCODE_ATTRIBUTE_BUNDLE_INFO_STRING "AmneziaVPN"
|
||||
XCODE_ATTRIBUTE_PRODUCT_NAME "${CLIENT_APPLICATION_NAME}"
|
||||
XCODE_ATTRIBUTE_BUNDLE_INFO_STRING "${CLIENT_APPLICATION_NAME}"
|
||||
XCODE_GENERATE_SCHEME TRUE
|
||||
XCODE_ATTRIBUTE_ENABLE_BITCODE "NO"
|
||||
XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME "AppIcon"
|
||||
XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2"
|
||||
XCODE_EMBED_FRAMEWORKS_CODE_SIGN_ON_COPY "NO"
|
||||
XCODE_EMBED_FRAMEWORKS_REMOVE_HEADERS_ON_COPY "YES"
|
||||
XCODE_ATTRIBUTE_MACOSX_DEPLOYMENT_TARGET "11.0"
|
||||
|
||||
XCODE_LINK_BUILD_PHASE_MODE KNOWN_LOCATION
|
||||
XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/../Frameworks"
|
||||
XCODE_EMBED_APP_EXTENSIONS AmneziaVPNNetworkExtension
|
||||
XCODE_EMBED_APP_EXTENSIONS ${CLIENT_MACOS_NE_TARGET_NAME}
|
||||
)
|
||||
|
||||
if(DEPLOY)
|
||||
@@ -97,8 +84,8 @@ if(DEPLOY)
|
||||
XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "Apple Distribution"
|
||||
XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY[variant=Debug] "Apple Development"
|
||||
XCODE_ATTRIBUTE_CODE_SIGN_STYLE Manual
|
||||
XCODE_ATTRIBUTE_PROVISIONING_PROFILE_SPECIFIER "distr macos.org.amnezia.AmneziaVPN"
|
||||
XCODE_ATTRIBUTE_PROVISIONING_PROFILE_SPECIFIER[variant=Debug] "dev macos.org.amnezia.AmneziaVPN"
|
||||
XCODE_ATTRIBUTE_PROVISIONING_PROFILE_SPECIFIER "${CLIENT_MACOS_PROVISIONING_PROFILE_SPECIFIER}"
|
||||
XCODE_ATTRIBUTE_PROVISIONING_PROFILE_SPECIFIER[variant=Debug] "${CLIENT_MACOS_PROVISIONING_PROFILE_SPECIFIER_DEBUG}"
|
||||
)
|
||||
else()
|
||||
set_target_properties(${PROJECT} PROPERTIES
|
||||
@@ -110,11 +97,11 @@ set_target_properties(${PROJECT} PROPERTIES
|
||||
XCODE_ATTRIBUTE_SWIFT_VERSION "5.0"
|
||||
XCODE_ATTRIBUTE_CLANG_ENABLE_MODULES "YES"
|
||||
XCODE_ATTRIBUTE_SWIFT_PRECOMPILE_BRIDGING_HEADER "NO"
|
||||
XCODE_ATTRIBUTE_SWIFT_OBJC_INTERFACE_HEADER_NAME "AmneziaVPN-Swift.h"
|
||||
XCODE_ATTRIBUTE_SWIFT_OBJC_INTERFACE_HEADER_NAME "${CLIENT_SWIFT_OBJC_HEADER_NAME}"
|
||||
XCODE_ATTRIBUTE_SWIFT_OBJC_INTEROP_MODE "objcxx"
|
||||
)
|
||||
set_target_properties(${PROJECT} PROPERTIES
|
||||
XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "X7UJ388FXK"
|
||||
XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "${BUILD_VPN_DEVELOPMENT_TEAM}"
|
||||
)
|
||||
target_include_directories(${PROJECT} PRIVATE ${CMAKE_CURRENT_LIST_DIR})
|
||||
target_compile_options(${PROJECT} PRIVATE
|
||||
@@ -128,24 +115,29 @@ target_sources(${PROJECT} PRIVATE
|
||||
${WG_APPLE_SOURCE_DIR}/WireGuardKitC/x25519.c
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/LogController.swift
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/Log.swift
|
||||
${CLIENT_GROUP_IDENTIFIER_SWIFT_FILE}
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/ScreenProtection.swift
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/VPNCController.swift
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/StoreKit2Helper.swift
|
||||
)
|
||||
|
||||
set_source_files_properties(
|
||||
${CLIENT_MACOS_MEDIA_ASSETS_PATH}
|
||||
PROPERTIES MACOSX_PACKAGE_LOCATION Resources
|
||||
)
|
||||
|
||||
target_sources(${PROJECT} PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/macos/app/Images.xcassets
|
||||
${CLIENT_MACOS_MEDIA_ASSETS_PATH}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy
|
||||
)
|
||||
|
||||
set_property(TARGET ${PROJECT} APPEND PROPERTY RESOURCE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/macos/app/Images.xcassets
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy
|
||||
)
|
||||
|
||||
add_subdirectory(macos/networkextension)
|
||||
add_dependencies(${PROJECT} AmneziaVPNNetworkExtension)
|
||||
add_dependencies(${PROJECT} ${CLIENT_MACOS_NE_TARGET_NAME})
|
||||
|
||||
get_target_property(QtCore_location Qt6::Core LOCATION)
|
||||
message("QtCore_location")
|
||||
@@ -153,19 +145,6 @@ message(${QtCore_location})
|
||||
|
||||
get_filename_component(QT_BIN_DIR_DETECTED "${QtCore_location}/../../../../../bin" ABSOLUTE)
|
||||
|
||||
set_property(TARGET ${PROJECT} PROPERTY XCODE_EMBED_FRAMEWORKS
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-macos/OpenVPNAdapter.framework"
|
||||
)
|
||||
|
||||
set(CMAKE_XCODE_ATTRIBUTE_FRAMEWORK_SEARCH_PATHS ${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-macos)
|
||||
target_link_libraries("AmneziaVPNNetworkExtension" PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-macos/OpenVPNAdapter.framework")
|
||||
|
||||
add_custom_command(TARGET ${PROJECT} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory
|
||||
$<TARGET_BUNDLE_DIR:AmneziaVPN>/Contents/Frameworks
|
||||
COMMAND /usr/bin/find "$<TARGET_BUNDLE_DIR:AmneziaVPN>/Contents/Frameworks/OpenVPNAdapter.framework" -name "*.sha256" -delete
|
||||
COMMAND /usr/bin/codesign --force --sign "Apple Distribution: Privacy Technologies OU"
|
||||
"$<TARGET_BUNDLE_DIR:AmneziaVPN>/Contents/Frameworks/OpenVPNAdapter.framework/Versions/Current/OpenVPNAdapter"
|
||||
COMMAND ${QT_BIN_DIR_DETECTED}/macdeployqt $<TARGET_BUNDLE_DIR:AmneziaVPN> -appstore-compliant -qmldir=${CMAKE_CURRENT_SOURCE_DIR}
|
||||
COMMENT "Signing OpenVPNAdapter framework"
|
||||
COMMAND ${QT_BIN_DIR_DETECTED}/macdeployqt $<TARGET_BUNDLE_DIR:${PROJECT}> -appstore-compliant -qmldir=${CMAKE_CURRENT_SOURCE_DIR} -no-codesign
|
||||
)
|
||||
@@ -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,30 +35,46 @@ 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/installers/tProxyInstaller.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/appSplitTunnelingController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/ipSplitTunnelingController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/allowedDnsController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/selfhosted/exportController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/connectionController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/settingsController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/localProxyController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/logsController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/servicesCatalogController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/storePurchaseController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/subscriptionController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/newsController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/updateController.h
|
||||
${CLIENT_ROOT_DIR}/core/repositories/secureServersRepository.h
|
||||
${CLIENT_ROOT_DIR}/core/repositories/secureAppSettingsRepository.h
|
||||
${CLIENT_ROOT_DIR}/core/protocols/qmlRegisterProtocols.h
|
||||
${CLIENT_ROOT_DIR}/ui/utils/pages.h
|
||||
${CLIENT_PAGE_ENUM_HEADER}
|
||||
${CLIENT_ROOT_DIR}/ui/utils/qAutoStart.h
|
||||
${CLIENT_ROOT_DIR}/ui/utils/mtProxyPublicHostInput.h
|
||||
${CLIENT_ROOT_DIR}/core/protocols/vpnProtocol.h
|
||||
${CMAKE_CURRENT_BINARY_DIR}/version.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/selfhosted/sshClient.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/networkUtilities.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/payloadSender.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/serialization/serialization.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/serialization/transfer.h
|
||||
${CLIENT_ROOT_DIR}/../common/logger/logger.h
|
||||
${CLIENT_ROOT_DIR}/ui/utils/qmlUtils.h
|
||||
${CLIENT_ROOT_DIR}/../common/crypto/cryptoUtils.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/api/apiUtils.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/api/countryCatalog.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/api/gatewayPayloadBuilder.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/localProxy/localProxyConstants.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/localProxy/portUtils.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/localProxy/xrayConfigUtils.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/osSignalHandler.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/swiftBridge.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/utilities.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/managementServer.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/constants.h
|
||||
@@ -110,21 +125,30 @@ 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/installers/tProxyInstaller.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/appSplitTunnelingController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/ipSplitTunnelingController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/allowedDnsController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/selfhosted/exportController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/connectionController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/settingsController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/localProxyController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/logsController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/servicesCatalogController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/storePurchaseController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/subscriptionController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/newsController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/updateController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/repositories/secureServersRepository.cpp
|
||||
${CLIENT_ROOT_DIR}/core/repositories/secureAppSettingsRepository.cpp
|
||||
${CLIENT_ROOT_DIR}/ui/utils/qAutoStart.cpp
|
||||
${CLIENT_ROOT_DIR}/ui/utils/mtProxyPublicHostInput.cpp
|
||||
${CLIENT_ROOT_DIR}/core/protocols/vpnProtocol.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/selfhosted/sshClient.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/networkUtilities.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/payloadSender.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/serialization/outbound.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/serialization/inbound.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/serialization/ss.cpp
|
||||
@@ -134,8 +158,13 @@ set(SOURCES ${SOURCES}
|
||||
${CLIENT_ROOT_DIR}/core/utils/serialization/vmess.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/serialization/vmess_new.cpp
|
||||
${CLIENT_ROOT_DIR}/../common/logger/logger.cpp
|
||||
${CLIENT_ROOT_DIR}/ui/utils/qmlUtils.cpp
|
||||
${CLIENT_ROOT_DIR}/../common/crypto/cryptoUtils.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/api/apiUtils.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/api/countryCatalog.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/api/gatewayPayloadBuilder.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/localProxy/portUtils.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/localProxy/xrayConfigUtils.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
|
||||
@@ -199,12 +228,14 @@ 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
|
||||
)
|
||||
|
||||
|
||||
@@ -96,13 +96,19 @@ ProtocolConfig AwgConfigurator::createConfig(const ServerCredentials &credential
|
||||
newClientConfig.specialJunk4 = configMap.value(configKey::specialJunk4);
|
||||
newClientConfig.specialJunk5 = configMap.value(configKey::specialJunk5);
|
||||
|
||||
if (container == DockerContainer::Awg2) {
|
||||
newClientConfig.cookieReplyPacketJunkSize = configMap.value(configKey::cookieReplyPacketJunkSize);
|
||||
newClientConfig.transportPacketJunkSize = configMap.value(configKey::transportPacketJunkSize);
|
||||
}
|
||||
|
||||
newClientConfig.isObfuscationEnabled = false;
|
||||
|
||||
newClientConfig.cookieReplyPacketJunkSize = configMap.value(configKey::cookieReplyPacketJunkSize);
|
||||
newClientConfig.transportPacketJunkSize = configMap.value(configKey::transportPacketJunkSize);
|
||||
|
||||
newClientConfig.headerProtectionKey = configMap.value(configKey::headerProtectionKey);
|
||||
newClientConfig.contentPaddingAddition = configMap.value(configKey::contentPaddingAddition);
|
||||
newClientConfig.rekeyAfterTime = configMap.value(configKey::rekeyAfterTime);
|
||||
newClientConfig.rekeyTimeout = configMap.value(configKey::rekeyTimeout);
|
||||
newClientConfig.rejectAfterTime = configMap.value(configKey::rejectAfterTime);
|
||||
newClientConfig.keepaliveTimeout = configMap.value(configKey::keepaliveTimeout);
|
||||
newClientConfig.maxHandshakeAttempts = configMap.value(configKey::maxHandshakeAttempts);
|
||||
newClientConfig.randomTrailers = configMap.value(configKey::randomTrailers);
|
||||
newClientConfig.disableCookies = configMap.value(configKey::disableCookies);
|
||||
|
||||
protocolConfig.setClientConfig(newClientConfig);
|
||||
|
||||
return protocolConfig;
|
||||
|
||||
@@ -241,7 +241,7 @@ OpenVpnConfigurator::ConnectionData OpenVpnConfigurator::createCertRequest()
|
||||
connData.clientId = Utils::getRandomString(32);
|
||||
|
||||
int ret = 0;
|
||||
int nVersion = 1;
|
||||
int nVersion = 0;
|
||||
|
||||
QByteArray clientIdUtf8 = connData.clientId.toUtf8();
|
||||
|
||||
|
||||
@@ -233,6 +233,13 @@ ProtocolConfig WireguardConfigurator::createConfig(const ServerCredentials &cred
|
||||
QString scriptData = amnezia::scriptData(m_configTemplate, container);
|
||||
QString config = m_sshSession->replaceVars(scriptData, vars);
|
||||
|
||||
// The template lists every possible key, but each parameter is optional -
|
||||
// drop the lines whose value came out empty
|
||||
static const QRegularExpression emptyValueLine(R"(^\s*\S+\s*=\s*$)");
|
||||
auto configTemplateLines = config.split("\n");
|
||||
configTemplateLines.removeIf([](const QString &line) { return emptyValueLine.match(line).hasMatch(); });
|
||||
config = configTemplateLines.join("\n");
|
||||
|
||||
ConnectionData connData = prepareWireguardConfig(credentials, container, wireguardServerConfig, awgServerConfig, dnsSettings, errorCode);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return WireGuardProtocolConfig{};
|
||||
@@ -266,7 +273,9 @@ ProtocolConfig WireguardConfigurator::createConfig(const ServerCredentials &cred
|
||||
clientConfig.presharedKey = connData.pskKey;
|
||||
clientConfig.clientId = connData.clientPubKey;
|
||||
clientConfig.allowedIps = QStringList { "0.0.0.0/0", "::/0" };
|
||||
clientConfig.persistentKeepAlive = "25";
|
||||
const bool useKeepAliveRange = awgServerConfig && awgServerConfig->hasAwg3Params();
|
||||
clientConfig.persistentKeepAlive = useKeepAliveRange ? protocols::awg::defaultPersistentKeepAlive
|
||||
: protocols::wireguard::defaultPersistentKeepAlive;
|
||||
clientConfig.mtu = mtu;
|
||||
clientConfig.isObfuscationEnabled = false;
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QDateTime>
|
||||
#include <QThread>
|
||||
#include <QUuid>
|
||||
#include "logger.h"
|
||||
|
||||
@@ -20,183 +22,661 @@
|
||||
#include "core/models/protocols/xrayProtocolConfig.h"
|
||||
|
||||
namespace {
|
||||
Logger logger("XrayConfigurator");
|
||||
}
|
||||
Logger logger("XrayConfigurator");
|
||||
|
||||
QString effectiveClientFlow(const amnezia::XrayServerConfig &srv)
|
||||
{
|
||||
return amnezia::xrayEffective::clientFlow(srv);
|
||||
}
|
||||
|
||||
QString effectiveSecurity(const amnezia::XrayServerConfig &srv)
|
||||
{
|
||||
return amnezia::xrayEffective::security(srv);
|
||||
}
|
||||
|
||||
// Desktop applies this in XrayProtocol::start(); iOS/Android pass JSON straight to libxray — same fixes here.
|
||||
void sanitizeXrayNativeConfig(amnezia::ProtocolConfig &pc)
|
||||
{
|
||||
const QString before = pc.nativeConfig();
|
||||
const QString after = amnezia::sanitizeNativeConfig(before);
|
||||
if (after != before) {
|
||||
pc.setNativeConfig(after);
|
||||
}
|
||||
}
|
||||
|
||||
QString tlsFingerprintFromEnsureOutput(const QString &stdOut)
|
||||
{
|
||||
const QString prefix = QStringLiteral("amnezia_tls_fp=");
|
||||
const auto lines = stdOut.split(QLatin1Char('\n'));
|
||||
for (QString line : lines) {
|
||||
line = line.trimmed();
|
||||
if (!line.startsWith(prefix))
|
||||
continue;
|
||||
QString fp = line.mid(prefix.size());
|
||||
fp.remove(QLatin1Char(':'));
|
||||
fp.remove(QLatin1Char(' '));
|
||||
fp = fp.toLower();
|
||||
if (fp.size() != 64)
|
||||
return {};
|
||||
for (const QChar ch : fp) {
|
||||
const ushort u = ch.unicode();
|
||||
const bool hex = (u >= '0' && u <= '9') || (u >= 'a' && u <= 'f');
|
||||
if (!hex)
|
||||
return {};
|
||||
}
|
||||
return fp;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
} // 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::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 (fileError != ErrorCode::NoError) {
|
||||
logger.error() << "Xray readRealityKeyFiles: ssh failed path=" << path
|
||||
<< "error=" << static_cast<int>(fileError);
|
||||
return fileError;
|
||||
}
|
||||
if (attempt < 2) {
|
||||
QThread::msleep(500);
|
||||
}
|
||||
}
|
||||
logger.error() << "Xray readRealityKeyFiles: key file empty path=" << path;
|
||||
return ErrorCode::XrayKeysReadFailed;
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const QString flowValue = effectiveClientFlow(srv);
|
||||
QString realityPublicKey;
|
||||
QString realityShortId;
|
||||
if (effectiveSecurity(srv) == 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();
|
||||
const QJsonObject originalConfig = serverConfig;
|
||||
|
||||
QJsonArray clients = XrayServerConfig::clientsFromServerInboundJson(serverConfig);
|
||||
QString clientId;
|
||||
|
||||
if (appendNewClient) {
|
||||
const bool adoptingOwnAccount = !(xrayCfg->hasClientConfig() && !xrayCfg->clientConfig->id.isEmpty());
|
||||
|
||||
QString ownClientId;
|
||||
if (readContainerKeyFile(container, credentials, QString::fromLatin1(amnezia::protocols::xray::uuidPath),
|
||||
ownClientId)
|
||||
!= ErrorCode::NoError) {
|
||||
ownClientId.clear();
|
||||
}
|
||||
|
||||
const int existingIndex =
|
||||
adoptingOwnAccount ? XrayServerConfig::indexOfClient(clients, ownClientId) : -1;
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
clientId = ownClientId;
|
||||
clients[existingIndex] =
|
||||
XrayServerConfig::applyFlowToClient(clients[existingIndex].toObject(), flowValue);
|
||||
} else {
|
||||
const bool takeOverOwnAccount = adoptingOwnAccount && !ownClientId.isEmpty();
|
||||
clientId = takeOverOwnAccount ? ownClientId : QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
clients.append(XrayServerConfig::makeClientEntry(clientId, flowValue));
|
||||
}
|
||||
} else {
|
||||
if (clients.isEmpty()) {
|
||||
logger.error() << "Server config has no VLESS clients";
|
||||
return ErrorCode::XrayServerNoVlessClients;
|
||||
}
|
||||
clientId = XrayServerConfig::firstClientId(clients);
|
||||
if (clientId.isEmpty()) {
|
||||
logger.error() << "Server config VLESS client has empty id";
|
||||
return ErrorCode::XrayServerNoVlessClients;
|
||||
}
|
||||
clients = XrayServerConfig::applyFlowToClients(clients, flowValue);
|
||||
}
|
||||
|
||||
const XrayServerJsonStatus writeStatus =
|
||||
XrayServerConfig::setClientsInServerInboundJson(serverConfig, clients);
|
||||
if (writeStatus != XrayServerJsonStatus::Ok) {
|
||||
return ErrorCode::XrayServerConfigInvalid;
|
||||
}
|
||||
|
||||
const QString listenPort = srv.port.isEmpty() ? QString::fromLatin1(amnezia::protocols::xray::defaultPort)
|
||||
: srv.port;
|
||||
if (serverConfig != originalConfig) {
|
||||
errorCode = uploadServerConfigAtomically(credentials, container, listenPort, serverConfig);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
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;
|
||||
}
|
||||
|
||||
ErrorCode XrayConfigurator::readContainerKeyFile(DockerContainer container, const ServerCredentials &credentials,
|
||||
const QString &path, QString &out) const
|
||||
{
|
||||
out.clear();
|
||||
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 (fileError != ErrorCode::NoError) {
|
||||
logger.error() << "Xray readContainerKeyFile: ssh failed path=" << path
|
||||
<< "error=" << static_cast<int>(fileError);
|
||||
return fileError;
|
||||
}
|
||||
if (attempt < 2) {
|
||||
QThread::msleep(500);
|
||||
}
|
||||
}
|
||||
logger.error() << "Xray readContainerKeyFile: key file empty path=" << path;
|
||||
return ErrorCode::XrayKeysReadFailed;
|
||||
}
|
||||
|
||||
ErrorCode XrayConfigurator::writeServerConfigForSetup(const ServerCredentials &credentials, DockerContainer container,
|
||||
ContainerConfig &containerConfig, const DnsSettings &dnsSettings,
|
||||
bool useAtomicApply)
|
||||
{
|
||||
Q_UNUSED(dnsSettings);
|
||||
|
||||
const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>();
|
||||
if (!xrayCfg) {
|
||||
logger.error() << "Xray writeServerConfigForSetup: missing XrayProtocolConfig";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
const XrayServerConfig &srv = xrayCfg->serverConfig;
|
||||
if (srv.isThirdPartyConfig) {
|
||||
logger.info() << "Xray writeServerConfigForSetup: skipped (third-party/native profile)";
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode errorCode = ErrorCode::NoError;
|
||||
|
||||
QString clientId;
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(amnezia::protocols::xray::uuidPath), clientId);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
const QString securityEff = effectiveSecurity(srv);
|
||||
QString tlsPin;
|
||||
if (securityEff == QLatin1String("tls")) {
|
||||
errorCode = ensureTlsCertificate(credentials, container, tlsPin);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
|
||||
QString realityPrivateKey;
|
||||
QString realityPublicKey;
|
||||
QString realityShortId;
|
||||
if (securityEff == QLatin1String("reality")) {
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(amnezia::protocols::xray::PrivateKeyPath), realityPrivateKey);
|
||||
if (errorCode != ErrorCode::NoError)
|
||||
return errorCode;
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(amnezia::protocols::xray::PublicKeyPath), realityPublicKey);
|
||||
if (errorCode != ErrorCode::NoError)
|
||||
return errorCode;
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(amnezia::protocols::xray::shortidPath), realityShortId);
|
||||
if (errorCode != ErrorCode::NoError)
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
const QString flowValue = effectiveClientFlow(srv);
|
||||
const QJsonArray clients = collectServerClients(credentials, container, flowValue, clientId, errorCode);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Xray writeServerConfigForSetup: upload failed, error=" << static_cast<int>(errorCode);
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
const QString portEff = srv.port.isEmpty() ? QString::fromLatin1(amnezia::protocols::xray::defaultPort) : srv.port;
|
||||
|
||||
|
||||
XrayServerInboundInputs inputs;
|
||||
inputs.clients = clients;
|
||||
inputs.realityPrivateKey = realityPrivateKey;
|
||||
inputs.realityShortId = realityShortId;
|
||||
|
||||
const QJsonObject inboundJson = srv.toServerInboundJson(inputs);
|
||||
if (useAtomicApply) {
|
||||
errorCode = uploadServerConfigAtomically(credentials, container, portEff, inboundJson);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
} else {
|
||||
const QString json = QString::fromUtf8(QJsonDocument(inboundJson).toJson());
|
||||
errorCode = m_sshSession->uploadTextFileToContainer(container, credentials, json,
|
||||
QString::fromLatin1(amnezia::protocols::xray::serverConfigPath),
|
||||
libssh::ScpOverwriteMode::ScpOverwriteExisting);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
|
||||
XrayProtocolConfig updated =
|
||||
buildClientProtocolConfig(credentials, container, srv, clientId, errorCode,
|
||||
realityPublicKey, realityShortId, tlsPin);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Xray writeServerConfigForSetup: buildClientProtocolConfig failed, error="
|
||||
<< static_cast<int>(errorCode);
|
||||
return errorCode;
|
||||
}
|
||||
containerConfig.protocolConfig = updated;
|
||||
logger.info() << "Xray writeServerConfigForSetup: done, clientId=" << clientId;
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
QJsonArray XrayConfigurator::collectServerClients(const ServerCredentials &credentials, DockerContainer container,
|
||||
const QString &flowValue, const QString &fallbackClientId,
|
||||
ErrorCode &outError) const
|
||||
{
|
||||
outError = ErrorCode::NoError;
|
||||
const QString configPath = QString::fromLatin1(amnezia::protocols::xray::serverConfigPath);
|
||||
|
||||
QString currentConfig;
|
||||
for (int attempt = 0; attempt < 3; ++attempt) {
|
||||
ErrorCode readError = ErrorCode::NoError;
|
||||
currentConfig = m_sshSession->getTextFileFromContainer(container, credentials, configPath, readError);
|
||||
if (readError == ErrorCode::NoError && !currentConfig.trimmed().isEmpty()) {
|
||||
break;
|
||||
}
|
||||
currentConfig.clear();
|
||||
if (attempt < 2) {
|
||||
QThread::msleep(500);
|
||||
}
|
||||
}
|
||||
|
||||
QJsonArray existing;
|
||||
if (currentConfig.trimmed().isEmpty()) {
|
||||
QString probeOut;
|
||||
auto collect = [&probeOut](const QString &data, libssh::Client &) {
|
||||
probeOut += data + "\n";
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
const QString probe =
|
||||
QStringLiteral("if [ -s %1 ]; then echo amnezia_config=present; else echo amnezia_config=absent; fi")
|
||||
.arg(configPath);
|
||||
const ErrorCode probeError = m_sshSession->runContainerScript(credentials, container, probe, collect, collect);
|
||||
|
||||
if (probeError != ErrorCode::NoError || !probeOut.contains(QLatin1String("amnezia_config="))) {
|
||||
outError = ErrorCode::XrayServerConfigInvalid;
|
||||
return {};
|
||||
}
|
||||
if (probeOut.contains(QLatin1String("amnezia_config=present"))) {
|
||||
outError = ErrorCode::XrayServerConfigInvalid;
|
||||
return {};
|
||||
}
|
||||
} else {
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(currentConfig.toUtf8());
|
||||
if (!doc.isObject()) {
|
||||
outError = ErrorCode::XrayServerConfigInvalid;
|
||||
return {};
|
||||
}
|
||||
existing = XrayServerConfig::clientsFromServerInboundJson(doc.object());
|
||||
}
|
||||
|
||||
QJsonArray clients =
|
||||
XrayServerConfig::applyFlowToClients(existing, flowValue, XrayClientListFilter::DropWithoutId);
|
||||
|
||||
if (!fallbackClientId.isEmpty()
|
||||
&& XrayServerConfig::indexOfClient(clients, fallbackClientId) < 0) {
|
||||
clients.append(XrayServerConfig::makeClientEntry(fallbackClientId, flowValue));
|
||||
}
|
||||
|
||||
return clients;
|
||||
}
|
||||
|
||||
ErrorCode XrayConfigurator::ensureTlsCertificate(const ServerCredentials &credentials, DockerContainer container,
|
||||
QString &outFingerprint) const
|
||||
{
|
||||
outFingerprint.clear();
|
||||
|
||||
QString stdOut;
|
||||
auto collect = [&stdOut](const QString &data, libssh::Client &) {
|
||||
stdOut += data + QLatin1Char('\n');
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
|
||||
const QString certPath = QString::fromLatin1(amnezia::protocols::xray::tlsCertPath);
|
||||
const QString keyPath = QString::fromLatin1(amnezia::protocols::xray::tlsKeyPath);
|
||||
const QString script = QStringLiteral(
|
||||
"has_key() { [ -f \"$1\" ] && [ -n \"$(tr -d '[:space:]' < \"$1\" 2>/dev/null)\" ]; }\n"
|
||||
"CERT=%1\n"
|
||||
"KEY=%2\n"
|
||||
"if has_key \"$CERT\" && has_key \"$KEY\"; then echo amnezia_tls=present;\n"
|
||||
"else\n"
|
||||
" openssl req -x509 -newkey rsa:2048 -nodes -days 3650 -subj \"/CN=amnezia-xray\" "
|
||||
"-keyout \"$KEY\" -out \"$CERT\" || true\n"
|
||||
" if has_key \"$CERT\" && has_key \"$KEY\"; then echo amnezia_tls=created;\n"
|
||||
" else echo amnezia_tls=failed; exit 1; fi\n"
|
||||
"fi\n"
|
||||
"FP=$(openssl x509 -noout -fingerprint -sha256 -in \"$CERT\" 2>/dev/null "
|
||||
"| sed 's/.*Fingerprint=//' | tr -d ':' | tr 'A-F' 'a-f')\n"
|
||||
"if [ -z \"$FP\" ]; then echo amnezia_tls=failed; exit 1; fi\n"
|
||||
"echo amnezia_tls_fp=$FP\n"
|
||||
"exit 0\n")
|
||||
.arg(certPath, keyPath);
|
||||
|
||||
const ErrorCode scriptError = m_sshSession->runContainerScript(credentials, container, script, collect, collect);
|
||||
const bool present = stdOut.contains(QLatin1String("amnezia_tls=present"));
|
||||
const bool created = stdOut.contains(QLatin1String("amnezia_tls=created"));
|
||||
outFingerprint = tlsFingerprintFromEnsureOutput(stdOut);
|
||||
if (scriptError != ErrorCode::NoError || (!present && !created) || outFingerprint.isEmpty()) {
|
||||
outFingerprint.clear();
|
||||
return ErrorCode::XrayTlsNotSupported;
|
||||
}
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode XrayConfigurator::uploadServerConfigAtomically(const ServerCredentials &credentials,
|
||||
DockerContainer container, const QString &listenPort,
|
||||
const QJsonObject &serverConfig) const
|
||||
{
|
||||
const QString livePath = QString::fromLatin1(amnezia::protocols::xray::serverConfigPath);
|
||||
const QString stagedPath = QStringLiteral("/opt/amnezia/xray/server.new.json");
|
||||
|
||||
const QString json = QString::fromUtf8(QJsonDocument(serverConfig).toJson());
|
||||
ErrorCode errorCode = m_sshSession->uploadTextFileToContainer(container, credentials, json, stagedPath,
|
||||
libssh::ScpOverwriteMode::ScpOverwriteExisting);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
const QString swapScript = QStringLiteral(
|
||||
"LIVE=%1\n"
|
||||
"STAGED=%2\n"
|
||||
"BACKUP=\"$LIVE.bak\"\n"
|
||||
"if [ ! -s \"$STAGED\" ]; then echo \"amnezia_apply=missing\"; exit 0; fi\n"
|
||||
"if ! xray -test -format json -config \"$STAGED\" > /tmp/xray_test.log 2>&1; then\n"
|
||||
" echo \"amnezia_apply=invalid\"\n"
|
||||
" rm -f \"$STAGED\"\n"
|
||||
" exit 0\n"
|
||||
"fi\n"
|
||||
"if [ -f \"$LIVE\" ]; then cp \"$LIVE\" \"$BACKUP\"; fi\n"
|
||||
"mv \"$STAGED\" \"$LIVE\"\n"
|
||||
"echo \"amnezia_apply=swapped\"\n")
|
||||
.arg(livePath, stagedPath);
|
||||
|
||||
QString swapOut;
|
||||
auto collectSwap = [&swapOut](const QString &data, libssh::Client &) {
|
||||
swapOut += data + "\n";
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
errorCode = m_sshSession->runContainerScript(credentials, container, swapScript, collectSwap, collectSwap);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
if (swapOut.contains(QLatin1String("amnezia_apply=missing"))) {
|
||||
return ErrorCode::XrayServerConfigRejected;
|
||||
}
|
||||
if (swapOut.contains(QLatin1String("amnezia_apply=invalid"))) {
|
||||
return ErrorCode::XrayServerConfigRejected;
|
||||
}
|
||||
if (!swapOut.contains(QLatin1String("amnezia_apply=swapped"))) {
|
||||
return ErrorCode::XrayServerConfigInvalid;
|
||||
}
|
||||
|
||||
if (restartXrayContainer(credentials, container) && xrayProcessIsUp(credentials, container)) {
|
||||
m_sshSession->runContainerScript(credentials, container,
|
||||
QStringLiteral("rm -f %1.bak").arg(livePath));
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
const QString restoreScript = QStringLiteral(
|
||||
"LIVE=%1\n"
|
||||
"BACKUP=\"$LIVE.bak\"\n"
|
||||
"if [ -f \"$BACKUP\" ]; then cp \"$BACKUP\" \"$LIVE\"; echo amnezia_restore=done; "
|
||||
"else echo amnezia_restore=nobackup; fi\n")
|
||||
.arg(livePath);
|
||||
QString restoreOut;
|
||||
auto collectRestore = [&restoreOut](const QString &data, libssh::Client &) {
|
||||
restoreOut += data + "\n";
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
m_sshSession->runContainerScript(credentials, container, restoreScript, collectRestore, collectRestore);
|
||||
if (!restoreOut.contains(QLatin1String("amnezia_restore=done"))) {
|
||||
return ErrorCode::XrayServerNotServing;
|
||||
}
|
||||
if (restartXrayContainer(credentials, container) && xrayProcessIsUp(credentials, container)) {
|
||||
return ErrorCode::XrayServerConfigRolledBack;
|
||||
}
|
||||
return ErrorCode::XrayServerNotServing;
|
||||
}
|
||||
|
||||
bool XrayConfigurator::restartXrayContainer(const ServerCredentials &credentials, DockerContainer container) const
|
||||
{
|
||||
const ErrorCode errorCode = m_sshSession->runScript(
|
||||
credentials,
|
||||
SshSession::replaceVars(QStringLiteral("sudo docker restart $CONTAINER_NAME"),
|
||||
amnezia::genBaseVars(credentials, container, QString(), QString())));
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool XrayConfigurator::xrayProcessIsUp(const ServerCredentials &credentials, DockerContainer container) const
|
||||
{
|
||||
const QString script = QStringLiteral(
|
||||
"for i in 1 2 3 4 5 6; do\n"
|
||||
" if pgrep xray >/dev/null 2>&1 || ps 2>/dev/null | grep -v grep | grep -q \"[x]ray\"; then\n"
|
||||
" echo amnezia_serving=yes\n"
|
||||
" exit 0\n"
|
||||
" fi\n"
|
||||
" sleep 1\n"
|
||||
"done\n"
|
||||
"echo amnezia_serving=no\n");
|
||||
QString out;
|
||||
auto collect = [&out](const QString &data, libssh::Client &) {
|
||||
out += data + "\n";
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
const ErrorCode errorCode = m_sshSession->runContainerScript(credentials, container, script, collect, collect);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return false;
|
||||
}
|
||||
return out.contains(QLatin1String("amnezia_serving=yes"));
|
||||
}
|
||||
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 QString &prefetchedTlsPin) const
|
||||
{
|
||||
const XrayServerConfig* serverConfig = nullptr;
|
||||
if (auto* xrayConfig = containerConfig.protocolConfig.as<XrayProtocolConfig>()) {
|
||||
serverConfig = &xrayConfig->serverConfig;
|
||||
QString xrayPublicKey = prefetchedRealityPublicKey;
|
||||
QString xrayShortId = prefetchedRealityShortId;
|
||||
QString tlsPin = prefetchedTlsPin;
|
||||
|
||||
const QString securityEff = effectiveSecurity(srv);
|
||||
|
||||
|
||||
if (securityEff == QLatin1String(amnezia::protocols::xray::securityReality)) {
|
||||
if (xrayPublicKey.isEmpty() || xrayShortId.isEmpty()) {
|
||||
errorCode = readRealityKeyFiles(container, credentials, xrayPublicKey, xrayShortId);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (securityEff == QLatin1String(amnezia::protocols::xray::securityTls) && !srv.isThirdPartyConfig) {
|
||||
if (tlsPin.isEmpty()) {
|
||||
errorCode = ensureTlsCertificate(credentials, container, tlsPin);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
if (tlsPin.isEmpty()) {
|
||||
errorCode = ErrorCode::XrayTlsNotSupported;
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
XrayProtocolConfig protocolConfig;
|
||||
protocolConfig.serverConfig = srv;
|
||||
|
||||
XrayClientOutboundInputs inputs;
|
||||
inputs.serverAddress = credentials.hostName;
|
||||
inputs.clientId = clientId;
|
||||
inputs.realityPublicKey = xrayPublicKey;
|
||||
inputs.realityShortId = xrayShortId;
|
||||
inputs.tlsPinnedPeerCertSha256 = tlsPin;
|
||||
|
||||
XrayClientConfig clientConfig;
|
||||
const QJsonObject clientOutbound = protocolConfig.toClientOutboundJson(inputs);
|
||||
clientConfig.nativeConfig = QString::fromUtf8(QJsonDocument(clientOutbound).toJson(QJsonDocument::Compact));
|
||||
clientConfig.localPort = QString::fromLatin1(amnezia::protocols::xray::defaultLocalProxyPort);
|
||||
clientConfig.id = clientId;
|
||||
protocolConfig.setClientConfig(clientConfig);
|
||||
|
||||
return protocolConfig;
|
||||
}
|
||||
|
||||
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,14 @@
|
||||
#define XRAY_CONFIGURATOR_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QJsonArray>
|
||||
#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 +21,62 @@ 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);
|
||||
|
||||
amnezia::ErrorCode writeServerConfigForSetup(const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container,
|
||||
amnezia::ContainerConfig &containerConfig,
|
||||
const amnezia::DnsSettings &dnsSettings,
|
||||
bool useAtomicApply = false);
|
||||
|
||||
private:
|
||||
QJsonArray collectServerClients(const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container, const QString &flowValue,
|
||||
const QString &fallbackClientId, amnezia::ErrorCode &outError) const;
|
||||
|
||||
amnezia::ErrorCode uploadServerConfigAtomically(const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container, const QString &listenPort,
|
||||
const QJsonObject &serverConfig) const;
|
||||
|
||||
bool restartXrayContainer(const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container) const;
|
||||
|
||||
bool xrayProcessIsUp(const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container) const;
|
||||
|
||||
amnezia::ErrorCode readContainerKeyFile(amnezia::DockerContainer container,
|
||||
const amnezia::ServerCredentials &credentials,
|
||||
const QString &path, QString &out) const;
|
||||
|
||||
QString prepareServerConfig(const amnezia::ServerCredentials &credentials, amnezia::DockerContainer container, const amnezia::ContainerConfig &containerConfig,
|
||||
const amnezia::DnsSettings &dnsSettings,
|
||||
amnezia::ErrorCode &errorCode);
|
||||
|
||||
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 QString &prefetchedTlsPin = {}) const;
|
||||
|
||||
amnezia::ErrorCode readRealityKeyFiles(amnezia::DockerContainer container,
|
||||
const amnezia::ServerCredentials &credentials,
|
||||
QString &outPublicKey,
|
||||
QString &outShortId) const;
|
||||
|
||||
amnezia::ErrorCode ensureTlsCertificate(const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container,
|
||||
QString &outFingerprint) const;
|
||||
};
|
||||
|
||||
#endif // XRAY_CONFIGURATOR_H
|
||||
@@ -1,52 +1,95 @@
|
||||
#include "newsController.h"
|
||||
|
||||
#include "core/controllers/gatewayController.h"
|
||||
#include "core/utils/api/apiEnums.h"
|
||||
#include "core/repositories/secureServersRepository.h"
|
||||
#include "core/utils/api/apiUtils.h"
|
||||
#include "core/utils/api/gatewayPayloadBuilder.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());
|
||||
|
||||
QJsonObject payload;
|
||||
payload.insert("locale", m_appSettingsRepository->getAppLanguage().name().split("_").first());
|
||||
QSharedPointer<GatewayController> gatewayController(
|
||||
new GatewayController(m_appSettingsRepository->getGatewayEndpoint(),
|
||||
m_appSettingsRepository->isDevGatewayEnv(),
|
||||
apiDefs::requestTimeoutMsecs,
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled(),
|
||||
m_appSettingsRepository),
|
||||
&QObject::deleteLater);
|
||||
|
||||
const QJsonObject stacksJson = stacks.toJson();
|
||||
if (stacksJson.contains(apiDefs::key::userCountryCode)) {
|
||||
payload.insert(apiDefs::key::userCountryCode, stacksJson.value(apiDefs::key::userCountryCode));
|
||||
}
|
||||
if (stacksJson.contains(apiDefs::key::serviceType)) {
|
||||
payload.insert(apiDefs::key::serviceType, stacksJson.value(apiDefs::key::serviceType));
|
||||
}
|
||||
// both country codes and service types are arrays here, one entry per gateway stack the user has
|
||||
const QJsonObject payload = GatewayPayloadBuilder(m_appSettingsRepository)
|
||||
.addField(apiDefs::key::locale, apiUtils::getAppLanguageCode(m_appSettingsRepository))
|
||||
.addField(apiDefs::key::userCountryCode, services.value(apiDefs::key::userCountryCode))
|
||||
.addField(apiDefs::key::serviceType, services.value(apiDefs::key::serviceType))
|
||||
.build();
|
||||
|
||||
auto future = gatewayController->postAsync(QString("%1v1/news"), payload);
|
||||
return future.then([gatewayController](QPair<ErrorCode, QByteArray> result) -> QPair<ErrorCode, QJsonArray> {
|
||||
@@ -69,4 +112,3 @@ QFuture<QPair<ErrorCode, QJsonArray>> NewsController::fetchNews()
|
||||
return qMakePair(ErrorCode::NoError, newsArray);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "servicesCatalogController.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QSysInfo>
|
||||
#include <QJsonArray>
|
||||
#include <QEventLoop>
|
||||
#include <QDebug>
|
||||
@@ -11,13 +10,17 @@
|
||||
#include <limits>
|
||||
|
||||
#include "core/controllers/gatewayController.h"
|
||||
#include "core/utils/api/apiEnums.h"
|
||||
#include "core/utils/api/gatewayPayloadBuilder.h"
|
||||
#include "core/utils/serverConfigUtils.h"
|
||||
#include "core/utils/constants/apiKeys.h"
|
||||
#include "core/utils/constants/apiConstants.h"
|
||||
#include "version.h"
|
||||
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
#include "platforms/ios/ios_controller.h"
|
||||
#elif defined(Q_OS_ANDROID)
|
||||
#include "platforms/android/android_controller.h"
|
||||
#include "platforms/android/android_utils.h"
|
||||
#endif
|
||||
|
||||
namespace
|
||||
@@ -28,9 +31,12 @@ namespace
|
||||
constexpr char subscriptionPlans[] = "subscription_plans";
|
||||
constexpr char storeProductId[] = "store_product_id";
|
||||
constexpr char priceLabel[] = "price_label";
|
||||
constexpr char priceAmount[] = "price_amount";
|
||||
constexpr char subtitle[] = "subtitle";
|
||||
constexpr char isTrial[] = "is_trial";
|
||||
constexpr char minPriceLabel[] = "min_price_label";
|
||||
constexpr char hasFreeTrial[] = "has_free_trial";
|
||||
constexpr char trialDays[] = "trial_days";
|
||||
}
|
||||
|
||||
namespace serviceType
|
||||
@@ -38,12 +44,14 @@ namespace
|
||||
constexpr char amneziaPremium[] = "amnezia-premium";
|
||||
}
|
||||
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
struct StoreKitPlanQuote {
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE) || defined(Q_OS_ANDROID)
|
||||
struct SubscriptionPlanQuote {
|
||||
QString displayPrice;
|
||||
double priceAmount = 0.0;
|
||||
double subscriptionBillingMonths = 0.0;
|
||||
QString displayPricePerMonth;
|
||||
bool hasFreeTrial = false;
|
||||
int trialDays = 0;
|
||||
};
|
||||
|
||||
constexpr double oneMonthThreshold = 1.0 + 1e-6;
|
||||
@@ -76,48 +84,9 @@ namespace
|
||||
return productIds;
|
||||
}
|
||||
|
||||
QHash<QString, StoreKitPlanQuote> buildStoreKitQuoteMap(const QList<QVariantMap> &fetchedProducts)
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
QHash<QString, SubscriptionPlanQuote> buildStoreKitQuoteMap(const QStringList &productIds)
|
||||
{
|
||||
QHash<QString, StoreKitPlanQuote> quotesByProductId;
|
||||
quotesByProductId.reserve(fetchedProducts.size());
|
||||
|
||||
for (const QVariantMap &productInfo : fetchedProducts) {
|
||||
const QString productId = productInfo.value(QStringLiteral("productId")).toString();
|
||||
if (productId.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString displayPrice = productInfo.value(QStringLiteral("displayPrice")).toString();
|
||||
if (displayPrice.isEmpty()) {
|
||||
const QString price = productInfo.value(QStringLiteral("price")).toString();
|
||||
const QString currencyCode = productInfo.value(QStringLiteral("currencyCode")).toString();
|
||||
displayPrice = currencyCode.isEmpty() ? price : (price + QLatin1Char(' ') + currencyCode);
|
||||
}
|
||||
|
||||
StoreKitPlanQuote quote;
|
||||
quote.displayPrice = displayPrice;
|
||||
quote.priceAmount = productInfo.value(QStringLiteral("priceAmount")).toDouble();
|
||||
quote.subscriptionBillingMonths = productInfo.value(QStringLiteral("subscriptionBillingMonths")).toDouble();
|
||||
quote.displayPricePerMonth = productInfo.value(QStringLiteral("displayPricePerMonth")).toString();
|
||||
quotesByProductId.insert(productId, quote);
|
||||
}
|
||||
|
||||
return quotesByProductId;
|
||||
}
|
||||
|
||||
void mergeStoreKitPricesIntoPremiumPlans(QJsonObject &data)
|
||||
{
|
||||
QJsonArray services = data.value(apiDefs::key::services).toArray();
|
||||
if (services.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QStringList productIds = collectPremiumStoreProductIds(services);
|
||||
if (productIds.isEmpty()) {
|
||||
qInfo().noquote() << "[IAP] No store_product_id in premium plans; skip StoreKit merge into services payload";
|
||||
return;
|
||||
}
|
||||
|
||||
QList<QVariantMap> fetchedProducts;
|
||||
QEventLoop loop;
|
||||
IosController::Instance()->fetchProducts(productIds,
|
||||
@@ -134,7 +103,117 @@ namespace
|
||||
});
|
||||
loop.exec();
|
||||
|
||||
const QHash<QString, StoreKitPlanQuote> quotesByProductId = buildStoreKitQuoteMap(fetchedProducts);
|
||||
QHash<QString, SubscriptionPlanQuote> quotesByProductId;
|
||||
quotesByProductId.reserve(fetchedProducts.size());
|
||||
|
||||
for (const QVariantMap &productInfo : fetchedProducts) {
|
||||
const QString productId = productInfo.value(QStringLiteral("productId")).toString();
|
||||
if (productId.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString displayPrice = productInfo.value(QStringLiteral("displayPrice")).toString();
|
||||
if (displayPrice.isEmpty()) {
|
||||
const QString price = productInfo.value(QStringLiteral("price")).toString();
|
||||
const QString currencyCode = productInfo.value(QStringLiteral("currencyCode")).toString();
|
||||
displayPrice = currencyCode.isEmpty() ? price : (price + QLatin1Char(' ') + currencyCode);
|
||||
}
|
||||
|
||||
SubscriptionPlanQuote quote;
|
||||
quote.priceAmount = productInfo.value(QStringLiteral("priceAmount")).toDouble();
|
||||
quote.subscriptionBillingMonths = productInfo.value(QStringLiteral("subscriptionBillingMonths")).toDouble();
|
||||
quote.displayPricePerMonth = productInfo.value(QStringLiteral("displayPricePerMonth")).toString();
|
||||
quote.hasFreeTrial = productInfo.value(QStringLiteral("hasFreeTrial")).toBool();
|
||||
quote.trialDays = productInfo.value(QStringLiteral("trialDays")).toInt();
|
||||
|
||||
// If the account is eligible for a paid introductory discount (not a free trial, which
|
||||
// stays invisible here and just applies silently at purchase time), show that price instead.
|
||||
const QString introOfferDisplayPrice = productInfo.value(QStringLiteral("introOfferDisplayPrice")).toString();
|
||||
if (introOfferDisplayPrice.isEmpty()) {
|
||||
quote.displayPrice = displayPrice;
|
||||
} else {
|
||||
quote.displayPrice = introOfferDisplayPrice;
|
||||
qInfo().noquote() << "[IAP] Applying" << productInfo.value(QStringLiteral("introOfferPaymentMode")).toString()
|
||||
<< "intro offer price for" << productId << ":" << introOfferDisplayPrice;
|
||||
}
|
||||
quotesByProductId.insert(productId, quote);
|
||||
}
|
||||
|
||||
return quotesByProductId;
|
||||
}
|
||||
#elif defined(Q_OS_ANDROID)
|
||||
QHash<QString, SubscriptionPlanQuote> buildGooglePlayQuoteMap(const QStringList &productIds)
|
||||
{
|
||||
Q_UNUSED(productIds); // Google Play returns every offer for the app's single product in one call
|
||||
|
||||
auto androidController = AndroidController::instance();
|
||||
const QJsonObject plansResult = AndroidUtils::runOnWorkerThread([androidController]() {
|
||||
return androidController->getSubscriptionPlans();
|
||||
});
|
||||
|
||||
QHash<QString, SubscriptionPlanQuote> quotesByProductId;
|
||||
if (plansResult.value("responseCode").toInt(-1) != 0) {
|
||||
qWarning() << "[Billing] Failed to get subscription plans for price display, responseCode:"
|
||||
<< plansResult.value("responseCode").toInt(-1);
|
||||
return quotesByProductId;
|
||||
}
|
||||
|
||||
const QJsonArray products = plansResult.value("products").toArray();
|
||||
|
||||
QHash<QString, int> trialDaysByBasePlanId;
|
||||
for (const QJsonValue &productValue : products) {
|
||||
const QJsonArray offers = productValue.toObject().value("offers").toArray();
|
||||
for (const QJsonValue &offerValue : offers) {
|
||||
const QJsonObject offer = offerValue.toObject();
|
||||
const QString basePlanId = offer.value("basePlanId").toString();
|
||||
if (basePlanId.isEmpty() || trialDaysByBasePlanId.contains(basePlanId)) {
|
||||
continue;
|
||||
}
|
||||
if (offer.value("hasFreeTrial").toBool()) {
|
||||
trialDaysByBasePlanId.insert(basePlanId, offer.value("trialDays").toInt());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const QJsonValue &productValue : products) {
|
||||
const QJsonArray offers = productValue.toObject().value("offers").toArray();
|
||||
for (const QJsonValue &offerValue : offers) {
|
||||
const QJsonObject offer = offerValue.toObject();
|
||||
const QString basePlanId = offer.value("basePlanId").toString();
|
||||
if (basePlanId.isEmpty() || quotesByProductId.contains(basePlanId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const QJsonArray pricingPhases = offer.value("pricingPhases").toArray();
|
||||
if (pricingPhases.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
// Last phase is always the ongoing recurring price, after any trial/intro phases
|
||||
const QJsonObject regularPhase = pricingPhases.last().toObject();
|
||||
|
||||
SubscriptionPlanQuote quote;
|
||||
quote.displayPrice = regularPhase.value("formattedPrice").toString();
|
||||
if (quote.displayPrice.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
quote.priceAmount = regularPhase.value("priceAmountMicros").toDouble() / 1000000.0;
|
||||
quote.subscriptionBillingMonths = regularPhase.value("subscriptionBillingMonths").toDouble();
|
||||
quote.displayPricePerMonth = regularPhase.value("displayPricePerMonth").toString();
|
||||
quote.hasFreeTrial = trialDaysByBasePlanId.contains(basePlanId);
|
||||
quote.trialDays = trialDaysByBasePlanId.value(basePlanId, 0);
|
||||
quotesByProductId.insert(basePlanId, quote);
|
||||
}
|
||||
}
|
||||
return quotesByProductId;
|
||||
}
|
||||
#endif
|
||||
|
||||
void mergeQuotesIntoPremiumPlans(QJsonObject &data, const QHash<QString, SubscriptionPlanQuote> "esByProductId)
|
||||
{
|
||||
QJsonArray services = data.value(apiDefs::key::services).toArray();
|
||||
if (services.isEmpty() || quotesByProductId.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int serviceIndex = 0; serviceIndex < services.size(); ++serviceIndex) {
|
||||
QJsonObject serviceObject = services.at(serviceIndex).toObject();
|
||||
@@ -166,8 +245,11 @@ namespace
|
||||
}
|
||||
|
||||
const bool isTrialPlan = planObject.value(configKey::isTrial).toBool();
|
||||
const StoreKitPlanQuote "e = *quoteIterator;
|
||||
const SubscriptionPlanQuote "e = *quoteIterator;
|
||||
planObject.insert(configKey::priceLabel, quote.displayPrice);
|
||||
planObject.insert(configKey::priceAmount, quote.priceAmount);
|
||||
planObject.insert(configKey::hasFreeTrial, quote.hasFreeTrial);
|
||||
planObject.insert(configKey::trialDays, quote.trialDays);
|
||||
|
||||
const double months = quote.subscriptionBillingMonths;
|
||||
if (!isTrialPlan && months > oneMonthThreshold && !quote.displayPricePerMonth.isEmpty()) {
|
||||
@@ -202,7 +284,29 @@ namespace
|
||||
}
|
||||
data.insert(apiDefs::key::services, services);
|
||||
}
|
||||
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
void mergeStoreKitPricesIntoPremiumPlans(QJsonObject &data)
|
||||
{
|
||||
const QStringList productIds = collectPremiumStoreProductIds(data.value(apiDefs::key::services).toArray());
|
||||
if (productIds.isEmpty()) {
|
||||
qInfo().noquote() << "[IAP] No store_product_id in premium plans; skip StoreKit merge into services payload";
|
||||
return;
|
||||
}
|
||||
mergeQuotesIntoPremiumPlans(data, buildStoreKitQuoteMap(productIds));
|
||||
}
|
||||
#elif defined(Q_OS_ANDROID)
|
||||
void mergeGooglePlayPricesIntoPremiumPlans(QJsonObject &data)
|
||||
{
|
||||
const QStringList productIds = collectPremiumStoreProductIds(data.value(apiDefs::key::services).toArray());
|
||||
if (productIds.isEmpty()) {
|
||||
qInfo().noquote() << "[Billing] No store_product_id in premium plans; skip Google Play merge into services payload";
|
||||
return;
|
||||
}
|
||||
mergeQuotesIntoPremiumPlans(data, buildGooglePlayQuoteMap(productIds));
|
||||
}
|
||||
#endif
|
||||
#endif // Q_OS_IOS || MACOS_NE || Q_OS_ANDROID
|
||||
}
|
||||
|
||||
ServicesCatalogController::ServicesCatalogController(SecureAppSettingsRepository* appSettingsRepository)
|
||||
@@ -212,11 +316,7 @@ ServicesCatalogController::ServicesCatalogController(SecureAppSettingsRepository
|
||||
|
||||
ErrorCode ServicesCatalogController::fillAvailableServices(QJsonObject &servicesData)
|
||||
{
|
||||
QJsonObject apiPayload;
|
||||
apiPayload[apiDefs::key::osVersion] = QSysInfo::productType();
|
||||
apiPayload[apiDefs::key::appVersion] = QString(APP_VERSION);
|
||||
apiPayload[apiDefs::key::cliName] = QString(APPLICATION_NAME);
|
||||
apiPayload[apiDefs::key::appLanguage] = m_appSettingsRepository->getAppLanguage().name().split("_").first();
|
||||
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository).build();
|
||||
|
||||
QByteArray responseBody;
|
||||
ErrorCode errorCode = executeRequest(QString("%1v1/services"), apiPayload, responseBody);
|
||||
@@ -234,6 +334,8 @@ ErrorCode ServicesCatalogController::fillAvailableServices(QJsonObject &services
|
||||
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
mergeStoreKitPricesIntoPremiumPlans(servicesData);
|
||||
#elif defined(Q_OS_ANDROID)
|
||||
mergeGooglePlayPricesIntoPremiumPlans(servicesData);
|
||||
#endif
|
||||
|
||||
return ErrorCode::NoError;
|
||||
@@ -242,7 +344,7 @@ ErrorCode ServicesCatalogController::fillAvailableServices(QJsonObject &services
|
||||
ErrorCode ServicesCatalogController::executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody)
|
||||
{
|
||||
GatewayController gatewayController(m_appSettingsRepository->getGatewayEndpoint(), m_appSettingsRepository->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs,
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled());
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled(), m_appSettingsRepository);
|
||||
return gatewayController.post(endpoint, apiPayload, responseBody);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,781 @@
|
||||
#include "storePurchaseController.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QEventLoop>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QSet>
|
||||
#include <QVariantMap>
|
||||
|
||||
#include "core/controllers/gatewayController.h"
|
||||
#include "core/models/api/apiConfig.h"
|
||||
#include "core/utils/api/gatewayPayloadBuilder.h"
|
||||
#include "core/utils/constants/apiConstants.h"
|
||||
#include "core/utils/constants/apiKeys.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/serverConfigUtils.h"
|
||||
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
#include "platforms/ios/ios_controller.h"
|
||||
#elif defined(Q_OS_ANDROID)
|
||||
#include "platforms/android/android_controller.h"
|
||||
#include "platforms/android/android_utils.h"
|
||||
#endif
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
namespace
|
||||
{
|
||||
bool isTestPurchaseEnvironment(const QString &storeEnvironment)
|
||||
{
|
||||
if (storeEnvironment.isEmpty()) {
|
||||
return IosController::Instance()->isTestFlight();
|
||||
}
|
||||
return storeEnvironment.compare(QStringLiteral("production"), Qt::CaseInsensitive) != 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_ANDROID)
|
||||
namespace
|
||||
{
|
||||
constexpr int purchaseStatePurchased = 1;
|
||||
constexpr int purchaseStatePending = 2;
|
||||
|
||||
ErrorCode billingErrorFromResponse(int responseCode)
|
||||
{
|
||||
if (responseCode >= ErrorCode::BillingCanceled && responseCode <= ErrorCode::BillingNetworkError) {
|
||||
return static_cast<ErrorCode>(responseCode);
|
||||
}
|
||||
return ErrorCode::ApiPurchaseError;
|
||||
}
|
||||
|
||||
struct PlayPurchaseOutcome
|
||||
{
|
||||
ErrorCode errorCode = ErrorCode::NoError;
|
||||
QString purchaseToken;
|
||||
bool isUpgrade = false;
|
||||
bool isAcknowledged = false;
|
||||
};
|
||||
|
||||
void acknowledgePlayPurchase(const QString &purchaseToken, bool isAcknowledged)
|
||||
{
|
||||
if (isAcknowledged) {
|
||||
return;
|
||||
}
|
||||
auto androidController = AndroidController::instance();
|
||||
const QJsonObject ackResult = AndroidUtils::runOnWorkerThread([androidController, purchaseToken]() {
|
||||
return androidController->acknowledgePurchase(purchaseToken);
|
||||
});
|
||||
if (ackResult.value("responseCode").toInt(-1) != 0) {
|
||||
qWarning() << "[Billing] Acknowledge failed, responseCode:" << ackResult.value("responseCode").toInt(-1);
|
||||
} else {
|
||||
qInfo() << "[Billing] Purchase acknowledged successfully";
|
||||
}
|
||||
}
|
||||
|
||||
QJsonArray queryPlayPurchases()
|
||||
{
|
||||
auto androidController = AndroidController::instance();
|
||||
const QJsonObject purchasesResult = AndroidUtils::runOnWorkerThread([androidController]() {
|
||||
return androidController->queryPurchases();
|
||||
});
|
||||
if (purchasesResult.value("responseCode").toInt(-1) != 0) {
|
||||
qWarning().noquote() << "[Billing] queryPurchases failed, responseCode ="
|
||||
<< purchasesResult.value("responseCode").toInt(-1);
|
||||
return {};
|
||||
}
|
||||
return purchasesResult.value("purchases").toArray();
|
||||
}
|
||||
|
||||
bool isPaidButUnacknowledged(const QJsonObject &purchaseObj)
|
||||
{
|
||||
return purchaseObj.value("purchaseState").toInt(-1) == purchaseStatePurchased
|
||||
&& !purchaseObj.value("isAcknowledged").toBool(true)
|
||||
&& !purchaseObj.value("purchaseToken").toString().isEmpty();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
StorePurchaseController::StorePurchaseController(SecureServersRepository *serversRepository,
|
||||
SecureAppSettingsRepository *appSettingsRepository)
|
||||
: m_serversRepository(serversRepository), m_appSettingsRepository(appSettingsRepository)
|
||||
{
|
||||
}
|
||||
|
||||
ErrorCode StorePurchaseController::executeRequest(const QString &endpoint, const QJsonObject &apiPayload,
|
||||
QByteArray &responseBody, bool isTestPurchase)
|
||||
{
|
||||
GatewayController gatewayController(m_appSettingsRepository->getGatewayEndpoint(isTestPurchase),
|
||||
m_appSettingsRepository->isDevGatewayEnv(isTestPurchase), apiDefs::requestTimeoutMsecs,
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled(), m_appSettingsRepository);
|
||||
return gatewayController.post(endpoint, apiPayload, responseBody);
|
||||
}
|
||||
|
||||
ErrorCode StorePurchaseController::getSubscriptionInfo(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const QString &purchaseToken,
|
||||
QByteArray &responseBody)
|
||||
{
|
||||
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
|
||||
.addField(apiDefs::key::userCountryCode, userCountryCode)
|
||||
.addField(apiDefs::key::serviceType, serviceType)
|
||||
.addField(apiDefs::key::serviceProtocol, serviceProtocol)
|
||||
.addField(apiDefs::key::transactionId, purchaseToken)
|
||||
.build();
|
||||
return executeRequest(QString("%1v1/get_subscription_info"), apiPayload, responseBody, false);
|
||||
}
|
||||
|
||||
ErrorCode StorePurchaseController::importServiceFromMarket(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol,
|
||||
const SubscriptionController::ProtocolData &protocolData,
|
||||
const QString &transactionId, bool isTestPurchase,
|
||||
int *duplicateServerIndex, const QString &endpoint)
|
||||
{
|
||||
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
|
||||
.addField(apiDefs::key::userCountryCode, userCountryCode)
|
||||
.addField(apiDefs::key::serviceType, serviceType)
|
||||
.addField(apiDefs::key::serviceProtocol, serviceProtocol)
|
||||
.addField(apiDefs::key::publicKey, SubscriptionController::publicKeyForProtocol(serviceProtocol, protocolData))
|
||||
.addField(apiDefs::key::transactionId, transactionId)
|
||||
.build();
|
||||
|
||||
QByteArray responseBody;
|
||||
qInfo() << "[Billing][importServiceFromMarket] endpoint:" << endpoint << "isTestPurchase:" << isTestPurchase;
|
||||
ErrorCode errorCode = executeRequest(QString("%1") + endpoint, apiPayload, responseBody, isTestPurchase);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
// Parse the subscription response
|
||||
QJsonObject responseObject = QJsonDocument::fromJson(responseBody).object();
|
||||
QString key = responseObject.value(QStringLiteral("key")).toString();
|
||||
if (key.isEmpty()) {
|
||||
qWarning().noquote() << "[IAP] Subscription response does not contain a key field";
|
||||
return ErrorCode::ApiPurchaseError;
|
||||
}
|
||||
|
||||
QString normalizedKey = key;
|
||||
normalizedKey.replace(QStringLiteral("vpn://"), QString());
|
||||
|
||||
// Check if server with this VPN key already exists
|
||||
for (int i = 0; i < m_serversRepository->serversCount(); ++i) {
|
||||
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) {
|
||||
*duplicateServerIndex = i;
|
||||
}
|
||||
qInfo().noquote() << "[IAP] Subscription config with the same vpn_key already exists";
|
||||
return ErrorCode::ApiConfigAlreadyAdded;
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray configString = QByteArray::fromBase64(normalizedKey.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
|
||||
QByteArray configUncompressed = qUncompress(configString);
|
||||
if (!configUncompressed.isEmpty()) {
|
||||
configString = configUncompressed;
|
||||
}
|
||||
|
||||
if (configString.isEmpty()) {
|
||||
qWarning().noquote() << "[IAP] Subscription response config payload is empty";
|
||||
return ErrorCode::ApiPurchaseError;
|
||||
}
|
||||
|
||||
QJsonObject configObject = QJsonDocument::fromJson(configString).object();
|
||||
|
||||
quint16 crc = qChecksum(QJsonDocument(configObject).toJson());
|
||||
|
||||
if (!serverConfigUtils::isConfigFormatVersionSupported(configObject)) {
|
||||
return ErrorCode::ConfigFormatVersionNotSupportedError;
|
||||
}
|
||||
if (configObject.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
ApiV2ServerConfig apiV2ServerConfig = ApiV2ServerConfig::fromJson(configObject);
|
||||
apiV2ServerConfig.apiConfig.vpnKey = normalizedKey;
|
||||
apiV2ServerConfig.apiConfig.subscriptionExpiredByServer = false;
|
||||
apiV2ServerConfig.crc = crc;
|
||||
|
||||
m_serversRepository->addServer(QString(), apiV2ServerConfig.toJson(),
|
||||
serverConfigUtils::configTypeFromJson(apiV2ServerConfig.toJson()));
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode StorePurchaseController::processAppStorePurchase(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const QString &productId,
|
||||
int *duplicateServerIndex, bool *wasUpgrade)
|
||||
{
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
const bool hadActivePlanBeforePurchase = !resolveActiveStoreProductIds().isEmpty();
|
||||
if (wasUpgrade) {
|
||||
*wasUpgrade = hadActivePlanBeforePurchase;
|
||||
}
|
||||
|
||||
bool purchaseOk = false;
|
||||
QString originalTransactionId;
|
||||
QString storeTransactionId;
|
||||
QString storeProductId;
|
||||
QString storeEnvironment;
|
||||
QString purchaseError;
|
||||
IosController::StorePurchaseFailure failureReason = IosController::StorePurchaseFailure::Other;
|
||||
QEventLoop waitPurchase;
|
||||
|
||||
IosController::Instance()->purchaseProduct(productId,
|
||||
[&](bool success, const QString &txId, const QString &purchasedProductId,
|
||||
const QString &originalTxId, const QString &environment,
|
||||
const QString &errorString,
|
||||
IosController::StorePurchaseFailure reason) {
|
||||
purchaseOk = success;
|
||||
originalTransactionId = originalTxId;
|
||||
storeTransactionId = txId;
|
||||
storeProductId = purchasedProductId;
|
||||
storeEnvironment = environment;
|
||||
purchaseError = errorString;
|
||||
failureReason = reason;
|
||||
waitPurchase.quit();
|
||||
});
|
||||
waitPurchase.exec();
|
||||
|
||||
if (!purchaseOk || originalTransactionId.isEmpty()) {
|
||||
qDebug() << "IAP purchase failed:" << purchaseError;
|
||||
switch (failureReason) {
|
||||
case IosController::StorePurchaseFailure::Cancelled: return ErrorCode::BillingCanceled;
|
||||
case IosController::StorePurchaseFailure::Pending: return ErrorCode::ApiPurchasePendingError;
|
||||
default: return ErrorCode::ApiPurchaseError;
|
||||
}
|
||||
}
|
||||
qInfo().noquote() << "[IAP] Purchase success. transactionId =" << storeTransactionId
|
||||
<< "originalTransactionId =" << originalTransactionId << "productId =" << storeProductId;
|
||||
|
||||
bool isTestPurchase = isTestPurchaseEnvironment(storeEnvironment);
|
||||
|
||||
SubscriptionController::ProtocolData protocolData = SubscriptionController::generateProtocolData(serviceProtocol);
|
||||
ErrorCode importError = importServiceFromMarket(userCountryCode, serviceType, serviceProtocol, protocolData,
|
||||
originalTransactionId, isTestPurchase, duplicateServerIndex,
|
||||
QStringLiteral("v1/subscriptions"));
|
||||
|
||||
// Finish the StoreKit transaction only after the gateway has validated the purchase;
|
||||
// otherwise it stays in the unfinished queue and is retried via Transaction.updates
|
||||
if (importError == ErrorCode::NoError || importError == ErrorCode::ApiConfigAlreadyAdded) {
|
||||
IosController::Instance()->finishStoreTransaction(storeTransactionId);
|
||||
}
|
||||
|
||||
return importError;
|
||||
#else
|
||||
Q_UNUSED(userCountryCode);
|
||||
Q_UNUSED(serviceType);
|
||||
Q_UNUSED(serviceProtocol);
|
||||
Q_UNUSED(productId);
|
||||
Q_UNUSED(duplicateServerIndex);
|
||||
Q_UNUSED(wasUpgrade);
|
||||
return ErrorCode::ApiPurchaseError;
|
||||
#endif
|
||||
}
|
||||
|
||||
ErrorCode StorePurchaseController::processPlayMarketPurchase(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const QString &productId,
|
||||
int *duplicateServerIndex, bool *wasUpgrade)
|
||||
{
|
||||
#if defined(Q_OS_ANDROID)
|
||||
auto androidController = AndroidController::instance();
|
||||
|
||||
const PlayPurchaseOutcome outcome = AndroidUtils::runOnWorkerThread([androidController, productId]() {
|
||||
PlayPurchaseOutcome outcome;
|
||||
|
||||
QString oldPurchaseToken;
|
||||
QJsonObject existingPurchasesResult = androidController->queryPurchases();
|
||||
if (existingPurchasesResult.value("responseCode").toInt(-1) == 0) {
|
||||
const QJsonArray existingPurchases = existingPurchasesResult.value("purchases").toArray();
|
||||
for (const QJsonValue &purchaseValue : existingPurchases) {
|
||||
const QJsonObject existingPurchase = purchaseValue.toObject();
|
||||
if (existingPurchase.value("purchaseState").toInt(-1) == purchaseStatePurchased) {
|
||||
oldPurchaseToken = existingPurchase.value("purchaseToken").toString();
|
||||
qInfo().noquote() << "[Billing] Found existing active subscription, will upgrade instead of purchasing a new one";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
outcome.isUpgrade = !oldPurchaseToken.isEmpty();
|
||||
|
||||
QJsonObject plansResult = androidController->getSubscriptionPlans();
|
||||
int responseCode = plansResult.value("responseCode").toInt(-1);
|
||||
if (responseCode != 0) {
|
||||
qWarning() << "[Billing] Failed to get subscription plans, responseCode:" << responseCode;
|
||||
outcome.errorCode = billingErrorFromResponse(responseCode);
|
||||
return outcome;
|
||||
}
|
||||
QJsonArray products = plansResult.value("products").toArray();
|
||||
QString offerToken;
|
||||
QString fallbackOfferToken;
|
||||
for (const QJsonValue &productValue : products) {
|
||||
QJsonObject product = productValue.toObject();
|
||||
QJsonArray offers = product.value("offers").toArray();
|
||||
for (const QJsonValue &offerValue : offers) {
|
||||
QJsonObject offer = offerValue.toObject();
|
||||
if (offer.value("basePlanId").toString() != productId) continue;
|
||||
|
||||
const QString token = offer.value("offerToken").toString();
|
||||
QJsonArray pricingPhases = offer.value("pricingPhases").toArray();
|
||||
const bool hasFreeTrial = !pricingPhases.isEmpty()
|
||||
&& pricingPhases.first().toObject().value("priceAmountMicros").toDouble() == 0;
|
||||
|
||||
if (hasFreeTrial && !oldPurchaseToken.isEmpty()) continue;
|
||||
|
||||
if (fallbackOfferToken.isEmpty()) fallbackOfferToken = token;
|
||||
|
||||
if (hasFreeTrial) {
|
||||
offerToken = token;
|
||||
qInfo() << "[Billing] Found free trial offer for basePlanId:" << productId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!offerToken.isEmpty()) break;
|
||||
}
|
||||
if (offerToken.isEmpty()) offerToken = fallbackOfferToken;
|
||||
if (offerToken.isEmpty()) {
|
||||
qWarning() << "[Billing] No offer token found for basePlanId:" << productId;
|
||||
outcome.errorCode = ErrorCode::SubscriptionUnavailable;
|
||||
return outcome;
|
||||
}
|
||||
QJsonObject purchaseResult = oldPurchaseToken.isEmpty()
|
||||
? androidController->purchaseSubscription(offerToken)
|
||||
: androidController->upgradeSubscription(offerToken, oldPurchaseToken);
|
||||
responseCode = purchaseResult.value("responseCode").toInt(-1);
|
||||
if (responseCode != 0) {
|
||||
qWarning() << "[Billing] Purchase failed, responseCode:" << responseCode;
|
||||
outcome.errorCode = billingErrorFromResponse(responseCode);
|
||||
return outcome;
|
||||
}
|
||||
QJsonArray purchases = purchaseResult.value("purchases").toArray();
|
||||
if (purchases.isEmpty()) {
|
||||
qWarning() << "[Billing] Purchase succeeded but no purchases returned";
|
||||
outcome.errorCode = ErrorCode::ApiPurchaseError;
|
||||
return outcome;
|
||||
}
|
||||
QJsonObject purchase = purchases.at(0).toObject();
|
||||
outcome.purchaseToken = purchase.value("purchaseToken").toString();
|
||||
outcome.isAcknowledged = purchase.value("isAcknowledged").toBool();
|
||||
int purchaseState = purchase.value("purchaseState").toInt(-1);
|
||||
qInfo().noquote() << "[Billing] Purchase success. purchaseToken:" << outcome.purchaseToken
|
||||
<< "isAcknowledged:" << outcome.isAcknowledged << "purchaseState:" << purchaseState;
|
||||
// purchaseState: 1 = PURCHASED, 2 = PENDING (user must confirm payment in Google Play), 0 = UNSPECIFIED
|
||||
if (purchaseState == purchaseStatePending) {
|
||||
qWarning() << "[Billing] Purchase is in PENDING state, waiting for user to confirm payment";
|
||||
outcome.errorCode = ErrorCode::ApiPurchasePendingError;
|
||||
return outcome;
|
||||
}
|
||||
if (purchaseState != purchaseStatePurchased || outcome.purchaseToken.isEmpty()) {
|
||||
outcome.errorCode = ErrorCode::ApiPurchaseError;
|
||||
return outcome;
|
||||
}
|
||||
return outcome;
|
||||
});
|
||||
|
||||
if (outcome.errorCode != ErrorCode::NoError) {
|
||||
return outcome.errorCode;
|
||||
}
|
||||
|
||||
if (wasUpgrade) {
|
||||
*wasUpgrade = outcome.isUpgrade;
|
||||
}
|
||||
|
||||
if (outcome.purchaseToken != m_lastPlayPurchaseToken) {
|
||||
m_lastPlayPurchaseToken = outcome.purchaseToken;
|
||||
m_lastPlayBasePlanId = productId;
|
||||
}
|
||||
|
||||
return finalizePlayPurchase(userCountryCode, serviceType, serviceProtocol, outcome.purchaseToken,
|
||||
outcome.isAcknowledged, duplicateServerIndex, QStringLiteral("v1/subscriptions"));
|
||||
#else
|
||||
Q_UNUSED(userCountryCode);
|
||||
Q_UNUSED(serviceType);
|
||||
Q_UNUSED(serviceProtocol);
|
||||
Q_UNUSED(productId);
|
||||
Q_UNUSED(duplicateServerIndex);
|
||||
Q_UNUSED(wasUpgrade);
|
||||
return ErrorCode::ApiPurchaseError;
|
||||
#endif
|
||||
}
|
||||
|
||||
StorePurchaseController::StoreRestoreResult StorePurchaseController::processAppStoreRestore(const QString &userCountryCode,
|
||||
const QString &serviceType,
|
||||
const QString &serviceProtocol)
|
||||
{
|
||||
StoreRestoreResult result;
|
||||
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
bool restoreSuccess = false;
|
||||
QList<QVariantMap> restoredTransactions;
|
||||
QString restoreError;
|
||||
QEventLoop waitRestore;
|
||||
|
||||
IosController::Instance()->restorePurchases([&](bool success, const QList<QVariantMap> &transactions, const QString &errorString) {
|
||||
restoreSuccess = success;
|
||||
restoredTransactions = transactions;
|
||||
restoreError = errorString;
|
||||
waitRestore.quit();
|
||||
});
|
||||
waitRestore.exec();
|
||||
|
||||
qInfo().noquote() << "[IAP][processAppStoreRestore] restorePurchases result: success=" << restoreSuccess
|
||||
<< "transactions count=" << restoredTransactions.size()
|
||||
<< "error=" << restoreError;
|
||||
|
||||
if (!restoreSuccess) {
|
||||
qWarning().noquote() << "[IAP] Restore failed:" << restoreError;
|
||||
result.errorCode = ErrorCode::ApiPurchaseError;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (restoredTransactions.isEmpty()) {
|
||||
qInfo().noquote() << "[IAP] Restore completed, but no transactions were returned";
|
||||
result.errorCode = ErrorCode::ApiNoPurchasesToRestore;
|
||||
return result;
|
||||
}
|
||||
|
||||
QSet<QString> processedTransactions;
|
||||
|
||||
for (const QVariantMap &transaction : restoredTransactions) {
|
||||
const QString originalTransactionId = transaction.value(QStringLiteral("originalTransactionId")).toString();
|
||||
const QString transactionId = transaction.value(QStringLiteral("transactionId")).toString();
|
||||
const QString transactionProductId = transaction.value(QStringLiteral("productId")).toString();
|
||||
const bool isTestPurchase = isTestPurchaseEnvironment(transaction.value(QStringLiteral("environment")).toString());
|
||||
|
||||
qInfo().noquote() << "[IAP][processAppStoreRestore] Processing transaction: transactionId=" << transactionId
|
||||
<< "originalTransactionId=" << originalTransactionId << "productId=" << transactionProductId;
|
||||
|
||||
if (originalTransactionId.isEmpty()) {
|
||||
qWarning().noquote() << "[IAP] Skipping restored transaction without originalTransactionId" << transactionId;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (processedTransactions.contains(originalTransactionId)) {
|
||||
result.duplicateCount++;
|
||||
continue;
|
||||
}
|
||||
processedTransactions.insert(originalTransactionId);
|
||||
|
||||
qInfo().noquote() << "[IAP] Restoring subscription. transactionId =" << transactionId
|
||||
<< "originalTransactionId =" << originalTransactionId << "productId =" << transactionProductId;
|
||||
|
||||
SubscriptionController::ProtocolData protocolData = SubscriptionController::generateProtocolData(serviceProtocol);
|
||||
int currentDuplicateServerIndex = -1;
|
||||
ErrorCode errorCode = importServiceFromMarket(userCountryCode, serviceType, serviceProtocol, protocolData,
|
||||
originalTransactionId, isTestPurchase,
|
||||
¤tDuplicateServerIndex,
|
||||
QStringLiteral("v1/restore_subscription"));
|
||||
|
||||
qInfo().noquote() << "[IAP][processAppStoreRestore] importServiceFromMarket errorCode=" << static_cast<int>(errorCode)
|
||||
<< "for originalTransactionId=" << originalTransactionId;
|
||||
|
||||
if (errorCode == ErrorCode::ApiConfigAlreadyAdded) {
|
||||
result.duplicateConfigAlreadyPresent = true;
|
||||
if (result.duplicateServerIndex < 0) {
|
||||
result.duplicateServerIndex = currentDuplicateServerIndex;
|
||||
}
|
||||
qInfo().noquote() << "[IAP] Skipping restored transaction" << originalTransactionId
|
||||
<< "because subscription config with the same vpn_key already exists";
|
||||
} else if (errorCode != ErrorCode::NoError) {
|
||||
qWarning().noquote() << "[IAP] Failed to process restored subscription response for transaction" << originalTransactionId
|
||||
<< "errorCode=" << static_cast<int>(errorCode);
|
||||
result.errorCode = errorCode;
|
||||
} else {
|
||||
result.hasInstalledConfig = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.hasInstalledConfig && result.errorCode == ErrorCode::NoError) {
|
||||
result.errorCode = result.duplicateConfigAlreadyPresent ? ErrorCode::ApiConfigAlreadyAdded : ErrorCode::ApiPurchaseError;
|
||||
}
|
||||
|
||||
qInfo().noquote() << "[IAP][processAppStoreRestore] Done. hasInstalledConfig=" << result.hasInstalledConfig
|
||||
<< "duplicateConfigAlreadyPresent=" << result.duplicateConfigAlreadyPresent
|
||||
<< "duplicateCount=" << result.duplicateCount
|
||||
<< "errorCode=" << static_cast<int>(result.errorCode);
|
||||
|
||||
return result;
|
||||
#else
|
||||
Q_UNUSED(userCountryCode);
|
||||
Q_UNUSED(serviceType);
|
||||
Q_UNUSED(serviceProtocol);
|
||||
result.errorCode = ErrorCode::ApiPurchaseError;
|
||||
return result;
|
||||
#endif
|
||||
}
|
||||
|
||||
ErrorCode StorePurchaseController::processAppStoreTransactionUpdate(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const QString &originalTransactionId,
|
||||
const QString &transactionId, const QString &storeEnvironment,
|
||||
int *duplicateServerIndex)
|
||||
{
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
if (originalTransactionId.isEmpty()) {
|
||||
return ErrorCode::ApiPurchaseError;
|
||||
}
|
||||
|
||||
qInfo().noquote() << "[IAP] Processing transaction update. transactionId =" << transactionId
|
||||
<< "originalTransactionId =" << originalTransactionId << "environment =" << storeEnvironment;
|
||||
|
||||
bool isTestPurchase = isTestPurchaseEnvironment(storeEnvironment);
|
||||
SubscriptionController::ProtocolData protocolData = SubscriptionController::generateProtocolData(serviceProtocol);
|
||||
ErrorCode errorCode = importServiceFromMarket(userCountryCode, serviceType, serviceProtocol, protocolData,
|
||||
originalTransactionId, isTestPurchase, duplicateServerIndex,
|
||||
QStringLiteral("v1/restore_subscription"));
|
||||
|
||||
// On validation failure the transaction is left unfinished so it is redelivered
|
||||
// by the Transaction.updates listener on the next app launch
|
||||
if (errorCode == ErrorCode::NoError || errorCode == ErrorCode::ApiConfigAlreadyAdded) {
|
||||
IosController::Instance()->finishStoreTransaction(transactionId);
|
||||
}
|
||||
|
||||
return errorCode;
|
||||
#else
|
||||
Q_UNUSED(userCountryCode);
|
||||
Q_UNUSED(serviceType);
|
||||
Q_UNUSED(serviceProtocol);
|
||||
Q_UNUSED(originalTransactionId);
|
||||
Q_UNUSED(transactionId);
|
||||
Q_UNUSED(storeEnvironment);
|
||||
Q_UNUSED(duplicateServerIndex);
|
||||
return ErrorCode::ApiPurchaseError;
|
||||
#endif
|
||||
}
|
||||
|
||||
StorePurchaseController::StoreRestoreResult StorePurchaseController::processPlayMarketRestore(const QString &userCountryCode,
|
||||
const QString &serviceType,
|
||||
const QString &serviceProtocol)
|
||||
{
|
||||
StoreRestoreResult result;
|
||||
|
||||
#if defined(Q_OS_ANDROID)
|
||||
auto androidController = AndroidController::instance();
|
||||
|
||||
const QJsonObject purchasesResult = AndroidUtils::runOnWorkerThread([androidController]() {
|
||||
return androidController->queryPurchases();
|
||||
});
|
||||
|
||||
int responseCode = purchasesResult.value("responseCode").toInt(-1);
|
||||
if (responseCode != 0) {
|
||||
qWarning().noquote() << "[Billing] queryPurchases failed, responseCode =" << responseCode;
|
||||
result.errorCode = billingErrorFromResponse(responseCode);
|
||||
return result;
|
||||
}
|
||||
|
||||
QJsonArray purchases = purchasesResult.value("purchases").toArray();
|
||||
if (purchases.isEmpty()) {
|
||||
qInfo().noquote() << "[Billing] Restore completed, but no purchases were found";
|
||||
result.errorCode = ErrorCode::ApiNoPurchasesToRestore;
|
||||
return result;
|
||||
}
|
||||
|
||||
QSet<QString> processedTokens;
|
||||
for (const QJsonValue &purchaseValue : std::as_const(purchases)) {
|
||||
const QJsonObject purchaseObj = purchaseValue.toObject();
|
||||
const QString purchaseToken = purchaseObj.value("purchaseToken").toString();
|
||||
|
||||
if (purchaseToken.isEmpty()) {
|
||||
qWarning().noquote() << "[Billing] Skipping purchase without purchaseToken";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (purchaseObj.value("purchaseState").toInt(-1) != purchaseStatePurchased) {
|
||||
qInfo().noquote() << "[Billing] Skipping purchase in state" << purchaseObj.value("purchaseState").toInt(-1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (processedTokens.contains(purchaseToken)) {
|
||||
result.duplicateCount++;
|
||||
continue;
|
||||
}
|
||||
processedTokens.insert(purchaseToken);
|
||||
|
||||
qInfo().noquote() << "[Billing] Restoring subscription";
|
||||
|
||||
int currentDuplicateServerIndex = -1;
|
||||
ErrorCode errorCode = finalizePlayPurchase(userCountryCode, serviceType, serviceProtocol,
|
||||
purchaseToken, purchaseObj.value("isAcknowledged").toBool(),
|
||||
¤tDuplicateServerIndex, QStringLiteral("v1/restore_subscription"));
|
||||
|
||||
if (errorCode == ErrorCode::ApiConfigAlreadyAdded) {
|
||||
result.duplicateConfigAlreadyPresent = true;
|
||||
if (result.duplicateServerIndex < 0) {
|
||||
result.duplicateServerIndex = currentDuplicateServerIndex;
|
||||
}
|
||||
qInfo().noquote() << "[Billing] Skipping purchase because subscription config with the same vpn_key already exists";
|
||||
} else if (errorCode != ErrorCode::NoError) {
|
||||
qWarning().noquote() << "[Billing] Failed to process restored subscription, errorCode =" << static_cast<int>(errorCode);
|
||||
result.errorCode = errorCode;
|
||||
} else {
|
||||
result.hasInstalledConfig = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.hasInstalledConfig && !result.duplicateConfigAlreadyPresent && result.errorCode == ErrorCode::NoError) {
|
||||
result.errorCode = ErrorCode::ApiNoPurchasesToRestore;
|
||||
} else if (!result.hasInstalledConfig && result.duplicateConfigAlreadyPresent && result.errorCode == ErrorCode::NoError) {
|
||||
result.errorCode = ErrorCode::ApiConfigAlreadyAdded;
|
||||
}
|
||||
|
||||
return result;
|
||||
#else
|
||||
Q_UNUSED(userCountryCode);
|
||||
Q_UNUSED(serviceType);
|
||||
Q_UNUSED(serviceProtocol);
|
||||
result.errorCode = ErrorCode::ApiPurchaseError;
|
||||
return result;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(Q_OS_ANDROID)
|
||||
ErrorCode StorePurchaseController::finalizePlayPurchase(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const QString &purchaseToken,
|
||||
bool isAcknowledged, int *duplicateServerIndex, const QString &endpoint)
|
||||
{
|
||||
QByteArray checkResponse;
|
||||
ErrorCode checkError = getSubscriptionInfo(userCountryCode, serviceType, serviceProtocol, purchaseToken, checkResponse);
|
||||
if (checkError != ErrorCode::NoError) {
|
||||
qWarning().noquote() << "[Billing] Initial subscriptions check failed:" << static_cast<int>(checkError);
|
||||
return checkError;
|
||||
}
|
||||
|
||||
QJsonObject checkObject = QJsonDocument::fromJson(checkResponse).object();
|
||||
bool isTestPurchase = checkObject.value(apiDefs::key::isTestPurchase).toBool(false);
|
||||
qInfo().noquote() << "[Billing] Purchase isTestPurchase =" << isTestPurchase;
|
||||
|
||||
SubscriptionController::ProtocolData protocolData = SubscriptionController::generateProtocolData(serviceProtocol);
|
||||
ErrorCode errorCode = importServiceFromMarket(userCountryCode, serviceType, serviceProtocol, protocolData,
|
||||
purchaseToken, isTestPurchase, duplicateServerIndex, endpoint);
|
||||
|
||||
if (errorCode == ErrorCode::NoError || errorCode == ErrorCode::ApiConfigAlreadyAdded) {
|
||||
acknowledgePlayPurchase(purchaseToken, isAcknowledged);
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
#endif
|
||||
|
||||
QJsonArray StorePurchaseController::findUnacknowledgedPlayPurchases()
|
||||
{
|
||||
#if defined(Q_OS_ANDROID)
|
||||
QJsonArray unacknowledged;
|
||||
for (const QJsonValue &purchaseValue : queryPlayPurchases()) {
|
||||
if (isPaidButUnacknowledged(purchaseValue.toObject())) {
|
||||
unacknowledged.append(purchaseValue);
|
||||
}
|
||||
}
|
||||
return unacknowledged;
|
||||
#else
|
||||
return {};
|
||||
#endif
|
||||
}
|
||||
|
||||
bool StorePurchaseController::processUnacknowledgedPlayPurchases(const QJsonArray &purchases, const QString &userCountryCode,
|
||||
const QString &serviceType, const QString &serviceProtocol)
|
||||
{
|
||||
#if defined(Q_OS_ANDROID)
|
||||
bool installedNewConfig = false;
|
||||
QSet<QString> processedTokens;
|
||||
|
||||
for (const QJsonValue &purchaseValue : purchases) {
|
||||
const QJsonObject purchaseObj = purchaseValue.toObject();
|
||||
const QString purchaseToken = purchaseObj.value("purchaseToken").toString();
|
||||
if (processedTokens.contains(purchaseToken)) {
|
||||
continue;
|
||||
}
|
||||
processedTokens.insert(purchaseToken);
|
||||
|
||||
qInfo().noquote() << "[Billing] Found paid but unacknowledged purchase, validating";
|
||||
|
||||
ErrorCode errorCode = finalizePlayPurchase(userCountryCode, serviceType, serviceProtocol,
|
||||
purchaseToken, false, nullptr, QStringLiteral("v1/restore_subscription"));
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
installedNewConfig = true;
|
||||
} else if (errorCode != ErrorCode::ApiConfigAlreadyAdded) {
|
||||
qWarning().noquote() << "[Billing] Purchase validation failed, errorCode =" << static_cast<int>(errorCode)
|
||||
<< "- will retry on next launch";
|
||||
}
|
||||
}
|
||||
return installedNewConfig;
|
||||
#else
|
||||
Q_UNUSED(userCountryCode);
|
||||
Q_UNUSED(serviceType);
|
||||
Q_UNUSED(serviceProtocol);
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
QStringList StorePurchaseController::resolveActiveStoreProductIds()
|
||||
{
|
||||
QStringList activeProductIds;
|
||||
|
||||
#if defined(Q_OS_ANDROID)
|
||||
auto androidController = AndroidController::instance();
|
||||
|
||||
const QJsonObject purchasesResult = AndroidUtils::runOnWorkerThread([androidController]() {
|
||||
return androidController->queryPurchases();
|
||||
});
|
||||
|
||||
if (purchasesResult.value("responseCode").toInt(-1) != 0) {
|
||||
qWarning().noquote() << "[Billing][resolveActiveStoreProductIds] queryPurchases failed";
|
||||
return activeProductIds;
|
||||
}
|
||||
|
||||
const QJsonArray purchases = purchasesResult.value("purchases").toArray();
|
||||
for (const QJsonValue &purchaseValue : purchases) {
|
||||
const QJsonObject purchaseObj = purchaseValue.toObject();
|
||||
const QString purchaseToken = purchaseObj.value("purchaseToken").toString();
|
||||
qInfo().noquote() << "[Billing][resolveActiveStoreProductIds] purchase found."
|
||||
<< "purchaseState:" << purchaseObj.value("purchaseState").toInt()
|
||||
<< "productIds:" << purchaseObj.value("productIds").toArray();
|
||||
if (purchaseObj.value("purchaseState").toInt() != purchaseStatePurchased) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!m_lastPlayBasePlanId.isEmpty() && !purchaseToken.isEmpty() && purchaseToken == m_lastPlayPurchaseToken) {
|
||||
if (!activeProductIds.contains(m_lastPlayBasePlanId)) {
|
||||
activeProductIds.append(m_lastPlayBasePlanId);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const QJsonArray productIds = purchaseObj.value("productIds").toArray();
|
||||
for (const QJsonValue &productIdValue : productIds) {
|
||||
const QString productId = productIdValue.toString();
|
||||
if (!productId.isEmpty() && !activeProductIds.contains(productId)) {
|
||||
activeProductIds.append(productId);
|
||||
}
|
||||
}
|
||||
}
|
||||
#elif defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
bool restoreSuccess = false;
|
||||
QList<QVariantMap> transactions;
|
||||
QString restoreError;
|
||||
QEventLoop waitRestore;
|
||||
|
||||
IosController::Instance()->fetchLocalEntitlements([&](bool success, const QList<QVariantMap> &localTransactions, const QString &errorString) {
|
||||
restoreSuccess = success;
|
||||
transactions = localTransactions;
|
||||
restoreError = errorString;
|
||||
waitRestore.quit();
|
||||
});
|
||||
waitRestore.exec();
|
||||
|
||||
if (!restoreSuccess) {
|
||||
qWarning().noquote() << "[Billing][resolveActiveStoreProductIds] fetchLocalEntitlements failed:" << restoreError;
|
||||
return activeProductIds;
|
||||
}
|
||||
|
||||
for (const QVariantMap &transaction : std::as_const(transactions)) {
|
||||
const QString productId = transaction.value("productId").toString();
|
||||
if (!productId.isEmpty() && !activeProductIds.contains(productId)) {
|
||||
activeProductIds.append(productId);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return activeProductIds;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
#ifndef STOREPURCHASECONTROLLER_H
|
||||
#define STOREPURCHASECONTROLLER_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QJsonArray>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
#include "core/controllers/api/subscriptionController.h"
|
||||
#include "core/repositories/secureAppSettingsRepository.h"
|
||||
#include "core/repositories/secureServersRepository.h"
|
||||
#include "core/utils/errorCodes.h"
|
||||
|
||||
// In-app purchase flows for App Store and Play Market: purchase, restore,
|
||||
// gateway validation and store transaction lifecycle (finish / acknowledge)
|
||||
class StorePurchaseController
|
||||
{
|
||||
public:
|
||||
struct StoreRestoreResult
|
||||
{
|
||||
bool hasInstalledConfig = false;
|
||||
bool duplicateConfigAlreadyPresent = false;
|
||||
int duplicateCount = 0;
|
||||
int duplicateServerIndex = -1;
|
||||
ErrorCode errorCode = ErrorCode::NoError;
|
||||
};
|
||||
|
||||
explicit StorePurchaseController(SecureServersRepository *serversRepository,
|
||||
SecureAppSettingsRepository *appSettingsRepository);
|
||||
|
||||
ErrorCode processAppStorePurchase(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const QString &productId,
|
||||
int *duplicateServerIndex = nullptr, bool *wasUpgrade = nullptr);
|
||||
|
||||
ErrorCode processPlayMarketPurchase(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const QString &productId,
|
||||
int *duplicateServerIndex = nullptr, bool *wasUpgrade = nullptr);
|
||||
|
||||
StoreRestoreResult processAppStoreRestore(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol);
|
||||
|
||||
StoreRestoreResult processPlayMarketRestore(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol);
|
||||
|
||||
// Validates a transaction delivered by the StoreKit Transaction.updates listener
|
||||
// (Ask to Buy approval, interrupted purchase, renewal) and finishes it on success
|
||||
ErrorCode processAppStoreTransactionUpdate(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const QString &originalTransactionId,
|
||||
const QString &transactionId, const QString &storeEnvironment,
|
||||
int *duplicateServerIndex = nullptr);
|
||||
|
||||
QStringList resolveActiveStoreProductIds();
|
||||
|
||||
// Android counterpart of the iOS Transaction.updates listener: purchases that were
|
||||
// paid but never acknowledged (validation failed earlier, or a PENDING purchase was
|
||||
// completed outside the app). Query once with findUnacknowledgedPlayPurchases(), then
|
||||
// pass the result to processUnacknowledgedPlayPurchases() to validate each purchase on
|
||||
// the gateway and acknowledge it on success.
|
||||
QJsonArray findUnacknowledgedPlayPurchases();
|
||||
bool processUnacknowledgedPlayPurchases(const QJsonArray &purchases, const QString &userCountryCode,
|
||||
const QString &serviceType, const QString &serviceProtocol);
|
||||
|
||||
private:
|
||||
ErrorCode executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody,
|
||||
bool isTestPurchase = false);
|
||||
|
||||
ErrorCode getSubscriptionInfo(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const QString &purchaseToken,
|
||||
QByteArray &responseBody);
|
||||
|
||||
ErrorCode importServiceFromMarket(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol,
|
||||
const SubscriptionController::ProtocolData &protocolData,
|
||||
const QString &transactionId, bool isTestPurchase,
|
||||
int *duplicateServerIndex, const QString &endpoint);
|
||||
|
||||
// Defined for Android builds only
|
||||
ErrorCode finalizePlayPurchase(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const QString &purchaseToken,
|
||||
bool isAcknowledged, int *duplicateServerIndex, const QString &endpoint);
|
||||
|
||||
SecureServersRepository *m_serversRepository;
|
||||
SecureAppSettingsRepository *m_appSettingsRepository;
|
||||
|
||||
QString m_lastPlayPurchaseToken;
|
||||
QString m_lastPlayBasePlanId;
|
||||
};
|
||||
|
||||
#endif // STOREPURCHASECONTROLLER_H
|
||||
File diff suppressed because it is too large.
Load diff
@@ -4,15 +4,12 @@
|
||||
#include <QJsonObject>
|
||||
#include <QByteArray>
|
||||
#include <QFuture>
|
||||
#include <QList>
|
||||
#include <QVariantMap>
|
||||
|
||||
#include "core/utils/errorCodes.h"
|
||||
#include "core/utils/routeModes.h"
|
||||
#include "core/utils/commonStructs.h"
|
||||
#include "core/repositories/secureServersRepository.h"
|
||||
#include "core/repositories/secureAppSettingsRepository.h"
|
||||
#include "core/models/serverConfig.h"
|
||||
|
||||
class ServersController;
|
||||
|
||||
@@ -28,89 +25,93 @@ public:
|
||||
QString xrayUuid;
|
||||
};
|
||||
|
||||
struct GatewayRequestData
|
||||
{
|
||||
QString osVersion;
|
||||
QString appVersion;
|
||||
QString appLanguage;
|
||||
QString installationUuid;
|
||||
QString userCountryCode;
|
||||
QString serverCountryCode;
|
||||
QString serviceType;
|
||||
QString serviceProtocol;
|
||||
QJsonObject authData;
|
||||
|
||||
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);
|
||||
static ProtocolData generateProtocolData(const QString &protocol);
|
||||
|
||||
static QString publicKeyForProtocol(const QString &protocol, const ProtocolData &protocolData);
|
||||
|
||||
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 fetchServiceConfig(const QString &serverId, const QString &serviceProtocol, QJsonObject &serverConfigJson);
|
||||
|
||||
ErrorCode updateServiceFromGateway(int serverIndex, const QString &newCountryCode, bool isConnectEvent);
|
||||
ErrorCode updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, bool isConnectEvent,
|
||||
CaptchaInfo *captchaInfoOut = nullptr, ProtocolData *usedProtocolDataOut = nullptr);
|
||||
|
||||
ErrorCode deactivateDevice(int serverIndex, bool isRemoveEvent);
|
||||
ErrorCode resolveUpdateServiceCaptcha(const QString &serverId, const QString &newCountryCode, bool isConnectEvent,
|
||||
const ProtocolData &protocolData, const QString &captchaId,
|
||||
const QString &captchaSolution, CaptchaInfo *retryCaptchaOut = nullptr);
|
||||
|
||||
ErrorCode deactivateExternalDevice(int serverIndex, const QString &uuid, const QString &serverCountryCode);
|
||||
ErrorCode deactivateDevice(const QString &serverId);
|
||||
|
||||
ErrorCode exportNativeConfig(int serverIndex, const QString &serverCountryCode, QString &nativeConfig);
|
||||
ErrorCode deactivateExternalDevice(const QString &serverId, const QString &uuid, const QString &serverCountryCode);
|
||||
|
||||
ErrorCode revokeNativeConfig(int serverIndex, const QString &serverCountryCode);
|
||||
ErrorCode exportNativeConfig(const QString &serverId, const QString &serverCountryCode, QString &nativeConfig);
|
||||
|
||||
ErrorCode updateServiceFromTelegram(int serverIndex);
|
||||
ErrorCode revokeNativeConfig(const QString &serverId, const QString &serverCountryCode);
|
||||
|
||||
ErrorCode prepareVpnKeyExport(int serverIndex, QString &vpnKey);
|
||||
ErrorCode prepareVpnKeyExport(const QString &serverId, QString &vpnKey);
|
||||
|
||||
ErrorCode validateAndUpdateConfig(int serverIndex, bool hasInstalledContainers);
|
||||
ErrorCode validateAndUpdateConfig(const QString &serverId, bool hasInstalledContainers,
|
||||
CaptchaInfo *captchaInfoOut = nullptr, ProtocolData *usedProtocolDataOut = nullptr);
|
||||
|
||||
void removeApiConfig(int serverIndex);
|
||||
void removeApiConfig(const QString &serverId);
|
||||
|
||||
void setCurrentProtocol(int serverIndex, const QString &protocolName);
|
||||
bool isVlessProtocol(int serverIndex) const;
|
||||
bool removeServer(const QString &serverId);
|
||||
|
||||
ErrorCode getAccountInfo(int serverIndex, QJsonObject &accountInfo);
|
||||
QFuture<QPair<ErrorCode, QString>> getRenewalLink(int serverIndex);
|
||||
void setCurrentProtocol(const QString &serverId, const QString &protocolName);
|
||||
bool isVlessProtocol(const QString &serverId) const;
|
||||
QString currentProtocol(const QString &serverId) const;
|
||||
QStringList availableProtocols(const QString &serverId) const;
|
||||
|
||||
struct AppStoreRestoreResult
|
||||
ErrorCode getAccountInfo(const QString &serverId, QJsonObject &accountInfo);
|
||||
QFuture<QPair<ErrorCode, QString>> getRenewalLink(const QString &serverId);
|
||||
|
||||
ErrorCode resolveImportServiceCaptcha(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const ProtocolData &protocolData,
|
||||
const QString &captchaId, const QString &captchaSolution,
|
||||
CaptchaInfo *retryCaptchaOut = nullptr);
|
||||
|
||||
#ifdef AMNEZIA_BUILD_TESTS
|
||||
// Exposes the gateway response parsing path without performing a network request.
|
||||
ErrorCode applyImportedServiceConfigForTest(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const ProtocolData &protocolData,
|
||||
const QByteArray &responseBody)
|
||||
{
|
||||
bool hasInstalledConfig = false;
|
||||
bool duplicateConfigAlreadyPresent = false;
|
||||
int duplicateCount = 0;
|
||||
int duplicateServerIndex = -1;
|
||||
ErrorCode errorCode = ErrorCode::NoError;
|
||||
};
|
||||
|
||||
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);
|
||||
return applyImportedServiceConfig(userCountryCode, serviceType, serviceProtocol, protocolData, responseBody);
|
||||
}
|
||||
ErrorCode applyUpdatedServiceConfigForTest(const QString &serverId, const QString &serviceProtocol,
|
||||
const ProtocolData &protocolData, const QByteArray &responseBody)
|
||||
{
|
||||
return applyUpdatedServiceConfig(serverId, serviceProtocol, protocolData, responseBody);
|
||||
}
|
||||
#endif
|
||||
|
||||
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,
|
||||
ErrorCode extractServerConfigJsonFromResponse(const QByteArray &apiResponseBody, const QString &protocol,
|
||||
const ProtocolData &protocolData, QJsonObject &serverConfigJson);
|
||||
void updateApiConfigInJson(QJsonObject &serverConfigJson, const QString &serviceType,
|
||||
ErrorCode applyUpdatedServiceConfig(const QString &serverId, const QString &serviceProtocol,
|
||||
const ProtocolData &protocolData, const QByteArray &responseBody);
|
||||
ErrorCode applyImportedServiceConfig(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const ProtocolData &protocolData,
|
||||
const QByteArray &responseBody);
|
||||
void markSubscriptionExpiredByServer(const QString &serverId, const ApiV2ServerConfig &apiV2);
|
||||
void updateApiConfigInJson(QJsonObject &serverConfigJson, const QString &serviceType,
|
||||
const QString &serviceProtocol, const QString &userCountryCode,
|
||||
const QByteArray &apiResponseBody);
|
||||
|
||||
|
||||
@@ -6,14 +6,13 @@
|
||||
#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/payloadSender.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"
|
||||
|
||||
@@ -32,7 +31,6 @@ ConnectionController::ConnectionController(SecureServersRepository* serversRepos
|
||||
connect(m_vpnConnection, &VpnConnection::connectionStateChanged, this, &ConnectionController::connectionStateChanged);
|
||||
connect(this, &ConnectionController::openConnectionRequested, m_vpnConnection, &VpnConnection::connectToVpn, Qt::QueuedConnection);
|
||||
connect(this, &ConnectionController::closeConnectionRequested, m_vpnConnection, &VpnConnection::disconnectFromVpn, Qt::QueuedConnection);
|
||||
connect(this, &ConnectionController::setConnectionStateRequested, m_vpnConnection, &VpnConnection::setConnectionState, Qt::QueuedConnection);
|
||||
connect(this, &ConnectionController::killSwitchModeChangedRequested, m_vpnConnection, &VpnConnection::onKillSwitchModeChanged, Qt::QueuedConnection);
|
||||
#ifdef Q_OS_ANDROID
|
||||
connect(this, &ConnectionController::restoreConnectionRequested, m_vpnConnection, &VpnConnection::restoreConnection, Qt::QueuedConnection);
|
||||
@@ -46,48 +44,184 @@ bool ConnectionController::isConnected() const
|
||||
|
||||
void ConnectionController::setConnectionState(Vpn::ConnectionState state)
|
||||
{
|
||||
if (m_vpnConnection) {
|
||||
emit setConnectionStateRequested(state);
|
||||
emit connectionStateChanged(state);
|
||||
}
|
||||
|
||||
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::prepareConnection(int serverIndex,
|
||||
QJsonObject& vpnConfiguration,
|
||||
DockerContainer& container)
|
||||
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);
|
||||
const auto apiV2 = m_serversRepository->apiV2Config(serverId);
|
||||
if (apiV2.has_value() && !apiV2->sendPayload.isEmpty()) {
|
||||
PayloadSender::sendAll(apiV2->sendPayload);
|
||||
}
|
||||
|
||||
emit openConnectionRequested(serverId, container, vpnConfiguration);
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
@@ -120,7 +254,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 +271,7 @@ QJsonObject ConnectionController::createConnectionConfiguration(const QPair<QStr
|
||||
|
||||
ConnectionSettings connectionSettings = {
|
||||
{ dns.first, dns.second },
|
||||
serverConfig.isApiConfig(),
|
||||
isApiConfig,
|
||||
{
|
||||
m_appSettingsRepository->isSitesSplitTunnelingEnabled(),
|
||||
m_appSettingsRepository->routeMode()
|
||||
@@ -160,10 +297,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;
|
||||
}
|
||||
|
||||
@@ -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,9 +65,8 @@ 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);
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
@@ -70,6 +74,8 @@ signals:
|
||||
#endif
|
||||
|
||||
private:
|
||||
ErrorCode defaultContainerForServer(const QString &serverId, DockerContainer &container) const;
|
||||
|
||||
SecureServersRepository* m_serversRepository;
|
||||
SecureAppSettingsRepository* m_appSettingsRepository;
|
||||
VpnConnection* m_vpnConnection;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "coreController.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDirIterator>
|
||||
#include <QTranslator>
|
||||
#include <QTimer>
|
||||
@@ -8,9 +9,9 @@
|
||||
#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"
|
||||
#include "core/utils/appUiConfig.h"
|
||||
|
||||
#if defined(Q_OS_ANDROID)
|
||||
#include "core/utils/installedAppsImageProvider.h"
|
||||
@@ -19,11 +20,15 @@
|
||||
|
||||
#if defined(Q_OS_IOS)
|
||||
#include "platforms/ios/ios_controller.h"
|
||||
#include <AmneziaVPN-Swift.h>
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
#include "core/utils/swiftBridge.h"
|
||||
#endif
|
||||
|
||||
CoreController::CoreController(const QSharedPointer<VpnConnection> &vpnConnection, SecureQSettings* settings,
|
||||
QQmlApplicationEngine *engine, QObject *parent)
|
||||
QQmlApplicationEngine *engine, QObject *parent,
|
||||
bool skipPlatformControllerInit)
|
||||
: QObject(parent), m_vpnConnection(vpnConnection), m_settings(settings), m_engine(engine)
|
||||
{
|
||||
initRepositories();
|
||||
@@ -32,8 +37,10 @@ CoreController::CoreController(const QSharedPointer<VpnConnection> &vpnConnectio
|
||||
initControllers();
|
||||
initSignalHandlers();
|
||||
|
||||
initAndroidController();
|
||||
initAppleController();
|
||||
if (!skipPlatformControllerInit) {
|
||||
initAndroidController();
|
||||
initAppleController();
|
||||
}
|
||||
initLogging();
|
||||
|
||||
m_translator = new QTranslator(this);
|
||||
@@ -87,6 +94,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 +111,15 @@ 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_tProxyConfigModel = new TProxyConfigModel(this);
|
||||
setQmlContextProperty("TProxyConfigModel", m_tProxyConfigModel);
|
||||
|
||||
m_clientManagementModel = new ClientManagementModel(this);
|
||||
setQmlContextProperty("ClientManagementModel", m_clientManagementModel);
|
||||
|
||||
@@ -110,6 +129,12 @@ void CoreController::initModels()
|
||||
m_apiCountryModel = new ApiCountryModel(this);
|
||||
setQmlContextProperty("ApiCountryModel", m_apiCountryModel);
|
||||
|
||||
m_apiCountryListModel = new ApiCountryListModel(m_apiCountryModel, "picker", this);
|
||||
setQmlContextProperty("ApiCountryListModel", m_apiCountryListModel);
|
||||
|
||||
m_apiConfigsCountryListModel = new ApiCountryListModel(m_apiCountryModel, "nativeConfigs", this);
|
||||
setQmlContextProperty("ApiConfigsCountryListModel", m_apiConfigsCountryListModel);
|
||||
|
||||
m_apiSubscriptionPlansModel = new ApiSubscriptionPlansModel(this);
|
||||
setQmlContextProperty("ApiSubscriptionPlansModel", m_apiSubscriptionPlansModel);
|
||||
|
||||
@@ -145,13 +170,16 @@ 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_storePurchaseController = new StorePurchaseController(m_serversRepository, m_appSettingsRepository);
|
||||
m_newsController = new NewsController(m_appSettingsRepository, m_serversRepository);
|
||||
m_updateController = new UpdateController(m_appSettingsRepository, this);
|
||||
|
||||
m_installController = new InstallController(m_serversRepository, m_appSettingsRepository, this);
|
||||
m_exportController = new ExportController(m_serversRepository, m_appSettingsRepository, this);
|
||||
m_importCoreController = new ImportController(m_serversRepository, m_appSettingsRepository, this);
|
||||
m_connectionController = new ConnectionController(m_serversRepository, m_appSettingsRepository, m_vpnConnection.get(), this);
|
||||
m_settingsController = new SettingsController(m_serversRepository, m_appSettingsRepository, this);
|
||||
m_localProxyController = new LocalProxyController(m_serversRepository, m_appSettingsRepository, m_subscriptionController, this);
|
||||
}
|
||||
|
||||
void CoreController::initControllers()
|
||||
@@ -161,15 +189,16 @@ void CoreController::initControllers()
|
||||
|
||||
if (m_engine) {
|
||||
m_focusController = new FocusController(m_engine, this);
|
||||
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_tProxyConfigModel,
|
||||
m_connectionController, this);
|
||||
setQmlContextProperty("InstallController", m_installUiController);
|
||||
|
||||
m_importController = new ImportUiController(m_importCoreController, this);
|
||||
@@ -181,9 +210,15 @@ 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_localProxyUiController = new LocalProxyUiController(m_localProxyController, this);
|
||||
setQmlContextProperty("LocalProxyController", m_localProxyUiController);
|
||||
|
||||
m_logsUiController = new LogsUiController(m_settingsController, this);
|
||||
setQmlContextProperty("LogsController", m_logsUiController);
|
||||
|
||||
m_pageController = new PageController(m_serversController, m_settingsController, this);
|
||||
setQmlContextProperty("PageController", m_pageController);
|
||||
|
||||
@@ -202,16 +237,25 @@ 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_storePurchaseController,
|
||||
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);
|
||||
setQmlContextProperty("ApiNewsController", m_apiNewsUiController);
|
||||
|
||||
m_updateUiController = new UpdateUiController(m_updateController, this);
|
||||
setQmlContextProperty("UpdateController", m_updateUiController);
|
||||
}
|
||||
|
||||
void CoreController::initAndroidController()
|
||||
@@ -237,13 +281,13 @@ void CoreController::initAppleController()
|
||||
{
|
||||
#ifdef Q_OS_IOS
|
||||
IosController::Instance()->initialize();
|
||||
QTimer::singleShot(0, this, [this]() { AmneziaVPN::toggleScreenshots(m_appSettingsRepository->isScreenshotsEnabled()); });
|
||||
QTimer::singleShot(0, this, [this]() { SWIFT_BRIDGE_NAMESPACE::toggleScreenshots(m_appSettingsRepository->isScreenshotsEnabled()); });
|
||||
#endif
|
||||
}
|
||||
|
||||
void CoreController::initLogging()
|
||||
{
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
#ifndef Q_OS_ANDROID
|
||||
bool enabled = m_appSettingsRepository->isSaveLogs();
|
||||
if (enabled) {
|
||||
if (!Logger::init(false)) {
|
||||
@@ -252,15 +296,32 @@ void CoreController::initLogging()
|
||||
}
|
||||
Logger::setServiceLogsEnabled(enabled);
|
||||
#endif
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
if (m_appSettingsRepository->isSaveLogs()) {
|
||||
SWIFT_BRIDGE_NAMESPACE::toggleLogging(true);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
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::checkForAppUpdates()
|
||||
{
|
||||
if (!m_appSettingsRepository->isAutoUpdateCheckEnabled()) {
|
||||
return;
|
||||
}
|
||||
m_updateController->checkForUpdates();
|
||||
}
|
||||
|
||||
void CoreController::updateTranslator(const QLocale &locale)
|
||||
@@ -270,15 +331,15 @@ void CoreController::updateTranslator(const QLocale &locale)
|
||||
}
|
||||
|
||||
QStringList availableTranslations;
|
||||
QDirIterator it(":/translations", QStringList("amneziavpn_*.qm"), QDir::Files);
|
||||
QDirIterator it(":/translations", QStringList(APP_TS_PREFIX "_*.qm"), QDir::Files);
|
||||
while (it.hasNext()) {
|
||||
availableTranslations << it.next();
|
||||
}
|
||||
|
||||
// This code allow to load translation for the language only, without country code
|
||||
const QString lang = locale.name().split("_").first();
|
||||
const QString translationFilePrefix = QString(":/translations/amneziavpn_") + lang;
|
||||
QString strFileName = QString(":/translations/amneziavpn_%1.qm").arg(locale.name());
|
||||
const QString translationFilePrefix = QString(":/translations/" APP_TS_PREFIX "_") + lang;
|
||||
QString strFileName = QString(":/translations/" APP_TS_PREFIX "_%1.qm").arg(locale.name());
|
||||
for (const QString &translation : availableTranslations) {
|
||||
if (translation.contains(translationFilePrefix)) {
|
||||
strFileName = translation;
|
||||
@@ -289,7 +350,7 @@ void CoreController::updateTranslator(const QLocale &locale)
|
||||
if (m_translator->load(strFileName)) {
|
||||
QCoreApplication::installTranslator(m_translator);
|
||||
} else {
|
||||
if (m_translator->load(QString(":/translations/amneziavpn_en.qm"))) {
|
||||
if (m_translator->load(QString(":/translations/" APP_TS_PREFIX "_en.qm"))) {
|
||||
QCoreApplication::installTranslator(m_translator);
|
||||
}
|
||||
}
|
||||
@@ -318,11 +379,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();
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
#include <QObject>
|
||||
#include <QQmlContext>
|
||||
#include <QSharedPointer>
|
||||
#include <QThread>
|
||||
#include <QTranslator>
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
#include "ui/utils/systemTrayNotificationHandler.h"
|
||||
@@ -22,11 +24,15 @@
|
||||
#include "ui/controllers/selfhosted/installUiController.h"
|
||||
#include "ui/controllers/qml/pageController.h"
|
||||
#include "ui/controllers/settingsUiController.h"
|
||||
#include "ui/controllers/localProxyUiController.h"
|
||||
#include "ui/controllers/logsUiController.h"
|
||||
#include "ui/controllers/serversUiController.h"
|
||||
#include "ui/controllers/ipSplitTunnelingUiController.h"
|
||||
#include "ui/controllers/systemController.h"
|
||||
#include "ui/controllers/languageUiController.h"
|
||||
#include "ui/controllers/updateUiController.h"
|
||||
#include "ui/controllers/api/servicesCatalogUiController.h"
|
||||
#include "ui/controllers/networkReachabilityController.h"
|
||||
|
||||
#include "core/controllers/serversController.h"
|
||||
#include "core/controllers/selfhosted/usersController.h"
|
||||
@@ -34,11 +40,14 @@
|
||||
#include "core/controllers/ipSplitTunnelingController.h"
|
||||
#include "core/controllers/allowedDnsController.h"
|
||||
#include "core/controllers/api/servicesCatalogController.h"
|
||||
#include "core/controllers/api/storePurchaseController.h"
|
||||
#include "core/controllers/api/subscriptionController.h"
|
||||
#include "core/controllers/api/newsController.h"
|
||||
#include "core/controllers/selfhosted/installController.h"
|
||||
#include "core/controllers/settingsController.h"
|
||||
#include "core/controllers/localProxyController.h"
|
||||
#include "core/controllers/connectionController.h"
|
||||
#include "core/controllers/updateController.h"
|
||||
|
||||
#include "core/repositories/secureServersRepository.h"
|
||||
#include "core/repositories/secureAppSettingsRepository.h"
|
||||
@@ -53,6 +62,7 @@
|
||||
#include "ui/models/api/apiAccountInfoModel.h"
|
||||
#include "ui/models/api/apiBenefitsModel.h"
|
||||
#include "ui/models/api/apiCountryModel.h"
|
||||
#include "ui/models/api/apiCountryListModel.h"
|
||||
#include "ui/models/api/apiDevicesModel.h"
|
||||
#include "ui/models/api/apiServicesModel.h"
|
||||
#include "ui/models/api/apiSubscriptionPlansModel.h"
|
||||
@@ -62,11 +72,16 @@
|
||||
#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/services/tProxyConfigModel.h"
|
||||
|
||||
#include "ui/models/ipSplitTunnelingModel.h"
|
||||
#include "ui/models/newsModel.h"
|
||||
|
||||
@@ -75,42 +90,20 @@
|
||||
#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,
|
||||
QQmlApplicationEngine *engine, QObject *parent = nullptr);
|
||||
QQmlApplicationEngine *engine, QObject *parent = nullptr,
|
||||
bool skipPlatformControllerInit = false);
|
||||
|
||||
PageController* pageController() const;
|
||||
void setQmlRoot();
|
||||
void checkForAppUpdates();
|
||||
|
||||
void openConnectionByIndex(int serverIndex);
|
||||
void importConfigFromData(const QString &data);
|
||||
@@ -120,6 +113,37 @@ 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; }
|
||||
ConnectionUiController* connectionUiControllerProtected() const { return m_connectionUiController; }
|
||||
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; }
|
||||
SubscriptionController* subscriptionControllerProtected() const { return m_subscriptionController; }
|
||||
|
||||
private:
|
||||
void initRepositories();
|
||||
void initCoreControllers();
|
||||
@@ -153,12 +177,16 @@ private:
|
||||
ImportController* m_importCoreController;
|
||||
ExportUiController* m_exportUiController;
|
||||
SettingsUiController* m_settingsUiController;
|
||||
LocalProxyUiController* m_localProxyUiController;
|
||||
LogsUiController* m_logsUiController;
|
||||
ServersUiController* m_serversUiController;
|
||||
IpSplitTunnelingUiController* m_ipSplitTunnelingUiController;
|
||||
SystemController* m_systemController;
|
||||
NetworkReachabilityController* m_networkReachabilityController;
|
||||
AppSplitTunnelingUiController* m_appSplitTunnelingUiController;
|
||||
AllowedDnsUiController* m_allowedDnsUiController;
|
||||
LanguageUiController* m_languageUiController;
|
||||
UpdateUiController* m_updateUiController;
|
||||
|
||||
SubscriptionUiController* m_subscriptionUiController;
|
||||
ApiNewsUiController* m_apiNewsUiController;
|
||||
@@ -172,11 +200,14 @@ private:
|
||||
AllowedDnsController* m_allowedDnsController;
|
||||
ServicesCatalogController* m_servicesCatalogController;
|
||||
SubscriptionController* m_subscriptionController;
|
||||
StorePurchaseController* m_storePurchaseController;
|
||||
NewsController* m_newsController;
|
||||
UpdateController* m_updateController;
|
||||
InstallController* m_installController;
|
||||
ExportController* m_exportController;
|
||||
ConnectionController* m_connectionController;
|
||||
SettingsController* m_settingsController;
|
||||
LocalProxyController* m_localProxyController;
|
||||
|
||||
ContainersModel* m_containersModel;
|
||||
ContainersModel* m_defaultServerContainersModel;
|
||||
@@ -193,11 +224,14 @@ private:
|
||||
ApiSubscriptionPlansModel* m_apiSubscriptionPlansModel;
|
||||
ApiBenefitsModel* m_apiBenefitsModel;
|
||||
ApiCountryModel* m_apiCountryModel;
|
||||
ApiCountryListModel* m_apiCountryListModel;
|
||||
ApiCountryListModel* m_apiConfigsCountryListModel;
|
||||
ApiAccountInfoModel* m_apiAccountInfoModel;
|
||||
ApiDevicesModel* m_apiDevicesModel;
|
||||
|
||||
OpenVpnConfigModel* m_openVpnConfigModel;
|
||||
XrayConfigModel* m_xrayConfigModel;
|
||||
XrayConfigSnapshotsModel* m_xrayConfigSnapshotsModel;
|
||||
TorConfigModel* m_torConfigModel;
|
||||
WireGuardConfigModel* m_wireGuardConfigModel;
|
||||
AwgConfigModel* m_awgConfigModel;
|
||||
@@ -206,6 +240,9 @@ private:
|
||||
#endif
|
||||
SftpConfigModel* m_sftpConfigModel;
|
||||
Socks5ProxyConfigModel* m_socks5ConfigModel;
|
||||
MtProxyConfigModel* m_mtProxyConfigModel;
|
||||
TelemtConfigModel* m_telemtConfigModel;
|
||||
TProxyConfigModel* m_tProxyConfigModel;
|
||||
|
||||
CoreSignalHandlers* m_signalHandlers;
|
||||
};
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
#include "coreSignalHandlers.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#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"
|
||||
@@ -20,6 +23,7 @@
|
||||
#include "ui/controllers/selfhosted/installUiController.h"
|
||||
#include "ui/controllers/importUiController.h"
|
||||
#include "ui/controllers/api/subscriptionUiController.h"
|
||||
#include "ui/controllers/updateUiController.h"
|
||||
#include "ui/models/serversModel.h"
|
||||
#include "core/controllers/serversController.h"
|
||||
#include "core/controllers/ipSplitTunnelingController.h"
|
||||
@@ -29,9 +33,9 @@
|
||||
#include "core/controllers/selfhosted/installController.h"
|
||||
#include "core/controllers/selfhosted/exportController.h"
|
||||
#include "core/controllers/connectionController.h"
|
||||
#include "core/controllers/localProxyController.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"
|
||||
|
||||
@@ -46,7 +50,7 @@
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#include "platforms/ios/ios_controller.h"
|
||||
#include <AmneziaVPN-Swift.h>
|
||||
#include "core/utils/swiftBridge.h"
|
||||
#endif
|
||||
|
||||
CoreSignalHandlers::CoreSignalHandlers(CoreController* coreController, QObject* parent)
|
||||
@@ -63,8 +67,9 @@ void CoreSignalHandlers::initAllHandlers()
|
||||
initExportControllerHandler();
|
||||
initImportControllerHandler();
|
||||
initApiCountryModelUpdateHandler();
|
||||
initCountryListSortModeHandler();
|
||||
initCountryListStateHandler();
|
||||
initSubscriptionRefreshHandler();
|
||||
initContainerModelUpdateHandler();
|
||||
initAdminConfigRevokedHandler();
|
||||
initPassphraseRequestHandler();
|
||||
initTranslationsUpdatedHandler();
|
||||
@@ -77,12 +82,15 @@ void CoreSignalHandlers::initAllHandlers()
|
||||
initAllowedDnsModelUpdateHandler();
|
||||
initAppSplitTunnelingModelUpdateHandler();
|
||||
initPrepareConfigHandler();
|
||||
initUnsupportedConnectDrawerHandler();
|
||||
initStrictKillSwitchHandler();
|
||||
initAndroidSettingsHandler();
|
||||
initAndroidConnectionHandler();
|
||||
initIosImportHandler();
|
||||
initIosSettingsHandler();
|
||||
initNotificationHandler();
|
||||
initUpdateFoundHandler();
|
||||
initLocalProxyHandler();
|
||||
}
|
||||
|
||||
void CoreSignalHandlers::initErrorMessagesHandler()
|
||||
@@ -122,11 +130,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();
|
||||
}
|
||||
});
|
||||
@@ -135,75 +141,103 @@ 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void CoreSignalHandlers::initCountryListSortModeHandler()
|
||||
{
|
||||
SecureAppSettingsRepository *repository = m_coreController->m_appSettingsRepository;
|
||||
|
||||
const QVector<ApiCountryListModel *> models { m_coreController->m_apiCountryListModel,
|
||||
m_coreController->m_apiConfigsCountryListModel };
|
||||
|
||||
for (ApiCountryListModel *model : models) {
|
||||
model->setSortMode(repository->countryListSortMode(model->listId()));
|
||||
connect(model, &ApiCountryListModel::sortModeChanged, this, [repository, model]() {
|
||||
repository->setCountryListSortMode(model->listId(), model->sortMode());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void CoreSignalHandlers::initCountryListStateHandler()
|
||||
{
|
||||
SecureAppSettingsRepository *repository = m_coreController->m_appSettingsRepository;
|
||||
|
||||
const QVector<ApiCountryListModel *> models { m_coreController->m_apiCountryListModel,
|
||||
m_coreController->m_apiConfigsCountryListModel };
|
||||
|
||||
for (ApiCountryListModel *model : models) {
|
||||
model->setFavorites(repository->favoriteLocations());
|
||||
connect(model, &ApiCountryListModel::favoritesChanged, this, [repository, models, model](const QStringList &codes) {
|
||||
repository->setFavoriteLocations(codes);
|
||||
for (ApiCountryListModel *other : models) {
|
||||
if (other != model) {
|
||||
other->setFavorites(codes);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
apiV2->apiConfig.userCountryCode);
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -211,17 +245,17 @@ 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) {
|
||||
m_coreController->m_usersController->revokeClient(serverId, containerConfig, container);
|
||||
}, Qt::DirectConnection);
|
||||
|
||||
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()
|
||||
@@ -237,6 +271,18 @@ void CoreSignalHandlers::initTranslationsUpdatedHandler()
|
||||
connect(m_coreController->m_languageUiController, &LanguageUiController::updateTranslations, m_coreController, &CoreController::updateTranslator);
|
||||
connect(m_coreController, &CoreController::translationsUpdated, m_coreController->m_languageUiController, &LanguageUiController::translationsUpdated);
|
||||
connect(m_coreController, &CoreController::translationsUpdated, m_coreController->m_connectionUiController, &ConnectionUiController::onTranslationsUpdated);
|
||||
connect(m_coreController, &CoreController::translationsUpdated, m_coreController->m_logsUiController, &LogsUiController::refreshSizes);
|
||||
|
||||
const QVector<ApiCountryListModel *> countryListModels { m_coreController->m_apiCountryListModel,
|
||||
m_coreController->m_apiConfigsCountryListModel };
|
||||
auto applyUiLanguage = [this, countryListModels]() {
|
||||
const QString languageCode = QLocale::languageToCode(m_coreController->m_settingsController->getAppLanguage().language());
|
||||
for (ApiCountryListModel *model : countryListModels) {
|
||||
model->setUiLanguage(languageCode);
|
||||
}
|
||||
};
|
||||
applyUiLanguage();
|
||||
connect(m_coreController, &CoreController::translationsUpdated, this, applyUiLanguage);
|
||||
}
|
||||
|
||||
void CoreSignalHandlers::initLanguageHandler()
|
||||
@@ -245,12 +291,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(); });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,16 +319,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()
|
||||
@@ -287,6 +341,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()
|
||||
@@ -313,7 +369,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) {
|
||||
@@ -322,7 +390,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) {
|
||||
@@ -335,6 +403,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,
|
||||
@@ -346,7 +423,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
|
||||
}
|
||||
@@ -386,7 +466,7 @@ void CoreSignalHandlers::initIosImportHandler()
|
||||
void CoreSignalHandlers::initIosSettingsHandler()
|
||||
{
|
||||
#ifdef Q_OS_IOS
|
||||
connect(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::screenshotsEnabledChanged, [](bool enabled) { AmneziaVPN::toggleScreenshots(enabled); });
|
||||
connect(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::screenshotsEnabledChanged, [](bool enabled) { SWIFT_BRIDGE_NAMESPACE::toggleScreenshots(enabled); });
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -410,3 +490,19 @@ void CoreSignalHandlers::initNotificationHandler()
|
||||
#endif
|
||||
}
|
||||
|
||||
void CoreSignalHandlers::initUpdateFoundHandler()
|
||||
{
|
||||
connect(m_coreController->m_updateUiController, &UpdateUiController::updateFound, this, [this]() {
|
||||
emit m_coreController->m_pageController->goToPage(PageLoader::PageEnum::PageUpdate);
|
||||
});
|
||||
}
|
||||
|
||||
void CoreSignalHandlers::initLocalProxyHandler()
|
||||
{
|
||||
connect(m_coreController->m_connectionController, &ConnectionController::openConnectionRequested, m_coreController->m_localProxyController,
|
||||
&LocalProxyController::onVpnConnectionRequested);
|
||||
connect(m_coreController->m_localProxyController, &LocalProxyController::stoppedByVpn, m_coreController->m_pageController,
|
||||
&PageController::showNotificationMessage);
|
||||
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, m_coreController->m_localProxyController,
|
||||
&LocalProxyController::disable);
|
||||
}
|
||||
@@ -20,8 +20,9 @@ private:
|
||||
void initExportControllerHandler();
|
||||
void initImportControllerHandler();
|
||||
void initApiCountryModelUpdateHandler();
|
||||
void initCountryListSortModeHandler();
|
||||
void initCountryListStateHandler();
|
||||
void initSubscriptionRefreshHandler();
|
||||
void initContainerModelUpdateHandler();
|
||||
void initAdminConfigRevokedHandler();
|
||||
void initPassphraseRequestHandler();
|
||||
void initTranslationsUpdatedHandler();
|
||||
@@ -34,12 +35,15 @@ private:
|
||||
void initAllowedDnsModelUpdateHandler();
|
||||
void initAppSplitTunnelingModelUpdateHandler();
|
||||
void initPrepareConfigHandler();
|
||||
void initUnsupportedConnectDrawerHandler();
|
||||
void initStrictKillSwitchHandler();
|
||||
void initAndroidSettingsHandler();
|
||||
void initAndroidConnectionHandler();
|
||||
void initIosImportHandler();
|
||||
void initIosSettingsHandler();
|
||||
void initNotificationHandler();
|
||||
void initUpdateFoundHandler();
|
||||
void initLocalProxyHandler();
|
||||
|
||||
CoreController* m_coreController;
|
||||
};
|
||||
|
||||
@@ -1,702 +1,268 @@
|
||||
#include "gatewayController.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <random>
|
||||
|
||||
#include <QCryptographicHash>
|
||||
#include <QDebug>
|
||||
#include <QEventLoop>
|
||||
#include <QFutureWatcher>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkReply>
|
||||
#include <QPromise>
|
||||
#include <QThread>
|
||||
#include <QUrl>
|
||||
#include <QtConcurrent/QtConcurrentRun>
|
||||
|
||||
#include "QBlockCipher.h"
|
||||
#include "QRsa.h"
|
||||
|
||||
#include "amneziaApplication.h"
|
||||
#include "core/utils/api/apiUtils.h"
|
||||
#include "core/repositories/secureAppSettingsRepository.h"
|
||||
#include "core/utils/constants/apiConstants.h"
|
||||
#include "core/utils/constants/apiKeys.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
#include "core/utils/utilities.h"
|
||||
#include "core/utils/api/apiUtils.h"
|
||||
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
#include "core/utils/ipcClient.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#include "platforms/ios/ios_controller.h"
|
||||
#endif
|
||||
|
||||
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.");
|
||||
namespace agwConfigKey
|
||||
{
|
||||
constexpr QLatin1String gatewayEndpoint("gateway_endpoint");
|
||||
constexpr QLatin1String publicKeyPem("public_key_pem");
|
||||
constexpr QLatin1String s3PrimaryEndpoints("s3_primary_endpoints");
|
||||
constexpr QLatin1String s3FallbackEndpoints("s3_fallback_endpoints");
|
||||
constexpr QLatin1String isDevEnvironment("is_dev_environment");
|
||||
constexpr QLatin1String requestTimeoutMsecs("request_timeout_msecs");
|
||||
constexpr QLatin1String proxyRequestTimeoutMsecs("proxy_request_timeout_msecs");
|
||||
constexpr QLatin1String proxyStorageTimeoutMsecs("proxy_storage_timeout_msecs");
|
||||
constexpr QLatin1String proxyHealthTimeoutMsecs("proxy_health_timeout_msecs");
|
||||
}
|
||||
|
||||
constexpr QLatin1String updateRequestResponsePattern("client version update is required");
|
||||
namespace agwOptionsKey
|
||||
{
|
||||
constexpr QLatin1String serviceType("service_type");
|
||||
constexpr QLatin1String userCountryCode("user_country_code");
|
||||
}
|
||||
|
||||
constexpr int httpStatusCodeNotFound = 404;
|
||||
constexpr int httpStatusCodeConflict = 409;
|
||||
constexpr int httpStatusCodeNotImplemented = 501;
|
||||
constexpr int httpStatusCodePaymentRequired = 402;
|
||||
constexpr int httpStatusCodeUnprocessableEntity = 422;
|
||||
namespace agwStateKey
|
||||
{
|
||||
constexpr QLatin1String workingProxy("working_proxy");
|
||||
}
|
||||
|
||||
constexpr QLatin1String unprocessableSubscriptionMessage("Failed to retrieve subscription information. Is it activated?");
|
||||
constexpr QLatin1String agwStateCacheKey("agw_state");
|
||||
constexpr QLatin1String agwTracePrefix("agw trace:");
|
||||
constexpr QLatin1String agwTraceFailoverMarker("suspicious");
|
||||
constexpr QLatin1String endpointListSeparator(", ");
|
||||
constexpr QLatin1String legacyEndpointPlaceholder("%1");
|
||||
|
||||
constexpr int proxyStorageRequestTimeoutMsecs = 3000;
|
||||
QStringList splitEndpoints(const char *raw)
|
||||
{
|
||||
return QString::fromUtf8(raw).split(endpointListSeparator, Qt::SkipEmptyParts);
|
||||
}
|
||||
|
||||
void logAgwTrace(const QString &path, const int code, const QString &trace)
|
||||
{
|
||||
if (trace.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const QString text = QStringLiteral("%1 %2\n%3").arg(agwTracePrefix, path, trace).replace(QLatin1Char('\n'), QStringLiteral("\n "));
|
||||
if (code != AGW_OK) {
|
||||
qWarning().noquote() << text;
|
||||
} else if (trace.contains(agwTraceFailoverMarker)) {
|
||||
qInfo().noquote() << text;
|
||||
} else {
|
||||
qDebug().noquote() << text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
|
||||
const bool isStrictKillSwitchEnabled, QObject *parent)
|
||||
const bool isStrictKillSwitchEnabled, SecureAppSettingsRepository *appSettingsRepository,
|
||||
QObject *parent)
|
||||
: QObject(parent),
|
||||
m_gatewayEndpoint(gatewayEndpoint),
|
||||
m_isDevEnvironment(isDevEnvironment),
|
||||
m_requestTimeoutMsecs(requestTimeoutMsecs),
|
||||
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled)
|
||||
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled),
|
||||
m_appSettingsRepository(appSettingsRepository)
|
||||
{
|
||||
const QByteArray publicKey = isDevEnvironment ? QByteArray(DEV_AGW_PUBLIC_KEY) : QByteArray(PROD_AGW_PUBLIC_KEY);
|
||||
m_publicKeyMissing = publicKey.isEmpty();
|
||||
|
||||
QStringList primaryEndpoints;
|
||||
QStringList fallbackEndpoints;
|
||||
if (isDevEnvironment) {
|
||||
primaryEndpoints = splitEndpoints(DEV_S3_ENDPOINT);
|
||||
} else {
|
||||
primaryEndpoints = splitEndpoints(PROD_S3_ENDPOINT);
|
||||
fallbackEndpoints = splitEndpoints(FALLBACK_S3_ENDPOINT);
|
||||
}
|
||||
|
||||
QJsonObject config;
|
||||
config[agwConfigKey::gatewayEndpoint] = gatewayEndpoint;
|
||||
config[agwConfigKey::publicKeyPem] = QString::fromUtf8(publicKey);
|
||||
config[agwConfigKey::s3PrimaryEndpoints] = QJsonArray::fromStringList(primaryEndpoints);
|
||||
config[agwConfigKey::s3FallbackEndpoints] = QJsonArray::fromStringList(fallbackEndpoints);
|
||||
config[agwConfigKey::isDevEnvironment] = isDevEnvironment;
|
||||
config[agwConfigKey::requestTimeoutMsecs] = requestTimeoutMsecs;
|
||||
config[agwConfigKey::proxyRequestTimeoutMsecs] = apiDefs::proxyRequestTimeoutMsecs;
|
||||
config[agwConfigKey::proxyStorageTimeoutMsecs] = apiDefs::proxyStorageTimeoutMsecs;
|
||||
config[agwConfigKey::proxyHealthTimeoutMsecs] = apiDefs::proxyHealthTimeoutMsecs;
|
||||
|
||||
m_client = agw_client_create(QJsonDocument(config).toJson(QJsonDocument::Compact).constData(), nullptr);
|
||||
if (m_client == 0) {
|
||||
qCritical() << "GatewayController: failed to create gateway client (missing key or endpoint?)";
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_appSettingsRepository != nullptr) {
|
||||
const QByteArray state = m_appSettingsRepository->readGatewayProxyUrls(agwStateCacheKey);
|
||||
if (!state.isEmpty() && agw_import_state(m_client, state.constData()) == AGW_OK) {
|
||||
m_lastPersistedState = state;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GatewayController::EncryptedRequestData GatewayController::prepareRequest(const QString &endpoint, const QJsonObject &apiPayload)
|
||||
GatewayController::~GatewayController()
|
||||
{
|
||||
EncryptedRequestData encRequestData;
|
||||
encRequestData.errorCode = ErrorCode::NoError;
|
||||
agw_client_destroy(m_client);
|
||||
}
|
||||
|
||||
amnezia::ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody)
|
||||
{
|
||||
QFuture<QPair<amnezia::ErrorCode, QByteArray>> future = postAsync(endpoint, apiPayload);
|
||||
|
||||
QFutureWatcher<QPair<amnezia::ErrorCode, QByteArray>> watcher;
|
||||
QEventLoop wait;
|
||||
connect(&watcher, &QFutureWatcherBase::finished, &wait, &QEventLoop::quit);
|
||||
watcher.setFuture(future);
|
||||
if (!future.isFinished()) {
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
}
|
||||
|
||||
const QPair<amnezia::ErrorCode, QByteArray> result = future.result();
|
||||
responseBody = result.second;
|
||||
return result.first;
|
||||
}
|
||||
|
||||
QFuture<QPair<amnezia::ErrorCode, QByteArray>> GatewayController::postAsync(const QString &endpoint, const QJsonObject apiPayload)
|
||||
{
|
||||
prepareRequest();
|
||||
return QtConcurrent::run(
|
||||
[this, endpoint, apiPayload]() -> QPair<amnezia::ErrorCode, QByteArray> { return executePost(endpoint, apiPayload); });
|
||||
}
|
||||
|
||||
QPair<amnezia::ErrorCode, QByteArray> GatewayController::executePost(const QString &endpoint, const QJsonObject &apiPayload)
|
||||
{
|
||||
if (m_client == 0) {
|
||||
return qMakePair(m_publicKeyMissing ? amnezia::ErrorCode::ApiMissingAgwPublicKey : amnezia::ErrorCode::ApiConfigDownloadError,
|
||||
QByteArray());
|
||||
}
|
||||
|
||||
QString path = endpoint;
|
||||
path.remove(legacyEndpointPlaceholder);
|
||||
|
||||
QJsonObject options;
|
||||
options[agwOptionsKey::serviceType] = apiPayload.value(apiDefs::key::serviceType).toString();
|
||||
options[agwOptionsKey::userCountryCode] = apiPayload.value(apiDefs::key::userCountryCode).toString();
|
||||
|
||||
const QByteArray payload = QJsonDocument(apiPayload).toJson(QJsonDocument::Compact);
|
||||
const QByteArray optionsJson = QJsonDocument(options).toJson(QJsonDocument::Compact);
|
||||
|
||||
agw_result result = agw_post(m_client, path.toUtf8().constData(), payload.constData(), optionsJson.constData(), 0);
|
||||
|
||||
QByteArray responseBody;
|
||||
if (result.body != nullptr) {
|
||||
responseBody = QByteArray(result.body, static_cast<qsizetype>(result.body_len));
|
||||
}
|
||||
QString trace;
|
||||
if (result.trace != nullptr) {
|
||||
trace = QString::fromUtf8(result.trace, static_cast<qsizetype>(result.trace_len));
|
||||
}
|
||||
const int code = result.code;
|
||||
agw_result_free(&result);
|
||||
|
||||
logAgwTrace(path, code, trace);
|
||||
persistState();
|
||||
|
||||
return qMakePair(mapResultCode(code, responseBody), responseBody);
|
||||
}
|
||||
|
||||
amnezia::ErrorCode GatewayController::mapResultCode(const int code, const QByteArray &responseBody)
|
||||
{
|
||||
switch (code) {
|
||||
case AGW_OK: return apiUtils::checkApiResponseErrors(responseBody);
|
||||
case AGW_CANCELLED:
|
||||
case AGW_ERR_TIMEOUT: return amnezia::ErrorCode::ApiConfigTimeoutError;
|
||||
case AGW_ERR_SSL: return amnezia::ErrorCode::ApiConfigSslError;
|
||||
case AGW_ERR_CONFIG: return amnezia::ErrorCode::ApiMissingAgwPublicKey;
|
||||
case AGW_ERR_DECRYPT: return amnezia::ErrorCode::ApiConfigDecryptionError;
|
||||
default: return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||
}
|
||||
}
|
||||
|
||||
void GatewayController::prepareRequest()
|
||||
{
|
||||
#ifdef Q_OS_IOS
|
||||
IosController::Instance()->requestInetAccess();
|
||||
QThread::msleep(10);
|
||||
#endif
|
||||
|
||||
encRequestData.request.setTransferTimeout(m_requestTimeoutMsecs);
|
||||
encRequestData.request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
encRequestData.request.setRawHeader(QString("X-Client-Request-ID").toUtf8(), QUuid::createUuid().toString(QUuid::WithoutBraces).toUtf8());
|
||||
encRequestData.request.setUrl(endpoint.arg(m_proxyUrl.isEmpty() ? m_gatewayEndpoint : m_proxyUrl));
|
||||
|
||||
// bypass killSwitch exceptions for API-gateway
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
if (m_isStrictKillSwitchEnabled) {
|
||||
QString host = QUrl(encRequestData.request.url()).host();
|
||||
QString ip = NetworkUtilities::getIPAddress(host);
|
||||
if (!m_isStrictKillSwitchEnabled) {
|
||||
return;
|
||||
}
|
||||
QStringList ips;
|
||||
for (const QString &host : requestHosts()) {
|
||||
const QString ip = NetworkUtilities::getIPAddress(host);
|
||||
if (!ip.isEmpty()) {
|
||||
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
QRemoteObjectPendingReply<bool> reply = iface->addKillSwitchAllowedRange(QStringList { ip });
|
||||
if (!reply.waitForFinished(1000) || !reply.returnValue())
|
||||
qWarning() << "GatewayController::prepareRequest(): Failed to execute remote addKillSwitchAllowedRange call";
|
||||
});
|
||||
ips.append(ip);
|
||||
}
|
||||
}
|
||||
if (ips.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
QRemoteObjectPendingReply<bool> reply = iface->addKillSwitchAllowedRange(ips);
|
||||
if (!reply.waitForFinished(1000) || !reply.returnValue()) {
|
||||
qWarning() << "GatewayController::prepareRequest(): Failed to execute remote addKillSwitchAllowedRange call";
|
||||
}
|
||||
});
|
||||
#endif
|
||||
|
||||
QSimpleCrypto::QBlockCipher blockCipher;
|
||||
encRequestData.key = blockCipher.generatePrivateSalt(32);
|
||||
encRequestData.iv = blockCipher.generatePrivateSalt(32);
|
||||
encRequestData.salt = blockCipher.generatePrivateSalt(8);
|
||||
|
||||
QJsonObject keyPayload;
|
||||
keyPayload[apiDefs::key::aesKey] = QString(encRequestData.key.toBase64());
|
||||
keyPayload[apiDefs::key::aesIv] = QString(encRequestData.iv.toBase64());
|
||||
keyPayload[apiDefs::key::aesSalt] = QString(encRequestData.salt.toBase64());
|
||||
|
||||
QByteArray encryptedKeyPayload;
|
||||
QByteArray encryptedApiPayload;
|
||||
try {
|
||||
QSimpleCrypto::QRsa rsa;
|
||||
|
||||
EVP_PKEY *publicKey = nullptr;
|
||||
try {
|
||||
QByteArray rsaKey = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||
QSimpleCrypto::QRsa rsa;
|
||||
publicKey = rsa.getPublicKeyFromByteArray(rsaKey);
|
||||
} catch (...) {
|
||||
Utils::logException();
|
||||
qCritical() << "error loading public key from environment variables";
|
||||
encRequestData.errorCode = ErrorCode::ApiMissingAgwPublicKey;
|
||||
return encRequestData;
|
||||
}
|
||||
|
||||
encryptedKeyPayload = rsa.encrypt(QJsonDocument(keyPayload).toJson(), publicKey, RSA_PKCS1_PADDING);
|
||||
EVP_PKEY_free(publicKey);
|
||||
|
||||
encryptedApiPayload = blockCipher.encryptAesBlockCipher(QJsonDocument(apiPayload).toJson(), encRequestData.key, encRequestData.iv,
|
||||
"", encRequestData.salt);
|
||||
} catch (...) {
|
||||
Utils::logException();
|
||||
qCritical() << "error when encrypting the request body";
|
||||
encRequestData.errorCode = ErrorCode::ApiConfigDecryptionError;
|
||||
return encRequestData;
|
||||
}
|
||||
|
||||
QJsonObject requestBody;
|
||||
requestBody[apiDefs::key::keyPayload] = QString(encryptedKeyPayload.toBase64());
|
||||
requestBody[apiDefs::key::apiPayload] = QString(encryptedApiPayload.toBase64());
|
||||
|
||||
encRequestData.requestBody = QJsonDocument(requestBody).toJson();
|
||||
return encRequestData;
|
||||
}
|
||||
|
||||
GatewayController::DecryptionResult GatewayController::tryDecryptResponseBody(const QByteArray &encryptedResponseBody,
|
||||
QNetworkReply::NetworkError replyError, const QByteArray &key,
|
||||
const QByteArray &iv, const QByteArray &salt)
|
||||
QStringList GatewayController::requestHosts() const
|
||||
{
|
||||
DecryptionResult result;
|
||||
result.decryptedBody = encryptedResponseBody;
|
||||
result.isDecryptionSuccessful = false;
|
||||
|
||||
try {
|
||||
QSimpleCrypto::QBlockCipher blockCipher;
|
||||
result.decryptedBody = blockCipher.decryptAesBlockCipher(encryptedResponseBody, key, iv, "", salt);
|
||||
result.isDecryptionSuccessful = true;
|
||||
} catch (...) {
|
||||
result.decryptedBody = encryptedResponseBody;
|
||||
result.isDecryptionSuccessful = false;
|
||||
QStringList hosts;
|
||||
if (m_client != 0) {
|
||||
char *state = agw_export_state(m_client);
|
||||
if (state != nullptr) {
|
||||
const QString workingProxy = QJsonDocument::fromJson(QByteArray(state)).object().value(agwStateKey::workingProxy).toString();
|
||||
agw_string_free(state);
|
||||
if (!workingProxy.isEmpty()) {
|
||||
hosts.append(QUrl(workingProxy).host());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
hosts.append(QUrl(m_gatewayEndpoint).host());
|
||||
hosts.removeAll(QString());
|
||||
hosts.removeDuplicates();
|
||||
return hosts;
|
||||
}
|
||||
|
||||
ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody)
|
||||
void GatewayController::persistState()
|
||||
{
|
||||
EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload);
|
||||
if (encRequestData.errorCode != ErrorCode::NoError) {
|
||||
return encRequestData.errorCode;
|
||||
}
|
||||
|
||||
QNetworkReply *reply = amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody);
|
||||
|
||||
QEventLoop wait;
|
||||
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
|
||||
|
||||
QList<QSslError> sslErrors;
|
||||
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
|
||||
QByteArray encryptedResponseBody = reply->readAll();
|
||||
QString replyErrorString = reply->errorString();
|
||||
auto replyError = reply->error();
|
||||
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
auto decryptionResult =
|
||||
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
|
||||
|
||||
if (sslErrors.isEmpty() && shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
|
||||
auto requestFunction = [&encRequestData, &encryptedResponseBody](const QString &url) {
|
||||
encRequestData.request.setUrl(url);
|
||||
return amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody);
|
||||
};
|
||||
|
||||
auto replyProcessingFunction = [&encryptedResponseBody, &replyErrorString, &replyError, &httpStatusCode, &sslErrors, &encRequestData,
|
||||
&decryptionResult, this](QNetworkReply *reply, const QList<QSslError> &nestedSslErrors) {
|
||||
encryptedResponseBody = reply->readAll();
|
||||
replyErrorString = reply->errorString();
|
||||
replyError = reply->error();
|
||||
httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
|
||||
decryptionResult =
|
||||
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
|
||||
|
||||
if (!sslErrors.isEmpty()
|
||||
|| shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
|
||||
sslErrors = nestedSslErrors;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString("");
|
||||
auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString("");
|
||||
bypassProxy(endpoint, serviceType, userCountryCode, requestFunction, replyProcessingFunction);
|
||||
}
|
||||
|
||||
auto errorCode =
|
||||
apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, decryptionResult.decryptedBody);
|
||||
if (errorCode) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
if (!decryptionResult.isDecryptionSuccessful) {
|
||||
qCritical() << "error when decrypting the request body";
|
||||
return ErrorCode::ApiConfigDecryptionError;
|
||||
}
|
||||
|
||||
responseBody = decryptionResult.decryptedBody;
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString &endpoint, const QJsonObject apiPayload)
|
||||
{
|
||||
auto promise = QSharedPointer<QPromise<QPair<ErrorCode, QByteArray>>>::create();
|
||||
promise->start();
|
||||
|
||||
EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload);
|
||||
if (encRequestData.errorCode != ErrorCode::NoError) {
|
||||
promise->addResult(qMakePair(encRequestData.errorCode, QByteArray()));
|
||||
promise->finish();
|
||||
return promise->future();
|
||||
}
|
||||
|
||||
QNetworkReply *reply = amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody);
|
||||
|
||||
auto sslErrors = QSharedPointer<QList<QSslError>>::create();
|
||||
|
||||
connect(reply, &QNetworkReply::sslErrors, [sslErrors](const QList<QSslError> &errors) { *sslErrors = errors; });
|
||||
|
||||
connect(reply, &QNetworkReply::finished, reply, [promise, sslErrors, encRequestData, endpoint, apiPayload, reply, this]() mutable {
|
||||
QByteArray encryptedResponseBody = reply->readAll();
|
||||
QString replyErrorString = reply->errorString();
|
||||
auto replyError = reply->error();
|
||||
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
auto decryptionResult =
|
||||
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
|
||||
|
||||
auto processResponse = [promise, encRequestData](const GatewayController::DecryptionResult &decryptionResult,
|
||||
const QList<QSslError> &sslErrors, QNetworkReply::NetworkError replyError,
|
||||
const QString &replyErrorString, int httpStatusCode) {
|
||||
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode,
|
||||
decryptionResult.decryptedBody);
|
||||
if (errorCode) {
|
||||
promise->addResult(qMakePair(errorCode, QByteArray()));
|
||||
promise->finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!decryptionResult.isDecryptionSuccessful) {
|
||||
Utils::logException();
|
||||
qCritical() << "error when decrypting the request body";
|
||||
promise->addResult(qMakePair(ErrorCode::ApiConfigDecryptionError, QByteArray()));
|
||||
promise->finish();
|
||||
return;
|
||||
}
|
||||
|
||||
promise->addResult(qMakePair(ErrorCode::NoError, decryptionResult.decryptedBody));
|
||||
promise->finish();
|
||||
};
|
||||
|
||||
if (sslErrors->isEmpty() && shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
|
||||
auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString("");
|
||||
auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString("");
|
||||
|
||||
QStringList primaryBaseUrls;
|
||||
QStringList fallbackBaseUrls;
|
||||
if (m_isDevEnvironment) {
|
||||
primaryBaseUrls = QString(DEV_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
} else {
|
||||
primaryBaseUrls = QString(PROD_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
fallbackBaseUrls = QString(FALLBACK_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
}
|
||||
std::random_device randomDevice;
|
||||
std::mt19937 generator(randomDevice());
|
||||
std::shuffle(primaryBaseUrls.begin(), primaryBaseUrls.end(), generator);
|
||||
std::shuffle(fallbackBaseUrls.begin(), fallbackBaseUrls.end(), generator);
|
||||
|
||||
auto appendStorageUrls = [&serviceType, &userCountryCode](const QStringList &baseUrls, QStringList &target) {
|
||||
if (!serviceType.isEmpty()) {
|
||||
for (const auto &baseUrl : baseUrls) {
|
||||
QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8();
|
||||
target.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) + ".json");
|
||||
}
|
||||
}
|
||||
for (const auto &baseUrl : baseUrls) {
|
||||
target.push_back(baseUrl + "endpoints.json");
|
||||
}
|
||||
};
|
||||
|
||||
QStringList proxyStorageUrls;
|
||||
appendStorageUrls(primaryBaseUrls, proxyStorageUrls);
|
||||
appendStorageUrls(fallbackBaseUrls, proxyStorageUrls);
|
||||
|
||||
getProxyUrlsAsync(proxyStorageUrls, 0, [this, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) {
|
||||
getProxyUrlAsync(proxyUrls, 0, [this, encRequestData, endpoint, processResponse](const QString &proxyUrl) {
|
||||
bypassProxyAsync(endpoint, proxyUrl, encRequestData,
|
||||
[processResponse, this](const QByteArray &decryptedBody, bool isDecryptionSuccessful,
|
||||
const QList<QSslError> &sslErrors, QNetworkReply::NetworkError replyError,
|
||||
const QString &replyErrorString, int httpStatusCode) {
|
||||
GatewayController::DecryptionResult result;
|
||||
result.decryptedBody = decryptedBody;
|
||||
result.isDecryptionSuccessful = isDecryptionSuccessful;
|
||||
processResponse(result, sslErrors, replyError, replyErrorString, httpStatusCode);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
} else {
|
||||
processResponse(decryptionResult, *sslErrors, replyError, replyErrorString, httpStatusCode);
|
||||
}
|
||||
});
|
||||
|
||||
return promise->future();
|
||||
}
|
||||
|
||||
QStringList GatewayController::getProxyUrls(const QString &serviceType, const QString &userCountryCode)
|
||||
{
|
||||
QNetworkRequest request;
|
||||
request.setTransferTimeout(proxyStorageRequestTimeoutMsecs);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
|
||||
QEventLoop wait;
|
||||
QList<QSslError> sslErrors;
|
||||
QNetworkReply *reply;
|
||||
|
||||
QStringList primaryBaseUrls;
|
||||
QStringList fallbackBaseUrls;
|
||||
if (m_isDevEnvironment) {
|
||||
primaryBaseUrls = QString(DEV_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
} else {
|
||||
primaryBaseUrls = QString(PROD_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
fallbackBaseUrls = QString(FALLBACK_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
}
|
||||
|
||||
std::random_device randomDevice;
|
||||
std::mt19937 generator(randomDevice());
|
||||
std::shuffle(primaryBaseUrls.begin(), primaryBaseUrls.end(), generator);
|
||||
std::shuffle(fallbackBaseUrls.begin(), fallbackBaseUrls.end(), generator);
|
||||
|
||||
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||
|
||||
auto appendStorageUrls = [&serviceType, &userCountryCode](const QStringList &baseUrls, QStringList &target) {
|
||||
if (!serviceType.isEmpty()) {
|
||||
for (const auto &baseUrl : baseUrls) {
|
||||
QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8();
|
||||
target.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) + ".json");
|
||||
}
|
||||
}
|
||||
for (const auto &baseUrl : baseUrls) {
|
||||
target.push_back(baseUrl + "endpoints.json");
|
||||
}
|
||||
};
|
||||
|
||||
QStringList proxyStorageUrls;
|
||||
appendStorageUrls(primaryBaseUrls, proxyStorageUrls);
|
||||
appendStorageUrls(fallbackBaseUrls, proxyStorageUrls);
|
||||
|
||||
if (proxyStorageUrls.empty()) {
|
||||
qDebug() << "empty storage endpoint list";
|
||||
return {};
|
||||
}
|
||||
|
||||
for (const auto &proxyStorageUrl : proxyStorageUrls) {
|
||||
request.setUrl(proxyStorageUrl);
|
||||
reply = amnApp->networkManager()->get(request);
|
||||
|
||||
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
|
||||
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
|
||||
if (reply->error() == QNetworkReply::NetworkError::NoError) {
|
||||
auto encryptedResponseBody = reply->readAll();
|
||||
reply->deleteLater();
|
||||
|
||||
EVP_PKEY *privateKey = nullptr;
|
||||
QByteArray responseBody;
|
||||
try {
|
||||
if (!m_isDevEnvironment) {
|
||||
QCryptographicHash hash(QCryptographicHash::Sha512);
|
||||
hash.addData(key);
|
||||
QByteArray hashResult = hash.result().toHex();
|
||||
|
||||
QByteArray key = QByteArray::fromHex(hashResult.left(64));
|
||||
QByteArray iv = QByteArray::fromHex(hashResult.mid(64, 32));
|
||||
|
||||
QByteArray ba = QByteArray::fromBase64(encryptedResponseBody);
|
||||
|
||||
QSimpleCrypto::QBlockCipher blockCipher;
|
||||
responseBody = blockCipher.decryptAesBlockCipher(ba, key, iv);
|
||||
} else {
|
||||
responseBody = encryptedResponseBody;
|
||||
}
|
||||
} catch (...) {
|
||||
Utils::logException();
|
||||
qCritical() << "error loading private key from environment variables or decrypting payload" << encryptedResponseBody;
|
||||
continue;
|
||||
}
|
||||
|
||||
auto endpointsArray = QJsonDocument::fromJson(responseBody).array();
|
||||
|
||||
QStringList endpoints;
|
||||
for (const auto &endpoint : endpointsArray) {
|
||||
endpoints.push_back(endpoint.toString());
|
||||
}
|
||||
return endpoints;
|
||||
} else {
|
||||
auto replyError = reply->error();
|
||||
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
qDebug() << replyError;
|
||||
qDebug() << httpStatusCode;
|
||||
qDebug() << "go to the next storage endpoint";
|
||||
|
||||
reply->deleteLater();
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody,
|
||||
bool isDecryptionSuccessful)
|
||||
{
|
||||
const QByteArray &responseBody = decryptedResponseBody;
|
||||
|
||||
int apiHttpStatus = -1;
|
||||
QString apiErrorMessage;
|
||||
if (isDecryptionSuccessful) {
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
|
||||
if (jsonDoc.isObject()) {
|
||||
QJsonObject jsonObj = jsonDoc.object();
|
||||
apiHttpStatus = jsonObj.value("http_status").toInt(-1);
|
||||
apiErrorMessage = jsonObj.value(QStringLiteral("message")).toString().trimmed();
|
||||
}
|
||||
} else {
|
||||
qDebug() << "failed to decrypt the data";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (replyError == QNetworkReply::NetworkError::OperationCanceledError || replyError == QNetworkReply::NetworkError::TimeoutError) {
|
||||
qDebug() << "timeout occurred";
|
||||
qDebug() << replyError;
|
||||
return true;
|
||||
}
|
||||
if (responseBody.contains("html")) {
|
||||
qDebug() << "the response contains an html tag";
|
||||
return true;
|
||||
}
|
||||
if (apiHttpStatus == httpStatusCodeNotFound) {
|
||||
if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2)
|
||||
|| responseBody.contains(errorResponsePattern3)) {
|
||||
return false;
|
||||
} else {
|
||||
qDebug() << replyError;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (apiHttpStatus == httpStatusCodeNotImplemented) {
|
||||
if (responseBody.contains(updateRequestResponsePattern)) {
|
||||
return false;
|
||||
} else {
|
||||
qDebug() << replyError;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (apiHttpStatus == httpStatusCodeConflict) {
|
||||
return false;
|
||||
}
|
||||
if (apiHttpStatus == httpStatusCodePaymentRequired) {
|
||||
return false;
|
||||
}
|
||||
if (apiHttpStatus == httpStatusCodeUnprocessableEntity) {
|
||||
return apiErrorMessage != unprocessableSubscriptionMessage;
|
||||
}
|
||||
if (replyError != QNetworkReply::NetworkError::NoError) {
|
||||
qDebug() << replyError;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void GatewayController::bypassProxy(const QString &endpoint, const QString &serviceType, const QString &userCountryCode,
|
||||
std::function<QNetworkReply *(const QString &url)> requestFunction,
|
||||
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction)
|
||||
{
|
||||
QStringList proxyUrls = getProxyUrls(serviceType, userCountryCode);
|
||||
std::random_device randomDevice;
|
||||
std::mt19937 generator(randomDevice());
|
||||
std::shuffle(proxyUrls.begin(), proxyUrls.end(), generator);
|
||||
|
||||
QByteArray responseBody;
|
||||
|
||||
auto bypassFunction = [this](const QString &endpoint, const QString &proxyUrl,
|
||||
std::function<QNetworkReply *(const QString &url)> requestFunction,
|
||||
std::function<bool(QNetworkReply * reply, const QList<QSslError> &sslErrors)> replyProcessingFunction) {
|
||||
QEventLoop wait;
|
||||
QList<QSslError> sslErrors;
|
||||
|
||||
qDebug() << "go to the next proxy endpoint";
|
||||
QNetworkReply *reply = requestFunction(endpoint.arg(proxyUrl));
|
||||
|
||||
QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
|
||||
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
|
||||
auto result = replyProcessingFunction(reply, sslErrors);
|
||||
reply->deleteLater();
|
||||
return result;
|
||||
};
|
||||
|
||||
if (m_proxyUrl.isEmpty()) {
|
||||
QNetworkRequest request;
|
||||
request.setTransferTimeout(1000);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
|
||||
QEventLoop wait;
|
||||
QList<QSslError> sslErrors;
|
||||
QNetworkReply *reply;
|
||||
|
||||
for (const QString &proxyUrl : proxyUrls) {
|
||||
request.setUrl(proxyUrl + "lmbd-health");
|
||||
reply = amnApp->networkManager()->get(request);
|
||||
|
||||
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
|
||||
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
|
||||
if (reply->error() == QNetworkReply::NetworkError::NoError) {
|
||||
reply->deleteLater();
|
||||
|
||||
m_proxyUrl = proxyUrl;
|
||||
if (!m_proxyUrl.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
reply->deleteLater();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_proxyUrl.isEmpty()) {
|
||||
if (bypassFunction(endpoint, m_proxyUrl, requestFunction, replyProcessingFunction)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (const QString &proxyUrl : proxyUrls) {
|
||||
if (bypassFunction(endpoint, proxyUrl, requestFunction, replyProcessingFunction)) {
|
||||
m_proxyUrl = proxyUrl;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
|
||||
std::function<void(const QStringList &)> onComplete)
|
||||
{
|
||||
if (currentProxyStorageIndex >= proxyStorageUrls.size()) {
|
||||
onComplete({});
|
||||
if (m_client == 0 || m_appSettingsRepository == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
QNetworkRequest request;
|
||||
request.setTransferTimeout(proxyStorageRequestTimeoutMsecs);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
request.setUrl(proxyStorageUrls[currentProxyStorageIndex]);
|
||||
|
||||
QNetworkReply *reply = amnApp->networkManager()->get(request);
|
||||
|
||||
// connect(reply, &QNetworkReply::sslErrors, this, [state](const QList<QSslError> &e) { *(state->sslErrors) = e; });
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [this, proxyStorageUrls, currentProxyStorageIndex, onComplete, reply]() {
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
QByteArray encrypted = reply->readAll();
|
||||
reply->deleteLater();
|
||||
|
||||
QByteArray responseBody;
|
||||
try {
|
||||
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||
if (!m_isDevEnvironment) {
|
||||
QCryptographicHash hash(QCryptographicHash::Sha512);
|
||||
hash.addData(key);
|
||||
QByteArray h = hash.result().toHex();
|
||||
|
||||
QByteArray decKey = QByteArray::fromHex(h.left(64));
|
||||
QByteArray iv = QByteArray::fromHex(h.mid(64, 32));
|
||||
QByteArray ba = QByteArray::fromBase64(encrypted);
|
||||
|
||||
QSimpleCrypto::QBlockCipher cipher;
|
||||
responseBody = cipher.decryptAesBlockCipher(ba, decKey, iv);
|
||||
} else {
|
||||
responseBody = encrypted;
|
||||
}
|
||||
} catch (...) {
|
||||
Utils::logException();
|
||||
qCritical() << "error decrypting payload";
|
||||
QMetaObject::invokeMethod(
|
||||
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonArray endpointsArray = QJsonDocument::fromJson(responseBody).array();
|
||||
QStringList endpoints;
|
||||
for (const QJsonValue &endpoint : endpointsArray)
|
||||
endpoints.push_back(endpoint.toString());
|
||||
|
||||
QStringList shuffled = endpoints;
|
||||
std::random_device randomDevice;
|
||||
std::mt19937 generator(randomDevice());
|
||||
std::shuffle(shuffled.begin(), shuffled.end(), generator);
|
||||
|
||||
onComplete(shuffled);
|
||||
return;
|
||||
}
|
||||
|
||||
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
qDebug() << httpStatusCode;
|
||||
qDebug() << "go to the next storage endpoint";
|
||||
reply->deleteLater();
|
||||
QMetaObject::invokeMethod(
|
||||
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection);
|
||||
});
|
||||
}
|
||||
|
||||
void GatewayController::getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex,
|
||||
std::function<void(const QString &)> onComplete)
|
||||
{
|
||||
if (currentProxyIndex >= proxyUrls.size()) {
|
||||
onComplete("");
|
||||
char *state = agw_export_state(m_client);
|
||||
if (state == nullptr) {
|
||||
return;
|
||||
}
|
||||
const QByteArray blob(state);
|
||||
agw_string_free(state);
|
||||
|
||||
QNetworkRequest request;
|
||||
request.setTransferTimeout(1000);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
request.setUrl(proxyUrls[currentProxyIndex] + "lmbd-health");
|
||||
|
||||
QNetworkReply *reply = amnApp->networkManager()->get(request);
|
||||
|
||||
// connect(reply, &QNetworkReply::sslErrors, this, [state](const QList<QSslError> &e) {
|
||||
// *(state->sslErrors) = e;
|
||||
// });
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [this, proxyUrls, currentProxyIndex, onComplete, reply]() {
|
||||
reply->deleteLater();
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
m_proxyUrl = proxyUrls[currentProxyIndex];
|
||||
onComplete(m_proxyUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "go to the next proxy endpoint";
|
||||
QMetaObject::invokeMethod(this, [=]() { getProxyUrlAsync(proxyUrls, currentProxyIndex + 1, onComplete); }, Qt::QueuedConnection);
|
||||
});
|
||||
}
|
||||
|
||||
void GatewayController::bypassProxyAsync(
|
||||
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
|
||||
std::function<void(const QByteArray &, bool, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> onComplete)
|
||||
{
|
||||
auto sslErrors = QSharedPointer<QList<QSslError>>::create();
|
||||
if (proxyUrl.isEmpty()) {
|
||||
onComplete(QByteArray(), false, *sslErrors, QNetworkReply::InternalServerError, "empty proxy url", 0);
|
||||
return;
|
||||
if (blob != m_lastPersistedState) {
|
||||
m_appSettingsRepository->writeGatewayProxyUrls(agwStateCacheKey, blob);
|
||||
m_lastPersistedState = blob;
|
||||
}
|
||||
|
||||
QNetworkRequest request = encRequestData.request;
|
||||
request.setUrl(endpoint.arg(proxyUrl));
|
||||
|
||||
QNetworkReply *reply = amnApp->networkManager()->post(request, encRequestData.requestBody);
|
||||
|
||||
connect(reply, &QNetworkReply::sslErrors, this, [sslErrors](const QList<QSslError> &errors) { *sslErrors = errors; });
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [sslErrors, onComplete, encRequestData, reply, this]() {
|
||||
QByteArray encryptedResponseBody = reply->readAll();
|
||||
QString replyErrorString = reply->errorString();
|
||||
auto replyError = reply->error();
|
||||
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
auto decryptionResult =
|
||||
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
|
||||
|
||||
onComplete(decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful, *sslErrors, replyError, replyErrorString,
|
||||
httpStatusCode);
|
||||
});
|
||||
}
|
||||
@@ -1,20 +1,19 @@
|
||||
#ifndef GATEWAYCONTROLLER_H
|
||||
#define GATEWAYCONTROLLER_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QFuture>
|
||||
#include <QNetworkReply>
|
||||
#include <QJsonObject>
|
||||
#include <QObject>
|
||||
#include <QPair>
|
||||
#include <QPromise>
|
||||
#include <QSharedPointer>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
#include "core/utils/errorCodes.h"
|
||||
#include "core/utils/routeModes.h"
|
||||
#include "core/utils/commonStructs.h"
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#include "platforms/ios/ios_controller.h"
|
||||
#endif
|
||||
#include "agw.h"
|
||||
|
||||
class SecureAppSettingsRepository;
|
||||
|
||||
class GatewayController : public QObject
|
||||
{
|
||||
@@ -22,51 +21,28 @@ class GatewayController : public QObject
|
||||
|
||||
public:
|
||||
explicit GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
|
||||
const bool isStrictKillSwitchEnabled, QObject *parent = nullptr);
|
||||
const bool isStrictKillSwitchEnabled, SecureAppSettingsRepository *appSettingsRepository,
|
||||
QObject *parent = nullptr);
|
||||
~GatewayController() override;
|
||||
|
||||
amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody);
|
||||
QFuture<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject apiPayload);
|
||||
|
||||
private:
|
||||
struct EncryptedRequestData
|
||||
{
|
||||
QNetworkRequest request;
|
||||
QByteArray requestBody;
|
||||
QByteArray key;
|
||||
QByteArray iv;
|
||||
QByteArray salt;
|
||||
amnezia::ErrorCode errorCode;
|
||||
};
|
||||
void prepareRequest();
|
||||
QStringList requestHosts() const;
|
||||
|
||||
struct DecryptionResult
|
||||
{
|
||||
QByteArray decryptedBody;
|
||||
bool isDecryptionSuccessful;
|
||||
};
|
||||
QPair<amnezia::ErrorCode, QByteArray> executePost(const QString &endpoint, const QJsonObject &apiPayload);
|
||||
static amnezia::ErrorCode mapResultCode(const int code, const QByteArray &responseBody);
|
||||
void persistState();
|
||||
|
||||
EncryptedRequestData prepareRequest(const QString &endpoint, const QJsonObject &apiPayload);
|
||||
DecryptionResult tryDecryptResponseBody(const QByteArray &encryptedResponseBody, QNetworkReply::NetworkError replyError,
|
||||
const QByteArray &key, const QByteArray &iv, const QByteArray &salt);
|
||||
|
||||
QStringList getProxyUrls(const QString &serviceType, const QString &userCountryCode);
|
||||
bool shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody, bool isDecryptionSuccessful);
|
||||
void bypassProxy(const QString &endpoint, const QString &serviceType, const QString &userCountryCode,
|
||||
std::function<QNetworkReply *(const QString &url)> requestFunction,
|
||||
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction);
|
||||
|
||||
void getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
|
||||
std::function<void(const QStringList &)> onComplete);
|
||||
void getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex, std::function<void(const QString &)> onComplete);
|
||||
void bypassProxyAsync(
|
||||
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
|
||||
std::function<void(const QByteArray &, bool, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> onComplete);
|
||||
|
||||
int m_requestTimeoutMsecs;
|
||||
QString m_gatewayEndpoint;
|
||||
bool m_isDevEnvironment = false;
|
||||
bool m_isStrictKillSwitchEnabled = false;
|
||||
SecureAppSettingsRepository *m_appSettingsRepository = nullptr;
|
||||
|
||||
inline static QString m_proxyUrl;
|
||||
agw_client_handle m_client = 0;
|
||||
bool m_publicKeyMissing = false;
|
||||
QByteArray m_lastPersistedState;
|
||||
};
|
||||
|
||||
#endif // GATEWAYCONTROLLER_H
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "ipSplitTunnelingController.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
#include <QJsonObject>
|
||||
#include <QDebug>
|
||||
|
||||
IpSplitTunnelingController::IpSplitTunnelingController(SecureAppSettingsRepository* appSettingsRepository, QObject* parent)
|
||||
: QObject(parent),
|
||||
@@ -14,47 +15,56 @@ IpSplitTunnelingController::IpSplitTunnelingController(SecureAppSettingsReposito
|
||||
fillSites();
|
||||
}
|
||||
|
||||
bool IpSplitTunnelingController::addSiteInternal(const QString &hostname, const QString &ip)
|
||||
bool IpSplitTunnelingController::addSiteInternal(const QString &hostname, const QStringList &ips)
|
||||
{
|
||||
QVariantMap existing = m_appSettingsRepository->vpnSites(m_currentRouteMode);
|
||||
if (existing.contains(hostname) && ip.isEmpty()) {
|
||||
if (existing.contains(hostname) && ips.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < m_sites.size(); i++) {
|
||||
if (m_sites[i].first == hostname && (m_sites[i].second.isEmpty() && !ip.isEmpty())) {
|
||||
m_sites[i].second = ip;
|
||||
m_appSettingsRepository->addVpnSite(m_currentRouteMode, hostname, ip);
|
||||
if (m_sites[i].first == hostname) {
|
||||
bool changed = false;
|
||||
for (const QString &ip : ips) {
|
||||
if (!ip.isEmpty() && !m_sites[i].second.contains(ip)) {
|
||||
m_sites[i].second.append(ip);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (!changed) {
|
||||
return false;
|
||||
}
|
||||
m_appSettingsRepository->addVpnSite(m_currentRouteMode, hostname, ips);
|
||||
return true;
|
||||
} else if (m_sites[i].first == hostname && (m_sites[i].second == ip)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
m_sites.append(qMakePair(hostname, ip));
|
||||
m_appSettingsRepository->addVpnSite(m_currentRouteMode, hostname, ip);
|
||||
m_sites.append(qMakePair(hostname, ips));
|
||||
m_appSettingsRepository->addVpnSite(m_currentRouteMode, hostname, ips);
|
||||
return true;
|
||||
}
|
||||
|
||||
void IpSplitTunnelingController::addSites(const QMap<QString, QString> &sites, bool replaceExisting)
|
||||
void IpSplitTunnelingController::addSites(const QMap<QString, QStringList> &sites, bool replaceExisting)
|
||||
{
|
||||
if (replaceExisting) {
|
||||
m_sites.clear();
|
||||
}
|
||||
for (auto it = sites.constBegin(); it != sites.constEnd(); ++it) {
|
||||
const QString &hostname = it.key();
|
||||
const QString &ip = it.value();
|
||||
const QStringList &ips = it.value();
|
||||
bool found = false;
|
||||
for (int i = 0; i < m_sites.size(); i++) {
|
||||
if (m_sites[i].first == hostname) {
|
||||
if (!ip.isEmpty()) {
|
||||
m_sites[i].second = ip;
|
||||
for (const QString &ip : ips) {
|
||||
if (!ip.isEmpty() && !m_sites[i].second.contains(ip)) {
|
||||
m_sites[i].second.append(ip);
|
||||
}
|
||||
}
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
m_sites.append(qMakePair(hostname, ip));
|
||||
m_sites.append(qMakePair(hostname, ips));
|
||||
}
|
||||
}
|
||||
if (replaceExisting) {
|
||||
@@ -72,11 +82,11 @@ bool IpSplitTunnelingController::addSite(const QString &hostname)
|
||||
}
|
||||
|
||||
if (NetworkUtilities::ipAddressWithSubnetRegExp().exactMatch(normalizedHostname)) {
|
||||
processSite(normalizedHostname, "");
|
||||
processSite(normalizedHostname, {});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (addSiteInternal(normalizedHostname, "")) {
|
||||
if (addSiteInternal(normalizedHostname, {})) {
|
||||
QHostInfo::lookupHost(normalizedHostname, this, SLOT(onHostResolved(QHostInfo)));
|
||||
return true;
|
||||
}
|
||||
@@ -124,7 +134,7 @@ bool IpSplitTunnelingController::isSplitTunnelingEnabled() const
|
||||
return m_appSettingsRepository->isSitesSplitTunnelingEnabled();
|
||||
}
|
||||
|
||||
QVector<QPair<QString, QString>> IpSplitTunnelingController::getCurrentSites() const
|
||||
QVector<QPair<QString, QStringList>> IpSplitTunnelingController::getCurrentSites() const
|
||||
{
|
||||
return m_sites;
|
||||
}
|
||||
@@ -134,7 +144,7 @@ void IpSplitTunnelingController::fillSites()
|
||||
QVariantMap sitesMap = m_appSettingsRepository->vpnSites(m_currentRouteMode);
|
||||
m_sites.clear();
|
||||
for (auto it = sitesMap.begin(); it != sitesMap.end(); ++it) {
|
||||
m_sites.append(qMakePair(it.key(), it.value().toString()));
|
||||
m_sites.append(qMakePair(it.key(), SecureAppSettingsRepository::siteIpList(it.value())));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,8 +154,13 @@ QString IpSplitTunnelingController::normalizeHostname(const QString &hostname) c
|
||||
normalized.replace("https://", "");
|
||||
normalized.replace("http://", "");
|
||||
normalized.replace("ftp://", "");
|
||||
normalized = normalized.split("/", Qt::SkipEmptyParts).first();
|
||||
return normalized;
|
||||
|
||||
if (NetworkUtilities::ipAddressWithSubnetRegExp().exactMatch(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
const QStringList parts = normalized.split("/", Qt::SkipEmptyParts);
|
||||
return parts.isEmpty() ? QString() : parts.first();
|
||||
}
|
||||
|
||||
bool IpSplitTunnelingController::validateHostname(const QString &hostname) const
|
||||
@@ -164,29 +179,40 @@ void IpSplitTunnelingController::onHostResolved(const QHostInfo &hostInfo)
|
||||
{
|
||||
const QList<QHostAddress> &addresses = hostInfo.addresses();
|
||||
QString hostname = hostInfo.hostName();
|
||||
|
||||
|
||||
QStringList allIpv4;
|
||||
for (const QHostAddress &addr : addresses) {
|
||||
if (addr.protocol() == QAbstractSocket::NetworkLayerProtocol::IPv4Protocol) {
|
||||
processSiteAfterResolve(hostname, addr.toString());
|
||||
break;
|
||||
allIpv4.append(addr.toString());
|
||||
}
|
||||
}
|
||||
allIpv4.removeDuplicates();
|
||||
qDebug() << "[SplitTunneling] Host resolved:" << hostname
|
||||
<< "-> adding all IPv4 addresses to list:" << allIpv4;
|
||||
|
||||
if (!allIpv4.isEmpty()) {
|
||||
processSiteAfterResolve(hostname, allIpv4);
|
||||
}
|
||||
}
|
||||
|
||||
void IpSplitTunnelingController::processSiteAfterResolve(const QString &hostname, const QString &ip)
|
||||
void IpSplitTunnelingController::processSiteAfterResolve(const QString &hostname, const QStringList &ips)
|
||||
{
|
||||
for (int i = 0; i < m_sites.size(); i++) {
|
||||
if (m_sites[i].first == hostname && m_sites[i].second.isEmpty()) {
|
||||
m_sites[i].second = ip;
|
||||
m_appSettingsRepository->addVpnSite(m_currentRouteMode, hostname, ip);
|
||||
if (m_sites[i].first == hostname) {
|
||||
for (const QString &ip : ips) {
|
||||
if (!ip.isEmpty() && !m_sites[i].second.contains(ip)) {
|
||||
m_sites[i].second.append(ip);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
m_appSettingsRepository->addVpnSite(m_currentRouteMode, hostname, ips);
|
||||
}
|
||||
|
||||
void IpSplitTunnelingController::processSite(const QString &hostname, const QString &ip)
|
||||
void IpSplitTunnelingController::processSite(const QString &hostname, const QStringList &ips)
|
||||
{
|
||||
addSiteInternal(hostname, ip);
|
||||
addSiteInternal(hostname, ips);
|
||||
}
|
||||
|
||||
bool IpSplitTunnelingController::importSitesFromJson(const QByteArray& jsonData, bool replaceExisting, QString &errorMessage)
|
||||
@@ -205,12 +231,25 @@ bool IpSplitTunnelingController::importSitesFromJson(const QByteArray& jsonData,
|
||||
}
|
||||
|
||||
QJsonArray jsonArray = jsonDocument.array();
|
||||
QMap<QString, QString> sites;
|
||||
QMap<QString, QStringList> sites;
|
||||
|
||||
for (auto jsonValue : jsonArray) {
|
||||
QJsonObject jsonObject = jsonValue.toObject();
|
||||
QString hostname = jsonObject.value("hostname").toString("");
|
||||
QString ip = jsonObject.value("ip").toString("");
|
||||
|
||||
QStringList ips;
|
||||
if (jsonObject.value("ips").isArray()) {
|
||||
const QJsonArray ipsArray = jsonObject.value("ips").toArray();
|
||||
for (const auto &ipValue : ipsArray) {
|
||||
ips.append(ipValue.toString());
|
||||
}
|
||||
}
|
||||
const QString singleIp = jsonObject.value("ip").toString("");
|
||||
if (!singleIp.isEmpty()) {
|
||||
ips.append(singleIp);
|
||||
}
|
||||
ips.removeAll(QString());
|
||||
ips.removeDuplicates();
|
||||
|
||||
QString normalizedHostname = normalizeHostname(hostname);
|
||||
|
||||
@@ -219,7 +258,7 @@ bool IpSplitTunnelingController::importSitesFromJson(const QByteArray& jsonData,
|
||||
continue;
|
||||
}
|
||||
|
||||
sites.insert(normalizedHostname, ip);
|
||||
sites.insert(normalizedHostname, ips);
|
||||
}
|
||||
|
||||
addSites(sites, replaceExisting);
|
||||
@@ -229,13 +268,21 @@ bool IpSplitTunnelingController::importSitesFromJson(const QByteArray& jsonData,
|
||||
|
||||
QByteArray IpSplitTunnelingController::exportSitesToJson() const
|
||||
{
|
||||
QVector<QPair<QString, QString>> sites = getCurrentSites();
|
||||
QVector<QPair<QString, QStringList>> sites = getCurrentSites();
|
||||
QJsonArray jsonArray;
|
||||
|
||||
for (const auto &site : sites) {
|
||||
QJsonObject jsonObject;
|
||||
jsonObject["hostname"] = site.first;
|
||||
jsonObject["ip"] = site.second;
|
||||
|
||||
QJsonArray ipsArray;
|
||||
for (const QString &ip : site.second) {
|
||||
ipsArray.append(ip);
|
||||
}
|
||||
jsonObject["ips"] = ipsArray;
|
||||
// Keep the legacy "ip" field (first address) for backward compatibility.
|
||||
jsonObject["ip"] = site.second.isEmpty() ? QString() : site.second.first();
|
||||
|
||||
jsonArray.append(jsonObject);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ public:
|
||||
explicit IpSplitTunnelingController(SecureAppSettingsRepository* appSettingsRepository, QObject* parent = nullptr);
|
||||
|
||||
bool addSite(const QString &hostname);
|
||||
void addSites(const QMap<QString, QString> &sites, bool replaceExisting);
|
||||
void addSites(const QMap<QString, QStringList> &sites, bool replaceExisting);
|
||||
bool removeSite(const QString &hostname);
|
||||
void removeSites();
|
||||
void setRouteMode(RouteMode routeMode);
|
||||
@@ -33,7 +33,7 @@ public:
|
||||
|
||||
RouteMode getRouteMode() const;
|
||||
bool isSplitTunnelingEnabled() const;
|
||||
QVector<QPair<QString, QString>> getCurrentSites() const;
|
||||
QVector<QPair<QString, QStringList>> getCurrentSites() const;
|
||||
|
||||
bool importSitesFromJson(const QByteArray& jsonData, bool replaceExisting, QString &errorMessage);
|
||||
QByteArray exportSitesToJson() const;
|
||||
@@ -43,15 +43,15 @@ private slots:
|
||||
|
||||
private:
|
||||
void fillSites();
|
||||
bool addSiteInternal(const QString &hostname, const QString &ip);
|
||||
bool addSiteInternal(const QString &hostname, const QStringList &ips);
|
||||
QString normalizeHostname(const QString &hostname) const;
|
||||
bool validateHostname(const QString &hostname) const;
|
||||
void processSiteAfterResolve(const QString &hostname, const QString &ip);
|
||||
void processSite(const QString &hostname, const QString &ip);
|
||||
void processSiteAfterResolve(const QString &hostname, const QStringList &ips);
|
||||
void processSite(const QString &hostname, const QStringList &ips);
|
||||
|
||||
SecureAppSettingsRepository* m_appSettingsRepository;
|
||||
RouteMode m_currentRouteMode;
|
||||
QVector<QPair<QString, QString>> m_sites;
|
||||
QVector<QPair<QString, QStringList>> m_sites;
|
||||
};
|
||||
|
||||
#endif // IPSPLITTUNNELINGCONTROLLER_H
|
||||
|
||||
@@ -0,0 +1,525 @@
|
||||
#include "localProxyController.h"
|
||||
|
||||
#include "core/controllers/api/subscriptionController.h"
|
||||
#include "core/repositories/secureAppSettingsRepository.h"
|
||||
#include "core/repositories/secureServersRepository.h"
|
||||
#include "core/utils/api/apiUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/localProxy/localProxyConstants.h"
|
||||
#include "core/utils/localProxy/portUtils.h"
|
||||
#include "core/utils/localProxy/xrayConfigUtils.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonParseError>
|
||||
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
#include "core/utils/ipcClient.h"
|
||||
|
||||
#include <QHostAddress>
|
||||
#include <QHttpServer>
|
||||
#include <QHttpServerResponse>
|
||||
#include <QTcpServer>
|
||||
#endif
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
LocalProxyController::LocalProxyController(SecureServersRepository *serversRepository,
|
||||
SecureAppSettingsRepository *appSettingsRepository,
|
||||
SubscriptionController *subscriptionController, QObject *parent)
|
||||
: QObject(parent),
|
||||
m_serversRepository(serversRepository),
|
||||
m_appSettingsRepository(appSettingsRepository),
|
||||
m_subscriptionController(subscriptionController)
|
||||
{
|
||||
connect(m_serversRepository, &SecureServersRepository::serverEdited, this, &LocalProxyController::onServerEdited);
|
||||
connect(m_serversRepository, &SecureServersRepository::serverRemoved, this,
|
||||
[this](const QString &serverId, int) { onServerRemoved(serverId); });
|
||||
}
|
||||
|
||||
LocalProxyController::~LocalProxyController()
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
bool LocalProxyController::isEnabled() const
|
||||
{
|
||||
return m_isEnabled;
|
||||
}
|
||||
|
||||
int LocalProxyController::port() const
|
||||
{
|
||||
return m_appSettingsRepository->localProxyPort();
|
||||
}
|
||||
|
||||
QString LocalProxyController::ownerId() const
|
||||
{
|
||||
return m_appSettingsRepository->localProxyOwnerId();
|
||||
}
|
||||
|
||||
bool LocalProxyController::isPortUserDefined() const
|
||||
{
|
||||
return m_appSettingsRepository->isLocalProxyPortUserDefined();
|
||||
}
|
||||
|
||||
int LocalProxyController::activePort() const
|
||||
{
|
||||
return m_activePort;
|
||||
}
|
||||
|
||||
bool LocalProxyController::setPort(int port)
|
||||
{
|
||||
if (port < localProxy::proxyPortMin || port > localProxy::proxyPortMax) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool isPortChanged = m_appSettingsRepository->localProxyPort() != static_cast<quint16>(port);
|
||||
if (isPortChanged) {
|
||||
m_appSettingsRepository->setLocalProxyPort(static_cast<quint16>(port));
|
||||
}
|
||||
m_appSettingsRepository->setLocalProxyPortUserDefined(true);
|
||||
emit stateChanged();
|
||||
|
||||
if (isPortChanged && m_isEnabled) {
|
||||
start();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LocalProxyController::isPortBusy(int port) const
|
||||
{
|
||||
return !portUtils::isPortAvailable(port);
|
||||
}
|
||||
|
||||
bool LocalProxyController::enable(const QString &ownerId, int port)
|
||||
{
|
||||
if (!isSupported() || ownerId.isEmpty() || port < localProxy::proxyPortMin || port > localProxy::proxyPortMax) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_isEnabled && m_appSettingsRepository->localProxyOwnerId() != ownerId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!portUtils::isPortAvailable(port) && m_appSettingsRepository->isLocalProxyPortUserDefined()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_appSettingsRepository->localProxyPort() != static_cast<quint16>(port)) {
|
||||
m_appSettingsRepository->setLocalProxyPort(static_cast<quint16>(port));
|
||||
}
|
||||
|
||||
m_appSettingsRepository->setLocalProxyOwnerId(ownerId);
|
||||
m_isEnabled = true;
|
||||
emit stateChanged();
|
||||
|
||||
start();
|
||||
return true;
|
||||
}
|
||||
|
||||
void LocalProxyController::disable()
|
||||
{
|
||||
if (!m_isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "Local proxy is disabled";
|
||||
m_isEnabled = false;
|
||||
stop();
|
||||
emit stateChanged();
|
||||
}
|
||||
|
||||
void LocalProxyController::start()
|
||||
{
|
||||
if (!startApi(localProxy::apiPort)) {
|
||||
disableWithError(tr("Local proxy failed to start. Check if the port is available"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!syncXray()) {
|
||||
disableWithError(tr("Couldn’t start the proxy due to an internal error. Try restarting the app"));
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "Local proxy is running on 127.0.0.1:" << m_apiPort;
|
||||
}
|
||||
|
||||
void LocalProxyController::onServerEdited(const QString &serverId)
|
||||
{
|
||||
if (m_apiPort == 0 || m_appSettingsRepository->localProxyOwnerId() != serverId) {
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "Owner server edited, restarting Xray";
|
||||
if (restartXray()) {
|
||||
m_appliedProxyPort = m_appSettingsRepository->localProxyPort();
|
||||
} else {
|
||||
disableWithError(tr("Couldn’t start the proxy due to an internal error. Try restarting the app"));
|
||||
}
|
||||
}
|
||||
|
||||
void LocalProxyController::onServerRemoved(const QString &serverId)
|
||||
{
|
||||
if (m_appSettingsRepository->localProxyOwnerId() != serverId) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_appSettingsRepository->setLocalProxyOwnerId(QString());
|
||||
if (m_isEnabled) {
|
||||
m_isEnabled = false;
|
||||
stop();
|
||||
}
|
||||
emit stateChanged();
|
||||
}
|
||||
|
||||
void LocalProxyController::onVpnConnectionRequested()
|
||||
{
|
||||
if (!m_isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
disable();
|
||||
emit stoppedByVpn(tr("Local proxy stopped because VPN was turned on"));
|
||||
}
|
||||
|
||||
void LocalProxyController::stop()
|
||||
{
|
||||
stopXray();
|
||||
stopApi();
|
||||
m_appliedProxyPort = 0;
|
||||
}
|
||||
|
||||
bool LocalProxyController::syncXray()
|
||||
{
|
||||
const quint16 proxyPort = m_appSettingsRepository->localProxyPort();
|
||||
|
||||
bool synced;
|
||||
if (!isXrayEngineRunning()) {
|
||||
synced = startXray();
|
||||
} else if (m_appliedProxyPort != proxyPort) {
|
||||
qDebug() << "Proxy port changed from" << m_appliedProxyPort << "to" << proxyPort;
|
||||
synced = restartXray();
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (synced) {
|
||||
m_appliedProxyPort = proxyPort;
|
||||
}
|
||||
return synced;
|
||||
}
|
||||
|
||||
bool LocalProxyController::startXray()
|
||||
{
|
||||
if (isXrayEngineRunning()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto config = buildXrayConfig();
|
||||
if (!config) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!startXrayEngine(config->serializedConfig)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
qDebug() << "Xray started on port" << config->proxyPort;
|
||||
setActivePort(config->proxyPort);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LocalProxyController::stopXray()
|
||||
{
|
||||
if (!stopXrayEngine()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setActivePort(0);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LocalProxyController::restartXray()
|
||||
{
|
||||
qDebug() << "Restarting Xray with updated config";
|
||||
|
||||
if (!stopXray()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return startXray();
|
||||
}
|
||||
|
||||
void LocalProxyController::disableWithError(const QString &message)
|
||||
{
|
||||
qWarning() << message;
|
||||
|
||||
m_isEnabled = false;
|
||||
stop();
|
||||
emit stateChanged();
|
||||
emit startFailed(message);
|
||||
}
|
||||
|
||||
void LocalProxyController::setActivePort(int port)
|
||||
{
|
||||
if (m_activePort == port) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_activePort = port;
|
||||
emit stateChanged();
|
||||
}
|
||||
|
||||
int LocalProxyController::runningProxyPort() const
|
||||
{
|
||||
return isXrayEngineRunning() ? m_activePort : 0;
|
||||
}
|
||||
|
||||
std::optional<LocalProxyController::XrayConfig> LocalProxyController::buildXrayConfig() const
|
||||
{
|
||||
const QString ownerId = m_appSettingsRepository->localProxyOwnerId();
|
||||
if (ownerId.isEmpty()) {
|
||||
qWarning() << "Local proxy owner server id is not configured";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto ownerServer = m_serversRepository->serverJsonById(ownerId);
|
||||
if (!ownerServer) {
|
||||
qWarning() << "Owner server with id" << ownerId << "not found";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (!apiUtils::isPremiumServer(*ownerServer)) {
|
||||
qWarning() << "Server" << ownerServer->value(configKey::name).toString() << "is not premium, local proxy is unavailable";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto serializedConfig = xrayConfigUtils::extractLastConfig(*ownerServer);
|
||||
if (!serializedConfig) {
|
||||
serializedConfig = fetchXrayConfigFromGateway(ownerId);
|
||||
if (!serializedConfig) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(serializedConfig->toUtf8(), &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !doc.isObject()) {
|
||||
qWarning() << "Failed to parse Xray config JSON:" << parseError.errorString();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto proxyPort = selectProxyPort();
|
||||
if (!proxyPort) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
QJsonObject xrayConfig = doc.object();
|
||||
if (!xrayConfigUtils::setInboundPort(xrayConfig, *proxyPort)) {
|
||||
qWarning() << "Failed to override local proxy inbound port, using original config";
|
||||
return XrayConfig { *serializedConfig, 0 };
|
||||
}
|
||||
|
||||
return XrayConfig { QString::fromUtf8(QJsonDocument(xrayConfig).toJson(QJsonDocument::Compact)), *proxyPort };
|
||||
}
|
||||
|
||||
std::optional<int> LocalProxyController::selectProxyPort() const
|
||||
{
|
||||
int port = m_appSettingsRepository->localProxyPort();
|
||||
if (port < localProxy::proxyPortMin || port > localProxy::proxyPortMax) {
|
||||
port = localProxy::defaultProxyPort;
|
||||
}
|
||||
|
||||
if (portUtils::waitForPort(port, localProxy::portReleaseWaitMs)) {
|
||||
return port;
|
||||
}
|
||||
|
||||
if (m_appSettingsRepository->isLocalProxyPortUserDefined()) {
|
||||
qWarning() << "Local proxy port" << port << "is already in use";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<int> freePort;
|
||||
if (port != localProxy::defaultProxyPort && portUtils::isPortAvailable(localProxy::defaultProxyPort)) {
|
||||
freePort = localProxy::defaultProxyPort;
|
||||
} else {
|
||||
freePort = portUtils::findFirstAvailablePort(localProxy::defaultProxyPort + 1, localProxy::proxyPortMax);
|
||||
}
|
||||
|
||||
if (!freePort) {
|
||||
qWarning() << "No available local proxy port in range" << localProxy::defaultProxyPort + 1 << "-"
|
||||
<< localProxy::proxyPortMax;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
qDebug() << "Local proxy port" << port << "is busy, using" << *freePort;
|
||||
return freePort;
|
||||
}
|
||||
|
||||
std::optional<QString> LocalProxyController::fetchXrayConfigFromGateway(const QString &serverId) const
|
||||
{
|
||||
QJsonObject serverConfig;
|
||||
const ErrorCode errorCode = m_subscriptionController->fetchServiceConfig(serverId, configKey::vless, serverConfig);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
qWarning() << "Gateway request failed with error code" << static_cast<int>(errorCode);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto serializedConfig = xrayConfigUtils::extractLastConfig(serverConfig);
|
||||
if (!serializedConfig) {
|
||||
qWarning() << "Gateway response lacks Xray last_config payload";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
qDebug() << "Fetched Xray config from gateway";
|
||||
return serializedConfig;
|
||||
}
|
||||
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
|
||||
bool LocalProxyController::isSupported() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LocalProxyController::startApi(quint16 port)
|
||||
{
|
||||
if (m_httpServer && m_apiPort == port) {
|
||||
return true;
|
||||
}
|
||||
|
||||
stopApi();
|
||||
|
||||
const auto proxyPortValue = [this]() {
|
||||
const int port = runningProxyPort();
|
||||
return port > 0 ? QJsonValue(port) : QJsonValue::Null;
|
||||
};
|
||||
|
||||
auto httpServer = new QHttpServer(this);
|
||||
httpServer->route("/api/v1/up", QHttpServerRequest::Method::Post, [this, proxyPortValue] {
|
||||
const bool started = startXray();
|
||||
return QHttpServerResponse(QJsonObject { { "status", started ? "ok" : "error" }, { "proxyPort", proxyPortValue() } });
|
||||
});
|
||||
httpServer->route("/api/v1/down", QHttpServerRequest::Method::Post, [this] {
|
||||
const bool stopped = stopXray();
|
||||
return QHttpServerResponse(QJsonObject { { "status", stopped ? "ok" : "error" } });
|
||||
});
|
||||
httpServer->route("/api/v1/ping", QHttpServerRequest::Method::Get, [proxyPortValue] {
|
||||
return QHttpServerResponse(QJsonObject { { "status", "ok" }, { "proxyPort", proxyPortValue() } });
|
||||
});
|
||||
|
||||
auto tcpServer = new QTcpServer(httpServer);
|
||||
if (!tcpServer->listen(QHostAddress::LocalHost, port) || !httpServer->bind(tcpServer)) {
|
||||
qWarning() << "Failed to start HTTP API server on port" << port;
|
||||
delete httpServer;
|
||||
return false;
|
||||
}
|
||||
|
||||
m_httpServer = httpServer;
|
||||
m_apiPort = port;
|
||||
qDebug() << "HTTP API server is running on localhost:" << port;
|
||||
return true;
|
||||
}
|
||||
|
||||
void LocalProxyController::stopApi()
|
||||
{
|
||||
delete m_httpServer;
|
||||
m_httpServer = nullptr;
|
||||
m_apiPort = 0;
|
||||
}
|
||||
|
||||
bool LocalProxyController::startXrayEngine(const QString &config)
|
||||
{
|
||||
if (config.trimmed().isEmpty()) {
|
||||
qWarning() << "Xray config content is empty";
|
||||
return false;
|
||||
}
|
||||
|
||||
const qint64 token = IpcClient::withInterface(
|
||||
[&](QSharedPointer<IpcInterfaceReplica> iface) -> qint64 {
|
||||
auto xrayStart = iface->xrayStartOwned(config);
|
||||
if (!xrayStart.waitForFinished()) {
|
||||
return 0;
|
||||
}
|
||||
return xrayStart.returnValue();
|
||||
},
|
||||
[]() -> qint64 { return 0; });
|
||||
|
||||
if (token == 0) {
|
||||
qWarning() << "Failed to start Xray via IPC";
|
||||
return false;
|
||||
}
|
||||
|
||||
m_xrayToken = token;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LocalProxyController::stopXrayEngine()
|
||||
{
|
||||
if (m_xrayToken == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const bool stopped = IpcClient::withInterface(
|
||||
[this](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
auto xrayStop = iface->xrayStopOwned(m_xrayToken);
|
||||
return xrayStop.waitForFinished() && xrayStop.returnValue();
|
||||
},
|
||||
[]() { return false; });
|
||||
|
||||
if (!stopped) {
|
||||
qWarning() << "Failed to stop Xray via IPC";
|
||||
return false;
|
||||
}
|
||||
|
||||
m_xrayToken = 0;
|
||||
qDebug() << "Xray stopped";
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LocalProxyController::isXrayEngineRunning() const
|
||||
{
|
||||
if (m_xrayToken == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return IpcClient::withInterface(
|
||||
[this](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
auto currentToken = iface->xrayCurrentToken();
|
||||
return currentToken.waitForFinished() && currentToken.returnValue() == m_xrayToken;
|
||||
},
|
||||
[]() { return false; });
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
bool LocalProxyController::isSupported() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool LocalProxyController::startApi(quint16)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void LocalProxyController::stopApi()
|
||||
{
|
||||
}
|
||||
|
||||
bool LocalProxyController::startXrayEngine(const QString &)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool LocalProxyController::stopXrayEngine()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LocalProxyController::isXrayEngineRunning() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,84 @@
|
||||
#ifndef LOCALPROXYCONTROLLER_H
|
||||
#define LOCALPROXYCONTROLLER_H
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
class QHttpServer;
|
||||
class SecureServersRepository;
|
||||
class SecureAppSettingsRepository;
|
||||
class SubscriptionController;
|
||||
|
||||
class LocalProxyController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
LocalProxyController(SecureServersRepository *serversRepository, SecureAppSettingsRepository *appSettingsRepository,
|
||||
SubscriptionController *subscriptionController, QObject *parent = nullptr);
|
||||
~LocalProxyController();
|
||||
|
||||
bool isSupported() const;
|
||||
bool isEnabled() const;
|
||||
int port() const;
|
||||
QString ownerId() const;
|
||||
bool isPortUserDefined() const;
|
||||
int activePort() const;
|
||||
|
||||
bool setPort(int port);
|
||||
bool isPortBusy(int port) const;
|
||||
bool enable(const QString &ownerId, int port);
|
||||
void disable();
|
||||
|
||||
public slots:
|
||||
void onServerEdited(const QString &serverId);
|
||||
void onServerRemoved(const QString &serverId);
|
||||
void onVpnConnectionRequested();
|
||||
|
||||
signals:
|
||||
void stateChanged();
|
||||
void startFailed(const QString &message);
|
||||
void stoppedByVpn(const QString &message);
|
||||
|
||||
private:
|
||||
struct XrayConfig
|
||||
{
|
||||
QString serializedConfig;
|
||||
int proxyPort = 0;
|
||||
};
|
||||
|
||||
void start();
|
||||
void stop();
|
||||
bool syncXray();
|
||||
bool startXray();
|
||||
bool stopXray();
|
||||
bool restartXray();
|
||||
void disableWithError(const QString &message);
|
||||
void setActivePort(int port);
|
||||
int runningProxyPort() const;
|
||||
|
||||
std::optional<XrayConfig> buildXrayConfig() const;
|
||||
std::optional<int> selectProxyPort() const;
|
||||
std::optional<QString> fetchXrayConfigFromGateway(const QString &serverId) const;
|
||||
|
||||
bool startApi(quint16 port);
|
||||
void stopApi();
|
||||
bool startXrayEngine(const QString &config);
|
||||
bool stopXrayEngine();
|
||||
bool isXrayEngineRunning() const;
|
||||
|
||||
SecureServersRepository *m_serversRepository;
|
||||
SecureAppSettingsRepository *m_appSettingsRepository;
|
||||
SubscriptionController *m_subscriptionController;
|
||||
|
||||
bool m_isEnabled = false;
|
||||
QHttpServer *m_httpServer = nullptr;
|
||||
quint16 m_apiPort = 0;
|
||||
qint64 m_xrayToken = 0;
|
||||
quint16 m_appliedProxyPort = 0;
|
||||
int m_activePort = 0;
|
||||
};
|
||||
|
||||
#endif // LOCALPROXYCONTROLLER_H
|
||||
@@ -0,0 +1,270 @@
|
||||
#include "logsController.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
|
||||
#ifdef AMNEZIA_HAS_QZIPWRITER
|
||||
#include <QBuffer>
|
||||
#include <QtCore/private/qzipwriter_p.h>
|
||||
#else
|
||||
#include <QDateTime>
|
||||
|
||||
#include <array>
|
||||
#endif
|
||||
|
||||
#include <limits>
|
||||
|
||||
#include "logger.h"
|
||||
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
#include "core/utils/swiftBridge.h"
|
||||
#elif defined(Q_OS_ANDROID)
|
||||
#include "platforms/android/android_controller.h"
|
||||
#endif
|
||||
|
||||
#ifndef AMNEZIA_HAS_QZIPWRITER
|
||||
namespace
|
||||
{
|
||||
quint32 crc32(const QByteArray &data)
|
||||
{
|
||||
static const std::array<quint32, 256> table = [] {
|
||||
std::array<quint32, 256> result {};
|
||||
for (quint32 i = 0; i < 256; ++i) {
|
||||
quint32 value = i;
|
||||
for (int bit = 0; bit < 8; ++bit) {
|
||||
value = (value & 1) ? 0xEDB88320u ^ (value >> 1) : value >> 1;
|
||||
}
|
||||
result[i] = value;
|
||||
}
|
||||
return result;
|
||||
}();
|
||||
|
||||
quint32 crc = 0xFFFFFFFFu;
|
||||
for (const char byte : data) {
|
||||
crc = table[(crc ^ static_cast<uchar>(byte)) & 0xFF] ^ (crc >> 8);
|
||||
}
|
||||
return crc ^ 0xFFFFFFFFu;
|
||||
}
|
||||
|
||||
void put16(QByteArray &out, quint16 value)
|
||||
{
|
||||
out.append(static_cast<char>(value & 0xFF));
|
||||
out.append(static_cast<char>(value >> 8));
|
||||
}
|
||||
|
||||
void put32(QByteArray &out, quint32 value)
|
||||
{
|
||||
put16(out, static_cast<quint16>(value & 0xFFFF));
|
||||
put16(out, static_cast<quint16>(value >> 16));
|
||||
}
|
||||
|
||||
QByteArray storedZip(const QList<QPair<QString, QByteArray>> &entries)
|
||||
{
|
||||
const QDateTime now = QDateTime::currentDateTime();
|
||||
const quint16 dosTime = (now.time().hour() << 11) | (now.time().minute() << 5) | (now.time().second() / 2);
|
||||
const quint16 dosDate = ((now.date().year() - 1980) << 9) | (now.date().month() << 5) | now.date().day();
|
||||
|
||||
QByteArray archive;
|
||||
QByteArray centralDirectory;
|
||||
for (const auto &[name, data] : entries) {
|
||||
const QByteArray fileName = name.toUtf8();
|
||||
const quint32 offset = static_cast<quint32>(archive.size());
|
||||
const quint32 size = static_cast<quint32>(data.size());
|
||||
|
||||
QByteArray fields;
|
||||
put16(fields, 10);
|
||||
put16(fields, 0);
|
||||
put16(fields, 0);
|
||||
put16(fields, dosTime);
|
||||
put16(fields, dosDate);
|
||||
put32(fields, crc32(data));
|
||||
put32(fields, size);
|
||||
put32(fields, size);
|
||||
put16(fields, static_cast<quint16>(fileName.size()));
|
||||
put16(fields, 0);
|
||||
|
||||
put32(archive, 0x04034b50);
|
||||
archive.append(fields).append(fileName).append(data);
|
||||
|
||||
put32(centralDirectory, 0x02014b50);
|
||||
put16(centralDirectory, 20);
|
||||
centralDirectory.append(fields);
|
||||
put16(centralDirectory, 0);
|
||||
put16(centralDirectory, 0);
|
||||
put16(centralDirectory, 0);
|
||||
put32(centralDirectory, 0);
|
||||
put32(centralDirectory, offset);
|
||||
centralDirectory.append(fileName);
|
||||
}
|
||||
|
||||
const quint32 centralDirectoryOffset = static_cast<quint32>(archive.size());
|
||||
archive.append(centralDirectory);
|
||||
|
||||
put32(archive, 0x06054b50);
|
||||
put16(archive, 0);
|
||||
put16(archive, 0);
|
||||
put16(archive, static_cast<quint16>(entries.size()));
|
||||
put16(archive, static_cast<quint16>(entries.size()));
|
||||
put32(archive, static_cast<quint32>(centralDirectory.size()));
|
||||
put32(archive, centralDirectoryOffset);
|
||||
put16(archive, 0);
|
||||
return archive;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
QStringList LogsController::files(Stream stream)
|
||||
{
|
||||
#if defined(AMNEZIA_DESKTOP)
|
||||
if (stream == Stream::App) {
|
||||
return { Logger::userLogsFilePath() };
|
||||
}
|
||||
return { Logger::serviceLogsFilePath() };
|
||||
#elif defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
if (stream == Stream::App) {
|
||||
return { QString::fromStdString(SWIFT_BRIDGE_NAMESPACE::swiftAppLogPath()), Logger::userLogsFilePath() };
|
||||
}
|
||||
return { QString::fromStdString(SWIFT_BRIDGE_NAMESPACE::swiftTunnelLogPath()) };
|
||||
#elif defined(Q_OS_ANDROID)
|
||||
return AndroidController::logFiles(static_cast<int>(stream));
|
||||
#endif
|
||||
}
|
||||
|
||||
qint64 LogsController::size(Stream stream)
|
||||
{
|
||||
qint64 total = 0;
|
||||
for (const QString &path : files(stream)) {
|
||||
total += fileSize(path);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
qint64 LogsController::fileSize(const QString &path)
|
||||
{
|
||||
QFile file(path);
|
||||
return file.open(QIODevice::ReadOnly) ? file.size() : 0;
|
||||
}
|
||||
|
||||
QString LogsController::readTail(Stream stream, qint64 *newestFileEnd, qint64 maxBytes)
|
||||
{
|
||||
return QString::fromUtf8(read(stream, maxBytes, newestFileEnd));
|
||||
}
|
||||
|
||||
QByteArray LogsController::readAll(Stream stream)
|
||||
{
|
||||
return read(stream, std::numeric_limits<qint64>::max());
|
||||
}
|
||||
|
||||
QByteArray LogsController::read(Stream stream, qint64 maxBytes, qint64 *newestFileEnd)
|
||||
{
|
||||
QByteArray data = tailOfFiles(files(stream), maxBytes, newestFileEnd);
|
||||
#if defined(Q_OS_ANDROID)
|
||||
if (stream == Stream::App && maxBytes == std::numeric_limits<qint64>::max()) {
|
||||
data.prepend(AndroidController::deviceInfo().toUtf8() + '\n');
|
||||
}
|
||||
#endif
|
||||
return data;
|
||||
}
|
||||
|
||||
void LogsController::clear(Stream stream)
|
||||
{
|
||||
#if defined(AMNEZIA_DESKTOP)
|
||||
if (stream == Stream::App) {
|
||||
Logger::clearLogs(false);
|
||||
} else {
|
||||
Logger::clearServiceLogs();
|
||||
}
|
||||
#elif defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
if (stream == Stream::App) {
|
||||
Logger::clearLogs(false);
|
||||
QFile::resize(QString::fromStdString(SWIFT_BRIDGE_NAMESPACE::swiftAppLogPath()), 0);
|
||||
} else {
|
||||
QFile::resize(QString::fromStdString(SWIFT_BRIDGE_NAMESPACE::swiftTunnelLogPath()), 0);
|
||||
}
|
||||
#elif defined(Q_OS_ANDROID)
|
||||
AndroidController::clearLogStream(static_cast<int>(stream));
|
||||
#endif
|
||||
}
|
||||
|
||||
QByteArray LogsController::zipAll()
|
||||
{
|
||||
return zipFiles({ { QStringLiteral("app.log"), readAll(Stream::App) },
|
||||
{ QStringLiteral("tunnel.log"), readAll(Stream::Tunnel) } });
|
||||
}
|
||||
|
||||
QByteArray LogsController::zipFiles(const QList<QPair<QString, QByteArray>> &entries)
|
||||
{
|
||||
#ifdef AMNEZIA_HAS_QZIPWRITER
|
||||
QBuffer buffer;
|
||||
buffer.open(QIODevice::WriteOnly);
|
||||
|
||||
QZipWriter zip(&buffer);
|
||||
zip.setCompressionPolicy(QZipWriter::AlwaysCompress);
|
||||
for (const auto &[name, data] : entries) {
|
||||
zip.addFile(name, data);
|
||||
}
|
||||
zip.close();
|
||||
|
||||
return buffer.data();
|
||||
#else
|
||||
return storedZip(entries);
|
||||
#endif
|
||||
}
|
||||
|
||||
QByteArray LogsController::tailOfFiles(const QStringList &paths, qint64 maxBytes, qint64 *newestFileEnd)
|
||||
{
|
||||
if (newestFileEnd) {
|
||||
*newestFileEnd = 0;
|
||||
}
|
||||
|
||||
QList<QPair<QString, QByteArray>> chunks;
|
||||
qint64 remaining = maxBytes;
|
||||
|
||||
for (auto it = paths.crbegin(); it != paths.crend() && remaining > 0; ++it) {
|
||||
QFile file(*it);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const qint64 fileSize = file.size();
|
||||
QByteArray chunk;
|
||||
if (fileSize <= remaining) {
|
||||
chunk = file.read(fileSize);
|
||||
remaining -= fileSize;
|
||||
} else {
|
||||
file.seek(fileSize - remaining - 1);
|
||||
chunk = file.read(remaining + 1);
|
||||
const qsizetype newline = chunk.indexOf('\n');
|
||||
qsizetype start = newline + 1;
|
||||
if (newline < 0) {
|
||||
start = 1;
|
||||
while (start < chunk.size() && (static_cast<uchar>(chunk.at(start)) & 0xC0) == 0x80) {
|
||||
++start;
|
||||
}
|
||||
}
|
||||
chunk = chunk.mid(start);
|
||||
remaining = 0;
|
||||
}
|
||||
|
||||
if (newestFileEnd && it == paths.crbegin()) {
|
||||
*newestFileEnd = file.pos();
|
||||
}
|
||||
|
||||
if (!chunk.isEmpty()) {
|
||||
chunks.prepend({ QFileInfo(*it).fileName(), chunk });
|
||||
}
|
||||
}
|
||||
|
||||
if (chunks.size() == 1) {
|
||||
return chunks.first().second;
|
||||
}
|
||||
|
||||
QByteArray result;
|
||||
for (const auto &[fileName, chunk] : chunks) {
|
||||
result += "----- " + fileName.toUtf8() + " -----\n" + chunk;
|
||||
if (!chunk.endsWith('\n')) {
|
||||
result += '\n';
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
#ifndef LOGSCONTROLLER_H
|
||||
#define LOGSCONTROLLER_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QList>
|
||||
#include <QPair>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
class LogsController
|
||||
{
|
||||
public:
|
||||
enum class Stream { App, Tunnel };
|
||||
|
||||
static QStringList files(Stream stream);
|
||||
static qint64 size(Stream stream);
|
||||
static qint64 fileSize(const QString &path);
|
||||
static QString readTail(Stream stream, qint64 *newestFileEnd = nullptr, qint64 maxBytes = 512 * 1024);
|
||||
static QByteArray readAll(Stream stream);
|
||||
static void clear(Stream stream);
|
||||
static QByteArray zipAll();
|
||||
static QByteArray zipFiles(const QList<QPair<QString, QByteArray>> &entries);
|
||||
|
||||
static QByteArray tailOfFiles(const QStringList &paths, qint64 maxBytes, qint64 *newestFileEnd = nullptr);
|
||||
|
||||
private:
|
||||
static QByteArray read(Stream stream, qint64 maxBytes, qint64 *newestFileEnd = nullptr);
|
||||
};
|
||||
|
||||
#endif // LOGSCONTROLLER_H
|
||||
@@ -5,16 +5,16 @@
|
||||
|
||||
#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"
|
||||
#include "version.h"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
@@ -27,18 +27,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 +49,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 +85,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 +112,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 +124,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 +166,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 +199,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;
|
||||
@@ -202,11 +221,14 @@ ExportController::ExportResult ExportController::generateWireGuardConfig(int ser
|
||||
result.config.append(line + "\n");
|
||||
}
|
||||
|
||||
result.qrCodes << generateSingleQrCode(result.config.toUtf8());
|
||||
const QString qr = qrCodeUtils::generatePlainQrCodeImage(result.config.toUtf8());
|
||||
if (!qr.isEmpty()) {
|
||||
result.qrCodes << qr;
|
||||
}
|
||||
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 +237,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;
|
||||
@@ -228,18 +255,26 @@ ExportController::ExportResult ExportController::generateAwgConfig(int serverInd
|
||||
result.config.append(line + "\n");
|
||||
}
|
||||
|
||||
result.qrCodes << generateSingleQrCode(result.config.toUtf8());
|
||||
const QString qr = qrCodeUtils::generatePlainQrCodeImage(result.config.toUtf8());
|
||||
if (!qr.isEmpty()) {
|
||||
result.qrCodes << qr;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
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,29 +330,41 @@ 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");
|
||||
result.nativeConfigString = amnezia::serialization::vless::Serialize(vlessServer, APPLICATION_NAME);
|
||||
|
||||
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)
|
||||
@@ -329,9 +376,3 @@ QList<QString> ExportController::generateQrCodesFromConfig(const QByteArray &dat
|
||||
{
|
||||
return qrCodeUtils::generateQrCodeImageSeries(data);
|
||||
}
|
||||
|
||||
QString ExportController::generateSingleQrCode(const QByteArray &data)
|
||||
{
|
||||
auto qr = qrCodeUtils::generateQrCode(data);
|
||||
return qrCodeUtils::svgToBase64(QString::fromStdString(toSvgString(qr, 1)));
|
||||
}
|
||||
@@ -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,13 +63,12 @@ private:
|
||||
QJsonObject jsonNativeConfig;
|
||||
};
|
||||
|
||||
NativeConfigResult generateNativeConfig(int serverIndex, DockerContainer container,
|
||||
NativeConfigResult generateNativeConfig(const QString &serverId, DockerContainer container,
|
||||
const ContainerConfig &containerConfig,
|
||||
const QString &clientName);
|
||||
|
||||
QString generateVpnUrl(const QByteArray &compressedConfig);
|
||||
QList<QString> generateQrCodesFromConfig(const QByteArray &data);
|
||||
QString generateSingleQrCode(const QByteArray &data);
|
||||
|
||||
SecureServersRepository* m_serversRepository;
|
||||
SecureAppSettingsRepository* m_appSettingsRepository;
|
||||
|
||||
@@ -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,13 +27,24 @@
|
||||
#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;
|
||||
|
||||
namespace
|
||||
{
|
||||
bool rejectUnsupportedFormatVersion(ImportController::ImportResult &result)
|
||||
{
|
||||
if (serverConfigUtils::isConfigFormatVersionSupported(result.config)) {
|
||||
return false;
|
||||
}
|
||||
qWarning() << "Config format version" << serverConfigUtils::configFormatVersion(result.config)
|
||||
<< "is newer than supported" << serverConfigUtils::currentConfigFormatVersion;
|
||||
result.errorCode = ErrorCode::ConfigFormatVersionNotSupportedError;
|
||||
result.config = {};
|
||||
return true;
|
||||
}
|
||||
|
||||
ConfigTypes checkConfigFormat(const QString &config)
|
||||
{
|
||||
const QString openVpnConfigPatternCli = "client";
|
||||
@@ -53,6 +64,7 @@ namespace
|
||||
const QString amneziaFreeConfigPattern = "api_key";
|
||||
const QString amneziaPremiumConfigPattern = "auth_data";
|
||||
const QString backupPattern = "Servers/serversList";
|
||||
const QString xraySettingsPattern = "serverConfig";
|
||||
|
||||
if (config.contains(backupPattern)) {
|
||||
return ConfigTypes::Backup;
|
||||
@@ -68,6 +80,8 @@ namespace
|
||||
} else if (config.contains(openVpnConfigPatternCli)
|
||||
&& (config.contains(openVpnConfigPatternDriver1) || config.contains(openVpnConfigPatternDriver2))) {
|
||||
return ConfigTypes::OpenVpn;
|
||||
} else if (QJsonDocument::fromJson(config.toUtf8()).object().value(xraySettingsPattern).isObject()) {
|
||||
return ConfigTypes::XraySettings;
|
||||
}
|
||||
return ConfigTypes::Invalid;
|
||||
}
|
||||
@@ -208,12 +222,22 @@ ImportController::ImportResult ImportController::extractConfigFromData(const QSt
|
||||
case ConfigTypes::Amnezia: {
|
||||
result.config = QJsonDocument::fromJson(config.toUtf8()).object();
|
||||
|
||||
if (apiUtils::isServerFromApi(result.config)) {
|
||||
if (rejectUnsupportedFormatVersion(result)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -226,6 +250,10 @@ ImportController::ImportResult ImportController::extractConfigFromData(const QSt
|
||||
result.errorCode = ErrorCode::ImportBackupFileUseRestoreInstead;
|
||||
return result;
|
||||
}
|
||||
case ConfigTypes::XraySettings: {
|
||||
result.errorCode = ErrorCode::ImportXraySettingsFileUseXrayPage;
|
||||
return result;
|
||||
}
|
||||
case ConfigTypes::Invalid: {
|
||||
result.errorCode = ErrorCode::ImportInvalidConfigError;
|
||||
result.configFileName.clear();
|
||||
@@ -251,6 +279,7 @@ ImportController::ImportResult ImportController::extractConfigFromQr(const QByte
|
||||
if (!dataObj.isEmpty()) {
|
||||
result.config = dataObj;
|
||||
result.configType = ConfigTypes::Amnezia;
|
||||
rejectUnsupportedFormatVersion(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -262,6 +291,7 @@ ImportController::ImportResult ImportController::extractConfigFromQr(const QByte
|
||||
return result;
|
||||
}
|
||||
result.configType = ConfigTypes::Amnezia;
|
||||
rejectUnsupportedFormatVersion(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -279,6 +309,7 @@ ImportController::ImportResult ImportController::extractConfigFromQr(const QByte
|
||||
return result;
|
||||
}
|
||||
result.configType = ConfigTypes::Amnezia;
|
||||
rejectUnsupportedFormatVersion(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -374,6 +405,11 @@ int ImportController::qrChunksTotal() const
|
||||
|
||||
void ImportController::importConfig(const QJsonObject &config)
|
||||
{
|
||||
if (!serverConfigUtils::isConfigFormatVersionSupported(config)) {
|
||||
emit importErrorOccurred(ErrorCode::ConfigFormatVersionNotSupportedError, false);
|
||||
return;
|
||||
}
|
||||
|
||||
ServerCredentials credentials;
|
||||
credentials.hostName = config.value(configKey::hostName).toString();
|
||||
credentials.port = config.value(configKey::port).toInt();
|
||||
@@ -381,18 +417,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 +517,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);
|
||||
@@ -495,9 +542,10 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data, Config
|
||||
if (trimmedLine.startsWith("[") && trimmedLine.endsWith("]")) {
|
||||
continue;
|
||||
} else {
|
||||
QStringList parts = trimmedLine.split(" = ");
|
||||
if (parts.count() == 2) {
|
||||
configMap[parts.at(0).trimmed()] = parts.at(1).trimmed();
|
||||
const qsizetype separatorIndex = trimmedLine.indexOf('=');
|
||||
if (separatorIndex > 0) {
|
||||
configMap[trimmedLine.left(separatorIndex).trimmed()] =
|
||||
trimmedLine.mid(separatorIndex + 1).trimmed();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -550,53 +598,26 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data, Config
|
||||
lastConfig[configKey::persistentKeepAlive] = configMap.value(protocols::wireguard::PersistentKeepalive);
|
||||
}
|
||||
|
||||
QJsonArray allowedIpsJsonArray = QJsonArray::fromStringList(
|
||||
configMap.value(protocols::wireguard::AllowedIPs).split(", "));
|
||||
const QStringList allowedIps = configMap.value(protocols::wireguard::AllowedIPs).split(
|
||||
QRegularExpression("\\s*,\\s*"), Qt::SkipEmptyParts);
|
||||
QJsonArray allowedIpsJsonArray = QJsonArray::fromStringList(allowedIps);
|
||||
|
||||
lastConfig[configKey::allowedIps] = allowedIpsJsonArray;
|
||||
|
||||
QString protocolName = configKey::wireguard;
|
||||
QString protocolVersion;
|
||||
ConfigTypes detectedType = ConfigTypes::WireGuard;
|
||||
|
||||
const QStringList requiredJunkFields = { configKey::junkPacketCount, configKey::junkPacketMinSize,
|
||||
configKey::junkPacketMaxSize, configKey::initPacketJunkSize,
|
||||
configKey::responsePacketJunkSize, configKey::initPacketMagicHeader,
|
||||
configKey::responsePacketMagicHeader, configKey::underloadPacketMagicHeader,
|
||||
configKey::transportPacketMagicHeader };
|
||||
const QStringList awgProtocolKeys = configKey::awgProtocolKeys();
|
||||
|
||||
const QStringList optionalJunkFields = { configKey::cookieReplyPacketJunkSize,
|
||||
configKey::transportPacketJunkSize,
|
||||
configKey::specialJunk1, configKey::specialJunk2, configKey::specialJunk3,
|
||||
configKey::specialJunk4, configKey::specialJunk5
|
||||
};
|
||||
|
||||
bool hasAllRequiredFields = std::all_of(requiredJunkFields.begin(), requiredJunkFields.end(),
|
||||
[&configMap](const QString &field) { return !configMap.value(field).isEmpty(); });
|
||||
if (hasAllRequiredFields) {
|
||||
for (const QString &field : requiredJunkFields) {
|
||||
lastConfig[field] = configMap.value(field);
|
||||
}
|
||||
|
||||
for (const QString &field : optionalJunkFields) {
|
||||
if (!configMap.value(field).isEmpty()) {
|
||||
lastConfig[field] = configMap.value(field);
|
||||
bool hasAwgKeys = std::any_of(awgProtocolKeys.begin(), awgProtocolKeys.end(),
|
||||
[&configMap](const QString &field) { return !configMap.value(field).isEmpty(); });
|
||||
if (hasAwgKeys) {
|
||||
for (const QString &key : awgProtocolKeys) {
|
||||
if (!configMap.value(key).isEmpty()) {
|
||||
lastConfig[key] = configMap.value(key);
|
||||
}
|
||||
}
|
||||
|
||||
bool hasCookieReplyPacketJunkSize = !configMap.value(configKey::cookieReplyPacketJunkSize).isEmpty();
|
||||
bool hasTransportPacketJunkSize = !configMap.value(configKey::transportPacketJunkSize).isEmpty();
|
||||
bool hasSpecialJunk = !configMap.value(configKey::specialJunk1).isEmpty() ||
|
||||
!configMap.value(configKey::specialJunk2).isEmpty() ||
|
||||
!configMap.value(configKey::specialJunk3).isEmpty() ||
|
||||
!configMap.value(configKey::specialJunk4).isEmpty() ||
|
||||
!configMap.value(configKey::specialJunk5).isEmpty();
|
||||
|
||||
if (hasCookieReplyPacketJunkSize && hasTransportPacketJunkSize) {
|
||||
protocolVersion = "2";
|
||||
} else if (hasSpecialJunk && !hasCookieReplyPacketJunkSize && !hasTransportPacketJunkSize) {
|
||||
protocolVersion = "1.5";
|
||||
}
|
||||
protocolName = configKey::awg;
|
||||
detectedType = ConfigTypes::Awg;
|
||||
}
|
||||
@@ -614,9 +635,6 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data, Config
|
||||
wireguardConfig[configKey::isThirdPartyConfig] = true;
|
||||
wireguardConfig[configKey::port] = port;
|
||||
wireguardConfig[configKey::transportProto] = protocols::openvpn::defaultTransportProto;
|
||||
if (protocolName == configKey::awg && !protocolVersion.isEmpty()) {
|
||||
wireguardConfig[configKey::protocolVersion] = protocolVersion;
|
||||
}
|
||||
|
||||
QJsonObject containers;
|
||||
QString containerName = (protocolName == configKey::awg) ? configKey::amneziaAwg : configKey::amneziaWireguard;
|
||||
@@ -629,7 +647,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 = "
|
||||
@@ -650,11 +668,24 @@ QJsonObject ImportController::extractXrayConfig(const QString &data, ConfigTypes
|
||||
{
|
||||
QJsonParseError parserErr;
|
||||
QJsonDocument jsonConf = QJsonDocument::fromJson(data.toLocal8Bit(), &parserErr);
|
||||
if (parserErr.error != QJsonParseError::NoError || !jsonConf.isObject()) {
|
||||
qDebug() << "Xray config JSON parse failed:" << parserErr.errorString();
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
const QJsonObject parsedConfig = jsonConf.object();
|
||||
if (!parsedConfig.value(protocols::xray::inbounds).isArray()
|
||||
|| !parsedConfig.value(protocols::xray::outbounds).isArray()) {
|
||||
qDebug() << "Xray config is missing inbounds or outbounds";
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
const QString serializedConfig = QString::fromUtf8(jsonConf.toJson());
|
||||
|
||||
QJsonObject xrayVpnConfig;
|
||||
xrayVpnConfig[configKey::config] = jsonConf.toJson().constData();
|
||||
xrayVpnConfig[configKey::config] = serializedConfig;
|
||||
QJsonObject lastConfig;
|
||||
lastConfig[configKey::lastConfig] = jsonConf.toJson().constData();
|
||||
lastConfig[configKey::lastConfig] = serializedConfig;
|
||||
lastConfig[configKey::isThirdPartyConfig] = true;
|
||||
|
||||
QJsonObject containers;
|
||||
@@ -683,7 +714,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;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ namespace
|
||||
Xray,
|
||||
ShadowSocks,
|
||||
Backup,
|
||||
XraySettings,
|
||||
Invalid
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -32,28 +33,46 @@ public:
|
||||
QObject *parent = nullptr);
|
||||
~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 setupContainer(const ServerCredentials &credentials, DockerContainer container, ContainerConfig &config, bool isUpdate = false, bool acceptXrayKeyLoss = false);
|
||||
|
||||
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, bool acceptXrayKeyLoss = false);
|
||||
|
||||
// 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);
|
||||
|
||||
void setTProxyInstallHints(const QString &hostname, const QString &email);
|
||||
|
||||
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 +81,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 +95,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);
|
||||
@@ -85,6 +106,11 @@ private:
|
||||
ErrorCode configureContainerWorker(const ServerCredentials &credentials, DockerContainer container, ContainerConfig &config, SshSession &sshSession);
|
||||
ErrorCode startupContainerWorker(const ServerCredentials &credentials, DockerContainer container, const ContainerConfig &config, SshSession &sshSession);
|
||||
|
||||
ErrorCode readXrayStateBeforeVolumeMigration(const ServerCredentials &credentials, DockerContainer container,
|
||||
SshSession &sshSession, QMap<QString, QString> &outFiles);
|
||||
ErrorCode restoreXrayStateIntoDataVolume(const ServerCredentials &credentials, DockerContainer container,
|
||||
SshSession &sshSession, const QMap<QString, QString> &files);
|
||||
|
||||
ErrorCode isServerPortBusy(const ServerCredentials &credentials, DockerContainer container, const ContainerConfig &config, SshSession &sshSession);
|
||||
ErrorCode isUserInSudo(const ServerCredentials &credentials, SshSession &sshSession);
|
||||
ErrorCode isServerDpkgBusy(const ServerCredentials &credentials, SshSession &sshSession);
|
||||
@@ -95,9 +121,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);
|
||||
@@ -107,11 +133,12 @@ private:
|
||||
SecureServersRepository* m_serversRepository;
|
||||
SecureAppSettingsRepository* m_appSettingsRepository;
|
||||
bool m_cancelInstallation = false;
|
||||
|
||||
QString m_tproxyInstallHostname;
|
||||
QString m_tproxyInstallEmail;
|
||||
|
||||
#ifndef Q_OS_IOS
|
||||
QList<QSharedPointer<QProcess>> m_sftpMountProcesses;
|
||||
#endif
|
||||
};
|
||||
|
||||
#endif // INSTALLCONTROLLER_H
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QDateTime>
|
||||
|
||||
#include "core/utils/containerEnum.h"
|
||||
@@ -14,7 +15,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;
|
||||
@@ -56,6 +56,33 @@ int UsersController::clientIndexById(const QString &clientId, const QJsonArray &
|
||||
return -1;
|
||||
}
|
||||
|
||||
QString UsersController::readXrayVolumeUuid(const DockerContainer container, const ServerCredentials &credentials,
|
||||
SshSession *sshSession) const
|
||||
{
|
||||
ErrorCode error = ErrorCode::NoError;
|
||||
const QString uuid = QString::fromUtf8(sshSession->getTextFileFromContainer(
|
||||
container, credentials, QString::fromLatin1(amnezia::protocols::xray::uuidPath), error));
|
||||
if (error != ErrorCode::NoError) {
|
||||
return {};
|
||||
}
|
||||
return uuid.trimmed();
|
||||
}
|
||||
|
||||
int UsersController::stripXrayVolumeUuidFromTable(QJsonArray &clientsTable, const QString &volumeUuid)
|
||||
{
|
||||
if (volumeUuid.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
int removed = 0;
|
||||
for (int i = clientsTable.size() - 1; i >= 0; --i) {
|
||||
if (clientsTable.at(i).toObject().value(configKey::clientId).toString() == volumeUuid) {
|
||||
clientsTable.removeAt(i);
|
||||
++removed;
|
||||
}
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
void UsersController::migration(const QByteArray &clientsTableString, QJsonArray &clientsTable)
|
||||
{
|
||||
QJsonObject clientsTableObj = QJsonDocument::fromJson(clientsTableString).object();
|
||||
@@ -82,7 +109,7 @@ ErrorCode UsersController::wgShow(const DockerContainer container, const ServerC
|
||||
ErrorCode error = ErrorCode::NoError;
|
||||
QString stdOut;
|
||||
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
|
||||
stdOut += data + "\n";
|
||||
stdOut += data;
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
|
||||
@@ -104,16 +131,6 @@ ErrorCode UsersController::wgShow(const DockerContainer container, const ServerC
|
||||
|
||||
const auto getStrValue = [](const auto str) { return str.mid(str.indexOf(":") + 1).trimmed(); };
|
||||
|
||||
const auto parts = stdOut.split('\n');
|
||||
const auto peerList = parts.filter("peer:");
|
||||
const auto latestHandshakeList = parts.filter("latest handshake:");
|
||||
const auto transferredDataList = parts.filter("transfer:");
|
||||
const auto allowedIpsList = parts.filter("allowed ips:");
|
||||
|
||||
if (allowedIpsList.isEmpty() || latestHandshakeList.isEmpty() || transferredDataList.isEmpty() || peerList.isEmpty()) {
|
||||
return error;
|
||||
}
|
||||
|
||||
const auto changeHandshakeFormat = [](QString &latestHandshake) {
|
||||
const std::vector<std::pair<QString, QString>> replaceMap = { { " days", "d" }, { " hours", "h" }, { " minutes", "m" },
|
||||
{ " seconds", "s" }, { " day", "d" }, { " hour", "h" },
|
||||
@@ -124,20 +141,49 @@ ErrorCode UsersController::wgShow(const DockerContainer container, const ServerC
|
||||
}
|
||||
};
|
||||
|
||||
for (int i = 0; i < peerList.size() && i < transferredDataList.size() && i < latestHandshakeList.size() && i < allowedIpsList.size(); ++i) {
|
||||
const auto lines = stdOut.split('\n');
|
||||
WgShowData currentPeer;
|
||||
bool hasPeer = false;
|
||||
|
||||
const auto transferredData = getStrValue(transferredDataList[i]).split(",");
|
||||
auto latestHandshake = getStrValue(latestHandshakeList[i]);
|
||||
auto serverBytesReceived = transferredData.front().trimmed();
|
||||
auto serverBytesSent = transferredData.back().trimmed();
|
||||
auto allowedIps = getStrValue(allowedIpsList[i]);
|
||||
for (const QString &line : lines) {
|
||||
const QString trimmedLine = line.trimmed();
|
||||
|
||||
changeHandshakeFormat(latestHandshake);
|
||||
|
||||
serverBytesReceived.chop(QStringLiteral(" received").length());
|
||||
serverBytesSent.chop(QStringLiteral(" sent").length());
|
||||
|
||||
data.push_back({ getStrValue(peerList[i]), latestHandshake, serverBytesSent, serverBytesReceived, allowedIps });
|
||||
if (trimmedLine.startsWith("peer:")) {
|
||||
if (hasPeer) {
|
||||
data.push_back(currentPeer);
|
||||
}
|
||||
currentPeer = WgShowData();
|
||||
currentPeer.clientId = getStrValue(trimmedLine);
|
||||
hasPeer = true;
|
||||
} else if (hasPeer) {
|
||||
if (trimmedLine.startsWith("latest handshake:")) {
|
||||
auto latestHandshake = getStrValue(trimmedLine);
|
||||
changeHandshakeFormat(latestHandshake);
|
||||
currentPeer.latestHandshake = latestHandshake;
|
||||
} else if (trimmedLine.startsWith("transfer:")) {
|
||||
const auto transferredData = getStrValue(trimmedLine).split(",");
|
||||
if (transferredData.size() == 2) {
|
||||
auto serverBytesReceived = transferredData.front().trimmed();
|
||||
auto serverBytesSent = transferredData.back().trimmed();
|
||||
if (serverBytesReceived.endsWith(" received")) {
|
||||
serverBytesReceived.chop(QStringLiteral(" received").length());
|
||||
}
|
||||
if (serverBytesSent.endsWith(" sent")) {
|
||||
serverBytesSent.chop(QStringLiteral(" sent").length());
|
||||
}
|
||||
currentPeer.dataReceived = serverBytesSent;
|
||||
currentPeer.dataSent = serverBytesReceived;
|
||||
} else {
|
||||
logger.warning() << QString("Unexpected %1 show transfer format, skipping traffic stats for the peer")
|
||||
.arg(showBin);
|
||||
}
|
||||
} else if (trimmedLine.startsWith("allowed ips:")) {
|
||||
currentPeer.allowedIps = getStrValue(trimmedLine);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasPeer) {
|
||||
data.push_back(currentPeer);
|
||||
}
|
||||
|
||||
return error;
|
||||
@@ -230,7 +276,8 @@ ErrorCode UsersController::getWireGuardClients(const DockerContainer container,
|
||||
}
|
||||
|
||||
ErrorCode UsersController::getXrayClients(const DockerContainer container, const ServerCredentials& credentials,
|
||||
SshSession* sshSession, int &count, QJsonArray &clientsTable)
|
||||
SshSession* sshSession, int &count, QJsonArray &clientsTable,
|
||||
const QString &volumeDefaultUuid)
|
||||
{
|
||||
ErrorCode error = ErrorCode::NoError;
|
||||
|
||||
@@ -265,6 +312,7 @@ ErrorCode UsersController::getXrayClients(const DockerContainer container, const
|
||||
}
|
||||
|
||||
const QJsonArray clients = settings[protocols::xray::clients].toArray();
|
||||
const QString xrayDefaultUuid = volumeDefaultUuid;
|
||||
for (const auto &clientValue : clients) {
|
||||
const QJsonObject clientObj = clientValue.toObject();
|
||||
if (!clientObj.contains(protocols::xray::id)) {
|
||||
@@ -272,9 +320,6 @@ ErrorCode UsersController::getXrayClients(const DockerContainer container, const
|
||||
continue;
|
||||
}
|
||||
QString clientId = clientObj[protocols::xray::id].toString();
|
||||
|
||||
QString xrayDefaultUuid = sshSession->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::uuidPath, error);
|
||||
xrayDefaultUuid.replace("\n", "");
|
||||
|
||||
if (!isClientExists(clientId, clientsTable) && clientId != xrayDefaultUuid) {
|
||||
QJsonObject client;
|
||||
@@ -292,11 +337,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) {
|
||||
@@ -314,6 +366,9 @@ ErrorCode UsersController::updateClients(int serverIndex, const DockerContainer
|
||||
|
||||
m_clientsTable = QJsonDocument::fromJson(clientsTableString).array();
|
||||
|
||||
const QString xrayVolumeUuid =
|
||||
(container == DockerContainer::Xray) ? readXrayVolumeUuid(container, credentials, &sshSession) : QString();
|
||||
|
||||
if (m_clientsTable.isEmpty()) {
|
||||
migration(clientsTableString, m_clientsTable);
|
||||
|
||||
@@ -324,7 +379,7 @@ ErrorCode UsersController::updateClients(int serverIndex, const DockerContainer
|
||||
} else if (container == DockerContainer::WireGuard || ContainerUtils::isAwgContainer(container)) {
|
||||
error = getWireGuardClients(container, credentials, &sshSession, count, m_clientsTable);
|
||||
} else if (container == DockerContainer::Xray) {
|
||||
error = getXrayClients(container, credentials, &sshSession, count, m_clientsTable);
|
||||
error = getXrayClients(container, credentials, &sshSession, count, m_clientsTable, xrayVolumeUuid);
|
||||
}
|
||||
if (error != ErrorCode::NoError) {
|
||||
emit clientsUpdated(QJsonArray());
|
||||
@@ -340,6 +395,15 @@ ErrorCode UsersController::updateClients(int serverIndex, const DockerContainer
|
||||
}
|
||||
}
|
||||
|
||||
if (container == DockerContainer::Xray) {
|
||||
const int stripped = stripXrayVolumeUuidFromTable(m_clientsTable, xrayVolumeUuid);
|
||||
if (stripped > 0) {
|
||||
const QByteArray strippedTable = QJsonDocument(m_clientsTable).toJson();
|
||||
const ErrorCode writeError =
|
||||
sshSession.uploadTextFileToContainer(container, credentials, strippedTable, clientsTableFile);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<WgShowData> data;
|
||||
wgShow(container, credentials, &sshSession, data);
|
||||
|
||||
@@ -381,20 +445,34 @@ 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;
|
||||
}
|
||||
|
||||
if (container == DockerContainer::Xray) {
|
||||
const QString volumeUuid = readXrayVolumeUuid(container, credentials, &sshSession);
|
||||
if (!volumeUuid.isEmpty() && clientId == volumeUuid) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
}
|
||||
|
||||
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 +504,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 +512,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 +555,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;
|
||||
@@ -595,6 +680,19 @@ ErrorCode UsersController::revokeXray(const int row,
|
||||
|
||||
ErrorCode error = ErrorCode::NoError;
|
||||
|
||||
auto client = clientsTable.at(row).toObject();
|
||||
QString clientId = client.value(configKey::clientId).toString();
|
||||
|
||||
const QString volumeUuid = readXrayVolumeUuid(container, credentials, sshSession);
|
||||
if (!volumeUuid.isEmpty() && clientId == volumeUuid) {
|
||||
clientsTable.removeAt(row);
|
||||
const QByteArray clientsTableString = QJsonDocument(clientsTable).toJson();
|
||||
const QString clientsTableFile = QString("/opt/amnezia/%1/clientsTable")
|
||||
.arg(ContainerUtils::containerTypeToString(container));
|
||||
error = sshSession->uploadTextFileToContainer(container, credentials, clientsTableString, clientsTableFile);
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
const QString serverConfigPath = amnezia::protocols::xray::serverConfigPath;
|
||||
const QString configString = sshSession->getTextFileFromContainer(container, credentials, serverConfigPath, error);
|
||||
if (error != ErrorCode::NoError) {
|
||||
@@ -608,9 +706,6 @@ ErrorCode UsersController::revokeXray(const int row,
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
auto client = clientsTable.at(row).toObject();
|
||||
QString clientId = client.value(configKey::clientId).toString();
|
||||
|
||||
QJsonObject configObj = serverConfig.object();
|
||||
if (!configObj.contains(protocols::xray::inbounds)) {
|
||||
logger.error() << "Missing inbounds in xray config";
|
||||
@@ -678,7 +773,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 +784,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 +806,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 +826,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 +893,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 +912,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);
|
||||
@@ -63,7 +64,12 @@ private:
|
||||
ErrorCode getWireGuardClients(const DockerContainer container, const ServerCredentials &credentials,
|
||||
SshSession* sshSession, int &count, QJsonArray &clientsTable);
|
||||
ErrorCode getXrayClients(const DockerContainer container, const ServerCredentials& credentials,
|
||||
SshSession* sshSession, int &count, QJsonArray &clientsTable);
|
||||
SshSession* sshSession, int &count, QJsonArray &clientsTable,
|
||||
const QString &volumeDefaultUuid);
|
||||
|
||||
QString readXrayVolumeUuid(const DockerContainer container, const ServerCredentials &credentials,
|
||||
SshSession *sshSession) const;
|
||||
static int stripXrayVolumeUuidFromTable(QJsonArray &clientsTable, const QString &volumeUuid);
|
||||
|
||||
ErrorCode wgShow(const DockerContainer container, const ServerCredentials &credentials,
|
||||
SshSession* sshSession, std::vector<WgShowData> &data);
|
||||
@@ -73,4 +79,3 @@ private:
|
||||
};
|
||||
|
||||
#endif // USERSCONTROLLER_H
|
||||
|
||||
@@ -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>
|
||||
#include "core/utils/swiftBridge.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));
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
#include "version.h"
|
||||
#include "ui/utils/qAutoStart.h"
|
||||
#include "logger.h"
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include "platforms/android/android_controller.h"
|
||||
#include "core/controllers/logsController.h"
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
#include "core/utils/swiftBridge.h"
|
||||
#endif
|
||||
|
||||
QString getPlatformName()
|
||||
@@ -86,6 +87,9 @@ void SettingsController::toggleLogging(bool enable)
|
||||
qWarning() << "Initialization of debug subsystem failed";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
SWIFT_BRIDGE_NAMESPACE::toggleLogging(enable);
|
||||
#endif
|
||||
Logger::setServiceLogsEnabled(enable);
|
||||
|
||||
@@ -96,12 +100,8 @@ void SettingsController::toggleLogging(bool enable)
|
||||
|
||||
void SettingsController::clearLogs()
|
||||
{
|
||||
#ifdef Q_OS_ANDROID
|
||||
AndroidController::instance()->clearLogs();
|
||||
#else
|
||||
Logger::clearLogs(false);
|
||||
Logger::clearServiceLogs();
|
||||
#endif
|
||||
LogsController::clear(LogsController::Stream::App);
|
||||
LogsController::clear(LogsController::Stream::Tunnel);
|
||||
}
|
||||
|
||||
QByteArray SettingsController::backupAppConfig() const
|
||||
@@ -119,6 +119,11 @@ QByteArray SettingsController::backupAppConfig() const
|
||||
return QJsonDocument(config).toJson();
|
||||
}
|
||||
|
||||
int SettingsController::unsupportedFormatConfigsSkippedCount() const
|
||||
{
|
||||
return m_serversRepository->unsupportedFormatConfigsCount();
|
||||
}
|
||||
|
||||
ErrorCode SettingsController::restoreAppConfigFromData(const QByteArray &data)
|
||||
{
|
||||
if (!m_appSettingsRepository->restoreAppConfig(data)) {
|
||||
@@ -179,12 +184,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 +222,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();
|
||||
}
|
||||
|
||||
@@ -248,6 +255,16 @@ void SettingsController::toggleNewsNotificationsEnabled(bool enable)
|
||||
m_appSettingsRepository->setNewsNotifications(enable);
|
||||
}
|
||||
|
||||
bool SettingsController::isAutoUpdateCheckEnabled() const
|
||||
{
|
||||
return m_appSettingsRepository->isAutoUpdateCheckEnabled();
|
||||
}
|
||||
|
||||
void SettingsController::toggleAutoUpdateCheckEnabled(bool enable)
|
||||
{
|
||||
m_appSettingsRepository->setAutoUpdateCheckEnabled(enable);
|
||||
}
|
||||
|
||||
bool SettingsController::isKillSwitchEnabled() const
|
||||
{
|
||||
return m_appSettingsRepository->isKillSwitchEnabled();
|
||||
@@ -349,18 +366,8 @@ void SettingsController::setAppLanguage(const QLocale &locale)
|
||||
m_appSettingsRepository->setAppLanguage(locale);
|
||||
}
|
||||
|
||||
bool SettingsController::isPremV1MigrationReminderActive() const
|
||||
{
|
||||
return m_appSettingsRepository->isPremV1MigrationReminderActive();
|
||||
}
|
||||
|
||||
void SettingsController::disablePremV1MigrationReminder()
|
||||
{
|
||||
m_appSettingsRepository->disablePremV1MigrationReminder();
|
||||
}
|
||||
|
||||
QString SettingsController::nextAvailableServerName() const
|
||||
{
|
||||
return m_appSettingsRepository->nextAvailableServerName();
|
||||
return m_serversRepository->nextAvailableServerName();
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ public:
|
||||
|
||||
QByteArray backupAppConfig() const;
|
||||
ErrorCode restoreAppConfigFromData(const QByteArray &data);
|
||||
int unsupportedFormatConfigsSkippedCount() const;
|
||||
|
||||
QString getAppVersion() const;
|
||||
|
||||
@@ -59,6 +60,9 @@ public:
|
||||
bool isNewsNotificationsEnabled() const;
|
||||
void toggleNewsNotificationsEnabled(bool enable);
|
||||
|
||||
bool isAutoUpdateCheckEnabled() const;
|
||||
void toggleAutoUpdateCheckEnabled(bool enable);
|
||||
|
||||
bool isKillSwitchEnabled() const;
|
||||
void toggleKillSwitch(bool enable);
|
||||
|
||||
@@ -69,9 +73,6 @@ public:
|
||||
|
||||
void enableDevMode();
|
||||
|
||||
bool isPremV1MigrationReminderActive() const;
|
||||
void disablePremV1MigrationReminder();
|
||||
|
||||
QString nextAvailableServerName() const;
|
||||
bool isDevModeEnabled() const;
|
||||
|
||||
|
||||
Loaded 100 of 755 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user