Compare commits

..
535 changed files with 45405 additions and 87046 deletions

No files matched your search

+113 -228
View File
@@ -5,8 +5,38 @@ on:
branches:
- '**'
# Self-hosted server tests (SelfHostedServerSetup, MultipleExports) install and
# remove docker containers on a single shared VPS: two workflow runs executing
# simultaneously corrupt each other's state (error 202, "No such container").
# Queue runs instead of running them in parallel; do not cancel the active one.
concurrency:
group: vps-tests
cancel-in-progress: false
env:
QT_MIRROR: https://mirrors.ocf.berkeley.edu/qt/ # https://download.qt.io/static/mirrorlist/
QTEST_FUNCTION_TIMEOUT: 7200000
SELF_HOSTED_ADMIN_EXPORT_ROUND_TRIP_VPN_KEY: ${{ secrets.SELF_HOSTED_ADMIN_EXPORT_ROUND_TRIP_VPN_KEY }}
THIRD_PARTY_AWG_VPN_KEY: ${{ secrets.THIRD_PARTY_AWG_VPN_KEY }}
THIRD_PARTY_WIRE_GUARD_VPN_KEY: ${{ secrets.THIRD_PARTY_WIRE_GUARD_VPN_KEY }}
THIRD_PARTY_XRAY_VPN_KEY: ${{ secrets.THIRD_PARTY_XRAY_VPN_KEY }}
THIRD_PARTY_OPEN_VPN_KEY: ${{ secrets.THIRD_PARTY_OPEN_VPN_KEY }}
THIRD_PARTY_CLOAK_VPN_KEY: ${{ secrets.THIRD_PARTY_CLOAK_VPN_KEY }}
THIRD_PARTY_SS_VPN_KEY: ${{ secrets.THIRD_PARTY_SS_VPN_KEY }}
THIRD_PARTY_IKEV_VPN_KEY: ${{ secrets.THIRD_PARTY_IKEV_VPN_KEY }}
THIRD_PARTY_PREMIUM_VPN_KEY: ${{ secrets.THIRD_PARTY_PREMIUM_VPN_KEY }}
THIRD_PARTY_VMESS_NEW_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VMESS_NEW_IMPORT_DATA }}
THIRD_PARTY_VMESS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VMESS_IMPORT_DATA }}
THIRD_PARTY_TROJAN_IMPORT_DATA: ${{ secrets.THIRD_PARTY_TROJAN_IMPORT_DATA }}
THIRD_PARTY_VLESS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VLESS_IMPORT_DATA }}
THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA }}
THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA }}
SELF_HOSTED_SERVER_HOST_NAME: ${{ secrets.SELF_HOSTED_SERVER_HOST_NAME }}
SELF_HOSTED_SERVER_USER_NAME: ${{ secrets.SELF_HOSTED_SERVER_USER_NAME }}
SELF_HOSTED_SERVER_PASSWORD: ${{ secrets.SELF_HOSTED_SERVER_PASSWORD }}
SELF_HOSTED_SERVER_SSH_PORT: ${{ secrets.SELF_HOSTED_SERVER_SSH_PORT }}
DNS_LIST_PATH: ${{ github.workspace }}/tests/data/dns_list.json
IP_LIST_PATH: ${{ github.workspace }}/tests/data/ip_list.json
jobs:
Detect-Changes:
@@ -88,7 +118,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'
@@ -118,7 +148,6 @@ jobs:
shell: bash
env:
QT_INSTALL_DIR: ${{ runner.temp }}
AMNEZIA_BUILD_TESTS: ON
run: deploy/build.sh --generator Ninja --installer all
- name: 'Upload Linux test build'
@@ -209,7 +238,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'
@@ -250,7 +279,6 @@ jobs:
shell: cmd
env:
QT_INSTALL_DIR: ${{ runner.temp }}
AMNEZIA_BUILD_TESTS: ON
run: |
set WIX_ROOT_PATH="${{ env.USERPROFILE }}\.dotnet\tools"
deploy\build.bat --installer all
@@ -324,7 +352,7 @@ jobs:
# ------------------------------------------------------
Build-iOS:
runs-on: macos-26
runs-on: macos-latest
needs: Bake-Prebuilts-iOS
if: ${{ always() }}
@@ -472,7 +500,7 @@ jobs:
# ------------------------------------------------------
Build-MacOS:
runs-on: macos-26
runs-on: macos-15
needs: Bake-Prebuilts-MacOS
if: ${{ always() }}
@@ -522,13 +550,13 @@ jobs:
xcode-version: '26.3.0'
- name: 'Install Qt'
uses: jurplel/install-qt-action@v4.3.1
uses: jurplel/install-qt-action@v4
with:
version: ${{ env.QT_VERSION }}
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'
@@ -554,10 +582,29 @@ jobs:
NOTARYTOOL_TEAM_ID: ${{ secrets.MAC_TEAM_ID }}
NOTARYTOOL_EMAIL: ${{ secrets.APPLE_DEV_EMAIL }}
NOTARYTOOL_PASSWORD: ${{ secrets.APPLE_DEV_PASSWORD }}
AMNEZIA_BUILD_TESTS: ON
shell: bash
run: deploy/build.sh --generator Ninja --installer all
- name: 'Prepare macOS test runtime'
shell: bash
run: |
FRAMEWORKS_DIR=$(find deploy/build/_CPack_Packages \
-type d \
-path '*/AmneziaVPN.app/Contents/Frameworks' \
-print -quit)
echo "Frameworks directory: $FRAMEWORKS_DIR"
mkdir -p deploy/build/client/tests/openssl
cp "$FRAMEWORKS_DIR/libssl.3.dylib" \
deploy/build/client/tests/openssl/
cp "$FRAMEWORKS_DIR/libcrypto.3.dylib" \
deploy/build/client/tests/openssl/
ls -la deploy/build/client/tests/openssl
- name: 'Upload macOS test build'
uses: actions/upload-artifact@v7
with:
@@ -671,7 +718,7 @@ 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'
@@ -818,7 +865,7 @@ jobs:
submodules: 'true'
- name: 'Install desktop Qt'
uses: jurplel/install-qt-action@v4.3.1
uses: jurplel/install-qt-action@v4
with:
version: ${{ env.QT_VERSION }}
host: 'linux'
@@ -830,10 +877,10 @@ jobs:
extra: '--base ${{ env.QT_MIRROR }}'
- name: 'Install android_x86_64 Qt'
uses: jurplel/install-qt-action@v4.3.1
uses: jurplel/install-qt-action@v4
with:
version: ${{ env.QT_VERSION }}
host: 'all_os'
host: 'linux'
target: 'android'
arch: 'android_x86_64'
modules: ${{ env.QT_MODULES }}
@@ -842,10 +889,10 @@ jobs:
extra: '--base ${{ env.QT_MIRROR }}'
- name: 'Install android_x86 Qt'
uses: jurplel/install-qt-action@v4.3.1
uses: jurplel/install-qt-action@v4
with:
version: ${{ env.QT_VERSION }}
host: 'all_os'
host: 'linux'
target: 'android'
arch: 'android_x86'
modules: ${{ env.QT_MODULES }}
@@ -854,10 +901,10 @@ jobs:
extra: '--base ${{ env.QT_MIRROR }}'
- name: 'Install android_armv7 Qt'
uses: jurplel/install-qt-action@v4.3.1
uses: jurplel/install-qt-action@v4
with:
version: ${{ env.QT_VERSION }}
host: 'all_os'
host: 'linux'
target: 'android'
arch: 'android_armv7'
modules: ${{ env.QT_MODULES }}
@@ -866,10 +913,10 @@ jobs:
extra: '--base ${{ env.QT_MIRROR }}'
- name: 'Install android_arm64_v8a Qt'
uses: jurplel/install-qt-action@v4.3.1
uses: jurplel/install-qt-action@v4
with:
version: ${{ env.QT_VERSION }}
host: 'all_os'
host: 'linux'
target: 'android'
arch: 'android_arm64_v8a'
modules: ${{ env.QT_MODULES }}
@@ -1049,39 +1096,18 @@ jobs:
- SettingsSignalsTest
- UiServersModelAndControllerTest
- XraySerializationTest
- XrayServerInboundTest
- XrayGeneratorMatrixTest
- XrayConfigModelTest
- XrayCoreAcceptsTest
- UiLanguageModelAndControllerTest
- UiIpModelAndControllerTest
- UiAppSTModelAndControllerTest
- UiAllowedDnsModelAndControllerTest
- BackupExportImportTest
- FavoriteLocationsRestoreTest
- ConnectionTest
- OpenSSLLibraryTest
- GatewayServiceImportTest
runs-on: ubuntu-22.04
env:
QT_VERSION: 6.10.1
QTEST_FUNCTION_TIMEOUT: 7200000
THIRD_PARTY_AWG_VPN_KEY: ${{ secrets.THIRD_PARTY_AWG_VPN_KEY }}
THIRD_PARTY_WIRE_GUARD_VPN_KEY: ${{ secrets.THIRD_PARTY_WIRE_GUARD_VPN_KEY }}
THIRD_PARTY_XRAY_VPN_KEY: ${{ secrets.THIRD_PARTY_XRAY_VPN_KEY }}
THIRD_PARTY_OPEN_VPN_KEY: ${{ secrets.THIRD_PARTY_OPEN_VPN_KEY }}
THIRD_PARTY_CLOAK_VPN_KEY: ${{ secrets.THIRD_PARTY_CLOAK_VPN_KEY }}
THIRD_PARTY_SS_VPN_KEY: ${{ secrets.THIRD_PARTY_SS_VPN_KEY }}
THIRD_PARTY_IKEV_VPN_KEY: ${{ secrets.THIRD_PARTY_IKEV_VPN_KEY }}
THIRD_PARTY_PREMIUM_VPN_KEY: ${{ secrets.THIRD_PARTY_PREMIUM_VPN_KEY }}
THIRD_PARTY_VMESS_NEW_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VMESS_NEW_IMPORT_DATA }}
THIRD_PARTY_VMESS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VMESS_IMPORT_DATA }}
THIRD_PARTY_TROJAN_IMPORT_DATA: ${{ secrets.THIRD_PARTY_TROJAN_IMPORT_DATA }}
THIRD_PARTY_VLESS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VLESS_IMPORT_DATA }}
THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA }}
THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA }}
steps:
- name: Checkout
@@ -1092,16 +1118,12 @@ jobs:
- name: Decode backup file
if: matrix.filter == 'BackupExportImportTest'
shell: bash
env:
BACKUP_FILE_BASE64: ${{ secrets.BACKUP_FILE_BASE64 }}
shell: pwsh
run: |
if [ -z "$BACKUP_FILE_BASE64" ]; then
echo "::error::BACKUP_FILE_BASE64 secret is not set"
exit 1
fi
echo "$BACKUP_FILE_BASE64" | base64 --decode > "$RUNNER_TEMP/test.backup"
echo "BACKUP_PATH=$RUNNER_TEMP/test.backup" >> "$GITHUB_ENV"
$backup = Join-Path $env:RUNNER_TEMP "test.backup"
$bytes = [Convert]::FromBase64String("${{ secrets.BACKUP_FILE_BASE64 }}")
[IO.File]::WriteAllBytes($backup, $bytes)
"BACKUP_PATH=$backup" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
- name: Install Qt
uses: jurplel/install-qt-action@v3
@@ -1110,7 +1132,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'
@@ -1133,118 +1155,20 @@ jobs:
- name: Restore executable permissions
run: |
find deploy/build/client/tests \
-maxdepth 1 \
-type f \
-name "test_*" \
-exec chmod +x {} \;
- name: Download Xray core
if: matrix.filter == 'XrayCoreAcceptsTest'
shell: bash
run: |
XRAY_RELEASE=$(sed -n 's/^ARG XRAY_RELEASE="\(.*\)"/\1/p' client/server_scripts/xray/Dockerfile)
curl -fsSL -o "$RUNNER_TEMP/xray.zip" \
"https://github.com/XTLS/Xray-core/releases/download/${XRAY_RELEASE}/Xray-linux-64.zip"
unzip -q -o "$RUNNER_TEMP/xray.zip" xray -d "$RUNNER_TEMP/xray"
chmod +x "$RUNNER_TEMP/xray/xray"
echo "XRAY_BIN=$RUNNER_TEMP/xray/xray" >> "$GITHUB_ENV"
- name: Run tests
env:
AMNEZIA_TEST_API_KEY: ${{ secrets.AMNEZIA_TEST_API_KEY }}
shell: bash
run: |
QT_ROOT="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/gcc_64"
export LD_LIBRARY_PATH="$PWD/deploy/build/client/tests:$QT_ROOT/lib:${LD_LIBRARY_PATH:-}"
export QT_QPA_PLATFORM=offscreen
ctest --test-dir deploy/build/client/tests --output-on-failure -VV --timeout 600 -R "^${{ matrix.filter }}$"
# ------------------------------------------------------
test-vps:
name: Test self-hosted VPS
needs: Build-Linux-Ubuntu
# These tests install and remove docker containers on a single shared VPS:
# two runs at once corrupt each other's state, so only one job may touch it.
concurrency:
group: vps-tests
cancel-in-progress: false
runs-on: ubuntu-22.04
env:
QT_VERSION: 6.10.1
QTEST_FUNCTION_TIMEOUT: 7200000
SELF_HOSTED_SERVER_HOST_NAME: ${{ secrets.SELF_HOSTED_SERVER_HOST_NAME }}
SELF_HOSTED_SERVER_USER_NAME: ${{ secrets.SELF_HOSTED_SERVER_USER_NAME }}
SELF_HOSTED_SERVER_PASSWORD: ${{ secrets.SELF_HOSTED_SERVER_PASSWORD }}
SELF_HOSTED_SERVER_SSH_PORT: ${{ secrets.SELF_HOSTED_SERVER_SSH_PORT }}
SELF_HOSTED_ADMIN_EXPORT_ROUND_TRIP_VPN_KEY: ${{ secrets.SELF_HOSTED_ADMIN_EXPORT_ROUND_TRIP_VPN_KEY }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: 'true'
fetch-depth: 10
- name: Install Qt
uses: jurplel/install-qt-action@v3
with:
version: ${{ env.QT_VERSION }}
host: linux
target: desktop
arch: linux_gcc_64
modules: qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets
dir: ${{ runner.temp }}
set-env: true
aqtversion: '==3.3.0'
py7zrversion: '==0.22.*'
extra: '--base ${{ env.QT_MIRROR }}'
- name: Install system packages
run: |
sudo apt-get update
sudo apt-get install -y \
libxkbcommon-x11-0 \
libsecret-1-dev
- name: Download build
uses: actions/download-artifact@v4
with:
name: linux-build
path: deploy/build/client/tests
- name: Restore executable permissions
run: |
find deploy/build/client/tests \
-type f \
-name "test_*" \
-exec chmod +x {} \;
- name: Download Xray core
shell: bash
run: |
XRAY_RELEASE=$(sed -n 's/^ARG XRAY_RELEASE="\(.*\)"/\1/p' client/server_scripts/xray/Dockerfile)
curl -fsSL -o "$RUNNER_TEMP/xray.zip" \
"https://github.com/XTLS/Xray-core/releases/download/${XRAY_RELEASE}/Xray-linux-64.zip"
unzip -q -o "$RUNNER_TEMP/xray.zip" xray -d "$RUNNER_TEMP/xray"
chmod +x "$RUNNER_TEMP/xray/xray"
echo "XRAY_BIN=$RUNNER_TEMP/xray/xray" >> "$GITHUB_ENV"
- name: Run tests
shell: bash
run: |
QT_ROOT="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/gcc_64"
export LD_LIBRARY_PATH="$PWD/deploy/build/client/tests:$QT_ROOT/lib:${LD_LIBRARY_PATH:-}"
export LD_LIBRARY_PATH="$QT_ROOT/lib:${LD_LIBRARY_PATH:-}"
export QT_QPA_PLATFORM=offscreen
ctest --test-dir deploy/build/client/tests --output-on-failure -VV \
--timeout 3600 \
-R "^(SelfHostedServerSetupTest|MultipleExportsTest|AdminSelfHostedExportTest|XrayVpsMatrixTest)$"
ctest --test-dir deploy/build/client/tests --output-on-failure -VV -R "^${{ matrix.filter }}$"
# ------------------------------------------------------
@@ -1266,39 +1190,18 @@ jobs:
- SettingsSignalsTest
- UiServersModelAndControllerTest
- XraySerializationTest
- XrayServerInboundTest
- XrayGeneratorMatrixTest
- XrayConfigModelTest
- XrayCoreAcceptsTest
- UiLanguageModelAndControllerTest
- UiIpModelAndControllerTest
- UiAppSTModelAndControllerTest
- UiAllowedDnsModelAndControllerTest
- BackupExportImportTest
- FavoriteLocationsRestoreTest
- ConnectionTest
- OpenSSLLibraryTest
- GatewayServiceImportTest
runs-on: windows-latest
env:
QT_VERSION: 6.10.1
QTEST_FUNCTION_TIMEOUT: 7200000
THIRD_PARTY_AWG_VPN_KEY: ${{ secrets.THIRD_PARTY_AWG_VPN_KEY }}
THIRD_PARTY_WIRE_GUARD_VPN_KEY: ${{ secrets.THIRD_PARTY_WIRE_GUARD_VPN_KEY }}
THIRD_PARTY_XRAY_VPN_KEY: ${{ secrets.THIRD_PARTY_XRAY_VPN_KEY }}
THIRD_PARTY_OPEN_VPN_KEY: ${{ secrets.THIRD_PARTY_OPEN_VPN_KEY }}
THIRD_PARTY_CLOAK_VPN_KEY: ${{ secrets.THIRD_PARTY_CLOAK_VPN_KEY }}
THIRD_PARTY_SS_VPN_KEY: ${{ secrets.THIRD_PARTY_SS_VPN_KEY }}
THIRD_PARTY_IKEV_VPN_KEY: ${{ secrets.THIRD_PARTY_IKEV_VPN_KEY }}
THIRD_PARTY_PREMIUM_VPN_KEY: ${{ secrets.THIRD_PARTY_PREMIUM_VPN_KEY }}
THIRD_PARTY_VMESS_NEW_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VMESS_NEW_IMPORT_DATA }}
THIRD_PARTY_VMESS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VMESS_IMPORT_DATA }}
THIRD_PARTY_TROJAN_IMPORT_DATA: ${{ secrets.THIRD_PARTY_TROJAN_IMPORT_DATA }}
THIRD_PARTY_VLESS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VLESS_IMPORT_DATA }}
THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA }}
THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA }}
steps:
- name: Checkout
@@ -1310,15 +1213,9 @@ jobs:
- name: Decode backup file
if: matrix.filter == 'BackupExportImportTest'
shell: pwsh
env:
BACKUP_FILE_BASE64: ${{ secrets.BACKUP_FILE_BASE64 }}
run: |
if ([string]::IsNullOrWhiteSpace($env:BACKUP_FILE_BASE64)) {
Write-Output "::error::BACKUP_FILE_BASE64 secret is not set"
exit 1
}
$backup = Join-Path $env:RUNNER_TEMP "test.backup"
$bytes = [Convert]::FromBase64String($env:BACKUP_FILE_BASE64)
$bytes = [Convert]::FromBase64String("${{ secrets.BACKUP_FILE_BASE64 }}")
[IO.File]::WriteAllBytes($backup, $bytes)
"BACKUP_PATH=$backup" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
@@ -1335,7 +1232,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
@@ -1344,20 +1241,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
@@ -1367,7 +1251,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
# ------------------------------------------------------
@@ -1394,30 +1278,13 @@ jobs:
- UiAppSTModelAndControllerTest
- UiAllowedDnsModelAndControllerTest
- BackupExportImportTest
- FavoriteLocationsRestoreTest
- ConnectionTest
- OpenSSLLibraryTest
- GatewayServiceImportTest
runs-on: macos-15
env:
QT_VERSION: 6.10.1
QTEST_FUNCTION_TIMEOUT: 7200000
THIRD_PARTY_AWG_VPN_KEY: ${{ secrets.THIRD_PARTY_AWG_VPN_KEY }}
THIRD_PARTY_WIRE_GUARD_VPN_KEY: ${{ secrets.THIRD_PARTY_WIRE_GUARD_VPN_KEY }}
THIRD_PARTY_XRAY_VPN_KEY: ${{ secrets.THIRD_PARTY_XRAY_VPN_KEY }}
THIRD_PARTY_OPEN_VPN_KEY: ${{ secrets.THIRD_PARTY_OPEN_VPN_KEY }}
THIRD_PARTY_CLOAK_VPN_KEY: ${{ secrets.THIRD_PARTY_CLOAK_VPN_KEY }}
THIRD_PARTY_SS_VPN_KEY: ${{ secrets.THIRD_PARTY_SS_VPN_KEY }}
THIRD_PARTY_IKEV_VPN_KEY: ${{ secrets.THIRD_PARTY_IKEV_VPN_KEY }}
THIRD_PARTY_PREMIUM_VPN_KEY: ${{ secrets.THIRD_PARTY_PREMIUM_VPN_KEY }}
THIRD_PARTY_VMESS_NEW_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VMESS_NEW_IMPORT_DATA }}
THIRD_PARTY_VMESS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VMESS_IMPORT_DATA }}
THIRD_PARTY_TROJAN_IMPORT_DATA: ${{ secrets.THIRD_PARTY_TROJAN_IMPORT_DATA }}
THIRD_PARTY_VLESS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VLESS_IMPORT_DATA }}
THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA }}
THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA }}
steps:
- name: Setup Xcode
@@ -1433,16 +1300,12 @@ jobs:
- name: Decode backup file
if: matrix.filter == 'BackupExportImportTest'
shell: bash
env:
BACKUP_FILE_BASE64: ${{ secrets.BACKUP_FILE_BASE64 }}
shell: pwsh
run: |
if [ -z "$BACKUP_FILE_BASE64" ]; then
echo "::error::BACKUP_FILE_BASE64 secret is not set"
exit 1
fi
echo "$BACKUP_FILE_BASE64" | base64 --decode > "$RUNNER_TEMP/test.backup"
echo "BACKUP_PATH=$RUNNER_TEMP/test.backup" >> "$GITHUB_ENV"
$backup = Join-Path $env:RUNNER_TEMP "test.backup"
$bytes = [Convert]::FromBase64String("${{ secrets.BACKUP_FILE_BASE64 }}")
[IO.File]::WriteAllBytes($backup, $bytes)
"BACKUP_PATH=$backup" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
- name: Download build
uses: actions/download-artifact@v4
@@ -1457,27 +1320,41 @@ jobs:
host: mac
target: desktop
arch: clang_64
modules: qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets
modules: qtremoteobjects qt5compat qtshadertools
dir: ${{ runner.temp }}
set-env: true
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: "3.14"
- name: Install Conan
run: pip install "conan==2.28.0"
- name: Install dependencies
run: |
brew update
brew install cmake ninja
- name: Restore executable permissions
run: |
find deploy/build/client/tests \
-maxdepth 1 \
-type f \
-name "test_*" \
-exec chmod +x {} \;
- name: Run tests
env:
AMNEZIA_TEST_API_KEY: ${{ secrets.AMNEZIA_TEST_API_KEY }}
shell: bash
run: |
QT_ROOT="$RUNNER_TEMP/Qt/$QT_VERSION/macos"
export DYLD_LIBRARY_PATH="$PWD/deploy/build/client/tests:$QT_ROOT/lib:${DYLD_LIBRARY_PATH:-}"
export DYLD_LIBRARY_PATH="$PWD/deploy/build/client/tests/openssl:$QT_ROOT/lib:$DYLD_LIBRARY_PATH"
ctest --test-dir deploy/build/client/tests -R "${{ matrix.filter }}" --output-on-failure -VV --timeout 600
# export QT_QPA_PLATFORM=offscreen
ctest --test-dir deploy/build/client/tests -R "${{ matrix.filter }}" --output-on-failure
# ------------------------------------------------------
@@ -1519,7 +1396,7 @@ jobs:
uses: jurplel/install-qt-action@v4
with:
version: ${{ env.QT_VERSION }}
host: 'all_os'
host: 'linux'
target: 'android'
arch: 'android_x86_64'
modules: ${{ env.QT_MODULES }}
@@ -1564,8 +1441,7 @@ jobs:
-DANDROID_PLATFORM=${{ env.ANDROID_PLATFORM }} \
-DCMAKE_ANDROID_NDK=$CMAKE_ANDROID_NDK \
-DQT_HOST_PATH=$QT_HOST \
-DANDROID_SDK_ROOT=$ANDROID_HOME \
-DAMNEZIA_BUILD_TESTS=ON
-DANDROID_SDK_ROOT=$ANDROID_HOME
- name: 'Build Android tests'
# env:
@@ -1638,6 +1514,15 @@ jobs:
EOF
chmod +x ci_run_android_tests.sh
- name: 'Verify Android test files'
run: |
echo "Runner script:"
grep 'local apk=' ci_run_android_tests.sh
echo
echo "APKs:"
find build -type f -name '*.apk' -print
- name: 'Run Android tests on emulator'
uses: reactivecircus/android-emulator-runner@v2
with:
-1
View File
@@ -146,4 +146,3 @@ macos-with-sign-build.sh
DeveloperIdApplicationCertificate.p12
DeveloperIdInstallerCertificate.p12
gwenv.sh
-219
View File
@@ -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.
-6
View File
@@ -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 -4
View File
@@ -5,7 +5,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(PROJECT AmneziaVPN)
if(NOT AMNEZIAVPN_VERSION)
set(AMNEZIAVPN_VERSION 5.1.0.6 CACHE STRING "Client app version")
set(AMNEZIAVPN_VERSION 5.0.2.1 CACHE STRING "Client app version")
endif()
set(QT_CREATOR_SKIP_PACKAGE_MANAGER_SETUP ON CACHE BOOL "" FORCE)
@@ -20,8 +20,6 @@ project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
HOMEPAGE_URL "https://amnezia.org/"
)
option(AMNEZIA_BUILD_TESTS "Build test executables" OFF)
# trigger conan to kick off `conan install` globally
find_package(OpenSSL REQUIRED)
if (PREBUILTS_ONLY)
@@ -34,7 +32,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 2190)
set(APP_ANDROID_VERSION_CODE 2161)
if(DEFINED APP_ANDROID_VERSION_CODE_OFFSET)
math(EXPR APP_ANDROID_VERSION_CODE "${APP_ANDROID_VERSION_CODE} + ${APP_ANDROID_VERSION_CODE_OFFSET}")
endif()
+7 -22
View File
@@ -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
@@ -102,9 +92,6 @@ else()
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_hi_IN.ts
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_es_ES.ts
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_ko_KR.ts
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_fr_FR.ts
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_th_TH.ts
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_id_ID.ts
)
endif()
@@ -227,10 +214,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)
@@ -331,6 +314,8 @@ if(OVPN_SCRIPTS)
)
endif()
if(AMNEZIA_BUILD_TESTS)
add_subdirectory(tests)
endif()
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
add_subdirectory(tests)
-7
View File
@@ -129,11 +129,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
@@ -184,9 +179,7 @@ void AmneziaApplication::init()
m_coreController->setQmlRoot();
#if CLIENT_ENABLE_APP_UPDATES
m_coreController->checkForAppUpdates();
#endif
#ifdef Q_OS_WIN //TODO
if (m_parser.isSet(m_optAutostart))
+1 -1
View File
@@ -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,7 +37,6 @@ 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
@@ -45,7 +44,6 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import java.io.File
import java.io.IOException
import java.io.OutputStream
import kotlin.LazyThreadSafetyMode.NONE
import kotlin.coroutines.CoroutineContext
import kotlin.text.RegexOption.IGNORE_CASE
@@ -79,7 +77,6 @@ private const val CHECK_NOTIFICATION_PERMISSION_ACTION_CODE = 4
private const val PREFS_NOTIFICATION_PERMISSION_ASKED = "NOTIFICATION_PERMISSION_ASKED"
private const val OPEN_FILE_AFTER_RESUME_DELAY_MS = 400L
private const val KEY_PENDING_OPEN_FILE_URI = "pending_open_file_uri"
private const val FILE_PROVIDER_AUTHORITY = "org.amnezia.vpn.qtprovider"
class AmneziaActivity : QtActivity() {
@@ -736,69 +733,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()
}
@@ -1015,6 +971,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")
@@ -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)
+48 -138
View File
@@ -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 {
+4 -12
View File
@@ -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)
-51
View File
@@ -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()
-4
View File
@@ -58,7 +58,3 @@ endif()
if(NOT CLIENT_NOTIFICATION_IDENTIFIER)
set(CLIENT_NOTIFICATION_IDENTIFIER "${CLIENT_TS_PREFIX}" CACHE STRING "Local notification request identifier")
endif()
if(NOT DEFINED CLIENT_ENABLE_APP_UPDATES)
set(CLIENT_ENABLE_APP_UPDATES 1)
endif()
+2 -2
View File
@@ -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)
+3 -3
View File
@@ -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)
+3 -7
View File
@@ -127,18 +127,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
)
+4 -36
View File
@@ -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}\"
+5 -16
View File
@@ -122,24 +122,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
)
+58
View File
@@ -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.
+2 -13
View File
@@ -38,15 +38,12 @@ set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/core/installers/mtProxyInstaller.h
${CLIENT_ROOT_DIR}/core/installers/telemtInstaller.h
${CLIENT_ROOT_DIR}/core/installers/tProxyInstaller.h
${CLIENT_ROOT_DIR}/core/installers/whatsAppProxyInstaller.h
${CLIENT_ROOT_DIR}/core/controllers/appSplitTunnelingController.h
${CLIENT_ROOT_DIR}/core/controllers/ipSplitTunnelingController.h
${CLIENT_ROOT_DIR}/core/controllers/allowedDnsController.h
${CLIENT_ROOT_DIR}/core/controllers/selfhosted/exportController.h
${CLIENT_ROOT_DIR}/core/controllers/connectionController.h
${CLIENT_ROOT_DIR}/core/controllers/settingsController.h
${CLIENT_ROOT_DIR}/core/controllers/localProxyController.h
${CLIENT_ROOT_DIR}/core/controllers/logsController.h
${CLIENT_ROOT_DIR}/core/controllers/api/servicesCatalogController.h
${CLIENT_ROOT_DIR}/core/controllers/api/storePurchaseController.h
${CLIENT_ROOT_DIR}/core/controllers/api/subscriptionController.h
@@ -68,12 +65,9 @@ set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/core/utils/serialization/transfer.h
${CLIENT_ROOT_DIR}/../common/logger/logger.h
${CLIENT_ROOT_DIR}/../common/crypto/cryptoUtils.h
${CLIENT_ROOT_DIR}/ui/utils/qmlUtils.h
${CLIENT_ROOT_DIR}/core/utils/api/apiUtils.h
${CLIENT_ROOT_DIR}/core/utils/api/countryCatalog.h
${CLIENT_ROOT_DIR}/core/utils/api/gatewayPayloadBuilder.h
${CLIENT_ROOT_DIR}/core/utils/localProxy/localProxyConstants.h
${CLIENT_ROOT_DIR}/core/utils/localProxy/portUtils.h
${CLIENT_ROOT_DIR}/core/utils/localProxy/xrayConfigUtils.h
${CLIENT_ROOT_DIR}/core/utils/osSignalHandler.h
${CLIENT_ROOT_DIR}/core/utils/swiftBridge.h
${CLIENT_ROOT_DIR}/core/utils/utilities.h
@@ -129,15 +123,12 @@ set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/core/installers/mtProxyInstaller.cpp
${CLIENT_ROOT_DIR}/core/installers/telemtInstaller.cpp
${CLIENT_ROOT_DIR}/core/installers/tProxyInstaller.cpp
${CLIENT_ROOT_DIR}/core/installers/whatsAppProxyInstaller.cpp
${CLIENT_ROOT_DIR}/core/controllers/appSplitTunnelingController.cpp
${CLIENT_ROOT_DIR}/core/controllers/ipSplitTunnelingController.cpp
${CLIENT_ROOT_DIR}/core/controllers/allowedDnsController.cpp
${CLIENT_ROOT_DIR}/core/controllers/selfhosted/exportController.cpp
${CLIENT_ROOT_DIR}/core/controllers/connectionController.cpp
${CLIENT_ROOT_DIR}/core/controllers/settingsController.cpp
${CLIENT_ROOT_DIR}/core/controllers/localProxyController.cpp
${CLIENT_ROOT_DIR}/core/controllers/logsController.cpp
${CLIENT_ROOT_DIR}/core/controllers/api/servicesCatalogController.cpp
${CLIENT_ROOT_DIR}/core/controllers/api/storePurchaseController.cpp
${CLIENT_ROOT_DIR}/core/controllers/api/subscriptionController.cpp
@@ -161,11 +152,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
+468 -367
View File
@@ -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,
+7 -22
View File
@@ -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)
@@ -188,9 +188,6 @@ ErrorCode StorePurchaseController::importServiceFromMarket(const QString &userCo
quint16 crc = qChecksum(QJsonDocument(configObject).toJson());
if (!serverConfigUtils::isConfigFormatVersionSupported(configObject)) {
return ErrorCode::ConfigFormatVersionNotSupportedError;
}
if (configObject.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
return ErrorCode::InternalError;
}
@@ -239,9 +239,6 @@ ErrorCode SubscriptionController::applyImportedServiceConfig(const QString &user
updateApiConfigInJson(serverConfigJson, serviceType, serviceProtocol, userCountryCode, responseBody);
if (!serverConfigUtils::isConfigFormatVersionSupported(serverConfigJson)) {
return ErrorCode::ConfigFormatVersionNotSupportedError;
}
if (serverConfigJson.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
return ErrorCode::InternalError;
}
@@ -294,9 +291,6 @@ ErrorCode SubscriptionController::importTrialFromGateway(const QString &userCoun
}
QJsonObject configObject = QJsonDocument::fromJson(configBytes).object();
if (!serverConfigUtils::isConfigFormatVersionSupported(configObject)) {
return ErrorCode::ConfigFormatVersionNotSupportedError;
}
if (configObject.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
return ErrorCode::InternalError;
}
@@ -307,7 +301,7 @@ ErrorCode SubscriptionController::importTrialFromGateway(const QString &userCoun
return ErrorCode::NoError;
}
ErrorCode SubscriptionController::updateServiceFromGateway(const QString &serverId, const QString &serverCountryCode, bool isConnectEvent,
ErrorCode SubscriptionController::updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, bool isConnectEvent,
CaptchaInfo *captchaInfoOut, ProtocolData *usedProtocolDataOut)
{
auto apiV2 = m_serversRepository->apiV2Config(serverId);
@@ -317,11 +311,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 +331,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 +356,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;
@@ -414,9 +380,6 @@ ErrorCode SubscriptionController::applyUpdatedServiceConfig(const QString &serve
updateApiConfigInJson(serverConfigJson, apiV2->apiConfig.serviceType, serviceProtocol, apiV2->apiConfig.userCountryCode, responseBody);
if (!serverConfigUtils::isConfigFormatVersionSupported(serverConfigJson)) {
return ErrorCode::ConfigFormatVersionNotSupportedError;
}
if (serverConfigJson.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
return ErrorCode::InternalError;
}
@@ -440,7 +403,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 +417,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 +590,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;
@@ -45,12 +45,10 @@ 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 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 +82,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;
+3 -27
View File
@@ -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
@@ -120,9 +116,6 @@ void CoreController::initModels()
m_tProxyConfigModel = new TProxyConfigModel(this);
setQmlContextProperty("TProxyConfigModel", m_tProxyConfigModel);
m_whatsAppProxyConfigModel = new WhatsAppProxyConfigModel(this);
setQmlContextProperty("WhatsAppProxyConfigModel", m_whatsAppProxyConfigModel);
m_clientManagementModel = new ClientManagementModel(this);
setQmlContextProperty("ClientManagementModel", m_clientManagementModel);
@@ -132,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);
@@ -182,7 +169,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()
@@ -192,6 +178,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,
@@ -200,7 +187,7 @@ void CoreController::initControllers()
m_ikev2ConfigModel,
#endif
m_sftpConfigModel, m_socks5ConfigModel, m_mtProxyConfigModel, m_telemtConfigModel,
m_tProxyConfigModel, m_whatsAppProxyConfigModel,
m_tProxyConfigModel,
m_connectionController, this);
setQmlContextProperty("InstallController", m_installUiController);
@@ -216,12 +203,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);
@@ -290,7 +271,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)) {
@@ -299,11 +280,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()
+1 -18
View File
@@ -3,9 +3,7 @@
#include <QObject>
#include <QQmlContext>
#include <QSharedPointer>
#include <QThread>
#include <QTranslator>
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
#include "ui/utils/systemTrayNotificationHandler.h"
@@ -24,8 +22,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 +41,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 +57,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"
@@ -81,7 +75,6 @@
#include "ui/models/services/mtProxyConfigModel.h"
#include "ui/models/services/telemtConfigModel.h"
#include "ui/models/services/tProxyConfigModel.h"
#include "ui/models/services/whatsAppProxyConfigModel.h"
#include "ui/models/ipSplitTunnelingModel.h"
#include "ui/models/newsModel.h"
@@ -143,10 +136,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();
@@ -174,15 +163,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;
@@ -211,7 +198,6 @@ private:
ExportController* m_exportController;
ConnectionController* m_connectionController;
SettingsController* m_settingsController;
LocalProxyController* m_localProxyController;
ContainersModel* m_containersModel;
ContainersModel* m_defaultServerContainersModel;
@@ -228,8 +214,6 @@ private:
ApiSubscriptionPlansModel* m_apiSubscriptionPlansModel;
ApiBenefitsModel* m_apiBenefitsModel;
ApiCountryModel* m_apiCountryModel;
ApiCountryListModel* m_apiCountryListModel;
ApiCountryListModel* m_apiConfigsCountryListModel;
ApiAccountInfoModel* m_apiAccountInfoModel;
ApiDevicesModel* m_apiDevicesModel;
@@ -247,7 +231,6 @@ private:
MtProxyConfigModel* m_mtProxyConfigModel;
TelemtConfigModel* m_telemtConfigModel;
TProxyConfigModel* m_tProxyConfigModel;
WhatsAppProxyConfigModel* m_whatsAppProxyConfigModel;
CoreSignalHandlers* m_signalHandlers;
};
+1 -120
View File
@@ -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"
@@ -34,7 +32,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 +65,6 @@ void CoreSignalHandlers::initAllHandlers()
initExportControllerHandler();
initImportControllerHandler();
initApiCountryModelUpdateHandler();
initCountryListSortModeHandler();
initCountryListStateHandler();
initSubscriptionRefreshHandler();
initAdminConfigRevokedHandler();
initPassphraseRequestHandler();
@@ -84,7 +79,6 @@ void CoreSignalHandlers::initAllHandlers()
initAppSplitTunnelingModelUpdateHandler();
initPrepareConfigHandler();
initUnsupportedConnectDrawerHandler();
initBackNavigationHandler();
initStrictKillSwitchHandler();
initAndroidSettingsHandler();
initAndroidConnectionHandler();
@@ -92,7 +86,6 @@ void CoreSignalHandlers::initAllHandlers()
initIosSettingsHandler();
initNotificationHandler();
initUpdateFoundHandler();
initLocalProxyHandler();
}
void CoreSignalHandlers::initErrorMessagesHandler()
@@ -180,85 +173,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 +186,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 +230,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 +359,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 +444,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;
};
+660 -204
View File
@@ -1,79 +1,107 @@
#include "gatewayController.h"
#include <QDebug>
#include <QEventLoop>
#include <QFutureWatcher>
#include <algorithm>
#include <functional>
#include <random>
#include <QCryptographicHash>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QThread>
#include <QNetworkReply>
#include <QPromise>
#include <QUrl>
#include <QtConcurrent/QtConcurrentRun>
#include <openssl/rsa.h>
#include "amneziaApplication.h"
#include "core/repositories/secureAppSettingsRepository.h"
#include "core/utils/constants/apiConstants.h"
#include "core/utils/constants/apiKeys.h"
#include "core/utils/api/apiUtils.h"
#include "core/utils/constants/apiKeys.h"
#include "core/utils/networkUtilities.h"
#include "cryptoUtils.h"
#ifdef AMNEZIA_DESKTOP
#include "core/utils/ipcClient.h"
#include "core/utils/networkUtilities.h"
#endif
#ifdef Q_OS_IOS
#include "platforms/ios/ios_controller.h"
#endif
namespace
{
namespace agwConfigKey
constexpr QLatin1String errorResponsePattern1("No active configuration found for");
constexpr QLatin1String errorResponsePattern2("No non-revoked public key found for");
constexpr QLatin1String errorResponsePattern3("Account not found.");
constexpr QLatin1String errorResponsePatternQrSessionNotFound("QR session not found");
constexpr QLatin1String errorResponsePatternSessionNotFound("Session not found");
constexpr QLatin1String updateRequestResponsePattern("client version update is required");
constexpr int httpStatusCodeNotFound = 404;
constexpr int httpStatusCodeConflict = 409;
constexpr int httpStatusCodeNotImplemented = 501;
constexpr int httpStatusCodePaymentRequired = 402;
constexpr int httpStatusCodeRequestTimeout = 408;
constexpr int httpStatusCodeUnprocessableEntity = 422;
constexpr QLatin1String unprocessableSubscriptionMessage("Failed to retrieve subscription information. Is it activated?");
constexpr int proxyStorageRequestTimeoutMsecs = 3000;
QStringList shuffledProxyUrls(const QStringList &proxyUrls)
{
constexpr QLatin1String gatewayEndpoint("gateway_endpoint");
constexpr QLatin1String publicKeyPem("public_key_pem");
constexpr QLatin1String s3PrimaryEndpoints("s3_primary_endpoints");
constexpr QLatin1String s3FallbackEndpoints("s3_fallback_endpoints");
constexpr QLatin1String isDevEnvironment("is_dev_environment");
constexpr QLatin1String requestTimeoutMsecs("request_timeout_msecs");
constexpr QLatin1String proxyRequestTimeoutMsecs("proxy_request_timeout_msecs");
constexpr QLatin1String proxyStorageTimeoutMsecs("proxy_storage_timeout_msecs");
constexpr QLatin1String proxyHealthTimeoutMsecs("proxy_health_timeout_msecs");
QStringList shuffled = proxyUrls;
std::random_device randomDevice;
std::mt19937 generator(randomDevice());
std::shuffle(shuffled.begin(), shuffled.end(), generator);
return shuffled;
}
namespace agwOptionsKey
QString getProxyUrlsCacheKey(const QString &serviceType, const QString &userCountryCode)
{
constexpr QLatin1String serviceType("service_type");
constexpr QLatin1String userCountryCode("user_country_code");
return QStringLiteral("service_%1_country_%2").arg(serviceType, userCountryCode);
}
namespace agwStateKey
bool decryptProxyUrlsPayload(const QByteArray &encryptedPayload, bool isDevEnvironment, QByteArray &decryptedPayload)
{
constexpr QLatin1String workingProxy("working_proxy");
}
QByteArray key = isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
if (!isDevEnvironment) {
QCryptographicHash hash(QCryptographicHash::Sha512);
hash.addData(key);
QByteArray h = hash.result().toHex();
constexpr QLatin1String agwStateCacheKey("agw_state");
constexpr QLatin1String agwTracePrefix("agw trace:");
constexpr QLatin1String agwTraceFailoverMarker("suspicious");
constexpr QLatin1String endpointListSeparator(", ");
constexpr QLatin1String legacyEndpointPlaceholder("%1");
QByteArray decKey = QByteArray::fromHex(h.left(64));
QByteArray iv = QByteArray::fromHex(h.mid(64, 32));
QByteArray ba = QByteArray::fromBase64(encryptedPayload);
QStringList splitEndpoints(const char *raw)
{
return QString::fromUtf8(raw).split(endpointListSeparator, Qt::SkipEmptyParts);
}
void logAgwTrace(const QString &path, const int code, const QString &trace)
{
if (trace.isEmpty()) {
return;
}
const QString text = QStringLiteral("%1 %2\n%3").arg(agwTracePrefix, path, trace).replace(QLatin1Char('\n'), QStringLiteral("\n "));
if (code != AGW_OK) {
qWarning().noquote() << text;
} else if (trace.contains(agwTraceFailoverMarker)) {
qInfo().noquote() << text;
decryptedPayload = CryptoUtils::decryptAes256Cbc(ba, decKey, iv);
if (decryptedPayload.isEmpty()) {
return false;
}
} else {
qDebug().noquote() << text;
decryptedPayload = encryptedPayload;
}
return true;
}
QStringList readCachedProxyUrls(const QByteArray &cachedProxyUrlsEncrypted, bool isDevEnvironment)
{
if (cachedProxyUrlsEncrypted.isEmpty()) {
return {};
}
QByteArray cachedProxyUrlsDecrypted;
if (!decryptProxyUrlsPayload(cachedProxyUrlsEncrypted, isDevEnvironment, cachedProxyUrlsDecrypted)) {
qCritical() << "error decrypting cached proxy urls payload";
return {};
}
QJsonArray endpointsArray = QJsonDocument::fromJson(cachedProxyUrlsDecrypted).array();
QStringList endpoints;
endpoints.reserve(endpointsArray.size());
for (const QJsonValue &endpoint : endpointsArray) {
endpoints.push_back(endpoint.toString());
}
return endpoints;
}
}
@@ -82,187 +110,615 @@ GatewayController::GatewayController(const QString &gatewayEndpoint, const bool
QObject *parent)
: QObject(parent),
m_gatewayEndpoint(gatewayEndpoint),
m_isDevEnvironment(isDevEnvironment),
m_requestTimeoutMsecs(requestTimeoutMsecs),
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled),
m_appSettingsRepository(appSettingsRepository)
{
const QByteArray publicKey = isDevEnvironment ? QByteArray(DEV_AGW_PUBLIC_KEY) : QByteArray(PROD_AGW_PUBLIC_KEY);
m_publicKeyMissing = publicKey.isEmpty();
QStringList primaryEndpoints;
QStringList fallbackEndpoints;
if (isDevEnvironment) {
primaryEndpoints = splitEndpoints(DEV_S3_ENDPOINT);
} else {
primaryEndpoints = splitEndpoints(PROD_S3_ENDPOINT);
fallbackEndpoints = splitEndpoints(FALLBACK_S3_ENDPOINT);
}
QJsonObject config;
config[agwConfigKey::gatewayEndpoint] = gatewayEndpoint;
config[agwConfigKey::publicKeyPem] = QString::fromUtf8(publicKey);
config[agwConfigKey::s3PrimaryEndpoints] = QJsonArray::fromStringList(primaryEndpoints);
config[agwConfigKey::s3FallbackEndpoints] = QJsonArray::fromStringList(fallbackEndpoints);
config[agwConfigKey::isDevEnvironment] = isDevEnvironment;
config[agwConfigKey::requestTimeoutMsecs] = requestTimeoutMsecs;
config[agwConfigKey::proxyRequestTimeoutMsecs] = apiDefs::proxyRequestTimeoutMsecs;
config[agwConfigKey::proxyStorageTimeoutMsecs] = apiDefs::proxyStorageTimeoutMsecs;
config[agwConfigKey::proxyHealthTimeoutMsecs] = apiDefs::proxyHealthTimeoutMsecs;
m_client = agw_client_create(QJsonDocument(config).toJson(QJsonDocument::Compact).constData(), nullptr);
if (m_client == 0) {
qCritical() << "GatewayController: failed to create gateway client (missing key or endpoint?)";
return;
}
if (m_appSettingsRepository != nullptr) {
const QByteArray state = m_appSettingsRepository->readGatewayProxyUrls(agwStateCacheKey);
if (!state.isEmpty() && agw_import_state(m_client, state.constData()) == AGW_OK) {
m_lastPersistedState = state;
}
}
}
GatewayController::~GatewayController()
GatewayController::EncryptedRequestData GatewayController::prepareRequest(const QString &endpoint, const QJsonObject &apiPayload)
{
agw_client_destroy(m_client);
}
EncryptedRequestData encRequestData;
encRequestData.errorCode = ErrorCode::NoError;
amnezia::ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody)
{
QFuture<QPair<amnezia::ErrorCode, QByteArray>> future = postAsync(endpoint, apiPayload);
QFutureWatcher<QPair<amnezia::ErrorCode, QByteArray>> watcher;
QEventLoop wait;
connect(&watcher, &QFutureWatcherBase::finished, &wait, &QEventLoop::quit);
watcher.setFuture(future);
if (!future.isFinished()) {
wait.exec(QEventLoop::ExcludeUserInputEvents);
}
const QPair<amnezia::ErrorCode, QByteArray> result = future.result();
responseBody = result.second;
return result.first;
}
QFuture<QPair<amnezia::ErrorCode, QByteArray>> GatewayController::postAsync(const QString &endpoint, const QJsonObject apiPayload)
{
prepareRequest();
return QtConcurrent::run(
[this, endpoint, apiPayload]() -> QPair<amnezia::ErrorCode, QByteArray> { return executePost(endpoint, apiPayload); });
}
QPair<amnezia::ErrorCode, QByteArray> GatewayController::executePost(const QString &endpoint, const QJsonObject &apiPayload)
{
if (m_client == 0) {
return qMakePair(m_publicKeyMissing ? amnezia::ErrorCode::ApiMissingAgwPublicKey : amnezia::ErrorCode::ApiConfigDownloadError,
QByteArray());
}
QString path = endpoint;
path.remove(legacyEndpointPlaceholder);
QJsonObject options;
options[agwOptionsKey::serviceType] = apiPayload.value(apiDefs::key::serviceType).toString();
options[agwOptionsKey::userCountryCode] = apiPayload.value(apiDefs::key::userCountryCode).toString();
const QByteArray payload = QJsonDocument(apiPayload).toJson(QJsonDocument::Compact);
const QByteArray optionsJson = QJsonDocument(options).toJson(QJsonDocument::Compact);
agw_result result = agw_post(m_client, path.toUtf8().constData(), payload.constData(), optionsJson.constData(), 0);
QByteArray responseBody;
if (result.body != nullptr) {
responseBody = QByteArray(result.body, static_cast<qsizetype>(result.body_len));
}
QString trace;
if (result.trace != nullptr) {
trace = QString::fromUtf8(result.trace, static_cast<qsizetype>(result.trace_len));
}
const int code = result.code;
agw_result_free(&result);
logAgwTrace(path, code, trace);
persistState();
return qMakePair(mapResultCode(code, responseBody), responseBody);
}
amnezia::ErrorCode GatewayController::mapResultCode(const int code, const QByteArray &responseBody)
{
switch (code) {
case AGW_OK: return apiUtils::checkApiResponseErrors(responseBody);
case AGW_CANCELLED:
case AGW_ERR_TIMEOUT: return amnezia::ErrorCode::ApiConfigTimeoutError;
case AGW_ERR_SSL: return amnezia::ErrorCode::ApiConfigSslError;
case AGW_ERR_CONFIG: return amnezia::ErrorCode::ApiMissingAgwPublicKey;
case AGW_ERR_DECRYPT: return amnezia::ErrorCode::ApiConfigDecryptionError;
default: return amnezia::ErrorCode::ApiConfigDownloadError;
}
}
void GatewayController::prepareRequest()
{
#ifdef Q_OS_IOS
IosController::Instance()->requestInetAccess();
QThread::msleep(10);
#endif
encRequestData.request.setTransferTimeout(m_requestTimeoutMsecs);
encRequestData.request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
encRequestData.request.setRawHeader(QString("X-Client-Request-ID").toUtf8(), QUuid::createUuid().toString(QUuid::WithoutBraces).toUtf8());
encRequestData.request.setUrl(endpoint.arg(m_proxyUrl.isEmpty() ? m_gatewayEndpoint : m_proxyUrl));
// bypass killSwitch exceptions for API-gateway
#ifdef AMNEZIA_DESKTOP
if (!m_isStrictKillSwitchEnabled) {
return;
}
QStringList ips;
for (const QString &host : requestHosts()) {
const QString ip = NetworkUtilities::getIPAddress(host);
if (m_isStrictKillSwitchEnabled) {
QString host = QUrl(encRequestData.request.url()).host();
QString ip = NetworkUtilities::getIPAddress(host);
if (!ip.isEmpty()) {
ips.append(ip);
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
QRemoteObjectPendingReply<bool> reply = iface->addKillSwitchAllowedRange(QStringList { ip });
if (!reply.waitForFinished(1000) || !reply.returnValue())
qWarning() << "GatewayController::prepareRequest(): Failed to execute remote addKillSwitchAllowedRange call";
});
}
}
if (ips.isEmpty()) {
return;
}
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
QRemoteObjectPendingReply<bool> reply = iface->addKillSwitchAllowedRange(ips);
if (!reply.waitForFinished(1000) || !reply.returnValue()) {
qWarning() << "GatewayController::prepareRequest(): Failed to execute remote addKillSwitchAllowedRange call";
}
});
#endif
encRequestData.key = CryptoUtils::generateRandomBytes(32);
encRequestData.iv = CryptoUtils::generateRandomBytes(32);
encRequestData.salt = CryptoUtils::generateRandomBytes(8);
QJsonObject keyPayload;
keyPayload[apiDefs::key::aesKey] = QString(encRequestData.key.toBase64());
keyPayload[apiDefs::key::aesIv] = QString(encRequestData.iv.toBase64());
keyPayload[apiDefs::key::aesSalt] = QString(encRequestData.salt.toBase64());
QByteArray rsaKey = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
EVP_PKEY *publicKey = CryptoUtils::loadPublicKeyFromPem(rsaKey);
if (publicKey == nullptr) {
qCritical() << "error loading public key from environment variables";
encRequestData.errorCode = ErrorCode::ApiMissingAgwPublicKey;
return encRequestData;
}
QByteArray encryptedKeyPayload = CryptoUtils::rsaEncrypt(QJsonDocument(keyPayload).toJson(), publicKey, RSA_PKCS1_PADDING);
EVP_PKEY_free(publicKey);
QByteArray encryptedApiPayload = CryptoUtils::encryptAes256Cbc(QJsonDocument(apiPayload).toJson(), encRequestData.key, encRequestData.iv);
if (encryptedKeyPayload.isEmpty() || encryptedApiPayload.isEmpty()) {
qCritical() << "error when encrypting the request body";
encRequestData.errorCode = ErrorCode::ApiConfigDecryptionError;
return encRequestData;
}
QJsonObject requestBody;
requestBody[apiDefs::key::keyPayload] = QString(encryptedKeyPayload.toBase64());
requestBody[apiDefs::key::apiPayload] = QString(encryptedApiPayload.toBase64());
encRequestData.requestBody = QJsonDocument(requestBody).toJson();
return encRequestData;
}
QStringList GatewayController::requestHosts() const
GatewayController::DecryptionResult GatewayController::tryDecryptResponseBody(const QByteArray &encryptedResponseBody,
QNetworkReply::NetworkError replyError, const QByteArray &key,
const QByteArray &iv, const QByteArray &salt)
{
QStringList hosts;
if (m_client != 0) {
char *state = agw_export_state(m_client);
if (state != nullptr) {
const QString workingProxy = QJsonDocument::fromJson(QByteArray(state)).object().value(agwStateKey::workingProxy).toString();
agw_string_free(state);
if (!workingProxy.isEmpty()) {
hosts.append(QUrl(workingProxy).host());
DecryptionResult result;
result.decryptedBody = encryptedResponseBody;
result.isDecryptionSuccessful = false;
QByteArray decrypted = CryptoUtils::decryptAes256Cbc(encryptedResponseBody, key, iv);
if (!decrypted.isEmpty()) {
result.decryptedBody = decrypted;
result.isDecryptionSuccessful = true;
} else {
result.decryptedBody = encryptedResponseBody;
result.isDecryptionSuccessful = false;
}
return result;
}
ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody)
{
EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload);
if (encRequestData.errorCode != ErrorCode::NoError) {
return encRequestData.errorCode;
}
QNetworkReply *reply = amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody);
QEventLoop wait;
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
QList<QSslError> sslErrors;
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec(QEventLoop::ExcludeUserInputEvents);
QByteArray encryptedResponseBody = reply->readAll();
QString replyErrorString = reply->errorString();
auto replyError = reply->error();
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
reply->deleteLater();
auto decryptionResult =
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
if (sslErrors.isEmpty() && shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
auto requestFunction = [&encRequestData, &encryptedResponseBody](const QString &url) {
encRequestData.request.setUrl(url);
return amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody);
};
auto replyProcessingFunction = [&encryptedResponseBody, &replyErrorString, &replyError, &httpStatusCode, &sslErrors, &encRequestData,
&decryptionResult, this](QNetworkReply *reply, const QList<QSslError> &nestedSslErrors) {
encryptedResponseBody = reply->readAll();
replyErrorString = reply->errorString();
replyError = reply->error();
httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
decryptionResult =
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
if (!sslErrors.isEmpty()
|| shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
sslErrors = nestedSslErrors;
return false;
}
return true;
};
auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString("");
auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString("");
bypassProxy(endpoint, serviceType, userCountryCode, requestFunction, replyProcessingFunction);
}
responseBody = decryptionResult.decryptedBody;
const auto errorCode =
apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, responseBody);
if (errorCode) {
return errorCode;
}
if (!decryptionResult.isDecryptionSuccessful) {
qCritical() << "error when decrypting the request body";
return ErrorCode::ApiConfigDecryptionError;
}
return ErrorCode::NoError;
}
QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString &endpoint, const QJsonObject apiPayload)
{
auto promise = QSharedPointer<QPromise<QPair<ErrorCode, QByteArray>>>::create();
promise->start();
EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload);
if (encRequestData.errorCode != ErrorCode::NoError) {
promise->addResult(qMakePair(encRequestData.errorCode, QByteArray()));
promise->finish();
return promise->future();
}
QNetworkReply *reply = amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody);
auto sslErrors = QSharedPointer<QList<QSslError>>::create();
connect(reply, &QNetworkReply::sslErrors, [sslErrors](const QList<QSslError> &errors) { *sslErrors = errors; });
connect(reply, &QNetworkReply::finished, this, [promise, sslErrors, encRequestData, endpoint, apiPayload, reply, this]() mutable {
QByteArray encryptedResponseBody = reply->readAll();
QString replyErrorString = reply->errorString();
auto replyError = reply->error();
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
reply->deleteLater();
auto decryptionResult =
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
auto processResponse = [promise, encRequestData](const GatewayController::DecryptionResult &decryptionResult,
const QList<QSslError> &sslErrors, QNetworkReply::NetworkError replyError,
const QString &replyErrorString, int httpStatusCode) {
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode,
decryptionResult.decryptedBody);
if (errorCode) {
promise->addResult(qMakePair(errorCode, decryptionResult.decryptedBody));
promise->finish();
return;
}
if (!decryptionResult.isDecryptionSuccessful) {
qCritical() << "error when decrypting the request body";
promise->addResult(qMakePair(ErrorCode::ApiConfigDecryptionError, QByteArray()));
promise->finish();
return;
}
promise->addResult(qMakePair(ErrorCode::NoError, decryptionResult.decryptedBody));
promise->finish();
};
if (sslErrors->isEmpty() && shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString("");
auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString("");
QStringList primaryBaseUrls;
QStringList fallbackBaseUrls;
if (m_isDevEnvironment) {
primaryBaseUrls = QString(DEV_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
} else {
primaryBaseUrls = QString(PROD_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
fallbackBaseUrls = QString(FALLBACK_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
}
std::random_device randomDevice;
std::mt19937 generator(randomDevice());
std::shuffle(primaryBaseUrls.begin(), primaryBaseUrls.end(), generator);
std::shuffle(fallbackBaseUrls.begin(), fallbackBaseUrls.end(), generator);
auto appendStorageUrls = [&serviceType, &userCountryCode](const QStringList &baseUrls, QStringList &target) {
if (!serviceType.isEmpty()) {
for (const auto &baseUrl : baseUrls) {
QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8();
target.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) + ".json");
}
}
for (const auto &baseUrl : baseUrls) {
target.push_back(baseUrl + "endpoints.json");
}
};
QStringList proxyStorageUrls;
appendStorageUrls(primaryBaseUrls, proxyStorageUrls);
appendStorageUrls(fallbackBaseUrls, proxyStorageUrls);
const QString proxyUrlsCacheKey = getProxyUrlsCacheKey(serviceType, userCountryCode);
getProxyUrlsAsync(proxyStorageUrls, 0, proxyUrlsCacheKey, [this, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) {
getProxyUrlAsync(proxyUrls, 0, [this, encRequestData, endpoint, processResponse](const QString &proxyUrl) {
bypassProxyAsync(endpoint, proxyUrl, encRequestData,
[processResponse, this](const QByteArray &decryptedBody, bool isDecryptionSuccessful,
const QList<QSslError> &sslErrors, QNetworkReply::NetworkError replyError,
const QString &replyErrorString, int httpStatusCode) {
GatewayController::DecryptionResult result;
result.decryptedBody = decryptedBody;
result.isDecryptionSuccessful = isDecryptionSuccessful;
processResponse(result, sslErrors, replyError, replyErrorString, httpStatusCode);
});
});
});
} else {
processResponse(decryptionResult, *sslErrors, replyError, replyErrorString, httpStatusCode);
}
});
return promise->future();
}
QStringList GatewayController::getProxyUrls(const QString &serviceType, const QString &userCountryCode)
{
QNetworkRequest request;
request.setTransferTimeout(proxyStorageRequestTimeoutMsecs);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QEventLoop wait;
QList<QSslError> sslErrors;
QNetworkReply *reply;
QStringList primaryBaseUrls;
QStringList fallbackBaseUrls;
if (m_isDevEnvironment) {
primaryBaseUrls = QString(DEV_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
} else {
primaryBaseUrls = QString(PROD_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
fallbackBaseUrls = QString(FALLBACK_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
}
std::random_device randomDevice;
std::mt19937 generator(randomDevice());
std::shuffle(primaryBaseUrls.begin(), primaryBaseUrls.end(), generator);
std::shuffle(fallbackBaseUrls.begin(), fallbackBaseUrls.end(), generator);
auto appendStorageUrls = [&serviceType, &userCountryCode](const QStringList &baseUrls, QStringList &target) {
if (!serviceType.isEmpty()) {
for (const auto &baseUrl : baseUrls) {
QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8();
target.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) + ".json");
}
}
for (const auto &baseUrl : baseUrls) {
target.push_back(baseUrl + "endpoints.json");
}
};
QStringList proxyStorageUrls;
appendStorageUrls(primaryBaseUrls, proxyStorageUrls);
appendStorageUrls(fallbackBaseUrls, proxyStorageUrls);
const QString proxyUrlsCacheKey = getProxyUrlsCacheKey(serviceType, userCountryCode);
const QByteArray cachedProxyUrlsEncrypted = m_appSettingsRepository->readGatewayProxyUrls(proxyUrlsCacheKey);
if (proxyStorageUrls.empty()) {
qDebug() << "empty storage endpoint list";
return readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment);
}
for (const auto &proxyStorageUrl : proxyStorageUrls) {
request.setUrl(proxyStorageUrl);
reply = amnApp->networkManager()->get(request);
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec(QEventLoop::ExcludeUserInputEvents);
if (reply->error() == QNetworkReply::NetworkError::NoError) {
auto encryptedResponseBody = reply->readAll();
reply->deleteLater();
QByteArray responseBody;
if (!decryptProxyUrlsPayload(encryptedResponseBody, m_isDevEnvironment, responseBody)) {
qCritical() << "error loading private key from environment variables or decrypting payload" << encryptedResponseBody;
continue;
}
auto endpointsArray = QJsonDocument::fromJson(responseBody).array();
QStringList endpoints;
for (const auto &endpoint : endpointsArray) {
endpoints.push_back(endpoint.toString());
}
m_appSettingsRepository->writeGatewayProxyUrls(proxyUrlsCacheKey, encryptedResponseBody);
return endpoints;
} else {
auto replyError = reply->error();
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
qDebug() << replyError;
qDebug() << httpStatusCode;
qDebug() << "go to the next storage endpoint";
reply->deleteLater();
}
}
return readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment);
}
bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody,
bool isDecryptionSuccessful)
{
const QByteArray &responseBody = decryptedResponseBody;
int apiHttpStatus = -1;
QString apiErrorMessage;
if (isDecryptionSuccessful) {
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
if (jsonDoc.isObject()) {
QJsonObject jsonObj = jsonDoc.object();
apiHttpStatus = jsonObj.value("http_status").toInt(-1);
apiErrorMessage = jsonObj.value(QStringLiteral("message")).toString().trimmed();
}
} else {
qDebug() << "failed to decrypt the data";
return true;
}
if (replyError == QNetworkReply::NetworkError::OperationCanceledError || replyError == QNetworkReply::NetworkError::TimeoutError) {
qDebug() << "timeout occurred";
qDebug() << replyError;
return true;
}
if (responseBody.contains("html")) {
qDebug() << "the response contains an html tag";
return true;
}
if (apiHttpStatus == httpStatusCodeRequestTimeout) {
return false;
}
if (apiHttpStatus == httpStatusCodeNotFound) {
if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2)
|| responseBody.contains(errorResponsePattern3) || responseBody.contains(errorResponsePatternQrSessionNotFound)
|| responseBody.contains(errorResponsePatternSessionNotFound)) {
return false;
} else {
qDebug() << replyError;
return true;
}
}
if (apiHttpStatus == httpStatusCodeNotImplemented) {
if (responseBody.contains(updateRequestResponsePattern)) {
return false;
} else {
qDebug() << replyError;
return true;
}
}
if (apiHttpStatus == httpStatusCodeConflict) {
return false;
}
if (apiHttpStatus == httpStatusCodePaymentRequired) {
return false;
}
if (apiHttpStatus == httpStatusCodeUnprocessableEntity) {
return apiErrorMessage != unprocessableSubscriptionMessage;
}
if (replyError != QNetworkReply::NetworkError::NoError) {
qDebug() << replyError;
return true;
}
return false;
}
void GatewayController::bypassProxy(const QString &endpoint, const QString &serviceType, const QString &userCountryCode,
std::function<QNetworkReply *(const QString &url)> requestFunction,
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction)
{
QStringList proxyUrls = getProxyUrls(serviceType, userCountryCode);
std::random_device randomDevice;
std::mt19937 generator(randomDevice());
std::shuffle(proxyUrls.begin(), proxyUrls.end(), generator);
QByteArray responseBody;
auto bypassFunction = [this](const QString &endpoint, const QString &proxyUrl,
std::function<QNetworkReply *(const QString &url)> requestFunction,
std::function<bool(QNetworkReply * reply, const QList<QSslError> &sslErrors)> replyProcessingFunction) {
QEventLoop wait;
QList<QSslError> sslErrors;
qDebug() << "go to the next proxy endpoint";
QNetworkReply *reply = requestFunction(endpoint.arg(proxyUrl));
QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec(QEventLoop::ExcludeUserInputEvents);
auto result = replyProcessingFunction(reply, sslErrors);
reply->deleteLater();
return result;
};
if (m_proxyUrl.isEmpty()) {
QNetworkRequest request;
request.setTransferTimeout(1000);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QEventLoop wait;
QList<QSslError> sslErrors;
QNetworkReply *reply;
for (const QString &proxyUrl : proxyUrls) {
request.setUrl(proxyUrl + "lmbd-health");
reply = amnApp->networkManager()->get(request);
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec(QEventLoop::ExcludeUserInputEvents);
if (reply->error() == QNetworkReply::NetworkError::NoError) {
reply->deleteLater();
m_proxyUrl = proxyUrl;
if (!m_proxyUrl.isEmpty()) {
break;
}
} else {
reply->deleteLater();
}
}
}
hosts.append(QUrl(m_gatewayEndpoint).host());
hosts.removeAll(QString());
hosts.removeDuplicates();
return hosts;
if (!m_proxyUrl.isEmpty()) {
if (bypassFunction(endpoint, m_proxyUrl, requestFunction, replyProcessingFunction)) {
return;
}
}
for (const QString &proxyUrl : proxyUrls) {
if (bypassFunction(endpoint, proxyUrl, requestFunction, replyProcessingFunction)) {
m_proxyUrl = proxyUrl;
break;
}
}
}
void GatewayController::persistState()
void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
const QString &proxyUrlsCacheKey, std::function<void(const QStringList &)> onComplete)
{
if (m_client == 0 || m_appSettingsRepository == nullptr) {
return;
}
char *state = agw_export_state(m_client);
if (state == nullptr) {
return;
}
const QByteArray blob(state);
agw_string_free(state);
const QByteArray cachedProxyUrlsEncrypted = m_appSettingsRepository->readGatewayProxyUrls(proxyUrlsCacheKey);
if (blob != m_lastPersistedState) {
m_appSettingsRepository->writeGatewayProxyUrls(agwStateCacheKey, blob);
m_lastPersistedState = blob;
if (currentProxyStorageIndex >= proxyStorageUrls.size()) {
onComplete(shuffledProxyUrls(readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment)));
return;
}
QNetworkRequest request;
request.setTransferTimeout(proxyStorageRequestTimeoutMsecs);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setUrl(proxyStorageUrls[currentProxyStorageIndex]);
QNetworkReply *reply = amnApp->networkManager()->get(request);
// connect(reply, &QNetworkReply::sslErrors, this, [state](const QList<QSslError> &e) { *(state->sslErrors) = e; });
connect(reply, &QNetworkReply::finished, this,
[this, proxyStorageUrls, currentProxyStorageIndex, proxyUrlsCacheKey, onComplete, reply]() {
if (reply->error() == QNetworkReply::NoError) {
QByteArray encrypted = reply->readAll();
reply->deleteLater();
QByteArray responseBody;
if (!decryptProxyUrlsPayload(encrypted, m_isDevEnvironment, responseBody)) {
qCritical() << "error decrypting payload";
QMetaObject::invokeMethod(
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, proxyUrlsCacheKey, onComplete); }, Qt::QueuedConnection);
return;
}
QJsonArray endpointsArray = QJsonDocument::fromJson(responseBody).array();
QStringList endpoints;
for (const QJsonValue &endpoint : endpointsArray)
endpoints.push_back(endpoint.toString());
m_appSettingsRepository->writeGatewayProxyUrls(proxyUrlsCacheKey, encrypted);
onComplete(shuffledProxyUrls(endpoints));
return;
}
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
qDebug() << httpStatusCode;
qDebug() << "go to the next storage endpoint";
reply->deleteLater();
QMetaObject::invokeMethod(
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, proxyUrlsCacheKey, onComplete); }, Qt::QueuedConnection);
});
}
void GatewayController::getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex,
std::function<void(const QString &)> onComplete)
{
if (currentProxyIndex >= proxyUrls.size()) {
onComplete("");
return;
}
QNetworkRequest request;
request.setTransferTimeout(1000);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setUrl(proxyUrls[currentProxyIndex] + "lmbd-health");
QNetworkReply *reply = amnApp->networkManager()->get(request);
// connect(reply, &QNetworkReply::sslErrors, this, [state](const QList<QSslError> &e) {
// *(state->sslErrors) = e;
// });
connect(reply, &QNetworkReply::finished, this, [this, proxyUrls, currentProxyIndex, onComplete, reply]() {
reply->deleteLater();
if (reply->error() == QNetworkReply::NoError) {
m_proxyUrl = proxyUrls[currentProxyIndex];
onComplete(m_proxyUrl);
return;
}
qDebug() << "go to the next proxy endpoint";
QMetaObject::invokeMethod(this, [=]() { getProxyUrlAsync(proxyUrls, currentProxyIndex + 1, onComplete); }, Qt::QueuedConnection);
});
}
void GatewayController::bypassProxyAsync(
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
std::function<void(const QByteArray &, bool, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> onComplete)
{
auto sslErrors = QSharedPointer<QList<QSslError>>::create();
if (proxyUrl.isEmpty()) {
onComplete(QByteArray(), false, *sslErrors, QNetworkReply::InternalServerError, "empty proxy url", 0);
return;
}
QNetworkRequest request = encRequestData.request;
request.setUrl(endpoint.arg(proxyUrl));
QNetworkReply *reply = amnApp->networkManager()->post(request, encRequestData.requestBody);
connect(reply, &QNetworkReply::sslErrors, this, [sslErrors](const QList<QSslError> &errors) { *sslErrors = errors; });
connect(reply, &QNetworkReply::finished, this, [sslErrors, onComplete, encRequestData, reply, this]() {
QByteArray encryptedResponseBody = reply->readAll();
QString replyErrorString = reply->errorString();
auto replyError = reply->error();
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
reply->deleteLater();
auto decryptionResult =
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
onComplete(decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful, *sslErrors, replyError, replyErrorString,
httpStatusCode);
});
}
+42 -14
View File
@@ -1,17 +1,20 @@
#ifndef GATEWAYCONTROLLER_H
#define GATEWAYCONTROLLER_H
#include <QByteArray>
#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 +26,51 @@ public:
explicit GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
const bool isStrictKillSwitchEnabled, SecureAppSettingsRepository *appSettingsRepository,
QObject *parent = nullptr);
~GatewayController() override;
amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody);
QFuture<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject apiPayload);
private:
void prepareRequest();
QStringList requestHosts() const;
struct EncryptedRequestData
{
QNetworkRequest request;
QByteArray requestBody;
QByteArray key;
QByteArray iv;
QByteArray salt;
amnezia::ErrorCode errorCode;
};
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);
QStringList getProxyUrls(const QString &serviceType, const QString &userCountryCode);
bool shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody, bool isDecryptionSuccessful);
void bypassProxy(const QString &endpoint, const QString &serviceType, const QString &userCountryCode,
std::function<QNetworkReply *(const QString &url)> requestFunction,
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction);
void getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
const QString &proxyUrlsCacheKey, std::function<void(const QStringList &)> onComplete);
void getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex, std::function<void(const QString &)> onComplete);
void bypassProxyAsync(
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
std::function<void(const QByteArray &, bool, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> onComplete);
int m_requestTimeoutMsecs;
QString m_gatewayEndpoint;
bool m_isDevEnvironment = false;
bool m_isStrictKillSwitchEnabled = false;
SecureAppSettingsRepository *m_appSettingsRepository = nullptr;
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
-270
View File
@@ -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;
}
-30
View File
@@ -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();
@@ -33,18 +33,6 @@ using namespace ProtocolUtils;
namespace
{
bool rejectUnsupportedFormatVersion(ImportController::ImportResult &result)
{
if (serverConfigUtils::isConfigFormatVersionSupported(result.config)) {
return false;
}
qWarning() << "Config format version" << serverConfigUtils::configFormatVersion(result.config)
<< "is newer than supported" << serverConfigUtils::currentConfigFormatVersion;
result.errorCode = ErrorCode::ConfigFormatVersionNotSupportedError;
result.config = {};
return true;
}
ConfigTypes checkConfigFormat(const QString &config)
{
const QString openVpnConfigPatternCli = "client";
@@ -64,7 +52,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 +67,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;
}
@@ -222,10 +207,6 @@ ImportController::ImportResult ImportController::extractConfigFromData(const QSt
case ConfigTypes::Amnezia: {
result.config = QJsonDocument::fromJson(config.toUtf8()).object();
if (rejectUnsupportedFormatVersion(result)) {
return result;
}
if (serverConfigUtils::isServerFromApi(result.config)) {
auto apiConfig = result.config.value(apiDefs::key::apiConfig).toObject();
apiConfig[apiDefs::key::vpnKey] = data;
@@ -250,10 +231,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();
@@ -279,7 +256,6 @@ ImportController::ImportResult ImportController::extractConfigFromQr(const QByte
if (!dataObj.isEmpty()) {
result.config = dataObj;
result.configType = ConfigTypes::Amnezia;
rejectUnsupportedFormatVersion(result);
return result;
}
@@ -291,7 +267,6 @@ ImportController::ImportResult ImportController::extractConfigFromQr(const QByte
return result;
}
result.configType = ConfigTypes::Amnezia;
rejectUnsupportedFormatVersion(result);
return result;
}
@@ -309,7 +284,6 @@ ImportController::ImportResult ImportController::extractConfigFromQr(const QByte
return result;
}
result.configType = ConfigTypes::Amnezia;
rejectUnsupportedFormatVersion(result);
return result;
}
@@ -405,11 +379,6 @@ int ImportController::qrChunksTotal() const
void ImportController::importConfig(const QJsonObject &config)
{
if (!serverConfigUtils::isConfigFormatVersionSupported(config)) {
emit importErrorOccurred(ErrorCode::ConfigFormatVersionNotSupportedError, false);
return;
}
ServerCredentials credentials;
credentials.hostName = config.value(configKey::hostName).toString();
credentials.port = config.value(configKey::port).toInt();
@@ -779,11 +748,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 +759,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"
@@ -25,7 +24,6 @@
#include "core/installers/mtProxyInstaller.h"
#include "core/installers/telemtInstaller.h"
#include "core/installers/tProxyInstaller.h"
#include "core/installers/whatsAppProxyInstaller.h"
#include "core/installers/torInstaller.h"
#include "core/installers/wireguardInstaller.h"
#include "core/installers/xrayInstaller.h"
@@ -62,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)) {
@@ -141,35 +75,15 @@ namespace
return false;
}
bool containerKeepsIdentityInDataVolume(DockerContainer container)
{
return container == DockerContainer::MtProxy
|| container == DockerContainer::Telemt
|| container == DockerContainer::TProxy
|| container == DockerContainer::WhatsAppProxy
|| 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,
@@ -188,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;
@@ -215,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;
@@ -255,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;
@@ -277,72 +161,47 @@ 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();
SshSession sshSession;
TProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
} else if (container == DockerContainer::WhatsAppProxy) {
WhatsAppProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
}
adminConfig->updateContainerConfig(container, newConfig);
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
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) {
@@ -350,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);
@@ -376,13 +230,9 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
} else if (container == DockerContainer::TProxy) {
TProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
} else if (container == DockerContainer::WhatsAppProxy) {
WhatsAppProxyInstaller::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);
@@ -591,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;
@@ -723,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) {
@@ -735,8 +578,6 @@ ErrorCode InstallController::configureContainerWorker(const ServerCredentials &c
TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, config);
} else if (container == DockerContainer::TProxy) {
TProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, config);
} else if (container == DockerContainer::WhatsAppProxy) {
WhatsAppProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, config);
}
return ErrorCode::NoError;
@@ -765,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) {
@@ -918,14 +638,6 @@ ErrorCode InstallController::isServerPortBusy(const ServerCredentials &credentia
}
}
}
if (container == DockerContainer::WhatsAppProxy) {
if (const auto *whatsAppProxyConfig = config.getWhatsAppProxyProtocolConfig()) {
const QString mediaPort = whatsAppProxyConfig->effectiveMediaPort();
if (!fixedPorts.contains(mediaPort) && mediaPort != port) {
fixedPorts.append(mediaPort);
}
}
}
QString transportProto = config.protocolConfig.transportProto();
if (transportProto.isEmpty()) {
transportProto = ProtocolUtils::transportProtoToString(ProtocolUtils::defaultTransportProto(protocol), protocol);
@@ -1031,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();
@@ -1101,14 +800,6 @@ bool InstallController::isReinstallContainerRequired(DockerContainer container,
}
}
if (container == DockerContainer::WhatsAppProxy) {
const auto *oldW = oldConfig.getWhatsAppProxyProtocolConfig();
const auto *newW = newConfig.getWhatsAppProxyProtocolConfig();
if (oldW && newW && !oldW->equalsDockerDeploymentSettings(*newW)) {
return true;
}
}
if (container == DockerContainer::Socks5Proxy) {
return true;
}
@@ -1146,7 +837,7 @@ ErrorCode InstallController::installDockerWorker(const ServerCredentials &creden
qDebug().noquote() << "InstallController::installDockerWorker" << stdOut;
if (container == DockerContainer::MtProxy || container == DockerContainer::Telemt
|| container == DockerContainer::TProxy || container == DockerContainer::WhatsAppProxy) {
|| container == DockerContainer::TProxy) {
QString conntrackOut;
auto cbConntrack = [&](const QString &data, libssh::Client &) {
conntrackOut += data + "\n";
@@ -1324,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);
@@ -1396,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;
@@ -1446,7 +1091,6 @@ QScopedPointer<InstallerBase> InstallController::createInstaller(DockerContainer
case DockerContainer::MtProxy: return QScopedPointer<InstallerBase>(new MtProxyInstaller(this));
case DockerContainer::Telemt: return QScopedPointer<InstallerBase>(new TelemtInstaller(this));
case DockerContainer::TProxy: return QScopedPointer<InstallerBase>(new TProxyInstaller(this));
case DockerContainer::WhatsAppProxy: return QScopedPointer<InstallerBase>(new WhatsAppProxyInstaller(this));
default: return QScopedPointer<InstallerBase>(new InstallerBase(this));
}
}
@@ -1481,14 +1125,6 @@ ErrorCode InstallController::installContainer(const ServerCredentials &credentia
return ErrorCode::InternalError;
}
}
if (container == DockerContainer::WhatsAppProxy) {
if (auto *whatsAppProxyConfig = config.getWhatsAppProxyProtocolConfig()) {
if (!m_whatsAppProxyInstallMediaPort.isEmpty()) {
whatsAppProxyConfig->mediaPort = m_whatsAppProxyInstallMediaPort;
}
}
m_whatsAppProxyInstallMediaPort.clear();
}
return setupContainer(credentials, container, config, false);
}
@@ -1498,11 +1134,6 @@ void InstallController::setTProxyInstallHints(const QString &hostname, const QSt
m_tproxyInstallEmail = email;
}
void InstallController::setWhatsAppProxyInstallHints(const QString &mediaPort)
{
m_whatsAppProxyInstallMediaPort = mediaPort;
}
bool InstallController::isUpdateDockerContainerRequired(DockerContainer container, const ContainerConfig &oldConfig, const ContainerConfig &newConfig)
{
@@ -1524,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();
@@ -1552,13 +1176,6 @@ bool InstallController::isUpdateDockerContainerRequired(DockerContainer containe
return true;
}
return !oldP->equalsDockerDeploymentSettings(*newP);
} else if (container == DockerContainer::WhatsAppProxy) {
const auto *oldW = oldConfig.getWhatsAppProxyProtocolConfig();
const auto *newW = newConfig.getWhatsAppProxyProtocolConfig();
if (!oldW || !newW) {
return true;
}
return !oldW->equalsDockerDeploymentSettings(*newW);
}
return true;
@@ -1990,11 +1607,6 @@ ErrorCode InstallController::getAlreadyInstalledContainers(const ServerCredentia
TProxyInstaller::applyDockerPublishedPorts(containerInfo, *tProxyConfig);
}
}
if (container == DockerContainer::WhatsAppProxy) {
if (auto *whatsAppProxyConfig = config.getWhatsAppProxyProtocolConfig()) {
WhatsAppProxyInstaller::applyDockerPublishedPorts(containerInfo, *whatsAppProxyConfig);
}
}
ErrorCode extractError = installer->extractConfigFromContainer(container, credentials, &sshSession, config);
if (extractError != ErrorCode::NoError && extractError != ErrorCode::ServerContainerMissingError) {
@@ -2025,11 +1637,6 @@ ErrorCode InstallController::getAlreadyInstalledContainers(const ServerCredentia
TProxyInstaller::applyDockerPublishedPorts(containerInfo, *tProxyConfig);
}
}
if (container == DockerContainer::WhatsAppProxy) {
if (auto *whatsAppProxyConfig = config.getWhatsAppProxyProtocolConfig()) {
WhatsAppProxyInstaller::applyDockerPublishedPorts(containerInfo, *whatsAppProxyConfig);
}
}
ErrorCode extractError = installer->extractConfigFromContainer(container, credentials, &sshSession, config);
if (extractError != ErrorCode::NoError && extractError != ErrorCode::ServerContainerMissingError) {
@@ -2046,7 +1653,7 @@ ErrorCode InstallController::getAlreadyInstalledContainers(const ServerCredentia
ErrorCode InstallController::setDockerContainerEnabledState(const QString &serverId, DockerContainer container, bool enabled)
{
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt
&& container != DockerContainer::TProxy && container != DockerContainer::WhatsAppProxy) {
&& container != DockerContainer::TProxy) {
return ErrorCode::InternalError;
}
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
@@ -2076,9 +1683,6 @@ ErrorCode InstallController::setDockerContainerEnabledState(const QString &serve
} else if (auto *tProxyConfig = currentConfig.getTProxyProtocolConfig()) {
tProxyConfig->isEnabled = enabled;
persist = true;
} else if (auto *whatsAppProxyConfig = currentConfig.getWhatsAppProxyProtocolConfig()) {
whatsAppProxyConfig->isEnabled = enabled;
persist = true;
}
if (persist) {
adminConfig->updateContainerConfig(container, currentConfig);
@@ -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);
@@ -57,7 +56,6 @@ public:
QString fetchDockerContainerSecret(const QString &serverId, DockerContainer container);
void setTProxyInstallHints(const QString &hostname, const QString &email);
void setWhatsAppProxyInstallHints(const QString &mediaPort);
ContainerConfig generateConfig(DockerContainer container, int port, TransportProto transportProto);
ErrorCode getAlreadyInstalledContainers(const ServerCredentials &credentials, QMap<DockerContainer, ContainerConfig> &installedContainers, SshSession &sshSession);
@@ -108,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);
@@ -137,8 +130,7 @@ private:
bool m_cancelInstallation = false;
QString m_tproxyInstallHostname;
QString m_tproxyInstallEmail;
QString m_whatsAppProxyInstallMediaPort;
#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 -13
View File
@@ -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
@@ -119,11 +119,6 @@ QByteArray SettingsController::backupAppConfig() const
return QJsonDocument(config).toJson();
}
int SettingsController::unsupportedFormatConfigsSkippedCount() const
{
return m_serversRepository->unsupportedFormatConfigsCount();
}
ErrorCode SettingsController::restoreAppConfigFromData(const QByteArray &data)
{
if (!m_appSettingsRepository->restoreAppConfig(data)) {
@@ -39,7 +39,6 @@ public:
QByteArray backupAppConfig() const;
ErrorCode restoreAppConfigFromData(const QByteArray &data);
int unsupportedFormatConfigsSkippedCount() const;
QString getAppVersion() const;
+1 -6
View File
@@ -13,7 +13,6 @@
#include "core/controllers/gatewayController.h"
#include "core/utils/api/gatewayPayloadBuilder.h"
#include "core/utils/appUiConfig.h"
#include "core/utils/constants/apiConstants.h"
#include "core/utils/constants/apiKeys.h"
#include "core/utils/selfhosted/scriptsRegistry.h"
@@ -130,10 +129,6 @@ void UpdateController::setUpdateCheckRunning(bool running)
void UpdateController::checkForUpdates()
{
#if !CLIENT_ENABLE_APP_UPDATES
return;
#endif
if (m_updateCheckRunning || !m_appSettingsRepository) {
return;
}
@@ -141,7 +136,7 @@ void UpdateController::checkForUpdates()
auto gatewayController = QSharedPointer<GatewayController>::create(m_appSettingsRepository->getGatewayEndpoint(),
m_appSettingsRepository->isDevGatewayEnv(),
apiDefs::requestTimeoutMsecs,
7000,
m_appSettingsRepository->isStrictKillSwitchEnabled(),
m_appSettingsRepository);
+2
View File
@@ -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
View File
@@ -87,8 +87,8 @@ ContainerConfig InstallerBase::createBaseConfig(DockerContainer container, int p
srv.flow = protocols::xray::defaultFlow;
srv.site = protocols::xray::defaultSite;
srv.sni = protocols::xray::defaultSni;
srv.alpn = protocols::xray::defaultAlpn;
srv.fingerprint = protocols::xray::defaultFingerprint;
srv.alpn = protocols::xray::defaultAlpn;
config.protocolConfig = xrayConfig;
break;
}
@@ -125,13 +125,6 @@ ContainerConfig InstallerBase::createBaseConfig(DockerContainer container, int p
config.protocolConfig = tProxyConfig;
break;
}
case Proto::WhatsAppProxy: {
WhatsAppProxyProtocolConfig whatsAppProxyConfig;
whatsAppProxyConfig.port = portStr.isEmpty() ? QString(protocols::whatsAppProxy::defaultPort) : portStr;
whatsAppProxyConfig.mediaPort = QString(protocols::whatsAppProxy::defaultMediaPort);
config.protocolConfig = whatsAppProxyConfig;
break;
}
case Proto::Ikev2: {
Ikev2ProtocolConfig ikev2Config;
config.protocolConfig = ikev2Config;
@@ -1,84 +0,0 @@
#include "whatsAppProxyInstaller.h"
#include "core/models/containerConfig.h"
#include "core/utils/containerEnum.h"
#include "core/utils/selfhosted/sshSession.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QRegularExpression>
using namespace amnezia;
namespace
{
constexpr QLatin1String kClientJsonPath("/data/amnezia-whatsappproxy-client.json");
constexpr QLatin1String kClientJsonUploadPath("data/amnezia-whatsappproxy-client.json");
}
void WhatsAppProxyInstaller::applyDockerPublishedPorts(const QString &dockerPsPortsLine,
WhatsAppProxyProtocolConfig &config)
{
static const QRegularExpression chatRe(QStringLiteral(":(\\d+)->443/tcp"));
static const QRegularExpression mediaRe(QStringLiteral(":(\\d+)->587/tcp"));
const QRegularExpressionMatch chatMatch = chatRe.match(dockerPsPortsLine);
const QRegularExpressionMatch mediaMatch = mediaRe.match(dockerPsPortsLine);
if (chatMatch.hasMatch()) {
config.port = chatMatch.captured(1);
}
if (mediaMatch.hasMatch()) {
config.mediaPort = mediaMatch.captured(1);
}
}
WhatsAppProxyInstaller::WhatsAppProxyInstaller(QObject *parent) : InstallerBase(parent)
{
}
ErrorCode WhatsAppProxyInstaller::extractConfigFromContainer(DockerContainer container,
const ServerCredentials &credentials,
SshSession *sshSession, ContainerConfig &config)
{
if (container != DockerContainer::WhatsAppProxy || !sshSession) {
return ErrorCode::NoError;
}
WhatsAppProxyProtocolConfig *wc = config.getWhatsAppProxyProtocolConfig();
if (!wc) {
return ErrorCode::NoError;
}
ErrorCode jsonErr = ErrorCode::NoError;
const QByteArray jsonRaw =
sshSession->getTextFileFromContainer(container, credentials, QString(kClientJsonPath), jsonErr);
if (jsonErr != ErrorCode::NoError || jsonRaw.trimmed().isEmpty()) {
return ErrorCode::NoError;
}
QJsonParseError parseError;
const QJsonDocument doc = QJsonDocument::fromJson(jsonRaw.trimmed(), &parseError);
if (parseError.error != QJsonParseError::NoError || !doc.isObject()) {
return ErrorCode::NoError;
}
const WhatsAppProxyProtocolConfig snapshot = WhatsAppProxyProtocolConfig::fromJson(doc.object());
wc->publicHost = snapshot.publicHost;
wc->isEnabled = snapshot.isEnabled;
return ErrorCode::NoError;
}
void WhatsAppProxyInstaller::uploadClientSettingsSnapshot(SshSession &sshSession, const ServerCredentials &credentials,
DockerContainer container, const ContainerConfig &config)
{
const WhatsAppProxyProtocolConfig *wc = config.getWhatsAppProxyProtocolConfig();
if (!wc) {
return;
}
const QByteArray payload = QJsonDocument(wc->toJson()).toJson(QJsonDocument::Compact);
const ErrorCode err = sshSession.uploadTextFileToContainer(container, credentials, QString::fromUtf8(payload),
QString(kClientJsonUploadPath));
if (err != ErrorCode::NoError) {
qWarning() << "WhatsAppProxyInstaller::uploadClientSettingsSnapshot failed" << err;
}
}
@@ -1,24 +0,0 @@
#ifndef WHATSAPPPROXYINSTALLER_H
#define WHATSAPPPROXYINSTALLER_H
#include "installerBase.h"
#include "core/models/protocols/whatsAppProxyProtocolConfig.h"
class WhatsAppProxyInstaller : public InstallerBase
{
Q_OBJECT
public:
explicit WhatsAppProxyInstaller(QObject *parent = nullptr);
amnezia::ErrorCode extractConfigFromContainer(amnezia::DockerContainer container,
const amnezia::ServerCredentials &credentials, SshSession *sshSession,
amnezia::ContainerConfig &config) override;
static void applyDockerPublishedPorts(const QString &dockerPsPortsLine, amnezia::WhatsAppProxyProtocolConfig &config);
static void uploadClientSettingsSnapshot(SshSession &sshSession, const amnezia::ServerCredentials &credentials,
amnezia::DockerContainer container, const amnezia::ContainerConfig &config);
};
#endif // WHATSAPPPROXYINSTALLER_H
+286 -19
View File
@@ -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;
@@ -85,7 +85,6 @@ QPair<QString, QString> ApiV2ServerConfig::getDnsPair(const QString &primaryDns,
QJsonObject ApiV2ServerConfig::toJson() const
{
QJsonObject obj;
obj[configKey::formatVersion] = serverConfigUtils::currentConfigFormatVersion;
if (!name.isEmpty()) {
obj[configKey::name] = name;
-10
View File
@@ -143,16 +143,6 @@ const TProxyProtocolConfig* ContainerConfig::getTProxyProtocolConfig() const
return protocolConfig.as<TProxyProtocolConfig>();
}
WhatsAppProxyProtocolConfig* ContainerConfig::getWhatsAppProxyProtocolConfig()
{
return protocolConfig.as<WhatsAppProxyProtocolConfig>();
}
const WhatsAppProxyProtocolConfig* ContainerConfig::getWhatsAppProxyProtocolConfig() const
{
return protocolConfig.as<WhatsAppProxyProtocolConfig>();
}
Ikev2ProtocolConfig* ContainerConfig::getIkev2ProtocolConfig()
{
return protocolConfig.as<Ikev2ProtocolConfig>();
-3
View File
@@ -66,9 +66,6 @@ struct ContainerConfig {
TProxyProtocolConfig* getTProxyProtocolConfig();
const TProxyProtocolConfig* getTProxyProtocolConfig() const;
WhatsAppProxyProtocolConfig* getWhatsAppProxyProtocolConfig();
const WhatsAppProxyProtocolConfig* getWhatsAppProxyProtocolConfig() const;
Ikev2ProtocolConfig* getIkev2ProtocolConfig();
const Ikev2ProtocolConfig* getIkev2ProtocolConfig() const;
-9
View File
@@ -12,7 +12,6 @@
#include "core/models/protocols/mtProxyProtocolConfig.h"
#include "core/models/protocols/telemtProtocolConfig.h"
#include "core/models/protocols/tProxyProtocolConfig.h"
#include "core/models/protocols/whatsAppProxyProtocolConfig.h"
namespace amnezia
{
@@ -48,8 +47,6 @@ Proto ProtocolConfig::type() const
return Proto::Telemt;
} else if constexpr (std::is_same_v<T, TProxyProtocolConfig>) {
return Proto::TProxy;
} else if constexpr (std::is_same_v<T, WhatsAppProxyProtocolConfig>) {
return Proto::WhatsAppProxy;
}
return Proto::Unknown;
}, data);
@@ -83,8 +80,6 @@ QString ProtocolConfig::port() const
return arg.port.isEmpty() ? QString(protocols::telemt::defaultPort) : arg.port;
} else if constexpr (std::is_same_v<T, TProxyProtocolConfig>) {
return arg.port.isEmpty() ? QString(protocols::tProxy::defaultPort) : arg.port;
} else if constexpr (std::is_same_v<T, WhatsAppProxyProtocolConfig>) {
return arg.effectivePort();
}
return QString();
}, data);
@@ -114,8 +109,6 @@ QString ProtocolConfig::transportProto() const
return QStringLiteral("tcp");
} else if constexpr (std::is_same_v<T, TProxyProtocolConfig>) {
return QStringLiteral("tcp");
} else if constexpr (std::is_same_v<T, WhatsAppProxyProtocolConfig>) {
return QStringLiteral("tcp");
}
return QString();
}, data);
@@ -333,8 +326,6 @@ ProtocolConfig ProtocolConfig::fromJson(const QJsonObject& json, Proto type)
return ProtocolConfig{TelemtProtocolConfig::fromJson(json)};
case Proto::TProxy:
return ProtocolConfig{TProxyProtocolConfig::fromJson(json)};
case Proto::WhatsAppProxy:
return ProtocolConfig{WhatsAppProxyProtocolConfig::fromJson(json)};
default:
return ProtocolConfig{AwgProtocolConfig{}};
}
-2
View File
@@ -25,7 +25,6 @@
#include "core/models/protocols/mtProxyProtocolConfig.h"
#include "core/models/protocols/telemtProtocolConfig.h"
#include "core/models/protocols/tProxyProtocolConfig.h"
#include "core/models/protocols/whatsAppProxyProtocolConfig.h"
namespace amnezia
{
@@ -43,7 +42,6 @@ struct ProtocolConfig {
MtProxyProtocolConfig,
TelemtProtocolConfig,
TProxyProtocolConfig,
WhatsAppProxyProtocolConfig,
Ikev2ProtocolConfig,
TorProtocolConfig,
DnsProtocolConfig
@@ -1,47 +0,0 @@
#include "whatsAppProxyProtocolConfig.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
using namespace amnezia;
QString WhatsAppProxyProtocolConfig::effectivePort() const
{
return port.isEmpty() ? QString(protocols::whatsAppProxy::defaultPort) : port;
}
QString WhatsAppProxyProtocolConfig::effectiveMediaPort() const
{
return mediaPort.isEmpty() ? QString(protocols::whatsAppProxy::defaultMediaPort) : mediaPort;
}
QJsonObject WhatsAppProxyProtocolConfig::toJson() const
{
QJsonObject obj;
if (!port.isEmpty()) {
obj[QString(configKey::port)] = port;
}
if (!mediaPort.isEmpty()) {
obj[protocols::whatsAppProxy::mediaPortKey] = mediaPort;
}
if (!publicHost.isEmpty()) {
obj[protocols::whatsAppProxy::publicHostKey] = publicHost;
}
obj[protocols::whatsAppProxy::isEnabledKey] = isEnabled;
return obj;
}
WhatsAppProxyProtocolConfig WhatsAppProxyProtocolConfig::fromJson(const QJsonObject &json)
{
WhatsAppProxyProtocolConfig c;
c.port = json.value(QString(configKey::port)).toString();
c.mediaPort = json.value(protocols::whatsAppProxy::mediaPortKey).toString();
c.publicHost = json.value(protocols::whatsAppProxy::publicHostKey).toString();
c.isEnabled = json.value(protocols::whatsAppProxy::isEnabledKey).toBool(true);
return c;
}
bool WhatsAppProxyProtocolConfig::equalsDockerDeploymentSettings(const WhatsAppProxyProtocolConfig &other) const
{
return effectivePort() == other.effectivePort() && effectiveMediaPort() == other.effectiveMediaPort();
}
@@ -1,27 +0,0 @@
#ifndef WHATSAPPPROXYPROTOCOLCONFIG_H
#define WHATSAPPPROXYPROTOCOLCONFIG_H
#include <QJsonObject>
#include <QString>
namespace amnezia
{
struct WhatsAppProxyProtocolConfig
{
QString port;
QString mediaPort;
QString publicHost;
bool isEnabled = true;
QString effectivePort() const;
QString effectiveMediaPort() const;
QJsonObject toJson() const;
static WhatsAppProxyProtocolConfig fromJson(const QJsonObject &json);
bool equalsDockerDeploymentSettings(const WhatsAppProxyProtocolConfig &other) const;
};
} // namespace amnezia
#endif // WHATSAPPPROXYPROTOCOLCONFIG_H
File diff suppressed because it is too large. Load diff
+21 -119
View File
@@ -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
@@ -8,7 +8,6 @@
#include "core/utils/protocolEnum.h"
#include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/serverConfigUtils.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/networkUtilities.h"
@@ -52,7 +51,6 @@ QPair<QString, QString> NativeServerConfig::getDnsPair(const QString &primaryDns
QJsonObject NativeServerConfig::toJson() const
{
QJsonObject obj;
obj[configKey::formatVersion] = serverConfigUtils::currentConfigFormatVersion;
if (!description.isEmpty()) {
obj[configKey::description] = this->description;
@@ -9,7 +9,6 @@
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
#include "core/utils/networkUtilities.h"
#include "core/utils/serverConfigUtils.h"
namespace amnezia
{
@@ -84,7 +83,6 @@ QPair<QString, QString> SelfHostedAdminServerConfig::getDnsPair(bool isAmneziaDn
QJsonObject SelfHostedAdminServerConfig::toJson() const
{
QJsonObject obj;
obj[configKey::formatVersion] = serverConfigUtils::currentConfigFormatVersion;
if (!description.isEmpty()) {
obj[configKey::description] = this->description;
@@ -4,7 +4,6 @@
#include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/serverConfigUtils.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
@@ -67,7 +66,6 @@ QPair<QString, QString> SelfHostedUserServerConfig::getDnsPair(const QString &pr
QJsonObject SelfHostedUserServerConfig::toJson() const
{
QJsonObject obj;
obj[configKey::formatVersion] = serverConfigUtils::currentConfigFormatVersion;
if (!description.isEmpty()) {
obj[configKey::description] = this->description;
+4 -7
View File
@@ -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())) {
-7
View File
@@ -72,7 +72,6 @@ QMap<Proto, QString> ProtocolUtils::protocolHumanNames()
{ Proto::MtProxy, QObject::tr("MTProxy (Telegram)") },
{ Proto::Telemt, QObject::tr("Telemt (Telegram)") },
{ Proto::TProxy, QObject::tr("TProxy (Telegram WEB)") },
{ Proto::WhatsAppProxy, QObject::tr("WhatsApp proxy") },
};
}
@@ -100,7 +99,6 @@ ServiceType ProtocolUtils::protocolService(Proto p)
case Proto::MtProxy: return ServiceType::Other;
case Proto::Telemt: return ServiceType::Other;
case Proto::TProxy: return ServiceType::Other;
case Proto::WhatsAppProxy: return ServiceType::Other;
default: return ServiceType::Other;
}
}
@@ -116,7 +114,6 @@ int ProtocolUtils::getPortForInstall(Proto p)
case MtProxy:
case Telemt:
case TProxy:
case WhatsAppProxy:
default:
return defaultPort(p);
}
@@ -139,7 +136,6 @@ int ProtocolUtils::defaultPort(Proto p)
case Proto::MtProxy: return QString(protocols::mtProxy::defaultPort).toInt();
case Proto::Telemt: return QString(protocols::telemt::defaultPort).toInt();
case Proto::TProxy: return QString(protocols::tProxy::defaultPort).toInt();
case Proto::WhatsAppProxy: return QString(protocols::whatsAppProxy::defaultPort).toInt();
default: return -1;
}
}
@@ -161,7 +157,6 @@ bool ProtocolUtils::defaultPortChangeable(Proto p)
case Proto::MtProxy: return true;
case Proto::Telemt: return true;
case Proto::TProxy: return true;
case Proto::WhatsAppProxy: return true;
default: return false;
}
}
@@ -185,7 +180,6 @@ TransportProto ProtocolUtils::defaultTransportProto(Proto p)
case Proto::MtProxy: return TransportProto::Tcp;
case Proto::Telemt: return TransportProto::Tcp;
case Proto::TProxy: return TransportProto::Tcp;
case Proto::WhatsAppProxy: return TransportProto::Tcp;
default: return TransportProto::Udp;
}
}
@@ -208,7 +202,6 @@ bool ProtocolUtils::defaultTransportProtoChangeable(Proto p)
case Proto::MtProxy: return false;
case Proto::Telemt: return false;
case Proto::TProxy: return false;
case Proto::WhatsAppProxy: return false;
default: return false;
}
}
-14
View File
@@ -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;
-5
View File
@@ -89,11 +89,6 @@ protected:
void startTimeoutTimer();
void stopTimeoutTimer();
bool guardStop();
void resetStopGuard();
bool m_stopped = false;
Vpn::ConnectionState m_connectionState;
QString m_routeGateway;
+1 -4
View File
@@ -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();
}
+20 -14
View File
@@ -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) {
-4
View File
@@ -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
@@ -5,7 +5,6 @@
#include <QJsonValue>
#include <QSet>
#include <QUuid>
#include <QDebug>
#include "core/utils/serverConfigUtils.h"
#include "core/utils/constants/apiKeys.h"
@@ -143,43 +142,23 @@ void SecureServersRepository::persistDefaultServerFields()
void SecureServersRepository::loadFromStorage()
{
clearServerStateMaps();
m_unsupportedFormatConfigsCount = 0;
const QJsonArray serversArray =
QJsonDocument::fromJson(value(QStringLiteral("Servers/serversList"), QByteArray()).toByteArray())
.array();
QJsonArray persistedArray;
bool hasGeneratedIds = false;
for (int i = 0; i < serversArray.size(); ++i) {
const QJsonObject json = serversArray.at(i).toObject();
const QString candidateId = readStorageServerId(json);
const QString serverId = normalizedOrGeneratedServerId(candidateId);
const QJsonObject strippedJson = withoutStorageServerId(json);
if (!serverConfigUtils::isConfigFormatVersionSupported(strippedJson)) {
qWarning() << "Skipping stored server config with unsupported format version"
<< serverConfigUtils::configFormatVersion(strippedJson);
++m_unsupportedFormatConfigsCount;
persistedArray.append(json);
continue;
}
const serverConfigUtils::ConfigType kind = serverConfigUtils::configTypeFromJson(strippedJson);
if (m_serverJsonById.contains(serverId) || kind == serverConfigUtils::ConfigType::Invalid) {
persistedArray.append(json);
continue;
}
m_serverJsonById.insert(serverId, embedStorageServerId(serverId, strippedJson));
m_orderedServerIds.append(serverId);
persistedArray.append(m_serverJsonById.value(serverId));
hasGeneratedIds = hasGeneratedIds || serverId != candidateId;
}
// Per-server settings are keyed by id, so a generated one must survive restarts.
// Unlike syncToStorage(), this keeps entries that were skipped above
if (hasGeneratedIds) {
setValue(QStringLiteral("Servers/serversList"), QJsonDocument(persistedArray).toJson());
}
updateDefaultServerFromStorage();
@@ -205,11 +184,6 @@ void SecureServersRepository::invalidateCache()
loadFromStorage();
}
int SecureServersRepository::unsupportedFormatConfigsCount() const
{
return m_unsupportedFormatConfigsCount;
}
void SecureServersRepository::clearServers()
{
clearServerStateMaps();
@@ -248,8 +222,7 @@ QString SecureServersRepository::addServer(const QString &serverId, const QJsonO
return id;
}
const QJsonObject strippedJson = withoutStorageServerId(serverJson);
if (!serverConfigUtils::isConfigFormatVersionSupported(strippedJson)
|| serverConfigUtils::configTypeFromJson(strippedJson) != kind) {
if (serverConfigUtils::configTypeFromJson(strippedJson) != kind) {
return id;
}
m_serverJsonById.insert(id, embedStorageServerId(id, strippedJson));
@@ -336,15 +309,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;
@@ -54,8 +52,6 @@ public:
void invalidateCache();
int unsupportedFormatConfigsCount() const;
signals:
void serverAdded(const QString &serverId);
void serverEdited(const QString &serverId);
@@ -81,7 +77,6 @@ private:
QVector<QString> m_orderedServerIds;
QString m_defaultServerId;
int m_unsupportedFormatConfigsCount = 0;
};
#endif // SECURESERVERSREPOSITORY_H
+61 -46
View File
@@ -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,9 @@ bool apiUtils::isSubscriptionExpiringSoon(const QString &subscriptionEndDate, in
return endDate <= nowUtc.addDays(withinDays);
}
amnezia::ErrorCode apiUtils::checkApiResponseErrors(const QByteArray &responseBody)
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 +123,76 @@ 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;
}
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;
qDebug() << "something went wrong";
return amnezia::ErrorCode::ApiConfigDownloadError;
}
bool apiUtils::isPremiumServer(const QJsonObject &serverConfigObject)
+3 -1
View File
@@ -29,7 +29,9 @@ 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);
QString getPremiumV2VpnKey(const QJsonObject &serverConfigObject);
}
-308
View File
@@ -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 &regionId) const
{
for (const Region &region : m_regions) {
if (region.id == regionId) {
return !region.subregions.isEmpty();
}
}
return false;
}
bool Catalog::isSplit(const QString &regionId, int visibleCount, bool wasSplit) const
{
for (const Region &region : 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 &regionId, const QString &subregionId, int visibleCount, bool wasSplit) const
{
for (const Region &region : 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;
}
}
-98
View File
@@ -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> &regions() const;
const QVector<UseCase> &useCases() const;
const Entry *find(const QString &countryCode, const QString &isoCode) const;
bool hasSubregions(const QString &regionId) const;
bool isSplit(const QString &regionId, int visibleCount, bool wasSplit) const;
bool isSplit(const QString &regionId, 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
-1
View File
@@ -11,6 +11,5 @@
#define APP_IOS_BUNDLE_ID "@CLIENT_IOS_BUNDLE_ID@"
#define APP_IOS_STORE_URL_FALLBACK "@CLIENT_IOS_STORE_URL_FALLBACK@"
#define APP_ANDROID_PACKAGE "@CLIENT_ANDROID_PACKAGE@"
#define CLIENT_ENABLE_APP_UPDATES @CLIENT_ENABLE_APP_UPDATES@
#endif
+1 -4
View File
@@ -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
-2
View File
@@ -45,8 +45,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");
+3 -7
View File
@@ -25,7 +25,6 @@ namespace amnezia
constexpr QLatin1String accessToken("api_key");
constexpr QLatin1String config("config");
constexpr QLatin1String configVersion("config_version");
constexpr QLatin1String formatVersion("format_version");
constexpr QLatin1String sendPayload("send_payload");
constexpr QLatin1String sendPayloadEndpoint("endpoint");
@@ -144,7 +143,6 @@ namespace amnezia
constexpr QLatin1String mtproxy("mtproxy");
constexpr QLatin1String telemt("telemt");
constexpr QLatin1String tproxy("tproxy");
constexpr QLatin1String whatsappproxy("whatsappproxy");
constexpr QLatin1String splitTunnelSites("splitTunnelSites");
constexpr QLatin1String splitTunnelType("splitTunnelType");
@@ -179,8 +177,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
@@ -219,11 +215,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");
+8 -114
View File
@@ -48,15 +48,11 @@ 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 char defaultSite[] = "www.googletagmanager.com";
constexpr char defaultPort[] = "443";
@@ -64,16 +60,14 @@ namespace amnezia
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[] = "hellochrome_120";
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[] = "";
@@ -81,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";
@@ -94,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";
@@ -130,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
@@ -413,16 +317,6 @@ namespace amnezia
constexpr int maxWorkers = 32;
}
namespace whatsAppProxy
{
constexpr char mediaPortKey[] = "whatsapp_proxy_media_port";
constexpr char publicHostKey[] = "whatsapp_proxy_public_host";
constexpr char isEnabledKey[] = "whatsapp_proxy_is_enabled";
constexpr char defaultPort[] = "443";
constexpr char defaultMediaPort[] = "587";
}
} // namespace protocols
}
-1
View File
@@ -29,7 +29,6 @@ namespace amnezia
MtProxy,
Telemt,
TProxy,
WhatsAppProxy,
};
Q_ENUM_NS(DockerContainer)
} // namespace ContainerEnumNS
@@ -80,7 +80,6 @@ QMap<DockerContainer, QString> ContainerUtils::containerHumanNames()
{ DockerContainer::MtProxy, QObject::tr("MTProxy (Telegram)") },
{ DockerContainer::Telemt, QObject::tr("Telemt (Telegram)") },
{ DockerContainer::TProxy, QObject::tr("TProxy (Telegram WEB)") },
{ DockerContainer::WhatsAppProxy, QObject::tr("WhatsApp proxy") },
};
}
@@ -121,8 +120,6 @@ QMap<DockerContainer, QString> ContainerUtils::containerDescriptions()
QObject::tr("Telegram MTProto proxy (Telemt, Rust)") },
{ DockerContainer::TProxy,
QObject::tr("Telegram WEB proxy (tproxy-server)") },
{ DockerContainer::WhatsAppProxy,
QObject::tr("WhatsApp chat and media proxy") },
};
}
@@ -206,11 +203,6 @@ QMap<DockerContainer, QString> ContainerUtils::containerDetailedDescriptions()
QObject::tr("Telegram WEB proxy. Clients connect over HTTPS to a hostname; "
"the server relays traffic to official MTProxy. Requires a domain, "
"ports 80 and 443, and a WEB-capable Telegram app.") },
{ DockerContainer::WhatsAppProxy,
QObject::tr("WhatsApp proxy based on the official Meta implementation. "
"Relays chat traffic through the chat port and media through the media port. "
"Users enter the server address and both ports in WhatsApp settings. "
"Calls are not proxied.") },
};
}
@@ -243,7 +235,6 @@ Proto ContainerUtils::defaultProtocol(DockerContainer c)
case DockerContainer::MtProxy: return Proto::MtProxy;
case DockerContainer::Telemt: return Proto::Telemt;
case DockerContainer::TProxy: return Proto::TProxy;
case DockerContainer::WhatsAppProxy: return Proto::WhatsAppProxy;
default: return Proto::Unknown;
}
}
@@ -274,7 +265,6 @@ bool ContainerUtils::isSupportedByCurrentPlatform(DockerContainer c)
case DockerContainer::MtProxy: return true;
case DockerContainer::Telemt: return true;
case DockerContainer::TProxy: return true;
case DockerContainer::WhatsAppProxy: return true;
default:
return false;
}
@@ -293,7 +283,6 @@ bool ContainerUtils::isSupportedByCurrentPlatform(DockerContainer c)
case DockerContainer::MtProxy: return true;
case DockerContainer::Telemt: return true;
case DockerContainer::TProxy: return true;
case DockerContainer::WhatsAppProxy: return true;
default:
return false;
}
@@ -315,7 +304,6 @@ bool ContainerUtils::isSupportedByCurrentPlatform(DockerContainer c)
case DockerContainer::MtProxy: return true;
case DockerContainer::Telemt: return true;
case DockerContainer::TProxy: return true;
case DockerContainer::WhatsAppProxy: return true;
default: return false;
}
@@ -385,7 +373,6 @@ bool ContainerUtils::isShareable(DockerContainer container)
case DockerContainer::MtProxy: return false;
case DockerContainer::Telemt: return false;
case DockerContainer::TProxy: return false;
case DockerContainer::WhatsAppProxy: return false;
default: return true;
}
}
@@ -422,7 +409,6 @@ int ContainerUtils::installPageOrder(DockerContainer container)
case DockerContainer::MtProxy:
case DockerContainer::Telemt:
case DockerContainer::TProxy:
case DockerContainer::WhatsAppProxy:
return 20;
default: return 0;
}
+1 -12
View File
@@ -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,
@@ -90,9 +82,6 @@ namespace amnezia
RestoreBackupInvalidError = 904,
LegacyApiV1NotSupportedError = 905,
LegacyContainerNotSupportedError = 906,
ConfigFormatVersionNotSupportedError = 907,
RestoreBackupUnsupportedConfigsSkipped = 908,
ImportXraySettingsFileUseXrayPage = 909,
// Android errors
AndroidError = 1000,
+1 -40
View File
@@ -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,11 +69,8 @@ QString errorString(ErrorCode code) {
case (ErrorCode::ImportInvalidConfigError): errorMessage = QObject::tr("The config does not contain any containers and credentials for connecting to the server"); break;
case (ErrorCode::ImportBackupFileUseRestoreInstead): errorMessage = QObject::tr("Backup files cannot be imported here. Use 'Restore from backup' instead."); break;
case (ErrorCode::ImportXraySettingsFileUseXrayPage): errorMessage = QObject::tr("This is an XRay settings file, not a connection config. Import it on the XRay Configurations page using 'Import settings'."); break;
case (ErrorCode::RestoreBackupInvalidError): errorMessage = QObject::tr("Backup file is corrupted or has invalid format"); break;
case (ErrorCode::LegacyApiV1NotSupportedError): errorMessage = QObject::tr("This legacy Amnezia subscription format is no longer supported"); break;
case (ErrorCode::ConfigFormatVersionNotSupportedError): errorMessage = QObject::tr("This configuration was created in a newer version of the application and is not fully supported. Please update the application"); break;
case (ErrorCode::RestoreBackupUnsupportedConfigsSkipped): errorMessage = QObject::tr("Some configurations from the backup were not restored because they require a newer version of the application"); break;
case (ErrorCode::LegacyContainerNotSupportedError): errorMessage = QObject::tr("This protocol is no longer supported. Please select another protocol or remove this container from the server settings."); break;
case (ErrorCode::ImportOpenConfigError): errorMessage = QObject::tr("Unable to open config file"); break;
case (ErrorCode::NoInstalledContainersError): errorMessage = QObject::tr("VPN Protocols is not installed.\n Please install VPN container at first"); break;
@@ -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;
}
-16
View File
@@ -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
-1
View File
@@ -34,7 +34,6 @@ namespace amnezia
MtProxy,
Telemt,
TProxy,
WhatsAppProxy,
};
Q_ENUM_NS(Proto)
@@ -22,7 +22,6 @@
#include "core/models/protocols/mtProxyProtocolConfig.h"
#include "core/models/protocols/telemtProtocolConfig.h"
#include "core/models/protocols/tProxyProtocolConfig.h"
#include "core/models/protocols/whatsAppProxyProtocolConfig.h"
using namespace amnezia;
using namespace ProtocolUtils;
@@ -44,7 +43,6 @@ QString amnezia::scriptFolder(amnezia::DockerContainer container)
case DockerContainer::MtProxy: return QLatin1String("mtproxy");
case DockerContainer::Telemt: return QLatin1String("telemt");
case DockerContainer::TProxy: return QLatin1String("tproxy");
case DockerContainer::WhatsAppProxy: return QLatin1String("whatsapp_proxy");
default: return QString();
}
}
@@ -62,7 +60,6 @@ QString amnezia::scriptName(SharedScriptType type)
case SharedScriptType::check_connection: return QLatin1String("check_connection.sh");
case SharedScriptType::check_server_is_busy: return QLatin1String("check_server_is_busy.sh");
case SharedScriptType::check_user_in_sudo: return QLatin1String("check_user_in_sudo.sh");
case SharedScriptType::update_server_packages: return QLatin1String("update_server_packages.sh");
default: return QString();
}
}
@@ -77,6 +74,7 @@ QString amnezia::scriptName(ProtocolScriptType type)
case ProtocolScriptType::openvpn_template: return QLatin1String("template.ovpn");
case ProtocolScriptType::wireguard_template: return QLatin1String("template.conf");
case ProtocolScriptType::awg_template: return QLatin1String("template.conf");
case ProtocolScriptType::xray_template: return QLatin1String("template.json");
default: return QString();
}
}
@@ -430,18 +428,6 @@ amnezia::ScriptVars amnezia::genTProxyVars(const ContainerConfig &containerConfi
return vars;
}
amnezia::ScriptVars amnezia::genWhatsAppProxyVars(const ContainerConfig &containerConfig)
{
ScriptVars vars;
if (auto *whatsAppProxyConfig = containerConfig.getWhatsAppProxyProtocolConfig()) {
vars.append({ { "$WHATSAPP_PROXY_CHAT_PORT", whatsAppProxyConfig->effectivePort() } });
vars.append({ { "$WHATSAPP_PROXY_MEDIA_PORT", whatsAppProxyConfig->effectiveMediaPort() } });
}
return vars;
}
amnezia::ScriptVars amnezia::genProtocolVarsForContainer(DockerContainer container, const ContainerConfig &containerConfig)
{
ScriptVars vars;
@@ -475,9 +461,6 @@ amnezia::ScriptVars amnezia::genProtocolVarsForContainer(DockerContainer contain
case Proto::TProxy:
vars.append(genTProxyVars(containerConfig));
break;
case Proto::WhatsAppProxy:
vars.append(genWhatsAppProxyVars(containerConfig));
break;
default:
break;
}
@@ -28,8 +28,7 @@ enum SharedScriptType {
setup_host_firewall,
check_connection,
check_server_is_busy,
check_user_in_sudo,
update_server_packages
check_user_in_sudo
};
enum ProtocolScriptType {
// Protocol scripts
@@ -39,7 +38,8 @@ enum ProtocolScriptType {
container_startup,
openvpn_template,
wireguard_template,
awg_template
awg_template,
xray_template
};
enum ClientScriptType {
@@ -71,7 +71,6 @@ ScriptVars genSocks5ProxyVars(const ContainerConfig &containerConfig);
ScriptVars genMtProxyVars(const ContainerConfig &containerConfig);
ScriptVars genTelemtVars(const ContainerConfig &containerConfig);
ScriptVars genTProxyVars(const ContainerConfig &containerConfig);
ScriptVars genWhatsAppProxyVars(const ContainerConfig &containerConfig);
ScriptVars genProtocolVarsForContainer(DockerContainer container, const ContainerConfig &containerConfig);
}
+1 -2
View File
@@ -104,8 +104,7 @@ ErrorCode SshSession::runContainerScript(const ServerCredentials &credentials, D
return e;
const bool useSh = container == DockerContainer::Socks5Proxy || container == DockerContainer::MtProxy
|| container == DockerContainer::Telemt || container == DockerContainer::TProxy
|| container == DockerContainer::WhatsAppProxy;
|| container == DockerContainer::Telemt || container == DockerContainer::TProxy;
QString runner = QString("sudo docker exec -i $CONTAINER_NAME %2 %1 ").arg(fileName, useSh ? "sh" : "bash");
e = runScript(credentials, replaceVars(runner, amnezia::genBaseVars(credentials, container, QString(), QString())), cbReadStdOut, cbReadStdErr);
-10
View File
@@ -34,16 +34,6 @@ bool hasThirdPartyConfig(const QJsonObject &json)
namespace serverConfigUtils
{
int configFormatVersion(const QJsonObject &serverConfigObject)
{
return serverConfigObject.value(amnezia::configKey::formatVersion).toInt(0);
}
bool isConfigFormatVersionSupported(const QJsonObject &serverConfigObject)
{
return configFormatVersion(serverConfigObject) <= currentConfigFormatVersion;
}
bool isServerFromApi(const QJsonObject &serverConfigObject)
{
const int configVersion = serverConfigObject.value(amnezia::configKey::configVersion).toInt();
-6
View File
@@ -25,12 +25,6 @@ enum ConfigSource {
AmneziaGateway
};
constexpr int currentConfigFormatVersion = 1;
int configFormatVersion(const QJsonObject &serverConfigObject);
bool isConfigFormatVersionSupported(const QJsonObject &serverConfigObject);
bool isServerFromApi(const QJsonObject &serverConfigObject);
ConfigSource getConfigSource(const QJsonObject &serverConfigObject);
Loaded 100 of 535 files, more files were not shown because too many files have changed in this diff. Show more