mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-11-14 00:23:42 +09:00
Compare commits
52
Commits
feat/tests
...
5.1.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f15932bf22 | ||
|
|
8165462865 | ||
|
|
864c1faab8 | ||
|
|
3a6bf7376c | ||
|
|
ba402f898a | ||
|
|
ac175c8a81 | ||
|
|
3f6ecacff1 | ||
|
|
7f9b7db482 | ||
|
|
6c0e62753d | ||
|
|
da8ce1809b | ||
|
|
94ea5b00aa | ||
|
|
c18f7773e3 | ||
|
|
95501dba35 | ||
|
|
4cacfb41ab | ||
|
|
8a51177a44 | ||
|
|
3121775a6f | ||
|
|
3977447d4a | ||
|
|
bf95867a7c | ||
|
|
a55ef420e8 | ||
|
|
3b5a93889f | ||
|
|
02838a94d5 | ||
|
|
9a1200d6f3 | ||
|
|
ddde43635e | ||
|
|
ffba1cb2a4 | ||
|
|
30ff3451c7 | ||
|
|
db951e9928 | ||
|
|
a36399a4c3 | ||
|
|
8940f8e273 | ||
|
|
705a373d51 | ||
|
|
3d4a45246a | ||
|
|
11355e1de0 | ||
|
|
3e8077f2ca | ||
|
|
c52b1a50a8 | ||
|
|
2c0f169946 | ||
|
|
693d00ded5 | ||
|
|
f1af8efb4b | ||
|
|
4c2e950864 | ||
|
|
9017984b1d | ||
|
|
c5ecf6281f | ||
|
|
3e9b70a5f1 | ||
|
|
94b51df247 | ||
|
|
de93650a90 | ||
|
|
448baad3e1 | ||
|
|
aca1b7dc5c | ||
|
|
de36a81b58 | ||
|
|
76b81b43c1 | ||
|
|
bea2f5f7db | ||
|
|
a4d38b5642 | ||
|
|
b76c88bba3 | ||
|
|
eab14fd957 | ||
|
|
2a8242d306 | ||
|
|
e60842bd9d |
No files matched your search
@@ -0,0 +1,23 @@
|
||||
## Version differences
|
||||
|
||||
### Android
|
||||
* For Android version 11 and higher, you need to use .apk that start with - AmneziaVPN_${VERSION}_android11+
|
||||
* For Android version 9-10, you need to use .apk that start with - AmneziaVPN_${VERSION}_android9-10
|
||||
* ***The app version for Android 7 and 8 is temporarily unavailable***
|
||||
|
||||
### macOS
|
||||
!!! The previous version (4.8.8.2 and lower) of the application (installed via the .dmg installer) must be uninstalled via “move to bin” in the applications folder
|
||||
|
||||
* For macOS version 13 and higher, you need to use .pkg - AmneziaVPN_${VERSION}_macos.pkg
|
||||
* ***The app version for macOS 10.15 - 12, is temporarily unavailable***
|
||||
|
||||
### Linux
|
||||
For Ubuntu you need to install xcb plugin:
|
||||
apt install libxcb-cursor0 libxcb-xinerama0 libxcb-icccm4 libxcb-keysyms1 libopengl0 libxkbcommon-x11-0
|
||||
|
||||
## Changelog
|
||||
|
||||
### General
|
||||
* Improve stability
|
||||
|
||||
**Full Changelog**: https://github.com/amnezia-vpn/amnezia-client/compare/${PREVIOUS_TAG}...${VERSION}
|
||||
+719
-20
@@ -4,6 +4,8 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
tags:
|
||||
- '[0-9]+.[0-9]+.[0-9]+.[0-9]+'
|
||||
|
||||
env:
|
||||
QT_MIRROR: https://mirrors.ocf.berkeley.edu/qt/ # https://download.qt.io/static/mirrorlist/
|
||||
@@ -88,7 +90,7 @@ jobs:
|
||||
host: 'linux'
|
||||
target: 'desktop'
|
||||
arch: 'linux_gcc_64'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
|
||||
dir: ${{ runner.temp }}
|
||||
setup-python: 'true'
|
||||
tools: 'tools_ifw'
|
||||
@@ -118,8 +120,16 @@ jobs:
|
||||
shell: bash
|
||||
env:
|
||||
QT_INSTALL_DIR: ${{ runner.temp }}
|
||||
AMNEZIA_BUILD_TESTS: ON
|
||||
run: deploy/build.sh --generator Ninja --installer all
|
||||
|
||||
- name: 'Upload Linux test build'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: linux-build
|
||||
path: deploy/build/client/tests
|
||||
retention-days: 7
|
||||
|
||||
- name: 'Upload installer artifact'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
@@ -201,7 +211,7 @@ jobs:
|
||||
host: 'windows'
|
||||
target: 'desktop'
|
||||
arch: 'win64_msvc2022_64'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
|
||||
dir: ${{ runner.temp }}
|
||||
setup-python: 'true'
|
||||
tools: 'tools_ifw'
|
||||
@@ -242,10 +252,20 @@ jobs:
|
||||
shell: cmd
|
||||
env:
|
||||
QT_INSTALL_DIR: ${{ runner.temp }}
|
||||
AMNEZIA_BUILD_TESTS: ON
|
||||
run: |
|
||||
set WIX_ROOT_PATH="${{ env.USERPROFILE }}\.dotnet\tools"
|
||||
deploy\build.bat --installer all
|
||||
|
||||
- name: 'Upload Windows test build'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: windows-build
|
||||
path: |
|
||||
deploy/build/client/tests
|
||||
deploy/build/client/Release
|
||||
retention-days: 7
|
||||
|
||||
- name: 'Upload WIX installer artifact'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
@@ -306,7 +326,7 @@ jobs:
|
||||
# ------------------------------------------------------
|
||||
|
||||
Build-iOS:
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-26
|
||||
|
||||
needs: Bake-Prebuilts-iOS
|
||||
if: ${{ always() }}
|
||||
@@ -454,7 +474,7 @@ jobs:
|
||||
# ------------------------------------------------------
|
||||
|
||||
Build-MacOS:
|
||||
runs-on: macos-15
|
||||
runs-on: macos-26
|
||||
|
||||
needs: Bake-Prebuilts-MacOS
|
||||
if: ${{ always() }}
|
||||
@@ -504,13 +524,13 @@ jobs:
|
||||
xcode-version: '26.3.0'
|
||||
|
||||
- name: 'Install Qt'
|
||||
uses: jurplel/install-qt-action@v4
|
||||
uses: jurplel/install-qt-action@v4.3.1
|
||||
with:
|
||||
version: ${{ env.QT_VERSION }}
|
||||
host: 'mac'
|
||||
target: 'desktop'
|
||||
arch: 'clang_64'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
|
||||
dir: ${{ runner.temp }}
|
||||
setup-python: 'true'
|
||||
set-env: 'true'
|
||||
@@ -536,9 +556,17 @@ jobs:
|
||||
NOTARYTOOL_TEAM_ID: ${{ secrets.MAC_TEAM_ID }}
|
||||
NOTARYTOOL_EMAIL: ${{ secrets.APPLE_DEV_EMAIL }}
|
||||
NOTARYTOOL_PASSWORD: ${{ secrets.APPLE_DEV_PASSWORD }}
|
||||
AMNEZIA_BUILD_TESTS: ON
|
||||
shell: bash
|
||||
run: deploy/build.sh --generator Ninja --installer all
|
||||
|
||||
- name: 'Upload macOS test build'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: macos-build
|
||||
path: deploy/build/client/tests
|
||||
retention-days: 7
|
||||
|
||||
- name: 'Upload installer artifact'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
@@ -645,7 +673,7 @@ jobs:
|
||||
version: ${{ env.QT_VERSION }}
|
||||
host: 'mac'
|
||||
target: 'desktop'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools qtmultimedia'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools qtmultimedia qthttpserver qtwebsockets'
|
||||
arch: 'clang_64'
|
||||
dir: ${{ runner.temp }}
|
||||
set-env: 'true'
|
||||
@@ -776,6 +804,8 @@ jobs:
|
||||
APP_ANDROID_MIN_SDK: ${{ matrix.min_sdk }}
|
||||
APP_ANDROID_MAX_SDK: ${{ matrix.max_sdk }}
|
||||
APP_ANDROID_VERSION_CODE_OFFSET: ${{ matrix.version_code_offset }}
|
||||
CMAKE_C_COMPILER_LAUNCHER: ccache
|
||||
CMAKE_CXX_COMPILER_LAUNCHER: ccache
|
||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
|
||||
@@ -792,7 +822,7 @@ jobs:
|
||||
submodules: 'true'
|
||||
|
||||
- name: 'Install desktop Qt'
|
||||
uses: jurplel/install-qt-action@v4
|
||||
uses: jurplel/install-qt-action@v4.3.1
|
||||
with:
|
||||
version: ${{ env.QT_VERSION }}
|
||||
host: 'linux'
|
||||
@@ -804,10 +834,10 @@ jobs:
|
||||
extra: '--base ${{ env.QT_MIRROR }}'
|
||||
|
||||
- name: 'Install android_x86_64 Qt'
|
||||
uses: jurplel/install-qt-action@v4
|
||||
uses: jurplel/install-qt-action@v4.3.1
|
||||
with:
|
||||
version: ${{ env.QT_VERSION }}
|
||||
host: 'linux'
|
||||
host: 'all_os'
|
||||
target: 'android'
|
||||
arch: 'android_x86_64'
|
||||
modules: ${{ env.QT_MODULES }}
|
||||
@@ -816,10 +846,10 @@ jobs:
|
||||
extra: '--base ${{ env.QT_MIRROR }}'
|
||||
|
||||
- name: 'Install android_x86 Qt'
|
||||
uses: jurplel/install-qt-action@v4
|
||||
uses: jurplel/install-qt-action@v4.3.1
|
||||
with:
|
||||
version: ${{ env.QT_VERSION }}
|
||||
host: 'linux'
|
||||
host: 'all_os'
|
||||
target: 'android'
|
||||
arch: 'android_x86'
|
||||
modules: ${{ env.QT_MODULES }}
|
||||
@@ -828,10 +858,10 @@ jobs:
|
||||
extra: '--base ${{ env.QT_MIRROR }}'
|
||||
|
||||
- name: 'Install android_armv7 Qt'
|
||||
uses: jurplel/install-qt-action@v4
|
||||
uses: jurplel/install-qt-action@v4.3.1
|
||||
with:
|
||||
version: ${{ env.QT_VERSION }}
|
||||
host: 'linux'
|
||||
host: 'all_os'
|
||||
target: 'android'
|
||||
arch: 'android_armv7'
|
||||
modules: ${{ env.QT_MODULES }}
|
||||
@@ -840,10 +870,10 @@ jobs:
|
||||
extra: '--base ${{ env.QT_MIRROR }}'
|
||||
|
||||
- name: 'Install android_arm64_v8a Qt'
|
||||
uses: jurplel/install-qt-action@v4
|
||||
uses: jurplel/install-qt-action@v4.3.1
|
||||
with:
|
||||
version: ${{ env.QT_VERSION }}
|
||||
host: 'linux'
|
||||
host: 'all_os'
|
||||
target: 'android'
|
||||
arch: 'android_arm64_v8a'
|
||||
modules: ${{ env.QT_MODULES }}
|
||||
@@ -870,6 +900,12 @@ jobs:
|
||||
- name: 'Install conan'
|
||||
run: pip install "conan==2.28.0"
|
||||
|
||||
- name: 'Setup ccache'
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
with:
|
||||
key: android-${{ matrix.label }}
|
||||
max-size: 1G
|
||||
|
||||
- name: 'Decode keystore secret to file'
|
||||
env:
|
||||
KEYSTORE_BASE64: ${{ secrets.ANDROID_RELEASE_KEYSTORE_BASE64 }}
|
||||
@@ -907,8 +943,7 @@ jobs:
|
||||
fi
|
||||
|
||||
for abi in arm64-v8a armeabi-v7a x86 x86_64; do
|
||||
deploy/build.sh -t android --sign --abi ${abi} --build ./deploy/build/${abi}
|
||||
mv deploy/build/${abi}/client/android-build/AmneziaVPN.apk \
|
||||
mv deploy/build/client/android-build/build/outputs/apk/oss/release/*-${abi}-release.apk \
|
||||
$OUT/AmneziaVPN_${VERSION}_${LABEL}_${abi}.apk
|
||||
done
|
||||
|
||||
@@ -927,9 +962,9 @@ jobs:
|
||||
OUT=deploy/artifacts
|
||||
VERSION=$(grep CMAKE_PROJECT_VERSION:STATIC deploy/build/CMakeCache.txt | cut -d= -f2)
|
||||
|
||||
deploy/build.sh -t android --sign --aab --play --build ./deploy/build/play
|
||||
deploy/build.sh -t android --sign --aab --play
|
||||
|
||||
mv deploy/build/play/client/android-build/build/outputs/bundle/playRelease/android-build-play-release.aab \
|
||||
mv deploy/build/client/android-build/build/outputs/bundle/playRelease/android-build-play-release.aab \
|
||||
$OUT/AmneziaVPN_${VERSION}_${LABEL}_play.aab
|
||||
|
||||
- name: 'Upload universal APK'
|
||||
@@ -1003,3 +1038,667 @@ jobs:
|
||||
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
|
||||
|
||||
# ------------------------------------------------------
|
||||
|
||||
test-linux:
|
||||
name: Test Linux (${{ matrix.filter }})
|
||||
needs: Build-Linux-Ubuntu
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
filter:
|
||||
- MultipleImportsTest
|
||||
- ServerEditTest
|
||||
- DefaultServerChangeTest
|
||||
- ServerEdgeCasesTest
|
||||
- SignalOrderTest
|
||||
- ServersModelSyncTest
|
||||
- ServerRenameTest
|
||||
- SettingsSignalsTest
|
||||
- UiServersModelAndControllerTest
|
||||
- XraySerializationTest
|
||||
- XrayServerInboundTest
|
||||
- XrayGeneratorMatrixTest
|
||||
- XrayConfigModelTest
|
||||
- XrayCoreAcceptsTest
|
||||
- UiLanguageModelAndControllerTest
|
||||
- UiIpModelAndControllerTest
|
||||
- UiAppSTModelAndControllerTest
|
||||
- UiAllowedDnsModelAndControllerTest
|
||||
- BackupExportImportTest
|
||||
- FavoriteLocationsRestoreTest
|
||||
- ConnectionTest
|
||||
- OpenSSLLibraryTest
|
||||
- GatewayServiceImportTest
|
||||
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
env:
|
||||
QT_VERSION: 6.10.1
|
||||
QTEST_FUNCTION_TIMEOUT: 7200000
|
||||
THIRD_PARTY_AWG_VPN_KEY: ${{ secrets.THIRD_PARTY_AWG_VPN_KEY }}
|
||||
THIRD_PARTY_WIRE_GUARD_VPN_KEY: ${{ secrets.THIRD_PARTY_WIRE_GUARD_VPN_KEY }}
|
||||
THIRD_PARTY_XRAY_VPN_KEY: ${{ secrets.THIRD_PARTY_XRAY_VPN_KEY }}
|
||||
THIRD_PARTY_OPEN_VPN_KEY: ${{ secrets.THIRD_PARTY_OPEN_VPN_KEY }}
|
||||
THIRD_PARTY_CLOAK_VPN_KEY: ${{ secrets.THIRD_PARTY_CLOAK_VPN_KEY }}
|
||||
THIRD_PARTY_SS_VPN_KEY: ${{ secrets.THIRD_PARTY_SS_VPN_KEY }}
|
||||
THIRD_PARTY_IKEV_VPN_KEY: ${{ secrets.THIRD_PARTY_IKEV_VPN_KEY }}
|
||||
THIRD_PARTY_PREMIUM_VPN_KEY: ${{ secrets.THIRD_PARTY_PREMIUM_VPN_KEY }}
|
||||
THIRD_PARTY_VMESS_NEW_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VMESS_NEW_IMPORT_DATA }}
|
||||
THIRD_PARTY_VMESS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VMESS_IMPORT_DATA }}
|
||||
THIRD_PARTY_TROJAN_IMPORT_DATA: ${{ secrets.THIRD_PARTY_TROJAN_IMPORT_DATA }}
|
||||
THIRD_PARTY_VLESS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VLESS_IMPORT_DATA }}
|
||||
THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA }}
|
||||
THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: 'true'
|
||||
fetch-depth: 10
|
||||
|
||||
- name: Decode backup file
|
||||
if: matrix.filter == 'BackupExportImportTest'
|
||||
shell: bash
|
||||
env:
|
||||
BACKUP_FILE_BASE64: ${{ secrets.BACKUP_FILE_BASE64 }}
|
||||
run: |
|
||||
if [ -z "$BACKUP_FILE_BASE64" ]; then
|
||||
echo "::error::BACKUP_FILE_BASE64 secret is not set"
|
||||
exit 1
|
||||
fi
|
||||
echo "$BACKUP_FILE_BASE64" | base64 --decode > "$RUNNER_TEMP/test.backup"
|
||||
echo "BACKUP_PATH=$RUNNER_TEMP/test.backup" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install Qt
|
||||
uses: jurplel/install-qt-action@v3
|
||||
with:
|
||||
version: ${{ env.QT_VERSION }}
|
||||
host: linux
|
||||
target: desktop
|
||||
arch: linux_gcc_64
|
||||
modules: qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets
|
||||
dir: ${{ runner.temp }}
|
||||
set-env: true
|
||||
aqtversion: '==3.3.0'
|
||||
py7zrversion: '==0.22.*'
|
||||
extra: '--base ${{ env.QT_MIRROR }}'
|
||||
|
||||
- name: Install system packages
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
libxkbcommon-x11-0 \
|
||||
libsecret-1-dev
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: linux-build
|
||||
path: deploy/build/client/tests
|
||||
|
||||
- name: Restore executable permissions
|
||||
run: |
|
||||
find deploy/build/client/tests \
|
||||
-type f \
|
||||
-name "test_*" \
|
||||
-exec chmod +x {} \;
|
||||
|
||||
- name: Download Xray core
|
||||
if: matrix.filter == 'XrayCoreAcceptsTest'
|
||||
shell: bash
|
||||
run: |
|
||||
XRAY_RELEASE=$(sed -n 's/^ARG XRAY_RELEASE="\(.*\)"/\1/p' client/server_scripts/xray/Dockerfile)
|
||||
curl -fsSL -o "$RUNNER_TEMP/xray.zip" \
|
||||
"https://github.com/XTLS/Xray-core/releases/download/${XRAY_RELEASE}/Xray-linux-64.zip"
|
||||
unzip -q -o "$RUNNER_TEMP/xray.zip" xray -d "$RUNNER_TEMP/xray"
|
||||
chmod +x "$RUNNER_TEMP/xray/xray"
|
||||
echo "XRAY_BIN=$RUNNER_TEMP/xray/xray" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Run tests
|
||||
env:
|
||||
AMNEZIA_TEST_API_KEY: ${{ secrets.AMNEZIA_TEST_API_KEY }}
|
||||
shell: bash
|
||||
run: |
|
||||
QT_ROOT="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/gcc_64"
|
||||
|
||||
export LD_LIBRARY_PATH="$PWD/deploy/build/client/tests:$QT_ROOT/lib:${LD_LIBRARY_PATH:-}"
|
||||
export QT_QPA_PLATFORM=offscreen
|
||||
|
||||
ctest --test-dir deploy/build/client/tests --output-on-failure -VV --timeout 600 -R "^${{ matrix.filter }}$"
|
||||
|
||||
# ------------------------------------------------------
|
||||
|
||||
test-vps:
|
||||
name: Test self-hosted VPS
|
||||
needs: Build-Linux-Ubuntu
|
||||
|
||||
# These tests install and remove docker containers on a single shared VPS:
|
||||
# two runs at once corrupt each other's state, so only one job may touch it.
|
||||
concurrency:
|
||||
group: vps-tests
|
||||
cancel-in-progress: false
|
||||
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
env:
|
||||
QT_VERSION: 6.10.1
|
||||
QTEST_FUNCTION_TIMEOUT: 7200000
|
||||
SELF_HOSTED_SERVER_HOST_NAME: ${{ secrets.SELF_HOSTED_SERVER_HOST_NAME }}
|
||||
SELF_HOSTED_SERVER_USER_NAME: ${{ secrets.SELF_HOSTED_SERVER_USER_NAME }}
|
||||
SELF_HOSTED_SERVER_PASSWORD: ${{ secrets.SELF_HOSTED_SERVER_PASSWORD }}
|
||||
SELF_HOSTED_SERVER_SSH_PORT: ${{ secrets.SELF_HOSTED_SERVER_SSH_PORT }}
|
||||
SELF_HOSTED_ADMIN_EXPORT_ROUND_TRIP_VPN_KEY: ${{ secrets.SELF_HOSTED_ADMIN_EXPORT_ROUND_TRIP_VPN_KEY }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: 'true'
|
||||
fetch-depth: 10
|
||||
|
||||
- name: Install Qt
|
||||
uses: jurplel/install-qt-action@v3
|
||||
with:
|
||||
version: ${{ env.QT_VERSION }}
|
||||
host: linux
|
||||
target: desktop
|
||||
arch: linux_gcc_64
|
||||
modules: qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets
|
||||
dir: ${{ runner.temp }}
|
||||
set-env: true
|
||||
aqtversion: '==3.3.0'
|
||||
py7zrversion: '==0.22.*'
|
||||
extra: '--base ${{ env.QT_MIRROR }}'
|
||||
|
||||
- name: Install system packages
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
libxkbcommon-x11-0 \
|
||||
libsecret-1-dev
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: linux-build
|
||||
path: deploy/build/client/tests
|
||||
|
||||
- name: Restore executable permissions
|
||||
run: |
|
||||
find deploy/build/client/tests \
|
||||
-type f \
|
||||
-name "test_*" \
|
||||
-exec chmod +x {} \;
|
||||
|
||||
- name: Download Xray core
|
||||
shell: bash
|
||||
run: |
|
||||
XRAY_RELEASE=$(sed -n 's/^ARG XRAY_RELEASE="\(.*\)"/\1/p' client/server_scripts/xray/Dockerfile)
|
||||
curl -fsSL -o "$RUNNER_TEMP/xray.zip" \
|
||||
"https://github.com/XTLS/Xray-core/releases/download/${XRAY_RELEASE}/Xray-linux-64.zip"
|
||||
unzip -q -o "$RUNNER_TEMP/xray.zip" xray -d "$RUNNER_TEMP/xray"
|
||||
chmod +x "$RUNNER_TEMP/xray/xray"
|
||||
echo "XRAY_BIN=$RUNNER_TEMP/xray/xray" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Run tests
|
||||
shell: bash
|
||||
run: |
|
||||
QT_ROOT="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/gcc_64"
|
||||
|
||||
export LD_LIBRARY_PATH="$PWD/deploy/build/client/tests:$QT_ROOT/lib:${LD_LIBRARY_PATH:-}"
|
||||
export QT_QPA_PLATFORM=offscreen
|
||||
|
||||
ctest --test-dir deploy/build/client/tests --output-on-failure -VV \
|
||||
--timeout 3600 \
|
||||
-R "^(SelfHostedServerSetupTest|MultipleExportsTest|AdminSelfHostedExportTest|XrayVpsMatrixTest)$"
|
||||
|
||||
# ------------------------------------------------------
|
||||
|
||||
test-windows:
|
||||
name: Test Windows (${{ matrix.filter }})
|
||||
needs: Build-Windows
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
filter:
|
||||
- MultipleImportsTest
|
||||
- ServerEditTest
|
||||
- DefaultServerChangeTest
|
||||
- ServerEdgeCasesTest
|
||||
- SignalOrderTest
|
||||
- ServersModelSyncTest
|
||||
- ServerRenameTest
|
||||
- SettingsSignalsTest
|
||||
- UiServersModelAndControllerTest
|
||||
- XraySerializationTest
|
||||
- XrayServerInboundTest
|
||||
- XrayGeneratorMatrixTest
|
||||
- XrayConfigModelTest
|
||||
- XrayCoreAcceptsTest
|
||||
- UiLanguageModelAndControllerTest
|
||||
- UiIpModelAndControllerTest
|
||||
- UiAppSTModelAndControllerTest
|
||||
- UiAllowedDnsModelAndControllerTest
|
||||
- BackupExportImportTest
|
||||
- FavoriteLocationsRestoreTest
|
||||
- ConnectionTest
|
||||
- OpenSSLLibraryTest
|
||||
- GatewayServiceImportTest
|
||||
|
||||
runs-on: windows-latest
|
||||
|
||||
env:
|
||||
QT_VERSION: 6.10.1
|
||||
QTEST_FUNCTION_TIMEOUT: 7200000
|
||||
THIRD_PARTY_AWG_VPN_KEY: ${{ secrets.THIRD_PARTY_AWG_VPN_KEY }}
|
||||
THIRD_PARTY_WIRE_GUARD_VPN_KEY: ${{ secrets.THIRD_PARTY_WIRE_GUARD_VPN_KEY }}
|
||||
THIRD_PARTY_XRAY_VPN_KEY: ${{ secrets.THIRD_PARTY_XRAY_VPN_KEY }}
|
||||
THIRD_PARTY_OPEN_VPN_KEY: ${{ secrets.THIRD_PARTY_OPEN_VPN_KEY }}
|
||||
THIRD_PARTY_CLOAK_VPN_KEY: ${{ secrets.THIRD_PARTY_CLOAK_VPN_KEY }}
|
||||
THIRD_PARTY_SS_VPN_KEY: ${{ secrets.THIRD_PARTY_SS_VPN_KEY }}
|
||||
THIRD_PARTY_IKEV_VPN_KEY: ${{ secrets.THIRD_PARTY_IKEV_VPN_KEY }}
|
||||
THIRD_PARTY_PREMIUM_VPN_KEY: ${{ secrets.THIRD_PARTY_PREMIUM_VPN_KEY }}
|
||||
THIRD_PARTY_VMESS_NEW_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VMESS_NEW_IMPORT_DATA }}
|
||||
THIRD_PARTY_VMESS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VMESS_IMPORT_DATA }}
|
||||
THIRD_PARTY_TROJAN_IMPORT_DATA: ${{ secrets.THIRD_PARTY_TROJAN_IMPORT_DATA }}
|
||||
THIRD_PARTY_VLESS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VLESS_IMPORT_DATA }}
|
||||
THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA }}
|
||||
THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: 'true'
|
||||
fetch-depth: 10
|
||||
|
||||
- name: Decode backup file
|
||||
if: matrix.filter == 'BackupExportImportTest'
|
||||
shell: pwsh
|
||||
env:
|
||||
BACKUP_FILE_BASE64: ${{ secrets.BACKUP_FILE_BASE64 }}
|
||||
run: |
|
||||
if ([string]::IsNullOrWhiteSpace($env:BACKUP_FILE_BASE64)) {
|
||||
Write-Output "::error::BACKUP_FILE_BASE64 secret is not set"
|
||||
exit 1
|
||||
}
|
||||
$backup = Join-Path $env:RUNNER_TEMP "test.backup"
|
||||
$bytes = [Convert]::FromBase64String($env:BACKUP_FILE_BASE64)
|
||||
[IO.File]::WriteAllBytes($backup, $bytes)
|
||||
"BACKUP_PATH=$backup" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: windows-build
|
||||
path: deploy/build/client
|
||||
|
||||
- name: Install Qt
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
version: ${{ env.QT_VERSION }}
|
||||
host: windows
|
||||
target: desktop
|
||||
arch: win64_msvc2022_64
|
||||
modules: qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets
|
||||
dir: ${{ runner.temp }}
|
||||
set-env: true
|
||||
|
||||
- name: Setup MSVC
|
||||
uses: ilammy/msvc-dev-cmd@v1
|
||||
with:
|
||||
arch: x64
|
||||
|
||||
- name: Download Xray core
|
||||
if: matrix.filter == 'XrayCoreAcceptsTest'
|
||||
shell: pwsh
|
||||
run: |
|
||||
$release = (Select-String -Path client/server_scripts/xray/Dockerfile -Pattern '^ARG XRAY_RELEASE="(.*)"').Matches[0].Groups[1].Value
|
||||
$zip = Join-Path $env:RUNNER_TEMP "xray.zip"
|
||||
$dir = Join-Path $env:RUNNER_TEMP "xray"
|
||||
Invoke-WebRequest -Uri "https://github.com/XTLS/Xray-core/releases/download/$release/Xray-windows-64.zip" -OutFile $zip
|
||||
Expand-Archive -Path $zip -DestinationPath $dir -Force
|
||||
"XRAY_BIN=$(Join-Path $dir 'xray.exe')" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
|
||||
|
||||
- name: Run tests
|
||||
env:
|
||||
AMNEZIA_TEST_API_KEY: ${{ secrets.AMNEZIA_TEST_API_KEY }}
|
||||
shell: cmd
|
||||
run: |
|
||||
rem Without an attached console (GitHub runner), QTest sends its output to
|
||||
rem OutputDebugString instead of stdout, so ctest captures nothing.
|
||||
set QT_FORCE_STDERR_LOGGING=1
|
||||
|
||||
set PATH=%PATH%;%CD%\client\3rd-prebuilt\deploy-prebuilt\windows\x64
|
||||
set PATH=%PATH%;%Qt6_DIR%\..\..\..\bin
|
||||
|
||||
ctest --test-dir deploy\build\client\tests -C Release -R "${{ matrix.filter }}" --output-on-failure -VV --timeout 600
|
||||
|
||||
# ------------------------------------------------------
|
||||
|
||||
test-macos:
|
||||
name: Test macOS (${{ matrix.filter }})
|
||||
needs: Build-MacOS
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
filter:
|
||||
- MultipleImportsTest
|
||||
- ServerEditTest
|
||||
- DefaultServerChangeTest
|
||||
- ServerEdgeCasesTest
|
||||
- SignalOrderTest
|
||||
- ServersModelSyncTest
|
||||
- ServerRenameTest
|
||||
- SettingsSignalsTest
|
||||
- UiServersModelAndControllerTest
|
||||
- XraySerializationTest
|
||||
- UiLanguageModelAndControllerTest
|
||||
- UiIpModelAndControllerTest
|
||||
- UiAppSTModelAndControllerTest
|
||||
- UiAllowedDnsModelAndControllerTest
|
||||
- BackupExportImportTest
|
||||
- FavoriteLocationsRestoreTest
|
||||
- ConnectionTest
|
||||
- OpenSSLLibraryTest
|
||||
- GatewayServiceImportTest
|
||||
|
||||
runs-on: macos-15
|
||||
|
||||
env:
|
||||
QT_VERSION: 6.10.1
|
||||
QTEST_FUNCTION_TIMEOUT: 7200000
|
||||
THIRD_PARTY_AWG_VPN_KEY: ${{ secrets.THIRD_PARTY_AWG_VPN_KEY }}
|
||||
THIRD_PARTY_WIRE_GUARD_VPN_KEY: ${{ secrets.THIRD_PARTY_WIRE_GUARD_VPN_KEY }}
|
||||
THIRD_PARTY_XRAY_VPN_KEY: ${{ secrets.THIRD_PARTY_XRAY_VPN_KEY }}
|
||||
THIRD_PARTY_OPEN_VPN_KEY: ${{ secrets.THIRD_PARTY_OPEN_VPN_KEY }}
|
||||
THIRD_PARTY_CLOAK_VPN_KEY: ${{ secrets.THIRD_PARTY_CLOAK_VPN_KEY }}
|
||||
THIRD_PARTY_SS_VPN_KEY: ${{ secrets.THIRD_PARTY_SS_VPN_KEY }}
|
||||
THIRD_PARTY_IKEV_VPN_KEY: ${{ secrets.THIRD_PARTY_IKEV_VPN_KEY }}
|
||||
THIRD_PARTY_PREMIUM_VPN_KEY: ${{ secrets.THIRD_PARTY_PREMIUM_VPN_KEY }}
|
||||
THIRD_PARTY_VMESS_NEW_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VMESS_NEW_IMPORT_DATA }}
|
||||
THIRD_PARTY_VMESS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VMESS_IMPORT_DATA }}
|
||||
THIRD_PARTY_TROJAN_IMPORT_DATA: ${{ secrets.THIRD_PARTY_TROJAN_IMPORT_DATA }}
|
||||
THIRD_PARTY_VLESS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VLESS_IMPORT_DATA }}
|
||||
THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA }}
|
||||
THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA }}
|
||||
|
||||
steps:
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: "26.3.0"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: 'true'
|
||||
fetch-depth: 10
|
||||
|
||||
- name: Decode backup file
|
||||
if: matrix.filter == 'BackupExportImportTest'
|
||||
shell: bash
|
||||
env:
|
||||
BACKUP_FILE_BASE64: ${{ secrets.BACKUP_FILE_BASE64 }}
|
||||
run: |
|
||||
if [ -z "$BACKUP_FILE_BASE64" ]; then
|
||||
echo "::error::BACKUP_FILE_BASE64 secret is not set"
|
||||
exit 1
|
||||
fi
|
||||
echo "$BACKUP_FILE_BASE64" | base64 --decode > "$RUNNER_TEMP/test.backup"
|
||||
echo "BACKUP_PATH=$RUNNER_TEMP/test.backup" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: macos-build
|
||||
path: deploy/build/client/tests
|
||||
|
||||
- name: Install Qt
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
version: ${{ env.QT_VERSION }}
|
||||
host: mac
|
||||
target: desktop
|
||||
arch: clang_64
|
||||
modules: qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets
|
||||
dir: ${{ runner.temp }}
|
||||
set-env: true
|
||||
|
||||
- name: Restore executable permissions
|
||||
run: |
|
||||
find deploy/build/client/tests \
|
||||
-type f \
|
||||
-name "test_*" \
|
||||
-exec chmod +x {} \;
|
||||
|
||||
- name: Run tests
|
||||
env:
|
||||
AMNEZIA_TEST_API_KEY: ${{ secrets.AMNEZIA_TEST_API_KEY }}
|
||||
shell: bash
|
||||
run: |
|
||||
QT_ROOT="$RUNNER_TEMP/Qt/$QT_VERSION/macos"
|
||||
|
||||
export DYLD_LIBRARY_PATH="$PWD/deploy/build/client/tests:$QT_ROOT/lib:${DYLD_LIBRARY_PATH:-}"
|
||||
|
||||
ctest --test-dir deploy/build/client/tests -R "${{ matrix.filter }}" --output-on-failure -VV --timeout 600
|
||||
|
||||
# ------------------------------------------------------
|
||||
|
||||
test-android:
|
||||
runs-on: android-runner
|
||||
|
||||
env:
|
||||
ANDROID_PLATFORM: android-28
|
||||
NDK_VERSION: 27.0.11718014
|
||||
QT_VERSION: 6.10.1
|
||||
QT_MODULES: 'qtremoteobjects qt5compat qtshadertools'
|
||||
|
||||
steps:
|
||||
- name: 'Get sources'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 10
|
||||
|
||||
- name: 'Decode backup file'
|
||||
run: |
|
||||
echo "${{ secrets.BACKUP_FILE_BASE64 }}" | base64 --decode > /tmp/test.backup
|
||||
echo "BACKUP_PATH=/tmp/test.backup" >> $GITHUB_ENV
|
||||
|
||||
|
||||
- name: 'Install desktop Qt'
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
version: ${{ env.QT_VERSION }}
|
||||
host: 'linux'
|
||||
target: 'desktop'
|
||||
arch: 'linux_gcc_64'
|
||||
modules: ${{ env.QT_MODULES }}
|
||||
dir: ${{ runner.temp }}
|
||||
py7zrversion: '==0.22.*'
|
||||
extra: '--base ${{ env.QT_MIRROR }}'
|
||||
|
||||
- name: 'Install android_x86_64 Qt'
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
version: ${{ env.QT_VERSION }}
|
||||
host: 'all_os'
|
||||
target: 'android'
|
||||
arch: 'android_x86_64'
|
||||
modules: ${{ env.QT_MODULES }}
|
||||
dir: ${{ runner.temp }}
|
||||
py7zrversion: '==0.22.*'
|
||||
extra: '--base ${{ env.QT_MIRROR }}'
|
||||
|
||||
- name: 'Setup Java'
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: 'Setup Android SDK'
|
||||
uses: android-actions/setup-android@v4
|
||||
with:
|
||||
packages: "platforms;${{ env.ANDROID_PLATFORM }} ndk;${{ env.NDK_VERSION }}"
|
||||
|
||||
- name: 'Setup python'
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.14'
|
||||
|
||||
- name: 'Install conan'
|
||||
run: pip install "conan==2.28.0"
|
||||
|
||||
# Uncomment to sign APK with release keystore (requires secrets):
|
||||
# - name: 'Decode keystore'
|
||||
# env:
|
||||
# KEYSTORE_BASE64: ${{ secrets.ANDROID_RELEASE_KEYSTORE_BASE64 }}
|
||||
# run: echo $KEYSTORE_BASE64 | base64 --decode > android.keystore
|
||||
|
||||
- name: 'Configure (Android x86_64)'
|
||||
env:
|
||||
QT_ANDROID: ${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/android_x86_64
|
||||
QT_HOST: ${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/gcc_64
|
||||
CMAKE_ANDROID_NDK: ${{ env.ANDROID_HOME }}/ndk/${{ env.NDK_VERSION }}
|
||||
run: |
|
||||
cmake -S . -B build -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DCMAKE_TOOLCHAIN_FILE=$QT_ANDROID/lib/cmake/Qt6/qt.toolchain.cmake \
|
||||
-DANDROID_PLATFORM=${{ env.ANDROID_PLATFORM }} \
|
||||
-DCMAKE_ANDROID_NDK=$CMAKE_ANDROID_NDK \
|
||||
-DQT_HOST_PATH=$QT_HOST \
|
||||
-DANDROID_SDK_ROOT=$ANDROID_HOME \
|
||||
-DAMNEZIA_BUILD_TESTS=ON
|
||||
|
||||
- name: 'Build Android tests'
|
||||
# env:
|
||||
# QT_ANDROID_KEYSTORE_PATH: ${{ github.workspace }}/android.keystore
|
||||
# QT_ANDROID_KEYSTORE_ALIAS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_ALIAS }}
|
||||
# QT_ANDROID_KEYSTORE_STORE_PASS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_PASS }}
|
||||
run: |
|
||||
cmake --build build --target test_android_openssl_make_apk
|
||||
cmake --build build --target test_android_restore_backup_make_apk
|
||||
|
||||
- name: 'Write Android test runner script'
|
||||
run: |
|
||||
cat > ci_run_android_tests.sh << 'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ADB=$(which adb)
|
||||
|
||||
echo ">>> Waiting for package manager to be ready..."
|
||||
"$ADB" shell 'while ! pm list packages > /dev/null 2>&1; do sleep 2; done'
|
||||
echo ">>> Package manager ready."
|
||||
|
||||
run_test() {
|
||||
local target="$1"
|
||||
local apk="build/client/tests/android-build/${target}.apk"
|
||||
local pkg_name="org.qtproject.example.${target}"
|
||||
local activity="${pkg_name}/org.qtproject.qt.android.bindings.QtActivity"
|
||||
|
||||
echo ">>> Installing ${target} APK..."
|
||||
timeout 120 "$ADB" install -r "$apk"
|
||||
|
||||
"$ADB" shell "run-as $pkg_name sh -c 'mkdir -p files && : > files/stdout.txt'" 2>/dev/null || true
|
||||
"$ADB" shell "run-as $pkg_name sh -c 'rm -f files/qtest_last_exit_code'" 2>/dev/null || true
|
||||
|
||||
echo ">>> Launching ${target}..."
|
||||
"$ADB" shell am start -n "$activity"
|
||||
|
||||
echo ">>> Waiting for test to finish (up to 120s)..."
|
||||
local elapsed=0 exit_code=""
|
||||
while [[ $elapsed -lt 120 ]]; do
|
||||
sleep 2
|
||||
elapsed=$((elapsed + 2))
|
||||
exit_code=$("$ADB" shell \
|
||||
"run-as $pkg_name cat files/qtest_last_exit_code 2>/dev/null" \
|
||||
| tr -d '[:space:]' || true)
|
||||
[[ -n "$exit_code" ]] && break
|
||||
done
|
||||
|
||||
local stdout_content
|
||||
stdout_content=$("$ADB" shell \
|
||||
"run-as $pkg_name cat files/stdout.txt 2>/dev/null" || true)
|
||||
if [[ -n "$stdout_content" ]]; then
|
||||
echo ">>> Test output:"
|
||||
echo "$stdout_content"
|
||||
fi
|
||||
|
||||
if [[ -z "$exit_code" ]]; then
|
||||
echo ">>> TIMEOUT: ${target} did not complete within 120s."
|
||||
exit 1
|
||||
elif [[ "$exit_code" != "0" ]]; then
|
||||
echo ">>> ${target} FAILED (exit code: $exit_code)"
|
||||
exit 1
|
||||
else
|
||||
echo ">>> ${target} PASSED"
|
||||
fi
|
||||
}
|
||||
|
||||
run_test test_android_openssl
|
||||
run_test test_android_restore_backup
|
||||
EOF
|
||||
chmod +x ci_run_android_tests.sh
|
||||
|
||||
- name: 'Run Android tests on emulator'
|
||||
uses: reactivecircus/android-emulator-runner@v2
|
||||
with:
|
||||
api-level: 28
|
||||
arch: x86_64
|
||||
emulator-options: -no-snapshot -no-window -no-audio -no-boot-anim -accel on
|
||||
disable-animations: true
|
||||
script: bash ci_run_android_tests.sh
|
||||
|
||||
# ------------------------------------------------------
|
||||
|
||||
Pre-Release:
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
needs: [Build-Linux-Ubuntu, Build-MacOS, Build-Android]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: 'Get release notes template'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .github/release-notes.md
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: 'Download Android APKs'
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: 'AmneziaVPN_*_{android11+,android9-10}_{arm64-v8a,armeabi-v7a,x86,x86_64}.apk'
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
|
||||
- name: 'Download desktop installers'
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: 'AmneziaVPN_*_{linux_x64.run,macos_x64.pkg}'
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
|
||||
- name: 'Publish pre-release'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
ls -l dist
|
||||
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
|
||||
gh release upload "$TAG" dist/* --repo "$GITHUB_REPOSITORY" --clobber
|
||||
else
|
||||
export VERSION="$TAG"
|
||||
export PREVIOUS_TAG=$(gh release view --repo "$GITHUB_REPOSITORY" --json tagName -q .tagName)
|
||||
envsubst '$VERSION $PREVIOUS_TAG' < .github/release-notes.md > release-notes.md
|
||||
|
||||
gh release create "$TAG" dist/* --repo "$GITHUB_REPOSITORY" \
|
||||
--title "$TAG" --notes-file release-notes.md --draft --prerelease --verify-tag
|
||||
fi
|
||||
@@ -146,3 +146,4 @@ macos-with-sign-build.sh
|
||||
DeveloperIdApplicationCertificate.p12
|
||||
DeveloperIdInstallerCertificate.p12
|
||||
|
||||
gwenv.sh
|
||||
@@ -0,0 +1,219 @@
|
||||
# AGENTS.md
|
||||
|
||||
Operating guide for this repository. Read it before changing anything.
|
||||
|
||||
## Project Overview
|
||||
|
||||
AmneziaVPN is a cross-platform VPN client built with C++ and Qt Quick (QML) for
|
||||
Windows, macOS, Linux, Android and iOS. It has three kinds of servers, each
|
||||
with its own config model (`serverConfigUtils::ConfigType`):
|
||||
|
||||
- **Self-hosted.** The admin connects the app to their own server over SSH; the
|
||||
app installs protocols as Docker containers (AmneziaWG, WireGuard, OpenVPN,
|
||||
XRay, IPsec and others) and generates client configs (`SelfHostedAdmin`).
|
||||
Access is shared with other users as `vpn://` keys, which import without
|
||||
server management rights (`SelfHostedUser`).
|
||||
- **Amnezia Premium and Free.** Configs come from the Amnezia gateway API. The
|
||||
gateway passes the backend's config through; the client does not invent
|
||||
server-side values.
|
||||
- **Native protocol configs.** Third-party configs imported as they are:
|
||||
WireGuard and AmneziaWG `.conf`, OpenVPN `.ovpn`, XRay and Shadowsocks links
|
||||
(`Native`). The app only connects with them; there is no server to manage.
|
||||
|
||||
A change to shared code (protocol configs, import, connection) must keep
|
||||
working for all three.
|
||||
|
||||
The desktop builds run a privileged service next to the GUI; the GUI talks to
|
||||
it over Qt Remote Objects. Android, iOS and the macOS App Store build
|
||||
(`MACOS_NE`) run the tunnel in the platform's VPN extension instead.
|
||||
|
||||
## Repository Layout
|
||||
|
||||
- `client/core/` — business logic with no UI: controllers, configurators
|
||||
(client config generation per protocol), installers (server-side setup over
|
||||
SSH), protocol and server config models, repositories (encrypted settings
|
||||
storage).
|
||||
- `client/ui/controllers/` — `*UiController` classes exposed to QML. Thin
|
||||
adapters over `core/`: they translate between QML and core types and hold no
|
||||
business logic of their own.
|
||||
- `client/ui/models/` — list and property models for QML.
|
||||
- `client/ui/qml/` — the interface:
|
||||
- `Pages2/` — screens, one file per page.
|
||||
- `Components/` — composite pieces of screens.
|
||||
- `Controls2/` — design system controls, named `<Name>Type.qml`; text styles
|
||||
live in `Controls2/TextTypes/`.
|
||||
- `Modules/Style/AmneziaStyle.qml` — design tokens (colours and the like).
|
||||
- `Config/` — `GlobalConfig`, `Dialogs` and other singletons.
|
||||
- `client/platforms/`, `client/android/`, `client/ios/`, `client/macos/` —
|
||||
platform code: VPN extensions, Kotlin, Swift, Objective-C++.
|
||||
- `client/daemon/`, `client/mozilla/`, `service/`, `ipc/` — the privileged
|
||||
service and its IPC.
|
||||
- `client/server_scripts/` — the shell scripts and Dockerfiles run on the
|
||||
user's server.
|
||||
- `client/translations/` — Qt `.ts` files.
|
||||
- `client/tests/` — Qt Test suites, run through `ctest`.
|
||||
- `recipes/` — Conan recipes for native dependencies (protocol libraries,
|
||||
OpenSSL, libssh and others).
|
||||
- `deploy/` — build and installer scripts. `.github/workflows/` — CI.
|
||||
- `client/3rd/` — vendored third-party code. Do not edit it.
|
||||
|
||||
## Essential Commands
|
||||
|
||||
Run from the repository root.
|
||||
|
||||
```bash
|
||||
deploy/build.sh -h # build options
|
||||
deploy/build.sh # build for the host platform
|
||||
deploy/build.sh -t android --aab # Android APK and AAB
|
||||
cmake -S . -B build -DCMAKE_PREFIX_PATH=<Qt>/<platform> -DAMNEZIA_BUILD_TESTS=ON
|
||||
cmake --build build --parallel
|
||||
ctest --test-dir build/client/tests --output-on-failure
|
||||
```
|
||||
|
||||
Qt 6.10 or later and Conan are required; native dependencies are built by Conan
|
||||
from `recipes/` on the first configure.
|
||||
|
||||
## Repository Rules
|
||||
|
||||
These three come first. Each one has been asked for repeatedly.
|
||||
|
||||
### 1. Clean, self-documenting code, no comments
|
||||
|
||||
- Names and structure carry the meaning. Code must be readable without help.
|
||||
- No explanatory comments in code: no `//` notes in QML or C++, no doc blocks.
|
||||
- The one exception is a hack that looks wrong on purpose: a workaround for a
|
||||
platform or Qt bug, an ordering that must not be "fixed". Then a short comment
|
||||
next to it says why.
|
||||
- Explanations of a change go into the chat summary or the commit message, not
|
||||
into the code.
|
||||
- Leave existing comments alone unless the change removes the code they
|
||||
describe.
|
||||
|
||||
### 2. Reusable code (DRY), without abstraction for its own sake
|
||||
|
||||
- **QML.** Every reusable component lives in its own file. A piece of a page
|
||||
goes into its own file in `Components/` even when it is used once, if that
|
||||
keeps the page short and readable. No inline `component` definitions or
|
||||
`Component` blocks inside pages. Before writing a new control, look in
|
||||
`Controls2/` and `Components/` for one that already does the job, and extend
|
||||
it if it almost does.
|
||||
- **C++.** Logic shared by several places is extracted once and reused:
|
||||
duplicated parsing, validation or config building is a bug waiting to diverge.
|
||||
- **No fanaticism.** Do not create a base class, interface, helper or file
|
||||
that only renames a single call, has one trivial user, or exists "for the
|
||||
future". Decompose when it makes the code shorter, clearer or removes
|
||||
duplication; otherwise keep it direct.
|
||||
|
||||
### 3. Do not add what was not asked for
|
||||
|
||||
- Implement exactly the request. No extra screens, buttons, settings,
|
||||
refactors, renames, translations or "while I'm here" fixes.
|
||||
- Ideas, improvements and problems noticed along the way are reported to the
|
||||
user as suggestions. Nothing is implemented without the user agreeing first.
|
||||
- When a request is ambiguous, ask instead of guessing big.
|
||||
- Keep the diff small. If a fix grows beyond the obvious change, say so and
|
||||
explain why before making it.
|
||||
|
||||
### Other rules
|
||||
|
||||
- **Committing is the user's call.** Prepare the change and a commit message;
|
||||
do not commit or push unless asked. Commit messages use a conventional prefix
|
||||
with a scope (`fix(ui): …`, `feat(macos): …`, `chore: …`): a one-line
|
||||
subject plus one or two short body lines saying what was wrong and what
|
||||
changed. Pull requests target `dev`.
|
||||
- **Format C++ with `.clang-format`.** Format the lines you touch; do not
|
||||
reformat files the change does not otherwise touch.
|
||||
- **Delete unused code** instead of keeping it "for later".
|
||||
- **Do not edit `client/3rd/`.** Fix vendored code through its upstream or a
|
||||
Conan recipe.
|
||||
|
||||
## UI And Design System
|
||||
|
||||
- **Figma is the source of truth.** Screens follow the "Amnezia APP VPN" file;
|
||||
components follow the "Amnezia DS Library". Where the two disagree, the app
|
||||
screens win.
|
||||
- **Build UI from the design system controls** in `Controls2/`
|
||||
(`ButtonType`, `ListItemType`, `TextFieldType`, `SheetType`,
|
||||
`NavigationBarType` and the others) and the text types in
|
||||
`Controls2/TextTypes/`. Do not style raw Qt Quick Controls on a page.
|
||||
- **No hex colours in QML.** Every colour is a semantic token from
|
||||
`AmneziaStyle.color`. A colour the tokens do not have is added to
|
||||
`AmneziaStyle` first, with the name the design system gives it.
|
||||
- **Icons come from Figma**, exported from the DS Library. Never draw an icon
|
||||
by hand. Exports include page background rectangles — strip them and keep
|
||||
the `0 0 24 24` viewBox.
|
||||
- **Restyling a component never redesigns a screen.** A design system update
|
||||
changes components; screen layout and behaviour stay unless the user asks.
|
||||
- **No Qt5Compat.GraphicalEffects.** It overflows the interpreted JS stack on
|
||||
iOS. Use `QtQuick.Effects` (`MultiEffect`), and create effects only while
|
||||
they are visible: each instance is expensive to create.
|
||||
|
||||
### QML pitfalls
|
||||
|
||||
- A vertical `ListView` resets the delegate's `x`. Put insets on the
|
||||
ListView's `leftMargin`/`rightMargin`, or on a child inside the delegate.
|
||||
- `lineHeightMode: Text.FixedHeight` puts all the extra leading below the text,
|
||||
while Figma splits it evenly. The text types already compensate with
|
||||
`halfLeading` padding; a new text type must do the same.
|
||||
- QML is loaded from `qml.qrc`, not compiled ahead of time: a QML error shows
|
||||
up only at runtime. When moving QML between files, compare the `import`
|
||||
lines — types registered from C++ under their own URIs fail only when the
|
||||
page is opened.
|
||||
|
||||
## Translations
|
||||
|
||||
- Every user-visible string goes through `qsTr()` in QML or `tr()` in C++.
|
||||
- The `.ts` files contain only finished, non-empty translations: no
|
||||
`vanished`, `obsolete` or `unfinished` entries, and every language holds the
|
||||
same set of strings. `lrelease` must report `N finished and 0 unfinished` for
|
||||
every file.
|
||||
- Refresh `.ts` files only when the task is about translations. `lupdate` run
|
||||
through `qt6_add_translations` misses QML, so run it over an explicit file
|
||||
list that skips `client/3rd` (from `client/translations`):
|
||||
|
||||
```bash
|
||||
find .. -path ../3rd -prune -o -path ../3rd-prebuilt -prune -o \
|
||||
\( -name '*.qml' -o -name '*.cpp' -o -name '*.h' -o -name '*.mm' \) -print \
|
||||
| sort > /tmp/src.lst
|
||||
<Qt>/bin/lupdate @/tmp/src.lst -ts amneziavpn_*.ts
|
||||
```
|
||||
|
||||
- `lupdate` output differs between macOS and Windows (keychain strings swap
|
||||
places), so a large `.ts` diff after a refresh is often only that.
|
||||
- Technical terms stay in English in every language: protocol parameter names
|
||||
and their descriptions (`Jc`, `S1`, `H1`…), config keys, transport and
|
||||
algorithm names. Ordinary UI words are translated.
|
||||
- Renaming a QML file changes its translation context, so its strings have to
|
||||
be carried over.
|
||||
|
||||
## Which Check When
|
||||
|
||||
- **Touched C++** — build the affected targets, and run `ctest` when the change
|
||||
touches anything the tests cover (servers, imports, configs, settings,
|
||||
split tunneling, XRay).
|
||||
- **Touched QML** — build, then open every page you changed. If the app cannot
|
||||
be launched, at least load the changed files through the QML engine so that
|
||||
syntax, imports and type errors surface, and say what was not checked.
|
||||
- **Touched a protocol config, import or connection path** — check self-hosted,
|
||||
Premium/Free and native configs, and every platform that consumes the field
|
||||
(desktop service, Android, iOS/macOS NE).
|
||||
- **Touched CMake, recipes or the app icon** — reconfigure from a clean build
|
||||
directory as well: branding paths are CMake cache variables and keep stale
|
||||
values in an existing build directory.
|
||||
- **Touched translations** — `lrelease` on every `.ts` file, see above.
|
||||
|
||||
## Privacy
|
||||
|
||||
- Never commit or repeat private keys, `vpn://` keys, server addresses,
|
||||
passwords or tokens — including ones found in pasted configs, logs or test
|
||||
output.
|
||||
- Logs must not contain secrets or full config bodies.
|
||||
- Local absolute paths (`/Users/...`, `C:\...`) do not belong in repository
|
||||
files.
|
||||
|
||||
## Verification Culture
|
||||
|
||||
- Verify your own work before reporting it. "Should work" is not a result;
|
||||
say exactly what was built, run or opened, and what was not.
|
||||
- Hypotheses are settled by an experiment, not by reasoning about the code.
|
||||
- After a wrong diagnosis, remove the code it produced.
|
||||
@@ -0,0 +1,6 @@
|
||||
# CLAUDE.md
|
||||
|
||||
The operating guide for this repository lives in [AGENTS.md](AGENTS.md), so
|
||||
every agent runtime reads the same file.
|
||||
|
||||
@AGENTS.md
|
||||
+4
-2
@@ -5,7 +5,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
set(PROJECT AmneziaVPN)
|
||||
if(NOT AMNEZIAVPN_VERSION)
|
||||
set(AMNEZIAVPN_VERSION 5.0.2.1 CACHE STRING "Client app version")
|
||||
set(AMNEZIAVPN_VERSION 5.1.1.0 CACHE STRING "Client app version")
|
||||
endif()
|
||||
|
||||
set(QT_CREATOR_SKIP_PACKAGE_MANAGER_SETUP ON CACHE BOOL "" FORCE)
|
||||
@@ -20,6 +20,8 @@ project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
|
||||
HOMEPAGE_URL "https://amnezia.org/"
|
||||
)
|
||||
|
||||
option(AMNEZIA_BUILD_TESTS "Build test executables" OFF)
|
||||
|
||||
# trigger conan to kick off `conan install` globally
|
||||
find_package(OpenSSL REQUIRED)
|
||||
if (PREBUILTS_ONLY)
|
||||
@@ -32,7 +34,7 @@ set(RELEASE_DATE "${CURRENT_DATE}")
|
||||
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
|
||||
|
||||
# bump by 2 on every release, because we're releasing two versions on the play store
|
||||
set(APP_ANDROID_VERSION_CODE 2161)
|
||||
set(APP_ANDROID_VERSION_CODE 2192)
|
||||
if(DEFINED APP_ANDROID_VERSION_CODE_OFFSET)
|
||||
math(EXPR APP_ANDROID_VERSION_CODE "${APP_ANDROID_VERSION_CODE} + ${APP_ANDROID_VERSION_CODE_OFFSET}")
|
||||
endif()
|
||||
|
||||
+23
-2
@@ -37,21 +37,30 @@ add_definitions(-DFREE_V2_ENDPOINT="$ENV{FREE_V2_ENDPOINT}")
|
||||
add_definitions(-DPREM_V1_ENDPOINT="$ENV{PREM_V1_ENDPOINT}")
|
||||
|
||||
if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
|
||||
set(PACKAGES ${PACKAGES} Widgets)
|
||||
set(PACKAGES ${PACKAGES} Widgets HttpServer)
|
||||
endif()
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS ${PACKAGES})
|
||||
find_package(OpenSSL REQUIRED)
|
||||
|
||||
find_package(libagw REQUIRED)
|
||||
|
||||
set(LIBS ${LIBS}
|
||||
Qt6::Core Qt6::Gui
|
||||
Qt6::Network Qt6::Xml Qt6::RemoteObjects
|
||||
Qt6::Quick Qt6::Svg Qt6::QuickControls2
|
||||
Qt6::Core5Compat Qt6::Concurrent
|
||||
amnezia::libagw
|
||||
)
|
||||
|
||||
find_package(Qt6 QUIET OPTIONAL_COMPONENTS CorePrivate)
|
||||
if(TARGET Qt6::CorePrivate)
|
||||
set(LIBS ${LIBS} Qt6::CorePrivate)
|
||||
add_compile_definitions(AMNEZIA_HAS_QZIPWRITER)
|
||||
endif()
|
||||
|
||||
if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
|
||||
set(LIBS ${LIBS} Qt6::Widgets)
|
||||
set(LIBS ${LIBS} Qt6::Widgets Qt6::HttpServer)
|
||||
endif()
|
||||
|
||||
qt_standard_project_setup()
|
||||
@@ -71,6 +80,7 @@ endif()
|
||||
qt6_add_resources(QRC ${QRC}
|
||||
${CMAKE_CURRENT_LIST_DIR}/images/images.qrc
|
||||
${CMAKE_CURRENT_LIST_DIR}/images/flagKit.qrc
|
||||
${CMAKE_CURRENT_LIST_DIR}/resources/countries.qrc
|
||||
${CMAKE_CURRENT_LIST_DIR}/client_scripts/clientScripts.qrc
|
||||
${CLIENT_QML_QRC_FILE}
|
||||
${CMAKE_CURRENT_LIST_DIR}/server_scripts/serverScripts.qrc
|
||||
@@ -92,6 +102,9 @@ else()
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_hi_IN.ts
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_es_ES.ts
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_ko_KR.ts
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_fr_FR.ts
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_th_TH.ts
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_id_ID.ts
|
||||
)
|
||||
endif()
|
||||
|
||||
@@ -214,6 +227,10 @@ if(ANDROID)
|
||||
include(cmake/android.cmake)
|
||||
endif()
|
||||
|
||||
if(APPLE)
|
||||
include(cmake/appleAppIcon.cmake)
|
||||
endif()
|
||||
|
||||
if(IOS)
|
||||
include(cmake/ios.cmake)
|
||||
include(cmake/ios-arch-fixup.cmake)
|
||||
@@ -313,3 +330,7 @@ if(OVPN_SCRIPTS)
|
||||
WORLD_READ WORLD_EXECUTE
|
||||
)
|
||||
endif()
|
||||
|
||||
if(AMNEZIA_BUILD_TESTS)
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
@@ -129,6 +129,11 @@ void AmneziaApplication::init()
|
||||
win->setPersistentSceneGraph(true);
|
||||
win->setPersistentGraphics(true);
|
||||
#endif
|
||||
#ifdef Q_OS_IOS
|
||||
auto pageController = m_coreController->pageController();
|
||||
pageController->setSafeAreaMargins(win->safeAreaMargins());
|
||||
QObject::connect(win, &QWindow::safeAreaMarginsChanged, pageController, &PageController::setSafeAreaMargins);
|
||||
#endif
|
||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
||||
win->show();
|
||||
#else
|
||||
@@ -179,7 +184,9 @@ void AmneziaApplication::init()
|
||||
|
||||
m_coreController->setQmlRoot();
|
||||
|
||||
#if CLIENT_ENABLE_APP_UPDATES
|
||||
m_coreController->checkForAppUpdates();
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_WIN //TODO
|
||||
if (m_parser.isSet(m_optAutostart))
|
||||
|
||||
@@ -54,6 +54,7 @@ android {
|
||||
storePassword = providers.environmentVariable("QT_ANDROID_KEYSTORE_STORE_PASS").orNull
|
||||
keyAlias = providers.environmentVariable("QT_ANDROID_KEYSTORE_ALIAS").orNull
|
||||
keyPassword = providers.environmentVariable("QT_ANDROID_KEYSTORE_STORE_PASS").orNull
|
||||
enableV3Signing = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +112,7 @@ android {
|
||||
isEnable = abiList.size > 1
|
||||
reset()
|
||||
include(*abiList.toTypedArray())
|
||||
isUniversalApk = false
|
||||
isUniversalApk = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +123,8 @@ android {
|
||||
outputs.map { it as BaseVariantOutputImpl }
|
||||
.forEach { output ->
|
||||
if (output.outputFileName.endsWith(".apk")) {
|
||||
output.outputFileName = "$outputBaseName-${buildType.name}.apk"
|
||||
output.outputFileName = listOfNotNull(outputBaseName, output.getFilter("ABI"), buildType.name)
|
||||
.joinToString("-", postfix = ".apk")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -143,14 +145,15 @@ android {
|
||||
val base = outputBaseName.ifEmpty { "android-build" }
|
||||
val unsignedSuffix = if (buildTypeName == "release") "-unsigned" else ""
|
||||
|
||||
val universalOutput = outputs.map { it as BaseVariantOutputImpl }.first { it.getFilter("ABI") == null }
|
||||
|
||||
packageApplicationProvider.configure {
|
||||
doLast {
|
||||
val srcDir = layout.buildDirectory.dir("outputs/apk/oss/$buildTypeName").get().asFile
|
||||
val dstDir = layout.buildDirectory.dir("outputs/apk").get().asFile
|
||||
dstDir.mkdirs()
|
||||
srcDir.listFiles()?.filter { it.name.endsWith(".apk") }?.forEach { apk ->
|
||||
apk.copyTo(File(dstDir, "$base-$buildTypeName$unsignedSuffix.apk"), overwrite = true)
|
||||
}
|
||||
File(srcDir, universalOutput.outputFileName)
|
||||
.copyTo(File(dstDir, "$base-$buildTypeName$unsignedSuffix.apk"), overwrite = true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="black">#FF0E0E11</color>
|
||||
<color name="black">#FF101012</color>
|
||||
<style name="NoActionBar">
|
||||
<item name="android:windowBackground">@color/black</item>
|
||||
<item name="android:colorBackground">@color/black</item>
|
||||
|
||||
@@ -37,6 +37,7 @@ import android.widget.Toast
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.OnApplyWindowInsetsListener
|
||||
import androidx.core.view.ViewCompat
|
||||
@@ -44,6 +45,7 @@ import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import kotlin.LazyThreadSafetyMode.NONE
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.text.RegexOption.IGNORE_CASE
|
||||
@@ -77,6 +79,7 @@ private const val CHECK_NOTIFICATION_PERMISSION_ACTION_CODE = 4
|
||||
private const val PREFS_NOTIFICATION_PERMISSION_ASKED = "NOTIFICATION_PERMISSION_ASKED"
|
||||
private const val OPEN_FILE_AFTER_RESUME_DELAY_MS = 400L
|
||||
private const val KEY_PENDING_OPEN_FILE_URI = "pending_open_file_uri"
|
||||
private const val FILE_PROVIDER_AUTHORITY = "org.amnezia.vpn.qtprovider"
|
||||
|
||||
class AmneziaActivity : QtActivity() {
|
||||
|
||||
@@ -733,28 +736,69 @@ class AmneziaActivity : QtActivity() {
|
||||
@Suppress("unused")
|
||||
fun saveFile(fileName: String, data: String) {
|
||||
Log.d(TAG, "Save file $fileName")
|
||||
createFile(fileName, "text/*") { os -> os.bufferedWriter().use { it.write(data) } }
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun saveFileBytes(fileName: String, data: ByteArray, mime: String) {
|
||||
Log.d(TAG, "Save file $fileName, ${data.size} bytes, $mime")
|
||||
createFile(fileName, mime) { os -> os.write(data) }
|
||||
}
|
||||
|
||||
private fun createFile(fileName: String, mime: String, write: (OutputStream) -> Unit) {
|
||||
mainScope.launch {
|
||||
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "text/*"
|
||||
type = mime
|
||||
putExtra(Intent.EXTRA_TITLE, fileName)
|
||||
}.also {
|
||||
try {
|
||||
startActivityForResult(it, CREATE_FILE_ACTION_CODE, ActivityResultHandler(
|
||||
onSuccess = {
|
||||
it?.data?.let { uri ->
|
||||
val uri = it?.data
|
||||
var saved = false
|
||||
if (uri != null) {
|
||||
Log.v(TAG, "Save file to $uri")
|
||||
try {
|
||||
contentResolver.openOutputStream(uri)?.use { os ->
|
||||
os.bufferedWriter().use { it.write(data) }
|
||||
write(os)
|
||||
saved = true
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Failed to save file $uri: $e")
|
||||
// todo: send error to Qt
|
||||
}
|
||||
}
|
||||
QtAndroidController.onFileSaved(saved, false)
|
||||
},
|
||||
onFail = {
|
||||
QtAndroidController.onFileSaved(false, true)
|
||||
}
|
||||
))
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
Toast.makeText(this@AmneziaActivity, "Unsupported", Toast.LENGTH_LONG).show()
|
||||
QtAndroidController.onFileSaved(false, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun shareFile(path: String, mime: String) {
|
||||
Log.d(TAG, "Share file $path, $mime")
|
||||
mainScope.launch {
|
||||
val uri = try {
|
||||
FileProvider.getUriForFile(this@AmneziaActivity, FILE_PROVIDER_AUTHORITY, File(path))
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log.e(TAG, "Failed to share file $path: $e")
|
||||
return@launch
|
||||
}
|
||||
Intent(Intent.ACTION_SEND).apply {
|
||||
type = mime
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}.also {
|
||||
try {
|
||||
startActivity(Intent.createChooser(it, null))
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
Toast.makeText(this@AmneziaActivity, "Unsupported", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
@@ -971,20 +1015,6 @@ class AmneziaActivity : QtActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun exportLogsFile(fileName: String) {
|
||||
Log.v(TAG, "Export logs file")
|
||||
saveFile(fileName, Log.getLogs())
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun clearLogs() {
|
||||
Log.v(TAG, "Clear logs")
|
||||
mainScope.launch {
|
||||
Log.clearLogs()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun setScreenshotsEnabled(enabled: Boolean) {
|
||||
Log.v(TAG, "Set screenshots enabled: $enabled")
|
||||
|
||||
@@ -22,6 +22,7 @@ object QtAndroidController {
|
||||
external fun onStatisticsUpdate(rxBytes: Long, txBytes: Long)
|
||||
|
||||
external fun onFileOpened(uri: String)
|
||||
external fun onFileSaved(success: Boolean, cancelled: Boolean)
|
||||
|
||||
external fun onConfigImported(data: String)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.amnezia.vpn.util
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Process
|
||||
@@ -22,9 +23,18 @@ import org.amnezia.vpn.util.Log.Priority.W
|
||||
import android.util.Log as NativeLog
|
||||
|
||||
private const val TAG = "Log"
|
||||
private const val LOG_FILE_NAME = "amneziaVPN.log"
|
||||
private const val ROTATE_LOG_FILE_NAME = "amneziaVPN.rotate.log"
|
||||
private const val APP_LOG_FILE_NAME = "app.log"
|
||||
private const val APP_ROTATE_LOG_FILE_NAME = "app.rotate.log"
|
||||
private const val TUNNEL_LOG_FILE_NAME = "tunnel.log"
|
||||
private const val TUNNEL_ROTATE_LOG_FILE_NAME = "tunnel.rotate.log"
|
||||
private const val LEGACY_LOG_FILE_NAME = "amneziaVPN.log"
|
||||
private const val LEGACY_ROTATE_LOG_FILE_NAME = "amneziaVPN.rotate.log"
|
||||
private const val LOCK_FILE_NAME = ".lock"
|
||||
private const val TILE_SERVICE_PROCESS_SUFFIX = ":amneziaTileService"
|
||||
private const val AWG_SERVICE_PROCESS_SUFFIX = ":amneziaAwgService"
|
||||
private const val AWG_LOGCAT_TAG_PREFIX = " AmneziaWG/"
|
||||
private const val STREAM_APP = 0
|
||||
private const val STREAM_TUNNEL = 1
|
||||
private const val DATE_TIME_PATTERN = "MM-dd HH:mm:ss.SSS"
|
||||
private const val PREFS_SAVE_LOGS_KEY = "SAVE_LOGS"
|
||||
private const val LOG_MAX_FILE_SIZE = 1024 * 1024
|
||||
@@ -42,12 +52,16 @@ object Log {
|
||||
private val dateTimeFormat: DateTimeFormatter = DateTimeFormatter.ofPattern(DATE_TIME_PATTERN)
|
||||
|
||||
private lateinit var logDir: File
|
||||
private val logFile: File by lazy { File(logDir, LOG_FILE_NAME) }
|
||||
private val rotateLogFile: File by lazy { File(logDir, ROTATE_LOG_FILE_NAME) }
|
||||
private lateinit var logFile: File
|
||||
private lateinit var rotateLogFile: File
|
||||
|
||||
private val fileLock: FileChannel by lazy { RandomAccessFile(File(logDir, LOCK_FILE_NAME).path, "rw").channel }
|
||||
private val threadLock: ReentrantLock by lazy { ReentrantLock() }
|
||||
|
||||
private var isAwgProcess: Boolean = false
|
||||
private val awgLogcatLock = Any()
|
||||
private var awgLogcatReader: AwgLogcatReader? = null
|
||||
|
||||
@Volatile
|
||||
private var _saveLogs: Boolean = false
|
||||
var saveLogs: Boolean
|
||||
@@ -60,6 +74,9 @@ object Log {
|
||||
}
|
||||
_saveLogs = value
|
||||
Prefs.save(PREFS_SAVE_LOGS_KEY, value)
|
||||
if (isAwgProcess) {
|
||||
if (value) startAwgLogcatReader() else stopAwgLogcatReader()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,21 +113,60 @@ object Log {
|
||||
fun init(context: Context) {
|
||||
v(TAG, "Init Log")
|
||||
logDir = File(context.cacheDir, "logs")
|
||||
val processName = Application.getProcessName()
|
||||
val stream = if (':' in processName && !processName.endsWith(TILE_SERVICE_PROCESS_SUFFIX)) {
|
||||
STREAM_TUNNEL
|
||||
} else {
|
||||
STREAM_APP
|
||||
}
|
||||
val (rotate, current) = streamFiles(stream)
|
||||
rotateLogFile = rotate
|
||||
logFile = current
|
||||
isAwgProcess = processName.endsWith(AWG_SERVICE_PROCESS_SUFFIX)
|
||||
saveLogs = Prefs.load(PREFS_SAVE_LOGS_KEY)
|
||||
}
|
||||
|
||||
fun getLogs(): String =
|
||||
"${deviceInfo()}\n${readLogs()}\nLOGCAT:\n${getLogcat()}"
|
||||
@JvmStatic
|
||||
fun getLogFiles(stream: Int): String {
|
||||
val files = streamFiles(stream).toMutableList()
|
||||
if (stream == STREAM_APP) files.addAll(0, legacyFiles().filter { it.exists() })
|
||||
return files.joinToString("\n") { it.absolutePath }
|
||||
}
|
||||
|
||||
fun clearLogs() {
|
||||
@JvmStatic
|
||||
fun getDeviceInfo(): String {
|
||||
val sb = StringBuilder()
|
||||
sb.append("Model: ").appendLine(Build.MODEL)
|
||||
sb.append("Brand: ").appendLine(Build.BRAND)
|
||||
sb.append("Product: ").appendLine(Build.PRODUCT)
|
||||
sb.append("Device: ").appendLine(Build.DEVICE)
|
||||
sb.append("Codename: ").appendLine(Build.VERSION.CODENAME)
|
||||
sb.append("Release: ").appendLine(Build.VERSION.RELEASE)
|
||||
sb.append("SDK: ").appendLine(Build.VERSION.SDK_INT)
|
||||
sb.append("ABI: ").appendLine(Build.SUPPORTED_ABIS.joinToString())
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun clearStream(stream: Int) {
|
||||
if (logDir.exists()) {
|
||||
withLock {
|
||||
logFile.delete()
|
||||
rotateLogFile.delete()
|
||||
streamFiles(stream).forEach { it.delete() }
|
||||
if (stream == STREAM_APP) legacyFiles().forEach { it.delete() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun streamFiles(stream: Int): List<File> =
|
||||
if (stream == STREAM_TUNNEL) {
|
||||
listOf(File(logDir, TUNNEL_ROTATE_LOG_FILE_NAME), File(logDir, TUNNEL_LOG_FILE_NAME))
|
||||
} else {
|
||||
listOf(File(logDir, APP_ROTATE_LOG_FILE_NAME), File(logDir, APP_LOG_FILE_NAME))
|
||||
}
|
||||
|
||||
private fun legacyFiles(): List<File> =
|
||||
listOf(File(logDir, LEGACY_ROTATE_LOG_FILE_NAME), File(logDir, LEGACY_LOG_FILE_NAME))
|
||||
|
||||
private fun log(tag: String, msg: String, priority: Priority) {
|
||||
if (saveLogs && priority != V) saveLogMsg(formatLogMsg(tag, msg, priority))
|
||||
|
||||
@@ -136,51 +192,85 @@ object Log {
|
||||
}
|
||||
}
|
||||
|
||||
private fun startAwgLogcatReader() {
|
||||
synchronized(awgLogcatLock) {
|
||||
if (awgLogcatReader == null) {
|
||||
awgLogcatReader = AwgLogcatReader().also { it.start() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopAwgLogcatReader() {
|
||||
synchronized(awgLogcatLock) {
|
||||
awgLogcatReader?.shutdown()
|
||||
awgLogcatReader = null
|
||||
}
|
||||
}
|
||||
|
||||
private class AwgLogcatReader : Thread("AwgLogcatReader") {
|
||||
private val lock = Any()
|
||||
private var process: java.lang.Process? = null
|
||||
@Volatile
|
||||
private var stopped = false
|
||||
|
||||
init {
|
||||
isDaemon = true
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
try {
|
||||
follow()
|
||||
} finally {
|
||||
synchronized(awgLogcatLock) {
|
||||
if (awgLogcatReader === this) awgLogcatReader = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun follow() {
|
||||
val logcat = try {
|
||||
ProcessBuilder(
|
||||
"logcat", "-v", "threadtime", "-v", "UTC", "--pid=${Process.myPid()}", "-T", "1"
|
||||
).redirectErrorStream(true).start()
|
||||
} catch (e: IOException) {
|
||||
NativeLog.e(TAG, "Failed to start logcat: $e")
|
||||
return
|
||||
}
|
||||
synchronized(lock) {
|
||||
if (stopped) {
|
||||
logcat.destroy()
|
||||
return
|
||||
}
|
||||
process = logcat
|
||||
}
|
||||
try {
|
||||
logcat.inputStream.bufferedReader().useLines { lines ->
|
||||
lines.filter { it.contains(AWG_LOGCAT_TAG_PREFIX) }.forEach {
|
||||
if (!stopped) saveLogMsg("$it\n")
|
||||
}
|
||||
}
|
||||
if (!stopped) NativeLog.w(TAG, "Logcat exited unexpectedly")
|
||||
} catch (e: IOException) {
|
||||
if (!stopped) NativeLog.e(TAG, "Failed to read logcat: $e")
|
||||
} finally {
|
||||
logcat.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
fun shutdown() {
|
||||
synchronized(lock) {
|
||||
stopped = true
|
||||
process?.destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatLogMsg(tag: String, msg: String, priority: Priority): String {
|
||||
val utcDate = ZonedDateTime.now(ZoneOffset.UTC).format(dateTimeFormat)
|
||||
return "${utcDate}Z ${Process.myPid()} ${Process.myTid()} $priority [${Thread.currentThread().name}] " +
|
||||
"$tag: $msg\n"
|
||||
}
|
||||
|
||||
private fun deviceInfo(): String {
|
||||
val sb = StringBuilder()
|
||||
sb.append("Model: ").appendLine(Build.MODEL)
|
||||
sb.append("Brand: ").appendLine(Build.BRAND)
|
||||
sb.append("Product: ").appendLine(Build.PRODUCT)
|
||||
sb.append("Device: ").appendLine(Build.DEVICE)
|
||||
sb.append("Codename: ").appendLine(Build.VERSION.CODENAME)
|
||||
sb.append("Release: ").appendLine(Build.VERSION.RELEASE)
|
||||
sb.append("SDK: ").appendLine(Build.VERSION.SDK_INT)
|
||||
sb.append("ABI: ").appendLine(Build.SUPPORTED_ABIS.joinToString())
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
private fun readLogs(): String {
|
||||
var logText = ""
|
||||
withLock {
|
||||
try {
|
||||
if (rotateLogFile.exists()) logText = rotateLogFile.readText()
|
||||
if (logFile.exists()) logText += logFile.readText()
|
||||
} catch (e: IOException) {
|
||||
val errorMsg = "Failed to read log: $e"
|
||||
NativeLog.e(TAG, errorMsg)
|
||||
logText += errorMsg
|
||||
}
|
||||
}
|
||||
return logText
|
||||
}
|
||||
|
||||
private fun getLogcat(): String {
|
||||
try {
|
||||
val process = ProcessBuilder("logcat", "-d").redirectErrorStream(true).start()
|
||||
return process.inputStream.reader().readText()
|
||||
} catch (e: IOException) {
|
||||
val errorMsg = "Failed to get logcat log: $e"
|
||||
NativeLog.e(TAG, errorMsg)
|
||||
return errorMsg
|
||||
}
|
||||
}
|
||||
|
||||
private fun withLock(block: () -> Unit) {
|
||||
threadLock.lock()
|
||||
try {
|
||||
|
||||
@@ -49,15 +49,23 @@ set(SOURCES ${SOURCES}
|
||||
|
||||
|
||||
find_package(awg-android REQUIRED)
|
||||
set(LIBS ${LIBS} amnezia::awg-android)
|
||||
set_property(TARGET ${PROJECT} APPEND PROPERTY QT_ANDROID_EXTRA_LIBS ${AMNEZIA_ANDROID_LIBWG_PATH} ${AMNEZIA_ANDROID_LIBWG_QUICK_PATH})
|
||||
set_property(TARGET ${PROJECT} APPEND PROPERTY QT_ANDROID_EXTRA_LIBS
|
||||
$<TARGET_FILE:amnezia::awg-android>
|
||||
${AMNEZIA_ANDROID_LIBWG_PATH}
|
||||
${AMNEZIA_ANDROID_LIBWG_QUICK_PATH}
|
||||
)
|
||||
|
||||
find_package(amnezia-libxray REQUIRED)
|
||||
file(COPY ${AMNEZIA_LIBXRAY_PATH} DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/android/xray/libXray)
|
||||
|
||||
find_package(openvpn-pt-android REQUIRED)
|
||||
set(LIBS ${LIBS} amnezia::openvpn-pt-android)
|
||||
set_property(TARGET ${PROJECT} APPEND PROPERTY QT_ANDROID_EXTRA_LIBS ${OPENVPN_PT_ANDROID_LIBCK_OVPN_PLUGIN_PATH})
|
||||
get_filename_component(OPENVPN_PT_ANDROID_LIB_DIR ${OPENVPN_PT_ANDROID_LIBCK_OVPN_PLUGIN_PATH} DIRECTORY)
|
||||
set_property(TARGET ${PROJECT} APPEND PROPERTY QT_ANDROID_EXTRA_LIBS
|
||||
${OPENVPN_PT_ANDROID_LIBCK_OVPN_PLUGIN_PATH}
|
||||
${OPENVPN_PT_ANDROID_LIB_DIR}/libovpn3.so
|
||||
${OPENVPN_PT_ANDROID_LIB_DIR}/libovpnutil.so
|
||||
${OPENVPN_PT_ANDROID_LIB_DIR}/librsapss.so
|
||||
)
|
||||
|
||||
set(APP_ANDROID_PACKAGE_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/android)
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
function(apple_icon_composer_supported OUT_VAR)
|
||||
set(${OUT_VAR} FALSE PARENT_SCOPE)
|
||||
|
||||
execute_process(
|
||||
COMMAND xcrun actool --version --output-format human-readable-text
|
||||
OUTPUT_VARIABLE ACTOOL_VERSION_OUTPUT
|
||||
ERROR_QUIET
|
||||
)
|
||||
string(REGEX MATCH "short-bundle-version: ([0-9]+)" ACTOOL_VERSION_MATCH "${ACTOOL_VERSION_OUTPUT}")
|
||||
if(NOT ACTOOL_VERSION_MATCH OR CMAKE_MATCH_1 LESS 26)
|
||||
return()
|
||||
endif()
|
||||
|
||||
execute_process(
|
||||
COMMAND sw_vers -productVersion
|
||||
OUTPUT_VARIABLE HOST_MACOS_VERSION
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
ERROR_QUIET
|
||||
)
|
||||
if(HOST_MACOS_VERSION VERSION_LESS 26)
|
||||
return()
|
||||
endif()
|
||||
|
||||
set(${OUT_VAR} TRUE PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
function(check_apple_app_icon ICON_PATH)
|
||||
if(NOT EXISTS "${ICON_PATH}/icon.json")
|
||||
message(FATAL_ERROR "App icon not found: ${ICON_PATH} (expected an Icon Composer .icon bundle)")
|
||||
endif()
|
||||
|
||||
apple_icon_composer_supported(ICON_COMPOSER_SUPPORTED)
|
||||
if(NOT ICON_COMPOSER_SUPPORTED)
|
||||
message(FATAL_ERROR "Xcode 26 and macOS 26 or later are required to build the app icon ${ICON_PATH}")
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
function(select_macos_app_icon ICON_PATH ICNS_PATH OUT_USE_ICON_COMPOSER)
|
||||
apple_icon_composer_supported(ICON_COMPOSER_SUPPORTED)
|
||||
if(ICON_COMPOSER_SUPPORTED)
|
||||
check_apple_app_icon(${ICON_PATH})
|
||||
set(${OUT_USE_ICON_COMPOSER} TRUE PARENT_SCOPE)
|
||||
return()
|
||||
endif()
|
||||
|
||||
if(NOT EXISTS "${ICNS_PATH}")
|
||||
message(FATAL_ERROR "Xcode 26 and macOS 26 are required for ${ICON_PATH}, and the fallback icon ${ICNS_PATH} was not found")
|
||||
endif()
|
||||
message(STATUS "Icon Composer needs Xcode 26 and macOS 26, using the fallback app icon ${ICNS_PATH}")
|
||||
set(${OUT_USE_ICON_COMPOSER} FALSE PARENT_SCOPE)
|
||||
endfunction()
|
||||
@@ -58,3 +58,7 @@ endif()
|
||||
if(NOT CLIENT_NOTIFICATION_IDENTIFIER)
|
||||
set(CLIENT_NOTIFICATION_IDENTIFIER "${CLIENT_TS_PREFIX}" CACHE STRING "Local notification request identifier")
|
||||
endif()
|
||||
|
||||
if(NOT DEFINED CLIENT_ENABLE_APP_UPDATES)
|
||||
set(CLIENT_ENABLE_APP_UPDATES 1)
|
||||
endif()
|
||||
@@ -19,8 +19,8 @@ endif()
|
||||
if(NOT CLIENT_IOS_LAUNCHSCREEN_PATH)
|
||||
set(CLIENT_IOS_LAUNCHSCREEN_PATH "${_CLIENT_SRC_DIR}/ios/app/${CLIENT_IOS_LAUNCHSCREEN_FILE}" CACHE FILEPATH "iOS launch screen storyboard path")
|
||||
endif()
|
||||
if(NOT CLIENT_IOS_MEDIA_ASSETS_PATH)
|
||||
set(CLIENT_IOS_MEDIA_ASSETS_PATH "${_CLIENT_SRC_DIR}/ios/app/Media.xcassets" CACHE FILEPATH "iOS app Media.xcassets path")
|
||||
if(NOT CLIENT_IOS_APP_ICON_PATH)
|
||||
set(CLIENT_IOS_APP_ICON_PATH "${_CLIENT_SRC_DIR}/images/AppIcon.icon" CACHE PATH "iOS app icon (Icon Composer .icon)")
|
||||
endif()
|
||||
|
||||
if(NOT CLIENT_IOS_BUNDLE_ID)
|
||||
|
||||
@@ -11,11 +11,11 @@ endif()
|
||||
if(NOT CLIENT_MACOS_NE_ENTITLEMENTS_PATH)
|
||||
set(CLIENT_MACOS_NE_ENTITLEMENTS_PATH "${_CLIENT_SRC_DIR}/macos/networkextension/${CLIENT_MACOS_NE_ENTITLEMENTS_FILE}" CACHE FILEPATH "macOS network extension entitlements file path")
|
||||
endif()
|
||||
if(NOT CLIENT_MACOS_MEDIA_ASSETS_PATH)
|
||||
set(CLIENT_MACOS_MEDIA_ASSETS_PATH "${_CLIENT_SRC_DIR}/macos/app/Images.xcassets" CACHE FILEPATH "macOS app Images.xcassets path")
|
||||
if(NOT CLIENT_MACOS_APP_ICON_PATH)
|
||||
set(CLIENT_MACOS_APP_ICON_PATH "${_CLIENT_SRC_DIR}/images/AppIcon.icon" CACHE PATH "macOS app icon (Icon Composer .icon)")
|
||||
endif()
|
||||
if(NOT CLIENT_MACOS_APP_ICNS_PATH)
|
||||
set(CLIENT_MACOS_APP_ICNS_PATH "${_CLIENT_SRC_DIR}/images/app.icns" CACHE FILEPATH "macOS app icon (.icns) used by non-App Store builds")
|
||||
set(CLIENT_MACOS_APP_ICNS_PATH "${_CLIENT_SRC_DIR}/macos/app/AppIcon.icns" CACHE FILEPATH "macOS app icon (.icns) for builds without Xcode 26 and macOS 26")
|
||||
endif()
|
||||
|
||||
if(NOT CLIENT_MACOS_EXTRA_PLIST_ENTRIES)
|
||||
|
||||
@@ -127,14 +127,18 @@ target_sources(${PROJECT} PRIVATE
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/StoreKit2Helper.swift
|
||||
)
|
||||
|
||||
check_apple_app_icon(${CLIENT_IOS_APP_ICON_PATH})
|
||||
|
||||
set_source_files_properties(
|
||||
${CLIENT_IOS_MEDIA_ASSETS_PATH}
|
||||
PROPERTIES MACOSX_PACKAGE_LOCATION Resources
|
||||
${CLIENT_IOS_APP_ICON_PATH}
|
||||
PROPERTIES
|
||||
MACOSX_PACKAGE_LOCATION Resources
|
||||
XCODE_LAST_KNOWN_FILE_TYPE folder.iconcomposer.icon
|
||||
)
|
||||
|
||||
target_sources(${PROJECT} PRIVATE
|
||||
${CLIENT_IOS_LAUNCHSCREEN_PATH}
|
||||
${CLIENT_IOS_MEDIA_ASSETS_PATH}
|
||||
${CLIENT_IOS_APP_ICON_PATH}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy
|
||||
)
|
||||
|
||||
|
||||
@@ -34,10 +34,42 @@ set(SOURCES ${SOURCES}
|
||||
|
||||
|
||||
|
||||
set(ICON_FILE ${CLIENT_MACOS_APP_ICNS_PATH})
|
||||
get_filename_component(MACOSX_BUNDLE_ICON_FILE "${CLIENT_MACOS_APP_ICNS_PATH}" NAME)
|
||||
set_source_files_properties(${ICON_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
|
||||
set(SOURCES ${SOURCES} ${ICON_FILE})
|
||||
select_macos_app_icon(${CLIENT_MACOS_APP_ICON_PATH} ${CLIENT_MACOS_APP_ICNS_PATH} USE_ICON_COMPOSER)
|
||||
|
||||
set(APP_ICON_OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/appicon)
|
||||
set(MACOSX_BUNDLE_ICON_FILE AppIcon)
|
||||
|
||||
if(USE_ICON_COMPOSER)
|
||||
set(APP_ICON_FILES ${APP_ICON_OUTPUT_DIR}/Assets.car ${APP_ICON_OUTPUT_DIR}/AppIcon.icns)
|
||||
file(GLOB_RECURSE APP_ICON_INPUTS CONFIGURE_DEPENDS "${CLIENT_MACOS_APP_ICON_PATH}/*")
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${APP_ICON_FILES}
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${APP_ICON_OUTPUT_DIR}
|
||||
COMMAND xcrun actool --output-format human-readable-text --warnings --errors
|
||||
--platform macosx
|
||||
--minimum-deployment-target ${CMAKE_OSX_DEPLOYMENT_TARGET}
|
||||
--app-icon AppIcon
|
||||
--output-partial-info-plist ${APP_ICON_OUTPUT_DIR}/partial.plist
|
||||
--compile ${APP_ICON_OUTPUT_DIR}
|
||||
${CLIENT_MACOS_APP_ICON_PATH}
|
||||
DEPENDS ${APP_ICON_INPUTS}
|
||||
COMMENT "Compiling macOS app icon"
|
||||
VERBATIM
|
||||
)
|
||||
set_source_files_properties(${APP_ICON_FILES} PROPERTIES GENERATED TRUE)
|
||||
|
||||
add_custom_command(TARGET ${PROJECT} POST_BUILD
|
||||
COMMAND plutil -replace CFBundleIconName -string AppIcon $<TARGET_BUNDLE_CONTENT_DIR:${PROJECT}>/Info.plist
|
||||
VERBATIM
|
||||
)
|
||||
else()
|
||||
set(APP_ICON_FILES ${APP_ICON_OUTPUT_DIR}/AppIcon.icns)
|
||||
configure_file(${CLIENT_MACOS_APP_ICNS_PATH} ${APP_ICON_FILES} COPYONLY)
|
||||
endif()
|
||||
|
||||
set_source_files_properties(${APP_ICON_FILES} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
|
||||
set(SOURCES ${SOURCES} ${APP_ICON_FILES})
|
||||
|
||||
target_compile_options(${PROJECT} PRIVATE
|
||||
-DGROUP_ID=\"${BUILD_IOS_GROUP_IDENTIFIER}\"
|
||||
|
||||
@@ -122,13 +122,24 @@ target_sources(${PROJECT} PRIVATE
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/StoreKit2Helper.swift
|
||||
)
|
||||
|
||||
set_source_files_properties(
|
||||
${CLIENT_MACOS_MEDIA_ASSETS_PATH}
|
||||
PROPERTIES MACOSX_PACKAGE_LOCATION Resources
|
||||
)
|
||||
select_macos_app_icon(${CLIENT_MACOS_APP_ICON_PATH} ${CLIENT_MACOS_APP_ICNS_PATH} USE_ICON_COMPOSER)
|
||||
|
||||
if(USE_ICON_COMPOSER)
|
||||
set(APP_ICON_SOURCE ${CLIENT_MACOS_APP_ICON_PATH})
|
||||
set_source_files_properties(
|
||||
${APP_ICON_SOURCE}
|
||||
PROPERTIES
|
||||
MACOSX_PACKAGE_LOCATION Resources
|
||||
XCODE_LAST_KNOWN_FILE_TYPE folder.iconcomposer.icon
|
||||
)
|
||||
else()
|
||||
set(APP_ICON_SOURCE ${CMAKE_CURRENT_BINARY_DIR}/appicon/AppIcon.icns)
|
||||
configure_file(${CLIENT_MACOS_APP_ICNS_PATH} ${APP_ICON_SOURCE} COPYONLY)
|
||||
set_source_files_properties(${APP_ICON_SOURCE} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
|
||||
endif()
|
||||
|
||||
target_sources(${PROJECT} PRIVATE
|
||||
${CLIENT_MACOS_MEDIA_ASSETS_PATH}
|
||||
${APP_ICON_SOURCE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy
|
||||
)
|
||||
|
||||
|
||||
@@ -40,64 +40,6 @@ function(osx_bundle_files TARGET)
|
||||
endforeach()
|
||||
endfunction(osx_bundle_files)
|
||||
|
||||
## A helper to bundle an asset catalog.
|
||||
function(osx_bundle_assetcatalog TARGET)
|
||||
cmake_parse_arguments(XCASSETS
|
||||
""
|
||||
"CATALOG;PLATFORM"
|
||||
"DEVICES"
|
||||
${ARGN})
|
||||
|
||||
if(XCASSETS_PLATFORM)
|
||||
set(XCASSETS_TARGET_ARGS --platform ${XCASSETS_PLATFORM})
|
||||
elseif(IOS)
|
||||
set(XCASSETS_TARGET_ARGS --platform iphoneos)
|
||||
else()
|
||||
set(XCASSETS_TARGET_ARGS --platform macosx)
|
||||
endif()
|
||||
foreach(DEVNAME ${XCASSETS_DEVICES})
|
||||
list(APPEND XCASSETS_TARGET_ARGS --target-device ${DEVNAME})
|
||||
endforeach()
|
||||
list(APPEND XCASSETS_TARGET_ARGS --minimum-deployment-target ${CMAKE_OSX_DEPLOYMENT_TARGET})
|
||||
|
||||
## Compile the asset catalog
|
||||
set(XCASSETS_GEN_PLIST ${CMAKE_CURRENT_BINARY_DIR}/xcassets_generated_info.plist)
|
||||
add_custom_command(
|
||||
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/xcassets/Assets.car ${XCASSETS_GEN_PLIST}
|
||||
MAIN_DEPENDENCY ${XCASSETS_CATALOG}/Contents.json
|
||||
COMMENT "Building asset catalog"
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/xcassets
|
||||
COMMAND actool --output-format human-readable-text --notices --warnings
|
||||
${XCASSETS_TARGET_ARGS}
|
||||
--app-icon AppIcon
|
||||
--output-partial-info-plist ${XCASSETS_GEN_PLIST}
|
||||
--development-region en --enable-on-demand-resources NO
|
||||
--compile ${CMAKE_CURRENT_BINARY_DIR}/xcassets ${XCASSETS_CATALOG}
|
||||
)
|
||||
|
||||
## Patch the asset catalog into the target bundle.
|
||||
if(NOT IOS)
|
||||
set(XCASSETS_RESOURCE_DIR "Resources")
|
||||
endif()
|
||||
add_custom_command(TARGET ${TARGET} POST_BUILD
|
||||
COMMENT "Bundling asset catalog"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_BINARY_DIR}/xcassets $<TARGET_BUNDLE_CONTENT_DIR:${TARGET}>/${XCASSETS_RESOURCE_DIR}
|
||||
COMMAND ${CLIENT_ROOT_DIR}/scripts/macos/merge_plist.py ${XCASSETS_GEN_PLIST} -o $<TARGET_BUNDLE_CONTENT_DIR:${TARGET}>/Info.plist
|
||||
)
|
||||
|
||||
target_sources(${TARGET} PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/xcassets/Assets.car)
|
||||
set_source_files_properties(
|
||||
${CMAKE_CURRENT_BINARY_DIR}/xcassets/Assets.car
|
||||
${XCASSETS_GEN_PLIST}
|
||||
PROPERTIES
|
||||
GENERATED TRUE
|
||||
HEADER_FILE_ONLY TRUE
|
||||
)
|
||||
|
||||
target_sources(${TARGET} PRIVATE ${XCASSETS_GEN_PLIST})
|
||||
set_source_files_properties(${XCASSETS_GEN_PLIST} PROPERTIES GENERATED TRUE)
|
||||
endfunction()
|
||||
|
||||
## A helper to code-sign an executable.
|
||||
function(osx_codesign_target TARGET)
|
||||
## Xcode should perform automatic code-signing for us.
|
||||
|
||||
@@ -44,6 +44,8 @@ set(HEADERS ${HEADERS}
|
||||
${CLIENT_ROOT_DIR}/core/controllers/selfhosted/exportController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/connectionController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/settingsController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/localProxyController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/logsController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/servicesCatalogController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/storePurchaseController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/subscriptionController.h
|
||||
@@ -65,9 +67,12 @@ set(HEADERS ${HEADERS}
|
||||
${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/api/countryCatalog.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/api/gatewayPayloadBuilder.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/localProxy/localProxyConstants.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/localProxy/portUtils.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/localProxy/xrayConfigUtils.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/osSignalHandler.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/swiftBridge.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/utilities.h
|
||||
@@ -129,6 +134,8 @@ set(SOURCES ${SOURCES}
|
||||
${CLIENT_ROOT_DIR}/core/controllers/selfhosted/exportController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/connectionController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/settingsController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/localProxyController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/logsController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/servicesCatalogController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/storePurchaseController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/subscriptionController.cpp
|
||||
@@ -152,9 +159,11 @@ set(SOURCES ${SOURCES}
|
||||
${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/api/countryCatalog.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/api/gatewayPayloadBuilder.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/localProxy/portUtils.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/localProxy/xrayConfigUtils.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/serverConfigUtils.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/osSignalHandler.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/utilities.cpp
|
||||
|
||||
@@ -66,6 +66,8 @@ ProtocolConfig AwgConfigurator::createConfig(const ServerCredentials &credential
|
||||
newClientConfig.nativeConfig = awgConfig;
|
||||
newClientConfig.hostName = wgConfig->clientConfig->hostName;
|
||||
newClientConfig.port = wgConfig->clientConfig->port;
|
||||
newClientConfig.cascadeEndpoint = wgConfig->clientConfig->cascadeEndpoint;
|
||||
newClientConfig.cascadePort = wgConfig->clientConfig->cascadePort;
|
||||
newClientConfig.clientIp = wgConfig->clientConfig->clientIp;
|
||||
newClientConfig.clientPrivateKey = wgConfig->clientConfig->clientPrivateKey;
|
||||
newClientConfig.clientPublicKey = wgConfig->clientConfig->clientPublicKey;
|
||||
|
||||
@@ -266,6 +266,11 @@ ProtocolConfig WireguardConfigurator::createConfig(const ServerCredentials &cred
|
||||
clientConfig.nativeConfig = config;
|
||||
clientConfig.hostName = connData.host;
|
||||
clientConfig.port = connData.port.toInt();
|
||||
if (wireguardServerConfig) {
|
||||
clientConfig.setCascade(wireguardServerConfig->cascadeEndpoint, wireguardServerConfig->cascadePort);
|
||||
} else if (awgServerConfig) {
|
||||
clientConfig.setCascade(awgServerConfig->cascadeEndpoint, awgServerConfig->cascadePort);
|
||||
}
|
||||
clientConfig.clientIp = connData.clientIP;
|
||||
clientConfig.clientPrivateKey = connData.clientPrivKey;
|
||||
clientConfig.clientPublicKey = connData.clientPubKey;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QDateTime>
|
||||
#include <QThread>
|
||||
#include <QUuid>
|
||||
#include "logger.h"
|
||||
@@ -23,124 +24,49 @@
|
||||
namespace {
|
||||
Logger logger("XrayConfigurator");
|
||||
|
||||
QString normalizeXhttpMode(const QString &m) {
|
||||
const QString t = m.trimmed();
|
||||
if (t.isEmpty() || t.compare(QLatin1String("Auto"), Qt::CaseInsensitive) == 0) {
|
||||
return QStringLiteral("auto");
|
||||
}
|
||||
if (t.compare(QLatin1String("Packet-up"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("packet-up");
|
||||
if (t.compare(QLatin1String("Stream-up"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("stream-up");
|
||||
if (t.compare(QLatin1String("Stream-one"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("stream-one");
|
||||
return t.toLower();
|
||||
}
|
||||
|
||||
// Xray-core: empty → path; "None" in UI → omit (core default path)
|
||||
QString normalizeSessionSeqPlacement(const QString &p)
|
||||
QString effectiveServerFlow(const amnezia::XrayServerConfig &srv)
|
||||
{
|
||||
if (p.isEmpty() || p.compare(QLatin1String("None"), Qt::CaseInsensitive) == 0)
|
||||
return {};
|
||||
return p.toLower();
|
||||
}
|
||||
|
||||
QString normalizeUplinkDataPlacement(const QString &p)
|
||||
{
|
||||
if (p.isEmpty() || p.compare(QLatin1String("Body"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("body");
|
||||
if (p.compare(QLatin1String("Auto"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("auto");
|
||||
if (p.compare(QLatin1String("Query"), Qt::CaseInsensitive) == 0)
|
||||
// "Query" is not valid for uplink payload in splithttp; closest documented mode
|
||||
return QStringLiteral("header");
|
||||
return p.toLower();
|
||||
}
|
||||
|
||||
// splithttp: cookie | header | query | queryInHeader (not "body")
|
||||
QString normalizeXPaddingPlacement(const QString &p)
|
||||
{
|
||||
QString t = p.trimmed();
|
||||
if (t.isEmpty())
|
||||
return QString::fromLatin1(amnezia::protocols::xray::defaultXPaddingPlacement).toLower();
|
||||
if (t.compare(QLatin1String("Body"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("queryInHeader");
|
||||
if (t.contains(QLatin1String("queryInHeader"), Qt::CaseInsensitive)
|
||||
|| t.compare(QLatin1String("Query in header"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("queryInHeader");
|
||||
return t.toLower();
|
||||
}
|
||||
|
||||
// splithttp: repeat-x | tokenish
|
||||
QString normalizeXPaddingMethod(const QString &m)
|
||||
{
|
||||
QString t = m.trimmed();
|
||||
if (t.isEmpty() || t.compare(QLatin1String("Repeat-x"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("repeat-x");
|
||||
if (t.compare(QLatin1String("Tokenish"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("tokenish");
|
||||
if (t.compare(QLatin1String("Random"), Qt::CaseInsensitive) == 0
|
||||
|| t.compare(QLatin1String("Zero"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("repeat-x");
|
||||
return t.toLower();
|
||||
}
|
||||
|
||||
// xray wants int ranges as "from-to" string, not a {from,to} object.
|
||||
QString makeRangeString(const QString &minV, const QString &maxV)
|
||||
{
|
||||
return minV + QLatin1Char('-') + maxV;
|
||||
}
|
||||
|
||||
void putIntRangeIfAny(QJsonObject &obj, const char *key, QString minV, QString maxV, const char *fallbackMin,
|
||||
const char *fallbackMax)
|
||||
{
|
||||
if (minV.isEmpty() && maxV.isEmpty())
|
||||
return;
|
||||
if (minV.isEmpty())
|
||||
minV = QString::fromLatin1(fallbackMin);
|
||||
if (maxV.isEmpty())
|
||||
maxV = QString::fromLatin1(fallbackMax);
|
||||
obj[QString::fromUtf8(key)] = makeRangeString(minV, maxV);
|
||||
}
|
||||
|
||||
QString effectiveClientFlow(const amnezia::XrayServerConfig &srv)
|
||||
{
|
||||
const bool rawTransport = srv.transport.isEmpty() || srv.transport == QLatin1String("raw");
|
||||
const bool secureFlow =
|
||||
srv.security == QLatin1String("tls") || srv.security == QLatin1String("reality");
|
||||
return (rawTransport && secureFlow) ? srv.flow : QString();
|
||||
return amnezia::xrayEffective::serverFlow(srv);
|
||||
}
|
||||
|
||||
QString effectiveSecurity(const amnezia::XrayServerConfig &srv)
|
||||
{
|
||||
if (srv.transport == QLatin1String("mkcp") && srv.security == QLatin1String("reality")) {
|
||||
return QStringLiteral("none");
|
||||
}
|
||||
return srv.security;
|
||||
return amnezia::xrayEffective::security(srv);
|
||||
}
|
||||
|
||||
// Desktop applies this in XrayProtocol::start(); iOS/Android pass JSON straight to libxray — same fixes here.
|
||||
void sanitizeXrayNativeConfig(amnezia::ProtocolConfig &pc)
|
||||
{
|
||||
QString c = pc.nativeConfig();
|
||||
if (c.isEmpty()) {
|
||||
return;
|
||||
const QString before = pc.nativeConfig();
|
||||
const QString after = amnezia::sanitizeNativeConfig(before);
|
||||
if (after != before) {
|
||||
pc.setNativeConfig(after);
|
||||
}
|
||||
bool changed = false;
|
||||
if (c.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) {
|
||||
c.replace(QLatin1String("Mozilla/5.0"), QString::fromLatin1(amnezia::protocols::xray::defaultFingerprint),
|
||||
Qt::CaseInsensitive);
|
||||
changed = true;
|
||||
}
|
||||
const QString legacyListen = QString::fromLatin1(amnezia::protocols::xray::defaultLocalAddr);
|
||||
const QString listenOk = QString::fromLatin1(amnezia::protocols::xray::defaultLocalListenAddr);
|
||||
if (c.contains(legacyListen)) {
|
||||
c.replace(legacyListen, listenOk);
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
pc.setNativeConfig(c);
|
||||
}
|
||||
|
||||
QString tlsFingerprintFromEnsureOutput(const QString &stdOut)
|
||||
{
|
||||
const QString prefix = QStringLiteral("amnezia_tls_fp=");
|
||||
const auto lines = stdOut.split(QLatin1Char('\n'));
|
||||
for (QString line : lines) {
|
||||
line = line.trimmed();
|
||||
if (!line.startsWith(prefix))
|
||||
continue;
|
||||
QString fp = line.mid(prefix.size());
|
||||
fp.remove(QLatin1Char(':'));
|
||||
fp.remove(QLatin1Char(' '));
|
||||
fp = fp.toLower();
|
||||
if (fp.size() != 64)
|
||||
return {};
|
||||
for (const QChar ch : fp) {
|
||||
const ushort u = ch.unicode();
|
||||
const bool hex = (u >= '0' && u <= '9') || (u >= 'a' && u <= 'f');
|
||||
if (!hex)
|
||||
return {};
|
||||
}
|
||||
return fp;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -157,30 +83,6 @@ 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
|
||||
{
|
||||
@@ -196,12 +98,17 @@ ErrorCode XrayConfigurator::readRealityKeyFiles(const DockerContainer container,
|
||||
if (fileError == ErrorCode::NoError && !out.isEmpty()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
if (fileError != ErrorCode::NoError) {
|
||||
logger.error() << "Xray readRealityKeyFiles: ssh failed path=" << path
|
||||
<< "error=" << static_cast<int>(fileError);
|
||||
return fileError;
|
||||
}
|
||||
if (attempt < 2) {
|
||||
QThread::msleep(500);
|
||||
}
|
||||
}
|
||||
logger.error() << "Xray readRealityKeyFiles: failed path=" << path;
|
||||
return ErrorCode::XrayRealityKeysReadFailed;
|
||||
logger.error() << "Xray readRealityKeyFiles: key file empty path=" << path;
|
||||
return ErrorCode::XrayKeysReadFailed;
|
||||
};
|
||||
|
||||
ErrorCode errorCode = readKeyFile(QString::fromLatin1(amnezia::protocols::xray::PublicKeyPath), outPublicKey);
|
||||
@@ -232,11 +139,7 @@ ErrorCode XrayConfigurator::applyServerSettingsToRemote(const ServerCredentials
|
||||
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 = effectiveClientFlow(srv);
|
||||
const QString flowValue = effectiveServerFlow(srv);
|
||||
QString realityPublicKey;
|
||||
QString realityShortId;
|
||||
if (effectiveSecurity(srv) == QLatin1String("reality")) {
|
||||
@@ -264,71 +167,59 @@ ErrorCode XrayConfigurator::applyServerSettingsToRemote(const ServerCredentials
|
||||
}
|
||||
|
||||
QJsonObject serverConfig = doc.object();
|
||||
if (!serverConfig.contains(amnezia::protocols::xray::inbounds)) {
|
||||
logger.error() << "Server config missing 'inbounds' field";
|
||||
return ErrorCode::XrayServerConfigInvalid;
|
||||
}
|
||||
const QJsonObject originalConfig = serverConfig;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
QJsonArray clients = XrayServerConfig::clientsFromServerInboundJson(serverConfig);
|
||||
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;
|
||||
const bool adoptingOwnAccount = !(xrayCfg->hasClientConfig() && !xrayCfg->clientConfig->id.isEmpty());
|
||||
|
||||
QString ownClientId;
|
||||
if (readContainerKeyFile(container, credentials, QString::fromLatin1(amnezia::protocols::xray::uuidPath),
|
||||
ownClientId)
|
||||
!= ErrorCode::NoError) {
|
||||
ownClientId.clear();
|
||||
}
|
||||
|
||||
const int existingIndex =
|
||||
adoptingOwnAccount ? XrayServerConfig::indexOfClient(clients, ownClientId) : -1;
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
clientId = ownClientId;
|
||||
clients[existingIndex] =
|
||||
XrayServerConfig::applyFlowToClient(clients[existingIndex].toObject(), flowValue);
|
||||
} else {
|
||||
const bool takeOverOwnAccount = adoptingOwnAccount && !ownClientId.isEmpty();
|
||||
clientId = takeOverOwnAccount ? ownClientId : QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
clients.append(XrayServerConfig::makeClientEntry(clientId, flowValue));
|
||||
}
|
||||
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();
|
||||
clientId = XrayServerConfig::firstClientId(clients);
|
||||
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;
|
||||
clients = XrayServerConfig::applyFlowToClients(clients, flowValue);
|
||||
}
|
||||
|
||||
settings[amnezia::protocols::xray::clients] = clients;
|
||||
inbound[amnezia::protocols::xray::settings] = settings;
|
||||
inbounds[0] = inbound;
|
||||
serverConfig[amnezia::protocols::xray::inbounds] = inbounds;
|
||||
const XrayServerJsonStatus writeStatus =
|
||||
XrayServerConfig::setClientsInServerInboundJson(serverConfig, clients);
|
||||
if (writeStatus != XrayServerJsonStatus::Ok) {
|
||||
return ErrorCode::XrayServerConfigInvalid;
|
||||
}
|
||||
|
||||
errorCode = uploadServerConfigJson(credentials, container, dnsSettings, serverConfig);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Xray applyServerSettings: upload/restart failed, error=" << static_cast<int>(errorCode);
|
||||
return errorCode;
|
||||
const QString listenPort = srv.port.isEmpty() ? QString::fromLatin1(amnezia::protocols::xray::defaultPort)
|
||||
: srv.port;
|
||||
if (serverConfig != originalConfig) {
|
||||
errorCode = uploadServerConfigAtomically(credentials, container, listenPort, serverConfig);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
logger.info() << "Xray applyServerSettings: server config uploaded and container restarted";
|
||||
|
||||
@@ -337,7 +228,8 @@ ErrorCode XrayConfigurator::applyServerSettingsToRemote(const ServerCredentials
|
||||
}
|
||||
|
||||
XrayProtocolConfig updated =
|
||||
buildClientProtocolConfig(credentials, container, srv, clientId, errorCode, realityPublicKey, realityShortId);
|
||||
buildClientProtocolConfig(credentials, container, srv, clientId, errorCode,
|
||||
realityPublicKey, realityShortId);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Xray applyServerSettings: buildClientProtocolConfig failed, error="
|
||||
<< static_cast<int>(errorCode);
|
||||
@@ -360,19 +252,24 @@ ErrorCode XrayConfigurator::readContainerKeyFile(DockerContainer container, cons
|
||||
if (fileError == ErrorCode::NoError && !out.isEmpty()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
if (fileError != ErrorCode::NoError) {
|
||||
logger.error() << "Xray readContainerKeyFile: ssh failed path=" << path
|
||||
<< "error=" << static_cast<int>(fileError);
|
||||
return fileError;
|
||||
}
|
||||
if (attempt < 2) {
|
||||
QThread::msleep(500);
|
||||
}
|
||||
}
|
||||
logger.error() << "Xray readContainerKeyFile: failed path=" << path;
|
||||
return ErrorCode::XrayRealityKeysReadFailed;
|
||||
logger.error() << "Xray readContainerKeyFile: key file empty path=" << path;
|
||||
return ErrorCode::XrayKeysReadFailed;
|
||||
}
|
||||
|
||||
ErrorCode XrayConfigurator::writeServerConfigForSetup(const ServerCredentials &credentials, DockerContainer container,
|
||||
ContainerConfig &containerConfig, const DnsSettings &dnsSettings)
|
||||
ContainerConfig &containerConfig, const DnsSettings &dnsSettings,
|
||||
bool useAtomicApply)
|
||||
{
|
||||
Q_UNUSED(dnsSettings);
|
||||
namespace px = amnezia::protocols::xray;
|
||||
|
||||
const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>();
|
||||
if (!xrayCfg) {
|
||||
@@ -385,82 +282,72 @@ ErrorCode XrayConfigurator::writeServerConfigForSetup(const ServerCredentials &c
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
logger.info() << "Xray writeServerConfigForSetup: start container=" << static_cast<int>(container)
|
||||
<< "transport=" << srv.transport << "security=" << srv.security << "port=" << srv.port;
|
||||
|
||||
ErrorCode errorCode = ErrorCode::NoError;
|
||||
|
||||
QString clientId;
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(px::uuidPath), clientId);
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(amnezia::protocols::xray::uuidPath), clientId);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
const QString securityEff = effectiveSecurity(srv);
|
||||
QString tlsPin;
|
||||
if (securityEff == QLatin1String("tls")) {
|
||||
errorCode = ensureTlsCertificate(credentials, container, tlsPin);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
|
||||
QString realityPrivateKey;
|
||||
QString realityPublicKey;
|
||||
QString realityShortId;
|
||||
if (securityEff == QLatin1String("reality")) {
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(px::PrivateKeyPath), realityPrivateKey);
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(amnezia::protocols::xray::PrivateKeyPath), realityPrivateKey);
|
||||
if (errorCode != ErrorCode::NoError)
|
||||
return errorCode;
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(px::PublicKeyPath), realityPublicKey);
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(amnezia::protocols::xray::PublicKeyPath), realityPublicKey);
|
||||
if (errorCode != ErrorCode::NoError)
|
||||
return errorCode;
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(px::shortidPath), realityShortId);
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(amnezia::protocols::xray::shortidPath), realityShortId);
|
||||
if (errorCode != ErrorCode::NoError)
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
QJsonObject streamSettings = buildStreamSettings(srv, clientId);
|
||||
if (securityEff == QLatin1String("reality")) {
|
||||
const QString siteEff = srv.site.isEmpty() ? QString::fromLatin1(px::defaultSite) : srv.site;
|
||||
const QString sniEff = srv.sni.isEmpty() ? siteEff : srv.sni;
|
||||
const QString fpEff = srv.fingerprint.isEmpty() ? QString::fromLatin1(px::defaultFingerprint) : srv.fingerprint;
|
||||
QJsonObject rs;
|
||||
rs[QStringLiteral("dest")] = siteEff + QStringLiteral(":443");
|
||||
rs[px::fingerprint] = fpEff;
|
||||
rs[QStringLiteral("privateKey")] = realityPrivateKey;
|
||||
rs[px::serverNames] = QJsonArray { sniEff };
|
||||
rs[QStringLiteral("shortIds")] = QJsonArray { realityShortId };
|
||||
streamSettings[px::realitySettings] = rs;
|
||||
}
|
||||
|
||||
QJsonObject clientEntry;
|
||||
clientEntry[px::id] = clientId;
|
||||
const QString flowValue = effectiveClientFlow(srv);
|
||||
if (!flowValue.isEmpty()) {
|
||||
clientEntry[px::flow] = flowValue;
|
||||
}
|
||||
|
||||
QJsonObject settings;
|
||||
settings[px::clients] = QJsonArray { clientEntry };
|
||||
settings[QStringLiteral("decryption")] = QStringLiteral("none");
|
||||
|
||||
QJsonObject inbound;
|
||||
inbound[px::port] = srv.port.isEmpty() ? QString(px::defaultPort).toInt() : srv.port.toInt();
|
||||
inbound[QStringLiteral("protocol")] = QStringLiteral("vless");
|
||||
inbound[px::settings] = settings;
|
||||
inbound[px::streamSettings] = streamSettings;
|
||||
|
||||
QJsonObject serverConfig;
|
||||
serverConfig[QStringLiteral("log")] = QJsonObject { { QStringLiteral("loglevel"), QStringLiteral("error") } };
|
||||
serverConfig[px::inbounds] = QJsonArray { inbound };
|
||||
serverConfig[px::outbounds] =
|
||||
QJsonArray { QJsonObject { { QStringLiteral("protocol"), QStringLiteral("freedom") } } };
|
||||
|
||||
const QString json = QString::fromUtf8(QJsonDocument(serverConfig).toJson());
|
||||
errorCode = m_sshSession->uploadTextFileToContainer(container, credentials, json,
|
||||
QString::fromLatin1(px::serverConfigPath),
|
||||
libssh::ScpOverwriteMode::ScpOverwriteExisting);
|
||||
const QString flowValue = effectiveServerFlow(srv);
|
||||
const QJsonArray clients = collectServerClients(credentials, container, flowValue, clientId, errorCode);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Xray writeServerConfigForSetup: upload failed, error=" << static_cast<int>(errorCode);
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
const QString portEff = srv.port.isEmpty() ? QString::fromLatin1(amnezia::protocols::xray::defaultPort) : srv.port;
|
||||
|
||||
|
||||
XrayServerInboundInputs inputs;
|
||||
inputs.clients = clients;
|
||||
inputs.realityPrivateKey = realityPrivateKey;
|
||||
inputs.realityShortId = realityShortId;
|
||||
|
||||
const QJsonObject inboundJson = srv.toServerInboundJson(inputs);
|
||||
if (useAtomicApply) {
|
||||
errorCode = uploadServerConfigAtomically(credentials, container, portEff, inboundJson);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
} else {
|
||||
const QString json = QString::fromUtf8(QJsonDocument(inboundJson).toJson());
|
||||
errorCode = m_sshSession->uploadTextFileToContainer(container, credentials, json,
|
||||
QString::fromLatin1(amnezia::protocols::xray::serverConfigPath),
|
||||
libssh::ScpOverwriteMode::ScpOverwriteExisting);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
|
||||
XrayProtocolConfig updated =
|
||||
buildClientProtocolConfig(credentials, container, srv, clientId, errorCode, realityPublicKey, realityShortId);
|
||||
buildClientProtocolConfig(credentials, container, srv, clientId, errorCode,
|
||||
realityPublicKey, realityShortId, tlsPin);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Xray writeServerConfigForSetup: buildClientProtocolConfig failed, error="
|
||||
<< static_cast<int>(errorCode);
|
||||
@@ -471,6 +358,217 @@ ErrorCode XrayConfigurator::writeServerConfigForSetup(const ServerCredentials &c
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
QJsonArray XrayConfigurator::collectServerClients(const ServerCredentials &credentials, DockerContainer container,
|
||||
const QString &flowValue, const QString &fallbackClientId,
|
||||
ErrorCode &outError) const
|
||||
{
|
||||
outError = ErrorCode::NoError;
|
||||
const QString configPath = QString::fromLatin1(amnezia::protocols::xray::serverConfigPath);
|
||||
|
||||
QString currentConfig;
|
||||
for (int attempt = 0; attempt < 3; ++attempt) {
|
||||
ErrorCode readError = ErrorCode::NoError;
|
||||
currentConfig = m_sshSession->getTextFileFromContainer(container, credentials, configPath, readError);
|
||||
if (readError == ErrorCode::NoError && !currentConfig.trimmed().isEmpty()) {
|
||||
break;
|
||||
}
|
||||
currentConfig.clear();
|
||||
if (attempt < 2) {
|
||||
QThread::msleep(500);
|
||||
}
|
||||
}
|
||||
|
||||
QJsonArray existing;
|
||||
if (currentConfig.trimmed().isEmpty()) {
|
||||
QString probeOut;
|
||||
auto collect = [&probeOut](const QString &data, libssh::Client &) {
|
||||
probeOut += data + "\n";
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
const QString probe =
|
||||
QStringLiteral("if [ -s %1 ]; then echo amnezia_config=present; else echo amnezia_config=absent; fi")
|
||||
.arg(configPath);
|
||||
const ErrorCode probeError = m_sshSession->runContainerScript(credentials, container, probe, collect, collect);
|
||||
|
||||
if (probeError != ErrorCode::NoError || !probeOut.contains(QLatin1String("amnezia_config="))) {
|
||||
outError = ErrorCode::XrayServerConfigInvalid;
|
||||
return {};
|
||||
}
|
||||
if (probeOut.contains(QLatin1String("amnezia_config=present"))) {
|
||||
outError = ErrorCode::XrayServerConfigInvalid;
|
||||
return {};
|
||||
}
|
||||
} else {
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(currentConfig.toUtf8());
|
||||
if (!doc.isObject()) {
|
||||
outError = ErrorCode::XrayServerConfigInvalid;
|
||||
return {};
|
||||
}
|
||||
existing = XrayServerConfig::clientsFromServerInboundJson(doc.object());
|
||||
}
|
||||
|
||||
QJsonArray clients =
|
||||
XrayServerConfig::applyFlowToClients(existing, flowValue, XrayClientListFilter::DropWithoutId);
|
||||
|
||||
if (!fallbackClientId.isEmpty()
|
||||
&& XrayServerConfig::indexOfClient(clients, fallbackClientId) < 0) {
|
||||
clients.append(XrayServerConfig::makeClientEntry(fallbackClientId, flowValue));
|
||||
}
|
||||
|
||||
return clients;
|
||||
}
|
||||
|
||||
ErrorCode XrayConfigurator::ensureTlsCertificate(const ServerCredentials &credentials, DockerContainer container,
|
||||
QString &outFingerprint) const
|
||||
{
|
||||
outFingerprint.clear();
|
||||
|
||||
QString stdOut;
|
||||
auto collect = [&stdOut](const QString &data, libssh::Client &) {
|
||||
stdOut += data + QLatin1Char('\n');
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
|
||||
const QString certPath = QString::fromLatin1(amnezia::protocols::xray::tlsCertPath);
|
||||
const QString keyPath = QString::fromLatin1(amnezia::protocols::xray::tlsKeyPath);
|
||||
const QString script = QStringLiteral(
|
||||
"has_key() { [ -f \"$1\" ] && [ -n \"$(tr -d '[:space:]' < \"$1\" 2>/dev/null)\" ]; }\n"
|
||||
"CERT=%1\n"
|
||||
"KEY=%2\n"
|
||||
"if has_key \"$CERT\" && has_key \"$KEY\"; then echo amnezia_tls=present;\n"
|
||||
"else\n"
|
||||
" openssl req -x509 -newkey rsa:2048 -nodes -days 3650 -subj \"/CN=amnezia-xray\" "
|
||||
"-keyout \"$KEY\" -out \"$CERT\" || true\n"
|
||||
" if has_key \"$CERT\" && has_key \"$KEY\"; then echo amnezia_tls=created;\n"
|
||||
" else echo amnezia_tls=failed; exit 1; fi\n"
|
||||
"fi\n"
|
||||
"FP=$(openssl x509 -noout -fingerprint -sha256 -in \"$CERT\" 2>/dev/null "
|
||||
"| sed 's/.*Fingerprint=//' | tr -d ':' | tr 'A-F' 'a-f')\n"
|
||||
"if [ -z \"$FP\" ]; then echo amnezia_tls=failed; exit 1; fi\n"
|
||||
"echo amnezia_tls_fp=$FP\n"
|
||||
"exit 0\n")
|
||||
.arg(certPath, keyPath);
|
||||
|
||||
const ErrorCode scriptError = m_sshSession->runContainerScript(credentials, container, script, collect, collect);
|
||||
const bool present = stdOut.contains(QLatin1String("amnezia_tls=present"));
|
||||
const bool created = stdOut.contains(QLatin1String("amnezia_tls=created"));
|
||||
outFingerprint = tlsFingerprintFromEnsureOutput(stdOut);
|
||||
if (scriptError != ErrorCode::NoError || (!present && !created) || outFingerprint.isEmpty()) {
|
||||
outFingerprint.clear();
|
||||
return ErrorCode::XrayTlsNotSupported;
|
||||
}
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode XrayConfigurator::uploadServerConfigAtomically(const ServerCredentials &credentials,
|
||||
DockerContainer container, const QString &listenPort,
|
||||
const QJsonObject &serverConfig) const
|
||||
{
|
||||
const QString livePath = QString::fromLatin1(amnezia::protocols::xray::serverConfigPath);
|
||||
const QString stagedPath = QStringLiteral("/opt/amnezia/xray/server.new.json");
|
||||
|
||||
const QString json = QString::fromUtf8(QJsonDocument(serverConfig).toJson());
|
||||
ErrorCode errorCode = m_sshSession->uploadTextFileToContainer(container, credentials, json, stagedPath,
|
||||
libssh::ScpOverwriteMode::ScpOverwriteExisting);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
const QString swapScript = QStringLiteral(
|
||||
"LIVE=%1\n"
|
||||
"STAGED=%2\n"
|
||||
"BACKUP=\"$LIVE.bak\"\n"
|
||||
"if [ ! -s \"$STAGED\" ]; then echo \"amnezia_apply=missing\"; exit 0; fi\n"
|
||||
"if ! xray -test -format json -config \"$STAGED\" > /tmp/xray_test.log 2>&1; then\n"
|
||||
" echo \"amnezia_apply=invalid\"\n"
|
||||
" rm -f \"$STAGED\"\n"
|
||||
" exit 0\n"
|
||||
"fi\n"
|
||||
"if [ -f \"$LIVE\" ]; then cp \"$LIVE\" \"$BACKUP\"; fi\n"
|
||||
"mv \"$STAGED\" \"$LIVE\"\n"
|
||||
"echo \"amnezia_apply=swapped\"\n")
|
||||
.arg(livePath, stagedPath);
|
||||
|
||||
QString swapOut;
|
||||
auto collectSwap = [&swapOut](const QString &data, libssh::Client &) {
|
||||
swapOut += data + "\n";
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
errorCode = m_sshSession->runContainerScript(credentials, container, swapScript, collectSwap, collectSwap);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
if (swapOut.contains(QLatin1String("amnezia_apply=missing"))) {
|
||||
return ErrorCode::XrayServerConfigRejected;
|
||||
}
|
||||
if (swapOut.contains(QLatin1String("amnezia_apply=invalid"))) {
|
||||
return ErrorCode::XrayServerConfigRejected;
|
||||
}
|
||||
if (!swapOut.contains(QLatin1String("amnezia_apply=swapped"))) {
|
||||
return ErrorCode::XrayServerConfigInvalid;
|
||||
}
|
||||
|
||||
if (restartXrayContainer(credentials, container) && xrayProcessIsUp(credentials, container)) {
|
||||
m_sshSession->runContainerScript(credentials, container,
|
||||
QStringLiteral("rm -f %1.bak").arg(livePath));
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
const QString restoreScript = QStringLiteral(
|
||||
"LIVE=%1\n"
|
||||
"BACKUP=\"$LIVE.bak\"\n"
|
||||
"if [ -f \"$BACKUP\" ]; then cp \"$BACKUP\" \"$LIVE\"; echo amnezia_restore=done; "
|
||||
"else echo amnezia_restore=nobackup; fi\n")
|
||||
.arg(livePath);
|
||||
QString restoreOut;
|
||||
auto collectRestore = [&restoreOut](const QString &data, libssh::Client &) {
|
||||
restoreOut += data + "\n";
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
m_sshSession->runContainerScript(credentials, container, restoreScript, collectRestore, collectRestore);
|
||||
if (!restoreOut.contains(QLatin1String("amnezia_restore=done"))) {
|
||||
return ErrorCode::XrayServerNotServing;
|
||||
}
|
||||
if (restartXrayContainer(credentials, container) && xrayProcessIsUp(credentials, container)) {
|
||||
return ErrorCode::XrayServerConfigRolledBack;
|
||||
}
|
||||
return ErrorCode::XrayServerNotServing;
|
||||
}
|
||||
|
||||
bool XrayConfigurator::restartXrayContainer(const ServerCredentials &credentials, DockerContainer container) const
|
||||
{
|
||||
const ErrorCode errorCode = m_sshSession->runScript(
|
||||
credentials,
|
||||
SshSession::replaceVars(QStringLiteral("sudo docker restart $CONTAINER_NAME"),
|
||||
amnezia::genBaseVars(credentials, container, QString(), QString())));
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool XrayConfigurator::xrayProcessIsUp(const ServerCredentials &credentials, DockerContainer container) const
|
||||
{
|
||||
const QString script = QStringLiteral(
|
||||
"for i in 1 2 3 4 5 6; do\n"
|
||||
" if pgrep xray >/dev/null 2>&1 || ps 2>/dev/null | grep -v grep | grep -q \"[x]ray\"; then\n"
|
||||
" echo amnezia_serving=yes\n"
|
||||
" exit 0\n"
|
||||
" fi\n"
|
||||
" sleep 1\n"
|
||||
"done\n"
|
||||
"echo amnezia_serving=no\n");
|
||||
QString out;
|
||||
auto collect = [&out](const QString &data, libssh::Client &) {
|
||||
out += data + "\n";
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
const ErrorCode errorCode = m_sshSession->runContainerScript(credentials, container, script, collect, collect);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return false;
|
||||
}
|
||||
return out.contains(QLatin1String("amnezia_serving=yes"));
|
||||
}
|
||||
QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentials, DockerContainer container,
|
||||
const ContainerConfig &containerConfig,
|
||||
const DnsSettings &dnsSettings,
|
||||
@@ -489,17 +587,21 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia
|
||||
|
||||
XrayProtocolConfig XrayConfigurator::buildClientProtocolConfig(const ServerCredentials &credentials,
|
||||
DockerContainer container,
|
||||
const XrayServerConfig &srv, const QString &clientId,
|
||||
const XrayServerConfig &srv,
|
||||
const QString &clientId,
|
||||
ErrorCode &errorCode,
|
||||
const QString &prefetchedRealityPublicKey,
|
||||
const QString &prefetchedRealityShortId) const
|
||||
const QString &prefetchedRealityShortId,
|
||||
const QString &prefetchedTlsPin) const
|
||||
{
|
||||
QString xrayPublicKey = prefetchedRealityPublicKey;
|
||||
QString xrayShortId = prefetchedRealityShortId;
|
||||
QString tlsPin = prefetchedTlsPin;
|
||||
|
||||
const QString securityEff = effectiveSecurity(srv);
|
||||
|
||||
if (securityEff == QLatin1String("reality")) {
|
||||
|
||||
if (securityEff == QLatin1String(amnezia::protocols::xray::securityReality)) {
|
||||
if (xrayPublicKey.isEmpty() || xrayShortId.isEmpty()) {
|
||||
errorCode = readRealityKeyFiles(container, credentials, xrayPublicKey, xrayShortId);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
@@ -508,243 +610,40 @@ XrayProtocolConfig XrayConfigurator::buildClientProtocolConfig(const ServerCrede
|
||||
}
|
||||
}
|
||||
|
||||
QJsonObject userObj;
|
||||
userObj[amnezia::protocols::xray::id] = clientId;
|
||||
userObj[amnezia::protocols::xray::encryption] = QStringLiteral("none");
|
||||
const QString flowValue = effectiveClientFlow(srv);
|
||||
if (!flowValue.isEmpty()) {
|
||||
userObj[amnezia::protocols::xray::flow] = flowValue;
|
||||
if (securityEff == QLatin1String(amnezia::protocols::xray::securityTls) && !srv.isThirdPartyConfig) {
|
||||
if (tlsPin.isEmpty()) {
|
||||
errorCode = ensureTlsCertificate(credentials, container, tlsPin);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
if (tlsPin.isEmpty()) {
|
||||
errorCode = ErrorCode::XrayTlsNotSupported;
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
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 (securityEff == QLatin1String("reality")) {
|
||||
QJsonObject rs = streamObj[amnezia::protocols::xray::realitySettings].toObject();
|
||||
rs[amnezia::protocols::xray::publicKey] = xrayPublicKey;
|
||||
rs[amnezia::protocols::xray::shortId] = xrayShortId;
|
||||
rs[amnezia::protocols::xray::spiderX] = QString();
|
||||
streamObj[amnezia::protocols::xray::realitySettings] = rs;
|
||||
}
|
||||
|
||||
outbound[amnezia::protocols::xray::streamSettings] = streamObj;
|
||||
|
||||
QJsonObject inboundObj;
|
||||
inboundObj[QStringLiteral("listen")] = amnezia::protocols::xray::defaultLocalListenAddr;
|
||||
inboundObj[amnezia::protocols::xray::port] = amnezia::protocols::xray::defaultLocalProxyPort;
|
||||
inboundObj[QStringLiteral("protocol")] = QStringLiteral("socks");
|
||||
inboundObj[amnezia::protocols::xray::settings] = QJsonObject { { QStringLiteral("udp"), true } };
|
||||
|
||||
QJsonObject clientJson;
|
||||
clientJson[QStringLiteral("log")] = QJsonObject { { QStringLiteral("loglevel"), QStringLiteral("error") } };
|
||||
clientJson[amnezia::protocols::xray::inbounds] = QJsonArray { inboundObj };
|
||||
clientJson[amnezia::protocols::xray::outbounds] = QJsonArray { outbound };
|
||||
|
||||
const QString config = QString::fromUtf8(QJsonDocument(clientJson).toJson(QJsonDocument::Compact));
|
||||
|
||||
XrayProtocolConfig protocolConfig;
|
||||
protocolConfig.serverConfig = srv;
|
||||
|
||||
XrayClientOutboundInputs inputs;
|
||||
inputs.serverAddress = credentials.hostName;
|
||||
inputs.clientId = clientId;
|
||||
inputs.realityPublicKey = xrayPublicKey;
|
||||
inputs.realityShortId = xrayShortId;
|
||||
inputs.tlsPinnedPeerCertSha256 = tlsPin;
|
||||
|
||||
XrayClientConfig clientConfig;
|
||||
clientConfig.nativeConfig = config;
|
||||
clientConfig.localPort = QString(amnezia::protocols::xray::defaultLocalProxyPort);
|
||||
const QJsonObject clientOutbound = protocolConfig.toClientOutboundJson(inputs);
|
||||
clientConfig.nativeConfig = QString::fromUtf8(QJsonDocument(clientOutbound).toJson(QJsonDocument::Compact));
|
||||
clientConfig.localPort = QString::fromLatin1(amnezia::protocols::xray::defaultLocalProxyPort);
|
||||
clientConfig.id = clientId;
|
||||
protocolConfig.setClientConfig(clientConfig);
|
||||
|
||||
return protocolConfig;
|
||||
}
|
||||
|
||||
QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, const QString &clientId) const
|
||||
{
|
||||
QJsonObject streamSettings;
|
||||
const auto &xhttp = srv.xhttp;
|
||||
const auto &mkcp = srv.mkcp;
|
||||
namespace px = amnezia::protocols::xray;
|
||||
|
||||
QString networkValue = QStringLiteral("tcp");
|
||||
if (srv.transport == QLatin1String("xhttp"))
|
||||
networkValue = QStringLiteral("xhttp");
|
||||
else if (srv.transport == QLatin1String("mkcp"))
|
||||
networkValue = QStringLiteral("kcp");
|
||||
streamSettings[px::network] = networkValue;
|
||||
|
||||
const QString securityEff = effectiveSecurity(srv);
|
||||
streamSettings[px::security] = securityEff;
|
||||
|
||||
if (securityEff == QLatin1String("tls")) {
|
||||
QJsonObject tlsSettings;
|
||||
const QString sniEff = srv.sni.isEmpty() ? QString::fromLatin1(px::defaultSni) : srv.sni;
|
||||
tlsSettings[px::serverName] = sniEff;
|
||||
const QString alpnEff = srv.alpn.isEmpty() ? QString::fromLatin1(px::defaultAlpn) : srv.alpn;
|
||||
QJsonArray alpnArray;
|
||||
for (const QString &a : alpnEff.split(QLatin1Char(','))) {
|
||||
QString t = a.trimmed();
|
||||
if (t.isEmpty())
|
||||
continue;
|
||||
if (t.compare(QLatin1String("HTTP/2"), Qt::CaseInsensitive) == 0)
|
||||
t = QStringLiteral("h2");
|
||||
else if (t.compare(QLatin1String("HTTP/1.1"), Qt::CaseInsensitive) == 0)
|
||||
t = QStringLiteral("http/1.1");
|
||||
alpnArray.append(t);
|
||||
}
|
||||
if (!alpnArray.isEmpty())
|
||||
tlsSettings[QStringLiteral("alpn")] = alpnArray;
|
||||
const QString fpEff = srv.fingerprint.isEmpty() ? QString::fromLatin1(px::defaultFingerprint) : srv.fingerprint;
|
||||
tlsSettings[px::fingerprint] = fpEff;
|
||||
streamSettings[QStringLiteral("tlsSettings")] = tlsSettings;
|
||||
}
|
||||
|
||||
if (securityEff == QLatin1String("reality")) {
|
||||
QJsonObject realSettings;
|
||||
const QString fpEff = srv.fingerprint.isEmpty() ? QString::fromLatin1(px::defaultFingerprint) : srv.fingerprint;
|
||||
realSettings[px::fingerprint] = fpEff;
|
||||
const QString sniEff = srv.sni.isEmpty() ? QString::fromLatin1(px::defaultSni) : srv.sni;
|
||||
realSettings[px::serverName] = sniEff;
|
||||
streamSettings[px::realitySettings] = realSettings;
|
||||
}
|
||||
|
||||
// XHTTP — JSON must match Xray-core SplitHTTPConfig (flat xPadding fields, see transport_internet.go)
|
||||
if (srv.transport == QLatin1String("xhttp")) {
|
||||
QJsonObject xo;
|
||||
const QString hostEff = xhttp.host.isEmpty() ? QString::fromLatin1(px::defaultXhttpHost) : xhttp.host;
|
||||
xo[QStringLiteral("host")] = hostEff;
|
||||
if (!xhttp.path.isEmpty())
|
||||
xo[QStringLiteral("path")] = xhttp.path;
|
||||
QString modeEff = normalizeXhttpMode(xhttp.mode);
|
||||
if (modeEff == QLatin1String("auto") || modeEff == QLatin1String("packet-up")) {
|
||||
modeEff = QStringLiteral("stream-one");
|
||||
}
|
||||
xo[QStringLiteral("mode")] = modeEff;
|
||||
|
||||
// No "Host" in headers: xray rejects it when the top-level "host" field is set.
|
||||
|
||||
const QString methodEff =
|
||||
xhttp.uplinkMethod.isEmpty() ? QString::fromLatin1(px::defaultXhttpUplinkMethod) : xhttp.uplinkMethod;
|
||||
xo[QStringLiteral("uplinkHTTPMethod")] = methodEff.toUpper();
|
||||
|
||||
xo[QStringLiteral("noGRPCHeader")] = xhttp.disableGrpc;
|
||||
xo[QStringLiteral("noSSEHeader")] = xhttp.disableSse;
|
||||
|
||||
const QString sessPl = normalizeSessionSeqPlacement(xhttp.sessionPlacement);
|
||||
if (!sessPl.isEmpty())
|
||||
xo[QStringLiteral("sessionIDPlacement")] = sessPl;
|
||||
const QString seqPl = normalizeSessionSeqPlacement(xhttp.seqPlacement);
|
||||
if (!seqPl.isEmpty())
|
||||
xo[QStringLiteral("seqPlacement")] = seqPl;
|
||||
if (!xhttp.sessionKey.isEmpty())
|
||||
xo[QStringLiteral("sessionIDKey")] = xhttp.sessionKey;
|
||||
if (!xhttp.seqKey.isEmpty())
|
||||
xo[QStringLiteral("seqKey")] = xhttp.seqKey;
|
||||
|
||||
const QString uDataPl = normalizeUplinkDataPlacement(xhttp.uplinkDataPlacement);
|
||||
const bool uDataNeedsPacketUp =
|
||||
uDataPl == QLatin1String("header") || uDataPl == QLatin1String("cookie");
|
||||
if (!(uDataNeedsPacketUp && modeEff != QLatin1String("packet-up")))
|
||||
xo[QStringLiteral("uplinkDataPlacement")] = uDataPl;
|
||||
if (!xhttp.uplinkDataKey.isEmpty())
|
||||
xo[QStringLiteral("uplinkDataKey")] = xhttp.uplinkDataKey;
|
||||
|
||||
const QString ucs = xhttp.uplinkChunkSize.isEmpty() ? QString::fromLatin1(px::defaultXhttpUplinkChunkSize)
|
||||
: xhttp.uplinkChunkSize;
|
||||
if (!ucs.isEmpty() && ucs != QLatin1String("0")) {
|
||||
xo[QStringLiteral("uplinkChunkSize")] = ucs.toInt();
|
||||
}
|
||||
|
||||
if (!xhttp.scMaxBufferedPosts.isEmpty())
|
||||
xo[QStringLiteral("scMaxBufferedPosts")] = xhttp.scMaxBufferedPosts.toLongLong();
|
||||
|
||||
putIntRangeIfAny(xo, "scMaxEachPostBytes", xhttp.scMaxEachPostBytesMin, xhttp.scMaxEachPostBytesMax,
|
||||
px::defaultXhttpScMaxEachPostBytesMin, px::defaultXhttpScMaxEachPostBytesMax);
|
||||
putIntRangeIfAny(xo, "scMinPostsIntervalMs", xhttp.scMinPostsIntervalMsMin, xhttp.scMinPostsIntervalMsMax,
|
||||
px::defaultXhttpScMinPostsIntervalMsMin, px::defaultXhttpScMinPostsIntervalMsMax);
|
||||
putIntRangeIfAny(xo, "scStreamUpServerSecs", xhttp.scStreamUpServerSecsMin, xhttp.scStreamUpServerSecsMax,
|
||||
px::defaultXhttpScStreamUpServerSecsMin, px::defaultXhttpScStreamUpServerSecsMax);
|
||||
|
||||
const auto &pad = xhttp.xPadding;
|
||||
xo[QStringLiteral("xPaddingObfsMode")] = pad.obfsMode;
|
||||
if (pad.obfsMode) {
|
||||
if (!pad.bytesMin.isEmpty() || !pad.bytesMax.isEmpty()) {
|
||||
const int fromV = pad.bytesMin.isEmpty()
|
||||
? QString::fromLatin1(px::defaultXPaddingBytesMin).toInt()
|
||||
: pad.bytesMin.toInt();
|
||||
int toV = pad.bytesMax.isEmpty()
|
||||
? QString::fromLatin1(px::defaultXPaddingBytesMax).toInt()
|
||||
: pad.bytesMax.toInt();
|
||||
if (toV < fromV)
|
||||
toV = fromV;
|
||||
xo[QStringLiteral("xPaddingBytes")] = makeRangeString(QString::number(fromV), QString::number(toV));
|
||||
}
|
||||
xo[QStringLiteral("xPaddingKey")] =
|
||||
pad.key.isEmpty() ? QString::fromLatin1(px::defaultXPaddingKey) : pad.key;
|
||||
xo[QStringLiteral("xPaddingHeader")] =
|
||||
pad.header.isEmpty() ? QString::fromLatin1(px::defaultXPaddingHeader) : pad.header;
|
||||
xo[QStringLiteral("xPaddingPlacement")] = normalizeXPaddingPlacement(
|
||||
pad.placement.isEmpty() ? QString::fromLatin1(px::defaultXPaddingPlacement) : pad.placement);
|
||||
xo[QStringLiteral("xPaddingMethod")] = normalizeXPaddingMethod(
|
||||
pad.method.isEmpty() ? QString::fromLatin1(px::defaultXPaddingMethod) : pad.method);
|
||||
}
|
||||
|
||||
// xmux: Xray has no "enabled" flag; omit object when UI disables multiplex tuning.
|
||||
if (xhttp.xmux.enabled) {
|
||||
QJsonObject mux;
|
||||
auto addMuxRange = [&](const char *key, const QString &a, const QString &b) {
|
||||
// omit empty / 0-0 ranges (xray may reject "0-0")
|
||||
const bool aZero = a.isEmpty() || a == QLatin1String("0");
|
||||
const bool bZero = b.isEmpty() || b == QLatin1String("0");
|
||||
if (aZero && bZero)
|
||||
return;
|
||||
const QString aV = a.isEmpty() ? QStringLiteral("0") : a;
|
||||
const QString bV = b.isEmpty() ? QStringLiteral("0") : b;
|
||||
mux[QString::fromUtf8(key)] = makeRangeString(aV, bV);
|
||||
};
|
||||
addMuxRange("maxConcurrency", xhttp.xmux.maxConcurrencyMin, xhttp.xmux.maxConcurrencyMax);
|
||||
addMuxRange("maxConnections", xhttp.xmux.maxConnectionsMin, xhttp.xmux.maxConnectionsMax);
|
||||
addMuxRange("cMaxReuseTimes", xhttp.xmux.cMaxReuseTimesMin, xhttp.xmux.cMaxReuseTimesMax);
|
||||
addMuxRange("hMaxRequestTimes", xhttp.xmux.hMaxRequestTimesMin, xhttp.xmux.hMaxRequestTimesMax);
|
||||
addMuxRange("hMaxReusableSecs", xhttp.xmux.hMaxReusableSecsMin, xhttp.xmux.hMaxReusableSecsMax);
|
||||
if (!xhttp.xmux.hKeepAlivePeriod.isEmpty())
|
||||
mux[QStringLiteral("hKeepAlivePeriod")] = xhttp.xmux.hKeepAlivePeriod.toLongLong();
|
||||
if (!mux.isEmpty())
|
||||
xo[QStringLiteral("xmux")] = mux;
|
||||
}
|
||||
|
||||
streamSettings[QStringLiteral("xhttpSettings")] = xo;
|
||||
}
|
||||
|
||||
if (srv.transport == QLatin1String("mkcp")) {
|
||||
QJsonObject kcpObj;
|
||||
const QString ttiEff = mkcp.tti.isEmpty() ? QString::fromLatin1(px::defaultMkcpTti) : mkcp.tti;
|
||||
const QString upEff = mkcp.uplinkCapacity.isEmpty() ? QString::fromLatin1(px::defaultMkcpUplinkCapacity)
|
||||
: mkcp.uplinkCapacity;
|
||||
const QString downEff = mkcp.downlinkCapacity.isEmpty() ? QString::fromLatin1(px::defaultMkcpDownlinkCapacity)
|
||||
: mkcp.downlinkCapacity;
|
||||
const QString rbufEff = mkcp.readBufferSize.isEmpty() ? QString::fromLatin1(px::defaultMkcpReadBufferSize)
|
||||
: mkcp.readBufferSize;
|
||||
const QString wbufEff = mkcp.writeBufferSize.isEmpty() ? QString::fromLatin1(px::defaultMkcpWriteBufferSize)
|
||||
: mkcp.writeBufferSize;
|
||||
kcpObj[QStringLiteral("tti")] = ttiEff.toInt();
|
||||
kcpObj[QStringLiteral("uplinkCapacity")] = upEff.toInt();
|
||||
kcpObj[QStringLiteral("downlinkCapacity")] = downEff.toInt();
|
||||
kcpObj[QStringLiteral("readBufferSize")] = rbufEff.toInt();
|
||||
kcpObj[QStringLiteral("writeBufferSize")] = wbufEff.toInt();
|
||||
kcpObj[QStringLiteral("congestion")] = mkcp.congestion;
|
||||
streamSettings[QStringLiteral("kcpSettings")] = kcpObj;
|
||||
}
|
||||
|
||||
return streamSettings;
|
||||
}
|
||||
|
||||
ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container,
|
||||
const ContainerConfig &containerConfig,
|
||||
const DnsSettings &dnsSettings,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#define XRAY_CONFIGURATOR_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "configuratorBase.h"
|
||||
@@ -33,9 +34,24 @@ public:
|
||||
amnezia::ErrorCode writeServerConfigForSetup(const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container,
|
||||
amnezia::ContainerConfig &containerConfig,
|
||||
const amnezia::DnsSettings &dnsSettings);
|
||||
const amnezia::DnsSettings &dnsSettings,
|
||||
bool useAtomicApply = false);
|
||||
|
||||
private:
|
||||
QJsonArray collectServerClients(const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container, const QString &flowValue,
|
||||
const QString &fallbackClientId, amnezia::ErrorCode &outError) const;
|
||||
|
||||
amnezia::ErrorCode uploadServerConfigAtomically(const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container, const QString &listenPort,
|
||||
const QJsonObject &serverConfig) const;
|
||||
|
||||
bool restartXrayContainer(const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container) const;
|
||||
|
||||
bool xrayProcessIsUp(const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container) const;
|
||||
|
||||
amnezia::ErrorCode readContainerKeyFile(amnezia::DockerContainer container,
|
||||
const amnezia::ServerCredentials &credentials,
|
||||
const QString &path, QString &out) const;
|
||||
@@ -44,24 +60,23 @@ private:
|
||||
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;
|
||||
const QString &prefetchedRealityShortId = {},
|
||||
const QString &prefetchedTlsPin = {}) const;
|
||||
|
||||
amnezia::ErrorCode readRealityKeyFiles(amnezia::DockerContainer container,
|
||||
const amnezia::ServerCredentials &credentials,
|
||||
QString &outPublicKey,
|
||||
QString &outShortId) const;
|
||||
|
||||
QJsonObject buildStreamSettings(const amnezia::XrayServerConfig &srv,
|
||||
const QString &clientId) const;
|
||||
amnezia::ErrorCode ensureTlsCertificate(const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container,
|
||||
QString &outFingerprint) const;
|
||||
};
|
||||
|
||||
#endif // XRAY_CONFIGURATOR_H
|
||||
@@ -76,12 +76,13 @@ QFuture<QPair<ErrorCode, QJsonArray>> NewsController::fetchNews()
|
||||
return QtFuture::makeReadyFuture(qMakePair(ErrorCode::NoError, QJsonArray()));
|
||||
}
|
||||
|
||||
auto gatewayController = QSharedPointer<GatewayController>::create(
|
||||
m_appSettingsRepository->getGatewayEndpoint(),
|
||||
m_appSettingsRepository->isDevGatewayEnv(),
|
||||
apiDefs::requestTimeoutMsecs,
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled(),
|
||||
m_appSettingsRepository);
|
||||
QSharedPointer<GatewayController> gatewayController(
|
||||
new GatewayController(m_appSettingsRepository->getGatewayEndpoint(),
|
||||
m_appSettingsRepository->isDevGatewayEnv(),
|
||||
apiDefs::requestTimeoutMsecs,
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled(),
|
||||
m_appSettingsRepository),
|
||||
&QObject::deleteLater);
|
||||
|
||||
// both country codes and service types are arrays here, one entry per gateway stack the user has
|
||||
const QJsonObject payload = GatewayPayloadBuilder(m_appSettingsRepository)
|
||||
|
||||
@@ -188,6 +188,9 @@ ErrorCode StorePurchaseController::importServiceFromMarket(const QString &userCo
|
||||
|
||||
quint16 crc = qChecksum(QJsonDocument(configObject).toJson());
|
||||
|
||||
if (!serverConfigUtils::isConfigFormatVersionSupported(configObject)) {
|
||||
return ErrorCode::ConfigFormatVersionNotSupportedError;
|
||||
}
|
||||
if (configObject.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
@@ -239,6 +239,9 @@ ErrorCode SubscriptionController::applyImportedServiceConfig(const QString &user
|
||||
|
||||
updateApiConfigInJson(serverConfigJson, serviceType, serviceProtocol, userCountryCode, responseBody);
|
||||
|
||||
if (!serverConfigUtils::isConfigFormatVersionSupported(serverConfigJson)) {
|
||||
return ErrorCode::ConfigFormatVersionNotSupportedError;
|
||||
}
|
||||
if (serverConfigJson.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
@@ -291,6 +294,9 @@ ErrorCode SubscriptionController::importTrialFromGateway(const QString &userCoun
|
||||
}
|
||||
|
||||
QJsonObject configObject = QJsonDocument::fromJson(configBytes).object();
|
||||
if (!serverConfigUtils::isConfigFormatVersionSupported(configObject)) {
|
||||
return ErrorCode::ConfigFormatVersionNotSupportedError;
|
||||
}
|
||||
if (configObject.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
@@ -301,7 +307,7 @@ ErrorCode SubscriptionController::importTrialFromGateway(const QString &userCoun
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, bool isConnectEvent,
|
||||
ErrorCode SubscriptionController::updateServiceFromGateway(const QString &serverId, const QString &serverCountryCode, bool isConnectEvent,
|
||||
CaptchaInfo *captchaInfoOut, ProtocolData *usedProtocolDataOut)
|
||||
{
|
||||
auto apiV2 = m_serversRepository->apiV2Config(serverId);
|
||||
@@ -311,11 +317,11 @@ ErrorCode SubscriptionController::updateServiceFromGateway(const QString &server
|
||||
const bool isTestPurchase = apiV2->apiConfig.isTestPurchase;
|
||||
QString serviceProtocol = apiV2->serviceProtocol();
|
||||
|
||||
if (!newCountryCode.isEmpty()) {
|
||||
if (!serverCountryCode.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) {
|
||||
if (countryObject.value(apiDefs::key::serverCountryCode).toString() != serverCountryCode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -331,7 +337,7 @@ ErrorCode SubscriptionController::updateServiceFromGateway(const QString &server
|
||||
|
||||
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
|
||||
.addField(apiDefs::key::userCountryCode, apiV2->apiConfig.userCountryCode)
|
||||
.addField(apiDefs::key::serverCountryCode, newCountryCode)
|
||||
.addField(apiDefs::key::serverCountryCode, serverCountryCode)
|
||||
.addField(apiDefs::key::serviceType, apiV2->serviceType())
|
||||
.addField(apiDefs::key::serviceProtocol, serviceProtocol)
|
||||
.addField(apiDefs::key::publicKey, publicKeyForProtocol(serviceProtocol, protocolData))
|
||||
@@ -356,6 +362,34 @@ ErrorCode SubscriptionController::updateServiceFromGateway(const QString &server
|
||||
return applyUpdatedServiceConfig(serverId, serviceProtocol, protocolData, responseBody);
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::fetchServiceConfig(const QString &serverId, const QString &serviceProtocol,
|
||||
QJsonObject &serverConfigJson)
|
||||
{
|
||||
auto apiV2 = m_serversRepository->apiV2Config(serverId);
|
||||
if (!apiV2.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
ProtocolData protocolData = generateProtocolData(serviceProtocol);
|
||||
|
||||
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
|
||||
.addField(apiDefs::key::userCountryCode, apiV2->apiConfig.userCountryCode)
|
||||
.addField(apiDefs::key::serverCountryCode, apiV2->apiConfig.serverCountryCode)
|
||||
.addField(apiDefs::key::serviceType, apiV2->serviceType())
|
||||
.addField(apiDefs::key::serviceProtocol, serviceProtocol)
|
||||
.addField(apiDefs::key::publicKey, publicKeyForProtocol(serviceProtocol, protocolData))
|
||||
.addField(apiDefs::key::authData, apiV2->authData.toJson())
|
||||
.build();
|
||||
|
||||
QByteArray responseBody;
|
||||
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody, apiV2->apiConfig.isTestPurchase);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
return extractServerConfigJsonFromResponse(responseBody, serviceProtocol, protocolData, serverConfigJson);
|
||||
}
|
||||
|
||||
void SubscriptionController::markSubscriptionExpiredByServer(const QString &serverId, const ApiV2ServerConfig &apiV2)
|
||||
{
|
||||
ApiV2ServerConfig expiredApiV2 = apiV2;
|
||||
@@ -380,6 +414,9 @@ ErrorCode SubscriptionController::applyUpdatedServiceConfig(const QString &serve
|
||||
|
||||
updateApiConfigInJson(serverConfigJson, apiV2->apiConfig.serviceType, serviceProtocol, apiV2->apiConfig.userCountryCode, responseBody);
|
||||
|
||||
if (!serverConfigUtils::isConfigFormatVersionSupported(serverConfigJson)) {
|
||||
return ErrorCode::ConfigFormatVersionNotSupportedError;
|
||||
}
|
||||
if (serverConfigJson.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
@@ -403,7 +440,7 @@ ErrorCode SubscriptionController::applyUpdatedServiceConfig(const QString &serve
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::resolveUpdateServiceCaptcha(const QString &serverId, const QString &newCountryCode,
|
||||
ErrorCode SubscriptionController::resolveUpdateServiceCaptcha(const QString &serverId, const QString &serverCountryCode,
|
||||
bool isConnectEvent, const ProtocolData &protocolData,
|
||||
const QString &captchaId, const QString &captchaSolution,
|
||||
CaptchaInfo *retryCaptchaOut)
|
||||
@@ -417,7 +454,7 @@ ErrorCode SubscriptionController::resolveUpdateServiceCaptcha(const QString &ser
|
||||
|
||||
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
|
||||
.addField(apiDefs::key::userCountryCode, apiV2->apiConfig.userCountryCode)
|
||||
.addField(apiDefs::key::serverCountryCode, newCountryCode)
|
||||
.addField(apiDefs::key::serverCountryCode, serverCountryCode)
|
||||
.addField(apiDefs::key::serviceType, apiV2->serviceType())
|
||||
.addField(apiDefs::key::serviceProtocol, serviceProtocol)
|
||||
.addField(apiDefs::key::publicKey, publicKeyForProtocol(serviceProtocol, protocolData))
|
||||
@@ -590,17 +627,19 @@ ErrorCode SubscriptionController::prepareVpnKeyExport(const QString &serverId, Q
|
||||
ErrorCode SubscriptionController::validateAndUpdateConfig(const QString &serverId, bool hasInstalledContainers,
|
||||
CaptchaInfo *captchaInfoOut, ProtocolData *usedProtocolDataOut)
|
||||
{
|
||||
if (!m_serversRepository->apiV2Config(serverId).has_value()) {
|
||||
const auto apiV2 = m_serversRepository->apiV2Config(serverId);
|
||||
if (!apiV2.has_value()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
const QString serverCountryCode = apiV2->apiConfig.serverCountryCode;
|
||||
|
||||
if (!hasInstalledContainers) {
|
||||
return updateServiceFromGateway(serverId, "", true, captchaInfoOut, usedProtocolDataOut);
|
||||
return updateServiceFromGateway(serverId, serverCountryCode, true, captchaInfoOut, usedProtocolDataOut);
|
||||
}
|
||||
|
||||
if (isApiKeyExpired(serverId)) {
|
||||
qDebug() << "attempt to update api config by expires_at event";
|
||||
return updateServiceFromGateway(serverId, "", true, captchaInfoOut, usedProtocolDataOut);
|
||||
return updateServiceFromGateway(serverId, serverCountryCode, true, captchaInfoOut, usedProtocolDataOut);
|
||||
}
|
||||
|
||||
return ErrorCode::NoError;
|
||||
|
||||
@@ -45,10 +45,12 @@ public:
|
||||
ErrorCode importTrialFromGateway(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const QString &email);
|
||||
|
||||
ErrorCode updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, bool isConnectEvent,
|
||||
ErrorCode fetchServiceConfig(const QString &serverId, const QString &serviceProtocol, QJsonObject &serverConfigJson);
|
||||
|
||||
ErrorCode updateServiceFromGateway(const QString &serverId, const QString &serverCountryCode, bool isConnectEvent,
|
||||
CaptchaInfo *captchaInfoOut = nullptr, ProtocolData *usedProtocolDataOut = nullptr);
|
||||
|
||||
ErrorCode resolveUpdateServiceCaptcha(const QString &serverId, const QString &newCountryCode, bool isConnectEvent,
|
||||
ErrorCode resolveUpdateServiceCaptcha(const QString &serverId, const QString &serverCountryCode, bool isConnectEvent,
|
||||
const ProtocolData &protocolData, const QString &captchaId,
|
||||
const QString &captchaSolution, CaptchaInfo *retryCaptchaOut = nullptr);
|
||||
|
||||
@@ -82,6 +84,21 @@ public:
|
||||
const QString &captchaId, const QString &captchaSolution,
|
||||
CaptchaInfo *retryCaptchaOut = nullptr);
|
||||
|
||||
#ifdef AMNEZIA_BUILD_TESTS
|
||||
// Exposes the gateway response parsing path without performing a network request.
|
||||
ErrorCode applyImportedServiceConfigForTest(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const ProtocolData &protocolData,
|
||||
const QByteArray &responseBody)
|
||||
{
|
||||
return applyImportedServiceConfig(userCountryCode, serviceType, serviceProtocol, protocolData, responseBody);
|
||||
}
|
||||
ErrorCode applyUpdatedServiceConfigForTest(const QString &serverId, const QString &serviceProtocol,
|
||||
const ProtocolData &protocolData, const QByteArray &responseBody)
|
||||
{
|
||||
return applyUpdatedServiceConfig(serverId, serviceProtocol, protocolData, responseBody);
|
||||
}
|
||||
#endif
|
||||
|
||||
private:
|
||||
ErrorCode executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody, bool isTestPurchase = false);
|
||||
bool isApiKeyExpired(const QString &serverId) const;
|
||||
|
||||
@@ -289,6 +289,15 @@ QJsonObject ConnectionController::createConnectionConfiguration(const QPair<QStr
|
||||
ContainerUtils::isAwgContainer(container) ? protocols::awg::defaultMtu :
|
||||
protocols::wireguard::defaultMtu;
|
||||
}
|
||||
|
||||
const QString cascadeEndpoint = vpnConfigData.value(configKey::cascadeEndpoint).toString();
|
||||
if (!cascadeEndpoint.isEmpty()) {
|
||||
vpnConfigData[configKey::hostName] = cascadeEndpoint;
|
||||
}
|
||||
const QString cascadePort = vpnConfigData.value(configKey::cascadePort).toString();
|
||||
if (!cascadePort.isEmpty()) {
|
||||
vpnConfigData[configKey::port] = cascadePort.toInt();
|
||||
}
|
||||
}
|
||||
|
||||
vpnConfiguration.insert(ProtocolUtils::key_proto_config_data(proto), vpnConfigData);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "coreController.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDirIterator>
|
||||
#include <QTranslator>
|
||||
#include <QTimer>
|
||||
@@ -19,6 +20,9 @@
|
||||
|
||||
#if defined(Q_OS_IOS)
|
||||
#include "platforms/ios/ios_controller.h"
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
#include "core/utils/swiftBridge.h"
|
||||
#endif
|
||||
|
||||
@@ -125,6 +129,12 @@ void CoreController::initModels()
|
||||
m_apiCountryModel = new ApiCountryModel(this);
|
||||
setQmlContextProperty("ApiCountryModel", m_apiCountryModel);
|
||||
|
||||
m_apiCountryListModel = new ApiCountryListModel(m_apiCountryModel, "picker", this);
|
||||
setQmlContextProperty("ApiCountryListModel", m_apiCountryListModel);
|
||||
|
||||
m_apiConfigsCountryListModel = new ApiCountryListModel(m_apiCountryModel, "nativeConfigs", this);
|
||||
setQmlContextProperty("ApiConfigsCountryListModel", m_apiConfigsCountryListModel);
|
||||
|
||||
m_apiSubscriptionPlansModel = new ApiSubscriptionPlansModel(this);
|
||||
setQmlContextProperty("ApiSubscriptionPlansModel", m_apiSubscriptionPlansModel);
|
||||
|
||||
@@ -169,6 +179,7 @@ void CoreController::initCoreControllers()
|
||||
m_importCoreController = new ImportController(m_serversRepository, m_appSettingsRepository, this);
|
||||
m_connectionController = new ConnectionController(m_serversRepository, m_appSettingsRepository, m_vpnConnection.get(), this);
|
||||
m_settingsController = new SettingsController(m_serversRepository, m_appSettingsRepository, this);
|
||||
m_localProxyController = new LocalProxyController(m_serversRepository, m_appSettingsRepository, m_subscriptionController, this);
|
||||
}
|
||||
|
||||
void CoreController::initControllers()
|
||||
@@ -178,7 +189,6 @@ void CoreController::initControllers()
|
||||
|
||||
if (m_engine) {
|
||||
m_focusController = new FocusController(m_engine, this);
|
||||
setQmlContextProperty("FocusController", m_focusController);
|
||||
}
|
||||
|
||||
m_installUiController = new InstallUiController(m_installController, m_serversController, m_settingsController, m_protocolsModel, m_usersController,
|
||||
@@ -203,6 +213,12 @@ void CoreController::initControllers()
|
||||
m_settingsUiController = new SettingsUiController(m_settingsController, m_serversController, this);
|
||||
setQmlContextProperty("SettingsController", m_settingsUiController);
|
||||
|
||||
m_localProxyUiController = new LocalProxyUiController(m_localProxyController, this);
|
||||
setQmlContextProperty("LocalProxyController", m_localProxyUiController);
|
||||
|
||||
m_logsUiController = new LogsUiController(m_settingsController, this);
|
||||
setQmlContextProperty("LogsController", m_logsUiController);
|
||||
|
||||
m_pageController = new PageController(m_serversController, m_settingsController, this);
|
||||
setQmlContextProperty("PageController", m_pageController);
|
||||
|
||||
@@ -271,7 +287,7 @@ void CoreController::initAppleController()
|
||||
|
||||
void CoreController::initLogging()
|
||||
{
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
#ifndef Q_OS_ANDROID
|
||||
bool enabled = m_appSettingsRepository->isSaveLogs();
|
||||
if (enabled) {
|
||||
if (!Logger::init(false)) {
|
||||
@@ -280,6 +296,11 @@ void CoreController::initLogging()
|
||||
}
|
||||
Logger::setServiceLogsEnabled(enabled);
|
||||
#endif
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
if (m_appSettingsRepository->isSaveLogs()) {
|
||||
SWIFT_BRIDGE_NAMESPACE::toggleLogging(true);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void CoreController::initSignalHandlers()
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
#include <QObject>
|
||||
#include <QQmlContext>
|
||||
#include <QSharedPointer>
|
||||
#include <QThread>
|
||||
#include <QTranslator>
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
#include "ui/utils/systemTrayNotificationHandler.h"
|
||||
@@ -22,6 +24,8 @@
|
||||
#include "ui/controllers/selfhosted/installUiController.h"
|
||||
#include "ui/controllers/qml/pageController.h"
|
||||
#include "ui/controllers/settingsUiController.h"
|
||||
#include "ui/controllers/localProxyUiController.h"
|
||||
#include "ui/controllers/logsUiController.h"
|
||||
#include "ui/controllers/serversUiController.h"
|
||||
#include "ui/controllers/ipSplitTunnelingUiController.h"
|
||||
#include "ui/controllers/systemController.h"
|
||||
@@ -41,6 +45,7 @@
|
||||
#include "core/controllers/api/newsController.h"
|
||||
#include "core/controllers/selfhosted/installController.h"
|
||||
#include "core/controllers/settingsController.h"
|
||||
#include "core/controllers/localProxyController.h"
|
||||
#include "core/controllers/connectionController.h"
|
||||
#include "core/controllers/updateController.h"
|
||||
|
||||
@@ -57,6 +62,7 @@
|
||||
#include "ui/models/api/apiAccountInfoModel.h"
|
||||
#include "ui/models/api/apiBenefitsModel.h"
|
||||
#include "ui/models/api/apiCountryModel.h"
|
||||
#include "ui/models/api/apiCountryListModel.h"
|
||||
#include "ui/models/api/apiDevicesModel.h"
|
||||
#include "ui/models/api/apiServicesModel.h"
|
||||
#include "ui/models/api/apiSubscriptionPlansModel.h"
|
||||
@@ -136,6 +142,10 @@ protected:
|
||||
ServersUiController* serversUiControllerProtected() const { return m_serversUiController; }
|
||||
ServicesCatalogUiController* servicesCatalogUiControllerProtected() const { return m_servicesCatalogUiController; }
|
||||
ApiNewsUiController* apiNewsUiControllerProtected() const { return m_apiNewsUiController; }
|
||||
SubscriptionController* subscriptionControllerProtected() const { return m_subscriptionController; }
|
||||
ApiCountryModel* apiCountryModelProtected() const { return m_apiCountryModel; }
|
||||
ApiCountryListModel* apiCountryListModelProtected() const { return m_apiCountryListModel; }
|
||||
ApiCountryListModel* apiConfigsCountryListModelProtected() const { return m_apiConfigsCountryListModel; }
|
||||
|
||||
private:
|
||||
void initRepositories();
|
||||
@@ -163,13 +173,15 @@ private:
|
||||
QMetaObject::Connection m_reloadConfigErrorOccurredConnection;
|
||||
|
||||
ConnectionUiController* m_connectionUiController;
|
||||
FocusController* m_focusController;
|
||||
FocusController* m_focusController = nullptr;
|
||||
PageController* m_pageController;
|
||||
InstallUiController* m_installUiController;
|
||||
ImportUiController* m_importController;
|
||||
ImportController* m_importCoreController;
|
||||
ExportUiController* m_exportUiController;
|
||||
SettingsUiController* m_settingsUiController;
|
||||
LocalProxyUiController* m_localProxyUiController;
|
||||
LogsUiController* m_logsUiController;
|
||||
ServersUiController* m_serversUiController;
|
||||
IpSplitTunnelingUiController* m_ipSplitTunnelingUiController;
|
||||
SystemController* m_systemController;
|
||||
@@ -198,6 +210,7 @@ private:
|
||||
ExportController* m_exportController;
|
||||
ConnectionController* m_connectionController;
|
||||
SettingsController* m_settingsController;
|
||||
LocalProxyController* m_localProxyController;
|
||||
|
||||
ContainersModel* m_containersModel;
|
||||
ContainersModel* m_defaultServerContainersModel;
|
||||
@@ -214,6 +227,8 @@ private:
|
||||
ApiSubscriptionPlansModel* m_apiSubscriptionPlansModel;
|
||||
ApiBenefitsModel* m_apiBenefitsModel;
|
||||
ApiCountryModel* m_apiCountryModel;
|
||||
ApiCountryListModel* m_apiCountryListModel;
|
||||
ApiCountryListModel* m_apiConfigsCountryListModel;
|
||||
ApiAccountInfoModel* m_apiAccountInfoModel;
|
||||
ApiDevicesModel* m_apiDevicesModel;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "coreSignalHandlers.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QTimer>
|
||||
#include <QtConcurrent>
|
||||
|
||||
@@ -11,6 +12,7 @@
|
||||
#include "core/utils/serverConfigUtils.h"
|
||||
#include "core/repositories/secureAppSettingsRepository.h"
|
||||
#include "vpnConnection.h"
|
||||
#include "ui/controllers/qml/focusController.h"
|
||||
#include "ui/controllers/qml/pageController.h"
|
||||
#include "ui/controllers/connectionUiController.h"
|
||||
#include "ui/controllers/settingsUiController.h"
|
||||
@@ -32,6 +34,7 @@
|
||||
#include "core/controllers/selfhosted/installController.h"
|
||||
#include "core/controllers/selfhosted/exportController.h"
|
||||
#include "core/controllers/connectionController.h"
|
||||
#include "core/controllers/localProxyController.h"
|
||||
#include "ui/models/clientManagementModel.h"
|
||||
#include "ui/controllers/api/apiNewsUiController.h"
|
||||
#include "ui/models/containersModel.h"
|
||||
@@ -65,6 +68,8 @@ void CoreSignalHandlers::initAllHandlers()
|
||||
initExportControllerHandler();
|
||||
initImportControllerHandler();
|
||||
initApiCountryModelUpdateHandler();
|
||||
initCountryListSortModeHandler();
|
||||
initCountryListStateHandler();
|
||||
initSubscriptionRefreshHandler();
|
||||
initAdminConfigRevokedHandler();
|
||||
initPassphraseRequestHandler();
|
||||
@@ -79,6 +84,7 @@ void CoreSignalHandlers::initAllHandlers()
|
||||
initAppSplitTunnelingModelUpdateHandler();
|
||||
initPrepareConfigHandler();
|
||||
initUnsupportedConnectDrawerHandler();
|
||||
initBackNavigationHandler();
|
||||
initStrictKillSwitchHandler();
|
||||
initAndroidSettingsHandler();
|
||||
initAndroidConnectionHandler();
|
||||
@@ -86,6 +92,7 @@ void CoreSignalHandlers::initAllHandlers()
|
||||
initIosSettingsHandler();
|
||||
initNotificationHandler();
|
||||
initUpdateFoundHandler();
|
||||
initLocalProxyHandler();
|
||||
}
|
||||
|
||||
void CoreSignalHandlers::initErrorMessagesHandler()
|
||||
@@ -173,6 +180,85 @@ void CoreSignalHandlers::initImportControllerHandler()
|
||||
});
|
||||
}
|
||||
|
||||
void CoreSignalHandlers::initCountryListSortModeHandler()
|
||||
{
|
||||
SecureAppSettingsRepository *repository = m_coreController->m_appSettingsRepository;
|
||||
|
||||
const QVector<ApiCountryListModel *> models { m_coreController->m_apiCountryListModel,
|
||||
m_coreController->m_apiConfigsCountryListModel };
|
||||
|
||||
const auto reloadSortModes = [repository, models]() {
|
||||
for (ApiCountryListModel *model : models) {
|
||||
model->setSortMode(repository->countryListSortMode(model->listId()));
|
||||
}
|
||||
};
|
||||
reloadSortModes();
|
||||
|
||||
for (ApiCountryListModel *model : models) {
|
||||
connect(model, &ApiCountryListModel::sortModeChanged, this, [repository, model]() {
|
||||
repository->setCountryListSortMode(model->listId(), model->sortMode());
|
||||
});
|
||||
}
|
||||
|
||||
connect(m_coreController->m_settingsUiController, &SettingsUiController::restoreBackupFinished, this, reloadSortModes);
|
||||
connect(repository, &SecureAppSettingsRepository::settingsCleared, this, reloadSortModes);
|
||||
}
|
||||
|
||||
void CoreSignalHandlers::initCountryListStateHandler()
|
||||
{
|
||||
SecureAppSettingsRepository *repository = m_coreController->m_appSettingsRepository;
|
||||
|
||||
const QVector<ApiCountryListModel *> models { m_coreController->m_apiCountryListModel,
|
||||
m_coreController->m_apiConfigsCountryListModel };
|
||||
|
||||
ApiCountryModel *countryModel = m_coreController->m_apiCountryModel;
|
||||
SecureServersRepository *serversRepository = m_coreController->m_serversRepository;
|
||||
|
||||
const auto reloadFavorites = [repository, models, countryModel]() {
|
||||
const QStringList codes = repository->favoriteLocations(countryModel->serverId());
|
||||
for (ApiCountryListModel *model : models) {
|
||||
model->setFavorites(codes);
|
||||
}
|
||||
};
|
||||
const auto migrateLegacyFavorites = [repository, serversRepository]() {
|
||||
QStringList apiServerIds;
|
||||
for (const QString &serverId : serversRepository->orderedServerIds()) {
|
||||
if (serversRepository->apiV2Config(serverId).has_value()) {
|
||||
apiServerIds.append(serverId);
|
||||
}
|
||||
}
|
||||
repository->migrateLegacyFavoriteLocations(apiServerIds);
|
||||
};
|
||||
migrateLegacyFavorites();
|
||||
reloadFavorites();
|
||||
|
||||
connect(countryModel, &ApiCountryModel::serverIdChanged, this, reloadFavorites);
|
||||
connect(m_coreController->m_settingsUiController, &SettingsUiController::restoreBackupFinished, this,
|
||||
[migrateLegacyFavorites, reloadFavorites]() {
|
||||
migrateLegacyFavorites();
|
||||
reloadFavorites();
|
||||
});
|
||||
connect(repository, &SecureAppSettingsRepository::settingsCleared, this, reloadFavorites);
|
||||
connect(serversRepository, &SecureServersRepository::serverRemoved, this,
|
||||
[repository, countryModel](const QString &serverId) {
|
||||
repository->removeFavoriteLocations(serverId);
|
||||
if (countryModel->serverId() == serverId) {
|
||||
countryModel->setServerId(QString());
|
||||
}
|
||||
});
|
||||
|
||||
for (ApiCountryListModel *model : models) {
|
||||
connect(model, &ApiCountryListModel::favoritesChanged, this, [repository, models, model, countryModel](const QStringList &codes) {
|
||||
repository->setFavoriteLocations(countryModel->serverId(), codes);
|
||||
for (ApiCountryListModel *other : models) {
|
||||
if (other != model) {
|
||||
other->setFavorites(codes);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void CoreSignalHandlers::initApiCountryModelUpdateHandler()
|
||||
{
|
||||
connect(m_coreController->m_serversUiController, &ServersUiController::updateApiCountryModel, this, [this]() {
|
||||
@@ -186,8 +272,10 @@ void CoreSignalHandlers::initApiCountryModelUpdateHandler()
|
||||
return;
|
||||
}
|
||||
|
||||
m_coreController->m_apiCountryModel->setServerId(processedServerId);
|
||||
m_coreController->m_apiCountryModel->updateModel(apiV2->apiConfig.availableCountries,
|
||||
apiV2->apiConfig.serverCountryCode);
|
||||
apiV2->apiConfig.serverCountryCode,
|
||||
apiV2->apiConfig.userCountryCode);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -230,6 +318,18 @@ void CoreSignalHandlers::initTranslationsUpdatedHandler()
|
||||
connect(m_coreController->m_languageUiController, &LanguageUiController::updateTranslations, m_coreController, &CoreController::updateTranslator);
|
||||
connect(m_coreController, &CoreController::translationsUpdated, m_coreController->m_languageUiController, &LanguageUiController::translationsUpdated);
|
||||
connect(m_coreController, &CoreController::translationsUpdated, m_coreController->m_connectionUiController, &ConnectionUiController::onTranslationsUpdated);
|
||||
connect(m_coreController, &CoreController::translationsUpdated, m_coreController->m_logsUiController, &LogsUiController::refreshSizes);
|
||||
|
||||
const QVector<ApiCountryListModel *> countryListModels { m_coreController->m_apiCountryListModel,
|
||||
m_coreController->m_apiConfigsCountryListModel };
|
||||
auto applyUiLanguage = [this, countryListModels]() {
|
||||
const QString languageCode = QLocale::languageToCode(m_coreController->m_settingsController->getAppLanguage().language());
|
||||
for (ApiCountryListModel *model : countryListModels) {
|
||||
model->setUiLanguage(languageCode);
|
||||
}
|
||||
};
|
||||
applyUiLanguage();
|
||||
connect(m_coreController, &CoreController::translationsUpdated, this, applyUiLanguage);
|
||||
}
|
||||
|
||||
void CoreSignalHandlers::initLanguageHandler()
|
||||
@@ -359,6 +459,16 @@ void CoreSignalHandlers::initUnsupportedConnectDrawerHandler()
|
||||
m_coreController->m_pageController, &PageController::unsupportedConnectDrawerRequested);
|
||||
}
|
||||
|
||||
void CoreSignalHandlers::initBackNavigationHandler()
|
||||
{
|
||||
if (!m_coreController->m_focusController) {
|
||||
return;
|
||||
}
|
||||
|
||||
connect(m_coreController->m_focusController, &FocusController::backRequested,
|
||||
m_coreController->m_pageController, &PageController::escapePressed);
|
||||
}
|
||||
|
||||
void CoreSignalHandlers::initStrictKillSwitchHandler()
|
||||
{
|
||||
connect(m_coreController->m_settingsUiController, &SettingsUiController::strictKillSwitchEnabledChanged, m_coreController->m_connectionController,
|
||||
@@ -444,3 +554,12 @@ void CoreSignalHandlers::initUpdateFoundHandler()
|
||||
});
|
||||
}
|
||||
|
||||
void CoreSignalHandlers::initLocalProxyHandler()
|
||||
{
|
||||
connect(m_coreController->m_connectionController, &ConnectionController::openConnectionRequested, m_coreController->m_localProxyController,
|
||||
&LocalProxyController::onVpnConnectionRequested);
|
||||
connect(m_coreController->m_localProxyController, &LocalProxyController::stoppedByVpn, m_coreController->m_pageController,
|
||||
&PageController::showNotificationMessage);
|
||||
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, m_coreController->m_localProxyController,
|
||||
&LocalProxyController::disable);
|
||||
}
|
||||
@@ -20,6 +20,8 @@ private:
|
||||
void initExportControllerHandler();
|
||||
void initImportControllerHandler();
|
||||
void initApiCountryModelUpdateHandler();
|
||||
void initCountryListSortModeHandler();
|
||||
void initCountryListStateHandler();
|
||||
void initSubscriptionRefreshHandler();
|
||||
void initAdminConfigRevokedHandler();
|
||||
void initPassphraseRequestHandler();
|
||||
@@ -34,6 +36,7 @@ private:
|
||||
void initAppSplitTunnelingModelUpdateHandler();
|
||||
void initPrepareConfigHandler();
|
||||
void initUnsupportedConnectDrawerHandler();
|
||||
void initBackNavigationHandler();
|
||||
void initStrictKillSwitchHandler();
|
||||
void initAndroidSettingsHandler();
|
||||
void initAndroidConnectionHandler();
|
||||
@@ -41,6 +44,7 @@ private:
|
||||
void initIosSettingsHandler();
|
||||
void initNotificationHandler();
|
||||
void initUpdateFoundHandler();
|
||||
void initLocalProxyHandler();
|
||||
|
||||
CoreController* m_coreController;
|
||||
};
|
||||
|
||||
@@ -1,107 +1,79 @@
|
||||
#include "gatewayController.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <random>
|
||||
|
||||
#include <QCryptographicHash>
|
||||
#include <QDebug>
|
||||
#include <QEventLoop>
|
||||
#include <QFutureWatcher>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkReply>
|
||||
#include <QPromise>
|
||||
#include <QThread>
|
||||
#include <QUrl>
|
||||
#include <QtConcurrent/QtConcurrentRun>
|
||||
|
||||
#include <openssl/rsa.h>
|
||||
|
||||
#include "amneziaApplication.h"
|
||||
#include "core/repositories/secureAppSettingsRepository.h"
|
||||
#include "core/utils/api/apiUtils.h"
|
||||
#include "core/utils/constants/apiConstants.h"
|
||||
#include "core/utils/constants/apiKeys.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
#include "cryptoUtils.h"
|
||||
#include "core/utils/api/apiUtils.h"
|
||||
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
#include "core/utils/ipcClient.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#include "platforms/ios/ios_controller.h"
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr QLatin1String errorResponsePattern1("No active configuration found for");
|
||||
constexpr QLatin1String errorResponsePattern2("No non-revoked public key found for");
|
||||
constexpr QLatin1String errorResponsePattern3("Account not found.");
|
||||
constexpr QLatin1String errorResponsePatternQrSessionNotFound("QR session not found");
|
||||
constexpr QLatin1String errorResponsePatternSessionNotFound("Session not found");
|
||||
|
||||
constexpr QLatin1String updateRequestResponsePattern("client version update is required");
|
||||
|
||||
constexpr int httpStatusCodeNotFound = 404;
|
||||
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)
|
||||
namespace agwConfigKey
|
||||
{
|
||||
QStringList shuffled = proxyUrls;
|
||||
std::random_device randomDevice;
|
||||
std::mt19937 generator(randomDevice());
|
||||
std::shuffle(shuffled.begin(), shuffled.end(), generator);
|
||||
return shuffled;
|
||||
constexpr QLatin1String gatewayEndpoint("gateway_endpoint");
|
||||
constexpr QLatin1String publicKeyPem("public_key_pem");
|
||||
constexpr QLatin1String s3PrimaryEndpoints("s3_primary_endpoints");
|
||||
constexpr QLatin1String s3FallbackEndpoints("s3_fallback_endpoints");
|
||||
constexpr QLatin1String isDevEnvironment("is_dev_environment");
|
||||
constexpr QLatin1String requestTimeoutMsecs("request_timeout_msecs");
|
||||
constexpr QLatin1String proxyRequestTimeoutMsecs("proxy_request_timeout_msecs");
|
||||
constexpr QLatin1String proxyStorageTimeoutMsecs("proxy_storage_timeout_msecs");
|
||||
constexpr QLatin1String proxyHealthTimeoutMsecs("proxy_health_timeout_msecs");
|
||||
}
|
||||
|
||||
QString getProxyUrlsCacheKey(const QString &serviceType, const QString &userCountryCode)
|
||||
namespace agwOptionsKey
|
||||
{
|
||||
return QStringLiteral("service_%1_country_%2").arg(serviceType, userCountryCode);
|
||||
constexpr QLatin1String serviceType("service_type");
|
||||
constexpr QLatin1String userCountryCode("user_country_code");
|
||||
}
|
||||
|
||||
bool decryptProxyUrlsPayload(const QByteArray &encryptedPayload, bool isDevEnvironment, QByteArray &decryptedPayload)
|
||||
namespace agwStateKey
|
||||
{
|
||||
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();
|
||||
constexpr QLatin1String workingProxy("working_proxy");
|
||||
}
|
||||
|
||||
QByteArray decKey = QByteArray::fromHex(h.left(64));
|
||||
QByteArray iv = QByteArray::fromHex(h.mid(64, 32));
|
||||
QByteArray ba = QByteArray::fromBase64(encryptedPayload);
|
||||
constexpr QLatin1String agwStateCacheKey("agw_state");
|
||||
constexpr QLatin1String agwTracePrefix("agw trace:");
|
||||
constexpr QLatin1String agwTraceFailoverMarker("suspicious");
|
||||
constexpr QLatin1String endpointListSeparator(", ");
|
||||
constexpr QLatin1String legacyEndpointPlaceholder("%1");
|
||||
|
||||
decryptedPayload = CryptoUtils::decryptAes256Cbc(ba, decKey, iv);
|
||||
if (decryptedPayload.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
QStringList splitEndpoints(const char *raw)
|
||||
{
|
||||
return QString::fromUtf8(raw).split(endpointListSeparator, Qt::SkipEmptyParts);
|
||||
}
|
||||
|
||||
void logAgwTrace(const QString &path, const int code, const QString &trace)
|
||||
{
|
||||
if (trace.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const QString text = QStringLiteral("%1 %2\n%3").arg(agwTracePrefix, path, trace).replace(QLatin1Char('\n'), QStringLiteral("\n "));
|
||||
if (code != AGW_OK) {
|
||||
qWarning().noquote() << text;
|
||||
} else if (trace.contains(agwTraceFailoverMarker)) {
|
||||
qInfo().noquote() << text;
|
||||
} else {
|
||||
decryptedPayload = encryptedPayload;
|
||||
qDebug().noquote() << text;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,615 +82,187 @@ GatewayController::GatewayController(const QString &gatewayEndpoint, const bool
|
||||
QObject *parent)
|
||||
: QObject(parent),
|
||||
m_gatewayEndpoint(gatewayEndpoint),
|
||||
m_isDevEnvironment(isDevEnvironment),
|
||||
m_requestTimeoutMsecs(requestTimeoutMsecs),
|
||||
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled),
|
||||
m_appSettingsRepository(appSettingsRepository)
|
||||
{
|
||||
const QByteArray publicKey = isDevEnvironment ? QByteArray(DEV_AGW_PUBLIC_KEY) : QByteArray(PROD_AGW_PUBLIC_KEY);
|
||||
m_publicKeyMissing = publicKey.isEmpty();
|
||||
|
||||
QStringList primaryEndpoints;
|
||||
QStringList fallbackEndpoints;
|
||||
if (isDevEnvironment) {
|
||||
primaryEndpoints = splitEndpoints(DEV_S3_ENDPOINT);
|
||||
} else {
|
||||
primaryEndpoints = splitEndpoints(PROD_S3_ENDPOINT);
|
||||
fallbackEndpoints = splitEndpoints(FALLBACK_S3_ENDPOINT);
|
||||
}
|
||||
|
||||
QJsonObject config;
|
||||
config[agwConfigKey::gatewayEndpoint] = gatewayEndpoint;
|
||||
config[agwConfigKey::publicKeyPem] = QString::fromUtf8(publicKey);
|
||||
config[agwConfigKey::s3PrimaryEndpoints] = QJsonArray::fromStringList(primaryEndpoints);
|
||||
config[agwConfigKey::s3FallbackEndpoints] = QJsonArray::fromStringList(fallbackEndpoints);
|
||||
config[agwConfigKey::isDevEnvironment] = isDevEnvironment;
|
||||
config[agwConfigKey::requestTimeoutMsecs] = requestTimeoutMsecs;
|
||||
config[agwConfigKey::proxyRequestTimeoutMsecs] = apiDefs::proxyRequestTimeoutMsecs;
|
||||
config[agwConfigKey::proxyStorageTimeoutMsecs] = apiDefs::proxyStorageTimeoutMsecs;
|
||||
config[agwConfigKey::proxyHealthTimeoutMsecs] = apiDefs::proxyHealthTimeoutMsecs;
|
||||
|
||||
m_client = agw_client_create(QJsonDocument(config).toJson(QJsonDocument::Compact).constData(), nullptr);
|
||||
if (m_client == 0) {
|
||||
qCritical() << "GatewayController: failed to create gateway client (missing key or endpoint?)";
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_appSettingsRepository != nullptr) {
|
||||
const QByteArray state = m_appSettingsRepository->readGatewayProxyUrls(agwStateCacheKey);
|
||||
if (!state.isEmpty() && agw_import_state(m_client, state.constData()) == AGW_OK) {
|
||||
m_lastPersistedState = state;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GatewayController::EncryptedRequestData GatewayController::prepareRequest(const QString &endpoint, const QJsonObject &apiPayload)
|
||||
GatewayController::~GatewayController()
|
||||
{
|
||||
EncryptedRequestData encRequestData;
|
||||
encRequestData.errorCode = ErrorCode::NoError;
|
||||
agw_client_destroy(m_client);
|
||||
}
|
||||
|
||||
amnezia::ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody)
|
||||
{
|
||||
QFuture<QPair<amnezia::ErrorCode, QByteArray>> future = postAsync(endpoint, apiPayload);
|
||||
|
||||
QFutureWatcher<QPair<amnezia::ErrorCode, QByteArray>> watcher;
|
||||
QEventLoop wait;
|
||||
connect(&watcher, &QFutureWatcherBase::finished, &wait, &QEventLoop::quit);
|
||||
watcher.setFuture(future);
|
||||
if (!future.isFinished()) {
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
}
|
||||
|
||||
const QPair<amnezia::ErrorCode, QByteArray> result = future.result();
|
||||
responseBody = result.second;
|
||||
return result.first;
|
||||
}
|
||||
|
||||
QFuture<QPair<amnezia::ErrorCode, QByteArray>> GatewayController::postAsync(const QString &endpoint, const QJsonObject apiPayload)
|
||||
{
|
||||
prepareRequest();
|
||||
return QtConcurrent::run(
|
||||
[this, endpoint, apiPayload]() -> QPair<amnezia::ErrorCode, QByteArray> { return executePost(endpoint, apiPayload); });
|
||||
}
|
||||
|
||||
QPair<amnezia::ErrorCode, QByteArray> GatewayController::executePost(const QString &endpoint, const QJsonObject &apiPayload)
|
||||
{
|
||||
if (m_client == 0) {
|
||||
return qMakePair(m_publicKeyMissing ? amnezia::ErrorCode::ApiMissingAgwPublicKey : amnezia::ErrorCode::ApiConfigDownloadError,
|
||||
QByteArray());
|
||||
}
|
||||
|
||||
QString path = endpoint;
|
||||
path.remove(legacyEndpointPlaceholder);
|
||||
|
||||
QJsonObject options;
|
||||
options[agwOptionsKey::serviceType] = apiPayload.value(apiDefs::key::serviceType).toString();
|
||||
options[agwOptionsKey::userCountryCode] = apiPayload.value(apiDefs::key::userCountryCode).toString();
|
||||
|
||||
const QByteArray payload = QJsonDocument(apiPayload).toJson(QJsonDocument::Compact);
|
||||
const QByteArray optionsJson = QJsonDocument(options).toJson(QJsonDocument::Compact);
|
||||
|
||||
agw_result result = agw_post(m_client, path.toUtf8().constData(), payload.constData(), optionsJson.constData(), 0);
|
||||
|
||||
QByteArray responseBody;
|
||||
if (result.body != nullptr) {
|
||||
responseBody = QByteArray(result.body, static_cast<qsizetype>(result.body_len));
|
||||
}
|
||||
QString trace;
|
||||
if (result.trace != nullptr) {
|
||||
trace = QString::fromUtf8(result.trace, static_cast<qsizetype>(result.trace_len));
|
||||
}
|
||||
const int code = result.code;
|
||||
agw_result_free(&result);
|
||||
|
||||
logAgwTrace(path, code, trace);
|
||||
persistState();
|
||||
|
||||
return qMakePair(mapResultCode(code, responseBody), responseBody);
|
||||
}
|
||||
|
||||
amnezia::ErrorCode GatewayController::mapResultCode(const int code, const QByteArray &responseBody)
|
||||
{
|
||||
switch (code) {
|
||||
case AGW_OK: return apiUtils::checkApiResponseErrors(responseBody);
|
||||
case AGW_CANCELLED:
|
||||
case AGW_ERR_TIMEOUT: return amnezia::ErrorCode::ApiConfigTimeoutError;
|
||||
case AGW_ERR_SSL: return amnezia::ErrorCode::ApiConfigSslError;
|
||||
case AGW_ERR_CONFIG: return amnezia::ErrorCode::ApiMissingAgwPublicKey;
|
||||
case AGW_ERR_DECRYPT: return amnezia::ErrorCode::ApiConfigDecryptionError;
|
||||
default: return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||
}
|
||||
}
|
||||
|
||||
void GatewayController::prepareRequest()
|
||||
{
|
||||
#ifdef Q_OS_IOS
|
||||
IosController::Instance()->requestInetAccess();
|
||||
QThread::msleep(10);
|
||||
#endif
|
||||
|
||||
encRequestData.request.setTransferTimeout(m_requestTimeoutMsecs);
|
||||
encRequestData.request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
encRequestData.request.setRawHeader(QString("X-Client-Request-ID").toUtf8(), QUuid::createUuid().toString(QUuid::WithoutBraces).toUtf8());
|
||||
encRequestData.request.setUrl(endpoint.arg(m_proxyUrl.isEmpty() ? m_gatewayEndpoint : m_proxyUrl));
|
||||
|
||||
// bypass killSwitch exceptions for API-gateway
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
if (m_isStrictKillSwitchEnabled) {
|
||||
QString host = QUrl(encRequestData.request.url()).host();
|
||||
QString ip = NetworkUtilities::getIPAddress(host);
|
||||
if (!m_isStrictKillSwitchEnabled) {
|
||||
return;
|
||||
}
|
||||
QStringList ips;
|
||||
for (const QString &host : requestHosts()) {
|
||||
const QString ip = NetworkUtilities::getIPAddress(host);
|
||||
if (!ip.isEmpty()) {
|
||||
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
QRemoteObjectPendingReply<bool> reply = iface->addKillSwitchAllowedRange(QStringList { ip });
|
||||
if (!reply.waitForFinished(1000) || !reply.returnValue())
|
||||
qWarning() << "GatewayController::prepareRequest(): Failed to execute remote addKillSwitchAllowedRange call";
|
||||
});
|
||||
ips.append(ip);
|
||||
}
|
||||
}
|
||||
if (ips.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
QRemoteObjectPendingReply<bool> reply = iface->addKillSwitchAllowedRange(ips);
|
||||
if (!reply.waitForFinished(1000) || !reply.returnValue()) {
|
||||
qWarning() << "GatewayController::prepareRequest(): Failed to execute remote addKillSwitchAllowedRange call";
|
||||
}
|
||||
});
|
||||
#endif
|
||||
|
||||
encRequestData.key = CryptoUtils::generateRandomBytes(32);
|
||||
encRequestData.iv = CryptoUtils::generateRandomBytes(32);
|
||||
encRequestData.salt = CryptoUtils::generateRandomBytes(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 = CryptoUtils::rsaEncrypt(QJsonDocument(keyPayload).toJson(), publicKey, RSA_PKCS1_PADDING);
|
||||
EVP_PKEY_free(publicKey);
|
||||
|
||||
QByteArray encryptedApiPayload = CryptoUtils::encryptAes256Cbc(QJsonDocument(apiPayload).toJson(), encRequestData.key, encRequestData.iv);
|
||||
|
||||
if (encryptedKeyPayload.isEmpty() || encryptedApiPayload.isEmpty()) {
|
||||
qCritical() << "error when encrypting the request body";
|
||||
encRequestData.errorCode = ErrorCode::ApiConfigDecryptionError;
|
||||
return encRequestData;
|
||||
}
|
||||
|
||||
QJsonObject requestBody;
|
||||
requestBody[apiDefs::key::keyPayload] = QString(encryptedKeyPayload.toBase64());
|
||||
requestBody[apiDefs::key::apiPayload] = QString(encryptedApiPayload.toBase64());
|
||||
|
||||
encRequestData.requestBody = QJsonDocument(requestBody).toJson();
|
||||
return encRequestData;
|
||||
}
|
||||
|
||||
GatewayController::DecryptionResult GatewayController::tryDecryptResponseBody(const QByteArray &encryptedResponseBody,
|
||||
QNetworkReply::NetworkError replyError, const QByteArray &key,
|
||||
const QByteArray &iv, const QByteArray &salt)
|
||||
QStringList GatewayController::requestHosts() const
|
||||
{
|
||||
DecryptionResult result;
|
||||
result.decryptedBody = encryptedResponseBody;
|
||||
result.isDecryptionSuccessful = false;
|
||||
|
||||
QByteArray decrypted = CryptoUtils::decryptAes256Cbc(encryptedResponseBody, key, iv);
|
||||
if (!decrypted.isEmpty()) {
|
||||
result.decryptedBody = decrypted;
|
||||
result.isDecryptionSuccessful = true;
|
||||
} else {
|
||||
result.decryptedBody = encryptedResponseBody;
|
||||
result.isDecryptionSuccessful = false;
|
||||
QStringList hosts;
|
||||
if (m_client != 0) {
|
||||
char *state = agw_export_state(m_client);
|
||||
if (state != nullptr) {
|
||||
const QString workingProxy = QJsonDocument::fromJson(QByteArray(state)).object().value(agwStateKey::workingProxy).toString();
|
||||
agw_string_free(state);
|
||||
if (!workingProxy.isEmpty()) {
|
||||
hosts.append(QUrl(workingProxy).host());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
hosts.append(QUrl(m_gatewayEndpoint).host());
|
||||
hosts.removeAll(QString());
|
||||
hosts.removeDuplicates();
|
||||
return hosts;
|
||||
}
|
||||
|
||||
ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody)
|
||||
void GatewayController::persistState()
|
||||
{
|
||||
EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload);
|
||||
if (encRequestData.errorCode != ErrorCode::NoError) {
|
||||
return encRequestData.errorCode;
|
||||
}
|
||||
|
||||
QNetworkReply *reply = amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody);
|
||||
|
||||
QEventLoop wait;
|
||||
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
|
||||
|
||||
QList<QSslError> sslErrors;
|
||||
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
|
||||
QByteArray encryptedResponseBody = reply->readAll();
|
||||
QString replyErrorString = reply->errorString();
|
||||
auto replyError = reply->error();
|
||||
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
auto decryptionResult =
|
||||
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
|
||||
|
||||
if (sslErrors.isEmpty() && shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
|
||||
auto requestFunction = [&encRequestData, &encryptedResponseBody](const QString &url) {
|
||||
encRequestData.request.setUrl(url);
|
||||
return amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody);
|
||||
};
|
||||
|
||||
auto replyProcessingFunction = [&encryptedResponseBody, &replyErrorString, &replyError, &httpStatusCode, &sslErrors, &encRequestData,
|
||||
&decryptionResult, this](QNetworkReply *reply, const QList<QSslError> &nestedSslErrors) {
|
||||
encryptedResponseBody = reply->readAll();
|
||||
replyErrorString = reply->errorString();
|
||||
replyError = reply->error();
|
||||
httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
|
||||
decryptionResult =
|
||||
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
|
||||
|
||||
if (!sslErrors.isEmpty()
|
||||
|| shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
|
||||
sslErrors = nestedSslErrors;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString("");
|
||||
auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString("");
|
||||
bypassProxy(endpoint, serviceType, userCountryCode, requestFunction, replyProcessingFunction);
|
||||
}
|
||||
|
||||
responseBody = decryptionResult.decryptedBody;
|
||||
const auto errorCode =
|
||||
apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, responseBody);
|
||||
if (errorCode) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
if (!decryptionResult.isDecryptionSuccessful) {
|
||||
qCritical() << "error when decrypting the request body";
|
||||
return ErrorCode::ApiConfigDecryptionError;
|
||||
}
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString &endpoint, const QJsonObject apiPayload)
|
||||
{
|
||||
auto promise = QSharedPointer<QPromise<QPair<ErrorCode, QByteArray>>>::create();
|
||||
promise->start();
|
||||
|
||||
EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload);
|
||||
if (encRequestData.errorCode != ErrorCode::NoError) {
|
||||
promise->addResult(qMakePair(encRequestData.errorCode, QByteArray()));
|
||||
promise->finish();
|
||||
return promise->future();
|
||||
}
|
||||
|
||||
QNetworkReply *reply = amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody);
|
||||
|
||||
auto sslErrors = QSharedPointer<QList<QSslError>>::create();
|
||||
|
||||
connect(reply, &QNetworkReply::sslErrors, [sslErrors](const QList<QSslError> &errors) { *sslErrors = errors; });
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [promise, sslErrors, encRequestData, endpoint, apiPayload, reply, this]() mutable {
|
||||
QByteArray encryptedResponseBody = reply->readAll();
|
||||
QString replyErrorString = reply->errorString();
|
||||
auto replyError = reply->error();
|
||||
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
auto decryptionResult =
|
||||
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
|
||||
|
||||
auto processResponse = [promise, encRequestData](const GatewayController::DecryptionResult &decryptionResult,
|
||||
const QList<QSslError> &sslErrors, QNetworkReply::NetworkError replyError,
|
||||
const QString &replyErrorString, int httpStatusCode) {
|
||||
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode,
|
||||
decryptionResult.decryptedBody);
|
||||
if (errorCode) {
|
||||
promise->addResult(qMakePair(errorCode, decryptionResult.decryptedBody));
|
||||
promise->finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!decryptionResult.isDecryptionSuccessful) {
|
||||
qCritical() << "error when decrypting the request body";
|
||||
promise->addResult(qMakePair(ErrorCode::ApiConfigDecryptionError, QByteArray()));
|
||||
promise->finish();
|
||||
return;
|
||||
}
|
||||
|
||||
promise->addResult(qMakePair(ErrorCode::NoError, decryptionResult.decryptedBody));
|
||||
promise->finish();
|
||||
};
|
||||
|
||||
if (sslErrors->isEmpty() && shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
|
||||
auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString("");
|
||||
auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString("");
|
||||
|
||||
QStringList primaryBaseUrls;
|
||||
QStringList fallbackBaseUrls;
|
||||
if (m_isDevEnvironment) {
|
||||
primaryBaseUrls = QString(DEV_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
} else {
|
||||
primaryBaseUrls = QString(PROD_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
fallbackBaseUrls = QString(FALLBACK_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
}
|
||||
std::random_device randomDevice;
|
||||
std::mt19937 generator(randomDevice());
|
||||
std::shuffle(primaryBaseUrls.begin(), primaryBaseUrls.end(), generator);
|
||||
std::shuffle(fallbackBaseUrls.begin(), fallbackBaseUrls.end(), generator);
|
||||
|
||||
auto appendStorageUrls = [&serviceType, &userCountryCode](const QStringList &baseUrls, QStringList &target) {
|
||||
if (!serviceType.isEmpty()) {
|
||||
for (const auto &baseUrl : baseUrls) {
|
||||
QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8();
|
||||
target.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) + ".json");
|
||||
}
|
||||
}
|
||||
for (const auto &baseUrl : baseUrls) {
|
||||
target.push_back(baseUrl + "endpoints.json");
|
||||
}
|
||||
};
|
||||
|
||||
QStringList proxyStorageUrls;
|
||||
appendStorageUrls(primaryBaseUrls, proxyStorageUrls);
|
||||
appendStorageUrls(fallbackBaseUrls, proxyStorageUrls);
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
} else {
|
||||
processResponse(decryptionResult, *sslErrors, replyError, replyErrorString, httpStatusCode);
|
||||
}
|
||||
});
|
||||
|
||||
return promise->future();
|
||||
}
|
||||
|
||||
QStringList GatewayController::getProxyUrls(const QString &serviceType, const QString &userCountryCode)
|
||||
{
|
||||
QNetworkRequest request;
|
||||
request.setTransferTimeout(proxyStorageRequestTimeoutMsecs);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
|
||||
QEventLoop wait;
|
||||
QList<QSslError> sslErrors;
|
||||
QNetworkReply *reply;
|
||||
|
||||
QStringList primaryBaseUrls;
|
||||
QStringList fallbackBaseUrls;
|
||||
if (m_isDevEnvironment) {
|
||||
primaryBaseUrls = QString(DEV_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
} else {
|
||||
primaryBaseUrls = QString(PROD_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
fallbackBaseUrls = QString(FALLBACK_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
}
|
||||
|
||||
std::random_device randomDevice;
|
||||
std::mt19937 generator(randomDevice());
|
||||
std::shuffle(primaryBaseUrls.begin(), primaryBaseUrls.end(), generator);
|
||||
std::shuffle(fallbackBaseUrls.begin(), fallbackBaseUrls.end(), generator);
|
||||
|
||||
auto appendStorageUrls = [&serviceType, &userCountryCode](const QStringList &baseUrls, QStringList &target) {
|
||||
if (!serviceType.isEmpty()) {
|
||||
for (const auto &baseUrl : baseUrls) {
|
||||
QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8();
|
||||
target.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) + ".json");
|
||||
}
|
||||
}
|
||||
for (const auto &baseUrl : baseUrls) {
|
||||
target.push_back(baseUrl + "endpoints.json");
|
||||
}
|
||||
};
|
||||
|
||||
QStringList proxyStorageUrls;
|
||||
appendStorageUrls(primaryBaseUrls, proxyStorageUrls);
|
||||
appendStorageUrls(fallbackBaseUrls, proxyStorageUrls);
|
||||
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);
|
||||
}
|
||||
|
||||
for (const auto &proxyStorageUrl : proxyStorageUrls) {
|
||||
request.setUrl(proxyStorageUrl);
|
||||
reply = amnApp->networkManager()->get(request);
|
||||
|
||||
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
|
||||
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
|
||||
if (reply->error() == QNetworkReply::NetworkError::NoError) {
|
||||
auto encryptedResponseBody = reply->readAll();
|
||||
reply->deleteLater();
|
||||
|
||||
QByteArray responseBody;
|
||||
if (!decryptProxyUrlsPayload(encryptedResponseBody, m_isDevEnvironment, responseBody)) {
|
||||
qCritical() << "error loading private key from environment variables or decrypting payload" << encryptedResponseBody;
|
||||
continue;
|
||||
}
|
||||
|
||||
auto endpointsArray = QJsonDocument::fromJson(responseBody).array();
|
||||
|
||||
QStringList endpoints;
|
||||
for (const auto &endpoint : endpointsArray) {
|
||||
endpoints.push_back(endpoint.toString());
|
||||
}
|
||||
m_appSettingsRepository->writeGatewayProxyUrls(proxyUrlsCacheKey, encryptedResponseBody);
|
||||
|
||||
return endpoints;
|
||||
} else {
|
||||
auto replyError = reply->error();
|
||||
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
qDebug() << replyError;
|
||||
qDebug() << httpStatusCode;
|
||||
qDebug() << "go to the next storage endpoint";
|
||||
|
||||
reply->deleteLater();
|
||||
}
|
||||
}
|
||||
return readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment);
|
||||
}
|
||||
|
||||
bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody,
|
||||
bool isDecryptionSuccessful)
|
||||
{
|
||||
const QByteArray &responseBody = decryptedResponseBody;
|
||||
|
||||
int apiHttpStatus = -1;
|
||||
QString apiErrorMessage;
|
||||
if (isDecryptionSuccessful) {
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
|
||||
if (jsonDoc.isObject()) {
|
||||
QJsonObject jsonObj = jsonDoc.object();
|
||||
apiHttpStatus = jsonObj.value("http_status").toInt(-1);
|
||||
apiErrorMessage = jsonObj.value(QStringLiteral("message")).toString().trimmed();
|
||||
}
|
||||
} else {
|
||||
qDebug() << "failed to decrypt the data";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (replyError == QNetworkReply::NetworkError::OperationCanceledError || replyError == QNetworkReply::NetworkError::TimeoutError) {
|
||||
qDebug() << "timeout occurred";
|
||||
qDebug() << replyError;
|
||||
return true;
|
||||
}
|
||||
if (responseBody.contains("html")) {
|
||||
qDebug() << "the response contains an html tag";
|
||||
return true;
|
||||
}
|
||||
if (apiHttpStatus == httpStatusCodeRequestTimeout) {
|
||||
return false;
|
||||
}
|
||||
if (apiHttpStatus == httpStatusCodeNotFound) {
|
||||
if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2)
|
||||
|| responseBody.contains(errorResponsePattern3) || responseBody.contains(errorResponsePatternQrSessionNotFound)
|
||||
|| responseBody.contains(errorResponsePatternSessionNotFound)) {
|
||||
return false;
|
||||
} else {
|
||||
qDebug() << replyError;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (apiHttpStatus == httpStatusCodeNotImplemented) {
|
||||
if (responseBody.contains(updateRequestResponsePattern)) {
|
||||
return false;
|
||||
} else {
|
||||
qDebug() << replyError;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (apiHttpStatus == httpStatusCodeConflict) {
|
||||
return false;
|
||||
}
|
||||
if (apiHttpStatus == httpStatusCodePaymentRequired) {
|
||||
return false;
|
||||
}
|
||||
if (apiHttpStatus == httpStatusCodeUnprocessableEntity) {
|
||||
return apiErrorMessage != unprocessableSubscriptionMessage;
|
||||
}
|
||||
if (replyError != QNetworkReply::NetworkError::NoError) {
|
||||
qDebug() << replyError;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void GatewayController::bypassProxy(const QString &endpoint, const QString &serviceType, const QString &userCountryCode,
|
||||
std::function<QNetworkReply *(const QString &url)> requestFunction,
|
||||
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction)
|
||||
{
|
||||
QStringList proxyUrls = getProxyUrls(serviceType, userCountryCode);
|
||||
std::random_device randomDevice;
|
||||
std::mt19937 generator(randomDevice());
|
||||
std::shuffle(proxyUrls.begin(), proxyUrls.end(), generator);
|
||||
|
||||
QByteArray responseBody;
|
||||
|
||||
auto bypassFunction = [this](const QString &endpoint, const QString &proxyUrl,
|
||||
std::function<QNetworkReply *(const QString &url)> requestFunction,
|
||||
std::function<bool(QNetworkReply * reply, const QList<QSslError> &sslErrors)> replyProcessingFunction) {
|
||||
QEventLoop wait;
|
||||
QList<QSslError> sslErrors;
|
||||
|
||||
qDebug() << "go to the next proxy endpoint";
|
||||
QNetworkReply *reply = requestFunction(endpoint.arg(proxyUrl));
|
||||
|
||||
QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
|
||||
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
|
||||
auto result = replyProcessingFunction(reply, sslErrors);
|
||||
reply->deleteLater();
|
||||
return result;
|
||||
};
|
||||
|
||||
if (m_proxyUrl.isEmpty()) {
|
||||
QNetworkRequest request;
|
||||
request.setTransferTimeout(1000);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
|
||||
QEventLoop wait;
|
||||
QList<QSslError> sslErrors;
|
||||
QNetworkReply *reply;
|
||||
|
||||
for (const QString &proxyUrl : proxyUrls) {
|
||||
request.setUrl(proxyUrl + "lmbd-health");
|
||||
reply = amnApp->networkManager()->get(request);
|
||||
|
||||
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
|
||||
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
|
||||
if (reply->error() == QNetworkReply::NetworkError::NoError) {
|
||||
reply->deleteLater();
|
||||
|
||||
m_proxyUrl = proxyUrl;
|
||||
if (!m_proxyUrl.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
reply->deleteLater();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_proxyUrl.isEmpty()) {
|
||||
if (bypassFunction(endpoint, m_proxyUrl, requestFunction, replyProcessingFunction)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (const QString &proxyUrl : proxyUrls) {
|
||||
if (bypassFunction(endpoint, proxyUrl, requestFunction, replyProcessingFunction)) {
|
||||
m_proxyUrl = proxyUrl;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
|
||||
const QString &proxyUrlsCacheKey, std::function<void(const QStringList &)> onComplete)
|
||||
{
|
||||
const QByteArray cachedProxyUrlsEncrypted = m_appSettingsRepository->readGatewayProxyUrls(proxyUrlsCacheKey);
|
||||
|
||||
if (currentProxyStorageIndex >= proxyStorageUrls.size()) {
|
||||
onComplete(shuffledProxyUrls(readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment)));
|
||||
if (m_client == 0 || m_appSettingsRepository == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
QNetworkRequest request;
|
||||
request.setTransferTimeout(proxyStorageRequestTimeoutMsecs);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
request.setUrl(proxyStorageUrls[currentProxyStorageIndex]);
|
||||
|
||||
QNetworkReply *reply = amnApp->networkManager()->get(request);
|
||||
|
||||
// connect(reply, &QNetworkReply::sslErrors, this, [state](const QList<QSslError> &e) { *(state->sslErrors) = e; });
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this,
|
||||
[this, proxyStorageUrls, currentProxyStorageIndex, proxyUrlsCacheKey, onComplete, reply]() {
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
QByteArray encrypted = reply->readAll();
|
||||
reply->deleteLater();
|
||||
|
||||
QByteArray responseBody;
|
||||
if (!decryptProxyUrlsPayload(encrypted, m_isDevEnvironment, responseBody)) {
|
||||
qCritical() << "error decrypting payload";
|
||||
QMetaObject::invokeMethod(
|
||||
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, proxyUrlsCacheKey, onComplete); }, Qt::QueuedConnection);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonArray endpointsArray = QJsonDocument::fromJson(responseBody).array();
|
||||
QStringList endpoints;
|
||||
for (const QJsonValue &endpoint : endpointsArray)
|
||||
endpoints.push_back(endpoint.toString());
|
||||
m_appSettingsRepository->writeGatewayProxyUrls(proxyUrlsCacheKey, encrypted);
|
||||
|
||||
onComplete(shuffledProxyUrls(endpoints));
|
||||
return;
|
||||
}
|
||||
|
||||
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
qDebug() << httpStatusCode;
|
||||
qDebug() << "go to the next storage endpoint";
|
||||
reply->deleteLater();
|
||||
QMetaObject::invokeMethod(
|
||||
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, proxyUrlsCacheKey, onComplete); }, Qt::QueuedConnection);
|
||||
});
|
||||
}
|
||||
|
||||
void GatewayController::getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex,
|
||||
std::function<void(const QString &)> onComplete)
|
||||
{
|
||||
if (currentProxyIndex >= proxyUrls.size()) {
|
||||
onComplete("");
|
||||
char *state = agw_export_state(m_client);
|
||||
if (state == nullptr) {
|
||||
return;
|
||||
}
|
||||
const QByteArray blob(state);
|
||||
agw_string_free(state);
|
||||
|
||||
QNetworkRequest request;
|
||||
request.setTransferTimeout(1000);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
request.setUrl(proxyUrls[currentProxyIndex] + "lmbd-health");
|
||||
|
||||
QNetworkReply *reply = amnApp->networkManager()->get(request);
|
||||
|
||||
// connect(reply, &QNetworkReply::sslErrors, this, [state](const QList<QSslError> &e) {
|
||||
// *(state->sslErrors) = e;
|
||||
// });
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [this, proxyUrls, currentProxyIndex, onComplete, reply]() {
|
||||
reply->deleteLater();
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
m_proxyUrl = proxyUrls[currentProxyIndex];
|
||||
onComplete(m_proxyUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "go to the next proxy endpoint";
|
||||
QMetaObject::invokeMethod(this, [=]() { getProxyUrlAsync(proxyUrls, currentProxyIndex + 1, onComplete); }, Qt::QueuedConnection);
|
||||
});
|
||||
}
|
||||
|
||||
void GatewayController::bypassProxyAsync(
|
||||
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
|
||||
std::function<void(const QByteArray &, bool, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> onComplete)
|
||||
{
|
||||
auto sslErrors = QSharedPointer<QList<QSslError>>::create();
|
||||
if (proxyUrl.isEmpty()) {
|
||||
onComplete(QByteArray(), false, *sslErrors, QNetworkReply::InternalServerError, "empty proxy url", 0);
|
||||
return;
|
||||
if (blob != m_lastPersistedState) {
|
||||
m_appSettingsRepository->writeGatewayProxyUrls(agwStateCacheKey, blob);
|
||||
m_lastPersistedState = blob;
|
||||
}
|
||||
|
||||
QNetworkRequest request = encRequestData.request;
|
||||
request.setUrl(endpoint.arg(proxyUrl));
|
||||
|
||||
QNetworkReply *reply = amnApp->networkManager()->post(request, encRequestData.requestBody);
|
||||
|
||||
connect(reply, &QNetworkReply::sslErrors, this, [sslErrors](const QList<QSslError> &errors) { *sslErrors = errors; });
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [sslErrors, onComplete, encRequestData, reply, this]() {
|
||||
QByteArray encryptedResponseBody = reply->readAll();
|
||||
QString replyErrorString = reply->errorString();
|
||||
auto replyError = reply->error();
|
||||
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
auto decryptionResult =
|
||||
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
|
||||
|
||||
onComplete(decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful, *sslErrors, replyError, replyErrorString,
|
||||
httpStatusCode);
|
||||
});
|
||||
}
|
||||
@@ -1,20 +1,17 @@
|
||||
#ifndef GATEWAYCONTROLLER_H
|
||||
#define GATEWAYCONTROLLER_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QFuture>
|
||||
#include <QNetworkReply>
|
||||
#include <QJsonObject>
|
||||
#include <QObject>
|
||||
#include <QPair>
|
||||
#include <QPromise>
|
||||
#include <QSharedPointer>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
#include "core/utils/errorCodes.h"
|
||||
#include "core/utils/routeModes.h"
|
||||
#include "core/utils/commonStructs.h"
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#include "platforms/ios/ios_controller.h"
|
||||
#endif
|
||||
#include "agw.h"
|
||||
|
||||
class SecureAppSettingsRepository;
|
||||
|
||||
@@ -26,51 +23,26 @@ public:
|
||||
explicit GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
|
||||
const bool isStrictKillSwitchEnabled, SecureAppSettingsRepository *appSettingsRepository,
|
||||
QObject *parent = nullptr);
|
||||
~GatewayController() override;
|
||||
|
||||
amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody);
|
||||
QFuture<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject apiPayload);
|
||||
|
||||
private:
|
||||
struct EncryptedRequestData
|
||||
{
|
||||
QNetworkRequest request;
|
||||
QByteArray requestBody;
|
||||
QByteArray key;
|
||||
QByteArray iv;
|
||||
QByteArray salt;
|
||||
amnezia::ErrorCode errorCode;
|
||||
};
|
||||
void prepareRequest();
|
||||
QStringList requestHosts() const;
|
||||
|
||||
struct DecryptionResult
|
||||
{
|
||||
QByteArray decryptedBody;
|
||||
bool isDecryptionSuccessful;
|
||||
};
|
||||
QPair<amnezia::ErrorCode, QByteArray> executePost(const QString &endpoint, const QJsonObject &apiPayload);
|
||||
static amnezia::ErrorCode mapResultCode(const int code, const QByteArray &responseBody);
|
||||
void persistState();
|
||||
|
||||
EncryptedRequestData prepareRequest(const QString &endpoint, const QJsonObject &apiPayload);
|
||||
DecryptionResult tryDecryptResponseBody(const QByteArray &encryptedResponseBody, QNetworkReply::NetworkError replyError,
|
||||
const QByteArray &key, const QByteArray &iv, const QByteArray &salt);
|
||||
|
||||
QStringList getProxyUrls(const QString &serviceType, const QString &userCountryCode);
|
||||
bool shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody, bool isDecryptionSuccessful);
|
||||
void bypassProxy(const QString &endpoint, const QString &serviceType, const QString &userCountryCode,
|
||||
std::function<QNetworkReply *(const QString &url)> requestFunction,
|
||||
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction);
|
||||
|
||||
void getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
|
||||
const QString &proxyUrlsCacheKey, std::function<void(const QStringList &)> onComplete);
|
||||
void getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex, std::function<void(const QString &)> onComplete);
|
||||
void bypassProxyAsync(
|
||||
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
|
||||
std::function<void(const QByteArray &, bool, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> onComplete);
|
||||
|
||||
int m_requestTimeoutMsecs;
|
||||
QString m_gatewayEndpoint;
|
||||
bool m_isDevEnvironment = false;
|
||||
bool m_isStrictKillSwitchEnabled = false;
|
||||
SecureAppSettingsRepository *m_appSettingsRepository = nullptr;
|
||||
|
||||
inline static QString m_proxyUrl;
|
||||
agw_client_handle m_client = 0;
|
||||
bool m_publicKeyMissing = false;
|
||||
QByteArray m_lastPersistedState;
|
||||
};
|
||||
|
||||
#endif // GATEWAYCONTROLLER_H
|
||||
@@ -0,0 +1,545 @@
|
||||
#include "localProxyController.h"
|
||||
|
||||
#include "core/controllers/api/subscriptionController.h"
|
||||
#include "core/repositories/secureAppSettingsRepository.h"
|
||||
#include "core/repositories/secureServersRepository.h"
|
||||
#include "core/utils/api/apiUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/localProxy/localProxyConstants.h"
|
||||
#include "core/utils/localProxy/portUtils.h"
|
||||
#include "core/utils/localProxy/xrayConfigUtils.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonParseError>
|
||||
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
#include "core/utils/ipcClient.h"
|
||||
|
||||
#include <QHostAddress>
|
||||
#include <QHttpServer>
|
||||
#include <QHttpServerResponse>
|
||||
#include <QTcpServer>
|
||||
#endif
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
LocalProxyController::LocalProxyController(SecureServersRepository *serversRepository,
|
||||
SecureAppSettingsRepository *appSettingsRepository,
|
||||
SubscriptionController *subscriptionController, QObject *parent)
|
||||
: QObject(parent),
|
||||
m_serversRepository(serversRepository),
|
||||
m_appSettingsRepository(appSettingsRepository),
|
||||
m_subscriptionController(subscriptionController)
|
||||
{
|
||||
connect(m_serversRepository, &SecureServersRepository::serverEdited, this, &LocalProxyController::onServerEdited);
|
||||
connect(m_serversRepository, &SecureServersRepository::serverRemoved, this,
|
||||
[this](const QString &serverId, int) { onServerRemoved(serverId); });
|
||||
}
|
||||
|
||||
LocalProxyController::~LocalProxyController()
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
bool LocalProxyController::isEnabled() const
|
||||
{
|
||||
return m_isEnabled;
|
||||
}
|
||||
|
||||
int LocalProxyController::port() const
|
||||
{
|
||||
return m_appSettingsRepository->localProxyPort();
|
||||
}
|
||||
|
||||
QString LocalProxyController::ownerId() const
|
||||
{
|
||||
return m_appSettingsRepository->localProxyOwnerId();
|
||||
}
|
||||
|
||||
bool LocalProxyController::isPortUserDefined() const
|
||||
{
|
||||
return m_appSettingsRepository->isLocalProxyPortUserDefined();
|
||||
}
|
||||
|
||||
int LocalProxyController::activePort() const
|
||||
{
|
||||
return m_activePort;
|
||||
}
|
||||
|
||||
bool LocalProxyController::setPort(int port)
|
||||
{
|
||||
if (port < localProxy::proxyPortMin || port > localProxy::proxyPortMax) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool isPortChanged = m_appSettingsRepository->localProxyPort() != static_cast<quint16>(port);
|
||||
if (isPortChanged) {
|
||||
m_appSettingsRepository->setLocalProxyPort(static_cast<quint16>(port));
|
||||
}
|
||||
m_appSettingsRepository->setLocalProxyPortUserDefined(true);
|
||||
emit stateChanged();
|
||||
|
||||
if (isPortChanged && m_isEnabled) {
|
||||
start();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LocalProxyController::isPortBusy(int port) const
|
||||
{
|
||||
return !isPortAvailable(port);
|
||||
}
|
||||
|
||||
bool LocalProxyController::enable(const QString &ownerId, int port)
|
||||
{
|
||||
if (!isSupported() || ownerId.isEmpty() || port < localProxy::proxyPortMin || port > localProxy::proxyPortMax) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_isEnabled && m_appSettingsRepository->localProxyOwnerId() != ownerId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isPortAvailable(port) && m_appSettingsRepository->isLocalProxyPortUserDefined()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_appSettingsRepository->localProxyPort() != static_cast<quint16>(port)) {
|
||||
m_appSettingsRepository->setLocalProxyPort(static_cast<quint16>(port));
|
||||
}
|
||||
|
||||
m_appSettingsRepository->setLocalProxyOwnerId(ownerId);
|
||||
m_isEnabled = true;
|
||||
emit stateChanged();
|
||||
|
||||
start();
|
||||
return true;
|
||||
}
|
||||
|
||||
void LocalProxyController::disable()
|
||||
{
|
||||
if (!m_isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "Local proxy is disabled";
|
||||
m_isEnabled = false;
|
||||
stop();
|
||||
emit stateChanged();
|
||||
}
|
||||
|
||||
void LocalProxyController::start()
|
||||
{
|
||||
if (!startApi(localProxy::apiPort)) {
|
||||
disableWithError(tr("Local proxy failed to start. Check if the port is available"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!syncXray()) {
|
||||
disableWithError(tr("Couldn’t start the proxy due to an internal error. Try restarting the app"));
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "Local proxy is running on 127.0.0.1:" << m_apiPort;
|
||||
}
|
||||
|
||||
void LocalProxyController::onServerEdited(const QString &serverId)
|
||||
{
|
||||
if (m_apiPort == 0 || m_appSettingsRepository->localProxyOwnerId() != serverId) {
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "Owner server edited, restarting Xray";
|
||||
if (restartXray()) {
|
||||
m_appliedProxyPort = m_appSettingsRepository->localProxyPort();
|
||||
} else {
|
||||
disableWithError(tr("Couldn’t start the proxy due to an internal error. Try restarting the app"));
|
||||
}
|
||||
}
|
||||
|
||||
void LocalProxyController::onServerRemoved(const QString &serverId)
|
||||
{
|
||||
if (m_appSettingsRepository->localProxyOwnerId() != serverId) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_appSettingsRepository->setLocalProxyOwnerId(QString());
|
||||
if (m_isEnabled) {
|
||||
m_isEnabled = false;
|
||||
stop();
|
||||
}
|
||||
emit stateChanged();
|
||||
}
|
||||
|
||||
void LocalProxyController::onVpnConnectionRequested()
|
||||
{
|
||||
if (!m_isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
disable();
|
||||
emit stoppedByVpn(tr("Local proxy stopped because VPN was turned on"));
|
||||
}
|
||||
|
||||
void LocalProxyController::stop()
|
||||
{
|
||||
stopXray();
|
||||
stopApi();
|
||||
m_appliedProxyPort = 0;
|
||||
}
|
||||
|
||||
bool LocalProxyController::syncXray()
|
||||
{
|
||||
const quint16 proxyPort = m_appSettingsRepository->localProxyPort();
|
||||
|
||||
bool synced;
|
||||
if (!isXrayEngineRunning()) {
|
||||
synced = startXray();
|
||||
} else if (m_appliedProxyPort != proxyPort) {
|
||||
qDebug() << "Proxy port changed from" << m_appliedProxyPort << "to" << proxyPort;
|
||||
synced = restartXray();
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (synced) {
|
||||
m_appliedProxyPort = proxyPort;
|
||||
}
|
||||
return synced;
|
||||
}
|
||||
|
||||
bool LocalProxyController::startXray()
|
||||
{
|
||||
if (isXrayEngineRunning()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto config = buildXrayConfig();
|
||||
if (!config) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!startXrayEngine(config->serializedConfig)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
qDebug() << "Xray started on port" << config->proxyPort;
|
||||
setActivePort(config->proxyPort);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LocalProxyController::stopXray()
|
||||
{
|
||||
if (!stopXrayEngine()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setActivePort(0);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LocalProxyController::restartXray()
|
||||
{
|
||||
qDebug() << "Restarting Xray with updated config";
|
||||
|
||||
if (!stopXray()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return startXray();
|
||||
}
|
||||
|
||||
void LocalProxyController::disableWithError(const QString &message)
|
||||
{
|
||||
qWarning() << message;
|
||||
|
||||
m_isEnabled = false;
|
||||
stop();
|
||||
emit stateChanged();
|
||||
emit startFailed(message);
|
||||
}
|
||||
|
||||
void LocalProxyController::setActivePort(int port)
|
||||
{
|
||||
if (m_activePort == port) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_activePort = port;
|
||||
emit stateChanged();
|
||||
}
|
||||
|
||||
int LocalProxyController::runningProxyPort() const
|
||||
{
|
||||
return isXrayEngineRunning() ? m_activePort : 0;
|
||||
}
|
||||
|
||||
std::optional<LocalProxyController::XrayConfig> LocalProxyController::buildXrayConfig() const
|
||||
{
|
||||
const QString ownerId = m_appSettingsRepository->localProxyOwnerId();
|
||||
if (ownerId.isEmpty()) {
|
||||
qWarning() << "Local proxy owner server id is not configured";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto ownerServer = m_serversRepository->serverJsonById(ownerId);
|
||||
if (!ownerServer) {
|
||||
qWarning() << "Owner server with id" << ownerId << "not found";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (!apiUtils::isPremiumServer(*ownerServer)) {
|
||||
qWarning() << "Server" << ownerServer->value(configKey::name).toString() << "is not premium, local proxy is unavailable";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto serializedConfig = xrayConfigUtils::extractLastConfig(*ownerServer);
|
||||
if (!serializedConfig) {
|
||||
serializedConfig = fetchXrayConfigFromGateway(ownerId);
|
||||
if (!serializedConfig) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(serializedConfig->toUtf8(), &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !doc.isObject()) {
|
||||
qWarning() << "Failed to parse Xray config JSON:" << parseError.errorString();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto proxyPort = selectProxyPort();
|
||||
if (!proxyPort) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
QJsonObject xrayConfig = doc.object();
|
||||
if (!xrayConfigUtils::setInboundPort(xrayConfig, *proxyPort)) {
|
||||
qWarning() << "Failed to override local proxy inbound port, using original config";
|
||||
return XrayConfig { *serializedConfig, 0 };
|
||||
}
|
||||
|
||||
return XrayConfig { QString::fromUtf8(QJsonDocument(xrayConfig).toJson(QJsonDocument::Compact)), *proxyPort };
|
||||
}
|
||||
|
||||
std::optional<int> LocalProxyController::selectProxyPort() const
|
||||
{
|
||||
int port = m_appSettingsRepository->localProxyPort();
|
||||
if (port < localProxy::proxyPortMin || port > localProxy::proxyPortMax) {
|
||||
port = localProxy::defaultProxyPort;
|
||||
}
|
||||
|
||||
const auto isAvailable = [this](int p) { return isPortAvailable(p); };
|
||||
if (portUtils::waitForPort(port, localProxy::portReleaseWaitMs, isAvailable)) {
|
||||
return port;
|
||||
}
|
||||
|
||||
if (m_appSettingsRepository->isLocalProxyPortUserDefined()) {
|
||||
qWarning() << "Local proxy port" << port << "is already in use";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<int> freePort;
|
||||
if (port != localProxy::defaultProxyPort && isPortAvailable(localProxy::defaultProxyPort)) {
|
||||
freePort = localProxy::defaultProxyPort;
|
||||
} else {
|
||||
freePort = portUtils::findFirstAvailablePort(localProxy::defaultProxyPort + 1, localProxy::proxyPortMax, isAvailable);
|
||||
}
|
||||
|
||||
if (!freePort) {
|
||||
qWarning() << "No available local proxy port in range" << localProxy::defaultProxyPort + 1 << "-"
|
||||
<< localProxy::proxyPortMax;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
qDebug() << "Local proxy port" << port << "is busy, using" << *freePort;
|
||||
return freePort;
|
||||
}
|
||||
|
||||
std::optional<QString> LocalProxyController::fetchXrayConfigFromGateway(const QString &serverId) const
|
||||
{
|
||||
QJsonObject serverConfig;
|
||||
const ErrorCode errorCode = m_subscriptionController->fetchServiceConfig(serverId, configKey::vless, serverConfig);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
qWarning() << "Gateway request failed with error code" << static_cast<int>(errorCode);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto serializedConfig = xrayConfigUtils::extractLastConfig(serverConfig);
|
||||
if (!serializedConfig) {
|
||||
qWarning() << "Gateway response lacks Xray last_config payload";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
qDebug() << "Fetched Xray config from gateway";
|
||||
return serializedConfig;
|
||||
}
|
||||
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
|
||||
bool LocalProxyController::isSupported() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LocalProxyController::startApi(quint16 port)
|
||||
{
|
||||
if (m_httpServer && m_apiPort == port) {
|
||||
return true;
|
||||
}
|
||||
|
||||
stopApi();
|
||||
|
||||
const auto proxyPortValue = [this]() {
|
||||
const int port = runningProxyPort();
|
||||
return port > 0 ? QJsonValue(port) : QJsonValue::Null;
|
||||
};
|
||||
|
||||
auto httpServer = new QHttpServer(this);
|
||||
httpServer->route("/api/v1/up", QHttpServerRequest::Method::Post, [this, proxyPortValue] {
|
||||
const bool started = startXray();
|
||||
return QHttpServerResponse(QJsonObject { { "status", started ? "ok" : "error" }, { "proxyPort", proxyPortValue() } });
|
||||
});
|
||||
httpServer->route("/api/v1/down", QHttpServerRequest::Method::Post, [this] {
|
||||
const bool stopped = stopXray();
|
||||
return QHttpServerResponse(QJsonObject { { "status", stopped ? "ok" : "error" } });
|
||||
});
|
||||
httpServer->route("/api/v1/ping", QHttpServerRequest::Method::Get, [proxyPortValue] {
|
||||
return QHttpServerResponse(QJsonObject { { "status", "ok" }, { "proxyPort", proxyPortValue() } });
|
||||
});
|
||||
|
||||
auto tcpServer = new QTcpServer(httpServer);
|
||||
if (!tcpServer->listen(QHostAddress::LocalHost, port) || !httpServer->bind(tcpServer)) {
|
||||
qWarning() << "Failed to start HTTP API server on port" << port;
|
||||
delete httpServer;
|
||||
return false;
|
||||
}
|
||||
|
||||
m_httpServer = httpServer;
|
||||
m_apiPort = port;
|
||||
qDebug() << "HTTP API server is running on localhost:" << port;
|
||||
return true;
|
||||
}
|
||||
|
||||
void LocalProxyController::stopApi()
|
||||
{
|
||||
delete m_httpServer;
|
||||
m_httpServer = nullptr;
|
||||
m_apiPort = 0;
|
||||
}
|
||||
|
||||
bool LocalProxyController::startXrayEngine(const QString &config)
|
||||
{
|
||||
if (config.trimmed().isEmpty()) {
|
||||
qWarning() << "Xray config content is empty";
|
||||
return false;
|
||||
}
|
||||
|
||||
const qint64 token = IpcClient::withInterface(
|
||||
[&](QSharedPointer<IpcInterfaceReplica> iface) -> qint64 {
|
||||
auto xrayStart = iface->xrayStartOwned(config);
|
||||
if (!xrayStart.waitForFinished()) {
|
||||
return 0;
|
||||
}
|
||||
return xrayStart.returnValue();
|
||||
},
|
||||
[]() -> qint64 { return 0; });
|
||||
|
||||
if (token == 0) {
|
||||
qWarning() << "Failed to start Xray via IPC";
|
||||
return false;
|
||||
}
|
||||
|
||||
m_xrayToken = token;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LocalProxyController::stopXrayEngine()
|
||||
{
|
||||
if (m_xrayToken == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const bool stopped = IpcClient::withInterface(
|
||||
[this](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
auto xrayStop = iface->xrayStopOwned(m_xrayToken);
|
||||
return xrayStop.waitForFinished() && xrayStop.returnValue();
|
||||
},
|
||||
[]() { return false; });
|
||||
|
||||
if (!stopped) {
|
||||
qWarning() << "Failed to stop Xray via IPC";
|
||||
return false;
|
||||
}
|
||||
|
||||
m_xrayToken = 0;
|
||||
qDebug() << "Xray stopped";
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LocalProxyController::isXrayEngineRunning() const
|
||||
{
|
||||
if (m_xrayToken == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return IpcClient::withInterface(
|
||||
[this](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
auto currentToken = iface->xrayCurrentToken();
|
||||
return currentToken.waitForFinished() && currentToken.returnValue() == m_xrayToken;
|
||||
},
|
||||
[]() { return false; });
|
||||
}
|
||||
|
||||
bool LocalProxyController::isPortAvailable(int port) const
|
||||
{
|
||||
if (port < localProxy::proxyPortMin || port > localProxy::proxyPortMax) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return IpcClient::withInterface(
|
||||
[port](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
auto reply = iface->isTcpPortAvailable(port);
|
||||
return reply.waitForFinished() && reply.returnValue();
|
||||
},
|
||||
[port]() { return portUtils::isPortAvailable(port); });
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
bool LocalProxyController::isSupported() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool LocalProxyController::startApi(quint16)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void LocalProxyController::stopApi()
|
||||
{
|
||||
}
|
||||
|
||||
bool LocalProxyController::startXrayEngine(const QString &)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool LocalProxyController::stopXrayEngine()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LocalProxyController::isXrayEngineRunning() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool LocalProxyController::isPortAvailable(int port) const
|
||||
{
|
||||
return portUtils::isPortAvailable(port);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,85 @@
|
||||
#ifndef LOCALPROXYCONTROLLER_H
|
||||
#define LOCALPROXYCONTROLLER_H
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
class QHttpServer;
|
||||
class SecureServersRepository;
|
||||
class SecureAppSettingsRepository;
|
||||
class SubscriptionController;
|
||||
|
||||
class LocalProxyController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
LocalProxyController(SecureServersRepository *serversRepository, SecureAppSettingsRepository *appSettingsRepository,
|
||||
SubscriptionController *subscriptionController, QObject *parent = nullptr);
|
||||
~LocalProxyController();
|
||||
|
||||
bool isSupported() const;
|
||||
bool isEnabled() const;
|
||||
int port() const;
|
||||
QString ownerId() const;
|
||||
bool isPortUserDefined() const;
|
||||
int activePort() const;
|
||||
|
||||
bool setPort(int port);
|
||||
bool isPortBusy(int port) const;
|
||||
bool enable(const QString &ownerId, int port);
|
||||
void disable();
|
||||
|
||||
public slots:
|
||||
void onServerEdited(const QString &serverId);
|
||||
void onServerRemoved(const QString &serverId);
|
||||
void onVpnConnectionRequested();
|
||||
|
||||
signals:
|
||||
void stateChanged();
|
||||
void startFailed(const QString &message);
|
||||
void stoppedByVpn(const QString &message);
|
||||
|
||||
private:
|
||||
struct XrayConfig
|
||||
{
|
||||
QString serializedConfig;
|
||||
int proxyPort = 0;
|
||||
};
|
||||
|
||||
void start();
|
||||
void stop();
|
||||
bool syncXray();
|
||||
bool startXray();
|
||||
bool stopXray();
|
||||
bool restartXray();
|
||||
void disableWithError(const QString &message);
|
||||
void setActivePort(int port);
|
||||
int runningProxyPort() const;
|
||||
|
||||
std::optional<XrayConfig> buildXrayConfig() const;
|
||||
std::optional<int> selectProxyPort() const;
|
||||
std::optional<QString> fetchXrayConfigFromGateway(const QString &serverId) const;
|
||||
|
||||
bool startApi(quint16 port);
|
||||
void stopApi();
|
||||
bool startXrayEngine(const QString &config);
|
||||
bool stopXrayEngine();
|
||||
bool isXrayEngineRunning() const;
|
||||
bool isPortAvailable(int port) const;
|
||||
|
||||
SecureServersRepository *m_serversRepository;
|
||||
SecureAppSettingsRepository *m_appSettingsRepository;
|
||||
SubscriptionController *m_subscriptionController;
|
||||
|
||||
bool m_isEnabled = false;
|
||||
QHttpServer *m_httpServer = nullptr;
|
||||
quint16 m_apiPort = 0;
|
||||
qint64 m_xrayToken = 0;
|
||||
quint16 m_appliedProxyPort = 0;
|
||||
int m_activePort = 0;
|
||||
};
|
||||
|
||||
#endif // LOCALPROXYCONTROLLER_H
|
||||
@@ -0,0 +1,270 @@
|
||||
#include "logsController.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
|
||||
#ifdef AMNEZIA_HAS_QZIPWRITER
|
||||
#include <QBuffer>
|
||||
#include <QtCore/private/qzipwriter_p.h>
|
||||
#else
|
||||
#include <QDateTime>
|
||||
|
||||
#include <array>
|
||||
#endif
|
||||
|
||||
#include <limits>
|
||||
|
||||
#include "logger.h"
|
||||
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
#include "core/utils/swiftBridge.h"
|
||||
#elif defined(Q_OS_ANDROID)
|
||||
#include "platforms/android/android_controller.h"
|
||||
#endif
|
||||
|
||||
#ifndef AMNEZIA_HAS_QZIPWRITER
|
||||
namespace
|
||||
{
|
||||
quint32 crc32(const QByteArray &data)
|
||||
{
|
||||
static const std::array<quint32, 256> table = [] {
|
||||
std::array<quint32, 256> result {};
|
||||
for (quint32 i = 0; i < 256; ++i) {
|
||||
quint32 value = i;
|
||||
for (int bit = 0; bit < 8; ++bit) {
|
||||
value = (value & 1) ? 0xEDB88320u ^ (value >> 1) : value >> 1;
|
||||
}
|
||||
result[i] = value;
|
||||
}
|
||||
return result;
|
||||
}();
|
||||
|
||||
quint32 crc = 0xFFFFFFFFu;
|
||||
for (const char byte : data) {
|
||||
crc = table[(crc ^ static_cast<uchar>(byte)) & 0xFF] ^ (crc >> 8);
|
||||
}
|
||||
return crc ^ 0xFFFFFFFFu;
|
||||
}
|
||||
|
||||
void put16(QByteArray &out, quint16 value)
|
||||
{
|
||||
out.append(static_cast<char>(value & 0xFF));
|
||||
out.append(static_cast<char>(value >> 8));
|
||||
}
|
||||
|
||||
void put32(QByteArray &out, quint32 value)
|
||||
{
|
||||
put16(out, static_cast<quint16>(value & 0xFFFF));
|
||||
put16(out, static_cast<quint16>(value >> 16));
|
||||
}
|
||||
|
||||
QByteArray storedZip(const QList<QPair<QString, QByteArray>> &entries)
|
||||
{
|
||||
const QDateTime now = QDateTime::currentDateTime();
|
||||
const quint16 dosTime = (now.time().hour() << 11) | (now.time().minute() << 5) | (now.time().second() / 2);
|
||||
const quint16 dosDate = ((now.date().year() - 1980) << 9) | (now.date().month() << 5) | now.date().day();
|
||||
|
||||
QByteArray archive;
|
||||
QByteArray centralDirectory;
|
||||
for (const auto &[name, data] : entries) {
|
||||
const QByteArray fileName = name.toUtf8();
|
||||
const quint32 offset = static_cast<quint32>(archive.size());
|
||||
const quint32 size = static_cast<quint32>(data.size());
|
||||
|
||||
QByteArray fields;
|
||||
put16(fields, 10);
|
||||
put16(fields, 0);
|
||||
put16(fields, 0);
|
||||
put16(fields, dosTime);
|
||||
put16(fields, dosDate);
|
||||
put32(fields, crc32(data));
|
||||
put32(fields, size);
|
||||
put32(fields, size);
|
||||
put16(fields, static_cast<quint16>(fileName.size()));
|
||||
put16(fields, 0);
|
||||
|
||||
put32(archive, 0x04034b50);
|
||||
archive.append(fields).append(fileName).append(data);
|
||||
|
||||
put32(centralDirectory, 0x02014b50);
|
||||
put16(centralDirectory, 20);
|
||||
centralDirectory.append(fields);
|
||||
put16(centralDirectory, 0);
|
||||
put16(centralDirectory, 0);
|
||||
put16(centralDirectory, 0);
|
||||
put32(centralDirectory, 0);
|
||||
put32(centralDirectory, offset);
|
||||
centralDirectory.append(fileName);
|
||||
}
|
||||
|
||||
const quint32 centralDirectoryOffset = static_cast<quint32>(archive.size());
|
||||
archive.append(centralDirectory);
|
||||
|
||||
put32(archive, 0x06054b50);
|
||||
put16(archive, 0);
|
||||
put16(archive, 0);
|
||||
put16(archive, static_cast<quint16>(entries.size()));
|
||||
put16(archive, static_cast<quint16>(entries.size()));
|
||||
put32(archive, static_cast<quint32>(centralDirectory.size()));
|
||||
put32(archive, centralDirectoryOffset);
|
||||
put16(archive, 0);
|
||||
return archive;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
QStringList LogsController::files(Stream stream)
|
||||
{
|
||||
#if defined(AMNEZIA_DESKTOP)
|
||||
if (stream == Stream::App) {
|
||||
return { Logger::userLogsFilePath() };
|
||||
}
|
||||
return { Logger::serviceLogsFilePath() };
|
||||
#elif defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
if (stream == Stream::App) {
|
||||
return { QString::fromStdString(SWIFT_BRIDGE_NAMESPACE::swiftAppLogPath()), Logger::userLogsFilePath() };
|
||||
}
|
||||
return { QString::fromStdString(SWIFT_BRIDGE_NAMESPACE::swiftTunnelLogPath()) };
|
||||
#elif defined(Q_OS_ANDROID)
|
||||
return AndroidController::logFiles(static_cast<int>(stream));
|
||||
#endif
|
||||
}
|
||||
|
||||
qint64 LogsController::size(Stream stream)
|
||||
{
|
||||
qint64 total = 0;
|
||||
for (const QString &path : files(stream)) {
|
||||
total += fileSize(path);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
qint64 LogsController::fileSize(const QString &path)
|
||||
{
|
||||
QFile file(path);
|
||||
return file.open(QIODevice::ReadOnly) ? file.size() : 0;
|
||||
}
|
||||
|
||||
QString LogsController::readTail(Stream stream, qint64 *newestFileEnd, qint64 maxBytes)
|
||||
{
|
||||
return QString::fromUtf8(read(stream, maxBytes, newestFileEnd));
|
||||
}
|
||||
|
||||
QByteArray LogsController::readAll(Stream stream)
|
||||
{
|
||||
return read(stream, std::numeric_limits<qint64>::max());
|
||||
}
|
||||
|
||||
QByteArray LogsController::read(Stream stream, qint64 maxBytes, qint64 *newestFileEnd)
|
||||
{
|
||||
QByteArray data = tailOfFiles(files(stream), maxBytes, newestFileEnd);
|
||||
#if defined(Q_OS_ANDROID)
|
||||
if (stream == Stream::App && maxBytes == std::numeric_limits<qint64>::max()) {
|
||||
data.prepend(AndroidController::deviceInfo().toUtf8() + '\n');
|
||||
}
|
||||
#endif
|
||||
return data;
|
||||
}
|
||||
|
||||
void LogsController::clear(Stream stream)
|
||||
{
|
||||
#if defined(AMNEZIA_DESKTOP)
|
||||
if (stream == Stream::App) {
|
||||
Logger::clearLogs(false);
|
||||
} else {
|
||||
Logger::clearServiceLogs();
|
||||
}
|
||||
#elif defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
if (stream == Stream::App) {
|
||||
Logger::clearLogs(false);
|
||||
QFile::resize(QString::fromStdString(SWIFT_BRIDGE_NAMESPACE::swiftAppLogPath()), 0);
|
||||
} else {
|
||||
QFile::resize(QString::fromStdString(SWIFT_BRIDGE_NAMESPACE::swiftTunnelLogPath()), 0);
|
||||
}
|
||||
#elif defined(Q_OS_ANDROID)
|
||||
AndroidController::clearLogStream(static_cast<int>(stream));
|
||||
#endif
|
||||
}
|
||||
|
||||
QByteArray LogsController::zipAll()
|
||||
{
|
||||
return zipFiles({ { QStringLiteral("app.log"), readAll(Stream::App) },
|
||||
{ QStringLiteral("tunnel.log"), readAll(Stream::Tunnel) } });
|
||||
}
|
||||
|
||||
QByteArray LogsController::zipFiles(const QList<QPair<QString, QByteArray>> &entries)
|
||||
{
|
||||
#ifdef AMNEZIA_HAS_QZIPWRITER
|
||||
QBuffer buffer;
|
||||
buffer.open(QIODevice::WriteOnly);
|
||||
|
||||
QZipWriter zip(&buffer);
|
||||
zip.setCompressionPolicy(QZipWriter::AlwaysCompress);
|
||||
for (const auto &[name, data] : entries) {
|
||||
zip.addFile(name, data);
|
||||
}
|
||||
zip.close();
|
||||
|
||||
return buffer.data();
|
||||
#else
|
||||
return storedZip(entries);
|
||||
#endif
|
||||
}
|
||||
|
||||
QByteArray LogsController::tailOfFiles(const QStringList &paths, qint64 maxBytes, qint64 *newestFileEnd)
|
||||
{
|
||||
if (newestFileEnd) {
|
||||
*newestFileEnd = 0;
|
||||
}
|
||||
|
||||
QList<QPair<QString, QByteArray>> chunks;
|
||||
qint64 remaining = maxBytes;
|
||||
|
||||
for (auto it = paths.crbegin(); it != paths.crend() && remaining > 0; ++it) {
|
||||
QFile file(*it);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const qint64 fileSize = file.size();
|
||||
QByteArray chunk;
|
||||
if (fileSize <= remaining) {
|
||||
chunk = file.read(fileSize);
|
||||
remaining -= fileSize;
|
||||
} else {
|
||||
file.seek(fileSize - remaining - 1);
|
||||
chunk = file.read(remaining + 1);
|
||||
const qsizetype newline = chunk.indexOf('\n');
|
||||
qsizetype start = newline + 1;
|
||||
if (newline < 0) {
|
||||
start = 1;
|
||||
while (start < chunk.size() && (static_cast<uchar>(chunk.at(start)) & 0xC0) == 0x80) {
|
||||
++start;
|
||||
}
|
||||
}
|
||||
chunk = chunk.mid(start);
|
||||
remaining = 0;
|
||||
}
|
||||
|
||||
if (newestFileEnd && it == paths.crbegin()) {
|
||||
*newestFileEnd = file.pos();
|
||||
}
|
||||
|
||||
if (!chunk.isEmpty()) {
|
||||
chunks.prepend({ QFileInfo(*it).fileName(), chunk });
|
||||
}
|
||||
}
|
||||
|
||||
if (chunks.size() == 1) {
|
||||
return chunks.first().second;
|
||||
}
|
||||
|
||||
QByteArray result;
|
||||
for (const auto &[fileName, chunk] : chunks) {
|
||||
result += "----- " + fileName.toUtf8() + " -----\n" + chunk;
|
||||
if (!chunk.endsWith('\n')) {
|
||||
result += '\n';
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
#ifndef LOGSCONTROLLER_H
|
||||
#define LOGSCONTROLLER_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QList>
|
||||
#include <QPair>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
class LogsController
|
||||
{
|
||||
public:
|
||||
enum class Stream { App, Tunnel };
|
||||
|
||||
static QStringList files(Stream stream);
|
||||
static qint64 size(Stream stream);
|
||||
static qint64 fileSize(const QString &path);
|
||||
static QString readTail(Stream stream, qint64 *newestFileEnd = nullptr, qint64 maxBytes = 512 * 1024);
|
||||
static QByteArray readAll(Stream stream);
|
||||
static void clear(Stream stream);
|
||||
static QByteArray zipAll();
|
||||
static QByteArray zipFiles(const QList<QPair<QString, QByteArray>> &entries);
|
||||
|
||||
static QByteArray tailOfFiles(const QStringList &paths, qint64 maxBytes, qint64 *newestFileEnd = nullptr);
|
||||
|
||||
private:
|
||||
static QByteArray read(Stream stream, qint64 maxBytes, qint64 *newestFileEnd = nullptr);
|
||||
};
|
||||
|
||||
#endif // LOGSCONTROLLER_H
|
||||
@@ -328,7 +328,7 @@ ExportController::ExportResult ExportController::generateXrayConfig(const QStrin
|
||||
vlessServer.serverName = realitySettings.value(amnezia::protocols::xray::serverName).toString();
|
||||
vlessServer.publicKey = realitySettings.value(amnezia::protocols::xray::publicKey).toString();
|
||||
vlessServer.shortId = realitySettings.value(amnezia::protocols::xray::shortId).toString();
|
||||
vlessServer.fingerprint = realitySettings.value(amnezia::protocols::xray::fingerprint).toString("chrome");
|
||||
vlessServer.fingerprint = realitySettings.value(amnezia::protocols::xray::fingerprint).toString(QString::fromLatin1(amnezia::protocols::xray::defaultFingerprint));
|
||||
vlessServer.spiderX = realitySettings.value(amnezia::protocols::xray::spiderX).toString("");
|
||||
} else if (vlessServer.security == "tls") {
|
||||
QJsonObject tlsSettings = streamSettings.value("tlsSettings").toObject();
|
||||
|
||||
@@ -33,6 +33,18 @@ using namespace ProtocolUtils;
|
||||
|
||||
namespace
|
||||
{
|
||||
bool rejectUnsupportedFormatVersion(ImportController::ImportResult &result)
|
||||
{
|
||||
if (serverConfigUtils::isConfigFormatVersionSupported(result.config)) {
|
||||
return false;
|
||||
}
|
||||
qWarning() << "Config format version" << serverConfigUtils::configFormatVersion(result.config)
|
||||
<< "is newer than supported" << serverConfigUtils::currentConfigFormatVersion;
|
||||
result.errorCode = ErrorCode::ConfigFormatVersionNotSupportedError;
|
||||
result.config = {};
|
||||
return true;
|
||||
}
|
||||
|
||||
ConfigTypes checkConfigFormat(const QString &config)
|
||||
{
|
||||
const QString openVpnConfigPatternCli = "client";
|
||||
@@ -52,6 +64,7 @@ namespace
|
||||
const QString amneziaFreeConfigPattern = "api_key";
|
||||
const QString amneziaPremiumConfigPattern = "auth_data";
|
||||
const QString backupPattern = "Servers/serversList";
|
||||
const QString xraySettingsPattern = "serverConfig";
|
||||
|
||||
if (config.contains(backupPattern)) {
|
||||
return ConfigTypes::Backup;
|
||||
@@ -67,6 +80,8 @@ namespace
|
||||
} else if (config.contains(openVpnConfigPatternCli)
|
||||
&& (config.contains(openVpnConfigPatternDriver1) || config.contains(openVpnConfigPatternDriver2))) {
|
||||
return ConfigTypes::OpenVpn;
|
||||
} else if (QJsonDocument::fromJson(config.toUtf8()).object().value(xraySettingsPattern).isObject()) {
|
||||
return ConfigTypes::XraySettings;
|
||||
}
|
||||
return ConfigTypes::Invalid;
|
||||
}
|
||||
@@ -207,6 +222,10 @@ ImportController::ImportResult ImportController::extractConfigFromData(const QSt
|
||||
case ConfigTypes::Amnezia: {
|
||||
result.config = QJsonDocument::fromJson(config.toUtf8()).object();
|
||||
|
||||
if (rejectUnsupportedFormatVersion(result)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (serverConfigUtils::isServerFromApi(result.config)) {
|
||||
auto apiConfig = result.config.value(apiDefs::key::apiConfig).toObject();
|
||||
apiConfig[apiDefs::key::vpnKey] = data;
|
||||
@@ -231,6 +250,10 @@ ImportController::ImportResult ImportController::extractConfigFromData(const QSt
|
||||
result.errorCode = ErrorCode::ImportBackupFileUseRestoreInstead;
|
||||
return result;
|
||||
}
|
||||
case ConfigTypes::XraySettings: {
|
||||
result.errorCode = ErrorCode::ImportXraySettingsFileUseXrayPage;
|
||||
return result;
|
||||
}
|
||||
case ConfigTypes::Invalid: {
|
||||
result.errorCode = ErrorCode::ImportInvalidConfigError;
|
||||
result.configFileName.clear();
|
||||
@@ -256,6 +279,7 @@ ImportController::ImportResult ImportController::extractConfigFromQr(const QByte
|
||||
if (!dataObj.isEmpty()) {
|
||||
result.config = dataObj;
|
||||
result.configType = ConfigTypes::Amnezia;
|
||||
rejectUnsupportedFormatVersion(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -267,6 +291,7 @@ ImportController::ImportResult ImportController::extractConfigFromQr(const QByte
|
||||
return result;
|
||||
}
|
||||
result.configType = ConfigTypes::Amnezia;
|
||||
rejectUnsupportedFormatVersion(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -284,6 +309,7 @@ ImportController::ImportResult ImportController::extractConfigFromQr(const QByte
|
||||
return result;
|
||||
}
|
||||
result.configType = ConfigTypes::Amnezia;
|
||||
rejectUnsupportedFormatVersion(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -379,6 +405,11 @@ int ImportController::qrChunksTotal() const
|
||||
|
||||
void ImportController::importConfig(const QJsonObject &config)
|
||||
{
|
||||
if (!serverConfigUtils::isConfigFormatVersionSupported(config)) {
|
||||
emit importErrorOccurred(ErrorCode::ConfigFormatVersionNotSupportedError, false);
|
||||
return;
|
||||
}
|
||||
|
||||
ServerCredentials credentials;
|
||||
credentials.hostName = config.value(configKey::hostName).toString();
|
||||
credentials.port = config.value(configKey::port).toInt();
|
||||
@@ -748,8 +779,11 @@ void ImportController::processAmneziaConfig(QJsonObject &config) const
|
||||
}
|
||||
|
||||
QJsonObject jsonConfig = QJsonDocument::fromJson(protocolConfig.toUtf8()).object();
|
||||
jsonConfig[configKey::mtu] =
|
||||
ContainerUtils::isAwgContainer(dockerContainer) ? protocols::awg::defaultMtu : protocols::wireguard::defaultMtu;
|
||||
if (jsonConfig.value(configKey::mtu).toString().trimmed().isEmpty()) {
|
||||
jsonConfig[configKey::mtu] = ContainerUtils::isAwgContainer(dockerContainer)
|
||||
? protocols::awg::defaultMtu
|
||||
: protocols::wireguard::defaultMtu;
|
||||
}
|
||||
|
||||
containerConfig[configKey::lastConfig] = QString(QJsonDocument(jsonConfig).toJson());
|
||||
|
||||
@@ -759,4 +793,3 @@ void ImportController::processAmneziaConfig(QJsonObject &config) const
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ namespace
|
||||
Xray,
|
||||
ShadowSocks,
|
||||
Backup,
|
||||
XraySettings,
|
||||
Invalid
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
#include "core/configurators/configuratorBase.h"
|
||||
#include "core/configurators/xrayConfigurator.h"
|
||||
#include "core/models/protocols/xrayProtocolConfig.h"
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
@@ -60,6 +61,70 @@ namespace
|
||||
{
|
||||
Logger logger("InstallController");
|
||||
|
||||
QString effectiveXrayPort(const XrayProtocolConfig *cfg)
|
||||
{
|
||||
if (!cfg || cfg->serverConfig.port.isEmpty()) {
|
||||
return QString::fromLatin1(protocols::xray::defaultPort);
|
||||
}
|
||||
return cfg->serverConfig.port;
|
||||
}
|
||||
|
||||
QString normalizeXrayRelease(QString raw)
|
||||
{
|
||||
raw = raw.trimmed();
|
||||
if (raw.startsWith(QLatin1String("v"), Qt::CaseInsensitive)) {
|
||||
raw.remove(0, 1);
|
||||
}
|
||||
return raw.trimmed();
|
||||
}
|
||||
|
||||
QString parseXrayVersionOutput(const QString &output)
|
||||
{
|
||||
static const QRegularExpression re(QStringLiteral(R"(Xray\s+v?(\d+\.\d+\.\d+))"),
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
const auto match = re.match(output);
|
||||
if (!match.hasMatch()) {
|
||||
return {};
|
||||
}
|
||||
return match.captured(1);
|
||||
}
|
||||
|
||||
enum class XrayBinaryProbeResult {
|
||||
Match,
|
||||
Mismatch,
|
||||
Failed
|
||||
};
|
||||
|
||||
XrayBinaryProbeResult probeXrayServerBinary(const ServerCredentials &credentials, SshSession &sshSession)
|
||||
{
|
||||
const QString expected =
|
||||
normalizeXrayRelease(QString::fromLatin1(protocols::xray::expectedServerXrayRelease));
|
||||
|
||||
QString stdOut;
|
||||
auto collect = [&stdOut](const QString &data, libssh::Client &) {
|
||||
stdOut += data;
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
const QString script = SshSession::replaceVars(
|
||||
QStringLiteral("sudo docker exec $CONTAINER_NAME xray version 2>/dev/null | head -1"),
|
||||
amnezia::genBaseVars(credentials, DockerContainer::Xray, QString(), QString()));
|
||||
const ErrorCode errorCode = sshSession.runScript(credentials, script, collect, collect);
|
||||
const QString live = parseXrayVersionOutput(stdOut);
|
||||
|
||||
if (errorCode != ErrorCode::NoError || live.isEmpty()) {
|
||||
return XrayBinaryProbeResult::Failed;
|
||||
}
|
||||
if (live != expected) {
|
||||
return XrayBinaryProbeResult::Mismatch;
|
||||
}
|
||||
return XrayBinaryProbeResult::Match;
|
||||
}
|
||||
|
||||
bool xrayServerBinaryMismatchesExpected(const ServerCredentials &credentials, SshSession &sshSession)
|
||||
{
|
||||
return probeXrayServerBinary(credentials, sshSession) == XrayBinaryProbeResult::Mismatch;
|
||||
}
|
||||
|
||||
bool dockerDaemonContainerMissing(const QString &out, const QString &containerDockerName)
|
||||
{
|
||||
if (!out.contains(QLatin1String("Error response from daemon"), Qt::CaseInsensitive)) {
|
||||
@@ -75,15 +140,34 @@ namespace
|
||||
return false;
|
||||
}
|
||||
|
||||
bool containerKeepsIdentityInDataVolume(DockerContainer container)
|
||||
{
|
||||
return container == DockerContainer::MtProxy
|
||||
|| container == DockerContainer::Telemt
|
||||
|| container == DockerContainer::TProxy
|
||||
|| container == DockerContainer::Xray;
|
||||
}
|
||||
|
||||
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 += QLatin1String(
|
||||
"\nfor attempt in 1 2 3 4 5; do"
|
||||
"\n sudo docker volume rm -f $CONTAINER_NAME-data && break"
|
||||
"\n sleep 1"
|
||||
"\ndone"
|
||||
"\necho \"amnezia_volume_left=$(sudo docker volume ls -q -f name=^$CONTAINER_NAME-data$ | head -1)\"");
|
||||
script = SshSession::replaceVars(script, vars);
|
||||
}
|
||||
return script;
|
||||
}
|
||||
|
||||
bool dataVolumeSurvivedRemoval(const QString &out)
|
||||
{
|
||||
static const QRegularExpression reLeft(QStringLiteral("amnezia_volume_left=(\\S+)"));
|
||||
return reLeft.match(out).hasMatch();
|
||||
}
|
||||
}
|
||||
|
||||
InstallController::InstallController(SecureServersRepository *serversRepository,
|
||||
@@ -102,7 +186,7 @@ InstallController::~InstallController()
|
||||
}
|
||||
|
||||
ErrorCode InstallController::setupContainer(const ServerCredentials &credentials, DockerContainer container, ContainerConfig &config,
|
||||
bool isUpdate)
|
||||
bool isUpdate, bool acceptXrayKeyLoss)
|
||||
{
|
||||
SshSession sshSession;
|
||||
ErrorCode e = ErrorCode::NoError;
|
||||
@@ -129,12 +213,38 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
|
||||
if (e)
|
||||
return e;
|
||||
|
||||
QMap<QString, QString> xrayStateToMigrate;
|
||||
if (isUpdate) {
|
||||
if (!acceptXrayKeyLoss) {
|
||||
e = readXrayStateBeforeVolumeMigration(credentials, container, sshSession, xrayStateToMigrate);
|
||||
if (e)
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
||||
const amnezia::ScriptVars removeContainerVars =
|
||||
amnezia::genBaseVars(credentials, container, QString(), QString());
|
||||
const bool removeDataVolume = !isUpdate && (container == DockerContainer::MtProxy
|
||||
|| container == DockerContainer::Telemt || container == DockerContainer::TProxy);
|
||||
sshSession.runScript(credentials, buildRemoveContainerScript(removeContainerVars, removeDataVolume));
|
||||
const bool removeDataVolume = !isUpdate && containerKeepsIdentityInDataVolume(container);
|
||||
QString removeOut;
|
||||
auto collectRemoveOut = [&removeOut](const QString &data, libssh::Client &) {
|
||||
removeOut += data + "\n";
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
const ErrorCode removeError =
|
||||
sshSession.runScript(credentials, buildRemoveContainerScript(removeContainerVars, removeDataVolume),
|
||||
collectRemoveOut, collectRemoveOut);
|
||||
qDebug().noquote() << "InstallController::setupContainer removeContainer finished";
|
||||
|
||||
if (removeDataVolume) {
|
||||
if (removeError != ErrorCode::NoError) {
|
||||
return ErrorCode::ServerDataVolumeNotRemoved;
|
||||
}
|
||||
if (dataVolumeSurvivedRemoval(removeOut)) {
|
||||
return ErrorCode::ServerDataVolumeNotRemoved;
|
||||
}
|
||||
}
|
||||
|
||||
qDebug().noquote() << "buildContainerWorker start";
|
||||
e = buildContainerWorker(credentials, container, config, sshSession);
|
||||
if (e)
|
||||
return e;
|
||||
@@ -143,6 +253,10 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
|
||||
if (e)
|
||||
return e;
|
||||
|
||||
e = restoreXrayStateIntoDataVolume(credentials, container, sshSession, xrayStateToMigrate);
|
||||
if (e)
|
||||
return e;
|
||||
|
||||
e = configureContainerWorker(credentials, container, config, sshSession);
|
||||
if (e)
|
||||
return e;
|
||||
@@ -161,20 +275,41 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
|
||||
}
|
||||
|
||||
ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerContainer container, const ContainerConfig &oldConfig,
|
||||
ContainerConfig &newConfig)
|
||||
ContainerConfig &newConfig, bool acceptXrayKeyLoss)
|
||||
{
|
||||
if (!isUpdateDockerContainerRequired(container, oldConfig, newConfig)) {
|
||||
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
if (!adminConfig.has_value()) {
|
||||
const bool serverSettingsChanged = isUpdateDockerContainerRequired(container, oldConfig, newConfig);
|
||||
|
||||
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
if (!adminConfig.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
ServerCredentials credentials = adminConfig->credentials();
|
||||
SshSession sshSession;
|
||||
|
||||
bool reinstallRequired = false;
|
||||
bool portReinstall = false;
|
||||
bool binaryMismatch = false;
|
||||
if (serverSettingsChanged) {
|
||||
if (!credentials.isValid()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
portReinstall = isReinstallContainerRequired(container, oldConfig, newConfig);
|
||||
reinstallRequired = portReinstall;
|
||||
}
|
||||
if (container == DockerContainer::Xray && serverSettingsChanged) {
|
||||
if (credentials.isValid()) {
|
||||
binaryMismatch = xrayServerBinaryMismatchesExpected(credentials, sshSession);
|
||||
if (binaryMismatch) {
|
||||
reinstallRequired = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!serverSettingsChanged && !reinstallRequired) {
|
||||
if (container == DockerContainer::MtProxy) {
|
||||
ServerCredentials credentials = adminConfig->credentials();
|
||||
SshSession sshSession;
|
||||
MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
|
||||
} else if (container == DockerContainer::Telemt) {
|
||||
ServerCredentials credentials = adminConfig->credentials();
|
||||
SshSession sshSession;
|
||||
TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
|
||||
} else if (container == DockerContainer::TProxy) {
|
||||
ServerCredentials credentials = adminConfig->credentials();
|
||||
@@ -186,22 +321,24 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
if (!adminConfig.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
ServerCredentials credentials = adminConfig->credentials();
|
||||
if (!credentials.isValid()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
SshSession sshSession;
|
||||
|
||||
bool reinstallRequired = isReinstallContainerRequired(container, oldConfig, newConfig);
|
||||
qDebug() << "InstallController::updateServerConfig for container" << container << "reinstall required is" << reinstallRequired;
|
||||
|
||||
ErrorCode errorCode = ErrorCode::NoError;
|
||||
if (reinstallRequired) {
|
||||
errorCode = setupContainer(credentials, container, newConfig, true);
|
||||
if (container == DockerContainer::Xray) {
|
||||
const QString oldPort = effectiveXrayPort(oldConfig.getXrayProtocolConfig());
|
||||
const QString newPort = effectiveXrayPort(newConfig.getXrayProtocolConfig());
|
||||
if (oldPort != newPort) {
|
||||
errorCode = isServerPortBusy(credentials, container, newConfig, sshSession);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
errorCode = setupContainer(credentials, container, newConfig, true, acceptXrayKeyLoss);
|
||||
|
||||
// Reinstall pulls the latest container image, so the server runs the latest protocol version
|
||||
if (errorCode == ErrorCode::NoError && container == DockerContainer::Awg2) {
|
||||
@@ -209,7 +346,12 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
|
||||
awgConfig->serverConfig.protocolVersion = protocols::awg::awgV3;
|
||||
}
|
||||
}
|
||||
} else if (container != DockerContainer::Xray && container != DockerContainer::SSXray) {
|
||||
} else if (container == DockerContainer::Xray) {
|
||||
DnsSettings dnsSettings = { m_appSettingsRepository->primaryDns(), m_appSettingsRepository->secondaryDns() };
|
||||
XrayConfigurator xrayConfigurator(&sshSession);
|
||||
errorCode = xrayConfigurator.writeServerConfigForSetup(credentials, container, newConfig, dnsSettings,
|
||||
true);
|
||||
} else if (container != DockerContainer::SSXray) {
|
||||
errorCode = configureContainerWorker(credentials, container, newConfig, sshSession);
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
errorCode = startupContainerWorker(credentials, container, newConfig, sshSession);
|
||||
@@ -232,7 +374,9 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
|
||||
TProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
|
||||
}
|
||||
if (reinstallRequired) {
|
||||
clearCachedProfile(serverId, container);
|
||||
if (container != DockerContainer::Xray) {
|
||||
clearCachedProfile(serverId, container);
|
||||
}
|
||||
}
|
||||
adminConfig->updateContainerConfig(container, newConfig);
|
||||
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
|
||||
@@ -441,6 +585,9 @@ ErrorCode InstallController::prepareContainerConfig(DockerContainer container, c
|
||||
void InstallController::adminAppendRequested(const QString &serverId, DockerContainer container,
|
||||
const ContainerConfig &containerConfig, const QString &clientName)
|
||||
{
|
||||
if (container == DockerContainer::Xray) {
|
||||
return;
|
||||
}
|
||||
if (ContainerUtils::containerService(container) == ServiceType::Other
|
||||
|| !containerConfig.protocolConfig.hasClientConfig()) {
|
||||
return;
|
||||
@@ -570,6 +717,10 @@ ErrorCode InstallController::configureContainerWorker(const ServerCredentials &c
|
||||
return ErrorCode::ServerContainerMissingError;
|
||||
}
|
||||
|
||||
if (stdOut.contains(QLatin1String("amnezia_xray_keygen=failed"))) {
|
||||
return ErrorCode::XrayKeyGenerationFailed;
|
||||
}
|
||||
|
||||
updateContainerConfigAfterInstallation(container, config, stdOut);
|
||||
|
||||
if (container == DockerContainer::MtProxy) {
|
||||
@@ -606,6 +757,127 @@ ErrorCode InstallController::startupContainerWorker(const ServerCredentials &cre
|
||||
baseVars));
|
||||
}
|
||||
|
||||
ErrorCode InstallController::readXrayStateBeforeVolumeMigration(const ServerCredentials &credentials,
|
||||
DockerContainer container, SshSession &sshSession,
|
||||
QMap<QString, QString> &outFiles)
|
||||
{
|
||||
outFiles.clear();
|
||||
|
||||
if (container != DockerContainer::Xray) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
const amnezia::ScriptVars vars = amnezia::genBaseVars(credentials, container, QString(), QString());
|
||||
|
||||
QString stdOut;
|
||||
auto collect = [&stdOut](const QString &data, libssh::Client &) {
|
||||
stdOut += data + "\n";
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
|
||||
const QString probe = QStringLiteral(
|
||||
"echo \"amnezia_container=$(sudo docker ps -a -q -f name=^$CONTAINER_NAME$ | head -1)\"\n"
|
||||
"if [ -n \"$(sudo docker ps -a -q -f name=^$CONTAINER_NAME$ | head -1)\" ] && "
|
||||
"[ -z \"$(sudo docker ps -q -f name=^$CONTAINER_NAME$ | head -1)\" ]; then\n"
|
||||
" sudo docker start $CONTAINER_NAME > /dev/null 2>&1\n"
|
||||
" sleep 2\n"
|
||||
"fi\n"
|
||||
"echo \"amnezia_running=$(sudo docker ps -q -f name=^$CONTAINER_NAME$ | head -1)\"\n"
|
||||
"echo \"amnezia_mounted=$(sudo docker inspect -f "
|
||||
"'{{range .Mounts}}{{if and (eq .Destination \"%1\") "
|
||||
"(eq .Name \"$CONTAINER_NAME-data\")}}yes{{end}}{{end}}' "
|
||||
"$CONTAINER_NAME 2>/dev/null | head -1)\"")
|
||||
.arg(QString::fromLatin1(amnezia::protocols::xray::dataDir));
|
||||
ErrorCode errorCode = sshSession.runScript(credentials, SshSession::replaceVars(probe, vars), collect, collect);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return ErrorCode::XrayKeyMigrationFailed;
|
||||
}
|
||||
|
||||
static const QRegularExpression reContainer(QStringLiteral("amnezia_container=(\\S+)"));
|
||||
static const QRegularExpression reRunning(QStringLiteral("amnezia_running=(\\S+)"));
|
||||
static const QRegularExpression reMounted(QStringLiteral("amnezia_mounted=(\\S+)"));
|
||||
const bool containerExists = reContainer.match(stdOut).hasMatch();
|
||||
const bool containerRunning = reRunning.match(stdOut).hasMatch();
|
||||
const bool dataDirIsVolume = reMounted.match(stdOut).hasMatch();
|
||||
|
||||
if (!containerExists) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
if (dataDirIsVolume) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
if (!containerRunning) {
|
||||
return ErrorCode::XrayKeyMigrationNeedsConfirm;
|
||||
}
|
||||
|
||||
const QStringList requiredPaths = {
|
||||
QString::fromLatin1(amnezia::protocols::xray::PrivateKeyPath),
|
||||
QString::fromLatin1(amnezia::protocols::xray::PublicKeyPath),
|
||||
QString::fromLatin1(amnezia::protocols::xray::uuidPath),
|
||||
QString::fromLatin1(amnezia::protocols::xray::shortidPath),
|
||||
};
|
||||
|
||||
for (const QString &path : requiredPaths) {
|
||||
QString content;
|
||||
bool read = false;
|
||||
for (int attempt = 0; attempt < 3 && !read; ++attempt) {
|
||||
ErrorCode fileError = ErrorCode::NoError;
|
||||
content = QString::fromUtf8(sshSession.getTextFileFromContainer(container, credentials, path, fileError));
|
||||
if (fileError == ErrorCode::NoError && !content.trimmed().isEmpty()) {
|
||||
read = true;
|
||||
break;
|
||||
}
|
||||
if (attempt < 2) {
|
||||
QThread::msleep(500);
|
||||
}
|
||||
}
|
||||
if (!read) {
|
||||
return ErrorCode::XrayKeyMigrationFailed;
|
||||
}
|
||||
outFiles.insert(path, content);
|
||||
}
|
||||
|
||||
QString serverConfig;
|
||||
bool configRead = false;
|
||||
for (int attempt = 0; attempt < 3 && !configRead; ++attempt) {
|
||||
ErrorCode configError = ErrorCode::NoError;
|
||||
serverConfig = QString::fromUtf8(sshSession.getTextFileFromContainer(
|
||||
container, credentials, QString::fromLatin1(amnezia::protocols::xray::serverConfigPath), configError));
|
||||
if (configError == ErrorCode::NoError && !serverConfig.trimmed().isEmpty()) {
|
||||
configRead = true;
|
||||
break;
|
||||
}
|
||||
if (attempt < 2) {
|
||||
QThread::msleep(500);
|
||||
}
|
||||
}
|
||||
if (!configRead) {
|
||||
return ErrorCode::XrayKeyMigrationFailed;
|
||||
}
|
||||
outFiles.insert(QString::fromLatin1(amnezia::protocols::xray::serverConfigPath), serverConfig);
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode InstallController::restoreXrayStateIntoDataVolume(const ServerCredentials &credentials,
|
||||
DockerContainer container, SshSession &sshSession,
|
||||
const QMap<QString, QString> &files)
|
||||
{
|
||||
if (files.isEmpty()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
for (auto it = files.constBegin(); it != files.constEnd(); ++it) {
|
||||
const ErrorCode errorCode = sshSession.uploadTextFileToContainer(
|
||||
container, credentials, it.value(), it.key(), libssh::ScpOverwriteMode::ScpOverwriteExisting);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return ErrorCode::XrayKeyMigrationFailed;
|
||||
}
|
||||
}
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode InstallController::isServerPortBusy(const ServerCredentials &credentials, DockerContainer container, const ContainerConfig &config, SshSession &sshSession)
|
||||
{
|
||||
if (container == DockerContainer::Dns) {
|
||||
@@ -743,7 +1015,20 @@ bool InstallController::isReinstallContainerRequired(DockerContainer container,
|
||||
}
|
||||
}
|
||||
|
||||
if (container == DockerContainer::Xray || container == DockerContainer::SSXray) {
|
||||
if (container == DockerContainer::Xray) {
|
||||
const auto *oldXrayConfig = oldConfig.getXrayProtocolConfig();
|
||||
const auto *newXrayConfig = newConfig.getXrayProtocolConfig();
|
||||
|
||||
if (oldXrayConfig && newXrayConfig) {
|
||||
const QString oldPort = effectiveXrayPort(oldXrayConfig);
|
||||
const QString newPort = effectiveXrayPort(newXrayConfig);
|
||||
if (oldPort != newPort) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (container == DockerContainer::SSXray) {
|
||||
const auto *oldXrayConfig = oldConfig.getXrayProtocolConfig();
|
||||
const auto *newXrayConfig = newConfig.getXrayProtocolConfig();
|
||||
|
||||
@@ -1015,6 +1300,43 @@ ErrorCode InstallController::rebootServer(const QString &serverId)
|
||||
return sshSession.runScript(credentials, script, cbReadStdOut, cbReadStdErr);
|
||||
}
|
||||
|
||||
ErrorCode InstallController::updateServerPackages(const QString &serverId)
|
||||
{
|
||||
const auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
if (!adminConfig.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
ServerCredentials credentials = adminConfig->credentials();
|
||||
if (!credentials.isValid()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
SshSession sshSession;
|
||||
|
||||
ErrorCode errorCode = isServerDpkgBusy(credentials, sshSession);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
QString stdOut;
|
||||
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
|
||||
stdOut += data + "\n";
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
|
||||
errorCode = sshSession.runScript(credentials, amnezia::scriptData(SharedScriptType::update_server_packages),
|
||||
cbReadStdOut, cbReadStdOut);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
if (!stdOut.contains("Packages update finished")) {
|
||||
qDebug().noquote() << "InstallController::updateServerPackages" << stdOut;
|
||||
return ErrorCode::ServerPacketManagerError;
|
||||
}
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode InstallController::removeAllContainers(const QString &serverId)
|
||||
{
|
||||
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
@@ -1050,10 +1372,19 @@ ErrorCode InstallController::removeContainer(const QString &serverId, DockerCont
|
||||
SshSession sshSession;
|
||||
const amnezia::ScriptVars removeContainerVars =
|
||||
amnezia::genBaseVars(credentials, container, QString(), QString());
|
||||
const bool removeDataVolume = (container == DockerContainer::MtProxy || container == DockerContainer::Telemt
|
||||
|| container == DockerContainer::TProxy);
|
||||
ErrorCode errorCode =
|
||||
sshSession.runScript(credentials, buildRemoveContainerScript(removeContainerVars, removeDataVolume));
|
||||
const bool removeDataVolume = containerKeepsIdentityInDataVolume(container);
|
||||
QString removeOut;
|
||||
auto collectRemoveOut = [&removeOut](const QString &data, libssh::Client &) {
|
||||
removeOut += data + "\n";
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
ErrorCode errorCode = sshSession.runScript(
|
||||
credentials, buildRemoveContainerScript(removeContainerVars, removeDataVolume), collectRemoveOut,
|
||||
collectRemoveOut);
|
||||
|
||||
if (errorCode == ErrorCode::NoError && removeDataVolume && dataVolumeSurvivedRemoval(removeOut)) {
|
||||
errorCode = ErrorCode::ServerDataVolumeNotRemoved;
|
||||
}
|
||||
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
QMap<DockerContainer, ContainerConfig> containers = adminConfig->containers;
|
||||
@@ -1155,6 +1486,13 @@ bool InstallController::isUpdateDockerContainerRequired(DockerContainer containe
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else if (container == DockerContainer::Xray) {
|
||||
const auto *oldXray = oldConfig.getXrayProtocolConfig();
|
||||
const auto *newXray = newConfig.getXrayProtocolConfig();
|
||||
if (oldXray && newXray && oldXray->serverConfig.hasEqualServerSettings(newXray->serverConfig)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} else if (container == DockerContainer::MtProxy) {
|
||||
const auto *oldMt = oldConfig.getMtProxyProtocolConfig();
|
||||
const auto *newMt = newConfig.getMtProxyProtocolConfig();
|
||||
|
||||
@@ -33,15 +33,16 @@ public:
|
||||
QObject *parent = nullptr);
|
||||
~InstallController();
|
||||
|
||||
ErrorCode setupContainer(const ServerCredentials &credentials, DockerContainer container, ContainerConfig &config, bool isUpdate = false);
|
||||
ErrorCode setupContainer(const ServerCredentials &credentials, DockerContainer container, ContainerConfig &config, bool isUpdate = false, bool acceptXrayKeyLoss = 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);
|
||||
ErrorCode updateServerConfig(const QString &serverId, DockerContainer container, const ContainerConfig &oldConfig, ContainerConfig &newConfig, bool acceptXrayKeyLoss = false);
|
||||
|
||||
// Updates client-local settings only: rewrites the stored container config for any self-hosted/native server. No SSH.
|
||||
ErrorCode updateClientConfig(const QString &serverId, DockerContainer container, ContainerConfig &newConfig);
|
||||
|
||||
ErrorCode rebootServer(const QString &serverId);
|
||||
ErrorCode updateServerPackages(const QString &serverId);
|
||||
ErrorCode removeAllContainers(const QString &serverId);
|
||||
ErrorCode removeContainer(const QString &serverId, DockerContainer container);
|
||||
|
||||
@@ -106,6 +107,11 @@ private:
|
||||
ErrorCode configureContainerWorker(const ServerCredentials &credentials, DockerContainer container, ContainerConfig &config, SshSession &sshSession);
|
||||
ErrorCode startupContainerWorker(const ServerCredentials &credentials, DockerContainer container, const ContainerConfig &config, SshSession &sshSession);
|
||||
|
||||
ErrorCode readXrayStateBeforeVolumeMigration(const ServerCredentials &credentials, DockerContainer container,
|
||||
SshSession &sshSession, QMap<QString, QString> &outFiles);
|
||||
ErrorCode restoreXrayStateIntoDataVolume(const ServerCredentials &credentials, DockerContainer container,
|
||||
SshSession &sshSession, const QMap<QString, QString> &files);
|
||||
|
||||
ErrorCode isServerPortBusy(const ServerCredentials &credentials, DockerContainer container, const ContainerConfig &config, SshSession &sshSession);
|
||||
ErrorCode isUserInSudo(const ServerCredentials &credentials, SshSession &sshSession);
|
||||
ErrorCode isServerDpkgBusy(const ServerCredentials &credentials, SshSession &sshSession);
|
||||
@@ -130,7 +136,7 @@ private:
|
||||
bool m_cancelInstallation = false;
|
||||
QString m_tproxyInstallHostname;
|
||||
QString m_tproxyInstallEmail;
|
||||
|
||||
|
||||
#ifndef Q_OS_IOS
|
||||
QList<QSharedPointer<QProcess>> m_sftpMountProcesses;
|
||||
#endif
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QDateTime>
|
||||
|
||||
#include "core/utils/containerEnum.h"
|
||||
@@ -55,6 +56,33 @@ int UsersController::clientIndexById(const QString &clientId, const QJsonArray &
|
||||
return -1;
|
||||
}
|
||||
|
||||
QString UsersController::readXrayVolumeUuid(const DockerContainer container, const ServerCredentials &credentials,
|
||||
SshSession *sshSession) const
|
||||
{
|
||||
ErrorCode error = ErrorCode::NoError;
|
||||
const QString uuid = QString::fromUtf8(sshSession->getTextFileFromContainer(
|
||||
container, credentials, QString::fromLatin1(amnezia::protocols::xray::uuidPath), error));
|
||||
if (error != ErrorCode::NoError) {
|
||||
return {};
|
||||
}
|
||||
return uuid.trimmed();
|
||||
}
|
||||
|
||||
int UsersController::stripXrayVolumeUuidFromTable(QJsonArray &clientsTable, const QString &volumeUuid)
|
||||
{
|
||||
if (volumeUuid.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
int removed = 0;
|
||||
for (int i = clientsTable.size() - 1; i >= 0; --i) {
|
||||
if (clientsTable.at(i).toObject().value(configKey::clientId).toString() == volumeUuid) {
|
||||
clientsTable.removeAt(i);
|
||||
++removed;
|
||||
}
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
void UsersController::migration(const QByteArray &clientsTableString, QJsonArray &clientsTable)
|
||||
{
|
||||
QJsonObject clientsTableObj = QJsonDocument::fromJson(clientsTableString).object();
|
||||
@@ -248,7 +276,8 @@ ErrorCode UsersController::getWireGuardClients(const DockerContainer container,
|
||||
}
|
||||
|
||||
ErrorCode UsersController::getXrayClients(const DockerContainer container, const ServerCredentials& credentials,
|
||||
SshSession* sshSession, int &count, QJsonArray &clientsTable)
|
||||
SshSession* sshSession, int &count, QJsonArray &clientsTable,
|
||||
const QString &volumeDefaultUuid)
|
||||
{
|
||||
ErrorCode error = ErrorCode::NoError;
|
||||
|
||||
@@ -283,6 +312,7 @@ ErrorCode UsersController::getXrayClients(const DockerContainer container, const
|
||||
}
|
||||
|
||||
const QJsonArray clients = settings[protocols::xray::clients].toArray();
|
||||
const QString xrayDefaultUuid = volumeDefaultUuid;
|
||||
for (const auto &clientValue : clients) {
|
||||
const QJsonObject clientObj = clientValue.toObject();
|
||||
if (!clientObj.contains(protocols::xray::id)) {
|
||||
@@ -290,9 +320,6 @@ ErrorCode UsersController::getXrayClients(const DockerContainer container, const
|
||||
continue;
|
||||
}
|
||||
QString clientId = clientObj[protocols::xray::id].toString();
|
||||
|
||||
QString xrayDefaultUuid = sshSession->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::uuidPath, error);
|
||||
xrayDefaultUuid.replace("\n", "");
|
||||
|
||||
if (!isClientExists(clientId, clientsTable) && clientId != xrayDefaultUuid) {
|
||||
QJsonObject client;
|
||||
@@ -339,6 +366,9 @@ ErrorCode UsersController::updateClients(const QString &serverId, const DockerCo
|
||||
|
||||
m_clientsTable = QJsonDocument::fromJson(clientsTableString).array();
|
||||
|
||||
const QString xrayVolumeUuid =
|
||||
(container == DockerContainer::Xray) ? readXrayVolumeUuid(container, credentials, &sshSession) : QString();
|
||||
|
||||
if (m_clientsTable.isEmpty()) {
|
||||
migration(clientsTableString, m_clientsTable);
|
||||
|
||||
@@ -349,7 +379,7 @@ ErrorCode UsersController::updateClients(const QString &serverId, const DockerCo
|
||||
} else if (container == DockerContainer::WireGuard || ContainerUtils::isAwgContainer(container)) {
|
||||
error = getWireGuardClients(container, credentials, &sshSession, count, m_clientsTable);
|
||||
} else if (container == DockerContainer::Xray) {
|
||||
error = getXrayClients(container, credentials, &sshSession, count, m_clientsTable);
|
||||
error = getXrayClients(container, credentials, &sshSession, count, m_clientsTable, xrayVolumeUuid);
|
||||
}
|
||||
if (error != ErrorCode::NoError) {
|
||||
emit clientsUpdated(QJsonArray());
|
||||
@@ -365,6 +395,15 @@ ErrorCode UsersController::updateClients(const QString &serverId, const DockerCo
|
||||
}
|
||||
}
|
||||
|
||||
if (container == DockerContainer::Xray) {
|
||||
const int stripped = stripXrayVolumeUuidFromTable(m_clientsTable, xrayVolumeUuid);
|
||||
if (stripped > 0) {
|
||||
const QByteArray strippedTable = QJsonDocument(m_clientsTable).toJson();
|
||||
const ErrorCode writeError =
|
||||
sshSession.uploadTextFileToContainer(container, credentials, strippedTable, clientsTableFile);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<WgShowData> data;
|
||||
wgShow(container, credentials, &sshSession, data);
|
||||
|
||||
@@ -424,6 +463,13 @@ ErrorCode UsersController::appendClient(const QString &serverId, const QString &
|
||||
return error;
|
||||
}
|
||||
|
||||
if (container == DockerContainer::Xray) {
|
||||
const QString volumeUuid = readXrayVolumeUuid(container, credentials, &sshSession);
|
||||
if (!volumeUuid.isEmpty() && clientId == volumeUuid) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
}
|
||||
|
||||
int existingIndex = clientIndexById(clientId, m_clientsTable);
|
||||
if (existingIndex >= 0) {
|
||||
return renameClient(serverId, existingIndex, clientName, container, true);
|
||||
@@ -634,6 +680,19 @@ ErrorCode UsersController::revokeXray(const int row,
|
||||
|
||||
ErrorCode error = ErrorCode::NoError;
|
||||
|
||||
auto client = clientsTable.at(row).toObject();
|
||||
QString clientId = client.value(configKey::clientId).toString();
|
||||
|
||||
const QString volumeUuid = readXrayVolumeUuid(container, credentials, sshSession);
|
||||
if (!volumeUuid.isEmpty() && clientId == volumeUuid) {
|
||||
clientsTable.removeAt(row);
|
||||
const QByteArray clientsTableString = QJsonDocument(clientsTable).toJson();
|
||||
const QString clientsTableFile = QString("/opt/amnezia/%1/clientsTable")
|
||||
.arg(ContainerUtils::containerTypeToString(container));
|
||||
error = sshSession->uploadTextFileToContainer(container, credentials, clientsTableString, clientsTableFile);
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
const QString serverConfigPath = amnezia::protocols::xray::serverConfigPath;
|
||||
const QString configString = sshSession->getTextFileFromContainer(container, credentials, serverConfigPath, error);
|
||||
if (error != ErrorCode::NoError) {
|
||||
@@ -647,9 +706,6 @@ ErrorCode UsersController::revokeXray(const int row,
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
auto client = clientsTable.at(row).toObject();
|
||||
QString clientId = client.value(configKey::clientId).toString();
|
||||
|
||||
QJsonObject configObj = serverConfig.object();
|
||||
if (!configObj.contains(protocols::xray::inbounds)) {
|
||||
logger.error() << "Missing inbounds in xray config";
|
||||
|
||||
@@ -64,7 +64,12 @@ private:
|
||||
ErrorCode getWireGuardClients(const DockerContainer container, const ServerCredentials &credentials,
|
||||
SshSession* sshSession, int &count, QJsonArray &clientsTable);
|
||||
ErrorCode getXrayClients(const DockerContainer container, const ServerCredentials& credentials,
|
||||
SshSession* sshSession, int &count, QJsonArray &clientsTable);
|
||||
SshSession* sshSession, int &count, QJsonArray &clientsTable,
|
||||
const QString &volumeDefaultUuid);
|
||||
|
||||
QString readXrayVolumeUuid(const DockerContainer container, const ServerCredentials &credentials,
|
||||
SshSession *sshSession) const;
|
||||
static int stripXrayVolumeUuidFromTable(QJsonArray &clientsTable, const QString &volumeUuid);
|
||||
|
||||
ErrorCode wgShow(const DockerContainer container, const ServerCredentials &credentials,
|
||||
SshSession* sshSession, std::vector<WgShowData> &data);
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
#include "version.h"
|
||||
#include "ui/utils/qAutoStart.h"
|
||||
#include "logger.h"
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include "platforms/android/android_controller.h"
|
||||
#include "core/controllers/logsController.h"
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
#include "core/utils/swiftBridge.h"
|
||||
#endif
|
||||
|
||||
QString getPlatformName()
|
||||
@@ -86,6 +87,9 @@ void SettingsController::toggleLogging(bool enable)
|
||||
qWarning() << "Initialization of debug subsystem failed";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
SWIFT_BRIDGE_NAMESPACE::toggleLogging(enable);
|
||||
#endif
|
||||
Logger::setServiceLogsEnabled(enable);
|
||||
|
||||
@@ -96,12 +100,8 @@ void SettingsController::toggleLogging(bool enable)
|
||||
|
||||
void SettingsController::clearLogs()
|
||||
{
|
||||
#ifdef Q_OS_ANDROID
|
||||
AndroidController::instance()->clearLogs();
|
||||
#else
|
||||
Logger::clearLogs(false);
|
||||
Logger::clearServiceLogs();
|
||||
#endif
|
||||
LogsController::clear(LogsController::Stream::App);
|
||||
LogsController::clear(LogsController::Stream::Tunnel);
|
||||
}
|
||||
|
||||
QByteArray SettingsController::backupAppConfig() const
|
||||
@@ -119,6 +119,11 @@ QByteArray SettingsController::backupAppConfig() const
|
||||
return QJsonDocument(config).toJson();
|
||||
}
|
||||
|
||||
int SettingsController::unsupportedFormatConfigsSkippedCount() const
|
||||
{
|
||||
return m_serversRepository->unsupportedFormatConfigsCount();
|
||||
}
|
||||
|
||||
ErrorCode SettingsController::restoreAppConfigFromData(const QByteArray &data)
|
||||
{
|
||||
if (!m_appSettingsRepository->restoreAppConfig(data)) {
|
||||
|
||||
@@ -39,6 +39,7 @@ public:
|
||||
|
||||
QByteArray backupAppConfig() const;
|
||||
ErrorCode restoreAppConfigFromData(const QByteArray &data);
|
||||
int unsupportedFormatConfigsSkippedCount() const;
|
||||
|
||||
QString getAppVersion() const;
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "core/controllers/gatewayController.h"
|
||||
#include "core/utils/api/gatewayPayloadBuilder.h"
|
||||
#include "core/utils/appUiConfig.h"
|
||||
#include "core/utils/constants/apiConstants.h"
|
||||
#include "core/utils/constants/apiKeys.h"
|
||||
#include "core/utils/selfhosted/scriptsRegistry.h"
|
||||
|
||||
@@ -129,6 +130,10 @@ void UpdateController::setUpdateCheckRunning(bool running)
|
||||
|
||||
void UpdateController::checkForUpdates()
|
||||
{
|
||||
#if !CLIENT_ENABLE_APP_UPDATES
|
||||
return;
|
||||
#endif
|
||||
|
||||
if (m_updateCheckRunning || !m_appSettingsRepository) {
|
||||
return;
|
||||
}
|
||||
@@ -136,7 +141,7 @@ void UpdateController::checkForUpdates()
|
||||
|
||||
auto gatewayController = QSharedPointer<GatewayController>::create(m_appSettingsRepository->getGatewayEndpoint(),
|
||||
m_appSettingsRepository->isDevGatewayEnv(),
|
||||
7000,
|
||||
apiDefs::requestTimeoutMsecs,
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled(),
|
||||
m_appSettingsRepository);
|
||||
|
||||
|
||||
@@ -61,8 +61,6 @@ void AwgInstaller::generateAwgParameters(AwgServerConfig &serverConfig)
|
||||
serverConfig.maxHandshakeAttempts = protocols::awg::defaultMaxHandshakeAttempts;
|
||||
serverConfig.randomTrailers = protocols::awg::defaultRandomTrailers;
|
||||
serverConfig.disableCookies = protocols::awg::defaultDisableCookies;
|
||||
|
||||
serverConfig.specialJunk1 = protocols::awg::defaultSpecialJunk1;
|
||||
}
|
||||
|
||||
ErrorCode AwgInstaller::extractConfigFromContainer(DockerContainer container, const ServerCredentials &credentials,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#include "installerBase.h"
|
||||
|
||||
#include <QRandomGenerator>
|
||||
#include <iterator>
|
||||
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
@@ -85,10 +88,12 @@ ContainerConfig InstallerBase::createBaseConfig(DockerContainer container, int p
|
||||
srv.transport = protocols::xray::defaultTransport;
|
||||
srv.security = protocols::xray::defaultSecurity;
|
||||
srv.flow = protocols::xray::defaultFlow;
|
||||
srv.site = protocols::xray::defaultSite;
|
||||
srv.sni = protocols::xray::defaultSni;
|
||||
srv.fingerprint = protocols::xray::defaultFingerprint;
|
||||
const int siteIndex =
|
||||
QRandomGenerator::global()->bounded(static_cast<int>(std::size(protocols::xray::defaultSites)));
|
||||
srv.site = protocols::xray::defaultSites[siteIndex];
|
||||
srv.sni = srv.site;
|
||||
srv.alpn = protocols::xray::defaultAlpn;
|
||||
srv.fingerprint = protocols::xray::defaultFingerprint;
|
||||
config.protocolConfig = xrayConfig;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "xrayInstaller.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonArray>
|
||||
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
@@ -11,6 +10,7 @@
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include "core/utils/selfhosted/sshSession.h"
|
||||
#include "core/configurators/xrayConfigurator.h"
|
||||
#include "core/models/protocols/xrayProtocolConfig.h"
|
||||
#include "logger.h"
|
||||
|
||||
@@ -19,37 +19,21 @@ namespace
|
||||
Logger logger("XrayInstaller");
|
||||
|
||||
// Xray expects uTLS preset names (chrome, firefox, …). Old Amnezia/server templates used "Mozilla/5.0".
|
||||
QString normalizeXrayFingerprint(const QString &fp)
|
||||
QString describeServerJsonStatus(amnezia::XrayServerJsonStatus status)
|
||||
{
|
||||
if (fp.isEmpty() || fp.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) {
|
||||
return QString::fromLatin1(protocols::xray::defaultFingerprint);
|
||||
}
|
||||
return fp;
|
||||
}
|
||||
|
||||
// Parse an xray int range: "from-to" string, plain int, or legacy {from,to} object.
|
||||
void parseIntRange(const QJsonValue &v, QString &minOut, QString &maxOut)
|
||||
{
|
||||
if (v.isString()) {
|
||||
const QString s = v.toString().trimmed();
|
||||
const int dash = s.indexOf(QLatin1Char('-'), 1);
|
||||
if (dash > 0) {
|
||||
minOut = s.left(dash).trimmed();
|
||||
maxOut = s.mid(dash + 1).trimmed();
|
||||
} else if (!s.isEmpty()) {
|
||||
minOut = s;
|
||||
maxOut = s;
|
||||
}
|
||||
} else if (v.isDouble()) {
|
||||
minOut = QString::number(v.toInt());
|
||||
maxOut = minOut;
|
||||
} else if (v.isObject()) {
|
||||
const QJsonObject o = v.toObject();
|
||||
if (o.contains(QLatin1String("from")) || o.contains(QLatin1String("to"))) {
|
||||
minOut = QString::number(o.value(QLatin1String("from")).toInt());
|
||||
maxOut = QString::number(o.value(QLatin1String("to")).toInt());
|
||||
}
|
||||
switch (status) {
|
||||
case amnezia::XrayServerJsonStatus::MissingInbounds:
|
||||
return QStringLiteral("server config missing 'inbounds' field");
|
||||
case amnezia::XrayServerJsonStatus::EmptyInbounds:
|
||||
return QStringLiteral("server config has empty 'inbounds' array");
|
||||
case amnezia::XrayServerJsonStatus::MissingStreamSettings:
|
||||
return QStringLiteral("inbound missing 'streamSettings' field");
|
||||
case amnezia::XrayServerJsonStatus::MissingSettings:
|
||||
return QStringLiteral("inbound missing 'settings' field");
|
||||
case amnezia::XrayServerJsonStatus::Ok:
|
||||
break;
|
||||
}
|
||||
return QStringLiteral("ok");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +49,7 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c
|
||||
SshSession* sshSession, ContainerConfig &config)
|
||||
{
|
||||
ErrorCode errorCode = ErrorCode::NoError;
|
||||
|
||||
|
||||
QString currentConfig = sshSession->getTextFileFromContainer(
|
||||
container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode);
|
||||
|
||||
@@ -78,267 +62,16 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c
|
||||
logger.error() << "Failed to parse server config JSON";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
QJsonObject serverConfig = doc.object();
|
||||
|
||||
if (!serverConfig.contains(protocols::xray::inbounds)) {
|
||||
logger.error() << "Server config missing 'inbounds' field";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
QJsonArray inbounds = serverConfig[protocols::xray::inbounds].toArray();
|
||||
if (inbounds.isEmpty()) {
|
||||
logger.error() << "Server config has empty 'inbounds' array";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
QJsonObject inbound = inbounds[0].toObject();
|
||||
if (!inbound.contains(protocols::xray::streamSettings)) {
|
||||
logger.error() << "Inbound missing 'streamSettings' field";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
QJsonObject streamSettings = inbound[protocols::xray::streamSettings].toObject();
|
||||
auto *xrayConfig = config.getXrayProtocolConfig();
|
||||
if (!xrayConfig) {
|
||||
logger.error() << "No XrayProtocolConfig in ContainerConfig";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
XrayServerConfig &srv = xrayConfig->serverConfig;
|
||||
|
||||
// ── Port ─────────────────────────────────────────────────────────
|
||||
if (inbound.contains(protocols::xray::port)) {
|
||||
srv.port = QString::number(inbound[protocols::xray::port].toInt());
|
||||
}
|
||||
|
||||
// ── Network (transport) ───────────────────────────────────────────
|
||||
QString networkVal = streamSettings.value(protocols::xray::network).toString("tcp");
|
||||
if (networkVal == "xhttp") {
|
||||
srv.transport = "xhttp";
|
||||
} else if (networkVal == "kcp") {
|
||||
srv.transport = "mkcp";
|
||||
} else {
|
||||
srv.transport = "raw";
|
||||
}
|
||||
|
||||
// ── Security ──────────────────────────────────────────────────────
|
||||
srv.security = streamSettings.value(protocols::xray::security).toString("reality");
|
||||
|
||||
// ── Reality settings ──────────────────────────────────────────────
|
||||
if (srv.security == "reality") {
|
||||
QJsonObject rs = streamSettings.value(protocols::xray::realitySettings).toObject();
|
||||
|
||||
// serverNames array → site + sni
|
||||
if (rs.contains(protocols::xray::serverNames)) {
|
||||
QString sniVal = rs[protocols::xray::serverNames].toArray().first().toString();
|
||||
srv.sni = sniVal;
|
||||
srv.site = sniVal;
|
||||
} else if (rs.contains(protocols::xray::serverName)) {
|
||||
srv.sni = rs[protocols::xray::serverName].toString();
|
||||
srv.site = srv.sni;
|
||||
}
|
||||
|
||||
srv.fingerprint = normalizeXrayFingerprint(rs.value(protocols::xray::fingerprint).toString());
|
||||
}
|
||||
|
||||
// ── TLS settings ──────────────────────────────────────────────────
|
||||
if (srv.security == "tls") {
|
||||
QJsonObject tls = streamSettings.value("tlsSettings").toObject();
|
||||
srv.sni = tls.value(protocols::xray::serverName).toString();
|
||||
srv.fingerprint = normalizeXrayFingerprint(tls.value(protocols::xray::fingerprint).toString());
|
||||
|
||||
QJsonArray alpnArr = tls.value("alpn").toArray();
|
||||
QStringList alpnList;
|
||||
for (const QJsonValue &v : alpnArr) {
|
||||
QString t = v.toString().trimmed();
|
||||
if (t.compare(QLatin1String("HTTP/2"), Qt::CaseInsensitive) == 0)
|
||||
t = QStringLiteral("h2");
|
||||
else if (t.compare(QLatin1String("HTTP/1.1"), Qt::CaseInsensitive) == 0)
|
||||
t = QStringLiteral("http/1.1");
|
||||
if (!t.isEmpty())
|
||||
alpnList << t;
|
||||
}
|
||||
srv.alpn = alpnList.join(",");
|
||||
}
|
||||
|
||||
// ── Flow (from users array) ───────────────────────────────────────
|
||||
if (inbound.contains(protocols::xray::settings)) {
|
||||
QJsonObject s = inbound[protocols::xray::settings].toObject();
|
||||
QJsonArray clientsArr = s.value(protocols::xray::clients).toArray();
|
||||
if (!clientsArr.isEmpty()) {
|
||||
srv.flow = clientsArr[0].toObject().value(protocols::xray::flow).toString();
|
||||
}
|
||||
}
|
||||
|
||||
// ── XHTTP settings (Xray-core SplitHTTPConfig + legacy Amnezia keys) ──
|
||||
if (srv.transport == "xhttp") {
|
||||
QJsonObject xhttpObj = streamSettings.value("xhttpSettings").toObject();
|
||||
{
|
||||
const QString m = xhttpObj.value("mode").toString();
|
||||
if (m.isEmpty() || m == QLatin1String("auto"))
|
||||
srv.xhttp.mode = QStringLiteral("Auto");
|
||||
else if (m == QLatin1String("packet-up"))
|
||||
srv.xhttp.mode = QStringLiteral("Packet-up");
|
||||
else if (m == QLatin1String("stream-up"))
|
||||
srv.xhttp.mode = QStringLiteral("Stream-up");
|
||||
else if (m == QLatin1String("stream-one"))
|
||||
srv.xhttp.mode = QStringLiteral("Stream-one");
|
||||
else
|
||||
srv.xhttp.mode = m;
|
||||
}
|
||||
|
||||
srv.xhttp.host = xhttpObj.value("host").toString();
|
||||
srv.xhttp.path = xhttpObj.value("path").toString();
|
||||
|
||||
if (xhttpObj.contains(QLatin1String("uplinkHTTPMethod")))
|
||||
srv.xhttp.uplinkMethod = xhttpObj.value("uplinkHTTPMethod").toString();
|
||||
else
|
||||
srv.xhttp.uplinkMethod = xhttpObj.value("method").toString();
|
||||
|
||||
srv.xhttp.disableGrpc = xhttpObj.value("noGRPCHeader").toBool(true);
|
||||
srv.xhttp.disableSse = xhttpObj.value("noSSEHeader").toBool(true);
|
||||
|
||||
auto sessionSeqUi = [](const QString &core) -> QString {
|
||||
if (core.isEmpty() || core == QLatin1String("path"))
|
||||
return QStringLiteral("Path");
|
||||
if (core == QLatin1String("cookie"))
|
||||
return QStringLiteral("Cookie");
|
||||
if (core == QLatin1String("header"))
|
||||
return QStringLiteral("Header");
|
||||
if (core == QLatin1String("query"))
|
||||
return QStringLiteral("Query");
|
||||
return core;
|
||||
};
|
||||
QString sess = xhttpObj.value("sessionIDPlacement").toString();
|
||||
if (sess.isEmpty())
|
||||
sess = xhttpObj.value("sessionPlacement").toString();
|
||||
if (sess.isEmpty())
|
||||
sess = xhttpObj.value("scSessionPlacement").toString();
|
||||
srv.xhttp.sessionPlacement = sessionSeqUi(sess);
|
||||
|
||||
QString seq = xhttpObj.value("seqPlacement").toString();
|
||||
if (seq.isEmpty())
|
||||
seq = xhttpObj.value("scSeqPlacement").toString();
|
||||
srv.xhttp.seqPlacement = sessionSeqUi(seq);
|
||||
|
||||
auto uplinkDataUi = [](const QString &core) -> QString {
|
||||
if (core.isEmpty() || core == QLatin1String("body"))
|
||||
return QStringLiteral("Body");
|
||||
if (core == QLatin1String("auto"))
|
||||
return QStringLiteral("Auto");
|
||||
if (core == QLatin1String("header"))
|
||||
return QStringLiteral("Header");
|
||||
if (core == QLatin1String("cookie"))
|
||||
return QStringLiteral("Cookie");
|
||||
return core;
|
||||
};
|
||||
QString udata = xhttpObj.value("uplinkDataPlacement").toString();
|
||||
if (udata.isEmpty())
|
||||
udata = xhttpObj.value("scUplinkDataPlacement").toString();
|
||||
srv.xhttp.uplinkDataPlacement = uplinkDataUi(udata);
|
||||
|
||||
srv.xhttp.sessionKey = xhttpObj.value("sessionIDKey").toString();
|
||||
if (srv.xhttp.sessionKey.isEmpty())
|
||||
srv.xhttp.sessionKey = xhttpObj.value("sessionKey").toString();
|
||||
srv.xhttp.seqKey = xhttpObj.value("seqKey").toString();
|
||||
srv.xhttp.uplinkDataKey = xhttpObj.value("uplinkDataKey").toString();
|
||||
|
||||
if (xhttpObj.contains(QLatin1String("uplinkChunkSize"))) {
|
||||
QString ucMin, ucMax;
|
||||
parseIntRange(xhttpObj.value("uplinkChunkSize"), ucMin, ucMax);
|
||||
if (!ucMin.isEmpty())
|
||||
srv.xhttp.uplinkChunkSize = ucMin;
|
||||
} else if (xhttpObj.contains(QLatin1String("xhttpUplinkChunkSize"))) {
|
||||
srv.xhttp.uplinkChunkSize = QString::number(xhttpObj.value("xhttpUplinkChunkSize").toInt());
|
||||
}
|
||||
if (xhttpObj.contains(QLatin1String("scMaxBufferedPosts"))) {
|
||||
srv.xhttp.scMaxBufferedPosts = QString::number(xhttpObj.value("scMaxBufferedPosts").toVariant().toLongLong());
|
||||
}
|
||||
|
||||
auto readRange = [&](const char *key, QString &minOut, QString &maxOut) {
|
||||
parseIntRange(xhttpObj.value(QLatin1String(key)), minOut, maxOut);
|
||||
};
|
||||
readRange("scMaxEachPostBytes", srv.xhttp.scMaxEachPostBytesMin, srv.xhttp.scMaxEachPostBytesMax);
|
||||
readRange("scMinPostsIntervalMs", srv.xhttp.scMinPostsIntervalMsMin, srv.xhttp.scMinPostsIntervalMsMax);
|
||||
readRange("scStreamUpServerSecs", srv.xhttp.scStreamUpServerSecsMin, srv.xhttp.scStreamUpServerSecsMax);
|
||||
|
||||
auto loadPaddingFromObject = [&](const QJsonObject &pad) {
|
||||
if (pad.contains(QLatin1String("xPaddingObfsMode")))
|
||||
srv.xhttp.xPadding.obfsMode = pad.value("xPaddingObfsMode").toBool(true);
|
||||
srv.xhttp.xPadding.key = pad.value("xPaddingKey").toString();
|
||||
srv.xhttp.xPadding.header = pad.value("xPaddingHeader").toString();
|
||||
srv.xhttp.xPadding.placement = pad.value("xPaddingPlacement").toString();
|
||||
srv.xhttp.xPadding.method = pad.value("xPaddingMethod").toString();
|
||||
QString bytesMin, bytesMax;
|
||||
parseIntRange(pad.value("xPaddingBytes"), bytesMin, bytesMax);
|
||||
if (!bytesMin.isEmpty()) {
|
||||
srv.xhttp.xPadding.bytesMin = bytesMin;
|
||||
srv.xhttp.xPadding.bytesMax = bytesMax;
|
||||
}
|
||||
QString pl = srv.xhttp.xPadding.placement.toLower();
|
||||
if (pl == QLatin1String("cookie"))
|
||||
srv.xhttp.xPadding.placement = QStringLiteral("Cookie");
|
||||
else if (pl == QLatin1String("header"))
|
||||
srv.xhttp.xPadding.placement = QStringLiteral("Header");
|
||||
else if (pl == QLatin1String("query"))
|
||||
srv.xhttp.xPadding.placement = QStringLiteral("Query");
|
||||
else if (pl == QLatin1String("queryinheader"))
|
||||
srv.xhttp.xPadding.placement = QStringLiteral("Query in header");
|
||||
QString met = srv.xhttp.xPadding.method.toLower();
|
||||
if (met == QLatin1String("repeat-x"))
|
||||
srv.xhttp.xPadding.method = QStringLiteral("Repeat-x");
|
||||
else if (met == QLatin1String("tokenish"))
|
||||
srv.xhttp.xPadding.method = QStringLiteral("Tokenish");
|
||||
};
|
||||
if (xhttpObj.contains(QLatin1String("xPaddingObfsMode")) || xhttpObj.contains(QLatin1String("xPaddingKey"))
|
||||
|| xhttpObj.contains(QLatin1String("xPaddingBytes"))) {
|
||||
loadPaddingFromObject(xhttpObj);
|
||||
} else if (xhttpObj.contains(QLatin1String("xPadding")) && xhttpObj.value("xPadding").isObject()) {
|
||||
const QJsonObject nested = xhttpObj.value("xPadding").toObject();
|
||||
if (!nested.isEmpty()) {
|
||||
loadPaddingFromObject(nested);
|
||||
if (!nested.contains(QLatin1String("xPaddingObfsMode")))
|
||||
srv.xhttp.xPadding.obfsMode = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (xhttpObj.contains(QLatin1String("xmux"))) {
|
||||
QJsonObject mux = xhttpObj.value("xmux").toObject();
|
||||
srv.xhttp.xmux.enabled = true;
|
||||
|
||||
auto readMuxRange = [&](const char *key, QString &minOut, QString &maxOut) {
|
||||
parseIntRange(mux.value(QLatin1String(key)), minOut, maxOut);
|
||||
};
|
||||
readMuxRange("maxConcurrency", srv.xhttp.xmux.maxConcurrencyMin, srv.xhttp.xmux.maxConcurrencyMax);
|
||||
readMuxRange("maxConnections", srv.xhttp.xmux.maxConnectionsMin, srv.xhttp.xmux.maxConnectionsMax);
|
||||
readMuxRange("cMaxReuseTimes", srv.xhttp.xmux.cMaxReuseTimesMin, srv.xhttp.xmux.cMaxReuseTimesMax);
|
||||
readMuxRange("hMaxRequestTimes", srv.xhttp.xmux.hMaxRequestTimesMin, srv.xhttp.xmux.hMaxRequestTimesMax);
|
||||
readMuxRange("hMaxReusableSecs", srv.xhttp.xmux.hMaxReusableSecsMin, srv.xhttp.xmux.hMaxReusableSecsMax);
|
||||
|
||||
if (mux.contains(QLatin1String("hKeepAlivePeriod")))
|
||||
srv.xhttp.xmux.hKeepAlivePeriod = QString::number(mux.value("hKeepAlivePeriod").toVariant().toLongLong());
|
||||
}
|
||||
}
|
||||
|
||||
// ── mKCP settings ─────────────────────────────────────────────────
|
||||
if (srv.transport == "mkcp") {
|
||||
QJsonObject kcp = streamSettings.value("kcpSettings").toObject();
|
||||
if (kcp.contains("tti")) {
|
||||
srv.mkcp.tti = QString::number(kcp["tti"].toInt());
|
||||
}
|
||||
if (kcp.contains("uplinkCapacity")) {
|
||||
srv.mkcp.uplinkCapacity = QString::number(kcp["uplinkCapacity"].toInt());
|
||||
}
|
||||
if (kcp.contains("downlinkCapacity")) {
|
||||
srv.mkcp.downlinkCapacity = QString::number(kcp["downlinkCapacity"].toInt());
|
||||
}
|
||||
if (kcp.contains("readBufferSize")) {
|
||||
srv.mkcp.readBufferSize = QString::number(kcp["readBufferSize"].toInt());
|
||||
}
|
||||
if (kcp.contains("writeBufferSize")) {
|
||||
srv.mkcp.writeBufferSize = QString::number(kcp["writeBufferSize"].toInt());
|
||||
}
|
||||
srv.mkcp.congestion = kcp.value("congestion").toBool(true);
|
||||
const amnezia::XrayServerJsonStatus status =
|
||||
XrayServerConfig::fromServerInboundJson(doc.object(), xrayConfig->serverConfig);
|
||||
if (status != amnezia::XrayServerJsonStatus::Ok) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
return ErrorCode::NoError;
|
||||
|
||||
@@ -85,6 +85,7 @@ QPair<QString, QString> ApiV2ServerConfig::getDnsPair(const QString &primaryDns,
|
||||
QJsonObject ApiV2ServerConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
obj[configKey::formatVersion] = serverConfigUtils::currentConfigFormatVersion;
|
||||
|
||||
if (!name.isEmpty()) {
|
||||
obj[configKey::name] = name;
|
||||
|
||||
@@ -72,6 +72,12 @@ QJsonObject AwgServerConfig::toJson() const
|
||||
if (!port.isEmpty()) {
|
||||
obj[configKey::port] = this->port;
|
||||
}
|
||||
if (!cascadeEndpoint.isEmpty()) {
|
||||
obj[configKey::cascadeEndpoint] = cascadeEndpoint;
|
||||
}
|
||||
if (!cascadePort.isEmpty()) {
|
||||
obj[configKey::cascadePort] = cascadePort;
|
||||
}
|
||||
if (!transportProto.isEmpty()) {
|
||||
obj[configKey::transportProto] = transportProto;
|
||||
}
|
||||
@@ -166,6 +172,8 @@ AwgServerConfig AwgServerConfig::fromJson(const QJsonObject& json)
|
||||
AwgServerConfig config;
|
||||
|
||||
config.port = json.value(configKey::port).toString();
|
||||
config.cascadeEndpoint = json.value(configKey::cascadeEndpoint).toString();
|
||||
config.cascadePort = json.value(configKey::cascadePort).toString();
|
||||
config.transportProto = json.value(configKey::transportProto).toString();
|
||||
config.protocolVersion = json.value(configKey::protocolVersion).toString();
|
||||
config.subnetAddress = json.value(configKey::subnetAddress).toString();
|
||||
@@ -218,6 +226,12 @@ QJsonObject AwgClientConfig::toJson() const
|
||||
if (port > 0) {
|
||||
obj[configKey::port] = port;
|
||||
}
|
||||
if (!cascadeEndpoint.isEmpty()) {
|
||||
obj[configKey::cascadeEndpoint] = cascadeEndpoint;
|
||||
}
|
||||
if (!cascadePort.isEmpty()) {
|
||||
obj[configKey::cascadePort] = cascadePort;
|
||||
}
|
||||
if (!clientIp.isEmpty()) {
|
||||
obj[configKey::clientIp] = clientIp;
|
||||
}
|
||||
@@ -333,6 +347,14 @@ QJsonObject AwgClientConfig::toJson() const
|
||||
return obj;
|
||||
}
|
||||
|
||||
void AwgClientConfig::setCascade(const QString &newEndpoint, const QString &newPort)
|
||||
{
|
||||
cascadeEndpoint = newEndpoint;
|
||||
cascadePort = newPort;
|
||||
nativeConfig = replaceWireGuardEndpoint(nativeConfig, newEndpoint.isEmpty() ? hostName : newEndpoint,
|
||||
newPort.isEmpty() ? port : newPort.toInt());
|
||||
}
|
||||
|
||||
AwgClientConfig AwgClientConfig::fromJson(const QJsonObject& json)
|
||||
{
|
||||
AwgClientConfig config;
|
||||
@@ -340,6 +362,8 @@ AwgClientConfig AwgClientConfig::fromJson(const QJsonObject& json)
|
||||
config.nativeConfig = json.value(configKey::config).toString();
|
||||
config.hostName = json.value(configKey::hostName).toString();
|
||||
config.port = json.value(configKey::port).toInt(0);
|
||||
config.cascadeEndpoint = json.value(configKey::cascadeEndpoint).toString();
|
||||
config.cascadePort = json.value(configKey::cascadePort).toString();
|
||||
config.clientIp = json.value(configKey::clientIp).toString();
|
||||
config.clientPrivateKey = json.value(configKey::clientPrivKey).toString();
|
||||
config.clientPublicKey = json.value(configKey::clientPubKey).toString();
|
||||
|
||||
@@ -19,6 +19,8 @@ namespace AwgConstant
|
||||
|
||||
struct AwgServerConfig {
|
||||
QString port;
|
||||
QString cascadeEndpoint;
|
||||
QString cascadePort;
|
||||
QString transportProto;
|
||||
QString protocolVersion;
|
||||
QString subnetAddress;
|
||||
@@ -61,6 +63,8 @@ struct AwgClientConfig {
|
||||
QString nativeConfig;
|
||||
QString hostName;
|
||||
int port;
|
||||
QString cascadeEndpoint;
|
||||
QString cascadePort;
|
||||
QString clientIp;
|
||||
QString clientPrivateKey;
|
||||
QString clientPublicKey;
|
||||
@@ -95,6 +99,9 @@ struct AwgClientConfig {
|
||||
QString maxHandshakeAttempts;
|
||||
QString randomTrailers;
|
||||
QString disableCookies;
|
||||
|
||||
void setCascade(const QString &newEndpoint, const QString &newPort);
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static AwgClientConfig fromJson(const QJsonObject& json);
|
||||
};
|
||||
|
||||
@@ -20,6 +20,12 @@ QJsonObject WireGuardServerConfig::toJson() const
|
||||
if (!port.isEmpty()) {
|
||||
obj[configKey::port] = port;
|
||||
}
|
||||
if (!cascadeEndpoint.isEmpty()) {
|
||||
obj[configKey::cascadeEndpoint] = cascadeEndpoint;
|
||||
}
|
||||
if (!cascadePort.isEmpty()) {
|
||||
obj[configKey::cascadePort] = cascadePort;
|
||||
}
|
||||
if (!transportProto.isEmpty()) {
|
||||
obj[configKey::transportProto] = transportProto;
|
||||
}
|
||||
@@ -45,6 +51,8 @@ WireGuardServerConfig WireGuardServerConfig::fromJson(const QJsonObject& json)
|
||||
WireGuardServerConfig config;
|
||||
|
||||
config.port = json.value(configKey::port).toString();
|
||||
config.cascadeEndpoint = json.value(configKey::cascadeEndpoint).toString();
|
||||
config.cascadePort = json.value(configKey::cascadePort).toString();
|
||||
config.transportProto = json.value(configKey::transportProto).toString();
|
||||
config.subnetAddress = json.value(configKey::subnetAddress).toString();
|
||||
config.subnetMask = json.value(configKey::subnetMask).toString();
|
||||
@@ -73,6 +81,12 @@ QJsonObject WireGuardClientConfig::toJson() const
|
||||
if (port > 0) {
|
||||
obj[configKey::port] = port;
|
||||
}
|
||||
if (!cascadeEndpoint.isEmpty()) {
|
||||
obj[configKey::cascadeEndpoint] = cascadeEndpoint;
|
||||
}
|
||||
if (!cascadePort.isEmpty()) {
|
||||
obj[configKey::cascadePort] = cascadePort;
|
||||
}
|
||||
if (!clientIp.isEmpty()) {
|
||||
obj[configKey::clientIp] = clientIp;
|
||||
}
|
||||
@@ -119,6 +133,14 @@ QJsonObject WireGuardClientConfig::toJson() const
|
||||
return obj;
|
||||
}
|
||||
|
||||
void WireGuardClientConfig::setCascade(const QString &newEndpoint, const QString &newPort)
|
||||
{
|
||||
cascadeEndpoint = newEndpoint;
|
||||
cascadePort = newPort;
|
||||
nativeConfig = replaceWireGuardEndpoint(nativeConfig, newEndpoint.isEmpty() ? hostName : newEndpoint,
|
||||
newPort.isEmpty() ? port : newPort.toInt());
|
||||
}
|
||||
|
||||
WireGuardClientConfig WireGuardClientConfig::fromJson(const QJsonObject& json)
|
||||
{
|
||||
WireGuardClientConfig config;
|
||||
@@ -126,6 +148,8 @@ WireGuardClientConfig WireGuardClientConfig::fromJson(const QJsonObject& json)
|
||||
config.nativeConfig = json.value(configKey::config).toString();
|
||||
config.hostName = json.value(configKey::hostName).toString();
|
||||
config.port = json.value(configKey::port).toInt(0);
|
||||
config.cascadeEndpoint = json.value(configKey::cascadeEndpoint).toString();
|
||||
config.cascadePort = json.value(configKey::cascadePort).toString();
|
||||
config.clientIp = json.value(configKey::clientIp).toString();
|
||||
config.clientPrivateKey = json.value(configKey::clientPrivKey).toString();
|
||||
config.clientPublicKey = json.value(configKey::clientPubKey).toString();
|
||||
|
||||
@@ -12,6 +12,8 @@ namespace amnezia
|
||||
|
||||
struct WireGuardServerConfig {
|
||||
QString port;
|
||||
QString cascadeEndpoint;
|
||||
QString cascadePort;
|
||||
QString transportProto;
|
||||
QString subnetAddress;
|
||||
QString subnetMask;
|
||||
@@ -28,6 +30,8 @@ struct WireGuardClientConfig {
|
||||
QString nativeConfig;
|
||||
QString hostName;
|
||||
int port;
|
||||
QString cascadeEndpoint;
|
||||
QString cascadePort;
|
||||
QString clientIp;
|
||||
QString clientPrivateKey;
|
||||
QString clientPublicKey;
|
||||
@@ -41,6 +45,8 @@ struct WireGuardClientConfig {
|
||||
QMap<QString, QString> awgParams;
|
||||
bool isObfuscationEnabled = false;
|
||||
|
||||
void setCascade(const QString &newEndpoint, const QString &newPort);
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static WireGuardClientConfig fromJson(const QJsonObject& json);
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,9 +1,11 @@
|
||||
#ifndef XRAYPROTOCOLCONFIG_H
|
||||
#define XRAYPROTOCOLCONFIG_H
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <optional>
|
||||
|
||||
namespace amnezia
|
||||
@@ -16,8 +18,8 @@ struct XrayXPaddingConfig {
|
||||
bool obfsMode = true; // xPaddingObfsMode
|
||||
QString key; // xPaddingKey
|
||||
QString header; // xPaddingHeader
|
||||
QString placement = protocols::xray::defaultXPaddingPlacement; // xPaddingPlacement: Cookie|Header|Query|Body
|
||||
QString method = protocols::xray::defaultXPaddingMethod; // xPaddingMethod: Repeat-x|Random|Zero
|
||||
QString placement; // xPaddingPlacement: Cookie|Header|Query|Query in header
|
||||
QString method; // xPaddingMethod: Repeat-x|Tokenish
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static XrayXPaddingConfig fromJson(const QJsonObject &json);
|
||||
@@ -45,33 +47,35 @@ struct XrayXmuxConfig {
|
||||
|
||||
// ── XHTTP transport ───────────────────────────────────────────────────────────
|
||||
struct XrayXhttpConfig {
|
||||
QString mode = protocols::xray::defaultXhttpMode; // Auto|Packet-up|Stream-up|Stream-one
|
||||
QString host = protocols::xray::defaultXhttpHost;
|
||||
QString mode;
|
||||
QString host;
|
||||
QString path;
|
||||
QString uplinkMethod = protocols::xray::defaultXhttpUplinkMethod; // POST|PUT|PATCH
|
||||
bool disableGrpc = true;
|
||||
bool disableSse = true;
|
||||
|
||||
// Session & Sequence
|
||||
QString sessionPlacement = protocols::xray::defaultXhttpSessionPlacement;
|
||||
QString sessionKey = protocols::xray::defaultXhttpSessionKey;
|
||||
QString seqPlacement = protocols::xray::defaultXhttpSeqPlacement;
|
||||
QString sessionPlacement;
|
||||
QString sessionKey;
|
||||
QString seqPlacement;
|
||||
QString seqKey;
|
||||
QString uplinkDataPlacement = protocols::xray::defaultXhttpUplinkDataPlacement;
|
||||
QString uplinkDataPlacement;
|
||||
QString uplinkDataKey;
|
||||
|
||||
// Traffic Shaping
|
||||
QString uplinkChunkSize = protocols::xray::defaultXhttpUplinkChunkSize;
|
||||
QString uplinkMethod;
|
||||
QString uplinkChunkSize;
|
||||
QString scMinPostsIntervalMsMin;
|
||||
QString scMinPostsIntervalMsMax;
|
||||
|
||||
XrayXmuxConfig xmux;
|
||||
|
||||
QString scMaxBufferedPosts;
|
||||
QString scMaxEachPostBytesMin = protocols::xray::defaultXhttpScMaxEachPostBytesMin;
|
||||
QString scMaxEachPostBytesMax = protocols::xray::defaultXhttpScMaxEachPostBytesMax;
|
||||
QString scMinPostsIntervalMsMin = protocols::xray::defaultXhttpScMinPostsIntervalMsMin;
|
||||
QString scMinPostsIntervalMsMax = protocols::xray::defaultXhttpScMinPostsIntervalMsMax;
|
||||
QString scStreamUpServerSecsMin = protocols::xray::defaultXhttpScStreamUpServerSecsMin;
|
||||
QString scStreamUpServerSecsMax = protocols::xray::defaultXhttpScStreamUpServerSecsMax;
|
||||
QString scMaxEachPostBytesMin;
|
||||
QString scMaxEachPostBytesMax;
|
||||
QString scStreamUpServerSecsMin;
|
||||
QString scStreamUpServerSecsMax;
|
||||
|
||||
XrayXPaddingConfig xPadding;
|
||||
XrayXmuxConfig xmux;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
/// Reads only keys present in JSON (no Amnezia UI defaults). Use XrayConfigModel::applyDefaultsToServerConfig for UI.
|
||||
@@ -81,16 +85,40 @@ struct XrayXhttpConfig {
|
||||
// ── mKCP transport ────────────────────────────────────────────────────────────
|
||||
struct XrayMkcpConfig {
|
||||
QString tti;
|
||||
QString mtu;
|
||||
QString uplinkCapacity;
|
||||
QString downlinkCapacity;
|
||||
QString readBufferSize;
|
||||
QString writeBufferSize;
|
||||
bool congestion = true;
|
||||
QString cwndMultiplier;
|
||||
QString maxSendingWindow;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static XrayMkcpConfig fromJson(const QJsonObject &json);
|
||||
};
|
||||
|
||||
enum class XrayStreamSide {
|
||||
Server,
|
||||
Client,
|
||||
};
|
||||
|
||||
struct XrayServerInboundInputs {
|
||||
QJsonArray clients;
|
||||
QString realityPrivateKey;
|
||||
QString realityShortId;
|
||||
};
|
||||
|
||||
enum class XrayServerJsonStatus {
|
||||
Ok,
|
||||
MissingInbounds,
|
||||
EmptyInbounds,
|
||||
MissingStreamSettings,
|
||||
MissingSettings,
|
||||
};
|
||||
|
||||
enum class XrayClientListFilter {
|
||||
KeepAll,
|
||||
DropWithoutId,
|
||||
};
|
||||
|
||||
// ── Server config (settings editable by user) ─────────────────────────────────
|
||||
struct XrayServerConfig {
|
||||
QString port;
|
||||
@@ -99,11 +127,13 @@ struct XrayServerConfig {
|
||||
QString site;
|
||||
bool isThirdPartyConfig = false;
|
||||
|
||||
int settingsVersion = 0;
|
||||
|
||||
QString security;
|
||||
QString flow;
|
||||
QString fingerprint;
|
||||
QString sni;
|
||||
QString alpn;
|
||||
QString fingerprint = protocols::xray::defaultFingerprint;
|
||||
|
||||
QString transport;
|
||||
XrayXhttpConfig xhttp;
|
||||
@@ -113,7 +143,65 @@ struct XrayServerConfig {
|
||||
|
||||
static XrayServerConfig fromJson(const QJsonObject &json);
|
||||
|
||||
void applyDefaults(bool fillFlowDefault = false);
|
||||
|
||||
QJsonObject streamSettingsJson(XrayStreamSide side) const;
|
||||
|
||||
QJsonObject serverStreamSettings() const;
|
||||
|
||||
QJsonObject clientStreamSettings() const;
|
||||
|
||||
QJsonObject toServerInboundJson(const XrayServerInboundInputs &inputs) const;
|
||||
|
||||
static XrayServerJsonStatus fromServerInboundJson(const QJsonObject &serverJson, XrayServerConfig &outServerConfig);
|
||||
|
||||
static QJsonArray clientsFromServerInboundJson(const QJsonObject &serverJson);
|
||||
static XrayServerJsonStatus setClientsInServerInboundJson(QJsonObject &serverJson, const QJsonArray &clients);
|
||||
|
||||
static QJsonObject makeClientEntry(const QString &clientId, const QString &flowValue);
|
||||
static QJsonObject applyFlowToClient(const QJsonObject &client, const QString &flowValue);
|
||||
static int indexOfClient(const QJsonArray &clients, const QString &clientId);
|
||||
static QString firstClientId(const QJsonArray &clients);
|
||||
static QJsonArray applyFlowToClients(const QJsonArray &clients, const QString &flowValue,
|
||||
XrayClientListFilter filter = XrayClientListFilter::KeepAll);
|
||||
|
||||
QJsonObject serverView() const;
|
||||
|
||||
QJsonObject issuedConfigView() const;
|
||||
|
||||
bool hasEqualServerSettings(const XrayServerConfig &other) const;
|
||||
|
||||
bool breaksIssuedConfigs(const XrayServerConfig &other) const;
|
||||
};
|
||||
|
||||
namespace xrayEffective
|
||||
{
|
||||
QString xhttpMode(const QString &mode);
|
||||
QString sessionSeqPlacement(const QString &placement);
|
||||
QString uplinkDataPlacement(const QString &placement);
|
||||
QString xPaddingPlacement(const QString &placement);
|
||||
QString xPaddingMethod(const QString &method);
|
||||
QString range(const QString &minV, const QString &maxV);
|
||||
void putRangeIfAny(QJsonObject &obj, const char *key, QString minV, QString maxV, const char *fallbackMin,
|
||||
const char *fallbackMax);
|
||||
|
||||
QString security(const XrayServerConfig &srv);
|
||||
QString clientFlow(const XrayServerConfig &srv);
|
||||
QString serverFlow(const XrayServerConfig &srv);
|
||||
QString network(const XrayServerConfig &srv);
|
||||
QString xhttpModeSent(const XrayServerConfig &srv);
|
||||
}
|
||||
|
||||
QString sanitizeNativeConfig(const QString &nativeConfig);
|
||||
|
||||
struct XrayClientOutboundInputs {
|
||||
QString serverAddress;
|
||||
QString clientId;
|
||||
QString realityPublicKey;
|
||||
QString realityShortId;
|
||||
QString tlsPinnedPeerCertSha256;
|
||||
|
||||
static std::optional<XrayClientOutboundInputs> fromClientNativeJson(const QJsonObject &nativeJson);
|
||||
};
|
||||
|
||||
// ── Client config (generated, not edited by user) ─────────────────────────────
|
||||
@@ -124,6 +212,9 @@ struct XrayClientConfig {
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static XrayClientConfig fromJson(const QJsonObject &json);
|
||||
|
||||
static QString idFromNativeJson(const QJsonObject &nativeJson);
|
||||
static QString localPortFromNativeJson(const QJsonObject &nativeJson);
|
||||
};
|
||||
|
||||
// ── Top-level protocol config ──────────────────────────────────────────────────
|
||||
@@ -140,7 +231,14 @@ struct XrayProtocolConfig {
|
||||
|
||||
bool needsClientHydration = false;
|
||||
|
||||
QJsonObject toClientOutboundJson(const XrayClientOutboundInputs &inputs) const;
|
||||
|
||||
bool fromClientOutboundJson(const QJsonObject &nativeJson);
|
||||
|
||||
bool hydrateServerConfigFromClientNative();
|
||||
|
||||
bool regenerateClientConfigFromNative();
|
||||
|
||||
};
|
||||
|
||||
} // namespace amnezia
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/protocols/protocolUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/serverConfigUtils.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
|
||||
@@ -51,6 +52,7 @@ QPair<QString, QString> NativeServerConfig::getDnsPair(const QString &primaryDns
|
||||
QJsonObject NativeServerConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
obj[configKey::formatVersion] = serverConfigUtils::currentConfigFormatVersion;
|
||||
|
||||
if (!description.isEmpty()) {
|
||||
obj[configKey::description] = this->description;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
#include "core/utils/serverConfigUtils.h"
|
||||
|
||||
namespace amnezia
|
||||
{
|
||||
@@ -83,6 +84,7 @@ QPair<QString, QString> SelfHostedAdminServerConfig::getDnsPair(bool isAmneziaDn
|
||||
QJsonObject SelfHostedAdminServerConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
obj[configKey::formatVersion] = serverConfigUtils::currentConfigFormatVersion;
|
||||
|
||||
if (!description.isEmpty()) {
|
||||
obj[configKey::description] = this->description;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "core/protocols/protocolUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/serverConfigUtils.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
@@ -66,6 +67,7 @@ QPair<QString, QString> SelfHostedUserServerConfig::getDnsPair(const QString &pr
|
||||
QJsonObject SelfHostedUserServerConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
obj[configKey::formatVersion] = serverConfigUtils::currentConfigFormatVersion;
|
||||
|
||||
if (!description.isEmpty()) {
|
||||
obj[configKey::description] = this->description;
|
||||
|
||||
@@ -161,12 +161,15 @@ ServerDescription buildServerDescription(const ApiV2ServerConfig &server, bool /
|
||||
row.hasWriteAccess = false;
|
||||
|
||||
row.serverName = server.displayName;
|
||||
row.baseDescription = server.apiConfig.serverCountryCode.isEmpty() ? server.description : server.apiConfig.serverCountryName;
|
||||
const bool showServerCountry = !server.isFree() && !server.apiConfig.serverCountryCode.isEmpty();
|
||||
row.baseDescription = showServerCountry ? server.apiConfig.serverCountryName : server.description;
|
||||
|
||||
row.isCountrySelectionAvailable = !server.apiConfig.availableCountries.isEmpty();
|
||||
row.isCountrySelectionAvailable = !server.isFree() && !server.apiConfig.availableCountries.isEmpty();
|
||||
row.apiAvailableCountries = server.apiConfig.availableCountries;
|
||||
row.apiServerCountryCode = server.apiConfig.serverCountryCode;
|
||||
row.apiServerCountryCodeL10n = server.apiConfig.serverCountryCodeL10n;
|
||||
if (showServerCountry) {
|
||||
row.apiServerCountryCode = server.apiConfig.serverCountryCode;
|
||||
row.apiServerCountryCodeL10n = server.apiConfig.serverCountryCodeL10n;
|
||||
}
|
||||
|
||||
row.isAdVisible = server.apiConfig.serviceInfo.isAdVisible;
|
||||
row.adHeader = server.apiConfig.serviceInfo.adHeader;
|
||||
|
||||
@@ -61,6 +61,10 @@ void OpenVpnProtocol::cleanupResources()
|
||||
|
||||
void OpenVpnProtocol::stop()
|
||||
{
|
||||
if (guardStop()) {
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "OpenVpnProtocol::stop()";
|
||||
|
||||
const bool wasActive = m_connectionState == Vpn::ConnectionState::Preparing
|
||||
@@ -179,6 +183,8 @@ void OpenVpnProtocol::updateRouteGateway(QString line)
|
||||
|
||||
ErrorCode OpenVpnProtocol::start()
|
||||
{
|
||||
resetStopGuard();
|
||||
|
||||
cleanupResources();
|
||||
|
||||
if (!QFileInfo::exists(configPath())) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "protocolUtils.h"
|
||||
|
||||
#include <QRandomGenerator>
|
||||
#include <QRegularExpression>
|
||||
#include <QJsonObject>
|
||||
#include <QObject>
|
||||
|
||||
@@ -216,3 +217,10 @@ QString ProtocolUtils::key_proto_config_path(Proto p)
|
||||
return protoToString(p) + "_config_path";
|
||||
}
|
||||
|
||||
QString ProtocolUtils::replaceWireGuardEndpoint(QString nativeConfig, const QString &host, int port)
|
||||
{
|
||||
static const QRegularExpression endpointLine(QString(R"(^(\s*%1\s*=\s*).*$)").arg(protocols::wireguard::Endpoint),
|
||||
QRegularExpression::MultilineOption);
|
||||
const QString endpointHost = host.contains(':') ? QString("[%1]").arg(host) : host;
|
||||
return nativeConfig.replace(endpointLine, QString("\\1%1:%2").arg(endpointHost).arg(port));
|
||||
}
|
||||
@@ -39,6 +39,8 @@ namespace amnezia
|
||||
QString key_proto_config_data(Proto p);
|
||||
QString key_proto_config_path(Proto p);
|
||||
|
||||
QString replaceWireGuardEndpoint(QString nativeConfig, const QString &host, int port);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,20 @@ VpnProtocol::VpnProtocol(const QJsonObject &configuration, QObject *parent)
|
||||
connect(m_timeoutTimer, &QTimer::timeout, this, &VpnProtocol::onTimeout);
|
||||
}
|
||||
|
||||
bool VpnProtocol::guardStop()
|
||||
{
|
||||
if (m_stopped) {
|
||||
return true;
|
||||
}
|
||||
m_stopped = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
void VpnProtocol::resetStopGuard()
|
||||
{
|
||||
m_stopped = false;
|
||||
}
|
||||
|
||||
void VpnProtocol::setLastError(ErrorCode lastError)
|
||||
{
|
||||
m_lastError = lastError;
|
||||
|
||||
@@ -89,6 +89,11 @@ protected:
|
||||
void startTimeoutTimer();
|
||||
void stopTimeoutTimer();
|
||||
|
||||
bool guardStop();
|
||||
void resetStopGuard();
|
||||
|
||||
bool m_stopped = false;
|
||||
|
||||
Vpn::ConnectionState m_connectionState;
|
||||
|
||||
QString m_routeGateway;
|
||||
|
||||
@@ -50,8 +50,10 @@ WireguardProtocol::~WireguardProtocol()
|
||||
|
||||
void WireguardProtocol::stop()
|
||||
{
|
||||
if (guardStop()) {
|
||||
return;
|
||||
}
|
||||
stopMzImpl();
|
||||
return;
|
||||
}
|
||||
|
||||
ErrorCode WireguardProtocol::startMzImpl()
|
||||
@@ -75,5 +77,6 @@ ErrorCode WireguardProtocol::stopMzImpl()
|
||||
|
||||
ErrorCode WireguardProtocol::start()
|
||||
{
|
||||
resetStopGuard();
|
||||
return startMzImpl();
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "xrayProtocol.h"
|
||||
|
||||
#include "core/models/protocols/xrayProtocolConfig.h"
|
||||
#include "core/protocols/protocolUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
@@ -66,6 +67,8 @@ XrayProtocol::~XrayProtocol()
|
||||
|
||||
ErrorCode XrayProtocol::start()
|
||||
{
|
||||
resetStopGuard();
|
||||
|
||||
qDebug() << "XrayProtocol::start()";
|
||||
|
||||
// Inject SOCKS5 auth into the inbound before starting xray.
|
||||
@@ -87,30 +90,16 @@ ErrorCode XrayProtocol::start()
|
||||
return ErrorCode::XrayExecutableCrashed;
|
||||
}
|
||||
|
||||
// Fix fingerprint: old configs may contain "Mozilla/5.0" which xray-core rejects.
|
||||
// Replace with the correct default at runtime so stale stored configs still work.
|
||||
if (xrayConfigStr.contains("Mozilla/5.0", Qt::CaseInsensitive)) {
|
||||
xrayConfigStr.replace("Mozilla/5.0", amnezia::protocols::xray::defaultFingerprint,
|
||||
Qt::CaseInsensitive);
|
||||
qDebug() << "XrayProtocol: patched legacy fingerprint to"
|
||||
<< amnezia::protocols::xray::defaultFingerprint;
|
||||
}
|
||||
|
||||
// Fix inbound listen address: old configs may use "10.33.0.2" which doesn't exist
|
||||
// until TUN is created. xray must listen on 127.0.0.1 so tun2socks can connect.
|
||||
if (xrayConfigStr.contains(amnezia::protocols::xray::defaultLocalAddr)) {
|
||||
xrayConfigStr.replace(amnezia::protocols::xray::defaultLocalAddr,
|
||||
amnezia::protocols::xray::defaultLocalListenAddr);
|
||||
qDebug() << "XrayProtocol: patched legacy inbound listen address to 127.0.0.1";
|
||||
}
|
||||
xrayConfigStr = amnezia::sanitizeNativeConfig(xrayConfigStr);
|
||||
|
||||
return IpcClient::withInterface(
|
||||
[&](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
auto xrayStart = iface->xrayStart(xrayConfigStr);
|
||||
if (!xrayStart.waitForFinished() || !xrayStart.returnValue()) {
|
||||
auto xrayStart = iface->xrayStartOwned(xrayConfigStr);
|
||||
if (!xrayStart.waitForFinished() || xrayStart.returnValue() == 0) {
|
||||
qCritical() << "Failed to start xray";
|
||||
return ErrorCode::XrayExecutableCrashed;
|
||||
}
|
||||
m_xrayToken = xrayStart.returnValue();
|
||||
return startTun2Socks();
|
||||
},
|
||||
[]() { return ErrorCode::AmneziaServiceConnectionFailed; });
|
||||
@@ -118,9 +107,13 @@ ErrorCode XrayProtocol::start()
|
||||
|
||||
void XrayProtocol::stop()
|
||||
{
|
||||
if (guardStop()) {
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "XrayProtocol::stop()";
|
||||
|
||||
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
IpcClient::withInterface([this](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
auto disableKillSwitch = iface->disableKillSwitch();
|
||||
if (!disableKillSwitch.waitForFinished() || !disableKillSwitch.returnValue())
|
||||
qWarning() << "Failed to disable killswitch";
|
||||
@@ -137,9 +130,10 @@ void XrayProtocol::stop()
|
||||
if (!deleteTun.waitForFinished() || !deleteTun.returnValue())
|
||||
qWarning() << "Failed to delete tun";
|
||||
|
||||
auto xrayStop = iface->xrayStop();
|
||||
auto xrayStop = iface->xrayStopOwned(m_xrayToken);
|
||||
if (!xrayStop.waitForFinished() || !xrayStop.returnValue())
|
||||
qWarning() << "Failed to stop xray";
|
||||
m_xrayToken = 0;
|
||||
});
|
||||
|
||||
if (m_tun2socksProcess) {
|
||||
|
||||
@@ -38,6 +38,10 @@ private:
|
||||
int m_tun2socksRetryCount = 0;
|
||||
static constexpr int maxTun2SocksRetries = 5;
|
||||
static constexpr int tun2socksRetryDelayMs = 400;
|
||||
|
||||
// Owner token of the shared xray engine; a late stop with a stale token
|
||||
// won't kill an engine restarted by another consumer (e.g. the local proxy).
|
||||
qint64 m_xrayToken = 0;
|
||||
};
|
||||
|
||||
#endif // XRAYPROTOCOL_H
|
||||
@@ -375,6 +375,69 @@ void SecureAppSettingsRepository::setScreenshotsEnabled(bool enabled)
|
||||
emit screenshotsEnabledChanged(enabled);
|
||||
}
|
||||
|
||||
int SecureAppSettingsRepository::countryListSortMode(const QString &listId) const
|
||||
{
|
||||
return value("Conf/countryListSortMode/" + listId, 0).toInt();
|
||||
}
|
||||
|
||||
void SecureAppSettingsRepository::setCountryListSortMode(const QString &listId, int mode)
|
||||
{
|
||||
setValue("Conf/countryListSortMode/" + listId, mode);
|
||||
}
|
||||
|
||||
QStringList SecureAppSettingsRepository::favoriteLocations(const QString &serverId) const
|
||||
{
|
||||
QStringList codes = value("Conf/favoriteLocations").toMap().value(serverId).toStringList();
|
||||
codes.removeAll(QString());
|
||||
codes.removeDuplicates();
|
||||
codes.sort();
|
||||
return codes;
|
||||
}
|
||||
|
||||
void SecureAppSettingsRepository::setFavoriteLocations(const QString &serverId, const QStringList &codes)
|
||||
{
|
||||
if (serverId.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
QVariantMap favorites = value("Conf/favoriteLocations").toMap();
|
||||
if (codes.isEmpty()) {
|
||||
favorites.remove(serverId);
|
||||
} else {
|
||||
favorites.insert(serverId, codes);
|
||||
}
|
||||
setValue("Conf/favoriteLocations", favorites);
|
||||
}
|
||||
|
||||
void SecureAppSettingsRepository::removeFavoriteLocations(const QString &serverId)
|
||||
{
|
||||
QVariantMap favorites = value("Conf/favoriteLocations").toMap();
|
||||
if (favorites.remove(serverId) > 0) {
|
||||
setValue("Conf/favoriteLocations", favorites);
|
||||
}
|
||||
}
|
||||
|
||||
void SecureAppSettingsRepository::migrateLegacyFavoriteLocations(const QStringList &serverIds)
|
||||
{
|
||||
// Before per-subscription favorites the key held one list shared by all servers
|
||||
const QVariant stored = value("Conf/favoriteLocations");
|
||||
const int type = stored.typeId();
|
||||
if (type != QMetaType::QStringList && type != QMetaType::QVariantList && type != QMetaType::QString) {
|
||||
return;
|
||||
}
|
||||
|
||||
QStringList codes = stored.toStringList();
|
||||
codes.removeAll(QString());
|
||||
codes.removeDuplicates();
|
||||
|
||||
QVariantMap favorites;
|
||||
if (!codes.isEmpty()) {
|
||||
for (const QString &serverId : serverIds) {
|
||||
favorites.insert(serverId, codes);
|
||||
}
|
||||
}
|
||||
setValue("Conf/favoriteLocations", favorites);
|
||||
}
|
||||
|
||||
bool SecureAppSettingsRepository::isNewsNotifications() const
|
||||
{
|
||||
return value("Conf/newsNotifications", true).toBool();
|
||||
@@ -484,3 +547,33 @@ void SecureAppSettingsRepository::setXraySavedConfigs(const QByteArray &data)
|
||||
{
|
||||
setValue("Xray/savedConfigs", data);
|
||||
}
|
||||
|
||||
QString SecureAppSettingsRepository::localProxyOwnerId() const
|
||||
{
|
||||
return value("Conf/localProxyOwnerId", "").toString();
|
||||
}
|
||||
|
||||
void SecureAppSettingsRepository::setLocalProxyOwnerId(const QString &serverId)
|
||||
{
|
||||
setValue("Conf/localProxyOwnerId", serverId);
|
||||
}
|
||||
|
||||
quint16 SecureAppSettingsRepository::localProxyPort() const
|
||||
{
|
||||
return static_cast<quint16>(value("Conf/localProxyPort", 10808).toUInt());
|
||||
}
|
||||
|
||||
void SecureAppSettingsRepository::setLocalProxyPort(quint16 port)
|
||||
{
|
||||
setValue("Conf/localProxyPort", port);
|
||||
}
|
||||
|
||||
bool SecureAppSettingsRepository::isLocalProxyPortUserDefined() const
|
||||
{
|
||||
return value("Conf/localProxyPortUserDefined", false).toBool();
|
||||
}
|
||||
|
||||
void SecureAppSettingsRepository::setLocalProxyPortUserDefined(bool userDefined)
|
||||
{
|
||||
setValue("Conf/localProxyPortUserDefined", userDefined);
|
||||
}
|
||||
@@ -77,6 +77,13 @@ public:
|
||||
void setStartMinimized(bool enabled);
|
||||
bool isScreenshotsEnabled() const;
|
||||
void setScreenshotsEnabled(bool enabled);
|
||||
|
||||
int countryListSortMode(const QString &listId) const;
|
||||
void setCountryListSortMode(const QString &listId, int mode);
|
||||
QStringList favoriteLocations(const QString &serverId) const;
|
||||
void setFavoriteLocations(const QString &serverId, const QStringList &codes);
|
||||
void removeFavoriteLocations(const QString &serverId);
|
||||
void migrateLegacyFavoriteLocations(const QStringList &serverIds);
|
||||
bool isNewsNotifications() const;
|
||||
void setNewsNotifications(bool enabled);
|
||||
|
||||
@@ -100,6 +107,13 @@ public:
|
||||
QByteArray xraySavedConfigs() const;
|
||||
void setXraySavedConfigs(const QByteArray &data);
|
||||
|
||||
QString localProxyOwnerId() const;
|
||||
void setLocalProxyOwnerId(const QString &serverId);
|
||||
quint16 localProxyPort() const;
|
||||
void setLocalProxyPort(quint16 port);
|
||||
bool isLocalProxyPortUserDefined() const;
|
||||
void setLocalProxyPortUserDefined(bool userDefined);
|
||||
|
||||
signals:
|
||||
void appLanguageChanged(QLocale locale);
|
||||
void allowedDnsServersChanged(const QStringList &servers);
|
||||
@@ -126,4 +140,3 @@ private:
|
||||
};
|
||||
|
||||
#endif // SECUREAPPSETTINGSREPOSITORY_H
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <QJsonValue>
|
||||
#include <QSet>
|
||||
#include <QUuid>
|
||||
#include <QDebug>
|
||||
|
||||
#include "core/utils/serverConfigUtils.h"
|
||||
#include "core/utils/constants/apiKeys.h"
|
||||
@@ -142,23 +143,43 @@ void SecureServersRepository::persistDefaultServerFields()
|
||||
void SecureServersRepository::loadFromStorage()
|
||||
{
|
||||
clearServerStateMaps();
|
||||
m_unsupportedFormatConfigsCount = 0;
|
||||
|
||||
const QJsonArray serversArray =
|
||||
QJsonDocument::fromJson(value(QStringLiteral("Servers/serversList"), QByteArray()).toByteArray())
|
||||
.array();
|
||||
|
||||
QJsonArray persistedArray;
|
||||
bool hasGeneratedIds = false;
|
||||
|
||||
for (int i = 0; i < serversArray.size(); ++i) {
|
||||
const QJsonObject json = serversArray.at(i).toObject();
|
||||
const QString candidateId = readStorageServerId(json);
|
||||
const QString serverId = normalizedOrGeneratedServerId(candidateId);
|
||||
const QJsonObject strippedJson = withoutStorageServerId(json);
|
||||
if (!serverConfigUtils::isConfigFormatVersionSupported(strippedJson)) {
|
||||
qWarning() << "Skipping stored server config with unsupported format version"
|
||||
<< serverConfigUtils::configFormatVersion(strippedJson);
|
||||
++m_unsupportedFormatConfigsCount;
|
||||
persistedArray.append(json);
|
||||
continue;
|
||||
}
|
||||
const serverConfigUtils::ConfigType kind = serverConfigUtils::configTypeFromJson(strippedJson);
|
||||
|
||||
if (m_serverJsonById.contains(serverId) || kind == serverConfigUtils::ConfigType::Invalid) {
|
||||
persistedArray.append(json);
|
||||
continue;
|
||||
}
|
||||
m_serverJsonById.insert(serverId, embedStorageServerId(serverId, strippedJson));
|
||||
m_orderedServerIds.append(serverId);
|
||||
persistedArray.append(m_serverJsonById.value(serverId));
|
||||
hasGeneratedIds = hasGeneratedIds || serverId != candidateId;
|
||||
}
|
||||
|
||||
// Per-server settings are keyed by id, so a generated one must survive restarts.
|
||||
// Unlike syncToStorage(), this keeps entries that were skipped above
|
||||
if (hasGeneratedIds) {
|
||||
setValue(QStringLiteral("Servers/serversList"), QJsonDocument(persistedArray).toJson());
|
||||
}
|
||||
|
||||
updateDefaultServerFromStorage();
|
||||
@@ -184,6 +205,11 @@ void SecureServersRepository::invalidateCache()
|
||||
loadFromStorage();
|
||||
}
|
||||
|
||||
int SecureServersRepository::unsupportedFormatConfigsCount() const
|
||||
{
|
||||
return m_unsupportedFormatConfigsCount;
|
||||
}
|
||||
|
||||
void SecureServersRepository::clearServers()
|
||||
{
|
||||
clearServerStateMaps();
|
||||
@@ -222,7 +248,8 @@ QString SecureServersRepository::addServer(const QString &serverId, const QJsonO
|
||||
return id;
|
||||
}
|
||||
const QJsonObject strippedJson = withoutStorageServerId(serverJson);
|
||||
if (serverConfigUtils::configTypeFromJson(strippedJson) != kind) {
|
||||
if (!serverConfigUtils::isConfigFormatVersionSupported(strippedJson)
|
||||
|| serverConfigUtils::configTypeFromJson(strippedJson) != kind) {
|
||||
return id;
|
||||
}
|
||||
m_serverJsonById.insert(id, embedStorageServerId(id, strippedJson));
|
||||
@@ -309,6 +336,15 @@ serverConfigUtils::ConfigType SecureServersRepository::serverKind(const QString
|
||||
return serverConfigUtils::configTypeFromJson(withoutStorageServerId(it.value()));
|
||||
}
|
||||
|
||||
std::optional<QJsonObject> SecureServersRepository::serverJsonById(const QString &serverId) const
|
||||
{
|
||||
const auto it = m_serverJsonById.constFind(serverId);
|
||||
if (it == m_serverJsonById.constEnd()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return it.value();
|
||||
}
|
||||
|
||||
std::optional<SelfHostedAdminServerConfig> SecureServersRepository::selfHostedAdminConfig(const QString &serverId) const
|
||||
{
|
||||
const auto it = m_serverJsonById.constFind(serverId);
|
||||
|
||||
@@ -31,6 +31,8 @@ public:
|
||||
void removeServer(const QString &serverId);
|
||||
serverConfigUtils::ConfigType serverKind(const QString &serverId) const;
|
||||
|
||||
std::optional<QJsonObject> serverJsonById(const QString &serverId) const;
|
||||
|
||||
std::optional<SelfHostedAdminServerConfig> selfHostedAdminConfig(const QString &serverId) const;
|
||||
std::optional<SelfHostedUserServerConfig> selfHostedUserConfig(const QString &serverId) const;
|
||||
std::optional<NativeServerConfig> nativeConfig(const QString &serverId) const;
|
||||
@@ -52,6 +54,8 @@ public:
|
||||
|
||||
void invalidateCache();
|
||||
|
||||
int unsupportedFormatConfigsCount() const;
|
||||
|
||||
signals:
|
||||
void serverAdded(const QString &serverId);
|
||||
void serverEdited(const QString &serverId);
|
||||
@@ -77,6 +81,7 @@ private:
|
||||
QVector<QString> m_orderedServerIds;
|
||||
|
||||
QString m_defaultServerId;
|
||||
int m_unsupportedFormatConfigsCount = 0;
|
||||
};
|
||||
|
||||
#endif // SECURESERVERSREPOSITORY_H
|
||||
@@ -20,6 +20,9 @@ namespace
|
||||
|
||||
constexpr QLatin1String unprocessableSubscriptionMessage("Failed to retrieve subscription information. Is it activated?");
|
||||
constexpr QLatin1String trialAlreadyUsedMessage("trial subscription already used");
|
||||
constexpr QLatin1String refreshCaptchaMessage("refresh_captcha");
|
||||
constexpr QLatin1String invalidCaptchaMessage("invalid_captcha");
|
||||
constexpr QLatin1String rateLimitExceededMessage("rate_limit_exceeded");
|
||||
|
||||
QDateTime subscriptionEndUtcFromString(const QString &subscriptionEndDate)
|
||||
{
|
||||
@@ -111,9 +114,7 @@ bool apiUtils::isSubscriptionExpiringSoon(const QString &subscriptionEndDate, in
|
||||
return endDate <= nowUtc.addDays(withinDays);
|
||||
}
|
||||
|
||||
amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &sslErrors, const QString &replyErrorString,
|
||||
const QNetworkReply::NetworkError &replyError, const int httpStatusCode,
|
||||
const QByteArray &responseBody)
|
||||
amnezia::ErrorCode apiUtils::checkApiResponseErrors(const QByteArray &responseBody)
|
||||
{
|
||||
const int httpStatusCodeConflict = 409;
|
||||
const int httpStatusCodeNotFound = 404;
|
||||
@@ -123,76 +124,60 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
|
||||
const int httpStatusCodeRequestTimeout = 408;
|
||||
const int httpStatusCodeUnprocessableEntity = 422;
|
||||
|
||||
if (!sslErrors.empty()) {
|
||||
qDebug().noquote() << sslErrors;
|
||||
return amnezia::ErrorCode::ApiConfigSslError;
|
||||
}
|
||||
if (replyError == QNetworkReply::NetworkError::OperationCanceledError
|
||||
|| replyError == QNetworkReply::NetworkError::TimeoutError) {
|
||||
qDebug() << replyError;
|
||||
return amnezia::ErrorCode::ApiConfigTimeoutError;
|
||||
}
|
||||
if (replyError == QNetworkReply::NetworkError::OperationNotImplementedError) {
|
||||
qDebug() << replyError;
|
||||
return amnezia::ErrorCode::ApiUpdateRequestError;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
return amnezia::ErrorCode::ApiConfigLimitError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodeNotFound) {
|
||||
return amnezia::ErrorCode::ApiNotFoundError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodeRequestTimeout) {
|
||||
return amnezia::ErrorCode::ApiConfigTimeoutError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodeNotImplemented) {
|
||||
return amnezia::ErrorCode::ApiUpdateRequestError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodeUnprocessableEntity) {
|
||||
if (apiErrorMessageFromJson(jsonObj) == unprocessableSubscriptionMessage) {
|
||||
return amnezia::ErrorCode::ApiSubscriptionExpiredError;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if (replyError == QNetworkReply::NoError) {
|
||||
if (!jsonDoc.isObject()) {
|
||||
return amnezia::ErrorCode::NoError;
|
||||
}
|
||||
|
||||
qDebug() << "something went wrong";
|
||||
return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||
QJsonObject jsonObj = jsonDoc.object();
|
||||
const int httpStatusFromBody = jsonObj.value(apiDefs::key::httpStatus).toInt(-1);
|
||||
if (httpStatusFromBody < 0) {
|
||||
return amnezia::ErrorCode::NoError;
|
||||
}
|
||||
|
||||
if (httpStatusFromBody == httpStatusCodeTooManyRequests) {
|
||||
return amnezia::ErrorCode::ApiRateLimitError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodeConflict) {
|
||||
if (apiErrorMessageFromJson(jsonObj).contains(trialAlreadyUsedMessage, Qt::CaseInsensitive)) {
|
||||
return amnezia::ErrorCode::ApiTrialAlreadyUsedError;
|
||||
}
|
||||
return amnezia::ErrorCode::ApiConfigLimitError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodeNotFound) {
|
||||
return amnezia::ErrorCode::ApiNotFoundError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodeRequestTimeout) {
|
||||
return amnezia::ErrorCode::ApiConfigTimeoutError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodeNotImplemented) {
|
||||
return amnezia::ErrorCode::ApiUpdateRequestError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodeUnprocessableEntity) {
|
||||
if (apiErrorMessageFromJson(jsonObj) == unprocessableSubscriptionMessage) {
|
||||
return amnezia::ErrorCode::ApiSubscriptionExpiredError;
|
||||
}
|
||||
return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodePaymentRequired) {
|
||||
const QString message = apiErrorMessageFromJson(jsonObj);
|
||||
if (message.contains(refreshCaptchaMessage, Qt::CaseInsensitive)) {
|
||||
return amnezia::ErrorCode::ApiCaptchaRefreshError;
|
||||
}
|
||||
if (message.contains(invalidCaptchaMessage, Qt::CaseInsensitive)) {
|
||||
return amnezia::ErrorCode::ApiCaptchaInvalidError;
|
||||
}
|
||||
if (jsonObj.contains(apiDefs::key::captchaId) || jsonObj.contains(apiDefs::key::captchaImage)
|
||||
|| message.contains(rateLimitExceededMessage, Qt::CaseInsensitive)) {
|
||||
return amnezia::ErrorCode::ApiCaptchaRequiredError;
|
||||
}
|
||||
return amnezia::ErrorCode::ApiSubscriptionNotActiveError;
|
||||
}
|
||||
|
||||
if (httpStatusFromBody >= 300) {
|
||||
return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||
}
|
||||
return amnezia::ErrorCode::NoError;
|
||||
}
|
||||
|
||||
bool apiUtils::isPremiumServer(const QJsonObject &serverConfigObject)
|
||||
|
||||
@@ -29,9 +29,7 @@ namespace apiUtils
|
||||
|
||||
bool isPremiumServer(const QJsonObject &serverConfigObject);
|
||||
|
||||
amnezia::ErrorCode checkNetworkReplyErrors(const QList<QSslError> &sslErrors, const QString &replyErrorString,
|
||||
const QNetworkReply::NetworkError &replyError, const int httpStatusCode,
|
||||
const QByteArray &responseBody);
|
||||
amnezia::ErrorCode checkApiResponseErrors(const QByteArray &responseBody);
|
||||
|
||||
QString getPremiumV2VpnKey(const QJsonObject &serverConfigObject);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
#include "countryCatalog.h"
|
||||
|
||||
#include <limits>
|
||||
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "logger.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
Logger logger("CountryCatalog");
|
||||
|
||||
constexpr QLatin1String bundledPath(":/countries/regions.json");
|
||||
|
||||
constexpr QLatin1String keyVersion("version");
|
||||
constexpr QLatin1String keySplitThreshold("splitThreshold");
|
||||
constexpr QLatin1String keyCollapseThreshold("collapseThreshold");
|
||||
constexpr QLatin1String keyRegions("regions");
|
||||
constexpr QLatin1String keySubregions("subregions");
|
||||
constexpr QLatin1String keyCountries("countries");
|
||||
constexpr QLatin1String keyId("id");
|
||||
constexpr QLatin1String keyOrder("order");
|
||||
constexpr QLatin1String keyCode("code");
|
||||
constexpr QLatin1String keyIso("iso");
|
||||
constexpr QLatin1String keyRegion("region");
|
||||
constexpr QLatin1String keySubregion("subregion");
|
||||
constexpr QLatin1String keySubsubregion("subsubregion");
|
||||
constexpr QLatin1String keyNameEn("nameEn");
|
||||
constexpr QLatin1String keyNameRu("nameRu");
|
||||
constexpr QLatin1String keyCity("city");
|
||||
constexpr QLatin1String keyAliases("aliases");
|
||||
constexpr QLatin1String keySplit("split");
|
||||
constexpr QLatin1String keyUseCases("useCases");
|
||||
constexpr QLatin1String keyLocationIds("locationIds");
|
||||
constexpr QLatin1String keyNameLocalized("nameLocalized");
|
||||
|
||||
constexpr QLatin1String splitAlways("always");
|
||||
constexpr QLatin1String splitNever("never");
|
||||
|
||||
countryCatalog::SplitMode parseSplit(const QJsonObject &object)
|
||||
{
|
||||
const QString split = object.value(keySplit).toString();
|
||||
if (split == splitAlways) {
|
||||
return countryCatalog::SplitMode::Always;
|
||||
}
|
||||
if (split == splitNever) {
|
||||
return countryCatalog::SplitMode::Never;
|
||||
}
|
||||
return countryCatalog::SplitMode::Auto;
|
||||
}
|
||||
}
|
||||
|
||||
namespace countryCatalog
|
||||
{
|
||||
|
||||
Catalog Catalog::fromJson(const QByteArray &json)
|
||||
{
|
||||
Catalog catalog;
|
||||
|
||||
QJsonParseError parseError {};
|
||||
const QJsonDocument document = QJsonDocument::fromJson(json, &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !document.isObject()) {
|
||||
logger.error() << "failed to parse the country catalog:" << parseError.errorString();
|
||||
return catalog;
|
||||
}
|
||||
|
||||
const QJsonObject root = document.object();
|
||||
catalog.m_version = root.value(keyVersion).toInt();
|
||||
if (root.contains(keySplitThreshold)) {
|
||||
catalog.m_splitThreshold = root.value(keySplitThreshold).toInt(catalog.m_splitThreshold);
|
||||
}
|
||||
if (root.contains(keyCollapseThreshold)) {
|
||||
catalog.m_collapseThreshold = root.value(keyCollapseThreshold).toInt(catalog.m_collapseThreshold);
|
||||
}
|
||||
|
||||
const QJsonArray regions = root.value(keyRegions).toArray();
|
||||
for (const QJsonValue &value : regions) {
|
||||
const QJsonObject regionObject = value.toObject();
|
||||
|
||||
Region region;
|
||||
region.id = regionObject.value(keyId).toString();
|
||||
if (region.id.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
region.order = regionObject.value(keyOrder).toInt();
|
||||
region.split = parseSplit(regionObject);
|
||||
|
||||
const QJsonArray subregions = regionObject.value(keySubregions).toArray();
|
||||
for (const QJsonValue &subregionValue : subregions) {
|
||||
const QJsonObject subregionObject = subregionValue.toObject();
|
||||
|
||||
Subregion subregion;
|
||||
subregion.id = subregionObject.value(keyId).toString();
|
||||
if (subregion.id.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
subregion.order = subregionObject.value(keyOrder).toInt();
|
||||
subregion.split = parseSplit(subregionObject);
|
||||
|
||||
const QJsonArray subsubregions = subregionObject.value(keySubregions).toArray();
|
||||
for (const QJsonValue &subsubregionValue : subsubregions) {
|
||||
const QJsonObject subsubregionObject = subsubregionValue.toObject();
|
||||
|
||||
Subsubregion subsubregion;
|
||||
subsubregion.id = subsubregionObject.value(keyId).toString();
|
||||
if (subsubregion.id.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
subsubregion.order = subsubregionObject.value(keyOrder).toInt();
|
||||
subregion.subsubregions.push_back(subsubregion);
|
||||
}
|
||||
|
||||
region.subregions.push_back(subregion);
|
||||
}
|
||||
|
||||
catalog.m_regions.push_back(region);
|
||||
}
|
||||
|
||||
const QJsonArray countries = root.value(keyCountries).toArray();
|
||||
for (const QJsonValue &value : countries) {
|
||||
const QJsonObject countryObject = value.toObject();
|
||||
|
||||
Entry entry;
|
||||
entry.code = countryObject.value(keyCode).toString();
|
||||
if (entry.code.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
entry.isoCode = countryObject.value(keyIso).toString(entry.code).toUpper();
|
||||
entry.regionId = countryObject.value(keyRegion).toString();
|
||||
entry.subregionId = countryObject.value(keySubregion).toString();
|
||||
entry.subsubregionId = countryObject.value(keySubsubregion).toString();
|
||||
entry.nameEn = countryObject.value(keyNameEn).toString();
|
||||
entry.nameRu = countryObject.value(keyNameRu).toString();
|
||||
entry.city = countryObject.value(keyCity).toString();
|
||||
|
||||
const QJsonArray aliases = countryObject.value(keyAliases).toArray();
|
||||
for (const QJsonValue &aliasValue : aliases) {
|
||||
const QString alias = aliasValue.toString();
|
||||
if (!alias.isEmpty()) {
|
||||
entry.aliases.push_back(alias);
|
||||
}
|
||||
}
|
||||
|
||||
catalog.m_byCode.insert(entry.code.toUpper(), entry);
|
||||
if (!catalog.m_byIso.contains(entry.isoCode)) {
|
||||
catalog.m_byIso.insert(entry.isoCode, entry);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const QJsonArray useCases = root.value(keyUseCases).toArray();
|
||||
for (const QJsonValue &value : useCases) {
|
||||
const QJsonObject useCaseObject = value.toObject();
|
||||
|
||||
UseCase useCase;
|
||||
useCase.id = useCaseObject.value(keyId).toString();
|
||||
if (useCase.id.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
useCase.order = useCaseObject.value(keyOrder).toInt();
|
||||
|
||||
const QJsonObject names = useCaseObject.value(keyNameLocalized).toObject();
|
||||
for (auto it = names.constBegin(); it != names.constEnd(); ++it) {
|
||||
const QString name = it.value().toString().trimmed();
|
||||
if (!name.isEmpty()) {
|
||||
useCase.nameLocalized.insert(it.key(), name);
|
||||
}
|
||||
}
|
||||
|
||||
const QJsonArray locationIds = useCaseObject.value(keyLocationIds).toArray();
|
||||
for (const QJsonValue &locationId : locationIds) {
|
||||
const QString id = locationId.toString();
|
||||
if (!id.isEmpty()) {
|
||||
useCase.locationIds.push_back(id);
|
||||
}
|
||||
}
|
||||
catalog.m_useCases.push_back(useCase);
|
||||
}
|
||||
|
||||
logger.info() << "loaded" << catalog.m_regions.size() << "regions and"
|
||||
<< catalog.m_byCode.size() << "locations, catalog version"
|
||||
<< catalog.m_version;
|
||||
|
||||
return catalog;
|
||||
}
|
||||
|
||||
Catalog Catalog::bundled()
|
||||
{
|
||||
QFile file(bundledPath);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
logger.error() << "the bundled country catalog is missing at" << bundledPath;
|
||||
return {};
|
||||
}
|
||||
return fromJson(file.readAll());
|
||||
}
|
||||
|
||||
bool Catalog::isEmpty() const
|
||||
{
|
||||
return m_regions.isEmpty() || m_byCode.isEmpty();
|
||||
}
|
||||
|
||||
int Catalog::splitThreshold() const
|
||||
{
|
||||
return m_splitThreshold;
|
||||
}
|
||||
|
||||
int Catalog::collapseThreshold() const
|
||||
{
|
||||
return m_collapseThreshold;
|
||||
}
|
||||
|
||||
bool Catalog::splitByCount(int visibleCount, bool wasSplit) const
|
||||
{
|
||||
if (visibleCount > m_splitThreshold) {
|
||||
return true;
|
||||
}
|
||||
if (visibleCount < m_collapseThreshold) {
|
||||
return false;
|
||||
}
|
||||
return wasSplit;
|
||||
}
|
||||
|
||||
int Catalog::version() const
|
||||
{
|
||||
return m_version;
|
||||
}
|
||||
|
||||
const QVector<Region> &Catalog::regions() const
|
||||
{
|
||||
return m_regions;
|
||||
}
|
||||
|
||||
const QVector<UseCase> &Catalog::useCases() const
|
||||
{
|
||||
return m_useCases;
|
||||
}
|
||||
|
||||
const Entry *Catalog::find(const QString &countryCode, const QString &isoCode) const
|
||||
{
|
||||
const auto byCode = m_byCode.constFind(countryCode.toUpper());
|
||||
if (byCode != m_byCode.constEnd()) {
|
||||
return &byCode.value();
|
||||
}
|
||||
|
||||
const auto byIso = m_byIso.constFind(isoCode.toUpper());
|
||||
if (byIso != m_byIso.constEnd()) {
|
||||
return &byIso.value();
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
|
||||
bool Catalog::hasSubregions(const QString ®ionId) const
|
||||
{
|
||||
for (const Region ®ion : m_regions) {
|
||||
if (region.id == regionId) {
|
||||
return !region.subregions.isEmpty();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Catalog::isSplit(const QString ®ionId, int visibleCount, bool wasSplit) const
|
||||
{
|
||||
for (const Region ®ion : m_regions) {
|
||||
if (region.id != regionId) {
|
||||
continue;
|
||||
}
|
||||
if (region.subregions.isEmpty() || region.split == SplitMode::Never) {
|
||||
return false;
|
||||
}
|
||||
if (region.split == SplitMode::Always) {
|
||||
return true;
|
||||
}
|
||||
return splitByCount(visibleCount, wasSplit);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Catalog::isSplit(const QString ®ionId, const QString &subregionId, int visibleCount, bool wasSplit) const
|
||||
{
|
||||
for (const Region ®ion : m_regions) {
|
||||
if (region.id != regionId) {
|
||||
continue;
|
||||
}
|
||||
for (const Subregion &subregion : region.subregions) {
|
||||
if (subregion.id != subregionId) {
|
||||
continue;
|
||||
}
|
||||
if (subregion.subsubregions.isEmpty() || subregion.split == SplitMode::Never) {
|
||||
return false;
|
||||
}
|
||||
if (subregion.split == SplitMode::Always) {
|
||||
return true;
|
||||
}
|
||||
return splitByCount(visibleCount, wasSplit);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
#ifndef COUNTRYCATALOG_H
|
||||
#define COUNTRYCATALOG_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QHash>
|
||||
#include <QLatin1String>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QVector>
|
||||
|
||||
namespace countryCatalog
|
||||
{
|
||||
constexpr QLatin1String otherRegionId("other");
|
||||
|
||||
struct Entry
|
||||
{
|
||||
QString code;
|
||||
QString isoCode;
|
||||
QString regionId;
|
||||
QString subregionId;
|
||||
QString subsubregionId;
|
||||
QString nameEn;
|
||||
QString nameRu;
|
||||
QString city;
|
||||
QStringList aliases;
|
||||
};
|
||||
|
||||
enum class SplitMode {
|
||||
Auto,
|
||||
Always,
|
||||
Never
|
||||
};
|
||||
|
||||
struct Subsubregion
|
||||
{
|
||||
QString id;
|
||||
int order = 0;
|
||||
};
|
||||
|
||||
struct Subregion
|
||||
{
|
||||
QString id;
|
||||
int order = 0;
|
||||
SplitMode split = SplitMode::Auto;
|
||||
QVector<Subsubregion> subsubregions;
|
||||
};
|
||||
|
||||
struct Region
|
||||
{
|
||||
QString id;
|
||||
int order = 0;
|
||||
SplitMode split = SplitMode::Auto;
|
||||
QVector<Subregion> subregions;
|
||||
};
|
||||
|
||||
struct UseCase
|
||||
{
|
||||
QString id;
|
||||
int order = 0;
|
||||
QHash<QString, QString> nameLocalized;
|
||||
QStringList locationIds;
|
||||
};
|
||||
|
||||
class Catalog
|
||||
{
|
||||
public:
|
||||
Catalog() = default;
|
||||
|
||||
static Catalog fromJson(const QByteArray &json);
|
||||
static Catalog bundled();
|
||||
|
||||
bool isEmpty() const;
|
||||
int splitThreshold() const;
|
||||
int collapseThreshold() const;
|
||||
int version() const;
|
||||
const QVector<Region> ®ions() const;
|
||||
const QVector<UseCase> &useCases() const;
|
||||
|
||||
const Entry *find(const QString &countryCode, const QString &isoCode) const;
|
||||
bool hasSubregions(const QString ®ionId) const;
|
||||
|
||||
bool isSplit(const QString ®ionId, int visibleCount, bool wasSplit) const;
|
||||
bool isSplit(const QString ®ionId, const QString &subregionId, int visibleCount, bool wasSplit) const;
|
||||
|
||||
private:
|
||||
QVector<Region> m_regions;
|
||||
QVector<UseCase> m_useCases;
|
||||
QHash<QString, Entry> m_byCode;
|
||||
QHash<QString, Entry> m_byIso;
|
||||
bool splitByCount(int visibleCount, bool wasSplit) const;
|
||||
|
||||
int m_splitThreshold = 15;
|
||||
int m_collapseThreshold = 13;
|
||||
int m_version = 0;
|
||||
};
|
||||
} // namespace countryCatalog
|
||||
|
||||
#endif // COUNTRYCATALOG_H
|
||||
@@ -11,5 +11,6 @@
|
||||
#define APP_IOS_BUNDLE_ID "@CLIENT_IOS_BUNDLE_ID@"
|
||||
#define APP_IOS_STORE_URL_FALLBACK "@CLIENT_IOS_STORE_URL_FALLBACK@"
|
||||
#define APP_ANDROID_PACKAGE "@CLIENT_ANDROID_PACKAGE@"
|
||||
#define CLIENT_ENABLE_APP_UPDATES @CLIENT_ENABLE_APP_UPDATES@
|
||||
|
||||
#endif
|
||||
@@ -4,7 +4,10 @@
|
||||
namespace apiDefs
|
||||
{
|
||||
|
||||
constexpr int requestTimeoutMsecs = 12 * 1000; // 12 secs
|
||||
constexpr int requestTimeoutMsecs = 6 * 1000;
|
||||
constexpr int proxyRequestTimeoutMsecs = 6 * 1000;
|
||||
constexpr int proxyStorageTimeoutMsecs = 6 * 1000;
|
||||
constexpr int proxyHealthTimeoutMsecs = 1000;
|
||||
|
||||
} // namespace apiDefs
|
||||
|
||||
|
||||
@@ -45,6 +45,8 @@ namespace apiDefs
|
||||
constexpr QLatin1String locale("locale");
|
||||
|
||||
constexpr QLatin1String captchaId("captcha_id");
|
||||
constexpr QLatin1String captchaImage("captcha_image");
|
||||
constexpr QLatin1String httpStatus("http_status");
|
||||
constexpr QLatin1String captchaSolution("captcha_solution");
|
||||
|
||||
constexpr QLatin1String activeDeviceCount("active_device_count");
|
||||
|
||||
@@ -13,6 +13,8 @@ namespace amnezia
|
||||
constexpr QLatin1String password("password");
|
||||
constexpr QLatin1String port("port");
|
||||
constexpr QLatin1String localPort("local_port");
|
||||
constexpr QLatin1String cascadeEndpoint("cascade_endpoint");
|
||||
constexpr QLatin1String cascadePort("cascade_port");
|
||||
|
||||
constexpr QLatin1String dns1("dns1");
|
||||
constexpr QLatin1String dns2("dns2");
|
||||
@@ -25,6 +27,7 @@ namespace amnezia
|
||||
constexpr QLatin1String accessToken("api_key");
|
||||
constexpr QLatin1String config("config");
|
||||
constexpr QLatin1String configVersion("config_version");
|
||||
constexpr QLatin1String formatVersion("format_version");
|
||||
|
||||
constexpr QLatin1String sendPayload("send_payload");
|
||||
constexpr QLatin1String sendPayloadEndpoint("endpoint");
|
||||
@@ -177,6 +180,8 @@ namespace amnezia
|
||||
|
||||
// ── Xray-specific keys ────────────────────────────────────────
|
||||
|
||||
constexpr QLatin1String xraySettingsVersion("xray_settings_version");
|
||||
|
||||
// Security
|
||||
constexpr QLatin1String xraySecurity("xray_security"); // none | tls | reality
|
||||
constexpr QLatin1String xrayFlow("xray_flow"); // "" | xtls-rprx-vision | xtls-rprx-vision-udp443
|
||||
@@ -215,11 +220,11 @@ namespace amnezia
|
||||
|
||||
// Transport — mKCP
|
||||
constexpr QLatin1String mkcpTti("mkcp_tti");
|
||||
constexpr QLatin1String mkcpMtu("mkcp_mtu");
|
||||
constexpr QLatin1String mkcpUplinkCapacity("mkcp_uplink_capacity");
|
||||
constexpr QLatin1String mkcpDownlinkCapacity("mkcp_downlink_capacity");
|
||||
constexpr QLatin1String mkcpReadBufferSize("mkcp_read_buffer_size");
|
||||
constexpr QLatin1String mkcpWriteBufferSize("mkcp_write_buffer_size");
|
||||
constexpr QLatin1String mkcpCongestion("mkcp_congestion"); // bool
|
||||
constexpr QLatin1String mkcpCwndMultiplier("mkcp_cwnd_multiplier");
|
||||
constexpr QLatin1String mkcpMaxSendingWindow("mkcp_max_sending_window");
|
||||
|
||||
// xPadding
|
||||
constexpr QLatin1String xPaddingBytesMin("xpadding_bytes_min");
|
||||
|
||||
@@ -48,26 +48,46 @@ namespace amnezia
|
||||
|
||||
namespace xray
|
||||
{
|
||||
constexpr char dataDir[] = "/opt/amnezia/xray";
|
||||
constexpr char serverConfigPath[] = "/opt/amnezia/xray/server.json";
|
||||
constexpr char expectedServerXrayRelease[] = "v26.7.28";
|
||||
constexpr char uuidPath[] = "/opt/amnezia/xray/xray_uuid.key";
|
||||
constexpr char PublicKeyPath[] = "/opt/amnezia/xray/xray_public.key";
|
||||
constexpr char PrivateKeyPath[] = "/opt/amnezia/xray/xray_private.key";
|
||||
constexpr char shortidPath[] = "/opt/amnezia/xray/xray_short_id.key";
|
||||
constexpr char defaultSite[] = "www.googletagmanager.com";
|
||||
constexpr char tlsCertPath[] = "/opt/amnezia/xray/tls_cert.pem";
|
||||
constexpr char tlsKeyPath[] = "/opt/amnezia/xray/tls_key.pem";
|
||||
constexpr const char *defaultSites[] = {
|
||||
"www.microsoft.com",
|
||||
"www.apple.com",
|
||||
"www.icloud.com",
|
||||
"dl.google.com",
|
||||
"www.bing.com",
|
||||
"www.samsung.com",
|
||||
"www.nvidia.com",
|
||||
"www.cisco.com",
|
||||
"www.asus.com",
|
||||
"www.logitech.com",
|
||||
"www.dropbox.com",
|
||||
"www.mozilla.org",
|
||||
"addons.mozilla.org",
|
||||
"www.speedtest.net",
|
||||
};
|
||||
|
||||
constexpr char defaultPort[] = "443";
|
||||
constexpr char defaultLocalProxyPort[] = "10808";
|
||||
constexpr char defaultLocalAddr[] = "10.33.0.2";
|
||||
constexpr char defaultLocalListenAddr[] = "127.0.0.1";
|
||||
|
||||
constexpr int settingsVersionCurrent = 2;
|
||||
|
||||
constexpr char defaultSecurity[] = "reality";
|
||||
constexpr char defaultFlow[] = "xtls-rprx-vision";
|
||||
constexpr char defaultTransport[] = "raw";
|
||||
constexpr char defaultFingerprint[] = "chrome";
|
||||
constexpr char defaultSni[] = "www.googletagmanager.com";
|
||||
constexpr char defaultFingerprint[] = "firefox";
|
||||
constexpr char defaultAlpn[] = "h2";
|
||||
|
||||
constexpr char defaultXhttpMode[] = "Auto";
|
||||
constexpr char defaultXhttpMode[] = "Stream-one";
|
||||
constexpr char defaultXhttpUplinkMethod[] = "POST";
|
||||
constexpr char defaultXhttpSessionPlacement[] = "Path";
|
||||
constexpr char defaultXhttpSessionKey[] = "";
|
||||
@@ -75,9 +95,10 @@ namespace amnezia
|
||||
constexpr char defaultXhttpUplinkDataPlacement[] = "Body";
|
||||
|
||||
constexpr char defaultXhttpHost[] = "www.googletagmanager.com";
|
||||
constexpr char defaultXhttpPath[] = "/";
|
||||
constexpr char defaultXhttpUplinkChunkSize[] = "0";
|
||||
constexpr char defaultXhttpScMaxEachPostBytesMin[] = "1";
|
||||
constexpr char defaultXhttpScMaxEachPostBytesMax[] = "100";
|
||||
constexpr char defaultXhttpScMaxEachPostBytesMin[] = "1000000";
|
||||
constexpr char defaultXhttpScMaxEachPostBytesMax[] = "1000000";
|
||||
constexpr char defaultXhttpScMinPostsIntervalMsMin[] = "100";
|
||||
constexpr char defaultXhttpScMinPostsIntervalMsMax[] = "800";
|
||||
constexpr char defaultXhttpScStreamUpServerSecsMin[] = "1";
|
||||
@@ -87,19 +108,22 @@ namespace amnezia
|
||||
constexpr char defaultXPaddingMethod[] = "Repeat-x";
|
||||
constexpr char defaultXPaddingKey[] = "x_padding";
|
||||
constexpr char defaultXPaddingHeader[] = "X-Padding";
|
||||
constexpr char defaultXPaddingBytesMin[] = "1";
|
||||
constexpr char defaultXPaddingBytesMax[] = "256";
|
||||
constexpr char defaultXPaddingBytesMin[] = "100";
|
||||
constexpr char defaultXPaddingBytesMax[] = "1000";
|
||||
|
||||
constexpr char defaultMkcpTti[] = "50";
|
||||
constexpr char defaultMkcpUplinkCapacity[] = "5";
|
||||
constexpr char defaultMkcpDownlinkCapacity[] = "20";
|
||||
constexpr char defaultMkcpReadBufferSize[] = "2";
|
||||
constexpr char defaultMkcpWriteBufferSize[] = "2";
|
||||
constexpr char defaultMkcpMtu[] = "1350";
|
||||
constexpr char defaultMkcpCwndMultiplier[] = "1";
|
||||
|
||||
constexpr char outbounds[] = "outbounds";
|
||||
constexpr char inbounds[] = "inbounds";
|
||||
constexpr char settings[] = "settings";
|
||||
constexpr char streamSettings[] = "streamSettings";
|
||||
constexpr char tag[] = "tag";
|
||||
constexpr char sockopt[] = "sockopt";
|
||||
constexpr char dialerProxy[] = "dialerProxy";
|
||||
constexpr char vnext[] = "vnext";
|
||||
constexpr char users[] = "users";
|
||||
constexpr char servers[] = "servers";
|
||||
@@ -120,6 +144,92 @@ namespace amnezia
|
||||
constexpr char spiderX[] = "spiderX";
|
||||
constexpr char user[] = "user";
|
||||
constexpr char pass[] = "pass";
|
||||
|
||||
constexpr char logBlock[] = "log";
|
||||
constexpr char logLevel[] = "loglevel";
|
||||
constexpr char logLevelError[] = "error";
|
||||
constexpr char protocol[] = "protocol";
|
||||
constexpr char protocolVless[] = "vless";
|
||||
constexpr char protocolFreedom[] = "freedom";
|
||||
constexpr char protocolSocks[] = "socks";
|
||||
constexpr char decryption[] = "decryption";
|
||||
constexpr char decryptionNone[] = "none";
|
||||
constexpr char listen[] = "listen";
|
||||
constexpr char udp[] = "udp";
|
||||
|
||||
constexpr char tlsSettings[] = "tlsSettings";
|
||||
constexpr char xhttpSettings[] = "xhttpSettings";
|
||||
constexpr char kcpSettings[] = "kcpSettings";
|
||||
constexpr char alpn[] = "alpn";
|
||||
constexpr char certificates[] = "certificates";
|
||||
constexpr char certificateFile[] = "certificateFile";
|
||||
constexpr char keyFile[] = "keyFile";
|
||||
constexpr char allowInsecure[] = "allowInsecure";
|
||||
constexpr char pinnedPeerCertSha256[] = "pinnedPeerCertSha256";
|
||||
|
||||
constexpr char dest[] = "dest";
|
||||
constexpr char privateKey[] = "privateKey";
|
||||
constexpr char shortIds[] = "shortIds";
|
||||
|
||||
constexpr char xhttpHost[] = "host";
|
||||
constexpr char xhttpPath[] = "path";
|
||||
constexpr char xhttpMode[] = "mode";
|
||||
constexpr char noGrpcHeader[] = "noGRPCHeader";
|
||||
constexpr char noSseHeader[] = "noSSEHeader";
|
||||
constexpr char sessionIdPlacement[] = "sessionIDPlacement";
|
||||
constexpr char sessionIdKey[] = "sessionIDKey";
|
||||
constexpr char seqPlacement[] = "seqPlacement";
|
||||
constexpr char seqKey[] = "seqKey";
|
||||
constexpr char uplinkDataPlacement[] = "uplinkDataPlacement";
|
||||
constexpr char uplinkDataKey[] = "uplinkDataKey";
|
||||
constexpr char uplinkHttpMethod[] = "uplinkHTTPMethod";
|
||||
constexpr char uplinkChunkSize[] = "uplinkChunkSize";
|
||||
constexpr char scMaxBufferedPosts[] = "scMaxBufferedPosts";
|
||||
constexpr char scMaxEachPostBytes[] = "scMaxEachPostBytes";
|
||||
constexpr char scMinPostsIntervalMs[] = "scMinPostsIntervalMs";
|
||||
constexpr char scStreamUpServerSecs[] = "scStreamUpServerSecs";
|
||||
constexpr char xPaddingObfsMode[] = "xPaddingObfsMode";
|
||||
constexpr char xPaddingBytes[] = "xPaddingBytes";
|
||||
constexpr char xPaddingKey[] = "xPaddingKey";
|
||||
constexpr char xPaddingHeader[] = "xPaddingHeader";
|
||||
constexpr char xPaddingPlacement[] = "xPaddingPlacement";
|
||||
constexpr char xPaddingMethod[] = "xPaddingMethod";
|
||||
|
||||
constexpr char xmux[] = "xmux";
|
||||
constexpr char xmuxMaxConcurrency[] = "maxConcurrency";
|
||||
constexpr char xmuxMaxConnections[] = "maxConnections";
|
||||
constexpr char xmuxCMaxReuseTimes[] = "cMaxReuseTimes";
|
||||
constexpr char xmuxHMaxRequestTimes[] = "hMaxRequestTimes";
|
||||
constexpr char xmuxHMaxReusableSecs[] = "hMaxReusableSecs";
|
||||
constexpr char xmuxHKeepAlivePeriod[] = "hKeepAlivePeriod";
|
||||
|
||||
constexpr char kcpTti[] = "tti";
|
||||
constexpr char kcpMtu[] = "mtu";
|
||||
constexpr char kcpUplinkCapacity[] = "uplinkCapacity";
|
||||
constexpr char kcpDownlinkCapacity[] = "downlinkCapacity";
|
||||
constexpr char kcpCwndMultiplier[] = "cwndMultiplier";
|
||||
constexpr char kcpMaxSendingWindow[] = "maxSendingWindow";
|
||||
|
||||
constexpr char legacyXhttpMethod[] = "method";
|
||||
constexpr char legacySessionPlacement[] = "sessionPlacement";
|
||||
constexpr char legacyScSessionPlacement[] = "scSessionPlacement";
|
||||
constexpr char legacyScSeqPlacement[] = "scSeqPlacement";
|
||||
constexpr char legacyScUplinkDataPlacement[] = "scUplinkDataPlacement";
|
||||
constexpr char legacySessionKey[] = "sessionKey";
|
||||
constexpr char legacyUplinkChunkSize[] = "xhttpUplinkChunkSize";
|
||||
constexpr char legacyXPaddingBlock[] = "xPadding";
|
||||
constexpr char legacyRangeFrom[] = "from";
|
||||
constexpr char legacyRangeTo[] = "to";
|
||||
|
||||
constexpr char networkTcp[] = "tcp";
|
||||
constexpr char networkXhttp[] = "xhttp";
|
||||
constexpr char networkKcp[] = "kcp";
|
||||
constexpr char transportRaw[] = "raw";
|
||||
constexpr char transportXhttp[] = "xhttp";
|
||||
constexpr char transportMkcp[] = "mkcp";
|
||||
constexpr char securityNone[] = "none";
|
||||
constexpr char securityTls[] = "tls";
|
||||
constexpr char securityReality[] = "reality";
|
||||
}
|
||||
|
||||
namespace cloak
|
||||
|
||||
@@ -37,9 +37,17 @@ namespace amnezia
|
||||
ServerLinuxKernelTooOld = 214,
|
||||
XrayServerConfigInvalid = 215,
|
||||
XrayServerNoVlessClients = 216,
|
||||
XrayRealityKeysReadFailed = 217,
|
||||
XrayKeysReadFailed = 217,
|
||||
ServerContainerRuntimeNotSupported = 218,
|
||||
ContainerRuntimeServiceNotRunning = 219,
|
||||
XrayKeyMigrationFailed = 220,
|
||||
XrayTlsNotSupported = 221,
|
||||
XrayServerConfigRejected = 222,
|
||||
XrayServerConfigRolledBack = 223,
|
||||
XrayServerNotServing = 224,
|
||||
ServerDataVolumeNotRemoved = 225,
|
||||
XrayKeyGenerationFailed = 226,
|
||||
XrayKeyMigrationNeedsConfirm = 227,
|
||||
|
||||
// Ssh connection errors
|
||||
SshRequestDeniedError = 300,
|
||||
@@ -82,6 +90,9 @@ namespace amnezia
|
||||
RestoreBackupInvalidError = 904,
|
||||
LegacyApiV1NotSupportedError = 905,
|
||||
LegacyContainerNotSupportedError = 906,
|
||||
ConfigFormatVersionNotSupportedError = 907,
|
||||
RestoreBackupUnsupportedConfigsSkipped = 908,
|
||||
ImportXraySettingsFileUseXrayPage = 909,
|
||||
|
||||
// Android errors
|
||||
AndroidError = 1000,
|
||||
|
||||
@@ -36,9 +36,45 @@ QString errorString(ErrorCode code) {
|
||||
case(ErrorCode::XrayServerNoVlessClients):
|
||||
errorMessage = QObject::tr("Server error: XRay server has no VLESS clients");
|
||||
break;
|
||||
case(ErrorCode::XrayRealityKeysReadFailed):
|
||||
case(ErrorCode::XrayKeysReadFailed):
|
||||
errorMessage = QObject::tr("Server error: failed to read XRay Reality keys from the server");
|
||||
break;
|
||||
case(ErrorCode::XrayTlsNotSupported):
|
||||
errorMessage = QObject::tr("Server error: could not create a TLS certificate on the XRay server. "
|
||||
"The previous settings were left unchanged. Try again, or use Reality.");
|
||||
break;
|
||||
case(ErrorCode::XrayServerConfigRejected):
|
||||
errorMessage = QObject::tr("Server error: XRay rejected the new configuration. "
|
||||
"Nothing was changed, the server keeps running with the previous settings.");
|
||||
break;
|
||||
case(ErrorCode::XrayServerConfigRolledBack):
|
||||
errorMessage = QObject::tr("Server error: XRay did not start with the new configuration. "
|
||||
"The previous settings were restored and the server is running again.");
|
||||
break;
|
||||
case(ErrorCode::XrayServerNotServing):
|
||||
errorMessage = QObject::tr("Server error: XRay is not accepting connections and the previous configuration "
|
||||
"could not be restored. The server needs attention: check it over SSH, "
|
||||
"or reinstall the protocol.");
|
||||
break;
|
||||
case(ErrorCode::ServerDataVolumeNotRemoved):
|
||||
errorMessage = QObject::tr("Server error: the previous data volume could not be removed. "
|
||||
"Installing on top of it would reuse the old keys, so the configurations you "
|
||||
"expected to revoke would keep working. Check the server and try again.");
|
||||
break;
|
||||
case(ErrorCode::XrayKeyGenerationFailed):
|
||||
errorMessage = QObject::tr("Server error: XRay could not generate its keys on the server. "
|
||||
"Check the server and try again.");
|
||||
break;
|
||||
case(ErrorCode::XrayKeyMigrationFailed):
|
||||
errorMessage = QObject::tr("Server error: could not preserve the XRay keys stored on the server. "
|
||||
"The settings were not changed, so the existing configurations keep working. "
|
||||
"Check that the server is reachable and try again.");
|
||||
break;
|
||||
case(ErrorCode::XrayKeyMigrationNeedsConfirm):
|
||||
errorMessage = QObject::tr("The old XRay container will not start, so its keys cannot be preserved. "
|
||||
"Applying these settings will recreate it with new keys, and all existing "
|
||||
"configurations will stop working.");
|
||||
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;
|
||||
|
||||
@@ -69,8 +105,11 @@ QString errorString(ErrorCode code) {
|
||||
|
||||
case (ErrorCode::ImportInvalidConfigError): errorMessage = QObject::tr("The config does not contain any containers and credentials for connecting to the server"); break;
|
||||
case (ErrorCode::ImportBackupFileUseRestoreInstead): errorMessage = QObject::tr("Backup files cannot be imported here. Use 'Restore from backup' instead."); break;
|
||||
case (ErrorCode::ImportXraySettingsFileUseXrayPage): errorMessage = QObject::tr("This is an XRay settings file, not a connection config. Import it on the XRay Configurations page using 'Import settings'."); 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::ConfigFormatVersionNotSupportedError): errorMessage = QObject::tr("This configuration was created in a newer version of the application and is not fully supported. Please update the application"); break;
|
||||
case (ErrorCode::RestoreBackupUnsupportedConfigsSkipped): errorMessage = QObject::tr("Some configurations from the backup were not restored because they require a newer version of the application"); 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;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
#ifndef LOCALPROXYDEFS_H
|
||||
#define LOCALPROXYDEFS_H
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
namespace amnezia::localProxy
|
||||
{
|
||||
constexpr int defaultProxyPort = 10808;
|
||||
constexpr int proxyPortMin = 1024;
|
||||
constexpr int proxyPortMax = 65535;
|
||||
constexpr quint16 apiPort = 49490;
|
||||
constexpr int portReleaseWaitMs = 800;
|
||||
}
|
||||
|
||||
#endif // LOCALPROXYDEFS_H
|
||||
@@ -0,0 +1,51 @@
|
||||
#include "portUtils.h"
|
||||
|
||||
#include "localProxyConstants.h"
|
||||
|
||||
#include <QElapsedTimer>
|
||||
#include <QHostAddress>
|
||||
#include <QTcpServer>
|
||||
#include <QThread>
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
bool portUtils::isPortAvailable(int port)
|
||||
{
|
||||
if (port < localProxy::proxyPortMin || port > localProxy::proxyPortMax) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QTcpServer server;
|
||||
const bool success = server.listen(QHostAddress::LocalHost, static_cast<quint16>(port));
|
||||
server.close();
|
||||
return success;
|
||||
}
|
||||
|
||||
bool portUtils::waitForPort(int port, int timeoutMs, const PortCheck &isAvailable)
|
||||
{
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
|
||||
while (!isAvailable(port)) {
|
||||
if (timer.elapsed() >= timeoutMs) {
|
||||
return false;
|
||||
}
|
||||
QThread::msleep(200);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<int> portUtils::findFirstAvailablePort(int startPort, int endPort, const PortCheck &isAvailable)
|
||||
{
|
||||
startPort = qMax(startPort, localProxy::proxyPortMin);
|
||||
endPort = qMin(endPort, localProxy::proxyPortMax);
|
||||
|
||||
for (int port = startPort; port <= endPort; ++port) {
|
||||
if (isAvailable(port)) {
|
||||
return port;
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#ifndef PORTUTILS_H
|
||||
#define PORTUTILS_H
|
||||
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
|
||||
namespace portUtils
|
||||
{
|
||||
using PortCheck = std::function<bool(int)>;
|
||||
|
||||
bool isPortAvailable(int port);
|
||||
bool waitForPort(int port, int timeoutMs, const PortCheck &isAvailable = isPortAvailable);
|
||||
std::optional<int> findFirstAvailablePort(int startPort, int endPort, const PortCheck &isAvailable = isPortAvailable);
|
||||
}
|
||||
|
||||
#endif // PORTUTILS_H
|
||||
@@ -0,0 +1,47 @@
|
||||
#include "xrayConfigUtils.h"
|
||||
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
std::optional<QString> xrayConfigUtils::extractLastConfig(const QJsonObject &serverConfig)
|
||||
{
|
||||
const QJsonArray containers = serverConfig.value(configKey::containers).toArray();
|
||||
const QString targetContainer = ContainerUtils::containerToString(DockerContainer::Xray);
|
||||
|
||||
for (const QJsonValue &value : containers) {
|
||||
const QJsonObject container = value.toObject();
|
||||
if (container.value(configKey::container).toString() != targetContainer) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const QJsonObject proto = container.value(QString(configKey::xray)).toObject();
|
||||
const QString serialized = proto.value(configKey::lastConfig).toString();
|
||||
if (!serialized.isEmpty()) {
|
||||
return serialized;
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool xrayConfigUtils::setInboundPort(QJsonObject &xrayConfig, int port)
|
||||
{
|
||||
if (!xrayConfig.value(QLatin1String("inbounds")).isArray()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonArray inbounds = xrayConfig.value(QLatin1String("inbounds")).toArray();
|
||||
if (inbounds.isEmpty() || !inbounds.at(0).isObject()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonObject firstInbound = inbounds.at(0).toObject();
|
||||
firstInbound.insert(QLatin1String("port"), port);
|
||||
inbounds[0] = firstInbound;
|
||||
xrayConfig.insert(QLatin1String("inbounds"), inbounds);
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#ifndef XRAYCONFIGUTILS_H
|
||||
#define XRAYCONFIGUTILS_H
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
|
||||
namespace xrayConfigUtils
|
||||
{
|
||||
std::optional<QString> extractLastConfig(const QJsonObject &serverConfig);
|
||||
bool setInboundPort(QJsonObject &xrayConfig, int port);
|
||||
}
|
||||
|
||||
#endif // XRAYCONFIGUTILS_H
|
||||
@@ -60,6 +60,7 @@ QString amnezia::scriptName(SharedScriptType type)
|
||||
case SharedScriptType::check_connection: return QLatin1String("check_connection.sh");
|
||||
case SharedScriptType::check_server_is_busy: return QLatin1String("check_server_is_busy.sh");
|
||||
case SharedScriptType::check_user_in_sudo: return QLatin1String("check_user_in_sudo.sh");
|
||||
case SharedScriptType::update_server_packages: return QLatin1String("update_server_packages.sh");
|
||||
default: return QString();
|
||||
}
|
||||
}
|
||||
@@ -74,7 +75,6 @@ QString amnezia::scriptName(ProtocolScriptType type)
|
||||
case ProtocolScriptType::openvpn_template: return QLatin1String("template.ovpn");
|
||||
case ProtocolScriptType::wireguard_template: return QLatin1String("template.conf");
|
||||
case ProtocolScriptType::awg_template: return QLatin1String("template.conf");
|
||||
case ProtocolScriptType::xray_template: return QLatin1String("template.json");
|
||||
default: return QString();
|
||||
}
|
||||
}
|
||||
@@ -208,7 +208,7 @@ amnezia::ScriptVars amnezia::genXrayVars(const ContainerConfig &containerConfig)
|
||||
if (auto* xrayProtocolConfig = containerConfig.getXrayProtocolConfig()) {
|
||||
const XrayServerConfig& config = xrayProtocolConfig->serverConfig;
|
||||
|
||||
vars.append({ { "$XRAY_SITE_NAME", config.site.isEmpty() ? protocols::xray::defaultSite : config.site } });
|
||||
vars.append({ { "$XRAY_SITE_NAME", config.site } });
|
||||
vars.append({ { "$XRAY_SERVER_PORT", config.port.isEmpty() ? protocols::xray::defaultPort : config.port } });
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,8 @@ enum SharedScriptType {
|
||||
setup_host_firewall,
|
||||
check_connection,
|
||||
check_server_is_busy,
|
||||
check_user_in_sudo
|
||||
check_user_in_sudo,
|
||||
update_server_packages
|
||||
};
|
||||
enum ProtocolScriptType {
|
||||
// Protocol scripts
|
||||
@@ -38,8 +39,7 @@ enum ProtocolScriptType {
|
||||
container_startup,
|
||||
openvpn_template,
|
||||
wireguard_template,
|
||||
awg_template,
|
||||
xray_template
|
||||
awg_template
|
||||
};
|
||||
|
||||
enum ClientScriptType {
|
||||
|
||||
@@ -34,6 +34,16 @@ bool hasThirdPartyConfig(const QJsonObject &json)
|
||||
namespace serverConfigUtils
|
||||
{
|
||||
|
||||
int configFormatVersion(const QJsonObject &serverConfigObject)
|
||||
{
|
||||
return serverConfigObject.value(amnezia::configKey::formatVersion).toInt(0);
|
||||
}
|
||||
|
||||
bool isConfigFormatVersionSupported(const QJsonObject &serverConfigObject)
|
||||
{
|
||||
return configFormatVersion(serverConfigObject) <= currentConfigFormatVersion;
|
||||
}
|
||||
|
||||
bool isServerFromApi(const QJsonObject &serverConfigObject)
|
||||
{
|
||||
const int configVersion = serverConfigObject.value(amnezia::configKey::configVersion).toInt();
|
||||
|
||||
@@ -25,6 +25,12 @@ enum ConfigSource {
|
||||
AmneziaGateway
|
||||
};
|
||||
|
||||
constexpr int currentConfigFormatVersion = 1;
|
||||
|
||||
int configFormatVersion(const QJsonObject &serverConfigObject);
|
||||
|
||||
bool isConfigFormatVersionSupported(const QJsonObject &serverConfigObject);
|
||||
|
||||
bool isServerFromApi(const QJsonObject &serverConfigObject);
|
||||
|
||||
ConfigSource getConfigSource(const QJsonObject &serverConfigObject);
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg x="94.969" y="72.212" width="840" height="809.053" viewBox="0 0 840 809.053" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="mask0_5470_4284" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="841" height="810">
|
||||
<path d="M838.697 289.781C839.128 290.057 839.092 290.7 838.633 290.929C835.038 292.719 831.485 294.6 827.806 296.232C799.555 308.776 772.521 323.383 749.551 344.372C741.699 351.545 734.598 359.689 727.949 368.025C721.643 375.923 719.536 385.597 719.631 395.64C719.651 397.39 719.936 399.14 720.232 401.108C720.434 402.455 721.585 403.47 722.947 403.47H754.934C756.426 403.47 757.641 404.682 757.573 406.173C757.006 418.719 752.651 429.513 745.064 438.533C734.995 450.489 730.891 464.7 726.144 478.966C719.221 499.763 718.756 521.202 720.398 542.478C722.108 564.655 713.899 582.337 700.656 598.83C690.259 611.783 680.19 625.011 669.847 638.019C659.244 651.355 655.291 666.414 656.426 683.344C657.89 705.043 662.979 725.963 667.727 747.088C671.598 764.332 672.871 782.151 675.21 799.723C675.317 800.514 675.272 801.319 675.174 802.406C675.006 804.292 672.993 805.496 671.237 804.786C667.425 803.244 663.961 801.983 661.064 799.914C647.862 790.5 639.736 777.081 633.087 762.665C629.836 755.612 626.686 748.509 623.528 741.499C623.236 740.851 622.7 740.346 622.038 740.086C612.637 736.39 610.069 734.129 607.429 727.667C607.012 726.646 606.042 725.945 604.94 725.931C590.847 725.747 576.823 727.121 563.328 731.798C539.442 740.065 515.514 748.249 491.969 757.391C465.73 767.585 438.573 769.648 411.239 766.355C385.068 763.198 359.197 757.514 333.176 753.1C324.448 751.625 315.61 750.75 306.841 749.425C304.666 749.097 302.504 748.249 300.507 747.279C284.746 739.6 269.178 731.511 253.267 724.159C236.015 716.193 222.69 721.877 216.301 739.559C213.675 746.828 212.115 754.467 209.57 761.764C205.685 772.9 201.622 784.009 197.148 794.913C193.962 802.692 187.887 806.658 178.915 807.795C177.278 808.003 175.934 806.595 176.072 804.951C176.687 797.655 177.055 790.615 178.241 783.722C180.676 769.607 184.781 755.724 186.272 741.527C187.749 727.343 187.024 712.913 186.86 698.607C186.833 696.53 184.918 694.125 183.289 692.458C171.62 680.597 159.95 668.736 147.993 657.176C135.338 644.933 123.039 632.28 114.585 616.812C106.896 602.751 94.8842 600.743 81.5728 603.339C60.1486 607.52 38.9845 612.959 17.7108 617.905C12.3942 619.143 7.11331 620.525 1.82366 621.848C1.1673 622.013 0.484093 621.706 0.169055 621.108C-0.123688 620.551 -0.0299188 619.87 0.401333 619.413C2.93984 616.719 5.31074 614.184 8.16162 611.811C24.2639 598.42 40.7356 585.452 56.6464 571.842C66.1408 563.712 74.9923 554.803 83.7754 545.894C85.5402 544.104 86.6484 540.674 86.4705 538.105C82.0653 474.457 96.8679 414.73 123.327 357.407C134.518 333.152 146.142 309.094 157.878 284.322C158.345 283.335 159.369 282.751 160.46 282.687C170.461 282.106 171.998 271.978 175.957 264.599C178.669 259.543 180.643 254.095 182.891 248.386C183.584 246.625 185.831 246.144 187.204 247.447C188.931 249.086 190.564 250.638 192.338 252.327C193.394 253.332 195.059 253.317 196.13 252.327C199.595 249.128 203.201 246.202 205.753 242.545C215.658 228.389 225.399 214.096 234.415 199.379C238.574 192.588 243.198 188.325 251.16 186.097C277.787 178.649 304.16 170.335 330.424 162.392C331.068 162.197 331.64 161.784 332.011 161.223C332.138 161.03 332.228 160.859 332.246 160.682C334.189 145.022 335.461 143.71 351.303 141.114C368.172 138.354 378.802 129.336 381.647 112.282C384.479 95.2702 385.314 77.9165 386.969 60.6994C388.351 46.2015 389.377 31.6626 391.087 17.2057C392.145 8.24311 394.608 3.97571 400.598 0.355299C401.894 -0.42824 403.557 0.142784 404.243 1.49332C407.282 7.4772 410.711 13.3289 412.744 19.6243C418.257 36.7184 422.635 54.1678 427.998 71.3166C432.608 86.0468 437.465 100.736 443.047 115.125C450.517 134.405 465.798 142.576 485.854 142.918C493.611 143.055 501.368 143.328 509.111 142.986C518.592 142.576 527.403 144.653 534.995 150.092C555.53 164.822 576.257 179.32 596.012 195.047C610.924 206.928 624.546 220.435 639.396 233.918C640.083 234.542 641.043 234.751 641.954 234.576C649.443 233.136 656.045 237.059 662.73 241.666C663.822 242.419 664.193 243.86 663.612 245.053C661.428 249.534 659.268 253.957 656.99 258.64C656.297 260.065 656.98 261.773 658.432 262.408C673.511 269.004 684.03 280.872 695.731 291.217C699.822 294.825 704.213 298.118 708.55 301.438C716.718 307.697 725.76 308.995 735.693 306.494C754.901 301.657 774.095 296.751 793.439 292.556C808.443 289.3 822.647 287.5 838.432 289.683C838.526 289.696 838.617 289.729 838.697 289.781ZM432.82 707.072C432.825 707.068 432.831 707.065 432.837 707.065C448.553 707.065 464.269 707.243 479.985 706.956C484.185 706.874 488.809 706.259 492.489 704.401C512.19 694.466 532.013 684.724 551.07 673.656C570.73 662.246 572.385 656.575 566.557 634.343C561.304 614.325 551.604 596.22 541.343 578.511C529.208 557.55 519.741 536.083 519.139 511.268C518.825 498.246 515.076 485.757 508.879 474.074C503.393 463.73 494.897 458.838 483.446 458.688C451.775 458.278 420.761 462.063 390.595 472.27C312.614 498.683 265.114 562.824 263.801 642.911C263.486 662.232 271.859 675.35 288.755 683.002C300.534 688.345 312.806 692.786 325.173 696.585C360.261 707.365 396.485 708.363 432.804 707.079C432.81 707.079 432.Line truncated
|
||||
</mask>
|
||||
<g mask="url(#mask0_5470_4284)">
|
||||
<path d="M313.469 658.228C312.707 668.769 309.913 679.69 311.564 689.723C315.755 715.122 296.832 713.09 282.736 714.487C267.37 716.011 254.543 731.378 237.526 722.615C238.288 721.599 238.923 720.71 239.685 719.694C259.623 705.979 270.417 681.087 296.832 674.357C301.912 673.214 301.658 657.974 313.469 658.228Z" fill="#FEDAA5"/>
|
||||
<path d="M-8.20716 410.326C-8.20716 267.947 -8.20716 125.569 -8.32803 -16.8094C-8.32803 -20.9187 -7.48198 -21.7648 -3.37258 -21.7648C281.747 -21.6439 566.987 -21.6439 852.106 -21.7648C856.216 -21.7648 857.062 -20.9187 857.062 -16.8094C856.941 268.31 856.941 553.55 857.062 838.669C857.062 842.779 856.216 843.625 852.106 843.625C566.987 843.504 281.747 843.504 -3.37258 843.625C-7.48198 843.625 -8.32803 842.779 -8.32803 838.669C-8.20716 695.808 -8.20716 553.067 -8.20716 410.326ZM-0.350972 449.607C-0.350972 573.493 -0.471837 697.379 -0.471837 821.144C1.94545 825.858 -6.87765 835.89 4.72534 835.89C258.057 836.131 511.389 836.131 764.721 836.01C766.897 836.01 771.127 832.989 770.885 832.264C766.413 812.563 778.742 813.892 791.674 814.497C805.694 815.222 819.835 814.376 833.977 814.738C839.053 814.859 840.262 813.409 840.262 808.332C840.02 751.043 840.141 693.874 840.141 636.584C840.02 421.929 840.02 207.273 839.899 -7.38192C838.932 -8.22797 838.086 -9.67835 837.119 -9.67835C811.133 -9.67835 785.268 -9.67835 759.282 -9.31575C757.953 -9.31575 755.415 -5.20636 755.415 -3.15166C755.173 69.7296 755.415 142.49 756.382 215.25C756.624 232.655 747.075 239.544 733.418 243.654C728.825 244.862 724.715 245.467 720.122 246.675C702.597 246.675 684.467 241.236 677.095 267.343C672.381 283.66 657.152 296.955 646.637 311.579C641.681 315.205 637.209 319.798 631.77 322.336C606.751 334.423 583.062 315.568 556.351 319.315C526.86 323.424 507.486 331.521 506.519 362.583C506.519 364.033 521.526 393.84 493.742 376.753C492.533 376.028 490.842 376.725 489.271 376.725C469.087 363.309 444.914 377.088 424.246 366.935C423.4 365.364 422.554 363.793 421.587 362.222C421.708 354.124 422.433 346.026 421.708 338.049C420.62 326.808 422.191 320.403 436.212 322.82C444.551 324.27 458.088 327.05 461.11 317.864C472.108 284.627 488.667 254.29 502.929 222.744C511.148 204.494 492.172 186.968 489.875 168.355C487.579 150.467 472.834 135.238 478.514 116.142C486.491 89.7931 505.467 69.9714 520.817 47.974C522.388 46.4028 523.959 44.8315 525.651 43.1394C538.826 23.5594 554.175 6.63836 577.865 -0.371785C581.491 -2.30562 589.468 1.19945 588.259 -6.41501C587.292 -12.9417 580.403 -9.55748 576.535 -9.55748C391.371 -9.43662 206.206 -9.07402 20.9212 -10.1618C-0.713563 -10.2827 -4.09777 -2.78907 -0.592699 14.7363C-0.592699 154.818 -0.592699 295.021 -0.592699 435.103C-0.471835 439.938 -0.350972 444.772 -0.350972 449.607ZM640.352 95.8363C640.11 95.8363 639.868 95.8363 639.506 95.8363C639.506 64.049 639.506 32.2616 639.506 0.474274C639.506 -3.0308 641.077 -7.50278 634.429 -7.38192C627.782 -7.26105 629.716 -2.54734 629.716 0.715998C629.232 62.7195 628.87 124.723 628.386 186.726C628.386 193.616 631.287 198.088 637.572 200.505C638.055 200.747 640.231 198.692 640.231 197.846C640.352 163.762 640.352 129.799 640.352 95.8363Z" fill="#FCE8EC"/>
|
||||
<path d="M839.683 -7.14046C839.803 207.515 839.803 422.17 839.924 636.825C839.924 694.115 839.924 751.284 840.045 808.574C840.045 813.529 838.836 815.101 833.76 814.98C819.619 814.617 805.599 815.342 791.458 814.738C778.404 814.134 766.076 812.804 770.669 832.505C770.911 833.351 766.68 836.252 764.505 836.252C511.173 836.373 257.841 836.373 4.50893 836.131C-7.09406 836.131 1.60818 826.099 -0.688248 821.386C0.641262 820.056 1.97077 818.727 3.17942 817.397C16.3536 802.772 29.5279 788.148 42.5812 773.523C89.3558 772.798 136.251 772.556 183.026 770.381C188.465 770.139 189.673 787.664 196.321 772.919C198.617 767.843 202.848 763.733 206.111 759.14C205.386 762.524 202.968 766.876 204.177 769.172C207.803 775.82 210.583 768.326 212.396 766.996C224.12 757.569 234.151 768.205 243.941 770.622C250.347 772.194 254.578 783.313 259.412 790.323C263.642 796.487 267.268 803.135 271.136 809.541C271.378 804.223 271.861 798.905 271.982 793.708C272.103 788.873 271.499 784.038 272.103 779.204C274.037 763.975 270.773 749.713 290.233 738.231C305.824 729.045 329.151 716.959 324.921 689.039C323.108 677.194 324.921 664.866 325.162 652.659C337.249 643.956 349.456 635.133 361.543 626.431C378.584 611.806 398.285 602.379 419.558 595.852C420.162 595.852 420.766 595.852 421.371 595.852C422.337 595.49 423.425 595.006 424.392 594.644C448.323 597.061 445.543 577.36 449.774 563.702C480.594 551.979 515.645 574.58 545.257 551.616C553.475 545.21 564.474 541.826 571.122 534.453C582.241 522.125 593.844 512.818 610.886 509.918C620.313 508.346 635.542 514.511 638.201 502.424C640.981 489.854 636.026 475.713 634.455 462.176C640.256 454.924 645.937 447.793 651.738 440.542C656.573 425.554 666.363 437.036 673.615 436.795C697.908 435.707 701.776 413.468 713.137 399.448C734.047 373.583 746.617 342.762 756.89 311.821C766.68 282.693 757.495 251.872 757.495 221.656C757.616 147.566 757.495 73.597 757.615 -0.492919C757.615 -2.42675 758.582 -4.48145 759.066 -6.41528C760.516 -5.69009 761.967 -5.08577 763.296 -4.36058C762.329 -3.99798 761.241 -3.6354 760.275 -3.27281C760.637 -2.91021 760.879 -2.54762 761.241 -2.06416C761.846 -2.78935 762.571 -3.51453 763.175 -4.36058C788.919 -5.56922 814.301 -6.41528 839.683 -7.14046Z" fill="#F18928" stroke-width="1.34889" stroke="#F18928"/>
|
||||
<path d="M-0.807763 435.101C-0.807763 295.019 -0.807763 154.816 -0.807763 14.7345C-0.203441 14.7345 0.521746 14.6137 1.12607 14.6137C74.6117 14.6137 148.097 15.3388 221.583 14.1302C241.646 13.7676 255.546 19.3274 268.962 33.952C281.773 47.7305 297.969 58.3666 312.715 70.3322C325.768 81.8143 319.725 98.7353 319.362 110.943C318.395 146.114 318.395 181.89 307.034 216.216C304.254 224.797 304.617 234.587 301.716 243.168C295.673 261.298 306.551 281.482 292.651 299.129C281.532 313.27 273.675 330.07 264.369 345.661C256.15 358.836 244.184 369.955 242.613 386.634C241.767 386.876 240.921 386.997 240.075 387.239C222.187 383.734 200.794 403.193 186.17 378.657C186.17 378.657 185.203 379.02 185.082 379.262C180.731 392.919 166.469 393.403 157.162 395.82C116.068 406.577 73.5239 406.094 31.5839 403.072C4.51027 401.138 -4.0711 409.599 0.884341 434.98C0.400883 435.101 -0.203441 435.101 -0.807763 435.101Z" fill="#80C9C1" stroke-width="1.34889" stroke="#80C9C1"/>
|
||||
<path d="M264.488 345.543C273.794 329.951 281.65 313.151 292.77 299.01C306.669 281.364 295.792 261.179 301.835 243.05C304.736 234.468 304.252 224.799 307.153 216.097C318.393 181.892 318.393 145.996 319.481 110.824C319.844 98.4958 325.766 81.6956 312.833 70.2135C330.48 78.3114 354.169 68.2796 367.102 92.5734C377.979 112.999 393.934 87.3762 407.833 92.2108C410.371 83.992 417.381 78.3114 417.623 68.0379C417.865 59.94 420.161 42.1729 429.951 44.7111C460.288 52.6881 490.625 42.8981 520.479 48.337C505.129 70.3343 486.032 90.1561 478.176 116.505C472.495 135.48 487.241 150.83 489.537 168.718C491.834 187.331 510.809 204.856 502.591 223.107C488.329 254.653 471.77 285.111 460.772 318.227C457.75 327.413 444.213 324.633 435.874 323.183C421.732 320.766 420.282 327.05 421.37 338.412C422.095 346.389 421.37 354.487 421.249 362.585C404.207 361.739 387.165 361.376 370.244 359.805C365.168 359.321 360.333 355.937 355.378 353.882C357.795 349.048 355.499 341.433 363.717 339.862C364.563 339.741 366.014 336.72 365.651 336.115C360.938 329.589 355.982 323.304 350.906 317.14C350.785 317.019 347.884 318.59 346.796 319.799C324.799 345.18 299.659 361.497 264.488 345.543Z" fill="#95C7DB" stroke-width="1.34889" stroke="#95C7DB"/>
|
||||
<path d="M520.601 48.3367C490.747 43.0186 460.41 52.6878 430.073 44.7107C420.162 42.1726 417.987 59.9397 417.745 68.0376C417.382 78.3111 410.493 83.9917 407.955 92.2105C394.056 87.3759 378.101 112.999 367.224 92.5731C354.291 68.2793 330.602 78.3111 312.955 70.2132C298.21 58.2476 281.893 47.6115 269.203 33.8329C255.787 19.2083 241.766 13.6486 221.824 14.0112C148.096 15.3407 74.6107 14.6155 1.12514 14.6155C4.9928 7.84708 -7.33537 -6.77753 8.25614 -6.8984C190.641 -7.50272 373.146 -7.38185 555.53 -6.8984C562.903 -6.8984 570.155 -2.4264 577.528 -0.0091138C553.838 6.88016 538.489 23.9221 525.314 43.5021C523.743 45.0733 522.172 46.6446 520.601 48.3367Z" fill="#87ADD4" stroke-width="1.34889" stroke="#87ADD4"/>
|
||||
<path d="M498.602 502.547C499.81 490.098 505.854 483.934 519.028 482.967C532.806 482 524.104 468.947 527.73 461.937C536.07 445.62 541.75 427.611 540.904 408.515C546.222 406.581 551.903 405.372 556.617 402.471C573.054 392.319 584.778 376.969 605.929 371.168C617.532 367.904 629.377 368.75 640.255 365.003C660.197 358.114 659.231 336.479 668.054 321.613C678.931 312.911 683.887 300.945 690.172 288.859C695.973 277.86 687.15 256.83 709.268 255.742C715.795 255.379 718.212 252.116 719.3 246.556C723.893 245.348 728.585 244.232 733.178 243.023C744.056 296.445 721.476 339.984 682.316 369.838C655.242 390.385 656.088 414.437 651.737 440.302C645.935 447.554 640.255 454.685 634.453 461.937C628.289 473.661 617.291 467.738 608.347 468.826C600.37 469.793 592.392 467.496 585.02 473.661C560.243 494.57 531.477 505.085 498.602 502.547Z" fill="#FEDAA5" stroke-width="1.34889" stroke="#FEDAA5"/>
|
||||
<path d="M668.424 321.413C659.601 336.279 660.396 357.987 640.454 364.998C629.576 368.744 617.68 367.868 606.077 371.131C584.926 376.933 573.171 392.323 556.733 402.475C552.02 405.376 546.124 406.608 540.926 408.542C523.643 398.027 506.368 387.192 488.963 376.676C490.534 376.676 492.589 375.951 493.677 376.676C520.509 393.235 506.126 363.986 506.126 362.535C507.093 331.473 526.673 323.375 556.043 319.266C582.754 315.519 606.322 334.374 631.462 322.287C636.901 319.628 641.373 315.156 646.329 311.53C653.701 314.794 661.172 318.149 668.424 321.413Z" fill="#FFF1CF" stroke-width="1.34889" stroke-linejoin="bevel" stroke="#FFF1CF"/>
|
||||
<path d="M449.73 563.404C445.372 577.206 448.278 596.82 424.305 594.399C423.787 592.976 421.607 594.497 421.278 595.731C420.672 595.731 419.762 595.3 419.156 595.3C415.524 526.166 418.493 456.979 407.112 388.329C408.081 377.554 414.861 371.258 424.063 366.778C444.766 377.069 468.981 363.267 489.201 376.585C506.515 387.119 523.481 397.969 540.916 408.503C541.763 427.633 536.42 445.235 528.066 461.701C524.434 468.723 533.151 481.799 519.349 482.768C506.151 483.615 500.098 489.911 498.887 502.382C482.3 522.722 465.954 543.063 449.73 563.404Z" fill="#A29EA1" stroke-width="1.34889" stroke="#A29EA1"/>
|
||||
<path d="M-0.807763 435.103C-0.203441 435.103 0.400882 434.982 1.0052 434.982C75.216 437.642 149.427 441.147 223.517 432.686C202.003 460.122 180.731 482.966 139.032 479.34C93.95 475.472 48.0215 480.79 2.57644 482.241C1.48866 471.363 0.521747 460.485 -0.566033 449.607C-0.686898 444.773 -0.807763 439.938 -0.807763 435.103Z" fill="#797AAA" stroke-width="1.34889" stroke="#797AAA"/>
|
||||
<path d="M651.736 440.3C656.088 414.435 655.242 390.383 682.315 369.836C721.475 339.982 744.077 296.471 733.078 243.049C746.736 238.94 755.68 233.017 755.438 215.613C754.471 142.853 755.076 69.9712 755.196 -2.7892C755.196 -4.96476 757.614 -8.95329 759.064 -8.95329C785.05 -9.43674 810.915 -9.43674 836.901 -9.31588C837.868 -9.31588 838.714 -7.86551 839.681 -7.01946C814.299 -6.29427 788.918 -5.44822 763.536 -4.72303C762.086 -5.44822 760.635 -6.05253 759.306 -6.77772C758.822 -4.84389 757.855 -2.7892 757.855 -0.855369C757.735 73.2346 757.855 147.204 757.735 221.294C757.614 251.389 766.92 282.33 757.13 311.458C746.736 342.4 734.287 373.22 713.377 399.085C702.016 413.105 698.148 435.465 673.855 436.432C666.361 436.795 656.571 425.313 651.736 440.3Z" fill="#F8B570"/>
|
||||
<path d="M424.42 366.647C415.355 371.118 408.194 377.935 407.106 388.692C402.271 391.955 397.316 395.339 392.481 398.603C368.309 397.877 344.257 397.031 320.084 396.306C315.249 395.581 310.414 394.735 305.58 394.01C285.637 382.528 263.761 388.208 242.73 386.516C244.302 369.837 256.267 358.717 264.486 345.543C299.658 361.376 324.676 345.06 346.916 319.678C348.003 318.469 350.904 316.898 351.025 317.019C356.101 323.183 361.057 329.468 365.77 335.995C366.133 336.599 364.803 339.5 363.837 339.742C355.618 341.313 358.035 348.927 355.497 353.762C360.452 355.816 365.287 359.201 370.363 359.684C387.284 361.135 404.806 361.362 421.848 362.208C422.694 363.78 423.574 365.075 424.42 366.647Z" fill="#87ADD4"/>
|
||||
<path d="M640.578 95.6923C640.578 129.796 640.398 164.837 640.276 198.941C640.276 199.912 638.092 201.854 637.606 201.611C631.416 199.184 628.382 194.814 628.382 187.775C628.868 125.514 629.072 63.1936 629.558 0.932391C629.558 -2.34452 627.727 -7.33012 634.402 -7.45149C640.956 -7.57285 639.703 -3.06418 639.824 0.455463C639.824 32.375 639.728 63.6514 639.728 95.5709C639.971 95.6923 640.214 95.6923 640.578 95.6923Z" fill="#FCE8EC"/>
|
||||
<path d="M668.056 321.732C660.804 318.468 653.311 314.601 646.059 311.337C656.574 296.713 671.561 283.539 676.275 267.222C683.769 241.236 702.922 246.407 720.327 246.286C719.239 251.846 716.039 255.498 709.513 255.74C687.395 256.828 696.58 277.979 690.779 288.977C684.615 301.185 678.934 313.03 668.056 321.732Z" fill="#F8C9D1" stroke-width="1.34889" stroke="#F8C9D1"/>
|
||||
<path d="M577.525 -0.0101719C570.153 -2.42746 562.901 -6.89945 555.528 -6.89945C373.144 -7.26205 190.638 -7.50377 8.25367 -6.89945C-7.33785 -6.89945 4.86947 7.72515 1.12267 14.6144C0.518345 14.6144 -0.206842 14.7353 -0.811164 14.7353C-4.4371 -2.79006 -1.05289 -10.2837 20.5818 -10.1628C205.746 -9.07501 391.031 -9.55847 576.196 -9.31674C580.184 -9.31674 586.953 -12.7009 587.92 -6.17426C589.249 1.56107 581.272 -1.944 577.525 -0.0101719Z" fill="#95C7DB" stroke-width="1.34889" stroke="#95C7DB"/>
|
||||
<path d="M-0.686898 449.607C0.400883 460.485 1.3678 471.363 2.45558 482.24C1.97212 489.613 1.0052 496.986 1.0052 504.359C0.88434 606.368 0.88434 708.378 1.0052 810.388C1.0052 812.684 2.33472 814.98 3.0599 817.277C1.73039 818.606 0.400882 819.936 -0.807763 821.265C-0.807763 697.379 -0.686898 573.493 -0.686898 449.607Z" fill="#ADA0CC"/>
|
||||
<path d="M520.6 48.339C522.171 46.7677 523.743 45.1965 525.435 43.5044C523.743 45.0756 522.171 46.6469 520.6 48.339Z" fill="#95C7DB"/>
|
||||
<path d="M3.05605 817.275C2.33087 814.979 1.00135 812.682 1.00135 810.386C0.88049 708.376 0.88049 606.367 1.00135 504.357C1.00135 496.984 1.96827 489.612 2.45173 482.239C48.0176 480.909 93.8253 475.471 138.908 479.338C180.606 482.964 201.878 460.121 223.392 432.684C223.996 432.564 224.722 432.443 225.326 432.201C233.786 429.059 237.775 433.893 245.148 438.244C273.793 454.924 304.976 446.342 332.412 433.047C350.783 424.224 363.353 429.421 369.759 443.079C386.438 479.217 379.186 518.982 383.537 557.054C388.493 599.84 362.628 589.567 340.51 589.687C325.764 589.808 311.14 589.929 296.394 589.567C293.252 589.446 289.626 588.116 287.209 586.061C270.892 573.008 251.674 588.841 234.995 580.623C217.107 571.799 205.141 594.039 188.704 589.204C183.628 587.754 179.881 577.964 175.53 578.084C149.423 578.568 123.074 578.689 100.956 596.818C91.408 604.675 75.333 606.367 74.6078 622.079C73.3992 646.373 73.6409 670.787 73.7618 695.081C73.8826 717.441 74.1244 717.441 86.936 722.034C87.6612 722.276 87.842 724.11 88.2046 725.198C72.9757 741.273 57.8077 757.326 42.5787 773.281C29.4045 788.026 16.2303 802.651 3.05605 817.275Z" fill="#6E5EA6" stroke-width="1.34889" stroke="#6E5EA6"/>
|
||||
<path d="M88.1444 725.295C87.7818 724.208 87.6609 722.395 86.9357 722.153C74.1241 717.56 73.8824 717.681 73.7615 695.2C73.6406 670.906 73.5198 646.492 74.6076 622.198C75.3327 606.486 91.4077 604.793 100.956 596.937C123.074 578.808 149.423 578.566 175.529 578.203C179.76 578.082 183.627 587.872 188.704 589.323C205.141 594.157 217.107 571.918 234.995 580.741C251.553 588.96 270.891 573.127 287.208 586.18C289.625 588.114 293.251 589.565 296.394 589.685C311.139 590.048 325.764 589.927 340.509 589.806C362.628 589.685 388.372 599.959 383.537 557.173C379.186 519.101 386.438 479.336 369.759 443.198C363.474 429.54 350.783 424.343 332.411 433.166C304.975 446.461 273.792 455.042 245.147 438.363C237.775 434.012 233.786 429.177 225.326 432.32C230.281 417.333 235.222 402.117 240.177 387.13C241.023 386.888 241.895 386.753 242.741 386.511C263.771 388.203 285.629 382.493 305.571 393.976C310.406 394.701 315.274 395.559 320.108 396.284C323.371 401.844 386.47 403.709 392.414 398.626C397.249 395.363 402.287 391.866 407.121 388.603C418.482 457.133 415.687 526.836 419.313 595.85C398.041 602.255 378.219 611.683 361.298 626.428C340.993 625.582 320.204 626.791 300.382 623.286C272.1 618.33 250.345 630.538 228.468 644.437C219.403 650.118 209.492 651.568 199.944 648.788C167.19 639.482 137.336 659.424 139.028 692.541C140.237 714.78 137.336 727.954 110.746 720.461C105.307 718.89 98.6596 721.428 92.6164 722.032C91.166 722.999 89.5948 724.087 88.1444 725.295Z" fill="#797AAA" stroke-width="1.34889" stroke="#797AAA"/>
|
||||
<path d="M92.7403 721.915C98.7835 721.311 105.431 718.773 110.87 720.344C137.581 727.838 140.361 714.784 139.152 692.424C137.46 659.307 167.193 639.365 200.068 648.671C209.616 651.451 219.527 650.001 228.592 644.32C250.468 630.542 272.224 618.213 300.506 623.169C320.328 626.674 341.117 625.465 361.422 626.311C349.336 635.014 337.128 643.837 325.042 652.539C320.812 655.077 316.702 657.615 312.472 660.033C301.352 659.791 301.473 674.295 296.518 675.503C271.378 681.909 261.105 705.599 242.129 718.652C240.195 715.147 238.865 714.18 234.273 716.718C227.142 720.586 218.923 724.091 210.946 724.453C174.445 726.025 138.064 726.508 101.563 726.871C98.5418 726.992 95.641 723.607 92.7403 721.915Z" fill="#A29EA1" stroke-width="1.34889" stroke="#A29EA1"/>
|
||||
<path d="M498.602 502.543C531.477 505.202 560.243 494.566 585.141 473.656C592.514 467.492 600.491 469.789 608.468 468.822C617.412 467.734 628.41 473.656 634.575 461.933C636.146 475.469 640.98 489.611 638.321 502.18C635.662 514.388 620.433 508.103 611.006 509.674C593.964 512.575 582.361 521.761 571.242 534.21C564.473 541.703 553.475 545.087 545.377 551.372C515.765 574.457 480.714 551.735 449.894 563.459C465.969 543.154 482.285 522.848 498.602 502.543Z" fill="#F8B570"/>
|
||||
<path d="M92.7394 721.915C95.7085 723.67 98.6479 726.973 101.549 726.852C138.05 726.369 174.551 725.895 210.931 724.444C218.908 724.082 227.104 720.615 234.235 716.626C238.828 714.088 240.339 715.075 242.273 718.58C241.548 719.547 240.798 720.585 240.073 721.552C228.712 734.001 217.472 746.571 206.11 759.02C202.726 763.613 198.617 767.722 196.32 772.799C189.673 787.544 188.585 770.019 183.025 770.261C136.251 772.557 89.2649 772.636 42.4903 773.362C57.7193 757.287 72.8736 741.239 88.1025 725.285C89.5529 724.076 91.1681 723.003 92.7394 721.915Z" fill="#B59259" stroke-width="1.34889" stroke="#B59259"/>
|
||||
<path d="M206.107 758.899C217.469 746.45 228.709 733.88 240.07 721.431C256.266 729.771 268.473 715.146 283.098 713.696C296.514 712.366 314.523 714.3 310.534 690.127C308.963 680.458 311.622 670.185 312.347 660.153C316.577 657.615 320.687 655.077 324.917 652.659C324.796 664.746 322.862 677.195 324.675 689.04C328.906 716.959 305.579 729.046 289.987 738.231C270.528 749.714 273.791 763.975 271.858 779.204C271.253 783.918 271.858 788.874 271.737 793.708C271.616 799.026 271.132 804.344 270.891 809.541C267.023 803.136 263.397 796.488 259.167 790.324C254.332 783.193 250.102 772.194 243.696 770.623C233.906 768.206 223.874 757.691 212.15 766.997C210.458 768.327 207.679 775.82 203.932 769.173C202.965 766.755 205.382 762.404 206.107 758.899Z" fill="#F8B570"/>
|
||||
<path d="M240.193 387.12C235.238 402.107 230.403 417.215 225.448 432.202C224.843 432.323 224.118 432.444 223.514 432.686C149.424 441.267 75.2133 437.641 1.00251 434.982C-3.95294 409.721 4.74931 401.261 31.7021 403.074C73.642 405.975 116.186 406.458 157.28 395.822C166.587 393.405 180.849 392.8 185.2 379.264C185.321 378.901 186.288 378.659 186.288 378.659C200.912 403.074 222.426 383.736 240.193 387.12Z" fill="#A29EA1" stroke-width="1.34889" stroke="#A29EA1"/>
|
||||
<path d="M392.483 398.6C387.285 403.918 323.469 401.863 320.085 396.304C344.258 397.029 368.431 397.875 392.483 398.6Z" fill="#7D89C1" stroke-width="1.34889" stroke="#7D89C1"/>
|
||||
<path d="M320.079 396.306C315.245 395.581 310.41 394.735 305.575 394.01C310.41 394.735 315.245 395.581 320.079 396.306Z" fill="#7D89C1"/>
|
||||
</g>
|
||||
</svg>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 28 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg x="94.969" y="72.212" width="840" height="809.053" viewBox="0 0 840 809.053" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M838.697 289.781C839.129 290.058 839.092 290.701 838.633 290.929C835.039 292.719 831.485 294.6 827.806 296.232C799.555 308.776 772.522 323.383 749.552 344.372C741.699 351.546 734.598 359.69 727.95 368.025C721.643 375.923 719.536 385.597 719.632 395.64C719.651 397.39 719.936 399.14 720.232 401.108C720.435 402.455 721.585 403.47 722.947 403.47H754.934C756.426 403.47 757.641 404.683 757.574 406.173C757.007 418.72 752.651 429.514 745.064 438.533C734.995 450.489 730.891 464.7 726.144 478.966C719.221 499.763 718.756 521.203 720.398 542.478C722.108 564.655 713.899 582.337 700.656 598.83C690.259 611.784 680.19 625.011 669.847 638.019C659.245 651.356 655.291 666.414 656.426 683.344C657.89 705.043 662.979 725.963 667.727 747.088C671.598 764.333 672.871 782.151 675.21 799.723C675.317 800.514 675.272 801.319 675.175 802.406C675.006 804.292 672.993 805.496 671.237 804.786C667.425 803.245 663.961 801.983 661.064 799.915C647.862 790.5 639.736 777.082 633.087 762.666C629.837 755.612 626.686 748.51 623.528 741.499C623.236 740.852 622.7 740.346 622.039 740.087C612.637 736.391 610.069 734.129 607.43 727.667C607.013 726.646 606.042 725.945 604.94 725.931C590.847 725.748 576.824 727.122 563.328 731.798C539.442 740.065 515.514 748.25 491.969 757.391C465.73 767.585 438.573 769.648 411.239 766.355C385.068 763.199 359.197 757.514 333.176 753.101C324.448 751.625 315.61 750.75 306.841 749.425C304.666 749.097 302.504 748.25 300.507 747.28C284.746 739.6 269.178 731.511 253.267 724.159C236.015 716.193 222.69 721.877 216.301 739.559C213.675 746.829 212.115 754.467 209.57 761.764C205.685 772.9 201.622 784.009 197.148 794.914C193.962 802.692 187.887 806.658 178.915 807.795C177.278 808.003 175.934 806.595 176.072 804.951C176.687 797.655 177.055 790.615 178.241 783.722C180.676 769.607 184.781 755.724 186.272 741.527C187.749 727.343 187.024 712.914 186.86 698.607C186.833 696.53 184.917 694.125 183.289 692.458C171.62 680.597 159.95 668.737 147.993 657.177C135.338 644.933 123.039 632.28 114.584 616.812C106.896 602.751 94.8841 600.743 81.5727 603.339C60.1486 607.52 38.9844 612.959 17.7107 617.905C12.3941 619.143 7.11322 620.525 1.82357 621.849C1.16721 622.013 0.484001 621.707 0.168963 621.108C-0.12378 620.551 -0.0300104 619.87 0.401242 619.413C2.93975 616.72 5.31065 614.184 8.16154 611.811C24.2638 598.42 40.7355 585.452 56.6463 571.843C66.1408 563.712 74.9923 554.803 83.7753 545.894C85.5401 544.104 86.6483 540.674 86.4704 538.105C82.0652 474.457 96.8679 414.73 123.327 357.408C134.518 333.153 146.142 309.094 157.878 284.322C158.345 283.335 159.369 282.751 160.459 282.687C170.461 282.106 171.998 271.978 175.957 264.599C178.669 259.543 180.643 254.095 182.891 248.386C183.584 246.625 185.831 246.144 187.204 247.447C188.931 249.086 190.564 250.638 192.338 252.327C193.394 253.332 195.059 253.317 196.13 252.327C199.595 249.128 203.201 246.202 205.753 242.545C215.658 228.389 225.399 214.096 234.415 199.379C238.574 192.588 243.198 188.325 251.16 186.097C277.787 178.649 304.16 170.335 330.424 162.392C331.068 162.197 331.64 161.784 332.011 161.223C332.138 161.03 332.228 160.859 332.246 160.682C334.189 145.022 335.461 143.71 351.303 141.114C368.172 138.354 378.802 129.336 381.647 112.282C384.479 95.2703 385.314 77.9165 386.969 60.6994C388.351 46.2015 389.377 31.6626 391.087 17.2057C392.145 8.24311 394.608 3.97571 400.598 0.355299C401.894 -0.42824 403.557 0.142784 404.243 1.49332C407.282 7.4772 410.711 13.3289 412.744 19.6243C418.257 36.7184 422.635 54.1678 427.998 71.3166C432.608 86.0468 437.465 100.736 443.047 115.125C450.517 134.405 465.798 142.576 485.854 142.918C493.611 143.055 501.368 143.328 509.111 142.986C518.592 142.576 527.403 144.653 534.996 150.092C555.53 164.822 576.257 179.32 596.012 195.048C610.924 206.928 624.546 220.435 639.396 233.918C640.083 234.542 641.043 234.751 641.954 234.576C649.443 233.136 656.045 237.059 662.73 241.666C663.822 242.419 664.193 243.86 663.612 245.053C661.429 249.534 659.269 253.957 656.99 258.64C656.297 260.065 656.98 261.773 658.432 262.408C673.511 269.004 684.03 280.872 695.731 291.217C699.822 294.825 704.213 298.118 708.55 301.438C716.718 307.697 725.761 308.995 735.693 306.494C754.901 301.657 774.095 296.751 793.44 292.556C808.443 289.301 822.647 287.5 838.432 289.683C838.526 289.696 838.617 289.73 838.697 289.781ZM432.82 707.072C432.825 707.068 432.831 707.065 432.837 707.065C448.553 707.065 464.269 707.243 479.985 706.956C484.185 706.874 488.809 706.259 492.489 704.401C512.19 694.467 532.013 684.724 551.07 673.656C570.73 662.246 572.385 656.575 566.557 634.344C561.304 614.325 551.604 596.22 541.343 578.511C529.209 557.55 519.741 536.083 519.139 511.268C518.825 498.246 515.076 485.757 508.879 474.074C503.393 463.73 494.897 458.838 483.446 458.688C451.775 458.278 420.761 462.063 390.595 472.27C312.614 498.684 265.114 562.824 263.801 642.911C263.486 662.232 271.859 675.35 288.755 683.002C300.534 688.345 312.806 692.786 325.173 696.585C360.261 707.365 396.485 708.363 432.804 707.079C432.81 707.079 432.81Line truncated
|
||||
</svg>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.0 KiB |
Loaded 100 of 547 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user