mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-11-14 16:43:28 +09:00
Compare commits
51
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e73e989365 | ||
|
|
ad03473be0 | ||
|
|
44dd8ce8d3 | ||
|
|
97003c671c | ||
|
|
03313a45fa | ||
|
|
d2024edb75 | ||
|
|
67dcadcd42 | ||
|
|
a4b2b3e3ad | ||
|
|
3d540b22bf | ||
|
|
d670bca9d4 | ||
|
|
b09a4ecd8d | ||
|
|
c327d3e3c8 | ||
|
|
9c9e1700af | ||
|
|
eba2097d1d | ||
|
|
22de0c2a16 | ||
|
|
614973a4ce | ||
|
|
e554e9b8b4 | ||
|
|
d4833454ef | ||
|
|
9851b4bacb | ||
|
|
29ad1f0c02 | ||
|
|
d6c34b3f60 | ||
|
|
d8668742b4 | ||
|
|
5eab5fc18b | ||
|
|
b46a9e389f | ||
|
|
81b8cd05c2 | ||
|
|
d0a9f6e4d5 | ||
|
|
8a29b49fd7 | ||
|
|
1baa2d85bd | ||
|
|
e226fadb07 | ||
|
|
bf4bf9972d | ||
|
|
f781bf6a23 | ||
|
|
2fa0ec81ad | ||
|
|
1ee0a6c9c7 | ||
|
|
14c7aab0fb | ||
|
|
d3347e6007 | ||
|
|
026826970c | ||
|
|
d2d3545961 | ||
|
|
b7e2847393 | ||
|
|
bb56008c3d | ||
|
|
a53db6eafe | ||
|
|
433ecb448f | ||
|
|
ab12a0b3f0 | ||
|
|
5a192cec15 | ||
|
|
6fc65dba8a | ||
|
|
f65fd4a8c5 | ||
|
|
c877e1e5cb | ||
|
|
2cb12c596c | ||
|
|
5beae954c7 | ||
|
|
5583c0a2a9 | ||
|
|
2cb7b30d8a | ||
|
|
2f6714e278 |
No files matched your search
@@ -14,6 +14,7 @@ runs:
|
||||
- name: Setup provisioning profile
|
||||
shell: bash
|
||||
run: |
|
||||
PROFILES_DIR="$HOME/Library/MobileDevice/Provisioning Profiles"
|
||||
TEMP_FILE=$(mktemp)
|
||||
|
||||
echo "${{ inputs.provisioning_profile_base64 }}" | base64 --decode > "$TEMP_FILE"
|
||||
@@ -25,12 +26,6 @@ runs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PROFILE_EXTENSION=mobileprovision
|
||||
grep -aq "<string>OSX</string>" "$TEMP_FILE" && PROFILE_EXTENSION=provisionprofile
|
||||
|
||||
for PROFILES_DIR in "$HOME/Library/MobileDevice/Provisioning Profiles" "$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles"; do
|
||||
mkdir -p "$PROFILES_DIR"
|
||||
cp "$TEMP_FILE" "$PROFILES_DIR/$PROFILE_UUID.$PROFILE_EXTENSION"
|
||||
done
|
||||
rm -f "$TEMP_FILE"
|
||||
echo "Installed profile: $PROFILE_UUID.$PROFILE_EXTENSION"
|
||||
mkdir -p "$PROFILES_DIR"
|
||||
mv "$TEMP_FILE" "$PROFILES_DIR/$PROFILE_UUID.mobileprovision"
|
||||
echo "Installed profile: $PROFILE_UUID"
|
||||
@@ -1,23 +0,0 @@
|
||||
## 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}
|
||||
+49
-182
@@ -4,8 +4,6 @@ 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/
|
||||
@@ -90,7 +88,7 @@ jobs:
|
||||
host: 'linux'
|
||||
target: 'desktop'
|
||||
arch: 'linux_gcc_64'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools'
|
||||
dir: ${{ runner.temp }}
|
||||
setup-python: 'true'
|
||||
tools: 'tools_ifw'
|
||||
@@ -211,7 +209,7 @@ jobs:
|
||||
host: 'windows'
|
||||
target: 'desktop'
|
||||
arch: 'win64_msvc2022_64'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools'
|
||||
dir: ${{ runner.temp }}
|
||||
setup-python: 'true'
|
||||
tools: 'tools_ifw'
|
||||
@@ -326,7 +324,7 @@ jobs:
|
||||
# ------------------------------------------------------
|
||||
|
||||
Build-iOS:
|
||||
runs-on: macos-26
|
||||
runs-on: macos-latest
|
||||
|
||||
needs: Bake-Prebuilts-iOS
|
||||
if: ${{ always() }}
|
||||
@@ -417,19 +415,12 @@ jobs:
|
||||
APPSTORE_CONNECT_KEY_ID: ${{ secrets.APPSTORE_CONNECT_KEY_ID }}
|
||||
APPSTORE_CONNECT_ISSUER_ID: ${{ secrets.APPSTORE_CONNECT_ISSUER_ID }}
|
||||
APPSTORE_CONNECT_PRIVATE_KEY: ${{ secrets.APPSTORE_CONNECT_PRIVATE_KEY }}
|
||||
shell: bash
|
||||
run: |
|
||||
sh deploy/build.sh -t ios | sed -f deploy/hide_secrets.sed
|
||||
|
||||
- name: 'Archive and upload to TestFlight'
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
working-directory: deploy
|
||||
shell: bash
|
||||
env:
|
||||
APPSTORE_CONNECT_KEY_ID: ${{ secrets.APPSTORE_CONNECT_KEY_ID }}
|
||||
APPSTORE_CONNECT_ISSUER_ID: ${{ secrets.APPSTORE_CONNECT_ISSUER_ID }}
|
||||
APPSTORE_CONNECT_PRIVATE_KEY: ${{ secrets.APPSTORE_CONNECT_PRIVATE_KEY }}
|
||||
run: fastlane ios testflight_upload 2>&1 | sed -f hide_secrets.sed
|
||||
sh deploy/build.sh -t ios | \
|
||||
sed -e '/-Xcc -DPROD_AGW_PUBLIC_KEY/,/-Xcc/ { /-Xcc/!d; }' -e '/-Xcc -DPROD_AGW_PUBLIC_KEY/d' | \
|
||||
sed -e '/-Xcc -DDEV_AGW_PUBLIC_KEY/,/-Xcc/ { /-Xcc/!d; }' -e '/-Xcc -DDEV_AGW_PUBLIC_KEY/d' | \
|
||||
sed -e '/-DPROD_AGW_PUBLIC_KEY/,/-D/ { /-D/!d; }' -e '/-DPROD_AGW_PUBLIC_KEY/d' | \
|
||||
sed -e '/-DDEV_AGW_PUBLIC_KEY/,/-D/ { /-D/!d; }' -e '/-DDEV_AGW_PUBLIC_KEY/d'
|
||||
|
||||
# ------------------------------------------------------
|
||||
|
||||
@@ -481,7 +472,7 @@ jobs:
|
||||
# ------------------------------------------------------
|
||||
|
||||
Build-MacOS:
|
||||
runs-on: macos-26
|
||||
runs-on: macos-15
|
||||
|
||||
needs: Bake-Prebuilts-MacOS
|
||||
if: ${{ always() }}
|
||||
@@ -537,7 +528,7 @@ jobs:
|
||||
host: 'mac'
|
||||
target: 'desktop'
|
||||
arch: 'clang_64'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools'
|
||||
dir: ${{ runner.temp }}
|
||||
setup-python: 'true'
|
||||
set-env: 'true'
|
||||
@@ -638,7 +629,12 @@ jobs:
|
||||
|
||||
env:
|
||||
QT_VERSION: 6.10.1
|
||||
KEYCHAIN_PASSWORD: ""
|
||||
|
||||
MAC_TEAM_ID: ${{ secrets.MAC_TEAM_ID }}
|
||||
|
||||
MAC_APP_CERT_CERT: ${{ secrets.MAC_APP_CERT_CERT }}
|
||||
MAC_SIGNER_ID: ${{ secrets.MAC_SIGNER_ID }}
|
||||
MAC_APP_CERT_PW: ${{ secrets.MAC_APP_CERT_PW }}
|
||||
|
||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||
@@ -656,37 +652,13 @@ jobs:
|
||||
submodules: 'true'
|
||||
fetch-depth: 10
|
||||
|
||||
- uses: ./.github/actions/apple-setup-keychain
|
||||
id: setup-keychain
|
||||
- uses: ./.github/actions/apple-setup-provisioning-profile
|
||||
with:
|
||||
keychain-password: ${{ env.KEYCHAIN_PASSWORD }}
|
||||
provisioning_profile_base64: ${{ secrets.MAC_APP_PROVISIONING_PROFILE }}
|
||||
|
||||
- name: 'Install signing certificate'
|
||||
uses: ./.github/actions/apple-install-cert
|
||||
- uses: ./.github/actions/apple-setup-provisioning-profile
|
||||
with:
|
||||
keychain-path: ${{ steps.setup-keychain.outputs.keychain-path }}
|
||||
keychain-password: ${{ env.KEYCHAIN_PASSWORD }}
|
||||
cert-base64: ${{ secrets.IOS_SIGNING_CERT_BASE64 }}
|
||||
cert-password: ${{ secrets.IOS_SIGNING_CERT_PASSWORD }}
|
||||
|
||||
- name: 'Install installer certificate'
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
uses: ./.github/actions/apple-install-cert
|
||||
with:
|
||||
keychain-path: ${{ steps.setup-keychain.outputs.keychain-path }}
|
||||
keychain-password: ${{ env.KEYCHAIN_PASSWORD }}
|
||||
cert-base64: ${{ secrets.MAC_APPSTORE_INSTALLER_CERT_BASE64 }}
|
||||
cert-password: ${{ secrets.MAC_APPSTORE_INSTALLER_CERT_PASSWORD }}
|
||||
|
||||
- name: 'Install app provisioning profile'
|
||||
uses: ./.github/actions/apple-setup-provisioning-profile
|
||||
with:
|
||||
provisioning_profile_base64: ${{ secrets.APPSTORE_CONNECT_MAC_PROVISIONING }}
|
||||
|
||||
- name: 'Install NE provisioning profile'
|
||||
uses: ./.github/actions/apple-setup-provisioning-profile
|
||||
with:
|
||||
provisioning_profile_base64: ${{ secrets.APPSTORE_CONNECT_MAC_NE_PROVISIONING }}
|
||||
provisioning_profile_base64: ${{ secrets.MAC_NE_PROVISIONING_PROFILE }}
|
||||
|
||||
- name: 'Setup xcode'
|
||||
uses: maxim-lobanov/setup-xcode@v1
|
||||
@@ -699,12 +671,24 @@ jobs:
|
||||
version: ${{ env.QT_VERSION }}
|
||||
host: 'mac'
|
||||
target: 'desktop'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools qtmultimedia qthttpserver qtwebsockets'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools qtmultimedia'
|
||||
arch: 'clang_64'
|
||||
dir: ${{ runner.temp }}
|
||||
set-env: 'true'
|
||||
extra: '--base ${{ env.QT_MIRROR }}'
|
||||
|
||||
- name: 'Install go'
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.24'
|
||||
cache: false
|
||||
|
||||
- name: 'Setup gomobile'
|
||||
run: |
|
||||
export PATH=$PATH:~/go/bin
|
||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||
gomobile init
|
||||
|
||||
- name: 'Setup python'
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
@@ -714,28 +698,17 @@ jobs:
|
||||
run: pip install "conan==2.28.0"
|
||||
|
||||
- name: 'Build project'
|
||||
env:
|
||||
QT_INSTALL_DIR: ${{ runner.temp }}
|
||||
shell: bash
|
||||
run: deploy/build.sh -t macos-ne | sed -f deploy/hide_secrets.sed
|
||||
run: |
|
||||
export QT_BIN_DIR="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/macos/bin"
|
||||
bash deploy/build_macos_ne.sh
|
||||
|
||||
- name: 'Upload unpacked artifact'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: AmneziaVPN_MacOS_unpacked
|
||||
path: deploy/build/client/Release/AmneziaVPN.app
|
||||
path: deploy/build/client/AmneziaVPN.app
|
||||
retention-days: 7
|
||||
|
||||
- name: 'Archive and upload to TestFlight'
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
working-directory: deploy
|
||||
shell: bash
|
||||
env:
|
||||
APPSTORE_CONNECT_KEY_ID: ${{ secrets.APPSTORE_CONNECT_KEY_ID }}
|
||||
APPSTORE_CONNECT_ISSUER_ID: ${{ secrets.APPSTORE_CONNECT_ISSUER_ID }}
|
||||
APPSTORE_CONNECT_PRIVATE_KEY: ${{ secrets.APPSTORE_CONNECT_PRIVATE_KEY }}
|
||||
run: fastlane mac testflight_upload 2>&1 | sed -f hide_secrets.sed
|
||||
|
||||
# ------------------------------------------------------
|
||||
|
||||
Bake-Prebuilts-Android:
|
||||
@@ -829,8 +802,6 @@ 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 }}
|
||||
@@ -925,12 +896,6 @@ 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 }}
|
||||
@@ -968,7 +933,8 @@ jobs:
|
||||
fi
|
||||
|
||||
for abi in arm64-v8a armeabi-v7a x86 x86_64; do
|
||||
mv deploy/build/client/android-build/build/outputs/apk/oss/release/*-${abi}-release.apk \
|
||||
deploy/build.sh -t android --sign --abi ${abi} --build ./deploy/build/${abi}
|
||||
mv deploy/build/${abi}/client/android-build/AmneziaVPN.apk \
|
||||
$OUT/AmneziaVPN_${VERSION}_${LABEL}_${abi}.apk
|
||||
done
|
||||
|
||||
@@ -987,9 +953,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
|
||||
deploy/build.sh -t android --sign --aab --play --build ./deploy/build/play
|
||||
|
||||
mv deploy/build/client/android-build/build/outputs/bundle/playRelease/android-build-play-release.aab \
|
||||
mv deploy/build/play/client/android-build/build/outputs/bundle/playRelease/android-build-play-release.aab \
|
||||
$OUT/AmneziaVPN_${VERSION}_${LABEL}_play.aab
|
||||
|
||||
- name: 'Upload universal APK'
|
||||
@@ -1083,19 +1049,13 @@ jobs:
|
||||
- SettingsSignalsTest
|
||||
- UiServersModelAndControllerTest
|
||||
- XraySerializationTest
|
||||
- XrayServerInboundTest
|
||||
- XrayGeneratorMatrixTest
|
||||
- XrayConfigModelTest
|
||||
- XrayCoreAcceptsTest
|
||||
- UiLanguageModelAndControllerTest
|
||||
- UiIpModelAndControllerTest
|
||||
- UiAppSTModelAndControllerTest
|
||||
- UiAllowedDnsModelAndControllerTest
|
||||
- BackupExportImportTest
|
||||
- FavoriteLocationsRestoreTest
|
||||
- ConnectionTest
|
||||
- OpenSSLLibraryTest
|
||||
- GatewayServiceImportTest
|
||||
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
@@ -1144,7 +1104,7 @@ jobs:
|
||||
host: linux
|
||||
target: desktop
|
||||
arch: linux_gcc_64
|
||||
modules: qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets
|
||||
modules: qtremoteobjects qt5compat qtshadertools
|
||||
dir: ${{ runner.temp }}
|
||||
set-env: true
|
||||
aqtversion: '==3.3.0'
|
||||
@@ -1171,20 +1131,7 @@ jobs:
|
||||
-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"
|
||||
@@ -1192,7 +1139,7 @@ jobs:
|
||||
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 }}$"
|
||||
ctest --test-dir deploy/build/client/tests --output-on-failure -VV -R "^${{ matrix.filter }}$"
|
||||
|
||||
# ------------------------------------------------------
|
||||
|
||||
@@ -1231,7 +1178,7 @@ jobs:
|
||||
host: linux
|
||||
target: desktop
|
||||
arch: linux_gcc_64
|
||||
modules: qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets
|
||||
modules: qtremoteobjects qt5compat qtshadertools
|
||||
dir: ${{ runner.temp }}
|
||||
set-env: true
|
||||
aqtversion: '==3.3.0'
|
||||
@@ -1258,16 +1205,6 @@ jobs:
|
||||
-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: |
|
||||
@@ -1277,8 +1214,7 @@ jobs:
|
||||
export QT_QPA_PLATFORM=offscreen
|
||||
|
||||
ctest --test-dir deploy/build/client/tests --output-on-failure -VV \
|
||||
--timeout 3600 \
|
||||
-R "^(SelfHostedServerSetupTest|MultipleExportsTest|AdminSelfHostedExportTest|XrayVpsMatrixTest)$"
|
||||
-R "^(SelfHostedServerSetupTest|MultipleExportsTest|AdminSelfHostedExportTest)$"
|
||||
|
||||
# ------------------------------------------------------
|
||||
|
||||
@@ -1300,19 +1236,13 @@ jobs:
|
||||
- SettingsSignalsTest
|
||||
- UiServersModelAndControllerTest
|
||||
- XraySerializationTest
|
||||
- XrayServerInboundTest
|
||||
- XrayGeneratorMatrixTest
|
||||
- XrayConfigModelTest
|
||||
- XrayCoreAcceptsTest
|
||||
- UiLanguageModelAndControllerTest
|
||||
- UiIpModelAndControllerTest
|
||||
- UiAppSTModelAndControllerTest
|
||||
- UiAllowedDnsModelAndControllerTest
|
||||
- BackupExportImportTest
|
||||
- FavoriteLocationsRestoreTest
|
||||
- ConnectionTest
|
||||
- OpenSSLLibraryTest
|
||||
- GatewayServiceImportTest
|
||||
|
||||
runs-on: windows-latest
|
||||
|
||||
@@ -1369,7 +1299,7 @@ jobs:
|
||||
host: windows
|
||||
target: desktop
|
||||
arch: win64_msvc2022_64
|
||||
modules: qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets
|
||||
modules: qtremoteobjects qt5compat qtshadertools
|
||||
dir: ${{ runner.temp }}
|
||||
set-env: true
|
||||
|
||||
@@ -1378,20 +1308,7 @@ jobs:
|
||||
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
|
||||
@@ -1401,7 +1318,7 @@ jobs:
|
||||
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
|
||||
ctest --test-dir deploy\build\client\tests -C Release -R "${{ matrix.filter }}" --output-on-failure
|
||||
|
||||
# ------------------------------------------------------
|
||||
|
||||
@@ -1428,10 +1345,8 @@ jobs:
|
||||
- UiAppSTModelAndControllerTest
|
||||
- UiAllowedDnsModelAndControllerTest
|
||||
- BackupExportImportTest
|
||||
- FavoriteLocationsRestoreTest
|
||||
- ConnectionTest
|
||||
- OpenSSLLibraryTest
|
||||
- GatewayServiceImportTest
|
||||
|
||||
runs-on: macos-15
|
||||
|
||||
@@ -1491,7 +1406,7 @@ jobs:
|
||||
host: mac
|
||||
target: desktop
|
||||
arch: clang_64
|
||||
modules: qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets
|
||||
modules: qtremoteobjects qt5compat qtshadertools
|
||||
dir: ${{ runner.temp }}
|
||||
set-env: true
|
||||
|
||||
@@ -1503,15 +1418,13 @@ jobs:
|
||||
-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
|
||||
ctest --test-dir deploy/build/client/tests -R "${{ matrix.filter }}" --output-on-failure
|
||||
|
||||
# ------------------------------------------------------
|
||||
|
||||
@@ -1681,49 +1594,3 @@ jobs:
|
||||
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,4 +146,3 @@ macos-with-sign-build.sh
|
||||
DeveloperIdApplicationCertificate.p12
|
||||
DeveloperIdInstallerCertificate.p12
|
||||
|
||||
gwenv.sh
|
||||
@@ -1,219 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,6 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
The operating guide for this repository lives in [AGENTS.md](AGENTS.md), so
|
||||
every agent runtime reads the same file.
|
||||
|
||||
@AGENTS.md
|
||||
+2
-2
@@ -5,7 +5,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
set(PROJECT AmneziaVPN)
|
||||
if(NOT AMNEZIAVPN_VERSION)
|
||||
set(AMNEZIAVPN_VERSION 5.1.1.1 CACHE STRING "Client app version")
|
||||
set(AMNEZIAVPN_VERSION 5.0.3.1 CACHE STRING "Client app version")
|
||||
endif()
|
||||
|
||||
set(QT_CREATOR_SKIP_PACKAGE_MANAGER_SETUP ON CACHE BOOL "" FORCE)
|
||||
@@ -34,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 2192)
|
||||
set(APP_ANDROID_VERSION_CODE 2171)
|
||||
if(DEFINED APP_ANDROID_VERSION_CODE_OFFSET)
|
||||
math(EXPR APP_ANDROID_VERSION_CODE "${APP_ANDROID_VERSION_CODE} + ${APP_ANDROID_VERSION_CODE_OFFSET}")
|
||||
endif()
|
||||
|
||||
+2
-18
@@ -37,30 +37,21 @@ 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 HttpServer)
|
||||
set(PACKAGES ${PACKAGES} Widgets)
|
||||
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 Qt6::HttpServer)
|
||||
set(LIBS ${LIBS} Qt6::Widgets)
|
||||
endif()
|
||||
|
||||
qt_standard_project_setup()
|
||||
@@ -80,7 +71,6 @@ 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
|
||||
@@ -103,8 +93,6 @@ else()
|
||||
${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()
|
||||
|
||||
@@ -227,10 +215,6 @@ 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)
|
||||
|
||||
@@ -119,6 +119,9 @@ void AmneziaApplication::init()
|
||||
// install filter on main window
|
||||
if (auto win = qobject_cast<QQuickWindow*>(obj)) {
|
||||
win->installEventFilter(this);
|
||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
||||
win->setDefaultAlphaBuffer(true);
|
||||
#endif
|
||||
#ifdef Q_OS_ANDROID
|
||||
QObject::connect(win, &QQuickWindow::sceneGraphError,
|
||||
[](QQuickWindow::SceneGraphError, const QString &msg) {
|
||||
@@ -129,11 +132,6 @@ 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
|
||||
|
||||
@@ -54,7 +54,6 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +111,7 @@ android {
|
||||
isEnable = abiList.size > 1
|
||||
reset()
|
||||
include(*abiList.toTypedArray())
|
||||
isUniversalApk = true
|
||||
isUniversalApk = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,8 +122,7 @@ android {
|
||||
outputs.map { it as BaseVariantOutputImpl }
|
||||
.forEach { output ->
|
||||
if (output.outputFileName.endsWith(".apk")) {
|
||||
output.outputFileName = listOfNotNull(outputBaseName, output.getFilter("ABI"), buildType.name)
|
||||
.joinToString("-", postfix = ".apk")
|
||||
output.outputFileName = "$outputBaseName-${buildType.name}.apk"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,15 +143,14 @@ 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()
|
||||
File(srcDir, universalOutput.outputFileName)
|
||||
.copyTo(File(dstDir, "$base-$buildTypeName$unsignedSuffix.apk"), overwrite = true)
|
||||
srcDir.listFiles()?.filter { it.name.endsWith(".apk") }?.forEach { apk ->
|
||||
apk.copyTo(File(dstDir, "$base-$buildTypeName$unsignedSuffix.apk"), overwrite = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFE8E8EC"
|
||||
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="#38FFFFFF" />
|
||||
</shape>
|
||||
@@ -8,4 +8,75 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<org.amnezia.vpn.PairingQrScanOverlayView
|
||||
android:id="@+id/pairingScanOverlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/pairingChrome"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="top"
|
||||
android:background="@android:color/transparent"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingTop="28dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/pairingBack"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="top"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/pairing_qr_camera_back"
|
||||
android:padding="12dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_pairing_back" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pairingTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/pairing_qr_camera_title"
|
||||
android:textColor="#FFE8E8EC"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pairingSubtitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/pairing_qr_camera_subtitle"
|
||||
android:textColor="#FFB8B8C0"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/torchButton"
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="56dp"
|
||||
android:layout_gravity="bottom|center_horizontal"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:background="@drawable/torch_fab_bg"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center"
|
||||
android:text="🔦"
|
||||
android:textSize="26sp"
|
||||
android:contentDescription="@string/camera_torch" />
|
||||
|
||||
</FrameLayout>
|
||||
@@ -24,5 +24,13 @@
|
||||
<string name="notificationSettingsDialogMessage">Для показа уведомлений необходимо включить уведомления в системных настройках</string>
|
||||
<string name="openNotificationSettings">Открыть настройки уведомлений</string>
|
||||
|
||||
<string name="cameraPermissionDialogTitle">Доступ к камере</string>
|
||||
<string name="cameraPermissionDialogMessage">Чтобы отсканировать QR-код для добавления устройства, Amnezia VPN нужен доступ к камере.</string>
|
||||
<string name="cameraPermissionContinue">Продолжить</string>
|
||||
<string name="camera_torch">Фонарик</string>
|
||||
<string name="pairing_qr_camera_title">Добавить устройство по QR</string>
|
||||
<string name="pairing_qr_camera_subtitle">Отсканируйте QR сессии на устройстве, которое хотите добавить. Перед отправкой подписки будет подтверждение.</string>
|
||||
<string name="pairing_qr_camera_back">Назад</string>
|
||||
|
||||
<string name="tvNoFileBrowser">Пожалуйста, установите приложение для просмотра файлов</string>
|
||||
</resources>
|
||||
@@ -24,5 +24,13 @@
|
||||
<string name="notificationSettingsDialogMessage">To show notifications, you must enable notifications in the system settings</string>
|
||||
<string name="openNotificationSettings">Open notification settings</string>
|
||||
|
||||
<string name="cameraPermissionDialogTitle">Camera access</string>
|
||||
<string name="cameraPermissionDialogMessage">To scan a QR code for device pairing, Amnezia VPN needs access to the camera.</string>
|
||||
<string name="cameraPermissionContinue">Continue</string>
|
||||
<string name="camera_torch">Flashlight</string>
|
||||
<string name="pairing_qr_camera_title">Add device via QR</string>
|
||||
<string name="pairing_qr_camera_subtitle">Scan the session QR shown on the device you want to add. You will confirm before the subscription is sent.</string>
|
||||
<string name="pairing_qr_camera_back">Back</string>
|
||||
|
||||
<string name="tvNoFileBrowser">Please install a file management utility to browse files</string>
|
||||
</resources>
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="black">#FF101012</color>
|
||||
<color name="black">#FF0E0E11</color>
|
||||
<style name="NoActionBar">
|
||||
<item name="android:windowBackground">@color/black</item>
|
||||
<item name="android:colorBackground">@color/black</item>
|
||||
|
||||
@@ -37,15 +37,16 @@ import android.widget.Toast
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.OnApplyWindowInsetsListener
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LifecycleRegistry
|
||||
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
|
||||
@@ -75,13 +76,18 @@ private const val CHECK_VPN_PERMISSION_ACTION_CODE = 1
|
||||
private const val CREATE_FILE_ACTION_CODE = 2
|
||||
private const val OPEN_FILE_ACTION_CODE = 3
|
||||
private const val CHECK_NOTIFICATION_PERMISSION_ACTION_CODE = 4
|
||||
private const val CHECK_CAMERA_PERMISSION_ACTION_CODE = 5
|
||||
|
||||
private const val PREFS_NOTIFICATION_PERMISSION_ASKED = "NOTIFICATION_PERMISSION_ASKED"
|
||||
private const val OPEN_FILE_AFTER_RESUME_DELAY_MS = 400L
|
||||
private const val KEY_PENDING_OPEN_FILE_URI = "pending_open_file_uri"
|
||||
private const val FILE_PROVIDER_AUTHORITY = "org.amnezia.vpn.qtprovider"
|
||||
|
||||
class AmneziaActivity : QtActivity() {
|
||||
class AmneziaActivity : QtActivity(), LifecycleOwner {
|
||||
|
||||
private val lifecycleRegistry = LifecycleRegistry(this)
|
||||
|
||||
override val lifecycle: Lifecycle
|
||||
get() = lifecycleRegistry
|
||||
|
||||
private lateinit var mainScope: CoroutineScope
|
||||
private val qtInitialized = CompletableDeferred<Unit>()
|
||||
@@ -103,6 +109,8 @@ class AmneziaActivity : QtActivity() {
|
||||
private var pendingOpenFileUri: String? = null
|
||||
private var openFileDeliveryScheduled = false
|
||||
|
||||
private var lastPairingQrReaderStartUptimeMs: Long = 0L
|
||||
private var updateCoverDialog: Dialog? = null
|
||||
|
||||
private val vpnServiceEventHandler: Handler by lazy(NONE) {
|
||||
object : Handler(Looper.getMainLooper()) {
|
||||
@@ -210,6 +218,7 @@ class AmneziaActivity : QtActivity() {
|
||||
registerBroadcastReceivers()
|
||||
intent?.let(::processIntent)
|
||||
runBlocking { vpnProto = proto.await() }
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
|
||||
billingRepository = BillingPaymentRepository(applicationContext)
|
||||
}
|
||||
|
||||
@@ -268,6 +277,7 @@ class AmneziaActivity : QtActivity() {
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
|
||||
Log.d(TAG, "Start Amnezia activity")
|
||||
mainScope.launch {
|
||||
qtInitialized.await()
|
||||
@@ -291,6 +301,7 @@ class AmneziaActivity : QtActivity() {
|
||||
qtInitialized.await()
|
||||
QtAndroidController.onServiceDisconnected()
|
||||
}
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
@@ -363,6 +374,7 @@ class AmneziaActivity : QtActivity() {
|
||||
if (qtInitialized.isCompleted) {
|
||||
QtAndroidController.onActivityPaused()
|
||||
}
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
|
||||
super.onPause()
|
||||
isActivityResumed = false
|
||||
// Cancel all pending operations when activity pauses
|
||||
@@ -373,6 +385,7 @@ class AmneziaActivity : QtActivity() {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
||||
isActivityResumed = true
|
||||
Log.d(TAG, "Resume Amnezia activity")
|
||||
if (qtInitialized.isCompleted) {
|
||||
@@ -489,6 +502,7 @@ class AmneziaActivity : QtActivity() {
|
||||
unregisterBroadcastReceiver(notificationStateReceiver)
|
||||
notificationStateReceiver = null
|
||||
mainScope.cancel()
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
@@ -736,69 +750,28 @@ 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 = mime
|
||||
type = "text/*"
|
||||
putExtra(Intent.EXTRA_TITLE, fileName)
|
||||
}.also {
|
||||
try {
|
||||
startActivityForResult(it, CREATE_FILE_ACTION_CODE, ActivityResultHandler(
|
||||
onSuccess = {
|
||||
val uri = it?.data
|
||||
var saved = false
|
||||
if (uri != null) {
|
||||
it?.data?.let { uri ->
|
||||
Log.v(TAG, "Save file to $uri")
|
||||
try {
|
||||
contentResolver.openOutputStream(uri)?.use { os ->
|
||||
write(os)
|
||||
saved = true
|
||||
os.bufferedWriter().use { it.write(data) }
|
||||
}
|
||||
} 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()
|
||||
}
|
||||
@@ -954,6 +927,66 @@ class AmneziaActivity : QtActivity() {
|
||||
@SuppressLint("UnsupportedChromeOsCameraSystemFeature")
|
||||
fun isCameraPresent(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA)
|
||||
|
||||
@Suppress("unused")
|
||||
fun isCameraPermissionGranted(): Boolean =
|
||||
ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
|
||||
|
||||
@Suppress("unused")
|
||||
fun requestCameraPermissionForQrPairing() {
|
||||
if (isCameraPermissionGranted()) {
|
||||
mainScope.launch {
|
||||
qtInitialized.await()
|
||||
QtAndroidController.onCameraPermissionResult(true)
|
||||
}
|
||||
return
|
||||
}
|
||||
runOnUiThread {
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.cameraPermissionDialogTitle)
|
||||
.setMessage(R.string.cameraPermissionDialogMessage)
|
||||
.setNegativeButton(R.string.cancel) { _, _ ->
|
||||
mainScope.launch {
|
||||
qtInitialized.await()
|
||||
QtAndroidController.onCameraPermissionResult(false)
|
||||
}
|
||||
}
|
||||
.setPositiveButton(R.string.cameraPermissionContinue) { _, _ ->
|
||||
requestPermission(
|
||||
Manifest.permission.CAMERA,
|
||||
CHECK_CAMERA_PERMISSION_ACTION_CODE,
|
||||
PermissionRequestHandler(
|
||||
onSuccess = {
|
||||
mainScope.launch {
|
||||
qtInitialized.await()
|
||||
QtAndroidController.onCameraPermissionResult(true)
|
||||
}
|
||||
},
|
||||
onFail = {
|
||||
mainScope.launch {
|
||||
qtInitialized.await()
|
||||
QtAndroidController.onCameraPermissionResult(false)
|
||||
}
|
||||
},
|
||||
onAny = {}
|
||||
)
|
||||
)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun openApplicationDetailsSettings() {
|
||||
try {
|
||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||
data = Uri.fromParts("package", packageName, null)
|
||||
startActivity(this)
|
||||
}
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Log.e(TAG, "openApplicationDetailsSettings: $e")
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun isOnTv(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
||||
|
||||
@@ -1002,6 +1035,19 @@ class AmneziaActivity : QtActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun startPairingQrCodeReader() {
|
||||
val now = SystemClock.uptimeMillis()
|
||||
if (now - lastPairingQrReaderStartUptimeMs < 1200L) {
|
||||
return
|
||||
}
|
||||
lastPairingQrReaderStartUptimeMs = now
|
||||
Intent(this, CameraActivity::class.java).also {
|
||||
it.putExtra(CameraActivity.EXTRA_PAIRING_QR_CAMERA, true)
|
||||
startActivity(it)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun setSaveLogs(enabled: Boolean) {
|
||||
Log.v(TAG, "Set save logs: $enabled")
|
||||
@@ -1015,6 +1061,20 @@ 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")
|
||||
@@ -1268,6 +1328,7 @@ class AmneziaActivity : QtActivity() {
|
||||
CREATE_FILE_ACTION_CODE -> "CREATE_FILE"
|
||||
OPEN_FILE_ACTION_CODE -> "OPEN_FILE"
|
||||
CHECK_NOTIFICATION_PERMISSION_ACTION_CODE -> "CHECK_NOTIFICATION_PERMISSION"
|
||||
CHECK_CAMERA_PERMISSION_ACTION_CODE -> "CHECK_CAMERA_PERMISSION"
|
||||
else -> actionCode.toString()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,47 +2,384 @@ package org.amnezia.vpn
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.os.Bundle
|
||||
import android.view.MotionEvent.ACTION_DOWN
|
||||
import android.view.MotionEvent.ACTION_UP
|
||||
import android.graphics.RectF
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
|
||||
import androidx.camera.core.Camera
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.camera.core.ExperimentalGetImage
|
||||
import androidx.camera.core.FocusMeteringAction
|
||||
import androidx.camera.core.FocusMeteringAction.FLAG_AE
|
||||
import androidx.camera.core.FocusMeteringAction.FLAG_AF
|
||||
import androidx.camera.core.ImageAnalysis
|
||||
import androidx.camera.core.ImageProxy
|
||||
import androidx.camera.core.Preview
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import androidx.camera.view.PreviewView
|
||||
import androidx.camera.view.TransformExperimental
|
||||
import androidx.camera.view.transform.CoordinateTransform
|
||||
import androidx.camera.view.transform.ImageProxyTransformFactory
|
||||
import androidx.camera.view.transform.OutputTransform
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.lifecycle.Observer
|
||||
import com.google.mlkit.vision.barcode.BarcodeScanner
|
||||
import com.google.mlkit.vision.barcode.BarcodeScannerOptions.Builder
|
||||
import com.google.mlkit.vision.barcode.BarcodeScanning
|
||||
import com.google.mlkit.vision.barcode.ZoomSuggestionOptions
|
||||
import com.google.mlkit.vision.barcode.common.Barcode
|
||||
import com.google.mlkit.vision.common.InputImage
|
||||
import org.amnezia.vpn.databinding.CameraPreviewBinding
|
||||
import org.amnezia.vpn.qt.QtAndroidController
|
||||
import org.amnezia.vpn.util.Log
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private const val TAG = "CameraActivity"
|
||||
|
||||
@OptIn(TransformExperimental::class)
|
||||
class CameraActivity : ComponentActivity() {
|
||||
|
||||
companion object {
|
||||
const val EXTRA_PAIRING_QR_CAMERA = "org.amnezia.vpn.extra.PAIRING_QR_CAMERA"
|
||||
}
|
||||
|
||||
private lateinit var viewBinding: CameraPreviewBinding
|
||||
private lateinit var cameraProvider: ProcessCameraProvider
|
||||
private var cameraProvider: ProcessCameraProvider? = null
|
||||
private var boundCamera: Camera? = null
|
||||
private var boundImageAnalysis: ImageAnalysis? = null
|
||||
private var torchOn: Boolean = false
|
||||
|
||||
private var imageAnalysisExecutor: ExecutorService? = null
|
||||
|
||||
private val qrHandledOrClosing = AtomicBoolean(false)
|
||||
|
||||
private var pairingQrDeliveredToQt = false
|
||||
|
||||
private var pairingQrUserDismissedCamera = false
|
||||
|
||||
private var barcodeScanner: BarcodeScanner? = null
|
||||
|
||||
private val cachedPreviewOutputTransform = AtomicReference<OutputTransform?>(null)
|
||||
|
||||
private var previewTransformLayoutListener: View.OnLayoutChangeListener? = null
|
||||
|
||||
private var previewStreamStateObserver: Observer<PreviewView.StreamState>? = null
|
||||
|
||||
@Volatile
|
||||
private var pairingGeomHeaderBottomPx = 0f
|
||||
|
||||
@Volatile
|
||||
private var pairingGeomStatusBarTopPx = 0f
|
||||
|
||||
@Volatile
|
||||
private var pairingGeomDensity = 1f
|
||||
|
||||
@ExperimentalGetImage
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
viewBinding = CameraPreviewBinding.inflate(layoutInflater)
|
||||
setContentView(viewBinding.root)
|
||||
viewBinding.viewFinder.scaleType = PreviewView.ScaleType.FILL_CENTER
|
||||
|
||||
if (intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)) {
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
val density = resources.displayMetrics.density
|
||||
val padH = (8 * density).toInt()
|
||||
val padTopBase = (28 * density).toInt()
|
||||
val padBottom = (12 * density).toInt()
|
||||
ViewCompat.setOnApplyWindowInsetsListener(viewBinding.pairingChrome) { v, windowInsets ->
|
||||
val bars = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars())
|
||||
v.setPadding(padH, padTopBase + bars.top, (16 * density).toInt(), padBottom)
|
||||
v.post { onPairingLayoutGeometryChanged() }
|
||||
windowInsets
|
||||
}
|
||||
viewBinding.pairingScanOverlay.visibility = View.VISIBLE
|
||||
viewBinding.pairingChrome.visibility = View.VISIBLE
|
||||
viewBinding.root.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
|
||||
viewBinding.root.post { onPairingLayoutGeometryChanged() }
|
||||
}
|
||||
viewBinding.root.post {
|
||||
onPairingLayoutGeometryChanged()
|
||||
applyPairingTorchButtonChrome()
|
||||
}
|
||||
}
|
||||
|
||||
viewBinding.pairingBack.setOnClickListener { releaseCameraAndFinish() }
|
||||
|
||||
onBackPressedDispatcher.addCallback(
|
||||
this,
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
releaseCameraAndFinish()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
viewBinding.torchButton.setOnClickListener {
|
||||
torchOn = !torchOn
|
||||
try {
|
||||
boundCamera?.cameraControl?.enableTorch(torchOn)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Torch: $e")
|
||||
}
|
||||
if (intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)) {
|
||||
applyPairingTorchButtonChrome()
|
||||
}
|
||||
}
|
||||
|
||||
checkPermissions(onSuccess = ::startCamera, onFail = ::finish)
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
if (!intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)) {
|
||||
return
|
||||
}
|
||||
if (!::viewBinding.isInitialized) {
|
||||
return
|
||||
}
|
||||
cleanupCameraResources()
|
||||
qrHandledOrClosing.set(false)
|
||||
pairingQrDeliveredToQt = false
|
||||
pairingQrUserDismissedCamera = false
|
||||
torchOn = false
|
||||
viewBinding.pairingScanOverlay.visibility = View.VISIBLE
|
||||
viewBinding.pairingChrome.visibility = View.VISIBLE
|
||||
viewBinding.root.post {
|
||||
onPairingLayoutGeometryChanged()
|
||||
applyPairingTorchButtonChrome()
|
||||
}
|
||||
checkPermissions(onSuccess = ::startCamera, onFail = ::finish)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
cleanupCameraResources()
|
||||
val pairing = intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)
|
||||
if (pairing && !pairingQrDeliveredToQt && !pairingQrUserDismissedCamera) {
|
||||
try {
|
||||
QtAndroidController.onPairingQrCameraClosed()
|
||||
} catch (t: Throwable) {
|
||||
Log.e(TAG, "onPairingQrCameraClosed: $t")
|
||||
}
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
/** Idempotent: safe from back, successful decode, or process death. */
|
||||
private fun cleanupCameraResources() {
|
||||
qrHandledOrClosing.set(true)
|
||||
try {
|
||||
boundImageAnalysis?.clearAnalyzer()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
boundImageAnalysis = null
|
||||
try {
|
||||
barcodeScanner?.close()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
barcodeScanner = null
|
||||
try {
|
||||
boundCamera?.cameraControl?.enableTorch(false)
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
boundCamera = null
|
||||
try {
|
||||
cameraProvider?.unbindAll()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
imageAnalysisExecutor?.let { ex ->
|
||||
try {
|
||||
ex.shutdown()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
imageAnalysisExecutor = null
|
||||
previewTransformLayoutListener?.let { listener ->
|
||||
if (::viewBinding.isInitialized) {
|
||||
viewBinding.viewFinder.removeOnLayoutChangeListener(listener)
|
||||
}
|
||||
}
|
||||
previewTransformLayoutListener = null
|
||||
previewStreamStateObserver?.let { obs ->
|
||||
if (::viewBinding.isInitialized) {
|
||||
viewBinding.viewFinder.previewStreamState.removeObserver(obs)
|
||||
}
|
||||
}
|
||||
previewStreamStateObserver = null
|
||||
cachedPreviewOutputTransform.set(null)
|
||||
}
|
||||
|
||||
private fun refreshCachedPreviewOutputTransform() {
|
||||
if (!::viewBinding.isInitialized) {
|
||||
return
|
||||
}
|
||||
val vf = viewBinding.viewFinder
|
||||
try {
|
||||
val out = vf.outputTransform
|
||||
cachedPreviewOutputTransform.set(out)
|
||||
} catch (t: Throwable) {
|
||||
Log.e(TAG, "refreshCachedPreviewOutputTransform: $t")
|
||||
cachedPreviewOutputTransform.set(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun scheduleCachedPreviewOutputTransformRefresh() {
|
||||
if (!::viewBinding.isInitialized) {
|
||||
return
|
||||
}
|
||||
viewBinding.viewFinder.post { refreshCachedPreviewOutputTransform() }
|
||||
}
|
||||
|
||||
private fun onPairingLayoutGeometryChanged() {
|
||||
if (!::viewBinding.isInitialized || !intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)) {
|
||||
return
|
||||
}
|
||||
val root = viewBinding.root
|
||||
val chrome = viewBinding.pairingChrome
|
||||
val w = root.width
|
||||
val h = root.height
|
||||
if (w <= 0 || h <= 0) {
|
||||
return
|
||||
}
|
||||
val density = resources.displayMetrics.density
|
||||
val headerBottom = if (chrome.visibility == View.VISIBLE) chrome.bottom.toFloat() else 0f
|
||||
val insets = ViewCompat.getRootWindowInsets(root)
|
||||
val statusTop = insets?.getInsets(WindowInsetsCompat.Type.statusBars())?.top?.toFloat() ?: 0f
|
||||
val safeBottom = insets?.getInsets(WindowInsetsCompat.Type.systemBars())?.bottom?.toFloat() ?: 0f
|
||||
|
||||
pairingGeomHeaderBottomPx = headerBottom
|
||||
pairingGeomStatusBarTopPx = statusTop
|
||||
pairingGeomDensity = density
|
||||
|
||||
viewBinding.pairingScanOverlay.setPairingHeaderBottomPx(headerBottom)
|
||||
|
||||
val hole = PairingQrScanGeometry.pairingIosStyleHoleRectF(w, h, headerBottom, statusTop, density)
|
||||
val torchCy = PairingQrScanGeometry.pairingIosStyleTorchCenterYPx(
|
||||
hole.bottom,
|
||||
h.toFloat(),
|
||||
headerBottom,
|
||||
safeBottom,
|
||||
density
|
||||
)
|
||||
val torchSizePx = (56f * density).roundToInt().coerceAtLeast(1)
|
||||
val topMargin = (torchCy - torchSizePx / 2f).roundToInt().coerceAtLeast(0)
|
||||
val wantGravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
|
||||
viewBinding.torchButton.post {
|
||||
if (!::viewBinding.isInitialized || !intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)) {
|
||||
return@post
|
||||
}
|
||||
val btn = viewBinding.torchButton
|
||||
val lp = btn.layoutParams as FrameLayout.LayoutParams
|
||||
if (lp.gravity == wantGravity && lp.topMargin == topMargin && lp.bottomMargin == 0) {
|
||||
return@post
|
||||
}
|
||||
lp.gravity = wantGravity
|
||||
lp.topMargin = topMargin
|
||||
lp.bottomMargin = 0
|
||||
btn.layoutParams = lp
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyPairingTorchButtonChrome() {
|
||||
if (!::viewBinding.isInitialized || !intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)) {
|
||||
return
|
||||
}
|
||||
val btn = viewBinding.torchButton
|
||||
val d = resources.displayMetrics.density
|
||||
val alpha = if (torchOn) (0.42f * 255f).toInt() else (0.22f * 255f).toInt()
|
||||
val bg = GradientDrawable().apply {
|
||||
shape = GradientDrawable.OVAL
|
||||
setColor(Color.argb(alpha, 255, 255, 255))
|
||||
if (torchOn) {
|
||||
setStroke((2f * d).roundToInt(), Color.rgb(255, 191, 115))
|
||||
} else {
|
||||
setStroke(0, 0)
|
||||
}
|
||||
}
|
||||
btn.background = bg
|
||||
}
|
||||
|
||||
private fun pairingHoleRectInImageSpace(
|
||||
viewFinder: PreviewView,
|
||||
imageProxy: ImageProxy,
|
||||
imageWidth: Int,
|
||||
imageHeight: Int
|
||||
): RectF {
|
||||
val vw = viewFinder.width
|
||||
val vh = viewFinder.height
|
||||
fun geomFallback(): RectF =
|
||||
PairingQrScanGeometry.pairingIosStyleHoleInImageCoords(
|
||||
vw,
|
||||
vh,
|
||||
pairingGeomHeaderBottomPx,
|
||||
pairingGeomStatusBarTopPx,
|
||||
pairingGeomDensity,
|
||||
imageWidth,
|
||||
imageHeight
|
||||
)
|
||||
if (vw <= 0 || vh <= 0 || imageWidth <= 0 || imageHeight <= 0) {
|
||||
return geomFallback()
|
||||
}
|
||||
return try {
|
||||
val previewOut = cachedPreviewOutputTransform.get()
|
||||
if (previewOut == null) {
|
||||
geomFallback()
|
||||
} else {
|
||||
val imageFactory = ImageProxyTransformFactory().apply {
|
||||
setUsingRotationDegrees(true)
|
||||
}
|
||||
val imageOut = imageFactory.getOutputTransform(imageProxy)
|
||||
val holeView = PairingQrScanGeometry.pairingIosStyleHoleRectF(
|
||||
vw,
|
||||
vh,
|
||||
pairingGeomHeaderBottomPx,
|
||||
pairingGeomStatusBarTopPx,
|
||||
pairingGeomDensity
|
||||
)
|
||||
if (holeView.width() <= 0f || holeView.height() <= 0f) {
|
||||
return geomFallback()
|
||||
}
|
||||
val hole = RectF(holeView)
|
||||
CoordinateTransform(previewOut, imageOut).mapRect(hole)
|
||||
hole
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
Log.e(TAG, "pairingHoleRectInImageSpace: $t")
|
||||
geomFallback()
|
||||
}
|
||||
}
|
||||
|
||||
private fun releaseCameraAndFinish() {
|
||||
if (intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)) {
|
||||
pairingQrUserDismissedCamera = true
|
||||
try {
|
||||
QtAndroidController.onPairingQrCameraUserDismissed()
|
||||
} catch (t: Throwable) {
|
||||
Log.e(TAG, "onPairingQrCameraUserDismissed: $t")
|
||||
}
|
||||
}
|
||||
cleanupCameraResources()
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun checkPermissions(onSuccess: () -> Unit, onFail: () -> Unit) {
|
||||
if (checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
||||
onSuccess()
|
||||
@@ -67,26 +404,41 @@ class CameraActivity : ComponentActivity() {
|
||||
|
||||
cameraProviderFuture.addListener({
|
||||
cameraProvider = cameraProviderFuture.get()
|
||||
bindPreview()
|
||||
bindImageAnalysis()
|
||||
bindCameraUseCases()
|
||||
}, ContextCompat.getMainExecutor(this))
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private fun bindPreview() {
|
||||
@ExperimentalGetImage
|
||||
private fun bindCameraUseCases() {
|
||||
val provider = cameraProvider ?: return
|
||||
imageAnalysisExecutor?.shutdown()
|
||||
imageAnalysisExecutor = Executors.newSingleThreadExecutor()
|
||||
|
||||
val viewFinder = viewBinding.viewFinder
|
||||
val preview = Preview.Builder().build().also {
|
||||
it.setSurfaceProvider(viewFinder.surfaceProvider)
|
||||
}
|
||||
|
||||
val camera = cameraProvider.bindToLifecycle(this, CameraSelector.DEFAULT_BACK_CAMERA, preview)
|
||||
val imageAnalysis = ImageAnalysis.Builder()
|
||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||
.build()
|
||||
|
||||
val camera = provider.bindToLifecycle(
|
||||
this,
|
||||
CameraSelector.DEFAULT_BACK_CAMERA,
|
||||
preview,
|
||||
imageAnalysis
|
||||
)
|
||||
boundCamera = camera
|
||||
boundImageAnalysis = imageAnalysis
|
||||
|
||||
viewFinder.setOnTouchListener { _, motionEvent ->
|
||||
when (motionEvent.action) {
|
||||
ACTION_DOWN -> true
|
||||
ACTION_UP -> {
|
||||
val point = viewFinder
|
||||
.meteringPointFactory.createPoint(motionEvent.x, motionEvent.x)
|
||||
.meteringPointFactory.createPoint(motionEvent.x, motionEvent.y)
|
||||
|
||||
val action = FocusMeteringAction
|
||||
.Builder(point, FLAG_AF or FLAG_AE).build()
|
||||
@@ -98,58 +450,121 @@ class CameraActivity : ComponentActivity() {
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ExperimentalGetImage
|
||||
private fun bindImageAnalysis() {
|
||||
val imageAnalysis = ImageAnalysis.Builder().build()
|
||||
if (intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)) {
|
||||
previewTransformLayoutListener?.let { viewFinder.removeOnLayoutChangeListener(it) }
|
||||
val layoutListener = View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
|
||||
viewFinder.post {
|
||||
scheduleCachedPreviewOutputTransformRefresh()
|
||||
onPairingLayoutGeometryChanged()
|
||||
}
|
||||
}
|
||||
previewTransformLayoutListener = layoutListener
|
||||
viewFinder.addOnLayoutChangeListener(layoutListener)
|
||||
previewStreamStateObserver?.let { viewFinder.previewStreamState.removeObserver(it) }
|
||||
val streamObserver = Observer<PreviewView.StreamState> { state ->
|
||||
if (state == PreviewView.StreamState.STREAMING) {
|
||||
viewFinder.post {
|
||||
scheduleCachedPreviewOutputTransformRefresh()
|
||||
onPairingLayoutGeometryChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
previewStreamStateObserver = streamObserver
|
||||
viewFinder.previewStreamState.observe(this, streamObserver)
|
||||
scheduleCachedPreviewOutputTransformRefresh()
|
||||
}
|
||||
|
||||
val camera = cameraProvider.bindToLifecycle(this, CameraSelector.DEFAULT_BACK_CAMERA, imageAnalysis)
|
||||
try {
|
||||
barcodeScanner?.close()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
|
||||
val barcodeScanner = BarcodeScanning.getClient(
|
||||
barcodeScanner = BarcodeScanning.getClient(
|
||||
Builder()
|
||||
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
|
||||
.setZoomSuggestionOptions(
|
||||
ZoomSuggestionOptions.Builder { zoomLevel ->
|
||||
camera.cameraControl.setZoomRatio(zoomLevel)
|
||||
true
|
||||
}.apply {
|
||||
camera.cameraInfo.zoomState.value?.maxZoomRatio?.let { maxZoomRation ->
|
||||
setMaxSupportedZoomRatio(maxZoomRation)
|
||||
}
|
||||
}.build()
|
||||
).build()
|
||||
.build()
|
||||
)
|
||||
|
||||
// optimization
|
||||
val checkedBarcodes = hashSetOf<String>()
|
||||
val analysisExecutor = imageAnalysisExecutor!!
|
||||
val mainExecutor = ContextCompat.getMainExecutor(this)
|
||||
val pairingQrMode = intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)
|
||||
|
||||
imageAnalysis.setAnalyzer(ContextCompat.getMainExecutor(this)) { imageProxy ->
|
||||
imageProxy.image?.let { InputImage.fromMediaImage(it, imageProxy.imageInfo.rotationDegrees) }
|
||||
?.let { image ->
|
||||
barcodeScanner.process(image).addOnSuccessListener { barcodes ->
|
||||
barcodes.firstOrNull()?.let { barcode ->
|
||||
barcode.displayValue?.let { code ->
|
||||
if (code.isNotEmpty() && code !in checkedBarcodes) {
|
||||
if (QtAndroidController.decodeQrCode(code)) {
|
||||
barcodeScanner.close()
|
||||
imageAnalysis.setAnalyzer(analysisExecutor) { imageProxy ->
|
||||
if (qrHandledOrClosing.get()) {
|
||||
imageProxy.close()
|
||||
return@setAnalyzer
|
||||
}
|
||||
val mediaImage = imageProxy.image
|
||||
if (mediaImage == null) {
|
||||
imageProxy.close()
|
||||
return@setAnalyzer
|
||||
}
|
||||
val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
|
||||
val viewW = viewFinder.width
|
||||
val viewH = viewFinder.height
|
||||
val pairingRoi = if (pairingQrMode) {
|
||||
pairingHoleRectInImageSpace(viewFinder, imageProxy, image.width, image.height)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val scanner = barcodeScanner ?: run {
|
||||
imageProxy.close()
|
||||
return@setAnalyzer
|
||||
}
|
||||
scanner.process(image)
|
||||
.addOnSuccessListener(mainExecutor) { barcodes ->
|
||||
if (qrHandledOrClosing.get()) {
|
||||
return@addOnSuccessListener
|
||||
}
|
||||
val barcode = if (pairingQrMode) {
|
||||
val roi = pairingRoi
|
||||
?: PairingQrScanGeometry.pairingIosStyleHoleInImageCoords(
|
||||
viewW,
|
||||
viewH,
|
||||
pairingGeomHeaderBottomPx,
|
||||
pairingGeomStatusBarTopPx,
|
||||
pairingGeomDensity,
|
||||
image.width,
|
||||
image.height
|
||||
)
|
||||
barcodes.firstOrNull {
|
||||
PairingQrScanGeometry.barcodeMatchesPairingHole(
|
||||
roi,
|
||||
image.width,
|
||||
image.height,
|
||||
it
|
||||
)
|
||||
}
|
||||
} else {
|
||||
barcodes.firstOrNull()
|
||||
}
|
||||
barcode?.displayValue?.let { code ->
|
||||
if (code.isNotEmpty() && code !in checkedBarcodes) {
|
||||
checkedBarcodes.add(code)
|
||||
if (QtAndroidController.decodeQrCode(code)) {
|
||||
if (qrHandledOrClosing.compareAndSet(false, true)) {
|
||||
if (pairingQrMode) {
|
||||
pairingQrDeliveredToQt = true
|
||||
}
|
||||
stopCamera()
|
||||
}
|
||||
checkedBarcodes.add(code)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.addOnFailureListener {
|
||||
Log.e(TAG, "Processing QR code image failed: ${it.message}")
|
||||
}.addOnCompleteListener {
|
||||
imageProxy.close()
|
||||
}
|
||||
}
|
||||
.addOnFailureListener(mainExecutor) {
|
||||
Log.e(TAG, "Processing QR code image failed: ${it.message}")
|
||||
}
|
||||
.addOnCompleteListener(mainExecutor) {
|
||||
imageProxy.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopCamera() {
|
||||
cameraProvider.unbindAll()
|
||||
cleanupCameraResources()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package org.amnezia.vpn
|
||||
|
||||
import android.graphics.Path
|
||||
import android.graphics.RectF
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
object PairingQrScanBracketPaths {
|
||||
|
||||
private fun Path.addCornerMinorArc(
|
||||
cx: Float,
|
||||
cy: Float,
|
||||
r: Float,
|
||||
sx: Float,
|
||||
sy: Float,
|
||||
ex: Float,
|
||||
ey: Float
|
||||
) {
|
||||
var asRad = atan2((sy - cy).toDouble(), (sx - cx).toDouble())
|
||||
var aeRad = atan2((ey - cy).toDouble(), (ex - cx).toDouble())
|
||||
while (aeRad - asRad > PI) {
|
||||
aeRad -= 2.0 * PI
|
||||
}
|
||||
while (aeRad - asRad < -PI) {
|
||||
aeRad += 2.0 * PI
|
||||
}
|
||||
val minor = aeRad - asRad
|
||||
val startDeg = Math.toDegrees(asRad).toFloat()
|
||||
val sweepDeg = Math.toDegrees(minor).toFloat()
|
||||
addArc(RectF(cx - r, cy - r, cx + r, cy + r), startDeg, sweepDeg)
|
||||
}
|
||||
|
||||
fun bracketStrokePath(corner: Int, x0: Float, y0: Float, s: Float, R: Float, L: Float, t: Float): Path {
|
||||
val r = max(1.5f, R - t * 0.5f)
|
||||
val p = Path()
|
||||
val yy = y0 + t * 0.5f
|
||||
val yyb = y0 + s - t * 0.5f
|
||||
val xx = x0 + t * 0.5f
|
||||
val xxb = x0 + s - t * 0.5f
|
||||
|
||||
when (corner) {
|
||||
0 -> {
|
||||
val cTLx = x0 + R
|
||||
val cTLy = y0 + R
|
||||
val sTLx = x0 + R
|
||||
val sTLy = yy
|
||||
val eTLx = xx
|
||||
val eTLy = y0 + R
|
||||
p.moveTo(x0 + R + L, yy)
|
||||
p.lineTo(sTLx, sTLy)
|
||||
p.addCornerMinorArc(cTLx, cTLy, r, sTLx, sTLy, eTLx, eTLy)
|
||||
val yEndTL = min(y0 + R + L, y0 + s - R - t * 0.5f)
|
||||
p.lineTo(xx, max(yEndTL, y0 + R + 2f))
|
||||
}
|
||||
1 -> {
|
||||
val cTRx = x0 + s - R
|
||||
val cTRy = y0 + R
|
||||
val sTRx = x0 + s - R
|
||||
val sTRy = yy
|
||||
val eTRx = xxb
|
||||
val eTRy = y0 + R
|
||||
p.moveTo(x0 + s - R - L, yy)
|
||||
p.lineTo(sTRx, sTRy)
|
||||
p.addCornerMinorArc(cTRx, cTRy, r, sTRx, sTRy, eTRx, eTRy)
|
||||
val yEndTR = min(y0 + R + L, y0 + s - R - t * 0.5f)
|
||||
p.lineTo(xxb, max(yEndTR, y0 + R + 2f))
|
||||
}
|
||||
2 -> {
|
||||
val cBLx = x0 + R
|
||||
val cBLy = y0 + s - R
|
||||
val sBLx = x0 + R
|
||||
val sBLy = yyb
|
||||
val eBLx = xx
|
||||
val eBLy = y0 + s - R
|
||||
p.moveTo(x0 + R + L, yyb)
|
||||
p.lineTo(sBLx, sBLy)
|
||||
p.addCornerMinorArc(cBLx, cBLy, r, sBLx, sBLy, eBLx, eBLy)
|
||||
val yEndTopRef = max(min(y0 + R + L, y0 + s - R - t * 0.5f), y0 + R + 2f)
|
||||
val yLegBL = y0 + s + y0 - yEndTopRef
|
||||
p.lineTo(xx, yLegBL)
|
||||
}
|
||||
3 -> {
|
||||
val cBRx = x0 + s - R
|
||||
val cBRy = y0 + s - R
|
||||
val sBRx = x0 + s - R
|
||||
val sBRy = yyb
|
||||
val eBRx = xxb
|
||||
val eBRy = y0 + s - R
|
||||
p.moveTo(x0 + s - R - L, yyb)
|
||||
p.lineTo(sBRx, sBRy)
|
||||
p.addCornerMinorArc(cBRx, cBRy, r, sBRx, sBRy, eBRx, eBRy)
|
||||
val yEndTopRef = max(min(y0 + R + L, y0 + s - R - t * 0.5f), y0 + R + 2f)
|
||||
val yLegBR = y0 + s + y0 - yEndTopRef
|
||||
p.lineTo(xxb, yLegBR)
|
||||
}
|
||||
}
|
||||
return p
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package org.amnezia.vpn
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import com.google.mlkit.vision.barcode.common.Barcode
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
object PairingQrScanGeometry {
|
||||
fun viewRectToInputImageRectFillCenter(
|
||||
viewW: Int,
|
||||
viewH: Int,
|
||||
imageW: Int,
|
||||
imageH: Int,
|
||||
viewRect: RectF
|
||||
): RectF {
|
||||
val scale = max(viewW / imageW.toFloat(), viewH / imageH.toFloat())
|
||||
val drawLeft = (viewW - imageW * scale) / 2f
|
||||
val drawTop = (viewH - imageH * scale) / 2f
|
||||
return RectF(
|
||||
(viewRect.left - drawLeft) / scale,
|
||||
(viewRect.top - drawTop) / scale,
|
||||
(viewRect.right - drawLeft) / scale,
|
||||
(viewRect.bottom - drawTop) / scale
|
||||
)
|
||||
}
|
||||
|
||||
fun pairingIosStyleHoleCornerRadiusPx(sidePx: Float, density: Float): Float {
|
||||
val d = density
|
||||
var holeR = min(28f * d, max(10f * d, sidePx * 0.056f))
|
||||
val half = 0.5f * sidePx
|
||||
holeR = min(holeR, max(6f * d, half - 2f * d))
|
||||
return max(holeR, 1f)
|
||||
}
|
||||
|
||||
fun barcodeBoxOverlapFraction(roi: RectF, box: Rect): Float {
|
||||
val bf = RectF(box)
|
||||
val inter = RectF(roi)
|
||||
if (!inter.intersect(bf)) return 0f
|
||||
val interArea = inter.width() * inter.height()
|
||||
val boxArea = bf.width() * bf.height()
|
||||
return if (boxArea <= 0f) 0f else interArea / boxArea
|
||||
}
|
||||
|
||||
fun barcodeMatchesPairingHole(
|
||||
roiInImageSpace: RectF,
|
||||
imageW: Int,
|
||||
imageH: Int,
|
||||
barcode: Barcode,
|
||||
minOverlapFraction: Float = PAIRING_SEND_MIN_OVERLAP_BBOX_FALLBACK
|
||||
): Boolean {
|
||||
if (imageW <= 0 || imageH <= 0) {
|
||||
return false
|
||||
}
|
||||
val roi = RectF(roiInImageSpace)
|
||||
val iw = imageW.toFloat()
|
||||
val ih = imageH.toFloat()
|
||||
roi.left = max(0f, roi.left)
|
||||
roi.top = max(0f, roi.top)
|
||||
roi.right = min(iw, roi.right)
|
||||
roi.bottom = min(ih, roi.bottom)
|
||||
if (roi.width() <= 0f || roi.height() <= 0f) {
|
||||
return false
|
||||
}
|
||||
|
||||
val corners = barcode.cornerPoints
|
||||
if (corners != null && corners.size >= 4) {
|
||||
for (p in corners) {
|
||||
if (!roi.contains(p.x.toFloat(), p.y.toFloat())) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
val box = barcode.boundingBox ?: return false
|
||||
val cx = box.centerX().toFloat()
|
||||
val cy = box.centerY().toFloat()
|
||||
if (!roi.contains(cx, cy)) {
|
||||
return false
|
||||
}
|
||||
return barcodeBoxOverlapFraction(roi, box) >= minOverlapFraction
|
||||
}
|
||||
|
||||
private const val PAIRING_SEND_MIN_OVERLAP_BBOX_FALLBACK = 0.72f
|
||||
|
||||
fun pairingIosStyleHoleRectF(
|
||||
viewW: Int,
|
||||
viewH: Int,
|
||||
headerBottomPx: Float,
|
||||
statusBarTopPx: Float,
|
||||
density: Float
|
||||
): RectF {
|
||||
val w = viewW.toFloat()
|
||||
val h = viewH.toFloat()
|
||||
val d = density
|
||||
if (w < 32f || h < 32f) {
|
||||
return RectF()
|
||||
}
|
||||
var hdrBottom = headerBottomPx
|
||||
if (hdrBottom < 8f * d) {
|
||||
hdrBottom = 132f * d + statusBarTopPx
|
||||
}
|
||||
val sqSz = floor(min(w, h) * 0.72).toFloat()
|
||||
var sqX = (w - sqSz) / 2f
|
||||
var sqY = (h - sqSz) / 2f
|
||||
sqY = max(sqY, hdrBottom + 8f * d)
|
||||
val kBottomBand = 80f * d
|
||||
val maxHoleBottom = h - kBottomBand
|
||||
if (sqY + sqSz > maxHoleBottom) {
|
||||
sqY = maxHoleBottom - sqSz
|
||||
sqY = max(sqY, hdrBottom + 8f * d)
|
||||
}
|
||||
sqX = max(8f * d, min(sqX, w - sqSz - 8f * d))
|
||||
sqY = max(hdrBottom + 4f * d, min(sqY, h - sqSz - 8f * d))
|
||||
return RectF(sqX, sqY, sqX + sqSz, sqY + sqSz)
|
||||
}
|
||||
|
||||
fun pairingIosStyleTorchCenterYPx(
|
||||
holeBottomPx: Float,
|
||||
bandBottomPx: Float,
|
||||
headerBottomPx: Float,
|
||||
safeBottomPx: Float,
|
||||
density: Float
|
||||
): Float {
|
||||
val torchH = 56f * density
|
||||
val d = density
|
||||
var torchCy = (holeBottomPx + bandBottomPx) * 0.5f
|
||||
val minC = holeBottomPx + torchH * 0.5f + 6f * d
|
||||
val maxC = bandBottomPx - torchH * 0.5f - max(6f * d, safeBottomPx)
|
||||
torchCy = max(minC, min(maxC, torchCy))
|
||||
if (minC > maxC) {
|
||||
torchCy = (minC + maxC) * 0.5f
|
||||
}
|
||||
val hdr = headerBottomPx + torchH * 0.5f + 10f * d
|
||||
return max(torchCy, hdr)
|
||||
}
|
||||
|
||||
fun pairingIosStyleHoleInImageCoords(
|
||||
viewW: Int,
|
||||
viewH: Int,
|
||||
headerBottomPx: Float,
|
||||
statusBarTopPx: Float,
|
||||
density: Float,
|
||||
imageW: Int,
|
||||
imageH: Int
|
||||
): RectF {
|
||||
val hv = pairingIosStyleHoleRectF(viewW, viewH, headerBottomPx, statusBarTopPx, density)
|
||||
return viewRectToInputImageRectFillCenter(viewW, viewH, imageW, imageH, hv)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package org.amnezia.vpn
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Path
|
||||
import android.graphics.RectF
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import kotlin.math.max
|
||||
|
||||
class PairingQrScanOverlayView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : View(context, attrs, defStyleAttr) {
|
||||
|
||||
init {
|
||||
isClickable = false
|
||||
isFocusable = false
|
||||
}
|
||||
|
||||
@Suppress("ClickableViewAccessibility")
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean = false
|
||||
|
||||
private val dimPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = 0x8C000000.toInt()
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
|
||||
private val bracketPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = 0xFFE8E8EC.toInt()
|
||||
style = Paint.Style.STROKE
|
||||
strokeCap = Paint.Cap.ROUND
|
||||
strokeJoin = Paint.Join.ROUND
|
||||
}
|
||||
|
||||
private var hole = RectF()
|
||||
|
||||
private val bracketPaths = arrayOfNulls<Path>(4)
|
||||
|
||||
private val dimPath = Path()
|
||||
|
||||
private var pairingHeaderBottomPx = 0f
|
||||
|
||||
fun setPairingHeaderBottomPx(px: Float) {
|
||||
if (pairingHeaderBottomPx == px) {
|
||||
return
|
||||
}
|
||||
pairingHeaderBottomPx = px
|
||||
recomputePairingHole()
|
||||
invalidate()
|
||||
}
|
||||
|
||||
private fun recomputePairingHole() {
|
||||
val w = width
|
||||
val h = height
|
||||
if (w <= 0 || h <= 0) {
|
||||
return
|
||||
}
|
||||
val topInset = ViewCompat.getRootWindowInsets(this)
|
||||
?.getInsets(WindowInsetsCompat.Type.statusBars())?.top?.toFloat() ?: 0f
|
||||
val d = resources.displayMetrics.density
|
||||
hole = PairingQrScanGeometry.pairingIosStyleHoleRectF(w, h, pairingHeaderBottomPx, topInset, d)
|
||||
rebuildBracketPaths()
|
||||
}
|
||||
|
||||
private fun rebuildBracketPaths() {
|
||||
val s = hole.width()
|
||||
if (s <= 0f) {
|
||||
bracketPaths.fill(null)
|
||||
return
|
||||
}
|
||||
val x0 = hole.left
|
||||
val y0 = hole.top
|
||||
val t = bracketPaint.strokeWidth
|
||||
val d = resources.displayMetrics.density
|
||||
val l = max(28f * d, s * 0.13f)
|
||||
val r = PairingQrScanGeometry.pairingIosStyleHoleCornerRadiusPx(s, d)
|
||||
for (i in 0..3) {
|
||||
bracketPaths[i] = PairingQrScanBracketPaths.bracketStrokePath(i, x0, y0, s, r, l, t)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
super.onSizeChanged(w, h, oldw, oldh)
|
||||
bracketPaint.strokeWidth = max(3f, 5f * resources.displayMetrics.density)
|
||||
recomputePairingHole()
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
val w = width.toFloat()
|
||||
val h = height.toFloat()
|
||||
val side = hole.width()
|
||||
if (side > 0f) {
|
||||
val d = resources.displayMetrics.density
|
||||
val rx = PairingQrScanGeometry.pairingIosStyleHoleCornerRadiusPx(side, d)
|
||||
dimPath.rewind()
|
||||
dimPath.fillType = Path.FillType.EVEN_ODD
|
||||
dimPath.addRect(0f, 0f, w, h, Path.Direction.CW)
|
||||
dimPath.addRoundRect(hole, rx, rx, Path.Direction.CW)
|
||||
canvas.drawPath(dimPath, dimPaint)
|
||||
} else {
|
||||
canvas.drawRect(0f, 0f, w, h, dimPaint)
|
||||
}
|
||||
|
||||
for (i in 0..3) {
|
||||
bracketPaths[i]?.let { canvas.drawPath(it, bracketPaint) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,6 @@ 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)
|
||||
|
||||
@@ -35,4 +34,10 @@ object QtAndroidController {
|
||||
|
||||
external fun onActivityPaused()
|
||||
external fun onActivityResumed()
|
||||
|
||||
external fun onCameraPermissionResult(granted: Boolean)
|
||||
|
||||
external fun onPairingQrCameraClosed()
|
||||
|
||||
external fun onPairingQrCameraUserDismissed()
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.amnezia.vpn.util
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Process
|
||||
@@ -23,18 +22,9 @@ import org.amnezia.vpn.util.Log.Priority.W
|
||||
import android.util.Log as NativeLog
|
||||
|
||||
private const val TAG = "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 LOG_FILE_NAME = "amneziaVPN.log"
|
||||
private const val 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
|
||||
@@ -52,16 +42,12 @@ object Log {
|
||||
private val dateTimeFormat: DateTimeFormatter = DateTimeFormatter.ofPattern(DATE_TIME_PATTERN)
|
||||
|
||||
private lateinit var logDir: File
|
||||
private lateinit var logFile: File
|
||||
private lateinit var rotateLogFile: 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 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
|
||||
@@ -74,9 +60,6 @@ object Log {
|
||||
}
|
||||
_saveLogs = value
|
||||
Prefs.save(PREFS_SAVE_LOGS_KEY, value)
|
||||
if (isAwgProcess) {
|
||||
if (value) startAwgLogcatReader() else stopAwgLogcatReader()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,60 +96,21 @@ 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)
|
||||
}
|
||||
|
||||
@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 getLogs(): String =
|
||||
"${deviceInfo()}\n${readLogs()}\nLOGCAT:\n${getLogcat()}"
|
||||
|
||||
@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) {
|
||||
fun clearLogs() {
|
||||
if (logDir.exists()) {
|
||||
withLock {
|
||||
streamFiles(stream).forEach { it.delete() }
|
||||
if (stream == STREAM_APP) legacyFiles().forEach { it.delete() }
|
||||
logFile.delete()
|
||||
rotateLogFile.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))
|
||||
|
||||
@@ -192,85 +136,51 @@ 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,23 +49,15 @@ set(SOURCES ${SOURCES}
|
||||
|
||||
|
||||
find_package(awg-android REQUIRED)
|
||||
set_property(TARGET ${PROJECT} APPEND PROPERTY QT_ANDROID_EXTRA_LIBS
|
||||
$<TARGET_FILE:amnezia::awg-android>
|
||||
${AMNEZIA_ANDROID_LIBWG_PATH}
|
||||
${AMNEZIA_ANDROID_LIBWG_QUICK_PATH}
|
||||
)
|
||||
set(LIBS ${LIBS} amnezia::awg-android)
|
||||
set_property(TARGET ${PROJECT} APPEND PROPERTY QT_ANDROID_EXTRA_LIBS ${AMNEZIA_ANDROID_LIBWG_PATH} ${AMNEZIA_ANDROID_LIBWG_QUICK_PATH})
|
||||
|
||||
find_package(amnezia-libxray REQUIRED)
|
||||
file(COPY ${AMNEZIA_LIBXRAY_PATH} DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/android/xray/libXray)
|
||||
|
||||
find_package(openvpn-pt-android REQUIRED)
|
||||
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(LIBS ${LIBS} amnezia::openvpn-pt-android)
|
||||
set_property(TARGET ${PROJECT} APPEND PROPERTY QT_ANDROID_EXTRA_LIBS ${OPENVPN_PT_ANDROID_LIBCK_OVPN_PLUGIN_PATH})
|
||||
|
||||
set(APP_ANDROID_PACKAGE_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/android)
|
||||
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
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()
|
||||
@@ -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_APP_ICON_PATH)
|
||||
set(CLIENT_IOS_APP_ICON_PATH "${_CLIENT_SRC_DIR}/images/AppIcon.icon" CACHE PATH "iOS app icon (Icon Composer .icon)")
|
||||
if(NOT CLIENT_IOS_MEDIA_ASSETS_PATH)
|
||||
set(CLIENT_IOS_MEDIA_ASSETS_PATH "${_CLIENT_SRC_DIR}/ios/app/Media.xcassets" CACHE FILEPATH "iOS app Media.xcassets path")
|
||||
endif()
|
||||
|
||||
if(NOT CLIENT_IOS_BUNDLE_ID)
|
||||
@@ -32,14 +32,14 @@ endif()
|
||||
|
||||
# -- signing
|
||||
if(NOT CLIENT_IOS_PROVISIONING_PROFILE_SPECIFIER)
|
||||
set(CLIENT_IOS_PROVISIONING_PROFILE_SPECIFIER "distr ios.org.amnezia.amneziaVPN" CACHE STRING "iOS app provisioning profile specifier for release")
|
||||
set(CLIENT_IOS_PROVISIONING_PROFILE_SPECIFIER "distr ios.org.amnezia.AmneziaVPN" CACHE STRING "iOS app provisioning profile specifier for release")
|
||||
endif()
|
||||
if(NOT CLIENT_IOS_PROVISIONING_PROFILE_SPECIFIER_DEBUG)
|
||||
set(CLIENT_IOS_PROVISIONING_PROFILE_SPECIFIER_DEBUG "dev ios.org.amnezia.amneziaVPN" CACHE STRING "iOS app provisioning profile specifier for debug")
|
||||
set(CLIENT_IOS_PROVISIONING_PROFILE_SPECIFIER_DEBUG "dev ios.org.amnezia.AmneziaVPN" CACHE STRING "iOS app provisioning profile specifier for debug")
|
||||
endif()
|
||||
if(NOT CLIENT_IOS_NE_PROVISIONING_PROFILE_SPECIFIER)
|
||||
set(CLIENT_IOS_NE_PROVISIONING_PROFILE_SPECIFIER "distr ios.org.amnezia.amneziaVPN.NE" CACHE STRING "iOS network extension provisioning profile specifier for release")
|
||||
set(CLIENT_IOS_NE_PROVISIONING_PROFILE_SPECIFIER "distr ios.org.amnezia.AmneziaVPN" CACHE STRING "iOS network extension provisioning profile specifier for release")
|
||||
endif()
|
||||
if(NOT CLIENT_IOS_NE_PROVISIONING_PROFILE_SPECIFIER_DEBUG)
|
||||
set(CLIENT_IOS_NE_PROVISIONING_PROFILE_SPECIFIER_DEBUG "dev ios.org.amnezia.amneziaVPN.NE" CACHE STRING "iOS network extension provisioning profile specifier for debug")
|
||||
set(CLIENT_IOS_NE_PROVISIONING_PROFILE_SPECIFIER_DEBUG "dev ios.org.amnezia.AmneziaVPN" CACHE STRING "iOS network extension provisioning profile specifier for debug")
|
||||
endif()
|
||||
@@ -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_APP_ICON_PATH)
|
||||
set(CLIENT_MACOS_APP_ICON_PATH "${_CLIENT_SRC_DIR}/images/AppIcon.icon" CACHE PATH "macOS app icon (Icon Composer .icon)")
|
||||
if(NOT CLIENT_MACOS_MEDIA_ASSETS_PATH)
|
||||
set(CLIENT_MACOS_MEDIA_ASSETS_PATH "${_CLIENT_SRC_DIR}/macos/app/Images.xcassets" CACHE FILEPATH "macOS app Images.xcassets path")
|
||||
endif()
|
||||
if(NOT CLIENT_MACOS_APP_ICNS_PATH)
|
||||
set(CLIENT_MACOS_APP_ICNS_PATH "${_CLIENT_SRC_DIR}/macos/app/AppIcon.icns" CACHE FILEPATH "macOS app icon (.icns) for builds without Xcode 26 and macOS 26")
|
||||
set(CLIENT_MACOS_APP_ICNS_PATH "${_CLIENT_SRC_DIR}/images/app.icns" CACHE FILEPATH "macOS app icon (.icns) used by non-App Store builds")
|
||||
endif()
|
||||
|
||||
if(NOT CLIENT_MACOS_EXTRA_PLIST_ENTRIES)
|
||||
@@ -24,7 +24,7 @@ endif()
|
||||
|
||||
# -- signing
|
||||
if(NOT CLIENT_MACOS_PROVISIONING_PROFILE_SPECIFIER)
|
||||
set(CLIENT_MACOS_PROVISIONING_PROFILE_SPECIFIER "distr macos.org.amnezia.amneziaVPN" CACHE STRING "macOS app provisioning profile specifier for release")
|
||||
set(CLIENT_MACOS_PROVISIONING_PROFILE_SPECIFIER "distr macos.org.amnezia.AmneziaVPN" CACHE STRING "macOS app provisioning profile specifier for release")
|
||||
endif()
|
||||
if(NOT CLIENT_MACOS_PROVISIONING_PROFILE_SPECIFIER_DEBUG)
|
||||
set(CLIENT_MACOS_PROVISIONING_PROFILE_SPECIFIER_DEBUG "dev macos.org.amnezia.AmneziaVPN" CACHE STRING "macOS app provisioning profile specifier for debug")
|
||||
|
||||
@@ -28,6 +28,7 @@ set(LIBS ${LIBS}
|
||||
|
||||
|
||||
set(HEADERS ${HEADERS}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosPairingQrOverlayWindow.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.h
|
||||
@@ -45,6 +46,8 @@ set(SOURCES ${SOURCES}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ioscontextmenu.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosPairingCameraAccess.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosPairingQrOverlayWindow.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/AmneziaSceneDelegateHooks.mm
|
||||
)
|
||||
@@ -127,18 +130,14 @@ 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_APP_ICON_PATH}
|
||||
PROPERTIES
|
||||
MACOSX_PACKAGE_LOCATION Resources
|
||||
XCODE_LAST_KNOWN_FILE_TYPE folder.iconcomposer.icon
|
||||
${CLIENT_IOS_MEDIA_ASSETS_PATH}
|
||||
PROPERTIES MACOSX_PACKAGE_LOCATION Resources
|
||||
)
|
||||
|
||||
target_sources(${PROJECT} PRIVATE
|
||||
${CLIENT_IOS_LAUNCHSCREEN_PATH}
|
||||
${CLIENT_IOS_APP_ICON_PATH}
|
||||
${CLIENT_IOS_MEDIA_ASSETS_PATH}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy
|
||||
)
|
||||
|
||||
|
||||
@@ -34,42 +34,10 @@ set(SOURCES ${SOURCES}
|
||||
|
||||
|
||||
|
||||
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})
|
||||
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})
|
||||
|
||||
target_compile_options(${PROJECT} PRIVATE
|
||||
-DGROUP_ID=\"${BUILD_IOS_GROUP_IDENTIFIER}\"
|
||||
|
||||
@@ -46,6 +46,7 @@ set(SOURCES ${SOURCES}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosPairingCameraAccess_stub.cpp
|
||||
)
|
||||
|
||||
target_include_directories(${PROJECT} PRIVATE
|
||||
@@ -122,24 +123,13 @@ target_sources(${PROJECT} PRIVATE
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/StoreKit2Helper.swift
|
||||
)
|
||||
|
||||
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()
|
||||
set_source_files_properties(
|
||||
${CLIENT_MACOS_MEDIA_ASSETS_PATH}
|
||||
PROPERTIES MACOSX_PACKAGE_LOCATION Resources
|
||||
)
|
||||
|
||||
target_sources(${PROJECT} PRIVATE
|
||||
${APP_ICON_SOURCE}
|
||||
${CLIENT_MACOS_MEDIA_ASSETS_PATH}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy
|
||||
)
|
||||
|
||||
|
||||
@@ -40,6 +40,64 @@ 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,11 +44,10 @@ 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
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/pairingController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/newsController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/updateController.h
|
||||
${CLIENT_ROOT_DIR}/core/repositories/secureServersRepository.h
|
||||
@@ -67,17 +66,16 @@ 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
|
||||
${CLIENT_ROOT_DIR}/core/utils/managementServer.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/constants.h
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/iosPairingCameraAccess.h
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/iosPairingQrOverlayWindow.h
|
||||
)
|
||||
|
||||
# Mozilla headres
|
||||
@@ -134,11 +132,10 @@ 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
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/pairingController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/newsController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/updateController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/repositories/secureServersRepository.cpp
|
||||
@@ -159,11 +156,9 @@ 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
|
||||
@@ -180,6 +175,7 @@ set(SOURCES ${SOURCES}
|
||||
if(NOT IOS AND NOT MACOS_NE)
|
||||
set(SOURCES ${SOURCES}
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/QRCodeReaderBase.cpp
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/iosPairingCameraAccess_stub.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
|
||||
@@ -66,8 +66,6 @@ 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,11 +266,6 @@ 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,7 +4,6 @@
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QDateTime>
|
||||
#include <QThread>
|
||||
#include <QUuid>
|
||||
#include "logger.h"
|
||||
@@ -24,49 +23,124 @@
|
||||
namespace {
|
||||
Logger logger("XrayConfigurator");
|
||||
|
||||
QString effectiveServerFlow(const amnezia::XrayServerConfig &srv)
|
||||
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)
|
||||
{
|
||||
return amnezia::xrayEffective::serverFlow(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();
|
||||
}
|
||||
|
||||
QString effectiveSecurity(const amnezia::XrayServerConfig &srv)
|
||||
{
|
||||
return amnezia::xrayEffective::security(srv);
|
||||
if (srv.transport == QLatin1String("mkcp") && srv.security == QLatin1String("reality")) {
|
||||
return QStringLiteral("none");
|
||||
}
|
||||
return srv.security;
|
||||
}
|
||||
|
||||
// Desktop applies this in XrayProtocol::start(); iOS/Android pass JSON straight to libxray — same fixes here.
|
||||
void sanitizeXrayNativeConfig(amnezia::ProtocolConfig &pc)
|
||||
{
|
||||
const QString before = pc.nativeConfig();
|
||||
const QString after = amnezia::sanitizeNativeConfig(before);
|
||||
if (after != before) {
|
||||
pc.setNativeConfig(after);
|
||||
QString c = pc.nativeConfig();
|
||||
if (c.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
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);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -83,6 +157,30 @@ 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
|
||||
{
|
||||
@@ -98,17 +196,12 @@ 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: key file empty path=" << path;
|
||||
return ErrorCode::XrayKeysReadFailed;
|
||||
logger.error() << "Xray readRealityKeyFiles: failed path=" << path;
|
||||
return ErrorCode::XrayRealityKeysReadFailed;
|
||||
};
|
||||
|
||||
ErrorCode errorCode = readKeyFile(QString::fromLatin1(amnezia::protocols::xray::PublicKeyPath), outPublicKey);
|
||||
@@ -139,7 +232,11 @@ ErrorCode XrayConfigurator::applyServerSettingsToRemote(const ServerCredentials
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
const QString flowValue = effectiveServerFlow(srv);
|
||||
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);
|
||||
QString realityPublicKey;
|
||||
QString realityShortId;
|
||||
if (effectiveSecurity(srv) == QLatin1String("reality")) {
|
||||
@@ -167,59 +264,71 @@ ErrorCode XrayConfigurator::applyServerSettingsToRemote(const ServerCredentials
|
||||
}
|
||||
|
||||
QJsonObject serverConfig = doc.object();
|
||||
const QJsonObject originalConfig = serverConfig;
|
||||
if (!serverConfig.contains(amnezia::protocols::xray::inbounds)) {
|
||||
logger.error() << "Server config missing 'inbounds' field";
|
||||
return ErrorCode::XrayServerConfigInvalid;
|
||||
}
|
||||
|
||||
QJsonArray clients = XrayServerConfig::clientsFromServerInboundJson(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();
|
||||
QString clientId;
|
||||
|
||||
if (appendNewClient) {
|
||||
const bool adoptingOwnAccount = !(xrayCfg->hasClientConfig() && !xrayCfg->clientConfig->id.isEmpty());
|
||||
|
||||
QString ownClientId;
|
||||
if (readContainerKeyFile(container, credentials, QString::fromLatin1(amnezia::protocols::xray::uuidPath),
|
||||
ownClientId)
|
||||
!= ErrorCode::NoError) {
|
||||
ownClientId.clear();
|
||||
}
|
||||
|
||||
const int existingIndex =
|
||||
adoptingOwnAccount ? XrayServerConfig::indexOfClient(clients, ownClientId) : -1;
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
clientId = ownClientId;
|
||||
clients[existingIndex] =
|
||||
XrayServerConfig::applyFlowToClient(clients[existingIndex].toObject(), flowValue);
|
||||
} else {
|
||||
const bool takeOverOwnAccount = adoptingOwnAccount && !ownClientId.isEmpty();
|
||||
clientId = takeOverOwnAccount ? ownClientId : QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
clients.append(XrayServerConfig::makeClientEntry(clientId, flowValue));
|
||||
clientId = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
QJsonObject clientEntry;
|
||||
clientEntry[amnezia::protocols::xray::id] = clientId;
|
||||
if (!flowValue.isEmpty()) {
|
||||
clientEntry[amnezia::protocols::xray::flow] = flowValue;
|
||||
}
|
||||
clients.append(clientEntry);
|
||||
} else {
|
||||
if (clients.isEmpty()) {
|
||||
logger.error() << "Server config has no VLESS clients";
|
||||
return ErrorCode::XrayServerNoVlessClients;
|
||||
}
|
||||
clientId = XrayServerConfig::firstClientId(clients);
|
||||
clientId = clients[0].toObject()[amnezia::protocols::xray::id].toString();
|
||||
if (clientId.isEmpty()) {
|
||||
logger.error() << "Server config VLESS client has empty id";
|
||||
return ErrorCode::XrayServerNoVlessClients;
|
||||
}
|
||||
clients = XrayServerConfig::applyFlowToClients(clients, flowValue);
|
||||
}
|
||||
|
||||
const XrayServerJsonStatus writeStatus =
|
||||
XrayServerConfig::setClientsInServerInboundJson(serverConfig, clients);
|
||||
if (writeStatus != XrayServerJsonStatus::Ok) {
|
||||
return ErrorCode::XrayServerConfigInvalid;
|
||||
}
|
||||
|
||||
const QString listenPort = srv.port.isEmpty() ? QString::fromLatin1(amnezia::protocols::xray::defaultPort)
|
||||
: srv.port;
|
||||
if (serverConfig != originalConfig) {
|
||||
errorCode = uploadServerConfigAtomically(credentials, container, listenPort, serverConfig);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
QJsonArray updatedClients;
|
||||
for (const QJsonValue &v : clients) {
|
||||
QJsonObject c = v.toObject();
|
||||
if (flowValue.isEmpty()) {
|
||||
c.remove(amnezia::protocols::xray::flow);
|
||||
} else {
|
||||
c[amnezia::protocols::xray::flow] = flowValue;
|
||||
}
|
||||
updatedClients.append(c);
|
||||
}
|
||||
clients = updatedClients;
|
||||
}
|
||||
|
||||
settings[amnezia::protocols::xray::clients] = clients;
|
||||
inbound[amnezia::protocols::xray::settings] = settings;
|
||||
inbounds[0] = inbound;
|
||||
serverConfig[amnezia::protocols::xray::inbounds] = inbounds;
|
||||
|
||||
errorCode = uploadServerConfigJson(credentials, container, dnsSettings, serverConfig);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Xray applyServerSettings: upload/restart failed, error=" << static_cast<int>(errorCode);
|
||||
return errorCode;
|
||||
}
|
||||
logger.info() << "Xray applyServerSettings: server config uploaded and container restarted";
|
||||
|
||||
@@ -228,8 +337,7 @@ 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);
|
||||
@@ -252,24 +360,19 @@ 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: key file empty path=" << path;
|
||||
return ErrorCode::XrayKeysReadFailed;
|
||||
logger.error() << "Xray readContainerKeyFile: failed path=" << path;
|
||||
return ErrorCode::XrayRealityKeysReadFailed;
|
||||
}
|
||||
|
||||
ErrorCode XrayConfigurator::writeServerConfigForSetup(const ServerCredentials &credentials, DockerContainer container,
|
||||
ContainerConfig &containerConfig, const DnsSettings &dnsSettings,
|
||||
bool useAtomicApply)
|
||||
ContainerConfig &containerConfig, const DnsSettings &dnsSettings)
|
||||
{
|
||||
Q_UNUSED(dnsSettings);
|
||||
namespace px = amnezia::protocols::xray;
|
||||
|
||||
const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>();
|
||||
if (!xrayCfg) {
|
||||
@@ -282,72 +385,82 @@ 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(amnezia::protocols::xray::uuidPath), clientId);
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(px::uuidPath), clientId);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
const QString securityEff = effectiveSecurity(srv);
|
||||
QString tlsPin;
|
||||
if (securityEff == QLatin1String("tls")) {
|
||||
errorCode = ensureTlsCertificate(credentials, container, tlsPin);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
|
||||
QString realityPrivateKey;
|
||||
QString realityPublicKey;
|
||||
QString realityShortId;
|
||||
if (securityEff == QLatin1String("reality")) {
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(amnezia::protocols::xray::PrivateKeyPath), realityPrivateKey);
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(px::PrivateKeyPath), realityPrivateKey);
|
||||
if (errorCode != ErrorCode::NoError)
|
||||
return errorCode;
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(amnezia::protocols::xray::PublicKeyPath), realityPublicKey);
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(px::PublicKeyPath), realityPublicKey);
|
||||
if (errorCode != ErrorCode::NoError)
|
||||
return errorCode;
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(amnezia::protocols::xray::shortidPath), realityShortId);
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(px::shortidPath), realityShortId);
|
||||
if (errorCode != ErrorCode::NoError)
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
const QString flowValue = effectiveServerFlow(srv);
|
||||
const QJsonArray clients = collectServerClients(credentials, container, flowValue, clientId, 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);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Xray writeServerConfigForSetup: upload failed, error=" << static_cast<int>(errorCode);
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
const QString portEff = srv.port.isEmpty() ? QString::fromLatin1(amnezia::protocols::xray::defaultPort) : srv.port;
|
||||
|
||||
|
||||
XrayServerInboundInputs inputs;
|
||||
inputs.clients = clients;
|
||||
inputs.realityPrivateKey = realityPrivateKey;
|
||||
inputs.realityShortId = realityShortId;
|
||||
|
||||
const QJsonObject inboundJson = srv.toServerInboundJson(inputs);
|
||||
if (useAtomicApply) {
|
||||
errorCode = uploadServerConfigAtomically(credentials, container, portEff, inboundJson);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
} else {
|
||||
const QString json = QString::fromUtf8(QJsonDocument(inboundJson).toJson());
|
||||
errorCode = m_sshSession->uploadTextFileToContainer(container, credentials, json,
|
||||
QString::fromLatin1(amnezia::protocols::xray::serverConfigPath),
|
||||
libssh::ScpOverwriteMode::ScpOverwriteExisting);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
|
||||
XrayProtocolConfig updated =
|
||||
buildClientProtocolConfig(credentials, container, srv, clientId, errorCode,
|
||||
realityPublicKey, realityShortId, tlsPin);
|
||||
buildClientProtocolConfig(credentials, container, srv, clientId, errorCode, realityPublicKey, realityShortId);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Xray writeServerConfigForSetup: buildClientProtocolConfig failed, error="
|
||||
<< static_cast<int>(errorCode);
|
||||
@@ -358,217 +471,6 @@ 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,
|
||||
@@ -587,21 +489,17 @@ 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 QString &prefetchedTlsPin) const
|
||||
const QString &prefetchedRealityShortId) const
|
||||
{
|
||||
QString xrayPublicKey = prefetchedRealityPublicKey;
|
||||
QString xrayShortId = prefetchedRealityShortId;
|
||||
QString tlsPin = prefetchedTlsPin;
|
||||
|
||||
const QString securityEff = effectiveSecurity(srv);
|
||||
|
||||
|
||||
if (securityEff == QLatin1String(amnezia::protocols::xray::securityReality)) {
|
||||
if (securityEff == QLatin1String("reality")) {
|
||||
if (xrayPublicKey.isEmpty() || xrayShortId.isEmpty()) {
|
||||
errorCode = readRealityKeyFiles(container, credentials, xrayPublicKey, xrayShortId);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
@@ -610,40 +508,243 @@ XrayProtocolConfig XrayConfigurator::buildClientProtocolConfig(const ServerCrede
|
||||
}
|
||||
}
|
||||
|
||||
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 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;
|
||||
}
|
||||
|
||||
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;
|
||||
const QJsonObject clientOutbound = protocolConfig.toClientOutboundJson(inputs);
|
||||
clientConfig.nativeConfig = QString::fromUtf8(QJsonDocument(clientOutbound).toJson(QJsonDocument::Compact));
|
||||
clientConfig.localPort = QString::fromLatin1(amnezia::protocols::xray::defaultLocalProxyPort);
|
||||
clientConfig.nativeConfig = config;
|
||||
clientConfig.localPort = QString(amnezia::protocols::xray::defaultLocalProxyPort);
|
||||
clientConfig.id = clientId;
|
||||
protocolConfig.setClientConfig(clientConfig);
|
||||
|
||||
return protocolConfig;
|
||||
}
|
||||
|
||||
QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, const QString &clientId) const
|
||||
{
|
||||
QJsonObject streamSettings;
|
||||
const auto &xhttp = srv.xhttp;
|
||||
const auto &mkcp = srv.mkcp;
|
||||
namespace px = amnezia::protocols::xray;
|
||||
|
||||
QString networkValue = QStringLiteral("tcp");
|
||||
if (srv.transport == QLatin1String("xhttp"))
|
||||
networkValue = QStringLiteral("xhttp");
|
||||
else if (srv.transport == QLatin1String("mkcp"))
|
||||
networkValue = QStringLiteral("kcp");
|
||||
streamSettings[px::network] = networkValue;
|
||||
|
||||
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,7 +2,6 @@
|
||||
#define XRAY_CONFIGURATOR_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "configuratorBase.h"
|
||||
@@ -34,24 +33,9 @@ public:
|
||||
amnezia::ErrorCode writeServerConfigForSetup(const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container,
|
||||
amnezia::ContainerConfig &containerConfig,
|
||||
const amnezia::DnsSettings &dnsSettings,
|
||||
bool useAtomicApply = false);
|
||||
const amnezia::DnsSettings &dnsSettings);
|
||||
|
||||
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;
|
||||
@@ -60,23 +44,24 @@ 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 QString &prefetchedTlsPin = {}) const;
|
||||
const QString &prefetchedRealityShortId = {}) const;
|
||||
|
||||
amnezia::ErrorCode readRealityKeyFiles(amnezia::DockerContainer container,
|
||||
const amnezia::ServerCredentials &credentials,
|
||||
QString &outPublicKey,
|
||||
QString &outShortId) const;
|
||||
|
||||
amnezia::ErrorCode ensureTlsCertificate(const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container,
|
||||
QString &outFingerprint) const;
|
||||
QJsonObject buildStreamSettings(const amnezia::XrayServerConfig &srv,
|
||||
const QString &clientId) const;
|
||||
};
|
||||
|
||||
#endif // XRAY_CONFIGURATOR_H
|
||||
@@ -76,13 +76,12 @@ QFuture<QPair<ErrorCode, QJsonArray>> NewsController::fetchNews()
|
||||
return QtFuture::makeReadyFuture(qMakePair(ErrorCode::NoError, QJsonArray()));
|
||||
}
|
||||
|
||||
QSharedPointer<GatewayController> gatewayController(
|
||||
new GatewayController(m_appSettingsRepository->getGatewayEndpoint(),
|
||||
m_appSettingsRepository->isDevGatewayEnv(),
|
||||
apiDefs::requestTimeoutMsecs,
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled(),
|
||||
m_appSettingsRepository),
|
||||
&QObject::deleteLater);
|
||||
auto gatewayController = QSharedPointer<GatewayController>::create(
|
||||
m_appSettingsRepository->getGatewayEndpoint(),
|
||||
m_appSettingsRepository->isDevGatewayEnv(),
|
||||
apiDefs::requestTimeoutMsecs,
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled(),
|
||||
m_appSettingsRepository);
|
||||
|
||||
// both country codes and service types are arrays here, one entry per gateway stack the user has
|
||||
const QJsonObject payload = GatewayPayloadBuilder(m_appSettingsRepository)
|
||||
@@ -91,7 +90,7 @@ QFuture<QPair<ErrorCode, QJsonArray>> NewsController::fetchNews()
|
||||
.addField(apiDefs::key::serviceType, services.value(apiDefs::key::serviceType))
|
||||
.build();
|
||||
|
||||
auto future = gatewayController->postAsync(QString("%1v1/news"), payload);
|
||||
auto future = gatewayController->postAsync(QString("%1v1/news"), payload, nullptr, gatewayController);
|
||||
return future.then([gatewayController](QPair<ErrorCode, QByteArray> result) -> QPair<ErrorCode, QJsonArray> {
|
||||
auto [errorCode, responseBody] = result;
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
#include "pairingController.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QSysInfo>
|
||||
#include "core/repositories/secureAppSettingsRepository.h"
|
||||
#include "core/utils/api/apiUtils.h"
|
||||
#include "core/utils/constants/apiConstants.h"
|
||||
#include "core/utils/constants/apiKeys.h"
|
||||
#include "version.h"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr qsizetype kPairingMaxQrUuidChars = 128;
|
||||
constexpr qsizetype kPairingMaxVpnConfigChars = 256 * 1024;
|
||||
constexpr qsizetype kPairingMaxApiKeyChars = 8192;
|
||||
constexpr qsizetype kPairingMaxServiceTypeChars = 64;
|
||||
constexpr qsizetype kPairingMaxUserCountryCodeChars = 32;
|
||||
|
||||
ErrorCode applyGatewayOrOpenApiGenerateError(const QJsonObject &obj, PairingController::QrPairingConfigPayload &outPayload)
|
||||
{
|
||||
ErrorCode apiStatus = apiUtils::errorCodeFromGatewayJsonHttpStatus(obj);
|
||||
if (apiStatus != ErrorCode::NoError) {
|
||||
return apiStatus;
|
||||
}
|
||||
|
||||
const QString config = obj.value(apiDefs::key::config).toString();
|
||||
if (!config.isEmpty()) {
|
||||
outPayload.config = config;
|
||||
outPayload.serviceInfo = obj.value(apiDefs::key::serviceInfo).toObject();
|
||||
outPayload.supportedProtocols = obj.value(apiDefs::key::supportedProtocols).toArray();
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
if (obj.contains(QStringLiteral("detail"))) {
|
||||
return ErrorCode::ApiConfigEmptyError;
|
||||
}
|
||||
|
||||
const QString msg = obj.value(QStringLiteral("message")).toString();
|
||||
if (msg.contains(QStringLiteral("timeout"), Qt::CaseInsensitive)) {
|
||||
return ErrorCode::ApiConfigTimeoutError;
|
||||
}
|
||||
if (msg.contains(QStringLiteral("Too Many"), Qt::CaseInsensitive)) {
|
||||
return ErrorCode::ApiPairingRateLimitedError;
|
||||
}
|
||||
if (msg.contains(QStringLiteral("Unavailable"), Qt::CaseInsensitive)) {
|
||||
return ErrorCode::ApiPairingServiceUnavailableError;
|
||||
}
|
||||
if (!msg.isEmpty()) {
|
||||
return ErrorCode::ApiConfigDownloadError;
|
||||
}
|
||||
|
||||
return ErrorCode::ApiConfigEmptyError;
|
||||
}
|
||||
|
||||
ErrorCode applyGatewayOrOpenApiScanError(const QJsonObject &obj)
|
||||
{
|
||||
const QString msgProbe = obj.value(QStringLiteral("message")).toString();
|
||||
if (msgProbe.contains(QStringLiteral("limit"), Qt::CaseInsensitive)
|
||||
&& (msgProbe.contains(QStringLiteral("device"), Qt::CaseInsensitive)
|
||||
|| msgProbe.contains(QStringLiteral("maximum"), Qt::CaseInsensitive)
|
||||
|| msgProbe.contains(QStringLiteral("max"), Qt::CaseInsensitive))) {
|
||||
return ErrorCode::ApiConfigLimitError;
|
||||
}
|
||||
|
||||
ErrorCode apiStatus = apiUtils::errorCodeFromGatewayJsonHttpStatus(obj);
|
||||
if (apiStatus != ErrorCode::NoError) {
|
||||
return apiStatus;
|
||||
}
|
||||
|
||||
if (obj.value(QStringLiteral("message")).toString() == QLatin1String("OK")) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
if (obj.contains(QStringLiteral("detail"))) {
|
||||
return ErrorCode::ApiPairingForbiddenError;
|
||||
}
|
||||
|
||||
const QString msg = obj.value(QStringLiteral("message")).toString();
|
||||
if (msg.contains(QStringLiteral("QR session"), Qt::CaseInsensitive)
|
||||
&& (msg.contains(QStringLiteral("not found"), Qt::CaseInsensitive)
|
||||
|| msg.contains(QStringLiteral("expired"), Qt::CaseInsensitive))) {
|
||||
return ErrorCode::ApiPairingSessionExpiredError;
|
||||
}
|
||||
if (msg.contains(QStringLiteral("not found"), Qt::CaseInsensitive) || msg.contains(QStringLiteral("expired"), Qt::CaseInsensitive)) {
|
||||
return ErrorCode::ApiNotFoundError;
|
||||
}
|
||||
if (msg.contains(QStringLiteral("Conflict"), Qt::CaseInsensitive) || msg.contains(QStringLiteral("already"), Qt::CaseInsensitive)) {
|
||||
return ErrorCode::ApiPairingConflictError;
|
||||
}
|
||||
if (msg.contains(QStringLiteral("Too Many"), Qt::CaseInsensitive)) {
|
||||
return ErrorCode::ApiPairingRateLimitedError;
|
||||
}
|
||||
if (msg.contains(QStringLiteral("Unavailable"), Qt::CaseInsensitive)) {
|
||||
return ErrorCode::ApiPairingServiceUnavailableError;
|
||||
}
|
||||
if (!msg.isEmpty()) {
|
||||
return ErrorCode::ApiConfigDownloadError;
|
||||
}
|
||||
|
||||
return ErrorCode::ApiConfigEmptyError;
|
||||
}
|
||||
|
||||
ErrorCode interpretGenerateQrJson(const QJsonObject &obj, PairingController::QrPairingConfigPayload &outPayload)
|
||||
{
|
||||
return applyGatewayOrOpenApiGenerateError(obj, outPayload);
|
||||
}
|
||||
|
||||
ErrorCode interpretScanQrJson(const QJsonObject &obj)
|
||||
{
|
||||
return applyGatewayOrOpenApiScanError(obj);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ErrorCode PairingController::parseGenerateQrResponseBody(const QByteArray &responseBody, QrPairingConfigPayload &outPayload)
|
||||
{
|
||||
outPayload = QrPairingConfigPayload {};
|
||||
const QJsonObject obj = QJsonDocument::fromJson(responseBody).object();
|
||||
return interpretGenerateQrJson(obj, outPayload);
|
||||
}
|
||||
|
||||
ErrorCode PairingController::parseScanQrResponseBody(const QByteArray &responseBody, QString *outOptionalDisplayName)
|
||||
{
|
||||
if (outOptionalDisplayName) {
|
||||
outOptionalDisplayName->clear();
|
||||
}
|
||||
const QJsonObject obj = QJsonDocument::fromJson(responseBody).object();
|
||||
const ErrorCode err = interpretScanQrJson(obj);
|
||||
if (err != ErrorCode::NoError) {
|
||||
return err;
|
||||
}
|
||||
if (outOptionalDisplayName) {
|
||||
const QString deviceName = obj.value(QStringLiteral("device_name")).toString().trimmed();
|
||||
if (!deviceName.isEmpty()) {
|
||||
*outOptionalDisplayName = deviceName;
|
||||
}
|
||||
}
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode PairingController::validatePairingScanFields(const QString &qrUuid, const QString &vpnConfig, const QString &apiKey,
|
||||
const QString &serviceType, const QString &userCountryCode)
|
||||
{
|
||||
if (qrUuid.size() > kPairingMaxQrUuidChars) {
|
||||
return ErrorCode::ApiConfigEmptyError;
|
||||
}
|
||||
if (vpnConfig.size() > kPairingMaxVpnConfigChars) {
|
||||
return ErrorCode::ApiPairingPayloadTooLargeError;
|
||||
}
|
||||
if (apiKey.size() > kPairingMaxApiKeyChars) {
|
||||
return ErrorCode::ApiPairingPayloadTooLargeError;
|
||||
}
|
||||
const QString st = serviceType.trimmed();
|
||||
const QString cc = userCountryCode.trimmed();
|
||||
if (st.isEmpty() || cc.isEmpty()) {
|
||||
return ErrorCode::ApiPairingMissingMetadataError;
|
||||
}
|
||||
if (st.size() > kPairingMaxServiceTypeChars || cc.size() > kPairingMaxUserCountryCodeChars) {
|
||||
return ErrorCode::ApiPairingPayloadTooLargeError;
|
||||
}
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
PairingController::PairingController(SecureAppSettingsRepository *appSettingsRepository)
|
||||
: m_appSettingsRepository(appSettingsRepository)
|
||||
{
|
||||
}
|
||||
|
||||
int PairingController::pairingLongPollTimeoutMsecs() const
|
||||
{
|
||||
return 60 * 1000;
|
||||
}
|
||||
|
||||
QJsonObject PairingController::buildGenerateQrPayload(const QString &qrUuid) const
|
||||
{
|
||||
QJsonObject o;
|
||||
o[apiDefs::key::qrUuid] = qrUuid;
|
||||
o[apiDefs::key::installationUuid] = m_appSettingsRepository->getInstallationUuid(true);
|
||||
o[apiDefs::key::appVersion] = QString(APP_VERSION);
|
||||
o[apiDefs::key::osVersion] = QSysInfo::productType();
|
||||
return o;
|
||||
}
|
||||
|
||||
QJsonObject PairingController::buildScanQrPayload(const QString &qrUuid, const QString &vpnConfig, const QJsonObject &serviceInfo,
|
||||
const QJsonArray &supportedProtocols, const QString &apiKey,
|
||||
const QString &serviceType, const QString &userCountryCode) const
|
||||
{
|
||||
QJsonObject auth;
|
||||
auth[apiDefs::key::apiKey] = apiKey;
|
||||
|
||||
QJsonObject o;
|
||||
o[apiDefs::key::qrUuid] = qrUuid;
|
||||
o[apiDefs::key::config] = vpnConfig;
|
||||
o[apiDefs::key::serviceInfo] = serviceInfo;
|
||||
o[apiDefs::key::supportedProtocols] = supportedProtocols;
|
||||
o[apiDefs::key::authData] = auth;
|
||||
o[apiDefs::key::installationUuid] = m_appSettingsRepository->getInstallationUuid(true);
|
||||
o[apiDefs::key::appVersion] = QString(APP_VERSION);
|
||||
o[apiDefs::key::cliName] = QString(APPLICATION_NAME);
|
||||
o[apiDefs::key::osVersion] = QSysInfo::productType();
|
||||
o[apiDefs::key::serviceType] = serviceType.trimmed();
|
||||
o[apiDefs::key::userCountryCode] = userCountryCode.trimmed();
|
||||
return o;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
#ifndef PAIRINGCONTROLLER_H
|
||||
#define PAIRINGCONTROLLER_H
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
|
||||
#include "core/utils/errorCodes.h"
|
||||
|
||||
class SecureAppSettingsRepository;
|
||||
|
||||
class PairingController
|
||||
{
|
||||
public:
|
||||
struct QrPairingConfigPayload
|
||||
{
|
||||
QString config;
|
||||
QJsonObject serviceInfo;
|
||||
QJsonArray supportedProtocols;
|
||||
};
|
||||
|
||||
explicit PairingController(SecureAppSettingsRepository *appSettingsRepository);
|
||||
|
||||
int pairingLongPollTimeoutMsecs() const;
|
||||
|
||||
QJsonObject buildGenerateQrPayload(const QString &qrUuid) const;
|
||||
QJsonObject buildScanQrPayload(const QString &qrUuid, const QString &vpnConfig, const QJsonObject &serviceInfo,
|
||||
const QJsonArray &supportedProtocols, const QString &apiKey, const QString &serviceType,
|
||||
const QString &userCountryCode) const;
|
||||
|
||||
static amnezia::ErrorCode parseGenerateQrResponseBody(const QByteArray &responseBody, QrPairingConfigPayload &outPayload);
|
||||
static amnezia::ErrorCode parseScanQrResponseBody(const QByteArray &responseBody, QString *outOptionalDisplayName = nullptr);
|
||||
|
||||
static amnezia::ErrorCode validatePairingScanFields(const QString &qrUuid, const QString &vpnConfig, const QString &apiKey,
|
||||
const QString &serviceType, const QString &userCountryCode);
|
||||
|
||||
private:
|
||||
SecureAppSettingsRepository *m_appSettingsRepository;
|
||||
};
|
||||
|
||||
#endif // PAIRINGCONTROLLER_H
|
||||
@@ -307,7 +307,72 @@ ErrorCode SubscriptionController::importTrialFromGateway(const QString &userCoun
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::updateServiceFromGateway(const QString &serverId, const QString &serverCountryCode, bool isConnectEvent,
|
||||
ErrorCode SubscriptionController::importServerFromQrPairingResponse(const QString &vpnConfigKey, const QJsonObject &serviceInfo,
|
||||
const QJsonArray &supportedProtocols,
|
||||
int *duplicateServerIndex)
|
||||
{
|
||||
if (vpnConfigKey.isEmpty()) {
|
||||
return ErrorCode::ApiConfigEmptyError;
|
||||
}
|
||||
|
||||
QString normalizedKey = vpnConfigKey;
|
||||
normalizedKey.replace(QStringLiteral("vpn://"), QString());
|
||||
|
||||
for (int i = 0; i < m_serversRepository->serversCount(); ++i) {
|
||||
const auto apiV2 = m_serversRepository->apiV2Config(m_serversRepository->serverIdAt(i));
|
||||
QString existingVpnKey = apiV2.has_value() ? apiV2->vpnKey() : QString();
|
||||
existingVpnKey.replace(QStringLiteral("vpn://"), QString());
|
||||
if (!existingVpnKey.isEmpty() && existingVpnKey == normalizedKey) {
|
||||
if (duplicateServerIndex) {
|
||||
*duplicateServerIndex = i;
|
||||
}
|
||||
return ErrorCode::ApiConfigAlreadyAdded;
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray configString =
|
||||
QByteArray::fromBase64(normalizedKey.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
|
||||
QByteArray configUncompressed = qUncompress(configString);
|
||||
if (!configUncompressed.isEmpty()) {
|
||||
configString = configUncompressed;
|
||||
}
|
||||
if (configString.isEmpty()) {
|
||||
return ErrorCode::ApiConfigEmptyError;
|
||||
}
|
||||
|
||||
QJsonObject serverJson = QJsonDocument::fromJson(configString).object();
|
||||
if (serverJson.isEmpty()) {
|
||||
return ErrorCode::ApiConfigEmptyError;
|
||||
}
|
||||
|
||||
if (serverJson.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
QJsonObject apiConfig = serverJson.value(apiDefs::key::apiConfig).toObject();
|
||||
if (!serviceInfo.isEmpty()) {
|
||||
apiConfig.insert(apiDefs::key::serviceInfo, serviceInfo);
|
||||
}
|
||||
if (!supportedProtocols.isEmpty()) {
|
||||
apiConfig.insert(apiDefs::key::supportedProtocols, supportedProtocols);
|
||||
}
|
||||
serverJson[apiDefs::key::apiConfig] = apiConfig;
|
||||
|
||||
ApiV2ServerConfig apiV2ServerConfig = ApiV2ServerConfig::fromJson(serverJson);
|
||||
if (apiV2ServerConfig.apiConfig.vpnKey.isEmpty()) {
|
||||
QString fullKey = vpnConfigKey.trimmed();
|
||||
if (!fullKey.startsWith(QStringLiteral("vpn://"))) {
|
||||
fullKey = QStringLiteral("vpn://") + fullKey;
|
||||
}
|
||||
apiV2ServerConfig.apiConfig.vpnKey = fullKey;
|
||||
}
|
||||
|
||||
m_serversRepository->addServer(QString(), apiV2ServerConfig.toJson(),
|
||||
serverConfigUtils::configTypeFromJson(apiV2ServerConfig.toJson()));
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, bool isConnectEvent,
|
||||
CaptchaInfo *captchaInfoOut, ProtocolData *usedProtocolDataOut)
|
||||
{
|
||||
auto apiV2 = m_serversRepository->apiV2Config(serverId);
|
||||
@@ -317,11 +382,11 @@ ErrorCode SubscriptionController::updateServiceFromGateway(const QString &server
|
||||
const bool isTestPurchase = apiV2->apiConfig.isTestPurchase;
|
||||
QString serviceProtocol = apiV2->serviceProtocol();
|
||||
|
||||
if (!serverCountryCode.isEmpty()) {
|
||||
if (!newCountryCode.isEmpty()) {
|
||||
const auto availableCountries = apiV2->apiConfig.availableCountries;
|
||||
for (const auto &country : availableCountries) {
|
||||
const auto countryObject = country.toObject();
|
||||
if (countryObject.value(apiDefs::key::serverCountryCode).toString() != serverCountryCode) {
|
||||
if (countryObject.value(apiDefs::key::serverCountryCode).toString() != newCountryCode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -337,7 +402,7 @@ ErrorCode SubscriptionController::updateServiceFromGateway(const QString &server
|
||||
|
||||
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
|
||||
.addField(apiDefs::key::userCountryCode, apiV2->apiConfig.userCountryCode)
|
||||
.addField(apiDefs::key::serverCountryCode, serverCountryCode)
|
||||
.addField(apiDefs::key::serverCountryCode, newCountryCode)
|
||||
.addField(apiDefs::key::serviceType, apiV2->serviceType())
|
||||
.addField(apiDefs::key::serviceProtocol, serviceProtocol)
|
||||
.addField(apiDefs::key::publicKey, publicKeyForProtocol(serviceProtocol, protocolData))
|
||||
@@ -362,34 +427,6 @@ 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;
|
||||
@@ -440,7 +477,7 @@ ErrorCode SubscriptionController::applyUpdatedServiceConfig(const QString &serve
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::resolveUpdateServiceCaptcha(const QString &serverId, const QString &serverCountryCode,
|
||||
ErrorCode SubscriptionController::resolveUpdateServiceCaptcha(const QString &serverId, const QString &newCountryCode,
|
||||
bool isConnectEvent, const ProtocolData &protocolData,
|
||||
const QString &captchaId, const QString &captchaSolution,
|
||||
CaptchaInfo *retryCaptchaOut)
|
||||
@@ -454,7 +491,7 @@ ErrorCode SubscriptionController::resolveUpdateServiceCaptcha(const QString &ser
|
||||
|
||||
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
|
||||
.addField(apiDefs::key::userCountryCode, apiV2->apiConfig.userCountryCode)
|
||||
.addField(apiDefs::key::serverCountryCode, serverCountryCode)
|
||||
.addField(apiDefs::key::serverCountryCode, newCountryCode)
|
||||
.addField(apiDefs::key::serviceType, apiV2->serviceType())
|
||||
.addField(apiDefs::key::serviceProtocol, serviceProtocol)
|
||||
.addField(apiDefs::key::publicKey, publicKeyForProtocol(serviceProtocol, protocolData))
|
||||
@@ -627,19 +664,17 @@ ErrorCode SubscriptionController::prepareVpnKeyExport(const QString &serverId, Q
|
||||
ErrorCode SubscriptionController::validateAndUpdateConfig(const QString &serverId, bool hasInstalledContainers,
|
||||
CaptchaInfo *captchaInfoOut, ProtocolData *usedProtocolDataOut)
|
||||
{
|
||||
const auto apiV2 = m_serversRepository->apiV2Config(serverId);
|
||||
if (!apiV2.has_value()) {
|
||||
if (!m_serversRepository->apiV2Config(serverId).has_value()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
const QString serverCountryCode = apiV2->apiConfig.serverCountryCode;
|
||||
|
||||
if (!hasInstalledContainers) {
|
||||
return updateServiceFromGateway(serverId, serverCountryCode, true, captchaInfoOut, usedProtocolDataOut);
|
||||
return updateServiceFromGateway(serverId, "", true, captchaInfoOut, usedProtocolDataOut);
|
||||
}
|
||||
|
||||
if (isApiKeyExpired(serverId)) {
|
||||
qDebug() << "attempt to update api config by expires_at event";
|
||||
return updateServiceFromGateway(serverId, serverCountryCode, true, captchaInfoOut, usedProtocolDataOut);
|
||||
return updateServiceFromGateway(serverId, "", true, captchaInfoOut, usedProtocolDataOut);
|
||||
}
|
||||
|
||||
return ErrorCode::NoError;
|
||||
@@ -785,7 +820,7 @@ ErrorCode SubscriptionController::getAccountInfo(const QString &serverId, QJsonO
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
accountInfo = QJsonDocument::fromJson(responseBody).object();
|
||||
accountInfo = QJsonDocument::fromJson(responseBody).object();
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
@@ -816,7 +851,7 @@ QFuture<QPair<ErrorCode, QString>> SubscriptionController::getRenewalLink(const
|
||||
apiDefs::requestTimeoutMsecs,
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled(),
|
||||
m_appSettingsRepository);
|
||||
auto postFuture = gatewayController->postAsync(QString("%1v1/renewal_link"), apiPayload);
|
||||
auto postFuture = gatewayController->postAsync(QString("%1v1/renewal_link"), apiPayload, nullptr, gatewayController);
|
||||
auto *watcher = new QFutureWatcher<QPair<ErrorCode, QByteArray>>();
|
||||
QObject::connect(watcher, &QFutureWatcher<QPair<ErrorCode, QByteArray>>::finished,
|
||||
[promise, watcher, gatewayController]() {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef SUBSCRIPTIONCONTROLLER_H
|
||||
#define SUBSCRIPTIONCONTROLLER_H
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QByteArray>
|
||||
#include <QFuture>
|
||||
@@ -45,12 +46,13 @@ public:
|
||||
ErrorCode importTrialFromGateway(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const QString &email);
|
||||
|
||||
ErrorCode fetchServiceConfig(const QString &serverId, const QString &serviceProtocol, QJsonObject &serverConfigJson);
|
||||
ErrorCode importServerFromQrPairingResponse(const QString &vpnConfigKey, const QJsonObject &serviceInfo,
|
||||
const QJsonArray &supportedProtocols, int *duplicateServerIndex = nullptr);
|
||||
|
||||
ErrorCode updateServiceFromGateway(const QString &serverId, const QString &serverCountryCode, bool isConnectEvent,
|
||||
ErrorCode updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, bool isConnectEvent,
|
||||
CaptchaInfo *captchaInfoOut = nullptr, ProtocolData *usedProtocolDataOut = nullptr);
|
||||
|
||||
ErrorCode resolveUpdateServiceCaptcha(const QString &serverId, const QString &serverCountryCode, bool isConnectEvent,
|
||||
ErrorCode resolveUpdateServiceCaptcha(const QString &serverId, const QString &newCountryCode, bool isConnectEvent,
|
||||
const ProtocolData &protocolData, const QString &captchaId,
|
||||
const QString &captchaSolution, CaptchaInfo *retryCaptchaOut = nullptr);
|
||||
|
||||
@@ -84,21 +86,6 @@ 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,15 +289,6 @@ 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,6 +1,5 @@
|
||||
#include "coreController.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDirIterator>
|
||||
#include <QTranslator>
|
||||
#include <QTimer>
|
||||
@@ -20,9 +19,6 @@
|
||||
|
||||
#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
|
||||
|
||||
@@ -129,12 +125,6 @@ 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);
|
||||
|
||||
@@ -170,6 +160,7 @@ void CoreController::initCoreControllers()
|
||||
m_allowedDnsController = new AllowedDnsController(m_appSettingsRepository);
|
||||
m_servicesCatalogController = new ServicesCatalogController(m_appSettingsRepository);
|
||||
m_subscriptionController = new SubscriptionController(m_serversRepository, m_appSettingsRepository);
|
||||
m_pairingController = new PairingController(m_appSettingsRepository);
|
||||
m_storePurchaseController = new StorePurchaseController(m_serversRepository, m_appSettingsRepository);
|
||||
m_newsController = new NewsController(m_appSettingsRepository, m_serversRepository);
|
||||
m_updateController = new UpdateController(m_appSettingsRepository, this);
|
||||
@@ -179,7 +170,6 @@ 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()
|
||||
@@ -189,6 +179,7 @@ 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,
|
||||
@@ -213,12 +204,6 @@ 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);
|
||||
|
||||
@@ -251,6 +236,9 @@ void CoreController::initControllers()
|
||||
m_connectionController, this);
|
||||
setQmlContextProperty("SubscriptionUiController", m_subscriptionUiController);
|
||||
|
||||
m_pairingUiController = new PairingUiController(m_pairingController, m_serversController, m_subscriptionController, m_appSettingsRepository, this);
|
||||
setQmlContextProperty("PairingUiController", m_pairingUiController);
|
||||
|
||||
m_apiNewsUiController = new ApiNewsUiController(m_newsModel, m_newsController, this);
|
||||
setQmlContextProperty("ApiNewsController", m_apiNewsUiController);
|
||||
|
||||
@@ -287,7 +275,7 @@ void CoreController::initAppleController()
|
||||
|
||||
void CoreController::initLogging()
|
||||
{
|
||||
#ifndef Q_OS_ANDROID
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
bool enabled = m_appSettingsRepository->isSaveLogs();
|
||||
if (enabled) {
|
||||
if (!Logger::init(false)) {
|
||||
@@ -296,11 +284,6 @@ 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,15 +3,15 @@
|
||||
|
||||
#include <QObject>
|
||||
#include <QQmlContext>
|
||||
#include <QSharedPointer>
|
||||
#include <QThread>
|
||||
#include <QTranslator>
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
#include "ui/utils/systemTrayNotificationHandler.h"
|
||||
#endif
|
||||
|
||||
#include "ui/controllers/api/subscriptionUiController.h"
|
||||
#include "ui/controllers/api/pairingUiController.h"
|
||||
#include "core/controllers/api/pairingController.h"
|
||||
#include "ui/controllers/api/apiNewsUiController.h"
|
||||
#include "ui/controllers/appSplitTunnelingUiController.h"
|
||||
#include "ui/controllers/allowedDnsUiController.h"
|
||||
@@ -24,8 +24,6 @@
|
||||
#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"
|
||||
@@ -45,7 +43,6 @@
|
||||
#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"
|
||||
|
||||
@@ -62,7 +59,6 @@
|
||||
#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"
|
||||
@@ -142,10 +138,6 @@ 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();
|
||||
@@ -173,15 +165,13 @@ private:
|
||||
QMetaObject::Connection m_reloadConfigErrorOccurredConnection;
|
||||
|
||||
ConnectionUiController* m_connectionUiController;
|
||||
FocusController* m_focusController = nullptr;
|
||||
FocusController* m_focusController;
|
||||
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;
|
||||
@@ -192,6 +182,7 @@ private:
|
||||
UpdateUiController* m_updateUiController;
|
||||
|
||||
SubscriptionUiController* m_subscriptionUiController;
|
||||
PairingUiController* m_pairingUiController;
|
||||
ApiNewsUiController* m_apiNewsUiController;
|
||||
|
||||
ServicesCatalogUiController* m_servicesCatalogUiController;
|
||||
@@ -203,6 +194,7 @@ private:
|
||||
AllowedDnsController* m_allowedDnsController;
|
||||
ServicesCatalogController* m_servicesCatalogController;
|
||||
SubscriptionController* m_subscriptionController;
|
||||
PairingController* m_pairingController;
|
||||
StorePurchaseController* m_storePurchaseController;
|
||||
NewsController* m_newsController;
|
||||
UpdateController* m_updateController;
|
||||
@@ -210,7 +202,6 @@ private:
|
||||
ExportController* m_exportController;
|
||||
ConnectionController* m_connectionController;
|
||||
SettingsController* m_settingsController;
|
||||
LocalProxyController* m_localProxyController;
|
||||
|
||||
ContainersModel* m_containersModel;
|
||||
ContainersModel* m_defaultServerContainersModel;
|
||||
@@ -227,8 +218,6 @@ private:
|
||||
ApiSubscriptionPlansModel* m_apiSubscriptionPlansModel;
|
||||
ApiBenefitsModel* m_apiBenefitsModel;
|
||||
ApiCountryModel* m_apiCountryModel;
|
||||
ApiCountryListModel* m_apiCountryListModel;
|
||||
ApiCountryListModel* m_apiConfigsCountryListModel;
|
||||
ApiAccountInfoModel* m_apiAccountInfoModel;
|
||||
ApiDevicesModel* m_apiDevicesModel;
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "coreSignalHandlers.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QTimer>
|
||||
#include <QtConcurrent>
|
||||
|
||||
@@ -12,7 +11,6 @@
|
||||
#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"
|
||||
@@ -24,6 +22,7 @@
|
||||
#include "ui/controllers/selfhosted/installUiController.h"
|
||||
#include "ui/controllers/importUiController.h"
|
||||
#include "ui/controllers/api/subscriptionUiController.h"
|
||||
#include "ui/controllers/api/pairingUiController.h"
|
||||
#include "ui/controllers/updateUiController.h"
|
||||
#include "ui/models/serversModel.h"
|
||||
#include "core/controllers/serversController.h"
|
||||
@@ -34,7 +33,6 @@
|
||||
#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"
|
||||
@@ -68,8 +66,6 @@ void CoreSignalHandlers::initAllHandlers()
|
||||
initExportControllerHandler();
|
||||
initImportControllerHandler();
|
||||
initApiCountryModelUpdateHandler();
|
||||
initCountryListSortModeHandler();
|
||||
initCountryListStateHandler();
|
||||
initSubscriptionRefreshHandler();
|
||||
initAdminConfigRevokedHandler();
|
||||
initPassphraseRequestHandler();
|
||||
@@ -84,7 +80,6 @@ void CoreSignalHandlers::initAllHandlers()
|
||||
initAppSplitTunnelingModelUpdateHandler();
|
||||
initPrepareConfigHandler();
|
||||
initUnsupportedConnectDrawerHandler();
|
||||
initBackNavigationHandler();
|
||||
initStrictKillSwitchHandler();
|
||||
initAndroidSettingsHandler();
|
||||
initAndroidConnectionHandler();
|
||||
@@ -92,7 +87,6 @@ void CoreSignalHandlers::initAllHandlers()
|
||||
initIosSettingsHandler();
|
||||
initNotificationHandler();
|
||||
initUpdateFoundHandler();
|
||||
initLocalProxyHandler();
|
||||
}
|
||||
|
||||
void CoreSignalHandlers::initErrorMessagesHandler()
|
||||
@@ -105,6 +99,9 @@ void CoreSignalHandlers::initErrorMessagesHandler()
|
||||
connect(m_coreController->m_subscriptionUiController, &SubscriptionUiController::errorOccurred, m_coreController->m_pageController,
|
||||
qOverload<ErrorCode>(&PageController::showErrorMessage));
|
||||
|
||||
connect(m_coreController->m_pairingUiController, &PairingUiController::errorOccurred, m_coreController->m_pageController,
|
||||
qOverload<ErrorCode>(&PageController::showErrorMessage));
|
||||
|
||||
connect(m_coreController->m_settingsUiController, &SettingsUiController::errorOccurred, m_coreController->m_pageController,
|
||||
qOverload<ErrorCode>(&PageController::showErrorMessage));
|
||||
}
|
||||
@@ -180,85 +177,6 @@ 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]() {
|
||||
@@ -272,10 +190,8 @@ void CoreSignalHandlers::initApiCountryModelUpdateHandler()
|
||||
return;
|
||||
}
|
||||
|
||||
m_coreController->m_apiCountryModel->setServerId(processedServerId);
|
||||
m_coreController->m_apiCountryModel->updateModel(apiV2->apiConfig.availableCountries,
|
||||
apiV2->apiConfig.serverCountryCode,
|
||||
apiV2->apiConfig.userCountryCode);
|
||||
apiV2->apiConfig.serverCountryCode);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -318,18 +234,6 @@ 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()
|
||||
@@ -459,16 +363,6 @@ 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,
|
||||
@@ -554,12 +448,3 @@ 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,8 +20,6 @@ private:
|
||||
void initExportControllerHandler();
|
||||
void initImportControllerHandler();
|
||||
void initApiCountryModelUpdateHandler();
|
||||
void initCountryListSortModeHandler();
|
||||
void initCountryListStateHandler();
|
||||
void initSubscriptionRefreshHandler();
|
||||
void initAdminConfigRevokedHandler();
|
||||
void initPassphraseRequestHandler();
|
||||
@@ -36,7 +34,6 @@ private:
|
||||
void initAppSplitTunnelingModelUpdateHandler();
|
||||
void initPrepareConfigHandler();
|
||||
void initUnsupportedConnectDrawerHandler();
|
||||
void initBackNavigationHandler();
|
||||
void initStrictKillSwitchHandler();
|
||||
void initAndroidSettingsHandler();
|
||||
void initAndroidConnectionHandler();
|
||||
@@ -44,7 +41,6 @@ private:
|
||||
void initIosSettingsHandler();
|
||||
void initNotificationHandler();
|
||||
void initUpdateFoundHandler();
|
||||
void initLocalProxyHandler();
|
||||
|
||||
CoreController* m_coreController;
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,17 +1,22 @@
|
||||
#ifndef GATEWAYCONTROLLER_H
|
||||
#define GATEWAYCONTROLLER_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <functional>
|
||||
|
||||
#include <QFuture>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkReply>
|
||||
#include <QObject>
|
||||
#include <QPair>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QPromise>
|
||||
#include <QSharedPointer>
|
||||
|
||||
#include "core/utils/errorCodes.h"
|
||||
#include "core/utils/routeModes.h"
|
||||
#include "core/utils/commonStructs.h"
|
||||
|
||||
#include "agw.h"
|
||||
#ifdef Q_OS_IOS
|
||||
#include "platforms/ios/ios_controller.h"
|
||||
#endif
|
||||
|
||||
class SecureAppSettingsRepository;
|
||||
|
||||
@@ -23,26 +28,57 @@ 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);
|
||||
QFuture<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject &apiPayload,
|
||||
QNetworkReply **activeReplyOut = nullptr,
|
||||
const QSharedPointer<GatewayController> &keepAlive = {});
|
||||
|
||||
private:
|
||||
void prepareRequest();
|
||||
QStringList requestHosts() const;
|
||||
struct EncryptedRequestData
|
||||
{
|
||||
QNetworkRequest request;
|
||||
QByteArray requestBody;
|
||||
QByteArray key;
|
||||
QByteArray iv;
|
||||
QByteArray salt;
|
||||
amnezia::ErrorCode errorCode;
|
||||
bool isPlaintextLocalGateway = false;
|
||||
};
|
||||
|
||||
QPair<amnezia::ErrorCode, QByteArray> executePost(const QString &endpoint, const QJsonObject &apiPayload);
|
||||
static amnezia::ErrorCode mapResultCode(const int code, const QByteArray &responseBody);
|
||||
void persistState();
|
||||
struct DecryptionResult
|
||||
{
|
||||
QByteArray decryptedBody;
|
||||
bool isDecryptionSuccessful;
|
||||
};
|
||||
|
||||
EncryptedRequestData prepareRequest(const QString &endpoint, const QJsonObject &apiPayload);
|
||||
DecryptionResult tryDecryptResponseBody(const QByteArray &encryptedResponseBody, QNetworkReply::NetworkError replyError,
|
||||
const QByteArray &key, const QByteArray &iv, const QByteArray &salt);
|
||||
DecryptionResult resolveResponseBody(const QByteArray &responseBody, QNetworkReply::NetworkError replyError, const QByteArray &key,
|
||||
const QByteArray &iv, const QByteArray &salt);
|
||||
|
||||
QStringList getProxyUrls(const QString &serviceType, const QString &userCountryCode);
|
||||
bool shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody, bool isDecryptionSuccessful);
|
||||
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 QSharedPointer<GatewayController> &life, const QStringList &proxyStorageUrls, int currentProxyStorageIndex,
|
||||
const QString &proxyUrlsCacheKey, const std::function<void(const QStringList &)> &onComplete);
|
||||
void getProxyUrlAsync(const QSharedPointer<GatewayController> &life, const QStringList &proxyUrls, int currentProxyIndex,
|
||||
const std::function<void(const QString &)> &onComplete);
|
||||
void bypassProxyAsync(
|
||||
const QSharedPointer<GatewayController> &life, const QString &endpoint, const QString &proxyUrl, const EncryptedRequestData &encRequestData,
|
||||
const std::function<void(const QByteArray &, bool, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> &onComplete);
|
||||
|
||||
int m_requestTimeoutMsecs;
|
||||
QString m_gatewayEndpoint;
|
||||
bool m_isDevEnvironment = false;
|
||||
bool m_isStrictKillSwitchEnabled = false;
|
||||
SecureAppSettingsRepository *m_appSettingsRepository = nullptr;
|
||||
|
||||
agw_client_handle m_client = 0;
|
||||
bool m_publicKeyMissing = false;
|
||||
QByteArray m_lastPersistedState;
|
||||
inline static QString m_proxyUrl;
|
||||
};
|
||||
|
||||
#endif // GATEWAYCONTROLLER_H
|
||||
@@ -1,545 +0,0 @@
|
||||
#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
|
||||
@@ -1,85 +0,0 @@
|
||||
#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
|
||||
@@ -1,270 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
#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(QString::fromLatin1(amnezia::protocols::xray::defaultFingerprint));
|
||||
vlessServer.fingerprint = realitySettings.value(amnezia::protocols::xray::fingerprint).toString("chrome");
|
||||
vlessServer.spiderX = realitySettings.value(amnezia::protocols::xray::spiderX).toString("");
|
||||
} else if (vlessServer.security == "tls") {
|
||||
QJsonObject tlsSettings = streamSettings.value("tlsSettings").toObject();
|
||||
|
||||
@@ -64,7 +64,6 @@ 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;
|
||||
@@ -80,8 +79,6 @@ 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;
|
||||
}
|
||||
@@ -250,10 +247,6 @@ 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();
|
||||
@@ -779,11 +772,8 @@ void ImportController::processAmneziaConfig(QJsonObject &config) const
|
||||
}
|
||||
|
||||
QJsonObject jsonConfig = QJsonDocument::fromJson(protocolConfig.toUtf8()).object();
|
||||
if (jsonConfig.value(configKey::mtu).toString().trimmed().isEmpty()) {
|
||||
jsonConfig[configKey::mtu] = ContainerUtils::isAwgContainer(dockerContainer)
|
||||
? protocols::awg::defaultMtu
|
||||
: protocols::wireguard::defaultMtu;
|
||||
}
|
||||
jsonConfig[configKey::mtu] =
|
||||
ContainerUtils::isAwgContainer(dockerContainer) ? protocols::awg::defaultMtu : protocols::wireguard::defaultMtu;
|
||||
|
||||
containerConfig[configKey::lastConfig] = QString(QJsonDocument(jsonConfig).toJson());
|
||||
|
||||
@@ -793,3 +783,4 @@ void ImportController::processAmneziaConfig(QJsonObject &config) const
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ namespace
|
||||
Xray,
|
||||
ShadowSocks,
|
||||
Backup,
|
||||
XraySettings,
|
||||
Invalid
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
|
||||
#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"
|
||||
@@ -61,70 +60,6 @@ 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)) {
|
||||
@@ -140,34 +75,15 @@ 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(
|
||||
"\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 += QLatin1String("\nsudo docker volume rm -f $CONTAINER_NAME-data 2>/dev/null || true");
|
||||
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,
|
||||
@@ -186,7 +102,7 @@ InstallController::~InstallController()
|
||||
}
|
||||
|
||||
ErrorCode InstallController::setupContainer(const ServerCredentials &credentials, DockerContainer container, ContainerConfig &config,
|
||||
bool isUpdate, bool acceptXrayKeyLoss)
|
||||
bool isUpdate)
|
||||
{
|
||||
SshSession sshSession;
|
||||
ErrorCode e = ErrorCode::NoError;
|
||||
@@ -213,38 +129,12 @@ 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 && 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";
|
||||
const bool removeDataVolume = !isUpdate && (container == DockerContainer::MtProxy
|
||||
|| container == DockerContainer::Telemt || container == DockerContainer::TProxy);
|
||||
sshSession.runScript(credentials, buildRemoveContainerScript(removeContainerVars, removeDataVolume));
|
||||
|
||||
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;
|
||||
@@ -253,10 +143,6 @@ 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;
|
||||
@@ -275,41 +161,20 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
|
||||
}
|
||||
|
||||
ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerContainer container, const ContainerConfig &oldConfig,
|
||||
ContainerConfig &newConfig, bool acceptXrayKeyLoss)
|
||||
ContainerConfig &newConfig)
|
||||
{
|
||||
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()) {
|
||||
if (!isUpdateDockerContainerRequired(container, oldConfig, newConfig)) {
|
||||
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
if (!adminConfig.has_value()) {
|
||||
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();
|
||||
@@ -321,24 +186,22 @@ 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) {
|
||||
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);
|
||||
errorCode = setupContainer(credentials, container, newConfig, true);
|
||||
|
||||
// Reinstall pulls the latest container image, so the server runs the latest protocol version
|
||||
if (errorCode == ErrorCode::NoError && container == DockerContainer::Awg2) {
|
||||
@@ -346,12 +209,7 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
|
||||
awgConfig->serverConfig.protocolVersion = protocols::awg::awgV3;
|
||||
}
|
||||
}
|
||||
} 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) {
|
||||
} else if (container != DockerContainer::Xray && container != DockerContainer::SSXray) {
|
||||
errorCode = configureContainerWorker(credentials, container, newConfig, sshSession);
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
errorCode = startupContainerWorker(credentials, container, newConfig, sshSession);
|
||||
@@ -374,9 +232,7 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
|
||||
TProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
|
||||
}
|
||||
if (reinstallRequired) {
|
||||
if (container != DockerContainer::Xray) {
|
||||
clearCachedProfile(serverId, container);
|
||||
}
|
||||
clearCachedProfile(serverId, container);
|
||||
}
|
||||
adminConfig->updateContainerConfig(container, newConfig);
|
||||
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
|
||||
@@ -585,9 +441,6 @@ 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;
|
||||
@@ -717,10 +570,6 @@ 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) {
|
||||
@@ -757,127 +606,6 @@ 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) {
|
||||
@@ -1015,20 +743,7 @@ bool InstallController::isReinstallContainerRequired(DockerContainer container,
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
if (container == DockerContainer::Xray || container == DockerContainer::SSXray) {
|
||||
const auto *oldXrayConfig = oldConfig.getXrayProtocolConfig();
|
||||
const auto *newXrayConfig = newConfig.getXrayProtocolConfig();
|
||||
|
||||
@@ -1300,43 +1015,6 @@ 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);
|
||||
@@ -1372,19 +1050,10 @@ ErrorCode InstallController::removeContainer(const QString &serverId, DockerCont
|
||||
SshSession sshSession;
|
||||
const amnezia::ScriptVars removeContainerVars =
|
||||
amnezia::genBaseVars(credentials, container, QString(), QString());
|
||||
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;
|
||||
}
|
||||
const bool removeDataVolume = (container == DockerContainer::MtProxy || container == DockerContainer::Telemt
|
||||
|| container == DockerContainer::TProxy);
|
||||
ErrorCode errorCode =
|
||||
sshSession.runScript(credentials, buildRemoveContainerScript(removeContainerVars, removeDataVolume));
|
||||
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
QMap<DockerContainer, ContainerConfig> containers = adminConfig->containers;
|
||||
@@ -1486,13 +1155,6 @@ 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,16 +33,15 @@ public:
|
||||
QObject *parent = nullptr);
|
||||
~InstallController();
|
||||
|
||||
ErrorCode setupContainer(const ServerCredentials &credentials, DockerContainer container, ContainerConfig &config, bool isUpdate = false, bool acceptXrayKeyLoss = false);
|
||||
ErrorCode setupContainer(const ServerCredentials &credentials, DockerContainer container, ContainerConfig &config, bool isUpdate = false);
|
||||
|
||||
// Updates server-side container settings (admin self-hosted only): reconfigures the container over SSH.
|
||||
ErrorCode updateServerConfig(const QString &serverId, DockerContainer container, const ContainerConfig &oldConfig, ContainerConfig &newConfig, bool acceptXrayKeyLoss = false);
|
||||
ErrorCode updateServerConfig(const QString &serverId, DockerContainer container, const ContainerConfig &oldConfig, ContainerConfig &newConfig);
|
||||
|
||||
// Updates client-local settings only: rewrites the stored container config for any self-hosted/native server. No SSH.
|
||||
ErrorCode updateClientConfig(const QString &serverId, DockerContainer container, ContainerConfig &newConfig);
|
||||
|
||||
ErrorCode rebootServer(const QString &serverId);
|
||||
ErrorCode updateServerPackages(const QString &serverId);
|
||||
ErrorCode removeAllContainers(const QString &serverId);
|
||||
ErrorCode removeContainer(const QString &serverId, DockerContainer container);
|
||||
|
||||
@@ -107,11 +106,6 @@ 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);
|
||||
@@ -136,7 +130,7 @@ private:
|
||||
bool m_cancelInstallation = false;
|
||||
QString m_tproxyInstallHostname;
|
||||
QString m_tproxyInstallEmail;
|
||||
|
||||
|
||||
#ifndef Q_OS_IOS
|
||||
QList<QSharedPointer<QProcess>> m_sftpMountProcesses;
|
||||
#endif
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QDateTime>
|
||||
|
||||
#include "core/utils/containerEnum.h"
|
||||
@@ -56,33 +55,6 @@ 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();
|
||||
@@ -276,8 +248,7 @@ ErrorCode UsersController::getWireGuardClients(const DockerContainer container,
|
||||
}
|
||||
|
||||
ErrorCode UsersController::getXrayClients(const DockerContainer container, const ServerCredentials& credentials,
|
||||
SshSession* sshSession, int &count, QJsonArray &clientsTable,
|
||||
const QString &volumeDefaultUuid)
|
||||
SshSession* sshSession, int &count, QJsonArray &clientsTable)
|
||||
{
|
||||
ErrorCode error = ErrorCode::NoError;
|
||||
|
||||
@@ -312,7 +283,6 @@ 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)) {
|
||||
@@ -320,6 +290,9 @@ 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;
|
||||
@@ -366,9 +339,6 @@ 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);
|
||||
|
||||
@@ -379,7 +349,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, xrayVolumeUuid);
|
||||
error = getXrayClients(container, credentials, &sshSession, count, m_clientsTable);
|
||||
}
|
||||
if (error != ErrorCode::NoError) {
|
||||
emit clientsUpdated(QJsonArray());
|
||||
@@ -395,15 +365,6 @@ 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);
|
||||
|
||||
@@ -463,13 +424,6 @@ 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);
|
||||
@@ -680,19 +634,6 @@ 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) {
|
||||
@@ -706,6 +647,9 @@ 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,12 +64,7 @@ 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,
|
||||
const QString &volumeDefaultUuid);
|
||||
|
||||
QString readXrayVolumeUuid(const DockerContainer container, const ServerCredentials &credentials,
|
||||
SshSession *sshSession) const;
|
||||
static int stripXrayVolumeUuidFromTable(QJsonArray &clientsTable, const QString &volumeUuid);
|
||||
SshSession* sshSession, int &count, QJsonArray &clientsTable);
|
||||
|
||||
ErrorCode wgShow(const DockerContainer container, const ServerCredentials &credentials,
|
||||
SshSession* sshSession, std::vector<WgShowData> &data);
|
||||
|
||||
@@ -8,9 +8,8 @@
|
||||
#include "version.h"
|
||||
#include "ui/utils/qAutoStart.h"
|
||||
#include "logger.h"
|
||||
#include "core/controllers/logsController.h"
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
#include "core/utils/swiftBridge.h"
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include "platforms/android/android_controller.h"
|
||||
#endif
|
||||
|
||||
QString getPlatformName()
|
||||
@@ -87,9 +86,6 @@ 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);
|
||||
|
||||
@@ -100,8 +96,12 @@ void SettingsController::toggleLogging(bool enable)
|
||||
|
||||
void SettingsController::clearLogs()
|
||||
{
|
||||
LogsController::clear(LogsController::Stream::App);
|
||||
LogsController::clear(LogsController::Stream::Tunnel);
|
||||
#ifdef Q_OS_ANDROID
|
||||
AndroidController::instance()->clearLogs();
|
||||
#else
|
||||
Logger::clearLogs(false);
|
||||
Logger::clearServiceLogs();
|
||||
#endif
|
||||
}
|
||||
|
||||
QByteArray SettingsController::backupAppConfig() const
|
||||
|
||||
@@ -9,13 +9,14 @@
|
||||
#include <QUrl>
|
||||
|
||||
#include "amneziaApplication.h"
|
||||
#include "logger.h"
|
||||
#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/errorStrings.h"
|
||||
#include "core/utils/selfhosted/scriptsRegistry.h"
|
||||
#include "logger.h"
|
||||
#include "version.h"
|
||||
|
||||
#if defined(Q_OS_ANDROID)
|
||||
#include "platforms/android/android_controller.h"
|
||||
@@ -141,7 +142,7 @@ void UpdateController::checkForUpdates()
|
||||
|
||||
auto gatewayController = QSharedPointer<GatewayController>::create(m_appSettingsRepository->getGatewayEndpoint(),
|
||||
m_appSettingsRepository->isDevGatewayEnv(),
|
||||
apiDefs::requestTimeoutMsecs,
|
||||
7000,
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled(),
|
||||
m_appSettingsRepository);
|
||||
|
||||
|
||||
@@ -61,6 +61,8 @@ 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,8 +1,5 @@
|
||||
#include "installerBase.h"
|
||||
|
||||
#include <QRandomGenerator>
|
||||
#include <iterator>
|
||||
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
@@ -88,12 +85,10 @@ ContainerConfig InstallerBase::createBaseConfig(DockerContainer container, int p
|
||||
srv.transport = protocols::xray::defaultTransport;
|
||||
srv.security = protocols::xray::defaultSecurity;
|
||||
srv.flow = protocols::xray::defaultFlow;
|
||||
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.site = protocols::xray::defaultSite;
|
||||
srv.sni = protocols::xray::defaultSni;
|
||||
srv.fingerprint = protocols::xray::defaultFingerprint;
|
||||
srv.alpn = protocols::xray::defaultAlpn;
|
||||
config.protocolConfig = xrayConfig;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "xrayInstaller.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonArray>
|
||||
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
@@ -10,7 +11,6 @@
|
||||
#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,21 +19,37 @@ namespace
|
||||
Logger logger("XrayInstaller");
|
||||
|
||||
// Xray expects uTLS preset names (chrome, firefox, …). Old Amnezia/server templates used "Mozilla/5.0".
|
||||
QString describeServerJsonStatus(amnezia::XrayServerJsonStatus status)
|
||||
QString normalizeXrayFingerprint(const QString &fp)
|
||||
{
|
||||
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;
|
||||
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());
|
||||
}
|
||||
}
|
||||
return QStringLiteral("ok");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +65,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);
|
||||
|
||||
@@ -62,16 +78,267 @@ 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;
|
||||
}
|
||||
|
||||
const amnezia::XrayServerJsonStatus status =
|
||||
XrayServerConfig::fromServerInboundJson(doc.object(), xrayConfig->serverConfig);
|
||||
if (status != amnezia::XrayServerJsonStatus::Ok) {
|
||||
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);
|
||||
}
|
||||
|
||||
return ErrorCode::NoError;
|
||||
|
||||
@@ -146,7 +146,10 @@ QJsonObject ApiConfig::toJson() const
|
||||
if (!availableCountries.isEmpty()) {
|
||||
obj[apiDefs::key::availableCountries] = availableCountries;
|
||||
}
|
||||
|
||||
if (!supportedProtocols.isEmpty()) {
|
||||
obj[apiDefs::key::supportedProtocols] = supportedProtocols;
|
||||
}
|
||||
|
||||
QJsonObject serviceInfoObj = serviceInfo.toJson();
|
||||
if (!serviceInfoObj.isEmpty()) {
|
||||
obj[apiDefs::key::serviceInfo] = serviceInfoObj;
|
||||
@@ -198,6 +201,7 @@ ApiConfig ApiConfig::fromJson(const QJsonObject& json)
|
||||
config.issuedConfigs = json.value(apiDefs::key::issuedConfigs).toInt(0);
|
||||
|
||||
config.availableCountries = json.value(apiDefs::key::availableCountries).toArray();
|
||||
config.supportedProtocols = json.value(apiDefs::key::supportedProtocols).toArray();
|
||||
|
||||
QJsonObject serviceInfoObj = json.value(apiDefs::key::serviceInfo).toObject();
|
||||
if (!serviceInfoObj.isEmpty()) {
|
||||
|
||||
@@ -35,7 +35,8 @@ struct ApiConfig
|
||||
int maxDeviceCount;
|
||||
int issuedConfigs;
|
||||
QJsonArray availableCountries;
|
||||
|
||||
QJsonArray supportedProtocols;
|
||||
|
||||
struct ServiceInfo {
|
||||
bool isAdVisible = false;
|
||||
bool isRenewalAvailable = false;
|
||||
|
||||
@@ -72,12 +72,6 @@ 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;
|
||||
}
|
||||
@@ -172,8 +166,6 @@ 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();
|
||||
@@ -226,12 +218,6 @@ 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;
|
||||
}
|
||||
@@ -347,14 +333,6 @@ 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;
|
||||
@@ -362,8 +340,6 @@ 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,8 +19,6 @@ namespace AwgConstant
|
||||
|
||||
struct AwgServerConfig {
|
||||
QString port;
|
||||
QString cascadeEndpoint;
|
||||
QString cascadePort;
|
||||
QString transportProto;
|
||||
QString protocolVersion;
|
||||
QString subnetAddress;
|
||||
@@ -63,8 +61,6 @@ struct AwgClientConfig {
|
||||
QString nativeConfig;
|
||||
QString hostName;
|
||||
int port;
|
||||
QString cascadeEndpoint;
|
||||
QString cascadePort;
|
||||
QString clientIp;
|
||||
QString clientPrivateKey;
|
||||
QString clientPublicKey;
|
||||
@@ -99,9 +95,6 @@ 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,12 +20,6 @@ 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;
|
||||
}
|
||||
@@ -51,8 +45,6 @@ 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();
|
||||
@@ -81,12 +73,6 @@ 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;
|
||||
}
|
||||
@@ -133,14 +119,6 @@ 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;
|
||||
@@ -148,8 +126,6 @@ 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,8 +12,6 @@ namespace amnezia
|
||||
|
||||
struct WireGuardServerConfig {
|
||||
QString port;
|
||||
QString cascadeEndpoint;
|
||||
QString cascadePort;
|
||||
QString transportProto;
|
||||
QString subnetAddress;
|
||||
QString subnetMask;
|
||||
@@ -30,8 +28,6 @@ struct WireGuardClientConfig {
|
||||
QString nativeConfig;
|
||||
QString hostName;
|
||||
int port;
|
||||
QString cascadeEndpoint;
|
||||
QString cascadePort;
|
||||
QString clientIp;
|
||||
QString clientPrivateKey;
|
||||
QString clientPublicKey;
|
||||
@@ -45,8 +41,6 @@ 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,11 +1,9 @@
|
||||
#ifndef XRAYPROTOCOLCONFIG_H
|
||||
#define XRAYPROTOCOLCONFIG_H
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <optional>
|
||||
|
||||
namespace amnezia
|
||||
@@ -18,8 +16,8 @@ struct XrayXPaddingConfig {
|
||||
bool obfsMode = true; // xPaddingObfsMode
|
||||
QString key; // xPaddingKey
|
||||
QString header; // xPaddingHeader
|
||||
QString placement; // xPaddingPlacement: Cookie|Header|Query|Query in header
|
||||
QString method; // xPaddingMethod: Repeat-x|Tokenish
|
||||
QString placement = protocols::xray::defaultXPaddingPlacement; // xPaddingPlacement: Cookie|Header|Query|Body
|
||||
QString method = protocols::xray::defaultXPaddingMethod; // xPaddingMethod: Repeat-x|Random|Zero
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static XrayXPaddingConfig fromJson(const QJsonObject &json);
|
||||
@@ -47,35 +45,33 @@ struct XrayXmuxConfig {
|
||||
|
||||
// ── XHTTP transport ───────────────────────────────────────────────────────────
|
||||
struct XrayXhttpConfig {
|
||||
QString mode;
|
||||
QString host;
|
||||
QString mode = protocols::xray::defaultXhttpMode; // Auto|Packet-up|Stream-up|Stream-one
|
||||
QString host = protocols::xray::defaultXhttpHost;
|
||||
QString path;
|
||||
QString uplinkMethod = protocols::xray::defaultXhttpUplinkMethod; // POST|PUT|PATCH
|
||||
bool disableGrpc = true;
|
||||
bool disableSse = true;
|
||||
|
||||
// Session & Sequence
|
||||
QString sessionPlacement;
|
||||
QString sessionKey;
|
||||
QString seqPlacement;
|
||||
QString sessionPlacement = protocols::xray::defaultXhttpSessionPlacement;
|
||||
QString sessionKey = protocols::xray::defaultXhttpSessionKey;
|
||||
QString seqPlacement = protocols::xray::defaultXhttpSeqPlacement;
|
||||
QString seqKey;
|
||||
QString uplinkDataPlacement;
|
||||
QString uplinkDataPlacement = protocols::xray::defaultXhttpUplinkDataPlacement;
|
||||
QString uplinkDataKey;
|
||||
|
||||
// Traffic Shaping
|
||||
QString uplinkMethod;
|
||||
QString uplinkChunkSize;
|
||||
QString scMinPostsIntervalMsMin;
|
||||
QString scMinPostsIntervalMsMax;
|
||||
|
||||
XrayXmuxConfig xmux;
|
||||
|
||||
QString uplinkChunkSize = protocols::xray::defaultXhttpUplinkChunkSize;
|
||||
QString scMaxBufferedPosts;
|
||||
QString scMaxEachPostBytesMin;
|
||||
QString scMaxEachPostBytesMax;
|
||||
QString scStreamUpServerSecsMin;
|
||||
QString scStreamUpServerSecsMax;
|
||||
QString scMaxEachPostBytesMin = protocols::xray::defaultXhttpScMaxEachPostBytesMin;
|
||||
QString scMaxEachPostBytesMax = protocols::xray::defaultXhttpScMaxEachPostBytesMax;
|
||||
QString scMinPostsIntervalMsMin = protocols::xray::defaultXhttpScMinPostsIntervalMsMin;
|
||||
QString scMinPostsIntervalMsMax = protocols::xray::defaultXhttpScMinPostsIntervalMsMax;
|
||||
QString scStreamUpServerSecsMin = protocols::xray::defaultXhttpScStreamUpServerSecsMin;
|
||||
QString scStreamUpServerSecsMax = protocols::xray::defaultXhttpScStreamUpServerSecsMax;
|
||||
|
||||
XrayXPaddingConfig xPadding;
|
||||
XrayXmuxConfig xmux;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
/// Reads only keys present in JSON (no Amnezia UI defaults). Use XrayConfigModel::applyDefaultsToServerConfig for UI.
|
||||
@@ -85,40 +81,16 @@ struct XrayXhttpConfig {
|
||||
// ── mKCP transport ────────────────────────────────────────────────────────────
|
||||
struct XrayMkcpConfig {
|
||||
QString tti;
|
||||
QString mtu;
|
||||
QString uplinkCapacity;
|
||||
QString downlinkCapacity;
|
||||
QString cwndMultiplier;
|
||||
QString maxSendingWindow;
|
||||
QString readBufferSize;
|
||||
QString writeBufferSize;
|
||||
bool congestion = true;
|
||||
|
||||
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;
|
||||
@@ -127,13 +99,11 @@ 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;
|
||||
@@ -143,65 +113,7 @@ 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) ─────────────────────────────
|
||||
@@ -212,9 +124,6 @@ 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 ──────────────────────────────────────────────────
|
||||
@@ -231,14 +140,7 @@ struct XrayProtocolConfig {
|
||||
|
||||
bool needsClientHydration = false;
|
||||
|
||||
QJsonObject toClientOutboundJson(const XrayClientOutboundInputs &inputs) const;
|
||||
|
||||
bool fromClientOutboundJson(const QJsonObject &nativeJson);
|
||||
|
||||
bool hydrateServerConfigFromClientNative();
|
||||
|
||||
bool regenerateClientConfigFromNative();
|
||||
|
||||
};
|
||||
|
||||
} // namespace amnezia
|
||||
|
||||
@@ -161,15 +161,12 @@ ServerDescription buildServerDescription(const ApiV2ServerConfig &server, bool /
|
||||
row.hasWriteAccess = false;
|
||||
|
||||
row.serverName = server.displayName;
|
||||
const bool showServerCountry = !server.isFree() && !server.apiConfig.serverCountryCode.isEmpty();
|
||||
row.baseDescription = showServerCountry ? server.apiConfig.serverCountryName : server.description;
|
||||
row.baseDescription = server.apiConfig.serverCountryCode.isEmpty() ? server.description : server.apiConfig.serverCountryName;
|
||||
|
||||
row.isCountrySelectionAvailable = !server.isFree() && !server.apiConfig.availableCountries.isEmpty();
|
||||
row.isCountrySelectionAvailable = !server.apiConfig.availableCountries.isEmpty();
|
||||
row.apiAvailableCountries = server.apiConfig.availableCountries;
|
||||
if (showServerCountry) {
|
||||
row.apiServerCountryCode = server.apiConfig.serverCountryCode;
|
||||
row.apiServerCountryCodeL10n = server.apiConfig.serverCountryCodeL10n;
|
||||
}
|
||||
row.apiServerCountryCode = server.apiConfig.serverCountryCode;
|
||||
row.apiServerCountryCodeL10n = server.apiConfig.serverCountryCodeL10n;
|
||||
|
||||
row.isAdVisible = server.apiConfig.serviceInfo.isAdVisible;
|
||||
row.adHeader = server.apiConfig.serviceInfo.adHeader;
|
||||
|
||||
@@ -61,10 +61,6 @@ void OpenVpnProtocol::cleanupResources()
|
||||
|
||||
void OpenVpnProtocol::stop()
|
||||
{
|
||||
if (guardStop()) {
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "OpenVpnProtocol::stop()";
|
||||
|
||||
const bool wasActive = m_connectionState == Vpn::ConnectionState::Preparing
|
||||
@@ -183,8 +179,6 @@ void OpenVpnProtocol::updateRouteGateway(QString line)
|
||||
|
||||
ErrorCode OpenVpnProtocol::start()
|
||||
{
|
||||
resetStopGuard();
|
||||
|
||||
cleanupResources();
|
||||
|
||||
if (!QFileInfo::exists(configPath())) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "protocolUtils.h"
|
||||
|
||||
#include <QRandomGenerator>
|
||||
#include <QRegularExpression>
|
||||
#include <QJsonObject>
|
||||
#include <QObject>
|
||||
|
||||
@@ -217,10 +216,3 @@ 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,8 +39,6 @@ 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,20 +27,6 @@ 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,11 +89,6 @@ protected:
|
||||
void startTimeoutTimer();
|
||||
void stopTimeoutTimer();
|
||||
|
||||
bool guardStop();
|
||||
void resetStopGuard();
|
||||
|
||||
bool m_stopped = false;
|
||||
|
||||
Vpn::ConnectionState m_connectionState;
|
||||
|
||||
QString m_routeGateway;
|
||||
|
||||
@@ -50,10 +50,8 @@ WireguardProtocol::~WireguardProtocol()
|
||||
|
||||
void WireguardProtocol::stop()
|
||||
{
|
||||
if (guardStop()) {
|
||||
return;
|
||||
}
|
||||
stopMzImpl();
|
||||
return;
|
||||
}
|
||||
|
||||
ErrorCode WireguardProtocol::startMzImpl()
|
||||
@@ -77,6 +75,5 @@ ErrorCode WireguardProtocol::stopMzImpl()
|
||||
|
||||
ErrorCode WireguardProtocol::start()
|
||||
{
|
||||
resetStopGuard();
|
||||
return startMzImpl();
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
#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"
|
||||
@@ -67,8 +66,6 @@ XrayProtocol::~XrayProtocol()
|
||||
|
||||
ErrorCode XrayProtocol::start()
|
||||
{
|
||||
resetStopGuard();
|
||||
|
||||
qDebug() << "XrayProtocol::start()";
|
||||
|
||||
// Inject SOCKS5 auth into the inbound before starting xray.
|
||||
@@ -90,16 +87,30 @@ ErrorCode XrayProtocol::start()
|
||||
return ErrorCode::XrayExecutableCrashed;
|
||||
}
|
||||
|
||||
xrayConfigStr = amnezia::sanitizeNativeConfig(xrayConfigStr);
|
||||
// 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";
|
||||
}
|
||||
|
||||
return IpcClient::withInterface(
|
||||
[&](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
auto xrayStart = iface->xrayStartOwned(xrayConfigStr);
|
||||
if (!xrayStart.waitForFinished() || xrayStart.returnValue() == 0) {
|
||||
auto xrayStart = iface->xrayStart(xrayConfigStr);
|
||||
if (!xrayStart.waitForFinished() || !xrayStart.returnValue()) {
|
||||
qCritical() << "Failed to start xray";
|
||||
return ErrorCode::XrayExecutableCrashed;
|
||||
}
|
||||
m_xrayToken = xrayStart.returnValue();
|
||||
return startTun2Socks();
|
||||
},
|
||||
[]() { return ErrorCode::AmneziaServiceConnectionFailed; });
|
||||
@@ -107,13 +118,9 @@ ErrorCode XrayProtocol::start()
|
||||
|
||||
void XrayProtocol::stop()
|
||||
{
|
||||
if (guardStop()) {
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "XrayProtocol::stop()";
|
||||
|
||||
IpcClient::withInterface([this](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
auto disableKillSwitch = iface->disableKillSwitch();
|
||||
if (!disableKillSwitch.waitForFinished() || !disableKillSwitch.returnValue())
|
||||
qWarning() << "Failed to disable killswitch";
|
||||
@@ -130,10 +137,9 @@ void XrayProtocol::stop()
|
||||
if (!deleteTun.waitForFinished() || !deleteTun.returnValue())
|
||||
qWarning() << "Failed to delete tun";
|
||||
|
||||
auto xrayStop = iface->xrayStopOwned(m_xrayToken);
|
||||
auto xrayStop = iface->xrayStop();
|
||||
if (!xrayStop.waitForFinished() || !xrayStop.returnValue())
|
||||
qWarning() << "Failed to stop xray";
|
||||
m_xrayToken = 0;
|
||||
});
|
||||
|
||||
if (m_tun2socksProcess) {
|
||||
|
||||
@@ -38,10 +38,6 @@ 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,69 +375,6 @@ 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();
|
||||
@@ -547,33 +484,3 @@ 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,13 +77,6 @@ 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);
|
||||
|
||||
@@ -107,13 +100,6 @@ 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);
|
||||
@@ -140,3 +126,4 @@ private:
|
||||
};
|
||||
|
||||
#endif // SECUREAPPSETTINGSREPOSITORY_H
|
||||
|
||||
@@ -149,9 +149,6 @@ void SecureServersRepository::loadFromStorage()
|
||||
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);
|
||||
@@ -161,25 +158,15 @@ void SecureServersRepository::loadFromStorage()
|
||||
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();
|
||||
@@ -336,15 +323,6 @@ 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,8 +31,6 @@ 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;
|
||||
|
||||
@@ -20,9 +20,6 @@ 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)
|
||||
{
|
||||
@@ -114,7 +111,29 @@ bool apiUtils::isSubscriptionExpiringSoon(const QString &subscriptionEndDate, in
|
||||
return endDate <= nowUtc.addDays(withinDays);
|
||||
}
|
||||
|
||||
amnezia::ErrorCode apiUtils::checkApiResponseErrors(const QByteArray &responseBody)
|
||||
amnezia::ErrorCode apiUtils::errorCodeFromGatewayJsonHttpStatus(const QJsonObject &jsonObj)
|
||||
{
|
||||
if (!jsonObj.contains(QStringLiteral("http_status"))) {
|
||||
return amnezia::ErrorCode::NoError;
|
||||
}
|
||||
const int st = jsonObj.value(QStringLiteral("http_status")).toInt(-1);
|
||||
switch (st) {
|
||||
case 200: return amnezia::ErrorCode::NoError;
|
||||
case 400: return amnezia::ErrorCode::ApiConfigEmptyError;
|
||||
case 403: return amnezia::ErrorCode::ApiPairingForbiddenError;
|
||||
case 404: return amnezia::ErrorCode::ApiNotFoundError;
|
||||
case 408: return amnezia::ErrorCode::ApiConfigTimeoutError;
|
||||
case 409: return amnezia::ErrorCode::ApiPairingConflictError;
|
||||
case 429: return amnezia::ErrorCode::ApiPairingRateLimitedError;
|
||||
case 500: return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||
case 503: return amnezia::ErrorCode::ApiPairingServiceUnavailableError;
|
||||
default: return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||
}
|
||||
}
|
||||
|
||||
amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &sslErrors, const QString &replyErrorString,
|
||||
const QNetworkReply::NetworkError &replyError, const int httpStatusCode,
|
||||
const QByteArray &responseBody)
|
||||
{
|
||||
const int httpStatusCodeConflict = 409;
|
||||
const int httpStatusCodeNotFound = 404;
|
||||
@@ -124,60 +143,91 @@ amnezia::ErrorCode apiUtils::checkApiResponseErrors(const QByteArray &responseBo
|
||||
const int httpStatusCodeRequestTimeout = 408;
|
||||
const int httpStatusCodeUnprocessableEntity = 422;
|
||||
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
|
||||
if (!jsonDoc.isObject()) {
|
||||
return amnezia::ErrorCode::NoError;
|
||||
if (!sslErrors.empty()) {
|
||||
qDebug().noquote() << sslErrors;
|
||||
return amnezia::ErrorCode::ApiConfigSslError;
|
||||
}
|
||||
|
||||
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) {
|
||||
if (replyError == QNetworkReply::NetworkError::OperationCanceledError
|
||||
|| replyError == QNetworkReply::NetworkError::TimeoutError) {
|
||||
qDebug() << replyError;
|
||||
return amnezia::ErrorCode::ApiConfigTimeoutError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodeNotImplemented) {
|
||||
if (replyError == QNetworkReply::NetworkError::OperationNotImplementedError) {
|
||||
qDebug() << replyError;
|
||||
return amnezia::ErrorCode::ApiUpdateRequestError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodeUnprocessableEntity) {
|
||||
if (apiErrorMessageFromJson(jsonObj) == unprocessableSubscriptionMessage) {
|
||||
return amnezia::ErrorCode::ApiSubscriptionExpiredError;
|
||||
|
||||
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;
|
||||
}
|
||||
return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodePaymentRequired) {
|
||||
const QString message = apiErrorMessageFromJson(jsonObj);
|
||||
if (message.contains(refreshCaptchaMessage, Qt::CaseInsensitive)) {
|
||||
return amnezia::ErrorCode::ApiCaptchaRefreshError;
|
||||
if (httpStatusFromBody == httpStatusCodeConflict) {
|
||||
if (apiErrorMessageFromJson(jsonObj).contains(trialAlreadyUsedMessage, Qt::CaseInsensitive)) {
|
||||
return amnezia::ErrorCode::ApiTrialAlreadyUsedError;
|
||||
}
|
||||
return amnezia::ErrorCode::ApiConfigLimitError;
|
||||
}
|
||||
if (message.contains(invalidCaptchaMessage, Qt::CaseInsensitive)) {
|
||||
return amnezia::ErrorCode::ApiCaptchaInvalidError;
|
||||
if (httpStatusFromBody == httpStatusCodeNotFound) {
|
||||
return amnezia::ErrorCode::ApiNotFoundError;
|
||||
}
|
||||
if (jsonObj.contains(apiDefs::key::captchaId) || jsonObj.contains(apiDefs::key::captchaImage)
|
||||
|| message.contains(rateLimitExceededMessage, Qt::CaseInsensitive)) {
|
||||
return amnezia::ErrorCode::ApiCaptchaRequiredError;
|
||||
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;
|
||||
}
|
||||
|
||||
const QString msg = apiErrorMessageFromJson(jsonObj);
|
||||
if (msg.contains(QStringLiteral("QR session"), Qt::CaseInsensitive)
|
||||
&& (msg.contains(QStringLiteral("not found"), Qt::CaseInsensitive)
|
||||
|| msg.contains(QStringLiteral("expired"), Qt::CaseInsensitive))) {
|
||||
return amnezia::ErrorCode::ApiPairingSessionExpiredError;
|
||||
}
|
||||
if (msg.contains(QStringLiteral("not found"), Qt::CaseInsensitive)
|
||||
|| msg.contains(QStringLiteral("expired"), Qt::CaseInsensitive)) {
|
||||
return amnezia::ErrorCode::ApiNotFoundError;
|
||||
}
|
||||
|
||||
if (httpStatusFromBody >= 300) {
|
||||
return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||
}
|
||||
return amnezia::ErrorCode::ApiSubscriptionNotActiveError;
|
||||
}
|
||||
|
||||
if (httpStatusFromBody >= 300) {
|
||||
return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||
if (replyError == QNetworkReply::NoError) {
|
||||
return amnezia::ErrorCode::NoError;
|
||||
}
|
||||
return amnezia::ErrorCode::NoError;
|
||||
|
||||
if (httpStatusCode == httpStatusCodeNotFound) {
|
||||
return amnezia::ErrorCode::ApiNotFoundError;
|
||||
}
|
||||
|
||||
qDebug() << "something went wrong";
|
||||
return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||
}
|
||||
|
||||
bool apiUtils::isPremiumServer(const QJsonObject &serverConfigObject)
|
||||
|
||||
@@ -29,7 +29,11 @@ namespace apiUtils
|
||||
|
||||
bool isPremiumServer(const QJsonObject &serverConfigObject);
|
||||
|
||||
amnezia::ErrorCode checkApiResponseErrors(const QByteArray &responseBody);
|
||||
amnezia::ErrorCode checkNetworkReplyErrors(const QList<QSslError> &sslErrors, const QString &replyErrorString,
|
||||
const QNetworkReply::NetworkError &replyError, const int httpStatusCode,
|
||||
const QByteArray &responseBody);
|
||||
|
||||
amnezia::ErrorCode errorCodeFromGatewayJsonHttpStatus(const QJsonObject &jsonObj);
|
||||
|
||||
QString getPremiumV2VpnKey(const QJsonObject &serverConfigObject);
|
||||
}
|
||||
|
||||
@@ -1,308 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
#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
|
||||
@@ -4,10 +4,7 @@
|
||||
namespace apiDefs
|
||||
{
|
||||
|
||||
constexpr int requestTimeoutMsecs = 6 * 1000;
|
||||
constexpr int proxyRequestTimeoutMsecs = 6 * 1000;
|
||||
constexpr int proxyStorageTimeoutMsecs = 6 * 1000;
|
||||
constexpr int proxyHealthTimeoutMsecs = 1000;
|
||||
constexpr int requestTimeoutMsecs = 12 * 1000; // 12 secs
|
||||
|
||||
} // namespace apiDefs
|
||||
|
||||
|
||||
@@ -18,10 +18,12 @@ namespace apiDefs
|
||||
constexpr QLatin1String stackType("stack_type");
|
||||
constexpr QLatin1String cliVersion("cli_version");
|
||||
constexpr QLatin1String cliName("cli_name");
|
||||
constexpr QLatin1String supportedProtocols("supported_protocols");
|
||||
constexpr QLatin1String availableCountries("available_countries");
|
||||
constexpr QLatin1String availableProtocols("available_protocols");
|
||||
constexpr QLatin1String installationUuid("installation_uuid");
|
||||
constexpr QLatin1String uuid("installation_uuid");
|
||||
constexpr QLatin1String qrUuid("qr_uuid");
|
||||
constexpr QLatin1String osVersion("os_version");
|
||||
constexpr QLatin1String distribution("distribution");
|
||||
constexpr QLatin1String userCountryCode("user_country_code");
|
||||
@@ -45,8 +47,6 @@ 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,8 +13,6 @@ 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");
|
||||
@@ -180,8 +178,6 @@ 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
|
||||
@@ -220,11 +216,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 mkcpCwndMultiplier("mkcp_cwnd_multiplier");
|
||||
constexpr QLatin1String mkcpMaxSendingWindow("mkcp_max_sending_window");
|
||||
constexpr QLatin1String mkcpReadBufferSize("mkcp_read_buffer_size");
|
||||
constexpr QLatin1String mkcpWriteBufferSize("mkcp_write_buffer_size");
|
||||
constexpr QLatin1String mkcpCongestion("mkcp_congestion"); // bool
|
||||
|
||||
// xPadding
|
||||
constexpr QLatin1String xPaddingBytesMin("xpadding_bytes_min");
|
||||
|
||||
@@ -48,46 +48,26 @@ 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 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 defaultSite[] = "www.googletagmanager.com";
|
||||
|
||||
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[] = "firefox";
|
||||
constexpr char defaultFingerprint[] = "chrome";
|
||||
constexpr char defaultSni[] = "www.googletagmanager.com";
|
||||
constexpr char defaultAlpn[] = "h2";
|
||||
|
||||
constexpr char defaultXhttpMode[] = "Stream-one";
|
||||
constexpr char defaultXhttpMode[] = "Auto";
|
||||
constexpr char defaultXhttpUplinkMethod[] = "POST";
|
||||
constexpr char defaultXhttpSessionPlacement[] = "Path";
|
||||
constexpr char defaultXhttpSessionKey[] = "";
|
||||
@@ -95,10 +75,9 @@ namespace amnezia
|
||||
constexpr char defaultXhttpUplinkDataPlacement[] = "Body";
|
||||
|
||||
constexpr char defaultXhttpHost[] = "www.googletagmanager.com";
|
||||
constexpr char defaultXhttpPath[] = "/";
|
||||
constexpr char defaultXhttpUplinkChunkSize[] = "0";
|
||||
constexpr char defaultXhttpScMaxEachPostBytesMin[] = "1000000";
|
||||
constexpr char defaultXhttpScMaxEachPostBytesMax[] = "1000000";
|
||||
constexpr char defaultXhttpScMaxEachPostBytesMin[] = "1";
|
||||
constexpr char defaultXhttpScMaxEachPostBytesMax[] = "100";
|
||||
constexpr char defaultXhttpScMinPostsIntervalMsMin[] = "100";
|
||||
constexpr char defaultXhttpScMinPostsIntervalMsMax[] = "800";
|
||||
constexpr char defaultXhttpScStreamUpServerSecsMin[] = "1";
|
||||
@@ -108,22 +87,19 @@ namespace amnezia
|
||||
constexpr char defaultXPaddingMethod[] = "Repeat-x";
|
||||
constexpr char defaultXPaddingKey[] = "x_padding";
|
||||
constexpr char defaultXPaddingHeader[] = "X-Padding";
|
||||
constexpr char defaultXPaddingBytesMin[] = "100";
|
||||
constexpr char defaultXPaddingBytesMax[] = "1000";
|
||||
constexpr char defaultXPaddingBytesMin[] = "1";
|
||||
constexpr char defaultXPaddingBytesMax[] = "256";
|
||||
|
||||
constexpr char defaultMkcpTti[] = "50";
|
||||
constexpr char defaultMkcpUplinkCapacity[] = "5";
|
||||
constexpr char defaultMkcpDownlinkCapacity[] = "20";
|
||||
constexpr char defaultMkcpMtu[] = "1350";
|
||||
constexpr char defaultMkcpCwndMultiplier[] = "1";
|
||||
constexpr char defaultMkcpReadBufferSize[] = "2";
|
||||
constexpr char defaultMkcpWriteBufferSize[] = "2";
|
||||
|
||||
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";
|
||||
@@ -144,92 +120,6 @@ 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,17 +37,9 @@ namespace amnezia
|
||||
ServerLinuxKernelTooOld = 214,
|
||||
XrayServerConfigInvalid = 215,
|
||||
XrayServerNoVlessClients = 216,
|
||||
XrayKeysReadFailed = 217,
|
||||
XrayRealityKeysReadFailed = 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,
|
||||
@@ -92,7 +84,6 @@ namespace amnezia
|
||||
LegacyContainerNotSupportedError = 906,
|
||||
ConfigFormatVersionNotSupportedError = 907,
|
||||
RestoreBackupUnsupportedConfigsSkipped = 908,
|
||||
ImportXraySettingsFileUseXrayPage = 909,
|
||||
|
||||
// Android errors
|
||||
AndroidError = 1000,
|
||||
@@ -114,13 +105,26 @@ namespace amnezia
|
||||
ApiSubscriptionNotActiveError = 1114,
|
||||
ApiNoPurchasedSubscriptionsError = 1115,
|
||||
ApiTrialAlreadyUsedError = 1116,
|
||||
|
||||
// Captcha errors (gateway 402 payment_required with captcha markers)
|
||||
ApiCaptchaRequiredError = 1117,
|
||||
ApiCaptchaInvalidError = 1118,
|
||||
ApiCaptchaRefreshError = 1119,
|
||||
|
||||
// Rate limiting (HTTP 429 Too Many Requests)
|
||||
ApiRateLimitError = 1120,
|
||||
ApiNoPurchasesToRestore = 1121,
|
||||
ApiPurchasePendingError = 1122,
|
||||
|
||||
// QR pairing (gateway /v1/generate_qr, /v1/scan_qr)
|
||||
ApiPairingForbiddenError = 1123,
|
||||
ApiPairingConflictError = 1124,
|
||||
ApiPairingRateLimitedError = 1125,
|
||||
ApiPairingServiceUnavailableError = 1126,
|
||||
ApiPairingPayloadTooLargeError = 1127,
|
||||
ApiPairingMissingMetadataError = 1128,
|
||||
ApiPairingSessionExpiredError = 1129,
|
||||
|
||||
// QFile errors
|
||||
OpenError = 1200,
|
||||
ReadError = 1201,
|
||||
|
||||
@@ -36,45 +36,9 @@ QString errorString(ErrorCode code) {
|
||||
case(ErrorCode::XrayServerNoVlessClients):
|
||||
errorMessage = QObject::tr("Server error: XRay server has no VLESS clients");
|
||||
break;
|
||||
case(ErrorCode::XrayKeysReadFailed):
|
||||
case(ErrorCode::XrayRealityKeysReadFailed):
|
||||
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;
|
||||
|
||||
@@ -105,7 +69,6 @@ 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;
|
||||
@@ -156,6 +119,13 @@ QString errorString(ErrorCode code) {
|
||||
errorMessage = QObject::tr("No purchases to restore. If you have an active subscription, make sure you're signed in with the same account used for the purchase.");
|
||||
#endif
|
||||
break;
|
||||
case (ErrorCode::ApiPairingForbiddenError): errorMessage = QObject::tr("QR pairing was rejected (forbidden)"); break;
|
||||
case (ErrorCode::ApiPairingConflictError): errorMessage = QObject::tr("This QR code has already been used"); break;
|
||||
case (ErrorCode::ApiPairingRateLimitedError): errorMessage = QObject::tr("Too many requests. Please try again later"); break;
|
||||
case (ErrorCode::ApiPairingServiceUnavailableError): errorMessage = QObject::tr("Service temporarily unavailable. Please try again later"); break;
|
||||
case (ErrorCode::ApiPairingPayloadTooLargeError): errorMessage = QObject::tr("QR pairing data is too large to send"); break;
|
||||
case (ErrorCode::ApiPairingMissingMetadataError): errorMessage = QObject::tr("This subscription is missing data required to transfer via QR (service type or country). Refresh the subscription or pick another server."); break;
|
||||
case (ErrorCode::ApiPairingSessionExpiredError): errorMessage = QObject::tr("The QR code session has ended. Show a new QR code on the other device and scan again."); break;
|
||||
|
||||
// QFile errors
|
||||
case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break;
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
#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
|
||||
@@ -1,51 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
#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
|
||||
@@ -1,47 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
#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
|
||||
Loaded 100 of 532 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user