mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-01 00:00:36 +03:00
Compare commits
1 Commits
feat/fallb
...
feat_add_w
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3cdd41c833 |
71
.github/workflows/deploy.yml
vendored
71
.github/workflows/deploy.yml
vendored
@@ -10,14 +10,13 @@ env:
|
||||
|
||||
jobs:
|
||||
Build-Linux-Ubuntu:
|
||||
runs-on: android-runner
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
env:
|
||||
QT_VERSION: 6.10.1
|
||||
QT_VERSION: 6.9.2
|
||||
QIF_VERSION: 4.7
|
||||
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 }}
|
||||
@@ -31,15 +30,13 @@ jobs:
|
||||
version: ${{ env.QT_VERSION }}
|
||||
host: 'linux'
|
||||
target: 'desktop'
|
||||
arch: 'linux_gcc_64'
|
||||
arch: 'gcc_64'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools'
|
||||
dir: ${{ runner.temp }}
|
||||
setup-python: 'true'
|
||||
tools: 'tools_ifw'
|
||||
set-env: 'true'
|
||||
aqtversion: '==3.3.0'
|
||||
py7zrversion: '==0.22.*'
|
||||
extra: '--base ${{ env.QT_MIRROR }}'
|
||||
extra: '--external 7z --base ${{ env.QT_MIRROR }}'
|
||||
|
||||
- name: 'Get sources'
|
||||
uses: actions/checkout@v4
|
||||
@@ -54,12 +51,12 @@ jobs:
|
||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||
echo "Version: $VERSION"
|
||||
|
||||
# - name: 'Setup ccache'
|
||||
# uses: hendrikmuhs/ccache-action@v1.2
|
||||
- name: 'Setup ccache'
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
|
||||
- name: 'Build project'
|
||||
run: |
|
||||
sudo apt-get install libxkbcommon-x11-0 libsecret-1-dev
|
||||
sudo apt-get install libxkbcommon-x11-0
|
||||
export QT_BIN_DIR=${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/gcc_64/bin
|
||||
export QIF_BIN_DIR=${{ runner.temp }}/Qt/Tools/QtInstallerFramework/${{ env.QIF_VERSION }}/bin
|
||||
bash deploy/build_linux.sh
|
||||
@@ -94,12 +91,11 @@ jobs:
|
||||
runs-on: windows-latest
|
||||
|
||||
env:
|
||||
QT_VERSION: 6.10.1
|
||||
QT_VERSION: 6.9.2
|
||||
QIF_VERSION: 4.7
|
||||
BUILD_ARCH: 64
|
||||
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 }}
|
||||
@@ -121,8 +117,8 @@ jobs:
|
||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||
echo "Version: $VERSION"
|
||||
|
||||
# - name: 'Setup ccache'
|
||||
# uses: hendrikmuhs/ccache-action@v1.2
|
||||
- name: 'Setup ccache'
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
|
||||
- name: 'Install Qt'
|
||||
uses: jurplel/install-qt-action@v3
|
||||
@@ -130,15 +126,13 @@ jobs:
|
||||
version: ${{ env.QT_VERSION }}
|
||||
host: 'windows'
|
||||
target: 'desktop'
|
||||
arch: 'win64_msvc2022_64'
|
||||
arch: 'win64_msvc2019_64'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools'
|
||||
dir: ${{ runner.temp }}
|
||||
setup-python: 'true'
|
||||
tools: 'tools_ifw'
|
||||
set-env: 'true'
|
||||
aqtversion: '==3.3.0'
|
||||
py7zrversion: '==0.22.*'
|
||||
extra: '--base ${{ env.QT_MIRROR }}'
|
||||
extra: '--external 7z --base ${{ env.QT_MIRROR }}'
|
||||
|
||||
- name: 'Setup mvsc'
|
||||
uses: ilammy/msvc-dev-cmd@v1
|
||||
@@ -164,7 +158,7 @@ jobs:
|
||||
shell: cmd
|
||||
run: |
|
||||
set BUILD_ARCH=${{ env.BUILD_ARCH }}
|
||||
set QT_BIN_DIR="${{ runner.temp }}\\Qt\\${{ env.QT_VERSION }}\\msvc2022_64\\bin"
|
||||
set QT_BIN_DIR="${{ runner.temp }}\\Qt\\${{ env.QT_VERSION }}\\msvc2019_64\\bin"
|
||||
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
|
||||
@@ -201,12 +195,11 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
|
||||
env:
|
||||
QT_VERSION: 6.10.1
|
||||
QT_VERSION: 6.9.2
|
||||
CC: cc
|
||||
CXX: c++
|
||||
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 }}
|
||||
@@ -261,8 +254,8 @@ jobs:
|
||||
submodules: 'true'
|
||||
fetch-depth: 10
|
||||
|
||||
# - name: 'Setup ccache'
|
||||
# uses: hendrikmuhs/ccache-action@v1.2
|
||||
- name: 'Setup ccache'
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: pip install jsonschema jinja2
|
||||
@@ -321,7 +314,6 @@ jobs:
|
||||
|
||||
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 }}
|
||||
@@ -354,8 +346,8 @@ jobs:
|
||||
submodules: 'true'
|
||||
fetch-depth: 10
|
||||
|
||||
# - name: 'Setup ccache'
|
||||
# uses: hendrikmuhs/ccache-action@v1.2
|
||||
- name: 'Setup ccache'
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
|
||||
- name: 'Build project'
|
||||
run: |
|
||||
@@ -382,7 +374,7 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
|
||||
env:
|
||||
QT_VERSION: 6.10.1
|
||||
QT_VERSION: 6.9.2
|
||||
|
||||
MAC_TEAM_ID: ${{ secrets.MAC_TEAM_ID }}
|
||||
|
||||
@@ -399,7 +391,6 @@ jobs:
|
||||
|
||||
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 }}
|
||||
@@ -421,11 +412,15 @@ jobs:
|
||||
arch: 'clang_64'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools'
|
||||
dir: ${{ runner.temp }}
|
||||
#setup-python: 'true'
|
||||
#set-env: 'true'
|
||||
#extra: '--external 7z --base ${{ env.QT_MIRROR }}'
|
||||
setup-python: 'true'
|
||||
set-env: 'true'
|
||||
aqtversion: '==3.3.0'
|
||||
py7zrversion: '==0.22.*'
|
||||
extra: '--base ${{ env.QT_MIRROR }}'
|
||||
cache: 'true'
|
||||
|
||||
- name: 'Get sources'
|
||||
uses: actions/checkout@v4
|
||||
@@ -440,8 +435,8 @@ jobs:
|
||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||
echo "Version: $VERSION"
|
||||
|
||||
# - name: 'Setup ccache'
|
||||
# uses: hendrikmuhs/ccache-action@v1.2
|
||||
- name: 'Setup ccache'
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
|
||||
- name: 'Build project'
|
||||
run: |
|
||||
@@ -472,7 +467,7 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
|
||||
env:
|
||||
QT_VERSION: 6.10.1
|
||||
QT_VERSION: 6.9.2
|
||||
|
||||
MAC_TEAM_ID: ${{ secrets.MAC_TEAM_ID }}
|
||||
|
||||
@@ -482,7 +477,6 @@ jobs:
|
||||
|
||||
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 }}
|
||||
@@ -525,8 +519,8 @@ jobs:
|
||||
submodules: 'true'
|
||||
fetch-depth: 10
|
||||
|
||||
# - name: 'Setup ccache'
|
||||
# uses: hendrikmuhs/ccache-action@v1.2
|
||||
- name: 'Setup ccache'
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
|
||||
- name: 'Build project'
|
||||
run: |
|
||||
@@ -543,7 +537,7 @@ jobs:
|
||||
# ------------------------------------------------------
|
||||
|
||||
Build-Android:
|
||||
runs-on: android-runner
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
ANDROID_BUILD_PLATFORM: android-36
|
||||
@@ -551,7 +545,6 @@ jobs:
|
||||
QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools'
|
||||
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 }}
|
||||
@@ -636,15 +629,15 @@ jobs:
|
||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||
echo "Version: $VERSION"
|
||||
|
||||
# - name: 'Setup ccache'
|
||||
# uses: hendrikmuhs/ccache-action@v1.2
|
||||
- name: 'Setup ccache'
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
|
||||
- name: 'Setup Java'
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
# cache: 'gradle'
|
||||
cache: 'gradle'
|
||||
|
||||
- name: 'Setup Android NDK'
|
||||
id: setup-ndk
|
||||
|
||||
1
.github/workflows/tag-deploy.yml
vendored
1
.github/workflows/tag-deploy.yml
vendored
@@ -17,7 +17,6 @@ jobs:
|
||||
QIF_VERSION: 4.5
|
||||
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 }}
|
||||
|
||||
2
.github/workflows/tag-upload.yml
vendored
2
.github/workflows/tag-upload.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
- name: Verify git tag
|
||||
run: |
|
||||
TAG_NAME=${{ inputs.RELEASE_VERSION }}
|
||||
CMAKE_TAG=$(grep 'set(AMNEZIAVPN_VERSION' CMakeLists.txt | sed -E 's/.*AMNEZIAVPN_VERSION ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+).*/\1/')
|
||||
CMAKE_TAG=$(grep 'project.*VERSION' CMakeLists.txt | sed -E 's/.* ([0-9]+.[0-9]+.[0-9]+.[0-9]+)$/\1/')
|
||||
if [[ "$TAG_NAME" == "$CMAKE_TAG" ]]; then
|
||||
echo "Git tag ($TAG_NAME) matches CMakeLists.txt version ($CMAKE_TAG)."
|
||||
else
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -140,6 +140,3 @@ ios-ne-build.sh
|
||||
macos-ne-build.sh
|
||||
macos-signed-build.sh
|
||||
macos-with-sign-build.sh
|
||||
DeveloperIdApplicationCertificate.p12
|
||||
DeveloperIdInstallerCertificate.p12
|
||||
|
||||
|
||||
4
.gitmodules
vendored
4
.gitmodules
vendored
@@ -14,7 +14,3 @@
|
||||
[submodule "client/3rd/QSimpleCrypto"]
|
||||
path = client/3rd/QSimpleCrypto
|
||||
url = https://github.com/amnezia-vpn/QSimpleCrypto.git
|
||||
[submodule "client/3rd/qtgamepad"]
|
||||
path = client/3rd/qtgamepad
|
||||
url = https://github.com/amnezia-vpn/qtgamepad.git
|
||||
branch = 6.6
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
|
||||
|
||||
set(PROJECT AmneziaVPN)
|
||||
set(AMNEZIAVPN_VERSION 4.8.15.2)
|
||||
set(AMNEZIAVPN_VERSION 4.8.12.5)
|
||||
|
||||
project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
|
||||
DESCRIPTION "AmneziaVPN"
|
||||
@@ -12,7 +12,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
|
||||
set(RELEASE_DATE "${CURRENT_DATE}")
|
||||
|
||||
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
|
||||
set(APP_ANDROID_VERSION_CODE 2119)
|
||||
set(APP_ANDROID_VERSION_CODE 2101)
|
||||
|
||||
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
||||
set(MZ_PLATFORM_NAME "linux")
|
||||
@@ -61,9 +61,6 @@ if(WIN32 AND NOT IOS AND NOT ANDROID AND NOT MACOS_NE)
|
||||
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")
|
||||
|
||||
@@ -179,7 +179,7 @@ You may face compiling issues in QT Creator after you've worked in Android Studi
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
@@ -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
|
||||
Submodule client/3rd-prebuilt updated: 51bb4703a4...adf5eb920f
1
client/3rd/qtgamepad
vendored
1
client/3rd/qtgamepad
vendored
Submodule client/3rd/qtgamepad deleted from f72b3e0c62
@@ -25,7 +25,6 @@ add_definitions(-DGIT_COMMIT_HASH="${GIT_COMMIT_HASH}")
|
||||
|
||||
add_definitions(-DPROD_AGW_PUBLIC_KEY="$ENV{PROD_AGW_PUBLIC_KEY}")
|
||||
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_ENDPOINT="$ENV{DEV_AGW_ENDPOINT}")
|
||||
@@ -60,6 +59,7 @@ target_include_directories(${PROJECT} PUBLIC
|
||||
if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) 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_process_interface.rep)
|
||||
qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_process_tun2socks.rep)
|
||||
endif()
|
||||
|
||||
qt6_add_resources(QRC ${QRC} ${CMAKE_CURRENT_LIST_DIR}/resources.qrc)
|
||||
@@ -79,7 +79,6 @@ set(AMNEZIAVPN_TS_FILES
|
||||
)
|
||||
|
||||
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})
|
||||
|
||||
@@ -106,6 +105,9 @@ endif()
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/cmake/3rdparty.cmake)
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/cmake/sources.cmake)
|
||||
|
||||
# Add webview module
|
||||
add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/core/webview)
|
||||
|
||||
include_directories(
|
||||
${CMAKE_CURRENT_LIST_DIR}/../ipc
|
||||
${CMAKE_CURRENT_LIST_DIR}/../common/logger
|
||||
@@ -195,7 +197,7 @@ elseif(APPLE)
|
||||
include(cmake/macos.cmake)
|
||||
endif()
|
||||
|
||||
target_link_libraries(${PROJECT} PRIVATE ${LIBS})
|
||||
target_link_libraries(${PROJECT} PRIVATE ${LIBS} webview)
|
||||
target_compile_definitions(${PROJECT} PRIVATE "MZ_$<UPPER_CASE:${MZ_PLATFORM_NAME}>")
|
||||
|
||||
# deploy artifacts required to run the application to the debug build folder
|
||||
@@ -229,13 +231,4 @@ if(NOT IOS AND NOT ANDROID AND NOT MACOS_NE)
|
||||
endif()
|
||||
|
||||
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).
|
||||
if(COMMAND qt_import_qml_plugins)
|
||||
qt_import_qml_plugins(${PROJECT})
|
||||
endif()
|
||||
if(COMMAND qt_finalize_executable)
|
||||
qt_finalize_executable(${PROJECT})
|
||||
else()
|
||||
qt_finalize_target(${PROJECT})
|
||||
endif()
|
||||
qt_finalize_target(${PROJECT})
|
||||
|
||||
@@ -15,6 +15,11 @@
|
||||
#include <QEvent>
|
||||
#include <QDir>
|
||||
#include <QSettings>
|
||||
#include <QQmlExtensionPlugin>
|
||||
#include <QtPlugin>
|
||||
#include "core/webview/plugin.h"
|
||||
|
||||
Q_IMPORT_PLUGIN(WebViewPlugin)
|
||||
|
||||
#include "logger.h"
|
||||
#include "ui/controllers/pageController.h"
|
||||
@@ -61,10 +66,10 @@ AmneziaApplication::AmneziaApplication(int &argc, char *argv[]) : AMNEZIA_BASE_C
|
||||
AmneziaApplication::~AmneziaApplication()
|
||||
{
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
if (m_vpnConnection && m_vpnConnectionThread.isRunning()) {
|
||||
QMetaObject::invokeMethod(m_vpnConnection.get(), "disconnectSlots", Qt::BlockingQueuedConnection);
|
||||
|
||||
QMetaObject::invokeMethod(m_vpnConnection.get(), "disconnectFromVpn", Qt::BlockingQueuedConnection);
|
||||
if (m_vpnConnection) {
|
||||
QMetaObject::invokeMethod(m_vpnConnection.get(), "disconnectSlots", Qt::QueuedConnection);
|
||||
QMetaObject::invokeMethod(m_vpnConnection.get(), "disconnectFromVpn", Qt::QueuedConnection);
|
||||
QThread::msleep(2000);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -77,6 +82,7 @@ AmneziaApplication::~AmneziaApplication()
|
||||
}
|
||||
|
||||
if (m_engine) {
|
||||
QObject::disconnect(m_engine, 0, 0, 0);
|
||||
delete m_engine;
|
||||
}
|
||||
}
|
||||
@@ -98,6 +104,14 @@ void AmneziaApplication::init()
|
||||
{
|
||||
m_engine = new QQmlApplicationEngine;
|
||||
|
||||
// Register AmneziaWebView plugin explicitly
|
||||
QObject *pluginInstance = qt_static_plugin_WebViewPlugin().instance();
|
||||
QQmlExtensionPlugin *p = qobject_cast<QQmlExtensionPlugin*>(pluginInstance);
|
||||
if (p) {
|
||||
p->registerTypes("AmneziaWebView");
|
||||
p->initializeEngine(m_engine, "AmneziaWebView");
|
||||
}
|
||||
|
||||
const QUrl url(QStringLiteral("qrc:/ui/qml/main2.qml"));
|
||||
QObject::connect(
|
||||
m_engine, &QQmlApplicationEngine::objectCreated, this,
|
||||
@@ -109,16 +123,6 @@ void AmneziaApplication::init()
|
||||
// install filter on main window
|
||||
if (auto win = qobject_cast<QQuickWindow*>(obj)) {
|
||||
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();
|
||||
}
|
||||
},
|
||||
@@ -139,6 +143,7 @@ void AmneziaApplication::init()
|
||||
m_coreController.reset(new CoreController(m_vpnConnection, m_settings, m_engine));
|
||||
|
||||
m_engine->addImportPath("qrc:/ui/qml/Modules/");
|
||||
m_engine->addImportPath("qrc:/");
|
||||
|
||||
if (m_parser.isSet(m_optImport)) {
|
||||
const QString data = m_parser.value(m_optImport);
|
||||
|
||||
@@ -26,8 +26,6 @@ import android.os.ParcelFileDescriptor
|
||||
import android.os.SystemClock
|
||||
import android.provider.OpenableColumns
|
||||
import android.provider.Settings
|
||||
import android.view.InputDevice
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -75,8 +73,6 @@ private const val OPEN_FILE_ACTION_CODE = 3
|
||||
private const val CHECK_NOTIFICATION_PERMISSION_ACTION_CODE = 4
|
||||
|
||||
private const val PREFS_NOTIFICATION_PERMISSION_ASKED = "NOTIFICATION_PERMISSION_ASKED"
|
||||
private const val OPEN_FILE_AFTER_RESUME_DELAY_MS = 400L
|
||||
private const val KEY_PENDING_OPEN_FILE_URI = "pending_open_file_uri"
|
||||
|
||||
class AmneziaActivity : QtActivity() {
|
||||
|
||||
@@ -93,12 +89,6 @@ class AmneziaActivity : QtActivity() {
|
||||
private val actionResultHandlers = mutableMapOf<Int, ActivityResultHandler>()
|
||||
private val permissionRequestHandlers = mutableMapOf<Int, PermissionRequestHandler>()
|
||||
|
||||
private var isActivityResumed = false
|
||||
private var hasWindowFocus = false
|
||||
private val resumeHandler = Handler(Looper.getMainLooper())
|
||||
private var pendingOpenFileUri: String? = null
|
||||
private var openFileDeliveryScheduled = false
|
||||
|
||||
private val vpnServiceEventHandler: Handler by lazy(NONE) {
|
||||
object : Handler(Looper.getMainLooper()) {
|
||||
override fun handleMessage(msg: Message) {
|
||||
@@ -200,18 +190,11 @@ class AmneziaActivity : QtActivity() {
|
||||
doBindService()
|
||||
}
|
||||
)
|
||||
pendingOpenFileUri = savedInstanceState?.getString(KEY_PENDING_OPEN_FILE_URI)
|
||||
openFileDeliveryScheduled = false
|
||||
registerBroadcastReceivers()
|
||||
intent?.let(::processIntent)
|
||||
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() {
|
||||
listOf(
|
||||
"rsapss",
|
||||
@@ -277,11 +260,6 @@ class AmneziaActivity : QtActivity() {
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
isActivityResumed = false
|
||||
hasWindowFocus = false
|
||||
// Cancel all pending operations when activity stops
|
||||
resumeHandler.removeCallbacksAndMessages(null)
|
||||
openFileDeliveryScheduled = false
|
||||
Log.d(TAG, "Stop Amnezia activity")
|
||||
doUnbindService()
|
||||
mainScope.launch {
|
||||
@@ -293,129 +271,35 @@ class AmneziaActivity : QtActivity() {
|
||||
|
||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||
super.onWindowFocusChanged(hasFocus)
|
||||
hasWindowFocus = hasFocus
|
||||
Log.d(TAG, "Window focus changed: hasFocus=$hasFocus")
|
||||
|
||||
if (!hasFocus) {
|
||||
// Cancel pending operations if window loses focus
|
||||
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 {
|
||||
val keyCode = event.keyCode
|
||||
val pressed = event.action == KeyEvent.ACTION_DOWN
|
||||
|
||||
when (keyCode) {
|
||||
KeyEvent.KEYCODE_BUTTON_A,
|
||||
KeyEvent.KEYCODE_BUTTON_B,
|
||||
KeyEvent.KEYCODE_BUTTON_X,
|
||||
KeyEvent.KEYCODE_BUTTON_Y,
|
||||
KeyEvent.KEYCODE_BUTTON_START,
|
||||
KeyEvent.KEYCODE_BUTTON_SELECT -> {
|
||||
nativeGamepadKeyEvent(0, keyCode, pressed)
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_DPAD_CENTER,
|
||||
KeyEvent.KEYCODE_DPAD_UP,
|
||||
KeyEvent.KEYCODE_DPAD_DOWN,
|
||||
KeyEvent.KEYCODE_DPAD_LEFT,
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT -> {
|
||||
val syntheticKeyCode = if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) KeyEvent.KEYCODE_ENTER else keyCode
|
||||
val synthetic = KeyEvent(
|
||||
event.downTime, event.eventTime, event.action, syntheticKeyCode,
|
||||
event.repeatCount, event.metaState, -1, event.scanCode,
|
||||
event.flags, InputDevice.SOURCE_KEYBOARD
|
||||
)
|
||||
return super.dispatchKeyEvent(synthetic)
|
||||
}
|
||||
}
|
||||
|
||||
return super.dispatchKeyEvent(event)
|
||||
}
|
||||
|
||||
private external fun nativeGamepadKeyEvent(deviceId: Int, keyCode: Int, pressed: Boolean)
|
||||
|
||||
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()
|
||||
isActivityResumed = false
|
||||
// Cancel all pending operations when activity pauses
|
||||
resumeHandler.removeCallbacksAndMessages(null)
|
||||
openFileDeliveryScheduled = false
|
||||
Log.d(TAG, "Pause Amnezia activity")
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
isActivityResumed = true
|
||||
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 {
|
||||
invalidate()
|
||||
|
||||
resumeHandler.postDelayed({
|
||||
// Check if activity is still resumed and has focus before executing
|
||||
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
|
||||
sendTouch(1f, 1f)
|
||||
}
|
||||
postDelayed({
|
||||
sendTouch(1f, 1f)
|
||||
}, 100)
|
||||
|
||||
resumeHandler.postDelayed({
|
||||
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
|
||||
sendTouch(2f, 2f)
|
||||
}
|
||||
|
||||
postDelayed({
|
||||
sendTouch(2f, 2f)
|
||||
}, 200)
|
||||
|
||||
resumeHandler.postDelayed({
|
||||
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
|
||||
requestLayout()
|
||||
invalidate()
|
||||
}
|
||||
|
||||
postDelayed({
|
||||
requestLayout()
|
||||
invalidate()
|
||||
}, 250)
|
||||
}
|
||||
}
|
||||
} */
|
||||
Log.d(TAG, "Resume Amnezia activity")
|
||||
}
|
||||
|
||||
private fun configureWindowForEdgeToEdge() {
|
||||
@@ -453,35 +337,31 @@ class AmneziaActivity : QtActivity() {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { view, windowInsets ->
|
||||
val imeInsets = windowInsets.getInsets(WindowInsetsCompat.Type.ime())
|
||||
val imeVisible = windowInsets.isVisible(WindowInsetsCompat.Type.ime())
|
||||
|
||||
|
||||
val imeHeight = if (imeVisible) imeInsets.bottom else 0
|
||||
|
||||
val density = resources.displayMetrics.density
|
||||
val imeHeightDp = (imeHeight / density).toInt()
|
||||
|
||||
|
||||
// Also track system bars (navigation bar, status bar) changes
|
||||
val systemBarsInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
val navBarHeight = systemBarsInsets.bottom
|
||||
val navBarHeightDp = (navBarHeight / density).toInt()
|
||||
val statusBarHeight = systemBarsInsets.top
|
||||
val statusBarHeightDp = (statusBarHeight / density).toInt()
|
||||
|
||||
|
||||
mainScope.launch {
|
||||
qtInitialized.await()
|
||||
QtAndroidController.onImeInsetsChanged(imeHeightDp)
|
||||
QtAndroidController.onSystemBarsInsetsChanged(navBarHeightDp, statusBarHeightDp)
|
||||
}
|
||||
|
||||
|
||||
// Return windowInsets instead of CONSUMED to allow proper handling
|
||||
windowInsets
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
isActivityResumed = false
|
||||
hasWindowFocus = false
|
||||
// Cancel all pending operations when activity is destroyed
|
||||
resumeHandler.removeCallbacksAndMessages(null)
|
||||
Log.d(TAG, "Destroy Amnezia activity")
|
||||
unregisterBroadcastReceiver(notificationStateReceiver)
|
||||
notificationStateReceiver = null
|
||||
@@ -807,13 +687,9 @@ class AmneziaActivity : QtActivity() {
|
||||
grantUriPermission(packageName, this, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}?.toString() ?: ""
|
||||
Log.v(TAG, "Open file: $uri")
|
||||
if (uri.isNotEmpty()) {
|
||||
pendingOpenFileUri = uri
|
||||
} else {
|
||||
mainScope.launch {
|
||||
qtInitialized.await()
|
||||
QtAndroidController.onFileOpened(uri)
|
||||
}
|
||||
mainScope.launch {
|
||||
qtInitialized.await()
|
||||
QtAndroidController.onFileOpened(uri)
|
||||
}
|
||||
}
|
||||
))
|
||||
@@ -842,7 +718,7 @@ class AmneziaActivity : QtActivity() {
|
||||
@Suppress("unused")
|
||||
fun getFd(fileName: String): Int {
|
||||
Log.v(TAG, "Get fd for $fileName")
|
||||
return blockingCall(Dispatchers.IO) {
|
||||
return blockingCall {
|
||||
try {
|
||||
pfd = contentResolver.openFileDescriptor(Uri.parse(fileName), "r")
|
||||
pfd?.fd ?: -1
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package org.amnezia.vpn
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
@@ -14,29 +11,8 @@ private const val TAG = "TvFilePicker"
|
||||
|
||||
class TvFilePicker : ComponentActivity() {
|
||||
|
||||
private val fileChooseResultLauncher = registerForActivityResult(object : ActivityResultContracts.OpenDocument() {
|
||||
override fun createIntent(context: Context, input: Array<String>): Intent {
|
||||
val intent = super.createIntent(context, input)
|
||||
|
||||
val activitiesToResolveIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
context.packageManager.queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()))
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
context.packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
|
||||
}
|
||||
if (activitiesToResolveIntent.all {
|
||||
val name = it.activityInfo.packageName
|
||||
name.startsWith("com.google.android.tv.frameworkpackagestubs") || name.startsWith("com.android.tv.frameworkpackagestubs")
|
||||
}) {
|
||||
throw ActivityNotFoundException()
|
||||
}
|
||||
return intent
|
||||
}
|
||||
}) {
|
||||
setResult(RESULT_OK, Intent().apply {
|
||||
data = it
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
})
|
||||
private val fileChooseResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) {
|
||||
setResult(RESULT_OK, Intent().apply { data = it })
|
||||
finish()
|
||||
}
|
||||
|
||||
@@ -55,7 +31,7 @@ class TvFilePicker : ComponentActivity() {
|
||||
private fun getFile() {
|
||||
try {
|
||||
Log.v(TAG, "getFile")
|
||||
fileChooseResultLauncher.launch(arrayOf("*/*"))
|
||||
fileChooseResultLauncher.launch("*/*")
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
Log.w(TAG, "Activity not found")
|
||||
setResult(RESULT_CANCELED, Intent().apply { putExtra("activityNotFound", true) })
|
||||
|
||||
609
client/android/src/org/amnezia/vpn/WebViewController.java
Normal file
609
client/android/src/org/amnezia/vpn/WebViewController.java
Normal file
@@ -0,0 +1,609 @@
|
||||
package org.amnezia.vpn;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.MotionEvent;
|
||||
import android.webkit.*;
|
||||
import android.net.http.SslError;
|
||||
import android.os.Message;
|
||||
import org.qtproject.qt.android.WebViewControllerEx;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.net.URLDecoder;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import android.app.Activity;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
public class WebViewController
|
||||
{
|
||||
private interface RequestFinished {
|
||||
void onRequestCompleted();
|
||||
}
|
||||
|
||||
private String baseUrl = "";
|
||||
private static final String INTERNAL_BASE_URL = "file:///";
|
||||
private static final long GEOMETRY_STABLE_INTERVAL = 150; //ms wait geometry settle
|
||||
|
||||
private final Activity m_activity;
|
||||
private final long m_id;
|
||||
private WebView m_webView = null;
|
||||
private ViewGroup m_layout = null;
|
||||
private boolean m_loading = true;
|
||||
private long mLastGeometryChange = 0L;
|
||||
|
||||
private float m_displayDensity = (float) 1.0;
|
||||
|
||||
public native void urlChanged(long viewId, String url);
|
||||
public native byte[] dataForUrl(long viewId, String url, StringBuilder mimeType, StringBuilder encoding);
|
||||
public native boolean canHandleUrl(long viewId, String url);
|
||||
private final Handler m_handler;
|
||||
|
||||
private native void pageFinished(long id, String url);
|
||||
private native void pageStarted(long id, String url);
|
||||
private static final String TAG = WebViewController.class.getSimpleName();
|
||||
|
||||
private class AndroidWebChromeClient extends WebChromeClient {
|
||||
@Override
|
||||
public boolean onCreateWindow(WebView view, boolean dialog, boolean userGesture, Message resultMsg)
|
||||
{
|
||||
// Prevent opening new windows/tabs - load URLs in the same WebView instead
|
||||
// This handles links with target="_blank" or window.open()
|
||||
// Return false to prevent creating new windows - URLs will be handled by shouldOverrideUrlLoading
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public WebViewController(final Activity activity, final long id) {
|
||||
m_activity = activity;
|
||||
m_id = id;
|
||||
|
||||
ViewGroup root = (ViewGroup)(((ViewGroup)(m_activity.findViewById(android.R.id.content))).getChildAt(0));
|
||||
if (root != null) {
|
||||
m_layout = root;
|
||||
}
|
||||
|
||||
m_displayDensity = m_activity.getResources().getDisplayMetrics().density;
|
||||
|
||||
m_handler = new Handler(Looper.getMainLooper());
|
||||
m_handler.post(new Runnable() {
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
@Override
|
||||
public void run() {
|
||||
m_webView = new WebView(m_activity);
|
||||
m_webView.setFocusable(true);
|
||||
|
||||
m_webView.setFocusableInTouchMode(true);
|
||||
m_webView.getSettings().setJavaScriptEnabled(true);
|
||||
m_webView.getSettings().setAllowFileAccess(true);
|
||||
m_webView.getSettings().setAllowFileAccessFromFileURLs(true);
|
||||
m_webView.getSettings().setAllowUniversalAccessFromFileURLs(true);
|
||||
m_webView.getSettings().setAllowContentAccess(true);
|
||||
m_webView.getSettings().setBuiltInZoomControls(true);
|
||||
m_webView.getSettings().setDisplayZoomControls(false);
|
||||
m_webView.getSettings().setLoadWithOverviewMode(true);
|
||||
|
||||
m_webView.getSettings().setSupportMultipleWindows(false); // Prevent opening new windows
|
||||
m_webView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
|
||||
|
||||
m_webView.getSettings().setUseWideViewPort(true);
|
||||
m_webView.getSettings().setLoadWithOverviewMode(true);
|
||||
m_webView.getSettings().setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NORMAL);
|
||||
|
||||
m_webView.getSettings().setSupportZoom(true);
|
||||
m_webView.getSettings().setBuiltInZoomControls(true);
|
||||
m_webView.getSettings().setDisplayZoomControls(false);
|
||||
|
||||
m_webView.setInitialScale(0);
|
||||
m_webView.setVisibility(android.view.View.INVISIBLE);
|
||||
|
||||
// Ensure WebView can receive and handle touch events for link clicks
|
||||
m_webView.setClickable(true);
|
||||
m_webView.setLongClickable(true);
|
||||
m_webView.setHapticFeedbackEnabled(false);
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
|
||||
m_webView.setElevation(0f);
|
||||
}
|
||||
|
||||
m_webView.setWebViewClient(buildWebViewClient());
|
||||
m_webView.setWebChromeClient(buildWebChromeClient());
|
||||
|
||||
// Ensure IME appears on tap when focusing editable content
|
||||
m_webView.setOnTouchListener(new android.view.View.OnTouchListener() {
|
||||
@Override
|
||||
public boolean onTouch(android.view.View v, MotionEvent event) {
|
||||
// Let WebView handle touch events normally for link clicks
|
||||
// Only request focus on ACTION_UP to allow IME to appear for input fields
|
||||
if (event.getAction() == MotionEvent.ACTION_UP) {
|
||||
v.requestFocus();
|
||||
// Do not show IME if app temporarily suppresses it
|
||||
try {
|
||||
android.view.inputmethod.InputMethodManager imm = (android.view.inputmethod.InputMethodManager) v.getContext().getSystemService(android.content.Context.INPUT_METHOD_SERVICE);
|
||||
if (imm != null) {
|
||||
imm.showSoftInput(v, 0);
|
||||
}
|
||||
} catch (Throwable ignore) {}
|
||||
}
|
||||
// Return false to let WebView handle the touch event (for link clicks, etc.)
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private WebViewClient buildWebViewClient() {
|
||||
return new WebViewClient() {
|
||||
@Override
|
||||
public void onReceivedSslError (WebView view, SslErrorHandler handler, SslError error) {
|
||||
handler.proceed();
|
||||
Log.e(TAG, "SSL certificate error");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageStarted (WebView view, String url, Bitmap favicon) {
|
||||
m_loading = true;
|
||||
|
||||
String dataUrl = updateUrl(url);
|
||||
urlChanged(m_id, dataUrl);
|
||||
pageStarted(m_id, dataUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageFinished(WebView view, String url) {
|
||||
m_loading = false;
|
||||
|
||||
String dataUrl = updateUrl(url);
|
||||
pageFinished(m_id, dataUrl);
|
||||
urlChanged(m_id, dataUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||
Log.d(TAG, "shouldOverrideUrlLoading (deprecated): " + url);
|
||||
if (url == null || url.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
urlChanged(m_id, url);
|
||||
|
||||
// Always load URLs within WebView, don't open in external browser
|
||||
// Explicitly load the URL in the WebView and return true to indicate we handled it
|
||||
view.loadUrl(url);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView view, android.webkit.WebResourceRequest request) {
|
||||
String url = request.getUrl().toString();
|
||||
Log.d(TAG, "shouldOverrideUrlLoading (new): " + url + ", isMainFrame: " + request.isForMainFrame() + ", method: " + request.getMethod());
|
||||
|
||||
if (url == null || url.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
urlChanged(m_id, url);
|
||||
|
||||
// Always load URLs within WebView, don't open in external browser
|
||||
// Handle main frame navigation (link clicks, form submissions, etc.)
|
||||
// For sub-resources (images, CSS, JS), let WebView handle normally by returning false
|
||||
if (request.isForMainFrame()) {
|
||||
view.loadUrl(url);
|
||||
return true;
|
||||
}
|
||||
// For sub-resources, let WebView handle normally
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
|
||||
|
||||
if (url.startsWith("data:") || !canHandleUrl(m_id, url)) {
|
||||
return super.shouldInterceptRequest(view, url);
|
||||
}
|
||||
|
||||
StringBuilder mimeType = new StringBuilder();
|
||||
StringBuilder encoding = new StringBuilder();
|
||||
byte[] data = dataForUrl(m_id, url, mimeType, encoding);
|
||||
|
||||
boolean isDataInvalid = (data == null) || (data.length == 0);
|
||||
if (isDataInvalid) {
|
||||
Log.w(TAG, String.format("Invalid data received for url: %s", url));
|
||||
return null;
|
||||
}
|
||||
if ((mimeType.length() == 0) || mimeType.toString().isEmpty()) {
|
||||
Log.w(TAG, String.format("Invalid mimeType received for url: %s", url));
|
||||
}
|
||||
if ((encoding.length() == 0) || encoding.toString().isEmpty()) {
|
||||
Log.w(TAG, String.format("Invalid encoding received for url: %s", url));
|
||||
}
|
||||
|
||||
ByteArrayInputStream dataStream = new ByteArrayInputStream(data);
|
||||
return new WebResourceResponse(mimeType.toString(), encoding.toString(), dataStream);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private WebChromeClient buildWebChromeClient() {
|
||||
return new AndroidWebChromeClient() {
|
||||
@Override
|
||||
public void onProgressChanged(WebView view, int newProgress) {
|
||||
super.onProgressChanged(view, newProgress);
|
||||
if (newProgress == 100) {
|
||||
m_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
|
||||
callback.invoke(origin, true, false);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private String updateUrl(String url) {
|
||||
if (!url.startsWith(INTERNAL_BASE_URL)) {
|
||||
return url;
|
||||
}
|
||||
|
||||
String dataUrl = url;
|
||||
|
||||
try {
|
||||
dataUrl = URLDecoder.decode(url.substring(INTERNAL_BASE_URL.length()), "UTF-8");
|
||||
if ((dataUrl.length() == 1) && dataUrl.endsWith("#")) {
|
||||
dataUrl = baseUrl;
|
||||
} else {
|
||||
dataUrl = baseUrl + dataUrl;
|
||||
}
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
Log.e(TAG, e.toString());
|
||||
}
|
||||
|
||||
return dataUrl;
|
||||
}
|
||||
|
||||
public void release() {
|
||||
if (m_handler == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_handler.post(() -> {
|
||||
if (m_webView == null) {
|
||||
return;
|
||||
}
|
||||
m_webView.setVisibility(android.view.View.INVISIBLE);
|
||||
m_layout.removeView(m_webView);
|
||||
m_webView.stopLoading();
|
||||
m_webView.setWebViewClient(new WebViewClient());
|
||||
m_webView.setWebChromeClient(null);
|
||||
m_webView = null;
|
||||
});
|
||||
}
|
||||
|
||||
public void setGeometry(final int x, final int y, final int width, final int height) {
|
||||
|
||||
|
||||
Log.d(TAG, String.format(
|
||||
"setGeometry called: x=%d, y=%d, width=%d, height=%d",
|
||||
x, y, width, height));
|
||||
|
||||
|
||||
if (m_handler == null)
|
||||
return;
|
||||
|
||||
m_handler.post(() -> {
|
||||
if (m_webView == null || m_layout == null)
|
||||
return;
|
||||
|
||||
if (m_layout.indexOfChild(m_webView) < 0) {
|
||||
m_layout.addView(m_webView);
|
||||
}
|
||||
|
||||
float scale = m_activity.getResources().getDisplayMetrics().density;
|
||||
|
||||
int pxX = Math.round(x * scale);
|
||||
int pxY = Math.round(y * scale);
|
||||
int pxW = Math.round(width * scale);
|
||||
int pxH = Math.round(height * scale);
|
||||
|
||||
Log.d(TAG, String.format(
|
||||
"density=%.2f qml: x=%d y=%d w=%d h=%d -> px: x=%d y=%d w=%d h=%d",
|
||||
scale, x, y, width, height, pxX, pxY, pxW, pxH));
|
||||
|
||||
ViewGroup.LayoutParams params =
|
||||
WebViewControllerEx.createQtLayoutParams(
|
||||
pxW,
|
||||
pxH,
|
||||
pxX,
|
||||
pxY
|
||||
);
|
||||
|
||||
m_webView.setLayoutParams(params);
|
||||
m_webView.setInitialScale(0);
|
||||
|
||||
Log.d(TAG, String.format(
|
||||
"WebView positioned (QtLayout) at px: x=%d, y=%d, w=%d, h=%d",
|
||||
pxX, pxY, pxW, pxH));
|
||||
|
||||
m_layout.requestLayout();
|
||||
m_webView.requestLayout();
|
||||
|
||||
mLastGeometryChange = SystemClock.uptimeMillis();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public void show() {
|
||||
if (m_handler == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (m_webView == null) {
|
||||
return;
|
||||
}
|
||||
if (m_webView.getVisibility() != android.view.View.VISIBLE) {
|
||||
m_webView.setVisibility(android.view.View.VISIBLE);
|
||||
}
|
||||
// Don't bring WebView to front - let QML elements render on top
|
||||
// Set low elevation so QML elements can appear above WebView
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
|
||||
m_webView.setElevation(0f);
|
||||
}
|
||||
m_webView.requestLayout();
|
||||
m_layout.requestLayout();
|
||||
m_layout.postInvalidate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
hideWebView();
|
||||
long now = SystemClock.uptimeMillis();
|
||||
}
|
||||
|
||||
private void hideWebView() {
|
||||
if (m_handler == null) {
|
||||
return;
|
||||
}
|
||||
m_handler.post(() -> {
|
||||
if (m_webView == null) {
|
||||
return;
|
||||
}
|
||||
if (m_webView.getVisibility() == android.view.View.VISIBLE) {
|
||||
m_webView.setVisibility(android.view.View.INVISIBLE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void loadUrl(String url) {
|
||||
final String newUrl;
|
||||
if ((baseUrl.length() > 0) && url.startsWith(baseUrl)) {
|
||||
newUrl = INTERNAL_BASE_URL + url.substring(baseUrl.length());
|
||||
} else {
|
||||
newUrl = url;
|
||||
}
|
||||
|
||||
if (m_handler == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (m_webView == null) {
|
||||
return;
|
||||
}
|
||||
m_webView.loadUrl(newUrl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void loadDataWithBaseURL(final String url, final String html, final String mime, final String encoding) {
|
||||
baseUrl = url.trim();
|
||||
if (m_handler == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_handler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (m_webView == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_webView.loadUrl("about:blank");
|
||||
m_webView.loadDataWithBaseURL(INTERNAL_BASE_URL, html, mime, encoding, null);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
public void evaluateJavaScript(final String script) {
|
||||
if (m_handler == null || script == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (m_webView == null) {
|
||||
return;
|
||||
}
|
||||
m_webView.evaluateJavascript(script, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public boolean canGoBack() {
|
||||
final WebView view = m_webView;
|
||||
if (view == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final boolean[] ret = new boolean[1];
|
||||
ret[0] = false;
|
||||
boolean can = false;
|
||||
|
||||
final Semaphore semaphore = new Semaphore(0);
|
||||
if (m_activity != null) {
|
||||
m_activity.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ret[0] = view.canGoBack();
|
||||
semaphore.release();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
semaphore.acquire(1);
|
||||
can = ret[0];
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(TAG, e.toString());
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
return can;
|
||||
}
|
||||
|
||||
public void goBack() {
|
||||
if (m_handler == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (m_webView == null) {
|
||||
return;
|
||||
}
|
||||
m_webView.goBack();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public boolean canGoForward() {
|
||||
final WebView view = m_webView;
|
||||
if (view == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final boolean[] ret = new boolean[1];
|
||||
ret[0] = false;
|
||||
boolean can = false;
|
||||
|
||||
final Semaphore semaphore = new Semaphore(0);
|
||||
if (m_activity != null) {
|
||||
m_activity.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ret[0] = view.canGoForward();
|
||||
semaphore.release();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
semaphore.acquire(1);
|
||||
can = ret[0];
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(TAG, e.toString());
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
return can;
|
||||
}
|
||||
|
||||
public void goForward() {
|
||||
if (m_handler == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (m_webView == null) {
|
||||
return;
|
||||
}
|
||||
m_webView.goForward();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setBackgroundColor(final int color) {
|
||||
if (m_handler == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (m_webView == null) {
|
||||
return;
|
||||
}
|
||||
m_webView.setBackgroundColor(color);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setTextZoom(final int percent) {
|
||||
if (m_handler == null) {
|
||||
return;
|
||||
}
|
||||
final int clamped = Math.max(25, Math.min(500, percent));
|
||||
m_handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (m_webView == null) {
|
||||
return;
|
||||
}
|
||||
m_webView.getSettings().setTextZoom(clamped);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private int convertToDp(int input) {
|
||||
return (int)(input / m_displayDensity + 0.5f);
|
||||
}
|
||||
|
||||
public void setDefaultFontSize(int size) {
|
||||
if (m_handler == null) {
|
||||
return;
|
||||
}
|
||||
final int fontSize = size;
|
||||
m_handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
m_webView.getSettings().setDefaultFontSize(convertToDp(fontSize));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void setStandardFontFamily(String family) {
|
||||
if (m_handler == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final String fontFamily = family;
|
||||
m_handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
m_webView.getSettings().setStandardFontFamily(fontFamily);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -31,7 +31,4 @@ object QtAndroidController {
|
||||
|
||||
external fun onImeInsetsChanged(heightDp: Int)
|
||||
external fun onSystemBarsInsetsChanged(navBarHeightDp: Int, statusBarHeightDp: Int)
|
||||
|
||||
external fun onActivityPaused()
|
||||
external fun onActivityResumed()
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.qtproject.qt.android;
|
||||
|
||||
import android.view.ViewGroup;
|
||||
|
||||
public class WebViewControllerEx {
|
||||
|
||||
public static ViewGroup.LayoutParams createQtLayoutParams(int width, int height, int x, int y) {
|
||||
return new QtLayout.LayoutParams(width, height, x, y);
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,6 @@ import android.content.Context
|
||||
import android.net.VpnService.Builder
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.net.InetAddress
|
||||
import java.net.ServerSocket
|
||||
import java.util.UUID
|
||||
import go.Seq
|
||||
import org.amnezia.vpn.protocol.BadConfigException
|
||||
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.ip
|
||||
import org.amnezia.vpn.util.net.parseInetAddress
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
private const val TAG = "Xray"
|
||||
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() {
|
||||
|
||||
private var isRunning: Boolean = false
|
||||
@@ -80,10 +56,6 @@ class Xray : Protocol() {
|
||||
val xrayJsonConfig = config.optJSONObject("xray_config_data")
|
||||
?: config.optJSONObject("ssxray_config_data")
|
||||
?: 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)
|
||||
|
||||
(xrayJsonConfig.optJSONObject("log") ?: JSONObject().also { xrayJsonConfig.put("log", it) })
|
||||
@@ -125,22 +97,9 @@ class Xray : Protocol() {
|
||||
if (it.isNotBlank()) setMtu(it.toInt())
|
||||
}
|
||||
|
||||
val inbounds = xrayJsonConfig.getJSONArray("inbounds")
|
||||
val socksIdx = findSocksInboundIndex(inbounds)
|
||||
if (socksIdx < 0) {
|
||||
throw BadConfigException("socks inbound not found")
|
||||
}
|
||||
val socksConfig = inbounds.getJSONObject(socksIdx)
|
||||
val socksConfig = xrayJsonConfig.getJSONArray("inbounds")[0] as JSONObject
|
||||
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)
|
||||
configAppSplitTunneling(config)
|
||||
}
|
||||
@@ -203,10 +162,9 @@ class Xray : Protocol() {
|
||||
}
|
||||
|
||||
private fun runTun2Socks(config: XrayConfig, fd: Int) {
|
||||
val proxyUrl = "socks5://${config.socksUser}:${config.socksPass}@127.0.0.1:${config.socksPort}"
|
||||
val tun2SocksConfig = Tun2SocksConfig().apply {
|
||||
mtu = config.mtu.toLong()
|
||||
proxy = proxyUrl
|
||||
proxy = "socks5://127.0.0.1:${config.socksPort}"
|
||||
device = "fd://$fd"
|
||||
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 {
|
||||
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(
|
||||
protocolConfigBuilder: ProtocolConfig.Builder,
|
||||
val socksPort: Int,
|
||||
val socksUser: String,
|
||||
val socksPass: String,
|
||||
val maxMemory: Long,
|
||||
) : ProtocolConfig(protocolConfigBuilder) {
|
||||
|
||||
protected constructor(builder: Builder) : this(
|
||||
builder,
|
||||
builder.socksPort,
|
||||
builder.socksUser,
|
||||
builder.socksPass,
|
||||
builder.maxMemory
|
||||
)
|
||||
|
||||
@@ -26,12 +22,6 @@ class XrayConfig protected constructor(
|
||||
internal var socksPort: Int = 0
|
||||
private set
|
||||
|
||||
internal var socksUser: String = ""
|
||||
private set
|
||||
|
||||
internal var socksPass: String = ""
|
||||
private set
|
||||
|
||||
internal var maxMemory: Long = XRAY_DEFAULT_MAX_MEMORY
|
||||
private set
|
||||
|
||||
@@ -39,10 +29,6 @@ class XrayConfig protected constructor(
|
||||
|
||||
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 }
|
||||
|
||||
override fun build(): XrayConfig = configBuild().run { XrayConfig(this@Builder) }
|
||||
|
||||
@@ -83,26 +83,6 @@ add_compile_definitions(_WINSOCKAPI_)
|
||||
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
|
||||
set(BUILD_WITH_QT6 ON)
|
||||
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/qtkeychain)
|
||||
|
||||
if(ANDROID)
|
||||
# Use qtgamepad from amnezia-vpn/qtgamepad repository
|
||||
# Only if Qt6CorePrivate is available (required by qtgamepad)
|
||||
find_package(Qt6CorePrivate CONFIG QUIET)
|
||||
if(Qt6CorePrivate_FOUND)
|
||||
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/qtgamepad)
|
||||
# Link both the C++ module and QML plugin
|
||||
if(TARGET GamepadLegacy)
|
||||
target_link_libraries(${PROJECT} PRIVATE GamepadLegacy)
|
||||
endif()
|
||||
if(TARGET GamepadLegacyQuickPrivate)
|
||||
target_link_libraries(${PROJECT} PRIVATE GamepadLegacyQuickPrivate)
|
||||
endif()
|
||||
message(STATUS "Gamepad support enabled for Android")
|
||||
else()
|
||||
message(STATUS "Qt6CorePrivate not found. Gamepad support disabled for Android.")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(LIBS ${LIBS} qt6keychain)
|
||||
|
||||
include_directories(
|
||||
|
||||
@@ -121,7 +121,6 @@ target_sources(${PROJECT} PRIVATE
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/ScreenProtection.swift
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/VPNCController.swift
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/StoreKit2Helper.swift
|
||||
)
|
||||
|
||||
target_sources(${PROJECT} PRIVATE
|
||||
|
||||
@@ -131,7 +131,6 @@ target_sources(${PROJECT} PRIVATE
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/ScreenProtection.swift
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/VPNCController.swift
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/StoreKit2Helper.swift
|
||||
)
|
||||
|
||||
target_sources(${PROJECT} PRIVATE
|
||||
@@ -164,7 +163,7 @@ 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"
|
||||
COMMAND /usr/bin/codesign --force --sign "Apple Distribution"
|
||||
"$<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}
|
||||
COMMENT "Signing OpenVPNAdapter framework"
|
||||
|
||||
@@ -181,6 +181,7 @@ if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID))
|
||||
|
||||
set(HEADERS ${HEADERS}
|
||||
${CLIENT_ROOT_DIR}/core/ipcclient.h
|
||||
${CLIENT_ROOT_DIR}/core/privileged_process.h
|
||||
${CLIENT_ROOT_DIR}/ui/systemtray_notificationhandler.h
|
||||
${CLIENT_ROOT_DIR}/protocols/openvpnprotocol.h
|
||||
${CLIENT_ROOT_DIR}/protocols/openvpnovercloakprotocol.h
|
||||
@@ -193,6 +194,7 @@ if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID))
|
||||
|
||||
set(SOURCES ${SOURCES}
|
||||
${CLIENT_ROOT_DIR}/core/ipcclient.cpp
|
||||
${CLIENT_ROOT_DIR}/core/privileged_process.cpp
|
||||
${CLIENT_ROOT_DIR}/mozilla/localsocketcontroller.cpp
|
||||
${CLIENT_ROOT_DIR}/ui/systemtray_notificationhandler.cpp
|
||||
${CLIENT_ROOT_DIR}/protocols/openvpnprotocol.cpp
|
||||
|
||||
@@ -127,8 +127,7 @@ QMap<DockerContainer, QString> ContainerProps::containerDescriptions()
|
||||
QObject::tr("WireGuard - popular VPN protocol with high performance, high speed and low power "
|
||||
"consumption.") },
|
||||
{ DockerContainer::Awg,
|
||||
QObject::tr("AmneziaWG is a special protocol from Amnezia based on WireGuard. "
|
||||
"It provides high connection speed and ensures stable operation even in the most challenging network conditions.") },
|
||||
QObject::tr("AmneziaWG Legacy is a outdated version of AmneziaWG protocol. To upgrade, install AmneziaWG and recreate users.") },
|
||||
{ DockerContainer::Awg2,
|
||||
QObject::tr("AmneziaWG is a special protocol from Amnezia based on WireGuard. "
|
||||
"It provides high connection speed and ensures stable operation even in the most challenging network conditions.") },
|
||||
@@ -298,14 +297,14 @@ bool ContainerProps::isSupportedByCurrentPlatform(DockerContainer c)
|
||||
}
|
||||
|
||||
#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) {
|
||||
case DockerContainer::OpenVpn: return true;
|
||||
case DockerContainer::WireGuard: return true;
|
||||
case DockerContainer::Awg2: return true;
|
||||
case DockerContainer::Awg: return true;
|
||||
case DockerContainer::Xray: return true;
|
||||
case DockerContainer::SSXray: return true;
|
||||
case DockerContainer::OpenVpn:
|
||||
case DockerContainer::Cloak:
|
||||
case DockerContainer::ShadowSocks:
|
||||
return false;
|
||||
|
||||
@@ -11,8 +11,7 @@ namespace apiDefs
|
||||
AmneziaPremiumV1,
|
||||
AmneziaPremiumV2,
|
||||
SelfHosted,
|
||||
ExternalPremium,
|
||||
ExternalTrial
|
||||
ExternalPremium
|
||||
};
|
||||
|
||||
enum ConfigSource {
|
||||
@@ -33,7 +32,6 @@ namespace apiDefs
|
||||
constexpr QLatin1String stackType("stack_type");
|
||||
constexpr QLatin1String serviceType("service_type");
|
||||
constexpr QLatin1String cliVersion("cli_version");
|
||||
constexpr QLatin1String cliName("cli_name");
|
||||
constexpr QLatin1String supportedProtocols("supported_protocols");
|
||||
|
||||
constexpr QLatin1String vpnKey("vpn_key");
|
||||
@@ -55,14 +53,8 @@ namespace apiDefs
|
||||
constexpr QLatin1String activeDeviceCount("active_device_count");
|
||||
constexpr QLatin1String maxDeviceCount("max_device_count");
|
||||
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 subscriptionDescription("subscription_description");
|
||||
constexpr QLatin1String termsOfUseUrl("terms_of_use_url");
|
||||
constexpr QLatin1String privacyPolicyUrl("privacy_policy_url");
|
||||
|
||||
constexpr QLatin1String supportInfo("support_info");
|
||||
constexpr QLatin1String email("email");
|
||||
@@ -77,13 +69,11 @@ namespace apiDefs
|
||||
|
||||
constexpr QLatin1String transactionId("transaction_id");
|
||||
constexpr QLatin1String isTestPurchase("is_test_purchase");
|
||||
constexpr QLatin1String isInAppPurchase("is_in_app_purchase");
|
||||
|
||||
constexpr QLatin1String userCountryCode("user_country_code");
|
||||
|
||||
constexpr QLatin1String serviceInfo("service_info");
|
||||
constexpr QLatin1String isAdVisible("is_ad_visible");
|
||||
constexpr QLatin1String isRenewalAvailable("is_renewal_available");
|
||||
constexpr QLatin1String adHeader("ad_header");
|
||||
constexpr QLatin1String adDescription("ad_description");
|
||||
constexpr QLatin1String adEndpoint("ad_endpoint");
|
||||
|
||||
@@ -3,33 +3,11 @@
|
||||
#include <QDateTime>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
|
||||
namespace
|
||||
{
|
||||
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 output;
|
||||
@@ -46,30 +24,9 @@ namespace
|
||||
|
||||
bool apiUtils::isSubscriptionExpired(const QString &subscriptionEndDate)
|
||||
{
|
||||
if (subscriptionEndDate.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
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);
|
||||
QDateTime now = QDateTime::currentDateTimeUtc();
|
||||
QDateTime endDate = QDateTime::fromString(subscriptionEndDate, Qt::ISODateWithMs);
|
||||
return endDate < now;
|
||||
}
|
||||
|
||||
bool apiUtils::isServerFromApi(const QJsonObject &serverConfigObject)
|
||||
@@ -103,7 +60,6 @@ apiDefs::ConfigType apiUtils::getConfigType(const QJsonObject &serverConfigObjec
|
||||
constexpr QLatin1String servicePremium("amnezia-premium");
|
||||
constexpr QLatin1String serviceFree("amnezia-free");
|
||||
constexpr QLatin1String serviceExternalPremium("external-premium");
|
||||
constexpr QLatin1String serviceExternalTrial("external-trial");
|
||||
|
||||
auto apiConfigObject = serverConfigObject.value(apiDefs::key::apiConfig).toObject();
|
||||
auto serviceType = apiConfigObject.value(apiDefs::key::serviceType).toString();
|
||||
@@ -114,8 +70,6 @@ apiDefs::ConfigType apiUtils::getConfigType(const QJsonObject &serverConfigObjec
|
||||
return apiDefs::ConfigType::AmneziaFreeV3;
|
||||
} else if (serviceType == serviceExternalPremium) {
|
||||
return apiDefs::ConfigType::ExternalPremium;
|
||||
} else if (serviceType == serviceExternalTrial) {
|
||||
return apiDefs::ConfigType::ExternalTrial;
|
||||
}
|
||||
}
|
||||
default: {
|
||||
@@ -136,66 +90,50 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
|
||||
const int httpStatusCodeConflict = 409;
|
||||
const int httpStatusCodeNotFound = 404;
|
||||
const int httpStatusCodeNotImplemented = 501;
|
||||
const int httpStatusCodePaymentRequired = 402;
|
||||
const int httpStatusCodeUnprocessableEntity = 422;
|
||||
|
||||
if (!sslErrors.empty()) {
|
||||
qDebug().noquote() << sslErrors;
|
||||
return amnezia::ErrorCode::ApiConfigSslError;
|
||||
}
|
||||
if (replyError == QNetworkReply::NoError) {
|
||||
} else if (replyError == QNetworkReply::NoError) {
|
||||
return amnezia::ErrorCode::NoError;
|
||||
}
|
||||
if (replyError == QNetworkReply::NetworkError::OperationCanceledError
|
||||
|| replyError == QNetworkReply::NetworkError::TimeoutError) {
|
||||
} else if (replyError == QNetworkReply::NetworkError::OperationCanceledError
|
||||
|| replyError == QNetworkReply::NetworkError::TimeoutError) {
|
||||
qDebug() << replyError;
|
||||
return amnezia::ErrorCode::ApiConfigTimeoutError;
|
||||
}
|
||||
if (replyError == QNetworkReply::NetworkError::OperationNotImplementedError) {
|
||||
} else if (replyError == QNetworkReply::NetworkError::OperationNotImplementedError) {
|
||||
qDebug() << replyError;
|
||||
return amnezia::ErrorCode::ApiUpdateRequestError;
|
||||
}
|
||||
} else {
|
||||
qDebug() << QString::fromUtf8(responseBody);
|
||||
qDebug() << replyError;
|
||||
qDebug() << replyErrorString;
|
||||
qDebug() << httpStatusCode;
|
||||
|
||||
qDebug() << QString::fromUtf8(responseBody);
|
||||
qDebug() << replyError;
|
||||
qDebug() << httpStatusCode;
|
||||
int httpStatusFromBody = -1;
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
|
||||
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 (apiErrorMessageFromJson(jsonObj).contains(trialAlreadyUsedMessage, Qt::CaseInsensitive)) {
|
||||
return amnezia::ErrorCode::ApiTrialAlreadyUsedError;
|
||||
}
|
||||
return amnezia::ErrorCode::ApiConfigLimitError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodeNotFound) {
|
||||
} else if (httpStatusFromBody == httpStatusCodeNotFound) {
|
||||
return amnezia::ErrorCode::ApiNotFoundError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodeNotImplemented) {
|
||||
} else if (httpStatusFromBody == httpStatusCodeNotImplemented) {
|
||||
return amnezia::ErrorCode::ApiUpdateRequestError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodeUnprocessableEntity) {
|
||||
if (apiErrorMessageFromJson(jsonObj) == unprocessableSubscriptionMessage) {
|
||||
return amnezia::ErrorCode::ApiSubscriptionExpiredError;
|
||||
}
|
||||
return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodePaymentRequired) {
|
||||
return amnezia::ErrorCode::ApiSubscriptionNotActiveError;
|
||||
}
|
||||
return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||
}
|
||||
|
||||
qDebug() << "something went wrong";
|
||||
return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||
return amnezia::ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
bool apiUtils::isPremiumServer(const QJsonObject &serverConfigObject)
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -239,9 +177,7 @@ QString apiUtils::getPremiumV1VpnKey(const QJsonObject &serverConfigObject)
|
||||
|
||||
QString apiUtils::getPremiumV2VpnKey(const QJsonObject &serverConfigObject)
|
||||
{
|
||||
auto configType = apiUtils::getConfigType(serverConfigObject);
|
||||
if (configType != apiDefs::ConfigType::AmneziaPremiumV2 && configType != apiDefs::ConfigType::ExternalPremium
|
||||
&& configType != apiDefs::ConfigType::ExternalTrial) {
|
||||
if (apiUtils::getConfigType(serverConfigObject) != apiDefs::ConfigType::AmneziaPremiumV2) {
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
@@ -13,8 +13,6 @@ namespace apiUtils
|
||||
|
||||
bool isSubscriptionExpired(const QString &subscriptionEndDate);
|
||||
|
||||
bool isSubscriptionExpiringSoon(const QString &subscriptionEndDate, int withinDays = 30);
|
||||
|
||||
bool isPremiumServer(const QJsonObject &serverConfigObject);
|
||||
|
||||
apiDefs::ConfigType getConfigType(const QJsonObject &serverConfigObject);
|
||||
|
||||
@@ -91,12 +91,6 @@ void CoreController::initModels()
|
||||
m_apiServicesModel.reset(new ApiServicesModel(this));
|
||||
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_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));
|
||||
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_allowedDnsController.reset(new AllowedDnsController(m_settings, m_allowedDnsModel));
|
||||
@@ -157,11 +151,11 @@ void CoreController::initControllers()
|
||||
new ApiSettingsController(m_serversModel, m_apiAccountInfoModel, m_apiCountryModel, m_apiDevicesModel, m_settings));
|
||||
m_engine->rootContext()->setContextProperty("ApiSettingsController", m_apiSettingsController.get());
|
||||
|
||||
m_apiConfigsController.reset(
|
||||
new ApiConfigsController(m_serversModel, m_apiServicesModel, m_apiSubscriptionPlansModel, m_apiBenefitsModel, m_settings));
|
||||
m_apiConfigsController.reset(new ApiConfigsController(m_serversModel, m_apiServicesModel, m_settings));
|
||||
m_engine->rootContext()->setContextProperty("ApiConfigsController", m_apiConfigsController.get());
|
||||
connect(m_apiConfigsController.get(), &ApiConfigsController::subscriptionRefreshNeeded,
|
||||
this, [this]() { m_apiSettingsController->getAccountInfo(false); });
|
||||
|
||||
m_apiPremV1MigrationController.reset(new ApiPremV1MigrationController(m_serversModel, m_settings, this));
|
||||
m_engine->rootContext()->setContextProperty("ApiPremV1MigrationController", m_apiPremV1MigrationController.get());
|
||||
|
||||
m_apiNewsController.reset(new ApiNewsController(m_newsModel, m_settings, m_serversModel, this));
|
||||
m_engine->rootContext()->setContextProperty("ApiNewsController", m_apiNewsController.get());
|
||||
@@ -237,6 +231,8 @@ void CoreController::initSignalHandlers()
|
||||
initAutoConnectHandler();
|
||||
initAmneziaDnsToggledHandler();
|
||||
initPrepareConfigHandler();
|
||||
initImportPremiumV2VpnKeyHandler();
|
||||
initShowMigrationDrawerHandler();
|
||||
initStrictKillSwitchHandler();
|
||||
}
|
||||
|
||||
@@ -377,11 +373,7 @@ void CoreController::initPrepareConfigHandler()
|
||||
return;
|
||||
}
|
||||
|
||||
m_installController->validateConfig();
|
||||
});
|
||||
|
||||
connect(m_installController.get(), &InstallController::configValidated, this, [this](bool isValid) {
|
||||
if (!isValid) {
|
||||
if (!m_installController->isConfigValid()) {
|
||||
emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected);
|
||||
return;
|
||||
}
|
||||
@@ -390,6 +382,25 @@ void CoreController::initPrepareConfigHandler()
|
||||
});
|
||||
}
|
||||
|
||||
void CoreController::initImportPremiumV2VpnKeyHandler()
|
||||
{
|
||||
connect(m_apiPremV1MigrationController.get(), &ApiPremV1MigrationController::importPremiumV2VpnKey, this, [this](const QString &vpnKey) {
|
||||
m_importController->extractConfigFromData(vpnKey);
|
||||
m_importController->importConfig();
|
||||
|
||||
emit m_apiPremV1MigrationController->migrationFinished();
|
||||
});
|
||||
}
|
||||
|
||||
void CoreController::initShowMigrationDrawerHandler()
|
||||
{
|
||||
QTimer::singleShot(1000, this, [this]() {
|
||||
if (m_apiPremV1MigrationController->isPremV1MigrationReminderActive() && m_apiPremV1MigrationController->hasConfigsToMigration()) {
|
||||
m_apiPremV1MigrationController->showMigrationDrawer();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void CoreController::initStrictKillSwitchHandler()
|
||||
{
|
||||
connect(m_settingsController.get(), &SettingsController::strictKillSwitchEnabledChanged, m_vpnConnection.get(),
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
#include "ui/controllers/api/apiConfigsController.h"
|
||||
#include "ui/controllers/api/apiSettingsController.h"
|
||||
#include "ui/controllers/api/apiPremV1MigrationController.h"
|
||||
#include "ui/controllers/api/apiNewsController.h"
|
||||
#include "ui/controllers/appSplitTunnelingController.h"
|
||||
#include "ui/controllers/allowedDnsController.h"
|
||||
@@ -32,11 +33,9 @@
|
||||
#include "ui/models/protocols/ikev2ConfigModel.h"
|
||||
#endif
|
||||
#include "ui/models/api/apiAccountInfoModel.h"
|
||||
#include "ui/models/api/apiBenefitsModel.h"
|
||||
#include "ui/models/api/apiCountryModel.h"
|
||||
#include "ui/models/api/apiDevicesModel.h"
|
||||
#include "ui/models/api/apiServicesModel.h"
|
||||
#include "ui/models/api/apiSubscriptionPlansModel.h"
|
||||
#include "ui/models/appSplitTunnelingModel.h"
|
||||
#include "ui/models/clientManagementModel.h"
|
||||
#include "ui/models/protocols/awgConfigModel.h"
|
||||
@@ -94,6 +93,8 @@ private:
|
||||
void initAutoConnectHandler();
|
||||
void initAmneziaDnsToggledHandler();
|
||||
void initPrepareConfigHandler();
|
||||
void initImportPremiumV2VpnKeyHandler();
|
||||
void initShowMigrationDrawerHandler();
|
||||
void initStrictKillSwitchHandler();
|
||||
|
||||
QQmlApplicationEngine *m_engine {}; // TODO use parent child system here?
|
||||
@@ -121,6 +122,7 @@ private:
|
||||
|
||||
QScopedPointer<ApiSettingsController> m_apiSettingsController;
|
||||
QScopedPointer<ApiConfigsController> m_apiConfigsController;
|
||||
QScopedPointer<ApiPremV1MigrationController> m_apiPremV1MigrationController;
|
||||
QScopedPointer<ApiNewsController> m_apiNewsController;
|
||||
|
||||
QSharedPointer<ContainersModel> m_containersModel;
|
||||
@@ -135,8 +137,6 @@ private:
|
||||
QSharedPointer<ClientManagementModel> m_clientManagementModel;
|
||||
|
||||
QSharedPointer<ApiServicesModel> m_apiServicesModel;
|
||||
QSharedPointer<ApiSubscriptionPlansModel> m_apiSubscriptionPlansModel;
|
||||
QSharedPointer<ApiBenefitsModel> m_apiBenefitsModel;
|
||||
QSharedPointer<ApiCountryModel> m_apiCountryModel;
|
||||
QSharedPointer<ApiAccountInfoModel> m_apiAccountInfoModel;
|
||||
QSharedPointer<ApiDevicesModel> m_apiDevicesModel;
|
||||
|
||||
@@ -44,13 +44,8 @@ namespace
|
||||
|
||||
constexpr int httpStatusCodeNotFound = 404;
|
||||
constexpr int httpStatusCodeConflict = 409;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
|
||||
@@ -286,30 +281,23 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
|
||||
auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString("");
|
||||
auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString("");
|
||||
|
||||
QStringList primaryBaseUrls;
|
||||
QStringList fallbackBaseUrls;
|
||||
QStringList baseUrls;
|
||||
if (m_isDevEnvironment) {
|
||||
primaryBaseUrls = QString(DEV_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
baseUrls = QString(DEV_S3_ENDPOINT).split(", ");
|
||||
} else {
|
||||
primaryBaseUrls = QString(PROD_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
fallbackBaseUrls = QString(FALLBACK_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
baseUrls = QString(PROD_S3_ENDPOINT).split(", ");
|
||||
}
|
||||
|
||||
auto appendStorageUrls = [&serviceType, &userCountryCode](const QStringList &baseUrls, QStringList &target) {
|
||||
if (!serviceType.isEmpty()) {
|
||||
for (const auto &baseUrl : baseUrls) {
|
||||
QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8();
|
||||
target.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) + ".json");
|
||||
}
|
||||
}
|
||||
for (const auto &baseUrl : baseUrls) {
|
||||
target.push_back(baseUrl + "endpoints.json");
|
||||
}
|
||||
};
|
||||
|
||||
QStringList proxyStorageUrls;
|
||||
appendStorageUrls(primaryBaseUrls, proxyStorageUrls);
|
||||
appendStorageUrls(fallbackBaseUrls, proxyStorageUrls);
|
||||
if (!serviceType.isEmpty()) {
|
||||
for (const auto &baseUrl : baseUrls) {
|
||||
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, [this, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) {
|
||||
getProxyUrlAsync(proxyUrls, 0, [this, encRequestData, endpoint, processResponse](const QString &proxyUrl) {
|
||||
@@ -336,48 +324,31 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
|
||||
QStringList GatewayController::getProxyUrls(const QString &serviceType, const QString &userCountryCode)
|
||||
{
|
||||
QNetworkRequest request;
|
||||
request.setTransferTimeout(proxyStorageRequestTimeoutMsecs);
|
||||
request.setTransferTimeout(m_requestTimeoutMsecs);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
|
||||
QEventLoop wait;
|
||||
QList<QSslError> sslErrors;
|
||||
QNetworkReply *reply;
|
||||
|
||||
QStringList primaryBaseUrls;
|
||||
QStringList fallbackBaseUrls;
|
||||
QStringList baseUrls;
|
||||
if (m_isDevEnvironment) {
|
||||
primaryBaseUrls = QString(DEV_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
baseUrls = QString(DEV_S3_ENDPOINT).split(", ");
|
||||
} else {
|
||||
primaryBaseUrls = QString(PROD_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
fallbackBaseUrls = QString(FALLBACK_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
baseUrls = QString(PROD_S3_ENDPOINT).split(", ");
|
||||
}
|
||||
|
||||
std::random_device randomDevice;
|
||||
std::mt19937 generator(randomDevice());
|
||||
std::shuffle(primaryBaseUrls.begin(), primaryBaseUrls.end(), generator);
|
||||
std::shuffle(fallbackBaseUrls.begin(), fallbackBaseUrls.end(), generator);
|
||||
|
||||
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||
|
||||
auto appendStorageUrls = [&serviceType, &userCountryCode](const QStringList &baseUrls, QStringList &target) {
|
||||
if (!serviceType.isEmpty()) {
|
||||
for (const auto &baseUrl : baseUrls) {
|
||||
QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8();
|
||||
target.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) + ".json");
|
||||
}
|
||||
}
|
||||
for (const auto &baseUrl : baseUrls) {
|
||||
target.push_back(baseUrl + "endpoints.json");
|
||||
}
|
||||
};
|
||||
|
||||
QStringList proxyStorageUrls;
|
||||
appendStorageUrls(primaryBaseUrls, proxyStorageUrls);
|
||||
appendStorageUrls(fallbackBaseUrls, proxyStorageUrls);
|
||||
|
||||
if (proxyStorageUrls.empty()) {
|
||||
qDebug() << "empty storage endpoint list";
|
||||
return {};
|
||||
if (!serviceType.isEmpty()) {
|
||||
for (const auto &baseUrl : baseUrls) {
|
||||
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");
|
||||
}
|
||||
|
||||
for (const auto &proxyStorageUrl : proxyStorageUrls) {
|
||||
@@ -441,14 +412,12 @@ bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &rep
|
||||
{
|
||||
const QByteArray &responseBody = decryptedResponseBody;
|
||||
|
||||
int apiHttpStatus = -1;
|
||||
QString apiErrorMessage;
|
||||
int httpStatus = -1;
|
||||
if (isDecryptionSuccessful) {
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
|
||||
if (jsonDoc.isObject()) {
|
||||
QJsonObject jsonObj = jsonDoc.object();
|
||||
apiHttpStatus = jsonObj.value("http_status").toInt(-1);
|
||||
apiErrorMessage = jsonObj.value(QStringLiteral("message")).toString().trimmed();
|
||||
httpStatus = jsonObj.value("http_status").toInt(-1);
|
||||
}
|
||||
} else {
|
||||
qDebug() << "failed to decrypt the data";
|
||||
@@ -459,12 +428,10 @@ bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &rep
|
||||
qDebug() << "timeout occurred";
|
||||
qDebug() << replyError;
|
||||
return true;
|
||||
}
|
||||
if (responseBody.contains("html")) {
|
||||
} else if (responseBody.contains("html")) {
|
||||
qDebug() << "the response contains an html tag";
|
||||
return true;
|
||||
}
|
||||
if (apiHttpStatus == httpStatusCodeNotFound) {
|
||||
} else if (httpStatus == httpStatusCodeNotFound) {
|
||||
if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2)
|
||||
|| responseBody.contains(errorResponsePattern3)) {
|
||||
return false;
|
||||
@@ -472,25 +439,16 @@ bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &rep
|
||||
qDebug() << replyError;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (apiHttpStatus == httpStatusCodeNotImplemented) {
|
||||
} else if (httpStatus == httpStatusCodeNotImplemented) {
|
||||
if (responseBody.contains(updateRequestResponsePattern)) {
|
||||
return false;
|
||||
} else {
|
||||
qDebug() << replyError;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (apiHttpStatus == httpStatusCodeConflict) {
|
||||
} else if (httpStatus == httpStatusCodeConflict) {
|
||||
return false;
|
||||
}
|
||||
if (apiHttpStatus == httpStatusCodePaymentRequired) {
|
||||
return false;
|
||||
}
|
||||
if (apiHttpStatus == httpStatusCodeUnprocessableEntity) {
|
||||
return apiErrorMessage != unprocessableSubscriptionMessage;
|
||||
}
|
||||
if (replyError != QNetworkReply::NetworkError::NoError) {
|
||||
} else if (replyError != QNetworkReply::NetworkError::NoError) {
|
||||
qDebug() << replyError;
|
||||
return true;
|
||||
}
|
||||
@@ -579,7 +537,7 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co
|
||||
}
|
||||
|
||||
QNetworkRequest request;
|
||||
request.setTransferTimeout(proxyStorageRequestTimeoutMsecs);
|
||||
request.setTransferTimeout(m_requestTimeoutMsecs);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
request.setUrl(proxyStorageUrls[currentProxyStorageIndex]);
|
||||
|
||||
|
||||
@@ -419,18 +419,6 @@ ErrorCode ServerController::installDockerWorker(const ServerCredentials &credent
|
||||
cbReadStdOut, cbReadStdErr);
|
||||
|
||||
qDebug().noquote() << "ServerController::installDockerWorker" << stdOut;
|
||||
if (container == DockerContainer::Awg2) {
|
||||
QRegularExpression regex(R"(Linux\s+(\d+)\.(\d+)[^\d]*)");
|
||||
QRegularExpressionMatch match = regex.match(stdOut);
|
||||
if (match.hasMatch()) {
|
||||
int majorVersion = match.captured(1).toInt();
|
||||
int minorVersion = match.captured(2).toInt();
|
||||
|
||||
if (majorVersion < 4 || (majorVersion == 4 && minorVersion < 14)) {
|
||||
return ErrorCode::ServerLinuxKernelTooOld;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (stdOut.contains("lock"))
|
||||
return ErrorCode::ServerPacketManagerError;
|
||||
if (stdOut.contains("command not found"))
|
||||
|
||||
@@ -61,7 +61,6 @@ namespace amnezia
|
||||
ServerDockerOnCgroupsV2 = 211,
|
||||
ServerCgroupMountpoint = 212,
|
||||
DockerPullRateLimit = 213,
|
||||
ServerLinuxKernelTooOld = 214,
|
||||
|
||||
// Ssh connection errors
|
||||
SshRequestDeniedError = 300,
|
||||
@@ -123,9 +122,6 @@ namespace amnezia
|
||||
ApiUpdateRequestError = 1111,
|
||||
ApiSubscriptionExpiredError = 1112,
|
||||
ApiPurchaseError = 1113,
|
||||
ApiSubscriptionNotActiveError = 1114,
|
||||
ApiNoPurchasedSubscriptionsError = 1115,
|
||||
ApiTrialAlreadyUsedError = 1116,
|
||||
|
||||
// QFile errors
|
||||
OpenError = 1200,
|
||||
|
||||
@@ -29,7 +29,6 @@ QString errorString(ErrorCode code) {
|
||||
case(ErrorCode::ServerDockerOnCgroupsV2): errorMessage = QObject::tr("Docker error: runc doesn't work on cgroups v2"); break;
|
||||
case(ErrorCode::ServerCgroupMountpoint): errorMessage = QObject::tr("Server error: cgroup mountpoint does not exist"); break;
|
||||
case(ErrorCode::DockerPullRateLimit): errorMessage = QObject::tr("Docker error: The pull rate limit has been reached"); break;
|
||||
case(ErrorCode::ServerLinuxKernelTooOld): errorMessage = QObject::tr("Server error: Linux kernel is too old"); break;
|
||||
|
||||
// Libssh errors
|
||||
case(ErrorCode::SshRequestDeniedError): errorMessage = QObject::tr("SSH request was denied"); break;
|
||||
@@ -80,9 +79,6 @@ QString errorString(ErrorCode code) {
|
||||
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::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
|
||||
case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break;
|
||||
|
||||
@@ -7,6 +7,7 @@ IpcClient::IpcClient(QObject *parent) : QObject(parent)
|
||||
{
|
||||
m_node.connectToNode(QUrl("local:" + amnezia::getIpcServiceUrl()));
|
||||
m_interface.reset(m_node.acquire<IpcInterfaceReplica>());
|
||||
m_tun2socks.reset(m_node.acquire<IpcProcessTun2SocksReplica>());
|
||||
}
|
||||
|
||||
IpcClient& IpcClient::Instance()
|
||||
@@ -32,43 +33,68 @@ QSharedPointer<IpcInterfaceReplica> IpcClient::Interface()
|
||||
return rep;
|
||||
}
|
||||
|
||||
QSharedPointer<IpcProcessInterfaceReplica> IpcClient::CreatePrivilegedProcess()
|
||||
QSharedPointer<IpcProcessTun2SocksReplica> IpcClient::InterfaceTun2Socks()
|
||||
{
|
||||
return withInterface([](QSharedPointer<IpcInterfaceReplica> &iface) -> QSharedPointer<IpcProcessInterfaceReplica> {
|
||||
auto createPrivilegedProcess = iface->createPrivilegedProcess();
|
||||
if (!createPrivilegedProcess.waitForFinished()) {
|
||||
qCritical() << "Failed to create privileged process";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const int pid = createPrivilegedProcess.returnValue();
|
||||
|
||||
auto* node = new QRemoteObjectNode();
|
||||
node->connectToNode(QUrl(QString("local:%1").arg(amnezia::getIpcProcessUrl(pid))));
|
||||
|
||||
QSharedPointer<IpcProcessInterfaceReplica> rep(
|
||||
node->acquire<IpcProcessInterfaceReplica>(),
|
||||
[node] (IpcProcessInterfaceReplica *ptr) {
|
||||
delete ptr;
|
||||
node->deleteLater();
|
||||
}
|
||||
);
|
||||
if (rep.isNull()) {
|
||||
qCritical() << "IpcClient::CreatePrivilegedProcess(): Failed to acquire replica";
|
||||
return nullptr;
|
||||
}
|
||||
if (!rep->waitForSource()) {
|
||||
qCritical() << "IpcClient::CreatePrivilegedProcess(): Failed to initialize replica";
|
||||
return nullptr;
|
||||
}
|
||||
if (!rep->isReplicaValid()) {
|
||||
qCritical() << "IpcClient::CreatePrivilegedProcess(): Replica is invalid";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return rep;
|
||||
},
|
||||
[]() -> QSharedPointer<IpcProcessInterfaceReplica> {
|
||||
QSharedPointer<IpcProcessTun2SocksReplica> rep = Instance().m_tun2socks;
|
||||
if (rep.isNull()) {
|
||||
qCritical() << "IpcClient::InterfaceTun2Socks: Replica is undefined";
|
||||
return nullptr;
|
||||
});
|
||||
}
|
||||
if (!rep->waitForSource(1000)) {
|
||||
qCritical() << "IpcClient::InterfaceTun2Socks: Failed to initialize replica";
|
||||
return nullptr;
|
||||
}
|
||||
if (!rep->isReplicaValid()) {
|
||||
qWarning() << "IpcClient::InterfaceTun2Socks(): Replica is invalid";
|
||||
}
|
||||
return rep;
|
||||
}
|
||||
|
||||
QSharedPointer<PrivilegedProcess> IpcClient::CreatePrivilegedProcess()
|
||||
{
|
||||
QSharedPointer<IpcInterfaceReplica> rep = Interface();
|
||||
if (!rep) {
|
||||
qCritical() << "IpcClient::createPrivilegedProcess: Replica is invalid";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QRemoteObjectPendingReply<int> pidReply = rep->createPrivilegedProcess();
|
||||
if (!pidReply.waitForFinished(5000)){
|
||||
qCritical() << "IpcClient::createPrivilegedProcess: Failed to execute RO createPrivilegedProcess call";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int pid = pidReply.returnValue();
|
||||
QSharedPointer<ProcessDescriptor> pd(new ProcessDescriptor());
|
||||
|
||||
pd->localSocket.reset(new QLocalSocket(pd->replicaNode.data()));
|
||||
|
||||
connect(pd->localSocket.data(), &QLocalSocket::connected, pd->replicaNode.data(), [pd]() {
|
||||
pd->replicaNode->addClientSideConnection(pd->localSocket.data());
|
||||
|
||||
IpcProcessInterfaceReplica *repl = pd->replicaNode->acquire<IpcProcessInterfaceReplica>();
|
||||
// TODO: rework the unsafe cast below
|
||||
PrivilegedProcess *priv = static_cast<PrivilegedProcess *>(repl);
|
||||
pd->ipcProcess.reset(priv);
|
||||
if (!pd->ipcProcess) {
|
||||
qWarning() << "Acquire PrivilegedProcess failed";
|
||||
} else {
|
||||
pd->ipcProcess->waitForSource(1000);
|
||||
if (!pd->ipcProcess->isReplicaValid()) {
|
||||
qWarning() << "PrivilegedProcess replica is not connected!";
|
||||
}
|
||||
|
||||
QObject::connect(pd->ipcProcess.data(), &PrivilegedProcess::destroyed, pd->ipcProcess.data(),
|
||||
[pd]() { pd->replicaNode->deleteLater(); });
|
||||
}
|
||||
});
|
||||
|
||||
pd->localSocket->connectToServer(amnezia::getIpcProcessUrl(pid));
|
||||
if (!pd->localSocket->waitForConnected()) {
|
||||
qCritical() << "IpcClient::createPrivilegedProcess: Failed to connect to process' socket";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto processReplica = QSharedPointer<PrivilegedProcess>(pd->ipcProcess);
|
||||
return processReplica;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
#include <QObject>
|
||||
|
||||
#include "rep_ipc_interface_replica.h"
|
||||
#include "rep_ipc_process_interface_replica.h"
|
||||
#include "rep_ipc_process_tun2socks_replica.h"
|
||||
|
||||
#include "privileged_process.h"
|
||||
|
||||
class IpcClient : public QObject
|
||||
{
|
||||
@@ -16,7 +18,8 @@ public:
|
||||
static IpcClient& Instance();
|
||||
|
||||
static QSharedPointer<IpcInterfaceReplica> Interface();
|
||||
static QSharedPointer<IpcProcessInterfaceReplica> CreatePrivilegedProcess();
|
||||
static QSharedPointer<IpcProcessTun2SocksReplica> InterfaceTun2Socks();
|
||||
static QSharedPointer<PrivilegedProcess> CreatePrivilegedProcess();
|
||||
|
||||
template <typename Func>
|
||||
static auto withInterface(Func func)
|
||||
@@ -51,6 +54,18 @@ signals:
|
||||
private:
|
||||
QRemoteObjectNode m_node;
|
||||
QSharedPointer<IpcInterfaceReplica> m_interface;
|
||||
QSharedPointer<IpcProcessTun2SocksReplica> m_tun2socks;
|
||||
|
||||
struct ProcessDescriptor {
|
||||
ProcessDescriptor () {
|
||||
replicaNode = QSharedPointer<QRemoteObjectNode>(new QRemoteObjectNode());
|
||||
ipcProcess = QSharedPointer<PrivilegedProcess>();
|
||||
localSocket = QSharedPointer<QLocalSocket>();
|
||||
}
|
||||
QSharedPointer<PrivilegedProcess> ipcProcess;
|
||||
QSharedPointer<QRemoteObjectNode> replicaNode;
|
||||
QSharedPointer<QLocalSocket> localSocket;
|
||||
};
|
||||
};
|
||||
|
||||
#endif // IPCCLIENT_H
|
||||
|
||||
27
client/core/privileged_process.cpp
Normal file
27
client/core/privileged_process.cpp
Normal file
@@ -0,0 +1,27 @@
|
||||
#include "privileged_process.h"
|
||||
|
||||
PrivilegedProcess::PrivilegedProcess() :
|
||||
IpcProcessInterfaceReplica()
|
||||
{
|
||||
}
|
||||
|
||||
PrivilegedProcess::~PrivilegedProcess()
|
||||
{
|
||||
qDebug() << "PrivilegedProcess::~PrivilegedProcess()";
|
||||
}
|
||||
|
||||
void PrivilegedProcess::waitForFinished(int msecs)
|
||||
{
|
||||
QSharedPointer<QEventLoop> loop(new QEventLoop);
|
||||
connect(this, &PrivilegedProcess::finished, this, [this, loop](int exitCode, QProcess::ExitStatus exitStatus) mutable{
|
||||
loop->quit();
|
||||
loop.clear();
|
||||
});
|
||||
|
||||
QTimer::singleShot(msecs, this, [this, loop]() mutable {
|
||||
loop->quit();
|
||||
loop.clear();
|
||||
});
|
||||
|
||||
loop->exec();
|
||||
}
|
||||
24
client/core/privileged_process.h
Normal file
24
client/core/privileged_process.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#ifndef PRIVILEGED_PROCESS_H
|
||||
#define PRIVILEGED_PROCESS_H
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "rep_ipc_process_interface_replica.h"
|
||||
// This class is dangerous - instance of this class casted from base class,
|
||||
// so it support only functions
|
||||
// Do not add any members into it
|
||||
//
|
||||
class PrivilegedProcess : public IpcProcessInterfaceReplica
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
PrivilegedProcess();
|
||||
~PrivilegedProcess() override;
|
||||
|
||||
void waitForFinished(int msecs);
|
||||
|
||||
};
|
||||
|
||||
#endif // PRIVILEGED_PROCESS_H
|
||||
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
#include <QString>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QList>
|
||||
#include <QHostAddress>
|
||||
#include <QRandomGenerator>
|
||||
#include <QTcpServer>
|
||||
#include <stdexcept>
|
||||
#include "3rd/QJsonStruct/QJsonIO.hpp"
|
||||
#include "transfer.h"
|
||||
#include "serialization.h"
|
||||
@@ -19,125 +14,25 @@ namespace amnezia::serialization::inbounds
|
||||
// "port": 10808,
|
||||
// "protocol": "socks",
|
||||
// "settings": {
|
||||
// "auth": "password",
|
||||
// "accounts": [{"user": "...", "pass": "..."}],
|
||||
// "udp": true
|
||||
// }
|
||||
// }
|
||||
//],
|
||||
|
||||
const static QString listen = "127.0.0.1";
|
||||
const static int defaultPort = 10808;
|
||||
const static int port = 10808;
|
||||
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 root;
|
||||
QJsonIO::SetValue(root, listen, "listen");
|
||||
QJsonIO::SetValue(root, defaultPort, "port");
|
||||
QJsonIO::SetValue(root, port, "port");
|
||||
QJsonIO::SetValue(root, protocol, "protocol");
|
||||
QJsonIO::SetValue(root, true, "settings", "udp");
|
||||
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
|
||||
|
||||
|
||||
@@ -60,24 +60,7 @@ namespace amnezia::serialization
|
||||
|
||||
namespace inbounds
|
||||
{
|
||||
struct InboundCredentials {
|
||||
QString username;
|
||||
QString password;
|
||||
int port;
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
124
client/core/webview/CMakeLists.txt
Normal file
124
client/core/webview/CMakeLists.txt
Normal file
@@ -0,0 +1,124 @@
|
||||
get_filename_component(DIR_NAME ${CMAKE_CURRENT_SOURCE_DIR} NAME)
|
||||
message("Configuring " ${DIR_NAME})
|
||||
|
||||
set(webview_URI AmneziaWebView)
|
||||
|
||||
find_package(QT NAMES Qt6 REQUIRED COMPONENTS Quick)
|
||||
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Quick)
|
||||
|
||||
# Widgets and WebEngineWidgets are only available on desktop platforms
|
||||
if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
|
||||
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets)
|
||||
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS WebEngineWidgets)
|
||||
endif()
|
||||
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_INCLUDE_CURRENT_DIR ON)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
|
||||
set(PLUGIN_CLASS_NAME WebViewPlugin)
|
||||
add_definitions(-DURI=${webview_URI})
|
||||
|
||||
include_directories(${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
set(webview_HEADERS
|
||||
amneziawebview.h
|
||||
amneziawebview_p.h
|
||||
websettings.h
|
||||
mimecache.h
|
||||
filehandler.h
|
||||
qrchandler.h
|
||||
jshandler.h
|
||||
plugin.h
|
||||
amneziawebhistory.h
|
||||
amneziawebhistory_p.h
|
||||
)
|
||||
|
||||
set(webview_SOURCES
|
||||
amneziawebview.cpp
|
||||
amneziawebview_p.cpp
|
||||
websettings.cpp
|
||||
mimecache.cpp
|
||||
qrchandler.cpp
|
||||
jshandler.cpp
|
||||
filehandler.cpp
|
||||
plugin.cpp
|
||||
amneziawebhistory.cpp
|
||||
)
|
||||
|
||||
if (CMAKE_CROSSCOMPILING AND ANDROID)
|
||||
|
||||
list(APPEND webview_SOURCES
|
||||
"${CMAKE_CURRENT_LIST_DIR}/jshandler_android.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/qrchandler_android.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/filehandler_android.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/amneziawebview_android.cpp"
|
||||
)
|
||||
|
||||
endif ()
|
||||
|
||||
if (CMAKE_CROSSCOMPILING AND APPLE)
|
||||
|
||||
add_definitions(-DENABLE_WKWEBVIEW)
|
||||
list(APPEND webview_SOURCES
|
||||
"${CMAKE_CURRENT_LIST_DIR}/amneziawebview_ios.mm"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/qrchandler_ios.mm"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/jshandler_ios.mm"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/filehandler_ios.mm"
|
||||
)
|
||||
|
||||
endif ()
|
||||
|
||||
if (NOT CMAKE_CROSSCOMPILING)
|
||||
# Require WebEngineWidgets for desktop platforms (QtWebKit is not available in Qt 6)
|
||||
if (Qt6WebEngineWidgets_FOUND)
|
||||
message(STATUS "Using Qt WebEngineWidgets for desktop webview")
|
||||
list(APPEND webview_HEADERS
|
||||
amneziawebview_webengine_p.h
|
||||
)
|
||||
list(APPEND webview_SOURCES
|
||||
amneziawebview_webengine.cpp
|
||||
)
|
||||
else ()
|
||||
message(FATAL_ERROR "Qt WebEngineWidgets is required for desktop builds. QtWebKit is not available in Qt 6. Please install Qt WebEngineWidgets module.")
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
add_library(webview STATIC ${webview_SOURCES} ${webview_HEADERS})
|
||||
|
||||
target_compile_definitions(webview PRIVATE
|
||||
QT_PLUGIN
|
||||
QT_STATICPLUGIN
|
||||
)
|
||||
|
||||
target_link_libraries(webview PUBLIC
|
||||
Qt${QT_VERSION_MAJOR}::Core
|
||||
Qt${QT_VERSION_MAJOR}::Quick
|
||||
)
|
||||
|
||||
# Widgets and WebEngineWidgets are only available on desktop platforms
|
||||
if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
|
||||
if(TARGET Qt${QT_VERSION_MAJOR}::Widgets)
|
||||
target_link_libraries(webview PUBLIC Qt${QT_VERSION_MAJOR}::Widgets)
|
||||
endif()
|
||||
|
||||
if (Qt6WebEngineWidgets_FOUND)
|
||||
target_link_libraries(webview PRIVATE
|
||||
Qt${QT_VERSION_MAJOR}::WebEngineWidgets
|
||||
)
|
||||
endif ()
|
||||
endif()
|
||||
|
||||
# Link WebKit framework for iOS
|
||||
if (CMAKE_CROSSCOMPILING AND APPLE)
|
||||
find_library(FW_WEBKIT WebKit)
|
||||
if(FW_WEBKIT)
|
||||
target_link_libraries(webview PRIVATE ${FW_WEBKIT})
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set_target_properties(webview PROPERTIES AUTOMOC_MOC_OPTIONS "-Muri=${webview_URI}")
|
||||
|
||||
#include(precompiled.headers)
|
||||
#add_precompiled_header(webview pch.h FORCEINCLUDE)
|
||||
434
client/core/webview/amneziawebhistory.cpp
Normal file
434
client/core/webview/amneziawebhistory.cpp
Normal file
@@ -0,0 +1,434 @@
|
||||
#include "amneziawebhistory.h"
|
||||
#include "amneziawebhistory_p.h"
|
||||
#include "amneziawebview.h"
|
||||
#include "amneziawebview_p.h"
|
||||
|
||||
#include <QSharedData>
|
||||
#include <QDebug>
|
||||
|
||||
|
||||
/*!
|
||||
Constructs a history item from \a other. The new item and \a other
|
||||
will share their data, and modifying either this item or \a other will
|
||||
modify both instances.
|
||||
*/
|
||||
AmneziaWebHistoryItem::AmneziaWebHistoryItem(const AmneziaWebHistoryItem &other)
|
||||
: d_ptr(other.d_ptr)
|
||||
{
|
||||
}
|
||||
|
||||
/*!
|
||||
Assigns the \a other history item to this. This item and \a other
|
||||
will share their data, and modifying either this item or \a other will
|
||||
modify both instances.
|
||||
*/
|
||||
AmneziaWebHistoryItem &AmneziaWebHistoryItem::operator=(const AmneziaWebHistoryItem &other)
|
||||
{
|
||||
d_ptr = other.d_ptr;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/*!
|
||||
Destroys the history item.
|
||||
*/
|
||||
AmneziaWebHistoryItem::~AmneziaWebHistoryItem()
|
||||
{
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the URL associated with the history item.
|
||||
|
||||
\sa originalUrl(), title(), lastVisited(), data(), mimeType()
|
||||
*/
|
||||
QUrl AmneziaWebHistoryItem::url() const
|
||||
{
|
||||
Q_D(const AmneziaWebHistoryItem);
|
||||
if (d)
|
||||
return d->url();
|
||||
return QUrl();
|
||||
}
|
||||
|
||||
|
||||
/*!
|
||||
Returns the title of the page associated with the history item.
|
||||
|
||||
\sa icon(), url(), lastVisited(), data(), mimeType()
|
||||
*/
|
||||
QString AmneziaWebHistoryItem::title() const
|
||||
{
|
||||
Q_D(const AmneziaWebHistoryItem);
|
||||
if (d)
|
||||
return d->title();
|
||||
return QString();
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the icon associated with the history item.
|
||||
|
||||
\sa title(), url(), lastVisited(), data(), mimeType()
|
||||
*/
|
||||
QIcon AmneziaWebHistoryItem::icon() const
|
||||
{
|
||||
Q_D(const AmneziaWebHistoryItem);
|
||||
if (d)
|
||||
return d->icon();
|
||||
|
||||
return QIcon();
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the data associated with the history item.
|
||||
|
||||
\sa icon(), title(), url(), lastVisited(), mimeType()
|
||||
*/
|
||||
QByteArray AmneziaWebHistoryItem::data() const
|
||||
{
|
||||
Q_D(const AmneziaWebHistoryItem);
|
||||
if(d) return d->data();
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the mimeType associated with the history item.
|
||||
|
||||
\sa icon(), title(), url(), lastVisited(), data()
|
||||
*/
|
||||
QString AmneziaWebHistoryItem::mimeType() const
|
||||
{
|
||||
Q_D(const AmneziaWebHistoryItem);
|
||||
if(d) return d->mimeType();
|
||||
return QString("text/html");
|
||||
}
|
||||
|
||||
/*!*
|
||||
\internal
|
||||
*/
|
||||
AmneziaWebHistoryItem::AmneziaWebHistoryItem(AmneziaWebHistoryItemPrivate *priv) : d_ptr(priv)
|
||||
{
|
||||
}
|
||||
|
||||
/*!
|
||||
\since 4.5
|
||||
Returns whether this is a valid history item.
|
||||
*/
|
||||
bool AmneziaWebHistoryItem::isValid() const
|
||||
{
|
||||
Q_D(const AmneziaWebHistoryItem);
|
||||
bool valid = (d);
|
||||
return valid;
|
||||
}
|
||||
|
||||
AmneziaWebHistory::AmneziaWebHistory(AmneziaWebView *parent) : QObject(parent)
|
||||
, d_ptr(new AmneziaWebHistoryPrivate())
|
||||
{
|
||||
Q_D(AmneziaWebHistory);
|
||||
d->q_ptr = this;
|
||||
}
|
||||
|
||||
AmneziaWebHistory::~AmneziaWebHistory()
|
||||
{
|
||||
clear();
|
||||
}
|
||||
|
||||
/*!
|
||||
Clears the history.
|
||||
|
||||
\sa count(), items()
|
||||
*/
|
||||
void AmneziaWebHistory::clear()
|
||||
{
|
||||
Q_D(AmneziaWebHistory);
|
||||
while (d->items.count()) {
|
||||
d->items.removeFirst();
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns a list of all items currently in the history.
|
||||
|
||||
\sa count(), clear()
|
||||
*/
|
||||
QList<AmneziaWebHistoryItem> AmneziaWebHistory::items() const
|
||||
{
|
||||
Q_D(const AmneziaWebHistory);
|
||||
QList<AmneziaWebHistoryItem> ret;
|
||||
|
||||
for (int i = 0; i < d->items.size(); ++i) {
|
||||
AmneziaWebHistoryItem item(d->items[i]);
|
||||
ret.append(item);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the list of items in the backwards history list.
|
||||
At most \a maxItems entries are returned.
|
||||
|
||||
\sa forwardItems()
|
||||
*/
|
||||
QList<AmneziaWebHistoryItem> AmneziaWebHistory::backItems(int maxItems) const
|
||||
{
|
||||
Q_D(const AmneziaWebHistory);
|
||||
|
||||
int count = d->currentIndex;
|
||||
if (maxItems >= 0) {
|
||||
count = qMin(count, maxItems);
|
||||
}
|
||||
|
||||
QList<AmneziaWebHistoryItem> ret;
|
||||
for (int i = (d->currentIndex - count); i < d->currentIndex; i++) {
|
||||
ret.append(d->items[i]);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the list of items in the forward history list.
|
||||
At most \a maxItems entries are returned.
|
||||
|
||||
\sa backItems()
|
||||
*/
|
||||
QList<AmneziaWebHistoryItem> AmneziaWebHistory::forwardItems(int maxItems) const
|
||||
{
|
||||
Q_D(const AmneziaWebHistory);
|
||||
|
||||
int count = d->items.count() - d->currentIndex - 1;
|
||||
if (maxItems >= 0) {
|
||||
count = qMin(count, maxItems);
|
||||
}
|
||||
|
||||
QList<AmneziaWebHistoryItem> ret;
|
||||
for (int i = (d->currentIndex + 1); i <= d->currentIndex + count; i++) {
|
||||
ret.append(d->items[i]);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
/*!
|
||||
Returns true if there is an item preceding the current item in the history;
|
||||
otherwise returns false.
|
||||
|
||||
\sa canGoForward()
|
||||
*/
|
||||
bool AmneziaWebHistory::canGoBack() const
|
||||
{
|
||||
const AmneziaWebHistoryItem current = currentItem();
|
||||
bool can = (current.isValid() && current.d_ptr->backItem() != nullptr);
|
||||
return can;
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns true if we have an item to go forward to; otherwise returns false.
|
||||
|
||||
\sa canGoBack()
|
||||
*/
|
||||
bool AmneziaWebHistory::canGoForward() const
|
||||
{
|
||||
const AmneziaWebHistoryItem current = currentItem();
|
||||
bool can = (current.isValid() && current.d_ptr->forwardItem() != nullptr);
|
||||
return can;
|
||||
}
|
||||
|
||||
/*!
|
||||
Set the current item to be the previous item in the history and goes to the
|
||||
corresponding page; i.e., goes back one history item.
|
||||
|
||||
\sa forward(), goToItem()
|
||||
*/
|
||||
void AmneziaWebHistory::back()
|
||||
{
|
||||
Q_D(AmneziaWebHistory);
|
||||
if(!canGoBack()) return;
|
||||
AmneziaWebView *view = qobject_cast<AmneziaWebView*>(parent());
|
||||
AmneziaWebHistoryItem item = backItem();
|
||||
|
||||
d->currentIndex--;
|
||||
if (view) {
|
||||
if (item.data().length() > 0) {
|
||||
view->setContent(item.data(), item.mimeType(), item.url());
|
||||
}
|
||||
else {
|
||||
|
||||
view->setUrl(item.url());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the current item to be the next item in the history and goes to the
|
||||
corresponding page; i.e., goes forward one history item.
|
||||
|
||||
\sa back(), goToItem()
|
||||
*/
|
||||
void AmneziaWebHistory::forward()
|
||||
{
|
||||
Q_D(AmneziaWebHistory);
|
||||
if(!canGoForward()) return;
|
||||
AmneziaWebView *view = qobject_cast<AmneziaWebView*>(parent());
|
||||
AmneziaWebHistoryItem item = backItem();
|
||||
|
||||
d->currentIndex++;
|
||||
if (view) {
|
||||
if (item.data().length() > 0) {
|
||||
view->setContent(item.data(), item.mimeType(), item.url());
|
||||
}
|
||||
else {
|
||||
view->setUrl(item.url());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the current item to be the specified \a item in the history and goes to the page.
|
||||
|
||||
\sa back(), forward()
|
||||
*/
|
||||
void AmneziaWebHistory::goToItem(const AmneziaWebHistoryItem &item)
|
||||
{
|
||||
Q_D(AmneziaWebHistory);
|
||||
if(!item.isValid()) return;
|
||||
AmneziaWebView *view = qobject_cast<AmneziaWebView*>(parent());
|
||||
if (!view) return; //There is no view to go.
|
||||
if (item.url().isEmpty()) return; //
|
||||
|
||||
int index = -1;
|
||||
for(int i= 0; i < d->items.count(); ++i) {
|
||||
if(d->items[i].d_ptr.data() == item.d_ptr.data()) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (index >= 0) {
|
||||
|
||||
d->currentIndex = index;
|
||||
if (item.data().length() > 0) {
|
||||
view->setContent(item.data(), item.mimeType(), item.url());
|
||||
}
|
||||
else {
|
||||
view->setUrl(item.url());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the current item in the history.
|
||||
*/
|
||||
AmneziaWebHistoryItem AmneziaWebHistory::currentItem() const
|
||||
{
|
||||
Q_D(const AmneziaWebHistory);
|
||||
|
||||
if ((d->currentIndex >= 0) && (d->currentIndex < d->items.count())) {
|
||||
return AmneziaWebHistoryItem(d->items.at(d->currentIndex));
|
||||
}
|
||||
return AmneziaWebHistoryItem(nullptr);
|
||||
}
|
||||
|
||||
void AmneziaWebHistory::append(const QUrl& url, const QByteArray& data, const QString& mimeType)
|
||||
{
|
||||
Q_D(AmneziaWebHistory);
|
||||
|
||||
const AmneziaWebHistoryItem current = currentItem();
|
||||
// Check if url is same as current, and do not add it second time.
|
||||
if (current.url() == url) return;
|
||||
|
||||
AmneziaWebHistoryItemPrivate *priv = new AmneziaWebHistoryItemPrivate();
|
||||
if(current.isValid()) {
|
||||
current.d_ptr->_forwardItem = priv;
|
||||
priv->_backItem = current.d_ptr.data();
|
||||
}
|
||||
priv->_data = data;
|
||||
priv->_url = url;
|
||||
priv->_mimeType = mimeType;
|
||||
|
||||
//Remove last items till current
|
||||
while (d->items.count() > (d->currentIndex + 1)) {
|
||||
d->items.removeLast();
|
||||
}
|
||||
|
||||
//No more then maximum
|
||||
while (d->items.count() >= d->maximumCount) {
|
||||
d->items.removeFirst();
|
||||
}
|
||||
|
||||
d->items.append(AmneziaWebHistoryItem(priv));
|
||||
d->currentIndex = (d->items.count() - 1);
|
||||
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the item before the current item in the history.
|
||||
*/
|
||||
AmneziaWebHistoryItem AmneziaWebHistory::backItem() const
|
||||
{
|
||||
AmneziaWebHistoryItem current = currentItem();
|
||||
return AmneziaWebHistoryItem(current.d_ptr->backItem());
|
||||
}
|
||||
|
||||
|
||||
/*!
|
||||
Returns the item after the current item in the history.
|
||||
*/
|
||||
AmneziaWebHistoryItem AmneziaWebHistory::forwardItem() const
|
||||
{
|
||||
AmneziaWebHistoryItem current = currentItem();
|
||||
return AmneziaWebHistoryItem(current.d_ptr->forwardItem());
|
||||
}
|
||||
|
||||
/*!
|
||||
\since 4.5
|
||||
Returns the index of the current item in history.
|
||||
*/
|
||||
int AmneziaWebHistory::currentItemIndex() const
|
||||
{
|
||||
Q_D(const AmneziaWebHistory);
|
||||
return d->currentIndex;
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the item at index \a i in the history.
|
||||
*/
|
||||
AmneziaWebHistoryItem AmneziaWebHistory::itemAt(int i) const
|
||||
{
|
||||
Q_D(const AmneziaWebHistory);
|
||||
int index = (i < 0) ? 0 : i;
|
||||
index = (index >= count()) ? (count() -1) : index;
|
||||
if (index >= 0) {
|
||||
return AmneziaWebHistoryItem(d->items.at(index));
|
||||
}
|
||||
return AmneziaWebHistoryItem(nullptr);
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the total number of items in the history.
|
||||
*/
|
||||
int AmneziaWebHistory::count() const
|
||||
{
|
||||
Q_D(const AmneziaWebHistory);
|
||||
return d->items.count();
|
||||
}
|
||||
|
||||
/*!
|
||||
\since 4.5
|
||||
Returns the maximum number of items in the history.
|
||||
|
||||
\sa setMaximumItemCount()
|
||||
*/
|
||||
int AmneziaWebHistory::maximumItemCount() const
|
||||
{
|
||||
Q_D(const AmneziaWebHistory);
|
||||
return d->maximumCount;
|
||||
}
|
||||
|
||||
/*!
|
||||
\since 4.5
|
||||
Sets the maximum number of items in the history to \a count.
|
||||
|
||||
\sa maximumItemCount()
|
||||
*/
|
||||
void AmneziaWebHistory::setMaximumItemCount(int count)
|
||||
{
|
||||
Q_D(AmneziaWebHistory);
|
||||
d->maximumCount = count;
|
||||
}
|
||||
78
client/core/webview/amneziawebhistory.h
Normal file
78
client/core/webview/amneziawebhistory.h
Normal file
@@ -0,0 +1,78 @@
|
||||
#ifndef WEBHISTORY_H
|
||||
#define WEBHISTORY_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QIcon>
|
||||
|
||||
class AmneziaWebViewPrivate;
|
||||
class AmneziaWebView;
|
||||
|
||||
class AmneziaWebHistory;
|
||||
class AmneziaWebHistoryItemPrivate;
|
||||
class AmneziaWebHistoryItem
|
||||
{
|
||||
Q_DECLARE_PRIVATE(AmneziaWebHistoryItem)
|
||||
public:
|
||||
|
||||
AmneziaWebHistoryItem(const AmneziaWebHistoryItem &other);
|
||||
AmneziaWebHistoryItem &operator=(const AmneziaWebHistoryItem &other);
|
||||
~AmneziaWebHistoryItem();
|
||||
|
||||
QUrl url() const;
|
||||
QString title() const;
|
||||
QIcon icon() const;
|
||||
|
||||
QByteArray data() const;
|
||||
QString mimeType() const;
|
||||
|
||||
bool isValid() const;
|
||||
|
||||
private:
|
||||
explicit AmneziaWebHistoryItem(AmneziaWebHistoryItemPrivate *priv);
|
||||
friend class AmneziaWebHistory;
|
||||
friend class AmneziaWebViewPrivate;
|
||||
QExplicitlySharedDataPointer<AmneziaWebHistoryItemPrivate> d_ptr;
|
||||
};
|
||||
|
||||
class AmneziaWebHistoryPrivate;
|
||||
class AmneziaWebHistory : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DECLARE_PRIVATE(AmneziaWebHistory)
|
||||
|
||||
public:
|
||||
virtual ~AmneziaWebHistory();
|
||||
|
||||
void append(const QUrl& url, const QByteArray& data = QByteArray(), const QString& mimeType = QString("text/html"));
|
||||
void clear();
|
||||
|
||||
QList<AmneziaWebHistoryItem> items() const;
|
||||
QList<AmneziaWebHistoryItem> backItems(int maxItems) const;
|
||||
QList<AmneziaWebHistoryItem> forwardItems(int maxItems) const;
|
||||
|
||||
bool canGoBack() const;
|
||||
bool canGoForward() const;
|
||||
|
||||
void back();
|
||||
void forward();
|
||||
void goToItem(const AmneziaWebHistoryItem &item);
|
||||
|
||||
AmneziaWebHistoryItem backItem() const;
|
||||
AmneziaWebHistoryItem currentItem() const;
|
||||
AmneziaWebHistoryItem forwardItem() const;
|
||||
AmneziaWebHistoryItem itemAt(int i) const;
|
||||
int currentItemIndex() const;
|
||||
|
||||
int count() const;
|
||||
int maximumItemCount() const;
|
||||
void setMaximumItemCount(int count);
|
||||
|
||||
private:
|
||||
|
||||
friend class AmneziaWebViewPrivate;
|
||||
explicit AmneziaWebHistory(AmneziaWebView *parent);
|
||||
Q_DISABLE_COPY(AmneziaWebHistory)
|
||||
QScopedPointer<AmneziaWebHistoryPrivate> d_ptr;
|
||||
};
|
||||
|
||||
#endif
|
||||
72
client/core/webview/amneziawebhistory_p.h
Normal file
72
client/core/webview/amneziawebhistory_p.h
Normal file
@@ -0,0 +1,72 @@
|
||||
#ifndef WEBHISTORY_P_H
|
||||
#define WEBHISTORY_P_H
|
||||
|
||||
#include <QUrl>
|
||||
|
||||
#include "amneziawebhistory.h"
|
||||
|
||||
class AmneziaWebHistoryItemPrivate;
|
||||
class AmneziaWebHistoryItem;
|
||||
|
||||
class AmneziaWebHistoryPrivate
|
||||
{
|
||||
Q_DECLARE_PUBLIC(AmneziaWebHistory)
|
||||
public:
|
||||
|
||||
static AmneziaWebHistoryPrivate *get(AmneziaWebHistory *q)
|
||||
{
|
||||
if (!q) { return nullptr; }
|
||||
return q->d_func();
|
||||
}
|
||||
|
||||
AmneziaWebHistoryPrivate(): currentIndex(-1), maximumCount(10), q_ptr(nullptr) { }
|
||||
~AmneziaWebHistoryPrivate() = default;
|
||||
|
||||
|
||||
private:
|
||||
friend class AmneziaWebHistoryItemPrivate;
|
||||
int currentIndex;
|
||||
int maximumCount;
|
||||
QList<AmneziaWebHistoryItem> items;
|
||||
AmneziaWebHistory *q_ptr;
|
||||
};
|
||||
|
||||
class AmneziaWebHistoryItemPrivate : public QSharedData
|
||||
{
|
||||
public:
|
||||
|
||||
static QExplicitlySharedDataPointer<AmneziaWebHistoryItemPrivate> get(AmneziaWebHistoryItem *q)
|
||||
{
|
||||
return q->d_ptr;
|
||||
}
|
||||
|
||||
~AmneziaWebHistoryItemPrivate()
|
||||
{
|
||||
}
|
||||
|
||||
QUrl url() const { return _url; }
|
||||
QString title() const { return _title; }
|
||||
QIcon icon() const {return _icon;}
|
||||
QByteArray data() const {return _data;}
|
||||
QString mimeType() const {return _mimeType;}
|
||||
|
||||
// Every item knows its back and forward items
|
||||
AmneziaWebHistoryItemPrivate *backItem() {return _backItem; }
|
||||
AmneziaWebHistoryItemPrivate *forwardItem() {return _forwardItem; }
|
||||
|
||||
private:
|
||||
friend class AmneziaWebHistory;
|
||||
AmneziaWebHistoryItemPrivate() = default;
|
||||
|
||||
AmneziaWebHistoryItemPrivate *_backItem = nullptr;
|
||||
AmneziaWebHistoryItemPrivate *_forwardItem = nullptr;
|
||||
QIcon _icon;
|
||||
QString _title;
|
||||
QUrl _url;
|
||||
QString _html;
|
||||
QByteArray _data;
|
||||
QString _mimeType;
|
||||
};
|
||||
|
||||
|
||||
#endif
|
||||
751
client/core/webview/amneziawebview.cpp
Normal file
751
client/core/webview/amneziawebview.cpp
Normal file
@@ -0,0 +1,751 @@
|
||||
#include <QDebug>
|
||||
#include <QEvent>
|
||||
#include <QFile>
|
||||
#include <QThread>
|
||||
#include <QMetaObject>
|
||||
#include <QQmlContext>
|
||||
#include <QQmlEngine>
|
||||
#include <QKeyEvent>
|
||||
#include <QMouseEvent>
|
||||
#include <QPen>
|
||||
#include <QList>
|
||||
#include <QQuickWindow>
|
||||
#include <QTimer>
|
||||
|
||||
#include <QPainter>
|
||||
|
||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
||||
#include <QGuiApplication>
|
||||
#define qApp qGuiApp
|
||||
#else
|
||||
#include <QApplication>
|
||||
#endif
|
||||
|
||||
#include "amneziawebview.h"
|
||||
#include "amneziawebview_p.h"
|
||||
|
||||
QUrl defaultBaseUrl()
|
||||
{
|
||||
#if defined(Q_OS_MACOS) || defined(Q_OS_WINDOWS)
|
||||
return QUrl(QLatin1String("local:///"));
|
||||
#else
|
||||
return QUrl(QLatin1String("file:///"));
|
||||
#endif
|
||||
}
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
AmneziaWebView::AmneziaWebView(QQuickItem *parent) : QQuickPaintedItem(parent),
|
||||
d_ptr(AmneziaWebViewPrivate::create(this))
|
||||
{
|
||||
Q_D(AmneziaWebView);
|
||||
d->q_ptr = this;
|
||||
d->init();
|
||||
init();
|
||||
}
|
||||
|
||||
AmneziaWebView::~AmneziaWebView()
|
||||
{
|
||||
disconnect(this);
|
||||
}
|
||||
|
||||
void AmneziaWebView::init()
|
||||
{
|
||||
Q_D(AmneziaWebView);
|
||||
setAcceptedMouseButtons(Qt::LeftButton);
|
||||
setFlag(QQuickItem::ItemHasContents, true);
|
||||
setOpaquePainting(true);
|
||||
|
||||
setClip(true);
|
||||
|
||||
connect(this, SIGNAL(windowChanged(QQuickWindow*)), this, SLOT(windowWasChanged(QQuickWindow*)));
|
||||
connect(this, SIGNAL(parentChanged(QQuickItem*)), this, SLOT(parentWasChanged()));
|
||||
connect(this, SIGNAL(fillColorChanged()), this,SLOT(fillColorWasChanged()));
|
||||
|
||||
connect(d, SIGNAL(titleChanged(QString)), this, SIGNAL(titleChanged(QString)));
|
||||
connect(d, SIGNAL(loadStarted()), this, SLOT(doLoadStarted()));
|
||||
connect(d, SIGNAL(loadFinished(bool)), this, SLOT(doLoadFinished(bool)));
|
||||
}
|
||||
|
||||
void AmneziaWebView::componentComplete()
|
||||
{
|
||||
Q_D(AmneziaWebView);
|
||||
QQuickItem::componentComplete();
|
||||
|
||||
// Update geometry after component is complete
|
||||
if (window()) {
|
||||
QMetaObject::invokeMethod(this, "updateGeometry", Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
switch (d->pending) {
|
||||
case AmneziaWebViewPrivate::PendingUrl:
|
||||
// Make WebView visible before loading
|
||||
if (isVisible() && !d->visible) {
|
||||
d->show();
|
||||
}
|
||||
setUrl(d->pendingUrl);
|
||||
break;
|
||||
case AmneziaWebViewPrivate::PendingHtml:
|
||||
if (isVisible() && !d->visible) {
|
||||
d->show();
|
||||
}
|
||||
setHtml(d->pendingString, d->pendingUrl);
|
||||
break;
|
||||
case AmneziaWebViewPrivate::PendingContent:
|
||||
if (isVisible() && !d->visible) {
|
||||
d->show();
|
||||
}
|
||||
setContent(d->pendingData, d->pendingString, d->pendingUrl);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
AmneziaWebView::Status AmneziaWebView::status() const
|
||||
{
|
||||
Q_D(const AmneziaWebView);
|
||||
return d->status;
|
||||
}
|
||||
|
||||
/*!
|
||||
\qmlproperty real WebView::progress
|
||||
This property holds the progress of loading the current URL, from 0 to 1.
|
||||
|
||||
If you just want to know when progress gets to 1, use
|
||||
WebView::onLoadFinished() or WebView::onLoadFailed() instead.
|
||||
*/
|
||||
qreal AmneziaWebView::progress() const
|
||||
{
|
||||
Q_D(const AmneziaWebView);
|
||||
return d->progress;
|
||||
}
|
||||
|
||||
void AmneziaWebView::doLoadStarted()
|
||||
{
|
||||
Q_D(AmneziaWebView);
|
||||
|
||||
if (!d->url.isEmpty()) {
|
||||
d->status = Loading;
|
||||
emit statusChanged(d->status);
|
||||
}
|
||||
emit loadStarted();
|
||||
}
|
||||
|
||||
void AmneziaWebView::doLoadProgress(int p)
|
||||
{
|
||||
Q_D(AmneziaWebView);
|
||||
|
||||
if (d->progress == p / 100.0)
|
||||
return;
|
||||
d->progress = p / 100.0;
|
||||
emit progressChanged();
|
||||
}
|
||||
|
||||
|
||||
void AmneziaWebView::doLoadFinished(bool ok)
|
||||
{
|
||||
Q_D(AmneziaWebView);
|
||||
|
||||
if (ok) {
|
||||
d->status = d->url.isEmpty() ? Null : Ready;
|
||||
emit loadFinished();
|
||||
} else {
|
||||
d->status = Error;
|
||||
emit loadFailed();
|
||||
}
|
||||
emit statusChanged(d->status);
|
||||
}
|
||||
|
||||
/*!
|
||||
\qmlproperty url AmneziaWebView::url
|
||||
This property holds the URL to the page displayed in this item. It can be set,
|
||||
but also can change spontaneously (eg. because of network redirection).
|
||||
|
||||
If the url is empty, the page is blank.
|
||||
|
||||
The url is always absolute (QML will resolve relative URL strings in the context
|
||||
of the containing QML document).
|
||||
*/
|
||||
QUrl AmneziaWebView::url() const
|
||||
{
|
||||
Q_D(const AmneziaWebView);
|
||||
return d->url;
|
||||
}
|
||||
|
||||
void AmneziaWebView::setUrl(const QUrl& url)
|
||||
{
|
||||
Q_D(AmneziaWebView);
|
||||
|
||||
QString urlString = url.toString();
|
||||
while ( urlString.endsWith('#')) urlString.chop(1);
|
||||
QUrl newUrl(urlString);
|
||||
|
||||
if (newUrl == QUrl(QLatin1String("about:blank")) ) {
|
||||
newUrl = QUrl("");
|
||||
}
|
||||
|
||||
if ((url == d->url) || (newUrl == d->url))
|
||||
return;
|
||||
|
||||
if (isComponentComplete()) {
|
||||
// Make WebView visible before loading
|
||||
if (isVisible() && !d->visible) {
|
||||
d->show();
|
||||
}
|
||||
d->load(url);
|
||||
|
||||
} else {
|
||||
|
||||
d->pending = d->PendingUrl;
|
||||
d->pendingUrl = url;
|
||||
}
|
||||
}
|
||||
|
||||
qreal AmneziaWebView::preferredWidth() const
|
||||
{
|
||||
Q_D(const AmneziaWebView);
|
||||
return d->preferredwidth;
|
||||
}
|
||||
|
||||
void AmneziaWebView::setPreferredWidth(qreal width)
|
||||
{
|
||||
Q_D(AmneziaWebView);
|
||||
|
||||
if (d->preferredwidth == width)
|
||||
return;
|
||||
|
||||
d->preferredwidth = width;
|
||||
updateContentsSize();
|
||||
setImplicitWidth(width);
|
||||
emit preferredWidthChanged();
|
||||
}
|
||||
|
||||
qreal AmneziaWebView::preferredHeight() const
|
||||
{
|
||||
Q_D(const AmneziaWebView);
|
||||
return d->preferredheight;
|
||||
}
|
||||
|
||||
void AmneziaWebView::setPreferredHeight(qreal height)
|
||||
{
|
||||
Q_D(AmneziaWebView);
|
||||
|
||||
if (d->preferredheight == height)
|
||||
return;
|
||||
|
||||
d->preferredheight = height;
|
||||
updateContentsSize();
|
||||
setImplicitHeight(height);
|
||||
emit preferredHeightChanged();
|
||||
}
|
||||
|
||||
|
||||
/*!
|
||||
\qmlmethod bool AmneziaWebView::evaluateJavaScript(string scriptSource)
|
||||
|
||||
Evaluates the \a scriptSource JavaScript inside the context of the
|
||||
main web frame, and returns the result of the last executed statement.
|
||||
|
||||
Note that this JavaScript does \e not have any access to QML objects
|
||||
except as made available as windowObjects.
|
||||
*/
|
||||
void AmneziaWebView::evaluateJavaScript(const QString& scriptSource)
|
||||
{
|
||||
Q_D(AmneziaWebView);
|
||||
if (qApp->thread() == QThread::currentThread()) {
|
||||
d->evaluateJavaScript(scriptSource);
|
||||
}
|
||||
else {
|
||||
QMetaObject::invokeMethod(d, "evaluateJavaScript", Qt::BlockingQueuedConnection,
|
||||
Q_ARG(const QString, scriptSource));
|
||||
}
|
||||
}
|
||||
|
||||
void AmneziaWebView::windowWasChanged(QQuickWindow* window)
|
||||
{
|
||||
Q_D(AmneziaWebView);
|
||||
d->setWindowParent(window);
|
||||
}
|
||||
|
||||
void AmneziaWebView::updateGeometry()
|
||||
{
|
||||
Q_D(AmneziaWebView);
|
||||
QRectF geometry = QRectF(QPointF(x(), y()), QSizeF(width(), height()));
|
||||
if (!geometry.isEmpty() && window()) {
|
||||
QRectF sceneGeometry = mapRectToScene(geometry);
|
||||
QRect rect = sceneGeometry.toRect();
|
||||
qDebug() << "AmneziaWebView::updateGeometry() - local:" << geometry
|
||||
<< "scene:" << sceneGeometry << "rect:" << rect
|
||||
<< "width:" << width() << "height:" << height();
|
||||
d->setGeometry(rect);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void AmneziaWebView::parentWasChanged()
|
||||
{
|
||||
if (parentItem()) {
|
||||
updateGeometry();
|
||||
}
|
||||
}
|
||||
|
||||
QList<QQuickItem *> recurseChildren(QQuickItem * parentItem)
|
||||
{
|
||||
QList<QQuickItem *>childs = parentItem->childItems();
|
||||
QList<QQuickItem *> items;
|
||||
int count = childs.count();
|
||||
for(int i = count - 1; i >= 0; --i) {
|
||||
QQuickItem *next = childs.at(i);
|
||||
items.append(recurseChildren(next));
|
||||
}
|
||||
items.append(childs);
|
||||
return items;
|
||||
}
|
||||
|
||||
|
||||
void AmneziaWebView::paint(QPainter *painter)
|
||||
{
|
||||
Q_D(AmneziaWebView);
|
||||
if (!painter || !window() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
QRectF contentRect = contentsBoundingRect();
|
||||
if ((contentRect.height() <= 0) || (contentRect.width() <= 0)) return;
|
||||
|
||||
painter->setOpacity(1.0);
|
||||
QMutexLocker lock(&d->renderMutex);
|
||||
|
||||
QColor color = d->backgroundColor;
|
||||
painter->fillRect(contentRect, color);
|
||||
|
||||
}
|
||||
|
||||
void AmneziaWebView::afterRendering()
|
||||
{
|
||||
Q_D(AmneziaWebView);
|
||||
|
||||
Qt::ApplicationState state = qApp->applicationState();
|
||||
if ( state != Qt::ApplicationActive) {
|
||||
if (d->visible && isVisible()) {
|
||||
QMetaObject::invokeMethod(d, "requestHide", Qt::QueuedConnection);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window()) return;
|
||||
|
||||
if (isVisible()) {
|
||||
QMetaObject::invokeMethod(this, "updateGeometry", Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
QMetaObject::invokeMethod(d, "requestShow", Qt::QueuedConnection);
|
||||
|
||||
}
|
||||
|
||||
void AmneziaWebView::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
|
||||
{
|
||||
Q_D(AmneziaWebView);
|
||||
|
||||
QQuickPaintedItem::geometryChange(newGeometry, oldGeometry);
|
||||
|
||||
// Update WebView geometry when QML item size changes
|
||||
if (window() && !newGeometry.isEmpty() && newGeometry != oldGeometry) {
|
||||
QMetaObject::invokeMethod(this, "updateGeometry", Qt::QueuedConnection);
|
||||
}
|
||||
}
|
||||
|
||||
void AmneziaWebView::itemChange(ItemChange change, const ItemChangeData & value)
|
||||
{
|
||||
Q_D(AmneziaWebView);
|
||||
switch (change) {
|
||||
case ItemSceneChange: {
|
||||
QQuickWindow *sc = value.window;
|
||||
if (sc) {
|
||||
connect(sc, SIGNAL(afterRendering()), this, SLOT(afterRendering()), Qt::QueuedConnection);
|
||||
}
|
||||
else {
|
||||
disconnect(this, SLOT(afterRendering()));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ItemVisibleHasChanged: {
|
||||
|
||||
if (!window()) break;
|
||||
|
||||
if (value.boolValue) {
|
||||
// Component became visible - show WebView
|
||||
if (!d->visible) {
|
||||
d->show();
|
||||
}
|
||||
} else {
|
||||
QMetaObject::invokeMethod(d, "requestHide", Qt::QueuedConnection);
|
||||
}
|
||||
if (value.boolValue && !d->overlapped) {
|
||||
QMetaObject::invokeMethod(d, "requestShow", Qt::QueuedConnection);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
QQuickPaintedItem::itemChange(change, value);
|
||||
}
|
||||
|
||||
/*!
|
||||
\qmlproperty list<object> WebView::javaScriptWindowObjects
|
||||
|
||||
A list of QML objects to expose to the web page.
|
||||
|
||||
Each object will be added as a property of the web frame's window object. The
|
||||
property name is controlled by the value of \c WebView.windowObjectName
|
||||
attached property.
|
||||
|
||||
Exposing QML objects to a web page allows JavaScript executing in the web
|
||||
page itself to communicate with QML, by reading and writing properties and
|
||||
by calling methods of the exposed QML objects.
|
||||
|
||||
This example shows how to call into a QML method using a window object.
|
||||
|
||||
\qml
|
||||
WebView {
|
||||
javaScriptWindowObjects: QtObject {
|
||||
WebView.windowObjectName: "qml"
|
||||
|
||||
function qmlCall() {
|
||||
console.log("This call is in QML!");
|
||||
}
|
||||
}
|
||||
|
||||
html: "<script>window.qml.qmlCall();</script>"
|
||||
}
|
||||
\endqml
|
||||
|
||||
The output of the example will be:
|
||||
\code
|
||||
This call is in QML!
|
||||
\endcode
|
||||
|
||||
If Javascript is not enabled for the page, then this property does nothing.
|
||||
*/
|
||||
QQmlListProperty<QObject> AmneziaWebView::javaScriptWindowObjects()
|
||||
{
|
||||
Q_D(AmneziaWebView);
|
||||
return QQmlListProperty<QObject>(this, d, &AmneziaWebViewPrivate::windowObjectsAppend,
|
||||
&AmneziaWebViewPrivate::windowObjectsCount,
|
||||
&AmneziaWebViewPrivate::windowObjectsAt,
|
||||
&AmneziaWebViewPrivate::windowObjectsClear );
|
||||
}
|
||||
|
||||
AmneziaWebViewSettings* AmneziaWebView::settingsObject() const
|
||||
{
|
||||
Q_D(const AmneziaWebView);
|
||||
return d->m_settings.data();
|
||||
}
|
||||
|
||||
|
||||
AmneziaWebViewAttached* AmneziaWebView::qmlAttachedProperties(QObject* o)
|
||||
{
|
||||
return new AmneziaWebViewAttached(o);
|
||||
}
|
||||
|
||||
void AmneziaWebViewPrivate::updateWindowObjects()
|
||||
{
|
||||
|
||||
if (!q_ptr->isComponentCompletePublic())
|
||||
return;
|
||||
|
||||
for (int i = 0; i < windowObjects.count(); ++i) {
|
||||
QObject* object = windowObjects.at(i);
|
||||
AmneziaWebViewAttached* attached = static_cast<AmneziaWebViewAttached *>(qmlAttachedPropertiesObject<AmneziaWebView>(object));
|
||||
if (attached && !attached->windowObjectName().isEmpty())
|
||||
addToJavaScriptWindowObject(attached->windowObjectName(), object);
|
||||
}
|
||||
}
|
||||
|
||||
int AmneziaWebView::pressGrabTime() const
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
void AmneziaWebView::setPressGrabTime(int millis)
|
||||
{
|
||||
Q_UNUSED(millis)
|
||||
|
||||
emit pressGrabTimeChanged();
|
||||
}
|
||||
|
||||
#ifndef QT_NO_ACTION
|
||||
/*!
|
||||
\qmlproperty action WebView::back
|
||||
This property holds the action for causing the previous URL in the history to be displayed.
|
||||
*/
|
||||
QAction* AmneziaWebView::backAction() const
|
||||
{
|
||||
return action(AmneziaWebView::Back);
|
||||
}
|
||||
|
||||
/*!
|
||||
\qmlproperty action WebView::forward
|
||||
This property holds the action for causing the next URL in the history to be displayed.
|
||||
*/
|
||||
QAction* AmneziaWebView::forwardAction() const
|
||||
{
|
||||
return action(AmneziaWebView::Forward);
|
||||
}
|
||||
|
||||
/*!
|
||||
\qmlproperty action WebView::reload
|
||||
This property holds the action for reloading with the current URL
|
||||
*/
|
||||
QAction* AmneziaWebView::reloadAction() const
|
||||
{
|
||||
return action(AmneziaWebView::Reload);
|
||||
}
|
||||
|
||||
/*!
|
||||
\qmlproperty action WebView::stop
|
||||
This property holds the action for stopping loading with the current URL
|
||||
*/
|
||||
QAction* AmneziaWebView::stopAction() const
|
||||
{
|
||||
return action(AmneziaWebView::Stop);
|
||||
}
|
||||
#endif // QT_NO_ACTION
|
||||
|
||||
/*!
|
||||
\qmlproperty string WebView::title
|
||||
This property holds the title of the web page currently viewed
|
||||
|
||||
By default, this property contains an empty string.
|
||||
*/
|
||||
QString AmneziaWebView::title() const
|
||||
{
|
||||
Q_D(const AmneziaWebView);
|
||||
return d->title;
|
||||
}
|
||||
|
||||
/*!
|
||||
\qmlproperty pixmap WebView::icon
|
||||
This property holds the icon associated with the web page currently viewed
|
||||
*/
|
||||
QPixmap AmneziaWebView::icon() const
|
||||
{
|
||||
Q_D(const AmneziaWebView);
|
||||
return d->icon().pixmap(QSize(256, 256));
|
||||
}
|
||||
|
||||
/*!
|
||||
\qmlproperty string WebView::statusText
|
||||
|
||||
This property is the current status suggested by the current web page. In a web browser,
|
||||
such status is often shown in some kind of status bar.
|
||||
*/
|
||||
void AmneziaWebView::setStatusText(const QString& text)
|
||||
{
|
||||
Q_D(AmneziaWebView);
|
||||
d->statusText = text;
|
||||
emit statusTextChanged();
|
||||
}
|
||||
|
||||
void AmneziaWebView::windowObjectCleared()
|
||||
{
|
||||
Q_D(AmneziaWebView);
|
||||
d->updateWindowObjects();
|
||||
}
|
||||
|
||||
QString AmneziaWebView::statusText() const
|
||||
{
|
||||
Q_D(const AmneziaWebView);
|
||||
return d->statusText;
|
||||
}
|
||||
|
||||
|
||||
void AmneziaWebView::load(const QNetworkRequest& request, QNetworkAccessManager::Operation operation, const QByteArray& body)
|
||||
{
|
||||
Q_D(AmneziaWebView);
|
||||
d->load(request, operation, body);
|
||||
}
|
||||
|
||||
QString AmneziaWebView::html() const
|
||||
{
|
||||
Q_D(const AmneziaWebView);
|
||||
return d->toHtml();
|
||||
}
|
||||
|
||||
void AmneziaWebView::setHtml(const QString& html, const QUrl& baseUrl)
|
||||
{
|
||||
Q_D(AmneziaWebView);
|
||||
auto originUrl = baseUrl.isValid() ? baseUrl : defaultBaseUrl();
|
||||
|
||||
updateContentsSize();
|
||||
if (isComponentComplete()) {
|
||||
d->setHtml(html, originUrl);
|
||||
}
|
||||
else {
|
||||
d->pending = d->PendingHtml;
|
||||
d->pendingUrl = originUrl;
|
||||
d->pendingString = html;
|
||||
}
|
||||
emit htmlChanged();
|
||||
}
|
||||
|
||||
void AmneziaWebView::setContent(const QByteArray& data, const QString& mimeType, const QUrl& baseUrl)
|
||||
{
|
||||
Q_D(AmneziaWebView);
|
||||
|
||||
updateContentsSize();
|
||||
auto originUrl = baseUrl.isValid() ? baseUrl : defaultBaseUrl();
|
||||
|
||||
if (isComponentComplete())
|
||||
d->setContent(data, mimeType, qmlContext(this)->resolvedUrl(baseUrl));
|
||||
else {
|
||||
d->pending = d->PendingContent;
|
||||
d->pendingUrl = originUrl;
|
||||
d->pendingString = mimeType;
|
||||
d->pendingData = data;
|
||||
}
|
||||
}
|
||||
|
||||
AmneziaWebHistory* AmneziaWebView::history() const
|
||||
{
|
||||
Q_D(const AmneziaWebView);
|
||||
return d->history();
|
||||
}
|
||||
|
||||
#ifndef QT_NO_ACTION
|
||||
QAction* AmneziaWebView::action(AmneziaWebView::WebAction action) const
|
||||
{
|
||||
Q_D(const AmneziaWebView);
|
||||
return d->action(action);
|
||||
}
|
||||
#endif
|
||||
|
||||
/*!
|
||||
\qmlproperty component WebView::newWindowComponent
|
||||
|
||||
This property holds the component to use for new windows.
|
||||
The component must have a WebView somewhere in its structure.
|
||||
|
||||
When the web engine requests a new window, it will be an instance of
|
||||
this component.
|
||||
|
||||
The parent of the new window is set by newWindowParent. It must be set.
|
||||
*/
|
||||
QQmlComponent* AmneziaWebView::newWindowComponent() const
|
||||
{
|
||||
Q_D(const AmneziaWebView);
|
||||
return d->newWindowComponent;
|
||||
}
|
||||
|
||||
void AmneziaWebView::setNewWindowComponent(QQmlComponent* newWindow)
|
||||
{
|
||||
Q_D(AmneziaWebView);
|
||||
if (newWindow == d->newWindowComponent)
|
||||
return;
|
||||
d->newWindowComponent = newWindow;
|
||||
emit newWindowComponentChanged();
|
||||
}
|
||||
|
||||
|
||||
/*!
|
||||
\qmlproperty item WebView::newWindowParent
|
||||
|
||||
The parent item for new windows.
|
||||
|
||||
\sa newWindowComponent
|
||||
*/
|
||||
QQuickItem* AmneziaWebView::newWindowParent() const
|
||||
{
|
||||
Q_D(const AmneziaWebView);
|
||||
return d->newWindowParent;
|
||||
}
|
||||
|
||||
void AmneziaWebView::setNewWindowParent(QQuickItem *parent)
|
||||
{
|
||||
Q_D(AmneziaWebView);
|
||||
if (parent == d->newWindowParent)
|
||||
return;
|
||||
if (d->newWindowParent && parent) {
|
||||
QList<QQuickItem *> children = d->newWindowParent->childItems();
|
||||
for (int i = 0; i < children.count(); ++i)
|
||||
children.at(i)->setParentItem(parent);
|
||||
}
|
||||
d->newWindowParent = parent;
|
||||
emit newWindowParentChanged();
|
||||
}
|
||||
|
||||
QSize AmneziaWebView::contentsSize() const
|
||||
{
|
||||
Q_D(const AmneziaWebView);
|
||||
return d->contentsSize() * contentsScale();
|
||||
}
|
||||
|
||||
qreal AmneziaWebView::contentsScale() const
|
||||
{
|
||||
Q_D(const AmneziaWebView);
|
||||
return d->scale();
|
||||
}
|
||||
|
||||
void AmneziaWebView::setContentsScale(qreal scale)
|
||||
{
|
||||
Q_D(AmneziaWebView);
|
||||
if (scale == d->scale())
|
||||
return;
|
||||
d->setScale(scale);
|
||||
|
||||
//updateGeometry();
|
||||
emit contentsScaleChanged();
|
||||
}
|
||||
|
||||
void AmneziaWebView::setDefaultFontSize(int size)
|
||||
{
|
||||
Q_D(AmneziaWebView);
|
||||
d->setDefaultFontSize(size);
|
||||
}
|
||||
|
||||
void AmneziaWebView::setStandardFontFamily(const QString &family)
|
||||
{
|
||||
Q_D(AmneziaWebView);
|
||||
d->setStandardFontFamily(family);
|
||||
}
|
||||
|
||||
void AmneziaWebView::setTextZoom(int percent)
|
||||
{
|
||||
Q_D(AmneziaWebView);
|
||||
d->setTextZoom(percent);
|
||||
}
|
||||
|
||||
|
||||
#ifdef Q_REVISION
|
||||
/*!
|
||||
\qmlproperty color WebView::backgroundColor
|
||||
\since QtWebKit 1.1
|
||||
This property holds the background color of the view.
|
||||
*/
|
||||
|
||||
QColor AmneziaWebView::backgroundColor() const
|
||||
{
|
||||
Q_D(const AmneziaWebView);
|
||||
return d->backgroundColor;
|
||||
}
|
||||
|
||||
void AmneziaWebView::setBackgroundColor(const QColor& color)
|
||||
{
|
||||
setFillColor(color);
|
||||
}
|
||||
|
||||
void AmneziaWebView::fillColorWasChanged()
|
||||
{
|
||||
Q_D(AmneziaWebView);
|
||||
QColor color = fillColor();
|
||||
d->setBackgroundColor(color);
|
||||
emit backgroundColorChanged();
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
QT_END_NAMESPACE
|
||||
|
||||
309
client/core/webview/amneziawebview.h
Normal file
309
client/core/webview/amneziawebview.h
Normal file
@@ -0,0 +1,309 @@
|
||||
#ifndef DECLARATIVEWEBVIEW_H
|
||||
#define DECLARATIVEWEBVIEW_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QAction>
|
||||
#include <QBasicTimer>
|
||||
#include <QUrl>
|
||||
#include <QtNetwork/QNetworkAccessManager>
|
||||
|
||||
#include <QtQml>
|
||||
#include <QQuickPaintedItem>
|
||||
|
||||
#include "websettings.h"
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
class AmneziaWebViewSettings;
|
||||
class AmneziaWebViewPrivate;
|
||||
class AmneziaWebViewAttached;
|
||||
class AmneziaWebHistory;
|
||||
|
||||
class AmneziaWebView : public QQuickPaintedItem
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Q_ENUMS(Status SelectionMode)
|
||||
|
||||
Q_PROPERTY(QString title READ title NOTIFY titleChanged)
|
||||
Q_PROPERTY(QPixmap icon READ icon NOTIFY iconChanged)
|
||||
Q_PROPERTY(QString statusText READ statusText NOTIFY statusTextChanged)
|
||||
Q_PROPERTY(QString html READ html WRITE setHtml NOTIFY htmlChanged)
|
||||
Q_PROPERTY(int pressGrabTime READ pressGrabTime WRITE setPressGrabTime NOTIFY pressGrabTimeChanged)
|
||||
Q_PROPERTY(qreal preferredWidth READ preferredWidth WRITE setPreferredWidth NOTIFY preferredWidthChanged)
|
||||
Q_PROPERTY(qreal preferredHeight READ preferredHeight WRITE setPreferredHeight NOTIFY preferredHeightChanged)
|
||||
Q_PROPERTY(QUrl url READ url WRITE setUrl NOTIFY urlChanged)
|
||||
Q_PROPERTY(qreal progress READ progress NOTIFY progressChanged)
|
||||
Q_PROPERTY(Status status READ status NOTIFY statusChanged)
|
||||
|
||||
|
||||
#ifndef QT_NO_ACTION
|
||||
Q_PROPERTY(QAction* reload READ reloadAction CONSTANT)
|
||||
Q_PROPERTY(QAction* back READ backAction CONSTANT)
|
||||
Q_PROPERTY(QAction* forward READ forwardAction CONSTANT)
|
||||
Q_PROPERTY(QAction* stop READ stopAction CONSTANT)
|
||||
#endif
|
||||
|
||||
Q_PROPERTY(AmneziaWebViewSettings* settings READ settingsObject CONSTANT)
|
||||
Q_PROPERTY(QQmlListProperty<QObject> javaScriptWindowObjects READ javaScriptWindowObjects CONSTANT)
|
||||
Q_PROPERTY(QQmlComponent* newWindowComponent READ newWindowComponent WRITE setNewWindowComponent NOTIFY newWindowComponentChanged)
|
||||
Q_PROPERTY(QQuickItem* newWindowParent READ newWindowParent WRITE setNewWindowParent NOTIFY newWindowParentChanged)
|
||||
Q_PROPERTY(QSize contentsSize READ contentsSize NOTIFY contentsSizeChanged)
|
||||
Q_PROPERTY(qreal contentsScale READ contentsScale WRITE setContentsScale NOTIFY contentsScaleChanged)
|
||||
Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor NOTIFY backgroundColorChanged)
|
||||
|
||||
public:
|
||||
|
||||
enum WebAction {
|
||||
NoWebAction = - 1,
|
||||
|
||||
OpenLink,
|
||||
|
||||
OpenLinkInNewWindow,
|
||||
OpenFrameInNewWindow,
|
||||
|
||||
DownloadLinkToDisk,
|
||||
CopyLinkToClipboard,
|
||||
|
||||
OpenImageInNewWindow,
|
||||
DownloadImageToDisk,
|
||||
CopyImageToClipboard,
|
||||
|
||||
Back,
|
||||
Forward,
|
||||
Stop,
|
||||
Reload,
|
||||
|
||||
Cut,
|
||||
Copy,
|
||||
Paste,
|
||||
|
||||
Undo,
|
||||
Redo,
|
||||
MoveToNextChar,
|
||||
MoveToPreviousChar,
|
||||
MoveToNextWord,
|
||||
MoveToPreviousWord,
|
||||
MoveToNextLine,
|
||||
MoveToPreviousLine,
|
||||
MoveToStartOfLine,
|
||||
MoveToEndOfLine,
|
||||
MoveToStartOfBlock,
|
||||
MoveToEndOfBlock,
|
||||
MoveToStartOfDocument,
|
||||
MoveToEndOfDocument,
|
||||
SelectNextChar,
|
||||
SelectPreviousChar,
|
||||
SelectNextWord,
|
||||
SelectPreviousWord,
|
||||
SelectNextLine,
|
||||
SelectPreviousLine,
|
||||
SelectStartOfLine,
|
||||
SelectEndOfLine,
|
||||
SelectStartOfBlock,
|
||||
SelectEndOfBlock,
|
||||
SelectStartOfDocument,
|
||||
SelectEndOfDocument,
|
||||
DeleteStartOfWord,
|
||||
DeleteEndOfWord,
|
||||
|
||||
SetTextDirectionDefault,
|
||||
SetTextDirectionLeftToRight,
|
||||
SetTextDirectionRightToLeft,
|
||||
|
||||
ToggleBold,
|
||||
ToggleItalic,
|
||||
ToggleUnderline,
|
||||
|
||||
InspectElement,
|
||||
|
||||
InsertParagraphSeparator,
|
||||
InsertLineSeparator,
|
||||
|
||||
SelectAll,
|
||||
ReloadAndBypassCache,
|
||||
|
||||
PasteAndMatchStyle,
|
||||
RemoveFormat,
|
||||
|
||||
ToggleStrikethrough,
|
||||
ToggleSubscript,
|
||||
ToggleSuperscript,
|
||||
InsertUnorderedList,
|
||||
InsertOrderedList,
|
||||
Indent,
|
||||
Outdent,
|
||||
|
||||
AlignCenter,
|
||||
AlignJustified,
|
||||
AlignLeft,
|
||||
AlignRight,
|
||||
|
||||
StopScheduledPageRefresh,
|
||||
|
||||
CopyImageUrlToClipboard,
|
||||
|
||||
WebActionCount
|
||||
};
|
||||
|
||||
|
||||
explicit AmneziaWebView(QQuickItem *parent = nullptr);
|
||||
virtual ~AmneziaWebView();
|
||||
|
||||
QUrl url() const;
|
||||
void setUrl(const QUrl &);
|
||||
|
||||
QString title() const;
|
||||
|
||||
QPixmap icon() const;
|
||||
|
||||
int pressGrabTime() const;
|
||||
void setPressGrabTime(int);
|
||||
|
||||
qreal preferredWidth() const;
|
||||
void setPreferredWidth(qreal);
|
||||
qreal preferredHeight() const;
|
||||
void setPreferredHeight(qreal);
|
||||
|
||||
enum Status { Null, Ready, Loading, Error };
|
||||
Status status() const;
|
||||
qreal progress() const;
|
||||
QString statusText() const;
|
||||
|
||||
#ifndef QT_NO_ACTION
|
||||
QAction *reloadAction() const;
|
||||
QAction *backAction() const;
|
||||
QAction *forwardAction() const;
|
||||
QAction *stopAction() const;
|
||||
QAction* action(AmneziaWebView::WebAction) const;
|
||||
#endif
|
||||
|
||||
void load(const QNetworkRequest &request, QNetworkAccessManager::Operation operation = QNetworkAccessManager::GetOperation,
|
||||
const QByteArray &body = QByteArray());
|
||||
|
||||
QString html() const;
|
||||
|
||||
void setHtml(const QString &html, const QUrl &baseUrl = QUrl());
|
||||
void setContent(const QByteArray &data, const QString &mimeType = QString(), const QUrl &baseUrl = QUrl());
|
||||
|
||||
AmneziaWebHistory* history() const;
|
||||
|
||||
QQmlListProperty<QObject> javaScriptWindowObjects();
|
||||
AmneziaWebViewSettings* settingsObject() const;
|
||||
static AmneziaWebViewAttached* qmlAttachedProperties(QObject*);
|
||||
|
||||
QQmlComponent *newWindowComponent() const;
|
||||
void setNewWindowComponent(QQmlComponent *newWindow);
|
||||
QQuickItem* newWindowParent() const;
|
||||
void setNewWindowParent(QQuickItem* newWindow);
|
||||
|
||||
bool isComponentCompletePublic() const { return isComponentComplete(); }
|
||||
|
||||
QSize contentsSize() const;
|
||||
|
||||
void setContentsScale(qreal scale);
|
||||
qreal contentsScale() const;
|
||||
|
||||
QColor backgroundColor() const;
|
||||
void setBackgroundColor(const QColor&);
|
||||
|
||||
void paint(QPainter *painter) override;
|
||||
|
||||
void setDefaultFontSize(int size);
|
||||
void setStandardFontFamily(const QString &family);
|
||||
Q_INVOKABLE void setTextZoom(int percent);
|
||||
|
||||
Q_SIGNALS:
|
||||
|
||||
void preferredWidthChanged();
|
||||
void preferredHeightChanged();
|
||||
|
||||
void urlChanged();
|
||||
void progressChanged();
|
||||
void statusChanged(Status);
|
||||
void titleChanged(const QString&);
|
||||
void iconChanged();
|
||||
void statusTextChanged();
|
||||
void htmlChanged();
|
||||
void pressGrabTimeChanged();
|
||||
void newWindowComponentChanged();
|
||||
void newWindowParentChanged();
|
||||
void renderingEnabledChanged();
|
||||
void contentsSizeChanged(const QSize&);
|
||||
void contentsScaleChanged();
|
||||
void backgroundColorChanged();
|
||||
|
||||
void loadStarted();
|
||||
void loadFinished();
|
||||
void loadFinished(bool ok);
|
||||
void loadFailed();
|
||||
|
||||
void doubleClick(int clickX, int clickY);
|
||||
void zoomTo(qreal zoom, int centerX, int centerY);
|
||||
void alert(const QString& message);
|
||||
|
||||
public Q_SLOTS:
|
||||
void evaluateJavaScript(const QString&);
|
||||
|
||||
private Q_SLOTS:
|
||||
void afterRendering();
|
||||
void updateGeometry();
|
||||
void windowWasChanged(QQuickWindow* window);
|
||||
void parentWasChanged();
|
||||
void fillColorWasChanged();
|
||||
|
||||
void doLoadStarted();
|
||||
void doLoadProgress(int p);
|
||||
void doLoadFinished(bool ok);
|
||||
void setStatusText(const QString&);
|
||||
void windowObjectCleared();
|
||||
|
||||
protected:
|
||||
|
||||
void itemChange(ItemChange, const ItemChangeData &) override;
|
||||
void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override;
|
||||
QScopedPointer<AmneziaWebViewPrivate> d_ptr;
|
||||
|
||||
private:
|
||||
void updateContentsSize() {}
|
||||
void init();
|
||||
void componentComplete() override;
|
||||
QTimer upadeTimer;
|
||||
|
||||
|
||||
Q_DISABLE_COPY(AmneziaWebView)
|
||||
Q_DECLARE_PRIVATE(AmneziaWebView)
|
||||
|
||||
friend class QDeclarativeWebPage;
|
||||
};
|
||||
|
||||
class AmneziaWebViewAttached : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QString windowObjectName READ windowObjectName WRITE setWindowObjectName)
|
||||
public:
|
||||
explicit AmneziaWebViewAttached(QObject* parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
QString windowObjectName() const
|
||||
{
|
||||
return m_windowObjectName;
|
||||
}
|
||||
|
||||
void setWindowObjectName(const QString &n)
|
||||
{
|
||||
m_windowObjectName = n;
|
||||
}
|
||||
|
||||
private:
|
||||
QString m_windowObjectName;
|
||||
};
|
||||
|
||||
QT_END_NAMESPACE
|
||||
|
||||
QML_DECLARE_TYPE(AmneziaWebView)
|
||||
QML_DECLARE_TYPEINFO(AmneziaWebView, QML_HAS_ATTACHED_PROPERTIES)
|
||||
|
||||
#endif
|
||||
369
client/core/webview/amneziawebview_android.cpp
Normal file
369
client/core/webview/amneziawebview_android.cpp
Normal file
@@ -0,0 +1,369 @@
|
||||
#include <QtCore/qglobal.h>
|
||||
#include <QtCore>
|
||||
#include <QGuiApplication>
|
||||
#include <QCoreApplication>
|
||||
#include <QDebug>
|
||||
#include <QMap>
|
||||
#include <QMutex>
|
||||
#include <QMutexLocker>
|
||||
#include <QTimer>
|
||||
#include <jni.h>
|
||||
#include <android/bitmap.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "amneziawebview_p.h"
|
||||
#include "qrchandler.h"
|
||||
#include "filehandler.h"
|
||||
|
||||
#include <QJniObject>
|
||||
#include <QJniEnvironment>
|
||||
|
||||
namespace Jni
|
||||
{
|
||||
|
||||
using Object = QJniObject;
|
||||
}
|
||||
static const char qtAndroidWebViewControllerClass[] = "org/amnezia/vpn/WebViewController";
|
||||
|
||||
class AndroidWebViewPrivate;
|
||||
|
||||
typedef QMap<quintptr, AndroidWebViewPrivate *> WebViews;
|
||||
Q_GLOBAL_STATIC(WebViews, g_webViews)
|
||||
Q_GLOBAL_STATIC(QMutex, g_webMutex)
|
||||
|
||||
class AndroidWebViewPrivate : public AmneziaWebViewPrivate
|
||||
{
|
||||
Q_DECLARE_PUBLIC(AmneziaWebView)
|
||||
|
||||
public:
|
||||
|
||||
explicit AndroidWebViewPrivate(AmneziaWebView* q);
|
||||
virtual ~AndroidWebViewPrivate();
|
||||
|
||||
static AndroidWebViewPrivate *get(AmneziaWebView *q)
|
||||
{
|
||||
return static_cast<AndroidWebViewPrivate*>(AmneziaWebViewPrivate::get(q));
|
||||
}
|
||||
|
||||
virtual void setWindowParent(QWindow *parent);
|
||||
virtual void setBackgroundColor(const QColor backgroundColor);
|
||||
virtual void show();
|
||||
virtual void hide();
|
||||
virtual void setGeometry(const QRect &);
|
||||
virtual QString innerHTML() const;
|
||||
virtual void load(const QUrl& url);
|
||||
virtual void setHtml(const QString& html, const QUrl& baseUrl = QUrl());
|
||||
virtual void setContent(const QByteArray& data, const QString& mimeType, const QUrl& baseUrl);
|
||||
virtual void evaluateJavaScript(const QString& scriptSource);
|
||||
virtual bool isLoading() const;
|
||||
virtual bool canGoBack() const;
|
||||
virtual bool canGoForward() const;
|
||||
virtual void back();
|
||||
virtual void forward();
|
||||
virtual void reload();
|
||||
virtual void stop();
|
||||
virtual void setUrl(const QUrl &url);
|
||||
virtual QIcon icon() const;
|
||||
virtual void setScale(qreal scale);
|
||||
virtual qreal scale() const;
|
||||
virtual QSize contentsSize() const;
|
||||
virtual void setDefaultFontSize(int size);
|
||||
virtual void setStandardFontFamily(const QString &family);
|
||||
virtual void setTextZoom(int percent);
|
||||
private:
|
||||
|
||||
quintptr viewId;
|
||||
Jni::Object m_viewController;
|
||||
};
|
||||
|
||||
AndroidWebViewPrivate::AndroidWebViewPrivate(AmneziaWebView* q): AmneziaWebViewPrivate(q),
|
||||
viewId(reinterpret_cast<quintptr>(this))
|
||||
{
|
||||
m_viewController = Jni::Object(qtAndroidWebViewControllerClass,
|
||||
"(Landroid/app/Activity;J)V",
|
||||
QNativeInterface::QAndroidApplication::context().object(),
|
||||
viewId);
|
||||
QMutexLocker lock(g_webMutex());
|
||||
g_webViews->insert(viewId, this);
|
||||
setBackgroundColor(backgroundColor);
|
||||
}
|
||||
|
||||
AndroidWebViewPrivate::~AndroidWebViewPrivate()
|
||||
{
|
||||
QMutexLocker lock(g_webMutex());
|
||||
m_viewController.callMethod<void>("release", "()V");
|
||||
g_webViews->take(viewId);
|
||||
}
|
||||
|
||||
AmneziaWebViewPrivate *AmneziaWebViewPrivate::create(AmneziaWebView *q)
|
||||
{
|
||||
return new AndroidWebViewPrivate(q);
|
||||
}
|
||||
|
||||
void AndroidWebViewPrivate::setWindowParent(QWindow *parent)
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
}
|
||||
|
||||
|
||||
void AndroidWebViewPrivate::setBackgroundColor(const QColor backgroundColor)
|
||||
{
|
||||
m_viewController.callMethod<void>("setBackgroundColor", "(I)V",
|
||||
jint(backgroundColor.rgb()));
|
||||
emit backgroundColorChanged();
|
||||
}
|
||||
|
||||
|
||||
bool AndroidWebViewPrivate::isLoading() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Deprecated
|
||||
void AndroidWebViewPrivate::setScale(qreal scale)
|
||||
{
|
||||
Q_UNUSED(scale);
|
||||
}
|
||||
|
||||
/// Deprecated
|
||||
qreal AndroidWebViewPrivate::scale() const
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
QIcon AndroidWebViewPrivate::icon() const
|
||||
{
|
||||
return QIcon();
|
||||
}
|
||||
|
||||
QSize AndroidWebViewPrivate::contentsSize() const
|
||||
{
|
||||
return QSize();
|
||||
}
|
||||
|
||||
void AndroidWebViewPrivate::setGeometry(const QRect &geometry)
|
||||
{
|
||||
if (this->geometry != geometry) {
|
||||
this->geometry = geometry;
|
||||
|
||||
m_viewController.callMethod<void>("setGeometry", "(IIII)V",
|
||||
jint(geometry.x()), jint(geometry.y()),
|
||||
jint(geometry.width()), jint(geometry.height()) );
|
||||
}
|
||||
}
|
||||
|
||||
void AndroidWebViewPrivate::setTextZoom(int percent)
|
||||
{
|
||||
m_viewController.callMethod<void>("setTextZoom", "(I)V", jint(percent));
|
||||
}
|
||||
|
||||
void AndroidWebViewPrivate::hide()
|
||||
{
|
||||
Q_Q(AmneziaWebView);
|
||||
if (visible) {
|
||||
m_viewController.callMethod<void>("hide", "()V");
|
||||
visible = false;
|
||||
q->update();
|
||||
}
|
||||
}
|
||||
|
||||
void AndroidWebViewPrivate::show()
|
||||
{
|
||||
if (!visible) {
|
||||
m_viewController.callMethod<void>("show", "()V");
|
||||
visible = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void AndroidWebViewPrivate::setContent(const QByteArray& data, const QString& mimeType, const QUrl& baseUrl)
|
||||
{
|
||||
Q_UNUSED(data);
|
||||
Q_UNUSED(mimeType);
|
||||
Q_UNUSED(baseUrl);
|
||||
}
|
||||
|
||||
void AndroidWebViewPrivate::setUrl(const QUrl &url)
|
||||
{
|
||||
AmneziaWebViewPrivate::setUrl(url);
|
||||
}
|
||||
|
||||
void AndroidWebViewPrivate::load(const QUrl &url)
|
||||
{
|
||||
// Make WebView visible before loading
|
||||
if (!visible) {
|
||||
show();
|
||||
}
|
||||
Jni::Object jurl = Jni::Object::fromString(url.isValid() ? url.toString() : QString("about:blank"));
|
||||
m_viewController.callMethod<void>("loadUrl", "(Ljava/lang/String;)V", jurl.object<jstring>());
|
||||
}
|
||||
|
||||
void AndroidWebViewPrivate::setHtml(const QString &html, const QUrl &baseUrl)
|
||||
{
|
||||
if (html.isNull()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Jni::Object url = Jni::Object::fromString(baseUrl.isValid() ? baseUrl.toString() : QString("about:blank"));
|
||||
Jni::Object data = Jni::Object::fromString(html);
|
||||
Jni::Object mime = Jni::Object::fromString(QString("text/html"));
|
||||
Jni::Object encoding = Jni::Object::fromString(QString("utf-8"));
|
||||
|
||||
m_viewController.callMethod<void>("loadDataWithBaseURL", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V",
|
||||
url.object<jstring>(), data.object<jstring>(), mime.object<jstring>(), encoding.object<jstring>());
|
||||
}
|
||||
|
||||
void AndroidWebViewPrivate::evaluateJavaScript(const QString &scriptSource)
|
||||
{
|
||||
Jni::Object script = Jni::Object::fromString(scriptSource);
|
||||
m_viewController.callMethod<void>("evaluateJavaScript", "(Ljava/lang/String;)V", script.object<jstring>());
|
||||
}
|
||||
|
||||
bool AndroidWebViewPrivate::canGoBack() const
|
||||
{
|
||||
jboolean can = m_viewController.callMethod<jboolean>("canGoBack", "()Z");
|
||||
return can;
|
||||
}
|
||||
|
||||
bool AndroidWebViewPrivate::canGoForward() const
|
||||
{
|
||||
jboolean can = m_viewController.callMethod<jboolean>("canGoForward", "()Z");
|
||||
return can;
|
||||
}
|
||||
|
||||
void AndroidWebViewPrivate::back()
|
||||
{
|
||||
m_viewController.callMethod<void>("goBack", "()V");
|
||||
}
|
||||
|
||||
void AndroidWebViewPrivate::forward()
|
||||
{
|
||||
m_viewController.callMethod<void>("goForward", "()V");
|
||||
}
|
||||
|
||||
void AndroidWebViewPrivate::reload()
|
||||
{
|
||||
}
|
||||
|
||||
void AndroidWebViewPrivate::stop()
|
||||
{
|
||||
}
|
||||
|
||||
QString AndroidWebViewPrivate::innerHTML() const
|
||||
{
|
||||
return QString();
|
||||
}
|
||||
|
||||
void AndroidWebViewPrivate::setDefaultFontSize(int size)
|
||||
{
|
||||
m_viewController.callMethod<void>("setDefaultFontSize", "(I)V", jint(size));
|
||||
}
|
||||
|
||||
void AndroidWebViewPrivate::setStandardFontFamily(const QString &family)
|
||||
{
|
||||
Jni::Object fontFamily = Jni::Object::fromString(family);
|
||||
m_viewController.callMethod<void>("setStandardFontFamily", "(Ljava/lang/String;)V", fontFamily.object<jstring>());
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
JNIEXPORT void Java_org_amnezia_vpn_WebViewController_pageStarted(JNIEnv *env, jobject obj, jlong viewId, jstring url)
|
||||
{
|
||||
Q_UNUSED(env);
|
||||
Q_UNUSED(obj);
|
||||
|
||||
QMutexLocker lock(g_webMutex());
|
||||
AndroidWebViewPrivate *view = g_webViews()->value(viewId);
|
||||
if (view) {
|
||||
const char *urlChars = env->GetStringUTFChars(url, 0);
|
||||
const QUrl url = QUrl(QString(urlChars));
|
||||
QMetaObject::invokeMethod(view, "onPageStarted", Qt::QueuedConnection);
|
||||
}
|
||||
}
|
||||
|
||||
JNIEXPORT void Java_org_amnezia_vpn_WebViewController_pageFinished(JNIEnv *env, jobject obj, jlong viewId, jstring url)
|
||||
{
|
||||
Q_UNUSED(env);
|
||||
Q_UNUSED(obj);
|
||||
QMutexLocker lock(g_webMutex());
|
||||
AndroidWebViewPrivate *view = g_webViews()->value(viewId);
|
||||
if (view) {
|
||||
const char *urlChars = env->GetStringUTFChars(url, 0);
|
||||
const QUrl url = QUrl(QString(urlChars));
|
||||
QMetaObject::invokeMethod(view, "onPageFinished", Qt::QueuedConnection);
|
||||
}
|
||||
}
|
||||
|
||||
JNIEXPORT void Java_org_amnezia_vpn_WebViewController_urlChanged(JNIEnv *env, jobject obj, jlong viewId, jstring url)
|
||||
{
|
||||
Q_UNUSED(env);
|
||||
Q_UNUSED(obj);
|
||||
QMutexLocker lock(g_webMutex());
|
||||
AndroidWebViewPrivate *view = g_webViews()->value(viewId);
|
||||
if (view) {
|
||||
|
||||
const char *urlChars = env->GetStringUTFChars(url, 0);
|
||||
const QUrl url = QUrl(QString(urlChars));
|
||||
QMetaObject::invokeMethod(view, "onUrlChanged", Qt::QueuedConnection, Q_ARG( const QUrl, url));
|
||||
}
|
||||
}
|
||||
|
||||
JNIEXPORT jbyteArray Java_org_amnezia_vpn_WebViewController_dataForUrl(JNIEnv *env, jobject obj, jlong viewId, jstring url, jobject mimeType, jobject encoding)
|
||||
{
|
||||
Q_UNUSED(env)
|
||||
Q_UNUSED(obj)
|
||||
|
||||
QMutexLocker lock(g_webMutex());
|
||||
AndroidWebViewPrivate *view = g_webViews()->value(viewId);
|
||||
if (view) {
|
||||
|
||||
const char *urlChars = env->GetStringUTFChars(url, 0);
|
||||
const QUrl url = QUrl(QString(urlChars));
|
||||
|
||||
QByteArray buffer = view->dataForUrl(url);
|
||||
QString mime = view->mimeTypeForUrl(url);
|
||||
QString enc("utf-8");
|
||||
|
||||
jstring jMimeType = env->NewStringUTF(mime.toUtf8().constData());
|
||||
Jni::Object jMimeTypeObject(mimeType);
|
||||
if (jMimeTypeObject.isValid()) {
|
||||
jMimeTypeObject.callObjectMethod("insert", "(ILjava/lang/String;)Ljava/lang/StringBuilder;", 0, jMimeType);
|
||||
}
|
||||
|
||||
jstring jEncoding = env->NewStringUTF(enc.toUtf8().constData());
|
||||
Jni::Object jEncodingObject(encoding);
|
||||
if (jEncodingObject.isValid()) {
|
||||
|
||||
jEncodingObject.callObjectMethod("insert", "(ILjava/lang/String;)Ljava/lang/StringBuilder;", 0, jEncoding);
|
||||
}
|
||||
|
||||
env->DeleteLocalRef(jEncoding);
|
||||
env->DeleteLocalRef(jMimeType);
|
||||
|
||||
jbyteArray data = env->NewByteArray(buffer.size());
|
||||
env->SetByteArrayRegion(data, 0, buffer.size(), (const jbyte*) buffer.data());
|
||||
return data;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean Java_org_amnezia_vpn_WebViewController_canHandleUrl(JNIEnv *env, jobject obj, jlong viewId, jstring url)
|
||||
|
||||
{
|
||||
Q_UNUSED(env);
|
||||
Q_UNUSED(obj);
|
||||
QMutexLocker lock(g_webMutex());
|
||||
AndroidWebViewPrivate *view = g_webViews()->value(viewId);
|
||||
if (view) {
|
||||
const char *urlChars = env->GetStringUTFChars(url, 0);
|
||||
const QUrl url = QUrl(QString(urlChars));
|
||||
return view->canHandleUrl(url);
|
||||
}
|
||||
|
||||
return jboolean(false);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
433
client/core/webview/amneziawebview_desktop.cpp
Normal file
433
client/core/webview/amneziawebview_desktop.cpp
Normal file
@@ -0,0 +1,433 @@
|
||||
#include <QtCore>
|
||||
#include <QDebug>
|
||||
#include <QBoxLayout>
|
||||
#include <QApplication>
|
||||
#include <QGuiApplication>
|
||||
#include <QStyle>
|
||||
#include <QQuickWindow>
|
||||
|
||||
#include "amneziawebview_desktop_p.h"
|
||||
#include "qrchandler.h"
|
||||
#include "filehandler.h"
|
||||
|
||||
typedef QMap<quintptr, AmneziaWebViewPrivate *> WebViews;
|
||||
Q_GLOBAL_STATIC(WebViews, g_webViews)
|
||||
|
||||
|
||||
QrcHandler::QrcHandler()
|
||||
{}
|
||||
|
||||
|
||||
FileHandler::FileHandler()
|
||||
{
|
||||
}
|
||||
|
||||
JsHandler::JsHandler(AmneziaWebView *host): _host(host), scriptObjectsInjected(false)
|
||||
{
|
||||
init();
|
||||
}
|
||||
|
||||
JsHandler::~JsHandler() {}
|
||||
|
||||
WebPage::WebPage(QObject *parent)
|
||||
: QWebPage(parent)
|
||||
{
|
||||
connect(this, SIGNAL(unsupportedContent(QNetworkReply*)),
|
||||
this, SLOT(handleUnsupportedContent(QNetworkReply*)));
|
||||
}
|
||||
|
||||
void WebPage::javaScriptAlert(QWebFrame *frame, const QString& msg)
|
||||
{
|
||||
Q_UNUSED(frame)
|
||||
Q_UNUSED(msg)
|
||||
}
|
||||
|
||||
WebPage::~WebPage()
|
||||
{
|
||||
disconnect(this);
|
||||
}
|
||||
|
||||
bool WebPage::acceptNavigationRequest(QWebFrame *frame, const QNetworkRequest &request, NavigationType type)
|
||||
{
|
||||
return QWebPage::acceptNavigationRequest(frame, request, type);
|
||||
}
|
||||
|
||||
void WebPage::handleUnsupportedContent(QNetworkReply *reply)
|
||||
{
|
||||
QString errorString = reply->errorString();
|
||||
|
||||
if (m_loadingUrl != reply->url()) {
|
||||
// sub resource of this page
|
||||
qWarning() << "Resource" << reply->url().toEncoded() << "has unknown Content-Type, will be ignored.";
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError && !reply->header(QNetworkRequest::ContentTypeHeader).isValid()) {
|
||||
errorString = "Unknown Content-Type";
|
||||
}
|
||||
|
||||
QFile file(QLatin1String(":/notfound.html"));
|
||||
bool isOpened = file.open(QIODevice::ReadOnly);
|
||||
Q_ASSERT(isOpened);
|
||||
Q_UNUSED(isOpened)
|
||||
|
||||
QString title = QCoreApplication::translate("webview", "Error loading page: %1").arg(reply->url().toString());
|
||||
QString html = QString(QLatin1String(file.readAll()))
|
||||
.arg(title)
|
||||
.arg(errorString)
|
||||
.arg(reply->url().toString());
|
||||
|
||||
QBuffer imageBuffer;
|
||||
imageBuffer.open(QBuffer::ReadWrite);
|
||||
QIcon icon = view()->style()->standardIcon(QStyle::SP_MessageBoxWarning, nullptr, view());
|
||||
QPixmap pixmap = icon.pixmap(QSize(32,32));
|
||||
if (pixmap.save(&imageBuffer, "PNG")) {
|
||||
html.replace(QLatin1String("IMAGE_BINARY_DATA_HERE"),
|
||||
QString(QLatin1String(imageBuffer.buffer().toBase64())));
|
||||
}
|
||||
|
||||
QList<QWebFrame*> frames;
|
||||
frames.append(mainFrame());
|
||||
while (!frames.isEmpty()) {
|
||||
QWebFrame *frame = frames.takeFirst();
|
||||
if (frame->url() == reply->url()) {
|
||||
frame->setHtml(html, reply->url());
|
||||
return;
|
||||
}
|
||||
QList<QWebFrame *> children = frame->childFrames();
|
||||
foreach(QWebFrame *frame, children)
|
||||
frames.append(frame);
|
||||
}
|
||||
if (m_loadingUrl == reply->url()) {
|
||||
mainFrame()->setHtml(html, reply->url());
|
||||
}
|
||||
}
|
||||
|
||||
DesktopWebViewPrivate::DesktopWebViewPrivate(AmneziaWebView* q): AmneziaWebViewPrivate(q)
|
||||
, viewId(reinterpret_cast<quintptr>(this))
|
||||
, containerWindow(nullptr)
|
||||
, window(nullptr)
|
||||
{
|
||||
container = new QWidget(0, Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint | Qt::Tool);
|
||||
container->setAttribute(Qt::WA_NativeWindow, true);
|
||||
container->setAttribute(Qt::WA_DontCreateNativeAncestors, true);
|
||||
|
||||
// Do not remove next line -> prevent some sort of spontaneous crashes
|
||||
QWebSettings::setObjectCacheCapacities(0, 0, 0);
|
||||
|
||||
view = new QWebView(container);
|
||||
WebPage *page = new WebPage(view);
|
||||
page->setForwardUnsupportedContent(true);
|
||||
page->setNetworkAccessManager(networkAccessManager());
|
||||
page->settings()->setAttribute(QWebSettings::DeveloperExtrasEnabled, true);
|
||||
view->setPage(page);
|
||||
|
||||
container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||
container->setLayout(new QHBoxLayout(container));
|
||||
container->layout()->setSpacing(0);
|
||||
container->layout()->setMargin(0);
|
||||
container->layout()->addWidget(view);
|
||||
|
||||
setBackgroundColor(backgroundColor);
|
||||
g_webViews->insert(viewId, this);
|
||||
|
||||
connect(view, SIGNAL(loadFinished(bool)), this, SIGNAL(loadFinished(bool)));
|
||||
|
||||
connect(page, SIGNAL(loadFinished(bool)), this, SLOT(onLoadFinished(bool)), Qt::QueuedConnection);
|
||||
connect(page, SIGNAL(loadStarted()), this, SLOT(onPageStarted()), Qt::QueuedConnection);
|
||||
connect(view, SIGNAL(urlChanged(const QUrl &)), this, SLOT(onUrlChanged(const QUrl &)), Qt::QueuedConnection);
|
||||
container->createWinId();
|
||||
}
|
||||
|
||||
DesktopWebViewPrivate::~DesktopWebViewPrivate()
|
||||
{
|
||||
disconnect(this, SLOT(loadFinished(bool)));
|
||||
disconnect(this, SLOT(applicationStateChanged(Qt::ApplicationState)));
|
||||
disconnect(this, SLOT(onUrlChanged(const QUrl &)));
|
||||
disconnect(this, SLOT(onPageStarted()));
|
||||
disconnect(this, SLOT(onLoadFinished(bool)));
|
||||
|
||||
g_webViews->take(viewId);
|
||||
view->stop();
|
||||
view->setPage(nullptr);
|
||||
|
||||
container->deleteLater();
|
||||
}
|
||||
|
||||
AmneziaWebViewPrivate *AmneziaWebViewPrivate::create(AmneziaWebView *q)
|
||||
{
|
||||
return new DesktopWebViewPrivate(q);
|
||||
}
|
||||
|
||||
void DesktopWebViewPrivate::setWindowParent(QWindow *parent)
|
||||
{
|
||||
if (window) {
|
||||
window->removeEventFilter(this);
|
||||
}
|
||||
|
||||
if (parent) {
|
||||
|
||||
containerWindow = qobject_cast<QWindow*>(container->windowHandle());
|
||||
containerWindow->setTransientParent(parent);
|
||||
parent->installEventFilter(this);
|
||||
|
||||
}
|
||||
window = parent;
|
||||
}
|
||||
|
||||
void DesktopWebViewPrivate::setBackgroundColor(const QColor backgroundColor)
|
||||
{
|
||||
this->backgroundColor = backgroundColor;
|
||||
QPalette p = container->palette();
|
||||
p.setColor(QPalette::Background, backgroundColor);
|
||||
container->setPalette(p);
|
||||
p = view->palette();
|
||||
p.setColor(QPalette::Background, backgroundColor);
|
||||
view->setPalette(p);
|
||||
emit backgroundColorChanged();
|
||||
}
|
||||
|
||||
/// Deprecated
|
||||
void DesktopWebViewPrivate::setScale(qreal scale)
|
||||
{
|
||||
Q_UNUSED(scale)
|
||||
//qreal s = view->geometry().width() / view->page()->preferredContentsSize().width();
|
||||
//view->setZoomFactor(s);
|
||||
}
|
||||
|
||||
/// Deprecated
|
||||
qreal DesktopWebViewPrivate::scale() const
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
QIcon DesktopWebViewPrivate::icon() const
|
||||
{
|
||||
return QIcon();
|
||||
}
|
||||
|
||||
QSize DesktopWebViewPrivate::contentsSize() const
|
||||
{
|
||||
return QSize();
|
||||
}
|
||||
|
||||
void DesktopWebViewPrivate::takeSnapshot()
|
||||
{
|
||||
if (geometry.isEmpty() || !containerWindow) {
|
||||
return;
|
||||
}
|
||||
else {
|
||||
|
||||
container->updateGeometry();
|
||||
QPixmap pixmap = container->grab();
|
||||
snapshot = pixmap.toImage();
|
||||
emit snapshotChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void DesktopWebViewPrivate::setGeometry(const QRect &geometry)
|
||||
{
|
||||
Q_Q(AmneziaWebView);
|
||||
QQuickWindow *window = q->window();
|
||||
if (!window) return;
|
||||
QRect newGeometry = QRect(window->mapToGlobal(geometry.topLeft()), QSize(geometry.width(), geometry.height()));
|
||||
if (newGeometry.isValid() && container->geometry() != newGeometry ) {
|
||||
|
||||
this->geometry = geometry;
|
||||
container->setGeometry(newGeometry);
|
||||
container->updateGeometry();
|
||||
}
|
||||
}
|
||||
|
||||
bool DesktopWebViewPrivate::eventFilter(QObject *obj, QEvent *event)
|
||||
{
|
||||
Q_UNUSED(obj)
|
||||
Q_Q(AmneziaWebView);
|
||||
|
||||
switch (event->type()) {
|
||||
case QEvent::Move: {
|
||||
QMoveEvent *moveEvent = static_cast<QMoveEvent*>(event);
|
||||
QPoint p = q->mapToScene(QPointF(moveEvent->pos())).toPoint();
|
||||
container->move(p);
|
||||
//container->updateGeometry();
|
||||
|
||||
if (visible) {
|
||||
show();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case QEvent::Resize: {
|
||||
|
||||
//QRect newGeometry = QRect(window->mapToGlobal(geometry.topLeft()), QSize(geometry.width(), geometry.height()));
|
||||
//container->setGeometry(newGeometry);
|
||||
//container->updateGeometry();
|
||||
|
||||
if (visible) {
|
||||
show();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case QEvent::WindowStateChange: {
|
||||
|
||||
Qt::WindowState state = window->windowState();
|
||||
if ((state == Qt::WindowMaximized) || (state == Qt::WindowFullScreen) || (state == Qt::WindowActive)) {
|
||||
show();
|
||||
}
|
||||
else {
|
||||
hide();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DesktopWebViewPrivate::hide()
|
||||
{
|
||||
Q_Q(AmneziaWebView);
|
||||
if (!q->window()) return;
|
||||
|
||||
if (visible) {
|
||||
|
||||
QMetaObject::invokeMethod(this, "requestSnapshot", Qt::QueuedConnection);
|
||||
QMetaObject::invokeMethod(container, "hide", Qt::QueuedConnection);
|
||||
visible = false;
|
||||
|
||||
//if (q->isVisible())
|
||||
// q->update();
|
||||
}
|
||||
}
|
||||
|
||||
void DesktopWebViewPrivate::show()
|
||||
{
|
||||
Q_Q(AmneziaWebView);
|
||||
if (!q->window()) return;
|
||||
if (!visible) {
|
||||
|
||||
QMetaObject::invokeMethod(container, "show", Qt::QueuedConnection);
|
||||
QMetaObject::invokeMethod(container, "update", Qt::QueuedConnection);
|
||||
visible = true;
|
||||
}
|
||||
|
||||
if ((qApp->topLevelWindows().at(0) != containerWindow) || !containerWindow->isVisible()) {
|
||||
|
||||
containerWindow->raise();
|
||||
}
|
||||
}
|
||||
|
||||
void DesktopWebViewPrivate::load(const QUrl& baseUrl)
|
||||
{
|
||||
QUrl url = baseUrl;
|
||||
if (!url.isValid()) {
|
||||
url = QUrl(QLatin1String("about:blank"));
|
||||
}
|
||||
view->load(url);
|
||||
history()->append(baseUrl);
|
||||
}
|
||||
|
||||
void DesktopWebViewPrivate::setContent(const QByteArray& data, const QString& mimeType, const QUrl& baseUrl)
|
||||
{
|
||||
QUrl url = baseUrl;
|
||||
if (!url.isValid()) {
|
||||
url = QUrl(QLatin1String("about:blank"));
|
||||
}
|
||||
view->setContent(data, mimeType, url);
|
||||
history()->append(url, data, mimeType);
|
||||
}
|
||||
|
||||
void DesktopWebViewPrivate::setHtml(const QString& html, const QUrl& baseUrl)
|
||||
{
|
||||
if(html.isNull()) return;
|
||||
QUrl url = baseUrl;
|
||||
if (!baseUrl.isValid())
|
||||
url = QUrl(QLatin1String("about:blank"));
|
||||
|
||||
view->setHtml(html, url);
|
||||
history()->append(url, html.toUtf8(), "text/html");
|
||||
}
|
||||
|
||||
void DesktopWebViewPrivate::evaluateJavaScript(const QString& scriptSource)
|
||||
{
|
||||
view->page()->mainFrame()->evaluateJavaScript(scriptSource);
|
||||
}
|
||||
|
||||
bool DesktopWebViewPrivate::canGoBack() const
|
||||
{
|
||||
QAction *pageAction = view->pageAction(QWebPage::Back);
|
||||
bool can = (pageAction && pageAction->isEnabled());
|
||||
return can;
|
||||
}
|
||||
|
||||
bool DesktopWebViewPrivate::canGoForward() const
|
||||
{
|
||||
QAction *pageAction = view->pageAction(QWebPage::Forward);
|
||||
bool can = (pageAction && pageAction->isEnabled());
|
||||
return can;
|
||||
}
|
||||
|
||||
void DesktopWebViewPrivate::back()
|
||||
{
|
||||
QAction *pageAction = view->pageAction(QWebPage::Back);
|
||||
if (pageAction)
|
||||
emit pageAction->trigger();
|
||||
}
|
||||
|
||||
void DesktopWebViewPrivate::forward()
|
||||
{
|
||||
QAction *pageAction = view->pageAction(QWebPage::Forward);
|
||||
if (pageAction)
|
||||
emit pageAction->trigger();
|
||||
}
|
||||
|
||||
void DesktopWebViewPrivate::reload()
|
||||
{
|
||||
QAction *pageAction = view->pageAction(QWebPage::Reload);
|
||||
if (pageAction)
|
||||
emit pageAction->trigger();
|
||||
}
|
||||
|
||||
void DesktopWebViewPrivate::stop()
|
||||
{
|
||||
QAction *pageAction = view->pageAction(QWebPage::Stop);
|
||||
if (pageAction)
|
||||
emit pageAction->trigger();
|
||||
}
|
||||
|
||||
bool DesktopWebViewPrivate::isLoading() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
QString DesktopWebViewPrivate::innerHTML() const
|
||||
{
|
||||
QVariant result = view->page()->mainFrame()->evaluateJavaScript("document.body.innerHTML");
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
void DesktopWebViewPrivate::onLoadFinished(bool success)
|
||||
{
|
||||
if (success) {
|
||||
QMetaObject::invokeMethod(this, "onPageFinished", Qt::QueuedConnection);
|
||||
}
|
||||
else {
|
||||
QMetaObject::invokeMethod(this, "onPageError", Qt::QueuedConnection);
|
||||
}
|
||||
}
|
||||
|
||||
void DesktopWebViewPrivate::setDefaultFontSize(int size)
|
||||
{
|
||||
view->settings()->setFontSize(QWebSettings::DefaultFontSize, size);
|
||||
}
|
||||
|
||||
void DesktopWebViewPrivate::setStandardFontFamily(const QString &family)
|
||||
{
|
||||
view->settings()->setFontFamily(QWebSettings::StandardFont, family);
|
||||
}
|
||||
|
||||
void DesktopWebViewPrivate::setTextZoom(int percent)
|
||||
{
|
||||
Q_UNUSED(percent)
|
||||
}
|
||||
96
client/core/webview/amneziawebview_desktop_p.h
Normal file
96
client/core/webview/amneziawebview_desktop_p.h
Normal file
@@ -0,0 +1,96 @@
|
||||
#ifndef AMNEZIAWEBVIEW_DESKTOP_P_H
|
||||
#define AMNEZIAWEBVIEW_DESKTOP_P_H
|
||||
|
||||
// QtWebKit is deprecated and not available in Qt 6
|
||||
// This file should only be used with Qt 5 when WebEngineWidgets is not available
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
#error "amneziawebview_desktop_p.h uses QtWebKit which is not available in Qt 6. Use amneziawebview_webengine_p.h instead."
|
||||
#endif
|
||||
|
||||
#include <QtWebKitWidgets/QWebView>
|
||||
#include <QtWebKitWidgets/QWebPage>
|
||||
#include <QtWebKitWidgets/QWebFrame>
|
||||
#include <QtWebKit/QWebSettings>
|
||||
|
||||
#include "amneziawebview.h"
|
||||
#include "amneziawebview_p.h"
|
||||
|
||||
class DesktopWebViewPrivate;
|
||||
|
||||
class WebPage : public QWebPage
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
signals:
|
||||
void loadingUrl(const QUrl &url);
|
||||
|
||||
public:
|
||||
explicit WebPage(QObject *parent = nullptr);
|
||||
virtual ~WebPage();
|
||||
|
||||
protected:
|
||||
bool acceptNavigationRequest(QWebFrame *frame, const QNetworkRequest &request, NavigationType type);
|
||||
virtual void javaScriptAlert(QWebFrame *frame, const QString& msg);
|
||||
|
||||
private slots:
|
||||
void handleUnsupportedContent(QNetworkReply *reply);
|
||||
|
||||
private:
|
||||
|
||||
friend class DesktopWebViewPrivate;
|
||||
QUrl m_loadingUrl;
|
||||
};
|
||||
|
||||
class DesktopWebViewPrivate : public AmneziaWebViewPrivate
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DECLARE_PUBLIC(AmneziaWebView)
|
||||
public:
|
||||
|
||||
explicit DesktopWebViewPrivate(AmneziaWebView* q);
|
||||
virtual ~DesktopWebViewPrivate();
|
||||
|
||||
virtual void setWindowParent(QWindow *parent);
|
||||
|
||||
virtual void setBackgroundColor(const QColor backgroundColor);
|
||||
virtual void show();
|
||||
virtual void hide();
|
||||
virtual void takeSnapshot();
|
||||
virtual void setGeometry(const QRect &);
|
||||
virtual QString innerHTML() const;
|
||||
virtual void load(const QUrl& url);
|
||||
virtual void setHtml(const QString& html, const QUrl& baseUrl = QUrl());
|
||||
virtual void setContent(const QByteArray& data, const QString& mimeType, const QUrl& baseUrl);
|
||||
virtual void evaluateJavaScript(const QString& scriptSource);
|
||||
virtual bool isLoading() const;
|
||||
virtual bool canGoBack() const;
|
||||
virtual bool canGoForward() const;
|
||||
virtual void back();
|
||||
virtual void forward();
|
||||
virtual void reload();
|
||||
virtual void stop();
|
||||
|
||||
virtual QIcon icon() const;
|
||||
virtual void setScale(qreal scale);
|
||||
virtual qreal scale() const;
|
||||
virtual QSize contentsSize() const;
|
||||
virtual void setDefaultFontSize(int size);
|
||||
virtual void setTextZoom(int percent) { Q_UNUSED(percent); }
|
||||
virtual void setStandardFontFamily(const QString &family);
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject *obj, QEvent *event);
|
||||
|
||||
private slots:
|
||||
void onLoadFinished(bool);
|
||||
|
||||
private:
|
||||
quintptr viewId;
|
||||
QWebView *view;
|
||||
QWidget *container;
|
||||
QWindow *containerWindow;
|
||||
QWindow *window;
|
||||
};
|
||||
|
||||
|
||||
#endif // AMNEZIAWEBVIEW_DESKTOP_P_H
|
||||
1045
client/core/webview/amneziawebview_ios.mm
Normal file
1045
client/core/webview/amneziawebview_ios.mm
Normal file
File diff suppressed because it is too large
Load Diff
423
client/core/webview/amneziawebview_p.cpp
Normal file
423
client/core/webview/amneziawebview_p.cpp
Normal file
@@ -0,0 +1,423 @@
|
||||
#include "amneziawebview_p.h"
|
||||
#include "amneziawebhistory.h"
|
||||
#include "websettings.h"
|
||||
|
||||
NetworkAccessManager::NetworkAccessManager(QNetworkAccessManager *manager, QObject *parent)
|
||||
: QNetworkAccessManager(parent)
|
||||
{
|
||||
this->manager = manager;
|
||||
setCache(manager->cache());
|
||||
setCookieJar(manager->cookieJar());
|
||||
setProxy(manager->proxy());
|
||||
setProxyFactory(manager->proxyFactory());
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
void NetworkAccessManager::init()
|
||||
{
|
||||
connect(this, SIGNAL(sslErrors(QNetworkReply*, const QList<QSslError> & )), this,
|
||||
SLOT(handleSslErrors(QNetworkReply*, const QList<QSslError> & )));
|
||||
}
|
||||
|
||||
QNetworkReply *NetworkAccessManager::createRequest(QNetworkAccessManager::Operation operation, const QNetworkRequest &request, QIODevice *device)
|
||||
{
|
||||
AmneziaWebView *view = qobject_cast<AmneziaWebView *>(parent());
|
||||
AmneziaWebViewPrivate *d = AmneziaWebViewPrivate::get(view);
|
||||
|
||||
if (!d || !d->canHandleUrl(request.url())) {
|
||||
return QNetworkAccessManager::createRequest(operation, request, device);
|
||||
}
|
||||
|
||||
if (operation == GetOperation) {
|
||||
return new DataReply(this, operation, request, view);
|
||||
}
|
||||
else
|
||||
|
||||
return QNetworkAccessManager::createRequest(operation, request, device);
|
||||
}
|
||||
|
||||
void NetworkAccessManager::handleSslErrors(QNetworkReply* reply, const QList<QSslError> &errors)
|
||||
{
|
||||
Q_UNUSED(errors)
|
||||
reply->ignoreSslErrors();
|
||||
}
|
||||
|
||||
DataReply::DataReply(QObject *parent, const QNetworkAccessManager::Operation operation, const QNetworkRequest &request, AmneziaWebView *view): QNetworkReply(parent)
|
||||
{
|
||||
setRequest(request);
|
||||
setUrl(request.url());
|
||||
setOperation(operation);
|
||||
setFinished(true);
|
||||
this->view = view;
|
||||
offset = 0;
|
||||
|
||||
//QUrl url = request.url();
|
||||
//url.setHost(QString());
|
||||
//if (url.path().isEmpty())
|
||||
// url.setPath(QLatin1String("/"));
|
||||
//setUrl(url);
|
||||
|
||||
QMetaObject::invokeMethod(this, "setContent", Qt::QueuedConnection );
|
||||
}
|
||||
|
||||
void DataReply::setContent()
|
||||
{
|
||||
if (!view || view.isNull()) return;
|
||||
|
||||
AmneziaWebViewPrivate *q = AmneziaWebViewPrivate::get(view);
|
||||
content = q->dataForUrl(url());
|
||||
QString mimeType = q->mimeTypeForUrl(url()).toLower();
|
||||
//open(ReadOnly | Unbuffered);
|
||||
QNetworkReply::open(QIODevice::ReadOnly);
|
||||
|
||||
int size = content.size();
|
||||
if (size <= 0 ) {
|
||||
|
||||
QString msg = QString("Error opening %1").arg(url().toString());
|
||||
qCritical() << msg;
|
||||
setError(QNetworkReply::ContentNotFoundError, msg);
|
||||
QMetaObject::invokeMethod(this, "error", Qt::QueuedConnection,
|
||||
Q_ARG(QNetworkReply::NetworkError, QNetworkReply::ContentNotFoundError));
|
||||
|
||||
QMetaObject::invokeMethod(this, "finished", Qt::QueuedConnection);
|
||||
return;
|
||||
}
|
||||
|
||||
setHeader(QNetworkRequest::ContentTypeHeader, QVariant(QString("%1; charset=%2").arg(mimeType, "utf-8")));
|
||||
|
||||
setHeader(QNetworkRequest::ContentLengthHeader, size);
|
||||
QMetaObject::invokeMethod(this, "metaDataChanged", Qt::QueuedConnection);
|
||||
QMetaObject::invokeMethod(this, "downloadProgress", Qt::QueuedConnection,
|
||||
Q_ARG(qint64, size), Q_ARG(qint64, size));
|
||||
QMetaObject::invokeMethod(this, "readyRead", Qt::QueuedConnection);
|
||||
QMetaObject::invokeMethod(this, "finished", Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void DataReply::abort()
|
||||
{
|
||||
QNetworkReply::close();
|
||||
}
|
||||
|
||||
void DataReply::close()
|
||||
{
|
||||
QNetworkReply::close();
|
||||
}
|
||||
|
||||
qint64 DataReply::size() const
|
||||
{
|
||||
return content.size();
|
||||
}
|
||||
|
||||
bool DataReply::isSequential() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
qint64 DataReply::bytesAvailable() const
|
||||
{
|
||||
return content.size() - offset + QIODevice::bytesAvailable();
|
||||
}
|
||||
qint64 DataReply::readData(char *data, qint64 maxSize)
|
||||
{
|
||||
if (offset < content.size()) {
|
||||
qint64 number = qMin(maxSize, content.size() - offset);
|
||||
memcpy(data, content.constData() + offset, number);
|
||||
offset += number;
|
||||
return number;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
AmneziaWebViewPrivate::AmneziaWebViewPrivate(AmneziaWebView *q) : QObject(q)
|
||||
, pending(PendingNone)
|
||||
, status(AmneziaWebView::Null)
|
||||
, preferredwidth(0)
|
||||
, preferredheight(0)
|
||||
, progress(1.0)
|
||||
, newWindowComponent(nullptr)
|
||||
, newWindowParent(nullptr)
|
||||
, jsHandler(q)
|
||||
, rendering(true)
|
||||
, overlapped(false)
|
||||
, backgroundColor(QColor::fromRgb(28, 29, 33))
|
||||
, visible(false)
|
||||
, networkManager(nullptr)
|
||||
, q_ptr(q)
|
||||
, m_history(new AmneziaWebHistory(q))
|
||||
, m_settings(new AmneziaWebViewSettings(q))
|
||||
{
|
||||
}
|
||||
|
||||
void AmneziaWebViewPrivate::init()
|
||||
{
|
||||
Q_Q(AmneziaWebView);
|
||||
m_settings->apply();
|
||||
|
||||
actions.setMapping(new QAction(q), (int)AmneziaWebView::Back);
|
||||
actions.setMapping(new QAction(q), (int)AmneziaWebView::Forward);
|
||||
actions.setMapping(new QAction(q), (int)AmneziaWebView::Stop);
|
||||
actions.setMapping(new QAction(q), (int)AmneziaWebView::Reload);
|
||||
connect(action(AmneziaWebView::Back), SIGNAL(triggered()), &actions, SLOT(map()));
|
||||
connect(action(AmneziaWebView::Forward), SIGNAL(triggered()), &actions, SLOT(map()));
|
||||
connect(action(AmneziaWebView::Forward), SIGNAL(triggered()), &actions, SLOT(map()));
|
||||
connect(action(AmneziaWebView::Forward), SIGNAL(triggered()), &actions, SLOT(map()));
|
||||
connect(&actions, SIGNAL(mappedInt(int)), this, SLOT(onAction(int)));
|
||||
connect(qApp, SIGNAL(applicationStateChanged(Qt::ApplicationState)), this, SLOT(applicationStateChanged(Qt::ApplicationState)));
|
||||
}
|
||||
|
||||
AmneziaWebViewPrivate::~AmneziaWebViewPrivate()
|
||||
{
|
||||
Q_Q(AmneziaWebView);
|
||||
disconnect(this, SLOT(onAction(int)));
|
||||
disconnect(&actions, SLOT(map()));
|
||||
if (networkManager && networkManager->parent() == q)
|
||||
delete networkManager;
|
||||
}
|
||||
|
||||
AmneziaWebViewPrivate *AmneziaWebViewPrivate::get(AmneziaWebView *q)
|
||||
{
|
||||
if (!q) { return nullptr; }
|
||||
return q->d_func();
|
||||
}
|
||||
|
||||
void AmneziaWebViewPrivate::applicationStateChanged(Qt::ApplicationState state)
|
||||
{
|
||||
Q_Q(AmneziaWebView);
|
||||
if ((state == Qt::ApplicationActive) && q->isVisible()) {
|
||||
emit q->update();
|
||||
}
|
||||
else {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
|
||||
void AmneziaWebViewPrivate::requestShow()
|
||||
{
|
||||
show();
|
||||
}
|
||||
|
||||
void AmneziaWebViewPrivate::requestHide()
|
||||
{
|
||||
hide();
|
||||
}
|
||||
|
||||
void AmneziaWebViewPrivate::move(const QPoint &point)
|
||||
{
|
||||
QRect newGeomentry = geometry;
|
||||
newGeomentry.moveTo(point);
|
||||
setGeometry(newGeomentry);
|
||||
}
|
||||
|
||||
void AmneziaWebViewPrivate::windowObjectsClear(QQmlListProperty<QObject>* prop)
|
||||
{
|
||||
static_cast<AmneziaWebViewPrivate*>(prop->data)->windowObjects.clear();
|
||||
}
|
||||
|
||||
QObject *AmneziaWebViewPrivate::windowObjectsAt(QQmlListProperty<QObject> *prop, qsizetype index)
|
||||
{
|
||||
return static_cast<AmneziaWebViewPrivate *>(prop->data)->windowObjects.at(index);
|
||||
}
|
||||
|
||||
qsizetype AmneziaWebViewPrivate::windowObjectsCount(QQmlListProperty<QObject> *prop)
|
||||
{
|
||||
return static_cast<AmneziaWebViewPrivate *>(prop->data)->windowObjects.count();
|
||||
}
|
||||
|
||||
void AmneziaWebViewPrivate::windowObjectsAppend(QQmlListProperty<QObject>* prop, QObject* o)
|
||||
{
|
||||
static_cast<AmneziaWebViewPrivate*>(prop->data)->windowObjects.append(o);
|
||||
static_cast<AmneziaWebViewPrivate*>(prop->data)->updateWindowObjects();
|
||||
}
|
||||
|
||||
void AmneziaWebViewPrivate::setTitle(const QString &title)
|
||||
{
|
||||
Q_Q(AmneziaWebView);
|
||||
if (this->title != title) {
|
||||
this->title = title;
|
||||
emit q->titleChanged(this->title);
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray AmneziaWebViewPrivate::dataForUrl(const QUrl &url) const
|
||||
{
|
||||
QByteArray data;
|
||||
if (qrcHandler.canHandleUrl(url)) {
|
||||
data = qrcHandler.dataForUrl(url);
|
||||
}
|
||||
else if (jsHandler.canHandleUrl(url)) {
|
||||
|
||||
data = jsHandler.dataForUrl(url);
|
||||
}
|
||||
else if (fileHandler.canHandleUrl(url)) {
|
||||
|
||||
data = fileHandler.dataForUrl(url);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
QString AmneziaWebViewPrivate::mimeTypeForUrl(const QUrl &url) const
|
||||
{
|
||||
QString mimeType("application/octet-stream");
|
||||
if (qrcHandler.canHandleUrl(url)) {
|
||||
mimeType = qrcHandler.mimeTypeForUrl(url);
|
||||
}
|
||||
else if (jsHandler.canHandleUrl(url)) {
|
||||
mimeType = jsHandler.mimeTypeForUrl(url);
|
||||
}
|
||||
else if (fileHandler.canHandleUrl(url)) {
|
||||
mimeType = fileHandler.mimeTypeForUrl(url);
|
||||
}
|
||||
return mimeType;
|
||||
}
|
||||
|
||||
void AmneziaWebViewPrivate::onPageStarted()
|
||||
{
|
||||
Q_Q(AmneziaWebView);
|
||||
emit q->loadStarted();
|
||||
}
|
||||
|
||||
void AmneziaWebViewPrivate::onPageFinished()
|
||||
{
|
||||
Q_Q(AmneziaWebView);
|
||||
jsHandler.updateWebView();
|
||||
|
||||
if (lastError.length() > 0) {
|
||||
emit q->loadFinished(false);
|
||||
}
|
||||
else {
|
||||
emit q->loadFinished(true);
|
||||
}
|
||||
}
|
||||
|
||||
void AmneziaWebViewPrivate::onPageError()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void AmneziaWebViewPrivate::onUrlChanged(const QUrl &url)
|
||||
{
|
||||
history()->append(url);
|
||||
setUrl(url);
|
||||
}
|
||||
|
||||
void AmneziaWebViewPrivate::setUrl(const QUrl &url)
|
||||
{
|
||||
Q_Q(AmneziaWebView);
|
||||
|
||||
QString urlString = url.toString();
|
||||
while ( urlString.endsWith('#')) urlString.chop(1);
|
||||
QUrl newUrl(urlString);
|
||||
|
||||
if (newUrl == QUrl(QLatin1String("about:blank")) ) {
|
||||
newUrl = QUrl("");
|
||||
}
|
||||
|
||||
if (this->url != newUrl) {
|
||||
|
||||
this->url = newUrl;
|
||||
emit q->urlChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void AmneziaWebViewPrivate::addToJavaScriptWindowObject(const QString& name, QObject* object)
|
||||
{
|
||||
jsHandler.addToJavaScriptWindowObject(name, object);
|
||||
}
|
||||
|
||||
|
||||
bool AmneziaWebViewPrivate::canHandleUrl(const QUrl &url) const
|
||||
{
|
||||
bool can = (jsHandler.canHandleUrl(url) || qrcHandler.canHandleUrl(url) || fileHandler.canHandleUrl(url));
|
||||
return can;
|
||||
}
|
||||
|
||||
QString AmneziaWebViewPrivate::toHtml() const
|
||||
{
|
||||
return QString();
|
||||
}
|
||||
|
||||
void AmneziaWebViewPrivate::load(const QNetworkRequest& request, QNetworkAccessManager::Operation operation, const QByteArray& body)
|
||||
{
|
||||
Q_UNUSED(request)
|
||||
Q_UNUSED(operation)
|
||||
Q_UNUSED(body)
|
||||
}
|
||||
|
||||
QNetworkAccessManager* AmneziaWebViewPrivate::networkAccessManager()
|
||||
{
|
||||
Q_Q(AmneziaWebView);
|
||||
|
||||
if (!networkManager)
|
||||
networkManager = new NetworkAccessManager(q);
|
||||
return networkManager;
|
||||
}
|
||||
|
||||
void AmneziaWebViewPrivate::setNetworkAccessManager(QNetworkAccessManager* manager)
|
||||
{
|
||||
Q_Q(AmneziaWebView);
|
||||
if (manager == networkManager)
|
||||
return;
|
||||
if (networkManager && networkManager->parent() == q)
|
||||
delete networkManager;
|
||||
|
||||
NetworkAccessManager *newManager = qobject_cast<NetworkAccessManager *>(manager);
|
||||
if (!newManager && manager) {
|
||||
newManager = new NetworkAccessManager(manager, q);
|
||||
}
|
||||
networkManager = newManager;
|
||||
}
|
||||
|
||||
QAction *AmneziaWebViewPrivate::action(AmneziaWebView::WebAction action) const
|
||||
{
|
||||
QAction *ret = qobject_cast<QAction*>(actions.mapping((int)action));
|
||||
if (ret) {
|
||||
switch (action) {
|
||||
case AmneziaWebView::Back: {
|
||||
bool can = canGoBack() || history()->canGoBack();
|
||||
ret->setEnabled(can);
|
||||
}
|
||||
break;
|
||||
case AmneziaWebView::Forward: {
|
||||
bool can = canGoForward() || history()->canGoForward();
|
||||
ret->setEnabled(can);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
void AmneziaWebViewPrivate::onAction(int action)
|
||||
{
|
||||
switch (action) {
|
||||
case AmneziaWebView::Back: {
|
||||
if (canGoBack()) {
|
||||
back();
|
||||
}
|
||||
//else {
|
||||
// history()->back();
|
||||
//}
|
||||
}
|
||||
break;
|
||||
case AmneziaWebView::Forward:
|
||||
if (canGoForward()) forward();
|
||||
break;
|
||||
case AmneziaWebView::Stop:
|
||||
stop();
|
||||
break;
|
||||
case AmneziaWebView::Reload:
|
||||
reload();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
AmneziaWebHistory* AmneziaWebViewPrivate::history() const
|
||||
{
|
||||
return m_history.data();
|
||||
}
|
||||
187
client/core/webview/amneziawebview_p.h
Normal file
187
client/core/webview/amneziawebview_p.h
Normal file
@@ -0,0 +1,187 @@
|
||||
#ifndef WEBVIEW_P_H
|
||||
#define WEBVIEW_P_H
|
||||
|
||||
#include "amneziawebview.h"
|
||||
#include "qrchandler.h"
|
||||
#include "jshandler.h"
|
||||
#include "filehandler.h"
|
||||
|
||||
#include "amneziawebhistory.h"
|
||||
|
||||
class WebSettings;
|
||||
class QIcon;
|
||||
class QSize;
|
||||
|
||||
|
||||
class WebViewSettings;
|
||||
class WebSettings;
|
||||
|
||||
class AmneziaWebViewPrivate : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DECLARE_PUBLIC(AmneziaWebView)
|
||||
|
||||
public:
|
||||
explicit AmneziaWebViewPrivate(AmneziaWebView *q);
|
||||
virtual ~AmneziaWebViewPrivate();
|
||||
static AmneziaWebViewPrivate *create(AmneziaWebView *q);
|
||||
void init();
|
||||
|
||||
virtual void setWindowParent(QWindow *parent) = 0;
|
||||
virtual void setBackgroundColor(const QColor backgroundColor) = 0;
|
||||
|
||||
virtual void setGeometry(const QRect &) = 0;
|
||||
virtual QString innerHTML() const = 0;
|
||||
virtual void load(const QUrl& url) = 0;
|
||||
virtual void setHtml(const QString& html, const QUrl& baseUrl = QUrl()) = 0;
|
||||
virtual void setContent(const QByteArray& data, const QString& mimeType, const QUrl& baseUrl) = 0;
|
||||
virtual void evaluateJavaScript(const QString& scriptSource) = 0;
|
||||
virtual bool isLoading() const = 0;
|
||||
virtual bool canGoBack() const = 0;
|
||||
virtual bool canGoForward() const = 0;
|
||||
virtual void back() = 0;
|
||||
virtual void forward() = 0;
|
||||
virtual void reload() = 0;
|
||||
virtual void stop() = 0;
|
||||
virtual QIcon icon() const = 0;
|
||||
virtual void setScale(qreal scale) = 0;
|
||||
virtual qreal scale() const = 0;
|
||||
virtual QSize contentsSize() const = 0;
|
||||
virtual void show() = 0;
|
||||
virtual void hide() = 0;
|
||||
virtual void setDefaultFontSize(int size) = 0;
|
||||
virtual void setStandardFontFamily(const QString &family) = 0;
|
||||
virtual void setTextZoom(int percent) = 0;
|
||||
|
||||
virtual void setUrl(const QUrl &url);
|
||||
|
||||
static AmneziaWebViewPrivate *get(AmneziaWebView *q);
|
||||
|
||||
enum { PendingNone, PendingUrl, PendingHtml, PendingContent } pending;
|
||||
|
||||
QString toHtml() const;
|
||||
AmneziaWebHistory* history() const;
|
||||
|
||||
QByteArray dataForUrl(const QUrl &url) const;
|
||||
QString mimeTypeForUrl(const QUrl &url) const;
|
||||
bool canHandleUrl(const QUrl &url) const;
|
||||
|
||||
static void windowObjectsClear(QQmlListProperty<QObject> *prop);
|
||||
static QObject *windowObjectsAt(QQmlListProperty<QObject> *prop, qsizetype index);
|
||||
static qsizetype windowObjectsCount(QQmlListProperty<QObject> *prop);
|
||||
static void windowObjectsAppend(QQmlListProperty<QObject> *prop, QObject *o);
|
||||
|
||||
void updateWindowObjects();
|
||||
QObjectList windowObjects;
|
||||
|
||||
QNetworkAccessManager* networkAccessManager();
|
||||
void setNetworkAccessManager(QNetworkAccessManager* manager);
|
||||
void load(const QNetworkRequest& request, QNetworkAccessManager::Operation operation, const QByteArray& body);
|
||||
QAction *action(AmneziaWebView::WebAction) const;
|
||||
|
||||
public Q_SLOTS:
|
||||
void setTitle(const QString &title);
|
||||
void move(const QPoint &);
|
||||
void requestHide();
|
||||
void requestShow();
|
||||
|
||||
Q_SIGNALS:
|
||||
void loadStarted();
|
||||
void loadFinished(bool ok);
|
||||
void loadProgress(int progress);
|
||||
void titleChanged(const QString& title);
|
||||
void urlChanged(const QUrl& url);
|
||||
void backgroundColorChanged();
|
||||
public:
|
||||
|
||||
QUrl url;
|
||||
AmneziaWebView::Status status;
|
||||
qreal preferredwidth, preferredheight;
|
||||
qreal progress;
|
||||
QString statusText;
|
||||
QUrl pendingUrl;
|
||||
QString pendingString;
|
||||
QByteArray pendingData;
|
||||
|
||||
QQmlComponent* newWindowComponent;
|
||||
QQuickItem* newWindowParent;
|
||||
|
||||
QrcHandler qrcHandler;
|
||||
JsHandler jsHandler;
|
||||
FileHandler fileHandler;
|
||||
|
||||
bool rendering;
|
||||
bool overlapped;
|
||||
|
||||
QColor backgroundColor;
|
||||
QUrl baseUrl;
|
||||
QString title;
|
||||
QRect geometry;
|
||||
QString lastError;
|
||||
mutable bool visible;
|
||||
|
||||
protected Q_SLOTS:
|
||||
void applicationStateChanged(Qt::ApplicationState state);
|
||||
void onPageStarted();
|
||||
void onPageFinished();
|
||||
void onPageError();
|
||||
void onUrlChanged(const QUrl &url);
|
||||
void onAction(int);
|
||||
|
||||
protected:
|
||||
|
||||
void addToJavaScriptWindowObject(const QString& name, QObject* object);
|
||||
|
||||
QMutex renderMutex;
|
||||
QNetworkAccessManager *networkManager;
|
||||
AmneziaWebView *q_ptr;
|
||||
|
||||
private:
|
||||
QSignalMapper actions;
|
||||
QScopedPointer<AmneziaWebHistory> m_history;
|
||||
QScopedPointer<AmneziaWebViewSettings> m_settings;
|
||||
};
|
||||
|
||||
//!internal
|
||||
class NetworkAccessManager : public QNetworkAccessManager
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit NetworkAccessManager(QNetworkAccessManager *manager, QObject *parent);
|
||||
explicit NetworkAccessManager(QObject *parent): QNetworkAccessManager(parent) { init(); }
|
||||
|
||||
public Q_SLOTS:
|
||||
void handleSslErrors(QNetworkReply* reply, const QList<QSslError> &errors);
|
||||
|
||||
protected:
|
||||
virtual QNetworkReply *createRequest(Operation op, const QNetworkRequest &request, QIODevice *outgoingData = nullptr);
|
||||
private:
|
||||
void init();
|
||||
QNetworkAccessManager *manager;
|
||||
};
|
||||
|
||||
class DataReply : public QNetworkReply
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DataReply(QObject *parent, const QNetworkAccessManager::Operation operation, const QNetworkRequest &request, AmneziaWebView *view = nullptr);
|
||||
virtual ~DataReply() = default;
|
||||
|
||||
virtual void abort();
|
||||
virtual void close();
|
||||
virtual qint64 size() const;
|
||||
|
||||
virtual qint64 bytesAvailable() const;
|
||||
virtual bool isSequential() const;
|
||||
protected:
|
||||
virtual qint64 readData(char *data, qint64 maxSize);
|
||||
Q_INVOKABLE void setContent();
|
||||
|
||||
private:
|
||||
QByteArray content;
|
||||
qint64 offset;
|
||||
QPointer<AmneziaWebView> view;
|
||||
};
|
||||
|
||||
#endif
|
||||
494
client/core/webview/amneziawebview_webengine.cpp
Normal file
494
client/core/webview/amneziawebview_webengine.cpp
Normal file
@@ -0,0 +1,494 @@
|
||||
#include <QCoreApplication>
|
||||
#include <QWebEngineUrlScheme>
|
||||
|
||||
#include "amneziawebview_webengine_p.h"
|
||||
#include "qrchandler.h"
|
||||
#include "filehandler.h"
|
||||
#include "amneziawebhistory.h"
|
||||
|
||||
typedef QMap<quintptr, AmneziaWebViewPrivate *> WebViews;
|
||||
Q_GLOBAL_STATIC_WITH_ARGS(WebViews, g_webViews, ())
|
||||
|
||||
QrcHandler::QrcHandler()
|
||||
{}
|
||||
|
||||
FileHandler::FileHandler()
|
||||
{
|
||||
}
|
||||
|
||||
JsHandler::JsHandler(AmneziaWebView *host): _host(host), scriptObjectsInjected(false)
|
||||
{
|
||||
init();
|
||||
}
|
||||
|
||||
JsHandler::~JsHandler() {}
|
||||
|
||||
WebPage::WebPage(QObject *parent)
|
||||
: QWebEnginePage(parent)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
WebPage::WebPage(QWebEngineProfile *profile, QObject *parent)
|
||||
: QWebEnginePage(profile, parent)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
WebPage::~WebPage()
|
||||
{
|
||||
disconnect(this);
|
||||
}
|
||||
|
||||
bool WebPage::acceptNavigationRequest(const QUrl &url, QWebEnginePage::NavigationType type, bool isMainFrame)
|
||||
{
|
||||
Q_UNUSED(type);
|
||||
// Always accept navigation requests to open links within WebView
|
||||
// This prevents opening links in external browser
|
||||
if (isMainFrame) {
|
||||
m_loadingUrl = url;
|
||||
emit loadingUrl(url);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
QWebEnginePage *WebPage::createWindow(QWebEnginePage::WebWindowType type)
|
||||
{
|
||||
Q_UNUSED(type);
|
||||
// Return this page instead of creating a new window
|
||||
// This prevents opening new browser windows/tabs
|
||||
return this;
|
||||
}
|
||||
|
||||
void WebPage::handleUnsupportedContent(QNetworkReply *reply)
|
||||
{
|
||||
QString errorString = reply->errorString();
|
||||
|
||||
if (m_loadingUrl != reply->url()) {
|
||||
// sub resource of this page
|
||||
qWarning() << "Resource" << reply->url().toEncoded() << "has unknown Content-Type, will be ignored.";
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError && !reply->header(QNetworkRequest::ContentTypeHeader).isValid()) {
|
||||
errorString = "Unknown Content-Type";
|
||||
}
|
||||
|
||||
QFile file(QLatin1String(":/notfound.html"));
|
||||
bool isOpened = file.open(QIODevice::ReadOnly);
|
||||
Q_ASSERT(isOpened);
|
||||
Q_UNUSED(isOpened)
|
||||
|
||||
QString title = QCoreApplication::translate("webview", "Error loading page: %1").arg(reply->url().toString());
|
||||
QString html = QString(QLatin1String(file.readAll()))
|
||||
.arg(title)
|
||||
.arg(errorString)
|
||||
.arg(reply->url().toString());
|
||||
|
||||
QBuffer imageBuffer;
|
||||
imageBuffer.open(QBuffer::ReadWrite);
|
||||
QIcon icon = qApp->style()->standardIcon(QStyle::SP_MessageBoxWarning, 0);
|
||||
QPixmap pixmap = icon.pixmap(QSize(32,32));
|
||||
if (pixmap.save(&imageBuffer, "PNG")) {
|
||||
html.replace(QLatin1String("IMAGE_BINARY_DATA_HERE"),
|
||||
QString(QLatin1String(imageBuffer.buffer().toBase64())));
|
||||
}
|
||||
|
||||
if (m_loadingUrl == reply->url()) {
|
||||
setHtml(html, reply->url());
|
||||
}
|
||||
}
|
||||
|
||||
const QString &LocalSchemeHandler::scheme()
|
||||
{
|
||||
static const QString localScheme("local");
|
||||
return localScheme;
|
||||
}
|
||||
|
||||
const QMimeDatabase &LocalSchemeHandler::mimeDatabase()
|
||||
{
|
||||
static const QMimeDatabase mimeDatabase;
|
||||
return mimeDatabase;
|
||||
}
|
||||
|
||||
void LocalSchemeHandler::requestStarted(QWebEngineUrlRequestJob *job)
|
||||
{
|
||||
const QByteArray requestMethod = job->requestMethod();
|
||||
const QUrl requestUrl = job->requestUrl();
|
||||
QString requestScheme = requestUrl.scheme();
|
||||
DesktopWebViewPrivate *d = qobject_cast<DesktopWebViewPrivate *>(parent());
|
||||
|
||||
if (!d) {
|
||||
job->fail(QWebEngineUrlRequestJob::RequestFailed);
|
||||
return;
|
||||
}
|
||||
|
||||
if (requestScheme != LocalSchemeHandler::scheme()) {
|
||||
job->fail(QWebEngineUrlRequestJob::UrlInvalid);
|
||||
return;
|
||||
}
|
||||
|
||||
QBuffer *buffer = nullptr;
|
||||
QMimeType mimeType = mimeDatabase().mimeTypeForFile(requestUrl.fileName(), QMimeDatabase::MatchExtension);
|
||||
|
||||
if (d->fileHandler.canHandleUrl(requestUrl)) {
|
||||
const QByteArray content = d->fileHandler.dataForUrl(requestUrl);
|
||||
if (!content.isNull()) {
|
||||
buffer = new QBuffer();
|
||||
buffer->setData(content);
|
||||
}
|
||||
}
|
||||
if (!buffer) {
|
||||
|
||||
auto historyItems = d->history()->items();
|
||||
const auto it = std::find_if(historyItems.begin(), historyItems.end(),
|
||||
[requestUrl](const auto &item) {
|
||||
bool urlsMatch = item.url().matches(requestUrl, QUrl::RemoveQuery | QUrl::RemoveFragment);
|
||||
bool hasData = (item.data().length() > 0);
|
||||
return urlsMatch && hasData; });
|
||||
|
||||
if (it != historyItems.end()) {
|
||||
buffer = new QBuffer();
|
||||
buffer->setData(it->data());
|
||||
mimeType = mimeDatabase().mimeTypeForName(it->mimeType());
|
||||
}
|
||||
}
|
||||
|
||||
if (!buffer) {
|
||||
job->fail(QWebEngineUrlRequestJob::UrlNotFound);
|
||||
return;
|
||||
}
|
||||
|
||||
connect(job, &QObject::destroyed, buffer, &QObject::deleteLater);
|
||||
job->reply(mimeType.name().toLocal8Bit(), buffer);
|
||||
}
|
||||
|
||||
DesktopWebViewPrivate::DesktopWebViewPrivate(AmneziaWebView* q): AmneziaWebViewPrivate(q)
|
||||
, viewId(reinterpret_cast<quintptr>(this))
|
||||
, containerWindow(0)
|
||||
, window(0)
|
||||
{
|
||||
m_localHandler = new LocalSchemeHandler(this);
|
||||
|
||||
container = new QWidget(0, Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint | Qt::Tool);
|
||||
container->setAttribute(Qt::WA_NativeWindow, true);
|
||||
container->setAttribute(Qt::WA_DontCreateNativeAncestors, true);
|
||||
|
||||
QWebEngineUrlScheme localScheme(LocalSchemeHandler::scheme().toUtf8());
|
||||
localScheme.setFlags(QWebEngineUrlScheme::LocalAccessAllowed |
|
||||
QWebEngineUrlScheme::SecureScheme |
|
||||
QWebEngineUrlScheme::ViewSourceAllowed |
|
||||
QWebEngineUrlScheme::ContentSecurityPolicyIgnored |
|
||||
QWebEngineUrlScheme::CorsEnabled |
|
||||
QWebEngineUrlScheme::FetchApiAllowed);
|
||||
QWebEngineUrlScheme::registerScheme(localScheme);
|
||||
|
||||
QWebEngineUrlScheme qrcScheme("qrc");
|
||||
qrcScheme.setFlags(QWebEngineUrlScheme::LocalScheme |
|
||||
QWebEngineUrlScheme::LocalAccessAllowed |
|
||||
QWebEngineUrlScheme::SecureScheme |
|
||||
QWebEngineUrlScheme::ContentSecurityPolicyIgnored |
|
||||
QWebEngineUrlScheme::CorsEnabled |
|
||||
QWebEngineUrlScheme::FetchApiAllowed);
|
||||
QWebEngineUrlScheme::registerScheme(qrcScheme);
|
||||
|
||||
view = new QWebEngineView(container);
|
||||
QWebEngineProfile *profile = new QWebEngineProfile(view);
|
||||
profile->installUrlSchemeHandler(LocalSchemeHandler::scheme().toUtf8(), m_localHandler);
|
||||
|
||||
m_page = new WebPage(profile, profile);
|
||||
m_page->settings()->setAttribute(QWebEngineSettings::PluginsEnabled, true);
|
||||
connect(m_page, &QWebEnginePage::fileSystemAccessRequested, this, [](QWebEngineFileSystemAccessRequest request) {
|
||||
request.accept();
|
||||
});
|
||||
|
||||
view->settings()->setUnknownUrlSchemePolicy(QWebEngineSettings::AllowAllUnknownUrlSchemes);
|
||||
view->settings()->setAttribute(QWebEngineSettings::LocalContentCanAccessRemoteUrls, true);
|
||||
view->settings()->setAttribute(QWebEngineSettings::LocalContentCanAccessFileUrls, true);
|
||||
view->settings()->setAttribute(QWebEngineSettings::AllowRunningInsecureContent, true);
|
||||
view->settings()->setAttribute(QWebEngineSettings::AllowGeolocationOnInsecureOrigins, true);
|
||||
|
||||
view->setPage(m_page);
|
||||
container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||
container->setLayout(new QHBoxLayout(container));
|
||||
container->layout()->setSpacing(0);
|
||||
container->layout()->setContentsMargins(0, 0, 0, 0);
|
||||
container->layout()->addWidget(view);
|
||||
|
||||
setBackgroundColor(backgroundColor);
|
||||
g_webViews->insert(viewId, this);
|
||||
|
||||
connect(view, SIGNAL(loadFinished(bool)), this, SIGNAL(loadFinished(bool)));
|
||||
connect(m_page, SIGNAL(loadFinished(bool)), this, SLOT(onLoadFinished(bool)), Qt::QueuedConnection);
|
||||
connect(m_page, SIGNAL(loadStarted()), this, SLOT(onPageStarted()), Qt::QueuedConnection);
|
||||
connect(view, SIGNAL(urlChanged(const QUrl &)), this, SLOT(onUrlChanged(const QUrl &)), Qt::QueuedConnection);
|
||||
container->createWinId();
|
||||
}
|
||||
|
||||
DesktopWebViewPrivate::~DesktopWebViewPrivate()
|
||||
{
|
||||
g_webViews->take(viewId);
|
||||
disconnect(this, SLOT(loadFinished(bool)));
|
||||
disconnect(this, SLOT(applicationStateChanged(Qt::ApplicationState)));
|
||||
disconnect(this, SLOT(onUrlChanged(const QUrl &)));
|
||||
disconnect(this, SLOT(onPageStarted()));
|
||||
disconnect(this, SLOT(onLoadFinished(bool)));
|
||||
|
||||
delete container;
|
||||
}
|
||||
|
||||
AmneziaWebViewPrivate *AmneziaWebViewPrivate::create(AmneziaWebView *q)
|
||||
{
|
||||
return new DesktopWebViewPrivate(q);
|
||||
}
|
||||
|
||||
void DesktopWebViewPrivate::setWindowParent(QWindow *parent)
|
||||
{
|
||||
if (window) {
|
||||
window->removeEventFilter(this);
|
||||
}
|
||||
|
||||
if (parent) {
|
||||
|
||||
containerWindow = qobject_cast<QWindow*>(container->windowHandle());
|
||||
containerWindow->setTransientParent(parent);
|
||||
parent->installEventFilter(this);
|
||||
|
||||
}
|
||||
window = parent;
|
||||
}
|
||||
|
||||
void DesktopWebViewPrivate::setBackgroundColor(const QColor backgroundColor)
|
||||
{
|
||||
this->backgroundColor = backgroundColor;
|
||||
QPalette p = container->palette();
|
||||
p.setColor(QPalette::Window, backgroundColor);
|
||||
container->setPalette(p);
|
||||
p = view->palette();
|
||||
p.setColor(QPalette::Window, backgroundColor);
|
||||
view->setPalette(p);
|
||||
emit backgroundColorChanged();
|
||||
}
|
||||
|
||||
/// Deprecated
|
||||
void DesktopWebViewPrivate::setScale(qreal scale)
|
||||
{
|
||||
Q_UNUSED(scale);
|
||||
}
|
||||
|
||||
/// Deprecated
|
||||
qreal DesktopWebViewPrivate::scale() const
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
QIcon DesktopWebViewPrivate::icon() const
|
||||
{
|
||||
return QIcon();
|
||||
}
|
||||
|
||||
QSize DesktopWebViewPrivate::contentsSize() const
|
||||
{
|
||||
return QSize();
|
||||
}
|
||||
|
||||
void DesktopWebViewPrivate::setGeometry(const QRect &geometry)
|
||||
{
|
||||
Q_Q(AmneziaWebView);
|
||||
QQuickWindow *window = q->window();
|
||||
if (!window) return;
|
||||
QRect newGeometry = QRect(window->mapToGlobal(geometry.topLeft()), QSize(geometry.width(), geometry.height()));
|
||||
if (newGeometry.isValid() && container->geometry() != newGeometry ) {
|
||||
|
||||
this->geometry = geometry;
|
||||
container->setGeometry(newGeometry);
|
||||
container->updateGeometry();
|
||||
}
|
||||
}
|
||||
|
||||
bool DesktopWebViewPrivate::eventFilter(QObject *obj, QEvent *event)
|
||||
{
|
||||
Q_UNUSED(obj);
|
||||
Q_Q(AmneziaWebView);
|
||||
|
||||
switch (event->type()) {
|
||||
case QEvent::Move: {
|
||||
QMoveEvent *moveEvent = static_cast<QMoveEvent*>(event);
|
||||
QPoint p = q->mapToScene(QPointF(moveEvent->pos())).toPoint();
|
||||
container->move(p);
|
||||
|
||||
if (visible) {
|
||||
show();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case QEvent::Resize: {
|
||||
if (visible) {
|
||||
show();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case QEvent::WindowStateChange: {
|
||||
|
||||
Qt::WindowState state = window->windowState();
|
||||
if ((state == Qt::WindowMaximized) || (state == Qt::WindowFullScreen) || (state == Qt::WindowActive)) {
|
||||
show();
|
||||
}
|
||||
else {
|
||||
hide();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void DesktopWebViewPrivate::hide()
|
||||
{
|
||||
Q_Q(AmneziaWebView);
|
||||
if (!q->window()) return;
|
||||
|
||||
if (visible) {
|
||||
QMetaObject::invokeMethod(container, "hide", Qt::QueuedConnection);
|
||||
visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
void DesktopWebViewPrivate::show()
|
||||
{
|
||||
Q_Q(AmneziaWebView);
|
||||
if (!q->window()) return;
|
||||
if (!visible) {
|
||||
|
||||
QMetaObject::invokeMethod(container, "show", Qt::QueuedConnection);
|
||||
QMetaObject::invokeMethod(container, "update", Qt::QueuedConnection);
|
||||
visible = true;
|
||||
}
|
||||
|
||||
containerWindow = qobject_cast<QWindow*>(container->windowHandle());
|
||||
|
||||
if ((containerWindow != nullptr) &&
|
||||
((qApp->topLevelWindows().at(0) != containerWindow) || !containerWindow->isVisible())) {
|
||||
|
||||
containerWindow->raise();
|
||||
}
|
||||
}
|
||||
|
||||
void DesktopWebViewPrivate::load(const QUrl& baseUrl)
|
||||
{
|
||||
QUrl url = baseUrl;
|
||||
if (!url.isValid()) {
|
||||
url = QUrl(QLatin1String("about:blank"));
|
||||
}
|
||||
view->load(url);
|
||||
history()->append(baseUrl);
|
||||
}
|
||||
|
||||
void DesktopWebViewPrivate::setContent(const QByteArray& data, const QString& mimeType, const QUrl& baseUrl)
|
||||
{
|
||||
QUrl url = baseUrl;
|
||||
if (!url.isValid()) {
|
||||
url = QUrl(QLatin1String("about:blank"));
|
||||
}
|
||||
view->setContent(data, mimeType, url);
|
||||
history()->append(url, data, mimeType);
|
||||
}
|
||||
|
||||
void DesktopWebViewPrivate::setHtml(const QString& html, const QUrl& baseUrl)
|
||||
{
|
||||
if(html.isNull()) return;
|
||||
QUrl url = baseUrl;
|
||||
if (!baseUrl.isValid())
|
||||
url = QUrl(QLatin1String("about:blank"));
|
||||
|
||||
view->setHtml(html, url);
|
||||
history()->append(url, html.toUtf8(), "text/html");
|
||||
}
|
||||
|
||||
void DesktopWebViewPrivate::evaluateJavaScript(const QString& scriptSource)
|
||||
{
|
||||
view->page()->runJavaScript(scriptSource);
|
||||
}
|
||||
|
||||
bool DesktopWebViewPrivate::canGoBack() const
|
||||
{
|
||||
QAction *pageAction = view->pageAction(QWebEnginePage::Back);
|
||||
bool can = (pageAction && pageAction->isEnabled());
|
||||
return can;
|
||||
}
|
||||
|
||||
bool DesktopWebViewPrivate::canGoForward() const
|
||||
{
|
||||
QAction *pageAction = view->pageAction(QWebEnginePage::Forward);
|
||||
bool can = (pageAction && pageAction->isEnabled());
|
||||
return can;
|
||||
}
|
||||
|
||||
void DesktopWebViewPrivate::back()
|
||||
{
|
||||
QAction *pageAction = view->pageAction(QWebEnginePage::Back);
|
||||
if (pageAction)
|
||||
emit pageAction->trigger();
|
||||
}
|
||||
|
||||
void DesktopWebViewPrivate::forward()
|
||||
{
|
||||
QAction *pageAction = view->pageAction(QWebEnginePage::Forward);
|
||||
if (pageAction)
|
||||
emit pageAction->trigger();
|
||||
}
|
||||
|
||||
void DesktopWebViewPrivate::reload()
|
||||
{
|
||||
QAction *pageAction = view->pageAction(QWebEnginePage::Reload);
|
||||
if (pageAction)
|
||||
emit pageAction->trigger();
|
||||
}
|
||||
|
||||
void DesktopWebViewPrivate::stop()
|
||||
{
|
||||
QAction *pageAction = view->pageAction(QWebEnginePage::Stop);
|
||||
if (pageAction)
|
||||
emit pageAction->trigger();
|
||||
}
|
||||
|
||||
bool DesktopWebViewPrivate::isLoading() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
QString DesktopWebViewPrivate::innerHTML() const
|
||||
{
|
||||
return QString();
|
||||
}
|
||||
|
||||
void DesktopWebViewPrivate::onLoadFinished(bool success)
|
||||
{
|
||||
if (success) {
|
||||
QMetaObject::invokeMethod(this, "onPageFinished", Qt::QueuedConnection);
|
||||
}
|
||||
else {
|
||||
QMetaObject::invokeMethod(this, "onPageError", Qt::QueuedConnection);
|
||||
}
|
||||
}
|
||||
|
||||
void DesktopWebViewPrivate::setDefaultFontSize(int size)
|
||||
{
|
||||
view->settings()->setFontSize(QWebEngineSettings::DefaultFontSize, size);
|
||||
}
|
||||
|
||||
void DesktopWebViewPrivate::setStandardFontFamily(const QString &family)
|
||||
{
|
||||
view->settings()->setFontFamily(QWebEngineSettings::StandardFont, family);
|
||||
}
|
||||
|
||||
void DesktopWebViewPrivate::setTextZoom(int percent)
|
||||
{
|
||||
Q_UNUSED(percent);
|
||||
}
|
||||
109
client/core/webview/amneziawebview_webengine_p.h
Normal file
109
client/core/webview/amneziawebview_webengine_p.h
Normal file
@@ -0,0 +1,109 @@
|
||||
#ifndef AMNEZIAWEBVIEW_WEBENGINE_P_H
|
||||
#define AMNEZIAWEBVIEW_WEBENGINE_P_H
|
||||
|
||||
#include <QtWebEngineWidgets/QtWebEngineWidgets>
|
||||
|
||||
#include <QtWebEngineWidgets>
|
||||
#include <QWebEngineView>
|
||||
#include <QWebEnginePage>
|
||||
#include <QWebEngineUrlSchemeHandler>
|
||||
#include <QWebEngineUrlRequestJob>
|
||||
|
||||
#include "amneziawebview.h"
|
||||
#include "amneziawebview_p.h"
|
||||
|
||||
class DesktopWebViewPrivate;
|
||||
class LocalSchemeHandler;
|
||||
|
||||
class WebPage : public QWebEnginePage
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
signals:
|
||||
void loadingUrl(const QUrl &url);
|
||||
|
||||
public:
|
||||
explicit WebPage(QObject *parent = 0);
|
||||
explicit WebPage(QWebEngineProfile *profile, QObject *parent = 0);
|
||||
virtual ~WebPage();
|
||||
|
||||
protected:
|
||||
bool acceptNavigationRequest(const QUrl &url, QWebEnginePage::NavigationType type, bool isMainFrame) override;
|
||||
QWebEnginePage *createWindow(QWebEnginePage::WebWindowType type) override;
|
||||
|
||||
private slots:
|
||||
void handleUnsupportedContent(QNetworkReply *reply);
|
||||
|
||||
private:
|
||||
|
||||
friend class DesktopWebViewPrivate;
|
||||
QUrl m_loadingUrl;
|
||||
};
|
||||
|
||||
class DesktopWebViewPrivate : public AmneziaWebViewPrivate
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DECLARE_PUBLIC(AmneziaWebView)
|
||||
public:
|
||||
|
||||
explicit DesktopWebViewPrivate(AmneziaWebView* q);
|
||||
virtual ~DesktopWebViewPrivate();
|
||||
|
||||
virtual void setWindowParent(QWindow *parent);
|
||||
|
||||
virtual void setBackgroundColor(const QColor backgroundColor);
|
||||
virtual void show();
|
||||
virtual void hide();
|
||||
virtual void setGeometry(const QRect &);
|
||||
virtual QString innerHTML() const;
|
||||
virtual void load(const QUrl& url);
|
||||
virtual void setHtml(const QString& html, const QUrl& baseUrl = QUrl());
|
||||
virtual void setContent(const QByteArray& data, const QString& mimeType, const QUrl& baseUrl);
|
||||
virtual void evaluateJavaScript(const QString& scriptSource);
|
||||
virtual bool isLoading() const;
|
||||
virtual bool canGoBack() const;
|
||||
virtual bool canGoForward() const;
|
||||
virtual void back();
|
||||
virtual void forward();
|
||||
virtual void reload();
|
||||
virtual void stop();
|
||||
|
||||
virtual QIcon icon() const;
|
||||
virtual void setScale(qreal scale);
|
||||
virtual qreal scale() const;
|
||||
virtual QSize contentsSize() const;
|
||||
virtual void setDefaultFontSize(int size);
|
||||
virtual void setTextZoom(int percent);
|
||||
virtual void setStandardFontFamily(const QString &family);
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject *obj, QEvent *event);
|
||||
|
||||
private slots:
|
||||
void onLoadFinished(bool);
|
||||
|
||||
private:
|
||||
quintptr viewId;
|
||||
QWebEngineView *view;
|
||||
QWidget *container;
|
||||
QWindow *containerWindow;
|
||||
QWindow *window;
|
||||
WebPage *m_page;
|
||||
LocalSchemeHandler *m_localHandler;
|
||||
};
|
||||
|
||||
class LocalSchemeHandler : public QWebEngineUrlSchemeHandler
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
LocalSchemeHandler(QObject *parent) : QWebEngineUrlSchemeHandler(parent)
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
}
|
||||
|
||||
static const QString &scheme();
|
||||
static const QMimeDatabase &mimeDatabase();
|
||||
void requestStarted(QWebEngineUrlRequestJob *job) override;
|
||||
};
|
||||
|
||||
#endif // AMNEZIAWEBVIEW_WEBENGINE_P_H
|
||||
43
client/core/webview/filehandler.cpp
Normal file
43
client/core/webview/filehandler.cpp
Normal file
@@ -0,0 +1,43 @@
|
||||
#include <QtCore/QString>
|
||||
#include <QtCore/QUrl>
|
||||
#include <QtCore/QFile>
|
||||
#include <QtCore/QByteArray>
|
||||
|
||||
#include "filehandler.h"
|
||||
#include "mimecache.h"
|
||||
|
||||
QList<QString> FileHandler::schemes()
|
||||
{
|
||||
static QList<QString> list = QList<QString>() << "file" << "local";
|
||||
return list;
|
||||
}
|
||||
|
||||
QByteArray FileHandler::dataForUrl(const QUrl &url) const
|
||||
{
|
||||
QUrl fileUrl = url;
|
||||
if (fileUrl.scheme() != "file") {
|
||||
fileUrl.setScheme("file");
|
||||
}
|
||||
QString requestUrl(fileUrl.toLocalFile());
|
||||
QFile resource(requestUrl);
|
||||
QByteArray buffer;
|
||||
if (resource.exists() && resource.open(QIODevice::ReadOnly)) {
|
||||
|
||||
buffer = resource.readAll();
|
||||
resource.close();
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
bool FileHandler::canHandleUrl(const QUrl &url) const
|
||||
{
|
||||
if (schemes().contains(url.scheme().toLower()))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
QString FileHandler::mimeTypeForUrl(const QUrl &url) const
|
||||
{
|
||||
return mimeTypeForExtension(url.path().section('.', -1));
|
||||
}
|
||||
18
client/core/webview/filehandler.h
Normal file
18
client/core/webview/filehandler.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#ifndef FILEHANDLER_H
|
||||
#define FILEHANDLER_H
|
||||
|
||||
class QString;
|
||||
class QByteArray;
|
||||
class QUrl;
|
||||
|
||||
class FileHandler
|
||||
{
|
||||
public:
|
||||
explicit FileHandler();
|
||||
static QList<QString> schemes();
|
||||
bool canHandleUrl(const QUrl &url) const;
|
||||
QByteArray dataForUrl(const QUrl &url) const;
|
||||
QString mimeTypeForUrl(const QUrl &url) const;
|
||||
};
|
||||
|
||||
#endif
|
||||
10
client/core/webview/filehandler_android.cpp
Normal file
10
client/core/webview/filehandler_android.cpp
Normal file
@@ -0,0 +1,10 @@
|
||||
#include <QtCore/QString>
|
||||
#include <QtCore/QUrl>
|
||||
#include <QtCore/QFile>
|
||||
#include <QtCore/QByteArray>
|
||||
|
||||
#include "filehandler.h"
|
||||
|
||||
FileHandler::FileHandler()
|
||||
{
|
||||
}
|
||||
135
client/core/webview/filehandler_ios.mm
Normal file
135
client/core/webview/filehandler_ios.mm
Normal file
@@ -0,0 +1,135 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <MobileCoreServices/UTCoreTypes.h>
|
||||
#import <MobileCoreServices/UTType.h>
|
||||
|
||||
#include <QtCore/QFile>
|
||||
#include <QtCore/QString>
|
||||
#include <QtCore/QDebug>
|
||||
#include "mimecache.h"
|
||||
#include "filehandler.h"
|
||||
#define kProtocolFileScheme @"file"
|
||||
|
||||
#if !defined(ENABLE_WKWEBVIEW)
|
||||
|
||||
@interface FileProtocol : NSURLProtocol
|
||||
@end
|
||||
|
||||
#endif
|
||||
|
||||
FileHandler::FileHandler()
|
||||
{
|
||||
#if !defined(ENABLE_WKWEBVIEW)
|
||||
static bool protocolRegistered = false;
|
||||
if (!protocolRegistered) {
|
||||
[NSURLProtocol registerClass:[FileProtocol class]];
|
||||
protocolRegistered = true;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !defined(ENABLE_WKWEBVIEW)
|
||||
|
||||
@implementation FileProtocol
|
||||
|
||||
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
|
||||
{
|
||||
NSString *scheme = request.URL.scheme;
|
||||
if ([kProtocolFileScheme caseInsensitiveCompare:scheme] == NSOrderedSame) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
|
||||
{
|
||||
return request;
|
||||
}
|
||||
|
||||
+ (NSString*)headFromHtml:(NSString*)html
|
||||
{
|
||||
if (!html) return nil;
|
||||
|
||||
NSString *head = nil;
|
||||
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"(?<=<head>)[\\w\\W.]*(?=</head>)" options:NSRegularExpressionCaseInsensitive error:nil];
|
||||
NSTextCheckingResult *headResult = [regex firstMatchInString:html options:0 range:NSMakeRange(0, html.length)];
|
||||
|
||||
if (headResult && headResult.range.location != NSNotFound) {
|
||||
head = [html substringWithRange:[headResult range]];
|
||||
}
|
||||
return head;
|
||||
}
|
||||
|
||||
+ (NSString *)charsetFromHtml:(NSString *)html
|
||||
{
|
||||
if (!html) return nil;
|
||||
|
||||
NSString *charset = nil;
|
||||
NSString *charsetPattern = @"((?<=charset=)\\s*[a-zA-Z0-9-]*)";
|
||||
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:charsetPattern options: NSRegularExpressionCaseInsensitive error:nil];
|
||||
NSTextCheckingResult *charsetResult = [regex firstMatchInString:html options:kNilOptions range:NSMakeRange(0, [html length])];
|
||||
if (charsetResult && charsetResult.range.location != NSNotFound) {
|
||||
charset = [[html substringWithRange:[charsetResult range]] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||
charset = [charset lowercaseString];
|
||||
}
|
||||
return charset;
|
||||
}
|
||||
|
||||
- (void)startLoading
|
||||
{
|
||||
/* retrieve the current request. */
|
||||
NSURLRequest *request = [self request];
|
||||
NSString *url = [[request URL] path];
|
||||
NSString *mimeType = mimeTypeForExtension(QString::fromNSString(url.pathExtension)).toNSString();
|
||||
|
||||
QString requestUrl(QString("%1").arg(QString::fromNSString(url)));
|
||||
QFile resource(requestUrl);
|
||||
|
||||
NSData *pageData = nil;
|
||||
if (resource.exists() && resource.open(QIODevice::ReadOnly)) {
|
||||
|
||||
QByteArray buffer = resource.readAll();
|
||||
|
||||
//pageData = [[NSData alloc] initWithBytes:buffer.constData() length:buffer.size()];
|
||||
pageData = [NSData dataWithBytes:buffer.constData() length:buffer.size()];
|
||||
resource.close();
|
||||
}
|
||||
|
||||
if (pageData) {
|
||||
|
||||
NSString *encoding = @"utf-8";
|
||||
if ([mimeType isEqualToString:@"text/html"]) {
|
||||
|
||||
NSString *content = [[NSString alloc] initWithBytesNoCopy: (char* )[pageData bytes] length:pageData.length encoding:NSISOLatin1StringEncoding freeWhenDone:NO];
|
||||
|
||||
NSString *charset = [FileProtocol charsetFromHtml:[FileProtocol headFromHtml:content]];
|
||||
if (charset) {
|
||||
encoding = charset;
|
||||
}
|
||||
[content autorelease];
|
||||
}
|
||||
|
||||
NSURLResponse *response =[[NSURLResponse alloc]initWithURL:self.request.URL
|
||||
MIMEType:mimeType
|
||||
expectedContentLength:[pageData length]
|
||||
textEncodingName:encoding];
|
||||
|
||||
|
||||
[[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
|
||||
[[self client] URLProtocol:self didLoadData:pageData];
|
||||
[[self client] URLProtocolDidFinishLoading:self];
|
||||
[response autorelease];
|
||||
}
|
||||
else {
|
||||
[[self client] URLProtocol:self didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil]];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)stopLoading
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
432
client/core/webview/jshandler.cpp
Normal file
432
client/core/webview/jshandler.cpp
Normal file
@@ -0,0 +1,432 @@
|
||||
#include <QtCore/QString>
|
||||
#include <QtCore/QUrl>
|
||||
#include <QtCore/QFile>
|
||||
#include <QtCore/QByteArray>
|
||||
#include <QtCore/QDebug>
|
||||
#include <QtCore/QVariant>
|
||||
#include <QtCore/QMetaMethod>
|
||||
#include <QtCore/QMetaObject>
|
||||
#include <QtCore/QGenericArgument>
|
||||
#include <QtCore/QJsonObject>
|
||||
#include <QtCore/QJsonArray>
|
||||
#include <QtCore/QJsonDocument>
|
||||
|
||||
#include "amneziawebview_p.h"
|
||||
#include "mimecache.h"
|
||||
#include "jshandler.h"
|
||||
|
||||
QString JsHandler::scheme() const
|
||||
{
|
||||
return QString("js");
|
||||
}
|
||||
|
||||
QString JsHandler::host() const
|
||||
{
|
||||
qulonglong h = (qulonglong)(void*)_host;
|
||||
return QString("js%1").arg(h);
|
||||
}
|
||||
|
||||
QString JsHandler::scriptObjectsUrl() const
|
||||
{
|
||||
return QString("%1://%2/%3.js").arg(scheme()).arg(host()).arg(scriptObjectsId());
|
||||
}
|
||||
|
||||
QString JsHandler::scriptObjectsId() const
|
||||
{
|
||||
return QString("__scriptObjects__");
|
||||
}
|
||||
|
||||
QString JsHandler::scriptObjects() const
|
||||
{
|
||||
QString script;
|
||||
QList<QString> scripts = scriptParts.values();
|
||||
|
||||
for ( int i = 0; i < scripts.count(); i++) {
|
||||
script = QString("%1 %2").arg(script).arg(scripts.at(i)).trimmed();
|
||||
}
|
||||
return script;
|
||||
}
|
||||
|
||||
void JsHandler::updateWebView()
|
||||
{
|
||||
if (!scriptObjectsInjected) {
|
||||
|
||||
QString injector = QString(
|
||||
" var script = document.getElementById(\"%1\"); "
|
||||
" if(script === null) { "
|
||||
" script = document.createElement('script'); "
|
||||
" script.type = \"text/javascript\"; "
|
||||
" script.id = \"%2\"; "
|
||||
" document.getElementsByTagName('head')[0].appendChild(script); "
|
||||
" } "
|
||||
" script.text = \"%3\"; "
|
||||
).arg(scriptObjectsId()).arg(scriptObjectsId()).arg(scriptObjects());
|
||||
|
||||
|
||||
|
||||
/*
|
||||
QString injector = QString(
|
||||
" var script = document.getElementById(\"%1\"); "
|
||||
" if(script === null) { "
|
||||
" script = document.createElement('script'); "
|
||||
" script.type = \"text/javascript\"; "
|
||||
" script.id = \"%2\"; "
|
||||
" document.getElementsByTagName('head')[0].appendChild(script); "
|
||||
" } "
|
||||
" script.src = \"%3\"; "
|
||||
).arg(scriptObjectsId()).arg(scriptObjectsId()).arg(scriptObjectsUrl());
|
||||
|
||||
*/
|
||||
|
||||
//убрали, так как теперь есть рабочий вебинспектор
|
||||
//_host->evaluateJavaScript(injector);
|
||||
scriptObjectsInjected = true;
|
||||
}
|
||||
}
|
||||
|
||||
void JsHandler::addToJavaScriptWindowObject(const QString& name, QObject* object)
|
||||
{
|
||||
windowObjects[name] = object;
|
||||
QString script = windowScriptObject(name, object);
|
||||
scriptParts[name] = script;
|
||||
}
|
||||
|
||||
bool JsHandler::canHandleUrl(const QUrl &url) const
|
||||
{
|
||||
if (scheme() == url.scheme() && url.host() == host())
|
||||
return true;
|
||||
|
||||
if (this->host() == url.host() && (url.scheme() == QString("http")))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
QString JsHandler::mimeTypeForUrl(const QUrl &url) const
|
||||
{
|
||||
if (scheme() == url.scheme() && url.host() == host())
|
||||
return mimeTypeForUrl(url);
|
||||
|
||||
if (this->host() == url.host() && (url.scheme() == QString("http")))
|
||||
return mimeTypeForExtension("json");
|
||||
|
||||
return QString("application/octet-stream");
|
||||
}
|
||||
|
||||
QString JsHandler::windowScriptObject(const QString& name, QObject* object) const
|
||||
{
|
||||
QString script = QString(" var %1 = {}; "
|
||||
" %2.responseText = null; "
|
||||
).arg(name).arg(name);
|
||||
if (object) {
|
||||
const QMetaObject *meta = object->metaObject();
|
||||
const QString prefix = QString("%1.").arg(name);
|
||||
|
||||
int methodCount = meta->methodCount();
|
||||
for (int i = 0; i < methodCount; i++) {
|
||||
QMetaMethod metaMethod = meta->method(i);
|
||||
|
||||
if ((metaMethod.access() == QMetaMethod::Public) && ((metaMethod.methodType() == QMetaMethod::Slot) ||
|
||||
(metaMethod.methodType() == QMetaMethod::Method)) ) {
|
||||
const QString methodName = QString("%1%2").arg(prefix).arg(QString(metaMethod.name()));
|
||||
|
||||
//Parameters
|
||||
QList<QByteArray> parametersNames = metaMethod.parameterNames();
|
||||
QString methodParameters = QString("");
|
||||
QString stringifyParameters = QString("");
|
||||
|
||||
for (int j = 0; j < parametersNames.count(); j++) {
|
||||
methodParameters += QString(parametersNames.at(j));
|
||||
|
||||
stringifyParameters += QString("'%1=' + encodeURIComponent(%2)").arg(QString(parametersNames.at(j)))
|
||||
.arg(QString(parametersNames.at(j)));
|
||||
|
||||
if(j < (parametersNames.count() -1)) {
|
||||
methodParameters += QString(", ");
|
||||
stringifyParameters += QString(" + '&' + ");
|
||||
}
|
||||
}
|
||||
if (stringifyParameters.length() > 0)
|
||||
stringifyParameters = QString(" + '?'+ %1").arg(stringifyParameters);
|
||||
|
||||
const QString methodUrl = QString("http://%1/%2").arg(host()).arg(methodName);
|
||||
//Body
|
||||
const QString bodyTemplate = QString(""
|
||||
" var obj = this; "
|
||||
" if (obj.responseText !== null) { "
|
||||
" var ret = eval( obj.responseText ); "
|
||||
" obj.responseText = null; "
|
||||
" return ret;"
|
||||
" }; "
|
||||
" var caller = arguments.callee.caller; "
|
||||
" var callerArgs = caller.arguments; "
|
||||
" var xhr = new XMLHttpRequest; "
|
||||
" xhr.onload=function(){ "
|
||||
" if (xhr.status == 200) { "
|
||||
" obj.responseText = xhr.responseText; "
|
||||
" if (obj.responseText == null) { "
|
||||
" obj.responceText = ''; "
|
||||
" } "
|
||||
" caller(callerArgs); "
|
||||
" }; "
|
||||
" }; "
|
||||
" xhr.open('GET', '%1'%2, true); "
|
||||
//" xhr.setRequestHeader('Access-Control-Allow-Origin', '*'); "
|
||||
//" xhr.setRequestHeader('Access-Control-Allow-Headers', 'Content-Type'); "
|
||||
" xhr.send(null);"
|
||||
).arg(methodUrl).arg(stringifyParameters);
|
||||
|
||||
|
||||
QString methodBody = QString("%1").arg(bodyTemplate);
|
||||
|
||||
script = script + QString("%1=function(%2){ %3 }; " )
|
||||
.arg(methodName)
|
||||
.arg(methodParameters)
|
||||
.arg(methodBody);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
return script;
|
||||
}
|
||||
|
||||
|
||||
const QString createPopup = QString( ""
|
||||
" window.createPopup=function(){ "
|
||||
" var popup=document.createElement('iframe'), "
|
||||
" isShown=false, popupClicked=false; "
|
||||
" popup.src='about:blank'; "
|
||||
" popup.style.position='absolute'; "
|
||||
" popup.style.border='0px'; "
|
||||
" popup.style.display='none'; "
|
||||
" popup.addEventListener('load', function(e){ "
|
||||
" popup.document=(popup.contentWindow || popup.contentDocument); "
|
||||
" if(popup.document.document) popup.document=popup.document.document; "
|
||||
" }); "
|
||||
" document.body.appendChild(popup); "
|
||||
" var hidepopup=function(event){ "
|
||||
" if(isShown) "
|
||||
" setTimeout(function(){ "
|
||||
//" if(!popupClicked){ "
|
||||
" popup.hide(); "
|
||||
//" } "
|
||||
" popupClicked=false; "
|
||||
" }, 150); "
|
||||
" }; "
|
||||
" popup.show=function(x, y, w, h, pElement){ "
|
||||
" if(typeof(x) !== 'undefined'){ "
|
||||
" var elPos=[0, 0]; "
|
||||
" if(pElement) elPos=findPos(pElement); "
|
||||
" elPos[0]+=y, elPos[1]+=x; "
|
||||
" if(isNaN(w)) w=popup.document.scrollWidth; "
|
||||
" if(isNaN(h)) h=popup.document.scrollHeight; "
|
||||
" if(elPos[0] + w > document.body.clientWidth) elPos[0]=document.body.clientWidth - w - 5; "
|
||||
" if(elPos[1] + h > document.body.clientHeight) elPos[1]=document.body.clientHeight - h - 5; "
|
||||
" popup.style.left=elPos[0] + 'px'; "
|
||||
" popup.style.top=elPos[1] + 'px'; "
|
||||
" popup.style.width=(w + 'px'); "
|
||||
" popup.style.height=(h + 'px'); "
|
||||
" } "
|
||||
" popup.style.display='block'; "
|
||||
" isShown=true; "
|
||||
" }; "
|
||||
" popup.hide=function(){ "
|
||||
" isShown=false; "
|
||||
" popup.style.display='none'; "
|
||||
" }; "
|
||||
" window.addEventListener('click', hidepopup, true); "
|
||||
" window.addEventListener('blur', hidepopup, true); "
|
||||
" return popup; "
|
||||
" }; "
|
||||
" function findPos(obj, foundScrollLeft, foundScrollTop) { "
|
||||
" var curleft = 0; "
|
||||
" var curtop = 0; "
|
||||
" if(obj.offsetLeft) curleft += parseInt(obj.offsetLeft); "
|
||||
" if(obj.offsetTop) curtop += parseInt(obj.offsetTop); "
|
||||
" if(obj.scrollTop && obj.scrollTop > 0) { "
|
||||
" curtop -= parseInt(obj.scrollTop); "
|
||||
" foundScrollTop = true; "
|
||||
" } "
|
||||
" if(obj.scrollLeft && obj.scrollLeft > 0) { "
|
||||
" curleft -= parseInt(obj.scrollLeft); "
|
||||
" foundScrollLeft = true; "
|
||||
" } "
|
||||
" if(obj.offsetParent) { "
|
||||
" var pos = findPos(obj.offsetParent, foundScrollLeft, foundScrollTop); "
|
||||
" curleft += pos[0]; "
|
||||
" curtop += pos[1]; "
|
||||
" } else if(obj.ownerDocument) { "
|
||||
" var thewindow = obj.ownerDocument.defaultView; "
|
||||
" if(!thewindow && obj.ownerDocument.parentWindow) "
|
||||
" thewindow = obj.ownerDocument.parentWindow; "
|
||||
" if(thewindow) { "
|
||||
" if (!foundScrollTop && thewindow.scrollY && thewindow.scrollY > 0) curtop -= parseInt(thewindow.scrollY); "
|
||||
" if (!foundScrollLeft && thewindow.scrollX && thewindow.scrollX > 0) curleft -= parseInt(thewindow.scrollX); "
|
||||
" if(thewindow.frameElement) { "
|
||||
" var pos = findPos(thewindow.frameElement); "
|
||||
" curleft += pos[0]; "
|
||||
" curtop += pos[1]; "
|
||||
" } "
|
||||
" }"
|
||||
" }"
|
||||
" return [curleft,curtop]; "
|
||||
" } " );
|
||||
|
||||
|
||||
void JsHandler::init()
|
||||
{
|
||||
/*
|
||||
QString consoleObject = createPopup + QString (
|
||||
"function popup(msg){ "
|
||||
" var p = window.createPopup(); "
|
||||
" var pbody = p.document.body; "
|
||||
" pbody.style.backgroundColor='lime'; "
|
||||
" pbody.style.border='solid black 1px'; "
|
||||
" pbody.innerHTML=msg; "
|
||||
" p.show(NaN,NaN,NaN,NaN, document.body); }"
|
||||
" window.console={ "
|
||||
" log=function(msg){ popup(msg); }"
|
||||
" warning=function(msg){ popup(msg); }"
|
||||
" error=function(msg){ popup(msg); }"
|
||||
" info=function(msg){ popup(msg); }"
|
||||
" }");
|
||||
*/
|
||||
|
||||
QString consoleObject = QString ( ""
|
||||
" webviewPopup=function(msg){ "
|
||||
" var p = window.createPopup(); "
|
||||
" var pbody = p.document.body; "
|
||||
" pbody.style.backgroundColor='white'; "
|
||||
" pbody.style.border='solid black 1px'; "
|
||||
" pbody.innerHTML=msg + ' ' + document.body.clientWidth; "
|
||||
" p.show(0, 0, document.body.clientWidth, NaN, document.body); }; "
|
||||
" window.console.log=function(msg){ webviewPopup(msg); };"
|
||||
" window.console.warning=function(msg){ webviewPopup(msg); };"
|
||||
" window.console.error=function(msg){ webviewPopup(msg); };"
|
||||
" window.console.info=function(msg){ webviewPopup(msg); };"
|
||||
) + createPopup;
|
||||
/*
|
||||
QString object = QString( " var script = document.getElementById(\"consoleScriptObject\"); "
|
||||
" if(script === null) { "
|
||||
" script = document.createElement('script'); "
|
||||
" script.type = \"text/javascript\";"
|
||||
" script.text = \"%1\";"
|
||||
" script.id = \"consoleScriptObject\"; "
|
||||
" document.getElementsByTagName('head')[0].appendChild(script); "
|
||||
" } "
|
||||
).arg(consoleObject);
|
||||
*/
|
||||
|
||||
scriptParts["__console__"] = consoleObject;
|
||||
}
|
||||
|
||||
QByteArray JsHandler::dataForUrl(const QUrl &url) const
|
||||
{
|
||||
QByteArray buffer;
|
||||
QVariant ret = callMethodForUrl(url);
|
||||
if (ret.isValid()) {
|
||||
|
||||
QJsonValue value = QJsonValue::fromVariant(ret);
|
||||
QJsonArray jsonArray;
|
||||
jsonArray.append(value);
|
||||
QJsonDocument jsonDoc(jsonArray);
|
||||
buffer = jsonDoc.toJson();
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
|
||||
// Method call specified by url of type: http://scripthost/object.methodname?paramName1=paramValue1¶mName2=paramValue2&...
|
||||
QVariant JsHandler::callMethodForUrl(const QUrl &url) const
|
||||
{
|
||||
qDebug() << url.toString();
|
||||
qDebug() << "Path: " << url.path();
|
||||
qDebug() << "Query: " << url.query();
|
||||
|
||||
QVariant ret;
|
||||
QStringList objectPath = url.path().trimmed().remove('/').split(".");
|
||||
QStringList query;
|
||||
if (url.query().trimmed().length() > 0)
|
||||
query = url.query().trimmed().split("&");
|
||||
QList<QGenericArgument> arguments;
|
||||
QList<QByteArray> parametersNames;
|
||||
QList<QByteArray> parametersTypes;
|
||||
QString methodName;
|
||||
QString signature;
|
||||
|
||||
AmneziaWebViewPrivate *d = AmneziaWebViewPrivate::get(_host);
|
||||
|
||||
if (d && objectPath.count() == 2) {
|
||||
qDebug() << "Called method: " << objectPath[0] << "."<< objectPath[1];
|
||||
QObject *object = d->jsHandler.windowObjects.value(objectPath[0]);
|
||||
if (object) {
|
||||
int methodCount = object->metaObject()->methodCount();
|
||||
int methodIndex = -1;
|
||||
for(int i = 0; i < methodCount; i++) {
|
||||
const QMetaMethod method = object->metaObject()->method(i);
|
||||
parametersNames = method.parameterNames();
|
||||
methodName = method.name();
|
||||
if ((query.count() == parametersNames.count()) && (methodName == objectPath[1])) {
|
||||
methodIndex = i;
|
||||
parametersTypes = method.parameterTypes();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (methodIndex >= 0) {
|
||||
const QMetaMethod method = object->metaObject()->method(methodIndex);
|
||||
QVariantList params;
|
||||
for (int i = 0; i < query.count(); i++) {
|
||||
QStringList param = query[i].split('=');
|
||||
|
||||
params.append(QVariant(QString(param[1].toLocal8Bit())));
|
||||
QGenericArgument arg(param[0].toLocal8Bit().constData(), ¶ms[i]);
|
||||
arguments.append(arg);
|
||||
}
|
||||
switch (arguments.count()) {
|
||||
case 1:
|
||||
method.invoke(object, Qt::DirectConnection, Q_RETURN_ARG(QVariant, ret), arguments[0]);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
method.invoke(object, Qt::DirectConnection, Q_RETURN_ARG(QVariant, ret), arguments[0], arguments[1]);
|
||||
break;
|
||||
|
||||
case 3:
|
||||
method.invoke(object, Qt::DirectConnection, Q_RETURN_ARG(QVariant, ret), arguments[0], arguments[1], arguments[2]);
|
||||
break;
|
||||
|
||||
case 4:
|
||||
method.invoke(object, Qt::DirectConnection, Q_RETURN_ARG(QVariant, ret), arguments[0], arguments[1], arguments[2],
|
||||
arguments[3]);
|
||||
break;
|
||||
|
||||
case 5:
|
||||
method.invoke(object, Qt::DirectConnection, Q_RETURN_ARG(QVariant, ret), arguments[0], arguments[1], arguments[2],
|
||||
arguments[3], arguments[4]);
|
||||
break;
|
||||
case 6:
|
||||
method.invoke(object, Qt::DirectConnection, Q_RETURN_ARG(QVariant, ret), arguments[0], arguments[1], arguments[2],
|
||||
arguments[3], arguments[4], arguments[5]);
|
||||
break;
|
||||
|
||||
case 7:
|
||||
method.invoke(object, Qt::DirectConnection, Q_RETURN_ARG(QVariant, ret), arguments[0], arguments[1], arguments[2],
|
||||
arguments[3], arguments[4], arguments[5], arguments[6]);
|
||||
break;
|
||||
|
||||
case 8:
|
||||
method.invoke(object, Qt::DirectConnection, Q_RETURN_ARG(QVariant, ret), arguments[0], arguments[1], arguments[2],
|
||||
arguments[3], arguments[4], arguments[5], arguments[6], arguments[7]);
|
||||
break;
|
||||
|
||||
default:
|
||||
method.invoke(object, Qt::DirectConnection, Q_RETURN_ARG(QVariant, ret));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
41
client/core/webview/jshandler.h
Normal file
41
client/core/webview/jshandler.h
Normal file
@@ -0,0 +1,41 @@
|
||||
#ifndef JSHANDLER_H
|
||||
#define JSHANDLER_H
|
||||
|
||||
class QUrl;
|
||||
class QVariant;
|
||||
class AmneziaWebView;
|
||||
|
||||
class JsHandler
|
||||
{
|
||||
friend class AmneziaWebView;
|
||||
|
||||
public:
|
||||
explicit JsHandler(AmneziaWebView *host);
|
||||
virtual ~JsHandler();
|
||||
|
||||
bool canHandleUrl(const QUrl &url) const;
|
||||
QByteArray dataForUrl(const QUrl &url) const;
|
||||
void addToJavaScriptWindowObject(const QString& name, QObject* object);
|
||||
QString mimeTypeForUrl(const QUrl &url) const;
|
||||
|
||||
void updateWebView();
|
||||
QString host() const;
|
||||
QString scheme() const;
|
||||
QString scriptObjectsUrl() const;
|
||||
QString scriptObjectsId() const;
|
||||
QString scriptObjects() const;
|
||||
|
||||
private:
|
||||
|
||||
QVariant callMethodForUrl(const QUrl &url) const;
|
||||
QString windowScriptObject(const QString& name, QObject* object) const;
|
||||
|
||||
void init();
|
||||
AmneziaWebView *_host;
|
||||
bool scriptObjectsInjected;
|
||||
|
||||
QHash<QString, QObject*> windowObjects;
|
||||
QHash<QString, QString> scriptParts;
|
||||
};
|
||||
|
||||
#endif
|
||||
24
client/core/webview/jshandler_android.cpp
Normal file
24
client/core/webview/jshandler_android.cpp
Normal file
@@ -0,0 +1,24 @@
|
||||
#include <QtCore/QString>
|
||||
#include <QtCore/QUrl>
|
||||
#include <QtCore/QFile>
|
||||
#include <QtCore/QByteArray>
|
||||
#include <QtCore/QDebug>
|
||||
#include <QtCore/QVariant>
|
||||
#include <QtCore/QMetaMethod>
|
||||
#include <QtCore/QMetaObject>
|
||||
#include <QtCore/QGenericArgument>
|
||||
#include <QtCore/QJsonObject>
|
||||
#include <QtCore/QJsonArray>
|
||||
#include <QtCore/QJsonDocument>
|
||||
|
||||
#include "amneziawebview_p.h"
|
||||
#include "jshandler.h"
|
||||
#include "mimecache.h"
|
||||
|
||||
JsHandler::JsHandler(AmneziaWebView *host): _host(host)
|
||||
{
|
||||
init();
|
||||
}
|
||||
|
||||
JsHandler::~JsHandler() {}
|
||||
|
||||
185
client/core/webview/jshandler_ios.mm
Normal file
185
client/core/webview/jshandler_ios.mm
Normal file
@@ -0,0 +1,185 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <MobileCoreServices/UTCoreTypes.h>
|
||||
#import <MobileCoreServices/UTType.h>
|
||||
|
||||
#include <QtCore/QFile>
|
||||
#include <QtCore/QString>
|
||||
#include <QtCore/QHash>
|
||||
#include <QtCore/QDebug>
|
||||
#include <QtCore/QUrl>
|
||||
#include <QtCore/QRect>
|
||||
#include <QColor>
|
||||
#include <QtCore/QDebug>
|
||||
#include <QtCore/QVariant>
|
||||
#include <QtCore/QMetaMethod>
|
||||
#include <QtCore/QMetaObject>
|
||||
#include <QtCore/QGenericArgument>
|
||||
#include <QtCore/QJsonObject>
|
||||
#include <QtCore/QJsonArray>
|
||||
#include <QtCore/QJsonDocument>
|
||||
|
||||
#include "amneziawebview_p.h"
|
||||
#include "mimecache.h"
|
||||
#import "jshandler.h"
|
||||
|
||||
typedef QHash<QString, AmneziaWebView*> JavaScriptHosts;
|
||||
Q_GLOBAL_STATIC(JavaScriptHosts, hosts);
|
||||
|
||||
#if !defined(ENABLE_WKWEBVIEW)
|
||||
|
||||
@interface JsProtocol : NSURLProtocol
|
||||
+ (NSString*) requestVarsKey;
|
||||
@end
|
||||
|
||||
@interface NSURLRequest (JsProtocol)
|
||||
- (NSDictionary *)requestVars;
|
||||
@end
|
||||
|
||||
@interface NSMutableURLRequest (JsProtocol)
|
||||
- (void)setRequestVars:(NSDictionary *)vars;
|
||||
@end
|
||||
|
||||
#endif
|
||||
|
||||
JsHandler::JsHandler(AmneziaWebView *h): _host(h)
|
||||
, scriptObjectsInjected(false)
|
||||
{
|
||||
#if !defined(ENABLE_WKWEBVIEW)
|
||||
static bool protocolRegistered = false;
|
||||
if (!protocolRegistered) {
|
||||
[NSURLProtocol registerClass:[JsProtocol class]];
|
||||
protocolRegistered = true;
|
||||
}
|
||||
#endif
|
||||
QString key = host();
|
||||
if (!hosts()->keys().contains(key)) {
|
||||
hosts()->insert(key, h);
|
||||
}
|
||||
init();
|
||||
}
|
||||
|
||||
JsHandler::~JsHandler()
|
||||
{
|
||||
QString key = host();
|
||||
if (hosts()->keys().contains(key)) {
|
||||
hosts()->remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
#if !defined(ENABLE_WKWEBVIEW)
|
||||
|
||||
@implementation NSURLRequest (JsProtocol)
|
||||
|
||||
- (NSDictionary *)requestVars {
|
||||
NSLog(@"%@ received %@", self, NSStringFromSelector(_cmd));
|
||||
return [NSURLProtocol propertyForKey:[JsProtocol requestVarsKey] inRequest:self];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation NSMutableURLRequest (JsProtocol)
|
||||
|
||||
- (void)setRequestVars:(NSDictionary *)requestVars {
|
||||
|
||||
NSLog(@"%@ received %@", self, NSStringFromSelector(_cmd));
|
||||
|
||||
NSDictionary *specialVarsCopy = [requestVars copy];
|
||||
[NSURLProtocol setProperty:specialVarsCopy forKey:[JsProtocol requestVarsKey] inRequest:self];
|
||||
[specialVarsCopy release];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation JsProtocol
|
||||
|
||||
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
|
||||
{
|
||||
//NSString *url = request.URL.absoluteString;
|
||||
QString host = QString::fromNSString(request.URL.host).toLower();
|
||||
if (hosts()->keys().contains(host))
|
||||
return YES;
|
||||
|
||||
//NSLog(@"Requested Url: %@", url);
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
|
||||
{
|
||||
return request;
|
||||
}
|
||||
|
||||
+ (NSString*) requestVarsKey
|
||||
{
|
||||
return @"requestVars";
|
||||
}
|
||||
|
||||
- (void)startLoading
|
||||
{
|
||||
QString host = QString::fromNSString(self.request.URL.host);
|
||||
AmneziaWebViewPrivate *d = AmneziaWebViewPrivate::get(hosts()->value(host));
|
||||
NSURLRequest *request = [self request];
|
||||
|
||||
if ([request.URL.absoluteString isEqualToString: d->jsHandler.scriptObjectsUrl().toNSString()]) {
|
||||
|
||||
//NSString *mimeType = mimeTypeForExtension(QString::fromNSString(request.URL.path.pathExtension)).toNSString();
|
||||
|
||||
NSString *mimeType = d->jsHandler.mimeTypeForUrl(QUrl::fromNSURL(request.URL)).toNSString();
|
||||
QByteArray buffer = d->jsHandler.scriptObjects().toLocal8Bit();
|
||||
|
||||
//NSData *data = [[NSData alloc] initWithBytes:buffer.constData() length:buffer.size()];
|
||||
NSData *data = [NSData dataWithBytes:buffer.constData() length:buffer.size()];
|
||||
|
||||
NSURLResponse *response =[[NSURLResponse alloc]initWithURL:self.request.URL
|
||||
MIMEType:mimeType
|
||||
expectedContentLength:[data length]
|
||||
textEncodingName:@"utf-8"];
|
||||
|
||||
|
||||
[[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
|
||||
[[self client] URLProtocol:self didLoadData:data];
|
||||
[[self client] URLProtocolDidFinishLoading:self];
|
||||
[response autorelease];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ( d && [request.URL.scheme isEqualToString:@"http"]
|
||||
&& [request.URL.host isEqualToString:host.toNSString()]) {
|
||||
|
||||
const QByteArray buffer = d->dataForUrl(QUrl::fromNSURL(request.URL));
|
||||
//NSData *data = data = [[NSData alloc] initWithBytes:buffer.constData() length:buffer.size()];
|
||||
NSData *data = data = [NSData dataWithBytes:buffer.constData() length:buffer.size()];
|
||||
|
||||
//NSString *mimeType = mimeTypeForExtension(QString("json")).toNSString();
|
||||
NSString *mimeType = d->jsHandler.mimeTypeForUrl(QUrl::fromNSURL(request.URL)).toNSString();
|
||||
|
||||
NSDictionary *headers = @{@"Access-Control-Allow-Origin" : @"*",
|
||||
@"Access-Control-Allow-Headers" : @"Content-Type",
|
||||
@"Cache-Control" : @"no-cache",
|
||||
@"Content-Type" : [NSString stringWithFormat:@"%@; %@", mimeType, @"charset=UTF-8"] };
|
||||
|
||||
NSHTTPURLResponse* response = [[NSHTTPURLResponse alloc] initWithURL:request.URL
|
||||
statusCode:200
|
||||
HTTPVersion:@"HTTP/1.1"
|
||||
headerFields:headers];
|
||||
|
||||
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
|
||||
[self.client URLProtocol:self didLoadData:data];
|
||||
[self.client URLProtocolDidFinishLoading:self];
|
||||
[response autorelease];
|
||||
return;
|
||||
}
|
||||
else {
|
||||
[[self client] URLProtocol:self didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil]];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)stopLoading
|
||||
{
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
274
client/core/webview/mimecache.cpp
Normal file
274
client/core/webview/mimecache.cpp
Normal file
@@ -0,0 +1,274 @@
|
||||
#include "mimecache.h"
|
||||
|
||||
#include <QtCore/QFile>
|
||||
#include <QtCore/QUrl>
|
||||
#include <QtCore/QString>
|
||||
#include <QtCore/QMimeDatabase>
|
||||
#include <QtCore/QMimeType>
|
||||
#include <QtCore/QHash>
|
||||
#include <QtCore/QDebug>
|
||||
|
||||
typedef QHash<QString, QString> MimeTypes;
|
||||
Q_GLOBAL_STATIC(MimeTypes, cache)
|
||||
Q_GLOBAL_STATIC(QMimeDatabase, mimeDatabase)
|
||||
|
||||
|
||||
void initCache()
|
||||
{
|
||||
if (cache()->isEmpty()) {
|
||||
|
||||
cache()->insert("323", "text/h323");
|
||||
cache()->insert("*", "application/octet-stream");
|
||||
cache()->insert("acx", "application/internet-property-stream");
|
||||
cache()->insert("ai", "application/postscript");
|
||||
cache()->insert("aif", "audio/x-aiff");
|
||||
cache()->insert("aifc", "audio/x-aiff");
|
||||
cache()->insert("aiff", "audio/x-aiff");
|
||||
cache()->insert("asf", "video/x-ms-asf");
|
||||
cache()->insert("asr", "video/x-ms-asf");
|
||||
cache()->insert("asx", "video/x-ms-asf");
|
||||
cache()->insert("au", "audio/basic");
|
||||
cache()->insert("avi", "video/x-msvideo");
|
||||
cache()->insert("axs", "application/olescript");
|
||||
cache()->insert("bas", "text/plain");
|
||||
cache()->insert("bcpio", "application/x-bcpio");
|
||||
cache()->insert("bin", "application/octet-stream");
|
||||
cache()->insert("bmp", "image/bmp");
|
||||
cache()->insert("c", "text/plain");
|
||||
cache()->insert("cat", "application/vnd.ms-pkiseccat");
|
||||
cache()->insert("cdf", "application/x-cdf");
|
||||
cache()->insert("cdf", "application/x-netcdf");
|
||||
cache()->insert("cer", "application/x-x509-ca-cert""cer");
|
||||
cache()->insert("class", "application/octet-stream");
|
||||
cache()->insert("clp", "application/x-msclip");
|
||||
cache()->insert("cmx", "image/x-cmx");
|
||||
cache()->insert("cod", "image/cis-cod");
|
||||
cache()->insert("cpio", "application/x-cpio");
|
||||
cache()->insert("crd", "application/x-mscardfile");
|
||||
cache()->insert("crl", "application/pkix-crl");
|
||||
cache()->insert("crt", "application/x-x509-ca-cert");
|
||||
cache()->insert("csh", "application/x-csh");
|
||||
cache()->insert("css", "text/css");
|
||||
cache()->insert("dcr", "application/x-director");
|
||||
cache()->insert("der", "application/x-x509-ca-cert");
|
||||
cache()->insert("dir", "application/x-director");
|
||||
cache()->insert("dll", "application/x-msdownload");
|
||||
cache()->insert("dms", "application/octet-stream");
|
||||
cache()->insert("doc", "application/msword");
|
||||
cache()->insert("dot", "application/msword");
|
||||
cache()->insert("dvi", "application/x-dvi");
|
||||
cache()->insert("dxr", "application/x-director");
|
||||
cache()->insert("eot", "application/vnd.ms-fontobject");
|
||||
cache()->insert("eps", "application/postscript");
|
||||
cache()->insert("etx", "text/x-setext");
|
||||
cache()->insert("evy", "application/envoy");
|
||||
cache()->insert("exe", "application/octet-stream");
|
||||
cache()->insert("fif", "application/fractals");
|
||||
cache()->insert("flr", "x-world/x-vrml");
|
||||
cache()->insert("gif", "image/gif");
|
||||
cache()->insert("gtar", "application/x-gtar");
|
||||
cache()->insert("gz", "application/x-gzip");
|
||||
cache()->insert("h", "text/plain");
|
||||
cache()->insert("hdf", "application/x-hdf");
|
||||
cache()->insert("hlp", "application/winhlp");
|
||||
cache()->insert("hqx", "application/mac-binhex40");
|
||||
cache()->insert("hta", "application/hta");
|
||||
cache()->insert("htc", "text/x-component");
|
||||
cache()->insert("htm", "text/html");
|
||||
cache()->insert("html", "text/html");
|
||||
cache()->insert("htt", "text/webviewhtml");
|
||||
cache()->insert("ico", "image/x-icon");
|
||||
cache()->insert("ief", "image/ief");
|
||||
cache()->insert("iii", "application/x-iphone");
|
||||
cache()->insert("ins", "application/x-internet-signup");
|
||||
cache()->insert("isp", "application/x-internet-signup");
|
||||
cache()->insert("jfif", "image/pipeg");
|
||||
cache()->insert("jpe", "image/jpeg");
|
||||
cache()->insert("jpeg", "image/jpeg");
|
||||
cache()->insert("jpg", "image/jpeg");
|
||||
cache()->insert("js", "application/x-javascript");
|
||||
cache()->insert("json", "application/json");
|
||||
cache()->insert("latex", "application/x-latex");
|
||||
cache()->insert("lha", "application/octet-stream");
|
||||
cache()->insert("lsf", "video/x-la-asf");
|
||||
cache()->insert("lsx", "video/x-la-asf");
|
||||
cache()->insert("lzh", "application/octet-stream");
|
||||
cache()->insert("m13", "application/x-msmediaview");
|
||||
cache()->insert("m14", "application/x-msmediaview");
|
||||
cache()->insert("m3u", "audio/x-mpegurl");
|
||||
cache()->insert("m4v", "video/x-m4v");
|
||||
cache()->insert("man", "application/x-troff-man");
|
||||
cache()->insert("mdb", "application/x-msaccess");
|
||||
cache()->insert("me", "application/x-troff-me");
|
||||
cache()->insert("mht", "message/rfc822");
|
||||
cache()->insert("mhtml", "message/rfc822");
|
||||
cache()->insert("mid", "audio/mid");
|
||||
cache()->insert("mny", "application/x-msmoney");
|
||||
cache()->insert("mov", "video/quicktime");
|
||||
cache()->insert("movie", "video/x-sgi-movie""movie");
|
||||
cache()->insert("mp2", "video/mpeg");
|
||||
cache()->insert("mp3", "audio/mpeg");
|
||||
cache()->insert("mpa", "video/mpeg");
|
||||
cache()->insert("mpe", "video/mpeg");
|
||||
cache()->insert("mpeg", "video/mpeg");
|
||||
cache()->insert("mpg", "video/mpeg");
|
||||
cache()->insert("mpp", "application/vnd.ms-project");
|
||||
cache()->insert("mpv2", "video/mpeg");
|
||||
cache()->insert("ms", "application/x-troff-ms");
|
||||
cache()->insert("msg", "application/vnd.ms-outlook");
|
||||
cache()->insert("mvb", "application/x-msmediaview");
|
||||
cache()->insert("nc", "application/x-netcdf");
|
||||
cache()->insert("nws", "message/rfc822");
|
||||
cache()->insert("oda", "application/oda");
|
||||
cache()->insert("otf", "application/font-sfnt");
|
||||
cache()->insert("p10", "application/pkcs10");
|
||||
cache()->insert("p12", "application/x-pkcs12");
|
||||
cache()->insert("p7b", "application/x-pkcs7-certificates");
|
||||
cache()->insert("p7c", "application/x-pkcs7-mime");
|
||||
cache()->insert("p7m", "application/x-pkcs7-mime");
|
||||
cache()->insert("p7r", "application/x-pkcs7-certreqresp");
|
||||
cache()->insert("p7s", "application/x-pkcs7-signature");
|
||||
cache()->insert("pbm", "image/x-portable-bitmap");
|
||||
cache()->insert("pdf", "application/pdf");
|
||||
cache()->insert("pfx", "application/x-pkcs12");
|
||||
cache()->insert("pgm", "image/x-portable-graymap");
|
||||
cache()->insert("pko", "application/ynd.ms-pkipko");
|
||||
cache()->insert("pma", "application/x-perfmon");
|
||||
cache()->insert("pmc", "application/x-perfmon");
|
||||
cache()->insert("pml", "application/x-perfmon");
|
||||
cache()->insert("pmr", "application/x-perfmon");
|
||||
cache()->insert("pmw", "application/x-perfmon");
|
||||
cache()->insert("pnm", "image/x-portable-anymap");
|
||||
cache()->insert("pot", "application/vnd.ms-powerpoint");
|
||||
cache()->insert("ppm", "image/x-portable-pixmap");
|
||||
cache()->insert("pps", "application/vnd.ms-powerpoint");
|
||||
cache()->insert("ppt", "application/vnd.ms-powerpoint");
|
||||
cache()->insert("prf", "application/pics-rules");
|
||||
cache()->insert("ps", "application/postscript");
|
||||
cache()->insert("pub", "application/x-mspublisher");
|
||||
cache()->insert("qt", "video/quicktime");
|
||||
cache()->insert("ra", "audio/x-pn-realaudio");
|
||||
cache()->insert("ram", "audio/x-pn-realaudio");
|
||||
cache()->insert("ras", "image/x-cmu-raster");
|
||||
cache()->insert("rgb", "image/x-rgb");
|
||||
cache()->insert("rmi", "audio/mid");
|
||||
cache()->insert("roff", "application/x-troff");
|
||||
cache()->insert("rtf", "application/rtf""rtf");
|
||||
cache()->insert("rtx", "text/richtext""rtx");
|
||||
cache()->insert("scd", "application/x-msschedule");
|
||||
cache()->insert("sct", "text/scriptlet");
|
||||
cache()->insert("setpay", "application/set-payment-initiation");
|
||||
cache()->insert("setreg", "application/set-registration-initiation");
|
||||
cache()->insert("sh", "application/x-sh");
|
||||
cache()->insert("shar", "application/x-shar");
|
||||
cache()->insert("sit", "application/x-stuffit");
|
||||
cache()->insert("snd", "audio/basic");
|
||||
cache()->insert("spc", "application/x-pkcs7-certificates");
|
||||
cache()->insert("spl", "application/futuresplash");
|
||||
cache()->insert("src", "application/x-wais-source");
|
||||
cache()->insert("sst", "application/vnd.ms-pkicertstore");
|
||||
cache()->insert("stl", "application/vnd.ms-pkistl");
|
||||
cache()->insert("stm", "text/html");
|
||||
cache()->insert("sv4cpio", "application/x-sv4cpio");
|
||||
cache()->insert("sv4crc", "application/x-sv4crc");
|
||||
cache()->insert("svg", "image/svg+xml");
|
||||
cache()->insert("swf", "application/x-shockwave-flash");
|
||||
cache()->insert("t", "application/x-troff");
|
||||
cache()->insert("tar", "application/x-tar");
|
||||
cache()->insert("tcl", "application/x-tcl");
|
||||
cache()->insert("tex", "application/x-tex");
|
||||
cache()->insert("texi", "application/x-texinfo");
|
||||
cache()->insert("texinfo", "application/x-texinfo");
|
||||
cache()->insert("tgz", "application/x-compressed");
|
||||
cache()->insert("tif", "image/tiff");
|
||||
cache()->insert("tiff", "image/tiff");
|
||||
cache()->insert("tr", "application/x-troff");
|
||||
cache()->insert("trm", "application/x-msterminal");
|
||||
cache()->insert("tsv", "text/tab-separated-values");
|
||||
cache()->insert("txt", "text/plain");
|
||||
cache()->insert("ttf", "application/font-sfnt");
|
||||
cache()->insert("uls", "text/iuls");
|
||||
cache()->insert("ustar", "application/x-ustar");
|
||||
cache()->insert("vcf", "text/x-vcard");
|
||||
cache()->insert("vrml", "x-world/x-vrml");
|
||||
cache()->insert("wav", "audio/x-wav");
|
||||
cache()->insert("wcm", "application/vnd.ms-works");
|
||||
cache()->insert("wdb", "application/vnd.ms-works");
|
||||
cache()->insert("wks", "application/vnd.ms-works");
|
||||
cache()->insert("wmf", "application/x-msmetafile");
|
||||
cache()->insert("woff", "application/font-woff");
|
||||
cache()->insert("wps", "application/vnd.ms-works");
|
||||
cache()->insert("wri", "application/x-mswrite");
|
||||
cache()->insert("wrl", "x-world/x-vrml");
|
||||
cache()->insert("wrz", "x-world/x-vrml");
|
||||
cache()->insert("xaf", "x-world/x-vrml");
|
||||
cache()->insert("xbm", "image/x-xbitmap");
|
||||
cache()->insert("xla", "application/vnd.ms-excel");
|
||||
cache()->insert("xlc", "application/vnd.ms-excel");
|
||||
cache()->insert("xlm", "application/vnd.ms-excel");
|
||||
cache()->insert("xls", "application/vnd.ms-excel");
|
||||
cache()->insert("xlt", "application/vnd.ms-excel");
|
||||
cache()->insert("xlw", "application/vnd.ms-excel");
|
||||
cache()->insert("xof", "x-world/x-vrml");
|
||||
cache()->insert("xpm", "image/x-xpixmap");
|
||||
cache()->insert("xwd", "image/x-xwindowdump");
|
||||
cache()->insert("z", "application/x-compress");
|
||||
cache()->insert("zip", "application/zip");
|
||||
}
|
||||
}
|
||||
|
||||
QString mimeTypeForExtension(const QString &extension)
|
||||
{
|
||||
initCache();
|
||||
QString ext = extension;
|
||||
|
||||
const int lastDot = ext.lastIndexOf(QLatin1Char('.'));
|
||||
if (lastDot != -1) {
|
||||
const int extLength = ext.length() - lastDot - 1;
|
||||
ext = ext.right(extLength).toLower();
|
||||
}
|
||||
|
||||
QString mimeType = cache()->value(ext);
|
||||
|
||||
if (mimeType.isNull()) {
|
||||
mimeType = QString("application/octet-stream");
|
||||
}
|
||||
|
||||
return mimeType;
|
||||
}
|
||||
|
||||
QString mimeTypeForUrl(const QUrl &url)
|
||||
{
|
||||
initCache();
|
||||
QString path = url.path();
|
||||
|
||||
QString extension;
|
||||
QString mimeType;
|
||||
|
||||
const int lastDot = path.lastIndexOf(QLatin1Char('.'));
|
||||
if (lastDot != -1) {
|
||||
const int extLength = path.length() - lastDot - 1;
|
||||
extension = path.right(extLength).toLower();
|
||||
}
|
||||
|
||||
if (!extension.isNull()) {
|
||||
mimeType = cache()->value(extension);
|
||||
}
|
||||
if (mimeType.isNull()) {
|
||||
QMimeType mime = mimeDatabase()->mimeTypeForUrl(url);
|
||||
if (mime.isValid()) {
|
||||
mimeType = mime.name();
|
||||
if(!extension.isNull()) {
|
||||
cache()->insert(extension, mimeType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mimeType.isNull()) {
|
||||
mimeType = QString("application/octet-stream");
|
||||
}
|
||||
|
||||
qDebug() << mimeType;
|
||||
return mimeType;
|
||||
}
|
||||
|
||||
10
client/core/webview/mimecache.h
Normal file
10
client/core/webview/mimecache.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#ifndef MIMECACHE_H
|
||||
#define MIMECACHE_H
|
||||
|
||||
class QString;
|
||||
class QUrl;
|
||||
|
||||
QString mimeTypeForExtension(const QString &extension);
|
||||
QString mimeTypeForUrl(const QUrl &url);
|
||||
|
||||
#endif
|
||||
66
client/core/webview/pch.h
Normal file
66
client/core/webview/pch.h
Normal file
@@ -0,0 +1,66 @@
|
||||
/****************************************************************************
|
||||
**
|
||||
** Copyright (C) 2016 The Qt Company Ltd.
|
||||
** Contact: https://www.qt.io/licensing/
|
||||
**
|
||||
** This file is part of the QtCore module of the Qt Toolkit.
|
||||
**
|
||||
** $QT_BEGIN_LICENSE:LGPL$
|
||||
** Commercial License Usage
|
||||
** Licensees holding valid commercial Qt licenses may use this file in
|
||||
** accordance with the commercial license agreement provided with the
|
||||
** Software or, alternatively, in accordance with the terms contained in
|
||||
** a written agreement between you and The Qt Company. For licensing terms
|
||||
** and conditions see https://www.qt.io/terms-conditions. For further
|
||||
** information use the contact form at https://www.qt.io/contact-us.
|
||||
**
|
||||
** GNU Lesser General Public License Usage
|
||||
** Alternatively, this file may be used under the terms of the GNU Lesser
|
||||
** General Public License version 3 as published by the Free Software
|
||||
** Foundation and appearing in the file LICENSE.LGPL3 included in the
|
||||
** packaging of this file. Please review the following information to
|
||||
** ensure the GNU Lesser General Public License version 3 requirements
|
||||
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
|
||||
**
|
||||
** GNU General Public License Usage
|
||||
** Alternatively, this file may be used under the terms of the GNU
|
||||
** General Public License version 2.0 or (at your option) the GNU General
|
||||
** Public license version 3 or any later version approved by the KDE Free
|
||||
** Qt Foundation. The licenses are as published by the Free Software
|
||||
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
|
||||
** included in the packaging of this file. Please review the following
|
||||
** information to ensure the GNU General Public License requirements will
|
||||
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
|
||||
** https://www.gnu.org/licenses/gpl-3.0.html.
|
||||
**
|
||||
** $QT_END_LICENSE$
|
||||
**
|
||||
****************************************************************************/
|
||||
|
||||
/*
|
||||
* This is a precompiled header file for use in Xcode / Mac GCC /
|
||||
* GCC >= 3.4 / VC to greatly speed the building of Qt. It may also be
|
||||
* of use to people developing their own project, but it is probably
|
||||
* better to define your own header. Use of this header is currently
|
||||
* UNSUPPORTED.
|
||||
*/
|
||||
|
||||
|
||||
#if defined __cplusplus
|
||||
// for rand_s, _CRT_RAND_S must be #defined before #including stdlib.h.
|
||||
// put it at the beginning so some indirect inclusion doesn't break it
|
||||
#ifndef _CRT_RAND_S
|
||||
#define _CRT_RAND_S
|
||||
#endif
|
||||
#include <stdlib.h>
|
||||
#include <qglobal.h>
|
||||
#ifdef Q_OS_WIN
|
||||
# define _POSIX_
|
||||
# include <limits.h>
|
||||
# undef _POSIX_
|
||||
#endif
|
||||
#include <QtCore/QtCore>
|
||||
#ifndef Q_OS_WIN
|
||||
#include <QtQuick/QtQuick>
|
||||
#endif
|
||||
#endif
|
||||
17
client/core/webview/plugin.cpp
Normal file
17
client/core/webview/plugin.cpp
Normal file
@@ -0,0 +1,17 @@
|
||||
#include "plugin.h"
|
||||
|
||||
#include "amneziawebview.h"
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
void WebViewPlugin::registerTypes(const char* uri)
|
||||
{
|
||||
#ifndef QT_NO_ACTION
|
||||
qmlRegisterAnonymousType<QAction>(uri, 1);
|
||||
#endif
|
||||
qmlRegisterAnonymousType<AmneziaWebViewSettings>(uri, 1);
|
||||
qmlRegisterType<AmneziaWebView>(uri, 1, 0, "AmneziaWebView");
|
||||
qmlRegisterRevision<AmneziaWebView, 0>("AmneziaWebView", 1, 0);
|
||||
}
|
||||
|
||||
QT_END_NAMESPACE
|
||||
15
client/core/webview/plugin.h
Normal file
15
client/core/webview/plugin.h
Normal file
@@ -0,0 +1,15 @@
|
||||
#include <QQmlExtensionPlugin>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
class WebViewPlugin : public QQmlExtensionPlugin
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface" FILE "webview.json")
|
||||
Q_INTERFACES(QQmlExtensionInterface)
|
||||
|
||||
public:
|
||||
void registerTypes(const char* uri) override;
|
||||
};
|
||||
|
||||
QT_END_NAMESPACE
|
||||
2
client/core/webview/qmldir
Normal file
2
client/core/webview/qmldir
Normal file
@@ -0,0 +1,2 @@
|
||||
module AmneziaWebView
|
||||
plugin webview
|
||||
37
client/core/webview/qrchandler.cpp
Normal file
37
client/core/webview/qrchandler.cpp
Normal file
@@ -0,0 +1,37 @@
|
||||
#include <QtCore/QString>
|
||||
#include <QtCore/QUrl>
|
||||
#include <QtCore/QFile>
|
||||
#include <QtCore/QByteArray>
|
||||
|
||||
#include "qrchandler.h"
|
||||
#include "mimecache.h"
|
||||
|
||||
QString QrcHandler::scheme()
|
||||
{
|
||||
return QString("qrc");
|
||||
}
|
||||
|
||||
QByteArray QrcHandler::dataForUrl(const QUrl &url) const
|
||||
{
|
||||
QString requestUrl(QString(":/%1").arg(url.path()));
|
||||
QFile resource(requestUrl);
|
||||
QByteArray buffer;
|
||||
if (resource.exists() && resource.open(QIODevice::ReadOnly)) {
|
||||
|
||||
buffer = resource.readAll();
|
||||
resource.close();
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
bool QrcHandler::canHandleUrl(const QUrl &url) const
|
||||
{
|
||||
if (scheme() == url.scheme())
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
QString QrcHandler::mimeTypeForUrl(const QUrl &url) const
|
||||
{
|
||||
return mimeTypeForExtension(url.path().section('.', -1));
|
||||
}
|
||||
18
client/core/webview/qrchandler.h
Normal file
18
client/core/webview/qrchandler.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#ifndef QRCHANDLER_H
|
||||
#define QRCHANDLER_H
|
||||
|
||||
class QString;
|
||||
class QByteArray;
|
||||
class QUrl;
|
||||
|
||||
class QrcHandler
|
||||
{
|
||||
public:
|
||||
explicit QrcHandler();
|
||||
static QString scheme();
|
||||
bool canHandleUrl(const QUrl &url) const;
|
||||
QByteArray dataForUrl(const QUrl &url) const;
|
||||
QString mimeTypeForUrl(const QUrl &url) const;
|
||||
};
|
||||
|
||||
#endif
|
||||
10
client/core/webview/qrchandler_android.cpp
Normal file
10
client/core/webview/qrchandler_android.cpp
Normal file
@@ -0,0 +1,10 @@
|
||||
#include <QtCore/QString>
|
||||
#include <QtCore/QUrl>
|
||||
#include <QtCore/QFile>
|
||||
#include <QtCore/QByteArray>
|
||||
|
||||
#include "qrchandler.h"
|
||||
#include "mimecache.h"
|
||||
|
||||
QrcHandler::QrcHandler()
|
||||
{}
|
||||
189
client/core/webview/qrchandler_ios.mm
Normal file
189
client/core/webview/qrchandler_ios.mm
Normal file
@@ -0,0 +1,189 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <MobileCoreServices/UTCoreTypes.h>
|
||||
#import <MobileCoreServices/UTType.h>
|
||||
|
||||
#include <QtCore/QFile>
|
||||
#include <QtCore/QUrl>
|
||||
#include <QtCore/QString>
|
||||
#include <QtCore/QDebug>
|
||||
#include "mimecache.h"
|
||||
|
||||
#include "qrchandler.h"
|
||||
#define kProtocolQrcScheme @"qrc"
|
||||
|
||||
#if !defined(ENABLE_WKWEBVIEW)
|
||||
@interface QrcProtocol : NSURLProtocol
|
||||
|
||||
+ (NSString*)protocolScheme;
|
||||
+ (NSString*)requestVarsKey;
|
||||
+ (void)registerProtocol;
|
||||
|
||||
@end
|
||||
|
||||
@interface NSURLRequest (QrcProtocol)
|
||||
- (NSDictionary *)requestVars;
|
||||
@end
|
||||
|
||||
@interface NSMutableURLRequest (QrcProtocol)
|
||||
- (void)setRequestVars:(NSDictionary *)vars;
|
||||
@end
|
||||
#endif
|
||||
|
||||
QrcHandler::QrcHandler()
|
||||
{
|
||||
#if !defined(ENABLE_WKWEBVIEW)
|
||||
[QrcProtocol registerProtocol];
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !defined(ENABLE_WKWEBVIEW)
|
||||
|
||||
@implementation NSURLRequest (QrcProtocol)
|
||||
|
||||
- (NSDictionary *)requestVars {
|
||||
NSLog(@"%@ received %@", self, NSStringFromSelector(_cmd));
|
||||
return [NSURLProtocol propertyForKey:[QrcProtocol requestVarsKey] inRequest:self];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation NSMutableURLRequest (QrcProtocol)
|
||||
|
||||
- (void)setRequestVars:(NSDictionary *)requestVars {
|
||||
|
||||
NSLog(@"%@ received %@", self, NSStringFromSelector(_cmd));
|
||||
|
||||
NSDictionary *specialVarsCopy = [requestVars copy];
|
||||
[NSURLProtocol setProperty:specialVarsCopy forKey:[QrcProtocol requestVarsKey] inRequest:self];
|
||||
[specialVarsCopy release];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation QrcProtocol
|
||||
|
||||
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
|
||||
{
|
||||
NSString *scheme = request.URL.scheme;
|
||||
if ([kProtocolQrcScheme caseInsensitiveCompare:scheme] == NSOrderedSame) {
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
|
||||
{
|
||||
return request;
|
||||
}
|
||||
|
||||
+ (NSString*) protocolScheme
|
||||
{
|
||||
return kProtocolQrcScheme;
|
||||
}
|
||||
|
||||
+ (NSString*) requestVarsKey {
|
||||
return @"requestVars";
|
||||
}
|
||||
|
||||
+ (void)registerProtocol
|
||||
{
|
||||
static bool qrcProtocolRegistered = false;
|
||||
if (!qrcProtocolRegistered) {
|
||||
[NSURLProtocol registerClass:[QrcProtocol class]];
|
||||
qrcProtocolRegistered = true;
|
||||
}
|
||||
}
|
||||
|
||||
+ (NSString*)headFromHtml:(NSString*)html
|
||||
{
|
||||
if (!html) return nil;
|
||||
|
||||
NSString *head = nil;
|
||||
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"(?<=<head>)[\\w\\W.]*(?=</head>)" options:NSRegularExpressionCaseInsensitive error:nil];
|
||||
NSTextCheckingResult *headResult = [regex firstMatchInString:html options:0 range:NSMakeRange(0, html.length)];
|
||||
|
||||
if (headResult && headResult.range.location != NSNotFound) {
|
||||
head = [html substringWithRange:[headResult range]];
|
||||
}
|
||||
return head;
|
||||
}
|
||||
|
||||
+ (NSString *)charsetFromHtml:(NSString *)html
|
||||
{
|
||||
if (!html) return nil;
|
||||
|
||||
NSString *charset = nil;
|
||||
NSString *charsetPattern = @"((?<=charset=)\\s*[a-zA-Z0-9-]*)";
|
||||
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:charsetPattern options: NSRegularExpressionCaseInsensitive error:nil];
|
||||
NSTextCheckingResult *charsetResult = [regex firstMatchInString:html options:kNilOptions range:NSMakeRange(0, [html length])];
|
||||
if (charsetResult && charsetResult.range.location != NSNotFound) {
|
||||
charset = [[html substringWithRange:[charsetResult range]] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||
charset = [charset lowercaseString];
|
||||
}
|
||||
return charset;
|
||||
}
|
||||
|
||||
|
||||
- (void)startLoading
|
||||
{
|
||||
/* retrieve the current request. */
|
||||
NSURLRequest *request = [self request];
|
||||
NSString *url = [[request URL] path];
|
||||
//NSString *absoluteString = request.URL.absoluteString;
|
||||
|
||||
//NSString *mimeType = [self mimeTypeForExtension: [url pathExtension]];
|
||||
NSString *mimeType = mimeTypeForExtension(QString::fromNSString(url.pathExtension)).toNSString();
|
||||
|
||||
/* extract our special variables from the request. */
|
||||
//NSDictionary *requestVars = [request requestVars];
|
||||
NSData *pageData = nil;
|
||||
|
||||
QString requestUrl(QString(":/%1").arg(QString::fromNSString(url)));
|
||||
QFile resource(requestUrl);
|
||||
if (resource.exists() && resource.open(QIODevice::ReadOnly)) {
|
||||
|
||||
QByteArray buffer = resource.readAll();
|
||||
|
||||
//pageData = [[NSData alloc] initWithBytes:buffer.constData() length:buffer.size()];
|
||||
pageData = [NSData dataWithBytes:buffer.constData() length:buffer.size()];
|
||||
|
||||
resource.close();
|
||||
}
|
||||
|
||||
if (pageData) {
|
||||
|
||||
NSString *encoding = @"utf-8";
|
||||
if ([mimeType isEqualToString:@"text/html"]) {
|
||||
|
||||
NSString *content = [[NSString alloc] initWithBytesNoCopy: (char* )[pageData bytes] length:pageData.length encoding:NSISOLatin1StringEncoding freeWhenDone:NO];
|
||||
|
||||
NSString *charset = [QrcProtocol charsetFromHtml:[QrcProtocol headFromHtml:content]];
|
||||
if (charset) {
|
||||
encoding = charset;
|
||||
}
|
||||
[content autorelease];
|
||||
}
|
||||
|
||||
NSURLResponse *response = [[NSURLResponse alloc]initWithURL:self.request.URL
|
||||
MIMEType:mimeType
|
||||
expectedContentLength:[pageData length]
|
||||
textEncodingName:encoding];
|
||||
|
||||
[[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
|
||||
[[self client] URLProtocol:self didLoadData:pageData];
|
||||
[[self client] URLProtocolDidFinishLoading:self];
|
||||
[response autorelease];
|
||||
}
|
||||
else {
|
||||
[[self client] URLProtocol:self didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil]];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)stopLoading
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
202
client/core/webview/websettings.cpp
Normal file
202
client/core/webview/websettings.cpp
Normal file
@@ -0,0 +1,202 @@
|
||||
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QFont>
|
||||
#include <QGuiApplication>
|
||||
#include <QHash>
|
||||
#include <QSharedData>
|
||||
#include <QStandardPaths>
|
||||
#include <QUrl>
|
||||
|
||||
#include "websettings.h"
|
||||
#include "amneziawebview_p.h"
|
||||
|
||||
AmneziaWebViewSettings::AmneziaWebViewSettings(AmneziaWebView *view): QObject(view), s(new WebSettings(view))
|
||||
{}
|
||||
|
||||
Q_GLOBAL_STATIC(QList<WebSettings*>, allSettings)
|
||||
|
||||
void WebSettings::apply()
|
||||
{
|
||||
if (view) {
|
||||
|
||||
WebSettings* global = WebSettings::globalSettings();
|
||||
|
||||
|
||||
QString family = fontFamilies.value(WebSettings::StandardFont,
|
||||
global->fontFamilies.value(WebSettings::StandardFont));
|
||||
view->setStandardFontFamily(family);
|
||||
|
||||
|
||||
int size = fontSizes.value(WebSettings::DefaultFontSize,
|
||||
global->fontSizes.value(WebSettings::DefaultFontSize));
|
||||
view->setDefaultFontSize(size);
|
||||
|
||||
bool value = attributes.value(WebSettings::AutoLoadImages,
|
||||
global->attributes.value(WebSettings::AutoLoadImages));
|
||||
|
||||
QString encoding = !defaultTextEncoding.isEmpty() ? defaultTextEncoding: global->defaultTextEncoding;
|
||||
|
||||
|
||||
Q_UNUSED(value)
|
||||
|
||||
} else {
|
||||
|
||||
QList<WebSettings*> settings = *::allSettings();
|
||||
for (int i = 0; i < settings.count(); ++i)
|
||||
settings[i]->apply();
|
||||
}
|
||||
}
|
||||
|
||||
WebSettings* WebSettings::globalSettings()
|
||||
{
|
||||
static WebSettings *global = nullptr;
|
||||
if (!global) {
|
||||
global = new WebSettings;
|
||||
}
|
||||
return global;
|
||||
}
|
||||
|
||||
WebSettings::WebSettings()
|
||||
{
|
||||
// Initialize our global defaults
|
||||
fontSizes.insert(WebSettings::MinimumFontSize, 0);
|
||||
fontSizes.insert(WebSettings::MinimumLogicalFontSize, 0);
|
||||
fontSizes.insert(WebSettings::DefaultFontSize, 16);
|
||||
fontSizes.insert(WebSettings::DefaultFixedFontSize, 13);
|
||||
|
||||
QFont defaultFont;
|
||||
defaultFont.setStyleHint(QFont::Serif);
|
||||
fontFamilies.insert(WebSettings::StandardFont, defaultFont.defaultFamily());
|
||||
fontFamilies.insert(WebSettings::SerifFont, defaultFont.defaultFamily());
|
||||
|
||||
defaultFont.setStyleHint(QFont::Fantasy);
|
||||
fontFamilies.insert(WebSettings::FantasyFont, defaultFont.defaultFamily());
|
||||
|
||||
defaultFont.setStyleHint(QFont::Cursive);
|
||||
fontFamilies.insert(WebSettings::CursiveFont, defaultFont.defaultFamily());
|
||||
|
||||
defaultFont.setStyleHint(QFont::SansSerif);
|
||||
fontFamilies.insert(WebSettings::SansSerifFont, defaultFont.defaultFamily());
|
||||
|
||||
defaultFont.setStyleHint(QFont::Monospace);
|
||||
fontFamilies.insert(WebSettings::FixedFont, defaultFont.defaultFamily());
|
||||
|
||||
attributes.insert(WebSettings::AutoLoadImages, true);
|
||||
attributes.insert(WebSettings::DnsPrefetchEnabled, false);
|
||||
attributes.insert(WebSettings::JavascriptEnabled, true);
|
||||
attributes.insert(WebSettings::SpatialNavigationEnabled, false);
|
||||
attributes.insert(WebSettings::LinksIncludedInFocusChain, true);
|
||||
attributes.insert(WebSettings::ZoomTextOnly, false);
|
||||
attributes.insert(WebSettings::PrintElementBackgrounds, true);
|
||||
attributes.insert(WebSettings::OfflineStorageDatabaseEnabled, false);
|
||||
attributes.insert(WebSettings::OfflineWebApplicationCacheEnabled, false);
|
||||
attributes.insert(WebSettings::LocalStorageEnabled, false);
|
||||
attributes.insert(WebSettings::LocalContentCanAccessRemoteUrls, false);
|
||||
attributes.insert(WebSettings::LocalContentCanAccessFileUrls, true);
|
||||
attributes.insert(WebSettings::AcceleratedCompositingEnabled, true);
|
||||
attributes.insert(WebSettings::WebGLEnabled, true);
|
||||
attributes.insert(WebSettings::WebAudioEnabled, false);
|
||||
attributes.insert(WebSettings::CSSRegionsEnabled, true);
|
||||
attributes.insert(WebSettings::CSSGridLayoutEnabled, false);
|
||||
attributes.insert(WebSettings::HyperlinkAuditingEnabled, false);
|
||||
attributes.insert(WebSettings::TiledBackingStoreEnabled, false);
|
||||
attributes.insert(WebSettings::FrameFlatteningEnabled, false);
|
||||
attributes.insert(WebSettings::SiteSpecificQuirksEnabled, true);
|
||||
attributes.insert(WebSettings::ScrollAnimatorEnabled, false);
|
||||
attributes.insert(WebSettings::CaretBrowsingEnabled, false);
|
||||
attributes.insert(WebSettings::NotificationsEnabled, true);
|
||||
|
||||
#if defined(Q_OS_WIN32) && defined(DEBUG)
|
||||
attributes.insert(WebSettings::DeveloperExtrasEnabled,true);
|
||||
#endif
|
||||
}
|
||||
|
||||
/*!
|
||||
\internal
|
||||
*/
|
||||
WebSettings::WebSettings(AmneziaWebView *v)
|
||||
: view(v)
|
||||
|
||||
{
|
||||
allSettings()->append(this);
|
||||
}
|
||||
|
||||
WebSettings::~WebSettings()
|
||||
{
|
||||
if (view)
|
||||
allSettings()->removeAll(this);
|
||||
|
||||
}
|
||||
|
||||
void WebSettings::setFontSize(FontSize type, int size)
|
||||
{
|
||||
fontSizes.insert(type, size);
|
||||
apply();
|
||||
}
|
||||
|
||||
int WebSettings::fontSize(FontSize type) const
|
||||
{
|
||||
int defaultValue = 0;
|
||||
if (view) {
|
||||
WebSettings* global = WebSettings::globalSettings();
|
||||
defaultValue = global->fontSizes.value(type);
|
||||
}
|
||||
return fontSizes.value(type, defaultValue);
|
||||
}
|
||||
|
||||
void WebSettings::resetFontSize(FontSize type)
|
||||
{
|
||||
if (view) {
|
||||
fontSizes.remove(type);
|
||||
apply();
|
||||
}
|
||||
}
|
||||
|
||||
void WebSettings::setFontFamily(FontFamily which, const QString& family)
|
||||
{
|
||||
fontFamilies.insert(which, family);
|
||||
apply();
|
||||
}
|
||||
|
||||
QString WebSettings::fontFamily(FontFamily which) const
|
||||
{
|
||||
QString defaultValue;
|
||||
if (view) {
|
||||
WebSettings* global = WebSettings::globalSettings();
|
||||
defaultValue = global->fontFamilies.value(which);
|
||||
}
|
||||
return fontFamilies.value(which, defaultValue);
|
||||
}
|
||||
|
||||
void WebSettings::resetFontFamily(FontFamily which)
|
||||
{
|
||||
if (view) {
|
||||
fontFamilies.remove(which);
|
||||
apply();
|
||||
}
|
||||
}
|
||||
|
||||
void WebSettings::setAttribute(WebAttribute attr, bool on)
|
||||
{
|
||||
attributes.insert(attr, on);
|
||||
apply();
|
||||
}
|
||||
|
||||
bool WebSettings::testAttribute(WebAttribute attr) const
|
||||
{
|
||||
bool defaultValue = false;
|
||||
if (view) {
|
||||
WebSettings* global = WebSettings::globalSettings();
|
||||
defaultValue = global->attributes.value(attr);
|
||||
}
|
||||
return attributes.value(attr, defaultValue);
|
||||
}
|
||||
|
||||
void WebSettings::resetAttribute(WebAttribute attr)
|
||||
{
|
||||
if (view) {
|
||||
attributes.remove(attr);
|
||||
apply();
|
||||
}
|
||||
}
|
||||
144
client/core/webview/websettings.h
Normal file
144
client/core/webview/websettings.h
Normal file
@@ -0,0 +1,144 @@
|
||||
#ifndef WEBSETTINGS_H
|
||||
#define WEBSETTINGS_H
|
||||
|
||||
#include <QtQml>
|
||||
|
||||
class AmneziaWebView;
|
||||
class AmneziaWebViewPrivate;
|
||||
class WebSettingsData;
|
||||
|
||||
class WebSettings
|
||||
{
|
||||
public:
|
||||
enum FontFamily {
|
||||
StandardFont,
|
||||
FixedFont,
|
||||
SerifFont,
|
||||
SansSerifFont,
|
||||
CursiveFont,
|
||||
FantasyFont
|
||||
};
|
||||
enum WebAttribute {
|
||||
AutoLoadImages,
|
||||
JavascriptEnabled,
|
||||
JavaEnabled,
|
||||
PluginsEnabled,
|
||||
PrivateBrowsingEnabled,
|
||||
JavascriptCanOpenWindows,
|
||||
JavascriptCanAccessClipboard,
|
||||
DeveloperExtrasEnabled,
|
||||
LinksIncludedInFocusChain,
|
||||
ZoomTextOnly,
|
||||
PrintElementBackgrounds,
|
||||
OfflineStorageDatabaseEnabled,
|
||||
OfflineWebApplicationCacheEnabled,
|
||||
LocalStorageEnabled,
|
||||
LocalContentCanAccessRemoteUrls,
|
||||
DnsPrefetchEnabled,
|
||||
XSSAuditingEnabled,
|
||||
AcceleratedCompositingEnabled,
|
||||
SpatialNavigationEnabled,
|
||||
LocalContentCanAccessFileUrls,
|
||||
TiledBackingStoreEnabled,
|
||||
FrameFlatteningEnabled,
|
||||
SiteSpecificQuirksEnabled,
|
||||
JavascriptCanCloseWindows,
|
||||
WebGLEnabled,
|
||||
CSSRegionsEnabled,
|
||||
HyperlinkAuditingEnabled,
|
||||
CSSGridLayoutEnabled,
|
||||
ScrollAnimatorEnabled,
|
||||
CaretBrowsingEnabled,
|
||||
NotificationsEnabled,
|
||||
WebAudioEnabled
|
||||
};
|
||||
enum WebGraphic {
|
||||
MissingImageGraphic,
|
||||
MissingPluginGraphic,
|
||||
DefaultFrameIconGraphic,
|
||||
TextAreaSizeGripCornerGraphic,
|
||||
DeleteButtonGraphic,
|
||||
InputSpeechButtonGraphic,
|
||||
SearchCancelButtonGraphic,
|
||||
SearchCancelButtonPressedGraphic
|
||||
};
|
||||
enum FontSize {
|
||||
MinimumFontSize,
|
||||
MinimumLogicalFontSize,
|
||||
DefaultFontSize,
|
||||
DefaultFixedFontSize
|
||||
};
|
||||
enum ThirdPartyCookiePolicy {
|
||||
AlwaysAllowThirdPartyCookies,
|
||||
AlwaysBlockThirdPartyCookies,
|
||||
AllowThirdPartyWithExistingCookies
|
||||
};
|
||||
|
||||
static WebSettings *globalSettings();
|
||||
|
||||
void setFontSize(FontSize type, int size);
|
||||
int fontSize(FontSize type) const;
|
||||
void resetFontSize(FontSize type);
|
||||
|
||||
|
||||
void setFontFamily(FontFamily which, const QString &family);
|
||||
QString fontFamily(FontFamily which) const;
|
||||
void resetFontFamily(FontFamily which);
|
||||
|
||||
void setAttribute(WebAttribute attr, bool on);
|
||||
bool testAttribute(WebAttribute attr) const;
|
||||
void resetAttribute(WebAttribute attr);
|
||||
|
||||
void apply();
|
||||
|
||||
WebSettings();
|
||||
explicit WebSettings(AmneziaWebView *v);
|
||||
virtual ~WebSettings();
|
||||
|
||||
private:
|
||||
friend class WebSettingsData;
|
||||
friend class AmneziaWebViewPrivate;
|
||||
friend class WebViewPrivate;
|
||||
|
||||
|
||||
|
||||
Q_DISABLE_COPY(WebSettings)
|
||||
|
||||
QHash<int, QString> fontFamilies;
|
||||
QHash<int, int> fontSizes;
|
||||
QHash<int, bool> attributes;
|
||||
QString defaultTextEncoding;
|
||||
AmneziaWebView *view;
|
||||
|
||||
};
|
||||
|
||||
class AmneziaWebViewSettings : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(int defaultFontSize READ defaultFontSize WRITE setDefaultFontSize)
|
||||
Q_PROPERTY(QString standardFontFamily READ standardFontFamily WRITE setStandardFontFamily)
|
||||
Q_PROPERTY(bool developerExtrasEnabled READ developerExtrasEnabled WRITE setDeveloperExtrasEnabled)
|
||||
|
||||
public:
|
||||
explicit AmneziaWebViewSettings(AmneziaWebView *parent);
|
||||
|
||||
int defaultFontSize() const { return s->fontSize(WebSettings::DefaultFontSize); }
|
||||
void setDefaultFontSize(int size) { s->setFontSize(WebSettings::DefaultFontSize, size); }
|
||||
|
||||
QString standardFontFamily() const { return s->fontFamily(WebSettings::StandardFont); }
|
||||
void setStandardFontFamily(const QString& f) { s->setFontFamily(WebSettings::StandardFont, f); }
|
||||
|
||||
bool developerExtrasEnabled() const { return s->testAttribute(WebSettings::DeveloperExtrasEnabled); }
|
||||
void setDeveloperExtrasEnabled(bool on) { s->setAttribute(WebSettings::DeveloperExtrasEnabled, on); }
|
||||
|
||||
void apply() { s->apply(); }
|
||||
|
||||
private:
|
||||
QScopedPointer<WebSettings> s;
|
||||
};
|
||||
|
||||
QML_DECLARE_TYPE(AmneziaWebViewSettings)
|
||||
|
||||
|
||||
#endif
|
||||
3
client/core/webview/webview.json
Normal file
3
client/core/webview/webview.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"Keys": [ "AmneziaWebView" ]
|
||||
}
|
||||
@@ -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 |
@@ -16,7 +16,7 @@ set_target_properties(AmneziaVPNNetworkExtension PROPERTIES
|
||||
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_NAME "${BUILD_IOS_APP_IDENTIFIER}.network-extension"
|
||||
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS ${CMAKE_CURRENT_SOURCE_DIR}/AmneziaVPNNetworkExtension.entitlements
|
||||
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_APPLICATION_EXTENSION_API_ONLY "YES"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<string>AmneziaVPNNetworkExtension</string>
|
||||
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<string>org.amnezia.AmneziaVPN.network-extension</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
@@ -16,9 +16,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<string>${APPLE_PROJECT_VERSION}</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<string>${CMAKE_PROJECT_VERSION_TWEAK}</string>
|
||||
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
@@ -41,6 +41,6 @@
|
||||
<string>group.org.amnezia.AmneziaVPN</string>
|
||||
|
||||
<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>
|
||||
</plist>
|
||||
|
||||
@@ -270,7 +270,12 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
|
||||
&& !wgConfig.value(amnezia::config_key::initPacketMagicHeader).isUndefined()
|
||||
&& !wgConfig.value(amnezia::config_key::responsePacketMagicHeader).isUndefined()
|
||||
&& !wgConfig.value(amnezia::config_key::underloadPacketMagicHeader).isUndefined()
|
||||
&& !wgConfig.value(amnezia::config_key::transportPacketMagicHeader).isUndefined()) {
|
||||
&& !wgConfig.value(amnezia::config_key::transportPacketMagicHeader).isUndefined()
|
||||
&& !wgConfig.value(amnezia::config_key::specialJunk1).isUndefined()
|
||||
&& !wgConfig.value(amnezia::config_key::specialJunk2).isUndefined()
|
||||
&& !wgConfig.value(amnezia::config_key::specialJunk3).isUndefined()
|
||||
&& !wgConfig.value(amnezia::config_key::specialJunk4).isUndefined()
|
||||
&& !wgConfig.value(amnezia::config_key::specialJunk5).isUndefined()) {
|
||||
json.insert(amnezia::config_key::junkPacketCount, wgConfig.value(amnezia::config_key::junkPacketCount));
|
||||
json.insert(amnezia::config_key::junkPacketMinSize, wgConfig.value(amnezia::config_key::junkPacketMinSize));
|
||||
json.insert(amnezia::config_key::junkPacketMaxSize, wgConfig.value(amnezia::config_key::junkPacketMaxSize));
|
||||
|
||||
@@ -72,9 +72,9 @@ void NetworkWatcher::initialize() {
|
||||
connect(m_impl, &NetworkWatcherImpl::unsecuredNetwork, this,
|
||||
&NetworkWatcher::unsecuredNetwork);
|
||||
connect(m_impl, &NetworkWatcherImpl::networkChanged, this,
|
||||
&NetworkWatcher::networkChanged);
|
||||
connect(m_impl, &NetworkWatcherImpl::wakeup, this,
|
||||
&NetworkWatcher::wakeup);
|
||||
&NetworkWatcher::networkChange);
|
||||
connect(m_impl, &NetworkWatcherImpl::sleepMode, this,
|
||||
&NetworkWatcher::onSleepMode);
|
||||
m_impl->initialize();
|
||||
|
||||
// Enable sleep/wake monitoring for VPN auto-reconnection
|
||||
@@ -97,6 +97,12 @@ void NetworkWatcher::settingsChanged() {
|
||||
logger.debug() << "NetworkWatcher settings changed - keeping sleep monitoring active";
|
||||
}
|
||||
|
||||
void NetworkWatcher::onSleepMode()
|
||||
{
|
||||
logger.debug() << "Resumed from sleep mode";
|
||||
emit sleepMode();
|
||||
}
|
||||
|
||||
void NetworkWatcher::unsecuredNetwork(const QString& networkName,
|
||||
const QString& networkId) {
|
||||
logger.debug() << "Unsecured network:" << logger.sensitive(networkName)
|
||||
|
||||
@@ -29,11 +29,13 @@ public:
|
||||
// false to restore.
|
||||
void simulateDisconnection(bool simulatedDisconnection);
|
||||
|
||||
void onSleepMode();
|
||||
|
||||
QNetworkInformation::Reachability getReachability();
|
||||
|
||||
signals:
|
||||
void networkChanged();
|
||||
void wakeup();
|
||||
void networkChange();
|
||||
void sleepMode();
|
||||
|
||||
private:
|
||||
void settingsChanged();
|
||||
|
||||
@@ -41,7 +41,7 @@ signals:
|
||||
// TODO: Only windows-networkwatcher has this, the other plattforms should
|
||||
// too.
|
||||
void networkChanged(QString newBSSID);
|
||||
void wakeup();
|
||||
void sleepMode();
|
||||
|
||||
|
||||
private:
|
||||
|
||||
@@ -101,9 +101,7 @@ bool AndroidController::initialize()
|
||||
{"onAuthResult", "(Z)V", reinterpret_cast<void *>(onAuthResult)},
|
||||
{"decodeQrCode", "(Ljava/lang/String;)Z", reinterpret_cast<bool *>(decodeQrCode)},
|
||||
{"onImeInsetsChanged", "(I)V", reinterpret_cast<void *>(onImeInsetsChanged)},
|
||||
{"onSystemBarsInsetsChanged", "(II)V", reinterpret_cast<void *>(onSystemBarsInsetsChanged)},
|
||||
{"onActivityPaused", "()V", reinterpret_cast<void *>(onActivityPaused)},
|
||||
{"onActivityResumed", "()V", reinterpret_cast<void *>(onActivityResumed)}
|
||||
{"onSystemBarsInsetsChanged", "(II)V", reinterpret_cast<void *>(onSystemBarsInsetsChanged)}
|
||||
};
|
||||
|
||||
QJniEnvironment env;
|
||||
@@ -560,22 +558,3 @@ void AndroidController::onSystemBarsInsetsChanged(JNIEnv *env, jobject thiz, jin
|
||||
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 imeInsetsChanged(int heightDp);
|
||||
void systemBarsInsetsChanged(int navBarHeightDp, int statusBarHeightDp);
|
||||
void activityPaused();
|
||||
void activityResumed();
|
||||
|
||||
private:
|
||||
bool isWaitingStatus = true;
|
||||
@@ -107,8 +105,6 @@ private:
|
||||
static bool decodeQrCode(JNIEnv *env, jobject thiz, jstring data);
|
||||
static void onImeInsetsChanged(JNIEnv *env, jobject thiz, jint heightDp);
|
||||
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>
|
||||
static auto callActivityMethod(const char *methodName, const char *signature, Args &&...args);
|
||||
|
||||
@@ -15,12 +15,6 @@ struct OpenVPNConfig: Decodable {
|
||||
|
||||
extension PacketTunnelProvider {
|
||||
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,
|
||||
let providerConfiguration = protocolConfiguration.providerConfiguration,
|
||||
let openVPNConfigData = providerConfiguration[Constants.ovpnConfigKey] as? Data else {
|
||||
@@ -31,25 +25,7 @@ extension PacketTunnelProvider {
|
||||
do {
|
||||
let openVPNConfig = try JSONDecoder().decode(OpenVPNConfig.self, from: openVPNConfigData)
|
||||
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)
|
||||
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)
|
||||
} catch {
|
||||
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()
|
||||
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 hasTlsAuthClose = configString.contains("</tls-auth>")
|
||||
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: "ConfigTail", message: tail)
|
||||
|
||||
if hasTlsAuthOpen && hasTlsAuthClose {
|
||||
ovpnLog(.info, title: "TLSAuthSanitized", message: "preserve original tls-auth block")
|
||||
if let start = configString.range(of: "<tls-auth>"),
|
||||
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")
|
||||
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 normalizedConfig = configString.replacingOccurrences(of: "\r\n", with: "\n")
|
||||
let sanitizedData = Data(normalizedConfig.utf8)
|
||||
|
||||
let configuration = OpenVPNConfiguration()
|
||||
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") {
|
||||
configuration.setPTCloak()
|
||||
}
|
||||
@@ -224,15 +124,11 @@ extension PacketTunnelProvider {
|
||||
if evaluation?.autologin == false {
|
||||
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
|
||||
self?.handleOpenVPNReachabilityChange(status)
|
||||
guard status == .reachableViaWiFi else { return }
|
||||
self?.ovpnAdapter?.reconnect(afterTimeInterval: 5)
|
||||
}
|
||||
#endif
|
||||
|
||||
startHandler = completionHandler
|
||||
ovpnAdapter?.connect(using: openVPNPacketFlow())
|
||||
@@ -248,8 +144,6 @@ extension PacketTunnelProvider {
|
||||
return
|
||||
}
|
||||
|
||||
ovpnLog(.info, title: "Transport", message: "bytesIn=\(bytesin) bytesOut=\(bytesout)")
|
||||
|
||||
let response: [String: Any] = [
|
||||
"rx_bytes": bytesin,
|
||||
"tx_bytes": bytesout
|
||||
@@ -262,10 +156,6 @@ extension PacketTunnelProvider {
|
||||
ovpnLog(.info, message: "Stopping tunnel: reason: \(reason.amneziaDescription)")
|
||||
|
||||
stopHandler = completionHandler
|
||||
openVpnGatewayAddress = nil
|
||||
openVpnLocalAddress = nil
|
||||
openVpnLocalMask = nil
|
||||
lastOpenVPNSettings = nil
|
||||
if vpnReachability.isTracking {
|
||||
vpnReachability.stopTracking()
|
||||
}
|
||||
@@ -285,99 +175,11 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
|
||||
configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings?,
|
||||
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
|
||||
// send empty string to NEDNSSettings.matchDomains
|
||||
if let dnsSettings = effectiveSettings.dnsSettings {
|
||||
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
|
||||
}
|
||||
networkSettings?.dnsSettings?.matchDomains = [""]
|
||||
|
||||
effectiveSettings.dnsSettings?.matchDomains = [""]
|
||||
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 {
|
||||
if splitTunnelType == 1 {
|
||||
var ipv4IncludedRoutes = [NEIPv4Route]()
|
||||
|
||||
guard let splitTunnelSites else {
|
||||
@@ -393,8 +195,9 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
effectiveSettings.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
|
||||
} else if splitType == 2 {
|
||||
networkSettings?.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
|
||||
} else {
|
||||
if splitTunnelType == 2 {
|
||||
var ipv4ExcludedRoutes = [NEIPv4Route]()
|
||||
var ipv4IncludedRoutes = [NEIPv4Route]()
|
||||
var ipv6IncludedRoutes = [NEIPv6Route]()
|
||||
@@ -422,418 +225,14 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
|
||||
destinationAddress: "\(allIPv6.address)",
|
||||
networkPrefixLength: NSNumber(value: allIPv6.networkPrefixLength)))
|
||||
}
|
||||
effectiveSettings.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
|
||||
effectiveSettings.ipv6Settings?.includedRoutes = ipv6IncludedRoutes
|
||||
effectiveSettings.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
|
||||
networkSettings?.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
|
||||
networkSettings?.ipv6Settings?.includedRoutes = ipv6IncludedRoutes
|
||||
networkSettings?.ipv4Settings?.excludedRoutes = ipv4ExcludedRoutes
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
setTunnelNetworkSettings(effectiveSettings) { error in
|
||||
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
|
||||
setTunnelNetworkSettings(networkSettings, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
// Process events returned by the OpenVPN library
|
||||
@@ -851,9 +250,6 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
|
||||
|
||||
startHandler(nil)
|
||||
self.startHandler = nil
|
||||
|
||||
logOpenVPNConnectionInfo()
|
||||
refreshOpenVPNSettingsAfterConnect()
|
||||
case .disconnected:
|
||||
guard let stopHandler = stopHandler else { return }
|
||||
|
||||
@@ -896,41 +292,4 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
|
||||
// Handle log messages
|
||||
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 NetworkExtension
|
||||
|
||||
@@ -7,7 +6,6 @@ enum XrayErrors: Error {
|
||||
case xrayConfigIsWrong
|
||||
case cantSaveXrayConfig
|
||||
case cantParseListenAndPort
|
||||
case cantAcquireLocalPort
|
||||
case cantSaveHevSocksConfig
|
||||
}
|
||||
|
||||
@@ -23,80 +21,6 @@ extension Constants {
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
// Xray configuration
|
||||
@@ -148,7 +72,6 @@ extension PacketTunnelProvider {
|
||||
settings.dnsSettings = !dnsArray.isEmpty
|
||||
? NEDNSSettings(servers: dnsArray)
|
||||
: NEDNSSettings(servers: ["1.1.1.1"])
|
||||
applyXraySplitTunnel(xrayConfig, settings: settings)
|
||||
|
||||
let xrayConfigData = xrayConfig.config.data(using: .utf8)
|
||||
|
||||
@@ -167,11 +90,14 @@ extension PacketTunnelProvider {
|
||||
return
|
||||
}
|
||||
|
||||
let port = try acquireFreeLocalPort()
|
||||
let port = 10808
|
||||
let address = "::1"
|
||||
|
||||
// Extract existing SOCKS5 credentials or generate new ones per session.
|
||||
let socksCredentials = ensureInboundAuth(jsonDict: &jsonDict, port: port, address: address)
|
||||
if var inboundsArray = jsonDict["inbounds"] as? [[String: Any]], !inboundsArray.isEmpty {
|
||||
inboundsArray[0]["port"] = port
|
||||
inboundsArray[0]["listen"] = address
|
||||
jsonDict["inbounds"] = inboundsArray
|
||||
}
|
||||
|
||||
let updatedData = try JSONSerialization.data(withJSONObject: jsonDict, options: [])
|
||||
|
||||
@@ -194,8 +120,6 @@ extension PacketTunnelProvider {
|
||||
self?.setupAndRunTun2socks(configData: updatedData,
|
||||
address: address,
|
||||
port: port,
|
||||
username: socksCredentials.username,
|
||||
password: socksCredentials.password,
|
||||
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,
|
||||
completionHandler: @escaping (Error?) -> Void) {
|
||||
let path = Constants.cachesDirectory.appendingPathComponent("config.json", isDirectory: false).path
|
||||
@@ -307,8 +175,6 @@ extension PacketTunnelProvider {
|
||||
private func setupAndRunTun2socks(configData: Data,
|
||||
address: String,
|
||||
port: Int,
|
||||
username: String,
|
||||
password: String,
|
||||
completionHandler: @escaping (Error?) -> Void) {
|
||||
let config = """
|
||||
tunnel:
|
||||
@@ -316,8 +182,6 @@ extension PacketTunnelProvider {
|
||||
socks5:
|
||||
port: \(port)
|
||||
address: \(address)
|
||||
username: \(username)
|
||||
password: \(password)
|
||||
udp: 'udp'
|
||||
misc:
|
||||
task-stack-size: 20480
|
||||
|
||||
@@ -41,26 +41,13 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
var ovpnAdapter: OpenVPNAdapter?
|
||||
private lazy var openVPNPacketFlowAdapter = PacketTunnelFlowAdapter(flow: packetFlow)
|
||||
private let pathMonitorQueue = DispatchQueue(label: Constants.processQueueName + ".path-monitor")
|
||||
private let networkChangeQueue = DispatchQueue(label: Constants.processQueueName + ".network-change")
|
||||
private let pathMonitor = NWPathMonitor()
|
||||
private var didReceiveInitialPathUpdate = false
|
||||
private var currentPath: Network.NWPath?
|
||||
private var currentPathSignature: String?
|
||||
private var pendingOpenVPNReconnectWorkItem: DispatchWorkItem?
|
||||
private var pendingNetworkChangeWorkItem: DispatchWorkItem?
|
||||
private var isApplyingNetworkChange = false
|
||||
private var lastOpenVPNReachabilityStatus: OpenVPNReachabilityStatus?
|
||||
|
||||
var splitTunnelType: Int?
|
||||
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
|
||||
|
||||
let vpnReachability = OpenVPNReachability()
|
||||
|
||||
@@ -91,22 +78,14 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
|
||||
guard hasMeaningfulChange, let proto = self.protoType else { return }
|
||||
|
||||
// WireGuard/AWG and OpenVPN manages network changes internally in its own adapter.
|
||||
if proto == .wireguard || proto == .openvpn {
|
||||
// WireGuard/AWG manages network changes internally; avoid restarting the tunnel here.
|
||||
if proto == .wireguard {
|
||||
return
|
||||
}
|
||||
|
||||
if proto == .openvpn {
|
||||
self.scheduleOpenVPNReconnect(reason: "NWPath changed")
|
||||
return
|
||||
DispatchQueue.main.async {
|
||||
self.handle(networkChange: path) { _ in }
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -200,26 +179,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
let errorNotifier = ErrorNotifier(activationAttemptId: activationAttemptId)
|
||||
|
||||
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 {
|
||||
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 {
|
||||
protoType = .openvpn
|
||||
} else if (providerConfiguration?[Constants.wireGuardConfigKey] as? Data) != nil {
|
||||
@@ -235,8 +197,6 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
return
|
||||
}
|
||||
|
||||
cancelPendingOpenVPNReconnect()
|
||||
cancelPendingNetworkChangeHandling()
|
||||
didReceiveInitialPathUpdate = false
|
||||
updateActiveInterfaceIndexForCurrentPath()
|
||||
|
||||
@@ -255,9 +215,6 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
|
||||
|
||||
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
|
||||
cancelPendingOpenVPNReconnect()
|
||||
cancelPendingNetworkChangeHandling()
|
||||
|
||||
guard let protoType else {
|
||||
completionHandler()
|
||||
return
|
||||
@@ -302,111 +259,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
}
|
||||
|
||||
private func handle(networkChange changePath: Network.NWPath, completion: @escaping (Error?) -> Void) {
|
||||
guard protoType == .xray else {
|
||||
updateActiveInterfaceIndex(for: changePath)
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
updateActiveInterfaceIndex(for: changePath)
|
||||
reasserting = true
|
||||
xrayLog(.info, message: "Applying network change to xray tunnel")
|
||||
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
|
||||
wg_log(.info, message: "Tunnel restarted.")
|
||||
startTunnel(options: nil, completionHandler: completion)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,14 +271,8 @@ private extension PacketTunnelProvider {
|
||||
signatureComponents.append(path.isExpensive ? "exp" : "noexp")
|
||||
signatureComponents.append(path.isConstrained ? "con" : "nocon")
|
||||
|
||||
// Ignore loopback and tunnel-style `.other` interfaces so Xray does not
|
||||
// react to its own utun lifecycle as if the physical uplink changed.
|
||||
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
|
||||
let preferredTypes: [NWInterface.InterfaceType] = [.wiredEthernet, .wifi, .cellular, .loopback, .other]
|
||||
let sortedInterfaces = path.availableInterfaces.sorted { lhs, rhs in
|
||||
if lhs.type == rhs.type {
|
||||
return lhs.index < rhs.index
|
||||
}
|
||||
@@ -444,8 +293,8 @@ private extension PacketTunnelProvider {
|
||||
case .wiredEthernet: typeName = "ethernet"
|
||||
case .wifi: typeName = "wifi"
|
||||
case .cellular: typeName = "cellular"
|
||||
case .loopback, .other:
|
||||
continue
|
||||
case .loopback: typeName = "loopback"
|
||||
case .other: typeName = "other"
|
||||
@unknown default: typeName = "unknown"
|
||||
}
|
||||
signatureComponents.append("\(typeName):\(interface.index)")
|
||||
@@ -474,8 +323,6 @@ extension WireGuardLogLevel {
|
||||
|
||||
final class PacketTunnelFlowAdapter: NSObject, OpenVPNAdapterPacketFlow {
|
||||
private let flow: NEPacketTunnelFlow
|
||||
private var readLogCounter = 0
|
||||
private var writeLogCounter = 0
|
||||
|
||||
init(flow: NEPacketTunnelFlow) {
|
||||
self.flow = flow
|
||||
@@ -484,98 +331,15 @@ final class PacketTunnelFlowAdapter: NSObject, OpenVPNAdapterPacketFlow {
|
||||
|
||||
@objc(readPacketsWithCompletionHandler:)
|
||||
func readPackets(completionHandler: @escaping ([Data], [NSNumber]) -> Void) {
|
||||
flow.readPackets { packets, protocols in
|
||||
#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)
|
||||
}
|
||||
flow.readPackets(completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@objc(writePackets:withProtocols:)
|
||||
func writePackets(_ packets: [Data], withProtocols protocols: [NSNumber]) -> Bool {
|
||||
#if os(macOS)
|
||||
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)"
|
||||
flow.writePackets(packets, withProtocols: protocols)
|
||||
}
|
||||
}
|
||||
|
||||
extension NEPacketTunnelFlow: OpenVPNAdapterPacketFlow {}
|
||||
|
||||
extension NEProviderStopReason {
|
||||
var amneziaDescription: String {
|
||||
switch self {
|
||||
|
||||
@@ -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 <StoreKit/StoreKit.h>
|
||||
#import <AmneziaVPN-Swift.h>
|
||||
|
||||
#include <QtCore/QDebug>
|
||||
#include <QtCore/QString>
|
||||
|
||||
namespace
|
||||
{
|
||||
QString toQString(NSString *value)
|
||||
{
|
||||
return QString::fromUtf8((value ?: @"").UTF8String);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
+ (instancetype)sharedInstance
|
||||
@@ -35,9 +42,17 @@ API_AVAILABLE(ios(15.0), macos(12.0))
|
||||
- (instancetype)init API_AVAILABLE(ios(15.0), macos(12.0))
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
|
||||
}
|
||||
|
||||
- (void)purchaseProduct:(NSString *)productIdentifier
|
||||
completion:(void (^)(BOOL success,
|
||||
NSString *_Nullable transactionId,
|
||||
@@ -45,48 +60,41 @@ API_AVAILABLE(ios(15.0), macos(12.0))
|
||||
NSString *_Nullable originalTransactionId,
|
||||
NSError *_Nullable error))completion API_AVAILABLE(ios(15.0), macos(12.0))
|
||||
{
|
||||
qInfo().noquote() << "[IAP][StoreKit2] Starting purchase for" << QString::fromUtf8(productIdentifier.UTF8String);
|
||||
[[StoreKit2Helper shared] purchaseProductWithProductIdentifier:productIdentifier
|
||||
completion:^(BOOL success,
|
||||
NSString *transactionId,
|
||||
NSString *productId,
|
||||
NSString *originalTransactionId,
|
||||
NSError *error) {
|
||||
if (success) {
|
||||
qInfo().noquote() << "[IAP][StoreKit2] Purchase success. transactionId =" << toQString(transactionId)
|
||||
<< "originalTransactionId =" << toQString(originalTransactionId) << "productId =" << toQString(productId);
|
||||
} else if (error) {
|
||||
qWarning().noquote() << "[IAP][StoreKit2] Purchase failed:" << toQString(error.localizedDescription);
|
||||
self.purchaseCompletion = completion;
|
||||
|
||||
qInfo().noquote() << "[IAP][StoreKit] Starting purchase for" << QString::fromUtf8(productIdentifier.UTF8String);
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
[self performPurchaseAsync:productIdentifier];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)performPurchaseAsync:(NSString *)productIdentifier API_AVAILABLE(ios(15.0), macos(12.0))
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
@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,
|
||||
NSArray<NSDictionary *> *_Nullable restoredTransactions,
|
||||
NSError *_Nullable error))completion API_AVAILABLE(ios(15.0), macos(12.0))
|
||||
{
|
||||
[[StoreKit2Helper shared] fetchCurrentEntitlementsWithCompletion:^(BOOL success,
|
||||
NSArray<NSDictionary *> *entitlements,
|
||||
NSError *error) {
|
||||
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);
|
||||
}
|
||||
}];
|
||||
self.restoreCompletion = completion;
|
||||
self.restoredTransactions = [NSMutableArray array];
|
||||
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
|
||||
}
|
||||
|
||||
- (void)fetchProductsWithIdentifiers:(NSSet<NSString *> *)productIdentifiers
|
||||
@@ -94,21 +102,163 @@ API_AVAILABLE(ios(15.0), macos(12.0))
|
||||
NSArray<NSString *> *invalidIdentifiers,
|
||||
NSError *_Nullable error))completion API_AVAILABLE(ios(15.0), macos(12.0))
|
||||
{
|
||||
[[StoreKit2Helper shared] fetchProductsWithIdentifiers:productIdentifiers
|
||||
completion:^(NSArray<NSDictionary *> *products,
|
||||
NSArray<NSString *> *invalidIdentifiers,
|
||||
NSError *error) {
|
||||
if (!error) {
|
||||
for (NSDictionary *productInfo in products) {
|
||||
qInfo().noquote() << "[IAP][StoreKit2] Fetched product info" << toQString(productInfo[@"productId"])
|
||||
<< "price=" << toQString(productInfo[@"price"])
|
||||
<< "currency=" << toQString(productInfo[@"currencyCode"]);
|
||||
self.productsFetchCompletion = completion;
|
||||
self.productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIdentifiers];
|
||||
self.productsRequest.delegate = self;
|
||||
[self.productsRequest start];
|
||||
}
|
||||
|
||||
#pragma mark - SKProductsRequestDelegate / SKRequestDelegate
|
||||
|
||||
- (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) {
|
||||
completion(products ?: @[], invalidIdentifiers ?: @[], error);
|
||||
case SKPaymentTransactionStateFailed:
|
||||
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
|
||||
|
||||
@@ -3,7 +3,5 @@ import Foundation
|
||||
struct XrayConfig: Decodable {
|
||||
let dns1: String?
|
||||
let dns2: String?
|
||||
let splitTunnelType: Int?
|
||||
let splitTunnelSites: [String]?
|
||||
let config: String
|
||||
}
|
||||
|
||||
@@ -179,9 +179,8 @@ bool IosController::initialize()
|
||||
[NETunnelProviderManager loadAllFromPreferencesWithCompletionHandler:^(NSArray<NETunnelProviderManager *> * _Nullable managers, NSError * _Nullable error) {
|
||||
@try {
|
||||
if (error) {
|
||||
qWarning() << "IosController::initialize : loadAllFromPreferences failed:"
|
||||
<< [error.localizedDescription UTF8String]
|
||||
<< "domain:" << [error.domain UTF8String] << "code:" << error.code;
|
||||
qDebug() << "IosController::initialize : Error:" << [error.localizedDescription UTF8String];
|
||||
emit connectionStateChanged(Vpn::ConnectionState::Error);
|
||||
ok = false;
|
||||
return;
|
||||
}
|
||||
@@ -218,13 +217,16 @@ bool IosController::connectVpn(amnezia::Proto proto, const QJsonObject& configur
|
||||
m_rawConfig = configuration;
|
||||
m_serverAddress = configuration.value(config_key::hostName).toString().toNSString();
|
||||
|
||||
const QString serverDescription = configuration.value(config_key::description).toString().trimmed();
|
||||
QString tunnelName;
|
||||
if (serverDescription.isEmpty()) {
|
||||
tunnelName = ProtocolProps::protoToString(proto);
|
||||
} else {
|
||||
if (configuration.value(config_key::description).toString().isEmpty()) {
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -395,14 +397,8 @@ void IosController::vpnStatusDidChange(void *pNotification)
|
||||
{
|
||||
NETunnelProviderSession *session = (NETunnelProviderSession *)pNotification;
|
||||
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
if (!m_currentTunnel || (NETunnelProviderSession *)m_currentTunnel.connection != session) {
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "IosController::vpnStatusDidChange" << iosStatusToState(session.status) << session;
|
||||
if (session /* && session == TunnelManager.session */ ) {
|
||||
qDebug() << "IosController::vpnStatusDidChange" << iosStatusToState(session.status) << session;
|
||||
|
||||
if (session.status == NEVPNStatusDisconnected) {
|
||||
if (@available(iOS 16.0, *)) {
|
||||
@@ -516,6 +512,7 @@ void IosController::vpnStatusDidChange(void *pNotification)
|
||||
m_statusRequestInFlight = false;
|
||||
}
|
||||
emitConnectionStateIfChanged(nextState);
|
||||
}
|
||||
}
|
||||
|
||||
void IosController::vpnConfigurationDidChange(void *pNotification)
|
||||
@@ -549,16 +546,6 @@ bool IosController::setupOpenVPN()
|
||||
|
||||
QJsonDocument openVPNConfigDoc(openVPNConfig);
|
||||
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);
|
||||
}
|
||||
@@ -697,15 +684,6 @@ bool IosController::setupXray()
|
||||
QJsonObject finalConfig;
|
||||
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::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);
|
||||
|
||||
QJsonDocument finalConfigDoc(finalConfig);
|
||||
@@ -807,59 +785,11 @@ bool IosController::startOpenVPN(const QString &config)
|
||||
|
||||
NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init];
|
||||
tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID];
|
||||
QByteArray configUtf8 = config.toUtf8();
|
||||
NSData *ovpnConfigData = [NSData dataWithBytes:configUtf8.constData() length:configUtf8.size()];
|
||||
tunnelProtocol.providerConfiguration = @{@"ovpn": ovpnConfigData};
|
||||
tunnelProtocol.providerConfiguration = @{@"ovpn": [[NSString stringWithUTF8String:config.toStdString().c_str()] dataUsingEncoding:NSUTF8StringEncoding]};
|
||||
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;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -869,9 +799,7 @@ bool IosController::startWireGuard(const QString &config)
|
||||
|
||||
NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init];
|
||||
tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID];
|
||||
QByteArray configUtf8 = config.toUtf8();
|
||||
NSData *wgConfigData = [NSData dataWithBytes:configUtf8.constData() length:configUtf8.size()];
|
||||
tunnelProtocol.providerConfiguration = @{@"wireguard": wgConfigData};
|
||||
tunnelProtocol.providerConfiguration = @{@"wireguard": [[NSString stringWithUTF8String:config.toStdString().c_str()] dataUsingEncoding:NSUTF8StringEncoding]};
|
||||
tunnelProtocol.serverAddress = m_serverAddress;
|
||||
|
||||
m_currentTunnel.protocolConfiguration = tunnelProtocol;
|
||||
@@ -885,9 +813,7 @@ bool IosController::startXray(const QString &config)
|
||||
|
||||
NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init];
|
||||
tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID];
|
||||
QByteArray configUtf8 = config.toUtf8();
|
||||
NSData *xrayConfigData = [NSData dataWithBytes:configUtf8.constData() length:configUtf8.size()];
|
||||
tunnelProtocol.providerConfiguration = @{@"xray": xrayConfigData};
|
||||
tunnelProtocol.providerConfiguration = @{@"xray": [[NSString stringWithUTF8String:config.toStdString().c_str()] dataUsingEncoding:NSUTF8StringEncoding]};
|
||||
tunnelProtocol.serverAddress = m_serverAddress;
|
||||
|
||||
m_currentTunnel.protocolConfiguration = tunnelProtocol;
|
||||
@@ -909,49 +835,39 @@ void IosController::startTunnel()
|
||||
m_rxBytes = 0;
|
||||
m_txBytes = 0;
|
||||
|
||||
NETunnelProviderManager *tunnel = m_currentTunnel;
|
||||
[tunnel setEnabled:YES];
|
||||
[m_currentTunnel setEnabled:YES];
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[tunnel saveToPreferencesWithCompletionHandler:^(NSError *saveError) {
|
||||
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;
|
||||
}
|
||||
[m_currentTunnel saveToPreferencesWithCompletionHandler:^(NSError *saveError) {
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
|
||||
[tunnel loadFromPreferencesWithCompletionHandler:^(NSError *loadError) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (loadError) {
|
||||
qDebug().nospace() << "IosController::startTunnel :" << tunnel.localizedDescription << protocolName
|
||||
<< ": Connect " << protocolName << " Tunnel Load Error"
|
||||
<< loadError.localizedDescription.UTF8String;
|
||||
emit connectionStateChanged(Vpn::ConnectionState::Error);
|
||||
return;
|
||||
}
|
||||
if (saveError) {
|
||||
qDebug().nospace() << "IosController::startTunnel" << protocolName << ": Connect " << protocolName << " Tunnel Save Error" << saveError.localizedDescription.UTF8String;
|
||||
emit connectionStateChanged(Vpn::ConnectionState::Error);
|
||||
return;
|
||||
}
|
||||
|
||||
NSError *startError = nil;
|
||||
qDebug() << iosStatusToState(tunnel.connection.status);
|
||||
[m_currentTunnel loadFromPreferencesWithCompletionHandler:^(NSError *loadError) {
|
||||
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) {
|
||||
qDebug().nospace() << "IosController::startTunnel :" << tunnel.localizedDescription << protocolName
|
||||
<< " : Connect " << protocolName << " Tunnel Start Error"
|
||||
<< (startError ? startError.localizedDescription.UTF8String : "");
|
||||
emit connectionStateChanged(Vpn::ConnectionState::Error);
|
||||
} else {
|
||||
qDebug().nospace() << "IosController::startTunnel :" << tunnel.localizedDescription << protocolName
|
||||
<< " : Starting the tunnel succeeded";
|
||||
}
|
||||
});
|
||||
}];
|
||||
});
|
||||
}];
|
||||
});
|
||||
BOOL started = [m_currentTunnel.connection startVPNTunnelWithOptions:nil andReturnError:&startError];
|
||||
|
||||
if (!started || startError) {
|
||||
qDebug().nospace() << "IosController::startTunnel :" << m_currentTunnel.localizedDescription << protocolName << " : Connect " << protocolName << " Tunnel Start Error"
|
||||
<< (startError ? startError.localizedDescription.UTF8String : "");
|
||||
emit connectionStateChanged(Vpn::ConnectionState::Error);
|
||||
} else {
|
||||
qDebug().nospace() << "IosController::startTunnel :" << m_currentTunnel.localizedDescription << protocolName << " : Starting the tunnel succeeded";
|
||||
}
|
||||
}];
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
bool IosController::isOurManager(NETunnelProviderManager* manager) {
|
||||
@@ -1206,26 +1122,14 @@ void IosController::fetchProducts(const QStringList &productIds,
|
||||
NSArray<NSString *> * _Nonnull invalidIdentifiers,
|
||||
NSError * _Nullable error) {
|
||||
QList<QVariantMap> outProducts;
|
||||
for (NSDictionary *productInfo in products) {
|
||||
QVariantMap productData;
|
||||
productData["productId"] = QString::fromUtf8([productInfo[@"productId"] UTF8String]);
|
||||
productData["title"] = QString::fromUtf8([productInfo[@"title"] UTF8String]);
|
||||
productData["description"] = QString::fromUtf8([productInfo[@"description"] UTF8String]);
|
||||
productData["price"] = QString::fromUtf8([productInfo[@"price"] UTF8String]);
|
||||
if (productInfo[@"displayPrice"]) {
|
||||
productData["displayPrice"] = QString::fromUtf8([productInfo[@"displayPrice"] UTF8String]);
|
||||
}
|
||||
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);
|
||||
for (NSDictionary *p in products) {
|
||||
QVariantMap m;
|
||||
m["productId"] = QString::fromUtf8([p[@"productId"] UTF8String]);
|
||||
m["title"] = QString::fromUtf8([p[@"title"] UTF8String]);
|
||||
m["description"] = QString::fromUtf8([p[@"description"] UTF8String]);
|
||||
m["price"] = QString::fromUtf8([p[@"price"] UTF8String]);
|
||||
m["currencyCode"] = QString::fromUtf8([p[@"currencyCode"] UTF8String]);
|
||||
outProducts.push_back(m);
|
||||
}
|
||||
|
||||
QStringList invalid;
|
||||
|
||||
@@ -164,13 +164,8 @@ bool LinuxRouteMonitor::rtmSendRoute(int action, int flags, int type,
|
||||
}
|
||||
|
||||
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;
|
||||
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_attr32(nlmsg, sizeof(buf), RTA_PRIORITY, 0);
|
||||
rtm->rtm_type = RTN_UNICAST;
|
||||
|
||||
@@ -41,11 +41,8 @@ void LinuxNetworkWatcher::initialize() {
|
||||
connect(m_worker, &LinuxNetworkWatcherWorker::unsecuredNetwork, this,
|
||||
&LinuxNetworkWatcher::unsecuredNetwork);
|
||||
|
||||
connect(m_worker, &LinuxNetworkWatcherWorker::wakeup, this,
|
||||
&NetworkWatcherImpl::wakeup);
|
||||
|
||||
connect(m_worker, &LinuxNetworkWatcherWorker::networkChanged, this,
|
||||
[this]() { emit networkChanged(""); });
|
||||
connect(m_worker, &LinuxNetworkWatcherWorker::sleepMode, this,
|
||||
&NetworkWatcherImpl::sleepMode);
|
||||
|
||||
// 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
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
enum NMState {
|
||||
NM_STATE_UNKNOWN = 0,
|
||||
NM_STATE_ASLEEP = 10,
|
||||
NM_STATE_DISABLED = 10,
|
||||
NM_STATE_DISCONNECTED = 20,
|
||||
NM_STATE_DISCONNECTING = 30,
|
||||
NM_STATE_CONNECTING = 40,
|
||||
@@ -200,11 +199,10 @@ void LinuxNetworkWatcherWorker::checkDevices() {
|
||||
|
||||
void LinuxNetworkWatcherWorker::NMStateChanged(quint32 state)
|
||||
{
|
||||
logger.debug() << "NMStateChanged " << state;
|
||||
if (state == NM_STATE_ASLEEP) {
|
||||
emit sleepMode();
|
||||
}
|
||||
|
||||
logger.debug() << "NMStateChanged " << state;
|
||||
}
|
||||
|
||||
if (state == NM_STATE_ASLEEP || state == NM_STATE_DISABLED) {
|
||||
emit wakeup();
|
||||
} else if (state == NM_STATE_CONNECTED_GLOBAL) {
|
||||
emit networkChanged();
|
||||
}
|
||||
}
|
||||
@@ -23,8 +23,7 @@ class LinuxNetworkWatcherWorker final : public QObject {
|
||||
|
||||
signals:
|
||||
void unsecuredNetwork(const QString& networkName, const QString& networkId);
|
||||
void wakeup();
|
||||
void networkChanged();
|
||||
void sleepMode();
|
||||
|
||||
public slots:
|
||||
void initialize();
|
||||
|
||||
@@ -173,10 +173,10 @@ void PowerNotificationsListener::sleepWakeupCallBack(void *refParam, io_service_
|
||||
|
||||
case kIOMessageSystemHasPoweredOn:
|
||||
/* Announces that the system and its devices have woken up. */
|
||||
logger.debug() << "System has powered on - emitting wakeup signal from dedicated CFRunLoop thread";
|
||||
logger.debug() << "System has powered on - emitting sleepMode signal from dedicated CFRunLoop thread";
|
||||
if (listener->m_watcher) {
|
||||
// Use QMetaObject::invokeMethod for thread-safe signal emission
|
||||
QMetaObject::invokeMethod(listener->m_watcher, "wakeup", Qt::QueuedConnection);
|
||||
QMetaObject::invokeMethod(listener->m_watcher, "sleepMode", Qt::QueuedConnection);
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
@@ -62,9 +62,6 @@ void WindowsDaemon::prepareActivation(const InterfaceConfig& config, int inetAda
|
||||
}
|
||||
|
||||
void WindowsDaemon::activateSplitTunnel(const InterfaceConfig& config, int vpnAdapterIndex) {
|
||||
if (m_splitTunnelManager == nullptr)
|
||||
return;
|
||||
|
||||
if (config.m_vpnDisabledApps.length() > 0) {
|
||||
m_splitTunnelManager->start(m_inetAdapterIndex, vpnAdapterIndex);
|
||||
m_splitTunnelManager->excludeApps(config.m_vpnDisabledApps);
|
||||
|
||||
@@ -41,7 +41,7 @@ LRESULT WindowsNetworkWatcher::PowerWndProcCallback(HWND hwnd, UINT uMsg, WPARAM
|
||||
switch (uMsg) {
|
||||
case WM_POWERBROADCAST:
|
||||
if (wParam == PBT_APMRESUMESUSPEND) {
|
||||
emit obj->wakeup();
|
||||
emit obj->sleepMode();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -232,6 +232,12 @@ ErrorCode OpenVpnProtocol::start()
|
||||
return ErrorCode::AmneziaServiceConnectionFailed;
|
||||
}
|
||||
|
||||
m_openVpnProcess->waitForSource(5000);
|
||||
if (!m_openVpnProcess->isInitialized()) {
|
||||
qWarning() << "IpcProcess replica is not connected!";
|
||||
setLastError(ErrorCode::AmneziaServiceConnectionFailed);
|
||||
return ErrorCode::AmneziaServiceConnectionFailed;
|
||||
}
|
||||
m_openVpnProcess->setProgram(PermittedProcess::OpenVPN);
|
||||
QStringList arguments({
|
||||
"--config", configPath(), "--management", m_managementHost, QString::number(mgmtPort),
|
||||
@@ -240,13 +246,13 @@ ErrorCode OpenVpnProtocol::start()
|
||||
m_openVpnProcess->setArguments(arguments);
|
||||
|
||||
qDebug() << arguments.join(" ");
|
||||
connect(m_openVpnProcess.data(), &IpcProcessInterfaceReplica::errorOccurred,
|
||||
connect(m_openVpnProcess.data(), &PrivilegedProcess::errorOccurred,
|
||||
[&](QProcess::ProcessError error) { qDebug() << "PrivilegedProcess errorOccurred" << error; });
|
||||
|
||||
connect(m_openVpnProcess.data(), &IpcProcessInterfaceReplica::stateChanged,
|
||||
connect(m_openVpnProcess.data(), &PrivilegedProcess::stateChanged,
|
||||
[&](QProcess::ProcessState newState) { qDebug() << "PrivilegedProcess stateChanged" << newState; });
|
||||
|
||||
connect(m_openVpnProcess.data(), &IpcProcessInterfaceReplica::finished, this,
|
||||
connect(m_openVpnProcess.data(), &PrivilegedProcess::finished, this,
|
||||
[&]() { setConnectionState(Vpn::ConnectionState::Disconnected); });
|
||||
|
||||
m_openVpnProcess->start();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user