mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-07-14 12:50:29 +03:00
Compare commits
45 Commits
dev
...
feat/imple
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67dcadcd42 | ||
|
|
a4b2b3e3ad | ||
|
|
3d540b22bf | ||
|
|
d670bca9d4 | ||
|
|
b09a4ecd8d | ||
|
|
c327d3e3c8 | ||
|
|
9c9e1700af | ||
|
|
eba2097d1d | ||
|
|
22de0c2a16 | ||
|
|
614973a4ce | ||
|
|
e554e9b8b4 | ||
|
|
d4833454ef | ||
|
|
9851b4bacb | ||
|
|
29ad1f0c02 | ||
|
|
d6c34b3f60 | ||
|
|
d8668742b4 | ||
|
|
5eab5fc18b | ||
|
|
b46a9e389f | ||
|
|
81b8cd05c2 | ||
|
|
d0a9f6e4d5 | ||
|
|
8a29b49fd7 | ||
|
|
1baa2d85bd | ||
|
|
e226fadb07 | ||
|
|
bf4bf9972d | ||
|
|
f781bf6a23 | ||
|
|
2fa0ec81ad | ||
|
|
1ee0a6c9c7 | ||
|
|
14c7aab0fb | ||
|
|
d3347e6007 | ||
|
|
026826970c | ||
|
|
d2d3545961 | ||
|
|
b7e2847393 | ||
|
|
bb56008c3d | ||
|
|
a53db6eafe | ||
|
|
433ecb448f | ||
|
|
ab12a0b3f0 | ||
|
|
5a192cec15 | ||
|
|
6fc65dba8a | ||
|
|
f65fd4a8c5 | ||
|
|
c877e1e5cb | ||
|
|
2cb12c596c | ||
|
|
5beae954c7 | ||
|
|
5583c0a2a9 | ||
|
|
2cb7b30d8a | ||
|
|
2f6714e278 |
107
.github/workflows/deploy.yml
vendored
107
.github/workflows/deploy.yml
vendored
@@ -18,14 +18,11 @@ jobs:
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
base: ${{ github.event.before }}
|
||||
filters: |
|
||||
recipes:
|
||||
- 'recipes/**'
|
||||
- 'conanfile.py'
|
||||
- '.github/workflows/deploy.yml'
|
||||
- 'cmake/conan_provider.cmake'
|
||||
- 'cmake/platform_settings.cmake'
|
||||
- 'cmake/recipes_bootstrap.cmake'
|
||||
|
||||
Bake-Prebuilts-Linux:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -43,7 +40,7 @@ jobs:
|
||||
python-version: 3.14
|
||||
|
||||
- name: 'Install conan'
|
||||
run: pip install "conan==2.28.0"
|
||||
run: pip install "conan==2.26.2"
|
||||
|
||||
- name: 'Build dependencies'
|
||||
shell: bash
|
||||
@@ -53,11 +50,9 @@ jobs:
|
||||
done
|
||||
|
||||
- name: 'Authorize in remote'
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
|
||||
|
||||
- name: 'Upload baked prebuilts'
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: conan upload -r amnezia "*" -c
|
||||
|
||||
# ------------------------------------------------------
|
||||
@@ -103,7 +98,7 @@ jobs:
|
||||
python-version: 3.14
|
||||
|
||||
- name: 'Install conan'
|
||||
run: pip install "conan==2.28.0"
|
||||
run: pip install "conan==2.26.2"
|
||||
|
||||
- name: 'Install system packages'
|
||||
run: sudo apt-get install libxkbcommon-x11-0 libsecret-1-dev
|
||||
@@ -123,7 +118,7 @@ jobs:
|
||||
- name: 'Upload installer artifact'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
path: deploy/build/AmneziaVPN_*_linux_x64.run
|
||||
path: deploy/build/AmneziaVPN-*-Linux.run
|
||||
archive: false
|
||||
retention-days: 7
|
||||
|
||||
@@ -154,17 +149,15 @@ jobs:
|
||||
- uses: ilammy/msvc-dev-cmd@v1
|
||||
|
||||
- name: 'Install conan'
|
||||
run: pip install "conan==2.28.0"
|
||||
run: pip install "conan==2.26.2"
|
||||
|
||||
- name: 'Build dependencies'
|
||||
run: cmake -S . -B build -DPREBUILTS_ONLY=1
|
||||
run: cmake -S . -B build -G "Visual Studio 17 2022" -DPREBUILTS_ONLY=1
|
||||
|
||||
- name: 'Authorize in remote'
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
|
||||
|
||||
- name: 'Upload baked prebuilts'
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: conan upload -r amnezia "*" -c
|
||||
|
||||
# ------------------------------------------------------
|
||||
@@ -236,7 +229,7 @@ jobs:
|
||||
python-version: 3.14
|
||||
|
||||
- name: 'Install conan'
|
||||
run: pip install "conan==2.28.0"
|
||||
run: pip install "conan==2.26.2"
|
||||
|
||||
- name: 'Build project'
|
||||
shell: cmd
|
||||
@@ -249,31 +242,27 @@ jobs:
|
||||
- name: 'Upload WIX installer artifact'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
path: deploy/build/AmneziaVPN_*_windows_x64.msi
|
||||
path: deploy/build/AmneziaVPN-*-win64.msi
|
||||
archive: false
|
||||
retention-days: 7
|
||||
|
||||
- name: 'Upload IFW installer artifact'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
path: deploy/build/AmneziaVPN_*_windows_x64.exe
|
||||
path: deploy/build/AmneziaVPN-*-win64.exe
|
||||
archive: false
|
||||
retention-days: 7
|
||||
|
||||
# ------------------------------------------------------
|
||||
|
||||
Bake-Prebuilts-iOS:
|
||||
runs-on: macos-latest
|
||||
needs: Detect-Changes
|
||||
if: needs.Detect-Changes.outputs.recipes_changed == 'true'
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
xcode-version: [26.0, 26.4]
|
||||
include:
|
||||
- xcode-version: 26.4
|
||||
os: macos-26
|
||||
|
||||
runs-on: ${{ matrix.os || 'macos-latest' }}
|
||||
xcode-version: [26.0]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -290,17 +279,15 @@ jobs:
|
||||
xcode-version: ${{ matrix.xcode-version }}
|
||||
|
||||
- name: 'Install conan'
|
||||
run: pip install "conan==2.28.0"
|
||||
run: pip install "conan==2.26.2"
|
||||
|
||||
- name: 'Build dependencies'
|
||||
run: cmake -S . -B build -G Xcode -DPREBUILTS_ONLY=1 -DCMAKE_SYSTEM_NAME=iOS -DCMAKE_OSX_SYSROOT=iphoneos
|
||||
|
||||
- name: 'Authorize in remote'
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
|
||||
|
||||
- name: 'Upload baked prebuilts'
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: conan upload -r amnezia "*" -c
|
||||
|
||||
# ------------------------------------------------------
|
||||
@@ -357,7 +344,7 @@ jobs:
|
||||
- name: 'Setup xcode'
|
||||
uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: '26.0'
|
||||
xcode-version: '26.1'
|
||||
|
||||
- name: 'Install desktop Qt'
|
||||
uses: jurplel/install-qt-action@v3
|
||||
@@ -389,7 +376,7 @@ jobs:
|
||||
python-version: 3.14
|
||||
|
||||
- name: 'Install deps'
|
||||
run: pip install "conan==2.28.0" jsonschema jinja2
|
||||
run: pip install "conan==2.26.2" jsonschema jinja2
|
||||
|
||||
- name: 'Build project'
|
||||
env:
|
||||
@@ -407,21 +394,14 @@ jobs:
|
||||
# ------------------------------------------------------
|
||||
|
||||
Bake-Prebuilts-MacOS:
|
||||
runs-on: macos-latest
|
||||
|
||||
needs: Detect-Changes
|
||||
if: needs.Detect-Changes.outputs.recipes_changed == 'true'
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
xcode-version: [16.2, 16.4, 26.4]
|
||||
include:
|
||||
- xcode-version: 16.2
|
||||
os: macos-15
|
||||
- xcode-version: 16.4
|
||||
os: macos-15
|
||||
- xcode-version: 26.4
|
||||
os: macos-26
|
||||
|
||||
runs-on: ${{ matrix.os || 'macos-latest' }}
|
||||
xcode-version: [16.2, 16.4]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -438,23 +418,21 @@ jobs:
|
||||
xcode-version: ${{ matrix.xcode-version }}
|
||||
|
||||
- name: 'Install conan'
|
||||
run: pip install "conan==2.28.0"
|
||||
run: pip install "conan==2.26.2"
|
||||
|
||||
- name: 'Build dependencies'
|
||||
run: cmake -S . -B build -G Xcode -DPREBUILTS_ONLY=1
|
||||
|
||||
- name: 'Authorize in remote'
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
|
||||
|
||||
- name: 'Upload baked prebuilts'
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: conan upload -r amnezia "*" -c
|
||||
|
||||
# ------------------------------------------------------
|
||||
|
||||
Build-MacOS:
|
||||
runs-on: macos-15
|
||||
runs-on: macos-latest
|
||||
|
||||
needs: Bake-Prebuilts-MacOS
|
||||
if: ${{ always() }}
|
||||
@@ -501,7 +479,7 @@ jobs:
|
||||
- name: 'Setup xcode'
|
||||
uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: '26.6.0'
|
||||
xcode-version: '16.2.0'
|
||||
|
||||
- name: 'Install Qt'
|
||||
uses: jurplel/install-qt-action@v4
|
||||
@@ -524,7 +502,7 @@ jobs:
|
||||
python-version: 3.14
|
||||
|
||||
- name: 'Install conan'
|
||||
run: pip install "conan==2.28.0"
|
||||
run: pip install "conan==2.26.2"
|
||||
|
||||
- name: 'Build project'
|
||||
env:
|
||||
@@ -540,28 +518,20 @@ jobs:
|
||||
- name: 'Upload installer artifact'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
path: deploy/build/AmneziaVPN_*_macos_x64.pkg
|
||||
path: deploy/build/AmneziaVPN-*-Darwin.pkg
|
||||
archive: false
|
||||
retention-days: 7
|
||||
|
||||
# ------------------------------------------------------
|
||||
|
||||
Bake-Prebuilts-MacOS-NE:
|
||||
runs-on: macos-latest
|
||||
needs: Detect-Changes
|
||||
if: needs.Detect-Changes.outputs.recipes_changed == 'true'
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
xcode-version: [16.2, 16.4, 26.4]
|
||||
include:
|
||||
- xcode-version: 16.2
|
||||
os: macos-15
|
||||
- xcode-version: 16.4
|
||||
os: macos-15
|
||||
- xcode-version: 26.4
|
||||
os: macos-26
|
||||
|
||||
runs-on: ${{ matrix.os || 'macos-latest' }}
|
||||
xcode-version: [16.2, 16.4]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -578,23 +548,21 @@ jobs:
|
||||
xcode-version: ${{ matrix.xcode-version }}
|
||||
|
||||
- name: 'Install conan'
|
||||
run: pip install "conan==2.28.0"
|
||||
run: pip install "conan==2.26.2"
|
||||
|
||||
- name: 'Build dependencies'
|
||||
run: cmake -S . -B build -G Xcode -DPREBUILTS_ONLY=1 -DMACOS_NE=TRUE
|
||||
|
||||
- name: 'Authorize in remote'
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
|
||||
|
||||
- name: 'Upload baked prebuilts'
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: conan upload -r amnezia "*" -c
|
||||
|
||||
# ------------------------------------------------------
|
||||
|
||||
Build-MacOS-NE:
|
||||
runs-on: macos-26
|
||||
runs-on: macos-latest
|
||||
|
||||
needs: Bake-Prebuilts-MacOS-NE
|
||||
if: ${{ always() }}
|
||||
@@ -618,12 +586,6 @@ jobs:
|
||||
PREM_V1_ENDPOINT: ${{ secrets.PREM_V1_ENDPOINT }}
|
||||
|
||||
steps:
|
||||
- name: 'Get sources'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: 'true'
|
||||
fetch-depth: 10
|
||||
|
||||
- uses: ./.github/actions/apple-setup-provisioning-profile
|
||||
with:
|
||||
provisioning_profile_base64: ${{ secrets.MAC_APP_PROVISIONING_PROFILE }}
|
||||
@@ -661,13 +623,19 @@ jobs:
|
||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||
gomobile init
|
||||
|
||||
- name: 'Get sources'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: 'true'
|
||||
fetch-depth: 10
|
||||
|
||||
- name: 'Setup python'
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.14
|
||||
|
||||
- name: 'Install conan'
|
||||
run: pip install "conan==2.28.0"
|
||||
run: pip install "conan==2.26.2"
|
||||
|
||||
- name: 'Build project'
|
||||
run: |
|
||||
@@ -703,7 +671,7 @@ jobs:
|
||||
python-version: 3.14
|
||||
|
||||
- name: 'Install conan'
|
||||
run: pip install "conan==2.28.0"
|
||||
run: pip install "conan==2.26.2"
|
||||
|
||||
- name: 'Setup Android SDK'
|
||||
uses: android-actions/setup-android@v4
|
||||
@@ -728,11 +696,9 @@ jobs:
|
||||
done
|
||||
|
||||
- name: 'Authorize in remote'
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
|
||||
|
||||
- name: 'Upload baked prebuilts'
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: conan upload -r amnezia "*" -c
|
||||
|
||||
# ------------------------------------------------------
|
||||
@@ -746,7 +712,7 @@ jobs:
|
||||
env:
|
||||
ANDROID_PLATFORM: android-28
|
||||
NDK_VERSION: 27.0.11718014
|
||||
QT_VERSION: 6.10.3
|
||||
QT_VERSION: 6.10.1
|
||||
QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools'
|
||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||
@@ -840,7 +806,7 @@ jobs:
|
||||
python-version: 3.14
|
||||
|
||||
- name: 'Install conan'
|
||||
run: pip install "conan==2.28.0"
|
||||
run: pip install "conan==2.26.2"
|
||||
|
||||
- name: 'Decode keystore secret to file'
|
||||
env:
|
||||
@@ -928,4 +894,3 @@ jobs:
|
||||
run: |
|
||||
echo "Pull request:" >> $GITHUB_STEP_SUMMARY
|
||||
echo "[[#${{ fromJSON(steps.pull_request.outputs.data)[0].number }}] ${{ fromJSON(steps.pull_request.outputs.data)[0].title }}](${{ fromJSON(steps.pull_request.outputs.data)[0].html_url }})" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -7,6 +7,9 @@
|
||||
[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
|
||||
|
||||
@@ -4,7 +4,7 @@ set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
set(PROJECT AmneziaVPN)
|
||||
set(AMNEZIAVPN_VERSION 4.9.0.3)
|
||||
set(AMNEZIAVPN_VERSION 4.9.0.2)
|
||||
|
||||
set(QT_CREATOR_SKIP_PACKAGE_MANAGER_SETUP ON CACHE BOOL "" FORCE)
|
||||
set(CMAKE_PROJECT_TOP_LEVEL_INCLUDES
|
||||
@@ -18,9 +18,9 @@ project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
|
||||
HOMEPAGE_URL "https://amnezia.org/"
|
||||
)
|
||||
|
||||
# trigger conan to kick off `conan install` globally
|
||||
find_package(OpenSSL REQUIRED)
|
||||
if (PREBUILTS_ONLY)
|
||||
# trigger conan to kick off `conan install`
|
||||
find_package(OpenSSL REQUIRED)
|
||||
return()
|
||||
endif()
|
||||
|
||||
@@ -28,7 +28,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
|
||||
set(RELEASE_DATE "${CURRENT_DATE}")
|
||||
|
||||
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
|
||||
set(APP_ANDROID_VERSION_CODE 2132)
|
||||
set(APP_ANDROID_VERSION_CODE 2120)
|
||||
|
||||
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
||||
set(MZ_PLATFORM_NAME "linux")
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
- Простой в использовании — введите 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/) и XRay.
|
||||
- Протоколы с маскировкой трафика (обфускацией): OpenVPN с плагином [Cloak](https://github.com/cbeuw/Cloak), Shadowsocks (OpenVPN over Shadowsocks), [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) and XRay.
|
||||
- Поддержка Split Tunneling — добавляйте любые сайты или приложения в список, чтобы включить VPN только для них.
|
||||
- Поддерживает платформы: Windows, macOS, Linux, Android, iOS.
|
||||
- Поддержка конфигурации протокола AmneziaWG на [бета-прошивке Keenetic](https://docs.keenetic.com/ua/air/kn-1611/en/6319-latest-development-release.html#UUID-186c4108-5afd-c10b-f38a-cdff6c17fab3_section-idm33192196168192-improved).
|
||||
|
||||
1
client/3rd/QSimpleCrypto
vendored
Submodule
1
client/3rd/QSimpleCrypto
vendored
Submodule
Submodule client/3rd/QSimpleCrypto added at c99b33f0e0
@@ -103,7 +103,6 @@ include(${CMAKE_CURRENT_LIST_DIR}/cmake/sources.cmake)
|
||||
include_directories(
|
||||
${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}
|
||||
)
|
||||
@@ -194,6 +193,10 @@ elseif(APPLE)
|
||||
include(cmake/macos.cmake)
|
||||
endif()
|
||||
|
||||
if(NOT IOS AND NOT ANDROID AND NOT MACOS_NE)
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
|
||||
list(APPEND SOURCES ${CMAKE_CURRENT_LIST_DIR}/main.cpp)
|
||||
|
||||
target_link_libraries(${PROJECT} PRIVATE ${LIBS})
|
||||
@@ -213,32 +216,11 @@ 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]]
|
||||
[[^[Qq]t.*]]
|
||||
POST_EXCLUDE_REGEXES
|
||||
[[^.*[\\/]system32[\\/].*\.dll$]]
|
||||
[[^/lib.*]]
|
||||
[[^/usr/lib.*]]
|
||||
DIRECTORIES ${CONAN_RUNTIME_LIB_DIRS}
|
||||
install(FILES $<TARGET_RUNTIME_DLLS:${PROJECT}>
|
||||
DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
COMPONENT AmneziaVPN
|
||||
DESTINATION "${RUNTIME_DEPS_DIR}"
|
||||
)
|
||||
|
||||
set(deploy_tool_options "")
|
||||
@@ -258,10 +240,6 @@ install(SCRIPT ${QT_DEPLOY_SCRIPT}
|
||||
|
||||
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")
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
#include "logger.h"
|
||||
#include "ui/controllers/qml/pageController.h"
|
||||
#include "ui/models/installedAppsModel.h"
|
||||
#include "ui/utils/mtProxyPublicHostInput.h"
|
||||
#include "version.h"
|
||||
|
||||
#include "platforms/ios/QRCodeReaderBase.h"
|
||||
@@ -110,6 +109,9 @@ void AmneziaApplication::init()
|
||||
// install filter on main window
|
||||
if (auto win = qobject_cast<QQuickWindow*>(obj)) {
|
||||
win->installEventFilter(this);
|
||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
||||
win->setDefaultAlphaBuffer(true);
|
||||
#endif
|
||||
#ifdef Q_OS_ANDROID
|
||||
QObject::connect(win, &QQuickWindow::sceneGraphError,
|
||||
[](QQuickWindow::SceneGraphError, const QString &msg) {
|
||||
@@ -120,13 +122,7 @@ void AmneziaApplication::init()
|
||||
win->setPersistentSceneGraph(true);
|
||||
win->setPersistentGraphics(true);
|
||||
#endif
|
||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
||||
win->show();
|
||||
#else
|
||||
if (!m_coreController || !m_coreController->pageController()->shouldStartMinimized()) {
|
||||
win->show();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
@@ -222,9 +218,6 @@ 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();
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<?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>
|
||||
@@ -1,7 +0,0 @@
|
||||
<?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%" />
|
||||
10
client/android/res/drawable/ic_pairing_back.xml
Normal file
10
client/android/res/drawable/ic_pairing_back.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFE8E8EC"
|
||||
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z" />
|
||||
</vector>
|
||||
5
client/android/res/drawable/torch_fab_bg.xml
Normal file
5
client/android/res/drawable/torch_fab_bg.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="#38FFFFFF" />
|
||||
</shape>
|
||||
@@ -8,4 +8,75 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<org.amnezia.vpn.PairingQrScanOverlayView
|
||||
android:id="@+id/pairingScanOverlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/pairingChrome"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="top"
|
||||
android:background="@android:color/transparent"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingTop="28dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/pairingBack"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="top"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/pairing_qr_camera_back"
|
||||
android:padding="12dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_pairing_back" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pairingTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/pairing_qr_camera_title"
|
||||
android:textColor="#FFE8E8EC"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pairingSubtitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/pairing_qr_camera_subtitle"
|
||||
android:textColor="#FFB8B8C0"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/torchButton"
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="56dp"
|
||||
android:layout_gravity="bottom|center_horizontal"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:background="@drawable/torch_fab_bg"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center"
|
||||
android:text="🔦"
|
||||
android:textSize="26sp"
|
||||
android:contentDescription="@string/camera_torch" />
|
||||
|
||||
</FrameLayout>
|
||||
@@ -1,6 +0,0 @@
|
||||
<?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>
|
||||
@@ -1,6 +0,0 @@
|
||||
<?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.
|
Before Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 35 KiB |
@@ -24,5 +24,13 @@
|
||||
<string name="notificationSettingsDialogMessage">Для показа уведомлений необходимо включить уведомления в системных настройках</string>
|
||||
<string name="openNotificationSettings">Открыть настройки уведомлений</string>
|
||||
|
||||
<string name="cameraPermissionDialogTitle">Доступ к камере</string>
|
||||
<string name="cameraPermissionDialogMessage">Чтобы отсканировать QR-код для добавления устройства, Amnezia VPN нужен доступ к камере.</string>
|
||||
<string name="cameraPermissionContinue">Продолжить</string>
|
||||
<string name="camera_torch">Фонарик</string>
|
||||
<string name="pairing_qr_camera_title">Добавить устройство по QR</string>
|
||||
<string name="pairing_qr_camera_subtitle">Отсканируйте QR сессии на устройстве, которое хотите добавить. Перед отправкой подписки будет подтверждение.</string>
|
||||
<string name="pairing_qr_camera_back">Назад</string>
|
||||
|
||||
<string name="tvNoFileBrowser">Пожалуйста, установите приложение для просмотра файлов</string>
|
||||
</resources>
|
||||
@@ -1,16 +0,0 @@
|
||||
<?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>
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#0E0E11</color>
|
||||
</resources>
|
||||
@@ -24,5 +24,13 @@
|
||||
<string name="notificationSettingsDialogMessage">To show notifications, you must enable notifications in the system settings</string>
|
||||
<string name="openNotificationSettings">Open notification settings</string>
|
||||
|
||||
<string name="cameraPermissionDialogTitle">Camera access</string>
|
||||
<string name="cameraPermissionDialogMessage">To scan a QR code for device pairing, Amnezia VPN needs access to the camera.</string>
|
||||
<string name="cameraPermissionContinue">Continue</string>
|
||||
<string name="camera_torch">Flashlight</string>
|
||||
<string name="pairing_qr_camera_title">Add device via QR</string>
|
||||
<string name="pairing_qr_camera_subtitle">Scan the session QR shown on the device you want to add. You will confirm before the subscription is sent.</string>
|
||||
<string name="pairing_qr_camera_back">Back</string>
|
||||
|
||||
<string name="tvNoFileBrowser">Please install a file management utility to browse files</string>
|
||||
</resources>
|
||||
@@ -42,7 +42,9 @@ 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 androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LifecycleRegistry
|
||||
import java.io.IOException
|
||||
import kotlin.LazyThreadSafetyMode.NONE
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
@@ -74,12 +76,18 @@ private const val CHECK_VPN_PERMISSION_ACTION_CODE = 1
|
||||
private const val CREATE_FILE_ACTION_CODE = 2
|
||||
private const val OPEN_FILE_ACTION_CODE = 3
|
||||
private const val CHECK_NOTIFICATION_PERMISSION_ACTION_CODE = 4
|
||||
private const val CHECK_CAMERA_PERMISSION_ACTION_CODE = 5
|
||||
|
||||
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"
|
||||
|
||||
class AmneziaActivity : QtActivity() {
|
||||
class AmneziaActivity : QtActivity(), LifecycleOwner {
|
||||
|
||||
private val lifecycleRegistry = LifecycleRegistry(this)
|
||||
|
||||
override val lifecycle: Lifecycle
|
||||
get() = lifecycleRegistry
|
||||
|
||||
private lateinit var mainScope: CoroutineScope
|
||||
private val qtInitialized = CompletableDeferred<Unit>()
|
||||
@@ -100,6 +108,8 @@ class AmneziaActivity : QtActivity() {
|
||||
private var pendingOpenFileUri: String? = null
|
||||
private var openFileDeliveryScheduled = false
|
||||
|
||||
private var lastPairingQrReaderStartUptimeMs: Long = 0L
|
||||
|
||||
private val vpnServiceEventHandler: Handler by lazy(NONE) {
|
||||
object : Handler(Looper.getMainLooper()) {
|
||||
override fun handleMessage(msg: Message) {
|
||||
@@ -206,6 +216,7 @@ class AmneziaActivity : QtActivity() {
|
||||
registerBroadcastReceivers()
|
||||
intent?.let(::processIntent)
|
||||
runBlocking { vpnProto = proto.await() }
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
@@ -263,6 +274,7 @@ class AmneziaActivity : QtActivity() {
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
|
||||
Log.d(TAG, "Start Amnezia activity")
|
||||
mainScope.launch {
|
||||
qtInitialized.await()
|
||||
@@ -286,6 +298,7 @@ class AmneziaActivity : QtActivity() {
|
||||
qtInitialized.await()
|
||||
QtAndroidController.onServiceDisconnected()
|
||||
}
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
@@ -358,6 +371,7 @@ class AmneziaActivity : QtActivity() {
|
||||
if (qtInitialized.isCompleted) {
|
||||
QtAndroidController.onActivityPaused()
|
||||
}
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
|
||||
super.onPause()
|
||||
isActivityResumed = false
|
||||
// Cancel all pending operations when activity pauses
|
||||
@@ -368,6 +382,7 @@ class AmneziaActivity : QtActivity() {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
||||
isActivityResumed = true
|
||||
Log.d(TAG, "Resume Amnezia activity")
|
||||
if (qtInitialized.isCompleted) {
|
||||
@@ -484,6 +499,7 @@ class AmneziaActivity : QtActivity() {
|
||||
unregisterBroadcastReceiver(notificationStateReceiver)
|
||||
notificationStateReceiver = null
|
||||
mainScope.cancel()
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
@@ -764,13 +780,7 @@ class AmneziaActivity : QtActivity() {
|
||||
fun openFile(filter: String?) {
|
||||
Log.v(TAG, "Open file with filter: $filter")
|
||||
mainScope.launch {
|
||||
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 intent = if (!isOnTv()) {
|
||||
val mimeTypes = if (!filter.isNullOrEmpty()) {
|
||||
val extensionRegex = "\\*\\.([a-z0-9]+)".toRegex(IGNORE_CASE)
|
||||
val mime = MimeTypeMap.getSingleton()
|
||||
@@ -796,7 +806,6 @@ class AmneziaActivity : QtActivity() {
|
||||
else -> type = "*/*"
|
||||
}
|
||||
}
|
||||
`package` = systemPickerPackage
|
||||
}
|
||||
} else {
|
||||
Intent(this@AmneziaActivity, TvFilePicker::class.java)
|
||||
@@ -808,11 +817,8 @@ class AmneziaActivity : QtActivity() {
|
||||
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) {}
|
||||
}
|
||||
u
|
||||
val uri = it?.data?.apply {
|
||||
grantUriPermission(packageName, this, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}?.toString() ?: ""
|
||||
Log.v(TAG, "Open file: $uri")
|
||||
if (uri.isNotEmpty()) {
|
||||
@@ -852,12 +858,7 @@ class AmneziaActivity : QtActivity() {
|
||||
Log.v(TAG, "Get fd for $fileName")
|
||||
return blockingCall(Dispatchers.IO) {
|
||||
try {
|
||||
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 = contentResolver.openFileDescriptor(Uri.parse(fileName), "r")
|
||||
pfd?.fd ?: -1
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to get fd: $e")
|
||||
@@ -896,6 +897,66 @@ class AmneziaActivity : QtActivity() {
|
||||
@SuppressLint("UnsupportedChromeOsCameraSystemFeature")
|
||||
fun isCameraPresent(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA)
|
||||
|
||||
@Suppress("unused")
|
||||
fun isCameraPermissionGranted(): Boolean =
|
||||
ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
|
||||
|
||||
@Suppress("unused")
|
||||
fun requestCameraPermissionForQrPairing() {
|
||||
if (isCameraPermissionGranted()) {
|
||||
mainScope.launch {
|
||||
qtInitialized.await()
|
||||
QtAndroidController.onCameraPermissionResult(true)
|
||||
}
|
||||
return
|
||||
}
|
||||
runOnUiThread {
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.cameraPermissionDialogTitle)
|
||||
.setMessage(R.string.cameraPermissionDialogMessage)
|
||||
.setNegativeButton(R.string.cancel) { _, _ ->
|
||||
mainScope.launch {
|
||||
qtInitialized.await()
|
||||
QtAndroidController.onCameraPermissionResult(false)
|
||||
}
|
||||
}
|
||||
.setPositiveButton(R.string.cameraPermissionContinue) { _, _ ->
|
||||
requestPermission(
|
||||
Manifest.permission.CAMERA,
|
||||
CHECK_CAMERA_PERMISSION_ACTION_CODE,
|
||||
PermissionRequestHandler(
|
||||
onSuccess = {
|
||||
mainScope.launch {
|
||||
qtInitialized.await()
|
||||
QtAndroidController.onCameraPermissionResult(true)
|
||||
}
|
||||
},
|
||||
onFail = {
|
||||
mainScope.launch {
|
||||
qtInitialized.await()
|
||||
QtAndroidController.onCameraPermissionResult(false)
|
||||
}
|
||||
},
|
||||
onAny = {}
|
||||
)
|
||||
)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun openApplicationDetailsSettings() {
|
||||
try {
|
||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||
data = Uri.fromParts("package", packageName, null)
|
||||
startActivity(this)
|
||||
}
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Log.e(TAG, "openApplicationDetailsSettings: $e")
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun isOnTv(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
||||
|
||||
@@ -944,6 +1005,19 @@ class AmneziaActivity : QtActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun startPairingQrCodeReader() {
|
||||
val now = SystemClock.uptimeMillis()
|
||||
if (now - lastPairingQrReaderStartUptimeMs < 1200L) {
|
||||
return
|
||||
}
|
||||
lastPairingQrReaderStartUptimeMs = now
|
||||
Intent(this, CameraActivity::class.java).also {
|
||||
it.putExtra(CameraActivity.EXTRA_PAIRING_QR_CAMERA, true)
|
||||
startActivity(it)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun setSaveLogs(enabled: Boolean) {
|
||||
Log.v(TAG, "Set save logs: $enabled")
|
||||
@@ -1077,10 +1151,12 @@ class AmneziaActivity : QtActivity() {
|
||||
@Suppress("unused")
|
||||
fun sendTouch(x: Float, y: Float) {
|
||||
Log.v(TAG, "Send touch: $x, $y")
|
||||
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))
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1193,6 +1269,7 @@ class AmneziaActivity : QtActivity() {
|
||||
CREATE_FILE_ACTION_CODE -> "CREATE_FILE"
|
||||
OPEN_FILE_ACTION_CODE -> "OPEN_FILE"
|
||||
CHECK_NOTIFICATION_PERMISSION_ACTION_CODE -> "CHECK_NOTIFICATION_PERMISSION"
|
||||
CHECK_CAMERA_PERMISSION_ACTION_CODE -> "CHECK_CAMERA_PERMISSION"
|
||||
else -> actionCode.toString()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,47 +2,384 @@ package org.amnezia.vpn
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.os.Bundle
|
||||
import android.view.MotionEvent.ACTION_DOWN
|
||||
import android.view.MotionEvent.ACTION_UP
|
||||
import android.graphics.RectF
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
|
||||
import androidx.camera.core.Camera
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.camera.core.ExperimentalGetImage
|
||||
import androidx.camera.core.FocusMeteringAction
|
||||
import androidx.camera.core.FocusMeteringAction.FLAG_AE
|
||||
import androidx.camera.core.FocusMeteringAction.FLAG_AF
|
||||
import androidx.camera.core.ImageAnalysis
|
||||
import androidx.camera.core.ImageProxy
|
||||
import androidx.camera.core.Preview
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import androidx.camera.view.PreviewView
|
||||
import androidx.camera.view.TransformExperimental
|
||||
import androidx.camera.view.transform.CoordinateTransform
|
||||
import androidx.camera.view.transform.ImageProxyTransformFactory
|
||||
import androidx.camera.view.transform.OutputTransform
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.lifecycle.Observer
|
||||
import com.google.mlkit.vision.barcode.BarcodeScanner
|
||||
import com.google.mlkit.vision.barcode.BarcodeScannerOptions.Builder
|
||||
import com.google.mlkit.vision.barcode.BarcodeScanning
|
||||
import com.google.mlkit.vision.barcode.ZoomSuggestionOptions
|
||||
import com.google.mlkit.vision.barcode.common.Barcode
|
||||
import com.google.mlkit.vision.common.InputImage
|
||||
import org.amnezia.vpn.databinding.CameraPreviewBinding
|
||||
import org.amnezia.vpn.qt.QtAndroidController
|
||||
import org.amnezia.vpn.util.Log
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private const val TAG = "CameraActivity"
|
||||
|
||||
@OptIn(TransformExperimental::class)
|
||||
class CameraActivity : ComponentActivity() {
|
||||
|
||||
companion object {
|
||||
const val EXTRA_PAIRING_QR_CAMERA = "org.amnezia.vpn.extra.PAIRING_QR_CAMERA"
|
||||
}
|
||||
|
||||
private lateinit var viewBinding: CameraPreviewBinding
|
||||
private lateinit var cameraProvider: ProcessCameraProvider
|
||||
private var cameraProvider: ProcessCameraProvider? = null
|
||||
private var boundCamera: Camera? = null
|
||||
private var boundImageAnalysis: ImageAnalysis? = null
|
||||
private var torchOn: Boolean = false
|
||||
|
||||
private var imageAnalysisExecutor: ExecutorService? = null
|
||||
|
||||
private val qrHandledOrClosing = AtomicBoolean(false)
|
||||
|
||||
private var pairingQrDeliveredToQt = false
|
||||
|
||||
private var pairingQrUserDismissedCamera = false
|
||||
|
||||
private var barcodeScanner: BarcodeScanner? = null
|
||||
|
||||
private val cachedPreviewOutputTransform = AtomicReference<OutputTransform?>(null)
|
||||
|
||||
private var previewTransformLayoutListener: View.OnLayoutChangeListener? = null
|
||||
|
||||
private var previewStreamStateObserver: Observer<PreviewView.StreamState>? = null
|
||||
|
||||
@Volatile
|
||||
private var pairingGeomHeaderBottomPx = 0f
|
||||
|
||||
@Volatile
|
||||
private var pairingGeomStatusBarTopPx = 0f
|
||||
|
||||
@Volatile
|
||||
private var pairingGeomDensity = 1f
|
||||
|
||||
@ExperimentalGetImage
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
viewBinding = CameraPreviewBinding.inflate(layoutInflater)
|
||||
setContentView(viewBinding.root)
|
||||
viewBinding.viewFinder.scaleType = PreviewView.ScaleType.FILL_CENTER
|
||||
|
||||
if (intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)) {
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
val density = resources.displayMetrics.density
|
||||
val padH = (8 * density).toInt()
|
||||
val padTopBase = (28 * density).toInt()
|
||||
val padBottom = (12 * density).toInt()
|
||||
ViewCompat.setOnApplyWindowInsetsListener(viewBinding.pairingChrome) { v, windowInsets ->
|
||||
val bars = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars())
|
||||
v.setPadding(padH, padTopBase + bars.top, (16 * density).toInt(), padBottom)
|
||||
v.post { onPairingLayoutGeometryChanged() }
|
||||
windowInsets
|
||||
}
|
||||
viewBinding.pairingScanOverlay.visibility = View.VISIBLE
|
||||
viewBinding.pairingChrome.visibility = View.VISIBLE
|
||||
viewBinding.root.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
|
||||
viewBinding.root.post { onPairingLayoutGeometryChanged() }
|
||||
}
|
||||
viewBinding.root.post {
|
||||
onPairingLayoutGeometryChanged()
|
||||
applyPairingTorchButtonChrome()
|
||||
}
|
||||
}
|
||||
|
||||
viewBinding.pairingBack.setOnClickListener { releaseCameraAndFinish() }
|
||||
|
||||
onBackPressedDispatcher.addCallback(
|
||||
this,
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
releaseCameraAndFinish()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
viewBinding.torchButton.setOnClickListener {
|
||||
torchOn = !torchOn
|
||||
try {
|
||||
boundCamera?.cameraControl?.enableTorch(torchOn)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Torch: $e")
|
||||
}
|
||||
if (intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)) {
|
||||
applyPairingTorchButtonChrome()
|
||||
}
|
||||
}
|
||||
|
||||
checkPermissions(onSuccess = ::startCamera, onFail = ::finish)
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
if (!intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)) {
|
||||
return
|
||||
}
|
||||
if (!::viewBinding.isInitialized) {
|
||||
return
|
||||
}
|
||||
cleanupCameraResources()
|
||||
qrHandledOrClosing.set(false)
|
||||
pairingQrDeliveredToQt = false
|
||||
pairingQrUserDismissedCamera = false
|
||||
torchOn = false
|
||||
viewBinding.pairingScanOverlay.visibility = View.VISIBLE
|
||||
viewBinding.pairingChrome.visibility = View.VISIBLE
|
||||
viewBinding.root.post {
|
||||
onPairingLayoutGeometryChanged()
|
||||
applyPairingTorchButtonChrome()
|
||||
}
|
||||
checkPermissions(onSuccess = ::startCamera, onFail = ::finish)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
cleanupCameraResources()
|
||||
val pairing = intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)
|
||||
if (pairing && !pairingQrDeliveredToQt && !pairingQrUserDismissedCamera) {
|
||||
try {
|
||||
QtAndroidController.onPairingQrCameraClosed()
|
||||
} catch (t: Throwable) {
|
||||
Log.e(TAG, "onPairingQrCameraClosed: $t")
|
||||
}
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
/** Idempotent: safe from back, successful decode, or process death. */
|
||||
private fun cleanupCameraResources() {
|
||||
qrHandledOrClosing.set(true)
|
||||
try {
|
||||
boundImageAnalysis?.clearAnalyzer()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
boundImageAnalysis = null
|
||||
try {
|
||||
barcodeScanner?.close()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
barcodeScanner = null
|
||||
try {
|
||||
boundCamera?.cameraControl?.enableTorch(false)
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
boundCamera = null
|
||||
try {
|
||||
cameraProvider?.unbindAll()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
imageAnalysisExecutor?.let { ex ->
|
||||
try {
|
||||
ex.shutdown()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
imageAnalysisExecutor = null
|
||||
previewTransformLayoutListener?.let { listener ->
|
||||
if (::viewBinding.isInitialized) {
|
||||
viewBinding.viewFinder.removeOnLayoutChangeListener(listener)
|
||||
}
|
||||
}
|
||||
previewTransformLayoutListener = null
|
||||
previewStreamStateObserver?.let { obs ->
|
||||
if (::viewBinding.isInitialized) {
|
||||
viewBinding.viewFinder.previewStreamState.removeObserver(obs)
|
||||
}
|
||||
}
|
||||
previewStreamStateObserver = null
|
||||
cachedPreviewOutputTransform.set(null)
|
||||
}
|
||||
|
||||
private fun refreshCachedPreviewOutputTransform() {
|
||||
if (!::viewBinding.isInitialized) {
|
||||
return
|
||||
}
|
||||
val vf = viewBinding.viewFinder
|
||||
try {
|
||||
val out = vf.outputTransform
|
||||
cachedPreviewOutputTransform.set(out)
|
||||
} catch (t: Throwable) {
|
||||
Log.e(TAG, "refreshCachedPreviewOutputTransform: $t")
|
||||
cachedPreviewOutputTransform.set(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun scheduleCachedPreviewOutputTransformRefresh() {
|
||||
if (!::viewBinding.isInitialized) {
|
||||
return
|
||||
}
|
||||
viewBinding.viewFinder.post { refreshCachedPreviewOutputTransform() }
|
||||
}
|
||||
|
||||
private fun onPairingLayoutGeometryChanged() {
|
||||
if (!::viewBinding.isInitialized || !intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)) {
|
||||
return
|
||||
}
|
||||
val root = viewBinding.root
|
||||
val chrome = viewBinding.pairingChrome
|
||||
val w = root.width
|
||||
val h = root.height
|
||||
if (w <= 0 || h <= 0) {
|
||||
return
|
||||
}
|
||||
val density = resources.displayMetrics.density
|
||||
val headerBottom = if (chrome.visibility == View.VISIBLE) chrome.bottom.toFloat() else 0f
|
||||
val insets = ViewCompat.getRootWindowInsets(root)
|
||||
val statusTop = insets?.getInsets(WindowInsetsCompat.Type.statusBars())?.top?.toFloat() ?: 0f
|
||||
val safeBottom = insets?.getInsets(WindowInsetsCompat.Type.systemBars())?.bottom?.toFloat() ?: 0f
|
||||
|
||||
pairingGeomHeaderBottomPx = headerBottom
|
||||
pairingGeomStatusBarTopPx = statusTop
|
||||
pairingGeomDensity = density
|
||||
|
||||
viewBinding.pairingScanOverlay.setPairingHeaderBottomPx(headerBottom)
|
||||
|
||||
val hole = PairingQrScanGeometry.pairingIosStyleHoleRectF(w, h, headerBottom, statusTop, density)
|
||||
val torchCy = PairingQrScanGeometry.pairingIosStyleTorchCenterYPx(
|
||||
hole.bottom,
|
||||
h.toFloat(),
|
||||
headerBottom,
|
||||
safeBottom,
|
||||
density
|
||||
)
|
||||
val torchSizePx = (56f * density).roundToInt().coerceAtLeast(1)
|
||||
val topMargin = (torchCy - torchSizePx / 2f).roundToInt().coerceAtLeast(0)
|
||||
val wantGravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
|
||||
viewBinding.torchButton.post {
|
||||
if (!::viewBinding.isInitialized || !intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)) {
|
||||
return@post
|
||||
}
|
||||
val btn = viewBinding.torchButton
|
||||
val lp = btn.layoutParams as FrameLayout.LayoutParams
|
||||
if (lp.gravity == wantGravity && lp.topMargin == topMargin && lp.bottomMargin == 0) {
|
||||
return@post
|
||||
}
|
||||
lp.gravity = wantGravity
|
||||
lp.topMargin = topMargin
|
||||
lp.bottomMargin = 0
|
||||
btn.layoutParams = lp
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyPairingTorchButtonChrome() {
|
||||
if (!::viewBinding.isInitialized || !intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)) {
|
||||
return
|
||||
}
|
||||
val btn = viewBinding.torchButton
|
||||
val d = resources.displayMetrics.density
|
||||
val alpha = if (torchOn) (0.42f * 255f).toInt() else (0.22f * 255f).toInt()
|
||||
val bg = GradientDrawable().apply {
|
||||
shape = GradientDrawable.OVAL
|
||||
setColor(Color.argb(alpha, 255, 255, 255))
|
||||
if (torchOn) {
|
||||
setStroke((2f * d).roundToInt(), Color.rgb(255, 191, 115))
|
||||
} else {
|
||||
setStroke(0, 0)
|
||||
}
|
||||
}
|
||||
btn.background = bg
|
||||
}
|
||||
|
||||
private fun pairingHoleRectInImageSpace(
|
||||
viewFinder: PreviewView,
|
||||
imageProxy: ImageProxy,
|
||||
imageWidth: Int,
|
||||
imageHeight: Int
|
||||
): RectF {
|
||||
val vw = viewFinder.width
|
||||
val vh = viewFinder.height
|
||||
fun geomFallback(): RectF =
|
||||
PairingQrScanGeometry.pairingIosStyleHoleInImageCoords(
|
||||
vw,
|
||||
vh,
|
||||
pairingGeomHeaderBottomPx,
|
||||
pairingGeomStatusBarTopPx,
|
||||
pairingGeomDensity,
|
||||
imageWidth,
|
||||
imageHeight
|
||||
)
|
||||
if (vw <= 0 || vh <= 0 || imageWidth <= 0 || imageHeight <= 0) {
|
||||
return geomFallback()
|
||||
}
|
||||
return try {
|
||||
val previewOut = cachedPreviewOutputTransform.get()
|
||||
if (previewOut == null) {
|
||||
geomFallback()
|
||||
} else {
|
||||
val imageFactory = ImageProxyTransformFactory().apply {
|
||||
setUsingRotationDegrees(true)
|
||||
}
|
||||
val imageOut = imageFactory.getOutputTransform(imageProxy)
|
||||
val holeView = PairingQrScanGeometry.pairingIosStyleHoleRectF(
|
||||
vw,
|
||||
vh,
|
||||
pairingGeomHeaderBottomPx,
|
||||
pairingGeomStatusBarTopPx,
|
||||
pairingGeomDensity
|
||||
)
|
||||
if (holeView.width() <= 0f || holeView.height() <= 0f) {
|
||||
return geomFallback()
|
||||
}
|
||||
val hole = RectF(holeView)
|
||||
CoordinateTransform(previewOut, imageOut).mapRect(hole)
|
||||
hole
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
Log.e(TAG, "pairingHoleRectInImageSpace: $t")
|
||||
geomFallback()
|
||||
}
|
||||
}
|
||||
|
||||
private fun releaseCameraAndFinish() {
|
||||
if (intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)) {
|
||||
pairingQrUserDismissedCamera = true
|
||||
try {
|
||||
QtAndroidController.onPairingQrCameraUserDismissed()
|
||||
} catch (t: Throwable) {
|
||||
Log.e(TAG, "onPairingQrCameraUserDismissed: $t")
|
||||
}
|
||||
}
|
||||
cleanupCameraResources()
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun checkPermissions(onSuccess: () -> Unit, onFail: () -> Unit) {
|
||||
if (checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
||||
onSuccess()
|
||||
@@ -67,26 +404,41 @@ class CameraActivity : ComponentActivity() {
|
||||
|
||||
cameraProviderFuture.addListener({
|
||||
cameraProvider = cameraProviderFuture.get()
|
||||
bindPreview()
|
||||
bindImageAnalysis()
|
||||
bindCameraUseCases()
|
||||
}, ContextCompat.getMainExecutor(this))
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private fun bindPreview() {
|
||||
@ExperimentalGetImage
|
||||
private fun bindCameraUseCases() {
|
||||
val provider = cameraProvider ?: return
|
||||
imageAnalysisExecutor?.shutdown()
|
||||
imageAnalysisExecutor = Executors.newSingleThreadExecutor()
|
||||
|
||||
val viewFinder = viewBinding.viewFinder
|
||||
val preview = Preview.Builder().build().also {
|
||||
it.setSurfaceProvider(viewFinder.surfaceProvider)
|
||||
}
|
||||
|
||||
val camera = cameraProvider.bindToLifecycle(this, CameraSelector.DEFAULT_BACK_CAMERA, preview)
|
||||
val imageAnalysis = ImageAnalysis.Builder()
|
||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||
.build()
|
||||
|
||||
val camera = provider.bindToLifecycle(
|
||||
this,
|
||||
CameraSelector.DEFAULT_BACK_CAMERA,
|
||||
preview,
|
||||
imageAnalysis
|
||||
)
|
||||
boundCamera = camera
|
||||
boundImageAnalysis = imageAnalysis
|
||||
|
||||
viewFinder.setOnTouchListener { _, motionEvent ->
|
||||
when (motionEvent.action) {
|
||||
ACTION_DOWN -> true
|
||||
ACTION_UP -> {
|
||||
val point = viewFinder
|
||||
.meteringPointFactory.createPoint(motionEvent.x, motionEvent.x)
|
||||
.meteringPointFactory.createPoint(motionEvent.x, motionEvent.y)
|
||||
|
||||
val action = FocusMeteringAction
|
||||
.Builder(point, FLAG_AF or FLAG_AE).build()
|
||||
@@ -98,58 +450,121 @@ class CameraActivity : ComponentActivity() {
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ExperimentalGetImage
|
||||
private fun bindImageAnalysis() {
|
||||
val imageAnalysis = ImageAnalysis.Builder().build()
|
||||
if (intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)) {
|
||||
previewTransformLayoutListener?.let { viewFinder.removeOnLayoutChangeListener(it) }
|
||||
val layoutListener = View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
|
||||
viewFinder.post {
|
||||
scheduleCachedPreviewOutputTransformRefresh()
|
||||
onPairingLayoutGeometryChanged()
|
||||
}
|
||||
}
|
||||
previewTransformLayoutListener = layoutListener
|
||||
viewFinder.addOnLayoutChangeListener(layoutListener)
|
||||
previewStreamStateObserver?.let { viewFinder.previewStreamState.removeObserver(it) }
|
||||
val streamObserver = Observer<PreviewView.StreamState> { state ->
|
||||
if (state == PreviewView.StreamState.STREAMING) {
|
||||
viewFinder.post {
|
||||
scheduleCachedPreviewOutputTransformRefresh()
|
||||
onPairingLayoutGeometryChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
previewStreamStateObserver = streamObserver
|
||||
viewFinder.previewStreamState.observe(this, streamObserver)
|
||||
scheduleCachedPreviewOutputTransformRefresh()
|
||||
}
|
||||
|
||||
val camera = cameraProvider.bindToLifecycle(this, CameraSelector.DEFAULT_BACK_CAMERA, imageAnalysis)
|
||||
try {
|
||||
barcodeScanner?.close()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
|
||||
val barcodeScanner = BarcodeScanning.getClient(
|
||||
barcodeScanner = BarcodeScanning.getClient(
|
||||
Builder()
|
||||
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
|
||||
.setZoomSuggestionOptions(
|
||||
ZoomSuggestionOptions.Builder { zoomLevel ->
|
||||
camera.cameraControl.setZoomRatio(zoomLevel)
|
||||
true
|
||||
}.apply {
|
||||
camera.cameraInfo.zoomState.value?.maxZoomRatio?.let { maxZoomRation ->
|
||||
setMaxSupportedZoomRatio(maxZoomRation)
|
||||
}
|
||||
}.build()
|
||||
).build()
|
||||
.build()
|
||||
)
|
||||
|
||||
// optimization
|
||||
val checkedBarcodes = hashSetOf<String>()
|
||||
val analysisExecutor = imageAnalysisExecutor!!
|
||||
val mainExecutor = ContextCompat.getMainExecutor(this)
|
||||
val pairingQrMode = intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)
|
||||
|
||||
imageAnalysis.setAnalyzer(ContextCompat.getMainExecutor(this)) { imageProxy ->
|
||||
imageProxy.image?.let { InputImage.fromMediaImage(it, imageProxy.imageInfo.rotationDegrees) }
|
||||
?.let { image ->
|
||||
barcodeScanner.process(image).addOnSuccessListener { barcodes ->
|
||||
barcodes.firstOrNull()?.let { barcode ->
|
||||
barcode.displayValue?.let { code ->
|
||||
if (code.isNotEmpty() && code !in checkedBarcodes) {
|
||||
if (QtAndroidController.decodeQrCode(code)) {
|
||||
barcodeScanner.close()
|
||||
imageAnalysis.setAnalyzer(analysisExecutor) { imageProxy ->
|
||||
if (qrHandledOrClosing.get()) {
|
||||
imageProxy.close()
|
||||
return@setAnalyzer
|
||||
}
|
||||
val mediaImage = imageProxy.image
|
||||
if (mediaImage == null) {
|
||||
imageProxy.close()
|
||||
return@setAnalyzer
|
||||
}
|
||||
val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
|
||||
val viewW = viewFinder.width
|
||||
val viewH = viewFinder.height
|
||||
val pairingRoi = if (pairingQrMode) {
|
||||
pairingHoleRectInImageSpace(viewFinder, imageProxy, image.width, image.height)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val scanner = barcodeScanner ?: run {
|
||||
imageProxy.close()
|
||||
return@setAnalyzer
|
||||
}
|
||||
scanner.process(image)
|
||||
.addOnSuccessListener(mainExecutor) { barcodes ->
|
||||
if (qrHandledOrClosing.get()) {
|
||||
return@addOnSuccessListener
|
||||
}
|
||||
val barcode = if (pairingQrMode) {
|
||||
val roi = pairingRoi
|
||||
?: PairingQrScanGeometry.pairingIosStyleHoleInImageCoords(
|
||||
viewW,
|
||||
viewH,
|
||||
pairingGeomHeaderBottomPx,
|
||||
pairingGeomStatusBarTopPx,
|
||||
pairingGeomDensity,
|
||||
image.width,
|
||||
image.height
|
||||
)
|
||||
barcodes.firstOrNull {
|
||||
PairingQrScanGeometry.barcodeMatchesPairingHole(
|
||||
roi,
|
||||
image.width,
|
||||
image.height,
|
||||
it
|
||||
)
|
||||
}
|
||||
} else {
|
||||
barcodes.firstOrNull()
|
||||
}
|
||||
barcode?.displayValue?.let { code ->
|
||||
if (code.isNotEmpty() && code !in checkedBarcodes) {
|
||||
checkedBarcodes.add(code)
|
||||
if (QtAndroidController.decodeQrCode(code)) {
|
||||
if (qrHandledOrClosing.compareAndSet(false, true)) {
|
||||
if (pairingQrMode) {
|
||||
pairingQrDeliveredToQt = true
|
||||
}
|
||||
stopCamera()
|
||||
}
|
||||
checkedBarcodes.add(code)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.addOnFailureListener {
|
||||
Log.e(TAG, "Processing QR code image failed: ${it.message}")
|
||||
}.addOnCompleteListener {
|
||||
imageProxy.close()
|
||||
}
|
||||
}
|
||||
.addOnFailureListener(mainExecutor) {
|
||||
Log.e(TAG, "Processing QR code image failed: ${it.message}")
|
||||
}
|
||||
.addOnCompleteListener(mainExecutor) {
|
||||
imageProxy.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopCamera() {
|
||||
cameraProvider.unbindAll()
|
||||
cleanupCameraResources()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
101
client/android/src/org/amnezia/vpn/PairingQrScanBracketPaths.kt
Normal file
101
client/android/src/org/amnezia/vpn/PairingQrScanBracketPaths.kt
Normal file
@@ -0,0 +1,101 @@
|
||||
package org.amnezia.vpn
|
||||
|
||||
import android.graphics.Path
|
||||
import android.graphics.RectF
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
object PairingQrScanBracketPaths {
|
||||
|
||||
private fun Path.addCornerMinorArc(
|
||||
cx: Float,
|
||||
cy: Float,
|
||||
r: Float,
|
||||
sx: Float,
|
||||
sy: Float,
|
||||
ex: Float,
|
||||
ey: Float
|
||||
) {
|
||||
var asRad = atan2((sy - cy).toDouble(), (sx - cx).toDouble())
|
||||
var aeRad = atan2((ey - cy).toDouble(), (ex - cx).toDouble())
|
||||
while (aeRad - asRad > PI) {
|
||||
aeRad -= 2.0 * PI
|
||||
}
|
||||
while (aeRad - asRad < -PI) {
|
||||
aeRad += 2.0 * PI
|
||||
}
|
||||
val minor = aeRad - asRad
|
||||
val startDeg = Math.toDegrees(asRad).toFloat()
|
||||
val sweepDeg = Math.toDegrees(minor).toFloat()
|
||||
addArc(RectF(cx - r, cy - r, cx + r, cy + r), startDeg, sweepDeg)
|
||||
}
|
||||
|
||||
fun bracketStrokePath(corner: Int, x0: Float, y0: Float, s: Float, R: Float, L: Float, t: Float): Path {
|
||||
val r = max(1.5f, R - t * 0.5f)
|
||||
val p = Path()
|
||||
val yy = y0 + t * 0.5f
|
||||
val yyb = y0 + s - t * 0.5f
|
||||
val xx = x0 + t * 0.5f
|
||||
val xxb = x0 + s - t * 0.5f
|
||||
|
||||
when (corner) {
|
||||
0 -> {
|
||||
val cTLx = x0 + R
|
||||
val cTLy = y0 + R
|
||||
val sTLx = x0 + R
|
||||
val sTLy = yy
|
||||
val eTLx = xx
|
||||
val eTLy = y0 + R
|
||||
p.moveTo(x0 + R + L, yy)
|
||||
p.lineTo(sTLx, sTLy)
|
||||
p.addCornerMinorArc(cTLx, cTLy, r, sTLx, sTLy, eTLx, eTLy)
|
||||
val yEndTL = min(y0 + R + L, y0 + s - R - t * 0.5f)
|
||||
p.lineTo(xx, max(yEndTL, y0 + R + 2f))
|
||||
}
|
||||
1 -> {
|
||||
val cTRx = x0 + s - R
|
||||
val cTRy = y0 + R
|
||||
val sTRx = x0 + s - R
|
||||
val sTRy = yy
|
||||
val eTRx = xxb
|
||||
val eTRy = y0 + R
|
||||
p.moveTo(x0 + s - R - L, yy)
|
||||
p.lineTo(sTRx, sTRy)
|
||||
p.addCornerMinorArc(cTRx, cTRy, r, sTRx, sTRy, eTRx, eTRy)
|
||||
val yEndTR = min(y0 + R + L, y0 + s - R - t * 0.5f)
|
||||
p.lineTo(xxb, max(yEndTR, y0 + R + 2f))
|
||||
}
|
||||
2 -> {
|
||||
val cBLx = x0 + R
|
||||
val cBLy = y0 + s - R
|
||||
val sBLx = x0 + R
|
||||
val sBLy = yyb
|
||||
val eBLx = xx
|
||||
val eBLy = y0 + s - R
|
||||
p.moveTo(x0 + R + L, yyb)
|
||||
p.lineTo(sBLx, sBLy)
|
||||
p.addCornerMinorArc(cBLx, cBLy, r, sBLx, sBLy, eBLx, eBLy)
|
||||
val yEndTopRef = max(min(y0 + R + L, y0 + s - R - t * 0.5f), y0 + R + 2f)
|
||||
val yLegBL = y0 + s + y0 - yEndTopRef
|
||||
p.lineTo(xx, yLegBL)
|
||||
}
|
||||
3 -> {
|
||||
val cBRx = x0 + s - R
|
||||
val cBRy = y0 + s - R
|
||||
val sBRx = x0 + s - R
|
||||
val sBRy = yyb
|
||||
val eBRx = xxb
|
||||
val eBRy = y0 + s - R
|
||||
p.moveTo(x0 + s - R - L, yyb)
|
||||
p.lineTo(sBRx, sBRy)
|
||||
p.addCornerMinorArc(cBRx, cBRy, r, sBRx, sBRy, eBRx, eBRy)
|
||||
val yEndTopRef = max(min(y0 + R + L, y0 + s - R - t * 0.5f), y0 + R + 2f)
|
||||
val yLegBR = y0 + s + y0 - yEndTopRef
|
||||
p.lineTo(xxb, yLegBR)
|
||||
}
|
||||
}
|
||||
return p
|
||||
}
|
||||
}
|
||||
152
client/android/src/org/amnezia/vpn/PairingQrScanGeometry.kt
Normal file
152
client/android/src/org/amnezia/vpn/PairingQrScanGeometry.kt
Normal file
@@ -0,0 +1,152 @@
|
||||
package org.amnezia.vpn
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import com.google.mlkit.vision.barcode.common.Barcode
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
object PairingQrScanGeometry {
|
||||
fun viewRectToInputImageRectFillCenter(
|
||||
viewW: Int,
|
||||
viewH: Int,
|
||||
imageW: Int,
|
||||
imageH: Int,
|
||||
viewRect: RectF
|
||||
): RectF {
|
||||
val scale = max(viewW / imageW.toFloat(), viewH / imageH.toFloat())
|
||||
val drawLeft = (viewW - imageW * scale) / 2f
|
||||
val drawTop = (viewH - imageH * scale) / 2f
|
||||
return RectF(
|
||||
(viewRect.left - drawLeft) / scale,
|
||||
(viewRect.top - drawTop) / scale,
|
||||
(viewRect.right - drawLeft) / scale,
|
||||
(viewRect.bottom - drawTop) / scale
|
||||
)
|
||||
}
|
||||
|
||||
fun pairingIosStyleHoleCornerRadiusPx(sidePx: Float, density: Float): Float {
|
||||
val d = density
|
||||
var holeR = min(28f * d, max(10f * d, sidePx * 0.056f))
|
||||
val half = 0.5f * sidePx
|
||||
holeR = min(holeR, max(6f * d, half - 2f * d))
|
||||
return max(holeR, 1f)
|
||||
}
|
||||
|
||||
fun barcodeBoxOverlapFraction(roi: RectF, box: Rect): Float {
|
||||
val bf = RectF(box)
|
||||
val inter = RectF(roi)
|
||||
if (!inter.intersect(bf)) return 0f
|
||||
val interArea = inter.width() * inter.height()
|
||||
val boxArea = bf.width() * bf.height()
|
||||
return if (boxArea <= 0f) 0f else interArea / boxArea
|
||||
}
|
||||
|
||||
fun barcodeMatchesPairingHole(
|
||||
roiInImageSpace: RectF,
|
||||
imageW: Int,
|
||||
imageH: Int,
|
||||
barcode: Barcode,
|
||||
minOverlapFraction: Float = PAIRING_SEND_MIN_OVERLAP_BBOX_FALLBACK
|
||||
): Boolean {
|
||||
if (imageW <= 0 || imageH <= 0) {
|
||||
return false
|
||||
}
|
||||
val roi = RectF(roiInImageSpace)
|
||||
val iw = imageW.toFloat()
|
||||
val ih = imageH.toFloat()
|
||||
roi.left = max(0f, roi.left)
|
||||
roi.top = max(0f, roi.top)
|
||||
roi.right = min(iw, roi.right)
|
||||
roi.bottom = min(ih, roi.bottom)
|
||||
if (roi.width() <= 0f || roi.height() <= 0f) {
|
||||
return false
|
||||
}
|
||||
|
||||
val corners = barcode.cornerPoints
|
||||
if (corners != null && corners.size >= 4) {
|
||||
for (p in corners) {
|
||||
if (!roi.contains(p.x.toFloat(), p.y.toFloat())) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
val box = barcode.boundingBox ?: return false
|
||||
val cx = box.centerX().toFloat()
|
||||
val cy = box.centerY().toFloat()
|
||||
if (!roi.contains(cx, cy)) {
|
||||
return false
|
||||
}
|
||||
return barcodeBoxOverlapFraction(roi, box) >= minOverlapFraction
|
||||
}
|
||||
|
||||
private const val PAIRING_SEND_MIN_OVERLAP_BBOX_FALLBACK = 0.72f
|
||||
|
||||
fun pairingIosStyleHoleRectF(
|
||||
viewW: Int,
|
||||
viewH: Int,
|
||||
headerBottomPx: Float,
|
||||
statusBarTopPx: Float,
|
||||
density: Float
|
||||
): RectF {
|
||||
val w = viewW.toFloat()
|
||||
val h = viewH.toFloat()
|
||||
val d = density
|
||||
if (w < 32f || h < 32f) {
|
||||
return RectF()
|
||||
}
|
||||
var hdrBottom = headerBottomPx
|
||||
if (hdrBottom < 8f * d) {
|
||||
hdrBottom = 132f * d + statusBarTopPx
|
||||
}
|
||||
val sqSz = floor(min(w, h) * 0.72).toFloat()
|
||||
var sqX = (w - sqSz) / 2f
|
||||
var sqY = (h - sqSz) / 2f
|
||||
sqY = max(sqY, hdrBottom + 8f * d)
|
||||
val kBottomBand = 80f * d
|
||||
val maxHoleBottom = h - kBottomBand
|
||||
if (sqY + sqSz > maxHoleBottom) {
|
||||
sqY = maxHoleBottom - sqSz
|
||||
sqY = max(sqY, hdrBottom + 8f * d)
|
||||
}
|
||||
sqX = max(8f * d, min(sqX, w - sqSz - 8f * d))
|
||||
sqY = max(hdrBottom + 4f * d, min(sqY, h - sqSz - 8f * d))
|
||||
return RectF(sqX, sqY, sqX + sqSz, sqY + sqSz)
|
||||
}
|
||||
|
||||
fun pairingIosStyleTorchCenterYPx(
|
||||
holeBottomPx: Float,
|
||||
bandBottomPx: Float,
|
||||
headerBottomPx: Float,
|
||||
safeBottomPx: Float,
|
||||
density: Float
|
||||
): Float {
|
||||
val torchH = 56f * density
|
||||
val d = density
|
||||
var torchCy = (holeBottomPx + bandBottomPx) * 0.5f
|
||||
val minC = holeBottomPx + torchH * 0.5f + 6f * d
|
||||
val maxC = bandBottomPx - torchH * 0.5f - max(6f * d, safeBottomPx)
|
||||
torchCy = max(minC, min(maxC, torchCy))
|
||||
if (minC > maxC) {
|
||||
torchCy = (minC + maxC) * 0.5f
|
||||
}
|
||||
val hdr = headerBottomPx + torchH * 0.5f + 10f * d
|
||||
return max(torchCy, hdr)
|
||||
}
|
||||
|
||||
fun pairingIosStyleHoleInImageCoords(
|
||||
viewW: Int,
|
||||
viewH: Int,
|
||||
headerBottomPx: Float,
|
||||
statusBarTopPx: Float,
|
||||
density: Float,
|
||||
imageW: Int,
|
||||
imageH: Int
|
||||
): RectF {
|
||||
val hv = pairingIosStyleHoleRectF(viewW, viewH, headerBottomPx, statusBarTopPx, density)
|
||||
return viewRectToInputImageRectFillCenter(viewW, viewH, imageW, imageH, hv)
|
||||
}
|
||||
}
|
||||
115
client/android/src/org/amnezia/vpn/PairingQrScanOverlayView.kt
Normal file
115
client/android/src/org/amnezia/vpn/PairingQrScanOverlayView.kt
Normal file
@@ -0,0 +1,115 @@
|
||||
package org.amnezia.vpn
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Path
|
||||
import android.graphics.RectF
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import kotlin.math.max
|
||||
|
||||
class PairingQrScanOverlayView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : View(context, attrs, defStyleAttr) {
|
||||
|
||||
init {
|
||||
isClickable = false
|
||||
isFocusable = false
|
||||
}
|
||||
|
||||
@Suppress("ClickableViewAccessibility")
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean = false
|
||||
|
||||
private val dimPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = 0x8C000000.toInt()
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
|
||||
private val bracketPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = 0xFFE8E8EC.toInt()
|
||||
style = Paint.Style.STROKE
|
||||
strokeCap = Paint.Cap.ROUND
|
||||
strokeJoin = Paint.Join.ROUND
|
||||
}
|
||||
|
||||
private var hole = RectF()
|
||||
|
||||
private val bracketPaths = arrayOfNulls<Path>(4)
|
||||
|
||||
private val dimPath = Path()
|
||||
|
||||
private var pairingHeaderBottomPx = 0f
|
||||
|
||||
fun setPairingHeaderBottomPx(px: Float) {
|
||||
if (pairingHeaderBottomPx == px) {
|
||||
return
|
||||
}
|
||||
pairingHeaderBottomPx = px
|
||||
recomputePairingHole()
|
||||
invalidate()
|
||||
}
|
||||
|
||||
private fun recomputePairingHole() {
|
||||
val w = width
|
||||
val h = height
|
||||
if (w <= 0 || h <= 0) {
|
||||
return
|
||||
}
|
||||
val topInset = ViewCompat.getRootWindowInsets(this)
|
||||
?.getInsets(WindowInsetsCompat.Type.statusBars())?.top?.toFloat() ?: 0f
|
||||
val d = resources.displayMetrics.density
|
||||
hole = PairingQrScanGeometry.pairingIosStyleHoleRectF(w, h, pairingHeaderBottomPx, topInset, d)
|
||||
rebuildBracketPaths()
|
||||
}
|
||||
|
||||
private fun rebuildBracketPaths() {
|
||||
val s = hole.width()
|
||||
if (s <= 0f) {
|
||||
bracketPaths.fill(null)
|
||||
return
|
||||
}
|
||||
val x0 = hole.left
|
||||
val y0 = hole.top
|
||||
val t = bracketPaint.strokeWidth
|
||||
val d = resources.displayMetrics.density
|
||||
val l = max(28f * d, s * 0.13f)
|
||||
val r = PairingQrScanGeometry.pairingIosStyleHoleCornerRadiusPx(s, d)
|
||||
for (i in 0..3) {
|
||||
bracketPaths[i] = PairingQrScanBracketPaths.bracketStrokePath(i, x0, y0, s, r, l, t)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
super.onSizeChanged(w, h, oldw, oldh)
|
||||
bracketPaint.strokeWidth = max(3f, 5f * resources.displayMetrics.density)
|
||||
recomputePairingHole()
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
val w = width.toFloat()
|
||||
val h = height.toFloat()
|
||||
val side = hole.width()
|
||||
if (side > 0f) {
|
||||
val d = resources.displayMetrics.density
|
||||
val rx = PairingQrScanGeometry.pairingIosStyleHoleCornerRadiusPx(side, d)
|
||||
dimPath.rewind()
|
||||
dimPath.fillType = Path.FillType.EVEN_ODD
|
||||
dimPath.addRect(0f, 0f, w, h, Path.Direction.CW)
|
||||
dimPath.addRoundRect(hole, rx, rx, Path.Direction.CW)
|
||||
canvas.drawPath(dimPath, dimPaint)
|
||||
} else {
|
||||
canvas.drawRect(0f, 0f, w, h, dimPaint)
|
||||
}
|
||||
|
||||
for (i in 0..3) {
|
||||
bracketPaths[i]?.let { canvas.drawPath(it, bracketPaint) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +1,30 @@
|
||||
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() {
|
||||
|
||||
// SAF launcher for Android 10+ where File API is blocked by scoped storage
|
||||
private val safLauncher = registerForActivityResult(object : ActivityResultContracts.OpenDocument() {
|
||||
private val fileChooseResultLauncher = registerForActivityResult(object : ActivityResultContracts.OpenDocument() {
|
||||
override fun createIntent(context: Context, input: Array<String>): Intent {
|
||||
val intent = super.createIntent(context, input)
|
||||
val activities = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
|
||||
val activitiesToResolveIntent = 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 (activities.all {
|
||||
if (activitiesToResolveIntent.all {
|
||||
val name = it.activityInfo.packageName
|
||||
name.startsWith("com.google.android.tv.frameworkpackagestubs") || name.startsWith("com.android.tv.frameworkpackagestubs")
|
||||
}) {
|
||||
@@ -38,140 +32,38 @@ class TvFilePicker : ComponentActivity() {
|
||||
}
|
||||
return intent
|
||||
}
|
||||
}) { uri ->
|
||||
}) {
|
||||
setResult(RESULT_OK, Intent().apply {
|
||||
data = uri
|
||||
data = it
|
||||
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")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
launchSaf()
|
||||
} else {
|
||||
checkPermissionAndBrowse()
|
||||
}
|
||||
getFile()
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onBackPressed() {
|
||||
navigateBack()
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
Log.v(TAG, "onNewIntent")
|
||||
getFile()
|
||||
}
|
||||
|
||||
private fun launchSaf() {
|
||||
private fun getFile() {
|
||||
try {
|
||||
safLauncher.launch(arrayOf("*/*"))
|
||||
Log.v(TAG, "getFile")
|
||||
fileChooseResultLauncher.launch(arrayOf("*/*"))
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
Log.w(TAG, "No SAF activity found")
|
||||
Log.w(TAG, "Activity not found")
|
||||
setResult(RESULT_CANCELED, Intent().apply { putExtra("activityNotFound", true) })
|
||||
finish()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "SAF launch failed: $e")
|
||||
Log.e(TAG, "Failed to get file: $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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,4 +34,10 @@ object QtAndroidController {
|
||||
|
||||
external fun onActivityPaused()
|
||||
external fun onActivityResumed()
|
||||
|
||||
external fun onCameraPermissionResult(granted: Boolean)
|
||||
|
||||
external fun onPairingQrCameraClosed()
|
||||
|
||||
external fun onPairingQrCameraUserDismissed()
|
||||
}
|
||||
@@ -2,8 +2,9 @@ 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 ${CMAKE_BINARY_DIR}/3rd/SortFilterProxyModel)
|
||||
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/SortFilterProxyModel)
|
||||
set(LIBS ${LIBS} SortFilterProxyModel)
|
||||
include(${CLIENT_ROOT_DIR}/cmake/QSimpleCrypto.cmake)
|
||||
|
||||
include(${CLIENT_ROOT_DIR}/3rd/qrcodegen/qrcodegen.cmake)
|
||||
|
||||
@@ -11,24 +12,20 @@ add_compile_definitions(_WINSOCKAPI_)
|
||||
|
||||
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
|
||||
set(BUILD_WITH_QT6 ON)
|
||||
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/qtkeychain ${CMAKE_BINARY_DIR}/3rd/qtkeychain EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(${CLIENT_ROOT_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 ${CMAKE_BINARY_DIR}/3rd/qtgamepad)
|
||||
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/qtgamepad)
|
||||
# Link both the C++ module and QML plugin
|
||||
if(TARGET ${PROJECT})
|
||||
if(TARGET GamepadLegacy)
|
||||
target_link_libraries(${PROJECT} PRIVATE GamepadLegacy)
|
||||
else()
|
||||
list(APPEND LIBS GamepadLegacy)
|
||||
endif()
|
||||
if(TARGET ${PROJECT})
|
||||
if(TARGET GamepadLegacyQuickPrivate)
|
||||
target_link_libraries(${PROJECT} PRIVATE GamepadLegacyQuickPrivate)
|
||||
else()
|
||||
list(APPEND LIBS GamepadLegacyQuickPrivate)
|
||||
endif()
|
||||
message(STATUS "Gamepad support enabled for Android")
|
||||
else()
|
||||
@@ -39,6 +36,7 @@ endif()
|
||||
set(LIBS ${LIBS} qt6keychain)
|
||||
|
||||
include_directories(
|
||||
${CLIENT_ROOT_DIR}/3rd/QSimpleCrypto/src/include
|
||||
${CLIENT_ROOT_DIR}/3rd/qtkeychain/qtkeychain
|
||||
${CMAKE_CURRENT_BINARY_DIR}/3rd/qtkeychain
|
||||
)
|
||||
|
||||
21
client/cmake/QSimpleCrypto.cmake
Normal file
21
client/cmake/QSimpleCrypto.cmake
Normal file
@@ -0,0 +1,21 @@
|
||||
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
|
||||
)
|
||||
@@ -28,6 +28,7 @@ set(LIBS ${LIBS}
|
||||
|
||||
|
||||
set(HEADERS ${HEADERS}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosPairingQrOverlayWindow.h
|
||||
${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
|
||||
@@ -44,6 +45,8 @@ set(SOURCES ${SOURCES}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosPairingCameraAccess.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosPairingQrOverlayWindow.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
|
||||
@@ -54,6 +57,7 @@ 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"
|
||||
@@ -121,11 +125,6 @@ target_sources(${PROJECT} PRIVATE
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/StoreKit2Helper.swift
|
||||
)
|
||||
|
||||
set_source_files_properties(
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Media.xcassets
|
||||
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
|
||||
|
||||
@@ -49,6 +49,7 @@ set(SOURCES ${SOURCES}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosPairingCameraAccess_stub.cpp
|
||||
)
|
||||
|
||||
set(ICON_FILE ${CMAKE_CURRENT_SOURCE_DIR}/images/app.icns)
|
||||
@@ -152,5 +153,5 @@ message(${QtCore_location})
|
||||
get_filename_component(QT_BIN_DIR_DETECTED "${QtCore_location}/../../../../../bin" ABSOLUTE)
|
||||
|
||||
add_custom_command(TARGET ${PROJECT} POST_BUILD
|
||||
COMMAND ${QT_BIN_DIR_DETECTED}/macdeployqt $<TARGET_BUNDLE_DIR:AmneziaVPN> -appstore-compliant -qmldir=${CMAKE_CURRENT_SOURCE_DIR} -no-codesign
|
||||
COMMAND ${QT_BIN_DIR_DETECTED}/macdeployqt $<TARGET_BUNDLE_DIR:AmneziaVPN> -appstore-compliant -qmldir=${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
|
||||
@@ -45,6 +45,7 @@ set(HEADERS ${HEADERS}
|
||||
${CLIENT_ROOT_DIR}/core/controllers/settingsController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/servicesCatalogController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/subscriptionController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/pairingController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/newsController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/updateController.h
|
||||
${CLIENT_ROOT_DIR}/core/repositories/secureServersRepository.h
|
||||
@@ -52,22 +53,21 @@ set(HEADERS ${HEADERS}
|
||||
${CLIENT_ROOT_DIR}/core/protocols/qmlRegisterProtocols.h
|
||||
${CLIENT_ROOT_DIR}/ui/utils/pages.h
|
||||
${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}/../common/crypto/cryptoUtils.h
|
||||
${CLIENT_ROOT_DIR}/ui/utils/qmlUtils.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/api/apiUtils.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/osSignalHandler.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/utilities.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/managementServer.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/constants.h
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/iosPairingCameraAccess.h
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/iosPairingQrOverlayWindow.h
|
||||
)
|
||||
|
||||
# Mozilla headres
|
||||
@@ -125,16 +125,15 @@ set(SOURCES ${SOURCES}
|
||||
${CLIENT_ROOT_DIR}/core/controllers/settingsController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/servicesCatalogController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/subscriptionController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/pairingController.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
|
||||
@@ -144,7 +143,6 @@ 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}/../common/crypto/cryptoUtils.cpp
|
||||
${CLIENT_ROOT_DIR}/ui/utils/qmlUtils.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/api/apiUtils.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/serverConfigUtils.cpp
|
||||
@@ -163,6 +161,7 @@ set(SOURCES ${SOURCES}
|
||||
if(NOT IOS AND NOT MACOS_NE)
|
||||
set(SOURCES ${SOURCES}
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/QRCodeReaderBase.cpp
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/iosPairingCameraAccess_stub.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QThread>
|
||||
#include <QUuid>
|
||||
#include "logger.h"
|
||||
|
||||
@@ -138,320 +137,115 @@ amnezia::ProtocolConfig XrayConfigurator::processConfigWithLocalSettings(const a
|
||||
return protocolConfig;
|
||||
}
|
||||
|
||||
ErrorCode XrayConfigurator::uploadServerConfigJson(const ServerCredentials &credentials, DockerContainer container,
|
||||
const DnsSettings &dnsSettings, const QJsonObject &serverConfig) const
|
||||
{
|
||||
const QString updatedConfig = QJsonDocument(serverConfig).toJson();
|
||||
ErrorCode errorCode = m_sshSession->uploadTextFileToContainer(
|
||||
container, credentials, updatedConfig, amnezia::protocols::xray::serverConfigPath,
|
||||
libssh::ScpOverwriteMode::ScpOverwriteExisting);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Failed to upload updated config";
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
const QString restartScript = QStringLiteral("sudo docker restart $CONTAINER_NAME");
|
||||
errorCode = m_sshSession->runScript(
|
||||
credentials,
|
||||
m_sshSession->replaceVars(restartScript,
|
||||
amnezia::genBaseVars(credentials, container, dnsSettings.primaryDns,
|
||||
dnsSettings.secondaryDns)));
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Failed to restart container";
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
ErrorCode XrayConfigurator::readRealityKeyFiles(const DockerContainer container, const ServerCredentials &credentials,
|
||||
QString &outPublicKey, QString &outShortId) const
|
||||
{
|
||||
outPublicKey.clear();
|
||||
outShortId.clear();
|
||||
|
||||
auto readKeyFile = [&](const QString &path, QString &out) -> ErrorCode {
|
||||
for (int attempt = 0; attempt < 3; ++attempt) {
|
||||
ErrorCode fileError = ErrorCode::NoError;
|
||||
out = QString::fromUtf8(m_sshSession->getTextFileFromContainer(container, credentials, path, fileError));
|
||||
out.replace(QLatin1Char('\n'), QString());
|
||||
out.replace(QLatin1Char('\r'), QString());
|
||||
if (fileError == ErrorCode::NoError && !out.isEmpty()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
if (attempt < 2) {
|
||||
QThread::msleep(500);
|
||||
}
|
||||
}
|
||||
logger.error() << "Xray readRealityKeyFiles: failed path=" << path;
|
||||
return ErrorCode::XrayRealityKeysReadFailed;
|
||||
};
|
||||
|
||||
ErrorCode errorCode = readKeyFile(QString::fromLatin1(amnezia::protocols::xray::PublicKeyPath), outPublicKey);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
return readKeyFile(QString::fromLatin1(amnezia::protocols::xray::shortidPath), outShortId);
|
||||
}
|
||||
|
||||
QJsonObject XrayConfigurator::mergeStreamSettingsForServerInbound(const XrayServerConfig &srv,
|
||||
const QJsonObject &existingStreamSettings) const
|
||||
{
|
||||
QJsonObject streamSettings = buildStreamSettings(srv, QString());
|
||||
|
||||
if (srv.security != QLatin1String("reality")) {
|
||||
return streamSettings;
|
||||
}
|
||||
|
||||
const QJsonObject newRs = streamSettings[amnezia::protocols::xray::realitySettings].toObject();
|
||||
QJsonObject oldRs = existingStreamSettings[amnezia::protocols::xray::realitySettings].toObject();
|
||||
QJsonObject merged = oldRs.isEmpty() ? newRs : oldRs;
|
||||
|
||||
const QString siteEff = srv.site.isEmpty() ? QString::fromLatin1(amnezia::protocols::xray::defaultSite) : srv.site;
|
||||
const QString sniEff = srv.sni.isEmpty() ? siteEff : srv.sni;
|
||||
|
||||
if (newRs.contains(amnezia::protocols::xray::fingerprint)) {
|
||||
merged[amnezia::protocols::xray::fingerprint] = newRs[amnezia::protocols::xray::fingerprint];
|
||||
}
|
||||
merged[amnezia::protocols::xray::serverNames] = QJsonArray { sniEff };
|
||||
if (!merged.contains(QStringLiteral("dest"))) {
|
||||
merged[QStringLiteral("dest")] = siteEff + QStringLiteral(":443");
|
||||
}
|
||||
|
||||
streamSettings[amnezia::protocols::xray::realitySettings] = merged;
|
||||
return streamSettings;
|
||||
}
|
||||
|
||||
ErrorCode XrayConfigurator::applyServerSettingsToRemote(const ServerCredentials &credentials, DockerContainer container,
|
||||
ContainerConfig &containerConfig, const DnsSettings &dnsSettings,
|
||||
bool appendNewClient, QString *outClientId)
|
||||
{
|
||||
ErrorCode errorCode = ErrorCode::NoError;
|
||||
const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>();
|
||||
if (!xrayCfg) {
|
||||
logger.error() << "Xray applyServerSettings: missing XrayProtocolConfig";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
const XrayServerConfig &srv = xrayCfg->serverConfig;
|
||||
if (srv.isThirdPartyConfig) {
|
||||
logger.info() << "Xray applyServerSettings: skipped (third-party/native profile)";
|
||||
if (outClientId && xrayCfg->hasClientConfig()) {
|
||||
*outClientId = xrayCfg->clientConfig->id;
|
||||
}
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
logger.info() << "Xray applyServerSettings: start"
|
||||
<< "container=" << static_cast<int>(container) << "host=" << credentials.hostName
|
||||
<< "transport=" << srv.transport << "security=" << srv.security << "port=" << srv.port
|
||||
<< "appendClient=" << appendNewClient;
|
||||
const QString flowValue = srv.flow;
|
||||
QString realityPublicKey;
|
||||
QString realityShortId;
|
||||
if (srv.security == QLatin1String("reality")) {
|
||||
errorCode = readRealityKeyFiles(container, credentials, realityPublicKey, realityShortId);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Xray applyServerSettings: readRealityKeyFiles failed, error="
|
||||
<< static_cast<int>(errorCode);
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
|
||||
QString currentConfig = m_sshSession->getTextFileFromContainer(
|
||||
container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Xray applyServerSettings: getTextFileFromContainer failed, error="
|
||||
<< static_cast<int>(errorCode) << "path=" << amnezia::protocols::xray::serverConfigPath;
|
||||
return errorCode;
|
||||
}
|
||||
logger.info() << "Xray applyServerSettings: read server config, bytes=" << currentConfig.size();
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(currentConfig.toUtf8());
|
||||
if (doc.isNull() || !doc.isObject()) {
|
||||
logger.error() << "Failed to parse server config JSON";
|
||||
return ErrorCode::XrayServerConfigInvalid;
|
||||
}
|
||||
|
||||
QJsonObject serverConfig = doc.object();
|
||||
if (!serverConfig.contains(amnezia::protocols::xray::inbounds)) {
|
||||
logger.error() << "Server config missing 'inbounds' field";
|
||||
return ErrorCode::XrayServerConfigInvalid;
|
||||
}
|
||||
|
||||
QJsonArray inbounds = serverConfig[amnezia::protocols::xray::inbounds].toArray();
|
||||
if (inbounds.isEmpty()) {
|
||||
logger.error() << "Server config has empty 'inbounds' array";
|
||||
return ErrorCode::XrayServerConfigInvalid;
|
||||
}
|
||||
|
||||
QJsonObject inbound = inbounds[0].toObject();
|
||||
if (!inbound.contains(amnezia::protocols::xray::settings)) {
|
||||
logger.error() << "Inbound missing 'settings' field";
|
||||
return ErrorCode::XrayServerConfigInvalid;
|
||||
}
|
||||
|
||||
const QJsonObject existingStream = inbound[amnezia::protocols::xray::streamSettings].toObject();
|
||||
inbound[amnezia::protocols::xray::streamSettings] = mergeStreamSettingsForServerInbound(srv, existingStream);
|
||||
|
||||
if (!srv.port.isEmpty()) {
|
||||
inbound[amnezia::protocols::xray::port] = srv.port.toInt();
|
||||
}
|
||||
|
||||
QJsonObject settings = inbound[amnezia::protocols::xray::settings].toObject();
|
||||
if (!settings.contains(amnezia::protocols::xray::clients)) {
|
||||
settings[amnezia::protocols::xray::clients] = QJsonArray {};
|
||||
}
|
||||
|
||||
QJsonArray clients = settings[amnezia::protocols::xray::clients].toArray();
|
||||
QString clientId;
|
||||
|
||||
if (appendNewClient) {
|
||||
clientId = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
QJsonObject clientEntry;
|
||||
clientEntry[amnezia::protocols::xray::id] = clientId;
|
||||
if (!flowValue.isEmpty()) {
|
||||
clientEntry[amnezia::protocols::xray::flow] = flowValue;
|
||||
}
|
||||
clients.append(clientEntry);
|
||||
} else {
|
||||
if (clients.isEmpty()) {
|
||||
logger.error() << "Server config has no VLESS clients";
|
||||
return ErrorCode::XrayServerNoVlessClients;
|
||||
}
|
||||
clientId = clients[0].toObject()[amnezia::protocols::xray::id].toString();
|
||||
if (clientId.isEmpty()) {
|
||||
logger.error() << "Server config VLESS client has empty id";
|
||||
return ErrorCode::XrayServerNoVlessClients;
|
||||
}
|
||||
QJsonArray updatedClients;
|
||||
for (const QJsonValue &v : clients) {
|
||||
QJsonObject c = v.toObject();
|
||||
if (flowValue.isEmpty()) {
|
||||
c.remove(amnezia::protocols::xray::flow);
|
||||
} else {
|
||||
c[amnezia::protocols::xray::flow] = flowValue;
|
||||
}
|
||||
updatedClients.append(c);
|
||||
}
|
||||
clients = updatedClients;
|
||||
}
|
||||
|
||||
settings[amnezia::protocols::xray::clients] = clients;
|
||||
inbound[amnezia::protocols::xray::settings] = settings;
|
||||
inbounds[0] = inbound;
|
||||
serverConfig[amnezia::protocols::xray::inbounds] = inbounds;
|
||||
|
||||
errorCode = uploadServerConfigJson(credentials, container, dnsSettings, serverConfig);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Xray applyServerSettings: upload/restart failed, error=" << static_cast<int>(errorCode);
|
||||
return errorCode;
|
||||
}
|
||||
logger.info() << "Xray applyServerSettings: server config uploaded and container restarted";
|
||||
|
||||
if (outClientId) {
|
||||
*outClientId = clientId;
|
||||
}
|
||||
|
||||
XrayProtocolConfig updated =
|
||||
buildClientProtocolConfig(credentials, container, srv, clientId, errorCode, realityPublicKey, realityShortId);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Xray applyServerSettings: buildClientProtocolConfig failed, error="
|
||||
<< static_cast<int>(errorCode);
|
||||
return errorCode;
|
||||
}
|
||||
containerConfig.protocolConfig = updated;
|
||||
logger.info() << "Xray applyServerSettings: done, clientId=" << clientId;
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentials, DockerContainer container,
|
||||
const ContainerConfig &containerConfig,
|
||||
const DnsSettings &dnsSettings,
|
||||
ErrorCode &errorCode)
|
||||
{
|
||||
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();
|
||||
}
|
||||
return clientId;
|
||||
}
|
||||
// Generate new UUID for client
|
||||
QString clientId = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
|
||||
XrayProtocolConfig XrayConfigurator::buildClientProtocolConfig(const ServerCredentials &credentials,
|
||||
DockerContainer container,
|
||||
const XrayServerConfig &srv, const QString &clientId,
|
||||
ErrorCode &errorCode,
|
||||
const QString &prefetchedRealityPublicKey,
|
||||
const QString &prefetchedRealityShortId) const
|
||||
{
|
||||
QString xrayPublicKey = prefetchedRealityPublicKey;
|
||||
QString xrayShortId = prefetchedRealityShortId;
|
||||
|
||||
if (srv.security == QLatin1String("reality")) {
|
||||
if (xrayPublicKey.isEmpty() || xrayShortId.isEmpty()) {
|
||||
errorCode = readRealityKeyFiles(container, credentials, xrayPublicKey, xrayShortId);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return {};
|
||||
}
|
||||
// Get flow value from settings (default xtls-rprx-vision)
|
||||
QString flowValue = "xtls-rprx-vision";
|
||||
if (const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>()) {
|
||||
if (!xrayCfg->serverConfig.flow.isEmpty()) {
|
||||
flowValue = xrayCfg->serverConfig.flow;
|
||||
}
|
||||
}
|
||||
|
||||
QJsonObject userObj;
|
||||
userObj[amnezia::protocols::xray::id] = clientId;
|
||||
userObj[amnezia::protocols::xray::encryption] = QStringLiteral("none");
|
||||
if (!srv.flow.isEmpty()) {
|
||||
userObj[amnezia::protocols::xray::flow] = srv.flow;
|
||||
// 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 "";
|
||||
}
|
||||
|
||||
QJsonObject vnextEntry;
|
||||
vnextEntry[amnezia::protocols::xray::address] = credentials.hostName;
|
||||
vnextEntry[amnezia::protocols::xray::port] =
|
||||
srv.port.isEmpty() ? QString(amnezia::protocols::xray::defaultPort).toInt() : srv.port.toInt();
|
||||
vnextEntry[amnezia::protocols::xray::users] = QJsonArray { userObj };
|
||||
|
||||
QJsonObject outboundSettings;
|
||||
outboundSettings[amnezia::protocols::xray::vnext] = QJsonArray { vnextEntry };
|
||||
|
||||
QJsonObject outbound;
|
||||
outbound[QStringLiteral("protocol")] = QStringLiteral("vless");
|
||||
outbound[amnezia::protocols::xray::settings] = outboundSettings;
|
||||
|
||||
QJsonObject streamObj = buildStreamSettings(srv, clientId);
|
||||
if (srv.security == QLatin1String("reality")) {
|
||||
QJsonObject rs = streamObj[amnezia::protocols::xray::realitySettings].toObject();
|
||||
rs[amnezia::protocols::xray::publicKey] = xrayPublicKey;
|
||||
rs[amnezia::protocols::xray::shortId] = xrayShortId;
|
||||
rs[amnezia::protocols::xray::spiderX] = QString();
|
||||
streamObj[amnezia::protocols::xray::realitySettings] = rs;
|
||||
// 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 "";
|
||||
}
|
||||
|
||||
outbound[amnezia::protocols::xray::streamSettings] = streamObj;
|
||||
QJsonObject serverConfig = doc.object();
|
||||
|
||||
QJsonObject inboundObj;
|
||||
inboundObj[QStringLiteral("listen")] = amnezia::protocols::xray::defaultLocalListenAddr;
|
||||
inboundObj[amnezia::protocols::xray::port] = amnezia::protocols::xray::defaultLocalProxyPort;
|
||||
inboundObj[QStringLiteral("protocol")] = QStringLiteral("socks");
|
||||
inboundObj[amnezia::protocols::xray::settings] = QJsonObject { { QStringLiteral("udp"), true } };
|
||||
// Validate server config structure
|
||||
if (!serverConfig.contains(amnezia::protocols::xray::inbounds)) {
|
||||
logger.error() << "Server config missing 'inbounds' field";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return "";
|
||||
}
|
||||
|
||||
QJsonObject clientJson;
|
||||
clientJson[QStringLiteral("log")] = QJsonObject { { QStringLiteral("loglevel"), QStringLiteral("error") } };
|
||||
clientJson[amnezia::protocols::xray::inbounds] = QJsonArray { inboundObj };
|
||||
clientJson[amnezia::protocols::xray::outbounds] = QJsonArray { outbound };
|
||||
QJsonArray inbounds = serverConfig[amnezia::protocols::xray::inbounds].toArray();
|
||||
if (inbounds.isEmpty()) {
|
||||
logger.error() << "Server config has empty 'inbounds' array";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return "";
|
||||
}
|
||||
|
||||
const QString config = QString::fromUtf8(QJsonDocument(clientJson).toJson(QJsonDocument::Compact));
|
||||
QJsonObject inbound = inbounds[0].toObject();
|
||||
if (!inbound.contains(amnezia::protocols::xray::settings)) {
|
||||
logger.error() << "Inbound missing 'settings' field";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return "";
|
||||
}
|
||||
|
||||
XrayProtocolConfig protocolConfig;
|
||||
protocolConfig.serverConfig = srv;
|
||||
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 "";
|
||||
}
|
||||
|
||||
XrayClientConfig clientConfig;
|
||||
clientConfig.nativeConfig = config;
|
||||
clientConfig.localPort = QString(amnezia::protocols::xray::defaultLocalProxyPort);
|
||||
clientConfig.id = clientId;
|
||||
protocolConfig.setClientConfig(clientConfig);
|
||||
QJsonArray clients = settings[amnezia::protocols::xray::clients].toArray();
|
||||
|
||||
return protocolConfig;
|
||||
// Create configuration for new client
|
||||
QJsonObject clientConfig {
|
||||
{amnezia::protocols::xray::id, clientId},
|
||||
};
|
||||
clientConfig[amnezia::protocols::xray::id] = clientId;
|
||||
if (!flowValue.isEmpty()) {
|
||||
clientConfig[amnezia::protocols::xray::flow] = flowValue;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, const QString &clientId) const
|
||||
@@ -559,12 +353,9 @@ QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, c
|
||||
if (pad.obfsMode) {
|
||||
if (!pad.bytesMin.isEmpty() || !pad.bytesMax.isEmpty()) {
|
||||
QJsonObject br;
|
||||
const int fromV = pad.bytesMin.isEmpty() ? 1 : pad.bytesMin.toInt();
|
||||
int toV = pad.bytesMax.isEmpty() ? 256 : pad.bytesMax.toInt();
|
||||
if (toV < fromV)
|
||||
toV = fromV;
|
||||
br[QStringLiteral("from")] = fromV;
|
||||
br[QStringLiteral("to")] = toV;
|
||||
br[QStringLiteral("from")] = pad.bytesMin.isEmpty() ? 1 : pad.bytesMin.toInt();
|
||||
br[QStringLiteral("to")] = pad.bytesMax.isEmpty() ? (pad.bytesMin.isEmpty() ? 256 : pad.bytesMin.toInt())
|
||||
: pad.bytesMax.toInt();
|
||||
xo[QStringLiteral("xPaddingBytes")] = br;
|
||||
}
|
||||
xo[QStringLiteral("xPaddingKey")] = pad.key.isEmpty() ? QStringLiteral("x_padding") : pad.key;
|
||||
@@ -628,13 +419,6 @@ ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentia
|
||||
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;
|
||||
@@ -657,5 +441,93 @@ ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentia
|
||||
return XrayProtocolConfig{};
|
||||
}
|
||||
|
||||
return buildClientProtocolConfig(credentials, container, srv, xrayClientId, errorCode);
|
||||
// Fetch server keys (Reality only)
|
||||
QString xrayPublicKey;
|
||||
QString xrayShortId;
|
||||
|
||||
if (srv.security == "reality") {
|
||||
xrayPublicKey = m_sshSession->getTextFileFromContainer(container, credentials,
|
||||
amnezia::protocols::xray::PublicKeyPath, errorCode);
|
||||
if (errorCode != ErrorCode::NoError || xrayPublicKey.isEmpty()) {
|
||||
logger.error() << "Failed to get public key";
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
errorCode = ErrorCode::InternalError;
|
||||
}
|
||||
return XrayProtocolConfig{};
|
||||
}
|
||||
xrayPublicKey.replace("\n", "");
|
||||
|
||||
xrayShortId = m_sshSession->getTextFileFromContainer(container, credentials,
|
||||
amnezia::protocols::xray::shortidPath, errorCode);
|
||||
if (errorCode != ErrorCode::NoError || xrayShortId.isEmpty()) {
|
||||
logger.error() << "Failed to get short ID";
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
errorCode = ErrorCode::InternalError;
|
||||
}
|
||||
return XrayProtocolConfig{};
|
||||
}
|
||||
xrayShortId.replace("\n", "");
|
||||
}
|
||||
|
||||
// Build outbound
|
||||
QJsonObject userObj;
|
||||
userObj[amnezia::protocols::xray::id] = xrayClientId;
|
||||
userObj[amnezia::protocols::xray::encryption] = "none";
|
||||
if (!srv.flow.isEmpty()) {
|
||||
userObj[amnezia::protocols::xray::flow] = srv.flow;
|
||||
}
|
||||
|
||||
QJsonObject vnextEntry;
|
||||
vnextEntry[amnezia::protocols::xray::address] = credentials.hostName;
|
||||
vnextEntry[amnezia::protocols::xray::port] = srv.port.toInt();
|
||||
vnextEntry[amnezia::protocols::xray::users] = QJsonArray { userObj };
|
||||
|
||||
QJsonObject outboundSettings;
|
||||
outboundSettings[amnezia::protocols::xray::vnext] = QJsonArray { vnextEntry };
|
||||
|
||||
QJsonObject outbound;
|
||||
outbound["protocol"] = "vless";
|
||||
outbound[amnezia::protocols::xray::settings] = outboundSettings;
|
||||
|
||||
// Build streamSettings
|
||||
QJsonObject streamObj = buildStreamSettings(srv, xrayClientId);
|
||||
|
||||
// Inject Reality keys
|
||||
if (srv.security == "reality") {
|
||||
QJsonObject rs = streamObj[amnezia::protocols::xray::realitySettings].toObject();
|
||||
rs[amnezia::protocols::xray::publicKey] = xrayPublicKey;
|
||||
rs[amnezia::protocols::xray::shortId] = xrayShortId;
|
||||
rs[amnezia::protocols::xray::spiderX] = "";
|
||||
streamObj[amnezia::protocols::xray::realitySettings] = rs;
|
||||
}
|
||||
|
||||
outbound[amnezia::protocols::xray::streamSettings] = streamObj;
|
||||
|
||||
// Build full client config
|
||||
QJsonObject inboundObj;
|
||||
inboundObj["listen"] = amnezia::protocols::xray::defaultLocalListenAddr;
|
||||
inboundObj[amnezia::protocols::xray::port] = amnezia::protocols::xray::defaultLocalProxyPort;
|
||||
inboundObj["protocol"] = "socks";
|
||||
inboundObj[amnezia::protocols::xray::settings] = QJsonObject { { "udp", true } };
|
||||
|
||||
QJsonObject clientJson;
|
||||
clientJson["log"] = QJsonObject { { "loglevel", "error" } };
|
||||
clientJson[amnezia::protocols::xray::inbounds] = QJsonArray { inboundObj };
|
||||
clientJson[amnezia::protocols::xray::outbounds] = QJsonArray { outbound };
|
||||
|
||||
QString config = QString::fromUtf8(QJsonDocument(clientJson).toJson(QJsonDocument::Compact));
|
||||
|
||||
// Return
|
||||
XrayProtocolConfig protocolConfig;
|
||||
protocolConfig.serverConfig = srv;
|
||||
|
||||
XrayClientConfig clientConfig;
|
||||
clientConfig.nativeConfig = config;
|
||||
qDebug() << "config:" << config;
|
||||
clientConfig.localPort = QString(amnezia::protocols::xray::defaultLocalProxyPort);
|
||||
clientConfig.id = xrayClientId;
|
||||
|
||||
protocolConfig.setClientConfig(clientConfig);
|
||||
|
||||
return protocolConfig;
|
||||
}
|
||||
@@ -23,37 +23,12 @@ public:
|
||||
amnezia::ProtocolConfig processConfigWithLocalSettings(const amnezia::ConnectionSettings &settings,
|
||||
amnezia::ProtocolConfig protocolConfig) override;
|
||||
|
||||
amnezia::ErrorCode applyServerSettingsToRemote(const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container,
|
||||
amnezia::ContainerConfig &containerConfig,
|
||||
const amnezia::DnsSettings &dnsSettings,
|
||||
bool appendNewClient,
|
||||
QString *outClientId = nullptr);
|
||||
|
||||
private:
|
||||
QString prepareServerConfig(const amnezia::ServerCredentials &credentials, amnezia::DockerContainer container, const amnezia::ContainerConfig &containerConfig,
|
||||
const amnezia::DnsSettings &dnsSettings,
|
||||
amnezia::ErrorCode &errorCode);
|
||||
|
||||
amnezia::ErrorCode uploadServerConfigJson(const amnezia::ServerCredentials &credentials, amnezia::DockerContainer container,
|
||||
const amnezia::DnsSettings &dnsSettings, const QJsonObject &serverConfig) const;
|
||||
|
||||
amnezia::XrayProtocolConfig buildClientProtocolConfig(const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container,
|
||||
const amnezia::XrayServerConfig &srv,
|
||||
const QString &clientId,
|
||||
amnezia::ErrorCode &errorCode,
|
||||
const QString &prefetchedRealityPublicKey = {},
|
||||
const QString &prefetchedRealityShortId = {}) const;
|
||||
|
||||
amnezia::ErrorCode readRealityKeyFiles(amnezia::DockerContainer container,
|
||||
const amnezia::ServerCredentials &credentials,
|
||||
QString &outPublicKey,
|
||||
QString &outShortId) const;
|
||||
|
||||
QJsonObject mergeStreamSettingsForServerInbound(const amnezia::XrayServerConfig &srv,
|
||||
const QJsonObject &existingStreamSettings) const;
|
||||
|
||||
// Builds the native xray "streamSettings" JSON object from XrayServerConfig
|
||||
QJsonObject buildStreamSettings(const amnezia::XrayServerConfig &srv,
|
||||
const QString &clientId) const;
|
||||
};
|
||||
|
||||
@@ -78,8 +78,7 @@ QFuture<QPair<ErrorCode, QJsonArray>> NewsController::fetchNews()
|
||||
m_appSettingsRepository->getGatewayEndpoint(),
|
||||
m_appSettingsRepository->isDevGatewayEnv(),
|
||||
apiDefs::requestTimeoutMsecs,
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled(),
|
||||
m_appSettingsRepository);
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled());
|
||||
|
||||
QJsonObject payload;
|
||||
payload.insert("locale", m_appSettingsRepository->getAppLanguage().name().split("_").first());
|
||||
@@ -91,7 +90,7 @@ QFuture<QPair<ErrorCode, QJsonArray>> NewsController::fetchNews()
|
||||
payload.insert(apiDefs::key::serviceType, services.value(apiDefs::key::serviceType));
|
||||
}
|
||||
|
||||
auto future = gatewayController->postAsync(QString("%1v1/news"), payload);
|
||||
auto future = gatewayController->postAsync(QString("%1v1/news"), payload, nullptr, gatewayController);
|
||||
return future.then([gatewayController](QPair<ErrorCode, QByteArray> result) -> QPair<ErrorCode, QJsonArray> {
|
||||
auto [errorCode, responseBody] = result;
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
|
||||
204
client/core/controllers/api/pairingController.cpp
Normal file
204
client/core/controllers/api/pairingController.cpp
Normal file
@@ -0,0 +1,204 @@
|
||||
#include "pairingController.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QSysInfo>
|
||||
#include "core/repositories/secureAppSettingsRepository.h"
|
||||
#include "core/utils/api/apiUtils.h"
|
||||
#include "core/utils/constants/apiConstants.h"
|
||||
#include "core/utils/constants/apiKeys.h"
|
||||
#include "version.h"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr qsizetype kPairingMaxQrUuidChars = 128;
|
||||
constexpr qsizetype kPairingMaxVpnConfigChars = 256 * 1024;
|
||||
constexpr qsizetype kPairingMaxApiKeyChars = 8192;
|
||||
constexpr qsizetype kPairingMaxServiceTypeChars = 64;
|
||||
constexpr qsizetype kPairingMaxUserCountryCodeChars = 32;
|
||||
|
||||
ErrorCode applyGatewayOrOpenApiGenerateError(const QJsonObject &obj, PairingController::QrPairingConfigPayload &outPayload)
|
||||
{
|
||||
ErrorCode apiStatus = apiUtils::errorCodeFromGatewayJsonHttpStatus(obj);
|
||||
if (apiStatus != ErrorCode::NoError) {
|
||||
return apiStatus;
|
||||
}
|
||||
|
||||
const QString config = obj.value(apiDefs::key::config).toString();
|
||||
if (!config.isEmpty()) {
|
||||
outPayload.config = config;
|
||||
outPayload.serviceInfo = obj.value(apiDefs::key::serviceInfo).toObject();
|
||||
outPayload.supportedProtocols = obj.value(apiDefs::key::supportedProtocols).toArray();
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
if (obj.contains(QStringLiteral("detail"))) {
|
||||
return ErrorCode::ApiConfigEmptyError;
|
||||
}
|
||||
|
||||
const QString msg = obj.value(QStringLiteral("message")).toString();
|
||||
if (msg.contains(QStringLiteral("timeout"), Qt::CaseInsensitive)) {
|
||||
return ErrorCode::ApiConfigTimeoutError;
|
||||
}
|
||||
if (msg.contains(QStringLiteral("Too Many"), Qt::CaseInsensitive)) {
|
||||
return ErrorCode::ApiPairingRateLimitedError;
|
||||
}
|
||||
if (msg.contains(QStringLiteral("Unavailable"), Qt::CaseInsensitive)) {
|
||||
return ErrorCode::ApiPairingServiceUnavailableError;
|
||||
}
|
||||
if (!msg.isEmpty()) {
|
||||
return ErrorCode::ApiConfigDownloadError;
|
||||
}
|
||||
|
||||
return ErrorCode::ApiConfigEmptyError;
|
||||
}
|
||||
|
||||
ErrorCode applyGatewayOrOpenApiScanError(const QJsonObject &obj)
|
||||
{
|
||||
const QString msgProbe = obj.value(QStringLiteral("message")).toString();
|
||||
if (msgProbe.contains(QStringLiteral("limit"), Qt::CaseInsensitive)
|
||||
&& (msgProbe.contains(QStringLiteral("device"), Qt::CaseInsensitive)
|
||||
|| msgProbe.contains(QStringLiteral("maximum"), Qt::CaseInsensitive)
|
||||
|| msgProbe.contains(QStringLiteral("max"), Qt::CaseInsensitive))) {
|
||||
return ErrorCode::ApiConfigLimitError;
|
||||
}
|
||||
|
||||
ErrorCode apiStatus = apiUtils::errorCodeFromGatewayJsonHttpStatus(obj);
|
||||
if (apiStatus != ErrorCode::NoError) {
|
||||
return apiStatus;
|
||||
}
|
||||
|
||||
if (obj.value(QStringLiteral("message")).toString() == QLatin1String("OK")) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
if (obj.contains(QStringLiteral("detail"))) {
|
||||
return ErrorCode::ApiPairingForbiddenError;
|
||||
}
|
||||
|
||||
const QString msg = obj.value(QStringLiteral("message")).toString();
|
||||
if (msg.contains(QStringLiteral("QR session"), Qt::CaseInsensitive)
|
||||
&& (msg.contains(QStringLiteral("not found"), Qt::CaseInsensitive)
|
||||
|| msg.contains(QStringLiteral("expired"), Qt::CaseInsensitive))) {
|
||||
return ErrorCode::ApiPairingSessionExpiredError;
|
||||
}
|
||||
if (msg.contains(QStringLiteral("not found"), Qt::CaseInsensitive) || msg.contains(QStringLiteral("expired"), Qt::CaseInsensitive)) {
|
||||
return ErrorCode::ApiNotFoundError;
|
||||
}
|
||||
if (msg.contains(QStringLiteral("Conflict"), Qt::CaseInsensitive) || msg.contains(QStringLiteral("already"), Qt::CaseInsensitive)) {
|
||||
return ErrorCode::ApiPairingConflictError;
|
||||
}
|
||||
if (msg.contains(QStringLiteral("Too Many"), Qt::CaseInsensitive)) {
|
||||
return ErrorCode::ApiPairingRateLimitedError;
|
||||
}
|
||||
if (msg.contains(QStringLiteral("Unavailable"), Qt::CaseInsensitive)) {
|
||||
return ErrorCode::ApiPairingServiceUnavailableError;
|
||||
}
|
||||
if (!msg.isEmpty()) {
|
||||
return ErrorCode::ApiConfigDownloadError;
|
||||
}
|
||||
|
||||
return ErrorCode::ApiConfigEmptyError;
|
||||
}
|
||||
|
||||
ErrorCode interpretGenerateQrJson(const QJsonObject &obj, PairingController::QrPairingConfigPayload &outPayload)
|
||||
{
|
||||
return applyGatewayOrOpenApiGenerateError(obj, outPayload);
|
||||
}
|
||||
|
||||
ErrorCode interpretScanQrJson(const QJsonObject &obj)
|
||||
{
|
||||
return applyGatewayOrOpenApiScanError(obj);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ErrorCode PairingController::parseGenerateQrResponseBody(const QByteArray &responseBody, QrPairingConfigPayload &outPayload)
|
||||
{
|
||||
outPayload = QrPairingConfigPayload {};
|
||||
const QJsonObject obj = QJsonDocument::fromJson(responseBody).object();
|
||||
return interpretGenerateQrJson(obj, outPayload);
|
||||
}
|
||||
|
||||
ErrorCode PairingController::parseScanQrResponseBody(const QByteArray &responseBody, QString *outOptionalDisplayName)
|
||||
{
|
||||
if (outOptionalDisplayName) {
|
||||
outOptionalDisplayName->clear();
|
||||
}
|
||||
const QJsonObject obj = QJsonDocument::fromJson(responseBody).object();
|
||||
const ErrorCode err = interpretScanQrJson(obj);
|
||||
if (err != ErrorCode::NoError) {
|
||||
return err;
|
||||
}
|
||||
if (outOptionalDisplayName) {
|
||||
const QString deviceName = obj.value(QStringLiteral("device_name")).toString().trimmed();
|
||||
if (!deviceName.isEmpty()) {
|
||||
*outOptionalDisplayName = deviceName;
|
||||
}
|
||||
}
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode PairingController::validatePairingScanFields(const QString &qrUuid, const QString &vpnConfig, const QString &apiKey,
|
||||
const QString &serviceType, const QString &userCountryCode)
|
||||
{
|
||||
if (qrUuid.size() > kPairingMaxQrUuidChars) {
|
||||
return ErrorCode::ApiConfigEmptyError;
|
||||
}
|
||||
if (vpnConfig.size() > kPairingMaxVpnConfigChars) {
|
||||
return ErrorCode::ApiPairingPayloadTooLargeError;
|
||||
}
|
||||
if (apiKey.size() > kPairingMaxApiKeyChars) {
|
||||
return ErrorCode::ApiPairingPayloadTooLargeError;
|
||||
}
|
||||
const QString st = serviceType.trimmed();
|
||||
const QString cc = userCountryCode.trimmed();
|
||||
if (st.isEmpty() || cc.isEmpty()) {
|
||||
return ErrorCode::ApiPairingMissingMetadataError;
|
||||
}
|
||||
if (st.size() > kPairingMaxServiceTypeChars || cc.size() > kPairingMaxUserCountryCodeChars) {
|
||||
return ErrorCode::ApiPairingPayloadTooLargeError;
|
||||
}
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
PairingController::PairingController(SecureAppSettingsRepository *appSettingsRepository)
|
||||
: m_appSettingsRepository(appSettingsRepository)
|
||||
{
|
||||
}
|
||||
|
||||
int PairingController::pairingLongPollTimeoutMsecs() const
|
||||
{
|
||||
return 60 * 1000;
|
||||
}
|
||||
|
||||
QJsonObject PairingController::buildGenerateQrPayload(const QString &qrUuid) const
|
||||
{
|
||||
QJsonObject o;
|
||||
o[apiDefs::key::qrUuid] = qrUuid;
|
||||
o[apiDefs::key::installationUuid] = m_appSettingsRepository->getInstallationUuid(true);
|
||||
o[apiDefs::key::appVersion] = QString(APP_VERSION);
|
||||
o[apiDefs::key::osVersion] = QSysInfo::productType();
|
||||
return o;
|
||||
}
|
||||
|
||||
QJsonObject PairingController::buildScanQrPayload(const QString &qrUuid, const QString &vpnConfig, const QJsonObject &serviceInfo,
|
||||
const QJsonArray &supportedProtocols, const QString &apiKey,
|
||||
const QString &serviceType, const QString &userCountryCode) const
|
||||
{
|
||||
QJsonObject auth;
|
||||
auth[apiDefs::key::apiKey] = apiKey;
|
||||
|
||||
QJsonObject o;
|
||||
o[apiDefs::key::qrUuid] = qrUuid;
|
||||
o[apiDefs::key::config] = vpnConfig;
|
||||
o[apiDefs::key::serviceInfo] = serviceInfo;
|
||||
o[apiDefs::key::supportedProtocols] = supportedProtocols;
|
||||
o[apiDefs::key::authData] = auth;
|
||||
o[apiDefs::key::installationUuid] = m_appSettingsRepository->getInstallationUuid(true);
|
||||
o[apiDefs::key::appVersion] = QString(APP_VERSION);
|
||||
o[apiDefs::key::osVersion] = QSysInfo::productType();
|
||||
o[apiDefs::key::serviceType] = serviceType.trimmed();
|
||||
o[apiDefs::key::userCountryCode] = userCountryCode.trimmed();
|
||||
return o;
|
||||
}
|
||||
41
client/core/controllers/api/pairingController.h
Normal file
41
client/core/controllers/api/pairingController.h
Normal file
@@ -0,0 +1,41 @@
|
||||
#ifndef PAIRINGCONTROLLER_H
|
||||
#define PAIRINGCONTROLLER_H
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
|
||||
#include "core/utils/errorCodes.h"
|
||||
|
||||
class SecureAppSettingsRepository;
|
||||
|
||||
class PairingController
|
||||
{
|
||||
public:
|
||||
struct QrPairingConfigPayload
|
||||
{
|
||||
QString config;
|
||||
QJsonObject serviceInfo;
|
||||
QJsonArray supportedProtocols;
|
||||
};
|
||||
|
||||
explicit PairingController(SecureAppSettingsRepository *appSettingsRepository);
|
||||
|
||||
int pairingLongPollTimeoutMsecs() const;
|
||||
|
||||
QJsonObject buildGenerateQrPayload(const QString &qrUuid) const;
|
||||
QJsonObject buildScanQrPayload(const QString &qrUuid, const QString &vpnConfig, const QJsonObject &serviceInfo,
|
||||
const QJsonArray &supportedProtocols, const QString &apiKey, const QString &serviceType,
|
||||
const QString &userCountryCode) const;
|
||||
|
||||
static amnezia::ErrorCode parseGenerateQrResponseBody(const QByteArray &responseBody, QrPairingConfigPayload &outPayload);
|
||||
static amnezia::ErrorCode parseScanQrResponseBody(const QByteArray &responseBody, QString *outOptionalDisplayName = nullptr);
|
||||
|
||||
static amnezia::ErrorCode validatePairingScanFields(const QString &qrUuid, const QString &vpnConfig, const QString &apiKey,
|
||||
const QString &serviceType, const QString &userCountryCode);
|
||||
|
||||
private:
|
||||
SecureAppSettingsRepository *m_appSettingsRepository;
|
||||
};
|
||||
|
||||
#endif // PAIRINGCONTROLLER_H
|
||||
@@ -242,7 +242,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);
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled());
|
||||
return gatewayController.post(endpoint, apiPayload, responseBody);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include <QEventLoop>
|
||||
#include <QFutureWatcher>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QPromise>
|
||||
#include <QSet>
|
||||
#include <QSysInfo>
|
||||
@@ -198,6 +197,9 @@ void SubscriptionController::updateApiConfigInJson(QJsonObject &serverConfigJson
|
||||
|
||||
if (serverConfigJson.value(configKey::configVersion).toInt() == serverConfigUtils::ConfigSource::AmneziaGateway) {
|
||||
QJsonObject responseObj = QJsonDocument::fromJson(apiResponseBody).object();
|
||||
if (responseObj.contains(apiDefs::key::supportedProtocols)) {
|
||||
apiConfig.insert(apiDefs::key::supportedProtocols, responseObj.value(apiDefs::key::supportedProtocols).toArray());
|
||||
}
|
||||
if (responseObj.contains(apiDefs::key::serviceInfo)) {
|
||||
apiConfig.insert(apiDefs::key::serviceInfo, responseObj.value(apiDefs::key::serviceInfo).toObject());
|
||||
}
|
||||
@@ -209,13 +211,12 @@ void SubscriptionController::updateApiConfigInJson(QJsonObject &serverConfigJson
|
||||
ErrorCode SubscriptionController::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);
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled());
|
||||
return gatewayController.post(endpoint, apiPayload, responseBody);
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::importServiceFromGateway(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const ProtocolData &protocolData,
|
||||
CaptchaInfo &captchaInfo)
|
||||
const QString &serviceProtocol, const ProtocolData &protocolData)
|
||||
{
|
||||
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
||||
QString(APP_VERSION),
|
||||
@@ -232,19 +233,6 @@ ErrorCode SubscriptionController::importServiceFromGateway(const QString &userCo
|
||||
|
||||
QByteArray responseBody;
|
||||
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody);
|
||||
|
||||
if (errorCode == ErrorCode::ApiCaptchaRequiredError) {
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
|
||||
if (jsonDoc.isObject()) {
|
||||
QJsonObject jsonObj = jsonDoc.object();
|
||||
captchaInfo.captchaId = jsonObj.value("captcha_id").toString();
|
||||
captchaInfo.captchaImageBase64 = jsonObj.value("captcha_image").toString();
|
||||
captchaInfo.hint = jsonObj.value("hint").toString();
|
||||
captchaInfo.isRequired = true;
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
@@ -254,9 +242,9 @@ ErrorCode SubscriptionController::importServiceFromGateway(const QString &userCo
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
|
||||
updateApiConfigInJson(serverConfigJson, serviceType, serviceProtocol, userCountryCode, responseBody);
|
||||
|
||||
|
||||
if (serverConfigJson.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
@@ -324,6 +312,71 @@ ErrorCode SubscriptionController::importTrialFromGateway(const QString &userCoun
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::importServerFromQrPairingResponse(const QString &vpnConfigKey, const QJsonObject &serviceInfo,
|
||||
const QJsonArray &supportedProtocols,
|
||||
int *duplicateServerIndex)
|
||||
{
|
||||
if (vpnConfigKey.isEmpty()) {
|
||||
return ErrorCode::ApiConfigEmptyError;
|
||||
}
|
||||
|
||||
QString normalizedKey = vpnConfigKey;
|
||||
normalizedKey.replace(QStringLiteral("vpn://"), QString());
|
||||
|
||||
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;
|
||||
}
|
||||
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()) {
|
||||
return ErrorCode::ApiConfigEmptyError;
|
||||
}
|
||||
|
||||
QJsonObject serverJson = QJsonDocument::fromJson(configString).object();
|
||||
if (serverJson.isEmpty()) {
|
||||
return ErrorCode::ApiConfigEmptyError;
|
||||
}
|
||||
|
||||
if (serverJson.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
QJsonObject apiConfig = serverJson.value(apiDefs::key::apiConfig).toObject();
|
||||
if (!serviceInfo.isEmpty()) {
|
||||
apiConfig.insert(apiDefs::key::serviceInfo, serviceInfo);
|
||||
}
|
||||
if (!supportedProtocols.isEmpty()) {
|
||||
apiConfig.insert(apiDefs::key::supportedProtocols, supportedProtocols);
|
||||
}
|
||||
serverJson[apiDefs::key::apiConfig] = apiConfig;
|
||||
|
||||
ApiV2ServerConfig apiV2ServerConfig = ApiV2ServerConfig::fromJson(serverJson);
|
||||
if (apiV2ServerConfig.apiConfig.vpnKey.isEmpty()) {
|
||||
QString fullKey = vpnConfigKey.trimmed();
|
||||
if (!fullKey.startsWith(QStringLiteral("vpn://"))) {
|
||||
fullKey = QStringLiteral("vpn://") + fullKey;
|
||||
}
|
||||
apiV2ServerConfig.apiConfig.vpnKey = fullKey;
|
||||
}
|
||||
|
||||
m_serversRepository->addServer(QString(), apiV2ServerConfig.toJson(),
|
||||
serverConfigUtils::configTypeFromJson(apiV2ServerConfig.toJson()));
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::importServiceFromAppStore(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const ProtocolData &protocolData,
|
||||
const QString &transactionId, bool isTestPurchase,
|
||||
@@ -415,23 +468,6 @@ ErrorCode SubscriptionController::updateServiceFromGateway(const QString &server
|
||||
}
|
||||
const bool isTestPurchase = apiV2->apiConfig.isTestPurchase;
|
||||
QString serviceProtocol = apiV2->serviceProtocol();
|
||||
|
||||
if (!newCountryCode.isEmpty()) {
|
||||
const auto availableCountries = apiV2->apiConfig.availableCountries;
|
||||
for (const auto &country : availableCountries) {
|
||||
const auto countryObject = country.toObject();
|
||||
if (countryObject.value(apiDefs::key::serverCountryCode).toString() != newCountryCode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto availableProtocols = countryObject.value(apiDefs::key::availableProtocols).toArray();
|
||||
if (!availableProtocols.isEmpty() && !availableProtocols.contains(serviceProtocol)) {
|
||||
serviceProtocol = availableProtocols.first().toString();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ProtocolData protocolData = generateProtocolData(serviceProtocol);
|
||||
|
||||
QJsonObject authDataJson = apiV2->authData.toJson();
|
||||
@@ -489,7 +525,6 @@ ErrorCode SubscriptionController::updateServiceFromGateway(const QString &server
|
||||
|
||||
if (apiV2->nameOverriddenByUser) {
|
||||
newApiV2->name = apiV2->name;
|
||||
newApiV2->displayName = apiV2->displayName;
|
||||
newApiV2->nameOverriddenByUser = true;
|
||||
}
|
||||
|
||||
@@ -766,36 +801,6 @@ bool SubscriptionController::isVlessProtocol(const QString &serverId) const
|
||||
return apiV2.has_value() && apiV2->serviceProtocol() == "vless";
|
||||
}
|
||||
|
||||
QString SubscriptionController::currentProtocol(const QString &serverId) const
|
||||
{
|
||||
auto apiV2 = m_serversRepository->apiV2Config(serverId);
|
||||
return apiV2.has_value() ? apiV2->serviceProtocol() : QString();
|
||||
}
|
||||
|
||||
QStringList SubscriptionController::availableProtocols(const QString &serverId) const
|
||||
{
|
||||
auto apiV2 = m_serversRepository->apiV2Config(serverId);
|
||||
if (!apiV2.has_value()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto currentCountryCode = apiV2->apiConfig.serverCountryCode;
|
||||
const auto availableCountries = apiV2->apiConfig.availableCountries;
|
||||
|
||||
QStringList protocols;
|
||||
for (const auto &country : availableCountries) {
|
||||
const auto countryObject = country.toObject();
|
||||
if (countryObject.value(apiDefs::key::serverCountryCode).toString() != currentCountryCode) {
|
||||
continue;
|
||||
}
|
||||
for (const auto &protocol : countryObject.value(apiDefs::key::availableProtocols).toArray()) {
|
||||
protocols.push_back(protocol.toString());
|
||||
}
|
||||
break;
|
||||
}
|
||||
return protocols;
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::processAppStorePurchase(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const QString &productId,
|
||||
int *duplicateServerIndex)
|
||||
@@ -993,9 +998,8 @@ QFuture<QPair<ErrorCode, QString>> SubscriptionController::getRenewalLink(const
|
||||
auto gatewayController = QSharedPointer<GatewayController>::create(m_appSettingsRepository->getGatewayEndpoint(isTestPurchase),
|
||||
m_appSettingsRepository->isDevGatewayEnv(isTestPurchase),
|
||||
apiDefs::requestTimeoutMsecs,
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled(),
|
||||
m_appSettingsRepository);
|
||||
auto postFuture = gatewayController->postAsync(QString("%1v1/renewal_link"), apiPayload);
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled());
|
||||
auto postFuture = gatewayController->postAsync(QString("%1v1/renewal_link"), apiPayload, nullptr, gatewayController);
|
||||
auto *watcher = new QFutureWatcher<QPair<ErrorCode, QByteArray>>();
|
||||
QObject::connect(watcher, &QFutureWatcher<QPair<ErrorCode, QByteArray>>::finished,
|
||||
[promise, watcher, gatewayController]() {
|
||||
@@ -1016,74 +1020,3 @@ QFuture<QPair<ErrorCode, QString>> SubscriptionController::getRenewalLink(const
|
||||
return promise->future();
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::resolveImportServiceCaptcha(const QString &userCountryCode,
|
||||
const QString &serviceType,
|
||||
const QString &serviceProtocol,
|
||||
const ProtocolData &protocolData,
|
||||
const QString &captchaId,
|
||||
const QString &captchaSolution,
|
||||
CaptchaInfo *retryCaptchaOut)
|
||||
{
|
||||
GatewayRequestData gatewayRequestData{QSysInfo::productType(),
|
||||
QString(APP_VERSION),
|
||||
m_appSettingsRepository->getAppLanguage().name().split("_").first(),
|
||||
m_appSettingsRepository->getInstallationUuid(true),
|
||||
userCountryCode,
|
||||
"",
|
||||
serviceType,
|
||||
serviceProtocol,
|
||||
QJsonObject()};
|
||||
|
||||
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
|
||||
appendProtocolDataToApiPayload(serviceProtocol, protocolData, apiPayload);
|
||||
|
||||
apiPayload["captcha_id"] = captchaId;
|
||||
QString normalizedSolution;
|
||||
normalizedSolution.reserve(captchaSolution.size());
|
||||
for (const QChar &ch : captchaSolution) {
|
||||
const ushort u = ch.unicode();
|
||||
if (u >= '0' && u <= '9') {
|
||||
normalizedSolution += ch;
|
||||
} else if (u >= 0xFF10 && u <= 0xFF19) {
|
||||
normalizedSolution += QChar(static_cast<char16_t>(u - 0xFF10 + '0'));
|
||||
}
|
||||
}
|
||||
apiPayload["captcha_solution"] = normalizedSolution.isEmpty() ? captchaSolution.trimmed() : normalizedSolution;
|
||||
|
||||
QByteArray responseBody;
|
||||
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
if (retryCaptchaOut
|
||||
&& (errorCode == ErrorCode::ApiCaptchaInvalidError || errorCode == ErrorCode::ApiCaptchaRefreshError
|
||||
|| errorCode == ErrorCode::ApiCaptchaRequiredError)) {
|
||||
const QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
|
||||
if (jsonDoc.isObject()) {
|
||||
const QJsonObject jsonObj = jsonDoc.object();
|
||||
if (jsonObj.contains(QStringLiteral("captcha_id")) && jsonObj.contains(QStringLiteral("captcha_image"))) {
|
||||
retryCaptchaOut->captchaId = jsonObj.value(QStringLiteral("captcha_id")).toString();
|
||||
retryCaptchaOut->captchaImageBase64 = jsonObj.value(QStringLiteral("captcha_image")).toString();
|
||||
retryCaptchaOut->hint = jsonObj.value(QStringLiteral("hint")).toString();
|
||||
retryCaptchaOut->isRequired = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
QJsonObject serverConfigJson;
|
||||
errorCode = extractServerConfigJsonFromResponse(responseBody, serviceProtocol, protocolData, serverConfigJson);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
updateApiConfigInJson(serverConfigJson, serviceType, serviceProtocol, userCountryCode, responseBody);
|
||||
|
||||
if (serverConfigJson.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
ApiV2ServerConfig apiV2ServerConfig = ApiV2ServerConfig::fromJson(serverConfigJson);
|
||||
m_serversRepository->addServer(QString(), apiV2ServerConfig.toJson(),
|
||||
serverConfigUtils::configTypeFromJson(apiV2ServerConfig.toJson()));
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef SUBSCRIPTIONCONTROLLER_H
|
||||
#define SUBSCRIPTIONCONTROLLER_H
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QByteArray>
|
||||
#include <QFuture>
|
||||
@@ -42,13 +43,6 @@ public:
|
||||
QJsonObject toJsonObject() const;
|
||||
};
|
||||
|
||||
struct CaptchaInfo {
|
||||
QString captchaId;
|
||||
QString captchaImageBase64;
|
||||
QString hint;
|
||||
bool isRequired = false;
|
||||
};
|
||||
|
||||
explicit SubscriptionController(SecureServersRepository* serversRepository,
|
||||
SecureAppSettingsRepository* appSettingsRepository);
|
||||
|
||||
@@ -56,11 +50,13 @@ public:
|
||||
void appendProtocolDataToApiPayload(const QString &protocol, const ProtocolData &protocolData, QJsonObject &apiPayload);
|
||||
|
||||
ErrorCode importServiceFromGateway(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const ProtocolData &protocolData,
|
||||
CaptchaInfo &captchaInfo);
|
||||
const QString &serviceProtocol, const ProtocolData &protocolData);
|
||||
ErrorCode importTrialFromGateway(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const QString &email);
|
||||
|
||||
ErrorCode importServerFromQrPairingResponse(const QString &vpnConfigKey, const QJsonObject &serviceInfo,
|
||||
const QJsonArray &supportedProtocols, int *duplicateServerIndex = nullptr);
|
||||
|
||||
ErrorCode importServiceFromAppStore(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const ProtocolData &protocolData,
|
||||
const QString &transactionId, bool isTestPurchase,
|
||||
@@ -86,8 +82,6 @@ public:
|
||||
|
||||
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;
|
||||
|
||||
ErrorCode getAccountInfo(const QString &serverId, QJsonObject &accountInfo);
|
||||
QFuture<QPair<ErrorCode, QString>> getRenewalLink(const QString &serverId);
|
||||
@@ -108,11 +102,6 @@ public:
|
||||
AppStoreRestoreResult processAppStoreRestore(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol);
|
||||
|
||||
ErrorCode resolveImportServiceCaptcha(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const ProtocolData &protocolData,
|
||||
const QString &captchaId, const QString &captchaSolution,
|
||||
CaptchaInfo *retryCaptchaOut = nullptr);
|
||||
|
||||
private:
|
||||
ErrorCode executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody, bool isTestPurchase = false);
|
||||
bool isApiKeyExpired(const QString &serverId) const;
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/protocols/protocolUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/payloadSender.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include "core/utils/utilities.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
#include "core/utils/serverConfigUtils.h"
|
||||
#include "version.h"
|
||||
#include "core/utils/containerEnum.h"
|
||||
@@ -50,96 +51,14 @@ void ConnectionController::setConnectionState(Vpn::ConnectionState 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::isConnectionSupported(const QString &serverId) const
|
||||
{
|
||||
if (serverId.isEmpty()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
if (!isServiceReady()) {
|
||||
return ErrorCode::AmneziaServiceNotRunning;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode ConnectionController::prepareConnection(const QString &serverId,
|
||||
QJsonObject& vpnConfiguration,
|
||||
DockerContainer& container)
|
||||
{
|
||||
if (!isServiceReady()) {
|
||||
return ErrorCode::AmneziaServiceNotRunning;
|
||||
}
|
||||
|
||||
ContainerConfig containerConfigModel;
|
||||
QPair<QString, QString> dns;
|
||||
QString hostName;
|
||||
@@ -148,15 +67,13 @@ ErrorCode ConnectionController::prepareConnection(const QString &serverId,
|
||||
bool isApiConfig = false;
|
||||
|
||||
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);
|
||||
dns = { cfg->dns1, cfg->dns2 };
|
||||
hostName = cfg->hostName;
|
||||
description = cfg->description;
|
||||
break;
|
||||
@@ -166,7 +83,7 @@ ErrorCode ConnectionController::prepareConnection(const QString &serverId,
|
||||
if (!cfg.has_value()) return ErrorCode::InternalError;
|
||||
container = cfg->defaultContainer;
|
||||
containerConfigModel = cfg->containerConfig(container);
|
||||
dns = cfg->getDnsPair(primaryDns, secondaryDns);
|
||||
dns = { cfg->dns1, cfg->dns2 };
|
||||
hostName = cfg->hostName;
|
||||
description = cfg->description;
|
||||
break;
|
||||
@@ -176,7 +93,7 @@ ErrorCode ConnectionController::prepareConnection(const QString &serverId,
|
||||
if (!cfg.has_value()) return ErrorCode::InternalError;
|
||||
container = cfg->defaultContainer;
|
||||
containerConfigModel = cfg->containerConfig(container);
|
||||
dns = cfg->getDnsPair(primaryDns, secondaryDns);
|
||||
dns = { cfg->dns1, cfg->dns2 };
|
||||
hostName = cfg->hostName;
|
||||
description = cfg->description;
|
||||
break;
|
||||
@@ -188,7 +105,7 @@ ErrorCode ConnectionController::prepareConnection(const QString &serverId,
|
||||
if (!cfg.has_value()) return ErrorCode::InternalError;
|
||||
container = cfg->defaultContainer;
|
||||
containerConfigModel = cfg->containerConfig(container);
|
||||
dns = cfg->getDnsPair(primaryDns, secondaryDns);
|
||||
dns = { cfg->dns1, cfg->dns2 };
|
||||
hostName = cfg->hostName;
|
||||
description = cfg->description;
|
||||
configVersion = serverConfigUtils::ConfigSource::AmneziaGateway;
|
||||
@@ -203,6 +120,20 @@ ErrorCode ConnectionController::prepareConnection(const QString &serverId,
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
if (!isContainerSupported(container)) {
|
||||
return ErrorCode::NotSupportedOnThisPlatform;
|
||||
}
|
||||
if (dns.first.isEmpty() || !NetworkUtilities::checkIPv4Format(dns.first)) {
|
||||
if (m_appSettingsRepository->useAmneziaDns()) {
|
||||
dns.first = protocols::dns::amneziaDnsIp;
|
||||
} else {
|
||||
dns.first = m_appSettingsRepository->primaryDns();
|
||||
}
|
||||
}
|
||||
if (dns.second.isEmpty() || !NetworkUtilities::checkIPv4Format(dns.second)) {
|
||||
dns.second = m_appSettingsRepository->secondaryDns();
|
||||
}
|
||||
|
||||
vpnConfiguration = createConnectionConfiguration(dns, isApiConfig, hostName, description, configVersion,
|
||||
containerConfigModel, container);
|
||||
|
||||
@@ -219,11 +150,6 @@ ErrorCode ConnectionController::openConnection(const QString &serverId)
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -34,8 +34,6 @@ public:
|
||||
QJsonObject& vpnConfiguration,
|
||||
DockerContainer& container);
|
||||
|
||||
ErrorCode isConnectionSupported(const QString &serverId) const;
|
||||
|
||||
ErrorCode openConnection(const QString &serverId);
|
||||
|
||||
void closeConnection();
|
||||
@@ -75,8 +73,6 @@ signals:
|
||||
#endif
|
||||
|
||||
private:
|
||||
ErrorCode defaultContainerForServer(const QString &serverId, DockerContainer &container) const;
|
||||
|
||||
SecureServersRepository* m_serversRepository;
|
||||
SecureAppSettingsRepository* m_appSettingsRepository;
|
||||
VpnConnection* m_vpnConnection;
|
||||
|
||||
@@ -22,8 +22,7 @@
|
||||
#endif
|
||||
|
||||
CoreController::CoreController(const QSharedPointer<VpnConnection> &vpnConnection, SecureQSettings* settings,
|
||||
QQmlApplicationEngine *engine, QObject *parent,
|
||||
bool skipPlatformControllerInit)
|
||||
QQmlApplicationEngine *engine, QObject *parent)
|
||||
: QObject(parent), m_vpnConnection(vpnConnection), m_settings(settings), m_engine(engine)
|
||||
{
|
||||
initRepositories();
|
||||
@@ -32,10 +31,8 @@ CoreController::CoreController(const QSharedPointer<VpnConnection> &vpnConnectio
|
||||
initControllers();
|
||||
initSignalHandlers();
|
||||
|
||||
if (!skipPlatformControllerInit) {
|
||||
initAndroidController();
|
||||
initAppleController();
|
||||
}
|
||||
initAndroidController();
|
||||
initAppleController();
|
||||
initLogging();
|
||||
|
||||
m_translator = new QTranslator(this);
|
||||
@@ -156,6 +153,7 @@ void CoreController::initCoreControllers()
|
||||
m_allowedDnsController = new AllowedDnsController(m_appSettingsRepository);
|
||||
m_servicesCatalogController = new ServicesCatalogController(m_appSettingsRepository);
|
||||
m_subscriptionController = new SubscriptionController(m_serversRepository, m_appSettingsRepository);
|
||||
m_pairingController = new PairingController(m_appSettingsRepository);
|
||||
m_newsController = new NewsController(m_appSettingsRepository, m_serversRepository);
|
||||
m_updateController = new UpdateController(m_appSettingsRepository, this);
|
||||
|
||||
@@ -181,8 +179,7 @@ void CoreController::initControllers()
|
||||
#ifdef Q_OS_WINDOWS
|
||||
m_ikev2ConfigModel,
|
||||
#endif
|
||||
m_sftpConfigModel, m_socks5ConfigModel, m_mtProxyConfigModel, m_telemtConfigModel,
|
||||
m_connectionController, this);
|
||||
m_sftpConfigModel, m_socks5ConfigModel, m_mtProxyConfigModel, m_telemtConfigModel, this);
|
||||
setQmlContextProperty("InstallController", m_installUiController);
|
||||
|
||||
m_importController = new ImportUiController(m_importCoreController, this);
|
||||
@@ -194,7 +191,7 @@ void CoreController::initControllers()
|
||||
m_languageUiController = new LanguageUiController(m_settingsController, m_languageModel, this);
|
||||
setQmlContextProperty("LanguageUiController", m_languageUiController);
|
||||
|
||||
m_settingsUiController = new SettingsUiController(m_settingsController, m_serversController, this);
|
||||
m_settingsUiController = new SettingsUiController(m_settingsController, m_serversController, m_languageUiController, this);
|
||||
setQmlContextProperty("SettingsController", m_settingsUiController);
|
||||
|
||||
m_pageController = new PageController(m_serversController, m_settingsController, this);
|
||||
@@ -216,18 +213,20 @@ void CoreController::initControllers()
|
||||
setQmlContextProperty("SystemController", m_systemController);
|
||||
|
||||
m_networkReachabilityController = new NetworkReachabilityController(this);
|
||||
setQmlContextProperty("NetworkReachabilityController", m_networkReachabilityController);
|
||||
setQmlContextProperty("NetworkReachability", m_networkReachabilityController);
|
||||
m_engine->rootContext()->setContextProperty("NetworkReachabilityController", m_networkReachabilityController);
|
||||
m_engine->rootContext()->setContextProperty("NetworkReachability", m_networkReachabilityController);
|
||||
|
||||
m_servicesCatalogUiController = new ServicesCatalogUiController(m_servicesCatalogController, m_apiServicesModel, this);
|
||||
setQmlContextProperty("ServicesCatalogUiController", m_servicesCatalogUiController);
|
||||
|
||||
m_subscriptionUiController = new SubscriptionUiController(m_serversController, m_apiServicesModel, m_servicesCatalogController, m_subscriptionController,
|
||||
m_apiSubscriptionPlansModel, m_apiBenefitsModel, m_apiAccountInfoModel,
|
||||
m_apiCountryModel, m_apiDevicesModel, m_settingsController,
|
||||
m_connectionController, this);
|
||||
m_apiCountryModel, m_apiDevicesModel, m_settingsController, this);
|
||||
setQmlContextProperty("SubscriptionUiController", m_subscriptionUiController);
|
||||
|
||||
m_pairingUiController = new PairingUiController(m_pairingController, m_serversController, m_subscriptionController, m_appSettingsRepository, this);
|
||||
setQmlContextProperty("PairingUiController", m_pairingUiController);
|
||||
|
||||
m_apiNewsUiController = new ApiNewsUiController(m_newsModel, m_newsController, this);
|
||||
setQmlContextProperty("ApiNewsController", m_apiNewsUiController);
|
||||
|
||||
@@ -347,6 +346,9 @@ void CoreController::openConnectionByIndex(int serverIndex)
|
||||
if (serverId.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (m_serversModel) {
|
||||
m_serversModel->setProcessedServerIndex(serverIndex);
|
||||
}
|
||||
if (m_serversController) {
|
||||
m_serversController->setDefaultServer(serverId);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
#endif
|
||||
|
||||
#include "ui/controllers/api/subscriptionUiController.h"
|
||||
#include "ui/controllers/api/pairingUiController.h"
|
||||
#include "core/controllers/api/pairingController.h"
|
||||
#include "ui/controllers/api/apiNewsUiController.h"
|
||||
#include "ui/controllers/appSplitTunnelingUiController.h"
|
||||
#include "ui/controllers/allowedDnsUiController.h"
|
||||
@@ -82,16 +84,37 @@
|
||||
#endif
|
||||
|
||||
class CoreSignalHandlers;
|
||||
class TestMultipleImports;
|
||||
class TestAdminSelfHostedExport;
|
||||
class TestServerEdit;
|
||||
class TestDefaultServerChange;
|
||||
class TestServerEdgeCases;
|
||||
class TestSignalOrder;
|
||||
class TestServersModelSync;
|
||||
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 TestComplexOperations;
|
||||
friend class TestSettingsSignals;
|
||||
friend class TestUiServersModelAndController;
|
||||
friend class TestSelfHostedServerSetup;
|
||||
|
||||
public:
|
||||
explicit CoreController(const QSharedPointer<VpnConnection> &vpnConnection, SecureQSettings* settings,
|
||||
QQmlApplicationEngine *engine, QObject *parent = nullptr,
|
||||
bool skipPlatformControllerInit = false);
|
||||
QQmlApplicationEngine *engine, QObject *parent = nullptr);
|
||||
|
||||
PageController* pageController() const;
|
||||
void setQmlRoot();
|
||||
@@ -104,36 +127,6 @@ 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; }
|
||||
|
||||
private:
|
||||
void initRepositories();
|
||||
void initCoreControllers();
|
||||
@@ -177,6 +170,7 @@ private:
|
||||
UpdateUiController* m_updateUiController;
|
||||
|
||||
SubscriptionUiController* m_subscriptionUiController;
|
||||
PairingUiController* m_pairingUiController;
|
||||
ApiNewsUiController* m_apiNewsUiController;
|
||||
|
||||
ServicesCatalogUiController* m_servicesCatalogUiController;
|
||||
@@ -188,6 +182,7 @@ private:
|
||||
AllowedDnsController* m_allowedDnsController;
|
||||
ServicesCatalogController* m_servicesCatalogController;
|
||||
SubscriptionController* m_subscriptionController;
|
||||
PairingController* m_pairingController;
|
||||
NewsController* m_newsController;
|
||||
UpdateController* m_updateController;
|
||||
InstallController* m_installController;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "coreSignalHandlers.h"
|
||||
|
||||
#include <QTimer>
|
||||
#include <QtConcurrent>
|
||||
|
||||
#include "core/utils/selfhosted/sshSession.h"
|
||||
#include "core/utils/errorCodes.h"
|
||||
@@ -22,6 +21,7 @@
|
||||
#include "ui/controllers/selfhosted/installUiController.h"
|
||||
#include "ui/controllers/importUiController.h"
|
||||
#include "ui/controllers/api/subscriptionUiController.h"
|
||||
#include "ui/controllers/api/pairingUiController.h"
|
||||
#include "ui/controllers/updateUiController.h"
|
||||
#include "ui/models/serversModel.h"
|
||||
#include "core/controllers/serversController.h"
|
||||
@@ -34,6 +34,7 @@
|
||||
#include "core/controllers/connectionController.h"
|
||||
#include "ui/models/clientManagementModel.h"
|
||||
#include "ui/controllers/api/apiNewsUiController.h"
|
||||
#include "ui/models/api/apiCountryModel.h"
|
||||
#include "ui/models/containersModel.h"
|
||||
#include "core/utils/containerEnum.h"
|
||||
|
||||
@@ -98,6 +99,9 @@ void CoreSignalHandlers::initErrorMessagesHandler()
|
||||
connect(m_coreController->m_subscriptionUiController, &SubscriptionUiController::errorOccurred, m_coreController->m_pageController,
|
||||
qOverload<ErrorCode>(&PageController::showErrorMessage));
|
||||
|
||||
connect(m_coreController->m_pairingUiController, &PairingUiController::errorOccurred, m_coreController->m_pageController,
|
||||
qOverload<ErrorCode>(&PageController::showErrorMessage));
|
||||
|
||||
connect(m_coreController->m_settingsUiController, &SettingsUiController::errorOccurred, m_coreController->m_pageController,
|
||||
qOverload<ErrorCode>(&PageController::showErrorMessage));
|
||||
}
|
||||
@@ -125,9 +129,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_serversUiController, &ServersUiController::processedServerIdChanged,
|
||||
m_coreController->m_installUiController, [this](const QString &serverId) {
|
||||
if (!serverId.isEmpty()) {
|
||||
connect(m_coreController->m_serversUiController, &ServersUiController::processedServerIndexChanged,
|
||||
m_coreController->m_installUiController, [this](int serverIndex) {
|
||||
if (serverIndex >= 0) {
|
||||
m_coreController->m_installUiController->clearProcessedServerCredentials();
|
||||
}
|
||||
});
|
||||
@@ -145,9 +149,7 @@ void CoreSignalHandlers::initExportControllerHandler()
|
||||
});
|
||||
connect(m_coreController->m_exportController, &ExportController::revokeClientRequested, this,
|
||||
[this](const QString &serverId, int row, DockerContainer container) {
|
||||
QtConcurrent::run([this, serverId, row, container]() {
|
||||
m_coreController->m_usersController->revokeClient(serverId, row, container);
|
||||
});
|
||||
m_coreController->m_usersController->revokeClient(serverId, row, container);
|
||||
});
|
||||
connect(m_coreController->m_exportController, &ExportController::renameClientRequested, this,
|
||||
[this](const QString &serverId, int row, const QString &clientName, DockerContainer container) {
|
||||
@@ -158,17 +160,15 @@ void CoreSignalHandlers::initExportControllerHandler()
|
||||
void CoreSignalHandlers::initImportControllerHandler()
|
||||
{
|
||||
connect(m_coreController->m_importCoreController, &ImportController::importFinished, this, [this]() {
|
||||
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);
|
||||
if (!m_coreController->m_connectionController->isConnected()) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -180,14 +180,17 @@ void CoreSignalHandlers::initApiCountryModelUpdateHandler()
|
||||
if (processedServerId.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonArray availableCountries;
|
||||
QString serverCountryCode;
|
||||
|
||||
const auto apiV2 = m_coreController->m_serversRepository->apiV2Config(processedServerId);
|
||||
if (!apiV2.has_value()) {
|
||||
return;
|
||||
if (apiV2.has_value()) {
|
||||
availableCountries = apiV2->apiConfig.availableCountries;
|
||||
serverCountryCode = apiV2->apiConfig.serverCountryCode;
|
||||
}
|
||||
|
||||
m_coreController->m_apiCountryModel->updateModel(apiV2->apiConfig.availableCountries,
|
||||
apiV2->apiConfig.serverCountryCode);
|
||||
|
||||
m_coreController->m_apiCountryModel->updateModel(availableCountries, serverCountryCode);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -205,15 +208,13 @@ void CoreSignalHandlers::initAdminConfigRevokedHandler()
|
||||
{
|
||||
connect(m_coreController->m_installController, &InstallController::clientRevocationRequested, this,
|
||||
[this](const QString &serverId, const ContainerConfig &containerConfig, DockerContainer container) {
|
||||
QtConcurrent::run([this, serverId, containerConfig, container]() {
|
||||
m_coreController->m_usersController->revokeClient(serverId, containerConfig, container);
|
||||
});
|
||||
m_coreController->m_usersController->revokeClient(serverId, containerConfig, container);
|
||||
});
|
||||
|
||||
connect(m_coreController->m_installController, &InstallController::clientAppendRequested, this,
|
||||
[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_installController,
|
||||
&InstallController::clearCachedProfile);
|
||||
@@ -240,16 +241,13 @@ 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->getDefaultServerId().isEmpty()) {
|
||||
QTimer::singleShot(1000, this, [this]() { m_coreController->m_connectionUiController->toggleConnection(); });
|
||||
QTimer::singleShot(1000, this, [this]() { m_coreController->m_connectionUiController->openConnection(); });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,8 +288,6 @@ 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()
|
||||
@@ -356,9 +352,6 @@ 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()
|
||||
|
||||
@@ -10,16 +10,21 @@
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkReply>
|
||||
#include <QPromise>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
|
||||
#include <openssl/rsa.h>
|
||||
#include "QBlockCipher.h"
|
||||
#include "QRsa.h"
|
||||
|
||||
#include "amneziaApplication.h"
|
||||
#include "core/repositories/secureAppSettingsRepository.h"
|
||||
#include "core/utils/api/apiUtils.h"
|
||||
#include "core/utils/constants/apiKeys.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
#include "cryptoUtils.h"
|
||||
#include "core/utils/utilities.h"
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#include "platforms/ios/ios_controller.h"
|
||||
#endif
|
||||
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
#include "core/utils/ipcClient.h"
|
||||
@@ -27,11 +32,18 @@
|
||||
|
||||
namespace
|
||||
{
|
||||
void execNetworkWaitLoop(QEventLoop &wait)
|
||||
{
|
||||
#ifdef Q_OS_IOS
|
||||
wait.exec();
|
||||
#else
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
#endif
|
||||
}
|
||||
|
||||
constexpr QLatin1String errorResponsePattern1("No active configuration found for");
|
||||
constexpr QLatin1String errorResponsePattern2("No non-revoked public key found for");
|
||||
constexpr QLatin1String errorResponsePattern3("Account not found.");
|
||||
constexpr QLatin1String errorResponsePatternQrSessionNotFound("QR session not found");
|
||||
constexpr QLatin1String errorResponsePatternSessionNotFound("Session not found");
|
||||
|
||||
constexpr QLatin1String updateRequestResponsePattern("client version update is required");
|
||||
|
||||
@@ -39,81 +51,32 @@ namespace
|
||||
constexpr int httpStatusCodeConflict = 409;
|
||||
constexpr int httpStatusCodeNotImplemented = 501;
|
||||
constexpr int httpStatusCodePaymentRequired = 402;
|
||||
constexpr int httpStatusCodeRequestTimeout = 408;
|
||||
constexpr int httpStatusCodeUnprocessableEntity = 422;
|
||||
|
||||
constexpr QLatin1String unprocessableSubscriptionMessage("Failed to retrieve subscription information. Is it activated?");
|
||||
|
||||
constexpr int proxyStorageRequestTimeoutMsecs = 3000;
|
||||
|
||||
QStringList shuffledProxyUrls(const QStringList &proxyUrls)
|
||||
QString normalizedGatewayBase(const QString &endpoint)
|
||||
{
|
||||
QStringList shuffled = proxyUrls;
|
||||
std::random_device randomDevice;
|
||||
std::mt19937 generator(randomDevice());
|
||||
std::shuffle(shuffled.begin(), shuffled.end(), generator);
|
||||
return shuffled;
|
||||
}
|
||||
|
||||
QString getProxyUrlsCacheKey(const QString &serviceType, const QString &userCountryCode)
|
||||
{
|
||||
return QStringLiteral("service_%1_country_%2").arg(serviceType, userCountryCode);
|
||||
}
|
||||
|
||||
bool decryptProxyUrlsPayload(const QByteArray &encryptedPayload, bool isDevEnvironment, QByteArray &decryptedPayload)
|
||||
{
|
||||
QByteArray key = isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||
if (!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(encryptedPayload);
|
||||
|
||||
decryptedPayload = CryptoUtils::decryptAes256Cbc(ba, decKey, iv);
|
||||
if (decryptedPayload.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
decryptedPayload = encryptedPayload;
|
||||
QString e = endpoint.trimmed();
|
||||
if (e.isEmpty()) {
|
||||
return e;
|
||||
}
|
||||
return true;
|
||||
if (!e.endsWith(QLatin1Char('/'))) {
|
||||
e.append(QLatin1Char('/'));
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
QStringList readCachedProxyUrls(const QByteArray &cachedProxyUrlsEncrypted, bool isDevEnvironment)
|
||||
{
|
||||
if (cachedProxyUrlsEncrypted.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QByteArray cachedProxyUrlsDecrypted;
|
||||
if (!decryptProxyUrlsPayload(cachedProxyUrlsEncrypted, isDevEnvironment, cachedProxyUrlsDecrypted)) {
|
||||
qCritical() << "error decrypting cached proxy urls payload";
|
||||
return {};
|
||||
}
|
||||
|
||||
QJsonArray endpointsArray = QJsonDocument::fromJson(cachedProxyUrlsDecrypted).array();
|
||||
QStringList endpoints;
|
||||
endpoints.reserve(endpointsArray.size());
|
||||
for (const QJsonValue &endpoint : endpointsArray) {
|
||||
endpoints.push_back(endpoint.toString());
|
||||
}
|
||||
|
||||
return endpoints;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
|
||||
const bool isStrictKillSwitchEnabled, SecureAppSettingsRepository *appSettingsRepository,
|
||||
QObject *parent)
|
||||
const bool isStrictKillSwitchEnabled, QObject *parent)
|
||||
: QObject(parent),
|
||||
m_gatewayEndpoint(gatewayEndpoint),
|
||||
m_gatewayEndpoint(normalizedGatewayBase(gatewayEndpoint)),
|
||||
m_isDevEnvironment(isDevEnvironment),
|
||||
m_requestTimeoutMsecs(requestTimeoutMsecs),
|
||||
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled),
|
||||
m_appSettingsRepository(appSettingsRepository)
|
||||
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -147,29 +110,40 @@ GatewayController::EncryptedRequestData GatewayController::prepareRequest(const
|
||||
}
|
||||
#endif
|
||||
|
||||
encRequestData.key = CryptoUtils::generateRandomBytes(32);
|
||||
encRequestData.iv = CryptoUtils::generateRandomBytes(32);
|
||||
encRequestData.salt = CryptoUtils::generateRandomBytes(8);
|
||||
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 rsaKey = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||
EVP_PKEY *publicKey = CryptoUtils::loadPublicKeyFromPem(rsaKey);
|
||||
if (publicKey == nullptr) {
|
||||
qCritical() << "error loading public key from environment variables";
|
||||
encRequestData.errorCode = ErrorCode::ApiMissingAgwPublicKey;
|
||||
return encRequestData;
|
||||
}
|
||||
QByteArray encryptedKeyPayload;
|
||||
QByteArray encryptedApiPayload;
|
||||
try {
|
||||
QSimpleCrypto::QRsa rsa;
|
||||
|
||||
QByteArray encryptedKeyPayload = CryptoUtils::rsaEncrypt(QJsonDocument(keyPayload).toJson(), publicKey, RSA_PKCS1_PADDING);
|
||||
EVP_PKEY_free(publicKey);
|
||||
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;
|
||||
}
|
||||
|
||||
QByteArray encryptedApiPayload = CryptoUtils::encryptAes256Cbc(QJsonDocument(apiPayload).toJson(), encRequestData.key, encRequestData.iv);
|
||||
encryptedKeyPayload = rsa.encrypt(QJsonDocument(keyPayload).toJson(), publicKey, RSA_PKCS1_PADDING);
|
||||
EVP_PKEY_free(publicKey);
|
||||
|
||||
if (encryptedKeyPayload.isEmpty() || encryptedApiPayload.isEmpty()) {
|
||||
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;
|
||||
@@ -187,15 +161,17 @@ GatewayController::DecryptionResult GatewayController::tryDecryptResponseBody(co
|
||||
QNetworkReply::NetworkError replyError, const QByteArray &key,
|
||||
const QByteArray &iv, const QByteArray &salt)
|
||||
{
|
||||
Q_UNUSED(replyError);
|
||||
|
||||
DecryptionResult result;
|
||||
result.decryptedBody = encryptedResponseBody;
|
||||
result.isDecryptionSuccessful = false;
|
||||
|
||||
QByteArray decrypted = CryptoUtils::decryptAes256Cbc(encryptedResponseBody, key, iv);
|
||||
if (!decrypted.isEmpty()) {
|
||||
result.decryptedBody = decrypted;
|
||||
try {
|
||||
QSimpleCrypto::QBlockCipher blockCipher;
|
||||
result.decryptedBody = blockCipher.decryptAesBlockCipher(encryptedResponseBody, key, iv, "", salt);
|
||||
result.isDecryptionSuccessful = true;
|
||||
} else {
|
||||
} catch (...) {
|
||||
result.decryptedBody = encryptedResponseBody;
|
||||
result.isDecryptionSuccessful = false;
|
||||
}
|
||||
@@ -203,6 +179,29 @@ GatewayController::DecryptionResult GatewayController::tryDecryptResponseBody(co
|
||||
return result;
|
||||
}
|
||||
|
||||
GatewayController::DecryptionResult GatewayController::resolveResponseBody(const QByteArray &responseBody,
|
||||
QNetworkReply::NetworkError replyError, const QByteArray &key,
|
||||
const QByteArray &iv, const QByteArray &salt)
|
||||
{
|
||||
DecryptionResult result = tryDecryptResponseBody(responseBody, replyError, key, iv, salt);
|
||||
if (result.isDecryptionSuccessful || !m_isDevEnvironment) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const QByteArray trimmed = responseBody.trimmed();
|
||||
if (trimmed.isEmpty() || trimmed.front() != '{') {
|
||||
return result;
|
||||
}
|
||||
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(trimmed, &parseError);
|
||||
if (parseError.error == QJsonParseError::NoError && doc.isObject()) {
|
||||
result.decryptedBody = trimmed;
|
||||
result.isDecryptionSuccessful = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody)
|
||||
{
|
||||
EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload);
|
||||
@@ -217,7 +216,7 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
|
||||
|
||||
QList<QSslError> sslErrors;
|
||||
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
execNetworkWaitLoop(wait);
|
||||
|
||||
QByteArray encryptedResponseBody = reply->readAll();
|
||||
QString replyErrorString = reply->errorString();
|
||||
@@ -226,8 +225,18 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
if (encRequestData.isPlaintextLocalGateway) {
|
||||
const auto errorCode =
|
||||
apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, encryptedResponseBody);
|
||||
if (errorCode) {
|
||||
return errorCode;
|
||||
}
|
||||
responseBody = encryptedResponseBody;
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
auto decryptionResult =
|
||||
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
|
||||
resolveResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
|
||||
|
||||
if (sslErrors.isEmpty() && shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
|
||||
auto requestFunction = [&encRequestData, &encryptedResponseBody](const QString &url) {
|
||||
@@ -243,7 +252,7 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
|
||||
httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
|
||||
decryptionResult =
|
||||
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
|
||||
resolveResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
|
||||
|
||||
if (!sslErrors.isEmpty()
|
||||
|| shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
|
||||
@@ -258,9 +267,8 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
|
||||
bypassProxy(endpoint, serviceType, userCountryCode, requestFunction, replyProcessingFunction);
|
||||
}
|
||||
|
||||
responseBody = decryptionResult.decryptedBody;
|
||||
const auto errorCode =
|
||||
apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, responseBody);
|
||||
auto errorCode =
|
||||
apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, decryptionResult.decryptedBody);
|
||||
if (errorCode) {
|
||||
return errorCode;
|
||||
}
|
||||
@@ -270,14 +278,19 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
|
||||
return ErrorCode::ApiConfigDecryptionError;
|
||||
}
|
||||
|
||||
responseBody = decryptionResult.decryptedBody;
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString &endpoint, const QJsonObject apiPayload)
|
||||
QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString &endpoint, const QJsonObject &apiPayload,
|
||||
QNetworkReply **activeReplyOut,
|
||||
const QSharedPointer<GatewayController> &keepAlive)
|
||||
{
|
||||
auto promise = QSharedPointer<QPromise<QPair<ErrorCode, QByteArray>>>::create();
|
||||
promise->start();
|
||||
|
||||
const QSharedPointer<GatewayController> life = keepAlive;
|
||||
|
||||
EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload);
|
||||
if (encRequestData.errorCode != ErrorCode::NoError) {
|
||||
promise->addResult(qMakePair(encRequestData.errorCode, QByteArray()));
|
||||
@@ -286,12 +299,22 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
|
||||
}
|
||||
|
||||
QNetworkReply *reply = amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody);
|
||||
if (activeReplyOut) {
|
||||
*activeReplyOut = reply;
|
||||
}
|
||||
|
||||
auto sslErrors = QSharedPointer<QList<QSslError>>::create();
|
||||
|
||||
connect(reply, &QNetworkReply::sslErrors, [sslErrors](const QList<QSslError> &errors) { *sslErrors = errors; });
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [promise, sslErrors, encRequestData, endpoint, apiPayload, reply, this]() mutable {
|
||||
connect(reply, &QNetworkReply::finished, reply, [promise, sslErrors, encRequestData, endpoint, apiPayload, reply, life]() mutable {
|
||||
if (!life) {
|
||||
promise->addResult(qMakePair(ErrorCode::ApiConfigDecryptionError, QByteArray()));
|
||||
promise->finish();
|
||||
return;
|
||||
}
|
||||
|
||||
GatewayController *const ctl = life.data();
|
||||
QByteArray encryptedResponseBody = reply->readAll();
|
||||
QString replyErrorString = reply->errorString();
|
||||
auto replyError = reply->error();
|
||||
@@ -299,8 +322,20 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
if (encRequestData.isPlaintextLocalGateway) {
|
||||
const auto errorCode = apiUtils::checkNetworkReplyErrors(*sslErrors, replyErrorString, replyError, httpStatusCode,
|
||||
encryptedResponseBody);
|
||||
if (errorCode) {
|
||||
promise->addResult(qMakePair(errorCode, QByteArray()));
|
||||
} else {
|
||||
promise->addResult(qMakePair(ErrorCode::NoError, encryptedResponseBody));
|
||||
}
|
||||
promise->finish();
|
||||
return;
|
||||
}
|
||||
|
||||
auto decryptionResult =
|
||||
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
|
||||
ctl->resolveResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
|
||||
|
||||
auto processResponse = [promise, encRequestData](const GatewayController::DecryptionResult &decryptionResult,
|
||||
const QList<QSslError> &sslErrors, QNetworkReply::NetworkError replyError,
|
||||
@@ -308,12 +343,13 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
|
||||
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode,
|
||||
decryptionResult.decryptedBody);
|
||||
if (errorCode) {
|
||||
promise->addResult(qMakePair(errorCode, decryptionResult.decryptedBody));
|
||||
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();
|
||||
@@ -324,13 +360,13 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
|
||||
promise->finish();
|
||||
};
|
||||
|
||||
if (sslErrors->isEmpty() && shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
|
||||
if (sslErrors->isEmpty() && ctl->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) {
|
||||
if (ctl->m_isDevEnvironment) {
|
||||
primaryBaseUrls = QString(DEV_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
} else {
|
||||
primaryBaseUrls = QString(PROD_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
@@ -356,21 +392,28 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
|
||||
QStringList proxyStorageUrls;
|
||||
appendStorageUrls(primaryBaseUrls, proxyStorageUrls);
|
||||
appendStorageUrls(fallbackBaseUrls, proxyStorageUrls);
|
||||
const QString proxyUrlsCacheKey = getProxyUrlsCacheKey(serviceType, userCountryCode);
|
||||
|
||||
getProxyUrlsAsync(proxyStorageUrls, 0, proxyUrlsCacheKey, [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);
|
||||
});
|
||||
});
|
||||
});
|
||||
life->getProxyUrlsAsync(life, proxyStorageUrls, 0,
|
||||
[life, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) {
|
||||
life->getProxyUrlAsync(life, proxyUrls, 0,
|
||||
[life, encRequestData, endpoint, processResponse](
|
||||
const QString &proxyUrl) {
|
||||
life->bypassProxyAsync(
|
||||
life, endpoint, proxyUrl, encRequestData,
|
||||
[processResponse](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);
|
||||
@@ -404,6 +447,8 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
|
||||
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) {
|
||||
@@ -419,12 +464,10 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
|
||||
QStringList proxyStorageUrls;
|
||||
appendStorageUrls(primaryBaseUrls, proxyStorageUrls);
|
||||
appendStorageUrls(fallbackBaseUrls, proxyStorageUrls);
|
||||
const QString proxyUrlsCacheKey = getProxyUrlsCacheKey(serviceType, userCountryCode);
|
||||
const QByteArray cachedProxyUrlsEncrypted = m_appSettingsRepository->readGatewayProxyUrls(proxyUrlsCacheKey);
|
||||
|
||||
if (proxyStorageUrls.empty()) {
|
||||
qDebug() << "empty storage endpoint list";
|
||||
return readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment);
|
||||
return {};
|
||||
}
|
||||
|
||||
for (const auto &proxyStorageUrl : proxyStorageUrls) {
|
||||
@@ -433,14 +476,32 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
|
||||
|
||||
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
|
||||
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
execNetworkWaitLoop(wait);
|
||||
|
||||
if (reply->error() == QNetworkReply::NetworkError::NoError) {
|
||||
auto encryptedResponseBody = reply->readAll();
|
||||
reply->deleteLater();
|
||||
|
||||
EVP_PKEY *privateKey = nullptr;
|
||||
QByteArray responseBody;
|
||||
if (!decryptProxyUrlsPayload(encryptedResponseBody, m_isDevEnvironment, 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;
|
||||
}
|
||||
@@ -451,8 +512,6 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
|
||||
for (const auto &endpoint : endpointsArray) {
|
||||
endpoints.push_back(endpoint.toString());
|
||||
}
|
||||
m_appSettingsRepository->writeGatewayProxyUrls(proxyUrlsCacheKey, encryptedResponseBody);
|
||||
|
||||
return endpoints;
|
||||
} else {
|
||||
auto replyError = reply->error();
|
||||
@@ -464,12 +523,16 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
|
||||
reply->deleteLater();
|
||||
}
|
||||
}
|
||||
return readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment);
|
||||
return {};
|
||||
}
|
||||
|
||||
bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody,
|
||||
bool isDecryptionSuccessful)
|
||||
{
|
||||
if (m_isDevEnvironment) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QByteArray &responseBody = decryptedResponseBody;
|
||||
|
||||
int apiHttpStatus = -1;
|
||||
@@ -495,19 +558,15 @@ bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &rep
|
||||
qDebug() << "the response contains an html tag";
|
||||
return true;
|
||||
}
|
||||
if (apiHttpStatus == httpStatusCodeRequestTimeout) {
|
||||
return false;
|
||||
}
|
||||
if (apiHttpStatus == httpStatusCodeNotFound) {
|
||||
if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2)
|
||||
|| responseBody.contains(errorResponsePattern3) || responseBody.contains(errorResponsePatternQrSessionNotFound)
|
||||
|| responseBody.contains(errorResponsePatternSessionNotFound)) {
|
||||
|| responseBody.contains(errorResponsePattern3)) {
|
||||
return false;
|
||||
} else {
|
||||
qDebug() << replyError;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (apiHttpStatus == httpStatusCodeNotImplemented) {
|
||||
if (responseBody.contains(updateRequestResponsePattern)) {
|
||||
return false;
|
||||
@@ -554,7 +613,7 @@ void GatewayController::bypassProxy(const QString &endpoint, const QString &serv
|
||||
|
||||
QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
|
||||
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
execNetworkWaitLoop(wait);
|
||||
|
||||
auto result = replyProcessingFunction(reply, sslErrors);
|
||||
reply->deleteLater();
|
||||
@@ -576,7 +635,7 @@ void GatewayController::bypassProxy(const QString &endpoint, const QString &serv
|
||||
|
||||
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
|
||||
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
execNetworkWaitLoop(wait);
|
||||
|
||||
if (reply->error() == QNetworkReply::NetworkError::NoError) {
|
||||
reply->deleteLater();
|
||||
@@ -605,13 +664,16 @@ void GatewayController::bypassProxy(const QString &endpoint, const QString &serv
|
||||
}
|
||||
}
|
||||
|
||||
void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
|
||||
const QString &proxyUrlsCacheKey, std::function<void(const QStringList &)> onComplete)
|
||||
void GatewayController::getProxyUrlsAsync(const QSharedPointer<GatewayController> &life, const QStringList &proxyStorageUrls,
|
||||
const int currentProxyStorageIndex, const std::function<void(const QStringList &)> &onComplete)
|
||||
{
|
||||
const QByteArray cachedProxyUrlsEncrypted = m_appSettingsRepository->readGatewayProxyUrls(proxyUrlsCacheKey);
|
||||
if (!life) {
|
||||
onComplete({});
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentProxyStorageIndex >= proxyStorageUrls.size()) {
|
||||
onComplete(shuffledProxyUrls(readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment)));
|
||||
onComplete({});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -622,29 +684,61 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co
|
||||
|
||||
QNetworkReply *reply = amnApp->networkManager()->get(request);
|
||||
|
||||
// connect(reply, &QNetworkReply::sslErrors, this, [state](const QList<QSslError> &e) { *(state->sslErrors) = e; });
|
||||
connect(reply, &QNetworkReply::finished, reply, [life, proxyStorageUrls, currentProxyStorageIndex, onComplete, reply]() {
|
||||
if (!life) {
|
||||
onComplete({});
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
GatewayController *const ctl = life.data();
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this,
|
||||
[this, proxyStorageUrls, currentProxyStorageIndex, proxyUrlsCacheKey, onComplete, reply]() {
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
QByteArray encrypted = reply->readAll();
|
||||
reply->deleteLater();
|
||||
|
||||
QByteArray responseBody;
|
||||
if (!decryptProxyUrlsPayload(encrypted, m_isDevEnvironment, responseBody)) {
|
||||
try {
|
||||
QByteArray key = ctl->m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||
if (!ctl->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, proxyUrlsCacheKey, onComplete); }, Qt::QueuedConnection);
|
||||
QTimer::singleShot(0, ctl, [life, proxyStorageUrls, currentProxyStorageIndex, onComplete]() {
|
||||
if (life) {
|
||||
life->getProxyUrlsAsync(life, proxyStorageUrls, currentProxyStorageIndex + 1, onComplete);
|
||||
} else {
|
||||
onComplete({});
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonArray endpointsArray = QJsonDocument::fromJson(responseBody).array();
|
||||
QStringList endpoints;
|
||||
for (const QJsonValue &endpoint : endpointsArray)
|
||||
for (const QJsonValue &endpoint : endpointsArray) {
|
||||
endpoints.push_back(endpoint.toString());
|
||||
m_appSettingsRepository->writeGatewayProxyUrls(proxyUrlsCacheKey, encrypted);
|
||||
}
|
||||
|
||||
onComplete(shuffledProxyUrls(endpoints));
|
||||
QStringList shuffled = endpoints;
|
||||
std::random_device randomDevice;
|
||||
std::mt19937 generator(randomDevice());
|
||||
std::shuffle(shuffled.begin(), shuffled.end(), generator);
|
||||
|
||||
onComplete(shuffled);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -652,16 +746,26 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co
|
||||
qDebug() << httpStatusCode;
|
||||
qDebug() << "go to the next storage endpoint";
|
||||
reply->deleteLater();
|
||||
QMetaObject::invokeMethod(
|
||||
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, proxyUrlsCacheKey, onComplete); }, Qt::QueuedConnection);
|
||||
QTimer::singleShot(0, ctl, [life, proxyStorageUrls, currentProxyStorageIndex, onComplete]() {
|
||||
if (life) {
|
||||
life->getProxyUrlsAsync(life, proxyStorageUrls, currentProxyStorageIndex + 1, onComplete);
|
||||
} else {
|
||||
onComplete({});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void GatewayController::getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex,
|
||||
std::function<void(const QString &)> onComplete)
|
||||
void GatewayController::getProxyUrlAsync(const QSharedPointer<GatewayController> &life, const QStringList &proxyUrls,
|
||||
const int currentProxyIndex, const std::function<void(const QString &)> &onComplete)
|
||||
{
|
||||
if (!life) {
|
||||
onComplete(QString());
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentProxyIndex >= proxyUrls.size()) {
|
||||
onComplete("");
|
||||
onComplete(QString());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -672,13 +776,16 @@ void GatewayController::getProxyUrlAsync(const QStringList proxyUrls, const int
|
||||
|
||||
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]() {
|
||||
connect(reply, &QNetworkReply::finished, reply, [life, proxyUrls, currentProxyIndex, onComplete, reply]() {
|
||||
reply->deleteLater();
|
||||
|
||||
if (!life) {
|
||||
onComplete(QString());
|
||||
return;
|
||||
}
|
||||
|
||||
GatewayController *const ctl = life.data();
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
m_proxyUrl = proxyUrls[currentProxyIndex];
|
||||
onComplete(m_proxyUrl);
|
||||
@@ -686,15 +793,28 @@ void GatewayController::getProxyUrlAsync(const QStringList proxyUrls, const int
|
||||
}
|
||||
|
||||
qDebug() << "go to the next proxy endpoint";
|
||||
QMetaObject::invokeMethod(this, [=]() { getProxyUrlAsync(proxyUrls, currentProxyIndex + 1, onComplete); }, Qt::QueuedConnection);
|
||||
QTimer::singleShot(0, ctl, [life, proxyUrls, currentProxyIndex, onComplete]() {
|
||||
if (life) {
|
||||
life->getProxyUrlAsync(life, proxyUrls, currentProxyIndex + 1, onComplete);
|
||||
} else {
|
||||
onComplete(QString());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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)
|
||||
const QSharedPointer<GatewayController> &life, const QString &endpoint, const QString &proxyUrl,
|
||||
const EncryptedRequestData &encRequestData,
|
||||
const std::function<void(const QByteArray &, bool, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)>
|
||||
&onComplete)
|
||||
{
|
||||
auto sslErrors = QSharedPointer<QList<QSslError>>::create();
|
||||
if (!life) {
|
||||
onComplete(QByteArray(), false, *sslErrors, QNetworkReply::InternalServerError, QStringLiteral("gateway gone"), 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (proxyUrl.isEmpty()) {
|
||||
onComplete(QByteArray(), false, *sslErrors, QNetworkReply::InternalServerError, "empty proxy url", 0);
|
||||
return;
|
||||
@@ -705,9 +825,9 @@ void GatewayController::bypassProxyAsync(
|
||||
|
||||
QNetworkReply *reply = amnApp->networkManager()->post(request, encRequestData.requestBody);
|
||||
|
||||
connect(reply, &QNetworkReply::sslErrors, this, [sslErrors](const QList<QSslError> &errors) { *sslErrors = errors; });
|
||||
connect(reply, &QNetworkReply::sslErrors, reply, [sslErrors](const QList<QSslError> &errors) { *sslErrors = errors; });
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [sslErrors, onComplete, encRequestData, reply, this]() {
|
||||
connect(reply, &QNetworkReply::finished, reply, [life, sslErrors, onComplete, encRequestData, reply]() {
|
||||
QByteArray encryptedResponseBody = reply->readAll();
|
||||
QString replyErrorString = reply->errorString();
|
||||
auto replyError = reply->error();
|
||||
@@ -715,8 +835,13 @@ void GatewayController::bypassProxyAsync(
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
auto decryptionResult =
|
||||
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
|
||||
if (!life) {
|
||||
onComplete(QByteArray(), false, *sslErrors, QNetworkReply::InternalServerError, QStringLiteral("gateway gone"), 0);
|
||||
return;
|
||||
}
|
||||
|
||||
auto decryptionResult = life->resolveResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv,
|
||||
encRequestData.salt);
|
||||
|
||||
onComplete(decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful, *sslErrors, replyError, replyErrorString,
|
||||
httpStatusCode);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#ifndef GATEWAYCONTROLLER_H
|
||||
#define GATEWAYCONTROLLER_H
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include <QFuture>
|
||||
#include <QNetworkReply>
|
||||
#include <QObject>
|
||||
@@ -16,19 +18,18 @@
|
||||
#include "platforms/ios/ios_controller.h"
|
||||
#endif
|
||||
|
||||
class SecureAppSettingsRepository;
|
||||
|
||||
class GatewayController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
|
||||
const bool isStrictKillSwitchEnabled, SecureAppSettingsRepository *appSettingsRepository,
|
||||
QObject *parent = nullptr);
|
||||
const bool isStrictKillSwitchEnabled, QObject *parent = nullptr);
|
||||
|
||||
amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody);
|
||||
QFuture<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject apiPayload);
|
||||
QFuture<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject &apiPayload,
|
||||
QNetworkReply **activeReplyOut = nullptr,
|
||||
const QSharedPointer<GatewayController> &keepAlive = {});
|
||||
|
||||
private:
|
||||
struct EncryptedRequestData
|
||||
@@ -39,6 +40,7 @@ private:
|
||||
QByteArray iv;
|
||||
QByteArray salt;
|
||||
amnezia::ErrorCode errorCode;
|
||||
bool isPlaintextLocalGateway = false;
|
||||
};
|
||||
|
||||
struct DecryptionResult
|
||||
@@ -50,6 +52,8 @@ private:
|
||||
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);
|
||||
DecryptionResult resolveResponseBody(const QByteArray &responseBody, 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);
|
||||
@@ -57,18 +61,18 @@ private:
|
||||
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,
|
||||
const QString &proxyUrlsCacheKey, std::function<void(const QStringList &)> onComplete);
|
||||
void getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex, std::function<void(const QString &)> onComplete);
|
||||
void getProxyUrlsAsync(const QSharedPointer<GatewayController> &life, const QStringList &proxyStorageUrls, int currentProxyStorageIndex,
|
||||
const std::function<void(const QStringList &)> &onComplete);
|
||||
void getProxyUrlAsync(const QSharedPointer<GatewayController> &life, const QStringList &proxyUrls, int currentProxyIndex,
|
||||
const 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);
|
||||
const QSharedPointer<GatewayController> &life, const QString &endpoint, const QString &proxyUrl, const EncryptedRequestData &encRequestData,
|
||||
const 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;
|
||||
};
|
||||
|
||||
@@ -48,7 +48,6 @@ signals:
|
||||
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:
|
||||
|
||||
@@ -486,7 +486,7 @@ QJsonObject ImportController::extractOpenVpnConfig(const QString &data) const
|
||||
QJsonObject config;
|
||||
config[configKey::containers] = arr;
|
||||
config[configKey::defaultContainer] = configKey::amneziaOpenvpn;
|
||||
config[configKey::description] = m_serversRepository->nextAvailableServerName();
|
||||
config[configKey::description] = m_appSettingsRepository->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);
|
||||
@@ -645,7 +645,7 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data, Config
|
||||
QJsonObject config;
|
||||
config[configKey::containers] = arr;
|
||||
config[configKey::defaultContainer] = containerName;
|
||||
config[configKey::description] = m_serversRepository->nextAvailableServerName();
|
||||
config[configKey::description] = m_appSettingsRepository->nextAvailableServerName();
|
||||
|
||||
const static QRegularExpression dnsRegExp(
|
||||
"DNS = "
|
||||
@@ -699,7 +699,7 @@ QJsonObject ImportController::extractXrayConfig(const QString &data, ConfigTypes
|
||||
? configKey::amneziaSsxray
|
||||
: configKey::amneziaXray;
|
||||
if (description.isEmpty()) {
|
||||
config[configKey::description] = m_serversRepository->nextAvailableServerName();
|
||||
config[configKey::description] = m_appSettingsRepository->nextAvailableServerName();
|
||||
} else {
|
||||
config[configKey::description] = description;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
#include "core/installers/sftpInstaller.h"
|
||||
#include "core/installers/socks5Installer.h"
|
||||
#include "core/installers/mtProxyInstaller.h"
|
||||
#include "core/configurators/xrayConfigurator.h"
|
||||
#include "core/installers/telemtInstaller.h"
|
||||
#include "core/installers/torInstaller.h"
|
||||
#include "core/installers/wireguardInstaller.h"
|
||||
@@ -72,16 +71,6 @@ namespace
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
QString buildRemoveContainerScript(const amnezia::ScriptVars &vars, bool removeDataVolume)
|
||||
{
|
||||
QString script = SshSession::replaceVars(amnezia::scriptData(SharedScriptType::remove_container), vars);
|
||||
if (removeDataVolume) {
|
||||
script += QLatin1String("\nsudo docker volume rm -f $CONTAINER_NAME-data 2>/dev/null || true");
|
||||
script = SshSession::replaceVars(script, vars);
|
||||
}
|
||||
return script;
|
||||
}
|
||||
}
|
||||
|
||||
InstallController::InstallController(SecureServersRepository *serversRepository,
|
||||
@@ -103,7 +92,7 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
|
||||
bool isUpdate)
|
||||
{
|
||||
qDebug().noquote() << "InstallController::setupContainer" << ContainerUtils::containerToString(container);
|
||||
SshSession sshSession;
|
||||
SshSession sshSession(this);
|
||||
ErrorCode e = ErrorCode::NoError;
|
||||
|
||||
e = isUserInSudo(credentials, sshSession);
|
||||
@@ -130,10 +119,9 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
|
||||
return e;
|
||||
qDebug().noquote() << "InstallController::setupContainer prepareHostWorker finished";
|
||||
|
||||
const amnezia::ScriptVars removeContainerVars =
|
||||
amnezia::genBaseVars(credentials, container, QString(), QString());
|
||||
const bool removeDataVolume = !isUpdate && (container == DockerContainer::MtProxy || container == DockerContainer::Telemt);
|
||||
sshSession.runScript(credentials, buildRemoveContainerScript(removeContainerVars, removeDataVolume));
|
||||
sshSession.runScript(credentials,
|
||||
sshSession.replaceVars(amnezia::scriptData(SharedScriptType::remove_container),
|
||||
amnezia::genBaseVars(credentials, container, QString(), QString())));
|
||||
qDebug().noquote() << "InstallController::setupContainer removeContainer finished";
|
||||
|
||||
qDebug().noquote() << "buildContainerWorker start";
|
||||
@@ -158,8 +146,8 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
|
||||
return startupContainerWorker(credentials, container, config, sshSession);
|
||||
}
|
||||
|
||||
ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerContainer container, const ContainerConfig &oldConfig,
|
||||
ContainerConfig &newConfig)
|
||||
ErrorCode InstallController::updateContainer(const QString &serverId, DockerContainer container, const ContainerConfig &oldConfig,
|
||||
ContainerConfig &newConfig)
|
||||
{
|
||||
if (!isUpdateDockerContainerRequired(container, oldConfig, newConfig)) {
|
||||
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
@@ -168,11 +156,11 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
|
||||
}
|
||||
if (container == DockerContainer::MtProxy) {
|
||||
ServerCredentials credentials = adminConfig->credentials();
|
||||
SshSession sshSession;
|
||||
SshSession sshSession(this);
|
||||
MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
|
||||
} else if (container == DockerContainer::Telemt) {
|
||||
ServerCredentials credentials = adminConfig->credentials();
|
||||
SshSession sshSession;
|
||||
SshSession sshSession(this);
|
||||
TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
|
||||
}
|
||||
adminConfig->updateContainerConfig(container, newConfig);
|
||||
@@ -188,20 +176,10 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
|
||||
if (!credentials.isValid()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
SshSession sshSession;
|
||||
SshSession sshSession(this);
|
||||
|
||||
bool reinstallRequired = isReinstallContainerRequired(container, oldConfig, newConfig);
|
||||
qDebug() << "InstallController::updateServerConfig for container" << container << "reinstall required is" << reinstallRequired;
|
||||
|
||||
bool xrayServerSettingsChanged = false;
|
||||
if (container == DockerContainer::Xray || container == DockerContainer::SSXray) {
|
||||
const auto *oldXrayConfig = oldConfig.getXrayProtocolConfig();
|
||||
const auto *newXrayConfig = newConfig.getXrayProtocolConfig();
|
||||
if (oldXrayConfig && newXrayConfig) {
|
||||
xrayServerSettingsChanged =
|
||||
!oldXrayConfig->serverConfig.hasEqualServerSettings(newXrayConfig->serverConfig);
|
||||
}
|
||||
}
|
||||
qDebug() << "InstallController::updateContainer for container" << container << "reinstall required is" << reinstallRequired;
|
||||
|
||||
ErrorCode errorCode = ErrorCode::NoError;
|
||||
if (reinstallRequired) {
|
||||
@@ -211,27 +189,6 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
errorCode = startupContainerWorker(credentials, container, newConfig, sshSession);
|
||||
}
|
||||
|
||||
if (errorCode == ErrorCode::NoError
|
||||
&& (container == DockerContainer::MtProxy || container == DockerContainer::Telemt)) {
|
||||
const QString containerName = ContainerUtils::containerToString(container);
|
||||
errorCode = sshSession.runScript(credentials, "sudo docker restart " + containerName);
|
||||
}
|
||||
}
|
||||
|
||||
const bool skipXrayInboundSync =
|
||||
newConfig.getXrayProtocolConfig() && newConfig.getXrayProtocolConfig()->serverConfig.isThirdPartyConfig;
|
||||
|
||||
if (errorCode == ErrorCode::NoError && xrayServerSettingsChanged && !skipXrayInboundSync) {
|
||||
DnsSettings dnsSettings = { m_appSettingsRepository->primaryDns(), m_appSettingsRepository->secondaryDns() };
|
||||
XrayConfigurator xrayConfigurator(&sshSession);
|
||||
qDebug() << "InstallController::updateServerConfig applying Xray server inbound sync, reinstall="
|
||||
<< reinstallRequired;
|
||||
errorCode = xrayConfigurator.applyServerSettingsToRemote(credentials, container, newConfig, dnsSettings, false);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
qDebug() << "InstallController::updateServerConfig Xray inbound sync failed, error="
|
||||
<< static_cast<int>(errorCode);
|
||||
}
|
||||
}
|
||||
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
@@ -240,9 +197,7 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
|
||||
} else if (container == DockerContainer::Telemt) {
|
||||
TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
|
||||
}
|
||||
if (reinstallRequired) {
|
||||
clearCachedProfile(serverId, container);
|
||||
}
|
||||
clearCachedProfile(serverId, container);
|
||||
adminConfig->updateContainerConfig(container, newConfig);
|
||||
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
|
||||
}
|
||||
@@ -250,41 +205,6 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
ErrorCode InstallController::updateClientConfig(const QString &serverId, DockerContainer container, ContainerConfig &newConfig)
|
||||
{
|
||||
switch (m_serversRepository->serverKind(serverId)) {
|
||||
case serverConfigUtils::ConfigType::SelfHostedAdmin: {
|
||||
auto config = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
if (!config.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
config->updateContainerConfig(container, newConfig);
|
||||
m_serversRepository->editServer(serverId, config->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
case serverConfigUtils::ConfigType::SelfHostedUser: {
|
||||
auto config = m_serversRepository->selfHostedUserConfig(serverId);
|
||||
if (!config.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
config->updateContainerConfig(container, newConfig);
|
||||
m_serversRepository->editServer(serverId, config->toJson(), serverConfigUtils::ConfigType::SelfHostedUser);
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
case serverConfigUtils::ConfigType::Native: {
|
||||
auto config = m_serversRepository->nativeConfig(serverId);
|
||||
if (!config.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
config->updateContainerConfig(container, newConfig);
|
||||
m_serversRepository->editServer(serverId, config->toJson(), serverConfigUtils::ConfigType::Native);
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
default:
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
}
|
||||
|
||||
void InstallController::clearCachedProfile(const QString &serverId, DockerContainer container)
|
||||
{
|
||||
if (ContainerUtils::containerService(container) == ServiceType::Other) {
|
||||
@@ -296,9 +216,9 @@ void InstallController::clearCachedProfile(const QString &serverId, DockerContai
|
||||
return;
|
||||
}
|
||||
|
||||
adminConfig->clearCachedClientProfile(container);
|
||||
const ContainerConfig containerConfigModel = adminConfig->containerConfig(container);
|
||||
|
||||
adminConfig->clearCachedClientProfile(container);
|
||||
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
|
||||
|
||||
emit clientRevocationRequested(serverId, containerConfigModel, container);
|
||||
@@ -306,74 +226,37 @@ void InstallController::clearCachedProfile(const QString &serverId, DockerContai
|
||||
|
||||
ErrorCode InstallController::validateAndPrepareConfig(const QString &serverId)
|
||||
{
|
||||
const auto kind = m_serversRepository->serverKind(serverId);
|
||||
|
||||
DockerContainer container = DockerContainer::None;
|
||||
ContainerConfig containerConfig;
|
||||
|
||||
switch (kind) {
|
||||
case serverConfigUtils::ConfigType::SelfHostedAdmin: {
|
||||
const auto cfg = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
if (!cfg.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
container = cfg->defaultContainer;
|
||||
containerConfig = cfg->containerConfig(container);
|
||||
break;
|
||||
}
|
||||
case serverConfigUtils::ConfigType::SelfHostedUser: {
|
||||
const auto cfg = m_serversRepository->selfHostedUserConfig(serverId);
|
||||
if (!cfg.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
container = cfg->defaultContainer;
|
||||
containerConfig = cfg->containerConfig(container);
|
||||
break;
|
||||
}
|
||||
case serverConfigUtils::ConfigType::Native: {
|
||||
const auto cfg = m_serversRepository->nativeConfig(serverId);
|
||||
if (!cfg.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
container = cfg->defaultContainer;
|
||||
containerConfig = cfg->containerConfig(container);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
if (container == DockerContainer::None) {
|
||||
return ErrorCode::NoInstalledContainersError;
|
||||
}
|
||||
|
||||
if (containerConfig.protocolConfig.hasClientConfig()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
if (kind != serverConfigUtils::ConfigType::SelfHostedAdmin) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
if (!adminConfig.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
DockerContainer container = adminConfig->defaultContainer;
|
||||
|
||||
if (container == DockerContainer::None) {
|
||||
return ErrorCode::NoInstalledContainersError;
|
||||
}
|
||||
|
||||
ContainerConfig containerConfig = adminConfig->containerConfig(container);
|
||||
ServerCredentials credentials = adminConfig->credentials();
|
||||
if (!credentials.isValid()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
SshSession sshSession;
|
||||
const QString clientName = QString("Admin [%1]").arg(QSysInfo::prettyProductName());
|
||||
const ErrorCode errorCode = processContainerForAdmin(container, containerConfig, credentials, sshSession, serverId, clientName);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
adminConfig->updateContainerConfig(container, containerConfig);
|
||||
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
|
||||
auto isProtocolConfigExists = [](const ContainerConfig &cfg) {
|
||||
return cfg.protocolConfig.hasClientConfig();
|
||||
};
|
||||
|
||||
if (!isProtocolConfigExists(containerConfig)) {
|
||||
QString clientName = QString("Admin [%1]").arg(QSysInfo::prettyProductName());
|
||||
ErrorCode errorCode = processContainerForAdmin(container, containerConfig, credentials, sshSession, serverId, clientName);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
adminConfig->updateContainerConfig(container, containerConfig);
|
||||
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
|
||||
}
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
@@ -407,7 +290,7 @@ void InstallController::addEmptyServer(const ServerCredentials &credentials)
|
||||
serverConfig.userName = credentials.userName;
|
||||
serverConfig.password = credentials.secretData;
|
||||
serverConfig.port = credentials.port;
|
||||
serverConfig.description = m_serversRepository->nextAvailableServerName();
|
||||
serverConfig.description = m_appSettingsRepository->nextAvailableServerName();
|
||||
serverConfig.displayName = serverConfig.description.isEmpty() ? serverConfig.hostName : serverConfig.description;
|
||||
serverConfig.defaultContainer = DockerContainer::None;
|
||||
|
||||
@@ -718,19 +601,12 @@ bool InstallController::isReinstallContainerRequired(DockerContainer container,
|
||||
}
|
||||
|
||||
if (container == DockerContainer::Xray || container == DockerContainer::SSXray) {
|
||||
const auto *oldXrayConfig = oldConfig.getXrayProtocolConfig();
|
||||
const auto *newXrayConfig = newConfig.getXrayProtocolConfig();
|
||||
|
||||
const auto* oldXrayConfig = oldConfig.getXrayProtocolConfig();
|
||||
const auto* newXrayConfig = newConfig.getXrayProtocolConfig();
|
||||
|
||||
if (oldXrayConfig && newXrayConfig) {
|
||||
const QString oldPort = oldXrayConfig->serverConfig.port.isEmpty()
|
||||
? QString(protocols::xray::defaultPort)
|
||||
: oldXrayConfig->serverConfig.port;
|
||||
const QString newPort = newXrayConfig->serverConfig.port.isEmpty()
|
||||
? QString(protocols::xray::defaultPort)
|
||||
: newXrayConfig->serverConfig.port;
|
||||
if (oldPort != newPort) {
|
||||
if (oldXrayConfig->serverConfig.port != newXrayConfig->serverConfig.port)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -745,6 +621,18 @@ bool InstallController::isReinstallContainerRequired(DockerContainer container,
|
||||
if (oldPort != newPort) {
|
||||
return true;
|
||||
}
|
||||
const QString oldTransport = oldMt->transportMode.isEmpty() ? QString(
|
||||
protocols::mtProxy::transportModeStandard)
|
||||
: oldMt->transportMode;
|
||||
const QString newTransport = newMt->transportMode.isEmpty() ? QString(
|
||||
protocols::mtProxy::transportModeStandard)
|
||||
: newMt->transportMode;
|
||||
if (oldTransport != newTransport) {
|
||||
return true;
|
||||
}
|
||||
if (oldMt->tlsDomain != newMt->tlsDomain) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -759,6 +647,39 @@ bool InstallController::isReinstallContainerRequired(DockerContainer container,
|
||||
if (oldPort != newPort) {
|
||||
return true;
|
||||
}
|
||||
const QString oldTransport = oldT->transportMode.isEmpty()
|
||||
? QString(protocols::telemt::transportModeStandard)
|
||||
: oldT->transportMode;
|
||||
const QString newTransport = newT->transportMode.isEmpty()
|
||||
? QString(protocols::telemt::transportModeStandard)
|
||||
: newT->transportMode;
|
||||
if (oldTransport != newTransport) {
|
||||
return true;
|
||||
}
|
||||
if (oldT->tlsDomain != newT->tlsDomain) {
|
||||
return true;
|
||||
}
|
||||
if (oldT->maskEnabled != newT->maskEnabled) {
|
||||
return true;
|
||||
}
|
||||
if (oldT->tlsEmulation != newT->tlsEmulation) {
|
||||
return true;
|
||||
}
|
||||
if (oldT->useMiddleProxy != newT->useMiddleProxy) {
|
||||
return true;
|
||||
}
|
||||
if (oldT->tag != newT->tag) {
|
||||
return true;
|
||||
}
|
||||
const QString oldUser = oldT->userName.isEmpty()
|
||||
? QString::fromUtf8(protocols::telemt::defaultUserName)
|
||||
: oldT->userName;
|
||||
const QString newUser = newT->userName.isEmpty()
|
||||
? QString::fromUtf8(protocols::telemt::defaultUserName)
|
||||
: newT->userName;
|
||||
if (oldUser != newUser) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -798,23 +719,9 @@ ErrorCode InstallController::installDockerWorker(const ServerCredentials &creden
|
||||
|
||||
qDebug().noquote() << "InstallController::installDockerWorker" << stdOut;
|
||||
|
||||
if (container == DockerContainer::MtProxy || container == DockerContainer::Telemt) {
|
||||
QString conntrackOut;
|
||||
auto cbConntrack = [&](const QString &data, libssh::Client &) {
|
||||
conntrackOut += data + "\n";
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
sshSession.runScript(
|
||||
credentials,
|
||||
sshSession.replaceVars(amnezia::scriptData(SharedScriptType::install_conntrack),
|
||||
amnezia::genBaseVars(credentials, DockerContainer::None, QString(), QString())),
|
||||
cbConntrack, cbConntrack);
|
||||
qDebug().noquote() << "InstallController::installDockerWorker install_conntrack:" << conntrackOut;
|
||||
}
|
||||
|
||||
if (container == DockerContainer::Awg2) {
|
||||
QRegularExpression kernelVersionRegex(R"(Linux\s+(\d+)\.(\d+)[^\d]*)");
|
||||
QRegularExpressionMatch match = kernelVersionRegex.match(stdOut);
|
||||
QRegularExpression regex(R"(Linux\s+(\d+)\.(\d+)[^\d]*)");
|
||||
QRegularExpressionMatch match = regex.match(stdOut);
|
||||
if (match.hasMatch()) {
|
||||
int majorVersion = match.captured(1).toInt();
|
||||
int minorVersion = match.captured(2).toInt();
|
||||
@@ -827,19 +734,8 @@ ErrorCode InstallController::installDockerWorker(const ServerCredentials &creden
|
||||
|
||||
if (stdOut.contains("lock"))
|
||||
return ErrorCode::ServerPacketManagerError;
|
||||
if (stdOut.contains("Container runtime is not supported"))
|
||||
return ErrorCode::ServerContainerRuntimeNotSupported;
|
||||
|
||||
QRegularExpression notFoundRegex(
|
||||
R"(^.*(?:sudo:|docker:).*not found.*$)",
|
||||
QRegularExpression::MultilineOption);
|
||||
|
||||
if (notFoundRegex.match(stdOut).hasMatch()) {
|
||||
if (stdOut.contains("command not found"))
|
||||
return ErrorCode::ServerDockerFailedError;
|
||||
}
|
||||
|
||||
if (stdOut.contains("Container runtime service not running"))
|
||||
return ErrorCode::ContainerRuntimeServiceNotRunning;
|
||||
|
||||
return error;
|
||||
}
|
||||
@@ -876,7 +772,7 @@ ErrorCode InstallController::isUserInSudo(const ServerCredentials &credentials,
|
||||
return ErrorCode::ServerUserNotInSudo;
|
||||
if (stdOut.contains("can't cd to") || stdOut.contains("Permission denied") || stdOut.contains("No such file or directory"))
|
||||
return ErrorCode::ServerUserDirectoryNotAccessible;
|
||||
if (stdOut.contains(QRegularExpression(R"(\bsudoers\b)")) || stdOut.contains("is not allowed to") || stdOut.contains("can't do that"))
|
||||
if (stdOut.contains("sudoers") || stdOut.contains("is not allowed to run sudo on"))
|
||||
return ErrorCode::ServerUserNotAllowedInSudoers;
|
||||
if (stdOut.contains("password is required") || stdOut.contains("authentication is required"))
|
||||
return ErrorCode::ServerUserPasswordRequired;
|
||||
@@ -958,7 +854,7 @@ ErrorCode InstallController::rebootServer(const QString &serverId)
|
||||
if (!credentials.isValid()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
SshSession sshSession;
|
||||
SshSession sshSession(this);
|
||||
|
||||
QString script = QString("sudo reboot");
|
||||
|
||||
@@ -986,7 +882,7 @@ ErrorCode InstallController::removeAllContainers(const QString &serverId)
|
||||
if (!credentials.isValid()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
SshSession sshSession;
|
||||
SshSession sshSession(this);
|
||||
ErrorCode errorCode = sshSession.runScript(credentials, amnezia::scriptData(SharedScriptType::remove_all_containers));
|
||||
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
@@ -1008,12 +904,11 @@ ErrorCode InstallController::removeContainer(const QString &serverId, DockerCont
|
||||
if (!credentials.isValid()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
SshSession sshSession;
|
||||
const amnezia::ScriptVars removeContainerVars =
|
||||
amnezia::genBaseVars(credentials, container, QString(), QString());
|
||||
const bool removeDataVolume = (container == DockerContainer::MtProxy || container == DockerContainer::Telemt);
|
||||
ErrorCode errorCode =
|
||||
sshSession.runScript(credentials, buildRemoveContainerScript(removeContainerVars, removeDataVolume));
|
||||
SshSession sshSession(this);
|
||||
ErrorCode errorCode = sshSession.runScript(
|
||||
credentials,
|
||||
sshSession.replaceVars(amnezia::scriptData(SharedScriptType::remove_container),
|
||||
amnezia::genBaseVars(credentials, container, QString(), QString())));
|
||||
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
QMap<DockerContainer, ContainerConfig> containers = adminConfig->containers;
|
||||
@@ -1117,7 +1012,7 @@ ErrorCode InstallController::scanServerForInstalledContainers(const QString &ser
|
||||
if (!credentials.isValid()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
SshSession sshSession;
|
||||
SshSession sshSession(this);
|
||||
|
||||
QMap<DockerContainer, ContainerConfig> installedContainers;
|
||||
ErrorCode errorCode = getAlreadyInstalledContainers(credentials, installedContainers, sshSession);
|
||||
@@ -1160,7 +1055,7 @@ ErrorCode InstallController::scanServerForInstalledContainers(const QString &ser
|
||||
ErrorCode InstallController::installServer(const ServerCredentials &credentials, DockerContainer container, int port,
|
||||
TransportProto transportProto, bool &wasContainerInstalled)
|
||||
{
|
||||
SshSession sshSession;
|
||||
SshSession sshSession(this);
|
||||
QMap<DockerContainer, ContainerConfig> installedContainers;
|
||||
ErrorCode errorCode = getAlreadyInstalledContainers(credentials, installedContainers, sshSession);
|
||||
if (errorCode) {
|
||||
@@ -1198,7 +1093,7 @@ ErrorCode InstallController::installServer(const ServerCredentials &credentials,
|
||||
serverConfig.userName = credentials.userName;
|
||||
serverConfig.password = credentials.secretData;
|
||||
serverConfig.port = credentials.port;
|
||||
serverConfig.description = m_serversRepository->nextAvailableServerName();
|
||||
serverConfig.description = m_appSettingsRepository->nextAvailableServerName();
|
||||
|
||||
for (auto iterator = preparedContainers.begin(); iterator != preparedContainers.end(); iterator++) {
|
||||
serverConfig.containers.insert(iterator.key(), iterator.value());
|
||||
@@ -1229,7 +1124,7 @@ ErrorCode InstallController::installContainer(const QString &serverId, DockerCon
|
||||
if (!credentials.isValid()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
SshSession sshSession;
|
||||
SshSession sshSession(this);
|
||||
|
||||
QMap<DockerContainer, ContainerConfig> installedContainers;
|
||||
ErrorCode errorCode = getAlreadyInstalledContainers(credentials, installedContainers, sshSession);
|
||||
@@ -1268,26 +1163,28 @@ ErrorCode InstallController::installContainer(const QString &serverId, DockerCon
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode InstallController::checkSshConnection(ServerCredentials &credentials, QString &output,
|
||||
ErrorCode InstallController::checkSshConnection(const ServerCredentials &credentials, QString &output,
|
||||
std::function<QString()> passphraseCallback)
|
||||
{
|
||||
SshSession sshSession;
|
||||
SshSession sshSession(this);
|
||||
ErrorCode errorCode = ErrorCode::NoError;
|
||||
|
||||
if (credentials.secretData.contains("BEGIN") && credentials.secretData.contains("PRIVATE KEY")) {
|
||||
ServerCredentials processedCredentials = credentials;
|
||||
|
||||
if (processedCredentials.secretData.contains("BEGIN") && processedCredentials.secretData.contains("PRIVATE KEY")) {
|
||||
if (!passphraseCallback) {
|
||||
return ErrorCode::SshPrivateKeyError;
|
||||
}
|
||||
|
||||
QString decryptedPrivateKey;
|
||||
errorCode = sshSession.getDecryptedPrivateKey(credentials, decryptedPrivateKey, passphraseCallback);
|
||||
errorCode = sshSession.getDecryptedPrivateKey(processedCredentials, decryptedPrivateKey, passphraseCallback);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
credentials.secretData = decryptedPrivateKey;
|
||||
processedCredentials.secretData = decryptedPrivateKey;
|
||||
}
|
||||
|
||||
output = sshSession.checkSshConnection(credentials, errorCode);
|
||||
output = sshSession.checkSshConnection(processedCredentials, errorCode);
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
@@ -1491,7 +1388,7 @@ ErrorCode InstallController::getAlreadyInstalledContainers(const ServerCredentia
|
||||
QString transportProtoStr = containerAndPortMatch.captured(3);
|
||||
DockerContainer container = ContainerUtils::containerFromString(name);
|
||||
|
||||
if (container == DockerContainer::None || ContainerUtils::isUnsupportedContainer(container)) {
|
||||
if (container == DockerContainer::None) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1516,7 +1413,7 @@ ErrorCode InstallController::getAlreadyInstalledContainers(const ServerCredentia
|
||||
QString transportProtoStr = torOrDnsRegMatch.captured(3);
|
||||
DockerContainer container = ContainerUtils::containerFromString(name);
|
||||
|
||||
if (container == DockerContainer::None || ContainerUtils::isUnsupportedContainer(container)) {
|
||||
if (container == DockerContainer::None) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1552,7 +1449,7 @@ ErrorCode InstallController::setDockerContainerEnabledState(const QString &serve
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
const QString containerName = ContainerUtils::containerToString(container);
|
||||
SshSession sshSession;
|
||||
SshSession sshSession(this);
|
||||
const QString script = enabled ? QStringLiteral("sudo docker start %1").arg(containerName)
|
||||
: QStringLiteral("sudo docker stop %1").arg(containerName);
|
||||
const ErrorCode runError = sshSession.runScript(credentials, script);
|
||||
@@ -1592,7 +1489,7 @@ ErrorCode InstallController::queryDockerContainerStatus(const QString &serverId,
|
||||
stdOut += data;
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
SshSession sshSession;
|
||||
SshSession sshSession(this);
|
||||
const QString script = QStringLiteral(
|
||||
"sudo docker inspect --format '{{.State.Status}}' %1 2>/dev/null || echo 'not_found'")
|
||||
.arg(containerName);
|
||||
@@ -1626,7 +1523,7 @@ ErrorCode InstallController::queryMtProxyDiagnostics(const QString &serverId, Do
|
||||
if (!credentials.isValid()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
SshSession sshSession;
|
||||
SshSession sshSession(this);
|
||||
return MtProxyInstaller::queryDiagnostics(sshSession, credentials, container, listenPort, out);
|
||||
}
|
||||
|
||||
@@ -1649,7 +1546,7 @@ QString InstallController::fetchDockerContainerSecret(const QString &serverId, D
|
||||
stdOut += data;
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
SshSession sshSession;
|
||||
SshSession sshSession(this);
|
||||
const QString path = QStringLiteral("/data/secret");
|
||||
const QString cmd = QStringLiteral("sudo docker exec %1 cat %2").arg(containerName, path);
|
||||
const ErrorCode errorCode = sshSession.runScript(credentials, cmd, cbReadStdOut);
|
||||
|
||||
@@ -34,12 +34,7 @@ public:
|
||||
~InstallController();
|
||||
|
||||
ErrorCode setupContainer(const ServerCredentials &credentials, DockerContainer container, ContainerConfig &config, bool isUpdate = false);
|
||||
|
||||
// Updates server-side container settings (admin self-hosted only): reconfigures the container over SSH.
|
||||
ErrorCode updateServerConfig(const QString &serverId, DockerContainer container, const ContainerConfig &oldConfig, ContainerConfig &newConfig);
|
||||
|
||||
// Updates client-local settings only: rewrites the stored container config for any self-hosted/native server. No SSH.
|
||||
ErrorCode updateClientConfig(const QString &serverId, DockerContainer container, ContainerConfig &newConfig);
|
||||
ErrorCode updateContainer(const QString &serverId, DockerContainer container, const ContainerConfig &oldConfig, ContainerConfig &newConfig);
|
||||
|
||||
ErrorCode rebootServer(const QString &serverId);
|
||||
ErrorCode removeAllContainers(const QString &serverId);
|
||||
@@ -69,8 +64,7 @@ public:
|
||||
|
||||
bool isUpdateDockerContainerRequired(DockerContainer container, const ContainerConfig &oldConfig, const ContainerConfig &newConfig);
|
||||
|
||||
ErrorCode checkSshConnection(ServerCredentials &credentials, QString &output,
|
||||
std::function<QString()> passphraseCallback = nullptr);
|
||||
ErrorCode checkSshConnection(const ServerCredentials &credentials, QString &output, std::function<QString()> passphraseCallback = nullptr);
|
||||
|
||||
bool isServerAlreadyExists(const ServerCredentials &credentials, int &existingServerIndex);
|
||||
|
||||
|
||||
@@ -698,7 +698,7 @@ ErrorCode UsersController::revokeXray(const int row,
|
||||
|
||||
QString restartScript = QString("sudo docker restart $CONTAINER_NAME");
|
||||
error = sshSession->runScript(
|
||||
credentials,
|
||||
credentials,
|
||||
sshSession->replaceVars(restartScript, amnezia::genBaseVars(credentials, container, QString(), QString()))
|
||||
);
|
||||
if (error != ErrorCode::NoError) {
|
||||
@@ -758,17 +758,14 @@ ErrorCode UsersController::revokeClient(const QString &serverId, const int index
|
||||
ContainerConfig containerCfg = adminConfig->containerConfig(container);
|
||||
QString containerClientId = containerCfg.protocolConfig.clientId();
|
||||
|
||||
const bool isAdminMatch = !clientId.isEmpty() && !containerClientId.isEmpty() && containerClientId.contains(clientId);
|
||||
if (isAdminMatch) {
|
||||
if (!clientId.isEmpty() && !containerClientId.isEmpty() && containerClientId.contains(clientId)) {
|
||||
emit adminConfigRevoked(serverId, container);
|
||||
}
|
||||
|
||||
emit clientRevoked(index);
|
||||
emit clientsUpdated(m_clientsTable);
|
||||
}
|
||||
|
||||
emit clientsUpdated(m_clientsTable);
|
||||
emit revokeFinished(errorCode);
|
||||
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ signals:
|
||||
void clientAdded(const QJsonObject &client);
|
||||
void clientRenamed(int row, const QString &newName);
|
||||
void clientRevoked(int row);
|
||||
void revokeFinished(ErrorCode errorCode);
|
||||
void adminConfigRevoked(const QString &serverId, DockerContainer container);
|
||||
|
||||
public slots:
|
||||
|
||||
@@ -44,7 +44,6 @@ bool ServersController::renameServer(const QString &serverId, const QString &nam
|
||||
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;
|
||||
}
|
||||
@@ -52,7 +51,6 @@ bool ServersController::renameServer(const QString &serverId, const QString &nam
|
||||
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;
|
||||
}
|
||||
@@ -60,7 +58,6 @@ bool ServersController::renameServer(const QString &serverId, const QString &nam
|
||||
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;
|
||||
}
|
||||
@@ -70,7 +67,6 @@ bool ServersController::renameServer(const QString &serverId, const QString &nam
|
||||
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;
|
||||
|
||||
@@ -217,11 +217,6 @@ 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();
|
||||
}
|
||||
|
||||
@@ -363,6 +358,6 @@ void SettingsController::disablePremV1MigrationReminder()
|
||||
|
||||
QString SettingsController::nextAvailableServerName() const
|
||||
{
|
||||
return m_serversRepository->nextAvailableServerName();
|
||||
return m_appSettingsRepository->nextAvailableServerName();
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "version.h"
|
||||
#include "core/controllers/gatewayController.h"
|
||||
#include "core/utils/constants/apiKeys.h"
|
||||
#include "core/utils/errorStrings.h"
|
||||
#include "core/utils/selfhosted/scriptsRegistry.h"
|
||||
|
||||
namespace
|
||||
@@ -20,13 +21,13 @@ namespace
|
||||
Logger logger("UpdateController");
|
||||
|
||||
#if defined(Q_OS_WINDOWS)
|
||||
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN_%1_windows_x64.exe");
|
||||
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN-%1-win64.exe");
|
||||
const QString kInstallerLocalPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN_installer.exe";
|
||||
#elif defined(Q_OS_MACOS) && !defined(MACOS_NE)
|
||||
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN_%1_macos_x64.pkg");
|
||||
#elif defined(Q_OS_MACOS)
|
||||
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN-%1-Darwin.pkg");
|
||||
const QString kInstallerLocalPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN.pkg";
|
||||
#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
|
||||
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN_%1_linux_x64.run");
|
||||
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN-%1-Linux.run");
|
||||
const QString kInstallerLocalPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN.run";
|
||||
#endif
|
||||
}
|
||||
@@ -56,6 +57,10 @@ void UpdateController::checkForUpdates()
|
||||
if (m_updateCheckRunning || !m_appSettingsRepository) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_appSettingsRepository->isDevGatewayEnv()) {
|
||||
return;
|
||||
}
|
||||
m_updateCheckRunning = true;
|
||||
|
||||
fetchGatewayUrl();
|
||||
@@ -92,11 +97,15 @@ void UpdateController::doGetAsync(const QString &endpoint, std::function<void(bo
|
||||
|
||||
void UpdateController::fetchGatewayUrl()
|
||||
{
|
||||
if (!m_appSettingsRepository || m_appSettingsRepository->isDevGatewayEnv()) {
|
||||
finishUpdateCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
auto gatewayController = QSharedPointer<GatewayController>::create(m_appSettingsRepository->getGatewayEndpoint(),
|
||||
m_appSettingsRepository->isDevGatewayEnv(),
|
||||
7000,
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled(),
|
||||
m_appSettingsRepository);
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled());
|
||||
|
||||
QJsonObject apiPayload;
|
||||
apiPayload[apiDefs::key::cliVersion] = QString(APP_VERSION);
|
||||
@@ -105,11 +114,19 @@ void UpdateController::fetchGatewayUrl()
|
||||
|
||||
// Workaround: wait before contacting gateway to avoid rate limit triggered by other requests (news etc.)
|
||||
QTimer::singleShot(1000, this, [this, gatewayController, apiPayload]() {
|
||||
gatewayController->postAsync(QStringLiteral("%1v1/updater_endpoint"), apiPayload)
|
||||
.then(this, [this, gatewayController](QPair<ErrorCode, QByteArray> result) {
|
||||
if (!m_appSettingsRepository || m_appSettingsRepository->isDevGatewayEnv()) {
|
||||
finishUpdateCheck();
|
||||
return;
|
||||
}
|
||||
gatewayController->postAsync(QStringLiteral("%1v1/updater_endpoint"), apiPayload, nullptr, gatewayController)
|
||||
.then(this, [this](QPair<ErrorCode, QByteArray> result) {
|
||||
auto [err, gatewayResponse] = result;
|
||||
if (err != ErrorCode::NoError) {
|
||||
logger.error() << "Gateway request failed, error code:" << static_cast<int>(err);
|
||||
if (err == ErrorCode::ApiNotFoundError) {
|
||||
logger.debug() << "Update check: updater_endpoint not found on gateway";
|
||||
} else {
|
||||
logger.error() << errorString(err);
|
||||
}
|
||||
finishUpdateCheck();
|
||||
return;
|
||||
}
|
||||
@@ -184,7 +201,7 @@ void UpdateController::setupNetworkErrorHandling(QNetworkReply* reply, const QSt
|
||||
logger.error() << QString("Network error occurred while fetching %1: %2 %3")
|
||||
.arg(operation, reply->errorString(), QString::number(error));
|
||||
});
|
||||
|
||||
|
||||
QObject::connect(reply, &QNetworkReply::sslErrors, [operation](const QList<QSslError> &errors) {
|
||||
QStringList errorStrings;
|
||||
for (const QSslError &err : errors) {
|
||||
@@ -196,13 +213,21 @@ void UpdateController::setupNetworkErrorHandling(QNetworkReply* reply, const QSt
|
||||
|
||||
void UpdateController::handleNetworkError(QNetworkReply* reply, const QString& operation)
|
||||
{
|
||||
logger.error() << "Network error code:" << QString::number(static_cast<int>(reply->error()));
|
||||
logger.error() << "HTTP status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError
|
||||
|| reply->error() == QNetworkReply::NetworkError::TimeoutError) {
|
||||
logger.error() << errorString(ErrorCode::ApiConfigTimeoutError);
|
||||
} else {
|
||||
QString err = reply->errorString();
|
||||
logger.error() << "Network error code:" << QString::number(static_cast<int>(reply->error()));
|
||||
logger.error() << "Error message:" << err;
|
||||
logger.error() << "HTTP status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
logger.error() << errorString(ErrorCode::ApiConfigDownloadError);
|
||||
}
|
||||
}
|
||||
|
||||
QString UpdateController::composeDownloadUrl() const
|
||||
{
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
const QString fileName = QString(kInstallerRemoteFileNamePattern).arg(m_version);
|
||||
return m_baseUrl + "/" + fileName;
|
||||
#else
|
||||
@@ -212,7 +237,7 @@ QString UpdateController::composeDownloadUrl() const
|
||||
|
||||
void UpdateController::runInstaller()
|
||||
{
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
if (m_downloadUrl.isEmpty()) {
|
||||
logger.error() << "Download URL is empty";
|
||||
return;
|
||||
@@ -244,15 +269,23 @@ void UpdateController::runInstaller()
|
||||
|
||||
#if defined(Q_OS_WINDOWS)
|
||||
runWindowsInstaller(kInstallerLocalPath);
|
||||
#elif defined(Q_OS_MACOS) && !defined(MACOS_NE)
|
||||
#elif defined(Q_OS_MACOS)
|
||||
runMacInstaller(kInstallerLocalPath);
|
||||
#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
|
||||
runLinuxInstaller(kInstallerLocalPath);
|
||||
#endif
|
||||
} else {
|
||||
logger.error() << "Installer download failed, network error:" << static_cast<int>(reply->error())
|
||||
<< reply->errorString();
|
||||
logger.error() << "HTTP status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError
|
||||
|| reply->error() == QNetworkReply::NetworkError::TimeoutError) {
|
||||
logger.error() << errorString(ErrorCode::ApiConfigTimeoutError);
|
||||
} else {
|
||||
QString err = reply->errorString();
|
||||
logger.error() << QString::fromUtf8(reply->readAll());
|
||||
logger.error() << "Network error code:" << QString::number(static_cast<int>(reply->error()));
|
||||
logger.error() << "Error message:" << err;
|
||||
logger.error() << "HTTP status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
logger.error() << errorString(ErrorCode::ApiConfigDownloadError);
|
||||
}
|
||||
}
|
||||
reply->deleteLater();
|
||||
});
|
||||
@@ -276,7 +309,7 @@ int UpdateController::runWindowsInstaller(const QString &installerPath)
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_MACOS) && !defined(MACOS_NE)
|
||||
#if defined(Q_OS_MACOS)
|
||||
int UpdateController::runMacInstaller(const QString &installerPath)
|
||||
{
|
||||
// Create temporary directory for extraction
|
||||
|
||||
@@ -71,62 +71,48 @@ ErrorCode MtProxyInstaller::queryDiagnostics(SshSession &sshSession, const Serve
|
||||
DockerContainer container, int listenPort,
|
||||
MtProxyContainerDiagnostics &out)
|
||||
{
|
||||
out = { };
|
||||
if (container == DockerContainer::MtProxy || container == DockerContainer::Telemt) {
|
||||
const QString containerName = ContainerUtils::containerToString(container);
|
||||
const bool isTelemt = container == DockerContainer::Telemt;
|
||||
|
||||
const QString sportFilter = QString::number(listenPort);
|
||||
const QString peersCmd = QStringLiteral("sudo conntrack -L -p tcp --dport ") + sportFilter
|
||||
+ QStringLiteral(" 2>/dev/null | grep ESTABLISHED | awk '{for(i=1;i<=NF;i++) if($i ~ /^src=/){print "
|
||||
"substr($i,5); break}}'");
|
||||
const QString publicFilter = QStringLiteral(" | grep -vE "
|
||||
"'^(10\\.|127\\.|169\\.254\\.|192\\.168\\.|172\\.(1[6-9]|2[0-9]|3["
|
||||
"01])\\.|::1$|fe80:|f[cd][0-9a-f][0-9a-f]:)'");
|
||||
const QString clientsCmd =
|
||||
QStringLiteral("CLIENTS=$(") + peersCmd + publicFilter + QStringLiteral(" | sort -u | grep -c .); ");
|
||||
const QString confFile =
|
||||
isTelemt ? QStringLiteral("/data/config.toml") : QStringLiteral("/data/proxy-multi.conf");
|
||||
const QString statsUrl = QString();
|
||||
|
||||
const QString script = QStringLiteral("CN=") + containerName + QStringLiteral("; ")
|
||||
+ QStringLiteral("PORT_OK=$(sudo ss -tlnp 2>/dev/null | grep -q :") + QString::number(listenPort)
|
||||
+ QStringLiteral(" && echo yes || echo no); ")
|
||||
+ QStringLiteral("TG_OK=$(curl -s --max-time 5 -o /dev/null -w '%{http_code}' "
|
||||
"https://core.telegram.org/getProxySecret 2>/dev/null | grep -q '200' && echo yes || "
|
||||
"echo no); ")
|
||||
+ clientsCmd + QStringLiteral("CONF_TIME=$(sudo docker exec \"$CN\" sh -c 'stat -c \"%y\" ") + confFile
|
||||
+ QStringLiteral(" 2>/dev/null | cut -d. -f1' 2>/dev/null || echo unknown); ")
|
||||
+ QStringLiteral("echo \"PORT_OK=${PORT_OK}\"; ") + QStringLiteral("echo \"TG_OK=${TG_OK}\"; ")
|
||||
+ QStringLiteral("echo \"CLIENTS=${CLIENTS:-0}\"; ") + QStringLiteral("echo \"CONF_TIME=${CONF_TIME}\"; ")
|
||||
+ QStringLiteral("echo \"STATS=") + statsUrl + QStringLiteral("\";");
|
||||
|
||||
QString stdOut;
|
||||
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
|
||||
stdOut += data;
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
const ErrorCode errorCode = sshSession.runScript(credentials, script, cbReadStdOut);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
for (const QString &line : stdOut.split('\n', Qt::SkipEmptyParts)) {
|
||||
if (line.startsWith(QLatin1String("PORT_OK="))) {
|
||||
out.portReachable = line.mid(8).trimmed() == QLatin1String("yes");
|
||||
} else if (line.startsWith(QLatin1String("TG_OK="))) {
|
||||
out.upstreamReachable = line.mid(6).trimmed() == QLatin1String("yes");
|
||||
} else if (line.startsWith(QLatin1String("CLIENTS="))) {
|
||||
out.clientsConnected = line.mid(8).trimmed().toInt();
|
||||
} else if (line.startsWith(QLatin1String("CONF_TIME="))) {
|
||||
out.lastConfigRefresh = line.mid(10).trimmed();
|
||||
} else if (line.startsWith(QLatin1String("STATS="))) {
|
||||
out.statsEndpoint = line.mid(6).trimmed();
|
||||
}
|
||||
}
|
||||
return ErrorCode::NoError;
|
||||
out = {};
|
||||
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
const QString containerName = ContainerUtils::containerToString(container);
|
||||
const QString script =
|
||||
QStringLiteral(
|
||||
"PORT_OK=$(sudo docker exec %1 sh -c 'ss -tlnp 2>/dev/null | grep -q :%2 && echo yes || echo no' 2>/dev/null || echo no); "
|
||||
"TG_OK=$(curl -s --max-time 5 -o /dev/null -w '%%{http_code}' https://core.telegram.org/getProxySecret 2>/dev/null | grep -q '200' && echo yes || echo no); "
|
||||
"CLIENTS=$(sudo docker exec amnezia-mtproxy sh -c 'curl -s --max-time 3 http://localhost:2398/stats 2>/dev/null | grep -o \"total_special_connections:[0-9]*\" | cut -d: -f2' 2>/dev/null); "
|
||||
"CONF_TIME=$(sudo docker exec amnezia-mtproxy sh -c 'stat -c \"%%y\" /data/proxy-multi.conf 2>/dev/null | cut -d. -f1' 2>/dev/null || echo unknown); "
|
||||
"echo \"PORT_OK=${PORT_OK}\"; "
|
||||
"echo \"TG_OK=${TG_OK}\"; "
|
||||
"echo \"CLIENTS=${CLIENTS:-0}\"; "
|
||||
"echo \"CONF_TIME=${CONF_TIME}\"; "
|
||||
"echo \"STATS=http://localhost:2398/stats\";")
|
||||
.arg(containerName)
|
||||
.arg(listenPort);
|
||||
|
||||
return ErrorCode::InternalError;
|
||||
QString stdOut;
|
||||
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
|
||||
stdOut += data;
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
const ErrorCode errorCode = sshSession.runScript(credentials, script, cbReadStdOut);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
for (const QString &line : stdOut.split('\n', Qt::SkipEmptyParts)) {
|
||||
if (line.startsWith(QLatin1String("PORT_OK="))) {
|
||||
out.portReachable = line.mid(8).trimmed() == QLatin1String("yes");
|
||||
} else if (line.startsWith(QLatin1String("TG_OK="))) {
|
||||
out.upstreamReachable = line.mid(6).trimmed() == QLatin1String("yes");
|
||||
} else if (line.startsWith(QLatin1String("CLIENTS="))) {
|
||||
out.clientsConnected = line.mid(8).trimmed().toInt();
|
||||
} else if (line.startsWith(QLatin1String("CONF_TIME="))) {
|
||||
out.lastConfigRefresh = line.mid(10).trimmed();
|
||||
} else if (line.startsWith(QLatin1String("STATS="))) {
|
||||
out.statsEndpoint = line.mid(6).trimmed();
|
||||
}
|
||||
}
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
void MtProxyInstaller::uploadClientSettingsSnapshot(SshSession &sshSession, const ServerCredentials &credentials,
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
#include "socks5Installer.h"
|
||||
|
||||
#include "core/models/protocols/socks5ProxyProtocolConfig.h"
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/protocols/protocolUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include "core/utils/selfhosted/sshSession.h"
|
||||
#include "core/utils/utilities.h"
|
||||
|
||||
#include <QRegularExpression>
|
||||
|
||||
using namespace amnezia;
|
||||
using namespace ProtocolUtils;
|
||||
|
||||
@@ -35,29 +33,10 @@ ContainerConfig Socks5Installer::generateConfig(DockerContainer container, int p
|
||||
ErrorCode Socks5Installer::extractConfigFromContainer(DockerContainer container, const ServerCredentials &credentials,
|
||||
SshSession* sshSession, ContainerConfig &config)
|
||||
{
|
||||
if (container != DockerContainer::Socks5Proxy || !sshSession) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
Socks5ProxyProtocolConfig *socks5Config = config.getSocks5ProxyProtocolConfig();
|
||||
if (!socks5Config) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode readError = ErrorCode::NoError;
|
||||
const QByteArray configRaw = sshSession->getTextFileFromContainer(
|
||||
container, credentials, QString::fromUtf8(protocols::socks5Proxy::proxyConfigPath), readError);
|
||||
if (readError != ErrorCode::NoError || configRaw.trimmed().isEmpty()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
const QString proxyConfig = QString::fromUtf8(configRaw);
|
||||
static const QRegularExpression usernameAndPasswordRegExp(QStringLiteral("users (\\w+):CL:(\\w+)"));
|
||||
const QRegularExpressionMatch usernameAndPasswordMatch = usernameAndPasswordRegExp.match(proxyConfig);
|
||||
if (usernameAndPasswordMatch.hasMatch()) {
|
||||
socks5Config->userName = usernameAndPasswordMatch.captured(1);
|
||||
socks5Config->password = usernameAndPasswordMatch.captured(2);
|
||||
}
|
||||
|
||||
Q_UNUSED(container);
|
||||
Q_UNUSED(credentials);
|
||||
Q_UNUSED(sshSession);
|
||||
Q_UNUSED(config);
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
|
||||
@@ -144,6 +144,10 @@ QJsonObject ApiConfig::toJson() const
|
||||
obj[apiDefs::key::availableCountries] = availableCountries;
|
||||
}
|
||||
|
||||
if (!supportedProtocols.isEmpty()) {
|
||||
obj[apiDefs::key::supportedProtocols] = supportedProtocols;
|
||||
}
|
||||
|
||||
QJsonObject serviceInfoObj = serviceInfo.toJson();
|
||||
if (!serviceInfoObj.isEmpty()) {
|
||||
obj[apiDefs::key::serviceInfo] = serviceInfoObj;
|
||||
@@ -194,6 +198,7 @@ ApiConfig ApiConfig::fromJson(const QJsonObject& json)
|
||||
config.issuedConfigs = json.value(apiDefs::key::issuedConfigs).toInt(0);
|
||||
|
||||
config.availableCountries = json.value(apiDefs::key::availableCountries).toArray();
|
||||
config.supportedProtocols = json.value(apiDefs::key::supportedProtocols).toArray();
|
||||
|
||||
QJsonObject serviceInfoObj = json.value(apiDefs::key::serviceInfo).toObject();
|
||||
if (!serviceInfoObj.isEmpty()) {
|
||||
|
||||
@@ -34,6 +34,7 @@ struct ApiConfig
|
||||
int maxDeviceCount;
|
||||
int issuedConfigs;
|
||||
QJsonArray availableCountries;
|
||||
QJsonArray supportedProtocols;
|
||||
|
||||
struct ServiceInfo {
|
||||
bool isAdVisible = false;
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
#include "core/utils/api/apiUtils.h"
|
||||
#include "core/models/api/apiConfig.h"
|
||||
#include "core/models/api/authData.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
|
||||
namespace amnezia
|
||||
{
|
||||
@@ -68,20 +67,6 @@ ContainerConfig ApiV2ServerConfig::containerConfig(DockerContainer container) co
|
||||
return containers.value(container);
|
||||
}
|
||||
|
||||
QPair<QString, QString> ApiV2ServerConfig::getDnsPair(const QString &primaryDns, const QString &secondaryDns) const
|
||||
{
|
||||
QString d1 = dns1;
|
||||
QString d2 = dns2;
|
||||
|
||||
if (d1.isEmpty() || !NetworkUtilities::checkIPv4Format(d1)) {
|
||||
d1 = primaryDns;
|
||||
}
|
||||
if (d2.isEmpty() || !NetworkUtilities::checkIPv4Format(d2)) {
|
||||
d2 = secondaryDns;
|
||||
}
|
||||
return { d1, d2 };
|
||||
}
|
||||
|
||||
QJsonObject ApiV2ServerConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
@@ -95,6 +80,9 @@ QJsonObject ApiV2ServerConfig::toJson() const
|
||||
if (!description.isEmpty()) {
|
||||
obj[configKey::description] = description;
|
||||
}
|
||||
if (!displayName.isEmpty()) {
|
||||
obj[configKey::displayName] = displayName;
|
||||
}
|
||||
|
||||
obj[configKey::configVersion] = configVersion;
|
||||
|
||||
@@ -121,11 +109,7 @@ QJsonObject ApiV2ServerConfig::toJson() const
|
||||
if (!dns2.isEmpty()) {
|
||||
obj[configKey::dns2] = dns2;
|
||||
}
|
||||
|
||||
if (!sendPayload.isEmpty()) {
|
||||
obj[configKey::sendPayload] = sendPayload;
|
||||
}
|
||||
|
||||
|
||||
if (crc > 0) {
|
||||
obj[configKey::crc] = crc;
|
||||
}
|
||||
@@ -150,6 +134,7 @@ ApiV2ServerConfig ApiV2ServerConfig::fromJson(const QJsonObject& json)
|
||||
config.name = json.value(configKey::name).toString();
|
||||
config.nameOverriddenByUser = json.value(configKey::nameOverriddenByUser).toBool(false);
|
||||
config.description = json.value(configKey::description).toString();
|
||||
config.displayName = json.value(configKey::displayName).toString();
|
||||
config.configVersion = json.value(configKey::configVersion).toInt(2);
|
||||
config.hostName = json.value(configKey::hostName).toString();
|
||||
|
||||
@@ -169,7 +154,6 @@ ApiV2ServerConfig ApiV2ServerConfig::fromJson(const QJsonObject& json)
|
||||
|
||||
config.dns1 = json.value(configKey::dns1).toString();
|
||||
config.dns2 = json.value(configKey::dns2).toString();
|
||||
config.sendPayload = json.value(configKey::sendPayload).toArray();
|
||||
|
||||
config.crc = json.value(configKey::crc).toInt(0);
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
#ifndef APIV2SERVERCONFIG_H
|
||||
#define APIV2SERVERCONFIG_H
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QMap>
|
||||
#include <QPair>
|
||||
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
@@ -29,7 +27,6 @@ struct ApiV2ServerConfig {
|
||||
DockerContainer defaultContainer;
|
||||
QString dns1;
|
||||
QString dns2;
|
||||
QJsonArray sendPayload;
|
||||
|
||||
QString name;
|
||||
bool nameOverriddenByUser = false;
|
||||
@@ -46,9 +43,6 @@ struct ApiV2ServerConfig {
|
||||
bool isExternalPremium() const;
|
||||
bool hasContainers() const;
|
||||
ContainerConfig containerConfig(DockerContainer container) const;
|
||||
|
||||
QPair<QString, QString> getDnsPair(const QString &primaryDns, const QString &secondaryDns) const;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static ApiV2ServerConfig fromJson(const QJsonObject& json);
|
||||
};
|
||||
|
||||
@@ -23,7 +23,9 @@ LegacyApiServerConfig LegacyApiServerConfig::fromJson(const QJsonObject &json)
|
||||
{
|
||||
LegacyApiServerConfig config;
|
||||
|
||||
config.name = json.value(configKey::name).toString();
|
||||
config.description = json.value(configKey::description).toString();
|
||||
config.displayName = json.value(configKey::displayName).toString();
|
||||
config.hostName = json.value(configKey::hostName).toString();
|
||||
|
||||
config.crc = json.value(configKey::crc).toInt(0);
|
||||
|
||||
@@ -32,7 +32,7 @@ XrayXPaddingConfig XrayXPaddingConfig::fromJson(const QJsonObject &json)
|
||||
c.bytesMin = json.value(configKey::xPaddingBytesMin).toString();
|
||||
c.bytesMax = json.value(configKey::xPaddingBytesMax).toString();
|
||||
c.obfsMode = json.value(configKey::xPaddingObfsMode).toBool(true);
|
||||
c.key = json.value(configKey::xPaddingKey).toString();
|
||||
c.key = json.value(configKey::xPaddingKey).toString(protocols::xray::defaultSite);
|
||||
c.header = json.value(configKey::xPaddingHeader).toString();
|
||||
c.placement = json.value(configKey::xPaddingPlacement).toString(protocols::xray::defaultXPaddingPlacement);
|
||||
c.method = json.value(configKey::xPaddingMethod).toString(protocols::xray::defaultXPaddingMethod);
|
||||
@@ -108,114 +108,35 @@ QJsonObject XrayXhttpConfig::toJson() const
|
||||
return obj;
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
XrayXhttpConfig clearedXhttpConfig()
|
||||
{
|
||||
XrayXhttpConfig c;
|
||||
c.mode = QString();
|
||||
c.host = QString();
|
||||
c.path = QString();
|
||||
c.headersTemplate = QString();
|
||||
c.uplinkMethod = QString();
|
||||
c.disableGrpc = false;
|
||||
c.disableSse = false;
|
||||
c.sessionPlacement = QString();
|
||||
c.sessionKey = QString();
|
||||
c.seqPlacement = QString();
|
||||
c.seqKey = QString();
|
||||
c.uplinkDataPlacement = QString();
|
||||
c.uplinkDataKey = QString();
|
||||
c.uplinkChunkSize = QString();
|
||||
c.scMaxBufferedPosts = QString();
|
||||
c.scMaxEachPostBytesMin = QString();
|
||||
c.scMaxEachPostBytesMax = QString();
|
||||
c.scMinPostsIntervalMsMin = QString();
|
||||
c.scMinPostsIntervalMsMax = QString();
|
||||
c.scStreamUpServerSecsMin = QString();
|
||||
c.scStreamUpServerSecsMax = QString();
|
||||
return c;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
XrayXhttpConfig XrayXhttpConfig::fromJson(const QJsonObject &json)
|
||||
{
|
||||
if (json.isEmpty()) {
|
||||
return clearedXhttpConfig();
|
||||
}
|
||||
XrayXhttpConfig c;
|
||||
c.mode = json.value(configKey::xhttpMode).toString(protocols::xray::defaultXhttpMode);
|
||||
c.host = json.value(configKey::xhttpHost).toString(protocols::xray::defaultSite);
|
||||
c.path = json.value(configKey::xhttpPath).toString();
|
||||
c.headersTemplate = json.value(configKey::xhttpHeadersTemplate).toString(protocols::xray::defaultXhttpHeadersTemplate);
|
||||
c.uplinkMethod = json.value(configKey::xhttpUplinkMethod).toString(protocols::xray::defaultXhttpUplinkMethod);
|
||||
c.disableGrpc = json.value(configKey::xhttpDisableGrpc).toBool(true);
|
||||
c.disableSse = json.value(configKey::xhttpDisableSse).toBool(true);
|
||||
|
||||
XrayXhttpConfig c = clearedXhttpConfig();
|
||||
c.sessionPlacement = json.value(configKey::xhttpSessionPlacement).toString(protocols::xray::defaultXhttpSessionPlacement);
|
||||
c.sessionKey = json.value(configKey::xhttpSessionKey).toString();
|
||||
c.seqPlacement = json.value(configKey::xhttpSeqPlacement).toString(protocols::xray::defaultXhttpSessionPlacement);
|
||||
c.seqKey = json.value(configKey::xhttpSeqKey).toString();
|
||||
c.uplinkDataPlacement = json.value(configKey::xhttpUplinkDataPlacement).toString(protocols::xray::defaultXhttpUplinkDataPlacement);
|
||||
c.uplinkDataKey = json.value(configKey::xhttpUplinkDataKey).toString();
|
||||
|
||||
if (json.contains(configKey::xhttpMode)) {
|
||||
c.mode = json.value(configKey::xhttpMode).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpHost)) {
|
||||
c.host = json.value(configKey::xhttpHost).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpPath)) {
|
||||
c.path = json.value(configKey::xhttpPath).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpHeadersTemplate)) {
|
||||
c.headersTemplate = json.value(configKey::xhttpHeadersTemplate).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpUplinkMethod)) {
|
||||
c.uplinkMethod = json.value(configKey::xhttpUplinkMethod).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpDisableGrpc)) {
|
||||
c.disableGrpc = json.value(configKey::xhttpDisableGrpc).toBool();
|
||||
}
|
||||
if (json.contains(configKey::xhttpDisableSse)) {
|
||||
c.disableSse = json.value(configKey::xhttpDisableSse).toBool();
|
||||
}
|
||||
if (json.contains(configKey::xhttpSessionPlacement)) {
|
||||
c.sessionPlacement = json.value(configKey::xhttpSessionPlacement).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpSessionKey)) {
|
||||
c.sessionKey = json.value(configKey::xhttpSessionKey).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpSeqPlacement)) {
|
||||
c.seqPlacement = json.value(configKey::xhttpSeqPlacement).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpSeqKey)) {
|
||||
c.seqKey = json.value(configKey::xhttpSeqKey).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpUplinkDataPlacement)) {
|
||||
c.uplinkDataPlacement = json.value(configKey::xhttpUplinkDataPlacement).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpUplinkDataKey)) {
|
||||
c.uplinkDataKey = json.value(configKey::xhttpUplinkDataKey).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpUplinkChunkSize)) {
|
||||
c.uplinkChunkSize = json.value(configKey::xhttpUplinkChunkSize).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpScMaxBufferedPosts)) {
|
||||
c.scMaxBufferedPosts = json.value(configKey::xhttpScMaxBufferedPosts).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpScMaxEachPostBytesMin)) {
|
||||
c.scMaxEachPostBytesMin = json.value(configKey::xhttpScMaxEachPostBytesMin).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpScMaxEachPostBytesMax)) {
|
||||
c.scMaxEachPostBytesMax = json.value(configKey::xhttpScMaxEachPostBytesMax).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpScMinPostsIntervalMsMin)) {
|
||||
c.scMinPostsIntervalMsMin = json.value(configKey::xhttpScMinPostsIntervalMsMin).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpScMinPostsIntervalMsMax)) {
|
||||
c.scMinPostsIntervalMsMax = json.value(configKey::xhttpScMinPostsIntervalMsMax).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpScStreamUpServerSecsMin)) {
|
||||
c.scStreamUpServerSecsMin = json.value(configKey::xhttpScStreamUpServerSecsMin).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpScStreamUpServerSecsMax)) {
|
||||
c.scStreamUpServerSecsMax = json.value(configKey::xhttpScStreamUpServerSecsMax).toString();
|
||||
}
|
||||
c.uplinkChunkSize = json.value(configKey::xhttpUplinkChunkSize).toString("0");
|
||||
c.scMaxBufferedPosts = json.value(configKey::xhttpScMaxBufferedPosts).toString();
|
||||
c.scMaxEachPostBytesMin = json.value(configKey::xhttpScMaxEachPostBytesMin).toString("1");
|
||||
c.scMaxEachPostBytesMax = json.value(configKey::xhttpScMaxEachPostBytesMax).toString("100");
|
||||
c.scMinPostsIntervalMsMin = json.value(configKey::xhttpScMinPostsIntervalMsMin).toString("100");
|
||||
c.scMinPostsIntervalMsMax = json.value(configKey::xhttpScMinPostsIntervalMsMax).toString("800");
|
||||
c.scStreamUpServerSecsMin = json.value(configKey::xhttpScStreamUpServerSecsMin).toString("1");
|
||||
c.scStreamUpServerSecsMax = json.value(configKey::xhttpScStreamUpServerSecsMax).toString("100");
|
||||
|
||||
if (json.contains(QLatin1String("xPadding"))) {
|
||||
c.xPadding = XrayXPaddingConfig::fromJson(json.value(QLatin1String("xPadding")).toObject());
|
||||
}
|
||||
if (json.contains(QLatin1String("xmux"))) {
|
||||
c.xmux = XrayXmuxConfig::fromJson(json.value(QLatin1String("xmux")).toObject());
|
||||
}
|
||||
c.xPadding = XrayXPaddingConfig::fromJson(json.value("xPadding").toObject());
|
||||
c.xmux = XrayXmuxConfig::fromJson(json.value("xmux").toObject());
|
||||
|
||||
return c;
|
||||
}
|
||||
@@ -235,27 +156,12 @@ QJsonObject XrayMkcpConfig::toJson() const
|
||||
XrayMkcpConfig XrayMkcpConfig::fromJson(const QJsonObject &json)
|
||||
{
|
||||
XrayMkcpConfig c;
|
||||
if (json.isEmpty()) {
|
||||
return c;
|
||||
}
|
||||
if (json.contains(configKey::mkcpTti)) {
|
||||
c.tti = json.value(configKey::mkcpTti).toString();
|
||||
}
|
||||
if (json.contains(configKey::mkcpUplinkCapacity)) {
|
||||
c.uplinkCapacity = json.value(configKey::mkcpUplinkCapacity).toString();
|
||||
}
|
||||
if (json.contains(configKey::mkcpDownlinkCapacity)) {
|
||||
c.downlinkCapacity = json.value(configKey::mkcpDownlinkCapacity).toString();
|
||||
}
|
||||
if (json.contains(configKey::mkcpReadBufferSize)) {
|
||||
c.readBufferSize = json.value(configKey::mkcpReadBufferSize).toString();
|
||||
}
|
||||
if (json.contains(configKey::mkcpWriteBufferSize)) {
|
||||
c.writeBufferSize = json.value(configKey::mkcpWriteBufferSize).toString();
|
||||
}
|
||||
if (json.contains(configKey::mkcpCongestion)) {
|
||||
c.congestion = json.value(configKey::mkcpCongestion).toBool();
|
||||
}
|
||||
c.tti = json.value(configKey::mkcpTti).toString();
|
||||
c.uplinkCapacity = json.value(configKey::mkcpUplinkCapacity).toString();
|
||||
c.downlinkCapacity = json.value(configKey::mkcpDownlinkCapacity).toString();
|
||||
c.readBufferSize = json.value(configKey::mkcpReadBufferSize).toString();
|
||||
c.writeBufferSize = json.value(configKey::mkcpWriteBufferSize).toString();
|
||||
c.congestion = json.value(configKey::mkcpCongestion).toBool(true);
|
||||
return c;
|
||||
}
|
||||
|
||||
@@ -302,14 +208,8 @@ QJsonObject XrayServerConfig::toJson() const
|
||||
if (!transport.isEmpty()) {
|
||||
obj[configKey::xrayTransport] = transport;
|
||||
}
|
||||
const QJsonObject xhttpObj = xhttp.toJson();
|
||||
if (!xhttpObj.isEmpty()) {
|
||||
obj[QStringLiteral("xhttp")] = xhttpObj;
|
||||
}
|
||||
const QJsonObject mkcpObj = mkcp.toJson();
|
||||
if (!mkcpObj.isEmpty()) {
|
||||
obj[QStringLiteral("mkcp")] = mkcpObj;
|
||||
}
|
||||
obj["xhttp"] = xhttp.toJson();
|
||||
obj["mkcp"] = mkcp.toJson();
|
||||
|
||||
return obj;
|
||||
}
|
||||
@@ -325,39 +225,20 @@ XrayServerConfig XrayServerConfig::fromJson(const QJsonObject &json)
|
||||
c.site = json.value(configKey::site).toString();
|
||||
c.isThirdPartyConfig = json.value(configKey::isThirdPartyConfig).toBool(false);
|
||||
|
||||
if (json.contains(configKey::xraySecurity)) {
|
||||
c.security = json.value(configKey::xraySecurity).toString();
|
||||
}
|
||||
if (json.contains(configKey::xrayFlow)) {
|
||||
c.flow = json.value(configKey::xrayFlow).toString();
|
||||
}
|
||||
if (json.contains(configKey::xrayFingerprint)) {
|
||||
c.fingerprint = json.value(configKey::xrayFingerprint).toString();
|
||||
if (c.fingerprint.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) {
|
||||
c.fingerprint = QString::fromLatin1(protocols::xray::defaultFingerprint);
|
||||
}
|
||||
}
|
||||
if (json.contains(configKey::xraySni)) {
|
||||
c.sni = json.value(configKey::xraySni).toString();
|
||||
}
|
||||
if (json.contains(configKey::xrayAlpn)) {
|
||||
c.alpn = json.value(configKey::xrayAlpn).toString();
|
||||
}
|
||||
if (json.contains(configKey::xrayTransport)) {
|
||||
c.transport = json.value(configKey::xrayTransport).toString();
|
||||
}
|
||||
if (json.contains(QLatin1String("xhttp"))) {
|
||||
const QJsonObject xhttpJson = json.value(QLatin1String("xhttp")).toObject();
|
||||
if (!xhttpJson.isEmpty()) {
|
||||
c.xhttp = XrayXhttpConfig::fromJson(xhttpJson);
|
||||
}
|
||||
}
|
||||
if (json.contains(QLatin1String("mkcp"))) {
|
||||
const QJsonObject mkcpJson = json.value(QLatin1String("mkcp")).toObject();
|
||||
if (!mkcpJson.isEmpty()) {
|
||||
c.mkcp = XrayMkcpConfig::fromJson(mkcpJson);
|
||||
}
|
||||
// New: Security
|
||||
c.security = json.value(configKey::xraySecurity).toString(protocols::xray::defaultSecurity);
|
||||
c.flow = json.value(configKey::xrayFlow).toString(protocols::xray::defaultFlow);
|
||||
c.fingerprint = json.value(configKey::xrayFingerprint).toString(protocols::xray::defaultFingerprint);
|
||||
if (c.fingerprint.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) {
|
||||
c.fingerprint = QString::fromLatin1(protocols::xray::defaultFingerprint);
|
||||
}
|
||||
c.sni = json.value(configKey::xraySni).toString(protocols::xray::defaultSni);
|
||||
c.alpn = json.value(configKey::xrayAlpn).toString(protocols::xray::defaultAlpn);
|
||||
|
||||
// New: Transport
|
||||
c.transport = json.value(configKey::xrayTransport).toString(protocols::xray::defaultTransport);
|
||||
c.xhttp = XrayXhttpConfig::fromJson(json.value("xhttp").toObject());
|
||||
c.mkcp = XrayMkcpConfig::fromJson(json.value("mkcp").toObject());
|
||||
|
||||
return c;
|
||||
}
|
||||
@@ -365,17 +246,12 @@ XrayServerConfig XrayServerConfig::fromJson(const QJsonObject &json)
|
||||
bool XrayServerConfig::hasEqualServerSettings(const XrayServerConfig &other) const
|
||||
{
|
||||
return port == other.port
|
||||
&& transportProto == other.transportProto
|
||||
&& subnetAddress == other.subnetAddress
|
||||
&& site == other.site
|
||||
&& security == other.security
|
||||
&& flow == other.flow
|
||||
&& transport == other.transport
|
||||
&& fingerprint == other.fingerprint
|
||||
&& sni == other.sni
|
||||
&& alpn == other.alpn
|
||||
&& xhttp.toJson() == other.xhttp.toJson()
|
||||
&& mkcp.toJson() == other.mkcp.toJson();
|
||||
&& sni == other.sni;
|
||||
}
|
||||
|
||||
QJsonObject XrayClientConfig::toJson() const
|
||||
@@ -468,17 +344,6 @@ XrayProtocolConfig XrayProtocolConfig::fromJson(const QJsonObject &json)
|
||||
}
|
||||
}
|
||||
}
|
||||
const QJsonArray outbounds = parsed.value(protocols::xray::outbounds).toArray();
|
||||
if (!outbounds.isEmpty()) {
|
||||
const QJsonObject settings = outbounds[0].toObject().value(protocols::xray::settings).toObject();
|
||||
const QJsonArray vnext = settings.value(protocols::xray::vnext).toArray();
|
||||
if (!vnext.isEmpty()) {
|
||||
const QJsonArray users = vnext[0].toObject().value(protocols::xray::users).toArray();
|
||||
if (!users.isEmpty()) {
|
||||
clientCfg.id = users[0].toObject().value(protocols::xray::id).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
c.clientConfig = clientCfg;
|
||||
} else {
|
||||
c.clientConfig = XrayClientConfig::fromJson(parsed);
|
||||
@@ -486,154 +351,9 @@ XrayProtocolConfig XrayProtocolConfig::fromJson(const QJsonObject &json)
|
||||
}
|
||||
}
|
||||
|
||||
c.needsClientHydration =
|
||||
c.hasClientConfig()
|
||||
&& (!json.contains(configKey::xrayTransport) || c.serverConfig.isThirdPartyConfig);
|
||||
if (c.needsClientHydration) {
|
||||
c.hydrateServerConfigFromClientNative();
|
||||
}
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
bool XrayProtocolConfig::hydrateServerConfigFromClientNative()
|
||||
{
|
||||
if (!clientConfig.has_value() || clientConfig->nativeConfig.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(clientConfig->nativeConfig.toUtf8());
|
||||
if (doc.isNull() || !doc.isObject()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QJsonObject root = doc.object();
|
||||
const QJsonArray outbounds = root.value(protocols::xray::outbounds).toArray();
|
||||
if (outbounds.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QJsonObject outbound = outbounds[0].toObject();
|
||||
const QJsonObject streamSettings = outbound.value(protocols::xray::streamSettings).toObject();
|
||||
if (streamSettings.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
XrayServerConfig &srv = serverConfig;
|
||||
|
||||
const QJsonObject settings = outbound.value(protocols::xray::settings).toObject();
|
||||
const QJsonArray vnext = settings.value(protocols::xray::vnext).toArray();
|
||||
if (!vnext.isEmpty()) {
|
||||
const QJsonObject vnextEntry = vnext[0].toObject();
|
||||
if (vnextEntry.contains(protocols::xray::port)) {
|
||||
srv.port = QString::number(vnextEntry.value(protocols::xray::port).toInt());
|
||||
}
|
||||
const QJsonArray users = vnextEntry.value(protocols::xray::users).toArray();
|
||||
if (!users.isEmpty()) {
|
||||
srv.flow = users[0].toObject().value(protocols::xray::flow).toString();
|
||||
}
|
||||
}
|
||||
|
||||
const QString networkVal = streamSettings.value(protocols::xray::network).toString(QStringLiteral("tcp"));
|
||||
if (networkVal == QLatin1String("xhttp")) {
|
||||
srv.transport = QStringLiteral("xhttp");
|
||||
} else if (networkVal == QLatin1String("kcp")) {
|
||||
srv.transport = QStringLiteral("mkcp");
|
||||
} else {
|
||||
srv.transport = QStringLiteral("raw");
|
||||
}
|
||||
|
||||
if (streamSettings.contains(protocols::xray::security)) {
|
||||
srv.security = streamSettings.value(protocols::xray::security).toString();
|
||||
}
|
||||
|
||||
if (srv.security == QLatin1String("reality")) {
|
||||
const QJsonObject rs = streamSettings.value(protocols::xray::realitySettings).toObject();
|
||||
srv.sni = rs.value(protocols::xray::serverName).toString();
|
||||
srv.site = srv.sni.isEmpty() ? srv.site : srv.sni;
|
||||
const QString fp = rs.value(protocols::xray::fingerprint).toString();
|
||||
if (!fp.isEmpty()) {
|
||||
srv.fingerprint = fp.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)
|
||||
? QString::fromLatin1(protocols::xray::defaultFingerprint)
|
||||
: fp;
|
||||
}
|
||||
}
|
||||
|
||||
if (srv.security == QLatin1String("tls")) {
|
||||
const QJsonObject tls = streamSettings.value(QStringLiteral("tlsSettings")).toObject();
|
||||
srv.sni = tls.value(protocols::xray::serverName).toString();
|
||||
const QString fp = tls.value(protocols::xray::fingerprint).toString();
|
||||
if (!fp.isEmpty()) {
|
||||
srv.fingerprint = fp;
|
||||
}
|
||||
QStringList alpnList;
|
||||
for (const QJsonValue &v : tls.value(QStringLiteral("alpn")).toArray()) {
|
||||
alpnList << v.toString();
|
||||
}
|
||||
if (!alpnList.isEmpty()) {
|
||||
srv.alpn = alpnList.join(QLatin1Char(','));
|
||||
}
|
||||
}
|
||||
|
||||
if (srv.transport == QLatin1String("xhttp")) {
|
||||
const QJsonObject xhttpObj = streamSettings.value(QStringLiteral("xhttpSettings")).toObject();
|
||||
QJsonObject xhttpJson;
|
||||
const QString mode = xhttpObj.value(QStringLiteral("mode")).toString();
|
||||
if (!mode.isEmpty()) {
|
||||
if (mode == QLatin1String("auto")) {
|
||||
xhttpJson[configKey::xhttpMode] = QStringLiteral("Auto");
|
||||
} else if (mode == QLatin1String("packet-up")) {
|
||||
xhttpJson[configKey::xhttpMode] = QStringLiteral("Packet-up");
|
||||
} else if (mode == QLatin1String("stream-up")) {
|
||||
xhttpJson[configKey::xhttpMode] = QStringLiteral("Stream-up");
|
||||
} else if (mode == QLatin1String("stream-one")) {
|
||||
xhttpJson[configKey::xhttpMode] = QStringLiteral("Stream-one");
|
||||
} else {
|
||||
xhttpJson[configKey::xhttpMode] = mode;
|
||||
}
|
||||
}
|
||||
if (xhttpObj.contains(QStringLiteral("host"))) {
|
||||
xhttpJson[configKey::xhttpHost] = xhttpObj.value(QStringLiteral("host")).toString();
|
||||
}
|
||||
if (xhttpObj.contains(QStringLiteral("path"))) {
|
||||
xhttpJson[configKey::xhttpPath] = xhttpObj.value(QStringLiteral("path")).toString();
|
||||
}
|
||||
if (xhttpObj.contains(QStringLiteral("uplinkHTTPMethod"))) {
|
||||
xhttpJson[configKey::xhttpUplinkMethod] = xhttpObj.value(QStringLiteral("uplinkHTTPMethod")).toString();
|
||||
}
|
||||
xhttpJson[configKey::xhttpDisableGrpc] = xhttpObj.value(QStringLiteral("noGRPCHeader")).toBool(true);
|
||||
xhttpJson[configKey::xhttpDisableSse] = xhttpObj.value(QStringLiteral("noSSEHeader")).toBool(true);
|
||||
srv.xhttp = XrayXhttpConfig::fromJson(xhttpJson);
|
||||
}
|
||||
|
||||
if (srv.transport == QLatin1String("mkcp")) {
|
||||
const QJsonObject kcpObj = streamSettings.value(QStringLiteral("kcpSettings")).toObject();
|
||||
XrayMkcpConfig mk;
|
||||
if (kcpObj.contains(QStringLiteral("tti"))) {
|
||||
mk.tti = QString::number(kcpObj.value(QStringLiteral("tti")).toInt());
|
||||
}
|
||||
if (kcpObj.contains(QStringLiteral("uplinkCapacity"))) {
|
||||
mk.uplinkCapacity = QString::number(kcpObj.value(QStringLiteral("uplinkCapacity")).toInt());
|
||||
}
|
||||
if (kcpObj.contains(QStringLiteral("downlinkCapacity"))) {
|
||||
mk.downlinkCapacity = QString::number(kcpObj.value(QStringLiteral("downlinkCapacity")).toInt());
|
||||
}
|
||||
if (kcpObj.contains(QStringLiteral("readBufferSize"))) {
|
||||
mk.readBufferSize = QString::number(kcpObj.value(QStringLiteral("readBufferSize")).toInt());
|
||||
}
|
||||
if (kcpObj.contains(QStringLiteral("writeBufferSize"))) {
|
||||
mk.writeBufferSize = QString::number(kcpObj.value(QStringLiteral("writeBufferSize")).toInt());
|
||||
}
|
||||
if (kcpObj.contains(QStringLiteral("congestion"))) {
|
||||
mk.congestion = kcpObj.value(QStringLiteral("congestion")).toBool(true);
|
||||
}
|
||||
srv.mkcp = mk;
|
||||
}
|
||||
|
||||
needsClientHydration = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool XrayProtocolConfig::hasClientConfig() const
|
||||
{
|
||||
return clientConfig.has_value();
|
||||
|
||||
@@ -75,7 +75,6 @@ struct XrayXhttpConfig {
|
||||
XrayXmuxConfig xmux;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
/// Reads only keys present in JSON (no Amnezia UI defaults). Use XrayConfigModel::applyDefaultsToServerConfig for UI.
|
||||
static XrayXhttpConfig fromJson(const QJsonObject &json);
|
||||
};
|
||||
|
||||
@@ -100,13 +99,15 @@ struct XrayServerConfig {
|
||||
QString site;
|
||||
bool isThirdPartyConfig = false;
|
||||
|
||||
QString security;
|
||||
QString flow;
|
||||
QString fingerprint;
|
||||
QString sni;
|
||||
QString alpn;
|
||||
// New: Security
|
||||
QString security = protocols::xray::defaultSecurity;
|
||||
QString flow = protocols::xray::defaultFlow;
|
||||
QString fingerprint = protocols::xray::defaultFingerprint;
|
||||
QString sni = protocols::xray::defaultSni;
|
||||
QString alpn = protocols::xray::defaultAlpn;
|
||||
|
||||
QString transport;
|
||||
// New: Transport
|
||||
QString transport = protocols::xray::defaultTransport;
|
||||
XrayXhttpConfig xhttp;
|
||||
XrayMkcpConfig mkcp;
|
||||
|
||||
@@ -138,10 +139,6 @@ struct XrayProtocolConfig {
|
||||
bool hasClientConfig() const;
|
||||
void setClientConfig(const XrayClientConfig &config);
|
||||
void clearClientConfig();
|
||||
|
||||
bool needsClientHydration = false;
|
||||
|
||||
bool hydrateServerConfigFromClientNative();
|
||||
};
|
||||
|
||||
} // namespace amnezia
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#include "core/protocols/protocolUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
|
||||
namespace amnezia
|
||||
{
|
||||
@@ -29,25 +28,6 @@ ContainerConfig NativeServerConfig::containerConfig(DockerContainer container) c
|
||||
return containers.value(container);
|
||||
}
|
||||
|
||||
void NativeServerConfig::updateContainerConfig(DockerContainer container, const ContainerConfig &config)
|
||||
{
|
||||
containers[container] = config;
|
||||
}
|
||||
|
||||
QPair<QString, QString> NativeServerConfig::getDnsPair(const QString &primaryDns, const QString &secondaryDns) const
|
||||
{
|
||||
QString d1 = dns1;
|
||||
QString d2 = dns2;
|
||||
|
||||
if (d1.isEmpty() || !NetworkUtilities::checkIPv4Format(d1)) {
|
||||
d1 = primaryDns;
|
||||
}
|
||||
if (d2.isEmpty() || !NetworkUtilities::checkIPv4Format(d2)) {
|
||||
d2 = secondaryDns;
|
||||
}
|
||||
return { d1, d2 };
|
||||
}
|
||||
|
||||
QJsonObject NativeServerConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
@@ -55,6 +35,9 @@ QJsonObject NativeServerConfig::toJson() const
|
||||
if (!description.isEmpty()) {
|
||||
obj[configKey::description] = this->description;
|
||||
}
|
||||
if (!displayName.isEmpty()) {
|
||||
obj[configKey::displayName] = displayName;
|
||||
}
|
||||
if (!hostName.isEmpty()) {
|
||||
obj[configKey::hostName] = hostName;
|
||||
}
|
||||
@@ -87,6 +70,7 @@ NativeServerConfig NativeServerConfig::fromJson(const QJsonObject& json)
|
||||
NativeServerConfig config;
|
||||
|
||||
config.description = json.value(configKey::description).toString();
|
||||
config.displayName = json.value(configKey::displayName).toString();
|
||||
config.hostName = json.value(configKey::hostName).toString();
|
||||
|
||||
QJsonArray containersArray = json.value(configKey::containers).toArray();
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QMap>
|
||||
#include <QPair>
|
||||
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
@@ -26,11 +25,6 @@ struct NativeServerConfig {
|
||||
|
||||
bool hasContainers() const;
|
||||
ContainerConfig containerConfig(DockerContainer container) const;
|
||||
|
||||
void updateContainerConfig(DockerContainer container, const ContainerConfig &config);
|
||||
|
||||
QPair<QString, QString> getDnsPair(const QString &primaryDns, const QString &secondaryDns) const;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static NativeServerConfig fromJson(const QJsonObject& json);
|
||||
};
|
||||
|
||||
@@ -87,6 +87,9 @@ QJsonObject SelfHostedAdminServerConfig::toJson() const
|
||||
if (!description.isEmpty()) {
|
||||
obj[configKey::description] = this->description;
|
||||
}
|
||||
if (!displayName.isEmpty()) {
|
||||
obj[configKey::displayName] = displayName;
|
||||
}
|
||||
if (!hostName.isEmpty()) {
|
||||
obj[configKey::hostName] = hostName;
|
||||
}
|
||||
@@ -129,6 +132,7 @@ SelfHostedAdminServerConfig SelfHostedAdminServerConfig::fromJson(const QJsonObj
|
||||
SelfHostedAdminServerConfig config;
|
||||
|
||||
config.description = json.value(configKey::description).toString();
|
||||
config.displayName = json.value(configKey::displayName).toString();
|
||||
config.hostName = json.value(configKey::hostName).toString();
|
||||
|
||||
QJsonArray containersArray = json.value(configKey::containers).toArray();
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
|
||||
namespace amnezia
|
||||
{
|
||||
@@ -43,26 +42,6 @@ ContainerConfig SelfHostedUserServerConfig::containerConfig(DockerContainer cont
|
||||
return containers.value(container);
|
||||
}
|
||||
|
||||
void SelfHostedUserServerConfig::updateContainerConfig(DockerContainer container, const ContainerConfig &config)
|
||||
{
|
||||
containers[container] = config;
|
||||
}
|
||||
|
||||
QPair<QString, QString> SelfHostedUserServerConfig::getDnsPair(const QString &primaryDns,
|
||||
const QString &secondaryDns) const
|
||||
{
|
||||
QString d1 = dns1;
|
||||
QString d2 = dns2;
|
||||
|
||||
if (d1.isEmpty() || !NetworkUtilities::checkIPv4Format(d1)) {
|
||||
d1 = primaryDns;
|
||||
}
|
||||
if (d2.isEmpty() || !NetworkUtilities::checkIPv4Format(d2)) {
|
||||
d2 = secondaryDns;
|
||||
}
|
||||
return { d1, d2 };
|
||||
}
|
||||
|
||||
QJsonObject SelfHostedUserServerConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
@@ -70,6 +49,9 @@ QJsonObject SelfHostedUserServerConfig::toJson() const
|
||||
if (!description.isEmpty()) {
|
||||
obj[configKey::description] = this->description;
|
||||
}
|
||||
if (!displayName.isEmpty()) {
|
||||
obj[configKey::displayName] = displayName;
|
||||
}
|
||||
if (!hostName.isEmpty()) {
|
||||
obj[configKey::hostName] = hostName;
|
||||
}
|
||||
@@ -102,6 +84,7 @@ SelfHostedUserServerConfig SelfHostedUserServerConfig::fromJson(const QJsonObjec
|
||||
SelfHostedUserServerConfig config;
|
||||
|
||||
config.description = json.value(configKey::description).toString();
|
||||
config.displayName = json.value(configKey::displayName).toString();
|
||||
config.hostName = json.value(configKey::hostName).toString();
|
||||
|
||||
QJsonArray containersArray = json.value(configKey::containers).toArray();
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QMap>
|
||||
#include <QPair>
|
||||
#include <optional>
|
||||
|
||||
#include "core/utils/containerEnum.h"
|
||||
@@ -31,11 +30,6 @@ struct SelfHostedUserServerConfig {
|
||||
std::optional<ServerCredentials> credentials() const;
|
||||
bool hasContainers() const;
|
||||
ContainerConfig containerConfig(DockerContainer container) const;
|
||||
|
||||
void updateContainerConfig(DockerContainer container, const ContainerConfig &config);
|
||||
|
||||
QPair<QString, QString> getDnsPair(const QString &primaryDns, const QString &secondaryDns) const;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static SelfHostedUserServerConfig fromJson(const QJsonObject &json);
|
||||
};
|
||||
|
||||
@@ -39,44 +39,33 @@ QString OpenVpnProtocol::defaultConfigPath()
|
||||
return p;
|
||||
}
|
||||
|
||||
void OpenVpnProtocol::cleanupResources()
|
||||
void OpenVpnProtocol::stop()
|
||||
{
|
||||
if (m_openVpnProcess || openVpnProcessIsRunning()) {
|
||||
qDebug() << "OpenVpnProtocol::stop()";
|
||||
setConnectionState(Vpn::ConnectionState::Disconnecting);
|
||||
|
||||
// TODO: need refactoring
|
||||
// sendTermSignal() will even return true while server connected ???
|
||||
if ((m_connectionState == Vpn::ConnectionState::Preparing) || (m_connectionState == Vpn::ConnectionState::Connecting)
|
||||
|| (m_connectionState == Vpn::ConnectionState::Connected)
|
||||
|| (m_connectionState == Vpn::ConnectionState::Reconnecting)) {
|
||||
if (!sendTermSignal()) {
|
||||
killOpenVpnProcess();
|
||||
}
|
||||
QThread::msleep(10);
|
||||
m_managementServer.stop();
|
||||
}
|
||||
m_managementServer.stop();
|
||||
|
||||
#if defined(Q_OS_WIN) || defined(Q_OS_LINUX) || defined(Q_OS_MACOS)
|
||||
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
QRemoteObjectPendingReply<bool> reply = iface->disableKillSwitch();
|
||||
if (!reply.waitForFinished(1000) && !reply.returnValue()) {
|
||||
qWarning() << "OpenVpnProtocol::cleanupResources(): Failed to disable killswitch";
|
||||
qWarning() << "OpenVpnProtocol::stop(): Failed to disable killswitch";
|
||||
}
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
void OpenVpnProtocol::stop()
|
||||
{
|
||||
qDebug() << "OpenVpnProtocol::stop()";
|
||||
|
||||
const bool wasActive = m_connectionState == Vpn::ConnectionState::Preparing
|
||||
|| m_connectionState == Vpn::ConnectionState::Connecting
|
||||
|| m_connectionState == Vpn::ConnectionState::Connected
|
||||
|| m_connectionState == Vpn::ConnectionState::Reconnecting;
|
||||
|
||||
if (wasActive) {
|
||||
setConnectionState(Vpn::ConnectionState::Disconnecting);
|
||||
}
|
||||
|
||||
cleanupResources();
|
||||
|
||||
if (wasActive || m_connectionState == Vpn::ConnectionState::Disconnecting) {
|
||||
setConnectionState(Vpn::ConnectionState::Disconnected);
|
||||
}
|
||||
setConnectionState(Vpn::ConnectionState::Disconnected);
|
||||
}
|
||||
|
||||
ErrorCode OpenVpnProtocol::prepare()
|
||||
@@ -179,7 +168,7 @@ void OpenVpnProtocol::updateRouteGateway(QString line)
|
||||
|
||||
ErrorCode OpenVpnProtocol::start()
|
||||
{
|
||||
cleanupResources();
|
||||
OpenVpnProtocol::stop();
|
||||
|
||||
if (!QFileInfo::exists(configPath())) {
|
||||
setLastError(ErrorCode::OpenVpnConfigMissing);
|
||||
|
||||
@@ -29,7 +29,6 @@ protected slots:
|
||||
void onReadyReadDataFromManagementServer();
|
||||
|
||||
private:
|
||||
void cleanupResources();
|
||||
QString configPath() const;
|
||||
bool openVpnProcessIsRunning() const;
|
||||
bool sendTermSignal();
|
||||
|
||||
@@ -280,24 +280,6 @@ void SecureAppSettingsRepository::toggleDevGatewayEnv(bool enabled)
|
||||
setValue("Conf/devGatewayEnv", enabled);
|
||||
}
|
||||
|
||||
QByteArray SecureAppSettingsRepository::readGatewayProxyUrls(const QString &cacheKey) const
|
||||
{
|
||||
if (cacheKey.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return value(QStringLiteral("Conf/proxyUrls/") + cacheKey).toByteArray();
|
||||
}
|
||||
|
||||
void SecureAppSettingsRepository::writeGatewayProxyUrls(const QString &cacheKey, const QByteArray &proxyUrlsEncrypted)
|
||||
{
|
||||
if (cacheKey.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setValue(QStringLiteral("Conf/proxyUrls/") + cacheKey, proxyUrlsEncrypted);
|
||||
}
|
||||
|
||||
bool SecureAppSettingsRepository::isKillSwitchEnabled() const
|
||||
{
|
||||
return value("Conf/killSwitchEnabled", true).toBool();
|
||||
@@ -444,6 +426,26 @@ void SecureAppSettingsRepository::clearSettings()
|
||||
emit settingsCleared();
|
||||
}
|
||||
|
||||
QString SecureAppSettingsRepository::nextAvailableServerName() const
|
||||
{
|
||||
int i = 0;
|
||||
bool nameExist = false;
|
||||
|
||||
do {
|
||||
i++;
|
||||
nameExist = false;
|
||||
QJsonArray servers = QJsonDocument::fromJson(value("Servers/serversList").toByteArray()).array();
|
||||
for (const QJsonValue &server : servers) {
|
||||
if (server.toObject().value(configKey::description).toString() == QString("Server") + " " + QString::number(i)) {
|
||||
nameExist = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} while (nameExist);
|
||||
|
||||
return QString("Server") + " " + QString::number(i);
|
||||
}
|
||||
|
||||
void SecureAppSettingsRepository::setInstallationUuid(const QString &uuid)
|
||||
{
|
||||
m_settings->setValue("Conf/installationUuid", uuid);
|
||||
|
||||
@@ -59,9 +59,7 @@ public:
|
||||
void setDevGatewayEndpoint();
|
||||
bool isDevGatewayEnv(bool isTestPurchase = false) const;
|
||||
void toggleDevGatewayEnv(bool enabled);
|
||||
QByteArray readGatewayProxyUrls(const QString &cacheKey) const;
|
||||
void writeGatewayProxyUrls(const QString &cacheKey, const QByteArray &proxyUrlsEncrypted);
|
||||
|
||||
|
||||
bool isKillSwitchEnabled() const;
|
||||
void setKillSwitchEnabled(bool enabled);
|
||||
bool isStrictKillSwitchEnabled() const;
|
||||
@@ -92,6 +90,8 @@ public:
|
||||
bool restoreAppConfig(const QByteArray &cfg);
|
||||
void clearSettings();
|
||||
|
||||
QString nextAvailableServerName() const;
|
||||
|
||||
QByteArray xraySavedConfigs() const;
|
||||
void setXraySavedConfigs(const QByteArray &data);
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonValue>
|
||||
#include <QSet>
|
||||
#include <QUuid>
|
||||
|
||||
#include "core/utils/serverConfigUtils.h"
|
||||
@@ -33,45 +32,6 @@ QJsonObject embedStorageServerId(const QString &serverId, const QJsonObject &pay
|
||||
return o;
|
||||
}
|
||||
|
||||
QString storedServerDisplayName(const SecureServersRepository *repository, const QString &serverId)
|
||||
{
|
||||
using Kind = serverConfigUtils::ConfigType;
|
||||
switch (repository->serverKind(serverId)) {
|
||||
case Kind::SelfHostedAdmin:
|
||||
if (const auto cfg = repository->selfHostedAdminConfig(serverId)) {
|
||||
return cfg->displayName;
|
||||
}
|
||||
break;
|
||||
case Kind::SelfHostedUser:
|
||||
if (const auto cfg = repository->selfHostedUserConfig(serverId)) {
|
||||
return cfg->displayName;
|
||||
}
|
||||
break;
|
||||
case Kind::Native:
|
||||
if (const auto cfg = repository->nativeConfig(serverId)) {
|
||||
return cfg->displayName;
|
||||
}
|
||||
break;
|
||||
case Kind::AmneziaPremiumV2:
|
||||
case Kind::AmneziaFreeV3:
|
||||
case Kind::ExternalPremium:
|
||||
if (const auto cfg = repository->apiV2Config(serverId)) {
|
||||
return cfg->displayName;
|
||||
}
|
||||
break;
|
||||
case Kind::AmneziaPremiumV1:
|
||||
case Kind::AmneziaFreeV2:
|
||||
if (const auto cfg = repository->legacyApiConfig(serverId)) {
|
||||
return cfg->displayName;
|
||||
}
|
||||
break;
|
||||
case Kind::Invalid:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SecureServersRepository::SecureServersRepository(SecureQSettings *settings, QObject *parent)
|
||||
@@ -193,28 +153,6 @@ void SecureServersRepository::clearServers()
|
||||
syncToStorage();
|
||||
}
|
||||
|
||||
QString SecureServersRepository::nextAvailableServerName() const
|
||||
{
|
||||
QSet<QString> usedNames;
|
||||
usedNames.reserve(m_orderedServerIds.size());
|
||||
|
||||
for (const QString &serverId : m_orderedServerIds) {
|
||||
const QString displayName = storedServerDisplayName(this, serverId);
|
||||
if (!displayName.isEmpty()) {
|
||||
usedNames.insert(displayName);
|
||||
}
|
||||
}
|
||||
|
||||
int i = 0;
|
||||
QString candidate;
|
||||
do {
|
||||
++i;
|
||||
candidate = tr("Server") + QLatin1Char(' ') + QString::number(i);
|
||||
} while (usedNames.contains(candidate));
|
||||
|
||||
return candidate;
|
||||
}
|
||||
|
||||
QString SecureServersRepository::addServer(const QString &serverId, const QJsonObject &serverJson, serverConfigUtils::ConfigType kind)
|
||||
{
|
||||
const QString id = normalizedOrGeneratedServerId(serverId);
|
||||
|
||||
@@ -48,8 +48,6 @@ public:
|
||||
|
||||
void clearServers();
|
||||
|
||||
QString nextAvailableServerName() const;
|
||||
|
||||
void invalidateCache();
|
||||
|
||||
signals:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "core/utils/serverConfigUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include <QLatin1Char>
|
||||
#include <QDateTime>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
@@ -76,6 +77,26 @@ bool apiUtils::isSubscriptionExpiringSoon(const QString &subscriptionEndDate, in
|
||||
return endDate <= nowUtc.addDays(withinDays);
|
||||
}
|
||||
|
||||
amnezia::ErrorCode apiUtils::errorCodeFromGatewayJsonHttpStatus(const QJsonObject &jsonObj)
|
||||
{
|
||||
if (!jsonObj.contains(QStringLiteral("http_status"))) {
|
||||
return amnezia::ErrorCode::NoError;
|
||||
}
|
||||
const int st = jsonObj.value(QStringLiteral("http_status")).toInt(-1);
|
||||
switch (st) {
|
||||
case 200: return amnezia::ErrorCode::NoError;
|
||||
case 400: return amnezia::ErrorCode::ApiConfigEmptyError;
|
||||
case 403: return amnezia::ErrorCode::ApiPairingForbiddenError;
|
||||
case 404: return amnezia::ErrorCode::ApiNotFoundError;
|
||||
case 408: return amnezia::ErrorCode::ApiConfigTimeoutError;
|
||||
case 409: return amnezia::ErrorCode::ApiPairingConflictError;
|
||||
case 429: return amnezia::ErrorCode::ApiPairingRateLimitedError;
|
||||
case 500: return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||
case 503: return amnezia::ErrorCode::ApiPairingServiceUnavailableError;
|
||||
default: return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||
}
|
||||
}
|
||||
|
||||
amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &sslErrors, const QString &replyErrorString,
|
||||
const QNetworkReply::NetworkError &replyError, const int httpStatusCode,
|
||||
const QByteArray &responseBody)
|
||||
@@ -84,14 +105,15 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
|
||||
const int httpStatusCodeNotFound = 404;
|
||||
const int httpStatusCodeNotImplemented = 501;
|
||||
const int httpStatusCodePaymentRequired = 402;
|
||||
const int httpStatusCodeTooManyRequests = 429;
|
||||
const int httpStatusCodeRequestTimeout = 408;
|
||||
const int httpStatusCodeUnprocessableEntity = 422;
|
||||
|
||||
if (!sslErrors.empty()) {
|
||||
qDebug().noquote() << sslErrors;
|
||||
return amnezia::ErrorCode::ApiConfigSslError;
|
||||
}
|
||||
if (replyError == QNetworkReply::NoError) {
|
||||
return amnezia::ErrorCode::NoError;
|
||||
}
|
||||
if (replyError == QNetworkReply::NetworkError::OperationCanceledError
|
||||
|| replyError == QNetworkReply::NetworkError::TimeoutError) {
|
||||
qDebug() << replyError;
|
||||
@@ -102,14 +124,14 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
|
||||
return amnezia::ErrorCode::ApiUpdateRequestError;
|
||||
}
|
||||
|
||||
qDebug() << QString::fromUtf8(responseBody);
|
||||
qDebug() << replyError;
|
||||
qDebug() << httpStatusCode;
|
||||
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
|
||||
if (jsonDoc.isObject()) {
|
||||
QJsonObject jsonObj = jsonDoc.object();
|
||||
const int httpStatusFromBody = jsonObj.value(QStringLiteral("http_status")).toInt(-1);
|
||||
|
||||
if (httpStatusFromBody == httpStatusCodeTooManyRequests) {
|
||||
return amnezia::ErrorCode::ApiRateLimitError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodeConflict) {
|
||||
if (apiErrorMessageFromJson(jsonObj).contains(trialAlreadyUsedMessage, Qt::CaseInsensitive)) {
|
||||
return amnezia::ErrorCode::ApiTrialAlreadyUsedError;
|
||||
@@ -119,9 +141,6 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
|
||||
if (httpStatusFromBody == httpStatusCodeNotFound) {
|
||||
return amnezia::ErrorCode::ApiNotFoundError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodeRequestTimeout) {
|
||||
return amnezia::ErrorCode::ApiConfigTimeoutError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodeNotImplemented) {
|
||||
return amnezia::ErrorCode::ApiUpdateRequestError;
|
||||
}
|
||||
@@ -132,28 +151,28 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
|
||||
return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodePaymentRequired) {
|
||||
const QString message = apiErrorMessageFromJson(jsonObj);
|
||||
if (message.contains(QLatin1String("refresh_captcha"), Qt::CaseInsensitive)) {
|
||||
return amnezia::ErrorCode::ApiCaptchaRefreshError;
|
||||
}
|
||||
if (message.contains(QLatin1String("invalid_captcha"), Qt::CaseInsensitive)) {
|
||||
return amnezia::ErrorCode::ApiCaptchaInvalidError;
|
||||
}
|
||||
if (jsonObj.contains(QStringLiteral("captcha_id")) || jsonObj.contains(QStringLiteral("captcha_image"))
|
||||
|| message.compare(QLatin1String("rate_limit_exceeded"), Qt::CaseInsensitive) == 0
|
||||
|| message.contains(QLatin1String("rate_limit_exceeded"), Qt::CaseInsensitive)) {
|
||||
return amnezia::ErrorCode::ApiCaptchaRequiredError;
|
||||
}
|
||||
return amnezia::ErrorCode::ApiSubscriptionNotActiveError;
|
||||
}
|
||||
|
||||
if (httpStatusFromBody >= 300) {
|
||||
return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||
const QString msg = apiErrorMessageFromJson(jsonObj);
|
||||
if (msg.contains(QStringLiteral("QR session"), Qt::CaseInsensitive)
|
||||
&& (msg.contains(QStringLiteral("not found"), Qt::CaseInsensitive)
|
||||
|| msg.contains(QStringLiteral("expired"), Qt::CaseInsensitive))) {
|
||||
return amnezia::ErrorCode::ApiPairingSessionExpiredError;
|
||||
}
|
||||
if (msg.contains(QStringLiteral("not found"), Qt::CaseInsensitive)
|
||||
|| msg.contains(QStringLiteral("expired"), Qt::CaseInsensitive)) {
|
||||
return amnezia::ErrorCode::ApiNotFoundError;
|
||||
}
|
||||
if (httpStatusCode == httpStatusCodeNotFound) {
|
||||
return amnezia::ErrorCode::ApiNotFoundError;
|
||||
}
|
||||
|
||||
return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||
}
|
||||
|
||||
if (replyError == QNetworkReply::NoError) {
|
||||
return amnezia::ErrorCode::NoError;
|
||||
if (httpStatusCode == httpStatusCodeNotFound) {
|
||||
return amnezia::ErrorCode::ApiNotFoundError;
|
||||
}
|
||||
|
||||
qDebug() << "something went wrong";
|
||||
@@ -253,3 +272,18 @@ QString apiUtils::getPremiumV2VpnKey(const QJsonObject &serverConfigObject)
|
||||
|
||||
return vpnKeyText;
|
||||
}
|
||||
|
||||
QString apiUtils::countryCodeBaseForFlag(const QString &fullCountryCode)
|
||||
{
|
||||
const QString trimmed = fullCountryCode.trimmed();
|
||||
if (trimmed.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
const int dashIdx = trimmed.indexOf(QLatin1Char('-'));
|
||||
const QString base = dashIdx < 0 ? trimmed : trimmed.left(dashIdx);
|
||||
const QString normalized = base.trimmed();
|
||||
if (normalized.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
return normalized.toUpper();
|
||||
}
|
||||
|
||||
@@ -23,8 +23,13 @@ namespace apiUtils
|
||||
const QNetworkReply::NetworkError &replyError, const int httpStatusCode,
|
||||
const QByteArray &responseBody);
|
||||
|
||||
amnezia::ErrorCode errorCodeFromGatewayJsonHttpStatus(const QJsonObject &jsonObj);
|
||||
|
||||
QString getPremiumV1VpnKey(const QJsonObject &serverConfigObject);
|
||||
QString getPremiumV2VpnKey(const QJsonObject &serverConfigObject);
|
||||
|
||||
// ISO2-style segment for flagKit assets (e.g. US-WEST -> US). Do not use in API request bodies.
|
||||
QString countryCodeBaseForFlag(const QString &fullCountryCode);
|
||||
}
|
||||
|
||||
#endif // APIUTILS_H
|
||||
|
||||
@@ -18,10 +18,11 @@ namespace apiDefs
|
||||
constexpr QLatin1String stackType("stack_type");
|
||||
constexpr QLatin1String cliVersion("cli_version");
|
||||
constexpr QLatin1String cliName("cli_name");
|
||||
constexpr QLatin1String supportedProtocols("supported_protocols");
|
||||
constexpr QLatin1String availableCountries("available_countries");
|
||||
constexpr QLatin1String availableProtocols("available_protocols");
|
||||
constexpr QLatin1String installationUuid("installation_uuid");
|
||||
constexpr QLatin1String uuid("installation_uuid");
|
||||
constexpr QLatin1String qrUuid("qr_uuid");
|
||||
constexpr QLatin1String osVersion("os_version");
|
||||
constexpr QLatin1String userCountryCode("user_country_code");
|
||||
constexpr QLatin1String serverCountryCode("server_country_code");
|
||||
|
||||
@@ -25,13 +25,6 @@ namespace amnezia
|
||||
constexpr QLatin1String config("config");
|
||||
constexpr QLatin1String configVersion("config_version");
|
||||
|
||||
constexpr QLatin1String sendPayload("send_payload");
|
||||
constexpr QLatin1String sendPayloadEndpoint("endpoint");
|
||||
constexpr QLatin1String sendPayloadTimeoutMs("timeout_ms");
|
||||
constexpr QLatin1String sendPayloadProtocol("protocol");
|
||||
constexpr QLatin1String sendPayloadData("payload");
|
||||
constexpr QLatin1String sendPayloadExpectedResponse("expected_response");
|
||||
|
||||
constexpr QLatin1String containers("containers");
|
||||
constexpr QLatin1String container("container");
|
||||
constexpr QLatin1String defaultContainer("defaultContainer");
|
||||
|
||||
@@ -271,7 +271,6 @@ namespace amnezia
|
||||
constexpr char workersModeAuto[] = "auto";
|
||||
constexpr char workersModeManual[] = "manual";
|
||||
constexpr int maxWorkers = 32;
|
||||
constexpr int botTagHexLength = 32;
|
||||
}
|
||||
|
||||
} // namespace protocols
|
||||
|
||||
@@ -15,8 +15,6 @@ namespace amnezia
|
||||
Awg2,
|
||||
WireGuard,
|
||||
OpenVpn,
|
||||
Cloak,
|
||||
ShadowSocks,
|
||||
Ipsec,
|
||||
Xray,
|
||||
SSXray,
|
||||
|
||||
@@ -21,8 +21,6 @@ QString ContainerUtils::containerToString(DockerContainer c)
|
||||
{
|
||||
if (c == DockerContainer::None)
|
||||
return "none";
|
||||
if (c == DockerContainer::Cloak)
|
||||
return "amnezia-openvpn-cloak";
|
||||
if (c == DockerContainer::Awg)
|
||||
return "amnezia-awg";
|
||||
if (c == DockerContainer::Awg2)
|
||||
@@ -64,8 +62,6 @@ QMap<DockerContainer, QString> ContainerUtils::containerHumanNames()
|
||||
{
|
||||
return { { DockerContainer::None, "Not installed" },
|
||||
{ DockerContainer::OpenVpn, "OpenVPN" },
|
||||
{ DockerContainer::ShadowSocks, "OpenVPN over SS" },
|
||||
{ DockerContainer::Cloak, "OpenVPN over Cloak" },
|
||||
{ DockerContainer::WireGuard, "WireGuard" },
|
||||
{ DockerContainer::Awg, "AmneziaWG" },
|
||||
{ DockerContainer::Awg2, "AmneziaWG" },
|
||||
@@ -87,10 +83,6 @@ QMap<DockerContainer, QString> ContainerUtils::containerDescriptions()
|
||||
return { { DockerContainer::OpenVpn,
|
||||
QObject::tr("OpenVPN is the most popular VPN protocol, with flexible configuration options. It uses its "
|
||||
"own security protocol with SSL/TLS for key exchange.") },
|
||||
{ DockerContainer::ShadowSocks,
|
||||
QObject::tr("This protocol is no longer supported.") },
|
||||
{ DockerContainer::Cloak,
|
||||
QObject::tr("This protocol is no longer supported.") },
|
||||
{ DockerContainer::WireGuard,
|
||||
QObject::tr("WireGuard - popular VPN protocol with high performance, high speed and low power "
|
||||
"consumption.") },
|
||||
@@ -202,9 +194,6 @@ QMap<DockerContainer, QString> ContainerUtils::containerDetailedDescriptions()
|
||||
|
||||
ServiceType ContainerUtils::containerService(DockerContainer c)
|
||||
{
|
||||
if (isUnsupportedContainer(c)) {
|
||||
return ServiceType::Vpn;
|
||||
}
|
||||
return ProtocolUtils::protocolService(defaultProtocol(c));
|
||||
}
|
||||
|
||||
@@ -213,8 +202,6 @@ Proto ContainerUtils::defaultProtocol(DockerContainer c)
|
||||
switch (c) {
|
||||
case DockerContainer::None: return Proto::Unknown;
|
||||
case DockerContainer::OpenVpn: return Proto::OpenVpn;
|
||||
case DockerContainer::Cloak:
|
||||
case DockerContainer::ShadowSocks: return Proto::Unknown;
|
||||
case DockerContainer::WireGuard: return Proto::WireGuard;
|
||||
case DockerContainer::Awg2: return Proto::Awg;
|
||||
case DockerContainer::Awg: return Proto::Awg;
|
||||
@@ -265,8 +252,6 @@ bool ContainerUtils::isSupportedByCurrentPlatform(DockerContainer c)
|
||||
// macOS build using Network Extension – allow OpenVPN for parity with iOS.
|
||||
switch (c) {
|
||||
case DockerContainer::OpenVpn: return true;
|
||||
case DockerContainer::Cloak: return false;
|
||||
case DockerContainer::ShadowSocks: return false;
|
||||
case DockerContainer::WireGuard: return true;
|
||||
case DockerContainer::Awg2: return true;
|
||||
case DockerContainer::Awg: return true;
|
||||
@@ -351,10 +336,6 @@ int ContainerUtils::easySetupOrder(DockerContainer container)
|
||||
|
||||
bool ContainerUtils::isShareable(DockerContainer container)
|
||||
{
|
||||
if (isUnsupportedContainer(container)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (container) {
|
||||
case DockerContainer::TorWebSite: return false;
|
||||
case DockerContainer::Dns: return false;
|
||||
@@ -371,11 +352,6 @@ bool ContainerUtils::isAwgContainer(DockerContainer container)
|
||||
return container == DockerContainer::Awg || container == DockerContainer::Awg2;
|
||||
}
|
||||
|
||||
bool ContainerUtils::isUnsupportedContainer(DockerContainer container)
|
||||
{
|
||||
return container == DockerContainer::Cloak || container == DockerContainer::ShadowSocks;
|
||||
}
|
||||
|
||||
QJsonObject ContainerUtils::getProtocolConfigFromContainer(const Proto protocol, const QJsonObject &containerConfig)
|
||||
{
|
||||
QString protocolConfigString = containerConfig.value(ProtocolUtils::protoToString(protocol))
|
||||
|
||||
@@ -45,8 +45,6 @@ namespace amnezia
|
||||
|
||||
bool isAwgContainer(DockerContainer container);
|
||||
|
||||
bool isUnsupportedContainer(DockerContainer container);
|
||||
|
||||
QJsonObject getProtocolConfigFromContainer(const Proto protocol, const QJsonObject &containerConfig);
|
||||
|
||||
int installPageOrder(DockerContainer container);
|
||||
|
||||
@@ -35,11 +35,6 @@ namespace amnezia
|
||||
ServerCgroupMountpoint = 212,
|
||||
DockerPullRateLimit = 213,
|
||||
ServerLinuxKernelTooOld = 214,
|
||||
XrayServerConfigInvalid = 215,
|
||||
XrayServerNoVlessClients = 216,
|
||||
XrayRealityKeysReadFailed = 217,
|
||||
ServerContainerRuntimeNotSupported = 218,
|
||||
ContainerRuntimeServiceNotRunning = 219,
|
||||
|
||||
// Ssh connection errors
|
||||
SshRequestDeniedError = 300,
|
||||
@@ -81,7 +76,6 @@ namespace amnezia
|
||||
ImportBackupFileUseRestoreInstead = 903,
|
||||
RestoreBackupInvalidError = 904,
|
||||
LegacyApiV1NotSupportedError = 905,
|
||||
LegacyContainerNotSupportedError = 906,
|
||||
|
||||
// Android errors
|
||||
AndroidError = 1000,
|
||||
@@ -104,10 +98,15 @@ namespace amnezia
|
||||
ApiSubscriptionNotActiveError = 1114,
|
||||
ApiNoPurchasedSubscriptionsError = 1115,
|
||||
ApiTrialAlreadyUsedError = 1116,
|
||||
ApiCaptchaRequiredError = 1117,
|
||||
ApiCaptchaInvalidError = 1118,
|
||||
ApiCaptchaRefreshError = 1119,
|
||||
ApiRateLimitError = 1120,
|
||||
|
||||
// QR pairing (gateway /v1/generate_qr, /v1/scan_qr)
|
||||
ApiPairingForbiddenError = 1117,
|
||||
ApiPairingConflictError = 1118,
|
||||
ApiPairingRateLimitedError = 1119,
|
||||
ApiPairingServiceUnavailableError = 1120,
|
||||
ApiPairingPayloadTooLargeError = 1121,
|
||||
ApiPairingMissingMetadataError = 1122,
|
||||
ApiPairingSessionExpiredError = 1123,
|
||||
|
||||
// QFile errors
|
||||
OpenError = 1200,
|
||||
@@ -126,3 +125,5 @@ namespace amnezia
|
||||
Q_DECLARE_METATYPE(amnezia::ErrorCode)
|
||||
|
||||
#endif // ERRORCODES_H
|
||||
|
||||
|
||||
|
||||
@@ -30,17 +30,6 @@ QString errorString(ErrorCode code) {
|
||||
case(ErrorCode::ServerCgroupMountpoint): errorMessage = QObject::tr("Server error: cgroup mountpoint does not exist"); break;
|
||||
case(ErrorCode::DockerPullRateLimit): errorMessage = QObject::tr("Docker error: The pull rate limit has been reached"); break;
|
||||
case(ErrorCode::ServerLinuxKernelTooOld): errorMessage = QObject::tr("Server error: Linux kernel is too old"); break;
|
||||
case(ErrorCode::XrayServerConfigInvalid):
|
||||
errorMessage = QObject::tr("Server error: invalid or unreadable XRay server configuration");
|
||||
break;
|
||||
case(ErrorCode::XrayServerNoVlessClients):
|
||||
errorMessage = QObject::tr("Server error: XRay server has no VLESS clients");
|
||||
break;
|
||||
case(ErrorCode::XrayRealityKeysReadFailed):
|
||||
errorMessage = QObject::tr("Server error: failed to read XRay Reality keys from the server");
|
||||
break;
|
||||
case(ErrorCode::ServerContainerRuntimeNotSupported): errorMessage = QObject::tr("Server error: The default container runtime available for installation on this server is not supported.\n Install Docker Engine on the server manually and try again."); break;
|
||||
case(ErrorCode::ContainerRuntimeServiceNotRunning): errorMessage = QObject::tr("Container runtime error: The container runtime service is not running.\n Check the container runtime service on the server, or wait about a minute and try again."); break;
|
||||
|
||||
// Libssh errors
|
||||
case(ErrorCode::SshRequestDeniedError): errorMessage = QObject::tr("SSH request was denied"); break;
|
||||
@@ -71,7 +60,6 @@ QString errorString(ErrorCode code) {
|
||||
case (ErrorCode::ImportBackupFileUseRestoreInstead): errorMessage = QObject::tr("Backup files cannot be imported here. Use 'Restore from backup' instead."); break;
|
||||
case (ErrorCode::RestoreBackupInvalidError): errorMessage = QObject::tr("Backup file is corrupted or has invalid format"); break;
|
||||
case (ErrorCode::LegacyApiV1NotSupportedError): errorMessage = QObject::tr("This legacy Amnezia subscription format is no longer supported"); break;
|
||||
case (ErrorCode::LegacyContainerNotSupportedError): errorMessage = QObject::tr("This protocol is no longer supported. Please select another protocol or remove this container from the server settings."); break;
|
||||
case (ErrorCode::ImportOpenConfigError): errorMessage = QObject::tr("Unable to open config file"); break;
|
||||
case (ErrorCode::NoInstalledContainersError): errorMessage = QObject::tr("VPN Protocols is not installed.\n Please install VPN container at first"); break;
|
||||
|
||||
@@ -96,10 +84,13 @@ QString errorString(ErrorCode code) {
|
||||
case (ErrorCode::ApiSubscriptionNotActiveError): errorMessage = QObject::tr("No active subscription found"); break;
|
||||
case (ErrorCode::ApiNoPurchasedSubscriptionsError): errorMessage = QObject::tr("No purchased subscriptions found. Please purchase a subscription first"); break;
|
||||
case (ErrorCode::ApiTrialAlreadyUsedError): errorMessage = QObject::tr("This email address has already been used to activate a trial"); break;
|
||||
case (ErrorCode::ApiCaptchaRequiredError): errorMessage = QObject::tr("CAPTCHA verification is required"); break;
|
||||
case (ErrorCode::ApiCaptchaInvalidError): errorMessage = QObject::tr("CAPTCHA was incorrect. Please try again"); break;
|
||||
case (ErrorCode::ApiCaptchaRefreshError): errorMessage = QObject::tr("CAPTCHA refreshed. Please try again"); break;
|
||||
case (ErrorCode::ApiRateLimitError): errorMessage = QObject::tr("Too many requests. Please try again later"); break;
|
||||
case (ErrorCode::ApiPairingForbiddenError): errorMessage = QObject::tr("QR pairing was rejected (forbidden)"); break;
|
||||
case (ErrorCode::ApiPairingConflictError): errorMessage = QObject::tr("This QR code has already been used"); break;
|
||||
case (ErrorCode::ApiPairingRateLimitedError): errorMessage = QObject::tr("Too many requests. Please try again later"); break;
|
||||
case (ErrorCode::ApiPairingServiceUnavailableError): errorMessage = QObject::tr("Service temporarily unavailable. Please try again later"); break;
|
||||
case (ErrorCode::ApiPairingPayloadTooLargeError): errorMessage = QObject::tr("QR pairing data is too large to send"); break;
|
||||
case (ErrorCode::ApiPairingMissingMetadataError): errorMessage = QObject::tr("This subscription is missing data required to transfer via QR (service type or country). Refresh the subscription or pick another server."); break;
|
||||
case (ErrorCode::ApiPairingSessionExpiredError): errorMessage = QObject::tr("The QR code session has ended. Show a new QR code on the other device and scan again."); break;
|
||||
|
||||
// QFile errors
|
||||
case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break;
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
#include "payloadSender.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QEventLoop>
|
||||
#include <QHostAddress>
|
||||
#include <QJsonObject>
|
||||
#include <QRandomGenerator>
|
||||
#include <QRegularExpression>
|
||||
#include <QTcpSocket>
|
||||
#include <QTimer>
|
||||
#include <QUdpSocket>
|
||||
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr char protoUdp[] = "udp";
|
||||
constexpr char protoTcp[] = "tcp";
|
||||
|
||||
QByteArray randomBytes(int count)
|
||||
{
|
||||
if (count <= 0) {
|
||||
return {};
|
||||
}
|
||||
QByteArray bytes(count, Qt::Uninitialized);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
bytes[i] = static_cast<char>(QRandomGenerator::global()->bounded(256));
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
bool parseEndpoint(const QString &endpoint, QString &host, quint16 &port)
|
||||
{
|
||||
const int separatorIndex = endpoint.lastIndexOf(QLatin1Char(':'));
|
||||
if (separatorIndex <= 0 || separatorIndex == endpoint.size() - 1) {
|
||||
return false;
|
||||
}
|
||||
host = endpoint.left(separatorIndex);
|
||||
bool ok = false;
|
||||
const uint parsedPort = endpoint.mid(separatorIndex + 1).toUInt(&ok);
|
||||
if (!ok || parsedPort == 0 || parsedPort > 65535) {
|
||||
return false;
|
||||
}
|
||||
port = static_cast<quint16>(parsedPort);
|
||||
return true;
|
||||
}
|
||||
|
||||
enum class ExchangeResult { Sent, Matched, Mismatch, Timeout };
|
||||
|
||||
ExchangeResult waitForResponse(QAbstractSocket &socket, const QByteArray &expectedResponse, int timeoutMs)
|
||||
{
|
||||
if (expectedResponse.isEmpty()) {
|
||||
return ExchangeResult::Sent;
|
||||
}
|
||||
|
||||
QByteArray response;
|
||||
QEventLoop loop;
|
||||
QTimer timer;
|
||||
timer.setSingleShot(true);
|
||||
QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
|
||||
QObject::connect(&socket, &QIODevice::readyRead, &loop, [&]() {
|
||||
response.append(socket.readAll());
|
||||
if (response.size() >= expectedResponse.size()) {
|
||||
loop.quit();
|
||||
}
|
||||
});
|
||||
|
||||
timer.start(timeoutMs);
|
||||
loop.exec();
|
||||
|
||||
if (response.isEmpty()) {
|
||||
return ExchangeResult::Timeout;
|
||||
}
|
||||
return response == expectedResponse ? ExchangeResult::Matched : ExchangeResult::Mismatch;
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray PayloadSender::buildPayload(const QString &tagString)
|
||||
{
|
||||
QByteArray result;
|
||||
if (tagString.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
static const QRegularExpression tagRegExp(QStringLiteral("<([^>]*)>"));
|
||||
QRegularExpressionMatchIterator it = tagRegExp.globalMatch(tagString);
|
||||
while (it.hasNext()) {
|
||||
const QRegularExpressionMatch match = it.next();
|
||||
const QStringList parts = match.captured(1).simplified().split(QLatin1Char(' '), Qt::SkipEmptyParts);
|
||||
if (parts.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const QString tag = parts.at(0).toLower();
|
||||
if (tag == QLatin1String("b")) {
|
||||
if (parts.size() < 2) {
|
||||
continue;
|
||||
}
|
||||
QString hex = parts.at(1);
|
||||
if (hex.startsWith(QLatin1String("0x"), Qt::CaseInsensitive)) {
|
||||
hex = hex.mid(2);
|
||||
}
|
||||
result.append(QByteArray::fromHex(hex.toLatin1()));
|
||||
} else if (tag == QLatin1String("r")) {
|
||||
if (parts.size() < 2) {
|
||||
continue;
|
||||
}
|
||||
bool ok = false;
|
||||
const int count = parts.at(1).toInt(&ok);
|
||||
if (ok) {
|
||||
result.append(randomBytes(count));
|
||||
}
|
||||
} else {
|
||||
qWarning() << "PayloadSender: unknown payload tag" << tag;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void PayloadSender::sendAll(const QJsonArray &sendPayload)
|
||||
{
|
||||
for (int i = 0; i < sendPayload.size(); ++i) {
|
||||
const QJsonValue value = sendPayload.at(i);
|
||||
if (!value.isObject()) {
|
||||
continue;
|
||||
}
|
||||
sendEntry(value.toObject(), i);
|
||||
}
|
||||
}
|
||||
|
||||
void PayloadSender::sendEntry(const QJsonObject &entry, int index)
|
||||
{
|
||||
const QString endpoint = entry.value(configKey::sendPayloadEndpoint).toString();
|
||||
QString host;
|
||||
quint16 port = 0;
|
||||
if (!parseEndpoint(endpoint, host, port)) {
|
||||
qWarning() << "PayloadSender: skipping entry" << index << "- invalid endpoint" << endpoint;
|
||||
return;
|
||||
}
|
||||
|
||||
const QString protocol = entry.value(configKey::sendPayloadProtocol).toString().toLower();
|
||||
const int timeoutMs = entry.value(configKey::sendPayloadTimeoutMs).toInt();
|
||||
const QByteArray payload = buildPayload(entry.value(configKey::sendPayloadData).toString());
|
||||
const QByteArray expectedResponse = buildPayload(entry.value(configKey::sendPayloadExpectedResponse).toString());
|
||||
|
||||
const QString resolvedHost = NetworkUtilities::getIPAddress(host);
|
||||
const QHostAddress hostAddress(resolvedHost.isEmpty() ? host : resolvedHost);
|
||||
|
||||
QUdpSocket udpSocket;
|
||||
QTcpSocket tcpSocket;
|
||||
QAbstractSocket *socket = nullptr;
|
||||
if (protocol == protoUdp) {
|
||||
socket = &udpSocket;
|
||||
if (udpSocket.writeDatagram(payload, hostAddress, port) < 0) {
|
||||
qWarning() << "PayloadSender: entry" << index << "- udp write failed to" << endpoint << udpSocket.errorString();
|
||||
return;
|
||||
}
|
||||
} else if (protocol == protoTcp) {
|
||||
socket = &tcpSocket;
|
||||
tcpSocket.connectToHost(hostAddress, port);
|
||||
if (!tcpSocket.waitForConnected(timeoutMs)) {
|
||||
qWarning() << "PayloadSender: entry" << index << "- tcp connect failed to" << endpoint << tcpSocket.errorString();
|
||||
return;
|
||||
}
|
||||
if (tcpSocket.write(payload) != payload.size()) {
|
||||
qWarning() << "PayloadSender: entry" << index << "- tcp write failed to" << endpoint << tcpSocket.errorString();
|
||||
return;
|
||||
}
|
||||
tcpSocket.flush();
|
||||
} else {
|
||||
qWarning() << "PayloadSender: skipping entry" << index << "- unsupported protocol" << protocol;
|
||||
return;
|
||||
}
|
||||
|
||||
switch (waitForResponse(*socket, expectedResponse, timeoutMs)) {
|
||||
case ExchangeResult::Sent:
|
||||
qInfo() << "PayloadSender: entry" << index << "-" << protocol << "payload sent to" << endpoint;
|
||||
break;
|
||||
case ExchangeResult::Matched:
|
||||
qInfo() << "PayloadSender: entry" << index << "-" << protocol << "expected response received from" << endpoint;
|
||||
break;
|
||||
case ExchangeResult::Mismatch:
|
||||
qWarning() << "PayloadSender: entry" << index << "-" << protocol << "response mismatch from" << endpoint;
|
||||
break;
|
||||
case ExchangeResult::Timeout:
|
||||
qWarning() << "PayloadSender: entry" << index << "-" << protocol << "response timeout from" << endpoint;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
#ifndef PAYLOADSENDER_H
|
||||
#define PAYLOADSENDER_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QJsonArray>
|
||||
#include <QString>
|
||||
|
||||
class PayloadSender
|
||||
{
|
||||
public:
|
||||
static void sendAll(const QJsonArray &sendPayload);
|
||||
static QByteArray buildPayload(const QString &tagString);
|
||||
|
||||
private:
|
||||
static void sendEntry(const QJsonObject &entry, int index);
|
||||
};
|
||||
|
||||
#endif // PAYLOADSENDER_H
|
||||
@@ -3,6 +3,14 @@
|
||||
#include <QIODevice>
|
||||
#include <QList>
|
||||
|
||||
QList<QString> qrCodeUtils::generateQrCodeImageSeriesPlainText(const QByteArray &utf8Text)
|
||||
{
|
||||
const QString text = QString::fromUtf8(utf8Text);
|
||||
qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(text.toUtf8().constData(), qrcodegen::QrCode::Ecc::LOW);
|
||||
const QString svg = QString::fromStdString(toSvgString(qr, 1));
|
||||
return { svgToBase64(svg) };
|
||||
}
|
||||
|
||||
QList<QString> qrCodeUtils::generateQrCodeImageSeries(const QByteArray &data)
|
||||
{
|
||||
double k = 850;
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace qrCodeUtils
|
||||
constexpr const qint16 qrMagicCode = 1984;
|
||||
|
||||
QList<QString> generateQrCodeImageSeries(const QByteArray &data);
|
||||
QList<QString> generateQrCodeImageSeriesPlainText(const QByteArray &utf8Text);
|
||||
qrcodegen::QrCode generateQrCode(const QByteArray &data);
|
||||
QString svgToBase64(const QString &image);
|
||||
};
|
||||
|
||||
@@ -50,7 +50,6 @@ QString amnezia::scriptName(SharedScriptType type)
|
||||
switch (type) {
|
||||
case SharedScriptType::prepare_host: return QLatin1String("prepare_host.sh");
|
||||
case SharedScriptType::install_docker: return QLatin1String("install_docker.sh");
|
||||
case SharedScriptType::install_conntrack: return QLatin1String("install_conntrack.sh");
|
||||
case SharedScriptType::build_container: return QLatin1String("build_container.sh");
|
||||
case SharedScriptType::remove_container: return QLatin1String("remove_container.sh");
|
||||
case SharedScriptType::remove_all_containers: return QLatin1String("remove_all_containers.sh");
|
||||
@@ -296,8 +295,6 @@ amnezia::ScriptVars amnezia::genMtProxyVars(const ContainerConfig &containerConf
|
||||
|
||||
vars.append({{"$MTPROXY_PORT", c.port.isEmpty() ? QString(protocols::mtProxy::defaultPort) : c.port}});
|
||||
vars.append({{"$MTPROXY_SECRET", c.secret}});
|
||||
vars.append({{"$MTPROXY_REGENERATE_SECRET",
|
||||
c.secret.isEmpty() ? QStringLiteral("1") : QStringLiteral("0")}});
|
||||
vars.append({{"$MTPROXY_TAG", c.tag}});
|
||||
vars.append({{"$MTPROXY_TRANSPORT_MODE",
|
||||
c.transportMode.isEmpty() ? QString(protocols::mtProxy::transportModeStandard)
|
||||
@@ -353,8 +350,6 @@ amnezia::ScriptVars amnezia::genTelemtVars(const ContainerConfig &containerConfi
|
||||
vars.append({ { "$TELEMT_TOML_TLS", faketls ? QLatin1String("true") : QLatin1String("false") } });
|
||||
vars.append({ { "$TELEMT_PORT", c.port.isEmpty() ? QString(protocols::telemt::defaultPort) : c.port } });
|
||||
vars.append({ { "$TELEMT_SECRET", c.secret } });
|
||||
vars.append({ { "$TELEMT_REGENERATE_SECRET",
|
||||
c.secret.isEmpty() ? QStringLiteral("1") : QStringLiteral("0") } });
|
||||
vars.append({ { "$TELEMT_TAG", c.tag } });
|
||||
QString tlsDomain = c.tlsDomain;
|
||||
if (tlsDomain.isEmpty()) {
|
||||
@@ -367,14 +362,6 @@ amnezia::ScriptVars amnezia::genTelemtVars(const ContainerConfig &containerConfi
|
||||
vars.append({ { "$TELEMT_USE_MIDDLE_PROXY", c.useMiddleProxy ? QLatin1String("true") : QLatin1String("false") } });
|
||||
vars.append({ { "$TELEMT_MASK", c.maskEnabled ? QLatin1String("true") : QLatin1String("false") } });
|
||||
vars.append({ { "$TELEMT_TLS_EMULATION", c.tlsEmulation ? QLatin1String("true") : QLatin1String("false") } });
|
||||
|
||||
QStringList additionalList;
|
||||
for (const QString &s : c.additionalSecrets) {
|
||||
if (!s.isEmpty()) {
|
||||
additionalList << s;
|
||||
}
|
||||
}
|
||||
vars.append({ { "$TELEMT_ADDITIONAL_SECRETS", additionalList.join(QLatin1Char(',')) } });
|
||||
}
|
||||
|
||||
return vars;
|
||||
|
||||
@@ -21,7 +21,6 @@ enum SharedScriptType {
|
||||
// General scripts
|
||||
prepare_host,
|
||||
install_docker,
|
||||
install_conntrack,
|
||||
build_container,
|
||||
remove_container,
|
||||
remove_all_containers,
|
||||
|
||||
@@ -390,55 +390,55 @@ bool Daemon::parseConfig(const QJsonObject& obj, InterfaceConfig& config) {
|
||||
|
||||
config.m_killSwitchEnabled = QVariant(obj.value("killSwitchOption").toString()).toBool();
|
||||
|
||||
if (const auto jc = obj.value("Jc"); !jc.isUndefined()) {
|
||||
config.m_junkPacketCount = jc.toString();
|
||||
if (!obj.value("Jc").isNull()) {
|
||||
config.m_junkPacketCount = obj.value("Jc").toString();
|
||||
}
|
||||
if (const auto jmin = obj.value("Jmin"); !jmin.isUndefined()) {
|
||||
config.m_junkPacketMinSize = jmin.toString();
|
||||
if (!obj.value("Jmin").isNull()) {
|
||||
config.m_junkPacketMinSize = obj.value("Jmin").toString();
|
||||
}
|
||||
if (const auto jmax = obj.value("Jmax"); !jmax.isUndefined()) {
|
||||
config.m_junkPacketMaxSize = jmax.toString();
|
||||
if (!obj.value("Jmax").isNull()) {
|
||||
config.m_junkPacketMaxSize = obj.value("Jmax").toString();
|
||||
}
|
||||
if (const auto s1 = obj.value("S1"); !s1.isUndefined()) {
|
||||
config.m_initPacketJunkSize = s1.toString();
|
||||
if (!obj.value("S1").isNull()) {
|
||||
config.m_initPacketJunkSize = obj.value("S1").toString();
|
||||
}
|
||||
if (const auto s2 = obj.value("S2"); !s2.isUndefined()) {
|
||||
config.m_responsePacketJunkSize = s2.toString();
|
||||
if (!obj.value("S2").isNull()) {
|
||||
config.m_responsePacketJunkSize = obj.value("S2").toString();
|
||||
}
|
||||
if (const auto s3 = obj.value("S3"); !s3.isUndefined()) {
|
||||
config.m_cookieReplyPacketJunkSize = s3.toString();
|
||||
if (!obj.value("S3").isNull()) {
|
||||
config.m_cookieReplyPacketJunkSize = obj.value("S3").toString();
|
||||
}
|
||||
if (const auto s4 = obj.value("S4"); !s4.isUndefined()) {
|
||||
config.m_transportPacketJunkSize = s4.toString();
|
||||
if (!obj.value("S4").isNull()) {
|
||||
config.m_transportPacketJunkSize = obj.value("S4").toString();
|
||||
}
|
||||
|
||||
if (const auto h1 = obj.value("H1"); !h1.isUndefined()) {
|
||||
config.m_initPacketMagicHeader = h1.toString();
|
||||
if (!obj.value("H1").isNull()) {
|
||||
config.m_initPacketMagicHeader = obj.value("H1").toString();
|
||||
}
|
||||
if (const auto h2 = obj.value("H2"); !h2.isUndefined()) {
|
||||
config.m_responsePacketMagicHeader = h2.toString();
|
||||
if (!obj.value("H2").isNull()) {
|
||||
config.m_responsePacketMagicHeader = obj.value("H2").toString();
|
||||
}
|
||||
if (const auto h3 = obj.value("H3"); !h3.isUndefined()) {
|
||||
config.m_underloadPacketMagicHeader = h3.toString();
|
||||
if (!obj.value("H3").isNull()) {
|
||||
config.m_underloadPacketMagicHeader = obj.value("H3").toString();
|
||||
}
|
||||
if (const auto h4 = obj.value("H4"); !h4.isUndefined()) {
|
||||
config.m_transportPacketMagicHeader = h4.toString();
|
||||
if (!obj.value("H4").isNull()) {
|
||||
config.m_transportPacketMagicHeader = obj.value("H4").toString();
|
||||
}
|
||||
|
||||
if (const auto i1 = obj.value("I1"); !i1.isUndefined()) {
|
||||
config.m_specialJunk["I1"] = i1.toString();
|
||||
if (!obj.value("I1").isNull()) {
|
||||
config.m_specialJunk["I1"] = obj.value("I1").toString();
|
||||
}
|
||||
if (const auto i2 = obj.value("I2"); !i2.isUndefined()) {
|
||||
config.m_specialJunk["I2"] = i2.toString();
|
||||
if (!obj.value("I2").isNull()) {
|
||||
config.m_specialJunk["I2"] = obj.value("I2").toString();
|
||||
}
|
||||
if (const auto i3 = obj.value("I3"); !i3.isUndefined()) {
|
||||
config.m_specialJunk["I3"] = i3.toString();
|
||||
if (!obj.value("I3").isNull()) {
|
||||
config.m_specialJunk["I3"] = obj.value("I3").toString();
|
||||
}
|
||||
if (const auto i4 = obj.value("I4"); !i4.isUndefined()) {
|
||||
config.m_specialJunk["I4"] = i4.toString();
|
||||
if (!obj.value("I4").isNull()) {
|
||||
config.m_specialJunk["I4"] = obj.value("I4").toString();
|
||||
}
|
||||
if (const auto i5 = obj.value("I5"); !i5.isUndefined()) {
|
||||
config.m_specialJunk["I5"] = i5.toString();
|
||||
if (!obj.value("I5").isNull()) {
|
||||
config.m_specialJunk["I5"] = obj.value("I5").toString();
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17 11L21 7L17 3" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M21 7H9" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7 21L3 17L7 13" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M15 17H3" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 525 B |
@@ -10,7 +10,6 @@
|
||||
<file>controls/app.svg</file>
|
||||
<file>controls/archive-restore.svg</file>
|
||||
<file>controls/arrow-left.svg</file>
|
||||
<file>controls/arrow-left-right.svg</file>
|
||||
<file>controls/arrow-right.svg</file>
|
||||
<file>controls/bug.svg</file>
|
||||
<file>controls/check.svg</file>
|
||||
|
||||
@@ -82,32 +82,10 @@
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>CFBundleIconName</key>
|
||||
<string>AppIcon</string>
|
||||
<key>CFBundleIcons</key>
|
||||
<dict>
|
||||
<key>CFBundlePrimaryIcon</key>
|
||||
<dict>
|
||||
<key>CFBundleIconFiles</key>
|
||||
<array>
|
||||
<string>AppIcon</string>
|
||||
</array>
|
||||
<key>CFBundleIconName</key>
|
||||
<string>AppIcon</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>CFBundleIcons~ipad</key>
|
||||
<dict>
|
||||
<key>CFBundlePrimaryIcon</key>
|
||||
<dict>
|
||||
<key>CFBundleIconFiles</key>
|
||||
<array>
|
||||
<string>AppIcon</string>
|
||||
</array>
|
||||
<key>CFBundleIconName</key>
|
||||
<string>AppIcon</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict/>
|
||||
<key>CFBundleIcons~ipad</key>
|
||||
<dict/>
|
||||
<key>UTImportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
|
||||
@@ -26,8 +26,6 @@ set_target_properties(networkextension PROPERTIES
|
||||
XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2"
|
||||
|
||||
XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/../../Frameworks"
|
||||
|
||||
XCODE_LINK_BUILD_PHASE_MODE KNOWN_LOCATION
|
||||
)
|
||||
|
||||
if(DEPLOY)
|
||||
@@ -116,20 +114,10 @@ target_include_directories(networkextension PRIVATE ${CLIENT_ROOT_DIR})
|
||||
target_include_directories(networkextension PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
|
||||
|
||||
find_package(openvpnadapter REQUIRED)
|
||||
# FIXME(ygurov): https://github.com/conan-io/conan/issues/20034
|
||||
set_property(TARGET amnezia::openvpnadapter APPEND PROPERTY IMPORTED_CONFIGURATIONS DEBUG)
|
||||
set_property(TARGET amnezia::openvpnadapter APPEND PROPERTY IMPORTED_CONFIGURATIONS MINSIZEREL)
|
||||
set_property(TARGET amnezia::openvpnadapter APPEND PROPERTY IMPORTED_CONFIGURATIONS RELEASE)
|
||||
set_property(TARGET amnezia::openvpnadapter APPEND PROPERTY IMPORTED_CONFIGURATIONS RELWITHDEBINFO)
|
||||
target_link_libraries(networkextension PRIVATE amnezia::openvpnadapter)
|
||||
|
||||
find_package(awg-apple REQUIRED)
|
||||
target_link_libraries(networkextension PRIVATE amnezia::awg-apple)
|
||||
|
||||
find_package(hev-socks5-tunnel REQUIRED)
|
||||
# FIXME(ygurov): https://github.com/conan-io/conan/issues/20034
|
||||
set_property(TARGET heiher::hev-socks5-tunnel APPEND PROPERTY IMPORTED_CONFIGURATIONS DEBUG)
|
||||
set_property(TARGET heiher::hev-socks5-tunnel APPEND PROPERTY IMPORTED_CONFIGURATIONS MINSIZEREL)
|
||||
set_property(TARGET heiher::hev-socks5-tunnel APPEND PROPERTY IMPORTED_CONFIGURATIONS RELEASE)
|
||||
set_property(TARGET heiher::hev-socks5-tunnel APPEND PROPERTY IMPORTED_CONFIGURATIONS RELWITHDEBINFO)
|
||||
target_link_libraries(networkextension PRIVATE heiher::hev-socks5-tunnel)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user