Compare commits

...
32 Commits
Author SHA1 Message Date
NickVs2015 02838a94d5 fix: add new dep to test (#3301) 2026-10-20 10:22:09 +08:00
NickVs2015 9a1200d6f3 fix: build libagw with go 1.27.1 (#3303) 2026-10-20 10:21:53 +08:00
NickVs2015andvkamn ddde43635e feat: local proxy mode (#3166)
* fix: share double-stop guard via VpnProtocol base class

* feat: add local proxy core module

* feat: local proxy settings api

* feat: wire local proxy into app controllers

* feat: local proxy settings ui

* feat: arbitrate shared xray engine ownership via ipc tokens

* fix: remove connection type

* chore: add qthttpserver to desktop ci qt modules

* fix: local proxy port fallback and active port reporting

* fix: keep text field button aligned when error is shown

* fix: hide local proxy for free servers and guard device unlink

* fix: show local proxy state per server

* fix: anchor text field button to content

* fix: adapt local proxy pages to design

* fix: copy logic and port logic

* refactor: move local proxy files to controllers and utils

* refactor: merge local proxy into LocalProxyController

* refactor: reuse gateway config request and keep local proxy state in memory

---------

Co-authored-by: vkamn <vk@amnezia.org>
2026-10-18 21:25:04 +08:00
vkamn ffba1cb2a4 chore: minor ui fixes (#3295)
* fix: sync ds component sizes with updated figma

* fix: hover only on action buttons in list rows, standalone country row
2026-10-17 23:51:22 +08:00
vkamn 30ff3451c7 fix: prepare gateway request (#3291)
* fix: prepare the gateway request once instead of a blocking callback per attempt

* chore: libagw 1.0.8, gateway and failover timeouts 6s, health check 1s
2026-10-16 00:10:29 +08:00
vkamn db951e9928 chore: minor ui fixes (#3290)
* chore: change amnezia hosting description

* fix: ios safe areas and ds component alignment

- extend window into ios safe areas, pass insets to PageController
- use dark interface style and portrait-only orientation on iphone
- drop bottom inset while keyboard is open
- align controls with figma: title-only cards, caption height,
  selectable list items, nav bar spacing, chip hover, switch rows

* fix: news settings icon unread badge

* fix: align ds effects with figma

- three-layer selection glow for switch, radio and connect ring
- connect button disk with glow in all states
- snackbar elevate shadow spread, dropdown panel shadow
- backdrop blur for loading overlay container
- pressed state layer 0.1 for icon buttons, segments, list items, cards

* refactor: remove Qt5Compat.GraphicalEffects
2026-10-15 23:50:51 +08:00
vkamn a36399a4c3 chore: minor ui fixes (#3289) 2026-10-15 12:58:24 +08:00
vkamn 8940f8e273 chore: minor ui fixes (#3288) 2026-10-15 12:13:31 +08:00
NickVs2015 705a373d51 feat: cover test gateway service config import (#3239)
* feature: cover gateway service config import

* fix: use AmneziaApplication in gateway service import test

* feature: cover gateway service update

* fix: drain gateway workers before test teardown
2026-10-14 21:52:29 +08:00
vkamn 3d4a45246a fix: fix missing includes (#3281) 2026-10-14 11:28:24 +08:00
vkamn 11355e1de0 feat: new design system (#3280)
* refactor: focus controller refactoring

* feat: update to new design system

* feat: update to new desing system

* chore: bump version
2026-10-14 11:13:36 +08:00
yp 3e8077f2ca feat: split app/tunnel logs viewer (#3262)
* feat: split app/tunnel log streams and add in-app log viewer

* fix: report real save result on android

* remove dead logging code & open qt log on start, stop ne logging on auto-off, pin log date locale

* drop dead share api and simplify logs ui components

* add ru strings for logs redesign
2026-10-13 22:10:30 +08:00
yp c52b1a50a8 fix: hint xray settings file on main import (#3276) 2026-10-13 21:26:55 +08:00
yp 2c0f169946 fix: location separation texts (#3275) 2026-10-13 21:26:20 +08:00
vkamn 693d00ded5 feat: use libagw (#3028)
* feat: use libagw

* feat: add conanfile for libagw

* chore: fix android build with agw

* refactor: gateway error code parsing

* fix: build fixes

* fix: return proxy cache

* chore: update link to libagw

* fix: libagw 1.0.6, log request trace, persist state synchronously

* chore: fetch libagw as a release archive with sha256 instead of git clone
2026-10-13 21:25:58 +08:00
Yakimov GrigoryandGrigory Yakimov f1af8efb4b feat: add Thai (th_TH) localization (#3253)
* Add Thai language

* Update amneziavpn_th_TH.ts

* Add small test for check languages

---------

Co-authored-by: Grigory Yakimov <GYakimov@alfabank.ru>
2026-10-12 11:50:24 +08:00
NickVs2015 4c2e950864 fix: restore backup from content:// uri on android (#3218) 2026-10-08 23:59:22 +08:00
yp 9017984b1d fix: rework xray settings - split client/server data, unify saving, fix tls (#3006)
* Feat: split xray client and server settings

* remove test logs

* in-place save and TLS pin

* fixed xhttp/mkcp + tls

* remove check version xray in start connect

* address review findings for the settings split

* reset namespace

* reset namespace

* remove template xray

* fix: stop xray defaults from overwriting entered settings

* revert dropdown selection refactor from the defaults fix

* move save 'updateServerConfig'

* remove comment
2026-10-08 23:58:25 +08:00
yp c5ecf6281f feat: location separation (#3181)
* Feat: location separation

* remove temp code

* remove tab filter

* fixed models

* remove settings button in native configs

* reset & fixed crash focus

* derive country list use-case set from listId

* fixed: search field: Esc on empty field -> passed on, page closes

* country list: three levels, split by full catalog

* favorites: keep unstarred row until list refresh

* favorites on configuration files screen

* split country list by visible locations

* hysteresis for region split: split above 15, merge below 13

* section counters ignore search, follow chip

* empty state: suggest switching keyboard layout

* section header: counter in parentheses on collapsed sections only

* country list: scroll-linked large title header

* current section expanded on entry without overwriting saved collapse

* country list: raise scrollbar above pinned overlays, drawer above it

* qml: collapsing header component, config files header, country list parts extracted

* country list: ignore unknown use case ids from catalog

* country list: use case names from catalog (nameLocalized), ui language in model

* country list: collapsed sections kept for the session only, as in the task

* country list: design review fixes

* favorites limit: toast from the mockup with 'show' action

* toast: geometry, typography, shadow and action button from figma
2026-10-08 23:31:05 +08:00
NickVs2015 3e9b70a5f1 feature: bump go toolchain to 1.27.1 (#3238)
* chore: bump go toolchain to 1.27.1

* chore: move go version back into recipe
2026-10-05 09:20:53 +02:00
vkamn 94b51df247 chore: bump version (#3187) 2026-09-18 20:44:38 +08:00
Mezooz de93650a90 fix: make Conan recipes parse on Python < 3.12 (#2870)
Three recipes use f-string syntax that is only valid on Python 3.12+
(PEP 701): reusing the same quote character inside an f-string
expression, and using a backslash inside one.

  recipes/amnezia-xray-bindings/conanfile.py:115
  recipes/tun2socks/conanfile.py:118
  recipes/hev-socks5-tunnel/conanfile.py:63-65

On older interpreters Conan fails to load the recipe with a SyntaxError
before resolving a single dependency, so the build cannot start:

  SyntaxError: unexpected character after line continuation character

This is not platform specific. It is invisible in CI only because
ubuntu-latest ships Python 3.12+. Debian 12 (bookworm), current stable
and the base for Raspberry Pi OS, ships Python 3.11.

Hoist the expressions out of the f-strings, and use single quotes for
the nested string literals. Behaviour is unchanged on every version.

Verified with python3 -m py_compile on Python 3.11.2 (aarch64,
Debian 12): all three fail before this change and pass after.
2026-09-18 13:59:14 +08:00
vkamn 448baad3e1 chore: bump version (#3185)
* chore: bump version

* feat: update translations
2026-09-18 13:52:33 +08:00
vkamn aca1b7dc5c feat: add format version for future config refactoring (#3184) 2026-09-18 13:31:07 +08:00
vkamn de36a81b58 chore: change android aqt host (#3182) 2026-09-17 13:43:35 +08:00
vkamn 76b81b43c1 chore: pin install-qt-action to v4.3.1 (#3180) 2026-09-16 22:24:34 +08:00
MrMirDan bea2f5f7db feat: clickable notification (#3178) 2026-09-16 22:22:51 +08:00
Yaroslav Gurov a4d38b5642 fix: windows certificate signing issues (#3175)
* fix: windows certificate signing issues

* fix: add has_ui option and phys/cloud opts
2026-09-16 22:22:19 +08:00
NickVs2015andMrMirDan b76c88bba3 feat: migrate test to main repo (#3172)
* basic tests  adaptation

* fix: ui servers model and controller test awg config

* fix: platforms test builds

* build dirs inspection

* fix: changed paths for tests and tests artifacts

* fix: 'deploy' parent dir

* windows ctest configuration and macos dependencies inspection

* windows check executable and macos copy openssl libs

* windows test executables check and macos copy openssl

* changed some path on windows

* android apk list

* fix: updated android apk path

* Verify Android test files

* fix: build tests only with AMNEZIA_BUILD_TESTS

* fix: copy openssl runtime next to test_openssl_library

* fix: awg2 test config and inverted description checks

* chore: move run_android_tests.sh to client/tests and drop personal paths

* chore: gate tests by option, scope secrets and drop global concurrency in ci

* chore: serialized vps test job, scoped secrets, backup secret guard, test data lookup

---------

Co-authored-by: MrMirDan <8mirdan8@gmail.com>
2026-09-16 17:05:43 +08:00
vkamn eab14fd957 chore: add define for app update (#3164) 2026-09-14 12:37:58 +08:00
SPTApyo 2a8242d306 feat: add French (fr_FR) localization (#3135)
* feat: add French (fr_FR) localization

* fix: align french catalogue with current telegram link
2026-09-09 21:44:51 +08:00
vkamn e60842bd9d chore: cleanup entitlements (#3149)
* chore: cleanup entitlements for macos ne

* chore: bump version
2026-09-09 21:12:03 +08:00
437 changed files with 63680 additions and 31563 deletions

No files matched your search

+614 -14
View File
@@ -88,7 +88,7 @@ jobs:
host: 'linux'
target: 'desktop'
arch: 'linux_gcc_64'
modules: 'qtremoteobjects qt5compat qtshadertools'
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
dir: ${{ runner.temp }}
setup-python: 'true'
tools: 'tools_ifw'
@@ -118,8 +118,16 @@ jobs:
shell: bash
env:
QT_INSTALL_DIR: ${{ runner.temp }}
AMNEZIA_BUILD_TESTS: ON
run: deploy/build.sh --generator Ninja --installer all
- name: 'Upload Linux test build'
uses: actions/upload-artifact@v7
with:
name: linux-build
path: deploy/build/client/tests
retention-days: 7
- name: 'Upload installer artifact'
uses: actions/upload-artifact@v7
with:
@@ -201,7 +209,7 @@ jobs:
host: 'windows'
target: 'desktop'
arch: 'win64_msvc2022_64'
modules: 'qtremoteobjects qt5compat qtshadertools'
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
dir: ${{ runner.temp }}
setup-python: 'true'
tools: 'tools_ifw'
@@ -242,10 +250,20 @@ jobs:
shell: cmd
env:
QT_INSTALL_DIR: ${{ runner.temp }}
AMNEZIA_BUILD_TESTS: ON
run: |
set WIX_ROOT_PATH="${{ env.USERPROFILE }}\.dotnet\tools"
deploy\build.bat --installer all
- name: 'Upload Windows test build'
uses: actions/upload-artifact@v7
with:
name: windows-build
path: |
deploy/build/client/tests
deploy/build/client/Release
retention-days: 7
- name: 'Upload WIX installer artifact'
uses: actions/upload-artifact@v7
with:
@@ -504,13 +522,13 @@ jobs:
xcode-version: '26.3.0'
- name: 'Install Qt'
uses: jurplel/install-qt-action@v4
uses: jurplel/install-qt-action@v4.3.1
with:
version: ${{ env.QT_VERSION }}
host: 'mac'
target: 'desktop'
arch: 'clang_64'
modules: 'qtremoteobjects qt5compat qtshadertools'
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
dir: ${{ runner.temp }}
setup-python: 'true'
set-env: 'true'
@@ -536,9 +554,17 @@ jobs:
NOTARYTOOL_TEAM_ID: ${{ secrets.MAC_TEAM_ID }}
NOTARYTOOL_EMAIL: ${{ secrets.APPLE_DEV_EMAIL }}
NOTARYTOOL_PASSWORD: ${{ secrets.APPLE_DEV_PASSWORD }}
AMNEZIA_BUILD_TESTS: ON
shell: bash
run: deploy/build.sh --generator Ninja --installer all
- name: 'Upload macOS test build'
uses: actions/upload-artifact@v7
with:
name: macos-build
path: deploy/build/client/tests
retention-days: 7
- name: 'Upload installer artifact'
uses: actions/upload-artifact@v7
with:
@@ -645,7 +671,7 @@ jobs:
version: ${{ env.QT_VERSION }}
host: 'mac'
target: 'desktop'
modules: 'qtremoteobjects qt5compat qtshadertools qtmultimedia'
modules: 'qtremoteobjects qt5compat qtshadertools qtmultimedia qthttpserver qtwebsockets'
arch: 'clang_64'
dir: ${{ runner.temp }}
set-env: 'true'
@@ -792,7 +818,7 @@ jobs:
submodules: 'true'
- name: 'Install desktop Qt'
uses: jurplel/install-qt-action@v4
uses: jurplel/install-qt-action@v4.3.1
with:
version: ${{ env.QT_VERSION }}
host: 'linux'
@@ -804,10 +830,10 @@ jobs:
extra: '--base ${{ env.QT_MIRROR }}'
- name: 'Install android_x86_64 Qt'
uses: jurplel/install-qt-action@v4
uses: jurplel/install-qt-action@v4.3.1
with:
version: ${{ env.QT_VERSION }}
host: 'linux'
host: 'all_os'
target: 'android'
arch: 'android_x86_64'
modules: ${{ env.QT_MODULES }}
@@ -816,10 +842,10 @@ jobs:
extra: '--base ${{ env.QT_MIRROR }}'
- name: 'Install android_x86 Qt'
uses: jurplel/install-qt-action@v4
uses: jurplel/install-qt-action@v4.3.1
with:
version: ${{ env.QT_VERSION }}
host: 'linux'
host: 'all_os'
target: 'android'
arch: 'android_x86'
modules: ${{ env.QT_MODULES }}
@@ -828,10 +854,10 @@ jobs:
extra: '--base ${{ env.QT_MIRROR }}'
- name: 'Install android_armv7 Qt'
uses: jurplel/install-qt-action@v4
uses: jurplel/install-qt-action@v4.3.1
with:
version: ${{ env.QT_VERSION }}
host: 'linux'
host: 'all_os'
target: 'android'
arch: 'android_armv7'
modules: ${{ env.QT_MODULES }}
@@ -840,10 +866,10 @@ jobs:
extra: '--base ${{ env.QT_MIRROR }}'
- name: 'Install android_arm64_v8a Qt'
uses: jurplel/install-qt-action@v4
uses: jurplel/install-qt-action@v4.3.1
with:
version: ${{ env.QT_VERSION }}
host: 'linux'
host: 'all_os'
target: 'android'
arch: 'android_arm64_v8a'
modules: ${{ env.QT_MODULES }}
@@ -1003,3 +1029,577 @@ jobs:
echo "Pull request:" >> $GITHUB_STEP_SUMMARY
echo "[[#${{ fromJSON(steps.pull_request.outputs.data)[0].number }}] ${{ fromJSON(steps.pull_request.outputs.data)[0].title }}](${{ fromJSON(steps.pull_request.outputs.data)[0].html_url }})" >> $GITHUB_STEP_SUMMARY
# ------------------------------------------------------
test-linux:
name: Test Linux (${{ matrix.filter }})
needs: Build-Linux-Ubuntu
strategy:
fail-fast: false
matrix:
filter:
- MultipleImportsTest
- ServerEditTest
- DefaultServerChangeTest
- ServerEdgeCasesTest
- SignalOrderTest
- ServersModelSyncTest
- ServerRenameTest
- SettingsSignalsTest
- UiServersModelAndControllerTest
- XraySerializationTest
- UiLanguageModelAndControllerTest
- UiIpModelAndControllerTest
- UiAppSTModelAndControllerTest
- UiAllowedDnsModelAndControllerTest
- BackupExportImportTest
- ConnectionTest
- OpenSSLLibraryTest
- GatewayServiceImportTest
runs-on: ubuntu-22.04
env:
QT_VERSION: 6.10.1
QTEST_FUNCTION_TIMEOUT: 7200000
THIRD_PARTY_AWG_VPN_KEY: ${{ secrets.THIRD_PARTY_AWG_VPN_KEY }}
THIRD_PARTY_WIRE_GUARD_VPN_KEY: ${{ secrets.THIRD_PARTY_WIRE_GUARD_VPN_KEY }}
THIRD_PARTY_XRAY_VPN_KEY: ${{ secrets.THIRD_PARTY_XRAY_VPN_KEY }}
THIRD_PARTY_OPEN_VPN_KEY: ${{ secrets.THIRD_PARTY_OPEN_VPN_KEY }}
THIRD_PARTY_CLOAK_VPN_KEY: ${{ secrets.THIRD_PARTY_CLOAK_VPN_KEY }}
THIRD_PARTY_SS_VPN_KEY: ${{ secrets.THIRD_PARTY_SS_VPN_KEY }}
THIRD_PARTY_IKEV_VPN_KEY: ${{ secrets.THIRD_PARTY_IKEV_VPN_KEY }}
THIRD_PARTY_PREMIUM_VPN_KEY: ${{ secrets.THIRD_PARTY_PREMIUM_VPN_KEY }}
THIRD_PARTY_VMESS_NEW_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VMESS_NEW_IMPORT_DATA }}
THIRD_PARTY_VMESS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VMESS_IMPORT_DATA }}
THIRD_PARTY_TROJAN_IMPORT_DATA: ${{ secrets.THIRD_PARTY_TROJAN_IMPORT_DATA }}
THIRD_PARTY_VLESS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VLESS_IMPORT_DATA }}
THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA }}
THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: 'true'
fetch-depth: 10
- name: Decode backup file
if: matrix.filter == 'BackupExportImportTest'
shell: bash
env:
BACKUP_FILE_BASE64: ${{ secrets.BACKUP_FILE_BASE64 }}
run: |
if [ -z "$BACKUP_FILE_BASE64" ]; then
echo "::error::BACKUP_FILE_BASE64 secret is not set"
exit 1
fi
echo "$BACKUP_FILE_BASE64" | base64 --decode > "$RUNNER_TEMP/test.backup"
echo "BACKUP_PATH=$RUNNER_TEMP/test.backup" >> "$GITHUB_ENV"
- name: Install Qt
uses: jurplel/install-qt-action@v3
with:
version: ${{ env.QT_VERSION }}
host: linux
target: desktop
arch: linux_gcc_64
modules: qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets
dir: ${{ runner.temp }}
set-env: true
aqtversion: '==3.3.0'
py7zrversion: '==0.22.*'
extra: '--base ${{ env.QT_MIRROR }}'
- name: Install system packages
run: |
sudo apt-get update
sudo apt-get install -y \
libxkbcommon-x11-0 \
libsecret-1-dev
- name: Download build
uses: actions/download-artifact@v4
with:
name: linux-build
path: deploy/build/client/tests
- name: Restore executable permissions
run: |
find deploy/build/client/tests \
-type f \
-name "test_*" \
-exec chmod +x {} \;
- name: Run tests
env:
AMNEZIA_TEST_API_KEY: ${{ secrets.AMNEZIA_TEST_API_KEY }}
shell: bash
run: |
QT_ROOT="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/gcc_64"
export LD_LIBRARY_PATH="$PWD/deploy/build/client/tests:$QT_ROOT/lib:${LD_LIBRARY_PATH:-}"
export QT_QPA_PLATFORM=offscreen
ctest --test-dir deploy/build/client/tests --output-on-failure -VV --timeout 600 -R "^${{ matrix.filter }}$"
# ------------------------------------------------------
test-vps:
name: Test self-hosted VPS
needs: Build-Linux-Ubuntu
# These tests install and remove docker containers on a single shared VPS:
# two runs at once corrupt each other's state, so only one job may touch it.
concurrency:
group: vps-tests
cancel-in-progress: false
runs-on: ubuntu-22.04
env:
QT_VERSION: 6.10.1
QTEST_FUNCTION_TIMEOUT: 7200000
SELF_HOSTED_SERVER_HOST_NAME: ${{ secrets.SELF_HOSTED_SERVER_HOST_NAME }}
SELF_HOSTED_SERVER_USER_NAME: ${{ secrets.SELF_HOSTED_SERVER_USER_NAME }}
SELF_HOSTED_SERVER_PASSWORD: ${{ secrets.SELF_HOSTED_SERVER_PASSWORD }}
SELF_HOSTED_SERVER_SSH_PORT: ${{ secrets.SELF_HOSTED_SERVER_SSH_PORT }}
SELF_HOSTED_ADMIN_EXPORT_ROUND_TRIP_VPN_KEY: ${{ secrets.SELF_HOSTED_ADMIN_EXPORT_ROUND_TRIP_VPN_KEY }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: 'true'
fetch-depth: 10
- name: Install Qt
uses: jurplel/install-qt-action@v3
with:
version: ${{ env.QT_VERSION }}
host: linux
target: desktop
arch: linux_gcc_64
modules: qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets
dir: ${{ runner.temp }}
set-env: true
aqtversion: '==3.3.0'
py7zrversion: '==0.22.*'
extra: '--base ${{ env.QT_MIRROR }}'
- name: Install system packages
run: |
sudo apt-get update
sudo apt-get install -y \
libxkbcommon-x11-0 \
libsecret-1-dev
- name: Download build
uses: actions/download-artifact@v4
with:
name: linux-build
path: deploy/build/client/tests
- name: Restore executable permissions
run: |
find deploy/build/client/tests \
-type f \
-name "test_*" \
-exec chmod +x {} \;
- name: Run tests
shell: bash
run: |
QT_ROOT="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/gcc_64"
export LD_LIBRARY_PATH="$PWD/deploy/build/client/tests:$QT_ROOT/lib:${LD_LIBRARY_PATH:-}"
export QT_QPA_PLATFORM=offscreen
ctest --test-dir deploy/build/client/tests --output-on-failure -VV \
-R "^(SelfHostedServerSetupTest|MultipleExportsTest|AdminSelfHostedExportTest)$"
# ------------------------------------------------------
test-windows:
name: Test Windows (${{ matrix.filter }})
needs: Build-Windows
strategy:
fail-fast: false
matrix:
filter:
- MultipleImportsTest
- ServerEditTest
- DefaultServerChangeTest
- ServerEdgeCasesTest
- SignalOrderTest
- ServersModelSyncTest
- ServerRenameTest
- SettingsSignalsTest
- UiServersModelAndControllerTest
- XraySerializationTest
- UiLanguageModelAndControllerTest
- UiIpModelAndControllerTest
- UiAppSTModelAndControllerTest
- UiAllowedDnsModelAndControllerTest
- BackupExportImportTest
- ConnectionTest
- OpenSSLLibraryTest
- GatewayServiceImportTest
runs-on: windows-latest
env:
QT_VERSION: 6.10.1
QTEST_FUNCTION_TIMEOUT: 7200000
THIRD_PARTY_AWG_VPN_KEY: ${{ secrets.THIRD_PARTY_AWG_VPN_KEY }}
THIRD_PARTY_WIRE_GUARD_VPN_KEY: ${{ secrets.THIRD_PARTY_WIRE_GUARD_VPN_KEY }}
THIRD_PARTY_XRAY_VPN_KEY: ${{ secrets.THIRD_PARTY_XRAY_VPN_KEY }}
THIRD_PARTY_OPEN_VPN_KEY: ${{ secrets.THIRD_PARTY_OPEN_VPN_KEY }}
THIRD_PARTY_CLOAK_VPN_KEY: ${{ secrets.THIRD_PARTY_CLOAK_VPN_KEY }}
THIRD_PARTY_SS_VPN_KEY: ${{ secrets.THIRD_PARTY_SS_VPN_KEY }}
THIRD_PARTY_IKEV_VPN_KEY: ${{ secrets.THIRD_PARTY_IKEV_VPN_KEY }}
THIRD_PARTY_PREMIUM_VPN_KEY: ${{ secrets.THIRD_PARTY_PREMIUM_VPN_KEY }}
THIRD_PARTY_VMESS_NEW_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VMESS_NEW_IMPORT_DATA }}
THIRD_PARTY_VMESS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VMESS_IMPORT_DATA }}
THIRD_PARTY_TROJAN_IMPORT_DATA: ${{ secrets.THIRD_PARTY_TROJAN_IMPORT_DATA }}
THIRD_PARTY_VLESS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VLESS_IMPORT_DATA }}
THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA }}
THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: 'true'
fetch-depth: 10
- name: Decode backup file
if: matrix.filter == 'BackupExportImportTest'
shell: pwsh
env:
BACKUP_FILE_BASE64: ${{ secrets.BACKUP_FILE_BASE64 }}
run: |
if ([string]::IsNullOrWhiteSpace($env:BACKUP_FILE_BASE64)) {
Write-Output "::error::BACKUP_FILE_BASE64 secret is not set"
exit 1
}
$backup = Join-Path $env:RUNNER_TEMP "test.backup"
$bytes = [Convert]::FromBase64String($env:BACKUP_FILE_BASE64)
[IO.File]::WriteAllBytes($backup, $bytes)
"BACKUP_PATH=$backup" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
- name: Download build
uses: actions/download-artifact@v4
with:
name: windows-build
path: deploy/build/client
- name: Install Qt
uses: jurplel/install-qt-action@v4
with:
version: ${{ env.QT_VERSION }}
host: windows
target: desktop
arch: win64_msvc2022_64
modules: qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets
dir: ${{ runner.temp }}
set-env: true
- name: Setup MSVC
uses: ilammy/msvc-dev-cmd@v1
with:
arch: x64
- name: Run tests
env:
AMNEZIA_TEST_API_KEY: ${{ secrets.AMNEZIA_TEST_API_KEY }}
shell: cmd
run: |
rem Without an attached console (GitHub runner), QTest sends its output to
rem OutputDebugString instead of stdout, so ctest captures nothing.
set QT_FORCE_STDERR_LOGGING=1
set PATH=%PATH%;%CD%\client\3rd-prebuilt\deploy-prebuilt\windows\x64
set PATH=%PATH%;%Qt6_DIR%\..\..\..\bin
ctest --test-dir deploy\build\client\tests -C Release -R "${{ matrix.filter }}" --output-on-failure -VV --timeout 600
# ------------------------------------------------------
test-macos:
name: Test macOS (${{ matrix.filter }})
needs: Build-MacOS
strategy:
fail-fast: false
matrix:
filter:
- MultipleImportsTest
- ServerEditTest
- DefaultServerChangeTest
- ServerEdgeCasesTest
- SignalOrderTest
- ServersModelSyncTest
- ServerRenameTest
- SettingsSignalsTest
- UiServersModelAndControllerTest
- XraySerializationTest
- UiLanguageModelAndControllerTest
- UiIpModelAndControllerTest
- UiAppSTModelAndControllerTest
- UiAllowedDnsModelAndControllerTest
- BackupExportImportTest
- ConnectionTest
- OpenSSLLibraryTest
- GatewayServiceImportTest
runs-on: macos-15
env:
QT_VERSION: 6.10.1
QTEST_FUNCTION_TIMEOUT: 7200000
THIRD_PARTY_AWG_VPN_KEY: ${{ secrets.THIRD_PARTY_AWG_VPN_KEY }}
THIRD_PARTY_WIRE_GUARD_VPN_KEY: ${{ secrets.THIRD_PARTY_WIRE_GUARD_VPN_KEY }}
THIRD_PARTY_XRAY_VPN_KEY: ${{ secrets.THIRD_PARTY_XRAY_VPN_KEY }}
THIRD_PARTY_OPEN_VPN_KEY: ${{ secrets.THIRD_PARTY_OPEN_VPN_KEY }}
THIRD_PARTY_CLOAK_VPN_KEY: ${{ secrets.THIRD_PARTY_CLOAK_VPN_KEY }}
THIRD_PARTY_SS_VPN_KEY: ${{ secrets.THIRD_PARTY_SS_VPN_KEY }}
THIRD_PARTY_IKEV_VPN_KEY: ${{ secrets.THIRD_PARTY_IKEV_VPN_KEY }}
THIRD_PARTY_PREMIUM_VPN_KEY: ${{ secrets.THIRD_PARTY_PREMIUM_VPN_KEY }}
THIRD_PARTY_VMESS_NEW_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VMESS_NEW_IMPORT_DATA }}
THIRD_PARTY_VMESS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VMESS_IMPORT_DATA }}
THIRD_PARTY_TROJAN_IMPORT_DATA: ${{ secrets.THIRD_PARTY_TROJAN_IMPORT_DATA }}
THIRD_PARTY_VLESS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VLESS_IMPORT_DATA }}
THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA }}
THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA }}
steps:
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: "26.3.0"
- name: Checkout
uses: actions/checkout@v4
with:
submodules: 'true'
fetch-depth: 10
- name: Decode backup file
if: matrix.filter == 'BackupExportImportTest'
shell: bash
env:
BACKUP_FILE_BASE64: ${{ secrets.BACKUP_FILE_BASE64 }}
run: |
if [ -z "$BACKUP_FILE_BASE64" ]; then
echo "::error::BACKUP_FILE_BASE64 secret is not set"
exit 1
fi
echo "$BACKUP_FILE_BASE64" | base64 --decode > "$RUNNER_TEMP/test.backup"
echo "BACKUP_PATH=$RUNNER_TEMP/test.backup" >> "$GITHUB_ENV"
- name: Download build
uses: actions/download-artifact@v4
with:
name: macos-build
path: deploy/build/client/tests
- name: Install Qt
uses: jurplel/install-qt-action@v4
with:
version: ${{ env.QT_VERSION }}
host: mac
target: desktop
arch: clang_64
modules: qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets
dir: ${{ runner.temp }}
set-env: true
- name: Restore executable permissions
run: |
find deploy/build/client/tests \
-type f \
-name "test_*" \
-exec chmod +x {} \;
- name: Run tests
env:
AMNEZIA_TEST_API_KEY: ${{ secrets.AMNEZIA_TEST_API_KEY }}
shell: bash
run: |
QT_ROOT="$RUNNER_TEMP/Qt/$QT_VERSION/macos"
export DYLD_LIBRARY_PATH="$PWD/deploy/build/client/tests:$QT_ROOT/lib:${DYLD_LIBRARY_PATH:-}"
ctest --test-dir deploy/build/client/tests -R "${{ matrix.filter }}" --output-on-failure -VV --timeout 600
# ------------------------------------------------------
test-android:
runs-on: android-runner
env:
ANDROID_PLATFORM: android-28
NDK_VERSION: 27.0.11718014
QT_VERSION: 6.10.1
QT_MODULES: 'qtremoteobjects qt5compat qtshadertools'
steps:
- name: 'Get sources'
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 10
- name: 'Decode backup file'
run: |
echo "${{ secrets.BACKUP_FILE_BASE64 }}" | base64 --decode > /tmp/test.backup
echo "BACKUP_PATH=/tmp/test.backup" >> $GITHUB_ENV
- name: 'Install desktop Qt'
uses: jurplel/install-qt-action@v4
with:
version: ${{ env.QT_VERSION }}
host: 'linux'
target: 'desktop'
arch: 'linux_gcc_64'
modules: ${{ env.QT_MODULES }}
dir: ${{ runner.temp }}
py7zrversion: '==0.22.*'
extra: '--base ${{ env.QT_MIRROR }}'
- name: 'Install android_x86_64 Qt'
uses: jurplel/install-qt-action@v4
with:
version: ${{ env.QT_VERSION }}
host: 'all_os'
target: 'android'
arch: 'android_x86_64'
modules: ${{ env.QT_MODULES }}
dir: ${{ runner.temp }}
py7zrversion: '==0.22.*'
extra: '--base ${{ env.QT_MIRROR }}'
- name: 'Setup Java'
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: 'Setup Android SDK'
uses: android-actions/setup-android@v4
with:
packages: "platforms;${{ env.ANDROID_PLATFORM }} ndk;${{ env.NDK_VERSION }}"
- name: 'Setup python'
uses: actions/setup-python@v6
with:
python-version: '3.14'
- name: 'Install conan'
run: pip install "conan==2.28.0"
# Uncomment to sign APK with release keystore (requires secrets):
# - name: 'Decode keystore'
# env:
# KEYSTORE_BASE64: ${{ secrets.ANDROID_RELEASE_KEYSTORE_BASE64 }}
# run: echo $KEYSTORE_BASE64 | base64 --decode > android.keystore
- name: 'Configure (Android x86_64)'
env:
QT_ANDROID: ${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/android_x86_64
QT_HOST: ${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/gcc_64
CMAKE_ANDROID_NDK: ${{ env.ANDROID_HOME }}/ndk/${{ env.NDK_VERSION }}
run: |
cmake -S . -B build -G Ninja \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_TOOLCHAIN_FILE=$QT_ANDROID/lib/cmake/Qt6/qt.toolchain.cmake \
-DANDROID_PLATFORM=${{ env.ANDROID_PLATFORM }} \
-DCMAKE_ANDROID_NDK=$CMAKE_ANDROID_NDK \
-DQT_HOST_PATH=$QT_HOST \
-DANDROID_SDK_ROOT=$ANDROID_HOME \
-DAMNEZIA_BUILD_TESTS=ON
- name: 'Build Android tests'
# env:
# QT_ANDROID_KEYSTORE_PATH: ${{ github.workspace }}/android.keystore
# QT_ANDROID_KEYSTORE_ALIAS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_ALIAS }}
# QT_ANDROID_KEYSTORE_STORE_PASS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_PASS }}
run: |
cmake --build build --target test_android_openssl_make_apk
cmake --build build --target test_android_restore_backup_make_apk
- name: 'Write Android test runner script'
run: |
cat > ci_run_android_tests.sh << 'EOF'
#!/usr/bin/env bash
set -euo pipefail
ADB=$(which adb)
echo ">>> Waiting for package manager to be ready..."
"$ADB" shell 'while ! pm list packages > /dev/null 2>&1; do sleep 2; done'
echo ">>> Package manager ready."
run_test() {
local target="$1"
local apk="build/client/tests/android-build/${target}.apk"
local pkg_name="org.qtproject.example.${target}"
local activity="${pkg_name}/org.qtproject.qt.android.bindings.QtActivity"
echo ">>> Installing ${target} APK..."
timeout 120 "$ADB" install -r "$apk"
"$ADB" shell "run-as $pkg_name sh -c 'mkdir -p files && : > files/stdout.txt'" 2>/dev/null || true
"$ADB" shell "run-as $pkg_name sh -c 'rm -f files/qtest_last_exit_code'" 2>/dev/null || true
echo ">>> Launching ${target}..."
"$ADB" shell am start -n "$activity"
echo ">>> Waiting for test to finish (up to 120s)..."
local elapsed=0 exit_code=""
while [[ $elapsed -lt 120 ]]; do
sleep 2
elapsed=$((elapsed + 2))
exit_code=$("$ADB" shell \
"run-as $pkg_name cat files/qtest_last_exit_code 2>/dev/null" \
| tr -d '[:space:]' || true)
[[ -n "$exit_code" ]] && break
done
local stdout_content
stdout_content=$("$ADB" shell \
"run-as $pkg_name cat files/stdout.txt 2>/dev/null" || true)
if [[ -n "$stdout_content" ]]; then
echo ">>> Test output:"
echo "$stdout_content"
fi
if [[ -z "$exit_code" ]]; then
echo ">>> TIMEOUT: ${target} did not complete within 120s."
exit 1
elif [[ "$exit_code" != "0" ]]; then
echo ">>> ${target} FAILED (exit code: $exit_code)"
exit 1
else
echo ">>> ${target} PASSED"
fi
}
run_test test_android_openssl
run_test test_android_restore_backup
EOF
chmod +x ci_run_android_tests.sh
- name: 'Run Android tests on emulator'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 28
arch: x86_64
emulator-options: -no-snapshot -no-window -no-audio -no-boot-anim -accel on
disable-animations: true
script: bash ci_run_android_tests.sh
+1
View File
@@ -146,3 +146,4 @@ macos-with-sign-build.sh
DeveloperIdApplicationCertificate.p12
DeveloperIdInstallerCertificate.p12
gwenv.sh
+4 -2
View File
@@ -5,7 +5,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(PROJECT AmneziaVPN)
if(NOT AMNEZIAVPN_VERSION)
set(AMNEZIAVPN_VERSION 5.0.2.1 CACHE STRING "Client app version")
set(AMNEZIAVPN_VERSION 5.1.0.0 CACHE STRING "Client app version")
endif()
set(QT_CREATOR_SKIP_PACKAGE_MANAGER_SETUP ON CACHE BOOL "" FORCE)
@@ -20,6 +20,8 @@ project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
HOMEPAGE_URL "https://amnezia.org/"
)
option(AMNEZIA_BUILD_TESTS "Build test executables" OFF)
# trigger conan to kick off `conan install` globally
find_package(OpenSSL REQUIRED)
if (PREBUILTS_ONLY)
@@ -32,7 +34,7 @@ set(RELEASE_DATE "${CURRENT_DATE}")
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
# bump by 2 on every release, because we're releasing two versions on the play store
set(APP_ANDROID_VERSION_CODE 2161)
set(APP_ANDROID_VERSION_CODE 2173)
if(DEFINED APP_ANDROID_VERSION_CODE_OFFSET)
math(EXPR APP_ANDROID_VERSION_CODE "${APP_ANDROID_VERSION_CODE} + ${APP_ANDROID_VERSION_CODE_OFFSET}")
endif()
+18 -2
View File
@@ -37,21 +37,30 @@ add_definitions(-DFREE_V2_ENDPOINT="$ENV{FREE_V2_ENDPOINT}")
add_definitions(-DPREM_V1_ENDPOINT="$ENV{PREM_V1_ENDPOINT}")
if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
set(PACKAGES ${PACKAGES} Widgets)
set(PACKAGES ${PACKAGES} Widgets HttpServer)
endif()
find_package(Qt6 REQUIRED COMPONENTS ${PACKAGES})
find_package(OpenSSL REQUIRED)
find_package(libagw REQUIRED)
set(LIBS ${LIBS}
Qt6::Core Qt6::Gui
Qt6::Network Qt6::Xml Qt6::RemoteObjects
Qt6::Quick Qt6::Svg Qt6::QuickControls2
Qt6::Core5Compat Qt6::Concurrent
amnezia::libagw
)
find_package(Qt6 QUIET OPTIONAL_COMPONENTS CorePrivate)
if(TARGET Qt6::CorePrivate)
set(LIBS ${LIBS} Qt6::CorePrivate)
add_compile_definitions(AMNEZIA_HAS_QZIPWRITER)
endif()
if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
set(LIBS ${LIBS} Qt6::Widgets)
set(LIBS ${LIBS} Qt6::Widgets Qt6::HttpServer)
endif()
qt_standard_project_setup()
@@ -71,6 +80,7 @@ endif()
qt6_add_resources(QRC ${QRC}
${CMAKE_CURRENT_LIST_DIR}/images/images.qrc
${CMAKE_CURRENT_LIST_DIR}/images/flagKit.qrc
${CMAKE_CURRENT_LIST_DIR}/resources/countries.qrc
${CMAKE_CURRENT_LIST_DIR}/client_scripts/clientScripts.qrc
${CLIENT_QML_QRC_FILE}
${CMAKE_CURRENT_LIST_DIR}/server_scripts/serverScripts.qrc
@@ -92,6 +102,8 @@ else()
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_hi_IN.ts
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_es_ES.ts
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_ko_KR.ts
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_fr_FR.ts
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_th_TH.ts
)
endif()
@@ -313,3 +325,7 @@ if(OVPN_SCRIPTS)
WORLD_READ WORLD_EXECUTE
)
endif()
if(AMNEZIA_BUILD_TESTS)
add_subdirectory(tests)
endif()
+7
View File
@@ -129,6 +129,11 @@ void AmneziaApplication::init()
win->setPersistentSceneGraph(true);
win->setPersistentGraphics(true);
#endif
#ifdef Q_OS_IOS
auto pageController = m_coreController->pageController();
pageController->setSafeAreaMargins(win->safeAreaMargins());
QObject::connect(win, &QWindow::safeAreaMarginsChanged, pageController, &PageController::setSafeAreaMargins);
#endif
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
win->show();
#else
@@ -179,7 +184,9 @@ void AmneziaApplication::init()
m_coreController->setQmlRoot();
#if CLIENT_ENABLE_APP_UPDATES
m_coreController->checkForAppUpdates();
#endif
#ifdef Q_OS_WIN //TODO
if (m_parser.isSet(m_optAutostart))
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="black">#FF0E0E11</color>
<color name="black">#FF101012</color>
<style name="NoActionBar">
<item name="android:windowBackground">@color/black</item>
<item name="android:colorBackground">@color/black</item>
@@ -37,6 +37,7 @@ import android.widget.Toast
import androidx.annotation.MainThread
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.graphics.Insets
import androidx.core.view.OnApplyWindowInsetsListener
import androidx.core.view.ViewCompat
@@ -44,6 +45,7 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import java.io.File
import java.io.IOException
import java.io.OutputStream
import kotlin.LazyThreadSafetyMode.NONE
import kotlin.coroutines.CoroutineContext
import kotlin.text.RegexOption.IGNORE_CASE
@@ -77,6 +79,7 @@ private const val CHECK_NOTIFICATION_PERMISSION_ACTION_CODE = 4
private const val PREFS_NOTIFICATION_PERMISSION_ASKED = "NOTIFICATION_PERMISSION_ASKED"
private const val OPEN_FILE_AFTER_RESUME_DELAY_MS = 400L
private const val KEY_PENDING_OPEN_FILE_URI = "pending_open_file_uri"
private const val FILE_PROVIDER_AUTHORITY = "org.amnezia.vpn.qtprovider"
class AmneziaActivity : QtActivity() {
@@ -733,28 +736,69 @@ class AmneziaActivity : QtActivity() {
@Suppress("unused")
fun saveFile(fileName: String, data: String) {
Log.d(TAG, "Save file $fileName")
createFile(fileName, "text/*") { os -> os.bufferedWriter().use { it.write(data) } }
}
@Suppress("unused")
fun saveFileBytes(fileName: String, data: ByteArray, mime: String) {
Log.d(TAG, "Save file $fileName, ${data.size} bytes, $mime")
createFile(fileName, mime) { os -> os.write(data) }
}
private fun createFile(fileName: String, mime: String, write: (OutputStream) -> Unit) {
mainScope.launch {
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "text/*"
type = mime
putExtra(Intent.EXTRA_TITLE, fileName)
}.also {
try {
startActivityForResult(it, CREATE_FILE_ACTION_CODE, ActivityResultHandler(
onSuccess = {
it?.data?.let { uri ->
val uri = it?.data
var saved = false
if (uri != null) {
Log.v(TAG, "Save file to $uri")
try {
contentResolver.openOutputStream(uri)?.use { os ->
os.bufferedWriter().use { it.write(data) }
write(os)
saved = true
}
} catch (e: IOException) {
Log.e(TAG, "Failed to save file $uri: $e")
// todo: send error to Qt
}
}
QtAndroidController.onFileSaved(saved, false)
},
onFail = {
QtAndroidController.onFileSaved(false, true)
}
))
} catch (_: ActivityNotFoundException) {
Toast.makeText(this@AmneziaActivity, "Unsupported", Toast.LENGTH_LONG).show()
QtAndroidController.onFileSaved(false, false)
}
}
}
}
@Suppress("unused")
fun shareFile(path: String, mime: String) {
Log.d(TAG, "Share file $path, $mime")
mainScope.launch {
val uri = try {
FileProvider.getUriForFile(this@AmneziaActivity, FILE_PROVIDER_AUTHORITY, File(path))
} catch (e: IllegalArgumentException) {
Log.e(TAG, "Failed to share file $path: $e")
return@launch
}
Intent(Intent.ACTION_SEND).apply {
type = mime
putExtra(Intent.EXTRA_STREAM, uri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}.also {
try {
startActivity(Intent.createChooser(it, null))
} catch (_: ActivityNotFoundException) {
Toast.makeText(this@AmneziaActivity, "Unsupported", Toast.LENGTH_LONG).show()
}
@@ -971,20 +1015,6 @@ class AmneziaActivity : QtActivity() {
}
}
@Suppress("unused")
fun exportLogsFile(fileName: String) {
Log.v(TAG, "Export logs file")
saveFile(fileName, Log.getLogs())
}
@Suppress("unused")
fun clearLogs() {
Log.v(TAG, "Clear logs")
mainScope.launch {
Log.clearLogs()
}
}
@Suppress("unused")
fun setScreenshotsEnabled(enabled: Boolean) {
Log.v(TAG, "Set screenshots enabled: $enabled")
@@ -22,6 +22,7 @@ object QtAndroidController {
external fun onStatisticsUpdate(rxBytes: Long, txBytes: Long)
external fun onFileOpened(uri: String)
external fun onFileSaved(success: Boolean, cancelled: Boolean)
external fun onConfigImported(data: String)
+138 -48
View File
@@ -1,5 +1,6 @@
package org.amnezia.vpn.util
import android.app.Application
import android.content.Context
import android.os.Build
import android.os.Process
@@ -22,9 +23,18 @@ import org.amnezia.vpn.util.Log.Priority.W
import android.util.Log as NativeLog
private const val TAG = "Log"
private const val LOG_FILE_NAME = "amneziaVPN.log"
private const val ROTATE_LOG_FILE_NAME = "amneziaVPN.rotate.log"
private const val APP_LOG_FILE_NAME = "app.log"
private const val APP_ROTATE_LOG_FILE_NAME = "app.rotate.log"
private const val TUNNEL_LOG_FILE_NAME = "tunnel.log"
private const val TUNNEL_ROTATE_LOG_FILE_NAME = "tunnel.rotate.log"
private const val LEGACY_LOG_FILE_NAME = "amneziaVPN.log"
private const val LEGACY_ROTATE_LOG_FILE_NAME = "amneziaVPN.rotate.log"
private const val LOCK_FILE_NAME = ".lock"
private const val TILE_SERVICE_PROCESS_SUFFIX = ":amneziaTileService"
private const val AWG_SERVICE_PROCESS_SUFFIX = ":amneziaAwgService"
private const val AWG_LOGCAT_TAG_PREFIX = " AmneziaWG/"
private const val STREAM_APP = 0
private const val STREAM_TUNNEL = 1
private const val DATE_TIME_PATTERN = "MM-dd HH:mm:ss.SSS"
private const val PREFS_SAVE_LOGS_KEY = "SAVE_LOGS"
private const val LOG_MAX_FILE_SIZE = 1024 * 1024
@@ -42,12 +52,16 @@ object Log {
private val dateTimeFormat: DateTimeFormatter = DateTimeFormatter.ofPattern(DATE_TIME_PATTERN)
private lateinit var logDir: File
private val logFile: File by lazy { File(logDir, LOG_FILE_NAME) }
private val rotateLogFile: File by lazy { File(logDir, ROTATE_LOG_FILE_NAME) }
private lateinit var logFile: File
private lateinit var rotateLogFile: File
private val fileLock: FileChannel by lazy { RandomAccessFile(File(logDir, LOCK_FILE_NAME).path, "rw").channel }
private val threadLock: ReentrantLock by lazy { ReentrantLock() }
private var isAwgProcess: Boolean = false
private val awgLogcatLock = Any()
private var awgLogcatReader: AwgLogcatReader? = null
@Volatile
private var _saveLogs: Boolean = false
var saveLogs: Boolean
@@ -60,6 +74,9 @@ object Log {
}
_saveLogs = value
Prefs.save(PREFS_SAVE_LOGS_KEY, value)
if (isAwgProcess) {
if (value) startAwgLogcatReader() else stopAwgLogcatReader()
}
}
}
@@ -96,21 +113,60 @@ object Log {
fun init(context: Context) {
v(TAG, "Init Log")
logDir = File(context.cacheDir, "logs")
val processName = Application.getProcessName()
val stream = if (':' in processName && !processName.endsWith(TILE_SERVICE_PROCESS_SUFFIX)) {
STREAM_TUNNEL
} else {
STREAM_APP
}
val (rotate, current) = streamFiles(stream)
rotateLogFile = rotate
logFile = current
isAwgProcess = processName.endsWith(AWG_SERVICE_PROCESS_SUFFIX)
saveLogs = Prefs.load(PREFS_SAVE_LOGS_KEY)
}
fun getLogs(): String =
"${deviceInfo()}\n${readLogs()}\nLOGCAT:\n${getLogcat()}"
@JvmStatic
fun getLogFiles(stream: Int): String {
val files = streamFiles(stream).toMutableList()
if (stream == STREAM_APP) files.addAll(0, legacyFiles().filter { it.exists() })
return files.joinToString("\n") { it.absolutePath }
}
fun clearLogs() {
@JvmStatic
fun getDeviceInfo(): String {
val sb = StringBuilder()
sb.append("Model: ").appendLine(Build.MODEL)
sb.append("Brand: ").appendLine(Build.BRAND)
sb.append("Product: ").appendLine(Build.PRODUCT)
sb.append("Device: ").appendLine(Build.DEVICE)
sb.append("Codename: ").appendLine(Build.VERSION.CODENAME)
sb.append("Release: ").appendLine(Build.VERSION.RELEASE)
sb.append("SDK: ").appendLine(Build.VERSION.SDK_INT)
sb.append("ABI: ").appendLine(Build.SUPPORTED_ABIS.joinToString())
return sb.toString()
}
@JvmStatic
fun clearStream(stream: Int) {
if (logDir.exists()) {
withLock {
logFile.delete()
rotateLogFile.delete()
streamFiles(stream).forEach { it.delete() }
if (stream == STREAM_APP) legacyFiles().forEach { it.delete() }
}
}
}
private fun streamFiles(stream: Int): List<File> =
if (stream == STREAM_TUNNEL) {
listOf(File(logDir, TUNNEL_ROTATE_LOG_FILE_NAME), File(logDir, TUNNEL_LOG_FILE_NAME))
} else {
listOf(File(logDir, APP_ROTATE_LOG_FILE_NAME), File(logDir, APP_LOG_FILE_NAME))
}
private fun legacyFiles(): List<File> =
listOf(File(logDir, LEGACY_ROTATE_LOG_FILE_NAME), File(logDir, LEGACY_LOG_FILE_NAME))
private fun log(tag: String, msg: String, priority: Priority) {
if (saveLogs && priority != V) saveLogMsg(formatLogMsg(tag, msg, priority))
@@ -136,51 +192,85 @@ object Log {
}
}
private fun startAwgLogcatReader() {
synchronized(awgLogcatLock) {
if (awgLogcatReader == null) {
awgLogcatReader = AwgLogcatReader().also { it.start() }
}
}
}
private fun stopAwgLogcatReader() {
synchronized(awgLogcatLock) {
awgLogcatReader?.shutdown()
awgLogcatReader = null
}
}
private class AwgLogcatReader : Thread("AwgLogcatReader") {
private val lock = Any()
private var process: java.lang.Process? = null
@Volatile
private var stopped = false
init {
isDaemon = true
}
override fun run() {
try {
follow()
} finally {
synchronized(awgLogcatLock) {
if (awgLogcatReader === this) awgLogcatReader = null
}
}
}
private fun follow() {
val logcat = try {
ProcessBuilder(
"logcat", "-v", "threadtime", "-v", "UTC", "--pid=${Process.myPid()}", "-T", "1"
).redirectErrorStream(true).start()
} catch (e: IOException) {
NativeLog.e(TAG, "Failed to start logcat: $e")
return
}
synchronized(lock) {
if (stopped) {
logcat.destroy()
return
}
process = logcat
}
try {
logcat.inputStream.bufferedReader().useLines { lines ->
lines.filter { it.contains(AWG_LOGCAT_TAG_PREFIX) }.forEach {
if (!stopped) saveLogMsg("$it\n")
}
}
if (!stopped) NativeLog.w(TAG, "Logcat exited unexpectedly")
} catch (e: IOException) {
if (!stopped) NativeLog.e(TAG, "Failed to read logcat: $e")
} finally {
logcat.destroy()
}
}
fun shutdown() {
synchronized(lock) {
stopped = true
process?.destroy()
}
}
}
private fun formatLogMsg(tag: String, msg: String, priority: Priority): String {
val utcDate = ZonedDateTime.now(ZoneOffset.UTC).format(dateTimeFormat)
return "${utcDate}Z ${Process.myPid()} ${Process.myTid()} $priority [${Thread.currentThread().name}] " +
"$tag: $msg\n"
}
private fun deviceInfo(): String {
val sb = StringBuilder()
sb.append("Model: ").appendLine(Build.MODEL)
sb.append("Brand: ").appendLine(Build.BRAND)
sb.append("Product: ").appendLine(Build.PRODUCT)
sb.append("Device: ").appendLine(Build.DEVICE)
sb.append("Codename: ").appendLine(Build.VERSION.CODENAME)
sb.append("Release: ").appendLine(Build.VERSION.RELEASE)
sb.append("SDK: ").appendLine(Build.VERSION.SDK_INT)
sb.append("ABI: ").appendLine(Build.SUPPORTED_ABIS.joinToString())
return sb.toString()
}
private fun readLogs(): String {
var logText = ""
withLock {
try {
if (rotateLogFile.exists()) logText = rotateLogFile.readText()
if (logFile.exists()) logText += logFile.readText()
} catch (e: IOException) {
val errorMsg = "Failed to read log: $e"
NativeLog.e(TAG, errorMsg)
logText += errorMsg
}
}
return logText
}
private fun getLogcat(): String {
try {
val process = ProcessBuilder("logcat", "-d").redirectErrorStream(true).start()
return process.inputStream.reader().readText()
} catch (e: IOException) {
val errorMsg = "Failed to get logcat log: $e"
NativeLog.e(TAG, errorMsg)
return errorMsg
}
}
private fun withLock(block: () -> Unit) {
threadLock.lock()
try {
+4
View File
@@ -58,3 +58,7 @@ endif()
if(NOT CLIENT_NOTIFICATION_IDENTIFIER)
set(CLIENT_NOTIFICATION_IDENTIFIER "${CLIENT_TS_PREFIX}" CACHE STRING "Local notification request identifier")
endif()
if(NOT DEFINED CLIENT_ENABLE_APP_UPDATES)
set(CLIENT_ENABLE_APP_UPDATES 1)
endif()
+11 -2
View File
@@ -44,6 +44,8 @@ set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/core/controllers/selfhosted/exportController.h
${CLIENT_ROOT_DIR}/core/controllers/connectionController.h
${CLIENT_ROOT_DIR}/core/controllers/settingsController.h
${CLIENT_ROOT_DIR}/core/controllers/localProxyController.h
${CLIENT_ROOT_DIR}/core/controllers/logsController.h
${CLIENT_ROOT_DIR}/core/controllers/api/servicesCatalogController.h
${CLIENT_ROOT_DIR}/core/controllers/api/storePurchaseController.h
${CLIENT_ROOT_DIR}/core/controllers/api/subscriptionController.h
@@ -65,9 +67,12 @@ set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/core/utils/serialization/transfer.h
${CLIENT_ROOT_DIR}/../common/logger/logger.h
${CLIENT_ROOT_DIR}/../common/crypto/cryptoUtils.h
${CLIENT_ROOT_DIR}/ui/utils/qmlUtils.h
${CLIENT_ROOT_DIR}/core/utils/api/apiUtils.h
${CLIENT_ROOT_DIR}/core/utils/api/countryCatalog.h
${CLIENT_ROOT_DIR}/core/utils/api/gatewayPayloadBuilder.h
${CLIENT_ROOT_DIR}/core/utils/localProxy/localProxyConstants.h
${CLIENT_ROOT_DIR}/core/utils/localProxy/portUtils.h
${CLIENT_ROOT_DIR}/core/utils/localProxy/xrayConfigUtils.h
${CLIENT_ROOT_DIR}/core/utils/osSignalHandler.h
${CLIENT_ROOT_DIR}/core/utils/swiftBridge.h
${CLIENT_ROOT_DIR}/core/utils/utilities.h
@@ -129,6 +134,8 @@ set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/core/controllers/selfhosted/exportController.cpp
${CLIENT_ROOT_DIR}/core/controllers/connectionController.cpp
${CLIENT_ROOT_DIR}/core/controllers/settingsController.cpp
${CLIENT_ROOT_DIR}/core/controllers/localProxyController.cpp
${CLIENT_ROOT_DIR}/core/controllers/logsController.cpp
${CLIENT_ROOT_DIR}/core/controllers/api/servicesCatalogController.cpp
${CLIENT_ROOT_DIR}/core/controllers/api/storePurchaseController.cpp
${CLIENT_ROOT_DIR}/core/controllers/api/subscriptionController.cpp
@@ -152,9 +159,11 @@ set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/core/utils/serialization/vmess_new.cpp
${CLIENT_ROOT_DIR}/../common/logger/logger.cpp
${CLIENT_ROOT_DIR}/../common/crypto/cryptoUtils.cpp
${CLIENT_ROOT_DIR}/ui/utils/qmlUtils.cpp
${CLIENT_ROOT_DIR}/core/utils/api/apiUtils.cpp
${CLIENT_ROOT_DIR}/core/utils/api/countryCatalog.cpp
${CLIENT_ROOT_DIR}/core/utils/api/gatewayPayloadBuilder.cpp
${CLIENT_ROOT_DIR}/core/utils/localProxy/portUtils.cpp
${CLIENT_ROOT_DIR}/core/utils/localProxy/xrayConfigUtils.cpp
${CLIENT_ROOT_DIR}/core/utils/serverConfigUtils.cpp
${CLIENT_ROOT_DIR}/core/utils/osSignalHandler.cpp
${CLIENT_ROOT_DIR}/core/utils/utilities.cpp
+362 -463
View File
@@ -4,6 +4,7 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QDateTime>
#include <QThread>
#include <QUuid>
#include "logger.h"
@@ -23,124 +24,49 @@
namespace {
Logger logger("XrayConfigurator");
QString normalizeXhttpMode(const QString &m) {
const QString t = m.trimmed();
if (t.isEmpty() || t.compare(QLatin1String("Auto"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("auto");
}
if (t.compare(QLatin1String("Packet-up"), Qt::CaseInsensitive) == 0)
return QStringLiteral("packet-up");
if (t.compare(QLatin1String("Stream-up"), Qt::CaseInsensitive) == 0)
return QStringLiteral("stream-up");
if (t.compare(QLatin1String("Stream-one"), Qt::CaseInsensitive) == 0)
return QStringLiteral("stream-one");
return t.toLower();
}
// Xray-core: empty → path; "None" in UI → omit (core default path)
QString normalizeSessionSeqPlacement(const QString &p)
{
if (p.isEmpty() || p.compare(QLatin1String("None"), Qt::CaseInsensitive) == 0)
return {};
return p.toLower();
}
QString normalizeUplinkDataPlacement(const QString &p)
{
if (p.isEmpty() || p.compare(QLatin1String("Body"), Qt::CaseInsensitive) == 0)
return QStringLiteral("body");
if (p.compare(QLatin1String("Auto"), Qt::CaseInsensitive) == 0)
return QStringLiteral("auto");
if (p.compare(QLatin1String("Query"), Qt::CaseInsensitive) == 0)
// "Query" is not valid for uplink payload in splithttp; closest documented mode
return QStringLiteral("header");
return p.toLower();
}
// splithttp: cookie | header | query | queryInHeader (not "body")
QString normalizeXPaddingPlacement(const QString &p)
{
QString t = p.trimmed();
if (t.isEmpty())
return QString::fromLatin1(amnezia::protocols::xray::defaultXPaddingPlacement).toLower();
if (t.compare(QLatin1String("Body"), Qt::CaseInsensitive) == 0)
return QStringLiteral("queryInHeader");
if (t.contains(QLatin1String("queryInHeader"), Qt::CaseInsensitive)
|| t.compare(QLatin1String("Query in header"), Qt::CaseInsensitive) == 0)
return QStringLiteral("queryInHeader");
return t.toLower();
}
// splithttp: repeat-x | tokenish
QString normalizeXPaddingMethod(const QString &m)
{
QString t = m.trimmed();
if (t.isEmpty() || t.compare(QLatin1String("Repeat-x"), Qt::CaseInsensitive) == 0)
return QStringLiteral("repeat-x");
if (t.compare(QLatin1String("Tokenish"), Qt::CaseInsensitive) == 0)
return QStringLiteral("tokenish");
if (t.compare(QLatin1String("Random"), Qt::CaseInsensitive) == 0
|| t.compare(QLatin1String("Zero"), Qt::CaseInsensitive) == 0)
return QStringLiteral("repeat-x");
return t.toLower();
}
// xray wants int ranges as "from-to" string, not a {from,to} object.
QString makeRangeString(const QString &minV, const QString &maxV)
{
return minV + QLatin1Char('-') + maxV;
}
void putIntRangeIfAny(QJsonObject &obj, const char *key, QString minV, QString maxV, const char *fallbackMin,
const char *fallbackMax)
{
if (minV.isEmpty() && maxV.isEmpty())
return;
if (minV.isEmpty())
minV = QString::fromLatin1(fallbackMin);
if (maxV.isEmpty())
maxV = QString::fromLatin1(fallbackMax);
obj[QString::fromUtf8(key)] = makeRangeString(minV, maxV);
}
QString effectiveClientFlow(const amnezia::XrayServerConfig &srv)
{
const bool rawTransport = srv.transport.isEmpty() || srv.transport == QLatin1String("raw");
const bool secureFlow =
srv.security == QLatin1String("tls") || srv.security == QLatin1String("reality");
return (rawTransport && secureFlow) ? srv.flow : QString();
return amnezia::xrayEffective::clientFlow(srv);
}
QString effectiveSecurity(const amnezia::XrayServerConfig &srv)
{
if (srv.transport == QLatin1String("mkcp") && srv.security == QLatin1String("reality")) {
return QStringLiteral("none");
}
return srv.security;
return amnezia::xrayEffective::security(srv);
}
// Desktop applies this in XrayProtocol::start(); iOS/Android pass JSON straight to libxray — same fixes here.
void sanitizeXrayNativeConfig(amnezia::ProtocolConfig &pc)
{
QString c = pc.nativeConfig();
if (c.isEmpty()) {
return;
const QString before = pc.nativeConfig();
const QString after = amnezia::sanitizeNativeConfig(before);
if (after != before) {
pc.setNativeConfig(after);
}
bool changed = false;
if (c.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) {
c.replace(QLatin1String("Mozilla/5.0"), QString::fromLatin1(amnezia::protocols::xray::defaultFingerprint),
Qt::CaseInsensitive);
changed = true;
}
const QString legacyListen = QString::fromLatin1(amnezia::protocols::xray::defaultLocalAddr);
const QString listenOk = QString::fromLatin1(amnezia::protocols::xray::defaultLocalListenAddr);
if (c.contains(legacyListen)) {
c.replace(legacyListen, listenOk);
changed = true;
}
if (changed) {
pc.setNativeConfig(c);
}
QString tlsFingerprintFromEnsureOutput(const QString &stdOut)
{
const QString prefix = QStringLiteral("amnezia_tls_fp=");
const auto lines = stdOut.split(QLatin1Char('\n'));
for (QString line : lines) {
line = line.trimmed();
if (!line.startsWith(prefix))
continue;
QString fp = line.mid(prefix.size());
fp.remove(QLatin1Char(':'));
fp.remove(QLatin1Char(' '));
fp = fp.toLower();
if (fp.size() != 64)
return {};
for (const QChar ch : fp) {
const ushort u = ch.unicode();
const bool hex = (u >= '0' && u <= '9') || (u >= 'a' && u <= 'f');
if (!hex)
return {};
}
return fp;
}
return {};
}
} // namespace
@@ -157,30 +83,6 @@ amnezia::ProtocolConfig XrayConfigurator::processConfigWithLocalSettings(const a
return protocolConfig;
}
ErrorCode XrayConfigurator::uploadServerConfigJson(const ServerCredentials &credentials, DockerContainer container,
const DnsSettings &dnsSettings, const QJsonObject &serverConfig) const
{
const QString updatedConfig = QJsonDocument(serverConfig).toJson();
ErrorCode errorCode = m_sshSession->uploadTextFileToContainer(
container, credentials, updatedConfig, amnezia::protocols::xray::serverConfigPath,
libssh::ScpOverwriteMode::ScpOverwriteExisting);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Failed to upload updated config";
return errorCode;
}
const QString restartScript = QStringLiteral("sudo docker restart $CONTAINER_NAME");
errorCode = m_sshSession->runScript(
credentials,
m_sshSession->replaceVars(restartScript,
amnezia::genBaseVars(credentials, container, dnsSettings.primaryDns,
dnsSettings.secondaryDns)));
if (errorCode != ErrorCode::NoError) {
logger.error() << "Failed to restart container";
}
return errorCode;
}
ErrorCode XrayConfigurator::readRealityKeyFiles(const DockerContainer container, const ServerCredentials &credentials,
QString &outPublicKey, QString &outShortId) const
{
@@ -196,12 +98,17 @@ ErrorCode XrayConfigurator::readRealityKeyFiles(const DockerContainer container,
if (fileError == ErrorCode::NoError && !out.isEmpty()) {
return ErrorCode::NoError;
}
if (fileError != ErrorCode::NoError) {
logger.error() << "Xray readRealityKeyFiles: ssh failed path=" << path
<< "error=" << static_cast<int>(fileError);
return fileError;
}
if (attempt < 2) {
QThread::msleep(500);
}
}
logger.error() << "Xray readRealityKeyFiles: failed path=" << path;
return ErrorCode::XrayRealityKeysReadFailed;
logger.error() << "Xray readRealityKeyFiles: key file empty path=" << path;
return ErrorCode::XrayKeysReadFailed;
};
ErrorCode errorCode = readKeyFile(QString::fromLatin1(amnezia::protocols::xray::PublicKeyPath), outPublicKey);
@@ -232,10 +139,6 @@ ErrorCode XrayConfigurator::applyServerSettingsToRemote(const ServerCredentials
return ErrorCode::NoError;
}
logger.info() << "Xray applyServerSettings: start"
<< "container=" << static_cast<int>(container) << "host=" << credentials.hostName
<< "transport=" << srv.transport << "security=" << srv.security << "port=" << srv.port
<< "appendClient=" << appendNewClient;
const QString flowValue = effectiveClientFlow(srv);
QString realityPublicKey;
QString realityShortId;
@@ -264,71 +167,59 @@ ErrorCode XrayConfigurator::applyServerSettingsToRemote(const ServerCredentials
}
QJsonObject serverConfig = doc.object();
if (!serverConfig.contains(amnezia::protocols::xray::inbounds)) {
logger.error() << "Server config missing 'inbounds' field";
return ErrorCode::XrayServerConfigInvalid;
}
const QJsonObject originalConfig = serverConfig;
QJsonArray inbounds = serverConfig[amnezia::protocols::xray::inbounds].toArray();
if (inbounds.isEmpty()) {
logger.error() << "Server config has empty 'inbounds' array";
return ErrorCode::XrayServerConfigInvalid;
}
QJsonObject inbound = inbounds[0].toObject();
if (!inbound.contains(amnezia::protocols::xray::settings)) {
logger.error() << "Inbound missing 'settings' field";
return ErrorCode::XrayServerConfigInvalid;
}
QJsonObject settings = inbound[amnezia::protocols::xray::settings].toObject();
if (!settings.contains(amnezia::protocols::xray::clients)) {
settings[amnezia::protocols::xray::clients] = QJsonArray {};
}
QJsonArray clients = settings[amnezia::protocols::xray::clients].toArray();
QJsonArray clients = XrayServerConfig::clientsFromServerInboundJson(serverConfig);
QString clientId;
if (appendNewClient) {
clientId = QUuid::createUuid().toString(QUuid::WithoutBraces);
QJsonObject clientEntry;
clientEntry[amnezia::protocols::xray::id] = clientId;
if (!flowValue.isEmpty()) {
clientEntry[amnezia::protocols::xray::flow] = flowValue;
const bool adoptingOwnAccount = !(xrayCfg->hasClientConfig() && !xrayCfg->clientConfig->id.isEmpty());
QString ownClientId;
if (readContainerKeyFile(container, credentials, QString::fromLatin1(amnezia::protocols::xray::uuidPath),
ownClientId)
!= ErrorCode::NoError) {
ownClientId.clear();
}
const int existingIndex =
adoptingOwnAccount ? XrayServerConfig::indexOfClient(clients, ownClientId) : -1;
if (existingIndex >= 0) {
clientId = ownClientId;
clients[existingIndex] =
XrayServerConfig::applyFlowToClient(clients[existingIndex].toObject(), flowValue);
} else {
const bool takeOverOwnAccount = adoptingOwnAccount && !ownClientId.isEmpty();
clientId = takeOverOwnAccount ? ownClientId : QUuid::createUuid().toString(QUuid::WithoutBraces);
clients.append(XrayServerConfig::makeClientEntry(clientId, flowValue));
}
clients.append(clientEntry);
} else {
if (clients.isEmpty()) {
logger.error() << "Server config has no VLESS clients";
return ErrorCode::XrayServerNoVlessClients;
}
clientId = clients[0].toObject()[amnezia::protocols::xray::id].toString();
clientId = XrayServerConfig::firstClientId(clients);
if (clientId.isEmpty()) {
logger.error() << "Server config VLESS client has empty id";
return ErrorCode::XrayServerNoVlessClients;
}
QJsonArray updatedClients;
for (const QJsonValue &v : clients) {
QJsonObject c = v.toObject();
if (flowValue.isEmpty()) {
c.remove(amnezia::protocols::xray::flow);
} else {
c[amnezia::protocols::xray::flow] = flowValue;
}
updatedClients.append(c);
}
clients = updatedClients;
clients = XrayServerConfig::applyFlowToClients(clients, flowValue);
}
settings[amnezia::protocols::xray::clients] = clients;
inbound[amnezia::protocols::xray::settings] = settings;
inbounds[0] = inbound;
serverConfig[amnezia::protocols::xray::inbounds] = inbounds;
const XrayServerJsonStatus writeStatus =
XrayServerConfig::setClientsInServerInboundJson(serverConfig, clients);
if (writeStatus != XrayServerJsonStatus::Ok) {
return ErrorCode::XrayServerConfigInvalid;
}
errorCode = uploadServerConfigJson(credentials, container, dnsSettings, serverConfig);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Xray applyServerSettings: upload/restart failed, error=" << static_cast<int>(errorCode);
return errorCode;
const QString listenPort = srv.port.isEmpty() ? QString::fromLatin1(amnezia::protocols::xray::defaultPort)
: srv.port;
if (serverConfig != originalConfig) {
errorCode = uploadServerConfigAtomically(credentials, container, listenPort, serverConfig);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
}
logger.info() << "Xray applyServerSettings: server config uploaded and container restarted";
@@ -337,7 +228,8 @@ ErrorCode XrayConfigurator::applyServerSettingsToRemote(const ServerCredentials
}
XrayProtocolConfig updated =
buildClientProtocolConfig(credentials, container, srv, clientId, errorCode, realityPublicKey, realityShortId);
buildClientProtocolConfig(credentials, container, srv, clientId, errorCode,
realityPublicKey, realityShortId);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Xray applyServerSettings: buildClientProtocolConfig failed, error="
<< static_cast<int>(errorCode);
@@ -360,19 +252,24 @@ ErrorCode XrayConfigurator::readContainerKeyFile(DockerContainer container, cons
if (fileError == ErrorCode::NoError && !out.isEmpty()) {
return ErrorCode::NoError;
}
if (fileError != ErrorCode::NoError) {
logger.error() << "Xray readContainerKeyFile: ssh failed path=" << path
<< "error=" << static_cast<int>(fileError);
return fileError;
}
if (attempt < 2) {
QThread::msleep(500);
}
}
logger.error() << "Xray readContainerKeyFile: failed path=" << path;
return ErrorCode::XrayRealityKeysReadFailed;
logger.error() << "Xray readContainerKeyFile: key file empty path=" << path;
return ErrorCode::XrayKeysReadFailed;
}
ErrorCode XrayConfigurator::writeServerConfigForSetup(const ServerCredentials &credentials, DockerContainer container,
ContainerConfig &containerConfig, const DnsSettings &dnsSettings)
ContainerConfig &containerConfig, const DnsSettings &dnsSettings,
bool useAtomicApply)
{
Q_UNUSED(dnsSettings);
namespace px = amnezia::protocols::xray;
const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>();
if (!xrayCfg) {
@@ -385,82 +282,72 @@ ErrorCode XrayConfigurator::writeServerConfigForSetup(const ServerCredentials &c
return ErrorCode::NoError;
}
logger.info() << "Xray writeServerConfigForSetup: start container=" << static_cast<int>(container)
<< "transport=" << srv.transport << "security=" << srv.security << "port=" << srv.port;
ErrorCode errorCode = ErrorCode::NoError;
QString clientId;
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(px::uuidPath), clientId);
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(amnezia::protocols::xray::uuidPath), clientId);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
const QString securityEff = effectiveSecurity(srv);
QString tlsPin;
if (securityEff == QLatin1String("tls")) {
errorCode = ensureTlsCertificate(credentials, container, tlsPin);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
}
QString realityPrivateKey;
QString realityPublicKey;
QString realityShortId;
if (securityEff == QLatin1String("reality")) {
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(px::PrivateKeyPath), realityPrivateKey);
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(amnezia::protocols::xray::PrivateKeyPath), realityPrivateKey);
if (errorCode != ErrorCode::NoError)
return errorCode;
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(px::PublicKeyPath), realityPublicKey);
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(amnezia::protocols::xray::PublicKeyPath), realityPublicKey);
if (errorCode != ErrorCode::NoError)
return errorCode;
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(px::shortidPath), realityShortId);
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(amnezia::protocols::xray::shortidPath), realityShortId);
if (errorCode != ErrorCode::NoError)
return errorCode;
}
QJsonObject streamSettings = buildStreamSettings(srv, clientId);
if (securityEff == QLatin1String("reality")) {
const QString siteEff = srv.site.isEmpty() ? QString::fromLatin1(px::defaultSite) : srv.site;
const QString sniEff = srv.sni.isEmpty() ? siteEff : srv.sni;
const QString fpEff = srv.fingerprint.isEmpty() ? QString::fromLatin1(px::defaultFingerprint) : srv.fingerprint;
QJsonObject rs;
rs[QStringLiteral("dest")] = siteEff + QStringLiteral(":443");
rs[px::fingerprint] = fpEff;
rs[QStringLiteral("privateKey")] = realityPrivateKey;
rs[px::serverNames] = QJsonArray { sniEff };
rs[QStringLiteral("shortIds")] = QJsonArray { realityShortId };
streamSettings[px::realitySettings] = rs;
}
QJsonObject clientEntry;
clientEntry[px::id] = clientId;
const QString flowValue = effectiveClientFlow(srv);
if (!flowValue.isEmpty()) {
clientEntry[px::flow] = flowValue;
}
QJsonObject settings;
settings[px::clients] = QJsonArray { clientEntry };
settings[QStringLiteral("decryption")] = QStringLiteral("none");
QJsonObject inbound;
inbound[px::port] = srv.port.isEmpty() ? QString(px::defaultPort).toInt() : srv.port.toInt();
inbound[QStringLiteral("protocol")] = QStringLiteral("vless");
inbound[px::settings] = settings;
inbound[px::streamSettings] = streamSettings;
QJsonObject serverConfig;
serverConfig[QStringLiteral("log")] = QJsonObject { { QStringLiteral("loglevel"), QStringLiteral("error") } };
serverConfig[px::inbounds] = QJsonArray { inbound };
serverConfig[px::outbounds] =
QJsonArray { QJsonObject { { QStringLiteral("protocol"), QStringLiteral("freedom") } } };
const QString json = QString::fromUtf8(QJsonDocument(serverConfig).toJson());
errorCode = m_sshSession->uploadTextFileToContainer(container, credentials, json,
QString::fromLatin1(px::serverConfigPath),
libssh::ScpOverwriteMode::ScpOverwriteExisting);
const QJsonArray clients = collectServerClients(credentials, container, flowValue, clientId, errorCode);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Xray writeServerConfigForSetup: upload failed, error=" << static_cast<int>(errorCode);
return errorCode;
}
const QString portEff = srv.port.isEmpty() ? QString::fromLatin1(amnezia::protocols::xray::defaultPort) : srv.port;
XrayServerInboundInputs inputs;
inputs.clients = clients;
inputs.realityPrivateKey = realityPrivateKey;
inputs.realityShortId = realityShortId;
const QJsonObject inboundJson = srv.toServerInboundJson(inputs);
if (useAtomicApply) {
errorCode = uploadServerConfigAtomically(credentials, container, portEff, inboundJson);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
} else {
const QString json = QString::fromUtf8(QJsonDocument(inboundJson).toJson());
errorCode = m_sshSession->uploadTextFileToContainer(container, credentials, json,
QString::fromLatin1(amnezia::protocols::xray::serverConfigPath),
libssh::ScpOverwriteMode::ScpOverwriteExisting);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
}
XrayProtocolConfig updated =
buildClientProtocolConfig(credentials, container, srv, clientId, errorCode, realityPublicKey, realityShortId);
buildClientProtocolConfig(credentials, container, srv, clientId, errorCode,
realityPublicKey, realityShortId, tlsPin);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Xray writeServerConfigForSetup: buildClientProtocolConfig failed, error="
<< static_cast<int>(errorCode);
@@ -471,6 +358,217 @@ ErrorCode XrayConfigurator::writeServerConfigForSetup(const ServerCredentials &c
return ErrorCode::NoError;
}
QJsonArray XrayConfigurator::collectServerClients(const ServerCredentials &credentials, DockerContainer container,
const QString &flowValue, const QString &fallbackClientId,
ErrorCode &outError) const
{
outError = ErrorCode::NoError;
const QString configPath = QString::fromLatin1(amnezia::protocols::xray::serverConfigPath);
QString currentConfig;
for (int attempt = 0; attempt < 3; ++attempt) {
ErrorCode readError = ErrorCode::NoError;
currentConfig = m_sshSession->getTextFileFromContainer(container, credentials, configPath, readError);
if (readError == ErrorCode::NoError && !currentConfig.trimmed().isEmpty()) {
break;
}
currentConfig.clear();
if (attempt < 2) {
QThread::msleep(500);
}
}
QJsonArray existing;
if (currentConfig.trimmed().isEmpty()) {
QString probeOut;
auto collect = [&probeOut](const QString &data, libssh::Client &) {
probeOut += data + "\n";
return ErrorCode::NoError;
};
const QString probe =
QStringLiteral("if [ -s %1 ]; then echo amnezia_config=present; else echo amnezia_config=absent; fi")
.arg(configPath);
const ErrorCode probeError = m_sshSession->runContainerScript(credentials, container, probe, collect, collect);
if (probeError != ErrorCode::NoError || !probeOut.contains(QLatin1String("amnezia_config="))) {
outError = ErrorCode::XrayServerConfigInvalid;
return {};
}
if (probeOut.contains(QLatin1String("amnezia_config=present"))) {
outError = ErrorCode::XrayServerConfigInvalid;
return {};
}
} else {
const QJsonDocument doc = QJsonDocument::fromJson(currentConfig.toUtf8());
if (!doc.isObject()) {
outError = ErrorCode::XrayServerConfigInvalid;
return {};
}
existing = XrayServerConfig::clientsFromServerInboundJson(doc.object());
}
QJsonArray clients =
XrayServerConfig::applyFlowToClients(existing, flowValue, XrayClientListFilter::DropWithoutId);
if (!fallbackClientId.isEmpty()
&& XrayServerConfig::indexOfClient(clients, fallbackClientId) < 0) {
clients.append(XrayServerConfig::makeClientEntry(fallbackClientId, flowValue));
}
return clients;
}
ErrorCode XrayConfigurator::ensureTlsCertificate(const ServerCredentials &credentials, DockerContainer container,
QString &outFingerprint) const
{
outFingerprint.clear();
QString stdOut;
auto collect = [&stdOut](const QString &data, libssh::Client &) {
stdOut += data + QLatin1Char('\n');
return ErrorCode::NoError;
};
const QString certPath = QString::fromLatin1(amnezia::protocols::xray::tlsCertPath);
const QString keyPath = QString::fromLatin1(amnezia::protocols::xray::tlsKeyPath);
const QString script = QStringLiteral(
"has_key() { [ -f \"$1\" ] && [ -n \"$(tr -d '[:space:]' < \"$1\" 2>/dev/null)\" ]; }\n"
"CERT=%1\n"
"KEY=%2\n"
"if has_key \"$CERT\" && has_key \"$KEY\"; then echo amnezia_tls=present;\n"
"else\n"
" openssl req -x509 -newkey rsa:2048 -nodes -days 3650 -subj \"/CN=amnezia-xray\" "
"-keyout \"$KEY\" -out \"$CERT\" || true\n"
" if has_key \"$CERT\" && has_key \"$KEY\"; then echo amnezia_tls=created;\n"
" else echo amnezia_tls=failed; exit 1; fi\n"
"fi\n"
"FP=$(openssl x509 -noout -fingerprint -sha256 -in \"$CERT\" 2>/dev/null "
"| sed 's/.*Fingerprint=//' | tr -d ':' | tr 'A-F' 'a-f')\n"
"if [ -z \"$FP\" ]; then echo amnezia_tls=failed; exit 1; fi\n"
"echo amnezia_tls_fp=$FP\n"
"exit 0\n")
.arg(certPath, keyPath);
const ErrorCode scriptError = m_sshSession->runContainerScript(credentials, container, script, collect, collect);
const bool present = stdOut.contains(QLatin1String("amnezia_tls=present"));
const bool created = stdOut.contains(QLatin1String("amnezia_tls=created"));
outFingerprint = tlsFingerprintFromEnsureOutput(stdOut);
if (scriptError != ErrorCode::NoError || (!present && !created) || outFingerprint.isEmpty()) {
outFingerprint.clear();
return ErrorCode::XrayTlsNotSupported;
}
return ErrorCode::NoError;
}
ErrorCode XrayConfigurator::uploadServerConfigAtomically(const ServerCredentials &credentials,
DockerContainer container, const QString &listenPort,
const QJsonObject &serverConfig) const
{
const QString livePath = QString::fromLatin1(amnezia::protocols::xray::serverConfigPath);
const QString stagedPath = QStringLiteral("/opt/amnezia/xray/server.new.json");
const QString json = QString::fromUtf8(QJsonDocument(serverConfig).toJson());
ErrorCode errorCode = m_sshSession->uploadTextFileToContainer(container, credentials, json, stagedPath,
libssh::ScpOverwriteMode::ScpOverwriteExisting);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
const QString swapScript = QStringLiteral(
"LIVE=%1\n"
"STAGED=%2\n"
"BACKUP=\"$LIVE.bak\"\n"
"if [ ! -s \"$STAGED\" ]; then echo \"amnezia_apply=missing\"; exit 0; fi\n"
"if ! xray -test -format json -config \"$STAGED\" > /tmp/xray_test.log 2>&1; then\n"
" echo \"amnezia_apply=invalid\"\n"
" rm -f \"$STAGED\"\n"
" exit 0\n"
"fi\n"
"if [ -f \"$LIVE\" ]; then cp \"$LIVE\" \"$BACKUP\"; fi\n"
"mv \"$STAGED\" \"$LIVE\"\n"
"echo \"amnezia_apply=swapped\"\n")
.arg(livePath, stagedPath);
QString swapOut;
auto collectSwap = [&swapOut](const QString &data, libssh::Client &) {
swapOut += data + "\n";
return ErrorCode::NoError;
};
errorCode = m_sshSession->runContainerScript(credentials, container, swapScript, collectSwap, collectSwap);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
if (swapOut.contains(QLatin1String("amnezia_apply=missing"))) {
return ErrorCode::XrayServerConfigRejected;
}
if (swapOut.contains(QLatin1String("amnezia_apply=invalid"))) {
return ErrorCode::XrayServerConfigRejected;
}
if (!swapOut.contains(QLatin1String("amnezia_apply=swapped"))) {
return ErrorCode::XrayServerConfigInvalid;
}
if (restartXrayContainer(credentials, container) && xrayProcessIsUp(credentials, container)) {
m_sshSession->runContainerScript(credentials, container,
QStringLiteral("rm -f %1.bak").arg(livePath));
return ErrorCode::NoError;
}
const QString restoreScript = QStringLiteral(
"LIVE=%1\n"
"BACKUP=\"$LIVE.bak\"\n"
"if [ -f \"$BACKUP\" ]; then cp \"$BACKUP\" \"$LIVE\"; echo amnezia_restore=done; "
"else echo amnezia_restore=nobackup; fi\n")
.arg(livePath);
QString restoreOut;
auto collectRestore = [&restoreOut](const QString &data, libssh::Client &) {
restoreOut += data + "\n";
return ErrorCode::NoError;
};
m_sshSession->runContainerScript(credentials, container, restoreScript, collectRestore, collectRestore);
if (!restoreOut.contains(QLatin1String("amnezia_restore=done"))) {
return ErrorCode::XrayServerNotServing;
}
if (restartXrayContainer(credentials, container) && xrayProcessIsUp(credentials, container)) {
return ErrorCode::XrayServerConfigRolledBack;
}
return ErrorCode::XrayServerNotServing;
}
bool XrayConfigurator::restartXrayContainer(const ServerCredentials &credentials, DockerContainer container) const
{
const ErrorCode errorCode = m_sshSession->runScript(
credentials,
SshSession::replaceVars(QStringLiteral("sudo docker restart $CONTAINER_NAME"),
amnezia::genBaseVars(credentials, container, QString(), QString())));
if (errorCode != ErrorCode::NoError) {
return false;
}
return true;
}
bool XrayConfigurator::xrayProcessIsUp(const ServerCredentials &credentials, DockerContainer container) const
{
const QString script = QStringLiteral(
"for i in 1 2 3 4 5 6; do\n"
" if pgrep xray >/dev/null 2>&1 || ps 2>/dev/null | grep -v grep | grep -q \"[x]ray\"; then\n"
" echo amnezia_serving=yes\n"
" exit 0\n"
" fi\n"
" sleep 1\n"
"done\n"
"echo amnezia_serving=no\n");
QString out;
auto collect = [&out](const QString &data, libssh::Client &) {
out += data + "\n";
return ErrorCode::NoError;
};
const ErrorCode errorCode = m_sshSession->runContainerScript(credentials, container, script, collect, collect);
if (errorCode != ErrorCode::NoError) {
return false;
}
return out.contains(QLatin1String("amnezia_serving=yes"));
}
QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentials, DockerContainer container,
const ContainerConfig &containerConfig,
const DnsSettings &dnsSettings,
@@ -489,17 +587,21 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia
XrayProtocolConfig XrayConfigurator::buildClientProtocolConfig(const ServerCredentials &credentials,
DockerContainer container,
const XrayServerConfig &srv, const QString &clientId,
const XrayServerConfig &srv,
const QString &clientId,
ErrorCode &errorCode,
const QString &prefetchedRealityPublicKey,
const QString &prefetchedRealityShortId) const
const QString &prefetchedRealityShortId,
const QString &prefetchedTlsPin) const
{
QString xrayPublicKey = prefetchedRealityPublicKey;
QString xrayShortId = prefetchedRealityShortId;
QString tlsPin = prefetchedTlsPin;
const QString securityEff = effectiveSecurity(srv);
if (securityEff == QLatin1String("reality")) {
if (securityEff == QLatin1String(amnezia::protocols::xray::securityReality)) {
if (xrayPublicKey.isEmpty() || xrayShortId.isEmpty()) {
errorCode = readRealityKeyFiles(container, credentials, xrayPublicKey, xrayShortId);
if (errorCode != ErrorCode::NoError) {
@@ -508,243 +610,40 @@ XrayProtocolConfig XrayConfigurator::buildClientProtocolConfig(const ServerCrede
}
}
QJsonObject userObj;
userObj[amnezia::protocols::xray::id] = clientId;
userObj[amnezia::protocols::xray::encryption] = QStringLiteral("none");
const QString flowValue = effectiveClientFlow(srv);
if (!flowValue.isEmpty()) {
userObj[amnezia::protocols::xray::flow] = flowValue;
if (securityEff == QLatin1String(amnezia::protocols::xray::securityTls) && !srv.isThirdPartyConfig) {
if (tlsPin.isEmpty()) {
errorCode = ensureTlsCertificate(credentials, container, tlsPin);
if (errorCode != ErrorCode::NoError) {
return {};
}
}
if (tlsPin.isEmpty()) {
errorCode = ErrorCode::XrayTlsNotSupported;
return {};
}
}
QJsonObject vnextEntry;
vnextEntry[amnezia::protocols::xray::address] = credentials.hostName;
vnextEntry[amnezia::protocols::xray::port] =
srv.port.isEmpty() ? QString(amnezia::protocols::xray::defaultPort).toInt() : srv.port.toInt();
vnextEntry[amnezia::protocols::xray::users] = QJsonArray { userObj };
QJsonObject outboundSettings;
outboundSettings[amnezia::protocols::xray::vnext] = QJsonArray { vnextEntry };
QJsonObject outbound;
outbound[QStringLiteral("protocol")] = QStringLiteral("vless");
outbound[amnezia::protocols::xray::settings] = outboundSettings;
QJsonObject streamObj = buildStreamSettings(srv, clientId);
if (securityEff == QLatin1String("reality")) {
QJsonObject rs = streamObj[amnezia::protocols::xray::realitySettings].toObject();
rs[amnezia::protocols::xray::publicKey] = xrayPublicKey;
rs[amnezia::protocols::xray::shortId] = xrayShortId;
rs[amnezia::protocols::xray::spiderX] = QString();
streamObj[amnezia::protocols::xray::realitySettings] = rs;
}
outbound[amnezia::protocols::xray::streamSettings] = streamObj;
QJsonObject inboundObj;
inboundObj[QStringLiteral("listen")] = amnezia::protocols::xray::defaultLocalListenAddr;
inboundObj[amnezia::protocols::xray::port] = amnezia::protocols::xray::defaultLocalProxyPort;
inboundObj[QStringLiteral("protocol")] = QStringLiteral("socks");
inboundObj[amnezia::protocols::xray::settings] = QJsonObject { { QStringLiteral("udp"), true } };
QJsonObject clientJson;
clientJson[QStringLiteral("log")] = QJsonObject { { QStringLiteral("loglevel"), QStringLiteral("error") } };
clientJson[amnezia::protocols::xray::inbounds] = QJsonArray { inboundObj };
clientJson[amnezia::protocols::xray::outbounds] = QJsonArray { outbound };
const QString config = QString::fromUtf8(QJsonDocument(clientJson).toJson(QJsonDocument::Compact));
XrayProtocolConfig protocolConfig;
protocolConfig.serverConfig = srv;
XrayClientOutboundInputs inputs;
inputs.serverAddress = credentials.hostName;
inputs.clientId = clientId;
inputs.realityPublicKey = xrayPublicKey;
inputs.realityShortId = xrayShortId;
inputs.tlsPinnedPeerCertSha256 = tlsPin;
XrayClientConfig clientConfig;
clientConfig.nativeConfig = config;
clientConfig.localPort = QString(amnezia::protocols::xray::defaultLocalProxyPort);
const QJsonObject clientOutbound = protocolConfig.toClientOutboundJson(inputs);
clientConfig.nativeConfig = QString::fromUtf8(QJsonDocument(clientOutbound).toJson(QJsonDocument::Compact));
clientConfig.localPort = QString::fromLatin1(amnezia::protocols::xray::defaultLocalProxyPort);
clientConfig.id = clientId;
protocolConfig.setClientConfig(clientConfig);
return protocolConfig;
}
QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, const QString &clientId) const
{
QJsonObject streamSettings;
const auto &xhttp = srv.xhttp;
const auto &mkcp = srv.mkcp;
namespace px = amnezia::protocols::xray;
QString networkValue = QStringLiteral("tcp");
if (srv.transport == QLatin1String("xhttp"))
networkValue = QStringLiteral("xhttp");
else if (srv.transport == QLatin1String("mkcp"))
networkValue = QStringLiteral("kcp");
streamSettings[px::network] = networkValue;
const QString securityEff = effectiveSecurity(srv);
streamSettings[px::security] = securityEff;
if (securityEff == QLatin1String("tls")) {
QJsonObject tlsSettings;
const QString sniEff = srv.sni.isEmpty() ? QString::fromLatin1(px::defaultSni) : srv.sni;
tlsSettings[px::serverName] = sniEff;
const QString alpnEff = srv.alpn.isEmpty() ? QString::fromLatin1(px::defaultAlpn) : srv.alpn;
QJsonArray alpnArray;
for (const QString &a : alpnEff.split(QLatin1Char(','))) {
QString t = a.trimmed();
if (t.isEmpty())
continue;
if (t.compare(QLatin1String("HTTP/2"), Qt::CaseInsensitive) == 0)
t = QStringLiteral("h2");
else if (t.compare(QLatin1String("HTTP/1.1"), Qt::CaseInsensitive) == 0)
t = QStringLiteral("http/1.1");
alpnArray.append(t);
}
if (!alpnArray.isEmpty())
tlsSettings[QStringLiteral("alpn")] = alpnArray;
const QString fpEff = srv.fingerprint.isEmpty() ? QString::fromLatin1(px::defaultFingerprint) : srv.fingerprint;
tlsSettings[px::fingerprint] = fpEff;
streamSettings[QStringLiteral("tlsSettings")] = tlsSettings;
}
if (securityEff == QLatin1String("reality")) {
QJsonObject realSettings;
const QString fpEff = srv.fingerprint.isEmpty() ? QString::fromLatin1(px::defaultFingerprint) : srv.fingerprint;
realSettings[px::fingerprint] = fpEff;
const QString sniEff = srv.sni.isEmpty() ? QString::fromLatin1(px::defaultSni) : srv.sni;
realSettings[px::serverName] = sniEff;
streamSettings[px::realitySettings] = realSettings;
}
// XHTTP — JSON must match Xray-core SplitHTTPConfig (flat xPadding fields, see transport_internet.go)
if (srv.transport == QLatin1String("xhttp")) {
QJsonObject xo;
const QString hostEff = xhttp.host.isEmpty() ? QString::fromLatin1(px::defaultXhttpHost) : xhttp.host;
xo[QStringLiteral("host")] = hostEff;
if (!xhttp.path.isEmpty())
xo[QStringLiteral("path")] = xhttp.path;
QString modeEff = normalizeXhttpMode(xhttp.mode);
if (modeEff == QLatin1String("auto") || modeEff == QLatin1String("packet-up")) {
modeEff = QStringLiteral("stream-one");
}
xo[QStringLiteral("mode")] = modeEff;
// No "Host" in headers: xray rejects it when the top-level "host" field is set.
const QString methodEff =
xhttp.uplinkMethod.isEmpty() ? QString::fromLatin1(px::defaultXhttpUplinkMethod) : xhttp.uplinkMethod;
xo[QStringLiteral("uplinkHTTPMethod")] = methodEff.toUpper();
xo[QStringLiteral("noGRPCHeader")] = xhttp.disableGrpc;
xo[QStringLiteral("noSSEHeader")] = xhttp.disableSse;
const QString sessPl = normalizeSessionSeqPlacement(xhttp.sessionPlacement);
if (!sessPl.isEmpty())
xo[QStringLiteral("sessionIDPlacement")] = sessPl;
const QString seqPl = normalizeSessionSeqPlacement(xhttp.seqPlacement);
if (!seqPl.isEmpty())
xo[QStringLiteral("seqPlacement")] = seqPl;
if (!xhttp.sessionKey.isEmpty())
xo[QStringLiteral("sessionIDKey")] = xhttp.sessionKey;
if (!xhttp.seqKey.isEmpty())
xo[QStringLiteral("seqKey")] = xhttp.seqKey;
const QString uDataPl = normalizeUplinkDataPlacement(xhttp.uplinkDataPlacement);
const bool uDataNeedsPacketUp =
uDataPl == QLatin1String("header") || uDataPl == QLatin1String("cookie");
if (!(uDataNeedsPacketUp && modeEff != QLatin1String("packet-up")))
xo[QStringLiteral("uplinkDataPlacement")] = uDataPl;
if (!xhttp.uplinkDataKey.isEmpty())
xo[QStringLiteral("uplinkDataKey")] = xhttp.uplinkDataKey;
const QString ucs = xhttp.uplinkChunkSize.isEmpty() ? QString::fromLatin1(px::defaultXhttpUplinkChunkSize)
: xhttp.uplinkChunkSize;
if (!ucs.isEmpty() && ucs != QLatin1String("0")) {
xo[QStringLiteral("uplinkChunkSize")] = ucs.toInt();
}
if (!xhttp.scMaxBufferedPosts.isEmpty())
xo[QStringLiteral("scMaxBufferedPosts")] = xhttp.scMaxBufferedPosts.toLongLong();
putIntRangeIfAny(xo, "scMaxEachPostBytes", xhttp.scMaxEachPostBytesMin, xhttp.scMaxEachPostBytesMax,
px::defaultXhttpScMaxEachPostBytesMin, px::defaultXhttpScMaxEachPostBytesMax);
putIntRangeIfAny(xo, "scMinPostsIntervalMs", xhttp.scMinPostsIntervalMsMin, xhttp.scMinPostsIntervalMsMax,
px::defaultXhttpScMinPostsIntervalMsMin, px::defaultXhttpScMinPostsIntervalMsMax);
putIntRangeIfAny(xo, "scStreamUpServerSecs", xhttp.scStreamUpServerSecsMin, xhttp.scStreamUpServerSecsMax,
px::defaultXhttpScStreamUpServerSecsMin, px::defaultXhttpScStreamUpServerSecsMax);
const auto &pad = xhttp.xPadding;
xo[QStringLiteral("xPaddingObfsMode")] = pad.obfsMode;
if (pad.obfsMode) {
if (!pad.bytesMin.isEmpty() || !pad.bytesMax.isEmpty()) {
const int fromV = pad.bytesMin.isEmpty()
? QString::fromLatin1(px::defaultXPaddingBytesMin).toInt()
: pad.bytesMin.toInt();
int toV = pad.bytesMax.isEmpty()
? QString::fromLatin1(px::defaultXPaddingBytesMax).toInt()
: pad.bytesMax.toInt();
if (toV < fromV)
toV = fromV;
xo[QStringLiteral("xPaddingBytes")] = makeRangeString(QString::number(fromV), QString::number(toV));
}
xo[QStringLiteral("xPaddingKey")] =
pad.key.isEmpty() ? QString::fromLatin1(px::defaultXPaddingKey) : pad.key;
xo[QStringLiteral("xPaddingHeader")] =
pad.header.isEmpty() ? QString::fromLatin1(px::defaultXPaddingHeader) : pad.header;
xo[QStringLiteral("xPaddingPlacement")] = normalizeXPaddingPlacement(
pad.placement.isEmpty() ? QString::fromLatin1(px::defaultXPaddingPlacement) : pad.placement);
xo[QStringLiteral("xPaddingMethod")] = normalizeXPaddingMethod(
pad.method.isEmpty() ? QString::fromLatin1(px::defaultXPaddingMethod) : pad.method);
}
// xmux: Xray has no "enabled" flag; omit object when UI disables multiplex tuning.
if (xhttp.xmux.enabled) {
QJsonObject mux;
auto addMuxRange = [&](const char *key, const QString &a, const QString &b) {
// omit empty / 0-0 ranges (xray may reject "0-0")
const bool aZero = a.isEmpty() || a == QLatin1String("0");
const bool bZero = b.isEmpty() || b == QLatin1String("0");
if (aZero && bZero)
return;
const QString aV = a.isEmpty() ? QStringLiteral("0") : a;
const QString bV = b.isEmpty() ? QStringLiteral("0") : b;
mux[QString::fromUtf8(key)] = makeRangeString(aV, bV);
};
addMuxRange("maxConcurrency", xhttp.xmux.maxConcurrencyMin, xhttp.xmux.maxConcurrencyMax);
addMuxRange("maxConnections", xhttp.xmux.maxConnectionsMin, xhttp.xmux.maxConnectionsMax);
addMuxRange("cMaxReuseTimes", xhttp.xmux.cMaxReuseTimesMin, xhttp.xmux.cMaxReuseTimesMax);
addMuxRange("hMaxRequestTimes", xhttp.xmux.hMaxRequestTimesMin, xhttp.xmux.hMaxRequestTimesMax);
addMuxRange("hMaxReusableSecs", xhttp.xmux.hMaxReusableSecsMin, xhttp.xmux.hMaxReusableSecsMax);
if (!xhttp.xmux.hKeepAlivePeriod.isEmpty())
mux[QStringLiteral("hKeepAlivePeriod")] = xhttp.xmux.hKeepAlivePeriod.toLongLong();
if (!mux.isEmpty())
xo[QStringLiteral("xmux")] = mux;
}
streamSettings[QStringLiteral("xhttpSettings")] = xo;
}
if (srv.transport == QLatin1String("mkcp")) {
QJsonObject kcpObj;
const QString ttiEff = mkcp.tti.isEmpty() ? QString::fromLatin1(px::defaultMkcpTti) : mkcp.tti;
const QString upEff = mkcp.uplinkCapacity.isEmpty() ? QString::fromLatin1(px::defaultMkcpUplinkCapacity)
: mkcp.uplinkCapacity;
const QString downEff = mkcp.downlinkCapacity.isEmpty() ? QString::fromLatin1(px::defaultMkcpDownlinkCapacity)
: mkcp.downlinkCapacity;
const QString rbufEff = mkcp.readBufferSize.isEmpty() ? QString::fromLatin1(px::defaultMkcpReadBufferSize)
: mkcp.readBufferSize;
const QString wbufEff = mkcp.writeBufferSize.isEmpty() ? QString::fromLatin1(px::defaultMkcpWriteBufferSize)
: mkcp.writeBufferSize;
kcpObj[QStringLiteral("tti")] = ttiEff.toInt();
kcpObj[QStringLiteral("uplinkCapacity")] = upEff.toInt();
kcpObj[QStringLiteral("downlinkCapacity")] = downEff.toInt();
kcpObj[QStringLiteral("readBufferSize")] = rbufEff.toInt();
kcpObj[QStringLiteral("writeBufferSize")] = wbufEff.toInt();
kcpObj[QStringLiteral("congestion")] = mkcp.congestion;
streamSettings[QStringLiteral("kcpSettings")] = kcpObj;
}
return streamSettings;
}
ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container,
const ContainerConfig &containerConfig,
const DnsSettings &dnsSettings,
+22 -7
View File
@@ -2,6 +2,7 @@
#define XRAY_CONFIGURATOR_H
#include <QObject>
#include <QJsonArray>
#include <QJsonObject>
#include "configuratorBase.h"
@@ -33,9 +34,24 @@ public:
amnezia::ErrorCode writeServerConfigForSetup(const amnezia::ServerCredentials &credentials,
amnezia::DockerContainer container,
amnezia::ContainerConfig &containerConfig,
const amnezia::DnsSettings &dnsSettings);
const amnezia::DnsSettings &dnsSettings,
bool useAtomicApply = false);
private:
QJsonArray collectServerClients(const amnezia::ServerCredentials &credentials,
amnezia::DockerContainer container, const QString &flowValue,
const QString &fallbackClientId, amnezia::ErrorCode &outError) const;
amnezia::ErrorCode uploadServerConfigAtomically(const amnezia::ServerCredentials &credentials,
amnezia::DockerContainer container, const QString &listenPort,
const QJsonObject &serverConfig) const;
bool restartXrayContainer(const amnezia::ServerCredentials &credentials,
amnezia::DockerContainer container) const;
bool xrayProcessIsUp(const amnezia::ServerCredentials &credentials,
amnezia::DockerContainer container) const;
amnezia::ErrorCode readContainerKeyFile(amnezia::DockerContainer container,
const amnezia::ServerCredentials &credentials,
const QString &path, QString &out) const;
@@ -44,24 +60,23 @@ private:
const amnezia::DnsSettings &dnsSettings,
amnezia::ErrorCode &errorCode);
amnezia::ErrorCode uploadServerConfigJson(const amnezia::ServerCredentials &credentials, amnezia::DockerContainer container,
const amnezia::DnsSettings &dnsSettings, const QJsonObject &serverConfig) const;
amnezia::XrayProtocolConfig buildClientProtocolConfig(const amnezia::ServerCredentials &credentials,
amnezia::DockerContainer container,
const amnezia::XrayServerConfig &srv,
const QString &clientId,
amnezia::ErrorCode &errorCode,
const QString &prefetchedRealityPublicKey = {},
const QString &prefetchedRealityShortId = {}) const;
const QString &prefetchedRealityShortId = {},
const QString &prefetchedTlsPin = {}) const;
amnezia::ErrorCode readRealityKeyFiles(amnezia::DockerContainer container,
const amnezia::ServerCredentials &credentials,
QString &outPublicKey,
QString &outShortId) const;
QJsonObject buildStreamSettings(const amnezia::XrayServerConfig &srv,
const QString &clientId) const;
amnezia::ErrorCode ensureTlsCertificate(const amnezia::ServerCredentials &credentials,
amnezia::DockerContainer container,
QString &outFingerprint) const;
};
#endif // XRAY_CONFIGURATOR_H
@@ -76,12 +76,13 @@ QFuture<QPair<ErrorCode, QJsonArray>> NewsController::fetchNews()
return QtFuture::makeReadyFuture(qMakePair(ErrorCode::NoError, QJsonArray()));
}
auto gatewayController = QSharedPointer<GatewayController>::create(
m_appSettingsRepository->getGatewayEndpoint(),
m_appSettingsRepository->isDevGatewayEnv(),
apiDefs::requestTimeoutMsecs,
m_appSettingsRepository->isStrictKillSwitchEnabled(),
m_appSettingsRepository);
QSharedPointer<GatewayController> gatewayController(
new GatewayController(m_appSettingsRepository->getGatewayEndpoint(),
m_appSettingsRepository->isDevGatewayEnv(),
apiDefs::requestTimeoutMsecs,
m_appSettingsRepository->isStrictKillSwitchEnabled(),
m_appSettingsRepository),
&QObject::deleteLater);
// both country codes and service types are arrays here, one entry per gateway stack the user has
const QJsonObject payload = GatewayPayloadBuilder(m_appSettingsRepository)
@@ -188,6 +188,9 @@ ErrorCode StorePurchaseController::importServiceFromMarket(const QString &userCo
quint16 crc = qChecksum(QJsonDocument(configObject).toJson());
if (!serverConfigUtils::isConfigFormatVersionSupported(configObject)) {
return ErrorCode::ConfigFormatVersionNotSupportedError;
}
if (configObject.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
return ErrorCode::InternalError;
}
@@ -239,6 +239,9 @@ ErrorCode SubscriptionController::applyImportedServiceConfig(const QString &user
updateApiConfigInJson(serverConfigJson, serviceType, serviceProtocol, userCountryCode, responseBody);
if (!serverConfigUtils::isConfigFormatVersionSupported(serverConfigJson)) {
return ErrorCode::ConfigFormatVersionNotSupportedError;
}
if (serverConfigJson.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
return ErrorCode::InternalError;
}
@@ -291,6 +294,9 @@ ErrorCode SubscriptionController::importTrialFromGateway(const QString &userCoun
}
QJsonObject configObject = QJsonDocument::fromJson(configBytes).object();
if (!serverConfigUtils::isConfigFormatVersionSupported(configObject)) {
return ErrorCode::ConfigFormatVersionNotSupportedError;
}
if (configObject.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
return ErrorCode::InternalError;
}
@@ -356,6 +362,33 @@ ErrorCode SubscriptionController::updateServiceFromGateway(const QString &server
return applyUpdatedServiceConfig(serverId, serviceProtocol, protocolData, responseBody);
}
ErrorCode SubscriptionController::fetchServiceConfig(const QString &serverId, const QString &serviceProtocol,
QJsonObject &serverConfigJson)
{
auto apiV2 = m_serversRepository->apiV2Config(serverId);
if (!apiV2.has_value()) {
return ErrorCode::InternalError;
}
ProtocolData protocolData = generateProtocolData(serviceProtocol);
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
.addField(apiDefs::key::userCountryCode, apiV2->apiConfig.userCountryCode)
.addField(apiDefs::key::serviceType, apiV2->serviceType())
.addField(apiDefs::key::serviceProtocol, serviceProtocol)
.addField(apiDefs::key::publicKey, publicKeyForProtocol(serviceProtocol, protocolData))
.addField(apiDefs::key::authData, apiV2->authData.toJson())
.build();
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody, apiV2->apiConfig.isTestPurchase);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
return extractServerConfigJsonFromResponse(responseBody, serviceProtocol, protocolData, serverConfigJson);
}
void SubscriptionController::markSubscriptionExpiredByServer(const QString &serverId, const ApiV2ServerConfig &apiV2)
{
ApiV2ServerConfig expiredApiV2 = apiV2;
@@ -380,6 +413,9 @@ ErrorCode SubscriptionController::applyUpdatedServiceConfig(const QString &serve
updateApiConfigInJson(serverConfigJson, apiV2->apiConfig.serviceType, serviceProtocol, apiV2->apiConfig.userCountryCode, responseBody);
if (!serverConfigUtils::isConfigFormatVersionSupported(serverConfigJson)) {
return ErrorCode::ConfigFormatVersionNotSupportedError;
}
if (serverConfigJson.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
return ErrorCode::InternalError;
}
@@ -45,6 +45,8 @@ public:
ErrorCode importTrialFromGateway(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol, const QString &email);
ErrorCode fetchServiceConfig(const QString &serverId, const QString &serviceProtocol, QJsonObject &serverConfigJson);
ErrorCode updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, bool isConnectEvent,
CaptchaInfo *captchaInfoOut = nullptr, ProtocolData *usedProtocolDataOut = nullptr);
@@ -82,6 +84,21 @@ public:
const QString &captchaId, const QString &captchaSolution,
CaptchaInfo *retryCaptchaOut = nullptr);
#ifdef AMNEZIA_BUILD_TESTS
// Exposes the gateway response parsing path without performing a network request.
ErrorCode applyImportedServiceConfigForTest(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol, const ProtocolData &protocolData,
const QByteArray &responseBody)
{
return applyImportedServiceConfig(userCountryCode, serviceType, serviceProtocol, protocolData, responseBody);
}
ErrorCode applyUpdatedServiceConfigForTest(const QString &serverId, const QString &serviceProtocol,
const ProtocolData &protocolData, const QByteArray &responseBody)
{
return applyUpdatedServiceConfig(serverId, serviceProtocol, protocolData, responseBody);
}
#endif
private:
ErrorCode executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody, bool isTestPurchase = false);
bool isApiKeyExpired(const QString &serverId) const;
+23 -2
View File
@@ -1,5 +1,6 @@
#include "coreController.h"
#include <QCoreApplication>
#include <QDirIterator>
#include <QTranslator>
#include <QTimer>
@@ -19,6 +20,9 @@
#if defined(Q_OS_IOS)
#include "platforms/ios/ios_controller.h"
#endif
#if defined(Q_OS_IOS) || defined(MACOS_NE)
#include "core/utils/swiftBridge.h"
#endif
@@ -125,6 +129,12 @@ void CoreController::initModels()
m_apiCountryModel = new ApiCountryModel(this);
setQmlContextProperty("ApiCountryModel", m_apiCountryModel);
m_apiCountryListModel = new ApiCountryListModel(m_apiCountryModel, "picker", this);
setQmlContextProperty("ApiCountryListModel", m_apiCountryListModel);
m_apiConfigsCountryListModel = new ApiCountryListModel(m_apiCountryModel, "nativeConfigs", this);
setQmlContextProperty("ApiConfigsCountryListModel", m_apiConfigsCountryListModel);
m_apiSubscriptionPlansModel = new ApiSubscriptionPlansModel(this);
setQmlContextProperty("ApiSubscriptionPlansModel", m_apiSubscriptionPlansModel);
@@ -169,6 +179,7 @@ void CoreController::initCoreControllers()
m_importCoreController = new ImportController(m_serversRepository, m_appSettingsRepository, this);
m_connectionController = new ConnectionController(m_serversRepository, m_appSettingsRepository, m_vpnConnection.get(), this);
m_settingsController = new SettingsController(m_serversRepository, m_appSettingsRepository, this);
m_localProxyController = new LocalProxyController(m_serversRepository, m_appSettingsRepository, m_subscriptionController, this);
}
void CoreController::initControllers()
@@ -178,7 +189,6 @@ void CoreController::initControllers()
if (m_engine) {
m_focusController = new FocusController(m_engine, this);
setQmlContextProperty("FocusController", m_focusController);
}
m_installUiController = new InstallUiController(m_installController, m_serversController, m_settingsController, m_protocolsModel, m_usersController,
@@ -203,6 +213,12 @@ void CoreController::initControllers()
m_settingsUiController = new SettingsUiController(m_settingsController, m_serversController, this);
setQmlContextProperty("SettingsController", m_settingsUiController);
m_localProxyUiController = new LocalProxyUiController(m_localProxyController, this);
setQmlContextProperty("LocalProxyController", m_localProxyUiController);
m_logsUiController = new LogsUiController(m_settingsController, this);
setQmlContextProperty("LogsController", m_logsUiController);
m_pageController = new PageController(m_serversController, m_settingsController, this);
setQmlContextProperty("PageController", m_pageController);
@@ -271,7 +287,7 @@ void CoreController::initAppleController()
void CoreController::initLogging()
{
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
#ifndef Q_OS_ANDROID
bool enabled = m_appSettingsRepository->isSaveLogs();
if (enabled) {
if (!Logger::init(false)) {
@@ -280,6 +296,11 @@ void CoreController::initLogging()
}
Logger::setServiceLogsEnabled(enabled);
#endif
#if defined(Q_OS_IOS) || defined(MACOS_NE)
if (m_appSettingsRepository->isSaveLogs()) {
SWIFT_BRIDGE_NAMESPACE::toggleLogging(true);
}
#endif
}
void CoreController::initSignalHandlers()
+12
View File
@@ -3,7 +3,9 @@
#include <QObject>
#include <QQmlContext>
#include <QSharedPointer>
#include <QThread>
#include <QTranslator>
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
#include "ui/utils/systemTrayNotificationHandler.h"
@@ -22,6 +24,8 @@
#include "ui/controllers/selfhosted/installUiController.h"
#include "ui/controllers/qml/pageController.h"
#include "ui/controllers/settingsUiController.h"
#include "ui/controllers/localProxyUiController.h"
#include "ui/controllers/logsUiController.h"
#include "ui/controllers/serversUiController.h"
#include "ui/controllers/ipSplitTunnelingUiController.h"
#include "ui/controllers/systemController.h"
@@ -41,6 +45,7 @@
#include "core/controllers/api/newsController.h"
#include "core/controllers/selfhosted/installController.h"
#include "core/controllers/settingsController.h"
#include "core/controllers/localProxyController.h"
#include "core/controllers/connectionController.h"
#include "core/controllers/updateController.h"
@@ -57,6 +62,7 @@
#include "ui/models/api/apiAccountInfoModel.h"
#include "ui/models/api/apiBenefitsModel.h"
#include "ui/models/api/apiCountryModel.h"
#include "ui/models/api/apiCountryListModel.h"
#include "ui/models/api/apiDevicesModel.h"
#include "ui/models/api/apiServicesModel.h"
#include "ui/models/api/apiSubscriptionPlansModel.h"
@@ -136,6 +142,7 @@ protected:
ServersUiController* serversUiControllerProtected() const { return m_serversUiController; }
ServicesCatalogUiController* servicesCatalogUiControllerProtected() const { return m_servicesCatalogUiController; }
ApiNewsUiController* apiNewsUiControllerProtected() const { return m_apiNewsUiController; }
SubscriptionController* subscriptionControllerProtected() const { return m_subscriptionController; }
private:
void initRepositories();
@@ -170,6 +177,8 @@ private:
ImportController* m_importCoreController;
ExportUiController* m_exportUiController;
SettingsUiController* m_settingsUiController;
LocalProxyUiController* m_localProxyUiController;
LogsUiController* m_logsUiController;
ServersUiController* m_serversUiController;
IpSplitTunnelingUiController* m_ipSplitTunnelingUiController;
SystemController* m_systemController;
@@ -198,6 +207,7 @@ private:
ExportController* m_exportController;
ConnectionController* m_connectionController;
SettingsController* m_settingsController;
LocalProxyController* m_localProxyController;
ContainersModel* m_containersModel;
ContainersModel* m_defaultServerContainersModel;
@@ -214,6 +224,8 @@ private:
ApiSubscriptionPlansModel* m_apiSubscriptionPlansModel;
ApiBenefitsModel* m_apiBenefitsModel;
ApiCountryModel* m_apiCountryModel;
ApiCountryListModel* m_apiCountryListModel;
ApiCountryListModel* m_apiConfigsCountryListModel;
ApiAccountInfoModel* m_apiAccountInfoModel;
ApiDevicesModel* m_apiDevicesModel;
+63 -1
View File
@@ -1,5 +1,6 @@
#include "coreSignalHandlers.h"
#include <QCoreApplication>
#include <QTimer>
#include <QtConcurrent>
@@ -32,6 +33,7 @@
#include "core/controllers/selfhosted/installController.h"
#include "core/controllers/selfhosted/exportController.h"
#include "core/controllers/connectionController.h"
#include "core/controllers/localProxyController.h"
#include "ui/models/clientManagementModel.h"
#include "ui/controllers/api/apiNewsUiController.h"
#include "ui/models/containersModel.h"
@@ -65,6 +67,8 @@ void CoreSignalHandlers::initAllHandlers()
initExportControllerHandler();
initImportControllerHandler();
initApiCountryModelUpdateHandler();
initCountryListSortModeHandler();
initCountryListStateHandler();
initSubscriptionRefreshHandler();
initAdminConfigRevokedHandler();
initPassphraseRequestHandler();
@@ -86,6 +90,7 @@ void CoreSignalHandlers::initAllHandlers()
initIosSettingsHandler();
initNotificationHandler();
initUpdateFoundHandler();
initLocalProxyHandler();
}
void CoreSignalHandlers::initErrorMessagesHandler()
@@ -173,6 +178,41 @@ void CoreSignalHandlers::initImportControllerHandler()
});
}
void CoreSignalHandlers::initCountryListSortModeHandler()
{
SecureAppSettingsRepository *repository = m_coreController->m_appSettingsRepository;
const QVector<ApiCountryListModel *> models { m_coreController->m_apiCountryListModel,
m_coreController->m_apiConfigsCountryListModel };
for (ApiCountryListModel *model : models) {
model->setSortMode(repository->countryListSortMode(model->listId()));
connect(model, &ApiCountryListModel::sortModeChanged, this, [repository, model]() {
repository->setCountryListSortMode(model->listId(), model->sortMode());
});
}
}
void CoreSignalHandlers::initCountryListStateHandler()
{
SecureAppSettingsRepository *repository = m_coreController->m_appSettingsRepository;
const QVector<ApiCountryListModel *> models { m_coreController->m_apiCountryListModel,
m_coreController->m_apiConfigsCountryListModel };
for (ApiCountryListModel *model : models) {
model->setFavorites(repository->favoriteLocations());
connect(model, &ApiCountryListModel::favoritesChanged, this, [repository, models, model](const QStringList &codes) {
repository->setFavoriteLocations(codes);
for (ApiCountryListModel *other : models) {
if (other != model) {
other->setFavorites(codes);
}
}
});
}
}
void CoreSignalHandlers::initApiCountryModelUpdateHandler()
{
connect(m_coreController->m_serversUiController, &ServersUiController::updateApiCountryModel, this, [this]() {
@@ -187,7 +227,8 @@ void CoreSignalHandlers::initApiCountryModelUpdateHandler()
}
m_coreController->m_apiCountryModel->updateModel(apiV2->apiConfig.availableCountries,
apiV2->apiConfig.serverCountryCode);
apiV2->apiConfig.serverCountryCode,
apiV2->apiConfig.userCountryCode);
});
}
@@ -230,6 +271,18 @@ void CoreSignalHandlers::initTranslationsUpdatedHandler()
connect(m_coreController->m_languageUiController, &LanguageUiController::updateTranslations, m_coreController, &CoreController::updateTranslator);
connect(m_coreController, &CoreController::translationsUpdated, m_coreController->m_languageUiController, &LanguageUiController::translationsUpdated);
connect(m_coreController, &CoreController::translationsUpdated, m_coreController->m_connectionUiController, &ConnectionUiController::onTranslationsUpdated);
connect(m_coreController, &CoreController::translationsUpdated, m_coreController->m_logsUiController, &LogsUiController::refreshSizes);
const QVector<ApiCountryListModel *> countryListModels { m_coreController->m_apiCountryListModel,
m_coreController->m_apiConfigsCountryListModel };
auto applyUiLanguage = [this, countryListModels]() {
const QString languageCode = QLocale::languageToCode(m_coreController->m_settingsController->getAppLanguage().language());
for (ApiCountryListModel *model : countryListModels) {
model->setUiLanguage(languageCode);
}
};
applyUiLanguage();
connect(m_coreController, &CoreController::translationsUpdated, this, applyUiLanguage);
}
void CoreSignalHandlers::initLanguageHandler()
@@ -444,3 +497,12 @@ void CoreSignalHandlers::initUpdateFoundHandler()
});
}
void CoreSignalHandlers::initLocalProxyHandler()
{
connect(m_coreController->m_connectionController, &ConnectionController::openConnectionRequested, m_coreController->m_localProxyController,
&LocalProxyController::onVpnConnectionRequested);
connect(m_coreController->m_localProxyController, &LocalProxyController::stoppedByVpn, m_coreController->m_pageController,
&PageController::showNotificationMessage);
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, m_coreController->m_localProxyController,
&LocalProxyController::disable);
}
@@ -20,6 +20,8 @@ private:
void initExportControllerHandler();
void initImportControllerHandler();
void initApiCountryModelUpdateHandler();
void initCountryListSortModeHandler();
void initCountryListStateHandler();
void initSubscriptionRefreshHandler();
void initAdminConfigRevokedHandler();
void initPassphraseRequestHandler();
@@ -41,6 +43,7 @@ private:
void initIosSettingsHandler();
void initNotificationHandler();
void initUpdateFoundHandler();
void initLocalProxyHandler();
CoreController* m_coreController;
};
+202 -658
View File
@@ -1,107 +1,79 @@
#include "gatewayController.h"
#include <algorithm>
#include <functional>
#include <random>
#include <QCryptographicHash>
#include <QDebug>
#include <QEventLoop>
#include <QFutureWatcher>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
#include <QPromise>
#include <QThread>
#include <QUrl>
#include <QtConcurrent/QtConcurrentRun>
#include <openssl/rsa.h>
#include "amneziaApplication.h"
#include "core/repositories/secureAppSettingsRepository.h"
#include "core/utils/api/apiUtils.h"
#include "core/utils/constants/apiConstants.h"
#include "core/utils/constants/apiKeys.h"
#include "core/utils/networkUtilities.h"
#include "cryptoUtils.h"
#include "core/utils/api/apiUtils.h"
#ifdef AMNEZIA_DESKTOP
#include "core/utils/ipcClient.h"
#include "core/utils/networkUtilities.h"
#endif
#ifdef Q_OS_IOS
#include "platforms/ios/ios_controller.h"
#endif
namespace
{
constexpr QLatin1String errorResponsePattern1("No active configuration found for");
constexpr QLatin1String errorResponsePattern2("No non-revoked public key found for");
constexpr QLatin1String errorResponsePattern3("Account not found.");
constexpr QLatin1String errorResponsePatternQrSessionNotFound("QR session not found");
constexpr QLatin1String errorResponsePatternSessionNotFound("Session not found");
constexpr QLatin1String updateRequestResponsePattern("client version update is required");
constexpr int httpStatusCodeNotFound = 404;
constexpr int httpStatusCodeConflict = 409;
constexpr int httpStatusCodeNotImplemented = 501;
constexpr int httpStatusCodePaymentRequired = 402;
constexpr int httpStatusCodeRequestTimeout = 408;
constexpr int httpStatusCodeUnprocessableEntity = 422;
constexpr QLatin1String unprocessableSubscriptionMessage("Failed to retrieve subscription information. Is it activated?");
constexpr int proxyStorageRequestTimeoutMsecs = 3000;
QStringList shuffledProxyUrls(const QStringList &proxyUrls)
namespace agwConfigKey
{
QStringList shuffled = proxyUrls;
std::random_device randomDevice;
std::mt19937 generator(randomDevice());
std::shuffle(shuffled.begin(), shuffled.end(), generator);
return shuffled;
constexpr QLatin1String gatewayEndpoint("gateway_endpoint");
constexpr QLatin1String publicKeyPem("public_key_pem");
constexpr QLatin1String s3PrimaryEndpoints("s3_primary_endpoints");
constexpr QLatin1String s3FallbackEndpoints("s3_fallback_endpoints");
constexpr QLatin1String isDevEnvironment("is_dev_environment");
constexpr QLatin1String requestTimeoutMsecs("request_timeout_msecs");
constexpr QLatin1String proxyRequestTimeoutMsecs("proxy_request_timeout_msecs");
constexpr QLatin1String proxyStorageTimeoutMsecs("proxy_storage_timeout_msecs");
constexpr QLatin1String proxyHealthTimeoutMsecs("proxy_health_timeout_msecs");
}
QString getProxyUrlsCacheKey(const QString &serviceType, const QString &userCountryCode)
namespace agwOptionsKey
{
return QStringLiteral("service_%1_country_%2").arg(serviceType, userCountryCode);
constexpr QLatin1String serviceType("service_type");
constexpr QLatin1String userCountryCode("user_country_code");
}
bool decryptProxyUrlsPayload(const QByteArray &encryptedPayload, bool isDevEnvironment, QByteArray &decryptedPayload)
namespace agwStateKey
{
QByteArray key = isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
if (!isDevEnvironment) {
QCryptographicHash hash(QCryptographicHash::Sha512);
hash.addData(key);
QByteArray h = hash.result().toHex();
constexpr QLatin1String workingProxy("working_proxy");
}
QByteArray decKey = QByteArray::fromHex(h.left(64));
QByteArray iv = QByteArray::fromHex(h.mid(64, 32));
QByteArray ba = QByteArray::fromBase64(encryptedPayload);
constexpr QLatin1String agwStateCacheKey("agw_state");
constexpr QLatin1String agwTracePrefix("agw trace:");
constexpr QLatin1String agwTraceFailoverMarker("suspicious");
constexpr QLatin1String endpointListSeparator(", ");
constexpr QLatin1String legacyEndpointPlaceholder("%1");
decryptedPayload = CryptoUtils::decryptAes256Cbc(ba, decKey, iv);
if (decryptedPayload.isEmpty()) {
return false;
}
QStringList splitEndpoints(const char *raw)
{
return QString::fromUtf8(raw).split(endpointListSeparator, Qt::SkipEmptyParts);
}
void logAgwTrace(const QString &path, const int code, const QString &trace)
{
if (trace.isEmpty()) {
return;
}
const QString text = QStringLiteral("%1 %2\n%3").arg(agwTracePrefix, path, trace).replace(QLatin1Char('\n'), QStringLiteral("\n "));
if (code != AGW_OK) {
qWarning().noquote() << text;
} else if (trace.contains(agwTraceFailoverMarker)) {
qInfo().noquote() << text;
} else {
decryptedPayload = encryptedPayload;
qDebug().noquote() << text;
}
return true;
}
QStringList readCachedProxyUrls(const QByteArray &cachedProxyUrlsEncrypted, bool isDevEnvironment)
{
if (cachedProxyUrlsEncrypted.isEmpty()) {
return {};
}
QByteArray cachedProxyUrlsDecrypted;
if (!decryptProxyUrlsPayload(cachedProxyUrlsEncrypted, isDevEnvironment, cachedProxyUrlsDecrypted)) {
qCritical() << "error decrypting cached proxy urls payload";
return {};
}
QJsonArray endpointsArray = QJsonDocument::fromJson(cachedProxyUrlsDecrypted).array();
QStringList endpoints;
endpoints.reserve(endpointsArray.size());
for (const QJsonValue &endpoint : endpointsArray) {
endpoints.push_back(endpoint.toString());
}
return endpoints;
}
}
@@ -110,615 +82,187 @@ GatewayController::GatewayController(const QString &gatewayEndpoint, const bool
QObject *parent)
: QObject(parent),
m_gatewayEndpoint(gatewayEndpoint),
m_isDevEnvironment(isDevEnvironment),
m_requestTimeoutMsecs(requestTimeoutMsecs),
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled),
m_appSettingsRepository(appSettingsRepository)
{
const QByteArray publicKey = isDevEnvironment ? QByteArray(DEV_AGW_PUBLIC_KEY) : QByteArray(PROD_AGW_PUBLIC_KEY);
m_publicKeyMissing = publicKey.isEmpty();
QStringList primaryEndpoints;
QStringList fallbackEndpoints;
if (isDevEnvironment) {
primaryEndpoints = splitEndpoints(DEV_S3_ENDPOINT);
} else {
primaryEndpoints = splitEndpoints(PROD_S3_ENDPOINT);
fallbackEndpoints = splitEndpoints(FALLBACK_S3_ENDPOINT);
}
QJsonObject config;
config[agwConfigKey::gatewayEndpoint] = gatewayEndpoint;
config[agwConfigKey::publicKeyPem] = QString::fromUtf8(publicKey);
config[agwConfigKey::s3PrimaryEndpoints] = QJsonArray::fromStringList(primaryEndpoints);
config[agwConfigKey::s3FallbackEndpoints] = QJsonArray::fromStringList(fallbackEndpoints);
config[agwConfigKey::isDevEnvironment] = isDevEnvironment;
config[agwConfigKey::requestTimeoutMsecs] = requestTimeoutMsecs;
config[agwConfigKey::proxyRequestTimeoutMsecs] = apiDefs::proxyRequestTimeoutMsecs;
config[agwConfigKey::proxyStorageTimeoutMsecs] = apiDefs::proxyStorageTimeoutMsecs;
config[agwConfigKey::proxyHealthTimeoutMsecs] = apiDefs::proxyHealthTimeoutMsecs;
m_client = agw_client_create(QJsonDocument(config).toJson(QJsonDocument::Compact).constData(), nullptr);
if (m_client == 0) {
qCritical() << "GatewayController: failed to create gateway client (missing key or endpoint?)";
return;
}
if (m_appSettingsRepository != nullptr) {
const QByteArray state = m_appSettingsRepository->readGatewayProxyUrls(agwStateCacheKey);
if (!state.isEmpty() && agw_import_state(m_client, state.constData()) == AGW_OK) {
m_lastPersistedState = state;
}
}
}
GatewayController::EncryptedRequestData GatewayController::prepareRequest(const QString &endpoint, const QJsonObject &apiPayload)
GatewayController::~GatewayController()
{
EncryptedRequestData encRequestData;
encRequestData.errorCode = ErrorCode::NoError;
agw_client_destroy(m_client);
}
amnezia::ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody)
{
QFuture<QPair<amnezia::ErrorCode, QByteArray>> future = postAsync(endpoint, apiPayload);
QFutureWatcher<QPair<amnezia::ErrorCode, QByteArray>> watcher;
QEventLoop wait;
connect(&watcher, &QFutureWatcherBase::finished, &wait, &QEventLoop::quit);
watcher.setFuture(future);
if (!future.isFinished()) {
wait.exec(QEventLoop::ExcludeUserInputEvents);
}
const QPair<amnezia::ErrorCode, QByteArray> result = future.result();
responseBody = result.second;
return result.first;
}
QFuture<QPair<amnezia::ErrorCode, QByteArray>> GatewayController::postAsync(const QString &endpoint, const QJsonObject apiPayload)
{
prepareRequest();
return QtConcurrent::run(
[this, endpoint, apiPayload]() -> QPair<amnezia::ErrorCode, QByteArray> { return executePost(endpoint, apiPayload); });
}
QPair<amnezia::ErrorCode, QByteArray> GatewayController::executePost(const QString &endpoint, const QJsonObject &apiPayload)
{
if (m_client == 0) {
return qMakePair(m_publicKeyMissing ? amnezia::ErrorCode::ApiMissingAgwPublicKey : amnezia::ErrorCode::ApiConfigDownloadError,
QByteArray());
}
QString path = endpoint;
path.remove(legacyEndpointPlaceholder);
QJsonObject options;
options[agwOptionsKey::serviceType] = apiPayload.value(apiDefs::key::serviceType).toString();
options[agwOptionsKey::userCountryCode] = apiPayload.value(apiDefs::key::userCountryCode).toString();
const QByteArray payload = QJsonDocument(apiPayload).toJson(QJsonDocument::Compact);
const QByteArray optionsJson = QJsonDocument(options).toJson(QJsonDocument::Compact);
agw_result result = agw_post(m_client, path.toUtf8().constData(), payload.constData(), optionsJson.constData(), 0);
QByteArray responseBody;
if (result.body != nullptr) {
responseBody = QByteArray(result.body, static_cast<qsizetype>(result.body_len));
}
QString trace;
if (result.trace != nullptr) {
trace = QString::fromUtf8(result.trace, static_cast<qsizetype>(result.trace_len));
}
const int code = result.code;
agw_result_free(&result);
logAgwTrace(path, code, trace);
persistState();
return qMakePair(mapResultCode(code, responseBody), responseBody);
}
amnezia::ErrorCode GatewayController::mapResultCode(const int code, const QByteArray &responseBody)
{
switch (code) {
case AGW_OK: return apiUtils::checkApiResponseErrors(responseBody);
case AGW_CANCELLED:
case AGW_ERR_TIMEOUT: return amnezia::ErrorCode::ApiConfigTimeoutError;
case AGW_ERR_SSL: return amnezia::ErrorCode::ApiConfigSslError;
case AGW_ERR_CONFIG: return amnezia::ErrorCode::ApiMissingAgwPublicKey;
case AGW_ERR_DECRYPT: return amnezia::ErrorCode::ApiConfigDecryptionError;
default: return amnezia::ErrorCode::ApiConfigDownloadError;
}
}
void GatewayController::prepareRequest()
{
#ifdef Q_OS_IOS
IosController::Instance()->requestInetAccess();
QThread::msleep(10);
#endif
encRequestData.request.setTransferTimeout(m_requestTimeoutMsecs);
encRequestData.request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
encRequestData.request.setRawHeader(QString("X-Client-Request-ID").toUtf8(), QUuid::createUuid().toString(QUuid::WithoutBraces).toUtf8());
encRequestData.request.setUrl(endpoint.arg(m_proxyUrl.isEmpty() ? m_gatewayEndpoint : m_proxyUrl));
// bypass killSwitch exceptions for API-gateway
#ifdef AMNEZIA_DESKTOP
if (m_isStrictKillSwitchEnabled) {
QString host = QUrl(encRequestData.request.url()).host();
QString ip = NetworkUtilities::getIPAddress(host);
if (!m_isStrictKillSwitchEnabled) {
return;
}
QStringList ips;
for (const QString &host : requestHosts()) {
const QString ip = NetworkUtilities::getIPAddress(host);
if (!ip.isEmpty()) {
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
QRemoteObjectPendingReply<bool> reply = iface->addKillSwitchAllowedRange(QStringList { ip });
if (!reply.waitForFinished(1000) || !reply.returnValue())
qWarning() << "GatewayController::prepareRequest(): Failed to execute remote addKillSwitchAllowedRange call";
});
ips.append(ip);
}
}
if (ips.isEmpty()) {
return;
}
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
QRemoteObjectPendingReply<bool> reply = iface->addKillSwitchAllowedRange(ips);
if (!reply.waitForFinished(1000) || !reply.returnValue()) {
qWarning() << "GatewayController::prepareRequest(): Failed to execute remote addKillSwitchAllowedRange call";
}
});
#endif
encRequestData.key = CryptoUtils::generateRandomBytes(32);
encRequestData.iv = CryptoUtils::generateRandomBytes(32);
encRequestData.salt = CryptoUtils::generateRandomBytes(8);
QJsonObject keyPayload;
keyPayload[apiDefs::key::aesKey] = QString(encRequestData.key.toBase64());
keyPayload[apiDefs::key::aesIv] = QString(encRequestData.iv.toBase64());
keyPayload[apiDefs::key::aesSalt] = QString(encRequestData.salt.toBase64());
QByteArray rsaKey = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
EVP_PKEY *publicKey = CryptoUtils::loadPublicKeyFromPem(rsaKey);
if (publicKey == nullptr) {
qCritical() << "error loading public key from environment variables";
encRequestData.errorCode = ErrorCode::ApiMissingAgwPublicKey;
return encRequestData;
}
QByteArray encryptedKeyPayload = CryptoUtils::rsaEncrypt(QJsonDocument(keyPayload).toJson(), publicKey, RSA_PKCS1_PADDING);
EVP_PKEY_free(publicKey);
QByteArray encryptedApiPayload = CryptoUtils::encryptAes256Cbc(QJsonDocument(apiPayload).toJson(), encRequestData.key, encRequestData.iv);
if (encryptedKeyPayload.isEmpty() || encryptedApiPayload.isEmpty()) {
qCritical() << "error when encrypting the request body";
encRequestData.errorCode = ErrorCode::ApiConfigDecryptionError;
return encRequestData;
}
QJsonObject requestBody;
requestBody[apiDefs::key::keyPayload] = QString(encryptedKeyPayload.toBase64());
requestBody[apiDefs::key::apiPayload] = QString(encryptedApiPayload.toBase64());
encRequestData.requestBody = QJsonDocument(requestBody).toJson();
return encRequestData;
}
GatewayController::DecryptionResult GatewayController::tryDecryptResponseBody(const QByteArray &encryptedResponseBody,
QNetworkReply::NetworkError replyError, const QByteArray &key,
const QByteArray &iv, const QByteArray &salt)
QStringList GatewayController::requestHosts() const
{
DecryptionResult result;
result.decryptedBody = encryptedResponseBody;
result.isDecryptionSuccessful = false;
QByteArray decrypted = CryptoUtils::decryptAes256Cbc(encryptedResponseBody, key, iv);
if (!decrypted.isEmpty()) {
result.decryptedBody = decrypted;
result.isDecryptionSuccessful = true;
} else {
result.decryptedBody = encryptedResponseBody;
result.isDecryptionSuccessful = false;
QStringList hosts;
if (m_client != 0) {
char *state = agw_export_state(m_client);
if (state != nullptr) {
const QString workingProxy = QJsonDocument::fromJson(QByteArray(state)).object().value(agwStateKey::workingProxy).toString();
agw_string_free(state);
if (!workingProxy.isEmpty()) {
hosts.append(QUrl(workingProxy).host());
}
}
}
return result;
hosts.append(QUrl(m_gatewayEndpoint).host());
hosts.removeAll(QString());
hosts.removeDuplicates();
return hosts;
}
ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody)
void GatewayController::persistState()
{
EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload);
if (encRequestData.errorCode != ErrorCode::NoError) {
return encRequestData.errorCode;
}
QNetworkReply *reply = amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody);
QEventLoop wait;
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
QList<QSslError> sslErrors;
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec(QEventLoop::ExcludeUserInputEvents);
QByteArray encryptedResponseBody = reply->readAll();
QString replyErrorString = reply->errorString();
auto replyError = reply->error();
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
reply->deleteLater();
auto decryptionResult =
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
if (sslErrors.isEmpty() && shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
auto requestFunction = [&encRequestData, &encryptedResponseBody](const QString &url) {
encRequestData.request.setUrl(url);
return amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody);
};
auto replyProcessingFunction = [&encryptedResponseBody, &replyErrorString, &replyError, &httpStatusCode, &sslErrors, &encRequestData,
&decryptionResult, this](QNetworkReply *reply, const QList<QSslError> &nestedSslErrors) {
encryptedResponseBody = reply->readAll();
replyErrorString = reply->errorString();
replyError = reply->error();
httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
decryptionResult =
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
if (!sslErrors.isEmpty()
|| shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
sslErrors = nestedSslErrors;
return false;
}
return true;
};
auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString("");
auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString("");
bypassProxy(endpoint, serviceType, userCountryCode, requestFunction, replyProcessingFunction);
}
responseBody = decryptionResult.decryptedBody;
const auto errorCode =
apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, responseBody);
if (errorCode) {
return errorCode;
}
if (!decryptionResult.isDecryptionSuccessful) {
qCritical() << "error when decrypting the request body";
return ErrorCode::ApiConfigDecryptionError;
}
return ErrorCode::NoError;
}
QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString &endpoint, const QJsonObject apiPayload)
{
auto promise = QSharedPointer<QPromise<QPair<ErrorCode, QByteArray>>>::create();
promise->start();
EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload);
if (encRequestData.errorCode != ErrorCode::NoError) {
promise->addResult(qMakePair(encRequestData.errorCode, QByteArray()));
promise->finish();
return promise->future();
}
QNetworkReply *reply = amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody);
auto sslErrors = QSharedPointer<QList<QSslError>>::create();
connect(reply, &QNetworkReply::sslErrors, [sslErrors](const QList<QSslError> &errors) { *sslErrors = errors; });
connect(reply, &QNetworkReply::finished, this, [promise, sslErrors, encRequestData, endpoint, apiPayload, reply, this]() mutable {
QByteArray encryptedResponseBody = reply->readAll();
QString replyErrorString = reply->errorString();
auto replyError = reply->error();
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
reply->deleteLater();
auto decryptionResult =
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
auto processResponse = [promise, encRequestData](const GatewayController::DecryptionResult &decryptionResult,
const QList<QSslError> &sslErrors, QNetworkReply::NetworkError replyError,
const QString &replyErrorString, int httpStatusCode) {
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode,
decryptionResult.decryptedBody);
if (errorCode) {
promise->addResult(qMakePair(errorCode, decryptionResult.decryptedBody));
promise->finish();
return;
}
if (!decryptionResult.isDecryptionSuccessful) {
qCritical() << "error when decrypting the request body";
promise->addResult(qMakePair(ErrorCode::ApiConfigDecryptionError, QByteArray()));
promise->finish();
return;
}
promise->addResult(qMakePair(ErrorCode::NoError, decryptionResult.decryptedBody));
promise->finish();
};
if (sslErrors->isEmpty() && shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString("");
auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString("");
QStringList primaryBaseUrls;
QStringList fallbackBaseUrls;
if (m_isDevEnvironment) {
primaryBaseUrls = QString(DEV_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
} else {
primaryBaseUrls = QString(PROD_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
fallbackBaseUrls = QString(FALLBACK_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
}
std::random_device randomDevice;
std::mt19937 generator(randomDevice());
std::shuffle(primaryBaseUrls.begin(), primaryBaseUrls.end(), generator);
std::shuffle(fallbackBaseUrls.begin(), fallbackBaseUrls.end(), generator);
auto appendStorageUrls = [&serviceType, &userCountryCode](const QStringList &baseUrls, QStringList &target) {
if (!serviceType.isEmpty()) {
for (const auto &baseUrl : baseUrls) {
QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8();
target.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) + ".json");
}
}
for (const auto &baseUrl : baseUrls) {
target.push_back(baseUrl + "endpoints.json");
}
};
QStringList proxyStorageUrls;
appendStorageUrls(primaryBaseUrls, proxyStorageUrls);
appendStorageUrls(fallbackBaseUrls, proxyStorageUrls);
const QString proxyUrlsCacheKey = getProxyUrlsCacheKey(serviceType, userCountryCode);
getProxyUrlsAsync(proxyStorageUrls, 0, proxyUrlsCacheKey, [this, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) {
getProxyUrlAsync(proxyUrls, 0, [this, encRequestData, endpoint, processResponse](const QString &proxyUrl) {
bypassProxyAsync(endpoint, proxyUrl, encRequestData,
[processResponse, this](const QByteArray &decryptedBody, bool isDecryptionSuccessful,
const QList<QSslError> &sslErrors, QNetworkReply::NetworkError replyError,
const QString &replyErrorString, int httpStatusCode) {
GatewayController::DecryptionResult result;
result.decryptedBody = decryptedBody;
result.isDecryptionSuccessful = isDecryptionSuccessful;
processResponse(result, sslErrors, replyError, replyErrorString, httpStatusCode);
});
});
});
} else {
processResponse(decryptionResult, *sslErrors, replyError, replyErrorString, httpStatusCode);
}
});
return promise->future();
}
QStringList GatewayController::getProxyUrls(const QString &serviceType, const QString &userCountryCode)
{
QNetworkRequest request;
request.setTransferTimeout(proxyStorageRequestTimeoutMsecs);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QEventLoop wait;
QList<QSslError> sslErrors;
QNetworkReply *reply;
QStringList primaryBaseUrls;
QStringList fallbackBaseUrls;
if (m_isDevEnvironment) {
primaryBaseUrls = QString(DEV_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
} else {
primaryBaseUrls = QString(PROD_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
fallbackBaseUrls = QString(FALLBACK_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
}
std::random_device randomDevice;
std::mt19937 generator(randomDevice());
std::shuffle(primaryBaseUrls.begin(), primaryBaseUrls.end(), generator);
std::shuffle(fallbackBaseUrls.begin(), fallbackBaseUrls.end(), generator);
auto appendStorageUrls = [&serviceType, &userCountryCode](const QStringList &baseUrls, QStringList &target) {
if (!serviceType.isEmpty()) {
for (const auto &baseUrl : baseUrls) {
QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8();
target.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) + ".json");
}
}
for (const auto &baseUrl : baseUrls) {
target.push_back(baseUrl + "endpoints.json");
}
};
QStringList proxyStorageUrls;
appendStorageUrls(primaryBaseUrls, proxyStorageUrls);
appendStorageUrls(fallbackBaseUrls, proxyStorageUrls);
const QString proxyUrlsCacheKey = getProxyUrlsCacheKey(serviceType, userCountryCode);
const QByteArray cachedProxyUrlsEncrypted = m_appSettingsRepository->readGatewayProxyUrls(proxyUrlsCacheKey);
if (proxyStorageUrls.empty()) {
qDebug() << "empty storage endpoint list";
return readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment);
}
for (const auto &proxyStorageUrl : proxyStorageUrls) {
request.setUrl(proxyStorageUrl);
reply = amnApp->networkManager()->get(request);
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec(QEventLoop::ExcludeUserInputEvents);
if (reply->error() == QNetworkReply::NetworkError::NoError) {
auto encryptedResponseBody = reply->readAll();
reply->deleteLater();
QByteArray responseBody;
if (!decryptProxyUrlsPayload(encryptedResponseBody, m_isDevEnvironment, responseBody)) {
qCritical() << "error loading private key from environment variables or decrypting payload" << encryptedResponseBody;
continue;
}
auto endpointsArray = QJsonDocument::fromJson(responseBody).array();
QStringList endpoints;
for (const auto &endpoint : endpointsArray) {
endpoints.push_back(endpoint.toString());
}
m_appSettingsRepository->writeGatewayProxyUrls(proxyUrlsCacheKey, encryptedResponseBody);
return endpoints;
} else {
auto replyError = reply->error();
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
qDebug() << replyError;
qDebug() << httpStatusCode;
qDebug() << "go to the next storage endpoint";
reply->deleteLater();
}
}
return readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment);
}
bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody,
bool isDecryptionSuccessful)
{
const QByteArray &responseBody = decryptedResponseBody;
int apiHttpStatus = -1;
QString apiErrorMessage;
if (isDecryptionSuccessful) {
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
if (jsonDoc.isObject()) {
QJsonObject jsonObj = jsonDoc.object();
apiHttpStatus = jsonObj.value("http_status").toInt(-1);
apiErrorMessage = jsonObj.value(QStringLiteral("message")).toString().trimmed();
}
} else {
qDebug() << "failed to decrypt the data";
return true;
}
if (replyError == QNetworkReply::NetworkError::OperationCanceledError || replyError == QNetworkReply::NetworkError::TimeoutError) {
qDebug() << "timeout occurred";
qDebug() << replyError;
return true;
}
if (responseBody.contains("html")) {
qDebug() << "the response contains an html tag";
return true;
}
if (apiHttpStatus == httpStatusCodeRequestTimeout) {
return false;
}
if (apiHttpStatus == httpStatusCodeNotFound) {
if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2)
|| responseBody.contains(errorResponsePattern3) || responseBody.contains(errorResponsePatternQrSessionNotFound)
|| responseBody.contains(errorResponsePatternSessionNotFound)) {
return false;
} else {
qDebug() << replyError;
return true;
}
}
if (apiHttpStatus == httpStatusCodeNotImplemented) {
if (responseBody.contains(updateRequestResponsePattern)) {
return false;
} else {
qDebug() << replyError;
return true;
}
}
if (apiHttpStatus == httpStatusCodeConflict) {
return false;
}
if (apiHttpStatus == httpStatusCodePaymentRequired) {
return false;
}
if (apiHttpStatus == httpStatusCodeUnprocessableEntity) {
return apiErrorMessage != unprocessableSubscriptionMessage;
}
if (replyError != QNetworkReply::NetworkError::NoError) {
qDebug() << replyError;
return true;
}
return false;
}
void GatewayController::bypassProxy(const QString &endpoint, const QString &serviceType, const QString &userCountryCode,
std::function<QNetworkReply *(const QString &url)> requestFunction,
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction)
{
QStringList proxyUrls = getProxyUrls(serviceType, userCountryCode);
std::random_device randomDevice;
std::mt19937 generator(randomDevice());
std::shuffle(proxyUrls.begin(), proxyUrls.end(), generator);
QByteArray responseBody;
auto bypassFunction = [this](const QString &endpoint, const QString &proxyUrl,
std::function<QNetworkReply *(const QString &url)> requestFunction,
std::function<bool(QNetworkReply * reply, const QList<QSslError> &sslErrors)> replyProcessingFunction) {
QEventLoop wait;
QList<QSslError> sslErrors;
qDebug() << "go to the next proxy endpoint";
QNetworkReply *reply = requestFunction(endpoint.arg(proxyUrl));
QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec(QEventLoop::ExcludeUserInputEvents);
auto result = replyProcessingFunction(reply, sslErrors);
reply->deleteLater();
return result;
};
if (m_proxyUrl.isEmpty()) {
QNetworkRequest request;
request.setTransferTimeout(1000);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QEventLoop wait;
QList<QSslError> sslErrors;
QNetworkReply *reply;
for (const QString &proxyUrl : proxyUrls) {
request.setUrl(proxyUrl + "lmbd-health");
reply = amnApp->networkManager()->get(request);
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec(QEventLoop::ExcludeUserInputEvents);
if (reply->error() == QNetworkReply::NetworkError::NoError) {
reply->deleteLater();
m_proxyUrl = proxyUrl;
if (!m_proxyUrl.isEmpty()) {
break;
}
} else {
reply->deleteLater();
}
}
}
if (!m_proxyUrl.isEmpty()) {
if (bypassFunction(endpoint, m_proxyUrl, requestFunction, replyProcessingFunction)) {
return;
}
}
for (const QString &proxyUrl : proxyUrls) {
if (bypassFunction(endpoint, proxyUrl, requestFunction, replyProcessingFunction)) {
m_proxyUrl = proxyUrl;
break;
}
}
}
void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
const QString &proxyUrlsCacheKey, std::function<void(const QStringList &)> onComplete)
{
const QByteArray cachedProxyUrlsEncrypted = m_appSettingsRepository->readGatewayProxyUrls(proxyUrlsCacheKey);
if (currentProxyStorageIndex >= proxyStorageUrls.size()) {
onComplete(shuffledProxyUrls(readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment)));
if (m_client == 0 || m_appSettingsRepository == nullptr) {
return;
}
QNetworkRequest request;
request.setTransferTimeout(proxyStorageRequestTimeoutMsecs);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setUrl(proxyStorageUrls[currentProxyStorageIndex]);
QNetworkReply *reply = amnApp->networkManager()->get(request);
// connect(reply, &QNetworkReply::sslErrors, this, [state](const QList<QSslError> &e) { *(state->sslErrors) = e; });
connect(reply, &QNetworkReply::finished, this,
[this, proxyStorageUrls, currentProxyStorageIndex, proxyUrlsCacheKey, onComplete, reply]() {
if (reply->error() == QNetworkReply::NoError) {
QByteArray encrypted = reply->readAll();
reply->deleteLater();
QByteArray responseBody;
if (!decryptProxyUrlsPayload(encrypted, m_isDevEnvironment, responseBody)) {
qCritical() << "error decrypting payload";
QMetaObject::invokeMethod(
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, proxyUrlsCacheKey, onComplete); }, Qt::QueuedConnection);
return;
}
QJsonArray endpointsArray = QJsonDocument::fromJson(responseBody).array();
QStringList endpoints;
for (const QJsonValue &endpoint : endpointsArray)
endpoints.push_back(endpoint.toString());
m_appSettingsRepository->writeGatewayProxyUrls(proxyUrlsCacheKey, encrypted);
onComplete(shuffledProxyUrls(endpoints));
return;
}
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
qDebug() << httpStatusCode;
qDebug() << "go to the next storage endpoint";
reply->deleteLater();
QMetaObject::invokeMethod(
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, proxyUrlsCacheKey, onComplete); }, Qt::QueuedConnection);
});
}
void GatewayController::getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex,
std::function<void(const QString &)> onComplete)
{
if (currentProxyIndex >= proxyUrls.size()) {
onComplete("");
char *state = agw_export_state(m_client);
if (state == nullptr) {
return;
}
const QByteArray blob(state);
agw_string_free(state);
QNetworkRequest request;
request.setTransferTimeout(1000);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setUrl(proxyUrls[currentProxyIndex] + "lmbd-health");
QNetworkReply *reply = amnApp->networkManager()->get(request);
// connect(reply, &QNetworkReply::sslErrors, this, [state](const QList<QSslError> &e) {
// *(state->sslErrors) = e;
// });
connect(reply, &QNetworkReply::finished, this, [this, proxyUrls, currentProxyIndex, onComplete, reply]() {
reply->deleteLater();
if (reply->error() == QNetworkReply::NoError) {
m_proxyUrl = proxyUrls[currentProxyIndex];
onComplete(m_proxyUrl);
return;
}
qDebug() << "go to the next proxy endpoint";
QMetaObject::invokeMethod(this, [=]() { getProxyUrlAsync(proxyUrls, currentProxyIndex + 1, onComplete); }, Qt::QueuedConnection);
});
}
void GatewayController::bypassProxyAsync(
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
std::function<void(const QByteArray &, bool, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> onComplete)
{
auto sslErrors = QSharedPointer<QList<QSslError>>::create();
if (proxyUrl.isEmpty()) {
onComplete(QByteArray(), false, *sslErrors, QNetworkReply::InternalServerError, "empty proxy url", 0);
return;
if (blob != m_lastPersistedState) {
m_appSettingsRepository->writeGatewayProxyUrls(agwStateCacheKey, blob);
m_lastPersistedState = blob;
}
QNetworkRequest request = encRequestData.request;
request.setUrl(endpoint.arg(proxyUrl));
QNetworkReply *reply = amnApp->networkManager()->post(request, encRequestData.requestBody);
connect(reply, &QNetworkReply::sslErrors, this, [sslErrors](const QList<QSslError> &errors) { *sslErrors = errors; });
connect(reply, &QNetworkReply::finished, this, [sslErrors, onComplete, encRequestData, reply, this]() {
QByteArray encryptedResponseBody = reply->readAll();
QString replyErrorString = reply->errorString();
auto replyError = reply->error();
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
reply->deleteLater();
auto decryptionResult =
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
onComplete(decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful, *sslErrors, replyError, replyErrorString,
httpStatusCode);
});
}
+14 -42
View File
@@ -1,20 +1,17 @@
#ifndef GATEWAYCONTROLLER_H
#define GATEWAYCONTROLLER_H
#include <QByteArray>
#include <QFuture>
#include <QNetworkReply>
#include <QJsonObject>
#include <QObject>
#include <QPair>
#include <QPromise>
#include <QSharedPointer>
#include <QString>
#include <QStringList>
#include "core/utils/errorCodes.h"
#include "core/utils/routeModes.h"
#include "core/utils/commonStructs.h"
#ifdef Q_OS_IOS
#include "platforms/ios/ios_controller.h"
#endif
#include "agw.h"
class SecureAppSettingsRepository;
@@ -26,51 +23,26 @@ public:
explicit GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
const bool isStrictKillSwitchEnabled, SecureAppSettingsRepository *appSettingsRepository,
QObject *parent = nullptr);
~GatewayController() override;
amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody);
QFuture<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject apiPayload);
private:
struct EncryptedRequestData
{
QNetworkRequest request;
QByteArray requestBody;
QByteArray key;
QByteArray iv;
QByteArray salt;
amnezia::ErrorCode errorCode;
};
void prepareRequest();
QStringList requestHosts() const;
struct DecryptionResult
{
QByteArray decryptedBody;
bool isDecryptionSuccessful;
};
QPair<amnezia::ErrorCode, QByteArray> executePost(const QString &endpoint, const QJsonObject &apiPayload);
static amnezia::ErrorCode mapResultCode(const int code, const QByteArray &responseBody);
void persistState();
EncryptedRequestData prepareRequest(const QString &endpoint, const QJsonObject &apiPayload);
DecryptionResult tryDecryptResponseBody(const QByteArray &encryptedResponseBody, QNetworkReply::NetworkError replyError,
const QByteArray &key, const QByteArray &iv, const QByteArray &salt);
QStringList getProxyUrls(const QString &serviceType, const QString &userCountryCode);
bool shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody, bool isDecryptionSuccessful);
void bypassProxy(const QString &endpoint, const QString &serviceType, const QString &userCountryCode,
std::function<QNetworkReply *(const QString &url)> requestFunction,
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction);
void getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
const QString &proxyUrlsCacheKey, std::function<void(const QStringList &)> onComplete);
void getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex, std::function<void(const QString &)> onComplete);
void bypassProxyAsync(
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
std::function<void(const QByteArray &, bool, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> onComplete);
int m_requestTimeoutMsecs;
QString m_gatewayEndpoint;
bool m_isDevEnvironment = false;
bool m_isStrictKillSwitchEnabled = false;
SecureAppSettingsRepository *m_appSettingsRepository = nullptr;
inline static QString m_proxyUrl;
agw_client_handle m_client = 0;
bool m_publicKeyMissing = false;
QByteArray m_lastPersistedState;
};
#endif // GATEWAYCONTROLLER_H
@@ -0,0 +1,525 @@
#include "localProxyController.h"
#include "core/controllers/api/subscriptionController.h"
#include "core/repositories/secureAppSettingsRepository.h"
#include "core/repositories/secureServersRepository.h"
#include "core/utils/api/apiUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/localProxy/localProxyConstants.h"
#include "core/utils/localProxy/portUtils.h"
#include "core/utils/localProxy/xrayConfigUtils.h"
#include <QDebug>
#include <QJsonDocument>
#include <QJsonParseError>
#ifdef AMNEZIA_DESKTOP
#include "core/utils/ipcClient.h"
#include <QHostAddress>
#include <QHttpServer>
#include <QHttpServerResponse>
#include <QTcpServer>
#endif
using namespace amnezia;
LocalProxyController::LocalProxyController(SecureServersRepository *serversRepository,
SecureAppSettingsRepository *appSettingsRepository,
SubscriptionController *subscriptionController, QObject *parent)
: QObject(parent),
m_serversRepository(serversRepository),
m_appSettingsRepository(appSettingsRepository),
m_subscriptionController(subscriptionController)
{
connect(m_serversRepository, &SecureServersRepository::serverEdited, this, &LocalProxyController::onServerEdited);
connect(m_serversRepository, &SecureServersRepository::serverRemoved, this,
[this](const QString &serverId, int) { onServerRemoved(serverId); });
}
LocalProxyController::~LocalProxyController()
{
stop();
}
bool LocalProxyController::isEnabled() const
{
return m_isEnabled;
}
int LocalProxyController::port() const
{
return m_appSettingsRepository->localProxyPort();
}
QString LocalProxyController::ownerId() const
{
return m_appSettingsRepository->localProxyOwnerId();
}
bool LocalProxyController::isPortUserDefined() const
{
return m_appSettingsRepository->isLocalProxyPortUserDefined();
}
int LocalProxyController::activePort() const
{
return m_activePort;
}
bool LocalProxyController::setPort(int port)
{
if (port < localProxy::proxyPortMin || port > localProxy::proxyPortMax) {
return false;
}
const bool isPortChanged = m_appSettingsRepository->localProxyPort() != static_cast<quint16>(port);
if (isPortChanged) {
m_appSettingsRepository->setLocalProxyPort(static_cast<quint16>(port));
}
m_appSettingsRepository->setLocalProxyPortUserDefined(true);
emit stateChanged();
if (isPortChanged && m_isEnabled) {
start();
}
return true;
}
bool LocalProxyController::isPortBusy(int port) const
{
return !portUtils::isPortAvailable(port);
}
bool LocalProxyController::enable(const QString &ownerId, int port)
{
if (!isSupported() || ownerId.isEmpty() || port < localProxy::proxyPortMin || port > localProxy::proxyPortMax) {
return false;
}
if (m_isEnabled && m_appSettingsRepository->localProxyOwnerId() != ownerId) {
return false;
}
if (!portUtils::isPortAvailable(port) && m_appSettingsRepository->isLocalProxyPortUserDefined()) {
return false;
}
if (m_appSettingsRepository->localProxyPort() != static_cast<quint16>(port)) {
m_appSettingsRepository->setLocalProxyPort(static_cast<quint16>(port));
}
m_appSettingsRepository->setLocalProxyOwnerId(ownerId);
m_isEnabled = true;
emit stateChanged();
start();
return true;
}
void LocalProxyController::disable()
{
if (!m_isEnabled) {
return;
}
qDebug() << "Local proxy is disabled";
m_isEnabled = false;
stop();
emit stateChanged();
}
void LocalProxyController::start()
{
if (!startApi(localProxy::apiPort)) {
disableWithError(tr("Local proxy failed to start. Check if the port is available"));
return;
}
if (!syncXray()) {
disableWithError(tr("Couldn’t start the proxy due to an internal error. Try restarting the app"));
return;
}
qDebug() << "Local proxy is running on 127.0.0.1:" << m_apiPort;
}
void LocalProxyController::onServerEdited(const QString &serverId)
{
if (m_apiPort == 0 || m_appSettingsRepository->localProxyOwnerId() != serverId) {
return;
}
qDebug() << "Owner server edited, restarting Xray";
if (restartXray()) {
m_appliedProxyPort = m_appSettingsRepository->localProxyPort();
} else {
disableWithError(tr("Couldn’t start the proxy due to an internal error. Try restarting the app"));
}
}
void LocalProxyController::onServerRemoved(const QString &serverId)
{
if (m_appSettingsRepository->localProxyOwnerId() != serverId) {
return;
}
m_appSettingsRepository->setLocalProxyOwnerId(QString());
if (m_isEnabled) {
m_isEnabled = false;
stop();
}
emit stateChanged();
}
void LocalProxyController::onVpnConnectionRequested()
{
if (!m_isEnabled) {
return;
}
disable();
emit stoppedByVpn(tr("Local proxy stopped because VPN was turned on"));
}
void LocalProxyController::stop()
{
stopXray();
stopApi();
m_appliedProxyPort = 0;
}
bool LocalProxyController::syncXray()
{
const quint16 proxyPort = m_appSettingsRepository->localProxyPort();
bool synced;
if (!isXrayEngineRunning()) {
synced = startXray();
} else if (m_appliedProxyPort != proxyPort) {
qDebug() << "Proxy port changed from" << m_appliedProxyPort << "to" << proxyPort;
synced = restartXray();
} else {
return true;
}
if (synced) {
m_appliedProxyPort = proxyPort;
}
return synced;
}
bool LocalProxyController::startXray()
{
if (isXrayEngineRunning()) {
return true;
}
const auto config = buildXrayConfig();
if (!config) {
return false;
}
if (!startXrayEngine(config->serializedConfig)) {
return false;
}
qDebug() << "Xray started on port" << config->proxyPort;
setActivePort(config->proxyPort);
return true;
}
bool LocalProxyController::stopXray()
{
if (!stopXrayEngine()) {
return false;
}
setActivePort(0);
return true;
}
bool LocalProxyController::restartXray()
{
qDebug() << "Restarting Xray with updated config";
if (!stopXray()) {
return false;
}
return startXray();
}
void LocalProxyController::disableWithError(const QString &message)
{
qWarning() << message;
m_isEnabled = false;
stop();
emit stateChanged();
emit startFailed(message);
}
void LocalProxyController::setActivePort(int port)
{
if (m_activePort == port) {
return;
}
m_activePort = port;
emit stateChanged();
}
int LocalProxyController::runningProxyPort() const
{
return isXrayEngineRunning() ? m_activePort : 0;
}
std::optional<LocalProxyController::XrayConfig> LocalProxyController::buildXrayConfig() const
{
const QString ownerId = m_appSettingsRepository->localProxyOwnerId();
if (ownerId.isEmpty()) {
qWarning() << "Local proxy owner server id is not configured";
return std::nullopt;
}
const auto ownerServer = m_serversRepository->serverJsonById(ownerId);
if (!ownerServer) {
qWarning() << "Owner server with id" << ownerId << "not found";
return std::nullopt;
}
if (!apiUtils::isPremiumServer(*ownerServer)) {
qWarning() << "Server" << ownerServer->value(configKey::name).toString() << "is not premium, local proxy is unavailable";
return std::nullopt;
}
auto serializedConfig = xrayConfigUtils::extractLastConfig(*ownerServer);
if (!serializedConfig) {
serializedConfig = fetchXrayConfigFromGateway(ownerId);
if (!serializedConfig) {
return std::nullopt;
}
}
QJsonParseError parseError;
const QJsonDocument doc = QJsonDocument::fromJson(serializedConfig->toUtf8(), &parseError);
if (parseError.error != QJsonParseError::NoError || !doc.isObject()) {
qWarning() << "Failed to parse Xray config JSON:" << parseError.errorString();
return std::nullopt;
}
const auto proxyPort = selectProxyPort();
if (!proxyPort) {
return std::nullopt;
}
QJsonObject xrayConfig = doc.object();
if (!xrayConfigUtils::setInboundPort(xrayConfig, *proxyPort)) {
qWarning() << "Failed to override local proxy inbound port, using original config";
return XrayConfig { *serializedConfig, 0 };
}
return XrayConfig { QString::fromUtf8(QJsonDocument(xrayConfig).toJson(QJsonDocument::Compact)), *proxyPort };
}
std::optional<int> LocalProxyController::selectProxyPort() const
{
int port = m_appSettingsRepository->localProxyPort();
if (port < localProxy::proxyPortMin || port > localProxy::proxyPortMax) {
port = localProxy::defaultProxyPort;
}
if (portUtils::waitForPort(port, localProxy::portReleaseWaitMs)) {
return port;
}
if (m_appSettingsRepository->isLocalProxyPortUserDefined()) {
qWarning() << "Local proxy port" << port << "is already in use";
return std::nullopt;
}
std::optional<int> freePort;
if (port != localProxy::defaultProxyPort && portUtils::isPortAvailable(localProxy::defaultProxyPort)) {
freePort = localProxy::defaultProxyPort;
} else {
freePort = portUtils::findFirstAvailablePort(localProxy::defaultProxyPort + 1, localProxy::proxyPortMax);
}
if (!freePort) {
qWarning() << "No available local proxy port in range" << localProxy::defaultProxyPort + 1 << "-"
<< localProxy::proxyPortMax;
return std::nullopt;
}
qDebug() << "Local proxy port" << port << "is busy, using" << *freePort;
return freePort;
}
std::optional<QString> LocalProxyController::fetchXrayConfigFromGateway(const QString &serverId) const
{
QJsonObject serverConfig;
const ErrorCode errorCode = m_subscriptionController->fetchServiceConfig(serverId, configKey::vless, serverConfig);
if (errorCode != ErrorCode::NoError) {
qWarning() << "Gateway request failed with error code" << static_cast<int>(errorCode);
return std::nullopt;
}
const auto serializedConfig = xrayConfigUtils::extractLastConfig(serverConfig);
if (!serializedConfig) {
qWarning() << "Gateway response lacks Xray last_config payload";
return std::nullopt;
}
qDebug() << "Fetched Xray config from gateway";
return serializedConfig;
}
#ifdef AMNEZIA_DESKTOP
bool LocalProxyController::isSupported() const
{
return true;
}
bool LocalProxyController::startApi(quint16 port)
{
if (m_httpServer && m_apiPort == port) {
return true;
}
stopApi();
const auto proxyPortValue = [this]() {
const int port = runningProxyPort();
return port > 0 ? QJsonValue(port) : QJsonValue::Null;
};
auto httpServer = new QHttpServer(this);
httpServer->route("/api/v1/up", QHttpServerRequest::Method::Post, [this, proxyPortValue] {
const bool started = startXray();
return QHttpServerResponse(QJsonObject { { "status", started ? "ok" : "error" }, { "proxyPort", proxyPortValue() } });
});
httpServer->route("/api/v1/down", QHttpServerRequest::Method::Post, [this] {
const bool stopped = stopXray();
return QHttpServerResponse(QJsonObject { { "status", stopped ? "ok" : "error" } });
});
httpServer->route("/api/v1/ping", QHttpServerRequest::Method::Get, [proxyPortValue] {
return QHttpServerResponse(QJsonObject { { "status", "ok" }, { "proxyPort", proxyPortValue() } });
});
auto tcpServer = new QTcpServer(httpServer);
if (!tcpServer->listen(QHostAddress::LocalHost, port) || !httpServer->bind(tcpServer)) {
qWarning() << "Failed to start HTTP API server on port" << port;
delete httpServer;
return false;
}
m_httpServer = httpServer;
m_apiPort = port;
qDebug() << "HTTP API server is running on localhost:" << port;
return true;
}
void LocalProxyController::stopApi()
{
delete m_httpServer;
m_httpServer = nullptr;
m_apiPort = 0;
}
bool LocalProxyController::startXrayEngine(const QString &config)
{
if (config.trimmed().isEmpty()) {
qWarning() << "Xray config content is empty";
return false;
}
const qint64 token = IpcClient::withInterface(
[&](QSharedPointer<IpcInterfaceReplica> iface) -> qint64 {
auto xrayStart = iface->xrayStartOwned(config);
if (!xrayStart.waitForFinished()) {
return 0;
}
return xrayStart.returnValue();
},
[]() -> qint64 { return 0; });
if (token == 0) {
qWarning() << "Failed to start Xray via IPC";
return false;
}
m_xrayToken = token;
return true;
}
bool LocalProxyController::stopXrayEngine()
{
if (m_xrayToken == 0) {
return true;
}
const bool stopped = IpcClient::withInterface(
[this](QSharedPointer<IpcInterfaceReplica> iface) {
auto xrayStop = iface->xrayStopOwned(m_xrayToken);
return xrayStop.waitForFinished() && xrayStop.returnValue();
},
[]() { return false; });
if (!stopped) {
qWarning() << "Failed to stop Xray via IPC";
return false;
}
m_xrayToken = 0;
qDebug() << "Xray stopped";
return true;
}
bool LocalProxyController::isXrayEngineRunning() const
{
if (m_xrayToken == 0) {
return false;
}
return IpcClient::withInterface(
[this](QSharedPointer<IpcInterfaceReplica> iface) {
auto currentToken = iface->xrayCurrentToken();
return currentToken.waitForFinished() && currentToken.returnValue() == m_xrayToken;
},
[]() { return false; });
}
#else
bool LocalProxyController::isSupported() const
{
return false;
}
bool LocalProxyController::startApi(quint16)
{
return false;
}
void LocalProxyController::stopApi()
{
}
bool LocalProxyController::startXrayEngine(const QString &)
{
return false;
}
bool LocalProxyController::stopXrayEngine()
{
return true;
}
bool LocalProxyController::isXrayEngineRunning() const
{
return false;
}
#endif
@@ -0,0 +1,84 @@
#ifndef LOCALPROXYCONTROLLER_H
#define LOCALPROXYCONTROLLER_H
#include <optional>
#include <QObject>
#include <QString>
class QHttpServer;
class SecureServersRepository;
class SecureAppSettingsRepository;
class SubscriptionController;
class LocalProxyController : public QObject
{
Q_OBJECT
public:
LocalProxyController(SecureServersRepository *serversRepository, SecureAppSettingsRepository *appSettingsRepository,
SubscriptionController *subscriptionController, QObject *parent = nullptr);
~LocalProxyController();
bool isSupported() const;
bool isEnabled() const;
int port() const;
QString ownerId() const;
bool isPortUserDefined() const;
int activePort() const;
bool setPort(int port);
bool isPortBusy(int port) const;
bool enable(const QString &ownerId, int port);
void disable();
public slots:
void onServerEdited(const QString &serverId);
void onServerRemoved(const QString &serverId);
void onVpnConnectionRequested();
signals:
void stateChanged();
void startFailed(const QString &message);
void stoppedByVpn(const QString &message);
private:
struct XrayConfig
{
QString serializedConfig;
int proxyPort = 0;
};
void start();
void stop();
bool syncXray();
bool startXray();
bool stopXray();
bool restartXray();
void disableWithError(const QString &message);
void setActivePort(int port);
int runningProxyPort() const;
std::optional<XrayConfig> buildXrayConfig() const;
std::optional<int> selectProxyPort() const;
std::optional<QString> fetchXrayConfigFromGateway(const QString &serverId) const;
bool startApi(quint16 port);
void stopApi();
bool startXrayEngine(const QString &config);
bool stopXrayEngine();
bool isXrayEngineRunning() const;
SecureServersRepository *m_serversRepository;
SecureAppSettingsRepository *m_appSettingsRepository;
SubscriptionController *m_subscriptionController;
bool m_isEnabled = false;
QHttpServer *m_httpServer = nullptr;
quint16 m_apiPort = 0;
qint64 m_xrayToken = 0;
quint16 m_appliedProxyPort = 0;
int m_activePort = 0;
};
#endif // LOCALPROXYCONTROLLER_H
+270
View File
@@ -0,0 +1,270 @@
#include "logsController.h"
#include <QFile>
#include <QFileInfo>
#ifdef AMNEZIA_HAS_QZIPWRITER
#include <QBuffer>
#include <QtCore/private/qzipwriter_p.h>
#else
#include <QDateTime>
#include <array>
#endif
#include <limits>
#include "logger.h"
#if defined(Q_OS_IOS) || defined(MACOS_NE)
#include "core/utils/swiftBridge.h"
#elif defined(Q_OS_ANDROID)
#include "platforms/android/android_controller.h"
#endif
#ifndef AMNEZIA_HAS_QZIPWRITER
namespace
{
quint32 crc32(const QByteArray &data)
{
static const std::array<quint32, 256> table = [] {
std::array<quint32, 256> result {};
for (quint32 i = 0; i < 256; ++i) {
quint32 value = i;
for (int bit = 0; bit < 8; ++bit) {
value = (value & 1) ? 0xEDB88320u ^ (value >> 1) : value >> 1;
}
result[i] = value;
}
return result;
}();
quint32 crc = 0xFFFFFFFFu;
for (const char byte : data) {
crc = table[(crc ^ static_cast<uchar>(byte)) & 0xFF] ^ (crc >> 8);
}
return crc ^ 0xFFFFFFFFu;
}
void put16(QByteArray &out, quint16 value)
{
out.append(static_cast<char>(value & 0xFF));
out.append(static_cast<char>(value >> 8));
}
void put32(QByteArray &out, quint32 value)
{
put16(out, static_cast<quint16>(value & 0xFFFF));
put16(out, static_cast<quint16>(value >> 16));
}
QByteArray storedZip(const QList<QPair<QString, QByteArray>> &entries)
{
const QDateTime now = QDateTime::currentDateTime();
const quint16 dosTime = (now.time().hour() << 11) | (now.time().minute() << 5) | (now.time().second() / 2);
const quint16 dosDate = ((now.date().year() - 1980) << 9) | (now.date().month() << 5) | now.date().day();
QByteArray archive;
QByteArray centralDirectory;
for (const auto &[name, data] : entries) {
const QByteArray fileName = name.toUtf8();
const quint32 offset = static_cast<quint32>(archive.size());
const quint32 size = static_cast<quint32>(data.size());
QByteArray fields;
put16(fields, 10);
put16(fields, 0);
put16(fields, 0);
put16(fields, dosTime);
put16(fields, dosDate);
put32(fields, crc32(data));
put32(fields, size);
put32(fields, size);
put16(fields, static_cast<quint16>(fileName.size()));
put16(fields, 0);
put32(archive, 0x04034b50);
archive.append(fields).append(fileName).append(data);
put32(centralDirectory, 0x02014b50);
put16(centralDirectory, 20);
centralDirectory.append(fields);
put16(centralDirectory, 0);
put16(centralDirectory, 0);
put16(centralDirectory, 0);
put32(centralDirectory, 0);
put32(centralDirectory, offset);
centralDirectory.append(fileName);
}
const quint32 centralDirectoryOffset = static_cast<quint32>(archive.size());
archive.append(centralDirectory);
put32(archive, 0x06054b50);
put16(archive, 0);
put16(archive, 0);
put16(archive, static_cast<quint16>(entries.size()));
put16(archive, static_cast<quint16>(entries.size()));
put32(archive, static_cast<quint32>(centralDirectory.size()));
put32(archive, centralDirectoryOffset);
put16(archive, 0);
return archive;
}
}
#endif
QStringList LogsController::files(Stream stream)
{
#if defined(AMNEZIA_DESKTOP)
if (stream == Stream::App) {
return { Logger::userLogsFilePath() };
}
return { Logger::serviceLogsFilePath() };
#elif defined(Q_OS_IOS) || defined(MACOS_NE)
if (stream == Stream::App) {
return { QString::fromStdString(SWIFT_BRIDGE_NAMESPACE::swiftAppLogPath()), Logger::userLogsFilePath() };
}
return { QString::fromStdString(SWIFT_BRIDGE_NAMESPACE::swiftTunnelLogPath()) };
#elif defined(Q_OS_ANDROID)
return AndroidController::logFiles(static_cast<int>(stream));
#endif
}
qint64 LogsController::size(Stream stream)
{
qint64 total = 0;
for (const QString &path : files(stream)) {
total += fileSize(path);
}
return total;
}
qint64 LogsController::fileSize(const QString &path)
{
QFile file(path);
return file.open(QIODevice::ReadOnly) ? file.size() : 0;
}
QString LogsController::readTail(Stream stream, qint64 *newestFileEnd, qint64 maxBytes)
{
return QString::fromUtf8(read(stream, maxBytes, newestFileEnd));
}
QByteArray LogsController::readAll(Stream stream)
{
return read(stream, std::numeric_limits<qint64>::max());
}
QByteArray LogsController::read(Stream stream, qint64 maxBytes, qint64 *newestFileEnd)
{
QByteArray data = tailOfFiles(files(stream), maxBytes, newestFileEnd);
#if defined(Q_OS_ANDROID)
if (stream == Stream::App && maxBytes == std::numeric_limits<qint64>::max()) {
data.prepend(AndroidController::deviceInfo().toUtf8() + '\n');
}
#endif
return data;
}
void LogsController::clear(Stream stream)
{
#if defined(AMNEZIA_DESKTOP)
if (stream == Stream::App) {
Logger::clearLogs(false);
} else {
Logger::clearServiceLogs();
}
#elif defined(Q_OS_IOS) || defined(MACOS_NE)
if (stream == Stream::App) {
Logger::clearLogs(false);
QFile::resize(QString::fromStdString(SWIFT_BRIDGE_NAMESPACE::swiftAppLogPath()), 0);
} else {
QFile::resize(QString::fromStdString(SWIFT_BRIDGE_NAMESPACE::swiftTunnelLogPath()), 0);
}
#elif defined(Q_OS_ANDROID)
AndroidController::clearLogStream(static_cast<int>(stream));
#endif
}
QByteArray LogsController::zipAll()
{
return zipFiles({ { QStringLiteral("app.log"), readAll(Stream::App) },
{ QStringLiteral("tunnel.log"), readAll(Stream::Tunnel) } });
}
QByteArray LogsController::zipFiles(const QList<QPair<QString, QByteArray>> &entries)
{
#ifdef AMNEZIA_HAS_QZIPWRITER
QBuffer buffer;
buffer.open(QIODevice::WriteOnly);
QZipWriter zip(&buffer);
zip.setCompressionPolicy(QZipWriter::AlwaysCompress);
for (const auto &[name, data] : entries) {
zip.addFile(name, data);
}
zip.close();
return buffer.data();
#else
return storedZip(entries);
#endif
}
QByteArray LogsController::tailOfFiles(const QStringList &paths, qint64 maxBytes, qint64 *newestFileEnd)
{
if (newestFileEnd) {
*newestFileEnd = 0;
}
QList<QPair<QString, QByteArray>> chunks;
qint64 remaining = maxBytes;
for (auto it = paths.crbegin(); it != paths.crend() && remaining > 0; ++it) {
QFile file(*it);
if (!file.open(QIODevice::ReadOnly)) {
continue;
}
const qint64 fileSize = file.size();
QByteArray chunk;
if (fileSize <= remaining) {
chunk = file.read(fileSize);
remaining -= fileSize;
} else {
file.seek(fileSize - remaining - 1);
chunk = file.read(remaining + 1);
const qsizetype newline = chunk.indexOf('\n');
qsizetype start = newline + 1;
if (newline < 0) {
start = 1;
while (start < chunk.size() && (static_cast<uchar>(chunk.at(start)) & 0xC0) == 0x80) {
++start;
}
}
chunk = chunk.mid(start);
remaining = 0;
}
if (newestFileEnd && it == paths.crbegin()) {
*newestFileEnd = file.pos();
}
if (!chunk.isEmpty()) {
chunks.prepend({ QFileInfo(*it).fileName(), chunk });
}
}
if (chunks.size() == 1) {
return chunks.first().second;
}
QByteArray result;
for (const auto &[fileName, chunk] : chunks) {
result += "----- " + fileName.toUtf8() + " -----\n" + chunk;
if (!chunk.endsWith('\n')) {
result += '\n';
}
}
return result;
}
+30
View File
@@ -0,0 +1,30 @@
#ifndef LOGSCONTROLLER_H
#define LOGSCONTROLLER_H
#include <QByteArray>
#include <QList>
#include <QPair>
#include <QString>
#include <QStringList>
class LogsController
{
public:
enum class Stream { App, Tunnel };
static QStringList files(Stream stream);
static qint64 size(Stream stream);
static qint64 fileSize(const QString &path);
static QString readTail(Stream stream, qint64 *newestFileEnd = nullptr, qint64 maxBytes = 512 * 1024);
static QByteArray readAll(Stream stream);
static void clear(Stream stream);
static QByteArray zipAll();
static QByteArray zipFiles(const QList<QPair<QString, QByteArray>> &entries);
static QByteArray tailOfFiles(const QStringList &paths, qint64 maxBytes, qint64 *newestFileEnd = nullptr);
private:
static QByteArray read(Stream stream, qint64 maxBytes, qint64 *newestFileEnd = nullptr);
};
#endif // LOGSCONTROLLER_H
@@ -33,6 +33,18 @@ using namespace ProtocolUtils;
namespace
{
bool rejectUnsupportedFormatVersion(ImportController::ImportResult &result)
{
if (serverConfigUtils::isConfigFormatVersionSupported(result.config)) {
return false;
}
qWarning() << "Config format version" << serverConfigUtils::configFormatVersion(result.config)
<< "is newer than supported" << serverConfigUtils::currentConfigFormatVersion;
result.errorCode = ErrorCode::ConfigFormatVersionNotSupportedError;
result.config = {};
return true;
}
ConfigTypes checkConfigFormat(const QString &config)
{
const QString openVpnConfigPatternCli = "client";
@@ -52,6 +64,7 @@ namespace
const QString amneziaFreeConfigPattern = "api_key";
const QString amneziaPremiumConfigPattern = "auth_data";
const QString backupPattern = "Servers/serversList";
const QString xraySettingsPattern = "serverConfig";
if (config.contains(backupPattern)) {
return ConfigTypes::Backup;
@@ -67,6 +80,8 @@ namespace
} else if (config.contains(openVpnConfigPatternCli)
&& (config.contains(openVpnConfigPatternDriver1) || config.contains(openVpnConfigPatternDriver2))) {
return ConfigTypes::OpenVpn;
} else if (QJsonDocument::fromJson(config.toUtf8()).object().value(xraySettingsPattern).isObject()) {
return ConfigTypes::XraySettings;
}
return ConfigTypes::Invalid;
}
@@ -207,6 +222,10 @@ ImportController::ImportResult ImportController::extractConfigFromData(const QSt
case ConfigTypes::Amnezia: {
result.config = QJsonDocument::fromJson(config.toUtf8()).object();
if (rejectUnsupportedFormatVersion(result)) {
return result;
}
if (serverConfigUtils::isServerFromApi(result.config)) {
auto apiConfig = result.config.value(apiDefs::key::apiConfig).toObject();
apiConfig[apiDefs::key::vpnKey] = data;
@@ -231,6 +250,10 @@ ImportController::ImportResult ImportController::extractConfigFromData(const QSt
result.errorCode = ErrorCode::ImportBackupFileUseRestoreInstead;
return result;
}
case ConfigTypes::XraySettings: {
result.errorCode = ErrorCode::ImportXraySettingsFileUseXrayPage;
return result;
}
case ConfigTypes::Invalid: {
result.errorCode = ErrorCode::ImportInvalidConfigError;
result.configFileName.clear();
@@ -256,6 +279,7 @@ ImportController::ImportResult ImportController::extractConfigFromQr(const QByte
if (!dataObj.isEmpty()) {
result.config = dataObj;
result.configType = ConfigTypes::Amnezia;
rejectUnsupportedFormatVersion(result);
return result;
}
@@ -267,6 +291,7 @@ ImportController::ImportResult ImportController::extractConfigFromQr(const QByte
return result;
}
result.configType = ConfigTypes::Amnezia;
rejectUnsupportedFormatVersion(result);
return result;
}
@@ -284,6 +309,7 @@ ImportController::ImportResult ImportController::extractConfigFromQr(const QByte
return result;
}
result.configType = ConfigTypes::Amnezia;
rejectUnsupportedFormatVersion(result);
return result;
}
@@ -379,6 +405,11 @@ int ImportController::qrChunksTotal() const
void ImportController::importConfig(const QJsonObject &config)
{
if (!serverConfigUtils::isConfigFormatVersionSupported(config)) {
emit importErrorOccurred(ErrorCode::ConfigFormatVersionNotSupportedError, false);
return;
}
ServerCredentials credentials;
credentials.hostName = config.value(configKey::hostName).toString();
credentials.port = config.value(configKey::port).toInt();
@@ -22,6 +22,7 @@ namespace
Xray,
ShadowSocks,
Backup,
XraySettings,
Invalid
};
}
@@ -12,6 +12,7 @@
#include "core/configurators/configuratorBase.h"
#include "core/configurators/xrayConfigurator.h"
#include "core/models/protocols/xrayProtocolConfig.h"
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
@@ -60,6 +61,70 @@ namespace
{
Logger logger("InstallController");
QString effectiveXrayPort(const XrayProtocolConfig *cfg)
{
if (!cfg || cfg->serverConfig.port.isEmpty()) {
return QString::fromLatin1(protocols::xray::defaultPort);
}
return cfg->serverConfig.port;
}
QString normalizeXrayRelease(QString raw)
{
raw = raw.trimmed();
if (raw.startsWith(QLatin1String("v"), Qt::CaseInsensitive)) {
raw.remove(0, 1);
}
return raw.trimmed();
}
QString parseXrayVersionOutput(const QString &output)
{
static const QRegularExpression re(QStringLiteral(R"(Xray\s+v?(\d+\.\d+\.\d+))"),
QRegularExpression::CaseInsensitiveOption);
const auto match = re.match(output);
if (!match.hasMatch()) {
return {};
}
return match.captured(1);
}
enum class XrayBinaryProbeResult {
Match,
Mismatch,
Failed
};
XrayBinaryProbeResult probeXrayServerBinary(const ServerCredentials &credentials, SshSession &sshSession)
{
const QString expected =
normalizeXrayRelease(QString::fromLatin1(protocols::xray::expectedServerXrayRelease));
QString stdOut;
auto collect = [&stdOut](const QString &data, libssh::Client &) {
stdOut += data;
return ErrorCode::NoError;
};
const QString script = SshSession::replaceVars(
QStringLiteral("sudo docker exec $CONTAINER_NAME xray version 2>/dev/null | head -1"),
amnezia::genBaseVars(credentials, DockerContainer::Xray, QString(), QString()));
const ErrorCode errorCode = sshSession.runScript(credentials, script, collect, collect);
const QString live = parseXrayVersionOutput(stdOut);
if (errorCode != ErrorCode::NoError || live.isEmpty()) {
return XrayBinaryProbeResult::Failed;
}
if (live != expected) {
return XrayBinaryProbeResult::Mismatch;
}
return XrayBinaryProbeResult::Match;
}
bool xrayServerBinaryMismatchesExpected(const ServerCredentials &credentials, SshSession &sshSession)
{
return probeXrayServerBinary(credentials, sshSession) == XrayBinaryProbeResult::Mismatch;
}
bool dockerDaemonContainerMissing(const QString &out, const QString &containerDockerName)
{
if (!out.contains(QLatin1String("Error response from daemon"), Qt::CaseInsensitive)) {
@@ -75,15 +140,34 @@ namespace
return false;
}
bool containerKeepsIdentityInDataVolume(DockerContainer container)
{
return container == DockerContainer::MtProxy
|| container == DockerContainer::Telemt
|| container == DockerContainer::TProxy
|| container == DockerContainer::Xray;
}
QString buildRemoveContainerScript(const amnezia::ScriptVars &vars, bool removeDataVolume)
{
QString script = SshSession::replaceVars(amnezia::scriptData(SharedScriptType::remove_container), vars);
if (removeDataVolume) {
script += QLatin1String("\nsudo docker volume rm -f $CONTAINER_NAME-data 2>/dev/null || true");
script += QLatin1String(
"\nfor attempt in 1 2 3 4 5; do"
"\n sudo docker volume rm -f $CONTAINER_NAME-data && break"
"\n sleep 1"
"\ndone"
"\necho \"amnezia_volume_left=$(sudo docker volume ls -q -f name=^$CONTAINER_NAME-data$ | head -1)\"");
script = SshSession::replaceVars(script, vars);
}
return script;
}
bool dataVolumeSurvivedRemoval(const QString &out)
{
static const QRegularExpression reLeft(QStringLiteral("amnezia_volume_left=(\\S+)"));
return reLeft.match(out).hasMatch();
}
}
InstallController::InstallController(SecureServersRepository *serversRepository,
@@ -102,7 +186,7 @@ InstallController::~InstallController()
}
ErrorCode InstallController::setupContainer(const ServerCredentials &credentials, DockerContainer container, ContainerConfig &config,
bool isUpdate)
bool isUpdate, bool acceptXrayKeyLoss)
{
SshSession sshSession;
ErrorCode e = ErrorCode::NoError;
@@ -129,12 +213,38 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
if (e)
return e;
QMap<QString, QString> xrayStateToMigrate;
if (isUpdate) {
if (!acceptXrayKeyLoss) {
e = readXrayStateBeforeVolumeMigration(credentials, container, sshSession, xrayStateToMigrate);
if (e)
return e;
}
}
const amnezia::ScriptVars removeContainerVars =
amnezia::genBaseVars(credentials, container, QString(), QString());
const bool removeDataVolume = !isUpdate && (container == DockerContainer::MtProxy
|| container == DockerContainer::Telemt || container == DockerContainer::TProxy);
sshSession.runScript(credentials, buildRemoveContainerScript(removeContainerVars, removeDataVolume));
const bool removeDataVolume = !isUpdate && containerKeepsIdentityInDataVolume(container);
QString removeOut;
auto collectRemoveOut = [&removeOut](const QString &data, libssh::Client &) {
removeOut += data + "\n";
return ErrorCode::NoError;
};
const ErrorCode removeError =
sshSession.runScript(credentials, buildRemoveContainerScript(removeContainerVars, removeDataVolume),
collectRemoveOut, collectRemoveOut);
qDebug().noquote() << "InstallController::setupContainer removeContainer finished";
if (removeDataVolume) {
if (removeError != ErrorCode::NoError) {
return ErrorCode::ServerDataVolumeNotRemoved;
}
if (dataVolumeSurvivedRemoval(removeOut)) {
return ErrorCode::ServerDataVolumeNotRemoved;
}
}
qDebug().noquote() << "buildContainerWorker start";
e = buildContainerWorker(credentials, container, config, sshSession);
if (e)
return e;
@@ -143,6 +253,10 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
if (e)
return e;
e = restoreXrayStateIntoDataVolume(credentials, container, sshSession, xrayStateToMigrate);
if (e)
return e;
e = configureContainerWorker(credentials, container, config, sshSession);
if (e)
return e;
@@ -161,20 +275,41 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
}
ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerContainer container, const ContainerConfig &oldConfig,
ContainerConfig &newConfig)
ContainerConfig &newConfig, bool acceptXrayKeyLoss)
{
if (!isUpdateDockerContainerRequired(container, oldConfig, newConfig)) {
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
if (!adminConfig.has_value()) {
const bool serverSettingsChanged = isUpdateDockerContainerRequired(container, oldConfig, newConfig);
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
if (!adminConfig.has_value()) {
return ErrorCode::InternalError;
}
ServerCredentials credentials = adminConfig->credentials();
SshSession sshSession;
bool reinstallRequired = false;
bool portReinstall = false;
bool binaryMismatch = false;
if (serverSettingsChanged) {
if (!credentials.isValid()) {
return ErrorCode::InternalError;
}
portReinstall = isReinstallContainerRequired(container, oldConfig, newConfig);
reinstallRequired = portReinstall;
}
if (container == DockerContainer::Xray) {
if (credentials.isValid()) {
binaryMismatch = xrayServerBinaryMismatchesExpected(credentials, sshSession);
if (binaryMismatch) {
reinstallRequired = true;
}
}
}
if (!serverSettingsChanged && !reinstallRequired) {
if (container == DockerContainer::MtProxy) {
ServerCredentials credentials = adminConfig->credentials();
SshSession sshSession;
MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
} else if (container == DockerContainer::Telemt) {
ServerCredentials credentials = adminConfig->credentials();
SshSession sshSession;
TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
} else if (container == DockerContainer::TProxy) {
ServerCredentials credentials = adminConfig->credentials();
@@ -186,22 +321,24 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
return ErrorCode::NoError;
}
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
if (!adminConfig.has_value()) {
return ErrorCode::InternalError;
}
ServerCredentials credentials = adminConfig->credentials();
if (!credentials.isValid()) {
return ErrorCode::InternalError;
}
SshSession sshSession;
bool reinstallRequired = isReinstallContainerRequired(container, oldConfig, newConfig);
qDebug() << "InstallController::updateServerConfig for container" << container << "reinstall required is" << reinstallRequired;
ErrorCode errorCode = ErrorCode::NoError;
if (reinstallRequired) {
errorCode = setupContainer(credentials, container, newConfig, true);
if (container == DockerContainer::Xray) {
const QString oldPort = effectiveXrayPort(oldConfig.getXrayProtocolConfig());
const QString newPort = effectiveXrayPort(newConfig.getXrayProtocolConfig());
if (oldPort != newPort) {
errorCode = isServerPortBusy(credentials, container, newConfig, sshSession);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
}
}
errorCode = setupContainer(credentials, container, newConfig, true, acceptXrayKeyLoss);
// Reinstall pulls the latest container image, so the server runs the latest protocol version
if (errorCode == ErrorCode::NoError && container == DockerContainer::Awg2) {
@@ -209,7 +346,12 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
awgConfig->serverConfig.protocolVersion = protocols::awg::awgV3;
}
}
} else if (container != DockerContainer::Xray && container != DockerContainer::SSXray) {
} else if (container == DockerContainer::Xray) {
DnsSettings dnsSettings = { m_appSettingsRepository->primaryDns(), m_appSettingsRepository->secondaryDns() };
XrayConfigurator xrayConfigurator(&sshSession);
errorCode = xrayConfigurator.writeServerConfigForSetup(credentials, container, newConfig, dnsSettings,
true);
} else if (container != DockerContainer::SSXray) {
errorCode = configureContainerWorker(credentials, container, newConfig, sshSession);
if (errorCode == ErrorCode::NoError) {
errorCode = startupContainerWorker(credentials, container, newConfig, sshSession);
@@ -232,7 +374,9 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
TProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
}
if (reinstallRequired) {
clearCachedProfile(serverId, container);
if (container != DockerContainer::Xray) {
clearCachedProfile(serverId, container);
}
}
adminConfig->updateContainerConfig(container, newConfig);
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
@@ -441,6 +585,9 @@ ErrorCode InstallController::prepareContainerConfig(DockerContainer container, c
void InstallController::adminAppendRequested(const QString &serverId, DockerContainer container,
const ContainerConfig &containerConfig, const QString &clientName)
{
if (container == DockerContainer::Xray) {
return;
}
if (ContainerUtils::containerService(container) == ServiceType::Other
|| !containerConfig.protocolConfig.hasClientConfig()) {
return;
@@ -570,6 +717,10 @@ ErrorCode InstallController::configureContainerWorker(const ServerCredentials &c
return ErrorCode::ServerContainerMissingError;
}
if (stdOut.contains(QLatin1String("amnezia_xray_keygen=failed"))) {
return ErrorCode::XrayKeyGenerationFailed;
}
updateContainerConfigAfterInstallation(container, config, stdOut);
if (container == DockerContainer::MtProxy) {
@@ -606,6 +757,127 @@ ErrorCode InstallController::startupContainerWorker(const ServerCredentials &cre
baseVars));
}
ErrorCode InstallController::readXrayStateBeforeVolumeMigration(const ServerCredentials &credentials,
DockerContainer container, SshSession &sshSession,
QMap<QString, QString> &outFiles)
{
outFiles.clear();
if (container != DockerContainer::Xray) {
return ErrorCode::NoError;
}
const amnezia::ScriptVars vars = amnezia::genBaseVars(credentials, container, QString(), QString());
QString stdOut;
auto collect = [&stdOut](const QString &data, libssh::Client &) {
stdOut += data + "\n";
return ErrorCode::NoError;
};
const QString probe = QStringLiteral(
"echo \"amnezia_container=$(sudo docker ps -a -q -f name=^$CONTAINER_NAME$ | head -1)\"\n"
"if [ -n \"$(sudo docker ps -a -q -f name=^$CONTAINER_NAME$ | head -1)\" ] && "
"[ -z \"$(sudo docker ps -q -f name=^$CONTAINER_NAME$ | head -1)\" ]; then\n"
" sudo docker start $CONTAINER_NAME > /dev/null 2>&1\n"
" sleep 2\n"
"fi\n"
"echo \"amnezia_running=$(sudo docker ps -q -f name=^$CONTAINER_NAME$ | head -1)\"\n"
"echo \"amnezia_mounted=$(sudo docker inspect -f "
"'{{range .Mounts}}{{if and (eq .Destination \"%1\") "
"(eq .Name \"$CONTAINER_NAME-data\")}}yes{{end}}{{end}}' "
"$CONTAINER_NAME 2>/dev/null | head -1)\"")
.arg(QString::fromLatin1(amnezia::protocols::xray::dataDir));
ErrorCode errorCode = sshSession.runScript(credentials, SshSession::replaceVars(probe, vars), collect, collect);
if (errorCode != ErrorCode::NoError) {
return ErrorCode::XrayKeyMigrationFailed;
}
static const QRegularExpression reContainer(QStringLiteral("amnezia_container=(\\S+)"));
static const QRegularExpression reRunning(QStringLiteral("amnezia_running=(\\S+)"));
static const QRegularExpression reMounted(QStringLiteral("amnezia_mounted=(\\S+)"));
const bool containerExists = reContainer.match(stdOut).hasMatch();
const bool containerRunning = reRunning.match(stdOut).hasMatch();
const bool dataDirIsVolume = reMounted.match(stdOut).hasMatch();
if (!containerExists) {
return ErrorCode::NoError;
}
if (dataDirIsVolume) {
return ErrorCode::NoError;
}
if (!containerRunning) {
return ErrorCode::XrayKeyMigrationNeedsConfirm;
}
const QStringList requiredPaths = {
QString::fromLatin1(amnezia::protocols::xray::PrivateKeyPath),
QString::fromLatin1(amnezia::protocols::xray::PublicKeyPath),
QString::fromLatin1(amnezia::protocols::xray::uuidPath),
QString::fromLatin1(amnezia::protocols::xray::shortidPath),
};
for (const QString &path : requiredPaths) {
QString content;
bool read = false;
for (int attempt = 0; attempt < 3 && !read; ++attempt) {
ErrorCode fileError = ErrorCode::NoError;
content = QString::fromUtf8(sshSession.getTextFileFromContainer(container, credentials, path, fileError));
if (fileError == ErrorCode::NoError && !content.trimmed().isEmpty()) {
read = true;
break;
}
if (attempt < 2) {
QThread::msleep(500);
}
}
if (!read) {
return ErrorCode::XrayKeyMigrationFailed;
}
outFiles.insert(path, content);
}
QString serverConfig;
bool configRead = false;
for (int attempt = 0; attempt < 3 && !configRead; ++attempt) {
ErrorCode configError = ErrorCode::NoError;
serverConfig = QString::fromUtf8(sshSession.getTextFileFromContainer(
container, credentials, QString::fromLatin1(amnezia::protocols::xray::serverConfigPath), configError));
if (configError == ErrorCode::NoError && !serverConfig.trimmed().isEmpty()) {
configRead = true;
break;
}
if (attempt < 2) {
QThread::msleep(500);
}
}
if (!configRead) {
return ErrorCode::XrayKeyMigrationFailed;
}
outFiles.insert(QString::fromLatin1(amnezia::protocols::xray::serverConfigPath), serverConfig);
return ErrorCode::NoError;
}
ErrorCode InstallController::restoreXrayStateIntoDataVolume(const ServerCredentials &credentials,
DockerContainer container, SshSession &sshSession,
const QMap<QString, QString> &files)
{
if (files.isEmpty()) {
return ErrorCode::NoError;
}
for (auto it = files.constBegin(); it != files.constEnd(); ++it) {
const ErrorCode errorCode = sshSession.uploadTextFileToContainer(
container, credentials, it.value(), it.key(), libssh::ScpOverwriteMode::ScpOverwriteExisting);
if (errorCode != ErrorCode::NoError) {
return ErrorCode::XrayKeyMigrationFailed;
}
}
return ErrorCode::NoError;
}
ErrorCode InstallController::isServerPortBusy(const ServerCredentials &credentials, DockerContainer container, const ContainerConfig &config, SshSession &sshSession)
{
if (container == DockerContainer::Dns) {
@@ -743,7 +1015,20 @@ bool InstallController::isReinstallContainerRequired(DockerContainer container,
}
}
if (container == DockerContainer::Xray || container == DockerContainer::SSXray) {
if (container == DockerContainer::Xray) {
const auto *oldXrayConfig = oldConfig.getXrayProtocolConfig();
const auto *newXrayConfig = newConfig.getXrayProtocolConfig();
if (oldXrayConfig && newXrayConfig) {
const QString oldPort = effectiveXrayPort(oldXrayConfig);
const QString newPort = effectiveXrayPort(newXrayConfig);
if (oldPort != newPort) {
return true;
}
}
}
if (container == DockerContainer::SSXray) {
const auto *oldXrayConfig = oldConfig.getXrayProtocolConfig();
const auto *newXrayConfig = newConfig.getXrayProtocolConfig();
@@ -1050,10 +1335,19 @@ ErrorCode InstallController::removeContainer(const QString &serverId, DockerCont
SshSession sshSession;
const amnezia::ScriptVars removeContainerVars =
amnezia::genBaseVars(credentials, container, QString(), QString());
const bool removeDataVolume = (container == DockerContainer::MtProxy || container == DockerContainer::Telemt
|| container == DockerContainer::TProxy);
ErrorCode errorCode =
sshSession.runScript(credentials, buildRemoveContainerScript(removeContainerVars, removeDataVolume));
const bool removeDataVolume = containerKeepsIdentityInDataVolume(container);
QString removeOut;
auto collectRemoveOut = [&removeOut](const QString &data, libssh::Client &) {
removeOut += data + "\n";
return ErrorCode::NoError;
};
ErrorCode errorCode = sshSession.runScript(
credentials, buildRemoveContainerScript(removeContainerVars, removeDataVolume), collectRemoveOut,
collectRemoveOut);
if (errorCode == ErrorCode::NoError && removeDataVolume && dataVolumeSurvivedRemoval(removeOut)) {
errorCode = ErrorCode::ServerDataVolumeNotRemoved;
}
if (errorCode == ErrorCode::NoError) {
QMap<DockerContainer, ContainerConfig> containers = adminConfig->containers;
@@ -1155,6 +1449,13 @@ bool InstallController::isUpdateDockerContainerRequired(DockerContainer containe
return false;
}
}
} else if (container == DockerContainer::Xray) {
const auto *oldXray = oldConfig.getXrayProtocolConfig();
const auto *newXray = newConfig.getXrayProtocolConfig();
if (oldXray && newXray && oldXray->serverConfig.hasEqualServerSettings(newXray->serverConfig)) {
return false;
}
return true;
} else if (container == DockerContainer::MtProxy) {
const auto *oldMt = oldConfig.getMtProxyProtocolConfig();
const auto *newMt = newConfig.getMtProxyProtocolConfig();
@@ -33,10 +33,10 @@ public:
QObject *parent = nullptr);
~InstallController();
ErrorCode setupContainer(const ServerCredentials &credentials, DockerContainer container, ContainerConfig &config, bool isUpdate = false);
ErrorCode setupContainer(const ServerCredentials &credentials, DockerContainer container, ContainerConfig &config, bool isUpdate = false, bool acceptXrayKeyLoss = false);
// Updates server-side container settings (admin self-hosted only): reconfigures the container over SSH.
ErrorCode updateServerConfig(const QString &serverId, DockerContainer container, const ContainerConfig &oldConfig, ContainerConfig &newConfig);
ErrorCode updateServerConfig(const QString &serverId, DockerContainer container, const ContainerConfig &oldConfig, ContainerConfig &newConfig, bool acceptXrayKeyLoss = false);
// Updates client-local settings only: rewrites the stored container config for any self-hosted/native server. No SSH.
ErrorCode updateClientConfig(const QString &serverId, DockerContainer container, ContainerConfig &newConfig);
@@ -106,6 +106,11 @@ private:
ErrorCode configureContainerWorker(const ServerCredentials &credentials, DockerContainer container, ContainerConfig &config, SshSession &sshSession);
ErrorCode startupContainerWorker(const ServerCredentials &credentials, DockerContainer container, const ContainerConfig &config, SshSession &sshSession);
ErrorCode readXrayStateBeforeVolumeMigration(const ServerCredentials &credentials, DockerContainer container,
SshSession &sshSession, QMap<QString, QString> &outFiles);
ErrorCode restoreXrayStateIntoDataVolume(const ServerCredentials &credentials, DockerContainer container,
SshSession &sshSession, const QMap<QString, QString> &files);
ErrorCode isServerPortBusy(const ServerCredentials &credentials, DockerContainer container, const ContainerConfig &config, SshSession &sshSession);
ErrorCode isUserInSudo(const ServerCredentials &credentials, SshSession &sshSession);
ErrorCode isServerDpkgBusy(const ServerCredentials &credentials, SshSession &sshSession);
@@ -130,7 +135,7 @@ private:
bool m_cancelInstallation = false;
QString m_tproxyInstallHostname;
QString m_tproxyInstallEmail;
#ifndef Q_OS_IOS
QList<QSharedPointer<QProcess>> m_sftpMountProcesses;
#endif
@@ -2,6 +2,7 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QDateTime>
#include "core/utils/containerEnum.h"
@@ -55,6 +56,33 @@ int UsersController::clientIndexById(const QString &clientId, const QJsonArray &
return -1;
}
QString UsersController::readXrayVolumeUuid(const DockerContainer container, const ServerCredentials &credentials,
SshSession *sshSession) const
{
ErrorCode error = ErrorCode::NoError;
const QString uuid = QString::fromUtf8(sshSession->getTextFileFromContainer(
container, credentials, QString::fromLatin1(amnezia::protocols::xray::uuidPath), error));
if (error != ErrorCode::NoError) {
return {};
}
return uuid.trimmed();
}
int UsersController::stripXrayVolumeUuidFromTable(QJsonArray &clientsTable, const QString &volumeUuid)
{
if (volumeUuid.isEmpty()) {
return 0;
}
int removed = 0;
for (int i = clientsTable.size() - 1; i >= 0; --i) {
if (clientsTable.at(i).toObject().value(configKey::clientId).toString() == volumeUuid) {
clientsTable.removeAt(i);
++removed;
}
}
return removed;
}
void UsersController::migration(const QByteArray &clientsTableString, QJsonArray &clientsTable)
{
QJsonObject clientsTableObj = QJsonDocument::fromJson(clientsTableString).object();
@@ -248,7 +276,8 @@ ErrorCode UsersController::getWireGuardClients(const DockerContainer container,
}
ErrorCode UsersController::getXrayClients(const DockerContainer container, const ServerCredentials& credentials,
SshSession* sshSession, int &count, QJsonArray &clientsTable)
SshSession* sshSession, int &count, QJsonArray &clientsTable,
const QString &volumeDefaultUuid)
{
ErrorCode error = ErrorCode::NoError;
@@ -283,6 +312,7 @@ ErrorCode UsersController::getXrayClients(const DockerContainer container, const
}
const QJsonArray clients = settings[protocols::xray::clients].toArray();
const QString xrayDefaultUuid = volumeDefaultUuid;
for (const auto &clientValue : clients) {
const QJsonObject clientObj = clientValue.toObject();
if (!clientObj.contains(protocols::xray::id)) {
@@ -290,9 +320,6 @@ ErrorCode UsersController::getXrayClients(const DockerContainer container, const
continue;
}
QString clientId = clientObj[protocols::xray::id].toString();
QString xrayDefaultUuid = sshSession->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::uuidPath, error);
xrayDefaultUuid.replace("\n", "");
if (!isClientExists(clientId, clientsTable) && clientId != xrayDefaultUuid) {
QJsonObject client;
@@ -339,6 +366,9 @@ ErrorCode UsersController::updateClients(const QString &serverId, const DockerCo
m_clientsTable = QJsonDocument::fromJson(clientsTableString).array();
const QString xrayVolumeUuid =
(container == DockerContainer::Xray) ? readXrayVolumeUuid(container, credentials, &sshSession) : QString();
if (m_clientsTable.isEmpty()) {
migration(clientsTableString, m_clientsTable);
@@ -349,7 +379,7 @@ ErrorCode UsersController::updateClients(const QString &serverId, const DockerCo
} else if (container == DockerContainer::WireGuard || ContainerUtils::isAwgContainer(container)) {
error = getWireGuardClients(container, credentials, &sshSession, count, m_clientsTable);
} else if (container == DockerContainer::Xray) {
error = getXrayClients(container, credentials, &sshSession, count, m_clientsTable);
error = getXrayClients(container, credentials, &sshSession, count, m_clientsTable, xrayVolumeUuid);
}
if (error != ErrorCode::NoError) {
emit clientsUpdated(QJsonArray());
@@ -365,6 +395,15 @@ ErrorCode UsersController::updateClients(const QString &serverId, const DockerCo
}
}
if (container == DockerContainer::Xray) {
const int stripped = stripXrayVolumeUuidFromTable(m_clientsTable, xrayVolumeUuid);
if (stripped > 0) {
const QByteArray strippedTable = QJsonDocument(m_clientsTable).toJson();
const ErrorCode writeError =
sshSession.uploadTextFileToContainer(container, credentials, strippedTable, clientsTableFile);
}
}
std::vector<WgShowData> data;
wgShow(container, credentials, &sshSession, data);
@@ -424,6 +463,13 @@ ErrorCode UsersController::appendClient(const QString &serverId, const QString &
return error;
}
if (container == DockerContainer::Xray) {
const QString volumeUuid = readXrayVolumeUuid(container, credentials, &sshSession);
if (!volumeUuid.isEmpty() && clientId == volumeUuid) {
return ErrorCode::NoError;
}
}
int existingIndex = clientIndexById(clientId, m_clientsTable);
if (existingIndex >= 0) {
return renameClient(serverId, existingIndex, clientName, container, true);
@@ -634,6 +680,19 @@ ErrorCode UsersController::revokeXray(const int row,
ErrorCode error = ErrorCode::NoError;
auto client = clientsTable.at(row).toObject();
QString clientId = client.value(configKey::clientId).toString();
const QString volumeUuid = readXrayVolumeUuid(container, credentials, sshSession);
if (!volumeUuid.isEmpty() && clientId == volumeUuid) {
clientsTable.removeAt(row);
const QByteArray clientsTableString = QJsonDocument(clientsTable).toJson();
const QString clientsTableFile = QString("/opt/amnezia/%1/clientsTable")
.arg(ContainerUtils::containerTypeToString(container));
error = sshSession->uploadTextFileToContainer(container, credentials, clientsTableString, clientsTableFile);
return ErrorCode::NoError;
}
const QString serverConfigPath = amnezia::protocols::xray::serverConfigPath;
const QString configString = sshSession->getTextFileFromContainer(container, credentials, serverConfigPath, error);
if (error != ErrorCode::NoError) {
@@ -647,9 +706,6 @@ ErrorCode UsersController::revokeXray(const int row,
return ErrorCode::InternalError;
}
auto client = clientsTable.at(row).toObject();
QString clientId = client.value(configKey::clientId).toString();
QJsonObject configObj = serverConfig.object();
if (!configObj.contains(protocols::xray::inbounds)) {
logger.error() << "Missing inbounds in xray config";
@@ -64,7 +64,12 @@ private:
ErrorCode getWireGuardClients(const DockerContainer container, const ServerCredentials &credentials,
SshSession* sshSession, int &count, QJsonArray &clientsTable);
ErrorCode getXrayClients(const DockerContainer container, const ServerCredentials& credentials,
SshSession* sshSession, int &count, QJsonArray &clientsTable);
SshSession* sshSession, int &count, QJsonArray &clientsTable,
const QString &volumeDefaultUuid);
QString readXrayVolumeUuid(const DockerContainer container, const ServerCredentials &credentials,
SshSession *sshSession) const;
static int stripXrayVolumeUuidFromTable(QJsonArray &clientsTable, const QString &volumeUuid);
ErrorCode wgShow(const DockerContainer container, const ServerCredentials &credentials,
SshSession* sshSession, std::vector<WgShowData> &data);
+13 -8
View File
@@ -8,8 +8,9 @@
#include "version.h"
#include "ui/utils/qAutoStart.h"
#include "logger.h"
#ifdef Q_OS_ANDROID
#include "platforms/android/android_controller.h"
#include "core/controllers/logsController.h"
#if defined(Q_OS_IOS) || defined(MACOS_NE)
#include "core/utils/swiftBridge.h"
#endif
QString getPlatformName()
@@ -86,6 +87,9 @@ void SettingsController::toggleLogging(bool enable)
qWarning() << "Initialization of debug subsystem failed";
}
}
#endif
#if defined(Q_OS_IOS) || defined(MACOS_NE)
SWIFT_BRIDGE_NAMESPACE::toggleLogging(enable);
#endif
Logger::setServiceLogsEnabled(enable);
@@ -96,12 +100,8 @@ void SettingsController::toggleLogging(bool enable)
void SettingsController::clearLogs()
{
#ifdef Q_OS_ANDROID
AndroidController::instance()->clearLogs();
#else
Logger::clearLogs(false);
Logger::clearServiceLogs();
#endif
LogsController::clear(LogsController::Stream::App);
LogsController::clear(LogsController::Stream::Tunnel);
}
QByteArray SettingsController::backupAppConfig() const
@@ -119,6 +119,11 @@ QByteArray SettingsController::backupAppConfig() const
return QJsonDocument(config).toJson();
}
int SettingsController::unsupportedFormatConfigsSkippedCount() const
{
return m_serversRepository->unsupportedFormatConfigsCount();
}
ErrorCode SettingsController::restoreAppConfigFromData(const QByteArray &data)
{
if (!m_appSettingsRepository->restoreAppConfig(data)) {
@@ -39,6 +39,7 @@ public:
QByteArray backupAppConfig() const;
ErrorCode restoreAppConfigFromData(const QByteArray &data);
int unsupportedFormatConfigsSkippedCount() const;
QString getAppVersion() const;
+6 -1
View File
@@ -13,6 +13,7 @@
#include "core/controllers/gatewayController.h"
#include "core/utils/api/gatewayPayloadBuilder.h"
#include "core/utils/appUiConfig.h"
#include "core/utils/constants/apiConstants.h"
#include "core/utils/constants/apiKeys.h"
#include "core/utils/selfhosted/scriptsRegistry.h"
@@ -129,6 +130,10 @@ void UpdateController::setUpdateCheckRunning(bool running)
void UpdateController::checkForUpdates()
{
#if !CLIENT_ENABLE_APP_UPDATES
return;
#endif
if (m_updateCheckRunning || !m_appSettingsRepository) {
return;
}
@@ -136,7 +141,7 @@ void UpdateController::checkForUpdates()
auto gatewayController = QSharedPointer<GatewayController>::create(m_appSettingsRepository->getGatewayEndpoint(),
m_appSettingsRepository->isDevGatewayEnv(),
7000,
apiDefs::requestTimeoutMsecs,
m_appSettingsRepository->isStrictKillSwitchEnabled(),
m_appSettingsRepository);
+1 -1
View File
@@ -87,8 +87,8 @@ ContainerConfig InstallerBase::createBaseConfig(DockerContainer container, int p
srv.flow = protocols::xray::defaultFlow;
srv.site = protocols::xray::defaultSite;
srv.sni = protocols::xray::defaultSni;
srv.fingerprint = protocols::xray::defaultFingerprint;
srv.alpn = protocols::xray::defaultAlpn;
srv.fingerprint = protocols::xray::defaultFingerprint;
config.protocolConfig = xrayConfig;
break;
}
+19 -286
View File
@@ -1,7 +1,6 @@
#include "xrayInstaller.h"
#include <QJsonDocument>
#include <QJsonArray>
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
@@ -11,6 +10,7 @@
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/selfhosted/sshSession.h"
#include "core/configurators/xrayConfigurator.h"
#include "core/models/protocols/xrayProtocolConfig.h"
#include "logger.h"
@@ -19,37 +19,21 @@ namespace
Logger logger("XrayInstaller");
// Xray expects uTLS preset names (chrome, firefox, …). Old Amnezia/server templates used "Mozilla/5.0".
QString normalizeXrayFingerprint(const QString &fp)
QString describeServerJsonStatus(amnezia::XrayServerJsonStatus status)
{
if (fp.isEmpty() || fp.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) {
return QString::fromLatin1(protocols::xray::defaultFingerprint);
}
return fp;
}
// Parse an xray int range: "from-to" string, plain int, or legacy {from,to} object.
void parseIntRange(const QJsonValue &v, QString &minOut, QString &maxOut)
{
if (v.isString()) {
const QString s = v.toString().trimmed();
const int dash = s.indexOf(QLatin1Char('-'), 1);
if (dash > 0) {
minOut = s.left(dash).trimmed();
maxOut = s.mid(dash + 1).trimmed();
} else if (!s.isEmpty()) {
minOut = s;
maxOut = s;
}
} else if (v.isDouble()) {
minOut = QString::number(v.toInt());
maxOut = minOut;
} else if (v.isObject()) {
const QJsonObject o = v.toObject();
if (o.contains(QLatin1String("from")) || o.contains(QLatin1String("to"))) {
minOut = QString::number(o.value(QLatin1String("from")).toInt());
maxOut = QString::number(o.value(QLatin1String("to")).toInt());
}
switch (status) {
case amnezia::XrayServerJsonStatus::MissingInbounds:
return QStringLiteral("server config missing 'inbounds' field");
case amnezia::XrayServerJsonStatus::EmptyInbounds:
return QStringLiteral("server config has empty 'inbounds' array");
case amnezia::XrayServerJsonStatus::MissingStreamSettings:
return QStringLiteral("inbound missing 'streamSettings' field");
case amnezia::XrayServerJsonStatus::MissingSettings:
return QStringLiteral("inbound missing 'settings' field");
case amnezia::XrayServerJsonStatus::Ok:
break;
}
return QStringLiteral("ok");
}
}
@@ -65,7 +49,7 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c
SshSession* sshSession, ContainerConfig &config)
{
ErrorCode errorCode = ErrorCode::NoError;
QString currentConfig = sshSession->getTextFileFromContainer(
container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode);
@@ -78,267 +62,16 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c
logger.error() << "Failed to parse server config JSON";
return ErrorCode::InternalError;
}
QJsonObject serverConfig = doc.object();
if (!serverConfig.contains(protocols::xray::inbounds)) {
logger.error() << "Server config missing 'inbounds' field";
return ErrorCode::InternalError;
}
QJsonArray inbounds = serverConfig[protocols::xray::inbounds].toArray();
if (inbounds.isEmpty()) {
logger.error() << "Server config has empty 'inbounds' array";
return ErrorCode::InternalError;
}
QJsonObject inbound = inbounds[0].toObject();
if (!inbound.contains(protocols::xray::streamSettings)) {
logger.error() << "Inbound missing 'streamSettings' field";
return ErrorCode::InternalError;
}
QJsonObject streamSettings = inbound[protocols::xray::streamSettings].toObject();
auto *xrayConfig = config.getXrayProtocolConfig();
if (!xrayConfig) {
logger.error() << "No XrayProtocolConfig in ContainerConfig";
return ErrorCode::InternalError;
}
XrayServerConfig &srv = xrayConfig->serverConfig;
// ── Port ─────────────────────────────────────────────────────────
if (inbound.contains(protocols::xray::port)) {
srv.port = QString::number(inbound[protocols::xray::port].toInt());
}
// ── Network (transport) ───────────────────────────────────────────
QString networkVal = streamSettings.value(protocols::xray::network).toString("tcp");
if (networkVal == "xhttp") {
srv.transport = "xhttp";
} else if (networkVal == "kcp") {
srv.transport = "mkcp";
} else {
srv.transport = "raw";
}
// ── Security ──────────────────────────────────────────────────────
srv.security = streamSettings.value(protocols::xray::security).toString("reality");
// ── Reality settings ──────────────────────────────────────────────
if (srv.security == "reality") {
QJsonObject rs = streamSettings.value(protocols::xray::realitySettings).toObject();
// serverNames array → site + sni
if (rs.contains(protocols::xray::serverNames)) {
QString sniVal = rs[protocols::xray::serverNames].toArray().first().toString();
srv.sni = sniVal;
srv.site = sniVal;
} else if (rs.contains(protocols::xray::serverName)) {
srv.sni = rs[protocols::xray::serverName].toString();
srv.site = srv.sni;
}
srv.fingerprint = normalizeXrayFingerprint(rs.value(protocols::xray::fingerprint).toString());
}
// ── TLS settings ──────────────────────────────────────────────────
if (srv.security == "tls") {
QJsonObject tls = streamSettings.value("tlsSettings").toObject();
srv.sni = tls.value(protocols::xray::serverName).toString();
srv.fingerprint = normalizeXrayFingerprint(tls.value(protocols::xray::fingerprint).toString());
QJsonArray alpnArr = tls.value("alpn").toArray();
QStringList alpnList;
for (const QJsonValue &v : alpnArr) {
QString t = v.toString().trimmed();
if (t.compare(QLatin1String("HTTP/2"), Qt::CaseInsensitive) == 0)
t = QStringLiteral("h2");
else if (t.compare(QLatin1String("HTTP/1.1"), Qt::CaseInsensitive) == 0)
t = QStringLiteral("http/1.1");
if (!t.isEmpty())
alpnList << t;
}
srv.alpn = alpnList.join(",");
}
// ── Flow (from users array) ───────────────────────────────────────
if (inbound.contains(protocols::xray::settings)) {
QJsonObject s = inbound[protocols::xray::settings].toObject();
QJsonArray clientsArr = s.value(protocols::xray::clients).toArray();
if (!clientsArr.isEmpty()) {
srv.flow = clientsArr[0].toObject().value(protocols::xray::flow).toString();
}
}
// ── XHTTP settings (Xray-core SplitHTTPConfig + legacy Amnezia keys) ──
if (srv.transport == "xhttp") {
QJsonObject xhttpObj = streamSettings.value("xhttpSettings").toObject();
{
const QString m = xhttpObj.value("mode").toString();
if (m.isEmpty() || m == QLatin1String("auto"))
srv.xhttp.mode = QStringLiteral("Auto");
else if (m == QLatin1String("packet-up"))
srv.xhttp.mode = QStringLiteral("Packet-up");
else if (m == QLatin1String("stream-up"))
srv.xhttp.mode = QStringLiteral("Stream-up");
else if (m == QLatin1String("stream-one"))
srv.xhttp.mode = QStringLiteral("Stream-one");
else
srv.xhttp.mode = m;
}
srv.xhttp.host = xhttpObj.value("host").toString();
srv.xhttp.path = xhttpObj.value("path").toString();
if (xhttpObj.contains(QLatin1String("uplinkHTTPMethod")))
srv.xhttp.uplinkMethod = xhttpObj.value("uplinkHTTPMethod").toString();
else
srv.xhttp.uplinkMethod = xhttpObj.value("method").toString();
srv.xhttp.disableGrpc = xhttpObj.value("noGRPCHeader").toBool(true);
srv.xhttp.disableSse = xhttpObj.value("noSSEHeader").toBool(true);
auto sessionSeqUi = [](const QString &core) -> QString {
if (core.isEmpty() || core == QLatin1String("path"))
return QStringLiteral("Path");
if (core == QLatin1String("cookie"))
return QStringLiteral("Cookie");
if (core == QLatin1String("header"))
return QStringLiteral("Header");
if (core == QLatin1String("query"))
return QStringLiteral("Query");
return core;
};
QString sess = xhttpObj.value("sessionIDPlacement").toString();
if (sess.isEmpty())
sess = xhttpObj.value("sessionPlacement").toString();
if (sess.isEmpty())
sess = xhttpObj.value("scSessionPlacement").toString();
srv.xhttp.sessionPlacement = sessionSeqUi(sess);
QString seq = xhttpObj.value("seqPlacement").toString();
if (seq.isEmpty())
seq = xhttpObj.value("scSeqPlacement").toString();
srv.xhttp.seqPlacement = sessionSeqUi(seq);
auto uplinkDataUi = [](const QString &core) -> QString {
if (core.isEmpty() || core == QLatin1String("body"))
return QStringLiteral("Body");
if (core == QLatin1String("auto"))
return QStringLiteral("Auto");
if (core == QLatin1String("header"))
return QStringLiteral("Header");
if (core == QLatin1String("cookie"))
return QStringLiteral("Cookie");
return core;
};
QString udata = xhttpObj.value("uplinkDataPlacement").toString();
if (udata.isEmpty())
udata = xhttpObj.value("scUplinkDataPlacement").toString();
srv.xhttp.uplinkDataPlacement = uplinkDataUi(udata);
srv.xhttp.sessionKey = xhttpObj.value("sessionIDKey").toString();
if (srv.xhttp.sessionKey.isEmpty())
srv.xhttp.sessionKey = xhttpObj.value("sessionKey").toString();
srv.xhttp.seqKey = xhttpObj.value("seqKey").toString();
srv.xhttp.uplinkDataKey = xhttpObj.value("uplinkDataKey").toString();
if (xhttpObj.contains(QLatin1String("uplinkChunkSize"))) {
QString ucMin, ucMax;
parseIntRange(xhttpObj.value("uplinkChunkSize"), ucMin, ucMax);
if (!ucMin.isEmpty())
srv.xhttp.uplinkChunkSize = ucMin;
} else if (xhttpObj.contains(QLatin1String("xhttpUplinkChunkSize"))) {
srv.xhttp.uplinkChunkSize = QString::number(xhttpObj.value("xhttpUplinkChunkSize").toInt());
}
if (xhttpObj.contains(QLatin1String("scMaxBufferedPosts"))) {
srv.xhttp.scMaxBufferedPosts = QString::number(xhttpObj.value("scMaxBufferedPosts").toVariant().toLongLong());
}
auto readRange = [&](const char *key, QString &minOut, QString &maxOut) {
parseIntRange(xhttpObj.value(QLatin1String(key)), minOut, maxOut);
};
readRange("scMaxEachPostBytes", srv.xhttp.scMaxEachPostBytesMin, srv.xhttp.scMaxEachPostBytesMax);
readRange("scMinPostsIntervalMs", srv.xhttp.scMinPostsIntervalMsMin, srv.xhttp.scMinPostsIntervalMsMax);
readRange("scStreamUpServerSecs", srv.xhttp.scStreamUpServerSecsMin, srv.xhttp.scStreamUpServerSecsMax);
auto loadPaddingFromObject = [&](const QJsonObject &pad) {
if (pad.contains(QLatin1String("xPaddingObfsMode")))
srv.xhttp.xPadding.obfsMode = pad.value("xPaddingObfsMode").toBool(true);
srv.xhttp.xPadding.key = pad.value("xPaddingKey").toString();
srv.xhttp.xPadding.header = pad.value("xPaddingHeader").toString();
srv.xhttp.xPadding.placement = pad.value("xPaddingPlacement").toString();
srv.xhttp.xPadding.method = pad.value("xPaddingMethod").toString();
QString bytesMin, bytesMax;
parseIntRange(pad.value("xPaddingBytes"), bytesMin, bytesMax);
if (!bytesMin.isEmpty()) {
srv.xhttp.xPadding.bytesMin = bytesMin;
srv.xhttp.xPadding.bytesMax = bytesMax;
}
QString pl = srv.xhttp.xPadding.placement.toLower();
if (pl == QLatin1String("cookie"))
srv.xhttp.xPadding.placement = QStringLiteral("Cookie");
else if (pl == QLatin1String("header"))
srv.xhttp.xPadding.placement = QStringLiteral("Header");
else if (pl == QLatin1String("query"))
srv.xhttp.xPadding.placement = QStringLiteral("Query");
else if (pl == QLatin1String("queryinheader"))
srv.xhttp.xPadding.placement = QStringLiteral("Query in header");
QString met = srv.xhttp.xPadding.method.toLower();
if (met == QLatin1String("repeat-x"))
srv.xhttp.xPadding.method = QStringLiteral("Repeat-x");
else if (met == QLatin1String("tokenish"))
srv.xhttp.xPadding.method = QStringLiteral("Tokenish");
};
if (xhttpObj.contains(QLatin1String("xPaddingObfsMode")) || xhttpObj.contains(QLatin1String("xPaddingKey"))
|| xhttpObj.contains(QLatin1String("xPaddingBytes"))) {
loadPaddingFromObject(xhttpObj);
} else if (xhttpObj.contains(QLatin1String("xPadding")) && xhttpObj.value("xPadding").isObject()) {
const QJsonObject nested = xhttpObj.value("xPadding").toObject();
if (!nested.isEmpty()) {
loadPaddingFromObject(nested);
if (!nested.contains(QLatin1String("xPaddingObfsMode")))
srv.xhttp.xPadding.obfsMode = true;
}
}
if (xhttpObj.contains(QLatin1String("xmux"))) {
QJsonObject mux = xhttpObj.value("xmux").toObject();
srv.xhttp.xmux.enabled = true;
auto readMuxRange = [&](const char *key, QString &minOut, QString &maxOut) {
parseIntRange(mux.value(QLatin1String(key)), minOut, maxOut);
};
readMuxRange("maxConcurrency", srv.xhttp.xmux.maxConcurrencyMin, srv.xhttp.xmux.maxConcurrencyMax);
readMuxRange("maxConnections", srv.xhttp.xmux.maxConnectionsMin, srv.xhttp.xmux.maxConnectionsMax);
readMuxRange("cMaxReuseTimes", srv.xhttp.xmux.cMaxReuseTimesMin, srv.xhttp.xmux.cMaxReuseTimesMax);
readMuxRange("hMaxRequestTimes", srv.xhttp.xmux.hMaxRequestTimesMin, srv.xhttp.xmux.hMaxRequestTimesMax);
readMuxRange("hMaxReusableSecs", srv.xhttp.xmux.hMaxReusableSecsMin, srv.xhttp.xmux.hMaxReusableSecsMax);
if (mux.contains(QLatin1String("hKeepAlivePeriod")))
srv.xhttp.xmux.hKeepAlivePeriod = QString::number(mux.value("hKeepAlivePeriod").toVariant().toLongLong());
}
}
// ── mKCP settings ─────────────────────────────────────────────────
if (srv.transport == "mkcp") {
QJsonObject kcp = streamSettings.value("kcpSettings").toObject();
if (kcp.contains("tti")) {
srv.mkcp.tti = QString::number(kcp["tti"].toInt());
}
if (kcp.contains("uplinkCapacity")) {
srv.mkcp.uplinkCapacity = QString::number(kcp["uplinkCapacity"].toInt());
}
if (kcp.contains("downlinkCapacity")) {
srv.mkcp.downlinkCapacity = QString::number(kcp["downlinkCapacity"].toInt());
}
if (kcp.contains("readBufferSize")) {
srv.mkcp.readBufferSize = QString::number(kcp["readBufferSize"].toInt());
}
if (kcp.contains("writeBufferSize")) {
srv.mkcp.writeBufferSize = QString::number(kcp["writeBufferSize"].toInt());
}
srv.mkcp.congestion = kcp.value("congestion").toBool(true);
const amnezia::XrayServerJsonStatus status =
XrayServerConfig::fromServerInboundJson(doc.object(), xrayConfig->serverConfig);
if (status != amnezia::XrayServerJsonStatus::Ok) {
return ErrorCode::InternalError;
}
return ErrorCode::NoError;
@@ -85,6 +85,7 @@ QPair<QString, QString> ApiV2ServerConfig::getDnsPair(const QString &primaryDns,
QJsonObject ApiV2ServerConfig::toJson() const
{
QJsonObject obj;
obj[configKey::formatVersion] = serverConfigUtils::currentConfigFormatVersion;
if (!name.isEmpty()) {
obj[configKey::name] = name;
File diff suppressed because it is too large. Load diff
+118 -21
View File
@@ -1,9 +1,11 @@
#ifndef XRAYPROTOCOLCONFIG_H
#define XRAYPROTOCOLCONFIG_H
#include <QJsonArray>
#include <QJsonObject>
#include "core/utils/constants/protocolConstants.h"
#include <QString>
#include <QStringList>
#include <optional>
namespace amnezia
@@ -16,8 +18,8 @@ struct XrayXPaddingConfig {
bool obfsMode = true; // xPaddingObfsMode
QString key; // xPaddingKey
QString header; // xPaddingHeader
QString placement = protocols::xray::defaultXPaddingPlacement; // xPaddingPlacement: Cookie|Header|Query|Body
QString method = protocols::xray::defaultXPaddingMethod; // xPaddingMethod: Repeat-x|Random|Zero
QString placement; // xPaddingPlacement: Cookie|Header|Query|Query in header
QString method; // xPaddingMethod: Repeat-x|Tokenish
QJsonObject toJson() const;
static XrayXPaddingConfig fromJson(const QJsonObject &json);
@@ -45,33 +47,35 @@ struct XrayXmuxConfig {
// ── XHTTP transport ───────────────────────────────────────────────────────────
struct XrayXhttpConfig {
QString mode = protocols::xray::defaultXhttpMode; // Auto|Packet-up|Stream-up|Stream-one
QString host = protocols::xray::defaultXhttpHost;
QString mode;
QString host;
QString path;
QString uplinkMethod = protocols::xray::defaultXhttpUplinkMethod; // POST|PUT|PATCH
bool disableGrpc = true;
bool disableSse = true;
// Session & Sequence
QString sessionPlacement = protocols::xray::defaultXhttpSessionPlacement;
QString sessionKey = protocols::xray::defaultXhttpSessionKey;
QString seqPlacement = protocols::xray::defaultXhttpSeqPlacement;
QString sessionPlacement;
QString sessionKey;
QString seqPlacement;
QString seqKey;
QString uplinkDataPlacement = protocols::xray::defaultXhttpUplinkDataPlacement;
QString uplinkDataPlacement;
QString uplinkDataKey;
// Traffic Shaping
QString uplinkChunkSize = protocols::xray::defaultXhttpUplinkChunkSize;
QString uplinkMethod;
QString uplinkChunkSize;
QString scMinPostsIntervalMsMin;
QString scMinPostsIntervalMsMax;
XrayXmuxConfig xmux;
QString scMaxBufferedPosts;
QString scMaxEachPostBytesMin = protocols::xray::defaultXhttpScMaxEachPostBytesMin;
QString scMaxEachPostBytesMax = protocols::xray::defaultXhttpScMaxEachPostBytesMax;
QString scMinPostsIntervalMsMin = protocols::xray::defaultXhttpScMinPostsIntervalMsMin;
QString scMinPostsIntervalMsMax = protocols::xray::defaultXhttpScMinPostsIntervalMsMax;
QString scStreamUpServerSecsMin = protocols::xray::defaultXhttpScStreamUpServerSecsMin;
QString scStreamUpServerSecsMax = protocols::xray::defaultXhttpScStreamUpServerSecsMax;
QString scMaxEachPostBytesMin;
QString scMaxEachPostBytesMax;
QString scStreamUpServerSecsMin;
QString scStreamUpServerSecsMax;
XrayXPaddingConfig xPadding;
XrayXmuxConfig xmux;
QJsonObject toJson() const;
/// Reads only keys present in JSON (no Amnezia UI defaults). Use XrayConfigModel::applyDefaultsToServerConfig for UI.
@@ -81,16 +85,40 @@ struct XrayXhttpConfig {
// ── mKCP transport ────────────────────────────────────────────────────────────
struct XrayMkcpConfig {
QString tti;
QString mtu;
QString uplinkCapacity;
QString downlinkCapacity;
QString readBufferSize;
QString writeBufferSize;
bool congestion = true;
QString cwndMultiplier;
QString maxSendingWindow;
QJsonObject toJson() const;
static XrayMkcpConfig fromJson(const QJsonObject &json);
};
enum class XrayStreamSide {
Server,
Client,
};
struct XrayServerInboundInputs {
QJsonArray clients;
QString realityPrivateKey;
QString realityShortId;
};
enum class XrayServerJsonStatus {
Ok,
MissingInbounds,
EmptyInbounds,
MissingStreamSettings,
MissingSettings,
};
enum class XrayClientListFilter {
KeepAll,
DropWithoutId,
};
// ── Server config (settings editable by user) ─────────────────────────────────
struct XrayServerConfig {
QString port;
@@ -99,11 +127,13 @@ struct XrayServerConfig {
QString site;
bool isThirdPartyConfig = false;
int settingsVersion = 0;
QString security;
QString flow;
QString fingerprint;
QString sni;
QString alpn;
QString fingerprint = protocols::xray::defaultFingerprint;
QString transport;
XrayXhttpConfig xhttp;
@@ -113,7 +143,64 @@ struct XrayServerConfig {
static XrayServerConfig fromJson(const QJsonObject &json);
void applyDefaults(bool fillFlowDefault = false);
QJsonObject streamSettingsJson(XrayStreamSide side) const;
QJsonObject serverStreamSettings() const;
QJsonObject clientStreamSettings() const;
QJsonObject toServerInboundJson(const XrayServerInboundInputs &inputs) const;
static XrayServerJsonStatus fromServerInboundJson(const QJsonObject &serverJson, XrayServerConfig &outServerConfig);
static QJsonArray clientsFromServerInboundJson(const QJsonObject &serverJson);
static XrayServerJsonStatus setClientsInServerInboundJson(QJsonObject &serverJson, const QJsonArray &clients);
static QJsonObject makeClientEntry(const QString &clientId, const QString &flowValue);
static QJsonObject applyFlowToClient(const QJsonObject &client, const QString &flowValue);
static int indexOfClient(const QJsonArray &clients, const QString &clientId);
static QString firstClientId(const QJsonArray &clients);
static QJsonArray applyFlowToClients(const QJsonArray &clients, const QString &flowValue,
XrayClientListFilter filter = XrayClientListFilter::KeepAll);
QJsonObject serverView() const;
QJsonObject issuedConfigView() const;
bool hasEqualServerSettings(const XrayServerConfig &other) const;
bool breaksIssuedConfigs(const XrayServerConfig &other) const;
};
namespace xrayEffective
{
QString xhttpMode(const QString &mode);
QString sessionSeqPlacement(const QString &placement);
QString uplinkDataPlacement(const QString &placement);
QString xPaddingPlacement(const QString &placement);
QString xPaddingMethod(const QString &method);
QString range(const QString &minV, const QString &maxV);
void putRangeIfAny(QJsonObject &obj, const char *key, QString minV, QString maxV, const char *fallbackMin,
const char *fallbackMax);
QString security(const XrayServerConfig &srv);
QString clientFlow(const XrayServerConfig &srv);
QString network(const XrayServerConfig &srv);
QString xhttpModeSent(const XrayServerConfig &srv);
}
QString sanitizeNativeConfig(const QString &nativeConfig);
struct XrayClientOutboundInputs {
QString serverAddress;
QString clientId;
QString realityPublicKey;
QString realityShortId;
QString tlsPinnedPeerCertSha256;
static std::optional<XrayClientOutboundInputs> fromClientNativeJson(const QJsonObject &nativeJson);
};
// ── Client config (generated, not edited by user) ─────────────────────────────
@@ -124,6 +211,9 @@ struct XrayClientConfig {
QJsonObject toJson() const;
static XrayClientConfig fromJson(const QJsonObject &json);
static QString idFromNativeJson(const QJsonObject &nativeJson);
static QString localPortFromNativeJson(const QJsonObject &nativeJson);
};
// ── Top-level protocol config ──────────────────────────────────────────────────
@@ -140,7 +230,14 @@ struct XrayProtocolConfig {
bool needsClientHydration = false;
QJsonObject toClientOutboundJson(const XrayClientOutboundInputs &inputs) const;
bool fromClientOutboundJson(const QJsonObject &nativeJson);
bool hydrateServerConfigFromClientNative();
bool regenerateClientConfigFromNative();
};
} // namespace amnezia
@@ -8,6 +8,7 @@
#include "core/utils/protocolEnum.h"
#include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/serverConfigUtils.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/networkUtilities.h"
@@ -51,6 +52,7 @@ QPair<QString, QString> NativeServerConfig::getDnsPair(const QString &primaryDns
QJsonObject NativeServerConfig::toJson() const
{
QJsonObject obj;
obj[configKey::formatVersion] = serverConfigUtils::currentConfigFormatVersion;
if (!description.isEmpty()) {
obj[configKey::description] = this->description;
@@ -9,6 +9,7 @@
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
#include "core/utils/networkUtilities.h"
#include "core/utils/serverConfigUtils.h"
namespace amnezia
{
@@ -83,6 +84,7 @@ QPair<QString, QString> SelfHostedAdminServerConfig::getDnsPair(bool isAmneziaDn
QJsonObject SelfHostedAdminServerConfig::toJson() const
{
QJsonObject obj;
obj[configKey::formatVersion] = serverConfigUtils::currentConfigFormatVersion;
if (!description.isEmpty()) {
obj[configKey::description] = this->description;
@@ -4,6 +4,7 @@
#include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/serverConfigUtils.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
@@ -66,6 +67,7 @@ QPair<QString, QString> SelfHostedUserServerConfig::getDnsPair(const QString &pr
QJsonObject SelfHostedUserServerConfig::toJson() const
{
QJsonObject obj;
obj[configKey::formatVersion] = serverConfigUtils::currentConfigFormatVersion;
if (!description.isEmpty()) {
obj[configKey::description] = this->description;
@@ -61,6 +61,10 @@ void OpenVpnProtocol::cleanupResources()
void OpenVpnProtocol::stop()
{
if (guardStop()) {
return;
}
qDebug() << "OpenVpnProtocol::stop()";
const bool wasActive = m_connectionState == Vpn::ConnectionState::Preparing
@@ -179,6 +183,8 @@ void OpenVpnProtocol::updateRouteGateway(QString line)
ErrorCode OpenVpnProtocol::start()
{
resetStopGuard();
cleanupResources();
if (!QFileInfo::exists(configPath())) {
+14
View File
@@ -27,6 +27,20 @@ VpnProtocol::VpnProtocol(const QJsonObject &configuration, QObject *parent)
connect(m_timeoutTimer, &QTimer::timeout, this, &VpnProtocol::onTimeout);
}
bool VpnProtocol::guardStop()
{
if (m_stopped) {
return true;
}
m_stopped = true;
return false;
}
void VpnProtocol::resetStopGuard()
{
m_stopped = false;
}
void VpnProtocol::setLastError(ErrorCode lastError)
{
m_lastError = lastError;
+5
View File
@@ -89,6 +89,11 @@ protected:
void startTimeoutTimer();
void stopTimeoutTimer();
bool guardStop();
void resetStopGuard();
bool m_stopped = false;
Vpn::ConnectionState m_connectionState;
QString m_routeGateway;
+4 -1
View File
@@ -50,8 +50,10 @@ WireguardProtocol::~WireguardProtocol()
void WireguardProtocol::stop()
{
if (guardStop()) {
return;
}
stopMzImpl();
return;
}
ErrorCode WireguardProtocol::startMzImpl()
@@ -75,5 +77,6 @@ ErrorCode WireguardProtocol::stopMzImpl()
ErrorCode WireguardProtocol::start()
{
resetStopGuard();
return startMzImpl();
}
+14 -20
View File
@@ -1,5 +1,6 @@
#include "xrayProtocol.h"
#include "core/models/protocols/xrayProtocolConfig.h"
#include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
@@ -66,6 +67,8 @@ XrayProtocol::~XrayProtocol()
ErrorCode XrayProtocol::start()
{
resetStopGuard();
qDebug() << "XrayProtocol::start()";
// Inject SOCKS5 auth into the inbound before starting xray.
@@ -87,30 +90,16 @@ ErrorCode XrayProtocol::start()
return ErrorCode::XrayExecutableCrashed;
}
// Fix fingerprint: old configs may contain "Mozilla/5.0" which xray-core rejects.
// Replace with the correct default at runtime so stale stored configs still work.
if (xrayConfigStr.contains("Mozilla/5.0", Qt::CaseInsensitive)) {
xrayConfigStr.replace("Mozilla/5.0", amnezia::protocols::xray::defaultFingerprint,
Qt::CaseInsensitive);
qDebug() << "XrayProtocol: patched legacy fingerprint to"
<< amnezia::protocols::xray::defaultFingerprint;
}
// Fix inbound listen address: old configs may use "10.33.0.2" which doesn't exist
// until TUN is created. xray must listen on 127.0.0.1 so tun2socks can connect.
if (xrayConfigStr.contains(amnezia::protocols::xray::defaultLocalAddr)) {
xrayConfigStr.replace(amnezia::protocols::xray::defaultLocalAddr,
amnezia::protocols::xray::defaultLocalListenAddr);
qDebug() << "XrayProtocol: patched legacy inbound listen address to 127.0.0.1";
}
xrayConfigStr = amnezia::sanitizeNativeConfig(xrayConfigStr);
return IpcClient::withInterface(
[&](QSharedPointer<IpcInterfaceReplica> iface) {
auto xrayStart = iface->xrayStart(xrayConfigStr);
if (!xrayStart.waitForFinished() || !xrayStart.returnValue()) {
auto xrayStart = iface->xrayStartOwned(xrayConfigStr);
if (!xrayStart.waitForFinished() || xrayStart.returnValue() == 0) {
qCritical() << "Failed to start xray";
return ErrorCode::XrayExecutableCrashed;
}
m_xrayToken = xrayStart.returnValue();
return startTun2Socks();
},
[]() { return ErrorCode::AmneziaServiceConnectionFailed; });
@@ -118,9 +107,13 @@ ErrorCode XrayProtocol::start()
void XrayProtocol::stop()
{
if (guardStop()) {
return;
}
qDebug() << "XrayProtocol::stop()";
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
IpcClient::withInterface([this](QSharedPointer<IpcInterfaceReplica> iface) {
auto disableKillSwitch = iface->disableKillSwitch();
if (!disableKillSwitch.waitForFinished() || !disableKillSwitch.returnValue())
qWarning() << "Failed to disable killswitch";
@@ -137,9 +130,10 @@ void XrayProtocol::stop()
if (!deleteTun.waitForFinished() || !deleteTun.returnValue())
qWarning() << "Failed to delete tun";
auto xrayStop = iface->xrayStop();
auto xrayStop = iface->xrayStopOwned(m_xrayToken);
if (!xrayStop.waitForFinished() || !xrayStop.returnValue())
qWarning() << "Failed to stop xray";
m_xrayToken = 0;
});
if (m_tun2socksProcess) {
+4
View File
@@ -38,6 +38,10 @@ private:
int m_tun2socksRetryCount = 0;
static constexpr int maxTun2SocksRetries = 5;
static constexpr int tun2socksRetryDelayMs = 400;
// Owner token of the shared xray engine; a late stop with a stale token
// won't kill an engine restarted by another consumer (e.g. the local proxy).
qint64 m_xrayToken = 0;
};
#endif // XRAYPROTOCOL_H
@@ -375,6 +375,26 @@ void SecureAppSettingsRepository::setScreenshotsEnabled(bool enabled)
emit screenshotsEnabledChanged(enabled);
}
int SecureAppSettingsRepository::countryListSortMode(const QString &listId) const
{
return value("Conf/countryListSortMode/" + listId, 0).toInt();
}
void SecureAppSettingsRepository::setCountryListSortMode(const QString &listId, int mode)
{
setValue("Conf/countryListSortMode/" + listId, mode);
}
QStringList SecureAppSettingsRepository::favoriteLocations() const
{
return value("Conf/favoriteLocations").toStringList();
}
void SecureAppSettingsRepository::setFavoriteLocations(const QStringList &codes)
{
setValue("Conf/favoriteLocations", codes);
}
bool SecureAppSettingsRepository::isNewsNotifications() const
{
return value("Conf/newsNotifications", true).toBool();
@@ -484,3 +504,33 @@ void SecureAppSettingsRepository::setXraySavedConfigs(const QByteArray &data)
{
setValue("Xray/savedConfigs", data);
}
QString SecureAppSettingsRepository::localProxyOwnerId() const
{
return value("Conf/localProxyOwnerId", "").toString();
}
void SecureAppSettingsRepository::setLocalProxyOwnerId(const QString &serverId)
{
setValue("Conf/localProxyOwnerId", serverId);
}
quint16 SecureAppSettingsRepository::localProxyPort() const
{
return static_cast<quint16>(value("Conf/localProxyPort", 10808).toUInt());
}
void SecureAppSettingsRepository::setLocalProxyPort(quint16 port)
{
setValue("Conf/localProxyPort", port);
}
bool SecureAppSettingsRepository::isLocalProxyPortUserDefined() const
{
return value("Conf/localProxyPortUserDefined", false).toBool();
}
void SecureAppSettingsRepository::setLocalProxyPortUserDefined(bool userDefined)
{
setValue("Conf/localProxyPortUserDefined", userDefined);
}
@@ -77,6 +77,11 @@ public:
void setStartMinimized(bool enabled);
bool isScreenshotsEnabled() const;
void setScreenshotsEnabled(bool enabled);
int countryListSortMode(const QString &listId) const;
void setCountryListSortMode(const QString &listId, int mode);
QStringList favoriteLocations() const;
void setFavoriteLocations(const QStringList &codes);
bool isNewsNotifications() const;
void setNewsNotifications(bool enabled);
@@ -100,6 +105,13 @@ public:
QByteArray xraySavedConfigs() const;
void setXraySavedConfigs(const QByteArray &data);
QString localProxyOwnerId() const;
void setLocalProxyOwnerId(const QString &serverId);
quint16 localProxyPort() const;
void setLocalProxyPort(quint16 port);
bool isLocalProxyPortUserDefined() const;
void setLocalProxyPortUserDefined(bool userDefined);
signals:
void appLanguageChanged(QLocale locale);
void allowedDnsServersChanged(const QStringList &servers);
@@ -126,4 +138,3 @@ private:
};
#endif // SECUREAPPSETTINGSREPOSITORY_H
@@ -5,6 +5,7 @@
#include <QJsonValue>
#include <QSet>
#include <QUuid>
#include <QDebug>
#include "core/utils/serverConfigUtils.h"
#include "core/utils/constants/apiKeys.h"
@@ -142,6 +143,7 @@ void SecureServersRepository::persistDefaultServerFields()
void SecureServersRepository::loadFromStorage()
{
clearServerStateMaps();
m_unsupportedFormatConfigsCount = 0;
const QJsonArray serversArray =
QJsonDocument::fromJson(value(QStringLiteral("Servers/serversList"), QByteArray()).toByteArray())
@@ -152,6 +154,12 @@ void SecureServersRepository::loadFromStorage()
const QString candidateId = readStorageServerId(json);
const QString serverId = normalizedOrGeneratedServerId(candidateId);
const QJsonObject strippedJson = withoutStorageServerId(json);
if (!serverConfigUtils::isConfigFormatVersionSupported(strippedJson)) {
qWarning() << "Skipping stored server config with unsupported format version"
<< serverConfigUtils::configFormatVersion(strippedJson);
++m_unsupportedFormatConfigsCount;
continue;
}
const serverConfigUtils::ConfigType kind = serverConfigUtils::configTypeFromJson(strippedJson);
if (m_serverJsonById.contains(serverId) || kind == serverConfigUtils::ConfigType::Invalid) {
@@ -184,6 +192,11 @@ void SecureServersRepository::invalidateCache()
loadFromStorage();
}
int SecureServersRepository::unsupportedFormatConfigsCount() const
{
return m_unsupportedFormatConfigsCount;
}
void SecureServersRepository::clearServers()
{
clearServerStateMaps();
@@ -222,7 +235,8 @@ QString SecureServersRepository::addServer(const QString &serverId, const QJsonO
return id;
}
const QJsonObject strippedJson = withoutStorageServerId(serverJson);
if (serverConfigUtils::configTypeFromJson(strippedJson) != kind) {
if (!serverConfigUtils::isConfigFormatVersionSupported(strippedJson)
|| serverConfigUtils::configTypeFromJson(strippedJson) != kind) {
return id;
}
m_serverJsonById.insert(id, embedStorageServerId(id, strippedJson));
@@ -309,6 +323,15 @@ serverConfigUtils::ConfigType SecureServersRepository::serverKind(const QString
return serverConfigUtils::configTypeFromJson(withoutStorageServerId(it.value()));
}
std::optional<QJsonObject> SecureServersRepository::serverJsonById(const QString &serverId) const
{
const auto it = m_serverJsonById.constFind(serverId);
if (it == m_serverJsonById.constEnd()) {
return std::nullopt;
}
return it.value();
}
std::optional<SelfHostedAdminServerConfig> SecureServersRepository::selfHostedAdminConfig(const QString &serverId) const
{
const auto it = m_serverJsonById.constFind(serverId);
@@ -31,6 +31,8 @@ public:
void removeServer(const QString &serverId);
serverConfigUtils::ConfigType serverKind(const QString &serverId) const;
std::optional<QJsonObject> serverJsonById(const QString &serverId) const;
std::optional<SelfHostedAdminServerConfig> selfHostedAdminConfig(const QString &serverId) const;
std::optional<SelfHostedUserServerConfig> selfHostedUserConfig(const QString &serverId) const;
std::optional<NativeServerConfig> nativeConfig(const QString &serverId) const;
@@ -52,6 +54,8 @@ public:
void invalidateCache();
int unsupportedFormatConfigsCount() const;
signals:
void serverAdded(const QString &serverId);
void serverEdited(const QString &serverId);
@@ -77,6 +81,7 @@ private:
QVector<QString> m_orderedServerIds;
QString m_defaultServerId;
int m_unsupportedFormatConfigsCount = 0;
};
#endif // SECURESERVERSREPOSITORY_H
+54 -69
View File
@@ -20,6 +20,9 @@ namespace
constexpr QLatin1String unprocessableSubscriptionMessage("Failed to retrieve subscription information. Is it activated?");
constexpr QLatin1String trialAlreadyUsedMessage("trial subscription already used");
constexpr QLatin1String refreshCaptchaMessage("refresh_captcha");
constexpr QLatin1String invalidCaptchaMessage("invalid_captcha");
constexpr QLatin1String rateLimitExceededMessage("rate_limit_exceeded");
QDateTime subscriptionEndUtcFromString(const QString &subscriptionEndDate)
{
@@ -111,9 +114,7 @@ bool apiUtils::isSubscriptionExpiringSoon(const QString &subscriptionEndDate, in
return endDate <= nowUtc.addDays(withinDays);
}
amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &sslErrors, const QString &replyErrorString,
const QNetworkReply::NetworkError &replyError, const int httpStatusCode,
const QByteArray &responseBody)
amnezia::ErrorCode apiUtils::checkApiResponseErrors(const QByteArray &responseBody)
{
const int httpStatusCodeConflict = 409;
const int httpStatusCodeNotFound = 404;
@@ -123,76 +124,60 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
const int httpStatusCodeRequestTimeout = 408;
const int httpStatusCodeUnprocessableEntity = 422;
if (!sslErrors.empty()) {
qDebug().noquote() << sslErrors;
return amnezia::ErrorCode::ApiConfigSslError;
}
if (replyError == QNetworkReply::NetworkError::OperationCanceledError
|| replyError == QNetworkReply::NetworkError::TimeoutError) {
qDebug() << replyError;
return amnezia::ErrorCode::ApiConfigTimeoutError;
}
if (replyError == QNetworkReply::NetworkError::OperationNotImplementedError) {
qDebug() << replyError;
return amnezia::ErrorCode::ApiUpdateRequestError;
}
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
if (jsonDoc.isObject()) {
QJsonObject jsonObj = jsonDoc.object();
const int httpStatusFromBody = jsonObj.value(QStringLiteral("http_status")).toInt(-1);
if (httpStatusFromBody == httpStatusCodeTooManyRequests) {
return amnezia::ErrorCode::ApiRateLimitError;
}
if (httpStatusFromBody == httpStatusCodeConflict) {
if (apiErrorMessageFromJson(jsonObj).contains(trialAlreadyUsedMessage, Qt::CaseInsensitive)) {
return amnezia::ErrorCode::ApiTrialAlreadyUsedError;
}
return amnezia::ErrorCode::ApiConfigLimitError;
}
if (httpStatusFromBody == httpStatusCodeNotFound) {
return amnezia::ErrorCode::ApiNotFoundError;
}
if (httpStatusFromBody == httpStatusCodeRequestTimeout) {
return amnezia::ErrorCode::ApiConfigTimeoutError;
}
if (httpStatusFromBody == httpStatusCodeNotImplemented) {
return amnezia::ErrorCode::ApiUpdateRequestError;
}
if (httpStatusFromBody == httpStatusCodeUnprocessableEntity) {
if (apiErrorMessageFromJson(jsonObj) == unprocessableSubscriptionMessage) {
return amnezia::ErrorCode::ApiSubscriptionExpiredError;
}
return amnezia::ErrorCode::ApiConfigDownloadError;
}
if (httpStatusFromBody == httpStatusCodePaymentRequired) {
const QString message = apiErrorMessageFromJson(jsonObj);
if (message.contains(QLatin1String("refresh_captcha"), Qt::CaseInsensitive)) {
return amnezia::ErrorCode::ApiCaptchaRefreshError;
}
if (message.contains(QLatin1String("invalid_captcha"), Qt::CaseInsensitive)) {
return amnezia::ErrorCode::ApiCaptchaInvalidError;
}
if (jsonObj.contains(QStringLiteral("captcha_id")) || jsonObj.contains(QStringLiteral("captcha_image"))
|| message.compare(QLatin1String("rate_limit_exceeded"), Qt::CaseInsensitive) == 0
|| message.contains(QLatin1String("rate_limit_exceeded"), Qt::CaseInsensitive)) {
return amnezia::ErrorCode::ApiCaptchaRequiredError;
}
return amnezia::ErrorCode::ApiSubscriptionNotActiveError;
}
if (httpStatusFromBody >= 300) {
return amnezia::ErrorCode::ApiConfigDownloadError;
}
}
if (replyError == QNetworkReply::NoError) {
if (!jsonDoc.isObject()) {
return amnezia::ErrorCode::NoError;
}
qDebug() << "something went wrong";
return amnezia::ErrorCode::ApiConfigDownloadError;
QJsonObject jsonObj = jsonDoc.object();
const int httpStatusFromBody = jsonObj.value(apiDefs::key::httpStatus).toInt(-1);
if (httpStatusFromBody < 0) {
return amnezia::ErrorCode::NoError;
}
if (httpStatusFromBody == httpStatusCodeTooManyRequests) {
return amnezia::ErrorCode::ApiRateLimitError;
}
if (httpStatusFromBody == httpStatusCodeConflict) {
if (apiErrorMessageFromJson(jsonObj).contains(trialAlreadyUsedMessage, Qt::CaseInsensitive)) {
return amnezia::ErrorCode::ApiTrialAlreadyUsedError;
}
return amnezia::ErrorCode::ApiConfigLimitError;
}
if (httpStatusFromBody == httpStatusCodeNotFound) {
return amnezia::ErrorCode::ApiNotFoundError;
}
if (httpStatusFromBody == httpStatusCodeRequestTimeout) {
return amnezia::ErrorCode::ApiConfigTimeoutError;
}
if (httpStatusFromBody == httpStatusCodeNotImplemented) {
return amnezia::ErrorCode::ApiUpdateRequestError;
}
if (httpStatusFromBody == httpStatusCodeUnprocessableEntity) {
if (apiErrorMessageFromJson(jsonObj) == unprocessableSubscriptionMessage) {
return amnezia::ErrorCode::ApiSubscriptionExpiredError;
}
return amnezia::ErrorCode::ApiConfigDownloadError;
}
if (httpStatusFromBody == httpStatusCodePaymentRequired) {
const QString message = apiErrorMessageFromJson(jsonObj);
if (message.contains(refreshCaptchaMessage, Qt::CaseInsensitive)) {
return amnezia::ErrorCode::ApiCaptchaRefreshError;
}
if (message.contains(invalidCaptchaMessage, Qt::CaseInsensitive)) {
return amnezia::ErrorCode::ApiCaptchaInvalidError;
}
if (jsonObj.contains(apiDefs::key::captchaId) || jsonObj.contains(apiDefs::key::captchaImage)
|| message.contains(rateLimitExceededMessage, Qt::CaseInsensitive)) {
return amnezia::ErrorCode::ApiCaptchaRequiredError;
}
return amnezia::ErrorCode::ApiSubscriptionNotActiveError;
}
if (httpStatusFromBody >= 300) {
return amnezia::ErrorCode::ApiConfigDownloadError;
}
return amnezia::ErrorCode::NoError;
}
bool apiUtils::isPremiumServer(const QJsonObject &serverConfigObject)
+1 -3
View File
@@ -29,9 +29,7 @@ namespace apiUtils
bool isPremiumServer(const QJsonObject &serverConfigObject);
amnezia::ErrorCode checkNetworkReplyErrors(const QList<QSslError> &sslErrors, const QString &replyErrorString,
const QNetworkReply::NetworkError &replyError, const int httpStatusCode,
const QByteArray &responseBody);
amnezia::ErrorCode checkApiResponseErrors(const QByteArray &responseBody);
QString getPremiumV2VpnKey(const QJsonObject &serverConfigObject);
}
+308
View File
@@ -0,0 +1,308 @@
#include "countryCatalog.h"
#include <limits>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include "logger.h"
namespace
{
Logger logger("CountryCatalog");
constexpr QLatin1String bundledPath(":/countries/regions.json");
constexpr QLatin1String keyVersion("version");
constexpr QLatin1String keySplitThreshold("splitThreshold");
constexpr QLatin1String keyCollapseThreshold("collapseThreshold");
constexpr QLatin1String keyRegions("regions");
constexpr QLatin1String keySubregions("subregions");
constexpr QLatin1String keyCountries("countries");
constexpr QLatin1String keyId("id");
constexpr QLatin1String keyOrder("order");
constexpr QLatin1String keyCode("code");
constexpr QLatin1String keyIso("iso");
constexpr QLatin1String keyRegion("region");
constexpr QLatin1String keySubregion("subregion");
constexpr QLatin1String keySubsubregion("subsubregion");
constexpr QLatin1String keyNameEn("nameEn");
constexpr QLatin1String keyNameRu("nameRu");
constexpr QLatin1String keyCity("city");
constexpr QLatin1String keyAliases("aliases");
constexpr QLatin1String keySplit("split");
constexpr QLatin1String keyUseCases("useCases");
constexpr QLatin1String keyLocationIds("locationIds");
constexpr QLatin1String keyNameLocalized("nameLocalized");
constexpr QLatin1String splitAlways("always");
constexpr QLatin1String splitNever("never");
countryCatalog::SplitMode parseSplit(const QJsonObject &object)
{
const QString split = object.value(keySplit).toString();
if (split == splitAlways) {
return countryCatalog::SplitMode::Always;
}
if (split == splitNever) {
return countryCatalog::SplitMode::Never;
}
return countryCatalog::SplitMode::Auto;
}
}
namespace countryCatalog
{
Catalog Catalog::fromJson(const QByteArray &json)
{
Catalog catalog;
QJsonParseError parseError {};
const QJsonDocument document = QJsonDocument::fromJson(json, &parseError);
if (parseError.error != QJsonParseError::NoError || !document.isObject()) {
logger.error() << "failed to parse the country catalog:" << parseError.errorString();
return catalog;
}
const QJsonObject root = document.object();
catalog.m_version = root.value(keyVersion).toInt();
if (root.contains(keySplitThreshold)) {
catalog.m_splitThreshold = root.value(keySplitThreshold).toInt(catalog.m_splitThreshold);
}
if (root.contains(keyCollapseThreshold)) {
catalog.m_collapseThreshold = root.value(keyCollapseThreshold).toInt(catalog.m_collapseThreshold);
}
const QJsonArray regions = root.value(keyRegions).toArray();
for (const QJsonValue &value : regions) {
const QJsonObject regionObject = value.toObject();
Region region;
region.id = regionObject.value(keyId).toString();
if (region.id.isEmpty()) {
continue;
}
region.order = regionObject.value(keyOrder).toInt();
region.split = parseSplit(regionObject);
const QJsonArray subregions = regionObject.value(keySubregions).toArray();
for (const QJsonValue &subregionValue : subregions) {
const QJsonObject subregionObject = subregionValue.toObject();
Subregion subregion;
subregion.id = subregionObject.value(keyId).toString();
if (subregion.id.isEmpty()) {
continue;
}
subregion.order = subregionObject.value(keyOrder).toInt();
subregion.split = parseSplit(subregionObject);
const QJsonArray subsubregions = subregionObject.value(keySubregions).toArray();
for (const QJsonValue &subsubregionValue : subsubregions) {
const QJsonObject subsubregionObject = subsubregionValue.toObject();
Subsubregion subsubregion;
subsubregion.id = subsubregionObject.value(keyId).toString();
if (subsubregion.id.isEmpty()) {
continue;
}
subsubregion.order = subsubregionObject.value(keyOrder).toInt();
subregion.subsubregions.push_back(subsubregion);
}
region.subregions.push_back(subregion);
}
catalog.m_regions.push_back(region);
}
const QJsonArray countries = root.value(keyCountries).toArray();
for (const QJsonValue &value : countries) {
const QJsonObject countryObject = value.toObject();
Entry entry;
entry.code = countryObject.value(keyCode).toString();
if (entry.code.isEmpty()) {
continue;
}
entry.isoCode = countryObject.value(keyIso).toString(entry.code).toUpper();
entry.regionId = countryObject.value(keyRegion).toString();
entry.subregionId = countryObject.value(keySubregion).toString();
entry.subsubregionId = countryObject.value(keySubsubregion).toString();
entry.nameEn = countryObject.value(keyNameEn).toString();
entry.nameRu = countryObject.value(keyNameRu).toString();
entry.city = countryObject.value(keyCity).toString();
const QJsonArray aliases = countryObject.value(keyAliases).toArray();
for (const QJsonValue &aliasValue : aliases) {
const QString alias = aliasValue.toString();
if (!alias.isEmpty()) {
entry.aliases.push_back(alias);
}
}
catalog.m_byCode.insert(entry.code.toUpper(), entry);
if (!catalog.m_byIso.contains(entry.isoCode)) {
catalog.m_byIso.insert(entry.isoCode, entry);
}
}
const QJsonArray useCases = root.value(keyUseCases).toArray();
for (const QJsonValue &value : useCases) {
const QJsonObject useCaseObject = value.toObject();
UseCase useCase;
useCase.id = useCaseObject.value(keyId).toString();
if (useCase.id.isEmpty()) {
continue;
}
useCase.order = useCaseObject.value(keyOrder).toInt();
const QJsonObject names = useCaseObject.value(keyNameLocalized).toObject();
for (auto it = names.constBegin(); it != names.constEnd(); ++it) {
const QString name = it.value().toString().trimmed();
if (!name.isEmpty()) {
useCase.nameLocalized.insert(it.key(), name);
}
}
const QJsonArray locationIds = useCaseObject.value(keyLocationIds).toArray();
for (const QJsonValue &locationId : locationIds) {
const QString id = locationId.toString();
if (!id.isEmpty()) {
useCase.locationIds.push_back(id);
}
}
catalog.m_useCases.push_back(useCase);
}
logger.info() << "loaded" << catalog.m_regions.size() << "regions and"
<< catalog.m_byCode.size() << "locations, catalog version"
<< catalog.m_version;
return catalog;
}
Catalog Catalog::bundled()
{
QFile file(bundledPath);
if (!file.open(QIODevice::ReadOnly)) {
logger.error() << "the bundled country catalog is missing at" << bundledPath;
return {};
}
return fromJson(file.readAll());
}
bool Catalog::isEmpty() const
{
return m_regions.isEmpty() || m_byCode.isEmpty();
}
int Catalog::splitThreshold() const
{
return m_splitThreshold;
}
int Catalog::collapseThreshold() const
{
return m_collapseThreshold;
}
bool Catalog::splitByCount(int visibleCount, bool wasSplit) const
{
if (visibleCount > m_splitThreshold) {
return true;
}
if (visibleCount < m_collapseThreshold) {
return false;
}
return wasSplit;
}
int Catalog::version() const
{
return m_version;
}
const QVector<Region> &Catalog::regions() const
{
return m_regions;
}
const QVector<UseCase> &Catalog::useCases() const
{
return m_useCases;
}
const Entry *Catalog::find(const QString &countryCode, const QString &isoCode) const
{
const auto byCode = m_byCode.constFind(countryCode.toUpper());
if (byCode != m_byCode.constEnd()) {
return &byCode.value();
}
const auto byIso = m_byIso.constFind(isoCode.toUpper());
if (byIso != m_byIso.constEnd()) {
return &byIso.value();
}
return nullptr;
}
bool Catalog::hasSubregions(const QString &regionId) const
{
for (const Region &region : m_regions) {
if (region.id == regionId) {
return !region.subregions.isEmpty();
}
}
return false;
}
bool Catalog::isSplit(const QString &regionId, int visibleCount, bool wasSplit) const
{
for (const Region &region : m_regions) {
if (region.id != regionId) {
continue;
}
if (region.subregions.isEmpty() || region.split == SplitMode::Never) {
return false;
}
if (region.split == SplitMode::Always) {
return true;
}
return splitByCount(visibleCount, wasSplit);
}
return false;
}
bool Catalog::isSplit(const QString &regionId, const QString &subregionId, int visibleCount, bool wasSplit) const
{
for (const Region &region : m_regions) {
if (region.id != regionId) {
continue;
}
for (const Subregion &subregion : region.subregions) {
if (subregion.id != subregionId) {
continue;
}
if (subregion.subsubregions.isEmpty() || subregion.split == SplitMode::Never) {
return false;
}
if (subregion.split == SplitMode::Always) {
return true;
}
return splitByCount(visibleCount, wasSplit);
}
return false;
}
return false;
}
}
+98
View File
@@ -0,0 +1,98 @@
#ifndef COUNTRYCATALOG_H
#define COUNTRYCATALOG_H
#include <QByteArray>
#include <QHash>
#include <QLatin1String>
#include <QString>
#include <QStringList>
#include <QVector>
namespace countryCatalog
{
constexpr QLatin1String otherRegionId("other");
struct Entry
{
QString code;
QString isoCode;
QString regionId;
QString subregionId;
QString subsubregionId;
QString nameEn;
QString nameRu;
QString city;
QStringList aliases;
};
enum class SplitMode {
Auto,
Always,
Never
};
struct Subsubregion
{
QString id;
int order = 0;
};
struct Subregion
{
QString id;
int order = 0;
SplitMode split = SplitMode::Auto;
QVector<Subsubregion> subsubregions;
};
struct Region
{
QString id;
int order = 0;
SplitMode split = SplitMode::Auto;
QVector<Subregion> subregions;
};
struct UseCase
{
QString id;
int order = 0;
QHash<QString, QString> nameLocalized;
QStringList locationIds;
};
class Catalog
{
public:
Catalog() = default;
static Catalog fromJson(const QByteArray &json);
static Catalog bundled();
bool isEmpty() const;
int splitThreshold() const;
int collapseThreshold() const;
int version() const;
const QVector<Region> &regions() const;
const QVector<UseCase> &useCases() const;
const Entry *find(const QString &countryCode, const QString &isoCode) const;
bool hasSubregions(const QString &regionId) const;
bool isSplit(const QString &regionId, int visibleCount, bool wasSplit) const;
bool isSplit(const QString &regionId, const QString &subregionId, int visibleCount, bool wasSplit) const;
private:
QVector<Region> m_regions;
QVector<UseCase> m_useCases;
QHash<QString, Entry> m_byCode;
QHash<QString, Entry> m_byIso;
bool splitByCount(int visibleCount, bool wasSplit) const;
int m_splitThreshold = 15;
int m_collapseThreshold = 13;
int m_version = 0;
};
} // namespace countryCatalog
#endif // COUNTRYCATALOG_H
+1
View File
@@ -11,5 +11,6 @@
#define APP_IOS_BUNDLE_ID "@CLIENT_IOS_BUNDLE_ID@"
#define APP_IOS_STORE_URL_FALLBACK "@CLIENT_IOS_STORE_URL_FALLBACK@"
#define APP_ANDROID_PACKAGE "@CLIENT_ANDROID_PACKAGE@"
#define CLIENT_ENABLE_APP_UPDATES @CLIENT_ENABLE_APP_UPDATES@
#endif
+4 -1
View File
@@ -4,7 +4,10 @@
namespace apiDefs
{
constexpr int requestTimeoutMsecs = 12 * 1000; // 12 secs
constexpr int requestTimeoutMsecs = 6 * 1000;
constexpr int proxyRequestTimeoutMsecs = 6 * 1000;
constexpr int proxyStorageTimeoutMsecs = 6 * 1000;
constexpr int proxyHealthTimeoutMsecs = 1000;
} // namespace apiDefs
+2
View File
@@ -45,6 +45,8 @@ namespace apiDefs
constexpr QLatin1String locale("locale");
constexpr QLatin1String captchaId("captcha_id");
constexpr QLatin1String captchaImage("captcha_image");
constexpr QLatin1String httpStatus("http_status");
constexpr QLatin1String captchaSolution("captcha_solution");
constexpr QLatin1String activeDeviceCount("active_device_count");
+6 -3
View File
@@ -25,6 +25,7 @@ namespace amnezia
constexpr QLatin1String accessToken("api_key");
constexpr QLatin1String config("config");
constexpr QLatin1String configVersion("config_version");
constexpr QLatin1String formatVersion("format_version");
constexpr QLatin1String sendPayload("send_payload");
constexpr QLatin1String sendPayloadEndpoint("endpoint");
@@ -177,6 +178,8 @@ namespace amnezia
// ── Xray-specific keys ────────────────────────────────────────
constexpr QLatin1String xraySettingsVersion("xray_settings_version");
// Security
constexpr QLatin1String xraySecurity("xray_security"); // none | tls | reality
constexpr QLatin1String xrayFlow("xray_flow"); // "" | xtls-rprx-vision | xtls-rprx-vision-udp443
@@ -215,11 +218,11 @@ namespace amnezia
// Transport — mKCP
constexpr QLatin1String mkcpTti("mkcp_tti");
constexpr QLatin1String mkcpMtu("mkcp_mtu");
constexpr QLatin1String mkcpUplinkCapacity("mkcp_uplink_capacity");
constexpr QLatin1String mkcpDownlinkCapacity("mkcp_downlink_capacity");
constexpr QLatin1String mkcpReadBufferSize("mkcp_read_buffer_size");
constexpr QLatin1String mkcpWriteBufferSize("mkcp_write_buffer_size");
constexpr QLatin1String mkcpCongestion("mkcp_congestion"); // bool
constexpr QLatin1String mkcpCwndMultiplier("mkcp_cwnd_multiplier");
constexpr QLatin1String mkcpMaxSendingWindow("mkcp_max_sending_window");
// xPadding
constexpr QLatin1String xPaddingBytesMin("xpadding_bytes_min");
+101 -5
View File
@@ -48,11 +48,15 @@ namespace amnezia
namespace xray
{
constexpr char dataDir[] = "/opt/amnezia/xray";
constexpr char serverConfigPath[] = "/opt/amnezia/xray/server.json";
constexpr char expectedServerXrayRelease[] = "v26.7.28";
constexpr char uuidPath[] = "/opt/amnezia/xray/xray_uuid.key";
constexpr char PublicKeyPath[] = "/opt/amnezia/xray/xray_public.key";
constexpr char PrivateKeyPath[] = "/opt/amnezia/xray/xray_private.key";
constexpr char shortidPath[] = "/opt/amnezia/xray/xray_short_id.key";
constexpr char tlsCertPath[] = "/opt/amnezia/xray/tls_cert.pem";
constexpr char tlsKeyPath[] = "/opt/amnezia/xray/tls_key.pem";
constexpr char defaultSite[] = "www.googletagmanager.com";
constexpr char defaultPort[] = "443";
@@ -60,6 +64,8 @@ namespace amnezia
constexpr char defaultLocalAddr[] = "10.33.0.2";
constexpr char defaultLocalListenAddr[] = "127.0.0.1";
constexpr int settingsVersionCurrent = 2;
constexpr char defaultSecurity[] = "reality";
constexpr char defaultFlow[] = "xtls-rprx-vision";
constexpr char defaultTransport[] = "raw";
@@ -67,7 +73,7 @@ namespace amnezia
constexpr char defaultSni[] = "www.googletagmanager.com";
constexpr char defaultAlpn[] = "h2";
constexpr char defaultXhttpMode[] = "Auto";
constexpr char defaultXhttpMode[] = "Stream-one";
constexpr char defaultXhttpUplinkMethod[] = "POST";
constexpr char defaultXhttpSessionPlacement[] = "Path";
constexpr char defaultXhttpSessionKey[] = "";
@@ -75,9 +81,10 @@ namespace amnezia
constexpr char defaultXhttpUplinkDataPlacement[] = "Body";
constexpr char defaultXhttpHost[] = "www.googletagmanager.com";
constexpr char defaultXhttpPath[] = "/";
constexpr char defaultXhttpUplinkChunkSize[] = "0";
constexpr char defaultXhttpScMaxEachPostBytesMin[] = "1";
constexpr char defaultXhttpScMaxEachPostBytesMax[] = "100";
constexpr char defaultXhttpScMaxEachPostBytesMin[] = "1000000";
constexpr char defaultXhttpScMaxEachPostBytesMax[] = "1000000";
constexpr char defaultXhttpScMinPostsIntervalMsMin[] = "100";
constexpr char defaultXhttpScMinPostsIntervalMsMax[] = "800";
constexpr char defaultXhttpScStreamUpServerSecsMin[] = "1";
@@ -93,13 +100,16 @@ namespace amnezia
constexpr char defaultMkcpTti[] = "50";
constexpr char defaultMkcpUplinkCapacity[] = "5";
constexpr char defaultMkcpDownlinkCapacity[] = "20";
constexpr char defaultMkcpReadBufferSize[] = "2";
constexpr char defaultMkcpWriteBufferSize[] = "2";
constexpr char defaultMkcpMtu[] = "1350";
constexpr char defaultMkcpCwndMultiplier[] = "1";
constexpr char outbounds[] = "outbounds";
constexpr char inbounds[] = "inbounds";
constexpr char settings[] = "settings";
constexpr char streamSettings[] = "streamSettings";
constexpr char tag[] = "tag";
constexpr char sockopt[] = "sockopt";
constexpr char dialerProxy[] = "dialerProxy";
constexpr char vnext[] = "vnext";
constexpr char users[] = "users";
constexpr char servers[] = "servers";
@@ -120,6 +130,92 @@ namespace amnezia
constexpr char spiderX[] = "spiderX";
constexpr char user[] = "user";
constexpr char pass[] = "pass";
constexpr char logBlock[] = "log";
constexpr char logLevel[] = "loglevel";
constexpr char logLevelError[] = "error";
constexpr char protocol[] = "protocol";
constexpr char protocolVless[] = "vless";
constexpr char protocolFreedom[] = "freedom";
constexpr char protocolSocks[] = "socks";
constexpr char decryption[] = "decryption";
constexpr char decryptionNone[] = "none";
constexpr char listen[] = "listen";
constexpr char udp[] = "udp";
constexpr char tlsSettings[] = "tlsSettings";
constexpr char xhttpSettings[] = "xhttpSettings";
constexpr char kcpSettings[] = "kcpSettings";
constexpr char alpn[] = "alpn";
constexpr char certificates[] = "certificates";
constexpr char certificateFile[] = "certificateFile";
constexpr char keyFile[] = "keyFile";
constexpr char allowInsecure[] = "allowInsecure";
constexpr char pinnedPeerCertSha256[] = "pinnedPeerCertSha256";
constexpr char dest[] = "dest";
constexpr char privateKey[] = "privateKey";
constexpr char shortIds[] = "shortIds";
constexpr char xhttpHost[] = "host";
constexpr char xhttpPath[] = "path";
constexpr char xhttpMode[] = "mode";
constexpr char noGrpcHeader[] = "noGRPCHeader";
constexpr char noSseHeader[] = "noSSEHeader";
constexpr char sessionIdPlacement[] = "sessionIDPlacement";
constexpr char sessionIdKey[] = "sessionIDKey";
constexpr char seqPlacement[] = "seqPlacement";
constexpr char seqKey[] = "seqKey";
constexpr char uplinkDataPlacement[] = "uplinkDataPlacement";
constexpr char uplinkDataKey[] = "uplinkDataKey";
constexpr char uplinkHttpMethod[] = "uplinkHTTPMethod";
constexpr char uplinkChunkSize[] = "uplinkChunkSize";
constexpr char scMaxBufferedPosts[] = "scMaxBufferedPosts";
constexpr char scMaxEachPostBytes[] = "scMaxEachPostBytes";
constexpr char scMinPostsIntervalMs[] = "scMinPostsIntervalMs";
constexpr char scStreamUpServerSecs[] = "scStreamUpServerSecs";
constexpr char xPaddingObfsMode[] = "xPaddingObfsMode";
constexpr char xPaddingBytes[] = "xPaddingBytes";
constexpr char xPaddingKey[] = "xPaddingKey";
constexpr char xPaddingHeader[] = "xPaddingHeader";
constexpr char xPaddingPlacement[] = "xPaddingPlacement";
constexpr char xPaddingMethod[] = "xPaddingMethod";
constexpr char xmux[] = "xmux";
constexpr char xmuxMaxConcurrency[] = "maxConcurrency";
constexpr char xmuxMaxConnections[] = "maxConnections";
constexpr char xmuxCMaxReuseTimes[] = "cMaxReuseTimes";
constexpr char xmuxHMaxRequestTimes[] = "hMaxRequestTimes";
constexpr char xmuxHMaxReusableSecs[] = "hMaxReusableSecs";
constexpr char xmuxHKeepAlivePeriod[] = "hKeepAlivePeriod";
constexpr char kcpTti[] = "tti";
constexpr char kcpMtu[] = "mtu";
constexpr char kcpUplinkCapacity[] = "uplinkCapacity";
constexpr char kcpDownlinkCapacity[] = "downlinkCapacity";
constexpr char kcpCwndMultiplier[] = "cwndMultiplier";
constexpr char kcpMaxSendingWindow[] = "maxSendingWindow";
constexpr char legacyXhttpMethod[] = "method";
constexpr char legacySessionPlacement[] = "sessionPlacement";
constexpr char legacyScSessionPlacement[] = "scSessionPlacement";
constexpr char legacyScSeqPlacement[] = "scSeqPlacement";
constexpr char legacyScUplinkDataPlacement[] = "scUplinkDataPlacement";
constexpr char legacySessionKey[] = "sessionKey";
constexpr char legacyUplinkChunkSize[] = "xhttpUplinkChunkSize";
constexpr char legacyXPaddingBlock[] = "xPadding";
constexpr char legacyRangeFrom[] = "from";
constexpr char legacyRangeTo[] = "to";
constexpr char networkTcp[] = "tcp";
constexpr char networkXhttp[] = "xhttp";
constexpr char networkKcp[] = "kcp";
constexpr char transportRaw[] = "raw";
constexpr char transportXhttp[] = "xhttp";
constexpr char transportMkcp[] = "mkcp";
constexpr char securityNone[] = "none";
constexpr char securityTls[] = "tls";
constexpr char securityReality[] = "reality";
}
namespace cloak
+12 -1
View File
@@ -37,9 +37,17 @@ namespace amnezia
ServerLinuxKernelTooOld = 214,
XrayServerConfigInvalid = 215,
XrayServerNoVlessClients = 216,
XrayRealityKeysReadFailed = 217,
XrayKeysReadFailed = 217,
ServerContainerRuntimeNotSupported = 218,
ContainerRuntimeServiceNotRunning = 219,
XrayKeyMigrationFailed = 220,
XrayTlsNotSupported = 221,
XrayServerConfigRejected = 222,
XrayServerConfigRolledBack = 223,
XrayServerNotServing = 224,
ServerDataVolumeNotRemoved = 225,
XrayKeyGenerationFailed = 226,
XrayKeyMigrationNeedsConfirm = 227,
// Ssh connection errors
SshRequestDeniedError = 300,
@@ -82,6 +90,9 @@ namespace amnezia
RestoreBackupInvalidError = 904,
LegacyApiV1NotSupportedError = 905,
LegacyContainerNotSupportedError = 906,
ConfigFormatVersionNotSupportedError = 907,
RestoreBackupUnsupportedConfigsSkipped = 908,
ImportXraySettingsFileUseXrayPage = 909,
// Android errors
AndroidError = 1000,
+40 -1
View File
@@ -36,9 +36,45 @@ QString errorString(ErrorCode code) {
case(ErrorCode::XrayServerNoVlessClients):
errorMessage = QObject::tr("Server error: XRay server has no VLESS clients");
break;
case(ErrorCode::XrayRealityKeysReadFailed):
case(ErrorCode::XrayKeysReadFailed):
errorMessage = QObject::tr("Server error: failed to read XRay Reality keys from the server");
break;
case(ErrorCode::XrayTlsNotSupported):
errorMessage = QObject::tr("Server error: could not create a TLS certificate on the XRay server. "
"The previous settings were left unchanged. Try again, or use Reality.");
break;
case(ErrorCode::XrayServerConfigRejected):
errorMessage = QObject::tr("Server error: XRay rejected the new configuration. "
"Nothing was changed, the server keeps running with the previous settings.");
break;
case(ErrorCode::XrayServerConfigRolledBack):
errorMessage = QObject::tr("Server error: XRay did not start with the new configuration. "
"The previous settings were restored and the server is running again.");
break;
case(ErrorCode::XrayServerNotServing):
errorMessage = QObject::tr("Server error: XRay is not accepting connections and the previous configuration "
"could not be restored. The server needs attention: check it over SSH, "
"or reinstall the protocol.");
break;
case(ErrorCode::ServerDataVolumeNotRemoved):
errorMessage = QObject::tr("Server error: the previous data volume could not be removed. "
"Installing on top of it would reuse the old keys, so the configurations you "
"expected to revoke would keep working. Check the server and try again.");
break;
case(ErrorCode::XrayKeyGenerationFailed):
errorMessage = QObject::tr("Server error: XRay could not generate its keys on the server. "
"Check the server and try again.");
break;
case(ErrorCode::XrayKeyMigrationFailed):
errorMessage = QObject::tr("Server error: could not preserve the XRay keys stored on the server. "
"The settings were not changed, so the existing configurations keep working. "
"Check that the server is reachable and try again.");
break;
case(ErrorCode::XrayKeyMigrationNeedsConfirm):
errorMessage = QObject::tr("The old XRay container will not start, so its keys cannot be preserved. "
"Applying these settings will recreate it with new keys, and all existing "
"configurations will stop working.");
break;
case(ErrorCode::ServerContainerRuntimeNotSupported): errorMessage = QObject::tr("Server error: The default container runtime available for installation on this server is not supported.\n Install Docker Engine on the server manually and try again."); break;
case(ErrorCode::ContainerRuntimeServiceNotRunning): errorMessage = QObject::tr("Container runtime error: The container runtime service is not running.\n Check the container runtime service on the server, or wait about a minute and try again."); break;
@@ -69,8 +105,11 @@ QString errorString(ErrorCode code) {
case (ErrorCode::ImportInvalidConfigError): errorMessage = QObject::tr("The config does not contain any containers and credentials for connecting to the server"); break;
case (ErrorCode::ImportBackupFileUseRestoreInstead): errorMessage = QObject::tr("Backup files cannot be imported here. Use 'Restore from backup' instead."); break;
case (ErrorCode::ImportXraySettingsFileUseXrayPage): errorMessage = QObject::tr("This is an XRay settings file, not a connection config. Import it on the XRay Configurations page using 'Import settings'."); break;
case (ErrorCode::RestoreBackupInvalidError): errorMessage = QObject::tr("Backup file is corrupted or has invalid format"); break;
case (ErrorCode::LegacyApiV1NotSupportedError): errorMessage = QObject::tr("This legacy Amnezia subscription format is no longer supported"); break;
case (ErrorCode::ConfigFormatVersionNotSupportedError): errorMessage = QObject::tr("This configuration was created in a newer version of the application and is not fully supported. Please update the application"); break;
case (ErrorCode::RestoreBackupUnsupportedConfigsSkipped): errorMessage = QObject::tr("Some configurations from the backup were not restored because they require a newer version of the application"); break;
case (ErrorCode::LegacyContainerNotSupportedError): errorMessage = QObject::tr("This protocol is no longer supported. Please select another protocol or remove this container from the server settings."); break;
case (ErrorCode::ImportOpenConfigError): errorMessage = QObject::tr("Unable to open config file"); break;
case (ErrorCode::NoInstalledContainersError): errorMessage = QObject::tr("VPN Protocols is not installed.\n Please install VPN container at first"); break;
@@ -0,0 +1,15 @@
#ifndef LOCALPROXYDEFS_H
#define LOCALPROXYDEFS_H
#include <QtGlobal>
namespace amnezia::localProxy
{
constexpr int defaultProxyPort = 10808;
constexpr int proxyPortMin = 1024;
constexpr int proxyPortMax = 65535;
constexpr quint16 apiPort = 49490;
constexpr int portReleaseWaitMs = 800;
}
#endif // LOCALPROXYDEFS_H
@@ -0,0 +1,51 @@
#include "portUtils.h"
#include "localProxyConstants.h"
#include <QElapsedTimer>
#include <QHostAddress>
#include <QTcpServer>
#include <QThread>
using namespace amnezia;
bool portUtils::isPortAvailable(int port)
{
if (port < localProxy::proxyPortMin || port > localProxy::proxyPortMax) {
return false;
}
QTcpServer server;
const bool success = server.listen(QHostAddress::LocalHost, static_cast<quint16>(port));
server.close();
return success;
}
bool portUtils::waitForPort(int port, int timeoutMs)
{
QElapsedTimer timer;
timer.start();
while (!isPortAvailable(port)) {
if (timer.elapsed() >= timeoutMs) {
return false;
}
QThread::msleep(200);
}
return true;
}
std::optional<int> portUtils::findFirstAvailablePort(int startPort, int endPort)
{
startPort = qMax(startPort, localProxy::proxyPortMin);
endPort = qMin(endPort, localProxy::proxyPortMax);
for (int port = startPort; port <= endPort; ++port) {
if (isPortAvailable(port)) {
return port;
}
}
return std::nullopt;
}
+13
View File
@@ -0,0 +1,13 @@
#ifndef PORTUTILS_H
#define PORTUTILS_H
#include <optional>
namespace portUtils
{
bool isPortAvailable(int port);
bool waitForPort(int port, int timeoutMs);
std::optional<int> findFirstAvailablePort(int startPort, int endPort);
}
#endif // PORTUTILS_H
@@ -0,0 +1,47 @@
#include "xrayConfigUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/containers/containerUtils.h"
#include <QJsonArray>
using namespace amnezia;
std::optional<QString> xrayConfigUtils::extractLastConfig(const QJsonObject &serverConfig)
{
const QJsonArray containers = serverConfig.value(configKey::containers).toArray();
const QString targetContainer = ContainerUtils::containerToString(DockerContainer::Xray);
for (const QJsonValue &value : containers) {
const QJsonObject container = value.toObject();
if (container.value(configKey::container).toString() != targetContainer) {
continue;
}
const QJsonObject proto = container.value(QString(configKey::xray)).toObject();
const QString serialized = proto.value(configKey::lastConfig).toString();
if (!serialized.isEmpty()) {
return serialized;
}
}
return std::nullopt;
}
bool xrayConfigUtils::setInboundPort(QJsonObject &xrayConfig, int port)
{
if (!xrayConfig.value(QLatin1String("inbounds")).isArray()) {
return false;
}
QJsonArray inbounds = xrayConfig.value(QLatin1String("inbounds")).toArray();
if (inbounds.isEmpty() || !inbounds.at(0).isObject()) {
return false;
}
QJsonObject firstInbound = inbounds.at(0).toObject();
firstInbound.insert(QLatin1String("port"), port);
inbounds[0] = firstInbound;
xrayConfig.insert(QLatin1String("inbounds"), inbounds);
return true;
}
@@ -0,0 +1,15 @@
#ifndef XRAYCONFIGUTILS_H
#define XRAYCONFIGUTILS_H
#include <optional>
#include <QJsonObject>
#include <QString>
namespace xrayConfigUtils
{
std::optional<QString> extractLastConfig(const QJsonObject &serverConfig);
bool setInboundPort(QJsonObject &xrayConfig, int port);
}
#endif // XRAYCONFIGUTILS_H
@@ -74,7 +74,6 @@ QString amnezia::scriptName(ProtocolScriptType type)
case ProtocolScriptType::openvpn_template: return QLatin1String("template.ovpn");
case ProtocolScriptType::wireguard_template: return QLatin1String("template.conf");
case ProtocolScriptType::awg_template: return QLatin1String("template.conf");
case ProtocolScriptType::xray_template: return QLatin1String("template.json");
default: return QString();
}
}
@@ -38,8 +38,7 @@ enum ProtocolScriptType {
container_startup,
openvpn_template,
wireguard_template,
awg_template,
xray_template
awg_template
};
enum ClientScriptType {
+10
View File
@@ -34,6 +34,16 @@ bool hasThirdPartyConfig(const QJsonObject &json)
namespace serverConfigUtils
{
int configFormatVersion(const QJsonObject &serverConfigObject)
{
return serverConfigObject.value(amnezia::configKey::formatVersion).toInt(0);
}
bool isConfigFormatVersionSupported(const QJsonObject &serverConfigObject)
{
return configFormatVersion(serverConfigObject) <= currentConfigFormatVersion;
}
bool isServerFromApi(const QJsonObject &serverConfigObject)
{
const int configVersion = serverConfigObject.value(amnezia::configKey::configVersion).toInt();
+6
View File
@@ -25,6 +25,12 @@ enum ConfigSource {
AmneziaGateway
};
constexpr int currentConfigFormatVersion = 1;
int configFormatVersion(const QJsonObject &serverConfigObject);
bool isConfigFormatVersionSupported(const QJsonObject &serverConfigObject);
bool isServerFromApi(const QJsonObject &serverConfigObject);
ConfigSource getConfigSource(const QJsonObject &serverConfigObject);
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

+79
View File
@@ -0,0 +1,79 @@
<svg width="153" height="131" viewBox="0 0 153 131" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M89.875 91.1621C90.3862 91.1861 90.7434 91.2127 90.9463 91.2432C91.1868 91.2752 91.3234 91.3078 91.3555 91.3398C91.387 91.3714 91.403 91.3952 91.4033 91.4111C91.4033 91.4432 91.3708 91.4841 91.3066 91.5322C91.2691 91.5604 91.2384 91.5923 91.2119 91.626C90.8882 91.5982 90.4526 91.4567 89.875 91.1621Z" fill="#FCE8EC"/>
<path d="M82.4795 85.0371C82.5595 85.0372 82.6238 85.0529 82.6719 85.085C82.7357 85.101 82.7674 85.1253 82.7676 85.1572C82.7676 85.1893 82.7918 85.2135 82.8398 85.2295H82.9365C83.0005 85.2295 83.0649 85.2221 83.1289 85.2061L83.249 85.1816C83.4252 85.1817 83.5294 85.2777 83.5615 85.4697C83.5775 85.5659 83.6176 85.6384 83.6816 85.6865C83.7458 85.7346 83.8341 85.7588 83.9463 85.7588H84.1865C84.3629 85.7588 84.5878 85.743 84.8604 85.7109C85.2131 85.6789 85.7027 85.6222 86.3281 85.542C87.5949 85.3977 89.0944 85.2778 90.8262 85.1816C91.1955 85.1613 91.5448 85.143 91.874 85.127C91.6239 85.7797 91.455 86.5243 91.374 87.377C91.094 87.3938 90.7909 87.4152 90.4648 87.4424C89.8877 87.4905 89.3747 87.5388 88.9258 87.5869C88.2204 87.6671 87.6591 87.7228 87.2422 87.7549C86.8417 87.7869 86.5608 87.8037 86.4004 87.8037H85.8711C85.791 87.7877 85.7268 87.7551 85.6787 87.707C85.5985 87.6429 85.5418 87.6113 85.5098 87.6113C85.4939 87.6114 85.4864 87.619 85.4863 87.6348C85.4863 87.6668 85.4937 87.6994 85.5098 87.7314C85.5095 87.9879 84.9566 88.132 83.8506 88.1641H83.8018C83.5614 88.1641 83.3689 88.1882 83.2246 88.2363C83.0803 88.2845 82.9279 88.4127 82.7676 88.6211C82.6151 88.8193 82.3981 89.1484 82.1152 89.6074C80.9537 89.662 79.7921 89.7185 78.6631 89.5508C78.7495 89.4099 78.8501 89.2761 78.9668 89.1504C79.047 89.0542 79.1278 88.9501 79.208 88.8379C79.3042 88.7096 79.3757 88.5888 79.4238 88.4766C79.472 88.3643 79.5603 88.2118 79.6885 88.0195C79.8167 87.8111 79.9618 87.6029 80.1221 87.3945C80.2663 87.2022 80.3788 87.0413 80.459 86.9131C80.539 86.769 80.5791 86.6805 80.5791 86.6484V86.625C80.5791 86.593 80.6267 86.5445 80.7227 86.4805C80.7787 86.4405 80.8448 86.3985 80.9209 86.3545L81.4385 86.0771C81.5145 86.0331 81.5807 85.9912 81.6367 85.9512C81.7486 85.8712 81.8053 85.8153 81.8057 85.7832C81.7896 85.7511 81.7812 85.7186 81.7812 85.6865C81.7813 85.6384 81.8055 85.5742 81.8535 85.4941C81.9337 85.3819 82.0303 85.2779 82.1426 85.1816C82.2708 85.0855 82.3833 85.0371 82.4795 85.0371Z" fill="#FCE8EC"/>
<path d="M76.1768 83.8105C76.1928 83.8907 76.2328 83.9632 76.2969 84.0273C76.377 84.0915 76.4579 84.1388 76.5381 84.1709C76.7302 84.219 76.8261 84.3316 76.8262 84.5078C76.8262 84.7323 76.6579 85.0771 76.3213 85.542C76.2872 85.595 76.2559 85.6491 76.2236 85.7021C76.2364 85.068 76.2311 84.4363 76.1758 83.8086L76.1768 83.8105Z" fill="#FCE8EC"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M147.645 81.502C147.692 81.5022 147.716 81.5347 147.716 81.5986C147.716 81.6306 147.733 81.6549 147.765 81.6709H147.788C147.82 81.6709 147.844 81.6625 147.86 81.6465C147.908 81.6144 147.973 81.5987 148.053 81.5986C148.133 81.5986 148.254 81.6302 148.414 81.6943C148.59 81.7425 148.791 81.8475 149.016 82.0078C149.24 82.152 149.4 82.3046 149.496 82.4648C149.544 82.545 149.568 82.6734 149.568 82.8496C149.568 82.994 149.544 83.2264 149.496 83.5469C149.448 83.8516 149.28 84.4452 148.991 85.3271C148.863 85.728 148.735 86.1369 148.606 86.5537C148.478 86.9704 148.374 87.307 148.294 87.5635C148.214 87.804 148.117 88.0771 148.005 88.3818C147.909 88.6865 147.812 88.9513 147.716 89.1758C147.636 89.3841 147.564 89.585 147.5 89.7773C147.468 89.9375 147.451 90.0576 147.451 90.1377V90.3057C147.451 90.3698 147.444 90.4423 147.428 90.5225C147.412 90.5865 147.372 90.6508 147.308 90.7148C147.26 90.779 147.195 90.9315 147.115 91.1719C147.035 91.4124 146.954 91.693 146.874 92.0137C146.762 92.4787 146.706 92.8002 146.706 92.9766V93.0479C146.706 93.1922 146.746 93.313 146.826 93.4092C146.89 93.4893 146.939 93.5618 146.971 93.626C146.987 93.6737 146.994 93.7057 146.994 93.7217V93.7939C146.994 93.81 147.003 93.8425 147.019 93.8906C147.051 93.9386 147.107 93.9862 147.188 94.0342C147.316 94.1464 147.38 94.2431 147.38 94.3232C147.38 94.4354 147.235 94.5154 146.946 94.5635C146.834 94.5795 146.754 94.6121 146.706 94.6602C146.658 94.6921 146.634 94.7239 146.634 94.7559V94.8281C146.634 94.8602 146.61 94.9085 146.562 94.9727C146.513 95.0207 146.441 95.0607 146.345 95.0928C146.201 95.141 146.081 95.2693 145.984 95.4775C145.904 95.686 145.808 96.0549 145.695 96.584C145.551 97.1772 145.407 97.7385 145.263 98.2676C145.118 98.7807 144.894 99.4951 144.589 100.409C144.429 100.89 144.3 101.187 144.204 101.299C144.188 101.331 144.172 101.347 144.156 101.347C144.124 101.347 144.108 101.291 144.108 101.179V100.77C144.108 100.657 144.1 100.602 144.084 100.602C144.02 100.602 143.907 100.794 143.747 101.179C143.619 101.515 143.475 101.699 143.314 101.731H143.267C143.106 101.731 142.897 101.603 142.641 101.347C142.32 101.042 142.16 100.777 142.16 100.553C142.16 100.457 142.192 100.369 142.256 100.289C142.32 100.193 142.376 99.992 142.424 99.6875C142.472 99.3668 142.504 99.0296 142.521 98.6768C142.537 98.5006 142.545 98.3405 142.545 98.1963C142.545 98.052 142.537 97.9312 142.521 97.835C142.504 97.7389 142.496 97.6588 142.496 97.5947C142.496 97.5306 142.504 97.4739 142.521 97.4258C142.553 97.3618 142.633 97.3143 142.761 97.2822C142.889 97.2342 142.97 97.1699 143.002 97.0898C143.018 97.0578 143.025 97.0252 143.025 96.9932C143.025 96.913 143.001 96.8006 142.953 96.6562C142.921 96.512 142.881 96.3837 142.833 96.2715C142.785 96.1437 142.753 96.0635 142.737 96.0312C142.721 96.0152 142.673 96.0068 142.593 96.0068C142.497 96.0068 142.296 96.031 141.991 96.0791C141.703 96.1112 141.294 96.1837 140.765 96.2959L138.912 96.6562L138.335 97.4258C138.159 97.6503 137.982 97.8593 137.806 98.0518C137.645 98.228 137.517 98.3564 137.421 98.4365C137.341 98.5006 137.277 98.5648 137.229 98.6289C137.18 98.677 137.156 98.7253 137.156 98.7734V98.7969C137.156 98.861 137.125 98.9577 137.061 99.0859C137.012 99.1982 136.948 99.2948 136.868 99.375C136.804 99.4391 136.747 99.4707 136.699 99.4707H136.627C136.611 99.4709 136.579 99.4868 136.531 99.5186C136.483 99.5506 136.443 99.5991 136.411 99.6631C136.363 99.7272 136.339 99.7839 136.339 99.832V99.8799C136.339 99.8957 136.347 99.9032 136.362 99.9033C136.362 99.9354 136.282 100.032 136.122 100.192C135.978 100.353 135.77 100.569 135.497 100.842C134.904 101.435 134.558 101.731 134.462 101.731C134.446 101.731 134.439 101.724 134.438 101.708C134.438 101.66 134.478 101.563 134.559 101.419C134.591 101.355 134.606 101.315 134.606 101.299H134.583C134.551 101.299 134.454 101.379 134.294 101.539C134.053 101.764 133.813 101.876 133.572 101.876H133.452C133.276 101.876 133.163 101.844 133.115 101.78C133.067 101.7 133.043 101.531 133.043 101.274C133.027 100.906 132.963 100.698 132.851 100.649C132.835 100.649 132.826 100.641 132.826 100.625C132.826 100.561 132.883 100.441 132.995 100.265C133.091 100.088 133.252 99.856 133.477 99.5674C133.701 99.2787 133.885 99.0214 134.029 98.7969C134.137 98.6289 134.205 98.5106 134.232 98.4414L134.26 98.3359C134.287 98.2789 134.354 98.2081 134.462 98.124C134.59 98.0118 134.727 97.9068 134.871 97.8105C134.999 97.7465 135.103 97.7149 135.184 97.7148H135.328C135.36 97.6988 135.393 97.6663 135.425 97.6182L135.448 97.5469C135.448 97.5309 135.441 97.5065 135.425 97.4746C135.409 97.4425 135.376 97.4184 135.328 97.4023C135.296 97.3863 135.264 97.3461 135.232 97.2822C135.216 97.2021 135.208 97.1137 135.208 97.0176V96.9453C135.208 96.8652 135.192 96.7768 135.16 96.6807C135.128 96.5684 135.071 96.4644 134.991 96.3682C134.895 96.224 134.848 96.1114 134.848 96.0312V95.959C134.848 95.9109 134.919 95.8142 135.063 95.6699C135.159 95.574 135.224 95.5022 135.256 95.4541V95.4053C135.256 95.3Line truncated
<path d="M125.933 98.4209C125.91 98.5009 125.899 98.5622 125.899 98.6045V98.6533C125.933 98.6694 126.085 98.6927 126.356 98.7246C126.645 98.7567 126.957 98.7892 127.294 98.8213C127.631 98.8373 127.911 98.8457 128.136 98.8457H128.593C128.817 98.8297 129.058 98.8213 129.314 98.8213C129.555 98.8213 129.772 98.8297 129.964 98.8457C130.172 98.8617 130.3 98.8859 130.349 98.918C130.413 98.95 130.494 98.9816 130.59 99.0137C130.702 99.0457 130.815 99.0699 130.927 99.0859C131.231 99.118 131.384 99.1821 131.384 99.2783C131.384 99.3104 131.343 99.3587 131.263 99.4229C131.215 99.4868 131.15 99.5186 131.07 99.5186C130.99 99.5185 130.886 99.4869 130.758 99.4229C130.646 99.3588 130.55 99.3263 130.47 99.3262C130.39 99.3262 130.325 99.3505 130.277 99.3984C130.245 99.4305 130.229 99.463 130.229 99.4951C130.229 99.5431 130.301 99.5907 130.445 99.6387C130.654 99.7189 130.758 99.8396 130.758 100V100.024C130.758 100.217 130.645 100.352 130.421 100.433C130.325 100.465 130.205 100.489 130.061 100.505H129.411C129.139 100.489 128.569 100.481 127.703 100.481C126.853 100.465 125.915 100.465 124.889 100.481C124.604 100.481 124.33 100.483 124.066 100.484C124.833 99.9439 125.417 99.2157 125.933 98.4209Z" fill="#F18928"/>
<path d="M136.482 83.8594C137.493 83.8754 138.11 83.9479 138.335 84.0762C138.415 84.1243 138.463 84.1642 138.479 84.1963L138.504 84.2441C138.504 84.2601 138.495 84.2685 138.479 84.2686C138.431 84.2686 138.399 84.3003 138.383 84.3643C138.367 84.4124 138.359 84.4533 138.359 84.4854C138.359 84.5332 138.367 84.5811 138.383 84.6289C138.399 84.677 138.407 84.7179 138.407 84.75C138.407 84.83 138.383 84.8944 138.335 84.9424C138.287 85.0063 138.182 85.0539 138.022 85.0859C137.83 85.134 137.734 85.1824 137.733 85.2305C137.733 85.2625 137.766 85.2951 137.83 85.3271C137.91 85.3752 137.95 85.4152 137.95 85.4473C137.95 85.4792 137.902 85.4951 137.806 85.4951C137.661 85.5112 137.421 85.5353 137.084 85.5674C136.763 85.5834 136.443 85.5992 136.122 85.6152C135.898 85.6312 135.786 85.6714 135.785 85.7354C135.785 85.7514 135.794 85.7681 135.81 85.7842C135.825 85.8001 135.833 85.8161 135.833 85.832C135.833 85.8801 135.777 85.9043 135.665 85.9043C135.569 85.9043 135.432 85.8885 135.256 85.8564C135.096 85.8244 134.96 85.8077 134.848 85.8076C134.672 85.8076 134.559 85.8479 134.511 85.9277C134.479 85.9918 134.43 86.0244 134.366 86.0244C134.302 86.0244 134.205 85.9919 134.077 85.9277C133.997 85.8798 133.901 85.8481 133.789 85.832C133.693 85.816 133.612 85.8076 133.548 85.8076C133.537 85.8076 133.526 85.8091 133.515 85.8096C133.793 85.1749 134.06 84.5354 134.316 83.8916C134.832 83.8702 135.314 83.8594 135.762 83.8594H136.482Z" fill="#F18928"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M35.1172 71.3984C35.1973 71.3984 35.3015 71.4626 35.4297 71.5908C35.574 71.7191 35.719 71.8798 35.8633 72.0723C36.0716 72.3447 36.2 72.553 36.248 72.6973C36.2641 72.7454 36.2715 72.8021 36.2715 72.8662C36.2714 72.9623 36.2472 73.0666 36.1992 73.1787C36.1511 73.3229 36.087 73.4755 36.0068 73.6357C35.9268 73.7958 35.8467 73.9318 35.7666 74.0439C35.6865 74.1722 35.5981 74.3247 35.502 74.501C35.4058 74.6612 35.3333 74.8138 35.2852 74.958C35.221 75.0863 35.1569 75.1987 35.0928 75.2949C35.0447 75.3911 35.0047 75.4395 34.9727 75.4395C34.9405 75.4398 34.8687 75.52 34.7568 75.6797C34.6446 75.824 34.5238 76.0006 34.3955 76.209C34.2672 76.4335 34.1474 76.6425 34.0352 76.835C33.923 77.0272 33.8421 77.163 33.7939 77.2432C33.7458 77.3073 33.6817 77.4281 33.6016 77.6045C33.5375 77.7647 33.4658 77.9331 33.3857 78.1094C33.3056 78.3017 33.2173 78.4784 33.1211 78.6387C33.0409 78.799 32.9684 78.9114 32.9043 78.9756C32.8402 79.0397 32.7677 79.1679 32.6875 79.3604C32.6234 79.5528 32.5593 79.7534 32.4951 79.9619C32.431 80.1863 32.3669 80.3787 32.3027 80.5391C32.2387 80.6991 32.1828 80.7874 32.1348 80.8037C32.0868 80.8197 32.0467 80.8516 32.0146 80.8994C31.9986 80.9474 31.9903 80.9959 31.9902 81.0439C31.9902 81.1722 31.8221 81.6219 31.4854 82.3916C31.1647 83.1452 30.7716 84.0029 30.3066 84.9648C30.0982 85.3978 29.9217 85.7834 29.7773 86.1201C29.6497 86.4393 29.5855 86.6153 29.585 86.6484C29.585 86.6965 29.5283 86.8415 29.416 87.082C29.3199 87.3064 29.1998 87.5548 29.0557 87.8271C28.8151 88.2922 28.5745 88.7818 28.334 89.2949C28.0935 89.808 27.8928 90.2568 27.7324 90.6416C27.5721 91.0265 27.4922 91.2513 27.4922 91.3154C27.4922 91.3475 27.5164 91.3958 27.5645 91.46C27.6125 91.5239 27.6684 91.5725 27.7324 91.6045C27.8446 91.6686 27.9014 91.7244 27.9014 91.7725C27.9171 91.8045 27.8846 91.8446 27.8047 91.8926C27.7245 91.9407 27.6846 91.989 27.6846 92.0371C27.6846 92.0691 27.7088 92.1008 27.7568 92.1328C27.869 92.2129 27.9247 92.2855 27.9248 92.3496C27.9248 92.4297 27.8288 92.5097 27.6367 92.5898C27.5566 92.6219 27.484 92.6703 27.4199 92.7344C27.3559 92.7984 27.3233 92.8627 27.3232 92.9268C27.3232 93.023 27.2833 93.0713 27.2031 93.0713C27.1551 93.0713 27.1066 93.0629 27.0586 93.0469C27.0267 93.031 26.9948 93.0235 26.9629 93.0234C26.8827 93.0234 26.7944 93.0793 26.6982 93.1914C26.6181 93.2876 26.4898 93.5283 26.3135 93.9131C26.1852 94.1696 26.0644 94.4261 25.9521 94.6826C25.9402 94.7125 25.9289 94.7416 25.918 94.7695C24.9547 94.9525 23.9881 95.1049 23.0186 95.2305C23.0654 95.0888 23.0898 94.9786 23.0898 94.8994C23.0898 94.8515 23.0824 94.8113 23.0664 94.7793C23.0504 94.7472 23.042 94.7147 23.042 94.6826V94.6592C23.058 94.6271 23.098 94.6104 23.1621 94.6104C23.2263 94.6104 23.2588 94.5946 23.2588 94.5625C23.2587 94.5465 23.2503 94.5306 23.2344 94.5146C23.2183 94.4986 23.21 94.4745 23.21 94.4424V94.3701C23.226 94.322 23.2585 94.2737 23.3066 94.2256C23.3705 94.1456 23.4022 94.0738 23.4023 94.0098C23.4023 93.9457 23.3791 93.8889 23.3311 93.8408C23.2509 93.7767 23.1301 93.7284 22.9697 93.6963H22.8252C22.729 93.6963 22.6165 93.7047 22.4883 93.7207C22.3762 93.7368 22.2961 93.7851 22.248 93.8652C22.2159 93.9132 22.1199 93.9533 21.96 93.9854C21.8477 94.0014 21.7269 94.0098 21.5986 94.0098H21.4062C21.1818 94.0098 20.9494 94.0172 20.709 94.0332C20.4847 94.0492 20.2921 94.0734 20.1318 94.1055L19.4814 94.1777C19.193 94.2098 18.9046 94.2256 18.6162 94.2256C17.7021 94.2737 17.0844 94.3462 16.7637 94.4424C16.4911 94.5386 16.3545 94.6752 16.3545 94.8516C16.3545 94.8835 16.363 94.9153 16.3789 94.9473C16.3789 94.9792 16.3629 95.0035 16.3311 95.0195C16.2991 95.0355 16.2589 95.0439 16.2109 95.0439H16.1865C16.1384 95.0439 16.0985 95.0681 16.0664 95.1162C16.0344 95.1642 16.0102 95.2361 15.9941 95.332C15.9818 95.4551 15.9496 95.5831 15.8994 95.7158C14.8176 95.7317 13.7349 95.7244 12.6523 95.6992L12.6562 95.6787C12.6682 95.6365 12.6988 95.5848 12.7471 95.5244C12.8272 95.4283 12.9157 95.3324 13.0117 95.2363C13.2039 95.1082 13.2997 95.0198 13.2998 94.9717V94.9473C13.2994 94.9153 13.2353 94.8835 13.1074 94.8516C13.0112 94.8195 12.9229 94.8037 12.8428 94.8037C12.7466 94.8037 12.6741 94.8195 12.626 94.8516C12.5779 94.8835 12.506 94.9153 12.4102 94.9473C12.33 94.9633 12.2417 94.9801 12.1455 94.9961C11.7446 95.0602 11.2634 95.1559 10.7021 95.2842C10.2132 95.382 9.79709 95.4803 9.4541 95.5781C8.74577 95.5411 8.03802 95.4976 7.33105 95.4502C7.28732 95.425 7.23981 95.3948 7.19043 95.3564C7.0461 95.2282 6.91792 95.0916 6.80566 94.9473C6.69348 94.803 6.63678 94.6831 6.63672 94.5869C6.63672 94.5548 6.66925 94.5149 6.7334 94.4668C6.79744 94.4188 6.86919 94.3862 6.94922 94.3701C7.04544 94.338 7.12628 94.2981 7.19043 94.25C7.2224 94.202 7.23823 94.1619 7.23828 94.1299V94.0576C7.23828 94.0255 7.18997 94.0098 7.09375 94.0098C6.98163 94.0097 6.92578 93.993 6.92578 93.9609V93.9131C6.92596 93.881 6.98168 93.8326 7.09375 93.7686C7.22204 93.7044 7.38276 93.6487 7.5752 93.6006C7.7676 93.5525 7.9925 93.4967 8.24902 93.4326C8.50532 93.368Line truncated
<path d="M36.1758 85.4463C36.3682 85.4463 36.5764 85.5188 36.8008 85.6631C36.9771 85.7914 37.1863 86.0553 37.4268 86.4561C37.6673 86.857 37.8837 87.2824 38.0762 87.7314C38.2685 88.1803 38.381 88.5493 38.4131 88.8379C38.4291 89.0142 38.4523 89.2067 38.4844 89.415C38.5164 89.6075 38.5574 89.7598 38.6055 89.8721C38.6375 90.0003 38.6691 90.1369 38.7012 90.2812C38.7332 90.4254 38.749 90.5455 38.749 90.6416C38.749 90.7218 38.7974 90.8584 38.8936 91.0508C39.0058 91.2272 39.134 91.4121 39.2783 91.6045C39.4225 91.7968 39.5435 91.9725 39.6396 92.1328C39.7355 92.2766 39.7838 92.3651 39.7842 92.3975C39.7842 92.4937 39.8399 92.542 39.9521 92.542C40.0163 92.542 40.113 92.5178 40.2412 92.4697C40.5298 92.3574 40.8989 92.1409 41.3477 91.8203C41.6201 91.612 41.8842 91.4194 42.1406 91.2432C42.4132 91.0507 42.6139 90.9142 42.7422 90.834C42.9988 90.6736 43.3601 90.433 43.8252 90.1123C44.2899 89.7918 44.6665 89.5193 44.9551 89.2949C45.131 89.1646 45.292 89.0481 45.4375 88.9443C44.7984 89.9981 44.3039 91.1283 44.1787 92.4561C44.0276 92.4992 43.8767 92.5213 43.7256 92.5645C42.0126 92.2288 40.1193 93.0705 38.3291 93.25C38.3365 93.2421 38.3408 93.231 38.3408 93.2158C38.3408 93.1517 38.2924 93.0475 38.1963 92.9033C38.1322 92.7911 38.0597 92.7028 37.9795 92.6387C37.9155 92.5586 37.8596 92.5187 37.8115 92.5186C37.7634 92.5186 37.6909 92.486 37.5947 92.4219C37.5146 92.3578 37.4262 92.2778 37.3301 92.1816C37.2339 92.0854 37.1298 91.9888 37.0176 91.8926C36.9053 91.7964 36.8087 91.7323 36.7285 91.7002C36.6007 91.6363 36.5364 91.5802 36.5361 91.5322V91.4355C36.5361 91.3393 36.5603 91.2111 36.6084 91.0508C36.6725 90.8904 36.7051 90.778 36.7051 90.7139V90.666C36.6891 90.5859 36.6248 90.5292 36.5127 90.4971C36.4165 90.465 36.3115 90.361 36.1992 90.1846C36.0871 90.0083 35.9987 89.8315 35.9346 89.6553C35.8866 89.4632 35.8633 89.303 35.8633 89.1748V89.0781C35.8791 89.0305 35.8866 88.9985 35.8867 88.9824C35.8867 88.9506 35.879 88.9339 35.8633 88.9336C35.8312 88.9336 35.7903 88.9578 35.7422 89.0059C35.6941 89.07 35.63 89.1908 35.5498 89.3672C35.4857 89.5435 35.4299 89.7444 35.3818 89.9688C35.3177 90.1931 35.2452 90.4098 35.165 90.6182C35.085 90.8263 35.0049 90.9865 34.9248 91.0986C34.8608 91.2107 34.8049 91.3151 34.7568 91.4111C34.7248 91.5072 34.708 91.5798 34.708 91.6279C34.708 91.6682 34.6889 91.7357 34.6553 91.832C34.4635 91.6 34.2765 91.3346 34.0957 91.0312C34.0957 91.0312 33.923 91.0965 33.9014 91.1396C33.4331 92.609 32.3218 93.2225 31.1494 93.5742C31.1377 93.5493 31.1222 93.5255 31.1006 93.5039C31.0525 93.4398 31.0283 93.3757 31.0283 93.3115V93.2637C31.0284 93.1996 31.0601 93.1036 31.124 92.9756C31.1882 92.8954 31.2449 92.8229 31.293 92.7588C31.3568 92.695 31.4045 92.6623 31.4365 92.6621C31.4686 92.6621 31.4928 92.6463 31.5088 92.6143C31.5408 92.5662 31.5576 92.5103 31.5576 92.4463C31.5576 92.3823 31.5651 92.3338 31.5811 92.3018C31.6131 92.2536 31.6457 92.2295 31.6777 92.2295C31.7257 92.229 31.7659 92.1249 31.7979 91.917C31.8459 91.7086 31.8859 91.4519 31.918 91.1475C31.9661 90.7306 32.0144 90.4416 32.0625 90.2812C32.1266 90.1209 32.2149 90.0326 32.3271 90.0166C32.3912 89.9846 32.4396 89.952 32.4717 89.9199C32.5035 89.888 32.5195 89.8636 32.5195 89.8477C32.5194 89.8318 32.511 89.8157 32.4951 89.7998C32.4792 89.7838 32.4717 89.7595 32.4717 89.7275C32.4718 89.6634 32.5033 89.5592 32.5674 89.415C32.6315 89.2226 32.7282 89.0219 32.8564 88.8135C32.9847 88.6052 33.113 88.4284 33.2412 88.2842C33.3853 88.1239 33.457 87.9556 33.457 87.7793V87.6836C33.441 87.6515 33.4336 87.619 33.4336 87.5869C33.4336 87.4105 33.5702 87.2024 33.8428 86.9619C33.971 86.8336 34.0993 86.7286 34.2275 86.6484C34.3556 86.5684 34.4441 86.5283 34.4922 86.5283C34.5401 86.5282 34.5879 86.5123 34.6357 86.4805C34.6998 86.4484 34.7482 86.4084 34.7803 86.3604C34.8123 86.3122 34.8281 86.2797 34.8281 86.2637H34.8047C34.7887 86.2637 34.7561 86.2804 34.708 86.3125C34.6601 86.3444 34.6199 86.3604 34.5879 86.3604C34.5719 86.3603 34.5635 86.3519 34.5635 86.3359C34.5636 86.3198 34.5804 86.2881 34.6123 86.2402C34.6443 86.1923 34.676 86.1681 34.708 86.168C34.74 86.152 34.7642 86.1436 34.7803 86.1436C34.8123 86.1436 34.8449 86.1519 34.877 86.168C34.8929 86.1838 34.9089 86.1914 34.9248 86.1914C34.9728 86.1914 35.0288 86.168 35.0928 86.1201C35.1729 86.072 35.2612 85.9911 35.3574 85.8789C35.614 85.5903 35.8871 85.4463 36.1758 85.4463Z" fill="#80C9C1"/>
<path d="M75.6484 83.5703C75.7928 83.5703 75.9052 83.5861 75.9854 83.6182C76.0313 83.6335 76.0693 83.6566 76.1006 83.6865C76.105 83.7376 76.1083 83.7896 76.1133 83.8418C76.1724 84.4923 76.176 85.1472 76.1621 85.8047C76.0816 85.942 76.0061 86.0787 75.9375 86.2158C75.8252 86.4403 75.7612 86.6252 75.7451 86.7695C75.6809 87.0581 75.5443 87.3546 75.3359 87.6592C75.2026 87.8592 75.068 88.0076 74.9346 88.1074C73.8274 88.0586 72.7207 88.0135 71.6152 87.9629C71.6761 87.8654 71.7315 87.7734 71.7764 87.6836C71.8565 87.5393 71.8965 87.4426 71.8965 87.3945C71.8965 87.3625 71.9123 87.3225 71.9443 87.2744C71.9764 87.2103 72.0247 87.162 72.0889 87.1299C72.137 87.0977 72.2012 87.0252 72.2812 86.9131C72.3774 86.8009 72.4658 86.6809 72.5459 86.5527C72.6261 86.4245 72.7301 86.2879 72.8584 86.1436C73.0026 85.9994 73.131 85.8794 73.2432 85.7832C73.3554 85.687 73.4595 85.5745 73.5557 85.4463C73.6519 85.318 73.716 85.1972 73.748 85.085C73.7801 84.9888 73.8528 84.8529 73.9648 84.6768C74.0771 84.5004 74.2137 84.3239 74.374 84.1475C74.5825 83.923 74.759 83.7706 74.9033 83.6904C75.0636 83.6103 75.2561 83.5703 75.4805 83.5703H75.6484Z" fill="#95C7DB"/>
<path d="M52.46 85.1094C52.6203 85.1094 52.7653 85.1493 52.8936 85.2295C53.0218 85.3097 53.1659 85.4781 53.3262 85.7344L53.6719 86.208C52.5665 86.3028 51.4006 86.2011 50.1602 85.8652C50.364 85.7257 50.5383 85.603 50.6807 85.4941C50.7287 85.4462 50.7688 85.4219 50.8008 85.4219H50.873C50.905 85.4219 50.9293 85.4303 50.9453 85.4463L50.9932 85.4697C51.0251 85.4696 51.041 85.438 51.041 85.374C51.041 85.2458 51.5141 85.1575 52.46 85.1094Z" fill="#95C7DB"/>
<path d="M112.116 89.2471C112.052 89.3271 112.004 89.383 111.972 89.415C111.956 89.431 111.94 89.4393 111.924 89.4395C111.916 89.4395 111.909 89.4377 111.905 89.4336L111.894 89.3965C111.889 89.3924 111.883 89.3906 111.875 89.3906C111.859 89.3907 111.835 89.3991 111.803 89.415C111.739 89.4472 111.659 89.5114 111.562 89.6074C111.466 89.6876 111.354 89.7601 111.226 89.8242C111.114 89.8722 111.018 89.8964 110.938 89.8965C110.841 89.8965 110.705 89.9522 110.528 90.0645C110.368 90.1607 110.207 90.2815 110.047 90.4258C109.887 90.57 109.694 90.7058 109.47 90.834C109.245 90.9623 109.053 91.0505 108.893 91.0986C108.716 91.1467 108.564 91.2109 108.436 91.291C108.308 91.355 108.228 91.4194 108.195 91.4834C108.163 91.5475 108.115 91.6042 108.051 91.6523C108.003 91.6844 107.954 91.7002 107.906 91.7002C107.842 91.7002 107.786 91.7161 107.738 91.748C107.706 91.7801 107.689 91.8127 107.689 91.8447C107.689 91.8927 107.666 91.9328 107.618 91.9648C107.586 91.9809 107.545 91.9893 107.497 91.9893C107.449 91.9895 107.353 92.0294 107.209 92.1094C107.081 92.1735 106.944 92.2618 106.8 92.374C106.656 92.47 106.536 92.5418 106.439 92.5898C106.343 92.6379 106.271 92.6621 106.223 92.6621H106.15C106.102 92.6621 106.014 92.6947 105.886 92.7588C105.741 92.855 105.565 92.9749 105.356 93.1191C104.875 93.4719 104.507 93.6484 104.25 93.6484C103.962 93.6485 103.729 93.7769 103.553 94.0332C103.473 94.1455 103.376 94.2421 103.264 94.3223C103.168 94.4023 103.079 94.4508 102.999 94.4668C102.919 94.4669 102.839 94.491 102.759 94.5391C102.695 94.5711 102.646 94.6111 102.614 94.6592C102.582 94.7072 102.534 94.7557 102.47 94.8037C102.422 94.8356 102.366 94.859 102.302 94.875C102.238 94.891 102.158 94.932 102.062 94.9961C101.965 95.0442 101.877 95.1083 101.797 95.1885C101.701 95.2686 101.588 95.3328 101.46 95.3809C101.348 95.4289 101.244 95.4531 101.147 95.4531C101.067 95.4531 100.962 95.4931 100.834 95.5732C100.722 95.6373 100.626 95.7174 100.546 95.8135C100.466 95.9257 100.369 96.0224 100.257 96.1025C100.161 96.1825 100.065 96.23 99.9688 96.2461C99.8886 96.2461 99.7844 96.2787 99.6562 96.3428C99.528 96.4069 99.4072 96.471 99.2949 96.5352C99.1346 96.6474 99.0547 96.7199 99.0547 96.752H99.1025C99.1827 96.752 99.351 96.7352 99.6074 96.7031C99.7998 96.6711 100.016 96.6479 100.257 96.6318C100.497 96.5998 100.706 96.5756 100.883 96.5596C101.043 96.5435 101.236 96.5194 101.46 96.4873C101.7 96.4553 101.909 96.4311 102.085 96.415C102.261 96.383 102.55 96.3588 102.951 96.3428C103.368 96.3107 103.801 96.2865 104.25 96.2705C104.699 96.2545 105.132 96.2387 105.549 96.2227C105.966 96.1906 106.263 96.158 106.439 96.126C106.696 96.0939 107.178 96.0539 107.883 96.0059C108.588 95.9578 109.326 95.9178 110.096 95.8857C110.865 95.8537 111.467 95.8379 111.899 95.8379C112.076 95.8379 112.341 95.8537 112.693 95.8857C113.062 95.9018 113.423 95.926 113.775 95.958C114.465 96.0221 115.026 96.0537 115.459 96.0537C116.02 96.0537 116.373 96.0062 116.518 95.9102C116.582 95.8781 116.646 95.8614 116.71 95.8613C116.822 95.8613 116.951 95.9098 117.095 96.0059C117.223 96.086 117.352 96.1344 117.48 96.1504C117.608 96.1664 117.72 96.1748 117.816 96.1748C117.94 96.1748 118.087 96.1688 118.258 96.1592C118.071 96.8806 117.934 97.6153 117.814 98.3613C117.559 98.3947 117.296 98.429 117.023 98.46C116.318 98.5241 115.828 98.564 115.556 98.5801C115.428 98.5801 115.307 98.5875 115.195 98.6035H114.738C114.642 98.5875 114.545 98.5633 114.449 98.5312C114.337 98.5152 114.2 98.5078 114.04 98.5078C113.88 98.5078 113.736 98.5236 113.607 98.5557C113.479 98.5877 113.351 98.6035 113.223 98.6035C113.062 98.6035 112.902 98.5794 112.741 98.5312C112.565 98.4672 112.437 98.4439 112.356 98.46C112.292 98.46 112.252 98.4916 112.236 98.5557C112.204 98.6357 112.164 98.6756 112.116 98.6758C112.068 98.6758 112.011 98.6442 111.947 98.5801C111.867 98.5161 111.795 98.4835 111.731 98.4834C111.683 98.4834 111.619 98.516 111.539 98.5801C111.475 98.6282 111.346 98.6681 111.153 98.7002C110.977 98.7162 110.801 98.7236 110.625 98.7236H110.36C110.088 98.7236 109.839 98.732 109.614 98.748C109.39 98.7641 109.253 98.7882 109.205 98.8203C109.157 98.8363 109.117 98.8447 109.085 98.8447H109.037C108.973 98.8287 108.925 98.7962 108.893 98.748C108.861 98.684 108.82 98.6524 108.772 98.6523H108.7C108.636 98.6523 108.556 98.684 108.46 98.748C108.364 98.7962 108.227 98.8287 108.051 98.8447C107.891 98.8607 107.746 98.8681 107.618 98.8682H107.426C107.217 98.8682 107.024 98.8765 106.848 98.8926C106.687 98.8926 106.583 98.9084 106.535 98.9404C106.439 98.9885 106.311 99.0127 106.15 99.0127H105.813C105.653 99.0127 105.501 99.0043 105.356 98.9883C105.132 98.9563 104.972 98.9087 104.876 98.8447C104.828 98.7967 104.764 98.7726 104.684 98.7725C104.571 98.7725 104.426 98.8282 104.25 98.9404C104.026 99.0847 103.833 99.1572 103.673 99.1572C103.513 99.1572 103.393 99.0929 103.312 98.9648C103.281 98.9171 103.256 98.8929 103.24 98.8926C103.224 98.8926 103.207 98.9325 103.191 99.0127V99.0371C103.191 99.101 103.063 99.1328 102.807 99.1328C102.614 99.1328 102.3Line truncated
<path d="M127.932 84.2969C127.371 85.0365 126.763 85.7472 126.113 86.4307C126.106 86.4232 126.098 86.4165 126.092 86.4082C126.044 86.3601 125.971 86.3202 125.875 86.2881C125.795 86.24 125.715 86.2159 125.635 86.2158C125.571 86.2158 125.506 86.2074 125.442 86.1914C125.394 86.1593 125.37 86.1194 125.37 86.0713C125.37 86.0394 125.346 86.0075 125.298 85.9756C125.266 85.9435 125.217 85.9268 125.153 85.9268C125.025 85.9268 124.953 85.8868 124.937 85.8066C124.921 85.7267 124.913 85.6707 124.913 85.6387C124.913 85.5585 124.929 85.4702 124.961 85.374C125.025 85.2297 125.122 85.1331 125.25 85.085C125.41 85.0529 125.49 85.0129 125.49 84.9648C125.49 84.9327 125.433 84.8769 125.321 84.7969C125.241 84.7488 125.194 84.7004 125.178 84.6523C125.162 84.6042 125.153 84.5717 125.153 84.5557C125.153 84.5399 125.162 84.5322 125.178 84.5322C125.242 84.5002 125.539 84.4676 126.067 84.4355C126.577 84.3892 127.199 84.3432 127.932 84.2969Z" fill="#FEDAA5"/>
<path d="M119.044 83.6182C119.044 83.7303 119.084 83.8503 119.164 83.9785C119.26 84.1389 119.309 84.2996 119.309 84.46C119.309 84.8717 118.977 85.2322 118.316 85.5469C118.599 84.9145 118.824 84.2409 119.05 83.5674C119.048 83.5844 119.044 83.6011 119.044 83.6182Z" fill="#FEDAA5"/>
<path d="M119.133 83.3223C119.129 83.3424 119.123 83.3617 119.113 83.3818C119.12 83.362 119.126 83.3421 119.133 83.3223Z" fill="#FEDAA5"/>
<path d="M114.594 82.4629C114.674 82.4629 114.738 82.4862 114.786 82.5342C114.914 82.6144 115.171 82.6868 115.556 82.751C115.941 82.8151 116.254 82.8551 116.494 82.8711H116.999C117.304 82.8711 117.536 82.8469 117.696 82.7988C117.857 82.7507 117.993 82.7266 118.105 82.7266C118.218 82.7266 118.29 82.7591 118.322 82.8232C118.354 82.8713 118.419 82.9197 118.515 82.9678C118.611 83.0158 118.715 83.0559 118.827 83.0879C118.987 83.1199 119.075 83.1516 119.092 83.1836C119.124 83.2157 119.14 83.2482 119.14 83.2803C119.14 83.3284 119.124 83.3767 119.092 83.4248C119.06 83.4889 119.044 83.5531 119.044 83.6172C119.044 83.6441 119.046 83.6714 119.051 83.6992C118.84 84.3206 118.627 84.94 118.363 85.5244C118.164 85.6222 117.935 85.7171 117.673 85.8057C117.545 85.8538 117.44 85.9021 117.359 85.9502C117.295 85.9983 117.264 86.0551 117.264 86.1191V86.1904C117.264 86.2383 117.248 86.2625 117.216 86.2627C117.2 86.2627 117.175 86.2552 117.144 86.2393C117.111 86.2232 117.079 86.2148 117.047 86.2148H117.023C117.007 86.2309 116.999 86.255 116.999 86.2871C116.999 86.3352 116.975 86.3752 116.927 86.4072C116.879 86.4392 116.823 86.455 116.759 86.4551C116.695 86.4551 116.638 86.4718 116.59 86.5039C116.558 86.5358 116.542 86.5677 116.542 86.5996C116.542 86.6477 116.518 86.6877 116.47 86.7197C116.438 86.7357 116.398 86.7441 116.35 86.7441H116.253C116.093 86.7442 115.884 86.8167 115.628 86.9609C115.339 87.1373 115.059 87.3538 114.786 87.6104C114.642 87.7547 114.497 87.8745 114.353 87.9707C114.224 88.0507 114.129 88.0908 114.064 88.0908C114.016 88.0908 113.96 88.115 113.896 88.1631C113.832 88.1951 113.776 88.2352 113.728 88.2832C113.695 88.3473 113.607 88.4282 113.463 88.5244C113.319 88.6206 113.158 88.7163 112.981 88.8125C112.805 88.9087 112.661 88.9812 112.549 89.0293C112.453 89.0614 112.389 89.0771 112.356 89.0771C112.341 89.0771 112.332 89.0695 112.332 89.0537V89.0293C112.316 89.0294 112.3 89.0378 112.284 89.0537C112.249 89.0772 112.211 89.1189 112.167 89.1768C111.154 89.2914 110.126 89.3999 109.11 89.6855C105.332 90.7219 103.231 93.4715 100.295 95.2852C99.453 95.8031 98.3999 96.0228 97.4717 96.3682C97.0259 96.097 96.5805 95.824 96.1348 95.5508C96.1751 95.5399 96.218 95.5305 96.2637 95.5234C96.6004 95.4753 96.8736 95.2755 97.082 94.9229C97.1782 94.7785 97.2823 94.6577 97.3945 94.5615C97.5067 94.4654 97.595 94.4096 97.6592 94.3936C97.7233 94.3936 97.8599 94.3369 98.0684 94.2246C98.2768 94.1124 98.5017 93.9767 98.7422 93.8164C98.9987 93.6401 99.2626 93.4709 99.5352 93.3105C99.8237 93.1503 100.064 93.0304 100.257 92.9502C100.433 92.87 100.658 92.7492 100.931 92.5889C101.203 92.4125 101.452 92.2285 101.677 92.0361C101.901 91.86 102.101 91.7157 102.277 91.6035C102.469 91.4755 102.582 91.4104 102.614 91.4102C102.71 91.4102 102.871 91.3628 103.096 91.2666C103.336 91.1544 103.568 91.0336 103.793 90.9053C104.017 90.777 104.162 90.673 104.227 90.5928C104.275 90.5288 104.362 90.4644 104.49 90.4004C104.619 90.3202 104.755 90.2477 104.899 90.1836C105.028 90.1195 105.204 90.0237 105.429 89.8955C105.669 89.7672 105.894 89.6307 106.103 89.4863C106.327 89.326 106.568 89.1736 106.824 89.0293C107.081 88.869 107.305 88.7481 107.497 88.668C107.689 88.5878 107.899 88.4763 108.123 88.332C108.347 88.1717 108.54 88.0193 108.7 87.875C108.845 87.7307 108.981 87.6099 109.109 87.5137C109.253 87.4178 109.349 87.3693 109.397 87.3691C109.526 87.3531 109.742 87.2732 110.047 87.1289C110.351 86.9846 110.632 86.8322 110.889 86.6719C111.161 86.4955 111.322 86.3589 111.37 86.2627C111.402 86.1826 111.459 86.1184 111.539 86.0703C111.619 86.0224 111.715 85.9981 111.827 85.998C111.955 85.998 112.124 85.9422 112.332 85.8301C112.54 85.7018 112.701 85.5736 112.813 85.4453C112.942 85.3172 113.006 85.2129 113.006 85.1328V85.084C112.958 85.02 112.885 84.9799 112.789 84.9639C112.693 84.9479 112.613 84.9404 112.549 84.9404L112.381 84.9639C112.253 84.9959 112.189 85.036 112.188 85.084C112.188 85.1321 111.907 85.173 111.346 85.2051C110.801 85.2211 110.127 85.2285 109.325 85.2285C108.508 85.2445 107.778 85.2603 107.137 85.2764C106.495 85.2924 106.094 85.3166 105.934 85.3486C105.821 85.3646 105.733 85.373 105.669 85.373C105.525 85.373 105.453 85.3329 105.453 85.2529C105.453 85.1567 105.244 85.1084 104.827 85.1084C104.667 85.1084 104.483 85.1168 104.274 85.1328H103.793C102.943 85.1328 102.519 85.0278 102.519 84.8193C102.519 84.8034 102.526 84.7796 102.542 84.748V84.6992C102.542 84.6031 102.429 84.5231 102.205 84.459C101.933 84.3948 101.749 84.274 101.652 84.0977C101.556 83.9214 101.508 83.7688 101.508 83.6406C101.508 83.5766 101.516 83.5122 101.532 83.4482C101.58 83.2561 101.725 83.1602 101.965 83.1602C102.109 83.1762 102.237 83.1836 102.35 83.1836C102.59 83.1836 102.711 83.152 102.711 83.0879C102.711 83.072 102.709 83.0579 102.705 83.0459L102.668 82.9854C102.664 82.9733 102.662 82.9593 102.662 82.9434C102.662 82.9114 102.695 82.8797 102.759 82.8477C102.855 82.7995 103.056 82.767 103.36 82.751C103.681 82.735 104.146 82.7266 104.755 82.7266H105.212C106.062 82.7266 106.487 82.7675 106.487 82Line truncated
<path d="M88.9258 91.125C89.2959 91.1363 89.6198 91.1489 89.8975 91.1621C90.5039 91.4647 90.9436 91.5862 91.2568 91.5781C91.2189 91.6151 91.1874 91.6545 91.165 91.6992C91.149 91.7472 91.1407 91.7873 91.1406 91.8193V91.8916C91.1406 91.9397 91.1081 91.9964 91.0439 92.0605C90.9958 92.1245 90.9157 92.1889 90.8037 92.2529C90.7985 92.2556 90.7932 92.2581 90.7881 92.2607C90.1683 91.8804 89.547 91.5024 88.9258 91.125Z" fill="#FFF1CF"/>
<path d="M96.3125 85.0117C97.3704 85.0117 98.0758 85.0443 98.4287 85.1084C98.6372 85.1725 98.7496 85.245 98.7656 85.3252C98.7975 85.3889 98.8133 85.4367 98.8135 85.4688C98.8135 85.5489 98.7494 85.6214 98.6211 85.6855C98.5577 85.7014 98.5259 85.7175 98.5254 85.7334C98.5254 85.7494 98.5572 85.7578 98.6211 85.7578C98.6692 85.7738 98.7101 85.8138 98.7422 85.8779C98.7902 85.926 98.8134 85.9903 98.8135 86.0703C98.8135 86.1826 98.7735 86.2634 98.6934 86.3115C98.6292 86.3595 98.509 86.3912 98.333 86.4072C98.1887 86.4233 98.0763 86.4558 97.9961 86.5039C97.9161 86.5359 97.8761 86.5676 97.876 86.5996V86.624C97.8758 86.7361 97.7715 86.8162 97.5635 86.8643C97.355 86.9124 97.146 86.9365 96.9375 86.9365C96.7932 86.9365 96.6408 86.9281 96.4805 86.9121L96.2402 86.8887L96.0234 86.9121C95.8793 86.9121 95.7835 86.9289 95.7354 86.9609C95.6712 87.009 95.5346 87.049 95.3262 87.0811C95.1177 87.1131 94.917 87.1289 94.7246 87.1289H94.0029C93.8267 87.1129 93.731 87.0887 93.7148 87.0566C93.7148 87.0247 93.698 87.0088 93.666 87.0088C93.65 87.009 93.6257 87.0332 93.5938 87.0811C93.5612 87.1291 93.3929 87.1776 93.0889 87.2256C92.8002 87.2737 92.4313 87.3136 91.9824 87.3457C91.7731 87.3535 91.5468 87.3648 91.3037 87.3799C91.3842 86.5275 91.5531 85.7828 91.8037 85.1299C93.0676 85.0676 94.0412 85.0361 94.7246 85.0361C95.5745 85.0201 96.104 85.0117 96.3125 85.0117Z" fill="#FFF1CF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M78.6709 89.541C79.8003 89.7079 80.9618 89.6509 82.123 89.5967C82.1061 89.6242 82.0887 89.6522 82.0713 89.6807C82.0553 89.7127 82.0311 89.7453 81.999 89.7773C81.919 89.9214 81.8389 90.0416 81.7588 90.1377C81.6786 90.2339 81.6219 90.2822 81.5898 90.2822C81.5418 90.2824 81.4775 90.3465 81.3975 90.4746C81.3334 90.5868 81.2692 90.7151 81.2051 90.8594C81.157 91.0037 81.1328 91.1161 81.1328 91.1963C81.1332 91.2443 81.2782 91.2686 81.5664 91.2686C81.8069 91.2685 82.1435 91.2518 82.5762 91.2197C83.2497 91.1716 84.0356 91.1401 84.9336 91.124C85.8476 91.108 86.5852 91.0996 87.1465 91.0996C87.7237 91.0996 88.2849 91.108 88.8301 91.124C88.8841 91.1256 88.9371 91.1273 88.9893 91.1289C89.5973 91.5016 90.2043 91.8745 90.8096 92.25C90.8072 92.2514 90.8052 92.2535 90.8027 92.2549C90.7068 92.3029 90.6265 92.3504 90.5625 92.3984C90.5144 92.4465 90.4902 92.4875 90.4902 92.5195V92.543L90.5146 92.5674C90.5145 92.5994 90.4736 92.6311 90.3936 92.6631C90.2974 92.6951 90.169 92.7277 90.0088 92.7598C89.8647 92.7918 89.7446 92.8076 89.6484 92.8076C89.5041 92.8076 89.4233 92.7677 89.4072 92.6875C89.3913 92.6237 89.3678 92.5911 89.3359 92.5908C89.2878 92.5908 89.2311 92.6392 89.167 92.7354C89.1029 92.7995 88.9746 92.8636 88.7822 92.9277C88.5898 92.9758 88.3975 93.0158 88.2051 93.0479C88.0126 93.0639 87.8519 93.0723 87.7236 93.0723H87.5312C87.3554 93.0722 87.267 93.0404 87.2666 92.9766C87.2666 92.9124 87.2267 92.8715 87.1465 92.8555C87.0664 92.8395 86.9938 92.832 86.9297 92.832H86.834C86.7378 92.8481 86.6737 92.8964 86.6416 92.9766C86.6252 93.0245 86.5285 93.0721 86.3525 93.1201C86.1922 93.1682 85.8957 93.2166 85.4629 93.2646C85.03 93.2967 84.4046 93.345 83.5869 93.4092C82.9936 93.4573 82.3916 93.513 81.7822 93.5771C81.173 93.6413 80.7 93.698 80.3633 93.7461C80.0265 93.7942 79.7618 93.8499 79.5693 93.9141C79.3932 93.9621 79.233 94.0423 79.0889 94.1543C78.9606 94.2505 78.7997 94.403 78.6074 94.6113C78.4311 94.8037 78.2786 94.9803 78.1504 95.1406C78.0385 95.2845 77.9818 95.3729 77.9814 95.4053C77.9654 95.5015 77.7973 95.7662 77.4766 96.1992C77.1719 96.6322 76.8272 97.0893 76.4424 97.5703C76.282 97.7627 76.1213 97.9634 75.9609 98.1719C75.8167 98.3802 75.7126 98.5485 75.6484 98.6768C75.5522 98.8532 75.5039 98.9739 75.5039 99.0381V99.0615C75.5199 99.0936 75.6407 99.1104 75.8652 99.1104C75.9775 99.1103 76.1458 99.102 76.3701 99.0859C76.5945 99.0539 76.8111 99.0139 77.0195 98.9658C77.244 98.9177 77.7011 98.8369 78.3906 98.7246C79.0801 98.6124 79.842 98.4842 80.6758 98.3398C81.5097 98.2116 82.3039 98.0917 83.0576 97.9795C83.827 97.8513 84.38 97.7546 84.7168 97.6904C87.8439 97.1773 89.8411 96.8726 90.707 96.7764C91.5885 96.6802 92.1816 96.6328 92.4863 96.6328C92.9513 96.6328 93.2161 96.6885 93.2803 96.8008C93.3444 96.8649 93.377 96.9132 93.377 96.9453C93.3768 96.9933 93.3201 97.0176 93.208 97.0176C93.0799 97.0176 93.0156 97.0334 93.0156 97.0654C93.0159 97.0816 93.0725 97.1458 93.1846 97.2578C93.3127 97.386 93.377 97.4828 93.377 97.5469C93.3929 97.6107 93.4004 97.651 93.4004 97.667C93.4003 97.7309 93.3603 97.7627 93.2803 97.7627C93.2482 97.7628 93.184 97.7953 93.0879 97.8594C93.0078 97.9075 92.9278 97.9716 92.8477 98.0518C92.6071 98.2442 92.0934 98.4039 91.3076 98.5322C90.522 98.6445 90.089 98.7737 90.0088 98.918C89.9925 99.0138 89.8966 99.0615 89.7207 99.0615C89.6405 99.0615 89.5439 99.0541 89.4316 99.0381C89.3194 99.006 89.2153 98.9893 89.1191 98.9893C89.055 98.9893 88.9983 98.9976 88.9502 99.0137C88.8222 99.0458 88.7103 99.1101 88.6143 99.2061C88.4218 99.3825 88.0205 99.4865 87.4111 99.5186C87.1546 99.5186 86.9139 99.5427 86.6895 99.5908C86.481 99.6229 86.3444 99.6712 86.2803 99.7354C86.216 99.7834 86.0635 99.8234 85.8232 99.8555C85.5988 99.8875 85.3422 99.9117 85.0537 99.9277C84.7492 99.9277 84.4768 99.9519 84.2363 100C84.0118 100.032 83.8669 100.072 83.8027 100.12C83.7386 100.168 83.6345 100.217 83.4902 100.265C83.362 100.297 83.2253 100.328 83.0811 100.36C82.9207 100.376 82.6799 100.417 82.3594 100.481C82.0389 100.546 81.7105 100.61 81.374 100.674C80.9093 100.786 80.6202 100.842 80.5078 100.842H80.3154C80.1873 100.842 80.1064 100.81 80.0742 100.746C80.0262 100.682 79.9787 100.65 79.9307 100.649C79.8826 100.633 79.8185 100.666 79.7383 100.746C79.6742 100.794 79.61 100.834 79.5459 100.866C79.4978 100.882 79.4569 100.89 79.4248 100.89H79.377C79.3448 100.89 79.2723 100.906 79.1602 100.938C79.0641 100.971 78.9597 101.011 78.8477 101.059C78.7034 101.139 78.5667 101.179 78.4385 101.179C78.3426 101.179 78.2548 101.154 78.1748 101.106C78.0947 101.042 78.0062 101.01 77.9102 101.01C77.8139 101.01 77.7089 101.042 77.5967 101.106C77.5165 101.17 77.3564 101.219 77.1162 101.251C76.8757 101.283 76.6109 101.307 76.3223 101.323C76.0497 101.323 75.7124 101.331 75.3115 101.347C75.0638 101.357 74.8327 101.368 74.6182 101.379C74.4808 99.7526 74.3144 98.1275 74.1094 96.5039V96.3682C74.1094 96.3361 74.1335 96.3036 74.1816 96.2715C74.2297 96.2234 74.2939 96.175 74.374 96.127C74.4701 96.0789 74.5502 96.0147 74.6143 95Line truncated
<path d="M97.4697 96.3652C97.5146 97.378 97.4598 98.3664 97.3252 99.333C97.3085 99.3312 97.2914 99.3278 97.2734 99.3262C97.1131 99.3101 96.9607 99.3027 96.8164 99.3027C96.6882 99.3027 96.544 99.3102 96.3838 99.3262H95.9746C95.7501 99.3262 95.5894 99.3188 95.4932 99.3027C95.3329 99.2707 95.2529 99.2139 95.2529 99.1338C95.2528 98.9896 95.0921 98.9012 94.7715 98.8691C94.5792 98.8531 94.4508 98.8215 94.3867 98.7734C94.3226 98.7253 94.291 98.6529 94.291 98.5566V98.4609C94.291 98.3006 94.2426 98.2198 94.1465 98.2197C94.1304 98.2197 94.1063 98.2281 94.0742 98.2441H94.002C94.002 98.2121 94.0586 98.1554 94.1709 98.0752C94.2829 97.9791 94.3547 97.8906 94.3867 97.8105C94.4187 97.7306 94.4345 97.6587 94.4346 97.5947C94.4346 97.5306 94.4272 97.4581 94.4111 97.3779C94.3791 97.2657 94.3633 97.1774 94.3633 97.1133C94.3633 97.0655 94.3709 97.0337 94.3867 97.0176C94.4348 96.9695 94.5556 96.9453 94.748 96.9453C94.924 96.9453 95.0601 96.9051 95.1562 96.8252C95.2525 96.729 95.3575 96.5441 95.4697 96.2715C95.5819 96.0151 95.6935 95.8384 95.8057 95.7422C95.8817 95.6535 95.9933 95.5905 96.1396 95.5518C96.5821 95.8228 97.0243 96.0961 97.4697 96.3652Z" fill="#A29EA1"/>
<path d="M93.8574 97.6182C93.8894 97.6182 93.9137 97.6266 93.9297 97.6426C93.9778 97.6747 94.002 97.723 94.002 97.7871C93.9859 97.8352 93.9617 97.8836 93.9297 97.9316C93.8816 97.9796 93.8173 98.0039 93.7373 98.0039C93.7053 98.0039 93.6895 97.9871 93.6895 97.9551V97.7627C93.7213 97.7149 93.7533 97.6831 93.7852 97.667C93.817 97.6351 93.8414 97.6184 93.8574 97.6182Z" fill="#A29EA1"/>
<path d="M8.3125 101.354C9.22629 101.385 10.14 101.417 11.0537 101.445C10.9053 101.635 10.748 101.843 10.582 102.068C10.3736 102.357 10.1887 102.622 10.0283 102.862C9.86807 103.103 9.78817 103.255 9.78809 103.319C9.78809 103.351 9.77214 103.376 9.74023 103.392H9.69141C9.6594 103.392 9.61919 103.383 9.57129 103.367H9.49902C9.38693 103.367 9.28262 103.48 9.18652 103.704C9.13841 103.816 9.07432 103.937 8.99414 104.065C8.91401 104.178 8.82564 104.266 8.72949 104.33C8.64942 104.394 8.57677 104.466 8.5127 104.546C8.46471 104.626 8.44153 104.69 8.44141 104.738C8.44141 104.802 8.31224 104.987 8.05566 105.292C7.79915 105.581 7.47845 105.917 7.09375 106.302C6.72492 106.687 6.39607 107.048 6.10742 107.385C5.80289 107.721 5.61045 107.954 5.53027 108.082C5.41802 108.291 5.28146 108.475 5.12109 108.636C4.96086 108.812 4.80829 108.932 4.66406 108.996C4.60029 109.028 4.55254 109.044 4.52051 109.044C4.45654 109.044 4.40805 109.021 4.37598 108.973C4.35994 108.925 4.32741 108.9 4.2793 108.9C4.18306 108.901 4.03051 109.005 3.82227 109.213C3.73131 109.31 3.64843 109.386 3.57812 109.446C3.45342 109.452 3.32878 109.458 3.2041 109.464C3.20039 109.439 3.19727 109.412 3.19727 109.381C3.19724 109.317 3.18885 109.261 3.17285 109.213C3.14079 109.181 3.10082 109.165 3.05273 109.165C2.98862 109.165 2.92446 109.068 2.86035 108.876C2.78018 108.7 2.72445 108.515 2.69238 108.322C2.67641 108.242 2.66799 108.17 2.66797 108.106C2.66797 108.026 2.68375 107.954 2.71582 107.89C2.87617 107.633 3.22927 107.184 3.77441 106.543C4.31964 105.902 4.86493 105.284 5.41016 104.69C5.55436 104.546 5.67433 104.418 5.77051 104.306C5.85044 104.178 5.89135 104.098 5.8916 104.065C5.8916 104.049 5.94745 103.985 6.05957 103.873C6.10769 103.809 6.16238 103.743 6.22266 103.675L6.61914 103.253C6.6791 103.185 6.73321 103.119 6.78125 103.055C6.8771 102.911 6.93297 102.815 6.94922 102.767C6.94922 102.686 7.03007 102.55 7.19043 102.357C7.36673 102.149 7.55085 101.957 7.74316 101.78C7.93524 101.604 8.06367 101.516 8.12793 101.516C8.16 101.516 8.20091 101.491 8.24902 101.443C8.27594 101.416 8.29574 101.386 8.3125 101.354Z" fill="#797AAA"/>
<path d="M20.5693 101.632C21.4568 101.637 22.3441 101.638 23.2314 101.633C23.1878 101.719 23.1322 101.832 23.0664 101.973C22.9702 102.213 22.8578 102.478 22.7295 102.767C22.5211 103.279 22.3687 103.56 22.2725 103.608H22.248C22.2321 103.608 22.2236 103.592 22.2236 103.56C22.2238 103.479 22.2721 103.287 22.3682 102.982C22.4322 102.774 22.4648 102.614 22.4648 102.502V102.453L22.4404 102.405C22.4084 102.406 22.3599 102.486 22.2959 102.646C22.2639 102.726 22.208 102.814 22.1279 102.91C22.0638 103.006 21.9996 103.095 21.9355 103.175C21.8555 103.255 21.7754 103.343 21.6953 103.439C21.6552 103.488 21.6191 103.534 21.5869 103.578L21.4189 103.83C21.3947 103.859 21.3745 103.873 21.3584 103.873H21.2861C21.2701 103.873 21.19 103.785 21.0459 103.608C20.9176 103.48 20.8368 103.344 20.8047 103.199C20.7726 103.119 20.7568 103.031 20.7568 102.935C20.7568 102.87 20.7652 102.798 20.7812 102.718V102.55C20.7812 102.438 20.7645 102.309 20.7324 102.165C20.7164 102.021 20.6848 101.884 20.6367 101.756C20.6109 101.713 20.5883 101.671 20.5693 101.632Z" fill="#797AAA"/>
<path d="M134.322 83.8906C134.065 84.5341 133.797 85.1733 133.519 85.8076C133.464 85.8097 133.409 85.8174 133.354 85.8311C133.242 85.8471 133.122 85.8629 132.994 85.8789C132.866 85.8949 132.762 85.9033 132.682 85.9033C132.521 85.9354 132.385 86.1195 132.272 86.4561C132.208 86.6164 132.112 86.8097 131.983 87.0342C131.855 87.2584 131.719 87.4669 131.575 87.6592C131.431 87.8515 131.303 88.044 131.19 88.2363C131.078 88.4287 131.006 88.5811 130.974 88.6934C130.958 88.7896 130.918 88.8862 130.854 88.9824C130.805 89.0625 130.749 89.1099 130.685 89.126C130.636 89.158 130.581 89.2231 130.517 89.3193C130.469 89.4154 130.429 89.5273 130.396 89.6553C130.364 89.7835 130.324 89.9044 130.276 90.0166C130.228 90.1289 130.172 90.2013 130.107 90.2334C130.059 90.2819 129.947 90.4502 129.771 90.7383C129.61 91.0269 129.434 91.3717 129.241 91.7725C129.033 92.1893 128.809 92.6224 128.568 93.0713C128.344 93.5202 128.152 93.8891 127.991 94.1777C127.366 95.2842 126.86 96.255 126.476 97.0889C126.178 97.733 125.996 98.1809 125.929 98.4326C125.406 99.2301 124.809 99.9541 124.024 100.483C123.367 100.487 122.773 100.493 122.242 100.504C121.505 100.504 121.103 100.512 121.039 100.528H120.895C120.847 100.528 120.791 100.504 120.727 100.456C120.662 100.424 120.566 100.391 120.438 100.359C120.293 100.327 120.149 100.312 120.005 100.312C119.877 100.312 119.764 100.304 119.668 100.288C119.572 100.256 119.523 100.215 119.523 100.167C119.524 100.119 119.54 100.087 119.571 100.071C119.603 100.039 119.636 100.023 119.668 100.023C119.716 100.023 119.756 99.9991 119.788 99.9512C119.82 99.9031 119.836 99.8464 119.836 99.7822C119.836 99.7342 119.82 99.6857 119.788 99.6377C119.74 99.5898 119.7 99.5664 119.668 99.5664C119.604 99.5664 119.571 99.5497 119.571 99.5176C119.571 99.4696 119.604 99.4136 119.668 99.3496C119.748 99.2374 120.005 99.1333 120.438 99.0371C120.87 98.9409 121.592 98.8285 122.603 98.7002C122.875 98.6681 123.076 98.6356 123.204 98.6035C123.332 98.5715 123.405 98.5399 123.421 98.5078C123.453 98.4599 123.469 98.4197 123.469 98.3877C123.469 98.3396 123.46 98.2912 123.444 98.2432V98.123C123.444 98.0429 123.453 97.962 123.469 97.8818C123.485 97.7377 123.509 97.6092 123.541 97.4971C123.589 97.3688 123.637 97.2163 123.685 97.04C123.733 96.8637 123.774 96.6955 123.806 96.5352C123.822 96.3748 123.853 96.2382 123.901 96.126C123.965 96.0138 124.022 95.958 124.07 95.958C124.198 95.9577 124.319 95.8294 124.431 95.5732C124.559 95.3007 124.656 94.9558 124.72 94.5391C124.752 94.3466 124.808 94.1617 124.888 93.9854C124.968 93.7929 125.065 93.6406 125.177 93.5283C125.273 93.4161 125.353 93.3036 125.417 93.1914C125.481 93.0798 125.512 93.0079 125.513 92.9756C125.513 92.8313 125.561 92.6705 125.657 92.4941C125.753 92.3019 125.858 92.1335 125.97 91.9893C126.082 91.8451 126.195 91.7568 126.307 91.7246C126.467 91.6765 126.548 91.5957 126.548 91.4834L126.523 91.4111V91.1709C126.555 91.0909 126.611 91.0107 126.691 90.9307C126.755 90.8506 126.812 90.7705 126.86 90.6904C126.908 90.5942 126.94 90.5134 126.956 90.4492C126.972 90.1287 127.005 89.9205 127.053 89.8242C127.101 89.728 127.189 89.6639 127.317 89.6318C127.414 89.5998 127.518 89.4955 127.63 89.3193C127.758 89.127 127.855 88.9504 127.919 88.79C127.999 88.6297 128.039 88.5247 128.039 88.4766C128.039 88.4287 128.031 88.3968 128.015 88.3809V88.3564C128.015 88.3243 128.063 88.2525 128.159 88.1406C128.255 88.0124 128.384 87.8599 128.544 87.6836C128.913 87.2506 129.098 86.9134 129.098 86.6729C129.098 86.5126 129.025 86.3927 128.881 86.3125C128.753 86.2644 128.64 86.2402 128.544 86.2402H128.327C128.247 86.2403 128.167 86.2477 128.087 86.2637C128.003 86.2756 127.951 86.3011 127.93 86.3398L127.908 86.4277C127.887 86.4669 127.834 86.4929 127.75 86.5049H127.245C127.101 86.4888 127.004 86.4479 126.956 86.3838C126.924 86.336 126.884 86.3125 126.836 86.3125C126.756 86.3126 126.667 86.3441 126.571 86.4082C126.459 86.4883 126.371 86.5283 126.307 86.5283C126.235 86.5282 126.171 86.4949 126.112 86.4307C126.762 85.7472 127.37 85.0364 127.931 84.2969C127.958 84.2951 127.987 84.2938 128.015 84.292C128.784 84.2439 129.619 84.1956 130.517 84.1475C131.414 84.0833 132.168 84.0266 132.777 83.9785C133.322 83.9396 133.837 83.9106 134.322 83.8906Z" fill="#F8B570"/>
<path d="M119.043 96.126C119.171 96.126 119.251 96.1428 119.283 96.1748C119.331 96.1909 119.355 96.2225 119.355 96.2705V96.3428C119.355 96.471 119.468 96.5751 119.692 96.6553C119.82 96.6713 119.916 96.7197 119.98 96.7998C120.045 96.8799 120.077 97 120.077 97.1602V97.2812C120.077 97.4893 120.037 97.6496 119.957 97.7617C119.909 97.8579 119.828 97.9062 119.716 97.9062C119.668 97.9062 119.619 97.8978 119.571 97.8818C119.523 97.8659 119.483 97.8584 119.451 97.8584C119.419 97.8585 119.403 97.8743 119.403 97.9062L119.428 97.9541C119.444 97.9701 119.451 97.9869 119.451 98.0029C119.451 98.0829 119.291 98.1473 118.971 98.1953C118.606 98.2545 118.22 98.3082 117.812 98.3613C117.932 97.6153 118.069 96.8806 118.256 96.1592C118.307 96.1563 118.361 96.1541 118.417 96.1504C118.69 96.1344 118.899 96.126 119.043 96.126Z" fill="#F8B570"/>
<path d="M71.6494 87.9102C72.7727 87.9547 73.8972 87.9963 75.0137 88.043C74.9284 88.1194 74.844 88.1763 74.7588 88.2119C74.6786 88.244 74.5819 88.3323 74.4697 88.4766C74.3735 88.6209 74.2936 88.7816 74.2295 88.958C74.1493 89.1504 74.0694 89.3112 73.9893 89.4395C73.9252 89.5675 73.8768 89.6316 73.8447 89.6318C73.8127 89.6318 73.7727 89.6802 73.7246 89.7764C73.6766 89.8565 73.644 89.9523 73.6279 90.0645C73.5959 90.1927 73.5475 90.3052 73.4834 90.4014C73.4193 90.4975 73.3551 90.5617 73.291 90.5938C73.2269 90.626 73.1385 90.7301 73.0264 90.9062C72.9302 91.0666 72.8261 91.259 72.7139 91.4834C72.6177 91.7239 72.522 91.933 72.4258 92.1094C72.33 92.2691 72.2656 92.365 72.2334 92.3975C72.1853 92.4295 72.0487 92.6544 71.8242 93.0713C71.6115 93.4663 71.3699 93.9407 71.0996 94.4941C71.0505 94.5274 71.0021 94.5616 70.9531 94.5947C69.9971 94.566 69.0423 94.5333 68.0879 94.5029C68.1228 94.4388 68.1434 94.3876 68.1436 94.3467C68.1436 94.2986 68.1594 94.2418 68.1914 94.1777C68.2395 94.1136 68.2962 94.0653 68.3604 94.0332C68.4565 93.9691 68.5449 93.8807 68.625 93.7686C68.705 93.6404 68.7535 93.5362 68.7695 93.4561C68.8014 93.3762 68.8173 93.3202 68.8174 93.2881C68.8174 93.224 68.7931 93.184 68.7451 93.168C68.7291 93.152 68.7208 93.1276 68.7207 93.0957C68.7207 93.0476 68.7375 92.9751 68.7695 92.8789C68.8337 92.7186 68.9294 92.5261 69.0576 92.3018C69.1859 92.0612 69.3309 91.7807 69.4912 91.46C69.6676 91.1393 69.82 90.8345 69.9482 90.5459C70.2529 89.9526 70.5009 89.6153 70.6934 89.5352C70.7894 89.5031 70.8695 89.4472 70.9336 89.3672C71.0137 89.2711 71.0704 89.1668 71.1025 89.0547C71.1346 88.9424 71.1903 88.7975 71.2705 88.6211C71.3507 88.4287 71.439 88.2521 71.5352 88.0918C71.5764 88.0299 71.6139 87.969 71.6494 87.9102Z" fill="#87ADD4"/>
<path d="M59.6289 85.1338C59.8051 85.1498 59.9894 85.1572 60.1816 85.1572C60.422 85.1732 60.6145 85.2133 60.7588 85.2773C60.9031 85.3415 61.0481 85.4539 61.1924 85.6143C61.2886 85.7425 61.3769 85.8875 61.457 86.0479C61.553 86.2079 61.6089 86.344 61.625 86.4561C61.6571 86.5843 61.6896 86.7293 61.7217 86.8896C61.7697 87.0498 61.8172 87.1941 61.8652 87.3223C61.8973 87.4504 61.9299 87.5947 61.9619 87.7549C61.9939 87.899 62.0097 88.0116 62.0098 88.0918C62.0098 88.172 62.0181 88.2528 62.0342 88.333C62.0502 88.3969 62.0745 88.4446 62.1064 88.4766C62.1706 88.5567 62.2505 88.7334 62.3467 89.0059C62.4429 89.2785 62.5154 89.5358 62.5635 89.7764C62.6275 90.0166 62.6592 90.1851 62.6592 90.2812V90.3535C62.6593 90.4337 62.6992 90.5621 62.7793 90.7383C62.8755 90.9467 63.0121 91.1874 63.1885 91.46C63.3648 91.7165 63.5089 91.9572 63.6211 92.1816C63.7333 92.3901 63.79 92.5183 63.79 92.5664C63.7901 92.6145 63.8218 92.6947 63.8857 92.8066C63.9499 92.9189 64.0224 93.0313 64.1025 93.1436C64.1988 93.2558 64.3112 93.4165 64.4395 93.625C64.5676 93.8333 64.6876 94.0417 64.7998 94.25C64.8283 94.3018 64.8586 94.352 64.8877 94.4023C64.2289 94.3809 63.57 94.3602 62.9111 94.3389C62.7711 94.2133 62.6068 94.0875 62.4189 93.9609C62.2586 93.8647 62.122 93.7765 62.0098 93.6963C61.8977 93.6002 61.8418 93.536 61.8418 93.5039C61.8415 93.4719 61.8014 93.4316 61.7217 93.3838C61.6576 93.3197 61.5691 93.2639 61.457 93.2158C61.3448 93.1837 61.2639 93.1512 61.2158 93.1191C61.1681 93.0714 61.1438 93.0395 61.1436 93.0234V92.9512C61.1436 92.9191 61.1362 92.8624 61.1201 92.7822C61.088 92.6701 61.0313 92.5416 60.9512 92.3975C60.8711 92.2533 60.8152 92.149 60.7832 92.085C60.7672 92.0051 60.7588 91.9491 60.7588 91.917C60.7588 91.901 60.7672 91.8926 60.7832 91.8926C60.8152 91.8925 60.8554 91.8608 60.9033 91.7969C60.9514 91.7168 60.9839 91.6283 61 91.5322C61.016 91.452 61.0234 91.3712 61.0234 91.291C61.0234 91.195 61.016 91.0989 61 91.0029C60.9679 90.8105 60.9112 90.634 60.8311 90.4736C60.7669 90.3133 60.7028 90.2334 60.6387 90.2334C60.5907 90.2334 60.5665 90.2249 60.5664 90.209V90.0889C60.5824 90.0568 60.5908 90.0243 60.5908 89.9922V89.9688C60.5748 89.9527 60.5422 89.9443 60.4941 89.9443C60.4304 89.9443 60.3986 89.9283 60.3984 89.8965C60.3984 89.8484 60.43 89.7917 60.4941 89.7275C60.5422 89.6794 60.5664 89.6311 60.5664 89.583C60.5663 89.5512 60.5589 89.5192 60.543 89.4873C60.5269 89.3911 60.4628 89.2703 60.3506 89.126C60.2546 89.014 60.166 88.9264 60.0859 88.8623C60.006 88.7824 59.9416 88.7415 59.8936 88.7412C59.8615 88.7412 59.7889 88.8297 59.6768 89.0059C59.5645 89.1823 59.4437 89.4071 59.3154 89.6797C59.1712 89.9521 59.027 90.2246 58.8828 90.4971C58.7385 90.7697 58.6261 90.9787 58.5459 91.123C58.4657 91.2834 58.3932 91.4358 58.3291 91.5801C58.281 91.7243 58.2578 91.8369 58.2578 91.917C58.2577 91.9811 58.2168 92.077 58.1367 92.2051C58.0726 92.3334 57.9843 92.4542 57.8721 92.5664C57.7601 92.6784 57.6725 92.7828 57.6084 92.8789C57.5603 92.9751 57.5361 93.0476 57.5361 93.0957V93.168C57.536 93.216 57.5193 93.272 57.4873 93.3359C57.4232 93.416 57.3432 93.4961 57.2471 93.5762C57.151 93.6563 57.0709 93.7447 57.0068 93.8408C56.9671 93.9004 56.9401 93.9572 56.9248 94.0107C56.4262 93.9299 55.9273 93.8492 55.4287 93.7744C55.0383 93.5496 54.6432 93.363 54.2451 93.2061C54.2511 93.193 54.2591 93.181 54.2646 93.168C54.3288 93.0397 54.3604 92.9431 54.3604 92.8789C54.3604 92.8148 54.3846 92.7348 54.4326 92.6387C54.4968 92.5264 54.5692 92.4214 54.6494 92.3252C54.7455 92.2451 54.8182 92.1651 54.8662 92.085C54.9302 91.9889 54.9619 91.9162 54.9619 91.8682C54.9622 91.8197 55.0105 91.6996 55.1064 91.5078C55.2026 91.2994 55.3309 91.067 55.4912 90.8105C55.6355 90.554 55.7554 90.3291 55.8516 90.1367C55.9475 89.9449 55.9958 89.8248 55.9961 89.7764C55.9961 89.7443 56.0045 89.7118 56.0205 89.6797C56.0524 89.6478 56.0843 89.632 56.1162 89.6318C56.1483 89.6318 56.1966 89.5835 56.2607 89.4873C56.3249 89.3911 56.3816 89.2703 56.4297 89.126C56.4778 88.9818 56.5494 88.8291 56.6455 88.6689C56.7577 88.4927 56.8619 88.3485 56.958 88.2363C57.0701 88.1243 57.1426 88.0283 57.1748 87.9482C57.2227 87.8685 57.2468 87.82 57.2471 87.8037V87.7793C57.2311 87.7633 57.2227 87.7474 57.2227 87.7314C57.2227 87.6834 57.2469 87.6191 57.2949 87.5391C57.359 87.4108 57.4474 87.2583 57.5596 87.082C57.6718 86.9056 57.7527 86.7607 57.8008 86.6484C57.8648 86.5204 57.8965 86.4402 57.8965 86.4082V86.3838C57.8806 86.3678 57.8721 86.3519 57.8721 86.3359C57.8722 86.3199 57.889 86.2956 57.9209 86.2637C57.9689 86.2157 58.0409 86.168 58.1367 86.1201C58.2329 86.072 58.3296 85.9912 58.4258 85.8789C58.538 85.7667 58.6347 85.6468 58.7148 85.5186C58.8592 85.2459 59.0273 85.1094 59.2197 85.1094H59.2432C59.3233 85.1094 59.4525 85.1178 59.6289 85.1338Z" fill="#87ADD4"/>
<path d="M50.166 85.8623C51.4009 86.1925 52.5615 86.2902 53.6621 86.1943L53.7832 86.3604L53.5186 87.0342C53.4544 87.2104 53.366 87.3863 53.2539 87.5625C53.1577 87.7389 53.0694 87.8681 52.9893 87.9482C52.8933 88.0282 52.813 88.116 52.749 88.2119C52.7009 88.3081 52.6768 88.389 52.6768 88.4531C52.6767 88.5173 52.6451 88.6215 52.5811 88.7656C52.5329 88.8939 52.4679 89.0221 52.3877 89.1504C52.3075 89.2786 52.236 89.4153 52.1719 89.5596C52.1238 89.7037 52.0912 89.8238 52.0752 89.9199C52.0752 90.0161 52.051 90.1128 52.0029 90.209C51.9709 90.2891 51.9309 90.3449 51.8828 90.377C51.8026 90.4251 51.6587 90.6341 51.4502 91.0029C51.2418 91.3557 51.0094 91.7646 50.7529 92.2295C50.7096 92.3133 50.6678 92.3963 50.626 92.4775C49.469 92.412 48.2998 92.4552 47.1328 92.4814C47.1622 92.411 47.1889 92.3424 47.2168 92.2773C47.3129 92.053 47.3771 91.9163 47.4092 91.8682C47.4571 91.8201 47.4973 91.7482 47.5293 91.6523C47.5774 91.5561 47.609 91.4595 47.625 91.3633C47.6571 91.2831 47.6738 91.219 47.6738 91.1709C47.6738 91.1229 47.657 91.0912 47.625 91.0752C47.5769 91.0432 47.4651 91.0264 47.2891 91.0264C47.0165 91.0264 46.8 91.1472 46.6396 91.3877C46.5595 91.4999 46.4627 91.5956 46.3506 91.6758C46.2784 91.7239 46.2106 91.7546 46.1475 91.7666L46.0244 91.7783C45.9613 91.7903 45.8934 91.8201 45.8213 91.8682C45.7251 91.9323 45.6526 92.0048 45.6045 92.085C45.5529 92.1623 45.4156 92.2938 45.1982 92.4854C44.865 92.4762 44.5324 92.4622 44.2012 92.4355C44.326 91.1115 44.8177 89.9838 45.4541 88.9326C46.014 88.5338 46.3445 88.3336 46.4463 88.333C46.4944 88.333 46.5752 88.2921 46.6875 88.2119C46.7996 88.1158 46.9198 88.004 47.0479 87.876C47.1922 87.7477 47.3771 87.6111 47.6016 87.4668C47.8259 87.3065 48.05 87.1698 48.2744 87.0576C48.6272 86.8652 49.0444 86.6087 49.5254 86.2881C49.7651 86.1334 49.9783 85.9909 50.166 85.8623Z" fill="#87ADD4"/>
<path d="M3.57129 109.451C3.45862 109.547 3.3752 109.598 3.31641 109.598C3.25442 109.598 3.21714 109.554 3.20312 109.468C3.32585 109.463 3.44855 109.457 3.57129 109.451Z" fill="#6E5EA6"/>
<path d="M47.1338 92.4805C48.3012 92.4536 49.4707 92.4097 50.6279 92.4746C50.4371 92.8449 50.2616 93.1877 50.1035 93.5039C49.9271 93.8888 49.8398 94.122 49.8398 94.2021C49.8397 94.2504 49.8072 94.3546 49.7432 94.5146C49.6951 94.659 49.6393 94.8197 49.5752 94.9961C49.3988 95.3809 49.1981 95.8381 48.9736 96.3672C48.7652 96.8963 48.5803 97.3851 48.4199 97.834C48.2596 98.267 48.1797 98.5159 48.1797 98.5801C48.1796 98.6284 48.1554 98.741 48.1074 98.917C48.0594 99.0771 47.9951 99.2532 47.915 99.4453C47.7388 99.846 47.6105 100.151 47.5303 100.359C47.4502 100.552 47.3934 100.704 47.3613 100.816C47.3293 100.913 47.3135 101.018 47.3135 101.13C47.2974 101.194 47.2574 101.283 47.1934 101.395C47.1453 101.491 47.081 101.587 47.001 101.683L46.7119 102.044L46.6885 101.515V100.985L46.4473 101.395C46.3351 101.619 46.2309 101.747 46.1348 101.779C46.0549 101.795 45.9829 101.803 45.9189 101.803C45.7907 101.803 45.614 101.755 45.3896 101.659C45.1331 101.563 44.9566 101.442 44.8604 101.298C44.7802 101.154 44.7402 100.913 44.7402 100.576V100.48C44.7402 100.176 44.8201 100.023 44.9805 100.023C45.0766 100.023 45.1323 99.9749 45.1484 99.8789C45.1805 99.7827 45.1973 99.6703 45.1973 99.542C45.1973 99.3977 45.1889 99.2294 45.1729 99.0371C45.1568 98.8609 45.1485 98.7083 45.1484 98.5801C45.1484 98.1952 45.2051 97.9788 45.3174 97.9307C45.3815 97.8986 45.4131 97.8661 45.4131 97.834C45.4291 97.7859 45.4375 97.7459 45.4375 97.7139C45.4375 97.6818 45.4291 97.6493 45.4131 97.6172C45.3812 97.5371 45.3652 97.4411 45.3652 97.3291V97.1846C45.3813 96.9922 45.4212 96.8156 45.4854 96.6553C45.5655 96.4949 45.6464 96.415 45.7266 96.415C45.8385 96.4149 45.8945 96.3582 45.8945 96.2461C45.8944 96.198 45.8787 96.1337 45.8467 96.0537V95.7891C45.8627 95.6931 45.8869 95.6287 45.9189 95.5967C45.9669 95.5645 46.0071 95.5002 46.0391 95.4043C46.0711 95.3082 46.0869 95.2039 46.0869 95.0918C46.0869 94.9797 46.1028 94.8595 46.1348 94.7314C46.1829 94.5871 46.2396 94.4663 46.3037 94.3701C46.3678 94.2739 46.4562 94.1056 46.5684 93.8652C46.6806 93.6087 46.7921 93.3355 46.9043 93.0469C46.9841 92.8416 47.0618 92.6533 47.1338 92.4805Z" fill="#797AAA"/>
<path d="M74.1094 96.5254C74.314 98.1401 74.4808 99.7579 74.6191 101.378C74.5005 101.384 74.3868 101.389 74.2783 101.395H73.5088C73.3007 101.379 73.1882 101.354 73.1719 101.322C73.1719 101.29 73.1477 101.266 73.0996 101.25C73.0515 101.234 72.9873 101.218 72.9072 101.202C72.8433 101.202 72.7713 101.194 72.6914 101.178C72.5952 101.146 72.5143 101.113 72.4502 101.081C72.3864 101.033 72.3547 100.985 72.3545 100.938V100.889C72.3546 100.825 72.402 100.72 72.498 100.576C72.5941 100.416 72.6424 100.296 72.6426 100.216C72.6426 100.152 72.6268 100.111 72.5947 100.095C72.4987 100.031 72.4502 99.9264 72.4502 99.7822V99.71C72.4503 99.5337 72.5228 99.3895 72.667 99.2773C72.7311 99.2132 72.7869 99.1407 72.835 99.0605C72.867 98.9644 72.8838 98.8844 72.8838 98.8203C72.8838 98.7403 72.9071 98.6601 72.9551 98.5801C72.9871 98.4999 73.0355 98.4274 73.0996 98.3633C73.1638 98.3152 73.2362 98.2185 73.3164 98.0742C73.3965 97.93 73.4607 97.7699 73.5088 97.5938C73.5569 97.4334 73.621 97.2968 73.7012 97.1846C73.7651 97.0727 73.8211 97.017 73.8691 97.0166C73.9654 97.0166 74.0304 96.9683 74.0625 96.8721C74.0933 96.7641 74.1082 96.6482 74.1094 96.5254Z" fill="#797AAA"/>
<path d="M71.0557 94.5869C70.7671 95.1641 70.5022 95.717 70.2617 96.2461C70.0212 96.7593 69.8447 97.1448 69.7324 97.4014C69.5079 98.0107 69.323 98.4594 69.1787 98.748C69.0504 99.0367 68.9622 99.189 68.9141 99.2051C68.8821 99.221 68.8578 99.2295 68.8418 99.2295C68.7939 99.2291 68.7462 99.1647 68.6982 99.0371C68.6822 98.989 68.6497 98.9648 68.6016 98.9648C68.5534 98.9649 68.4651 99.0133 68.3369 99.1094C68.321 99.1411 68.3049 99.1647 68.2891 99.1807C68.1928 99.2608 68.1204 99.3092 68.0723 99.3252C68.0403 99.3412 68.0085 99.3496 67.9766 99.3496C67.9445 99.3496 67.8961 99.3338 67.832 99.3018C67.7359 99.2697 67.5917 99.1971 67.3994 99.085C67.3673 99.0529 67.3264 99.0287 67.2783 99.0127C67.0861 98.9006 66.8859 98.7805 66.6777 98.6523C66.4854 98.5241 66.3246 98.4116 66.1963 98.3154C66.068 98.2352 65.9714 98.1786 65.9072 98.1465C65.8433 98.1146 65.8031 98.0986 65.7871 98.0986C65.7552 98.0987 65.7393 98.1071 65.7393 98.123C65.7392 98.1549 65.7308 98.1709 65.7148 98.1709C65.6828 98.1707 65.6427 98.1466 65.5947 98.0986C65.4986 98.0185 65.3785 97.8984 65.2344 97.7383C65.09 97.5779 64.9451 97.4088 64.8008 97.2324C64.6566 97.0562 64.5366 96.8878 64.4404 96.7275C64.3282 96.5351 64.2074 96.367 64.0791 96.2227C63.9669 96.0784 63.8712 95.9817 63.791 95.9336C63.727 95.8856 63.6627 95.8138 63.5986 95.7178C63.5505 95.6216 63.5264 95.5249 63.5264 95.4287C63.5264 95.309 63.5027 95.1891 63.457 95.0684C64.0881 95.0975 64.7326 95.1181 65.3662 95.126C65.7614 95.6483 66.1976 96.0547 66.6777 96.3428C67.0143 96.5351 67.2068 96.6392 67.2549 96.6553H67.3506C67.3665 96.6553 67.3748 96.6476 67.375 96.6318C67.375 96.6158 67.3592 96.5749 67.3271 96.5107C67.3111 96.4947 67.3027 96.4705 67.3027 96.4385C67.3028 96.4065 67.3112 96.3662 67.3271 96.3184C67.3592 96.2383 67.4151 96.1498 67.4951 96.0537C67.5752 95.9735 67.632 95.8694 67.6641 95.7412C67.7121 95.597 67.7354 95.4602 67.7354 95.332V95.2119C67.7354 95.1766 67.7372 95.1388 67.7422 95.0986C69.3589 95.0353 70.6193 94.8749 70.9414 94.5996C70.9958 94.5629 71.0499 94.5253 71.1045 94.4883C71.0885 94.521 71.0718 94.5537 71.0557 94.5869Z" fill="#797AAA"/>
<path d="M54.248 93.2021C54.6447 93.3586 55.0386 93.5445 55.4277 93.7686C55.9261 93.8433 56.4267 93.9254 56.9268 94.0068C56.9167 94.041 56.9112 94.0738 56.9111 94.1055C56.9111 94.1696 56.887 94.2505 56.8389 94.3467C56.8069 94.4265 56.7592 94.4986 56.6953 94.5625C56.6312 94.6427 56.4788 94.8917 56.2383 95.3086C56.0138 95.7255 55.7648 96.2067 55.4922 96.752C54.6745 98.3552 54.1533 99.157 53.9287 99.1572C53.8806 99.1572 53.8481 99.1247 53.832 99.0605C53.8 99.0288 53.7522 99.0128 53.6885 99.0127C53.6245 98.9967 53.576 98.9883 53.5439 98.9883C53.4638 98.9883 53.3829 98.9967 53.3027 99.0127C53.2228 99.0287 53.1509 99.0371 53.0869 99.0371C53.0067 99.0371 52.9426 99.0204 52.8945 98.9883C52.7823 98.9241 52.6457 98.7718 52.4854 98.5312C52.3892 98.371 52.3008 98.2185 52.2207 98.0742C52.1407 97.9302 52.1007 97.8343 52.1006 97.7861C52.1006 97.6899 52.1647 97.505 52.293 97.2324C52.4052 96.9599 52.5419 96.6717 52.7021 96.3672C52.8465 96.0465 52.9821 95.7733 53.1104 95.5488C53.2547 95.3083 53.3513 95.1885 53.3994 95.1885C53.4475 95.1885 53.4958 95.1318 53.5439 95.0195C53.608 94.8914 53.6564 94.7312 53.6885 94.5391C53.7205 94.3627 53.7679 94.1778 53.832 93.9854C53.9122 93.7929 53.9931 93.6322 54.0732 93.5039C54.1449 93.4036 54.202 93.3025 54.248 93.2021Z" fill="#797AAA"/>
<path d="M44.2031 92.4355C44.5343 92.4622 44.8671 92.4753 45.2002 92.4844C45.1469 92.5313 45.0916 92.5849 45.0283 92.6387C44.7077 92.9112 44.3387 93.2077 43.9219 93.5283C43.6915 93.699 43.47 93.8647 43.2578 94.0264C43.4204 93.5319 43.5824 93.0381 43.7451 92.5459C43.8961 92.5027 44.0521 92.4787 44.2031 92.4355Z" fill="#797AAA"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M25.9209 94.7656C25.8428 94.9642 25.789 95.113 25.7607 95.2119C25.7287 95.324 25.6887 95.4283 25.6406 95.5244C25.5926 95.6206 25.5526 95.6931 25.5205 95.7412C25.4884 95.8054 25.3918 96.0302 25.2314 96.415C25.0711 96.7998 24.8787 97.2571 24.6543 97.7861C24.4459 98.3152 24.2451 98.804 24.0527 99.2529C23.8763 99.7019 23.7481 99.9992 23.668 100.144C23.6039 100.272 23.5481 100.416 23.5 100.576C23.4519 100.72 23.4277 100.833 23.4277 100.913C23.4277 100.993 23.4119 101.09 23.3799 101.202C23.3639 101.314 23.3396 101.41 23.3076 101.49C23.2868 101.525 23.2603 101.575 23.2275 101.64C22.3428 101.644 21.4581 101.642 20.5732 101.637C20.5198 101.526 20.4932 101.429 20.4932 101.346C20.4932 101.266 20.5258 101.201 20.5898 101.153C20.6218 101.089 20.6451 101.017 20.6611 100.938L20.6855 100.816L20.6611 100.721C20.6451 100.673 20.6377 100.633 20.6377 100.601C20.6377 100.504 20.6935 100.432 20.8057 100.384C20.8697 100.352 20.9181 100.32 20.9502 100.288C20.9662 100.256 20.9746 100.223 20.9746 100.191V100.167C20.9586 100.151 20.9502 100.135 20.9502 100.119C20.9502 100.087 20.9586 100.063 20.9746 100.047C21.0066 100.015 21.0544 99.9831 21.1182 99.9512C21.1822 99.9352 21.2306 99.8949 21.2627 99.8311C21.3108 99.7509 21.335 99.67 21.335 99.5898C21.335 99.5259 21.3509 99.4539 21.3828 99.374C21.4149 99.2778 21.4558 99.2053 21.5039 99.1572C21.568 99.077 21.6396 98.892 21.7197 98.6035C21.8159 98.2989 21.8884 97.9941 21.9365 97.6895C21.9845 97.4813 22.0087 97.3212 22.0088 97.209C22.0088 97.161 22.0003 97.1209 21.9844 97.0889V97.0645C21.9844 97.0163 22.0085 96.9439 22.0566 96.8477C22.1047 96.7515 22.1773 96.6557 22.2734 96.5596C22.4177 96.3993 22.5534 96.1984 22.6816 95.958C22.8259 95.7015 22.9384 95.4608 23.0186 95.2363C23.0204 95.2309 23.0207 95.2251 23.0225 95.2197C23.9916 95.0959 24.958 94.946 25.9209 94.7656ZM25.0879 95.6689C25.0558 95.6689 25.0233 95.6857 24.9912 95.7178C24.9593 95.7497 24.9434 95.7815 24.9434 95.8135V95.9336C24.9594 95.9496 24.9752 95.958 24.9912 95.958C25.0233 95.958 25.0474 95.9496 25.0635 95.9336C25.0955 95.9015 25.1281 95.8615 25.1602 95.8135C25.176 95.7974 25.1836 95.7731 25.1836 95.7412V95.7178C25.1676 95.6858 25.1358 95.669 25.0879 95.6689Z" fill="#A29EA1"/>
<path d="M12.6553 95.6865C13.7392 95.7105 14.8231 95.7154 15.9062 95.6992C15.8895 95.7448 15.8718 95.7913 15.8506 95.8379C15.7705 95.9979 15.6745 96.134 15.5625 96.2461C15.4664 96.3261 15.3779 96.3671 15.2979 96.3672H15.2256C15.1454 96.3672 15.0404 96.4145 14.9121 96.5107C14.7999 96.6069 14.6958 96.7194 14.5996 96.8477C14.5355 96.9598 14.504 97.0482 14.5039 97.1123V97.1846C14.5039 97.2166 14.4796 97.2567 14.4316 97.3047C14.3835 97.3528 14.3111 97.4095 14.2148 97.4736C14.0066 97.5698 13.9024 97.6898 13.9023 97.834C13.9023 97.8821 13.9107 97.922 13.9268 97.9541C13.9428 98.0181 13.9427 98.0825 13.9268 98.1465C13.9268 98.1945 13.9024 98.2186 13.8545 98.2188C13.8065 98.2188 13.7664 98.2431 13.7344 98.291C13.7023 98.3231 13.6855 98.3714 13.6855 98.4355C13.6855 98.4996 13.6614 98.5554 13.6133 98.6035C13.5813 98.6515 13.5337 98.6757 13.4697 98.6758C13.4056 98.6758 13.3489 98.7083 13.3008 98.7725C13.2688 98.8205 13.2529 98.8848 13.2529 98.9648C13.2529 99.0447 13.237 99.1167 13.2051 99.1807C13.173 99.2448 13.1321 99.2773 13.084 99.2773C13.036 99.2774 12.9799 99.2932 12.916 99.3252C12.8519 99.3572 12.7877 99.3973 12.7236 99.4453C12.6595 99.5094 12.5795 99.582 12.4834 99.6621C12.3873 99.7262 12.2989 99.7904 12.2188 99.8545C12.1226 99.9186 12.0417 99.9986 11.9775 100.095C11.9134 100.191 11.8818 100.272 11.8818 100.336C11.8818 100.416 11.8576 100.504 11.8096 100.601C11.7615 100.681 11.6973 100.745 11.6172 100.793C11.537 100.857 11.4004 101.01 11.208 101.25C11.1575 101.313 11.1064 101.379 11.0537 101.446C10.1403 101.418 9.22694 101.385 8.31348 101.354C8.32682 101.328 8.33859 101.302 8.3457 101.273C8.37768 101.21 8.41793 101.162 8.46582 101.13L8.51367 101.105L8.58594 101.13H8.61035C8.62635 101.13 8.64221 101.121 8.6582 101.105V101.058C8.6582 101.01 8.65077 100.961 8.63477 100.913V100.841C8.63481 100.648 8.8754 100.288 9.35645 99.7588C9.56478 99.5184 9.76471 99.3017 9.95703 99.1094C10.1653 98.9171 10.3178 98.7962 10.4141 98.748C10.4942 98.716 10.5751 98.6435 10.6553 98.5312C10.7353 98.4031 10.7996 98.2671 10.8477 98.123C10.8957 97.9788 10.9515 97.8663 11.0156 97.7861C11.0798 97.6899 11.1365 97.6416 11.1846 97.6416H11.2324C11.3446 97.6416 11.473 97.5858 11.6172 97.4736C11.7615 97.3614 11.8897 97.2248 12.002 97.0645C12.0821 96.9362 12.1221 96.8238 12.1221 96.7275V96.6318C12.106 96.5837 12.0986 96.5428 12.0986 96.5107C12.0988 96.3505 12.1786 96.2221 12.3389 96.126C12.4349 96.0619 12.5076 95.9902 12.5557 95.9102C12.6036 95.8502 12.6334 95.7989 12.6455 95.7568L12.6553 95.6865Z" fill="#A29EA1"/>
<path d="M34.1182 91.0312C34.2943 91.3254 34.4763 91.5839 34.6631 91.8105C34.6546 91.8355 34.6476 91.8635 34.6367 91.8926C34.6047 92.0047 34.5572 92.1332 34.4932 92.2773C34.429 92.4217 34.3408 92.6307 34.2285 92.9033C34.1323 93.1758 34.0356 93.4482 33.9395 93.7207C33.8433 94.0092 33.7233 94.33 33.5791 94.6826C33.4508 95.0354 33.3216 95.3401 33.1934 95.5967C33.0811 95.8532 32.9613 96.1422 32.833 96.4629C32.7047 96.7836 32.6081 97.0567 32.5439 97.2812C32.4798 97.5055 32.3998 97.7298 32.3037 97.9541C32.2235 98.1786 32.1511 98.3551 32.0869 98.4834C31.9747 98.7078 31.839 98.9886 31.6787 99.3252C31.5344 99.662 31.422 99.9592 31.3418 100.216C31.3257 100.28 31.2856 100.336 31.2217 100.384C31.1575 100.432 31.1008 100.456 31.0527 100.456C30.9887 100.456 30.9244 100.48 30.8604 100.528C30.8124 100.576 30.7723 100.632 30.7402 100.696C30.6921 100.825 30.6039 100.889 30.4756 100.889C30.4436 100.889 30.4035 100.881 30.3555 100.865H30.2832C30.2351 100.865 30.1868 100.873 30.1387 100.889C30.0587 100.905 30.0102 100.945 29.9941 101.009C29.9621 101.057 29.9221 101.089 29.874 101.105C29.8421 101.121 29.8102 101.13 29.7783 101.13H29.6816C29.5854 101.082 29.473 100.945 29.3447 100.721C29.2004 100.496 29.089 100.271 29.0088 100.047C28.9287 99.8227 28.8878 99.6627 28.8877 99.5664V99.5176C28.9196 99.4696 28.9439 99.422 28.96 99.374V99.2051C28.976 99.173 29.0002 99.133 29.0322 99.085C29.0643 99.0369 29.1127 98.9969 29.1768 98.9648C29.2249 98.9328 29.2648 98.8603 29.2969 98.748C29.3289 98.6198 29.3447 98.4832 29.3447 98.3389C29.3448 98.2107 29.3689 98.0823 29.417 97.9541C29.4651 97.8259 29.5218 97.7301 29.5859 97.666C29.65 97.6179 29.7058 97.5295 29.7539 97.4014C29.802 97.257 29.8345 97.0963 29.8506 96.9199C29.8666 96.7596 29.8908 96.623 29.9229 96.5107C29.9709 96.3827 30.0109 96.3109 30.043 96.2949C30.091 96.2788 30.131 96.2063 30.1631 96.0781C30.1951 95.9339 30.2109 95.7656 30.2109 95.5732C30.2109 95.3007 30.2435 95.0842 30.3076 94.9238C30.3718 94.7635 30.5158 94.5628 30.7402 94.3223C30.9647 94.0817 31.0929 93.921 31.125 93.8408C31.157 93.7609 31.1728 93.7049 31.1729 93.6729C31.1729 93.6375 31.1648 93.6049 31.1504 93.5742C32.3307 93.2194 33.4527 92.6049 33.9238 91.1396C33.9451 91.076 34.1125 91.0327 34.1182 91.0312Z" fill="#A29EA1"/>
<path d="M7.32422 95.4453C8.04288 95.4917 8.76235 95.5342 9.48242 95.5703C9.42196 95.5874 9.36362 95.604 9.30762 95.6211C8.8749 95.7493 8.53833 95.8134 8.29785 95.8135C8.08938 95.8135 7.96119 95.7652 7.91309 95.6689C7.89701 95.6209 7.84857 95.5893 7.76855 95.5732C7.70441 95.5412 7.63194 95.5244 7.55176 95.5244C7.48655 95.5244 7.41147 95.4968 7.32422 95.4453Z" fill="#A29EA1"/>
<path d="M38.335 93.2412C40.1356 93.067 42.0389 92.2174 43.748 92.543C43.5854 93.035 43.4252 93.5283 43.2637 94.0215C43.075 94.1652 42.8941 94.3059 42.7197 94.4424C42.3509 94.731 42.0935 94.9317 41.9492 95.0439C41.7569 95.1882 41.5644 95.2923 41.3721 95.3564C41.1797 95.4205 41.0115 95.4531 40.8672 95.4531H40.7705C40.6265 95.437 40.5547 95.3644 40.5547 95.2363C40.5547 95.1401 40.5379 95.0918 40.5059 95.0918C40.4738 95.0921 40.4261 95.1246 40.3623 95.1885C40.2822 95.2525 40.1937 95.3 40.0977 95.332C40.0175 95.3481 39.945 95.3564 39.8809 95.3564H39.7852C39.689 95.3404 39.6407 95.3004 39.6406 95.2363C39.6406 95.2043 39.5765 95.1476 39.4482 95.0674C39.32 94.9712 39.1675 94.8755 38.9912 94.7793C38.7027 94.651 38.5177 94.547 38.4375 94.4668C38.3734 94.3866 38.3418 94.29 38.3418 94.1777L38.3652 94.0576C38.3652 93.9293 38.3578 93.8169 38.3418 93.7207C38.3258 93.6085 38.2932 93.5202 38.2451 93.4561C38.1971 93.408 38.1729 93.368 38.1729 93.3359C38.1729 93.2879 38.213 93.2637 38.293 93.2637C38.3146 93.2637 38.3277 93.2557 38.335 93.2412Z" fill="#A29EA1"/>
<path d="M62.9102 94.3379C63.5699 94.3592 64.2294 94.3789 64.8887 94.4004C65.0435 94.6685 65.2052 94.9152 65.376 95.1396C64.7406 95.1308 64.0941 95.1086 63.4609 95.0781C63.3709 94.8335 63.1877 94.5866 62.9102 94.3379Z" fill="#7D89C1"/>
<path d="M70.9541 94.5938C70.6683 94.8862 69.3901 95.0532 67.7402 95.1162C67.7443 95.0784 67.7513 95.0384 67.7598 94.9961C67.8078 94.8841 67.8638 94.7956 67.9277 94.7314C67.9918 94.6834 68.0402 94.619 68.0723 94.5391C68.0796 94.5268 68.0836 94.5135 68.0898 94.502C69.0455 94.5324 70.0004 94.565 70.9541 94.5938Z" fill="#7D89C1"/>
<path d="M68.3213 64.3398C68.4783 64.4342 68.6451 64.5228 68.8232 64.6035C69.4989 64.91 70.194 65.1795 70.8965 65.4258C71.1283 67.4689 69.5045 67.3029 68.2842 67.4238C67.3883 67.5128 66.5881 68.1238 65.7188 68.3252C65.694 68.3137 65.6693 68.3015 65.6445 68.29C65.1845 68.0776 64.7558 67.9752 64.3652 67.9756C64.3856 67.9479 64.4067 67.9199 64.4287 67.8906C65.8709 66.8986 66.7791 65.2536 68.3213 64.3398Z" fill="#FEDAA5"/>
<path d="M86.0889 27.4473C86.6765 29.2825 87.2656 31.1178 87.8994 32.9375C88.0816 33.4609 88.3187 33.9669 88.5752 34.4648C88.4307 34.8715 88.3455 35.3473 88.3281 35.9062C88.3301 36.0398 89.6703 38.7041 87.1836 37.1748C87.0754 37.1102 86.9238 37.1729 86.7832 37.1729C85.966 36.6297 85.076 36.584 84.1748 36.6133C84.0815 36.0218 83.9682 35.4274 83.7764 34.8662C83.4948 34.0457 83.1707 33.2458 82.8164 32.4609C83.4383 32.5229 84.0748 32.4687 84.2617 31.9004C84.7703 30.3636 85.4117 28.8962 86.0889 27.4473Z" fill="#FCE8EC"/>
<path d="M86.0586 16.209C86.1954 16.2204 86.3353 16.229 86.4775 16.2314C87.1721 16.2437 87.8672 16.2689 88.5605 16.2383C89.4093 16.2017 90.1981 16.3873 90.8779 16.874C92.7169 18.1932 94.5736 19.4919 96.3428 20.9004C97.669 21.957 98.882 23.1573 100.201 24.3564C100.28 24.4282 100.39 24.4521 100.495 24.4346C101.133 24.3284 101.699 24.6525 102.271 25.043C102.396 25.129 102.439 25.2946 102.372 25.4316C102.196 25.7935 102.02 26.1528 101.837 26.5293C101.758 26.6929 101.836 26.8886 102.002 26.9639C102.529 27.2029 102.993 27.5152 103.427 27.8604C102.879 29.1069 101.713 30.176 100.878 31.3379C100.434 31.6626 100.033 32.0744 99.5459 32.3018C98.9597 32.5849 98.3816 32.6773 97.8047 32.667C97.6905 32.3776 97.5316 32.1062 97.3281 31.8496C96.9864 31.4202 96.6423 30.9794 96.2344 30.6172C93.5547 28.2339 90.5196 26.4342 87.1484 25.2168C87.4391 24.6075 87.7285 23.9976 88.0068 23.3818C88.7429 21.7473 87.0436 20.1768 86.8379 18.5098C86.7348 17.7075 86.351 16.9653 86.0586 16.209Z" fill="#FCE8EC"/>
<path d="M61.421 71.4238C61.356 71.7268 61.139 72.1165 61.2472 72.3222C61.3069 72.4317 61.3645 72.498 61.419 72.5331C61.1652 73.2363 60.9015 73.9365 60.6183 74.6269C60.3423 75.3004 59.8249 75.6547 59.0645 75.7695C58.877 75.7978 58.7219 75.6352 58.7374 75.4462C58.7887 74.8212 58.823 74.217 58.9249 73.6249C58.9934 73.228 59.0771 72.8327 59.1632 72.4384C59.2263 72.4355 59.2895 72.4335 59.3526 72.4306C59.8397 72.4089 59.9488 73.9784 60.544 72.6581C60.7497 72.2035 61.1287 71.8351 61.421 71.4238Z" fill="#F18928"/>
<path d="M107.429 38.4159C107.418 38.5647 107.413 38.7149 107.414 38.8661C107.416 39.0049 107.436 39.1445 107.459 39.2968C107.483 39.4514 107.615 39.5673 107.772 39.5673H110.507C110.678 39.5673 110.819 39.7076 110.809 39.8788C110.745 40.9732 110.357 41.9167 109.691 42.7079C108.79 43.7787 108.422 45.0515 107.997 46.329C107.377 48.1916 107.336 50.1121 107.483 52.0175C107.636 54.0036 106.901 55.5873 105.715 57.0644C104.784 58.2246 103.881 59.41 102.955 60.5751C102.006 61.7695 101.651 63.1185 101.753 64.6347C101.884 66.578 102.341 68.4518 102.766 70.3437C103.112 71.888 103.226 73.4838 103.436 75.0575C103.442 75.1019 103.442 75.1469 103.44 75.1982C103.429 75.4156 103.189 75.5544 102.987 75.4736C102.683 75.3516 102.405 75.2433 102.169 75.0751C100.987 74.2319 100.259 73.0293 99.6632 71.7382C99.3761 71.1152 99.0974 70.4876 98.8185 69.8681C98.7849 69.7938 98.7224 69.736 98.6466 69.706C97.847 69.3893 97.611 69.1868 97.3829 68.6464C97.3336 68.5297 97.2224 68.4496 97.0958 68.4482C95.8492 68.4354 94.6089 68.5599 93.4151 68.9736C91.2758 69.714 89.1323 70.4478 87.0235 71.2665C84.6735 72.1794 82.2411 72.3642 79.7931 72.0693C77.4492 71.7866 75.1323 71.277 72.8019 70.8818C72.0202 70.7496 71.2288 70.6713 70.4435 70.5527C70.2487 70.5233 70.0549 70.4472 69.8761 70.3603C69.4646 70.1598 69.0554 69.9541 68.6456 69.7499C68.7407 69.6825 68.8442 69.6162 68.9552 69.5507C70.2179 68.8068 72.0462 67.8503 72.1046 65.8251C75.2412 66.7851 78.4785 66.8744 81.7247 66.7597H81.7267L81.7276 66.7587C83.1351 66.7587 84.5428 66.7746 85.9503 66.749C86.3264 66.7416 86.7408 66.6868 87.0704 66.5204C88.8348 65.6307 90.6106 64.7578 92.3175 63.7665C94.0779 62.7448 94.226 62.2368 93.7042 60.246C93.2337 58.4532 92.3653 56.8311 91.4464 55.245C91.1174 54.6767 90.81 54.1041 90.5401 53.5204C90.9678 53.3711 91.3882 53.1531 91.796 52.8368C92.5321 52.2633 93.5171 51.96 94.1124 51.2997C94.4073 50.9727 94.7065 50.67 95.0206 50.3984C95.1236 50.6824 95.2285 50.9657 95.336 51.248C96.081 53.2023 96.9976 55.0931 97.8712 56.996C97.9818 57.236 98.1658 57.4429 98.3702 57.6738C98.5162 57.8384 98.788 57.8141 98.8936 57.621C99.8469 55.8787 100.561 54.2017 101.012 52.413C101.857 49.0583 102.161 45.6539 102.241 42.2284C102.593 42.3173 102.97 42.5644 103.292 42.5536C105.468 42.4561 105.815 40.4636 106.832 39.2079C107.041 38.9498 107.238 38.6845 107.429 38.4159Z" fill="#F18928"/>
<path d="M114.025 29.6337C115.368 29.3421 116.641 29.1814 118.055 29.3769C118.063 29.3781 118.071 29.3812 118.078 29.3857C118.117 29.4105 118.113 29.4678 118.072 29.4882C117.75 29.6485 117.432 29.8167 117.103 29.9628C114.572 31.0863 112.151 32.3946 110.094 34.2743C109.84 34.506 109.595 34.7489 109.358 34.9999C109.893 33.8138 110.342 32.5883 110.75 31.3603C110.863 31.0233 110.947 30.6833 111.009 30.3417C112.012 30.0936 113.016 29.8525 114.025 29.6337Z" fill="#F18928"/>
<path d="M71.3389 18.3457C71.1964 19.8452 70.9443 21.3353 70.4609 22.7959C70.212 23.5644 70.2441 24.4414 69.9844 25.21C69.8178 25.7098 69.795 26.2273 69.8066 26.75C67.9507 27.9138 66.2127 29.2378 64.6133 30.7393C62.2541 32.9527 60.2147 35.398 58.8398 38.3184C58.2911 38.6734 57.5736 38.7428 57.0381 38.8818C55.4911 39.2868 53.921 39.512 52.3418 39.6191C52.8139 38.2099 53.3718 36.8181 54.0068 35.4424C55.003 33.2835 56.0376 31.1415 57.082 28.9375C57.1358 28.8244 57.2532 28.7588 57.3779 28.7471C58.2329 28.6673 58.3722 27.7793 58.7207 27.1299C58.9488 26.7045 59.1191 26.248 59.3057 25.7705C59.3849 25.5683 59.6421 25.5129 59.7998 25.6621C59.9129 25.7695 60.0236 25.8749 60.1396 25.9854C60.261 26.1007 60.4524 26.0991 60.5762 25.9863C60.8711 25.7171 61.173 25.4655 61.3896 25.1553C62.2767 23.8875 63.1486 22.607 63.9561 21.2891C64.3285 20.6809 64.743 20.2991 65.4561 20.0996C67.4301 19.5474 69.3886 18.9414 71.3389 18.3457Z" fill="#80C9C1"/>
<path d="M75.1394 35.291C75.0235 35.233 74.9075 35.1764 74.7907 35.1279C75.0072 34.6949 74.8017 34.0128 75.5378 33.8721C75.5591 33.8689 75.5831 33.844 75.6081 33.8105C75.4524 34.3042 75.2947 34.7973 75.1394 35.291Z" fill="#95C7DB"/>
<path d="M80.7165 7.85352C80.9032 8.51085 81.0907 9.16819 81.2946 9.82031C81.7075 11.1395 82.1434 12.4546 82.6433 13.7432C83.2538 15.3192 84.447 16.0643 86.0163 16.2061C86.3102 16.9747 86.7019 17.7303 86.8064 18.5439C87.0122 20.2109 88.7113 21.7806 87.9753 23.415C87.7028 24.0178 87.4199 24.6157 87.1355 25.2129C87.1198 25.2072 87.1043 25.2009 87.0886 25.1953C86.5851 25.0142 86.0311 24.9743 85.5007 24.8691L85.3552 25.0205C85.3338 25.0429 85.3261 25.075 85.3356 25.1045C85.5871 25.879 85.8344 26.655 86.0827 27.4307C85.3962 28.8988 84.7451 30.3843 84.2302 31.9346C84.0479 32.4881 83.439 32.5532 82.8327 32.499C81.8118 30.2302 80.5352 28.0868 79.2868 25.9307C79.0307 25.4876 78.5867 25.5011 78.3122 25.9307C78.1714 26.1521 78.0564 26.3972 77.9694 26.6455C77.5578 27.8165 77.1378 28.985 76.7605 30.167C76.4048 31.2843 76.0519 32.4031 75.6989 33.5215C75.2801 32.9425 74.8409 32.3841 74.3903 31.8369C74.3795 31.8261 74.1196 31.9669 74.0222 32.0752C72.8082 33.4759 71.4868 34.5663 69.9147 34.8643C70.8254 32.0114 71.7343 29.1578 72.6433 26.3047C72.6624 26.2443 72.6775 26.1828 72.6911 26.1191C72.7268 25.953 72.6189 25.7846 72.4499 25.7666C71.7773 25.6956 71.1923 25.9183 70.6325 26.249C70.3585 26.411 70.0863 26.5755 69.8171 26.7441C69.8053 26.2204 69.8279 25.702 69.9948 25.2012C70.2546 24.4326 70.2116 23.5665 70.4714 22.7871C70.9491 21.3335 71.1982 19.8454 71.3405 18.3467C71.7375 18.2254 72.1346 18.1043 72.531 17.9844C72.6048 17.962 72.6784 17.9192 72.7116 17.8496C72.7157 17.8411 72.7185 17.8326 72.7194 17.8242C72.8934 16.4219 73.007 16.3038 74.4255 16.0713C75.9362 15.8241 76.8883 15.0164 77.1433 13.4893C77.2069 13.1069 77.2593 12.7224 77.3044 12.3369C78.0135 12.0787 78.7815 11.4453 79.489 11.6914C79.7163 10.9553 80.3442 10.4465 80.3659 9.52637C80.379 9.08735 80.4605 8.33173 80.7165 7.85352Z" fill="#95C7DB"/>
<path d="M77.3047 12.3383C77.4396 11.1866 77.5099 10.0237 77.6211 8.86763C77.7448 7.56915 77.8361 6.26693 77.9893 4.97212C78.0804 4.20003 78.2881 3.81709 78.7812 3.50044C78.9278 3.40642 79.1197 3.47256 79.1992 3.6274C79.4632 4.14169 79.7537 4.647 79.9287 5.18892C80.2137 6.07242 80.465 6.96642 80.7188 7.85982C80.4668 8.33838 80.39 9.08821 80.377 9.52486C80.3445 10.445 79.7273 10.9538 79.5 11.6899C78.7889 11.4426 78.0168 12.0826 77.3047 12.3383Z" fill="#87ADD4"/>
<path d="M105.273 29.5137C105.64 29.8368 106.033 30.1323 106.422 30.4297C106.999 30.8715 107.624 31.036 108.299 30.9766C107.531 33.1799 106.034 35.0599 104.072 36.5557C103.147 37.2576 102.586 38.0051 102.225 38.79C102.222 38.7751 102.218 38.7595 102.215 38.7432C102.018 38.743 101.836 38.7164 101.664 38.748C100.783 38.9047 99.8939 39.0353 99.0264 39.252C97.1616 39.7182 95.3677 40.3826 93.6953 41.3457C92.7593 41.8853 92.5919 42.2406 92.7793 43.2939C92.8467 43.6744 92.9149 44.059 93.0361 44.4248C93.3119 45.2619 93.5803 46.1025 93.8516 46.9414C92.4833 47.7794 90.9976 48.2913 89.3887 48.4316C89.3068 47.8612 89.1535 47.3045 88.9395 46.7637C89.0937 46.7269 89.2625 46.7011 89.4473 46.6875C90.6813 46.6009 89.9018 45.4316 90.2266 44.8037C90.9734 43.3424 91.4829 41.7298 91.4072 40.0195C91.8835 39.8464 92.3923 39.7383 92.8145 39.4785C94.2865 38.5693 95.3364 37.1945 97.2305 36.6748C98.2696 36.3826 99.3305 36.4575 100.305 36.1221C102.091 35.5051 102.005 33.5678 102.795 32.2363C103.769 31.457 104.213 30.3851 104.775 29.3027C104.805 29.247 104.83 29.1879 104.852 29.127C104.992 29.2567 105.131 29.3874 105.273 29.5137Z" fill="#FEDAA5"/>
<path d="M102.829 32.2188C102.039 33.5502 102.11 35.4951 100.324 36.123C99.35 36.4586 98.2843 36.3796 97.2451 36.6719C95.3507 37.1915 94.2974 38.5702 92.8252 39.4795C92.4032 39.7391 91.8754 39.8494 91.4102 40.0225C89.8622 39.0807 88.3146 38.1107 86.7559 37.1689C86.8965 37.1689 87.0803 37.1041 87.1777 37.1689C89.5797 38.6513 88.2942 36.0346 88.293 35.9023C88.3109 35.3273 88.4003 34.8401 88.5537 34.4258C88.7791 34.8658 89.0215 35.2989 89.2598 35.7295C89.7168 36.5568 90.4439 36.8606 91.418 36.7725C93.8245 36.5561 95.7936 35.3008 97.8271 34.2871C97.9197 34.2409 97.984 34.1519 97.9932 34.0488C98.0386 33.5374 97.9685 33.0812 97.8027 32.6631C98.3701 32.6696 98.9398 32.5761 99.5186 32.2979C100.006 32.0598 100.406 31.6587 100.85 31.334C101.51 31.6263 102.18 31.9265 102.829 32.2188Z" fill="#FFF1CF"/>
<path d="M84.1738 36.6083C85.0729 36.5793 85.9611 36.6231 86.7764 37.1601C88.327 38.1035 89.8467 39.0761 91.4082 40.0195C91.484 41.7327 91.005 43.3094 90.2568 44.7841C89.9319 45.4131 90.712 46.5839 89.4766 46.6708C89.2787 46.6835 89.0985 46.7106 88.9355 46.7509C88.8199 46.4601 88.6881 46.1734 88.5391 45.8925C88.0478 44.9662 87.2871 44.5281 86.2617 44.5146C84.1824 44.4877 82.1344 44.6626 80.125 45.1181C80.0508 43.6915 79.9428 42.2667 79.7832 40.8437C80.9556 40.5086 82.0693 40.0405 83.0889 39.3505C83.9686 38.7545 84.3744 37.9372 84.2188 36.8896C84.2048 36.7959 84.1887 36.7023 84.1738 36.6083Z" fill="#A29EA1"/>
<path d="M57.3994 42.5918C57.1295 43.8227 56.9645 45.0781 56.8779 46.3633C56.4223 46.4052 55.9361 46.4087 55.4141 46.3633C53.896 46.233 52.3672 46.2186 50.834 46.2588C50.9754 45.0393 51.1912 43.8351 51.4736 42.6445C53.4494 42.6735 55.4247 42.6653 57.3994 42.5918Z" fill="#797AAA"/>
<path d="M111.026 30.3379C110.965 30.6699 110.882 30.9998 110.772 31.3271C110.357 32.5649 109.903 33.8002 109.362 34.9951C108.94 35.4417 108.541 35.9144 108.159 36.3936C107.69 36.9811 107.481 37.6783 107.429 38.4131C107.244 38.6714 107.055 38.9271 106.854 39.1758C105.836 40.4314 105.49 42.4336 103.314 42.5205C102.981 42.5366 102.596 42.2909 102.241 42.2041C102.266 41.1404 102.27 40.0747 102.261 39.0078C102.261 38.9431 102.243 38.8769 102.225 38.7891C102.586 38.0047 103.147 37.2572 104.071 36.5557C106.034 35.0594 107.531 33.1801 108.298 30.9766C108.479 30.9606 108.664 30.9302 108.853 30.8828C109.577 30.7004 110.301 30.5173 111.026 30.3379Z" fill="#F8B570"/>
<path d="M74.4004 31.8248C74.8494 32.37 75.2876 32.9258 75.7051 33.5025C75.6723 33.6064 75.6392 33.7102 75.6064 33.8141C75.5862 33.8372 75.5669 33.8552 75.5479 33.86C74.812 34.0007 75.0279 34.6819 74.8008 35.1148C74.9159 35.1626 75.0303 35.2189 75.1445 35.276C74.754 36.5172 74.3634 37.7583 73.9736 38.9996C73.1929 38.9743 72.4115 38.9489 71.6289 38.9254C71.196 38.8605 70.763 38.7853 70.3301 38.7203C69.8483 38.4429 69.3537 38.2791 68.8516 38.1822C69.2066 37.0724 69.5636 35.9632 69.918 34.8531C71.4862 34.551 72.8065 33.4611 74.0322 32.0621C74.1298 31.9539 74.3896 31.814 74.4004 31.8248Z" fill="#87ADD4"/>
<path d="M103.369 27.8145C103.906 28.2347 104.397 28.7067 104.896 29.167C104.876 29.2178 104.856 29.2675 104.831 29.3145C104.279 30.4077 103.77 31.4687 102.796 32.248C102.146 31.9558 101.476 31.6086 100.826 31.3164C101.669 30.1437 102.832 29.0742 103.369 27.8145Z" fill="#F8C9D1"/>
<path d="M43.0465 59.1211C43.0181 59.1099 42.9932 59.09 42.9781 59.0615C42.9519 59.0117 42.96 58.9502 42.9986 58.9092C43.015 58.8918 43.0313 58.8746 43.0475 58.8574C43.0474 58.9453 43.0465 59.0332 43.0465 59.1211Z" fill="#ADA0CC"/>
<path d="M72.7344 42.2163C74.3798 41.4261 75.5054 41.8915 76.0791 43.1148C76.5278 44.0871 76.7815 45.0889 76.9297 46.107C72.3958 47.9277 69.1392 51.2447 67.5947 55.4605C66.4595 55.3148 65.2488 55.9371 64.1504 55.4956L63.8604 55.3667C63.3429 55.162 62.8803 55.2435 62.4385 55.4214C62.6012 54.8455 62.7655 54.2703 62.9248 53.6939C62.9909 53.4556 63.0095 53.2046 63.0332 52.9273C63.0497 52.7341 62.8915 52.5665 62.6982 52.5816C61.0988 52.7061 59.6437 53.0114 58.4287 54.0523C58.059 54.3688 57.7396 54.7645 57.6836 55.2281C55.6803 55.2859 53.7026 55.491 52.0039 56.8833C51.7648 57.08 51.48 57.2334 51.1924 57.3833C50.8958 57.3739 50.5859 57.4062 50.2676 57.4683C48.3488 57.8428 46.4531 58.33 44.5479 58.773C44.0718 58.8839 43.5987 59.0079 43.125 59.1265C43.0976 59.1333 43.0698 59.1295 43.0449 59.1197C43.0449 59.0318 43.0459 58.9439 43.0459 58.856C43.2552 58.634 43.4554 58.4243 43.6924 58.2271C45.1345 57.0277 46.6102 55.8659 48.0352 54.647C48.8853 53.9189 49.6783 53.1215 50.4648 52.3238C50.6229 52.1634 50.722 51.8556 50.7061 51.6255C50.5799 49.8027 50.6295 48.0157 50.833 46.2603C52.3625 46.2189 53.8875 46.2318 55.4033 46.3618C55.9294 46.4076 56.4192 46.4047 56.8779 46.3618C56.8352 46.995 56.8103 47.6355 56.8027 48.2837H56.8047C57.0899 48.2605 57.2907 48.2589 57.4854 48.2271C59.9654 47.822 62.2763 46.915 64.5088 45.8062C65.2158 45.4549 65.8775 44.9995 66.5195 44.5357C66.7645 44.3582 66.9713 44.0486 67.0645 43.7573C67.0974 43.6549 67.1292 43.5521 67.1621 43.4497C69.054 43.7 70.9797 43.0666 72.7344 42.2163Z" fill="#6E5EA6"/>
<path d="M57.6846 55.2363C57.6715 55.351 57.6748 55.4699 57.6973 55.5928C57.9754 57.12 58.6287 58.4543 59.9912 59.333C60.3596 59.5703 60.7475 59.6603 61.1895 59.4658C61.2658 59.4323 61.3199 59.3634 61.3428 59.2832C61.7105 57.9935 62.0752 56.7131 62.4365 55.4346C62.9214 55.2387 63.4311 55.1586 64.0107 55.4443C65.1373 56.0035 66.4076 55.3203 67.5938 55.4717C67.1318 56.7352 66.8245 58.079 66.6816 59.4873C65.5378 59.8531 64.4835 60.478 63.4268 61.1494C62.615 61.6581 61.7272 61.7879 60.8721 61.5391C59.1295 61.044 57.4776 61.4727 56.4561 62.5244C56.3764 62.4468 56.2977 62.3683 56.2178 62.291C55.0844 61.1945 53.9828 60.0611 53.2256 58.6758C52.7247 57.7598 52.0192 57.4153 51.2129 57.3848C51.494 57.238 51.772 57.0879 52.0059 56.8955C53.7043 55.5033 55.6815 55.2896 57.6846 55.2363Z" fill="#797AAA"/>
<path d="M68.8535 38.1797C69.3555 38.2762 69.8502 38.4403 70.332 38.7178C70.7649 38.7827 71.2009 38.8589 71.6338 38.9238C71.7501 39.122 72.7151 39.2658 73.8682 39.3408C73.6767 39.9505 73.4856 40.5604 73.2939 41.1699C73.2585 41.2811 73.2383 41.3983 73.2041 41.54L73.2051 41.542C73.3331 41.5591 73.4299 41.5839 73.5264 41.584C75.5089 41.5852 77.4786 41.4478 79.4072 40.9473C79.534 40.9144 79.6597 40.8786 79.7852 40.8428C79.9456 42.2637 80.0539 43.6887 80.1299 45.1162C79.3969 45.2822 78.6688 45.485 77.9463 45.7295C77.5999 45.8468 77.2615 45.9757 76.9287 46.1094C76.7803 45.0945 76.5285 44.0954 76.0811 43.126C75.5182 41.9028 74.3816 41.4375 72.7363 42.2275C70.9809 43.0782 69.0539 43.7121 67.1611 43.4609C67.7272 41.7012 68.2902 39.9404 68.8535 38.1797Z" fill="#797AAA"/>
<path d="M66.6816 59.4824C66.6306 59.9843 66.5983 60.4942 66.5898 61.0117C66.5644 62.5733 67.1743 63.6805 68.4053 64.3857C66.9056 65.2488 66.0372 66.8406 64.6484 67.7959C64.4752 67.482 64.3557 67.3947 63.9443 67.6221C63.3057 67.9684 62.5698 68.283 61.8555 68.3154C61.1451 68.346 60.4352 68.3703 59.7256 68.3926C59.737 67.5962 59.7084 66.7958 59.6992 66C59.6966 65.8141 59.5246 65.5984 59.3789 65.4492C58.4114 64.4659 57.443 63.4835 56.4551 62.5215C57.4738 61.4639 59.1276 61.0311 60.8809 61.5273C61.736 61.7763 62.6237 61.6464 63.4355 61.1377C64.4895 60.4739 65.5413 59.8504 66.6816 59.4824Z" fill="#A29EA1"/>
<path d="M93.8545 46.9453C94.227 48.0975 94.6047 49.2476 95.0166 50.3848C94.7082 50.6533 94.4141 50.9535 94.124 51.2783C93.5178 51.9493 92.5328 52.2526 91.8076 52.8154C91.3946 53.1374 90.9695 53.3596 90.5361 53.5098C89.9174 52.1686 89.4955 50.7682 89.458 49.2227C89.4516 48.958 89.4277 48.696 89.3906 48.4365C90.9981 48.2979 92.4836 47.7839 93.8545 46.9453Z" fill="#F8B570"/>
<path d="M63.9404 67.6152C64.3514 67.3882 64.487 67.4771 64.6602 67.7909C64.614 67.8524 64.5653 67.916 64.5186 67.9794C63.5226 67.9131 62.7579 68.4959 62.334 69.6689C62.2032 70.0308 62.1013 70.4029 61.998 70.7743C61.8054 70.9867 61.6141 71.2003 61.4209 71.412C61.1178 71.8234 60.7496 72.1918 60.5439 72.6464C59.9486 73.9669 59.8513 72.3975 59.3535 72.4189C59.2911 72.4219 59.2285 72.4237 59.166 72.4267C59.3533 71.5691 59.5533 70.7139 59.6445 69.8456C59.6949 69.362 59.7168 68.8748 59.7236 68.3866C60.4337 68.3657 61.1434 68.3436 61.8525 68.3154C62.567 68.2829 63.3018 67.9724 63.9404 67.6152Z" fill="#B59259"/>
<path d="M62.0026 70.7607C61.9189 71.0621 61.8349 71.3635 61.7321 71.6582C61.6294 71.9527 61.5237 72.2464 61.4176 72.54C61.3578 72.5076 61.294 72.4406 61.2272 72.3223C61.1407 72.1059 61.3565 71.7162 61.4216 71.4023C61.6159 71.1893 61.8089 70.9744 62.0026 70.7607Z" fill="#F8B570"/>
<path d="M70.7946 65.3906C71.2222 65.5423 71.6523 65.6865 72.0846 65.8193C72.0295 67.8487 70.1982 68.8061 68.9342 69.5508C68.8262 69.6145 68.7255 69.6785 68.6325 69.7441C67.6411 69.2501 66.6503 68.7544 65.6452 68.29C65.6225 68.2796 65.5993 68.2707 65.5768 68.2607C66.5231 68.1652 67.3628 67.4482 68.3171 67.3535C69.4906 67.2372 71.0549 67.3998 70.7946 65.3906Z" fill="#F8B570"/>
<path d="M58.8398 38.3223C58.6487 38.7285 58.4702 39.1441 58.3057 39.5693C57.9212 40.5633 57.625 41.5716 57.4004 42.5967C55.4257 42.6706 53.4504 42.6774 51.4746 42.6475C51.7168 41.6261 52.0098 40.6154 52.3457 39.6133C53.9276 39.5071 55.5003 39.2839 57.0498 38.8828C57.5815 38.7447 58.2926 38.671 58.8398 38.3223Z" fill="#A29EA1"/>
<path d="M34.9275 128V124.94L31.6875 119.6H32.6235L35.3475 124.088H35.3715L37.8555 119.6H38.7675L35.7675 124.94V128H34.9275Z" fill="#FDBA74"/>
<path d="M42.9117 128.084C42.3997 128.084 41.9277 127.996 41.4957 127.82C41.0717 127.644 40.7037 127.388 40.3917 127.052C40.0877 126.708 39.8517 126.288 39.6837 125.792C39.5157 125.288 39.4317 124.712 39.4317 124.064V123.416C39.4317 122.8 39.5157 122.252 39.6837 121.772C39.8597 121.284 40.0997 120.876 40.4037 120.548C40.7077 120.212 41.0717 119.956 41.4957 119.78C41.9277 119.604 42.3997 119.516 42.9117 119.516C43.4317 119.516 43.9037 119.604 44.3277 119.78C44.7517 119.948 45.1157 120.2 45.4197 120.536C45.7317 120.864 45.9717 121.272 46.1397 121.76C46.3077 122.24 46.3917 122.792 46.3917 123.416V124.064C46.3917 124.712 46.3037 125.288 46.1277 125.792C45.9597 126.288 45.7237 126.708 45.4197 127.052C45.1157 127.388 44.7477 127.644 44.3157 127.82C43.8917 127.996 43.4237 128.084 42.9117 128.084ZM42.9117 127.316C43.2957 127.316 43.6477 127.248 43.9677 127.112C44.2877 126.976 44.5637 126.772 44.7957 126.5C45.0277 126.228 45.2077 125.888 45.3357 125.48C45.4637 125.072 45.5277 124.6 45.5277 124.064V123.416C45.5277 122.912 45.4597 122.468 45.3237 122.084C45.1957 121.692 45.0157 121.364 44.7837 121.1C44.5517 120.828 44.2757 120.624 43.9557 120.488C43.6357 120.352 43.2877 120.284 42.9117 120.284C42.5277 120.284 42.1757 120.352 41.8557 120.488C41.5357 120.624 41.2597 120.824 41.0277 121.088C40.7957 121.352 40.6157 121.68 40.4877 122.072C40.3597 122.456 40.2957 122.904 40.2957 123.416V124.064C40.2957 124.6 40.3597 125.072 40.4877 125.48C40.6237 125.88 40.8077 126.216 41.0397 126.488C41.2717 126.76 41.5477 126.968 41.8677 127.112C42.1877 127.248 42.5357 127.316 42.9117 127.316Z" fill="#FDBA74"/>
<path d="M51.3113 128.084C50.8393 128.084 50.4113 128.012 50.0273 127.868C49.6432 127.716 49.3152 127.5 49.0432 127.22C48.7712 126.932 48.5592 126.584 48.4072 126.176C48.2632 125.76 48.1912 125.288 48.1912 124.76V119.6H49.0312V124.7C49.0312 125.556 49.2393 126.208 49.6553 126.656C50.0713 127.096 50.6233 127.316 51.3113 127.316C52.0072 127.316 52.5592 127.096 52.9673 126.656C53.3833 126.216 53.5912 125.564 53.5912 124.7V119.6H54.4312V124.76C54.4312 125.272 54.3553 125.736 54.2033 126.152C54.0513 126.56 53.8393 126.908 53.5673 127.196C53.2952 127.484 52.9673 127.704 52.5833 127.856C52.1992 128.008 51.7753 128.084 51.3113 128.084Z" fill="#FDBA74"/>
<path d="M56.5364 128V119.6H59.2364C60.2284 119.6 60.9644 119.824 61.4444 120.272C61.9324 120.72 62.1764 121.324 62.1764 122.084C62.1764 122.66 62.0364 123.148 61.7564 123.548C61.4764 123.948 61.0564 124.232 60.4964 124.4L62.4164 128H61.4564L59.6564 124.556C59.5924 124.564 59.5244 124.568 59.4524 124.568C59.3804 124.568 59.3084 124.568 59.2364 124.568H57.3764V128H56.5364ZM57.3764 123.8H59.1764C59.9364 123.8 60.4804 123.652 60.8084 123.356C61.1444 123.06 61.3124 122.636 61.3124 122.084C61.3124 121.548 61.1444 121.128 60.8084 120.824C60.4724 120.52 59.9284 120.368 59.1764 120.368H57.3764V123.8Z" fill="#FDBA74"/>
<path d="M70.1578 128.084C69.6458 128.084 69.1738 127.996 68.7418 127.82C68.3178 127.644 67.9498 127.388 67.6378 127.052C67.3338 126.708 67.0978 126.288 66.9298 125.792C66.7618 125.288 66.6778 124.712 66.6778 124.064V123.416C66.6778 122.8 66.7618 122.252 66.9298 121.772C67.1058 121.284 67.3458 120.876 67.6498 120.548C67.9538 120.212 68.3178 119.956 68.7418 119.78C69.1738 119.604 69.6458 119.516 70.1578 119.516C70.6778 119.516 71.1498 119.604 71.5738 119.78C71.9978 119.948 72.3618 120.2 72.6658 120.536C72.9778 120.864 73.2178 121.272 73.3858 121.76C73.5538 122.24 73.6378 122.792 73.6378 123.416V124.064C73.6378 124.712 73.5498 125.288 73.3738 125.792C73.2058 126.288 72.9698 126.708 72.6658 127.052C72.3618 127.388 71.9938 127.644 71.5618 127.82C71.1378 127.996 70.6698 128.084 70.1578 128.084ZM70.1578 127.316C70.5418 127.316 70.8938 127.248 71.2138 127.112C71.5338 126.976 71.8098 126.772 72.0418 126.5C72.2738 126.228 72.4538 125.888 72.5818 125.48C72.7098 125.072 72.7738 124.6 72.7738 124.064V123.416C72.7738 122.912 72.7058 122.468 72.5698 122.084C72.4418 121.692 72.2618 121.364 72.0298 121.1C71.7978 120.828 71.5218 120.624 71.2018 120.488C70.8818 120.352 70.5338 120.284 70.1578 120.284C69.7738 120.284 69.4218 120.352 69.1018 120.488C68.7818 120.624 68.5058 120.824 68.2738 121.088C68.0418 121.352 67.8618 121.68 67.7338 122.072C67.6058 122.456 67.5418 122.904 67.5418 123.416V124.064C67.5418 124.6 67.6058 125.072 67.7338 125.48C67.8698 125.88 68.0538 126.216 68.2858 126.488C68.5178 126.76 68.7938 126.968 69.1138 127.112C69.4338 127.248 69.7818 127.316 70.1578 127.316Z" fill="#FDBA74"/>
<path d="M76.991 128L74.663 119.6H75.527L77.531 127.04H77.555L79.451 119.6H80.555L82.571 127.04H82.595L84.503 119.6H85.343L83.135 128H82.031L80.015 120.56H79.991L78.095 128H76.991Z" fill="#FDBA74"/>
<path d="M86.8997 128V119.6H88.1477L92.3957 126.92H92.4197V119.6H93.2597V128H92.0837L87.7637 120.56H87.7397V128H86.8997Z" fill="#FDBA74"/>
<path d="M100.939 128L97.6992 119.6H98.6112L101.419 127.04H101.443L104.035 119.6H104.899L101.899 128H100.939Z" fill="#FDBA74"/>
<path d="M106.341 128V119.6H109.041C110.041 119.6 110.781 119.828 111.261 120.284C111.741 120.74 111.981 121.38 111.981 122.204C111.981 123.02 111.737 123.66 111.249 124.124C110.761 124.58 110.025 124.808 109.041 124.808H107.181V128H106.341ZM108.981 120.368H107.181V124.04H108.981C109.741 124.04 110.285 123.888 110.613 123.584C110.949 123.272 111.117 122.812 111.117 122.204C111.117 121.604 110.949 121.148 110.613 120.836C110.277 120.524 109.733 120.368 108.981 120.368Z" fill="#FDBA74"/>
<path d="M113.536 128V119.6H114.784L119.032 126.92H119.056V119.6H119.896V128H118.72L114.4 120.56H114.376V128H113.536Z" fill="#FDBA74"/>
</svg>

After

Width:  |  Height:  |  Size: 110 KiB

@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.73 18L13.73 4C13.5556 3.69219 13.3025 3.43619 12.9969 3.25807C12.6912 3.07995 12.3438 2.98611 11.99 2.98611C11.6362 2.98611 11.2888 3.07995 10.9831 3.25807C10.6775 3.43619 10.4244 3.69219 10.25 4L2.25 18C2.07368 18.3054 1.98127 18.6519 1.98211 19.0045C1.98296 19.3571 2.07704 19.7032 2.25479 20.0077C2.43254 20.3123 2.68763 20.5644 2.99426 20.7386C3.30089 20.9128 3.64824 21.0029 4 21H20C20.3501 20.9997 20.6939 20.9071 20.997 20.7319C21.3001 20.5566 21.5518 20.3046 21.7268 20.0013C21.9017 19.6979 21.9938 19.3539 21.9937 19.0038C21.9936 18.6537 21.9013 18.3098 21.7262 18.0065" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 9V13" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 17H12.01" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 987 B

-3
View File
@@ -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="M6 14L7.45 11.1C7.61696 10.7687 7.87281 10.4903 8.18893 10.296C8.50504 10.1018 8.86897 9.99927 9.24 10H20C20.3055 9.99946 20.6071 10.0689 20.8816 10.2031C21.1561 10.3372 21.3963 10.5325 21.5836 10.7739C21.7709 11.0152 21.9004 11.2963 21.9622 11.5956C22.024 11.8948 22.0164 12.2042 21.94 12.5L20.39 18.5C20.279 18.9299 20.0281 19.3106 19.6769 19.5822C19.3256 19.8538 18.894 20.0008 18.45 20H4C3.46957 20 2.96086 19.7893 2.58579 19.4142C2.21071 19.0391 2 18.5304 2 18V5C2 3.9 2.9 3 4 3H7.93C8.25941 3.0017 8.58331 3.08475 8.8729 3.24176C9.1625 3.39877 9.40882 3.62488 9.59 3.9L10.41 5.1C10.5912 5.37512 10.8375 5.60123 11.1271 5.75824C11.4167 5.91525 11.7406 5.9983 12.07 6H18C18.5304 6 19.0391 6.21071 19.4142 6.58579C19.7893 6.96086 20 7.46957 20 8V10" stroke="#CBCBCB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 948 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 21C20.5523 21 21 21.4477 21 22V22.0098C21 22.5621 20.5523 23.0098 20 23.0098C19.4477 23.0098 19 22.5621 19 22.0098V22C19 21.4477 19.4477 21 20 21ZM20 3C20.7957 3 21.5585 3.3163 22.1211 3.87891C22.6837 4.44152 23 5.20435 23 6V10.5C23 11.0523 22.5523 11.5 22 11.5C21.4477 11.5 21 11.0523 21 10.5V8.81934L13.5664 13.5439L13.5605 13.5479C13.0928 13.8408 12.5519 13.9961 12 13.9961C11.4481 13.9961 10.9072 13.8408 10.4395 13.5479L10.4336 13.5439L3 8.81934V18C3 18.5477 3.45228 19 4 19H16.5C17.0523 19 17.5 19.4477 17.5 20C17.5 20.5523 17.0523 21 16.5 21H4C2.34772 21 1 19.6523 1 18V6C1 5.20435 1.3163 4.44152 1.87891 3.87891C2.44152 3.3163 3.20435 3 4 3H20ZM20 13C20.5523 13 21 13.4477 21 14V18C21 18.5523 20.5523 19 20 19C19.4477 19 19 18.5523 19 18V14C19 13.4477 19.4477 13 20 13ZM4 5C3.73478 5 3.4805 5.10543 3.29297 5.29297C3.10543 5.4805 3 5.73478 3 6V6.4502L11.5 11.8525C11.6496 11.9463 11.8235 11.9961 12 11.9961C12.1765 11.9961 12.3494 11.9463 12.499 11.8525L21 6.4502V6C21 5.73478 20.8946 5.48051 20.707 5.29297C20.5195 5.10543 20.2652 5 20 5H4Z" fill="#F87171"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

-14
View File
@@ -1,14 +0,0 @@
<svg width="24" height="24" viewBox="0 0 74 74" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_4_34)">
<path d="M55.5 12.3333H18.5C15.0942 12.3333 12.3333 15.0943 12.3333 18.5V55.5C12.3333 58.9058 15.0942 61.6667 18.5 61.6667H55.5C58.9057 61.6667 61.6666 58.9058 61.6666 55.5V18.5C61.6666 15.0943 58.9057 12.3333 55.5 12.3333Z" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21.5833 24.6667H52.4167" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21.5833 37H52.4167" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21.5833 49.3333H40.0833" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="61.5" cy="12.5" r="15" fill="#FBB36B" stroke="#1C1D21" stroke-width="5"/>
</g>
<defs>
<clipPath id="clip0_4_34">
<rect width="74" height="74" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 982 B

+3 -8
View File
@@ -1,8 +1,3 @@
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#CBCAC8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<!-- Основа газеты -->
<rect x="4" y="4" width="16" height="16" rx="2"/>
<!-- Линии текста -->
<line x1="7" y1="8" x2="17" y2="8"/>
<line x1="7" y1="12" x2="17" y2="12"/>
<line x1="7" y1="16" x2="13" y2="16"/>
</svg>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 1C20.7957 1 21.5585 1.3163 22.1211 1.87891C22.6837 2.44152 23 3.20435 23 4V20C23 20.7957 22.6837 21.5585 22.1211 22.1211C21.5585 22.6837 20.7957 23 20 23H4C3.9647 23 3.92987 22.9977 3.89551 22.9941C3.13785 22.9677 2.41681 22.659 1.87891 22.1211C1.3163 21.5585 1 20.7957 1 20V11C1 9.34772 2.34772 8 4 8H5V4C5 3.20435 5.3163 2.44152 5.87891 1.87891C6.44152 1.3163 7.20435 1 8 1H20ZM4 10C3.45228 10 3 10.4523 3 11V20C3 20.2652 3.10543 20.5195 3.29297 20.707C3.48051 20.8946 3.73478 21 4 21C4.26522 21 4.51949 20.8946 4.70703 20.707C4.89457 20.5195 5 20.2652 5 20V10H4ZM8 3C7.73478 3 7.4805 3.10543 7.29297 3.29297C7.10543 3.4805 7 3.73478 7 4V20C7 20.3444 6.93703 20.6814 6.82422 21H20C20.2652 21 20.5195 20.8946 20.707 20.707C20.8946 20.5195 21 20.2652 21 20V4C21 3.73478 20.8946 3.48051 20.707 3.29297C20.5195 3.10543 20.2652 3 20 3H8ZM15 17C15.5523 17 16 17.4477 16 18C16 18.5523 15.5523 19 15 19H10C9.44772 19 9 18.5523 9 18C9 17.4477 9.44771 17 10 17H15ZM18 13C18.5523 13 19 13.4477 19 14C19 14.5523 18.5523 15 18 15H10C9.44771 15 9 14.5523 9 14C9 13.4477 9.44771 13 10 13H18ZM18 5C18.5523 5 19 5.44772 19 6V10C19 10.5523 18.5523 11 18 11H10C9.44771 11 9 10.5523 9 10V6C9 5.44772 9.44771 5 10 5H18ZM11 9H17V7H11V9Z" fill="#E4E4E7"/>
</svg>

Before

Width:  |  Height:  |  Size: 410 B

After

Width:  |  Height:  |  Size: 1.3 KiB

+6
View File
@@ -0,0 +1,6 @@
<svg width="298" height="298" viewBox="46 46 298 298" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M129.5 50H64C56.268 50 50 56.268 50 64V129.5" stroke="white" stroke-width="4"/>
<path d="M260.5 50H326C333.732 50 340 56.268 340 64V129.5" stroke="white" stroke-width="4"/>
<path d="M340 260.5V326C340 333.732 333.732 340 326 340H260.5" stroke="white" stroke-width="4"/>
<path d="M129.5 340H64C56.268 340 50 333.732 50 326V260.5" stroke="white" stroke-width="4"/>
</svg>

After

Width:  |  Height:  |  Size: 481 B

-5
View File
@@ -1,5 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.0001 12C14.0001 10.8954 13.1047 10 12.0001 10C10.8955 10 10.0001 10.8954 10.0001 12C10.0001 13.1046 10.8955 14 12.0001 14C13.1047 14 14.0001 13.1046 14.0001 12ZM16.0001 12C16.0001 14.2091 14.2092 16 12.0001 16C9.79094 16 8.00008 14.2091 8.00008 12C8.00008 9.79086 9.79094 8 12.0001 8C14.2092 8 16.0001 9.79086 16.0001 12Z" fill="#D7D8DB"/>
<path d="M20.5001 6C21.8808 6 23.0001 4.88071 23.0001 3.5C23.0001 2.11929 21.8808 1 20.5001 1C19.1194 1 18.0001 2.11929 18.0001 3.5C18.0001 4.88071 19.1194 6 20.5001 6Z" fill="#FBB36A"/>
<path d="M12.2198 1C12.9657 1 13.6811 1.28098 14.2315 1.7793C14.0814 2.32745 14.0001 2.90421 14.0001 3.5C14.0001 4.75704 14.3578 5.93021 14.9757 6.9248L14.7198 6.77539C14.2644 6.51244 13.8864 6.13489 13.6231 5.67969C13.3599 5.22423 13.2203 4.70672 13.2198 4.18066V4C13.2198 3.73478 13.1144 3.48051 12.9268 3.29297C12.7393 3.10557 12.4849 3 12.2198 3H11.7803C11.5153 3.00005 11.2608 3.10556 11.0733 3.29297C10.8858 3.4805 10.7803 3.73482 10.7803 4V4.18066C10.7798 4.70675 10.6403 5.22422 10.377 5.67969C10.1138 6.1349 9.73575 6.51343 9.28035 6.77637L9.27937 6.77539L8.85261 7.02441L8.84968 7.02637C8.39376 7.28946 7.87607 7.42773 7.34968 7.42773C6.82327 7.42764 6.30558 7.28958 5.84968 7.02637L5.72956 6.96191L5.70027 6.94629C5.47084 6.81401 5.1983 6.77827 4.94245 6.84668C4.71846 6.90662 4.52295 7.04188 4.38874 7.22852L4.33503 7.31152L4.11531 7.69141C3.98385 7.92039 3.94855 8.19212 4.01667 8.44727C4.07655 8.6712 4.21199 8.86674 4.39851 9.00098L4.48152 9.05469L4.53523 9.08789L4.68464 9.1875C5.11379 9.44747 5.47116 9.8098 5.7237 10.2441C5.98685 10.6968 6.12679 11.2108 6.12995 11.7344V12.2461L6.1241 12.4434C6.09561 12.9034 5.96204 13.3518 5.73152 13.7529C5.47084 14.2065 5.09507 14.5827 4.64363 14.8467L4.6446 14.8477L4.49421 14.9375L4.48152 14.9453C4.25235 15.0781 4.08509 15.2969 4.01667 15.5527C3.94855 15.8079 3.98385 16.0796 4.11531 16.3086L4.33406 16.6885C4.4668 16.9177 4.68662 17.0849 4.94245 17.1533C5.1983 17.2217 5.47084 17.186 5.70027 17.0537L5.72956 17.0381L5.84968 16.9736L6.02351 16.8809C6.43513 16.678 6.88902 16.5724 7.34968 16.5723C7.81025 16.5723 8.26427 16.6781 8.67585 16.8809L8.84968 16.9736L8.85261 16.9756L9.27937 17.2236C9.73477 17.4866 10.1138 17.8651 10.377 18.3203C10.6403 18.7758 10.7798 19.2932 10.7803 19.8193V20C10.7803 20.2652 10.8858 20.5195 11.0733 20.707C11.2608 20.8944 11.5153 21 11.7803 21H12.2198C12.4849 21 12.7393 20.8944 12.9268 20.707C13.1144 20.5195 13.2198 20.2652 13.2198 20V19.8193L13.2266 19.6221C13.2572 19.1646 13.3928 18.7188 13.6231 18.3203C13.8534 17.9221 14.1718 17.5828 14.5528 17.3281L14.7198 17.2236L15.1475 16.9756L15.1505 16.9736L15.3243 16.8809C15.7359 16.6781 16.1899 16.5723 16.6505 16.5723C17.1769 16.5723 17.6946 16.7104 18.1505 16.9736L18.2999 17.0537C18.5293 17.186 18.8018 17.2217 19.0577 17.1533C19.3136 17.0849 19.5323 16.9177 19.6651 16.6885L19.879 16.3086L19.8839 16.3008L19.9288 16.2119C20.0233 16.0026 20.0433 15.7658 19.9835 15.542C19.915 15.2863 19.7477 15.0683 19.5186 14.9355L19.3995 14.8721C19.3898 14.8669 19.3797 14.862 19.3702 14.8564C18.9124 14.5921 18.532 14.2112 18.2686 13.7529C18.0053 13.2946 17.8681 12.7746 17.8702 12.2461V11.75C17.8688 11.2228 18.0059 10.7042 18.2686 10.2471C18.3758 10.0606 18.5033 9.88765 18.6466 9.72949C19.2341 9.90406 19.8558 10 20.5001 10C20.8249 10 21.144 9.97473 21.4561 9.92871C21.2126 10.2693 20.9013 10.5607 20.5333 10.7764L20.5343 10.7773L20.3702 10.876C20.2176 10.9641 20.0908 11.0914 20.003 11.2441C19.9153 11.3968 19.8695 11.57 19.8702 11.7461V12.2539C19.8695 12.43 19.9153 12.6032 20.003 12.7559C20.0894 12.9062 20.214 13.0312 20.3634 13.1191L20.4903 13.1875L20.5216 13.2051L20.7706 13.3662C21.3298 13.7689 21.7365 14.3539 21.9161 15.0254C22.1205 15.7898 22.0131 16.6035 21.6202 17.29L21.6212 17.291L21.4014 17.6816L21.3956 17.6914C20.9972 18.379 20.342 18.8807 19.5743 19.0859C18.817 19.2883 18.0116 19.1847 17.3292 18.8008V18.8027L17.1798 18.7227C17.17 18.7175 17.16 18.7116 17.1505 18.7061C16.9986 18.6184 16.8258 18.5723 16.6505 18.5723C16.4751 18.5723 16.3024 18.6184 16.1505 18.7061L16.1495 18.7051L15.7227 18.9541L15.6114 19.0283C15.5077 19.1078 15.4204 19.2075 15.3546 19.3213C15.2668 19.4731 15.22 19.646 15.2198 19.8213V20C15.2198 20.7955 14.9043 21.5585 14.3419 22.1211C13.7793 22.6836 13.0154 23 12.2198 23H11.7803C10.9848 23 10.2208 22.6837 9.65828 22.1211C9.09594 21.5585 8.78035 20.7954 8.78035 20V19.8213L8.77156 19.6904C8.75435 19.5609 8.71141 19.4352 8.64558 19.3213C8.57971 19.2075 8.49243 19.1078 8.38874 19.0283L8.27742 18.9541L7.84968 18.7051C7.69776 18.6175 7.52506 18.5723 7.34968 18.5723C7.21824 18.5723 7.08852 18.5985 6.96785 18.6484L6.84968 18.7061C6.84011 18.7116 6.83013 18.7175 6.82038 18.7227L6.67097 18.8027L6.66999 18.8008C5.98771 19.1844 5.18286 19.2883 4.42585 19.0859C3.75418 18.9063 3.16836 18.4998 2.7657 17.9404L2.60456 17.6914L2.38484 17.3105L2.38386 17.3096C1.98692 16.6214 1.87897 15.8037 2.08406 15.0361C2.28803 14.2733 2.78502 13.6217 3.46589 13.2227L3.62995 13.124L3.73933 13.0508C3.84344 12.9708 3.93128 12.8704 3.99714 12.7559C4.08484 12.6032 4.13065 12.43 4.12995 12.2539V11.7461L4.12117 11.6162C4.10349 11.4873 4.06003 11.3622 3.99421 11.249C3.92841 11.136 3.84152 11.037 3.73835 10.958L3.62995 10.8857L3.57527 10.8516L3.42585 10.752C2.76614 10.351 2.2839 9.71122 2.08406 8.96387C1.87897 8.19633 1.98692 7.37864 2.38386 6.69043L2.38484 6.68848L2.60456 6.30859L2.7657 6.05957C3.16836 5.50016 3.75418 5.09371 4.42585 4.91406C5.1827 4.71172 5.9878 4.81485 6.66999 5.19824L6.67097 5.19727L6.82038 5.27734L6.84968 5.29395L6.967Line truncated
</svg>

Before

Width:  |  Height:  |  Size: 5.9 KiB

+7
View File
@@ -0,0 +1,7 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="sort-desc">
<path id="Line_1" d="M3 5H21" stroke="#E4E4E7" stroke-width="2" stroke-linecap="round"/>
<path id="Line_2" d="M3 12H15" stroke="#E4E4E7" stroke-width="2" stroke-linecap="round"/>
<path id="Line_3" d="M3 19H10" stroke="#E4E4E7" stroke-width="2" stroke-linecap="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 396 B

+2 -5
View File
@@ -1,6 +1,3 @@
<svg width="19" height="18" viewBox="0 0 19 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" width="18" height="18" rx="5" fill="white"/>
<path d="M8.49219 13.5L8.49219 9.44141L14.0191 4.99484" stroke="#0E0E11" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.47363 5.49805L6.98828 8.0127" stroke="#0E0E11" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14.4727 9.5L14.4727 4.5033L9.50195 4.5033" stroke="#0E0E11" stroke-linecap="round" stroke-linejoin="round"/>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.0275 5C19.0858 5.00093 19.1439 5.0068 19.2014 5.01758C19.2155 5.02023 19.2294 5.02411 19.2434 5.02734C19.2617 5.03158 19.28 5.03576 19.298 5.04102C19.3522 5.05672 19.4046 5.07722 19.4553 5.10156C19.4608 5.10422 19.4664 5.10661 19.4719 5.10938L19.4875 5.11719C19.4978 5.12261 19.5076 5.12899 19.5178 5.13477C19.5626 5.16041 19.6064 5.18867 19.6477 5.22168C19.6746 5.24318 19.701 5.26568 19.7258 5.29004C19.7418 5.30575 19.7559 5.32322 19.7707 5.33984C19.7771 5.3471 19.784 5.35393 19.7902 5.36133C19.9384 5.53804 20.0207 5.76027 20.0207 5.99121L20.0197 12.9941C20.0196 13.5415 19.5679 13.9853 19.0109 13.9854C18.454 13.9852 18.0022 13.5415 18.0021 12.9941V8.41309L12.2385 14.1777V18.4404C12.2382 18.9876 11.7875 19.4315 11.2307 19.4316C10.6737 19.4316 10.2221 18.9877 10.2219 18.4404V13.7715L10.2268 13.6748C10.2492 13.4505 10.3488 13.2393 10.5109 13.0771L16.6037 6.98242H12.0109C11.454 6.98233 11.0023 6.53854 11.0021 5.99121C11.0023 5.44389 11.454 5.00009 12.0109 5H19.0275ZM4.30097 6.0625C4.69824 5.67878 5.33821 5.6847 5.72871 6.0752L9.61836 9.96387C10.0087 10.3543 10.0026 10.9825 9.60566 11.3662C9.20853 11.7496 8.57034 11.7445 8.17988 11.3545L4.28925 7.46387C3.89917 7.07347 3.90418 6.44616 4.30097 6.0625Z" fill="#E4E4E7"/>
</svg>

Before

Width:  |  Height:  |  Size: 511 B

After

Width:  |  Height:  |  Size: 1.3 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z" fill="#E4E4E7" stroke="#E4E4E7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 308 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z" stroke="#E4E4E7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 293 B

+7
View File
@@ -0,0 +1,7 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 6H21" stroke="#CBCBCB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19 6V20C19 21 18 22 17 22H7C6 22 5 21 5 20V6" stroke="#CBCBCB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 6V4C8 3 9 2 10 2H14C15 2 16 3 16 4V6" stroke="#CBCBCB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 11V17" stroke="#CBCBCB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14 11V17" stroke="#CBCBCB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 682 B

+9 -5
View File
@@ -3,13 +3,15 @@
<file>../fonts/pt-root-ui_vf.ttf</file>
</qresource>
<qresource prefix="/images">
<file>amneziaBigLogo.png</file>
<file>amneziaLogo.svg</file>
<file>AmneziaVPN.png</file>
<file>controls/alert-circle.svg</file>
<file>controls/alert-triangle.svg</file>
<file>update/updateHeroDark.jpg</file>
<file>update/plus-circle.svg</file>
<file>update/arrow-up-circle.svg</file>
<file>update/wrench.svg</file>
<file>update/arrow-down-circle.svg</file>
<file>controls/amnezia.svg</file>
<file>controls/app.svg</file>
<file>controls/archive-restore.svg</file>
@@ -31,7 +33,6 @@
<file>controls/external-link.svg</file>
<file>controls/file-check-2.svg</file>
<file>controls/file-cog-2.svg</file>
<file>controls/folder-open.svg</file>
<file>controls/folder-search-2.svg</file>
<file>controls/gauge.svg</file>
<file>controls/globe-2.svg</file>
@@ -43,13 +44,14 @@
<file>controls/infinity.svg</file>
<file>controls/info.svg</file>
<file>controls/mail.svg</file>
<file>controls/mail-warning.svg</file>
<file>controls/map-pin.svg</file>
<file>controls/more-vertical.svg</file>
<file>controls/news.svg</file>
<file>controls/news-unread.svg</file>
<file>controls/unread-dot.svg</file>
<file>controls/plus.svg</file>
<file>controls/qr-code.svg</file>
<file>controls/qr-scan-frame.svg</file>
<file>controls/radio-button-inner-circle-pressed.png</file>
<file>controls/radio-button-inner-circle.png</file>
<file>controls/radio-button-pressed.svg</file>
@@ -59,17 +61,20 @@
<file>controls/save.svg</file>
<file>controls/scan-line.svg</file>
<file>controls/search.svg</file>
<file>controls/sort-desc.svg</file>
<file>controls/server.svg</file>
<file>controls/settings-2.svg</file>
<file>controls/settings.svg</file>
<file>controls/settings-news.svg</file>
<file>controls/share-2.svg</file>
<file>controls/star.svg</file>
<file>controls/star-filled.svg</file>
<file>controls/split-tunneling.svg</file>
<file>controls/smartphone.svg</file>
<file>controls/tag.svg</file>
<file>controls/telegram.svg</file>
<file>controls/text-cursor.svg</file>
<file>controls/trash.svg</file>
<file>controls/trash-2.svg</file>
<file>controls/x-circle.svg</file>
<file>tray/active.png</file>
<file>tray/default.png</file>
@@ -77,4 +82,3 @@
<file>controls/monitor.svg</file>
</qresource>
</RCC>
@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="arrow-down-circle">
<path id="Union" d="M12 1C18.0751 1 23 5.92487 23 12C23 18.0751 18.0751 23 12 23C5.92487 23 1 18.0751 1 12C1 5.92487 5.92487 1 12 1ZM12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3ZM12 7C12.5523 7 13 7.44772 13 8V13.5859L15.293 11.293C15.6835 10.9024 16.3165 10.9024 16.707 11.293C17.0976 11.6835 17.0976 12.3165 16.707 12.707L12.707 16.707C12.6592 16.7548 12.6065 16.7976 12.5498 16.835C12.5121 16.8599 12.4727 16.8811 12.4326 16.9004C12.3874 16.9222 12.3407 16.9412 12.292 16.9561C12.2717 16.9622 12.251 16.9668 12.2305 16.9717C12.2191 16.9744 12.2078 16.9772 12.1963 16.9795C12.1859 16.9816 12.1754 16.9826 12.165 16.9844C12.1112 16.9933 12.0563 17 12 17C11.9433 17 11.8881 16.9934 11.834 16.9844C11.8236 16.9826 11.8131 16.9816 11.8027 16.9795C11.7912 16.9772 11.7799 16.9744 11.7686 16.9717C11.748 16.9668 11.7273 16.9623 11.707 16.9561C11.6584 16.9412 11.6116 16.9222 11.5664 16.9004C11.5266 16.8812 11.4877 16.8597 11.4502 16.835C11.3935 16.7976 11.3408 16.7548 11.293 16.707L7.29297 12.707C6.90244 12.3165 6.90244 11.6835 7.29297 11.293C7.68349 10.9024 8.31651 10.9024 8.70703 11.293L11 13.5859V8C11 7.44772 11.4477 7 12 7Z" fill="#09090B"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+1 -2
View File
@@ -57,7 +57,6 @@
<false/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
@@ -68,7 +67,7 @@
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIUserInterfaceStyle</key>
<string>Light</string>
<string>Dark</string>
<key>com.wireguard.ios.app_group_id</key>
<string>${BUILD_IOS_GROUP_IDENTIFIER}</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
-7
View File
@@ -6,15 +6,8 @@
<true/>
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>app-proxy-provider</string>
<string>packet-tunnel-provider</string>
<string>dns-settings</string>
<string>relay</string>
<string>content-filter-provider</string>
<string>dns-proxy</string>
</array>
<key>com.apple.developer.system-extension.install</key>
<true/>
<key>com.apple.developer.networking.vpn.api</key>
<array>
<string>allow-vpn</string>
@@ -6,12 +6,7 @@
<true/>
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>dns-settings</string>
<string>relay</string>
<string>packet-tunnel-provider</string>
<string>content-filter-provider</string>
<string>dns-proxy</string>
<string>app-proxy-provider</string>
</array>
<key>com.apple.developer.networking.vpn.api</key>
<array>
+92 -11
View File
@@ -97,6 +97,7 @@ bool AndroidController::initialize()
{"onVpnStateChanged", "(I)V", reinterpret_cast<void *>(onVpnStateChanged)},
{"onStatisticsUpdate", "(JJ)V", reinterpret_cast<void *>(onStatisticsUpdate)},
{"onFileOpened", "(Ljava/lang/String;)V", reinterpret_cast<void *>(onFileOpened)},
{"onFileSaved", "(ZZ)V", reinterpret_cast<void *>(onFileSaved)},
{"onConfigImported", "(Ljava/lang/String;)V", reinterpret_cast<void *>(onConfigImported)},
{"onAuthResult", "(Z)V", reinterpret_cast<void *>(onAuthResult)},
{"decodeQrCode", "(Ljava/lang/String;)Z", reinterpret_cast<bool *>(decodeQrCode)},
@@ -161,6 +162,46 @@ void AndroidController::saveFile(const QString &fileName, const QString &data)
QJniObject::fromString(data).object<jstring>());
}
bool AndroidController::saveFile(const QString &fileName, const QByteArray &data, const QString &mime, bool *cancelled)
{
QJniEnvironment env;
const jsize size = static_cast<jsize>(data.size());
jbyteArray bytes = env->NewByteArray(size);
if (!bytes) {
env.checkAndClearExceptions();
qCritical() << "AndroidController::saveFile: cannot allocate byte array of size" << size;
return false;
}
env->SetByteArrayRegion(bytes, 0, size, reinterpret_cast<const jbyte *>(data.constData()));
QEventLoop wait;
bool saved = false;
bool saveCancelled = false;
connect(this, &AndroidController::fileSaved, this,
[&saved, &saveCancelled, &wait](bool success, bool isCancelled) {
saved = success;
saveCancelled = isCancelled;
wait.quit();
},
static_cast<Qt::ConnectionType>(Qt::QueuedConnection | Qt::SingleShotConnection));
callActivityMethod("saveFileBytes", "(Ljava/lang/String;[BLjava/lang/String;)V",
QJniObject::fromString(fileName).object<jstring>(), bytes,
QJniObject::fromString(mime).object<jstring>());
env->DeleteLocalRef(bytes);
wait.exec();
if (cancelled) {
*cancelled = saveCancelled;
}
return saved;
}
void AndroidController::shareFile(const QString &path, const QString &mime)
{
callActivityMethod("shareFile", "(Ljava/lang/String;Ljava/lang/String;)V",
QJniObject::fromString(path).object<jstring>(),
QJniObject::fromString(mime).object<jstring>());
}
QString AndroidController::openFile(const QString &filter)
{
QEventLoop wait;
@@ -231,17 +272,6 @@ void AndroidController::setSaveLogs(bool enabled)
callActivityMethod("setSaveLogs", "(Z)V", enabled);
}
void AndroidController::exportLogsFile(const QString &fileName)
{
callActivityMethod("exportLogsFile", "(Ljava/lang/String;)V",
QJniObject::fromString(fileName).object<jstring>());
}
void AndroidController::clearLogs()
{
callActivityMethod("clearLogs", "()V");
}
void AndroidController::setScreenshotsEnabled(bool enabled)
{
callActivityMethod("setScreenshotsEnabled", "(Z)V", enabled);
@@ -456,6 +486,48 @@ void AndroidController::messageHandler(QtMsgType type, const QMessageLogContext
QJniObject::fromString(formattedMessage).object<jstring>());
}
// static
jclass AndroidController::findLogClass()
{
if (log != nullptr) {
return log;
}
return QJniEnvironment().findClass(ANDROID_LOG_CLASS);
}
// static
QStringList AndroidController::logFiles(int stream)
{
const jclass logClass = findLogClass();
if (logClass == nullptr) {
return {};
}
const QString files =
QJniObject::callStaticObjectMethod(logClass, "getLogFiles", "(I)Ljava/lang/String;", static_cast<jint>(stream))
.toString();
return files.split('\n', Qt::SkipEmptyParts);
}
// static
QString AndroidController::deviceInfo()
{
const jclass logClass = findLogClass();
if (logClass == nullptr) {
return {};
}
return QJniObject::callStaticObjectMethod(logClass, "getDeviceInfo", "()Ljava/lang/String;").toString();
}
// static
void AndroidController::clearLogStream(int stream)
{
const jclass logClass = findLogClass();
if (logClass == nullptr) {
return;
}
QJniObject::callStaticMethod<void>(logClass, "clearStream", "(I)V", static_cast<jint>(stream));
}
void AndroidController::qtAndroidControllerInitialized()
{
callActivityMethod("qtAndroidControllerInitialized", "()V");
@@ -563,6 +635,15 @@ void AndroidController::onFileOpened(JNIEnv *env, jobject thiz, jstring uri)
emit AndroidController::instance()->fileOpened(AndroidUtils::convertJString(env, uri));
}
// static
void AndroidController::onFileSaved(JNIEnv *env, jobject thiz, jboolean success, jboolean cancelled)
{
Q_UNUSED(env);
Q_UNUSED(thiz);
emit AndroidController::instance()->fileSaved(success, cancelled);
}
// static
void AndroidController::onConfigImported(JNIEnv *env, jobject thiz, jstring data)
{
+11 -2
View File
@@ -3,6 +3,7 @@
#include <QJniObject>
#include <QPixmap>
#include <QStringList>
#include "core/protocols/vpnProtocol.h"
@@ -33,6 +34,8 @@ public:
void stop();
void resetLastServer(int serverIndex);
void saveFile(const QString &fileName, const QString &data);
bool saveFile(const QString &fileName, const QByteArray &data, const QString &mime, bool *cancelled = nullptr);
void shareFile(const QString &path, const QString &mime);
QString openFile(const QString &filter);
int getFd(const QString &fileName);
void closeFd();
@@ -44,8 +47,6 @@ public:
int getNavigationBarHeight();
void startQrReaderActivity();
void setSaveLogs(bool enabled);
void exportLogsFile(const QString &fileName);
void clearLogs();
void setScreenshotsEnabled(bool enabled);
void setNavigationBarColor(unsigned int color);
void minimizeApp();
@@ -65,6 +66,10 @@ public:
static bool initLogging();
static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message);
static QStringList logFiles(int stream);
static QString deviceInfo();
static void clearLogStream(int stream);
signals:
void connectionStateChanged(Vpn::ConnectionState state);
void status(ConnectionState state);
@@ -75,6 +80,7 @@ signals:
void vpnStateChanged(ConnectionState state);
void statisticsUpdated(quint64 rxBytes, quint64 txBytes);
void fileOpened(QString uri);
void fileSaved(bool success, bool cancelled);
void configImported(QString config);
void importConfigFromOutside(QString config);
void initConnectionState(Vpn::ConnectionState state);
@@ -94,6 +100,8 @@ private:
static jmethodID logError;
static jmethodID logFatal;
static jclass findLogClass();
void qtAndroidControllerInitialized();
static Vpn::ConnectionState convertState(ConnectionState state);
@@ -109,6 +117,7 @@ private:
static void onStatisticsUpdate(JNIEnv *env, jobject thiz, jlong rxBytes, jlong txBytes);
static void onConfigImported(JNIEnv *env, jobject thiz, jstring data);
static void onFileOpened(JNIEnv *env, jobject thiz, jstring uri);
static void onFileSaved(JNIEnv *env, jobject thiz, jboolean success, jboolean cancelled);
static void onAuthResult(JNIEnv *env, jobject thiz, jboolean result);
static bool decodeQrCode(JNIEnv *env, jobject thiz, jstring data);
static void onImeInsetsChanged(JNIEnv *env, jobject thiz, jint heightDp);
+11 -66
View File
@@ -17,64 +17,29 @@ struct Log {
private static let appGroupID = BuildConfig.appGroupIdentifier
static let appLogURL = {
let sharedContainerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupID)!
return sharedContainerURL.appendingPathComponent("app.log", isDirectory: false)
private static let logDirectoryURL: URL = {
if let sharedContainerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupID) {
return sharedContainerURL
}
return FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first
?? FileManager.default.temporaryDirectory
}()
static let neLogURL = {
let sharedContainerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupID)!
return sharedContainerURL.appendingPathComponent("ne.log", isDirectory: false)
}()
static let appLogURL = logDirectoryURL.appendingPathComponent("app.log", isDirectory: false)
static let neLogURL = logDirectoryURL.appendingPathComponent("ne.log", isDirectory: false)
private static var sharedUserDefaults = {
UserDefaults(suiteName: appGroupID)!
UserDefaults(suiteName: appGroupID) ?? .standard
}()
static let dateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
return dateFormatter
}()
var records = [Record]()
var lastRecordDate = Date.distantPast
init() {
self.records = []
}
init(_ str: String) {
records = str.split(whereSeparator: \.isNewline)
.map {
if let record = Record(String($0)) {
lastRecordDate = record.date
return record
} else {
return Record(date: lastRecordDate, level: .error, message: "LOG: \($0)")
}
}
}
init?(at url: URL) {
if !FileManager.default.fileExists(atPath: url.path) {
guard (try? "".data(using: .utf8)?.write(to: url)) != nil else { return nil }
}
guard let fileHandle = try? FileHandle(forUpdating: url) else { return nil }
defer { fileHandle.closeFile() }
guard
let data = try? fileHandle.readToEnd(),
let str = String(data: data, encoding: .utf8) else {
return nil
}
self.init(str)
}
static func log(_ type: OSLogType, title: String = "", message: String, url: URL = neLogURL) {
NSLog("\(title) \(message)")
@@ -127,26 +92,6 @@ struct Log {
}
}
}
static func clear(at url: URL) {
if FileManager.default.fileExists(atPath: url.path) {
guard let fileHandle = try? FileHandle(forUpdating: url) else { return }
defer { fileHandle.closeFile() }
try? fileHandle.truncate(atOffset: 0)
}
}
}
extension Log: CustomStringConvertible {
var description: String {
records
.map {
$0.description
}
.joined(separator: "\n")
}
}
func log(_ type: OSLogType, title: String = "", message: String) {
+4 -24
View File
@@ -1,31 +1,11 @@
import Foundation
public func swiftUpdateLogData(_ qtString: std.string) -> std.string {
let qtLog = Log(String(describing: qtString))
var log = qtLog
if let appLog = Log(at: Log.appLogURL) {
appLog.records.forEach {
log.records.append($0)
}
}
if let neLog = Log(at: Log.neLogURL) {
neLog.records.forEach {
log.records.append($0)
}
}
log.records.sort {
$0.date < $1.date
}
return std.string(log.description)
public func swiftAppLogPath() -> std.string {
return std.string(Log.appLogURL.path)
}
public func swiftDeleteLog() {
Log.clear(at: Log.appLogURL)
Log.clear(at: Log.neLogURL)
public func swiftTunnelLogPath() -> std.string {
return std.string(Log.neLogURL.path)
}
public func toggleLogging(_ isEnabled: Bool) {
+55 -33
View File
@@ -7,43 +7,74 @@ extension Log {
let level: Level
let message: String
init?(_ str: String) {
let dateStr = String(str.prefix(19))
guard let date = Log.dateFormatter.date(from: dateStr) else { return nil }
let str = str.dropFirst(20)
guard let endIndex = str.firstIndex(of: " ") else { return nil }
let levelStr = String(str[str.startIndex..<endIndex])
guard let level = Level(rawValue: levelStr) else { return nil }
let messageStartIndex = str.index(after: endIndex)
let message = String(str[messageStartIndex..<str.endIndex])
self.init(date: date, level: level, message: message)
}
init(date: Date, level: Level, message: String) {
self.date = date
self.level = level
self.message = message
}
private static let saveLock = NSLock()
private static let maxFileSize: off_t = 4 * 1024 * 1024
private static let keepTailSize: off_t = 2 * 1024 * 1024
func save(at url: URL) {
osLog.log(level: level.osLogType, "\(message)")
guard let data = "\n\(description)".data(using: .utf8) else { return }
let data = Array("\(description)\n".utf8)
if !FileManager.default.fileExists(atPath: url.path) {
guard (try? "".data(using: .utf8)?.write(to: url)) != nil else { return }
Record.saveLock.lock()
defer { Record.saveLock.unlock() }
let fd = open(url.path, O_RDWR | O_APPEND | O_CREAT | O_CLOEXEC, 0o644)
guard fd >= 0 else { return }
defer { close(fd) }
guard data.withUnsafeBytes({ Record.writeAll(fd, $0) }) else { return }
var fileStat = stat()
guard fstat(fd, &fileStat) == 0, fileStat.st_size > Record.maxFileSize else { return }
Record.keepTail(fd, fileSize: fileStat.st_size)
}
private static func writeAll(_ fd: Int32, _ buffer: UnsafeRawBufferPointer) -> Bool {
guard var pointer = buffer.baseAddress else { return true }
var remaining = buffer.count
while remaining > 0 {
let written = write(fd, pointer, remaining)
if written < 0 {
if errno == EINTR { continue }
return false
}
pointer += written
remaining -= written
}
return true
}
private static func keepTail(_ fd: Int32, fileSize: off_t) {
let tailSize = min(keepTailSize, fileSize)
let tailOffset = fileSize - tailSize
var tail = [UInt8](repeating: 0, count: Int(tailSize))
var readTotal = 0
while readTotal < tail.count {
let count = tail.withUnsafeMutableBytes { buffer in
pread(fd, buffer.baseAddress! + readTotal, buffer.count - readTotal, tailOffset + off_t(readTotal))
}
if count < 0 {
if errno == EINTR { continue }
return
}
if count == 0 { break }
readTotal += count
}
guard let fileHandle = try? FileHandle(forUpdating: url) else { return }
let start = tail[0..<readTotal].firstIndex(of: UInt8(ascii: "\n")).map { $0 + 1 } ?? 0
defer { fileHandle.closeFile() }
guard (try? fileHandle.seekToEnd()) != nil else { return }
try? fileHandle.write(contentsOf: data)
guard ftruncate(fd, 0) == 0 else { return }
_ = tail[start..<readTotal].withUnsafeBytes { writeAll(fd, $0) }
}
}
}
@@ -57,12 +88,9 @@ extension Log.Record: CustomStringConvertible {
extension Log.Record {
enum Level: String {
case debug
case warning
case error
case critical
case fatal
case info
case system // critical
init(from osLogType: OSLogType) {
switch osLogType {
@@ -91,12 +119,6 @@ extension Log.Record {
return .error
case .fatal:
return .fault
case .warning:
return .info
case .critical:
return .fault
case .system:
return .fault
}
}
}
@@ -227,8 +227,6 @@ extension PacketTunnelProvider {
return
}
ovpnLog(.info, title: "Transport", message: "bytesIn=\(bytesin) bytesOut=\(bytesout)")
let response: [String: Any] = [
"rx_bytes": bytesin,
"tx_bytes": bytesout
@@ -304,6 +304,16 @@ extension PacketTunnelProvider {
xrayLog(.info, message: "Xray started")
}
private func hevSocksLogFile() -> String {
guard Log.isLoggingEnabled else { return "stderr" }
let path = Log.neLogURL.path
let fd = open(path, O_WRONLY | O_APPEND | O_CREAT | O_CLOEXEC, 0o644)
guard fd >= 0 else { return "stderr" }
close(fd)
return path
}
private func setupAndRunTun2socks(configData: Data,
address: String,
port: Int,
@@ -323,8 +333,8 @@ extension PacketTunnelProvider {
task-stack-size: 20480
connect-timeout: 5000
read-write-timeout: 60000
log-file: stderr
log-level: error
log-file: '\(hevSocksLogFile())'
log-level: warn
limit-nofile: 65535
"""
Loaded 100 of 437 files, more files were not shown because too many files have changed in this diff. Show more