mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-22 02:01:08 +07:00
Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fa72d8de43 | |||
| e43973f8ad | |||
| 8d28beacd8 | |||
| 495d59da07 | |||
| 528015d17e | |||
| 55237f951e | |||
| 1b4f3d5872 | |||
| 8eefe7d26b | |||
| fd34bf08f1 | |||
| a7734d6093 | |||
| 526a7cad89 | |||
| 13955dd36b | |||
| d81c1e5c1a | |||
| 3ad6a7a46d | |||
| de5e2635e3 | |||
| b08154671e | |||
| af2ade1d20 | |||
| f456db5392 | |||
| ea71e9b87d | |||
| 7f3a4aaa14 | |||
| 7c455591f6 | |||
| f8e229ff7b | |||
| b033fefa31 | |||
| aab0927242 | |||
| ca8164fde4 | |||
| 7efb532bc9 | |||
| 98915b3cf2 | |||
| fa5076dff0 | |||
| dbb918b68c | |||
| 5078d1a2ca | |||
| 6907886c44 | |||
| 2f01aac483 | |||
| b94cddcb1c | |||
| 2b5c2cb021 | |||
| 729d6ee6d3 | |||
| cd87253844 | |||
| 8cdaa0c2ee | |||
| d563d66b28 | |||
| 37327e30d3 | |||
| e0af63ce1c | |||
| 041d0fcab5 | |||
| 2a6960a4fb | |||
| 8481367fb6 | |||
| 70d6f9996d | |||
| 616d58e901 | |||
| 0bfbd82536 | |||
| 544ad4ba6e |
@@ -0,0 +1,56 @@
|
|||||||
|
# .github/actions/setup-keychain/action.yml
|
||||||
|
name: Setup apple keychain
|
||||||
|
description: Creates and configures a temporary build keychain
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
keychain-path:
|
||||||
|
description: Name of the keychain
|
||||||
|
required: true
|
||||||
|
keychain-password:
|
||||||
|
description: Temporary keychain password
|
||||||
|
required: true
|
||||||
|
app-cert-base64:
|
||||||
|
description: Base64-encoded P12 app certificate
|
||||||
|
required: true
|
||||||
|
app-cert-password:
|
||||||
|
description: Application certificate password
|
||||||
|
required: true
|
||||||
|
installer-cert-base64:
|
||||||
|
description: Base64-encoded P12 installer certificate
|
||||||
|
required: true
|
||||||
|
installer-cert-password:
|
||||||
|
description: Installer certificate password
|
||||||
|
required: true
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- name: Create keychain
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
KEYCHAIN_PATH: ${{ inputs.keychain-path }}
|
||||||
|
KEYCHAIN_PASSWORD: ${{ inputs.keychain-password }}
|
||||||
|
APP_CERT_BASE64: ${{ inputs.app-cert-base64 }}
|
||||||
|
APP_CERT_PASSWORD: ${{ inputs.app-cert-password }}
|
||||||
|
INSTALLER_CERT_BASE64: ${{ inputs.installer-cert-base64 }}
|
||||||
|
INSTALLER_CERT_PASSWORD: ${{ inputs.installer-cert-password }}
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
|
||||||
|
APP_CERT_PATH=$RUNNER_TEMP/DeveloperIdApplicationCertificate.p12
|
||||||
|
INSTALLER_CERT_PATH=$RUNNER_TEMP/DeveloperIdInstallerCertificate.p12
|
||||||
|
|
||||||
|
echo -n "$APP_CERT_BASE64" | base64 --decode -o "$APP_CERT_PATH"
|
||||||
|
echo -n "$INSTALLER_CERT_BASE64" | base64 --decode -o "$INSTALLER_CERT_PATH"
|
||||||
|
|
||||||
|
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||||
|
security default-keychain -s "$KEYCHAIN_PATH"
|
||||||
|
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
|
||||||
|
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||||
|
|
||||||
|
security import "${{ github.action_path }}/DeveloperIDG2CA.cer" -k "$KEYCHAIN_PATH" -A
|
||||||
|
security import "$APP_CERT_PATH" -k "$KEYCHAIN_PATH" -P "$APP_CERT_PASSWORD" -A -t cert -f pkcs12
|
||||||
|
security import "$INSTALLER_CERT_PATH" -k "$KEYCHAIN_PATH" -P "$INSTALLER_CERT_PASSWORD" -A -t cert -f pkcs12
|
||||||
|
|
||||||
|
security set-key-partition-list -S apple-tool:,apple:,codesign: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||||
|
security list-keychain -d user -s "$KEYCHAIN_PATH"
|
||||||
+96
-210
@@ -17,7 +17,6 @@ jobs:
|
|||||||
QIF_VERSION: 4.7
|
QIF_VERSION: 4.7
|
||||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||||
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
|
|
||||||
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
||||||
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
||||||
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
||||||
@@ -39,7 +38,18 @@ jobs:
|
|||||||
set-env: 'true'
|
set-env: 'true'
|
||||||
aqtversion: '==3.3.0'
|
aqtversion: '==3.3.0'
|
||||||
py7zrversion: '==0.22.*'
|
py7zrversion: '==0.22.*'
|
||||||
extra: '--base ${{ env.QT_MIRROR }}'
|
extra: '--base ${{ env.QT_MIRROR }}'
|
||||||
|
|
||||||
|
- name: 'Setup python'
|
||||||
|
uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: 3.14
|
||||||
|
|
||||||
|
- name: 'Install conan'
|
||||||
|
run: pip install "conan==2.26.2"
|
||||||
|
|
||||||
|
- name: 'Install system packages'
|
||||||
|
run: sudo apt-get install libxkbcommon-x11-0 libsecret-1-dev
|
||||||
|
|
||||||
- name: 'Get sources'
|
- name: 'Get sources'
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -47,38 +57,17 @@ jobs:
|
|||||||
submodules: 'true'
|
submodules: 'true'
|
||||||
fetch-depth: 10
|
fetch-depth: 10
|
||||||
|
|
||||||
- name: 'Get version from CMakeLists.txt'
|
|
||||||
id: get_version
|
|
||||||
run: |
|
|
||||||
VERSION=$(grep 'set(AMNEZIAVPN_VERSION' CMakeLists.txt | sed -E 's/.*AMNEZIAVPN_VERSION ([0-9]+.[0-9]+.[0-9]+.[0-9]+)\)/\1/')
|
|
||||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
|
||||||
echo "Version: $VERSION"
|
|
||||||
|
|
||||||
# - name: 'Setup ccache'
|
|
||||||
# uses: hendrikmuhs/ccache-action@v1.2
|
|
||||||
|
|
||||||
- name: 'Build project'
|
- name: 'Build project'
|
||||||
run: |
|
shell: bash
|
||||||
sudo apt-get install libxkbcommon-x11-0 libsecret-1-dev
|
env:
|
||||||
export QT_BIN_DIR=${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/gcc_64/bin
|
QT_INSTALL_DIR: ${{ runner.temp }}
|
||||||
export QIF_BIN_DIR=${{ runner.temp }}/Qt/Tools/QtInstallerFramework/${{ env.QIF_VERSION }}/bin
|
run: ./deploy/build.sh
|
||||||
bash deploy/build_linux.sh
|
|
||||||
|
|
||||||
- name: 'Pack installer'
|
|
||||||
run: cd deploy && tar -cf AmneziaVPN_Linux_Installer.tar AmneziaVPN_Linux_Installer.bin && zip AmneziaVPN_${VERSION}_linux_x64.tar.zip AmneziaVPN_Linux_Installer.tar
|
|
||||||
|
|
||||||
- name: 'Upload installer artifact'
|
- name: 'Upload installer artifact'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: AmneziaVPN_${{ env.VERSION }}_linux_x64.tar.zip
|
path: deploy/build/AmneziaVPN-*-Linux.run
|
||||||
path: deploy/AmneziaVPN_${{ env.VERSION }}_linux_x64.tar.zip
|
archive: false
|
||||||
retention-days: 7
|
|
||||||
|
|
||||||
- name: 'Upload unpacked artifact'
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: AmneziaVPN_Linux_unpacked
|
|
||||||
path: deploy/AppDir
|
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
- name: 'Upload translations artifact'
|
- name: 'Upload translations artifact'
|
||||||
@@ -96,10 +85,8 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
QT_VERSION: 6.10.1
|
QT_VERSION: 6.10.1
|
||||||
QIF_VERSION: 4.7
|
QIF_VERSION: 4.7
|
||||||
BUILD_ARCH: 64
|
|
||||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||||
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
|
|
||||||
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
||||||
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
||||||
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
||||||
@@ -113,17 +100,6 @@ jobs:
|
|||||||
submodules: 'true'
|
submodules: 'true'
|
||||||
fetch-depth: 10
|
fetch-depth: 10
|
||||||
|
|
||||||
- name: 'Get version from CMakeLists.txt'
|
|
||||||
id: get_version
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
VERSION=$(grep 'set(AMNEZIAVPN_VERSION' CMakeLists.txt | sed -E 's/.*AMNEZIAVPN_VERSION ([0-9]+.[0-9]+.[0-9]+.[0-9]+)\)/\1/')
|
|
||||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
|
||||||
echo "Version: $VERSION"
|
|
||||||
|
|
||||||
# - name: 'Setup ccache'
|
|
||||||
# uses: hendrikmuhs/ccache-action@v1.2
|
|
||||||
|
|
||||||
- name: 'Install Qt'
|
- name: 'Install Qt'
|
||||||
uses: jurplel/install-qt-action@v3
|
uses: jurplel/install-qt-action@v3
|
||||||
with:
|
with:
|
||||||
@@ -160,39 +136,34 @@ jobs:
|
|||||||
$wixBinDir = Join-Path $env:USERPROFILE ".dotnet\tools"
|
$wixBinDir = Join-Path $env:USERPROFILE ".dotnet\tools"
|
||||||
echo "WIX_BIN_DIR=$wixBinDir" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
echo "WIX_BIN_DIR=$wixBinDir" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||||
|
|
||||||
|
- name: 'Setup python'
|
||||||
|
uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: 3.14
|
||||||
|
|
||||||
|
- name: 'Install conan'
|
||||||
|
run: pip install "conan==2.26.2"
|
||||||
|
|
||||||
- name: 'Build project'
|
- name: 'Build project'
|
||||||
shell: cmd
|
shell: cmd
|
||||||
|
env:
|
||||||
|
QT_INSTALL_DIR: ${{ runner.temp }}
|
||||||
run: |
|
run: |
|
||||||
set BUILD_ARCH=${{ env.BUILD_ARCH }}
|
set WIX_ROOT_PATH="${{ env.USERPROFILE }}\.dotnet\tools"
|
||||||
set QT_BIN_DIR="${{ runner.temp }}\\Qt\\${{ env.QT_VERSION }}\\msvc2022_64\\bin"
|
deploy\build.bat --installer all
|
||||||
set QIF_BIN_DIR="${{ runner.temp }}\\Qt\\Tools\\QtInstallerFramework\\${{ env.QIF_VERSION }}\\bin"
|
|
||||||
set WIX_BIN_DIR=%USERPROFILE%\.dotnet\tools
|
|
||||||
call deploy\\build_windows.bat
|
|
||||||
|
|
||||||
- name: 'Rename Windows installer'
|
- name: 'Upload WIX installer artifact'
|
||||||
shell: cmd
|
uses: actions/upload-artifact@v7
|
||||||
run: |
|
|
||||||
copy AmneziaVPN_x${{ env.BUILD_ARCH }}.exe AmneziaVPN_%VERSION%_x64.exe
|
|
||||||
|
|
||||||
- name: 'Upload installer artifact'
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
with:
|
||||||
name: AmneziaVPN_${{ env.VERSION }}_x64.exe
|
path: deploy/build/AmneziaVPN-*-win64.msi
|
||||||
path: AmneziaVPN_${{ env.VERSION }}_x64.exe
|
archive: false
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
- name: 'Upload MSI installer artifact'
|
- name: 'Upload IFW installer artifact'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: AmneziaVPN_Windows_MSI_installer
|
path: deploy/build/AmneziaVPN-*-win64.exe
|
||||||
path: AmneziaVPN_x${{ env.BUILD_ARCH }}.msi
|
archive: false
|
||||||
retention-days: 7
|
|
||||||
|
|
||||||
- name: 'Upload unpacked artifact'
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: AmneziaVPN_Windows_unpacked
|
|
||||||
path: deploy\\build_${{ env.BUILD_ARCH }}\\client\\Release
|
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
# ------------------------------------------------------
|
# ------------------------------------------------------
|
||||||
@@ -206,7 +177,6 @@ jobs:
|
|||||||
CXX: c++
|
CXX: c++
|
||||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||||
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
|
|
||||||
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
||||||
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
||||||
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
||||||
@@ -261,11 +231,13 @@ jobs:
|
|||||||
submodules: 'true'
|
submodules: 'true'
|
||||||
fetch-depth: 10
|
fetch-depth: 10
|
||||||
|
|
||||||
# - name: 'Setup ccache'
|
- name: 'Setup python'
|
||||||
# uses: hendrikmuhs/ccache-action@v1.2
|
uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: 3.14
|
||||||
|
|
||||||
- name: 'Install dependencies'
|
- name: 'Install deps'
|
||||||
run: pip install jsonschema jinja2
|
run: pip install "conan==2.26.2" jsonschema jinja2
|
||||||
|
|
||||||
- name: 'Build project'
|
- name: 'Build project'
|
||||||
run: |
|
run: |
|
||||||
@@ -288,94 +260,6 @@ jobs:
|
|||||||
IOS_APP_PROVISIONING_PROFILE: ${{ secrets.IOS_APP_PROVISIONING_PROFILE }}
|
IOS_APP_PROVISIONING_PROFILE: ${{ secrets.IOS_APP_PROVISIONING_PROFILE }}
|
||||||
IOS_NE_PROVISIONING_PROFILE: ${{ secrets.IOS_NE_PROVISIONING_PROFILE }}
|
IOS_NE_PROVISIONING_PROFILE: ${{ secrets.IOS_NE_PROVISIONING_PROFILE }}
|
||||||
|
|
||||||
# - name: 'Upload appstore .ipa and dSYMs to artifacts'
|
|
||||||
# uses: actions/upload-artifact@v4
|
|
||||||
# with:
|
|
||||||
# name: app-store ipa & dsyms
|
|
||||||
# path: |
|
|
||||||
# ${{ github.workspace }}/AmneziaVPN-iOS.ipa
|
|
||||||
# ${{ github.workspace }}/*.app.dSYM.zip
|
|
||||||
# retention-days: 7
|
|
||||||
|
|
||||||
# ------------------------------------------------------
|
|
||||||
|
|
||||||
Build-MacOS-old:
|
|
||||||
runs-on: macos-latest
|
|
||||||
|
|
||||||
env:
|
|
||||||
# Keep compat with MacOS 10.15 aka Catalina by Qt 6.4
|
|
||||||
QT_VERSION: 6.4.3
|
|
||||||
|
|
||||||
MAC_TEAM_ID: ${{ secrets.MAC_TEAM_ID }}
|
|
||||||
|
|
||||||
MAC_APP_CERT_CERT: ${{ secrets.MAC_APP_CERT_CERT }}
|
|
||||||
MAC_SIGNER_ID: ${{ secrets.MAC_SIGNER_ID }}
|
|
||||||
MAC_APP_CERT_PW: ${{ secrets.MAC_APP_CERT_PW }}
|
|
||||||
|
|
||||||
MAC_INSTALLER_SIGNER_CERT: ${{ secrets.MAC_INSTALLER_SIGNER_CERT }}
|
|
||||||
MAC_INSTALLER_SIGNER_ID: ${{ secrets.MAC_INSTALLER_SIGNER_ID }}
|
|
||||||
MAC_INSTALL_CERT_PW: ${{ secrets.MAC_INSTALL_CERT_PW }}
|
|
||||||
|
|
||||||
APPLE_DEV_EMAIL: ${{ secrets.APPLE_DEV_EMAIL }}
|
|
||||||
APPLE_DEV_PASSWORD: ${{ secrets.APPLE_DEV_PASSWORD }}
|
|
||||||
|
|
||||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
|
||||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
|
||||||
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
|
|
||||||
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
|
||||||
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
|
||||||
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
|
||||||
FREE_V2_ENDPOINT: ${{ secrets.FREE_V2_ENDPOINT }}
|
|
||||||
PREM_V1_ENDPOINT: ${{ secrets.PREM_V1_ENDPOINT }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: 'Setup xcode'
|
|
||||||
uses: maxim-lobanov/setup-xcode@v1
|
|
||||||
with:
|
|
||||||
xcode-version: '15.4.0'
|
|
||||||
|
|
||||||
- name: 'Install Qt'
|
|
||||||
uses: jurplel/install-qt-action@v3
|
|
||||||
with:
|
|
||||||
version: ${{ env.QT_VERSION }}
|
|
||||||
host: 'mac'
|
|
||||||
target: 'desktop'
|
|
||||||
arch: 'clang_64'
|
|
||||||
modules: 'qtremoteobjects qt5compat qtshadertools'
|
|
||||||
dir: ${{ runner.temp }}
|
|
||||||
setup-python: 'true'
|
|
||||||
set-env: 'true'
|
|
||||||
extra: '--external 7z --base ${{ env.QT_MIRROR }}'
|
|
||||||
|
|
||||||
|
|
||||||
- name: 'Get sources'
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
submodules: 'true'
|
|
||||||
fetch-depth: 10
|
|
||||||
|
|
||||||
# - name: 'Setup ccache'
|
|
||||||
# uses: hendrikmuhs/ccache-action@v1.2
|
|
||||||
|
|
||||||
- name: 'Build project'
|
|
||||||
run: |
|
|
||||||
export QT_BIN_DIR="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/macos/bin"
|
|
||||||
bash deploy/build_macos.sh -n
|
|
||||||
|
|
||||||
- name: 'Upload installer artifact'
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: AmneziaVPN_MacOS_old_installer
|
|
||||||
path: deploy/build/pkg/AmneziaVPN.pkg
|
|
||||||
retention-days: 7
|
|
||||||
|
|
||||||
- name: 'Upload unpacked artifact'
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: AmneziaVPN_MacOS_old_unpacked
|
|
||||||
path: deploy/build/client/AmneziaVPN.app
|
|
||||||
retention-days: 7
|
|
||||||
|
|
||||||
# ------------------------------------------------------
|
# ------------------------------------------------------
|
||||||
|
|
||||||
Build-MacOS:
|
Build-MacOS:
|
||||||
@@ -383,23 +267,11 @@ jobs:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
QT_VERSION: 6.10.1
|
QT_VERSION: 6.10.1
|
||||||
|
KEYCHAIN_NAME: "build.keychain"
|
||||||
MAC_TEAM_ID: ${{ secrets.MAC_TEAM_ID }}
|
KEYCHAIN_PASSWORD: ""
|
||||||
|
|
||||||
MAC_APP_CERT_CERT: ${{ secrets.MAC_APP_CERT_CERT }}
|
|
||||||
MAC_SIGNER_ID: ${{ secrets.MAC_SIGNER_ID }}
|
|
||||||
MAC_APP_CERT_PW: ${{ secrets.MAC_APP_CERT_PW }}
|
|
||||||
|
|
||||||
MAC_INSTALLER_SIGNER_CERT: ${{ secrets.MAC_INSTALLER_SIGNER_CERT }}
|
|
||||||
MAC_INSTALLER_SIGNER_ID: ${{ secrets.MAC_INSTALLER_SIGNER_ID }}
|
|
||||||
MAC_INSTALL_CERT_PW: ${{ secrets.MAC_INSTALL_CERT_PW }}
|
|
||||||
|
|
||||||
APPLE_DEV_EMAIL: ${{ secrets.APPLE_DEV_EMAIL }}
|
|
||||||
APPLE_DEV_PASSWORD: ${{ secrets.APPLE_DEV_PASSWORD }}
|
|
||||||
|
|
||||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||||
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
|
|
||||||
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
||||||
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
||||||
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
||||||
@@ -425,7 +297,15 @@ jobs:
|
|||||||
set-env: 'true'
|
set-env: 'true'
|
||||||
aqtversion: '==3.3.0'
|
aqtversion: '==3.3.0'
|
||||||
py7zrversion: '==0.22.*'
|
py7zrversion: '==0.22.*'
|
||||||
extra: '--base ${{ env.QT_MIRROR }}'
|
extra: '--base ${{ env.QT_MIRROR }}'
|
||||||
|
|
||||||
|
- name: 'Setup python'
|
||||||
|
uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: 3.14
|
||||||
|
|
||||||
|
- name: 'Install conan'
|
||||||
|
run: pip install "conan==2.26.2"
|
||||||
|
|
||||||
- name: 'Get sources'
|
- name: 'Get sources'
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -433,39 +313,34 @@ jobs:
|
|||||||
submodules: 'true'
|
submodules: 'true'
|
||||||
fetch-depth: 10
|
fetch-depth: 10
|
||||||
|
|
||||||
- name: 'Get version from CMakeLists.txt'
|
- name: 'Install certs'
|
||||||
id: get_version
|
uses: ./.github/actions/setup-keychain
|
||||||
run: |
|
with:
|
||||||
VERSION=$(grep 'set(AMNEZIAVPN_VERSION' CMakeLists.txt | sed -E 's/.*AMNEZIAVPN_VERSION ([0-9]+.[0-9]+.[0-9]+.[0-9]+)\)/\1/')
|
keychain-path: ${{ env.KEYCHAIN_NAME }}
|
||||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
keychain-password: ${{ env.KEYCHAIN_PASSWORD }}
|
||||||
echo "Version: $VERSION"
|
app-cert-base64: ${{ secrets.MAC_APP_CERT_CERT }}
|
||||||
|
app-cert-password: ${{ secrets.MAC_APP_CERT_PW }}
|
||||||
# - name: 'Setup ccache'
|
installer-cert-base64: ${{ secrets.MAC_INSTALLER_SIGNER_CERT }}
|
||||||
# uses: hendrikmuhs/ccache-action@v1.2
|
installer-cert-password: ${{ secrets.MAC_INSTALL_CERT_PW }}
|
||||||
|
|
||||||
- name: 'Build project'
|
- name: 'Build project'
|
||||||
run: |
|
env:
|
||||||
export QT_BIN_DIR="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/macos/bin"
|
QT_INSTALL_DIR: ${{ runner.temp }}
|
||||||
bash deploy/build_macos.sh -n
|
CODESIGN_KEYCHAIN: ${{ env.KEYCHAIN_NAME }}
|
||||||
|
CODESIGN_SIGNATURE: ${{ secrets.MAC_SIGNER_ID }}
|
||||||
- name: 'Pack macOS installer'
|
CODESIGN_INSTALLER_KEYCHAIN: ${{ env.KEYCHAIN_NAME }}
|
||||||
run: |
|
CODESIGN_INSTALLER_SIGNATURE: ${{ secrets.MAC_INSTALLER_SIGNER_ID }}
|
||||||
cd deploy/build/pkg
|
NOTARYTOOL_TEAM_ID: ${{ secrets.MAC_TEAM_ID }}
|
||||||
zip -r ../../AmneziaVPN_${VERSION}_macos.zip AmneziaVPN.pkg
|
NOTARYTOOL_EMAIL: ${{ secrets.APPLE_DEV_EMAIL }}
|
||||||
cd ../../..
|
NOTARYTOOL_PASSWORD: ${{ secrets.APPLE_DEV_PASSWORD }}
|
||||||
|
shell: bash
|
||||||
|
run: deploy/build.sh
|
||||||
|
|
||||||
- name: 'Upload installer artifact'
|
- name: 'Upload installer artifact'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: AmneziaVPN_${{ env.VERSION }}_macos.zip
|
path: deploy/build/AmneziaVPN-*-Darwin.pkg
|
||||||
path: deploy/AmneziaVPN_${{ env.VERSION }}_macos.zip
|
archive: false
|
||||||
retention-days: 7
|
|
||||||
|
|
||||||
- name: 'Upload unpacked artifact'
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: AmneziaVPN_MacOS_unpacked
|
|
||||||
path: deploy/build/client/AmneziaVPN.app
|
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
Build-MacOS-NE:
|
Build-MacOS-NE:
|
||||||
@@ -482,7 +357,6 @@ jobs:
|
|||||||
|
|
||||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||||
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
|
|
||||||
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
||||||
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
||||||
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
||||||
@@ -525,8 +399,13 @@ jobs:
|
|||||||
submodules: 'true'
|
submodules: 'true'
|
||||||
fetch-depth: 10
|
fetch-depth: 10
|
||||||
|
|
||||||
# - name: 'Setup ccache'
|
- name: 'Setup python'
|
||||||
# uses: hendrikmuhs/ccache-action@v1.2
|
uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: 3.14
|
||||||
|
|
||||||
|
- name: 'Install conan'
|
||||||
|
run: pip install "conan==2.26.2"
|
||||||
|
|
||||||
- name: 'Build project'
|
- name: 'Build project'
|
||||||
run: |
|
run: |
|
||||||
@@ -547,11 +426,10 @@ jobs:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
ANDROID_BUILD_PLATFORM: android-36
|
ANDROID_BUILD_PLATFORM: android-36
|
||||||
QT_VERSION: 6.11.1
|
QT_VERSION: 6.10.1
|
||||||
QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools'
|
QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools'
|
||||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||||
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
|
|
||||||
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
||||||
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
||||||
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
||||||
@@ -659,6 +537,14 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo $KEYSTORE_BASE64 | base64 --decode > android.keystore
|
echo $KEYSTORE_BASE64 | base64 --decode > android.keystore
|
||||||
|
|
||||||
|
- name: 'Setup python'
|
||||||
|
uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: 3.14
|
||||||
|
|
||||||
|
- name: 'Install conan'
|
||||||
|
run: pip install "conan==2.26.2"
|
||||||
|
|
||||||
- name: 'Build project'
|
- name: 'Build project'
|
||||||
env:
|
env:
|
||||||
ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }}
|
ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ jobs:
|
|||||||
QIF_VERSION: 4.5
|
QIF_VERSION: 4.5
|
||||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||||
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
|
|
||||||
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
||||||
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
||||||
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
||||||
|
|||||||
+15
-40
@@ -1,7 +1,14 @@
|
|||||||
cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
|
cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
|
||||||
|
|
||||||
set(PROJECT AmneziaVPN)
|
set(PROJECT AmneziaVPN)
|
||||||
set(AMNEZIAVPN_VERSION 4.8.19.0)
|
set(AMNEZIAVPN_VERSION 4.8.13.1)
|
||||||
|
|
||||||
|
set(QT_CREATOR_SKIP_PACKAGE_MANAGER_SETUP ON CACHE BOOL "" FORCE)
|
||||||
|
set(CMAKE_PROJECT_TOP_LEVEL_INCLUDES
|
||||||
|
${CMAKE_SOURCE_DIR}/cmake/platform_settings.cmake
|
||||||
|
${CMAKE_SOURCE_DIR}/cmake/recipes_bootstrap.cmake
|
||||||
|
${CMAKE_SOURCE_DIR}/cmake/conan_provider.cmake
|
||||||
|
CACHE STRING "" FORCE)
|
||||||
|
|
||||||
project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
|
project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
|
||||||
DESCRIPTION "AmneziaVPN"
|
DESCRIPTION "AmneziaVPN"
|
||||||
@@ -12,7 +19,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
|
|||||||
set(RELEASE_DATE "${CURRENT_DATE}")
|
set(RELEASE_DATE "${CURRENT_DATE}")
|
||||||
|
|
||||||
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
|
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
|
||||||
set(APP_ANDROID_VERSION_CODE 2129)
|
set(APP_ANDROID_VERSION_CODE 2107)
|
||||||
|
|
||||||
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
||||||
set(MZ_PLATFORM_NAME "linux")
|
set(MZ_PLATFORM_NAME "linux")
|
||||||
@@ -24,6 +31,8 @@ elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Android")
|
|||||||
set(MZ_PLATFORM_NAME "android")
|
set(MZ_PLATFORM_NAME "android")
|
||||||
elseif(${CMAKE_SYSTEM_NAME} STREQUAL "iOS")
|
elseif(${CMAKE_SYSTEM_NAME} STREQUAL "iOS")
|
||||||
set(MZ_PLATFORM_NAME "ios")
|
set(MZ_PLATFORM_NAME "ios")
|
||||||
|
elseif(${CMAKE_SYSTEM_NAME} STREQUAL "tvOS")
|
||||||
|
set(MZ_PLATFORM_NAME "ios")
|
||||||
elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Emscripten")
|
elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Emscripten")
|
||||||
set(MZ_PLATFORM_NAME "wasm")
|
set(MZ_PLATFORM_NAME "wasm")
|
||||||
endif()
|
endif()
|
||||||
@@ -33,7 +42,7 @@ set(CMAKE_CXX_STANDARD 17)
|
|||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
if(APPLE)
|
if(APPLE)
|
||||||
if(IOS)
|
if(IOS OR CMAKE_SYSTEM_NAME STREQUAL "tvOS")
|
||||||
set(CMAKE_OSX_ARCHITECTURES "arm64")
|
set(CMAKE_OSX_ARCHITECTURES "arm64")
|
||||||
elseif(MACOS_NE)
|
elseif(MACOS_NE)
|
||||||
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64")
|
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64")
|
||||||
@@ -44,44 +53,10 @@ endif()
|
|||||||
|
|
||||||
add_subdirectory(client)
|
add_subdirectory(client)
|
||||||
|
|
||||||
if(NOT IOS AND NOT ANDROID AND NOT MACOS_NE)
|
if(NOT IOS AND NOT ANDROID AND NOT MACOS_NE AND NOT CMAKE_SYSTEM_NAME STREQUAL "tvOS")
|
||||||
add_subdirectory(service)
|
add_subdirectory(service)
|
||||||
|
|
||||||
include(${CMAKE_SOURCE_DIR}/deploy/installer/config.cmake)
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
set(AMNEZIA_STAGE_DIR "${CMAKE_BINARY_DIR}/stage")
|
if ((LINUX AND NOT ANDROID) OR (APPLE AND NOT IOS AND NOT MACOS_NE AND NOT CMAKE_SYSTEM_NAME STREQUAL "tvOS") OR (WIN32))
|
||||||
|
include(${CMAKE_SOURCE_DIR}/cmake/CPack.cmake)
|
||||||
if(WIN32 AND NOT IOS AND NOT ANDROID AND NOT MACOS_NE)
|
|
||||||
file(TO_CMAKE_PATH "${AMNEZIA_STAGE_DIR}" AMNEZIA_STAGE_DIR_CMAKE)
|
|
||||||
|
|
||||||
set(CPACK_GENERATOR "WIX")
|
|
||||||
set(CPACK_WIX_VERSION 4)
|
|
||||||
set(CPACK_PACKAGE_NAME "AmneziaVPN")
|
|
||||||
set(CPACK_PACKAGE_VENDOR "AmneziaVPN")
|
|
||||||
set(CPACK_PACKAGE_VERSION ${AMNEZIAVPN_VERSION})
|
|
||||||
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "AmneziaVPN client")
|
|
||||||
set(AMNEZIA_LICENSE_TXT "${CMAKE_BINARY_DIR}/LICENSE.txt")
|
|
||||||
configure_file("${CMAKE_SOURCE_DIR}/LICENSE" "${AMNEZIA_LICENSE_TXT}" COPYONLY)
|
|
||||||
set(CPACK_RESOURCE_FILE_LICENSE "${AMNEZIA_LICENSE_TXT}")
|
|
||||||
set(CPACK_PACKAGE_INSTALL_DIRECTORY "AmneziaVPN")
|
|
||||||
set(CPACK_PACKAGE_DIRECTORY "${CMAKE_BINARY_DIR}")
|
|
||||||
set(CPACK_PACKAGE_EXECUTABLES "AmneziaVPN" "AmneziaVPN")
|
|
||||||
set(CPACK_WIX_UPGRADE_GUID "{2D55AC62-96D6-4692-8C05-0D85BBF95485}")
|
|
||||||
set(CPACK_WIX_PRODUCT_ICON "${CMAKE_SOURCE_DIR}/client/images/app.ico")
|
|
||||||
|
|
||||||
# WiX patches
|
|
||||||
set(_AMNEZIA_WIX_PATCH_SERVICE "${CMAKE_SOURCE_DIR}/deploy/installer/wix/service_install_patch.xml")
|
|
||||||
set(_AMNEZIA_WIX_PATCH_CLOSE_APP "${CMAKE_SOURCE_DIR}/deploy/installer/wix/close_client_patch.xml")
|
|
||||||
file(TO_CMAKE_PATH "${_AMNEZIA_WIX_PATCH_SERVICE}" _AMNEZIA_WIX_PATCH_SERVICE_CMAKE)
|
|
||||||
file(TO_CMAKE_PATH "${_AMNEZIA_WIX_PATCH_CLOSE_APP}" _AMNEZIA_WIX_PATCH_CLOSE_APP_CMAKE)
|
|
||||||
set(CPACK_WIX_PATCH_FILE "${_AMNEZIA_WIX_PATCH_SERVICE_CMAKE};${_AMNEZIA_WIX_PATCH_CLOSE_APP_CMAKE}")
|
|
||||||
|
|
||||||
# WiX v4 Util extension for CloseApplication + namespace for util
|
|
||||||
set(CPACK_WIX_EXTENSIONS "${CPACK_WIX_EXTENSIONS};WixToolset.Util.wixext")
|
|
||||||
set(CPACK_WIX_CUSTOM_XMLNS "util=http://wixtoolset.org/schemas/v4/wxs/util")
|
|
||||||
|
|
||||||
set(CPACK_INSTALLED_DIRECTORIES "${AMNEZIA_STAGE_DIR_CMAKE};/")
|
|
||||||
|
|
||||||
include(CPack)
|
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ You may face compiling issues in QT Creator after you've worked in Android Studi
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is licensed under the GNU General Public License v3.0 (see LICENSE) and also includes third-party components distributed under their own terms (see THIRD_PARTY_LICENSES.md).
|
GPL v3.0
|
||||||
|
|
||||||
## Donate
|
## Donate
|
||||||
|
|
||||||
|
|||||||
@@ -1,149 +0,0 @@
|
|||||||
# Third-Party Licenses
|
|
||||||
|
|
||||||
This project is licensed under the GNU General Public License v3.0.
|
|
||||||
This file lists third-party software components used by this repository.
|
|
||||||
Each component is distributed under its own license as linked below.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## QtKeychain
|
|
||||||
|
|
||||||
- Source: https://github.com/frankosterfeld/qtkeychain
|
|
||||||
- License: BSD License
|
|
||||||
- License Text: https://www.gnu.org/licenses/license-list.html#ModifiedBSD
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## QSimpleCrypto
|
|
||||||
|
|
||||||
- Source: https://github.com/n1flh31mur/QSimpleCrypto
|
|
||||||
- License: Apache License 2.0
|
|
||||||
- License Text: https://github.com/n1flh31mur/QSimpleCrypto/blob/master/LICENSE
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## SortFilterProxyModel
|
|
||||||
|
|
||||||
- Source: https://github.com/oKcerG/SortFilterProxyModel
|
|
||||||
- License: MIT License
|
|
||||||
- License Text: https://github.com/oKcerG/SortFilterProxyModel/blob/master/LICENSE
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## QJsonStruct
|
|
||||||
|
|
||||||
- Source: https://github.com/Qv2ray/QJsonStruct
|
|
||||||
- License: MIT License
|
|
||||||
- License Text: https://github.com/Qv2ray/QJsonStruct/blob/master/LICENSE
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## QR Code Generator (qrcodegen)
|
|
||||||
|
|
||||||
- Source: https://github.com/nayuki/QR-Code-generator
|
|
||||||
- License: MIT License
|
|
||||||
- License Text: https://www.nayuki.io/page/qr-code-generator-library
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Qt Gamepad
|
|
||||||
|
|
||||||
- Source: https://github.com/qt/qtgamepad
|
|
||||||
- License: GNU General Public License v3.0 (GPL-3.0)
|
|
||||||
- License Text: https://www.gnu.org/licenses/gpl-3.0.en.html
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## AmneziaWG Apple (WireGuard)
|
|
||||||
|
|
||||||
- Source: https://github.com/amnezia-vpn/amneziawg-apple
|
|
||||||
- License: MIT License
|
|
||||||
- License Text: https://github.com/amnezia-vpn/amneziawg-apple/blob/master/COPYING
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## AmneziaWG Android
|
|
||||||
|
|
||||||
- Source: https://github.com/amnezia-vpn/amneziawg-go
|
|
||||||
- License: MIT License
|
|
||||||
- License Text: https://github.com/amnezia-vpn/amneziawg-go/blob/master/LICENSE
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Xray Core
|
|
||||||
|
|
||||||
- Source: https://github.com/XTLS/Xray-core
|
|
||||||
- License: Mozilla Public License 2.0 (MPL-2.0)
|
|
||||||
- License Text: https://github.com/XTLS/Xray-core/blob/main/LICENSE
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Cloak
|
|
||||||
|
|
||||||
- Source: https://github.com/cbeuw/Cloak
|
|
||||||
- License: GNU General Public License v3.0 (GPL-3.0)
|
|
||||||
- License Text: https://github.com/cbeuw/Cloak/blob/master/LICENSE
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Shadowsocks
|
|
||||||
|
|
||||||
- Source: https://github.com/shadowsocks/shadowsocks-libev
|
|
||||||
- License: GPL-3.0-or-later
|
|
||||||
- License Text: http://www.gnu.org/licenses/
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## OpenSSL
|
|
||||||
|
|
||||||
- Source: https://github.com/openssl/openssl
|
|
||||||
- License: Apache License 2.0
|
|
||||||
- License Text: https://www.openssl.org/source/license.html
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## libssh
|
|
||||||
|
|
||||||
- Source: https://www.libssh.org/
|
|
||||||
- License: GNU Lesser General Public License (LGPL)
|
|
||||||
- License Text: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## OpenVPNAdapter
|
|
||||||
|
|
||||||
- Source: https://github.com/ss-abramchuk/OpenVPNAdapter
|
|
||||||
- License: GNU Affero General Public License v3.0 (AGPL-3.0)
|
|
||||||
- License Text: https://github.com/ss-abramchuk/OpenVPNAdapter/blob/master/LICENSE
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Wintun
|
|
||||||
|
|
||||||
- Source: https://www.wintun.net/
|
|
||||||
- License: Prebuilt Binaries License
|
|
||||||
- License Text: https://github.com/WireGuard/wintun/blob/master/prebuilt-binaries-license.txt
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Mullvad Split Tunnel Driver
|
|
||||||
|
|
||||||
- Source: https://github.com/mullvad/win-split-tunnel
|
|
||||||
- License: GNU General Public License v3.0 (GPL-3.0) and Mozilla Public License Version 2.0
|
|
||||||
- License Text: https://github.com/mullvad/win-split-tunnel/blob/master/LICENSE-GPL.md https://github.com/mullvad/win-split-tunnel/blob/master/LICENSE-MPL.txt
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## tun2socks
|
|
||||||
|
|
||||||
- Source: https://github.com/eycorsican/go-tun2socks
|
|
||||||
- License: MIT License
|
|
||||||
- License Text: https://github.com/eycorsican/go-tun2socks/blob/master/LICENSE
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## TAP-Windows Driver
|
|
||||||
|
|
||||||
- Source: https://github.com/OpenVPN/tap-windows6
|
|
||||||
- License: tap-windows6 license
|
|
||||||
- License Text: https://github.com/OpenVPN/tap-windows6/blob/master/COPYING
|
|
||||||
@@ -0,0 +1,340 @@
|
|||||||
|
# AmneziaVPN Apple TV Build
|
||||||
|
|
||||||
|
This document describes how to build the current branch for Apple TV from the repository root.
|
||||||
|
|
||||||
|
The pipeline is:
|
||||||
|
|
||||||
|
1. Use a separately built static Qt 6.9.2 for `tvOS`.
|
||||||
|
2. Let Conan build/provide C/C++ dependencies.
|
||||||
|
3. Generate an Xcode project with `qt-cmake`.
|
||||||
|
4. Build the `.app` and embedded Network Extension with `xcodebuild`.
|
||||||
|
|
||||||
|
Important:
|
||||||
|
|
||||||
|
- Run the project commands from the repository root.
|
||||||
|
- This is a device build for `appletvos`, not a simulator build.
|
||||||
|
- `xcodebuild build` produces `.app`.
|
||||||
|
- `.ipa` is produced later via `archive` and `-exportArchive`.
|
||||||
|
- The current tvOS Network Extension scope is WireGuard-only.
|
||||||
|
- The temporary tvOS WireGuard bridge prebuilt is opt-in. The Conan recipe does not contain machine-specific fallback paths.
|
||||||
|
- Do not initialize or update submodules just for this build. If a clean checkout has empty `client/3rd` folders, pass `AMNEZIA_THIRDPARTY_ROOT` to an already initialized read-only `client/3rd` tree.
|
||||||
|
|
||||||
|
## 1. Environment
|
||||||
|
|
||||||
|
Set these paths for your machine:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export REPO_ROOT="$PWD"
|
||||||
|
export QT_DESKTOP_PREFIX="$HOME/Qt/6.9.2/macos"
|
||||||
|
export QT_TVOS_SRC="$HOME/Qt_tv/qt-6.9.2-tvos-src"
|
||||||
|
export QT_TVOS_PREFIX="$HOME/Qt_tv/6.9.2/tvos-device"
|
||||||
|
export BUILD_DIR="$REPO_ROOT/build-tvos-device-conan"
|
||||||
|
```
|
||||||
|
|
||||||
|
If this checkout does not have initialized `client/3rd` sources, point CMake at an initialized tree:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export AMNEZIA_THIRDPARTY_ROOT="/path/to/initialized/amnezia/client/3rd"
|
||||||
|
```
|
||||||
|
|
||||||
|
If you are using a temporary prebuilt tvOS WireGuard bridge, point Conan at it explicitly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export AMNEZIA_TVOS_AWG_PREBUILT_DIR="/path/to/WireGuardKitGo-appletvos"
|
||||||
|
export AMNEZIA_TVOS_AWG_VERSION_HEADER_DIR="/path/to/directory/with/wireguard-go-version.h"
|
||||||
|
```
|
||||||
|
|
||||||
|
`AMNEZIA_TVOS_AWG_PREBUILT_DIR` must contain `libwg-go.a`.
|
||||||
|
|
||||||
|
`AMNEZIA_TVOS_AWG_VERSION_HEADER_DIR` is optional when `wireguard-go-version.h` lives in the same directory as `libwg-go.a`.
|
||||||
|
|
||||||
|
If the env vars are not set, the recipe uses the normal source build path. Rebuilding and publishing the tvOS WireGuard bridge through the registry is a separate task.
|
||||||
|
|
||||||
|
## 2. Required Local Tools
|
||||||
|
|
||||||
|
Conan must be available:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv tool install conan
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
conan --version
|
||||||
|
```
|
||||||
|
|
||||||
|
Validated version:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Conan version 2.27.1
|
||||||
|
```
|
||||||
|
|
||||||
|
The build uses Xcode's AppleTVOS SDK:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
xcrun --sdk appletvos --show-sdk-path
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Prepare Qt Sources
|
||||||
|
|
||||||
|
Do not edit the installed Qt sources in place. Copy them into a separate tvOS fork:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p "$HOME/Qt_tv"
|
||||||
|
rsync -a "$HOME/Qt/6.9.2/Src/" "$QT_TVOS_SRC/"
|
||||||
|
```
|
||||||
|
|
||||||
|
Recommended for reproducibility:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd "$QT_TVOS_SRC"
|
||||||
|
git init
|
||||||
|
git add .
|
||||||
|
git commit -m "Qt 6.9.2 source snapshot"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Apply the Qt tvOS Patchset
|
||||||
|
|
||||||
|
Apply the local Qt tvOS patchset to `$QT_TVOS_SRC`.
|
||||||
|
|
||||||
|
If you need to recreate the patchset from a fresh copy, compare these files against `$HOME/Qt/6.9.2/Src` and reapply the same changes:
|
||||||
|
|
||||||
|
- `qtbase/cmake/QtBaseGlobalTargets.cmake`
|
||||||
|
- `qtbase/cmake/QtBaseHelpers.cmake`
|
||||||
|
- `qtbase/cmake/QtBuildPathsHelpers.cmake`
|
||||||
|
- `qtbase/cmake/QtMkspecHelpers.cmake`
|
||||||
|
- `qtbase/cmake/QtConfig.cmake.in`
|
||||||
|
- `qtbase/mkspecs/unsupported/macx-tvos-clang/qplatformdefs.h`
|
||||||
|
- `qtbase/src/corelib/CMakeLists.txt`
|
||||||
|
- `qtbase/src/corelib/platform/darwin/qdarwinpermissionplugin_location.mm`
|
||||||
|
- `qtbase/src/gui/CMakeLists.txt`
|
||||||
|
- `qtbase/src/widgets/CMakeLists.txt`
|
||||||
|
- `qtbase/src/network/kernel/qnetworkproxy_darwin.cpp`
|
||||||
|
- `qtbase/src/testlib/qtestcrashhandler.cpp`
|
||||||
|
- `qtbase/src/plugins/platforms/ios/qiosapplicationdelegate.mm`
|
||||||
|
- `qtbase/src/plugins/platforms/ios/qiosscreen.mm`
|
||||||
|
- `qtbase/src/plugins/platforms/ios/qiostheme.mm`
|
||||||
|
- `qtbase/src/plugins/platforms/ios/quiview.mm`
|
||||||
|
- `qtbase/src/plugins/platforms/ios/qiosclipboard.mm`
|
||||||
|
|
||||||
|
Recommended after patching:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git -C "$QT_TVOS_SRC" diff > "$HOME/Qt_tv/qt-6.9.2-tvos.patch"
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not use `QT_APPLE_SDK=appletvos`. The working path is `CMAKE_SYSTEM_NAME=tvOS` with `CMAKE_OSX_SYSROOT=appletvos`.
|
||||||
|
|
||||||
|
## 5. Build Qt 6.9.2 for Apple TV
|
||||||
|
|
||||||
|
Only the modules required by this project are built.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p /private/tmp/qt6.9.2-tvos-device-build
|
||||||
|
cd /private/tmp/qt6.9.2-tvos-device-build
|
||||||
|
|
||||||
|
"$QT_TVOS_SRC/configure" \
|
||||||
|
-release -static -appstore-compliant \
|
||||||
|
-nomake tests -nomake examples \
|
||||||
|
-submodules qtbase,qtdeclarative,qtshadertools,qtremoteobjects,qtsvg,qt5compat,qttools \
|
||||||
|
-qt-host-path "$QT_DESKTOP_PREFIX" \
|
||||||
|
-prefix "$QT_TVOS_PREFIX" \
|
||||||
|
-- \
|
||||||
|
-G Ninja \
|
||||||
|
-DQT_QMAKE_TARGET_MKSPEC=macx-tvos-clang \
|
||||||
|
-DCMAKE_SYSTEM_NAME=tvOS \
|
||||||
|
-DCMAKE_OSX_SYSROOT=appletvos \
|
||||||
|
-DCMAKE_OSX_ARCHITECTURES=arm64 \
|
||||||
|
-DCMAKE_OSX_DEPLOYMENT_TARGET=17.0 \
|
||||||
|
-DBUILD_SHARED_LIBS=OFF \
|
||||||
|
-DQT_NO_APPLE_SDK_MAX_VERSION_CHECK=ON
|
||||||
|
|
||||||
|
cmake --build . --parallel 8
|
||||||
|
cmake --install .
|
||||||
|
```
|
||||||
|
|
||||||
|
Sanity checks:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"$QT_TVOS_PREFIX/bin/qt-cmake" --version
|
||||||
|
"$QT_TVOS_PREFIX/bin/qmake" -query QMAKE_XSPEC
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected `QMAKE_XSPEC`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
macx-tvos-clang
|
||||||
|
```
|
||||||
|
|
||||||
|
Return to the repository root after building Qt:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Conan Dependency Behavior
|
||||||
|
|
||||||
|
For `CMAKE_SYSTEM_NAME=tvOS`, the project-level Conan graph is intentionally reduced:
|
||||||
|
|
||||||
|
- included: `awg-apple/2.0.1`
|
||||||
|
- included: `libssh/0.11.3@amnezia`
|
||||||
|
- included: `openssl/3.6.1` with `no_apps=True`
|
||||||
|
- excluded: `openvpnadapter`
|
||||||
|
- excluded: `hev-socks5-tunnel`
|
||||||
|
|
||||||
|
This keeps the current Apple TV target in the same practical scope as before: app plus WireGuard-only Network Extension.
|
||||||
|
|
||||||
|
`libssh` is built with `WITH_EXEC=OFF` on tvOS because tvOS does not provide `fork()` or `execv()`.
|
||||||
|
|
||||||
|
## 7. Configure the Project
|
||||||
|
|
||||||
|
From the repository root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
|
"$QT_TVOS_PREFIX/bin/qt-cmake" \
|
||||||
|
-B"$BUILD_DIR" \
|
||||||
|
-GXcode \
|
||||||
|
-DQT_HOST_PATH="$QT_DESKTOP_PREFIX" \
|
||||||
|
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
|
||||||
|
-DCMAKE_SYSTEM_NAME=tvOS \
|
||||||
|
-DCMAKE_OSX_SYSROOT=appletvos
|
||||||
|
```
|
||||||
|
|
||||||
|
If you need to provide an external initialized `client/3rd` tree:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"$QT_TVOS_PREFIX/bin/qt-cmake" \
|
||||||
|
-B"$BUILD_DIR" \
|
||||||
|
-GXcode \
|
||||||
|
-DQT_HOST_PATH="$QT_DESKTOP_PREFIX" \
|
||||||
|
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
|
||||||
|
-DCMAKE_SYSTEM_NAME=tvOS \
|
||||||
|
-DCMAKE_OSX_SYSROOT=appletvos \
|
||||||
|
-DAMNEZIA_THIRDPARTY_ROOT="$AMNEZIA_THIRDPARTY_ROOT"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected non-fatal configure warnings:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Warning: plug-in QIOSIntegrationPlugin is not known to the current Qt installation.
|
||||||
|
Warning: plug-in QJpegPlugin is not known to the current Qt installation.
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
In this repo those warnings are tolerated because `client/cmake/ios.cmake` also links the static plugin targets explicitly when available.
|
||||||
|
|
||||||
|
## 8. Build the Apple TV App
|
||||||
|
|
||||||
|
```bash
|
||||||
|
xcodebuild -quiet \
|
||||||
|
-project "$BUILD_DIR/AmneziaVPN.xcodeproj" \
|
||||||
|
-scheme AmneziaVPN \
|
||||||
|
-configuration RelWithDebInfo \
|
||||||
|
-sdk appletvos \
|
||||||
|
CODE_SIGNING_ALLOWED=NO \
|
||||||
|
build
|
||||||
|
```
|
||||||
|
|
||||||
|
Outputs:
|
||||||
|
|
||||||
|
- `$BUILD_DIR/client/RelWithDebInfo-appletvos/AmneziaVPN.app`
|
||||||
|
- `$BUILD_DIR/client/RelWithDebInfo-appletvos/AmneziaVPN.app/PlugIns/AmneziaVPNNetworkExtension.appex`
|
||||||
|
|
||||||
|
Verification:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
file "$BUILD_DIR/client/RelWithDebInfo-appletvos/AmneziaVPN.app/AmneziaVPN"
|
||||||
|
file "$BUILD_DIR/client/RelWithDebInfo-appletvos/AmneziaVPN.app/PlugIns/AmneziaVPNNetworkExtension.appex/AmneziaVPNNetworkExtension"
|
||||||
|
lipo -info "$BUILD_DIR/client/RelWithDebInfo-appletvos/AmneziaVPN.app/AmneziaVPN"
|
||||||
|
lipo -info "$BUILD_DIR/client/RelWithDebInfo-appletvos/AmneziaVPN.app/PlugIns/AmneziaVPNNetworkExtension.appex/AmneziaVPNNetworkExtension"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Mach-O 64-bit executable arm64
|
||||||
|
Non-fat file: ... is architecture: arm64
|
||||||
|
```
|
||||||
|
|
||||||
|
Useful plist checks:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
plutil -p "$BUILD_DIR/client/RelWithDebInfo-appletvos/AmneziaVPN.app/Info.plist" | rg 'CFBundleIdentifier|DTPlatformName|UIDeviceFamily|MinimumOSVersion' -C 1
|
||||||
|
plutil -p "$BUILD_DIR/client/RelWithDebInfo-appletvos/AmneziaVPN.app/PlugIns/AmneziaVPNNetworkExtension.appex/Info.plist" | rg 'CFBundleIdentifier|NSExtension|DTPlatformName|MinimumOSVersion' -C 1
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
- `DTPlatformName => appletvos`
|
||||||
|
- `UIDeviceFamily => 3`
|
||||||
|
- `MinimumOSVersion => 17.0`
|
||||||
|
- extension point `com.apple.networkextension.packet-tunnel`
|
||||||
|
|
||||||
|
## 9. `.app` vs `.ipa`
|
||||||
|
|
||||||
|
This is the normal sequence:
|
||||||
|
|
||||||
|
1. `xcodebuild build` -> `.app`
|
||||||
|
2. `xcodebuild archive` -> `.xcarchive`
|
||||||
|
3. `xcodebuild -exportArchive` -> `.ipa`
|
||||||
|
|
||||||
|
So seeing `.app` after a successful `build` is correct.
|
||||||
|
|
||||||
|
## 10. Optional Archive and Export
|
||||||
|
|
||||||
|
The commands below are the next step for packaging, but signing and provisioning must be configured first.
|
||||||
|
|
||||||
|
Archive:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
xcodebuild \
|
||||||
|
-project "$BUILD_DIR/AmneziaVPN.xcodeproj" \
|
||||||
|
-scheme AmneziaVPN \
|
||||||
|
-configuration RelWithDebInfo \
|
||||||
|
-sdk appletvos \
|
||||||
|
-archivePath "$BUILD_DIR/AmneziaVPN-tvos.xcarchive" \
|
||||||
|
archive
|
||||||
|
```
|
||||||
|
|
||||||
|
Export:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
xcodebuild -exportArchive \
|
||||||
|
-archivePath "$BUILD_DIR/AmneziaVPN-tvos.xcarchive" \
|
||||||
|
-exportPath "$BUILD_DIR/export-tvos" \
|
||||||
|
-exportOptionsPlist /absolute/path/to/ExportOptions.plist
|
||||||
|
```
|
||||||
|
|
||||||
|
The resulting `.ipa` should appear under:
|
||||||
|
|
||||||
|
```text
|
||||||
|
$BUILD_DIR/export-tvos
|
||||||
|
```
|
||||||
|
|
||||||
|
## 11. Known Non-Fatal Warnings
|
||||||
|
|
||||||
|
The validated `xcodebuild` still prints warnings that do not break the build:
|
||||||
|
|
||||||
|
- missing Swift search path under the active Xcode Metal toolchain
|
||||||
|
- `SDKROOT[sdk=...]` target-level warnings generated by Xcode project export
|
||||||
|
- Swift conditional compilation flag warnings such as `GROUP_ID="..."`
|
||||||
|
- asset catalog warnings because the current icon set is still iOS-shaped, not a full tvOS Top Shelf asset set
|
||||||
|
- Go/WireGuard umbrella-header warnings from the temporary local `libwg-go.a` bridge
|
||||||
|
- deprecated libssh SCP API warnings in existing app code
|
||||||
|
- `qt_import_plugins()` warnings shown during configure
|
||||||
|
|
||||||
|
If the static platform plugin is not linked correctly, the typical failure is:
|
||||||
|
|
||||||
|
- `_OBJC_CLASS_$_QIOSApplicationDelegate`
|
||||||
|
- `_qt_main_wrapper`
|
||||||
|
|
||||||
|
Those are cleanup tasks, not blockers for the current build proof.
|
||||||
|
|
||||||
|
## 12. Fast Rebuild Checklist
|
||||||
|
|
||||||
|
If everything is already built once:
|
||||||
|
|
||||||
|
1. Reuse `$QT_TVOS_PREFIX`
|
||||||
|
2. Reuse Conan cache under `$HOME/.conan2`
|
||||||
|
3. Reuse or pass an initialized `AMNEZIA_THIRDPARTY_ROOT`
|
||||||
|
4. Re-run `qt-cmake` into `$BUILD_DIR`
|
||||||
|
5. Re-run `xcodebuild -quiet ... build`
|
||||||
+1
-1
Submodule client/3rd-prebuilt updated: 4680bd8fb4...b8c229288d
+36
-37
@@ -3,6 +3,9 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
|
|||||||
set(PROJECT AmneziaVPN)
|
set(PROJECT AmneziaVPN)
|
||||||
project(${PROJECT})
|
project(${PROJECT})
|
||||||
|
|
||||||
|
set(AMNEZIA_THIRDPARTY_ROOT "${CMAKE_CURRENT_LIST_DIR}/3rd" CACHE PATH "Path to Amnezia client/3rd sources")
|
||||||
|
get_filename_component(AMNEZIA_THIRDPARTY_CLIENT_ROOT "${AMNEZIA_THIRDPARTY_ROOT}/.." ABSOLUTE)
|
||||||
|
|
||||||
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
|
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
|
||||||
set_property(GLOBAL PROPERTY AUTOGEN_TARGETS_FOLDER "Autogen")
|
set_property(GLOBAL PROPERTY AUTOGEN_TARGETS_FOLDER "Autogen")
|
||||||
set_property(GLOBAL PROPERTY AUTOMOC_TARGETS_FOLDER "Autogen")
|
set_property(GLOBAL PROPERTY AUTOMOC_TARGETS_FOLDER "Autogen")
|
||||||
@@ -25,7 +28,6 @@ add_definitions(-DGIT_COMMIT_HASH="${GIT_COMMIT_HASH}")
|
|||||||
|
|
||||||
add_definitions(-DPROD_AGW_PUBLIC_KEY="$ENV{PROD_AGW_PUBLIC_KEY}")
|
add_definitions(-DPROD_AGW_PUBLIC_KEY="$ENV{PROD_AGW_PUBLIC_KEY}")
|
||||||
add_definitions(-DPROD_S3_ENDPOINT="$ENV{PROD_S3_ENDPOINT}")
|
add_definitions(-DPROD_S3_ENDPOINT="$ENV{PROD_S3_ENDPOINT}")
|
||||||
add_definitions(-DFALLBACK_S3_ENDPOINT="$ENV{FALLBACK_S3_ENDPOINT}")
|
|
||||||
|
|
||||||
add_definitions(-DDEV_AGW_PUBLIC_KEY="$ENV{DEV_AGW_PUBLIC_KEY}")
|
add_definitions(-DDEV_AGW_PUBLIC_KEY="$ENV{DEV_AGW_PUBLIC_KEY}")
|
||||||
add_definitions(-DDEV_AGW_ENDPOINT="$ENV{DEV_AGW_ENDPOINT}")
|
add_definitions(-DDEV_AGW_ENDPOINT="$ENV{DEV_AGW_ENDPOINT}")
|
||||||
@@ -34,7 +36,7 @@ add_definitions(-DDEV_S3_ENDPOINT="$ENV{DEV_S3_ENDPOINT}")
|
|||||||
add_definitions(-DFREE_V2_ENDPOINT="$ENV{FREE_V2_ENDPOINT}")
|
add_definitions(-DFREE_V2_ENDPOINT="$ENV{FREE_V2_ENDPOINT}")
|
||||||
add_definitions(-DPREM_V1_ENDPOINT="$ENV{PREM_V1_ENDPOINT}")
|
add_definitions(-DPREM_V1_ENDPOINT="$ENV{PREM_V1_ENDPOINT}")
|
||||||
|
|
||||||
if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
|
if(WIN32 OR (APPLE AND NOT IOS AND NOT CMAKE_SYSTEM_NAME STREQUAL "tvOS") OR (LINUX AND NOT ANDROID))
|
||||||
set(PACKAGES ${PACKAGES} Widgets)
|
set(PACKAGES ${PACKAGES} Widgets)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
@@ -47,7 +49,7 @@ set(LIBS ${LIBS}
|
|||||||
Qt6::Core5Compat Qt6::Concurrent
|
Qt6::Core5Compat Qt6::Concurrent
|
||||||
)
|
)
|
||||||
|
|
||||||
if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
|
if(WIN32 OR (APPLE AND NOT IOS AND NOT CMAKE_SYSTEM_NAME STREQUAL "tvOS") OR (LINUX AND NOT ANDROID))
|
||||||
set(LIBS ${LIBS} Qt6::Widgets)
|
set(LIBS ${LIBS} Qt6::Widgets)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
@@ -57,7 +59,7 @@ target_include_directories(${PROJECT} PUBLIC
|
|||||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>
|
$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>
|
||||||
)
|
)
|
||||||
|
|
||||||
if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID))
|
if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE AND NOT CMAKE_SYSTEM_NAME STREQUAL "tvOS") OR (LINUX AND NOT ANDROID))
|
||||||
qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_interface.rep)
|
qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_interface.rep)
|
||||||
qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_process_interface.rep)
|
qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_process_interface.rep)
|
||||||
endif()
|
endif()
|
||||||
@@ -79,7 +81,6 @@ set(AMNEZIAVPN_TS_FILES
|
|||||||
)
|
)
|
||||||
|
|
||||||
file(GLOB_RECURSE AMNEZIAVPN_TS_SOURCES *.qrc *.cpp *.h *.ui)
|
file(GLOB_RECURSE AMNEZIAVPN_TS_SOURCES *.qrc *.cpp *.h *.ui)
|
||||||
list(FILTER AMNEZIAVPN_TS_SOURCES EXCLUDE REGEX "qtgamepad/examples")
|
|
||||||
|
|
||||||
qt_create_translation(AMNEZIAVPN_QM_FILES ${AMNEZIAVPN_TS_SOURCES} ${AMNEZIAVPN_TS_FILES})
|
qt_create_translation(AMNEZIAVPN_QM_FILES ${AMNEZIAVPN_TS_SOURCES} ${AMNEZIAVPN_TS_FILES})
|
||||||
|
|
||||||
@@ -110,6 +111,7 @@ include_directories(
|
|||||||
${CMAKE_CURRENT_LIST_DIR}/../ipc
|
${CMAKE_CURRENT_LIST_DIR}/../ipc
|
||||||
${CMAKE_CURRENT_LIST_DIR}/../common/logger
|
${CMAKE_CURRENT_LIST_DIR}/../common/logger
|
||||||
${CMAKE_CURRENT_LIST_DIR}
|
${CMAKE_CURRENT_LIST_DIR}
|
||||||
|
${AMNEZIA_THIRDPARTY_CLIENT_ROOT}
|
||||||
${CMAKE_CURRENT_BINARY_DIR}
|
${CMAKE_CURRENT_BINARY_DIR}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -177,7 +179,7 @@ if(LINUX AND NOT ANDROID)
|
|||||||
link_directories(${CMAKE_CURRENT_LIST_DIR}/platforms/linux)
|
link_directories(${CMAKE_CURRENT_LIST_DIR}/platforms/linux)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID))
|
if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE AND NOT CMAKE_SYSTEM_NAME STREQUAL "tvOS") OR (LINUX AND NOT ANDROID))
|
||||||
add_compile_definitions(AMNEZIA_DESKTOP)
|
add_compile_definitions(AMNEZIA_DESKTOP)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
@@ -185,7 +187,8 @@ if(ANDROID)
|
|||||||
include(cmake/android.cmake)
|
include(cmake/android.cmake)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(IOS)
|
if(IOS OR CMAKE_SYSTEM_NAME STREQUAL "tvOS")
|
||||||
|
option(AMNEZIA_IOS_ENABLE_APPLETV_TARGET "Enable Apple TV target settings for iOS/Xcode projects" OFF)
|
||||||
include(cmake/ios.cmake)
|
include(cmake/ios.cmake)
|
||||||
include(cmake/ios-arch-fixup.cmake)
|
include(cmake/ios-arch-fixup.cmake)
|
||||||
elseif(APPLE AND MACOS_NE)
|
elseif(APPLE AND MACOS_NE)
|
||||||
@@ -198,36 +201,6 @@ endif()
|
|||||||
target_link_libraries(${PROJECT} PRIVATE ${LIBS})
|
target_link_libraries(${PROJECT} PRIVATE ${LIBS})
|
||||||
target_compile_definitions(${PROJECT} PRIVATE "MZ_$<UPPER_CASE:${MZ_PLATFORM_NAME}>")
|
target_compile_definitions(${PROJECT} PRIVATE "MZ_$<UPPER_CASE:${MZ_PLATFORM_NAME}>")
|
||||||
|
|
||||||
# deploy artifacts required to run the application to the debug build folder
|
|
||||||
if(WIN32)
|
|
||||||
if("${CMAKE_SIZEOF_VOID_P}" STREQUAL "8")
|
|
||||||
set(DEPLOY_PLATFORM_PATH "windows/x64")
|
|
||||||
else()
|
|
||||||
set(DEPLOY_PLATFORM_PATH "windows/x32")
|
|
||||||
endif()
|
|
||||||
elseif(LINUX)
|
|
||||||
set(DEPLOY_PLATFORM_PATH "linux/client")
|
|
||||||
elseif(APPLE AND NOT IOS)
|
|
||||||
set(DEPLOY_PLATFORM_PATH "macos")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(NOT IOS AND NOT ANDROID AND NOT MACOS_NE)
|
|
||||||
add_custom_command(
|
|
||||||
TARGET ${PROJECT} POST_BUILD
|
|
||||||
COMMAND ${CMAKE_COMMAND} -E $<IF:$<CONFIG:Debug>,copy_directory,true>
|
|
||||||
${CMAKE_SOURCE_DIR}/deploy/data/${DEPLOY_PLATFORM_PATH}
|
|
||||||
$<TARGET_FILE_DIR:${PROJECT}>
|
|
||||||
COMMAND_EXPAND_LISTS
|
|
||||||
)
|
|
||||||
add_custom_command(
|
|
||||||
TARGET ${PROJECT} POST_BUILD
|
|
||||||
COMMAND ${CMAKE_COMMAND} -E $<IF:$<CONFIG:Debug>,copy_directory,true>
|
|
||||||
${CMAKE_SOURCE_DIR}/client/3rd-prebuilt/deploy-prebuilt/${DEPLOY_PLATFORM_PATH}
|
|
||||||
$<TARGET_FILE_DIR:${PROJECT}>
|
|
||||||
COMMAND_EXPAND_LISTS
|
|
||||||
)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
target_sources(${PROJECT} PRIVATE ${SOURCES} ${HEADERS} ${RESOURCES} ${QRC} ${I18NQRC})
|
target_sources(${PROJECT} PRIVATE ${SOURCES} ${HEADERS} ${RESOURCES} ${QRC} ${I18NQRC})
|
||||||
|
|
||||||
# Finalize the executable so Qt can gather/deploy QML modules and plugins correctly (Android needs this).
|
# Finalize the executable so Qt can gather/deploy QML modules and plugins correctly (Android needs this).
|
||||||
@@ -239,3 +212,29 @@ if(COMMAND qt_finalize_executable)
|
|||||||
else()
|
else()
|
||||||
qt_finalize_target(${PROJECT})
|
qt_finalize_target(${PROJECT})
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if(NOT IOS AND NOT CMAKE_SYSTEM_NAME STREQUAL "tvOS")
|
||||||
|
install(TARGETS ${PROJECT}
|
||||||
|
DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||||
|
COMPONENT AmneziaVPN
|
||||||
|
)
|
||||||
|
install(FILES $<TARGET_RUNTIME_DLLS:${PROJECT}>
|
||||||
|
DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||||
|
COMPONENT AmneziaVPN
|
||||||
|
)
|
||||||
|
|
||||||
|
set(deploy_tool_options "")
|
||||||
|
if(WIN32)
|
||||||
|
set(deploy_tool_options "--force-openssl --force")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
qt_generate_deploy_qml_app_script(
|
||||||
|
TARGET ${PROJECT}
|
||||||
|
OUTPUT_SCRIPT QT_DEPLOY_SCRIPT
|
||||||
|
NO_UNSUPPORTED_PLATFORM_ERROR
|
||||||
|
DEPLOY_TOOL_OPTIONS ${deploy_tool_options}
|
||||||
|
)
|
||||||
|
install(SCRIPT ${QT_DEPLOY_SCRIPT}
|
||||||
|
COMPONENT AmneziaVPN
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|||||||
@@ -109,16 +109,6 @@ void AmneziaApplication::init()
|
|||||||
// install filter on main window
|
// install filter on main window
|
||||||
if (auto win = qobject_cast<QQuickWindow*>(obj)) {
|
if (auto win = qobject_cast<QQuickWindow*>(obj)) {
|
||||||
win->installEventFilter(this);
|
win->installEventFilter(this);
|
||||||
#ifdef Q_OS_ANDROID
|
|
||||||
QObject::connect(win, &QQuickWindow::sceneGraphError,
|
|
||||||
[](QQuickWindow::SceneGraphError, const QString &msg) {
|
|
||||||
qWarning() << "Scene graph error (suppressed):" << msg;
|
|
||||||
});
|
|
||||||
// Keep graphics context alive across hide/show cycles to avoid
|
|
||||||
// eglSwapBuffers/makeCurrent being called on a context Android has reclaimed.
|
|
||||||
win->setPersistentSceneGraph(true);
|
|
||||||
win->setPersistentGraphics(true);
|
|
||||||
#endif
|
|
||||||
win->show();
|
win->show();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -260,7 +250,7 @@ bool AmneziaApplication::parseCommands()
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(Q_OS_TVOS) && !defined(MACOS_NE)
|
||||||
void AmneziaApplication::startLocalServer() {
|
void AmneziaApplication::startLocalServer() {
|
||||||
const QString serverName("AmneziaVPNInstance");
|
const QString serverName("AmneziaVPNInstance");
|
||||||
QLocalServer::removeServer(serverName);
|
QLocalServer::removeServer(serverName);
|
||||||
@@ -281,7 +271,7 @@ void AmneziaApplication::startLocalServer() {
|
|||||||
bool AmneziaApplication::eventFilter(QObject *watched, QEvent *event)
|
bool AmneziaApplication::eventFilter(QObject *watched, QEvent *event)
|
||||||
{
|
{
|
||||||
if (event->type() == QEvent::Close) {
|
if (event->type() == QEvent::Close) {
|
||||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(Q_OS_TVOS)
|
||||||
quit();
|
quit();
|
||||||
#else
|
#else
|
||||||
if (m_forceQuit) {
|
if (m_forceQuit) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
#include <QQmlApplicationEngine>
|
#include <QQmlApplicationEngine>
|
||||||
#include <QQmlContext>
|
#include <QQmlContext>
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(Q_OS_TVOS)
|
||||||
#include <QGuiApplication>
|
#include <QGuiApplication>
|
||||||
#else
|
#else
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
#define amnApp (static_cast<AmneziaApplication *>(QCoreApplication::instance()))
|
#define amnApp (static_cast<AmneziaApplication *>(QCoreApplication::instance()))
|
||||||
|
|
||||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(Q_OS_TVOS)
|
||||||
#define AMNEZIA_BASE_CLASS QGuiApplication
|
#define AMNEZIA_BASE_CLASS QGuiApplication
|
||||||
#else
|
#else
|
||||||
#define AMNEZIA_BASE_CLASS QApplication
|
#define AMNEZIA_BASE_CLASS QApplication
|
||||||
@@ -37,7 +37,7 @@ public:
|
|||||||
void loadFonts();
|
void loadFonts();
|
||||||
bool parseCommands();
|
bool parseCommands();
|
||||||
|
|
||||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(Q_OS_TVOS) && !defined(MACOS_NE)
|
||||||
void startLocalServer();
|
void startLocalServer();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "8.5.2"
|
agp = "8.6.1"
|
||||||
kotlin = "1.9.24"
|
kotlin = "1.9.24"
|
||||||
androidx-core = "1.13.1"
|
androidx-core = "1.13.1"
|
||||||
androidx-activity = "1.9.1"
|
androidx-activity = "1.9.1"
|
||||||
androidx-annotation = "1.8.2"
|
androidx-annotation = "1.8.2"
|
||||||
androidx-biometric = "1.2.0-alpha05"
|
androidx-biometric = "1.2.0-alpha05"
|
||||||
androidx-camera = "1.3.4"
|
androidx-camera = "1.5.3"
|
||||||
androidx-fragment = "1.8.2"
|
androidx-fragment = "1.8.2"
|
||||||
androidx-security-crypto = "1.1.0-alpha06"
|
androidx-security-crypto = "1.1.0-alpha06"
|
||||||
androidx-datastore = "1.1.1"
|
androidx-datastore = "1.1.1"
|
||||||
|
|||||||
@@ -75,8 +75,6 @@ private const val OPEN_FILE_ACTION_CODE = 3
|
|||||||
private const val CHECK_NOTIFICATION_PERMISSION_ACTION_CODE = 4
|
private const val CHECK_NOTIFICATION_PERMISSION_ACTION_CODE = 4
|
||||||
|
|
||||||
private const val PREFS_NOTIFICATION_PERMISSION_ASKED = "NOTIFICATION_PERMISSION_ASKED"
|
private const val PREFS_NOTIFICATION_PERMISSION_ASKED = "NOTIFICATION_PERMISSION_ASKED"
|
||||||
private const val OPEN_FILE_AFTER_RESUME_DELAY_MS = 400L
|
|
||||||
private const val KEY_PENDING_OPEN_FILE_URI = "pending_open_file_uri"
|
|
||||||
|
|
||||||
class AmneziaActivity : QtActivity() {
|
class AmneziaActivity : QtActivity() {
|
||||||
|
|
||||||
@@ -92,12 +90,10 @@ class AmneziaActivity : QtActivity() {
|
|||||||
|
|
||||||
private val actionResultHandlers = mutableMapOf<Int, ActivityResultHandler>()
|
private val actionResultHandlers = mutableMapOf<Int, ActivityResultHandler>()
|
||||||
private val permissionRequestHandlers = mutableMapOf<Int, PermissionRequestHandler>()
|
private val permissionRequestHandlers = mutableMapOf<Int, PermissionRequestHandler>()
|
||||||
|
|
||||||
private var isActivityResumed = false
|
private var isActivityResumed = false
|
||||||
private var hasWindowFocus = false
|
private var hasWindowFocus = false
|
||||||
private val resumeHandler = Handler(Looper.getMainLooper())
|
private val resumeHandler = Handler(Looper.getMainLooper())
|
||||||
private var pendingOpenFileUri: String? = null
|
|
||||||
private var openFileDeliveryScheduled = false
|
|
||||||
|
|
||||||
private val vpnServiceEventHandler: Handler by lazy(NONE) {
|
private val vpnServiceEventHandler: Handler by lazy(NONE) {
|
||||||
object : Handler(Looper.getMainLooper()) {
|
object : Handler(Looper.getMainLooper()) {
|
||||||
@@ -200,24 +196,14 @@ class AmneziaActivity : QtActivity() {
|
|||||||
doBindService()
|
doBindService()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
pendingOpenFileUri = savedInstanceState?.getString(KEY_PENDING_OPEN_FILE_URI)
|
|
||||||
openFileDeliveryScheduled = false
|
|
||||||
registerBroadcastReceivers()
|
registerBroadcastReceivers()
|
||||||
intent?.let(::processIntent)
|
intent?.let(::processIntent)
|
||||||
runBlocking { vpnProto = proto.await() }
|
runBlocking { vpnProto = proto.await() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
pendingOpenFileUri?.let { outState.putString(KEY_PENDING_OPEN_FILE_URI, it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadLibs() {
|
private fun loadLibs() {
|
||||||
listOf(
|
listOf(
|
||||||
"rsapss",
|
"rsapss"
|
||||||
"crypto_3",
|
|
||||||
"ssl_3",
|
|
||||||
"ssh"
|
|
||||||
).forEach {
|
).forEach {
|
||||||
loadSharedLibrary(this.applicationContext, it)
|
loadSharedLibrary(this.applicationContext, it)
|
||||||
}
|
}
|
||||||
@@ -281,7 +267,6 @@ class AmneziaActivity : QtActivity() {
|
|||||||
hasWindowFocus = false
|
hasWindowFocus = false
|
||||||
// Cancel all pending operations when activity stops
|
// Cancel all pending operations when activity stops
|
||||||
resumeHandler.removeCallbacksAndMessages(null)
|
resumeHandler.removeCallbacksAndMessages(null)
|
||||||
openFileDeliveryScheduled = false
|
|
||||||
Log.d(TAG, "Stop Amnezia activity")
|
Log.d(TAG, "Stop Amnezia activity")
|
||||||
doUnbindService()
|
doUnbindService()
|
||||||
mainScope.launch {
|
mainScope.launch {
|
||||||
@@ -295,55 +280,43 @@ class AmneziaActivity : QtActivity() {
|
|||||||
super.onWindowFocusChanged(hasFocus)
|
super.onWindowFocusChanged(hasFocus)
|
||||||
hasWindowFocus = hasFocus
|
hasWindowFocus = hasFocus
|
||||||
Log.d(TAG, "Window focus changed: hasFocus=$hasFocus")
|
Log.d(TAG, "Window focus changed: hasFocus=$hasFocus")
|
||||||
|
|
||||||
|
// Cancel pending operations if window loses focus
|
||||||
if (!hasFocus) {
|
if (!hasFocus) {
|
||||||
// Cancel pending operations if window loses focus
|
|
||||||
resumeHandler.removeCallbacksAndMessages(null)
|
resumeHandler.removeCallbacksAndMessages(null)
|
||||||
} else if (isActivityResumed && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
||||||
window.decorView.apply {
|
|
||||||
invalidate()
|
|
||||||
resumeHandler.postDelayed({
|
|
||||||
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
|
|
||||||
sendTouch(1f, 1f)
|
|
||||||
}
|
|
||||||
}, 50)
|
|
||||||
resumeHandler.postDelayed({
|
|
||||||
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
|
|
||||||
sendTouch(2f, 2f)
|
|
||||||
requestLayout()
|
|
||||||
invalidate()
|
|
||||||
}
|
|
||||||
}, 150)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||||
|
val deviceId = event.deviceId
|
||||||
val keyCode = event.keyCode
|
val keyCode = event.keyCode
|
||||||
val pressed = event.action == KeyEvent.ACTION_DOWN
|
val pressed = event.action == KeyEvent.ACTION_DOWN
|
||||||
|
val source = event.source
|
||||||
|
|
||||||
when (keyCode) {
|
if (deviceId < 0 && pressed) {
|
||||||
KeyEvent.KEYCODE_BUTTON_A,
|
when (keyCode) {
|
||||||
KeyEvent.KEYCODE_BUTTON_B,
|
KeyEvent.KEYCODE_BUTTON_A,
|
||||||
KeyEvent.KEYCODE_BUTTON_X,
|
KeyEvent.KEYCODE_BUTTON_B,
|
||||||
KeyEvent.KEYCODE_BUTTON_Y,
|
KeyEvent.KEYCODE_BUTTON_X,
|
||||||
KeyEvent.KEYCODE_BUTTON_START,
|
KeyEvent.KEYCODE_BUTTON_Y,
|
||||||
KeyEvent.KEYCODE_BUTTON_SELECT -> {
|
KeyEvent.KEYCODE_BUTTON_START,
|
||||||
nativeGamepadKeyEvent(0, keyCode, pressed)
|
KeyEvent.KEYCODE_BUTTON_SELECT,
|
||||||
|
KeyEvent.KEYCODE_DPAD_CENTER -> {
|
||||||
|
nativeGamepadKeyEvent(0, keyCode, true)
|
||||||
|
nativeGamepadKeyEvent(0, keyCode, false)
|
||||||
return true
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
KeyEvent.KEYCODE_DPAD_CENTER,
|
}
|
||||||
KeyEvent.KEYCODE_DPAD_UP,
|
|
||||||
KeyEvent.KEYCODE_DPAD_DOWN,
|
// Real gamepad events (deviceId >= 0)
|
||||||
KeyEvent.KEYCODE_DPAD_LEFT,
|
if (deviceId >= 0) {
|
||||||
KeyEvent.KEYCODE_DPAD_RIGHT -> {
|
val isGamepad = (source and InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD
|
||||||
val syntheticKeyCode = if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) KeyEvent.KEYCODE_ENTER else keyCode
|
val isJoystick = (source and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK
|
||||||
val synthetic = KeyEvent(
|
val isDpad = (source and InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD
|
||||||
event.downTime, event.eventTime, event.action, syntheticKeyCode,
|
if (isGamepad || isJoystick || isDpad) {
|
||||||
event.repeatCount, event.metaState, -1, event.scanCode,
|
nativeGamepadKeyEvent(deviceId, keyCode, pressed)
|
||||||
event.flags, InputDevice.SOURCE_KEYBOARD
|
return true
|
||||||
)
|
|
||||||
return super.dispatchKeyEvent(synthetic)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,18 +326,10 @@ class AmneziaActivity : QtActivity() {
|
|||||||
private external fun nativeGamepadKeyEvent(deviceId: Int, keyCode: Int, pressed: Boolean)
|
private external fun nativeGamepadKeyEvent(deviceId: Int, keyCode: Int, pressed: Boolean)
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
// Notify Qt to stop rendering BEFORE super.onPause() destroys the EGL surface.
|
|
||||||
// Using a coroutine here would be too late — the surface is gone by the time
|
|
||||||
// the coroutine runs. A direct synchronous call gives Qt's render thread the
|
|
||||||
// best chance to process visible=false before surface destruction.
|
|
||||||
if (qtInitialized.isCompleted) {
|
|
||||||
QtAndroidController.onActivityPaused()
|
|
||||||
}
|
|
||||||
super.onPause()
|
super.onPause()
|
||||||
isActivityResumed = false
|
isActivityResumed = false
|
||||||
// Cancel all pending operations when activity pauses
|
// Cancel all pending operations when activity pauses
|
||||||
resumeHandler.removeCallbacksAndMessages(null)
|
resumeHandler.removeCallbacksAndMessages(null)
|
||||||
openFileDeliveryScheduled = false
|
|
||||||
Log.d(TAG, "Pause Amnezia activity")
|
Log.d(TAG, "Pause Amnezia activity")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,24 +337,6 @@ class AmneziaActivity : QtActivity() {
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
isActivityResumed = true
|
isActivityResumed = true
|
||||||
Log.d(TAG, "Resume Amnezia activity")
|
Log.d(TAG, "Resume Amnezia activity")
|
||||||
if (qtInitialized.isCompleted) {
|
|
||||||
QtAndroidController.onActivityResumed()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pendingOpenFileUri != null && !openFileDeliveryScheduled) {
|
|
||||||
val uri = pendingOpenFileUri!!
|
|
||||||
openFileDeliveryScheduled = true
|
|
||||||
resumeHandler.postDelayed({
|
|
||||||
if (!isFinishing && !isDestroyed) {
|
|
||||||
pendingOpenFileUri = null
|
|
||||||
openFileDeliveryScheduled = false
|
|
||||||
mainScope.launch {
|
|
||||||
qtInitialized.await()
|
|
||||||
QtAndroidController.onFileOpened(uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, OPEN_FILE_AFTER_RESUME_DELAY_MS)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
window.decorView.apply {
|
window.decorView.apply {
|
||||||
@@ -401,13 +348,13 @@ class AmneziaActivity : QtActivity() {
|
|||||||
sendTouch(1f, 1f)
|
sendTouch(1f, 1f)
|
||||||
}
|
}
|
||||||
}, 100)
|
}, 100)
|
||||||
|
|
||||||
resumeHandler.postDelayed({
|
resumeHandler.postDelayed({
|
||||||
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
|
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
|
||||||
sendTouch(2f, 2f)
|
sendTouch(2f, 2f)
|
||||||
}
|
}
|
||||||
}, 200)
|
}, 200)
|
||||||
|
|
||||||
resumeHandler.postDelayed({
|
resumeHandler.postDelayed({
|
||||||
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
|
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
|
||||||
requestLayout()
|
requestLayout()
|
||||||
@@ -453,25 +400,25 @@ class AmneziaActivity : QtActivity() {
|
|||||||
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { view, windowInsets ->
|
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { view, windowInsets ->
|
||||||
val imeInsets = windowInsets.getInsets(WindowInsetsCompat.Type.ime())
|
val imeInsets = windowInsets.getInsets(WindowInsetsCompat.Type.ime())
|
||||||
val imeVisible = windowInsets.isVisible(WindowInsetsCompat.Type.ime())
|
val imeVisible = windowInsets.isVisible(WindowInsetsCompat.Type.ime())
|
||||||
|
|
||||||
val imeHeight = if (imeVisible) imeInsets.bottom else 0
|
val imeHeight = if (imeVisible) imeInsets.bottom else 0
|
||||||
|
|
||||||
val density = resources.displayMetrics.density
|
val density = resources.displayMetrics.density
|
||||||
val imeHeightDp = (imeHeight / density).toInt()
|
val imeHeightDp = (imeHeight / density).toInt()
|
||||||
|
|
||||||
// Also track system bars (navigation bar, status bar) changes
|
// Also track system bars (navigation bar, status bar) changes
|
||||||
val systemBarsInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
val systemBarsInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
val navBarHeight = systemBarsInsets.bottom
|
val navBarHeight = systemBarsInsets.bottom
|
||||||
val navBarHeightDp = (navBarHeight / density).toInt()
|
val navBarHeightDp = (navBarHeight / density).toInt()
|
||||||
val statusBarHeight = systemBarsInsets.top
|
val statusBarHeight = systemBarsInsets.top
|
||||||
val statusBarHeightDp = (statusBarHeight / density).toInt()
|
val statusBarHeightDp = (statusBarHeight / density).toInt()
|
||||||
|
|
||||||
mainScope.launch {
|
mainScope.launch {
|
||||||
qtInitialized.await()
|
qtInitialized.await()
|
||||||
QtAndroidController.onImeInsetsChanged(imeHeightDp)
|
QtAndroidController.onImeInsetsChanged(imeHeightDp)
|
||||||
QtAndroidController.onSystemBarsInsetsChanged(navBarHeightDp, statusBarHeightDp)
|
QtAndroidController.onSystemBarsInsetsChanged(navBarHeightDp, statusBarHeightDp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return windowInsets instead of CONSUMED to allow proper handling
|
// Return windowInsets instead of CONSUMED to allow proper handling
|
||||||
windowInsets
|
windowInsets
|
||||||
}
|
}
|
||||||
@@ -792,16 +739,6 @@ class AmneziaActivity : QtActivity() {
|
|||||||
else -> type = "*/*"
|
else -> type = "*/*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Force system document picker to avoid third-party file managers
|
|
||||||
// that may lack storage permissions (common on Android TV devices)
|
|
||||||
val systemPickerPackage = listOf("com.google.android.documentsui", "com.android.documentsui")
|
|
||||||
.firstOrNull { pkg ->
|
|
||||||
try { packageManager.getPackageInfo(pkg, 0); true }
|
|
||||||
catch (_: PackageManager.NameNotFoundException) { false }
|
|
||||||
}
|
|
||||||
if (systemPickerPackage != null) {
|
|
||||||
`package` = systemPickerPackage
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Intent(this@AmneziaActivity, TvFilePicker::class.java)
|
Intent(this@AmneziaActivity, TvFilePicker::class.java)
|
||||||
@@ -817,13 +754,9 @@ class AmneziaActivity : QtActivity() {
|
|||||||
grantUriPermission(packageName, this, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
grantUriPermission(packageName, this, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
}?.toString() ?: ""
|
}?.toString() ?: ""
|
||||||
Log.v(TAG, "Open file: $uri")
|
Log.v(TAG, "Open file: $uri")
|
||||||
if (uri.isNotEmpty()) {
|
mainScope.launch {
|
||||||
pendingOpenFileUri = uri
|
qtInitialized.await()
|
||||||
} else {
|
QtAndroidController.onFileOpened(uri)
|
||||||
mainScope.launch {
|
|
||||||
qtInitialized.await()
|
|
||||||
QtAndroidController.onFileOpened(uri)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
@@ -852,7 +785,7 @@ class AmneziaActivity : QtActivity() {
|
|||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
fun getFd(fileName: String): Int {
|
fun getFd(fileName: String): Int {
|
||||||
Log.v(TAG, "Get fd for $fileName")
|
Log.v(TAG, "Get fd for $fileName")
|
||||||
return blockingCall(Dispatchers.IO) {
|
return blockingCall {
|
||||||
try {
|
try {
|
||||||
pfd = contentResolver.openFileDescriptor(Uri.parse(fileName), "r")
|
pfd = contentResolver.openFileDescriptor(Uri.parse(fileName), "r")
|
||||||
pfd?.fd ?: -1
|
pfd?.fd ?: -1
|
||||||
@@ -1074,11 +1007,13 @@ class AmneziaActivity : QtActivity() {
|
|||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
fun sendTouch(x: Float, y: Float) {
|
fun sendTouch(x: Float, y: Float) {
|
||||||
Log.v(TAG, "Send touch: $x, $y")
|
Log.v(TAG, "Send touch: $x, $y")
|
||||||
|
blockingCall {
|
||||||
findQtWindow(window.decorView)?.let {
|
findQtWindow(window.decorView)?.let {
|
||||||
Log.v(TAG, "Send touch to $it")
|
Log.v(TAG, "Send touch to $it")
|
||||||
it.dispatchTouchEvent(createEvent(x, y, SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN))
|
it.dispatchTouchEvent(createEvent(x, y, SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN))
|
||||||
it.dispatchTouchEvent(createEvent(x, y, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP))
|
it.dispatchTouchEvent(createEvent(x, y, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findQtWindow(view: View): View? {
|
private fun findQtWindow(view: View): View? {
|
||||||
|
|||||||
@@ -33,10 +33,7 @@ class TvFilePicker : ComponentActivity() {
|
|||||||
return intent
|
return intent
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
setResult(RESULT_OK, Intent().apply {
|
setResult(RESULT_OK, Intent().apply { data = it })
|
||||||
data = it
|
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
})
|
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,4 @@ object QtAndroidController {
|
|||||||
|
|
||||||
external fun onImeInsetsChanged(heightDp: Int)
|
external fun onImeInsetsChanged(heightDp: Int)
|
||||||
external fun onSystemBarsInsetsChanged(navBarHeightDp: Int, statusBarHeightDp: Int)
|
external fun onSystemBarsInsetsChanged(navBarHeightDp: Int, statusBarHeightDp: Int)
|
||||||
|
|
||||||
external fun onActivityPaused()
|
|
||||||
external fun onActivityResumed()
|
|
||||||
}
|
}
|
||||||
@@ -4,9 +4,6 @@ import android.content.Context
|
|||||||
import android.net.VpnService.Builder
|
import android.net.VpnService.Builder
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.net.InetAddress
|
|
||||||
import java.net.ServerSocket
|
|
||||||
import java.util.UUID
|
|
||||||
import go.Seq
|
import go.Seq
|
||||||
import org.amnezia.vpn.protocol.BadConfigException
|
import org.amnezia.vpn.protocol.BadConfigException
|
||||||
import org.amnezia.vpn.protocol.Protocol
|
import org.amnezia.vpn.protocol.Protocol
|
||||||
@@ -22,32 +19,11 @@ import org.amnezia.vpn.util.Log
|
|||||||
import org.amnezia.vpn.util.net.InetNetwork
|
import org.amnezia.vpn.util.net.InetNetwork
|
||||||
import org.amnezia.vpn.util.net.ip
|
import org.amnezia.vpn.util.net.ip
|
||||||
import org.amnezia.vpn.util.net.parseInetAddress
|
import org.amnezia.vpn.util.net.parseInetAddress
|
||||||
import org.json.JSONArray
|
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
|
||||||
private const val TAG = "Xray"
|
private const val TAG = "Xray"
|
||||||
private const val LIBXRAY_TAG = "libXray"
|
private const val LIBXRAY_TAG = "libXray"
|
||||||
|
|
||||||
private fun findSocksInboundIndex(inbounds: JSONArray): Int {
|
|
||||||
for (i in 0 until inbounds.length()) {
|
|
||||||
val o = inbounds.optJSONObject(i) ?: continue
|
|
||||||
if (o.optString("protocol").equals("socks", ignoreCase = true)) {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun acquireFreeLocalPort(): Int {
|
|
||||||
try {
|
|
||||||
ServerSocket(0, 1, InetAddress.getByName("127.0.0.1")).use { return it.localPort }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
throw VpnStartException(
|
|
||||||
"Failed to acquire free TCP port on 127.0.0.1 for SOCKS inbound: ${e.message}"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Xray : Protocol() {
|
class Xray : Protocol() {
|
||||||
|
|
||||||
private var isRunning: Boolean = false
|
private var isRunning: Boolean = false
|
||||||
@@ -80,10 +56,6 @@ class Xray : Protocol() {
|
|||||||
val xrayJsonConfig = config.optJSONObject("xray_config_data")
|
val xrayJsonConfig = config.optJSONObject("xray_config_data")
|
||||||
?: config.optJSONObject("ssxray_config_data")
|
?: config.optJSONObject("ssxray_config_data")
|
||||||
?: throw BadConfigException("config_data not found")
|
?: throw BadConfigException("config_data not found")
|
||||||
|
|
||||||
// Inject SOCKS5 auth before starting xray. Re-uses existing credentials if present.
|
|
||||||
ensureInboundAuth(xrayJsonConfig)
|
|
||||||
|
|
||||||
val xrayConfig = parseConfig(config, xrayJsonConfig)
|
val xrayConfig = parseConfig(config, xrayJsonConfig)
|
||||||
|
|
||||||
(xrayJsonConfig.optJSONObject("log") ?: JSONObject().also { xrayJsonConfig.put("log", it) })
|
(xrayJsonConfig.optJSONObject("log") ?: JSONObject().also { xrayJsonConfig.put("log", it) })
|
||||||
@@ -125,22 +97,9 @@ class Xray : Protocol() {
|
|||||||
if (it.isNotBlank()) setMtu(it.toInt())
|
if (it.isNotBlank()) setMtu(it.toInt())
|
||||||
}
|
}
|
||||||
|
|
||||||
val inbounds = xrayJsonConfig.getJSONArray("inbounds")
|
val socksConfig = xrayJsonConfig.getJSONArray("inbounds")[0] as JSONObject
|
||||||
val socksIdx = findSocksInboundIndex(inbounds)
|
|
||||||
if (socksIdx < 0) {
|
|
||||||
throw BadConfigException("socks inbound not found")
|
|
||||||
}
|
|
||||||
val socksConfig = inbounds.getJSONObject(socksIdx)
|
|
||||||
socksConfig.getInt("port").let { setSocksPort(it) }
|
socksConfig.getInt("port").let { setSocksPort(it) }
|
||||||
|
|
||||||
val socksSettings = socksConfig.optJSONObject("settings")
|
|
||||||
val accounts = socksSettings?.optJSONArray("accounts")
|
|
||||||
if (accounts != null && accounts.length() > 0) {
|
|
||||||
val account = accounts.getJSONObject(0)
|
|
||||||
setSocksUser(account.optString("user"))
|
|
||||||
setSocksPass(account.optString("pass"))
|
|
||||||
}
|
|
||||||
|
|
||||||
configSplitTunneling(config)
|
configSplitTunneling(config)
|
||||||
configAppSplitTunneling(config)
|
configAppSplitTunneling(config)
|
||||||
}
|
}
|
||||||
@@ -203,10 +162,9 @@ class Xray : Protocol() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun runTun2Socks(config: XrayConfig, fd: Int) {
|
private fun runTun2Socks(config: XrayConfig, fd: Int) {
|
||||||
val proxyUrl = "socks5://${config.socksUser}:${config.socksPass}@127.0.0.1:${config.socksPort}"
|
|
||||||
val tun2SocksConfig = Tun2SocksConfig().apply {
|
val tun2SocksConfig = Tun2SocksConfig().apply {
|
||||||
mtu = config.mtu.toLong()
|
mtu = config.mtu.toLong()
|
||||||
proxy = proxyUrl
|
proxy = "socks5://127.0.0.1:${config.socksPort}"
|
||||||
device = "fd://$fd"
|
device = "fd://$fd"
|
||||||
logLevel = "warn"
|
logLevel = "warn"
|
||||||
}
|
}
|
||||||
@@ -215,37 +173,6 @@ class Xray : Protocol() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensures SOCKS5 auth is present on the socks inbound settings.
|
|
||||||
// Re-uses existing credentials if already configured; otherwise generates random ones.
|
|
||||||
private fun ensureInboundAuth(xrayConfig: JSONObject) {
|
|
||||||
val inbounds = xrayConfig.optJSONArray("inbounds") ?: return
|
|
||||||
val socksIdx = findSocksInboundIndex(inbounds)
|
|
||||||
if (socksIdx < 0) return
|
|
||||||
|
|
||||||
val inbound = inbounds.getJSONObject(socksIdx)
|
|
||||||
inbound.put("port", acquireFreeLocalPort())
|
|
||||||
val settings = inbound.optJSONObject("settings") ?: JSONObject().also { inbound.put("settings", it) }
|
|
||||||
val accounts = settings.optJSONArray("accounts")
|
|
||||||
if (accounts != null && accounts.length() > 0) {
|
|
||||||
val account = accounts.getJSONObject(0)
|
|
||||||
if (account.optString("user").isNotEmpty() && account.optString("pass").isNotEmpty()) {
|
|
||||||
// Ensure auth mode is enforced even for imported configs that had accounts
|
|
||||||
// but auth: "noauth" (or no auth field).
|
|
||||||
settings.put("auth", "password")
|
|
||||||
inbound.put("settings", settings)
|
|
||||||
inbounds.put(socksIdx, inbound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val user = UUID.randomUUID().toString().replace("-", "").substring(0, 16)
|
|
||||||
val pass = UUID.randomUUID().toString().replace("-", "")
|
|
||||||
settings.put("auth", "password")
|
|
||||||
settings.put("accounts", JSONArray().put(JSONObject().put("user", user).put("pass", pass)))
|
|
||||||
inbound.put("settings", settings)
|
|
||||||
inbounds.put(socksIdx, inbound)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val instance: Xray by lazy { Xray() }
|
val instance: Xray by lazy { Xray() }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,16 +9,12 @@ private const val XRAY_DEFAULT_MAX_MEMORY: Long = 50 shl 20 // 50 MB
|
|||||||
class XrayConfig protected constructor(
|
class XrayConfig protected constructor(
|
||||||
protocolConfigBuilder: ProtocolConfig.Builder,
|
protocolConfigBuilder: ProtocolConfig.Builder,
|
||||||
val socksPort: Int,
|
val socksPort: Int,
|
||||||
val socksUser: String,
|
|
||||||
val socksPass: String,
|
|
||||||
val maxMemory: Long,
|
val maxMemory: Long,
|
||||||
) : ProtocolConfig(protocolConfigBuilder) {
|
) : ProtocolConfig(protocolConfigBuilder) {
|
||||||
|
|
||||||
protected constructor(builder: Builder) : this(
|
protected constructor(builder: Builder) : this(
|
||||||
builder,
|
builder,
|
||||||
builder.socksPort,
|
builder.socksPort,
|
||||||
builder.socksUser,
|
|
||||||
builder.socksPass,
|
|
||||||
builder.maxMemory
|
builder.maxMemory
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -26,12 +22,6 @@ class XrayConfig protected constructor(
|
|||||||
internal var socksPort: Int = 0
|
internal var socksPort: Int = 0
|
||||||
private set
|
private set
|
||||||
|
|
||||||
internal var socksUser: String = ""
|
|
||||||
private set
|
|
||||||
|
|
||||||
internal var socksPass: String = ""
|
|
||||||
private set
|
|
||||||
|
|
||||||
internal var maxMemory: Long = XRAY_DEFAULT_MAX_MEMORY
|
internal var maxMemory: Long = XRAY_DEFAULT_MAX_MEMORY
|
||||||
private set
|
private set
|
||||||
|
|
||||||
@@ -39,10 +29,6 @@ class XrayConfig protected constructor(
|
|||||||
|
|
||||||
fun setSocksPort(port: Int) = apply { socksPort = port }
|
fun setSocksPort(port: Int) = apply { socksPort = port }
|
||||||
|
|
||||||
fun setSocksUser(user: String) = apply { socksUser = user }
|
|
||||||
|
|
||||||
fun setSocksPass(pass: String) = apply { socksPass = pass }
|
|
||||||
|
|
||||||
fun setMaxMemory(maxMemory: Long) = apply { this.maxMemory = maxMemory }
|
fun setMaxMemory(maxMemory: Long) = apply { this.maxMemory = maxMemory }
|
||||||
|
|
||||||
override fun build(): XrayConfig = configBuild().run { XrayConfig(this@Builder) }
|
override fun build(): XrayConfig = configBuild().run { XrayConfig(this@Builder) }
|
||||||
|
|||||||
+12
-80
@@ -1,88 +1,19 @@
|
|||||||
set(CLIENT_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR}/..)
|
set(CLIENT_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR}/..)
|
||||||
|
set(AMNEZIA_THIRDPARTY_ROOT "${CLIENT_ROOT_DIR}/3rd" CACHE PATH "Path to Amnezia client/3rd sources")
|
||||||
|
|
||||||
set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/Modules;${CMAKE_MODULE_PATH}")
|
set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/Modules;${CMAKE_MODULE_PATH}")
|
||||||
|
|
||||||
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/SortFilterProxyModel)
|
add_subdirectory(${AMNEZIA_THIRDPARTY_ROOT}/SortFilterProxyModel ${CMAKE_CURRENT_BINARY_DIR}/3rd/SortFilterProxyModel)
|
||||||
set(LIBS ${LIBS} SortFilterProxyModel)
|
set(LIBS ${LIBS} SortFilterProxyModel)
|
||||||
include(${CLIENT_ROOT_DIR}/cmake/QSimpleCrypto.cmake)
|
include(${CLIENT_ROOT_DIR}/cmake/QSimpleCrypto.cmake)
|
||||||
|
|
||||||
include(${CLIENT_ROOT_DIR}/3rd/qrcodegen/qrcodegen.cmake)
|
include(${AMNEZIA_THIRDPARTY_ROOT}/qrcodegen/qrcodegen.cmake)
|
||||||
|
|
||||||
set(LIBSSH_ROOT_DIR "${CLIENT_ROOT_DIR}/3rd-prebuilt/3rd-prebuilt/libssh/")
|
|
||||||
set(OPENSSL_ROOT_DIR "${CLIENT_ROOT_DIR}/3rd-prebuilt/3rd-prebuilt/openssl/")
|
|
||||||
|
|
||||||
set(OPENSSL_LIBRARIES_DIR "${OPENSSL_ROOT_DIR}/lib")
|
|
||||||
|
|
||||||
if(WIN32)
|
|
||||||
set(OPENSSL_INCLUDE_DIR "${OPENSSL_ROOT_DIR}/windows/include")
|
|
||||||
if("${CMAKE_SIZEOF_VOID_P}" STREQUAL "8")
|
|
||||||
set(LIBSSH_LIB_PATH "${LIBSSH_ROOT_DIR}/windows/x86_64/ssh.lib")
|
|
||||||
set(LIBSSH_INCLUDE_DIR "${LIBSSH_ROOT_DIR}/windows/x86_64")
|
|
||||||
set(OPENSSL_LIB_SSL_PATH "${OPENSSL_ROOT_DIR}/windows/win64/libssl.lib")
|
|
||||||
set(OPENSSL_LIB_CRYPTO_PATH "${OPENSSL_ROOT_DIR}/windows/win64/libcrypto.lib")
|
|
||||||
else()
|
|
||||||
set(LIBSSH_LIB_PATH "${LIBSSH_ROOT_DIR}/windows/x86/ssh.lib")
|
|
||||||
set(LIBSSH_INCLUDE_DIR "${LIBSSH_ROOT_DIR}/windows/x86")
|
|
||||||
set(OPENSSL_LIB_SSL_PATH "${OPENSSL_ROOT_DIR}/windows/win32/libssl.lib")
|
|
||||||
set(OPENSSL_LIB_CRYPTO_PATH "${OPENSSL_ROOT_DIR}/windows/win32/libcrypto.lib")
|
|
||||||
endif()
|
|
||||||
elseif(APPLE AND NOT IOS)
|
|
||||||
if(MACOS_NE)
|
|
||||||
set(LIBSSH_LIB_PATH "${LIBSSH_ROOT_DIR}/macos/universal2/libssh.a")
|
|
||||||
set(ZLIB_LIB_PATH "${LIBSSH_ROOT_DIR}/macos/universal2/libz.a")
|
|
||||||
set(LIBSSH_INCLUDE_DIR "${LIBSSH_ROOT_DIR}/macos/universal2")
|
|
||||||
else()
|
|
||||||
set(LIBSSH_LIB_PATH "${LIBSSH_ROOT_DIR}/macos/x86_64/libssh.a")
|
|
||||||
set(ZLIB_LIB_PATH "${LIBSSH_ROOT_DIR}/macos/x86_64/libz.a")
|
|
||||||
set(LIBSSH_INCLUDE_DIR "${LIBSSH_ROOT_DIR}/macos/x86_64")
|
|
||||||
endif()
|
|
||||||
set(OPENSSL_INCLUDE_DIR "${OPENSSL_ROOT_DIR}/macos/include")
|
|
||||||
set(OPENSSL_LIB_SSL_PATH "${OPENSSL_ROOT_DIR}/macos/lib/libssl.a")
|
|
||||||
set(OPENSSL_LIB_CRYPTO_PATH "${OPENSSL_ROOT_DIR}/macos/lib/libcrypto.a")
|
|
||||||
elseif(IOS)
|
|
||||||
set(LIBSSH_INCLUDE_DIR "${LIBSSH_ROOT_DIR}/ios/arm64")
|
|
||||||
set(LIBSSH_LIB_PATH "${LIBSSH_ROOT_DIR}/ios/arm64/libssh.a")
|
|
||||||
set(ZLIB_LIB_PATH "${LIBSSH_ROOT_DIR}/ios/arm64/libz.a")
|
|
||||||
set(OPENSSL_INCLUDE_DIR "${OPENSSL_ROOT_DIR}/ios/iphone/include")
|
|
||||||
set(OPENSSL_LIB_SSL_PATH "${OPENSSL_ROOT_DIR}/ios/iphone/lib/libssl.a")
|
|
||||||
set(OPENSSL_LIB_CRYPTO_PATH "${OPENSSL_ROOT_DIR}/ios/iphone/lib/libcrypto.a")
|
|
||||||
elseif(ANDROID)
|
|
||||||
set(abi ${CMAKE_ANDROID_ARCH_ABI})
|
|
||||||
set(LIBSSH_INCLUDE_DIR "${LIBSSH_ROOT_DIR}/android/${abi}")
|
|
||||||
set(LIBSSH_LIB_PATH "${LIBSSH_ROOT_DIR}/android/${abi}/libssh.so")
|
|
||||||
set(OPENSSL_INCLUDE_DIR "${OPENSSL_ROOT_DIR}/android/include")
|
|
||||||
set(OPENSSL_LIB_SSL_PATH "${OPENSSL_ROOT_DIR}/android/${abi}/libssl.a")
|
|
||||||
set(OPENSSL_LIB_CRYPTO_PATH "${OPENSSL_ROOT_DIR}/android/${abi}/libcrypto.a")
|
|
||||||
set(OPENSSL_LIBRARIES_DIR "${OPENSSL_ROOT_DIR}/android/${abi}")
|
|
||||||
elseif(LINUX)
|
|
||||||
set(LIBSSH_INCLUDE_DIR "${LIBSSH_ROOT_DIR}/linux/x86_64")
|
|
||||||
set(ZLIB_LIB_PATH "${LIBSSH_ROOT_DIR}/linux/x86_64/libz.a")
|
|
||||||
set(LIBSSH_LIB_PATH "${LIBSSH_ROOT_DIR}/linux/x86_64/libssh.a")
|
|
||||||
set(OPENSSL_INCLUDE_DIR "${OPENSSL_ROOT_DIR}/linux/include")
|
|
||||||
set(OPENSSL_LIB_SSL_PATH "${OPENSSL_ROOT_DIR}/linux/x86_64/libssl.a")
|
|
||||||
set(OPENSSL_LIB_CRYPTO_PATH "${OPENSSL_ROOT_DIR}/linux/x86_64/libcrypto.a")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
file(COPY ${OPENSSL_LIB_SSL_PATH} ${OPENSSL_LIB_CRYPTO_PATH}
|
|
||||||
DESTINATION ${OPENSSL_LIBRARIES_DIR})
|
|
||||||
|
|
||||||
set(OPENSSL_USE_STATIC_LIBS TRUE)
|
|
||||||
|
|
||||||
set(LIBS ${LIBS}
|
|
||||||
${LIBSSH_LIB_PATH}
|
|
||||||
${ZLIB_LIB_PATH}
|
|
||||||
)
|
|
||||||
|
|
||||||
set(LIBS ${LIBS}
|
|
||||||
${OPENSSL_LIB_SSL_PATH}
|
|
||||||
${OPENSSL_LIB_CRYPTO_PATH}
|
|
||||||
)
|
|
||||||
|
|
||||||
add_compile_definitions(_WINSOCKAPI_)
|
add_compile_definitions(_WINSOCKAPI_)
|
||||||
|
|
||||||
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
|
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
|
||||||
set(BUILD_WITH_QT6 ON)
|
set(BUILD_WITH_QT6 ON)
|
||||||
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/qtkeychain)
|
add_subdirectory(${AMNEZIA_THIRDPARTY_ROOT}/qtkeychain ${CMAKE_CURRENT_BINARY_DIR}/3rd/qtkeychain EXCLUDE_FROM_ALL)
|
||||||
|
|
||||||
if(ANDROID)
|
if(ANDROID)
|
||||||
# Use qtgamepad from amnezia-vpn/qtgamepad repository
|
# Use qtgamepad from amnezia-vpn/qtgamepad repository
|
||||||
@@ -106,12 +37,13 @@ endif()
|
|||||||
set(LIBS ${LIBS} qt6keychain)
|
set(LIBS ${LIBS} qt6keychain)
|
||||||
|
|
||||||
include_directories(
|
include_directories(
|
||||||
${OPENSSL_INCLUDE_DIR}
|
${AMNEZIA_THIRDPARTY_ROOT}/QSimpleCrypto/src/include
|
||||||
${LIBSSH_INCLUDE_DIR}/include
|
${AMNEZIA_THIRDPARTY_ROOT}/qtkeychain/qtkeychain
|
||||||
${LIBSSH_ROOT_DIR}/include
|
|
||||||
${CLIENT_ROOT_DIR}/3rd/libssh/include
|
|
||||||
${CLIENT_ROOT_DIR}/3rd/QSimpleCrypto/src/include
|
|
||||||
${CLIENT_ROOT_DIR}/3rd/qtkeychain/qtkeychain
|
|
||||||
${CMAKE_CURRENT_BINARY_DIR}/3rd/qtkeychain
|
${CMAKE_CURRENT_BINARY_DIR}/3rd/qtkeychain
|
||||||
${CMAKE_CURRENT_BINARY_DIR}/3rd/libssh/include
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
find_package(OpenSSL REQUIRED)
|
||||||
|
list(APPEND LIBS OpenSSL::SSL OpenSSL::Crypto)
|
||||||
|
|
||||||
|
find_package(libssh REQUIRED)
|
||||||
|
list(APPEND LIBS ssh::ssh)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
set(CLIENT_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR}/..)
|
set(CLIENT_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR}/..)
|
||||||
set(QSIMPLECRYPTO_DIR ${CLIENT_ROOT_DIR}/3rd/QSimpleCrypto/src)
|
set(AMNEZIA_THIRDPARTY_ROOT "${CLIENT_ROOT_DIR}/3rd" CACHE PATH "Path to Amnezia client/3rd sources")
|
||||||
|
set(QSIMPLECRYPTO_DIR ${AMNEZIA_THIRDPARTY_ROOT}/QSimpleCrypto/src)
|
||||||
|
|
||||||
include_directories(${QSIMPLECRYPTO_DIR})
|
include_directories(${QSIMPLECRYPTO_DIR})
|
||||||
|
|
||||||
|
|||||||
+10
-14
@@ -42,18 +42,14 @@ set(SOURCES ${SOURCES}
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/core/installedAppsImageProvider.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/core/installedAppsImageProvider.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
foreach(abi IN ITEMS ${QT_ANDROID_ABIS})
|
|
||||||
set_property(TARGET ${PROJECT} PROPERTY QT_ANDROID_EXTRA_LIBS
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/amneziawg/android/${abi}/libwg-go.so
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/android/${abi}/libck-ovpn-plugin.so
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/android/${abi}/libovpn3.so
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/android/${abi}/libovpnutil.so
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/android/${abi}/librsapss.so
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openssl/android/${abi}/libcrypto_3.so
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openssl/android/${abi}/libssl_3.so
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/libssh/android/${abi}/libssh.so
|
|
||||||
)
|
|
||||||
endforeach()
|
|
||||||
|
|
||||||
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/xray/android/libxray.aar
|
find_package(awg-android REQUIRED)
|
||||||
DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/android/xray/libXray)
|
set(LIBS ${LIBS} amnezia::awg-android)
|
||||||
|
set_property(TARGET ${PROJECT} APPEND PROPERTY QT_ANDROID_EXTRA_LIBS ${AMNEZIA_ANDROID_LIBWG_PATH} ${AMNEZIA_ANDROID_LIBWG_QUICK_PATH})
|
||||||
|
|
||||||
|
find_package(amnezia-libxray REQUIRED)
|
||||||
|
file(COPY ${AMNEZIA_LIBXRAY_PATH} DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/android/xray/libXray)
|
||||||
|
|
||||||
|
find_package(openvpn-pt-android REQUIRED)
|
||||||
|
set(LIBS ${LIBS} amnezia::openvpn-pt-android)
|
||||||
|
set_property(TARGET ${PROJECT} APPEND PROPERTY QT_ANDROID_EXTRA_LIBS ${OPENVPN_PT_ANDROID_LIBCK_OVPN_PLUGIN_PATH})
|
||||||
|
|||||||
@@ -39,5 +39,7 @@ while(IOS_TARGETS)
|
|||||||
set_target_properties(${TARGET_NAME} PROPERTIES
|
set_target_properties(${TARGET_NAME} PROPERTIES
|
||||||
XCODE_ATTRIBUTE_ARCHS[sdk=iphoneos*] "arm64"
|
XCODE_ATTRIBUTE_ARCHS[sdk=iphoneos*] "arm64"
|
||||||
XCODE_ATTRIBUTE_ARCHS[sdk=iphonesimulator*] "x86_64"
|
XCODE_ATTRIBUTE_ARCHS[sdk=iphonesimulator*] "x86_64"
|
||||||
|
XCODE_ATTRIBUTE_ARCHS[sdk=appletvos*] "arm64"
|
||||||
|
XCODE_ATTRIBUTE_ARCHS[sdk=appletvsimulator*] "arm64"
|
||||||
)
|
)
|
||||||
endwhile()
|
endwhile()
|
||||||
|
|||||||
+136
-32
@@ -1,7 +1,19 @@
|
|||||||
message("Client iOS build")
|
message("Client iOS build")
|
||||||
set(CMAKE_OSX_DEPLOYMENT_TARGET 13.0)
|
|
||||||
set(APPLE_PROJECT_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
|
set(APPLE_PROJECT_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
|
||||||
|
set(AMNEZIA_IOS_APPLETV ${AMNEZIA_IOS_ENABLE_APPLETV_TARGET})
|
||||||
|
|
||||||
|
if(AMNEZIA_IOS_APPLETV)
|
||||||
|
message("Apple TV target mode is ON")
|
||||||
|
set(CMAKE_OSX_DEPLOYMENT_TARGET 17.0)
|
||||||
|
set(QT_NO_SET_DEFAULT_IOS_LAUNCH_SCREEN TRUE)
|
||||||
|
set(QT_NO_ADD_IOS_LAUNCH_SCREEN_TO_BUNDLE TRUE)
|
||||||
|
set(IOS_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Info-tvOS.plist.in)
|
||||||
|
set(IOS_LAUNCHSCREEN_STORYBOARD ${CMAKE_CURRENT_SOURCE_DIR}/ios/app/tvOS/AmneziaVPNLaunchScreen.storyboard)
|
||||||
|
else()
|
||||||
|
message("Apple TV target mode is OFF")
|
||||||
|
set(IOS_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Info.plist.in)
|
||||||
|
set(IOS_LAUNCHSCREEN_STORYBOARD ${CMAKE_CURRENT_SOURCE_DIR}/ios/app/AmneziaVPNLaunchScreen.storyboard)
|
||||||
|
endif()
|
||||||
|
|
||||||
enable_language(OBJC)
|
enable_language(OBJC)
|
||||||
enable_language(OBJCXX)
|
enable_language(OBJCXX)
|
||||||
@@ -10,13 +22,23 @@ enable_language(Swift)
|
|||||||
find_package(Qt6 REQUIRED COMPONENTS ShaderTools)
|
find_package(Qt6 REQUIRED COMPONENTS ShaderTools)
|
||||||
set(LIBS ${LIBS} Qt6::ShaderTools)
|
set(LIBS ${LIBS} Qt6::ShaderTools)
|
||||||
|
|
||||||
find_library(FW_AUTHENTICATIONSERVICES AuthenticationServices)
|
if(AMNEZIA_IOS_APPLETV)
|
||||||
find_library(FW_UIKIT UIKit)
|
# Use framework linker flags directly for tvOS to avoid iPhoneOS SDK absolute paths.
|
||||||
find_library(FW_AVFOUNDATION AVFoundation)
|
set(FW_AUTHENTICATIONSERVICES "-framework AuthenticationServices")
|
||||||
find_library(FW_FOUNDATION Foundation)
|
set(FW_UIKIT "-framework UIKit")
|
||||||
find_library(FW_STOREKIT StoreKit)
|
set(FW_AVFOUNDATION "-framework AVFoundation")
|
||||||
find_library(FW_USERNOTIFICATIONS UserNotifications)
|
set(FW_FOUNDATION "-framework Foundation")
|
||||||
find_library(FW_NETWORKEXTENSION NetworkExtension)
|
set(FW_STOREKIT "-framework StoreKit")
|
||||||
|
set(FW_USERNOTIFICATIONS "-framework UserNotifications")
|
||||||
|
else()
|
||||||
|
find_library(FW_AUTHENTICATIONSERVICES AuthenticationServices)
|
||||||
|
find_library(FW_UIKIT UIKit)
|
||||||
|
find_library(FW_AVFOUNDATION AVFoundation)
|
||||||
|
find_library(FW_FOUNDATION Foundation)
|
||||||
|
find_library(FW_STOREKIT StoreKit)
|
||||||
|
find_library(FW_USERNOTIFICATIONS UserNotifications)
|
||||||
|
find_library(FW_NETWORKEXTENSION NetworkExtension)
|
||||||
|
endif()
|
||||||
|
|
||||||
set(LIBS ${LIBS}
|
set(LIBS ${LIBS}
|
||||||
${FW_AUTHENTICATIONSERVICES}
|
${FW_AUTHENTICATIONSERVICES}
|
||||||
@@ -25,9 +47,12 @@ set(LIBS ${LIBS}
|
|||||||
${FW_FOUNDATION}
|
${FW_FOUNDATION}
|
||||||
${FW_STOREKIT}
|
${FW_STOREKIT}
|
||||||
${FW_USERNOTIFICATIONS}
|
${FW_USERNOTIFICATIONS}
|
||||||
${FW_NETWORKEXTENSION}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if(NOT AMNEZIA_IOS_APPLETV)
|
||||||
|
set(LIBS ${LIBS} ${FW_NETWORKEXTENSION})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
|
||||||
set(HEADERS ${HEADERS}
|
set(HEADERS ${HEADERS}
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.h
|
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.h
|
||||||
@@ -57,7 +82,7 @@ target_include_directories(${PROJECT} PRIVATE ${Qt6Gui_PRIVATE_INCLUDE_DIRS})
|
|||||||
|
|
||||||
set_target_properties(${PROJECT} PROPERTIES
|
set_target_properties(${PROJECT} PROPERTIES
|
||||||
XCODE_LINK_BUILD_PHASE_MODE KNOWN_LOCATION
|
XCODE_LINK_BUILD_PHASE_MODE KNOWN_LOCATION
|
||||||
MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Info.plist.in
|
MACOSX_BUNDLE_INFO_PLIST ${IOS_INFO_PLIST}
|
||||||
MACOSX_BUNDLE_ICON_FILE "AppIcon"
|
MACOSX_BUNDLE_ICON_FILE "AppIcon"
|
||||||
MACOSX_BUNDLE_INFO_STRING "AmneziaVPN"
|
MACOSX_BUNDLE_INFO_STRING "AmneziaVPN"
|
||||||
MACOSX_BUNDLE_BUNDLE_NAME "AmneziaVPN"
|
MACOSX_BUNDLE_BUNDLE_NAME "AmneziaVPN"
|
||||||
@@ -66,7 +91,6 @@ set_target_properties(${PROJECT} PROPERTIES
|
|||||||
MACOSX_BUNDLE_LONG_VERSION_STRING "${APPLE_PROJECT_VERSION}-${CMAKE_PROJECT_VERSION_TWEAK}"
|
MACOSX_BUNDLE_LONG_VERSION_STRING "${APPLE_PROJECT_VERSION}-${CMAKE_PROJECT_VERSION_TWEAK}"
|
||||||
MACOSX_BUNDLE_SHORT_VERSION_STRING "${APPLE_PROJECT_VERSION}"
|
MACOSX_BUNDLE_SHORT_VERSION_STRING "${APPLE_PROJECT_VERSION}"
|
||||||
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "${BUILD_IOS_APP_IDENTIFIER}"
|
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "${BUILD_IOS_APP_IDENTIFIER}"
|
||||||
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS "${CMAKE_CURRENT_SOURCE_DIR}/ios/app/main.entitlements"
|
|
||||||
XCODE_ATTRIBUTE_MARKETING_VERSION "${APPLE_PROJECT_VERSION}"
|
XCODE_ATTRIBUTE_MARKETING_VERSION "${APPLE_PROJECT_VERSION}"
|
||||||
XCODE_ATTRIBUTE_CURRENT_PROJECT_VERSION "${CMAKE_PROJECT_VERSION_TWEAK}"
|
XCODE_ATTRIBUTE_CURRENT_PROJECT_VERSION "${CMAKE_PROJECT_VERSION_TWEAK}"
|
||||||
XCODE_ATTRIBUTE_PRODUCT_NAME "AmneziaVPN"
|
XCODE_ATTRIBUTE_PRODUCT_NAME "AmneziaVPN"
|
||||||
@@ -74,13 +98,36 @@ set_target_properties(${PROJECT} PROPERTIES
|
|||||||
XCODE_GENERATE_SCHEME TRUE
|
XCODE_GENERATE_SCHEME TRUE
|
||||||
XCODE_ATTRIBUTE_ENABLE_BITCODE "NO"
|
XCODE_ATTRIBUTE_ENABLE_BITCODE "NO"
|
||||||
XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME "AppIcon"
|
XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME "AppIcon"
|
||||||
XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2"
|
|
||||||
XCODE_EMBED_FRAMEWORKS_CODE_SIGN_ON_COPY ON
|
XCODE_EMBED_FRAMEWORKS_CODE_SIGN_ON_COPY ON
|
||||||
XCODE_LINK_BUILD_PHASE_MODE KNOWN_LOCATION
|
XCODE_LINK_BUILD_PHASE_MODE KNOWN_LOCATION
|
||||||
XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/Frameworks"
|
XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/Frameworks"
|
||||||
XCODE_EMBED_APP_EXTENSIONS networkextension
|
XCODE_EMBED_APP_EXTENSIONS networkextension
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if(AMNEZIA_IOS_APPLETV)
|
||||||
|
set_target_properties(${PROJECT} PROPERTIES
|
||||||
|
XCODE_ATTRIBUTE_SUPPORTED_PLATFORMS "appletvos appletvsimulator"
|
||||||
|
XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "3"
|
||||||
|
XCODE_ATTRIBUTE_TVOS_DEPLOYMENT_TARGET "${CMAKE_OSX_DEPLOYMENT_TARGET}"
|
||||||
|
XCODE_ATTRIBUTE_SDKROOT "appletvos"
|
||||||
|
XCODE_ATTRIBUTE_SDKROOT[sdk=appletvos*] "appletvos"
|
||||||
|
XCODE_ATTRIBUTE_SDKROOT[sdk=appletvsimulator*] "appletvsimulator"
|
||||||
|
XCODE_ATTRIBUTE_LIBRARY_SEARCH_PATHS "$(inherited) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)"
|
||||||
|
XCODE_ATTRIBUTE_LIBRARY_SEARCH_PATHS[sdk=appletvos*] "$(inherited) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)"
|
||||||
|
XCODE_ATTRIBUTE_LIBRARY_SEARCH_PATHS[sdk=appletvsimulator*] "$(inherited) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)"
|
||||||
|
XCODE_ATTRIBUTE_EXCLUDED_LIBRARY_SEARCH_PATHS "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS*.sdk/usr/lib/swift"
|
||||||
|
XCODE_ATTRIBUTE_EXCLUDED_FRAMEWORK_SEARCH_PATHS "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS*.sdk/System/Library/Frameworks"
|
||||||
|
)
|
||||||
|
set_target_properties(${PROJECT} PROPERTIES
|
||||||
|
QT_IOS_PERMISSIONS ""
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
set_target_properties(${PROJECT} PROPERTIES
|
||||||
|
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS "${CMAKE_CURRENT_SOURCE_DIR}/ios/app/main.entitlements"
|
||||||
|
XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2"
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
if(DEFINED DEPLOY)
|
if(DEFINED DEPLOY)
|
||||||
set_target_properties(${PROJECT} PROPERTIES
|
set_target_properties(${PROJECT} PROPERTIES
|
||||||
XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "Apple Distribution"
|
XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "Apple Distribution"
|
||||||
@@ -111,7 +158,61 @@ target_compile_options(${PROJECT} PRIVATE
|
|||||||
-DVPN_NE_BUNDLEID=\"${BUILD_IOS_APP_IDENTIFIER}.network-extension\"
|
-DVPN_NE_BUNDLEID=\"${BUILD_IOS_APP_IDENTIFIER}.network-extension\"
|
||||||
)
|
)
|
||||||
|
|
||||||
set(WG_APPLE_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/3rd/amneziawg-apple/Sources)
|
if(AMNEZIA_IOS_APPLETV)
|
||||||
|
# qscnetworkreachability plugin links IOKit, which is unavailable on tvOS.
|
||||||
|
qt_import_plugins(${PROJECT}
|
||||||
|
NO_DEFAULT
|
||||||
|
INCLUDE
|
||||||
|
QIOSIntegrationPlugin
|
||||||
|
QJpegPlugin
|
||||||
|
QSvgPlugin
|
||||||
|
QGifPlugin
|
||||||
|
QICOPlugin
|
||||||
|
QSvgIconPlugin
|
||||||
|
QSecureTransportBackendPlugin
|
||||||
|
EXCLUDE
|
||||||
|
QSCNetworkReachabilityNetworkInformationPlugin
|
||||||
|
QDarwinCameraPermissionPlugin
|
||||||
|
)
|
||||||
|
|
||||||
|
# Static tvOS Qt build doesn't auto-link these plugin archives into the
|
||||||
|
# Xcode target, but the app entry point lives in QIOSIntegrationPlugin.
|
||||||
|
set(_amnezia_tvos_static_plugins
|
||||||
|
Qt6::QIOSIntegrationPlugin
|
||||||
|
Qt6::QIOSIntegrationPlugin_init
|
||||||
|
Qt6::QJpegPlugin
|
||||||
|
Qt6::QJpegPlugin_init
|
||||||
|
Qt6::QSvgPlugin
|
||||||
|
Qt6::QSvgPlugin_init
|
||||||
|
Qt6::QGifPlugin
|
||||||
|
Qt6::QGifPlugin_init
|
||||||
|
Qt6::QICOPlugin
|
||||||
|
Qt6::QICOPlugin_init
|
||||||
|
Qt6::QSvgIconPlugin
|
||||||
|
Qt6::QSvgIconPlugin_init
|
||||||
|
Qt6::QSecureTransportBackendPlugin
|
||||||
|
Qt6::QSecureTransportBackendPlugin_init
|
||||||
|
)
|
||||||
|
foreach(_amnezia_tvos_static_plugin IN LISTS _amnezia_tvos_static_plugins)
|
||||||
|
if(TARGET ${_amnezia_tvos_static_plugin})
|
||||||
|
target_link_libraries(${PROJECT} PRIVATE ${_amnezia_tvos_static_plugin})
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
unset(_amnezia_tvos_static_plugin)
|
||||||
|
unset(_amnezia_tvos_static_plugins)
|
||||||
|
|
||||||
|
# Qt 6.9.2 iOS package links IOKit via Qt6::Core interface, but tvOS SDK
|
||||||
|
# does not provide IOKit. Strip this single framework for Apple TV builds.
|
||||||
|
get_target_property(_qtcore_iface_libs Qt6::Core INTERFACE_LINK_LIBRARIES)
|
||||||
|
if(_qtcore_iface_libs)
|
||||||
|
string(REPLACE "-framework IOKit;" "" _qtcore_iface_libs "${_qtcore_iface_libs}")
|
||||||
|
string(REPLACE ";-framework IOKit" "" _qtcore_iface_libs "${_qtcore_iface_libs}")
|
||||||
|
set_property(TARGET Qt6::Core PROPERTY INTERFACE_LINK_LIBRARIES "${_qtcore_iface_libs}")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(AMNEZIA_THIRDPARTY_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/3rd" CACHE PATH "Path to Amnezia client/3rd sources")
|
||||||
|
set(WG_APPLE_SOURCE_DIR ${AMNEZIA_THIRDPARTY_ROOT}/amneziawg-apple/Sources)
|
||||||
|
|
||||||
target_sources(${PROJECT} PRIVATE
|
target_sources(${PROJECT} PRIVATE
|
||||||
# ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosvpnprotocol.swift
|
# ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosvpnprotocol.swift
|
||||||
@@ -121,28 +222,31 @@ target_sources(${PROJECT} PRIVATE
|
|||||||
${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift
|
${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift
|
||||||
${CLIENT_ROOT_DIR}/platforms/ios/ScreenProtection.swift
|
${CLIENT_ROOT_DIR}/platforms/ios/ScreenProtection.swift
|
||||||
${CLIENT_ROOT_DIR}/platforms/ios/VPNCController.swift
|
${CLIENT_ROOT_DIR}/platforms/ios/VPNCController.swift
|
||||||
${CLIENT_ROOT_DIR}/platforms/ios/StoreKit2Helper.swift
|
|
||||||
)
|
)
|
||||||
|
|
||||||
target_sources(${PROJECT} PRIVATE
|
if(IOS_LAUNCHSCREEN_STORYBOARD)
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/AmneziaVPNLaunchScreen.storyboard
|
target_sources(${PROJECT} PRIVATE
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Media.xcassets
|
${IOS_LAUNCHSCREEN_STORYBOARD}
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy
|
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Media.xcassets
|
||||||
)
|
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy
|
||||||
|
)
|
||||||
|
|
||||||
set_property(TARGET ${PROJECT} APPEND PROPERTY RESOURCE
|
set_property(TARGET ${PROJECT} APPEND PROPERTY RESOURCE
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/AmneziaVPNLaunchScreen.storyboard
|
${IOS_LAUNCHSCREEN_STORYBOARD}
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Media.xcassets
|
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Media.xcassets
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy
|
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy
|
||||||
)
|
)
|
||||||
|
else()
|
||||||
|
target_sources(${PROJECT} PRIVATE
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Media.xcassets
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy
|
||||||
|
)
|
||||||
|
|
||||||
|
set_property(TARGET ${PROJECT} APPEND PROPERTY RESOURCE
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Media.xcassets
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
add_subdirectory(ios/networkextension)
|
add_subdirectory(ios/networkextension)
|
||||||
add_dependencies(${PROJECT} networkextension)
|
add_dependencies(${PROJECT} networkextension)
|
||||||
|
|
||||||
set_property(TARGET ${PROJECT} PROPERTY XCODE_EMBED_FRAMEWORKS
|
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-ios/OpenVPNAdapter.framework"
|
|
||||||
)
|
|
||||||
|
|
||||||
set(CMAKE_XCODE_ATTRIBUTE_FRAMEWORK_SEARCH_PATHS ${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-ios/)
|
|
||||||
target_link_libraries("networkextension" PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-ios/OpenVPNAdapter.framework")
|
|
||||||
|
|
||||||
|
|||||||
@@ -23,9 +23,6 @@ set_target_properties(${PROJECT} PROPERTIES
|
|||||||
MACOSX_BUNDLE_SHORT_VERSION_STRING "${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}"
|
MACOSX_BUNDLE_SHORT_VERSION_STRING "${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}"
|
||||||
MACOSX_BUNDLE_BUNDLE_VERSION "${CMAKE_PROJECT_VERSION_TWEAK}"
|
MACOSX_BUNDLE_BUNDLE_VERSION "${CMAKE_PROJECT_VERSION_TWEAK}"
|
||||||
)
|
)
|
||||||
set(CMAKE_OSX_ARCHITECTURES "x86_64" CACHE INTERNAL "" FORCE)
|
|
||||||
set(CMAKE_OSX_DEPLOYMENT_TARGET 10.15)
|
|
||||||
|
|
||||||
|
|
||||||
set(HEADERS ${HEADERS}
|
set(HEADERS ${HEADERS}
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ui/macos_util.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ui/macos_util.h
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
message("Client ==> MacOS NE build")
|
message("Client ==> MacOS NE build")
|
||||||
|
|
||||||
set_target_properties(${PROJECT} PROPERTIES MACOSX_BUNDLE TRUE)
|
set_target_properties(${PROJECT} PROPERTIES MACOSX_BUNDLE TRUE)
|
||||||
set(CMAKE_OSX_DEPLOYMENT_TARGET 10.15)
|
|
||||||
|
|
||||||
set(APPLE_PROJECT_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
|
set(APPLE_PROJECT_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
|
||||||
|
|
||||||
@@ -131,7 +130,6 @@ target_sources(${PROJECT} PRIVATE
|
|||||||
${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift
|
${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift
|
||||||
${CLIENT_ROOT_DIR}/platforms/ios/ScreenProtection.swift
|
${CLIENT_ROOT_DIR}/platforms/ios/ScreenProtection.swift
|
||||||
${CLIENT_ROOT_DIR}/platforms/ios/VPNCController.swift
|
${CLIENT_ROOT_DIR}/platforms/ios/VPNCController.swift
|
||||||
${CLIENT_ROOT_DIR}/platforms/ios/StoreKit2Helper.swift
|
|
||||||
)
|
)
|
||||||
|
|
||||||
target_sources(${PROJECT} PRIVATE
|
target_sources(${PROJECT} PRIVATE
|
||||||
@@ -153,19 +151,6 @@ message(${QtCore_location})
|
|||||||
|
|
||||||
get_filename_component(QT_BIN_DIR_DETECTED "${QtCore_location}/../../../../../bin" ABSOLUTE)
|
get_filename_component(QT_BIN_DIR_DETECTED "${QtCore_location}/../../../../../bin" ABSOLUTE)
|
||||||
|
|
||||||
set_property(TARGET ${PROJECT} PROPERTY XCODE_EMBED_FRAMEWORKS
|
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-macos/OpenVPNAdapter.framework"
|
|
||||||
)
|
|
||||||
|
|
||||||
set(CMAKE_XCODE_ATTRIBUTE_FRAMEWORK_SEARCH_PATHS ${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-macos)
|
|
||||||
target_link_libraries("AmneziaVPNNetworkExtension" PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-macos/OpenVPNAdapter.framework")
|
|
||||||
|
|
||||||
add_custom_command(TARGET ${PROJECT} POST_BUILD
|
add_custom_command(TARGET ${PROJECT} POST_BUILD
|
||||||
COMMAND ${CMAKE_COMMAND} -E make_directory
|
|
||||||
$<TARGET_BUNDLE_DIR:AmneziaVPN>/Contents/Frameworks
|
|
||||||
COMMAND /usr/bin/find "$<TARGET_BUNDLE_DIR:AmneziaVPN>/Contents/Frameworks/OpenVPNAdapter.framework" -name "*.sha256" -delete
|
|
||||||
COMMAND /usr/bin/codesign --force --sign "Apple Distribution: Privacy Technologies OU"
|
|
||||||
"$<TARGET_BUNDLE_DIR:AmneziaVPN>/Contents/Frameworks/OpenVPNAdapter.framework/Versions/Current/OpenVPNAdapter"
|
|
||||||
COMMAND ${QT_BIN_DIR_DETECTED}/macdeployqt $<TARGET_BUNDLE_DIR:AmneziaVPN> -appstore-compliant -qmldir=${CMAKE_CURRENT_SOURCE_DIR}
|
COMMAND ${QT_BIN_DIR_DETECTED}/macdeployqt $<TARGET_BUNDLE_DIR:AmneziaVPN> -appstore-compliant -qmldir=${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
COMMENT "Signing OpenVPNAdapter framework"
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ set(HEADERS ${HEADERS}
|
|||||||
${CLIENT_ROOT_DIR}/mozilla/controllerimpl.h
|
${CLIENT_ROOT_DIR}/mozilla/controllerimpl.h
|
||||||
)
|
)
|
||||||
|
|
||||||
if(NOT IOS AND NOT MACOS_NE)
|
if(NOT IOS AND NOT MACOS_NE AND NOT CMAKE_SYSTEM_NAME STREQUAL "tvOS")
|
||||||
set(HEADERS ${HEADERS}
|
set(HEADERS ${HEADERS}
|
||||||
${CLIENT_ROOT_DIR}/platforms/ios/QRCodeReaderBase.h
|
${CLIENT_ROOT_DIR}/platforms/ios/QRCodeReaderBase.h
|
||||||
)
|
)
|
||||||
@@ -89,14 +89,14 @@ set(SOURCES ${SOURCES}
|
|||||||
${CLIENT_ROOT_DIR}/mozilla/shared/leakdetector.cpp
|
${CLIENT_ROOT_DIR}/mozilla/shared/leakdetector.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
if(NOT IOS AND NOT MACOS_NE)
|
if(NOT IOS AND NOT MACOS_NE AND NOT CMAKE_SYSTEM_NAME STREQUAL "tvOS")
|
||||||
set(SOURCES ${SOURCES}
|
set(SOURCES ${SOURCES}
|
||||||
${CLIENT_ROOT_DIR}/platforms/ios/QRCodeReaderBase.cpp
|
${CLIENT_ROOT_DIR}/platforms/ios/QRCodeReaderBase.cpp
|
||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Include native macOS platform helpers (dock/status-item)
|
# Include native macOS platform helpers (dock/status-item)
|
||||||
if(APPLE AND NOT IOS)
|
if(APPLE AND NOT IOS AND NOT CMAKE_SYSTEM_NAME STREQUAL "tvOS")
|
||||||
list(APPEND HEADERS
|
list(APPEND HEADERS
|
||||||
${CLIENT_ROOT_DIR}/platforms/macos/macosutils.h
|
${CLIENT_ROOT_DIR}/platforms/macos/macosutils.h
|
||||||
${CLIENT_ROOT_DIR}/platforms/macos/macosstatusicon.h
|
${CLIENT_ROOT_DIR}/platforms/macos/macosstatusicon.h
|
||||||
@@ -175,7 +175,7 @@ if(WIN32)
|
|||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID))
|
if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE AND NOT CMAKE_SYSTEM_NAME STREQUAL "tvOS") OR (LINUX AND NOT ANDROID))
|
||||||
message("Client desktop build")
|
message("Client desktop build")
|
||||||
add_compile_definitions(AMNEZIA_DESKTOP)
|
add_compile_definitions(AMNEZIA_DESKTOP)
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
#include "openvpn_configurator.h"
|
#include "openvpn_configurator.h"
|
||||||
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
#include <QCoreApplication>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QProcess>
|
#include <QProcess>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QTemporaryDir>
|
#include <QTemporaryDir>
|
||||||
#include <QTemporaryFile>
|
#include <QTemporaryFile>
|
||||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
|
||||||
#include <QGuiApplication>
|
|
||||||
#else
|
|
||||||
#include <QApplication>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include "core/networkUtilities.h"
|
#include "core/networkUtilities.h"
|
||||||
#include "containers/containers_defs.h"
|
#include "containers/containers_defs.h"
|
||||||
@@ -165,7 +161,7 @@ QString OpenVpnConfigurator::processConfigWithLocalSettings(const QPair<QString,
|
|||||||
QString dnsConf = QString("\nscript-security 2\n"
|
QString dnsConf = QString("\nscript-security 2\n"
|
||||||
"up %1/update-resolv-conf.sh\n"
|
"up %1/update-resolv-conf.sh\n"
|
||||||
"down %1/update-resolv-conf.sh\n")
|
"down %1/update-resolv-conf.sh\n")
|
||||||
.arg(qApp->applicationDirPath());
|
.arg(QCoreApplication::applicationDirPath());
|
||||||
|
|
||||||
config.append(dnsConf);
|
config.append(dnsConf);
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "ssh_configurator.h"
|
#include "ssh_configurator.h"
|
||||||
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
#include <QCoreApplication>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QProcess>
|
#include <QProcess>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
@@ -8,11 +9,6 @@
|
|||||||
#include <QTemporaryFile>
|
#include <QTemporaryFile>
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
#include <qtimer.h>
|
#include <qtimer.h>
|
||||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(MACOS_NE)
|
|
||||||
#include <QGuiApplication>
|
|
||||||
#else
|
|
||||||
#include <QApplication>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include "core/server_defs.h"
|
#include "core/server_defs.h"
|
||||||
#include "utilities.h"
|
#include "utilities.h"
|
||||||
@@ -24,7 +20,7 @@ SshConfigurator::SshConfigurator(std::shared_ptr<Settings> settings, const QShar
|
|||||||
|
|
||||||
QString SshConfigurator::convertOpenSShKey(const QString &key)
|
QString SshConfigurator::convertOpenSShKey(const QString &key)
|
||||||
{
|
{
|
||||||
#if !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
#if !defined(Q_OS_IOS) && !defined(Q_OS_TVOS) && !defined(MACOS_NE)
|
||||||
QProcess p;
|
QProcess p;
|
||||||
p.setProcessChannelMode(QProcess::MergedChannels);
|
p.setProcessChannelMode(QProcess::MergedChannels);
|
||||||
|
|
||||||
@@ -70,13 +66,13 @@ QString SshConfigurator::convertOpenSShKey(const QString &key)
|
|||||||
// DEAD CODE.
|
// DEAD CODE.
|
||||||
void SshConfigurator::openSshTerminal(const ServerCredentials &credentials)
|
void SshConfigurator::openSshTerminal(const ServerCredentials &credentials)
|
||||||
{
|
{
|
||||||
#if !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
#if !defined(Q_OS_IOS) && !defined(Q_OS_TVOS) && !defined(MACOS_NE)
|
||||||
QProcess *p = new QProcess();
|
QProcess *p = new QProcess();
|
||||||
p->setProcessChannelMode(QProcess::SeparateChannels);
|
p->setProcessChannelMode(QProcess::SeparateChannels);
|
||||||
|
|
||||||
#ifdef Q_OS_WIN
|
#ifdef Q_OS_WIN
|
||||||
p->setProcessEnvironment(prepareEnv());
|
p->setProcessEnvironment(prepareEnv());
|
||||||
p->setProgram(qApp->applicationDirPath() + "\\cygwin\\putty.exe");
|
p->setProgram(QCoreApplication::applicationDirPath() + "\\cygwin\\putty.exe");
|
||||||
|
|
||||||
if (credentials.secretData.contains("PRIVATE KEY")) {
|
if (credentials.secretData.contains("PRIVATE KEY")) {
|
||||||
// todo: connect by key
|
// todo: connect by key
|
||||||
@@ -100,10 +96,10 @@ QProcessEnvironment SshConfigurator::prepareEnv()
|
|||||||
|
|
||||||
#ifdef Q_OS_WIN
|
#ifdef Q_OS_WIN
|
||||||
pathEnvVar.clear();
|
pathEnvVar.clear();
|
||||||
pathEnvVar.prepend(QDir::toNativeSeparators(QApplication::applicationDirPath()) + "\\cygwin;");
|
pathEnvVar.prepend(QDir::toNativeSeparators(QCoreApplication::applicationDirPath()) + "\\cygwin;");
|
||||||
pathEnvVar.prepend(QDir::toNativeSeparators(QApplication::applicationDirPath()) + "\\openvpn;");
|
pathEnvVar.prepend(QDir::toNativeSeparators(QCoreApplication::applicationDirPath()) + "\\openvpn;");
|
||||||
#elif defined(Q_OS_MACX) && !defined(MACOS_NE)
|
#elif defined(Q_OS_MACX) && !defined(MACOS_NE)
|
||||||
pathEnvVar.prepend(QDir::toNativeSeparators(QApplication::applicationDirPath()) + "/Contents/MacOS");
|
pathEnvVar.prepend(QDir::toNativeSeparators(QCoreApplication::applicationDirPath()) + "/Contents/MacOS");
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
env.insert("PATH", pathEnvVar);
|
env.insert("PATH", pathEnvVar);
|
||||||
|
|||||||
@@ -298,14 +298,14 @@ bool ContainerProps::isSupportedByCurrentPlatform(DockerContainer c)
|
|||||||
}
|
}
|
||||||
|
|
||||||
#elif defined(MACOS_NE)
|
#elif defined(MACOS_NE)
|
||||||
// macOS build using Network Extension – allow OpenVPN for parity with iOS.
|
// macOS build using Network Extension – hide OpenVPN-based containers
|
||||||
switch (c) {
|
switch (c) {
|
||||||
case DockerContainer::OpenVpn: return true;
|
|
||||||
case DockerContainer::WireGuard: return true;
|
case DockerContainer::WireGuard: return true;
|
||||||
case DockerContainer::Awg2: return true;
|
case DockerContainer::Awg2: return true;
|
||||||
case DockerContainer::Awg: return true;
|
case DockerContainer::Awg: return true;
|
||||||
case DockerContainer::Xray: return true;
|
case DockerContainer::Xray: return true;
|
||||||
case DockerContainer::SSXray: return true;
|
case DockerContainer::SSXray: return true;
|
||||||
|
case DockerContainer::OpenVpn:
|
||||||
case DockerContainer::Cloak:
|
case DockerContainer::Cloak:
|
||||||
case DockerContainer::ShadowSocks:
|
case DockerContainer::ShadowSocks:
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ namespace apiDefs
|
|||||||
AmneziaPremiumV1,
|
AmneziaPremiumV1,
|
||||||
AmneziaPremiumV2,
|
AmneziaPremiumV2,
|
||||||
SelfHosted,
|
SelfHosted,
|
||||||
ExternalPremium,
|
ExternalPremium
|
||||||
ExternalTrial
|
|
||||||
};
|
};
|
||||||
|
|
||||||
enum ConfigSource {
|
enum ConfigSource {
|
||||||
@@ -33,7 +32,6 @@ namespace apiDefs
|
|||||||
constexpr QLatin1String stackType("stack_type");
|
constexpr QLatin1String stackType("stack_type");
|
||||||
constexpr QLatin1String serviceType("service_type");
|
constexpr QLatin1String serviceType("service_type");
|
||||||
constexpr QLatin1String cliVersion("cli_version");
|
constexpr QLatin1String cliVersion("cli_version");
|
||||||
constexpr QLatin1String cliName("cli_name");
|
|
||||||
constexpr QLatin1String supportedProtocols("supported_protocols");
|
constexpr QLatin1String supportedProtocols("supported_protocols");
|
||||||
|
|
||||||
constexpr QLatin1String vpnKey("vpn_key");
|
constexpr QLatin1String vpnKey("vpn_key");
|
||||||
@@ -55,14 +53,8 @@ namespace apiDefs
|
|||||||
constexpr QLatin1String activeDeviceCount("active_device_count");
|
constexpr QLatin1String activeDeviceCount("active_device_count");
|
||||||
constexpr QLatin1String maxDeviceCount("max_device_count");
|
constexpr QLatin1String maxDeviceCount("max_device_count");
|
||||||
constexpr QLatin1String subscriptionEndDate("subscription_end_date");
|
constexpr QLatin1String subscriptionEndDate("subscription_end_date");
|
||||||
constexpr QLatin1String subscriptionExpiredByServer("subscription_expired_by_server");
|
|
||||||
constexpr QLatin1String subscriptionStatus("subscription_status");
|
|
||||||
constexpr QLatin1String subscription("subscription");
|
|
||||||
constexpr QLatin1String endDate("end_date");
|
|
||||||
constexpr QLatin1String issuedConfigs("issued_configs");
|
constexpr QLatin1String issuedConfigs("issued_configs");
|
||||||
constexpr QLatin1String subscriptionDescription("subscription_description");
|
constexpr QLatin1String subscriptionDescription("subscription_description");
|
||||||
constexpr QLatin1String termsOfUseUrl("terms_of_use_url");
|
|
||||||
constexpr QLatin1String privacyPolicyUrl("privacy_policy_url");
|
|
||||||
|
|
||||||
constexpr QLatin1String supportInfo("support_info");
|
constexpr QLatin1String supportInfo("support_info");
|
||||||
constexpr QLatin1String email("email");
|
constexpr QLatin1String email("email");
|
||||||
@@ -77,13 +69,11 @@ namespace apiDefs
|
|||||||
|
|
||||||
constexpr QLatin1String transactionId("transaction_id");
|
constexpr QLatin1String transactionId("transaction_id");
|
||||||
constexpr QLatin1String isTestPurchase("is_test_purchase");
|
constexpr QLatin1String isTestPurchase("is_test_purchase");
|
||||||
constexpr QLatin1String isInAppPurchase("is_in_app_purchase");
|
|
||||||
|
|
||||||
constexpr QLatin1String userCountryCode("user_country_code");
|
constexpr QLatin1String userCountryCode("user_country_code");
|
||||||
|
|
||||||
constexpr QLatin1String serviceInfo("service_info");
|
constexpr QLatin1String serviceInfo("service_info");
|
||||||
constexpr QLatin1String isAdVisible("is_ad_visible");
|
constexpr QLatin1String isAdVisible("is_ad_visible");
|
||||||
constexpr QLatin1String isRenewalAvailable("is_renewal_available");
|
|
||||||
constexpr QLatin1String adHeader("ad_header");
|
constexpr QLatin1String adHeader("ad_header");
|
||||||
constexpr QLatin1String adDescription("ad_description");
|
constexpr QLatin1String adDescription("ad_description");
|
||||||
constexpr QLatin1String adEndpoint("ad_endpoint");
|
constexpr QLatin1String adEndpoint("ad_endpoint");
|
||||||
|
|||||||
@@ -3,33 +3,11 @@
|
|||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QJsonValue>
|
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
const QByteArray AMNEZIA_CONFIG_SIGNATURE = QByteArray::fromHex("000000ff");
|
const QByteArray AMNEZIA_CONFIG_SIGNATURE = QByteArray::fromHex("000000ff");
|
||||||
|
|
||||||
constexpr QLatin1String unprocessableSubscriptionMessage("Failed to retrieve subscription information. Is it activated?");
|
|
||||||
constexpr QLatin1String trialAlreadyUsedMessage("trial subscription already used");
|
|
||||||
|
|
||||||
QDateTime subscriptionEndUtcFromString(const QString &subscriptionEndDate)
|
|
||||||
{
|
|
||||||
if (subscriptionEndDate.isEmpty()) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
QDateTime endDate = QDateTime::fromString(subscriptionEndDate, Qt::ISODateWithMs).toUTC();
|
|
||||||
if (!endDate.isValid()) {
|
|
||||||
endDate = QDateTime::fromString(subscriptionEndDate, Qt::ISODate).toUTC();
|
|
||||||
}
|
|
||||||
return endDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString apiErrorMessageFromJson(const QJsonObject &jsonObj)
|
|
||||||
{
|
|
||||||
const QJsonValue value = jsonObj.value(QStringLiteral("message"));
|
|
||||||
return value.isString() ? value.toString().trimmed() : QString();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString escapeUnicode(const QString &input)
|
QString escapeUnicode(const QString &input)
|
||||||
{
|
{
|
||||||
QString output;
|
QString output;
|
||||||
@@ -46,30 +24,9 @@ namespace
|
|||||||
|
|
||||||
bool apiUtils::isSubscriptionExpired(const QString &subscriptionEndDate)
|
bool apiUtils::isSubscriptionExpired(const QString &subscriptionEndDate)
|
||||||
{
|
{
|
||||||
if (subscriptionEndDate.isEmpty()) {
|
QDateTime now = QDateTime::currentDateTimeUtc();
|
||||||
return false;
|
QDateTime endDate = QDateTime::fromString(subscriptionEndDate, Qt::ISODateWithMs);
|
||||||
}
|
return endDate < now;
|
||||||
const QDateTime endDate = subscriptionEndUtcFromString(subscriptionEndDate);
|
|
||||||
if (!endDate.isValid()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return endDate <= QDateTime::currentDateTimeUtc();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool apiUtils::isSubscriptionExpiringSoon(const QString &subscriptionEndDate, int withinDays)
|
|
||||||
{
|
|
||||||
if (subscriptionEndDate.isEmpty()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const QDateTime endDate = subscriptionEndUtcFromString(subscriptionEndDate);
|
|
||||||
if (!endDate.isValid()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const QDateTime nowUtc = QDateTime::currentDateTimeUtc();
|
|
||||||
if (endDate <= nowUtc) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return endDate <= nowUtc.addDays(withinDays);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool apiUtils::isServerFromApi(const QJsonObject &serverConfigObject)
|
bool apiUtils::isServerFromApi(const QJsonObject &serverConfigObject)
|
||||||
@@ -103,7 +60,6 @@ apiDefs::ConfigType apiUtils::getConfigType(const QJsonObject &serverConfigObjec
|
|||||||
constexpr QLatin1String servicePremium("amnezia-premium");
|
constexpr QLatin1String servicePremium("amnezia-premium");
|
||||||
constexpr QLatin1String serviceFree("amnezia-free");
|
constexpr QLatin1String serviceFree("amnezia-free");
|
||||||
constexpr QLatin1String serviceExternalPremium("external-premium");
|
constexpr QLatin1String serviceExternalPremium("external-premium");
|
||||||
constexpr QLatin1String serviceExternalTrial("external-trial");
|
|
||||||
|
|
||||||
auto apiConfigObject = serverConfigObject.value(apiDefs::key::apiConfig).toObject();
|
auto apiConfigObject = serverConfigObject.value(apiDefs::key::apiConfig).toObject();
|
||||||
auto serviceType = apiConfigObject.value(apiDefs::key::serviceType).toString();
|
auto serviceType = apiConfigObject.value(apiDefs::key::serviceType).toString();
|
||||||
@@ -114,8 +70,6 @@ apiDefs::ConfigType apiUtils::getConfigType(const QJsonObject &serverConfigObjec
|
|||||||
return apiDefs::ConfigType::AmneziaFreeV3;
|
return apiDefs::ConfigType::AmneziaFreeV3;
|
||||||
} else if (serviceType == serviceExternalPremium) {
|
} else if (serviceType == serviceExternalPremium) {
|
||||||
return apiDefs::ConfigType::ExternalPremium;
|
return apiDefs::ConfigType::ExternalPremium;
|
||||||
} else if (serviceType == serviceExternalTrial) {
|
|
||||||
return apiDefs::ConfigType::ExternalTrial;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
@@ -136,66 +90,50 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
|
|||||||
const int httpStatusCodeConflict = 409;
|
const int httpStatusCodeConflict = 409;
|
||||||
const int httpStatusCodeNotFound = 404;
|
const int httpStatusCodeNotFound = 404;
|
||||||
const int httpStatusCodeNotImplemented = 501;
|
const int httpStatusCodeNotImplemented = 501;
|
||||||
const int httpStatusCodePaymentRequired = 402;
|
|
||||||
const int httpStatusCodeUnprocessableEntity = 422;
|
|
||||||
|
|
||||||
if (!sslErrors.empty()) {
|
if (!sslErrors.empty()) {
|
||||||
qDebug().noquote() << sslErrors;
|
qDebug().noquote() << sslErrors;
|
||||||
return amnezia::ErrorCode::ApiConfigSslError;
|
return amnezia::ErrorCode::ApiConfigSslError;
|
||||||
}
|
} else if (replyError == QNetworkReply::NoError) {
|
||||||
if (replyError == QNetworkReply::NoError) {
|
|
||||||
return amnezia::ErrorCode::NoError;
|
return amnezia::ErrorCode::NoError;
|
||||||
}
|
} else if (replyError == QNetworkReply::NetworkError::OperationCanceledError
|
||||||
if (replyError == QNetworkReply::NetworkError::OperationCanceledError
|
|| replyError == QNetworkReply::NetworkError::TimeoutError) {
|
||||||
|| replyError == QNetworkReply::NetworkError::TimeoutError) {
|
|
||||||
qDebug() << replyError;
|
qDebug() << replyError;
|
||||||
return amnezia::ErrorCode::ApiConfigTimeoutError;
|
return amnezia::ErrorCode::ApiConfigTimeoutError;
|
||||||
}
|
} else if (replyError == QNetworkReply::NetworkError::OperationNotImplementedError) {
|
||||||
if (replyError == QNetworkReply::NetworkError::OperationNotImplementedError) {
|
|
||||||
qDebug() << replyError;
|
qDebug() << replyError;
|
||||||
return amnezia::ErrorCode::ApiUpdateRequestError;
|
return amnezia::ErrorCode::ApiUpdateRequestError;
|
||||||
}
|
} else {
|
||||||
|
qDebug() << QString::fromUtf8(responseBody);
|
||||||
|
qDebug() << replyError;
|
||||||
|
qDebug() << replyErrorString;
|
||||||
|
qDebug() << httpStatusCode;
|
||||||
|
|
||||||
qDebug() << QString::fromUtf8(responseBody);
|
int httpStatusFromBody = -1;
|
||||||
qDebug() << replyError;
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
|
||||||
qDebug() << httpStatusCode;
|
if (jsonDoc.isObject()) {
|
||||||
|
QJsonObject jsonObj = jsonDoc.object();
|
||||||
|
httpStatusFromBody = jsonObj.value("http_status").toInt(-1);
|
||||||
|
}
|
||||||
|
|
||||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
|
|
||||||
if (jsonDoc.isObject()) {
|
|
||||||
QJsonObject jsonObj = jsonDoc.object();
|
|
||||||
const int httpStatusFromBody = jsonObj.value(QStringLiteral("http_status")).toInt(-1);
|
|
||||||
if (httpStatusFromBody == httpStatusCodeConflict) {
|
if (httpStatusFromBody == httpStatusCodeConflict) {
|
||||||
if (apiErrorMessageFromJson(jsonObj).contains(trialAlreadyUsedMessage, Qt::CaseInsensitive)) {
|
|
||||||
return amnezia::ErrorCode::ApiTrialAlreadyUsedError;
|
|
||||||
}
|
|
||||||
return amnezia::ErrorCode::ApiConfigLimitError;
|
return amnezia::ErrorCode::ApiConfigLimitError;
|
||||||
}
|
} else if (httpStatusFromBody == httpStatusCodeNotFound) {
|
||||||
if (httpStatusFromBody == httpStatusCodeNotFound) {
|
|
||||||
return amnezia::ErrorCode::ApiNotFoundError;
|
return amnezia::ErrorCode::ApiNotFoundError;
|
||||||
}
|
} else if (httpStatusFromBody == httpStatusCodeNotImplemented) {
|
||||||
if (httpStatusFromBody == httpStatusCodeNotImplemented) {
|
|
||||||
return amnezia::ErrorCode::ApiUpdateRequestError;
|
return amnezia::ErrorCode::ApiUpdateRequestError;
|
||||||
}
|
}
|
||||||
if (httpStatusFromBody == httpStatusCodeUnprocessableEntity) {
|
|
||||||
if (apiErrorMessageFromJson(jsonObj) == unprocessableSubscriptionMessage) {
|
|
||||||
return amnezia::ErrorCode::ApiSubscriptionExpiredError;
|
|
||||||
}
|
|
||||||
return amnezia::ErrorCode::ApiConfigDownloadError;
|
|
||||||
}
|
|
||||||
if (httpStatusFromBody == httpStatusCodePaymentRequired) {
|
|
||||||
return amnezia::ErrorCode::ApiSubscriptionNotActiveError;
|
|
||||||
}
|
|
||||||
return amnezia::ErrorCode::ApiConfigDownloadError;
|
return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||||
}
|
}
|
||||||
|
|
||||||
qDebug() << "something went wrong";
|
qDebug() << "something went wrong";
|
||||||
return amnezia::ErrorCode::ApiConfigDownloadError;
|
return amnezia::ErrorCode::InternalError;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool apiUtils::isPremiumServer(const QJsonObject &serverConfigObject)
|
bool apiUtils::isPremiumServer(const QJsonObject &serverConfigObject)
|
||||||
{
|
{
|
||||||
static const QSet<apiDefs::ConfigType> premiumTypes = { apiDefs::ConfigType::AmneziaPremiumV1, apiDefs::ConfigType::AmneziaPremiumV2,
|
static const QSet<apiDefs::ConfigType> premiumTypes = { apiDefs::ConfigType::AmneziaPremiumV1, apiDefs::ConfigType::AmneziaPremiumV2,
|
||||||
apiDefs::ConfigType::ExternalPremium, apiDefs::ConfigType::ExternalTrial };
|
apiDefs::ConfigType::ExternalPremium };
|
||||||
return premiumTypes.contains(getConfigType(serverConfigObject));
|
return premiumTypes.contains(getConfigType(serverConfigObject));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,9 +177,7 @@ QString apiUtils::getPremiumV1VpnKey(const QJsonObject &serverConfigObject)
|
|||||||
|
|
||||||
QString apiUtils::getPremiumV2VpnKey(const QJsonObject &serverConfigObject)
|
QString apiUtils::getPremiumV2VpnKey(const QJsonObject &serverConfigObject)
|
||||||
{
|
{
|
||||||
auto configType = apiUtils::getConfigType(serverConfigObject);
|
if (apiUtils::getConfigType(serverConfigObject) != apiDefs::ConfigType::AmneziaPremiumV2) {
|
||||||
if (configType != apiDefs::ConfigType::AmneziaPremiumV2 && configType != apiDefs::ConfigType::ExternalPremium
|
|
||||||
&& configType != apiDefs::ConfigType::ExternalTrial) {
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ namespace apiUtils
|
|||||||
|
|
||||||
bool isSubscriptionExpired(const QString &subscriptionEndDate);
|
bool isSubscriptionExpired(const QString &subscriptionEndDate);
|
||||||
|
|
||||||
bool isSubscriptionExpiringSoon(const QString &subscriptionEndDate, int withinDays = 30);
|
|
||||||
|
|
||||||
bool isPremiumServer(const QJsonObject &serverConfigObject);
|
bool isPremiumServer(const QJsonObject &serverConfigObject);
|
||||||
|
|
||||||
apiDefs::ConfigType getConfigType(const QJsonObject &serverConfigObject);
|
apiDefs::ConfigType getConfigType(const QJsonObject &serverConfigObject);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
#include "platforms/android/android_controller.h"
|
#include "platforms/android/android_controller.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(Q_OS_IOS)
|
#if defined(Q_OS_IOS) || defined(Q_OS_TVOS)
|
||||||
#include "platforms/ios/ios_controller.h"
|
#include "platforms/ios/ios_controller.h"
|
||||||
#include <AmneziaVPN-Swift.h>
|
#include <AmneziaVPN-Swift.h>
|
||||||
#endif
|
#endif
|
||||||
@@ -91,12 +91,6 @@ void CoreController::initModels()
|
|||||||
m_apiServicesModel.reset(new ApiServicesModel(this));
|
m_apiServicesModel.reset(new ApiServicesModel(this));
|
||||||
m_engine->rootContext()->setContextProperty("ApiServicesModel", m_apiServicesModel.get());
|
m_engine->rootContext()->setContextProperty("ApiServicesModel", m_apiServicesModel.get());
|
||||||
|
|
||||||
m_apiSubscriptionPlansModel.reset(new ApiSubscriptionPlansModel(this));
|
|
||||||
m_engine->rootContext()->setContextProperty("ApiSubscriptionPlansModel", m_apiSubscriptionPlansModel.get());
|
|
||||||
|
|
||||||
m_apiBenefitsModel.reset(new ApiBenefitsModel(this));
|
|
||||||
m_engine->rootContext()->setContextProperty("ApiBenefitsModel", m_apiBenefitsModel.get());
|
|
||||||
|
|
||||||
m_apiCountryModel.reset(new ApiCountryModel(this));
|
m_apiCountryModel.reset(new ApiCountryModel(this));
|
||||||
m_engine->rootContext()->setContextProperty("ApiCountryModel", m_apiCountryModel.get());
|
m_engine->rootContext()->setContextProperty("ApiCountryModel", m_apiCountryModel.get());
|
||||||
|
|
||||||
@@ -141,7 +135,7 @@ void CoreController::initControllers()
|
|||||||
new SettingsController(m_serversModel, m_containersModel, m_languageModel, m_sitesModel, m_appSplitTunnelingModel, m_settings));
|
new SettingsController(m_serversModel, m_containersModel, m_languageModel, m_sitesModel, m_appSplitTunnelingModel, m_settings));
|
||||||
m_engine->rootContext()->setContextProperty("SettingsController", m_settingsController.get());
|
m_engine->rootContext()->setContextProperty("SettingsController", m_settingsController.get());
|
||||||
|
|
||||||
m_sitesController.reset(new SitesController(m_settings, m_sitesModel));
|
m_sitesController.reset(new SitesController(m_settings, m_vpnConnection, m_sitesModel));
|
||||||
m_engine->rootContext()->setContextProperty("SitesController", m_sitesController.get());
|
m_engine->rootContext()->setContextProperty("SitesController", m_sitesController.get());
|
||||||
|
|
||||||
m_allowedDnsController.reset(new AllowedDnsController(m_settings, m_allowedDnsModel));
|
m_allowedDnsController.reset(new AllowedDnsController(m_settings, m_allowedDnsModel));
|
||||||
@@ -157,11 +151,8 @@ void CoreController::initControllers()
|
|||||||
new ApiSettingsController(m_serversModel, m_apiAccountInfoModel, m_apiCountryModel, m_apiDevicesModel, m_settings));
|
new ApiSettingsController(m_serversModel, m_apiAccountInfoModel, m_apiCountryModel, m_apiDevicesModel, m_settings));
|
||||||
m_engine->rootContext()->setContextProperty("ApiSettingsController", m_apiSettingsController.get());
|
m_engine->rootContext()->setContextProperty("ApiSettingsController", m_apiSettingsController.get());
|
||||||
|
|
||||||
m_apiConfigsController.reset(
|
m_apiConfigsController.reset(new ApiConfigsController(m_serversModel, m_apiServicesModel, m_settings));
|
||||||
new ApiConfigsController(m_serversModel, m_apiServicesModel, m_apiSubscriptionPlansModel, m_apiBenefitsModel, m_settings));
|
|
||||||
m_engine->rootContext()->setContextProperty("ApiConfigsController", m_apiConfigsController.get());
|
m_engine->rootContext()->setContextProperty("ApiConfigsController", m_apiConfigsController.get());
|
||||||
connect(m_apiConfigsController.get(), &ApiConfigsController::subscriptionRefreshNeeded,
|
|
||||||
this, [this]() { m_apiSettingsController->getAccountInfo(false); });
|
|
||||||
|
|
||||||
m_apiNewsController.reset(new ApiNewsController(m_newsModel, m_settings, m_serversModel, this));
|
m_apiNewsController.reset(new ApiNewsController(m_newsModel, m_settings, m_serversModel, this));
|
||||||
m_engine->rootContext()->setContextProperty("ApiNewsController", m_apiNewsController.get());
|
m_engine->rootContext()->setContextProperty("ApiNewsController", m_apiNewsController.get());
|
||||||
@@ -205,7 +196,7 @@ void CoreController::initAndroidController()
|
|||||||
|
|
||||||
void CoreController::initAppleController()
|
void CoreController::initAppleController()
|
||||||
{
|
{
|
||||||
#ifdef Q_OS_IOS
|
#if defined(Q_OS_IOS) || defined(Q_OS_TVOS)
|
||||||
IosController::Instance()->initialize();
|
IosController::Instance()->initialize();
|
||||||
connect(IosController::Instance(), &IosController::importConfigFromOutside, this, [this](QString data) {
|
connect(IosController::Instance(), &IosController::importConfigFromOutside, this, [this](QString data) {
|
||||||
emit m_pageController->goToPageHome();
|
emit m_pageController->goToPageHome();
|
||||||
@@ -242,7 +233,7 @@ void CoreController::initSignalHandlers()
|
|||||||
|
|
||||||
void CoreController::initNotificationHandler()
|
void CoreController::initNotificationHandler()
|
||||||
{
|
{
|
||||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(Q_OS_TVOS)
|
||||||
m_notificationHandler.reset(NotificationHandler::create(nullptr));
|
m_notificationHandler.reset(NotificationHandler::create(nullptr));
|
||||||
|
|
||||||
connect(m_vpnConnection.get(), &VpnConnection::connectionStateChanged, m_notificationHandler.get(),
|
connect(m_vpnConnection.get(), &VpnConnection::connectionStateChanged, m_notificationHandler.get(),
|
||||||
@@ -257,7 +248,7 @@ void CoreController::initNotificationHandler()
|
|||||||
|
|
||||||
auto* trayHandler = qobject_cast<SystemTrayNotificationHandler*>(m_notificationHandler.get());
|
auto* trayHandler = qobject_cast<SystemTrayNotificationHandler*>(m_notificationHandler.get());
|
||||||
connect(this, &CoreController::websiteUrlChanged, trayHandler, &SystemTrayNotificationHandler::updateWebsiteUrl);
|
connect(this, &CoreController::websiteUrlChanged, trayHandler, &SystemTrayNotificationHandler::updateWebsiteUrl);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void CoreController::updateTranslator(const QLocale &locale)
|
void CoreController::updateTranslator(const QLocale &locale)
|
||||||
@@ -377,11 +368,7 @@ void CoreController::initPrepareConfigHandler()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_installController->validateConfig();
|
if (!m_installController->isConfigValid()) {
|
||||||
});
|
|
||||||
|
|
||||||
connect(m_installController.get(), &InstallController::configValidated, this, [this](bool isValid) {
|
|
||||||
if (!isValid) {
|
|
||||||
emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected);
|
emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
#include <QQmlContext>
|
#include <QQmlContext>
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
|
|
||||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(Q_OS_TVOS)
|
||||||
#include "ui/systemtray_notificationhandler.h"
|
#include "ui/systemtray_notificationhandler.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -32,11 +32,9 @@
|
|||||||
#include "ui/models/protocols/ikev2ConfigModel.h"
|
#include "ui/models/protocols/ikev2ConfigModel.h"
|
||||||
#endif
|
#endif
|
||||||
#include "ui/models/api/apiAccountInfoModel.h"
|
#include "ui/models/api/apiAccountInfoModel.h"
|
||||||
#include "ui/models/api/apiBenefitsModel.h"
|
|
||||||
#include "ui/models/api/apiCountryModel.h"
|
#include "ui/models/api/apiCountryModel.h"
|
||||||
#include "ui/models/api/apiDevicesModel.h"
|
#include "ui/models/api/apiDevicesModel.h"
|
||||||
#include "ui/models/api/apiServicesModel.h"
|
#include "ui/models/api/apiServicesModel.h"
|
||||||
#include "ui/models/api/apiSubscriptionPlansModel.h"
|
|
||||||
#include "ui/models/appSplitTunnelingModel.h"
|
#include "ui/models/appSplitTunnelingModel.h"
|
||||||
#include "ui/models/clientManagementModel.h"
|
#include "ui/models/clientManagementModel.h"
|
||||||
#include "ui/models/protocols/awgConfigModel.h"
|
#include "ui/models/protocols/awgConfigModel.h"
|
||||||
@@ -51,7 +49,7 @@
|
|||||||
#include "ui/models/sites_model.h"
|
#include "ui/models/sites_model.h"
|
||||||
#include "ui/models/newsModel.h"
|
#include "ui/models/newsModel.h"
|
||||||
|
|
||||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(Q_OS_TVOS)
|
||||||
#include "ui/notificationhandler.h"
|
#include "ui/notificationhandler.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -101,7 +99,7 @@ private:
|
|||||||
QSharedPointer<VpnConnection> m_vpnConnection;
|
QSharedPointer<VpnConnection> m_vpnConnection;
|
||||||
QSharedPointer<QTranslator> m_translator;
|
QSharedPointer<QTranslator> m_translator;
|
||||||
|
|
||||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(Q_OS_TVOS)
|
||||||
QScopedPointer<NotificationHandler> m_notificationHandler;
|
QScopedPointer<NotificationHandler> m_notificationHandler;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -135,8 +133,6 @@ private:
|
|||||||
QSharedPointer<ClientManagementModel> m_clientManagementModel;
|
QSharedPointer<ClientManagementModel> m_clientManagementModel;
|
||||||
|
|
||||||
QSharedPointer<ApiServicesModel> m_apiServicesModel;
|
QSharedPointer<ApiServicesModel> m_apiServicesModel;
|
||||||
QSharedPointer<ApiSubscriptionPlansModel> m_apiSubscriptionPlansModel;
|
|
||||||
QSharedPointer<ApiBenefitsModel> m_apiBenefitsModel;
|
|
||||||
QSharedPointer<ApiCountryModel> m_apiCountryModel;
|
QSharedPointer<ApiCountryModel> m_apiCountryModel;
|
||||||
QSharedPointer<ApiAccountInfoModel> m_apiAccountInfoModel;
|
QSharedPointer<ApiAccountInfoModel> m_apiAccountInfoModel;
|
||||||
QSharedPointer<ApiDevicesModel> m_apiDevicesModel;
|
QSharedPointer<ApiDevicesModel> m_apiDevicesModel;
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
#include "amnezia_application.h"
|
#include "amnezia_application.h"
|
||||||
#include "core/api/apiUtils.h"
|
#include "core/api/apiUtils.h"
|
||||||
#include "core/networkUtilities.h"
|
#include "core/networkUtilities.h"
|
||||||
#include "settings.h"
|
|
||||||
#include "utilities.h"
|
#include "utilities.h"
|
||||||
|
|
||||||
#ifdef AMNEZIA_DESKTOP
|
#ifdef AMNEZIA_DESKTOP
|
||||||
@@ -45,85 +44,17 @@ namespace
|
|||||||
|
|
||||||
constexpr int httpStatusCodeNotFound = 404;
|
constexpr int httpStatusCodeNotFound = 404;
|
||||||
constexpr int httpStatusCodeConflict = 409;
|
constexpr int httpStatusCodeConflict = 409;
|
||||||
|
|
||||||
constexpr int httpStatusCodeNotImplemented = 501;
|
constexpr int httpStatusCodeNotImplemented = 501;
|
||||||
constexpr int httpStatusCodePaymentRequired = 402;
|
|
||||||
constexpr int httpStatusCodeUnprocessableEntity = 422;
|
|
||||||
|
|
||||||
constexpr QLatin1String unprocessableSubscriptionMessage("Failed to retrieve subscription information. Is it activated?");
|
|
||||||
|
|
||||||
constexpr int proxyStorageRequestTimeoutMsecs = 3000;
|
|
||||||
|
|
||||||
QStringList shuffledProxyUrls(const QStringList &proxyUrls)
|
|
||||||
{
|
|
||||||
QStringList shuffled = proxyUrls;
|
|
||||||
std::random_device randomDevice;
|
|
||||||
std::mt19937 generator(randomDevice());
|
|
||||||
std::shuffle(shuffled.begin(), shuffled.end(), generator);
|
|
||||||
return shuffled;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString getProxyUrlsCacheKey(const QString &serviceType, const QString &userCountryCode)
|
|
||||||
{
|
|
||||||
return QStringLiteral("service_%1_country_%2").arg(serviceType, userCountryCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool decryptProxyUrlsPayload(const QByteArray &encryptedPayload, bool isDevEnvironment, QByteArray &decryptedPayload)
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
QByteArray key = isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
|
||||||
if (!isDevEnvironment) {
|
|
||||||
QCryptographicHash hash(QCryptographicHash::Sha512);
|
|
||||||
hash.addData(key);
|
|
||||||
QByteArray h = hash.result().toHex();
|
|
||||||
|
|
||||||
QByteArray decKey = QByteArray::fromHex(h.left(64));
|
|
||||||
QByteArray iv = QByteArray::fromHex(h.mid(64, 32));
|
|
||||||
QByteArray ba = QByteArray::fromBase64(encryptedPayload);
|
|
||||||
|
|
||||||
QSimpleCrypto::QBlockCipher cipher;
|
|
||||||
decryptedPayload = cipher.decryptAesBlockCipher(ba, decKey, iv);
|
|
||||||
} else {
|
|
||||||
decryptedPayload = encryptedPayload;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} catch (...) {
|
|
||||||
Utils::logException();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
|
GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
|
||||||
const bool isStrictKillSwitchEnabled, const std::shared_ptr<Settings> &settings,
|
const bool isStrictKillSwitchEnabled, QObject *parent)
|
||||||
QObject *parent)
|
|
||||||
: QObject(parent),
|
: QObject(parent),
|
||||||
m_gatewayEndpoint(gatewayEndpoint),
|
m_gatewayEndpoint(gatewayEndpoint),
|
||||||
m_isDevEnvironment(isDevEnvironment),
|
m_isDevEnvironment(isDevEnvironment),
|
||||||
m_requestTimeoutMsecs(requestTimeoutMsecs),
|
m_requestTimeoutMsecs(requestTimeoutMsecs),
|
||||||
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled),
|
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled)
|
||||||
m_settings(settings)
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,33 +281,25 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
|
|||||||
auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString("");
|
auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString("");
|
||||||
auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString("");
|
auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString("");
|
||||||
|
|
||||||
QStringList primaryBaseUrls;
|
QStringList baseUrls;
|
||||||
QStringList fallbackBaseUrls;
|
|
||||||
if (m_isDevEnvironment) {
|
if (m_isDevEnvironment) {
|
||||||
primaryBaseUrls = QString(DEV_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
baseUrls = QString(DEV_S3_ENDPOINT).split(", ");
|
||||||
} else {
|
} else {
|
||||||
primaryBaseUrls = QString(PROD_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
baseUrls = QString(PROD_S3_ENDPOINT).split(", ");
|
||||||
fallbackBaseUrls = QString(FALLBACK_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
QStringList proxyStorageUrls;
|
||||||
appendStorageUrls(primaryBaseUrls, proxyStorageUrls);
|
if (!serviceType.isEmpty()) {
|
||||||
appendStorageUrls(fallbackBaseUrls, proxyStorageUrls);
|
for (const auto &baseUrl : baseUrls) {
|
||||||
const QString proxyUrlsCacheKey = getProxyUrlsCacheKey(serviceType, userCountryCode);
|
QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8();
|
||||||
|
proxyStorageUrls.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)
|
||||||
|
+ ".json");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto &baseUrl : baseUrls)
|
||||||
|
proxyStorageUrls.push_back(baseUrl + "endpoints.json");
|
||||||
|
|
||||||
getProxyUrlsAsync(proxyStorageUrls, 0, proxyUrlsCacheKey, [this, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) {
|
getProxyUrlsAsync(proxyStorageUrls, 0, [this, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) {
|
||||||
getProxyUrlAsync(proxyUrls, 0, [this, encRequestData, endpoint, processResponse](const QString &proxyUrl) {
|
getProxyUrlAsync(proxyUrls, 0, [this, encRequestData, endpoint, processResponse](const QString &proxyUrl) {
|
||||||
bypassProxyAsync(endpoint, proxyUrl, encRequestData,
|
bypassProxyAsync(endpoint, proxyUrl, encRequestData,
|
||||||
[processResponse, this](const QByteArray &decryptedBody, bool isDecryptionSuccessful,
|
[processResponse, this](const QByteArray &decryptedBody, bool isDecryptionSuccessful,
|
||||||
@@ -401,48 +324,31 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
|
|||||||
QStringList GatewayController::getProxyUrls(const QString &serviceType, const QString &userCountryCode)
|
QStringList GatewayController::getProxyUrls(const QString &serviceType, const QString &userCountryCode)
|
||||||
{
|
{
|
||||||
QNetworkRequest request;
|
QNetworkRequest request;
|
||||||
request.setTransferTimeout(proxyStorageRequestTimeoutMsecs);
|
request.setTransferTimeout(m_requestTimeoutMsecs);
|
||||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
|
|
||||||
QEventLoop wait;
|
QEventLoop wait;
|
||||||
QList<QSslError> sslErrors;
|
QList<QSslError> sslErrors;
|
||||||
QNetworkReply *reply;
|
QNetworkReply *reply;
|
||||||
|
|
||||||
QStringList primaryBaseUrls;
|
QStringList baseUrls;
|
||||||
QStringList fallbackBaseUrls;
|
|
||||||
if (m_isDevEnvironment) {
|
if (m_isDevEnvironment) {
|
||||||
primaryBaseUrls = QString(DEV_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
baseUrls = QString(DEV_S3_ENDPOINT).split(", ");
|
||||||
} else {
|
} else {
|
||||||
primaryBaseUrls = QString(PROD_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
baseUrls = QString(PROD_S3_ENDPOINT).split(", ");
|
||||||
fallbackBaseUrls = QString(FALLBACK_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::random_device randomDevice;
|
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||||
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;
|
QStringList proxyStorageUrls;
|
||||||
appendStorageUrls(primaryBaseUrls, proxyStorageUrls);
|
if (!serviceType.isEmpty()) {
|
||||||
appendStorageUrls(fallbackBaseUrls, proxyStorageUrls);
|
for (const auto &baseUrl : baseUrls) {
|
||||||
const QString proxyUrlsCacheKey = getProxyUrlsCacheKey(serviceType, userCountryCode);
|
QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8();
|
||||||
const QByteArray cachedProxyUrlsEncrypted = m_settings->readGatewayProxyUrls(proxyUrlsCacheKey);
|
proxyStorageUrls.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) + ".json");
|
||||||
|
}
|
||||||
if (proxyStorageUrls.empty()) {
|
}
|
||||||
qDebug() << "empty storage endpoint list";
|
for (const auto &baseUrl : baseUrls) {
|
||||||
return readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment);
|
proxyStorageUrls.push_back(baseUrl + "endpoints.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto &proxyStorageUrl : proxyStorageUrls) {
|
for (const auto &proxyStorageUrl : proxyStorageUrls) {
|
||||||
@@ -457,8 +363,26 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
|
|||||||
auto encryptedResponseBody = reply->readAll();
|
auto encryptedResponseBody = reply->readAll();
|
||||||
reply->deleteLater();
|
reply->deleteLater();
|
||||||
|
|
||||||
|
EVP_PKEY *privateKey = nullptr;
|
||||||
QByteArray responseBody;
|
QByteArray responseBody;
|
||||||
if (!decryptProxyUrlsPayload(encryptedResponseBody, m_isDevEnvironment, responseBody)) {
|
try {
|
||||||
|
if (!m_isDevEnvironment) {
|
||||||
|
QCryptographicHash hash(QCryptographicHash::Sha512);
|
||||||
|
hash.addData(key);
|
||||||
|
QByteArray hashResult = hash.result().toHex();
|
||||||
|
|
||||||
|
QByteArray key = QByteArray::fromHex(hashResult.left(64));
|
||||||
|
QByteArray iv = QByteArray::fromHex(hashResult.mid(64, 32));
|
||||||
|
|
||||||
|
QByteArray ba = QByteArray::fromBase64(encryptedResponseBody);
|
||||||
|
|
||||||
|
QSimpleCrypto::QBlockCipher blockCipher;
|
||||||
|
responseBody = blockCipher.decryptAesBlockCipher(ba, key, iv);
|
||||||
|
} else {
|
||||||
|
responseBody = encryptedResponseBody;
|
||||||
|
}
|
||||||
|
} catch (...) {
|
||||||
|
Utils::logException();
|
||||||
qCritical() << "error loading private key from environment variables or decrypting payload" << encryptedResponseBody;
|
qCritical() << "error loading private key from environment variables or decrypting payload" << encryptedResponseBody;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -469,8 +393,6 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
|
|||||||
for (const auto &endpoint : endpointsArray) {
|
for (const auto &endpoint : endpointsArray) {
|
||||||
endpoints.push_back(endpoint.toString());
|
endpoints.push_back(endpoint.toString());
|
||||||
}
|
}
|
||||||
m_settings->writeGatewayProxyUrls(proxyUrlsCacheKey, encryptedResponseBody);
|
|
||||||
|
|
||||||
return endpoints;
|
return endpoints;
|
||||||
} else {
|
} else {
|
||||||
auto replyError = reply->error();
|
auto replyError = reply->error();
|
||||||
@@ -482,7 +404,7 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
|
|||||||
reply->deleteLater();
|
reply->deleteLater();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment);
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody,
|
bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody,
|
||||||
@@ -490,14 +412,12 @@ bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &rep
|
|||||||
{
|
{
|
||||||
const QByteArray &responseBody = decryptedResponseBody;
|
const QByteArray &responseBody = decryptedResponseBody;
|
||||||
|
|
||||||
int apiHttpStatus = -1;
|
int httpStatus = -1;
|
||||||
QString apiErrorMessage;
|
|
||||||
if (isDecryptionSuccessful) {
|
if (isDecryptionSuccessful) {
|
||||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
|
||||||
if (jsonDoc.isObject()) {
|
if (jsonDoc.isObject()) {
|
||||||
QJsonObject jsonObj = jsonDoc.object();
|
QJsonObject jsonObj = jsonDoc.object();
|
||||||
apiHttpStatus = jsonObj.value("http_status").toInt(-1);
|
httpStatus = jsonObj.value("http_status").toInt(-1);
|
||||||
apiErrorMessage = jsonObj.value(QStringLiteral("message")).toString().trimmed();
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
qDebug() << "failed to decrypt the data";
|
qDebug() << "failed to decrypt the data";
|
||||||
@@ -508,12 +428,10 @@ bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &rep
|
|||||||
qDebug() << "timeout occurred";
|
qDebug() << "timeout occurred";
|
||||||
qDebug() << replyError;
|
qDebug() << replyError;
|
||||||
return true;
|
return true;
|
||||||
}
|
} else if (responseBody.contains("html")) {
|
||||||
if (responseBody.contains("html")) {
|
|
||||||
qDebug() << "the response contains an html tag";
|
qDebug() << "the response contains an html tag";
|
||||||
return true;
|
return true;
|
||||||
}
|
} else if (httpStatus == httpStatusCodeNotFound) {
|
||||||
if (apiHttpStatus == httpStatusCodeNotFound) {
|
|
||||||
if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2)
|
if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2)
|
||||||
|| responseBody.contains(errorResponsePattern3)) {
|
|| responseBody.contains(errorResponsePattern3)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -521,25 +439,16 @@ bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &rep
|
|||||||
qDebug() << replyError;
|
qDebug() << replyError;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
} else if (httpStatus == httpStatusCodeNotImplemented) {
|
||||||
if (apiHttpStatus == httpStatusCodeNotImplemented) {
|
|
||||||
if (responseBody.contains(updateRequestResponsePattern)) {
|
if (responseBody.contains(updateRequestResponsePattern)) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
qDebug() << replyError;
|
qDebug() << replyError;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
} else if (httpStatus == httpStatusCodeConflict) {
|
||||||
if (apiHttpStatus == httpStatusCodeConflict) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
} else if (replyError != QNetworkReply::NetworkError::NoError) {
|
||||||
if (apiHttpStatus == httpStatusCodePaymentRequired) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (apiHttpStatus == httpStatusCodeUnprocessableEntity) {
|
|
||||||
return apiErrorMessage != unprocessableSubscriptionMessage;
|
|
||||||
}
|
|
||||||
if (replyError != QNetworkReply::NetworkError::NoError) {
|
|
||||||
qDebug() << replyError;
|
qDebug() << replyError;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -620,17 +529,15 @@ void GatewayController::bypassProxy(const QString &endpoint, const QString &serv
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
|
void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
|
||||||
const QString &proxyUrlsCacheKey, std::function<void(const QStringList &)> onComplete)
|
std::function<void(const QStringList &)> onComplete)
|
||||||
{
|
{
|
||||||
const QByteArray cachedProxyUrlsEncrypted = m_settings->readGatewayProxyUrls(proxyUrlsCacheKey);
|
|
||||||
|
|
||||||
if (currentProxyStorageIndex >= proxyStorageUrls.size()) {
|
if (currentProxyStorageIndex >= proxyStorageUrls.size()) {
|
||||||
onComplete(shuffledProxyUrls(readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment)));
|
onComplete({});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QNetworkRequest request;
|
QNetworkRequest request;
|
||||||
request.setTransferTimeout(proxyStorageRequestTimeoutMsecs);
|
request.setTransferTimeout(m_requestTimeoutMsecs);
|
||||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
request.setUrl(proxyStorageUrls[currentProxyStorageIndex]);
|
request.setUrl(proxyStorageUrls[currentProxyStorageIndex]);
|
||||||
|
|
||||||
@@ -638,17 +545,33 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co
|
|||||||
|
|
||||||
// connect(reply, &QNetworkReply::sslErrors, this, [state](const QList<QSslError> &e) { *(state->sslErrors) = e; });
|
// connect(reply, &QNetworkReply::sslErrors, this, [state](const QList<QSslError> &e) { *(state->sslErrors) = e; });
|
||||||
|
|
||||||
connect(reply, &QNetworkReply::finished, this,
|
connect(reply, &QNetworkReply::finished, this, [this, proxyStorageUrls, currentProxyStorageIndex, onComplete, reply]() {
|
||||||
[this, proxyStorageUrls, currentProxyStorageIndex, proxyUrlsCacheKey, onComplete, reply]() {
|
|
||||||
if (reply->error() == QNetworkReply::NoError) {
|
if (reply->error() == QNetworkReply::NoError) {
|
||||||
QByteArray encrypted = reply->readAll();
|
QByteArray encrypted = reply->readAll();
|
||||||
reply->deleteLater();
|
reply->deleteLater();
|
||||||
|
|
||||||
QByteArray responseBody;
|
QByteArray responseBody;
|
||||||
if (!decryptProxyUrlsPayload(encrypted, m_isDevEnvironment, responseBody)) {
|
try {
|
||||||
|
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||||
|
if (!m_isDevEnvironment) {
|
||||||
|
QCryptographicHash hash(QCryptographicHash::Sha512);
|
||||||
|
hash.addData(key);
|
||||||
|
QByteArray h = hash.result().toHex();
|
||||||
|
|
||||||
|
QByteArray decKey = QByteArray::fromHex(h.left(64));
|
||||||
|
QByteArray iv = QByteArray::fromHex(h.mid(64, 32));
|
||||||
|
QByteArray ba = QByteArray::fromBase64(encrypted);
|
||||||
|
|
||||||
|
QSimpleCrypto::QBlockCipher cipher;
|
||||||
|
responseBody = cipher.decryptAesBlockCipher(ba, decKey, iv);
|
||||||
|
} else {
|
||||||
|
responseBody = encrypted;
|
||||||
|
}
|
||||||
|
} catch (...) {
|
||||||
|
Utils::logException();
|
||||||
qCritical() << "error decrypting payload";
|
qCritical() << "error decrypting payload";
|
||||||
QMetaObject::invokeMethod(
|
QMetaObject::invokeMethod(
|
||||||
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, proxyUrlsCacheKey, onComplete); }, Qt::QueuedConnection);
|
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -656,9 +579,13 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co
|
|||||||
QStringList endpoints;
|
QStringList endpoints;
|
||||||
for (const QJsonValue &endpoint : endpointsArray)
|
for (const QJsonValue &endpoint : endpointsArray)
|
||||||
endpoints.push_back(endpoint.toString());
|
endpoints.push_back(endpoint.toString());
|
||||||
m_settings->writeGatewayProxyUrls(proxyUrlsCacheKey, encrypted);
|
|
||||||
|
|
||||||
onComplete(shuffledProxyUrls(endpoints));
|
QStringList shuffled = endpoints;
|
||||||
|
std::random_device randomDevice;
|
||||||
|
std::mt19937 generator(randomDevice());
|
||||||
|
std::shuffle(shuffled.begin(), shuffled.end(), generator);
|
||||||
|
|
||||||
|
onComplete(shuffled);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -667,7 +594,7 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co
|
|||||||
qDebug() << "go to the next storage endpoint";
|
qDebug() << "go to the next storage endpoint";
|
||||||
reply->deleteLater();
|
reply->deleteLater();
|
||||||
QMetaObject::invokeMethod(
|
QMetaObject::invokeMethod(
|
||||||
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, proxyUrlsCacheKey, onComplete); }, Qt::QueuedConnection);
|
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,6 @@
|
|||||||
#include <QPair>
|
#include <QPair>
|
||||||
#include <QPromise>
|
#include <QPromise>
|
||||||
#include <QSharedPointer>
|
#include <QSharedPointer>
|
||||||
#include <QString>
|
|
||||||
#include <QStringList>
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
#include "core/defs.h"
|
#include "core/defs.h"
|
||||||
|
|
||||||
@@ -17,16 +14,13 @@
|
|||||||
#include "platforms/ios/ios_controller.h"
|
#include "platforms/ios/ios_controller.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
class Settings;
|
|
||||||
|
|
||||||
class GatewayController : public QObject
|
class GatewayController : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
|
explicit GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
|
||||||
const bool isStrictKillSwitchEnabled, const std::shared_ptr<Settings> &settings,
|
const bool isStrictKillSwitchEnabled, QObject *parent = nullptr);
|
||||||
QObject *parent = nullptr);
|
|
||||||
|
|
||||||
amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody);
|
amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody);
|
||||||
QFuture<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject apiPayload);
|
QFuture<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject apiPayload);
|
||||||
@@ -59,7 +53,7 @@ private:
|
|||||||
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction);
|
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction);
|
||||||
|
|
||||||
void getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
|
void getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
|
||||||
const QString &proxyUrlsCacheKey, std::function<void(const QStringList &)> onComplete);
|
std::function<void(const QStringList &)> onComplete);
|
||||||
void getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex, std::function<void(const QString &)> onComplete);
|
void getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex, std::function<void(const QString &)> onComplete);
|
||||||
void bypassProxyAsync(
|
void bypassProxyAsync(
|
||||||
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
|
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
|
||||||
@@ -69,7 +63,6 @@ private:
|
|||||||
QString m_gatewayEndpoint;
|
QString m_gatewayEndpoint;
|
||||||
bool m_isDevEnvironment = false;
|
bool m_isDevEnvironment = false;
|
||||||
bool m_isStrictKillSwitchEnabled = false;
|
bool m_isStrictKillSwitchEnabled = false;
|
||||||
std::shared_ptr<Settings> m_settings;
|
|
||||||
|
|
||||||
inline static QString m_proxyUrl;
|
inline static QString m_proxyUrl;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -123,9 +123,6 @@ namespace amnezia
|
|||||||
ApiUpdateRequestError = 1111,
|
ApiUpdateRequestError = 1111,
|
||||||
ApiSubscriptionExpiredError = 1112,
|
ApiSubscriptionExpiredError = 1112,
|
||||||
ApiPurchaseError = 1113,
|
ApiPurchaseError = 1113,
|
||||||
ApiSubscriptionNotActiveError = 1114,
|
|
||||||
ApiNoPurchasedSubscriptionsError = 1115,
|
|
||||||
ApiTrialAlreadyUsedError = 1116,
|
|
||||||
|
|
||||||
// QFile errors
|
// QFile errors
|
||||||
OpenError = 1200,
|
OpenError = 1200,
|
||||||
|
|||||||
@@ -80,9 +80,6 @@ QString errorString(ErrorCode code) {
|
|||||||
case (ErrorCode::ApiUpdateRequestError): errorMessage = QObject::tr("Please update the application to use this feature"); break;
|
case (ErrorCode::ApiUpdateRequestError): errorMessage = QObject::tr("Please update the application to use this feature"); break;
|
||||||
case (ErrorCode::ApiSubscriptionExpiredError): errorMessage = QObject::tr("Your Amnezia Premium subscription has expired.\n Please check your email for renewal instructions.\n If you haven't received an email, please contact our support."); break;
|
case (ErrorCode::ApiSubscriptionExpiredError): errorMessage = QObject::tr("Your Amnezia Premium subscription has expired.\n Please check your email for renewal instructions.\n If you haven't received an email, please contact our support."); break;
|
||||||
case (ErrorCode::ApiPurchaseError): errorMessage = QObject::tr("Unable to process purchase"); break;
|
case (ErrorCode::ApiPurchaseError): errorMessage = QObject::tr("Unable to process purchase"); break;
|
||||||
case (ErrorCode::ApiSubscriptionNotActiveError): errorMessage = QObject::tr("No active subscription found"); break;
|
|
||||||
case (ErrorCode::ApiNoPurchasedSubscriptionsError): errorMessage = QObject::tr("No purchased subscriptions found. Please purchase a subscription first"); break;
|
|
||||||
case (ErrorCode::ApiTrialAlreadyUsedError): errorMessage = QObject::tr("This email address has already been used to activate a trial"); break;
|
|
||||||
|
|
||||||
// QFile errors
|
// QFile errors
|
||||||
case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break;
|
case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
#include <sys/socket.h>
|
#include <sys/socket.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#endif
|
#endif
|
||||||
#if defined(Q_OS_MAC) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
#if defined(Q_OS_MAC) && !defined(Q_OS_IOS) && !defined(Q_OS_TVOS) && !defined(MACOS_NE)
|
||||||
#include <sys/param.h>
|
#include <sys/param.h>
|
||||||
#include <sys/sysctl.h>
|
#include <sys/sysctl.h>
|
||||||
#include <sys/socket.h>
|
#include <sys/socket.h>
|
||||||
@@ -404,7 +404,7 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
|
|||||||
close(sock);
|
close(sock);
|
||||||
return { gateway_address, QNetworkInterface::interfaceFromName(interface) };
|
return { gateway_address, QNetworkInterface::interfaceFromName(interface) };
|
||||||
#endif
|
#endif
|
||||||
#if defined(Q_OS_MAC) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
#if defined(Q_OS_MAC) && !defined(Q_OS_IOS) && !defined(Q_OS_TVOS) && !defined(MACOS_NE)
|
||||||
QString gateway;
|
QString gateway;
|
||||||
int index = -1;
|
int index = -1;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QJsonArray>
|
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QHostAddress>
|
|
||||||
#include <QRandomGenerator>
|
|
||||||
#include <QTcpServer>
|
|
||||||
#include <stdexcept>
|
|
||||||
#include "3rd/QJsonStruct/QJsonIO.hpp"
|
#include "3rd/QJsonStruct/QJsonIO.hpp"
|
||||||
#include "transfer.h"
|
#include "transfer.h"
|
||||||
#include "serialization.h"
|
#include "serialization.h"
|
||||||
@@ -19,125 +14,25 @@ namespace amnezia::serialization::inbounds
|
|||||||
// "port": 10808,
|
// "port": 10808,
|
||||||
// "protocol": "socks",
|
// "protocol": "socks",
|
||||||
// "settings": {
|
// "settings": {
|
||||||
// "auth": "password",
|
|
||||||
// "accounts": [{"user": "...", "pass": "..."}],
|
|
||||||
// "udp": true
|
// "udp": true
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
//],
|
//],
|
||||||
|
|
||||||
const static QString listen = "127.0.0.1";
|
const static QString listen = "127.0.0.1";
|
||||||
const static int defaultPort = 10808;
|
const static int port = 10808;
|
||||||
const static QString protocol = "socks";
|
const static QString protocol = "socks";
|
||||||
|
|
||||||
static int indexOfSocksInbound(const QJsonArray &inbounds)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < inbounds.size(); ++i) {
|
|
||||||
const QString p = inbounds.at(i).toObject().value(QLatin1String("protocol")).toString();
|
|
||||||
if (p.compare(QLatin1String("socks"), Qt::CaseInsensitive) == 0)
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ask the OS for a free TCP port on loopback (same stack as inbound "listen": "127.0.0.1").
|
|
||||||
static int acquireFreeLocalPort()
|
|
||||||
{
|
|
||||||
QTcpServer probe;
|
|
||||||
if (!probe.listen(QHostAddress(QStringLiteral("127.0.0.1")), 0)) {
|
|
||||||
throw std::runtime_error(
|
|
||||||
"Failed to bind a local TCP port on 127.0.0.1 for SOCKS inbound "
|
|
||||||
"(QTcpServer::listen failed; possible permission or OS network error).");
|
|
||||||
}
|
|
||||||
return static_cast<int>(probe.serverPort());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generates a hex string of `byteCount` random bytes (URL-safe, no special chars).
|
|
||||||
static QString generateRandomHex(int byteCount)
|
|
||||||
{
|
|
||||||
if (byteCount <= 0)
|
|
||||||
return {};
|
|
||||||
// fillRange writes full quint32 words; size the buffer to a multiple of 4 bytes to avoid
|
|
||||||
// overrunning a short buffer when byteCount is not divisible by 4.
|
|
||||||
const int numUint32 = (byteCount + int(sizeof(quint32)) - 1) / int(sizeof(quint32));
|
|
||||||
QByteArray buf(numUint32 * int(sizeof(quint32)), '\0');
|
|
||||||
QRandomGenerator::system()->fillRange(reinterpret_cast<quint32 *>(buf.data()), numUint32);
|
|
||||||
return QString::fromLatin1(buf.left(byteCount).toHex());
|
|
||||||
}
|
|
||||||
|
|
||||||
QJsonObject GenerateInboundEntry()
|
QJsonObject GenerateInboundEntry()
|
||||||
{
|
{
|
||||||
QJsonObject root;
|
QJsonObject root;
|
||||||
QJsonIO::SetValue(root, listen, "listen");
|
QJsonIO::SetValue(root, listen, "listen");
|
||||||
QJsonIO::SetValue(root, defaultPort, "port");
|
QJsonIO::SetValue(root, port, "port");
|
||||||
QJsonIO::SetValue(root, protocol, "protocol");
|
QJsonIO::SetValue(root, protocol, "protocol");
|
||||||
QJsonIO::SetValue(root, true, "settings", "udp");
|
QJsonIO::SetValue(root, true, "settings", "udp");
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
InboundCredentials GetInboundCredentials(const QJsonObject &xrayConfig)
|
|
||||||
{
|
|
||||||
InboundCredentials creds;
|
|
||||||
creds.port = defaultPort;
|
|
||||||
|
|
||||||
const QJsonArray inbounds = xrayConfig.value("inbounds").toArray();
|
|
||||||
const int socksIdx = indexOfSocksInbound(inbounds);
|
|
||||||
if (socksIdx < 0)
|
|
||||||
return creds;
|
|
||||||
|
|
||||||
const QJsonObject inbound = inbounds.at(socksIdx).toObject();
|
|
||||||
creds.port = inbound.value("port").toInt(defaultPort);
|
|
||||||
|
|
||||||
const QJsonObject settings = inbound.value("settings").toObject();
|
|
||||||
const QJsonArray accounts = settings.value("accounts").toArray();
|
|
||||||
if (accounts.isEmpty())
|
|
||||||
return creds;
|
|
||||||
|
|
||||||
const QJsonObject account = accounts.first().toObject();
|
|
||||||
creds.username = account.value("user").toString();
|
|
||||||
creds.password = account.value("pass").toString();
|
|
||||||
return creds;
|
|
||||||
}
|
|
||||||
|
|
||||||
InboundCredentials EnsureInboundAuth(QJsonObject &xrayConfig)
|
|
||||||
{
|
|
||||||
QJsonArray inbounds = xrayConfig.value("inbounds").toArray();
|
|
||||||
const int socksIdx = indexOfSocksInbound(inbounds);
|
|
||||||
if (socksIdx < 0)
|
|
||||||
return GetInboundCredentials(xrayConfig); // no SOCKS inbound to patch
|
|
||||||
|
|
||||||
QJsonObject inbound = inbounds.at(socksIdx).toObject();
|
|
||||||
InboundCredentials creds;
|
|
||||||
creds.port = acquireFreeLocalPort();
|
|
||||||
inbound["port"] = creds.port;
|
|
||||||
|
|
||||||
QJsonObject settings = inbound.value("settings").toObject();
|
|
||||||
const QJsonArray accounts = settings.value("accounts").toArray();
|
|
||||||
if (!accounts.isEmpty()) {
|
|
||||||
const QJsonObject account = accounts.first().toObject();
|
|
||||||
creds.username = account.value("user").toString();
|
|
||||||
creds.password = account.value("pass").toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (creds.username.isEmpty() || creds.password.isEmpty()) {
|
|
||||||
// Generate fresh credentials for this session (never persisted)
|
|
||||||
creds.username = generateRandomHex(8); // 16 hex chars
|
|
||||||
creds.password = generateRandomHex(16); // 32 hex chars
|
|
||||||
QJsonObject account;
|
|
||||||
account["user"] = creds.username;
|
|
||||||
account["pass"] = creds.password;
|
|
||||||
settings["accounts"] = QJsonArray{ account };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always ensure auth mode is enforced, even for imported configs that had
|
|
||||||
// accounts but auth: "noauth" (or no auth field at all).
|
|
||||||
settings["auth"] = QStringLiteral("password");
|
|
||||||
inbound["settings"] = settings;
|
|
||||||
inbounds[socksIdx] = inbound;
|
|
||||||
xrayConfig["inbounds"] = inbounds;
|
|
||||||
|
|
||||||
return creds;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace amnezia::serialization::inbounds
|
} // namespace amnezia::serialization::inbounds
|
||||||
|
|
||||||
|
|||||||
@@ -60,24 +60,7 @@ namespace amnezia::serialization
|
|||||||
|
|
||||||
namespace inbounds
|
namespace inbounds
|
||||||
{
|
{
|
||||||
struct InboundCredentials {
|
|
||||||
QString username;
|
|
||||||
QString password;
|
|
||||||
int port;
|
|
||||||
};
|
|
||||||
|
|
||||||
QJsonObject GenerateInboundEntry();
|
QJsonObject GenerateInboundEntry();
|
||||||
|
|
||||||
// Reads existing SOCKS5 auth from the first inbound with protocol "socks"
|
|
||||||
// (.settings.accounts[0]). Returns empty username/password if none.
|
|
||||||
InboundCredentials GetInboundCredentials(const QJsonObject &xrayConfig);
|
|
||||||
|
|
||||||
// Ensures SOCKS5 auth is present on the inbound whose protocol is "socks".
|
|
||||||
// Re-uses existing credentials if already set; otherwise generates random ones
|
|
||||||
// and writes them into the config. Assigns a free loopback TCP port each session
|
|
||||||
// (OS-assigned). Throws std::runtime_error if a SOCKS inbound exists but binding
|
|
||||||
// a local port on 127.0.0.1 fails (e.g. permissions or OS error).
|
|
||||||
InboundCredentials EnsureInboundAuth(QJsonObject &xrayConfig);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+32
-32
@@ -390,55 +390,55 @@ bool Daemon::parseConfig(const QJsonObject& obj, InterfaceConfig& config) {
|
|||||||
|
|
||||||
config.m_killSwitchEnabled = QVariant(obj.value("killSwitchOption").toString()).toBool();
|
config.m_killSwitchEnabled = QVariant(obj.value("killSwitchOption").toString()).toBool();
|
||||||
|
|
||||||
if (const auto jc = obj.value("Jc"); !jc.isUndefined()) {
|
if (!obj.value("Jc").isNull()) {
|
||||||
config.m_junkPacketCount = jc.toString();
|
config.m_junkPacketCount = obj.value("Jc").toString();
|
||||||
}
|
}
|
||||||
if (const auto jmin = obj.value("Jmin"); !jmin.isUndefined()) {
|
if (!obj.value("Jmin").isNull()) {
|
||||||
config.m_junkPacketMinSize = jmin.toString();
|
config.m_junkPacketMinSize = obj.value("Jmin").toString();
|
||||||
}
|
}
|
||||||
if (const auto jmax = obj.value("Jmax"); !jmax.isUndefined()) {
|
if (!obj.value("Jmax").isNull()) {
|
||||||
config.m_junkPacketMaxSize = jmax.toString();
|
config.m_junkPacketMaxSize = obj.value("Jmax").toString();
|
||||||
}
|
}
|
||||||
if (const auto s1 = obj.value("S1"); !s1.isUndefined()) {
|
if (!obj.value("S1").isNull()) {
|
||||||
config.m_initPacketJunkSize = s1.toString();
|
config.m_initPacketJunkSize = obj.value("S1").toString();
|
||||||
}
|
}
|
||||||
if (const auto s2 = obj.value("S2"); !s2.isUndefined()) {
|
if (!obj.value("S2").isNull()) {
|
||||||
config.m_responsePacketJunkSize = s2.toString();
|
config.m_responsePacketJunkSize = obj.value("S2").toString();
|
||||||
}
|
}
|
||||||
if (const auto s3 = obj.value("S3"); !s3.isUndefined()) {
|
if (!obj.value("S3").isNull()) {
|
||||||
config.m_cookieReplyPacketJunkSize = s3.toString();
|
config.m_cookieReplyPacketJunkSize = obj.value("S3").toString();
|
||||||
}
|
}
|
||||||
if (const auto s4 = obj.value("S4"); !s4.isUndefined()) {
|
if (!obj.value("S4").isNull()) {
|
||||||
config.m_transportPacketJunkSize = s4.toString();
|
config.m_transportPacketJunkSize = obj.value("S4").toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (const auto h1 = obj.value("H1"); !h1.isUndefined()) {
|
if (!obj.value("H1").isNull()) {
|
||||||
config.m_initPacketMagicHeader = h1.toString();
|
config.m_initPacketMagicHeader = obj.value("H1").toString();
|
||||||
}
|
}
|
||||||
if (const auto h2 = obj.value("H2"); !h2.isUndefined()) {
|
if (!obj.value("H2").isNull()) {
|
||||||
config.m_responsePacketMagicHeader = h2.toString();
|
config.m_responsePacketMagicHeader = obj.value("H2").toString();
|
||||||
}
|
}
|
||||||
if (const auto h3 = obj.value("H3"); !h3.isUndefined()) {
|
if (!obj.value("H3").isNull()) {
|
||||||
config.m_underloadPacketMagicHeader = h3.toString();
|
config.m_underloadPacketMagicHeader = obj.value("H3").toString();
|
||||||
}
|
}
|
||||||
if (const auto h4 = obj.value("H4"); !h4.isUndefined()) {
|
if (!obj.value("H4").isNull()) {
|
||||||
config.m_transportPacketMagicHeader = h4.toString();
|
config.m_transportPacketMagicHeader = obj.value("H4").toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (const auto i1 = obj.value("I1"); !i1.isUndefined()) {
|
if (!obj.value("I1").isNull()) {
|
||||||
config.m_specialJunk["I1"] = i1.toString();
|
config.m_specialJunk["I1"] = obj.value("I1").toString();
|
||||||
}
|
}
|
||||||
if (const auto i2 = obj.value("I2"); !i2.isUndefined()) {
|
if (!obj.value("I2").isNull()) {
|
||||||
config.m_specialJunk["I2"] = i2.toString();
|
config.m_specialJunk["I2"] = obj.value("I2").toString();
|
||||||
}
|
}
|
||||||
if (const auto i3 = obj.value("I3"); !i3.isUndefined()) {
|
if (!obj.value("I3").isNull()) {
|
||||||
config.m_specialJunk["I3"] = i3.toString();
|
config.m_specialJunk["I3"] = obj.value("I3").toString();
|
||||||
}
|
}
|
||||||
if (const auto i4 = obj.value("I4"); !i4.isUndefined()) {
|
if (!obj.value("I4").isNull()) {
|
||||||
config.m_specialJunk["I4"] = i4.toString();
|
config.m_specialJunk["I4"] = obj.value("I4").toString();
|
||||||
}
|
}
|
||||||
if (const auto i5 = obj.value("I5"); !i5.isUndefined()) {
|
if (!obj.value("I5").isNull()) {
|
||||||
config.m_specialJunk["I5"] = i5.toString();
|
config.m_specialJunk["I5"] = obj.value("I5").toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M15 21V17C15 16.4696 15.2107 15.9609 15.5858 15.5858C15.9609 15.2107 16.4696 15 17 15H21" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
<path d="M7 4V6C7.21572 6.61347 7.62494 7.14024 8.16602 7.50096C8.7071 7.86168 9.35075 8.03682 10 8V8C10.5304 8 11.0391 8.21071 11.4142 8.58579C11.7893 8.96086 12 9.46957 12 10C12 10.5304 12.2107 11.0391 12.5858 11.4142C12.9609 11.7893 13.4696 12 14 12C14.5304 12 15.0391 11.7893 15.4142 11.4142C15.7893 11.0391 16 10.5304 16 10C16 9.46957 16.2107 8.96086 16.5858 8.58579C16.9609 8.21071 17.4696 8 18 8H21" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
<path d="M3 11H5C5.53043 11 6.03914 11.2107 6.41421 11.5858C6.78929 11.9609 7 12.4696 7 13V14C7 14.5304 7.21071 15.0391 7.58579 15.4142C7.96086 15.7893 8.46957 16 9 16C9.53043 16 10.0391 16.2107 10.4142 16.5858C10.7893 16.9609 11 17.4696 11 18V22" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
<path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,3 +0,0 @@
|
|||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M18.1777 8C23.2737 8 23.2737 16 18.1777 16C13.0827 16 11.0447 8 5.43875 8C0.85375 8 0.85375 16 5.43875 16C11.0447 16 13.0828 8 18.1788 8H18.1777Z" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 342 B |
@@ -1,4 +0,0 @@
|
|||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M17 2H7C5.89543 2 5 2.89543 5 4V20C5 21.1046 5.89543 22 7 22H17C18.1046 22 19 21.1046 19 20V4C19 2.89543 18.1046 2 17 2Z" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
<path d="M12 18H12.01" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 423 B |
@@ -0,0 +1,54 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleAllowMixedLocalizations</key>
|
||||||
|
<true/>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>en</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>${QT_INTERNAL_DOLLAR_VAR}{PRODUCT_NAME}</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>${MACOSX_BUNDLE_COPYRIGHT}</string>
|
||||||
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
<false/>
|
||||||
|
<key>UIRequiredDeviceCapabilities</key>
|
||||||
|
<array/>
|
||||||
|
<key>UIRequiresFullScreen</key>
|
||||||
|
<true/>
|
||||||
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>UILaunchStoryboardName</key>
|
||||||
|
<string>AmneziaVPNLaunchScreen</string>
|
||||||
|
<key>UIUserInterfaceStyle</key>
|
||||||
|
<string>Light</string>
|
||||||
|
<key>com.wireguard.ios.app_group_id</key>
|
||||||
|
<string>group.org.amnezia.AmneziaVPN</string>
|
||||||
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSAllowsLocalNetworking</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder.AppleTV.Storyboard" version="3.0" toolsVersion="13122.16" targetRuntime="AppleTV" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||||
|
<dependencies>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<scene sceneID="tne-QT-ifu">
|
||||||
|
<objects>
|
||||||
|
<viewController id="BYZ-38-t0r" sceneMemberID="viewController">
|
||||||
|
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="1920" height="1080"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
<viewLayoutGuide key="safeArea" id="wu6-TO-1qx"/>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
</document>
|
||||||
@@ -1,6 +1,14 @@
|
|||||||
enable_language(Swift)
|
enable_language(Swift)
|
||||||
|
|
||||||
set(CLIENT_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR}/../..)
|
set(CLIENT_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR}/../..)
|
||||||
|
set(AMNEZIA_THIRDPARTY_ROOT "${CLIENT_ROOT_DIR}/3rd" CACHE PATH "Path to Amnezia client/3rd sources")
|
||||||
|
set(AMNEZIA_IOS_APPLETV ${AMNEZIA_IOS_ENABLE_APPLETV_TARGET})
|
||||||
|
|
||||||
|
if(AMNEZIA_IOS_APPLETV)
|
||||||
|
message("Network Extension tvOS mode is ON")
|
||||||
|
else()
|
||||||
|
message("Network Extension tvOS mode is OFF")
|
||||||
|
endif()
|
||||||
|
|
||||||
add_executable(networkextension)
|
add_executable(networkextension)
|
||||||
set_target_properties(networkextension PROPERTIES
|
set_target_properties(networkextension PROPERTIES
|
||||||
@@ -28,6 +36,23 @@ set_target_properties(networkextension PROPERTIES
|
|||||||
XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/../../Frameworks"
|
XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/../../Frameworks"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if(AMNEZIA_IOS_APPLETV)
|
||||||
|
set_target_properties(networkextension PROPERTIES
|
||||||
|
XCODE_ATTRIBUTE_SUPPORTED_PLATFORMS "appletvos appletvsimulator"
|
||||||
|
XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "3"
|
||||||
|
XCODE_ATTRIBUTE_TVOS_DEPLOYMENT_TARGET "${CMAKE_OSX_DEPLOYMENT_TARGET}"
|
||||||
|
XCODE_ATTRIBUTE_SDKROOT "appletvos"
|
||||||
|
XCODE_ATTRIBUTE_SDKROOT[sdk=appletvos*] "appletvos"
|
||||||
|
XCODE_ATTRIBUTE_SDKROOT[sdk=appletvsimulator*] "appletvsimulator"
|
||||||
|
XCODE_ATTRIBUTE_LIBRARY_SEARCH_PATHS "$(inherited) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)"
|
||||||
|
XCODE_ATTRIBUTE_LIBRARY_SEARCH_PATHS[sdk=appletvos*] "$(inherited) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)"
|
||||||
|
XCODE_ATTRIBUTE_LIBRARY_SEARCH_PATHS[sdk=appletvsimulator*] "$(inherited) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)"
|
||||||
|
XCODE_ATTRIBUTE_EXCLUDED_LIBRARY_SEARCH_PATHS "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS*.sdk/usr/lib/swift"
|
||||||
|
XCODE_ATTRIBUTE_EXCLUDED_FRAMEWORK_SEARCH_PATHS "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS*.sdk/System/Library/Frameworks"
|
||||||
|
LINKER_LANGUAGE Swift
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
if(DEPLOY)
|
if(DEPLOY)
|
||||||
set_target_properties(networkextension PROPERTIES
|
set_target_properties(networkextension PROPERTIES
|
||||||
XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "Apple Distribution"
|
XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "Apple Distribution"
|
||||||
@@ -45,38 +70,49 @@ endif()
|
|||||||
set_target_properties(networkextension PROPERTIES
|
set_target_properties(networkextension PROPERTIES
|
||||||
XCODE_ATTRIBUTE_SWIFT_VERSION "5.0"
|
XCODE_ATTRIBUTE_SWIFT_VERSION "5.0"
|
||||||
XCODE_ATTRIBUTE_CLANG_ENABLE_MODULES "YES"
|
XCODE_ATTRIBUTE_CLANG_ENABLE_MODULES "YES"
|
||||||
XCODE_ATTRIBUTE_SWIFT_OBJC_BRIDGING_HEADER "${CMAKE_CURRENT_SOURCE_DIR}/WireGuardNetworkExtension-Bridging-Header.h"
|
|
||||||
XCODE_ATTRIBUTE_SWIFT_OPTIMIZATION_LEVEL "-Onone"
|
XCODE_ATTRIBUTE_SWIFT_OPTIMIZATION_LEVEL "-Onone"
|
||||||
XCODE_ATTRIBUTE_SWIFT_PRECOMPILE_BRIDGING_HEADER "NO"
|
XCODE_ATTRIBUTE_SWIFT_PRECOMPILE_BRIDGING_HEADER "NO"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
set_target_properties(networkextension PROPERTIES
|
||||||
|
XCODE_ATTRIBUTE_SWIFT_OBJC_BRIDGING_HEADER "${CMAKE_CURRENT_SOURCE_DIR}/WireGuardNetworkExtension-Bridging-Header.h"
|
||||||
|
)
|
||||||
|
|
||||||
set_target_properties("networkextension" PROPERTIES
|
set_target_properties("networkextension" PROPERTIES
|
||||||
XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "X7UJ388FXK"
|
XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "X7UJ388FXK"
|
||||||
)
|
)
|
||||||
|
|
||||||
find_library(FW_ASSETS_LIBRARY AssetsLibrary)
|
|
||||||
find_library(FW_MOBILE_CORE MobileCoreServices)
|
|
||||||
find_library(FW_UI_KIT UIKit)
|
find_library(FW_UI_KIT UIKit)
|
||||||
find_library(FW_LIBRESOLV libresolv.9.tbd)
|
find_library(FW_LIBRESOLV libresolv.9.tbd)
|
||||||
|
|
||||||
target_link_libraries(networkextension PRIVATE ${FW_ASSETS_LIBRARY})
|
if(NOT AMNEZIA_IOS_APPLETV)
|
||||||
target_link_libraries(networkextension PRIVATE ${FW_MOBILE_CORE})
|
target_link_libraries(networkextension PRIVATE ${FW_UI_KIT})
|
||||||
target_link_libraries(networkextension PRIVATE ${FW_UI_KIT})
|
target_link_libraries(networkextension PRIVATE ${FW_LIBRESOLV})
|
||||||
target_link_libraries(networkextension PRIVATE ${FW_LIBRESOLV})
|
else()
|
||||||
|
target_link_libraries(networkextension PRIVATE -lresolv)
|
||||||
|
endif()
|
||||||
|
|
||||||
target_compile_options(networkextension PRIVATE -DGROUP_ID=\"${BUILD_IOS_GROUP_IDENTIFIER}\")
|
target_compile_options(networkextension PRIVATE -DGROUP_ID=\"${BUILD_IOS_GROUP_IDENTIFIER}\")
|
||||||
target_compile_options(networkextension PRIVATE -DNETWORK_EXTENSION=1)
|
target_compile_options(networkextension PRIVATE -DNETWORK_EXTENSION=1)
|
||||||
|
|
||||||
set(WG_APPLE_SOURCE_DIR ${CLIENT_ROOT_DIR}/3rd/amneziawg-apple/Sources)
|
set(WG_APPLE_SOURCE_DIR ${AMNEZIA_THIRDPARTY_ROOT}/amneziawg-apple/Sources)
|
||||||
|
|
||||||
target_sources(networkextension PRIVATE
|
set(NE_COMMON_SOURCES
|
||||||
|
${CLIENT_ROOT_DIR}/platforms/ios/NELogController.swift
|
||||||
|
${CLIENT_ROOT_DIR}/platforms/ios/Log.swift
|
||||||
|
${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift
|
||||||
|
${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider.swift
|
||||||
|
)
|
||||||
|
|
||||||
|
set(NE_WIREGUARD_SOURCES
|
||||||
${WG_APPLE_SOURCE_DIR}/WireGuardKit/WireGuardAdapter.swift
|
${WG_APPLE_SOURCE_DIR}/WireGuardKit/WireGuardAdapter.swift
|
||||||
${WG_APPLE_SOURCE_DIR}/WireGuardKit/PacketTunnelSettingsGenerator.swift
|
${WG_APPLE_SOURCE_DIR}/WireGuardKit/PacketTunnelSettingsGenerator.swift
|
||||||
${WG_APPLE_SOURCE_DIR}/WireGuardKit/DNSResolver.swift
|
${WG_APPLE_SOURCE_DIR}/WireGuardKit/DNSResolver.swift
|
||||||
${WG_APPLE_SOURCE_DIR}/WireGuardNetworkExtension/ErrorNotifier.swift
|
${WG_APPLE_SOURCE_DIR}/WireGuardNetworkExtension/ErrorNotifier.swift
|
||||||
${WG_APPLE_SOURCE_DIR}/Shared/Keychain.swift
|
${WG_APPLE_SOURCE_DIR}/Shared/Keychain.swift
|
||||||
${WG_APPLE_SOURCE_DIR}/Shared/Model/TunnelConfiguration+WgQuickConfig.swift
|
${WG_APPLE_SOURCE_DIR}/Shared/FileManager+Extension.swift
|
||||||
${WG_APPLE_SOURCE_DIR}/Shared/Model/NETunnelProviderProtocol+Extension.swift
|
${WG_APPLE_SOURCE_DIR}/Shared/Model/NETunnelProviderProtocol+Extension.swift
|
||||||
|
${WG_APPLE_SOURCE_DIR}/Shared/Model/TunnelConfiguration+WgQuickConfig.swift
|
||||||
${WG_APPLE_SOURCE_DIR}/Shared/Model/String+ArrayConversion.swift
|
${WG_APPLE_SOURCE_DIR}/Shared/Model/String+ArrayConversion.swift
|
||||||
${WG_APPLE_SOURCE_DIR}/WireGuardKit/TunnelConfiguration.swift
|
${WG_APPLE_SOURCE_DIR}/WireGuardKit/TunnelConfiguration.swift
|
||||||
${WG_APPLE_SOURCE_DIR}/WireGuardKit/IPAddressRange.swift
|
${WG_APPLE_SOURCE_DIR}/WireGuardKit/IPAddressRange.swift
|
||||||
@@ -84,24 +120,50 @@ target_sources(networkextension PRIVATE
|
|||||||
${WG_APPLE_SOURCE_DIR}/WireGuardKit/DNSServer.swift
|
${WG_APPLE_SOURCE_DIR}/WireGuardKit/DNSServer.swift
|
||||||
${WG_APPLE_SOURCE_DIR}/WireGuardKit/InterfaceConfiguration.swift
|
${WG_APPLE_SOURCE_DIR}/WireGuardKit/InterfaceConfiguration.swift
|
||||||
${WG_APPLE_SOURCE_DIR}/WireGuardKit/PeerConfiguration.swift
|
${WG_APPLE_SOURCE_DIR}/WireGuardKit/PeerConfiguration.swift
|
||||||
${WG_APPLE_SOURCE_DIR}/Shared/FileManager+Extension.swift
|
|
||||||
${WG_APPLE_SOURCE_DIR}/WireGuardKitC/x25519.c
|
${WG_APPLE_SOURCE_DIR}/WireGuardKitC/x25519.c
|
||||||
${WG_APPLE_SOURCE_DIR}/WireGuardKit/Array+ConcurrentMap.swift
|
${WG_APPLE_SOURCE_DIR}/WireGuardKit/Array+ConcurrentMap.swift
|
||||||
${WG_APPLE_SOURCE_DIR}/WireGuardKit/IPAddress+AddrInfo.swift
|
${WG_APPLE_SOURCE_DIR}/WireGuardKit/IPAddress+AddrInfo.swift
|
||||||
${WG_APPLE_SOURCE_DIR}/WireGuardKit/PrivateKey.swift
|
${WG_APPLE_SOURCE_DIR}/WireGuardKit/PrivateKey.swift
|
||||||
${CLIENT_ROOT_DIR}/platforms/ios/HevSocksTunnel.swift
|
|
||||||
${CLIENT_ROOT_DIR}/platforms/ios/NELogController.swift
|
|
||||||
${CLIENT_ROOT_DIR}/platforms/ios/Log.swift
|
|
||||||
${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift
|
|
||||||
${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider.swift
|
|
||||||
${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider+WireGuard.swift
|
${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider+WireGuard.swift
|
||||||
${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider+OpenVPN.swift
|
|
||||||
${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider+Xray.swift
|
|
||||||
${CLIENT_ROOT_DIR}/platforms/ios/WGConfig.swift
|
${CLIENT_ROOT_DIR}/platforms/ios/WGConfig.swift
|
||||||
|
)
|
||||||
|
|
||||||
|
set(NE_XRAY_SOURCES
|
||||||
|
${CLIENT_ROOT_DIR}/platforms/ios/HevSocksTunnel.swift
|
||||||
|
${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider+Xray.swift
|
||||||
${CLIENT_ROOT_DIR}/platforms/ios/XrayConfig.swift
|
${CLIENT_ROOT_DIR}/platforms/ios/XrayConfig.swift
|
||||||
|
)
|
||||||
|
|
||||||
|
set(NE_OPENVPN_SOURCES
|
||||||
|
${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider+OpenVPN.swift
|
||||||
|
)
|
||||||
|
|
||||||
|
set(NE_APPLE_GLUE_SOURCES
|
||||||
${CLIENT_ROOT_DIR}/platforms/ios/iosglue.mm
|
${CLIENT_ROOT_DIR}/platforms/ios/iosglue.mm
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if(AMNEZIA_IOS_APPLETV)
|
||||||
|
list(APPEND NE_APPLE_GLUE_SOURCES
|
||||||
|
${CLIENT_ROOT_DIR}/platforms/ios/tvos_cgo_stubs.c
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
target_sources(networkextension PRIVATE ${NE_COMMON_SOURCES})
|
||||||
|
|
||||||
|
if(NOT AMNEZIA_IOS_APPLETV)
|
||||||
|
target_sources(networkextension PRIVATE
|
||||||
|
${NE_WIREGUARD_SOURCES}
|
||||||
|
${NE_OPENVPN_SOURCES}
|
||||||
|
${NE_XRAY_SOURCES}
|
||||||
|
${NE_APPLE_GLUE_SOURCES}
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
target_sources(networkextension PRIVATE
|
||||||
|
${NE_WIREGUARD_SOURCES}
|
||||||
|
${NE_APPLE_GLUE_SOURCES}
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
target_sources(networkextension PRIVATE
|
target_sources(networkextension PRIVATE
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/PrivacyInfo.xcprivacy
|
${CMAKE_CURRENT_SOURCE_DIR}/PrivacyInfo.xcprivacy
|
||||||
)
|
)
|
||||||
@@ -110,21 +172,16 @@ set_property(TARGET networkextension APPEND PROPERTY RESOURCE
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/PrivacyInfo.xcprivacy
|
${CMAKE_CURRENT_SOURCE_DIR}/PrivacyInfo.xcprivacy
|
||||||
)
|
)
|
||||||
|
|
||||||
## Build wireguard-go-version.h
|
|
||||||
execute_process(
|
|
||||||
COMMAND go list -m golang.zx2c4.com/wireguard
|
|
||||||
WORKING_DIRECTORY ${CLIENT_ROOT_DIR}/3rd/wireguard-apple/Sources/WireGuardKitGo
|
|
||||||
OUTPUT_VARIABLE WG_VERSION_FULL
|
|
||||||
)
|
|
||||||
string(REGEX REPLACE ".*v\([0-9.]*\).*" "\\1" WG_VERSION_STRING 1.1.1)
|
|
||||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/wireguard-go-version.h.in
|
|
||||||
${CMAKE_CURRENT_BINARY_DIR}/wireguard-go-version.h)
|
|
||||||
target_sources(networkextension PRIVATE
|
|
||||||
${CMAKE_CURRENT_BINARY_DIR}/wireguard-go-version.h)
|
|
||||||
|
|
||||||
target_include_directories(networkextension PRIVATE ${CLIENT_ROOT_DIR})
|
target_include_directories(networkextension PRIVATE ${CLIENT_ROOT_DIR})
|
||||||
target_include_directories(networkextension PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
|
target_include_directories(networkextension PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
|
||||||
|
|
||||||
target_link_libraries(networkextension PRIVATE ${CLIENT_ROOT_DIR}/3rd-prebuilt/3rd-prebuilt/wireguard/ios/arm64/libwg-go.a)
|
find_package(awg-apple REQUIRED)
|
||||||
|
target_link_libraries(networkextension PRIVATE amnezia::awg-apple)
|
||||||
|
|
||||||
target_link_libraries(networkextension PRIVATE ${CLIENT_ROOT_DIR}/3rd-prebuilt/3rd-prebuilt/xray/HevSocks5Tunnel.xcframework)
|
if(NOT AMNEZIA_IOS_APPLETV)
|
||||||
|
find_package(openvpnadapter REQUIRED)
|
||||||
|
target_link_libraries(networkextension PRIVATE amnezia::openvpnadapter)
|
||||||
|
|
||||||
|
find_package(hev-socks5-tunnel REQUIRED)
|
||||||
|
target_link_libraries(networkextension PRIVATE heiher::hev-socks5-tunnel)
|
||||||
|
endif()
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
#ifndef WIREGUARD_GO_VERSION
|
|
||||||
#define WIREGUARD_GO_VERSION "@WG_VERSION_STRING@"
|
|
||||||
#endif // WIREGUARD_GO_VERSION
|
|
||||||
@@ -16,7 +16,7 @@ set_target_properties(AmneziaVPNNetworkExtension PROPERTIES
|
|||||||
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_NAME "${BUILD_IOS_APP_IDENTIFIER}.network-extension"
|
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_NAME "${BUILD_IOS_APP_IDENTIFIER}.network-extension"
|
||||||
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS ${CMAKE_CURRENT_SOURCE_DIR}/AmneziaVPNNetworkExtension.entitlements
|
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS ${CMAKE_CURRENT_SOURCE_DIR}/AmneziaVPNNetworkExtension.entitlements
|
||||||
XCODE_ATTRIBUTE_MARKETING_VERSION "${APP_MAJOR_VERSION}"
|
XCODE_ATTRIBUTE_MARKETING_VERSION "${APP_MAJOR_VERSION}"
|
||||||
XCODE_ATTRIBUTE_CURRENT_PROJECT_VERSION "${CMAKE_PROJECT_VERSION_TWEAK}"
|
XCODE_ATTRIBUTE_CURRENT_PROJECT_VERSION "${BUILD_ID}"
|
||||||
XCODE_ATTRIBUTE_PRODUCT_NAME "AmneziaVPNNetworkExtension"
|
XCODE_ATTRIBUTE_PRODUCT_NAME "AmneziaVPNNetworkExtension"
|
||||||
|
|
||||||
XCODE_ATTRIBUTE_APPLICATION_EXTENSION_API_ONLY "YES"
|
XCODE_ATTRIBUTE_APPLICATION_EXTENSION_API_ONLY "YES"
|
||||||
@@ -114,25 +114,14 @@ set_property(TARGET AmneziaVPNNetworkExtension APPEND PROPERTY RESOURCE
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/PrivacyInfo.xcprivacy
|
${CMAKE_CURRENT_SOURCE_DIR}/PrivacyInfo.xcprivacy
|
||||||
)
|
)
|
||||||
|
|
||||||
## Build wireguard-go-version.h
|
|
||||||
execute_process(
|
|
||||||
COMMAND go list -m golang.zx2c4.com/wireguard
|
|
||||||
WORKING_DIRECTORY ${CLIENT_ROOT_DIR}/3rd/wireguard-apple/Sources/WireGuardKitGo
|
|
||||||
OUTPUT_VARIABLE WG_VERSION_FULL
|
|
||||||
)
|
|
||||||
string(REGEX REPLACE ".*v\([0-9.]*\).*" "\\1" WG_VERSION_STRING 1.1.1)
|
|
||||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/wireguard-go-version.h.in
|
|
||||||
${CMAKE_CURRENT_BINARY_DIR}/wireguard-go-version.h)
|
|
||||||
target_sources(AmneziaVPNNetworkExtension PRIVATE
|
|
||||||
${CMAKE_CURRENT_BINARY_DIR}/wireguard-go-version.h)
|
|
||||||
|
|
||||||
target_include_directories(AmneziaVPNNetworkExtension PRIVATE ${CLIENT_ROOT_DIR})
|
target_include_directories(AmneziaVPNNetworkExtension PRIVATE ${CLIENT_ROOT_DIR})
|
||||||
target_include_directories(AmneziaVPNNetworkExtension PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
|
target_include_directories(AmneziaVPNNetworkExtension PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
|
||||||
|
|
||||||
target_link_libraries(AmneziaVPNNetworkExtension PRIVATE ${CLIENT_ROOT_DIR}/3rd-prebuilt/3rd-prebuilt/wireguard/macos/universal2/libwg-go.a)
|
find_package(openvpnadapter REQUIRED)
|
||||||
|
target_link_libraries(AmneziaVPNNetworkExtension PRIVATE amnezia::openvpnadapter)
|
||||||
|
|
||||||
message(${CLIENT_ROOT_DIR})
|
find_package(awg-apple REQUIRED)
|
||||||
message(${CLIENT_ROOT_DIR}/3rd-prebuilt/3rd-prebuilt/xray/HevSocks5Tunnel.xcframework/macos-arm64_x86_64/libhev-socks5-tunnel.a)
|
target_link_libraries(AmneziaVPNNetworkExtension PRIVATE amnezia::awg-apple)
|
||||||
target_link_libraries(AmneziaVPNNetworkExtension PRIVATE ${CLIENT_ROOT_DIR}/3rd-prebuilt/3rd-prebuilt/xray/HevSocks5Tunnel.xcframework/macos-arm64_x86_64/libhev-socks5-tunnel.a)
|
|
||||||
|
|
||||||
target_include_directories(AmneziaVPNNetworkExtension PRIVATE ${CLIENT_ROOT_DIR}/3rd-prebuilt/3rd-prebuilt/xray/HevSocks5Tunnel.xcframework/macos-arm64_x86_64/Headers)
|
find_package(hev-socks5-tunnel REQUIRED)
|
||||||
|
target_link_libraries(AmneziaVPNNetworkExtension PRIVATE heiher::hev-socks5-tunnel)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<string>AmneziaVPNNetworkExtension</string>
|
<string>AmneziaVPNNetworkExtension</string>
|
||||||
|
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
<string>org.amnezia.AmneziaVPN.network-extension</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
@@ -16,9 +16,9 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>$(MARKETING_VERSION)</string>
|
<string>${APPLE_PROJECT_VERSION}</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
<string>${CMAKE_PROJECT_VERSION_TWEAK}</string>
|
||||||
|
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false/>
|
<false/>
|
||||||
@@ -41,6 +41,6 @@
|
|||||||
<string>group.org.amnezia.AmneziaVPN</string>
|
<string>group.org.amnezia.AmneziaVPN</string>
|
||||||
|
|
||||||
<key>com.wireguard.macos.app_group_id</key>
|
<key>com.wireguard.macos.app_group_id</key>
|
||||||
<string>$(DEVELOPMENT_TEAM).group.org.amnezia.AmneziaVPN</string>
|
<string>${BUILD_VPN_DEVELOPMENT_TEAM}.group.org.amnezia.AmneziaVPN</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
#ifndef WIREGUARD_GO_VERSION
|
|
||||||
#define WIREGUARD_GO_VERSION "@WG_VERSION_STRING@"
|
|
||||||
#endif // WIREGUARD_GO_VERSION
|
|
||||||
+3
-4
@@ -12,11 +12,11 @@
|
|||||||
#include "Windows.h"
|
#include "Windows.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(Q_OS_IOS)
|
#if defined(Q_OS_IOS) || defined(Q_OS_TVOS)
|
||||||
#include "platforms/ios/QtAppDelegate-C-Interface.h"
|
#include "platforms/ios/QtAppDelegate-C-Interface.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(Q_OS_TVOS) && !defined(MACOS_NE)
|
||||||
bool isAnotherInstanceRunning()
|
bool isAnotherInstanceRunning()
|
||||||
{
|
{
|
||||||
QLocalSocket socket;
|
QLocalSocket socket;
|
||||||
@@ -47,7 +47,7 @@ int main(int argc, char *argv[])
|
|||||||
AmneziaApplication app(argc, argv);
|
AmneziaApplication app(argc, argv);
|
||||||
OsSignalHandler::setup();
|
OsSignalHandler::setup();
|
||||||
|
|
||||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(Q_OS_TVOS) && !defined(MACOS_NE)
|
||||||
if (isAnotherInstanceRunning()) {
|
if (isAnotherInstanceRunning()) {
|
||||||
QTimer::singleShot(1000, &app, [&]() { app.quit(); });
|
QTimer::singleShot(1000, &app, [&]() { app.quit(); });
|
||||||
return app.exec();
|
return app.exec();
|
||||||
@@ -75,7 +75,6 @@ int main(int argc, char *argv[])
|
|||||||
|
|
||||||
qInfo().noquote() << QString("Started %1 version %2 %3").arg(APPLICATION_NAME, APP_VERSION, GIT_COMMIT_HASH);
|
qInfo().noquote() << QString("Started %1 version %2 %3").arg(APPLICATION_NAME, APP_VERSION, GIT_COMMIT_HASH);
|
||||||
qInfo().noquote() << QString("%1 (%2)").arg(QSysInfo::prettyProductName(), QSysInfo::currentCpuArchitecture());
|
qInfo().noquote() << QString("%1 (%2)").arg(QSysInfo::prettyProductName(), QSysInfo::currentCpuArchitecture());
|
||||||
qInfo().noquote() << QString("SSL backend: %1").arg(QSslSocket::sslLibraryVersionString());
|
|
||||||
|
|
||||||
return app.exec();
|
return app.exec();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,9 +101,7 @@ bool AndroidController::initialize()
|
|||||||
{"onAuthResult", "(Z)V", reinterpret_cast<void *>(onAuthResult)},
|
{"onAuthResult", "(Z)V", reinterpret_cast<void *>(onAuthResult)},
|
||||||
{"decodeQrCode", "(Ljava/lang/String;)Z", reinterpret_cast<bool *>(decodeQrCode)},
|
{"decodeQrCode", "(Ljava/lang/String;)Z", reinterpret_cast<bool *>(decodeQrCode)},
|
||||||
{"onImeInsetsChanged", "(I)V", reinterpret_cast<void *>(onImeInsetsChanged)},
|
{"onImeInsetsChanged", "(I)V", reinterpret_cast<void *>(onImeInsetsChanged)},
|
||||||
{"onSystemBarsInsetsChanged", "(II)V", reinterpret_cast<void *>(onSystemBarsInsetsChanged)},
|
{"onSystemBarsInsetsChanged", "(II)V", reinterpret_cast<void *>(onSystemBarsInsetsChanged)}
|
||||||
{"onActivityPaused", "()V", reinterpret_cast<void *>(onActivityPaused)},
|
|
||||||
{"onActivityResumed", "()V", reinterpret_cast<void *>(onActivityResumed)}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
QJniEnvironment env;
|
QJniEnvironment env;
|
||||||
@@ -560,22 +558,3 @@ void AndroidController::onSystemBarsInsetsChanged(JNIEnv *env, jobject thiz, jin
|
|||||||
emit AndroidController::instance()->systemBarsInsetsChanged(navBarHeightDp, statusBarHeightDp);
|
emit AndroidController::instance()->systemBarsInsetsChanged(navBarHeightDp, statusBarHeightDp);
|
||||||
}
|
}
|
||||||
|
|
||||||
// static
|
|
||||||
void AndroidController::onActivityPaused(JNIEnv *env, jobject thiz)
|
|
||||||
{
|
|
||||||
Q_UNUSED(env);
|
|
||||||
Q_UNUSED(thiz);
|
|
||||||
|
|
||||||
emit AndroidController::instance()->activityPaused();
|
|
||||||
}
|
|
||||||
|
|
||||||
// static
|
|
||||||
void AndroidController::onActivityResumed(JNIEnv *env, jobject thiz)
|
|
||||||
{
|
|
||||||
Q_UNUSED(env);
|
|
||||||
Q_UNUSED(thiz);
|
|
||||||
|
|
||||||
emit AndroidController::instance()->activityResumed();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -75,8 +75,6 @@ signals:
|
|||||||
void authenticationResult(bool result);
|
void authenticationResult(bool result);
|
||||||
void imeInsetsChanged(int heightDp);
|
void imeInsetsChanged(int heightDp);
|
||||||
void systemBarsInsetsChanged(int navBarHeightDp, int statusBarHeightDp);
|
void systemBarsInsetsChanged(int navBarHeightDp, int statusBarHeightDp);
|
||||||
void activityPaused();
|
|
||||||
void activityResumed();
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool isWaitingStatus = true;
|
bool isWaitingStatus = true;
|
||||||
@@ -107,8 +105,6 @@ private:
|
|||||||
static bool decodeQrCode(JNIEnv *env, jobject thiz, jstring data);
|
static bool decodeQrCode(JNIEnv *env, jobject thiz, jstring data);
|
||||||
static void onImeInsetsChanged(JNIEnv *env, jobject thiz, jint heightDp);
|
static void onImeInsetsChanged(JNIEnv *env, jobject thiz, jint heightDp);
|
||||||
static void onSystemBarsInsetsChanged(JNIEnv *env, jobject thiz, jint navBarHeightDp, jint statusBarHeightDp);
|
static void onSystemBarsInsetsChanged(JNIEnv *env, jobject thiz, jint navBarHeightDp, jint statusBarHeightDp);
|
||||||
static void onActivityPaused(JNIEnv *env, jobject thiz);
|
|
||||||
static void onActivityResumed(JNIEnv *env, jobject thiz);
|
|
||||||
|
|
||||||
template <typename Ret, typename ...Args>
|
template <typename Ret, typename ...Args>
|
||||||
static auto callActivityMethod(const char *methodName, const char *signature, Args &&...args);
|
static auto callActivityMethod(const char *methodName, const char *signature, Args &&...args);
|
||||||
|
|||||||
@@ -15,12 +15,6 @@ struct OpenVPNConfig: Decodable {
|
|||||||
|
|
||||||
extension PacketTunnelProvider {
|
extension PacketTunnelProvider {
|
||||||
func startOpenVPN(completionHandler: @escaping (Error?) -> Void) {
|
func startOpenVPN(completionHandler: @escaping (Error?) -> Void) {
|
||||||
// Reset session-derived state so reconnects never reuse stale gateway/address data.
|
|
||||||
openVpnGatewayAddress = nil
|
|
||||||
openVpnLocalAddress = nil
|
|
||||||
openVpnLocalMask = nil
|
|
||||||
lastOpenVPNSettings = nil
|
|
||||||
|
|
||||||
guard let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol,
|
guard let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol,
|
||||||
let providerConfiguration = protocolConfiguration.providerConfiguration,
|
let providerConfiguration = protocolConfiguration.providerConfiguration,
|
||||||
let openVPNConfigData = providerConfiguration[Constants.ovpnConfigKey] as? Data else {
|
let openVPNConfigData = providerConfiguration[Constants.ovpnConfigKey] as? Data else {
|
||||||
@@ -31,25 +25,7 @@ extension PacketTunnelProvider {
|
|||||||
do {
|
do {
|
||||||
let openVPNConfig = try JSONDecoder().decode(OpenVPNConfig.self, from: openVPNConfigData)
|
let openVPNConfig = try JSONDecoder().decode(OpenVPNConfig.self, from: openVPNConfigData)
|
||||||
ovpnLog(.info, title: "config: ", message: openVPNConfig.str)
|
ovpnLog(.info, title: "config: ", message: openVPNConfig.str)
|
||||||
let wrapperPreview = String(decoding: openVPNConfigData.prefix(512), as: UTF8.self)
|
|
||||||
let ovpnPreview = String(openVPNConfig.config.prefix(512))
|
|
||||||
ovpnLog(.info, title: "config wrapper", message: "bytes=\(openVPNConfigData.count) preview=\(wrapperPreview)")
|
|
||||||
ovpnLog(.info, title: "config raw", message: "chars=\(openVPNConfig.config.count) preview=\(ovpnPreview)")
|
|
||||||
let ovpnConfiguration = Data(openVPNConfig.config.utf8)
|
let ovpnConfiguration = Data(openVPNConfig.config.utf8)
|
||||||
splitTunnelType = openVPNConfig.splitTunnelType
|
|
||||||
splitTunnelSites = openVPNConfig.splitTunnelSites
|
|
||||||
openVpnDnsServers = Self.extractDnsServers(from: openVPNConfig.config)
|
|
||||||
openVpnRemoteAddress = Self.extractRemoteHost(from: openVPNConfig.config)
|
|
||||||
openVpnRedirectGatewayDef1 = Self.hasRedirectGatewayDef1(in: openVPNConfig.config)
|
|
||||||
if let openVpnRemoteAddress {
|
|
||||||
ovpnLog(.info, title: "Remote", message: "host=\(openVpnRemoteAddress)")
|
|
||||||
}
|
|
||||||
if !openVpnDnsServers.isEmpty {
|
|
||||||
ovpnLog(.info, title: "DNS", message: "servers=\(openVpnDnsServers)")
|
|
||||||
}
|
|
||||||
if openVpnRedirectGatewayDef1 {
|
|
||||||
ovpnLog(.info, title: "IPv4Routes", message: "redirect-gateway def1 detected")
|
|
||||||
}
|
|
||||||
setupAndlaunchOpenVPN(withConfig: ovpnConfiguration, completionHandler: completionHandler)
|
setupAndlaunchOpenVPN(withConfig: ovpnConfiguration, completionHandler: completionHandler)
|
||||||
} catch {
|
} catch {
|
||||||
ovpnLog(.error, message: "Can't parse OpenVPN config: \(error.localizedDescription)")
|
ovpnLog(.error, message: "Can't parse OpenVPN config: \(error.localizedDescription)")
|
||||||
@@ -97,11 +73,6 @@ extension PacketTunnelProvider {
|
|||||||
let digestString = digest.map { String(format: "%02x", $0) }.joined()
|
let digestString = digest.map { String(format: "%02x", $0) }.joined()
|
||||||
ovpnLog(.info, title: "ConfigDigest", message: digestString)
|
ovpnLog(.info, title: "ConfigDigest", message: digestString)
|
||||||
|
|
||||||
let hasCertTag = configString.contains("<cert>") && configString.contains("</cert>")
|
|
||||||
let hasKeyTag = configString.contains("<key>") && configString.contains("</key>")
|
|
||||||
let hasAuthUserPass = configString.contains("auth-user-pass")
|
|
||||||
ovpnLog(.info, title: "ConfigCreds", message: "inlineCert=\(hasCertTag) inlineKey=\(hasKeyTag) authUserPass=\(hasAuthUserPass)")
|
|
||||||
|
|
||||||
let hasTlsAuthOpen = configString.contains("<tls-auth>")
|
let hasTlsAuthOpen = configString.contains("<tls-auth>")
|
||||||
let hasTlsAuthClose = configString.contains("</tls-auth>")
|
let hasTlsAuthClose = configString.contains("</tls-auth>")
|
||||||
ovpnLog(.info, title: "ConfigFlags", message: "tls-auth open=\(hasTlsAuthOpen) close=\(hasTlsAuthClose)")
|
ovpnLog(.info, title: "ConfigFlags", message: "tls-auth open=\(hasTlsAuthOpen) close=\(hasTlsAuthClose)")
|
||||||
@@ -112,98 +83,27 @@ extension PacketTunnelProvider {
|
|||||||
ovpnLog(.debug, title: "ConfigHead", message: head)
|
ovpnLog(.debug, title: "ConfigHead", message: head)
|
||||||
ovpnLog(.debug, title: "ConfigTail", message: tail)
|
ovpnLog(.debug, title: "ConfigTail", message: tail)
|
||||||
|
|
||||||
if hasTlsAuthOpen && hasTlsAuthClose {
|
if let start = configString.range(of: "<tls-auth>"),
|
||||||
ovpnLog(.info, title: "TLSAuthSanitized", message: "preserve original tls-auth block")
|
let end = configString.range(of: "</tls-auth>", range: start.upperBound..<configString.endIndex) {
|
||||||
|
let keyBody = String(configString[start.upperBound..<end.lowerBound])
|
||||||
|
ovpnLog(.debug, title: "TLSAuthInline", message: keyBody)
|
||||||
|
let sanitizedLines = keyBody
|
||||||
|
.split(whereSeparator: { $0.isNewline })
|
||||||
|
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||||
|
.filter { !$0.isEmpty }
|
||||||
|
.filter { !$0.hasPrefix("#") }
|
||||||
|
|
||||||
|
let sanitizedKey = sanitizedLines.joined(separator: "\n")
|
||||||
|
ovpnLog(.debug, title: "TLSAuthSanitized", message: sanitizedKey)
|
||||||
|
let sanitizedBlock = "<tls-auth>\n\(sanitizedKey)\n</tls-auth>"
|
||||||
|
configString.replaceSubrange(start.lowerBound..<end.upperBound, with: sanitizedBlock)
|
||||||
}
|
}
|
||||||
|
|
||||||
var normalizedConfig = configString.replacingOccurrences(of: "\r\n", with: "\n")
|
let normalizedConfig = configString.replacingOccurrences(of: "\r\n", with: "\n")
|
||||||
normalizedConfig = Self.normalizeInlineBlock(
|
|
||||||
in: normalizedConfig,
|
|
||||||
tag: "ca",
|
|
||||||
beginMarkers: ["-----BEGIN CERTIFICATE-----"],
|
|
||||||
endMarkers: ["-----END CERTIFICATE-----"]
|
|
||||||
)
|
|
||||||
normalizedConfig = Self.normalizeInlineBlock(
|
|
||||||
in: normalizedConfig,
|
|
||||||
tag: "cert",
|
|
||||||
beginMarkers: ["-----BEGIN CERTIFICATE-----"],
|
|
||||||
endMarkers: ["-----END CERTIFICATE-----"]
|
|
||||||
)
|
|
||||||
normalizedConfig = Self.normalizeInlineBlock(
|
|
||||||
in: normalizedConfig,
|
|
||||||
tag: "key",
|
|
||||||
beginMarkers: [
|
|
||||||
"-----BEGIN PRIVATE KEY-----",
|
|
||||||
"-----BEGIN RSA PRIVATE KEY-----",
|
|
||||||
"-----BEGIN EC PRIVATE KEY-----",
|
|
||||||
"-----BEGIN ENCRYPTED PRIVATE KEY-----"
|
|
||||||
],
|
|
||||||
endMarkers: [
|
|
||||||
"-----END PRIVATE KEY-----",
|
|
||||||
"-----END RSA PRIVATE KEY-----",
|
|
||||||
"-----END EC PRIVATE KEY-----",
|
|
||||||
"-----END ENCRYPTED PRIVATE KEY-----"
|
|
||||||
]
|
|
||||||
)
|
|
||||||
normalizedConfig = Self.normalizeInlineBlock(
|
|
||||||
in: normalizedConfig,
|
|
||||||
tag: "tls-auth",
|
|
||||||
beginMarkers: ["-----BEGIN OpenVPN Static key V1-----"],
|
|
||||||
endMarkers: ["-----END OpenVPN Static key V1-----"]
|
|
||||||
)
|
|
||||||
normalizedConfig = Self.stripUnsupportedOptions(forOpenVPNAdapter: normalizedConfig)
|
|
||||||
if !normalizedConfig.hasSuffix("\n") {
|
|
||||||
normalizedConfig.append("\n")
|
|
||||||
}
|
|
||||||
let normalizedLines = normalizedConfig.split(whereSeparator: \.isNewline)
|
|
||||||
let normalizedTail = normalizedLines.suffix(10).joined(separator: "\n")
|
|
||||||
ovpnLog(.debug, title: "ConfigTailSanitized", message: normalizedTail)
|
|
||||||
let redirectLines = normalizedLines
|
|
||||||
.map(String.init)
|
|
||||||
.filter { $0.lowercased().contains("redirect-gateway") }
|
|
||||||
if !redirectLines.isEmpty {
|
|
||||||
ovpnLog(.info, title: "ConfigRedirect", message: redirectLines.joined(separator: " | "))
|
|
||||||
}
|
|
||||||
let controlScalars = normalizedConfig.unicodeScalars.filter {
|
|
||||||
($0.value < 0x20 && $0 != "\n" && $0 != "\r" && $0 != "\t")
|
|
||||||
}
|
|
||||||
if !controlScalars.isEmpty {
|
|
||||||
ovpnLog(.error, title: "ConfigChars", message: "nonPrintableControlCount=\(controlScalars.count)")
|
|
||||||
}
|
|
||||||
#if os(macOS)
|
|
||||||
let dumpBaseURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first
|
|
||||||
?? FileManager.default.temporaryDirectory
|
|
||||||
let dumpURL = dumpBaseURL.appendingPathComponent("amnezia_ovpn_adapter_config.conf")
|
|
||||||
do {
|
|
||||||
try normalizedConfig.write(to: dumpURL, atomically: true, encoding: .utf8)
|
|
||||||
ovpnLog(.info, title: "ConfigDump", message: "path=\(dumpURL.path) bytes=\(normalizedConfig.utf8.count)")
|
|
||||||
} catch {
|
|
||||||
ovpnLog(.error, title: "ConfigDump", message: "write failed: \(error.localizedDescription)")
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
let sanitizedData = Data(normalizedConfig.utf8)
|
let sanitizedData = Data(normalizedConfig.utf8)
|
||||||
|
|
||||||
let configuration = OpenVPNConfiguration()
|
let configuration = OpenVPNConfiguration()
|
||||||
configuration.fileContent = sanitizedData
|
configuration.fileContent = sanitizedData
|
||||||
// Be explicit: enum default is 0 (enabled), we need stubs-only behavior.
|
|
||||||
configuration.compressionMode = .disabled
|
|
||||||
// A-012: emulate OpenVPN2 CLI capability advertisement as closely as possible.
|
|
||||||
configuration.peerInfo = [
|
|
||||||
"IV_VER": "2.6.10",
|
|
||||||
"IV_PLAT": "mac",
|
|
||||||
"IV_TCPNL": "1",
|
|
||||||
"IV_MTU": "1600",
|
|
||||||
"IV_NCP": "2",
|
|
||||||
"IV_CIPHERS": "AES-256-GCM:AES-128-GCM:CHACHA20-POLY1305",
|
|
||||||
"IV_PROTO": "990",
|
|
||||||
"IV_LZO_STUB": "1",
|
|
||||||
"IV_COMP_STUB": "1",
|
|
||||||
"IV_COMP_STUBv2": "1"
|
|
||||||
]
|
|
||||||
if let peerInfo = configuration.peerInfo {
|
|
||||||
let peerInfoSummary = peerInfo.keys.sorted().map { "\($0)=\(peerInfo[$0] ?? "")" }.joined(separator: " ")
|
|
||||||
ovpnLog(.info, title: "PeerInfoOverride", message: peerInfoSummary)
|
|
||||||
}
|
|
||||||
if configString.contains("cloak") {
|
if configString.contains("cloak") {
|
||||||
configuration.setPTCloak()
|
configuration.setPTCloak()
|
||||||
}
|
}
|
||||||
@@ -224,15 +124,11 @@ extension PacketTunnelProvider {
|
|||||||
if evaluation?.autologin == false {
|
if evaluation?.autologin == false {
|
||||||
ovpnLog(.info, message: "Implement login with user credentials")
|
ovpnLog(.info, message: "Implement login with user credentials")
|
||||||
}
|
}
|
||||||
if let evaluation {
|
|
||||||
ovpnLog(.info, title: "ConfigEval", message: "autologin=\(evaluation.autologin) externalPki=\(evaluation.externalPki)")
|
|
||||||
}
|
|
||||||
|
|
||||||
#if !os(macOS)
|
|
||||||
vpnReachability.startTracking { [weak self] status in
|
vpnReachability.startTracking { [weak self] status in
|
||||||
self?.handleOpenVPNReachabilityChange(status)
|
guard status == .reachableViaWiFi else { return }
|
||||||
|
self?.ovpnAdapter?.reconnect(afterTimeInterval: 5)
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
startHandler = completionHandler
|
startHandler = completionHandler
|
||||||
ovpnAdapter?.connect(using: openVPNPacketFlow())
|
ovpnAdapter?.connect(using: openVPNPacketFlow())
|
||||||
@@ -248,8 +144,6 @@ extension PacketTunnelProvider {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ovpnLog(.info, title: "Transport", message: "bytesIn=\(bytesin) bytesOut=\(bytesout)")
|
|
||||||
|
|
||||||
let response: [String: Any] = [
|
let response: [String: Any] = [
|
||||||
"rx_bytes": bytesin,
|
"rx_bytes": bytesin,
|
||||||
"tx_bytes": bytesout
|
"tx_bytes": bytesout
|
||||||
@@ -262,10 +156,6 @@ extension PacketTunnelProvider {
|
|||||||
ovpnLog(.info, message: "Stopping tunnel: reason: \(reason.amneziaDescription)")
|
ovpnLog(.info, message: "Stopping tunnel: reason: \(reason.amneziaDescription)")
|
||||||
|
|
||||||
stopHandler = completionHandler
|
stopHandler = completionHandler
|
||||||
openVpnGatewayAddress = nil
|
|
||||||
openVpnLocalAddress = nil
|
|
||||||
openVpnLocalMask = nil
|
|
||||||
lastOpenVPNSettings = nil
|
|
||||||
if vpnReachability.isTracking {
|
if vpnReachability.isTracking {
|
||||||
vpnReachability.stopTracking()
|
vpnReachability.stopTracking()
|
||||||
}
|
}
|
||||||
@@ -285,99 +175,11 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
|
|||||||
configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings?,
|
configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings?,
|
||||||
completionHandler: @escaping (Error?) -> Void
|
completionHandler: @escaping (Error?) -> Void
|
||||||
) {
|
) {
|
||||||
guard var effectiveSettings = networkSettings else {
|
|
||||||
ovpnLog(.info, title: "SetTunnelNetworkSettings", message: "nil settings; skipping update")
|
|
||||||
completionHandler(nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let splitType = splitTunnelType ?? 0
|
|
||||||
|
|
||||||
if let ipv4Settings = effectiveSettings.ipv4Settings {
|
|
||||||
openVpnLocalAddress = ipv4Settings.addresses.first
|
|
||||||
openVpnLocalMask = ipv4Settings.subnetMasks.first
|
|
||||||
}
|
|
||||||
|
|
||||||
let serverIP = openVPNAdapter.connectionInformation?.serverIP
|
|
||||||
let configRemote = openVpnRemoteAddress
|
|
||||||
let serverEndpoint: String? = {
|
|
||||||
if let ip = serverIP, Self.isIPv4Address(ip) { return ip }
|
|
||||||
if let ip = configRemote, Self.isIPv4Address(ip) { return ip }
|
|
||||||
return effectiveSettings.tunnelRemoteAddress
|
|
||||||
}()
|
|
||||||
|
|
||||||
if let serverEndpoint,
|
|
||||||
Self.isIPv4Address(serverEndpoint),
|
|
||||||
effectiveSettings.tunnelRemoteAddress != serverEndpoint {
|
|
||||||
let updatedSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: serverEndpoint)
|
|
||||||
updatedSettings.ipv4Settings = effectiveSettings.ipv4Settings
|
|
||||||
updatedSettings.ipv6Settings = effectiveSettings.ipv6Settings
|
|
||||||
updatedSettings.dnsSettings = effectiveSettings.dnsSettings
|
|
||||||
updatedSettings.proxySettings = effectiveSettings.proxySettings
|
|
||||||
updatedSettings.mtu = effectiveSettings.mtu
|
|
||||||
effectiveSettings = updatedSettings
|
|
||||||
ovpnLog(.info, title: "Remote", message: "tunnelRemoteAddress set to server=\(serverEndpoint)")
|
|
||||||
} else if let serverEndpoint, !Self.isIPv4Address(serverEndpoint) {
|
|
||||||
ovpnLog(.info, title: "Remote", message: "skip tunnelRemoteAddress override; non-ip serverEndpoint=\(serverEndpoint)")
|
|
||||||
}
|
|
||||||
|
|
||||||
// In order to direct all DNS queries first to the VPN DNS servers before the primary DNS servers
|
// In order to direct all DNS queries first to the VPN DNS servers before the primary DNS servers
|
||||||
// send empty string to NEDNSSettings.matchDomains
|
// send empty string to NEDNSSettings.matchDomains
|
||||||
if let dnsSettings = effectiveSettings.dnsSettings {
|
networkSettings?.dnsSettings?.matchDomains = [""]
|
||||||
if dnsSettings.servers.isEmpty, !openVpnDnsServers.isEmpty {
|
|
||||||
let newSettings = NEDNSSettings(servers: openVpnDnsServers)
|
|
||||||
newSettings.matchDomains = dnsSettings.matchDomains
|
|
||||||
effectiveSettings.dnsSettings = newSettings
|
|
||||||
}
|
|
||||||
} else if !openVpnDnsServers.isEmpty {
|
|
||||||
let newSettings = NEDNSSettings(servers: openVpnDnsServers)
|
|
||||||
effectiveSettings.dnsSettings = newSettings
|
|
||||||
}
|
|
||||||
|
|
||||||
effectiveSettings.dnsSettings?.matchDomains = [""]
|
if splitTunnelType == 1 {
|
||||||
if let dnsSettings = effectiveSettings.dnsSettings {
|
|
||||||
let servers = dnsSettings.servers.joined(separator: ",")
|
|
||||||
let domains = dnsSettings.matchDomains?.joined(separator: ",") ?? ""
|
|
||||||
ovpnLog(.info, title: "DNS", message: "servers=[\(servers)] matchDomains=[\(domains)]")
|
|
||||||
} else {
|
|
||||||
ovpnLog(.error, title: "DNS", message: "dnsSettings is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
let tunnelRemote = effectiveSettings.tunnelRemoteAddress
|
|
||||||
if !tunnelRemote.isEmpty {
|
|
||||||
ovpnLog(.info, title: "Remote", message: "tunnelRemoteAddress=\(tunnelRemote)")
|
|
||||||
} else if let remoteAddress = openVpnRemoteAddress {
|
|
||||||
ovpnLog(.info, title: "Remote", message: "tunnelRemoteAddress is empty, configRemote=\(remoteAddress)")
|
|
||||||
}
|
|
||||||
|
|
||||||
if let ipv4Settings = effectiveSettings.ipv4Settings {
|
|
||||||
let included = (ipv4Settings.includedRoutes ?? []).map { "\($0.destinationAddress)/\($0.destinationSubnetMask)" }
|
|
||||||
let excluded = (ipv4Settings.excludedRoutes ?? []).map { "\($0.destinationAddress)/\($0.destinationSubnetMask)" }
|
|
||||||
let addresses = ipv4Settings.addresses.joined(separator: ",")
|
|
||||||
let masks = ipv4Settings.subnetMasks.joined(separator: ",")
|
|
||||||
let router: String
|
|
||||||
#if os(macOS)
|
|
||||||
if #available(macOS 13.0, *) {
|
|
||||||
router = ipv4Settings.router ?? ""
|
|
||||||
} else {
|
|
||||||
router = ""
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
router = ""
|
|
||||||
#endif
|
|
||||||
ovpnLog(.info, title: "IPv4RoutesPre", message: "addresses=[\(addresses)] masks=[\(masks)] router=\(router) included=\(included) excluded=\(excluded)")
|
|
||||||
} else {
|
|
||||||
ovpnLog(.error, title: "IPv4RoutesPre", message: "ipv4Settings is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if let ipv6Settings = effectiveSettings.ipv6Settings {
|
|
||||||
let included = (ipv6Settings.includedRoutes ?? []).map { "\($0.destinationAddress)/\($0.destinationNetworkPrefixLength)" }
|
|
||||||
let excluded = (ipv6Settings.excludedRoutes ?? []).map { "\($0.destinationAddress)/\($0.destinationNetworkPrefixLength)" }
|
|
||||||
let addresses = ipv6Settings.addresses.joined(separator: ",")
|
|
||||||
let prefixes = ipv6Settings.networkPrefixLengths.map { "\($0)" }.joined(separator: ",")
|
|
||||||
ovpnLog(.info, title: "IPv6RoutesPre", message: "addresses=[\(addresses)] prefixes=[\(prefixes)] included=\(included) excluded=\(excluded)")
|
|
||||||
}
|
|
||||||
|
|
||||||
if splitType == 1 {
|
|
||||||
var ipv4IncludedRoutes = [NEIPv4Route]()
|
var ipv4IncludedRoutes = [NEIPv4Route]()
|
||||||
|
|
||||||
guard let splitTunnelSites else {
|
guard let splitTunnelSites else {
|
||||||
@@ -393,8 +195,9 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
effectiveSettings.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
|
networkSettings?.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
|
||||||
} else if splitType == 2 {
|
} else {
|
||||||
|
if splitTunnelType == 2 {
|
||||||
var ipv4ExcludedRoutes = [NEIPv4Route]()
|
var ipv4ExcludedRoutes = [NEIPv4Route]()
|
||||||
var ipv4IncludedRoutes = [NEIPv4Route]()
|
var ipv4IncludedRoutes = [NEIPv4Route]()
|
||||||
var ipv6IncludedRoutes = [NEIPv6Route]()
|
var ipv6IncludedRoutes = [NEIPv6Route]()
|
||||||
@@ -422,418 +225,14 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
|
|||||||
destinationAddress: "\(allIPv6.address)",
|
destinationAddress: "\(allIPv6.address)",
|
||||||
networkPrefixLength: NSNumber(value: allIPv6.networkPrefixLength)))
|
networkPrefixLength: NSNumber(value: allIPv6.networkPrefixLength)))
|
||||||
}
|
}
|
||||||
effectiveSettings.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
|
networkSettings?.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
|
||||||
effectiveSettings.ipv6Settings?.includedRoutes = ipv6IncludedRoutes
|
networkSettings?.ipv6Settings?.includedRoutes = ipv6IncludedRoutes
|
||||||
effectiveSettings.ipv4Settings?.excludedRoutes = ipv4ExcludedRoutes
|
networkSettings?.ipv4Settings?.excludedRoutes = ipv4ExcludedRoutes
|
||||||
} else {
|
|
||||||
// Full tunnel: rely on adapter-provided routes.
|
|
||||||
}
|
|
||||||
|
|
||||||
if let serverEndpoint,
|
|
||||||
Self.isIPv4Address(serverEndpoint),
|
|
||||||
let ipv4Settings = effectiveSettings.ipv4Settings {
|
|
||||||
let hostMask = "255.255.255.255"
|
|
||||||
var excluded = ipv4Settings.excludedRoutes ?? []
|
|
||||||
let alreadyExcluded = excluded.contains {
|
|
||||||
$0.destinationAddress == serverEndpoint && $0.destinationSubnetMask == hostMask
|
|
||||||
}
|
|
||||||
if !alreadyExcluded {
|
|
||||||
excluded.append(NEIPv4Route(destinationAddress: serverEndpoint, subnetMask: hostMask))
|
|
||||||
ipv4Settings.excludedRoutes = excluded
|
|
||||||
ovpnLog(.info, title: "IPv4Routes", message: "excluded remoteAddress=\(serverEndpoint)")
|
|
||||||
}
|
|
||||||
} else if let serverEndpoint {
|
|
||||||
ovpnLog(.info, title: "IPv4Routes", message: "skip explicit remote exclude; non-ip server=\(serverEndpoint)")
|
|
||||||
}
|
|
||||||
|
|
||||||
let localAddr = openVpnLocalAddress
|
|
||||||
var net30Gateway: String?
|
|
||||||
if let localAddr, let mask = openVpnLocalMask {
|
|
||||||
net30Gateway = Self.net30Peer(for: localAddr, mask: mask)
|
|
||||||
}
|
|
||||||
var gateway = net30Gateway
|
|
||||||
if let adapterGateway = openVPNAdapter.connectionInformation?.gatewayIPv4, !adapterGateway.isEmpty {
|
|
||||||
if let localAddr, adapterGateway == localAddr {
|
|
||||||
ovpnLog(.info, title: "IPv4Gateway", message: "ignore adapter gateway equal to local address=\(adapterGateway)")
|
|
||||||
} else if let net30Gateway, net30Gateway != adapterGateway {
|
|
||||||
ovpnLog(.info, title: "IPv4Gateway", message: "ignore mismatched adapter gateway=\(adapterGateway), using net30 peer=\(net30Gateway)")
|
|
||||||
} else {
|
|
||||||
gateway = adapterGateway
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
openVpnGatewayAddress = gateway
|
|
||||||
if let gateway, !gateway.isEmpty {
|
|
||||||
ovpnLog(.info, title: "IPv4Gateway", message: "gateway=\(gateway)")
|
|
||||||
}
|
|
||||||
#if os(macOS)
|
|
||||||
if splitType == 0, let gateway, !gateway.isEmpty, effectiveSettings.tunnelRemoteAddress != gateway {
|
|
||||||
let updatedSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: gateway)
|
|
||||||
updatedSettings.ipv4Settings = effectiveSettings.ipv4Settings
|
|
||||||
updatedSettings.ipv6Settings = effectiveSettings.ipv6Settings
|
|
||||||
updatedSettings.dnsSettings = effectiveSettings.dnsSettings
|
|
||||||
updatedSettings.proxySettings = effectiveSettings.proxySettings
|
|
||||||
updatedSettings.mtu = effectiveSettings.mtu
|
|
||||||
effectiveSettings = updatedSettings
|
|
||||||
ovpnLog(.info, title: "Remote", message: "tunnelRemoteAddress set to gateway=\(gateway) on macOS full-tunnel")
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
#if os(macOS)
|
|
||||||
if var ipv4Settings = effectiveSettings.ipv4Settings {
|
|
||||||
if splitType == 0 {
|
|
||||||
let hasNet30Mask = ipv4Settings.subnetMasks.contains("255.255.255.252")
|
|
||||||
if hasNet30Mask {
|
|
||||||
let normalizedMasks = Array(repeating: "255.255.255.255",
|
|
||||||
count: ipv4Settings.subnetMasks.count)
|
|
||||||
let normalized = NEIPv4Settings(addresses: ipv4Settings.addresses,
|
|
||||||
subnetMasks: normalizedMasks)
|
|
||||||
normalized.includedRoutes = ipv4Settings.includedRoutes
|
|
||||||
normalized.excludedRoutes = ipv4Settings.excludedRoutes
|
|
||||||
if #available(macOS 13.0, *) {
|
|
||||||
normalized.router = ipv4Settings.router
|
|
||||||
}
|
|
||||||
ipv4Settings = normalized
|
|
||||||
ovpnLog(.info, title: "IPv4Routes", message: "normalized net30 /30 masks to /32 on macOS full-tunnel")
|
|
||||||
}
|
|
||||||
|
|
||||||
if let gateway, !gateway.isEmpty {
|
|
||||||
if #available(macOS 13.0, *) {
|
|
||||||
ipv4Settings.router = gateway
|
|
||||||
ovpnLog(.info, title: "IPv4Routes", message: "set ipv4 router=\(gateway) on macOS full-tunnel")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var included = ipv4Settings.includedRoutes ?? []
|
|
||||||
let hasDefault = included.contains {
|
|
||||||
$0.destinationAddress == "0.0.0.0" && $0.destinationSubnetMask == "0.0.0.0"
|
|
||||||
}
|
|
||||||
if hasDefault {
|
|
||||||
included.removeAll {
|
|
||||||
$0.destinationAddress == "0.0.0.0" && $0.destinationSubnetMask == "0.0.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let hasDef1Low = included.contains {
|
|
||||||
$0.destinationAddress == "0.0.0.0" && $0.destinationSubnetMask == "128.0.0.0"
|
|
||||||
}
|
|
||||||
let hasDef1High = included.contains {
|
|
||||||
$0.destinationAddress == "128.0.0.0" && $0.destinationSubnetMask == "128.0.0.0"
|
|
||||||
}
|
|
||||||
if (hasDefault || openVpnRedirectGatewayDef1) && !(hasDef1Low && hasDef1High) {
|
|
||||||
if !hasDef1Low {
|
|
||||||
let route = NEIPv4Route(destinationAddress: "0.0.0.0", subnetMask: "128.0.0.0")
|
|
||||||
if let gateway, !gateway.isEmpty {
|
|
||||||
route.gatewayAddress = gateway
|
|
||||||
}
|
|
||||||
included.append(route)
|
|
||||||
}
|
|
||||||
if !hasDef1High {
|
|
||||||
let route = NEIPv4Route(destinationAddress: "128.0.0.0", subnetMask: "128.0.0.0")
|
|
||||||
if let gateway, !gateway.isEmpty {
|
|
||||||
route.gatewayAddress = gateway
|
|
||||||
}
|
|
||||||
included.append(route)
|
|
||||||
}
|
|
||||||
ovpnLog(.info, title: "IPv4Routes", message: "ensured def1 routes (/1 + /1) on macOS full-tunnel")
|
|
||||||
}
|
|
||||||
if let gateway, !gateway.isEmpty {
|
|
||||||
included = included.map { route in
|
|
||||||
let isDef1 =
|
|
||||||
(route.destinationAddress == "0.0.0.0" && route.destinationSubnetMask == "128.0.0.0") ||
|
|
||||||
(route.destinationAddress == "128.0.0.0" && route.destinationSubnetMask == "128.0.0.0")
|
|
||||||
guard isDef1 else { return route }
|
|
||||||
if route.gatewayAddress == gateway {
|
|
||||||
return route
|
|
||||||
}
|
|
||||||
let updatedRoute = NEIPv4Route(destinationAddress: route.destinationAddress,
|
|
||||||
subnetMask: route.destinationSubnetMask)
|
|
||||||
updatedRoute.gatewayAddress = gateway
|
|
||||||
return updatedRoute
|
|
||||||
}
|
|
||||||
ovpnLog(.info, title: "IPv4Routes", message: "set gateway=\(gateway) on macOS def1 routes")
|
|
||||||
}
|
|
||||||
ipv4Settings.includedRoutes = included
|
|
||||||
effectiveSettings.ipv4Settings = ipv4Settings
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
if let ipv4Settings = effectiveSettings.ipv4Settings {
|
|
||||||
let included = (ipv4Settings.includedRoutes ?? []).map {
|
|
||||||
let gw = $0.gatewayAddress ?? ""
|
|
||||||
return "\($0.destinationAddress)/\($0.destinationSubnetMask) gw=\(gw)"
|
|
||||||
}
|
|
||||||
let excluded = (ipv4Settings.excludedRoutes ?? []).map { "\($0.destinationAddress)/\($0.destinationSubnetMask)" }
|
|
||||||
let addresses = ipv4Settings.addresses.joined(separator: ",")
|
|
||||||
let masks = ipv4Settings.subnetMasks.joined(separator: ",")
|
|
||||||
let router: String
|
|
||||||
#if os(macOS)
|
|
||||||
if #available(macOS 13.0, *) {
|
|
||||||
router = ipv4Settings.router ?? ""
|
|
||||||
} else {
|
|
||||||
router = ""
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
router = ""
|
|
||||||
#endif
|
|
||||||
ovpnLog(.info, title: "IPv4Routes", message: "addresses=[\(addresses)] masks=[\(masks)] router=\(router) included=\(included) excluded=\(excluded)")
|
|
||||||
} else {
|
|
||||||
ovpnLog(.error, title: "IPv4Routes", message: "ipv4Settings is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if let ipv6Settings = effectiveSettings.ipv6Settings {
|
|
||||||
let included = (ipv6Settings.includedRoutes ?? []).map { "\($0.destinationAddress)/\($0.destinationNetworkPrefixLength)" }
|
|
||||||
let excluded = (ipv6Settings.excludedRoutes ?? []).map { "\($0.destinationAddress)/\($0.destinationNetworkPrefixLength)" }
|
|
||||||
let addresses = ipv6Settings.addresses.joined(separator: ",")
|
|
||||||
let prefixes = ipv6Settings.networkPrefixLengths.map { "\($0)" }.joined(separator: ",")
|
|
||||||
ovpnLog(.info, title: "IPv6Routes", message: "addresses=[\(addresses)] prefixes=[\(prefixes)] included=\(included) excluded=\(excluded)")
|
|
||||||
}
|
|
||||||
#if os(macOS)
|
|
||||||
if effectiveSettings.ipv6Settings != nil {
|
|
||||||
effectiveSettings.ipv6Settings = nil
|
|
||||||
ovpnLog(.info, title: "IPv6", message: "cleared ipv6Settings on macOS")
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
lastOpenVPNSettings = effectiveSettings
|
|
||||||
|
|
||||||
// Set the network settings for the current tunneling session.
|
// Set the network settings for the current tunneling session.
|
||||||
setTunnelNetworkSettings(effectiveSettings) { error in
|
setTunnelNetworkSettings(networkSettings, completionHandler: completionHandler)
|
||||||
if let error {
|
|
||||||
ovpnLog(.error, title: "SetTunnelNetworkSettings", message: error.localizedDescription)
|
|
||||||
} else {
|
|
||||||
ovpnLog(.info, title: "SetTunnelNetworkSettings", message: "ok")
|
|
||||||
}
|
|
||||||
completionHandler(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func extractDnsServers(from config: String) -> [String] {
|
|
||||||
let lines = config.split(whereSeparator: \.isNewline)
|
|
||||||
var servers: [String] = []
|
|
||||||
for line in lines {
|
|
||||||
let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
if trimmed.hasPrefix("dhcp-option DNS ") {
|
|
||||||
let parts = trimmed.split(separator: " ")
|
|
||||||
if let last = parts.last {
|
|
||||||
servers.append(String(last))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return servers
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func extractRemoteHost(from config: String) -> String? {
|
|
||||||
let lines = config.split(whereSeparator: \.isNewline)
|
|
||||||
for line in lines {
|
|
||||||
let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
if trimmed.hasPrefix("remote ") {
|
|
||||||
let parts = trimmed.split(separator: " ")
|
|
||||||
if parts.count >= 2 {
|
|
||||||
return String(parts[1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func hasRedirectGatewayDef1(in config: String) -> Bool {
|
|
||||||
let lines = config.split(whereSeparator: \.isNewline)
|
|
||||||
for line in lines {
|
|
||||||
let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
if trimmed.hasPrefix("redirect-gateway") {
|
|
||||||
return trimmed.split(whereSeparator: { $0 == " " || $0 == "\t" }).contains("def1")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func net30Peer(for address: String, mask: String) -> String? {
|
|
||||||
guard mask == "255.255.255.252" else { return nil }
|
|
||||||
let parts = address.split(separator: ".")
|
|
||||||
guard parts.count == 4 else { return nil }
|
|
||||||
var octets: [Int] = []
|
|
||||||
for part in parts {
|
|
||||||
guard let num = Int(part), num >= 0 && num <= 255 else { return nil }
|
|
||||||
octets.append(num)
|
|
||||||
}
|
|
||||||
let ip = (octets[0] << 24) | (octets[1] << 16) | (octets[2] << 8) | octets[3]
|
|
||||||
let network = ip & ~3
|
|
||||||
let host = ip - network
|
|
||||||
let peerHost: Int
|
|
||||||
switch host {
|
|
||||||
case 1: peerHost = 2
|
|
||||||
case 2: peerHost = 1
|
|
||||||
default: return nil
|
|
||||||
}
|
|
||||||
let peerIP = network + peerHost
|
|
||||||
return "\((peerIP >> 24) & 0xff).\((peerIP >> 16) & 0xff).\((peerIP >> 8) & 0xff).\(peerIP & 0xff)"
|
|
||||||
}
|
|
||||||
|
|
||||||
private func logOpenVPNConnectionInfo() {
|
|
||||||
guard let info = ovpnAdapter?.connectionInformation else { return }
|
|
||||||
let message = "vpnIPv4=\(info.vpnIPv4 ?? "") gatewayIPv4=\(info.gatewayIPv4 ?? "") serverIP=\(info.serverIP ?? "") tun=\(info.tunName ?? "")"
|
|
||||||
ovpnLog(.info, title: "ConnInfo", message: message)
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func normalizeInlineBlock(
|
|
||||||
in config: String,
|
|
||||||
tag: String,
|
|
||||||
beginMarkers: [String],
|
|
||||||
endMarkers: [String]
|
|
||||||
) -> String {
|
|
||||||
guard !beginMarkers.isEmpty, !endMarkers.isEmpty else { return config }
|
|
||||||
|
|
||||||
var normalizedConfig = config
|
|
||||||
let openTag = "<\(tag)>"
|
|
||||||
let closeTag = "</\(tag)>"
|
|
||||||
var searchStart = normalizedConfig.startIndex
|
|
||||||
|
|
||||||
while let openRange = normalizedConfig.range(of: openTag, range: searchStart..<normalizedConfig.endIndex),
|
|
||||||
let closeRange = normalizedConfig.range(of: closeTag, range: openRange.upperBound..<normalizedConfig.endIndex) {
|
|
||||||
let rawBody = String(normalizedConfig[openRange.upperBound..<closeRange.lowerBound])
|
|
||||||
let lines = rawBody
|
|
||||||
.split(whereSeparator: \.isNewline)
|
|
||||||
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
|
||||||
.filter { !$0.isEmpty }
|
|
||||||
|
|
||||||
var beginIndex: Int?
|
|
||||||
var endIndex: Int?
|
|
||||||
for (idx, line) in lines.enumerated() {
|
|
||||||
if beginIndex == nil,
|
|
||||||
beginMarkers.contains(where: { line.contains($0) }) {
|
|
||||||
beginIndex = idx
|
|
||||||
}
|
|
||||||
if beginIndex != nil,
|
|
||||||
endMarkers.contains(where: { line.contains($0) }) {
|
|
||||||
endIndex = idx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let beginIndex,
|
|
||||||
let endIndex,
|
|
||||||
endIndex >= beginIndex {
|
|
||||||
let extracted = lines[beginIndex...endIndex].joined(separator: "\n")
|
|
||||||
let replacement = "<\(tag)>\n\(extracted)\n</\(tag)>"
|
|
||||||
normalizedConfig.replaceSubrange(openRange.lowerBound..<closeRange.upperBound, with: replacement)
|
|
||||||
ovpnLog(.info, title: "ConfigInline", message: "tag=<\(tag)> linesIn=\(lines.count) linesOut=\(endIndex - beginIndex + 1)")
|
|
||||||
searchStart = normalizedConfig.index(openRange.lowerBound, offsetBy: replacement.count)
|
|
||||||
} else {
|
|
||||||
ovpnLog(.error, title: "ConfigInline", message: "tag=<\(tag)> missing markers, keeping original body")
|
|
||||||
searchStart = closeRange.upperBound
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalizedConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static func stripUnsupportedOptions(forOpenVPNAdapter config: String) -> String {
|
|
||||||
let unsupportedTokens: Set<String> = [
|
|
||||||
"block-ipv6",
|
|
||||||
"script-security",
|
|
||||||
"up",
|
|
||||||
"down",
|
|
||||||
"resolv-retry",
|
|
||||||
"persist-key",
|
|
||||||
"persist-tun",
|
|
||||||
"compat-mode",
|
|
||||||
"disable-dco"
|
|
||||||
]
|
|
||||||
let inlineBlockTags: Set<String> = [
|
|
||||||
"ca",
|
|
||||||
"cert",
|
|
||||||
"key",
|
|
||||||
"pkcs12",
|
|
||||||
"tls-auth",
|
|
||||||
"tls-crypt",
|
|
||||||
"tls-crypt-v2",
|
|
||||||
"secret",
|
|
||||||
"crl-verify",
|
|
||||||
"extra-certs"
|
|
||||||
]
|
|
||||||
|
|
||||||
var removed: [String: Int] = [:]
|
|
||||||
var normalized: [String: Int] = [:]
|
|
||||||
var output: [String] = []
|
|
||||||
var activeInlineTag: String?
|
|
||||||
|
|
||||||
for rawLine in config.split(whereSeparator: \.isNewline) {
|
|
||||||
let line = String(rawLine)
|
|
||||||
let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
if trimmed.isEmpty {
|
|
||||||
output.append(line)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
let trimmedLowercased = trimmed.lowercased()
|
|
||||||
|
|
||||||
if let currentInlineTag = activeInlineTag {
|
|
||||||
output.append(line)
|
|
||||||
if trimmedLowercased == "</\(currentInlineTag)>" {
|
|
||||||
activeInlineTag = nil
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if trimmedLowercased.hasPrefix("<"),
|
|
||||||
trimmedLowercased.hasSuffix(">"),
|
|
||||||
!trimmedLowercased.hasPrefix("</") {
|
|
||||||
let tagContent = String(trimmedLowercased.dropFirst().dropLast())
|
|
||||||
let tagName = tagContent
|
|
||||||
.split(whereSeparator: { $0 == " " || $0 == "\t" })
|
|
||||||
.first
|
|
||||||
.map(String.init) ?? ""
|
|
||||||
if inlineBlockTags.contains(tagName) {
|
|
||||||
activeInlineTag = tagName
|
|
||||||
output.append(line)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if trimmed.hasPrefix("#") || trimmed.hasPrefix(";") {
|
|
||||||
output.append(line)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
let parts = trimmed.split(whereSeparator: { $0 == " " || $0 == "\t" })
|
|
||||||
let token = parts.first.map(String.init)?.lowercased() ?? ""
|
|
||||||
if trimmedLowercased.hasPrefix("redirect-gateway") || token.hasPrefix("redirect-gateway") {
|
|
||||||
let hasDef1 = parts.dropFirst().contains { String($0).lowercased().hasPrefix("def1") }
|
|
||||||
if hasDef1 {
|
|
||||||
output.append("redirect-gateway def1")
|
|
||||||
normalized["redirect-gateway", default: 0] += 1
|
|
||||||
} else {
|
|
||||||
removed["redirect-gateway", default: 0] += 1
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if let matchedUnsupported = unsupportedTokens.first(where: { token.hasPrefix($0) }) {
|
|
||||||
removed[matchedUnsupported, default: 0] += 1
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
output.append(line)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !removed.isEmpty {
|
|
||||||
let summary = removed.keys.sorted().map { "\($0)=\(removed[$0] ?? 0)" }.joined(separator: " ")
|
|
||||||
ovpnLog(.info, title: "ConfigStrip", message: summary)
|
|
||||||
}
|
|
||||||
if !normalized.isEmpty {
|
|
||||||
let summary = normalized.keys.sorted().map { "\($0)=\(normalized[$0] ?? 0)" }.joined(separator: " ")
|
|
||||||
ovpnLog(.info, title: "ConfigNormalize", message: summary)
|
|
||||||
}
|
|
||||||
|
|
||||||
return output.joined(separator: "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func isIPv4Address(_ value: String) -> Bool {
|
|
||||||
let parts = value.split(separator: ".")
|
|
||||||
if parts.count != 4 { return false }
|
|
||||||
for part in parts {
|
|
||||||
guard let num = Int(part), num >= 0 && num <= 255 else { return false }
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process events returned by the OpenVPN library
|
// Process events returned by the OpenVPN library
|
||||||
@@ -851,9 +250,6 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
|
|||||||
|
|
||||||
startHandler(nil)
|
startHandler(nil)
|
||||||
self.startHandler = nil
|
self.startHandler = nil
|
||||||
|
|
||||||
logOpenVPNConnectionInfo()
|
|
||||||
refreshOpenVPNSettingsAfterConnect()
|
|
||||||
case .disconnected:
|
case .disconnected:
|
||||||
guard let stopHandler = stopHandler else { return }
|
guard let stopHandler = stopHandler else { return }
|
||||||
|
|
||||||
@@ -896,41 +292,4 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
|
|||||||
// Handle log messages
|
// Handle log messages
|
||||||
ovpnLog(.info, message: logMessage)
|
ovpnLog(.info, message: logMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
func openVPNAdapterDidReceiveClockTick(_ openVPNAdapter: OpenVPNAdapter) {
|
|
||||||
let now = Date()
|
|
||||||
if now.timeIntervalSince(lastOpenVPNStatsLogTime) < 5 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
lastOpenVPNStatsLogTime = now
|
|
||||||
|
|
||||||
let transport = openVPNAdapter.transportStatistics
|
|
||||||
let iface = openVPNAdapter.interfaceStatistics
|
|
||||||
let transportLine = "transport bytesIn=\(transport.bytesIn) bytesOut=\(transport.bytesOut) packetsIn=\(transport.packetsIn) packetsOut=\(transport.packetsOut)"
|
|
||||||
let ifaceLine = "iface bytesIn=\(iface.bytesIn) bytesOut=\(iface.bytesOut) packetsIn=\(iface.packetsIn) packetsOut=\(iface.packetsOut) errorsIn=\(iface.errorsIn) errorsOut=\(iface.errorsOut)"
|
|
||||||
ovpnLog(.info, title: "Stats", message: "\(transportLine) | \(ifaceLine)")
|
|
||||||
}
|
|
||||||
|
|
||||||
private func refreshOpenVPNSettingsAfterConnect() {
|
|
||||||
let localAddr = openVpnLocalAddress
|
|
||||||
var net30Gateway: String?
|
|
||||||
if let localAddr, let mask = openVpnLocalMask {
|
|
||||||
net30Gateway = Self.net30Peer(for: localAddr, mask: mask)
|
|
||||||
}
|
|
||||||
var gateway = net30Gateway
|
|
||||||
if let adapterGateway = ovpnAdapter?.connectionInformation?.gatewayIPv4, !adapterGateway.isEmpty {
|
|
||||||
if let localAddr, adapterGateway == localAddr {
|
|
||||||
ovpnLog(.info, title: "IPv4Gateway", message: "post-connect ignoring adapter gateway equal to local address=\(adapterGateway)")
|
|
||||||
} else if let net30Gateway, net30Gateway != adapterGateway {
|
|
||||||
ovpnLog(.info, title: "IPv4Gateway", message: "post-connect keeping net30 peer=\(net30Gateway), adapter gateway=\(adapterGateway)")
|
|
||||||
} else {
|
|
||||||
gateway = adapterGateway
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let gateway, !gateway.isEmpty else { return }
|
|
||||||
openVpnGatewayAddress = gateway
|
|
||||||
ovpnLog(.info, title: "IPv4Gateway", message: "post-connect gateway=\(gateway)")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import Darwin
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import NetworkExtension
|
import NetworkExtension
|
||||||
|
|
||||||
@@ -7,7 +6,6 @@ enum XrayErrors: Error {
|
|||||||
case xrayConfigIsWrong
|
case xrayConfigIsWrong
|
||||||
case cantSaveXrayConfig
|
case cantSaveXrayConfig
|
||||||
case cantParseListenAndPort
|
case cantParseListenAndPort
|
||||||
case cantAcquireLocalPort
|
|
||||||
case cantSaveHevSocksConfig
|
case cantSaveHevSocksConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,80 +21,6 @@ extension Constants {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension PacketTunnelProvider {
|
extension PacketTunnelProvider {
|
||||||
/// TCP port chosen by the OS on IPv6 loopback (::1), matching inbound listen address.
|
|
||||||
private func acquireFreeLocalPort() throws -> Int {
|
|
||||||
let fd = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP)
|
|
||||||
guard fd != -1 else {
|
|
||||||
throw XrayErrors.cantAcquireLocalPort
|
|
||||||
}
|
|
||||||
defer { close(fd) }
|
|
||||||
var reuse: Int32 = 1
|
|
||||||
_ = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, socklen_t(MemoryLayout<Int32>.size))
|
|
||||||
var addr = sockaddr_in6()
|
|
||||||
addr.sin6_len = UInt8(MemoryLayout<sockaddr_in6>.size)
|
|
||||||
addr.sin6_family = sa_family_t(AF_INET6)
|
|
||||||
addr.sin6_port = in_port_t(0).bigEndian
|
|
||||||
addr.sin6_addr = in6addr_loopback
|
|
||||||
addr.sin6_scope_id = 0
|
|
||||||
let bindResult = withUnsafePointer(to: &addr) { ptr in
|
|
||||||
ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { p in
|
|
||||||
bind(fd, p, socklen_t(MemoryLayout<sockaddr_in6>.size))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
guard bindResult == 0 else {
|
|
||||||
throw XrayErrors.cantAcquireLocalPort
|
|
||||||
}
|
|
||||||
var bound = sockaddr_in6()
|
|
||||||
var len = socklen_t(MemoryLayout<sockaddr_in6>.size)
|
|
||||||
let gr = withUnsafeMutablePointer(to: &bound) { p in
|
|
||||||
p.withMemoryRebound(to: sockaddr.self, capacity: 1) { bp in
|
|
||||||
getsockname(fd, bp, &len)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
guard gr == 0 else {
|
|
||||||
throw XrayErrors.cantAcquireLocalPort
|
|
||||||
}
|
|
||||||
return Int(bound.sin6_port.byteSwapped)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func applyXraySplitTunnel(_ xrayConfig: XrayConfig,
|
|
||||||
settings: NEPacketTunnelNetworkSettings) {
|
|
||||||
guard let splitTunnelType = xrayConfig.splitTunnelType else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let splitTunnelSites = xrayConfig.splitTunnelSites else {
|
|
||||||
xrayLog(.error, message: "Split tunnel sites are not set")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if splitTunnelType == 1 {
|
|
||||||
var ipv4IncludedRoutes = [NEIPv4Route]()
|
|
||||||
|
|
||||||
for allowedIPString in splitTunnelSites {
|
|
||||||
if let allowedIP = IPAddressRange(from: allowedIPString) {
|
|
||||||
ipv4IncludedRoutes.append(NEIPv4Route(
|
|
||||||
destinationAddress: "\(allowedIP.address)",
|
|
||||||
subnetMask: "\(allowedIP.subnetMask())"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
settings.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
|
|
||||||
} else if splitTunnelType == 2 {
|
|
||||||
var ipv4ExcludedRoutes = [NEIPv4Route]()
|
|
||||||
|
|
||||||
for excludedIPString in splitTunnelSites {
|
|
||||||
if let excludedIP = IPAddressRange(from: excludedIPString) {
|
|
||||||
ipv4ExcludedRoutes.append(NEIPv4Route(
|
|
||||||
destinationAddress: "\(excludedIP.address)",
|
|
||||||
subnetMask: "\(excludedIP.subnetMask())"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
settings.ipv4Settings?.excludedRoutes = ipv4ExcludedRoutes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func startXray(completionHandler: @escaping (Error?) -> Void) {
|
func startXray(completionHandler: @escaping (Error?) -> Void) {
|
||||||
|
|
||||||
// Xray configuration
|
// Xray configuration
|
||||||
@@ -148,7 +72,6 @@ extension PacketTunnelProvider {
|
|||||||
settings.dnsSettings = !dnsArray.isEmpty
|
settings.dnsSettings = !dnsArray.isEmpty
|
||||||
? NEDNSSettings(servers: dnsArray)
|
? NEDNSSettings(servers: dnsArray)
|
||||||
: NEDNSSettings(servers: ["1.1.1.1"])
|
: NEDNSSettings(servers: ["1.1.1.1"])
|
||||||
applyXraySplitTunnel(xrayConfig, settings: settings)
|
|
||||||
|
|
||||||
let xrayConfigData = xrayConfig.config.data(using: .utf8)
|
let xrayConfigData = xrayConfig.config.data(using: .utf8)
|
||||||
|
|
||||||
@@ -167,11 +90,14 @@ extension PacketTunnelProvider {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let port = try acquireFreeLocalPort()
|
let port = 10808
|
||||||
let address = "::1"
|
let address = "::1"
|
||||||
|
|
||||||
// Extract existing SOCKS5 credentials or generate new ones per session.
|
if var inboundsArray = jsonDict["inbounds"] as? [[String: Any]], !inboundsArray.isEmpty {
|
||||||
let socksCredentials = ensureInboundAuth(jsonDict: &jsonDict, port: port, address: address)
|
inboundsArray[0]["port"] = port
|
||||||
|
inboundsArray[0]["listen"] = address
|
||||||
|
jsonDict["inbounds"] = inboundsArray
|
||||||
|
}
|
||||||
|
|
||||||
let updatedData = try JSONSerialization.data(withJSONObject: jsonDict, options: [])
|
let updatedData = try JSONSerialization.data(withJSONObject: jsonDict, options: [])
|
||||||
|
|
||||||
@@ -194,8 +120,6 @@ extension PacketTunnelProvider {
|
|||||||
self?.setupAndRunTun2socks(configData: updatedData,
|
self?.setupAndRunTun2socks(configData: updatedData,
|
||||||
address: address,
|
address: address,
|
||||||
port: port,
|
port: port,
|
||||||
username: socksCredentials.username,
|
|
||||||
password: socksCredentials.password,
|
|
||||||
completionHandler: completionHandler)
|
completionHandler: completionHandler)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -220,62 +144,6 @@ extension PacketTunnelProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct SocksCredentials {
|
|
||||||
let username: String
|
|
||||||
let password: String
|
|
||||||
}
|
|
||||||
|
|
||||||
private func indexOfSocksInbound(in inboundsArray: [[String: Any]]) -> Int? {
|
|
||||||
for (i, inbound) in inboundsArray.enumerated() {
|
|
||||||
guard let proto = inbound["protocol"] as? String else { continue }
|
|
||||||
if proto.caseInsensitiveCompare("socks") == .orderedSame {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns existing SOCKS5 credentials from the inbound config, or generates and injects
|
|
||||||
// new random ones. Also sets port and address on the socks inbound entry.
|
|
||||||
private func ensureInboundAuth(jsonDict: inout [String: Any], port: Int, address: String) -> SocksCredentials {
|
|
||||||
var inboundsArray = jsonDict["inbounds"] as? [[String: Any]] ?? []
|
|
||||||
|
|
||||||
if let socksIdx = indexOfSocksInbound(in: inboundsArray) {
|
|
||||||
var inbound = inboundsArray[socksIdx]
|
|
||||||
inbound["port"] = port
|
|
||||||
inbound["listen"] = address
|
|
||||||
|
|
||||||
var settings = inbound["settings"] as? [String: Any] ?? [:]
|
|
||||||
if let accounts = settings["accounts"] as? [[String: Any]],
|
|
||||||
let first = accounts.first,
|
|
||||||
let user = first["user"] as? String, !user.isEmpty,
|
|
||||||
let pass = first["pass"] as? String, !pass.isEmpty {
|
|
||||||
// Re-use existing credentials, but always enforce auth mode in case the
|
|
||||||
// imported config had accounts but auth: "noauth" (or no auth field).
|
|
||||||
settings["auth"] = "password"
|
|
||||||
inbound["settings"] = settings
|
|
||||||
inboundsArray[socksIdx] = inbound
|
|
||||||
jsonDict["inbounds"] = inboundsArray
|
|
||||||
return SocksCredentials(username: user, password: pass)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate new random credentials for this session
|
|
||||||
let user = UUID().uuidString.replacingOccurrences(of: "-", with: "").lowercased().prefix(16)
|
|
||||||
let pass = UUID().uuidString.replacingOccurrences(of: "-", with: "").lowercased()
|
|
||||||
settings["auth"] = "password"
|
|
||||||
settings["accounts"] = [["user": String(user), "pass": pass]]
|
|
||||||
inbound["settings"] = settings
|
|
||||||
inboundsArray[socksIdx] = inbound
|
|
||||||
jsonDict["inbounds"] = inboundsArray
|
|
||||||
return SocksCredentials(username: String(user), password: pass)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: no socks inbound — generate credentials but can't inject
|
|
||||||
let user = UUID().uuidString.replacingOccurrences(of: "-", with: "").lowercased().prefix(16)
|
|
||||||
let pass = UUID().uuidString.replacingOccurrences(of: "-", with: "").lowercased()
|
|
||||||
return SocksCredentials(username: String(user), password: pass)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setupAndStartXray(configData: Data,
|
private func setupAndStartXray(configData: Data,
|
||||||
completionHandler: @escaping (Error?) -> Void) {
|
completionHandler: @escaping (Error?) -> Void) {
|
||||||
let path = Constants.cachesDirectory.appendingPathComponent("config.json", isDirectory: false).path
|
let path = Constants.cachesDirectory.appendingPathComponent("config.json", isDirectory: false).path
|
||||||
@@ -307,8 +175,6 @@ extension PacketTunnelProvider {
|
|||||||
private func setupAndRunTun2socks(configData: Data,
|
private func setupAndRunTun2socks(configData: Data,
|
||||||
address: String,
|
address: String,
|
||||||
port: Int,
|
port: Int,
|
||||||
username: String,
|
|
||||||
password: String,
|
|
||||||
completionHandler: @escaping (Error?) -> Void) {
|
completionHandler: @escaping (Error?) -> Void) {
|
||||||
let config = """
|
let config = """
|
||||||
tunnel:
|
tunnel:
|
||||||
@@ -316,8 +182,6 @@ extension PacketTunnelProvider {
|
|||||||
socks5:
|
socks5:
|
||||||
port: \(port)
|
port: \(port)
|
||||||
address: \(address)
|
address: \(address)
|
||||||
username: \(username)
|
|
||||||
password: \(password)
|
|
||||||
udp: 'udp'
|
udp: 'udp'
|
||||||
misc:
|
misc:
|
||||||
task-stack-size: 20480
|
task-stack-size: 20480
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import NetworkExtension
|
|||||||
import Network
|
import Network
|
||||||
import os
|
import os
|
||||||
import Darwin
|
import Darwin
|
||||||
|
#if !os(tvOS)
|
||||||
import OpenVPNAdapter
|
import OpenVPNAdapter
|
||||||
|
#endif
|
||||||
|
|
||||||
enum TunnelProtoType: String {
|
enum TunnelProtoType: String {
|
||||||
case wireguard, openvpn, xray
|
case wireguard, openvpn, xray
|
||||||
@@ -38,31 +40,22 @@ struct Constants {
|
|||||||
|
|
||||||
class PacketTunnelProvider: NEPacketTunnelProvider {
|
class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||||
var wgAdapter: WireGuardAdapter?
|
var wgAdapter: WireGuardAdapter?
|
||||||
|
#if !os(tvOS)
|
||||||
var ovpnAdapter: OpenVPNAdapter?
|
var ovpnAdapter: OpenVPNAdapter?
|
||||||
private lazy var openVPNPacketFlowAdapter = PacketTunnelFlowAdapter(flow: packetFlow)
|
private lazy var openVPNPacketFlowAdapter = PacketTunnelFlowAdapter(flow: packetFlow)
|
||||||
|
#endif
|
||||||
private let pathMonitorQueue = DispatchQueue(label: Constants.processQueueName + ".path-monitor")
|
private let pathMonitorQueue = DispatchQueue(label: Constants.processQueueName + ".path-monitor")
|
||||||
private let networkChangeQueue = DispatchQueue(label: Constants.processQueueName + ".network-change")
|
|
||||||
private let pathMonitor = NWPathMonitor()
|
private let pathMonitor = NWPathMonitor()
|
||||||
private var didReceiveInitialPathUpdate = false
|
private var didReceiveInitialPathUpdate = false
|
||||||
private var currentPath: Network.NWPath?
|
private var currentPath: Network.NWPath?
|
||||||
private var currentPathSignature: String?
|
private var currentPathSignature: String?
|
||||||
private var pendingOpenVPNReconnectWorkItem: DispatchWorkItem?
|
|
||||||
private var pendingNetworkChangeWorkItem: DispatchWorkItem?
|
|
||||||
private var isApplyingNetworkChange = false
|
|
||||||
private var lastOpenVPNReachabilityStatus: OpenVPNReachabilityStatus?
|
|
||||||
|
|
||||||
var splitTunnelType: Int?
|
var splitTunnelType: Int?
|
||||||
var splitTunnelSites: [String]?
|
var splitTunnelSites: [String]?
|
||||||
var openVpnDnsServers: [String] = []
|
|
||||||
var openVpnRemoteAddress: String?
|
|
||||||
var openVpnRedirectGatewayDef1 = false
|
|
||||||
var openVpnLocalAddress: String?
|
|
||||||
var openVpnLocalMask: String?
|
|
||||||
var openVpnGatewayAddress: String?
|
|
||||||
var lastOpenVPNSettings: NEPacketTunnelNetworkSettings?
|
|
||||||
var lastOpenVPNStatsLogTime = Date.distantPast
|
|
||||||
|
|
||||||
|
#if !os(tvOS)
|
||||||
let vpnReachability = OpenVPNReachability()
|
let vpnReachability = OpenVPNReachability()
|
||||||
|
#endif
|
||||||
|
|
||||||
var startHandler: ((Error?) -> Void)?
|
var startHandler: ((Error?) -> Void)?
|
||||||
var stopHandler: (() -> Void)?
|
var stopHandler: (() -> Void)?
|
||||||
@@ -70,9 +63,11 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||||||
|
|
||||||
var activeIfaceIdx: UInt32 = 0
|
var activeIfaceIdx: UInt32 = 0
|
||||||
|
|
||||||
|
#if !os(tvOS)
|
||||||
func openVPNPacketFlow() -> OpenVPNAdapterPacketFlow {
|
func openVPNPacketFlow() -> OpenVPNAdapterPacketFlow {
|
||||||
openVPNPacketFlowAdapter
|
openVPNPacketFlowAdapter
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
super.init()
|
super.init()
|
||||||
@@ -91,22 +86,14 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||||||
|
|
||||||
guard hasMeaningfulChange, let proto = self.protoType else { return }
|
guard hasMeaningfulChange, let proto = self.protoType else { return }
|
||||||
|
|
||||||
// WireGuard/AWG and OpenVPN manages network changes internally in its own adapter.
|
// WireGuard/AWG manages network changes internally; avoid restarting the tunnel here.
|
||||||
if proto == .wireguard || proto == .openvpn {
|
if proto == .wireguard {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if proto == .openvpn {
|
DispatchQueue.main.async {
|
||||||
self.scheduleOpenVPNReconnect(reason: "NWPath changed")
|
self.handle(networkChange: path) { _ in }
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.isApplyingNetworkChange || self.reasserting {
|
|
||||||
xrayLog(.debug, message: "Ignoring path change while xray restart is in progress")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.scheduleNetworkChangeHandling(for: proto, path: path)
|
|
||||||
}
|
}
|
||||||
pathMonitor.start(queue: pathMonitorQueue)
|
pathMonitor.start(queue: pathMonitorQueue)
|
||||||
|
|
||||||
@@ -200,26 +187,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||||||
let errorNotifier = ErrorNotifier(activationAttemptId: activationAttemptId)
|
let errorNotifier = ErrorNotifier(activationAttemptId: activationAttemptId)
|
||||||
|
|
||||||
neLog(.info, message: "Start tunnel")
|
neLog(.info, message: "Start tunnel")
|
||||||
if let vpnProto = protocolConfiguration as? NEVPNProtocol {
|
|
||||||
if #available(iOS 14.0, macOS 11.0, *) {
|
|
||||||
var details = "includeAllNetworks=\(vpnProto.includeAllNetworks)"
|
|
||||||
if #available(iOS 14.2, macOS 11.0, *) {
|
|
||||||
details += " excludeLocalNetworks=\(vpnProto.excludeLocalNetworks)"
|
|
||||||
}
|
|
||||||
neLog(.info, title: "Protocol", message: details)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let protocolConfiguration = protocolConfiguration as? NETunnelProviderProtocol {
|
if let protocolConfiguration = protocolConfiguration as? NETunnelProviderProtocol {
|
||||||
let providerConfiguration = protocolConfiguration.providerConfiguration
|
let providerConfiguration = protocolConfiguration.providerConfiguration
|
||||||
let providerKeys = providerConfiguration?.keys.sorted().joined(separator: ",") ?? ""
|
|
||||||
var protocolDetails = "bundleId=\(protocolConfiguration.providerBundleIdentifier ?? "") keys=[\(providerKeys)]"
|
|
||||||
if let ovpnData = providerConfiguration?[Constants.ovpnConfigKey] as? Data {
|
|
||||||
let preview = String(decoding: ovpnData.prefix(512), as: UTF8.self)
|
|
||||||
protocolDetails += " ovpnBytes=\(ovpnData.count) ovpnPreview=\(preview)"
|
|
||||||
}
|
|
||||||
neLog(.info, title: "Protocol", message: protocolDetails)
|
|
||||||
|
|
||||||
if (providerConfiguration?[Constants.ovpnConfigKey] as? Data) != nil {
|
if (providerConfiguration?[Constants.ovpnConfigKey] as? Data) != nil {
|
||||||
protoType = .openvpn
|
protoType = .openvpn
|
||||||
} else if (providerConfiguration?[Constants.wireGuardConfigKey] as? Data) != nil {
|
} else if (providerConfiguration?[Constants.wireGuardConfigKey] as? Data) != nil {
|
||||||
@@ -235,8 +205,6 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelPendingOpenVPNReconnect()
|
|
||||||
cancelPendingNetworkChangeHandling()
|
|
||||||
didReceiveInitialPathUpdate = false
|
didReceiveInitialPathUpdate = false
|
||||||
updateActiveInterfaceIndexForCurrentPath()
|
updateActiveInterfaceIndexForCurrentPath()
|
||||||
|
|
||||||
@@ -246,18 +214,27 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||||||
errorNotifier: errorNotifier,
|
errorNotifier: errorNotifier,
|
||||||
completionHandler: completionHandler)
|
completionHandler: completionHandler)
|
||||||
case .openvpn:
|
case .openvpn:
|
||||||
|
#if os(tvOS)
|
||||||
|
completionHandler(NSError(domain: "org.amnezia.ne",
|
||||||
|
code: -1002,
|
||||||
|
userInfo: [NSLocalizedDescriptionKey: "OpenVPN backend is not available for tvOS in this build"]))
|
||||||
|
#else
|
||||||
startOpenVPN(completionHandler: completionHandler)
|
startOpenVPN(completionHandler: completionHandler)
|
||||||
|
#endif
|
||||||
case .xray:
|
case .xray:
|
||||||
|
#if os(tvOS)
|
||||||
|
completionHandler(NSError(domain: "org.amnezia.ne",
|
||||||
|
code: -1003,
|
||||||
|
userInfo: [NSLocalizedDescriptionKey: "Xray backend is not available for tvOS in this build"]))
|
||||||
|
#else
|
||||||
startXray(completionHandler: completionHandler)
|
startXray(completionHandler: completionHandler)
|
||||||
|
#endif
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
|
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
|
||||||
cancelPendingOpenVPNReconnect()
|
|
||||||
cancelPendingNetworkChangeHandling()
|
|
||||||
|
|
||||||
guard let protoType else {
|
guard let protoType else {
|
||||||
completionHandler()
|
completionHandler()
|
||||||
return
|
return
|
||||||
@@ -268,10 +245,18 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||||||
stopWireguard(with: reason,
|
stopWireguard(with: reason,
|
||||||
completionHandler: completionHandler)
|
completionHandler: completionHandler)
|
||||||
case .openvpn:
|
case .openvpn:
|
||||||
|
#if os(tvOS)
|
||||||
|
completionHandler()
|
||||||
|
#else
|
||||||
stopOpenVPN(with: reason,
|
stopOpenVPN(with: reason,
|
||||||
completionHandler: completionHandler)
|
completionHandler: completionHandler)
|
||||||
|
#endif
|
||||||
case .xray:
|
case .xray:
|
||||||
|
#if os(tvOS)
|
||||||
|
completionHandler()
|
||||||
|
#else
|
||||||
stopXray(completionHandler: completionHandler)
|
stopXray(completionHandler: completionHandler)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,7 +270,11 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||||||
case .wireguard:
|
case .wireguard:
|
||||||
handleWireguardStatusMessage(messageData, completionHandler: completionHandler)
|
handleWireguardStatusMessage(messageData, completionHandler: completionHandler)
|
||||||
case .openvpn:
|
case .openvpn:
|
||||||
|
#if !os(tvOS)
|
||||||
handleOpenVPNStatusMessage(messageData, completionHandler: completionHandler)
|
handleOpenVPNStatusMessage(messageData, completionHandler: completionHandler)
|
||||||
|
#else
|
||||||
|
completionHandler?(nil)
|
||||||
|
#endif
|
||||||
case .xray:
|
case .xray:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -302,111 +291,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func handle(networkChange changePath: Network.NWPath, completion: @escaping (Error?) -> Void) {
|
private func handle(networkChange changePath: Network.NWPath, completion: @escaping (Error?) -> Void) {
|
||||||
guard protoType == .xray else {
|
|
||||||
updateActiveInterfaceIndex(for: changePath)
|
|
||||||
completion(nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
updateActiveInterfaceIndex(for: changePath)
|
updateActiveInterfaceIndex(for: changePath)
|
||||||
reasserting = true
|
neLog(.info, message: "Tunnel restarted.")
|
||||||
xrayLog(.info, message: "Applying network change to xray tunnel")
|
startTunnel(options: nil, completionHandler: completion)
|
||||||
stopXray { }
|
|
||||||
startXray { [weak self] error in
|
|
||||||
self?.reasserting = false
|
|
||||||
completion(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func scheduleNetworkChangeHandling(for proto: TunnelProtoType, path: Network.NWPath) {
|
|
||||||
guard proto == .xray else { return }
|
|
||||||
|
|
||||||
pendingNetworkChangeWorkItem?.cancel()
|
|
||||||
|
|
||||||
let workItem = DispatchWorkItem { [weak self] in
|
|
||||||
guard let self else { return }
|
|
||||||
self.pendingNetworkChangeWorkItem = nil
|
|
||||||
|
|
||||||
if self.isApplyingNetworkChange || self.reasserting {
|
|
||||||
xrayLog(.debug, message: "Skipping network change while restart is already in progress")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.isApplyingNetworkChange = true
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.handle(networkChange: path) { [weak self] _ in
|
|
||||||
self?.networkChangeQueue.async {
|
|
||||||
self?.isApplyingNetworkChange = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pendingNetworkChangeWorkItem = workItem
|
|
||||||
networkChangeQueue.asyncAfter(deadline: .now() + 1.0, execute: workItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func scheduleOpenVPNReconnect(reason: String) {
|
|
||||||
guard protoType == .openvpn else { return }
|
|
||||||
|
|
||||||
pendingOpenVPNReconnectWorkItem?.cancel()
|
|
||||||
|
|
||||||
let workItem = DispatchWorkItem { [weak self] in
|
|
||||||
guard let self else { return }
|
|
||||||
self.pendingOpenVPNReconnectWorkItem = nil
|
|
||||||
|
|
||||||
guard self.protoType == .openvpn else { return }
|
|
||||||
|
|
||||||
if self.reasserting {
|
|
||||||
ovpnLog(.debug, message: "Skipping OpenVPN reconnect while session is already reasserting")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
DispatchQueue.main.async { [weak self] in
|
|
||||||
guard let self else { return }
|
|
||||||
guard !self.reasserting else {
|
|
||||||
ovpnLog(.debug, message: "Skipping OpenVPN reconnect while session is already reasserting")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ovpnLog(.info, message: "\(reason), reconnecting OpenVPN session")
|
|
||||||
self.ovpnAdapter?.reconnect(afterTimeInterval: 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pendingOpenVPNReconnectWorkItem = workItem
|
|
||||||
networkChangeQueue.asyncAfter(deadline: .now() + 1.0, execute: workItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleOpenVPNReachabilityChange(_ status: OpenVPNReachabilityStatus) {
|
|
||||||
defer { lastOpenVPNReachabilityStatus = status }
|
|
||||||
|
|
||||||
guard let previousStatus = lastOpenVPNReachabilityStatus else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard previousStatus != status else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch status {
|
|
||||||
case .reachableViaWiFi, .reachableViaWWAN:
|
|
||||||
scheduleOpenVPNReconnect(reason: "Reachability changed")
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func cancelPendingOpenVPNReconnect() {
|
|
||||||
pendingOpenVPNReconnectWorkItem?.cancel()
|
|
||||||
pendingOpenVPNReconnectWorkItem = nil
|
|
||||||
lastOpenVPNReachabilityStatus = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
private func cancelPendingNetworkChangeHandling() {
|
|
||||||
pendingNetworkChangeWorkItem?.cancel()
|
|
||||||
pendingNetworkChangeWorkItem = nil
|
|
||||||
isApplyingNetworkChange = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,14 +303,8 @@ private extension PacketTunnelProvider {
|
|||||||
signatureComponents.append(path.isExpensive ? "exp" : "noexp")
|
signatureComponents.append(path.isExpensive ? "exp" : "noexp")
|
||||||
signatureComponents.append(path.isConstrained ? "con" : "nocon")
|
signatureComponents.append(path.isConstrained ? "con" : "nocon")
|
||||||
|
|
||||||
// Ignore loopback and tunnel-style `.other` interfaces so Xray does not
|
let preferredTypes: [NWInterface.InterfaceType] = [.wiredEthernet, .wifi, .cellular, .loopback, .other]
|
||||||
// react to its own utun lifecycle as if the physical uplink changed.
|
let sortedInterfaces = path.availableInterfaces.sorted { lhs, rhs in
|
||||||
let preferredTypes: [NWInterface.InterfaceType] = [.wiredEthernet, .wifi, .cellular]
|
|
||||||
let externalInterfaces = path.availableInterfaces.filter { interface in
|
|
||||||
interface.type == .wiredEthernet || interface.type == .wifi || interface.type == .cellular
|
|
||||||
}
|
|
||||||
|
|
||||||
let sortedInterfaces = externalInterfaces.sorted { lhs, rhs in
|
|
||||||
if lhs.type == rhs.type {
|
if lhs.type == rhs.type {
|
||||||
return lhs.index < rhs.index
|
return lhs.index < rhs.index
|
||||||
}
|
}
|
||||||
@@ -444,8 +325,8 @@ private extension PacketTunnelProvider {
|
|||||||
case .wiredEthernet: typeName = "ethernet"
|
case .wiredEthernet: typeName = "ethernet"
|
||||||
case .wifi: typeName = "wifi"
|
case .wifi: typeName = "wifi"
|
||||||
case .cellular: typeName = "cellular"
|
case .cellular: typeName = "cellular"
|
||||||
case .loopback, .other:
|
case .loopback: typeName = "loopback"
|
||||||
continue
|
case .other: typeName = "other"
|
||||||
@unknown default: typeName = "unknown"
|
@unknown default: typeName = "unknown"
|
||||||
}
|
}
|
||||||
signatureComponents.append("\(typeName):\(interface.index)")
|
signatureComponents.append("\(typeName):\(interface.index)")
|
||||||
@@ -462,20 +343,19 @@ private extension PacketTunnelProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension WireGuardLogLevel {
|
extension WireGuardLogLevel {
|
||||||
var osLogLevel: OSLogType {
|
var osLogLevel: OSLogType {
|
||||||
switch self {
|
switch self {
|
||||||
case .verbose:
|
case .verbose:
|
||||||
return .debug
|
return .debug
|
||||||
case .error:
|
case .error:
|
||||||
return .error
|
return .error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if !os(tvOS)
|
||||||
final class PacketTunnelFlowAdapter: NSObject, OpenVPNAdapterPacketFlow {
|
final class PacketTunnelFlowAdapter: NSObject, OpenVPNAdapterPacketFlow {
|
||||||
private let flow: NEPacketTunnelFlow
|
private let flow: NEPacketTunnelFlow
|
||||||
private var readLogCounter = 0
|
|
||||||
private var writeLogCounter = 0
|
|
||||||
|
|
||||||
init(flow: NEPacketTunnelFlow) {
|
init(flow: NEPacketTunnelFlow) {
|
||||||
self.flow = flow
|
self.flow = flow
|
||||||
@@ -484,97 +364,15 @@ final class PacketTunnelFlowAdapter: NSObject, OpenVPNAdapterPacketFlow {
|
|||||||
|
|
||||||
@objc(readPacketsWithCompletionHandler:)
|
@objc(readPacketsWithCompletionHandler:)
|
||||||
func readPackets(completionHandler: @escaping ([Data], [NSNumber]) -> Void) {
|
func readPackets(completionHandler: @escaping ([Data], [NSNumber]) -> Void) {
|
||||||
flow.readPackets { packets, protocols in
|
flow.readPackets(completionHandler: completionHandler)
|
||||||
#if os(macOS)
|
|
||||||
if self.readLogCounter < 20, let firstPacket = packets.first, let firstProtocol = protocols.first {
|
|
||||||
let prefix = firstPacket.prefix(12).map { String(format: "%02x", $0) }.joined()
|
|
||||||
let header = Self.describePacketHeader(firstPacket)
|
|
||||||
ovpnLog(.info, title: "FlowRead", message: "count=\(packets.count) proto0=\(firstProtocol) len0=\(firstPacket.count) prefix=\(prefix) \(header)")
|
|
||||||
self.readLogCounter += 1
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
completionHandler(packets, protocols)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc(writePackets:withProtocols:)
|
@objc(writePackets:withProtocols:)
|
||||||
func writePackets(_ packets: [Data], withProtocols protocols: [NSNumber]) -> Bool {
|
func writePackets(_ packets: [Data], withProtocols protocols: [NSNumber]) -> Bool {
|
||||||
#if os(macOS)
|
flow.writePackets(packets, withProtocols: protocols)
|
||||||
if writeLogCounter < 20, let firstPacket = packets.first, let firstProtocol = protocols.first {
|
|
||||||
let prefix = firstPacket.prefix(12).map { String(format: "%02x", $0) }.joined()
|
|
||||||
let header = Self.describePacketHeader(firstPacket)
|
|
||||||
ovpnLog(.info, title: "FlowWrite", message: "count=\(packets.count) proto0=\(firstProtocol) len0=\(firstPacket.count) prefix=\(prefix) \(header)")
|
|
||||||
writeLogCounter += 1
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
return flow.writePackets(packets, withProtocols: protocols)
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func describePacketHeader(_ packet: Data) -> String {
|
|
||||||
guard let versionNibble = packet.first.map({ Int($0 >> 4) }) else {
|
|
||||||
return "ip=unknown"
|
|
||||||
}
|
|
||||||
|
|
||||||
if versionNibble == 4, packet.count >= 20 {
|
|
||||||
let ihl = Int(packet[0] & 0x0f) * 4
|
|
||||||
guard ihl >= 20, packet.count >= ihl else {
|
|
||||||
return "ip=ipv4 malformed"
|
|
||||||
}
|
|
||||||
|
|
||||||
let proto = packet[9]
|
|
||||||
let src = "\(packet[12]).\(packet[13]).\(packet[14]).\(packet[15])"
|
|
||||||
let dst = "\(packet[16]).\(packet[17]).\(packet[18]).\(packet[19])"
|
|
||||||
let l4Offset = ihl
|
|
||||||
let ports: String
|
|
||||||
if (proto == 6 || proto == 17) && packet.count >= l4Offset + 4 {
|
|
||||||
let srcPort = (UInt16(packet[l4Offset]) << 8) | UInt16(packet[l4Offset + 1])
|
|
||||||
let dstPort = (UInt16(packet[l4Offset + 2]) << 8) | UInt16(packet[l4Offset + 3])
|
|
||||||
ports = "sport=\(srcPort) dport=\(dstPort)"
|
|
||||||
} else {
|
|
||||||
ports = "sport=- dport=-"
|
|
||||||
}
|
|
||||||
let protoName: String
|
|
||||||
switch proto {
|
|
||||||
case 1: protoName = "ICMP"
|
|
||||||
case 6: protoName = "TCP"
|
|
||||||
case 17: protoName = "UDP"
|
|
||||||
default: protoName = "P\(proto)"
|
|
||||||
}
|
|
||||||
return "ip=ipv4 src=\(src) dst=\(dst) proto=\(protoName) \(ports)"
|
|
||||||
}
|
|
||||||
|
|
||||||
if versionNibble == 6, packet.count >= 40 {
|
|
||||||
let proto = packet[6]
|
|
||||||
func hex16(_ start: Int) -> String {
|
|
||||||
let value = (UInt16(packet[start]) << 8) | UInt16(packet[start + 1])
|
|
||||||
return String(format: "%x", value)
|
|
||||||
}
|
|
||||||
let src = stride(from: 8, to: 24, by: 2).map(hex16).joined(separator: ":")
|
|
||||||
let dst = stride(from: 24, to: 40, by: 2).map(hex16).joined(separator: ":")
|
|
||||||
let l4Offset = 40
|
|
||||||
let ports: String
|
|
||||||
if (proto == 6 || proto == 17) && packet.count >= l4Offset + 4 {
|
|
||||||
let srcPort = (UInt16(packet[l4Offset]) << 8) | UInt16(packet[l4Offset + 1])
|
|
||||||
let dstPort = (UInt16(packet[l4Offset + 2]) << 8) | UInt16(packet[l4Offset + 3])
|
|
||||||
ports = "sport=\(srcPort) dport=\(dstPort)"
|
|
||||||
} else {
|
|
||||||
ports = "sport=- dport=-"
|
|
||||||
}
|
|
||||||
let protoName: String
|
|
||||||
switch proto {
|
|
||||||
case 58: protoName = "ICMPv6"
|
|
||||||
case 6: protoName = "TCP"
|
|
||||||
case 17: protoName = "UDP"
|
|
||||||
default: protoName = "P\(proto)"
|
|
||||||
}
|
|
||||||
return "ip=ipv6 src=\(src) dst=\(dst) proto=\(protoName) \(ports)"
|
|
||||||
}
|
|
||||||
|
|
||||||
return "ip=v\(versionNibble) len=\(packet.count)"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
extension NEPacketTunnelFlow: OpenVPNAdapterPacketFlow {}
|
|
||||||
|
|
||||||
extension NEProviderStopReason {
|
extension NEProviderStopReason {
|
||||||
var amneziaDescription: String {
|
var amneziaDescription: String {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#if !MACOS_NE
|
#if !MACOS_NE && !TARGET_OS_TV
|
||||||
#include "QRCodeReaderBase.h"
|
#include "QRCodeReaderBase.h"
|
||||||
|
|
||||||
#import <UIKit/UIKit.h>
|
#import <UIKit/UIKit.h>
|
||||||
|
|||||||
@@ -1,178 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import StoreKit
|
|
||||||
|
|
||||||
@available(iOS 15.0, macOS 12.0, *)
|
|
||||||
@objcMembers
|
|
||||||
public class StoreKit2Helper: NSObject {
|
|
||||||
|
|
||||||
public static let shared = StoreKit2Helper()
|
|
||||||
private static let errorDomain = "StoreKit2Helper"
|
|
||||||
|
|
||||||
private struct EntitlementInfo {
|
|
||||||
let transactionId: UInt64
|
|
||||||
let originalTransactionId: UInt64
|
|
||||||
let productId: String
|
|
||||||
let purchaseDate: Date
|
|
||||||
|
|
||||||
var dictionary: NSDictionary {
|
|
||||||
[
|
|
||||||
"transactionId": String(transactionId),
|
|
||||||
"originalTransactionId": String(originalTransactionId),
|
|
||||||
"productId": productId
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func fetchCurrentEntitlements(completion: @escaping (Bool, [NSDictionary]?, NSError?) -> Void) {
|
|
||||||
Task { @MainActor in
|
|
||||||
do {
|
|
||||||
try await AppStore.sync()
|
|
||||||
|
|
||||||
var entitlements: [EntitlementInfo] = []
|
|
||||||
for await result in Transaction.currentEntitlements {
|
|
||||||
switch result {
|
|
||||||
case .verified(let transaction):
|
|
||||||
entitlements.append(EntitlementInfo(transactionId: transaction.id,
|
|
||||||
originalTransactionId: transaction.originalID,
|
|
||||||
productId: transaction.productID,
|
|
||||||
purchaseDate: transaction.purchaseDate))
|
|
||||||
case .unverified(_, let error):
|
|
||||||
print("[IAP][StoreKit2] Unverified transaction skipped: \(error.localizedDescription)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let sortedEntitlements = entitlements.sorted { lhs, rhs in
|
|
||||||
if lhs.purchaseDate != rhs.purchaseDate {
|
|
||||||
return lhs.purchaseDate > rhs.purchaseDate
|
|
||||||
}
|
|
||||||
return lhs.transactionId > rhs.transactionId
|
|
||||||
}.map { $0.dictionary }
|
|
||||||
completion(true, sortedEntitlements, nil)
|
|
||||||
} catch {
|
|
||||||
completion(false, nil, error as NSError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func purchaseProduct(productIdentifier: String, completion: @escaping (Bool, String?, String?, String?, NSError?) -> Void) {
|
|
||||||
Task {
|
|
||||||
do {
|
|
||||||
let products = try await Product.products(for: [productIdentifier])
|
|
||||||
guard let product = products.first else {
|
|
||||||
completePurchase(completion: completion, success: false, transactionId: nil, productId: nil, originalTransactionId: nil,
|
|
||||||
error: makeError(code: 0, description: "Product not found"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let result = try await product.purchase()
|
|
||||||
switch result {
|
|
||||||
case .success(let verification):
|
|
||||||
switch verification {
|
|
||||||
case .verified(let transaction):
|
|
||||||
await transaction.finish()
|
|
||||||
completePurchase(completion: completion, success: true, transactionId: String(transaction.id),
|
|
||||||
productId: transaction.productID, originalTransactionId: String(transaction.originalID), error: nil)
|
|
||||||
case .unverified(_, let error):
|
|
||||||
completePurchase(completion: completion, success: false, transactionId: nil, productId: nil, originalTransactionId: nil,
|
|
||||||
error: error as NSError)
|
|
||||||
}
|
|
||||||
case .userCancelled:
|
|
||||||
completePurchase(completion: completion, success: false, transactionId: nil, productId: nil, originalTransactionId: nil,
|
|
||||||
error: makeError(code: 1, description: "Purchase cancelled"))
|
|
||||||
case .pending:
|
|
||||||
completePurchase(completion: completion, success: false, transactionId: nil, productId: nil, originalTransactionId: nil,
|
|
||||||
error: makeError(code: 2, description: "Purchase pending"))
|
|
||||||
@unknown default:
|
|
||||||
completePurchase(completion: completion, success: false, transactionId: nil, productId: nil, originalTransactionId: nil,
|
|
||||||
error: makeError(code: 3, description: "Unknown purchase result"))
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
completePurchase(completion: completion, success: false, transactionId: nil, productId: nil, originalTransactionId: nil,
|
|
||||||
error: error as NSError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func storefrontCurrencyCode(for product: Product) -> String {
|
|
||||||
product.priceFormatStyle.locale.currencyCode ?? ""
|
|
||||||
}
|
|
||||||
|
|
||||||
private func subscriptionBillingMonths(_ period: Product.SubscriptionPeriod) -> Double {
|
|
||||||
let periodValue = Double(period.value)
|
|
||||||
switch period.unit {
|
|
||||||
case .day:
|
|
||||||
return periodValue / 30.0
|
|
||||||
case .week:
|
|
||||||
return periodValue * 7.0 / 30.0
|
|
||||||
case .month:
|
|
||||||
return periodValue
|
|
||||||
case .year:
|
|
||||||
return periodValue * 12.0
|
|
||||||
@unknown default:
|
|
||||||
return periodValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func fetchProducts(identifiers: Set<String>, completion: @escaping ([NSDictionary], [String], NSError?) -> Void) {
|
|
||||||
Task {
|
|
||||||
do {
|
|
||||||
let products = try await Product.products(for: identifiers)
|
|
||||||
let productDicts = products.map { product in productDictionary(for: product) }
|
|
||||||
let fetchedIds = Set(products.map { $0.id })
|
|
||||||
let invalidIdentifiers = identifiers.filter { !fetchedIds.contains($0) }
|
|
||||||
DispatchQueue.main.async { completion(productDicts, Array(invalidIdentifiers), nil) }
|
|
||||||
} catch {
|
|
||||||
DispatchQueue.main.async { completion([], Array(identifiers), error as NSError) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func makeError(code: Int, description: String) -> NSError {
|
|
||||||
NSError(domain: Self.errorDomain, code: code, userInfo: [NSLocalizedDescriptionKey: description])
|
|
||||||
}
|
|
||||||
|
|
||||||
private func completePurchase(completion: @escaping (Bool, String?, String?, String?, NSError?) -> Void,
|
|
||||||
success: Bool,
|
|
||||||
transactionId: String?,
|
|
||||||
productId: String?,
|
|
||||||
originalTransactionId: String?,
|
|
||||||
error: NSError?) {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
completion(success, transactionId, productId, originalTransactionId, error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func productDictionary(for product: Product) -> NSDictionary {
|
|
||||||
let currencyCode = storefrontCurrencyCode(for: product)
|
|
||||||
var productData: [String: Any] = [
|
|
||||||
"productId": product.id,
|
|
||||||
"title": product.displayName,
|
|
||||||
"description": product.description,
|
|
||||||
"price": "\(product.price)",
|
|
||||||
"displayPrice": product.displayPrice,
|
|
||||||
"currencyCode": currencyCode,
|
|
||||||
"priceAmount": NSDecimalNumber(decimal: product.price).doubleValue
|
|
||||||
]
|
|
||||||
if let subscription = product.subscription {
|
|
||||||
let billingMonths = subscriptionBillingMonths(subscription.subscriptionPeriod)
|
|
||||||
productData["subscriptionBillingMonths"] = billingMonths
|
|
||||||
if let perMonthPrice = displayPricePerMonth(for: product, billingMonths: billingMonths, currencyCode: currencyCode) {
|
|
||||||
productData["displayPricePerMonth"] = perMonthPrice
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return productData as NSDictionary
|
|
||||||
}
|
|
||||||
|
|
||||||
private func displayPricePerMonth(for product: Product, billingMonths: Double, currencyCode: String) -> String? {
|
|
||||||
if billingMonths <= 1e-6 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let perMonthPrice = product.price / Decimal(billingMonths)
|
|
||||||
let formatter = NumberFormatter()
|
|
||||||
formatter.numberStyle = .currency
|
|
||||||
formatter.locale = product.priceFormatStyle.locale
|
|
||||||
if !currencyCode.isEmpty {
|
|
||||||
formatter.currencyCode = currencyCode
|
|
||||||
}
|
|
||||||
return formatter.string(from: NSDecimalNumber(decimal: perMonthPrice))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,20 +4,27 @@
|
|||||||
|
|
||||||
#import "StoreKitController.h"
|
#import "StoreKitController.h"
|
||||||
#import <StoreKit/StoreKit.h>
|
#import <StoreKit/StoreKit.h>
|
||||||
#import <AmneziaVPN-Swift.h>
|
|
||||||
|
|
||||||
#include <QtCore/QDebug>
|
#include <QtCore/QDebug>
|
||||||
#include <QtCore/QString>
|
#include <QtCore/QString>
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
QString toQString(NSString *value)
|
|
||||||
{
|
|
||||||
return QString::fromUtf8((value ?: @"").UTF8String);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
API_AVAILABLE(ios(15.0), macos(12.0))
|
API_AVAILABLE(ios(15.0), macos(12.0))
|
||||||
|
@interface StoreKitController () <SKProductsRequestDelegate, SKPaymentTransactionObserver>
|
||||||
|
@property (nonatomic, copy) void (^purchaseCompletion)(BOOL success,
|
||||||
|
NSString *_Nullable transactionId,
|
||||||
|
NSString *_Nullable productId,
|
||||||
|
NSString *_Nullable originalTransactionId,
|
||||||
|
NSError *_Nullable error);
|
||||||
|
@property (nonatomic, copy) void (^restoreCompletion)(BOOL success,
|
||||||
|
NSArray<NSDictionary *> *_Nullable restoredTransactions,
|
||||||
|
NSError *_Nullable error);
|
||||||
|
@property (nonatomic, copy) void (^productsFetchCompletion)(NSArray<NSDictionary *> *products,
|
||||||
|
NSArray<NSString *> *invalidIdentifiers,
|
||||||
|
NSError *_Nullable error);
|
||||||
|
@property (nonatomic, strong) SKProductsRequest *productsRequest;
|
||||||
|
@property (nonatomic, strong) NSMutableArray<NSDictionary *> *restoredTransactions;
|
||||||
|
@end
|
||||||
|
|
||||||
@implementation StoreKitController
|
@implementation StoreKitController
|
||||||
|
|
||||||
+ (instancetype)sharedInstance
|
+ (instancetype)sharedInstance
|
||||||
@@ -35,9 +42,17 @@ API_AVAILABLE(ios(15.0), macos(12.0))
|
|||||||
- (instancetype)init API_AVAILABLE(ios(15.0), macos(12.0))
|
- (instancetype)init API_AVAILABLE(ios(15.0), macos(12.0))
|
||||||
{
|
{
|
||||||
self = [super init];
|
self = [super init];
|
||||||
|
if (self) {
|
||||||
|
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
|
||||||
|
}
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)dealloc
|
||||||
|
{
|
||||||
|
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
|
||||||
|
}
|
||||||
|
|
||||||
- (void)purchaseProduct:(NSString *)productIdentifier
|
- (void)purchaseProduct:(NSString *)productIdentifier
|
||||||
completion:(void (^)(BOOL success,
|
completion:(void (^)(BOOL success,
|
||||||
NSString *_Nullable transactionId,
|
NSString *_Nullable transactionId,
|
||||||
@@ -45,48 +60,41 @@ API_AVAILABLE(ios(15.0), macos(12.0))
|
|||||||
NSString *_Nullable originalTransactionId,
|
NSString *_Nullable originalTransactionId,
|
||||||
NSError *_Nullable error))completion API_AVAILABLE(ios(15.0), macos(12.0))
|
NSError *_Nullable error))completion API_AVAILABLE(ios(15.0), macos(12.0))
|
||||||
{
|
{
|
||||||
qInfo().noquote() << "[IAP][StoreKit2] Starting purchase for" << QString::fromUtf8(productIdentifier.UTF8String);
|
self.purchaseCompletion = completion;
|
||||||
[[StoreKit2Helper shared] purchaseProductWithProductIdentifier:productIdentifier
|
|
||||||
completion:^(BOOL success,
|
qInfo().noquote() << "[IAP][StoreKit] Starting purchase for" << QString::fromUtf8(productIdentifier.UTF8String);
|
||||||
NSString *transactionId,
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||||
NSString *productId,
|
[self performPurchaseAsync:productIdentifier];
|
||||||
NSString *originalTransactionId,
|
});
|
||||||
NSError *error) {
|
}
|
||||||
if (success) {
|
|
||||||
qInfo().noquote() << "[IAP][StoreKit2] Purchase success. transactionId =" << toQString(transactionId)
|
- (void)performPurchaseAsync:(NSString *)productIdentifier API_AVAILABLE(ios(15.0), macos(12.0))
|
||||||
<< "originalTransactionId =" << toQString(originalTransactionId) << "productId =" << toQString(productId);
|
{
|
||||||
} else if (error) {
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
qWarning().noquote() << "[IAP][StoreKit2] Purchase failed:" << toQString(error.localizedDescription);
|
@try {
|
||||||
|
SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithObject:productIdentifier]];
|
||||||
|
request.delegate = self;
|
||||||
|
[request start];
|
||||||
|
|
||||||
|
} @catch (NSException *exception) {
|
||||||
|
NSError *error = [NSError errorWithDomain:@"StoreKitController"
|
||||||
|
code:1
|
||||||
|
userInfo:@{ NSLocalizedDescriptionKey : exception.reason ?: @"Purchase failed" }];
|
||||||
|
if (self.purchaseCompletion) {
|
||||||
|
self.purchaseCompletion(NO, nil, nil, nil, error);
|
||||||
|
self.purchaseCompletion = nil;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (completion) {
|
});
|
||||||
completion(success, transactionId, productId, originalTransactionId, error);
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)restorePurchasesWithCompletion:(void (^)(BOOL success,
|
- (void)restorePurchasesWithCompletion:(void (^)(BOOL success,
|
||||||
NSArray<NSDictionary *> *_Nullable restoredTransactions,
|
NSArray<NSDictionary *> *_Nullable restoredTransactions,
|
||||||
NSError *_Nullable error))completion API_AVAILABLE(ios(15.0), macos(12.0))
|
NSError *_Nullable error))completion API_AVAILABLE(ios(15.0), macos(12.0))
|
||||||
{
|
{
|
||||||
[[StoreKit2Helper shared] fetchCurrentEntitlementsWithCompletion:^(BOOL success,
|
self.restoreCompletion = completion;
|
||||||
NSArray<NSDictionary *> *entitlements,
|
self.restoredTransactions = [NSMutableArray array];
|
||||||
NSError *error) {
|
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
|
||||||
if (success) {
|
|
||||||
qInfo().noquote() << "[IAP][StoreKit2] currentEntitlements returned"
|
|
||||||
<< (int)(entitlements ? entitlements.count : 0) << "active entitlements";
|
|
||||||
for (NSDictionary *entitlement in entitlements) {
|
|
||||||
qInfo().noquote() << "[IAP][StoreKit2] Active entitlement:"
|
|
||||||
<< "transactionId=" << toQString(entitlement[@"transactionId"])
|
|
||||||
<< "originalTransactionId=" << toQString(entitlement[@"originalTransactionId"])
|
|
||||||
<< "productId=" << toQString(entitlement[@"productId"]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
qWarning().noquote() << "[IAP][StoreKit2] fetchCurrentEntitlements failed:" << toQString(error.localizedDescription);
|
|
||||||
}
|
|
||||||
if (completion) {
|
|
||||||
completion(success, entitlements, error);
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)fetchProductsWithIdentifiers:(NSSet<NSString *> *)productIdentifiers
|
- (void)fetchProductsWithIdentifiers:(NSSet<NSString *> *)productIdentifiers
|
||||||
@@ -94,21 +102,163 @@ API_AVAILABLE(ios(15.0), macos(12.0))
|
|||||||
NSArray<NSString *> *invalidIdentifiers,
|
NSArray<NSString *> *invalidIdentifiers,
|
||||||
NSError *_Nullable error))completion API_AVAILABLE(ios(15.0), macos(12.0))
|
NSError *_Nullable error))completion API_AVAILABLE(ios(15.0), macos(12.0))
|
||||||
{
|
{
|
||||||
[[StoreKit2Helper shared] fetchProductsWithIdentifiers:productIdentifiers
|
self.productsFetchCompletion = completion;
|
||||||
completion:^(NSArray<NSDictionary *> *products,
|
self.productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIdentifiers];
|
||||||
NSArray<NSString *> *invalidIdentifiers,
|
self.productsRequest.delegate = self;
|
||||||
NSError *error) {
|
[self.productsRequest start];
|
||||||
if (!error) {
|
}
|
||||||
for (NSDictionary *productInfo in products) {
|
|
||||||
qInfo().noquote() << "[IAP][StoreKit2] Fetched product info" << toQString(productInfo[@"productId"])
|
#pragma mark - SKProductsRequestDelegate / SKRequestDelegate
|
||||||
<< "price=" << toQString(productInfo[@"price"])
|
|
||||||
<< "currency=" << toQString(productInfo[@"currencyCode"]);
|
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
|
||||||
|
{
|
||||||
|
if (self.purchaseCompletion) {
|
||||||
|
SKProduct *product = response.products.firstObject;
|
||||||
|
if (!product) {
|
||||||
|
NSError *error = [NSError errorWithDomain:@"StoreKitController"
|
||||||
|
code:0
|
||||||
|
userInfo:@{ NSLocalizedDescriptionKey : @"Product not found" }];
|
||||||
|
self.purchaseCompletion(NO, nil, nil, nil, error);
|
||||||
|
self.purchaseCompletion = nil;
|
||||||
|
self.productsRequest = nil;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSString *currencyCode = [product.priceLocale objectForKey:NSLocaleCurrencyCode] ?: @"";
|
||||||
|
NSString *priceString = [product.price stringValue] ?: @"";
|
||||||
|
qInfo().noquote() << "[IAP][StoreKit] Received product" << QString::fromUtf8(product.productIdentifier.UTF8String)
|
||||||
|
<< "price=" << QString::fromUtf8(priceString.UTF8String)
|
||||||
|
<< "currency=" << QString::fromUtf8(currencyCode.UTF8String);
|
||||||
|
SKPayment *payment = [SKPayment paymentWithProduct:product];
|
||||||
|
[[SKPaymentQueue defaultQueue] addPayment:payment];
|
||||||
|
self.productsRequest = nil;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.productsFetchCompletion) {
|
||||||
|
NSMutableArray<NSDictionary *> *productDicts = [NSMutableArray array];
|
||||||
|
for (SKProduct *p in response.products) {
|
||||||
|
NSDictionary *productDict = @{
|
||||||
|
@"productId": p.productIdentifier,
|
||||||
|
@"title": p.localizedTitle,
|
||||||
|
@"description": p.localizedDescription,
|
||||||
|
@"price": p.price.stringValue,
|
||||||
|
@"currencyCode": [p.priceLocale objectForKey:NSLocaleCurrencyCode] ?: @""
|
||||||
|
};
|
||||||
|
[productDicts addObject:productDict];
|
||||||
|
NSString *productCurrency = [p.priceLocale objectForKey:NSLocaleCurrencyCode] ?: @"";
|
||||||
|
NSString *productPrice = [p.price stringValue] ?: @"";
|
||||||
|
qInfo().noquote() << "[IAP][StoreKit] Fetched product info" << QString::fromUtf8(p.productIdentifier.UTF8String)
|
||||||
|
<< "price=" << QString::fromUtf8(productPrice.UTF8String)
|
||||||
|
<< "currency=" << QString::fromUtf8(productCurrency.UTF8String);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.productsFetchCompletion(productDicts, response.invalidProductIdentifiers, nil);
|
||||||
|
self.productsFetchCompletion = nil;
|
||||||
|
self.productsRequest = nil;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error
|
||||||
|
{
|
||||||
|
if (self.purchaseCompletion) {
|
||||||
|
self.purchaseCompletion(NO, nil, nil, nil, error);
|
||||||
|
self.purchaseCompletion = nil;
|
||||||
|
}
|
||||||
|
if (self.productsFetchCompletion) {
|
||||||
|
self.productsFetchCompletion(@[], @[], error);
|
||||||
|
self.productsFetchCompletion = nil;
|
||||||
|
}
|
||||||
|
self.productsRequest = nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - SKPaymentTransactionObserver
|
||||||
|
|
||||||
|
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions
|
||||||
|
{
|
||||||
|
for (SKPaymentTransaction *transaction in transactions) {
|
||||||
|
switch (transaction.transactionState) {
|
||||||
|
case SKPaymentTransactionStatePurchased: {
|
||||||
|
NSString *originalTransactionId = transaction.originalTransaction.transactionIdentifier ?: transaction.transactionIdentifier;
|
||||||
|
qInfo().noquote() << "[IAP][StoreKit] Transaction purchased" << QString::fromUtf8(transaction.transactionIdentifier.UTF8String)
|
||||||
|
<< "original=" << QString::fromUtf8((originalTransactionId ?: @"").UTF8String)
|
||||||
|
<< "product=" << QString::fromUtf8(transaction.payment.productIdentifier.UTF8String);
|
||||||
|
|
||||||
|
if (self.purchaseCompletion) {
|
||||||
|
self.purchaseCompletion(YES,
|
||||||
|
transaction.transactionIdentifier,
|
||||||
|
transaction.payment.productIdentifier,
|
||||||
|
originalTransactionId,
|
||||||
|
nil);
|
||||||
|
self.purchaseCompletion = nil;
|
||||||
}
|
}
|
||||||
|
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
if (completion) {
|
case SKPaymentTransactionStateFailed:
|
||||||
completion(products ?: @[], invalidIdentifiers ?: @[], error);
|
qInfo().noquote() << "[IAP][StoreKit] Transaction failed" << QString::fromUtf8(transaction.transactionIdentifier.UTF8String)
|
||||||
|
<< "product=" << QString::fromUtf8(transaction.payment.productIdentifier.UTF8String)
|
||||||
|
<< "error=" << QString::fromUtf8(transaction.error.localizedDescription.UTF8String);
|
||||||
|
if (self.purchaseCompletion) {
|
||||||
|
self.purchaseCompletion(NO,
|
||||||
|
transaction.transactionIdentifier,
|
||||||
|
transaction.payment.productIdentifier,
|
||||||
|
nil,
|
||||||
|
transaction.error);
|
||||||
|
self.purchaseCompletion = nil;
|
||||||
|
}
|
||||||
|
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
|
||||||
|
break;
|
||||||
|
case SKPaymentTransactionStateRestored: {
|
||||||
|
if (self.restoreCompletion) {
|
||||||
|
NSString *transactionId = transaction.transactionIdentifier ?: @"";
|
||||||
|
NSString *originalTransactionId = transaction.originalTransaction.transactionIdentifier ?: transactionId;
|
||||||
|
NSString *productId = transaction.payment.productIdentifier ?: @"";
|
||||||
|
|
||||||
|
qInfo().noquote() << "[IAP][StoreKit] Transaction restored"
|
||||||
|
<< QString::fromUtf8(transactionId.UTF8String)
|
||||||
|
<< "original="
|
||||||
|
<< QString::fromUtf8((originalTransactionId ?: @"").UTF8String)
|
||||||
|
<< "product="
|
||||||
|
<< QString::fromUtf8((productId ?: @"").UTF8String);
|
||||||
|
|
||||||
|
NSDictionary *info = @{
|
||||||
|
@"transactionId": transactionId,
|
||||||
|
@"originalTransactionId": originalTransactionId ?: @"",
|
||||||
|
@"productId": productId ?: @""
|
||||||
|
};
|
||||||
|
if (!self.restoredTransactions) {
|
||||||
|
self.restoredTransactions = [NSMutableArray array];
|
||||||
|
}
|
||||||
|
[self.restoredTransactions addObject:info];
|
||||||
|
}
|
||||||
|
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}];
|
case SKPaymentTransactionStatePurchasing:
|
||||||
|
case SKPaymentTransactionStateDeferred:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
|
||||||
|
{
|
||||||
|
if (self.restoreCompletion) {
|
||||||
|
NSArray<NSDictionary *> *transactions = [self.restoredTransactions copy];
|
||||||
|
self.restoreCompletion(YES, transactions, nil);
|
||||||
|
self.restoreCompletion = nil;
|
||||||
|
self.restoredTransactions = nil;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error
|
||||||
|
{
|
||||||
|
if (self.restoreCompletion) {
|
||||||
|
self.restoreCompletion(NO, nil, error);
|
||||||
|
self.restoreCompletion = nil;
|
||||||
|
self.restoredTransactions = nil;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -3,7 +3,5 @@ import Foundation
|
|||||||
struct XrayConfig: Decodable {
|
struct XrayConfig: Decodable {
|
||||||
let dns1: String?
|
let dns1: String?
|
||||||
let dns2: String?
|
let dns2: String?
|
||||||
let splitTunnelType: Int?
|
|
||||||
let splitTunnelSites: [String]?
|
|
||||||
let config: String
|
let config: String
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,9 +179,8 @@ bool IosController::initialize()
|
|||||||
[NETunnelProviderManager loadAllFromPreferencesWithCompletionHandler:^(NSArray<NETunnelProviderManager *> * _Nullable managers, NSError * _Nullable error) {
|
[NETunnelProviderManager loadAllFromPreferencesWithCompletionHandler:^(NSArray<NETunnelProviderManager *> * _Nullable managers, NSError * _Nullable error) {
|
||||||
@try {
|
@try {
|
||||||
if (error) {
|
if (error) {
|
||||||
qWarning() << "IosController::initialize : loadAllFromPreferences failed:"
|
qDebug() << "IosController::initialize : Error:" << [error.localizedDescription UTF8String];
|
||||||
<< [error.localizedDescription UTF8String]
|
emit connectionStateChanged(Vpn::ConnectionState::Error);
|
||||||
<< "domain:" << [error.domain UTF8String] << "code:" << error.code;
|
|
||||||
ok = false;
|
ok = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -218,13 +217,16 @@ bool IosController::connectVpn(amnezia::Proto proto, const QJsonObject& configur
|
|||||||
m_rawConfig = configuration;
|
m_rawConfig = configuration;
|
||||||
m_serverAddress = configuration.value(config_key::hostName).toString().toNSString();
|
m_serverAddress = configuration.value(config_key::hostName).toString().toNSString();
|
||||||
|
|
||||||
const QString serverDescription = configuration.value(config_key::description).toString().trimmed();
|
|
||||||
QString tunnelName;
|
QString tunnelName;
|
||||||
if (serverDescription.isEmpty()) {
|
if (configuration.value(config_key::description).toString().isEmpty()) {
|
||||||
tunnelName = ProtocolProps::protoToString(proto);
|
|
||||||
} else {
|
|
||||||
tunnelName = QString("%1 %2")
|
tunnelName = QString("%1 %2")
|
||||||
.arg(serverDescription)
|
.arg(configuration.value(config_key::hostName).toString())
|
||||||
|
.arg(ProtocolProps::protoToString(proto));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
tunnelName = QString("%1 (%2) %3")
|
||||||
|
.arg(configuration.value(config_key::description).toString())
|
||||||
|
.arg(configuration.value(config_key::hostName).toString())
|
||||||
.arg(ProtocolProps::protoToString(proto));
|
.arg(ProtocolProps::protoToString(proto));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,14 +397,8 @@ void IosController::vpnStatusDidChange(void *pNotification)
|
|||||||
{
|
{
|
||||||
NETunnelProviderSession *session = (NETunnelProviderSession *)pNotification;
|
NETunnelProviderSession *session = (NETunnelProviderSession *)pNotification;
|
||||||
|
|
||||||
if (!session) {
|
if (session /* && session == TunnelManager.session */ ) {
|
||||||
return;
|
qDebug() << "IosController::vpnStatusDidChange" << iosStatusToState(session.status) << session;
|
||||||
}
|
|
||||||
if (!m_currentTunnel || (NETunnelProviderSession *)m_currentTunnel.connection != session) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
qDebug() << "IosController::vpnStatusDidChange" << iosStatusToState(session.status) << session;
|
|
||||||
|
|
||||||
if (session.status == NEVPNStatusDisconnected) {
|
if (session.status == NEVPNStatusDisconnected) {
|
||||||
if (@available(iOS 16.0, *)) {
|
if (@available(iOS 16.0, *)) {
|
||||||
@@ -516,6 +512,7 @@ void IosController::vpnStatusDidChange(void *pNotification)
|
|||||||
m_statusRequestInFlight = false;
|
m_statusRequestInFlight = false;
|
||||||
}
|
}
|
||||||
emitConnectionStateIfChanged(nextState);
|
emitConnectionStateIfChanged(nextState);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void IosController::vpnConfigurationDidChange(void *pNotification)
|
void IosController::vpnConfigurationDidChange(void *pNotification)
|
||||||
@@ -549,16 +546,6 @@ bool IosController::setupOpenVPN()
|
|||||||
|
|
||||||
QJsonDocument openVPNConfigDoc(openVPNConfig);
|
QJsonDocument openVPNConfigDoc(openVPNConfig);
|
||||||
QString openVPNConfigStr(openVPNConfigDoc.toJson(QJsonDocument::Compact));
|
QString openVPNConfigStr(openVPNConfigDoc.toJson(QJsonDocument::Compact));
|
||||||
QString openVPNConfigPreview = openVPNConfigStr.left(512);
|
|
||||||
QString ovpnPreview = ovpnConfig.left(512);
|
|
||||||
|
|
||||||
qDebug().noquote() << "IosController::setupOpenVPN payload"
|
|
||||||
<< "jsonBytes=" << openVPNConfigStr.toUtf8().size()
|
|
||||||
<< "ovpnChars=" << ovpnConfig.size()
|
|
||||||
<< "splitTunnelType=" << m_rawConfig[config_key::splitTunnelType].toInt()
|
|
||||||
<< "splitTunnelSites=" << splitTunnelSites;
|
|
||||||
qDebug().noquote() << "IosController::setupOpenVPN payload jsonPreview=" << openVPNConfigPreview;
|
|
||||||
qDebug().noquote() << "IosController::setupOpenVPN payload ovpnPreview=" << ovpnPreview;
|
|
||||||
|
|
||||||
return startOpenVPN(openVPNConfigStr);
|
return startOpenVPN(openVPNConfigStr);
|
||||||
}
|
}
|
||||||
@@ -697,15 +684,6 @@ bool IosController::setupXray()
|
|||||||
QJsonObject finalConfig;
|
QJsonObject finalConfig;
|
||||||
finalConfig.insert(config_key::dns1, m_rawConfig[config_key::dns1].toString());
|
finalConfig.insert(config_key::dns1, m_rawConfig[config_key::dns1].toString());
|
||||||
finalConfig.insert(config_key::dns2, m_rawConfig[config_key::dns2].toString());
|
finalConfig.insert(config_key::dns2, m_rawConfig[config_key::dns2].toString());
|
||||||
finalConfig.insert(config_key::splitTunnelType, m_rawConfig[config_key::splitTunnelType]);
|
|
||||||
|
|
||||||
QJsonArray splitTunnelSites = m_rawConfig[config_key::splitTunnelSites].toArray();
|
|
||||||
|
|
||||||
for(int index = 0; index < splitTunnelSites.count(); index++) {
|
|
||||||
splitTunnelSites[index] = splitTunnelSites[index].toString().remove(" ");
|
|
||||||
}
|
|
||||||
|
|
||||||
finalConfig.insert(config_key::splitTunnelSites, splitTunnelSites);
|
|
||||||
finalConfig.insert(config_key::config, xrayConfigStr);
|
finalConfig.insert(config_key::config, xrayConfigStr);
|
||||||
|
|
||||||
QJsonDocument finalConfigDoc(finalConfig);
|
QJsonDocument finalConfigDoc(finalConfig);
|
||||||
@@ -807,59 +785,11 @@ bool IosController::startOpenVPN(const QString &config)
|
|||||||
|
|
||||||
NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init];
|
NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init];
|
||||||
tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID];
|
tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID];
|
||||||
QByteArray configUtf8 = config.toUtf8();
|
tunnelProtocol.providerConfiguration = @{@"ovpn": [[NSString stringWithUTF8String:config.toStdString().c_str()] dataUsingEncoding:NSUTF8StringEncoding]};
|
||||||
NSData *ovpnConfigData = [NSData dataWithBytes:configUtf8.constData() length:configUtf8.size()];
|
|
||||||
tunnelProtocol.providerConfiguration = @{@"ovpn": ovpnConfigData};
|
|
||||||
tunnelProtocol.serverAddress = m_serverAddress;
|
tunnelProtocol.serverAddress = m_serverAddress;
|
||||||
if (@available(iOS 14.0, macOS 11.0, *)) {
|
|
||||||
int splitTunnelType = 0;
|
|
||||||
QJsonParseError parseError;
|
|
||||||
QJsonDocument doc = QJsonDocument::fromJson(config.toUtf8(), &parseError);
|
|
||||||
if (parseError.error == QJsonParseError::NoError && doc.isObject()) {
|
|
||||||
QJsonObject obj = doc.object();
|
|
||||||
splitTunnelType = obj.value(config_key::splitTunnelType).toInt(0);
|
|
||||||
}
|
|
||||||
#if defined(MACOS_NE)
|
|
||||||
// On macOS NE use route-based full tunnel. includeAllNetworks enables
|
|
||||||
// policy-based drop-all mode and causes enforceRoutes to be ignored.
|
|
||||||
tunnelProtocol.includeAllNetworks = NO;
|
|
||||||
if (splitTunnelType == 0) {
|
|
||||||
tunnelProtocol.enforceRoutes = YES;
|
|
||||||
if (@available(iOS 14.2, macOS 11.0, *)) {
|
|
||||||
tunnelProtocol.excludeLocalNetworks = YES;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
tunnelProtocol.includeAllNetworks = (splitTunnelType == 0);
|
|
||||||
if (@available(iOS 14.2, macOS 11.0, *)) {
|
|
||||||
// Keep existing iOS behavior.
|
|
||||||
if (splitTunnelType == 0) {
|
|
||||||
tunnelProtocol.excludeLocalNetworks = NO;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
m_currentTunnel.protocolConfiguration = tunnelProtocol;
|
m_currentTunnel.protocolConfiguration = tunnelProtocol;
|
||||||
|
|
||||||
NETunnelProviderProtocol *appliedProtocol = (NETunnelProviderProtocol *)m_currentTunnel.protocolConfiguration;
|
|
||||||
NSData *ovpnPayload = appliedProtocol.providerConfiguration[@"ovpn"];
|
|
||||||
NSString *payloadPreview = @"";
|
|
||||||
if (ovpnPayload != nil) {
|
|
||||||
NSString *decodedPayload = [[NSString alloc] initWithData:ovpnPayload encoding:NSUTF8StringEncoding];
|
|
||||||
if (decodedPayload != nil) {
|
|
||||||
payloadPreview = [decodedPayload substringToIndex:MIN((NSUInteger)512, decodedPayload.length)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
qDebug().noquote() << "IosController::startOpenVPN protocolConfiguration"
|
|
||||||
<< "bundleId=" << QString::fromNSString(appliedProtocol.providerBundleIdentifier ?: @"")
|
|
||||||
<< "serverAddress=" << QString::fromNSString(appliedProtocol.serverAddress ?: @"")
|
|
||||||
<< "providerKeys=" << QString::fromNSString([[appliedProtocol.providerConfiguration.allKeys description] copy])
|
|
||||||
<< "ovpnBytes=" << (ovpnPayload != nil ? ovpnPayload.length : 0);
|
|
||||||
qDebug().noquote() << "IosController::startOpenVPN protocolConfiguration payloadPreview="
|
|
||||||
<< QString::fromNSString(payloadPreview);
|
|
||||||
|
|
||||||
startTunnel();
|
startTunnel();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -869,9 +799,7 @@ bool IosController::startWireGuard(const QString &config)
|
|||||||
|
|
||||||
NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init];
|
NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init];
|
||||||
tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID];
|
tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID];
|
||||||
QByteArray configUtf8 = config.toUtf8();
|
tunnelProtocol.providerConfiguration = @{@"wireguard": [[NSString stringWithUTF8String:config.toStdString().c_str()] dataUsingEncoding:NSUTF8StringEncoding]};
|
||||||
NSData *wgConfigData = [NSData dataWithBytes:configUtf8.constData() length:configUtf8.size()];
|
|
||||||
tunnelProtocol.providerConfiguration = @{@"wireguard": wgConfigData};
|
|
||||||
tunnelProtocol.serverAddress = m_serverAddress;
|
tunnelProtocol.serverAddress = m_serverAddress;
|
||||||
|
|
||||||
m_currentTunnel.protocolConfiguration = tunnelProtocol;
|
m_currentTunnel.protocolConfiguration = tunnelProtocol;
|
||||||
@@ -885,9 +813,7 @@ bool IosController::startXray(const QString &config)
|
|||||||
|
|
||||||
NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init];
|
NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init];
|
||||||
tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID];
|
tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID];
|
||||||
QByteArray configUtf8 = config.toUtf8();
|
tunnelProtocol.providerConfiguration = @{@"xray": [[NSString stringWithUTF8String:config.toStdString().c_str()] dataUsingEncoding:NSUTF8StringEncoding]};
|
||||||
NSData *xrayConfigData = [NSData dataWithBytes:configUtf8.constData() length:configUtf8.size()];
|
|
||||||
tunnelProtocol.providerConfiguration = @{@"xray": xrayConfigData};
|
|
||||||
tunnelProtocol.serverAddress = m_serverAddress;
|
tunnelProtocol.serverAddress = m_serverAddress;
|
||||||
|
|
||||||
m_currentTunnel.protocolConfiguration = tunnelProtocol;
|
m_currentTunnel.protocolConfiguration = tunnelProtocol;
|
||||||
@@ -909,49 +835,39 @@ void IosController::startTunnel()
|
|||||||
m_rxBytes = 0;
|
m_rxBytes = 0;
|
||||||
m_txBytes = 0;
|
m_txBytes = 0;
|
||||||
|
|
||||||
NETunnelProviderManager *tunnel = m_currentTunnel;
|
[m_currentTunnel setEnabled:YES];
|
||||||
[tunnel setEnabled:YES];
|
|
||||||
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
[m_currentTunnel saveToPreferencesWithCompletionHandler:^(NSError *saveError) {
|
||||||
[tunnel saveToPreferencesWithCompletionHandler:^(NSError *saveError) {
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
|
||||||
if (saveError) {
|
|
||||||
qDebug().nospace() << "IosController::startTunnel" << protocolName << ": Connect " << protocolName
|
|
||||||
<< " Tunnel Save Error" << saveError.localizedDescription.UTF8String << " domain:"
|
|
||||||
<< saveError.domain.UTF8String << " code:" << saveError.code;
|
|
||||||
emit connectionStateChanged(Vpn::ConnectionState::Error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
[tunnel loadFromPreferencesWithCompletionHandler:^(NSError *loadError) {
|
if (saveError) {
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
qDebug().nospace() << "IosController::startTunnel" << protocolName << ": Connect " << protocolName << " Tunnel Save Error" << saveError.localizedDescription.UTF8String;
|
||||||
if (loadError) {
|
emit connectionStateChanged(Vpn::ConnectionState::Error);
|
||||||
qDebug().nospace() << "IosController::startTunnel :" << tunnel.localizedDescription << protocolName
|
return;
|
||||||
<< ": Connect " << protocolName << " Tunnel Load Error"
|
}
|
||||||
<< loadError.localizedDescription.UTF8String;
|
|
||||||
emit connectionStateChanged(Vpn::ConnectionState::Error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSError *startError = nil;
|
[m_currentTunnel loadFromPreferencesWithCompletionHandler:^(NSError *loadError) {
|
||||||
qDebug() << iosStatusToState(tunnel.connection.status);
|
if (loadError) {
|
||||||
|
qDebug().nospace() << "IosController::startTunnel :" << m_currentTunnel.localizedDescription << protocolName << ": Connect " << protocolName << " Tunnel Load Error" << loadError.localizedDescription.UTF8String;
|
||||||
|
emit connectionStateChanged(Vpn::ConnectionState::Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
BOOL started = [tunnel.connection startVPNTunnelWithOptions:nil andReturnError:&startError];
|
NSError *startError = nil;
|
||||||
|
qDebug() << iosStatusToState(m_currentTunnel.connection.status);
|
||||||
|
|
||||||
if (!started || startError) {
|
BOOL started = [m_currentTunnel.connection startVPNTunnelWithOptions:nil andReturnError:&startError];
|
||||||
qDebug().nospace() << "IosController::startTunnel :" << tunnel.localizedDescription << protocolName
|
|
||||||
<< " : Connect " << protocolName << " Tunnel Start Error"
|
if (!started || startError) {
|
||||||
<< (startError ? startError.localizedDescription.UTF8String : "");
|
qDebug().nospace() << "IosController::startTunnel :" << m_currentTunnel.localizedDescription << protocolName << " : Connect " << protocolName << " Tunnel Start Error"
|
||||||
emit connectionStateChanged(Vpn::ConnectionState::Error);
|
<< (startError ? startError.localizedDescription.UTF8String : "");
|
||||||
} else {
|
emit connectionStateChanged(Vpn::ConnectionState::Error);
|
||||||
qDebug().nospace() << "IosController::startTunnel :" << tunnel.localizedDescription << protocolName
|
} else {
|
||||||
<< " : Starting the tunnel succeeded";
|
qDebug().nospace() << "IosController::startTunnel :" << m_currentTunnel.localizedDescription << protocolName << " : Starting the tunnel succeeded";
|
||||||
}
|
}
|
||||||
});
|
}];
|
||||||
}];
|
});
|
||||||
});
|
}];
|
||||||
}];
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool IosController::isOurManager(NETunnelProviderManager* manager) {
|
bool IosController::isOurManager(NETunnelProviderManager* manager) {
|
||||||
@@ -1043,6 +959,10 @@ void IosController::sendVpnExtensionMessage(NSDictionary* message, std::function
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool IosController::shareText(const QStringList& filesToSend) {
|
bool IosController::shareText(const QStringList& filesToSend) {
|
||||||
|
#if defined(Q_OS_TVOS)
|
||||||
|
Q_UNUSED(filesToSend)
|
||||||
|
return false;
|
||||||
|
#else
|
||||||
NSMutableArray *sharingItems = [NSMutableArray new];
|
NSMutableArray *sharingItems = [NSMutableArray new];
|
||||||
|
|
||||||
for (int i = 0; i < filesToSend.size(); i++) {
|
for (int i = 0; i < filesToSend.size(); i++) {
|
||||||
@@ -1051,7 +971,7 @@ bool IosController::shareText(const QStringList& filesToSend) {
|
|||||||
}
|
}
|
||||||
#if !MACOS_NE
|
#if !MACOS_NE
|
||||||
UIViewController *qtController = getViewController();
|
UIViewController *qtController = getViewController();
|
||||||
if (!qtController) return;
|
if (!qtController) return false;
|
||||||
|
|
||||||
UIActivityViewController *activityController = [[UIActivityViewController alloc] initWithActivityItems:sharingItems applicationActivities:nil];
|
UIActivityViewController *activityController = [[UIActivityViewController alloc] initWithActivityItems:sharingItems applicationActivities:nil];
|
||||||
#endif
|
#endif
|
||||||
@@ -1075,23 +995,25 @@ bool IosController::shareText(const QStringList& filesToSend) {
|
|||||||
wait.exec();
|
wait.exec();
|
||||||
|
|
||||||
return isAccepted;
|
return isAccepted;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
QString IosController::openFile() {
|
QString IosController::openFile() {
|
||||||
#if !MACOS_NE
|
#if defined(Q_OS_TVOS)
|
||||||
|
return QString();
|
||||||
|
#elif !MACOS_NE
|
||||||
UIDocumentPickerViewController *documentPicker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[@"public.item"] inMode:UIDocumentPickerModeOpen];
|
UIDocumentPickerViewController *documentPicker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[@"public.item"] inMode:UIDocumentPickerModeOpen];
|
||||||
|
|
||||||
DocumentPickerDelegate *documentPickerDelegate = [[DocumentPickerDelegate alloc] init];
|
DocumentPickerDelegate *documentPickerDelegate = [[DocumentPickerDelegate alloc] init];
|
||||||
documentPicker.delegate = documentPickerDelegate;
|
documentPicker.delegate = documentPickerDelegate;
|
||||||
|
|
||||||
UIViewController *qtController = getViewController();
|
UIViewController *qtController = getViewController();
|
||||||
if (!qtController) return;
|
if (!qtController) return QString();
|
||||||
|
|
||||||
[qtController presentViewController:documentPicker animated:YES completion:nil];
|
[qtController presentViewController:documentPicker animated:YES completion:nil];
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
__block QString filePath;
|
__block QString filePath;
|
||||||
#if !MACOS_NE
|
#if !MACOS_NE && !defined(Q_OS_TVOS)
|
||||||
documentPickerDelegate.documentPickerClosedCallback = ^(NSString *path) {
|
documentPickerDelegate.documentPickerClosedCallback = ^(NSString *path) {
|
||||||
if (path) {
|
if (path) {
|
||||||
filePath = QString::fromUtf8(path.UTF8String);
|
filePath = QString::fromUtf8(path.UTF8String);
|
||||||
@@ -1206,26 +1128,14 @@ void IosController::fetchProducts(const QStringList &productIds,
|
|||||||
NSArray<NSString *> * _Nonnull invalidIdentifiers,
|
NSArray<NSString *> * _Nonnull invalidIdentifiers,
|
||||||
NSError * _Nullable error) {
|
NSError * _Nullable error) {
|
||||||
QList<QVariantMap> outProducts;
|
QList<QVariantMap> outProducts;
|
||||||
for (NSDictionary *productInfo in products) {
|
for (NSDictionary *p in products) {
|
||||||
QVariantMap productData;
|
QVariantMap m;
|
||||||
productData["productId"] = QString::fromUtf8([productInfo[@"productId"] UTF8String]);
|
m["productId"] = QString::fromUtf8([p[@"productId"] UTF8String]);
|
||||||
productData["title"] = QString::fromUtf8([productInfo[@"title"] UTF8String]);
|
m["title"] = QString::fromUtf8([p[@"title"] UTF8String]);
|
||||||
productData["description"] = QString::fromUtf8([productInfo[@"description"] UTF8String]);
|
m["description"] = QString::fromUtf8([p[@"description"] UTF8String]);
|
||||||
productData["price"] = QString::fromUtf8([productInfo[@"price"] UTF8String]);
|
m["price"] = QString::fromUtf8([p[@"price"] UTF8String]);
|
||||||
if (productInfo[@"displayPrice"]) {
|
m["currencyCode"] = QString::fromUtf8([p[@"currencyCode"] UTF8String]);
|
||||||
productData["displayPrice"] = QString::fromUtf8([productInfo[@"displayPrice"] UTF8String]);
|
outProducts.push_back(m);
|
||||||
}
|
|
||||||
productData["currencyCode"] = QString::fromUtf8([productInfo[@"currencyCode"] UTF8String]);
|
|
||||||
if (productInfo[@"priceAmount"]) {
|
|
||||||
productData["priceAmount"] = [productInfo[@"priceAmount"] doubleValue];
|
|
||||||
}
|
|
||||||
if (productInfo[@"subscriptionBillingMonths"]) {
|
|
||||||
productData["subscriptionBillingMonths"] = [productInfo[@"subscriptionBillingMonths"] doubleValue];
|
|
||||||
}
|
|
||||||
if (productInfo[@"displayPricePerMonth"]) {
|
|
||||||
productData["displayPricePerMonth"] = QString::fromUtf8([productInfo[@"displayPricePerMonth"] UTF8String]);
|
|
||||||
}
|
|
||||||
outProducts.push_back(productData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringList invalid;
|
QStringList invalid;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#import <NetworkExtension/NetworkExtension.h>
|
#import <NetworkExtension/NetworkExtension.h>
|
||||||
#import <NetworkExtension/NETunnelProviderSession.h>
|
#import <NetworkExtension/NETunnelProviderSession.h>
|
||||||
#import <Foundation/Foundation.h>
|
#import <Foundation/Foundation.h>
|
||||||
|
#include <TargetConditionals.h>
|
||||||
|
|
||||||
#if !MACOS_NE
|
#if !MACOS_NE
|
||||||
#include <UIKit/UIKit.h>
|
#include <UIKit/UIKit.h>
|
||||||
@@ -21,7 +22,7 @@ class IosController;
|
|||||||
@end
|
@end
|
||||||
|
|
||||||
typedef void (^DocumentPickerClosedCallback)(NSString *path);
|
typedef void (^DocumentPickerClosedCallback)(NSString *path);
|
||||||
#if !MACOS_NE
|
#if !MACOS_NE && !TARGET_OS_TV
|
||||||
@interface DocumentPickerDelegate : NSObject <UIDocumentPickerDelegate>
|
@interface DocumentPickerDelegate : NSObject <UIDocumentPickerDelegate>
|
||||||
|
|
||||||
@property (nonatomic, copy) DocumentPickerClosedCallback documentPickerClosedCallback;
|
@property (nonatomic, copy) DocumentPickerClosedCallback documentPickerClosedCallback;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
#if !MACOS_NE
|
#if !MACOS_NE && !TARGET_OS_TV
|
||||||
@implementation DocumentPickerDelegate
|
@implementation DocumentPickerDelegate
|
||||||
|
|
||||||
- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls {
|
- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls {
|
||||||
|
|||||||
@@ -7,6 +7,24 @@
|
|||||||
#import <UserNotifications/UserNotifications.h>
|
#import <UserNotifications/UserNotifications.h>
|
||||||
#import <Foundation/Foundation.h>
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
#if defined(Q_OS_TVOS)
|
||||||
|
|
||||||
|
IOSNotificationHandler::IOSNotificationHandler(QObject* parent) : NotificationHandler(parent) {}
|
||||||
|
|
||||||
|
IOSNotificationHandler::~IOSNotificationHandler() {}
|
||||||
|
|
||||||
|
void IOSNotificationHandler::notify(NotificationHandler::Message type,
|
||||||
|
const QString& title,
|
||||||
|
const QString& message,
|
||||||
|
int timerMsec) {
|
||||||
|
Q_UNUSED(type)
|
||||||
|
Q_UNUSED(title)
|
||||||
|
Q_UNUSED(message)
|
||||||
|
Q_UNUSED(timerMsec)
|
||||||
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
#if !MACOS_NE
|
#if !MACOS_NE
|
||||||
#import <UIKit/UIKit.h>
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
@@ -172,3 +190,5 @@ void IOSNotificationHandler::notify(NotificationHandler::Message type, const QSt
|
|||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#endif // Q_OS_TVOS
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/*
|
||||||
|
* tvOS does not export these iOS runtime helpers used by Go cgo archives.
|
||||||
|
* WireGuardKitGo references them indirectly; provide no-op stubs for tvOS.
|
||||||
|
*/
|
||||||
|
void darwin_arm_init_mach_exception_handler(void) {}
|
||||||
|
void darwin_arm_init_thread_exception_port(void) {}
|
||||||
@@ -164,13 +164,8 @@ bool LinuxRouteMonitor::rtmSendRoute(int action, int flags, int type,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (rtm->rtm_type == RTN_THROW) {
|
if (rtm->rtm_type == RTN_THROW) {
|
||||||
QString gateway = NetworkUtilities::getGatewayAndIface().first;
|
|
||||||
if (gateway.isEmpty()) {
|
|
||||||
logger.warning() << "No default gateway available, skipping exclusion route";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
struct in_addr ip4;
|
struct in_addr ip4;
|
||||||
inet_pton(AF_INET, gateway.toUtf8(), &ip4);
|
inet_pton(AF_INET, NetworkUtilities::getGatewayAndIface().first.toUtf8(), &ip4);
|
||||||
nlmsg_append_attr(nlmsg, sizeof(buf), RTA_GATEWAY, &ip4, sizeof(ip4));
|
nlmsg_append_attr(nlmsg, sizeof(buf), RTA_GATEWAY, &ip4, sizeof(ip4));
|
||||||
nlmsg_append_attr32(nlmsg, sizeof(buf), RTA_PRIORITY, 0);
|
nlmsg_append_attr32(nlmsg, sizeof(buf), RTA_PRIORITY, 0);
|
||||||
rtm->rtm_type = RTN_UNICAST;
|
rtm->rtm_type = RTN_UNICAST;
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ bool WireguardUtilsLinux::addInterface(const InterfaceConfig& config) {
|
|||||||
|
|
||||||
QDir appPath(QCoreApplication::applicationDirPath());
|
QDir appPath(QCoreApplication::applicationDirPath());
|
||||||
QStringList wgArgs = {"-f", "amn0"};
|
QStringList wgArgs = {"-f", "amn0"};
|
||||||
m_tunnel.start(appPath.filePath("../../client/bin/wireguard-go"), wgArgs);
|
m_tunnel.start(appPath.filePath("amneziawg-go"), wgArgs);
|
||||||
if (!m_tunnel.waitForStarted(WG_TUN_PROC_TIMEOUT)) {
|
if (!m_tunnel.waitForStarted(WG_TUN_PROC_TIMEOUT)) {
|
||||||
logger.error() << "Unable to start tunnel process due to timeout";
|
logger.error() << "Unable to start tunnel process due to timeout";
|
||||||
m_tunnel.kill();
|
m_tunnel.kill();
|
||||||
|
|||||||
@@ -44,9 +44,6 @@ void LinuxNetworkWatcher::initialize() {
|
|||||||
connect(m_worker, &LinuxNetworkWatcherWorker::wakeup, this,
|
connect(m_worker, &LinuxNetworkWatcherWorker::wakeup, this,
|
||||||
&NetworkWatcherImpl::wakeup);
|
&NetworkWatcherImpl::wakeup);
|
||||||
|
|
||||||
connect(m_worker, &LinuxNetworkWatcherWorker::networkChanged, this,
|
|
||||||
[this]() { emit networkChanged(""); });
|
|
||||||
|
|
||||||
// Let's wait a few seconds to allow the UI to be fully loaded and shown.
|
// Let's wait a few seconds to allow the UI to be fully loaded and shown.
|
||||||
// This is not strictly needed, but it's better for user experience because
|
// This is not strictly needed, but it's better for user experience because
|
||||||
// it makes the UI faster to appear, plus it gives a bit of delay between the
|
// it makes the UI faster to appear, plus it gives a bit of delay between the
|
||||||
|
|||||||
@@ -37,7 +37,6 @@
|
|||||||
enum NMState {
|
enum NMState {
|
||||||
NM_STATE_UNKNOWN = 0,
|
NM_STATE_UNKNOWN = 0,
|
||||||
NM_STATE_ASLEEP = 10,
|
NM_STATE_ASLEEP = 10,
|
||||||
NM_STATE_DISABLED = 10,
|
|
||||||
NM_STATE_DISCONNECTED = 20,
|
NM_STATE_DISCONNECTED = 20,
|
||||||
NM_STATE_DISCONNECTING = 30,
|
NM_STATE_DISCONNECTING = 30,
|
||||||
NM_STATE_CONNECTING = 40,
|
NM_STATE_CONNECTING = 40,
|
||||||
@@ -200,11 +199,10 @@ void LinuxNetworkWatcherWorker::checkDevices() {
|
|||||||
|
|
||||||
void LinuxNetworkWatcherWorker::NMStateChanged(quint32 state)
|
void LinuxNetworkWatcherWorker::NMStateChanged(quint32 state)
|
||||||
{
|
{
|
||||||
logger.debug() << "NMStateChanged " << state;
|
if (state == NM_STATE_ASLEEP) {
|
||||||
|
emit wakeup();
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug() << "NMStateChanged " << state;
|
||||||
|
}
|
||||||
|
|
||||||
if (state == NM_STATE_ASLEEP || state == NM_STATE_DISABLED) {
|
|
||||||
emit wakeup();
|
|
||||||
} else if (state == NM_STATE_CONNECTED_GLOBAL) {
|
|
||||||
emit networkChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -24,7 +24,6 @@ class LinuxNetworkWatcherWorker final : public QObject {
|
|||||||
signals:
|
signals:
|
||||||
void unsecuredNetwork(const QString& networkName, const QString& networkId);
|
void unsecuredNetwork(const QString& networkName, const QString& networkId);
|
||||||
void wakeup();
|
void wakeup();
|
||||||
void networkChanged();
|
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void initialize();
|
void initialize();
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ bool WireguardUtilsMacos::addInterface(const InterfaceConfig& config) {
|
|||||||
|
|
||||||
QDir appPath(QCoreApplication::applicationDirPath());
|
QDir appPath(QCoreApplication::applicationDirPath());
|
||||||
QStringList wgArgs = {"-f", "utun"};
|
QStringList wgArgs = {"-f", "utun"};
|
||||||
m_tunnel.start(appPath.filePath("wireguard-go"), wgArgs);
|
m_tunnel.start(appPath.filePath("amneziawg-go"), wgArgs);
|
||||||
if (!m_tunnel.waitForStarted(WG_TUN_PROC_TIMEOUT)) {
|
if (!m_tunnel.waitForStarted(WG_TUN_PROC_TIMEOUT)) {
|
||||||
logger.error() << "Unable to start tunnel process due to timeout";
|
logger.error() << "Unable to start tunnel process due to timeout";
|
||||||
m_tunnel.kill();
|
m_tunnel.kill();
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ namespace amnezia
|
|||||||
|
|
||||||
constexpr char defaultPort[] = "51820";
|
constexpr char defaultPort[] = "51820";
|
||||||
|
|
||||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(MACOS_NE)
|
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(Q_OS_TVOS) || defined(MACOS_NE)
|
||||||
constexpr char defaultMtu[] = "1280";
|
constexpr char defaultMtu[] = "1280";
|
||||||
#else
|
#else
|
||||||
constexpr char defaultMtu[] = "1376";
|
constexpr char defaultMtu[] = "1376";
|
||||||
@@ -210,7 +210,7 @@ namespace amnezia
|
|||||||
namespace awg
|
namespace awg
|
||||||
{
|
{
|
||||||
constexpr char defaultPort[] = "55424";
|
constexpr char defaultPort[] = "55424";
|
||||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(MACOS_NE)
|
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(Q_OS_TVOS) || defined(MACOS_NE)
|
||||||
constexpr char defaultMtu[] = "1280";
|
constexpr char defaultMtu[] = "1280";
|
||||||
#else
|
#else
|
||||||
constexpr char defaultMtu[] = "1376";
|
constexpr char defaultMtu[] = "1376";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
#include "core/errorstrings.h"
|
#include "core/errorstrings.h"
|
||||||
#include "vpnprotocol.h"
|
#include "vpnprotocol.h"
|
||||||
|
|
||||||
#if defined(Q_OS_WINDOWS) || defined(Q_OS_MACX) and !defined MACOS_NE || (defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID))
|
#if defined(Q_OS_WINDOWS) || (defined(Q_OS_MACX) && !defined(Q_OS_IOS) && !defined(Q_OS_TVOS) && !defined(MACOS_NE)) || (defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID))
|
||||||
#include "openvpnovercloakprotocol.h"
|
#include "openvpnovercloakprotocol.h"
|
||||||
#include "openvpnprotocol.h"
|
#include "openvpnprotocol.h"
|
||||||
#include "shadowsocksvpnprotocol.h"
|
#include "shadowsocksvpnprotocol.h"
|
||||||
@@ -114,7 +114,7 @@ VpnProtocol *VpnProtocol::factory(DockerContainer container, const QJsonObject &
|
|||||||
#if defined(Q_OS_WINDOWS)
|
#if defined(Q_OS_WINDOWS)
|
||||||
case DockerContainer::Ipsec: return new Ikev2Protocol(configuration);
|
case DockerContainer::Ipsec: return new Ikev2Protocol(configuration);
|
||||||
#endif
|
#endif
|
||||||
#if defined(Q_OS_WINDOWS) || defined(Q_OS_MACX) and !defined MACOS_NE || (defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID))
|
#if defined(Q_OS_WINDOWS) || (defined(Q_OS_MACX) && !defined(Q_OS_IOS) && !defined(Q_OS_TVOS) && !defined(MACOS_NE)) || (defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID))
|
||||||
case DockerContainer::OpenVpn: return new OpenVpnProtocol(configuration);
|
case DockerContainer::OpenVpn: return new OpenVpnProtocol(configuration);
|
||||||
case DockerContainer::Cloak: return new OpenVpnOverCloakProtocol(configuration);
|
case DockerContainer::Cloak: return new OpenVpnOverCloakProtocol(configuration);
|
||||||
case DockerContainer::ShadowSocks: return new ShadowSocksVpnProtocol(configuration);
|
case DockerContainer::ShadowSocks: return new ShadowSocksVpnProtocol(configuration);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
#include "xrayprotocol.h"
|
#include "xrayprotocol.h"
|
||||||
|
|
||||||
#include "core/ipcclient.h"
|
#include "core/ipcclient.h"
|
||||||
#include "core/serialization/serialization.h"
|
|
||||||
#include "ipc.h"
|
#include "ipc.h"
|
||||||
#include "utilities.h"
|
#include "utilities.h"
|
||||||
#include "core/networkUtilities.h"
|
#include "core/networkUtilities.h"
|
||||||
@@ -15,8 +14,6 @@
|
|||||||
#include <QtCore/qobjectdefs.h>
|
#include <QtCore/qobjectdefs.h>
|
||||||
#include <QtCore/qprocess.h>
|
#include <QtCore/qprocess.h>
|
||||||
|
|
||||||
#include <exception>
|
|
||||||
|
|
||||||
#ifdef Q_OS_MACOS
|
#ifdef Q_OS_MACOS
|
||||||
static const QString tunName = "utun22";
|
static const QString tunName = "utun22";
|
||||||
#else
|
#else
|
||||||
@@ -56,19 +53,6 @@ ErrorCode XrayProtocol::start()
|
|||||||
{
|
{
|
||||||
qDebug() << "XrayProtocol::start()";
|
qDebug() << "XrayProtocol::start()";
|
||||||
|
|
||||||
// Inject SOCKS5 auth into the inbound before starting xray.
|
|
||||||
// Re-uses existing credentials if the config already has them (e.g. imported config).
|
|
||||||
amnezia::serialization::inbounds::InboundCredentials creds;
|
|
||||||
try {
|
|
||||||
creds = amnezia::serialization::inbounds::EnsureInboundAuth(m_xrayConfig);
|
|
||||||
} catch (const std::exception &e) {
|
|
||||||
qCritical() << "EnsureInboundAuth failed:" << e.what();
|
|
||||||
return ErrorCode::InternalError;
|
|
||||||
}
|
|
||||||
m_socksUser = creds.username;
|
|
||||||
m_socksPassword = creds.password;
|
|
||||||
m_socksPort = creds.port;
|
|
||||||
|
|
||||||
return IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
|
return IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||||
auto xrayStart = iface->xrayStart(QJsonDocument(m_xrayConfig).toJson());
|
auto xrayStart = iface->xrayStart(QJsonDocument(m_xrayConfig).toJson());
|
||||||
if (!xrayStart.waitForFinished() || !xrayStart.returnValue()) {
|
if (!xrayStart.waitForFinished() || !xrayStart.returnValue()) {
|
||||||
@@ -137,25 +121,22 @@ ErrorCode XrayProtocol::startTun2Socks()
|
|||||||
return ErrorCode::AmneziaServiceConnectionFailed;
|
return ErrorCode::AmneziaServiceConnectionFailed;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QString proxyUrl = QString("socks5://%1:%2@127.0.0.1:%3")
|
|
||||||
.arg(m_socksUser, m_socksPassword, QString::number(m_socksPort));
|
|
||||||
|
|
||||||
m_tun2socksProcess->setProgram(PermittedProcess::Tun2Socks);
|
m_tun2socksProcess->setProgram(PermittedProcess::Tun2Socks);
|
||||||
m_tun2socksProcess->setArguments({"-device", QString("tun://%1").arg(tunName), "-proxy", proxyUrl});
|
m_tun2socksProcess->setArguments({"-device", QString("tun://%1").arg(tunName), "-proxy", "socks5://127.0.0.1:10808" });
|
||||||
|
|
||||||
connect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardOutput, this, [this]() {
|
connect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardError, this, [this]() {
|
||||||
auto readAllStandardOutput = m_tun2socksProcess->readAllStandardOutput();
|
auto readAllStandardError = m_tun2socksProcess->readAllStandardError();
|
||||||
if (!readAllStandardOutput.waitForFinished()) {
|
if (!readAllStandardError.waitForFinished()) {
|
||||||
qWarning() << "Failed to read output from tun2socks";
|
qWarning() << "Failed to read output from tun2socks";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QString line = readAllStandardOutput.returnValue();
|
const QString line = readAllStandardError.returnValue();
|
||||||
|
|
||||||
if (!line.contains("[TCP]") && !line.contains("[UDP]"))
|
if (!line.contains("[TCP]") && !line.contains("[UDP]"))
|
||||||
qDebug() << "[tun2socks]:" << line;
|
qDebug() << "[tun2socks]:" << line;
|
||||||
|
|
||||||
if (line.contains("[STACK] tun://") && line.contains("<-> socks5://")) {
|
if (line.contains("[STACK] tun://") && line.contains("<-> socks5://127.0.0.1")) {
|
||||||
disconnect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardOutput, this, nullptr);
|
disconnect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardOutput, this, nullptr);
|
||||||
|
|
||||||
if (ErrorCode res = setupRouting(); res != ErrorCode::NoError) {
|
if (ErrorCode res = setupRouting(); res != ErrorCode::NoError) {
|
||||||
|
|||||||
@@ -26,10 +26,6 @@ private:
|
|||||||
QList<QHostAddress> m_dnsServers;
|
QList<QHostAddress> m_dnsServers;
|
||||||
QString m_remoteAddress;
|
QString m_remoteAddress;
|
||||||
|
|
||||||
QString m_socksUser;
|
|
||||||
QString m_socksPassword;
|
|
||||||
int m_socksPort = 10808;
|
|
||||||
|
|
||||||
QSharedPointer<IpcProcessInterfaceReplica> m_tun2socksProcess;
|
QSharedPointer<IpcProcessInterfaceReplica> m_tun2socksProcess;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+1
-12
@@ -27,12 +27,10 @@
|
|||||||
<file>images/controls/folder-open.svg</file>
|
<file>images/controls/folder-open.svg</file>
|
||||||
<file>images/controls/folder-search-2.svg</file>
|
<file>images/controls/folder-search-2.svg</file>
|
||||||
<file>images/controls/gauge.svg</file>
|
<file>images/controls/gauge.svg</file>
|
||||||
<file>images/controls/globe-2.svg</file>
|
|
||||||
<file>images/controls/github.svg</file>
|
<file>images/controls/github.svg</file>
|
||||||
<file>images/controls/help-circle.svg</file>
|
<file>images/controls/help-circle.svg</file>
|
||||||
<file>images/controls/history.svg</file>
|
<file>images/controls/history.svg</file>
|
||||||
<file>images/controls/home.svg</file>
|
<file>images/controls/home.svg</file>
|
||||||
<file>images/controls/infinity.svg</file>
|
|
||||||
<file>images/controls/info.svg</file>
|
<file>images/controls/info.svg</file>
|
||||||
<file>images/controls/mail.svg</file>
|
<file>images/controls/mail.svg</file>
|
||||||
<file>images/controls/map-pin.svg</file>
|
<file>images/controls/map-pin.svg</file>
|
||||||
@@ -57,7 +55,6 @@
|
|||||||
<file>images/controls/settings-news.svg</file>
|
<file>images/controls/settings-news.svg</file>
|
||||||
<file>images/controls/share-2.svg</file>
|
<file>images/controls/share-2.svg</file>
|
||||||
<file>images/controls/split-tunneling.svg</file>
|
<file>images/controls/split-tunneling.svg</file>
|
||||||
<file>images/controls/smartphone.svg</file>
|
|
||||||
<file>images/controls/tag.svg</file>
|
<file>images/controls/tag.svg</file>
|
||||||
<file>images/controls/telegram.svg</file>
|
<file>images/controls/telegram.svg</file>
|
||||||
<file>images/controls/text-cursor.svg</file>
|
<file>images/controls/text-cursor.svg</file>
|
||||||
@@ -136,13 +133,8 @@
|
|||||||
<file>ui/qml/Components/HomeContainersListView.qml</file>
|
<file>ui/qml/Components/HomeContainersListView.qml</file>
|
||||||
<file>ui/qml/Components/HomeSplitTunnelingDrawer.qml</file>
|
<file>ui/qml/Components/HomeSplitTunnelingDrawer.qml</file>
|
||||||
<file>ui/qml/Components/InstalledAppsDrawer.qml</file>
|
<file>ui/qml/Components/InstalledAppsDrawer.qml</file>
|
||||||
<file>ui/qml/Components/BenefitRow.qml</file>
|
|
||||||
<file>ui/qml/Components/BenefitsPanel.qml</file>
|
|
||||||
<file>ui/qml/Components/SubscriptionPlanCard.qml</file>
|
|
||||||
<file>ui/qml/Components/TermsAndPrivacyText.qml</file>
|
|
||||||
<file>ui/qml/Components/QuestionDrawer.qml</file>
|
<file>ui/qml/Components/QuestionDrawer.qml</file>
|
||||||
<file>ui/qml/Components/SelectLanguageDrawer.qml</file>
|
<file>ui/qml/Components/SelectLanguageDrawer.qml</file>
|
||||||
<file>ui/qml/Components/SubscriptionExpiredDrawer.qml</file>
|
|
||||||
<file>ui/qml/Components/ServersListView.qml</file>
|
<file>ui/qml/Components/ServersListView.qml</file>
|
||||||
<file>ui/qml/Components/SettingsContainersListView.qml</file>
|
<file>ui/qml/Components/SettingsContainersListView.qml</file>
|
||||||
<file>ui/qml/Components/TransportProtoSelector.qml</file>
|
<file>ui/qml/Components/TransportProtoSelector.qml</file>
|
||||||
@@ -188,7 +180,6 @@
|
|||||||
<file>ui/qml/Controls2/TextTypes/LabelTextType.qml</file>
|
<file>ui/qml/Controls2/TextTypes/LabelTextType.qml</file>
|
||||||
<file>ui/qml/Controls2/TextTypes/ListItemTitleType.qml</file>
|
<file>ui/qml/Controls2/TextTypes/ListItemTitleType.qml</file>
|
||||||
<file>ui/qml/Controls2/TextTypes/ParagraphTextType.qml</file>
|
<file>ui/qml/Controls2/TextTypes/ParagraphTextType.qml</file>
|
||||||
<file>ui/qml/Controls2/TextTypes/BadgeTextType.qml</file>
|
|
||||||
<file>ui/qml/Controls2/TextTypes/SmallTextType.qml</file>
|
<file>ui/qml/Controls2/TextTypes/SmallTextType.qml</file>
|
||||||
<file>ui/qml/Controls2/TopCloseButtonType.qml</file>
|
<file>ui/qml/Controls2/TopCloseButtonType.qml</file>
|
||||||
<file>ui/qml/Controls2/VerticalRadioButton.qml</file>
|
<file>ui/qml/Controls2/VerticalRadioButton.qml</file>
|
||||||
@@ -234,9 +225,7 @@
|
|||||||
<file>ui/qml/Pages2/PageSettingsNewsDetail.qml</file>
|
<file>ui/qml/Pages2/PageSettingsNewsDetail.qml</file>
|
||||||
<file>ui/qml/Pages2/PageProtocolAwgClientSettings.qml</file>
|
<file>ui/qml/Pages2/PageProtocolAwgClientSettings.qml</file>
|
||||||
<file>ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml</file>
|
<file>ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml</file>
|
||||||
<file>ui/qml/Pages2/PageSetupWizardApiFreeInfo.qml</file>
|
<file>ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml</file>
|
||||||
<file>ui/qml/Pages2/PageSetupWizardApiPremiumInfo.qml</file>
|
|
||||||
<file>ui/qml/Pages2/PageSetupWizardApiTrialEmail.qml</file>
|
|
||||||
<file>ui/qml/Pages2/PageSetupWizardApiServicesList.qml</file>
|
<file>ui/qml/Pages2/PageSetupWizardApiServicesList.qml</file>
|
||||||
<file>ui/qml/Pages2/PageSetupWizardConfigSource.qml</file>
|
<file>ui/qml/Pages2/PageSetupWizardConfigSource.qml</file>
|
||||||
<file>ui/qml/Pages2/PageSetupWizardCredentials.qml</file>
|
<file>ui/qml/Pages2/PageSetupWizardCredentials.qml</file>
|
||||||
|
|||||||
+21
-14
@@ -35,12 +35,13 @@ SecureQSettings::SecureQSettings(const QString &organization, const QString &app
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
m_settings.setValue("Conf/encrypted", true);
|
m_settings.setValue("Conf/encrypted", true);
|
||||||
|
m_settings.sync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariant SecureQSettings::value(const QString &key, const QVariant &defaultValue) const
|
QVariant SecureQSettings::value(const QString &key, const QVariant &defaultValue) const
|
||||||
{
|
{
|
||||||
QMutexLocker locker(&m_mutex);
|
QMutexLocker locker(&mutex);
|
||||||
|
|
||||||
if (m_cache.contains(key)) {
|
if (m_cache.contains(key)) {
|
||||||
return m_cache.value(key);
|
return m_cache.value(key);
|
||||||
@@ -84,7 +85,7 @@ QVariant SecureQSettings::value(const QString &key, const QVariant &defaultValue
|
|||||||
|
|
||||||
void SecureQSettings::setValue(const QString &key, const QVariant &value)
|
void SecureQSettings::setValue(const QString &key, const QVariant &value)
|
||||||
{
|
{
|
||||||
QMutexLocker locker(&m_mutex);
|
QMutexLocker locker(&mutex);
|
||||||
|
|
||||||
if (encryptionRequired() && encryptedKeys.contains(key)) {
|
if (encryptionRequired() && encryptedKeys.contains(key)) {
|
||||||
if (!getEncKey().isEmpty() && !getEncIv().isEmpty()) {
|
if (!getEncKey().isEmpty() && !getEncIv().isEmpty()) {
|
||||||
@@ -106,20 +107,26 @@ void SecureQSettings::setValue(const QString &key, const QVariant &value)
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_cache.insert(key, value);
|
m_cache.insert(key, value);
|
||||||
|
sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SecureQSettings::remove(const QString &key)
|
void SecureQSettings::remove(const QString &key)
|
||||||
{
|
{
|
||||||
QMutexLocker locker(&m_mutex);
|
QMutexLocker locker(&mutex);
|
||||||
|
|
||||||
m_settings.remove(key);
|
m_settings.remove(key);
|
||||||
m_cache.remove(key);
|
m_cache.remove(key);
|
||||||
|
|
||||||
|
sync();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SecureQSettings::sync()
|
||||||
|
{
|
||||||
|
m_settings.sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray SecureQSettings::backupAppConfig() const
|
QByteArray SecureQSettings::backupAppConfig() const
|
||||||
{
|
{
|
||||||
QMutexLocker locker(&m_mutex);
|
|
||||||
|
|
||||||
QJsonObject cfg;
|
QJsonObject cfg;
|
||||||
|
|
||||||
const auto needToBackup = [this](const auto &key) {
|
const auto needToBackup = [this](const auto &key) {
|
||||||
@@ -154,8 +161,6 @@ QByteArray SecureQSettings::backupAppConfig() const
|
|||||||
|
|
||||||
bool SecureQSettings::restoreAppConfig(const QByteArray &json)
|
bool SecureQSettings::restoreAppConfig(const QByteArray &json)
|
||||||
{
|
{
|
||||||
QMutexLocker locker(&m_mutex);
|
|
||||||
|
|
||||||
QJsonObject cfg = QJsonDocument::fromJson(json).object();
|
QJsonObject cfg = QJsonDocument::fromJson(json).object();
|
||||||
if (cfg.isEmpty())
|
if (cfg.isEmpty())
|
||||||
return false;
|
return false;
|
||||||
@@ -168,16 +173,10 @@ bool SecureQSettings::restoreAppConfig(const QByteArray &json)
|
|||||||
setValue(key, cfg.value(key).toVariant());
|
setValue(key, cfg.value(key).toVariant());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sync();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SecureQSettings::clearSettings()
|
|
||||||
{
|
|
||||||
QMutexLocker locker(&m_mutex);
|
|
||||||
m_settings.clear();
|
|
||||||
m_cache.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
QByteArray SecureQSettings::encryptText(const QByteArray &value) const
|
QByteArray SecureQSettings::encryptText(const QByteArray &value) const
|
||||||
{
|
{
|
||||||
QSimpleCrypto::QBlockCipher cipher;
|
QSimpleCrypto::QBlockCipher cipher;
|
||||||
@@ -295,3 +294,11 @@ void SecureQSettings::setSecTag(const QString &tag, const QByteArray &data)
|
|||||||
qCritical() << "SecureQSettings::setSecTag Error:" << job->errorString();
|
qCritical() << "SecureQSettings::setSecTag Error:" << job->errorString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SecureQSettings::clearSettings()
|
||||||
|
{
|
||||||
|
QMutexLocker locker(&mutex);
|
||||||
|
m_settings.clear();
|
||||||
|
m_cache.clear();
|
||||||
|
sync();
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,16 +16,14 @@ public:
|
|||||||
explicit SecureQSettings(const QString &organization, const QString &application = QString(),
|
explicit SecureQSettings(const QString &organization, const QString &application = QString(),
|
||||||
QObject *parent = nullptr);
|
QObject *parent = nullptr);
|
||||||
|
|
||||||
QVariant value(const QString &key, const QVariant &defaultValue = QVariant()) const;
|
Q_INVOKABLE QVariant value(const QString &key, const QVariant &defaultValue = QVariant()) const;
|
||||||
void setValue(const QString &key, const QVariant &value);
|
Q_INVOKABLE void setValue(const QString &key, const QVariant &value);
|
||||||
void remove(const QString &key);
|
void remove(const QString &key);
|
||||||
|
void sync();
|
||||||
|
|
||||||
QByteArray backupAppConfig() const;
|
QByteArray backupAppConfig() const;
|
||||||
bool restoreAppConfig(const QByteArray &json);
|
bool restoreAppConfig(const QByteArray &json);
|
||||||
|
|
||||||
void clearSettings();
|
|
||||||
|
|
||||||
private:
|
|
||||||
QByteArray encryptText(const QByteArray &value) const;
|
QByteArray encryptText(const QByteArray &value) const;
|
||||||
QByteArray decryptText(const QByteArray &ba) const;
|
QByteArray decryptText(const QByteArray &ba) const;
|
||||||
|
|
||||||
@@ -37,6 +35,9 @@ private:
|
|||||||
static QByteArray getSecTag(const QString &tag);
|
static QByteArray getSecTag(const QString &tag);
|
||||||
static void setSecTag(const QString &tag, const QByteArray &data);
|
static void setSecTag(const QString &tag, const QByteArray &data);
|
||||||
|
|
||||||
|
void clearSettings();
|
||||||
|
|
||||||
|
private:
|
||||||
QSettings m_settings;
|
QSettings m_settings;
|
||||||
|
|
||||||
mutable QHash<QString, QVariant> m_cache;
|
mutable QHash<QString, QVariant> m_cache;
|
||||||
@@ -52,7 +53,7 @@ private:
|
|||||||
|
|
||||||
const QByteArray magicString { "EncData" }; // Magic keyword used for mark encrypted QByteArray
|
const QByteArray magicString { "EncData" }; // Magic keyword used for mark encrypted QByteArray
|
||||||
|
|
||||||
mutable QRecursiveMutex m_mutex;
|
mutable QMutex mutex;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // SECUREQSETTINGS_H
|
#endif // SECUREQSETTINGS_H
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
sudo docker ps -a | grep amnezia | awk '{print $1}' | xargs sudo docker stop;\
|
sudo docker ps -a | grep amnezia | awk '{print $1}' | xargs sudo docker stop;\
|
||||||
sudo docker ps -a | grep amnezia | awk '{print $1}' | xargs sudo docker rm -fv;\
|
sudo docker ps -a | grep amnezia | awk '{print $1}' | xargs sudo docker rm -fv;\
|
||||||
sudo docker images -a --format table | grep amnezia | awk '{print $3, $1 ":" $2}' | xargs sudo docker rmi;\
|
sudo docker images -a --format table | grep amnezia | awk '{print $3}' | xargs sudo docker rmi;\
|
||||||
sudo docker network ls | grep amnezia-dns-net | awk '{print $1}' | xargs sudo docker network rm;\
|
sudo docker network ls | grep amnezia-dns-net | awk '{print $1}' | xargs sudo docker network rm;\
|
||||||
sudo rm -frd /opt/amnezia
|
sudo rm -frd /opt/amnezia
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM 3proxy/3proxy:0.9.5
|
FROM 3proxy/3proxy:latest
|
||||||
|
|
||||||
LABEL maintainer="AmneziaVPN"
|
LABEL maintainer="AmneziaVPN"
|
||||||
|
|
||||||
@@ -7,4 +7,4 @@ RUN echo -e "#!/bin/bash\ntail -f /dev/null" > /opt/amnezia/start.sh
|
|||||||
RUN chmod a+x /opt/amnezia/start.sh
|
RUN chmod a+x /opt/amnezia/start.sh
|
||||||
|
|
||||||
ENTRYPOINT [ "/bin/sh", "/opt/amnezia/start.sh" ]
|
ENTRYPOINT [ "/bin/sh", "/opt/amnezia/start.sh" ]
|
||||||
CMD [ "" ]
|
CMD [ "" ]
|
||||||
+57
-53
@@ -15,17 +15,16 @@ namespace
|
|||||||
const char cloudFlareNs2[] = "1.0.0.1";
|
const char cloudFlareNs2[] = "1.0.0.1";
|
||||||
|
|
||||||
constexpr char gatewayEndpoint[] = "http://gw.amnezia.org:80/";
|
constexpr char gatewayEndpoint[] = "http://gw.amnezia.org:80/";
|
||||||
constexpr char proxyUrlsKey[] = "Conf/proxyUrls/";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Settings::Settings(QObject *parent) : QObject(parent), m_settings(ORGANIZATION_NAME, APPLICATION_NAME, this)
|
Settings::Settings(QObject *parent) : QObject(parent), m_settings(ORGANIZATION_NAME, APPLICATION_NAME, this)
|
||||||
{
|
{
|
||||||
// Import old settings
|
// Import old settings
|
||||||
if (serversCount() == 0) {
|
if (serversCount() == 0) {
|
||||||
QString user = m_settings.value("Server/userName").toString();
|
QString user = value("Server/userName").toString();
|
||||||
QString password = m_settings.value("Server/password").toString();
|
QString password = value("Server/password").toString();
|
||||||
QString serverName = m_settings.value("Server/serverName").toString();
|
QString serverName = value("Server/serverName").toString();
|
||||||
int port = m_settings.value("Server/serverPort").toInt();
|
int port = value("Server/serverPort").toInt();
|
||||||
|
|
||||||
if (!user.isEmpty() && !password.isEmpty() && !serverName.isEmpty()) {
|
if (!user.isEmpty() && !password.isEmpty() && !serverName.isEmpty()) {
|
||||||
QJsonObject server;
|
QJsonObject server;
|
||||||
@@ -223,7 +222,7 @@ QString Settings::nextAvailableServerName() const
|
|||||||
|
|
||||||
void Settings::setSaveLogs(bool enabled)
|
void Settings::setSaveLogs(bool enabled)
|
||||||
{
|
{
|
||||||
m_settings.setValue("Conf/saveLogs", enabled);
|
setValue("Conf/saveLogs", enabled);
|
||||||
#ifndef Q_OS_ANDROID
|
#ifndef Q_OS_ANDROID
|
||||||
if (!isSaveLogs()) {
|
if (!isSaveLogs()) {
|
||||||
Logger::deInit();
|
Logger::deInit();
|
||||||
@@ -243,12 +242,12 @@ void Settings::setSaveLogs(bool enabled)
|
|||||||
|
|
||||||
QDateTime Settings::getLogEnableDate()
|
QDateTime Settings::getLogEnableDate()
|
||||||
{
|
{
|
||||||
return m_settings.value("Conf/logEnableDate").toDateTime();
|
return value("Conf/logEnableDate").toDateTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Settings::setLogEnableDate(QDateTime date)
|
void Settings::setLogEnableDate(QDateTime date)
|
||||||
{
|
{
|
||||||
m_settings.setValue("Conf/logEnableDate", date);
|
setValue("Conf/logEnableDate", date);
|
||||||
}
|
}
|
||||||
|
|
||||||
QString Settings::routeModeString(RouteMode mode) const
|
QString Settings::routeModeString(RouteMode mode) const
|
||||||
@@ -262,17 +261,17 @@ QString Settings::routeModeString(RouteMode mode) const
|
|||||||
|
|
||||||
Settings::RouteMode Settings::routeMode() const
|
Settings::RouteMode Settings::routeMode() const
|
||||||
{
|
{
|
||||||
return static_cast<RouteMode>(m_settings.value("Conf/routeMode", 0).toInt());
|
return static_cast<RouteMode>(value("Conf/routeMode", 0).toInt());
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Settings::isSitesSplitTunnelingEnabled() const
|
bool Settings::isSitesSplitTunnelingEnabled() const
|
||||||
{
|
{
|
||||||
return m_settings.value("Conf/sitesSplitTunnelingEnabled", false).toBool();
|
return value("Conf/sitesSplitTunnelingEnabled", false).toBool();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Settings::setSitesSplitTunnelingEnabled(bool enabled)
|
void Settings::setSitesSplitTunnelingEnabled(bool enabled)
|
||||||
{
|
{
|
||||||
m_settings.setValue("Conf/sitesSplitTunnelingEnabled", enabled);
|
setValue("Conf/sitesSplitTunnelingEnabled", enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Settings::addVpnSite(RouteMode mode, const QString &site, const QString &ip)
|
bool Settings::addVpnSite(RouteMode mode, const QString &site, const QString &ip)
|
||||||
@@ -360,12 +359,12 @@ void Settings::removeAllVpnSites(RouteMode mode)
|
|||||||
|
|
||||||
QString Settings::primaryDns() const
|
QString Settings::primaryDns() const
|
||||||
{
|
{
|
||||||
return m_settings.value("Conf/primaryDns", cloudFlareNs1).toString();
|
return value("Conf/primaryDns", cloudFlareNs1).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString Settings::secondaryDns() const
|
QString Settings::secondaryDns() const
|
||||||
{
|
{
|
||||||
return m_settings.value("Conf/secondaryDns", cloudFlareNs2).toString();
|
return value("Conf/secondaryDns", cloudFlareNs2).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Settings::clearSettings()
|
void Settings::clearSettings()
|
||||||
@@ -387,18 +386,18 @@ QString Settings::appsRouteModeString(AppsRouteMode mode) const
|
|||||||
|
|
||||||
Settings::AppsRouteMode Settings::getAppsRouteMode() const
|
Settings::AppsRouteMode Settings::getAppsRouteMode() const
|
||||||
{
|
{
|
||||||
return static_cast<AppsRouteMode>(m_settings.value("Conf/appsRouteMode", 0).toInt());
|
return static_cast<AppsRouteMode>(value("Conf/appsRouteMode", 0).toInt());
|
||||||
}
|
}
|
||||||
|
|
||||||
void Settings::setAppsRouteMode(AppsRouteMode mode)
|
void Settings::setAppsRouteMode(AppsRouteMode mode)
|
||||||
{
|
{
|
||||||
m_settings.setValue("Conf/appsRouteMode", mode);
|
setValue("Conf/appsRouteMode", mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
QVector<InstalledAppInfo> Settings::getVpnApps(AppsRouteMode mode) const
|
QVector<InstalledAppInfo> Settings::getVpnApps(AppsRouteMode mode) const
|
||||||
{
|
{
|
||||||
QVector<InstalledAppInfo> apps;
|
QVector<InstalledAppInfo> apps;
|
||||||
auto appsArray = m_settings.value("Conf/" + appsRouteModeString(mode)).toJsonArray();
|
auto appsArray = value("Conf/" + appsRouteModeString(mode)).toJsonArray();
|
||||||
for (const auto &app : appsArray) {
|
for (const auto &app : appsArray) {
|
||||||
InstalledAppInfo appInfo;
|
InstalledAppInfo appInfo;
|
||||||
appInfo.appName = app.toObject().value("appName").toString();
|
appInfo.appName = app.toObject().value("appName").toString();
|
||||||
@@ -420,42 +419,43 @@ void Settings::setVpnApps(AppsRouteMode mode, const QVector<InstalledAppInfo> &a
|
|||||||
appInfo.insert("appPath", app.appPath);
|
appInfo.insert("appPath", app.appPath);
|
||||||
appsArray.push_back(appInfo);
|
appsArray.push_back(appInfo);
|
||||||
}
|
}
|
||||||
m_settings.setValue("Conf/" + appsRouteModeString(mode), appsArray);
|
setValue("Conf/" + appsRouteModeString(mode), appsArray);
|
||||||
|
m_settings.sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Settings::isAppsSplitTunnelingEnabled() const
|
bool Settings::isAppsSplitTunnelingEnabled() const
|
||||||
{
|
{
|
||||||
return m_settings.value("Conf/appsSplitTunnelingEnabled", false).toBool();
|
return value("Conf/appsSplitTunnelingEnabled", false).toBool();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Settings::setAppsSplitTunnelingEnabled(bool enabled)
|
void Settings::setAppsSplitTunnelingEnabled(bool enabled)
|
||||||
{
|
{
|
||||||
m_settings.setValue("Conf/appsSplitTunnelingEnabled", enabled);
|
setValue("Conf/appsSplitTunnelingEnabled", enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Settings::isKillSwitchEnabled() const
|
bool Settings::isKillSwitchEnabled() const
|
||||||
{
|
{
|
||||||
return m_settings.value("Conf/killSwitchEnabled", true).toBool();
|
return value("Conf/killSwitchEnabled", true).toBool();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Settings::setKillSwitchEnabled(bool enabled)
|
void Settings::setKillSwitchEnabled(bool enabled)
|
||||||
{
|
{
|
||||||
m_settings.setValue("Conf/killSwitchEnabled", enabled);
|
setValue("Conf/killSwitchEnabled", enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Settings::isStrictKillSwitchEnabled() const
|
bool Settings::isStrictKillSwitchEnabled() const
|
||||||
{
|
{
|
||||||
return m_settings.value("Conf/strictKillSwitchEnabled", false).toBool();
|
return value("Conf/strictKillSwitchEnabled", false).toBool();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Settings::setStrictKillSwitchEnabled(bool enabled)
|
void Settings::setStrictKillSwitchEnabled(bool enabled)
|
||||||
{
|
{
|
||||||
m_settings.setValue("Conf/strictKillSwitchEnabled", enabled);
|
setValue("Conf/strictKillSwitchEnabled", enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
QString Settings::getInstallationUuid(const bool needCreate)
|
QString Settings::getInstallationUuid(const bool needCreate)
|
||||||
{
|
{
|
||||||
auto uuid = m_settings.value("Conf/installationUuid", "").toString();
|
auto uuid = value("Conf/installationUuid", "").toString();
|
||||||
if (needCreate && uuid.isEmpty()) {
|
if (needCreate && uuid.isEmpty()) {
|
||||||
uuid = QUuid::createUuid().toString();
|
uuid = QUuid::createUuid().toString();
|
||||||
|
|
||||||
@@ -476,7 +476,7 @@ QString Settings::getInstallationUuid(const bool needCreate)
|
|||||||
|
|
||||||
void Settings::setInstallationUuid(const QString &uuid)
|
void Settings::setInstallationUuid(const QString &uuid)
|
||||||
{
|
{
|
||||||
m_settings.setValue("Conf/installationUuid", uuid);
|
setValue("Conf/installationUuid", uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
ServerCredentials Settings::defaultServerCredentials() const
|
ServerCredentials Settings::defaultServerCredentials() const
|
||||||
@@ -497,6 +497,28 @@ ServerCredentials Settings::serverCredentials(int index) const
|
|||||||
return credentials;
|
return credentials;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QVariant Settings::value(const QString &key, const QVariant &defaultValue) const
|
||||||
|
{
|
||||||
|
QVariant returnValue;
|
||||||
|
if (QThread::currentThread() == QCoreApplication::instance()->thread()) {
|
||||||
|
returnValue = m_settings.value(key, defaultValue);
|
||||||
|
} else {
|
||||||
|
QMetaObject::invokeMethod(&m_settings, "value", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QVariant, returnValue),
|
||||||
|
Q_ARG(const QString &, key), Q_ARG(const QVariant &, defaultValue));
|
||||||
|
}
|
||||||
|
return returnValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Settings::setValue(const QString &key, const QVariant &value)
|
||||||
|
{
|
||||||
|
if (QThread::currentThread() == QCoreApplication::instance()->thread()) {
|
||||||
|
m_settings.setValue(key, value);
|
||||||
|
} else {
|
||||||
|
QMetaObject::invokeMethod(&m_settings, "setValue", Qt::BlockingQueuedConnection, Q_ARG(const QString &, key),
|
||||||
|
Q_ARG(const QVariant &, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Settings::resetGatewayEndpoint()
|
void Settings::resetGatewayEndpoint()
|
||||||
{
|
{
|
||||||
m_gatewayEndpoint = gatewayEndpoint;
|
m_gatewayEndpoint = gatewayEndpoint;
|
||||||
@@ -519,68 +541,50 @@ QString Settings::getGatewayEndpoint(bool isTestPurchase)
|
|||||||
|
|
||||||
bool Settings::isDevGatewayEnv(bool isTestPurchase)
|
bool Settings::isDevGatewayEnv(bool isTestPurchase)
|
||||||
{
|
{
|
||||||
return isTestPurchase ? true : m_settings.value("Conf/devGatewayEnv", false).toBool();
|
return isTestPurchase ? true : value("Conf/devGatewayEnv", false).toBool();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Settings::toggleDevGatewayEnv(bool enabled)
|
void Settings::toggleDevGatewayEnv(bool enabled)
|
||||||
{
|
{
|
||||||
m_settings.setValue("Conf/devGatewayEnv", enabled);
|
setValue("Conf/devGatewayEnv", enabled);
|
||||||
}
|
|
||||||
|
|
||||||
QByteArray Settings::readGatewayProxyUrls(const QString &cacheKey) const
|
|
||||||
{
|
|
||||||
if (cacheKey.isEmpty()) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
return m_settings.value(QString(proxyUrlsKey) + cacheKey).toByteArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Settings::writeGatewayProxyUrls(const QString &cacheKey, const QByteArray &proxyUrlsEncrypted)
|
|
||||||
{
|
|
||||||
if (cacheKey.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_settings.setValue(QString(proxyUrlsKey) + cacheKey, proxyUrlsEncrypted);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Settings::isHomeAdLabelVisible()
|
bool Settings::isHomeAdLabelVisible()
|
||||||
{
|
{
|
||||||
return m_settings.value("Conf/homeAdLabelVisible", true).toBool();
|
return value("Conf/homeAdLabelVisible", true).toBool();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Settings::disableHomeAdLabel()
|
void Settings::disableHomeAdLabel()
|
||||||
{
|
{
|
||||||
m_settings.setValue("Conf/homeAdLabelVisible", false);
|
setValue("Conf/homeAdLabelVisible", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Settings::isPremV1MigrationReminderActive()
|
bool Settings::isPremV1MigrationReminderActive()
|
||||||
{
|
{
|
||||||
return m_settings.value("Conf/premV1MigrationReminderActive", true).toBool();
|
return value("Conf/premV1MigrationReminderActive", true).toBool();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Settings::disablePremV1MigrationReminder()
|
void Settings::disablePremV1MigrationReminder()
|
||||||
{
|
{
|
||||||
m_settings.setValue("Conf/premV1MigrationReminderActive", false);
|
setValue("Conf/premV1MigrationReminderActive", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringList Settings::allowedDnsServers() const
|
QStringList Settings::allowedDnsServers() const
|
||||||
{
|
{
|
||||||
return m_settings.value("Conf/allowedDnsServers").toStringList();
|
return value("Conf/allowedDnsServers").toStringList();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Settings::setAllowedDnsServers(const QStringList &servers)
|
void Settings::setAllowedDnsServers(const QStringList &servers)
|
||||||
{
|
{
|
||||||
m_settings.setValue("Conf/allowedDnsServers", servers);
|
setValue("Conf/allowedDnsServers", servers);
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringList Settings::readNewsIds() const
|
QStringList Settings::readNewsIds() const
|
||||||
{
|
{
|
||||||
return m_settings.value("News/readIds").toStringList();
|
return value("News/readIds").toStringList();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Settings::setReadNewsIds(const QStringList &ids)
|
void Settings::setReadNewsIds(const QStringList &ids)
|
||||||
{
|
{
|
||||||
m_settings.setValue("News/readIds", ids);
|
setValue("News/readIds", ids);
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-24
@@ -4,7 +4,6 @@
|
|||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QByteArray>
|
|
||||||
|
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
@@ -30,11 +29,11 @@ public:
|
|||||||
|
|
||||||
QJsonArray serversArray() const
|
QJsonArray serversArray() const
|
||||||
{
|
{
|
||||||
return QJsonDocument::fromJson(m_settings.value("Servers/serversList").toByteArray()).array();
|
return QJsonDocument::fromJson(value("Servers/serversList").toByteArray()).array();
|
||||||
}
|
}
|
||||||
void setServersArray(const QJsonArray &servers)
|
void setServersArray(const QJsonArray &servers)
|
||||||
{
|
{
|
||||||
m_settings.setValue("Servers/serversList", QJsonDocument(servers).toJson());
|
setValue("Servers/serversList", QJsonDocument(servers).toJson());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Servers section
|
// Servers section
|
||||||
@@ -46,11 +45,11 @@ public:
|
|||||||
|
|
||||||
int defaultServerIndex() const
|
int defaultServerIndex() const
|
||||||
{
|
{
|
||||||
return m_settings.value("Servers/defaultServerIndex", 0).toInt();
|
return value("Servers/defaultServerIndex", 0).toInt();
|
||||||
}
|
}
|
||||||
void setDefaultServer(int index)
|
void setDefaultServer(int index)
|
||||||
{
|
{
|
||||||
m_settings.setValue("Servers/defaultServerIndex", index);
|
setValue("Servers/defaultServerIndex", index);
|
||||||
}
|
}
|
||||||
QJsonObject defaultServer() const
|
QJsonObject defaultServer() const
|
||||||
{
|
{
|
||||||
@@ -79,34 +78,34 @@ public:
|
|||||||
// App settings section
|
// App settings section
|
||||||
bool isAutoConnect() const
|
bool isAutoConnect() const
|
||||||
{
|
{
|
||||||
return m_settings.value("Conf/autoConnect", false).toBool();
|
return value("Conf/autoConnect", false).toBool();
|
||||||
}
|
}
|
||||||
void setAutoConnect(bool enabled)
|
void setAutoConnect(bool enabled)
|
||||||
{
|
{
|
||||||
m_settings.setValue("Conf/autoConnect", enabled);
|
setValue("Conf/autoConnect", enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isStartMinimized() const
|
bool isStartMinimized() const
|
||||||
{
|
{
|
||||||
return m_settings.value("Conf/startMinimized", false).toBool();
|
return value("Conf/startMinimized", false).toBool();
|
||||||
}
|
}
|
||||||
void setStartMinimized(bool enabled)
|
void setStartMinimized(bool enabled)
|
||||||
{
|
{
|
||||||
m_settings.setValue("Conf/startMinimized", enabled);
|
setValue("Conf/startMinimized", enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isNewsNotifications() const
|
bool isNewsNotifications() const
|
||||||
{
|
{
|
||||||
return m_settings.value("Conf/newsNotifications", true).toBool();
|
return value("Conf/newsNotifications", true).toBool();
|
||||||
}
|
}
|
||||||
void setNewsNotifications(bool enabled)
|
void setNewsNotifications(bool enabled)
|
||||||
{
|
{
|
||||||
m_settings.setValue("Conf/newsNotifications", enabled);
|
setValue("Conf/newsNotifications", enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isSaveLogs() const
|
bool isSaveLogs() const
|
||||||
{
|
{
|
||||||
return m_settings.value("Conf/saveLogs", false).toBool();
|
return value("Conf/saveLogs", false).toBool();
|
||||||
}
|
}
|
||||||
void setSaveLogs(bool enabled);
|
void setSaveLogs(bool enabled);
|
||||||
|
|
||||||
@@ -123,18 +122,19 @@ public:
|
|||||||
QString routeModeString(RouteMode mode) const;
|
QString routeModeString(RouteMode mode) const;
|
||||||
|
|
||||||
RouteMode routeMode() const;
|
RouteMode routeMode() const;
|
||||||
void setRouteMode(RouteMode mode) { m_settings.setValue("Conf/routeMode", mode); }
|
void setRouteMode(RouteMode mode) { setValue("Conf/routeMode", mode); }
|
||||||
|
|
||||||
bool isSitesSplitTunnelingEnabled() const;
|
bool isSitesSplitTunnelingEnabled() const;
|
||||||
void setSitesSplitTunnelingEnabled(bool enabled);
|
void setSitesSplitTunnelingEnabled(bool enabled);
|
||||||
|
|
||||||
QVariantMap vpnSites(RouteMode mode) const
|
QVariantMap vpnSites(RouteMode mode) const
|
||||||
{
|
{
|
||||||
return m_settings.value("Conf/" + routeModeString(mode)).toMap();
|
return value("Conf/" + routeModeString(mode)).toMap();
|
||||||
}
|
}
|
||||||
void setVpnSites(RouteMode mode, const QVariantMap &sites)
|
void setVpnSites(RouteMode mode, const QVariantMap &sites)
|
||||||
{
|
{
|
||||||
m_settings.setValue("Conf/" + routeModeString(mode), sites);
|
setValue("Conf/" + routeModeString(mode), sites);
|
||||||
|
m_settings.sync();
|
||||||
}
|
}
|
||||||
bool addVpnSite(RouteMode mode, const QString &site, const QString &ip = "");
|
bool addVpnSite(RouteMode mode, const QString &site, const QString &ip = "");
|
||||||
void addVpnSites(RouteMode mode, const QMap<QString, QString> &sites); // map <site, ip>
|
void addVpnSites(RouteMode mode, const QMap<QString, QString> &sites); // map <site, ip>
|
||||||
@@ -147,11 +147,11 @@ public:
|
|||||||
|
|
||||||
bool useAmneziaDns() const
|
bool useAmneziaDns() const
|
||||||
{
|
{
|
||||||
return m_settings.value("Conf/useAmneziaDns", true).toBool();
|
return value("Conf/useAmneziaDns", true).toBool();
|
||||||
}
|
}
|
||||||
void setUseAmneziaDns(bool enabled)
|
void setUseAmneziaDns(bool enabled)
|
||||||
{
|
{
|
||||||
m_settings.setValue("Conf/useAmneziaDns", enabled);
|
setValue("Conf/useAmneziaDns", enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
QString primaryDns() const;
|
QString primaryDns() const;
|
||||||
@@ -160,13 +160,13 @@ public:
|
|||||||
// QString primaryDns() const { return m_primaryDns; }
|
// QString primaryDns() const { return m_primaryDns; }
|
||||||
void setPrimaryDns(const QString &primaryDns)
|
void setPrimaryDns(const QString &primaryDns)
|
||||||
{
|
{
|
||||||
m_settings.setValue("Conf/primaryDns", primaryDns);
|
setValue("Conf/primaryDns", primaryDns);
|
||||||
}
|
}
|
||||||
|
|
||||||
// QString secondaryDns() const { return m_secondaryDns; }
|
// QString secondaryDns() const { return m_secondaryDns; }
|
||||||
void setSecondaryDns(const QString &secondaryDns)
|
void setSecondaryDns(const QString &secondaryDns)
|
||||||
{
|
{
|
||||||
m_settings.setValue("Conf/secondaryDns", secondaryDns);
|
setValue("Conf/secondaryDns", secondaryDns);
|
||||||
}
|
}
|
||||||
|
|
||||||
// static constexpr char openNicNs5[] = "94.103.153.176";
|
// static constexpr char openNicNs5[] = "94.103.153.176";
|
||||||
@@ -188,16 +188,16 @@ public:
|
|||||||
};
|
};
|
||||||
void setAppLanguage(QLocale locale)
|
void setAppLanguage(QLocale locale)
|
||||||
{
|
{
|
||||||
m_settings.setValue("Conf/appLanguage", locale.name());
|
setValue("Conf/appLanguage", locale.name());
|
||||||
};
|
};
|
||||||
|
|
||||||
bool isScreenshotsEnabled() const
|
bool isScreenshotsEnabled() const
|
||||||
{
|
{
|
||||||
return m_settings.value("Conf/screenshotsEnabled", true).toBool();
|
return value("Conf/screenshotsEnabled", true).toBool();
|
||||||
}
|
}
|
||||||
void setScreenshotsEnabled(bool enabled)
|
void setScreenshotsEnabled(bool enabled)
|
||||||
{
|
{
|
||||||
m_settings.setValue("Conf/screenshotsEnabled", enabled);
|
setValue("Conf/screenshotsEnabled", enabled);
|
||||||
emit screenshotsEnabledChanged(enabled);
|
emit screenshotsEnabledChanged(enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,8 +235,6 @@ public:
|
|||||||
QString getGatewayEndpoint(bool isTestPurchase = false);
|
QString getGatewayEndpoint(bool isTestPurchase = false);
|
||||||
bool isDevGatewayEnv(bool isTestPurchase = false);
|
bool isDevGatewayEnv(bool isTestPurchase = false);
|
||||||
void toggleDevGatewayEnv(bool enabled);
|
void toggleDevGatewayEnv(bool enabled);
|
||||||
QByteArray readGatewayProxyUrls(const QString &cacheKey) const;
|
|
||||||
void writeGatewayProxyUrls(const QString &cacheKey, const QByteArray &proxyUrlsEncrypted);
|
|
||||||
|
|
||||||
bool isHomeAdLabelVisible();
|
bool isHomeAdLabelVisible();
|
||||||
void disableHomeAdLabel();
|
void disableHomeAdLabel();
|
||||||
@@ -257,6 +255,9 @@ signals:
|
|||||||
void settingsCleared();
|
void settingsCleared();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
QVariant value(const QString &key, const QVariant &defaultValue = QVariant()) const;
|
||||||
|
void setValue(const QString &key, const QVariant &value);
|
||||||
|
|
||||||
void setInstallationUuid(const QString &uuid);
|
void setInstallationUuid(const QString &uuid);
|
||||||
|
|
||||||
mutable SecureQSettings m_settings;
|
mutable SecureQSettings m_settings;
|
||||||
|
|||||||
+1388
-841
File diff suppressed because it is too large
Load Diff
+1402
-867
File diff suppressed because it is too large
Load Diff
+1388
-841
File diff suppressed because it is too large
Load Diff
+1391
-840
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1408
-865
File diff suppressed because it is too large
Load Diff
+1396
-853
File diff suppressed because it is too large
Load Diff
+1402
-875
File diff suppressed because it is too large
Load Diff
@@ -9,14 +9,9 @@
|
|||||||
#include "ui/controllers/systemController.h"
|
#include "ui/controllers/systemController.h"
|
||||||
#include "version.h"
|
#include "version.h"
|
||||||
#include <QClipboard>
|
#include <QClipboard>
|
||||||
#include <QCoreApplication>
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QEventLoop>
|
#include <QEventLoop>
|
||||||
#include <QHash>
|
|
||||||
#include <QJsonArray>
|
|
||||||
#include <QSet>
|
#include <QSet>
|
||||||
#include <QVariantMap>
|
|
||||||
#include <limits>
|
|
||||||
|
|
||||||
#include "platforms/ios/ios_controller.h"
|
#include "platforms/ios/ios_controller.h"
|
||||||
|
|
||||||
@@ -44,15 +39,6 @@ namespace
|
|||||||
constexpr char serviceInfo[] = "service_info";
|
constexpr char serviceInfo[] = "service_info";
|
||||||
constexpr char serviceProtocol[] = "service_protocol";
|
constexpr char serviceProtocol[] = "service_protocol";
|
||||||
|
|
||||||
constexpr char services[] = "services";
|
|
||||||
constexpr char serviceDescription[] = "service_description";
|
|
||||||
constexpr char subscriptionPlans[] = "subscription_plans";
|
|
||||||
constexpr char storeProductId[] = "store_product_id";
|
|
||||||
constexpr char priceLabel[] = "price_label";
|
|
||||||
constexpr char subtitle[] = "subtitle";
|
|
||||||
constexpr char isTrial[] = "is_trial";
|
|
||||||
constexpr char minPriceLabel[] = "min_price_label";
|
|
||||||
|
|
||||||
constexpr char apiPayload[] = "api_payload";
|
constexpr char apiPayload[] = "api_payload";
|
||||||
constexpr char keyPayload[] = "key_payload";
|
constexpr char keyPayload[] = "key_payload";
|
||||||
|
|
||||||
@@ -61,6 +47,9 @@ namespace
|
|||||||
|
|
||||||
constexpr char config[] = "config";
|
constexpr char config[] = "config";
|
||||||
|
|
||||||
|
constexpr char subscription[] = "subscription";
|
||||||
|
constexpr char endDate[] = "end_date";
|
||||||
|
|
||||||
constexpr char isConnectEvent[] = "is_connect_event";
|
constexpr char isConnectEvent[] = "is_connect_event";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,190 +241,13 @@ namespace
|
|||||||
|
|
||||||
return ErrorCode::NoError;
|
return ErrorCode::NoError;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
|
||||||
struct StoreKitPlanQuote {
|
|
||||||
QString displayPrice;
|
|
||||||
double priceAmount = 0.0;
|
|
||||||
double subscriptionBillingMonths = 0.0;
|
|
||||||
QString displayPricePerMonth;
|
|
||||||
};
|
|
||||||
|
|
||||||
constexpr double kOneMonthThreshold = 1.0 + 1e-6;
|
|
||||||
constexpr double kMonthsFallbackThreshold = 1e-6;
|
|
||||||
constexpr double kMonthlyPriceEpsilon = 1e-9;
|
|
||||||
|
|
||||||
QStringList collectPremiumStoreProductIds(const QJsonArray &services)
|
|
||||||
{
|
|
||||||
QStringList productIds;
|
|
||||||
QSet<QString> seenProductIds;
|
|
||||||
for (const QJsonValue &serviceValue : services) {
|
|
||||||
const QJsonObject serviceObject = serviceValue.toObject();
|
|
||||||
if (serviceObject.value(configKey::serviceType).toString() != serviceType::amneziaPremium) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const QJsonArray subscriptionPlans =
|
|
||||||
serviceObject.value(configKey::serviceDescription).toObject().value(configKey::subscriptionPlans).toArray();
|
|
||||||
for (const QJsonValue &planValue : subscriptionPlans) {
|
|
||||||
if (!planValue.isObject()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const QString storeProductId = planValue.toObject().value(configKey::storeProductId).toString();
|
|
||||||
if (storeProductId.isEmpty() || seenProductIds.contains(storeProductId)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
seenProductIds.insert(storeProductId);
|
|
||||||
productIds.append(storeProductId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return productIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
QHash<QString, StoreKitPlanQuote> buildStoreKitQuoteMap(const QList<QVariantMap> &fetchedProducts)
|
|
||||||
{
|
|
||||||
QHash<QString, StoreKitPlanQuote> quotesByProductId;
|
|
||||||
quotesByProductId.reserve(fetchedProducts.size());
|
|
||||||
|
|
||||||
for (const QVariantMap &productInfo : fetchedProducts) {
|
|
||||||
const QString productId = productInfo.value(QStringLiteral("productId")).toString();
|
|
||||||
if (productId.isEmpty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString displayPrice = productInfo.value(QStringLiteral("displayPrice")).toString();
|
|
||||||
if (displayPrice.isEmpty()) {
|
|
||||||
const QString price = productInfo.value(QStringLiteral("price")).toString();
|
|
||||||
const QString currencyCode = productInfo.value(QStringLiteral("currencyCode")).toString();
|
|
||||||
displayPrice = currencyCode.isEmpty() ? price : (price + QLatin1Char(' ') + currencyCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
StoreKitPlanQuote quote;
|
|
||||||
quote.displayPrice = displayPrice;
|
|
||||||
quote.priceAmount = productInfo.value(QStringLiteral("priceAmount")).toDouble();
|
|
||||||
quote.subscriptionBillingMonths = productInfo.value(QStringLiteral("subscriptionBillingMonths")).toDouble();
|
|
||||||
quote.displayPricePerMonth = productInfo.value(QStringLiteral("displayPricePerMonth")).toString();
|
|
||||||
quotesByProductId.insert(productId, quote);
|
|
||||||
}
|
|
||||||
|
|
||||||
return quotesByProductId;
|
|
||||||
}
|
|
||||||
|
|
||||||
void mergeStoreKitPricesIntoPremiumPlans(QJsonObject &data)
|
|
||||||
{
|
|
||||||
QJsonArray services = data.value(configKey::services).toArray();
|
|
||||||
if (services.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QStringList productIds = collectPremiumStoreProductIds(services);
|
|
||||||
if (productIds.isEmpty()) {
|
|
||||||
qInfo().noquote() << "[IAP] No store_product_id in premium plans; skip StoreKit merge into services payload";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QList<QVariantMap> fetchedProducts;
|
|
||||||
QEventLoop loop;
|
|
||||||
IosController::Instance()->fetchProducts(productIds,
|
|
||||||
[&](const QList<QVariantMap> &products, const QStringList &invalidIds,
|
|
||||||
const QString &errorString) {
|
|
||||||
if (!errorString.isEmpty()) {
|
|
||||||
qWarning().noquote() << "[IAP] StoreKit merge fetch:" << errorString;
|
|
||||||
}
|
|
||||||
if (!invalidIds.isEmpty()) {
|
|
||||||
qWarning().noquote() << "[IAP] Unknown App Store product ids:" << invalidIds;
|
|
||||||
}
|
|
||||||
fetchedProducts = products;
|
|
||||||
loop.quit();
|
|
||||||
});
|
|
||||||
loop.exec();
|
|
||||||
|
|
||||||
const QHash<QString, StoreKitPlanQuote> quotesByProductId = buildStoreKitQuoteMap(fetchedProducts);
|
|
||||||
|
|
||||||
for (int serviceIndex = 0; serviceIndex < services.size(); ++serviceIndex) {
|
|
||||||
QJsonObject serviceObject = services.at(serviceIndex).toObject();
|
|
||||||
if (serviceObject.value(configKey::serviceType).toString() != serviceType::amneziaPremium) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
QJsonObject descriptionObject = serviceObject.value(configKey::serviceDescription).toObject();
|
|
||||||
const QJsonArray sourcePlans = descriptionObject.value(configKey::subscriptionPlans).toArray();
|
|
||||||
|
|
||||||
QJsonArray mergedPlans;
|
|
||||||
double minMonthlyAmount = std::numeric_limits<double>::infinity();
|
|
||||||
QString minMonthlyDisplay;
|
|
||||||
|
|
||||||
for (const QJsonValue &planValue : sourcePlans) {
|
|
||||||
if (!planValue.isObject()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
QJsonObject planObject = planValue.toObject();
|
|
||||||
const QString storeProductId = planObject.value(configKey::storeProductId).toString();
|
|
||||||
if (storeProductId.isEmpty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto quoteIterator = quotesByProductId.constFind(storeProductId);
|
|
||||||
if (quoteIterator == quotesByProductId.cend()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bool isTrialPlan = planObject.value(configKey::isTrial).toBool();
|
|
||||||
const StoreKitPlanQuote "e = *quoteIterator;
|
|
||||||
planObject.insert(configKey::priceLabel, quote.displayPrice);
|
|
||||||
|
|
||||||
const double months = quote.subscriptionBillingMonths;
|
|
||||||
if (!isTrialPlan && months > kOneMonthThreshold && !quote.displayPricePerMonth.isEmpty()) {
|
|
||||||
planObject.insert(
|
|
||||||
configKey::subtitle,
|
|
||||||
QCoreApplication::translate("ApiConfigsController", "%1/mo", "IAP: price per month in plan subtitle")
|
|
||||||
.arg(quote.displayPricePerMonth));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isTrialPlan && quote.priceAmount > 0.0) {
|
|
||||||
const double monthsForMin = months > kMonthsFallbackThreshold ? months : 1.0;
|
|
||||||
const double monthly = quote.priceAmount / monthsForMin;
|
|
||||||
if (monthly < minMonthlyAmount - kMonthlyPriceEpsilon) {
|
|
||||||
minMonthlyAmount = monthly;
|
|
||||||
minMonthlyDisplay = !quote.displayPricePerMonth.isEmpty() ? quote.displayPricePerMonth : quote.displayPrice;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mergedPlans.append(planObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
descriptionObject.insert(configKey::subscriptionPlans, mergedPlans);
|
|
||||||
if (minMonthlyAmount < std::numeric_limits<double>::infinity() && !minMonthlyDisplay.isEmpty()) {
|
|
||||||
descriptionObject.insert(configKey::minPriceLabel,
|
|
||||||
QCoreApplication::translate("ApiConfigsController", "from %1 per month",
|
|
||||||
"IAP: card footer minimum monthly price from StoreKit")
|
|
||||||
.arg(minMonthlyDisplay));
|
|
||||||
}
|
|
||||||
serviceObject.insert(configKey::serviceDescription, descriptionObject);
|
|
||||||
services.replace(serviceIndex, serviceObject);
|
|
||||||
}
|
|
||||||
data.insert(configKey::services, services);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ApiConfigsController::ApiConfigsController(const QSharedPointer<ServersModel> &serversModel,
|
ApiConfigsController::ApiConfigsController(const QSharedPointer<ServersModel> &serversModel,
|
||||||
const QSharedPointer<ApiServicesModel> &apiServicesModel,
|
const QSharedPointer<ApiServicesModel> &apiServicesModel,
|
||||||
const QSharedPointer<ApiSubscriptionPlansModel> &subscriptionPlansModel,
|
|
||||||
const QSharedPointer<ApiBenefitsModel> &benefitsModel,
|
|
||||||
const std::shared_ptr<Settings> &settings, QObject *parent)
|
const std::shared_ptr<Settings> &settings, QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent), m_serversModel(serversModel), m_apiServicesModel(apiServicesModel), m_settings(settings)
|
||||||
, m_serversModel(serversModel)
|
|
||||||
, m_apiServicesModel(apiServicesModel)
|
|
||||||
, m_subscriptionPlansModel(subscriptionPlansModel)
|
|
||||||
, m_benefitsModel(benefitsModel)
|
|
||||||
, m_settings(settings)
|
|
||||||
{
|
{
|
||||||
connect(m_apiServicesModel.data(), &ApiServicesModel::serviceSelectionChanged, this, [this]() {
|
|
||||||
const ApiServicesModel::ApiServicesData serviceData = m_apiServicesModel->selectedServiceData();
|
|
||||||
m_subscriptionPlansModel->updateModel(serviceData.subscriptionPlansJson);
|
|
||||||
m_benefitsModel->updateModel(serviceData.benefits);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ApiConfigsController::exportVpnKey(const QString &fileName)
|
bool ApiConfigsController::exportVpnKey(const QString &fileName)
|
||||||
@@ -554,8 +366,6 @@ bool ApiConfigsController::fillAvailableServices()
|
|||||||
{
|
{
|
||||||
QJsonObject apiPayload;
|
QJsonObject apiPayload;
|
||||||
apiPayload[configKey::osVersion] = QSysInfo::productType();
|
apiPayload[configKey::osVersion] = QSysInfo::productType();
|
||||||
apiPayload[configKey::appVersion] = QString(APP_VERSION);
|
|
||||||
apiPayload[apiDefs::key::cliName] = QString(APPLICATION_NAME);
|
|
||||||
apiPayload[apiDefs::key::appLanguage] = m_settings->getAppLanguage().name().split("_").first();
|
apiPayload[apiDefs::key::appLanguage] = m_settings->getAppLanguage().name().split("_").first();
|
||||||
|
|
||||||
QByteArray responseBody;
|
QByteArray responseBody;
|
||||||
@@ -572,11 +382,51 @@ bool ApiConfigsController::fillAvailableServices()
|
|||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject data = QJsonDocument::fromJson(responseBody).object();
|
QJsonObject data = QJsonDocument::fromJson(responseBody).object();
|
||||||
|
|
||||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||||
mergeStoreKitPricesIntoPremiumPlans(data);
|
QEventLoop waitProducts;
|
||||||
|
bool productsFetched = false;
|
||||||
|
QString productPrice;
|
||||||
|
QString productCurrency;
|
||||||
|
|
||||||
|
IosController::Instance()->fetchProducts(QStringList() << QStringLiteral("amnezia_premium_6_month"),
|
||||||
|
[&](const QList<QVariantMap> &products,
|
||||||
|
const QStringList &invalidIds,
|
||||||
|
const QString &errorString) {
|
||||||
|
if (!errorString.isEmpty() || products.isEmpty()) {
|
||||||
|
qWarning().noquote() << "[IAP] Failed to fetch product price:" << errorString;
|
||||||
|
} else {
|
||||||
|
const auto &product = products.first();
|
||||||
|
productPrice = product.value("price").toString();
|
||||||
|
productCurrency = product.value("currencyCode").toString();
|
||||||
|
productsFetched = true;
|
||||||
|
qInfo().noquote() << "[IAP] Fetched product price:" << productPrice << productCurrency;
|
||||||
|
}
|
||||||
|
waitProducts.quit();
|
||||||
|
});
|
||||||
|
waitProducts.exec();
|
||||||
|
|
||||||
|
if (productsFetched && !productPrice.isEmpty()) {
|
||||||
|
QJsonArray services = data.value("services").toArray();
|
||||||
|
for (int i = 0; i < services.size(); ++i) {
|
||||||
|
QJsonObject service = services[i].toObject();
|
||||||
|
if (service.value(configKey::serviceType).toString() == serviceType::amneziaPremium) {
|
||||||
|
QJsonObject serviceInfo = service.value(configKey::serviceInfo).toObject();
|
||||||
|
QString formattedPrice = productPrice;
|
||||||
|
if (!productCurrency.isEmpty()) {
|
||||||
|
formattedPrice += " " + productCurrency;
|
||||||
|
}
|
||||||
|
serviceInfo["price"] = formattedPrice;
|
||||||
|
service[configKey::serviceInfo] = serviceInfo;
|
||||||
|
services[i] = service;
|
||||||
|
data["services"] = services;
|
||||||
|
qInfo().noquote() << "[IAP] Updated premium service price in data:" << formattedPrice;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
m_apiServicesModel->updateModel(data);
|
m_apiServicesModel->updateModel(data);
|
||||||
if (m_apiServicesModel->rowCount() > 0) {
|
if (m_apiServicesModel->rowCount() > 0) {
|
||||||
m_apiServicesModel->setServiceIndex(0);
|
m_apiServicesModel->setServiceIndex(0);
|
||||||
@@ -587,42 +437,39 @@ bool ApiConfigsController::fillAvailableServices()
|
|||||||
bool ApiConfigsController::importService()
|
bool ApiConfigsController::importService()
|
||||||
{
|
{
|
||||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||||
const bool isIosOrMacOsNe = true;
|
bool isIosOrMacOsNe = true;
|
||||||
#else
|
#else
|
||||||
const bool isIosOrMacOsNe = false;
|
bool isIosOrMacOsNe = false;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (m_apiServicesModel->getSelectedServiceType() == serviceType::amneziaPremium) {
|
if (m_apiServicesModel->getSelectedServiceType() == serviceType::amneziaPremium) {
|
||||||
if (isIosOrMacOsNe) {
|
if (isIosOrMacOsNe) {
|
||||||
return importPremiumFromAppStore(QString());
|
importSerivceFromAppStore();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
} else if (m_apiServicesModel->getSelectedServiceType() == serviceType::amneziaFree) {
|
} else {
|
||||||
return importFreeFromGateway();
|
importServiceFromGateway();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ApiConfigsController::importPremiumFromAppStore(const QString &storeProductId)
|
bool ApiConfigsController::importSerivceFromAppStore()
|
||||||
{
|
{
|
||||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||||
QString productId = storeProductId.trimmed();
|
|
||||||
if (productId.isEmpty()) {
|
|
||||||
productId = QStringLiteral("amnezia_premium_6_month");
|
|
||||||
}
|
|
||||||
|
|
||||||
bool purchaseOk = false;
|
bool purchaseOk = false;
|
||||||
QString originalTransactionId;
|
QString originalTransactionId;
|
||||||
QString storeTransactionId;
|
QString storeTransactionId;
|
||||||
QString purchasedStoreProductId;
|
QString storeProductId;
|
||||||
QString purchaseError;
|
QString purchaseError;
|
||||||
QEventLoop waitPurchase;
|
QEventLoop waitPurchase;
|
||||||
IosController::Instance()->purchaseProduct(productId,
|
IosController::Instance()->purchaseProduct(QStringLiteral("amnezia_premium_6_month"),
|
||||||
[&](bool success, const QString &transactionId, const QString &purchasedProductId,
|
[&](bool success, const QString &txId, const QString &purchasedProductId,
|
||||||
const QString &originalTransactionIdResponse, const QString &errorString) {
|
const QString &originalTxId, const QString &errorString) {
|
||||||
purchaseOk = success;
|
purchaseOk = success;
|
||||||
originalTransactionId = originalTransactionIdResponse;
|
originalTransactionId = originalTxId;
|
||||||
storeTransactionId = transactionId;
|
storeTransactionId = txId;
|
||||||
purchasedStoreProductId = purchasedProductId;
|
storeProductId = purchasedProductId;
|
||||||
purchaseError = errorString;
|
purchaseError = errorString;
|
||||||
waitPurchase.quit();
|
waitPurchase.quit();
|
||||||
});
|
});
|
||||||
@@ -634,7 +481,7 @@ bool ApiConfigsController::importPremiumFromAppStore(const QString &storeProduct
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
qInfo().noquote() << "[IAP] Purchase success. transactionId =" << storeTransactionId
|
qInfo().noquote() << "[IAP] Purchase success. transactionId =" << storeTransactionId
|
||||||
<< "originalTransactionId =" << originalTransactionId << "productId =" << purchasedStoreProductId;
|
<< "originalTransactionId =" << originalTransactionId << "productId =" << storeProductId;
|
||||||
|
|
||||||
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
||||||
QString(APP_VERSION),
|
QString(APP_VERSION),
|
||||||
@@ -658,26 +505,18 @@ bool ApiConfigsController::importPremiumFromAppStore(const QString &storeProduct
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int duplicateServerIndex = -1;
|
errorCode = importServiceFromBilling(responseBody, isTestPurchase);
|
||||||
errorCode = importServiceFromBilling(responseBody, isTestPurchase, duplicateServerIndex);
|
|
||||||
if (errorCode == ErrorCode::ApiConfigAlreadyAdded) {
|
|
||||||
emit installServerFromApiFinished(tr("This subscription has already been added"), duplicateServerIndex);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (errorCode != ErrorCode::NoError) {
|
if (errorCode != ErrorCode::NoError) {
|
||||||
emit errorOccurred(errorCode);
|
emit errorOccurred(errorCode);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
emit installServerFromApiFinished(
|
|
||||||
tr("%1 has been added to the app").arg(m_apiServicesModel->getSelectedServiceName()));
|
emit installServerFromApiFinished(tr("%1 installed successfully.").arg(m_apiServicesModel->getSelectedServiceName()));
|
||||||
return true;
|
|
||||||
#else
|
|
||||||
Q_UNUSED(storeProductId);
|
|
||||||
return false;
|
|
||||||
#endif
|
#endif
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ApiConfigsController::restoreServiceFromAppStore()
|
bool ApiConfigsController::restoreSerivceFromAppStore()
|
||||||
{
|
{
|
||||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||||
const QString premiumServiceType = QStringLiteral("amnezia-premium");
|
const QString premiumServiceType = QStringLiteral("amnezia-premium");
|
||||||
@@ -693,12 +532,20 @@ bool ApiConfigsController::restoreServiceFromAppStore()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const int premiumServiceIndex = m_apiServicesModel->serviceIndexForType(premiumServiceType);
|
// Ensure we have a valid premium selection for gateway requests
|
||||||
if (premiumServiceIndex < 0) {
|
bool premiumSelected = false;
|
||||||
|
for (int i = 0; i < m_apiServicesModel->rowCount(); ++i) {
|
||||||
|
m_apiServicesModel->setServiceIndex(i);
|
||||||
|
if (m_apiServicesModel->getSelectedServiceType() == premiumServiceType) {
|
||||||
|
premiumSelected = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!premiumSelected) {
|
||||||
emit errorOccurred(ErrorCode::ApiServicesMissingError);
|
emit errorOccurred(ErrorCode::ApiServicesMissingError);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
m_apiServicesModel->setServiceIndex(premiumServiceIndex);
|
|
||||||
|
|
||||||
bool restoreSuccess = false;
|
bool restoreSuccess = false;
|
||||||
QList<QVariantMap> restoredTransactions;
|
QList<QVariantMap> restoredTransactions;
|
||||||
@@ -720,23 +567,15 @@ bool ApiConfigsController::restoreServiceFromAppStore()
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (restoredTransactions.isEmpty()) {
|
if (restoredTransactions.isEmpty()) {
|
||||||
qInfo().noquote() << "[IAP] Restore completed, but no active entitlements found";
|
qInfo().noquote() << "[IAP] Restore completed, but no transactions were returned";
|
||||||
emit errorOccurred(ErrorCode::ApiNoPurchasedSubscriptionsError);
|
emit errorOccurred(ErrorCode::ApiPurchaseError);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool isTestPurchase = IosController::Instance()->isTestFlight();
|
|
||||||
const QString serviceType = m_apiServicesModel->getSelectedServiceType();
|
|
||||||
const QString serviceProtocol = m_apiServicesModel->getSelectedServiceProtocol();
|
|
||||||
const QString countryCode = m_apiServicesModel->getCountryCode();
|
|
||||||
const QString appLanguage = m_settings->getAppLanguage().name().split("_").first();
|
|
||||||
const QString installationUuid = m_settings->getInstallationUuid(true);
|
|
||||||
|
|
||||||
bool hasInstalledConfig = false;
|
bool hasInstalledConfig = false;
|
||||||
bool duplicateConfigAlreadyPresent = false;
|
bool duplicateConfigAlreadyPresent = false;
|
||||||
int duplicateServerIndex = -1;
|
int duplicateCount = 0;
|
||||||
QSet<QString> processedOriginalTransactionIds;
|
QSet<QString> processedTransactions;
|
||||||
|
|
||||||
for (const QVariantMap &transaction : restoredTransactions) {
|
for (const QVariantMap &transaction : restoredTransactions) {
|
||||||
const QString originalTransactionId = transaction.value(QStringLiteral("originalTransactionId")).toString();
|
const QString originalTransactionId = transaction.value(QStringLiteral("originalTransactionId")).toString();
|
||||||
const QString transactionId = transaction.value(QStringLiteral("transactionId")).toString();
|
const QString transactionId = transaction.value(QStringLiteral("transactionId")).toString();
|
||||||
@@ -747,28 +586,28 @@ bool ApiConfigsController::restoreServiceFromAppStore()
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (processedOriginalTransactionIds.contains(originalTransactionId)) {
|
if (processedTransactions.contains(originalTransactionId)) {
|
||||||
qInfo().noquote() << "[IAP] Skipping duplicate restored transaction" << originalTransactionId;
|
duplicateCount++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
processedOriginalTransactionIds.insert(originalTransactionId);
|
processedTransactions.insert(originalTransactionId);
|
||||||
|
|
||||||
qInfo().noquote() << "[IAP] Restoring subscription. transactionId =" << transactionId
|
qInfo().noquote() << "[IAP] Restoring subscription. transactionId =" << transactionId
|
||||||
<< "originalTransactionId =" << originalTransactionId << "productId =" << productId;
|
<< "originalTransactionId =" << originalTransactionId << "productId =" << productId;
|
||||||
|
|
||||||
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
||||||
QString(APP_VERSION),
|
QString(APP_VERSION),
|
||||||
appLanguage,
|
m_settings->getAppLanguage().name().split("_").first(),
|
||||||
installationUuid,
|
m_settings->getInstallationUuid(true),
|
||||||
countryCode,
|
m_apiServicesModel->getCountryCode(),
|
||||||
"",
|
"",
|
||||||
serviceType,
|
m_apiServicesModel->getSelectedServiceType(),
|
||||||
serviceProtocol,
|
m_apiServicesModel->getSelectedServiceProtocol(),
|
||||||
QJsonObject() };
|
QJsonObject() };
|
||||||
|
|
||||||
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
|
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
|
||||||
apiPayload[apiDefs::key::transactionId] = originalTransactionId;
|
apiPayload[apiDefs::key::transactionId] = originalTransactionId;
|
||||||
|
auto isTestPurchase = IosController::Instance()->isTestFlight();
|
||||||
QByteArray responseBody;
|
QByteArray responseBody;
|
||||||
ErrorCode errorCode = executeRequest(QString("%1v1/subscriptions"), apiPayload, responseBody, isTestPurchase);
|
ErrorCode errorCode = executeRequest(QString("%1v1/subscriptions"), apiPayload, responseBody, isTestPurchase);
|
||||||
if (errorCode != ErrorCode::NoError) {
|
if (errorCode != ErrorCode::NoError) {
|
||||||
@@ -777,42 +616,34 @@ bool ApiConfigsController::restoreServiceFromAppStore()
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
int currentDuplicateServerIndex = -1;
|
ErrorCode installError = importServiceFromBilling(responseBody, isTestPurchase);
|
||||||
errorCode = importServiceFromBilling(responseBody, isTestPurchase, currentDuplicateServerIndex);
|
|
||||||
if (errorCode == ErrorCode::ApiConfigAlreadyAdded) {
|
if (errorCode == ErrorCode::ApiConfigAlreadyAdded) {
|
||||||
duplicateConfigAlreadyPresent = true;
|
duplicateConfigAlreadyPresent = true;
|
||||||
if (duplicateServerIndex < 0) {
|
qInfo().noquote() << "[IAP] Skipping restored transaction" << originalTransactionId
|
||||||
duplicateServerIndex = currentDuplicateServerIndex;
|
<< "because subscription config with the same vpn_key already exists";
|
||||||
}
|
} else if (errorCode != ErrorCode::NoError) {
|
||||||
qInfo().noquote() << "[IAP] Subscription config with the same vpn_key already exists" << originalTransactionId;
|
qWarning().noquote() << "[IAP] Failed to process restored subscription response for transaction" << originalTransactionId;
|
||||||
continue;
|
} else {
|
||||||
|
hasInstalledConfig = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errorCode != ErrorCode::NoError) {
|
|
||||||
qWarning().noquote() << "[IAP] Failed to process restored subscription response for transaction" << originalTransactionId
|
|
||||||
<< "errorCode =" << static_cast<int>(errorCode);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasInstalledConfig = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasInstalledConfig) {
|
if (!hasInstalledConfig) {
|
||||||
if (duplicateConfigAlreadyPresent) {
|
const ErrorCode restoreError = duplicateConfigAlreadyPresent ? ErrorCode::ApiConfigAlreadyAdded : ErrorCode::ApiPurchaseError;
|
||||||
emit installServerFromApiFinished(tr("This subscription has already been added"), duplicateServerIndex);
|
emit errorOccurred(restoreError);
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
emit errorOccurred(ErrorCode::ApiPurchaseError);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
emit installServerFromApiFinished(tr("Subscription restored successfully."));
|
emit installServerFromApiFinished(tr("Subscription restored successfully."));
|
||||||
|
if (duplicateCount > 0) {
|
||||||
|
qInfo().noquote() << "[IAP] Skipped" << duplicateCount
|
||||||
|
<< "duplicate restored transactions for original transaction IDs already processed";
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ApiConfigsController::importFreeFromGateway()
|
bool ApiConfigsController::importServiceFromGateway()
|
||||||
{
|
{
|
||||||
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
||||||
QString(APP_VERSION),
|
QString(APP_VERSION),
|
||||||
@@ -864,72 +695,6 @@ bool ApiConfigsController::importFreeFromGateway()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ApiConfigsController::importTrialFromGateway(const QString &email)
|
|
||||||
{
|
|
||||||
emit trialEmailError(QString());
|
|
||||||
|
|
||||||
const QString trimmedEmail = email.trimmed();
|
|
||||||
if (trimmedEmail.isEmpty()) {
|
|
||||||
emit errorOccurred(ErrorCode::ApiConfigEmptyError);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
|
||||||
QString(APP_VERSION),
|
|
||||||
m_settings->getAppLanguage().name().split("_").first(),
|
|
||||||
m_settings->getInstallationUuid(true),
|
|
||||||
m_apiServicesModel->getCountryCode(),
|
|
||||||
"",
|
|
||||||
m_apiServicesModel->getSelectedServiceType(),
|
|
||||||
m_apiServicesModel->getSelectedServiceProtocol(),
|
|
||||||
QJsonObject() };
|
|
||||||
|
|
||||||
ProtocolData protocolData = generateProtocolData(gatewayRequestData.serviceProtocol);
|
|
||||||
|
|
||||||
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
|
|
||||||
appendProtocolDataToApiPayload(gatewayRequestData.serviceProtocol, protocolData, apiPayload);
|
|
||||||
apiPayload.insert(apiDefs::key::email, trimmedEmail);
|
|
||||||
|
|
||||||
QByteArray responseBody;
|
|
||||||
ErrorCode errorCode = executeRequest(QString("%1v1/trial"), apiPayload, responseBody);
|
|
||||||
if (errorCode != ErrorCode::NoError) {
|
|
||||||
if (errorCode == ErrorCode::ApiTrialAlreadyUsedError) {
|
|
||||||
emit trialEmailError(tr("This email address has already been used to activate a trial. If you like the service, you can upgrade to Premium"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
emit errorOccurred(errorCode);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
QJsonObject responseObject = QJsonDocument::fromJson(responseBody).object();
|
|
||||||
QString key = responseObject.value(apiDefs::key::config).toString();
|
|
||||||
if (key.isEmpty()) {
|
|
||||||
qWarning().noquote() << "[Trial] trial response does not contain config field";
|
|
||||||
emit errorOccurred(ErrorCode::ApiConfigEmptyError);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
key.replace(QStringLiteral("vpn://"), QString());
|
|
||||||
QByteArray configBytes = QByteArray::fromBase64(key.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
|
|
||||||
QByteArray uncompressed = qUncompress(configBytes);
|
|
||||||
if (!uncompressed.isEmpty()) {
|
|
||||||
configBytes = uncompressed;
|
|
||||||
}
|
|
||||||
if (configBytes.isEmpty()) {
|
|
||||||
qWarning().noquote() << "[Trial] trial response config payload is empty";
|
|
||||||
emit errorOccurred(ErrorCode::ApiConfigEmptyError);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
QJsonObject configObject = QJsonDocument::fromJson(configBytes).object();
|
|
||||||
quint16 crc = qChecksum(QJsonDocument(configObject).toJson());
|
|
||||||
configObject.insert(config_key::crc, crc);
|
|
||||||
m_serversModel->addServer(configObject);
|
|
||||||
|
|
||||||
emit installServerFromApiFinished(tr("%1 installed successfully.").arg(m_apiServicesModel->getSelectedServiceName()));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const QString &newCountryCode, const QString &newCountryName,
|
bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const QString &newCountryCode, const QString &newCountryName,
|
||||||
bool reloadServiceConfig)
|
bool reloadServiceConfig)
|
||||||
{
|
{
|
||||||
@@ -956,7 +721,6 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool isTestPurchase = apiConfig.value(apiDefs::key::isTestPurchase).toBool(false);
|
bool isTestPurchase = apiConfig.value(apiDefs::key::isTestPurchase).toBool(false);
|
||||||
bool wasSubscriptionExpired = m_serversModel->data(serverIndex, ServersModel::IsSubscriptionExpiredRole).toBool();
|
|
||||||
QByteArray responseBody;
|
QByteArray responseBody;
|
||||||
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody, isTestPurchase);
|
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody, isTestPurchase);
|
||||||
|
|
||||||
@@ -973,12 +737,6 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
|
|||||||
newApiConfig.insert(configKey::serviceType, apiConfig.value(configKey::serviceType));
|
newApiConfig.insert(configKey::serviceType, apiConfig.value(configKey::serviceType));
|
||||||
newApiConfig.insert(configKey::serviceProtocol, apiConfig.value(configKey::serviceProtocol));
|
newApiConfig.insert(configKey::serviceProtocol, apiConfig.value(configKey::serviceProtocol));
|
||||||
newApiConfig.insert(apiDefs::key::vpnKey, apiConfig.value(apiDefs::key::vpnKey));
|
newApiConfig.insert(apiDefs::key::vpnKey, apiConfig.value(apiDefs::key::vpnKey));
|
||||||
if (apiConfig.contains(apiDefs::key::isInAppPurchase)) {
|
|
||||||
newApiConfig.insert(apiDefs::key::isInAppPurchase, apiConfig.value(apiDefs::key::isInAppPurchase));
|
|
||||||
}
|
|
||||||
if (apiConfig.contains(apiDefs::key::isTestPurchase)) {
|
|
||||||
newApiConfig.insert(apiDefs::key::isTestPurchase, apiConfig.value(apiDefs::key::isTestPurchase));
|
|
||||||
}
|
|
||||||
|
|
||||||
newServerConfig.insert(configKey::apiConfig, newApiConfig);
|
newServerConfig.insert(configKey::apiConfig, newApiConfig);
|
||||||
newServerConfig.insert(configKey::authData, gatewayRequestData.authData);
|
newServerConfig.insert(configKey::authData, gatewayRequestData.authData);
|
||||||
@@ -989,11 +747,6 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
|
|||||||
newServerConfig.insert(config_key::nameOverriddenByUser, true);
|
newServerConfig.insert(config_key::nameOverriddenByUser, true);
|
||||||
}
|
}
|
||||||
m_serversModel->editServer(newServerConfig, serverIndex);
|
m_serversModel->editServer(newServerConfig, serverIndex);
|
||||||
|
|
||||||
if (wasSubscriptionExpired) {
|
|
||||||
emit subscriptionRefreshNeeded();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reloadServiceConfig) {
|
if (reloadServiceConfig) {
|
||||||
emit reloadServerFromApiFinished(tr("API config reloaded"));
|
emit reloadServerFromApiFinished(tr("API config reloaded"));
|
||||||
} else if (newCountryName.isEmpty()) {
|
} else if (newCountryName.isEmpty()) {
|
||||||
@@ -1003,18 +756,7 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
if (errorCode == ErrorCode::ApiSubscriptionExpiredError) {
|
emit errorOccurred(errorCode);
|
||||||
if (!apiConfig.value(apiDefs::key::isInAppPurchase).toBool(false)) {
|
|
||||||
apiConfig.insert(apiDefs::key::subscriptionExpiredByServer, true);
|
|
||||||
serverConfig.insert(configKey::apiConfig, apiConfig);
|
|
||||||
m_serversModel->editServer(serverConfig, serverIndex);
|
|
||||||
emit subscriptionExpiredOnServer();
|
|
||||||
} else {
|
|
||||||
emit errorOccurred(errorCode);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
emit errorOccurred(errorCode);
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1027,7 +769,7 @@ bool ApiConfigsController::updateServiceFromTelegram(const int serverIndex)
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs,
|
GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs,
|
||||||
m_settings->isStrictKillSwitchEnabled(), m_settings);
|
m_settings->isStrictKillSwitchEnabled());
|
||||||
|
|
||||||
auto serverConfig = m_serversModel->getServerConfig(serverIndex);
|
auto serverConfig = m_serversModel->getServerConfig(serverIndex);
|
||||||
auto installationUuid = m_settings->getInstallationUuid(true);
|
auto installationUuid = m_settings->getInstallationUuid(true);
|
||||||
@@ -1200,63 +942,43 @@ QString ApiConfigsController::getVpnKey()
|
|||||||
return m_vpnKey;
|
return m_vpnKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
ErrorCode ApiConfigsController::importServiceFromBilling(const QByteArray &responseBody, const bool isTestPurchase,
|
ErrorCode ApiConfigsController::importServiceFromBilling(const QByteArray &responseBody, const bool isTestPurchase)
|
||||||
int &duplicateServerIndex)
|
|
||||||
{
|
{
|
||||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
#ifdef Q_OS_IOS
|
||||||
duplicateServerIndex = -1;
|
|
||||||
QJsonObject responseObject = QJsonDocument::fromJson(responseBody).object();
|
QJsonObject responseObject = QJsonDocument::fromJson(responseBody).object();
|
||||||
const QString rawVpnKey = responseObject.value(QStringLiteral("key")).toString();
|
QString key = responseObject.value(QStringLiteral("key")).toString();
|
||||||
if (rawVpnKey.isEmpty()) {
|
if (key.isEmpty()) {
|
||||||
qWarning().noquote() << "[IAP] Subscription response does not contain a key field";
|
qWarning().noquote() << "[IAP] Subscription response does not contain a key field";
|
||||||
return ErrorCode::ApiPurchaseError;
|
return ErrorCode::ApiPurchaseError;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString normalizedVpnKey = rawVpnKey;
|
if (m_serversModel->hasServerWithVpnKey(key)) {
|
||||||
normalizedVpnKey.replace(QStringLiteral("vpn://"), QString());
|
|
||||||
|
|
||||||
duplicateServerIndex = m_serversModel->indexOfServerWithVpnKey(normalizedVpnKey);
|
|
||||||
if (duplicateServerIndex >= 0) {
|
|
||||||
qInfo().noquote() << "[IAP] Subscription config with the same vpn_key already exists";
|
qInfo().noquote() << "[IAP] Subscription config with the same vpn_key already exists";
|
||||||
return ErrorCode::ApiConfigAlreadyAdded;
|
return ErrorCode::ApiConfigAlreadyAdded;
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray configPayload =
|
QString normalizedKey = key;
|
||||||
QByteArray::fromBase64(normalizedVpnKey.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
|
normalizedKey.replace(QStringLiteral("vpn://"), QString());
|
||||||
QByteArray configUncompressed = qUncompress(configPayload);
|
|
||||||
const bool payloadWasCompressed = !configUncompressed.isEmpty();
|
QByteArray configString = QByteArray::fromBase64(normalizedKey.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
|
||||||
if (payloadWasCompressed) {
|
QByteArray configUncompressed = qUncompress(configString);
|
||||||
configPayload = configUncompressed;
|
if (!configUncompressed.isEmpty()) {
|
||||||
|
configString = configUncompressed;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (configPayload.isEmpty()) {
|
if (configString.isEmpty()) {
|
||||||
qWarning().noquote() << "[IAP] Subscription response config payload is empty";
|
qWarning().noquote() << "[IAP] Subscription response config payload is empty";
|
||||||
return ErrorCode::ApiPurchaseError;
|
return ErrorCode::ApiPurchaseError;
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject configObject = QJsonDocument::fromJson(configPayload).object();
|
QJsonObject configObject = QJsonDocument::fromJson(configString).object();
|
||||||
|
|
||||||
auto apiConfig = configObject.value(apiDefs::key::apiConfig).toObject();
|
|
||||||
apiConfig.insert(apiDefs::key::isTestPurchase, isTestPurchase);
|
|
||||||
apiConfig.insert(apiDefs::key::isInAppPurchase, true);
|
|
||||||
configObject.insert(apiDefs::key::apiConfig, apiConfig);
|
|
||||||
|
|
||||||
configPayload = QJsonDocument(configObject).toJson();
|
|
||||||
if (payloadWasCompressed) {
|
|
||||||
configPayload = qCompress(configPayload, 8);
|
|
||||||
}
|
|
||||||
normalizedVpnKey = QString(configPayload.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals));
|
|
||||||
|
|
||||||
duplicateServerIndex = m_serversModel->indexOfServerWithVpnKey(normalizedVpnKey);
|
|
||||||
if (duplicateServerIndex >= 0) {
|
|
||||||
qInfo().noquote() << "[IAP] Subscription config with the same vpn_key already exists";
|
|
||||||
return ErrorCode::ApiConfigAlreadyAdded;
|
|
||||||
}
|
|
||||||
|
|
||||||
apiConfig.insert(apiDefs::key::vpnKey, normalizedVpnKey);
|
|
||||||
configObject.insert(apiDefs::key::apiConfig, apiConfig);
|
|
||||||
|
|
||||||
quint16 crc = qChecksum(QJsonDocument(configObject).toJson());
|
quint16 crc = qChecksum(QJsonDocument(configObject).toJson());
|
||||||
|
auto apiConfig = configObject.value(apiDefs::key::apiConfig).toObject();
|
||||||
|
apiConfig[apiDefs::key::vpnKey] = normalizedKey;
|
||||||
|
apiConfig[apiDefs::key::isTestPurchase] = isTestPurchase;
|
||||||
|
|
||||||
|
configObject.insert(apiDefs::key::apiConfig, apiConfig);
|
||||||
configObject.insert(config_key::crc, crc);
|
configObject.insert(config_key::crc, crc);
|
||||||
m_serversModel->addServer(configObject);
|
m_serversModel->addServer(configObject);
|
||||||
|
|
||||||
@@ -1264,7 +986,6 @@ ErrorCode ApiConfigsController::importServiceFromBilling(const QByteArray &respo
|
|||||||
#else
|
#else
|
||||||
Q_UNUSED(responseBody)
|
Q_UNUSED(responseBody)
|
||||||
Q_UNUSED(isTestPurchase)
|
Q_UNUSED(isTestPurchase)
|
||||||
duplicateServerIndex = -1;
|
|
||||||
return ErrorCode::NoError;
|
return ErrorCode::NoError;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -1273,6 +994,6 @@ ErrorCode ApiConfigsController::executeRequest(const QString &endpoint, const QJ
|
|||||||
bool isTestPurchase)
|
bool isTestPurchase)
|
||||||
{
|
{
|
||||||
GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase), m_settings->isDevGatewayEnv(isTestPurchase),
|
GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase), m_settings->isDevGatewayEnv(isTestPurchase),
|
||||||
apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled(), m_settings);
|
apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled());
|
||||||
return gatewayController.post(endpoint, apiPayload, responseBody);
|
return gatewayController.post(endpoint, apiPayload, responseBody);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
#ifndef APICONFIGSCONTROLLER_H
|
#ifndef APICONFIGSCONTROLLER_H
|
||||||
#define APICONFIGSCONTROLLER_H
|
#define APICONFIGSCONTROLLER_H
|
||||||
|
|
||||||
#include <QList>
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
|
||||||
#include "configurators/openvpn_configurator.h"
|
#include "configurators/openvpn_configurator.h"
|
||||||
#include "ui/models/api/apiBenefitsModel.h"
|
|
||||||
#include "ui/models/api/apiServicesModel.h"
|
#include "ui/models/api/apiServicesModel.h"
|
||||||
#include "ui/models/api/apiSubscriptionPlansModel.h"
|
|
||||||
#include "ui/models/servers_model.h"
|
#include "ui/models/servers_model.h"
|
||||||
|
|
||||||
class ApiConfigsController : public QObject
|
class ApiConfigsController : public QObject
|
||||||
@@ -15,9 +12,7 @@ class ApiConfigsController : public QObject
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
ApiConfigsController(const QSharedPointer<ServersModel> &serversModel, const QSharedPointer<ApiServicesModel> &apiServicesModel,
|
ApiConfigsController(const QSharedPointer<ServersModel> &serversModel, const QSharedPointer<ApiServicesModel> &apiServicesModel,
|
||||||
const QSharedPointer<ApiSubscriptionPlansModel> &subscriptionPlansModel,
|
const std::shared_ptr<Settings> &settings, QObject *parent = nullptr);
|
||||||
const QSharedPointer<ApiBenefitsModel> &benefitsModel, const std::shared_ptr<Settings> &settings,
|
|
||||||
QObject *parent = nullptr);
|
|
||||||
|
|
||||||
Q_PROPERTY(QList<QString> qrCodes READ getQrCodes NOTIFY vpnKeyExportReady)
|
Q_PROPERTY(QList<QString> qrCodes READ getQrCodes NOTIFY vpnKeyExportReady)
|
||||||
Q_PROPERTY(int qrCodesCount READ getQrCodesCount NOTIFY vpnKeyExportReady)
|
Q_PROPERTY(int qrCodesCount READ getQrCodesCount NOTIFY vpnKeyExportReady)
|
||||||
@@ -32,10 +27,9 @@ public slots:
|
|||||||
|
|
||||||
bool fillAvailableServices();
|
bool fillAvailableServices();
|
||||||
bool importService();
|
bool importService();
|
||||||
bool importPremiumFromAppStore(const QString &storeProductId);
|
bool importSerivceFromAppStore();
|
||||||
bool restoreServiceFromAppStore();
|
bool restoreSerivceFromAppStore();
|
||||||
bool importFreeFromGateway();
|
bool importServiceFromGateway();
|
||||||
bool importTrialFromGateway(const QString &email);
|
|
||||||
bool updateServiceFromGateway(const int serverIndex, const QString &newCountryCode, const QString &newCountryName,
|
bool updateServiceFromGateway(const int serverIndex, const QString &newCountryCode, const QString &newCountryName,
|
||||||
bool reloadServiceConfig = false);
|
bool reloadServiceConfig = false);
|
||||||
bool updateServiceFromTelegram(const int serverIndex);
|
bool updateServiceFromTelegram(const int serverIndex);
|
||||||
@@ -49,11 +43,8 @@ public slots:
|
|||||||
|
|
||||||
signals:
|
signals:
|
||||||
void errorOccurred(ErrorCode errorCode);
|
void errorOccurred(ErrorCode errorCode);
|
||||||
void trialEmailError(const QString &message);
|
|
||||||
void subscriptionExpiredOnServer();
|
|
||||||
void subscriptionRefreshNeeded();
|
|
||||||
|
|
||||||
void installServerFromApiFinished(const QString &message, int preferredDefaultServerIndex = -1);
|
void installServerFromApiFinished(const QString &message);
|
||||||
void changeApiCountryFinished(const QString &message);
|
void changeApiCountryFinished(const QString &message);
|
||||||
void reloadServerFromApiFinished(const QString &message);
|
void reloadServerFromApiFinished(const QString &message);
|
||||||
void updateServerFromApiFinished();
|
void updateServerFromApiFinished();
|
||||||
@@ -66,7 +57,7 @@ private:
|
|||||||
QString getVpnKey();
|
QString getVpnKey();
|
||||||
|
|
||||||
ErrorCode executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody, bool isTestPurchase = false);
|
ErrorCode executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody, bool isTestPurchase = false);
|
||||||
ErrorCode importServiceFromBilling(const QByteArray &responseBody, const bool isTestPurchase, int &duplicateServerIndex);
|
ErrorCode importServiceFromBilling(const QByteArray &responseBody, const bool isTestPurchase);
|
||||||
|
|
||||||
QList<QString> m_qrCodes;
|
QList<QString> m_qrCodes;
|
||||||
QString m_vpnKey;
|
QString m_vpnKey;
|
||||||
@@ -74,9 +65,6 @@ private:
|
|||||||
QSharedPointer<ServersModel> m_serversModel;
|
QSharedPointer<ServersModel> m_serversModel;
|
||||||
QSharedPointer<ApiServicesModel> m_apiServicesModel;
|
QSharedPointer<ApiServicesModel> m_apiServicesModel;
|
||||||
std::shared_ptr<Settings> m_settings;
|
std::shared_ptr<Settings> m_settings;
|
||||||
|
|
||||||
QSharedPointer<ApiSubscriptionPlansModel> m_subscriptionPlansModel;
|
|
||||||
QSharedPointer<ApiBenefitsModel> m_benefitsModel;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif // APICONFIGSCONTROLLER_H
|
||||||
|
|||||||
@@ -32,8 +32,7 @@ void ApiNewsController::fetchNews(bool showError)
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto gatewayController = QSharedPointer<GatewayController>::create(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(),
|
auto gatewayController = QSharedPointer<GatewayController>::create(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(),
|
||||||
apiDefs::requestTimeoutMsecs,
|
apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled());
|
||||||
m_settings->isStrictKillSwitchEnabled(), m_settings);
|
|
||||||
QJsonObject payload;
|
QJsonObject payload;
|
||||||
payload.insert("locale", m_settings->getAppLanguage().name().split("_").first());
|
payload.insert("locale", m_settings->getAppLanguage().name().split("_").first());
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
#include "apiSettingsController.h"
|
#include "apiSettingsController.h"
|
||||||
|
|
||||||
#include <QEventLoop>
|
#include <QEventLoop>
|
||||||
#include <QJsonDocument>
|
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
|
||||||
#include "core/api/apiUtils.h"
|
#include "core/api/apiUtils.h"
|
||||||
@@ -23,19 +22,6 @@ namespace
|
|||||||
}
|
}
|
||||||
|
|
||||||
const int requestTimeoutMsecs = 12 * 1000; // 12 secs
|
const int requestTimeoutMsecs = 12 * 1000; // 12 secs
|
||||||
|
|
||||||
QString getSubscriptionStatusForRenewal(const QSharedPointer<ApiAccountInfoModel> &accountInfoModel)
|
|
||||||
{
|
|
||||||
if (!accountInfoModel.isNull() && accountInfoModel->data(QStringLiteral("isSubscriptionExpired")).toBool()) {
|
|
||||||
return QStringLiteral("expired");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!accountInfoModel.isNull() && accountInfoModel->data(QStringLiteral("isSubscriptionExpiringSoon")).toBool()) {
|
|
||||||
return QStringLiteral("expire_soon");
|
|
||||||
}
|
|
||||||
|
|
||||||
return QStringLiteral("active");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ApiSettingsController::ApiSettingsController(const QSharedPointer<ServersModel> &serversModel,
|
ApiSettingsController::ApiSettingsController(const QSharedPointer<ServersModel> &serversModel,
|
||||||
@@ -71,7 +57,7 @@ bool ApiSettingsController::getAccountInfo(bool reload)
|
|||||||
|
|
||||||
bool isTestPurchase = apiConfig.value(apiDefs::key::isTestPurchase).toBool(false);
|
bool isTestPurchase = apiConfig.value(apiDefs::key::isTestPurchase).toBool(false);
|
||||||
GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase), m_settings->isDevGatewayEnv(isTestPurchase),
|
GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase), m_settings->isDevGatewayEnv(isTestPurchase),
|
||||||
requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled(), m_settings);
|
requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled());
|
||||||
|
|
||||||
QJsonObject apiPayload;
|
QJsonObject apiPayload;
|
||||||
apiPayload[configKey::userCountryCode] = apiConfig.value(configKey::userCountryCode).toString();
|
apiPayload[configKey::userCountryCode] = apiConfig.value(configKey::userCountryCode).toString();
|
||||||
@@ -99,43 +85,6 @@ bool ApiSettingsController::getAccountInfo(bool reload)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ApiSettingsController::getRenewalLink()
|
|
||||||
{
|
|
||||||
auto processedIndex = m_serversModel->getProcessedServerIndex();
|
|
||||||
auto serverConfig = m_serversModel->getServerConfig(processedIndex);
|
|
||||||
auto apiConfig = serverConfig.value(configKey::apiConfig).toObject();
|
|
||||||
auto authData = serverConfig.value(configKey::authData).toObject();
|
|
||||||
|
|
||||||
bool isTestPurchase = apiConfig.value(apiDefs::key::isTestPurchase).toBool(false);
|
|
||||||
auto gatewayController = QSharedPointer<GatewayController>::create(m_settings->getGatewayEndpoint(isTestPurchase),
|
|
||||||
m_settings->isDevGatewayEnv(isTestPurchase),
|
|
||||||
requestTimeoutMsecs,
|
|
||||||
m_settings->isStrictKillSwitchEnabled(), m_settings);
|
|
||||||
|
|
||||||
QJsonObject apiPayload;
|
|
||||||
apiPayload[configKey::userCountryCode] = apiConfig.value(configKey::userCountryCode).toString();
|
|
||||||
apiPayload[configKey::serviceType] = apiConfig.value(configKey::serviceType).toString();
|
|
||||||
apiPayload[configKey::authData] = authData;
|
|
||||||
apiPayload[apiDefs::key::cliVersion] = QString(APP_VERSION);
|
|
||||||
apiPayload[apiDefs::key::appLanguage] = m_settings->getAppLanguage().name().split("_").first();
|
|
||||||
apiPayload[apiDefs::key::subscriptionStatus] = getSubscriptionStatusForRenewal(m_apiAccountInfoModel);
|
|
||||||
|
|
||||||
auto future = gatewayController->postAsync(QString("%1v1/renewal_link"), apiPayload);
|
|
||||||
future.then(this, [this, gatewayController](QPair<ErrorCode, QByteArray> result) {
|
|
||||||
auto [errorCode, responseBody] = result;
|
|
||||||
if (errorCode != ErrorCode::NoError) {
|
|
||||||
emit errorOccurred(errorCode);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QJsonObject responseJson = QJsonDocument::fromJson(responseBody).object();
|
|
||||||
QString url = responseJson.value("renewal_url").toString();
|
|
||||||
if (!url.isEmpty()) {
|
|
||||||
emit renewalLinkReceived(url);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void ApiSettingsController::updateApiCountryModel()
|
void ApiSettingsController::updateApiCountryModel()
|
||||||
{
|
{
|
||||||
m_apiCountryModel->updateModel(m_apiAccountInfoModel->getAvailableCountries(), "");
|
m_apiCountryModel->updateModel(m_apiAccountInfoModel->getAvailableCountries(), "");
|
||||||
|
|||||||
@@ -21,11 +21,9 @@ public slots:
|
|||||||
bool getAccountInfo(bool reload);
|
bool getAccountInfo(bool reload);
|
||||||
void updateApiCountryModel();
|
void updateApiCountryModel();
|
||||||
void updateApiDevicesModel();
|
void updateApiDevicesModel();
|
||||||
void getRenewalLink();
|
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void errorOccurred(ErrorCode errorCode);
|
void errorOccurred(ErrorCode errorCode);
|
||||||
void renewalLinkReceived(const QString &url);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QSharedPointer<ServersModel> m_serversModel;
|
QSharedPointer<ServersModel> m_serversModel;
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
#include "connectionController.h"
|
#include "connectionController.h"
|
||||||
|
|
||||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(MACOS_NE)
|
|
||||||
#include <QGuiApplication>
|
|
||||||
#else
|
|
||||||
#include <QApplication>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include "amnezia_application.h"
|
|
||||||
#include "utilities.h"
|
#include "utilities.h"
|
||||||
#include "core/controllers/vpnConfigurationController.h"
|
#include "core/controllers/vpnConfigurationController.h"
|
||||||
#include "version.h"
|
#include "version.h"
|
||||||
@@ -34,7 +27,7 @@ ConnectionController::ConnectionController(const QSharedPointer<ServersModel> &s
|
|||||||
|
|
||||||
void ConnectionController::openConnection()
|
void ConnectionController::openConnection()
|
||||||
{
|
{
|
||||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(Q_OS_TVOS) && !defined(MACOS_NE)
|
||||||
if (!Utils::processIsRunning(Utils::executable(SERVICE_NAME, false), true))
|
if (!Utils::processIsRunning(Utils::executable(SERVICE_NAME, false), true))
|
||||||
{
|
{
|
||||||
emit connectionErrorOccurred(ErrorCode::AmneziaServiceNotRunning);
|
emit connectionErrorOccurred(ErrorCode::AmneziaServiceNotRunning);
|
||||||
@@ -82,8 +75,6 @@ void ConnectionController::onConnectionStateChanged(Vpn::ConnectionState state)
|
|||||||
m_connectionStateText = tr("Connecting...");
|
m_connectionStateText = tr("Connecting...");
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case Vpn::ConnectionState::Connected: {
|
case Vpn::ConnectionState::Connected: {
|
||||||
amnApp->networkManager()->clearConnectionCache();
|
|
||||||
|
|
||||||
m_isConnectionInProgress = false;
|
m_isConnectionInProgress = false;
|
||||||
m_isConnected = true;
|
m_isConnected = true;
|
||||||
m_connectionStateText = tr("Connected");
|
m_connectionStateText = tr("Connected");
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QImage>
|
#include <QImage>
|
||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
|
|
||||||
#include "core/controllers/vpnConfigurationController.h"
|
#include "core/controllers/vpnConfigurationController.h"
|
||||||
#include "core/qrCodeUtils.h"
|
#include "core/qrCodeUtils.h"
|
||||||
#include "core/serialization/serialization.h"
|
#include "core/serialization/serialization.h"
|
||||||
@@ -169,7 +170,8 @@ void ExportController::generateWireGuardConfig(const QString &clientName)
|
|||||||
m_config.append(line + "\n");
|
m_config.append(line + "\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
m_qrCodes = qrCodeUtils::generateQrCodeImageSeries(m_config.toUtf8());
|
auto qr = qrCodeUtils::generateQrCode(m_config.toUtf8());
|
||||||
|
m_qrCodes << qrCodeUtils::svgToBase64(QString::fromStdString(toSvgString(qr, 1)));
|
||||||
|
|
||||||
emit exportConfigChanged();
|
emit exportConfigChanged();
|
||||||
}
|
}
|
||||||
@@ -189,7 +191,8 @@ void ExportController::generateAwgConfig(const QString &clientName)
|
|||||||
m_config.append(line + "\n");
|
m_config.append(line + "\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
m_qrCodes = qrCodeUtils::generateQrCodeImageSeries(m_config.toUtf8());
|
auto qr = qrCodeUtils::generateQrCode(m_config.toUtf8());
|
||||||
|
m_qrCodes << qrCodeUtils::svgToBase64(QString::fromStdString(toSvgString(qr, 1)));
|
||||||
|
|
||||||
emit exportConfigChanged();
|
emit exportConfigChanged();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
#ifdef Q_OS_ANDROID
|
#ifdef Q_OS_ANDROID
|
||||||
#include "platforms/android/android_controller.h"
|
#include "platforms/android/android_controller.h"
|
||||||
#endif
|
#endif
|
||||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
#if defined(Q_OS_IOS) || defined(Q_OS_TVOS) || defined(MACOS_NE)
|
||||||
#include <CoreFoundation/CoreFoundation.h>
|
#include <CoreFoundation/CoreFoundation.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -217,8 +217,6 @@ bool ImportController::extractConfigFromData(QString data)
|
|||||||
|
|
||||||
bool ImportController::extractConfigFromQr(const QByteArray &data)
|
bool ImportController::extractConfigFromQr(const QByteArray &data)
|
||||||
{
|
{
|
||||||
m_configType = checkConfigFormat(QString::fromUtf8(data));
|
|
||||||
|
|
||||||
QJsonObject dataObj = QJsonDocument::fromJson(data).object();
|
QJsonObject dataObj = QJsonDocument::fromJson(data).object();
|
||||||
if (!dataObj.isEmpty()) {
|
if (!dataObj.isEmpty()) {
|
||||||
m_config = dataObj;
|
m_config = dataObj;
|
||||||
@@ -228,13 +226,10 @@ bool ImportController::extractConfigFromQr(const QByteArray &data)
|
|||||||
QByteArray ba_uncompressed = qUncompress(data);
|
QByteArray ba_uncompressed = qUncompress(data);
|
||||||
if (!ba_uncompressed.isEmpty()) {
|
if (!ba_uncompressed.isEmpty()) {
|
||||||
m_config = QJsonDocument::fromJson(ba_uncompressed).object();
|
m_config = QJsonDocument::fromJson(ba_uncompressed).object();
|
||||||
if (m_config.isEmpty()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
m_configType = checkConfigFormat(QString::fromUtf8(ba_uncompressed));
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_configType = checkConfigFormat(data);
|
||||||
if (m_configType == ConfigTypes::Invalid) {
|
if (m_configType == ConfigTypes::Invalid) {
|
||||||
QByteArray ba = QByteArray::fromBase64(data, QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
|
QByteArray ba = QByteArray::fromBase64(data, QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
|
||||||
QByteArray baUncompressed = qUncompress(ba);
|
QByteArray baUncompressed = qUncompress(ba);
|
||||||
@@ -245,10 +240,6 @@ bool ImportController::extractConfigFromQr(const QByteArray &data)
|
|||||||
|
|
||||||
if (!ba.isEmpty()) {
|
if (!ba.isEmpty()) {
|
||||||
m_config = QJsonDocument::fromJson(ba).object();
|
m_config = QJsonDocument::fromJson(ba).object();
|
||||||
if (m_config.isEmpty()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
m_configType = checkConfigFormat(QString::fromUtf8(ba));
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -613,14 +604,14 @@ bool ImportController::decodeQrCode(const QString &code)
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined Q_OS_ANDROID || defined Q_OS_IOS
|
#if defined Q_OS_ANDROID || defined Q_OS_IOS || defined(Q_OS_TVOS)
|
||||||
void ImportController::startDecodingQr()
|
void ImportController::startDecodingQr()
|
||||||
{
|
{
|
||||||
m_qrCodeChunks.clear();
|
m_qrCodeChunks.clear();
|
||||||
m_totalQrCodeChunksCount = 0;
|
m_totalQrCodeChunksCount = 0;
|
||||||
m_receivedQrCodeChunksCount = 0;
|
m_receivedQrCodeChunksCount = 0;
|
||||||
|
|
||||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
#if defined(Q_OS_IOS) || defined(Q_OS_TVOS) || defined(MACOS_NE)
|
||||||
m_isQrCodeProcessed = true;
|
m_isQrCodeProcessed = true;
|
||||||
#endif
|
#endif
|
||||||
#if defined Q_OS_ANDROID
|
#if defined Q_OS_ANDROID
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user