mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-11-11 23:14:22 +09:00
Compare commits
48
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc02c8910b | ||
|
|
ac175c8a81 | ||
|
|
3f6ecacff1 | ||
|
|
7f9b7db482 | ||
|
|
6c0e62753d | ||
|
|
da8ce1809b | ||
|
|
94ea5b00aa | ||
|
|
c18f7773e3 | ||
|
|
95501dba35 | ||
|
|
4cacfb41ab | ||
|
|
8a51177a44 | ||
|
|
3121775a6f | ||
|
|
3977447d4a | ||
|
|
bf95867a7c | ||
|
|
a55ef420e8 | ||
|
|
3b5a93889f | ||
|
|
02838a94d5 | ||
|
|
9a1200d6f3 | ||
|
|
ddde43635e | ||
|
|
ffba1cb2a4 | ||
|
|
30ff3451c7 | ||
|
|
db951e9928 | ||
|
|
a36399a4c3 | ||
|
|
8940f8e273 | ||
|
|
705a373d51 | ||
|
|
3d4a45246a | ||
|
|
11355e1de0 | ||
|
|
3e8077f2ca | ||
|
|
c52b1a50a8 | ||
|
|
2c0f169946 | ||
|
|
693d00ded5 | ||
|
|
f1af8efb4b | ||
|
|
4c2e950864 | ||
|
|
9017984b1d | ||
|
|
c5ecf6281f | ||
|
|
3e9b70a5f1 | ||
|
|
94b51df247 | ||
|
|
de93650a90 | ||
|
|
448baad3e1 | ||
|
|
aca1b7dc5c | ||
|
|
de36a81b58 | ||
|
|
76b81b43c1 | ||
|
|
bea2f5f7db | ||
|
|
a4d38b5642 | ||
|
|
b76c88bba3 | ||
|
|
eab14fd957 | ||
|
|
2a8242d306 | ||
|
|
e60842bd9d |
No files matched your search
+228
-113
@@ -5,38 +5,8 @@ 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:
|
||||
@@ -118,7 +88,7 @@ jobs:
|
||||
host: 'linux'
|
||||
target: 'desktop'
|
||||
arch: 'linux_gcc_64'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
|
||||
dir: ${{ runner.temp }}
|
||||
setup-python: 'true'
|
||||
tools: 'tools_ifw'
|
||||
@@ -148,6 +118,7 @@ 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'
|
||||
@@ -238,7 +209,7 @@ jobs:
|
||||
host: 'windows'
|
||||
target: 'desktop'
|
||||
arch: 'win64_msvc2022_64'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
|
||||
dir: ${{ runner.temp }}
|
||||
setup-python: 'true'
|
||||
tools: 'tools_ifw'
|
||||
@@ -279,6 +250,7 @@ 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
|
||||
@@ -352,7 +324,7 @@ jobs:
|
||||
# ------------------------------------------------------
|
||||
|
||||
Build-iOS:
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-26
|
||||
|
||||
needs: Bake-Prebuilts-iOS
|
||||
if: ${{ always() }}
|
||||
@@ -500,7 +472,7 @@ jobs:
|
||||
# ------------------------------------------------------
|
||||
|
||||
Build-MacOS:
|
||||
runs-on: macos-15
|
||||
runs-on: macos-26
|
||||
|
||||
needs: Bake-Prebuilts-MacOS
|
||||
if: ${{ always() }}
|
||||
@@ -550,13 +522,13 @@ jobs:
|
||||
xcode-version: '26.3.0'
|
||||
|
||||
- name: 'Install Qt'
|
||||
uses: jurplel/install-qt-action@v4
|
||||
uses: jurplel/install-qt-action@v4.3.1
|
||||
with:
|
||||
version: ${{ env.QT_VERSION }}
|
||||
host: 'mac'
|
||||
target: 'desktop'
|
||||
arch: 'clang_64'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
|
||||
dir: ${{ runner.temp }}
|
||||
setup-python: 'true'
|
||||
set-env: 'true'
|
||||
@@ -582,29 +554,10 @@ 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:
|
||||
@@ -718,7 +671,7 @@ jobs:
|
||||
version: ${{ env.QT_VERSION }}
|
||||
host: 'mac'
|
||||
target: 'desktop'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools qtmultimedia'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools qtmultimedia qthttpserver qtwebsockets'
|
||||
arch: 'clang_64'
|
||||
dir: ${{ runner.temp }}
|
||||
set-env: 'true'
|
||||
@@ -865,7 +818,7 @@ jobs:
|
||||
submodules: 'true'
|
||||
|
||||
- name: 'Install desktop Qt'
|
||||
uses: jurplel/install-qt-action@v4
|
||||
uses: jurplel/install-qt-action@v4.3.1
|
||||
with:
|
||||
version: ${{ env.QT_VERSION }}
|
||||
host: 'linux'
|
||||
@@ -877,10 +830,10 @@ jobs:
|
||||
extra: '--base ${{ env.QT_MIRROR }}'
|
||||
|
||||
- name: 'Install android_x86_64 Qt'
|
||||
uses: jurplel/install-qt-action@v4
|
||||
uses: jurplel/install-qt-action@v4.3.1
|
||||
with:
|
||||
version: ${{ env.QT_VERSION }}
|
||||
host: 'linux'
|
||||
host: 'all_os'
|
||||
target: 'android'
|
||||
arch: 'android_x86_64'
|
||||
modules: ${{ env.QT_MODULES }}
|
||||
@@ -889,10 +842,10 @@ jobs:
|
||||
extra: '--base ${{ env.QT_MIRROR }}'
|
||||
|
||||
- name: 'Install android_x86 Qt'
|
||||
uses: jurplel/install-qt-action@v4
|
||||
uses: jurplel/install-qt-action@v4.3.1
|
||||
with:
|
||||
version: ${{ env.QT_VERSION }}
|
||||
host: 'linux'
|
||||
host: 'all_os'
|
||||
target: 'android'
|
||||
arch: 'android_x86'
|
||||
modules: ${{ env.QT_MODULES }}
|
||||
@@ -901,10 +854,10 @@ jobs:
|
||||
extra: '--base ${{ env.QT_MIRROR }}'
|
||||
|
||||
- name: 'Install android_armv7 Qt'
|
||||
uses: jurplel/install-qt-action@v4
|
||||
uses: jurplel/install-qt-action@v4.3.1
|
||||
with:
|
||||
version: ${{ env.QT_VERSION }}
|
||||
host: 'linux'
|
||||
host: 'all_os'
|
||||
target: 'android'
|
||||
arch: 'android_armv7'
|
||||
modules: ${{ env.QT_MODULES }}
|
||||
@@ -913,10 +866,10 @@ jobs:
|
||||
extra: '--base ${{ env.QT_MIRROR }}'
|
||||
|
||||
- name: 'Install android_arm64_v8a Qt'
|
||||
uses: jurplel/install-qt-action@v4
|
||||
uses: jurplel/install-qt-action@v4.3.1
|
||||
with:
|
||||
version: ${{ env.QT_VERSION }}
|
||||
host: 'linux'
|
||||
host: 'all_os'
|
||||
target: 'android'
|
||||
arch: 'android_arm64_v8a'
|
||||
modules: ${{ env.QT_MODULES }}
|
||||
@@ -1096,18 +1049,39 @@ 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
|
||||
@@ -1118,12 +1092,16 @@ jobs:
|
||||
|
||||
- name: Decode backup file
|
||||
if: matrix.filter == 'BackupExportImportTest'
|
||||
shell: pwsh
|
||||
shell: bash
|
||||
env:
|
||||
BACKUP_FILE_BASE64: ${{ secrets.BACKUP_FILE_BASE64 }}
|
||||
run: |
|
||||
$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
|
||||
if [ -z "$BACKUP_FILE_BASE64" ]; then
|
||||
echo "::error::BACKUP_FILE_BASE64 secret is not set"
|
||||
exit 1
|
||||
fi
|
||||
echo "$BACKUP_FILE_BASE64" | base64 --decode > "$RUNNER_TEMP/test.backup"
|
||||
echo "BACKUP_PATH=$RUNNER_TEMP/test.backup" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install Qt
|
||||
uses: jurplel/install-qt-action@v3
|
||||
@@ -1132,7 +1110,7 @@ jobs:
|
||||
host: linux
|
||||
target: desktop
|
||||
arch: linux_gcc_64
|
||||
modules: qtremoteobjects qt5compat qtshadertools
|
||||
modules: qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets
|
||||
dir: ${{ runner.temp }}
|
||||
set-env: true
|
||||
aqtversion: '==3.3.0'
|
||||
@@ -1155,20 +1133,118 @@ 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="$QT_ROOT/lib:${LD_LIBRARY_PATH:-}"
|
||||
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 -R "^${{ matrix.filter }}$"
|
||||
ctest --test-dir deploy/build/client/tests --output-on-failure -VV \
|
||||
--timeout 3600 \
|
||||
-R "^(SelfHostedServerSetupTest|MultipleExportsTest|AdminSelfHostedExportTest|XrayVpsMatrixTest)$"
|
||||
|
||||
# ------------------------------------------------------
|
||||
|
||||
@@ -1190,18 +1266,39 @@ 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
|
||||
@@ -1213,9 +1310,15 @@ 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("${{ secrets.BACKUP_FILE_BASE64 }}")
|
||||
$bytes = [Convert]::FromBase64String($env:BACKUP_FILE_BASE64)
|
||||
[IO.File]::WriteAllBytes($backup, $bytes)
|
||||
"BACKUP_PATH=$backup" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
|
||||
|
||||
@@ -1232,7 +1335,7 @@ jobs:
|
||||
host: windows
|
||||
target: desktop
|
||||
arch: win64_msvc2022_64
|
||||
modules: qtremoteobjects qt5compat qtshadertools
|
||||
modules: qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets
|
||||
dir: ${{ runner.temp }}
|
||||
set-env: true
|
||||
|
||||
@@ -1241,7 +1344,20 @@ 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
|
||||
@@ -1251,7 +1367,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
|
||||
ctest --test-dir deploy\build\client\tests -C Release -R "${{ matrix.filter }}" --output-on-failure -VV --timeout 600
|
||||
|
||||
# ------------------------------------------------------
|
||||
|
||||
@@ -1278,13 +1394,30 @@ 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
|
||||
@@ -1300,12 +1433,16 @@ jobs:
|
||||
|
||||
- name: Decode backup file
|
||||
if: matrix.filter == 'BackupExportImportTest'
|
||||
shell: pwsh
|
||||
shell: bash
|
||||
env:
|
||||
BACKUP_FILE_BASE64: ${{ secrets.BACKUP_FILE_BASE64 }}
|
||||
run: |
|
||||
$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
|
||||
if [ -z "$BACKUP_FILE_BASE64" ]; then
|
||||
echo "::error::BACKUP_FILE_BASE64 secret is not set"
|
||||
exit 1
|
||||
fi
|
||||
echo "$BACKUP_FILE_BASE64" | base64 --decode > "$RUNNER_TEMP/test.backup"
|
||||
echo "BACKUP_PATH=$RUNNER_TEMP/test.backup" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
@@ -1320,41 +1457,27 @@ jobs:
|
||||
host: mac
|
||||
target: desktop
|
||||
arch: clang_64
|
||||
modules: qtremoteobjects qt5compat qtshadertools
|
||||
modules: qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets
|
||||
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/openssl:$QT_ROOT/lib:$DYLD_LIBRARY_PATH"
|
||||
export DYLD_LIBRARY_PATH="$PWD/deploy/build/client/tests:$QT_ROOT/lib:${DYLD_LIBRARY_PATH:-}"
|
||||
|
||||
# export QT_QPA_PLATFORM=offscreen
|
||||
|
||||
ctest --test-dir deploy/build/client/tests -R "${{ matrix.filter }}" --output-on-failure
|
||||
ctest --test-dir deploy/build/client/tests -R "${{ matrix.filter }}" --output-on-failure -VV --timeout 600
|
||||
|
||||
# ------------------------------------------------------
|
||||
|
||||
@@ -1396,7 +1519,7 @@ jobs:
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
version: ${{ env.QT_VERSION }}
|
||||
host: 'linux'
|
||||
host: 'all_os'
|
||||
target: 'android'
|
||||
arch: 'android_x86_64'
|
||||
modules: ${{ env.QT_MODULES }}
|
||||
@@ -1441,7 +1564,8 @@ jobs:
|
||||
-DANDROID_PLATFORM=${{ env.ANDROID_PLATFORM }} \
|
||||
-DCMAKE_ANDROID_NDK=$CMAKE_ANDROID_NDK \
|
||||
-DQT_HOST_PATH=$QT_HOST \
|
||||
-DANDROID_SDK_ROOT=$ANDROID_HOME
|
||||
-DANDROID_SDK_ROOT=$ANDROID_HOME \
|
||||
-DAMNEZIA_BUILD_TESTS=ON
|
||||
|
||||
- name: 'Build Android tests'
|
||||
# env:
|
||||
@@ -1514,15 +1638,6 @@ 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:
|
||||
|
||||
@@ -146,3 +146,4 @@ macos-with-sign-build.sh
|
||||
DeveloperIdApplicationCertificate.p12
|
||||
DeveloperIdInstallerCertificate.p12
|
||||
|
||||
gwenv.sh
|
||||
@@ -0,0 +1,219 @@
|
||||
# AGENTS.md
|
||||
|
||||
Operating guide for this repository. Read it before changing anything.
|
||||
|
||||
## Project Overview
|
||||
|
||||
AmneziaVPN is a cross-platform VPN client built with C++ and Qt Quick (QML) for
|
||||
Windows, macOS, Linux, Android and iOS. It has three kinds of servers, each
|
||||
with its own config model (`serverConfigUtils::ConfigType`):
|
||||
|
||||
- **Self-hosted.** The admin connects the app to their own server over SSH; the
|
||||
app installs protocols as Docker containers (AmneziaWG, WireGuard, OpenVPN,
|
||||
XRay, IPsec and others) and generates client configs (`SelfHostedAdmin`).
|
||||
Access is shared with other users as `vpn://` keys, which import without
|
||||
server management rights (`SelfHostedUser`).
|
||||
- **Amnezia Premium and Free.** Configs come from the Amnezia gateway API. The
|
||||
gateway passes the backend's config through; the client does not invent
|
||||
server-side values.
|
||||
- **Native protocol configs.** Third-party configs imported as they are:
|
||||
WireGuard and AmneziaWG `.conf`, OpenVPN `.ovpn`, XRay and Shadowsocks links
|
||||
(`Native`). The app only connects with them; there is no server to manage.
|
||||
|
||||
A change to shared code (protocol configs, import, connection) must keep
|
||||
working for all three.
|
||||
|
||||
The desktop builds run a privileged service next to the GUI; the GUI talks to
|
||||
it over Qt Remote Objects. Android, iOS and the macOS App Store build
|
||||
(`MACOS_NE`) run the tunnel in the platform's VPN extension instead.
|
||||
|
||||
## Repository Layout
|
||||
|
||||
- `client/core/` — business logic with no UI: controllers, configurators
|
||||
(client config generation per protocol), installers (server-side setup over
|
||||
SSH), protocol and server config models, repositories (encrypted settings
|
||||
storage).
|
||||
- `client/ui/controllers/` — `*UiController` classes exposed to QML. Thin
|
||||
adapters over `core/`: they translate between QML and core types and hold no
|
||||
business logic of their own.
|
||||
- `client/ui/models/` — list and property models for QML.
|
||||
- `client/ui/qml/` — the interface:
|
||||
- `Pages2/` — screens, one file per page.
|
||||
- `Components/` — composite pieces of screens.
|
||||
- `Controls2/` — design system controls, named `<Name>Type.qml`; text styles
|
||||
live in `Controls2/TextTypes/`.
|
||||
- `Modules/Style/AmneziaStyle.qml` — design tokens (colours and the like).
|
||||
- `Config/` — `GlobalConfig`, `Dialogs` and other singletons.
|
||||
- `client/platforms/`, `client/android/`, `client/ios/`, `client/macos/` —
|
||||
platform code: VPN extensions, Kotlin, Swift, Objective-C++.
|
||||
- `client/daemon/`, `client/mozilla/`, `service/`, `ipc/` — the privileged
|
||||
service and its IPC.
|
||||
- `client/server_scripts/` — the shell scripts and Dockerfiles run on the
|
||||
user's server.
|
||||
- `client/translations/` — Qt `.ts` files.
|
||||
- `client/tests/` — Qt Test suites, run through `ctest`.
|
||||
- `recipes/` — Conan recipes for native dependencies (protocol libraries,
|
||||
OpenSSL, libssh and others).
|
||||
- `deploy/` — build and installer scripts. `.github/workflows/` — CI.
|
||||
- `client/3rd/` — vendored third-party code. Do not edit it.
|
||||
|
||||
## Essential Commands
|
||||
|
||||
Run from the repository root.
|
||||
|
||||
```bash
|
||||
deploy/build.sh -h # build options
|
||||
deploy/build.sh # build for the host platform
|
||||
deploy/build.sh -t android --aab # Android APK and AAB
|
||||
cmake -S . -B build -DCMAKE_PREFIX_PATH=<Qt>/<platform> -DAMNEZIA_BUILD_TESTS=ON
|
||||
cmake --build build --parallel
|
||||
ctest --test-dir build/client/tests --output-on-failure
|
||||
```
|
||||
|
||||
Qt 6.10 or later and Conan are required; native dependencies are built by Conan
|
||||
from `recipes/` on the first configure.
|
||||
|
||||
## Repository Rules
|
||||
|
||||
These three come first. Each one has been asked for repeatedly.
|
||||
|
||||
### 1. Clean, self-documenting code, no comments
|
||||
|
||||
- Names and structure carry the meaning. Code must be readable without help.
|
||||
- No explanatory comments in code: no `//` notes in QML or C++, no doc blocks.
|
||||
- The one exception is a hack that looks wrong on purpose: a workaround for a
|
||||
platform or Qt bug, an ordering that must not be "fixed". Then a short comment
|
||||
next to it says why.
|
||||
- Explanations of a change go into the chat summary or the commit message, not
|
||||
into the code.
|
||||
- Leave existing comments alone unless the change removes the code they
|
||||
describe.
|
||||
|
||||
### 2. Reusable code (DRY), without abstraction for its own sake
|
||||
|
||||
- **QML.** Every reusable component lives in its own file. A piece of a page
|
||||
goes into its own file in `Components/` even when it is used once, if that
|
||||
keeps the page short and readable. No inline `component` definitions or
|
||||
`Component` blocks inside pages. Before writing a new control, look in
|
||||
`Controls2/` and `Components/` for one that already does the job, and extend
|
||||
it if it almost does.
|
||||
- **C++.** Logic shared by several places is extracted once and reused:
|
||||
duplicated parsing, validation or config building is a bug waiting to diverge.
|
||||
- **No fanaticism.** Do not create a base class, interface, helper or file
|
||||
that only renames a single call, has one trivial user, or exists "for the
|
||||
future". Decompose when it makes the code shorter, clearer or removes
|
||||
duplication; otherwise keep it direct.
|
||||
|
||||
### 3. Do not add what was not asked for
|
||||
|
||||
- Implement exactly the request. No extra screens, buttons, settings,
|
||||
refactors, renames, translations or "while I'm here" fixes.
|
||||
- Ideas, improvements and problems noticed along the way are reported to the
|
||||
user as suggestions. Nothing is implemented without the user agreeing first.
|
||||
- When a request is ambiguous, ask instead of guessing big.
|
||||
- Keep the diff small. If a fix grows beyond the obvious change, say so and
|
||||
explain why before making it.
|
||||
|
||||
### Other rules
|
||||
|
||||
- **Committing is the user's call.** Prepare the change and a commit message;
|
||||
do not commit or push unless asked. Commit messages use a conventional prefix
|
||||
with a scope (`fix(ui): …`, `feat(macos): …`, `chore: …`): a one-line
|
||||
subject plus one or two short body lines saying what was wrong and what
|
||||
changed. Pull requests target `dev`.
|
||||
- **Format C++ with `.clang-format`.** Format the lines you touch; do not
|
||||
reformat files the change does not otherwise touch.
|
||||
- **Delete unused code** instead of keeping it "for later".
|
||||
- **Do not edit `client/3rd/`.** Fix vendored code through its upstream or a
|
||||
Conan recipe.
|
||||
|
||||
## UI And Design System
|
||||
|
||||
- **Figma is the source of truth.** Screens follow the "Amnezia APP VPN" file;
|
||||
components follow the "Amnezia DS Library". Where the two disagree, the app
|
||||
screens win.
|
||||
- **Build UI from the design system controls** in `Controls2/`
|
||||
(`ButtonType`, `ListItemType`, `TextFieldType`, `SheetType`,
|
||||
`NavigationBarType` and the others) and the text types in
|
||||
`Controls2/TextTypes/`. Do not style raw Qt Quick Controls on a page.
|
||||
- **No hex colours in QML.** Every colour is a semantic token from
|
||||
`AmneziaStyle.color`. A colour the tokens do not have is added to
|
||||
`AmneziaStyle` first, with the name the design system gives it.
|
||||
- **Icons come from Figma**, exported from the DS Library. Never draw an icon
|
||||
by hand. Exports include page background rectangles — strip them and keep
|
||||
the `0 0 24 24` viewBox.
|
||||
- **Restyling a component never redesigns a screen.** A design system update
|
||||
changes components; screen layout and behaviour stay unless the user asks.
|
||||
- **No Qt5Compat.GraphicalEffects.** It overflows the interpreted JS stack on
|
||||
iOS. Use `QtQuick.Effects` (`MultiEffect`), and create effects only while
|
||||
they are visible: each instance is expensive to create.
|
||||
|
||||
### QML pitfalls
|
||||
|
||||
- A vertical `ListView` resets the delegate's `x`. Put insets on the
|
||||
ListView's `leftMargin`/`rightMargin`, or on a child inside the delegate.
|
||||
- `lineHeightMode: Text.FixedHeight` puts all the extra leading below the text,
|
||||
while Figma splits it evenly. The text types already compensate with
|
||||
`halfLeading` padding; a new text type must do the same.
|
||||
- QML is loaded from `qml.qrc`, not compiled ahead of time: a QML error shows
|
||||
up only at runtime. When moving QML between files, compare the `import`
|
||||
lines — types registered from C++ under their own URIs fail only when the
|
||||
page is opened.
|
||||
|
||||
## Translations
|
||||
|
||||
- Every user-visible string goes through `qsTr()` in QML or `tr()` in C++.
|
||||
- The `.ts` files contain only finished, non-empty translations: no
|
||||
`vanished`, `obsolete` or `unfinished` entries, and every language holds the
|
||||
same set of strings. `lrelease` must report `N finished and 0 unfinished` for
|
||||
every file.
|
||||
- Refresh `.ts` files only when the task is about translations. `lupdate` run
|
||||
through `qt6_add_translations` misses QML, so run it over an explicit file
|
||||
list that skips `client/3rd` (from `client/translations`):
|
||||
|
||||
```bash
|
||||
find .. -path ../3rd -prune -o -path ../3rd-prebuilt -prune -o \
|
||||
\( -name '*.qml' -o -name '*.cpp' -o -name '*.h' -o -name '*.mm' \) -print \
|
||||
| sort > /tmp/src.lst
|
||||
<Qt>/bin/lupdate @/tmp/src.lst -ts amneziavpn_*.ts
|
||||
```
|
||||
|
||||
- `lupdate` output differs between macOS and Windows (keychain strings swap
|
||||
places), so a large `.ts` diff after a refresh is often only that.
|
||||
- Technical terms stay in English in every language: protocol parameter names
|
||||
and their descriptions (`Jc`, `S1`, `H1`…), config keys, transport and
|
||||
algorithm names. Ordinary UI words are translated.
|
||||
- Renaming a QML file changes its translation context, so its strings have to
|
||||
be carried over.
|
||||
|
||||
## Which Check When
|
||||
|
||||
- **Touched C++** — build the affected targets, and run `ctest` when the change
|
||||
touches anything the tests cover (servers, imports, configs, settings,
|
||||
split tunneling, XRay).
|
||||
- **Touched QML** — build, then open every page you changed. If the app cannot
|
||||
be launched, at least load the changed files through the QML engine so that
|
||||
syntax, imports and type errors surface, and say what was not checked.
|
||||
- **Touched a protocol config, import or connection path** — check self-hosted,
|
||||
Premium/Free and native configs, and every platform that consumes the field
|
||||
(desktop service, Android, iOS/macOS NE).
|
||||
- **Touched CMake, recipes or the app icon** — reconfigure from a clean build
|
||||
directory as well: branding paths are CMake cache variables and keep stale
|
||||
values in an existing build directory.
|
||||
- **Touched translations** — `lrelease` on every `.ts` file, see above.
|
||||
|
||||
## Privacy
|
||||
|
||||
- Never commit or repeat private keys, `vpn://` keys, server addresses,
|
||||
passwords or tokens — including ones found in pasted configs, logs or test
|
||||
output.
|
||||
- Logs must not contain secrets or full config bodies.
|
||||
- Local absolute paths (`/Users/...`, `C:\...`) do not belong in repository
|
||||
files.
|
||||
|
||||
## Verification Culture
|
||||
|
||||
- Verify your own work before reporting it. "Should work" is not a result;
|
||||
say exactly what was built, run or opened, and what was not.
|
||||
- Hypotheses are settled by an experiment, not by reasoning about the code.
|
||||
- After a wrong diagnosis, remove the code it produced.
|
||||
@@ -0,0 +1,6 @@
|
||||
# CLAUDE.md
|
||||
|
||||
The operating guide for this repository lives in [AGENTS.md](AGENTS.md), so
|
||||
every agent runtime reads the same file.
|
||||
|
||||
@AGENTS.md
|
||||
+4
-2
@@ -5,7 +5,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
set(PROJECT AmneziaVPN)
|
||||
if(NOT AMNEZIAVPN_VERSION)
|
||||
set(AMNEZIAVPN_VERSION 5.0.2.1 CACHE STRING "Client app version")
|
||||
set(AMNEZIAVPN_VERSION 5.1.0.6 CACHE STRING "Client app version")
|
||||
endif()
|
||||
|
||||
set(QT_CREATOR_SKIP_PACKAGE_MANAGER_SETUP ON CACHE BOOL "" FORCE)
|
||||
@@ -20,6 +20,8 @@ project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
|
||||
HOMEPAGE_URL "https://amnezia.org/"
|
||||
)
|
||||
|
||||
option(AMNEZIA_BUILD_TESTS "Build test executables" OFF)
|
||||
|
||||
# trigger conan to kick off `conan install` globally
|
||||
find_package(OpenSSL REQUIRED)
|
||||
if (PREBUILTS_ONLY)
|
||||
@@ -32,7 +34,7 @@ set(RELEASE_DATE "${CURRENT_DATE}")
|
||||
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
|
||||
|
||||
# bump by 2 on every release, because we're releasing two versions on the play store
|
||||
set(APP_ANDROID_VERSION_CODE 2161)
|
||||
set(APP_ANDROID_VERSION_CODE 2190)
|
||||
if(DEFINED APP_ANDROID_VERSION_CODE_OFFSET)
|
||||
math(EXPR APP_ANDROID_VERSION_CODE "${APP_ANDROID_VERSION_CODE} + ${APP_ANDROID_VERSION_CODE_OFFSET}")
|
||||
endif()
|
||||
|
||||
+22
-7
@@ -37,21 +37,30 @@ add_definitions(-DFREE_V2_ENDPOINT="$ENV{FREE_V2_ENDPOINT}")
|
||||
add_definitions(-DPREM_V1_ENDPOINT="$ENV{PREM_V1_ENDPOINT}")
|
||||
|
||||
if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
|
||||
set(PACKAGES ${PACKAGES} Widgets)
|
||||
set(PACKAGES ${PACKAGES} Widgets HttpServer)
|
||||
endif()
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS ${PACKAGES})
|
||||
find_package(OpenSSL REQUIRED)
|
||||
|
||||
find_package(libagw REQUIRED)
|
||||
|
||||
set(LIBS ${LIBS}
|
||||
Qt6::Core Qt6::Gui
|
||||
Qt6::Network Qt6::Xml Qt6::RemoteObjects
|
||||
Qt6::Quick Qt6::Svg Qt6::QuickControls2
|
||||
Qt6::Core5Compat Qt6::Concurrent
|
||||
amnezia::libagw
|
||||
)
|
||||
|
||||
find_package(Qt6 QUIET OPTIONAL_COMPONENTS CorePrivate)
|
||||
if(TARGET Qt6::CorePrivate)
|
||||
set(LIBS ${LIBS} Qt6::CorePrivate)
|
||||
add_compile_definitions(AMNEZIA_HAS_QZIPWRITER)
|
||||
endif()
|
||||
|
||||
if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
|
||||
set(LIBS ${LIBS} Qt6::Widgets)
|
||||
set(LIBS ${LIBS} Qt6::Widgets Qt6::HttpServer)
|
||||
endif()
|
||||
|
||||
qt_standard_project_setup()
|
||||
@@ -71,6 +80,7 @@ endif()
|
||||
qt6_add_resources(QRC ${QRC}
|
||||
${CMAKE_CURRENT_LIST_DIR}/images/images.qrc
|
||||
${CMAKE_CURRENT_LIST_DIR}/images/flagKit.qrc
|
||||
${CMAKE_CURRENT_LIST_DIR}/resources/countries.qrc
|
||||
${CMAKE_CURRENT_LIST_DIR}/client_scripts/clientScripts.qrc
|
||||
${CLIENT_QML_QRC_FILE}
|
||||
${CMAKE_CURRENT_LIST_DIR}/server_scripts/serverScripts.qrc
|
||||
@@ -92,6 +102,9 @@ else()
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_hi_IN.ts
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_es_ES.ts
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_ko_KR.ts
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_fr_FR.ts
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_th_TH.ts
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_id_ID.ts
|
||||
)
|
||||
endif()
|
||||
|
||||
@@ -214,6 +227,10 @@ if(ANDROID)
|
||||
include(cmake/android.cmake)
|
||||
endif()
|
||||
|
||||
if(APPLE)
|
||||
include(cmake/appleAppIcon.cmake)
|
||||
endif()
|
||||
|
||||
if(IOS)
|
||||
include(cmake/ios.cmake)
|
||||
include(cmake/ios-arch-fixup.cmake)
|
||||
@@ -314,8 +331,6 @@ if(OVPN_SCRIPTS)
|
||||
)
|
||||
endif()
|
||||
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
|
||||
add_subdirectory(tests)
|
||||
if(AMNEZIA_BUILD_TESTS)
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
@@ -129,6 +129,11 @@ void AmneziaApplication::init()
|
||||
win->setPersistentSceneGraph(true);
|
||||
win->setPersistentGraphics(true);
|
||||
#endif
|
||||
#ifdef Q_OS_IOS
|
||||
auto pageController = m_coreController->pageController();
|
||||
pageController->setSafeAreaMargins(win->safeAreaMargins());
|
||||
QObject::connect(win, &QWindow::safeAreaMarginsChanged, pageController, &PageController::setSafeAreaMargins);
|
||||
#endif
|
||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
||||
win->show();
|
||||
#else
|
||||
@@ -179,7 +184,9 @@ void AmneziaApplication::init()
|
||||
|
||||
m_coreController->setQmlRoot();
|
||||
|
||||
#if CLIENT_ENABLE_APP_UPDATES
|
||||
m_coreController->checkForAppUpdates();
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_WIN //TODO
|
||||
if (m_parser.isSet(m_optAutostart))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="black">#FF0E0E11</color>
|
||||
<color name="black">#FF101012</color>
|
||||
<style name="NoActionBar">
|
||||
<item name="android:windowBackground">@color/black</item>
|
||||
<item name="android:colorBackground">@color/black</item>
|
||||
|
||||
@@ -37,6 +37,7 @@ import android.widget.Toast
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.OnApplyWindowInsetsListener
|
||||
import androidx.core.view.ViewCompat
|
||||
@@ -44,6 +45,7 @@ import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import kotlin.LazyThreadSafetyMode.NONE
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.text.RegexOption.IGNORE_CASE
|
||||
@@ -77,6 +79,7 @@ private const val CHECK_NOTIFICATION_PERMISSION_ACTION_CODE = 4
|
||||
private const val PREFS_NOTIFICATION_PERMISSION_ASKED = "NOTIFICATION_PERMISSION_ASKED"
|
||||
private const val OPEN_FILE_AFTER_RESUME_DELAY_MS = 400L
|
||||
private const val KEY_PENDING_OPEN_FILE_URI = "pending_open_file_uri"
|
||||
private const val FILE_PROVIDER_AUTHORITY = "org.amnezia.vpn.qtprovider"
|
||||
|
||||
class AmneziaActivity : QtActivity() {
|
||||
|
||||
@@ -733,28 +736,69 @@ class AmneziaActivity : QtActivity() {
|
||||
@Suppress("unused")
|
||||
fun saveFile(fileName: String, data: String) {
|
||||
Log.d(TAG, "Save file $fileName")
|
||||
createFile(fileName, "text/*") { os -> os.bufferedWriter().use { it.write(data) } }
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun saveFileBytes(fileName: String, data: ByteArray, mime: String) {
|
||||
Log.d(TAG, "Save file $fileName, ${data.size} bytes, $mime")
|
||||
createFile(fileName, mime) { os -> os.write(data) }
|
||||
}
|
||||
|
||||
private fun createFile(fileName: String, mime: String, write: (OutputStream) -> Unit) {
|
||||
mainScope.launch {
|
||||
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "text/*"
|
||||
type = mime
|
||||
putExtra(Intent.EXTRA_TITLE, fileName)
|
||||
}.also {
|
||||
try {
|
||||
startActivityForResult(it, CREATE_FILE_ACTION_CODE, ActivityResultHandler(
|
||||
onSuccess = {
|
||||
it?.data?.let { uri ->
|
||||
val uri = it?.data
|
||||
var saved = false
|
||||
if (uri != null) {
|
||||
Log.v(TAG, "Save file to $uri")
|
||||
try {
|
||||
contentResolver.openOutputStream(uri)?.use { os ->
|
||||
os.bufferedWriter().use { it.write(data) }
|
||||
write(os)
|
||||
saved = true
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Failed to save file $uri: $e")
|
||||
// todo: send error to Qt
|
||||
}
|
||||
}
|
||||
QtAndroidController.onFileSaved(saved, false)
|
||||
},
|
||||
onFail = {
|
||||
QtAndroidController.onFileSaved(false, true)
|
||||
}
|
||||
))
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
Toast.makeText(this@AmneziaActivity, "Unsupported", Toast.LENGTH_LONG).show()
|
||||
QtAndroidController.onFileSaved(false, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun shareFile(path: String, mime: String) {
|
||||
Log.d(TAG, "Share file $path, $mime")
|
||||
mainScope.launch {
|
||||
val uri = try {
|
||||
FileProvider.getUriForFile(this@AmneziaActivity, FILE_PROVIDER_AUTHORITY, File(path))
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log.e(TAG, "Failed to share file $path: $e")
|
||||
return@launch
|
||||
}
|
||||
Intent(Intent.ACTION_SEND).apply {
|
||||
type = mime
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}.also {
|
||||
try {
|
||||
startActivity(Intent.createChooser(it, null))
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
Toast.makeText(this@AmneziaActivity, "Unsupported", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
@@ -971,20 +1015,6 @@ class AmneziaActivity : QtActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun exportLogsFile(fileName: String) {
|
||||
Log.v(TAG, "Export logs file")
|
||||
saveFile(fileName, Log.getLogs())
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun clearLogs() {
|
||||
Log.v(TAG, "Clear logs")
|
||||
mainScope.launch {
|
||||
Log.clearLogs()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun setScreenshotsEnabled(enabled: Boolean) {
|
||||
Log.v(TAG, "Set screenshots enabled: $enabled")
|
||||
|
||||
@@ -22,6 +22,7 @@ object QtAndroidController {
|
||||
external fun onStatisticsUpdate(rxBytes: Long, txBytes: Long)
|
||||
|
||||
external fun onFileOpened(uri: String)
|
||||
external fun onFileSaved(success: Boolean, cancelled: Boolean)
|
||||
|
||||
external fun onConfigImported(data: String)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.amnezia.vpn.util
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Process
|
||||
@@ -22,9 +23,18 @@ import org.amnezia.vpn.util.Log.Priority.W
|
||||
import android.util.Log as NativeLog
|
||||
|
||||
private const val TAG = "Log"
|
||||
private const val LOG_FILE_NAME = "amneziaVPN.log"
|
||||
private const val ROTATE_LOG_FILE_NAME = "amneziaVPN.rotate.log"
|
||||
private const val APP_LOG_FILE_NAME = "app.log"
|
||||
private const val APP_ROTATE_LOG_FILE_NAME = "app.rotate.log"
|
||||
private const val TUNNEL_LOG_FILE_NAME = "tunnel.log"
|
||||
private const val TUNNEL_ROTATE_LOG_FILE_NAME = "tunnel.rotate.log"
|
||||
private const val LEGACY_LOG_FILE_NAME = "amneziaVPN.log"
|
||||
private const val LEGACY_ROTATE_LOG_FILE_NAME = "amneziaVPN.rotate.log"
|
||||
private const val LOCK_FILE_NAME = ".lock"
|
||||
private const val TILE_SERVICE_PROCESS_SUFFIX = ":amneziaTileService"
|
||||
private const val AWG_SERVICE_PROCESS_SUFFIX = ":amneziaAwgService"
|
||||
private const val AWG_LOGCAT_TAG_PREFIX = " AmneziaWG/"
|
||||
private const val STREAM_APP = 0
|
||||
private const val STREAM_TUNNEL = 1
|
||||
private const val DATE_TIME_PATTERN = "MM-dd HH:mm:ss.SSS"
|
||||
private const val PREFS_SAVE_LOGS_KEY = "SAVE_LOGS"
|
||||
private const val LOG_MAX_FILE_SIZE = 1024 * 1024
|
||||
@@ -42,12 +52,16 @@ object Log {
|
||||
private val dateTimeFormat: DateTimeFormatter = DateTimeFormatter.ofPattern(DATE_TIME_PATTERN)
|
||||
|
||||
private lateinit var logDir: File
|
||||
private val logFile: File by lazy { File(logDir, LOG_FILE_NAME) }
|
||||
private val rotateLogFile: File by lazy { File(logDir, ROTATE_LOG_FILE_NAME) }
|
||||
private lateinit var logFile: File
|
||||
private lateinit var rotateLogFile: File
|
||||
|
||||
private val fileLock: FileChannel by lazy { RandomAccessFile(File(logDir, LOCK_FILE_NAME).path, "rw").channel }
|
||||
private val threadLock: ReentrantLock by lazy { ReentrantLock() }
|
||||
|
||||
private var isAwgProcess: Boolean = false
|
||||
private val awgLogcatLock = Any()
|
||||
private var awgLogcatReader: AwgLogcatReader? = null
|
||||
|
||||
@Volatile
|
||||
private var _saveLogs: Boolean = false
|
||||
var saveLogs: Boolean
|
||||
@@ -60,6 +74,9 @@ object Log {
|
||||
}
|
||||
_saveLogs = value
|
||||
Prefs.save(PREFS_SAVE_LOGS_KEY, value)
|
||||
if (isAwgProcess) {
|
||||
if (value) startAwgLogcatReader() else stopAwgLogcatReader()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,21 +113,60 @@ object Log {
|
||||
fun init(context: Context) {
|
||||
v(TAG, "Init Log")
|
||||
logDir = File(context.cacheDir, "logs")
|
||||
val processName = Application.getProcessName()
|
||||
val stream = if (':' in processName && !processName.endsWith(TILE_SERVICE_PROCESS_SUFFIX)) {
|
||||
STREAM_TUNNEL
|
||||
} else {
|
||||
STREAM_APP
|
||||
}
|
||||
val (rotate, current) = streamFiles(stream)
|
||||
rotateLogFile = rotate
|
||||
logFile = current
|
||||
isAwgProcess = processName.endsWith(AWG_SERVICE_PROCESS_SUFFIX)
|
||||
saveLogs = Prefs.load(PREFS_SAVE_LOGS_KEY)
|
||||
}
|
||||
|
||||
fun getLogs(): String =
|
||||
"${deviceInfo()}\n${readLogs()}\nLOGCAT:\n${getLogcat()}"
|
||||
@JvmStatic
|
||||
fun getLogFiles(stream: Int): String {
|
||||
val files = streamFiles(stream).toMutableList()
|
||||
if (stream == STREAM_APP) files.addAll(0, legacyFiles().filter { it.exists() })
|
||||
return files.joinToString("\n") { it.absolutePath }
|
||||
}
|
||||
|
||||
fun clearLogs() {
|
||||
@JvmStatic
|
||||
fun getDeviceInfo(): String {
|
||||
val sb = StringBuilder()
|
||||
sb.append("Model: ").appendLine(Build.MODEL)
|
||||
sb.append("Brand: ").appendLine(Build.BRAND)
|
||||
sb.append("Product: ").appendLine(Build.PRODUCT)
|
||||
sb.append("Device: ").appendLine(Build.DEVICE)
|
||||
sb.append("Codename: ").appendLine(Build.VERSION.CODENAME)
|
||||
sb.append("Release: ").appendLine(Build.VERSION.RELEASE)
|
||||
sb.append("SDK: ").appendLine(Build.VERSION.SDK_INT)
|
||||
sb.append("ABI: ").appendLine(Build.SUPPORTED_ABIS.joinToString())
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun clearStream(stream: Int) {
|
||||
if (logDir.exists()) {
|
||||
withLock {
|
||||
logFile.delete()
|
||||
rotateLogFile.delete()
|
||||
streamFiles(stream).forEach { it.delete() }
|
||||
if (stream == STREAM_APP) legacyFiles().forEach { it.delete() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun streamFiles(stream: Int): List<File> =
|
||||
if (stream == STREAM_TUNNEL) {
|
||||
listOf(File(logDir, TUNNEL_ROTATE_LOG_FILE_NAME), File(logDir, TUNNEL_LOG_FILE_NAME))
|
||||
} else {
|
||||
listOf(File(logDir, APP_ROTATE_LOG_FILE_NAME), File(logDir, APP_LOG_FILE_NAME))
|
||||
}
|
||||
|
||||
private fun legacyFiles(): List<File> =
|
||||
listOf(File(logDir, LEGACY_ROTATE_LOG_FILE_NAME), File(logDir, LEGACY_LOG_FILE_NAME))
|
||||
|
||||
private fun log(tag: String, msg: String, priority: Priority) {
|
||||
if (saveLogs && priority != V) saveLogMsg(formatLogMsg(tag, msg, priority))
|
||||
|
||||
@@ -136,51 +192,85 @@ object Log {
|
||||
}
|
||||
}
|
||||
|
||||
private fun startAwgLogcatReader() {
|
||||
synchronized(awgLogcatLock) {
|
||||
if (awgLogcatReader == null) {
|
||||
awgLogcatReader = AwgLogcatReader().also { it.start() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopAwgLogcatReader() {
|
||||
synchronized(awgLogcatLock) {
|
||||
awgLogcatReader?.shutdown()
|
||||
awgLogcatReader = null
|
||||
}
|
||||
}
|
||||
|
||||
private class AwgLogcatReader : Thread("AwgLogcatReader") {
|
||||
private val lock = Any()
|
||||
private var process: java.lang.Process? = null
|
||||
@Volatile
|
||||
private var stopped = false
|
||||
|
||||
init {
|
||||
isDaemon = true
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
try {
|
||||
follow()
|
||||
} finally {
|
||||
synchronized(awgLogcatLock) {
|
||||
if (awgLogcatReader === this) awgLogcatReader = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun follow() {
|
||||
val logcat = try {
|
||||
ProcessBuilder(
|
||||
"logcat", "-v", "threadtime", "-v", "UTC", "--pid=${Process.myPid()}", "-T", "1"
|
||||
).redirectErrorStream(true).start()
|
||||
} catch (e: IOException) {
|
||||
NativeLog.e(TAG, "Failed to start logcat: $e")
|
||||
return
|
||||
}
|
||||
synchronized(lock) {
|
||||
if (stopped) {
|
||||
logcat.destroy()
|
||||
return
|
||||
}
|
||||
process = logcat
|
||||
}
|
||||
try {
|
||||
logcat.inputStream.bufferedReader().useLines { lines ->
|
||||
lines.filter { it.contains(AWG_LOGCAT_TAG_PREFIX) }.forEach {
|
||||
if (!stopped) saveLogMsg("$it\n")
|
||||
}
|
||||
}
|
||||
if (!stopped) NativeLog.w(TAG, "Logcat exited unexpectedly")
|
||||
} catch (e: IOException) {
|
||||
if (!stopped) NativeLog.e(TAG, "Failed to read logcat: $e")
|
||||
} finally {
|
||||
logcat.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
fun shutdown() {
|
||||
synchronized(lock) {
|
||||
stopped = true
|
||||
process?.destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatLogMsg(tag: String, msg: String, priority: Priority): String {
|
||||
val utcDate = ZonedDateTime.now(ZoneOffset.UTC).format(dateTimeFormat)
|
||||
return "${utcDate}Z ${Process.myPid()} ${Process.myTid()} $priority [${Thread.currentThread().name}] " +
|
||||
"$tag: $msg\n"
|
||||
}
|
||||
|
||||
private fun deviceInfo(): String {
|
||||
val sb = StringBuilder()
|
||||
sb.append("Model: ").appendLine(Build.MODEL)
|
||||
sb.append("Brand: ").appendLine(Build.BRAND)
|
||||
sb.append("Product: ").appendLine(Build.PRODUCT)
|
||||
sb.append("Device: ").appendLine(Build.DEVICE)
|
||||
sb.append("Codename: ").appendLine(Build.VERSION.CODENAME)
|
||||
sb.append("Release: ").appendLine(Build.VERSION.RELEASE)
|
||||
sb.append("SDK: ").appendLine(Build.VERSION.SDK_INT)
|
||||
sb.append("ABI: ").appendLine(Build.SUPPORTED_ABIS.joinToString())
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
private fun readLogs(): String {
|
||||
var logText = ""
|
||||
withLock {
|
||||
try {
|
||||
if (rotateLogFile.exists()) logText = rotateLogFile.readText()
|
||||
if (logFile.exists()) logText += logFile.readText()
|
||||
} catch (e: IOException) {
|
||||
val errorMsg = "Failed to read log: $e"
|
||||
NativeLog.e(TAG, errorMsg)
|
||||
logText += errorMsg
|
||||
}
|
||||
}
|
||||
return logText
|
||||
}
|
||||
|
||||
private fun getLogcat(): String {
|
||||
try {
|
||||
val process = ProcessBuilder("logcat", "-d").redirectErrorStream(true).start()
|
||||
return process.inputStream.reader().readText()
|
||||
} catch (e: IOException) {
|
||||
val errorMsg = "Failed to get logcat log: $e"
|
||||
NativeLog.e(TAG, errorMsg)
|
||||
return errorMsg
|
||||
}
|
||||
}
|
||||
|
||||
private fun withLock(block: () -> Unit) {
|
||||
threadLock.lock()
|
||||
try {
|
||||
|
||||
@@ -49,15 +49,23 @@ set(SOURCES ${SOURCES}
|
||||
|
||||
|
||||
find_package(awg-android REQUIRED)
|
||||
set(LIBS ${LIBS} amnezia::awg-android)
|
||||
set_property(TARGET ${PROJECT} APPEND PROPERTY QT_ANDROID_EXTRA_LIBS ${AMNEZIA_ANDROID_LIBWG_PATH} ${AMNEZIA_ANDROID_LIBWG_QUICK_PATH})
|
||||
set_property(TARGET ${PROJECT} APPEND PROPERTY QT_ANDROID_EXTRA_LIBS
|
||||
$<TARGET_FILE:amnezia::awg-android>
|
||||
${AMNEZIA_ANDROID_LIBWG_PATH}
|
||||
${AMNEZIA_ANDROID_LIBWG_QUICK_PATH}
|
||||
)
|
||||
|
||||
find_package(amnezia-libxray REQUIRED)
|
||||
file(COPY ${AMNEZIA_LIBXRAY_PATH} DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/android/xray/libXray)
|
||||
|
||||
find_package(openvpn-pt-android REQUIRED)
|
||||
set(LIBS ${LIBS} amnezia::openvpn-pt-android)
|
||||
set_property(TARGET ${PROJECT} APPEND PROPERTY QT_ANDROID_EXTRA_LIBS ${OPENVPN_PT_ANDROID_LIBCK_OVPN_PLUGIN_PATH})
|
||||
get_filename_component(OPENVPN_PT_ANDROID_LIB_DIR ${OPENVPN_PT_ANDROID_LIBCK_OVPN_PLUGIN_PATH} DIRECTORY)
|
||||
set_property(TARGET ${PROJECT} APPEND PROPERTY QT_ANDROID_EXTRA_LIBS
|
||||
${OPENVPN_PT_ANDROID_LIBCK_OVPN_PLUGIN_PATH}
|
||||
${OPENVPN_PT_ANDROID_LIB_DIR}/libovpn3.so
|
||||
${OPENVPN_PT_ANDROID_LIB_DIR}/libovpnutil.so
|
||||
${OPENVPN_PT_ANDROID_LIB_DIR}/librsapss.so
|
||||
)
|
||||
|
||||
set(APP_ANDROID_PACKAGE_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/android)
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
function(apple_icon_composer_supported OUT_VAR)
|
||||
set(${OUT_VAR} FALSE PARENT_SCOPE)
|
||||
|
||||
execute_process(
|
||||
COMMAND xcrun actool --version --output-format human-readable-text
|
||||
OUTPUT_VARIABLE ACTOOL_VERSION_OUTPUT
|
||||
ERROR_QUIET
|
||||
)
|
||||
string(REGEX MATCH "short-bundle-version: ([0-9]+)" ACTOOL_VERSION_MATCH "${ACTOOL_VERSION_OUTPUT}")
|
||||
if(NOT ACTOOL_VERSION_MATCH OR CMAKE_MATCH_1 LESS 26)
|
||||
return()
|
||||
endif()
|
||||
|
||||
execute_process(
|
||||
COMMAND sw_vers -productVersion
|
||||
OUTPUT_VARIABLE HOST_MACOS_VERSION
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
ERROR_QUIET
|
||||
)
|
||||
if(HOST_MACOS_VERSION VERSION_LESS 26)
|
||||
return()
|
||||
endif()
|
||||
|
||||
set(${OUT_VAR} TRUE PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
function(check_apple_app_icon ICON_PATH)
|
||||
if(NOT EXISTS "${ICON_PATH}/icon.json")
|
||||
message(FATAL_ERROR "App icon not found: ${ICON_PATH} (expected an Icon Composer .icon bundle)")
|
||||
endif()
|
||||
|
||||
apple_icon_composer_supported(ICON_COMPOSER_SUPPORTED)
|
||||
if(NOT ICON_COMPOSER_SUPPORTED)
|
||||
message(FATAL_ERROR "Xcode 26 and macOS 26 or later are required to build the app icon ${ICON_PATH}")
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
function(select_macos_app_icon ICON_PATH ICNS_PATH OUT_USE_ICON_COMPOSER)
|
||||
apple_icon_composer_supported(ICON_COMPOSER_SUPPORTED)
|
||||
if(ICON_COMPOSER_SUPPORTED)
|
||||
check_apple_app_icon(${ICON_PATH})
|
||||
set(${OUT_USE_ICON_COMPOSER} TRUE PARENT_SCOPE)
|
||||
return()
|
||||
endif()
|
||||
|
||||
if(NOT EXISTS "${ICNS_PATH}")
|
||||
message(FATAL_ERROR "Xcode 26 and macOS 26 are required for ${ICON_PATH}, and the fallback icon ${ICNS_PATH} was not found")
|
||||
endif()
|
||||
message(STATUS "Icon Composer needs Xcode 26 and macOS 26, using the fallback app icon ${ICNS_PATH}")
|
||||
set(${OUT_USE_ICON_COMPOSER} FALSE PARENT_SCOPE)
|
||||
endfunction()
|
||||
@@ -58,3 +58,7 @@ endif()
|
||||
if(NOT CLIENT_NOTIFICATION_IDENTIFIER)
|
||||
set(CLIENT_NOTIFICATION_IDENTIFIER "${CLIENT_TS_PREFIX}" CACHE STRING "Local notification request identifier")
|
||||
endif()
|
||||
|
||||
if(NOT DEFINED CLIENT_ENABLE_APP_UPDATES)
|
||||
set(CLIENT_ENABLE_APP_UPDATES 1)
|
||||
endif()
|
||||
@@ -19,8 +19,8 @@ endif()
|
||||
if(NOT CLIENT_IOS_LAUNCHSCREEN_PATH)
|
||||
set(CLIENT_IOS_LAUNCHSCREEN_PATH "${_CLIENT_SRC_DIR}/ios/app/${CLIENT_IOS_LAUNCHSCREEN_FILE}" CACHE FILEPATH "iOS launch screen storyboard path")
|
||||
endif()
|
||||
if(NOT CLIENT_IOS_MEDIA_ASSETS_PATH)
|
||||
set(CLIENT_IOS_MEDIA_ASSETS_PATH "${_CLIENT_SRC_DIR}/ios/app/Media.xcassets" CACHE FILEPATH "iOS app Media.xcassets path")
|
||||
if(NOT CLIENT_IOS_APP_ICON_PATH)
|
||||
set(CLIENT_IOS_APP_ICON_PATH "${_CLIENT_SRC_DIR}/images/AppIcon.icon" CACHE PATH "iOS app icon (Icon Composer .icon)")
|
||||
endif()
|
||||
|
||||
if(NOT CLIENT_IOS_BUNDLE_ID)
|
||||
|
||||
@@ -11,11 +11,11 @@ endif()
|
||||
if(NOT CLIENT_MACOS_NE_ENTITLEMENTS_PATH)
|
||||
set(CLIENT_MACOS_NE_ENTITLEMENTS_PATH "${_CLIENT_SRC_DIR}/macos/networkextension/${CLIENT_MACOS_NE_ENTITLEMENTS_FILE}" CACHE FILEPATH "macOS network extension entitlements file path")
|
||||
endif()
|
||||
if(NOT CLIENT_MACOS_MEDIA_ASSETS_PATH)
|
||||
set(CLIENT_MACOS_MEDIA_ASSETS_PATH "${_CLIENT_SRC_DIR}/macos/app/Images.xcassets" CACHE FILEPATH "macOS app Images.xcassets path")
|
||||
if(NOT CLIENT_MACOS_APP_ICON_PATH)
|
||||
set(CLIENT_MACOS_APP_ICON_PATH "${_CLIENT_SRC_DIR}/images/AppIcon.icon" CACHE PATH "macOS app icon (Icon Composer .icon)")
|
||||
endif()
|
||||
if(NOT CLIENT_MACOS_APP_ICNS_PATH)
|
||||
set(CLIENT_MACOS_APP_ICNS_PATH "${_CLIENT_SRC_DIR}/images/app.icns" CACHE FILEPATH "macOS app icon (.icns) used by non-App Store builds")
|
||||
set(CLIENT_MACOS_APP_ICNS_PATH "${_CLIENT_SRC_DIR}/macos/app/AppIcon.icns" CACHE FILEPATH "macOS app icon (.icns) for builds without Xcode 26 and macOS 26")
|
||||
endif()
|
||||
|
||||
if(NOT CLIENT_MACOS_EXTRA_PLIST_ENTRIES)
|
||||
|
||||
@@ -127,14 +127,18 @@ target_sources(${PROJECT} PRIVATE
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/StoreKit2Helper.swift
|
||||
)
|
||||
|
||||
check_apple_app_icon(${CLIENT_IOS_APP_ICON_PATH})
|
||||
|
||||
set_source_files_properties(
|
||||
${CLIENT_IOS_MEDIA_ASSETS_PATH}
|
||||
PROPERTIES MACOSX_PACKAGE_LOCATION Resources
|
||||
${CLIENT_IOS_APP_ICON_PATH}
|
||||
PROPERTIES
|
||||
MACOSX_PACKAGE_LOCATION Resources
|
||||
XCODE_LAST_KNOWN_FILE_TYPE folder.iconcomposer.icon
|
||||
)
|
||||
|
||||
target_sources(${PROJECT} PRIVATE
|
||||
${CLIENT_IOS_LAUNCHSCREEN_PATH}
|
||||
${CLIENT_IOS_MEDIA_ASSETS_PATH}
|
||||
${CLIENT_IOS_APP_ICON_PATH}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy
|
||||
)
|
||||
|
||||
|
||||
@@ -34,10 +34,42 @@ set(SOURCES ${SOURCES}
|
||||
|
||||
|
||||
|
||||
set(ICON_FILE ${CLIENT_MACOS_APP_ICNS_PATH})
|
||||
get_filename_component(MACOSX_BUNDLE_ICON_FILE "${CLIENT_MACOS_APP_ICNS_PATH}" NAME)
|
||||
set_source_files_properties(${ICON_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
|
||||
set(SOURCES ${SOURCES} ${ICON_FILE})
|
||||
select_macos_app_icon(${CLIENT_MACOS_APP_ICON_PATH} ${CLIENT_MACOS_APP_ICNS_PATH} USE_ICON_COMPOSER)
|
||||
|
||||
set(APP_ICON_OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/appicon)
|
||||
set(MACOSX_BUNDLE_ICON_FILE AppIcon)
|
||||
|
||||
if(USE_ICON_COMPOSER)
|
||||
set(APP_ICON_FILES ${APP_ICON_OUTPUT_DIR}/Assets.car ${APP_ICON_OUTPUT_DIR}/AppIcon.icns)
|
||||
file(GLOB_RECURSE APP_ICON_INPUTS CONFIGURE_DEPENDS "${CLIENT_MACOS_APP_ICON_PATH}/*")
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${APP_ICON_FILES}
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${APP_ICON_OUTPUT_DIR}
|
||||
COMMAND xcrun actool --output-format human-readable-text --warnings --errors
|
||||
--platform macosx
|
||||
--minimum-deployment-target ${CMAKE_OSX_DEPLOYMENT_TARGET}
|
||||
--app-icon AppIcon
|
||||
--output-partial-info-plist ${APP_ICON_OUTPUT_DIR}/partial.plist
|
||||
--compile ${APP_ICON_OUTPUT_DIR}
|
||||
${CLIENT_MACOS_APP_ICON_PATH}
|
||||
DEPENDS ${APP_ICON_INPUTS}
|
||||
COMMENT "Compiling macOS app icon"
|
||||
VERBATIM
|
||||
)
|
||||
set_source_files_properties(${APP_ICON_FILES} PROPERTIES GENERATED TRUE)
|
||||
|
||||
add_custom_command(TARGET ${PROJECT} POST_BUILD
|
||||
COMMAND plutil -replace CFBundleIconName -string AppIcon $<TARGET_BUNDLE_CONTENT_DIR:${PROJECT}>/Info.plist
|
||||
VERBATIM
|
||||
)
|
||||
else()
|
||||
set(APP_ICON_FILES ${APP_ICON_OUTPUT_DIR}/AppIcon.icns)
|
||||
configure_file(${CLIENT_MACOS_APP_ICNS_PATH} ${APP_ICON_FILES} COPYONLY)
|
||||
endif()
|
||||
|
||||
set_source_files_properties(${APP_ICON_FILES} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
|
||||
set(SOURCES ${SOURCES} ${APP_ICON_FILES})
|
||||
|
||||
target_compile_options(${PROJECT} PRIVATE
|
||||
-DGROUP_ID=\"${BUILD_IOS_GROUP_IDENTIFIER}\"
|
||||
|
||||
@@ -122,13 +122,24 @@ target_sources(${PROJECT} PRIVATE
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/StoreKit2Helper.swift
|
||||
)
|
||||
|
||||
set_source_files_properties(
|
||||
${CLIENT_MACOS_MEDIA_ASSETS_PATH}
|
||||
PROPERTIES MACOSX_PACKAGE_LOCATION Resources
|
||||
)
|
||||
select_macos_app_icon(${CLIENT_MACOS_APP_ICON_PATH} ${CLIENT_MACOS_APP_ICNS_PATH} USE_ICON_COMPOSER)
|
||||
|
||||
if(USE_ICON_COMPOSER)
|
||||
set(APP_ICON_SOURCE ${CLIENT_MACOS_APP_ICON_PATH})
|
||||
set_source_files_properties(
|
||||
${APP_ICON_SOURCE}
|
||||
PROPERTIES
|
||||
MACOSX_PACKAGE_LOCATION Resources
|
||||
XCODE_LAST_KNOWN_FILE_TYPE folder.iconcomposer.icon
|
||||
)
|
||||
else()
|
||||
set(APP_ICON_SOURCE ${CMAKE_CURRENT_BINARY_DIR}/appicon/AppIcon.icns)
|
||||
configure_file(${CLIENT_MACOS_APP_ICNS_PATH} ${APP_ICON_SOURCE} COPYONLY)
|
||||
set_source_files_properties(${APP_ICON_SOURCE} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
|
||||
endif()
|
||||
|
||||
target_sources(${PROJECT} PRIVATE
|
||||
${CLIENT_MACOS_MEDIA_ASSETS_PATH}
|
||||
${APP_ICON_SOURCE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy
|
||||
)
|
||||
|
||||
|
||||
@@ -40,64 +40,6 @@ function(osx_bundle_files TARGET)
|
||||
endforeach()
|
||||
endfunction(osx_bundle_files)
|
||||
|
||||
## A helper to bundle an asset catalog.
|
||||
function(osx_bundle_assetcatalog TARGET)
|
||||
cmake_parse_arguments(XCASSETS
|
||||
""
|
||||
"CATALOG;PLATFORM"
|
||||
"DEVICES"
|
||||
${ARGN})
|
||||
|
||||
if(XCASSETS_PLATFORM)
|
||||
set(XCASSETS_TARGET_ARGS --platform ${XCASSETS_PLATFORM})
|
||||
elseif(IOS)
|
||||
set(XCASSETS_TARGET_ARGS --platform iphoneos)
|
||||
else()
|
||||
set(XCASSETS_TARGET_ARGS --platform macosx)
|
||||
endif()
|
||||
foreach(DEVNAME ${XCASSETS_DEVICES})
|
||||
list(APPEND XCASSETS_TARGET_ARGS --target-device ${DEVNAME})
|
||||
endforeach()
|
||||
list(APPEND XCASSETS_TARGET_ARGS --minimum-deployment-target ${CMAKE_OSX_DEPLOYMENT_TARGET})
|
||||
|
||||
## Compile the asset catalog
|
||||
set(XCASSETS_GEN_PLIST ${CMAKE_CURRENT_BINARY_DIR}/xcassets_generated_info.plist)
|
||||
add_custom_command(
|
||||
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/xcassets/Assets.car ${XCASSETS_GEN_PLIST}
|
||||
MAIN_DEPENDENCY ${XCASSETS_CATALOG}/Contents.json
|
||||
COMMENT "Building asset catalog"
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/xcassets
|
||||
COMMAND actool --output-format human-readable-text --notices --warnings
|
||||
${XCASSETS_TARGET_ARGS}
|
||||
--app-icon AppIcon
|
||||
--output-partial-info-plist ${XCASSETS_GEN_PLIST}
|
||||
--development-region en --enable-on-demand-resources NO
|
||||
--compile ${CMAKE_CURRENT_BINARY_DIR}/xcassets ${XCASSETS_CATALOG}
|
||||
)
|
||||
|
||||
## Patch the asset catalog into the target bundle.
|
||||
if(NOT IOS)
|
||||
set(XCASSETS_RESOURCE_DIR "Resources")
|
||||
endif()
|
||||
add_custom_command(TARGET ${TARGET} POST_BUILD
|
||||
COMMENT "Bundling asset catalog"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_BINARY_DIR}/xcassets $<TARGET_BUNDLE_CONTENT_DIR:${TARGET}>/${XCASSETS_RESOURCE_DIR}
|
||||
COMMAND ${CLIENT_ROOT_DIR}/scripts/macos/merge_plist.py ${XCASSETS_GEN_PLIST} -o $<TARGET_BUNDLE_CONTENT_DIR:${TARGET}>/Info.plist
|
||||
)
|
||||
|
||||
target_sources(${TARGET} PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/xcassets/Assets.car)
|
||||
set_source_files_properties(
|
||||
${CMAKE_CURRENT_BINARY_DIR}/xcassets/Assets.car
|
||||
${XCASSETS_GEN_PLIST}
|
||||
PROPERTIES
|
||||
GENERATED TRUE
|
||||
HEADER_FILE_ONLY TRUE
|
||||
)
|
||||
|
||||
target_sources(${TARGET} PRIVATE ${XCASSETS_GEN_PLIST})
|
||||
set_source_files_properties(${XCASSETS_GEN_PLIST} PROPERTIES GENERATED TRUE)
|
||||
endfunction()
|
||||
|
||||
## A helper to code-sign an executable.
|
||||
function(osx_codesign_target TARGET)
|
||||
## Xcode should perform automatic code-signing for us.
|
||||
|
||||
@@ -38,12 +38,15 @@ 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
|
||||
@@ -65,9 +68,12 @@ set(HEADERS ${HEADERS}
|
||||
${CLIENT_ROOT_DIR}/core/utils/serialization/transfer.h
|
||||
${CLIENT_ROOT_DIR}/../common/logger/logger.h
|
||||
${CLIENT_ROOT_DIR}/../common/crypto/cryptoUtils.h
|
||||
${CLIENT_ROOT_DIR}/ui/utils/qmlUtils.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/api/apiUtils.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/api/countryCatalog.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/api/gatewayPayloadBuilder.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/localProxy/localProxyConstants.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/localProxy/portUtils.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/localProxy/xrayConfigUtils.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/osSignalHandler.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/swiftBridge.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/utilities.h
|
||||
@@ -123,12 +129,15 @@ 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
|
||||
@@ -152,9 +161,11 @@ set(SOURCES ${SOURCES}
|
||||
${CLIENT_ROOT_DIR}/core/utils/serialization/vmess_new.cpp
|
||||
${CLIENT_ROOT_DIR}/../common/logger/logger.cpp
|
||||
${CLIENT_ROOT_DIR}/../common/crypto/cryptoUtils.cpp
|
||||
${CLIENT_ROOT_DIR}/ui/utils/qmlUtils.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/api/apiUtils.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/api/countryCatalog.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/api/gatewayPayloadBuilder.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/localProxy/portUtils.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/localProxy/xrayConfigUtils.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/serverConfigUtils.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/osSignalHandler.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/utilities.cpp
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QDateTime>
|
||||
#include <QThread>
|
||||
#include <QUuid>
|
||||
#include "logger.h"
|
||||
@@ -23,124 +24,49 @@
|
||||
namespace {
|
||||
Logger logger("XrayConfigurator");
|
||||
|
||||
QString normalizeXhttpMode(const QString &m) {
|
||||
const QString t = m.trimmed();
|
||||
if (t.isEmpty() || t.compare(QLatin1String("Auto"), Qt::CaseInsensitive) == 0) {
|
||||
return QStringLiteral("auto");
|
||||
}
|
||||
if (t.compare(QLatin1String("Packet-up"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("packet-up");
|
||||
if (t.compare(QLatin1String("Stream-up"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("stream-up");
|
||||
if (t.compare(QLatin1String("Stream-one"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("stream-one");
|
||||
return t.toLower();
|
||||
}
|
||||
|
||||
// Xray-core: empty → path; "None" in UI → omit (core default path)
|
||||
QString normalizeSessionSeqPlacement(const QString &p)
|
||||
QString effectiveServerFlow(const amnezia::XrayServerConfig &srv)
|
||||
{
|
||||
if (p.isEmpty() || p.compare(QLatin1String("None"), Qt::CaseInsensitive) == 0)
|
||||
return {};
|
||||
return p.toLower();
|
||||
}
|
||||
|
||||
QString normalizeUplinkDataPlacement(const QString &p)
|
||||
{
|
||||
if (p.isEmpty() || p.compare(QLatin1String("Body"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("body");
|
||||
if (p.compare(QLatin1String("Auto"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("auto");
|
||||
if (p.compare(QLatin1String("Query"), Qt::CaseInsensitive) == 0)
|
||||
// "Query" is not valid for uplink payload in splithttp; closest documented mode
|
||||
return QStringLiteral("header");
|
||||
return p.toLower();
|
||||
}
|
||||
|
||||
// splithttp: cookie | header | query | queryInHeader (not "body")
|
||||
QString normalizeXPaddingPlacement(const QString &p)
|
||||
{
|
||||
QString t = p.trimmed();
|
||||
if (t.isEmpty())
|
||||
return QString::fromLatin1(amnezia::protocols::xray::defaultXPaddingPlacement).toLower();
|
||||
if (t.compare(QLatin1String("Body"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("queryInHeader");
|
||||
if (t.contains(QLatin1String("queryInHeader"), Qt::CaseInsensitive)
|
||||
|| t.compare(QLatin1String("Query in header"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("queryInHeader");
|
||||
return t.toLower();
|
||||
}
|
||||
|
||||
// splithttp: repeat-x | tokenish
|
||||
QString normalizeXPaddingMethod(const QString &m)
|
||||
{
|
||||
QString t = m.trimmed();
|
||||
if (t.isEmpty() || t.compare(QLatin1String("Repeat-x"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("repeat-x");
|
||||
if (t.compare(QLatin1String("Tokenish"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("tokenish");
|
||||
if (t.compare(QLatin1String("Random"), Qt::CaseInsensitive) == 0
|
||||
|| t.compare(QLatin1String("Zero"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("repeat-x");
|
||||
return t.toLower();
|
||||
}
|
||||
|
||||
// xray wants int ranges as "from-to" string, not a {from,to} object.
|
||||
QString makeRangeString(const QString &minV, const QString &maxV)
|
||||
{
|
||||
return minV + QLatin1Char('-') + maxV;
|
||||
}
|
||||
|
||||
void putIntRangeIfAny(QJsonObject &obj, const char *key, QString minV, QString maxV, const char *fallbackMin,
|
||||
const char *fallbackMax)
|
||||
{
|
||||
if (minV.isEmpty() && maxV.isEmpty())
|
||||
return;
|
||||
if (minV.isEmpty())
|
||||
minV = QString::fromLatin1(fallbackMin);
|
||||
if (maxV.isEmpty())
|
||||
maxV = QString::fromLatin1(fallbackMax);
|
||||
obj[QString::fromUtf8(key)] = makeRangeString(minV, maxV);
|
||||
}
|
||||
|
||||
QString effectiveClientFlow(const amnezia::XrayServerConfig &srv)
|
||||
{
|
||||
const bool rawTransport = srv.transport.isEmpty() || srv.transport == QLatin1String("raw");
|
||||
const bool secureFlow =
|
||||
srv.security == QLatin1String("tls") || srv.security == QLatin1String("reality");
|
||||
return (rawTransport && secureFlow) ? srv.flow : QString();
|
||||
return amnezia::xrayEffective::serverFlow(srv);
|
||||
}
|
||||
|
||||
QString effectiveSecurity(const amnezia::XrayServerConfig &srv)
|
||||
{
|
||||
if (srv.transport == QLatin1String("mkcp") && srv.security == QLatin1String("reality")) {
|
||||
return QStringLiteral("none");
|
||||
}
|
||||
return srv.security;
|
||||
return amnezia::xrayEffective::security(srv);
|
||||
}
|
||||
|
||||
// Desktop applies this in XrayProtocol::start(); iOS/Android pass JSON straight to libxray — same fixes here.
|
||||
void sanitizeXrayNativeConfig(amnezia::ProtocolConfig &pc)
|
||||
{
|
||||
QString c = pc.nativeConfig();
|
||||
if (c.isEmpty()) {
|
||||
return;
|
||||
const QString before = pc.nativeConfig();
|
||||
const QString after = amnezia::sanitizeNativeConfig(before);
|
||||
if (after != before) {
|
||||
pc.setNativeConfig(after);
|
||||
}
|
||||
bool changed = false;
|
||||
if (c.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) {
|
||||
c.replace(QLatin1String("Mozilla/5.0"), QString::fromLatin1(amnezia::protocols::xray::defaultFingerprint),
|
||||
Qt::CaseInsensitive);
|
||||
changed = true;
|
||||
}
|
||||
const QString legacyListen = QString::fromLatin1(amnezia::protocols::xray::defaultLocalAddr);
|
||||
const QString listenOk = QString::fromLatin1(amnezia::protocols::xray::defaultLocalListenAddr);
|
||||
if (c.contains(legacyListen)) {
|
||||
c.replace(legacyListen, listenOk);
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
pc.setNativeConfig(c);
|
||||
}
|
||||
|
||||
QString tlsFingerprintFromEnsureOutput(const QString &stdOut)
|
||||
{
|
||||
const QString prefix = QStringLiteral("amnezia_tls_fp=");
|
||||
const auto lines = stdOut.split(QLatin1Char('\n'));
|
||||
for (QString line : lines) {
|
||||
line = line.trimmed();
|
||||
if (!line.startsWith(prefix))
|
||||
continue;
|
||||
QString fp = line.mid(prefix.size());
|
||||
fp.remove(QLatin1Char(':'));
|
||||
fp.remove(QLatin1Char(' '));
|
||||
fp = fp.toLower();
|
||||
if (fp.size() != 64)
|
||||
return {};
|
||||
for (const QChar ch : fp) {
|
||||
const ushort u = ch.unicode();
|
||||
const bool hex = (u >= '0' && u <= '9') || (u >= 'a' && u <= 'f');
|
||||
if (!hex)
|
||||
return {};
|
||||
}
|
||||
return fp;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -157,30 +83,6 @@ amnezia::ProtocolConfig XrayConfigurator::processConfigWithLocalSettings(const a
|
||||
return protocolConfig;
|
||||
}
|
||||
|
||||
ErrorCode XrayConfigurator::uploadServerConfigJson(const ServerCredentials &credentials, DockerContainer container,
|
||||
const DnsSettings &dnsSettings, const QJsonObject &serverConfig) const
|
||||
{
|
||||
const QString updatedConfig = QJsonDocument(serverConfig).toJson();
|
||||
ErrorCode errorCode = m_sshSession->uploadTextFileToContainer(
|
||||
container, credentials, updatedConfig, amnezia::protocols::xray::serverConfigPath,
|
||||
libssh::ScpOverwriteMode::ScpOverwriteExisting);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Failed to upload updated config";
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
const QString restartScript = QStringLiteral("sudo docker restart $CONTAINER_NAME");
|
||||
errorCode = m_sshSession->runScript(
|
||||
credentials,
|
||||
m_sshSession->replaceVars(restartScript,
|
||||
amnezia::genBaseVars(credentials, container, dnsSettings.primaryDns,
|
||||
dnsSettings.secondaryDns)));
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Failed to restart container";
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
ErrorCode XrayConfigurator::readRealityKeyFiles(const DockerContainer container, const ServerCredentials &credentials,
|
||||
QString &outPublicKey, QString &outShortId) const
|
||||
{
|
||||
@@ -196,12 +98,17 @@ ErrorCode XrayConfigurator::readRealityKeyFiles(const DockerContainer container,
|
||||
if (fileError == ErrorCode::NoError && !out.isEmpty()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
if (fileError != ErrorCode::NoError) {
|
||||
logger.error() << "Xray readRealityKeyFiles: ssh failed path=" << path
|
||||
<< "error=" << static_cast<int>(fileError);
|
||||
return fileError;
|
||||
}
|
||||
if (attempt < 2) {
|
||||
QThread::msleep(500);
|
||||
}
|
||||
}
|
||||
logger.error() << "Xray readRealityKeyFiles: failed path=" << path;
|
||||
return ErrorCode::XrayRealityKeysReadFailed;
|
||||
logger.error() << "Xray readRealityKeyFiles: key file empty path=" << path;
|
||||
return ErrorCode::XrayKeysReadFailed;
|
||||
};
|
||||
|
||||
ErrorCode errorCode = readKeyFile(QString::fromLatin1(amnezia::protocols::xray::PublicKeyPath), outPublicKey);
|
||||
@@ -232,11 +139,7 @@ ErrorCode XrayConfigurator::applyServerSettingsToRemote(const ServerCredentials
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
logger.info() << "Xray applyServerSettings: start"
|
||||
<< "container=" << static_cast<int>(container) << "host=" << credentials.hostName
|
||||
<< "transport=" << srv.transport << "security=" << srv.security << "port=" << srv.port
|
||||
<< "appendClient=" << appendNewClient;
|
||||
const QString flowValue = effectiveClientFlow(srv);
|
||||
const QString flowValue = effectiveServerFlow(srv);
|
||||
QString realityPublicKey;
|
||||
QString realityShortId;
|
||||
if (effectiveSecurity(srv) == QLatin1String("reality")) {
|
||||
@@ -264,71 +167,59 @@ ErrorCode XrayConfigurator::applyServerSettingsToRemote(const ServerCredentials
|
||||
}
|
||||
|
||||
QJsonObject serverConfig = doc.object();
|
||||
if (!serverConfig.contains(amnezia::protocols::xray::inbounds)) {
|
||||
logger.error() << "Server config missing 'inbounds' field";
|
||||
return ErrorCode::XrayServerConfigInvalid;
|
||||
}
|
||||
const QJsonObject originalConfig = serverConfig;
|
||||
|
||||
QJsonArray inbounds = serverConfig[amnezia::protocols::xray::inbounds].toArray();
|
||||
if (inbounds.isEmpty()) {
|
||||
logger.error() << "Server config has empty 'inbounds' array";
|
||||
return ErrorCode::XrayServerConfigInvalid;
|
||||
}
|
||||
|
||||
QJsonObject inbound = inbounds[0].toObject();
|
||||
if (!inbound.contains(amnezia::protocols::xray::settings)) {
|
||||
logger.error() << "Inbound missing 'settings' field";
|
||||
return ErrorCode::XrayServerConfigInvalid;
|
||||
}
|
||||
|
||||
QJsonObject settings = inbound[amnezia::protocols::xray::settings].toObject();
|
||||
if (!settings.contains(amnezia::protocols::xray::clients)) {
|
||||
settings[amnezia::protocols::xray::clients] = QJsonArray {};
|
||||
}
|
||||
|
||||
QJsonArray clients = settings[amnezia::protocols::xray::clients].toArray();
|
||||
QJsonArray clients = XrayServerConfig::clientsFromServerInboundJson(serverConfig);
|
||||
QString clientId;
|
||||
|
||||
if (appendNewClient) {
|
||||
clientId = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
QJsonObject clientEntry;
|
||||
clientEntry[amnezia::protocols::xray::id] = clientId;
|
||||
if (!flowValue.isEmpty()) {
|
||||
clientEntry[amnezia::protocols::xray::flow] = flowValue;
|
||||
const bool adoptingOwnAccount = !(xrayCfg->hasClientConfig() && !xrayCfg->clientConfig->id.isEmpty());
|
||||
|
||||
QString ownClientId;
|
||||
if (readContainerKeyFile(container, credentials, QString::fromLatin1(amnezia::protocols::xray::uuidPath),
|
||||
ownClientId)
|
||||
!= ErrorCode::NoError) {
|
||||
ownClientId.clear();
|
||||
}
|
||||
|
||||
const int existingIndex =
|
||||
adoptingOwnAccount ? XrayServerConfig::indexOfClient(clients, ownClientId) : -1;
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
clientId = ownClientId;
|
||||
clients[existingIndex] =
|
||||
XrayServerConfig::applyFlowToClient(clients[existingIndex].toObject(), flowValue);
|
||||
} else {
|
||||
const bool takeOverOwnAccount = adoptingOwnAccount && !ownClientId.isEmpty();
|
||||
clientId = takeOverOwnAccount ? ownClientId : QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
clients.append(XrayServerConfig::makeClientEntry(clientId, flowValue));
|
||||
}
|
||||
clients.append(clientEntry);
|
||||
} else {
|
||||
if (clients.isEmpty()) {
|
||||
logger.error() << "Server config has no VLESS clients";
|
||||
return ErrorCode::XrayServerNoVlessClients;
|
||||
}
|
||||
clientId = clients[0].toObject()[amnezia::protocols::xray::id].toString();
|
||||
clientId = XrayServerConfig::firstClientId(clients);
|
||||
if (clientId.isEmpty()) {
|
||||
logger.error() << "Server config VLESS client has empty id";
|
||||
return ErrorCode::XrayServerNoVlessClients;
|
||||
}
|
||||
QJsonArray updatedClients;
|
||||
for (const QJsonValue &v : clients) {
|
||||
QJsonObject c = v.toObject();
|
||||
if (flowValue.isEmpty()) {
|
||||
c.remove(amnezia::protocols::xray::flow);
|
||||
} else {
|
||||
c[amnezia::protocols::xray::flow] = flowValue;
|
||||
}
|
||||
updatedClients.append(c);
|
||||
}
|
||||
clients = updatedClients;
|
||||
clients = XrayServerConfig::applyFlowToClients(clients, flowValue);
|
||||
}
|
||||
|
||||
settings[amnezia::protocols::xray::clients] = clients;
|
||||
inbound[amnezia::protocols::xray::settings] = settings;
|
||||
inbounds[0] = inbound;
|
||||
serverConfig[amnezia::protocols::xray::inbounds] = inbounds;
|
||||
const XrayServerJsonStatus writeStatus =
|
||||
XrayServerConfig::setClientsInServerInboundJson(serverConfig, clients);
|
||||
if (writeStatus != XrayServerJsonStatus::Ok) {
|
||||
return ErrorCode::XrayServerConfigInvalid;
|
||||
}
|
||||
|
||||
errorCode = uploadServerConfigJson(credentials, container, dnsSettings, serverConfig);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Xray applyServerSettings: upload/restart failed, error=" << static_cast<int>(errorCode);
|
||||
return errorCode;
|
||||
const QString listenPort = srv.port.isEmpty() ? QString::fromLatin1(amnezia::protocols::xray::defaultPort)
|
||||
: srv.port;
|
||||
if (serverConfig != originalConfig) {
|
||||
errorCode = uploadServerConfigAtomically(credentials, container, listenPort, serverConfig);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
logger.info() << "Xray applyServerSettings: server config uploaded and container restarted";
|
||||
|
||||
@@ -337,7 +228,8 @@ ErrorCode XrayConfigurator::applyServerSettingsToRemote(const ServerCredentials
|
||||
}
|
||||
|
||||
XrayProtocolConfig updated =
|
||||
buildClientProtocolConfig(credentials, container, srv, clientId, errorCode, realityPublicKey, realityShortId);
|
||||
buildClientProtocolConfig(credentials, container, srv, clientId, errorCode,
|
||||
realityPublicKey, realityShortId);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Xray applyServerSettings: buildClientProtocolConfig failed, error="
|
||||
<< static_cast<int>(errorCode);
|
||||
@@ -360,19 +252,24 @@ ErrorCode XrayConfigurator::readContainerKeyFile(DockerContainer container, cons
|
||||
if (fileError == ErrorCode::NoError && !out.isEmpty()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
if (fileError != ErrorCode::NoError) {
|
||||
logger.error() << "Xray readContainerKeyFile: ssh failed path=" << path
|
||||
<< "error=" << static_cast<int>(fileError);
|
||||
return fileError;
|
||||
}
|
||||
if (attempt < 2) {
|
||||
QThread::msleep(500);
|
||||
}
|
||||
}
|
||||
logger.error() << "Xray readContainerKeyFile: failed path=" << path;
|
||||
return ErrorCode::XrayRealityKeysReadFailed;
|
||||
logger.error() << "Xray readContainerKeyFile: key file empty path=" << path;
|
||||
return ErrorCode::XrayKeysReadFailed;
|
||||
}
|
||||
|
||||
ErrorCode XrayConfigurator::writeServerConfigForSetup(const ServerCredentials &credentials, DockerContainer container,
|
||||
ContainerConfig &containerConfig, const DnsSettings &dnsSettings)
|
||||
ContainerConfig &containerConfig, const DnsSettings &dnsSettings,
|
||||
bool useAtomicApply)
|
||||
{
|
||||
Q_UNUSED(dnsSettings);
|
||||
namespace px = amnezia::protocols::xray;
|
||||
|
||||
const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>();
|
||||
if (!xrayCfg) {
|
||||
@@ -385,82 +282,72 @@ ErrorCode XrayConfigurator::writeServerConfigForSetup(const ServerCredentials &c
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
logger.info() << "Xray writeServerConfigForSetup: start container=" << static_cast<int>(container)
|
||||
<< "transport=" << srv.transport << "security=" << srv.security << "port=" << srv.port;
|
||||
|
||||
ErrorCode errorCode = ErrorCode::NoError;
|
||||
|
||||
QString clientId;
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(px::uuidPath), clientId);
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(amnezia::protocols::xray::uuidPath), clientId);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
const QString securityEff = effectiveSecurity(srv);
|
||||
QString tlsPin;
|
||||
if (securityEff == QLatin1String("tls")) {
|
||||
errorCode = ensureTlsCertificate(credentials, container, tlsPin);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
|
||||
QString realityPrivateKey;
|
||||
QString realityPublicKey;
|
||||
QString realityShortId;
|
||||
if (securityEff == QLatin1String("reality")) {
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(px::PrivateKeyPath), realityPrivateKey);
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(amnezia::protocols::xray::PrivateKeyPath), realityPrivateKey);
|
||||
if (errorCode != ErrorCode::NoError)
|
||||
return errorCode;
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(px::PublicKeyPath), realityPublicKey);
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(amnezia::protocols::xray::PublicKeyPath), realityPublicKey);
|
||||
if (errorCode != ErrorCode::NoError)
|
||||
return errorCode;
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(px::shortidPath), realityShortId);
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(amnezia::protocols::xray::shortidPath), realityShortId);
|
||||
if (errorCode != ErrorCode::NoError)
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
QJsonObject streamSettings = buildStreamSettings(srv, clientId);
|
||||
if (securityEff == QLatin1String("reality")) {
|
||||
const QString siteEff = srv.site.isEmpty() ? QString::fromLatin1(px::defaultSite) : srv.site;
|
||||
const QString sniEff = srv.sni.isEmpty() ? siteEff : srv.sni;
|
||||
const QString fpEff = srv.fingerprint.isEmpty() ? QString::fromLatin1(px::defaultFingerprint) : srv.fingerprint;
|
||||
QJsonObject rs;
|
||||
rs[QStringLiteral("dest")] = siteEff + QStringLiteral(":443");
|
||||
rs[px::fingerprint] = fpEff;
|
||||
rs[QStringLiteral("privateKey")] = realityPrivateKey;
|
||||
rs[px::serverNames] = QJsonArray { sniEff };
|
||||
rs[QStringLiteral("shortIds")] = QJsonArray { realityShortId };
|
||||
streamSettings[px::realitySettings] = rs;
|
||||
}
|
||||
|
||||
QJsonObject clientEntry;
|
||||
clientEntry[px::id] = clientId;
|
||||
const QString flowValue = effectiveClientFlow(srv);
|
||||
if (!flowValue.isEmpty()) {
|
||||
clientEntry[px::flow] = flowValue;
|
||||
}
|
||||
|
||||
QJsonObject settings;
|
||||
settings[px::clients] = QJsonArray { clientEntry };
|
||||
settings[QStringLiteral("decryption")] = QStringLiteral("none");
|
||||
|
||||
QJsonObject inbound;
|
||||
inbound[px::port] = srv.port.isEmpty() ? QString(px::defaultPort).toInt() : srv.port.toInt();
|
||||
inbound[QStringLiteral("protocol")] = QStringLiteral("vless");
|
||||
inbound[px::settings] = settings;
|
||||
inbound[px::streamSettings] = streamSettings;
|
||||
|
||||
QJsonObject serverConfig;
|
||||
serverConfig[QStringLiteral("log")] = QJsonObject { { QStringLiteral("loglevel"), QStringLiteral("error") } };
|
||||
serverConfig[px::inbounds] = QJsonArray { inbound };
|
||||
serverConfig[px::outbounds] =
|
||||
QJsonArray { QJsonObject { { QStringLiteral("protocol"), QStringLiteral("freedom") } } };
|
||||
|
||||
const QString json = QString::fromUtf8(QJsonDocument(serverConfig).toJson());
|
||||
errorCode = m_sshSession->uploadTextFileToContainer(container, credentials, json,
|
||||
QString::fromLatin1(px::serverConfigPath),
|
||||
libssh::ScpOverwriteMode::ScpOverwriteExisting);
|
||||
const QString flowValue = effectiveServerFlow(srv);
|
||||
const QJsonArray clients = collectServerClients(credentials, container, flowValue, clientId, errorCode);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Xray writeServerConfigForSetup: upload failed, error=" << static_cast<int>(errorCode);
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
const QString portEff = srv.port.isEmpty() ? QString::fromLatin1(amnezia::protocols::xray::defaultPort) : srv.port;
|
||||
|
||||
|
||||
XrayServerInboundInputs inputs;
|
||||
inputs.clients = clients;
|
||||
inputs.realityPrivateKey = realityPrivateKey;
|
||||
inputs.realityShortId = realityShortId;
|
||||
|
||||
const QJsonObject inboundJson = srv.toServerInboundJson(inputs);
|
||||
if (useAtomicApply) {
|
||||
errorCode = uploadServerConfigAtomically(credentials, container, portEff, inboundJson);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
} else {
|
||||
const QString json = QString::fromUtf8(QJsonDocument(inboundJson).toJson());
|
||||
errorCode = m_sshSession->uploadTextFileToContainer(container, credentials, json,
|
||||
QString::fromLatin1(amnezia::protocols::xray::serverConfigPath),
|
||||
libssh::ScpOverwriteMode::ScpOverwriteExisting);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
|
||||
XrayProtocolConfig updated =
|
||||
buildClientProtocolConfig(credentials, container, srv, clientId, errorCode, realityPublicKey, realityShortId);
|
||||
buildClientProtocolConfig(credentials, container, srv, clientId, errorCode,
|
||||
realityPublicKey, realityShortId, tlsPin);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Xray writeServerConfigForSetup: buildClientProtocolConfig failed, error="
|
||||
<< static_cast<int>(errorCode);
|
||||
@@ -471,6 +358,217 @@ ErrorCode XrayConfigurator::writeServerConfigForSetup(const ServerCredentials &c
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
QJsonArray XrayConfigurator::collectServerClients(const ServerCredentials &credentials, DockerContainer container,
|
||||
const QString &flowValue, const QString &fallbackClientId,
|
||||
ErrorCode &outError) const
|
||||
{
|
||||
outError = ErrorCode::NoError;
|
||||
const QString configPath = QString::fromLatin1(amnezia::protocols::xray::serverConfigPath);
|
||||
|
||||
QString currentConfig;
|
||||
for (int attempt = 0; attempt < 3; ++attempt) {
|
||||
ErrorCode readError = ErrorCode::NoError;
|
||||
currentConfig = m_sshSession->getTextFileFromContainer(container, credentials, configPath, readError);
|
||||
if (readError == ErrorCode::NoError && !currentConfig.trimmed().isEmpty()) {
|
||||
break;
|
||||
}
|
||||
currentConfig.clear();
|
||||
if (attempt < 2) {
|
||||
QThread::msleep(500);
|
||||
}
|
||||
}
|
||||
|
||||
QJsonArray existing;
|
||||
if (currentConfig.trimmed().isEmpty()) {
|
||||
QString probeOut;
|
||||
auto collect = [&probeOut](const QString &data, libssh::Client &) {
|
||||
probeOut += data + "\n";
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
const QString probe =
|
||||
QStringLiteral("if [ -s %1 ]; then echo amnezia_config=present; else echo amnezia_config=absent; fi")
|
||||
.arg(configPath);
|
||||
const ErrorCode probeError = m_sshSession->runContainerScript(credentials, container, probe, collect, collect);
|
||||
|
||||
if (probeError != ErrorCode::NoError || !probeOut.contains(QLatin1String("amnezia_config="))) {
|
||||
outError = ErrorCode::XrayServerConfigInvalid;
|
||||
return {};
|
||||
}
|
||||
if (probeOut.contains(QLatin1String("amnezia_config=present"))) {
|
||||
outError = ErrorCode::XrayServerConfigInvalid;
|
||||
return {};
|
||||
}
|
||||
} else {
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(currentConfig.toUtf8());
|
||||
if (!doc.isObject()) {
|
||||
outError = ErrorCode::XrayServerConfigInvalid;
|
||||
return {};
|
||||
}
|
||||
existing = XrayServerConfig::clientsFromServerInboundJson(doc.object());
|
||||
}
|
||||
|
||||
QJsonArray clients =
|
||||
XrayServerConfig::applyFlowToClients(existing, flowValue, XrayClientListFilter::DropWithoutId);
|
||||
|
||||
if (!fallbackClientId.isEmpty()
|
||||
&& XrayServerConfig::indexOfClient(clients, fallbackClientId) < 0) {
|
||||
clients.append(XrayServerConfig::makeClientEntry(fallbackClientId, flowValue));
|
||||
}
|
||||
|
||||
return clients;
|
||||
}
|
||||
|
||||
ErrorCode XrayConfigurator::ensureTlsCertificate(const ServerCredentials &credentials, DockerContainer container,
|
||||
QString &outFingerprint) const
|
||||
{
|
||||
outFingerprint.clear();
|
||||
|
||||
QString stdOut;
|
||||
auto collect = [&stdOut](const QString &data, libssh::Client &) {
|
||||
stdOut += data + QLatin1Char('\n');
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
|
||||
const QString certPath = QString::fromLatin1(amnezia::protocols::xray::tlsCertPath);
|
||||
const QString keyPath = QString::fromLatin1(amnezia::protocols::xray::tlsKeyPath);
|
||||
const QString script = QStringLiteral(
|
||||
"has_key() { [ -f \"$1\" ] && [ -n \"$(tr -d '[:space:]' < \"$1\" 2>/dev/null)\" ]; }\n"
|
||||
"CERT=%1\n"
|
||||
"KEY=%2\n"
|
||||
"if has_key \"$CERT\" && has_key \"$KEY\"; then echo amnezia_tls=present;\n"
|
||||
"else\n"
|
||||
" openssl req -x509 -newkey rsa:2048 -nodes -days 3650 -subj \"/CN=amnezia-xray\" "
|
||||
"-keyout \"$KEY\" -out \"$CERT\" || true\n"
|
||||
" if has_key \"$CERT\" && has_key \"$KEY\"; then echo amnezia_tls=created;\n"
|
||||
" else echo amnezia_tls=failed; exit 1; fi\n"
|
||||
"fi\n"
|
||||
"FP=$(openssl x509 -noout -fingerprint -sha256 -in \"$CERT\" 2>/dev/null "
|
||||
"| sed 's/.*Fingerprint=//' | tr -d ':' | tr 'A-F' 'a-f')\n"
|
||||
"if [ -z \"$FP\" ]; then echo amnezia_tls=failed; exit 1; fi\n"
|
||||
"echo amnezia_tls_fp=$FP\n"
|
||||
"exit 0\n")
|
||||
.arg(certPath, keyPath);
|
||||
|
||||
const ErrorCode scriptError = m_sshSession->runContainerScript(credentials, container, script, collect, collect);
|
||||
const bool present = stdOut.contains(QLatin1String("amnezia_tls=present"));
|
||||
const bool created = stdOut.contains(QLatin1String("amnezia_tls=created"));
|
||||
outFingerprint = tlsFingerprintFromEnsureOutput(stdOut);
|
||||
if (scriptError != ErrorCode::NoError || (!present && !created) || outFingerprint.isEmpty()) {
|
||||
outFingerprint.clear();
|
||||
return ErrorCode::XrayTlsNotSupported;
|
||||
}
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode XrayConfigurator::uploadServerConfigAtomically(const ServerCredentials &credentials,
|
||||
DockerContainer container, const QString &listenPort,
|
||||
const QJsonObject &serverConfig) const
|
||||
{
|
||||
const QString livePath = QString::fromLatin1(amnezia::protocols::xray::serverConfigPath);
|
||||
const QString stagedPath = QStringLiteral("/opt/amnezia/xray/server.new.json");
|
||||
|
||||
const QString json = QString::fromUtf8(QJsonDocument(serverConfig).toJson());
|
||||
ErrorCode errorCode = m_sshSession->uploadTextFileToContainer(container, credentials, json, stagedPath,
|
||||
libssh::ScpOverwriteMode::ScpOverwriteExisting);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
const QString swapScript = QStringLiteral(
|
||||
"LIVE=%1\n"
|
||||
"STAGED=%2\n"
|
||||
"BACKUP=\"$LIVE.bak\"\n"
|
||||
"if [ ! -s \"$STAGED\" ]; then echo \"amnezia_apply=missing\"; exit 0; fi\n"
|
||||
"if ! xray -test -format json -config \"$STAGED\" > /tmp/xray_test.log 2>&1; then\n"
|
||||
" echo \"amnezia_apply=invalid\"\n"
|
||||
" rm -f \"$STAGED\"\n"
|
||||
" exit 0\n"
|
||||
"fi\n"
|
||||
"if [ -f \"$LIVE\" ]; then cp \"$LIVE\" \"$BACKUP\"; fi\n"
|
||||
"mv \"$STAGED\" \"$LIVE\"\n"
|
||||
"echo \"amnezia_apply=swapped\"\n")
|
||||
.arg(livePath, stagedPath);
|
||||
|
||||
QString swapOut;
|
||||
auto collectSwap = [&swapOut](const QString &data, libssh::Client &) {
|
||||
swapOut += data + "\n";
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
errorCode = m_sshSession->runContainerScript(credentials, container, swapScript, collectSwap, collectSwap);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
if (swapOut.contains(QLatin1String("amnezia_apply=missing"))) {
|
||||
return ErrorCode::XrayServerConfigRejected;
|
||||
}
|
||||
if (swapOut.contains(QLatin1String("amnezia_apply=invalid"))) {
|
||||
return ErrorCode::XrayServerConfigRejected;
|
||||
}
|
||||
if (!swapOut.contains(QLatin1String("amnezia_apply=swapped"))) {
|
||||
return ErrorCode::XrayServerConfigInvalid;
|
||||
}
|
||||
|
||||
if (restartXrayContainer(credentials, container) && xrayProcessIsUp(credentials, container)) {
|
||||
m_sshSession->runContainerScript(credentials, container,
|
||||
QStringLiteral("rm -f %1.bak").arg(livePath));
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
const QString restoreScript = QStringLiteral(
|
||||
"LIVE=%1\n"
|
||||
"BACKUP=\"$LIVE.bak\"\n"
|
||||
"if [ -f \"$BACKUP\" ]; then cp \"$BACKUP\" \"$LIVE\"; echo amnezia_restore=done; "
|
||||
"else echo amnezia_restore=nobackup; fi\n")
|
||||
.arg(livePath);
|
||||
QString restoreOut;
|
||||
auto collectRestore = [&restoreOut](const QString &data, libssh::Client &) {
|
||||
restoreOut += data + "\n";
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
m_sshSession->runContainerScript(credentials, container, restoreScript, collectRestore, collectRestore);
|
||||
if (!restoreOut.contains(QLatin1String("amnezia_restore=done"))) {
|
||||
return ErrorCode::XrayServerNotServing;
|
||||
}
|
||||
if (restartXrayContainer(credentials, container) && xrayProcessIsUp(credentials, container)) {
|
||||
return ErrorCode::XrayServerConfigRolledBack;
|
||||
}
|
||||
return ErrorCode::XrayServerNotServing;
|
||||
}
|
||||
|
||||
bool XrayConfigurator::restartXrayContainer(const ServerCredentials &credentials, DockerContainer container) const
|
||||
{
|
||||
const ErrorCode errorCode = m_sshSession->runScript(
|
||||
credentials,
|
||||
SshSession::replaceVars(QStringLiteral("sudo docker restart $CONTAINER_NAME"),
|
||||
amnezia::genBaseVars(credentials, container, QString(), QString())));
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool XrayConfigurator::xrayProcessIsUp(const ServerCredentials &credentials, DockerContainer container) const
|
||||
{
|
||||
const QString script = QStringLiteral(
|
||||
"for i in 1 2 3 4 5 6; do\n"
|
||||
" if pgrep xray >/dev/null 2>&1 || ps 2>/dev/null | grep -v grep | grep -q \"[x]ray\"; then\n"
|
||||
" echo amnezia_serving=yes\n"
|
||||
" exit 0\n"
|
||||
" fi\n"
|
||||
" sleep 1\n"
|
||||
"done\n"
|
||||
"echo amnezia_serving=no\n");
|
||||
QString out;
|
||||
auto collect = [&out](const QString &data, libssh::Client &) {
|
||||
out += data + "\n";
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
const ErrorCode errorCode = m_sshSession->runContainerScript(credentials, container, script, collect, collect);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return false;
|
||||
}
|
||||
return out.contains(QLatin1String("amnezia_serving=yes"));
|
||||
}
|
||||
QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentials, DockerContainer container,
|
||||
const ContainerConfig &containerConfig,
|
||||
const DnsSettings &dnsSettings,
|
||||
@@ -489,17 +587,21 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia
|
||||
|
||||
XrayProtocolConfig XrayConfigurator::buildClientProtocolConfig(const ServerCredentials &credentials,
|
||||
DockerContainer container,
|
||||
const XrayServerConfig &srv, const QString &clientId,
|
||||
const XrayServerConfig &srv,
|
||||
const QString &clientId,
|
||||
ErrorCode &errorCode,
|
||||
const QString &prefetchedRealityPublicKey,
|
||||
const QString &prefetchedRealityShortId) const
|
||||
const QString &prefetchedRealityShortId,
|
||||
const QString &prefetchedTlsPin) const
|
||||
{
|
||||
QString xrayPublicKey = prefetchedRealityPublicKey;
|
||||
QString xrayShortId = prefetchedRealityShortId;
|
||||
QString tlsPin = prefetchedTlsPin;
|
||||
|
||||
const QString securityEff = effectiveSecurity(srv);
|
||||
|
||||
if (securityEff == QLatin1String("reality")) {
|
||||
|
||||
if (securityEff == QLatin1String(amnezia::protocols::xray::securityReality)) {
|
||||
if (xrayPublicKey.isEmpty() || xrayShortId.isEmpty()) {
|
||||
errorCode = readRealityKeyFiles(container, credentials, xrayPublicKey, xrayShortId);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
@@ -508,243 +610,40 @@ XrayProtocolConfig XrayConfigurator::buildClientProtocolConfig(const ServerCrede
|
||||
}
|
||||
}
|
||||
|
||||
QJsonObject userObj;
|
||||
userObj[amnezia::protocols::xray::id] = clientId;
|
||||
userObj[amnezia::protocols::xray::encryption] = QStringLiteral("none");
|
||||
const QString flowValue = effectiveClientFlow(srv);
|
||||
if (!flowValue.isEmpty()) {
|
||||
userObj[amnezia::protocols::xray::flow] = flowValue;
|
||||
if (securityEff == QLatin1String(amnezia::protocols::xray::securityTls) && !srv.isThirdPartyConfig) {
|
||||
if (tlsPin.isEmpty()) {
|
||||
errorCode = ensureTlsCertificate(credentials, container, tlsPin);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
if (tlsPin.isEmpty()) {
|
||||
errorCode = ErrorCode::XrayTlsNotSupported;
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
QJsonObject vnextEntry;
|
||||
vnextEntry[amnezia::protocols::xray::address] = credentials.hostName;
|
||||
vnextEntry[amnezia::protocols::xray::port] =
|
||||
srv.port.isEmpty() ? QString(amnezia::protocols::xray::defaultPort).toInt() : srv.port.toInt();
|
||||
vnextEntry[amnezia::protocols::xray::users] = QJsonArray { userObj };
|
||||
|
||||
QJsonObject outboundSettings;
|
||||
outboundSettings[amnezia::protocols::xray::vnext] = QJsonArray { vnextEntry };
|
||||
|
||||
QJsonObject outbound;
|
||||
outbound[QStringLiteral("protocol")] = QStringLiteral("vless");
|
||||
outbound[amnezia::protocols::xray::settings] = outboundSettings;
|
||||
|
||||
QJsonObject streamObj = buildStreamSettings(srv, clientId);
|
||||
if (securityEff == QLatin1String("reality")) {
|
||||
QJsonObject rs = streamObj[amnezia::protocols::xray::realitySettings].toObject();
|
||||
rs[amnezia::protocols::xray::publicKey] = xrayPublicKey;
|
||||
rs[amnezia::protocols::xray::shortId] = xrayShortId;
|
||||
rs[amnezia::protocols::xray::spiderX] = QString();
|
||||
streamObj[amnezia::protocols::xray::realitySettings] = rs;
|
||||
}
|
||||
|
||||
outbound[amnezia::protocols::xray::streamSettings] = streamObj;
|
||||
|
||||
QJsonObject inboundObj;
|
||||
inboundObj[QStringLiteral("listen")] = amnezia::protocols::xray::defaultLocalListenAddr;
|
||||
inboundObj[amnezia::protocols::xray::port] = amnezia::protocols::xray::defaultLocalProxyPort;
|
||||
inboundObj[QStringLiteral("protocol")] = QStringLiteral("socks");
|
||||
inboundObj[amnezia::protocols::xray::settings] = QJsonObject { { QStringLiteral("udp"), true } };
|
||||
|
||||
QJsonObject clientJson;
|
||||
clientJson[QStringLiteral("log")] = QJsonObject { { QStringLiteral("loglevel"), QStringLiteral("error") } };
|
||||
clientJson[amnezia::protocols::xray::inbounds] = QJsonArray { inboundObj };
|
||||
clientJson[amnezia::protocols::xray::outbounds] = QJsonArray { outbound };
|
||||
|
||||
const QString config = QString::fromUtf8(QJsonDocument(clientJson).toJson(QJsonDocument::Compact));
|
||||
|
||||
XrayProtocolConfig protocolConfig;
|
||||
protocolConfig.serverConfig = srv;
|
||||
|
||||
XrayClientOutboundInputs inputs;
|
||||
inputs.serverAddress = credentials.hostName;
|
||||
inputs.clientId = clientId;
|
||||
inputs.realityPublicKey = xrayPublicKey;
|
||||
inputs.realityShortId = xrayShortId;
|
||||
inputs.tlsPinnedPeerCertSha256 = tlsPin;
|
||||
|
||||
XrayClientConfig clientConfig;
|
||||
clientConfig.nativeConfig = config;
|
||||
clientConfig.localPort = QString(amnezia::protocols::xray::defaultLocalProxyPort);
|
||||
const QJsonObject clientOutbound = protocolConfig.toClientOutboundJson(inputs);
|
||||
clientConfig.nativeConfig = QString::fromUtf8(QJsonDocument(clientOutbound).toJson(QJsonDocument::Compact));
|
||||
clientConfig.localPort = QString::fromLatin1(amnezia::protocols::xray::defaultLocalProxyPort);
|
||||
clientConfig.id = clientId;
|
||||
protocolConfig.setClientConfig(clientConfig);
|
||||
|
||||
return protocolConfig;
|
||||
}
|
||||
|
||||
QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, const QString &clientId) const
|
||||
{
|
||||
QJsonObject streamSettings;
|
||||
const auto &xhttp = srv.xhttp;
|
||||
const auto &mkcp = srv.mkcp;
|
||||
namespace px = amnezia::protocols::xray;
|
||||
|
||||
QString networkValue = QStringLiteral("tcp");
|
||||
if (srv.transport == QLatin1String("xhttp"))
|
||||
networkValue = QStringLiteral("xhttp");
|
||||
else if (srv.transport == QLatin1String("mkcp"))
|
||||
networkValue = QStringLiteral("kcp");
|
||||
streamSettings[px::network] = networkValue;
|
||||
|
||||
const QString securityEff = effectiveSecurity(srv);
|
||||
streamSettings[px::security] = securityEff;
|
||||
|
||||
if (securityEff == QLatin1String("tls")) {
|
||||
QJsonObject tlsSettings;
|
||||
const QString sniEff = srv.sni.isEmpty() ? QString::fromLatin1(px::defaultSni) : srv.sni;
|
||||
tlsSettings[px::serverName] = sniEff;
|
||||
const QString alpnEff = srv.alpn.isEmpty() ? QString::fromLatin1(px::defaultAlpn) : srv.alpn;
|
||||
QJsonArray alpnArray;
|
||||
for (const QString &a : alpnEff.split(QLatin1Char(','))) {
|
||||
QString t = a.trimmed();
|
||||
if (t.isEmpty())
|
||||
continue;
|
||||
if (t.compare(QLatin1String("HTTP/2"), Qt::CaseInsensitive) == 0)
|
||||
t = QStringLiteral("h2");
|
||||
else if (t.compare(QLatin1String("HTTP/1.1"), Qt::CaseInsensitive) == 0)
|
||||
t = QStringLiteral("http/1.1");
|
||||
alpnArray.append(t);
|
||||
}
|
||||
if (!alpnArray.isEmpty())
|
||||
tlsSettings[QStringLiteral("alpn")] = alpnArray;
|
||||
const QString fpEff = srv.fingerprint.isEmpty() ? QString::fromLatin1(px::defaultFingerprint) : srv.fingerprint;
|
||||
tlsSettings[px::fingerprint] = fpEff;
|
||||
streamSettings[QStringLiteral("tlsSettings")] = tlsSettings;
|
||||
}
|
||||
|
||||
if (securityEff == QLatin1String("reality")) {
|
||||
QJsonObject realSettings;
|
||||
const QString fpEff = srv.fingerprint.isEmpty() ? QString::fromLatin1(px::defaultFingerprint) : srv.fingerprint;
|
||||
realSettings[px::fingerprint] = fpEff;
|
||||
const QString sniEff = srv.sni.isEmpty() ? QString::fromLatin1(px::defaultSni) : srv.sni;
|
||||
realSettings[px::serverName] = sniEff;
|
||||
streamSettings[px::realitySettings] = realSettings;
|
||||
}
|
||||
|
||||
// XHTTP — JSON must match Xray-core SplitHTTPConfig (flat xPadding fields, see transport_internet.go)
|
||||
if (srv.transport == QLatin1String("xhttp")) {
|
||||
QJsonObject xo;
|
||||
const QString hostEff = xhttp.host.isEmpty() ? QString::fromLatin1(px::defaultXhttpHost) : xhttp.host;
|
||||
xo[QStringLiteral("host")] = hostEff;
|
||||
if (!xhttp.path.isEmpty())
|
||||
xo[QStringLiteral("path")] = xhttp.path;
|
||||
QString modeEff = normalizeXhttpMode(xhttp.mode);
|
||||
if (modeEff == QLatin1String("auto") || modeEff == QLatin1String("packet-up")) {
|
||||
modeEff = QStringLiteral("stream-one");
|
||||
}
|
||||
xo[QStringLiteral("mode")] = modeEff;
|
||||
|
||||
// No "Host" in headers: xray rejects it when the top-level "host" field is set.
|
||||
|
||||
const QString methodEff =
|
||||
xhttp.uplinkMethod.isEmpty() ? QString::fromLatin1(px::defaultXhttpUplinkMethod) : xhttp.uplinkMethod;
|
||||
xo[QStringLiteral("uplinkHTTPMethod")] = methodEff.toUpper();
|
||||
|
||||
xo[QStringLiteral("noGRPCHeader")] = xhttp.disableGrpc;
|
||||
xo[QStringLiteral("noSSEHeader")] = xhttp.disableSse;
|
||||
|
||||
const QString sessPl = normalizeSessionSeqPlacement(xhttp.sessionPlacement);
|
||||
if (!sessPl.isEmpty())
|
||||
xo[QStringLiteral("sessionIDPlacement")] = sessPl;
|
||||
const QString seqPl = normalizeSessionSeqPlacement(xhttp.seqPlacement);
|
||||
if (!seqPl.isEmpty())
|
||||
xo[QStringLiteral("seqPlacement")] = seqPl;
|
||||
if (!xhttp.sessionKey.isEmpty())
|
||||
xo[QStringLiteral("sessionIDKey")] = xhttp.sessionKey;
|
||||
if (!xhttp.seqKey.isEmpty())
|
||||
xo[QStringLiteral("seqKey")] = xhttp.seqKey;
|
||||
|
||||
const QString uDataPl = normalizeUplinkDataPlacement(xhttp.uplinkDataPlacement);
|
||||
const bool uDataNeedsPacketUp =
|
||||
uDataPl == QLatin1String("header") || uDataPl == QLatin1String("cookie");
|
||||
if (!(uDataNeedsPacketUp && modeEff != QLatin1String("packet-up")))
|
||||
xo[QStringLiteral("uplinkDataPlacement")] = uDataPl;
|
||||
if (!xhttp.uplinkDataKey.isEmpty())
|
||||
xo[QStringLiteral("uplinkDataKey")] = xhttp.uplinkDataKey;
|
||||
|
||||
const QString ucs = xhttp.uplinkChunkSize.isEmpty() ? QString::fromLatin1(px::defaultXhttpUplinkChunkSize)
|
||||
: xhttp.uplinkChunkSize;
|
||||
if (!ucs.isEmpty() && ucs != QLatin1String("0")) {
|
||||
xo[QStringLiteral("uplinkChunkSize")] = ucs.toInt();
|
||||
}
|
||||
|
||||
if (!xhttp.scMaxBufferedPosts.isEmpty())
|
||||
xo[QStringLiteral("scMaxBufferedPosts")] = xhttp.scMaxBufferedPosts.toLongLong();
|
||||
|
||||
putIntRangeIfAny(xo, "scMaxEachPostBytes", xhttp.scMaxEachPostBytesMin, xhttp.scMaxEachPostBytesMax,
|
||||
px::defaultXhttpScMaxEachPostBytesMin, px::defaultXhttpScMaxEachPostBytesMax);
|
||||
putIntRangeIfAny(xo, "scMinPostsIntervalMs", xhttp.scMinPostsIntervalMsMin, xhttp.scMinPostsIntervalMsMax,
|
||||
px::defaultXhttpScMinPostsIntervalMsMin, px::defaultXhttpScMinPostsIntervalMsMax);
|
||||
putIntRangeIfAny(xo, "scStreamUpServerSecs", xhttp.scStreamUpServerSecsMin, xhttp.scStreamUpServerSecsMax,
|
||||
px::defaultXhttpScStreamUpServerSecsMin, px::defaultXhttpScStreamUpServerSecsMax);
|
||||
|
||||
const auto &pad = xhttp.xPadding;
|
||||
xo[QStringLiteral("xPaddingObfsMode")] = pad.obfsMode;
|
||||
if (pad.obfsMode) {
|
||||
if (!pad.bytesMin.isEmpty() || !pad.bytesMax.isEmpty()) {
|
||||
const int fromV = pad.bytesMin.isEmpty()
|
||||
? QString::fromLatin1(px::defaultXPaddingBytesMin).toInt()
|
||||
: pad.bytesMin.toInt();
|
||||
int toV = pad.bytesMax.isEmpty()
|
||||
? QString::fromLatin1(px::defaultXPaddingBytesMax).toInt()
|
||||
: pad.bytesMax.toInt();
|
||||
if (toV < fromV)
|
||||
toV = fromV;
|
||||
xo[QStringLiteral("xPaddingBytes")] = makeRangeString(QString::number(fromV), QString::number(toV));
|
||||
}
|
||||
xo[QStringLiteral("xPaddingKey")] =
|
||||
pad.key.isEmpty() ? QString::fromLatin1(px::defaultXPaddingKey) : pad.key;
|
||||
xo[QStringLiteral("xPaddingHeader")] =
|
||||
pad.header.isEmpty() ? QString::fromLatin1(px::defaultXPaddingHeader) : pad.header;
|
||||
xo[QStringLiteral("xPaddingPlacement")] = normalizeXPaddingPlacement(
|
||||
pad.placement.isEmpty() ? QString::fromLatin1(px::defaultXPaddingPlacement) : pad.placement);
|
||||
xo[QStringLiteral("xPaddingMethod")] = normalizeXPaddingMethod(
|
||||
pad.method.isEmpty() ? QString::fromLatin1(px::defaultXPaddingMethod) : pad.method);
|
||||
}
|
||||
|
||||
// xmux: Xray has no "enabled" flag; omit object when UI disables multiplex tuning.
|
||||
if (xhttp.xmux.enabled) {
|
||||
QJsonObject mux;
|
||||
auto addMuxRange = [&](const char *key, const QString &a, const QString &b) {
|
||||
// omit empty / 0-0 ranges (xray may reject "0-0")
|
||||
const bool aZero = a.isEmpty() || a == QLatin1String("0");
|
||||
const bool bZero = b.isEmpty() || b == QLatin1String("0");
|
||||
if (aZero && bZero)
|
||||
return;
|
||||
const QString aV = a.isEmpty() ? QStringLiteral("0") : a;
|
||||
const QString bV = b.isEmpty() ? QStringLiteral("0") : b;
|
||||
mux[QString::fromUtf8(key)] = makeRangeString(aV, bV);
|
||||
};
|
||||
addMuxRange("maxConcurrency", xhttp.xmux.maxConcurrencyMin, xhttp.xmux.maxConcurrencyMax);
|
||||
addMuxRange("maxConnections", xhttp.xmux.maxConnectionsMin, xhttp.xmux.maxConnectionsMax);
|
||||
addMuxRange("cMaxReuseTimes", xhttp.xmux.cMaxReuseTimesMin, xhttp.xmux.cMaxReuseTimesMax);
|
||||
addMuxRange("hMaxRequestTimes", xhttp.xmux.hMaxRequestTimesMin, xhttp.xmux.hMaxRequestTimesMax);
|
||||
addMuxRange("hMaxReusableSecs", xhttp.xmux.hMaxReusableSecsMin, xhttp.xmux.hMaxReusableSecsMax);
|
||||
if (!xhttp.xmux.hKeepAlivePeriod.isEmpty())
|
||||
mux[QStringLiteral("hKeepAlivePeriod")] = xhttp.xmux.hKeepAlivePeriod.toLongLong();
|
||||
if (!mux.isEmpty())
|
||||
xo[QStringLiteral("xmux")] = mux;
|
||||
}
|
||||
|
||||
streamSettings[QStringLiteral("xhttpSettings")] = xo;
|
||||
}
|
||||
|
||||
if (srv.transport == QLatin1String("mkcp")) {
|
||||
QJsonObject kcpObj;
|
||||
const QString ttiEff = mkcp.tti.isEmpty() ? QString::fromLatin1(px::defaultMkcpTti) : mkcp.tti;
|
||||
const QString upEff = mkcp.uplinkCapacity.isEmpty() ? QString::fromLatin1(px::defaultMkcpUplinkCapacity)
|
||||
: mkcp.uplinkCapacity;
|
||||
const QString downEff = mkcp.downlinkCapacity.isEmpty() ? QString::fromLatin1(px::defaultMkcpDownlinkCapacity)
|
||||
: mkcp.downlinkCapacity;
|
||||
const QString rbufEff = mkcp.readBufferSize.isEmpty() ? QString::fromLatin1(px::defaultMkcpReadBufferSize)
|
||||
: mkcp.readBufferSize;
|
||||
const QString wbufEff = mkcp.writeBufferSize.isEmpty() ? QString::fromLatin1(px::defaultMkcpWriteBufferSize)
|
||||
: mkcp.writeBufferSize;
|
||||
kcpObj[QStringLiteral("tti")] = ttiEff.toInt();
|
||||
kcpObj[QStringLiteral("uplinkCapacity")] = upEff.toInt();
|
||||
kcpObj[QStringLiteral("downlinkCapacity")] = downEff.toInt();
|
||||
kcpObj[QStringLiteral("readBufferSize")] = rbufEff.toInt();
|
||||
kcpObj[QStringLiteral("writeBufferSize")] = wbufEff.toInt();
|
||||
kcpObj[QStringLiteral("congestion")] = mkcp.congestion;
|
||||
streamSettings[QStringLiteral("kcpSettings")] = kcpObj;
|
||||
}
|
||||
|
||||
return streamSettings;
|
||||
}
|
||||
|
||||
ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container,
|
||||
const ContainerConfig &containerConfig,
|
||||
const DnsSettings &dnsSettings,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#define XRAY_CONFIGURATOR_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "configuratorBase.h"
|
||||
@@ -33,9 +34,24 @@ public:
|
||||
amnezia::ErrorCode writeServerConfigForSetup(const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container,
|
||||
amnezia::ContainerConfig &containerConfig,
|
||||
const amnezia::DnsSettings &dnsSettings);
|
||||
const amnezia::DnsSettings &dnsSettings,
|
||||
bool useAtomicApply = false);
|
||||
|
||||
private:
|
||||
QJsonArray collectServerClients(const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container, const QString &flowValue,
|
||||
const QString &fallbackClientId, amnezia::ErrorCode &outError) const;
|
||||
|
||||
amnezia::ErrorCode uploadServerConfigAtomically(const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container, const QString &listenPort,
|
||||
const QJsonObject &serverConfig) const;
|
||||
|
||||
bool restartXrayContainer(const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container) const;
|
||||
|
||||
bool xrayProcessIsUp(const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container) const;
|
||||
|
||||
amnezia::ErrorCode readContainerKeyFile(amnezia::DockerContainer container,
|
||||
const amnezia::ServerCredentials &credentials,
|
||||
const QString &path, QString &out) const;
|
||||
@@ -44,24 +60,23 @@ private:
|
||||
const amnezia::DnsSettings &dnsSettings,
|
||||
amnezia::ErrorCode &errorCode);
|
||||
|
||||
amnezia::ErrorCode uploadServerConfigJson(const amnezia::ServerCredentials &credentials, amnezia::DockerContainer container,
|
||||
const amnezia::DnsSettings &dnsSettings, const QJsonObject &serverConfig) const;
|
||||
|
||||
amnezia::XrayProtocolConfig buildClientProtocolConfig(const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container,
|
||||
const amnezia::XrayServerConfig &srv,
|
||||
const QString &clientId,
|
||||
amnezia::ErrorCode &errorCode,
|
||||
const QString &prefetchedRealityPublicKey = {},
|
||||
const QString &prefetchedRealityShortId = {}) const;
|
||||
const QString &prefetchedRealityShortId = {},
|
||||
const QString &prefetchedTlsPin = {}) const;
|
||||
|
||||
amnezia::ErrorCode readRealityKeyFiles(amnezia::DockerContainer container,
|
||||
const amnezia::ServerCredentials &credentials,
|
||||
QString &outPublicKey,
|
||||
QString &outShortId) const;
|
||||
|
||||
QJsonObject buildStreamSettings(const amnezia::XrayServerConfig &srv,
|
||||
const QString &clientId) const;
|
||||
amnezia::ErrorCode ensureTlsCertificate(const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container,
|
||||
QString &outFingerprint) const;
|
||||
};
|
||||
|
||||
#endif // XRAY_CONFIGURATOR_H
|
||||
@@ -76,12 +76,13 @@ QFuture<QPair<ErrorCode, QJsonArray>> NewsController::fetchNews()
|
||||
return QtFuture::makeReadyFuture(qMakePair(ErrorCode::NoError, QJsonArray()));
|
||||
}
|
||||
|
||||
auto gatewayController = QSharedPointer<GatewayController>::create(
|
||||
m_appSettingsRepository->getGatewayEndpoint(),
|
||||
m_appSettingsRepository->isDevGatewayEnv(),
|
||||
apiDefs::requestTimeoutMsecs,
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled(),
|
||||
m_appSettingsRepository);
|
||||
QSharedPointer<GatewayController> gatewayController(
|
||||
new GatewayController(m_appSettingsRepository->getGatewayEndpoint(),
|
||||
m_appSettingsRepository->isDevGatewayEnv(),
|
||||
apiDefs::requestTimeoutMsecs,
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled(),
|
||||
m_appSettingsRepository),
|
||||
&QObject::deleteLater);
|
||||
|
||||
// both country codes and service types are arrays here, one entry per gateway stack the user has
|
||||
const QJsonObject payload = GatewayPayloadBuilder(m_appSettingsRepository)
|
||||
|
||||
@@ -188,6 +188,9 @@ ErrorCode StorePurchaseController::importServiceFromMarket(const QString &userCo
|
||||
|
||||
quint16 crc = qChecksum(QJsonDocument(configObject).toJson());
|
||||
|
||||
if (!serverConfigUtils::isConfigFormatVersionSupported(configObject)) {
|
||||
return ErrorCode::ConfigFormatVersionNotSupportedError;
|
||||
}
|
||||
if (configObject.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
@@ -239,6 +239,9 @@ ErrorCode SubscriptionController::applyImportedServiceConfig(const QString &user
|
||||
|
||||
updateApiConfigInJson(serverConfigJson, serviceType, serviceProtocol, userCountryCode, responseBody);
|
||||
|
||||
if (!serverConfigUtils::isConfigFormatVersionSupported(serverConfigJson)) {
|
||||
return ErrorCode::ConfigFormatVersionNotSupportedError;
|
||||
}
|
||||
if (serverConfigJson.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
@@ -291,6 +294,9 @@ ErrorCode SubscriptionController::importTrialFromGateway(const QString &userCoun
|
||||
}
|
||||
|
||||
QJsonObject configObject = QJsonDocument::fromJson(configBytes).object();
|
||||
if (!serverConfigUtils::isConfigFormatVersionSupported(configObject)) {
|
||||
return ErrorCode::ConfigFormatVersionNotSupportedError;
|
||||
}
|
||||
if (configObject.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
@@ -301,7 +307,7 @@ ErrorCode SubscriptionController::importTrialFromGateway(const QString &userCoun
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, bool isConnectEvent,
|
||||
ErrorCode SubscriptionController::updateServiceFromGateway(const QString &serverId, const QString &serverCountryCode, bool isConnectEvent,
|
||||
CaptchaInfo *captchaInfoOut, ProtocolData *usedProtocolDataOut)
|
||||
{
|
||||
auto apiV2 = m_serversRepository->apiV2Config(serverId);
|
||||
@@ -311,11 +317,11 @@ ErrorCode SubscriptionController::updateServiceFromGateway(const QString &server
|
||||
const bool isTestPurchase = apiV2->apiConfig.isTestPurchase;
|
||||
QString serviceProtocol = apiV2->serviceProtocol();
|
||||
|
||||
if (!newCountryCode.isEmpty()) {
|
||||
if (!serverCountryCode.isEmpty()) {
|
||||
const auto availableCountries = apiV2->apiConfig.availableCountries;
|
||||
for (const auto &country : availableCountries) {
|
||||
const auto countryObject = country.toObject();
|
||||
if (countryObject.value(apiDefs::key::serverCountryCode).toString() != newCountryCode) {
|
||||
if (countryObject.value(apiDefs::key::serverCountryCode).toString() != serverCountryCode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -331,7 +337,7 @@ ErrorCode SubscriptionController::updateServiceFromGateway(const QString &server
|
||||
|
||||
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
|
||||
.addField(apiDefs::key::userCountryCode, apiV2->apiConfig.userCountryCode)
|
||||
.addField(apiDefs::key::serverCountryCode, newCountryCode)
|
||||
.addField(apiDefs::key::serverCountryCode, serverCountryCode)
|
||||
.addField(apiDefs::key::serviceType, apiV2->serviceType())
|
||||
.addField(apiDefs::key::serviceProtocol, serviceProtocol)
|
||||
.addField(apiDefs::key::publicKey, publicKeyForProtocol(serviceProtocol, protocolData))
|
||||
@@ -356,6 +362,34 @@ ErrorCode SubscriptionController::updateServiceFromGateway(const QString &server
|
||||
return applyUpdatedServiceConfig(serverId, serviceProtocol, protocolData, responseBody);
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::fetchServiceConfig(const QString &serverId, const QString &serviceProtocol,
|
||||
QJsonObject &serverConfigJson)
|
||||
{
|
||||
auto apiV2 = m_serversRepository->apiV2Config(serverId);
|
||||
if (!apiV2.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
ProtocolData protocolData = generateProtocolData(serviceProtocol);
|
||||
|
||||
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
|
||||
.addField(apiDefs::key::userCountryCode, apiV2->apiConfig.userCountryCode)
|
||||
.addField(apiDefs::key::serverCountryCode, apiV2->apiConfig.serverCountryCode)
|
||||
.addField(apiDefs::key::serviceType, apiV2->serviceType())
|
||||
.addField(apiDefs::key::serviceProtocol, serviceProtocol)
|
||||
.addField(apiDefs::key::publicKey, publicKeyForProtocol(serviceProtocol, protocolData))
|
||||
.addField(apiDefs::key::authData, apiV2->authData.toJson())
|
||||
.build();
|
||||
|
||||
QByteArray responseBody;
|
||||
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody, apiV2->apiConfig.isTestPurchase);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
return extractServerConfigJsonFromResponse(responseBody, serviceProtocol, protocolData, serverConfigJson);
|
||||
}
|
||||
|
||||
void SubscriptionController::markSubscriptionExpiredByServer(const QString &serverId, const ApiV2ServerConfig &apiV2)
|
||||
{
|
||||
ApiV2ServerConfig expiredApiV2 = apiV2;
|
||||
@@ -380,6 +414,9 @@ ErrorCode SubscriptionController::applyUpdatedServiceConfig(const QString &serve
|
||||
|
||||
updateApiConfigInJson(serverConfigJson, apiV2->apiConfig.serviceType, serviceProtocol, apiV2->apiConfig.userCountryCode, responseBody);
|
||||
|
||||
if (!serverConfigUtils::isConfigFormatVersionSupported(serverConfigJson)) {
|
||||
return ErrorCode::ConfigFormatVersionNotSupportedError;
|
||||
}
|
||||
if (serverConfigJson.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
@@ -403,7 +440,7 @@ ErrorCode SubscriptionController::applyUpdatedServiceConfig(const QString &serve
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::resolveUpdateServiceCaptcha(const QString &serverId, const QString &newCountryCode,
|
||||
ErrorCode SubscriptionController::resolveUpdateServiceCaptcha(const QString &serverId, const QString &serverCountryCode,
|
||||
bool isConnectEvent, const ProtocolData &protocolData,
|
||||
const QString &captchaId, const QString &captchaSolution,
|
||||
CaptchaInfo *retryCaptchaOut)
|
||||
@@ -417,7 +454,7 @@ ErrorCode SubscriptionController::resolveUpdateServiceCaptcha(const QString &ser
|
||||
|
||||
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
|
||||
.addField(apiDefs::key::userCountryCode, apiV2->apiConfig.userCountryCode)
|
||||
.addField(apiDefs::key::serverCountryCode, newCountryCode)
|
||||
.addField(apiDefs::key::serverCountryCode, serverCountryCode)
|
||||
.addField(apiDefs::key::serviceType, apiV2->serviceType())
|
||||
.addField(apiDefs::key::serviceProtocol, serviceProtocol)
|
||||
.addField(apiDefs::key::publicKey, publicKeyForProtocol(serviceProtocol, protocolData))
|
||||
@@ -590,17 +627,19 @@ ErrorCode SubscriptionController::prepareVpnKeyExport(const QString &serverId, Q
|
||||
ErrorCode SubscriptionController::validateAndUpdateConfig(const QString &serverId, bool hasInstalledContainers,
|
||||
CaptchaInfo *captchaInfoOut, ProtocolData *usedProtocolDataOut)
|
||||
{
|
||||
if (!m_serversRepository->apiV2Config(serverId).has_value()) {
|
||||
const auto apiV2 = m_serversRepository->apiV2Config(serverId);
|
||||
if (!apiV2.has_value()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
const QString serverCountryCode = apiV2->apiConfig.serverCountryCode;
|
||||
|
||||
if (!hasInstalledContainers) {
|
||||
return updateServiceFromGateway(serverId, "", true, captchaInfoOut, usedProtocolDataOut);
|
||||
return updateServiceFromGateway(serverId, serverCountryCode, true, captchaInfoOut, usedProtocolDataOut);
|
||||
}
|
||||
|
||||
if (isApiKeyExpired(serverId)) {
|
||||
qDebug() << "attempt to update api config by expires_at event";
|
||||
return updateServiceFromGateway(serverId, "", true, captchaInfoOut, usedProtocolDataOut);
|
||||
return updateServiceFromGateway(serverId, serverCountryCode, true, captchaInfoOut, usedProtocolDataOut);
|
||||
}
|
||||
|
||||
return ErrorCode::NoError;
|
||||
|
||||
@@ -45,10 +45,12 @@ public:
|
||||
ErrorCode importTrialFromGateway(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const QString &email);
|
||||
|
||||
ErrorCode updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, bool isConnectEvent,
|
||||
ErrorCode fetchServiceConfig(const QString &serverId, const QString &serviceProtocol, QJsonObject &serverConfigJson);
|
||||
|
||||
ErrorCode updateServiceFromGateway(const QString &serverId, const QString &serverCountryCode, bool isConnectEvent,
|
||||
CaptchaInfo *captchaInfoOut = nullptr, ProtocolData *usedProtocolDataOut = nullptr);
|
||||
|
||||
ErrorCode resolveUpdateServiceCaptcha(const QString &serverId, const QString &newCountryCode, bool isConnectEvent,
|
||||
ErrorCode resolveUpdateServiceCaptcha(const QString &serverId, const QString &serverCountryCode, bool isConnectEvent,
|
||||
const ProtocolData &protocolData, const QString &captchaId,
|
||||
const QString &captchaSolution, CaptchaInfo *retryCaptchaOut = nullptr);
|
||||
|
||||
@@ -82,6 +84,21 @@ public:
|
||||
const QString &captchaId, const QString &captchaSolution,
|
||||
CaptchaInfo *retryCaptchaOut = nullptr);
|
||||
|
||||
#ifdef AMNEZIA_BUILD_TESTS
|
||||
// Exposes the gateway response parsing path without performing a network request.
|
||||
ErrorCode applyImportedServiceConfigForTest(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const ProtocolData &protocolData,
|
||||
const QByteArray &responseBody)
|
||||
{
|
||||
return applyImportedServiceConfig(userCountryCode, serviceType, serviceProtocol, protocolData, responseBody);
|
||||
}
|
||||
ErrorCode applyUpdatedServiceConfigForTest(const QString &serverId, const QString &serviceProtocol,
|
||||
const ProtocolData &protocolData, const QByteArray &responseBody)
|
||||
{
|
||||
return applyUpdatedServiceConfig(serverId, serviceProtocol, protocolData, responseBody);
|
||||
}
|
||||
#endif
|
||||
|
||||
private:
|
||||
ErrorCode executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody, bool isTestPurchase = false);
|
||||
bool isApiKeyExpired(const QString &serverId) const;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "coreController.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDirIterator>
|
||||
#include <QTranslator>
|
||||
#include <QTimer>
|
||||
@@ -19,6 +20,9 @@
|
||||
|
||||
#if defined(Q_OS_IOS)
|
||||
#include "platforms/ios/ios_controller.h"
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
#include "core/utils/swiftBridge.h"
|
||||
#endif
|
||||
|
||||
@@ -116,6 +120,9 @@ 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);
|
||||
|
||||
@@ -125,6 +132,12 @@ void CoreController::initModels()
|
||||
m_apiCountryModel = new ApiCountryModel(this);
|
||||
setQmlContextProperty("ApiCountryModel", m_apiCountryModel);
|
||||
|
||||
m_apiCountryListModel = new ApiCountryListModel(m_apiCountryModel, "picker", this);
|
||||
setQmlContextProperty("ApiCountryListModel", m_apiCountryListModel);
|
||||
|
||||
m_apiConfigsCountryListModel = new ApiCountryListModel(m_apiCountryModel, "nativeConfigs", this);
|
||||
setQmlContextProperty("ApiConfigsCountryListModel", m_apiConfigsCountryListModel);
|
||||
|
||||
m_apiSubscriptionPlansModel = new ApiSubscriptionPlansModel(this);
|
||||
setQmlContextProperty("ApiSubscriptionPlansModel", m_apiSubscriptionPlansModel);
|
||||
|
||||
@@ -169,6 +182,7 @@ void CoreController::initCoreControllers()
|
||||
m_importCoreController = new ImportController(m_serversRepository, m_appSettingsRepository, this);
|
||||
m_connectionController = new ConnectionController(m_serversRepository, m_appSettingsRepository, m_vpnConnection.get(), this);
|
||||
m_settingsController = new SettingsController(m_serversRepository, m_appSettingsRepository, this);
|
||||
m_localProxyController = new LocalProxyController(m_serversRepository, m_appSettingsRepository, m_subscriptionController, this);
|
||||
}
|
||||
|
||||
void CoreController::initControllers()
|
||||
@@ -178,7 +192,6 @@ void CoreController::initControllers()
|
||||
|
||||
if (m_engine) {
|
||||
m_focusController = new FocusController(m_engine, this);
|
||||
setQmlContextProperty("FocusController", m_focusController);
|
||||
}
|
||||
|
||||
m_installUiController = new InstallUiController(m_installController, m_serversController, m_settingsController, m_protocolsModel, m_usersController,
|
||||
@@ -187,7 +200,7 @@ void CoreController::initControllers()
|
||||
m_ikev2ConfigModel,
|
||||
#endif
|
||||
m_sftpConfigModel, m_socks5ConfigModel, m_mtProxyConfigModel, m_telemtConfigModel,
|
||||
m_tProxyConfigModel,
|
||||
m_tProxyConfigModel, m_whatsAppProxyConfigModel,
|
||||
m_connectionController, this);
|
||||
setQmlContextProperty("InstallController", m_installUiController);
|
||||
|
||||
@@ -203,6 +216,12 @@ void CoreController::initControllers()
|
||||
m_settingsUiController = new SettingsUiController(m_settingsController, m_serversController, this);
|
||||
setQmlContextProperty("SettingsController", m_settingsUiController);
|
||||
|
||||
m_localProxyUiController = new LocalProxyUiController(m_localProxyController, this);
|
||||
setQmlContextProperty("LocalProxyController", m_localProxyUiController);
|
||||
|
||||
m_logsUiController = new LogsUiController(m_settingsController, this);
|
||||
setQmlContextProperty("LogsController", m_logsUiController);
|
||||
|
||||
m_pageController = new PageController(m_serversController, m_settingsController, this);
|
||||
setQmlContextProperty("PageController", m_pageController);
|
||||
|
||||
@@ -271,7 +290,7 @@ void CoreController::initAppleController()
|
||||
|
||||
void CoreController::initLogging()
|
||||
{
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
#ifndef Q_OS_ANDROID
|
||||
bool enabled = m_appSettingsRepository->isSaveLogs();
|
||||
if (enabled) {
|
||||
if (!Logger::init(false)) {
|
||||
@@ -280,6 +299,11 @@ void CoreController::initLogging()
|
||||
}
|
||||
Logger::setServiceLogsEnabled(enabled);
|
||||
#endif
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
if (m_appSettingsRepository->isSaveLogs()) {
|
||||
SWIFT_BRIDGE_NAMESPACE::toggleLogging(true);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void CoreController::initSignalHandlers()
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
#include <QObject>
|
||||
#include <QQmlContext>
|
||||
#include <QSharedPointer>
|
||||
#include <QThread>
|
||||
#include <QTranslator>
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
#include "ui/utils/systemTrayNotificationHandler.h"
|
||||
@@ -22,6 +24,8 @@
|
||||
#include "ui/controllers/selfhosted/installUiController.h"
|
||||
#include "ui/controllers/qml/pageController.h"
|
||||
#include "ui/controllers/settingsUiController.h"
|
||||
#include "ui/controllers/localProxyUiController.h"
|
||||
#include "ui/controllers/logsUiController.h"
|
||||
#include "ui/controllers/serversUiController.h"
|
||||
#include "ui/controllers/ipSplitTunnelingUiController.h"
|
||||
#include "ui/controllers/systemController.h"
|
||||
@@ -41,6 +45,7 @@
|
||||
#include "core/controllers/api/newsController.h"
|
||||
#include "core/controllers/selfhosted/installController.h"
|
||||
#include "core/controllers/settingsController.h"
|
||||
#include "core/controllers/localProxyController.h"
|
||||
#include "core/controllers/connectionController.h"
|
||||
#include "core/controllers/updateController.h"
|
||||
|
||||
@@ -57,6 +62,7 @@
|
||||
#include "ui/models/api/apiAccountInfoModel.h"
|
||||
#include "ui/models/api/apiBenefitsModel.h"
|
||||
#include "ui/models/api/apiCountryModel.h"
|
||||
#include "ui/models/api/apiCountryListModel.h"
|
||||
#include "ui/models/api/apiDevicesModel.h"
|
||||
#include "ui/models/api/apiServicesModel.h"
|
||||
#include "ui/models/api/apiSubscriptionPlansModel.h"
|
||||
@@ -75,6 +81,7 @@
|
||||
#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"
|
||||
@@ -136,6 +143,10 @@ protected:
|
||||
ServersUiController* serversUiControllerProtected() const { return m_serversUiController; }
|
||||
ServicesCatalogUiController* servicesCatalogUiControllerProtected() const { return m_servicesCatalogUiController; }
|
||||
ApiNewsUiController* apiNewsUiControllerProtected() const { return m_apiNewsUiController; }
|
||||
SubscriptionController* subscriptionControllerProtected() const { return m_subscriptionController; }
|
||||
ApiCountryModel* apiCountryModelProtected() const { return m_apiCountryModel; }
|
||||
ApiCountryListModel* apiCountryListModelProtected() const { return m_apiCountryListModel; }
|
||||
ApiCountryListModel* apiConfigsCountryListModelProtected() const { return m_apiConfigsCountryListModel; }
|
||||
|
||||
private:
|
||||
void initRepositories();
|
||||
@@ -163,13 +174,15 @@ private:
|
||||
QMetaObject::Connection m_reloadConfigErrorOccurredConnection;
|
||||
|
||||
ConnectionUiController* m_connectionUiController;
|
||||
FocusController* m_focusController;
|
||||
FocusController* m_focusController = nullptr;
|
||||
PageController* m_pageController;
|
||||
InstallUiController* m_installUiController;
|
||||
ImportUiController* m_importController;
|
||||
ImportController* m_importCoreController;
|
||||
ExportUiController* m_exportUiController;
|
||||
SettingsUiController* m_settingsUiController;
|
||||
LocalProxyUiController* m_localProxyUiController;
|
||||
LogsUiController* m_logsUiController;
|
||||
ServersUiController* m_serversUiController;
|
||||
IpSplitTunnelingUiController* m_ipSplitTunnelingUiController;
|
||||
SystemController* m_systemController;
|
||||
@@ -198,6 +211,7 @@ private:
|
||||
ExportController* m_exportController;
|
||||
ConnectionController* m_connectionController;
|
||||
SettingsController* m_settingsController;
|
||||
LocalProxyController* m_localProxyController;
|
||||
|
||||
ContainersModel* m_containersModel;
|
||||
ContainersModel* m_defaultServerContainersModel;
|
||||
@@ -214,6 +228,8 @@ private:
|
||||
ApiSubscriptionPlansModel* m_apiSubscriptionPlansModel;
|
||||
ApiBenefitsModel* m_apiBenefitsModel;
|
||||
ApiCountryModel* m_apiCountryModel;
|
||||
ApiCountryListModel* m_apiCountryListModel;
|
||||
ApiCountryListModel* m_apiConfigsCountryListModel;
|
||||
ApiAccountInfoModel* m_apiAccountInfoModel;
|
||||
ApiDevicesModel* m_apiDevicesModel;
|
||||
|
||||
@@ -231,6 +247,7 @@ private:
|
||||
MtProxyConfigModel* m_mtProxyConfigModel;
|
||||
TelemtConfigModel* m_telemtConfigModel;
|
||||
TProxyConfigModel* m_tProxyConfigModel;
|
||||
WhatsAppProxyConfigModel* m_whatsAppProxyConfigModel;
|
||||
|
||||
CoreSignalHandlers* m_signalHandlers;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "coreSignalHandlers.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QTimer>
|
||||
#include <QtConcurrent>
|
||||
|
||||
@@ -11,6 +12,7 @@
|
||||
#include "core/utils/serverConfigUtils.h"
|
||||
#include "core/repositories/secureAppSettingsRepository.h"
|
||||
#include "vpnConnection.h"
|
||||
#include "ui/controllers/qml/focusController.h"
|
||||
#include "ui/controllers/qml/pageController.h"
|
||||
#include "ui/controllers/connectionUiController.h"
|
||||
#include "ui/controllers/settingsUiController.h"
|
||||
@@ -32,6 +34,7 @@
|
||||
#include "core/controllers/selfhosted/installController.h"
|
||||
#include "core/controllers/selfhosted/exportController.h"
|
||||
#include "core/controllers/connectionController.h"
|
||||
#include "core/controllers/localProxyController.h"
|
||||
#include "ui/models/clientManagementModel.h"
|
||||
#include "ui/controllers/api/apiNewsUiController.h"
|
||||
#include "ui/models/containersModel.h"
|
||||
@@ -65,6 +68,8 @@ void CoreSignalHandlers::initAllHandlers()
|
||||
initExportControllerHandler();
|
||||
initImportControllerHandler();
|
||||
initApiCountryModelUpdateHandler();
|
||||
initCountryListSortModeHandler();
|
||||
initCountryListStateHandler();
|
||||
initSubscriptionRefreshHandler();
|
||||
initAdminConfigRevokedHandler();
|
||||
initPassphraseRequestHandler();
|
||||
@@ -79,6 +84,7 @@ void CoreSignalHandlers::initAllHandlers()
|
||||
initAppSplitTunnelingModelUpdateHandler();
|
||||
initPrepareConfigHandler();
|
||||
initUnsupportedConnectDrawerHandler();
|
||||
initBackNavigationHandler();
|
||||
initStrictKillSwitchHandler();
|
||||
initAndroidSettingsHandler();
|
||||
initAndroidConnectionHandler();
|
||||
@@ -86,6 +92,7 @@ void CoreSignalHandlers::initAllHandlers()
|
||||
initIosSettingsHandler();
|
||||
initNotificationHandler();
|
||||
initUpdateFoundHandler();
|
||||
initLocalProxyHandler();
|
||||
}
|
||||
|
||||
void CoreSignalHandlers::initErrorMessagesHandler()
|
||||
@@ -173,6 +180,85 @@ void CoreSignalHandlers::initImportControllerHandler()
|
||||
});
|
||||
}
|
||||
|
||||
void CoreSignalHandlers::initCountryListSortModeHandler()
|
||||
{
|
||||
SecureAppSettingsRepository *repository = m_coreController->m_appSettingsRepository;
|
||||
|
||||
const QVector<ApiCountryListModel *> models { m_coreController->m_apiCountryListModel,
|
||||
m_coreController->m_apiConfigsCountryListModel };
|
||||
|
||||
const auto reloadSortModes = [repository, models]() {
|
||||
for (ApiCountryListModel *model : models) {
|
||||
model->setSortMode(repository->countryListSortMode(model->listId()));
|
||||
}
|
||||
};
|
||||
reloadSortModes();
|
||||
|
||||
for (ApiCountryListModel *model : models) {
|
||||
connect(model, &ApiCountryListModel::sortModeChanged, this, [repository, model]() {
|
||||
repository->setCountryListSortMode(model->listId(), model->sortMode());
|
||||
});
|
||||
}
|
||||
|
||||
connect(m_coreController->m_settingsUiController, &SettingsUiController::restoreBackupFinished, this, reloadSortModes);
|
||||
connect(repository, &SecureAppSettingsRepository::settingsCleared, this, reloadSortModes);
|
||||
}
|
||||
|
||||
void CoreSignalHandlers::initCountryListStateHandler()
|
||||
{
|
||||
SecureAppSettingsRepository *repository = m_coreController->m_appSettingsRepository;
|
||||
|
||||
const QVector<ApiCountryListModel *> models { m_coreController->m_apiCountryListModel,
|
||||
m_coreController->m_apiConfigsCountryListModel };
|
||||
|
||||
ApiCountryModel *countryModel = m_coreController->m_apiCountryModel;
|
||||
SecureServersRepository *serversRepository = m_coreController->m_serversRepository;
|
||||
|
||||
const auto reloadFavorites = [repository, models, countryModel]() {
|
||||
const QStringList codes = repository->favoriteLocations(countryModel->serverId());
|
||||
for (ApiCountryListModel *model : models) {
|
||||
model->setFavorites(codes);
|
||||
}
|
||||
};
|
||||
const auto migrateLegacyFavorites = [repository, serversRepository]() {
|
||||
QStringList apiServerIds;
|
||||
for (const QString &serverId : serversRepository->orderedServerIds()) {
|
||||
if (serversRepository->apiV2Config(serverId).has_value()) {
|
||||
apiServerIds.append(serverId);
|
||||
}
|
||||
}
|
||||
repository->migrateLegacyFavoriteLocations(apiServerIds);
|
||||
};
|
||||
migrateLegacyFavorites();
|
||||
reloadFavorites();
|
||||
|
||||
connect(countryModel, &ApiCountryModel::serverIdChanged, this, reloadFavorites);
|
||||
connect(m_coreController->m_settingsUiController, &SettingsUiController::restoreBackupFinished, this,
|
||||
[migrateLegacyFavorites, reloadFavorites]() {
|
||||
migrateLegacyFavorites();
|
||||
reloadFavorites();
|
||||
});
|
||||
connect(repository, &SecureAppSettingsRepository::settingsCleared, this, reloadFavorites);
|
||||
connect(serversRepository, &SecureServersRepository::serverRemoved, this,
|
||||
[repository, countryModel](const QString &serverId) {
|
||||
repository->removeFavoriteLocations(serverId);
|
||||
if (countryModel->serverId() == serverId) {
|
||||
countryModel->setServerId(QString());
|
||||
}
|
||||
});
|
||||
|
||||
for (ApiCountryListModel *model : models) {
|
||||
connect(model, &ApiCountryListModel::favoritesChanged, this, [repository, models, model, countryModel](const QStringList &codes) {
|
||||
repository->setFavoriteLocations(countryModel->serverId(), codes);
|
||||
for (ApiCountryListModel *other : models) {
|
||||
if (other != model) {
|
||||
other->setFavorites(codes);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void CoreSignalHandlers::initApiCountryModelUpdateHandler()
|
||||
{
|
||||
connect(m_coreController->m_serversUiController, &ServersUiController::updateApiCountryModel, this, [this]() {
|
||||
@@ -186,8 +272,10 @@ void CoreSignalHandlers::initApiCountryModelUpdateHandler()
|
||||
return;
|
||||
}
|
||||
|
||||
m_coreController->m_apiCountryModel->setServerId(processedServerId);
|
||||
m_coreController->m_apiCountryModel->updateModel(apiV2->apiConfig.availableCountries,
|
||||
apiV2->apiConfig.serverCountryCode);
|
||||
apiV2->apiConfig.serverCountryCode,
|
||||
apiV2->apiConfig.userCountryCode);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -230,6 +318,18 @@ void CoreSignalHandlers::initTranslationsUpdatedHandler()
|
||||
connect(m_coreController->m_languageUiController, &LanguageUiController::updateTranslations, m_coreController, &CoreController::updateTranslator);
|
||||
connect(m_coreController, &CoreController::translationsUpdated, m_coreController->m_languageUiController, &LanguageUiController::translationsUpdated);
|
||||
connect(m_coreController, &CoreController::translationsUpdated, m_coreController->m_connectionUiController, &ConnectionUiController::onTranslationsUpdated);
|
||||
connect(m_coreController, &CoreController::translationsUpdated, m_coreController->m_logsUiController, &LogsUiController::refreshSizes);
|
||||
|
||||
const QVector<ApiCountryListModel *> countryListModels { m_coreController->m_apiCountryListModel,
|
||||
m_coreController->m_apiConfigsCountryListModel };
|
||||
auto applyUiLanguage = [this, countryListModels]() {
|
||||
const QString languageCode = QLocale::languageToCode(m_coreController->m_settingsController->getAppLanguage().language());
|
||||
for (ApiCountryListModel *model : countryListModels) {
|
||||
model->setUiLanguage(languageCode);
|
||||
}
|
||||
};
|
||||
applyUiLanguage();
|
||||
connect(m_coreController, &CoreController::translationsUpdated, this, applyUiLanguage);
|
||||
}
|
||||
|
||||
void CoreSignalHandlers::initLanguageHandler()
|
||||
@@ -359,6 +459,16 @@ void CoreSignalHandlers::initUnsupportedConnectDrawerHandler()
|
||||
m_coreController->m_pageController, &PageController::unsupportedConnectDrawerRequested);
|
||||
}
|
||||
|
||||
void CoreSignalHandlers::initBackNavigationHandler()
|
||||
{
|
||||
if (!m_coreController->m_focusController) {
|
||||
return;
|
||||
}
|
||||
|
||||
connect(m_coreController->m_focusController, &FocusController::backRequested,
|
||||
m_coreController->m_pageController, &PageController::escapePressed);
|
||||
}
|
||||
|
||||
void CoreSignalHandlers::initStrictKillSwitchHandler()
|
||||
{
|
||||
connect(m_coreController->m_settingsUiController, &SettingsUiController::strictKillSwitchEnabledChanged, m_coreController->m_connectionController,
|
||||
@@ -444,3 +554,12 @@ void CoreSignalHandlers::initUpdateFoundHandler()
|
||||
});
|
||||
}
|
||||
|
||||
void CoreSignalHandlers::initLocalProxyHandler()
|
||||
{
|
||||
connect(m_coreController->m_connectionController, &ConnectionController::openConnectionRequested, m_coreController->m_localProxyController,
|
||||
&LocalProxyController::onVpnConnectionRequested);
|
||||
connect(m_coreController->m_localProxyController, &LocalProxyController::stoppedByVpn, m_coreController->m_pageController,
|
||||
&PageController::showNotificationMessage);
|
||||
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, m_coreController->m_localProxyController,
|
||||
&LocalProxyController::disable);
|
||||
}
|
||||
@@ -20,6 +20,8 @@ private:
|
||||
void initExportControllerHandler();
|
||||
void initImportControllerHandler();
|
||||
void initApiCountryModelUpdateHandler();
|
||||
void initCountryListSortModeHandler();
|
||||
void initCountryListStateHandler();
|
||||
void initSubscriptionRefreshHandler();
|
||||
void initAdminConfigRevokedHandler();
|
||||
void initPassphraseRequestHandler();
|
||||
@@ -34,6 +36,7 @@ private:
|
||||
void initAppSplitTunnelingModelUpdateHandler();
|
||||
void initPrepareConfigHandler();
|
||||
void initUnsupportedConnectDrawerHandler();
|
||||
void initBackNavigationHandler();
|
||||
void initStrictKillSwitchHandler();
|
||||
void initAndroidSettingsHandler();
|
||||
void initAndroidConnectionHandler();
|
||||
@@ -41,6 +44,7 @@ private:
|
||||
void initIosSettingsHandler();
|
||||
void initNotificationHandler();
|
||||
void initUpdateFoundHandler();
|
||||
void initLocalProxyHandler();
|
||||
|
||||
CoreController* m_coreController;
|
||||
};
|
||||
|
||||
@@ -1,107 +1,79 @@
|
||||
#include "gatewayController.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <random>
|
||||
|
||||
#include <QCryptographicHash>
|
||||
#include <QDebug>
|
||||
#include <QEventLoop>
|
||||
#include <QFutureWatcher>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkReply>
|
||||
#include <QPromise>
|
||||
#include <QThread>
|
||||
#include <QUrl>
|
||||
#include <QtConcurrent/QtConcurrentRun>
|
||||
|
||||
#include <openssl/rsa.h>
|
||||
|
||||
#include "amneziaApplication.h"
|
||||
#include "core/repositories/secureAppSettingsRepository.h"
|
||||
#include "core/utils/api/apiUtils.h"
|
||||
#include "core/utils/constants/apiConstants.h"
|
||||
#include "core/utils/constants/apiKeys.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
#include "cryptoUtils.h"
|
||||
#include "core/utils/api/apiUtils.h"
|
||||
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
#include "core/utils/ipcClient.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#include "platforms/ios/ios_controller.h"
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr QLatin1String errorResponsePattern1("No active configuration found for");
|
||||
constexpr QLatin1String errorResponsePattern2("No non-revoked public key found for");
|
||||
constexpr QLatin1String errorResponsePattern3("Account not found.");
|
||||
constexpr QLatin1String errorResponsePatternQrSessionNotFound("QR session not found");
|
||||
constexpr QLatin1String errorResponsePatternSessionNotFound("Session not found");
|
||||
|
||||
constexpr QLatin1String updateRequestResponsePattern("client version update is required");
|
||||
|
||||
constexpr int httpStatusCodeNotFound = 404;
|
||||
constexpr int httpStatusCodeConflict = 409;
|
||||
constexpr int httpStatusCodeNotImplemented = 501;
|
||||
constexpr int httpStatusCodePaymentRequired = 402;
|
||||
constexpr int httpStatusCodeRequestTimeout = 408;
|
||||
constexpr int httpStatusCodeUnprocessableEntity = 422;
|
||||
|
||||
constexpr QLatin1String unprocessableSubscriptionMessage("Failed to retrieve subscription information. Is it activated?");
|
||||
|
||||
constexpr int proxyStorageRequestTimeoutMsecs = 3000;
|
||||
|
||||
QStringList shuffledProxyUrls(const QStringList &proxyUrls)
|
||||
namespace agwConfigKey
|
||||
{
|
||||
QStringList shuffled = proxyUrls;
|
||||
std::random_device randomDevice;
|
||||
std::mt19937 generator(randomDevice());
|
||||
std::shuffle(shuffled.begin(), shuffled.end(), generator);
|
||||
return shuffled;
|
||||
constexpr QLatin1String gatewayEndpoint("gateway_endpoint");
|
||||
constexpr QLatin1String publicKeyPem("public_key_pem");
|
||||
constexpr QLatin1String s3PrimaryEndpoints("s3_primary_endpoints");
|
||||
constexpr QLatin1String s3FallbackEndpoints("s3_fallback_endpoints");
|
||||
constexpr QLatin1String isDevEnvironment("is_dev_environment");
|
||||
constexpr QLatin1String requestTimeoutMsecs("request_timeout_msecs");
|
||||
constexpr QLatin1String proxyRequestTimeoutMsecs("proxy_request_timeout_msecs");
|
||||
constexpr QLatin1String proxyStorageTimeoutMsecs("proxy_storage_timeout_msecs");
|
||||
constexpr QLatin1String proxyHealthTimeoutMsecs("proxy_health_timeout_msecs");
|
||||
}
|
||||
|
||||
QString getProxyUrlsCacheKey(const QString &serviceType, const QString &userCountryCode)
|
||||
namespace agwOptionsKey
|
||||
{
|
||||
return QStringLiteral("service_%1_country_%2").arg(serviceType, userCountryCode);
|
||||
constexpr QLatin1String serviceType("service_type");
|
||||
constexpr QLatin1String userCountryCode("user_country_code");
|
||||
}
|
||||
|
||||
bool decryptProxyUrlsPayload(const QByteArray &encryptedPayload, bool isDevEnvironment, QByteArray &decryptedPayload)
|
||||
namespace agwStateKey
|
||||
{
|
||||
QByteArray key = isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||
if (!isDevEnvironment) {
|
||||
QCryptographicHash hash(QCryptographicHash::Sha512);
|
||||
hash.addData(key);
|
||||
QByteArray h = hash.result().toHex();
|
||||
constexpr QLatin1String workingProxy("working_proxy");
|
||||
}
|
||||
|
||||
QByteArray decKey = QByteArray::fromHex(h.left(64));
|
||||
QByteArray iv = QByteArray::fromHex(h.mid(64, 32));
|
||||
QByteArray ba = QByteArray::fromBase64(encryptedPayload);
|
||||
constexpr QLatin1String agwStateCacheKey("agw_state");
|
||||
constexpr QLatin1String agwTracePrefix("agw trace:");
|
||||
constexpr QLatin1String agwTraceFailoverMarker("suspicious");
|
||||
constexpr QLatin1String endpointListSeparator(", ");
|
||||
constexpr QLatin1String legacyEndpointPlaceholder("%1");
|
||||
|
||||
decryptedPayload = CryptoUtils::decryptAes256Cbc(ba, decKey, iv);
|
||||
if (decryptedPayload.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
QStringList splitEndpoints(const char *raw)
|
||||
{
|
||||
return QString::fromUtf8(raw).split(endpointListSeparator, Qt::SkipEmptyParts);
|
||||
}
|
||||
|
||||
void logAgwTrace(const QString &path, const int code, const QString &trace)
|
||||
{
|
||||
if (trace.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const QString text = QStringLiteral("%1 %2\n%3").arg(agwTracePrefix, path, trace).replace(QLatin1Char('\n'), QStringLiteral("\n "));
|
||||
if (code != AGW_OK) {
|
||||
qWarning().noquote() << text;
|
||||
} else if (trace.contains(agwTraceFailoverMarker)) {
|
||||
qInfo().noquote() << text;
|
||||
} else {
|
||||
decryptedPayload = encryptedPayload;
|
||||
qDebug().noquote() << text;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
QStringList readCachedProxyUrls(const QByteArray &cachedProxyUrlsEncrypted, bool isDevEnvironment)
|
||||
{
|
||||
if (cachedProxyUrlsEncrypted.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QByteArray cachedProxyUrlsDecrypted;
|
||||
if (!decryptProxyUrlsPayload(cachedProxyUrlsEncrypted, isDevEnvironment, cachedProxyUrlsDecrypted)) {
|
||||
qCritical() << "error decrypting cached proxy urls payload";
|
||||
return {};
|
||||
}
|
||||
|
||||
QJsonArray endpointsArray = QJsonDocument::fromJson(cachedProxyUrlsDecrypted).array();
|
||||
QStringList endpoints;
|
||||
endpoints.reserve(endpointsArray.size());
|
||||
for (const QJsonValue &endpoint : endpointsArray) {
|
||||
endpoints.push_back(endpoint.toString());
|
||||
}
|
||||
|
||||
return endpoints;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,615 +82,187 @@ GatewayController::GatewayController(const QString &gatewayEndpoint, const bool
|
||||
QObject *parent)
|
||||
: QObject(parent),
|
||||
m_gatewayEndpoint(gatewayEndpoint),
|
||||
m_isDevEnvironment(isDevEnvironment),
|
||||
m_requestTimeoutMsecs(requestTimeoutMsecs),
|
||||
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled),
|
||||
m_appSettingsRepository(appSettingsRepository)
|
||||
{
|
||||
const QByteArray publicKey = isDevEnvironment ? QByteArray(DEV_AGW_PUBLIC_KEY) : QByteArray(PROD_AGW_PUBLIC_KEY);
|
||||
m_publicKeyMissing = publicKey.isEmpty();
|
||||
|
||||
QStringList primaryEndpoints;
|
||||
QStringList fallbackEndpoints;
|
||||
if (isDevEnvironment) {
|
||||
primaryEndpoints = splitEndpoints(DEV_S3_ENDPOINT);
|
||||
} else {
|
||||
primaryEndpoints = splitEndpoints(PROD_S3_ENDPOINT);
|
||||
fallbackEndpoints = splitEndpoints(FALLBACK_S3_ENDPOINT);
|
||||
}
|
||||
|
||||
QJsonObject config;
|
||||
config[agwConfigKey::gatewayEndpoint] = gatewayEndpoint;
|
||||
config[agwConfigKey::publicKeyPem] = QString::fromUtf8(publicKey);
|
||||
config[agwConfigKey::s3PrimaryEndpoints] = QJsonArray::fromStringList(primaryEndpoints);
|
||||
config[agwConfigKey::s3FallbackEndpoints] = QJsonArray::fromStringList(fallbackEndpoints);
|
||||
config[agwConfigKey::isDevEnvironment] = isDevEnvironment;
|
||||
config[agwConfigKey::requestTimeoutMsecs] = requestTimeoutMsecs;
|
||||
config[agwConfigKey::proxyRequestTimeoutMsecs] = apiDefs::proxyRequestTimeoutMsecs;
|
||||
config[agwConfigKey::proxyStorageTimeoutMsecs] = apiDefs::proxyStorageTimeoutMsecs;
|
||||
config[agwConfigKey::proxyHealthTimeoutMsecs] = apiDefs::proxyHealthTimeoutMsecs;
|
||||
|
||||
m_client = agw_client_create(QJsonDocument(config).toJson(QJsonDocument::Compact).constData(), nullptr);
|
||||
if (m_client == 0) {
|
||||
qCritical() << "GatewayController: failed to create gateway client (missing key or endpoint?)";
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_appSettingsRepository != nullptr) {
|
||||
const QByteArray state = m_appSettingsRepository->readGatewayProxyUrls(agwStateCacheKey);
|
||||
if (!state.isEmpty() && agw_import_state(m_client, state.constData()) == AGW_OK) {
|
||||
m_lastPersistedState = state;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GatewayController::EncryptedRequestData GatewayController::prepareRequest(const QString &endpoint, const QJsonObject &apiPayload)
|
||||
GatewayController::~GatewayController()
|
||||
{
|
||||
EncryptedRequestData encRequestData;
|
||||
encRequestData.errorCode = ErrorCode::NoError;
|
||||
agw_client_destroy(m_client);
|
||||
}
|
||||
|
||||
amnezia::ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody)
|
||||
{
|
||||
QFuture<QPair<amnezia::ErrorCode, QByteArray>> future = postAsync(endpoint, apiPayload);
|
||||
|
||||
QFutureWatcher<QPair<amnezia::ErrorCode, QByteArray>> watcher;
|
||||
QEventLoop wait;
|
||||
connect(&watcher, &QFutureWatcherBase::finished, &wait, &QEventLoop::quit);
|
||||
watcher.setFuture(future);
|
||||
if (!future.isFinished()) {
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
}
|
||||
|
||||
const QPair<amnezia::ErrorCode, QByteArray> result = future.result();
|
||||
responseBody = result.second;
|
||||
return result.first;
|
||||
}
|
||||
|
||||
QFuture<QPair<amnezia::ErrorCode, QByteArray>> GatewayController::postAsync(const QString &endpoint, const QJsonObject apiPayload)
|
||||
{
|
||||
prepareRequest();
|
||||
return QtConcurrent::run(
|
||||
[this, endpoint, apiPayload]() -> QPair<amnezia::ErrorCode, QByteArray> { return executePost(endpoint, apiPayload); });
|
||||
}
|
||||
|
||||
QPair<amnezia::ErrorCode, QByteArray> GatewayController::executePost(const QString &endpoint, const QJsonObject &apiPayload)
|
||||
{
|
||||
if (m_client == 0) {
|
||||
return qMakePair(m_publicKeyMissing ? amnezia::ErrorCode::ApiMissingAgwPublicKey : amnezia::ErrorCode::ApiConfigDownloadError,
|
||||
QByteArray());
|
||||
}
|
||||
|
||||
QString path = endpoint;
|
||||
path.remove(legacyEndpointPlaceholder);
|
||||
|
||||
QJsonObject options;
|
||||
options[agwOptionsKey::serviceType] = apiPayload.value(apiDefs::key::serviceType).toString();
|
||||
options[agwOptionsKey::userCountryCode] = apiPayload.value(apiDefs::key::userCountryCode).toString();
|
||||
|
||||
const QByteArray payload = QJsonDocument(apiPayload).toJson(QJsonDocument::Compact);
|
||||
const QByteArray optionsJson = QJsonDocument(options).toJson(QJsonDocument::Compact);
|
||||
|
||||
agw_result result = agw_post(m_client, path.toUtf8().constData(), payload.constData(), optionsJson.constData(), 0);
|
||||
|
||||
QByteArray responseBody;
|
||||
if (result.body != nullptr) {
|
||||
responseBody = QByteArray(result.body, static_cast<qsizetype>(result.body_len));
|
||||
}
|
||||
QString trace;
|
||||
if (result.trace != nullptr) {
|
||||
trace = QString::fromUtf8(result.trace, static_cast<qsizetype>(result.trace_len));
|
||||
}
|
||||
const int code = result.code;
|
||||
agw_result_free(&result);
|
||||
|
||||
logAgwTrace(path, code, trace);
|
||||
persistState();
|
||||
|
||||
return qMakePair(mapResultCode(code, responseBody), responseBody);
|
||||
}
|
||||
|
||||
amnezia::ErrorCode GatewayController::mapResultCode(const int code, const QByteArray &responseBody)
|
||||
{
|
||||
switch (code) {
|
||||
case AGW_OK: return apiUtils::checkApiResponseErrors(responseBody);
|
||||
case AGW_CANCELLED:
|
||||
case AGW_ERR_TIMEOUT: return amnezia::ErrorCode::ApiConfigTimeoutError;
|
||||
case AGW_ERR_SSL: return amnezia::ErrorCode::ApiConfigSslError;
|
||||
case AGW_ERR_CONFIG: return amnezia::ErrorCode::ApiMissingAgwPublicKey;
|
||||
case AGW_ERR_DECRYPT: return amnezia::ErrorCode::ApiConfigDecryptionError;
|
||||
default: return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||
}
|
||||
}
|
||||
|
||||
void GatewayController::prepareRequest()
|
||||
{
|
||||
#ifdef Q_OS_IOS
|
||||
IosController::Instance()->requestInetAccess();
|
||||
QThread::msleep(10);
|
||||
#endif
|
||||
|
||||
encRequestData.request.setTransferTimeout(m_requestTimeoutMsecs);
|
||||
encRequestData.request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
encRequestData.request.setRawHeader(QString("X-Client-Request-ID").toUtf8(), QUuid::createUuid().toString(QUuid::WithoutBraces).toUtf8());
|
||||
encRequestData.request.setUrl(endpoint.arg(m_proxyUrl.isEmpty() ? m_gatewayEndpoint : m_proxyUrl));
|
||||
|
||||
// bypass killSwitch exceptions for API-gateway
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
if (m_isStrictKillSwitchEnabled) {
|
||||
QString host = QUrl(encRequestData.request.url()).host();
|
||||
QString ip = NetworkUtilities::getIPAddress(host);
|
||||
if (!m_isStrictKillSwitchEnabled) {
|
||||
return;
|
||||
}
|
||||
QStringList ips;
|
||||
for (const QString &host : requestHosts()) {
|
||||
const QString ip = NetworkUtilities::getIPAddress(host);
|
||||
if (!ip.isEmpty()) {
|
||||
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
QRemoteObjectPendingReply<bool> reply = iface->addKillSwitchAllowedRange(QStringList { ip });
|
||||
if (!reply.waitForFinished(1000) || !reply.returnValue())
|
||||
qWarning() << "GatewayController::prepareRequest(): Failed to execute remote addKillSwitchAllowedRange call";
|
||||
});
|
||||
ips.append(ip);
|
||||
}
|
||||
}
|
||||
if (ips.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
QRemoteObjectPendingReply<bool> reply = iface->addKillSwitchAllowedRange(ips);
|
||||
if (!reply.waitForFinished(1000) || !reply.returnValue()) {
|
||||
qWarning() << "GatewayController::prepareRequest(): Failed to execute remote addKillSwitchAllowedRange call";
|
||||
}
|
||||
});
|
||||
#endif
|
||||
|
||||
encRequestData.key = CryptoUtils::generateRandomBytes(32);
|
||||
encRequestData.iv = CryptoUtils::generateRandomBytes(32);
|
||||
encRequestData.salt = CryptoUtils::generateRandomBytes(8);
|
||||
|
||||
QJsonObject keyPayload;
|
||||
keyPayload[apiDefs::key::aesKey] = QString(encRequestData.key.toBase64());
|
||||
keyPayload[apiDefs::key::aesIv] = QString(encRequestData.iv.toBase64());
|
||||
keyPayload[apiDefs::key::aesSalt] = QString(encRequestData.salt.toBase64());
|
||||
|
||||
QByteArray rsaKey = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||
EVP_PKEY *publicKey = CryptoUtils::loadPublicKeyFromPem(rsaKey);
|
||||
if (publicKey == nullptr) {
|
||||
qCritical() << "error loading public key from environment variables";
|
||||
encRequestData.errorCode = ErrorCode::ApiMissingAgwPublicKey;
|
||||
return encRequestData;
|
||||
}
|
||||
|
||||
QByteArray encryptedKeyPayload = CryptoUtils::rsaEncrypt(QJsonDocument(keyPayload).toJson(), publicKey, RSA_PKCS1_PADDING);
|
||||
EVP_PKEY_free(publicKey);
|
||||
|
||||
QByteArray encryptedApiPayload = CryptoUtils::encryptAes256Cbc(QJsonDocument(apiPayload).toJson(), encRequestData.key, encRequestData.iv);
|
||||
|
||||
if (encryptedKeyPayload.isEmpty() || encryptedApiPayload.isEmpty()) {
|
||||
qCritical() << "error when encrypting the request body";
|
||||
encRequestData.errorCode = ErrorCode::ApiConfigDecryptionError;
|
||||
return encRequestData;
|
||||
}
|
||||
|
||||
QJsonObject requestBody;
|
||||
requestBody[apiDefs::key::keyPayload] = QString(encryptedKeyPayload.toBase64());
|
||||
requestBody[apiDefs::key::apiPayload] = QString(encryptedApiPayload.toBase64());
|
||||
|
||||
encRequestData.requestBody = QJsonDocument(requestBody).toJson();
|
||||
return encRequestData;
|
||||
}
|
||||
|
||||
GatewayController::DecryptionResult GatewayController::tryDecryptResponseBody(const QByteArray &encryptedResponseBody,
|
||||
QNetworkReply::NetworkError replyError, const QByteArray &key,
|
||||
const QByteArray &iv, const QByteArray &salt)
|
||||
QStringList GatewayController::requestHosts() const
|
||||
{
|
||||
DecryptionResult result;
|
||||
result.decryptedBody = encryptedResponseBody;
|
||||
result.isDecryptionSuccessful = false;
|
||||
|
||||
QByteArray decrypted = CryptoUtils::decryptAes256Cbc(encryptedResponseBody, key, iv);
|
||||
if (!decrypted.isEmpty()) {
|
||||
result.decryptedBody = decrypted;
|
||||
result.isDecryptionSuccessful = true;
|
||||
} else {
|
||||
result.decryptedBody = encryptedResponseBody;
|
||||
result.isDecryptionSuccessful = false;
|
||||
QStringList hosts;
|
||||
if (m_client != 0) {
|
||||
char *state = agw_export_state(m_client);
|
||||
if (state != nullptr) {
|
||||
const QString workingProxy = QJsonDocument::fromJson(QByteArray(state)).object().value(agwStateKey::workingProxy).toString();
|
||||
agw_string_free(state);
|
||||
if (!workingProxy.isEmpty()) {
|
||||
hosts.append(QUrl(workingProxy).host());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
hosts.append(QUrl(m_gatewayEndpoint).host());
|
||||
hosts.removeAll(QString());
|
||||
hosts.removeDuplicates();
|
||||
return hosts;
|
||||
}
|
||||
|
||||
ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody)
|
||||
void GatewayController::persistState()
|
||||
{
|
||||
EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload);
|
||||
if (encRequestData.errorCode != ErrorCode::NoError) {
|
||||
return encRequestData.errorCode;
|
||||
}
|
||||
|
||||
QNetworkReply *reply = amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody);
|
||||
|
||||
QEventLoop wait;
|
||||
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
|
||||
|
||||
QList<QSslError> sslErrors;
|
||||
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
|
||||
QByteArray encryptedResponseBody = reply->readAll();
|
||||
QString replyErrorString = reply->errorString();
|
||||
auto replyError = reply->error();
|
||||
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
auto decryptionResult =
|
||||
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
|
||||
|
||||
if (sslErrors.isEmpty() && shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
|
||||
auto requestFunction = [&encRequestData, &encryptedResponseBody](const QString &url) {
|
||||
encRequestData.request.setUrl(url);
|
||||
return amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody);
|
||||
};
|
||||
|
||||
auto replyProcessingFunction = [&encryptedResponseBody, &replyErrorString, &replyError, &httpStatusCode, &sslErrors, &encRequestData,
|
||||
&decryptionResult, this](QNetworkReply *reply, const QList<QSslError> &nestedSslErrors) {
|
||||
encryptedResponseBody = reply->readAll();
|
||||
replyErrorString = reply->errorString();
|
||||
replyError = reply->error();
|
||||
httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
|
||||
decryptionResult =
|
||||
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
|
||||
|
||||
if (!sslErrors.isEmpty()
|
||||
|| shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
|
||||
sslErrors = nestedSslErrors;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString("");
|
||||
auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString("");
|
||||
bypassProxy(endpoint, serviceType, userCountryCode, requestFunction, replyProcessingFunction);
|
||||
}
|
||||
|
||||
responseBody = decryptionResult.decryptedBody;
|
||||
const auto errorCode =
|
||||
apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, responseBody);
|
||||
if (errorCode) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
if (!decryptionResult.isDecryptionSuccessful) {
|
||||
qCritical() << "error when decrypting the request body";
|
||||
return ErrorCode::ApiConfigDecryptionError;
|
||||
}
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString &endpoint, const QJsonObject apiPayload)
|
||||
{
|
||||
auto promise = QSharedPointer<QPromise<QPair<ErrorCode, QByteArray>>>::create();
|
||||
promise->start();
|
||||
|
||||
EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload);
|
||||
if (encRequestData.errorCode != ErrorCode::NoError) {
|
||||
promise->addResult(qMakePair(encRequestData.errorCode, QByteArray()));
|
||||
promise->finish();
|
||||
return promise->future();
|
||||
}
|
||||
|
||||
QNetworkReply *reply = amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody);
|
||||
|
||||
auto sslErrors = QSharedPointer<QList<QSslError>>::create();
|
||||
|
||||
connect(reply, &QNetworkReply::sslErrors, [sslErrors](const QList<QSslError> &errors) { *sslErrors = errors; });
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [promise, sslErrors, encRequestData, endpoint, apiPayload, reply, this]() mutable {
|
||||
QByteArray encryptedResponseBody = reply->readAll();
|
||||
QString replyErrorString = reply->errorString();
|
||||
auto replyError = reply->error();
|
||||
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
auto decryptionResult =
|
||||
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
|
||||
|
||||
auto processResponse = [promise, encRequestData](const GatewayController::DecryptionResult &decryptionResult,
|
||||
const QList<QSslError> &sslErrors, QNetworkReply::NetworkError replyError,
|
||||
const QString &replyErrorString, int httpStatusCode) {
|
||||
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode,
|
||||
decryptionResult.decryptedBody);
|
||||
if (errorCode) {
|
||||
promise->addResult(qMakePair(errorCode, decryptionResult.decryptedBody));
|
||||
promise->finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!decryptionResult.isDecryptionSuccessful) {
|
||||
qCritical() << "error when decrypting the request body";
|
||||
promise->addResult(qMakePair(ErrorCode::ApiConfigDecryptionError, QByteArray()));
|
||||
promise->finish();
|
||||
return;
|
||||
}
|
||||
|
||||
promise->addResult(qMakePair(ErrorCode::NoError, decryptionResult.decryptedBody));
|
||||
promise->finish();
|
||||
};
|
||||
|
||||
if (sslErrors->isEmpty() && shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
|
||||
auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString("");
|
||||
auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString("");
|
||||
|
||||
QStringList primaryBaseUrls;
|
||||
QStringList fallbackBaseUrls;
|
||||
if (m_isDevEnvironment) {
|
||||
primaryBaseUrls = QString(DEV_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
} else {
|
||||
primaryBaseUrls = QString(PROD_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
fallbackBaseUrls = QString(FALLBACK_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
}
|
||||
std::random_device randomDevice;
|
||||
std::mt19937 generator(randomDevice());
|
||||
std::shuffle(primaryBaseUrls.begin(), primaryBaseUrls.end(), generator);
|
||||
std::shuffle(fallbackBaseUrls.begin(), fallbackBaseUrls.end(), generator);
|
||||
|
||||
auto appendStorageUrls = [&serviceType, &userCountryCode](const QStringList &baseUrls, QStringList &target) {
|
||||
if (!serviceType.isEmpty()) {
|
||||
for (const auto &baseUrl : baseUrls) {
|
||||
QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8();
|
||||
target.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) + ".json");
|
||||
}
|
||||
}
|
||||
for (const auto &baseUrl : baseUrls) {
|
||||
target.push_back(baseUrl + "endpoints.json");
|
||||
}
|
||||
};
|
||||
|
||||
QStringList proxyStorageUrls;
|
||||
appendStorageUrls(primaryBaseUrls, proxyStorageUrls);
|
||||
appendStorageUrls(fallbackBaseUrls, proxyStorageUrls);
|
||||
const QString proxyUrlsCacheKey = getProxyUrlsCacheKey(serviceType, userCountryCode);
|
||||
|
||||
getProxyUrlsAsync(proxyStorageUrls, 0, proxyUrlsCacheKey, [this, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) {
|
||||
getProxyUrlAsync(proxyUrls, 0, [this, encRequestData, endpoint, processResponse](const QString &proxyUrl) {
|
||||
bypassProxyAsync(endpoint, proxyUrl, encRequestData,
|
||||
[processResponse, this](const QByteArray &decryptedBody, bool isDecryptionSuccessful,
|
||||
const QList<QSslError> &sslErrors, QNetworkReply::NetworkError replyError,
|
||||
const QString &replyErrorString, int httpStatusCode) {
|
||||
GatewayController::DecryptionResult result;
|
||||
result.decryptedBody = decryptedBody;
|
||||
result.isDecryptionSuccessful = isDecryptionSuccessful;
|
||||
processResponse(result, sslErrors, replyError, replyErrorString, httpStatusCode);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
} else {
|
||||
processResponse(decryptionResult, *sslErrors, replyError, replyErrorString, httpStatusCode);
|
||||
}
|
||||
});
|
||||
|
||||
return promise->future();
|
||||
}
|
||||
|
||||
QStringList GatewayController::getProxyUrls(const QString &serviceType, const QString &userCountryCode)
|
||||
{
|
||||
QNetworkRequest request;
|
||||
request.setTransferTimeout(proxyStorageRequestTimeoutMsecs);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
|
||||
QEventLoop wait;
|
||||
QList<QSslError> sslErrors;
|
||||
QNetworkReply *reply;
|
||||
|
||||
QStringList primaryBaseUrls;
|
||||
QStringList fallbackBaseUrls;
|
||||
if (m_isDevEnvironment) {
|
||||
primaryBaseUrls = QString(DEV_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
} else {
|
||||
primaryBaseUrls = QString(PROD_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
fallbackBaseUrls = QString(FALLBACK_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
}
|
||||
|
||||
std::random_device randomDevice;
|
||||
std::mt19937 generator(randomDevice());
|
||||
std::shuffle(primaryBaseUrls.begin(), primaryBaseUrls.end(), generator);
|
||||
std::shuffle(fallbackBaseUrls.begin(), fallbackBaseUrls.end(), generator);
|
||||
|
||||
auto appendStorageUrls = [&serviceType, &userCountryCode](const QStringList &baseUrls, QStringList &target) {
|
||||
if (!serviceType.isEmpty()) {
|
||||
for (const auto &baseUrl : baseUrls) {
|
||||
QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8();
|
||||
target.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) + ".json");
|
||||
}
|
||||
}
|
||||
for (const auto &baseUrl : baseUrls) {
|
||||
target.push_back(baseUrl + "endpoints.json");
|
||||
}
|
||||
};
|
||||
|
||||
QStringList proxyStorageUrls;
|
||||
appendStorageUrls(primaryBaseUrls, proxyStorageUrls);
|
||||
appendStorageUrls(fallbackBaseUrls, proxyStorageUrls);
|
||||
const QString proxyUrlsCacheKey = getProxyUrlsCacheKey(serviceType, userCountryCode);
|
||||
const QByteArray cachedProxyUrlsEncrypted = m_appSettingsRepository->readGatewayProxyUrls(proxyUrlsCacheKey);
|
||||
|
||||
if (proxyStorageUrls.empty()) {
|
||||
qDebug() << "empty storage endpoint list";
|
||||
return readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment);
|
||||
}
|
||||
|
||||
for (const auto &proxyStorageUrl : proxyStorageUrls) {
|
||||
request.setUrl(proxyStorageUrl);
|
||||
reply = amnApp->networkManager()->get(request);
|
||||
|
||||
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
|
||||
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
|
||||
if (reply->error() == QNetworkReply::NetworkError::NoError) {
|
||||
auto encryptedResponseBody = reply->readAll();
|
||||
reply->deleteLater();
|
||||
|
||||
QByteArray responseBody;
|
||||
if (!decryptProxyUrlsPayload(encryptedResponseBody, m_isDevEnvironment, responseBody)) {
|
||||
qCritical() << "error loading private key from environment variables or decrypting payload" << encryptedResponseBody;
|
||||
continue;
|
||||
}
|
||||
|
||||
auto endpointsArray = QJsonDocument::fromJson(responseBody).array();
|
||||
|
||||
QStringList endpoints;
|
||||
for (const auto &endpoint : endpointsArray) {
|
||||
endpoints.push_back(endpoint.toString());
|
||||
}
|
||||
m_appSettingsRepository->writeGatewayProxyUrls(proxyUrlsCacheKey, encryptedResponseBody);
|
||||
|
||||
return endpoints;
|
||||
} else {
|
||||
auto replyError = reply->error();
|
||||
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
qDebug() << replyError;
|
||||
qDebug() << httpStatusCode;
|
||||
qDebug() << "go to the next storage endpoint";
|
||||
|
||||
reply->deleteLater();
|
||||
}
|
||||
}
|
||||
return readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment);
|
||||
}
|
||||
|
||||
bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody,
|
||||
bool isDecryptionSuccessful)
|
||||
{
|
||||
const QByteArray &responseBody = decryptedResponseBody;
|
||||
|
||||
int apiHttpStatus = -1;
|
||||
QString apiErrorMessage;
|
||||
if (isDecryptionSuccessful) {
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
|
||||
if (jsonDoc.isObject()) {
|
||||
QJsonObject jsonObj = jsonDoc.object();
|
||||
apiHttpStatus = jsonObj.value("http_status").toInt(-1);
|
||||
apiErrorMessage = jsonObj.value(QStringLiteral("message")).toString().trimmed();
|
||||
}
|
||||
} else {
|
||||
qDebug() << "failed to decrypt the data";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (replyError == QNetworkReply::NetworkError::OperationCanceledError || replyError == QNetworkReply::NetworkError::TimeoutError) {
|
||||
qDebug() << "timeout occurred";
|
||||
qDebug() << replyError;
|
||||
return true;
|
||||
}
|
||||
if (responseBody.contains("html")) {
|
||||
qDebug() << "the response contains an html tag";
|
||||
return true;
|
||||
}
|
||||
if (apiHttpStatus == httpStatusCodeRequestTimeout) {
|
||||
return false;
|
||||
}
|
||||
if (apiHttpStatus == httpStatusCodeNotFound) {
|
||||
if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2)
|
||||
|| responseBody.contains(errorResponsePattern3) || responseBody.contains(errorResponsePatternQrSessionNotFound)
|
||||
|| responseBody.contains(errorResponsePatternSessionNotFound)) {
|
||||
return false;
|
||||
} else {
|
||||
qDebug() << replyError;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (apiHttpStatus == httpStatusCodeNotImplemented) {
|
||||
if (responseBody.contains(updateRequestResponsePattern)) {
|
||||
return false;
|
||||
} else {
|
||||
qDebug() << replyError;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (apiHttpStatus == httpStatusCodeConflict) {
|
||||
return false;
|
||||
}
|
||||
if (apiHttpStatus == httpStatusCodePaymentRequired) {
|
||||
return false;
|
||||
}
|
||||
if (apiHttpStatus == httpStatusCodeUnprocessableEntity) {
|
||||
return apiErrorMessage != unprocessableSubscriptionMessage;
|
||||
}
|
||||
if (replyError != QNetworkReply::NetworkError::NoError) {
|
||||
qDebug() << replyError;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void GatewayController::bypassProxy(const QString &endpoint, const QString &serviceType, const QString &userCountryCode,
|
||||
std::function<QNetworkReply *(const QString &url)> requestFunction,
|
||||
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction)
|
||||
{
|
||||
QStringList proxyUrls = getProxyUrls(serviceType, userCountryCode);
|
||||
std::random_device randomDevice;
|
||||
std::mt19937 generator(randomDevice());
|
||||
std::shuffle(proxyUrls.begin(), proxyUrls.end(), generator);
|
||||
|
||||
QByteArray responseBody;
|
||||
|
||||
auto bypassFunction = [this](const QString &endpoint, const QString &proxyUrl,
|
||||
std::function<QNetworkReply *(const QString &url)> requestFunction,
|
||||
std::function<bool(QNetworkReply * reply, const QList<QSslError> &sslErrors)> replyProcessingFunction) {
|
||||
QEventLoop wait;
|
||||
QList<QSslError> sslErrors;
|
||||
|
||||
qDebug() << "go to the next proxy endpoint";
|
||||
QNetworkReply *reply = requestFunction(endpoint.arg(proxyUrl));
|
||||
|
||||
QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
|
||||
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
|
||||
auto result = replyProcessingFunction(reply, sslErrors);
|
||||
reply->deleteLater();
|
||||
return result;
|
||||
};
|
||||
|
||||
if (m_proxyUrl.isEmpty()) {
|
||||
QNetworkRequest request;
|
||||
request.setTransferTimeout(1000);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
|
||||
QEventLoop wait;
|
||||
QList<QSslError> sslErrors;
|
||||
QNetworkReply *reply;
|
||||
|
||||
for (const QString &proxyUrl : proxyUrls) {
|
||||
request.setUrl(proxyUrl + "lmbd-health");
|
||||
reply = amnApp->networkManager()->get(request);
|
||||
|
||||
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
|
||||
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
|
||||
if (reply->error() == QNetworkReply::NetworkError::NoError) {
|
||||
reply->deleteLater();
|
||||
|
||||
m_proxyUrl = proxyUrl;
|
||||
if (!m_proxyUrl.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
reply->deleteLater();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_proxyUrl.isEmpty()) {
|
||||
if (bypassFunction(endpoint, m_proxyUrl, requestFunction, replyProcessingFunction)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (const QString &proxyUrl : proxyUrls) {
|
||||
if (bypassFunction(endpoint, proxyUrl, requestFunction, replyProcessingFunction)) {
|
||||
m_proxyUrl = proxyUrl;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
|
||||
const QString &proxyUrlsCacheKey, std::function<void(const QStringList &)> onComplete)
|
||||
{
|
||||
const QByteArray cachedProxyUrlsEncrypted = m_appSettingsRepository->readGatewayProxyUrls(proxyUrlsCacheKey);
|
||||
|
||||
if (currentProxyStorageIndex >= proxyStorageUrls.size()) {
|
||||
onComplete(shuffledProxyUrls(readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment)));
|
||||
if (m_client == 0 || m_appSettingsRepository == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
QNetworkRequest request;
|
||||
request.setTransferTimeout(proxyStorageRequestTimeoutMsecs);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
request.setUrl(proxyStorageUrls[currentProxyStorageIndex]);
|
||||
|
||||
QNetworkReply *reply = amnApp->networkManager()->get(request);
|
||||
|
||||
// connect(reply, &QNetworkReply::sslErrors, this, [state](const QList<QSslError> &e) { *(state->sslErrors) = e; });
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this,
|
||||
[this, proxyStorageUrls, currentProxyStorageIndex, proxyUrlsCacheKey, onComplete, reply]() {
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
QByteArray encrypted = reply->readAll();
|
||||
reply->deleteLater();
|
||||
|
||||
QByteArray responseBody;
|
||||
if (!decryptProxyUrlsPayload(encrypted, m_isDevEnvironment, responseBody)) {
|
||||
qCritical() << "error decrypting payload";
|
||||
QMetaObject::invokeMethod(
|
||||
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, proxyUrlsCacheKey, onComplete); }, Qt::QueuedConnection);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonArray endpointsArray = QJsonDocument::fromJson(responseBody).array();
|
||||
QStringList endpoints;
|
||||
for (const QJsonValue &endpoint : endpointsArray)
|
||||
endpoints.push_back(endpoint.toString());
|
||||
m_appSettingsRepository->writeGatewayProxyUrls(proxyUrlsCacheKey, encrypted);
|
||||
|
||||
onComplete(shuffledProxyUrls(endpoints));
|
||||
return;
|
||||
}
|
||||
|
||||
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
qDebug() << httpStatusCode;
|
||||
qDebug() << "go to the next storage endpoint";
|
||||
reply->deleteLater();
|
||||
QMetaObject::invokeMethod(
|
||||
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, proxyUrlsCacheKey, onComplete); }, Qt::QueuedConnection);
|
||||
});
|
||||
}
|
||||
|
||||
void GatewayController::getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex,
|
||||
std::function<void(const QString &)> onComplete)
|
||||
{
|
||||
if (currentProxyIndex >= proxyUrls.size()) {
|
||||
onComplete("");
|
||||
char *state = agw_export_state(m_client);
|
||||
if (state == nullptr) {
|
||||
return;
|
||||
}
|
||||
const QByteArray blob(state);
|
||||
agw_string_free(state);
|
||||
|
||||
QNetworkRequest request;
|
||||
request.setTransferTimeout(1000);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
request.setUrl(proxyUrls[currentProxyIndex] + "lmbd-health");
|
||||
|
||||
QNetworkReply *reply = amnApp->networkManager()->get(request);
|
||||
|
||||
// connect(reply, &QNetworkReply::sslErrors, this, [state](const QList<QSslError> &e) {
|
||||
// *(state->sslErrors) = e;
|
||||
// });
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [this, proxyUrls, currentProxyIndex, onComplete, reply]() {
|
||||
reply->deleteLater();
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
m_proxyUrl = proxyUrls[currentProxyIndex];
|
||||
onComplete(m_proxyUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "go to the next proxy endpoint";
|
||||
QMetaObject::invokeMethod(this, [=]() { getProxyUrlAsync(proxyUrls, currentProxyIndex + 1, onComplete); }, Qt::QueuedConnection);
|
||||
});
|
||||
}
|
||||
|
||||
void GatewayController::bypassProxyAsync(
|
||||
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
|
||||
std::function<void(const QByteArray &, bool, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> onComplete)
|
||||
{
|
||||
auto sslErrors = QSharedPointer<QList<QSslError>>::create();
|
||||
if (proxyUrl.isEmpty()) {
|
||||
onComplete(QByteArray(), false, *sslErrors, QNetworkReply::InternalServerError, "empty proxy url", 0);
|
||||
return;
|
||||
if (blob != m_lastPersistedState) {
|
||||
m_appSettingsRepository->writeGatewayProxyUrls(agwStateCacheKey, blob);
|
||||
m_lastPersistedState = blob;
|
||||
}
|
||||
|
||||
QNetworkRequest request = encRequestData.request;
|
||||
request.setUrl(endpoint.arg(proxyUrl));
|
||||
|
||||
QNetworkReply *reply = amnApp->networkManager()->post(request, encRequestData.requestBody);
|
||||
|
||||
connect(reply, &QNetworkReply::sslErrors, this, [sslErrors](const QList<QSslError> &errors) { *sslErrors = errors; });
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [sslErrors, onComplete, encRequestData, reply, this]() {
|
||||
QByteArray encryptedResponseBody = reply->readAll();
|
||||
QString replyErrorString = reply->errorString();
|
||||
auto replyError = reply->error();
|
||||
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
auto decryptionResult =
|
||||
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
|
||||
|
||||
onComplete(decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful, *sslErrors, replyError, replyErrorString,
|
||||
httpStatusCode);
|
||||
});
|
||||
}
|
||||
@@ -1,20 +1,17 @@
|
||||
#ifndef GATEWAYCONTROLLER_H
|
||||
#define GATEWAYCONTROLLER_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QFuture>
|
||||
#include <QNetworkReply>
|
||||
#include <QJsonObject>
|
||||
#include <QObject>
|
||||
#include <QPair>
|
||||
#include <QPromise>
|
||||
#include <QSharedPointer>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
#include "core/utils/errorCodes.h"
|
||||
#include "core/utils/routeModes.h"
|
||||
#include "core/utils/commonStructs.h"
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#include "platforms/ios/ios_controller.h"
|
||||
#endif
|
||||
#include "agw.h"
|
||||
|
||||
class SecureAppSettingsRepository;
|
||||
|
||||
@@ -26,51 +23,26 @@ public:
|
||||
explicit GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
|
||||
const bool isStrictKillSwitchEnabled, SecureAppSettingsRepository *appSettingsRepository,
|
||||
QObject *parent = nullptr);
|
||||
~GatewayController() override;
|
||||
|
||||
amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody);
|
||||
QFuture<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject apiPayload);
|
||||
|
||||
private:
|
||||
struct EncryptedRequestData
|
||||
{
|
||||
QNetworkRequest request;
|
||||
QByteArray requestBody;
|
||||
QByteArray key;
|
||||
QByteArray iv;
|
||||
QByteArray salt;
|
||||
amnezia::ErrorCode errorCode;
|
||||
};
|
||||
void prepareRequest();
|
||||
QStringList requestHosts() const;
|
||||
|
||||
struct DecryptionResult
|
||||
{
|
||||
QByteArray decryptedBody;
|
||||
bool isDecryptionSuccessful;
|
||||
};
|
||||
QPair<amnezia::ErrorCode, QByteArray> executePost(const QString &endpoint, const QJsonObject &apiPayload);
|
||||
static amnezia::ErrorCode mapResultCode(const int code, const QByteArray &responseBody);
|
||||
void persistState();
|
||||
|
||||
EncryptedRequestData prepareRequest(const QString &endpoint, const QJsonObject &apiPayload);
|
||||
DecryptionResult tryDecryptResponseBody(const QByteArray &encryptedResponseBody, QNetworkReply::NetworkError replyError,
|
||||
const QByteArray &key, const QByteArray &iv, const QByteArray &salt);
|
||||
|
||||
QStringList getProxyUrls(const QString &serviceType, const QString &userCountryCode);
|
||||
bool shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody, bool isDecryptionSuccessful);
|
||||
void bypassProxy(const QString &endpoint, const QString &serviceType, const QString &userCountryCode,
|
||||
std::function<QNetworkReply *(const QString &url)> requestFunction,
|
||||
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction);
|
||||
|
||||
void getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
|
||||
const QString &proxyUrlsCacheKey, std::function<void(const QStringList &)> onComplete);
|
||||
void getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex, std::function<void(const QString &)> onComplete);
|
||||
void bypassProxyAsync(
|
||||
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
|
||||
std::function<void(const QByteArray &, bool, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> onComplete);
|
||||
|
||||
int m_requestTimeoutMsecs;
|
||||
QString m_gatewayEndpoint;
|
||||
bool m_isDevEnvironment = false;
|
||||
bool m_isStrictKillSwitchEnabled = false;
|
||||
SecureAppSettingsRepository *m_appSettingsRepository = nullptr;
|
||||
|
||||
inline static QString m_proxyUrl;
|
||||
agw_client_handle m_client = 0;
|
||||
bool m_publicKeyMissing = false;
|
||||
QByteArray m_lastPersistedState;
|
||||
};
|
||||
|
||||
#endif // GATEWAYCONTROLLER_H
|
||||
@@ -0,0 +1,545 @@
|
||||
#include "localProxyController.h"
|
||||
|
||||
#include "core/controllers/api/subscriptionController.h"
|
||||
#include "core/repositories/secureAppSettingsRepository.h"
|
||||
#include "core/repositories/secureServersRepository.h"
|
||||
#include "core/utils/api/apiUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/localProxy/localProxyConstants.h"
|
||||
#include "core/utils/localProxy/portUtils.h"
|
||||
#include "core/utils/localProxy/xrayConfigUtils.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonParseError>
|
||||
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
#include "core/utils/ipcClient.h"
|
||||
|
||||
#include <QHostAddress>
|
||||
#include <QHttpServer>
|
||||
#include <QHttpServerResponse>
|
||||
#include <QTcpServer>
|
||||
#endif
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
LocalProxyController::LocalProxyController(SecureServersRepository *serversRepository,
|
||||
SecureAppSettingsRepository *appSettingsRepository,
|
||||
SubscriptionController *subscriptionController, QObject *parent)
|
||||
: QObject(parent),
|
||||
m_serversRepository(serversRepository),
|
||||
m_appSettingsRepository(appSettingsRepository),
|
||||
m_subscriptionController(subscriptionController)
|
||||
{
|
||||
connect(m_serversRepository, &SecureServersRepository::serverEdited, this, &LocalProxyController::onServerEdited);
|
||||
connect(m_serversRepository, &SecureServersRepository::serverRemoved, this,
|
||||
[this](const QString &serverId, int) { onServerRemoved(serverId); });
|
||||
}
|
||||
|
||||
LocalProxyController::~LocalProxyController()
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
bool LocalProxyController::isEnabled() const
|
||||
{
|
||||
return m_isEnabled;
|
||||
}
|
||||
|
||||
int LocalProxyController::port() const
|
||||
{
|
||||
return m_appSettingsRepository->localProxyPort();
|
||||
}
|
||||
|
||||
QString LocalProxyController::ownerId() const
|
||||
{
|
||||
return m_appSettingsRepository->localProxyOwnerId();
|
||||
}
|
||||
|
||||
bool LocalProxyController::isPortUserDefined() const
|
||||
{
|
||||
return m_appSettingsRepository->isLocalProxyPortUserDefined();
|
||||
}
|
||||
|
||||
int LocalProxyController::activePort() const
|
||||
{
|
||||
return m_activePort;
|
||||
}
|
||||
|
||||
bool LocalProxyController::setPort(int port)
|
||||
{
|
||||
if (port < localProxy::proxyPortMin || port > localProxy::proxyPortMax) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool isPortChanged = m_appSettingsRepository->localProxyPort() != static_cast<quint16>(port);
|
||||
if (isPortChanged) {
|
||||
m_appSettingsRepository->setLocalProxyPort(static_cast<quint16>(port));
|
||||
}
|
||||
m_appSettingsRepository->setLocalProxyPortUserDefined(true);
|
||||
emit stateChanged();
|
||||
|
||||
if (isPortChanged && m_isEnabled) {
|
||||
start();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LocalProxyController::isPortBusy(int port) const
|
||||
{
|
||||
return !isPortAvailable(port);
|
||||
}
|
||||
|
||||
bool LocalProxyController::enable(const QString &ownerId, int port)
|
||||
{
|
||||
if (!isSupported() || ownerId.isEmpty() || port < localProxy::proxyPortMin || port > localProxy::proxyPortMax) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_isEnabled && m_appSettingsRepository->localProxyOwnerId() != ownerId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isPortAvailable(port) && m_appSettingsRepository->isLocalProxyPortUserDefined()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_appSettingsRepository->localProxyPort() != static_cast<quint16>(port)) {
|
||||
m_appSettingsRepository->setLocalProxyPort(static_cast<quint16>(port));
|
||||
}
|
||||
|
||||
m_appSettingsRepository->setLocalProxyOwnerId(ownerId);
|
||||
m_isEnabled = true;
|
||||
emit stateChanged();
|
||||
|
||||
start();
|
||||
return true;
|
||||
}
|
||||
|
||||
void LocalProxyController::disable()
|
||||
{
|
||||
if (!m_isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "Local proxy is disabled";
|
||||
m_isEnabled = false;
|
||||
stop();
|
||||
emit stateChanged();
|
||||
}
|
||||
|
||||
void LocalProxyController::start()
|
||||
{
|
||||
if (!startApi(localProxy::apiPort)) {
|
||||
disableWithError(tr("Local proxy failed to start. Check if the port is available"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!syncXray()) {
|
||||
disableWithError(tr("Couldn’t start the proxy due to an internal error. Try restarting the app"));
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "Local proxy is running on 127.0.0.1:" << m_apiPort;
|
||||
}
|
||||
|
||||
void LocalProxyController::onServerEdited(const QString &serverId)
|
||||
{
|
||||
if (m_apiPort == 0 || m_appSettingsRepository->localProxyOwnerId() != serverId) {
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "Owner server edited, restarting Xray";
|
||||
if (restartXray()) {
|
||||
m_appliedProxyPort = m_appSettingsRepository->localProxyPort();
|
||||
} else {
|
||||
disableWithError(tr("Couldn’t start the proxy due to an internal error. Try restarting the app"));
|
||||
}
|
||||
}
|
||||
|
||||
void LocalProxyController::onServerRemoved(const QString &serverId)
|
||||
{
|
||||
if (m_appSettingsRepository->localProxyOwnerId() != serverId) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_appSettingsRepository->setLocalProxyOwnerId(QString());
|
||||
if (m_isEnabled) {
|
||||
m_isEnabled = false;
|
||||
stop();
|
||||
}
|
||||
emit stateChanged();
|
||||
}
|
||||
|
||||
void LocalProxyController::onVpnConnectionRequested()
|
||||
{
|
||||
if (!m_isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
disable();
|
||||
emit stoppedByVpn(tr("Local proxy stopped because VPN was turned on"));
|
||||
}
|
||||
|
||||
void LocalProxyController::stop()
|
||||
{
|
||||
stopXray();
|
||||
stopApi();
|
||||
m_appliedProxyPort = 0;
|
||||
}
|
||||
|
||||
bool LocalProxyController::syncXray()
|
||||
{
|
||||
const quint16 proxyPort = m_appSettingsRepository->localProxyPort();
|
||||
|
||||
bool synced;
|
||||
if (!isXrayEngineRunning()) {
|
||||
synced = startXray();
|
||||
} else if (m_appliedProxyPort != proxyPort) {
|
||||
qDebug() << "Proxy port changed from" << m_appliedProxyPort << "to" << proxyPort;
|
||||
synced = restartXray();
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (synced) {
|
||||
m_appliedProxyPort = proxyPort;
|
||||
}
|
||||
return synced;
|
||||
}
|
||||
|
||||
bool LocalProxyController::startXray()
|
||||
{
|
||||
if (isXrayEngineRunning()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto config = buildXrayConfig();
|
||||
if (!config) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!startXrayEngine(config->serializedConfig)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
qDebug() << "Xray started on port" << config->proxyPort;
|
||||
setActivePort(config->proxyPort);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LocalProxyController::stopXray()
|
||||
{
|
||||
if (!stopXrayEngine()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setActivePort(0);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LocalProxyController::restartXray()
|
||||
{
|
||||
qDebug() << "Restarting Xray with updated config";
|
||||
|
||||
if (!stopXray()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return startXray();
|
||||
}
|
||||
|
||||
void LocalProxyController::disableWithError(const QString &message)
|
||||
{
|
||||
qWarning() << message;
|
||||
|
||||
m_isEnabled = false;
|
||||
stop();
|
||||
emit stateChanged();
|
||||
emit startFailed(message);
|
||||
}
|
||||
|
||||
void LocalProxyController::setActivePort(int port)
|
||||
{
|
||||
if (m_activePort == port) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_activePort = port;
|
||||
emit stateChanged();
|
||||
}
|
||||
|
||||
int LocalProxyController::runningProxyPort() const
|
||||
{
|
||||
return isXrayEngineRunning() ? m_activePort : 0;
|
||||
}
|
||||
|
||||
std::optional<LocalProxyController::XrayConfig> LocalProxyController::buildXrayConfig() const
|
||||
{
|
||||
const QString ownerId = m_appSettingsRepository->localProxyOwnerId();
|
||||
if (ownerId.isEmpty()) {
|
||||
qWarning() << "Local proxy owner server id is not configured";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto ownerServer = m_serversRepository->serverJsonById(ownerId);
|
||||
if (!ownerServer) {
|
||||
qWarning() << "Owner server with id" << ownerId << "not found";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (!apiUtils::isPremiumServer(*ownerServer)) {
|
||||
qWarning() << "Server" << ownerServer->value(configKey::name).toString() << "is not premium, local proxy is unavailable";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto serializedConfig = xrayConfigUtils::extractLastConfig(*ownerServer);
|
||||
if (!serializedConfig) {
|
||||
serializedConfig = fetchXrayConfigFromGateway(ownerId);
|
||||
if (!serializedConfig) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(serializedConfig->toUtf8(), &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !doc.isObject()) {
|
||||
qWarning() << "Failed to parse Xray config JSON:" << parseError.errorString();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto proxyPort = selectProxyPort();
|
||||
if (!proxyPort) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
QJsonObject xrayConfig = doc.object();
|
||||
if (!xrayConfigUtils::setInboundPort(xrayConfig, *proxyPort)) {
|
||||
qWarning() << "Failed to override local proxy inbound port, using original config";
|
||||
return XrayConfig { *serializedConfig, 0 };
|
||||
}
|
||||
|
||||
return XrayConfig { QString::fromUtf8(QJsonDocument(xrayConfig).toJson(QJsonDocument::Compact)), *proxyPort };
|
||||
}
|
||||
|
||||
std::optional<int> LocalProxyController::selectProxyPort() const
|
||||
{
|
||||
int port = m_appSettingsRepository->localProxyPort();
|
||||
if (port < localProxy::proxyPortMin || port > localProxy::proxyPortMax) {
|
||||
port = localProxy::defaultProxyPort;
|
||||
}
|
||||
|
||||
const auto isAvailable = [this](int p) { return isPortAvailable(p); };
|
||||
if (portUtils::waitForPort(port, localProxy::portReleaseWaitMs, isAvailable)) {
|
||||
return port;
|
||||
}
|
||||
|
||||
if (m_appSettingsRepository->isLocalProxyPortUserDefined()) {
|
||||
qWarning() << "Local proxy port" << port << "is already in use";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<int> freePort;
|
||||
if (port != localProxy::defaultProxyPort && isPortAvailable(localProxy::defaultProxyPort)) {
|
||||
freePort = localProxy::defaultProxyPort;
|
||||
} else {
|
||||
freePort = portUtils::findFirstAvailablePort(localProxy::defaultProxyPort + 1, localProxy::proxyPortMax, isAvailable);
|
||||
}
|
||||
|
||||
if (!freePort) {
|
||||
qWarning() << "No available local proxy port in range" << localProxy::defaultProxyPort + 1 << "-"
|
||||
<< localProxy::proxyPortMax;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
qDebug() << "Local proxy port" << port << "is busy, using" << *freePort;
|
||||
return freePort;
|
||||
}
|
||||
|
||||
std::optional<QString> LocalProxyController::fetchXrayConfigFromGateway(const QString &serverId) const
|
||||
{
|
||||
QJsonObject serverConfig;
|
||||
const ErrorCode errorCode = m_subscriptionController->fetchServiceConfig(serverId, configKey::vless, serverConfig);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
qWarning() << "Gateway request failed with error code" << static_cast<int>(errorCode);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto serializedConfig = xrayConfigUtils::extractLastConfig(serverConfig);
|
||||
if (!serializedConfig) {
|
||||
qWarning() << "Gateway response lacks Xray last_config payload";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
qDebug() << "Fetched Xray config from gateway";
|
||||
return serializedConfig;
|
||||
}
|
||||
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
|
||||
bool LocalProxyController::isSupported() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LocalProxyController::startApi(quint16 port)
|
||||
{
|
||||
if (m_httpServer && m_apiPort == port) {
|
||||
return true;
|
||||
}
|
||||
|
||||
stopApi();
|
||||
|
||||
const auto proxyPortValue = [this]() {
|
||||
const int port = runningProxyPort();
|
||||
return port > 0 ? QJsonValue(port) : QJsonValue::Null;
|
||||
};
|
||||
|
||||
auto httpServer = new QHttpServer(this);
|
||||
httpServer->route("/api/v1/up", QHttpServerRequest::Method::Post, [this, proxyPortValue] {
|
||||
const bool started = startXray();
|
||||
return QHttpServerResponse(QJsonObject { { "status", started ? "ok" : "error" }, { "proxyPort", proxyPortValue() } });
|
||||
});
|
||||
httpServer->route("/api/v1/down", QHttpServerRequest::Method::Post, [this] {
|
||||
const bool stopped = stopXray();
|
||||
return QHttpServerResponse(QJsonObject { { "status", stopped ? "ok" : "error" } });
|
||||
});
|
||||
httpServer->route("/api/v1/ping", QHttpServerRequest::Method::Get, [proxyPortValue] {
|
||||
return QHttpServerResponse(QJsonObject { { "status", "ok" }, { "proxyPort", proxyPortValue() } });
|
||||
});
|
||||
|
||||
auto tcpServer = new QTcpServer(httpServer);
|
||||
if (!tcpServer->listen(QHostAddress::LocalHost, port) || !httpServer->bind(tcpServer)) {
|
||||
qWarning() << "Failed to start HTTP API server on port" << port;
|
||||
delete httpServer;
|
||||
return false;
|
||||
}
|
||||
|
||||
m_httpServer = httpServer;
|
||||
m_apiPort = port;
|
||||
qDebug() << "HTTP API server is running on localhost:" << port;
|
||||
return true;
|
||||
}
|
||||
|
||||
void LocalProxyController::stopApi()
|
||||
{
|
||||
delete m_httpServer;
|
||||
m_httpServer = nullptr;
|
||||
m_apiPort = 0;
|
||||
}
|
||||
|
||||
bool LocalProxyController::startXrayEngine(const QString &config)
|
||||
{
|
||||
if (config.trimmed().isEmpty()) {
|
||||
qWarning() << "Xray config content is empty";
|
||||
return false;
|
||||
}
|
||||
|
||||
const qint64 token = IpcClient::withInterface(
|
||||
[&](QSharedPointer<IpcInterfaceReplica> iface) -> qint64 {
|
||||
auto xrayStart = iface->xrayStartOwned(config);
|
||||
if (!xrayStart.waitForFinished()) {
|
||||
return 0;
|
||||
}
|
||||
return xrayStart.returnValue();
|
||||
},
|
||||
[]() -> qint64 { return 0; });
|
||||
|
||||
if (token == 0) {
|
||||
qWarning() << "Failed to start Xray via IPC";
|
||||
return false;
|
||||
}
|
||||
|
||||
m_xrayToken = token;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LocalProxyController::stopXrayEngine()
|
||||
{
|
||||
if (m_xrayToken == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const bool stopped = IpcClient::withInterface(
|
||||
[this](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
auto xrayStop = iface->xrayStopOwned(m_xrayToken);
|
||||
return xrayStop.waitForFinished() && xrayStop.returnValue();
|
||||
},
|
||||
[]() { return false; });
|
||||
|
||||
if (!stopped) {
|
||||
qWarning() << "Failed to stop Xray via IPC";
|
||||
return false;
|
||||
}
|
||||
|
||||
m_xrayToken = 0;
|
||||
qDebug() << "Xray stopped";
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LocalProxyController::isXrayEngineRunning() const
|
||||
{
|
||||
if (m_xrayToken == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return IpcClient::withInterface(
|
||||
[this](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
auto currentToken = iface->xrayCurrentToken();
|
||||
return currentToken.waitForFinished() && currentToken.returnValue() == m_xrayToken;
|
||||
},
|
||||
[]() { return false; });
|
||||
}
|
||||
|
||||
bool LocalProxyController::isPortAvailable(int port) const
|
||||
{
|
||||
if (port < localProxy::proxyPortMin || port > localProxy::proxyPortMax) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return IpcClient::withInterface(
|
||||
[port](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
auto reply = iface->isTcpPortAvailable(port);
|
||||
return reply.waitForFinished() && reply.returnValue();
|
||||
},
|
||||
[port]() { return portUtils::isPortAvailable(port); });
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
bool LocalProxyController::isSupported() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool LocalProxyController::startApi(quint16)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void LocalProxyController::stopApi()
|
||||
{
|
||||
}
|
||||
|
||||
bool LocalProxyController::startXrayEngine(const QString &)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool LocalProxyController::stopXrayEngine()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LocalProxyController::isXrayEngineRunning() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool LocalProxyController::isPortAvailable(int port) const
|
||||
{
|
||||
return portUtils::isPortAvailable(port);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,85 @@
|
||||
#ifndef LOCALPROXYCONTROLLER_H
|
||||
#define LOCALPROXYCONTROLLER_H
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
class QHttpServer;
|
||||
class SecureServersRepository;
|
||||
class SecureAppSettingsRepository;
|
||||
class SubscriptionController;
|
||||
|
||||
class LocalProxyController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
LocalProxyController(SecureServersRepository *serversRepository, SecureAppSettingsRepository *appSettingsRepository,
|
||||
SubscriptionController *subscriptionController, QObject *parent = nullptr);
|
||||
~LocalProxyController();
|
||||
|
||||
bool isSupported() const;
|
||||
bool isEnabled() const;
|
||||
int port() const;
|
||||
QString ownerId() const;
|
||||
bool isPortUserDefined() const;
|
||||
int activePort() const;
|
||||
|
||||
bool setPort(int port);
|
||||
bool isPortBusy(int port) const;
|
||||
bool enable(const QString &ownerId, int port);
|
||||
void disable();
|
||||
|
||||
public slots:
|
||||
void onServerEdited(const QString &serverId);
|
||||
void onServerRemoved(const QString &serverId);
|
||||
void onVpnConnectionRequested();
|
||||
|
||||
signals:
|
||||
void stateChanged();
|
||||
void startFailed(const QString &message);
|
||||
void stoppedByVpn(const QString &message);
|
||||
|
||||
private:
|
||||
struct XrayConfig
|
||||
{
|
||||
QString serializedConfig;
|
||||
int proxyPort = 0;
|
||||
};
|
||||
|
||||
void start();
|
||||
void stop();
|
||||
bool syncXray();
|
||||
bool startXray();
|
||||
bool stopXray();
|
||||
bool restartXray();
|
||||
void disableWithError(const QString &message);
|
||||
void setActivePort(int port);
|
||||
int runningProxyPort() const;
|
||||
|
||||
std::optional<XrayConfig> buildXrayConfig() const;
|
||||
std::optional<int> selectProxyPort() const;
|
||||
std::optional<QString> fetchXrayConfigFromGateway(const QString &serverId) const;
|
||||
|
||||
bool startApi(quint16 port);
|
||||
void stopApi();
|
||||
bool startXrayEngine(const QString &config);
|
||||
bool stopXrayEngine();
|
||||
bool isXrayEngineRunning() const;
|
||||
bool isPortAvailable(int port) const;
|
||||
|
||||
SecureServersRepository *m_serversRepository;
|
||||
SecureAppSettingsRepository *m_appSettingsRepository;
|
||||
SubscriptionController *m_subscriptionController;
|
||||
|
||||
bool m_isEnabled = false;
|
||||
QHttpServer *m_httpServer = nullptr;
|
||||
quint16 m_apiPort = 0;
|
||||
qint64 m_xrayToken = 0;
|
||||
quint16 m_appliedProxyPort = 0;
|
||||
int m_activePort = 0;
|
||||
};
|
||||
|
||||
#endif // LOCALPROXYCONTROLLER_H
|
||||
@@ -0,0 +1,270 @@
|
||||
#include "logsController.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
|
||||
#ifdef AMNEZIA_HAS_QZIPWRITER
|
||||
#include <QBuffer>
|
||||
#include <QtCore/private/qzipwriter_p.h>
|
||||
#else
|
||||
#include <QDateTime>
|
||||
|
||||
#include <array>
|
||||
#endif
|
||||
|
||||
#include <limits>
|
||||
|
||||
#include "logger.h"
|
||||
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
#include "core/utils/swiftBridge.h"
|
||||
#elif defined(Q_OS_ANDROID)
|
||||
#include "platforms/android/android_controller.h"
|
||||
#endif
|
||||
|
||||
#ifndef AMNEZIA_HAS_QZIPWRITER
|
||||
namespace
|
||||
{
|
||||
quint32 crc32(const QByteArray &data)
|
||||
{
|
||||
static const std::array<quint32, 256> table = [] {
|
||||
std::array<quint32, 256> result {};
|
||||
for (quint32 i = 0; i < 256; ++i) {
|
||||
quint32 value = i;
|
||||
for (int bit = 0; bit < 8; ++bit) {
|
||||
value = (value & 1) ? 0xEDB88320u ^ (value >> 1) : value >> 1;
|
||||
}
|
||||
result[i] = value;
|
||||
}
|
||||
return result;
|
||||
}();
|
||||
|
||||
quint32 crc = 0xFFFFFFFFu;
|
||||
for (const char byte : data) {
|
||||
crc = table[(crc ^ static_cast<uchar>(byte)) & 0xFF] ^ (crc >> 8);
|
||||
}
|
||||
return crc ^ 0xFFFFFFFFu;
|
||||
}
|
||||
|
||||
void put16(QByteArray &out, quint16 value)
|
||||
{
|
||||
out.append(static_cast<char>(value & 0xFF));
|
||||
out.append(static_cast<char>(value >> 8));
|
||||
}
|
||||
|
||||
void put32(QByteArray &out, quint32 value)
|
||||
{
|
||||
put16(out, static_cast<quint16>(value & 0xFFFF));
|
||||
put16(out, static_cast<quint16>(value >> 16));
|
||||
}
|
||||
|
||||
QByteArray storedZip(const QList<QPair<QString, QByteArray>> &entries)
|
||||
{
|
||||
const QDateTime now = QDateTime::currentDateTime();
|
||||
const quint16 dosTime = (now.time().hour() << 11) | (now.time().minute() << 5) | (now.time().second() / 2);
|
||||
const quint16 dosDate = ((now.date().year() - 1980) << 9) | (now.date().month() << 5) | now.date().day();
|
||||
|
||||
QByteArray archive;
|
||||
QByteArray centralDirectory;
|
||||
for (const auto &[name, data] : entries) {
|
||||
const QByteArray fileName = name.toUtf8();
|
||||
const quint32 offset = static_cast<quint32>(archive.size());
|
||||
const quint32 size = static_cast<quint32>(data.size());
|
||||
|
||||
QByteArray fields;
|
||||
put16(fields, 10);
|
||||
put16(fields, 0);
|
||||
put16(fields, 0);
|
||||
put16(fields, dosTime);
|
||||
put16(fields, dosDate);
|
||||
put32(fields, crc32(data));
|
||||
put32(fields, size);
|
||||
put32(fields, size);
|
||||
put16(fields, static_cast<quint16>(fileName.size()));
|
||||
put16(fields, 0);
|
||||
|
||||
put32(archive, 0x04034b50);
|
||||
archive.append(fields).append(fileName).append(data);
|
||||
|
||||
put32(centralDirectory, 0x02014b50);
|
||||
put16(centralDirectory, 20);
|
||||
centralDirectory.append(fields);
|
||||
put16(centralDirectory, 0);
|
||||
put16(centralDirectory, 0);
|
||||
put16(centralDirectory, 0);
|
||||
put32(centralDirectory, 0);
|
||||
put32(centralDirectory, offset);
|
||||
centralDirectory.append(fileName);
|
||||
}
|
||||
|
||||
const quint32 centralDirectoryOffset = static_cast<quint32>(archive.size());
|
||||
archive.append(centralDirectory);
|
||||
|
||||
put32(archive, 0x06054b50);
|
||||
put16(archive, 0);
|
||||
put16(archive, 0);
|
||||
put16(archive, static_cast<quint16>(entries.size()));
|
||||
put16(archive, static_cast<quint16>(entries.size()));
|
||||
put32(archive, static_cast<quint32>(centralDirectory.size()));
|
||||
put32(archive, centralDirectoryOffset);
|
||||
put16(archive, 0);
|
||||
return archive;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
QStringList LogsController::files(Stream stream)
|
||||
{
|
||||
#if defined(AMNEZIA_DESKTOP)
|
||||
if (stream == Stream::App) {
|
||||
return { Logger::userLogsFilePath() };
|
||||
}
|
||||
return { Logger::serviceLogsFilePath() };
|
||||
#elif defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
if (stream == Stream::App) {
|
||||
return { QString::fromStdString(SWIFT_BRIDGE_NAMESPACE::swiftAppLogPath()), Logger::userLogsFilePath() };
|
||||
}
|
||||
return { QString::fromStdString(SWIFT_BRIDGE_NAMESPACE::swiftTunnelLogPath()) };
|
||||
#elif defined(Q_OS_ANDROID)
|
||||
return AndroidController::logFiles(static_cast<int>(stream));
|
||||
#endif
|
||||
}
|
||||
|
||||
qint64 LogsController::size(Stream stream)
|
||||
{
|
||||
qint64 total = 0;
|
||||
for (const QString &path : files(stream)) {
|
||||
total += fileSize(path);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
qint64 LogsController::fileSize(const QString &path)
|
||||
{
|
||||
QFile file(path);
|
||||
return file.open(QIODevice::ReadOnly) ? file.size() : 0;
|
||||
}
|
||||
|
||||
QString LogsController::readTail(Stream stream, qint64 *newestFileEnd, qint64 maxBytes)
|
||||
{
|
||||
return QString::fromUtf8(read(stream, maxBytes, newestFileEnd));
|
||||
}
|
||||
|
||||
QByteArray LogsController::readAll(Stream stream)
|
||||
{
|
||||
return read(stream, std::numeric_limits<qint64>::max());
|
||||
}
|
||||
|
||||
QByteArray LogsController::read(Stream stream, qint64 maxBytes, qint64 *newestFileEnd)
|
||||
{
|
||||
QByteArray data = tailOfFiles(files(stream), maxBytes, newestFileEnd);
|
||||
#if defined(Q_OS_ANDROID)
|
||||
if (stream == Stream::App && maxBytes == std::numeric_limits<qint64>::max()) {
|
||||
data.prepend(AndroidController::deviceInfo().toUtf8() + '\n');
|
||||
}
|
||||
#endif
|
||||
return data;
|
||||
}
|
||||
|
||||
void LogsController::clear(Stream stream)
|
||||
{
|
||||
#if defined(AMNEZIA_DESKTOP)
|
||||
if (stream == Stream::App) {
|
||||
Logger::clearLogs(false);
|
||||
} else {
|
||||
Logger::clearServiceLogs();
|
||||
}
|
||||
#elif defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
if (stream == Stream::App) {
|
||||
Logger::clearLogs(false);
|
||||
QFile::resize(QString::fromStdString(SWIFT_BRIDGE_NAMESPACE::swiftAppLogPath()), 0);
|
||||
} else {
|
||||
QFile::resize(QString::fromStdString(SWIFT_BRIDGE_NAMESPACE::swiftTunnelLogPath()), 0);
|
||||
}
|
||||
#elif defined(Q_OS_ANDROID)
|
||||
AndroidController::clearLogStream(static_cast<int>(stream));
|
||||
#endif
|
||||
}
|
||||
|
||||
QByteArray LogsController::zipAll()
|
||||
{
|
||||
return zipFiles({ { QStringLiteral("app.log"), readAll(Stream::App) },
|
||||
{ QStringLiteral("tunnel.log"), readAll(Stream::Tunnel) } });
|
||||
}
|
||||
|
||||
QByteArray LogsController::zipFiles(const QList<QPair<QString, QByteArray>> &entries)
|
||||
{
|
||||
#ifdef AMNEZIA_HAS_QZIPWRITER
|
||||
QBuffer buffer;
|
||||
buffer.open(QIODevice::WriteOnly);
|
||||
|
||||
QZipWriter zip(&buffer);
|
||||
zip.setCompressionPolicy(QZipWriter::AlwaysCompress);
|
||||
for (const auto &[name, data] : entries) {
|
||||
zip.addFile(name, data);
|
||||
}
|
||||
zip.close();
|
||||
|
||||
return buffer.data();
|
||||
#else
|
||||
return storedZip(entries);
|
||||
#endif
|
||||
}
|
||||
|
||||
QByteArray LogsController::tailOfFiles(const QStringList &paths, qint64 maxBytes, qint64 *newestFileEnd)
|
||||
{
|
||||
if (newestFileEnd) {
|
||||
*newestFileEnd = 0;
|
||||
}
|
||||
|
||||
QList<QPair<QString, QByteArray>> chunks;
|
||||
qint64 remaining = maxBytes;
|
||||
|
||||
for (auto it = paths.crbegin(); it != paths.crend() && remaining > 0; ++it) {
|
||||
QFile file(*it);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const qint64 fileSize = file.size();
|
||||
QByteArray chunk;
|
||||
if (fileSize <= remaining) {
|
||||
chunk = file.read(fileSize);
|
||||
remaining -= fileSize;
|
||||
} else {
|
||||
file.seek(fileSize - remaining - 1);
|
||||
chunk = file.read(remaining + 1);
|
||||
const qsizetype newline = chunk.indexOf('\n');
|
||||
qsizetype start = newline + 1;
|
||||
if (newline < 0) {
|
||||
start = 1;
|
||||
while (start < chunk.size() && (static_cast<uchar>(chunk.at(start)) & 0xC0) == 0x80) {
|
||||
++start;
|
||||
}
|
||||
}
|
||||
chunk = chunk.mid(start);
|
||||
remaining = 0;
|
||||
}
|
||||
|
||||
if (newestFileEnd && it == paths.crbegin()) {
|
||||
*newestFileEnd = file.pos();
|
||||
}
|
||||
|
||||
if (!chunk.isEmpty()) {
|
||||
chunks.prepend({ QFileInfo(*it).fileName(), chunk });
|
||||
}
|
||||
}
|
||||
|
||||
if (chunks.size() == 1) {
|
||||
return chunks.first().second;
|
||||
}
|
||||
|
||||
QByteArray result;
|
||||
for (const auto &[fileName, chunk] : chunks) {
|
||||
result += "----- " + fileName.toUtf8() + " -----\n" + chunk;
|
||||
if (!chunk.endsWith('\n')) {
|
||||
result += '\n';
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
#ifndef LOGSCONTROLLER_H
|
||||
#define LOGSCONTROLLER_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QList>
|
||||
#include <QPair>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
class LogsController
|
||||
{
|
||||
public:
|
||||
enum class Stream { App, Tunnel };
|
||||
|
||||
static QStringList files(Stream stream);
|
||||
static qint64 size(Stream stream);
|
||||
static qint64 fileSize(const QString &path);
|
||||
static QString readTail(Stream stream, qint64 *newestFileEnd = nullptr, qint64 maxBytes = 512 * 1024);
|
||||
static QByteArray readAll(Stream stream);
|
||||
static void clear(Stream stream);
|
||||
static QByteArray zipAll();
|
||||
static QByteArray zipFiles(const QList<QPair<QString, QByteArray>> &entries);
|
||||
|
||||
static QByteArray tailOfFiles(const QStringList &paths, qint64 maxBytes, qint64 *newestFileEnd = nullptr);
|
||||
|
||||
private:
|
||||
static QByteArray read(Stream stream, qint64 maxBytes, qint64 *newestFileEnd = nullptr);
|
||||
};
|
||||
|
||||
#endif // LOGSCONTROLLER_H
|
||||
@@ -328,7 +328,7 @@ ExportController::ExportResult ExportController::generateXrayConfig(const QStrin
|
||||
vlessServer.serverName = realitySettings.value(amnezia::protocols::xray::serverName).toString();
|
||||
vlessServer.publicKey = realitySettings.value(amnezia::protocols::xray::publicKey).toString();
|
||||
vlessServer.shortId = realitySettings.value(amnezia::protocols::xray::shortId).toString();
|
||||
vlessServer.fingerprint = realitySettings.value(amnezia::protocols::xray::fingerprint).toString("chrome");
|
||||
vlessServer.fingerprint = realitySettings.value(amnezia::protocols::xray::fingerprint).toString(QString::fromLatin1(amnezia::protocols::xray::defaultFingerprint));
|
||||
vlessServer.spiderX = realitySettings.value(amnezia::protocols::xray::spiderX).toString("");
|
||||
} else if (vlessServer.security == "tls") {
|
||||
QJsonObject tlsSettings = streamSettings.value("tlsSettings").toObject();
|
||||
|
||||
@@ -33,6 +33,18 @@ using namespace ProtocolUtils;
|
||||
|
||||
namespace
|
||||
{
|
||||
bool rejectUnsupportedFormatVersion(ImportController::ImportResult &result)
|
||||
{
|
||||
if (serverConfigUtils::isConfigFormatVersionSupported(result.config)) {
|
||||
return false;
|
||||
}
|
||||
qWarning() << "Config format version" << serverConfigUtils::configFormatVersion(result.config)
|
||||
<< "is newer than supported" << serverConfigUtils::currentConfigFormatVersion;
|
||||
result.errorCode = ErrorCode::ConfigFormatVersionNotSupportedError;
|
||||
result.config = {};
|
||||
return true;
|
||||
}
|
||||
|
||||
ConfigTypes checkConfigFormat(const QString &config)
|
||||
{
|
||||
const QString openVpnConfigPatternCli = "client";
|
||||
@@ -52,6 +64,7 @@ namespace
|
||||
const QString amneziaFreeConfigPattern = "api_key";
|
||||
const QString amneziaPremiumConfigPattern = "auth_data";
|
||||
const QString backupPattern = "Servers/serversList";
|
||||
const QString xraySettingsPattern = "serverConfig";
|
||||
|
||||
if (config.contains(backupPattern)) {
|
||||
return ConfigTypes::Backup;
|
||||
@@ -67,6 +80,8 @@ namespace
|
||||
} else if (config.contains(openVpnConfigPatternCli)
|
||||
&& (config.contains(openVpnConfigPatternDriver1) || config.contains(openVpnConfigPatternDriver2))) {
|
||||
return ConfigTypes::OpenVpn;
|
||||
} else if (QJsonDocument::fromJson(config.toUtf8()).object().value(xraySettingsPattern).isObject()) {
|
||||
return ConfigTypes::XraySettings;
|
||||
}
|
||||
return ConfigTypes::Invalid;
|
||||
}
|
||||
@@ -207,6 +222,10 @@ ImportController::ImportResult ImportController::extractConfigFromData(const QSt
|
||||
case ConfigTypes::Amnezia: {
|
||||
result.config = QJsonDocument::fromJson(config.toUtf8()).object();
|
||||
|
||||
if (rejectUnsupportedFormatVersion(result)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (serverConfigUtils::isServerFromApi(result.config)) {
|
||||
auto apiConfig = result.config.value(apiDefs::key::apiConfig).toObject();
|
||||
apiConfig[apiDefs::key::vpnKey] = data;
|
||||
@@ -231,6 +250,10 @@ ImportController::ImportResult ImportController::extractConfigFromData(const QSt
|
||||
result.errorCode = ErrorCode::ImportBackupFileUseRestoreInstead;
|
||||
return result;
|
||||
}
|
||||
case ConfigTypes::XraySettings: {
|
||||
result.errorCode = ErrorCode::ImportXraySettingsFileUseXrayPage;
|
||||
return result;
|
||||
}
|
||||
case ConfigTypes::Invalid: {
|
||||
result.errorCode = ErrorCode::ImportInvalidConfigError;
|
||||
result.configFileName.clear();
|
||||
@@ -256,6 +279,7 @@ ImportController::ImportResult ImportController::extractConfigFromQr(const QByte
|
||||
if (!dataObj.isEmpty()) {
|
||||
result.config = dataObj;
|
||||
result.configType = ConfigTypes::Amnezia;
|
||||
rejectUnsupportedFormatVersion(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -267,6 +291,7 @@ ImportController::ImportResult ImportController::extractConfigFromQr(const QByte
|
||||
return result;
|
||||
}
|
||||
result.configType = ConfigTypes::Amnezia;
|
||||
rejectUnsupportedFormatVersion(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -284,6 +309,7 @@ ImportController::ImportResult ImportController::extractConfigFromQr(const QByte
|
||||
return result;
|
||||
}
|
||||
result.configType = ConfigTypes::Amnezia;
|
||||
rejectUnsupportedFormatVersion(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -379,6 +405,11 @@ int ImportController::qrChunksTotal() const
|
||||
|
||||
void ImportController::importConfig(const QJsonObject &config)
|
||||
{
|
||||
if (!serverConfigUtils::isConfigFormatVersionSupported(config)) {
|
||||
emit importErrorOccurred(ErrorCode::ConfigFormatVersionNotSupportedError, false);
|
||||
return;
|
||||
}
|
||||
|
||||
ServerCredentials credentials;
|
||||
credentials.hostName = config.value(configKey::hostName).toString();
|
||||
credentials.port = config.value(configKey::port).toInt();
|
||||
@@ -748,8 +779,11 @@ void ImportController::processAmneziaConfig(QJsonObject &config) const
|
||||
}
|
||||
|
||||
QJsonObject jsonConfig = QJsonDocument::fromJson(protocolConfig.toUtf8()).object();
|
||||
jsonConfig[configKey::mtu] =
|
||||
ContainerUtils::isAwgContainer(dockerContainer) ? protocols::awg::defaultMtu : protocols::wireguard::defaultMtu;
|
||||
if (jsonConfig.value(configKey::mtu).toString().trimmed().isEmpty()) {
|
||||
jsonConfig[configKey::mtu] = ContainerUtils::isAwgContainer(dockerContainer)
|
||||
? protocols::awg::defaultMtu
|
||||
: protocols::wireguard::defaultMtu;
|
||||
}
|
||||
|
||||
containerConfig[configKey::lastConfig] = QString(QJsonDocument(jsonConfig).toJson());
|
||||
|
||||
@@ -759,4 +793,3 @@ void ImportController::processAmneziaConfig(QJsonObject &config) const
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ namespace
|
||||
Xray,
|
||||
ShadowSocks,
|
||||
Backup,
|
||||
XraySettings,
|
||||
Invalid
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
#include "core/configurators/configuratorBase.h"
|
||||
#include "core/configurators/xrayConfigurator.h"
|
||||
#include "core/models/protocols/xrayProtocolConfig.h"
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
@@ -24,6 +25,7 @@
|
||||
#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"
|
||||
@@ -60,6 +62,70 @@ namespace
|
||||
{
|
||||
Logger logger("InstallController");
|
||||
|
||||
QString effectiveXrayPort(const XrayProtocolConfig *cfg)
|
||||
{
|
||||
if (!cfg || cfg->serverConfig.port.isEmpty()) {
|
||||
return QString::fromLatin1(protocols::xray::defaultPort);
|
||||
}
|
||||
return cfg->serverConfig.port;
|
||||
}
|
||||
|
||||
QString normalizeXrayRelease(QString raw)
|
||||
{
|
||||
raw = raw.trimmed();
|
||||
if (raw.startsWith(QLatin1String("v"), Qt::CaseInsensitive)) {
|
||||
raw.remove(0, 1);
|
||||
}
|
||||
return raw.trimmed();
|
||||
}
|
||||
|
||||
QString parseXrayVersionOutput(const QString &output)
|
||||
{
|
||||
static const QRegularExpression re(QStringLiteral(R"(Xray\s+v?(\d+\.\d+\.\d+))"),
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
const auto match = re.match(output);
|
||||
if (!match.hasMatch()) {
|
||||
return {};
|
||||
}
|
||||
return match.captured(1);
|
||||
}
|
||||
|
||||
enum class XrayBinaryProbeResult {
|
||||
Match,
|
||||
Mismatch,
|
||||
Failed
|
||||
};
|
||||
|
||||
XrayBinaryProbeResult probeXrayServerBinary(const ServerCredentials &credentials, SshSession &sshSession)
|
||||
{
|
||||
const QString expected =
|
||||
normalizeXrayRelease(QString::fromLatin1(protocols::xray::expectedServerXrayRelease));
|
||||
|
||||
QString stdOut;
|
||||
auto collect = [&stdOut](const QString &data, libssh::Client &) {
|
||||
stdOut += data;
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
const QString script = SshSession::replaceVars(
|
||||
QStringLiteral("sudo docker exec $CONTAINER_NAME xray version 2>/dev/null | head -1"),
|
||||
amnezia::genBaseVars(credentials, DockerContainer::Xray, QString(), QString()));
|
||||
const ErrorCode errorCode = sshSession.runScript(credentials, script, collect, collect);
|
||||
const QString live = parseXrayVersionOutput(stdOut);
|
||||
|
||||
if (errorCode != ErrorCode::NoError || live.isEmpty()) {
|
||||
return XrayBinaryProbeResult::Failed;
|
||||
}
|
||||
if (live != expected) {
|
||||
return XrayBinaryProbeResult::Mismatch;
|
||||
}
|
||||
return XrayBinaryProbeResult::Match;
|
||||
}
|
||||
|
||||
bool xrayServerBinaryMismatchesExpected(const ServerCredentials &credentials, SshSession &sshSession)
|
||||
{
|
||||
return probeXrayServerBinary(credentials, sshSession) == XrayBinaryProbeResult::Mismatch;
|
||||
}
|
||||
|
||||
bool dockerDaemonContainerMissing(const QString &out, const QString &containerDockerName)
|
||||
{
|
||||
if (!out.contains(QLatin1String("Error response from daemon"), Qt::CaseInsensitive)) {
|
||||
@@ -75,15 +141,35 @@ 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("\nsudo docker volume rm -f $CONTAINER_NAME-data 2>/dev/null || true");
|
||||
script += QLatin1String(
|
||||
"\nfor attempt in 1 2 3 4 5; do"
|
||||
"\n sudo docker volume rm -f $CONTAINER_NAME-data && break"
|
||||
"\n sleep 1"
|
||||
"\ndone"
|
||||
"\necho \"amnezia_volume_left=$(sudo docker volume ls -q -f name=^$CONTAINER_NAME-data$ | head -1)\"");
|
||||
script = SshSession::replaceVars(script, vars);
|
||||
}
|
||||
return script;
|
||||
}
|
||||
|
||||
bool dataVolumeSurvivedRemoval(const QString &out)
|
||||
{
|
||||
static const QRegularExpression reLeft(QStringLiteral("amnezia_volume_left=(\\S+)"));
|
||||
return reLeft.match(out).hasMatch();
|
||||
}
|
||||
}
|
||||
|
||||
InstallController::InstallController(SecureServersRepository *serversRepository,
|
||||
@@ -102,7 +188,7 @@ InstallController::~InstallController()
|
||||
}
|
||||
|
||||
ErrorCode InstallController::setupContainer(const ServerCredentials &credentials, DockerContainer container, ContainerConfig &config,
|
||||
bool isUpdate)
|
||||
bool isUpdate, bool acceptXrayKeyLoss)
|
||||
{
|
||||
SshSession sshSession;
|
||||
ErrorCode e = ErrorCode::NoError;
|
||||
@@ -129,12 +215,38 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
|
||||
if (e)
|
||||
return e;
|
||||
|
||||
QMap<QString, QString> xrayStateToMigrate;
|
||||
if (isUpdate) {
|
||||
if (!acceptXrayKeyLoss) {
|
||||
e = readXrayStateBeforeVolumeMigration(credentials, container, sshSession, xrayStateToMigrate);
|
||||
if (e)
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
||||
const amnezia::ScriptVars removeContainerVars =
|
||||
amnezia::genBaseVars(credentials, container, QString(), QString());
|
||||
const bool removeDataVolume = !isUpdate && (container == DockerContainer::MtProxy
|
||||
|| container == DockerContainer::Telemt || container == DockerContainer::TProxy);
|
||||
sshSession.runScript(credentials, buildRemoveContainerScript(removeContainerVars, removeDataVolume));
|
||||
const bool removeDataVolume = !isUpdate && containerKeepsIdentityInDataVolume(container);
|
||||
QString removeOut;
|
||||
auto collectRemoveOut = [&removeOut](const QString &data, libssh::Client &) {
|
||||
removeOut += data + "\n";
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
const ErrorCode removeError =
|
||||
sshSession.runScript(credentials, buildRemoveContainerScript(removeContainerVars, removeDataVolume),
|
||||
collectRemoveOut, collectRemoveOut);
|
||||
qDebug().noquote() << "InstallController::setupContainer removeContainer finished";
|
||||
|
||||
if (removeDataVolume) {
|
||||
if (removeError != ErrorCode::NoError) {
|
||||
return ErrorCode::ServerDataVolumeNotRemoved;
|
||||
}
|
||||
if (dataVolumeSurvivedRemoval(removeOut)) {
|
||||
return ErrorCode::ServerDataVolumeNotRemoved;
|
||||
}
|
||||
}
|
||||
|
||||
qDebug().noquote() << "buildContainerWorker start";
|
||||
e = buildContainerWorker(credentials, container, config, sshSession);
|
||||
if (e)
|
||||
return e;
|
||||
@@ -143,6 +255,10 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
|
||||
if (e)
|
||||
return e;
|
||||
|
||||
e = restoreXrayStateIntoDataVolume(credentials, container, sshSession, xrayStateToMigrate);
|
||||
if (e)
|
||||
return e;
|
||||
|
||||
e = configureContainerWorker(credentials, container, config, sshSession);
|
||||
if (e)
|
||||
return e;
|
||||
@@ -161,47 +277,72 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
|
||||
}
|
||||
|
||||
ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerContainer container, const ContainerConfig &oldConfig,
|
||||
ContainerConfig &newConfig)
|
||||
ContainerConfig &newConfig, bool acceptXrayKeyLoss)
|
||||
{
|
||||
if (!isUpdateDockerContainerRequired(container, oldConfig, newConfig)) {
|
||||
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
if (!adminConfig.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
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);
|
||||
}
|
||||
adminConfig->updateContainerConfig(container, newConfig);
|
||||
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
const bool serverSettingsChanged = isUpdateDockerContainerRequired(container, oldConfig, newConfig);
|
||||
|
||||
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
if (!adminConfig.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
ServerCredentials credentials = adminConfig->credentials();
|
||||
SshSession sshSession;
|
||||
|
||||
bool reinstallRequired = false;
|
||||
bool portReinstall = false;
|
||||
bool binaryMismatch = false;
|
||||
if (serverSettingsChanged) {
|
||||
if (!credentials.isValid()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
portReinstall = isReinstallContainerRequired(container, oldConfig, newConfig);
|
||||
reinstallRequired = portReinstall;
|
||||
}
|
||||
if (container == DockerContainer::Xray && serverSettingsChanged) {
|
||||
if (credentials.isValid()) {
|
||||
binaryMismatch = xrayServerBinaryMismatchesExpected(credentials, sshSession);
|
||||
if (binaryMismatch) {
|
||||
reinstallRequired = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!serverSettingsChanged && !reinstallRequired) {
|
||||
if (container == DockerContainer::MtProxy) {
|
||||
MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
|
||||
} else if (container == DockerContainer::Telemt) {
|
||||
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;
|
||||
}
|
||||
|
||||
if (!credentials.isValid()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
SshSession sshSession;
|
||||
|
||||
bool reinstallRequired = isReinstallContainerRequired(container, oldConfig, newConfig);
|
||||
qDebug() << "InstallController::updateServerConfig for container" << container << "reinstall required is" << reinstallRequired;
|
||||
|
||||
ErrorCode errorCode = ErrorCode::NoError;
|
||||
if (reinstallRequired) {
|
||||
errorCode = setupContainer(credentials, container, newConfig, true);
|
||||
if (container == DockerContainer::Xray) {
|
||||
const QString oldPort = effectiveXrayPort(oldConfig.getXrayProtocolConfig());
|
||||
const QString newPort = effectiveXrayPort(newConfig.getXrayProtocolConfig());
|
||||
if (oldPort != newPort) {
|
||||
errorCode = isServerPortBusy(credentials, container, newConfig, sshSession);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
errorCode = setupContainer(credentials, container, newConfig, true, acceptXrayKeyLoss);
|
||||
|
||||
// Reinstall pulls the latest container image, so the server runs the latest protocol version
|
||||
if (errorCode == ErrorCode::NoError && container == DockerContainer::Awg2) {
|
||||
@@ -209,7 +350,12 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
|
||||
awgConfig->serverConfig.protocolVersion = protocols::awg::awgV3;
|
||||
}
|
||||
}
|
||||
} else if (container != DockerContainer::Xray && container != DockerContainer::SSXray) {
|
||||
} else if (container == DockerContainer::Xray) {
|
||||
DnsSettings dnsSettings = { m_appSettingsRepository->primaryDns(), m_appSettingsRepository->secondaryDns() };
|
||||
XrayConfigurator xrayConfigurator(&sshSession);
|
||||
errorCode = xrayConfigurator.writeServerConfigForSetup(credentials, container, newConfig, dnsSettings,
|
||||
true);
|
||||
} else if (container != DockerContainer::SSXray) {
|
||||
errorCode = configureContainerWorker(credentials, container, newConfig, sshSession);
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
errorCode = startupContainerWorker(credentials, container, newConfig, sshSession);
|
||||
@@ -230,9 +376,13 @@ 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) {
|
||||
clearCachedProfile(serverId, container);
|
||||
if (container != DockerContainer::Xray) {
|
||||
clearCachedProfile(serverId, container);
|
||||
}
|
||||
}
|
||||
adminConfig->updateContainerConfig(container, newConfig);
|
||||
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
|
||||
@@ -441,6 +591,9 @@ ErrorCode InstallController::prepareContainerConfig(DockerContainer container, c
|
||||
void InstallController::adminAppendRequested(const QString &serverId, DockerContainer container,
|
||||
const ContainerConfig &containerConfig, const QString &clientName)
|
||||
{
|
||||
if (container == DockerContainer::Xray) {
|
||||
return;
|
||||
}
|
||||
if (ContainerUtils::containerService(container) == ServiceType::Other
|
||||
|| !containerConfig.protocolConfig.hasClientConfig()) {
|
||||
return;
|
||||
@@ -570,6 +723,10 @@ ErrorCode InstallController::configureContainerWorker(const ServerCredentials &c
|
||||
return ErrorCode::ServerContainerMissingError;
|
||||
}
|
||||
|
||||
if (stdOut.contains(QLatin1String("amnezia_xray_keygen=failed"))) {
|
||||
return ErrorCode::XrayKeyGenerationFailed;
|
||||
}
|
||||
|
||||
updateContainerConfigAfterInstallation(container, config, stdOut);
|
||||
|
||||
if (container == DockerContainer::MtProxy) {
|
||||
@@ -578,6 +735,8 @@ 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;
|
||||
@@ -606,6 +765,127 @@ ErrorCode InstallController::startupContainerWorker(const ServerCredentials &cre
|
||||
baseVars));
|
||||
}
|
||||
|
||||
ErrorCode InstallController::readXrayStateBeforeVolumeMigration(const ServerCredentials &credentials,
|
||||
DockerContainer container, SshSession &sshSession,
|
||||
QMap<QString, QString> &outFiles)
|
||||
{
|
||||
outFiles.clear();
|
||||
|
||||
if (container != DockerContainer::Xray) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
const amnezia::ScriptVars vars = amnezia::genBaseVars(credentials, container, QString(), QString());
|
||||
|
||||
QString stdOut;
|
||||
auto collect = [&stdOut](const QString &data, libssh::Client &) {
|
||||
stdOut += data + "\n";
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
|
||||
const QString probe = QStringLiteral(
|
||||
"echo \"amnezia_container=$(sudo docker ps -a -q -f name=^$CONTAINER_NAME$ | head -1)\"\n"
|
||||
"if [ -n \"$(sudo docker ps -a -q -f name=^$CONTAINER_NAME$ | head -1)\" ] && "
|
||||
"[ -z \"$(sudo docker ps -q -f name=^$CONTAINER_NAME$ | head -1)\" ]; then\n"
|
||||
" sudo docker start $CONTAINER_NAME > /dev/null 2>&1\n"
|
||||
" sleep 2\n"
|
||||
"fi\n"
|
||||
"echo \"amnezia_running=$(sudo docker ps -q -f name=^$CONTAINER_NAME$ | head -1)\"\n"
|
||||
"echo \"amnezia_mounted=$(sudo docker inspect -f "
|
||||
"'{{range .Mounts}}{{if and (eq .Destination \"%1\") "
|
||||
"(eq .Name \"$CONTAINER_NAME-data\")}}yes{{end}}{{end}}' "
|
||||
"$CONTAINER_NAME 2>/dev/null | head -1)\"")
|
||||
.arg(QString::fromLatin1(amnezia::protocols::xray::dataDir));
|
||||
ErrorCode errorCode = sshSession.runScript(credentials, SshSession::replaceVars(probe, vars), collect, collect);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return ErrorCode::XrayKeyMigrationFailed;
|
||||
}
|
||||
|
||||
static const QRegularExpression reContainer(QStringLiteral("amnezia_container=(\\S+)"));
|
||||
static const QRegularExpression reRunning(QStringLiteral("amnezia_running=(\\S+)"));
|
||||
static const QRegularExpression reMounted(QStringLiteral("amnezia_mounted=(\\S+)"));
|
||||
const bool containerExists = reContainer.match(stdOut).hasMatch();
|
||||
const bool containerRunning = reRunning.match(stdOut).hasMatch();
|
||||
const bool dataDirIsVolume = reMounted.match(stdOut).hasMatch();
|
||||
|
||||
if (!containerExists) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
if (dataDirIsVolume) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
if (!containerRunning) {
|
||||
return ErrorCode::XrayKeyMigrationNeedsConfirm;
|
||||
}
|
||||
|
||||
const QStringList requiredPaths = {
|
||||
QString::fromLatin1(amnezia::protocols::xray::PrivateKeyPath),
|
||||
QString::fromLatin1(amnezia::protocols::xray::PublicKeyPath),
|
||||
QString::fromLatin1(amnezia::protocols::xray::uuidPath),
|
||||
QString::fromLatin1(amnezia::protocols::xray::shortidPath),
|
||||
};
|
||||
|
||||
for (const QString &path : requiredPaths) {
|
||||
QString content;
|
||||
bool read = false;
|
||||
for (int attempt = 0; attempt < 3 && !read; ++attempt) {
|
||||
ErrorCode fileError = ErrorCode::NoError;
|
||||
content = QString::fromUtf8(sshSession.getTextFileFromContainer(container, credentials, path, fileError));
|
||||
if (fileError == ErrorCode::NoError && !content.trimmed().isEmpty()) {
|
||||
read = true;
|
||||
break;
|
||||
}
|
||||
if (attempt < 2) {
|
||||
QThread::msleep(500);
|
||||
}
|
||||
}
|
||||
if (!read) {
|
||||
return ErrorCode::XrayKeyMigrationFailed;
|
||||
}
|
||||
outFiles.insert(path, content);
|
||||
}
|
||||
|
||||
QString serverConfig;
|
||||
bool configRead = false;
|
||||
for (int attempt = 0; attempt < 3 && !configRead; ++attempt) {
|
||||
ErrorCode configError = ErrorCode::NoError;
|
||||
serverConfig = QString::fromUtf8(sshSession.getTextFileFromContainer(
|
||||
container, credentials, QString::fromLatin1(amnezia::protocols::xray::serverConfigPath), configError));
|
||||
if (configError == ErrorCode::NoError && !serverConfig.trimmed().isEmpty()) {
|
||||
configRead = true;
|
||||
break;
|
||||
}
|
||||
if (attempt < 2) {
|
||||
QThread::msleep(500);
|
||||
}
|
||||
}
|
||||
if (!configRead) {
|
||||
return ErrorCode::XrayKeyMigrationFailed;
|
||||
}
|
||||
outFiles.insert(QString::fromLatin1(amnezia::protocols::xray::serverConfigPath), serverConfig);
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode InstallController::restoreXrayStateIntoDataVolume(const ServerCredentials &credentials,
|
||||
DockerContainer container, SshSession &sshSession,
|
||||
const QMap<QString, QString> &files)
|
||||
{
|
||||
if (files.isEmpty()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
for (auto it = files.constBegin(); it != files.constEnd(); ++it) {
|
||||
const ErrorCode errorCode = sshSession.uploadTextFileToContainer(
|
||||
container, credentials, it.value(), it.key(), libssh::ScpOverwriteMode::ScpOverwriteExisting);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return ErrorCode::XrayKeyMigrationFailed;
|
||||
}
|
||||
}
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode InstallController::isServerPortBusy(const ServerCredentials &credentials, DockerContainer container, const ContainerConfig &config, SshSession &sshSession)
|
||||
{
|
||||
if (container == DockerContainer::Dns) {
|
||||
@@ -638,6 +918,14 @@ 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);
|
||||
@@ -743,7 +1031,20 @@ bool InstallController::isReinstallContainerRequired(DockerContainer container,
|
||||
}
|
||||
}
|
||||
|
||||
if (container == DockerContainer::Xray || container == DockerContainer::SSXray) {
|
||||
if (container == DockerContainer::Xray) {
|
||||
const auto *oldXrayConfig = oldConfig.getXrayProtocolConfig();
|
||||
const auto *newXrayConfig = newConfig.getXrayProtocolConfig();
|
||||
|
||||
if (oldXrayConfig && newXrayConfig) {
|
||||
const QString oldPort = effectiveXrayPort(oldXrayConfig);
|
||||
const QString newPort = effectiveXrayPort(newXrayConfig);
|
||||
if (oldPort != newPort) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (container == DockerContainer::SSXray) {
|
||||
const auto *oldXrayConfig = oldConfig.getXrayProtocolConfig();
|
||||
const auto *newXrayConfig = newConfig.getXrayProtocolConfig();
|
||||
|
||||
@@ -800,6 +1101,14 @@ 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;
|
||||
}
|
||||
@@ -837,7 +1146,7 @@ ErrorCode InstallController::installDockerWorker(const ServerCredentials &creden
|
||||
qDebug().noquote() << "InstallController::installDockerWorker" << stdOut;
|
||||
|
||||
if (container == DockerContainer::MtProxy || container == DockerContainer::Telemt
|
||||
|| container == DockerContainer::TProxy) {
|
||||
|| container == DockerContainer::TProxy || container == DockerContainer::WhatsAppProxy) {
|
||||
QString conntrackOut;
|
||||
auto cbConntrack = [&](const QString &data, libssh::Client &) {
|
||||
conntrackOut += data + "\n";
|
||||
@@ -1015,6 +1324,43 @@ ErrorCode InstallController::rebootServer(const QString &serverId)
|
||||
return sshSession.runScript(credentials, script, cbReadStdOut, cbReadStdErr);
|
||||
}
|
||||
|
||||
ErrorCode InstallController::updateServerPackages(const QString &serverId)
|
||||
{
|
||||
const auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
if (!adminConfig.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
ServerCredentials credentials = adminConfig->credentials();
|
||||
if (!credentials.isValid()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
SshSession sshSession;
|
||||
|
||||
ErrorCode errorCode = isServerDpkgBusy(credentials, sshSession);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
QString stdOut;
|
||||
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
|
||||
stdOut += data + "\n";
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
|
||||
errorCode = sshSession.runScript(credentials, amnezia::scriptData(SharedScriptType::update_server_packages),
|
||||
cbReadStdOut, cbReadStdOut);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
if (!stdOut.contains("Packages update finished")) {
|
||||
qDebug().noquote() << "InstallController::updateServerPackages" << stdOut;
|
||||
return ErrorCode::ServerPacketManagerError;
|
||||
}
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode InstallController::removeAllContainers(const QString &serverId)
|
||||
{
|
||||
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
@@ -1050,10 +1396,19 @@ ErrorCode InstallController::removeContainer(const QString &serverId, DockerCont
|
||||
SshSession sshSession;
|
||||
const amnezia::ScriptVars removeContainerVars =
|
||||
amnezia::genBaseVars(credentials, container, QString(), QString());
|
||||
const bool removeDataVolume = (container == DockerContainer::MtProxy || container == DockerContainer::Telemt
|
||||
|| container == DockerContainer::TProxy);
|
||||
ErrorCode errorCode =
|
||||
sshSession.runScript(credentials, buildRemoveContainerScript(removeContainerVars, removeDataVolume));
|
||||
const bool removeDataVolume = containerKeepsIdentityInDataVolume(container);
|
||||
QString removeOut;
|
||||
auto collectRemoveOut = [&removeOut](const QString &data, libssh::Client &) {
|
||||
removeOut += data + "\n";
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
ErrorCode errorCode = sshSession.runScript(
|
||||
credentials, buildRemoveContainerScript(removeContainerVars, removeDataVolume), collectRemoveOut,
|
||||
collectRemoveOut);
|
||||
|
||||
if (errorCode == ErrorCode::NoError && removeDataVolume && dataVolumeSurvivedRemoval(removeOut)) {
|
||||
errorCode = ErrorCode::ServerDataVolumeNotRemoved;
|
||||
}
|
||||
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
QMap<DockerContainer, ContainerConfig> containers = adminConfig->containers;
|
||||
@@ -1091,6 +1446,7 @@ 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));
|
||||
}
|
||||
}
|
||||
@@ -1125,6 +1481,14 @@ 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);
|
||||
}
|
||||
|
||||
@@ -1134,6 +1498,11 @@ 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)
|
||||
{
|
||||
@@ -1155,6 +1524,13 @@ bool InstallController::isUpdateDockerContainerRequired(DockerContainer containe
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else if (container == DockerContainer::Xray) {
|
||||
const auto *oldXray = oldConfig.getXrayProtocolConfig();
|
||||
const auto *newXray = newConfig.getXrayProtocolConfig();
|
||||
if (oldXray && newXray && oldXray->serverConfig.hasEqualServerSettings(newXray->serverConfig)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} else if (container == DockerContainer::MtProxy) {
|
||||
const auto *oldMt = oldConfig.getMtProxyProtocolConfig();
|
||||
const auto *newMt = newConfig.getMtProxyProtocolConfig();
|
||||
@@ -1176,6 +1552,13 @@ 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;
|
||||
@@ -1607,6 +1990,11 @@ 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) {
|
||||
@@ -1637,6 +2025,11 @@ 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) {
|
||||
@@ -1653,7 +2046,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::TProxy && container != DockerContainer::WhatsAppProxy) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
@@ -1683,6 +2076,9 @@ 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,15 +33,16 @@ public:
|
||||
QObject *parent = nullptr);
|
||||
~InstallController();
|
||||
|
||||
ErrorCode setupContainer(const ServerCredentials &credentials, DockerContainer container, ContainerConfig &config, bool isUpdate = false);
|
||||
ErrorCode setupContainer(const ServerCredentials &credentials, DockerContainer container, ContainerConfig &config, bool isUpdate = false, bool acceptXrayKeyLoss = false);
|
||||
|
||||
// Updates server-side container settings (admin self-hosted only): reconfigures the container over SSH.
|
||||
ErrorCode updateServerConfig(const QString &serverId, DockerContainer container, const ContainerConfig &oldConfig, ContainerConfig &newConfig);
|
||||
ErrorCode updateServerConfig(const QString &serverId, DockerContainer container, const ContainerConfig &oldConfig, ContainerConfig &newConfig, bool acceptXrayKeyLoss = false);
|
||||
|
||||
// Updates client-local settings only: rewrites the stored container config for any self-hosted/native server. No SSH.
|
||||
ErrorCode updateClientConfig(const QString &serverId, DockerContainer container, ContainerConfig &newConfig);
|
||||
|
||||
ErrorCode rebootServer(const QString &serverId);
|
||||
ErrorCode updateServerPackages(const QString &serverId);
|
||||
ErrorCode removeAllContainers(const QString &serverId);
|
||||
ErrorCode removeContainer(const QString &serverId, DockerContainer container);
|
||||
|
||||
@@ -56,6 +57,7 @@ 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);
|
||||
@@ -106,6 +108,11 @@ private:
|
||||
ErrorCode configureContainerWorker(const ServerCredentials &credentials, DockerContainer container, ContainerConfig &config, SshSession &sshSession);
|
||||
ErrorCode startupContainerWorker(const ServerCredentials &credentials, DockerContainer container, const ContainerConfig &config, SshSession &sshSession);
|
||||
|
||||
ErrorCode readXrayStateBeforeVolumeMigration(const ServerCredentials &credentials, DockerContainer container,
|
||||
SshSession &sshSession, QMap<QString, QString> &outFiles);
|
||||
ErrorCode restoreXrayStateIntoDataVolume(const ServerCredentials &credentials, DockerContainer container,
|
||||
SshSession &sshSession, const QMap<QString, QString> &files);
|
||||
|
||||
ErrorCode isServerPortBusy(const ServerCredentials &credentials, DockerContainer container, const ContainerConfig &config, SshSession &sshSession);
|
||||
ErrorCode isUserInSudo(const ServerCredentials &credentials, SshSession &sshSession);
|
||||
ErrorCode isServerDpkgBusy(const ServerCredentials &credentials, SshSession &sshSession);
|
||||
@@ -130,7 +137,8 @@ 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,6 +2,7 @@
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QDateTime>
|
||||
|
||||
#include "core/utils/containerEnum.h"
|
||||
@@ -55,6 +56,33 @@ int UsersController::clientIndexById(const QString &clientId, const QJsonArray &
|
||||
return -1;
|
||||
}
|
||||
|
||||
QString UsersController::readXrayVolumeUuid(const DockerContainer container, const ServerCredentials &credentials,
|
||||
SshSession *sshSession) const
|
||||
{
|
||||
ErrorCode error = ErrorCode::NoError;
|
||||
const QString uuid = QString::fromUtf8(sshSession->getTextFileFromContainer(
|
||||
container, credentials, QString::fromLatin1(amnezia::protocols::xray::uuidPath), error));
|
||||
if (error != ErrorCode::NoError) {
|
||||
return {};
|
||||
}
|
||||
return uuid.trimmed();
|
||||
}
|
||||
|
||||
int UsersController::stripXrayVolumeUuidFromTable(QJsonArray &clientsTable, const QString &volumeUuid)
|
||||
{
|
||||
if (volumeUuid.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
int removed = 0;
|
||||
for (int i = clientsTable.size() - 1; i >= 0; --i) {
|
||||
if (clientsTable.at(i).toObject().value(configKey::clientId).toString() == volumeUuid) {
|
||||
clientsTable.removeAt(i);
|
||||
++removed;
|
||||
}
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
void UsersController::migration(const QByteArray &clientsTableString, QJsonArray &clientsTable)
|
||||
{
|
||||
QJsonObject clientsTableObj = QJsonDocument::fromJson(clientsTableString).object();
|
||||
@@ -248,7 +276,8 @@ ErrorCode UsersController::getWireGuardClients(const DockerContainer container,
|
||||
}
|
||||
|
||||
ErrorCode UsersController::getXrayClients(const DockerContainer container, const ServerCredentials& credentials,
|
||||
SshSession* sshSession, int &count, QJsonArray &clientsTable)
|
||||
SshSession* sshSession, int &count, QJsonArray &clientsTable,
|
||||
const QString &volumeDefaultUuid)
|
||||
{
|
||||
ErrorCode error = ErrorCode::NoError;
|
||||
|
||||
@@ -283,6 +312,7 @@ ErrorCode UsersController::getXrayClients(const DockerContainer container, const
|
||||
}
|
||||
|
||||
const QJsonArray clients = settings[protocols::xray::clients].toArray();
|
||||
const QString xrayDefaultUuid = volumeDefaultUuid;
|
||||
for (const auto &clientValue : clients) {
|
||||
const QJsonObject clientObj = clientValue.toObject();
|
||||
if (!clientObj.contains(protocols::xray::id)) {
|
||||
@@ -290,9 +320,6 @@ ErrorCode UsersController::getXrayClients(const DockerContainer container, const
|
||||
continue;
|
||||
}
|
||||
QString clientId = clientObj[protocols::xray::id].toString();
|
||||
|
||||
QString xrayDefaultUuid = sshSession->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::uuidPath, error);
|
||||
xrayDefaultUuid.replace("\n", "");
|
||||
|
||||
if (!isClientExists(clientId, clientsTable) && clientId != xrayDefaultUuid) {
|
||||
QJsonObject client;
|
||||
@@ -339,6 +366,9 @@ ErrorCode UsersController::updateClients(const QString &serverId, const DockerCo
|
||||
|
||||
m_clientsTable = QJsonDocument::fromJson(clientsTableString).array();
|
||||
|
||||
const QString xrayVolumeUuid =
|
||||
(container == DockerContainer::Xray) ? readXrayVolumeUuid(container, credentials, &sshSession) : QString();
|
||||
|
||||
if (m_clientsTable.isEmpty()) {
|
||||
migration(clientsTableString, m_clientsTable);
|
||||
|
||||
@@ -349,7 +379,7 @@ ErrorCode UsersController::updateClients(const QString &serverId, const DockerCo
|
||||
} else if (container == DockerContainer::WireGuard || ContainerUtils::isAwgContainer(container)) {
|
||||
error = getWireGuardClients(container, credentials, &sshSession, count, m_clientsTable);
|
||||
} else if (container == DockerContainer::Xray) {
|
||||
error = getXrayClients(container, credentials, &sshSession, count, m_clientsTable);
|
||||
error = getXrayClients(container, credentials, &sshSession, count, m_clientsTable, xrayVolumeUuid);
|
||||
}
|
||||
if (error != ErrorCode::NoError) {
|
||||
emit clientsUpdated(QJsonArray());
|
||||
@@ -365,6 +395,15 @@ ErrorCode UsersController::updateClients(const QString &serverId, const DockerCo
|
||||
}
|
||||
}
|
||||
|
||||
if (container == DockerContainer::Xray) {
|
||||
const int stripped = stripXrayVolumeUuidFromTable(m_clientsTable, xrayVolumeUuid);
|
||||
if (stripped > 0) {
|
||||
const QByteArray strippedTable = QJsonDocument(m_clientsTable).toJson();
|
||||
const ErrorCode writeError =
|
||||
sshSession.uploadTextFileToContainer(container, credentials, strippedTable, clientsTableFile);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<WgShowData> data;
|
||||
wgShow(container, credentials, &sshSession, data);
|
||||
|
||||
@@ -424,6 +463,13 @@ ErrorCode UsersController::appendClient(const QString &serverId, const QString &
|
||||
return error;
|
||||
}
|
||||
|
||||
if (container == DockerContainer::Xray) {
|
||||
const QString volumeUuid = readXrayVolumeUuid(container, credentials, &sshSession);
|
||||
if (!volumeUuid.isEmpty() && clientId == volumeUuid) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
}
|
||||
|
||||
int existingIndex = clientIndexById(clientId, m_clientsTable);
|
||||
if (existingIndex >= 0) {
|
||||
return renameClient(serverId, existingIndex, clientName, container, true);
|
||||
@@ -634,6 +680,19 @@ ErrorCode UsersController::revokeXray(const int row,
|
||||
|
||||
ErrorCode error = ErrorCode::NoError;
|
||||
|
||||
auto client = clientsTable.at(row).toObject();
|
||||
QString clientId = client.value(configKey::clientId).toString();
|
||||
|
||||
const QString volumeUuid = readXrayVolumeUuid(container, credentials, sshSession);
|
||||
if (!volumeUuid.isEmpty() && clientId == volumeUuid) {
|
||||
clientsTable.removeAt(row);
|
||||
const QByteArray clientsTableString = QJsonDocument(clientsTable).toJson();
|
||||
const QString clientsTableFile = QString("/opt/amnezia/%1/clientsTable")
|
||||
.arg(ContainerUtils::containerTypeToString(container));
|
||||
error = sshSession->uploadTextFileToContainer(container, credentials, clientsTableString, clientsTableFile);
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
const QString serverConfigPath = amnezia::protocols::xray::serverConfigPath;
|
||||
const QString configString = sshSession->getTextFileFromContainer(container, credentials, serverConfigPath, error);
|
||||
if (error != ErrorCode::NoError) {
|
||||
@@ -647,9 +706,6 @@ ErrorCode UsersController::revokeXray(const int row,
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
auto client = clientsTable.at(row).toObject();
|
||||
QString clientId = client.value(configKey::clientId).toString();
|
||||
|
||||
QJsonObject configObj = serverConfig.object();
|
||||
if (!configObj.contains(protocols::xray::inbounds)) {
|
||||
logger.error() << "Missing inbounds in xray config";
|
||||
|
||||
@@ -64,7 +64,12 @@ private:
|
||||
ErrorCode getWireGuardClients(const DockerContainer container, const ServerCredentials &credentials,
|
||||
SshSession* sshSession, int &count, QJsonArray &clientsTable);
|
||||
ErrorCode getXrayClients(const DockerContainer container, const ServerCredentials& credentials,
|
||||
SshSession* sshSession, int &count, QJsonArray &clientsTable);
|
||||
SshSession* sshSession, int &count, QJsonArray &clientsTable,
|
||||
const QString &volumeDefaultUuid);
|
||||
|
||||
QString readXrayVolumeUuid(const DockerContainer container, const ServerCredentials &credentials,
|
||||
SshSession *sshSession) const;
|
||||
static int stripXrayVolumeUuidFromTable(QJsonArray &clientsTable, const QString &volumeUuid);
|
||||
|
||||
ErrorCode wgShow(const DockerContainer container, const ServerCredentials &credentials,
|
||||
SshSession* sshSession, std::vector<WgShowData> &data);
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
#include "version.h"
|
||||
#include "ui/utils/qAutoStart.h"
|
||||
#include "logger.h"
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include "platforms/android/android_controller.h"
|
||||
#include "core/controllers/logsController.h"
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
#include "core/utils/swiftBridge.h"
|
||||
#endif
|
||||
|
||||
QString getPlatformName()
|
||||
@@ -86,6 +87,9 @@ void SettingsController::toggleLogging(bool enable)
|
||||
qWarning() << "Initialization of debug subsystem failed";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
SWIFT_BRIDGE_NAMESPACE::toggleLogging(enable);
|
||||
#endif
|
||||
Logger::setServiceLogsEnabled(enable);
|
||||
|
||||
@@ -96,12 +100,8 @@ void SettingsController::toggleLogging(bool enable)
|
||||
|
||||
void SettingsController::clearLogs()
|
||||
{
|
||||
#ifdef Q_OS_ANDROID
|
||||
AndroidController::instance()->clearLogs();
|
||||
#else
|
||||
Logger::clearLogs(false);
|
||||
Logger::clearServiceLogs();
|
||||
#endif
|
||||
LogsController::clear(LogsController::Stream::App);
|
||||
LogsController::clear(LogsController::Stream::Tunnel);
|
||||
}
|
||||
|
||||
QByteArray SettingsController::backupAppConfig() const
|
||||
@@ -119,6 +119,11 @@ QByteArray SettingsController::backupAppConfig() const
|
||||
return QJsonDocument(config).toJson();
|
||||
}
|
||||
|
||||
int SettingsController::unsupportedFormatConfigsSkippedCount() const
|
||||
{
|
||||
return m_serversRepository->unsupportedFormatConfigsCount();
|
||||
}
|
||||
|
||||
ErrorCode SettingsController::restoreAppConfigFromData(const QByteArray &data)
|
||||
{
|
||||
if (!m_appSettingsRepository->restoreAppConfig(data)) {
|
||||
|
||||
@@ -39,6 +39,7 @@ public:
|
||||
|
||||
QByteArray backupAppConfig() const;
|
||||
ErrorCode restoreAppConfigFromData(const QByteArray &data);
|
||||
int unsupportedFormatConfigsSkippedCount() const;
|
||||
|
||||
QString getAppVersion() const;
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "core/controllers/gatewayController.h"
|
||||
#include "core/utils/api/gatewayPayloadBuilder.h"
|
||||
#include "core/utils/appUiConfig.h"
|
||||
#include "core/utils/constants/apiConstants.h"
|
||||
#include "core/utils/constants/apiKeys.h"
|
||||
#include "core/utils/selfhosted/scriptsRegistry.h"
|
||||
|
||||
@@ -129,6 +130,10 @@ void UpdateController::setUpdateCheckRunning(bool running)
|
||||
|
||||
void UpdateController::checkForUpdates()
|
||||
{
|
||||
#if !CLIENT_ENABLE_APP_UPDATES
|
||||
return;
|
||||
#endif
|
||||
|
||||
if (m_updateCheckRunning || !m_appSettingsRepository) {
|
||||
return;
|
||||
}
|
||||
@@ -136,7 +141,7 @@ void UpdateController::checkForUpdates()
|
||||
|
||||
auto gatewayController = QSharedPointer<GatewayController>::create(m_appSettingsRepository->getGatewayEndpoint(),
|
||||
m_appSettingsRepository->isDevGatewayEnv(),
|
||||
7000,
|
||||
apiDefs::requestTimeoutMsecs,
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled(),
|
||||
m_appSettingsRepository);
|
||||
|
||||
|
||||
@@ -61,8 +61,6 @@ void AwgInstaller::generateAwgParameters(AwgServerConfig &serverConfig)
|
||||
serverConfig.maxHandshakeAttempts = protocols::awg::defaultMaxHandshakeAttempts;
|
||||
serverConfig.randomTrailers = protocols::awg::defaultRandomTrailers;
|
||||
serverConfig.disableCookies = protocols::awg::defaultDisableCookies;
|
||||
|
||||
serverConfig.specialJunk1 = protocols::awg::defaultSpecialJunk1;
|
||||
}
|
||||
|
||||
ErrorCode AwgInstaller::extractConfigFromContainer(DockerContainer container, const ServerCredentials &credentials,
|
||||
|
||||
@@ -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.fingerprint = protocols::xray::defaultFingerprint;
|
||||
srv.alpn = protocols::xray::defaultAlpn;
|
||||
srv.fingerprint = protocols::xray::defaultFingerprint;
|
||||
config.protocolConfig = xrayConfig;
|
||||
break;
|
||||
}
|
||||
@@ -125,6 +125,13 @@ 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;
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#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
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "xrayInstaller.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonArray>
|
||||
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
@@ -11,6 +10,7 @@
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include "core/utils/selfhosted/sshSession.h"
|
||||
#include "core/configurators/xrayConfigurator.h"
|
||||
#include "core/models/protocols/xrayProtocolConfig.h"
|
||||
#include "logger.h"
|
||||
|
||||
@@ -19,37 +19,21 @@ namespace
|
||||
Logger logger("XrayInstaller");
|
||||
|
||||
// Xray expects uTLS preset names (chrome, firefox, …). Old Amnezia/server templates used "Mozilla/5.0".
|
||||
QString normalizeXrayFingerprint(const QString &fp)
|
||||
QString describeServerJsonStatus(amnezia::XrayServerJsonStatus status)
|
||||
{
|
||||
if (fp.isEmpty() || fp.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) {
|
||||
return QString::fromLatin1(protocols::xray::defaultFingerprint);
|
||||
}
|
||||
return fp;
|
||||
}
|
||||
|
||||
// Parse an xray int range: "from-to" string, plain int, or legacy {from,to} object.
|
||||
void parseIntRange(const QJsonValue &v, QString &minOut, QString &maxOut)
|
||||
{
|
||||
if (v.isString()) {
|
||||
const QString s = v.toString().trimmed();
|
||||
const int dash = s.indexOf(QLatin1Char('-'), 1);
|
||||
if (dash > 0) {
|
||||
minOut = s.left(dash).trimmed();
|
||||
maxOut = s.mid(dash + 1).trimmed();
|
||||
} else if (!s.isEmpty()) {
|
||||
minOut = s;
|
||||
maxOut = s;
|
||||
}
|
||||
} else if (v.isDouble()) {
|
||||
minOut = QString::number(v.toInt());
|
||||
maxOut = minOut;
|
||||
} else if (v.isObject()) {
|
||||
const QJsonObject o = v.toObject();
|
||||
if (o.contains(QLatin1String("from")) || o.contains(QLatin1String("to"))) {
|
||||
minOut = QString::number(o.value(QLatin1String("from")).toInt());
|
||||
maxOut = QString::number(o.value(QLatin1String("to")).toInt());
|
||||
}
|
||||
switch (status) {
|
||||
case amnezia::XrayServerJsonStatus::MissingInbounds:
|
||||
return QStringLiteral("server config missing 'inbounds' field");
|
||||
case amnezia::XrayServerJsonStatus::EmptyInbounds:
|
||||
return QStringLiteral("server config has empty 'inbounds' array");
|
||||
case amnezia::XrayServerJsonStatus::MissingStreamSettings:
|
||||
return QStringLiteral("inbound missing 'streamSettings' field");
|
||||
case amnezia::XrayServerJsonStatus::MissingSettings:
|
||||
return QStringLiteral("inbound missing 'settings' field");
|
||||
case amnezia::XrayServerJsonStatus::Ok:
|
||||
break;
|
||||
}
|
||||
return QStringLiteral("ok");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +49,7 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c
|
||||
SshSession* sshSession, ContainerConfig &config)
|
||||
{
|
||||
ErrorCode errorCode = ErrorCode::NoError;
|
||||
|
||||
|
||||
QString currentConfig = sshSession->getTextFileFromContainer(
|
||||
container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode);
|
||||
|
||||
@@ -78,267 +62,16 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c
|
||||
logger.error() << "Failed to parse server config JSON";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
QJsonObject serverConfig = doc.object();
|
||||
|
||||
if (!serverConfig.contains(protocols::xray::inbounds)) {
|
||||
logger.error() << "Server config missing 'inbounds' field";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
QJsonArray inbounds = serverConfig[protocols::xray::inbounds].toArray();
|
||||
if (inbounds.isEmpty()) {
|
||||
logger.error() << "Server config has empty 'inbounds' array";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
QJsonObject inbound = inbounds[0].toObject();
|
||||
if (!inbound.contains(protocols::xray::streamSettings)) {
|
||||
logger.error() << "Inbound missing 'streamSettings' field";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
QJsonObject streamSettings = inbound[protocols::xray::streamSettings].toObject();
|
||||
auto *xrayConfig = config.getXrayProtocolConfig();
|
||||
if (!xrayConfig) {
|
||||
logger.error() << "No XrayProtocolConfig in ContainerConfig";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
XrayServerConfig &srv = xrayConfig->serverConfig;
|
||||
|
||||
// ── Port ─────────────────────────────────────────────────────────
|
||||
if (inbound.contains(protocols::xray::port)) {
|
||||
srv.port = QString::number(inbound[protocols::xray::port].toInt());
|
||||
}
|
||||
|
||||
// ── Network (transport) ───────────────────────────────────────────
|
||||
QString networkVal = streamSettings.value(protocols::xray::network).toString("tcp");
|
||||
if (networkVal == "xhttp") {
|
||||
srv.transport = "xhttp";
|
||||
} else if (networkVal == "kcp") {
|
||||
srv.transport = "mkcp";
|
||||
} else {
|
||||
srv.transport = "raw";
|
||||
}
|
||||
|
||||
// ── Security ──────────────────────────────────────────────────────
|
||||
srv.security = streamSettings.value(protocols::xray::security).toString("reality");
|
||||
|
||||
// ── Reality settings ──────────────────────────────────────────────
|
||||
if (srv.security == "reality") {
|
||||
QJsonObject rs = streamSettings.value(protocols::xray::realitySettings).toObject();
|
||||
|
||||
// serverNames array → site + sni
|
||||
if (rs.contains(protocols::xray::serverNames)) {
|
||||
QString sniVal = rs[protocols::xray::serverNames].toArray().first().toString();
|
||||
srv.sni = sniVal;
|
||||
srv.site = sniVal;
|
||||
} else if (rs.contains(protocols::xray::serverName)) {
|
||||
srv.sni = rs[protocols::xray::serverName].toString();
|
||||
srv.site = srv.sni;
|
||||
}
|
||||
|
||||
srv.fingerprint = normalizeXrayFingerprint(rs.value(protocols::xray::fingerprint).toString());
|
||||
}
|
||||
|
||||
// ── TLS settings ──────────────────────────────────────────────────
|
||||
if (srv.security == "tls") {
|
||||
QJsonObject tls = streamSettings.value("tlsSettings").toObject();
|
||||
srv.sni = tls.value(protocols::xray::serverName).toString();
|
||||
srv.fingerprint = normalizeXrayFingerprint(tls.value(protocols::xray::fingerprint).toString());
|
||||
|
||||
QJsonArray alpnArr = tls.value("alpn").toArray();
|
||||
QStringList alpnList;
|
||||
for (const QJsonValue &v : alpnArr) {
|
||||
QString t = v.toString().trimmed();
|
||||
if (t.compare(QLatin1String("HTTP/2"), Qt::CaseInsensitive) == 0)
|
||||
t = QStringLiteral("h2");
|
||||
else if (t.compare(QLatin1String("HTTP/1.1"), Qt::CaseInsensitive) == 0)
|
||||
t = QStringLiteral("http/1.1");
|
||||
if (!t.isEmpty())
|
||||
alpnList << t;
|
||||
}
|
||||
srv.alpn = alpnList.join(",");
|
||||
}
|
||||
|
||||
// ── Flow (from users array) ───────────────────────────────────────
|
||||
if (inbound.contains(protocols::xray::settings)) {
|
||||
QJsonObject s = inbound[protocols::xray::settings].toObject();
|
||||
QJsonArray clientsArr = s.value(protocols::xray::clients).toArray();
|
||||
if (!clientsArr.isEmpty()) {
|
||||
srv.flow = clientsArr[0].toObject().value(protocols::xray::flow).toString();
|
||||
}
|
||||
}
|
||||
|
||||
// ── XHTTP settings (Xray-core SplitHTTPConfig + legacy Amnezia keys) ──
|
||||
if (srv.transport == "xhttp") {
|
||||
QJsonObject xhttpObj = streamSettings.value("xhttpSettings").toObject();
|
||||
{
|
||||
const QString m = xhttpObj.value("mode").toString();
|
||||
if (m.isEmpty() || m == QLatin1String("auto"))
|
||||
srv.xhttp.mode = QStringLiteral("Auto");
|
||||
else if (m == QLatin1String("packet-up"))
|
||||
srv.xhttp.mode = QStringLiteral("Packet-up");
|
||||
else if (m == QLatin1String("stream-up"))
|
||||
srv.xhttp.mode = QStringLiteral("Stream-up");
|
||||
else if (m == QLatin1String("stream-one"))
|
||||
srv.xhttp.mode = QStringLiteral("Stream-one");
|
||||
else
|
||||
srv.xhttp.mode = m;
|
||||
}
|
||||
|
||||
srv.xhttp.host = xhttpObj.value("host").toString();
|
||||
srv.xhttp.path = xhttpObj.value("path").toString();
|
||||
|
||||
if (xhttpObj.contains(QLatin1String("uplinkHTTPMethod")))
|
||||
srv.xhttp.uplinkMethod = xhttpObj.value("uplinkHTTPMethod").toString();
|
||||
else
|
||||
srv.xhttp.uplinkMethod = xhttpObj.value("method").toString();
|
||||
|
||||
srv.xhttp.disableGrpc = xhttpObj.value("noGRPCHeader").toBool(true);
|
||||
srv.xhttp.disableSse = xhttpObj.value("noSSEHeader").toBool(true);
|
||||
|
||||
auto sessionSeqUi = [](const QString &core) -> QString {
|
||||
if (core.isEmpty() || core == QLatin1String("path"))
|
||||
return QStringLiteral("Path");
|
||||
if (core == QLatin1String("cookie"))
|
||||
return QStringLiteral("Cookie");
|
||||
if (core == QLatin1String("header"))
|
||||
return QStringLiteral("Header");
|
||||
if (core == QLatin1String("query"))
|
||||
return QStringLiteral("Query");
|
||||
return core;
|
||||
};
|
||||
QString sess = xhttpObj.value("sessionIDPlacement").toString();
|
||||
if (sess.isEmpty())
|
||||
sess = xhttpObj.value("sessionPlacement").toString();
|
||||
if (sess.isEmpty())
|
||||
sess = xhttpObj.value("scSessionPlacement").toString();
|
||||
srv.xhttp.sessionPlacement = sessionSeqUi(sess);
|
||||
|
||||
QString seq = xhttpObj.value("seqPlacement").toString();
|
||||
if (seq.isEmpty())
|
||||
seq = xhttpObj.value("scSeqPlacement").toString();
|
||||
srv.xhttp.seqPlacement = sessionSeqUi(seq);
|
||||
|
||||
auto uplinkDataUi = [](const QString &core) -> QString {
|
||||
if (core.isEmpty() || core == QLatin1String("body"))
|
||||
return QStringLiteral("Body");
|
||||
if (core == QLatin1String("auto"))
|
||||
return QStringLiteral("Auto");
|
||||
if (core == QLatin1String("header"))
|
||||
return QStringLiteral("Header");
|
||||
if (core == QLatin1String("cookie"))
|
||||
return QStringLiteral("Cookie");
|
||||
return core;
|
||||
};
|
||||
QString udata = xhttpObj.value("uplinkDataPlacement").toString();
|
||||
if (udata.isEmpty())
|
||||
udata = xhttpObj.value("scUplinkDataPlacement").toString();
|
||||
srv.xhttp.uplinkDataPlacement = uplinkDataUi(udata);
|
||||
|
||||
srv.xhttp.sessionKey = xhttpObj.value("sessionIDKey").toString();
|
||||
if (srv.xhttp.sessionKey.isEmpty())
|
||||
srv.xhttp.sessionKey = xhttpObj.value("sessionKey").toString();
|
||||
srv.xhttp.seqKey = xhttpObj.value("seqKey").toString();
|
||||
srv.xhttp.uplinkDataKey = xhttpObj.value("uplinkDataKey").toString();
|
||||
|
||||
if (xhttpObj.contains(QLatin1String("uplinkChunkSize"))) {
|
||||
QString ucMin, ucMax;
|
||||
parseIntRange(xhttpObj.value("uplinkChunkSize"), ucMin, ucMax);
|
||||
if (!ucMin.isEmpty())
|
||||
srv.xhttp.uplinkChunkSize = ucMin;
|
||||
} else if (xhttpObj.contains(QLatin1String("xhttpUplinkChunkSize"))) {
|
||||
srv.xhttp.uplinkChunkSize = QString::number(xhttpObj.value("xhttpUplinkChunkSize").toInt());
|
||||
}
|
||||
if (xhttpObj.contains(QLatin1String("scMaxBufferedPosts"))) {
|
||||
srv.xhttp.scMaxBufferedPosts = QString::number(xhttpObj.value("scMaxBufferedPosts").toVariant().toLongLong());
|
||||
}
|
||||
|
||||
auto readRange = [&](const char *key, QString &minOut, QString &maxOut) {
|
||||
parseIntRange(xhttpObj.value(QLatin1String(key)), minOut, maxOut);
|
||||
};
|
||||
readRange("scMaxEachPostBytes", srv.xhttp.scMaxEachPostBytesMin, srv.xhttp.scMaxEachPostBytesMax);
|
||||
readRange("scMinPostsIntervalMs", srv.xhttp.scMinPostsIntervalMsMin, srv.xhttp.scMinPostsIntervalMsMax);
|
||||
readRange("scStreamUpServerSecs", srv.xhttp.scStreamUpServerSecsMin, srv.xhttp.scStreamUpServerSecsMax);
|
||||
|
||||
auto loadPaddingFromObject = [&](const QJsonObject &pad) {
|
||||
if (pad.contains(QLatin1String("xPaddingObfsMode")))
|
||||
srv.xhttp.xPadding.obfsMode = pad.value("xPaddingObfsMode").toBool(true);
|
||||
srv.xhttp.xPadding.key = pad.value("xPaddingKey").toString();
|
||||
srv.xhttp.xPadding.header = pad.value("xPaddingHeader").toString();
|
||||
srv.xhttp.xPadding.placement = pad.value("xPaddingPlacement").toString();
|
||||
srv.xhttp.xPadding.method = pad.value("xPaddingMethod").toString();
|
||||
QString bytesMin, bytesMax;
|
||||
parseIntRange(pad.value("xPaddingBytes"), bytesMin, bytesMax);
|
||||
if (!bytesMin.isEmpty()) {
|
||||
srv.xhttp.xPadding.bytesMin = bytesMin;
|
||||
srv.xhttp.xPadding.bytesMax = bytesMax;
|
||||
}
|
||||
QString pl = srv.xhttp.xPadding.placement.toLower();
|
||||
if (pl == QLatin1String("cookie"))
|
||||
srv.xhttp.xPadding.placement = QStringLiteral("Cookie");
|
||||
else if (pl == QLatin1String("header"))
|
||||
srv.xhttp.xPadding.placement = QStringLiteral("Header");
|
||||
else if (pl == QLatin1String("query"))
|
||||
srv.xhttp.xPadding.placement = QStringLiteral("Query");
|
||||
else if (pl == QLatin1String("queryinheader"))
|
||||
srv.xhttp.xPadding.placement = QStringLiteral("Query in header");
|
||||
QString met = srv.xhttp.xPadding.method.toLower();
|
||||
if (met == QLatin1String("repeat-x"))
|
||||
srv.xhttp.xPadding.method = QStringLiteral("Repeat-x");
|
||||
else if (met == QLatin1String("tokenish"))
|
||||
srv.xhttp.xPadding.method = QStringLiteral("Tokenish");
|
||||
};
|
||||
if (xhttpObj.contains(QLatin1String("xPaddingObfsMode")) || xhttpObj.contains(QLatin1String("xPaddingKey"))
|
||||
|| xhttpObj.contains(QLatin1String("xPaddingBytes"))) {
|
||||
loadPaddingFromObject(xhttpObj);
|
||||
} else if (xhttpObj.contains(QLatin1String("xPadding")) && xhttpObj.value("xPadding").isObject()) {
|
||||
const QJsonObject nested = xhttpObj.value("xPadding").toObject();
|
||||
if (!nested.isEmpty()) {
|
||||
loadPaddingFromObject(nested);
|
||||
if (!nested.contains(QLatin1String("xPaddingObfsMode")))
|
||||
srv.xhttp.xPadding.obfsMode = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (xhttpObj.contains(QLatin1String("xmux"))) {
|
||||
QJsonObject mux = xhttpObj.value("xmux").toObject();
|
||||
srv.xhttp.xmux.enabled = true;
|
||||
|
||||
auto readMuxRange = [&](const char *key, QString &minOut, QString &maxOut) {
|
||||
parseIntRange(mux.value(QLatin1String(key)), minOut, maxOut);
|
||||
};
|
||||
readMuxRange("maxConcurrency", srv.xhttp.xmux.maxConcurrencyMin, srv.xhttp.xmux.maxConcurrencyMax);
|
||||
readMuxRange("maxConnections", srv.xhttp.xmux.maxConnectionsMin, srv.xhttp.xmux.maxConnectionsMax);
|
||||
readMuxRange("cMaxReuseTimes", srv.xhttp.xmux.cMaxReuseTimesMin, srv.xhttp.xmux.cMaxReuseTimesMax);
|
||||
readMuxRange("hMaxRequestTimes", srv.xhttp.xmux.hMaxRequestTimesMin, srv.xhttp.xmux.hMaxRequestTimesMax);
|
||||
readMuxRange("hMaxReusableSecs", srv.xhttp.xmux.hMaxReusableSecsMin, srv.xhttp.xmux.hMaxReusableSecsMax);
|
||||
|
||||
if (mux.contains(QLatin1String("hKeepAlivePeriod")))
|
||||
srv.xhttp.xmux.hKeepAlivePeriod = QString::number(mux.value("hKeepAlivePeriod").toVariant().toLongLong());
|
||||
}
|
||||
}
|
||||
|
||||
// ── mKCP settings ─────────────────────────────────────────────────
|
||||
if (srv.transport == "mkcp") {
|
||||
QJsonObject kcp = streamSettings.value("kcpSettings").toObject();
|
||||
if (kcp.contains("tti")) {
|
||||
srv.mkcp.tti = QString::number(kcp["tti"].toInt());
|
||||
}
|
||||
if (kcp.contains("uplinkCapacity")) {
|
||||
srv.mkcp.uplinkCapacity = QString::number(kcp["uplinkCapacity"].toInt());
|
||||
}
|
||||
if (kcp.contains("downlinkCapacity")) {
|
||||
srv.mkcp.downlinkCapacity = QString::number(kcp["downlinkCapacity"].toInt());
|
||||
}
|
||||
if (kcp.contains("readBufferSize")) {
|
||||
srv.mkcp.readBufferSize = QString::number(kcp["readBufferSize"].toInt());
|
||||
}
|
||||
if (kcp.contains("writeBufferSize")) {
|
||||
srv.mkcp.writeBufferSize = QString::number(kcp["writeBufferSize"].toInt());
|
||||
}
|
||||
srv.mkcp.congestion = kcp.value("congestion").toBool(true);
|
||||
const amnezia::XrayServerJsonStatus status =
|
||||
XrayServerConfig::fromServerInboundJson(doc.object(), xrayConfig->serverConfig);
|
||||
if (status != amnezia::XrayServerJsonStatus::Ok) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
return ErrorCode::NoError;
|
||||
|
||||
@@ -85,6 +85,7 @@ QPair<QString, QString> ApiV2ServerConfig::getDnsPair(const QString &primaryDns,
|
||||
QJsonObject ApiV2ServerConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
obj[configKey::formatVersion] = serverConfigUtils::currentConfigFormatVersion;
|
||||
|
||||
if (!name.isEmpty()) {
|
||||
obj[configKey::name] = name;
|
||||
|
||||
@@ -143,6 +143,16 @@ 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>();
|
||||
|
||||
@@ -66,6 +66,9 @@ struct ContainerConfig {
|
||||
TProxyProtocolConfig* getTProxyProtocolConfig();
|
||||
const TProxyProtocolConfig* getTProxyProtocolConfig() const;
|
||||
|
||||
WhatsAppProxyProtocolConfig* getWhatsAppProxyProtocolConfig();
|
||||
const WhatsAppProxyProtocolConfig* getWhatsAppProxyProtocolConfig() const;
|
||||
|
||||
Ikev2ProtocolConfig* getIkev2ProtocolConfig();
|
||||
const Ikev2ProtocolConfig* getIkev2ProtocolConfig() const;
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#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
|
||||
{
|
||||
@@ -47,6 +48,8 @@ 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);
|
||||
@@ -80,6 +83,8 @@ 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);
|
||||
@@ -109,6 +114,8 @@ 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);
|
||||
@@ -326,6 +333,8 @@ 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{}};
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#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
|
||||
{
|
||||
@@ -42,6 +43,7 @@ struct ProtocolConfig {
|
||||
MtProxyProtocolConfig,
|
||||
TelemtProtocolConfig,
|
||||
TProxyProtocolConfig,
|
||||
WhatsAppProxyProtocolConfig,
|
||||
Ikev2ProtocolConfig,
|
||||
TorProtocolConfig,
|
||||
DnsProtocolConfig
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
#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();
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
#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
@@ -1,9 +1,11 @@
|
||||
#ifndef XRAYPROTOCOLCONFIG_H
|
||||
#define XRAYPROTOCOLCONFIG_H
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <optional>
|
||||
|
||||
namespace amnezia
|
||||
@@ -16,8 +18,8 @@ struct XrayXPaddingConfig {
|
||||
bool obfsMode = true; // xPaddingObfsMode
|
||||
QString key; // xPaddingKey
|
||||
QString header; // xPaddingHeader
|
||||
QString placement = protocols::xray::defaultXPaddingPlacement; // xPaddingPlacement: Cookie|Header|Query|Body
|
||||
QString method = protocols::xray::defaultXPaddingMethod; // xPaddingMethod: Repeat-x|Random|Zero
|
||||
QString placement; // xPaddingPlacement: Cookie|Header|Query|Query in header
|
||||
QString method; // xPaddingMethod: Repeat-x|Tokenish
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static XrayXPaddingConfig fromJson(const QJsonObject &json);
|
||||
@@ -45,33 +47,35 @@ struct XrayXmuxConfig {
|
||||
|
||||
// ── XHTTP transport ───────────────────────────────────────────────────────────
|
||||
struct XrayXhttpConfig {
|
||||
QString mode = protocols::xray::defaultXhttpMode; // Auto|Packet-up|Stream-up|Stream-one
|
||||
QString host = protocols::xray::defaultXhttpHost;
|
||||
QString mode;
|
||||
QString host;
|
||||
QString path;
|
||||
QString uplinkMethod = protocols::xray::defaultXhttpUplinkMethod; // POST|PUT|PATCH
|
||||
bool disableGrpc = true;
|
||||
bool disableSse = true;
|
||||
|
||||
// Session & Sequence
|
||||
QString sessionPlacement = protocols::xray::defaultXhttpSessionPlacement;
|
||||
QString sessionKey = protocols::xray::defaultXhttpSessionKey;
|
||||
QString seqPlacement = protocols::xray::defaultXhttpSeqPlacement;
|
||||
QString sessionPlacement;
|
||||
QString sessionKey;
|
||||
QString seqPlacement;
|
||||
QString seqKey;
|
||||
QString uplinkDataPlacement = protocols::xray::defaultXhttpUplinkDataPlacement;
|
||||
QString uplinkDataPlacement;
|
||||
QString uplinkDataKey;
|
||||
|
||||
// Traffic Shaping
|
||||
QString uplinkChunkSize = protocols::xray::defaultXhttpUplinkChunkSize;
|
||||
QString uplinkMethod;
|
||||
QString uplinkChunkSize;
|
||||
QString scMinPostsIntervalMsMin;
|
||||
QString scMinPostsIntervalMsMax;
|
||||
|
||||
XrayXmuxConfig xmux;
|
||||
|
||||
QString scMaxBufferedPosts;
|
||||
QString scMaxEachPostBytesMin = protocols::xray::defaultXhttpScMaxEachPostBytesMin;
|
||||
QString scMaxEachPostBytesMax = protocols::xray::defaultXhttpScMaxEachPostBytesMax;
|
||||
QString scMinPostsIntervalMsMin = protocols::xray::defaultXhttpScMinPostsIntervalMsMin;
|
||||
QString scMinPostsIntervalMsMax = protocols::xray::defaultXhttpScMinPostsIntervalMsMax;
|
||||
QString scStreamUpServerSecsMin = protocols::xray::defaultXhttpScStreamUpServerSecsMin;
|
||||
QString scStreamUpServerSecsMax = protocols::xray::defaultXhttpScStreamUpServerSecsMax;
|
||||
QString scMaxEachPostBytesMin;
|
||||
QString scMaxEachPostBytesMax;
|
||||
QString scStreamUpServerSecsMin;
|
||||
QString scStreamUpServerSecsMax;
|
||||
|
||||
XrayXPaddingConfig xPadding;
|
||||
XrayXmuxConfig xmux;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
/// Reads only keys present in JSON (no Amnezia UI defaults). Use XrayConfigModel::applyDefaultsToServerConfig for UI.
|
||||
@@ -81,16 +85,40 @@ struct XrayXhttpConfig {
|
||||
// ── mKCP transport ────────────────────────────────────────────────────────────
|
||||
struct XrayMkcpConfig {
|
||||
QString tti;
|
||||
QString mtu;
|
||||
QString uplinkCapacity;
|
||||
QString downlinkCapacity;
|
||||
QString readBufferSize;
|
||||
QString writeBufferSize;
|
||||
bool congestion = true;
|
||||
QString cwndMultiplier;
|
||||
QString maxSendingWindow;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static XrayMkcpConfig fromJson(const QJsonObject &json);
|
||||
};
|
||||
|
||||
enum class XrayStreamSide {
|
||||
Server,
|
||||
Client,
|
||||
};
|
||||
|
||||
struct XrayServerInboundInputs {
|
||||
QJsonArray clients;
|
||||
QString realityPrivateKey;
|
||||
QString realityShortId;
|
||||
};
|
||||
|
||||
enum class XrayServerJsonStatus {
|
||||
Ok,
|
||||
MissingInbounds,
|
||||
EmptyInbounds,
|
||||
MissingStreamSettings,
|
||||
MissingSettings,
|
||||
};
|
||||
|
||||
enum class XrayClientListFilter {
|
||||
KeepAll,
|
||||
DropWithoutId,
|
||||
};
|
||||
|
||||
// ── Server config (settings editable by user) ─────────────────────────────────
|
||||
struct XrayServerConfig {
|
||||
QString port;
|
||||
@@ -99,11 +127,13 @@ struct XrayServerConfig {
|
||||
QString site;
|
||||
bool isThirdPartyConfig = false;
|
||||
|
||||
int settingsVersion = 0;
|
||||
|
||||
QString security;
|
||||
QString flow;
|
||||
QString fingerprint;
|
||||
QString sni;
|
||||
QString alpn;
|
||||
QString fingerprint = protocols::xray::defaultFingerprint;
|
||||
|
||||
QString transport;
|
||||
XrayXhttpConfig xhttp;
|
||||
@@ -113,7 +143,65 @@ struct XrayServerConfig {
|
||||
|
||||
static XrayServerConfig fromJson(const QJsonObject &json);
|
||||
|
||||
void applyDefaults(bool fillFlowDefault = false);
|
||||
|
||||
QJsonObject streamSettingsJson(XrayStreamSide side) const;
|
||||
|
||||
QJsonObject serverStreamSettings() const;
|
||||
|
||||
QJsonObject clientStreamSettings() const;
|
||||
|
||||
QJsonObject toServerInboundJson(const XrayServerInboundInputs &inputs) const;
|
||||
|
||||
static XrayServerJsonStatus fromServerInboundJson(const QJsonObject &serverJson, XrayServerConfig &outServerConfig);
|
||||
|
||||
static QJsonArray clientsFromServerInboundJson(const QJsonObject &serverJson);
|
||||
static XrayServerJsonStatus setClientsInServerInboundJson(QJsonObject &serverJson, const QJsonArray &clients);
|
||||
|
||||
static QJsonObject makeClientEntry(const QString &clientId, const QString &flowValue);
|
||||
static QJsonObject applyFlowToClient(const QJsonObject &client, const QString &flowValue);
|
||||
static int indexOfClient(const QJsonArray &clients, const QString &clientId);
|
||||
static QString firstClientId(const QJsonArray &clients);
|
||||
static QJsonArray applyFlowToClients(const QJsonArray &clients, const QString &flowValue,
|
||||
XrayClientListFilter filter = XrayClientListFilter::KeepAll);
|
||||
|
||||
QJsonObject serverView() const;
|
||||
|
||||
QJsonObject issuedConfigView() const;
|
||||
|
||||
bool hasEqualServerSettings(const XrayServerConfig &other) const;
|
||||
|
||||
bool breaksIssuedConfigs(const XrayServerConfig &other) const;
|
||||
};
|
||||
|
||||
namespace xrayEffective
|
||||
{
|
||||
QString xhttpMode(const QString &mode);
|
||||
QString sessionSeqPlacement(const QString &placement);
|
||||
QString uplinkDataPlacement(const QString &placement);
|
||||
QString xPaddingPlacement(const QString &placement);
|
||||
QString xPaddingMethod(const QString &method);
|
||||
QString range(const QString &minV, const QString &maxV);
|
||||
void putRangeIfAny(QJsonObject &obj, const char *key, QString minV, QString maxV, const char *fallbackMin,
|
||||
const char *fallbackMax);
|
||||
|
||||
QString security(const XrayServerConfig &srv);
|
||||
QString clientFlow(const XrayServerConfig &srv);
|
||||
QString serverFlow(const XrayServerConfig &srv);
|
||||
QString network(const XrayServerConfig &srv);
|
||||
QString xhttpModeSent(const XrayServerConfig &srv);
|
||||
}
|
||||
|
||||
QString sanitizeNativeConfig(const QString &nativeConfig);
|
||||
|
||||
struct XrayClientOutboundInputs {
|
||||
QString serverAddress;
|
||||
QString clientId;
|
||||
QString realityPublicKey;
|
||||
QString realityShortId;
|
||||
QString tlsPinnedPeerCertSha256;
|
||||
|
||||
static std::optional<XrayClientOutboundInputs> fromClientNativeJson(const QJsonObject &nativeJson);
|
||||
};
|
||||
|
||||
// ── Client config (generated, not edited by user) ─────────────────────────────
|
||||
@@ -124,6 +212,9 @@ struct XrayClientConfig {
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static XrayClientConfig fromJson(const QJsonObject &json);
|
||||
|
||||
static QString idFromNativeJson(const QJsonObject &nativeJson);
|
||||
static QString localPortFromNativeJson(const QJsonObject &nativeJson);
|
||||
};
|
||||
|
||||
// ── Top-level protocol config ──────────────────────────────────────────────────
|
||||
@@ -140,7 +231,14 @@ struct XrayProtocolConfig {
|
||||
|
||||
bool needsClientHydration = false;
|
||||
|
||||
QJsonObject toClientOutboundJson(const XrayClientOutboundInputs &inputs) const;
|
||||
|
||||
bool fromClientOutboundJson(const QJsonObject &nativeJson);
|
||||
|
||||
bool hydrateServerConfigFromClientNative();
|
||||
|
||||
bool regenerateClientConfigFromNative();
|
||||
|
||||
};
|
||||
|
||||
} // namespace amnezia
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/protocols/protocolUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/serverConfigUtils.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
|
||||
@@ -51,6 +52,7 @@ QPair<QString, QString> NativeServerConfig::getDnsPair(const QString &primaryDns
|
||||
QJsonObject NativeServerConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
obj[configKey::formatVersion] = serverConfigUtils::currentConfigFormatVersion;
|
||||
|
||||
if (!description.isEmpty()) {
|
||||
obj[configKey::description] = this->description;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
#include "core/utils/serverConfigUtils.h"
|
||||
|
||||
namespace amnezia
|
||||
{
|
||||
@@ -83,6 +84,7 @@ QPair<QString, QString> SelfHostedAdminServerConfig::getDnsPair(bool isAmneziaDn
|
||||
QJsonObject SelfHostedAdminServerConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
obj[configKey::formatVersion] = serverConfigUtils::currentConfigFormatVersion;
|
||||
|
||||
if (!description.isEmpty()) {
|
||||
obj[configKey::description] = this->description;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "core/protocols/protocolUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/serverConfigUtils.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
@@ -66,6 +67,7 @@ QPair<QString, QString> SelfHostedUserServerConfig::getDnsPair(const QString &pr
|
||||
QJsonObject SelfHostedUserServerConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
obj[configKey::formatVersion] = serverConfigUtils::currentConfigFormatVersion;
|
||||
|
||||
if (!description.isEmpty()) {
|
||||
obj[configKey::description] = this->description;
|
||||
|
||||
@@ -161,12 +161,15 @@ ServerDescription buildServerDescription(const ApiV2ServerConfig &server, bool /
|
||||
row.hasWriteAccess = false;
|
||||
|
||||
row.serverName = server.displayName;
|
||||
row.baseDescription = server.apiConfig.serverCountryCode.isEmpty() ? server.description : server.apiConfig.serverCountryName;
|
||||
const bool showServerCountry = !server.isFree() && !server.apiConfig.serverCountryCode.isEmpty();
|
||||
row.baseDescription = showServerCountry ? server.apiConfig.serverCountryName : server.description;
|
||||
|
||||
row.isCountrySelectionAvailable = !server.apiConfig.availableCountries.isEmpty();
|
||||
row.isCountrySelectionAvailable = !server.isFree() && !server.apiConfig.availableCountries.isEmpty();
|
||||
row.apiAvailableCountries = server.apiConfig.availableCountries;
|
||||
row.apiServerCountryCode = server.apiConfig.serverCountryCode;
|
||||
row.apiServerCountryCodeL10n = server.apiConfig.serverCountryCodeL10n;
|
||||
if (showServerCountry) {
|
||||
row.apiServerCountryCode = server.apiConfig.serverCountryCode;
|
||||
row.apiServerCountryCodeL10n = server.apiConfig.serverCountryCodeL10n;
|
||||
}
|
||||
|
||||
row.isAdVisible = server.apiConfig.serviceInfo.isAdVisible;
|
||||
row.adHeader = server.apiConfig.serviceInfo.adHeader;
|
||||
|
||||
@@ -61,6 +61,10 @@ void OpenVpnProtocol::cleanupResources()
|
||||
|
||||
void OpenVpnProtocol::stop()
|
||||
{
|
||||
if (guardStop()) {
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "OpenVpnProtocol::stop()";
|
||||
|
||||
const bool wasActive = m_connectionState == Vpn::ConnectionState::Preparing
|
||||
@@ -179,6 +183,8 @@ void OpenVpnProtocol::updateRouteGateway(QString line)
|
||||
|
||||
ErrorCode OpenVpnProtocol::start()
|
||||
{
|
||||
resetStopGuard();
|
||||
|
||||
cleanupResources();
|
||||
|
||||
if (!QFileInfo::exists(configPath())) {
|
||||
|
||||
@@ -72,6 +72,7 @@ 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") },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -99,6 +100,7 @@ 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;
|
||||
}
|
||||
}
|
||||
@@ -114,6 +116,7 @@ int ProtocolUtils::getPortForInstall(Proto p)
|
||||
case MtProxy:
|
||||
case Telemt:
|
||||
case TProxy:
|
||||
case WhatsAppProxy:
|
||||
default:
|
||||
return defaultPort(p);
|
||||
}
|
||||
@@ -136,6 +139,7 @@ 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;
|
||||
}
|
||||
}
|
||||
@@ -157,6 +161,7 @@ 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;
|
||||
}
|
||||
}
|
||||
@@ -180,6 +185,7 @@ 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;
|
||||
}
|
||||
}
|
||||
@@ -202,6 +208,7 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,20 @@ VpnProtocol::VpnProtocol(const QJsonObject &configuration, QObject *parent)
|
||||
connect(m_timeoutTimer, &QTimer::timeout, this, &VpnProtocol::onTimeout);
|
||||
}
|
||||
|
||||
bool VpnProtocol::guardStop()
|
||||
{
|
||||
if (m_stopped) {
|
||||
return true;
|
||||
}
|
||||
m_stopped = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
void VpnProtocol::resetStopGuard()
|
||||
{
|
||||
m_stopped = false;
|
||||
}
|
||||
|
||||
void VpnProtocol::setLastError(ErrorCode lastError)
|
||||
{
|
||||
m_lastError = lastError;
|
||||
|
||||
@@ -89,6 +89,11 @@ protected:
|
||||
void startTimeoutTimer();
|
||||
void stopTimeoutTimer();
|
||||
|
||||
bool guardStop();
|
||||
void resetStopGuard();
|
||||
|
||||
bool m_stopped = false;
|
||||
|
||||
Vpn::ConnectionState m_connectionState;
|
||||
|
||||
QString m_routeGateway;
|
||||
|
||||
@@ -50,8 +50,10 @@ WireguardProtocol::~WireguardProtocol()
|
||||
|
||||
void WireguardProtocol::stop()
|
||||
{
|
||||
if (guardStop()) {
|
||||
return;
|
||||
}
|
||||
stopMzImpl();
|
||||
return;
|
||||
}
|
||||
|
||||
ErrorCode WireguardProtocol::startMzImpl()
|
||||
@@ -75,5 +77,6 @@ ErrorCode WireguardProtocol::stopMzImpl()
|
||||
|
||||
ErrorCode WireguardProtocol::start()
|
||||
{
|
||||
resetStopGuard();
|
||||
return startMzImpl();
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "xrayProtocol.h"
|
||||
|
||||
#include "core/models/protocols/xrayProtocolConfig.h"
|
||||
#include "core/protocols/protocolUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
@@ -66,6 +67,8 @@ XrayProtocol::~XrayProtocol()
|
||||
|
||||
ErrorCode XrayProtocol::start()
|
||||
{
|
||||
resetStopGuard();
|
||||
|
||||
qDebug() << "XrayProtocol::start()";
|
||||
|
||||
// Inject SOCKS5 auth into the inbound before starting xray.
|
||||
@@ -87,30 +90,16 @@ ErrorCode XrayProtocol::start()
|
||||
return ErrorCode::XrayExecutableCrashed;
|
||||
}
|
||||
|
||||
// Fix fingerprint: old configs may contain "Mozilla/5.0" which xray-core rejects.
|
||||
// Replace with the correct default at runtime so stale stored configs still work.
|
||||
if (xrayConfigStr.contains("Mozilla/5.0", Qt::CaseInsensitive)) {
|
||||
xrayConfigStr.replace("Mozilla/5.0", amnezia::protocols::xray::defaultFingerprint,
|
||||
Qt::CaseInsensitive);
|
||||
qDebug() << "XrayProtocol: patched legacy fingerprint to"
|
||||
<< amnezia::protocols::xray::defaultFingerprint;
|
||||
}
|
||||
|
||||
// Fix inbound listen address: old configs may use "10.33.0.2" which doesn't exist
|
||||
// until TUN is created. xray must listen on 127.0.0.1 so tun2socks can connect.
|
||||
if (xrayConfigStr.contains(amnezia::protocols::xray::defaultLocalAddr)) {
|
||||
xrayConfigStr.replace(amnezia::protocols::xray::defaultLocalAddr,
|
||||
amnezia::protocols::xray::defaultLocalListenAddr);
|
||||
qDebug() << "XrayProtocol: patched legacy inbound listen address to 127.0.0.1";
|
||||
}
|
||||
xrayConfigStr = amnezia::sanitizeNativeConfig(xrayConfigStr);
|
||||
|
||||
return IpcClient::withInterface(
|
||||
[&](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
auto xrayStart = iface->xrayStart(xrayConfigStr);
|
||||
if (!xrayStart.waitForFinished() || !xrayStart.returnValue()) {
|
||||
auto xrayStart = iface->xrayStartOwned(xrayConfigStr);
|
||||
if (!xrayStart.waitForFinished() || xrayStart.returnValue() == 0) {
|
||||
qCritical() << "Failed to start xray";
|
||||
return ErrorCode::XrayExecutableCrashed;
|
||||
}
|
||||
m_xrayToken = xrayStart.returnValue();
|
||||
return startTun2Socks();
|
||||
},
|
||||
[]() { return ErrorCode::AmneziaServiceConnectionFailed; });
|
||||
@@ -118,9 +107,13 @@ ErrorCode XrayProtocol::start()
|
||||
|
||||
void XrayProtocol::stop()
|
||||
{
|
||||
if (guardStop()) {
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "XrayProtocol::stop()";
|
||||
|
||||
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
IpcClient::withInterface([this](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
auto disableKillSwitch = iface->disableKillSwitch();
|
||||
if (!disableKillSwitch.waitForFinished() || !disableKillSwitch.returnValue())
|
||||
qWarning() << "Failed to disable killswitch";
|
||||
@@ -137,9 +130,10 @@ void XrayProtocol::stop()
|
||||
if (!deleteTun.waitForFinished() || !deleteTun.returnValue())
|
||||
qWarning() << "Failed to delete tun";
|
||||
|
||||
auto xrayStop = iface->xrayStop();
|
||||
auto xrayStop = iface->xrayStopOwned(m_xrayToken);
|
||||
if (!xrayStop.waitForFinished() || !xrayStop.returnValue())
|
||||
qWarning() << "Failed to stop xray";
|
||||
m_xrayToken = 0;
|
||||
});
|
||||
|
||||
if (m_tun2socksProcess) {
|
||||
|
||||
@@ -38,6 +38,10 @@ private:
|
||||
int m_tun2socksRetryCount = 0;
|
||||
static constexpr int maxTun2SocksRetries = 5;
|
||||
static constexpr int tun2socksRetryDelayMs = 400;
|
||||
|
||||
// Owner token of the shared xray engine; a late stop with a stale token
|
||||
// won't kill an engine restarted by another consumer (e.g. the local proxy).
|
||||
qint64 m_xrayToken = 0;
|
||||
};
|
||||
|
||||
#endif // XRAYPROTOCOL_H
|
||||
@@ -375,6 +375,69 @@ void SecureAppSettingsRepository::setScreenshotsEnabled(bool enabled)
|
||||
emit screenshotsEnabledChanged(enabled);
|
||||
}
|
||||
|
||||
int SecureAppSettingsRepository::countryListSortMode(const QString &listId) const
|
||||
{
|
||||
return value("Conf/countryListSortMode/" + listId, 0).toInt();
|
||||
}
|
||||
|
||||
void SecureAppSettingsRepository::setCountryListSortMode(const QString &listId, int mode)
|
||||
{
|
||||
setValue("Conf/countryListSortMode/" + listId, mode);
|
||||
}
|
||||
|
||||
QStringList SecureAppSettingsRepository::favoriteLocations(const QString &serverId) const
|
||||
{
|
||||
QStringList codes = value("Conf/favoriteLocations").toMap().value(serverId).toStringList();
|
||||
codes.removeAll(QString());
|
||||
codes.removeDuplicates();
|
||||
codes.sort();
|
||||
return codes;
|
||||
}
|
||||
|
||||
void SecureAppSettingsRepository::setFavoriteLocations(const QString &serverId, const QStringList &codes)
|
||||
{
|
||||
if (serverId.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
QVariantMap favorites = value("Conf/favoriteLocations").toMap();
|
||||
if (codes.isEmpty()) {
|
||||
favorites.remove(serverId);
|
||||
} else {
|
||||
favorites.insert(serverId, codes);
|
||||
}
|
||||
setValue("Conf/favoriteLocations", favorites);
|
||||
}
|
||||
|
||||
void SecureAppSettingsRepository::removeFavoriteLocations(const QString &serverId)
|
||||
{
|
||||
QVariantMap favorites = value("Conf/favoriteLocations").toMap();
|
||||
if (favorites.remove(serverId) > 0) {
|
||||
setValue("Conf/favoriteLocations", favorites);
|
||||
}
|
||||
}
|
||||
|
||||
void SecureAppSettingsRepository::migrateLegacyFavoriteLocations(const QStringList &serverIds)
|
||||
{
|
||||
// Before per-subscription favorites the key held one list shared by all servers
|
||||
const QVariant stored = value("Conf/favoriteLocations");
|
||||
const int type = stored.typeId();
|
||||
if (type != QMetaType::QStringList && type != QMetaType::QVariantList && type != QMetaType::QString) {
|
||||
return;
|
||||
}
|
||||
|
||||
QStringList codes = stored.toStringList();
|
||||
codes.removeAll(QString());
|
||||
codes.removeDuplicates();
|
||||
|
||||
QVariantMap favorites;
|
||||
if (!codes.isEmpty()) {
|
||||
for (const QString &serverId : serverIds) {
|
||||
favorites.insert(serverId, codes);
|
||||
}
|
||||
}
|
||||
setValue("Conf/favoriteLocations", favorites);
|
||||
}
|
||||
|
||||
bool SecureAppSettingsRepository::isNewsNotifications() const
|
||||
{
|
||||
return value("Conf/newsNotifications", true).toBool();
|
||||
@@ -484,3 +547,33 @@ void SecureAppSettingsRepository::setXraySavedConfigs(const QByteArray &data)
|
||||
{
|
||||
setValue("Xray/savedConfigs", data);
|
||||
}
|
||||
|
||||
QString SecureAppSettingsRepository::localProxyOwnerId() const
|
||||
{
|
||||
return value("Conf/localProxyOwnerId", "").toString();
|
||||
}
|
||||
|
||||
void SecureAppSettingsRepository::setLocalProxyOwnerId(const QString &serverId)
|
||||
{
|
||||
setValue("Conf/localProxyOwnerId", serverId);
|
||||
}
|
||||
|
||||
quint16 SecureAppSettingsRepository::localProxyPort() const
|
||||
{
|
||||
return static_cast<quint16>(value("Conf/localProxyPort", 10808).toUInt());
|
||||
}
|
||||
|
||||
void SecureAppSettingsRepository::setLocalProxyPort(quint16 port)
|
||||
{
|
||||
setValue("Conf/localProxyPort", port);
|
||||
}
|
||||
|
||||
bool SecureAppSettingsRepository::isLocalProxyPortUserDefined() const
|
||||
{
|
||||
return value("Conf/localProxyPortUserDefined", false).toBool();
|
||||
}
|
||||
|
||||
void SecureAppSettingsRepository::setLocalProxyPortUserDefined(bool userDefined)
|
||||
{
|
||||
setValue("Conf/localProxyPortUserDefined", userDefined);
|
||||
}
|
||||
@@ -77,6 +77,13 @@ public:
|
||||
void setStartMinimized(bool enabled);
|
||||
bool isScreenshotsEnabled() const;
|
||||
void setScreenshotsEnabled(bool enabled);
|
||||
|
||||
int countryListSortMode(const QString &listId) const;
|
||||
void setCountryListSortMode(const QString &listId, int mode);
|
||||
QStringList favoriteLocations(const QString &serverId) const;
|
||||
void setFavoriteLocations(const QString &serverId, const QStringList &codes);
|
||||
void removeFavoriteLocations(const QString &serverId);
|
||||
void migrateLegacyFavoriteLocations(const QStringList &serverIds);
|
||||
bool isNewsNotifications() const;
|
||||
void setNewsNotifications(bool enabled);
|
||||
|
||||
@@ -100,6 +107,13 @@ public:
|
||||
QByteArray xraySavedConfigs() const;
|
||||
void setXraySavedConfigs(const QByteArray &data);
|
||||
|
||||
QString localProxyOwnerId() const;
|
||||
void setLocalProxyOwnerId(const QString &serverId);
|
||||
quint16 localProxyPort() const;
|
||||
void setLocalProxyPort(quint16 port);
|
||||
bool isLocalProxyPortUserDefined() const;
|
||||
void setLocalProxyPortUserDefined(bool userDefined);
|
||||
|
||||
signals:
|
||||
void appLanguageChanged(QLocale locale);
|
||||
void allowedDnsServersChanged(const QStringList &servers);
|
||||
@@ -126,4 +140,3 @@ private:
|
||||
};
|
||||
|
||||
#endif // SECUREAPPSETTINGSREPOSITORY_H
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <QJsonValue>
|
||||
#include <QSet>
|
||||
#include <QUuid>
|
||||
#include <QDebug>
|
||||
|
||||
#include "core/utils/serverConfigUtils.h"
|
||||
#include "core/utils/constants/apiKeys.h"
|
||||
@@ -142,23 +143,43 @@ void SecureServersRepository::persistDefaultServerFields()
|
||||
void SecureServersRepository::loadFromStorage()
|
||||
{
|
||||
clearServerStateMaps();
|
||||
m_unsupportedFormatConfigsCount = 0;
|
||||
|
||||
const QJsonArray serversArray =
|
||||
QJsonDocument::fromJson(value(QStringLiteral("Servers/serversList"), QByteArray()).toByteArray())
|
||||
.array();
|
||||
|
||||
QJsonArray persistedArray;
|
||||
bool hasGeneratedIds = false;
|
||||
|
||||
for (int i = 0; i < serversArray.size(); ++i) {
|
||||
const QJsonObject json = serversArray.at(i).toObject();
|
||||
const QString candidateId = readStorageServerId(json);
|
||||
const QString serverId = normalizedOrGeneratedServerId(candidateId);
|
||||
const QJsonObject strippedJson = withoutStorageServerId(json);
|
||||
if (!serverConfigUtils::isConfigFormatVersionSupported(strippedJson)) {
|
||||
qWarning() << "Skipping stored server config with unsupported format version"
|
||||
<< serverConfigUtils::configFormatVersion(strippedJson);
|
||||
++m_unsupportedFormatConfigsCount;
|
||||
persistedArray.append(json);
|
||||
continue;
|
||||
}
|
||||
const serverConfigUtils::ConfigType kind = serverConfigUtils::configTypeFromJson(strippedJson);
|
||||
|
||||
if (m_serverJsonById.contains(serverId) || kind == serverConfigUtils::ConfigType::Invalid) {
|
||||
persistedArray.append(json);
|
||||
continue;
|
||||
}
|
||||
m_serverJsonById.insert(serverId, embedStorageServerId(serverId, strippedJson));
|
||||
m_orderedServerIds.append(serverId);
|
||||
persistedArray.append(m_serverJsonById.value(serverId));
|
||||
hasGeneratedIds = hasGeneratedIds || serverId != candidateId;
|
||||
}
|
||||
|
||||
// Per-server settings are keyed by id, so a generated one must survive restarts.
|
||||
// Unlike syncToStorage(), this keeps entries that were skipped above
|
||||
if (hasGeneratedIds) {
|
||||
setValue(QStringLiteral("Servers/serversList"), QJsonDocument(persistedArray).toJson());
|
||||
}
|
||||
|
||||
updateDefaultServerFromStorage();
|
||||
@@ -184,6 +205,11 @@ void SecureServersRepository::invalidateCache()
|
||||
loadFromStorage();
|
||||
}
|
||||
|
||||
int SecureServersRepository::unsupportedFormatConfigsCount() const
|
||||
{
|
||||
return m_unsupportedFormatConfigsCount;
|
||||
}
|
||||
|
||||
void SecureServersRepository::clearServers()
|
||||
{
|
||||
clearServerStateMaps();
|
||||
@@ -222,7 +248,8 @@ QString SecureServersRepository::addServer(const QString &serverId, const QJsonO
|
||||
return id;
|
||||
}
|
||||
const QJsonObject strippedJson = withoutStorageServerId(serverJson);
|
||||
if (serverConfigUtils::configTypeFromJson(strippedJson) != kind) {
|
||||
if (!serverConfigUtils::isConfigFormatVersionSupported(strippedJson)
|
||||
|| serverConfigUtils::configTypeFromJson(strippedJson) != kind) {
|
||||
return id;
|
||||
}
|
||||
m_serverJsonById.insert(id, embedStorageServerId(id, strippedJson));
|
||||
@@ -309,6 +336,15 @@ serverConfigUtils::ConfigType SecureServersRepository::serverKind(const QString
|
||||
return serverConfigUtils::configTypeFromJson(withoutStorageServerId(it.value()));
|
||||
}
|
||||
|
||||
std::optional<QJsonObject> SecureServersRepository::serverJsonById(const QString &serverId) const
|
||||
{
|
||||
const auto it = m_serverJsonById.constFind(serverId);
|
||||
if (it == m_serverJsonById.constEnd()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return it.value();
|
||||
}
|
||||
|
||||
std::optional<SelfHostedAdminServerConfig> SecureServersRepository::selfHostedAdminConfig(const QString &serverId) const
|
||||
{
|
||||
const auto it = m_serverJsonById.constFind(serverId);
|
||||
|
||||
@@ -31,6 +31,8 @@ public:
|
||||
void removeServer(const QString &serverId);
|
||||
serverConfigUtils::ConfigType serverKind(const QString &serverId) const;
|
||||
|
||||
std::optional<QJsonObject> serverJsonById(const QString &serverId) const;
|
||||
|
||||
std::optional<SelfHostedAdminServerConfig> selfHostedAdminConfig(const QString &serverId) const;
|
||||
std::optional<SelfHostedUserServerConfig> selfHostedUserConfig(const QString &serverId) const;
|
||||
std::optional<NativeServerConfig> nativeConfig(const QString &serverId) const;
|
||||
@@ -52,6 +54,8 @@ public:
|
||||
|
||||
void invalidateCache();
|
||||
|
||||
int unsupportedFormatConfigsCount() const;
|
||||
|
||||
signals:
|
||||
void serverAdded(const QString &serverId);
|
||||
void serverEdited(const QString &serverId);
|
||||
@@ -77,6 +81,7 @@ private:
|
||||
QVector<QString> m_orderedServerIds;
|
||||
|
||||
QString m_defaultServerId;
|
||||
int m_unsupportedFormatConfigsCount = 0;
|
||||
};
|
||||
|
||||
#endif // SECURESERVERSREPOSITORY_H
|
||||
@@ -20,6 +20,9 @@ namespace
|
||||
|
||||
constexpr QLatin1String unprocessableSubscriptionMessage("Failed to retrieve subscription information. Is it activated?");
|
||||
constexpr QLatin1String trialAlreadyUsedMessage("trial subscription already used");
|
||||
constexpr QLatin1String refreshCaptchaMessage("refresh_captcha");
|
||||
constexpr QLatin1String invalidCaptchaMessage("invalid_captcha");
|
||||
constexpr QLatin1String rateLimitExceededMessage("rate_limit_exceeded");
|
||||
|
||||
QDateTime subscriptionEndUtcFromString(const QString &subscriptionEndDate)
|
||||
{
|
||||
@@ -111,9 +114,7 @@ bool apiUtils::isSubscriptionExpiringSoon(const QString &subscriptionEndDate, in
|
||||
return endDate <= nowUtc.addDays(withinDays);
|
||||
}
|
||||
|
||||
amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &sslErrors, const QString &replyErrorString,
|
||||
const QNetworkReply::NetworkError &replyError, const int httpStatusCode,
|
||||
const QByteArray &responseBody)
|
||||
amnezia::ErrorCode apiUtils::checkApiResponseErrors(const QByteArray &responseBody)
|
||||
{
|
||||
const int httpStatusCodeConflict = 409;
|
||||
const int httpStatusCodeNotFound = 404;
|
||||
@@ -123,76 +124,60 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
|
||||
const int httpStatusCodeRequestTimeout = 408;
|
||||
const int httpStatusCodeUnprocessableEntity = 422;
|
||||
|
||||
if (!sslErrors.empty()) {
|
||||
qDebug().noquote() << sslErrors;
|
||||
return amnezia::ErrorCode::ApiConfigSslError;
|
||||
}
|
||||
if (replyError == QNetworkReply::NetworkError::OperationCanceledError
|
||||
|| replyError == QNetworkReply::NetworkError::TimeoutError) {
|
||||
qDebug() << replyError;
|
||||
return amnezia::ErrorCode::ApiConfigTimeoutError;
|
||||
}
|
||||
if (replyError == QNetworkReply::NetworkError::OperationNotImplementedError) {
|
||||
qDebug() << replyError;
|
||||
return amnezia::ErrorCode::ApiUpdateRequestError;
|
||||
}
|
||||
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
|
||||
if (jsonDoc.isObject()) {
|
||||
QJsonObject jsonObj = jsonDoc.object();
|
||||
const int httpStatusFromBody = jsonObj.value(QStringLiteral("http_status")).toInt(-1);
|
||||
|
||||
if (httpStatusFromBody == httpStatusCodeTooManyRequests) {
|
||||
return amnezia::ErrorCode::ApiRateLimitError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodeConflict) {
|
||||
if (apiErrorMessageFromJson(jsonObj).contains(trialAlreadyUsedMessage, Qt::CaseInsensitive)) {
|
||||
return amnezia::ErrorCode::ApiTrialAlreadyUsedError;
|
||||
}
|
||||
return amnezia::ErrorCode::ApiConfigLimitError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodeNotFound) {
|
||||
return amnezia::ErrorCode::ApiNotFoundError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodeRequestTimeout) {
|
||||
return amnezia::ErrorCode::ApiConfigTimeoutError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodeNotImplemented) {
|
||||
return amnezia::ErrorCode::ApiUpdateRequestError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodeUnprocessableEntity) {
|
||||
if (apiErrorMessageFromJson(jsonObj) == unprocessableSubscriptionMessage) {
|
||||
return amnezia::ErrorCode::ApiSubscriptionExpiredError;
|
||||
}
|
||||
return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodePaymentRequired) {
|
||||
const QString message = apiErrorMessageFromJson(jsonObj);
|
||||
if (message.contains(QLatin1String("refresh_captcha"), Qt::CaseInsensitive)) {
|
||||
return amnezia::ErrorCode::ApiCaptchaRefreshError;
|
||||
}
|
||||
if (message.contains(QLatin1String("invalid_captcha"), Qt::CaseInsensitive)) {
|
||||
return amnezia::ErrorCode::ApiCaptchaInvalidError;
|
||||
}
|
||||
if (jsonObj.contains(QStringLiteral("captcha_id")) || jsonObj.contains(QStringLiteral("captcha_image"))
|
||||
|| message.compare(QLatin1String("rate_limit_exceeded"), Qt::CaseInsensitive) == 0
|
||||
|| message.contains(QLatin1String("rate_limit_exceeded"), Qt::CaseInsensitive)) {
|
||||
return amnezia::ErrorCode::ApiCaptchaRequiredError;
|
||||
}
|
||||
return amnezia::ErrorCode::ApiSubscriptionNotActiveError;
|
||||
}
|
||||
|
||||
if (httpStatusFromBody >= 300) {
|
||||
return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||
}
|
||||
}
|
||||
|
||||
if (replyError == QNetworkReply::NoError) {
|
||||
if (!jsonDoc.isObject()) {
|
||||
return amnezia::ErrorCode::NoError;
|
||||
}
|
||||
|
||||
qDebug() << "something went wrong";
|
||||
return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||
QJsonObject jsonObj = jsonDoc.object();
|
||||
const int httpStatusFromBody = jsonObj.value(apiDefs::key::httpStatus).toInt(-1);
|
||||
if (httpStatusFromBody < 0) {
|
||||
return amnezia::ErrorCode::NoError;
|
||||
}
|
||||
|
||||
if (httpStatusFromBody == httpStatusCodeTooManyRequests) {
|
||||
return amnezia::ErrorCode::ApiRateLimitError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodeConflict) {
|
||||
if (apiErrorMessageFromJson(jsonObj).contains(trialAlreadyUsedMessage, Qt::CaseInsensitive)) {
|
||||
return amnezia::ErrorCode::ApiTrialAlreadyUsedError;
|
||||
}
|
||||
return amnezia::ErrorCode::ApiConfigLimitError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodeNotFound) {
|
||||
return amnezia::ErrorCode::ApiNotFoundError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodeRequestTimeout) {
|
||||
return amnezia::ErrorCode::ApiConfigTimeoutError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodeNotImplemented) {
|
||||
return amnezia::ErrorCode::ApiUpdateRequestError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodeUnprocessableEntity) {
|
||||
if (apiErrorMessageFromJson(jsonObj) == unprocessableSubscriptionMessage) {
|
||||
return amnezia::ErrorCode::ApiSubscriptionExpiredError;
|
||||
}
|
||||
return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodePaymentRequired) {
|
||||
const QString message = apiErrorMessageFromJson(jsonObj);
|
||||
if (message.contains(refreshCaptchaMessage, Qt::CaseInsensitive)) {
|
||||
return amnezia::ErrorCode::ApiCaptchaRefreshError;
|
||||
}
|
||||
if (message.contains(invalidCaptchaMessage, Qt::CaseInsensitive)) {
|
||||
return amnezia::ErrorCode::ApiCaptchaInvalidError;
|
||||
}
|
||||
if (jsonObj.contains(apiDefs::key::captchaId) || jsonObj.contains(apiDefs::key::captchaImage)
|
||||
|| message.contains(rateLimitExceededMessage, Qt::CaseInsensitive)) {
|
||||
return amnezia::ErrorCode::ApiCaptchaRequiredError;
|
||||
}
|
||||
return amnezia::ErrorCode::ApiSubscriptionNotActiveError;
|
||||
}
|
||||
|
||||
if (httpStatusFromBody >= 300) {
|
||||
return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||
}
|
||||
return amnezia::ErrorCode::NoError;
|
||||
}
|
||||
|
||||
bool apiUtils::isPremiumServer(const QJsonObject &serverConfigObject)
|
||||
|
||||
@@ -29,9 +29,7 @@ namespace apiUtils
|
||||
|
||||
bool isPremiumServer(const QJsonObject &serverConfigObject);
|
||||
|
||||
amnezia::ErrorCode checkNetworkReplyErrors(const QList<QSslError> &sslErrors, const QString &replyErrorString,
|
||||
const QNetworkReply::NetworkError &replyError, const int httpStatusCode,
|
||||
const QByteArray &responseBody);
|
||||
amnezia::ErrorCode checkApiResponseErrors(const QByteArray &responseBody);
|
||||
|
||||
QString getPremiumV2VpnKey(const QJsonObject &serverConfigObject);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
#include "countryCatalog.h"
|
||||
|
||||
#include <limits>
|
||||
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "logger.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
Logger logger("CountryCatalog");
|
||||
|
||||
constexpr QLatin1String bundledPath(":/countries/regions.json");
|
||||
|
||||
constexpr QLatin1String keyVersion("version");
|
||||
constexpr QLatin1String keySplitThreshold("splitThreshold");
|
||||
constexpr QLatin1String keyCollapseThreshold("collapseThreshold");
|
||||
constexpr QLatin1String keyRegions("regions");
|
||||
constexpr QLatin1String keySubregions("subregions");
|
||||
constexpr QLatin1String keyCountries("countries");
|
||||
constexpr QLatin1String keyId("id");
|
||||
constexpr QLatin1String keyOrder("order");
|
||||
constexpr QLatin1String keyCode("code");
|
||||
constexpr QLatin1String keyIso("iso");
|
||||
constexpr QLatin1String keyRegion("region");
|
||||
constexpr QLatin1String keySubregion("subregion");
|
||||
constexpr QLatin1String keySubsubregion("subsubregion");
|
||||
constexpr QLatin1String keyNameEn("nameEn");
|
||||
constexpr QLatin1String keyNameRu("nameRu");
|
||||
constexpr QLatin1String keyCity("city");
|
||||
constexpr QLatin1String keyAliases("aliases");
|
||||
constexpr QLatin1String keySplit("split");
|
||||
constexpr QLatin1String keyUseCases("useCases");
|
||||
constexpr QLatin1String keyLocationIds("locationIds");
|
||||
constexpr QLatin1String keyNameLocalized("nameLocalized");
|
||||
|
||||
constexpr QLatin1String splitAlways("always");
|
||||
constexpr QLatin1String splitNever("never");
|
||||
|
||||
countryCatalog::SplitMode parseSplit(const QJsonObject &object)
|
||||
{
|
||||
const QString split = object.value(keySplit).toString();
|
||||
if (split == splitAlways) {
|
||||
return countryCatalog::SplitMode::Always;
|
||||
}
|
||||
if (split == splitNever) {
|
||||
return countryCatalog::SplitMode::Never;
|
||||
}
|
||||
return countryCatalog::SplitMode::Auto;
|
||||
}
|
||||
}
|
||||
|
||||
namespace countryCatalog
|
||||
{
|
||||
|
||||
Catalog Catalog::fromJson(const QByteArray &json)
|
||||
{
|
||||
Catalog catalog;
|
||||
|
||||
QJsonParseError parseError {};
|
||||
const QJsonDocument document = QJsonDocument::fromJson(json, &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !document.isObject()) {
|
||||
logger.error() << "failed to parse the country catalog:" << parseError.errorString();
|
||||
return catalog;
|
||||
}
|
||||
|
||||
const QJsonObject root = document.object();
|
||||
catalog.m_version = root.value(keyVersion).toInt();
|
||||
if (root.contains(keySplitThreshold)) {
|
||||
catalog.m_splitThreshold = root.value(keySplitThreshold).toInt(catalog.m_splitThreshold);
|
||||
}
|
||||
if (root.contains(keyCollapseThreshold)) {
|
||||
catalog.m_collapseThreshold = root.value(keyCollapseThreshold).toInt(catalog.m_collapseThreshold);
|
||||
}
|
||||
|
||||
const QJsonArray regions = root.value(keyRegions).toArray();
|
||||
for (const QJsonValue &value : regions) {
|
||||
const QJsonObject regionObject = value.toObject();
|
||||
|
||||
Region region;
|
||||
region.id = regionObject.value(keyId).toString();
|
||||
if (region.id.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
region.order = regionObject.value(keyOrder).toInt();
|
||||
region.split = parseSplit(regionObject);
|
||||
|
||||
const QJsonArray subregions = regionObject.value(keySubregions).toArray();
|
||||
for (const QJsonValue &subregionValue : subregions) {
|
||||
const QJsonObject subregionObject = subregionValue.toObject();
|
||||
|
||||
Subregion subregion;
|
||||
subregion.id = subregionObject.value(keyId).toString();
|
||||
if (subregion.id.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
subregion.order = subregionObject.value(keyOrder).toInt();
|
||||
subregion.split = parseSplit(subregionObject);
|
||||
|
||||
const QJsonArray subsubregions = subregionObject.value(keySubregions).toArray();
|
||||
for (const QJsonValue &subsubregionValue : subsubregions) {
|
||||
const QJsonObject subsubregionObject = subsubregionValue.toObject();
|
||||
|
||||
Subsubregion subsubregion;
|
||||
subsubregion.id = subsubregionObject.value(keyId).toString();
|
||||
if (subsubregion.id.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
subsubregion.order = subsubregionObject.value(keyOrder).toInt();
|
||||
subregion.subsubregions.push_back(subsubregion);
|
||||
}
|
||||
|
||||
region.subregions.push_back(subregion);
|
||||
}
|
||||
|
||||
catalog.m_regions.push_back(region);
|
||||
}
|
||||
|
||||
const QJsonArray countries = root.value(keyCountries).toArray();
|
||||
for (const QJsonValue &value : countries) {
|
||||
const QJsonObject countryObject = value.toObject();
|
||||
|
||||
Entry entry;
|
||||
entry.code = countryObject.value(keyCode).toString();
|
||||
if (entry.code.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
entry.isoCode = countryObject.value(keyIso).toString(entry.code).toUpper();
|
||||
entry.regionId = countryObject.value(keyRegion).toString();
|
||||
entry.subregionId = countryObject.value(keySubregion).toString();
|
||||
entry.subsubregionId = countryObject.value(keySubsubregion).toString();
|
||||
entry.nameEn = countryObject.value(keyNameEn).toString();
|
||||
entry.nameRu = countryObject.value(keyNameRu).toString();
|
||||
entry.city = countryObject.value(keyCity).toString();
|
||||
|
||||
const QJsonArray aliases = countryObject.value(keyAliases).toArray();
|
||||
for (const QJsonValue &aliasValue : aliases) {
|
||||
const QString alias = aliasValue.toString();
|
||||
if (!alias.isEmpty()) {
|
||||
entry.aliases.push_back(alias);
|
||||
}
|
||||
}
|
||||
|
||||
catalog.m_byCode.insert(entry.code.toUpper(), entry);
|
||||
if (!catalog.m_byIso.contains(entry.isoCode)) {
|
||||
catalog.m_byIso.insert(entry.isoCode, entry);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const QJsonArray useCases = root.value(keyUseCases).toArray();
|
||||
for (const QJsonValue &value : useCases) {
|
||||
const QJsonObject useCaseObject = value.toObject();
|
||||
|
||||
UseCase useCase;
|
||||
useCase.id = useCaseObject.value(keyId).toString();
|
||||
if (useCase.id.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
useCase.order = useCaseObject.value(keyOrder).toInt();
|
||||
|
||||
const QJsonObject names = useCaseObject.value(keyNameLocalized).toObject();
|
||||
for (auto it = names.constBegin(); it != names.constEnd(); ++it) {
|
||||
const QString name = it.value().toString().trimmed();
|
||||
if (!name.isEmpty()) {
|
||||
useCase.nameLocalized.insert(it.key(), name);
|
||||
}
|
||||
}
|
||||
|
||||
const QJsonArray locationIds = useCaseObject.value(keyLocationIds).toArray();
|
||||
for (const QJsonValue &locationId : locationIds) {
|
||||
const QString id = locationId.toString();
|
||||
if (!id.isEmpty()) {
|
||||
useCase.locationIds.push_back(id);
|
||||
}
|
||||
}
|
||||
catalog.m_useCases.push_back(useCase);
|
||||
}
|
||||
|
||||
logger.info() << "loaded" << catalog.m_regions.size() << "regions and"
|
||||
<< catalog.m_byCode.size() << "locations, catalog version"
|
||||
<< catalog.m_version;
|
||||
|
||||
return catalog;
|
||||
}
|
||||
|
||||
Catalog Catalog::bundled()
|
||||
{
|
||||
QFile file(bundledPath);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
logger.error() << "the bundled country catalog is missing at" << bundledPath;
|
||||
return {};
|
||||
}
|
||||
return fromJson(file.readAll());
|
||||
}
|
||||
|
||||
bool Catalog::isEmpty() const
|
||||
{
|
||||
return m_regions.isEmpty() || m_byCode.isEmpty();
|
||||
}
|
||||
|
||||
int Catalog::splitThreshold() const
|
||||
{
|
||||
return m_splitThreshold;
|
||||
}
|
||||
|
||||
int Catalog::collapseThreshold() const
|
||||
{
|
||||
return m_collapseThreshold;
|
||||
}
|
||||
|
||||
bool Catalog::splitByCount(int visibleCount, bool wasSplit) const
|
||||
{
|
||||
if (visibleCount > m_splitThreshold) {
|
||||
return true;
|
||||
}
|
||||
if (visibleCount < m_collapseThreshold) {
|
||||
return false;
|
||||
}
|
||||
return wasSplit;
|
||||
}
|
||||
|
||||
int Catalog::version() const
|
||||
{
|
||||
return m_version;
|
||||
}
|
||||
|
||||
const QVector<Region> &Catalog::regions() const
|
||||
{
|
||||
return m_regions;
|
||||
}
|
||||
|
||||
const QVector<UseCase> &Catalog::useCases() const
|
||||
{
|
||||
return m_useCases;
|
||||
}
|
||||
|
||||
const Entry *Catalog::find(const QString &countryCode, const QString &isoCode) const
|
||||
{
|
||||
const auto byCode = m_byCode.constFind(countryCode.toUpper());
|
||||
if (byCode != m_byCode.constEnd()) {
|
||||
return &byCode.value();
|
||||
}
|
||||
|
||||
const auto byIso = m_byIso.constFind(isoCode.toUpper());
|
||||
if (byIso != m_byIso.constEnd()) {
|
||||
return &byIso.value();
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
|
||||
bool Catalog::hasSubregions(const QString ®ionId) const
|
||||
{
|
||||
for (const Region ®ion : m_regions) {
|
||||
if (region.id == regionId) {
|
||||
return !region.subregions.isEmpty();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Catalog::isSplit(const QString ®ionId, int visibleCount, bool wasSplit) const
|
||||
{
|
||||
for (const Region ®ion : m_regions) {
|
||||
if (region.id != regionId) {
|
||||
continue;
|
||||
}
|
||||
if (region.subregions.isEmpty() || region.split == SplitMode::Never) {
|
||||
return false;
|
||||
}
|
||||
if (region.split == SplitMode::Always) {
|
||||
return true;
|
||||
}
|
||||
return splitByCount(visibleCount, wasSplit);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Catalog::isSplit(const QString ®ionId, const QString &subregionId, int visibleCount, bool wasSplit) const
|
||||
{
|
||||
for (const Region ®ion : m_regions) {
|
||||
if (region.id != regionId) {
|
||||
continue;
|
||||
}
|
||||
for (const Subregion &subregion : region.subregions) {
|
||||
if (subregion.id != subregionId) {
|
||||
continue;
|
||||
}
|
||||
if (subregion.subsubregions.isEmpty() || subregion.split == SplitMode::Never) {
|
||||
return false;
|
||||
}
|
||||
if (subregion.split == SplitMode::Always) {
|
||||
return true;
|
||||
}
|
||||
return splitByCount(visibleCount, wasSplit);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
#ifndef COUNTRYCATALOG_H
|
||||
#define COUNTRYCATALOG_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QHash>
|
||||
#include <QLatin1String>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QVector>
|
||||
|
||||
namespace countryCatalog
|
||||
{
|
||||
constexpr QLatin1String otherRegionId("other");
|
||||
|
||||
struct Entry
|
||||
{
|
||||
QString code;
|
||||
QString isoCode;
|
||||
QString regionId;
|
||||
QString subregionId;
|
||||
QString subsubregionId;
|
||||
QString nameEn;
|
||||
QString nameRu;
|
||||
QString city;
|
||||
QStringList aliases;
|
||||
};
|
||||
|
||||
enum class SplitMode {
|
||||
Auto,
|
||||
Always,
|
||||
Never
|
||||
};
|
||||
|
||||
struct Subsubregion
|
||||
{
|
||||
QString id;
|
||||
int order = 0;
|
||||
};
|
||||
|
||||
struct Subregion
|
||||
{
|
||||
QString id;
|
||||
int order = 0;
|
||||
SplitMode split = SplitMode::Auto;
|
||||
QVector<Subsubregion> subsubregions;
|
||||
};
|
||||
|
||||
struct Region
|
||||
{
|
||||
QString id;
|
||||
int order = 0;
|
||||
SplitMode split = SplitMode::Auto;
|
||||
QVector<Subregion> subregions;
|
||||
};
|
||||
|
||||
struct UseCase
|
||||
{
|
||||
QString id;
|
||||
int order = 0;
|
||||
QHash<QString, QString> nameLocalized;
|
||||
QStringList locationIds;
|
||||
};
|
||||
|
||||
class Catalog
|
||||
{
|
||||
public:
|
||||
Catalog() = default;
|
||||
|
||||
static Catalog fromJson(const QByteArray &json);
|
||||
static Catalog bundled();
|
||||
|
||||
bool isEmpty() const;
|
||||
int splitThreshold() const;
|
||||
int collapseThreshold() const;
|
||||
int version() const;
|
||||
const QVector<Region> ®ions() const;
|
||||
const QVector<UseCase> &useCases() const;
|
||||
|
||||
const Entry *find(const QString &countryCode, const QString &isoCode) const;
|
||||
bool hasSubregions(const QString ®ionId) const;
|
||||
|
||||
bool isSplit(const QString ®ionId, int visibleCount, bool wasSplit) const;
|
||||
bool isSplit(const QString ®ionId, const QString &subregionId, int visibleCount, bool wasSplit) const;
|
||||
|
||||
private:
|
||||
QVector<Region> m_regions;
|
||||
QVector<UseCase> m_useCases;
|
||||
QHash<QString, Entry> m_byCode;
|
||||
QHash<QString, Entry> m_byIso;
|
||||
bool splitByCount(int visibleCount, bool wasSplit) const;
|
||||
|
||||
int m_splitThreshold = 15;
|
||||
int m_collapseThreshold = 13;
|
||||
int m_version = 0;
|
||||
};
|
||||
} // namespace countryCatalog
|
||||
|
||||
#endif // COUNTRYCATALOG_H
|
||||
@@ -11,5 +11,6 @@
|
||||
#define APP_IOS_BUNDLE_ID "@CLIENT_IOS_BUNDLE_ID@"
|
||||
#define APP_IOS_STORE_URL_FALLBACK "@CLIENT_IOS_STORE_URL_FALLBACK@"
|
||||
#define APP_ANDROID_PACKAGE "@CLIENT_ANDROID_PACKAGE@"
|
||||
#define CLIENT_ENABLE_APP_UPDATES @CLIENT_ENABLE_APP_UPDATES@
|
||||
|
||||
#endif
|
||||
@@ -4,7 +4,10 @@
|
||||
namespace apiDefs
|
||||
{
|
||||
|
||||
constexpr int requestTimeoutMsecs = 12 * 1000; // 12 secs
|
||||
constexpr int requestTimeoutMsecs = 6 * 1000;
|
||||
constexpr int proxyRequestTimeoutMsecs = 6 * 1000;
|
||||
constexpr int proxyStorageTimeoutMsecs = 6 * 1000;
|
||||
constexpr int proxyHealthTimeoutMsecs = 1000;
|
||||
|
||||
} // namespace apiDefs
|
||||
|
||||
|
||||
@@ -45,6 +45,8 @@ namespace apiDefs
|
||||
constexpr QLatin1String locale("locale");
|
||||
|
||||
constexpr QLatin1String captchaId("captcha_id");
|
||||
constexpr QLatin1String captchaImage("captcha_image");
|
||||
constexpr QLatin1String httpStatus("http_status");
|
||||
constexpr QLatin1String captchaSolution("captcha_solution");
|
||||
|
||||
constexpr QLatin1String activeDeviceCount("active_device_count");
|
||||
|
||||
@@ -25,6 +25,7 @@ namespace amnezia
|
||||
constexpr QLatin1String accessToken("api_key");
|
||||
constexpr QLatin1String config("config");
|
||||
constexpr QLatin1String configVersion("config_version");
|
||||
constexpr QLatin1String formatVersion("format_version");
|
||||
|
||||
constexpr QLatin1String sendPayload("send_payload");
|
||||
constexpr QLatin1String sendPayloadEndpoint("endpoint");
|
||||
@@ -143,6 +144,7 @@ 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");
|
||||
@@ -177,6 +179,8 @@ namespace amnezia
|
||||
|
||||
// ── Xray-specific keys ────────────────────────────────────────
|
||||
|
||||
constexpr QLatin1String xraySettingsVersion("xray_settings_version");
|
||||
|
||||
// Security
|
||||
constexpr QLatin1String xraySecurity("xray_security"); // none | tls | reality
|
||||
constexpr QLatin1String xrayFlow("xray_flow"); // "" | xtls-rprx-vision | xtls-rprx-vision-udp443
|
||||
@@ -215,11 +219,11 @@ namespace amnezia
|
||||
|
||||
// Transport — mKCP
|
||||
constexpr QLatin1String mkcpTti("mkcp_tti");
|
||||
constexpr QLatin1String mkcpMtu("mkcp_mtu");
|
||||
constexpr QLatin1String mkcpUplinkCapacity("mkcp_uplink_capacity");
|
||||
constexpr QLatin1String mkcpDownlinkCapacity("mkcp_downlink_capacity");
|
||||
constexpr QLatin1String mkcpReadBufferSize("mkcp_read_buffer_size");
|
||||
constexpr QLatin1String mkcpWriteBufferSize("mkcp_write_buffer_size");
|
||||
constexpr QLatin1String mkcpCongestion("mkcp_congestion"); // bool
|
||||
constexpr QLatin1String mkcpCwndMultiplier("mkcp_cwnd_multiplier");
|
||||
constexpr QLatin1String mkcpMaxSendingWindow("mkcp_max_sending_window");
|
||||
|
||||
// xPadding
|
||||
constexpr QLatin1String xPaddingBytesMin("xpadding_bytes_min");
|
||||
|
||||
@@ -48,11 +48,15 @@ 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";
|
||||
@@ -60,14 +64,16 @@ 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[] = "chrome";
|
||||
constexpr char defaultFingerprint[] = "hellochrome_120";
|
||||
constexpr char defaultSni[] = "www.googletagmanager.com";
|
||||
constexpr char defaultAlpn[] = "h2";
|
||||
|
||||
constexpr char defaultXhttpMode[] = "Auto";
|
||||
constexpr char defaultXhttpMode[] = "Stream-one";
|
||||
constexpr char defaultXhttpUplinkMethod[] = "POST";
|
||||
constexpr char defaultXhttpSessionPlacement[] = "Path";
|
||||
constexpr char defaultXhttpSessionKey[] = "";
|
||||
@@ -75,9 +81,10 @@ namespace amnezia
|
||||
constexpr char defaultXhttpUplinkDataPlacement[] = "Body";
|
||||
|
||||
constexpr char defaultXhttpHost[] = "www.googletagmanager.com";
|
||||
constexpr char defaultXhttpPath[] = "/";
|
||||
constexpr char defaultXhttpUplinkChunkSize[] = "0";
|
||||
constexpr char defaultXhttpScMaxEachPostBytesMin[] = "1";
|
||||
constexpr char defaultXhttpScMaxEachPostBytesMax[] = "100";
|
||||
constexpr char defaultXhttpScMaxEachPostBytesMin[] = "1000000";
|
||||
constexpr char defaultXhttpScMaxEachPostBytesMax[] = "1000000";
|
||||
constexpr char defaultXhttpScMinPostsIntervalMsMin[] = "100";
|
||||
constexpr char defaultXhttpScMinPostsIntervalMsMax[] = "800";
|
||||
constexpr char defaultXhttpScStreamUpServerSecsMin[] = "1";
|
||||
@@ -87,19 +94,22 @@ namespace amnezia
|
||||
constexpr char defaultXPaddingMethod[] = "Repeat-x";
|
||||
constexpr char defaultXPaddingKey[] = "x_padding";
|
||||
constexpr char defaultXPaddingHeader[] = "X-Padding";
|
||||
constexpr char defaultXPaddingBytesMin[] = "1";
|
||||
constexpr char defaultXPaddingBytesMax[] = "256";
|
||||
constexpr char defaultXPaddingBytesMin[] = "100";
|
||||
constexpr char defaultXPaddingBytesMax[] = "1000";
|
||||
|
||||
constexpr char defaultMkcpTti[] = "50";
|
||||
constexpr char defaultMkcpUplinkCapacity[] = "5";
|
||||
constexpr char defaultMkcpDownlinkCapacity[] = "20";
|
||||
constexpr char defaultMkcpReadBufferSize[] = "2";
|
||||
constexpr char defaultMkcpWriteBufferSize[] = "2";
|
||||
constexpr char defaultMkcpMtu[] = "1350";
|
||||
constexpr char defaultMkcpCwndMultiplier[] = "1";
|
||||
|
||||
constexpr char outbounds[] = "outbounds";
|
||||
constexpr char inbounds[] = "inbounds";
|
||||
constexpr char settings[] = "settings";
|
||||
constexpr char streamSettings[] = "streamSettings";
|
||||
constexpr char tag[] = "tag";
|
||||
constexpr char sockopt[] = "sockopt";
|
||||
constexpr char dialerProxy[] = "dialerProxy";
|
||||
constexpr char vnext[] = "vnext";
|
||||
constexpr char users[] = "users";
|
||||
constexpr char servers[] = "servers";
|
||||
@@ -120,6 +130,92 @@ namespace amnezia
|
||||
constexpr char spiderX[] = "spiderX";
|
||||
constexpr char user[] = "user";
|
||||
constexpr char pass[] = "pass";
|
||||
|
||||
constexpr char logBlock[] = "log";
|
||||
constexpr char logLevel[] = "loglevel";
|
||||
constexpr char logLevelError[] = "error";
|
||||
constexpr char protocol[] = "protocol";
|
||||
constexpr char protocolVless[] = "vless";
|
||||
constexpr char protocolFreedom[] = "freedom";
|
||||
constexpr char protocolSocks[] = "socks";
|
||||
constexpr char decryption[] = "decryption";
|
||||
constexpr char decryptionNone[] = "none";
|
||||
constexpr char listen[] = "listen";
|
||||
constexpr char udp[] = "udp";
|
||||
|
||||
constexpr char tlsSettings[] = "tlsSettings";
|
||||
constexpr char xhttpSettings[] = "xhttpSettings";
|
||||
constexpr char kcpSettings[] = "kcpSettings";
|
||||
constexpr char alpn[] = "alpn";
|
||||
constexpr char certificates[] = "certificates";
|
||||
constexpr char certificateFile[] = "certificateFile";
|
||||
constexpr char keyFile[] = "keyFile";
|
||||
constexpr char allowInsecure[] = "allowInsecure";
|
||||
constexpr char pinnedPeerCertSha256[] = "pinnedPeerCertSha256";
|
||||
|
||||
constexpr char dest[] = "dest";
|
||||
constexpr char privateKey[] = "privateKey";
|
||||
constexpr char shortIds[] = "shortIds";
|
||||
|
||||
constexpr char xhttpHost[] = "host";
|
||||
constexpr char xhttpPath[] = "path";
|
||||
constexpr char xhttpMode[] = "mode";
|
||||
constexpr char noGrpcHeader[] = "noGRPCHeader";
|
||||
constexpr char noSseHeader[] = "noSSEHeader";
|
||||
constexpr char sessionIdPlacement[] = "sessionIDPlacement";
|
||||
constexpr char sessionIdKey[] = "sessionIDKey";
|
||||
constexpr char seqPlacement[] = "seqPlacement";
|
||||
constexpr char seqKey[] = "seqKey";
|
||||
constexpr char uplinkDataPlacement[] = "uplinkDataPlacement";
|
||||
constexpr char uplinkDataKey[] = "uplinkDataKey";
|
||||
constexpr char uplinkHttpMethod[] = "uplinkHTTPMethod";
|
||||
constexpr char uplinkChunkSize[] = "uplinkChunkSize";
|
||||
constexpr char scMaxBufferedPosts[] = "scMaxBufferedPosts";
|
||||
constexpr char scMaxEachPostBytes[] = "scMaxEachPostBytes";
|
||||
constexpr char scMinPostsIntervalMs[] = "scMinPostsIntervalMs";
|
||||
constexpr char scStreamUpServerSecs[] = "scStreamUpServerSecs";
|
||||
constexpr char xPaddingObfsMode[] = "xPaddingObfsMode";
|
||||
constexpr char xPaddingBytes[] = "xPaddingBytes";
|
||||
constexpr char xPaddingKey[] = "xPaddingKey";
|
||||
constexpr char xPaddingHeader[] = "xPaddingHeader";
|
||||
constexpr char xPaddingPlacement[] = "xPaddingPlacement";
|
||||
constexpr char xPaddingMethod[] = "xPaddingMethod";
|
||||
|
||||
constexpr char xmux[] = "xmux";
|
||||
constexpr char xmuxMaxConcurrency[] = "maxConcurrency";
|
||||
constexpr char xmuxMaxConnections[] = "maxConnections";
|
||||
constexpr char xmuxCMaxReuseTimes[] = "cMaxReuseTimes";
|
||||
constexpr char xmuxHMaxRequestTimes[] = "hMaxRequestTimes";
|
||||
constexpr char xmuxHMaxReusableSecs[] = "hMaxReusableSecs";
|
||||
constexpr char xmuxHKeepAlivePeriod[] = "hKeepAlivePeriod";
|
||||
|
||||
constexpr char kcpTti[] = "tti";
|
||||
constexpr char kcpMtu[] = "mtu";
|
||||
constexpr char kcpUplinkCapacity[] = "uplinkCapacity";
|
||||
constexpr char kcpDownlinkCapacity[] = "downlinkCapacity";
|
||||
constexpr char kcpCwndMultiplier[] = "cwndMultiplier";
|
||||
constexpr char kcpMaxSendingWindow[] = "maxSendingWindow";
|
||||
|
||||
constexpr char legacyXhttpMethod[] = "method";
|
||||
constexpr char legacySessionPlacement[] = "sessionPlacement";
|
||||
constexpr char legacyScSessionPlacement[] = "scSessionPlacement";
|
||||
constexpr char legacyScSeqPlacement[] = "scSeqPlacement";
|
||||
constexpr char legacyScUplinkDataPlacement[] = "scUplinkDataPlacement";
|
||||
constexpr char legacySessionKey[] = "sessionKey";
|
||||
constexpr char legacyUplinkChunkSize[] = "xhttpUplinkChunkSize";
|
||||
constexpr char legacyXPaddingBlock[] = "xPadding";
|
||||
constexpr char legacyRangeFrom[] = "from";
|
||||
constexpr char legacyRangeTo[] = "to";
|
||||
|
||||
constexpr char networkTcp[] = "tcp";
|
||||
constexpr char networkXhttp[] = "xhttp";
|
||||
constexpr char networkKcp[] = "kcp";
|
||||
constexpr char transportRaw[] = "raw";
|
||||
constexpr char transportXhttp[] = "xhttp";
|
||||
constexpr char transportMkcp[] = "mkcp";
|
||||
constexpr char securityNone[] = "none";
|
||||
constexpr char securityTls[] = "tls";
|
||||
constexpr char securityReality[] = "reality";
|
||||
}
|
||||
|
||||
namespace cloak
|
||||
@@ -317,6 +413,16 @@ 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
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ namespace amnezia
|
||||
MtProxy,
|
||||
Telemt,
|
||||
TProxy,
|
||||
WhatsAppProxy,
|
||||
};
|
||||
Q_ENUM_NS(DockerContainer)
|
||||
} // namespace ContainerEnumNS
|
||||
|
||||
@@ -80,6 +80,7 @@ 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") },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -120,6 +121,8 @@ 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") },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -203,6 +206,11 @@ 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.") },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -235,6 +243,7 @@ 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;
|
||||
}
|
||||
}
|
||||
@@ -265,6 +274,7 @@ 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;
|
||||
}
|
||||
@@ -283,6 +293,7 @@ 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;
|
||||
}
|
||||
@@ -304,6 +315,7 @@ 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;
|
||||
}
|
||||
|
||||
@@ -373,6 +385,7 @@ 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;
|
||||
}
|
||||
}
|
||||
@@ -409,6 +422,7 @@ int ContainerUtils::installPageOrder(DockerContainer container)
|
||||
case DockerContainer::MtProxy:
|
||||
case DockerContainer::Telemt:
|
||||
case DockerContainer::TProxy:
|
||||
case DockerContainer::WhatsAppProxy:
|
||||
return 20;
|
||||
default: return 0;
|
||||
}
|
||||
|
||||
@@ -37,9 +37,17 @@ namespace amnezia
|
||||
ServerLinuxKernelTooOld = 214,
|
||||
XrayServerConfigInvalid = 215,
|
||||
XrayServerNoVlessClients = 216,
|
||||
XrayRealityKeysReadFailed = 217,
|
||||
XrayKeysReadFailed = 217,
|
||||
ServerContainerRuntimeNotSupported = 218,
|
||||
ContainerRuntimeServiceNotRunning = 219,
|
||||
XrayKeyMigrationFailed = 220,
|
||||
XrayTlsNotSupported = 221,
|
||||
XrayServerConfigRejected = 222,
|
||||
XrayServerConfigRolledBack = 223,
|
||||
XrayServerNotServing = 224,
|
||||
ServerDataVolumeNotRemoved = 225,
|
||||
XrayKeyGenerationFailed = 226,
|
||||
XrayKeyMigrationNeedsConfirm = 227,
|
||||
|
||||
// Ssh connection errors
|
||||
SshRequestDeniedError = 300,
|
||||
@@ -82,6 +90,9 @@ namespace amnezia
|
||||
RestoreBackupInvalidError = 904,
|
||||
LegacyApiV1NotSupportedError = 905,
|
||||
LegacyContainerNotSupportedError = 906,
|
||||
ConfigFormatVersionNotSupportedError = 907,
|
||||
RestoreBackupUnsupportedConfigsSkipped = 908,
|
||||
ImportXraySettingsFileUseXrayPage = 909,
|
||||
|
||||
// Android errors
|
||||
AndroidError = 1000,
|
||||
|
||||
@@ -36,9 +36,45 @@ QString errorString(ErrorCode code) {
|
||||
case(ErrorCode::XrayServerNoVlessClients):
|
||||
errorMessage = QObject::tr("Server error: XRay server has no VLESS clients");
|
||||
break;
|
||||
case(ErrorCode::XrayRealityKeysReadFailed):
|
||||
case(ErrorCode::XrayKeysReadFailed):
|
||||
errorMessage = QObject::tr("Server error: failed to read XRay Reality keys from the server");
|
||||
break;
|
||||
case(ErrorCode::XrayTlsNotSupported):
|
||||
errorMessage = QObject::tr("Server error: could not create a TLS certificate on the XRay server. "
|
||||
"The previous settings were left unchanged. Try again, or use Reality.");
|
||||
break;
|
||||
case(ErrorCode::XrayServerConfigRejected):
|
||||
errorMessage = QObject::tr("Server error: XRay rejected the new configuration. "
|
||||
"Nothing was changed, the server keeps running with the previous settings.");
|
||||
break;
|
||||
case(ErrorCode::XrayServerConfigRolledBack):
|
||||
errorMessage = QObject::tr("Server error: XRay did not start with the new configuration. "
|
||||
"The previous settings were restored and the server is running again.");
|
||||
break;
|
||||
case(ErrorCode::XrayServerNotServing):
|
||||
errorMessage = QObject::tr("Server error: XRay is not accepting connections and the previous configuration "
|
||||
"could not be restored. The server needs attention: check it over SSH, "
|
||||
"or reinstall the protocol.");
|
||||
break;
|
||||
case(ErrorCode::ServerDataVolumeNotRemoved):
|
||||
errorMessage = QObject::tr("Server error: the previous data volume could not be removed. "
|
||||
"Installing on top of it would reuse the old keys, so the configurations you "
|
||||
"expected to revoke would keep working. Check the server and try again.");
|
||||
break;
|
||||
case(ErrorCode::XrayKeyGenerationFailed):
|
||||
errorMessage = QObject::tr("Server error: XRay could not generate its keys on the server. "
|
||||
"Check the server and try again.");
|
||||
break;
|
||||
case(ErrorCode::XrayKeyMigrationFailed):
|
||||
errorMessage = QObject::tr("Server error: could not preserve the XRay keys stored on the server. "
|
||||
"The settings were not changed, so the existing configurations keep working. "
|
||||
"Check that the server is reachable and try again.");
|
||||
break;
|
||||
case(ErrorCode::XrayKeyMigrationNeedsConfirm):
|
||||
errorMessage = QObject::tr("The old XRay container will not start, so its keys cannot be preserved. "
|
||||
"Applying these settings will recreate it with new keys, and all existing "
|
||||
"configurations will stop working.");
|
||||
break;
|
||||
case(ErrorCode::ServerContainerRuntimeNotSupported): errorMessage = QObject::tr("Server error: The default container runtime available for installation on this server is not supported.\n Install Docker Engine on the server manually and try again."); break;
|
||||
case(ErrorCode::ContainerRuntimeServiceNotRunning): errorMessage = QObject::tr("Container runtime error: The container runtime service is not running.\n Check the container runtime service on the server, or wait about a minute and try again."); break;
|
||||
|
||||
@@ -69,8 +105,11 @@ QString errorString(ErrorCode code) {
|
||||
|
||||
case (ErrorCode::ImportInvalidConfigError): errorMessage = QObject::tr("The config does not contain any containers and credentials for connecting to the server"); break;
|
||||
case (ErrorCode::ImportBackupFileUseRestoreInstead): errorMessage = QObject::tr("Backup files cannot be imported here. Use 'Restore from backup' instead."); break;
|
||||
case (ErrorCode::ImportXraySettingsFileUseXrayPage): errorMessage = QObject::tr("This is an XRay settings file, not a connection config. Import it on the XRay Configurations page using 'Import settings'."); break;
|
||||
case (ErrorCode::RestoreBackupInvalidError): errorMessage = QObject::tr("Backup file is corrupted or has invalid format"); break;
|
||||
case (ErrorCode::LegacyApiV1NotSupportedError): errorMessage = QObject::tr("This legacy Amnezia subscription format is no longer supported"); break;
|
||||
case (ErrorCode::ConfigFormatVersionNotSupportedError): errorMessage = QObject::tr("This configuration was created in a newer version of the application and is not fully supported. Please update the application"); break;
|
||||
case (ErrorCode::RestoreBackupUnsupportedConfigsSkipped): errorMessage = QObject::tr("Some configurations from the backup were not restored because they require a newer version of the application"); break;
|
||||
case (ErrorCode::LegacyContainerNotSupportedError): errorMessage = QObject::tr("This protocol is no longer supported. Please select another protocol or remove this container from the server settings."); break;
|
||||
case (ErrorCode::ImportOpenConfigError): errorMessage = QObject::tr("Unable to open config file"); break;
|
||||
case (ErrorCode::NoInstalledContainersError): errorMessage = QObject::tr("VPN Protocols is not installed.\n Please install VPN container at first"); break;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
#ifndef LOCALPROXYDEFS_H
|
||||
#define LOCALPROXYDEFS_H
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
namespace amnezia::localProxy
|
||||
{
|
||||
constexpr int defaultProxyPort = 10808;
|
||||
constexpr int proxyPortMin = 1024;
|
||||
constexpr int proxyPortMax = 65535;
|
||||
constexpr quint16 apiPort = 49490;
|
||||
constexpr int portReleaseWaitMs = 800;
|
||||
}
|
||||
|
||||
#endif // LOCALPROXYDEFS_H
|
||||
@@ -0,0 +1,51 @@
|
||||
#include "portUtils.h"
|
||||
|
||||
#include "localProxyConstants.h"
|
||||
|
||||
#include <QElapsedTimer>
|
||||
#include <QHostAddress>
|
||||
#include <QTcpServer>
|
||||
#include <QThread>
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
bool portUtils::isPortAvailable(int port)
|
||||
{
|
||||
if (port < localProxy::proxyPortMin || port > localProxy::proxyPortMax) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QTcpServer server;
|
||||
const bool success = server.listen(QHostAddress::LocalHost, static_cast<quint16>(port));
|
||||
server.close();
|
||||
return success;
|
||||
}
|
||||
|
||||
bool portUtils::waitForPort(int port, int timeoutMs, const PortCheck &isAvailable)
|
||||
{
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
|
||||
while (!isAvailable(port)) {
|
||||
if (timer.elapsed() >= timeoutMs) {
|
||||
return false;
|
||||
}
|
||||
QThread::msleep(200);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<int> portUtils::findFirstAvailablePort(int startPort, int endPort, const PortCheck &isAvailable)
|
||||
{
|
||||
startPort = qMax(startPort, localProxy::proxyPortMin);
|
||||
endPort = qMin(endPort, localProxy::proxyPortMax);
|
||||
|
||||
for (int port = startPort; port <= endPort; ++port) {
|
||||
if (isAvailable(port)) {
|
||||
return port;
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#ifndef PORTUTILS_H
|
||||
#define PORTUTILS_H
|
||||
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
|
||||
namespace portUtils
|
||||
{
|
||||
using PortCheck = std::function<bool(int)>;
|
||||
|
||||
bool isPortAvailable(int port);
|
||||
bool waitForPort(int port, int timeoutMs, const PortCheck &isAvailable = isPortAvailable);
|
||||
std::optional<int> findFirstAvailablePort(int startPort, int endPort, const PortCheck &isAvailable = isPortAvailable);
|
||||
}
|
||||
|
||||
#endif // PORTUTILS_H
|
||||
@@ -0,0 +1,47 @@
|
||||
#include "xrayConfigUtils.h"
|
||||
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
std::optional<QString> xrayConfigUtils::extractLastConfig(const QJsonObject &serverConfig)
|
||||
{
|
||||
const QJsonArray containers = serverConfig.value(configKey::containers).toArray();
|
||||
const QString targetContainer = ContainerUtils::containerToString(DockerContainer::Xray);
|
||||
|
||||
for (const QJsonValue &value : containers) {
|
||||
const QJsonObject container = value.toObject();
|
||||
if (container.value(configKey::container).toString() != targetContainer) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const QJsonObject proto = container.value(QString(configKey::xray)).toObject();
|
||||
const QString serialized = proto.value(configKey::lastConfig).toString();
|
||||
if (!serialized.isEmpty()) {
|
||||
return serialized;
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool xrayConfigUtils::setInboundPort(QJsonObject &xrayConfig, int port)
|
||||
{
|
||||
if (!xrayConfig.value(QLatin1String("inbounds")).isArray()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonArray inbounds = xrayConfig.value(QLatin1String("inbounds")).toArray();
|
||||
if (inbounds.isEmpty() || !inbounds.at(0).isObject()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonObject firstInbound = inbounds.at(0).toObject();
|
||||
firstInbound.insert(QLatin1String("port"), port);
|
||||
inbounds[0] = firstInbound;
|
||||
xrayConfig.insert(QLatin1String("inbounds"), inbounds);
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#ifndef XRAYCONFIGUTILS_H
|
||||
#define XRAYCONFIGUTILS_H
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
|
||||
namespace xrayConfigUtils
|
||||
{
|
||||
std::optional<QString> extractLastConfig(const QJsonObject &serverConfig);
|
||||
bool setInboundPort(QJsonObject &xrayConfig, int port);
|
||||
}
|
||||
|
||||
#endif // XRAYCONFIGUTILS_H
|
||||
@@ -34,6 +34,7 @@ namespace amnezia
|
||||
MtProxy,
|
||||
Telemt,
|
||||
TProxy,
|
||||
WhatsAppProxy,
|
||||
};
|
||||
Q_ENUM_NS(Proto)
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#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;
|
||||
@@ -43,6 +44,7 @@ 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();
|
||||
}
|
||||
}
|
||||
@@ -60,6 +62,7 @@ QString amnezia::scriptName(SharedScriptType type)
|
||||
case SharedScriptType::check_connection: return QLatin1String("check_connection.sh");
|
||||
case SharedScriptType::check_server_is_busy: return QLatin1String("check_server_is_busy.sh");
|
||||
case SharedScriptType::check_user_in_sudo: return QLatin1String("check_user_in_sudo.sh");
|
||||
case SharedScriptType::update_server_packages: return QLatin1String("update_server_packages.sh");
|
||||
default: return QString();
|
||||
}
|
||||
}
|
||||
@@ -74,7 +77,6 @@ QString amnezia::scriptName(ProtocolScriptType type)
|
||||
case ProtocolScriptType::openvpn_template: return QLatin1String("template.ovpn");
|
||||
case ProtocolScriptType::wireguard_template: return QLatin1String("template.conf");
|
||||
case ProtocolScriptType::awg_template: return QLatin1String("template.conf");
|
||||
case ProtocolScriptType::xray_template: return QLatin1String("template.json");
|
||||
default: return QString();
|
||||
}
|
||||
}
|
||||
@@ -428,6 +430,18 @@ 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;
|
||||
@@ -461,6 +475,9 @@ 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,7 +28,8 @@ enum SharedScriptType {
|
||||
setup_host_firewall,
|
||||
check_connection,
|
||||
check_server_is_busy,
|
||||
check_user_in_sudo
|
||||
check_user_in_sudo,
|
||||
update_server_packages
|
||||
};
|
||||
enum ProtocolScriptType {
|
||||
// Protocol scripts
|
||||
@@ -38,8 +39,7 @@ enum ProtocolScriptType {
|
||||
container_startup,
|
||||
openvpn_template,
|
||||
wireguard_template,
|
||||
awg_template,
|
||||
xray_template
|
||||
awg_template
|
||||
};
|
||||
|
||||
enum ClientScriptType {
|
||||
@@ -71,6 +71,7 @@ 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);
|
||||
}
|
||||
|
||||
@@ -104,7 +104,8 @@ 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::Telemt || container == DockerContainer::TProxy
|
||||
|| container == DockerContainer::WhatsAppProxy;
|
||||
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);
|
||||
|
||||
|
||||
@@ -34,6 +34,16 @@ bool hasThirdPartyConfig(const QJsonObject &json)
|
||||
namespace serverConfigUtils
|
||||
{
|
||||
|
||||
int configFormatVersion(const QJsonObject &serverConfigObject)
|
||||
{
|
||||
return serverConfigObject.value(amnezia::configKey::formatVersion).toInt(0);
|
||||
}
|
||||
|
||||
bool isConfigFormatVersionSupported(const QJsonObject &serverConfigObject)
|
||||
{
|
||||
return configFormatVersion(serverConfigObject) <= currentConfigFormatVersion;
|
||||
}
|
||||
|
||||
bool isServerFromApi(const QJsonObject &serverConfigObject)
|
||||
{
|
||||
const int configVersion = serverConfigObject.value(amnezia::configKey::configVersion).toInt();
|
||||
|
||||
@@ -25,6 +25,12 @@ enum ConfigSource {
|
||||
AmneziaGateway
|
||||
};
|
||||
|
||||
constexpr int currentConfigFormatVersion = 1;
|
||||
|
||||
int configFormatVersion(const QJsonObject &serverConfigObject);
|
||||
|
||||
bool isConfigFormatVersionSupported(const QJsonObject &serverConfigObject);
|
||||
|
||||
bool isServerFromApi(const QJsonObject &serverConfigObject);
|
||||
|
||||
ConfigSource getConfigSource(const QJsonObject &serverConfigObject);
|
||||
|
||||
Loaded 100 of 535 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user