From 04a916a3e1a7ec84802384cd4469a3d7ef3b4234 Mon Sep 17 00:00:00 2001 From: Kevin Yue Date: Tue, 16 Jan 2024 22:18:20 +0800 Subject: [PATCH] Refactor using Tauri (#278) --- .devcontainer/Dockerfile | 62 + .devcontainer/devcontainer.json | 10 + .editorconfig | 14 +- .github/workflows/build.yaml | 250 + .github/workflows/build.yml | 275 - .github/workflows/pr.yml | 33 - .gitignore | 72 +- .gitmodules | 10 - .vscode/extensions.json | 9 + .vscode/settings.json | 73 +- 3rdparty/SingleApplication | 1 - 3rdparty/inih/CMakeLists.txt | 12 - 3rdparty/inih/LICENSE.txt | 27 - 3rdparty/inih/cpp/INIReader.cpp | 116 - 3rdparty/inih/cpp/INIReader.h | 94 - 3rdparty/inih/ini.c | 298 - 3rdparty/inih/ini.h | 178 - 3rdparty/plog | 1 - 3rdparty/qt-unix-signals/CMakeLists.txt | 14 - 3rdparty/qt-unix-signals/LICENCE | 21 - 3rdparty/qt-unix-signals/sigwatch.cpp | 176 - 3rdparty/qt-unix-signals/sigwatch.h | 59 - 3rdparty/qtkeychain | 1 - CMakeLists.txt | 39 - Cargo.lock | 5052 +++++++++++++++++ Cargo.toml | 52 + GPClient/CMakeLists.txt | 110 - GPClient/cdpcommand.cpp | 30 - GPClient/cdpcommand.h | 24 - GPClient/cdpcommandmanager.cpp | 87 - GPClient/cdpcommandmanager.h | 40 - GPClient/challengedialog.cpp | 38 - GPClient/challengedialog.h | 28 - GPClient/challengedialog.ui | 111 - GPClient/com.yuezk.qt.gpclient.desktop.in | 12 - .../com.yuezk.qt.gpclient.metainfo.xml.in | 43 - GPClient/com.yuezk.qt.gpclient.svg | 99 - GPClient/connected.png | Bin 18199 -> 0 bytes GPClient/enhancedwebpage.cpp | 8 - GPClient/enhancedwebpage.h | 12 - GPClient/enhancedwebview.cpp | 33 - GPClient/enhancedwebview.h | 29 - GPClient/gatewayauthenticator.cpp | 226 - GPClient/gatewayauthenticator.h | 48 - GPClient/gatewayauthenticatorparams.cpp | 66 - GPClient/gatewayauthenticatorparams.h | 38 - GPClient/gpclient.cpp | 519 -- GPClient/gpclient.h | 104 - GPClient/gpclient.ui | 143 - GPClient/gpgateway.cpp | 97 - GPClient/gpgateway.h | 33 - GPClient/gphelper.cpp | 178 - GPClient/gphelper.h | 47 - GPClient/loginparams.cpp | 88 - GPClient/loginparams.h | 28 - GPClient/main.cpp | 96 - GPClient/not_connected.png | Bin 16393 -> 0 bytes GPClient/pending.png | Bin 16369 -> 0 bytes GPClient/portalauthenticator.cpp | 219 - GPClient/portalauthenticator.h | 60 - GPClient/portalconfigresponse.cpp | 174 - GPClient/portalconfigresponse.h | 51 - GPClient/preloginresponse.cpp | 100 - GPClient/preloginresponse.h | 41 - GPClient/radio_selected.png | Bin 1219 -> 0 bytes GPClient/radio_unselected.png | Bin 993 -> 0 bytes GPClient/resources.qrc | 11 - GPClient/samlloginwindow.cpp | 136 - GPClient/samlloginwindow.h | 41 - GPClient/settings_icon.png | Bin 1104 -> 0 bytes GPClient/settingsdialog.cpp | 42 - GPClient/settingsdialog.h | 31 - GPClient/settingsdialog.ui | 117 - GPClient/standardloginwindow.cpp | 60 - GPClient/standardloginwindow.h | 34 - GPClient/standardloginwindow.ui | 148 - GPClient/vpn.h | 24 - GPClient/vpn_dbus.cpp | 13 - GPClient/vpn_dbus.h | 33 - GPClient/vpn_json.cpp | 24 - GPClient/vpn_json.h | 23 - GPService/CMakeLists.txt | 83 - GPService/dbus/com.yuezk.qt.GPService.conf.in | 18 - .../dbus/com.yuezk.qt.GPService.service.in | 5 - GPService/gp.conf | 17 - GPService/gpservice.cpp | 229 - GPService/gpservice.h | 62 - GPService/main.cpp | 27 - GPService/systemd/gpservice.service.in | 11 - README.md | 4 +- VERSION | 1 - apps/gpauth/Cargo.toml | 23 + apps/gpauth/build.rs | 3 + apps/gpauth/icons/128x128.png | Bin 0 -> 3512 bytes apps/gpauth/icons/128x128@2x.png | Bin 0 -> 7012 bytes apps/gpauth/icons/32x32.png | Bin 0 -> 974 bytes apps/gpauth/icons/icon.icns | Bin 0 -> 98451 bytes apps/gpauth/icons/icon.ico | Bin 0 -> 86642 bytes apps/gpauth/icons/icon.png | Bin 0 -> 14183 bytes apps/gpauth/index.html | 11 + apps/gpauth/src/auth_window.rs | 449 ++ apps/gpauth/src/cli.rs | 138 + apps/gpauth/src/main.rs | 9 + apps/gpauth/tauri.conf.json | 47 + apps/gpclient/Cargo.toml | 23 + apps/gpclient/src/cli.rs | 101 + apps/gpclient/src/connect.rs | 150 + apps/gpclient/src/disconnect.rs | 31 + apps/gpclient/src/launch_gui.rs | 88 + apps/gpclient/src/main.rs | 11 + apps/gpservice/Cargo.toml | 19 + apps/gpservice/com.yuezk.gpservice.policy | 19 + apps/gpservice/src/cli.rs | 182 + apps/gpservice/src/handlers.rs | 94 + apps/gpservice/src/main.rs | 11 + apps/gpservice/src/routes.rs | 13 + apps/gpservice/src/vpn_task.rs | 144 + apps/gpservice/src/ws_connection.rs | 53 + apps/gpservice/src/ws_server.rs | 158 + cmake/Add3rdParty.cmake | 27 - cmake/FindNetworkManager.cmake | 59 - cmakew | 102 - crates/gpapi/Cargo.toml | 32 + crates/gpapi/src/auth.rs | 63 + crates/gpapi/src/credential.rs | 223 + crates/gpapi/src/gateway/login.rs | 74 + crates/gpapi/src/gateway/mod.rs | 41 + crates/gpapi/src/gateway/parse_gateways.rs | 63 + crates/gpapi/src/gp_params.rs | 153 + crates/gpapi/src/lib.rs | 28 + crates/gpapi/src/portal/config.rs | 180 + crates/gpapi/src/portal/mod.rs | 5 + crates/gpapi/src/portal/prelogin.rs | 129 + crates/gpapi/src/process/auth_launcher.rs | 96 + crates/gpapi/src/process/command_traits.rs | 64 + crates/gpapi/src/process/gui_launcher.rs | 91 + crates/gpapi/src/process/mod.rs | 5 + crates/gpapi/src/process/service_launcher.rs | 72 + crates/gpapi/src/service/event.rs | 10 + crates/gpapi/src/service/mod.rs | 3 + crates/gpapi/src/service/request.rs | 118 + crates/gpapi/src/service/vpn_state.rs | 34 + crates/gpapi/src/utils/base64.rs | 21 + crates/gpapi/src/utils/crypto.rs | 108 + crates/gpapi/src/utils/endpoint.rs | 20 + crates/gpapi/src/utils/env_file.rs | 37 + crates/gpapi/src/utils/lock_file.rs | 39 + crates/gpapi/src/utils/mod.rs | 40 + crates/gpapi/src/utils/openssl.rs | 37 + crates/gpapi/src/utils/redact.rs | 227 + crates/gpapi/src/utils/shutdown_signal.rs | 22 + crates/gpapi/src/utils/window.rs | 90 + crates/gpapi/src/utils/xml.rs | 6 + crates/gpapi/tests/files/gateway_login.xml | 27 + crates/gpapi/tests/files/portal_config.xml | 212 + crates/gpapi/tests/files/prelogin_saml.xml | 22 + .../gpapi/tests/files/prelogin_standard.xml | 15 + crates/openconnect/Cargo.toml | 13 + crates/openconnect/build.rs | 12 + crates/openconnect/src/ffi/mod.rs | 71 + crates/openconnect/src/ffi/vpn.c | 144 + crates/openconnect/src/ffi/vpn.h | 68 + crates/openconnect/src/lib.rs | 5 + crates/openconnect/src/vpn.rs | 131 + crates/openconnect/src/vpnc_script.rs | 23 + debian/README.Debian | 5 - debian/changelog | 154 - debian/compat | 1 - debian/control | 13 - debian/copyright | 15 - debian/patches/series | 1 - debian/rules | 13 - debian/source/format | 1 - debian/source/local-options | 2 - debian/watch | 3 - packaging/aur/PKGBUILD | 40 - packaging/aur/PKGBUILD.in | 40 - packaging/aur/gp.install | 8 - packaging/flatpak/com.yuezk.qt.gpclient.yml | 26 - .../obs/globalprotect-openconnect-rpmlintrc | 1 - .../obs/globalprotect-openconnect.changes | 154 - packaging/obs/globalprotect-openconnect.spec | 98 - rustfmt.toml | 8 + scripts/_archive-all.sh | 26 - scripts/build.sh | 7 - scripts/bump-version.sh | 452 -- scripts/install-debian.sh | 13 - scripts/install-fedora.sh | 10 - scripts/install-opensuse.sh | 10 - scripts/install-ubuntu.sh | 13 - scripts/install.sh | 11 - scripts/prepare-packaging.sh | 46 - scripts/release-archive-all.sh | 3 - scripts/release.sh | 10 - scripts/snapshot-archive-all.sh | 14 - scripts/snapshot-version.sh | 4 - scripts/verify-debian-package.sh | 19 - snap/snapcraft.yaml | 92 - version.h.in | 1 - 199 files changed, 10153 insertions(+), 7203 deletions(-) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .github/workflows/build.yaml delete mode 100644 .github/workflows/build.yml delete mode 100644 .github/workflows/pr.yml delete mode 100644 .gitmodules create mode 100644 .vscode/extensions.json delete mode 160000 3rdparty/SingleApplication delete mode 100644 3rdparty/inih/CMakeLists.txt delete mode 100644 3rdparty/inih/LICENSE.txt delete mode 100644 3rdparty/inih/cpp/INIReader.cpp delete mode 100644 3rdparty/inih/cpp/INIReader.h delete mode 100644 3rdparty/inih/ini.c delete mode 100644 3rdparty/inih/ini.h delete mode 160000 3rdparty/plog delete mode 100644 3rdparty/qt-unix-signals/CMakeLists.txt delete mode 100644 3rdparty/qt-unix-signals/LICENCE delete mode 100644 3rdparty/qt-unix-signals/sigwatch.cpp delete mode 100644 3rdparty/qt-unix-signals/sigwatch.h delete mode 160000 3rdparty/qtkeychain delete mode 100644 CMakeLists.txt create mode 100644 Cargo.lock create mode 100644 Cargo.toml delete mode 100644 GPClient/CMakeLists.txt delete mode 100644 GPClient/cdpcommand.cpp delete mode 100644 GPClient/cdpcommand.h delete mode 100644 GPClient/cdpcommandmanager.cpp delete mode 100644 GPClient/cdpcommandmanager.h delete mode 100644 GPClient/challengedialog.cpp delete mode 100644 GPClient/challengedialog.h delete mode 100644 GPClient/challengedialog.ui delete mode 100644 GPClient/com.yuezk.qt.gpclient.desktop.in delete mode 100644 GPClient/com.yuezk.qt.gpclient.metainfo.xml.in delete mode 100644 GPClient/com.yuezk.qt.gpclient.svg delete mode 100644 GPClient/connected.png delete mode 100644 GPClient/enhancedwebpage.cpp delete mode 100644 GPClient/enhancedwebpage.h delete mode 100644 GPClient/enhancedwebview.cpp delete mode 100644 GPClient/enhancedwebview.h delete mode 100644 GPClient/gatewayauthenticator.cpp delete mode 100644 GPClient/gatewayauthenticator.h delete mode 100644 GPClient/gatewayauthenticatorparams.cpp delete mode 100644 GPClient/gatewayauthenticatorparams.h delete mode 100644 GPClient/gpclient.cpp delete mode 100644 GPClient/gpclient.h delete mode 100644 GPClient/gpclient.ui delete mode 100644 GPClient/gpgateway.cpp delete mode 100644 GPClient/gpgateway.h delete mode 100644 GPClient/gphelper.cpp delete mode 100644 GPClient/gphelper.h delete mode 100644 GPClient/loginparams.cpp delete mode 100644 GPClient/loginparams.h delete mode 100644 GPClient/main.cpp delete mode 100644 GPClient/not_connected.png delete mode 100644 GPClient/pending.png delete mode 100644 GPClient/portalauthenticator.cpp delete mode 100644 GPClient/portalauthenticator.h delete mode 100644 GPClient/portalconfigresponse.cpp delete mode 100644 GPClient/portalconfigresponse.h delete mode 100644 GPClient/preloginresponse.cpp delete mode 100644 GPClient/preloginresponse.h delete mode 100644 GPClient/radio_selected.png delete mode 100644 GPClient/radio_unselected.png delete mode 100644 GPClient/resources.qrc delete mode 100644 GPClient/samlloginwindow.cpp delete mode 100644 GPClient/samlloginwindow.h delete mode 100644 GPClient/settings_icon.png delete mode 100644 GPClient/settingsdialog.cpp delete mode 100644 GPClient/settingsdialog.h delete mode 100644 GPClient/settingsdialog.ui delete mode 100644 GPClient/standardloginwindow.cpp delete mode 100644 GPClient/standardloginwindow.h delete mode 100644 GPClient/standardloginwindow.ui delete mode 100644 GPClient/vpn.h delete mode 100644 GPClient/vpn_dbus.cpp delete mode 100644 GPClient/vpn_dbus.h delete mode 100644 GPClient/vpn_json.cpp delete mode 100644 GPClient/vpn_json.h delete mode 100644 GPService/CMakeLists.txt delete mode 100644 GPService/dbus/com.yuezk.qt.GPService.conf.in delete mode 100644 GPService/dbus/com.yuezk.qt.GPService.service.in delete mode 100644 GPService/gp.conf delete mode 100644 GPService/gpservice.cpp delete mode 100644 GPService/gpservice.h delete mode 100644 GPService/main.cpp delete mode 100644 GPService/systemd/gpservice.service.in delete mode 100644 VERSION create mode 100644 apps/gpauth/Cargo.toml create mode 100644 apps/gpauth/build.rs create mode 100644 apps/gpauth/icons/128x128.png create mode 100644 apps/gpauth/icons/128x128@2x.png create mode 100644 apps/gpauth/icons/32x32.png create mode 100644 apps/gpauth/icons/icon.icns create mode 100644 apps/gpauth/icons/icon.ico create mode 100644 apps/gpauth/icons/icon.png create mode 100644 apps/gpauth/index.html create mode 100644 apps/gpauth/src/auth_window.rs create mode 100644 apps/gpauth/src/cli.rs create mode 100644 apps/gpauth/src/main.rs create mode 100644 apps/gpauth/tauri.conf.json create mode 100644 apps/gpclient/Cargo.toml create mode 100644 apps/gpclient/src/cli.rs create mode 100644 apps/gpclient/src/connect.rs create mode 100644 apps/gpclient/src/disconnect.rs create mode 100644 apps/gpclient/src/launch_gui.rs create mode 100644 apps/gpclient/src/main.rs create mode 100644 apps/gpservice/Cargo.toml create mode 100644 apps/gpservice/com.yuezk.gpservice.policy create mode 100644 apps/gpservice/src/cli.rs create mode 100644 apps/gpservice/src/handlers.rs create mode 100644 apps/gpservice/src/main.rs create mode 100644 apps/gpservice/src/routes.rs create mode 100644 apps/gpservice/src/vpn_task.rs create mode 100644 apps/gpservice/src/ws_connection.rs create mode 100644 apps/gpservice/src/ws_server.rs delete mode 100644 cmake/Add3rdParty.cmake delete mode 100644 cmake/FindNetworkManager.cmake delete mode 100755 cmakew create mode 100644 crates/gpapi/Cargo.toml create mode 100644 crates/gpapi/src/auth.rs create mode 100644 crates/gpapi/src/credential.rs create mode 100644 crates/gpapi/src/gateway/login.rs create mode 100644 crates/gpapi/src/gateway/mod.rs create mode 100644 crates/gpapi/src/gateway/parse_gateways.rs create mode 100644 crates/gpapi/src/gp_params.rs create mode 100644 crates/gpapi/src/lib.rs create mode 100644 crates/gpapi/src/portal/config.rs create mode 100644 crates/gpapi/src/portal/mod.rs create mode 100644 crates/gpapi/src/portal/prelogin.rs create mode 100644 crates/gpapi/src/process/auth_launcher.rs create mode 100644 crates/gpapi/src/process/command_traits.rs create mode 100644 crates/gpapi/src/process/gui_launcher.rs create mode 100644 crates/gpapi/src/process/mod.rs create mode 100644 crates/gpapi/src/process/service_launcher.rs create mode 100644 crates/gpapi/src/service/event.rs create mode 100644 crates/gpapi/src/service/mod.rs create mode 100644 crates/gpapi/src/service/request.rs create mode 100644 crates/gpapi/src/service/vpn_state.rs create mode 100644 crates/gpapi/src/utils/base64.rs create mode 100644 crates/gpapi/src/utils/crypto.rs create mode 100644 crates/gpapi/src/utils/endpoint.rs create mode 100644 crates/gpapi/src/utils/env_file.rs create mode 100644 crates/gpapi/src/utils/lock_file.rs create mode 100644 crates/gpapi/src/utils/mod.rs create mode 100644 crates/gpapi/src/utils/openssl.rs create mode 100644 crates/gpapi/src/utils/redact.rs create mode 100644 crates/gpapi/src/utils/shutdown_signal.rs create mode 100644 crates/gpapi/src/utils/window.rs create mode 100644 crates/gpapi/src/utils/xml.rs create mode 100644 crates/gpapi/tests/files/gateway_login.xml create mode 100644 crates/gpapi/tests/files/portal_config.xml create mode 100644 crates/gpapi/tests/files/prelogin_saml.xml create mode 100644 crates/gpapi/tests/files/prelogin_standard.xml create mode 100644 crates/openconnect/Cargo.toml create mode 100644 crates/openconnect/build.rs create mode 100644 crates/openconnect/src/ffi/mod.rs create mode 100644 crates/openconnect/src/ffi/vpn.c create mode 100644 crates/openconnect/src/ffi/vpn.h create mode 100644 crates/openconnect/src/lib.rs create mode 100644 crates/openconnect/src/vpn.rs create mode 100644 crates/openconnect/src/vpnc_script.rs delete mode 100644 debian/README.Debian delete mode 100644 debian/changelog delete mode 100644 debian/compat delete mode 100644 debian/control delete mode 100644 debian/copyright delete mode 100644 debian/patches/series delete mode 100755 debian/rules delete mode 100644 debian/source/format delete mode 100644 debian/source/local-options delete mode 100644 debian/watch delete mode 100644 packaging/aur/PKGBUILD delete mode 100644 packaging/aur/PKGBUILD.in delete mode 100755 packaging/aur/gp.install delete mode 100644 packaging/flatpak/com.yuezk.qt.gpclient.yml delete mode 100644 packaging/obs/globalprotect-openconnect-rpmlintrc delete mode 100644 packaging/obs/globalprotect-openconnect.changes delete mode 100644 packaging/obs/globalprotect-openconnect.spec create mode 100644 rustfmt.toml delete mode 100755 scripts/_archive-all.sh delete mode 100755 scripts/build.sh delete mode 100755 scripts/bump-version.sh delete mode 100755 scripts/install-debian.sh delete mode 100755 scripts/install-fedora.sh delete mode 100755 scripts/install-opensuse.sh delete mode 100755 scripts/install-ubuntu.sh delete mode 100755 scripts/install.sh delete mode 100755 scripts/prepare-packaging.sh delete mode 100755 scripts/release-archive-all.sh delete mode 100755 scripts/release.sh delete mode 100755 scripts/snapshot-archive-all.sh delete mode 100755 scripts/snapshot-version.sh delete mode 100755 scripts/verify-debian-package.sh delete mode 100644 snap/snapcraft.yaml delete mode 100644 version.h.in diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..419fc3d --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,62 @@ +FROM ubuntu:18.04 + +ARG USERNAME=vscode +ARG USER_UID=1000 +ARG USER_GID=$USER_UID + +ENV RUSTUP_HOME=/usr/local/rustup \ + CARGO_HOME=/usr/local/cargo \ + PATH=/usr/local/cargo/bin:$PATH \ + RUST_VERSION=1.75.0 + +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + sudo \ + ca-certificates \ + curl \ + gnupg \ + git \ + less \ + software-properties-common \ + # Tauri dependencies + libwebkit2gtk-4.0-dev build-essential wget libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev; \ + # Install openconnect + add-apt-repository ppa:yuezk/globalprotect-openconnect; \ + apt-get update; \ + apt-get install -y openconnect libopenconnect-dev; \ + # Create a non-root user + groupadd --gid $USER_GID $USERNAME; \ + useradd --uid $USER_UID --gid $USER_GID -m $USERNAME; \ + echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME; \ + chmod 0440 /etc/sudoers.d/$USERNAME; \ + # Install Node.js + mkdir -p /etc/apt/keyrings; \ + curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg; \ + echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_16.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list; \ + apt-get update; \ + apt-get install -y nodejs; \ + corepack enable; \ + # Install diff-so-fancy + npm install -g diff-so-fancy; \ + # Install Rust + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain $RUST_VERSION; \ + chown -R $USERNAME:$USERNAME $RUSTUP_HOME $CARGO_HOME; \ + rustup --version; \ + cargo --version; \ + rustc --version + +USER $USERNAME + +# Install Oh My Zsh +RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/v1.1.5/zsh-in-docker.sh)" -- \ + -t https://github.com/denysdovhan/spaceship-prompt \ + -a 'SPACESHIP_PROMPT_ADD_NEWLINE="false"' \ + -a 'SPACESHIP_PROMPT_SEPARATE_LINE="false"' \ + -p git \ + -p https://github.com/zsh-users/zsh-autosuggestions \ + -p https://github.com/zsh-users/zsh-completions; \ + # Change the default shell + sudo chsh -s /bin/zsh $USERNAME; \ + # Change the XTERM to xterm-256color + sed -i 's/TERM=xterm/TERM=xterm-256color/g' $HOME/.zshrc; diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..c48cded --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,10 @@ +{ + "build": { + "dockerfile": "Dockerfile" + }, + "runArgs": [ + "--privileged", + "--cap-add=NET_ADMIN", + "--device=/dev/net/tun" + ] +} diff --git a/.editorconfig b/.editorconfig index ad0694f..9d08a1a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,13 +1,9 @@ -# top-most EditorConfig file root = true -# Unix-style newlines with a newline ending every file [*] -end_of_line = lf -insert_final_newline = false -trim_trailing_whitespace=true +charset = utf-8 indent_style = space -indent_size = 4 - -[*.sh] -indent_style = tab +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..38c5e72 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,250 @@ +name: Build GPGUI +on: + push: + paths-ignore: + - LICENSE + - "*.md" + - .vscode + - .devcontainer + branches: + - main + # tags: + # - v*.*.* +jobs: + # Include arm64 if ref is a tag + setup-matrix: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - name: Set up matrix + id: set-matrix + run: | + if [[ "${{ github.ref }}" == "refs/tags/"* ]]; then + echo "matrix=[\"amd64\", \"arm64\"]" >> $GITHUB_OUTPUT + else + echo "matrix=[\"amd64\"]" >> $GITHUB_OUTPUT + fi + + build-fe: + runs-on: ubuntu-latest + steps: + - name: Checkout gpgui repo + uses: actions/checkout@v4 + with: + token: ${{ secrets.GH_PAT }} + repository: yuezk/gpgui + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 18 + + - uses: pnpm/action-setup@v2 + with: + version: 8 + + - name: Install dependencies + run: | + cd app + pnpm install + - name: Build + run: | + cd app + pnpm run build + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: gpgui-fe + path: app/dist + + build-tauri: + needs: [setup-matrix, build-fe] + runs-on: ubuntu-latest + strategy: + matrix: + arch: ${{ fromJson(needs.setup-matrix.outputs.matrix) }} + steps: + - name: Checkout gpgui repo + uses: actions/checkout@v4 + with: + token: ${{ secrets.GH_PAT }} + repository: yuezk/gpgui + path: gpgui + + - name: Checkout gp repo + uses: actions/checkout@v4 + with: + token: ${{ secrets.GH_PAT }} + repository: yuezk/GlobalProtect-openconnect + path: gp + + - name: Download gpgui-fe artifact + uses: actions/download-artifact@v4 + with: + name: gpgui-fe + path: gpgui/app/dist + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: ${{ matrix.arch }} + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + + - name: Build Tauri in Docker + run: | + docker run \ + --rm \ + -v $(pwd):/${{ github.workspace }} \ + -w ${{ github.workspace }} \ + -e CI=true \ + --platform linux/${{ matrix.arch }} \ + yuezk/gpdev:main \ + "./gpgui/scripts/build.sh" + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: artifact-${{ matrix.arch }}-tauri + path: | + gpgui/.tmp/artifact + + package-rpm: + needs: [setup-matrix, build-tauri] + runs-on: ubuntu-latest + strategy: + matrix: + arch: ${{ fromJson(needs.setup-matrix.outputs.matrix) }} + steps: + - name: Checkout gpgui repo + uses: actions/checkout@v4 + with: + token: ${{ secrets.GH_PAT }} + repository: yuezk/gpgui + path: gpgui + + - name: Download artifact-${{ matrix.arch }} + uses: actions/download-artifact@v4 + with: + name: artifact-${{ matrix.arch }}-tauri + path: gpgui/.tmp/artifact + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: ${{ matrix.arch }} + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + + - name: Create RPM package + run: | + docker run \ + --rm \ + -v $(pwd):/${{ github.workspace }} \ + -w ${{ github.workspace }} \ + --platform linux/${{ matrix.arch }} \ + yuezk/gpdev:rpm-builder \ + "./gpgui/scripts/build-rpm.sh" + + - name: Upload rpm artifacts + uses: actions/upload-artifact@v4 + with: + name: artifact-${{ matrix.arch }}-rpm + path: | + gpgui/.tmp/artifact/*.rpm + + package-pkgbuild: + needs: [setup-matrix, build-tauri] + runs-on: ubuntu-latest + strategy: + matrix: + arch: ${{ fromJson(needs.setup-matrix.outputs.matrix) }} + steps: + - name: Checkout gpgui repo + uses: actions/checkout@v4 + with: + token: ${{ secrets.GH_PAT }} + repository: yuezk/gpgui + path: gpgui + + - name: Download artifact-${{ matrix.arch }} + uses: actions/download-artifact@v4 + with: + name: artifact-${{ matrix.arch }}-tauri + path: gpgui/.tmp/artifact + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: ${{ matrix.arch }} + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + + - name: Generate PKGBUILD + run: | + ./gpgui/scripts/generate-pkgbuild.sh + + - name: Build PKGBUILD package + run: | + # Generate PKGBUILD to .tmp/pkgbuild + ./gpgui/scripts/generate-pkgbuild.sh + + # Build package + docker run \ + --rm \ + -v $(pwd)/gpgui/.tmp/pkgbuild:/pkgbuild \ + --platform linux/${{ matrix.arch }} \ + yuezk/gpdev:pkgbuild + + - name: Upload pkgbuild artifacts + uses: actions/upload-artifact@v4 + with: + name: artifact-${{ matrix.arch }}-pkgbuild + path: | + gpgui/.tmp/pkgbuild/*.pkg.tar.zst + + gh-release: + if: startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + needs: + - package-rpm + - package-pkgbuild + + steps: + - name: Download artifact + uses: actions/download-artifact@v4 + with: + path: artifact + pattern: artifact-* + merge-multiple: true + + - name: Generate checksum + uses: jmgilman/actions-generate-checksum@v1 + with: + output: checksums.txt + patterns: | + artifact/* + + - name: Create GH release + uses: softprops/action-gh-release@v1 + with: + token: ${{ secrets.GH_PAT }} + prerelease: contains(github.ref, 'latest') + fail_on_unmatched_files: true + files: | + checksums.txt + artifact/* diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 78d0262..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,275 +0,0 @@ -name: Build - -on: - push: - branches: - - master - - develop - tags: - - "v*.*.*" - paths-ignore: - - LICENSE - - "*.md" - - .vscode - workflow_dispatch: - -# A workflow run is made up of one or more jobs that can run sequentially or in parallel -jobs: - build: - strategy: - matrix: - os: [ubuntu-18.04, ubuntu-20.04, ubuntu-22.04] - - runs-on: ${{ matrix.os }} - - steps: - # Checkout repository and submodules - - uses: actions/checkout@v2 - with: - submodules: recursive - - - name: Build - run: | - ./scripts/install-ubuntu.sh - # assert no library missing - test $(ldd $(which gpclient) | grep 'not found' | wc -l) -eq 0 - - snapshot-archive-all: - if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/develop' }} - needs: build - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - with: - submodules: recursive - fetch-depth: 0 - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install git-archive-all - - - name: Archive all - run: | - ./scripts/snapshot-archive-all.sh - - - name: Verify debian package - run: | - ./scripts/verify-debian-package.sh - - - uses: actions/upload-artifact@v2 - with: - name: snapshot-source-code - path: ./artifacts/* - - snapshot-ppa: - if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/develop' }} - needs: snapshot-archive-all - runs-on: ubuntu-latest - - steps: - - uses: actions/download-artifact@v2 - with: - name: snapshot-source-code - path: artifacts - - - name: Extract source code - run: | - cd $GITHUB_WORKSPACE/artifacts - mkdir deb-build && cp *.tar.gz deb-build && cd deb-build - tar xf *.tar.gz - - - name: Publish PPA - uses: yuezk/publish-ppa-package@develop - with: - repository: 'ppa:yuezk/globalprotect-openconnect-snapshot' - gpg_private_key: ${{ secrets.PPA_GPG_PRIVATE_KEY }} - gpg_passphrase: ${{ secrets.PPA_GPG_PASSPHRASE }} - pkgdir: '${{ github.workspace }}/artifacts/deb-build/globalprotect-openconnect*/' - - snapshot-aur: - if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/develop' }} - needs: snapshot-archive-all - runs-on: ubuntu-latest - - steps: - - uses: actions/download-artifact@v2 - with: - name: snapshot-source-code - path: artifacts - - - name: Publish AUR package - uses: yuezk/github-actions-deploy-aur@update-pkgver - with: - pkgname: globalprotect-openconnect-git - pkgbuild: ./artifacts/aur/PKGBUILD - assets: ./artifacts/aur/gp.install - update_pkgver: true - commit_username: ${{ secrets.AUR_USERNAME }} - commit_email: ${{ secrets.AUR_EMAIL }} - ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} - commit_message: 'Snapshot release: git#${{ github.sha }}' - - snapshot-obs: - if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/develop' }} - needs: snapshot-archive-all - runs-on: ubuntu-latest - - steps: - - uses: actions/download-artifact@v2 - with: - name: snapshot-source-code - path: artifacts - - - uses: yuezk/publish-obs-package@main - with: - project: home:yuezk - package: globalprotect-openconnect-snapshot - username: yuezk - password: ${{ secrets.OBS_PASSWORD }} - files: ./artifacts/obs/* - - snapshot-snap: - # if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/develop' }} - if: ${{ false }} - needs: snapshot-archive-all - runs-on: ubuntu-latest - - steps: - - uses: actions/download-artifact@v2 - with: - name: snapshot-source-code - path: artifacts - - - name: Extract source code - run: | - mkdir snap-source - tar xvf ./artifacts/globalprotect-openconnect-*tar.gz \ - --directory snap-source \ - --strip 1 - - - uses: snapcore/action-build@v1 - id: build - with: - path: ./snap-source - - - uses: snapcore/action-publish@v1 - with: - store_login: ${{ secrets.SNAPSTORE_LOGIN }} - snap: ${{ steps.build.outputs.snap }} - release: edge - - release-archive-all: - if: startsWith(github.ref, 'refs/tags/v') - needs: build - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - with: - submodules: recursive - fetch-depth: 0 - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install git-archive-all - - - name: Archive all - run: | - ./scripts/release-archive-all.sh - - - name: Verify debian package - run: | - ./scripts/verify-debian-package.sh - - - uses: actions/upload-artifact@v2 - with: - name: release-source-code - path: ./artifacts/* - - release-ppa: - if: startsWith(github.ref, 'refs/tags/v') - needs: release-archive-all - runs-on: ubuntu-latest - - steps: - - uses: actions/download-artifact@v2 - with: - name: release-source-code - path: artifacts - - - name: Extract source code - run: | - cd $GITHUB_WORKSPACE/artifacts - mkdir deb-build && cp *.tar.gz deb-build && cd deb-build - tar xf *.tar.gz - - - name: Publish PPA - uses: yuezk/publish-ppa-package@develop - with: - repository: 'ppa:yuezk/globalprotect-openconnect' - gpg_private_key: ${{ secrets.PPA_GPG_PRIVATE_KEY }} - gpg_passphrase: ${{ secrets.PPA_GPG_PASSPHRASE }} - pkgdir: '${{ github.workspace }}/artifacts/deb-build/globalprotect-openconnect*/' - - release-aur: - if: startsWith(github.ref, 'refs/tags/v') - needs: release-archive-all - runs-on: ubuntu-latest - - steps: - - uses: actions/download-artifact@v2 - with: - name: release-source-code - path: artifacts - - - name: Publish AUR package - uses: yuezk/github-actions-deploy-aur@update-pkgver - with: - pkgname: globalprotect-openconnect-git - pkgbuild: ./artifacts/aur/PKGBUILD - assets: ./artifacts/aur/gp.install - update_pkgver: true - commit_username: ${{ secrets.AUR_USERNAME }} - commit_email: ${{ secrets.AUR_EMAIL }} - ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} - commit_message: 'Release ${{ github.ref }}' - - release-obs: - if: startsWith(github.ref, 'refs/tags/v') - needs: release-archive-all - runs-on: ubuntu-latest - - steps: - - uses: actions/download-artifact@v2 - with: - name: release-source-code - path: artifacts - - - uses: yuezk/publish-obs-package@main - with: - project: home:yuezk - package: globalprotect-openconnect - username: yuezk - password: ${{ secrets.OBS_PASSWORD }} - files: ./artifacts/obs/* - - release-github: - if: startsWith(github.ref, 'refs/tags/v') - needs: - - release-ppa - - release-aur - - release-obs - runs-on: ubuntu-latest - - steps: - - uses: actions/download-artifact@v2 - with: - name: release-source-code - path: artifacts - - uses: softprops/action-gh-release@v1 - with: - files: | - ./artifacts/*.tar.gz diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml deleted file mode 100644 index 3a2c2d6..0000000 --- a/.github/workflows/pr.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: PR Build - -on: - pull_request: - branches: - - master - - develop - paths-ignore: - - LICENSE - - "*.md" - - .vscode - workflow_dispatch: - -# A workflow run is made up of one or more jobs that can run sequentially or in parallel -jobs: - build: - strategy: - matrix: - os: [ubuntu-18.04, ubuntu-20.04] - - runs-on: ${{ matrix.os }} - - steps: - # Checkout repository and submodules - - uses: actions/checkout@v2 - with: - submodules: recursive - - - name: Build - run: | - ./scripts/install-ubuntu.sh - # assert no library missing - test $(ldd $(which gpclient) | grep 'not found' | wc -l) -eq 0 diff --git a/.gitignore b/.gitignore index df170ab..20282f4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,70 +1,4 @@ -# Binaries -*.rpm -*.gz -*.snap -.DS_Store -build-debian -build -artifacts - -.cmake .idea - -# Auto generated DBus files -*_adaptor.cpp -*_adaptor.h - -gpservice_interface.* - -# C++ objects and libs -*.slo -*.lo -*.o -*.a -*.la -*.lai -*.so -*.so.* -*.dll -*.dylib - -# Qt-es -object_script.*.Release -object_script.*.Debug -*_plugin_import.cpp -/.qmake.cache -/.qmake.stash -*.pro.user -*.pro.user.* -*.qbs.user -*.qbs.user.* -*.moc -moc_*.cpp -moc_*.h -qrc_*.cpp -ui_*.h -*.qmlc -*.jsc -Makefile* -*build-* -*.qm -*.prl - -# Qt unit tests -target_wrapper.* - -# QtCreator -*.autosave - -# QtCreator Qml -*.qmlproject.user -*.qmlproject.user.* - -# QtCreator CMake -CMakeLists.txt.user* - -# QtCreator 4.8< compilation database -compile_commands.json - -# QtCreator local machine specific files for imported projects -*creator.user* +/target +.pnpm-store +.env diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 706e889..0000000 --- a/.gitmodules +++ /dev/null @@ -1,10 +0,0 @@ -[submodule "singleapplication"] - path = 3rdparty/SingleApplication - url = https://github.com/itay-grudev/SingleApplication.git - -[submodule "plog"] - path = 3rdparty/plog - url = https://github.com/SergiusTheBest/plog.git -[submodule "3rdparty/qtkeychain"] - path = 3rdparty/qtkeychain - url = git@github.com:frankosterfeld/qtkeychain.git diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..268bdcb --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + "recommendations": [ + "rust-lang.rust-analyzer", + "tamasfe.even-better-toml", + "eamodio.gitlens", + "EditorConfig.EditorConfig", + "streetsidesoftware.code-spell-checker", + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 94d245a..6426a3a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,26 +1,51 @@ { - "files.watcherExclude": { - "**/artifacts/**": true, - }, - "files.associations": { - "qregularexpression": "cpp", - "qfileinfo": "cpp", - "qregularexpressionmatch": "cpp", - "qdatetime": "cpp", - "qprocess": "cpp", - "qobject": "cpp", - "qstandardpaths": "cpp", - "qmainwindow": "cpp", - "qsystemtrayicon": "cpp", - "qpushbutton": "cpp", - "qmenu": "cpp", - "qjsondocument": "cpp", - "qnetworkaccessmanager": "cpp", - "qwebengineview": "cpp", - "qprocessenvironment": "cpp", - "qnetworkreply": "cpp", - "qicon": "cpp", - "qsslsocket": "cpp", - "qapplication": "cpp" - } + "cSpell.words": [ + "authcookie", + "bincode", + "chacha", + "clientos", + "datetime", + "disconnectable", + "distro", + "dotenv", + "dotenvy", + "getconfig", + "gpapi", + "gpauth", + "gpclient", + "gpcommon", + "gpgui", + "gpservice", + "hidpi", + "jnlp", + "LOGNAME", + "oneshot", + "openconnect", + "pkexec", + "Prelogin", + "prelogon", + "prelogonuserauthcookie", + "repr", + "reqwest", + "roxmltree", + "rspc", + "servercert", + "specta", + "sysinfo", + "tanstack", + "tauri", + "tempfile", + "thiserror", + "tungstenite", + "unistd", + "unlisten", + "urlencoding", + "userauthcookie", + "utsbuf", + "Vite", + "vpnc", + "vpninfo", + "wmctrl", + "XAUTHORITY" + ] } diff --git a/3rdparty/SingleApplication b/3rdparty/SingleApplication deleted file mode 160000 index bdbb09b..0000000 --- a/3rdparty/SingleApplication +++ /dev/null @@ -1 +0,0 @@ -Subproject commit bdbb09b5f21ebea4cd7dfb43b29114a94e04a3a1 diff --git a/3rdparty/inih/CMakeLists.txt b/3rdparty/inih/CMakeLists.txt deleted file mode 100644 index b4faa58..0000000 --- a/3rdparty/inih/CMakeLists.txt +++ /dev/null @@ -1,12 +0,0 @@ -cmake_minimum_required(VERSION 3.10.0) - -set(CMAKE_CXX_STANDARD 17) -project(inih) - -add_library(inih STATIC - ini.h - ini.c - cpp/INIReader.h - cpp/INIReader.cpp -) -target_include_directories(inih PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/cpp") diff --git a/3rdparty/inih/LICENSE.txt b/3rdparty/inih/LICENSE.txt deleted file mode 100644 index cb7ee2d..0000000 --- a/3rdparty/inih/LICENSE.txt +++ /dev/null @@ -1,27 +0,0 @@ - -The "inih" library is distributed under the New BSD license: - -Copyright (c) 2009, Ben Hoyt -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * Neither the name of Ben Hoyt nor the names of its contributors - may be used to endorse or promote products derived from this software - without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY BEN HOYT ''AS IS'' AND ANY -EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL BEN HOYT BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/3rdparty/inih/cpp/INIReader.cpp b/3rdparty/inih/cpp/INIReader.cpp deleted file mode 100644 index 1bdac40..0000000 --- a/3rdparty/inih/cpp/INIReader.cpp +++ /dev/null @@ -1,116 +0,0 @@ -// Read an INI file into easy-to-access name/value pairs. - -// SPDX-License-Identifier: BSD-3-Clause - -// Copyright (C) 2009-2020, Ben Hoyt - -// inih and INIReader are released under the New BSD license (see LICENSE.txt). -// Go to the project home page for more info: -// -// https://github.com/benhoyt/inih - -#include -#include -#include -#include "../ini.h" -#include "INIReader.h" - -using std::string; - -INIReader::INIReader(const string& filename) -{ - _error = ini_parse(filename.c_str(), ValueHandler, this); -} - -INIReader::INIReader(const char *buffer, size_t buffer_size) -{ - string content(buffer, buffer_size); - _error = ini_parse_string(content.c_str(), ValueHandler, this); -} - -int INIReader::ParseError() const -{ - return _error; -} - -string INIReader::Get(const string& section, const string& name, const string& default_value) const -{ - string key = MakeKey(section, name); - // Use _values.find() here instead of _values.at() to support pre C++11 compilers - return _values.count(key) ? _values.find(key)->second : default_value; -} - -string INIReader::GetString(const string& section, const string& name, const string& default_value) const -{ - const string str = Get(section, name, ""); - return str.empty() ? default_value : str; -} - -long INIReader::GetInteger(const string& section, const string& name, long default_value) const -{ - string valstr = Get(section, name, ""); - const char* value = valstr.c_str(); - char* end; - // This parses "1234" (decimal) and also "0x4D2" (hex) - long n = strtol(value, &end, 0); - return end > value ? n : default_value; -} - -double INIReader::GetReal(const string& section, const string& name, double default_value) const -{ - string valstr = Get(section, name, ""); - const char* value = valstr.c_str(); - char* end; - double n = strtod(value, &end); - return end > value ? n : default_value; -} - -bool INIReader::GetBoolean(const string& section, const string& name, bool default_value) const -{ - string valstr = Get(section, name, ""); - // Convert to lower case to make string comparisons case-insensitive - std::transform(valstr.begin(), valstr.end(), valstr.begin(), ::tolower); - if (valstr == "true" || valstr == "yes" || valstr == "on" || valstr == "1") - return true; - else if (valstr == "false" || valstr == "no" || valstr == "off" || valstr == "0") - return false; - else - return default_value; -} - -bool INIReader::HasSection(const string& section) const -{ - const string key = MakeKey(section, ""); - std::map::const_iterator pos = _values.lower_bound(key); - if (pos == _values.end()) - return false; - // Does the key at the lower_bound pos start with "section"? - return pos->first.compare(0, key.length(), key) == 0; -} - -bool INIReader::HasValue(const string& section, const string& name) const -{ - string key = MakeKey(section, name); - return _values.count(key); -} - -string INIReader::MakeKey(const string& section, const string& name) -{ - string key = section + "=" + name; - // Convert to lower case to make section/name lookups case-insensitive - std::transform(key.begin(), key.end(), key.begin(), ::tolower); - return key; -} - -int INIReader::ValueHandler(void* user, const char* section, const char* name, - const char* value) -{ - if (!name) // Happens when INI_CALL_HANDLER_ON_NEW_SECTION enabled - return 1; - INIReader* reader = static_cast(user); - string key = MakeKey(section, name); - if (reader->_values[key].size() > 0) - reader->_values[key] += "\n"; - reader->_values[key] += value ? value : ""; - return 1; -} diff --git a/3rdparty/inih/cpp/INIReader.h b/3rdparty/inih/cpp/INIReader.h deleted file mode 100644 index 1571756..0000000 --- a/3rdparty/inih/cpp/INIReader.h +++ /dev/null @@ -1,94 +0,0 @@ -// Read an INI file into easy-to-access name/value pairs. - -// SPDX-License-Identifier: BSD-3-Clause - -// Copyright (C) 2009-2020, Ben Hoyt - -// inih and INIReader are released under the New BSD license (see LICENSE.txt). -// Go to the project home page for more info: -// -// https://github.com/benhoyt/inih - -#ifndef INIREADER_H -#define INIREADER_H - -#include -#include - -// Visibility symbols, required for Windows DLLs -#ifndef INI_API -#if defined _WIN32 || defined __CYGWIN__ -# ifdef INI_SHARED_LIB -# ifdef INI_SHARED_LIB_BUILDING -# define INI_API __declspec(dllexport) -# else -# define INI_API __declspec(dllimport) -# endif -# else -# define INI_API -# endif -#else -# if defined(__GNUC__) && __GNUC__ >= 4 -# define INI_API __attribute__ ((visibility ("default"))) -# else -# define INI_API -# endif -#endif -#endif - -// Read an INI file into easy-to-access name/value pairs. (Note that I've gone -// for simplicity here rather than speed, but it should be pretty decent.) -class INIReader -{ -public: - // Construct INIReader and parse given filename. See ini.h for more info - // about the parsing. - INI_API explicit INIReader(const std::string& filename); - - // Construct INIReader and parse given buffer. See ini.h for more info - // about the parsing. - INI_API explicit INIReader(const char *buffer, size_t buffer_size); - - // Return the result of ini_parse(), i.e., 0 on success, line number of - // first error on parse error, or -1 on file open error. - INI_API int ParseError() const; - - // Get a string value from INI file, returning default_value if not found. - INI_API std::string Get(const std::string& section, const std::string& name, - const std::string& default_value) const; - - // Get a string value from INI file, returning default_value if not found, - // empty, or contains only whitespace. - INI_API std::string GetString(const std::string& section, const std::string& name, - const std::string& default_value) const; - - // Get an integer (long) value from INI file, returning default_value if - // not found or not a valid integer (decimal "1234", "-1234", or hex "0x4d2"). - INI_API long GetInteger(const std::string& section, const std::string& name, long default_value) const; - - // Get a real (floating point double) value from INI file, returning - // default_value if not found or not a valid floating point value - // according to strtod(). - INI_API double GetReal(const std::string& section, const std::string& name, double default_value) const; - - // Get a boolean value from INI file, returning default_value if not found or if - // not a valid true/false value. Valid true values are "true", "yes", "on", "1", - // and valid false values are "false", "no", "off", "0" (not case sensitive). - INI_API bool GetBoolean(const std::string& section, const std::string& name, bool default_value) const; - - // Return true if the given section exists (section must contain at least - // one name=value pair). - INI_API bool HasSection(const std::string& section) const; - - // Return true if a value exists with the given section and field names. - INI_API bool HasValue(const std::string& section, const std::string& name) const; - -private: - int _error; - std::map _values; - static std::string MakeKey(const std::string& section, const std::string& name); - static int ValueHandler(void* user, const char* section, const char* name, - const char* value); -}; - -#endif // INIREADER_H diff --git a/3rdparty/inih/ini.c b/3rdparty/inih/ini.c deleted file mode 100644 index f8a3ea3..0000000 --- a/3rdparty/inih/ini.c +++ /dev/null @@ -1,298 +0,0 @@ -/* inih -- simple .INI file parser - -SPDX-License-Identifier: BSD-3-Clause - -Copyright (C) 2009-2020, Ben Hoyt - -inih is released under the New BSD license (see LICENSE.txt). Go to the project -home page for more info: - -https://github.com/benhoyt/inih - -*/ - -#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) -#define _CRT_SECURE_NO_WARNINGS -#endif - -#include -#include -#include - -#include "ini.h" - -#if !INI_USE_STACK -#if INI_CUSTOM_ALLOCATOR -#include -void* ini_malloc(size_t size); -void ini_free(void* ptr); -void* ini_realloc(void* ptr, size_t size); -#else -#include -#define ini_malloc malloc -#define ini_free free -#define ini_realloc realloc -#endif -#endif - -#define MAX_SECTION 50 -#define MAX_NAME 50 - -/* Used by ini_parse_string() to keep track of string parsing state. */ -typedef struct { - const char* ptr; - size_t num_left; -} ini_parse_string_ctx; - -/* Strip whitespace chars off end of given string, in place. Return s. */ -static char* rstrip(char* s) -{ - char* p = s + strlen(s); - while (p > s && isspace((unsigned char)(*--p))) - *p = '\0'; - return s; -} - -/* Return pointer to first non-whitespace char in given string. */ -static char* lskip(const char* s) -{ - while (*s && isspace((unsigned char)(*s))) - s++; - return (char*)s; -} - -/* Return pointer to first char (of chars) or inline comment in given string, - or pointer to NUL at end of string if neither found. Inline comment must - be prefixed by a whitespace character to register as a comment. */ -static char* find_chars_or_comment(const char* s, const char* chars) -{ -#if INI_ALLOW_INLINE_COMMENTS - int was_space = 0; - while (*s && (!chars || !strchr(chars, *s)) && - !(was_space && strchr(INI_INLINE_COMMENT_PREFIXES, *s))) { - was_space = isspace((unsigned char)(*s)); - s++; - } -#else - while (*s && (!chars || !strchr(chars, *s))) { - s++; - } -#endif - return (char*)s; -} - -/* Similar to strncpy, but ensures dest (size bytes) is - NUL-terminated, and doesn't pad with NULs. */ -static char* strncpy0(char* dest, const char* src, size_t size) -{ - /* Could use strncpy internally, but it causes gcc warnings (see issue #91) */ - size_t i; - for (i = 0; i < size - 1 && src[i]; i++) - dest[i] = src[i]; - dest[i] = '\0'; - return dest; -} - -/* See documentation in header file. */ -int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler, - void* user) -{ - /* Uses a fair bit of stack (use heap instead if you need to) */ -#if INI_USE_STACK - char line[INI_MAX_LINE]; - int max_line = INI_MAX_LINE; -#else - char* line; - size_t max_line = INI_INITIAL_ALLOC; -#endif -#if INI_ALLOW_REALLOC && !INI_USE_STACK - char* new_line; - size_t offset; -#endif - char section[MAX_SECTION] = ""; - char prev_name[MAX_NAME] = ""; - - char* start; - char* end; - char* name; - char* value; - int lineno = 0; - int error = 0; - -#if !INI_USE_STACK - line = (char*)ini_malloc(INI_INITIAL_ALLOC); - if (!line) { - return -2; - } -#endif - -#if INI_HANDLER_LINENO -#define HANDLER(u, s, n, v) handler(u, s, n, v, lineno) -#else -#define HANDLER(u, s, n, v) handler(u, s, n, v) -#endif - - /* Scan through stream line by line */ - while (reader(line, (int)max_line, stream) != NULL) { -#if INI_ALLOW_REALLOC && !INI_USE_STACK - offset = strlen(line); - while (offset == max_line - 1 && line[offset - 1] != '\n') { - max_line *= 2; - if (max_line > INI_MAX_LINE) - max_line = INI_MAX_LINE; - new_line = ini_realloc(line, max_line); - if (!new_line) { - ini_free(line); - return -2; - } - line = new_line; - if (reader(line + offset, (int)(max_line - offset), stream) == NULL) - break; - if (max_line >= INI_MAX_LINE) - break; - offset += strlen(line + offset); - } -#endif - - lineno++; - - start = line; -#if INI_ALLOW_BOM - if (lineno == 1 && (unsigned char)start[0] == 0xEF && - (unsigned char)start[1] == 0xBB && - (unsigned char)start[2] == 0xBF) { - start += 3; - } -#endif - start = lskip(rstrip(start)); - - if (strchr(INI_START_COMMENT_PREFIXES, *start)) { - /* Start-of-line comment */ - } -#if INI_ALLOW_MULTILINE - else if (*prev_name && *start && start > line) { - /* Non-blank line with leading whitespace, treat as continuation - of previous name's value (as per Python configparser). */ - if (!HANDLER(user, section, prev_name, start) && !error) - error = lineno; - } -#endif - else if (*start == '[') { - /* A "[section]" line */ - end = find_chars_or_comment(start + 1, "]"); - if (*end == ']') { - *end = '\0'; - strncpy0(section, start + 1, sizeof(section)); - *prev_name = '\0'; -#if INI_CALL_HANDLER_ON_NEW_SECTION - if (!HANDLER(user, section, NULL, NULL) && !error) - error = lineno; -#endif - } - else if (!error) { - /* No ']' found on section line */ - error = lineno; - } - } - else if (*start) { - /* Not a comment, must be a name[=:]value pair */ - end = find_chars_or_comment(start, "=:"); - if (*end == '=' || *end == ':') { - *end = '\0'; - name = rstrip(start); - value = end + 1; -#if INI_ALLOW_INLINE_COMMENTS - end = find_chars_or_comment(value, NULL); - if (*end) - *end = '\0'; -#endif - value = lskip(value); - rstrip(value); - - /* Valid name[=:]value pair found, call handler */ - strncpy0(prev_name, name, sizeof(prev_name)); - if (!HANDLER(user, section, name, value) && !error) - error = lineno; - } - else if (!error) { - /* No '=' or ':' found on name[=:]value line */ -#if INI_ALLOW_NO_VALUE - *end = '\0'; - name = rstrip(start); - if (!HANDLER(user, section, name, NULL) && !error) - error = lineno; -#else - error = lineno; -#endif - } - } - -#if INI_STOP_ON_FIRST_ERROR - if (error) - break; -#endif - } - -#if !INI_USE_STACK - ini_free(line); -#endif - - return error; -} - -/* See documentation in header file. */ -int ini_parse_file(FILE* file, ini_handler handler, void* user) -{ - return ini_parse_stream((ini_reader)fgets, file, handler, user); -} - -/* See documentation in header file. */ -int ini_parse(const char* filename, ini_handler handler, void* user) -{ - FILE* file; - int error; - - file = fopen(filename, "r"); - if (!file) - return -1; - error = ini_parse_file(file, handler, user); - fclose(file); - return error; -} - -/* An ini_reader function to read the next line from a string buffer. This - is the fgets() equivalent used by ini_parse_string(). */ -static char* ini_reader_string(char* str, int num, void* stream) { - ini_parse_string_ctx* ctx = (ini_parse_string_ctx*)stream; - const char* ctx_ptr = ctx->ptr; - size_t ctx_num_left = ctx->num_left; - char* strp = str; - char c; - - if (ctx_num_left == 0 || num < 2) - return NULL; - - while (num > 1 && ctx_num_left != 0) { - c = *ctx_ptr++; - ctx_num_left--; - *strp++ = c; - if (c == '\n') - break; - num--; - } - - *strp = '\0'; - ctx->ptr = ctx_ptr; - ctx->num_left = ctx_num_left; - return str; -} - -/* See documentation in header file. */ -int ini_parse_string(const char* string, ini_handler handler, void* user) { - ini_parse_string_ctx ctx; - - ctx.ptr = string; - ctx.num_left = strlen(string); - return ini_parse_stream((ini_reader)ini_reader_string, &ctx, handler, - user); -} diff --git a/3rdparty/inih/ini.h b/3rdparty/inih/ini.h deleted file mode 100644 index d1a2ba8..0000000 --- a/3rdparty/inih/ini.h +++ /dev/null @@ -1,178 +0,0 @@ -/* inih -- simple .INI file parser - -SPDX-License-Identifier: BSD-3-Clause - -Copyright (C) 2009-2020, Ben Hoyt - -inih is released under the New BSD license (see LICENSE.txt). Go to the project -home page for more info: - -https://github.com/benhoyt/inih - -*/ - -#ifndef INI_H -#define INI_H - -/* Make this header file easier to include in C++ code */ -#ifdef __cplusplus -extern "C" { -#endif - -#include - -/* Nonzero if ini_handler callback should accept lineno parameter. */ -#ifndef INI_HANDLER_LINENO -#define INI_HANDLER_LINENO 0 -#endif - -/* Visibility symbols, required for Windows DLLs */ -#ifndef INI_API -#if defined _WIN32 || defined __CYGWIN__ -# ifdef INI_SHARED_LIB -# ifdef INI_SHARED_LIB_BUILDING -# define INI_API __declspec(dllexport) -# else -# define INI_API __declspec(dllimport) -# endif -# else -# define INI_API -# endif -#else -# if defined(__GNUC__) && __GNUC__ >= 4 -# define INI_API __attribute__ ((visibility ("default"))) -# else -# define INI_API -# endif -#endif -#endif - -/* Typedef for prototype of handler function. */ -#if INI_HANDLER_LINENO -typedef int (*ini_handler)(void* user, const char* section, - const char* name, const char* value, - int lineno); -#else -typedef int (*ini_handler)(void* user, const char* section, - const char* name, const char* value); -#endif - -/* Typedef for prototype of fgets-style reader function. */ -typedef char* (*ini_reader)(char* str, int num, void* stream); - -/* Parse given INI-style file. May have [section]s, name=value pairs - (whitespace stripped), and comments starting with ';' (semicolon). Section - is "" if name=value pair parsed before any section heading. name:value - pairs are also supported as a concession to Python's configparser. - - For each name=value pair parsed, call handler function with given user - pointer as well as section, name, and value (data only valid for duration - of handler call). Handler should return nonzero on success, zero on error. - - Returns 0 on success, line number of first error on parse error (doesn't - stop on first error), -1 on file open error, or -2 on memory allocation - error (only when INI_USE_STACK is zero). -*/ -INI_API int ini_parse(const char* filename, ini_handler handler, void* user); - -/* Same as ini_parse(), but takes a FILE* instead of filename. This doesn't - close the file when it's finished -- the caller must do that. */ -INI_API int ini_parse_file(FILE* file, ini_handler handler, void* user); - -/* Same as ini_parse(), but takes an ini_reader function pointer instead of - filename. Used for implementing custom or string-based I/O (see also - ini_parse_string). */ -INI_API int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler, - void* user); - -/* Same as ini_parse(), but takes a zero-terminated string with the INI data -instead of a file. Useful for parsing INI data from a network socket or -already in memory. */ -INI_API int ini_parse_string(const char* string, ini_handler handler, void* user); - -/* Nonzero to allow multi-line value parsing, in the style of Python's - configparser. If allowed, ini_parse() will call the handler with the same - name for each subsequent line parsed. */ -#ifndef INI_ALLOW_MULTILINE -#define INI_ALLOW_MULTILINE 1 -#endif - -/* Nonzero to allow a UTF-8 BOM sequence (0xEF 0xBB 0xBF) at the start of - the file. See https://github.com/benhoyt/inih/issues/21 */ -#ifndef INI_ALLOW_BOM -#define INI_ALLOW_BOM 1 -#endif - -/* Chars that begin a start-of-line comment. Per Python configparser, allow - both ; and # comments at the start of a line by default. */ -#ifndef INI_START_COMMENT_PREFIXES -#define INI_START_COMMENT_PREFIXES ";#" -#endif - -/* Nonzero to allow inline comments (with valid inline comment characters - specified by INI_INLINE_COMMENT_PREFIXES). Set to 0 to turn off and match - Python 3.2+ configparser behaviour. */ -#ifndef INI_ALLOW_INLINE_COMMENTS -#define INI_ALLOW_INLINE_COMMENTS 1 -#endif -#ifndef INI_INLINE_COMMENT_PREFIXES -#define INI_INLINE_COMMENT_PREFIXES ";" -#endif - -/* Nonzero to use stack for line buffer, zero to use heap (malloc/free). */ -#ifndef INI_USE_STACK -#define INI_USE_STACK 1 -#endif - -/* Maximum line length for any line in INI file (stack or heap). Note that - this must be 3 more than the longest line (due to '\r', '\n', and '\0'). */ -#ifndef INI_MAX_LINE -#define INI_MAX_LINE 200 -#endif - -/* Nonzero to allow heap line buffer to grow via realloc(), zero for a - fixed-size buffer of INI_MAX_LINE bytes. Only applies if INI_USE_STACK is - zero. */ -#ifndef INI_ALLOW_REALLOC -#define INI_ALLOW_REALLOC 0 -#endif - -/* Initial size in bytes for heap line buffer. Only applies if INI_USE_STACK - is zero. */ -#ifndef INI_INITIAL_ALLOC -#define INI_INITIAL_ALLOC 200 -#endif - -/* Stop parsing on first error (default is to keep parsing). */ -#ifndef INI_STOP_ON_FIRST_ERROR -#define INI_STOP_ON_FIRST_ERROR 0 -#endif - -/* Nonzero to call the handler at the start of each new section (with - name and value NULL). Default is to only call the handler on - each name=value pair. */ -#ifndef INI_CALL_HANDLER_ON_NEW_SECTION -#define INI_CALL_HANDLER_ON_NEW_SECTION 0 -#endif - -/* Nonzero to allow a name without a value (no '=' or ':' on the line) and - call the handler with value NULL in this case. Default is to treat - no-value lines as an error. */ -#ifndef INI_ALLOW_NO_VALUE -#define INI_ALLOW_NO_VALUE 0 -#endif - -/* Nonzero to use custom ini_malloc, ini_free, and ini_realloc memory - allocation functions (INI_USE_STACK must also be 0). These functions must - have the same signatures as malloc/free/realloc and behave in a similar - way. ini_realloc is only needed if INI_ALLOW_REALLOC is set. */ -#ifndef INI_CUSTOM_ALLOCATOR -#define INI_CUSTOM_ALLOCATOR 0 -#endif - - -#ifdef __cplusplus -} -#endif - -#endif /* INI_H */ diff --git a/3rdparty/plog b/3rdparty/plog deleted file mode 160000 index 914e799..0000000 --- a/3rdparty/plog +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 914e799d2b08d790f5d04d1c46928586b3a41250 diff --git a/3rdparty/qt-unix-signals/CMakeLists.txt b/3rdparty/qt-unix-signals/CMakeLists.txt deleted file mode 100644 index c2afbb7..0000000 --- a/3rdparty/qt-unix-signals/CMakeLists.txt +++ /dev/null @@ -1,14 +0,0 @@ -cmake_minimum_required(VERSION 3.1.0) - -project(QtSignals LANGUAGES CXX) - -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) -# Instruct CMake to run moc automatically when needed. -set(CMAKE_AUTOMOC ON) - -find_package(Qt5 REQUIRED COMPONENTS Core) - -add_library(QtSignals STATIC sigwatch.cpp) -target_include_directories(QtSignals INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(QtSignals Qt5::Core) diff --git a/3rdparty/qt-unix-signals/LICENCE b/3rdparty/qt-unix-signals/LICENCE deleted file mode 100644 index 7cee458..0000000 --- a/3rdparty/qt-unix-signals/LICENCE +++ /dev/null @@ -1,21 +0,0 @@ -Unix signal watcher for Qt. - -Copyright (C) 2014 Simon Knopp - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file diff --git a/3rdparty/qt-unix-signals/sigwatch.cpp b/3rdparty/qt-unix-signals/sigwatch.cpp deleted file mode 100644 index 0374d64..0000000 --- a/3rdparty/qt-unix-signals/sigwatch.cpp +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Unix signal watcher for Qt. - * - * Copyright (C) 2014 Simon Knopp - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -#include -#include -#include -#include -#include -#include -#include "sigwatch.h" - - -/*! - * \brief The UnixSignalWatcherPrivate class implements the back-end signal - * handling for the UnixSignalWatcher. - * - * \see http://qt-project.org/doc/qt-5.0/qtdoc/unix-signals.html - */ -class UnixSignalWatcherPrivate : public QObject -{ - UnixSignalWatcher * const q_ptr; - Q_DECLARE_PUBLIC(UnixSignalWatcher) - -public: - UnixSignalWatcherPrivate(UnixSignalWatcher *q); - ~UnixSignalWatcherPrivate(); - - void watchForSignal(int signal); - static void signalHandler(int signal); - - void _q_onNotify(int sockfd); - -private: - static int sockpair[2]; - QSocketNotifier *notifier; - QList watchedSignals; -}; - - -int UnixSignalWatcherPrivate::sockpair[2]; - -UnixSignalWatcherPrivate::UnixSignalWatcherPrivate(UnixSignalWatcher *q) : - q_ptr(q) -{ - // Create socket pair - if (::socketpair(AF_UNIX, SOCK_STREAM, 0, sockpair)) { - qDebug() << "UnixSignalWatcher: socketpair: " << ::strerror(errno); - return; - } - - // Create a notifier for the read end of the pair - notifier = new QSocketNotifier(sockpair[1], QSocketNotifier::Read); - QObject::connect(notifier, SIGNAL(activated(int)), q, SLOT(_q_onNotify(int))); - notifier->setEnabled(true); -} - -UnixSignalWatcherPrivate::~UnixSignalWatcherPrivate() -{ - delete notifier; -} - -/*! - * Registers a handler for the given Unix \a signal. The handler will write to - * a socket pair, the other end of which is connected to a QSocketNotifier. - * This provides a way to break out of the asynchronous context from which the - * signal handler is called and back into the Qt event loop. - */ -void UnixSignalWatcherPrivate::watchForSignal(int signal) -{ - if (watchedSignals.contains(signal)) { - qDebug() << "Already watching for signal" << signal; - return; - } - - // Register a sigaction which will write to the socket pair - struct sigaction sigact; - sigact.sa_handler = UnixSignalWatcherPrivate::signalHandler; - sigact.sa_flags = 0; - ::sigemptyset(&sigact.sa_mask); - sigact.sa_flags |= SA_RESTART; - if (::sigaction(signal, &sigact, NULL)) { - qDebug() << "UnixSignalWatcher: sigaction: " << ::strerror(errno); - return; - } - - watchedSignals.append(signal); -} - -/*! - * Called when a Unix \a signal is received. Write to the socket to wake up the - * QSocketNotifier. - */ -void UnixSignalWatcherPrivate::signalHandler(int signal) -{ - ssize_t nBytes = ::write(sockpair[0], &signal, sizeof(signal)); - Q_UNUSED(nBytes); -} - -/*! - * Called when the signal handler has written to the socket pair. Emits the Unix - * signal as a Qt signal. - */ -void UnixSignalWatcherPrivate::_q_onNotify(int sockfd) -{ - Q_Q(UnixSignalWatcher); - - int signal; - ssize_t nBytes = ::read(sockfd, &signal, sizeof(signal)); - Q_UNUSED(nBytes); - qDebug() << "Caught signal:" << ::strsignal(signal); - emit q->unixSignal(signal); -} - - -/*! - * Create a new UnixSignalWatcher as a child of the given \a parent. - */ -UnixSignalWatcher::UnixSignalWatcher(QObject *parent) : - QObject(parent), - d_ptr(new UnixSignalWatcherPrivate(this)) -{ -} - -/*! - * Destroy this UnixSignalWatcher. - */ -UnixSignalWatcher::~UnixSignalWatcher() -{ - delete d_ptr; -} - -/*! - * Register a signal handler for the given \a signal. - * - * After calling this method you can \c connect() to the unixSignal() Qt signal - * to be notified when the Unix signal is received. - */ -void UnixSignalWatcher::watchForSignal(int signal) -{ - Q_D(UnixSignalWatcher); - d->watchForSignal(signal); -} - -/*! - * \fn void UnixSignalWatcher::unixSignal(int signal) - * Emitted when the given Unix \a signal is received. - * - * watchForSignal() must be called for each Unix signal that you want to receive - * via the unixSignal() Qt signal. If a watcher is watching multiple signals, - * unixSignal() will be emitted whenever *any* of the watched Unix signals are - * received, and the \a signal argument can be inspected to find out which one - * was actually received. - */ - -#include "moc_sigwatch.cpp" diff --git a/3rdparty/qt-unix-signals/sigwatch.h b/3rdparty/qt-unix-signals/sigwatch.h deleted file mode 100644 index 135fd66..0000000 --- a/3rdparty/qt-unix-signals/sigwatch.h +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Unix signal watcher for Qt. - * - * Copyright (C) 2014 Simon Knopp - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -#ifndef SIGWATCH_H -#define SIGWATCH_H - -#include -#include - -class UnixSignalWatcherPrivate; - - -/*! - * \brief The UnixSignalWatcher class converts Unix signals to Qt signals. - * - * To watch for a given signal, e.g. \c SIGINT, call \c watchForSignal(SIGINT) - * and \c connect() your handler to unixSignal(). - */ - -class UnixSignalWatcher : public QObject -{ - Q_OBJECT -public: - explicit UnixSignalWatcher(QObject *parent = 0); - ~UnixSignalWatcher(); - - void watchForSignal(int signal); - -signals: - void unixSignal(int signal); - -private: - UnixSignalWatcherPrivate * const d_ptr; - Q_DECLARE_PRIVATE(UnixSignalWatcher) - Q_PRIVATE_SLOT(d_func(), void _q_onNotify(int)) -}; - -#endif // SIGWATCH_H diff --git a/3rdparty/qtkeychain b/3rdparty/qtkeychain deleted file mode 160000 index f197cdb..0000000 --- a/3rdparty/qtkeychain +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f197cdb935b0cfd9881fdc6860874cb8379d1238 diff --git a/CMakeLists.txt b/CMakeLists.txt deleted file mode 100644 index 5e0c087..0000000 --- a/CMakeLists.txt +++ /dev/null @@ -1,39 +0,0 @@ -cmake_minimum_required(VERSION 3.10.0) - -set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake;${CMAKE_MODULE_PATH}") -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - -set(CMAKE_AUTOMOC ON) -set(CMAKE_AUTORCC ON) -set(CMAKE_AUTOUIC ON) - -file(STRINGS "VERSION" version) -project(GlobalProtect-openconnect LANGUAGES CXX) - -# Set the CMAKE_INSTALL_PREFIX to /usr if not specified -if (CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) - set(CMAKE_INSTALL_PREFIX "/usr" CACHE PATH "The default install prefix" FORCE) -endif() - -message(STATUS "CMAKE_INSTALL_PREFIX was set to: ${CMAKE_INSTALL_PREFIX}") - -configure_file(version.h.in version.h) - -find_package(Qt5 REQUIRED COMPONENTS - Core - Widgets - Network - WebSockets - WebEngine - WebEngineWidgets - DBus -) - -find_package(Qt5Keychain REQUIRED) - -add_subdirectory(3rdparty/qt-unix-signals) -add_subdirectory(3rdparty/inih) -add_subdirectory(GPService) -add_subdirectory(GPClient) -add_dependencies(gpclient gpservice) diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..f93fe55 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,5052 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "anyhow" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" + +[[package]] +name = "async-trait" +version = "0.1.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "atk" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3d816ce6f0e2909a96830d6911c2aff044370b1ef92d7f267b43bae5addedd" +dependencies = [ + "atk-sys", + "bitflags 1.3.2", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58aeb089fb698e06db8089971c7ee317ab9644bade33383f63631437b03aafb6" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps 6.2.0", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "axum" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d09dbe0e490df5da9d69b36dca48a76635288a82f92eca90024883a56202026d" +dependencies = [ + "async-trait", + "axum-core", + "base64 0.21.5", + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.1.0", + "hyper-util", + "itoa 1.0.10", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "sync_wrapper", + "tokio", + "tokio-tungstenite", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e87c8503f93e6d144ee5690907ba22db7ba79ab001a932ab99034f0fe836b3df" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bstr" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c48f0051a4b4c5e0b6d365cd04af53aeaa209e3cc15ec2cdb69e73cc87fbd0dc" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "bytemuck" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.15.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c76ee391b03d35510d9fa917357c7f1855bd9a6659c95a1b392e33f49b3369bc" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "glib", + "libc", + "thiserror", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8" +dependencies = [ + "glib-sys", + "libc", + "system-deps 6.2.0", +] + +[[package]] +name = "cargo_toml" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "599aa35200ffff8f04c1925aa1acc92fa2e08874379ef42e210a80e527e60838" +dependencies = [ + "serde", + "toml 0.7.8", +] + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3431df59f28accaf4cb4eed4a9acc66bea3f3c3753aa6cdc2f024174ef232af7" +dependencies = [ + "smallvec", +] + +[[package]] +name = "cfg-expr" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6100bc57b6209840798d95cb2775684849d332f7bd788db2a8c8caf7ef82a41a" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets 0.48.5", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "clap" +version = "4.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52bdc885e4cacc7f7c9eedc1ef6da641603180c783c41a15c264944deeaab642" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb7fb5e4e979aec3be7791562fcba452f94ad85e954da024396433e0e25a79e9" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "clap_lex" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" + +[[package]] +name = "cocoa" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation", + "core-graphics", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation", + "core-graphics-types", + "libc", + "objc", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "compile-time" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e55ede5279d4d7c528906853743abeb26353ae1e6c440fcd6d18316c2c2dd903" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "rustc_version", + "semver", + "time", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "core-graphics" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82a9b73a36529d9c47029b9fb3a6f0ea3cc916a261195352ba19e770fc1748b2" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fca89a0e215bab21874660c67903c5f143333cab1da83d041c7ded6053774751" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e3681d554572a651dda4186cd47240627c3d0114d45a95f6ad27f2f22e7548d" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3a430a770ebd84726f584a90ee7f020d28db52c6d02138900f22341f866d39c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags 1.3.2", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa 0.4.8", + "matches", + "phf 0.8.0", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.48", +] + +[[package]] +name = "ctor" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d2b3721e861707777e3195b0158f950ae6dc4a27e4d02ff9f67e3eb3de199e" +dependencies = [ + "quote", + "syn 2.0.48", +] + +[[package]] +name = "darling" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.48", +] + +[[package]] +name = "darling_macro" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "data-encoding" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "directories" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "document-features" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef5282ad69563b5fc40319526ba27e0e7363d552a896f0297d54f767717f9b95" +dependencies = [ + "litrs", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dotenvy_macro" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0235d912a8c749f4e0c9f18ca253b4c28cfefc1d2518096016d6e3230b6424" +dependencies = [ + "dotenvy", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "dtoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" + +[[package]] +name = "dtoa-short" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbaceec3c6e4211c79e7b1800fb9680527106beb2f9c51904a3210c03a448c74" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" + +[[package]] +name = "dyn-clone" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "embed-resource" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bde55e389bea6a966bd467ad1ad7da0ae14546a5bc794d16d1e55e7fca44881" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 0.8.8", + "vswhom", + "winreg 0.51.0", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "fdeflate" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "209098dd6dfc4445aa6111f0e98653ac323eaa4dfd212c9ca3931bf9955c31bd" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "filetime" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "windows-sys 0.52.0", +] + +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "gdk" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6e05c1f572ab0e1f15be94217f0dc29088c248b14f792a5ff0af0d84bcda9e8" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38dd9cc8b099cceecdf41375bb6d481b1b5a7cd5cd603e10a69a9383f8619a" +dependencies = [ + "bitflags 1.3.2", + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140b2f5378256527150350a8346dbdb08fadc13453a7a2d73aecd5fab3c402a7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps 6.2.0", +] + +[[package]] +name = "gdk-sys" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e7a08c1e8f06f4177fb7e51a777b8c1689f743a7bc11ea91d44d2226073a88" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps 6.2.0", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cca49a59ad8cfdf36ef7330fe7bdfbe1d34323220cc16a0de2679ee773aee2c2" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps 6.2.0", +] + +[[package]] +name = "gdkx11-sys" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4b7f8c7a84b407aa9b143877e267e848ff34106578b64d1e0a24bf550716178" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps 6.2.0", + "x11", +] + +[[package]] +name = "generator" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "windows 0.48.0", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "gio" +version = "0.15.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68fdbc90312d462781a395f7a16d96a2b379bb6ef8cd6310a2df272771c4283b" +dependencies = [ + "bitflags 1.3.2", + "futures-channel", + "futures-core", + "futures-io", + "gio-sys", + "glib", + "libc", + "once_cell", + "thiserror", +] + +[[package]] +name = "gio-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32157a475271e2c4a023382e9cab31c4584ee30a97da41d3c4e9fdd605abcf8d" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps 6.2.0", + "winapi", +] + +[[package]] +name = "glib" +version = "0.15.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edb0306fbad0ab5428b0ca674a23893db909a98582969c9b537be4ced78c505d" +dependencies = [ + "bitflags 1.3.2", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "once_cell", + "smallvec", + "thiserror", +] + +[[package]] +name = "glib-macros" +version = "0.15.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10c6ae9f6fa26f4fb2ac16b528d138d971ead56141de489f8111e259b9df3c4a" +dependencies = [ + "anyhow", + "heck 0.4.1", + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "glib-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4" +dependencies = [ + "libc", + "system-deps 6.2.0", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "globset" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata 0.4.3", + "regex-syntax 0.8.2", +] + +[[package]] +name = "gobject-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a" +dependencies = [ + "glib-sys", + "libc", + "system-deps 6.2.0", +] + +[[package]] +name = "gpapi" +version = "2.0.0-beta.1" +dependencies = [ + "anyhow", + "base64 0.21.5", + "chacha20poly1305", + "dotenvy_macro", + "log", + "redact-engine", + "regex", + "reqwest", + "roxmltree", + "serde", + "serde_json", + "specta", + "specta-macros", + "tauri", + "tempfile", + "thiserror", + "tokio", + "url", + "urlencoding", + "users", + "whoami", +] + +[[package]] +name = "gpauth" +version = "2.0.0-beta.1" +dependencies = [ + "anyhow", + "clap", + "compile-time", + "env_logger", + "gpapi", + "log", + "regex", + "serde_json", + "tauri", + "tauri-build", + "tempfile", + "tokio", + "tokio-util", + "webkit2gtk", +] + +[[package]] +name = "gpclient" +version = "2.0.0-beta.1" +dependencies = [ + "anyhow", + "clap", + "compile-time", + "directories", + "env_logger", + "gpapi", + "inquire", + "log", + "openconnect", + "reqwest", + "serde_json", + "sysinfo", + "tempfile", + "tokio", + "whoami", +] + +[[package]] +name = "gpservice" +version = "2.0.0-beta.1" +dependencies = [ + "anyhow", + "axum", + "clap", + "compile-time", + "env_logger", + "futures", + "gpapi", + "log", + "openconnect", + "serde_json", + "tokio", + "tokio-util", +] + +[[package]] +name = "gtk" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e3004a2d5d6d8b5057d2b57b3712c9529b62e82c77f25c1fecde1fd5c23bd0" +dependencies = [ + "atk", + "bitflags 1.3.2", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "once_cell", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5bc2f0587cba247f60246a0ca11fe25fb733eabc3de12d1965fc07efab87c84" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps 6.2.0", +] + +[[package]] +name = "gtk3-macros" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "684c0456c086e8e7e9af73ec5b84e35938df394712054550e81558d21c44ab0d" +dependencies = [ + "anyhow", + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "h2" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.11", + "indexmap 2.1.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d308f63daf4181410c242d34c11f928dcb3aa105852019e043c9d1f4e4368a" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 1.0.0", + "indexmap 2.1.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "http" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +dependencies = [ + "bytes", + "fnv", + "itoa 1.0.10", +] + +[[package]] +name = "http" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea" +dependencies = [ + "bytes", + "fnv", + "itoa 1.0.10", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.11", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http 1.0.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840" +dependencies = [ + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "pin-project-lite", +] + +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.14.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.22", + "http 0.2.11", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa 1.0.10", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5aa53871fc917b1a9ed87b683a5d86db645e23acb32c2e0785a353e522fb75" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.0", + "http 1.0.0", + "http-body 1.0.0", + "httparse", + "httpdate", + "itoa 1.0.10", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper 0.14.28", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "hyper-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdea9aac0dbe5a9240d68cfd9501e2db94222c6dc06843e06640b9e07f0fdc67" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "hyper 1.1.0", + "pin-project-lite", + "socket2", + "tokio", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3804960be0bb5e4edb1e1ad67afd321a9ecfd875c3e65c099468fd2717d7cae" +dependencies = [ + "byteorder", + "png", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "ignore" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata 0.4.3", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "image" +version = "0.24.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "num-rational", + "num-traits", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown 0.14.3", + "serde", +] + +[[package]] +name = "indoc" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" + +[[package]] +name = "infer" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f551f8c3a39f68f986517db0d1759de85881894fdc7db798bd2a9df9cb04b7fc" +dependencies = [ + "cfb", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "inquire" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33e7c1ddeb15c9abcbfef6029d8e29f69b52b6d6c891031b88ed91b5065803b" +dependencies = [ + "bitflags 1.3.2", + "crossterm", + "dyn-clone", + "lazy_static", + "newline-converter", + "thiserror", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "is-terminal" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "is_executable" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa9acdc6d67b75e626ad644734e8bc6df893d9cd2a834129065d3dd6158ea9c8" +dependencies = [ + "winapi", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "javascriptcore-rs" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf053e7843f2812ff03ef5afe34bb9c06ffee120385caad4f6b9967fcd37d41c" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "905fbb87419c5cde6e3269537e4ea7d46431f3008c5d057e915ef3f115e7793c" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps 5.0.0", +] + +[[package]] +name = "jni" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "js-sys" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ff1e1486799e3f64129f8ccad108b38290df9cd7015cd31bed17239f0789d6" +dependencies = [ + "serde", + "serde_json", + "thiserror", + "treediff", +] + +[[package]] +name = "kuchikiki" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8" +dependencies = [ + "cssparser", + "html5ever", + "indexmap 1.9.3", + "matches", + "selectors", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" + +[[package]] +name = "libredox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +dependencies = [ + "bitflags 2.4.1", + "libc", + "redox_syscall", +] + +[[package]] +name = "line-wrap" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9" +dependencies = [ + "safemem", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" + +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "loom" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "serde", + "serde_json", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "markup5ever" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" +dependencies = [ + "log", + "phf 0.10.1", + "phf_codegen 0.10.0", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.48.0", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndk" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2032c77e030ddee34a6787a64166008da93f6a352b629261d0fee232b8742dd4" +dependencies = [ + "bitflags 1.3.2", + "jni-sys", + "ndk-sys", + "num_enum", + "thiserror", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e5a6ae77c8ee183dcbbba6150e2e6b9f3f4196a7666c02a715a95692ec1fa97" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" + +[[package]] +name = "newline-converter" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f71d09d5c87634207f894c6b31b6a2b2c64ea3bdcf71bd5599fdbbe1600c00f" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", + "objc_exception", +] + +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "openconnect" +version = "2.0.0-beta.1" +dependencies = [ + "cc", + "is_executable", + "log", +] + +[[package]] +name = "openssl" +version = "0.10.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cde4d2d9200ad5909f8dac647e29482e07c3a35de8a13fce7c9c7747ad9f671" +dependencies = [ + "bitflags 2.4.1", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-src" +version = "300.2.1+3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fe476c29791a5ca0d1273c697e96085bbabbbea2ef7afd5617e78a4b40332d3" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1665caf8ab2dc9aef43d1c0023bd904633a6a05cb30b0ad59bec2ae986e57a7" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "pango" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e4045548659aee5313bde6c582b0d83a627b7904dd20dc2d9ef0895d414e4f" +dependencies = [ + "bitflags 1.3.2", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2a00081cde4661982ed91d80ef437c20eacaf6aa1a5962c0279ae194662c3aa" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps 6.2.0", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.5", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_macros 0.8.0", + "phf_shared 0.8.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_shared 0.10.0", +] + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros 0.11.2", + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared 0.11.2", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d3587f8a9e599cc7ec2c00e331f71c4e69a5f9a4b8a6efd5b07466b9736f9a" + +[[package]] +name = "plist" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5699cc8a63d1aa2b1ee8e12b9ad70ac790d65788cd36101fa37f87ea46c4cef" +dependencies = [ + "base64 0.21.5", + "indexmap 2.1.0", + "line-wrap", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd75bf2d8dd3702b9707cdbc56a5b9ef42cec752eb8b3bafc01234558442aa64" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.11", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" + +[[package]] +name = "rayon" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redact-engine" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94d4ba41ad2b45cb0d6df9719881b43fca399dfa084debba338a08d5d599b52c" +dependencies = [ + "anyhow", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_regex", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_users" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +dependencies = [ + "getrandom 0.2.11", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.3", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "reqwest" +version = "0.11.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" +dependencies = [ + "base64 0.21.5", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.3.22", + "http 0.2.11", + "http-body 0.4.6", + "hyper 0.14.28", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "system-configuration", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "winreg 0.50.0", +] + +[[package]] +name = "roxmltree" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862340e351ce1b271a378ec53f304a5558f7db87f3769dc655a8f6ecbb68b302" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" +dependencies = [ + "bitflags 2.4.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "ryu" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" + +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" +dependencies = [ + "bitflags 1.3.2", + "cssparser", + "derive_more", + "fxhash", + "log", + "matches", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc", + "smallvec", + "thin-slice", +] + +[[package]] +name = "semver" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" +dependencies = [ + "serde", +] + +[[package]] +name = "serde" +version = "1.0.195" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.195" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "serde_json" +version = "1.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" +dependencies = [ + "itoa 1.0.10", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd154a240de39fdebcf5775d2675c204d7c13cf39a4c697be6493c8e734337c" +dependencies = [ + "itoa 1.0.10", + "serde", +] + +[[package]] +name = "serde_regex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8136f1a4ea815d7eac4101cfd0b16dc0cb5e1fe1b8609dfd728058656b7badf" +dependencies = [ + "regex", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "serde_spanned" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa 1.0.10", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd236ccc1b7a29e7e2739f27c0b2dd199804abc4290e32f59f3b68d6405c23" +dependencies = [ + "base64 0.21.5", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.1.0", + "serde", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93634eb5f75a2323b16de4748022ac4297f9e76b6dced2be287a099f41b5e788" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9823f2d3b6a81d98228151fdeaf848206a7855a7a042bbf9bf870449a66cafb" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74064874e9f6a15f04c1f3cb627902d0e6b410abbf36668afa873c61889f1763" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "servo_arc" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" + +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "soup2" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b4d76501d8ba387cf0fefbe055c3e0a59891d09f0f995ae4e4b16f6b60f3c0" +dependencies = [ + "bitflags 1.3.2", + "gio", + "glib", + "libc", + "once_cell", + "soup2-sys", +] + +[[package]] +name = "soup2-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009ef427103fcb17f802871647a7fa6c60cbb654b4c4e4c0ac60a31c5f6dc9cf" +dependencies = [ + "bitflags 1.3.2", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps 5.0.0", +] + +[[package]] +name = "specta" +version = "2.0.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899e1de0122914c5573ca81ced566c5aaeebf2c363595ee3dfdaac58f33ea351" +dependencies = [ + "document-features", + "indoc", + "once_cell", + "paste", + "serde", + "serde_json", + "specta-macros", + "thiserror", +] + +[[package]] +name = "specta-macros" +version = "2.0.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0de71d47f9076754480433ca4fdcbde756312b56e7991ac17f2cb2435897ed" +dependencies = [ + "Inflector", + "itertools", + "proc-macro2", + "quote", + "syn 1.0.109", + "termcolor", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "state" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbe866e1e51e8260c9eed836a042a5e7f6726bb2b411dffeaa712e19c388f23b" +dependencies = [ + "loom", +] + +[[package]] +name = "string_cache" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot", + "phf_shared 0.10.0", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sysinfo" +version = "0.29.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd727fc423c2060f6c92d9534cef765c65a6ed3f428a03d7def74a8c4348e666" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "rayon", + "winapi", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "system-deps" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18db855554db7bd0e73e06cf7ba3df39f97812cb11d3f75e71c39bf45171797e" +dependencies = [ + "cfg-expr 0.9.1", + "heck 0.3.3", + "pkg-config", + "toml 0.5.11", + "version-compare 0.0.11", +] + +[[package]] +name = "system-deps" +version = "6.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2d580ff6a20c55dfb86be5f9c238f67835d0e81cbdea8bf5680e0897320331" +dependencies = [ + "cfg-expr 0.15.6", + "heck 0.4.1", + "pkg-config", + "toml 0.8.8", + "version-compare 0.1.1", +] + +[[package]] +name = "tao" +version = "0.16.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75f5aefd6be4cd3ad3f047442242fd9f57cbfb3e565379f66b5e14749364fa4f" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "cc", + "cocoa", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dispatch", + "gdk", + "gdk-pixbuf", + "gdk-sys", + "gdkwayland-sys", + "gdkx11-sys", + "gio", + "glib", + "glib-sys", + "gtk", + "image", + "instant", + "jni", + "lazy_static", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc", + "once_cell", + "parking_lot", + "png", + "raw-window-handle", + "scopeguard", + "serde", + "tao-macros", + "unicode-segmentation", + "uuid", + "windows 0.39.0", + "windows-implement", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec114582505d158b669b136e6851f85840c109819d77c42bb7c0709f727d18c2" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "tar" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "target-lexicon" +version = "0.12.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69758bda2e78f098e4ccb393021a0963bb3442eac05f135c30f61b7370bbafae" + +[[package]] +name = "tauri" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd27c04b9543776a972c86ccf70660b517ecabbeced9fb58d8b961a13ad129af" +dependencies = [ + "anyhow", + "bytes", + "cocoa", + "dirs-next", + "embed_plist", + "encoding_rs", + "flate2", + "futures-util", + "glib", + "glob", + "gtk", + "heck 0.4.1", + "http 0.2.11", + "ignore", + "objc", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "raw-window-handle", + "reqwest", + "semver", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "state", + "tar", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "tempfile", + "thiserror", + "tokio", + "url", + "uuid", + "webkit2gtk", + "webview2-com", + "windows 0.39.0", +] + +[[package]] +name = "tauri-build" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9914a4715e0b75d9f387a285c7e26b5bbfeb1249ad9f842675a82481565c532" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs-next", + "heck 0.4.1", + "json-patch", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1554c5857f65dbc377cefb6b97c8ac77b1cb2a90d30d3448114d5d6b48a77fc" +dependencies = [ + "base64 0.21.5", + "brotli", + "ico", + "json-patch", + "plist", + "png", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "tauri-utils", + "thiserror", + "time", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "277abf361a3a6993ec16bcbb179de0d6518009b851090a01adfea12ac89fa875" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 1.0.109", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-runtime" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf2d0652aa2891ff3e9caa2401405257ea29ab8372cce01f186a5825f1bd0e76" +dependencies = [ + "gtk", + "http 0.2.11", + "http-range", + "rand 0.8.5", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror", + "url", + "uuid", + "webview2-com", + "windows 0.39.0", +] + +[[package]] +name = "tauri-runtime-wry" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cae61fbc731f690a4899681c9052dde6d05b159b44563ace8186fc1bfb7d158" +dependencies = [ + "cocoa", + "gtk", + "percent-encoding", + "rand 0.8.5", + "raw-window-handle", + "tauri-runtime", + "tauri-utils", + "uuid", + "webkit2gtk", + "webview2-com", + "windows 0.39.0", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ece74810b1d3d44f29f732a7ae09a63183d63949bbdd59c61f8ed2a1b70150db" +dependencies = [ + "brotli", + "ctor", + "dunce", + "glob", + "heck 0.4.1", + "html5ever", + "infer", + "json-patch", + "kuchikiki", + "log", + "memchr", + "phf 0.11.2", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "serde_with", + "thiserror", + "url", + "walkdir", + "windows-version", +] + +[[package]] +name = "tauri-winres" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5993dc129e544393574288923d1ec447c857f3f644187f4fbf7d9a875fbfc4fb" +dependencies = [ + "embed-resource", + "toml 0.7.8", +] + +[[package]] +name = "tempfile" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "termcolor" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thin-slice" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" + +[[package]] +name = "thiserror" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" +dependencies = [ + "deranged", + "itoa 1.0.10", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.35.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.19.15", +] + +[[package]] +name = "toml" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.21.0", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.1.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" +dependencies = [ + "indexmap 2.1.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "treediff" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52984d277bdf2a751072b5df30ec0377febdb02f7696d64c2d7d54630bac4303" +dependencies = [ + "serde_json", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.0.0", + "httparse", + "log", + "rand 0.8.5", + "sha1", + "thiserror", + "url", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-bidi" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "users" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" +dependencies = [ + "libc", + "log", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "uuid" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" +dependencies = [ + "getrandom 0.2.11", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version-compare" +version = "0.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c18c859eead79d8b95d09e4678566e8d70105c4e7b251f707a03df32442661b" + +[[package]] +name = "version-compare" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b17ae1f6c8a2b28506cd96d412eebf83b4a0ff2cbefeeb952f2f9dfa44ba18" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.48", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" + +[[package]] +name = "wasm-streams" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4609d447824375f43e1ffbc051b50ad8f4b3ae8219680c94452ea05eb240ac7" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webkit2gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8f859735e4a452aeb28c6c56a852967a8a76c8eb1cc32dbf931ad28a13d6370" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup2", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d76ca6ecc47aeba01ec61e480139dda143796abcae6f83bcddf50d6b5b1dcf3" +dependencies = [ + "atk-sys", + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pango-sys", + "pkg-config", + "soup2-sys", + "system-deps 6.2.0", +] + +[[package]] +name = "webview2-com" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a769c9f1a64a8734bde70caafac2b96cada12cd4aefa49196b3a386b8b4178" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows 0.39.0", + "windows-implement", +] + +[[package]] +name = "webview2-com-macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaebe196c01691db62e9e4ca52c5ef1e4fd837dcae27dae3ada599b5a8fd05ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "webview2-com-sys" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac48ef20ddf657755fdcda8dfed2a7b4fc7e4581acce6fe9b88c3d64f29dee7" +dependencies = [ + "regex", + "serde", + "serde_json", + "thiserror", + "windows 0.39.0", + "windows-bindgen", + "windows-metadata", +] + +[[package]] +name = "whoami" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1c4bd0a50ac6020f65184721f758dba47bb9fbc2133df715ec74a237b26794a" +dependencies = [ + "windows-implement", + "windows_aarch64_msvc 0.39.0", + "windows_i686_gnu 0.39.0", + "windows_i686_msvc 0.39.0", + "windows_x86_64_gnu 0.39.0", + "windows_x86_64_msvc 0.39.0", +] + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-bindgen" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68003dbd0e38abc0fb85b939240f4bce37c43a5981d3df37ccbaaa981b47cb41" +dependencies = [ + "windows-metadata", + "windows-tokens", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-implement" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba01f98f509cb5dc05f4e5fc95e535f78260f15fea8fe1a8abdd08f774f1cee7" +dependencies = [ + "syn 1.0.109", + "windows-tokens", +] + +[[package]] +name = "windows-metadata" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ee5e275231f07c6e240d14f34e1b635bf1faa1c76c57cfd59a5cdb9848e4278" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + +[[package]] +name = "windows-tokens" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f838de2fe15fe6bac988e74b798f26499a8b21a9d97edec321e79b28d1d7f597" + +[[package]] +name = "windows-version" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75aa004c988e080ad34aff5739c39d0312f4684699d6d71fc8a198d057b8b9b4" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7711666096bd4096ffa835238905bb33fb87267910e154b18b44eaabb340f2" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "763fc57100a5f7042e3057e7e8d9bdd7860d330070251a73d003563a3bb49e1b" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bc7cbfe58828921e10a9f446fcaaf649204dcfe6c1ddd712c5eebae6bda1106" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6868c165637d653ae1e8dc4d82c25d4f97dd6605eaa8d784b5c6e0ab2a252b65" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e4d40883ae9cae962787ca76ba76390ffa29214667a111db9e0a1ad8377e809" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "winnow" +version = "0.5.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7520bbdec7211caa7c4e682eb1fbe07abe20cee6756b6e00f537c82c11816aa" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "winreg" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "937f3df7948156640f46aacef17a70db0de5917bda9c92b0f751f3a955b588fc" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wry" +version = "0.24.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ad85d0e067359e409fcb88903c3eac817c392e5d638258abfb3da5ad8ba6fc4" +dependencies = [ + "base64 0.13.1", + "block", + "cocoa", + "core-graphics", + "crossbeam-channel", + "dunce", + "gdk", + "gio", + "glib", + "gtk", + "html5ever", + "http 0.2.11", + "kuchikiki", + "libc", + "log", + "objc", + "objc_id", + "once_cell", + "serde", + "serde_json", + "sha2", + "soup2", + "tao", + "thiserror", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows 0.39.0", + "windows-implement", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "xattr" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914566e6413e7fa959cc394fb30e563ba80f3541fbd40816d4c05a0fc3f2a0f1" +dependencies = [ + "libc", + "linux-raw-sys", + "rustix", +] + +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..13ca1e3 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,52 @@ +[workspace] +resolver = "2" + +members = ["crates/*", "apps/gpclient", "apps/gpservice", "apps/gpauth"] + +[workspace.package] +version = "2.0.0-beta.1" +authors = ["Kevin Yue "] +homepage = "https://github.com/yuezk/GlobalProtect-openconnect" +edition = "2021" +license = "GPL-3.0" + +[workspace.dependencies] +anyhow = "1.0" +base64 = "0.21" +clap = { version = "4.4.2", features = ["derive"] } +ctrlc = "3.4" +directories = "5.0" +env_logger = "0.10" +is_executable = "1.0" +log = "0.4" +regex = "1" +reqwest = { version = "0.11", features = ["native-tls-vendored", "json"] } +roxmltree = "0.18" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +sysinfo = "0.29" +tempfile = "3.8" +tokio = { version = "1", features = ["full"] } +tokio-util = "0.7" +url = "2.4" +urlencoding = "2.1.3" +axum = "0.7" +futures = "0.3" +futures-util = "0.3" +tokio-tungstenite = "0.20.1" +specta = "=2.0.0-rc.1" +specta-macros = "=2.0.0-rc.1" +users = "0.11" +whoami = "1" +tauri = { version = "1.5" } +thiserror = "1" +redact-engine = "0.1" +dotenvy_macro = "0.15" +compile-time = "0.2" + +[profile.release] +opt-level = 'z' # Optimize for size +lto = true # Enable link-time optimization +codegen-units = 1 # Reduce number of codegen units to increase optimizations +panic = 'abort' # Abort on panic +strip = true # Strip symbols from binary* diff --git a/GPClient/CMakeLists.txt b/GPClient/CMakeLists.txt deleted file mode 100644 index 4a172fa..0000000 --- a/GPClient/CMakeLists.txt +++ /dev/null @@ -1,110 +0,0 @@ -include("${CMAKE_SOURCE_DIR}/cmake/Add3rdParty.cmake") - -project(GPClient) - -set(gpclient_GENERATED_SOURCES) - -configure_file(com.yuezk.qt.gpclient.desktop.in com.yuezk.qt.gpclient.desktop) -configure_file(com.yuezk.qt.gpclient.metainfo.xml.in com.yuezk.qt.gpclient.metainfo.xml) - -qt5_add_dbus_interface( - gpclient_GENERATED_SOURCES - ${CMAKE_BINARY_DIR}/com.yuezk.qt.GPService.xml - gpserviceinterface -) - -add_executable(gpclient - cdpcommand.cpp - cdpcommandmanager.cpp - enhancedwebview.cpp - enhancedwebpage.cpp - gatewayauthenticator.cpp - gatewayauthenticatorparams.cpp - gpgateway.cpp - gphelper.cpp - loginparams.cpp - main.cpp - standardloginwindow.cpp - portalauthenticator.cpp - portalconfigresponse.cpp - preloginresponse.cpp - samlloginwindow.cpp - gpclient.cpp - settingsdialog.cpp - gpclient.ui - standardloginwindow.ui - settingsdialog.ui - challengedialog.h - challengedialog.cpp - challengedialog.ui - vpn_dbus.cpp - vpn_json.cpp - resources.qrc - ${gpclient_GENERATED_SOURCES} -) - -add_3rdparty( - SingleApplication - GIT_REPOSITORY https://github.com/itay-grudev/SingleApplication.git - GIT_TAG v3.3.0 - CMAKE_ARGS - -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} - -DCMAKE_CXX_FLAGS_RELEASE=${CMAKE_CXX_FLAGS_RELEASE} - -DCMAKE_FIND_ROOT_PATH=${CMAKE_FIND_ROOT_PATH} - -DCMAKE_PREFIX_PATH=$ENV{CMAKE_PREFIX_PATH} - -DQAPPLICATION_CLASS=QApplication -) - -add_3rdparty( - plog - GIT_REPOSITORY https://github.com/SergiusTheBest/plog.git - GIT_TAG master - CMAKE_ARGS - -DPLOG_BUILD_SAMPLES=OFF - -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} - -DCMAKE_CXX_FLAGS_RELEASE=${CMAKE_CXX_FLAGS_RELEASE} -) - -ExternalProject_Get_Property(SingleApplication-${PROJECT_NAME} SOURCE_DIR BINARY_DIR) -set(SingleApplication_INCLUDE_DIR ${SOURCE_DIR}) -set(SingleApplication_LIBRARY ${BINARY_DIR}/libSingleApplication.a) - -ExternalProject_Get_Property(plog-${PROJECT_NAME} SOURCE_DIR) -set(plog_INCLUDE_DIR "${SOURCE_DIR}/include") - -add_dependencies(gpclient - SingleApplication-${PROJECT_NAME} - plog-${PROJECT_NAME} -) - -target_include_directories(gpclient PRIVATE - ${CMAKE_BINARY_DIR} - ${CMAKE_CURRENT_SOURCE_DIR} - ${CMAKE_CURRENT_BINARY_DIR} - ${SingleApplication_INCLUDE_DIR} - ${plog_INCLUDE_DIR} - ${QTKEYCHAIN_INCLUDE_DIRS}/qt5keychain -) - -target_link_libraries(gpclient - ${SingleApplication_LIBRARY} - Qt5::Widgets - Qt5::Network - Qt5::WebSockets - Qt5::WebEngine - Qt5::WebEngineWidgets - Qt5::DBus - QtSignals - ${QTKEYCHAIN_LIBRARIES} -) - -if (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 8.0 AND CMAKE_BUILD_TYPE STREQUAL Release) - target_compile_options(gpclient PUBLIC "-ffile-prefix-map=${CMAKE_SOURCE_DIR}=.") -endif() - -target_compile_definitions(gpclient PUBLIC QAPPLICATION_CLASS=QApplication) - -install(TARGETS gpclient DESTINATION bin) -install(FILES "${CMAKE_CURRENT_BINARY_DIR}/com.yuezk.qt.gpclient.metainfo.xml" DESTINATION share/metainfo) -install(FILES "${CMAKE_CURRENT_BINARY_DIR}/com.yuezk.qt.gpclient.desktop" DESTINATION share/applications) -install(FILES "com.yuezk.qt.gpclient.svg" DESTINATION share/icons/hicolor/scalable/apps) diff --git a/GPClient/cdpcommand.cpp b/GPClient/cdpcommand.cpp deleted file mode 100644 index 0722aac..0000000 --- a/GPClient/cdpcommand.cpp +++ /dev/null @@ -1,30 +0,0 @@ -#include -#include -#include - -#include "cdpcommand.h" - -CDPCommand::CDPCommand(QObject *parent) : QObject(parent) -{ -} - -CDPCommand::CDPCommand(int id, QString cmd, QVariantMap& params) : - QObject(nullptr), - id(id), - cmd(cmd), - params(¶ms) -{ -} - -QByteArray CDPCommand::toJson() -{ - QVariantMap payloadMap; - payloadMap["id"] = id; - payloadMap["method"] = cmd; - payloadMap["params"] = *params; - - QJsonObject payloadJsonObject = QJsonObject::fromVariantMap(payloadMap); - QJsonDocument payloadJson(payloadJsonObject); - - return payloadJson.toJson(); -} diff --git a/GPClient/cdpcommand.h b/GPClient/cdpcommand.h deleted file mode 100644 index 8f37b7b..0000000 --- a/GPClient/cdpcommand.h +++ /dev/null @@ -1,24 +0,0 @@ -#ifndef CDPCOMMAND_H -#define CDPCOMMAND_H - -#include - -class CDPCommand : public QObject -{ - Q_OBJECT -public: - explicit CDPCommand(QObject *parent = nullptr); - CDPCommand(int id, QString cmd, QVariantMap& params); - - QByteArray toJson(); - -signals: - void finished(); - -private: - int id; - QString cmd; - QVariantMap *params; -}; - -#endif // CDPCOMMAND_H diff --git a/GPClient/cdpcommandmanager.cpp b/GPClient/cdpcommandmanager.cpp deleted file mode 100644 index 441e192..0000000 --- a/GPClient/cdpcommandmanager.cpp +++ /dev/null @@ -1,87 +0,0 @@ -#include -#include - -#include "cdpcommandmanager.h" - -CDPCommandManager::CDPCommandManager(QObject *parent) - : QObject(parent) - , networkManager(new QNetworkAccessManager) - , socket(new QWebSocket) -{ - // WebSocket setup - QObject::connect(socket, &QWebSocket::connected, this, &CDPCommandManager::ready); - QObject::connect(socket, &QWebSocket::textMessageReceived, this, &CDPCommandManager::onTextMessageReceived); - QObject::connect(socket, &QWebSocket::disconnected, this, &CDPCommandManager::onSocketDisconnected); - QObject::connect(socket, QOverload::of(&QWebSocket::error), this, &CDPCommandManager::onSocketError); -} - -CDPCommandManager::~CDPCommandManager() -{ - delete networkManager; - delete socket; -} - -void CDPCommandManager::initialize(QString endpoint) -{ - QNetworkReply *reply = networkManager->get(QNetworkRequest(endpoint)); - - QObject::connect( - reply, &QNetworkReply::finished, - [reply, this]() { - if (reply->error()) { - LOGE << "CDP request error"; - return; - } - - QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); - QJsonArray pages = doc.array(); - QJsonObject page = pages.first().toObject(); - QString wsUrl = page.value("webSocketDebuggerUrl").toString(); - - socket->open(wsUrl); - } - ); -} - -CDPCommand *CDPCommandManager::sendCommand(QString cmd) -{ - QVariantMap emptyParams; - return sendCommend(cmd, emptyParams); -} - -CDPCommand *CDPCommandManager::sendCommend(QString cmd, QVariantMap ¶ms) -{ - int id = ++commandId; - CDPCommand *command = new CDPCommand(id, cmd, params); - socket->sendTextMessage(command->toJson()); - commandPool.insert(id, command); - - return command; -} - -void CDPCommandManager::onTextMessageReceived(QString message) -{ - QJsonDocument responseDoc = QJsonDocument::fromJson(message.toUtf8()); - QJsonObject response = responseDoc.object(); - - // Response for method - if (response.contains("id")) { - int id = response.value("id").toInt(); - if (commandPool.contains(id)) { - CDPCommand *command = commandPool.take(id); - command->finished(); - } - } else { // Response for event - emit eventReceived(response.value("method").toString(), response.value("params").toObject()); - } -} - -void CDPCommandManager::onSocketDisconnected() -{ - LOGI << "WebSocket disconnected"; -} - -void CDPCommandManager::onSocketError(QAbstractSocket::SocketError error) -{ - LOGE << "WebSocket error" << error; -} diff --git a/GPClient/cdpcommandmanager.h b/GPClient/cdpcommandmanager.h deleted file mode 100644 index ef1238f..0000000 --- a/GPClient/cdpcommandmanager.h +++ /dev/null @@ -1,40 +0,0 @@ -#ifndef CDPCOMMANDMANAGER_H -#define CDPCOMMANDMANAGER_H - -#include -#include -#include -#include - -#include "cdpcommand.h" - -class CDPCommandManager : public QObject -{ - Q_OBJECT -public: - explicit CDPCommandManager(QObject *parent = nullptr); - ~CDPCommandManager(); - - void initialize(QString endpoint); - - CDPCommand *sendCommand(QString cmd); - CDPCommand *sendCommend(QString cmd, QVariantMap& params); - -signals: - void ready(); - void eventReceived(QString eventName, QJsonObject params); - -private: - QNetworkAccessManager *networkManager; - QWebSocket *socket; - - int commandId = 0; - QHash commandPool; - -private slots: - void onTextMessageReceived(QString message); - void onSocketDisconnected(); - void onSocketError(QAbstractSocket::SocketError error); -}; - -#endif // CDPCOMMANDMANAGER_H diff --git a/GPClient/challengedialog.cpp b/GPClient/challengedialog.cpp deleted file mode 100644 index bf842e7..0000000 --- a/GPClient/challengedialog.cpp +++ /dev/null @@ -1,38 +0,0 @@ -#include -#include - -#include "challengedialog.h" -#include "ui_challengedialog.h" - -ChallengeDialog::ChallengeDialog(QWidget *parent) : - QDialog(parent), - ui(new Ui::ChallengeDialog) -{ - ui->setupUi(this); - ui->buttonBox->button(QDialogButtonBox::Ok)->setDisabled(true); -} - -ChallengeDialog::~ChallengeDialog() -{ - delete ui; -} - -void ChallengeDialog::setMessage(const QString &message) -{ - ui->challengeMessage->setText(message); -} - -const QString ChallengeDialog::getChallenge() -{ - return ui->challengeInput->text(); -} - -void ChallengeDialog::on_challengeInput_textChanged(const QString &value) -{ - QPushButton *okBtn = ui->buttonBox->button(QDialogButtonBox::Ok); - if (value.isEmpty()) { - okBtn->setDisabled(true); - } else { - okBtn->setEnabled(true); - } -} diff --git a/GPClient/challengedialog.h b/GPClient/challengedialog.h deleted file mode 100644 index bd6b95d..0000000 --- a/GPClient/challengedialog.h +++ /dev/null @@ -1,28 +0,0 @@ -#ifndef CHALLENGEDIALOG_H -#define CHALLENGEDIALOG_H - -#include - -namespace Ui { -class ChallengeDialog; -} - -class ChallengeDialog : public QDialog -{ - Q_OBJECT - -public: - explicit ChallengeDialog(QWidget *parent = nullptr); - ~ChallengeDialog(); - - void setMessage(const QString &message); - const QString getChallenge(); - -private slots: - void on_challengeInput_textChanged(const QString &arg1); - -private: - Ui::ChallengeDialog *ui; -}; - -#endif // CHALLENGEDIALOG_H diff --git a/GPClient/challengedialog.ui b/GPClient/challengedialog.ui deleted file mode 100644 index 6aafa22..0000000 --- a/GPClient/challengedialog.ui +++ /dev/null @@ -1,111 +0,0 @@ - - - ChallengeDialog - - - - 0 - 0 - 405 - 200 - - - - GlobalProtect Challenge - - - true - - - - - - - - - 14 - 50 - false - - - - Sign In - - - - - - - Duo two-factor login for [redacted] Enter a passcode or select one of the following options: 1. Duo Push to XXX-XXX-[redacted] 2. SMS passcodes to XXX-XXX-[redacted] Passcode or option (1-2): - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - true - - - - - - - - - QLineEdit::Password - - - - - - - Qt::LeftToRight - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - false - - - - - - - - - buttonBox - accepted() - ChallengeDialog - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttonBox - rejected() - ChallengeDialog - reject() - - - 316 - 260 - - - 286 - 274 - - - - - diff --git a/GPClient/com.yuezk.qt.gpclient.desktop.in b/GPClient/com.yuezk.qt.gpclient.desktop.in deleted file mode 100644 index f15cc09..0000000 --- a/GPClient/com.yuezk.qt.gpclient.desktop.in +++ /dev/null @@ -1,12 +0,0 @@ -[Desktop Entry] - -Type=Application -Version=1.0 -Name=GlobalProtect VPN -Comment=A GlobalProtect VPN client (GUI) for Linux based on OpenConnect and built with Qt5, supports SAML auth mode. -GenericName=GlobalProtect VPN client, supports SAML auth mode -Categories=Network;Dialup; -Exec=env QT_AUTO_SCREEN_SCALE_FACTOR=1 @CMAKE_INSTALL_PREFIX@/bin/gpclient -Icon=com.yuezk.qt.gpclient -Keywords=GlobalProtect;Openconnect;SAML;connection;VPN; -StartupWMClass=gpclient diff --git a/GPClient/com.yuezk.qt.gpclient.metainfo.xml.in b/GPClient/com.yuezk.qt.gpclient.metainfo.xml.in deleted file mode 100644 index 3af48ab..0000000 --- a/GPClient/com.yuezk.qt.gpclient.metainfo.xml.in +++ /dev/null @@ -1,43 +0,0 @@ - - - com.yuezk.qt.gpclient - - globalprotect-openconnect - A GlobalProtect VPN client powered by OpenConnect - - CC0-1.0 - AGPL-3.0-or-later - - -

A GlobalProtect VPN client (GUI) for Linux based on OpenConnect and built with Qt5, supports the SAML auth mode.

-
- - - Network - - - k3vinyue_AT_gmail.com - Kevin Yue - - https://github.com/yuezk/GlobalProtect-openconnect - https://github.com/yuezk/GlobalProtect-openconnect/issues - https://github.com/yuezk/GlobalProtect-openconnect/issues - - - globalprotect - openconnect - vpn - saml - - - com.yuezk.qt.gpclient.desktop - - - https://user-images.githubusercontent.com/3297602/133869036-5c02b0d9-c2d9-4f87-8c81-e44f68cfd6ac.png - - - - @CMAKE_INSTALL_PREFIX@/bin/gpclient - com.yuezk.qt.GPService - -
\ No newline at end of file diff --git a/GPClient/com.yuezk.qt.gpclient.svg b/GPClient/com.yuezk.qt.gpclient.svg deleted file mode 100644 index bccc611..0000000 --- a/GPClient/com.yuezk.qt.gpclient.svg +++ /dev/null @@ -1,99 +0,0 @@ - - - -image/svg+xml - - - - - - - - \ No newline at end of file diff --git a/GPClient/connected.png b/GPClient/connected.png deleted file mode 100644 index e6e4e50422d426e241cfad923f0403660bbf2944..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18199 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 diff --git a/GPClient/enhancedwebpage.cpp b/GPClient/enhancedwebpage.cpp deleted file mode 100644 index b50db77..0000000 --- a/GPClient/enhancedwebpage.cpp +++ /dev/null @@ -1,8 +0,0 @@ -#include "enhancedwebpage.h" -#include -#include - -bool EnhancedWebPage::certificateError(const QWebEngineCertificateError &certificateError) { - LOGI << "An error occurred during certificate verification for " << certificateError.url().toString() << "; " << certificateError.errorDescription(); - return certificateError.isOverridable(); -}; diff --git a/GPClient/enhancedwebpage.h b/GPClient/enhancedwebpage.h deleted file mode 100644 index f043edc..0000000 --- a/GPClient/enhancedwebpage.h +++ /dev/null @@ -1,12 +0,0 @@ -#ifndef ENHANCEDWEBPAGE_H -#define ENHANCEDWEBPAGE_H - -#include - -class EnhancedWebPage : public QWebEnginePage -{ -protected: - bool certificateError(const QWebEngineCertificateError &certificateError) override; -}; - -#endif // !ECHANCEDWEBPAG diff --git a/GPClient/enhancedwebview.cpp b/GPClient/enhancedwebview.cpp deleted file mode 100644 index ea20be0..0000000 --- a/GPClient/enhancedwebview.cpp +++ /dev/null @@ -1,33 +0,0 @@ -#include -#include - -#include "enhancedwebpage.h" -#include "enhancedwebview.h" -#include "cdpcommandmanager.h" - -EnhancedWebView::EnhancedWebView(QWidget *parent) - : QWebEngineView(parent) - , cdp(new CDPCommandManager) -{ - QObject::connect(cdp, &CDPCommandManager::ready, this, &EnhancedWebView::onCDPReady); - QObject::connect(cdp, &CDPCommandManager::eventReceived, this, &EnhancedWebView::onEventReceived); -} - -void EnhancedWebView::initialize() -{ - setPage(new EnhancedWebPage()); - auto port = QProcessEnvironment::systemEnvironment().value(ENV_CDP_PORT); - cdp->initialize("http://127.0.0.1:" + port + "/json"); -} - -void EnhancedWebView::onCDPReady() -{ - cdp->sendCommand("Network.enable"); -} - -void EnhancedWebView::onEventReceived(QString eventName, QJsonObject params) -{ - if (eventName == "Network.responseReceived") { - emit responseReceived(params); - } -} diff --git a/GPClient/enhancedwebview.h b/GPClient/enhancedwebview.h deleted file mode 100644 index d402333..0000000 --- a/GPClient/enhancedwebview.h +++ /dev/null @@ -1,29 +0,0 @@ -#ifndef ENHANCEDWEBVIEW_H -#define ENHANCEDWEBVIEW_H - -#include - -#include "cdpcommandmanager.h" - -#define ENV_CDP_PORT "QTWEBENGINE_REMOTE_DEBUGGING" - -class EnhancedWebView : public QWebEngineView -{ - Q_OBJECT -public: - explicit EnhancedWebView(QWidget *parent = nullptr); - - void initialize(); - -signals: - void responseReceived(QJsonObject params); - -private slots: - void onCDPReady(); - void onEventReceived(QString eventName, QJsonObject params); - -private: - CDPCommandManager *cdp { nullptr }; -}; - -#endif // ENHANCEDWEBVIEW_H diff --git a/GPClient/gatewayauthenticator.cpp b/GPClient/gatewayauthenticator.cpp deleted file mode 100644 index 0d429a5..0000000 --- a/GPClient/gatewayauthenticator.cpp +++ /dev/null @@ -1,226 +0,0 @@ -#include -#include -#include -#include - -#include "gatewayauthenticator.h" -#include "gphelper.h" -#include "loginparams.h" -#include "preloginresponse.h" -#include "challengedialog.h" - -using namespace gpclient::helper; - -GatewayAuthenticator::GatewayAuthenticator(const QString& gateway, GatewayAuthenticatorParams params) - : QObject() - , gateway(gateway) - , params(params) - , preloginUrl("https://" + gateway + "/ssl-vpn/prelogin.esp?tmp=tmp&kerberos-support=yes&ipv6-support=yes&clientVer=4100") - , loginUrl("https://" + gateway + "/ssl-vpn/login.esp") -{ - if (!params.clientos().isEmpty()) { - preloginUrl = preloginUrl + "&clientos=" + params.clientos(); - } -} - -void GatewayAuthenticator::authenticate() -{ - LOGI << "Start gateway authentication..."; - - LoginParams loginParams { params.clientos() }; - loginParams.setUser(params.username()); - loginParams.setPassword(params.password()); - loginParams.setUserAuthCookie(params.userAuthCookie()); - loginParams.setInputStr(params.inputStr()); - - login(loginParams); -} - -void GatewayAuthenticator::login(const LoginParams &loginParams) -{ - LOGI << QString("Trying to login the gateway at %1, with %2").arg(loginUrl).arg(QString(loginParams.toUtf8())); - - auto *reply = createRequest(loginUrl, loginParams.toUtf8()); - connect(reply, &QNetworkReply::finished, this, &GatewayAuthenticator::onLoginFinished); -} - -void GatewayAuthenticator::onLoginFinished() -{ - QNetworkReply *reply = qobject_cast(sender()); - QByteArray response = reply->readAll(); - - if (reply->error() || response.contains("Authentication failure")) { - LOGE << QString("Failed to login the gateway at %1, %2").arg(loginUrl, reply->errorString()); - - if (standardLoginWindow) { - standardLoginWindow->setProcessing(false); - openMessageBox("Gateway login failed.", "Please check your credentials and try again."); - } else { - doAuth(); - } - return; - } - - // 2FA - if (response.contains("Challenge")) { - LOGI << "The server need input the challenge..."; - showChallenge(response); - return; - } - - if (standardLoginWindow) { - standardLoginWindow->close(); - } - - const auto params = gpclient::helper::parseGatewayResponse(response); - emit success(params.toString()); -} - -void GatewayAuthenticator::doAuth() -{ - LOGI << "Perform the gateway prelogin at " << preloginUrl; - - auto *reply = createRequest(preloginUrl); - connect(reply, &QNetworkReply::finished, this, &GatewayAuthenticator::onPreloginFinished); -} - -void GatewayAuthenticator::onPreloginFinished() -{ - auto *reply = qobject_cast(sender()); - - if (reply->error()) { - LOGE << QString("Failed to prelogin the gateway at %1, %2").arg(preloginUrl, reply->errorString()); - - emit fail("Error occurred on the gateway prelogin interface."); - return; - } - - LOGI << "Gateway prelogin succeeded."; - - auto response = PreloginResponse::parse(reply->readAll()); - - if (response.hasSamlAuthFields()) { - samlAuth(response.samlMethod(), response.samlRequest(), reply->url().toString()); - } else if (response.hasNormalAuthFields()) { - normalAuth(response.labelUsername(), response.labelPassword(), response.authMessage()); - } else { - LOGE << QString("Unknown prelogin response for %1, got %2").arg(preloginUrl, QString::fromUtf8(response.rawResponse())); - emit fail("Unknown response for gateway prelogin interface."); - } - - delete reply; -} - -void GatewayAuthenticator::normalAuth(QString labelUsername, QString labelPassword, QString authMessage) -{ - LOGI << QString("Trying to perform the normal login with %1 / %2 credentials").arg(labelUsername, labelPassword); - - standardLoginWindow = new StandardLoginWindow {gateway, labelUsername, labelPassword, authMessage}; - - // Do login - connect(standardLoginWindow, &StandardLoginWindow::performLogin, this, &GatewayAuthenticator::onPerformStandardLogin); - connect(standardLoginWindow, &StandardLoginWindow::rejected, this, &GatewayAuthenticator::onLoginWindowRejected); - connect(standardLoginWindow, &StandardLoginWindow::finished, this, &GatewayAuthenticator::onLoginWindowFinished); - - standardLoginWindow->show(); -} - -void GatewayAuthenticator::onPerformStandardLogin(const QString &username, const QString &password) -{ - LOGI << "Start to perform normal login..."; - - standardLoginWindow->setProcessing(true); - params.setUsername(username); - params.setPassword(password); - - authenticate(); -} - -void GatewayAuthenticator::onLoginWindowRejected() -{ - emit fail(); -} - -void GatewayAuthenticator::onLoginWindowFinished() -{ - delete standardLoginWindow; - standardLoginWindow = nullptr; -} - -void GatewayAuthenticator::samlAuth(QString samlMethod, QString samlRequest, QString preloginUrl) -{ - LOGI << "Trying to perform SAML login with saml-method " << samlMethod; - - auto *loginWindow = new SAMLLoginWindow; - - connect(loginWindow, &SAMLLoginWindow::success, [this, loginWindow](const QMap &samlResult) { - this->onSAMLLoginSuccess(samlResult); - loginWindow->deleteLater(); - }); - connect(loginWindow, &SAMLLoginWindow::fail, [this, loginWindow](const QString &code, const QString &error) { - this->onSAMLLoginFail(code, error); - loginWindow->deleteLater(); - }); - connect(loginWindow, &SAMLLoginWindow::rejected, [this, loginWindow]() { - this->onLoginWindowRejected(); - loginWindow->deleteLater(); - }); - - loginWindow->login(samlMethod, samlRequest, preloginUrl); -} - -void GatewayAuthenticator::onSAMLLoginSuccess(const QMap &samlResult) -{ - if (samlResult.contains("preloginCookie")) { - LOGI << "SAML login succeeded, got the prelogin-cookie " << samlResult.value("preloginCookie"); - } else { - LOGI << "SAML login succeeded, got the portal-userauthcookie " << samlResult.value("userAuthCookie"); - } - - LoginParams loginParams { params.clientos() }; - loginParams.setUser(samlResult.value("username")); - loginParams.setPreloginCookie(samlResult.value("preloginCookie")); - loginParams.setUserAuthCookie(samlResult.value("userAuthCookie")); - - login(loginParams); -} - -void GatewayAuthenticator::onSAMLLoginFail(const QString &code, const QString &msg) -{ - emit fail(msg); -} - -void GatewayAuthenticator::showChallenge(const QString &responseText) -{ - QRegularExpression re("\"(.*?)\";"); - QRegularExpressionMatchIterator i = re.globalMatch(responseText); - - i.next(); // Skip the status value - QString message = i.next().captured(1); - QString inputStr = i.next().captured(1); - // update the inputSrc field - params.setInputStr(inputStr); - - challengeDialog = new ChallengeDialog; - challengeDialog->setMessage(message); - - connect(challengeDialog, &ChallengeDialog::accepted, this, [this] { - params.setPassword(challengeDialog->getChallenge()); - LOGI << "Challenge submitted, try to re-authenticate..."; - authenticate(); - }); - - connect(challengeDialog, &ChallengeDialog::rejected, this, [this] { - if (standardLoginWindow) { - standardLoginWindow->close(); - } - emit fail(); - }); - - connect(challengeDialog, &ChallengeDialog::finished, this, [this] { - delete challengeDialog; - challengeDialog = nullptr; - }); - - challengeDialog->show(); -} diff --git a/GPClient/gatewayauthenticator.h b/GPClient/gatewayauthenticator.h deleted file mode 100644 index e3fba4e..0000000 --- a/GPClient/gatewayauthenticator.h +++ /dev/null @@ -1,48 +0,0 @@ -#ifndef GATEWAYAUTHENTICATOR_H -#define GATEWAYAUTHENTICATOR_H - -#include - -#include "standardloginwindow.h" -#include "challengedialog.h" -#include "loginparams.h" -#include "gatewayauthenticatorparams.h" - -class GatewayAuthenticator : public QObject -{ - Q_OBJECT -public: - explicit GatewayAuthenticator(const QString &gateway, GatewayAuthenticatorParams params); - - void authenticate(); - -signals: - void success(const QString &authCookie); - void fail(const QString &msg = ""); - -private slots: - void onLoginFinished(); - void onPreloginFinished(); - void onPerformStandardLogin(const QString &username, const QString &password); - void onLoginWindowRejected(); - void onLoginWindowFinished(); - void onSAMLLoginSuccess(const QMap &samlResult); - void onSAMLLoginFail(const QString &code, const QString &msg); - -private: - QString gateway; - GatewayAuthenticatorParams params; - QString preloginUrl; - QString loginUrl; - - StandardLoginWindow *standardLoginWindow { nullptr }; - ChallengeDialog *challengeDialog { nullptr }; - - void login(const LoginParams& loginParams); - void doAuth(); - void normalAuth(QString labelUsername, QString labelPassword, QString authMessage); - void samlAuth(QString samlMethod, QString samlRequest, QString preloginUrl = ""); - void showChallenge(const QString &responseText); -}; - -#endif // GATEWAYAUTHENTICATOR_H diff --git a/GPClient/gatewayauthenticatorparams.cpp b/GPClient/gatewayauthenticatorparams.cpp deleted file mode 100644 index 59eb789..0000000 --- a/GPClient/gatewayauthenticatorparams.cpp +++ /dev/null @@ -1,66 +0,0 @@ -#include "gatewayauthenticatorparams.h" - -GatewayAuthenticatorParams::GatewayAuthenticatorParams() -{ - -} - -GatewayAuthenticatorParams GatewayAuthenticatorParams::fromPortalConfigResponse(const PortalConfigResponse &portalConfig) -{ - GatewayAuthenticatorParams params; - params.setUsername(portalConfig.username()); - params.setPassword(portalConfig.password()); - params.setUserAuthCookie(portalConfig.userAuthCookie()); - - return params; -} - -const QString &GatewayAuthenticatorParams::username() const -{ - return m_username; -} - -void GatewayAuthenticatorParams::setUsername(const QString &newUsername) -{ - m_username = newUsername; -} - -const QString &GatewayAuthenticatorParams::password() const -{ - return m_password; -} - -void GatewayAuthenticatorParams::setPassword(const QString &newPassword) -{ - m_password = newPassword; -} - -const QString &GatewayAuthenticatorParams::userAuthCookie() const -{ - return m_userAuthCookie; -} - -void GatewayAuthenticatorParams::setUserAuthCookie(const QString &newUserAuthCookie) -{ - m_userAuthCookie = newUserAuthCookie; -} - -const QString &GatewayAuthenticatorParams::clientos() const -{ - return m_clientos; -} - -void GatewayAuthenticatorParams::setClientos(const QString &newClientos) -{ - m_clientos = newClientos; -} - -const QString &GatewayAuthenticatorParams::inputStr() const -{ - return m_inputStr; -} - -void GatewayAuthenticatorParams::setInputStr(const QString &inputStr) -{ - m_inputStr = inputStr; -} diff --git a/GPClient/gatewayauthenticatorparams.h b/GPClient/gatewayauthenticatorparams.h deleted file mode 100644 index ec48fc1..0000000 --- a/GPClient/gatewayauthenticatorparams.h +++ /dev/null @@ -1,38 +0,0 @@ -#ifndef GATEWAYAUTHENTICATORPARAMS_H -#define GATEWAYAUTHENTICATORPARAMS_H - -#include - -#include "portalconfigresponse.h" - -class GatewayAuthenticatorParams -{ -public: - GatewayAuthenticatorParams(); - - static GatewayAuthenticatorParams fromPortalConfigResponse(const PortalConfigResponse &portalConfig); - - const QString &username() const; - void setUsername(const QString &newUsername); - - const QString &password() const; - void setPassword(const QString &newPassword); - - const QString &userAuthCookie() const; - void setUserAuthCookie(const QString &newUserAuthCookie); - - const QString &clientos() const; - void setClientos(const QString &newClientos); - - const QString &inputStr() const; - void setInputStr(const QString &inputStr); - -private: - QString m_username; - QString m_password; - QString m_userAuthCookie; - QString m_clientos; - QString m_inputStr; -}; - -#endif // GATEWAYAUTHENTICATORPARAMS_H diff --git a/GPClient/gpclient.cpp b/GPClient/gpclient.cpp deleted file mode 100644 index 6137eff..0000000 --- a/GPClient/gpclient.cpp +++ /dev/null @@ -1,519 +0,0 @@ -#include -#include - -#include "gpclient.h" -#include "gphelper.h" -#include "ui_gpclient.h" -#include "portalauthenticator.h" -#include "gatewayauthenticator.h" -#include "settingsdialog.h" -#include "gatewayauthenticatorparams.h" - -using namespace gpclient::helper; - -GPClient::GPClient(QWidget *parent, IVpn *vpn) - : QMainWindow(parent) - , ui(new Ui::GPClient) - , vpn(vpn) - , settingsDialog(new SettingsDialog(this)) -{ - ui->setupUi(this); - - setWindowTitle("GlobalProtect"); - setFixedSize(width(), height()); - gpclient::helper::moveCenter(this); - - setupSettings(); - - // Restore portal from the previous settings - this->portal(settings::get("portal", "").toString()); - - // DBus service setup - QObject *ov = dynamic_cast(vpn); - connect(ov, SIGNAL(connected()), this, SLOT(onVPNConnected())); - connect(ov, SIGNAL(disconnected()), this, SLOT(onVPNDisconnected())); - connect(ov, SIGNAL(error(QString)), this, SLOT(onVPNError(QString))); - connect(ov, SIGNAL(logAvailable(QString)), this, SLOT(onVPNLogAvailable(QString))); - - // Initialize the context menu of system tray. - initSystemTrayIcon(); - initVpnStatus(); -} - -void GPClient::setupSettings() -{ - settingsButton = new QPushButton(this); - settingsButton->setIcon(QIcon(":/images/settings_icon.png")); - settingsButton->setFixedSize(QSize(28, 28)); - - QRect rect = this->geometry(); - settingsButton->setGeometry( - rect.width() - settingsButton->width() - 15, - 15, - settingsButton->geometry().width(), - settingsButton->geometry().height() - ); - - connect(settingsButton, &QPushButton::clicked, this, &GPClient::onSettingsButtonClicked); - connect(settingsDialog, &QDialog::accepted, this, &GPClient::onSettingsAccepted); -} - -void GPClient::onSettingsButtonClicked() -{ - settingsDialog->setClientos(settings::get("clientos", "Linux").toString()); - settingsDialog->setOsVersion(settings::get("os-version", QSysInfo::prettyProductName()).toString()); - settingsDialog->show(); -} - -void GPClient::onSettingsAccepted() -{ - settings::save("clientos", settingsDialog->clientos()); - settings::save("os-version", settingsDialog->osVersion()); -} - -void GPClient::on_connectButton_clicked() -{ - doConnect(); -} - -void GPClient::on_portalInput_returnPressed() -{ - doConnect(); -} - -void GPClient::on_portalInput_editingFinished() -{ - populateGatewayMenu(); -} - -void GPClient::initSystemTrayIcon() -{ - systemTrayIcon = new QSystemTrayIcon(this); - contextMenu = new QMenu("GlobalProtect", this); - - gatewaySwitchMenu = new QMenu("Switch Gateway", this); - gatewaySwitchMenu->setIcon(QIcon::fromTheme("network-workgroup")); - populateGatewayMenu(); - - systemTrayIcon->setIcon(QIcon(":/images/not_connected.png")); - systemTrayIcon->setToolTip("GlobalProtect"); - systemTrayIcon->setContextMenu(contextMenu); - - connect(systemTrayIcon, &QSystemTrayIcon::activated, this, &GPClient::onSystemTrayActivated); - connect(gatewaySwitchMenu, &QMenu::triggered, this, &GPClient::onGatewayChanged); - - openAction = contextMenu->addAction(QIcon::fromTheme("window-new"), "Open", this, &GPClient::activate); - connectAction = contextMenu->addAction(QIcon::fromTheme("preferences-system-network"), "Connect", this, &GPClient::doConnect); - contextMenu->addMenu(gatewaySwitchMenu); - contextMenu->addSeparator(); - clearAction = contextMenu->addAction(QIcon::fromTheme("edit-clear"), "Reset", this, &GPClient::reset); - quitAction = contextMenu->addAction(QIcon::fromTheme("application-exit"), "Quit", this, &GPClient::quit); - - systemTrayIcon->show(); -} - -void GPClient::initVpnStatus() { - int status = vpn->status(); - - if (status == 1) { - ui->statusLabel->setText("Connecting..."); - updateConnectionStatus(VpnStatus::pending); - } else if (status == 2) { - updateConnectionStatus(VpnStatus::connected); - } else if (status == 3) { - ui->statusLabel->setText("Disconnecting..."); - updateConnectionStatus(VpnStatus::pending); - } else { - updateConnectionStatus(VpnStatus::disconnected); - } -} - -void GPClient::populateGatewayMenu() -{ - LOGI << "Populating the Switch Gateway menu..."; - - const QList gateways = allGateways(); - gatewaySwitchMenu->clear(); - - if (gateways.isEmpty()) { - gatewaySwitchMenu->addAction("")->setData(-1); - return; - } - - const QString currentGatewayName = currentGateway().name(); - for (int i = 0; i < gateways.length(); i++) { - const GPGateway g = gateways.at(i); - QString iconImage = ":/images/radio_unselected.png"; - if (g.name() == currentGatewayName) { - iconImage = ":/images/radio_selected.png"; - } - gatewaySwitchMenu->addAction(QIcon(iconImage), QString("%1 (%2)").arg(g.name(), g.address()))->setData(i); - } -} - -void GPClient::updateConnectionStatus(const GPClient::VpnStatus &status) -{ - switch (status) { - case VpnStatus::disconnected: - ui->statusLabel->setText("Not Connected"); - ui->statusImage->setStyleSheet("image: url(:/images/not_connected.png); padding: 15;"); - ui->connectButton->setText("Connect"); - ui->connectButton->setDisabled(false); - ui->portalInput->setReadOnly(false); - - systemTrayIcon->setIcon(QIcon{ ":/images/not_connected.png" }); - connectAction->setEnabled(true); - connectAction->setText("Connect"); - gatewaySwitchMenu->setEnabled(true); - clearAction->setEnabled(true); - break; - case VpnStatus::pending: - ui->statusImage->setStyleSheet("image: url(:/images/pending.png); padding: 15;"); - ui->connectButton->setDisabled(true); - ui->portalInput->setReadOnly(true); - - systemTrayIcon->setIcon(QIcon{ ":/images/pending.png" }); - connectAction->setEnabled(false); - gatewaySwitchMenu->setEnabled(false); - clearAction->setEnabled(false); - break; - case VpnStatus::connected: - ui->statusLabel->setText("Connected"); - ui->statusImage->setStyleSheet("image: url(:/images/connected.png); padding: 15;"); - ui->connectButton->setText("Disconnect"); - ui->connectButton->setDisabled(false); - ui->portalInput->setReadOnly(true); - - systemTrayIcon->setIcon(QIcon{ ":/images/connected.png" }); - connectAction->setEnabled(true); - connectAction->setText("Disconnect"); - gatewaySwitchMenu->setEnabled(true); - clearAction->setEnabled(false); - break; - default: - break; - } -} - -void GPClient::onSystemTrayActivated(QSystemTrayIcon::ActivationReason reason) -{ - switch (reason) { - case QSystemTrayIcon::Trigger: - case QSystemTrayIcon::DoubleClick: - this->activate(); - break; - default: - break; - } -} - -void GPClient::onGatewayChanged(QAction *action) -{ - const int index = action->data().toInt(); - - if (index == -1) { - return; - } - - const auto g = allGateways().at(index); - - // If the selected gateway is the same as the current gateway - if (g.name() == currentGateway().name()) { - return; - } - - setCurrentGateway(g); - - if (connected()) { - ui->statusLabel->setText("Switching Gateway..."); - ui->connectButton->setEnabled(false); - - vpn->disconnect(); - isSwitchingGateway = true; - } -} - -void GPClient::doConnect() -{ - LOGI << "Start connecting..."; - - const auto btnText = ui->connectButton->text(); - const auto portal = this->portal(); - - // Display the main window if portal is empty - if (portal.isEmpty()) { - activate(); - return; - } - - if (btnText.endsWith("Connect")) { - settings::save("portal", portal); - - // Login to the previously saved gateway - if (!currentGateway().name().isEmpty()) { - LOGI << "Start gateway login using the previously saved gateway..."; - isQuickConnect = true; - gatewayLogin(); - } else { - // Perform the portal login - LOGI << "Start portal login..."; - portalLogin(); - } - } else { - LOGI << "Start disconnecting the VPN..."; - - ui->statusLabel->setText("Disconnecting..."); - updateConnectionStatus(VpnStatus::pending); - vpn->disconnect(); - } -} - -// Login to the portal interface to get the portal config and preferred gateway -void GPClient::portalLogin() -{ - auto *portalAuth = new PortalAuthenticator(portal(), settings::get("clientos", "Linux").toString()); - - connect(portalAuth, &PortalAuthenticator::success, [this, portalAuth](const PortalConfigResponse response, const QString region) { - this->onPortalSuccess(response, region); - portalAuth->deleteLater(); - }); - // Prelogin failed on the portal interface, try to treat the portal as a gateway interface - connect(portalAuth, &PortalAuthenticator::preloginFailed, [this, portalAuth](const QString msg) { - this->onPortalPreloginFail(msg); - portalAuth->deleteLater(); - }); - connect(portalAuth, &PortalAuthenticator::portalConfigFailed, [this, portalAuth](const QString msg) { - this->onPortalConfigFail(msg); - portalAuth->deleteLater(); - }); - // Portal login failed - connect(portalAuth, &PortalAuthenticator::fail, [this, portalAuth](const QString &msg) { - this->onPortalFail(msg); - portalAuth->deleteLater(); - }); - - ui->statusLabel->setText("Authenticating..."); - updateConnectionStatus(VpnStatus::pending); - portalAuth->authenticate(); -} - -void GPClient::onPortalSuccess(const PortalConfigResponse portalConfig, const QString region) -{ - LOGI << "Portal authentication succeeded."; - - // No gateway found in portal configuration - if (portalConfig.allGateways().size() == 0) { - LOGI << "No gateway found in portal configuration, treat the portal address as a gateway."; - tryGatewayLogin(); - return; - } - - GPGateway gateway = filterPreferredGateway(portalConfig.allGateways(), region); - setAllGateways(portalConfig.allGateways()); - setCurrentGateway(gateway); - this->portalConfig = portalConfig; - - gatewayLogin(); -} - -void GPClient::onPortalPreloginFail(const QString msg) -{ - LOGI << "Portal prelogin failed, treat the portal address as a gateway." << msg; - tryGatewayLogin(); -} - -void GPClient::onPortalConfigFail(const QString msg) -{ - LOGI << "Failed to get the portal configuration, " << msg << " Treat the portal address as gateway."; - tryGatewayLogin(); -} - -void GPClient::onPortalFail(const QString &msg) -{ - if (!msg.isEmpty()) { - openMessageBox("Portal authentication failed.", msg); - } - - updateConnectionStatus(VpnStatus::disconnected); -} - -void GPClient::tryGatewayLogin() -{ - LOGI << "Try to perform login on the the gateway interface..."; - - // Treat the portal input as the gateway address - GPGateway g; - g.setName(portal()); - g.setAddress(portal()); - - QList gateways; - gateways.append(g); - - setAllGateways(gateways); - setCurrentGateway(g); - - gatewayLogin(); -} - -// Login to the gateway -void GPClient::gatewayLogin() -{ - LOGI << "Performing gateway login..."; - - GatewayAuthenticatorParams params = GatewayAuthenticatorParams::fromPortalConfigResponse(portalConfig); - params.setClientos(settings::get("clientos", "Linux").toString()); - - GatewayAuthenticator *gatewayAuth; - gatewayAuth = new GatewayAuthenticator(currentGateway().address(), params); - - connect(gatewayAuth, &GatewayAuthenticator::success, [this, gatewayAuth](const QString &authToken) { - this->onGatewaySuccess(authToken); - gatewayAuth->deleteLater(); - }); - connect(gatewayAuth, &GatewayAuthenticator::fail, [this, gatewayAuth](const QString &msg) { - this->onGatewayFail(msg); - gatewayAuth->deleteLater(); - }); - - ui->statusLabel->setText("Authenticating..."); - updateConnectionStatus(VpnStatus::pending); - gatewayAuth->authenticate(); -} - -void GPClient::onGatewaySuccess(const QString &authCookie) -{ - LOGI << "Gateway login succeeded, got the cookie " << authCookie; - - isQuickConnect = false; - QList gatewayAddresses; - for (GPGateway &gw : allGateways()) { - gatewayAddresses.push_back(gw.address()); - } - vpn->connect(currentGateway().address(), gatewayAddresses, portalConfig.username(), authCookie); - ui->statusLabel->setText("Connecting..."); - updateConnectionStatus(VpnStatus::pending); -} - -void GPClient::onGatewayFail(const QString &msg) -{ - // If the quick connect on gateway failed, perform the portal login - if (isQuickConnect && !msg.isEmpty()) { - isQuickConnect = false; - LOGI << "Quick connection failed, trying to portal login..."; - portalLogin(); - return; - } - - if (!msg.isEmpty()) { - openMessageBox("Gateway authentication failed.", msg); - } - - updateConnectionStatus(VpnStatus::disconnected); -} - -void GPClient::activate() -{ - activateWindow(); - showNormal(); -} - -QString GPClient::portal() const -{ - const QString input = ui->portalInput->text().trimmed(); - - if (input.startsWith("http")) { - return QUrl(input).authority(); - } - return input; -} - -void GPClient::portal(QString p) -{ - ui->portalInput->setText(p); -} - -bool GPClient::connected() const -{ - const QString statusText = ui->statusLabel->text(); - return statusText.contains("Connected") && !statusText.contains("Not"); -} - -QList GPClient::allGateways() const -{ - - QList gateways; - - for (auto g :settings::get_all("_gateways$") ){ - - gateways.append(GPGateway::fromJson(settings::get(g).toString())); - } - return gateways; -} - -void GPClient::setAllGateways(QList gateways) -{ - LOGI << "Updating all the gateways..."; - - settings::save(portal() + "_gateways", GPGateway::serialize(gateways)); - populateGatewayMenu(); -} - -GPGateway GPClient::currentGateway() const -{ - const QString selectedGateway = settings::get(portal() + "_selectedGateway").toString(); - - for (auto g : allGateways()) { - if (g.name() == selectedGateway) { - return g; - } - } - return GPGateway{}; -} - -void GPClient::setCurrentGateway(const GPGateway gateway) -{ - LOGI << "Updating the current gateway to " << gateway.name(); - - settings::save(portal() + "_selectedGateway", gateway.name()); - ui->portalInput->setText(gateway.address()); - populateGatewayMenu(); -} - -void GPClient::reset() -{ - settings::clear(); - populateGatewayMenu(); - ui->portalInput->clear(); -} - -void GPClient::quit() -{ - vpn->disconnect(); - QApplication::quit(); -} - -void GPClient::onVPNConnected() -{ - updateConnectionStatus(VpnStatus::connected); -} - -void GPClient::onVPNDisconnected() -{ - updateConnectionStatus(VpnStatus::disconnected); - - if (isSwitchingGateway) { - gatewayLogin(); - isSwitchingGateway = false; - } -} - -void GPClient::onVPNError(QString errorMessage) -{ - updateConnectionStatus(VpnStatus::disconnected); - openMessageBox("Failed to connect", errorMessage); -} - -void GPClient::onVPNLogAvailable(QString log) -{ - LOGI << log; -} diff --git a/GPClient/gpclient.h b/GPClient/gpclient.h deleted file mode 100644 index 20abad4..0000000 --- a/GPClient/gpclient.h +++ /dev/null @@ -1,104 +0,0 @@ -#ifndef GPCLIENT_H -#define GPCLIENT_H - -#include -#include -#include -#include - -#include "portalconfigresponse.h" -#include "settingsdialog.h" -#include "vpn.h" -#include "gatewayauthenticator.h" - -QT_BEGIN_NAMESPACE -namespace Ui { class GPClient; } -QT_END_NAMESPACE - -class GPClient : public QMainWindow -{ - Q_OBJECT - -public: - GPClient(QWidget *parent, IVpn *vpn); - - void activate(); - void quit(); - - QString portal() const; - void portal(QString); - - GPGateway currentGateway() const; - void setCurrentGateway(const GPGateway gateway); - - void doConnect(); - void reset(); - -private slots: - void onSettingsButtonClicked(); - void onSettingsAccepted(); - - void on_connectButton_clicked(); - void on_portalInput_returnPressed(); - void on_portalInput_editingFinished(); - - void onSystemTrayActivated(QSystemTrayIcon::ActivationReason reason); - void onGatewayChanged(QAction *action); - - void onPortalSuccess(const PortalConfigResponse portalConfig, const QString region); - void onPortalPreloginFail(const QString msg); - void onPortalConfigFail(const QString msg); - void onPortalFail(const QString &msg); - - void onGatewaySuccess(const QString &authCookie); - void onGatewayFail(const QString &msg); - - void onVPNConnected(); - void onVPNDisconnected(); - void onVPNError(QString errorMessage); - void onVPNLogAvailable(QString log); - -private: - enum class VpnStatus - { - disconnected, - pending, - connected - }; - - Ui::GPClient *ui; - IVpn *vpn; - - QSystemTrayIcon *systemTrayIcon; - QMenu *contextMenu; - QAction *openAction; - QAction *connectAction; - - QMenu *gatewaySwitchMenu; - QAction *clearAction; - QAction *quitAction; - - SettingsDialog *settingsDialog; - QPushButton *settingsButton; - - bool isQuickConnect { false }; - bool isSwitchingGateway { false }; - PortalConfigResponse portalConfig; - - void setupSettings(); - - void initSystemTrayIcon(); - void initVpnStatus(); - void populateGatewayMenu(); - void updateConnectionStatus(const VpnStatus &status); - - void portalLogin(); - void tryGatewayLogin(); - void gatewayLogin(); - - bool connected() const; - - QList allGateways() const; - void setAllGateways(QList gateways); -}; -#endif // GPCLIENT_H diff --git a/GPClient/gpclient.ui b/GPClient/gpclient.ui deleted file mode 100644 index 0b685d7..0000000 --- a/GPClient/gpclient.ui +++ /dev/null @@ -1,143 +0,0 @@ - - - GPClient - - - - 0 - 0 - 260 - 362 - - - - GlobalProtect OpenConnect - - - - :/images/logo.svg:/images/logo.svg - - - - - - - 22 - 22 - - - - - - 0 - 0 - - - - Qt::LeftToRight - - - - 15 - - - 15 - - - 15 - - - 15 - - - - - 15 - - - - - #statusImage { - image: url(:/images/not_connected.png); - padding: 15 -} - - - - - - - - - - - 14 - 50 - false - false - - - - Not Connected - - - Qt::AlignCenter - - - - - - - - - 0 - - - - - - - - Please enter your portal address - - - - - - - - 0 - 0 - - - - Connect - - - true - - - false - - - - - - - - - <html><head/><body><p align="center"><a href="https://bit.ly/3g5DHqy"><span style=" text-decoration: underline; color:#4c6b8a;">Report a bug</span></a> / <a href="https://bit.ly/3jQYfEi"><span style=" text-decoration: underline; color:#4c6b8a;">Buy me a coffee</span></a></p></body></html> - - - true - - - - - - - - - - - diff --git a/GPClient/gpgateway.cpp b/GPClient/gpgateway.cpp deleted file mode 100644 index 7b587ed..0000000 --- a/GPClient/gpgateway.cpp +++ /dev/null @@ -1,97 +0,0 @@ -#include -#include -#include - -#include "gpgateway.h" - -GPGateway::GPGateway() -{ -} - -QString GPGateway::name() const -{ - return _name; -} - -QString GPGateway::address() const -{ - return _address; -} - -void GPGateway::setName(const QString &name) -{ - _name = name; -} - -void GPGateway::setAddress(const QString &address) -{ - _address = address; -} - -void GPGateway::setPriorityRules(const QMap &priorityRules) -{ - _priorityRules = priorityRules; -} - -int GPGateway::priorityOf(QString ruleName) const -{ - if (_priorityRules.contains(ruleName)) { - return _priorityRules.value(ruleName); - } - return 0; -} - -QJsonObject GPGateway::toJsonObject() const -{ - QJsonObject obj; - obj.insert("name", name()); - obj.insert("address", address()); - - return obj; -} - -QString GPGateway::toString() const -{ - QJsonDocument jsonDoc{ toJsonObject() }; - return QString::fromUtf8(jsonDoc.toJson()); -} - -QString GPGateway::serialize(QList &gateways) -{ - QJsonArray arr; - - for (auto g : gateways) { - arr.append(g.toJsonObject()); - } - - QJsonDocument jsonDoc{ arr }; - return QString::fromUtf8(jsonDoc.toJson()); -} - -QList GPGateway::fromJson(const QString &jsonString) -{ - QList gateways; - - if (jsonString.isEmpty()) { - return gateways; - } - - QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonString.toUtf8()); - - for (auto item : jsonDoc.array()) { - GPGateway g = GPGateway::fromJsonObject(item.toObject()); - gateways.append(g); - } - - return gateways; -} - -GPGateway GPGateway::fromJsonObject(const QJsonObject &jsonObj) -{ - GPGateway g; - - g.setName(jsonObj.value("name").toString()); - g.setAddress(jsonObj.value("address").toString()); - - return g; -} diff --git a/GPClient/gpgateway.h b/GPClient/gpgateway.h deleted file mode 100644 index d879259..0000000 --- a/GPClient/gpgateway.h +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef GPGATEWAY_H -#define GPGATEWAY_H - -#include -#include -#include - -class GPGateway -{ -public: - GPGateway(); - - QString name() const; - QString address() const; - - void setName(const QString &name); - void setAddress(const QString &address); - void setPriorityRules(const QMap &priorityRules); - int priorityOf(QString ruleName) const; - QJsonObject toJsonObject() const; - QString toString() const; - - static QString serialize(QList &gateways); - static QList fromJson(const QString &jsonString); - static GPGateway fromJsonObject(const QJsonObject &jsonObj); - -private: - QString _name; - QString _address; - QMap _priorityRules; -}; - -#endif // GPGATEWAY_H diff --git a/GPClient/gphelper.cpp b/GPClient/gphelper.cpp deleted file mode 100644 index 8e45227..0000000 --- a/GPClient/gphelper.cpp +++ /dev/null @@ -1,178 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "gphelper.h" - -using namespace QKeychain; - -QNetworkAccessManager* gpclient::helper::networkManager = new QNetworkAccessManager; - -QNetworkReply* gpclient::helper::createRequest(QString url, QByteArray params) -{ - QNetworkRequest request(url); - - // Skip the ssl verifying - QSslConfiguration conf = request.sslConfiguration(); - conf.setPeerVerifyMode(QSslSocket::VerifyNone); - conf.setSslOption(QSsl::SslOptionDisableLegacyRenegotiation, false); - request.setSslConfiguration(conf); - - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - request.setHeader(QNetworkRequest::UserAgentHeader, UA); - - if (params == nullptr) { - return networkManager->post(request, QByteArray(nullptr)); - } - return networkManager->post(request, params); -} - -GPGateway gpclient::helper::filterPreferredGateway(QList gateways, const QString ruleName) -{ - LOGI << gateways.size() << " gateway(s) available, filter the gateways with rule: " << ruleName; - - GPGateway gateway = gateways.first(); - - for (GPGateway g : gateways) { - if (g.priorityOf(ruleName) > gateway.priorityOf(ruleName)) { - LOGI << "Find a preferred gateway: " << g.name(); - gateway = g; - } - } - - return gateway; -} - -QUrlQuery gpclient::helper::parseGatewayResponse(const QByteArray &xml) -{ - LOGI << "Start parsing the gateway response..."; - LOGI << "The gateway response is: " << xml; - - QXmlStreamReader xmlReader{xml}; - QList args; - - while (!xmlReader.atEnd()) { - xmlReader.readNextStartElement(); - if (xmlReader.name() == "argument") { - args.append(QUrl::toPercentEncoding(xmlReader.readElementText())); - } - } - - QUrlQuery params{}; - params.addQueryItem("authcookie", args.at(1)); - params.addQueryItem("portal", args.at(3)); - params.addQueryItem("user", args.at(4)); - params.addQueryItem("domain", args.at(7)); - params.addQueryItem("preferred-ip", args.at(15)); - params.addQueryItem("computer", QUrl::toPercentEncoding(QSysInfo::machineHostName())); - - return params; -} - -void gpclient::helper::openMessageBox(const QString &message, const QString& informativeText) -{ - QMessageBox msgBox; - msgBox.setWindowTitle("Notice"); - msgBox.setText(message); - msgBox.setFixedWidth(500); - msgBox.setStyleSheet("QLabel{min-width: 250px}"); - msgBox.setInformativeText(informativeText); - msgBox.exec(); -} - -void gpclient::helper::moveCenter(QWidget *widget) -{ - QDesktopWidget *desktop = QApplication::desktop(); - - int screenWidth, width; - int screenHeight, height; - int x, y; - QSize windowSize; - - screenWidth = desktop->width(); - screenHeight = desktop->height(); - - windowSize = widget->size(); - width = windowSize.width(); - height = windowSize.height(); - - x = (screenWidth - width) / 2; - y = (screenHeight - height) / 2; - y -= 50; - widget->move(x, y); -} - -QSettings *gpclient::helper::settings::_settings = new QSettings("com.yuezk.qt", "GPClient"); - -QVariant gpclient::helper::settings::get(const QString &key, const QVariant &defaultValue) -{ - return _settings->value(key, defaultValue); -} - -QStringList gpclient::helper::settings::get_all(const QString &key, const QVariant &defaultValue) -{ - QRegularExpression re(key); - return _settings->allKeys().filter(re); -} - -void gpclient::helper::settings::save(const QString &key, const QVariant &value) -{ - _settings->setValue(key, value); -} - - -void gpclient::helper::settings::clear() -{ - QStringList keys = _settings->allKeys(); - for (const auto &key : qAsConst(keys)) { - if (!reservedKeys.contains(key)) { - _settings->remove(key); - } - } - - QWebEngineProfile::defaultProfile()->cookieStore()->deleteAllCookies(); -} - - -bool gpclient::helper::settings::secureSave(const QString &key, const QString &value) { - WritePasswordJob job( QLatin1String("gpclient") ); - job.setAutoDelete( false ); - job.setKey( key ); - job.setTextData( value ); - QEventLoop loop; - job.connect( &job, SIGNAL(finished(QKeychain::Job*)), &loop, SLOT(quit()) ); - job.start(); - loop.exec(); - if ( job.error() ) { - return false; - } - - return true; -} - -bool gpclient::helper::settings::secureGet(const QString &key, QString &value) { - ReadPasswordJob job( QLatin1String("gpclient") ); - job.setAutoDelete( false ); - job.setKey( key ); - QEventLoop loop; - job.connect( &job, SIGNAL(finished(QKeychain::Job*)), &loop, SLOT(quit()) ); - job.start(); - loop.exec(); - - const QString pw = job.textData(); - if ( job.error() ) { - return false; - } - - value = pw; - return true; -} diff --git a/GPClient/gphelper.h b/GPClient/gphelper.h deleted file mode 100644 index 634f82e..0000000 --- a/GPClient/gphelper.h +++ /dev/null @@ -1,47 +0,0 @@ -#ifndef GPHELPER_H -#define GPHELPER_H - -#include -#include -#include -#include -#include -#include - -#include "samlloginwindow.h" -#include "gpgateway.h" - - -const QString UA = "PAN GlobalProtect"; - -namespace gpclient { - namespace helper { - extern QNetworkAccessManager *networkManager; - - QNetworkReply* createRequest(QString url, QByteArray params = nullptr); - - GPGateway filterPreferredGateway(QList gateways, const QString ruleName); - - QUrlQuery parseGatewayResponse(const QByteArray& xml); - - void openMessageBox(const QString& message, const QString& informativeText = ""); - - void moveCenter(QWidget *widget); - - namespace settings { - - extern QSettings *_settings; - static const QStringList reservedKeys {"extraArgs", "clientos"}; - - QVariant get(const QString &key, const QVariant &defaultValue = QVariant()); - QStringList get_all(const QString &key, const QVariant &defaultValue = QVariant()); - void save(const QString &key, const QVariant &value); - void clear(); - - bool secureSave(const QString &key, const QString &value); - bool secureGet(const QString &key, QString &value); - } - } -} - -#endif // GPHELPER_H diff --git a/GPClient/loginparams.cpp b/GPClient/loginparams.cpp deleted file mode 100644 index 21ea259..0000000 --- a/GPClient/loginparams.cpp +++ /dev/null @@ -1,88 +0,0 @@ -#include - -#include "loginparams.h" -#include "gphelper.h" - -using namespace gpclient::helper; - -LoginParams::LoginParams(const QString clientos) -{ - params.addQueryItem("prot", QUrl::toPercentEncoding("https:")); - params.addQueryItem("server", ""); - params.addQueryItem("inputStr", ""); - params.addQueryItem("jnlpReady", "jnlpReady"); - params.addQueryItem("user", ""); - params.addQueryItem("passwd", ""); - params.addQueryItem("computer", QUrl::toPercentEncoding(QSysInfo::machineHostName())); - params.addQueryItem("ok", "Login"); - params.addQueryItem("direct", "yes"); - params.addQueryItem("clientVer", "4100"); - - // add the clientos parameter if not empty - if (!clientos.isEmpty()) { - params.addQueryItem("clientos", clientos); - } - - auto osVersion = settings::get("os-version", "").toString(); - if (osVersion.isEmpty()) { - osVersion = QSysInfo::prettyProductName(); - } - params.addQueryItem("os-version", QUrl::toPercentEncoding(osVersion)); - - params.addQueryItem("portal-userauthcookie", ""); - params.addQueryItem("portal-prelogonuserauthcookie", ""); - params.addQueryItem("prelogin-cookie", ""); - params.addQueryItem("ipv6-support", "yes"); -} - -LoginParams::~LoginParams() -{ -} - -void LoginParams::setUser(const QString user) -{ - updateQueryItem("user", user); -} - -void LoginParams::setServer(const QString server) -{ - updateQueryItem("server", server); -} - -void LoginParams::setPassword(const QString password) -{ - updateQueryItem("passwd", password); -} - -void LoginParams::setUserAuthCookie(const QString cookie) -{ - updateQueryItem("portal-userauthcookie", cookie); -} - -void LoginParams::setPrelogonAuthCookie(const QString cookie) -{ - updateQueryItem("portal-prelogonuserauthcookie", cookie); -} - -void LoginParams::setPreloginCookie(const QString cookie) -{ - updateQueryItem("prelogin-cookie", cookie); -} - -void LoginParams::setInputStr(const QString inputStr) -{ - updateQueryItem("inputStr", inputStr); -} - -QByteArray LoginParams::toUtf8() const -{ - return params.toString().toUtf8(); -} - -void LoginParams::updateQueryItem(const QString key, const QString value) -{ - if (params.hasQueryItem(key)) { - params.removeQueryItem(key); - } - params.addQueryItem(key, QUrl::toPercentEncoding(value)); -} diff --git a/GPClient/loginparams.h b/GPClient/loginparams.h deleted file mode 100644 index 1660ad5..0000000 --- a/GPClient/loginparams.h +++ /dev/null @@ -1,28 +0,0 @@ -#ifndef LOGINPARAMS_H -#define LOGINPARAMS_H - -#include - -class LoginParams -{ -public: - LoginParams(const QString clientos); - ~LoginParams(); - - void setUser(const QString user); - void setServer(const QString server); - void setPassword(const QString password); - void setUserAuthCookie(const QString cookie); - void setPrelogonAuthCookie(const QString cookie); - void setPreloginCookie(const QString cookie); - void setInputStr(const QString inputStr); - - QByteArray toUtf8() const; - -private: - QUrlQuery params; - - void updateQueryItem(const QString key, const QString value); -}; - -#endif // LOGINPARAMS_H diff --git a/GPClient/main.cpp b/GPClient/main.cpp deleted file mode 100644 index 9970681..0000000 --- a/GPClient/main.cpp +++ /dev/null @@ -1,96 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include - -#include "singleapplication.h" -#include "gpclient.h" -#include "vpn_dbus.h" -#include "vpn_json.h" -#include "enhancedwebview.h" -#include "sigwatch.h" -#include "version.h" - -#define QT_AUTO_SCREEN_SCALE_FACTOR "QT_AUTO_SCREEN_SCALE_FACTOR" - -int main(int argc, char *argv[]) -{ - plog::ColorConsoleAppender consoleAppender(plog::streamStdErr); - plog::init(plog::debug, &consoleAppender); - - LOGI << "GlobalProtect started, version: " << VERSION; - - auto port = QString::fromLocal8Bit(qgetenv(ENV_CDP_PORT)); - auto hidpiSupport = QString::fromLocal8Bit(qgetenv(QT_AUTO_SCREEN_SCALE_FACTOR)); - - if (port.isEmpty()) { - qputenv(ENV_CDP_PORT, "12315"); - } - - if (hidpiSupport.isEmpty()) { - qputenv(QT_AUTO_SCREEN_SCALE_FACTOR, "1"); - } - - SingleApplication app(argc, argv); - app.setQuitOnLastWindowClosed(false); - - QCommandLineParser parser; - parser.addHelpOption(); - parser.addVersionOption(); - parser.addPositionalArgument("server", "The URL of the VPN server. Optional."); - parser.addPositionalArgument("gateway", "The URL of the specific VPN gateway. Optional."); - parser.addOptions({ - {"json", "Write the result of the handshake with the GlobalConnect server to stdout as JSON and terminate. Useful for scripting."}, - {"now", "Do not show the dialog with the connect button; connect immediately instead."}, - {"start-minimized", "Launch the client minimized."}, - {"reset", "Reset the client's settings."}, - }); - parser.process(app); - - const auto positional = parser.positionalArguments(); - - auto *vpn = parser.isSet("json") // yes it leaks, but this is cleared on exit anyway - ? static_cast(new VpnJson(nullptr)) // Print to stdout and exit - : static_cast(new VpnDbus(nullptr)); // Contact GPService daemon via dbus - GPClient w(nullptr, vpn); - - if (positional.size() > 0) { - w.portal(positional.at(0)); - } - if (positional.size() > 1) { - GPGateway gw; - gw.setName(positional.at(1)); - gw.setAddress(positional.at(1)); - w.setCurrentGateway(gw); - } - - QObject::connect(&app, &SingleApplication::instanceStarted, &w, &GPClient::activate); - - UnixSignalWatcher sigwatch; - sigwatch.watchForSignal(SIGINT); - sigwatch.watchForSignal(SIGTERM); - sigwatch.watchForSignal(SIGQUIT); - sigwatch.watchForSignal(SIGHUP); - QObject::connect(&sigwatch, &UnixSignalWatcher::unixSignal, &w, &GPClient::quit); - - if (parser.isSet("json")) { - QObject::connect(static_cast(vpn), &VpnJson::connected, &w, &GPClient::quit); - } - - if (parser.isSet("reset")) { - w.reset(); - } - - if (parser.isSet("now")) { - w.doConnect(); - } else if (parser.isSet("start-minimized")) { - w.showMinimized(); - } else { - w.show(); - } - - return app.exec(); -} diff --git a/GPClient/not_connected.png b/GPClient/not_connected.png deleted file mode 100644 index 99adc1be0b877614e5e5a4168fc8cff9e74c0caf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16393 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 W8} -#include - -#include "portalauthenticator.h" -#include "gphelper.h" -#include "standardloginwindow.h" -#include "samlloginwindow.h" -#include "loginparams.h" -#include "preloginresponse.h" -#include "portalconfigresponse.h" -#include "gpgateway.h" - -using namespace gpclient::helper; - -PortalAuthenticator::PortalAuthenticator(const QString& portal, const QString& clientos) : QObject() - , portal(portal) - , clientos(clientos) - , preloginUrl("https://" + portal + "/global-protect/prelogin.esp?tmp=tmp&kerberos-support=yes&ipv6-support=yes&clientVer=4100") - , configUrl("https://" + portal + "/global-protect/getconfig.esp") -{ - if (!clientos.isEmpty()) { - preloginUrl = preloginUrl + "&clientos=" + clientos; - } -} - -PortalAuthenticator::~PortalAuthenticator() -{ - delete standardLoginWindow; -} - -void PortalAuthenticator::authenticate() -{ - attempts++; - - LOGI << QString("(%1/%2) attempts").arg(attempts).arg(MAX_ATTEMPTS) << ", perform portal prelogin at " << preloginUrl; - - QNetworkReply *reply = createRequest(preloginUrl); - connect(reply, &QNetworkReply::finished, this, &PortalAuthenticator::onPreloginFinished); -} - -void PortalAuthenticator::onPreloginFinished() -{ - auto *reply = qobject_cast(sender()); - - if (reply->error()) { - LOGE << QString("Error occurred while accessing %1, %2").arg(preloginUrl, reply->errorString()); - emit preloginFailed("Error occurred on the portal prelogin interface."); - delete reply; - return; - } - - LOGI << "Portal prelogin succeeded."; - - preloginResponse = PreloginResponse::parse(reply->readAll()); - - LOGI << "Finished parsing the prelogin response. The region field is: " << preloginResponse.region(); - - if (preloginResponse.hasSamlAuthFields()) { - // Do SAML authentication - samlAuth(); - } else if (preloginResponse.hasNormalAuthFields()) { - // Do normal username/password authentication - tryAutoLogin(); - } else { - LOGE << QString("Unknown prelogin response for %1 got %2").arg(preloginUrl).arg(QString::fromUtf8(preloginResponse.rawResponse())); - emit preloginFailed("Unknown response for portal prelogin interface."); - } - - delete reply; -} - -void PortalAuthenticator::tryAutoLogin() -{ - const QString username = settings::get("username").toString(); - const QString password = settings::get("password").toString(); - - if (!username.isEmpty() && !password.isEmpty()) { - LOGI << "Trying auto login using the saved credentials"; - isAutoLogin = true; - fetchConfig(settings::get("username").toString(), settings::get("password").toString()); - } else { - normalAuth(); - } -} - -void PortalAuthenticator::normalAuth() -{ - LOGI << "Trying to launch the normal login window..."; - - standardLoginWindow = new StandardLoginWindow {portal, preloginResponse.labelUsername(), preloginResponse.labelPassword(), preloginResponse.authMessage() }; - - // Do login - connect(standardLoginWindow, &StandardLoginWindow::performLogin, this, &PortalAuthenticator::onPerformNormalLogin); - connect(standardLoginWindow, &StandardLoginWindow::rejected, this, &PortalAuthenticator::onLoginWindowRejected); - connect(standardLoginWindow, &StandardLoginWindow::finished, this, &PortalAuthenticator::onLoginWindowFinished); - - standardLoginWindow->show(); -} - -void PortalAuthenticator::onPerformNormalLogin(const QString &username, const QString &password) -{ - standardLoginWindow->setProcessing(true); - fetchConfig(username, password); -} - -void PortalAuthenticator::onLoginWindowRejected() -{ - emitFail(); -} - -void PortalAuthenticator::onLoginWindowFinished() -{ - delete standardLoginWindow; - standardLoginWindow = nullptr; -} - -void PortalAuthenticator::samlAuth() -{ - LOGI << "Trying to perform SAML login with saml-method " << preloginResponse.samlMethod(); - - auto *loginWindow = new SAMLLoginWindow; - - connect(loginWindow, &SAMLLoginWindow::success, [this, loginWindow](const QMap samlResult) { - this->onSAMLLoginSuccess(samlResult); - loginWindow->deleteLater(); - }); - connect(loginWindow, &SAMLLoginWindow::fail, [this, loginWindow](const QString &code, const QString msg) { - this->onSAMLLoginFail(code, msg); - loginWindow->deleteLater(); - }); - connect(loginWindow, &SAMLLoginWindow::rejected, [this, loginWindow]() { - this->onLoginWindowRejected(); - loginWindow->deleteLater(); - }); - - loginWindow->login(preloginResponse.samlMethod(), preloginResponse.samlRequest(), preloginUrl); -} - -void PortalAuthenticator::onSAMLLoginSuccess(const QMap samlResult) -{ - if (samlResult.contains("preloginCookie")) { - LOGI << "SAML login succeeded, got the prelogin-cookie"; - } else { - LOGI << "SAML login succeeded, got the portal-userauthcookie"; - } - - fetchConfig(samlResult.value("username"), "", samlResult.value("preloginCookie"), samlResult.value("userAuthCookie")); -} - -void PortalAuthenticator::onSAMLLoginFail(const QString &code, const QString &msg) -{ - if (code == "ERR002" && attempts < MAX_ATTEMPTS) { - LOGI << "Failed to authenticate, trying to re-authenticate..."; - authenticate(); - } else { - emitFail(msg); - } -} - -void PortalAuthenticator::fetchConfig(QString username, QString password, QString preloginCookie, QString userAuthCookie) -{ - LoginParams loginParams { clientos }; - loginParams.setServer(portal); - loginParams.setUser(username); - loginParams.setPassword(password); - loginParams.setPreloginCookie(preloginCookie); - loginParams.setUserAuthCookie(userAuthCookie); - - // Save the username and password for future use. - this->username = username; - this->password = password; - - LOGI << "Fetching the portal config from " << configUrl; - - auto *reply = createRequest(configUrl, loginParams.toUtf8()); - connect(reply, &QNetworkReply::finished, this, &PortalAuthenticator::onFetchConfigFinished); -} - -void PortalAuthenticator::onFetchConfigFinished() -{ - QNetworkReply *reply = qobject_cast(sender()); - - if (reply->error()) { - LOGE << QString("Failed to fetch the portal config from %1, %2").arg(configUrl).arg(reply->errorString()); - - // Login failed, enable the fields of the normal login window - if (standardLoginWindow) { - standardLoginWindow->setProcessing(false); - openMessageBox("Portal login failed.", "Please check your credentials and try again."); - } else if (isAutoLogin) { - isAutoLogin = false; - normalAuth(); - } else { - emit portalConfigFailed("Failed to fetch the portal config."); - } - return; - } - - LOGI << "Fetch the portal config succeeded."; - PortalConfigResponse response = PortalConfigResponse::parse(reply->readAll()); - - // Add the username & password to the response object - response.setUsername(username); - response.setPassword(password); - - // Close the login window - if (standardLoginWindow) { - LOGI << "Closing the StandardLoginWindow..."; - - standardLoginWindow->close(); - } - - emit success(response, preloginResponse.region()); -} - -void PortalAuthenticator::emitFail(const QString& msg) -{ - emit fail(msg); -} diff --git a/GPClient/portalauthenticator.h b/GPClient/portalauthenticator.h deleted file mode 100644 index b0dadf8..0000000 --- a/GPClient/portalauthenticator.h +++ /dev/null @@ -1,60 +0,0 @@ -#ifndef PORTALAUTHENTICATOR_H -#define PORTALAUTHENTICATOR_H - -#include - -#include "portalconfigresponse.h" -#include "standardloginwindow.h" -#include "samlloginwindow.h" -#include "preloginresponse.h" - - -class PortalAuthenticator : public QObject -{ - Q_OBJECT -public: - explicit PortalAuthenticator(const QString& portal, const QString& clientos); - ~PortalAuthenticator(); - - void authenticate(); - -signals: - void success(const PortalConfigResponse response, const QString region); - void fail(const QString& msg); - void preloginFailed(const QString& msg); - void portalConfigFailed(const QString msg); - -private slots: - void onPreloginFinished(); - void onPerformNormalLogin(const QString &username, const QString &password); - void onLoginWindowRejected(); - void onLoginWindowFinished(); - void onSAMLLoginSuccess(const QMap samlResult); - void onSAMLLoginFail(const QString &code, const QString &msg); - void onFetchConfigFinished(); - -private: - static const auto MAX_ATTEMPTS{ 5 }; - - QString portal; - QString clientos; - QString preloginUrl; - QString configUrl; - QString username; - QString password; - - int attempts{ 0 }; - PreloginResponse preloginResponse; - - bool isAutoLogin{ false }; - - StandardLoginWindow *standardLoginWindow { nullptr }; - - void tryAutoLogin(); - void normalAuth(); - void samlAuth(); - void fetchConfig(QString username, QString password, QString preloginCookie = "", QString userAuthCookie = ""); - void emitFail(const QString& msg = ""); -}; - -#endif // PORTALAUTHENTICATOR_H diff --git a/GPClient/portalconfigresponse.cpp b/GPClient/portalconfigresponse.cpp deleted file mode 100644 index 45e117d..0000000 --- a/GPClient/portalconfigresponse.cpp +++ /dev/null @@ -1,174 +0,0 @@ -#include -#include - -#include "portalconfigresponse.h" - -QString PortalConfigResponse::xmlUserAuthCookie = "portal-userauthcookie"; -QString PortalConfigResponse::xmlPrelogonUserAuthCookie = "portal-prelogonuserauthcookie"; -QString PortalConfigResponse::xmlGateways = "gateways"; - -PortalConfigResponse::PortalConfigResponse() -{ -} - -PortalConfigResponse::~PortalConfigResponse() -{ -} - -PortalConfigResponse PortalConfigResponse::parse(const QByteArray xml) -{ - LOGI << "Start parsing the portal configuration..."; - - QXmlStreamReader xmlReader(xml); - PortalConfigResponse response; - response.setRawResponse(xml); - - while (!xmlReader.atEnd()) { - xmlReader.readNextStartElement(); - - QString name = xmlReader.name().toString(); - - if (name == xmlUserAuthCookie) { - LOGI << "Start reading " << name; - response.setUserAuthCookie(xmlReader.readElementText()); - } else if (name == xmlPrelogonUserAuthCookie) { - LOGI << "Start reading " << name; - response.setPrelogonUserAuthCookie(xmlReader.readElementText()); - } else if (name == xmlGateways) { - response.setAllGateways(parseGateways(xmlReader)); - } - } - - LOGI << "Finished parsing portal configuration."; - - return response; -} - -const QByteArray PortalConfigResponse::rawResponse() const -{ - return m_rawResponse; -} - -const QString &PortalConfigResponse::username() const -{ - return m_username; -} - -QString PortalConfigResponse::password() const -{ - return m_password; -} - -QList PortalConfigResponse::parseGateways(QXmlStreamReader &xmlReader) -{ - LOGI << "Start parsing the gateways from portal configuration..."; - - QList gateways; - - while (xmlReader.name() != "external"){ - xmlReader.readNext(); - } - - while (xmlReader.name() != "list"){ - xmlReader.readNext(); - } - - while (xmlReader.name() != xmlGateways || !xmlReader.isEndElement()) { - xmlReader.readNext(); - // Parse the gateways -> external -> list -> entry - if (xmlReader.name() == "entry" && xmlReader.isStartElement()) { - GPGateway g; - parseGateway(xmlReader, g); - gateways.append(g); - } - } - - LOGI << "Finished parsing the gateways."; - - return gateways; -} - -void PortalConfigResponse::parseGateway(QXmlStreamReader &reader, GPGateway &gateway) { - LOGI << "Start parsing gateway..."; - - auto finished = false; - while (!finished) { - if (reader.name() == "entry" && reader.isStartElement()) { - auto address = reader.attributes().value("name").toString(); - gateway.setAddress(address); - } else if (reader.name() == "description" && reader.isStartElement()) { // gateway name - gateway.setName(reader.readElementText()); - } else if (reader.name() == "priority-rule" && reader.isStartElement()) { // priority rules - parsePriorityRule(reader, gateway); - } - - auto result = reader.readNext(); - finished = result == QXmlStreamReader::Invalid || (reader.name() == "entry" && reader.isEndElement()); - } -} - -void PortalConfigResponse::parsePriorityRule(QXmlStreamReader &reader, GPGateway &gateway) { - LOGI << "Start parsing priority rule..."; - - QMap priorityRules; - auto finished = false; - - while (!finished) { - // Parse the priority-rule -> entry - if (reader.name() == "entry" && reader.isStartElement()) { - auto ruleName = reader.attributes().value("name").toString(); - // move to the priority value - while (reader.readNextStartElement()) { - if (reader.name() == "priority") { - auto priority = reader.readElementText().toInt(); - priorityRules.insert(ruleName, priority); - break; - } - } - } - auto result = reader.readNext(); - finished = result == QXmlStreamReader::Invalid || (reader.name() == "priority-rule" && reader.isEndElement()); - } - - gateway.setPriorityRules(priorityRules); -} - -QString PortalConfigResponse::userAuthCookie() const -{ - return m_userAuthCookie; -} - -QList PortalConfigResponse::allGateways() const -{ - return m_gateways; -} - -void PortalConfigResponse::setAllGateways(QList gateways) -{ - m_gateways = gateways; -} - -void PortalConfigResponse::setRawResponse(const QByteArray response) -{ - m_rawResponse = response; -} - -void PortalConfigResponse::setUsername(const QString username) -{ - m_username = username; -} - -void PortalConfigResponse::setPassword(const QString password) -{ - m_password = password; -} - -void PortalConfigResponse::setUserAuthCookie(const QString cookie) -{ - m_userAuthCookie = cookie; -} - -void PortalConfigResponse::setPrelogonUserAuthCookie(const QString cookie) -{ - m_prelogonAuthCookie = cookie; -} diff --git a/GPClient/portalconfigresponse.h b/GPClient/portalconfigresponse.h deleted file mode 100644 index bbfda12..0000000 --- a/GPClient/portalconfigresponse.h +++ /dev/null @@ -1,51 +0,0 @@ -#ifndef PORTALCONFIGRESPONSE_H -#define PORTALCONFIGRESPONSE_H - -#include -#include -#include - -#include "gpgateway.h" - -class PortalConfigResponse -{ -public: - PortalConfigResponse(); - ~PortalConfigResponse(); - - static PortalConfigResponse parse(const QByteArray xml); - - const QByteArray rawResponse() const; - const QString &username() const; - QString password() const; - QString userAuthCookie() const; - QList allGateways() const; - void setAllGateways(QList gateways); - - void setUsername(const QString username); - void setPassword(const QString password); - -private: - static QString xmlUserAuthCookie; - static QString xmlPrelogonUserAuthCookie; - static QString xmlGateways; - - QByteArray m_rawResponse; - QString m_username; - QString m_password; - QString m_userAuthCookie; - QString m_prelogonAuthCookie; - - QList m_gateways; - - void setRawResponse(const QByteArray response); - void setUserAuthCookie(const QString cookie); - void setPrelogonUserAuthCookie(const QString cookie); - - static QList parseGateways(QXmlStreamReader &xmlReader); - static void parseGateway(QXmlStreamReader &reader, GPGateway &gateway); - static void parsePriorityRule(QXmlStreamReader &reader, GPGateway &gateway); - -}; - -#endif // PORTALCONFIGRESPONSE_H diff --git a/GPClient/preloginresponse.cpp b/GPClient/preloginresponse.cpp deleted file mode 100644 index 1c378f1..0000000 --- a/GPClient/preloginresponse.cpp +++ /dev/null @@ -1,100 +0,0 @@ -#include -#include -#include - -#include "preloginresponse.h" - -QString PreloginResponse::xmlAuthMessage = "authentication-message"; -QString PreloginResponse::xmlLabelUsername = "username-label"; -QString PreloginResponse::xmlLabelPassword = "password-label"; -QString PreloginResponse::xmlSamlMethod = "saml-auth-method"; -QString PreloginResponse::xmlSamlRequest = "saml-request"; -QString PreloginResponse::xmlRegion = "region"; - -PreloginResponse::PreloginResponse() -{ - add(xmlAuthMessage, ""); - add(xmlLabelUsername, ""); - add(xmlLabelPassword, ""); - add(xmlSamlMethod, ""); - add(xmlSamlRequest, ""); - add(xmlRegion, ""); -} - -PreloginResponse PreloginResponse::parse(const QByteArray& xml) -{ - LOGI << "Start parsing the prelogin response..."; - - QXmlStreamReader xmlReader(xml); - PreloginResponse response; - response.setRawResponse(xml); - - while (!xmlReader.atEnd()) { - xmlReader.readNextStartElement(); - QString name = xmlReader.name().toString(); - if (response.has(name)) { - response.add(name, xmlReader.readElementText()); - } - } - return response; -} - -const QByteArray& PreloginResponse::rawResponse() const -{ - return _rawResponse; -} - -QString PreloginResponse::authMessage() const -{ - return resultMap.value(xmlAuthMessage); -} - -QString PreloginResponse::labelUsername() const -{ - return resultMap.value(xmlLabelUsername); -} - -QString PreloginResponse::labelPassword() const -{ - return resultMap.value(xmlLabelPassword); -} - -QString PreloginResponse::samlMethod() const -{ - return resultMap.value(xmlSamlMethod); -} - -QString PreloginResponse::samlRequest() const -{ - return QByteArray::fromBase64(resultMap.value(xmlSamlRequest).toUtf8()); -} - -QString PreloginResponse::region() const -{ - return resultMap.value(xmlRegion); -} - -bool PreloginResponse::hasSamlAuthFields() const -{ - return !samlMethod().isEmpty() && !samlRequest().isEmpty(); -} - -bool PreloginResponse::hasNormalAuthFields() const -{ - return !labelUsername().isEmpty() && !labelPassword().isEmpty(); -} - -void PreloginResponse::setRawResponse(const QByteArray response) -{ - _rawResponse = response; -} - -bool PreloginResponse::has(const QString name) const -{ - return resultMap.contains(name); -} - -void PreloginResponse::add(const QString name, const QString value) -{ - resultMap.insert(name, value); -} diff --git a/GPClient/preloginresponse.h b/GPClient/preloginresponse.h deleted file mode 100644 index 772a037..0000000 --- a/GPClient/preloginresponse.h +++ /dev/null @@ -1,41 +0,0 @@ -#ifndef PRELOGINRESPONSE_H -#define PRELOGINRESPONSE_H - -#include -#include - -class PreloginResponse -{ -public: - PreloginResponse(); - - static PreloginResponse parse(const QByteArray& xml); - - const QByteArray& rawResponse() const; - QString authMessage() const; - QString labelUsername() const; - QString labelPassword() const; - QString samlMethod() const; - QString samlRequest() const; - QString region() const; - - bool hasSamlAuthFields() const; - bool hasNormalAuthFields() const; - -private: - static QString xmlAuthMessage; - static QString xmlLabelUsername; - static QString xmlLabelPassword; - static QString xmlSamlMethod; - static QString xmlSamlRequest; - static QString xmlRegion; - - QMap resultMap; - QByteArray _rawResponse; - - void setRawResponse(const QByteArray response); - void add(const QString name, const QString value); - bool has(const QString name) const; -}; - -#endif // PRELOGINRESPONSE_H diff --git a/GPClient/radio_selected.png b/GPClient/radio_selected.png deleted file mode 100644 index adc3d69acffcc976480c694dbf61f7df582fcdf4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1219 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=3?wxlRx|@C#^NA%C&rs6b?Si}&H|6fVg?3o zVGw3ym^DWNs97vv5{yg= z1v~%z|M&KAtrm+)$D^6X0>wXir5ta?u$F)OJC%oX+RrUrYeaVN&C7hJyu@uim+g$- z3nDB`13us2Sg9nWTg2S3Ip?dP%tz_>7h|`dN#5%k{JKf}_RpU?bM9Sh29@c% zSKSJJy!Pt3x%&$~PU6>3SbF`cdBxu&VGBNfnwl`VD;b~m1+Y;9cRwg>)WxEP3P{r^i8j{eFcir4>XocKe1}U z?O6BqQ>KcG?ns~CzG(Lvw)f%_0(P<}@_%yK{Nzk)Rfb@dH!F)|SKtyqgU+H>`M@Rn zgqkKKcA0SenRszyMbe!a0xfFt&8NF#j4h|;pV`uAEnLf#XRlP1oUVRN5M4TZjVveiPXO5Lm=dyI` z=qRyFH9x0xP_V`3xoorRv>z2B(<-x*Zh5}Yu8Co``PKHA_uJY)9vXG?vy(+1kaqU%Dpp$)%dh#WPzU2(3EIb@BL{-UG`Y+wAYyH9L6HyMJQo z%O>n}oVc!hs=`g*)hAe9&2)+!m&sP2v0q@VcroweWV;x-&CA8z zUn)!aP5j0`hxxXB$gB@FN$Zb3*I!z6`#;y5eYN58=kGE42Q0m)tDlt~E-t`eUw5kG z?T#<)FR$kxTz>OR-qI9?&^I5>-&cNKwSU^38zBsqu6_Rh{+_q}5ty5M*S0}y4$~2V VBc1k(Pp5;jnWw9t%Q~loCII&DZS(*D diff --git a/GPClient/radio_unselected.png b/GPClient/radio_unselected.png deleted file mode 100644 index 4542f7347df11cdbb83cb8fb46fb500a7653038d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 993 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=3?wxlRx|@C#^NA%C&rs6b?Si}&H|6fVg?3o zVGw3ym^DWNDEK76C&U#<4|^bX=#w(glRhOue!&a^=2h#?-hUw-_vFui8UC`Hx5_#5 zU;qC0rhx1C>4q>fb*1{CquH`q!AzAu{vNUKeVM@8w@YK^zipx-TRGy_pR%Y9H9eFSbHiRct#S~MV>NWu+$y5~@|B@JtK62!Q(MY+&;4FM zKh0c4P|W;UJNLVqGpXs%r5hSlCQX_XbSYC+)%&DN!T*SJMnz#~D;K3b^|2FLFgN}Q zQ;UQZiORl=?6saAh>b(5|^(%v~RS2ck8*N+hx7z;MG^KyGE$o~Q%S4oa zK5|$uH-BCwN5jm&9s6XDOkO_Ge(Dd72JzX?4xEl&>9@CQ!@)NJ_h0?~yz_{IPM%D) z;tb}kzy7XS7=7%A2vb7O&TIR#pUddZT6pv8=_4*jm1?aHdxbdr8E7c4X=Xa>z_u|~ zkcGkY@dK^LRmyBH7r&BupCESqUS_G5ZOB|mb%0KUV%3WdPB>uCue&WI(uge%`>0O&LXZ|vl&|AMwXtHG2%D?mO zHM{XrN9v%|o8|5eYkF_%2Xb6H_LcdUbik$!%5To(9QGGu5L&uK^P!@Is-&G))5BVk zkCsz@^!z`bqFMOztL>b)K(?MEr_=s-9PxE~R5jtq!-SBCl55hNZcjY>tMp+X*Zet- z%fhZ&GM{8mS8%-8)q20z_KbDFNRV Rh)?xxQs4jp diff --git a/GPClient/resources.qrc b/GPClient/resources.qrc deleted file mode 100644 index cfeaa4c..0000000 --- a/GPClient/resources.qrc +++ /dev/null @@ -1,11 +0,0 @@ - - - com.yuezk.qt.gpclient.svg - connected.png - pending.png - not_connected.png - radio_unselected.png - radio_selected.png - settings_icon.png - - diff --git a/GPClient/samlloginwindow.cpp b/GPClient/samlloginwindow.cpp deleted file mode 100644 index d7b8d16..0000000 --- a/GPClient/samlloginwindow.cpp +++ /dev/null @@ -1,136 +0,0 @@ -#include -#include -#include -#include -#include - -#include "samlloginwindow.h" - -SAMLLoginWindow::SAMLLoginWindow(QWidget *parent) - : QDialog(parent) - , webView(new EnhancedWebView(this)) -{ - setWindowTitle("GlobalProtect Login"); - setModal(true); - resize(700, 550); - - QVBoxLayout *verticalLayout = new QVBoxLayout(this); - webView->setUrl(QUrl("about:blank")); - webView->setAttribute(Qt::WA_DeleteOnClose); - verticalLayout->addWidget(webView); - - webView->initialize(); - connect(webView, &EnhancedWebView::responseReceived, this, &SAMLLoginWindow::onResponseReceived); - connect(webView, &EnhancedWebView::loadFinished, this, &SAMLLoginWindow::onLoadFinished); - - // Show the login window automatically when exceeds the MAX_WAIT_TIME - QTimer::singleShot(MAX_WAIT_TIME, this, [this]() { - if (failed) { - return; - } - LOGI << "MAX_WAIT_TIME exceeded, display the login window."; - this->show(); - }); -} - -void SAMLLoginWindow::closeEvent(QCloseEvent *event) -{ - event->accept(); - reject(); -} - -void SAMLLoginWindow::login(const QString samlMethod, const QString samlRequest, const QString preloginUrl) -{ - webView->page()->profile()->cookieStore()->deleteSessionCookies(); - - if (samlMethod == "POST") { - webView->setHtml(samlRequest, preloginUrl); - } else if (samlMethod == "REDIRECT") { - LOGI << "Redirect to " << samlRequest; - webView->load(samlRequest); - } else { - LOGE << "Unknown saml-auth-method expected POST or REDIRECT, got " << samlMethod; - failed = true; - emit fail("ERR001", "Unknown saml-auth-method, got " + samlMethod); - } -} - -void SAMLLoginWindow::onResponseReceived(QJsonObject params) -{ - const auto type = params.value("type").toString(); - // Skip non-document response - if (type != "Document") { - return; - } - - auto response = params.value("response").toObject(); - auto headers = response.value("headers").toObject(); - - LOGI << "Trying to receive authentication cookie from " << response.value("url").toString(); - - const auto username = headers.value("saml-username").toString(); - const auto preloginCookie = headers.value("prelogin-cookie").toString(); - const auto userAuthCookie = headers.value("portal-userauthcookie").toString(); - - this->checkSamlResult(username, preloginCookie, userAuthCookie); -} - -void SAMLLoginWindow::checkSamlResult(QString username, QString preloginCookie, QString userAuthCookie) -{ - LOGI << "Checking the authentication result..."; - - if (!username.isEmpty()) { - samlResult.insert("username", username); - } - - if (!preloginCookie.isEmpty()) { - samlResult.insert("preloginCookie", preloginCookie); - } - - if (!userAuthCookie.isEmpty()) { - samlResult.insert("userAuthCookie", userAuthCookie); - } - - // Check the SAML result - if (samlResult.contains("username") - && (samlResult.contains("preloginCookie") || samlResult.contains("userAuthCookie"))) { - LOGI << "Got the SAML authentication information successfully. " - << "username: " << samlResult.value("username") - << ", preloginCookie: " << samlResult.value("preloginCookie") - << ", userAuthCookie: " << samlResult.value("userAuthCookie"); - - emit success(samlResult); - accept(); - } -} - -void SAMLLoginWindow::onLoadFinished() -{ - LOGI << "Load finished " << webView->page()->url().toString(); - webView->page()->toHtml([this] (const QString &html) { this->handleHtml(html); }); -} - -void SAMLLoginWindow::handleHtml(const QString &html) -{ - // try to check the html body and extract from there - const auto samlAuthStatus = parseTag("saml-auth-status", html); - - if (samlAuthStatus == "1") { - const auto preloginCookie = parseTag("prelogin-cookie", html); - const auto username = parseTag("saml-username", html); - const auto userAuthCookie = parseTag("portal-userauthcookie", html); - - checkSamlResult(username, preloginCookie, userAuthCookie); - } else if (samlAuthStatus == "-1") { - LOGI << "SAML authentication failed..."; - failed = true; - emit fail("ERR002", "Authentication failed, please try again."); - } else { - show(); - } -} - -QString SAMLLoginWindow::parseTag(const QString &tag, const QString &html) { - const QRegularExpression expression(QString("<%1>(.*)").arg(tag)); - return expression.match(html).captured(1); -} diff --git a/GPClient/samlloginwindow.h b/GPClient/samlloginwindow.h deleted file mode 100644 index 805368a..0000000 --- a/GPClient/samlloginwindow.h +++ /dev/null @@ -1,41 +0,0 @@ -#ifndef SAMLLOGINWINDOW_H -#define SAMLLOGINWINDOW_H - -#include -#include -#include - -#include "enhancedwebview.h" - -class SAMLLoginWindow : public QDialog -{ - Q_OBJECT - -public: - explicit SAMLLoginWindow(QWidget *parent = nullptr); - - void login(const QString samlMethod, const QString samlRequest, const QString preloginUrl); - -signals: - void success(QMap samlResult); - void fail(const QString code, const QString msg); - -private slots: - void onResponseReceived(QJsonObject params); - void onLoadFinished(); - void checkSamlResult(QString username, QString preloginCookie, QString userAuthCookie); - -private: - static const auto MAX_WAIT_TIME { 10 * 1000 }; - - bool failed { false }; - EnhancedWebView *webView { nullptr }; - QMap samlResult; - - void closeEvent(QCloseEvent *event); - void handleHtml(const QString &html); - - static QString parseTag(const QString &tag, const QString &html); -}; - -#endif // SAMLLOGINWINDOW_H diff --git a/GPClient/settings_icon.png b/GPClient/settings_icon.png deleted file mode 100644 index 619a88257b31ced6e396c419e7fd49e5a40de3e0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1104 zcmV-W1h4yvP)AP2o=5~$5<;-)!iq#J8VzDmR1ndPU}a-d ziECJB#VryGDG7qO#5JfUE>YEl)^G9sBh%^3oH=vOX}jo~e4GBK-+P`pGjq=Q=0LA{ z)uYv?yE~vz5ilAk0KNflfWIz`h5=Q;HlUvi1D_@VM}TKQHV`eqWuVli;Yi@Q&*Uwz z#->3|gMbr2JDtJ8Vx|q#VZf6JgKNN48z#U+I$p#iF~`(&2=G8*@&~9vu6|mL20kc! zf^o*c50DNwXc&G5%2W8{0e3ZgvcNeYKNVF@1Asf4re2h5dTr6n9|gA(SVn?bAp_h< z3FnPuQ9`&p;GIV9vmtPOfJ44EJ5;7q4dA?}NeH-5Gw+BHxTAqKuT-W5z-S?~yp4Stip z5$0QgHbnTr03QLz4UNVi&q*fY_fLlTqx3#$!@)ok=C@2O;gH)c z3`E-zkaI z51E`>9d*zoz5$a>;QX2ZTMwgAU=EQ^Hw;EJ40GxPWT{W*ER(fjsR9?lrO)FoiRM zEJErUw&Cv6xiW16mm0Fn0bCZ?=>RSjWThj(TAxP-`y1xq1^lmI4P&Ur#n6#IZyWH? za{g+hSgq6n+>GGWh)&t`Xa{BoO@bpBR~0^nyeL)xbusP2)D-f!UBG_LJaOs^>{B(G zOXn-|2iP2FR~phTE-GU@ZH{CcipMO2><+wt_zElzX*WXCczT>Eu*V|g0)-dl$jB>rTHFmm@iamOZNj2m>4_!4IN%p@I`IOsD{*p2wpYFC0{sIn Wpd^bsetupUi(this); -} - -SettingsDialog::~SettingsDialog() -{ - delete ui; -} - -void SettingsDialog::setExtraArgs(QString extraArgs) -{ - ui->extraArgsInput->setPlainText(extraArgs); -} - -QString SettingsDialog::extraArgs() -{ - return ui->extraArgsInput->toPlainText().trimmed(); -} - -void SettingsDialog::setClientos(QString clientos) -{ - ui->clientosInput->setText(clientos); -} - -QString SettingsDialog::clientos() -{ - return ui->clientosInput->text(); -} - -void SettingsDialog::setOsVersion(QString osVersion) { - ui->osVersionInput->setText(osVersion); -} - -QString SettingsDialog::osVersion() { - return ui->osVersionInput->text(); -} diff --git a/GPClient/settingsdialog.h b/GPClient/settingsdialog.h deleted file mode 100644 index ab2a607..0000000 --- a/GPClient/settingsdialog.h +++ /dev/null @@ -1,31 +0,0 @@ -#ifndef SETTINGSDIALOG_H -#define SETTINGSDIALOG_H - -#include - -namespace Ui { -class SettingsDialog; -} - -class SettingsDialog : public QDialog -{ - Q_OBJECT - -public: - explicit SettingsDialog(QWidget *parent = nullptr); - ~SettingsDialog(); - - void setExtraArgs(QString extraArgs); - QString extraArgs(); - - void setClientos(QString clientos); - QString clientos(); - - void setOsVersion(QString osVersion); - QString osVersion(); - -private: - Ui::SettingsDialog *ui; -}; - -#endif // SETTINGSDIALOG_H diff --git a/GPClient/settingsdialog.ui b/GPClient/settingsdialog.ui deleted file mode 100644 index ba27742..0000000 --- a/GPClient/settingsdialog.ui +++ /dev/null @@ -1,117 +0,0 @@ - - - SettingsDialog - - - - 0 - 0 - 488 - 220 - - - - - 0 - 0 - - - - Settings - - - - :/images/connected.png:/images/connected.png - - - - - - Custom Parameters: - - - - - - - true - - - The configuration has been moved to "/etc/gpservice/gp.conf" - - - - - - - clientos: - - - - - - - e.g., Windows - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - - - os-version: - - - - - - - - - - - buttonBox - accepted() - SettingsDialog - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttonBox - rejected() - SettingsDialog - reject() - - - 316 - 260 - - - 286 - 274 - - - - - diff --git a/GPClient/standardloginwindow.cpp b/GPClient/standardloginwindow.cpp deleted file mode 100644 index 7964d1c..0000000 --- a/GPClient/standardloginwindow.cpp +++ /dev/null @@ -1,60 +0,0 @@ -#include - -#include "standardloginwindow.h" -#include "ui_standardloginwindow.h" -#include "gphelper.h" - -using namespace gpclient::helper; - -StandardLoginWindow::StandardLoginWindow(const QString &portalAddress, const QString &labelUsername, - const QString &labelPassword, const QString &authMessage) : - QDialog(nullptr), - ui(new Ui::StandardLoginWindow) { - ui->setupUi(this); - ui->portalAddress->setText(portalAddress); - ui->username->setPlaceholderText(labelUsername); - ui->password->setPlaceholderText(labelPassword); - ui->authMessage->setText(authMessage); - - autocomplete(); - - setWindowTitle("GlobalProtect Login"); - setFixedSize(width(), height()); - setModal(true); -} - -void StandardLoginWindow::autocomplete() { - QString username, password; - settings::secureGet("username", username); - settings::secureGet("password", password); - - if (!username.isEmpty() && !password.isEmpty()) { - ui->username->setText(username); - ui->password->setText(password); - } -} - -void StandardLoginWindow::setProcessing(bool isProcessing) { - ui->username->setReadOnly(isProcessing); - ui->password->setReadOnly(isProcessing); - ui->loginButton->setDisabled(isProcessing); -} - -void StandardLoginWindow::on_loginButton_clicked() { - const QString username = ui->username->text().trimmed(); - const QString password = ui->password->text().trimmed(); - - if (username.isEmpty() || password.isEmpty()) { - return; - } - - settings::secureSave("username", username); - settings::secureSave("password", password); - - emit performLogin(username, password); -} - -void StandardLoginWindow::closeEvent(QCloseEvent *event) { - event->accept(); - reject(); -} diff --git a/GPClient/standardloginwindow.h b/GPClient/standardloginwindow.h deleted file mode 100644 index 894e7e3..0000000 --- a/GPClient/standardloginwindow.h +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef STANDARDLOGINWINDOW_H -#define STANDARDLOGINWINDOW_H - -#include - -namespace Ui { - class StandardLoginWindow; -} - -class StandardLoginWindow : public QDialog { -Q_OBJECT - -public: - explicit StandardLoginWindow(const QString &portalAddress, const QString &labelUsername, - const QString &labelPassword, const QString &authMessage); - - void setProcessing(bool isProcessing); - -private slots: - - void on_loginButton_clicked(); - -signals: - - void performLogin(QString username, QString password); - -private: - Ui::StandardLoginWindow *ui; - - void closeEvent(QCloseEvent *event); - void autocomplete(); -}; - -#endif // STANDARDLOGINWINDOW_H diff --git a/GPClient/standardloginwindow.ui b/GPClient/standardloginwindow.ui deleted file mode 100644 index 1190534..0000000 --- a/GPClient/standardloginwindow.ui +++ /dev/null @@ -1,148 +0,0 @@ - - - StandardLoginWindow - - - - 0 - 0 - 255 - 269 - - - - - 0 - 0 - - - - ArrowCursor - - - Login - - - true - - - - - - - - - - - 20 - - - - Login - - - Qt::AlignCenter - - - - - - - true - - - - 0 - 2 - - - - Please enter the login credentials - - - Qt::AlignCenter - - - - - - - - - 0 - - - 6 - - - - - - 0 - 0 - - - - Portal: - - - 0 - - - - - - - - 0 - 0 - - - - vpn.example.com - - - - - - - - - - - Username - - - - - - - - - - QLineEdit::Password - - - Password - - - false - - - - - - - Login - - - - - - - - - - - - diff --git a/GPClient/vpn.h b/GPClient/vpn.h deleted file mode 100644 index b389edd..0000000 --- a/GPClient/vpn.h +++ /dev/null @@ -1,24 +0,0 @@ -#ifndef VPN_H -#define VPN_H -#include -#include - -class IVpn -{ -public: - virtual ~IVpn() = default; - - virtual void connect(const QString &preferredServer, const QList &servers, const QString &username, const QString &passwd) = 0; - virtual void disconnect() = 0; - virtual int status() = 0; - -// signals: // SIGNALS -// virtual void connected(); -// virtual void disconnected(); -// virtual void error(const QString &errorMessage); -// virtual void logAvailable(const QString &log); -}; - -Q_DECLARE_INTERFACE(IVpn, "IVpn") // define this out of namespace scope - -#endif diff --git a/GPClient/vpn_dbus.cpp b/GPClient/vpn_dbus.cpp deleted file mode 100644 index 937d15d..0000000 --- a/GPClient/vpn_dbus.cpp +++ /dev/null @@ -1,13 +0,0 @@ -#include "vpn_dbus.h" - -void VpnDbus::connect(const QString &preferredServer, const QList &servers, const QString &username, const QString &passwd) { - inner->connect(preferredServer, username, passwd); -} - -void VpnDbus::disconnect() { - inner->disconnect(); -} - -int VpnDbus::status() { - return inner->status(); -} diff --git a/GPClient/vpn_dbus.h b/GPClient/vpn_dbus.h deleted file mode 100644 index 107e3d8..0000000 --- a/GPClient/vpn_dbus.h +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef VPN_DBUS_H -#define VPN_DBUS_H -#include "vpn.h" -#include "gpserviceinterface.h" - -class VpnDbus : public QObject, public IVpn -{ - Q_OBJECT - Q_INTERFACES(IVpn) - -private: - com::yuezk::qt::GPService *inner; - -public: - VpnDbus(QObject *parent) : QObject(parent) { - inner = new com::yuezk::qt::GPService("com.yuezk.qt.GPService", "/", QDBusConnection::systemBus(), this); - QObject::connect(inner, &com::yuezk::qt::GPService::connected, this, &VpnDbus::connected); - QObject::connect(inner, &com::yuezk::qt::GPService::disconnected, this, &VpnDbus::disconnected); - QObject::connect(inner, &com::yuezk::qt::GPService::error, this, &VpnDbus::error); - QObject::connect(inner, &com::yuezk::qt::GPService::logAvailable, this, &VpnDbus::logAvailable); - } - - void connect(const QString &preferredServer, const QList &servers, const QString &username, const QString &passwd); - void disconnect(); - int status(); - -signals: // SIGNALS - void connected(); - void disconnected(); - void error(QString errorMessage); - void logAvailable(QString log); -}; -#endif diff --git a/GPClient/vpn_json.cpp b/GPClient/vpn_json.cpp deleted file mode 100644 index dcfe213..0000000 --- a/GPClient/vpn_json.cpp +++ /dev/null @@ -1,24 +0,0 @@ -#include "vpn_json.h" -#include -#include -#include -#include - -void VpnJson::connect(const QString &preferredServer, const QList &servers, const QString &username, const QString &passwd) { - QJsonArray sl; - for (const QString &srv : servers) { - sl.push_back(QJsonValue(srv)); - } - QJsonObject j; - j["server"] = preferredServer; - j["availableServers"] = sl; - j["cookie"] = passwd; - QTextStream(stdout) << QJsonDocument(j).toJson(QJsonDocument::Compact) << "\n"; - emit connected(); -} - -void VpnJson::disconnect() { /* nop */ } - -int VpnJson::status() { - return 4; // disconnected -} diff --git a/GPClient/vpn_json.h b/GPClient/vpn_json.h deleted file mode 100644 index 8fbbfe0..0000000 --- a/GPClient/vpn_json.h +++ /dev/null @@ -1,23 +0,0 @@ -#ifndef VPN_JSON_H -#define VPN_JSON_H -#include "vpn.h" - -class VpnJson : public QObject, public IVpn -{ - Q_OBJECT - Q_INTERFACES(IVpn) - -public: - VpnJson(QObject *parent) : QObject(parent) {} - - void connect(const QString &preferredServer, const QList &servers, const QString &username, const QString &passwd); - void disconnect(); - int status(); - -signals: // SIGNALS - void connected(); - void disconnected(); - void error(const QString &errorMessage); - void logAvailable(const QString &log); -}; -#endif diff --git a/GPService/CMakeLists.txt b/GPService/CMakeLists.txt deleted file mode 100644 index 7f20655..0000000 --- a/GPService/CMakeLists.txt +++ /dev/null @@ -1,83 +0,0 @@ -include("${CMAKE_SOURCE_DIR}/cmake/Add3rdParty.cmake") - -project(GPService) - -set(gpservice_GENERATED_SOURCES) - -execute_process(COMMAND logname OUTPUT_VARIABLE CMAKE_LOGNAME) -string(STRIP "${CMAKE_LOGNAME}" CMAKE_LOGNAME) - -message(STATUS "CMAKE_LOGNAME: ${CMAKE_LOGNAME}") - -configure_file(dbus/com.yuezk.qt.GPService.conf.in dbus/com.yuezk.qt.GPService.conf) -configure_file(dbus/com.yuezk.qt.GPService.service.in dbus/com.yuezk.qt.GPService.service) -configure_file(systemd/gpservice.service.in systemd/gpservice.service) - -# generate the dbus xml definition -qt5_generate_dbus_interface( - gpservice.h - ${CMAKE_BINARY_DIR}/com.yuezk.qt.GPService.xml -) - -# generate dbus adaptor -qt5_add_dbus_adaptor( - gpservice_GENERATED_SOURCES - ${CMAKE_BINARY_DIR}/com.yuezk.qt.GPService.xml - gpservice.h - GPService -) - -add_executable(gpservice - gpservice.h - gpservice.cpp - main.cpp - ${gpservice_GENERATED_SOURCES} -) - -add_3rdparty( - SingleApplication - GIT_REPOSITORY https://github.com/itay-grudev/SingleApplication.git - GIT_TAG v3.3.0 - CMAKE_ARGS - -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} - -DCMAKE_CXX_FLAGS_RELEASE=${CMAKE_CXX_FLAGS_RELEASE} - -DCMAKE_FIND_ROOT_PATH=${CMAKE_FIND_ROOT_PATH} - -DCMAKE_PREFIX_PATH=$ENV{CMAKE_PREFIX_PATH} - -DQAPPLICATION_CLASS=QCoreApplication -) - -ExternalProject_Get_Property(SingleApplication-${PROJECT_NAME} SOURCE_DIR BINARY_DIR) - -set(SingleApplication_INCLUDE_DIR ${SOURCE_DIR}) -set(SingleApplication_LIBRARY ${BINARY_DIR}/libSingleApplication.a) - -add_dependencies(gpservice SingleApplication-${PROJECT_NAME}) - -target_include_directories(gpservice PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR} - ${CMAKE_CURRENT_BINARY_DIR} - ${SingleApplication_INCLUDE_DIR} -) - -target_link_libraries(gpservice - ${SingleApplication_LIBRARY} - Qt5::Core - Qt5::Network - Qt5::DBus - QtSignals - inih -) - -target_compile_definitions(gpservice PUBLIC QAPPLICATION_CLASS=QCoreApplication) - -install(TARGETS gpservice DESTINATION bin) -install(FILES "${CMAKE_CURRENT_BINARY_DIR}/dbus/com.yuezk.qt.GPService.conf" DESTINATION share/dbus-1/system.d ) -install(FILES "${CMAKE_CURRENT_BINARY_DIR}/dbus/com.yuezk.qt.GPService.service" DESTINATION share/dbus-1/system-services) -install(FILES "gp.conf" DESTINATION /etc/gpservice) - -if("$ENV{DEBIAN_PACKAGE}") - # Install the systemd unit files to /lib/systemd/system for debian package - install(FILES "${CMAKE_CURRENT_BINARY_DIR}/systemd/gpservice.service" DESTINATION /lib/systemd/system) -else() - install(FILES "${CMAKE_CURRENT_BINARY_DIR}/systemd/gpservice.service" DESTINATION lib/systemd/system) -endif() diff --git a/GPService/dbus/com.yuezk.qt.GPService.conf.in b/GPService/dbus/com.yuezk.qt.GPService.conf.in deleted file mode 100644 index 41069f1..0000000 --- a/GPService/dbus/com.yuezk.qt.GPService.conf.in +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - diff --git a/GPService/dbus/com.yuezk.qt.GPService.service.in b/GPService/dbus/com.yuezk.qt.GPService.service.in deleted file mode 100644 index ac21bea..0000000 --- a/GPService/dbus/com.yuezk.qt.GPService.service.in +++ /dev/null @@ -1,5 +0,0 @@ -[D-BUS Service] -Name=com.yuezk.qt.GPService -Exec=@CMAKE_INSTALL_PREFIX@/bin/gpservice -User=root -SystemdService=gpservice.service diff --git a/GPService/gp.conf b/GPService/gp.conf deleted file mode 100644 index 7d7f824..0000000 --- a/GPService/gp.conf +++ /dev/null @@ -1,17 +0,0 @@ -# Configuration file for GlobalProtect-openconnect -# -# Description: -# -# Each section is a VPN gateway address, and [*] is a special section that defines the default configuration. -# See https://github.com/yuezk/GlobalProtect-openconnect/wiki/Configuration for more details. -# -# Example: -# -# [*] -# openconnect-args= -# -# [vpn1.company.com] -# openconnect-args=--script=/path/to/vpnc-script - -[*] -openconnect-args= diff --git a/GPService/gpservice.cpp b/GPService/gpservice.cpp deleted file mode 100644 index acfb646..0000000 --- a/GPService/gpservice.cpp +++ /dev/null @@ -1,229 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#include "INIReader.h" -#include "gpservice.h" -#include "gpserviceadaptor.h" - -GPService::GPService(QObject *parent) - : QObject(parent) - , openconnect(new QProcess) -{ - // Register the DBus service - new GPServiceAdaptor(this); - QDBusConnection dbus = QDBusConnection::systemBus(); - dbus.registerObject("/", this); - dbus.registerService("com.yuezk.qt.GPService"); - - // Setup the openconnect process - QObject::connect(openconnect, &QProcess::started, this, &GPService::onProcessStarted); - QObject::connect(openconnect, &QProcess::errorOccurred, this, &GPService::onProcessError); - QObject::connect(openconnect, &QProcess::readyReadStandardOutput, this, &GPService::onProcessStdout); - QObject::connect(openconnect, &QProcess::readyReadStandardError, this, &GPService::onProcessStderr); - QObject::connect(openconnect, QOverload::of(&QProcess::finished), this, &GPService::onProcessFinished); -} - -GPService::~GPService() -{ - delete openconnect; -} - -QString GPService::findBinary() -{ - for (auto& binaryPath : binaryPaths) { - if (QFileInfo::exists(binaryPath)) { - return binaryPath; - } - } - return nullptr; -} - -QString GPService::extraOpenconnectArgs(const QString &gateway) -{ - INIReader reader("/etc/gpservice/gp.conf"); - - if (reader.ParseError() < 0) { - return ""; - } - - std::string defaultArgs = reader.Get("*", "openconnect-args", ""); - std::string extraArgs = reader.Get(gateway.toStdString(), "openconnect-args", defaultArgs); - - return QString::fromStdString(extraArgs); -} - -/* Port from https://github.com/qt/qtbase/blob/11d1dcc6e263c5059f34b44d531c9ccdf7c0b1d6/src/corelib/io/qprocess.cpp#L2115 */ -QStringList GPService::splitCommand(const QString &command) -{ - QStringList args; - QString tmp; - int quoteCount = 0; - bool inQuote = false; - - // handle quoting. tokens can be surrounded by double quotes - // "hello world". three consecutive double quotes represent - // the quote character itself. - for (int i = 0; i < command.size(); ++i) { - if (command.at(i) == QLatin1Char('"')) { - ++quoteCount; - if (quoteCount == 3) { - // third consecutive quote - quoteCount = 0; - tmp += command.at(i); - } - continue; - } - if (quoteCount) { - if (quoteCount == 1) - inQuote = !inQuote; - quoteCount = 0; - } - if (!inQuote && command.at(i).isSpace()) { - if (!tmp.isEmpty()) { - args += tmp; - tmp.clear(); - } - } else { - tmp += command.at(i); - } - } - if (!tmp.isEmpty()) - args += tmp; - - return args; -} - -void GPService::quit() -{ - if (openconnect->state() == QProcess::NotRunning) { - exit(0); - } else { - aboutToQuit = true; - openconnect->terminate(); - } -} - -void GPService::connect(QString server, QString username, QString passwd) -{ - if (vpnStatus != GPService::VpnNotConnected) { - log("VPN status is: " + QVariant::fromValue(vpnStatus).toString()); - return; - } - - QString bin = findBinary(); - if (bin == nullptr) { - log("Could not find openconnect binary, make sure openconnect is installed, exiting."); - emit error("The OpenConect CLI was not found, make sure it has been installed!"); - return; - } - - if (!isValidVersion(bin)) { - return; - } - - const QString extraArgs = extraOpenconnectArgs(server); - log(QString("Got extra OpenConnect args for server: %1, %2").arg(server, extraArgs.isEmpty() ? "" : extraArgs)); - - QStringList args; - args << QCoreApplication::arguments().mid(1) - << "--protocol=gp" - << splitCommand(extraArgs) - << "-u" << username - << "--cookie-on-stdin" - << server; - - log("Start process with arguments: " + args.join(", ")); - - openconnect->start(bin, args); - openconnect->write((passwd + "\n").toUtf8()); -} - -bool GPService::isValidVersion(QString &bin) { - QProcess p; - p.start(bin, QStringList("--version")); - p.waitForFinished(); - QString output = p.readAllStandardError() + p.readAllStandardOutput(); - - QRegularExpression re("v(\\d+).*?(\\s|\\n)"); - QRegularExpressionMatch match = re.match(output); - - if (match.hasMatch()) { - log("Output of `openconnect --version`: " + output); - - QString fullVersion = match.captured(0); - QString majorVersion = match.captured(1); - - if (majorVersion.toInt() < 8) { - emit error("The OpenConnect version must greater than v8.0.0, got " + fullVersion); - return false; - } - } else { - log("Failed to parse the OpenConnect version from " + output); - } - - return true; -} - -void GPService::disconnect() -{ - if (openconnect->state() != QProcess::NotRunning) { - vpnStatus = GPService::VpnDisconnecting; - openconnect->terminate(); - } -} - -int GPService::status() -{ - return vpnStatus; -} - -void GPService::onProcessStarted() -{ - log("Openconnect started successfully, PID=" + QString::number(openconnect->processId())); - vpnStatus = GPService::VpnConnecting; -} - -void GPService::onProcessError(QProcess::ProcessError error) -{ - log("Error occurred: " + QVariant::fromValue(error).toString()); - vpnStatus = GPService::VpnNotConnected; - emit disconnected(); -} - -void GPService::onProcessStdout() -{ - QString output = openconnect->readAllStandardOutput(); - - log(output); - if (output.indexOf("Connected as") >= 0 || - output.indexOf("Configured as") >= 0 || - output.indexOf("Configurado como") >= 0) { - vpnStatus = GPService::VpnConnected; - emit connected(); - } -} - -void GPService::onProcessStderr() -{ - log(openconnect->readAllStandardError()); -} - -void GPService::onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) -{ - log("Openconnect process exited with code " + QString::number(exitCode) + " and exit status " + QVariant::fromValue(exitStatus).toString()); - vpnStatus = GPService::VpnNotConnected; - emit disconnected(); - - if (aboutToQuit) { - exit(0); - }; -} - -void GPService::log(QString msg) -{ - emit logAvailable(msg); -} diff --git a/GPService/gpservice.h b/GPService/gpservice.h deleted file mode 100644 index 22e9fd4..0000000 --- a/GPService/gpservice.h +++ /dev/null @@ -1,62 +0,0 @@ -#ifndef GLOBALPROTECTSERVICE_H -#define GLOBALPROTECTSERVICE_H - -#include -#include - -static QList binaryPaths = QList() << - "/usr/local/bin/openconnect" << - "/usr/local/sbin/openconnect" << - "/usr/bin/openconnect" << - "/usr/sbin/openconnect" << - "/opt/bin/openconnect" << - "/opt/sbin/openconnect"; - -class GPService : public QObject -{ - Q_OBJECT - Q_CLASSINFO("D-Bus Interface", "com.yuezk.qt.GPService") -public: - explicit GPService(QObject *parent = nullptr); - ~GPService(); - - void quit(); - - enum VpnStatus { - VpnNotConnected, - VpnConnecting, - VpnConnected, - VpnDisconnecting, - }; - -signals: - void connected(); - void disconnected(); - void error(QString errorMessage); - void logAvailable(QString log); - -public slots: - void connect(QString server, QString username, QString passwd); - void disconnect(); - int status(); - -private slots: - void onProcessStarted(); - void onProcessError(QProcess::ProcessError error); - void onProcessStdout(); - void onProcessStderr(); - void onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus); - -private: - QProcess *openconnect; - bool aboutToQuit = false; - int vpnStatus = GPService::VpnNotConnected; - - void log(QString msg); - bool isValidVersion(QString &bin); - static QString findBinary(); - static QString extraOpenconnectArgs(const QString &gateway); - static QStringList splitCommand(const QString &command); -}; - -#endif // GLOBALPROTECTSERVICE_H diff --git a/GPService/main.cpp b/GPService/main.cpp deleted file mode 100644 index 06f8743..0000000 --- a/GPService/main.cpp +++ /dev/null @@ -1,27 +0,0 @@ -#include - -#include "gpservice.h" -#include "singleapplication.h" -#include "sigwatch.h" - -int main(int argc, char *argv[]) -{ - SingleApplication app(argc, argv); - - if (!QDBusConnection::systemBus().isConnected()) { - qWarning("Cannot connect to the D-Bus session bus.\n" - "Please check your system settings and try again.\n"); - return 1; - } - - GPService service; - - UnixSignalWatcher sigwatch; - sigwatch.watchForSignal(SIGINT); - sigwatch.watchForSignal(SIGTERM); - sigwatch.watchForSignal(SIGQUIT); - sigwatch.watchForSignal(SIGHUP); - QObject::connect(&sigwatch, &UnixSignalWatcher::unixSignal, &service, &GPService::quit); - - return app.exec(); -} diff --git a/GPService/systemd/gpservice.service.in b/GPService/systemd/gpservice.service.in deleted file mode 100644 index 29beb08..0000000 --- a/GPService/systemd/gpservice.service.in +++ /dev/null @@ -1,11 +0,0 @@ -[Unit] -Description=GlobalProtect openconnect DBus service - -[Service] -Environment="LANG=en_US.utf8" -Type=dbus -BusName=com.yuezk.qt.GPService -ExecStart=@CMAKE_INSTALL_PREFIX@/bin/gpservice - -[Install] -WantedBy=multi-user.target diff --git a/README.md b/README.md index c4d75f1..7911b31 100644 --- a/README.md +++ b/README.md @@ -77,9 +77,9 @@ sudo dnf install globalprotect-openconnect - openSUSE Leap - ```sh + ```sh sudo zypper ar https://download.opensuse.org/repositories/home:/yuezk/15.4/home:yuezk.repo - + sudo zypper ref sudo zypper install globalprotect-openconnect ``` diff --git a/VERSION b/VERSION deleted file mode 100644 index 5596554..0000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.4.9 \ No newline at end of file diff --git a/apps/gpauth/Cargo.toml b/apps/gpauth/Cargo.toml new file mode 100644 index 0000000..f429259 --- /dev/null +++ b/apps/gpauth/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "gpauth" +version.workspace = true +edition.workspace = true +license.workspace = true + +[build-dependencies] +tauri-build = { version = "1.5", features = [] } + +[dependencies] +gpapi = { path = "../../crates/gpapi", features = ["tauri"] } +anyhow.workspace = true +clap.workspace = true +env_logger.workspace = true +log.workspace = true +regex.workspace = true +serde_json.workspace = true +tokio.workspace = true +tokio-util.workspace = true +tempfile.workspace = true +webkit2gtk = "0.18.2" +tauri = { workspace = true, features = ["http-all"] } +compile-time.workspace = true diff --git a/apps/gpauth/build.rs b/apps/gpauth/build.rs new file mode 100644 index 0000000..795b9b7 --- /dev/null +++ b/apps/gpauth/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/apps/gpauth/icons/128x128.png b/apps/gpauth/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..6be5e50e9b9ae84d9e2ee433f32ef446495eaf3b GIT binary patch literal 3512 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 literal 0 HcmV?d00001 diff --git a/apps/gpauth/icons/128x128@2x.png b/apps/gpauth/icons/128x128@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..e81becee571e96f76aa5667f9324c05e5e7a4479 GIT binary patch literal 7012 zcmbVRhd10$wEyl}tP&+^)YVI(cM?|boe*`EAflJ(td=N=)q)^ML`czsM6^|+Bsw9{ zRxcr}zQo#ne((JUZ_b&yGjs0DnR90D=ibkqR5KIZYm{u1003Om*VD290MJzz1VG8I zghNo3$CaQ6(7P8508|YBRS-~E%=({7u!XJ$P&2~u=V}1)R5w-!fO-@a-h~tZ*v|E} z)UConyDt}l7;UoqkF36Q(znu2&;PA10!d*~p4ENpMbz?r+@PQ{MTUb1|7*T6z)FB~ zil2(zBtyMbF>;>;YG>)$qf`!S?sVx|uX~h;#^2)qS-lr5`eB=xj`VYjS8X{eYvqSCp!MVQ+Zp)ah!BOx=<<)3_%H{42A-g}l-uWe_bd zKmuE<1$6Cm4{Ur*DPRCoVkX)`R-k#@gC0(4##3?N&+rs2dc29|tL>p|VuZrAb9JK& zu{fyJ_ck5GVdO`1s(8Q(hzs^@I>vkbt=CxD`%fZW@OrB7f}n7S zw;MjWo)({rDJ~hK-aI$VGS)_z6L!~E>Sw6VryiT=rA^<5<)LCh@l9Q9guNI_1-`wRLpA_?^qeI@{^Zz{+lxCXjoOEdxXE6j- z-}9&QGt)!@Lv$n&M0F*?Hb^el0wLG3ZEh`FC7fc?dC$UOXV;wR?D<@Fx%}@lCaE@K zIe00?Dp@Oh{qg!N38;Yn{)LzJuvpv1zn$1R(Led#p|BoLjY%v((9Ybm z*H%8*p0=q|^Sip^4d*N28NWotn@mYF!A9x=%ax4iXabcaAT^36kx<~Xx_9Z zmX)Zbg@R;9>VW8w!AtFGN20whdPb6jV6zmUw`CA5Y~Jtt{stZLXe@PlM@=iR@?l%lMcTv-0ZzU_U#FCgjGl9SWhR#KYD8+^q?uLyD zO|^I%UB9q-$qloS&)ueZ-L=kPvH{M2=gZgt5NnQWGVW{GIcM9AZ-3@9r3p02?cOQ! z6<-Ax;vK=O(lb6SU&z$FE|NJ7tIQ2V>$uunOUI1U9{mf5g#oJ*fnO^A5o2jQ|85>b zxiFGScj!nQE6RN5JEjpG8HtPtYK%QTar{@da0B~8Gioh}Bu(t?6YSVbRMB;ezkU$dH2D9WD2x=-fhMo+Xrmz_NhjTC>f*Kw4P zCFIf?MYz_(N*>U}tV$}LObr)ZQ6gOh3yM*;Xowm7?{w(iu=5vV?>{(BC8}Eqv&Hmve6M6KY z(yc~_FL9R9AiV<_N~x_e=q`H=P6=SraZcXHy__lEyWKbCwW+zLmR*g;T+5bQuWmnW z>&^mpczmZLymWbQ(`LBo>Awvj&S+_>^0BGOi>j^1<;88Z|(NUz;t&t6tm)8}ZfC3K(_uHgh_ih($^E!prj$VF1Wn zVsVh@d4g6UzEwgH7f?&fm`a=c0VoElycf8Xs>}BwC!_lmvR~NSTP+M8Va5J&-uUw3 zkm&#$BSn~0`#mE<-F`2qy9>v0Hp*8zS_0kb6QKOb&}l7}5u>I^R!nbGvUgg0doF4| zCTlnSV5i=KID}qvz{fliGV6L=u1UX@B@pzlP-D4R9|WhA6reJVbGX0RIQK#A`yvA> zpbj^aklJmQE21PMBO2@`BNvY}Ru`m-*8`2jKR#bzdB^x;KL77ov_G?_n{5&!etI4E zzRj|hqdqqMW7&fn7t0b29wlhUe*?3>72W_0LF*E&57{;b+1JHi{yJkKIgg`H2yUA5 z?ft#B19b`5)ZA1_;&lst06-8%vi;8CpT9_`)n8cNAn-6#A`h60+e*JJNT^)lNbGnpq7O4IT;4OqFpvVOBgHJrdIiISpB_%g}P3%LTXGy{Gxy zU|>bk;iKN2+Vq2m!Fr`0sf>WGq2UyBhw`4Gbn>%gw)JuMf?tn$fF^j)<=6a~jL{=a zvp`UtgTIFmR@_!L=oauo^I!8r3>;?4soM7*aeWL-Do7lWKxD5!%U{UrMaY&Q8LQ&&oMA z(IdMY8o%{Pz4&ljBVA{Q6iyYBk<%}uG|SE)sPNibY9{Z!R|B=RsW50OOUkYYeCF4Y z|AGS>h<7dU18Shbm$?4#ZCMC?Z+^QQAg_+anCE^ruJ{DQSq4`VYI3oT3|$Nt$lDQ8 z)>rz~XD)z?8ZK+c1iBU7imvM8K1-oBO8n5K`ugqxPgByg7T}F9c4s>+Qb|jto;_wMBmB28Ycg=bmpXr_eU%4kv44A0ILV-n;&gI0GBDD1y&W}Uzxl2vlg<_T(41u zfKt8}C6r37nkv?w?odQ*#;_F_Q|rI_MrzNX)93XO;9x`dCUC3RR0C`7GD9X_={|HD zC-3TrtFml2f!SaFV`t=t3|OqAbF(hfio(fnLlT|6beHB=#W{2}0`tXy>>*?4;+7lV zYQC-0agzK56iVxN%#*KT`o zzx!1g@-DB>be(RfI8;iPl%A^g-Yl&xGoVRlsyh`#c6|!`OyLHl3Blgj`*zn0ap0h~!NXz?Zt*&Kj%LpRR zOa6H?3%(Ca8I})0W4*Vq<1w<5&*`d`{d1j&B^7c@*fD)SOGTggpxg1Vo>5K9 zy`8yA+mwS!me^MFCk>Zo`wHm_BDlFEW`W{6?G{dqt!b@fN-@5(Tc}RcyyMHC<*@z7 z(6aB5=3*DXkNYpp_g&%!pE-+2Y`1;=$j5WU8#+HXevdQty3>I~sMJ~c0Pd3kPfuLy z5zDp^(DDVv%S6De;l&gPIdz4DrRf>1oFSGLI;I1{O&>stES{Ay?3A%f!>@m;CMQH7 zltkY@2e#^+8@o$aYY}*{GKMq$@8g0u-rfawjwFBl+0i>5$uN4}g%xR2tF_PzYF$QK zu!B+xF8rPFwj+l%*tNmF)TV~4RqC6n1 ziCF|kZuIFU5e`v%M<@I5!R{Ui<^%wfa~uFo{_G z!vE%i*D)va{)^vY*@l}HioB-jMC@_uB#ZR(ss~s&0ns_)d!I$w8I>pA6qKp|0N=7J zJlz~_zcVb@`3Bf3Dsg%nLz%<|y-}$bzg0t2;xO?G@l4Xv{?WKnVACRD>6p{;B5>2G zh&Pe)Y3X*zUK~e`9B>fM)2?=(g)sV8soE*J<tI3{xUUc z>QMEw1i&RTcGrkghC&&M)k-;DWkR6|F9%2Cs=QOZCBL01@ZP;Z#cs@UUU2rm0ThGo zP-^9&<-_!Qo@^CjpY)Blt*#xcZ$<^`d?3}Ci#ji=*j2o|#G1`@FPaZgz-NeyS2i?e zccNB!z^$H^R7AB%U~L?^&L%}*qBswG9eT!D`TLb^)RpQ07{)#~zL#I5BTvw@JzQ6w zhJ4%Kj2Un)KIk9DEygl6(O%L@2?6433vv0>15oQ*3YVPOG$DL`wuPkkU-_e7XQJ`E z;SCh8h&&q*`0Ytu#uWY-7Z1&c$Lnu}CTlhCz)`p#4$f3DOc61odffv$!x@slp>NWK zdX52XEP-3l0zl8_PFQ~eCR^}+ha7XIJ7M#VrJGM27UaaUaS8&*YTqy-z>^l>o5vxM zRnw$j+fw|Yc_%xncJrS#(>W&oSD^Q!UupJz9^K>x*3Ubb6qA;V04fG)Q;}%nOh@a@ce8QZlcy zc3|xfJb^L1Twfc#`r8ncFbveugS6)S6?qnH9!zm2oX$3cHvKxR8!vioMA6xAO2m}I z_3Wg0skWXwC9dUKU4$yVtDAEb_Aj*m8Q|T-87^9I6DLU(x8O{zwC<&RsA`>F0Y%u} z#j~rKzLEnkWp6JciYs)Usr|i7uOIlpvXwo}igq;sEVfUpx|+Ay<1mK)p8X%;+OMtq zY8!<}0ne4Q9@=-+lK!8E&z`s3A}58xf`0z;f7C>jHPQwg4Rj%* z(SosTOk|YLYta%go>U}>4?2;e-~5j#df00hKObENO4&lFLmu=SK;TYm^55xhcv?G$ zy$p?fwDc>qYo|1|oe}mkFtQZ^4`+epWEBebld7J0)6fqMXa6()kKT zKnkxSiT@+j!gV`SU5{t~$K-Pf+TKbTo$NW=M9CXY{vtwSI}VO94ilNBYzt zoa8keqkQ02N$w71ibs_aE_F7P=ZtD}UuD)UW^PI#_Dc6Fy^o7JRHRn1i2Y?r5kPzs zyY{hIqtoc-A)ierVHVhx|h zri`g_ZIJ!Esm!Sux)4K2I(cn(fUkTDCo$gXm`Zl{0b64w@2h9W-LQM6=C<7y-doKFLUA%~4>`rc(HkX`vk@3T%C4^qVP3`SEB z{mJ_@#WNSWL~F%YgAWaxS^w^8(zf*^-9UX(YV@L&;jd1%!n5lu%R67cs;dZHAde8X zK%N>tivdF56Zo@^D=&7eJ+;DB)El)beYC=r1^DANlF09cPcNW9V;^#g}@|W z!3eiwiUr1U=P52IQH`VY)P@Yw*X_gIX)gPPk1{%6ZM0+dVieVL!ih{Bn;j}1^p{@0 zX;JN1{N|?Y`f+xux{zEM7r3lHG~=@fzY)1eX#W2?*p!j(FKXfzl?@+XW>BnOiuh^M zoT@s)jXjOL>)FkYj*>mqGP<3fSDcH#g0Zrl{C&AL<=VY~inebUWDzlqRL!rPkK!-s zmbh2c?DNu23oyuh_(>?<3bC;@6J7WQrD^JZ*o!u;b>fwjZ@NeGzPA%m-kq_c95&7_ zX)m3>@Ju>mSYQVt`1&eXvQK27!M+e++G_S;_kGi#zOAs+w+ETE6k}5F(%sh5UYgm9Ii_HAh$ZwG7|fXXto|C`Yu=Z+)AWE;^_rB<@G#cW zyx}6GuPp`8EKF8_@Ro*6$3EH-RTx8<1H(x@{OoMmlCC?WC*I(K+VNShFvA_ z#44N8Y+P!qKw&QTx>wlZ{GiVhQR&zuLPNzB%LqC@$E2~k<&HGucty&Z4J{7t^>6K{ zG4=Pf@7Ux+ho0(OAr31hj}>wMS2%5X{NU&*m;A2$@^kdxnowu=3u`v?#^r;O1zt%@ zHUrJRqvp1#C`kyHbpmo*QaV+q5mhOHJ{% zzs}7>*N=v3gfyfj(9G408bY8x?)F6nS8y z>t+|<->ZS)K*nn>{o9k(RTpHlNvqHP zuJ{{D#@b&cKXmS~G~W!3w+365J1q)aKO{yhQ-FfufQh<4!}iN?Mrb9xt;6aZ`z$Xn zVAhop+8K3~yjNX1*&%@-r~@1n1ud5I-%pT<;!i+eNst~DhNSz_4h&Kxr%U*v*Nhg? zjl!8N)C$odMZBu%a$m(3R-zDRCuCqrk}F`g>3>+AdjF$Yj*=|?imJn_7O7!?j8=N` zgNbtsav%9yqO2*)wdL;@Z^MB2v8vAX*c=n|Th}G>ypE1DG-_$LhzbG&t7;>RX&n~3 zr(ZLOi2v~kb&wAaT`qO**_s1EVA6$xZF`T@vbM^c-@&|8vBlvL3QPRlylwtMbN~tC zAB|4~;ydT{3mF@p0@RUT^>1H*8rTKb9!CgqufH4#AkK2f364d=fX9D!{|=2_9yv$e z-c)s`Pd2G>L$@9&6E4pB1#?lyQijJk6&w2 Sh@|Ye~|0>}wMPLT8jm@Y!H33Sz}5aFI6 zM9Lzqz|;A*0sGs=2A1uU!1nk2dGF7knQwr99SAFen)x(eCO;F8y2C~0FD1YxRTPcy zPWVxkUYmeuz}Tv?7&Fe-!UE{)ZW)Mb;H)^#eHDv$`dkZGguJz@^MA!ZNGAUqt{|0H zpZ7Ch9S`q5!>R%}>}62!+(T^evyO+ImSo2wpu)su4^3nw5(%)KD%gbSev^*HZZ&3( z#&c@Z0gH|}Ck)w6fh0&NBJ62ib%R}(3@$VFl*_#l2W$wQ-~4RmZZAt5O*^2Q5}Xr8Hy@c`#pM?kc?hFWxRXr*mUfUCXf4ka5DD~ zat6d85COB05l#(P9*cQZ3EC8fVdS~?&vN#rce(aF9@xp80O2{{FBvU+{X>Hoh;xI` z{$e^Nw1y*VbO8wv`8|-m?NwNaKGTGaF{P^JLB^DbOYWIbn%eT`*!^C1H36=O8Z-M> zkD~88ry`eSo`tEBN4>w7OWZwUzlh{WM1m8R6zepqGcGMaV7vWY9b?K4b6~|HVG)ec wi>I@ws#sZo7or4_*4M>7;p5{nr2pZ?Uu4>Krr0kU)&Kwi07*qoM6N<$f)&@lf&c&j literal 0 HcmV?d00001 diff --git a/apps/gpauth/icons/icon.icns b/apps/gpauth/icons/icon.icns new file mode 100644 index 0000000000000000000000000000000000000000..12a5bcee268851fdab744ec4a31f75d3d008e031 GIT binary patch literal 98451 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∓t$dRJGIadE?vc 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 Kx8Vjl{{H|h@<;Lj literal 0 HcmV?d00001 diff --git a/apps/gpauth/icons/icon.ico b/apps/gpauth/icons/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..b3636e4b22ba65db9061cd60a77b02c92022dfd6 GIT binary patch literal 86642 zcmeEP2|U!>7oQpXz6;qIyGWagPzg~;i?ooGXpc%o)+~`MC6#O`?P*_Srl`>>O4^Vl zt=7su|8s`v_4?O)M!om+p5N#5ojdpUyUV%foO|y2yFUVfNMI)j3lqRqBrISj5XKP* z1VzP8|30{X1nva{bow>8iG-;V5CAR=-#C~+ST9E;Xn-Gr!ky0h;1D2Lf*4;X82+F5 z^O!~^Jf^7tRQm(w05$`n0FD500O1jY`PTJCTr&uF8&Ctd3%CcU15g0^07(D;)9Adf zstIlhAP-;y5Cn(-CIB#7-_;YEcYcq9pC`~SCax^yT;tqFlpu0SAAgb0M(%>+U?7k~|H%oqaU zG7;{Jz;i$ysD3TnZ-VD-5EkR2olyjs0?__2E-*ZQm7VF#;NSU+_7OmYx`1^UZOBN# zZ~z&=UqaKwI`Y#Ck2VnUWrsY50ipqDyIunt0QGGg8gr?2RTL#iQ3}^>n-k1l{K?P(24g%0NBOjQwp>0N6 zhjzBRS^h3uXS+k@hxlm#X1Zv9Hv0OTvCgXwwP zq#48g-{<`$)9@L955ofX03HIiAkD1kBgDb{vAtuK;{yB_#QPb z7^H|%!06@BiN3iB9Ci78{h)m}hG)EA_Y1zH`^*1Wf4llgsP9;I#3BHLhv)*3H@g5R zlV^Z+P(Cg!<3L6m(}8Vg0JP8Z6)1FRdI6mvlhg2JHsAe^X#fq({sQKWx@-!-`2=vgJA|ipM_2(ARW89@<$pz0wRD0er!Mg=)&?pq^Uuj`CRX?9*x7azbOAK z@H2G-^F}=%gkdm!Y=a>`Q^09J3jk?AHwd1ygZo_)zQ|)8q{l2D{8#x>{=D$a3qS*8 z111CAXbTwW4yLv;z_e*M;Xm3zM*5f!0C|LU zg0Iuw|9`uKynsF=_C>Le(g8pk&cc1r&p*nakv`gza{%N4>RJSp5&Mw;$GgsaI*5=q zmKXbCpZlKhA9*1IxDCMk>j5T!|4WB?1IvT?0BiuDe+(M19t1$Sg}`OV0>fk8pmV72 z*#F7{U_NW0eAu7a2&1HW%{zY}3)Up9h#SY3NF47`W8{X8O(W ze>OhDK0LaB@qi`(hS@cO+Q^{od->yi%maY-6m1cfpQ(>qnED85VcK)M(q-n4ZhYr6 z?DL`?bPNYS@*baIA02u2N7*x;b?F+k<*G9Px4US_gnGiT>6iw<41l`L%)cG}F9P5* zCd}dgCjf>?g|QY9W!Ign^11>c|FRO{UA~Ycj6Ga{hP6N!@P*9aA*6#kz6$UJfa8a) z0PLSLo}&x!1~BPEU4Uop-N_!}GWdt%ozXHBy3E`wDI75VA-wBVTOGd0>2?(2cQ9fd87SHgfKkd{y|RPf7B@l#{7Ukq=937 zOc#Ow3jj#VQ2-6_9>9Fw2LE>h7~|aU=kVuGP^Lf!^3@q|AAsdz=JPEV<>d=;gux{Y zr8fO}CVvtF`Or1iSA;ZI04@NY0crqf2Qbg8fDHgW2v5Q|Kl{S^JB<1Pbg6?E@=*d9 z00sld071yJ+cxHB)Ap;SM`vCXf0#BfB^<>kvv01CC`J_@zV+k|RO1cjR9xrCYoxrEvTxwtwwxwz<|Ttaj%K_NO@n-D#) zNr4^!2~!9r^m2kfBuuAwurYI`<2*$GG7aW4KF?FYzrJ}2WJ=%F$ALZ$^l_k%1AQFm z<3Jw=`Z&D9AVFj7Vcf(hBajw0PLk8I{=n~yu$%I0l1F|_gft6 za?!s75C&KbVeKIv>~A1Tfy;$^S>XP!%94LQ-B@QI(6mS(b1{&Y5y)*h$P4#F-2%J> z;97ngfVrOkM=plL@Ku28fHc5jNOw5wlMyMV>41&U{MYlew-@jM$UKSWi1i%z1sVeU zKu$RT+^g7KS^tq9eEF;u(!{-I7eKdsAg{ro3%svrg3zYu_I6hNtLVeJcZW6<_r{5W z9Kf!t?gQX{w06LkGW)Ckqi#J1q=PO@02+j=XySeC!(Xgr4?*rvXo^_hg@NZ&fcK|B z2DlINuaa|j(yf8~j{!Y)ppOEuSE|n*`~`aO2=*ree>s8Aroiumy+H0?>jvsU2GBPG z=;Qz${R_D8-%ApBNhqbs;@(qPsP93*<4VBSyzfo^a-b9TrmIOkfqmOJ7U{cs#sQQ) zjN@?6E7p1FcYWRy+?(Y6En4vXkrP0-VF^tK#w6-JW59nn7TQmcKkWG@&j((X0=~uP z-hQtH=${GYfcI4T+Jo+@Gt?Wj_aeZ%V30fWU4-5)>+jL`7Rs>(#)^V{I`GFD0J6ru zJp$e{Cnta(-$VKyUw@_h`2Ke!0N-K#V2j;&S(5D06(DAN%k8`()z$2V%`%#|b`*UD>8D~&L zfjyZ4X%7X+0)!wxe4mgDfbZ8~`;2`JoL7(s41@o(;6BPL5AYs<>HR28r~{iIFUbG< z@AQ6yJ^$)kD0}E5;k#wH_VT0k4(-N0KqT;ZG^8y7X~P(Twf+~h*GLnNJ^BG%;~+iM zg$IBi)lFDeAp61^B&;{GM$^Ah34q72ZljHSUI@JXk-0palP!RBya8n3E&I>nZmDB5BQO}=69e2E^yug@xMGa#CiPk&bb{6;AaJ(r}h=s>B2xhYWHEhjXL#L zT%9(7@eZyQ0^+7G~b+gU#t=Xw1ZKfZik4slKJ9O2%+pQ3AyfCw(M=Qv-4dl$%aK>pZ2JOOwN zfOhPg`f#K-+qWO7cwd|$IUdSh^PTd4DRbt393%OH+*zK({SkV9X522Fz`f}Lpc85U z2Po4f;6Xm%%Q??i@N5*^Biy1H{!9}7@wA}qI7a7yvc&_Kvh9w06?mcm_{Yoevk1Vl z0N_knRcUZx3`~Zz1sP}f!rBEn9PB^p%FoKKSEPgG0VqH@3s{gp&Z)SUG4}lad*uJ6 zK)Uz>^@6dsuoB7}0}uy%8SIz-UqsV~ecSl{6xkli)d1*Dy~i-u0J4Bzy8PWC9{V-0 z*AePHSq#dH>(bqc_Dh7pxzb{qHVNdv5z5tF+2eT6r+_v9*2sRm?(d~}!CI3X@R+fO zoD8(s0hVAMoi6GoSrhVtd3{CD)xLeZKTEk#eqiT>f!7yVkUy*kGTy)ZVKPwvpnl;T z`v^!A_m!0Za8DNM81Cyp7yIPcH{S&?g|I)oo`h#o!}+OPa3-cMoSP{J;MVKGIjld- zfPXjv;3wLCZE(u~-L3ywAUFOWt@~Z=E9f4173BS_oB6+h@arKi>__T(KMc=hA3|+~ zb5c9-T=pVBI$!}{Am{{t*O}@6uyp>~?DJ_RAbZCAIIfj;x9!KdvsGm@d9WKjxBXw( z9UNE|d{;sF z_vFHOopqlvmjeBWZs+?gx~d^9E1Z`t?!kNBAXAV(T^aBIz?A#fE}m6h0tf(IQ5`|8 zBf?qzJt=yxi-YYa)J53m!8nWITm1djy=;&_w%I)@Pp9nFFwdkPlzkU%52T?`BIXX-^U=z+^%Y8wxZC4R-LQx=SMZCZEb4{{Hq(rkziK$fgt*zYTa{eX}c zj`x1XI~!fPKn~tVTZnBLOC$}2?{jXZZo}_~g!DlEs0TF=HxwX&x`gA2U+L`|6+@o_;pr6KgrvTE#aox*ecLry)%;_6Z@) zze9vSlt-8R1%ZEO0pH{A*Y|h-$ec@8|6dRC>+XE-*ZF_#$2kC8J7Ad?(1(ZqUmMQr zYy>dBMaYzAPh9-=*ilGV9_2rrTFWv`e`kbF`7_4i`&f|wg~zbBzbE|0vZ0NJej2<_ z%J}~K*Rt$^pA2WYsQ2hy1C&wM9B_a5KMQ3Ccn9c-?3r=e!4B*Ky%IzF(wi@o1=@0u z1@xb~UH^+g_DT@GM@57AMwoNPbK=NWkVa45FZohOY9O5{xE9fq@d&d3Aa4SEn;826 zI2U9MI09gPCy^;vR@^2?%OB(q>x;ct2XOu$&%^_Ht^ir!y3Uup{oem~5ZBSp} zJ1vSD$M^;`GmqZn-i32If%hnXJ8*H${g3#~e1?2qih9H9c>Bw;ceXubDabPwz^V=a z4XOvhe#wDL$bzx|&%ChzHkA4S=JwjPpdP1!9GTy%{+_JAcmEF5e;tSq-{t)DGfDhu zX<gsXSELq@*pp%q)9^DAK#0I_4q!_Cj%`o79|^koZSIofLK5{ zz!RR01i1?r!h1Zdj`M$%fjCcWNd3SL?E-$Q8^7iJ2lf41&pN0Ow|{T!3o>me@YoT+ z%9_k2kO#~i{`cF;d$hq^ou(?_`Ave)BK9R^tr0vGp%v7!Uns5`xJ zEYR5oFven+S&%>4fCmtF5V$|3FZe6yMOR;d2(n)e!1dqm>Od{%jWzBqAJNP9jxo;c zfbXzDeO?N(WOY8~0Q4gz{#)$;?j7rp0ohYnkU!{2M?BaN4(vF4z%Mu@kbVPpa5hq-y7QiTo1TTGr@QImiNF0 z;93lf)79`S&hE1DFA0b9EHGz70zN}uy`2x{-?#=-o5BBc`(04~u`h@=Addz4*F(Gs z5FXlq#=oTeKawcQ4rGY)>a6SuVU7uL?rsk10N8^cA%o?(U{|4E*1-n6RRq@&_!|Mp z1i+eZ#~yHTkDo0-dNAzU#Wws$FRa58s1?`__&~b&o93$w4Xv0I@sVgJ>dOuKzIA%xSp2=P{uhq)S;eUC_{iCq;(R|UHLzPu&RKbX8V`M zyANkVpxmJT;(Nh&dSC<4R>0hV>LEyDa50>n0Q&S(X&yvv0l8!Q+XnA%cU)nC_e>d~ zJ-|Ji3Mhw3)Q3Hy58HsQJ*2*nPIvbT)IiuVm~U^r@Jy&^S_taE6p-VO?9(ZMG?u~m zQ0f7siR%qN0Sz_)Y+t%V1KKH9 zoCkpUn!xbLRB z{lIU9!!;u+U^%4AI5!Obvs{oae)j{nCwBj9IiUX#)PMe-%b)Qcp(Lb31AHs}Z{14( z+2eX5%jN$&BV^Mi;#w@~K!0%e1G>9U@LTd{-oteR&(1R=S?d=t&*cCcU;(_wcJy1k zW%b^3kOQ9k(IeJ&jRE+97VLv|H}8Eg{^RcL^&c66?`?IS6QK%ogN!{oKdJ*bzl`V1 zqF%AYb8Pp!*3ogS$2_;AyFCA1IA}vUrlW2#-U(ufA_AlR2i?KTaa z|4eX{70&5^i#mXI;OjkF%(~qj7v_sqodJZ$`K;N0=&Rwp83}mzGv3)@>I3SL7s|gU z^FoF&7d(nu3v>GI+gXtRIS7m6#(zejJ;=2PzNvtA0P3s^$Sx7U%6_3Q^#bMZ(kXux zmMFpcX+o{Rb~AwmUNhzVJr~DqJ_aBQ)B#p6BbY<7pjP4jutXMUIuBugDfu(`($yyv z279m;WQhARzm#ov{^R~Z_s;KXXfc!RmJ4!+z1gj}_8P_lufHdE=6yWdVMZ~(^MnwV?1SGI!}(@bF0{|cGk_bQ zyYqcaIe*W^ar<~o7xsCwLJlJ=>Lk#`1M&9*zL&?>_m4t*!Pk@ahGhc(q6nx1xQ`#& z131rxyaRLq=6$YR{Gma zzJKjv+mCC7>^~@fIf!2f_&WXX`J-`7`d6<1U+M?W7vF?&Vprb~&+f%DMX;auJw3qh zfy#p2_%fMp{Wqr8b-l0IZU+3WWP#`3lEr<9uM1$bE8QaCt3X|Ghk^SF@U1+)z6axt z4li7P#JmD9J;1YA6hO9~;9dfJYaJQiBQ@=b{E=T+Z@_+HpKBHH9M|){=5crY zZ$S<&c#c<3>mkYy`;CylGoY!PbbJK5r$ShQQ7=Cupr^Wt?*+m4UU4rGtO2V|03-m4 z0L=GHVGfDB>J?1{`;k4$2G?!j-5ep{C5{DHeP0{j=UWEy=SDg7^uo9RY&+rs-O)J= zQw2N^TIFQNqc0DH{Ik)Q`T;3mL*z8_f=#Q9SI&fVi$Pzm7A z<^&n%I70a85buZkUnoO>G=P=4|C^w9xNq#2k>k%I6lD!E$Mb_k;J-Ya+rYu<81QRa zPzS&kumMj808fJf*8r~p*e;+=hBF)KF9B4LyAOmXgWbUQyT49~CBGr{Bg6JXnl_Mj z9iY4Qe>dcf?-8+-Uti!q<^b>?>mu#}lmd4IxDLQ)C(sK!_&)?(c=w|9r}eoZJzO*9 zguD^~-IYDsAI7_YJ?(S+F&F-sr&yPuKPCYDkc0odeqHlta0%py`Zf?y3h1u<(GD2` zeg+A>CJmH7jLYF2XU3QuZ7{wc1!Hsuk9rNAKZ_77FN_;d&vEXcyZgRSN6tcAJX7Ll zkj)VzJmUG@7?dzT}BRtvs|D|2<*eNQulF> zxHp~!@o$qqo^OLZfpU!l_Z@&~4?n{H2LRY_+c6(p$nn{k$*_)4S~= zt`8bf>ygemKr<_Se$yGf0cSyf$l$`c znLqYUMtA9DH5|@2;oc*VJ=(Bhz#ot{IMgtn2fe!*(qze;$lA2271@8aaJ$RF%O z;W^skfL>QzGwK`WSYHw7Jj-I)P!}=*zwCN{cLjp|0L9KaG8@W^^DbZ4gFo`adVa?y z&>tbxquz2s8K7^2?-$Z>UST)j&*m7vF5@fE>2avnnAX4j>KY4*LRqr_U-RP6{J1s} z0k&2c+mnC#!uJEQO@nga9Pcgw_F?|43|~Lr20Y>Ejdty?;IARrfUbVPSm4!*9`FnL z1Re3vACSiOwkLaXenz=akAZefN4_)2(>e$Jgzw^VohZ1Uv!!nXZ28Iio)dbPFRN z{)-p(1-p2Ob?8wK`G~x&1szBRJ;FUU9Pt0Av(ueQCE&aq%t!G+`ePuU!+@UdD?ys` zAsu`t5Yp_OXFvaRCVnHqPCMEG`?Wi8JkY~4lo|C8>r**k69Dyq7x2UVX{_%?ARnlw zxOQa*z&RS+pYg3a-Q9cTkd7suCI4To`(LU8w4*pDfb(8H09N#9jjCVIk=Li7z41Ap*tNu5T-W=$!;5$m+rQyH! zptCQ~j&&>?c#Ly?tn&3+;V~UtTfn)MRgm^X0KUg54}f{3cHEN<=d7U1m{(E+Kc3Yx z3E&GrnPdCj1o&3^tloomioP877;vJ__g%l|0Ms|M1Gx4X1$_EhI>3|>+6A;NINrPm z$OBvioCDco{~gyHiUBVH*sk}aKhMnTTP~jSz8dQNFZ(^v-%IPS@!@$F@Xa;cvx$2I z>H**4<*#<{HI!!w*tq}99M6wvN0%MIws$GWAM4|*3#ScKo77F_p|#1U)Ix~`5(`5 z-Uf85sx!uT|E_myvx$&;OZ-kKf_Id8od%ns0LX*Sl#5_0|}^-3#>?)|}~VObmlQdn`4I zFq3-y*DF*X#eE#;<3Jw=`Z&0DllK&!ua>irA=OR!#{huigfYLykpEG3q4fw4D1dLk#*$?DE zR*-2|eh?M@!Cn8(8*QB-Kl__HQx0Gf*wo1@3e#WPNm)6QBek7>x*W{e1QYHG_SsJl z=qeDUE90iF0#TTReeJ*2NnZdwFaOL8Iz0eH6~IRCQ0RQj@Iw(gnEb$JSVU&|zz;?C zr+1PG_nH2#{J;;)F~R$c>$AU$uHXFrzkAMP5U>a0E6@YFGWgBkN%U{=J2U*v-M zci#H!FYoks$pa*&z_`)TDL)W&XFgr>{4DscijKB|A^0u_{gBz`U??$$pv!^9jH}Cn zP?&y3^+OSwbUp{aKf~g5`56*K7QtP{6@VFl8SL^xOrQ|O)^&jeG=bos{ZKXVVo-rW zx-2MzO7w%Y@cL{tATC}C_zW)~2rm4B7vI|oS7^3&4^870BpDV)RJjwhl(t9ZRT^x0Gu~~X zUyxI9Re%$v?0t%aStR**yJ?DTL7DAhf8%VnRHf9y^ZKv$4?j)S3=oN~a-Sn2RzA$9 zgpFgDM)fm_2t_1F{*eAemo1~SO$B0z#{(X|e}3IG)zYefm^veNfY~s@LGd+H3o--U zC8lnpEjg5yqYyRzO;E-**Rd7i6zUOV`%3ZcRWtZ}5 z?fMJK57(U9a>n%GbdJ_=2f~!`C+qIBZRee7d9qHup+586v+DuMLTowGsa1NL6Zaq7 z`&eD7XoQ}}xdXhJgac6voy zpi9;Tt4U(<3EFv%=8{_VCS-$Q96q}Q8Vwbw6PNKS=CLWAZJ@hJ%Ef zoD=7(_Me)6;DY3$U7aaE$!UW@_hG1(cM!gKX$To%9va(ZaThX za1H;|<*Bl}ZIi1-*4r1H2*21Kowoa$>k;ke&JwQ4hvx>wCVN3h-thM=le9~$IodM} z)t!^}DGN=nENZWOf79;txni!k1kHg^Ug2AJC>3*KuNb{`=kU|ES4&n|Kh&}E%{+q# zZW^D~9^R~~YpV<;5Z;ku6(KACLX7|8PSRnk8-q!j0<(EWO}j$Ta>+IBcV2xDdqJBG z$!IS3?S`yjXK$rQO%L{)mQb%3Svf!TjpLx2w;A&eXiOwdPJG|C-&tyAi7 zkL}||1YH_o-8@Vy>|)C*uMz!U?utEWDUozxw`)lA!!31hj&Cs;P)iRupD}O6#c<_= zqi;%#dYTh9LXJm|9g+*b-S&#TVzX!Ad%c#BZO=*T3a@jPi>2ns@a)M?BJCrvHOCXL z`h+-t;3*4US7tj>PN~#=*o}P)Jy)haF^uBdY{(%zD6h?m-Dmeg>88Duk^2VZM3Ts< z{Y%nm^UX#E+!ii+J|}Xl`6zRdGUeeyGi)bEx$)bNeZC;wz-@bm`iX6gAwDUu_ICIi zYzYo6ZjDb+mrNps$M(C`k$kk7eOqite2(ShlVuS@vB=?Gy{~> zMl@eA_gH%-wM^|ieJ_#Ei1>u}3BS(1#=T|IPn#Vy$B&aaNe|$sdIZfTtUXO>%ILSa z|0CV1ccJyZ`d7yB7;@-`jD40po&V#^lv;O+nbi$;b_&V-NWaF-sdq^Gv+pd)zr#Tr zTsZPd>Qc@DvWuo9gqC^k%)6LpH(T@YX0q;$n3zy=xuN`}t()1F5cZOFCUWZ#){~y_ z&o>U4;zGu><`@gQ7q2 z_z!fXs#_)7RXRns9oQLqYWJ%{J2vGQp(9A7NEZ>KZQ+H;hh5wnHkE^F0)kbgbu zjTq<3DYNI_1TMHJ`isspc(}GDN3Ghza>=X&Y6WxFkHBFy`ZU@#VhaN zY*EAD%C(B##BDQf3hdo@=z!caamxDR%S)xBPH6K~rbhZ*Rv>P&qNUYp(6(``)3)?D zyQpp3&APmg?sIjk4DH8&QJypMGRj^x3 zIL$fMnRl&({pzQ4oU1$=E>0~TG;wcrk#5lX2%5}3pO8Ju{#tQ<7gA@PD?XjEZC=VU zUKbOMD%;VqEjlk0_|`5bDH|!cUK(tA>nJoAYAucJ$xCh&M)q+H|hQ`qXiLU+c^ zYZGc~KMi%Cop<&e-Dd6dk1{|+tZwtvac{gr45|!-TFWLI`k2RZjlOv;;YRGIi7xTc zJJ+o)w2tEr*3+9_E?Rzrq9h@wkStJFs!=^={hKRRde>$o=3 zB)(X~x_v1?i}{N5#{WP5QmPVD$F-j$*C@kJyYS-#c^rCE@hGwCA^lYYtPg zx5_#fJm}vzA!yONXO2S*IkL7bSkF0q{JkRo(_>>jw<>cFeBfQ!bXQ)cSZK9HS*hsC zR*zhDN7F5<{M8Lc-JwYU39j7bcI&?zb;7cx=HL?zO&K=FO4=D*MUq>;G!*%{ioP4(BvZz7cP} zGot0-$HV6e7fm6N4Q#j6nPgb*3Hqq+Q}RhOZoi~+0OUk_w8lNYNWe`q$ErYDLgr%) zu~gkG)V#uq99z7>O*4LuON6olDftlXY;_KA(j?tW1SnOE{Uh@nS?|O!zmZ#;S1Irf zoJLsaJKoARM=L^hk9=rgt8UeJ7i*4CIlh^kI}UR)GNKe0nTYM`xOUYz`Em=PMohBd ztZkwXHQIBWQ$M@(5RO|P6W_Jc@8)hR`Fb>mOQ(0wv?Nm`;5bBt?U$r<6YS4$%{ zu2@1icOZoRiJzLa`OQ)GA%}%xcDu2))o8Eq;s}+^q&;4{uVG_zd|YzJ04uFs$32^F z7%SwRIWuR!-&5gT9lVWf{Uwsw*2wtqI_{^*1kX}guud*-PW<(qoW~Cfr8iHXMJ#=3 z{PtMz{fN0^3cUJP?-a~9?;YbnxbW=MDtU96{>QiIxt0}cvkzsn)jIB2utD+!%_T)Q z{$aUTqs$^tYi|KP@sx^5)>Su1CTgX{i^2#m1C91JZ{NSE#GBV;m>W-4Vm$k<6JhkR zfwMQP3gilC4ctH}3VO$RXxauVl`BM#S*9^2^5#n<-#!eQEz=P5GI%!MakW?HYP=`J zNh;p*eqlTJRMa-jmYbhA+9?A%UKh8t@C82Bt(qNaH2ZQ{MOtxoS!Sf7zY)b-sMS4P zjlA5Ra{$MYuu&N+*AzPVOW!7yaC~SSI6YXF38i>pJR_!ME+x`|xTPpUSvrRx{v5dAsj1FtTr_P(=n zO3=ws=TAjbR#N&0CP;;im#v*pcy8YR91%W45O0SZnObmY? z(HK0Nvn8A=`Se0tt?Rkr8>g>&HlN(U=OQ?8Ix$GT%+z_1=0#3JJ{R@sRaO}*#ubVV zuW%{ow@lIgPOjKo+1Kq9p`umc`24Iu&cbw=c1mPe_|&>n3yf<=x=to+yeX&H`rNf6 zH+Am^YR1b}(rwbRw+R|&p6&>E>mxK$+R&*$MR)#1uIHq^YfEz2!mbUr8M#cY)_2Dtf;-W0m8JLPVMOD(0S?rW57d+RWQq6KT$N4o zPt$o7#j8WI5|*Dk_l<%b`~wY-;Xd^b>F&|TNPd@a6(4NoQA ziIZchPOqAukTNI2-%+62$9%_Y&C}~j>e+N(<;yA1Qle6K8*I7L&!^uqqnO9nHa~V9 zxO&D-A-|wCrdp2^Jl1n=T%DXcOxR)jYV%PlA(?5}z@79tpFMB}# zLV-!!*ch=ukJQ!u8|w*r9s`NhH&Z6&RH`1_IgvPuyiC%*XjA)~C~ET3tfNyaLk&8H zHKv4_oGX?!cFZ59E5*K8g|~j=o>Lc6PjJ$jC+}6G%0q)ET=b+^e%?pE;V$)|8WGht zF%M;)>YYg*P)upx>7ikAw=n5s$%6Hg<82oQf6TTh&<^AoW0b35rgum9B>Rf;t(14r zvm0W(MwB;XAtfg)QJkPZ#9DvioLPk@o^HHA;upEKVU@VS^vhPnDjoCLTuB63O7z@Y zDIa+5Om)kvPf%UE@sg!`hc~ItVpH*vJ5q1CN>+RM+fL{5B{e=UO_WrBRvuqYrsye2 zo;bwjBT(z&bi@p*l+cdHkEXxeR1xEH!_fStQ{|?47pIBrO1@yDFXD6a+Nk(O+4J?8 zb7J?Zy=&et~&cEUfz7%$SQODsZ z;*sNtf@A9T4i>+qVg5e)-KoJ0nnMB-YRYWX+zL#GlQHBZ0zlxmP^Q%74~C?h!cw}CO>#~f1rTZ zJvHgMYa6^4`Mqh&$b7po=sgcGbqC)&&cqG%v&xrBHXAMzZ>_SJJ}*|n>b7R?6=8Xm zYWMv!BTsBo($BlH{;J9%%kxpI+yXTyyK9dthAE9!AG*N#aK8uFYRJ$`BaQKorp75H zxfUD@ugEhY$X+x_(atik&Qh{Yq+J|Q@AXh|uAi9+yXu?3D4$^Em)fHX$D4|XPoFsX z?L3-@Ax(Wzy+gfd^%26z)N=)brlHGx_ths5YW#S|lyJ`6cGP|Ha;<}6+nrUi@4co( zkou`AQ*P`RX>6y^Me|;$kCWOJanSej2THY6sFX^zqoTx0(k_lHxf8sRQs&OZS1zSR ztv-?GJ9oh_6KE$-&$S0oZf~E^I5xCuZcX-ahtWo( zZ8FE{5tkR3R<>F$ihc}3c*PTZo9{Y0+L}DHdU|iYUT&L=;ij}tQ9|4;87VQ%H6jM% z*Ug@jb#%hmfL-y#0ffU=h57;m8!cy<(7Xl;#7ao*Od!Z+5&}Fn?BS2uzuolO&M`Mr zbXE-4*V_ARt@!k9_k<`{D#Vh<`%Yildc{gHBGkP2%x(9iRga|NSNXckTr}#cpYZ(L z!Y9Si2M8~C?Da;i=@%OzsXi-cYP!{n8(grjX37bxTgt!Xo?|RH`Kv9>?cOq{hyk|LDbp zpovGD%GZSw=Lho_D_Zg@2wfO{$yTWUCzETQ``n}hZM1dvh~<~6IFzN+`iTo3d{SMg zTWuONF?IRa#Rm(oSBlP-Y|B`ezFKtNyS!r-uM6Ws2LboA`8My?KOc2&Qml}u#F>3k zyvA&9alY*G7QP*u(#lPR4m%7U$l)?@OI_=UEsJa(58jrrtXyO_0V-+!0!!{NE}vQ`@B$iI(Mrj}b|sJu6B*+8yuoy0$< zUxCm)wQT;82{Fk5H%;RVxD#~9&IM-=1!Tx2>FF=h4Ol$h>lEohT*56O`5jSfJO+mN z>3N3vlS1fg!O$^;dGW1#>xc*j!wP6_Tt!+`2MZsR#7mF5?rk1No z2bbg-?+B{sKT^rg$I+ww?75r?cKngbT)9K7+TNdhLJHkVTCilH`=+S9fq`?!+@#0I zpP+My@7Jz)$?5uLT(;NMJK20guB9*Qm!T^8fxPfagJeytJ~ib<&HHw7J5KK$&rxqZ zcZ@O%i)4=?PBD8Xp;Xm6_SGH_v%n!ir95q=t|Q{>4Xi5z7N~em`EWg>-~5rU-oGJ# zvYE6!jzE_wH8YtoJKA;T-LydEorU$+^%sd#Do2kDUA8E^Sub^n#~Mx^_Jn|r+2xyg zwZ(bj-m#?yoZ)<{n_*3CWXn-7pBCd5Z*N|kwKCU1T-=3Fl32oiX0D?~!2S*Me72k* zw`ofZH}O~#?n+Z&Td!4pE8hF*qbUXn*PP<+P-BZZX53gZ%XTuGiLM9r6ZhKHg=Y$7 zt_x4miPm;bf1tcGFPp?KFo-wOqv(!E`K$x9RGm#@WvT`1jtCB%rI{aZ5~bm;EI72kH%ycfrW_{RPI68S9x*XN@6vVG zQ5GA-)}5Z4o$6edwRC}d{rw4zM`x^QahsZKlyN^dG~|3S=~hb;r_Te875;_wj+GCL z?{zGV)v?+^f2_YXQH!j7NH_MCrdm0BsR*Pz^~QqNniKhBk1klDd1Rj1(z>jd^SDif zjI1MTEpIHh(z`QY`l7utY5u3oN7)8tzZT!FP~n#ydudYP%KBk9M~c1Otzi(EsJxOr zd4JkblWlPpi3g?-ig>N_g^Rb;joMGssFbVz7K0L+ptAvl+vhYu|Zc?F6CpNmArTHHhHU$K}%LdrTZUHPD!u-)RCTQGPER8 z{QX143FlME=M0KlZ#11-eb>}>&55XvWb-2#2DX!}16Rv59+fw%FeaXH3EoaPQ?StEC!GjCy9FbNoQ|yzyGQeAnG5Ik!fz_`^K& z^)3TzCcD|&jM=cUZAk6~ZqE1Y)=rPy`ZcH*S{$|&A0zsp|I-G_fsB{ub*JoM2tQ2L zylt4qisj^MlHR9M6?C5a9gHe_P#SkYJh(l@`3-64b*Y8kw{(f6&5~XMcO!;OHrlgn zUcjef;fBPM118+c7m6XLMprxwx*f5Q-(0>X{nA`T@*IlYJYJWT;xGNPHch0D-_h}o z)9=&f@g}Xe%pOS}S+u{y!Qa9raUECvf&1(}+FbjZS8r$ta27lD=FzsWHvt-zP5qUs zKA0abyKYxHsi?)Y(BUajGBRmmRG>Yt(2%=w#ivh`jUV>2v@k4`FPP*L60|)}{Beh7 zr0=<)<3|Yt#^leHl2oH7Pr98#SRi?G@a9_Cf^(v?E?gCp5P#S~;0c`VGNd-ke95o{ z@{PkOdtc?2B`ErnB=^_xEER6Nm>Bwsr*5`h$(q@3RIF^9IS#0a`|y2`T|Dh#p=;@c z7eoC=s(3fBxj8A2G(6TruHp2#s#4;j zZ|3yA>B49`qee$F+sNgKnG#boZdD)Q<YKP2 zs4Qv7anqe`bdD<^lZ)P8a#8-ByplDJUTtf}CQQ)LsHZfnC^*j+=fQi*p>R+1s?iEV zyzPedue{7F@Q^t3oYBY^r`1|48mkoEN2Tv9ko6CtUY*x6#(T(hg|vkyj}57#z1bGC zmXSSM^~cdSM-F){*KZg(c>SK_icJpIH_rLruCvk$R8cFwJ+lAZiKeBN;&cVRjfVz2 z?{``J^jw>EiPX(98{Ot>i)MzdCz|=kDm9t$6Yj$4$pnsfLp+tB)* z?3)H{DRQbjt#*F=ro*4e#_zVpdh#h!RB~;mRnjNBoPEhL%HguJZd~-t#TLF%MS_#Z zDZCK7+J2z%P~MY0npX6u$@iQHgZLtSh91aYMy%WF{%CxDYMIkOk9t1=e#6W%eOMRJ zcrG1tBYb$$%vfKObD42E-siO^EhLKPFB5+w#8cZb|5$>4+q-nxX-cPalLYQ z1;w>CE0en=Ix$Sfu5$AP?=TO6pz+5@wRKtU+BT7E_DvxEpaHeVfwHwm36dNAt zDPvxVQ397o@1b2L)XcVe^-4%Hn{@Gbt)YOp7bQpZM4V`&y4buTw(acJ_9L~fB=~9% zdAit5(^;!};d6Q0*fRH(MSF*c9!!3yH_3yzrB=lIfO6*5;nAslzHe=(y^%V6HAp_% z*rH)jz{JZ}pWA-OQV90RUa`?g+Ow}EU9EVBn#G9H%qZOv>tQb(YV*!!2 z`TRb=BM}`LneW242kV%-yQ$){Du1-0>nB+8`J#s?+a2P#eDTibr?g;3_+^8DMDyEyDF?+!7U z5Nr6fj#%4Z(9sfcUh|daNY}9qgLp*hxb+5=e6rhaQ@GRA!M@CQb;fw&OhdW?f3dZR zgp}L^LlU3S+mwYGUJsHIkiLlMwpXdz!iHs6)+g)>HG6W1bG@Kz(fXD#*TpHLhbPJI zNm4$x!y~A)#Qfd)W0Q|_AK4uTOHdOUgJk{A+txbgPOEMpJ64_{&YqIg5i?qWKpU%g zx@1vcCP((3i1k%xGWG}7-rhdcUvp}%Lq>k;+#5c-17;4E8_)TUaJnf(PFf&%gV(rK z`VOrZ{n=)Xj~%G~!0zI>@_pl@4rUop=&{tPc_2{-f}~l&c1lRoxV!$cV_#l>ztJ(c zb)r|A+y)t;T~5)S_fKiq2<*<-w>I5fhj?A`72D9QbqQPZvqBJzrhf0`3QU_E(j?x7;L@8t-(q(7`rp@pkrvH6>i_;#Ko(wRPsL zo#Sye)tzVUZsi9HC-18;{W#H{Pk&tOgAIu(3AIZl8{48nhd^r_pFDrjq3xe!mJB*7 zno=$s+;K8)r$V*;%`?87#kzy#9Y!K43t zypQuqTFnsNpz8uu3wLo3fq^-^`ehDo6$3Zy8GPoHy73F8Jtk$NcYk!deXOBWt@=*j zZtdZh%$HQByvh zDKkj0khiI$!IFQ~0ox`A=sUg`<_}>GSY*wdDnvbeYNlxQoiqAQ7fz(fE=vn*4^CaGN?bTK_D##a z_E{z?_j`Js9+okh=os?+;|rf#n9o`gWxSuo_@Hb2E`14&A8 zjEMgh<*?kL>_!QpNp!H;3o^<=5{0JjD}E+upSUpA)}7}-#Y$6HT=h^M`R1woGhNPX z*#(xCNvA0OEg^TBHJc{96WVV_kfbUJA}QWm2)_bsMSl5C9W6(@#{CwIchZS$-k;ZYGPdJDSzC-KM=H0HL13b*21oL3(MEQj{zmO?B8`*HZ(B`{ zS!`E%k5Kc0SarUN>(TTzlUCRU+uu)COLgZjI6!;MZY(CXwQ&T|@#bM-X}^H=IUk;7 z{`XAm39l1syt7&MkhTny=z@%Whb(T z%WnKyiPQ0(E2ZfsS&=pG(=T}j`>iss;7xTt;qAHWZqsbSM#-X`8FYU!fvDZ;2Q4R= zXEqAR<;91hH(4b)c5kn&!Bi65Iw10fm(n%-a<(QjX26N@xiuRr#w7_!C zw6Zj1iHWA^V-(ej9IxoSIIia0ni1{2hJGe~7pEL^rTa^SpFJ zx9X|!z1c73SX5SpiE9L0@g8)va8H`q^GSpu@}~#pPcDDnIDN!^0aFEQoA9TK)p7a9 zkBp4i!NcpA5z%y=y4YH}DL8MYOJlRi;Jadzz05YZlb3VU?oHj)e_phfci!N!#mdj) zP7;*kNZ9N2gzML|%*QFtjd)11bDTRcMJH~}w16DP*{7D| z8n&()SHWA}p6Qp!c1kSf?4!oDB(b>gWsfBlBEx1WW+~g7t-9I3xz2e-v#4bH61(Ni zgzFpIbaU4|SCekvr91=|8bhjf3=o}05T24hutZ?F-zDWRE~x=K=$~?{9Ix))w&O$U z8M0dLMB&EwYMjZ3CZswC!5RdAki2A(u&u^S`>XUErP4OGm!%#S0!3M+eo7L&ietjf zi_MHIVlHdTXtZp;9vg9M`Meu$$JsUN*SSn^4Z4^#Kq!0tpbylb1l1iIWlW9JlZD6R zOKwm|pj|YJJ$Pcv$fx`1D<;+PYiMvj6;?J+k9n9@MKe=(sF-&&s$|1~6~W5WRCW0R zQqSC0E$@0Igk#HfLW%G%2(Gxj4!>QldTRHtF zr4z)>hLPUPm2r)_Tv<8sTtCg{_NpfeQ=K{1#*62rmaX5g$VZXm)+F^~H4Ige1LbqQ`G9?f1|^D=;_W3V&Zdh8?@x!Q&0z6Fs1JE^Oz-|SY=+Opc;YJ*Vu zvZuMuZmX6XESz@L@MeUm?haq0j^hdYZFF_C=W*vu%{3AB=`S()Drfeo(E3c>!t9KB zPOfj3E%(tTei$PEEPq{-?M8}gxnz3$dTGo2?ai$dwZtjTRTnqz=G7)9Wot-$)~4AtqbWl%UF-ZS=7MT=BuV(PN=JZO(iz2yu~XSwZGR?vKQ^camR z;^>vd_65$oEf1Hhc$4fY{d(FNKWe(qiPgev1za$K7NVJOEbf0%KJ@((las1768+s) z%;6YY+HxVl@w@|fO9QNaUkFR`%Xo1%BeRVJ0~-AWd&71#h&QCj>IZ|^ zA8`5j-Eb&ST-kncTEj(IxA`S6Oa_-&OC)nmPp=Iyd&y>P`hcx?S7TkQ3}0#}!E6|R z%&fG5nuM652ZKD7Yi(dzCxJuvn!$xy$7UYEmZ##yqoiC*(`aOv#ixr?oyvtc+n=$Y zHoCO&*r7#MM;h*&9=t%$;X{7Z<+8vst|o2L#Z&#=d|xf|D;{32HP%xnfbS(eILJoX zqSwQLd*aVm5xj`YjwoLf{c!V9e9ggrjsvR8OqamZ z@iC{HUq97rr#GImmX^*KMohw)slZVMf-&x<{rHR)#pZGEv>Uv*e_8B+NnRY`Aw0wcjnWgm z4i!>ko_R;gav3Ey`mWBq9`9Uob{3_r>h#BE$$_Vw4)D}@ve|G7Z_e7X`$?JRN^_xw zk8M}=FFp1W#wzzFUA}VURceQb>m&ljr+k8TOQw;}qG!t`)tdw_4dd5hx1Kyrzs`~K zTCL)gX@mf)4O@LmR?nz>B=uq)$w#i>y-nq_Ylki?^A~&DuS-;xGu_sjyxK-gA2ueX z>BqjS*I=LZT5QyolQ%uox1!y&ZK@rRqbd~!?pe5W~@TCR5E!f0-JN!)8k&=zgD^6*6Av;ORUa<$9WSQj4p+>Q!rnbp*1MHbl+wcce+CCaAD8EHNrX%LdbF_AnjY~B_%9fcdBzP_Gw zrh81kyr%xjCg?Z|-{XE{cU57Jy?$}pzKNoVqU94fqU|abl@~7cU-dqKvT0shg_!Ow zD_i3a8BXSc9m~`b>Xtf$Uzj&xvsqbxmm|X#cpk4hunQKhE`^95ILGgksr)?rJmJ3B z7tFgctx z7#`}v*seB<%c-(I?+I;vH$t1NW6Jx;#pf-vNsjjncFkYIx#@qcoQprx-yg@fF|ugN zHkVv7mzev?Epo|5C>q*?&2%GCa>=FK8d(x4m)x3-klPlLYq?)izN6Usb|ch64??x( z_WS%EzklKP2b}Xb=RD5k^?tpd@8e=e>N6zGj-$7>#TqEe3sjwJ5A|xk2E@VUmR}~_CV^_|G=M2k!(iDUumE&^I{=P=X)xH}?wRWc< z2F;X7-bcjxwF#TbxgR%n#L?`ReoLK-z1PV7ombro33=4Yb-THogZ*?IcY%?6+K#(4 zK@e5r+fYyYRPw!4luvp)%goUr9c;{s8AgGO;k?z@Fvk>hmX#N^FgTC_SD2)3J*)t?D97Ua|a#gP!HZ}h`w4mox{%kWQ(42T_f^)SiQ)z@&f zXk#qycX(ywOkEWlkr7RRX3Vw|JaU1nC3Z&AwbGh>#x^*c4Ji=s(}9VsXbA=y)8pXR z((g4{1*!O1oe|W$J7*{m8EY_H8=Fv(X!hNzDAWBu{Ak3&(TK za&>GY&WBz~?Q)RLdA_%|vnR02S+n;OX96yj&o#)dhO$n}-9mHRxW0&l67`Us%M!%$ z78^2fMaeWD-B-a(iLUPNkh4hBQNms@i{(e>FK^G@iYiLnp@;%Hs??>O9}zMLLh)gX zs;js(+-pwaMQ-9G!Oy>kr=|Ot*!a|t!JcNKEced7R?4MbJnGYIFOvT4f^79U8S>P> zW_*A{0LfZHlLycROBgSVT&TM)7(jcA?62rDT zxL-xiq>`bAEudHqA|ZRliL`pc**ZWW z7a5F8uC1O9K)|a^gF1Wo-PP@BFlE-5qivGFhQVL`Ncm!x2vvLzE3J!PKovkX=<^w;$#|*{-3#-;lz7(NC%ath)OXpeYXaQ>Elip9&N7C5th2!Gy$S zbJuxNuWhVjErkCvrw3*iu}>a=!f}L%Oy)Ne+E!rZN+?)6rep3w`P>y_2pjaik#!D+ zI$%7y@HaK>use5emETNuwjH~aC*rU2j72C0H*^bO@&!m)TefkO;l65964?5mde6ff6;y@+is%x(IOQNL zt{(rXW=OY1r{~9a`86Qq^WnBbRl>d|L`@;ORJj2DP?;w^Ex>+y;XO;HA;X>8&;qUW zGNDPBB=?8g#(a-%QYWC;V$ zFKw+WDK?O!^QcU`$z@`U452q;TGXTjafgXWv@K#b^v13h(Z<9b0PJxFWEd^3OLHm; zw(XQXlT2_PF%#F}5T@+8wo-A|=&^2HmVa(axq$&%DfCB5a8=n`1!|_}tbS@E!ZJ^1 zf#WmjlYIP!jZ)N?u|#3Yi1pLW_=atSAZ*JPfj1+Ws$OG z313h8CQjD5E5DYY*531m^G~Q~8W@ZTfLo1r+wU*x6ot?&aoHDOfRuV$rTM2D$4hlV z{?HdA<8tY0lJU4~CvkF~x?ld7vA0EKn@@q|ZWfrr5)&K@avzS-D)aeii2Hxl{QR$SC}|sBR)4XPFAh@xs+mB}csE@A5$cWq0B-FI AKmY&$ literal 0 HcmV?d00001 diff --git a/apps/gpauth/icons/icon.png b/apps/gpauth/icons/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e1cd2619e0b5ec089cbba5ec7b03ddf2b1dfceb6 GIT binary patch literal 14183 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

5l#v3A#xI3*z~1~fvo9Q(N^(==!|_FZ z*duZ=+M1~)8E|otX8KNZlr?qels#x_1Xq@9IIw~@9uAREJVH)Xw^}UclF6327}E42 zT)E&?U%TK?(+K7%R!`H5oX0i)4Qn5??Iw3p5J~6_u+aWehY{DSn}3V2p$bgjnAu?o)v@iC254fXeMv50$9YrpU`N?u@QIWs)T?SP|fa}(|9 zqAX+!7`cx=4)cCBg5h~pu(?@9`)aCr#oyz$ld=#RFxYCNZCZls@4v2~*e-t6PEVvV z&bbK3b3wt(Coc!ufAbXXC<**#HQ%J9k`New6iG<5RjtO4XVO?dCvwxD{kJ#tfQr(X zg^NTwF-FwAeS_{V4bfel8l`~NbfrTR2s!G>WduFWxH(t~aK4q=6rEE^$+Uox>gJO2 z{L<;6Q6nHa5#ZEM>H58not!)z(6*_=^~8}jWf*IG$AUKVWOZ4?)GfF z+BM#*wKKmLFD7E~W3U!$IVm$k_k1f&Kz6WV8@55P?r~bcg-Za-!rvW?ns&)KOGT2~ zlkAyqhQj=P$Eg3w#K~}zH@J5bo-BfHjInKSz$@?+Z)NPD4pHj^_Qxmi`UqoTy=`sV zLVxrXGuBr=QRm|}wg75yetQQK4fY3#P_~J}zEfPnb2C4Wo!E(d*(cA;b?7$g2in<( zPn)ghX}nzJPmb6(3Dpeg_GW~Hc}Lt=lgsSZz z!5QXyz7KaR;D`3Ee}d`af{H>WWZ|Io1QI3~4Ll_`g1(cRnhLK73Ro)7zPCd={1W2x zRp%Xlvv4>!<2@}$hz|!V{T}_eHx2xkLl^hQoZTCnsjCl|W_@5Fx2(+j0ogy&Y+;L- z<)G$*CiN7hOm^s!{U>1F7U=iNk{+u~dAC!eDz%=|glFW0jEZU1&o(G_c#wTxUjnG} z#cg3>jEpUi#Mlq@t?Msg_#geK^Lx@DyHWf7=AS5vVyM7YOjvUVCfcpVR<(+5!H?9- zySI6s>o3m&*zr||=wcPGyBkQV`EWJl@bH8qobjOp+sXL*)=&yX)8aAbf~tGv?a2SN zu^Ddo-z?DWk9h9Yz#5p^NU#x~wYSd?H@w@!2Gb4G)6-utEMV~~M85Br5ff(v5O1|T z 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 + + + + + GlobalProtect Login + + +

Redirecting to GlobalProtect Login...

+ + diff --git a/apps/gpauth/src/auth_window.rs b/apps/gpauth/src/auth_window.rs new file mode 100644 index 0000000..11f3ae3 --- /dev/null +++ b/apps/gpauth/src/auth_window.rs @@ -0,0 +1,449 @@ +use std::{ + rc::Rc, + sync::Arc, + time::{Duration, Instant}, +}; + +use anyhow::bail; +use gpapi::{ + auth::SamlAuthData, + portal::{prelogin, Prelogin}, + utils::{redact::redact_uri, window::WindowExt}, +}; +use log::{info, warn}; +use regex::Regex; +use tauri::{AppHandle, Window, WindowEvent, WindowUrl}; +use tokio::sync::{mpsc, oneshot, RwLock}; +use tokio_util::sync::CancellationToken; +use webkit2gtk::{ + gio::Cancellable, + glib::{GString, TimeSpan}, + LoadEvent, URIResponse, URIResponseExt, WebContextExt, WebResource, WebResourceExt, WebView, + WebViewExt, WebsiteDataManagerExtManual, WebsiteDataTypes, +}; + +enum AuthDataError { + /// 1. Found auth data in headers/body but it's invalid + /// 2. Loaded an empty page, failed to load page. etc. + Invalid, + /// No auth data found in headers/body + NotFound, +} + +type AuthResult = Result; + +pub(crate) struct AuthWindow<'a> { + app_handle: AppHandle, + server: &'a str, + saml_request: &'a str, + user_agent: &'a str, + clean: bool, +} + +impl<'a> AuthWindow<'a> { + pub fn new(app_handle: AppHandle) -> Self { + Self { + app_handle, + server: "", + saml_request: "", + user_agent: "", + clean: false, + } + } + + pub fn server(mut self, server: &'a str) -> Self { + self.server = server; + self + } + + pub fn saml_request(mut self, saml_request: &'a str) -> Self { + self.saml_request = saml_request; + self + } + + pub fn user_agent(mut self, user_agent: &'a str) -> Self { + self.user_agent = user_agent; + self + } + + pub fn clean(mut self, clean: bool) -> Self { + self.clean = clean; + self + } + + pub async fn open(&self) -> anyhow::Result { + info!("Open auth window, user_agent: {}", self.user_agent); + + let window = Window::builder(&self.app_handle, "auth_window", WindowUrl::default()) + .title("GlobalProtect Login") + .user_agent(self.user_agent) + .focused(true) + .visible(false) + .center() + .build()?; + + let window = Arc::new(window); + + let cancel_token = CancellationToken::new(); + let cancel_token_clone = cancel_token.clone(); + + window.on_window_event(move |event| { + if let WindowEvent::CloseRequested { .. } = event { + cancel_token_clone.cancel(); + } + }); + + let window_clone = Arc::clone(&window); + let timeout_secs = 15; + tokio::spawn(async move { + tokio::time::sleep(Duration::from_secs(timeout_secs)).await; + let visible = window_clone.is_visible().unwrap_or(false); + if !visible { + info!("Try to raise auth window after {} seconds", timeout_secs); + raise_window(&window_clone); + } + }); + + tokio::select! { + _ = cancel_token.cancelled() => { + bail!("Auth cancelled"); + } + saml_result = self.auth_loop(&window) => { + window.close()?; + saml_result + } + } + } + + async fn auth_loop(&self, window: &Arc) -> anyhow::Result { + let saml_request = self.saml_request.to_string(); + let (auth_result_tx, mut auth_result_rx) = mpsc::unbounded_channel::(); + let raise_window_cancel_token: Arc>> = Default::default(); + + if self.clean { + clear_webview_cookies(window).await?; + } + + let raise_window_cancel_token_clone = Arc::clone(&raise_window_cancel_token); + window.with_webview(move |wv| { + let wv = wv.inner(); + + // Load the initial SAML request + load_saml_request(&wv, &saml_request); + + let auth_result_tx_clone = auth_result_tx.clone(); + wv.connect_load_changed(move |wv, event| { + if event == LoadEvent::Started { + let Ok(mut cancel_token) = raise_window_cancel_token_clone.try_write() else { + return; + }; + + // Cancel the raise window task + if let Some(cancel_token) = cancel_token.take() { + cancel_token.cancel(); + } + return; + } + + if event != LoadEvent::Finished { + return; + } + + if let Some(main_resource) = wv.main_resource() { + let uri = main_resource.uri().unwrap_or("".into()); + + if uri.is_empty() { + warn!("Loaded an empty uri"); + send_auth_result(&auth_result_tx_clone, Err(AuthDataError::Invalid)); + return; + } + + info!("Loaded uri: {}", redact_uri(&uri)); + read_auth_data(&main_resource, auth_result_tx_clone.clone()); + } + }); + + wv.connect_load_failed_with_tls_errors(|_wv, uri, cert, err| { + let redacted_uri = redact_uri(uri); + warn!( + "Failed to load uri: {} with error: {}, cert: {}", + redacted_uri, err, cert + ); + true + }); + + wv.connect_load_failed(move |_wv, _event, uri, err| { + let redacted_uri = redact_uri(uri); + warn!("Failed to load uri: {} with error: {}", redacted_uri, err); + send_auth_result(&auth_result_tx, Err(AuthDataError::Invalid)); + // true to stop other handlers from being invoked for the event. false to propagate the event further. + true + }); + })?; + + let portal = self.server.to_string(); + let user_agent = self.user_agent.to_string(); + + loop { + if let Some(auth_result) = auth_result_rx.recv().await { + match auth_result { + Ok(auth_data) => return Ok(auth_data), + Err(AuthDataError::NotFound) => { + info!("No auth data found, it may not be the /SAML20/SP/ACS endpoint"); + + // The user may need to interact with the auth window, raise it in 3 seconds + if !window.is_visible().unwrap_or(false) { + let window = Arc::clone(window); + let cancel_token = CancellationToken::new(); + + raise_window_cancel_token + .write() + .await + .replace(cancel_token.clone()); + + tokio::spawn(async move { + let delay_secs = 1; + + info!("Raise window in {} second(s)", delay_secs); + tokio::select! { + _ = tokio::time::sleep(Duration::from_secs(delay_secs)) => { + raise_window(&window); + } + _ = cancel_token.cancelled() => { + info!("Raise window cancelled"); + } + } + }); + } + } + Err(AuthDataError::Invalid) => { + info!("Got invalid auth data, retrying..."); + + window.with_webview(|wv| { + let wv = wv.inner(); + wv.run_javascript(r#" + var loading = document.createElement("div"); + loading.innerHTML = '
Got invalid token, retrying...
'; + loading.style = "position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255, 255, 255, 0.85); z-index: 99999;"; + document.body.appendChild(loading); + "#, + Cancellable::NONE, + |_| info!("Injected loading element successfully"), + ); + })?; + + let saml_request = portal_prelogin(&portal, &user_agent).await?; + window.with_webview(move |wv| { + let wv = wv.inner(); + load_saml_request(&wv, &saml_request); + })?; + } + } + } + } + } +} + +fn raise_window(window: &Arc) { + let visible = window.is_visible().unwrap_or(false); + if !visible { + if let Err(err) = window.raise() { + warn!("Failed to raise window: {}", err); + } + } +} + +pub(crate) async fn portal_prelogin(portal: &str, user_agent: &str) -> anyhow::Result { + info!("Portal prelogin..."); + match prelogin(portal, user_agent).await? { + Prelogin::Saml(prelogin) => Ok(prelogin.saml_request().to_string()), + Prelogin::Standard(_) => Err(anyhow::anyhow!("Received non-SAML prelogin response")), + } +} + +fn send_auth_result(auth_result_tx: &mpsc::UnboundedSender, auth_result: AuthResult) { + if let Err(err) = auth_result_tx.send(auth_result) { + warn!("Failed to send auth event: {}", err); + } +} + +fn load_saml_request(wv: &Rc, saml_request: &str) { + if saml_request.starts_with("http") { + info!("Load the SAML request as URI..."); + wv.load_uri(saml_request); + } else { + info!("Load the SAML request as HTML..."); + wv.load_html(saml_request, None); + } +} + +fn read_auth_data_from_headers(response: &URIResponse) -> AuthResult { + response.http_headers().map_or_else( + || { + info!("No headers found in response"); + Err(AuthDataError::NotFound) + }, + |mut headers| match headers.get("saml-auth-status") { + Some(status) if status == "1" => { + let username = headers.get("saml-username").map(GString::into); + let prelogin_cookie = headers.get("prelogin-cookie").map(GString::into); + let portal_userauthcookie = headers.get("portal-userauthcookie").map(GString::into); + + if SamlAuthData::check(&username, &prelogin_cookie, &portal_userauthcookie) { + return Ok(SamlAuthData::new( + username.unwrap(), + prelogin_cookie, + portal_userauthcookie, + )); + } + + info!("Found invalid auth data in headers"); + Err(AuthDataError::Invalid) + } + Some(status) => { + info!("Found invalid SAML status: {} in headers", status); + Err(AuthDataError::Invalid) + } + None => { + info!("No saml-auth-status header found"); + Err(AuthDataError::NotFound) + } + }, + ) +} + +fn read_auth_data_from_body(main_resource: &WebResource, callback: F) +where + F: FnOnce(AuthResult) + Send + 'static, +{ + main_resource.data(Cancellable::NONE, |data| match data { + Ok(data) => { + let html = String::from_utf8_lossy(&data); + callback(read_auth_data_from_html(&html)); + } + Err(err) => { + info!("Failed to read response body: {}", err); + callback(Err(AuthDataError::Invalid)) + } + }); +} + +fn read_auth_data_from_html(html: &str) -> AuthResult { + if html.contains("Temporarily Unavailable") { + info!("Found 'Temporarily Unavailable' in HTML, auth failed"); + return Err(AuthDataError::Invalid); + } + + match parse_xml_tag(html, "saml-auth-status") { + Some(saml_status) if saml_status == "1" => { + let username = parse_xml_tag(html, "saml-username"); + let prelogin_cookie = parse_xml_tag(html, "prelogin-cookie"); + let portal_userauthcookie = parse_xml_tag(html, "portal-userauthcookie"); + + if SamlAuthData::check(&username, &prelogin_cookie, &portal_userauthcookie) { + return Ok(SamlAuthData::new( + username.unwrap(), + prelogin_cookie, + portal_userauthcookie, + )); + } + + info!("Found invalid auth data in HTML"); + Err(AuthDataError::Invalid) + } + Some(status) => { + info!("Found invalid SAML status {} in HTML", status); + Err(AuthDataError::Invalid) + } + None => { + info!("No auth data found in HTML"); + Err(AuthDataError::NotFound) + } + } +} + +fn read_auth_data(main_resource: &WebResource, auth_result_tx: mpsc::UnboundedSender) { + if main_resource.response().is_none() { + info!("No response found in main resource"); + send_auth_result(&auth_result_tx, Err(AuthDataError::Invalid)); + return; + } + + let response = main_resource.response().unwrap(); + info!("Trying to read auth data from response headers..."); + + match read_auth_data_from_headers(&response) { + Ok(auth_data) => { + info!("Got auth data from headers"); + send_auth_result(&auth_result_tx, Ok(auth_data)); + } + Err(AuthDataError::Invalid) => { + info!("Found invalid auth data in headers, trying to read from body..."); + read_auth_data_from_body(main_resource, move |auth_result| { + // Since we have already found invalid auth data in headers, which means this could be the `/SAML20/SP/ACS` endpoint + // any error result from body should be considered as invalid, and trigger a retry + let auth_result = auth_result.map_err(|_| AuthDataError::Invalid); + send_auth_result(&auth_result_tx, auth_result); + }); + } + Err(AuthDataError::NotFound) => { + info!("No auth data found in headers, trying to read from body..."); + read_auth_data_from_body(main_resource, move |auth_result| { + send_auth_result(&auth_result_tx, auth_result) + }); + } + } +} + +fn parse_xml_tag(html: &str, tag: &str) -> Option { + let re = Regex::new(&format!("<{}>(.*)", tag, tag)).unwrap(); + re.captures(html) + .and_then(|captures| captures.get(1)) + .map(|m| m.as_str().to_string()) +} + +pub(crate) async fn clear_webview_cookies(window: &Window) -> anyhow::Result<()> { + let (tx, rx) = oneshot::channel::>(); + + window.with_webview(|wv| { + let send_result = move |result: Result<(), String>| { + if let Err(err) = tx.send(result) { + info!("Failed to send result: {:?}", err); + } + }; + + let wv = wv.inner(); + let context = match wv.context() { + Some(context) => context, + None => { + send_result(Err("No webview context found".into())); + return; + } + }; + let data_manager = match context.website_data_manager() { + Some(manager) => manager, + None => { + send_result(Err("No data manager found".into())); + return; + } + }; + + let now = Instant::now(); + data_manager.clear( + WebsiteDataTypes::COOKIES, + TimeSpan(0), + Cancellable::NONE, + move |result| match result { + Err(err) => { + send_result(Err(err.to_string())); + } + Ok(_) => { + info!("Cookies cleared in {} ms", now.elapsed().as_millis()); + send_result(Ok(())); + } + }, + ); + })?; + + rx.await?.map_err(|err| anyhow::anyhow!(err)) +} diff --git a/apps/gpauth/src/cli.rs b/apps/gpauth/src/cli.rs new file mode 100644 index 0000000..9caf6a2 --- /dev/null +++ b/apps/gpauth/src/cli.rs @@ -0,0 +1,138 @@ +use clap::Parser; +use gpapi::{ + auth::{SamlAuthData, SamlAuthResult}, + utils::{normalize_server, openssl}, + GP_USER_AGENT, +}; +use log::{info, LevelFilter}; +use serde_json::json; +use tauri::{App, AppHandle, RunEvent}; +use tempfile::NamedTempFile; + +use crate::auth_window::{portal_prelogin, AuthWindow}; + +const VERSION: &str = concat!( + env!("CARGO_PKG_VERSION"), + " (", + compile_time::date_str!(), + ")" +); + +#[derive(Parser, Clone)] +#[command(version = VERSION)] +struct Cli { + server: String, + #[arg(long)] + saml_request: Option, + #[arg(long, default_value = GP_USER_AGENT)] + user_agent: String, + #[arg(long)] + hidpi: bool, + #[arg(long)] + fix_openssl: bool, + #[arg(long)] + clean: bool, +} + +impl Cli { + async fn run(&mut self) -> anyhow::Result<()> { + let mut openssl_conf = self.prepare_env()?; + + self.server = normalize_server(&self.server)?; + // Get the initial SAML request + let saml_request = match self.saml_request { + Some(ref saml_request) => saml_request.clone(), + None => portal_prelogin(&self.server, &self.user_agent).await?, + }; + + self.saml_request.replace(saml_request); + + let app = create_app(self.clone())?; + + app.run(move |_app_handle, event| { + if let RunEvent::Exit = event { + if let Some(file) = openssl_conf.take() { + if let Err(err) = file.close() { + info!("Error closing OpenSSL config file: {}", err); + } + } + } + }); + + Ok(()) + } + + fn prepare_env(&self) -> anyhow::Result> { + std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1"); + + if self.hidpi { + info!("Setting GDK_SCALE=2 and GDK_DPI_SCALE=0.5"); + + std::env::set_var("GDK_SCALE", "2"); + std::env::set_var("GDK_DPI_SCALE", "0.5"); + } + + if self.fix_openssl { + info!("Fixing OpenSSL environment"); + let file = openssl::fix_openssl_env()?; + + return Ok(Some(file)); + } + + Ok(None) + } + + async fn saml_auth(&self, app_handle: AppHandle) -> anyhow::Result { + let auth_window = AuthWindow::new(app_handle) + .server(&self.server) + .user_agent(&self.user_agent) + .saml_request(self.saml_request.as_ref().unwrap()) + .clean(self.clean); + + auth_window.open().await + } +} + +fn create_app(cli: Cli) -> anyhow::Result { + let app = tauri::Builder::default() + .setup(|app| { + let app_handle = app.handle(); + + tauri::async_runtime::spawn(async move { + let auth_result = match cli.saml_auth(app_handle.clone()).await { + Ok(auth_data) => SamlAuthResult::Success(auth_data), + Err(err) => SamlAuthResult::Failure(format!("{}", err)), + }; + + println!("{}", json!(auth_result)); + }); + Ok(()) + }) + .build(tauri::generate_context!())?; + + Ok(app) +} + +fn init_logger() { + env_logger::builder().filter_level(LevelFilter::Info).init(); +} + +pub async fn run() { + let mut cli = Cli::parse(); + + init_logger(); + info!("gpauth started: {}", VERSION); + + if let Err(err) = cli.run().await { + eprintln!("\nError: {}", err); + + if err.to_string().contains("unsafe legacy renegotiation") && !cli.fix_openssl { + eprintln!("\nRe-run it with the `--fix-openssl` option to work around this issue, e.g.:\n"); + // Print the command + let args = std::env::args().collect::>(); + eprintln!("{} --fix-openssl {}\n", args[0], args[1..].join(" ")); + } + + std::process::exit(1); + } +} diff --git a/apps/gpauth/src/main.rs b/apps/gpauth/src/main.rs new file mode 100644 index 0000000..74f86ec --- /dev/null +++ b/apps/gpauth/src/main.rs @@ -0,0 +1,9 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +mod auth_window; +mod cli; + +#[tokio::main] +async fn main() { + cli::run().await; +} diff --git a/apps/gpauth/tauri.conf.json b/apps/gpauth/tauri.conf.json new file mode 100644 index 0000000..7569835 --- /dev/null +++ b/apps/gpauth/tauri.conf.json @@ -0,0 +1,47 @@ +{ + "$schema": "https://cdn.jsdelivr.net/gh/tauri-apps/tauri@tauri-v1.5.0/tooling/cli/schema.json", + "build": { + "distDir": [ + "index.html" + ], + "devPath": [ + "index.html" + ], + "beforeDevCommand": "", + "beforeBuildCommand": "", + "withGlobalTauri": false + }, + "package": { + "productName": "gpauth", + "version": "0.0.0" + }, + "tauri": { + "allowlist": { + "all": false, + "http": { + "all": true, + "request": true, + "scope": [ + "http://**", + "https://**" + ] + } + }, + "bundle": { + "active": true, + "targets": "deb", + "identifier": "com.yuezk.gpauth", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + }, + "security": { + "csp": null + }, + "windows": [] + } +} diff --git a/apps/gpclient/Cargo.toml b/apps/gpclient/Cargo.toml new file mode 100644 index 0000000..6e38bcc --- /dev/null +++ b/apps/gpclient/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "gpclient" +authors.workspace = true +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +gpapi = { path = "../../crates/gpapi" } +openconnect = { path = "../../crates/openconnect" } +anyhow.workspace = true +clap.workspace = true +env_logger.workspace = true +inquire = "0.6.2" +log.workspace = true +tokio.workspace = true +sysinfo.workspace = true +serde_json.workspace = true +whoami.workspace = true +tempfile.workspace = true +reqwest.workspace = true +directories = "5.0" +compile-time.workspace = true diff --git a/apps/gpclient/src/cli.rs b/apps/gpclient/src/cli.rs new file mode 100644 index 0000000..b1475a2 --- /dev/null +++ b/apps/gpclient/src/cli.rs @@ -0,0 +1,101 @@ +use clap::{Parser, Subcommand}; +use gpapi::utils::openssl; +use log::{info, LevelFilter}; +use tempfile::NamedTempFile; + +use crate::{ + connect::{ConnectArgs, ConnectHandler}, + disconnect::DisconnectHandler, + launch_gui::{LaunchGuiArgs, LaunchGuiHandler}, +}; + +const VERSION: &str = concat!( + env!("CARGO_PKG_VERSION"), + " (", + compile_time::date_str!(), + ")" +); + +#[derive(Subcommand)] +enum CliCommand { + #[command(about = "Connect to a portal server")] + Connect(ConnectArgs), + #[command(about = "Disconnect from the server")] + Disconnect, + #[command(about = "Launch the GUI")] + LaunchGui(LaunchGuiArgs), +} + +#[derive(Parser)] +#[command( + version = VERSION, + author, + about = "The GlobalProtect VPN client, based on OpenConnect, supports the SSO authentication method.", + help_template = "\ +{before-help}{name} {version} +{author} + +{about} + +{usage-heading} {usage} + +{all-args}{after-help} +" +)] +struct Cli { + #[command(subcommand)] + command: CliCommand, + + #[arg( + long, + help = "Get around the OpenSSL `unsafe legacy renegotiation` error" + )] + fix_openssl: bool, +} + +impl Cli { + fn fix_openssl(&self) -> anyhow::Result> { + if self.fix_openssl { + let file = openssl::fix_openssl_env()?; + return Ok(Some(file)); + } + + Ok(None) + } + + async fn run(&self) -> anyhow::Result<()> { + // The temp file will be dropped automatically when the file handle is dropped + // So, declare it here to ensure it's not dropped + let _file = self.fix_openssl()?; + + match &self.command { + CliCommand::Connect(args) => ConnectHandler::new(args, self.fix_openssl).handle().await, + CliCommand::Disconnect => DisconnectHandler::new().handle(), + CliCommand::LaunchGui(args) => LaunchGuiHandler::new(args).handle().await, + } + } +} + +fn init_logger() { + env_logger::builder().filter_level(LevelFilter::Info).init(); +} + +pub(crate) async fn run() { + let cli = Cli::parse(); + + init_logger(); + info!("gpclient started: {}", VERSION); + + if let Err(err) = cli.run().await { + eprintln!("\nError: {}", err); + + if err.to_string().contains("unsafe legacy renegotiation") && !cli.fix_openssl { + eprintln!("\nRe-run it with the `--fix-openssl` option to work around this issue, e.g.:\n"); + // Print the command + let args = std::env::args().collect::>(); + eprintln!("{} --fix-openssl {}\n", args[0], args[1..].join(" ")); + } + + std::process::exit(1); + } +} diff --git a/apps/gpclient/src/connect.rs b/apps/gpclient/src/connect.rs new file mode 100644 index 0000000..8a02d19 --- /dev/null +++ b/apps/gpclient/src/connect.rs @@ -0,0 +1,150 @@ +use std::{fs, sync::Arc}; + +use clap::Args; +use gpapi::{ + credential::{Credential, PasswordCredential}, + gateway::gateway_login, + gp_params::GpParams, + portal::{prelogin, retrieve_config, Prelogin}, + process::auth_launcher::SamlAuthLauncher, + utils::{self, shutdown_signal}, + GP_USER_AGENT, +}; +use inquire::{Password, PasswordDisplayMode, Select, Text}; +use log::info; +use openconnect::Vpn; + +use crate::GP_CLIENT_LOCK_FILE; + +#[derive(Args)] +pub(crate) struct ConnectArgs { + #[arg(help = "The portal server to connect to")] + server: String, + #[arg( + short, + long, + help = "The gateway to connect to, it will prompt if not specified" + )] + gateway: Option, + #[arg( + short, + long, + help = "The username to use, it will prompt if not specified" + )] + user: Option, + #[arg(long, short, help = "The VPNC script to use")] + script: Option, + #[arg(long, default_value = GP_USER_AGENT, help = "The user agent to use")] + user_agent: String, + #[arg(long, help = "The HiDPI mode, useful for high resolution screens")] + hidpi: bool, + #[arg(long, help = "Do not reuse the remembered authentication cookie")] + clean: bool, +} + +pub(crate) struct ConnectHandler<'a> { + args: &'a ConnectArgs, + fix_openssl: bool, +} + +impl<'a> ConnectHandler<'a> { + pub(crate) fn new(args: &'a ConnectArgs, fix_openssl: bool) -> Self { + Self { args, fix_openssl } + } + + pub(crate) async fn handle(&self) -> anyhow::Result<()> { + let portal = utils::normalize_server(self.args.server.as_str())?; + + let gp_params = GpParams::builder() + .user_agent(&self.args.user_agent) + .build(); + + let prelogin = prelogin(&portal, &self.args.user_agent).await?; + let portal_credential = self.obtain_portal_credential(&prelogin).await?; + let mut portal_config = retrieve_config(&portal, &portal_credential, &gp_params).await?; + + let selected_gateway = match &self.args.gateway { + Some(gateway) => portal_config + .find_gateway(gateway) + .ok_or_else(|| anyhow::anyhow!("Cannot find gateway {}", gateway))?, + None => { + portal_config.sort_gateways(prelogin.region()); + let gateways = portal_config.gateways(); + + if gateways.len() > 1 { + Select::new("Which gateway do you want to connect to?", gateways) + .with_vim_mode(true) + .prompt()? + } else { + gateways[0] + } + } + }; + + let gateway = selected_gateway.server(); + let cred = portal_config.auth_cookie().into(); + let token = gateway_login(gateway, &cred, &gp_params).await?; + + let vpn = Vpn::builder(gateway, &token) + .user_agent(self.args.user_agent.clone()) + .script(self.args.script.clone()) + .build(); + + let vpn = Arc::new(vpn); + let vpn_clone = vpn.clone(); + + // Listen for the interrupt signal in the background + tokio::spawn(async move { + shutdown_signal().await; + info!("Received the interrupt signal, disconnecting..."); + vpn_clone.disconnect(); + }); + + vpn.connect(write_pid_file); + + if fs::metadata(GP_CLIENT_LOCK_FILE).is_ok() { + info!("Removing PID file"); + fs::remove_file(GP_CLIENT_LOCK_FILE)?; + } + + Ok(()) + } + + async fn obtain_portal_credential(&self, prelogin: &Prelogin) -> anyhow::Result { + match prelogin { + Prelogin::Saml(prelogin) => { + SamlAuthLauncher::new(&self.args.server) + .user_agent(&self.args.user_agent) + .saml_request(prelogin.saml_request()) + .hidpi(self.args.hidpi) + .fix_openssl(self.fix_openssl) + .clean(self.args.clean) + .launch() + .await + } + Prelogin::Standard(prelogin) => { + println!("{}", prelogin.auth_message()); + + let user = self.args.user.as_ref().map_or_else( + || Text::new(&format!("{}:", prelogin.label_username())).prompt(), + |user| Ok(user.to_owned()), + )?; + let password = Password::new(&format!("{}:", prelogin.label_password())) + .without_confirmation() + .with_display_mode(PasswordDisplayMode::Masked) + .prompt()?; + + let password_cred = PasswordCredential::new(&user, &password); + + Ok(password_cred.into()) + } + } + } +} + +fn write_pid_file() { + let pid = std::process::id(); + + fs::write(GP_CLIENT_LOCK_FILE, pid.to_string()).unwrap(); + info!("Wrote PID {} to {}", pid, GP_CLIENT_LOCK_FILE); +} diff --git a/apps/gpclient/src/disconnect.rs b/apps/gpclient/src/disconnect.rs new file mode 100644 index 0000000..7318d61 --- /dev/null +++ b/apps/gpclient/src/disconnect.rs @@ -0,0 +1,31 @@ +use crate::GP_CLIENT_LOCK_FILE; +use log::{info, warn}; +use std::fs; +use sysinfo::{Pid, ProcessExt, Signal, System, SystemExt}; + +pub(crate) struct DisconnectHandler; + +impl DisconnectHandler { + pub(crate) fn new() -> Self { + Self + } + + pub(crate) fn handle(&self) -> anyhow::Result<()> { + if fs::metadata(GP_CLIENT_LOCK_FILE).is_err() { + warn!("PID file not found, maybe the client is not running"); + return Ok(()); + } + + let pid = fs::read_to_string(GP_CLIENT_LOCK_FILE)?; + let pid = pid.trim().parse::()?; + let s = System::new_all(); + + if let Some(process) = s.process(Pid::from(pid)) { + info!("Found process {}, killing...", pid); + if process.kill_with(Signal::Interrupt).is_none() { + warn!("Failed to kill process {}", pid); + } + } + Ok(()) + } +} diff --git a/apps/gpclient/src/launch_gui.rs b/apps/gpclient/src/launch_gui.rs new file mode 100644 index 0000000..e8f8468 --- /dev/null +++ b/apps/gpclient/src/launch_gui.rs @@ -0,0 +1,88 @@ +use std::{collections::HashMap, fs, path::PathBuf}; + +use clap::Args; +use directories::ProjectDirs; +use gpapi::{ + process::service_launcher::ServiceLauncher, + utils::{endpoint::http_endpoint, env_file, shutdown_signal}, +}; +use log::info; + +#[derive(Args)] +pub(crate) struct LaunchGuiArgs { + #[clap(long, help = "Launch the GUI minimized")] + minimized: bool, +} + +pub(crate) struct LaunchGuiHandler<'a> { + args: &'a LaunchGuiArgs, +} + +impl<'a> LaunchGuiHandler<'a> { + pub(crate) fn new(args: &'a LaunchGuiArgs) -> Self { + Self { args } + } + + pub(crate) async fn handle(&self) -> anyhow::Result<()> { + // `launch-gui`cannot be run as root + let user = whoami::username(); + if user == "root" { + anyhow::bail!("`launch-gui` cannot be run as root"); + } + + if try_active_gui().await.is_ok() { + info!("The GUI is already running"); + return Ok(()); + } + + tokio::spawn(async move { + shutdown_signal().await; + info!("Shutting down..."); + }); + + let log_file = get_log_file()?; + let log_file_path = log_file.to_string_lossy().to_string(); + + info!("Log file: {}", log_file_path); + + let mut extra_envs = HashMap::::new(); + extra_envs.insert("GP_LOG_FILE".into(), log_file_path.clone()); + + // Persist the environment variables to a file + let env_file = env_file::persist_env_vars(Some(extra_envs))?; + let env_file = env_file.into_temp_path(); + let env_file_path = env_file.to_string_lossy().to_string(); + + let exit_status = ServiceLauncher::new() + .minimized(self.args.minimized) + .env_file(&env_file_path) + .log_file(&log_file_path) + .launch() + .await?; + + info!("Service exited with status: {}", exit_status); + + Ok(()) + } +} + +async fn try_active_gui() -> anyhow::Result<()> { + let service_endpoint = http_endpoint().await?; + + reqwest::Client::default() + .post(format!("{}/active-gui", service_endpoint)) + .send() + .await? + .error_for_status()?; + + Ok(()) +} + +pub fn get_log_file() -> anyhow::Result { + let dirs = ProjectDirs::from("com.yuezk", "GlobalProtect-openconnect", "gpclient") + .ok_or_else(|| anyhow::anyhow!("Failed to get project dirs"))?; + + fs::create_dir_all(dirs.data_dir())?; + + Ok(dirs.data_dir().join("gpclient.log")) +} diff --git a/apps/gpclient/src/main.rs b/apps/gpclient/src/main.rs new file mode 100644 index 0000000..41343ed --- /dev/null +++ b/apps/gpclient/src/main.rs @@ -0,0 +1,11 @@ +mod cli; +mod connect; +mod disconnect; +mod launch_gui; + +pub(crate) const GP_CLIENT_LOCK_FILE: &str = "/var/run/gpclient.lock"; + +#[tokio::main] +async fn main() { + cli::run().await; +} diff --git a/apps/gpservice/Cargo.toml b/apps/gpservice/Cargo.toml new file mode 100644 index 0000000..1ac8ead --- /dev/null +++ b/apps/gpservice/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "gpservice" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +gpapi = { path = "../../crates/gpapi" } +openconnect = { path = "../../crates/openconnect" } +clap.workspace = true +anyhow.workspace = true +tokio.workspace = true +tokio-util.workspace = true +axum = { workspace = true, features = ["ws"] } +futures.workspace = true +serde_json.workspace = true +env_logger.workspace = true +log.workspace = true +compile-time.workspace = true diff --git a/apps/gpservice/com.yuezk.gpservice.policy b/apps/gpservice/com.yuezk.gpservice.policy new file mode 100644 index 0000000..3c2128f --- /dev/null +++ b/apps/gpservice/com.yuezk.gpservice.policy @@ -0,0 +1,19 @@ + + + + GlobalProtect-openconnect + https://github.com/yuezk/GlobalProtect-openconnect + gpgui + + Run GPService as root + Authentication is required to run the GPService as root + + yes + yes + yes + + /home/kevin/Documents/repos/gp/target/debug/gpservice + --with-gui + true + + diff --git a/apps/gpservice/src/cli.rs b/apps/gpservice/src/cli.rs new file mode 100644 index 0000000..9c0435e --- /dev/null +++ b/apps/gpservice/src/cli.rs @@ -0,0 +1,182 @@ +use std::sync::Arc; +use std::{collections::HashMap, io::Write}; + +use anyhow::bail; +use clap::Parser; +use gpapi::{ + process::gui_launcher::GuiLauncher, + service::{request::WsRequest, vpn_state::VpnState}, + utils::{ + crypto::generate_key, env_file, lock_file::LockFile, redact::Redaction, shutdown_signal, + }, + GP_SERVICE_LOCK_FILE, +}; +use log::{info, warn, LevelFilter}; +use tokio::sync::{mpsc, watch}; + +use crate::{vpn_task::VpnTask, ws_server::WsServer}; + +const VERSION: &str = concat!( + env!("CARGO_PKG_VERSION"), + " (", + compile_time::date_str!(), + ")" +); + +#[derive(Parser)] +#[command(version = VERSION)] +struct Cli { + #[clap(long)] + minimized: bool, + #[clap(long)] + env_file: Option, + #[cfg(debug_assertions)] + #[clap(long)] + no_gui: bool, +} + +impl Cli { + async fn run(&mut self, redaction: Arc) -> anyhow::Result<()> { + let lock_file = Arc::new(LockFile::new(GP_SERVICE_LOCK_FILE)); + + if lock_file.check_health().await { + bail!("Another instance of the service is already running"); + } + + let api_key = self.prepare_api_key(); + + // Channel for sending requests to the VPN task + let (ws_req_tx, ws_req_rx) = mpsc::channel::(32); + // Channel for receiving the VPN state from the VPN task + let (vpn_state_tx, vpn_state_rx) = watch::channel(VpnState::Disconnected); + + let mut vpn_task = VpnTask::new(ws_req_rx, vpn_state_tx); + let ws_server = WsServer::new( + api_key.clone(), + ws_req_tx, + vpn_state_rx, + lock_file.clone(), + redaction, + ); + + let (shutdown_tx, mut shutdown_rx) = mpsc::channel::<()>(4); + let shutdown_tx_clone = shutdown_tx.clone(); + let vpn_task_token = vpn_task.cancel_token(); + let server_token = ws_server.cancel_token(); + + let vpn_task_handle = tokio::spawn(async move { vpn_task.start(server_token).await }); + let ws_server_handle = tokio::spawn(async move { ws_server.start(shutdown_tx_clone).await }); + + #[cfg(debug_assertions)] + let no_gui = self.no_gui; + + #[cfg(not(debug_assertions))] + let no_gui = false; + + if no_gui { + info!("GUI is disabled"); + } else { + let envs = self + .env_file + .as_ref() + .map(env_file::load_env_vars) + .transpose()?; + + let minimized = self.minimized; + + tokio::spawn(async move { + launch_gui(envs, api_key, minimized).await; + let _ = shutdown_tx.send(()).await; + }); + } + + tokio::select! { + _ = shutdown_signal() => { + info!("Shutdown signal received"); + } + _ = shutdown_rx.recv() => { + info!("Shutdown request received, shutting down"); + } + } + + vpn_task_token.cancel(); + let _ = tokio::join!(vpn_task_handle, ws_server_handle); + + lock_file.unlock()?; + + info!("gpservice stopped"); + + Ok(()) + } + + fn prepare_api_key(&self) -> Vec { + #[cfg(debug_assertions)] + if self.no_gui { + return gpapi::GP_API_KEY.to_vec(); + } + + generate_key().to_vec() + } +} + +fn init_logger() -> Arc { + let redaction = Arc::new(Redaction::new()); + let redaction_clone = Arc::clone(&redaction); + // let target = Box::new(File::create("log.txt").expect("Can't create file")); + env_logger::builder() + .filter_level(LevelFilter::Info) + .format(move |buf, record| { + let timestamp = buf.timestamp(); + writeln!( + buf, + "[{} {} {}] {}", + timestamp, + record.level(), + record.module_path().unwrap_or_default(), + redaction_clone.redact_str(&record.args().to_string()) + ) + }) + // .target(env_logger::Target::Pipe(target)) + .init(); + + redaction +} + +async fn launch_gui(envs: Option>, api_key: Vec, mut minimized: bool) { + loop { + let api_key_clone = api_key.clone(); + let gui_launcher = GuiLauncher::new() + .envs(envs.clone()) + .api_key(api_key_clone) + .minimized(minimized); + + match gui_launcher.launch().await { + Ok(exit_status) => { + // Exit code 99 means that the GUI needs to be restarted + if exit_status.code() != Some(99) { + info!("GUI exited with code {:?}", exit_status.code()); + break; + } + + info!("GUI exited with code 99, restarting"); + minimized = false; + } + Err(err) => { + warn!("Failed to launch GUI: {}", err); + break; + } + } + } +} + +pub async fn run() { + let mut cli = Cli::parse(); + + let redaction = init_logger(); + info!("gpservice started: {}", VERSION); + + if let Err(e) = cli.run(redaction).await { + eprintln!("Error: {}", e); + std::process::exit(1); + } +} diff --git a/apps/gpservice/src/handlers.rs b/apps/gpservice/src/handlers.rs new file mode 100644 index 0000000..4f90b06 --- /dev/null +++ b/apps/gpservice/src/handlers.rs @@ -0,0 +1,94 @@ +use std::{borrow::Cow, ops::ControlFlow, sync::Arc}; + +use axum::{ + extract::{ + ws::{self, CloseFrame, Message, WebSocket}, + State, WebSocketUpgrade, + }, + response::IntoResponse, +}; +use futures::{SinkExt, StreamExt}; +use gpapi::service::event::WsEvent; +use log::{info, warn}; + +use crate::ws_server::WsServerContext; + +pub(crate) async fn health() -> impl IntoResponse { + "OK" +} + +pub(crate) async fn active_gui(State(ctx): State>) -> impl IntoResponse { + ctx.send_event(WsEvent::ActiveGui).await; +} + +pub(crate) async fn ws_handler( + ws: WebSocketUpgrade, + State(ctx): State>, +) -> impl IntoResponse { + ws.on_upgrade(move |socket| handle_socket(socket, ctx)) +} + +async fn handle_socket(mut socket: WebSocket, ctx: Arc) { + // Send ping message + if let Err(err) = socket.send(Message::Ping("Hi".into())).await { + warn!("Failed to send ping: {}", err); + return; + } + + // Wait for pong message + if socket.recv().await.is_none() { + warn!("Failed to receive pong"); + return; + } + + info!("New client connected"); + + let (mut sender, mut receiver) = socket.split(); + let (connection, mut msg_rx) = ctx.add_connection().await; + + let send_task = tokio::spawn(async move { + while let Some(msg) = msg_rx.recv().await { + if let Err(err) = sender.send(msg).await { + info!("Failed to send message: {}", err); + break; + } + } + + let close_msg = Message::Close(Some(CloseFrame { + code: ws::close_code::NORMAL, + reason: Cow::from("Goodbye"), + })); + + if let Err(err) = sender.send(close_msg).await { + warn!("Failed to close socket: {}", err); + } + }); + + let conn = Arc::clone(&connection); + let ctx_clone = Arc::clone(&ctx); + let recv_task = tokio::spawn(async move { + while let Some(Ok(msg)) = receiver.next().await { + let ControlFlow::Continue(ws_req) = conn.recv_msg(msg) else { + break; + }; + + if let Err(err) = ctx_clone.forward_req(ws_req).await { + info!("Failed to forward request: {}", err); + break; + } + } + }); + + tokio::select! { + _ = send_task => { + info!("WS server send task completed"); + }, + _ = recv_task => { + info!("WS server recv task completed"); + } + } + + info!("Client disconnected"); + + ctx.remove_connection(connection).await; +} diff --git a/apps/gpservice/src/main.rs b/apps/gpservice/src/main.rs new file mode 100644 index 0000000..aa7daa3 --- /dev/null +++ b/apps/gpservice/src/main.rs @@ -0,0 +1,11 @@ +mod cli; +mod handlers; +mod routes; +mod vpn_task; +mod ws_server; +mod ws_connection; + +#[tokio::main] +async fn main() { + cli::run().await; +} diff --git a/apps/gpservice/src/routes.rs b/apps/gpservice/src/routes.rs new file mode 100644 index 0000000..780b11f --- /dev/null +++ b/apps/gpservice/src/routes.rs @@ -0,0 +1,13 @@ +use std::sync::Arc; + +use axum::{routing::{get, post}, Router}; + +use crate::{handlers, ws_server::WsServerContext}; + +pub(crate) fn routes(ctx: Arc) -> Router { + Router::new() + .route("/health", get(handlers::health)) + .route("/active-gui", post(handlers::active_gui)) + .route("/ws", get(handlers::ws_handler)) + .with_state(ctx) +} diff --git a/apps/gpservice/src/vpn_task.rs b/apps/gpservice/src/vpn_task.rs new file mode 100644 index 0000000..55424a1 --- /dev/null +++ b/apps/gpservice/src/vpn_task.rs @@ -0,0 +1,144 @@ +use std::{sync::Arc, thread}; + +use gpapi::service::{ + request::{ConnectRequest, WsRequest}, + vpn_state::VpnState, +}; +use log::info; +use openconnect::Vpn; +use tokio::sync::{mpsc, oneshot, watch, RwLock}; +use tokio_util::sync::CancellationToken; + +pub(crate) struct VpnTaskContext { + vpn_handle: Arc>>, + vpn_state_tx: Arc>, + disconnect_rx: RwLock>>, +} + +impl VpnTaskContext { + pub fn new(vpn_state_tx: watch::Sender) -> Self { + Self { + vpn_handle: Default::default(), + vpn_state_tx: Arc::new(vpn_state_tx), + disconnect_rx: Default::default(), + } + } + + pub async fn connect(&self, req: ConnectRequest) { + let vpn_state = self.vpn_state_tx.borrow().clone(); + if !matches!(vpn_state, VpnState::Disconnected) { + info!("VPN is not disconnected, ignore the request"); + return; + } + + let info = req.info().clone(); + let vpn_handle = self.vpn_handle.clone(); + let args = req.args(); + let vpn = Vpn::builder(req.gateway().server(), args.cookie()) + .user_agent(args.user_agent()) + .script(args.vpnc_script()) + .os(args.openconnect_os()) + .build(); + + // Save the VPN handle + vpn_handle.write().await.replace(vpn); + + let vpn_state_tx = self.vpn_state_tx.clone(); + let connect_info = Box::new(info.clone()); + vpn_state_tx.send(VpnState::Connecting(connect_info)).ok(); + + let (disconnect_tx, disconnect_rx) = oneshot::channel::<()>(); + self.disconnect_rx.write().await.replace(disconnect_rx); + + // Spawn a new thread to process the VPN connection, cannot use tokio::spawn here. + // Otherwise, it will block the tokio runtime and cannot send the VPN state to the channel + thread::spawn(move || { + let vpn_state_tx_clone = vpn_state_tx.clone(); + + vpn_handle.blocking_read().as_ref().map(|vpn| { + vpn.connect(move || { + let connect_info = Box::new(info.clone()); + vpn_state_tx.send(VpnState::Connected(connect_info)).ok(); + }) + }); + + // Notify the VPN is disconnected + vpn_state_tx_clone.send(VpnState::Disconnected).ok(); + // Remove the VPN handle + vpn_handle.blocking_write().take(); + + disconnect_tx.send(()).ok(); + }); + } + + pub async fn disconnect(&self) { + if let Some(disconnect_rx) = self.disconnect_rx.write().await.take() { + if let Some(vpn) = self.vpn_handle.read().await.as_ref() { + self.vpn_state_tx.send(VpnState::Disconnecting).ok(); + vpn.disconnect() + } + // Wait for the VPN to be disconnected + disconnect_rx.await.ok(); + info!("VPN disconnected"); + } else { + info!("VPN is not connected, skip disconnect"); + self.vpn_state_tx.send(VpnState::Disconnected).ok(); + } + } +} + +pub(crate) struct VpnTask { + ws_req_rx: mpsc::Receiver, + ctx: Arc, + cancel_token: CancellationToken, +} + +impl VpnTask { + pub fn new(ws_req_rx: mpsc::Receiver, vpn_state_tx: watch::Sender) -> Self { + let ctx = Arc::new(VpnTaskContext::new(vpn_state_tx)); + let cancel_token = CancellationToken::new(); + + Self { + ws_req_rx, + ctx, + cancel_token, + } + } + + pub fn cancel_token(&self) -> CancellationToken { + self.cancel_token.clone() + } + + pub async fn start(&mut self, server_cancel_token: CancellationToken) { + let cancel_token = self.cancel_token.clone(); + + tokio::select! { + _ = self.recv() => { + info!("VPN task stopped"); + } + _ = cancel_token.cancelled() => { + info!("VPN task cancelled"); + self.ctx.disconnect().await; + } + } + + server_cancel_token.cancel(); + } + + async fn recv(&mut self) { + while let Some(req) = self.ws_req_rx.recv().await { + tokio::spawn(process_ws_req(req, self.ctx.clone())); + } + } +} + +async fn process_ws_req(req: WsRequest, ctx: Arc) { + match req { + WsRequest::Connect(req) => { + ctx.connect(*req).await; + } + WsRequest::Disconnect(_) => { + ctx.disconnect().await; + } + } +} diff --git a/apps/gpservice/src/ws_connection.rs b/apps/gpservice/src/ws_connection.rs new file mode 100644 index 0000000..13c7f6c --- /dev/null +++ b/apps/gpservice/src/ws_connection.rs @@ -0,0 +1,53 @@ +use std::{ops::ControlFlow, sync::Arc}; + +use axum::extract::ws::{CloseFrame, Message}; +use gpapi::{ + service::{event::WsEvent, request::WsRequest}, + utils::crypto::Crypto, +}; +use log::{info, warn}; +use tokio::sync::mpsc; + +pub(crate) struct WsConnection { + crypto: Arc, + tx: mpsc::Sender, +} + +impl WsConnection { + pub fn new(crypto: Arc, tx: mpsc::Sender) -> Self { + Self { crypto, tx } + } + + pub async fn send_event(&self, event: &WsEvent) -> anyhow::Result<()> { + let encrypted = self.crypto.encrypt(event)?; + let msg = Message::Binary(encrypted); + + self.tx.send(msg).await?; + + Ok(()) + } + + pub fn recv_msg(&self, msg: Message) -> ControlFlow<(), WsRequest> { + match msg { + Message::Binary(data) => match self.crypto.decrypt(data) { + Ok(ws_req) => ControlFlow::Continue(ws_req), + Err(err) => { + info!("Failed to decrypt message: {}", err); + ControlFlow::Break(()) + } + }, + Message::Close(cf) => { + if let Some(CloseFrame { code, reason }) = cf { + info!("Client sent close, code {} and reason `{}`", code, reason); + } else { + info!("Client somehow sent close message without CloseFrame"); + } + ControlFlow::Break(()) + } + _ => { + warn!("WS server received unexpected message: {:?}", msg); + ControlFlow::Break(()) + } + } + } +} diff --git a/apps/gpservice/src/ws_server.rs b/apps/gpservice/src/ws_server.rs new file mode 100644 index 0000000..8f09542 --- /dev/null +++ b/apps/gpservice/src/ws_server.rs @@ -0,0 +1,158 @@ +use std::sync::Arc; + +use axum::extract::ws::Message; +use gpapi::{ + service::{event::WsEvent, request::WsRequest, vpn_state::VpnState}, + utils::{crypto::Crypto, lock_file::LockFile, redact::Redaction}, +}; +use log::{info, warn}; +use tokio::{ + net::TcpListener, + sync::{mpsc, watch, RwLock}, +}; +use tokio_util::sync::CancellationToken; + +use crate::{routes, ws_connection::WsConnection}; + +pub(crate) struct WsServerContext { + crypto: Arc, + ws_req_tx: mpsc::Sender, + vpn_state_rx: watch::Receiver, + redaction: Arc, + connections: RwLock>>, +} + +impl WsServerContext { + pub fn new( + api_key: Vec, + ws_req_tx: mpsc::Sender, + vpn_state_rx: watch::Receiver, + redaction: Arc, + ) -> Self { + Self { + crypto: Arc::new(Crypto::new(api_key)), + ws_req_tx, + vpn_state_rx, + redaction, + connections: Default::default(), + } + } + + pub async fn send_event(&self, event: WsEvent) { + let connections = self.connections.read().await; + + for conn in connections.iter() { + let _ = conn.send_event(&event).await; + } + } + + pub async fn add_connection(&self) -> (Arc, mpsc::Receiver) { + let (tx, rx) = mpsc::channel::(32); + let conn = Arc::new(WsConnection::new(Arc::clone(&self.crypto), tx)); + + // Send current VPN state to new client + info!("Sending current VPN state to new client"); + let vpn_state = self.vpn_state_rx.borrow().clone(); + if let Err(err) = conn.send_event(&WsEvent::VpnState(vpn_state)).await { + warn!("Failed to send VPN state to new client: {}", err); + } + + self.connections.write().await.push(Arc::clone(&conn)); + + (conn, rx) + } + + pub async fn remove_connection(&self, conn: Arc) { + let mut connections = self.connections.write().await; + connections.retain(|c| !Arc::ptr_eq(c, &conn)); + } + + fn vpn_state_rx(&self) -> watch::Receiver { + self.vpn_state_rx.clone() + } + + pub async fn forward_req(&self, req: WsRequest) -> anyhow::Result<()> { + if let WsRequest::Connect(ref req) = req { + self + .redaction + .add_values(&[req.gateway().server(), req.args().cookie()])? + } + + self.ws_req_tx.send(req).await?; + + Ok(()) + } +} + +pub(crate) struct WsServer { + ctx: Arc, + cancel_token: CancellationToken, + lock_file: Arc, +} + +impl WsServer { + pub fn new( + api_key: Vec, + ws_req_tx: mpsc::Sender, + vpn_state_rx: watch::Receiver, + lock_file: Arc, + redaction: Arc, + ) -> Self { + let ctx = Arc::new(WsServerContext::new( + api_key, + ws_req_tx, + vpn_state_rx, + redaction, + )); + let cancel_token = CancellationToken::new(); + + Self { + ctx, + cancel_token, + lock_file, + } + } + + pub fn cancel_token(&self) -> CancellationToken { + self.cancel_token.clone() + } + + pub async fn start(&self, shutdown_tx: mpsc::Sender<()>) { + if let Ok(listener) = TcpListener::bind("127.0.0.1:0").await { + let local_addr = listener.local_addr().unwrap(); + + self.lock_file.lock(local_addr.port().to_string()).unwrap(); + + info!("WS server listening on port: {}", local_addr.port()); + + tokio::select! { + _ = watch_vpn_state(self.ctx.vpn_state_rx(), Arc::clone(&self.ctx)) => { + info!("VPN state watch task completed"); + } + _ = start_server(listener, self.ctx.clone()) => { + info!("WS server stopped"); + } + _ = self.cancel_token.cancelled() => { + info!("WS server cancelled"); + } + } + } + + let _ = shutdown_tx.send(()).await; + } +} + +async fn watch_vpn_state(mut vpn_state_rx: watch::Receiver, ctx: Arc) { + while vpn_state_rx.changed().await.is_ok() { + let vpn_state = vpn_state_rx.borrow().clone(); + ctx.send_event(WsEvent::VpnState(vpn_state)).await; + } +} + +async fn start_server(listener: TcpListener, ctx: Arc) -> anyhow::Result<()> { + let routes = routes::routes(ctx); + + axum::serve(listener, routes).await?; + + Ok(()) +} diff --git a/cmake/Add3rdParty.cmake b/cmake/Add3rdParty.cmake deleted file mode 100644 index 3809a14..0000000 --- a/cmake/Add3rdParty.cmake +++ /dev/null @@ -1,27 +0,0 @@ -include(ExternalProject) - -function(add_3rdparty NAME) - set(oneValueArgs GIT_REPOSITORY GIT_TAG) - cmake_parse_arguments(add_3rdparty_args "" "${oneValueArgs}" "" ${ARGN}) - - if(EXISTS "${CMAKE_SOURCE_DIR}/3rdparty/${NAME}/CMakeLists.txt") - message(STATUS "Found third party locally for ${NAME}") - - ExternalProject_Add( - ${NAME}-${PROJECT_NAME} - PREFIX ${CMAKE_CURRENT_BINARY_DIR}/${NAME} - SOURCE_DIR "${CMAKE_SOURCE_DIR}/3rdparty/${NAME}" - INSTALL_COMMAND "" - "${add_3rdparty_args_UNPARSED_ARGUMENTS}" - ) - return() - endif() - - message(STATUS "Using ExternalProject to download ${NAME}") - ExternalProject_Add( - ${NAME}-${PROJECT_NAME} - PREFIX ${CMAKE_CURRENT_BINARY_DIR}/${NAME} - INSTALL_COMMAND "" - "${ARGN}" - ) -endfunction() diff --git a/cmake/FindNetworkManager.cmake b/cmake/FindNetworkManager.cmake deleted file mode 100644 index 14da5d8..0000000 --- a/cmake/FindNetworkManager.cmake +++ /dev/null @@ -1,59 +0,0 @@ -# - Try to find NetworkManager -# Once done this will define -# -# NETWORKMANAGER_FOUND - system has NetworkManager -# NETWORKMANAGER_INCLUDE_DIRS - the NetworkManager include directories -# NETWORKMANAGER_LIBRARIES - the libraries needed to use NetworkManager -# NETWORKMANAGER_CFLAGS - Compiler switches required for using NetworkManager -# NETWORKMANAGER_VERSION - version number of NetworkManager - -# Copyright (c) 2006, Alexander Neundorf, -# Copyright (c) 2007, Will Stephenson, -# Copyright (c) 2015-2018, Jan Grulich, - -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# 3. Neither the name of the University nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. - -IF (NETWORKMANAGER_INCLUDE_DIRS) - # in cache already - SET(NetworkManager_FIND_QUIETLY TRUE) -ENDIF (NETWORKMANAGER_INCLUDE_DIRS) - -IF (NOT WIN32) - find_package(PkgConfig) - PKG_SEARCH_MODULE(NETWORKMANAGER libnm) - IF (NETWORKMANAGER_FOUND) - IF (NetworkManager_FIND_VERSION AND ("${NETWORKMANAGER_VERSION}" VERSION_LESS "${NetworkManager_FIND_VERSION}")) - MESSAGE(FATAL_ERROR "NetworkManager ${NETWORKMANAGER_VERSION} is too old, need at least ${NetworkManager_FIND_VERSION}") - ELSE () - IF (NOT NetworkManager_FIND_QUIETLY) - MESSAGE(STATUS "Found NetworkManager: ${NETWORKMANAGER_LIBRARY_DIRS}") - ENDIF () - ENDIF () - ELSE () - MESSAGE(FATAL_ERROR "Could NOT find NetworkManager, check FindPkgConfig output above!") - ENDIF () -ENDIF (NOT WIN32) - -MARK_AS_ADVANCED(NETWORKMANAGER_INCLUDE_DIRS) diff --git a/cmakew b/cmakew deleted file mode 100755 index 2cdacc2..0000000 --- a/cmakew +++ /dev/null @@ -1,102 +0,0 @@ -#!/bin/bash - -cmake_version="3.21.2" - -arr_cmake_v=(${cmake_version//./ }) -cmake_version_major=(${arr_cmake_v[0]}) -cmake_version_minor=(${arr_cmake_v[1]}) -cmake_version_patch=(${arr_cmake_v[2]}) - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false - -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - -cmake_base="./.cmake" -cmake_bin="${cmake_base}/cmake-$cmake_version/bin/cmake" - -# download cmake if necessary -if [ ! -f "$cmake_bin" ]; then - download_link="" - - if [ "$darwin" = true ]; then - download_link="https://cmake.org/files/v$cmake_version_major.$cmake_version_minor/cmake-$cmake_version-Darwin-x86_64.tar.gz" - else - download_link="https://github.com/Kitware/CMake/releases/download/v${cmake_version}/cmake-${cmake_version}-linux-x86_64.tar.gz" - fi - - wget -nv --show-progress "$download_link" -O "/tmp/cmake-$cmake_version.tar.gz" - mkdir -p "${cmake_base}/cmake-$cmake_version" - tar -xzf "/tmp/cmake-$cmake_version.tar.gz" -C "${cmake_base}/cmake-$cmake_version" --strip-components=1 - rm "/tmp/cmake-$cmake_version.tar.gz" -fi - -# We build the pattern for arguments to be converted via cygpath -if [ "$cygwin" = true ]; then - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - - OURCYGPATTERN="(^($ROOTDIRS))" - - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - - i=$((i+1)) - done - - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# run cmake -exec "$cmake_bin" "$@" diff --git a/crates/gpapi/Cargo.toml b/crates/gpapi/Cargo.toml new file mode 100644 index 0000000..b5f252a --- /dev/null +++ b/crates/gpapi/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "gpapi" +version.workspace = true +edition.workspace = true +license = "MIT" + +[dependencies] +anyhow.workspace = true +base64.workspace = true +log.workspace = true +reqwest.workspace = true +roxmltree.workspace = true +serde.workspace = true +specta.workspace = true +specta-macros.workspace = true +urlencoding.workspace = true +tokio.workspace = true +serde_json.workspace = true +whoami.workspace = true +tempfile.workspace = true +thiserror.workspace = true +chacha20poly1305 = { version = "0.10", features = ["std"] } +redact-engine.workspace = true +url.workspace = true +regex.workspace = true +dotenvy_macro.workspace = true +users.workspace = true + +tauri = { workspace = true, optional = true } + +[features] +tauri = ["dep:tauri"] diff --git a/crates/gpapi/src/auth.rs b/crates/gpapi/src/auth.rs new file mode 100644 index 0000000..0e44666 --- /dev/null +++ b/crates/gpapi/src/auth.rs @@ -0,0 +1,63 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SamlAuthData { + username: String, + prelogin_cookie: Option, + portal_userauthcookie: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum SamlAuthResult { + Success(SamlAuthData), + Failure(String), +} + +impl SamlAuthResult { + pub fn is_success(&self) -> bool { + match self { + SamlAuthResult::Success(_) => true, + SamlAuthResult::Failure(_) => false, + } + } +} + +impl SamlAuthData { + pub fn new( + username: String, + prelogin_cookie: Option, + portal_userauthcookie: Option, + ) -> Self { + Self { + username, + prelogin_cookie, + portal_userauthcookie, + } + } + + pub fn username(&self) -> &str { + &self.username + } + + pub fn prelogin_cookie(&self) -> Option<&str> { + self.prelogin_cookie.as_deref() + } + + pub fn check( + username: &Option, + prelogin_cookie: &Option, + portal_userauthcookie: &Option, + ) -> bool { + let username_valid = username + .as_ref() + .is_some_and(|username| !username.is_empty()); + let prelogin_cookie_valid = prelogin_cookie.as_ref().is_some_and(|val| val.len() > 5); + let portal_userauthcookie_valid = portal_userauthcookie + .as_ref() + .is_some_and(|val| val.len() > 5); + + username_valid && (prelogin_cookie_valid || portal_userauthcookie_valid) + } +} diff --git a/crates/gpapi/src/credential.rs b/crates/gpapi/src/credential.rs new file mode 100644 index 0000000..b3b736b --- /dev/null +++ b/crates/gpapi/src/credential.rs @@ -0,0 +1,223 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; +use specta::Type; + +use crate::auth::SamlAuthData; + +#[derive(Debug, Serialize, Deserialize, Type, Clone)] +#[serde(rename_all = "camelCase")] +pub struct PasswordCredential { + username: String, + password: String, +} + +impl PasswordCredential { + pub fn new(username: &str, password: &str) -> Self { + Self { + username: username.to_string(), + password: password.to_string(), + } + } + + pub fn username(&self) -> &str { + &self.username + } + + pub fn password(&self) -> &str { + &self.password + } +} + +impl From<&CachedCredential> for PasswordCredential { + fn from(value: &CachedCredential) -> Self { + Self::new(value.username(), value.password().unwrap_or_default()) + } +} + +#[derive(Debug, Serialize, Deserialize, Type, Clone)] +#[serde(rename_all = "camelCase")] +pub struct PreloginCookieCredential { + username: String, + prelogin_cookie: String, +} + +impl PreloginCookieCredential { + pub fn new(username: &str, prelogin_cookie: &str) -> Self { + Self { + username: username.to_string(), + prelogin_cookie: prelogin_cookie.to_string(), + } + } + + pub fn username(&self) -> &str { + &self.username + } + + pub fn prelogin_cookie(&self) -> &str { + &self.prelogin_cookie + } +} + +impl TryFrom for PreloginCookieCredential { + type Error = anyhow::Error; + + fn try_from(value: SamlAuthData) -> Result { + let username = value.username().to_string(); + let prelogin_cookie = value + .prelogin_cookie() + .ok_or_else(|| anyhow::anyhow!("Missing prelogin cookie"))? + .to_string(); + + Ok(Self::new(&username, &prelogin_cookie)) + } +} + +#[derive(Debug, Serialize, Deserialize, Type, Clone)] +#[serde(rename_all = "camelCase")] +pub struct AuthCookieCredential { + username: String, + user_auth_cookie: String, + prelogon_user_auth_cookie: String, +} + +impl AuthCookieCredential { + pub fn new(username: &str, user_auth_cookie: &str, prelogon_user_auth_cookie: &str) -> Self { + Self { + username: username.to_string(), + user_auth_cookie: user_auth_cookie.to_string(), + prelogon_user_auth_cookie: prelogon_user_auth_cookie.to_string(), + } + } + + pub fn username(&self) -> &str { + &self.username + } + + pub fn user_auth_cookie(&self) -> &str { + &self.user_auth_cookie + } + + pub fn prelogon_user_auth_cookie(&self) -> &str { + &self.prelogon_user_auth_cookie + } +} + +#[derive(Debug, Serialize, Deserialize, Type, Clone)] +#[serde(rename_all = "camelCase")] +pub struct CachedCredential { + username: String, + password: Option, + auth_cookie: AuthCookieCredential, +} + +impl CachedCredential { + pub fn new( + username: String, + password: Option, + auth_cookie: AuthCookieCredential, + ) -> Self { + Self { + username, + password, + auth_cookie, + } + } + + pub fn username(&self) -> &str { + &self.username + } + + pub fn password(&self) -> Option<&str> { + self.password.as_deref() + } + + pub fn auth_cookie(&self) -> &AuthCookieCredential { + &self.auth_cookie + } + + pub fn set_auth_cookie(&mut self, auth_cookie: AuthCookieCredential) { + self.auth_cookie = auth_cookie; + } +} + +#[derive(Debug, Serialize, Deserialize, Type, Clone)] +#[serde(tag = "type", rename_all = "camelCase")] +pub enum Credential { + Password(PasswordCredential), + PreloginCookie(PreloginCookieCredential), + AuthCookie(AuthCookieCredential), + CachedCredential(CachedCredential), +} + +impl Credential { + pub fn username(&self) -> &str { + match self { + Credential::Password(cred) => cred.username(), + Credential::PreloginCookie(cred) => cred.username(), + Credential::AuthCookie(cred) => cred.username(), + Credential::CachedCredential(cred) => cred.username(), + } + } + + pub fn to_params(&self) -> HashMap<&str, &str> { + let mut params = HashMap::new(); + params.insert("user", self.username()); + + match self { + Credential::Password(cred) => { + params.insert("passwd", cred.password()); + } + Credential::PreloginCookie(cred) => { + params.insert("prelogin-cookie", cred.prelogin_cookie()); + } + Credential::AuthCookie(cred) => { + params.insert("portal-userauthcookie", cred.user_auth_cookie()); + params.insert( + "portal-prelogonuserauthcookie", + cred.prelogon_user_auth_cookie(), + ); + } + Credential::CachedCredential(cred) => { + if let Some(password) = cred.password() { + params.insert("passwd", password); + } + params.insert("portal-userauthcookie", cred.auth_cookie.user_auth_cookie()); + params.insert( + "portal-prelogonuserauthcookie", + cred.auth_cookie.prelogon_user_auth_cookie(), + ); + } + } + + params + } +} + +impl TryFrom for Credential { + type Error = anyhow::Error; + + fn try_from(value: SamlAuthData) -> Result { + let prelogin_cookie = PreloginCookieCredential::try_from(value)?; + + Ok(Self::PreloginCookie(prelogin_cookie)) + } +} + +impl From for Credential { + fn from(value: PasswordCredential) -> Self { + Self::Password(value) + } +} + +impl From<&AuthCookieCredential> for Credential { + fn from(value: &AuthCookieCredential) -> Self { + Self::AuthCookie(value.clone()) + } +} + +impl From<&CachedCredential> for Credential { + fn from(value: &CachedCredential) -> Self { + Self::CachedCredential(value.clone()) + } +} diff --git a/crates/gpapi/src/gateway/login.rs b/crates/gpapi/src/gateway/login.rs new file mode 100644 index 0000000..04a9b98 --- /dev/null +++ b/crates/gpapi/src/gateway/login.rs @@ -0,0 +1,74 @@ +use log::info; +use reqwest::Client; +use roxmltree::Document; +use urlencoding::encode; + +use crate::{credential::Credential, gp_params::GpParams}; + +pub async fn gateway_login( + gateway: &str, + cred: &Credential, + gp_params: &GpParams, +) -> anyhow::Result { + let login_url = format!("https://{}/ssl-vpn/login.esp", gateway); + let client = Client::builder() + .user_agent(gp_params.user_agent()) + .build()?; + + let mut params = cred.to_params(); + let extra_params = gp_params.to_params(); + + params.extend(extra_params); + params.insert("server", gateway); + + info!("Gateway login, user_agent: {}", gp_params.user_agent()); + + let res_xml = client + .post(&login_url) + .form(¶ms) + .send() + .await? + .error_for_status()? + .text() + .await?; + + let doc = Document::parse(&res_xml)?; + + build_gateway_token(&doc, gp_params.computer()) +} + +fn build_gateway_token(doc: &Document, computer: &str) -> anyhow::Result { + let args = doc + .descendants() + .filter(|n| n.has_tag_name("argument")) + .map(|n| n.text().unwrap_or("").to_string()) + .collect::>(); + + let params = [ + read_args(&args, 1, "authcookie")?, + read_args(&args, 3, "portal")?, + read_args(&args, 4, "user")?, + read_args(&args, 7, "domain")?, + read_args(&args, 15, "preferred-ip")?, + ("computer", computer), + ]; + + let token = params + .iter() + .map(|(k, v)| format!("{}={}", k, encode(v))) + .collect::>() + .join("&"); + + Ok(token) +} + +fn read_args<'a>( + args: &'a [String], + index: usize, + key: &'a str, +) -> anyhow::Result<(&'a str, &'a str)> { + args + .get(index) + .ok_or_else(|| anyhow::anyhow!("Failed to read {key} from args")) + .map(|s| (key, s.as_ref())) +} diff --git a/crates/gpapi/src/gateway/mod.rs b/crates/gpapi/src/gateway/mod.rs new file mode 100644 index 0000000..7db09bc --- /dev/null +++ b/crates/gpapi/src/gateway/mod.rs @@ -0,0 +1,41 @@ +mod login; +mod parse_gateways; + +pub use login::*; +pub(crate) use parse_gateways::*; + +use serde::{Deserialize, Serialize}; +use specta::Type; + +use std::fmt::Display; + +#[derive(Debug, Serialize, Deserialize, Type, Clone)] +pub(crate) struct PriorityRule { + pub(crate) name: String, + pub(crate) priority: u32, +} + +#[derive(Debug, Serialize, Deserialize, Type, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Gateway { + pub(crate) name: String, + pub(crate) address: String, + pub(crate) priority: u32, + pub(crate) priority_rules: Vec, +} + +impl Display for Gateway { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} ({})", self.name, self.address) + } +} + +impl Gateway { + pub fn name(&self) -> &str { + &self.name + } + + pub fn server(&self) -> &str { + &self.address + } +} diff --git a/crates/gpapi/src/gateway/parse_gateways.rs b/crates/gpapi/src/gateway/parse_gateways.rs new file mode 100644 index 0000000..384c382 --- /dev/null +++ b/crates/gpapi/src/gateway/parse_gateways.rs @@ -0,0 +1,63 @@ +use roxmltree::Document; + +use super::{Gateway, PriorityRule}; + +pub(crate) fn parse_gateways(doc: &Document) -> Option> { + let node_gateways = doc.descendants().find(|n| n.has_tag_name("gateways"))?; + let list_gateway = node_gateways + .descendants() + .find(|n| n.has_tag_name("list"))?; + + let gateways = list_gateway + .children() + .filter_map(|gateway_item| { + if !gateway_item.has_tag_name("entry") { + return None; + } + let address = gateway_item.attribute("name").unwrap_or("").to_string(); + let name = gateway_item + .children() + .find(|n| n.has_tag_name("description")) + .and_then(|n| n.text()) + .unwrap_or("") + .to_string(); + let priority = gateway_item + .children() + .find(|n| n.has_tag_name("priority")) + .and_then(|n| n.text()) + .and_then(|s| s.parse().ok()) + .unwrap_or(u32::MAX); + let priority_rules = gateway_item + .children() + .find(|n| n.has_tag_name("priority-rule")) + .map(|n| { + n.children() + .filter_map(|n| { + if !n.has_tag_name("entry") { + return None; + } + let name = n.attribute("name").unwrap_or("").to_string(); + let priority: u32 = n + .children() + .find(|n| n.has_tag_name("priority")) + .and_then(|n| n.text()) + .and_then(|s| s.parse().ok()) + .unwrap_or(u32::MAX); + + Some(PriorityRule { name, priority }) + }) + .collect() + }) + .unwrap_or_default(); + + Some(Gateway { + name, + address, + priority, + priority_rules, + }) + }) + .collect(); + + Some(gateways) +} diff --git a/crates/gpapi/src/gp_params.rs b/crates/gpapi/src/gp_params.rs new file mode 100644 index 0000000..770ec4b --- /dev/null +++ b/crates/gpapi/src/gp_params.rs @@ -0,0 +1,153 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; +use specta::Type; + +use crate::GP_USER_AGENT; + +#[derive(Debug, Serialize, Deserialize, Clone, Type, Default)] +pub enum ClientOs { + Linux, + #[default] + Windows, + Mac, +} + +impl From<&ClientOs> for &str { + fn from(os: &ClientOs) -> Self { + match os { + ClientOs::Linux => "Linux", + ClientOs::Windows => "Windows", + ClientOs::Mac => "Mac", + } + } +} + +impl ClientOs { + pub fn to_openconnect_os(&self) -> &str { + match self { + ClientOs::Linux => "linux", + ClientOs::Windows => "win", + ClientOs::Mac => "mac-intel", + } + } +} + +#[derive(Debug, Serialize, Deserialize, Type, Default)] +pub struct GpParams { + user_agent: String, + client_os: ClientOs, + os_version: Option, + client_version: Option, + computer: Option, +} + +impl GpParams { + pub fn builder() -> GpParamsBuilder { + GpParamsBuilder::new() + } + + pub(crate) fn user_agent(&self) -> &str { + &self.user_agent + } + + pub(crate) fn computer(&self) -> &str { + match self.computer { + Some(ref computer) => computer, + None => (&self.client_os).into() + } + } + + pub(crate) fn to_params(&self) -> HashMap<&str, &str> { + let mut params: HashMap<&str, &str> = HashMap::new(); + let client_os: &str = (&self.client_os).into(); + + // Common params + params.insert("prot", "https:"); + params.insert("jnlpReady", "jnlpReady"); + params.insert("ok", "Login"); + params.insert("direct", "yes"); + params.insert("ipv6-support", "yes"); + params.insert("inputStr", ""); + params.insert("clientVer", "4100"); + + params.insert("clientos", client_os); + + if let Some(computer) = &self.computer { + params.insert("computer", computer); + } else { + params.insert("computer", client_os); + } + + if let Some(os_version) = &self.os_version { + params.insert("os-version", os_version); + } + + if let Some(client_version) = &self.client_version { + params.insert("clientgpversion", client_version); + } + + params + } +} + +pub struct GpParamsBuilder { + user_agent: String, + client_os: ClientOs, + os_version: Option, + client_version: Option, + computer: Option, +} + +impl GpParamsBuilder { + pub fn new() -> Self { + Self { + user_agent: GP_USER_AGENT.to_string(), + client_os: ClientOs::Linux, + os_version: Default::default(), + client_version: Default::default(), + computer: Default::default(), + } + } + + pub fn user_agent(&mut self, user_agent: &str) -> &mut Self { + self.user_agent = user_agent.to_string(); + self + } + + pub fn client_os(&mut self, client_os: ClientOs) -> &mut Self { + self.client_os = client_os; + self + } + + pub fn os_version(&mut self, os_version: &str) -> &mut Self { + self.os_version = Some(os_version.to_string()); + self + } + + pub fn client_version(&mut self, client_version: &str) -> &mut Self { + self.client_version = Some(client_version.to_string()); + self + } + + pub fn computer(&mut self, computer: &str) -> &mut Self { + self.computer = Some(computer.to_string()); + self + } + + pub fn build(&self) -> GpParams { + GpParams { + user_agent: self.user_agent.clone(), + client_os: self.client_os.clone(), + os_version: self.os_version.clone(), + client_version: self.client_version.clone(), + computer: self.computer.clone(), + } + } +} + +impl Default for GpParamsBuilder { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/gpapi/src/lib.rs b/crates/gpapi/src/lib.rs new file mode 100644 index 0000000..ea4e001 --- /dev/null +++ b/crates/gpapi/src/lib.rs @@ -0,0 +1,28 @@ +pub mod auth; +pub mod credential; +pub mod gateway; +pub mod gp_params; +pub mod portal; +pub mod process; +pub mod service; +pub mod utils; + +#[cfg(debug_assertions)] +pub const GP_API_KEY: &[u8; 32] = &[0; 32]; + +pub const GP_USER_AGENT: &str = "PAN GlobalProtect"; +pub const GP_SERVICE_LOCK_FILE: &str = "/var/run/gpservice.lock"; + +#[cfg(not(debug_assertions))] +pub const GP_SERVICE_BINARY: &str = "/usr/bin/gpservice"; +#[cfg(not(debug_assertions))] +pub const GP_GUI_BINARY: &str = "/usr/bin/gpgui"; +#[cfg(not(debug_assertions))] +pub(crate) const GP_AUTH_BINARY: &str = "/usr/bin/gpauth"; + +#[cfg(debug_assertions)] +pub const GP_SERVICE_BINARY: &str = dotenvy_macro::dotenv!("GP_SERVICE_BINARY"); +#[cfg(debug_assertions)] +pub const GP_GUI_BINARY: &str = dotenvy_macro::dotenv!("GP_GUI_BINARY"); +#[cfg(debug_assertions)] +pub(crate) const GP_AUTH_BINARY: &str = dotenvy_macro::dotenv!("GP_AUTH_BINARY"); diff --git a/crates/gpapi/src/portal/config.rs b/crates/gpapi/src/portal/config.rs new file mode 100644 index 0000000..4becfbb --- /dev/null +++ b/crates/gpapi/src/portal/config.rs @@ -0,0 +1,180 @@ +use anyhow::ensure; +use log::info; +use reqwest::Client; +use roxmltree::Document; +use serde::Serialize; +use specta::Type; +use thiserror::Error; + +use crate::{ + credential::{AuthCookieCredential, Credential}, + gateway::{parse_gateways, Gateway}, + gp_params::GpParams, + utils::{normalize_server, xml}, +}; + +#[derive(Debug, Serialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct PortalConfig { + portal: String, + auth_cookie: AuthCookieCredential, + gateways: Vec, + config_digest: Option, +} + +impl PortalConfig { + pub fn new( + portal: String, + auth_cookie: AuthCookieCredential, + gateways: Vec, + config_digest: Option, + ) -> Self { + Self { + portal, + auth_cookie, + gateways, + config_digest, + } + } + + pub fn portal(&self) -> &str { + &self.portal + } + + pub fn gateways(&self) -> Vec<&Gateway> { + self.gateways.iter().collect() + } + + pub fn auth_cookie(&self) -> &AuthCookieCredential { + &self.auth_cookie + } + + /// In-place sort the gateways by region + pub fn sort_gateways(&mut self, region: &str) { + let preferred_gateway = self.find_preferred_gateway(region); + let preferred_gateway_index = self + .gateways() + .iter() + .position(|gateway| gateway.name == preferred_gateway.name) + .unwrap(); + + // Move the preferred gateway to the front of the list + self.gateways.swap(0, preferred_gateway_index); + } + + /// Find a gateway by name or address + pub fn find_gateway(&self, name_or_address: &str) -> Option<&Gateway> { + self + .gateways + .iter() + .find(|gateway| gateway.name == name_or_address || gateway.address == name_or_address) + } + + /// Find the preferred gateway for the given region + /// Iterates over the gateways and find the first one that + /// has the lowest priority for the given region. + /// If no gateway is found, returns the gateway with the lowest priority. + pub fn find_preferred_gateway(&self, region: &str) -> &Gateway { + let mut preferred_gateway: Option<&Gateway> = None; + let mut lowest_region_priority = u32::MAX; + + for gateway in &self.gateways { + for rule in &gateway.priority_rules { + if (rule.name == region || rule.name == "Any") && rule.priority < lowest_region_priority { + preferred_gateway = Some(gateway); + lowest_region_priority = rule.priority; + } + } + } + + // If no gateway is found, return the gateway with the lowest priority + preferred_gateway.unwrap_or_else(|| { + self + .gateways + .iter() + .min_by_key(|gateway| gateway.priority) + .unwrap() + }) + } +} + +#[derive(Error, Debug)] +pub enum PortalConfigError { + #[error("Empty response, retrying can help")] + EmptyResponse, + #[error("Empty auth cookie, retrying can help")] + EmptyAuthCookie, + #[error("Invalid auth cookie, retrying can help")] + InvalidAuthCookie, + #[error("Empty gateways, retrying can help")] + EmptyGateways, +} + +pub async fn retrieve_config( + portal: &str, + cred: &Credential, + gp_params: &GpParams, +) -> anyhow::Result { + let portal = normalize_server(portal)?; + let server = remove_url_scheme(&portal); + + let url = format!("{}/global-protect/getconfig.esp", portal); + let client = Client::builder() + .user_agent(gp_params.user_agent()) + .build()?; + + let mut params = cred.to_params(); + let extra_params = gp_params.to_params(); + + params.extend(extra_params); + params.insert("server", &server); + params.insert("host", &server); + + info!("Portal config, user_agent: {}", gp_params.user_agent()); + + let res_xml = client + .post(&url) + .form(¶ms) + .send() + .await? + .error_for_status()? + .text() + .await?; + + ensure!(!res_xml.is_empty(), PortalConfigError::EmptyResponse); + + let doc = Document::parse(&res_xml)?; + let gateways = parse_gateways(&doc).ok_or_else(|| anyhow::anyhow!("Failed to parse gateways"))?; + + let user_auth_cookie = xml::get_child_text(&doc, "portal-userauthcookie").unwrap_or_default(); + let prelogon_user_auth_cookie = + xml::get_child_text(&doc, "portal-prelogonuserauthcookie").unwrap_or_default(); + let config_digest = xml::get_child_text(&doc, "config-digest"); + + ensure!( + !user_auth_cookie.is_empty() && !prelogon_user_auth_cookie.is_empty(), + PortalConfigError::EmptyAuthCookie + ); + + ensure!( + user_auth_cookie != "empty" && prelogon_user_auth_cookie != "empty", + PortalConfigError::InvalidAuthCookie + ); + + ensure!(!gateways.is_empty(), PortalConfigError::EmptyGateways); + + Ok(PortalConfig::new( + server.to_string(), + AuthCookieCredential::new( + cred.username(), + &user_auth_cookie, + &prelogon_user_auth_cookie, + ), + gateways, + config_digest, + )) +} + +fn remove_url_scheme(s: &str) -> String { + s.replace("http://", "").replace("https://", "") +} diff --git a/crates/gpapi/src/portal/mod.rs b/crates/gpapi/src/portal/mod.rs new file mode 100644 index 0000000..8c111db --- /dev/null +++ b/crates/gpapi/src/portal/mod.rs @@ -0,0 +1,5 @@ +mod config; +mod prelogin; + +pub use config::*; +pub use prelogin::*; diff --git a/crates/gpapi/src/portal/prelogin.rs b/crates/gpapi/src/portal/prelogin.rs new file mode 100644 index 0000000..dbc4cab --- /dev/null +++ b/crates/gpapi/src/portal/prelogin.rs @@ -0,0 +1,129 @@ +use anyhow::bail; +use log::{info, trace}; +use reqwest::Client; +use roxmltree::Document; +use serde::Serialize; +use specta::Type; + +use crate::utils::{base64, normalize_server, xml}; + +#[derive(Debug, Serialize, Type, Clone)] +#[serde(rename_all = "camelCase")] +pub struct SamlPrelogin { + region: String, + saml_request: String, +} + +impl SamlPrelogin { + pub fn region(&self) -> &str { + &self.region + } + + pub fn saml_request(&self) -> &str { + &self.saml_request + } +} + +#[derive(Debug, Serialize, Type, Clone)] +#[serde(rename_all = "camelCase")] +pub struct StandardPrelogin { + region: String, + auth_message: String, + label_username: String, + label_password: String, +} + +impl StandardPrelogin { + pub fn region(&self) -> &str { + &self.region + } + + pub fn auth_message(&self) -> &str { + &self.auth_message + } + + pub fn label_username(&self) -> &str { + &self.label_username + } + + pub fn label_password(&self) -> &str { + &self.label_password + } +} + +#[derive(Debug, Serialize, Type, Clone)] +#[serde(tag = "type", rename_all = "camelCase")] +pub enum Prelogin { + Saml(SamlPrelogin), + Standard(StandardPrelogin), +} + +impl Prelogin { + pub fn region(&self) -> &str { + match self { + Prelogin::Saml(saml) => saml.region(), + Prelogin::Standard(standard) => standard.region(), + } + } +} + +pub async fn prelogin(portal: &str, user_agent: &str) -> anyhow::Result { + info!("Portal prelogin, user_agent: {}", user_agent); + + let portal = normalize_server(portal)?; + let prelogin_url = format!("{}/global-protect/prelogin.esp", portal); + let client = Client::builder().user_agent(user_agent).build()?; + + let res_xml = client + .get(&prelogin_url) + .send() + .await? + .error_for_status()? + .text() + .await?; + + trace!("Prelogin response: {}", res_xml); + let doc = Document::parse(&res_xml)?; + + let status = xml::get_child_text(&doc, "status") + .ok_or_else(|| anyhow::anyhow!("Prelogin response does not contain status element"))?; + // Check the status of the prelogin response + if status.to_uppercase() != "SUCCESS" { + let msg = xml::get_child_text(&doc, "msg").unwrap_or(String::from("Unknown error")); + bail!("Prelogin failed: {}", msg) + } + + let region = xml::get_child_text(&doc, "region") + .ok_or_else(|| anyhow::anyhow!("Prelogin response does not contain region element"))?; + + let saml_method = xml::get_child_text(&doc, "saml-auth-method"); + let saml_request = xml::get_child_text(&doc, "saml-request"); + // Check if the prelogin response is SAML + if saml_method.is_some() && saml_request.is_some() { + let saml_request = base64::decode_to_string(&saml_request.unwrap())?; + let saml_prelogin = SamlPrelogin { + region, + saml_request, + }; + + return Ok(Prelogin::Saml(saml_prelogin)); + } + + let label_username = xml::get_child_text(&doc, "username-label"); + let label_password = xml::get_child_text(&doc, "password-label"); + // Check if the prelogin response is standard login + if label_username.is_some() && label_password.is_some() { + let auth_message = xml::get_child_text(&doc, "authentication-message") + .unwrap_or(String::from("Please enter the login credentials")); + let standard_prelogin = StandardPrelogin { + region, + auth_message, + label_username: label_username.unwrap(), + label_password: label_password.unwrap(), + }; + + return Ok(Prelogin::Standard(standard_prelogin)); + } + + bail!("Invalid prelogin response"); +} diff --git a/crates/gpapi/src/process/auth_launcher.rs b/crates/gpapi/src/process/auth_launcher.rs new file mode 100644 index 0000000..05389ed --- /dev/null +++ b/crates/gpapi/src/process/auth_launcher.rs @@ -0,0 +1,96 @@ +use std::process::Stdio; + +use tokio::process::Command; + +use crate::{auth::SamlAuthResult, credential::Credential, GP_AUTH_BINARY}; + +use super::command_traits::CommandExt; + +pub struct SamlAuthLauncher<'a> { + server: &'a str, + user_agent: Option<&'a str>, + saml_request: Option<&'a str>, + hidpi: bool, + fix_openssl: bool, + clean: bool, +} + +impl<'a> SamlAuthLauncher<'a> { + pub fn new(server: &'a str) -> Self { + Self { + server, + user_agent: None, + saml_request: None, + hidpi: false, + fix_openssl: false, + clean: false, + } + } + + pub fn user_agent(mut self, user_agent: &'a str) -> Self { + self.user_agent = Some(user_agent); + self + } + + pub fn saml_request(mut self, saml_request: &'a str) -> Self { + self.saml_request = Some(saml_request); + self + } + + pub fn hidpi(mut self, hidpi: bool) -> Self { + self.hidpi = hidpi; + self + } + + pub fn fix_openssl(mut self, fix_openssl: bool) -> Self { + self.fix_openssl = fix_openssl; + self + } + + pub fn clean(mut self, clean: bool) -> Self { + self.clean = clean; + self + } + + /// Launch the authenticator binary as the current user or SUDO_USER if available. + pub async fn launch(self) -> anyhow::Result { + let mut auth_cmd = Command::new(GP_AUTH_BINARY); + auth_cmd.arg(self.server); + + if let Some(user_agent) = self.user_agent { + auth_cmd.arg("--user-agent").arg(user_agent); + } + + if let Some(saml_request) = self.saml_request { + auth_cmd.arg("--saml-request").arg(saml_request); + } + + if self.fix_openssl { + auth_cmd.arg("--fix-openssl"); + } + + if self.hidpi { + auth_cmd.arg("--hidpi"); + } + + if self.clean { + auth_cmd.arg("--clean"); + } + + let mut non_root_cmd = auth_cmd.into_non_root()?; + let output = non_root_cmd + .kill_on_drop(true) + .stdout(Stdio::piped()) + .spawn()? + .wait_with_output() + .await?; + + let auth_result: SamlAuthResult = serde_json::from_slice(&output.stdout) + .map_err(|_| anyhow::anyhow!("Failed to parse auth data"))?; + + match auth_result { + SamlAuthResult::Success(auth_data) => Credential::try_from(auth_data), + SamlAuthResult::Failure(msg) => Err(anyhow::anyhow!(msg)), + } + } +} diff --git a/crates/gpapi/src/process/command_traits.rs b/crates/gpapi/src/process/command_traits.rs new file mode 100644 index 0000000..e4dba3d --- /dev/null +++ b/crates/gpapi/src/process/command_traits.rs @@ -0,0 +1,64 @@ +use anyhow::bail; +use std::{env, ffi::OsStr}; +use tokio::process::Command; +use users::{os::unix::UserExt, User}; + +pub trait CommandExt { + fn new_pkexec>(program: S) -> Command; + fn into_non_root(self) -> anyhow::Result; +} + +impl CommandExt for Command { + fn new_pkexec>(program: S) -> Command { + let mut cmd = Command::new("pkexec"); + cmd + .arg("--disable-internal-agent") + .arg("--user") + .arg("root") + .arg(program); + + cmd + } + + fn into_non_root(mut self) -> anyhow::Result { + let user = + get_non_root_user().map_err(|_| anyhow::anyhow!("{:?} cannot be run as root", self))?; + + self + .env("HOME", user.home_dir()) + .env("USER", user.name()) + .env("LOGNAME", user.name()) + .env("USERNAME", user.name()) + .uid(user.uid()) + .gid(user.primary_group_id()); + + Ok(self) + } +} + +fn get_non_root_user() -> anyhow::Result { + let current_user = whoami::username(); + + let user = if current_user == "root" { + get_real_user()? + } else { + users::get_user_by_name(¤t_user) + .ok_or_else(|| anyhow::anyhow!("User ({}) not found", current_user))? + }; + + if user.uid() == 0 { + bail!("Non-root user not found") + } + + Ok(user) +} + +fn get_real_user() -> anyhow::Result { + // Read the UID from SUDO_UID or PKEXEC_UID environment variable if available. + let uid = match env::var("SUDO_UID") { + Ok(uid) => uid.parse::()?, + _ => env::var("PKEXEC_UID")?.parse::()?, + }; + + users::get_user_by_uid(uid).ok_or_else(|| anyhow::anyhow!("User not found")) +} diff --git a/crates/gpapi/src/process/gui_launcher.rs b/crates/gpapi/src/process/gui_launcher.rs new file mode 100644 index 0000000..47dc6c2 --- /dev/null +++ b/crates/gpapi/src/process/gui_launcher.rs @@ -0,0 +1,91 @@ +use std::{ + collections::HashMap, + path::PathBuf, + process::{ExitStatus, Stdio}, +}; + +use tokio::{io::AsyncWriteExt, process::Command}; + +use crate::{utils::base64, GP_GUI_BINARY}; + +use super::command_traits::CommandExt; + +pub struct GuiLauncher { + program: PathBuf, + api_key: Option>, + minimized: bool, + envs: Option>, +} + +impl Default for GuiLauncher { + fn default() -> Self { + Self::new() + } +} + +impl GuiLauncher { + pub fn new() -> Self { + Self { + program: GP_GUI_BINARY.into(), + api_key: None, + minimized: false, + envs: None, + } + } + + pub fn envs>>>(mut self, envs: T) -> Self { + self.envs = envs.into(); + self + } + + pub fn api_key(mut self, api_key: Vec) -> Self { + self.api_key = Some(api_key); + self + } + + pub fn minimized(mut self, minimized: bool) -> Self { + self.minimized = minimized; + self + } + + pub async fn launch(&self) -> anyhow::Result { + let mut cmd = Command::new(&self.program); + + if let Some(envs) = &self.envs { + cmd.env_clear(); + cmd.envs(envs); + } + + if self.api_key.is_some() { + cmd.arg("--api-key-on-stdin"); + } + + if self.minimized { + cmd.arg("--minimized"); + } + + let mut non_root_cmd = cmd.into_non_root()?; + + let mut child = non_root_cmd + .kill_on_drop(true) + .stdin(Stdio::piped()) + .spawn()?; + + let mut stdin = child + .stdin + .take() + .ok_or_else(|| anyhow::anyhow!("Failed to open stdin"))?; + + if let Some(api_key) = &self.api_key { + let api_key = base64::encode(api_key); + tokio::spawn(async move { + stdin.write_all(api_key.as_bytes()).await.unwrap(); + drop(stdin); + }); + } + + let exit_status = child.wait().await?; + + Ok(exit_status) + } +} diff --git a/crates/gpapi/src/process/mod.rs b/crates/gpapi/src/process/mod.rs new file mode 100644 index 0000000..e82d429 --- /dev/null +++ b/crates/gpapi/src/process/mod.rs @@ -0,0 +1,5 @@ +pub(crate) mod command_traits; + +pub mod auth_launcher; +pub mod gui_launcher; +pub mod service_launcher; diff --git a/crates/gpapi/src/process/service_launcher.rs b/crates/gpapi/src/process/service_launcher.rs new file mode 100644 index 0000000..05bca37 --- /dev/null +++ b/crates/gpapi/src/process/service_launcher.rs @@ -0,0 +1,72 @@ +use std::{ + fs::File, + path::PathBuf, + process::{ExitStatus, Stdio}, +}; + +use tokio::process::Command; + +use crate::GP_SERVICE_BINARY; + +use super::command_traits::CommandExt; + +pub struct ServiceLauncher { + program: PathBuf, + minimized: bool, + env_file: Option, + log_file: Option, +} + +impl Default for ServiceLauncher { + fn default() -> Self { + Self::new() + } +} + +impl ServiceLauncher { + pub fn new() -> Self { + Self { + program: GP_SERVICE_BINARY.into(), + minimized: false, + env_file: None, + log_file: None, + } + } + + pub fn minimized(mut self, minimized: bool) -> Self { + self.minimized = minimized; + self + } + + pub fn env_file(mut self, env_file: &str) -> Self { + self.env_file = Some(env_file.to_string()); + self + } + + pub fn log_file(mut self, log_file: &str) -> Self { + self.log_file = Some(log_file.to_string()); + self + } + + pub async fn launch(&self) -> anyhow::Result { + let mut cmd = Command::new_pkexec(&self.program); + + if self.minimized { + cmd.arg("--minimized"); + } + + if let Some(env_file) = &self.env_file { + cmd.arg("--env-file").arg(env_file); + } + + if let Some(log_file) = &self.log_file { + let log_file = File::create(log_file)?; + let stdio = Stdio::from(log_file); + cmd.stderr(stdio); + } + + let exit_status = cmd.kill_on_drop(true).spawn()?.wait().await?; + + Ok(exit_status) + } +} diff --git a/crates/gpapi/src/service/event.rs b/crates/gpapi/src/service/event.rs new file mode 100644 index 0000000..869b980 --- /dev/null +++ b/crates/gpapi/src/service/event.rs @@ -0,0 +1,10 @@ +use serde::{Deserialize, Serialize}; + +use super::vpn_state::VpnState; + +/// Events that can be emitted by the service +#[derive(Debug, Serialize, Deserialize, Clone)] +pub enum WsEvent { + VpnState(VpnState), + ActiveGui, +} diff --git a/crates/gpapi/src/service/mod.rs b/crates/gpapi/src/service/mod.rs new file mode 100644 index 0000000..7fd4b55 --- /dev/null +++ b/crates/gpapi/src/service/mod.rs @@ -0,0 +1,3 @@ +pub mod event; +pub mod request; +pub mod vpn_state; diff --git a/crates/gpapi/src/service/request.rs b/crates/gpapi/src/service/request.rs new file mode 100644 index 0000000..753b9ba --- /dev/null +++ b/crates/gpapi/src/service/request.rs @@ -0,0 +1,118 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; +use specta::Type; + +use crate::{gateway::Gateway, gp_params::ClientOs}; + +use super::vpn_state::ConnectInfo; + +#[derive(Debug, Deserialize, Serialize)] +pub struct LaunchGuiRequest { + user: String, + envs: HashMap, +} + +impl LaunchGuiRequest { + pub fn new(user: String, envs: HashMap) -> Self { + Self { user, envs } + } + + pub fn user(&self) -> &str { + &self.user + } + + pub fn envs(&self) -> &HashMap { + &self.envs + } +} + +#[derive(Debug, Deserialize, Serialize, Type)] +pub struct ConnectArgs { + cookie: String, + vpnc_script: Option, + user_agent: Option, + os: Option, +} + +impl ConnectArgs { + pub fn new(cookie: String) -> Self { + Self { + cookie, + vpnc_script: None, + user_agent: None, + os: None, + } + } + + pub fn cookie(&self) -> &str { + &self.cookie + } + + pub fn vpnc_script(&self) -> Option { + self.vpnc_script.clone() + } + + pub fn user_agent(&self) -> Option { + self.user_agent.clone() + } + + pub fn openconnect_os(&self) -> Option { + self + .os + .as_ref() + .map(|os| os.to_openconnect_os().to_string()) + } +} + +#[derive(Debug, Deserialize, Serialize, Type)] +pub struct ConnectRequest { + info: ConnectInfo, + args: ConnectArgs, +} + +impl ConnectRequest { + pub fn new(info: ConnectInfo, cookie: String) -> Self { + Self { + info, + args: ConnectArgs::new(cookie), + } + } + + pub fn with_vpnc_script>>(mut self, vpnc_script: T) -> Self { + self.args.vpnc_script = vpnc_script.into(); + self + } + + pub fn with_user_agent>>(mut self, user_agent: T) -> Self { + self.args.user_agent = user_agent.into(); + self + } + + pub fn with_os>>(mut self, os: T) -> Self { + self.args.os = os.into(); + self + } + + pub fn gateway(&self) -> &Gateway { + self.info.gateway() + } + + pub fn info(&self) -> &ConnectInfo { + &self.info + } + + pub fn args(&self) -> &ConnectArgs { + &self.args + } +} + +#[derive(Debug, Deserialize, Serialize, Type)] +pub struct DisconnectRequest; + +/// Requests that can be sent to the service +#[derive(Debug, Deserialize, Serialize)] +pub enum WsRequest { + Connect(Box), + Disconnect(DisconnectRequest), +} diff --git a/crates/gpapi/src/service/vpn_state.rs b/crates/gpapi/src/service/vpn_state.rs new file mode 100644 index 0000000..f800395 --- /dev/null +++ b/crates/gpapi/src/service/vpn_state.rs @@ -0,0 +1,34 @@ +use serde::{Deserialize, Serialize}; +use specta::Type; + +use crate::gateway::Gateway; + +#[derive(Debug, Deserialize, Serialize, Type, Clone)] +pub struct ConnectInfo { + portal: String, + gateway: Gateway, + gateways: Vec, +} + +impl ConnectInfo { + pub fn new(portal: String, gateway: Gateway, gateways: Vec) -> Self { + Self { + portal, + gateway, + gateways, + } + } + + pub fn gateway(&self) -> &Gateway { + &self.gateway + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub enum VpnState { + Disconnected, + Connecting(Box), + Connected(Box), + Disconnecting, +} diff --git a/crates/gpapi/src/utils/base64.rs b/crates/gpapi/src/utils/base64.rs new file mode 100644 index 0000000..d3bc064 --- /dev/null +++ b/crates/gpapi/src/utils/base64.rs @@ -0,0 +1,21 @@ +use base64::{engine::general_purpose, Engine}; + +pub fn encode(data: &[u8]) -> String { + let engine = general_purpose::STANDARD; + + engine.encode(data) +} + +pub fn decode_to_vec(s: &str) -> anyhow::Result> { + let engine = general_purpose::STANDARD; + let decoded = engine.decode(s)?; + + Ok(decoded) +} + +pub(crate) fn decode_to_string(s: &str) -> anyhow::Result { + let decoded = decode_to_vec(s)?; + let decoded = String::from_utf8(decoded)?; + + Ok(decoded) +} diff --git a/crates/gpapi/src/utils/crypto.rs b/crates/gpapi/src/utils/crypto.rs new file mode 100644 index 0000000..7fb9d73 --- /dev/null +++ b/crates/gpapi/src/utils/crypto.rs @@ -0,0 +1,108 @@ +use chacha20poly1305::{ + aead::{Aead, OsRng}, + AeadCore, ChaCha20Poly1305, Key, KeyInit, Nonce, +}; +use serde::{de::DeserializeOwned, Serialize}; + +pub fn generate_key() -> Key { + ChaCha20Poly1305::generate_key(&mut OsRng) +} + +pub fn encrypt(key: &Key, value: &T) -> anyhow::Result> +where + T: Serialize, +{ + let cipher = ChaCha20Poly1305::new(key); + let nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng); + + let data = serde_json::to_vec(value)?; + let cipher_text = cipher.encrypt(&nonce, data.as_ref())?; + + let mut encrypted = Vec::new(); + encrypted.extend_from_slice(&nonce); + encrypted.extend_from_slice(&cipher_text); + + Ok(encrypted) +} + +pub fn decrypt(key: &Key, encrypted: Vec) -> anyhow::Result +where + T: DeserializeOwned, +{ + let cipher = ChaCha20Poly1305::new(key); + + let nonce = Nonce::from_slice(&encrypted[..12]); + let cipher_text = &encrypted[12..]; + + let plaintext = cipher.decrypt(nonce, cipher_text)?; + + let value = serde_json::from_slice(&plaintext)?; + + Ok(value) +} + +pub struct Crypto { + key: Vec, +} + +impl Crypto { + pub fn new(key: Vec) -> Self { + Self { key } + } + + pub fn encrypt(&self, plain: T) -> anyhow::Result> { + let key: &[u8] = &self.key; + let encrypted_data = encrypt(key.into(), &plain)?; + + Ok(encrypted_data) + } + + pub fn decrypt(&self, encrypted: Vec) -> anyhow::Result { + let key: &[u8] = &self.key; + decrypt(key.into(), encrypted) + } + + pub fn encrypt_to(&self, path: &std::path::Path, plain: T) -> anyhow::Result<()> { + let encrypted_data = self.encrypt(plain)?; + std::fs::write(path, encrypted_data)?; + + Ok(()) + } + + pub fn decrypt_from(&self, path: &std::path::Path) -> anyhow::Result { + let encrypted_data = std::fs::read(path)?; + self.decrypt(encrypted_data) + } +} + +#[cfg(test)] +mod tests { + use serde::Deserialize; + + use super::*; + + #[derive(Serialize, Deserialize)] + struct User { + name: String, + age: u8, + } + + #[test] + fn it_works() -> anyhow::Result<()> { + let key = generate_key(); + + let user = User { + name: "test".to_string(), + age: 18, + }; + + let encrypted = encrypt(&key, &user)?; + + let decrypted_user = decrypt::(&key, encrypted)?; + + assert_eq!(user.name, decrypted_user.name); + assert_eq!(user.age, decrypted_user.age); + + Ok(()) + } +} diff --git a/crates/gpapi/src/utils/endpoint.rs b/crates/gpapi/src/utils/endpoint.rs new file mode 100644 index 0000000..0a88bc6 --- /dev/null +++ b/crates/gpapi/src/utils/endpoint.rs @@ -0,0 +1,20 @@ +use tokio::fs; + +use crate::GP_SERVICE_LOCK_FILE; + +async fn read_port() -> anyhow::Result { + let port = fs::read_to_string(GP_SERVICE_LOCK_FILE).await?; + Ok(port.trim().to_string()) +} + +pub async fn http_endpoint() -> anyhow::Result { + let port = read_port().await?; + + Ok(format!("http://127.0.0.1:{}", port)) +} + +pub async fn ws_endpoint() -> anyhow::Result { + let port = read_port().await?; + + Ok(format!("ws://127.0.0.1:{}/ws", port)) +} diff --git a/crates/gpapi/src/utils/env_file.rs b/crates/gpapi/src/utils/env_file.rs new file mode 100644 index 0000000..76d52c3 --- /dev/null +++ b/crates/gpapi/src/utils/env_file.rs @@ -0,0 +1,37 @@ +use std::collections::HashMap; +use std::env; +use std::io::Write; +use std::path::Path; + +use tempfile::NamedTempFile; + +pub fn persist_env_vars(extra: Option>) -> anyhow::Result { + let mut env_file = NamedTempFile::new()?; + let content = env::vars() + .map(|(key, value)| format!("{}={}", key, value)) + .chain( + extra + .unwrap_or_default() + .into_iter() + .map(|(key, value)| format!("{}={}", key, value)), + ) + .collect::>() + .join("\n"); + + writeln!(env_file, "{}", content)?; + + Ok(env_file) +} + +pub fn load_env_vars>(env_file: T) -> anyhow::Result> { + let content = std::fs::read_to_string(env_file)?; + let mut env_vars: HashMap = HashMap::new(); + + for line in content.lines() { + if let Some((key, value)) = line.split_once('=') { + env_vars.insert(key.to_string(), value.to_string()); + } + } + + Ok(env_vars) +} diff --git a/crates/gpapi/src/utils/lock_file.rs b/crates/gpapi/src/utils/lock_file.rs new file mode 100644 index 0000000..93ee510 --- /dev/null +++ b/crates/gpapi/src/utils/lock_file.rs @@ -0,0 +1,39 @@ +use std::path::PathBuf; + +pub struct LockFile { + path: PathBuf, +} + +impl LockFile { + pub fn new>(path: P) -> Self { + Self { path: path.into() } + } + + pub fn exists(&self) -> bool { + self.path.exists() + } + + pub fn lock(&self, content: impl AsRef<[u8]>) -> anyhow::Result<()> { + std::fs::write(&self.path, content)?; + Ok(()) + } + + pub fn unlock(&self) -> anyhow::Result<()> { + std::fs::remove_file(&self.path)?; + Ok(()) + } + + pub async fn check_health(&self) -> bool { + match std::fs::read_to_string(&self.path) { + Ok(content) => { + let url = format!("http://127.0.0.1:{}/health", content.trim()); + + match reqwest::get(&url).await { + Ok(resp) => resp.status().is_success(), + Err(_) => false, + } + } + Err(_) => false, + } + } +} diff --git a/crates/gpapi/src/utils/mod.rs b/crates/gpapi/src/utils/mod.rs new file mode 100644 index 0000000..048c23c --- /dev/null +++ b/crates/gpapi/src/utils/mod.rs @@ -0,0 +1,40 @@ +use reqwest::Url; + +pub(crate) mod xml; + +pub mod base64; +pub mod crypto; +pub mod endpoint; +pub mod env_file; +pub mod lock_file; +pub mod openssl; +pub mod redact; +#[cfg(feature = "tauri")] +pub mod window; + +mod shutdown_signal; + +pub use shutdown_signal::shutdown_signal; + +/// Normalize the server URL to the format `https://:` +pub fn normalize_server(server: &str) -> anyhow::Result { + let server = if server.starts_with("https://") || server.starts_with("http://") { + server.to_string() + } else { + format!("https://{}", server) + }; + + let normalized_url = Url::parse(&server)?; + let scheme = normalized_url.scheme(); + let host = normalized_url + .host_str() + .ok_or(anyhow::anyhow!("Invalid server URL: missing host"))?; + + let port: String = normalized_url + .port() + .map_or("".into(), |port| format!(":{}", port)); + + let normalized_url = format!("{}://{}{}", scheme, host, port); + + Ok(normalized_url) +} diff --git a/crates/gpapi/src/utils/openssl.rs b/crates/gpapi/src/utils/openssl.rs new file mode 100644 index 0000000..d2aea55 --- /dev/null +++ b/crates/gpapi/src/utils/openssl.rs @@ -0,0 +1,37 @@ +use std::path::Path; + +use tempfile::NamedTempFile; + +pub fn openssl_conf() -> String { + let option = "UnsafeLegacyServerConnect"; + + format!( + "openssl_conf = openssl_init + +[openssl_init] +ssl_conf = ssl_sect + +[ssl_sect] +system_default = system_default_sect + +[system_default_sect] +Options = {}", + option + ) +} + +pub fn fix_openssl>(path: P) -> anyhow::Result<()> { + let content = openssl_conf(); + std::fs::write(path, content)?; + Ok(()) +} + +pub fn fix_openssl_env() -> anyhow::Result { + let openssl_conf = NamedTempFile::new()?; + let openssl_conf_path = openssl_conf.path(); + + fix_openssl(openssl_conf_path)?; + std::env::set_var("OPENSSL_CONF", openssl_conf_path); + + Ok(openssl_conf) +} diff --git a/crates/gpapi/src/utils/redact.rs b/crates/gpapi/src/utils/redact.rs new file mode 100644 index 0000000..7917b18 --- /dev/null +++ b/crates/gpapi/src/utils/redact.rs @@ -0,0 +1,227 @@ +use std::sync::RwLock; + +use redact_engine::{Pattern, Redaction as RedactEngine}; +use regex::Regex; +use url::{form_urlencoded, Url}; + +pub struct Redaction { + redact_engine: RwLock>, +} + +impl Default for Redaction { + fn default() -> Self { + Self::new() + } +} + +impl Redaction { + pub fn new() -> Self { + let redact_engine = RedactEngine::custom("[**********]").add_pattern(Pattern { + test: Regex::new("(((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4})").unwrap(), + group: 1, + }); + + Self { + redact_engine: RwLock::new(Some(redact_engine)), + } + } + + pub fn add_value(&self, text: &str) -> anyhow::Result<()> { + let mut redact_engine = self + .redact_engine + .write() + .map_err(|_| anyhow::anyhow!("Failed to acquire write lock on redact engine"))?; + + *redact_engine = Some( + redact_engine + .take() + .ok_or_else(|| anyhow::anyhow!("Failed to take redact engine"))? + .add_value(text)?, + ); + + Ok(()) + } + + pub fn add_values(&self, texts: &[&str]) -> anyhow::Result<()> { + let mut redact_engine = self + .redact_engine + .write() + .map_err(|_| anyhow::anyhow!("Failed to acquire write lock on redact engine"))?; + + *redact_engine = Some( + redact_engine + .take() + .ok_or_else(|| anyhow::anyhow!("Failed to take redact engine"))? + .add_values(texts.to_vec())?, + ); + + Ok(()) + } + + pub fn redact_str(&self, text: &str) -> String { + self + .redact_engine + .read() + .expect("Failed to acquire read lock on redact engine") + .as_ref() + .expect("Failed to get redact engine") + .redact_str(text) + } +} + +/// Redact a value by replacing all but the first and last character with asterisks, +/// The length of the value to be redacted must be at least 3 characters. +/// e.g. "foo" -> "f**********o" +pub fn redact_value(text: &str) -> String { + if text.len() < 3 { + return text.to_string(); + } + + let mut redacted = String::new(); + redacted.push_str(&text[0..1]); + redacted.push_str(&"*".repeat(10)); + redacted.push_str(&text[text.len() - 1..]); + + redacted +} + +pub fn redact_uri(uri: &str) -> String { + let Ok(mut url) = Url::parse(uri) else { + return uri.to_string(); + }; + + // Could be a data: URI + if url.cannot_be_a_base() { + if url.scheme() == "about" { + return uri.to_string(); + } + + if url.path().len() > 15 { + return format!( + "{}:{}{}", + url.scheme(), + &url.path()[0..10], + redact_value(&url.path()[10..]) + ); + } + + return format!("{}:{}", url.scheme(), redact_value(url.path())); + } + + let host = url.host_str().unwrap_or_default(); + if url.set_host(Some(&redact_value(host))).is_err() { + let redacted_query = redact_query(url.query()) + .as_deref() + .map(|query| format!("?{}", query)) + .unwrap_or_default(); + + return format!( + "{}://[**********]{}{}", + url.scheme(), + url.path(), + redacted_query + ); + } + + let redacted_query = redact_query(url.query()); + url.set_query(redacted_query.as_deref()); + url.to_string() +} + +fn redact_query(query: Option<&str>) -> Option { + let query = query?; + + let query_pairs = form_urlencoded::parse(query.as_bytes()); + let mut redacted_pairs = query_pairs.map(|(key, value)| (key, redact_value(&value))); + + let query = form_urlencoded::Serializer::new(String::new()) + .extend_pairs(redacted_pairs.by_ref()) + .finish(); + + Some(query) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_should_not_redact_value() { + let text = "fo"; + + assert_eq!(redact_value(text), "fo"); + } + + #[test] + fn it_should_redact_value() { + let text = "foo"; + + assert_eq!(redact_value(text), "f**********o"); + } + + #[test] + fn it_should_redact_dynamic_value() { + let redaction = Redaction::new(); + + redaction.add_value("foo").unwrap(); + + assert_eq!( + redaction.redact_str("hello, foo, bar"), + "hello, [**********], bar" + ); + } + + #[test] + fn it_should_redact_dynamic_values() { + let redaction = Redaction::new(); + + redaction.add_values(&["foo", "bar"]).unwrap(); + + assert_eq!( + redaction.redact_str("hello, foo, bar"), + "hello, [**********], [**********]" + ); + } + + #[test] + fn it_should_redact_uri() { + let uri = "https://foo.bar"; + assert_eq!(redact_uri(uri), "https://f**********r/"); + + let uri = "https://foo.bar/"; + assert_eq!(redact_uri(uri), "https://f**********r/"); + + let uri = "https://foo.bar/baz"; + assert_eq!(redact_uri(uri), "https://f**********r/baz"); + + let uri = "https://foo.bar/baz?qux=quux"; + assert_eq!(redact_uri(uri), "https://f**********r/baz?qux=q**********x"); + } + + #[test] + fn it_should_redact_data_uri() { + let uri = "data:text/plain;a"; + assert_eq!(redact_uri(uri), "data:t**********a"); + + let uri = "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ=="; + assert_eq!(redact_uri(uri), "data:text/plain;**********="); + + let uri = "about:blank"; + assert_eq!(redact_uri(uri), "about:blank"); + } + + #[test] + fn it_should_redact_ipv6() { + let uri = "https://[2001:db8::1]:8080"; + assert_eq!(redact_uri(uri), "https://[**********]/"); + + let uri = "https://[2001:db8::1]:8080/"; + assert_eq!(redact_uri(uri), "https://[**********]/"); + + let uri = "https://[2001:db8::1]:8080/baz"; + assert_eq!(redact_uri(uri), "https://[**********]/baz"); + + let uri = "https://[2001:db8::1]:8080/baz?qux=quux"; + assert_eq!(redact_uri(uri), "https://[**********]/baz?qux=q**********x"); + } +} diff --git a/crates/gpapi/src/utils/shutdown_signal.rs b/crates/gpapi/src/utils/shutdown_signal.rs new file mode 100644 index 0000000..bab9069 --- /dev/null +++ b/crates/gpapi/src/utils/shutdown_signal.rs @@ -0,0 +1,22 @@ +use tokio::signal; + +pub async fn shutdown_signal() { + let ctrl_c = async { + signal::ctrl_c() + .await + .expect("failed to install Ctrl+C handler"); + }; + + #[cfg(unix)] + let terminate = async { + signal::unix::signal(signal::unix::SignalKind::terminate()) + .expect("failed to install signal handler") + .recv() + .await; + }; + + tokio::select! { + _ = ctrl_c => {}, + _ = terminate => {}, + } +} diff --git a/crates/gpapi/src/utils/window.rs b/crates/gpapi/src/utils/window.rs new file mode 100644 index 0000000..1b10d41 --- /dev/null +++ b/crates/gpapi/src/utils/window.rs @@ -0,0 +1,90 @@ +use std::{process::ExitStatus, time::Duration}; + +use anyhow::bail; +use log::{info, warn}; +use tauri::{window::MenuHandle, Window}; +use tokio::process::Command; + +pub trait WindowExt { + fn raise(&self) -> anyhow::Result<()>; +} + +impl WindowExt for Window { + fn raise(&self) -> anyhow::Result<()> { + raise_window(self) + } +} + +pub fn raise_window(win: &Window) -> anyhow::Result<()> { + let is_wayland = std::env::var("XDG_SESSION_TYPE").unwrap_or_default() == "wayland"; + + if is_wayland { + win.hide()?; + win.show()?; + } else { + if !win.is_visible()? { + win.show()?; + } + let title = win.title()?; + tokio::spawn(async move { + info!("Raising window: {}", title); + if let Err(err) = wmctrl_raise_window(&title).await { + warn!("Failed to raise window: {}", err); + } + }); + } + + // Calling window.show() on Windows will cause the menu to be shown. + hide_menu(win.menu_handle()); + + Ok(()) +} + +async fn wmctrl_raise_window(title: &str) -> anyhow::Result<()> { + let mut counter = 0; + + loop { + if let Ok(exit_status) = wmctrl_try_raise_window(title).await { + if exit_status.success() { + info!("Window raised after {} attempts", counter + 1); + return Ok(()); + } + } + + if counter >= 10 { + bail!("Failed to raise window: {}", title) + } + + counter += 1; + tokio::time::sleep(Duration::from_millis(100)).await; + } +} + +async fn wmctrl_try_raise_window(title: &str) -> anyhow::Result { + let exit_status = Command::new("wmctrl") + .arg("-F") + .arg("-a") + .arg(title) + .spawn()? + .wait() + .await?; + + Ok(exit_status) +} + +fn hide_menu(menu_handle: MenuHandle) { + tokio::spawn(async move { + loop { + let menu_visible = menu_handle.is_visible().unwrap_or(false); + + if !menu_visible { + break; + } + + if menu_visible { + let _ = menu_handle.hide(); + tokio::time::sleep(Duration::from_millis(10)).await; + } + } + }); +} diff --git a/crates/gpapi/src/utils/xml.rs b/crates/gpapi/src/utils/xml.rs new file mode 100644 index 0000000..674e866 --- /dev/null +++ b/crates/gpapi/src/utils/xml.rs @@ -0,0 +1,6 @@ +use roxmltree::Document; + +pub(crate) fn get_child_text(doc: &Document, name: &str) -> Option { + let node = doc.descendants().find(|n| n.has_tag_name(name))?; + node.text().map(|s| s.to_string()) +} diff --git a/crates/gpapi/tests/files/gateway_login.xml b/crates/gpapi/tests/files/gateway_login.xml new file mode 100644 index 0000000..cfb981f --- /dev/null +++ b/crates/gpapi/tests/files/gateway_login.xml @@ -0,0 +1,27 @@ + + + + (null) + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + XXX-GP-Gateway-N + user + AD_Authentication + vsys1 + corp.example.com + (null) + + + + tunnel + -1 + 4100 + + xxxxxx + aaaaaa + + 4 + unknown + + + \ No newline at end of file diff --git a/crates/gpapi/tests/files/portal_config.xml b/crates/gpapi/tests/files/portal_config.xml new file mode 100644 index 0000000..0aab3ca --- /dev/null +++ b/crates/gpapi/tests/files/portal_config.xml @@ -0,0 +1,212 @@ + + + vpn.example.com + 4100 + 6.0.1-19 + global-protect-full + **** + + + + -----BEGIN CERTIFICATE----- + -----END CERTIFICATE----- + + yes + + + + -----BEGIN CERTIFICATE----- + -----END CERTIFICATE----- + + yes + + + + -----BEGIN CERTIFICATE----- + -----END CERTIFICATE----- + + no + + + on-demand + yes + yes + 24 + + + + + yes + yes + + 365 + + vpn.example.com + + yes + + + + 5 + + + + + + 1 + + + 1 + vpn_gateway + + + + + + 5 + + + + xxx.xxx.xxx.xxx + + + 1 + + + 1 + + + + + + yes + + + 0 + 0 + + + + no + + + allowed + yes + yes + yes + yes + + no + no + + + 3600 + 20 + yes + + + antivirus + anti-spyware + host-info + data-loss-prevention + patch-management + firewall + anti-malware + disk-backup + disk-encryption + + + + + 1 + no + no + no + no + + allowed + prompt + yes + no + no + yes + yes + no + 30 + 5 + no + no + + + 0 + + 15 + yes + <div style="font-family:'Helvetica + Neue';"><h1 style="color:red;text-align:center; margin: 0; font-size: + 30px;">Notice</h1><p style="margin: 0;font-size: 15px; + line-height: 1.2em;">To access the network, you must first connect to + GlobalProtect.</p></div> + yes + no + <div style="font-family:'Helvetica + Neue';"><h1 style="color:red;text-align:center; margin: 0; font-size: + 30px;">Captive Portal Detected</h1><p style="margin: 0; font-size: + 15px; line-height: 1.2em;">GlobalProtect has temporarily permitted network + access for you to connect to the Internet. Follow instructions from your internet + provider.</p><p style="margin: 0; font-size: 15px; line-height: + 1.2em;">If you let the connection time out, open GlobalProtect and click Connect + to try again.</p></div> + 5 + user-and-machine + 7 + + yes + no + yes + yes + yes + 0 + 0 + 0 + 0 + yes + 0 + 1400 + 0 + no + 30 + 60 + 30 + network-traffic + yes + no + no + + no + yes + yes + no + 4501 + + You have attempted to access a protected resource that requires + additional authentication. Proceed to authenticate at + 0 + yes + + no + no + yes + + not-install + Access to the network from this device has been restricted as per + your organization's security policy. Please contact your IT Administrator. + Access to the network from this device has been restored as per + your organization's security policy. + + + user@example.com + xxxxxx + xxxxxx + 2d8e997765a2f59cbf80284b2f2fbd38 + diff --git a/crates/gpapi/tests/files/prelogin_saml.xml b/crates/gpapi/tests/files/prelogin_saml.xml new file mode 100644 index 0000000..bc07d9d --- /dev/null +++ b/crates/gpapi/tests/files/prelogin_saml.xml @@ -0,0 +1,22 @@ + + + Success + + false + + + Enter login credentials + Username + Password + 1 + yes + + + 0 + REDIRECT + 600 + 0 + U0FNTFJlcXVlc3Q9eHh4 + no + CN + \ No newline at end of file diff --git a/crates/gpapi/tests/files/prelogin_standard.xml b/crates/gpapi/tests/files/prelogin_standard.xml new file mode 100644 index 0000000..9d2ebab --- /dev/null +++ b/crates/gpapi/tests/files/prelogin_standard.xml @@ -0,0 +1,15 @@ + + + Success + + false + + + Enter login credentials + Username + Password + 1 + yes + no + US + \ No newline at end of file diff --git a/crates/openconnect/Cargo.toml b/crates/openconnect/Cargo.toml new file mode 100644 index 0000000..d3216a9 --- /dev/null +++ b/crates/openconnect/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "openconnect" +version.workspace = true +edition.workspace = true +license.workspace = true +links = "openconnect" + +[dependencies] +log.workspace = true +is_executable.workspace = true + +[build-dependencies] +cc = "1" diff --git a/crates/openconnect/build.rs b/crates/openconnect/build.rs new file mode 100644 index 0000000..42eb240 --- /dev/null +++ b/crates/openconnect/build.rs @@ -0,0 +1,12 @@ +fn main() { + // Link to the native openconnect library + println!("cargo:rustc-link-lib=openconnect"); + println!("cargo:rerun-if-changed=src/ffi/vpn.c"); + println!("cargo:rerun-if-changed=src/ffi/vpn.h"); + + // Compile the vpn.c file + cc::Build::new() + .file("src/ffi/vpn.c") + .include("src/ffi") + .compile("vpn"); +} diff --git a/crates/openconnect/src/ffi/mod.rs b/crates/openconnect/src/ffi/mod.rs new file mode 100644 index 0000000..733f246 --- /dev/null +++ b/crates/openconnect/src/ffi/mod.rs @@ -0,0 +1,71 @@ +use crate::Vpn; +use log::{debug, info, trace, warn}; +use std::ffi::{c_char, c_int, c_void}; + +#[repr(C)] +#[derive(Debug)] +pub(crate) struct ConnectOptions { + pub user_data: *mut c_void, + + pub server: *const c_char, + pub cookie: *const c_char, + pub user_agent: *const c_char, + + pub script: *const c_char, + pub os: *const c_char, + pub certificate: *const c_char, + pub servercert: *const c_char, +} + +#[link(name = "vpn")] +extern "C" { + #[link_name = "vpn_connect"] + fn vpn_connect( + options: *const ConnectOptions, + callback: extern "C" fn(i32, *mut c_void), + ) -> c_int; + + #[link_name = "vpn_disconnect"] + fn vpn_disconnect(); +} + +pub(crate) fn connect(options: &ConnectOptions) -> i32 { + unsafe { vpn_connect(options, on_vpn_connected) } +} + +pub(crate) fn disconnect() { + unsafe { vpn_disconnect() } +} + +#[no_mangle] +extern "C" fn on_vpn_connected(pipe_fd: i32, vpn: *mut c_void) { + let vpn = unsafe { &*(vpn as *const Vpn) }; + vpn.on_connected(pipe_fd); +} + +// Logger used in the C code. +// level: 0 = error, 1 = info, 2 = debug, 3 = trace +// map the error level log in openconnect to the warning level +#[no_mangle] +extern "C" fn vpn_log(level: i32, message: *const c_char) { + let message = unsafe { std::ffi::CStr::from_ptr(message) }; + let message = message.to_str().unwrap_or("Invalid log message"); + // Strip the trailing newline + let message = message.trim_end_matches('\n'); + + if level == 0 { + warn!("{}", message); + } else if level == 1 { + info!("{}", message); + } else if level == 2 { + debug!("{}", message); + } else if level == 3 { + trace!("{}", message); + } else { + warn!( + "Unknown log level: {}, enable DEBUG log level to see more details", + level + ); + debug!("{}", message); + } +} diff --git a/crates/openconnect/src/ffi/vpn.c b/crates/openconnect/src/ffi/vpn.c new file mode 100644 index 0000000..e3d00c3 --- /dev/null +++ b/crates/openconnect/src/ffi/vpn.c @@ -0,0 +1,144 @@ +#include +#include +#include +#include +#include +#include + +#include "vpn.h" + +void *g_user_data; + +static int g_cmd_pipe_fd; +static const char *g_vpnc_script; +static vpn_connected_callback on_vpn_connected; + +/* Validate the peer certificate */ +static int validate_peer_cert(__attribute__((unused)) void *_vpninfo, const char *reason) +{ + INFO("Validating peer cert: %s", reason); + return 0; +} + +/* Print progress messages */ +static void print_progress(__attribute__((unused)) void *_vpninfo, int level, const char *format, ...) +{ + va_list args; + va_start(args, format); + char *message = format_message(format, args); + va_end(args); + + if (message == NULL) + { + ERROR("Failed to format log message"); + } + else + { + LOG(level, message); + free(message); + } +} + +static void setup_tun_handler(void *_vpninfo) +{ + int ret = openconnect_setup_tun_device(_vpninfo, g_vpnc_script, NULL); + if (!ret) { + on_vpn_connected(g_cmd_pipe_fd, g_user_data); + } +} + +/* Initialize VPN connection */ +int vpn_connect(const vpn_options *options, vpn_connected_callback callback) +{ + INFO("openconnect version: %s", openconnect_get_version()); + struct openconnect_info *vpninfo; + struct utsname utsbuf; + + g_user_data = options->user_data; + g_vpnc_script = options->script; + on_vpn_connected = callback; + + INFO("User agent: %s", options->user_agent); + INFO("VPNC script: %s", options->script); + INFO("OS: %s", options->os); + + vpninfo = openconnect_vpninfo_new(options->user_agent, validate_peer_cert, NULL, NULL, print_progress, NULL); + + if (!vpninfo) + { + ERROR("openconnect_vpninfo_new failed"); + return 1; + } + + openconnect_set_loglevel(vpninfo, PRG_TRACE); + openconnect_init_ssl(); + openconnect_set_protocol(vpninfo, "gp"); + openconnect_set_hostname(vpninfo, options->server); + openconnect_set_cookie(vpninfo, options->cookie); + + if (options->os) { + openconnect_set_reported_os(vpninfo, options->os); + } + + if (options->certificate) + { + INFO("Setting client certificate: %s", options->certificate); + openconnect_set_client_cert(vpninfo, options->certificate, NULL); + } + + if (options->servercert) { + INFO("Setting server certificate: %s", options->servercert); + openconnect_set_system_trust(vpninfo, 0); + } + + g_cmd_pipe_fd = openconnect_setup_cmd_pipe(vpninfo); + if (g_cmd_pipe_fd < 0) + { + ERROR("openconnect_setup_cmd_pipe failed"); + return 1; + } + + if (!uname(&utsbuf)) + { + openconnect_set_localname(vpninfo, utsbuf.nodename); + } + + // Essential step + if (openconnect_make_cstp_connection(vpninfo) != 0) + { + ERROR("openconnect_make_cstp_connection failed"); + return 1; + } + + if (openconnect_setup_dtls(vpninfo, 60) != 0) + { + openconnect_disable_dtls(vpninfo); + } + + // Essential step + openconnect_set_setup_tun_handler(vpninfo, setup_tun_handler); + + while (1) + { + int ret = openconnect_mainloop(vpninfo, 300, 10); + + if (ret) + { + INFO("openconnect_mainloop returned %d, exiting", ret); + openconnect_vpninfo_free(vpninfo); + return ret; + } + + INFO("openconnect_mainloop returned 0, reconnecting"); + } +} + +/* Stop the VPN connection */ +void vpn_disconnect() +{ + char cmd = OC_CMD_CANCEL; + if (write(g_cmd_pipe_fd, &cmd, 1) < 0) + { + ERROR("Failed to write to command pipe, VPN connection may not be stopped"); + } +} diff --git a/crates/openconnect/src/ffi/vpn.h b/crates/openconnect/src/ffi/vpn.h new file mode 100644 index 0000000..91a31d4 --- /dev/null +++ b/crates/openconnect/src/ffi/vpn.h @@ -0,0 +1,68 @@ +#include +#include +#include +#include + +typedef void (*vpn_connected_callback)(int cmd_pipe_fd, void *user_data); + +typedef struct vpn_options +{ + void *user_data; + const char *server; + const char *cookie; + const char *user_agent; + + const char *script; + const char *os; + const char *certificate; + const char *servercert; +} vpn_options; + +int vpn_connect(const vpn_options *options, vpn_connected_callback callback); +void vpn_disconnect(); + +extern void vpn_log(int level, const char *msg); + +static char *format_message(const char *format, va_list args) +{ + va_list args_copy; + va_copy(args_copy, args); + int len = vsnprintf(NULL, 0, format, args_copy); + va_end(args_copy); + + char *buffer = malloc(len + 1); + if (buffer == NULL) + { + return NULL; + } + + vsnprintf(buffer, len + 1, format, args); + return buffer; +} + +static void _log(int level, ...) +{ + va_list args; + va_start(args, level); + + char *format = va_arg(args, char *); + char *message = format_message(format, args); + + va_end(args); + + if (message == NULL) + { + vpn_log(PRG_ERR, "Failed to format log message"); + } + else + { + vpn_log(level, message); + free(message); + } +} + +#define LOG(level, ...) _log(level, __VA_ARGS__) +#define ERROR(...) LOG(PRG_ERR, __VA_ARGS__) +#define INFO(...) LOG(PRG_INFO, __VA_ARGS__) +#define DEBUG(...) LOG(PRG_DEBUG, __VA_ARGS__) +#define TRACE(...) LOG(PRG_TRACE, __VA_ARGS__) diff --git a/crates/openconnect/src/lib.rs b/crates/openconnect/src/lib.rs new file mode 100644 index 0000000..80977a6 --- /dev/null +++ b/crates/openconnect/src/lib.rs @@ -0,0 +1,5 @@ +mod ffi; +mod vpn; +mod vpnc_script; + +pub use vpn::*; diff --git a/crates/openconnect/src/vpn.rs b/crates/openconnect/src/vpn.rs new file mode 100644 index 0000000..25905a6 --- /dev/null +++ b/crates/openconnect/src/vpn.rs @@ -0,0 +1,131 @@ +use std::{ + ffi::{c_char, CString}, + sync::{Arc, RwLock}, +}; + +use log::info; + +use crate::{ffi, vpnc_script::find_default_vpnc_script}; + +type OnConnectedCallback = Arc>>>; + +pub struct Vpn { + server: CString, + cookie: CString, + user_agent: CString, + script: CString, + os: CString, + certificate: Option, + servercert: Option, + + callback: OnConnectedCallback, +} + +impl Vpn { + pub fn builder(server: &str, cookie: &str) -> VpnBuilder { + VpnBuilder::new(server, cookie) + } + + pub fn connect(&self, on_connected: impl FnOnce() + 'static + Send + Sync) -> i32 { + self + .callback + .write() + .unwrap() + .replace(Box::new(on_connected)); + let options = self.build_connect_options(); + + ffi::connect(&options) + } + + pub(crate) fn on_connected(&self, pipe_fd: i32) { + info!("Connected to VPN, pipe_fd: {}", pipe_fd); + + if let Some(callback) = self.callback.write().unwrap().take() { + callback(); + } + } + + pub fn disconnect(&self) { + ffi::disconnect(); + } + + fn build_connect_options(&self) -> ffi::ConnectOptions { + ffi::ConnectOptions { + user_data: self as *const _ as *mut _, + + server: self.server.as_ptr(), + cookie: self.cookie.as_ptr(), + user_agent: self.user_agent.as_ptr(), + script: self.script.as_ptr(), + os: self.os.as_ptr(), + certificate: Self::option_to_ptr(&self.certificate), + servercert: Self::option_to_ptr(&self.servercert), + } + } + + fn option_to_ptr(option: &Option) -> *const c_char { + match option { + Some(value) => value.as_ptr(), + None => std::ptr::null(), + } + } +} + +pub struct VpnBuilder { + server: String, + cookie: String, + user_agent: Option, + script: Option, + os: Option, +} + +impl VpnBuilder { + fn new(server: &str, cookie: &str) -> Self { + Self { + server: server.to_string(), + cookie: cookie.to_string(), + user_agent: None, + script: None, + os: None, + } + } + + pub fn user_agent>>(mut self, user_agent: T) -> Self { + self.user_agent = user_agent.into(); + self + } + + pub fn script>>(mut self, script: T) -> Self { + self.script = script.into(); + self + } + + pub fn os>>(mut self, os: T) -> Self { + self.os = os.into(); + self + } + + pub fn build(self) -> Vpn { + let user_agent = self.user_agent.unwrap_or_default(); + let script = self + .script + .or_else(find_default_vpnc_script) + .unwrap_or_default(); + let os = self.os.unwrap_or("linux".to_string()); + + Vpn { + server: Self::to_cstring(&self.server), + cookie: Self::to_cstring(&self.cookie), + user_agent: Self::to_cstring(&user_agent), + script: Self::to_cstring(&script), + os: Self::to_cstring(&os), + certificate: None, + servercert: None, + callback: Default::default(), + } + } + + fn to_cstring(value: &str) -> CString { + CString::new(value.to_string()).expect("Failed to convert to CString") + } +} diff --git a/crates/openconnect/src/vpnc_script.rs b/crates/openconnect/src/vpnc_script.rs new file mode 100644 index 0000000..6a34918 --- /dev/null +++ b/crates/openconnect/src/vpnc_script.rs @@ -0,0 +1,23 @@ +use is_executable::IsExecutable; +use std::path::Path; + +const VPNC_SCRIPT_LOCATIONS: [&str; 5] = [ + "/usr/local/share/vpnc-scripts/vpnc-script", + "/usr/local/sbin/vpnc-script", + "/usr/share/vpnc-scripts/vpnc-script", + "/usr/sbin/vpnc-script", + "/etc/vpnc/vpnc-script", +]; + +pub(crate) fn find_default_vpnc_script() -> Option { + for location in VPNC_SCRIPT_LOCATIONS.iter() { + let path = Path::new(location); + if path.is_executable() { + return Some(location.to_string()); + } + } + + log::warn!("vpnc-script not found"); + + None +} diff --git a/debian/README.Debian b/debian/README.Debian deleted file mode 100644 index ab3e77e..0000000 --- a/debian/README.Debian +++ /dev/null @@ -1,5 +0,0 @@ -globalprotect-openconnect for Debian - -Added debian packaging - - -- Amit Joshi <> Fri, 29 May 2020 21:52:59 -0400 diff --git a/debian/changelog b/debian/changelog deleted file mode 100644 index 8d97c92..0000000 --- a/debian/changelog +++ /dev/null @@ -1,154 +0,0 @@ -globalprotect-openconnect (1.4.9-1) unstable; urgency=medium - - * Updated VERSION, Bumped 1.4.8 –> 1.4.9 - * fix: update cmake version - * fix: correct the package name - * fix: use the dev package - * fix: use qtkeychain package - * fix: add qt5-tools - * fix: add libsecret-1-dev - * fix: add pkg-config - * fix: use cmake 3.16 - * fix: add missing build dependency - * ci: fix CI - * Merge branch 'master' into develop - * feat: expose os-version to settings - * Add two missing dependencies for building on debian (#198) - * ci: assert no library missing - * fix: update qtkeychain - * ci: run gpclient after build - * fix: add qtkeychain - * chore: update CMake file - * Added install instructions for MX Linux. (#190) - * Credentials autocompleting (secure version) (#179) - * Read all saved Gateways (for selecting in Systray) (#181) - * copy install script for debian (#180) - * add es and pt support to shange status when connected to vpn (#162) - * fix: improve the cli support - * feat: add --reset option to gpclient - - -- Kevin Yue Sun, 08 Jan 2023 20:58:32 +0800 - -globalprotect-openconnect (1.4.8-1) unstable; urgency=medium - - * Updated VERSION, Bumped 1.4.7 –> 1.4.8 - * fix: fix compile error - * refactor: simplify the code - * chore: use auto to declare variables - * chore: use c++ 17 - * fix: clear cookies when click the Reset button - * fix: refine the authentication workflow - * chore: PLOG -> LOG - - -- Kevin Yue Sun, 12 Jun 2022 20:28:58 +0800 - -globalprotect-openconnect (1.4.7-1) unstable; urgency=medium - - * Updated VERSION, Bumped 1.4.6 –> 1.4.7 - * fix: release resources when properly - * fix: add support for parsing tokens from HTML - * handle html comment for saml result with okta 2fa (#156) - * chore: use auto to declare variable - * chore: simplify readme - - -- Kevin Yue Tue, 07 Jun 2022 21:46:04 +0800 - -globalprotect-openconnect (1.4.6-1) unstable; urgency=medium - - * Updated VERSION, Bumped 1.4.5 –> 1.4.6 - * feat: display address in gateway menu item - * fix: fix bug of parsing the portal response - - -- Kevin Yue Wed, 01 Jun 2022 23:55:50 +0800 - -globalprotect-openconnect (1.4.5-1) unstable; urgency=medium - - * Updated VERSION, Bumped 1.4.4 –> 1.4.5 - * chore: refine vscode settings - * fix: rollback dbus configuration - * feat: add option to start minimized - * packaging: fix postinst for debian - * packaging: add postinst for debian - * test: test debian packaging - * ci: fix the folder path - * chore: apt -> apt-get - * ci: verify debian package - * Revert "Revert "fix: improve the dbus security"" - * fix: improve the portal config parsing - * Revert "fix: improve the dbus security" - * fix: improve the dbus security - * fix: free resources in slots - * chore: refine cmake files - * fix: support high DPI screen - - -- Kevin Yue Sun, 29 May 2022 21:15:40 +0800 - -globalprotect-openconnect (1.4.4-1) unstable; urgency=medium - - * Updated VERSION, Bumped 1.4.3 –> 1.4.4 - * fix: support the HighDPI displays - * [misc] update the build script - * [ci] Enable build job for master branch - * [ci] Add ubuntu 22.04 - - -- Kevin Yue Sat, 14 May 2022 19:21:14 +0800 - -globalprotect-openconnect (1.4.3-1) unstable; urgency=medium - - * Updated VERSION, Bumped 1.4.2 –> 1.4.3 - * refine AUR packaging - * Prepare release 1.4.3 (#149) - - -- Kevin Yue Mon, 09 May 2022 22:20:54 +0800 - -globalprotect-openconnect (1.4.2-1) unstable; urgency=medium - - * Updated VERSION, Bumped 1.4.1 –> 1.4.2 - * Clear SSL_OP_LEGACY_SERVER_CONNECT (#146) - - -- Kevin Yue Fri, 06 May 2022 22:18:19 +0800 - -globalprotect-openconnect (1.4.1-1) unstable; urgency=medium - - * Updated VERSION, Bumped 1.4.0 –> 1.4.1 - * print the gpservice logs - * update AUR packaging - - -- Kevin Yue Thu, 03 Mar 2022 21:58:59 +0800 - -globalprotect-openconnect (1.4.0-1) unstable; urgency=medium - - * Updated VERSION, Bumped 1.3.4 –> 1.4.0 - * Fix gpservice after openconnect v8.20 (#124) - * Add 2FA support (#112) - * Add a scripting mode to GPClient (#110) - * Stop saving credentials (#111) - * update CI - * add editorconfig - * Update README.md - * Add a run entry (#108) - * update the installation instruction of Arch Linux - - -- Kevin Yue Wed, 02 Mar 2022 21:34:19 +0800 - -globalprotect-openconnect (1.3.4-1) unstable; urgency=medium - - * Updated VERSION, Bumped 1.3.3 –> 1.3.4 - * update packaging (#100) - * shorten the sponsor links - * Update README.md - * add sponsor links - * Adding application logs location in the README (#95) - * improve the doc - * Add snap packaging (#93) - * update doc - * Migrate to cmake and refine the code structure (#92) - * QStringView -> QString - - -- Kevin Yue Sun, 24 Oct 2021 12:13:24 +0800 - -globalprotect-openconnect (1.3.0-1) unstable; urgency=medium - - * Bump version to 1.3.0 - - -- Kevin Yue Thu, 09 Jul 2020 10:13:46 +0800 diff --git a/debian/compat b/debian/compat deleted file mode 100644 index b4de394..0000000 --- a/debian/compat +++ /dev/null @@ -1 +0,0 @@ -11 diff --git a/debian/control b/debian/control deleted file mode 100644 index b8645c3..0000000 --- a/debian/control +++ /dev/null @@ -1,13 +0,0 @@ -Source: globalprotect-openconnect -Section: net -Priority: optional -Maintainer: Kevin Yue -Build-Depends: cmake (>=3.10), pkg-config, debhelper (>=11~), qtbase5-dev, qttools5-dev, libqt5websockets5-dev (>=5.9), qtwebengine5-dev (>=5.9), qt5keychain-dev -Standards-Version: 4.1.4 -Homepage: https://github.com/yuezk/GlobalProtect-openconnect - -Package: globalprotect-openconnect -Architecture: any -Multi-Arch: foreign -Depends: ${misc:Depends}, ${shlibs:Depends}, openconnect (>=8.0), libqt5websockets5 (>=5.9), libqt5webengine5 (>=5.9), libqt5keychain1 -Description: A GlobalProtect VPN client (GUI) based on OpenConnect. diff --git a/debian/copyright b/debian/copyright deleted file mode 100644 index bdc7b6a..0000000 --- a/debian/copyright +++ /dev/null @@ -1,15 +0,0 @@ -Files: * -Copyright: 1975-present Kevin Yue -License: GPL-3+ - -Files: 3rdparty/plog -Copyright: 2016 Sergey Podobry (sergey.podobry at gmail.com) -License: MPL-2.0 - -Files: 3rdparty/qt-unix-signals -Copyright: 2014 Simon Knopp -License: MIT - -Files: 3rdparty/SingleApplication -Copyright: Itay Grudev 2015 - 2020 -License: MIT diff --git a/debian/patches/series b/debian/patches/series deleted file mode 100644 index 4a97dfa..0000000 --- a/debian/patches/series +++ /dev/null @@ -1 +0,0 @@ -# You must remove unused comment lines for the released package. diff --git a/debian/rules b/debian/rules deleted file mode 100755 index 6637bfa..0000000 --- a/debian/rules +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/make -f -# You must remove unused comment lines for the released package. -export DH_VERBOSE = 1 -export DEB_BUILD_MAINT_OPTIONS = hardening=+all -export DEB_CFLAGS_MAINT_APPEND = -Wall -pedantic -export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed - -export DEBIAN_PACKAGE=1 - -%: - dh $@ -override_dh_installsystemd: - dh_installsystemd gpservice.service diff --git a/debian/source/format b/debian/source/format deleted file mode 100644 index 9f67427..0000000 --- a/debian/source/format +++ /dev/null @@ -1 +0,0 @@ -3.0 (native) \ No newline at end of file diff --git a/debian/source/local-options b/debian/source/local-options deleted file mode 100644 index 00131ee..0000000 --- a/debian/source/local-options +++ /dev/null @@ -1,2 +0,0 @@ -#abort-on-upstream-changes -#unapply-patches diff --git a/debian/watch b/debian/watch deleted file mode 100644 index 1459ccd..0000000 --- a/debian/watch +++ /dev/null @@ -1,3 +0,0 @@ -version=4 -opts="mode=git,pgpmode=gittag" \ -https://github.com/yuezk/GlobalProtect-openconnect.git refs/tags/v([\d\.]+) diff --git a/packaging/aur/PKGBUILD b/packaging/aur/PKGBUILD deleted file mode 100644 index 7b663ee..0000000 --- a/packaging/aur/PKGBUILD +++ /dev/null @@ -1,40 +0,0 @@ -# Maintainer: Keinv Yue - -_pkgver="1.4.9" -_commit="acf184134a2ff19e4a39528bd6a7fbbafa4cf017" -pkgname=globalprotect-openconnect-git -pkgver=${_pkgver} -pkgrel=1 -pkgdesc="A GlobalProtect VPN client (GUI) for Linux based on Openconnect and built with Qt5, supports SAML auth mode. (development version)" -arch=(x86_64 aarch64) -url="https://github.com/yuezk/GlobalProtect-openconnect" -license=('GPL3') -backup=( - etc/gpservice/gp.conf -) -install=gp.install -depends=('openconnect>=8.0.0' qt5-base qt5-webengine qt5-websockets qt5-tools qtkeychain-qt5) -makedepends=(git cmake) -conflicts=('globalprotect-openconnect') -provides=('globalprotect-openconnect' 'gpclient' 'gpservice') - -source=(git+https://github.com/yuezk/GlobalProtect-openconnect#commit=${_commit}) -sha256sums=('SKIP') - -prepare() { - cd GlobalProtect-openconnect - echo "${_pkgver}" > VERSION -} - -build() { - cd GlobalProtect-openconnect - cmake -B build \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_CXX_FLAGS_RELEASE=-s - make -j$(nproc) -C build -} - -package() { - cd GlobalProtect-openconnect - make DESTDIR="$pkgdir/" install -C build -} diff --git a/packaging/aur/PKGBUILD.in b/packaging/aur/PKGBUILD.in deleted file mode 100644 index eb8b59b..0000000 --- a/packaging/aur/PKGBUILD.in +++ /dev/null @@ -1,40 +0,0 @@ -# Maintainer: Keinv Yue - -_pkgver="{VERSION}" -_commit="{COMMIT}" -pkgname=globalprotect-openconnect-git -pkgver=${_pkgver} -pkgrel=1 -pkgdesc="A GlobalProtect VPN client (GUI) for Linux based on Openconnect and built with Qt5, supports SAML auth mode. (development version)" -arch=(x86_64 aarch64) -url="https://github.com/yuezk/GlobalProtect-openconnect" -license=('GPL3') -backup=( - etc/gpservice/gp.conf -) -install=gp.install -depends=('openconnect>=8.0.0' qt5-base qt5-webengine qt5-websockets qt5-tools qtkeychain-qt5) -makedepends=(git cmake) -conflicts=('globalprotect-openconnect') -provides=('globalprotect-openconnect' 'gpclient' 'gpservice') - -source=(git+https://github.com/yuezk/GlobalProtect-openconnect#commit=${_commit}) -sha256sums=('SKIP') - -prepare() { - cd GlobalProtect-openconnect - echo "${_pkgver}" > VERSION -} - -build() { - cd GlobalProtect-openconnect - cmake -B build \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_CXX_FLAGS_RELEASE=-s - make -j$(nproc) -C build -} - -package() { - cd GlobalProtect-openconnect - make DESTDIR="$pkgdir/" install -C build -} diff --git a/packaging/aur/gp.install b/packaging/aur/gp.install deleted file mode 100755 index 063a2a3..0000000 --- a/packaging/aur/gp.install +++ /dev/null @@ -1,8 +0,0 @@ -post_install() { - systemctl enable gpservice.service - systemctl restart gpservice.service -} - -post_upgrade() { - post_install -} diff --git a/packaging/flatpak/com.yuezk.qt.gpclient.yml b/packaging/flatpak/com.yuezk.qt.gpclient.yml deleted file mode 100644 index 5384727..0000000 --- a/packaging/flatpak/com.yuezk.qt.gpclient.yml +++ /dev/null @@ -1,26 +0,0 @@ -app-id: com.yuezk.qt.gpclient -base: io.qt.qtwebengine.BaseApp -base-version: '5.15' -runtime: org.kde.Platform -runtime-version: '5.15' -sdk: org.kde.Sdk -command: gpclient -finish-args: - - --share=network - - --share=ipc - - --socket=x11 - - --socket=wayland - - --filesystem=host - - --device=dri - - --talk-name=org.kde.StatusNotifierWatcher - - --own-name=org.kde.* - - --system-own-name=com.yuezk.qt.GPService -modules: - - name: gpclient - buildsystem: cmake - config-opts: - - -DCMAKE_BUILD_TYPE=Release - - -DCMAKE_CXX_FLAGS_RELEASE=-s - sources: - - type: archive - path: globalprotect-openconnect.tar.gz diff --git a/packaging/obs/globalprotect-openconnect-rpmlintrc b/packaging/obs/globalprotect-openconnect-rpmlintrc deleted file mode 100644 index a43b389..0000000 --- a/packaging/obs/globalprotect-openconnect-rpmlintrc +++ /dev/null @@ -1 +0,0 @@ -setBadness('suse-dbus-unauthorized-service', 0) \ No newline at end of file diff --git a/packaging/obs/globalprotect-openconnect.changes b/packaging/obs/globalprotect-openconnect.changes deleted file mode 100644 index e90b111..0000000 --- a/packaging/obs/globalprotect-openconnect.changes +++ /dev/null @@ -1,154 +0,0 @@ -------------------------------------------------------------------- -Sun Jan 8 12:58:32 UTC 2023 - k3vinyue@gmail.com - 1.4.9 - -- Update to 1.4.9 - * Updated VERSION, Bumped 1.4.8 –> 1.4.9 - * fix: update cmake version - * fix: correct the package name - * fix: use the dev package - * fix: use qtkeychain package - * fix: add qt5-tools - * fix: add libsecret-1-dev - * fix: add pkg-config - * fix: use cmake 3.16 - * fix: add missing build dependency - * ci: fix CI - * Merge branch 'master' into develop - * feat: expose os-version to settings - * Add two missing dependencies for building on debian (#198) - * ci: assert no library missing - * fix: update qtkeychain - * ci: run gpclient after build - * fix: add qtkeychain - * chore: update CMake file - * Added install instructions for MX Linux. (#190) - * Credentials autocompleting (secure version) (#179) - * Read all saved Gateways (for selecting in Systray) (#181) - * copy install script for debian (#180) - * add es and pt support to change status when connected to vpn (#162) - * fix: improve the cli support - * feat: add --reset option to gpclient - -------------------------------------------------------------------- -Sun Jun 12 12:28:58 UTC 2022 - k3vinyue@gmail.com - 1.4.8 - -- Update to 1.4.8 - * Updated VERSION, Bumped 1.4.7 –> 1.4.8 - * fix: fix compile error - * refactor: simplify the code - * chore: use auto to declare variables - * chore: use c++ 17 - * fix: clear cookies when click the Reset button - * fix: refine the authentication workflow - * chore: PLOG -> LOG - -------------------------------------------------------------------- -Tue Jun 7 13:46:04 UTC 2022 - k3vinyue@gmail.com - 1.4.7 - -- Update to 1.4.7 - * Updated VERSION, Bumped 1.4.6 –> 1.4.7 - * fix: release resources when properly - * fix: add support for parsing tokens from HTML - * handle html comment for saml result with okta 2fa (#156) - * chore: use auto to declare variable - * chore: simplify readme - -------------------------------------------------------------------- -Wed Jun 1 15:55:50 UTC 2022 - k3vinyue@gmail.com - 1.4.6 - -- Update to 1.4.6 - * Updated VERSION, Bumped 1.4.5 –> 1.4.6 - * feat: display address in gateway menu item - * fix: fix bug of parsing the portal response - -------------------------------------------------------------------- -Sun May 29 13:15:40 UTC 2022 - k3vinyue@gmail.com - 1.4.5 - -- Update to 1.4.5 - * Updated VERSION, Bumped 1.4.4 –> 1.4.5 - * chore: refine vscode settings - * fix: rollback dbus configuration - * feat: add option to start minimized - * packaging: fix postinst for debian - * packaging: add postinst for debian - * test: test debian packaging - * ci: fix the folder path - * chore: apt -> apt-get - * ci: verify debian package - * Revert "Revert "fix: improve the dbus security"" - * fix: improve the portal config parsing - * Revert "fix: improve the dbus security" - * fix: improve the dbus security - * fix: free resources in slots - * chore: refine cmake files - * fix: support high DPI screen - -------------------------------------------------------------------- -Sat May 14 11:21:14 UTC 2022 - k3vinyue@gmail.com - 1.4.4 - -- Update to 1.4.4 - * Updated VERSION, Bumped 1.4.3 –> 1.4.4 - * fix: support the HighDPI displays - * [misc] update the build script - * [ci] Enable build job for master branch - * [ci] Add ubuntu 22.04 - -------------------------------------------------------------------- -Mon May 9 14:20:54 UTC 2022 - k3vinyue@gmail.com - 1.4.3 - -- Update to 1.4.3 - * Updated VERSION, Bumped 1.4.2 –> 1.4.3 - * refine AUR packaging - * Prepare release 1.4.3 (#149) - -------------------------------------------------------------------- -Fri May 6 14:18:19 UTC 2022 - k3vinyue@gmail.com - 1.4.2 - -- Update to 1.4.2 - * Updated VERSION, Bumped 1.4.1 –> 1.4.2 - * Clear SSL_OP_LEGACY_SERVER_CONNECT (#146) - -------------------------------------------------------------------- -Thu Mar 3 13:58:59 UTC 2022 - k3vinyue@gmail.com - 1.4.1 - -- Update to 1.4.1 - * Updated VERSION, Bumped 1.4.0 –> 1.4.1 - * print the gpservice logs - * update AUR packaging - -------------------------------------------------------------------- -Wed Mar 2 13:34:19 UTC 2022 - k3vinyue@gmail.com - 1.4.0 - -- Update to 1.4.0 - * Updated VERSION, Bumped 1.3.4 –> 1.4.0 - * Fix gpservice after openconnect v8.20 (#124) - * Add 2FA support (#112) - * Add a scripting mode to GPClient (#110) - * Stop saving credentials (#111) - * update CI - * add editorconfig - * Update README.md - * Add a run entry (#108) - * update the installation instruction of Arch Linux - -------------------------------------------------------------------- -Sun Oct 24 04:13:24 UTC 2021 - k3vinyue@gmail.com - 1.3.4 - -- Update to 1.3.4 - * Updated VERSION, Bumped 1.3.3 –> 1.3.4 - * update packaging (#100) - * shorten the sponsor links - * Update README.md - * add sponsor links - * Adding application logs location in the README (#95) - * improve the doc - * Add snap packaging (#93) - * update doc - * Migrate to cmake and refine the code structure (#92) - * QStringView -> QString - -------------------------------------------------------------------- -Thu Jul 9 02:13:46 UTC 2020 - k3vinyue@gmail.com - 1.3.0 - -- Update to 1.3.0 - * Bump version to 1.3.0 diff --git a/packaging/obs/globalprotect-openconnect.spec b/packaging/obs/globalprotect-openconnect.spec deleted file mode 100644 index defac74..0000000 --- a/packaging/obs/globalprotect-openconnect.spec +++ /dev/null @@ -1,98 +0,0 @@ -Name: globalprotect-openconnect -Version: 1.4.9 -Release: 1 -Summary: A GlobalProtect VPN client powered by OpenConnect -Group: Productivity/Networking/PPP -BuildRoot: %{_tmppath}/%{name}-%{version}-build - -License: GPL-3.0 -URL: https://github.com/yuezk/GlobalProtect-openconnect -Source0: %{name}.tar.gz -BuildRequires: cmake cmake(Qt5) cmake(Qt5Gui) cmake(Qt5WebEngine) cmake(Qt5WebSockets) cmake(Qt5DBus) cmake(Qt5Keychain) -BuildRequires: systemd-rpm-macros -Requires: openconnect >= 8.0 -Conflicts: globalprotect-openconnect-snapshot - - -%global debug_package %{nil} - -%description -A GlobalProtect VPN client (GUI) for Linux based on OpenConnect and built with Qt5, supports SAML auth mode. - - -%prep -%autosetup -n "globalprotect-openconnect-%{version}" - - -%pre - -%if 0%{?suse_version} - %service_add_pre gpservice.service -%endif - - -%post - -%if 0%{?suse_version} - %service_add_post gpservice.service -%else - %systemd_post gpservice.service -%endif - - -%preun - -%if 0%{?suse_version} - %service_del_preun gpservice.service -%else - %systemd_preun gpservice.service -%endif - - -%postun - -%if 0%{?suse_version} - %service_del_postun gpservice.service -%else - %systemd_postun_with_restart gpservice.service -%endif - - -%build - -%cmake -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_CXX_FLAGS_RELEASE=-s - -%if 0%{?fedora_version} && 0%{?fedora_version} <= 32 - %make_build -%else - %cmake_build -%endif - - -%install - -%if 0%{?fedora_version} && 0%{?fedora_version} <= 32 - %make_install -%else - %cmake_install -%endif - -%files -%defattr(-,root,root) -%{_unitdir}/gpservice.service -%{_bindir}/gpclient -%{_bindir}/gpservice -%{_datadir}/applications/com.yuezk.qt.gpclient.desktop -%{_datadir}/dbus-1/system-services/com.yuezk.qt.GPService.service -%{_datadir}/dbus-1/system.d/com.yuezk.qt.GPService.conf -%{_datadir}/icons/hicolor/scalable/apps/com.yuezk.qt.gpclient.svg -%{_datadir}/metainfo/com.yuezk.qt.gpclient.metainfo.xml -%config %{_sysconfdir}/gpservice/gp.conf - -%dir %{_sysconfdir}/gpservice -%dir %{_datadir}/icons/hicolor -%dir %{_datadir}/icons/hicolor/scalable -%dir %{_datadir}/icons/hicolor/scalable/apps - -%changelog diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..12f98a0 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,8 @@ +max_width = 100 +hard_tabs = false +tab_spaces = 2 +newline_style = "Unix" +reorder_imports = true +reorder_modules = true +edition = "2021" +merge_derives = true diff --git a/scripts/_archive-all.sh b/scripts/_archive-all.sh deleted file mode 100755 index ac1efeb..0000000 --- a/scripts/_archive-all.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -e - -VERSION="$(cat VERSION)" - -rm -rf ./artifacts && mkdir -p ./artifacts/{obs,aur,flatpak} - -# Add the version file -echo $VERSION > ./artifacts/VERSION - -# Archive the source code -git-archive-all \ - --force-submodules \ - --prefix=globalprotect-openconnect-${VERSION}/ \ - ./artifacts/globalprotect-openconnect-${VERSION}.tar.gz - -# Prepare the OBS package -cp -r ./packaging/obs ./artifacts -cp ./artifacts/*.tar.gz ./artifacts/obs/globalprotect-openconnect.tar.gz - -# Prepare the AUR package -cp ./packaging/aur/PKGBUILD ./artifacts/aur/PKGBUILD -cp ./packaging/aur/gp.install ./artifacts/aur/gp.install - -# Prepare the flatpak package -cp ./packaging/flatpak/com.yuezk.qt.gpclient.yml ./artifacts/flatpak -cp ./artifacts/*.tar.gz ./artifacts/flatpak/globalprotect-openconnect.tar.gz \ No newline at end of file diff --git a/scripts/build.sh b/scripts/build.sh deleted file mode 100755 index fd67bf8..0000000 --- a/scripts/build.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -e - -./cmakew -B build \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_CXX_FLAGS_RELEASE=-s - -MAKEFLAGS=-j$(nproc) ./cmakew --build build \ No newline at end of file diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh deleted file mode 100755 index ea5cdd5..0000000 --- a/scripts/bump-version.sh +++ /dev/null @@ -1,452 +0,0 @@ -#!/bin/bash -# -# █▄▄ █░█ █▀▄▀█ █▀█ ▄▄ █░█ █▀▀ █▀█ █▀ █ █▀█ █▄░█ -# █▄█ █▄█ █░▀░█ █▀▀ ░░ ▀▄▀ ██▄ █▀▄ ▄█ █ █▄█ █░▀█ -# -# Description: -# - This script automates bumping the git software project's version using automation. - -# - It does several things that are typically required for releasing a Git repository, like git tagging, -# automatic updating of CHANGELOG.md, and incrementing the version number in various JSON files. - -# - Increments / suggests the current software project's version number -# - Adds a Git tag, named after the chosen version number -# - Updates CHANGELOG.md -# - Updates VERSION file -# - Commits files to a new branch -# - Pushes to remote (optionally) -# - Updates "version" : "x.x.x" tag in JSON files if [-v file1 -v file2...] argument is supplied. -# -# Usage: -# ./bump-version.sh [-v ] [-m ] [-j ] [-j ].. [-n] [-p] [-b] [-h] -# -# Options: -# -v Specify a manual version number -# -m Custom release message. -# -f Update version number inside JSON files. -# * For multiple files, add a separate -f option for each one, -# * For example: ./bump-version.sh -f src/plugin/package.json -f composer.json -# -p Push commits to remote repository, eg `-p origin` -# -n Don't perform a commit automatically. -# * You may want to do that yourself, for example. -# -b Don't create automatic `release-` branch -# -h Show help message. - -# -# Detailed notes: -# – The contents of the `VERSION` file which should be a semantic version number such as "1.2.3" -# or even "1.2.3-beta+001.ab" -# -# – It pulls a list of changes from git history & prepends to a file called CHANGELOG.md -# under the title of the new version # number, allows the user to review and update the changelist -# -# – Creates a Git tag with the version number -# -# - Creates automatic `release-` branch -# -# – Commits the new version to the current repository -# -# – Optionally pushes the commit to remote repository -# -# – Make sure to set execute permissions for the script, eg `$ chmod 755 bump-version.sh` -# -# Credits: -# – https://github.com/jv-k/bump-version -# -# - Inspired by the scripts from @pete-otaqui and @mareksuscak -# https://gist.github.com/pete-otaqui/4188238 -# https://gist.github.com/mareksuscak/1f206fbc3bb9d97dec9c -# - -NOW="$(date +'%B %d, %Y')" - -# ANSI/VT100 colours -YELLOW='\033[1;33m' -LIGHTYELLOW='\033[0;33m' -RED='\033[0;31m' -LIGHTRED='\033[1;31m' -GREEN='\033[0;32m' -LIGHTGREEN='\033[1;32m' -BLUE='\033[0;34m' -LIGHTBLUE='\033[1;34m' -PURPLE='\033[0;35m' -LIGHTPURPLE='\033[1;35m' -CYAN='\033[0;36m' -LIGHTCYAN='\033[1;36m' -WHITE='\033[1;37m' -LIGHTGRAY='\033[0;37m' -DARKGRAY='\033[1;30m' -BOLD="\033[1m" -INVERT="\033[7m" -RESET='\033[0m' - -# Default options -FLAG_JSON="false" -FLAG_PUSH="false" - -I_OK="✅"; I_STOP="🚫"; I_ERROR="❌"; I_END="👋🏻" - -S_NORM="${WHITE}" -S_LIGHT="${LIGHTGRAY}" -S_NOTICE="${GREEN}" -S_QUESTION="${YELLOW}" -S_WARN="${LIGHTRED}" -S_ERROR="${RED}" - -V_SUGGEST="0.1.0" # This is suggested in case VERSION file or user supplied version via -v is missing -GIT_MSG="" -REL_NOTE="" -REL_PREFIX="release-" -PUSH_DEST="origin" - -# Show credits & help -usage() { - echo -e "$GREEN"\ - "\n █▄▄ █░█ █▀▄▀█ █▀█ ▄▄ █░█ █▀▀ █▀█ █▀ █ █▀█ █▄░█ "\ - "\n █▄█ █▄█ █░▀░█ █▀▀ ░░ ▀▄▀ ██▄ █▀▄ ▄█ █ █▄█ █░▀█ "\ - "\n\t\t\t\t\t$LIGHTGRAY v${SCRIPT_VER}"\ - - echo -e " ${S_NORM}${BOLD}Usage:${RESET}"\ - "\n $0 [-v ] [-m ] [-j ] [-j ].. [-n] [-p] [-h]" 1>&2; - - echo -e "\n ${S_NORM}${BOLD}Options:${RESET}" - echo -e " $S_WARN-v$S_NORM \tSpecify a manual version number" - echo -e " $S_WARN-m$S_NORM \tCustom release message." - echo -e " $S_WARN-f$S_NORM \tUpdate version number inside JSON files."\ - "\n\t\t\t* For multiple files, add a separate -f option for each one,"\ - "\n\t\t\t* For example: ./bump-version.sh -f src/plugin/package.json -f composer.json" - echo -e " $S_WARN-p$S_NORM \t\t\tPush commits to ORIGIN. " - echo -e " $S_WARN-n$S_NORM \t\t\tDon't perform a commit automatically. "\ - "\n\t\t\t* You may want to do that manually after checking everything, for example." - echo -e " $S_WARN-b$S_NORM \t\t\tDon't create automatic \`release-\` branch" - echo -e " $S_WARN-h$S_NORM \t\t\tShow this help message. " - echo -e "\n ${S_NORM}${BOLD}Author:$S_LIGHT https://github.com/jv-t/bump-version $RESET\n" - -} - -# If there are no commits in repo, quit, because you can't tag with zero commits. -check-commits-exist() { - git rev-parse HEAD &> /dev/null - if [ ! "$?" -eq 0 ]; then - echo -e "\n${I_STOP} ${S_ERROR}Your current branch doesn't have any commits yet. Can't tag without at least one commit." >&2 - echo - exit 1 - fi -} - -get-commit-msg() { - echo Bumped $([ -n "${V_PREV}" ] && echo "${V_PREV} –>" || echo "to ") "$V_USR_INPUT" -} - -exit_abnormal() { - echo -e " ${S_LIGHT}––––––" - usage # Show help - exit 1 -} - -# Process script options -process-arguments() { - local OPTIONS OPTIND OPTARG - - # Get positional parameters - JSON_FILES=( ) - while getopts ":v:p:m:f:hbn" OPTIONS; do # Note: Adding the first : before the flags takes control of flags and prevents default error msgs. - case "$OPTIONS" in - h ) - # Show help - exit_abnormal - ;; - v ) - # User has supplied a version number - V_USR_SUPPLIED=$OPTARG - ;; - m ) - REL_NOTE=$OPTARG - # Custom release note - echo -e "\n${S_LIGHT}Option set: ${S_NOTICE}Release note:" ${S_NORM}"'"$REL_NOTE"'" - ;; - f ) - FLAG_JSON=true - echo -e "\n${S_LIGHT}Option set: ${S_NOTICE}JSON file via [-f]: <${S_NORM}${OPTARG}${S_LIGHT}>" - # Store JSON filenames(s) - JSON_FILES+=($OPTARG) - ;; - p ) - FLAG_PUSH=true - PUSH_DEST=${OPTARG} # Replace default with user input - echo -e "\n${S_LIGHT}Option set: ${S_NOTICE}Pushing to <${S_NORM}${PUSH_DEST}${S_LIGHT}>, as the last action in this script." - ;; - n ) - FLAG_NOCOMMIT=true - echo -e "\n${S_LIGHT}Option set: ${S_NOTICE}Disable commit after tagging." - ;; - b ) - FLAG_NOBRANCH=true - echo -e "\n${S_LIGHT}Option set: ${S_NOTICE}Disable committing to new branch." - ;; - \? ) - echo -e "\n${I_ERROR}${S_ERROR} Invalid option: ${S_WARN}-$OPTARG" >&2 - echo - exit_abnormal - ;; - : ) - echo -e "\n${I_ERROR}${S_ERROR} Option ${S_WARN}-$OPTARG ${S_ERROR}requires an argument." >&2 - echo - exit_abnormal - ;; - esac - done -} - -# Suggests version from VERSION file, or grabs from user supplied -v . -# If none is set, suggest default from options. -process-version() { - if [ -f VERSION ] && [ -s VERSION ]; then - V_PREV=`cat VERSION` - - echo -e "\n${S_NOTICE}Current version from <${S_NORM}VERSION${S_NOTICE}> file: ${S_NORM}$V_PREV" - - # Suggest incremented value from VERSION file - V_PREV_LIST=(`echo $V_PREV | tr '.' ' '`) - V_MAJOR=${V_PREV_LIST[0]}; V_MINOR=${V_PREV_LIST[1]}; V_PATCH=${V_PREV_LIST[2]}; - - # Test if V_PATCH is a number, then increment it. Otherwise, do nothing - if [ "$V_PATCH" -eq "$V_PATCH" ] 2>/dev/null; then # discard stderr (2) output to black hole (suppress it) - V_PATCH=$((V_PATCH + 1)) # Increment - fi - - V_SUGGEST="$V_MAJOR.$V_MINOR.$V_PATCH" - else - echo -ne "\n${S_WARN}The [${S_NORM}VERSION${S_WARN}] " - if [ ! -f VERSION ]; then - echo "file was not found."; - elif [ ! -s VERSION ]; then - echo "file is empty."; - fi - fi - - # If a version number is supplied by the user with [-v ], then use it - if [ -n "$V_USR_SUPPLIED" ]; then - echo -e "\n${S_NOTICE}You selected version using [-v]:" "${S_WARN}${V_USR_SUPPLIED}" - V_USR_INPUT="${V_USR_SUPPLIED}" - else - echo -ne "\n${S_QUESTION}Enter a new version number [${S_NORM}$V_SUGGEST${S_QUESTION}]: " - echo -ne "$S_WARN" - read V_USR_INPUT - - if [ "$V_USR_INPUT" = "" ]; then - V_USR_INPUT="${V_SUGGEST}" - fi - fi - - # echo -e "${S_NOTICE}Setting version to [${S_NORM}${V_USR_INPUT}${S_NOTICE}] ...." -} - -# Only tag if tag doesn't already exist -check-tag-exists() { - TAG_CHECK_EXISTS=`git tag -l v"$V_USR_INPUT"` - if [ -n "$TAG_CHECK_EXISTS" ]; then - echo -e "\n${I_STOP} ${S_ERROR}Error: A release with that tag version number already exists!\n" - exit 0 - fi -} - -# $1 : version -# $2 : release note -tag() { - if [ -z "$2" ]; then - # Default release note - git tag -a "v$1" -m "Tag version $1." - else - # Custom release note - git tag -a "v$1" -m "$2" - fi - echo -e "\n${I_OK} ${S_NOTICE}Added GIT tag" -} - -# Change `version:` value in JSON files, like packager.json, composer.json, etc -bump-json-files() { - if [ "$FLAG_JSON" != true ]; then return; fi - - JSON_PROCESSED=( ) # holds filenames after they've been changed - - for FILE in "${JSON_FILES[@]}"; do - if [ -f $FILE ]; then - # Get the existing version number - V_OLD=$( sed -n 's/.*"version": "\(.*\)",/\1/p' $FILE ) - - if [ "$V_OLD" = "$V_USR_INPUT" ]; then - echo -e "\n${S_WARN}File <${S_NORM}$FILE${S_WARN}> already contains version: ${S_NORM}$V_OLD" - else - # Write to output file - FILE_MSG=`sed -i .temp "s/\"version\": \"$V_OLD\"/\"version\": \"$V_USR_INPUT\"/g" $FILE 2>&1` - if [ "$?" -eq 0 ]; then - echo -e "\n${I_OK} ${S_NOTICE}Updated file: <${S_NOTICE}$FILE${S_LIGHT}> from ${S_NORM}$V_OLD -> $V_USR_INPUT" - rm -f ${FILE}.temp - # Add file change to commit message: - GIT_MSG+="${GIT_MSG}Updated $FILE, " - else - echo -e "\n${I_STOP} ${S_ERROR}Error\n$PUSH_MSG\n" - fi - fi - - JSON_PROCESSED+=($FILE) - else - echo -e "\n${S_WARN}File <${S_NORM}$FILE${S_WARN}> not found." - fi - done - # Stage files that were changed: - [ -n "${JSON_PROCESSED}" ] && git add "${JSON_PROCESSED[@]}" -} - -# Handle VERSION file -do-versionfile() { - [ -f VERSION ] && ACTION_MSG="Updated" || ACTION_MSG="Created" - - GIT_MSG+="${ACTION_MSG} VERSION, " - echo $V_USR_INPUT | tr -d "\n" > VERSION # Create file - echo -e "\n${I_OK} ${S_NOTICE}${ACTION_MSG} [${S_NORM}VERSION${S_NOTICE}] file" - - # Stage file for commit - git add VERSION -} - -# Dump git log history to CHANGELOG.md -do-changelog() { - - # Log latest commits to CHANGELOG.md: - # Get latest commits - LOG_MSG=`git log --pretty=format:"- %s" $([ -n "$V_PREV" ] && echo "v${V_PREV}...HEAD") 2>&1` - if [ ! "$?" -eq 0 ]; then - echo -e "\n${I_STOP} ${S_ERROR}Error getting commit history for logging to CHANGELOG.\n$LOG_MSG\n" - exit 1 - fi - - [ -f CHANGELOG.md ] && ACTION_MSG="Updated" || ACTION_MSG="Created" - # Add info to commit message for later: - GIT_MSG+="${ACTION_MSG} CHANGELOG.md, " - - # Add heading - echo "## $V_USR_INPUT ($NOW)" > tmpfile - - # Log the bumping commit: - # - The final commit is done after do-changelog(), so we need to create the log entry for it manually: - echo "- ${GIT_MSG}$(get-commit-msg)" >> tmpfile - # Add previous commits - [ -n "$LOG_MSG" ] && echo "$LOG_MSG" >> tmpfile - - echo -en "\n" >> tmpfile - - if [ -f CHANGELOG.md ]; then - # Append existing log - cat CHANGELOG.md >> tmpfile - else - echo -e "\n${S_WARN}A [${S_NORM}CHANGELOG.md${S_WARN}] file was not found." - fi - - mv tmpfile CHANGELOG.md - - # User prompts - echo -e "\n${I_OK} ${S_NOTICE}${ACTION_MSG} [${S_NORM}CHANGELOG.md${S_NOTICE}] file" - # Pause & allow user to open and edit the file: - echo -en "\n${S_QUESTION}Make adjustments to [${S_NORM}CHANGELOG.md${S_QUESTION}] if required now. Press to continue." - read - - # Stage log file, to commit later - git add CHANGELOG.md -} - -# -check-branch-exist() { - [ "$FLAG_NOBRANCH" = true ] && return - - BRANCH_MSG=`git rev-parse --verify "${REL_PREFIX}${V_USR_INPUT}" 2>&1` - if [ "$?" -eq 0 ]; then - echo -e "\n${I_STOP} ${S_ERROR}Error: Branch <${S_NORM}${REL_PREFIX}${V_USR_INPUT}${S_ERROR}> already exists!\n" - exit 1 - fi -} - -# -do-branch() { - [ "$FLAG_NOBRANCH" = true ] && return - - echo -e "\n${S_NOTICE}Creating new release branch..." - - BRANCH_MSG=`git branch "${REL_PREFIX}${V_USR_INPUT}" 2>&1` - if [ ! "$?" -eq 0 ]; then - echo -e "\n${I_STOP} ${S_ERROR}Error\n$BRANCH_MSG\n" - exit 1 - else - BRANCH_MSG=`git checkout "${REL_PREFIX}${V_USR_INPUT}" 2>&1` - echo -e "\n${I_OK} ${S_NOTICE}${BRANCH_MSG}" - fi - - # REL_PREFIX -} - -# Stage & commit all files modified by this script -do-commit() { - [ "$FLAG_NOCOMMIT" = true ] && return - - GIT_MSG+="$(get-commit-msg)" - echo -e "\n${S_NOTICE}Committing..." - COMMIT_MSG=`git commit -m "${GIT_MSG}" 2>&1` - if [ ! "$?" -eq 0 ]; then - echo -e "\n${I_STOP} ${S_ERROR}Error\n$COMMIT_MSG\n" - exit 1 - else - echo -e "\n${I_OK} ${S_NOTICE}$COMMIT_MSG" - fi -} - -# Pushes files + tags to remote repo. Changes are staged by earlier functions -do-push() { - [ "$FLAG_NOCOMMIT" = true ] && return - - if [ "$FLAG_PUSH" = true ]; then - CONFIRM="Y" - else - echo -ne "\n${S_QUESTION}Push tags to <${S_NORM}${PUSH_DEST}${S_QUESTION}>? [${S_NORM}N/y${S_QUESTION}]: " - read CONFIRM - fi - - case "$CONFIRM" in - [yY][eE][sS]|[yY] ) - echo -e "\n${S_NOTICE}Pushing files + tags to <${S_NORM}${PUSH_DEST}${S_NOTICE}>..." - PUSH_MSG=`git push "${PUSH_DEST}" v"$V_USR_INPUT" 2>&1` # Push new tag - if [ ! "$?" -eq 0 ]; then - echo -e "\n${I_STOP} ${S_WARN}Warning\n$PUSH_MSG" - # exit 1 - else - echo -e "\n${I_OK} ${S_NOTICE}$PUSH_MSG" - fi - ;; - esac -} - -#### Initiate Script ########################### - -check-commits-exist - -# Process and prepare -process-arguments "$@" -process-version - -check-branch-exist -check-tag-exists - -echo -e "\n${S_LIGHT}––––––" - -# Update files -bump-json-files -do-versionfile -# do-changelog -# do-branch -do-commit -# tag "${V_USR_INPUT}" "${REL_NOTE}" -do-push - -echo -e "\n${S_LIGHT}––––––" -echo -e "\n${I_OK} ${S_NOTICE}"Bumped $([ -n "${V_PREV}" ] && echo "${V_PREV} –>" || echo "to ") "$V_USR_INPUT" -echo -e "\n${GREEN}Done ${I_END}\n" \ No newline at end of file diff --git a/scripts/install-debian.sh b/scripts/install-debian.sh deleted file mode 100755 index f6159be..0000000 --- a/scripts/install-debian.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -e - -sudo apt-get update -sudo apt-get install -y \ - build-essential \ - qtbase5-dev \ - libqt5websockets5-dev \ - qtwebengine5-dev \ - qttools5-dev \ - qt5keychain-dev \ - openconnect \ - -./scripts/install.sh diff --git a/scripts/install-fedora.sh b/scripts/install-fedora.sh deleted file mode 100755 index 469f554..0000000 --- a/scripts/install-fedora.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -e - -sudo dnf install -y \ - qt5-qtbase-devel \ - qt5-qtwebengine-devel \ - qt5-qtwebsockets-devel \ - qtkeychain-qt5-devel \ - openconnect - -./scripts/install.sh diff --git a/scripts/install-opensuse.sh b/scripts/install-opensuse.sh deleted file mode 100755 index 0e8bd91..0000000 --- a/scripts/install-opensuse.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -e - -sudo zypper install -y \ - libqt5-qtbase-devel \ - libqt5-qtwebsockets-devel \ - libqt5-qtwebengine-devel \ - qtkeychain-qt5-devel \ - openconnect - -./scripts/install.sh diff --git a/scripts/install-ubuntu.sh b/scripts/install-ubuntu.sh deleted file mode 100755 index 4e5e0df..0000000 --- a/scripts/install-ubuntu.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -e - -sudo apt-get update -sudo apt-get install -y \ - build-essential \ - qtbase5-dev \ - libqt5websockets5-dev \ - qtwebengine5-dev \ - qttools5-dev \ - qt5keychain-dev \ - openconnect - -./scripts/install.sh diff --git a/scripts/install.sh b/scripts/install.sh deleted file mode 100755 index 0a6543d..0000000 --- a/scripts/install.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash -e - -./scripts/build.sh - -sudo ./cmakew --install build - -sudo systemctl enable gpservice.service -sudo systemctl daemon-reload -sudo systemctl restart gpservice.service - -echo -e "\nSuccess. You can launch the GlobalProtect VPN client from the application dashboard.\n" \ No newline at end of file diff --git a/scripts/prepare-packaging.sh b/scripts/prepare-packaging.sh deleted file mode 100755 index 55d6a49..0000000 --- a/scripts/prepare-packaging.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/bash -e - -OLD_VERSION=$(git tag --sort=-v:refname --list "v[0-9]*" | head -n 1 | cut -c 2-) -NEW_VERSION="$(cat VERSION)" -HISTORY_ENTRIES=$(git log --format=" * %s" v${OLD_VERSION}.. | cat -n | sort -uk2 | sort -n | cut -f2-) - -function update_debian_changelog() { - local OLD_CHANGELOG=$(cat debian/changelog) - - cat > debian/changelog <<-EOF - globalprotect-openconnect (${NEW_VERSION}-1) unstable; urgency=medium - - ${HISTORY_ENTRIES} - - -- Kevin Yue $(date -R) - - ${OLD_CHANGELOG} - EOF -} - -function update_rpm_changelog() { - local OLD_CHANGELOG=$(cat packaging/obs/globalprotect-openconnect.changes) - - cat > packaging/obs/globalprotect-openconnect.changes <<-EOF - ------------------------------------------------------------------- - $(LC_ALL=en.US date -u "+%a %b %e %T %Z %Y") - k3vinyue@gmail.com - ${NEW_VERSION} - - - Update to ${NEW_VERSION} - ${HISTORY_ENTRIES} - - ${OLD_CHANGELOG} - EOF -} - -function generate_pkgbuild() { - local commit_id="$(git rev-parse HEAD)" - local version="$(cat VERSION)" - sed -e "s/{COMMIT}/${commit_id}/" -e "s/{VERSION}/${version}/" packaging/aur/PKGBUILD.in > packaging/aur/PKGBUILD -} - -# Update rpm version -sed -i"" -re "s/(Version:\s+).+/\1${NEW_VERSION}/" packaging/obs/globalprotect-openconnect.spec - -update_rpm_changelog -update_debian_changelog -generate_pkgbuild diff --git a/scripts/release-archive-all.sh b/scripts/release-archive-all.sh deleted file mode 100755 index 396e6f7..0000000 --- a/scripts/release-archive-all.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -e - -./scripts/_archive-all.sh \ No newline at end of file diff --git a/scripts/release.sh b/scripts/release.sh deleted file mode 100755 index 59fd11e..0000000 --- a/scripts/release.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -e - -VERSION=$(cat VERSION) - -# Update packaging, e.g., version, changelog, etc. -./scripts/prepare-packaging.sh - -# Commit the changes -git commit -m "Release ${VERSION}" . -git tag v$VERSION -a -m "Release ${VERSION}" \ No newline at end of file diff --git a/scripts/snapshot-archive-all.sh b/scripts/snapshot-archive-all.sh deleted file mode 100755 index 71b192c..0000000 --- a/scripts/snapshot-archive-all.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -e - -./scripts/snapshot-version.sh -./scripts/prepare-packaging.sh -./scripts/_archive-all.sh - -# Update the OBS packaging -mv ./artifacts/obs/globalprotect-openconnect.tar.gz ./artifacts/obs/globalprotect-openconnect-snapshot.tar.gz -mv ./artifacts/obs/globalprotect-openconnect.spec ./artifacts/obs/globalprotect-openconnect-snapshot.spec -mv ./artifacts/obs/globalprotect-openconnect.changes ./artifacts/obs/globalprotect-openconnect-snapshot.changes -mv ./artifacts/obs/globalprotect-openconnect-rpmlintrc ./artifacts/obs/globalprotect-openconnect-snapshot-rpmlintrc -sed -i"" -re "s/(Name:\s+).+/\1globalprotect-openconnect-snapshot/" \ - -re "s/(Conflicts:\s+).+/\1globalprotect-openconnect/" \ - ./artifacts/obs/globalprotect-openconnect-snapshot.spec diff --git a/scripts/snapshot-version.sh b/scripts/snapshot-version.sh deleted file mode 100755 index 99b60b8..0000000 --- a/scripts/snapshot-version.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -e - -VERSION="v$(cat VERSION)" -git describe --tags --match "${VERSION}" | sed -re 's/^v([^-]+)-([^-]+)-(.+)/\1+\2snapshot.\3/' > VERSION \ No newline at end of file diff --git a/scripts/verify-debian-package.sh b/scripts/verify-debian-package.sh deleted file mode 100755 index 9369618..0000000 --- a/scripts/verify-debian-package.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash -e - -sudo apt-get update -sudo apt-get install -y \ - build-essential \ - qtbase5-dev \ - libqt5websockets5-dev \ - qtwebengine5-dev \ - qt5keychain-dev \ - cmake \ - qttools5-dev \ - debhelper - -mkdir -p build - -cp ./artifacts/*.tar.gz build/ && cd build -tar -xzf *.tar.gz && cd globalprotect-openconnect-*/ - -dpkg-buildpackage -us -uc diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml deleted file mode 100644 index 4025185..0000000 --- a/snap/snapcraft.yaml +++ /dev/null @@ -1,92 +0,0 @@ -name: globalprotect-openconnect -base: core18 -confinement: strict -compression: lzo - -license: GPL-3.0 - -adopt-info: application - -package-repositories: - - type: apt - ppa: dwmw2/openconnect - -layout: - /usr/local/sbin: - bind: $SNAP/usr/sbin - /usr/share/vpnc-scripts: - bind: $SNAP/usr/share/vpnc-scripts - /usr/share/locale: - bind: $SNAP/usr/share/locale - -slots: - gpservice-slot: - interface: dbus - bus: system - name: com.yuezk.qt.GPService - -plugs: - gpservice-plug: - interface: dbus - bus: system - name: com.yuezk.qt.GPService - -apps: - gpservice: - common-id: com.yuezk.qt.gpservice - daemon: simple - command: usr/bin/gpservice - command-chain: - - snap/command-chain/desktop-launch - environment: - LC_ALL: en_US.UTF-8 - LANG: en_US.UTF-8 - plugs: - - network - slots: - - gpservice-slot - - gpclient: - common-id: com.yuezk.qt.gpclient - command: usr/bin/gpclient - desktop: usr/share/applications/com.yuezk.qt.gpclient.desktop - extensions: - - kde-neon - plugs: - - desktop - - desktop-legacy - - wayland - - unity7 - - x11 - - network - - gpservice-plug - -parts: - application: - override-pull: | - snapcraftctl pull - - VERSION=$(cat VERSION) - GRADE="stable" - - if echo "$VERSION" | grep -q "snapshot" - then - GRADE="devel" - fi - - snapcraftctl set-version "$VERSION" - snapcraftctl set-grade "$GRADE" - parse-info: - - usr/share/metainfo/com.yuezk.qt.gpclient.metainfo.xml - plugin: cmake - source: . - build-packages: - - libglu1-mesa-dev - build-snaps: - - kde-frameworks-5-core18-sdk - stage-packages: - - openconnect - - libatm1 - configflags: - - -DCMAKE_INSTALL_PREFIX=/usr - - -DCMAKE_BUILD_TYPE=Release \ No newline at end of file diff --git a/version.h.in b/version.h.in deleted file mode 100644 index eaab018..0000000 --- a/version.h.in +++ /dev/null @@ -1 +0,0 @@ -#define VERSION "@version@"