Compare commits

..

129 Commits

Author SHA1 Message Date
Kevin Yue
9655b735a1 Fix ignore TLS errors 2024-01-22 23:20:25 -05:00
Kevin Yue
c3bd7aeb93 Support SSO using default browser 2024-01-22 09:43:44 -05:00
Kevin Yue
0b55a80317 Bump version 2.0.0-beta4 2024-01-21 11:05:15 -05:00
Kevin Yue
c6315bf384 Handle auth window auth fail 2024-01-21 11:04:35 -05:00
Kevin Yue
87b965f80c Add default os-version for CLI 2024-01-21 08:54:08 -05:00
Kevin Yue
b09b21ae0f Bump 2.0.0-beta3 2024-01-21 05:43:49 -05:00
Kevin Yue
7e372cd113 Align with the old behavior of the portal config request (#293) 2024-01-21 18:31:39 +08:00
Kevin Yue
1e211e8912 Update README.md 2024-01-20 22:55:35 -05:00
Kevin Yue
8bc4049a0f Enhancements and Bug Fixes: Align Pre-login Behavior, TLS Error Ignorance, GUI Auto-Launch, and Documentation Improvements (#291) 2024-01-21 10:43:47 +08:00
Kevin Yue
03f8c98cb5 Use uzers crate 2024-01-18 08:54:08 -05:00
Kevin Yue
5c56acc677 Bump version 2.0.0-beta2 2024-01-18 08:51:11 -05:00
Kevin Yue
2d8393dcf7 Update doc (#282) 2024-01-18 20:48:40 +08:00
Kevin Yue
04a916a3e1 Refactor using Tauri (#278) 2024-01-16 22:18:20 +08:00
Kevin Yue
edc13ed14d Merge pull request #265 from fftmp/master
fix link in Readme
2023-11-13 18:04:46 +08:00
fftmp
dd737bc8c5 fix link in Readme 2023-11-10 22:58:40 +04:00
Kevin Yue
939f2bd94a Merge pull request #263 from iamtalhaasghar/master
chores: update opensuse leap repo link
2023-11-06 09:31:14 +08:00
Talha Asghar
abffa21268 chores: update opensuse leap repo link
The old link is broken!
2023-11-04 09:55:26 +05:00
Danilo Nascimento
705b03c0bb Fix: handshake failed by ERR_CERT_AUTHORITY_INVALID (#240) 2023-06-27 20:30:25 +08:00
Dimitri Papadopoulos Orfanos
7bef2ccc68 Fix typos found by codespell (#234) 2023-05-09 09:44:05 +08:00
Dmitry Mikushin
bffc5d733b Fixing binary paths array wrongly iterated up to binaryPaths->length() (#216) 2023-02-17 12:08:09 +08:00
Kevin Yue
8ca2610550 Release 1.4.9 2023-01-08 20:58:32 +08:00
Kevin Yue
acf184134a Updated VERSION, Bumped 1.4.8 –> 1.4.9 2023-01-08 20:58:21 +08:00
Kevin Yue
4a3f74f1c3 fix: update cmake version 2023-01-08 20:25:11 +08:00
Kevin Yue
b39983a0f8 fix: correct the package name 2023-01-08 19:57:36 +08:00
Kevin Yue
d6fa32d95d fix: correct the package name 2023-01-08 19:48:48 +08:00
Kevin Yue
7c299f6e68 fix: correct the package name 2023-01-08 19:42:12 +08:00
Kevin Yue
25e8ccd07e fix: use the dev package 2023-01-08 19:25:43 +08:00
Kevin Yue
092123b075 fix: use qtkeychain package 2023-01-08 19:21:44 +08:00
Kevin Yue
feb2956cc1 fix: add qt5-tools 2023-01-08 17:44:56 +08:00
Kevin Yue
d356839859 fix: add libsecret-1-dev 2023-01-03 12:25:55 +08:00
Kevin Yue
2ff39fd14e fix: add pkg-config 2023-01-03 11:39:35 +08:00
Kevin Yue
c3d300c807 fix: use cmake 3.16 2023-01-03 10:43:51 +08:00
Kevin Yue
ef43d10a70 fix: add missing build dependency 2023-01-02 20:27:52 +08:00
Kevin Yue
bd73466e48 ci: fix CI 2023-01-02 20:10:35 +08:00
Kevin Yue
cc2c0ae34e ci: fix CI 2023-01-02 19:56:45 +08:00
Kevin Yue
9207f7a798 Merge branch 'master' into develop 2023-01-02 19:47:58 +08:00
Kevin Yue
2069b7fd8e feat: expose os-version to settings 2023-01-01 17:18:50 +08:00
Nils Goroll
f552ef6204 Add two missing dependencies for building on debian (#198) 2022-12-08 17:41:23 +08:00
Kevin Yue
2761f7521a ci: assert no library missing 2022-10-30 21:48:46 +08:00
Kevin Yue
c3939a774b fix: update qtkeychain 2022-10-30 21:35:36 +08:00
Kevin Yue
49e5242bf2 ci: run gpclient after build 2022-10-30 21:28:26 +08:00
Kevin Yue
3181d37b20 fix: add qtkeychain 2022-10-30 21:21:47 +08:00
Kevin Yue
6d788a5e91 chore: update CMake file 2022-10-30 21:15:17 +08:00
VJatla
74c7549444 Added install instructions for MX Linux. (#190) 2022-10-30 19:07:27 +08:00
Carlo Ramponi
c52ccb87f1 Credentials autocompleting (secure version) (#179) 2022-10-12 10:25:49 +08:00
gmarco
fab25848e1 Read all saved Gateways (for selecting in Systray) (#181) 2022-10-07 12:37:51 +08:00
simonleary-umass-edu
75a24c89cd copy install script for debian (#180)
Co-authored-by: simon <simon.leary42@gmail.com>
2022-08-31 16:28:11 +08:00
Joe
15a73b7dba add es and pt support to shange status when connected to vpn (#162) 2022-06-20 10:28:02 +08:00
Kevin Yue
0adeaf9c28 fix: improve the cli support 2022-06-14 21:21:11 +08:00
Kevin Yue
fe64b2cd19 feat: add --reset option to gpclient 2022-06-14 21:14:16 +08:00
Kevin Yue
5788474d7e Release 1.4.8 2022-06-12 20:28:58 +08:00
Kevin Yue
3559834762 Updated VERSION, Bumped 1.4.7 –> 1.4.8 2022-06-12 20:28:49 +08:00
Kevin Yue
f9926b4026 fix: fix compile error 2022-06-12 20:21:07 +08:00
Kevin Yue
cb457c4b09 refactor: simplify the code 2022-06-12 20:15:12 +08:00
Kevin Yue
5ebfe9b0f4 chore: use auto to declare variables 2022-06-12 16:44:07 +08:00
Kevin Yue
35266dd8bf chore: use c++ 17 2022-06-12 15:40:46 +08:00
Kevin Yue
bf03d375e0 fix: clear cookies when click the Reset button 2022-06-12 13:52:36 +08:00
Kevin Yue
6cf909e34f fix: refine the authentication workflow 2022-06-11 21:13:03 +08:00
Kevin Yue
343a6d03c1 chore: PLOG -> LOG 2022-06-10 21:35:56 +08:00
Kevin Yue
fab8e7591e Release 1.4.7 2022-06-07 21:46:04 +08:00
Kevin Yue
5a485197b7 Updated VERSION, Bumped 1.4.6 –> 1.4.7 2022-06-07 21:45:49 +08:00
Kevin Yue
7bc02a4208 fix: release resources when properly 2022-06-06 18:05:08 +08:00
Kevin Yue
3067e6e911 fix: add support for parsing tokens from HTML 2022-06-06 15:01:50 +08:00
Samar Dhwoj Acharya
5db77e8404 handle html comment for saml result with okta 2fa (#156) 2022-06-06 13:39:06 +08:00
Kevin Yue
5714063457 chore: use auto to declare variable 2022-06-02 00:19:37 +08:00
Kevin Yue
41f88ed2e0 chore: simplify readme 2022-06-02 00:08:29 +08:00
Kevin Yue
4fada9bd14 Release 1.4.6 2022-06-01 23:55:50 +08:00
Kevin Yue
b57fb993ca Updated VERSION, Bumped 1.4.5 –> 1.4.6 2022-06-01 23:55:40 +08:00
Kevin Yue
f6d06ed978 feat: display address in gateway menu item 2022-06-01 23:53:02 +08:00
Kevin Yue
cc67de3a2b fix: fix bug of parsing the portal respponse 2022-06-01 23:52:12 +08:00
Kevin Yue
e2d28c83b2 Release 1.4.5 2022-05-29 21:15:40 +08:00
Kevin Yue
a489c5881b Updated VERSION, Bumped 1.4.4 –> 1.4.5 2022-05-29 21:15:32 +08:00
Kevin Yue
44fd2f1d3f chore: refine vscode settings 2022-05-29 21:15:01 +08:00
Kevin Yue
9c9b42b87f fix: rollback dbus configuration 2022-05-29 21:00:37 +08:00
Kevin Yue
fb2b148b72 feat: add option to start minimized 2022-05-29 17:33:12 +08:00
Kevin Yue
64bec9660a packaging: fix postinst for debian 2022-05-27 21:32:33 +08:00
Kevin Yue
0619e91bf5 packaging: add postinst for debian 2022-05-26 21:44:31 +08:00
Kevin Yue
048aa4799f test: test debian packaging 2022-05-26 15:33:39 +08:00
Kevin Yue
db0e8b801d test: test debian packaging 2022-05-26 15:12:25 +08:00
Kevin Yue
d03bbc339e test: test debian packaging 2022-05-26 15:06:17 +08:00
Kevin Yue
1312d54d08 test: test debian packaging 2022-05-26 14:41:10 +08:00
Kevin Yue
39f99d9143 test: test debian packaging 2022-05-26 14:23:29 +08:00
Kevin Yue
7a4eb0def3 ci: fix the foder path 2022-05-26 14:13:47 +08:00
Kevin Yue
d9b2094edd chore: apt -> apt-get 2022-05-26 14:11:38 +08:00
Kevin Yue
e6118af9f3 ci: verify debian package 2022-05-26 14:05:59 +08:00
Kevin Yue
108b4be3ec test: test debian packaging 2022-05-26 13:16:20 +08:00
Kevin Yue
65c59e47ec Revert "Revert "fix: improve the dbus security""
This reverts commit 4940830885.
2022-05-26 11:56:14 +08:00
Kevin Yue
177da7f3a2 Revert "Revert "fix: improve the dbus security""
This reverts commit ffa99d3783.
2022-05-26 11:56:06 +08:00
Kevin Yue
d5cd90373b fix: improve the portal config parsing 2022-05-26 11:48:55 +08:00
Kevin Yue
ffa99d3783 Revert "fix: improve the dbus security"
This reverts commit 829298bb84.
2022-05-23 22:20:06 +08:00
Kevin Yue
4940830885 Revert "fix: improve the dbus security"
This reverts commit ad178fe56c.
2022-05-23 22:20:03 +08:00
Kevin Yue
ad178fe56c fix: improve the dbus security 2022-05-23 21:55:21 +08:00
Kevin Yue
829298bb84 fix: improve the dbus security 2022-05-23 21:24:22 +08:00
Kevin Yue
8fe717d844 fix: free resources in slots 2022-05-22 23:17:11 +08:00
Kevin Yue
dffbc64ef5 chore: refine cmake files 2022-05-21 20:55:05 +08:00
Kevin Yue
b99c5a8391 fix: support high DPI screen 2022-05-21 11:43:17 +08:00
Kevin Yue
c2f7576d10 Release 1.4.4 2022-05-14 19:21:14 +08:00
Kevin Yue
4327235093 Updated VERSION, Bumped 1.4.3 –> 1.4.4 2022-05-14 19:21:03 +08:00
Kevin Yue
0699878b92 fix: support the HighDPI displays
Refs: #115
2022-05-14 19:12:07 +08:00
Kevin Yue
e3aba11506 [misc] update the build script 2022-05-09 22:40:00 +08:00
Kevin Yue
ff58258d5c [ci] Enable build job for master branch 2022-05-09 22:26:22 +08:00
Kevin Yue
991cf25a7b [ci] Add ubuntu 22.04 2022-05-09 22:23:08 +08:00
Kevin Yue
02c70150ba Release 1.4.3 2022-05-09 22:20:54 +08:00
Kevin Yue
28d8321958 Updated VERSION, Bumped 1.4.2 –> 1.4.3 2022-05-09 22:20:46 +08:00
Kevin Yue
e1c9180cae refine AUR packaging 2022-05-09 22:09:27 +08:00
Kevin Yue
57df34fd1e Prepare release 1.4.3 (#149)
* add inih

* add configuration file for gpservice

* Disable the UI configuration for extra args

* remove VERSION_SUFFIX

* remove ppa-publish.sh

* Use Git repo as the source for PKGBUILD

* remove VERSION_SUFFIX

* Use Git repo as the source for PKGBUILD

* add .install for PKGBUILD

* add configuration file

* Fix cmake

* Fix cmake

* Disable snap job

* update AUR packaging

* Disable the UI configuration for extra args

* improve packaging script

* update README.md

* restart gpservice after package upgrading
2022-05-09 21:58:58 +08:00
Kevin Yue
04d180e11a Release 1.4.2 2022-05-06 22:18:19 +08:00
Kevin Yue
6d3b127569 Updated VERSION, Bumped 1.4.1 –> 1.4.2 2022-05-06 22:17:49 +08:00
Erik Lindblad
e72b25e415 Clear SSL_OP_LEGACY_SERVER_CONNECT (#146)
Co-authored-by: Erik Lindblad <erili@spotify.com>
2022-05-06 21:26:27 +08:00
Kevin Yue
37a511c24d Release 1.4.1 2022-03-03 21:58:59 +08:00
Kevin Yue
ad7db36c92 Updated VERSION, Bumped 1.4.0 –> 1.4.1 2022-03-03 21:58:27 +08:00
Kevin Yue
11dc5920ef print the gpservice logs 2022-03-03 21:30:33 +08:00
Kevin Yue
e6383916c7 update AUR packaging 2022-03-02 22:11:47 +08:00
Kevin Yue
1d9d928b26 update AUR packaging 2022-03-02 22:06:26 +08:00
Kevin Yue
c02ad5d46d Release 1.4.0 2022-03-02 21:34:19 +08:00
Kevin Yue
2319c7c49c Updated VERSION, Bumped 1.3.4 –> 1.4.0 2022-03-02 21:28:02 +08:00
David Cohen
e0c2c14dc3 Fix gpservice after openconnect v8.20 (#124) 2022-03-01 15:41:29 +08:00
Kevin Yue
8f27c92e7b Add 2FA support (#112) 2021-12-20 22:20:02 +08:00
Karolin Varner
9d6ec84c14 Add a scripting mode to GPClient (#110) 2021-12-20 18:46:16 +08:00
Kevin Yue
dd81ed9519 Stop saving credentials (#111) 2021-12-20 18:43:37 +08:00
Kevin Yue
32bd713965 update CI 2021-12-20 18:32:18 +08:00
Kevin Yue
ba92517141 add editorconfig 2021-12-20 18:31:56 +08:00
Kevin Yue
0e4e082594 Update README.md 2021-11-30 16:42:04 +08:00
Kevin Yue
3e590cab7b Update README.md 2021-11-30 10:44:38 +08:00
Aloïs de Gouvello
3e0e4cff12 Add a run entry (#108)
Fix #107
2021-11-30 10:43:56 +08:00
Kevin Yue
692df2f2c5 update the installation instruction of Arch Linux 2021-11-01 17:12:15 +08:00
Kevin Yue
f2b9ffddde Update README.md 2021-10-30 09:41:32 +08:00
Kevin Yue
ca38925066 Update README.md 2021-10-24 17:26:02 +08:00
Kevin Yue
8591dd7e81 Update README.md 2021-10-24 16:43:01 +08:00
183 changed files with 10527 additions and 5800 deletions

62
.devcontainer/Dockerfile Normal file
View File

@@ -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;

View File

@@ -0,0 +1,10 @@
{
"build": {
"dockerfile": "Dockerfile"
},
"runArgs": [
"--privileged",
"--cap-add=NET_ADMIN",
"--device=/dev/net/tun"
]
}

9
.editorconfig Normal file
View File

@@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

116
.github/workflows/build.yaml vendored Normal file
View File

@@ -0,0 +1,116 @@
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

View File

@@ -1,354 +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]
runs-on: ${{ matrix.os }}
steps:
# Checkout repository and submodules
- uses: actions/checkout@v2
with:
submodules: recursive
- name: Build
run: |
./scripts/install-ubuntu.sh
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
- 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: Import GPG key
id: import_gpg
uses: crazy-max/ghaction-import-gpg@v4
with:
gpg_private_key: ${{ secrets.PPA_GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.PPA_GPG_PASSPHRASE }}
- name: Install dependencies
run: |
sudo apt update
sudo apt install debmake debhelper cmake \
libqt5websockets5-dev qtbase5-dev qtwebengine5-dev
- name: Build deb package for 18.04
run: |
cd $GITHUB_WORKSPACE/artifacts
mkdir build-18.04
cp *.tar.gz build-18.04 && cd build-18.04
tar xf *.tar.gz
cd globalprotect-openconnect-*/
PPA_GPG_PASSPHRASE=${{ secrets.PPA_GPG_PASSPHRASE }} \
PPA_GPG_KEYID=${{ steps.import_gpg.outputs.keyid }} ./scripts/ppa-publish.sh 18.04
- name: Build deb package for 20.04
run: |
cd $GITHUB_WORKSPACE/artifacts
mkdir build-20.04
cp *.tar.gz build-20.04 && cd build-20.04
tar xf *.tar.gz
cd globalprotect-openconnect-*/
PPA_GPG_PASSPHRASE=${{ secrets.PPA_GPG_PASSPHRASE }} \
PPA_GPG_KEYID=${{ steps.import_gpg.outputs.keyid }} ./scripts/ppa-publish.sh 20.04
- name: Build deb package for 21.04
run: |
cd $GITHUB_WORKSPACE/artifacts
mkdir build-21.04
cp *.tar.gz build-21.04 && cd build-21.04
tar xf *.tar.gz
cd globalprotect-openconnect-*/
PPA_GPG_PASSPHRASE=${{ secrets.PPA_GPG_PASSPHRASE }} \
PPA_GPG_KEYID=${{ steps.import_gpg.outputs.keyid }} ./scripts/ppa-publish.sh 21.04
- name: Build deb package for 21.10
run: |
cd $GITHUB_WORKSPACE/artifacts
mkdir build-21.10
cp *.tar.gz build-21.10 && cd build-21.10
tar xf *.tar.gz
cd globalprotect-openconnect-*/
PPA_GPG_PASSPHRASE=${{ secrets.PPA_GPG_PASSPHRASE }} \
PPA_GPG_KEYID=${{ steps.import_gpg.outputs.keyid }} ./scripts/ppa-publish.sh 21.10
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
env:
VERSION: $(cat ./artifacts/VERSION)
uses: yuezk/github-actions-deploy-aur@update-pkgver
with:
pkgname: globalprotect-openconnect-git
pkgbuild: ./artifacts/aur/PKGBUILD
assets: ./artifacts/aur/*.tar.gz
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' }}
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
- 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: Import GPG key
id: import_gpg
uses: crazy-max/ghaction-import-gpg@v4
with:
gpg_private_key: ${{ secrets.PPA_GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.PPA_GPG_PASSPHRASE }}
- name: Install dependencies
run: |
sudo apt update
sudo apt install debmake debhelper cmake \
libqt5websockets5-dev qtbase5-dev qtwebengine5-dev
- name: Build deb package for 18.04
run: |
cd $GITHUB_WORKSPACE/artifacts
mkdir build-18.04
cp *.tar.gz build-18.04 && cd build-18.04
tar xf *.tar.gz
cd globalprotect-openconnect-*/
PPA_GPG_PASSPHRASE=${{ secrets.PPA_GPG_PASSPHRASE }} \
PPA_GPG_KEYID=${{ steps.import_gpg.outputs.keyid }} ./scripts/ppa-publish.sh 18.04 --stable
- name: Build deb package for 20.04
run: |
cd $GITHUB_WORKSPACE/artifacts
mkdir build-20.04
cp *.tar.gz build-20.04 && cd build-20.04
tar xf *.tar.gz
cd globalprotect-openconnect-*/
PPA_GPG_PASSPHRASE=${{ secrets.PPA_GPG_PASSPHRASE }} \
PPA_GPG_KEYID=${{ steps.import_gpg.outputs.keyid }} ./scripts/ppa-publish.sh 20.04 --stable
- name: Build deb package for 21.04
run: |
cd $GITHUB_WORKSPACE/artifacts
mkdir build-21.04
cp *.tar.gz build-21.04 && cd build-21.04
tar xf *.tar.gz
cd globalprotect-openconnect-*/
PPA_GPG_PASSPHRASE=${{ secrets.PPA_GPG_PASSPHRASE }} \
PPA_GPG_KEYID=${{ steps.import_gpg.outputs.keyid }} ./scripts/ppa-publish.sh 21.04 --stable
- name: Build deb package for 21.10
run: |
cd $GITHUB_WORKSPACE/artifacts
mkdir build-21.10
cp *.tar.gz build-21.10 && cd build-21.10
tar xf *.tar.gz
cd globalprotect-openconnect-*/
PPA_GPG_PASSPHRASE=${{ secrets.PPA_GPG_PASSPHRASE }} \
PPA_GPG_KEYID=${{ steps.import_gpg.outputs.keyid }} ./scripts/ppa-publish.sh 21.10 --stable
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
env:
VERSION: $(cat ./artifacts/VERSION)
uses: yuezk/github-actions-deploy-aur@update-pkgver
with:
pkgname: globalprotect-openconnect
pkgbuild: ./artifacts/aur/PKGBUILD
assets: ./artifacts/aur/*.tar.gz
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

View File

@@ -1,31 +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

View File

@@ -1,58 +0,0 @@
name: Pre Release
on:
workflow_dispatch:
jobs:
pre-release:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
env:
DEBFULLNAME: "Kevin Yue"
DEBEMAIL: "yuezk001@gmail.com"
steps:
# Checkout repository and submodules
- uses: actions/checkout@v2
with:
submodules: recursive
fetch-depth: 0
- name: Init variables
id: vars
run: |
TAG=$(git tag --sort=-v:refname --list "v[0-9]*" | head -n 1 | cut -c 2-)
echo ::set-output name=VERSION::"${TAG}+SNAPSHOT$(date -u +"%Y%m%d%H%M%S")"
echo ::set-output name=TAG::${TAG}
- name: Update debian/changelog
run: |
sudo apt install devscripts
git log --format="%s" v${{ steps.vars.outputs.TAG }}.. | xargs -L1 dch -v ${{ steps.vars.outputs.VERSION }}-1ppa1
- name: "Archive all"
run: |
python -m pip install --upgrade pip
pip install git-archive-all
git-archive-all \
--force-submodules \
--prefix=globalprotect-openconnect-${{ steps.vars.outputs.VERSION }}/ \
./globalprotect-openconnect-${{ steps.vars.outputs.VERSION }}.full.tar.gz
- name: "Debian Packaging"
run: |
sudo apt update
sudo apt install qtbase5-dev libqt5websockets5-dev qtwebengine5-dev qttools5-dev debhelper
mkdir build-debian && cd build-debian
cp ../*.tar.gz globalprotect-openconnect_${{ steps.vars.outputs.VERSION }}.orig.tar.gz
tar xf *.tar.gz
cd globalprotect-openconnect-${{ steps.vars.outputs.VERSION }}
fakeroot dpkg-buildpackage -uc -us -sa
- uses: "marvinpinto/action-automatic-releases@latest"
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
automatic_release_tag: "latest"
prerelease: true
title: "globalprotect-openconnect_${{ steps.vars.outputs.VERSION }}"
files: |
*.tar.gz
build-debian/*.deb

View File

@@ -1,61 +0,0 @@
name: Publish
on:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Install Qt
uses: jurplel/install-qt-action@v2
with:
version: 5.12.11
modules: 'qtwebengine qtwebsockets'
# Checkout repository and submodules
- uses: actions/checkout@v2
with:
submodules: recursive
- name: Build
run: |
qmake CONFIG+=release
make
aur-publish:
needs:
- build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Get latest version
id: get-version
run: |
echo ::set-output name=VERSION::$(git tag --sort=-v:refname --list "v[0-9]*" | head -n 1 | cut -c 2-)
- name: Get the sha256sum
id: get-sha256sum
run: |
echo ::set-output name=SHA::$(curl -L https://github.com/yuezk/GlobalProtect-openconnect/archive/refs/tags/v${{ steps.get-version.outputs.VERSION }}.tar.gz | sha256sum | cut -f1 -d" ")
- name: Generate PKGBUILD
run: |
sed "s/{PKG_VERSION}/${{ steps.get-version.outputs.VERSION }}/g;s/{SOURCE_SHA}/${{ steps.get-sha256sum.outputs.SHA }}/g" PKGBUILD.template > PKGBUILD
- name: Publish AUR package
uses: KSXGitHub/github-actions-deploy-aur@v2.2.4
with:
pkgname: globalprotect-openconnect
pkgbuild: ./PKGBUILD
commit_username: ${{ secrets.AUR_USERNAME }}
commit_email: ${{ secrets.AUR_EMAIL }}
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
commit_message: 'Release v${{ steps.get-version.outputs.VERSION }}'
force_push: true

73
.gitignore vendored
View File

@@ -1,69 +1,4 @@
# Binaries
*.rpm
*.gz
*.snap
.DS_Store
build-debian
build
artifacts
.cmake
# 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*
.idea
/target
.pnpm-store
.env

7
.gitmodules vendored
View File

@@ -1,7 +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

9
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,9 @@
{
"recommendations": [
"rust-lang.rust-analyzer",
"tamasfe.even-better-toml",
"eamodio.gitlens",
"EditorConfig.EditorConfig",
"streetsidesoftware.code-spell-checker",
]
}

81
.vscode/settings.json vendored
View File

@@ -1,26 +1,57 @@
{
"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",
"globalprotect",
"globalprotectcallback",
"gpapi",
"gpauth",
"gpcallback",
"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",
"uzers",
"Vite",
"vpnc",
"vpninfo",
"wmctrl",
"XAUTHORITY",
"yuezk"
],
"rust-analyzer.cargo.features": "all",
}

1
3rdparty/plog vendored

Submodule 3rdparty/plog deleted from f4c22b03d5

View File

@@ -1,14 +0,0 @@
cmake_minimum_required(VERSION 3.1.0)
project(QtSignals LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 11)
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)

View File

@@ -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.

View File

@@ -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 <sys/socket.h>
#include <unistd.h>
#include <errno.h>
#include <QMap>
#include <QSocketNotifier>
#include <QDebug>
#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<int> 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"

View File

@@ -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 <QObject>
#include <signal.h>
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

View File

@@ -1,36 +0,0 @@
cmake_minimum_required(VERSION 3.10.0)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
file(STRINGS "VERSION" ver)
file(STRINGS "VERSION_SUFFIX" VERSION_SUFFIX)
project(GlobalProtect-openconnect VERSION ${ver} 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
)
add_subdirectory(3rdparty/qt-unix-signals)
add_subdirectory(GPService)
add_subdirectory(GPClient)
add_dependencies(gpclient gpservice)

5090
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

52
Cargo.toml Normal file
View File

@@ -0,0 +1,52 @@
[workspace]
resolver = "2"
members = ["crates/*", "apps/gpclient", "apps/gpservice", "apps/gpauth"]
[workspace.package]
version = "2.0.0-beta6"
authors = ["Kevin Yue <k3vinyue@gmail.com>"]
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"
uzers = "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*

View File

@@ -1,99 +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
gatewayauthenticator.cpp
gatewayauthenticatorparams.cpp
gpgateway.cpp
gphelper.cpp
loginparams.cpp
main.cpp
normalloginwindow.cpp
portalauthenticator.cpp
portalconfigresponse.cpp
preloginresponse.cpp
samlloginwindow.cpp
gpclient.cpp
settingsdialog.cpp
gpclient.ui
normalloginwindow.ui
settingsdialog.ui
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 1.1.5
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}
)
target_link_libraries(gpclient
${SingleApplication_LIBRARY}
Qt5::Widgets
Qt5::Network
Qt5::WebSockets
Qt5::WebEngine
Qt5::WebEngineWidgets
Qt5::DBus
QtSignals
)
if (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 8.0)
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)

View File

@@ -1,30 +0,0 @@
#include <QtCore/QVariantMap>
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject>
#include "cdpcommand.h"
CDPCommand::CDPCommand(QObject *parent) : QObject(parent)
{
}
CDPCommand::CDPCommand(int id, QString cmd, QVariantMap& params) :
QObject(nullptr),
id(id),
cmd(cmd),
params(&params)
{
}
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();
}

View File

@@ -1,24 +0,0 @@
#ifndef CDPCOMMAND_H
#define CDPCOMMAND_H
#include <QtCore/QObject>
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

View File

@@ -1,87 +0,0 @@
#include <QtCore/QVariantMap>
#include <plog/Log.h>
#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<QAbstractSocket::SocketError>::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()) {
PLOGE << "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 &params)
{
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()
{
PLOGI << "WebSocket disconnected";
}
void CDPCommandManager::onSocketError(QAbstractSocket::SocketError error)
{
PLOGE << "WebSocket error" << error;
}

View File

@@ -1,40 +0,0 @@
#ifndef CDPCOMMANDMANAGER_H
#define CDPCOMMANDMANAGER_H
#include <QtCore/QObject>
#include <QtCore/QHash>
#include <QtWebSockets/QtWebSockets>
#include <QtNetwork/QNetworkAccessManager>
#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<int, CDPCommand*> commandPool;
private slots:
void onTextMessageReceived(QString message);
void onSocketDisconnected();
void onSocketError(QAbstractSocket::SocketError error);
};
#endif // CDPCOMMANDMANAGER_H

View File

@@ -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=@CMAKE_INSTALL_PREFIX@/bin/gpclient
Icon=com.yuezk.qt.gpclient
Keywords=GlobalProtect;Openconnect;SAML;connection;VPN;
StartupWMClass=gpclient

View File

@@ -1,43 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>com.yuezk.qt.gpclient</id>
<name>globalprotect-openconnect</name>
<summary>A GlobalProtect VPN client powered by OpenConnect</summary>
<metadata_license>CC0-1.0</metadata_license>
<project_license>AGPL-3.0-or-later</project_license>
<description>
<p>A GlobalProtect VPN client (GUI) for Linux based on OpenConnect and built with Qt5, supports the SAML auth mode.</p>
</description>
<categories>
<category>Network</category>
</categories>
<update_contact>k3vinyue_AT_gmail.com</update_contact>
<developer_name>Kevin Yue</developer_name>
<url type="homepage">https://github.com/yuezk/GlobalProtect-openconnect</url>
<url type="bugtracker">https://github.com/yuezk/GlobalProtect-openconnect/issues</url>
<url type="help">https://github.com/yuezk/GlobalProtect-openconnect/issues</url>
<keywords>
<keyword>globalprotect</keyword>
<keyword>openconnect</keyword>
<keyword>vpn</keyword>
<keyword>saml</keyword>
</keywords>
<launchable type="desktop-id">com.yuezk.qt.gpclient.desktop</launchable>
<screenshots>
<screenshot type="default">
<image>https://user-images.githubusercontent.com/3297602/133869036-5c02b0d9-c2d9-4f87-8c81-e44f68cfd6ac.png</image>
</screenshot>
</screenshots>
<provides>
<binary>@CMAKE_INSTALL_PREFIX@/bin/gpclient</binary>
<dbus type="system">com.yuezk.qt.GPService</dbus>
</provides>
</component>

View File

@@ -1,99 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 96 96"
style="enable-background:new 0 0 96 96;"
xml:space="preserve"
sodipodi:docname="com.yuezk.qt.gpclient.svg"
inkscape:version="0.92.4 5da689c313, 2019-01-14"><metadata
id="metadata14"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
id="defs12" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1006"
id="namedview10"
showgrid="false"
inkscape:zoom="6.9532168"
inkscape:cx="7.9545315"
inkscape:cy="59.062386"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="g8499" />
<style
type="text/css"
id="style2">
.st0{fill:#2980B9;}
.st1{fill:#3498DB;}
.st2{fill:#2ECC71;}
.st3{fill:#27AE60;}
</style>
<g
id="g8499"
transform="matrix(1.3407388,0,0,1.3407388,-16.409202,-16.355463)"><g
id="XMLID_1_">
<circle
r="32.5"
cy="48"
cx="48"
class="st0"
id="XMLID_3_"
style="fill:#2980b9" />
<path
d="m 48,15.5 v 65 C 65.9,80.5 80.5,65.7 80.5,48 80.5,30 65.9,15.5 48,15.5 Z"
class="st1"
id="XMLID_4_"
inkscape:connector-curvature="0"
style="fill:#3498db" />
<path
d="m 48,15.5 v 0.6 l 1.2,-0.3 c 0.3,-0.3 0.4,-0.3 0.6,-0.3 h -1.1 z m 7.3,0.9 c -0.1,0 0.4,0.9 1.1,1.8 0.8,1.5 1.1,2.1 1.3,2.1 0.3,-0.3 1.9,-1.2 3,-2.1 -1.7,-0.9 -3.5,-1.5 -5.4,-1.8 z m 10.3,6.2 c -0.1,0 -0.4,0 -0.9,0.6 l -0.8,0.9 0.6,0.6 c 0.3,0.6 0.8,0.9 1,1.2 0.5,0.6 0.6,0.6 0.1,1.5 -0.2,0.6 -0.3,0.9 -0.3,0.9 0.1,0.3 0.3,0.3 1.4,0.3 h 1.6 c 0.1,0 0.3,-0.6 0.4,-1.2 l 0.1,-0.9 -1.1,-0.9 c -1,-0.9 -1,-0.9 -1.4,-1.8 -0.3,-0.6 -0.6,-1.2 -0.7,-1.2 z m -3,2.4 c -0.2,0 -1.3,2.1 -1.3,2.4 0,0 0.3,0.6 0.7,0.9 0.4,0.3 0.7,0.6 0.7,0.6 0.1,0 1.2,-1.2 1.4,-1.5 C 64.2,27.1 64,26.8 63.5,26.2 63.1,25.5 62.7,25 62.6,25 Z m 9.5,1.1 0.2,0.3 c 0,0.3 -0.7,0.9 -1.4,1.5 -1.2,0.9 -1.4,1.2 -2,1.2 -0.6,0 -0.9,0.3 -1.8,0.9 -0.6,0.6 -1.2,0.9 -1.2,1.2 0,0 0.2,0.3 0.6,0.9 0.7,0.6 0.7,0.9 0.2,1.8 l -0.4,0.3 h -1.1 c -0.6,0 -1.5,0 -1.8,-0.3 -0.9,0 -0.8,0 -0.1,2.1 1,3 1.1,3.2 1.3,3.2 0.1,0 1.3,-1.2 2.8,-2.4 1.5,-1.2 2.7,-2.4 2.8,-2.4 l 0.6,0.3 c 0.4,0.3 0.5,0 1.3,-0.6 l 0.8,-0.6 0.8,0.6 c 1.9,1.2 2.2,1.5 2.3,2.4 0.2,1.5 0.3,1.8 0.5,1.8 0.1,0 1.3,-1.5 1.6,-1.8 0.1,-0.3 -0.1,-0.6 -1.1,-2.1 -0.7,-0.9 -1.1,-1.8 -1.1,-2.1 0,0 0.1,0 0.3,-0.3 0.2,0 0.4,0.3 1,0.9 -1.6,-2.3 -3.2,-4.7 -5.1,-6.8 z m 2.8,10.7 c -0.2,0 -0.9,0.9 -0.8,1.2 l 0.5,0.3 H 75 c 0.2,0 0.3,0 0.2,-0.3 C 75.1,37.4 75,36.8 74.9,36.8 Z M 72.3,38 h -2.4 l -2.4,0.3 -4.5,3.5 -4.4,3.8 v 3.5 c 0,2.1 0,3.8 0.1,3.8 0.1,0 0.7,0.9 1.5,1.5 0.8,0.9 1.5,1.5 1.8,1.8 0.4,0.3 0.5,0.3 4,0.6 l 3.4,0.3 1.6,0.9 c 0.8,0.6 1.5,1.2 1.6,1.2 0.1,0 -0.3,0.3 -0.6,0.6 l -0.6,0.6 1,1.2 c 0.5,0.6 1.3,1.5 1.7,1.8 l 0.6,0.9 v 1.7 0.9 c 3.7,-5 5.9,-11.5 6.1,-18.3 0.1,-2.7 -0.3,-5.3 -0.8,-8 l -0.6,-0.3 c -0.1,0 -0.5,0.3 -1,0.6 -0.5,0.3 -1,0.9 -1.1,0.9 -0.1,0 -0.8,-0.3 -1.8,-0.6 l -1.8,-0.6 v -0.9 c 0,-0.6 0,-0.9 -0.6,-1.5 z M 48,63.7 V 64 h 0.2 z"
class="st2"
id="XMLID_13_"
inkscape:connector-curvature="0"
style="fill:#2ecc71" />
<path
d="m 48,15.5 c -3.1,0 -6.2,0.5 -9,1.3 0.3,0.4 0.3,0.4 0.6,0.9 1.5,2.5 1.7,2.8 2.1,2.9 0.3,0 0.9,0.1 1.6,0.1 h 1.2 l 0.9,-2 0.8,-1.9 1.8,-0.6 z m -16.9,4.7 c -2.8,1.7 -5.4,3.9 -7.6,6.4 -3.8,4.3 -6.3,9.6 -7.4,15.4 0.5,0 0.9,-0.1 1.8,-0.1 2.8,0.1 2.5,0 3.4,1.4 0.5,0.8 0.6,0.8 1.4,0.8 1,0.1 0.9,0 0.5,-1.6 -0.2,-0.6 -0.3,-1.2 -0.3,-1.4 0,-0.2 0.5,-0.7 1.7,-1.6 1.9,-1.5 1.8,-1.3 1.5,-2.9 -0.1,-0.3 0.1,-0.6 0.6,-1.2 0.7,-0.7 0.7,-0.6 1.4,-0.6 h 0.7 l 0.1,-1.2 c 0.1,-0.7 0.1,-1.3 0.2,-1.3 0,0 1.9,-1.1 4.1,-2.3 2.2,-1.2 4.1,-2.2 4.2,-2.3 0.2,-0.2 -0.3,-0.8 -2.7,-3.8 -1.5,-1.9 -2.8,-3.6 -2.9,-3.7 z m -5.8,23 c -0.1,0 -0.1,0.3 -0.1,0.6 0,0.6 0,0.7 0.6,1 0.8,0.4 0.9,0.5 0.8,0.2 -0.1,-0.4 -1.2,-1.9 -1.3,-1.8 z m -3.4,2.1 -0.5,1.8 c 0.1,0.1 0.9,0.3 1.8,0.5 1,0.2 1.6,0.4 1.8,0.3 l 0.5,-1.3 z m -3.8,1 -1.1,0.6 c -0.6,0.3 -1.2,0.6 -1.4,0.6 h -0.1 c 0,1.4 0.1,2.8 0.3,4.2 l 0.6,0.4 1,-0.1 h 1 l 0.6,1.4 c 0.3,0.7 0.7,1.4 0.8,1.5 0.1,0.1 1,0.1 1.8,0.1 h 1.5 L 23,56.2 c 0,1.2 0,1.3 -0.6,2.2 -0.4,0.5 -0.6,1.2 -0.6,1.4 0,0.2 0.7,2.1 1.6,4.3 l 1.5,4 1.6,0.8 c 1.2,0.6 1.5,0.8 1.5,1 0,0.1 -0.4,2.1 -0.6,3.1 3,2.5 6.4,4.5 10.2,5.8 3.5,-3.6 6.8,-7.1 7.3,-7.6 l 0.7,-0.7 0.2,-1.9 c 0.2,-1.1 0.4,-2.1 0.4,-2.2 0,-0.1 0.5,-0.6 1,-1.2 0.5,-0.5 0.8,-1 0.8,-1.1 v -0.2 c -0.1,-0.1 -1.4,-1.1 -3,-2.2 l -3.1,-2.1 -1.1,-0.1 c -0.8,0 -1.2,0 -1.3,-0.2 C 39.4,59.2 39.2,58.5 39.1,57.7 39,56.9 38.9,56.2 38.8,56.1 38.8,56 38,56 37.1,56 36.2,56 35.4,55.9 35.3,55.8 35.2,55.7 35.2,55.1 35.1,54.3 35,53.6 34.9,53 34.8,52.9 34.7,52.8 33.7,52.7 32.5,52.6 30.5,52.5 30.1,52.5 29.1,52 l -1.2,-0.6 -1.6,0.7 -1.7,0.9 -1.8,-0.1 c -2,0 -1.9,0.2 -2.1,-1.6 C 20.6,50.7 20.6,50.1 20.5,50.1 20.4,50 20,50 19.6,49.9 L 18.9,49.7 19,49.2 c 0,-0.3 0,-1 0.1,-1.4 L 19.2,47 18.7,46.5 Z m 9.1,1.1 C 27.1,47.5 27.1,47.8 27,48 l -0.1,0.5 2.9,1.2 c 2.9,1.1 3.4,1.2 3.9,0.7 0.2,-0.2 0.1,-0.2 -0.3,-0.4 -0.3,-0.1 -1.7,-0.9 -3.2,-1.6 -1.7,-0.7 -2.9,-1.1 -3,-1 z"
class="st3"
id="XMLID_20_"
inkscape:connector-curvature="0"
style="fill:#27ae60" />
</g><g
transform="matrix(1.458069,0,0,1.458069,-22.631538,-19.615144)"
id="g7664"><path
inkscape:connector-curvature="0"
id="XMLID_6_"
class="st3"
d="m 38.8,56.1 c 0,1.2 1,2.2 2.2,2.2 h 15.2 c 1.2,0 2.2,-1 2.2,-2.2 V 45.3 c 0,-1.2 -1,-2.2 -2.2,-2.2 H 40.9 c -1.2,0 -2.2,1 -2.2,2.2 v 10.8 z"
style="fill:#f1aa27;fill-opacity:1" /><path
style="fill:#e6e6e6"
inkscape:connector-curvature="0"
id="XMLID_7_"
class="st4"
d="m 55.5,43.1 h -3.3 v -3.7 c 0,-2.1 -1.7,-3.8 -3.8,-3.8 -2.1,0 -3.8,1.7 -3.8,3.8 v 3.8 h -3.1 v -3.8 c 0,-3.9 3.2,-7 7,-7 3.9,0 7,3.2 7,7 z" /><path
style="fill:#e6e6e6;fill-opacity:1"
inkscape:connector-curvature="0"
id="XMLID_8_"
class="st5"
d="m 50.35,48.2 c 0,-1 -0.8,-1.8 -1.8,-1.8 -1,0 -1.8,0.8 -1.8,1.8 0,0.7 0.4,1.3 1,1.6 l -1,5.2 h 3.6 l -1,-5.2 c 0.6,-0.3 1,-0.9 1,-1.6 z" /></g></g></svg>

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -1,36 +0,0 @@
#include <QtCore/QProcessEnvironment>
#include <QtWebEngineWidgets/QWebEngineView>
#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);
}
EnhancedWebView::~EnhancedWebView()
{
delete cdp;
}
void EnhancedWebView::initialize()
{
QString 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);
}
}

View File

@@ -1,30 +0,0 @@
#ifndef ENHANCEDWEBVIEW_H
#define ENHANCEDWEBVIEW_H
#include <QtWebEngineWidgets/QWebEngineView>
#include "cdpcommandmanager.h"
#define ENV_CDP_PORT "QTWEBENGINE_REMOTE_DEBUGGING"
class EnhancedWebView : public QWebEngineView
{
Q_OBJECT
public:
explicit EnhancedWebView(QWidget *parent = nullptr);
~EnhancedWebView();
void initialize();
signals:
void responseReceived(QJsonObject params);
private slots:
void onCDPReady();
void onEventReceived(QString eventName, QJsonObject params);
private:
CDPCommandManager *cdp;
};
#endif // ENHANCEDWEBVIEW_H

View File

@@ -1,181 +0,0 @@
#include <QtNetwork/QNetworkReply>
#include <plog/Log.h>
#include "gatewayauthenticator.h"
#include "gphelper.h"
#include "loginparams.h"
#include "preloginresponse.h"
using namespace gpclient::helper;
GatewayAuthenticator::GatewayAuthenticator(const QString& gateway, const 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();
}
}
GatewayAuthenticator::~GatewayAuthenticator()
{
delete normalLoginWindow;
}
void GatewayAuthenticator::authenticate()
{
PLOGI << "Start gateway authentication...";
LoginParams loginParams { params.clientos() };
loginParams.setUser(params.username());
loginParams.setPassword(params.password());
loginParams.setUserAuthCookie(params.userAuthCookie());
login(loginParams);
}
void GatewayAuthenticator::login(const LoginParams &loginParams)
{
PLOGI << "Trying to login the gateway at " << loginUrl << " with " << loginParams.toUtf8();
QNetworkReply *reply = createRequest(loginUrl, loginParams.toUtf8());
connect(reply, &QNetworkReply::finished, this, &GatewayAuthenticator::onLoginFinished);
}
void GatewayAuthenticator::onLoginFinished()
{
QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
QByteArray response;
if (reply->error() || (response = reply->readAll()).contains("Authentication failure")) {
PLOGE << QString("Failed to login the gateway at %1, %2").arg(loginUrl).arg(reply->errorString());
if (normalLoginWindow) {
normalLoginWindow->setProcessing(false);
openMessageBox("Gateway login failed.", "Please check your credentials and try again.");
} else {
doAuth();
}
return;
}
if (normalLoginWindow) {
normalLoginWindow->close();
}
const QUrlQuery params = gpclient::helper::parseGatewayResponse(response);
emit success(params.toString());
}
void GatewayAuthenticator::doAuth()
{
PLOGI << "Perform the gateway prelogin at " << preloginUrl;
QNetworkReply *reply = createRequest(preloginUrl);
connect(reply, &QNetworkReply::finished, this, &GatewayAuthenticator::onPreloginFinished);
}
void GatewayAuthenticator::onPreloginFinished()
{
QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
if (reply->error()) {
PLOGE << QString("Failed to prelogin the gateway at %1, %2").arg(preloginUrl).arg(reply->errorString());
emit fail("Error occurred on the gateway prelogin interface.");
return;
}
PLOGI << "Gateway prelogin succeeded.";
PreloginResponse 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 {
PLOGE << QString("Unknown prelogin response for %1, got %2").arg(preloginUrl).arg(QString::fromUtf8(response.rawResponse()));
emit fail("Unknown response for gateway prelogin interface.");
}
delete reply;
}
void GatewayAuthenticator::normalAuth(QString labelUsername, QString labelPassword, QString authMessage)
{
PLOGI << QString("Trying to perform the normal login with %1 / %2 credentials").arg(labelUsername).arg(labelPassword);
normalLoginWindow = new NormalLoginWindow;
normalLoginWindow->setPortalAddress(gateway);
normalLoginWindow->setAuthMessage(authMessage);
normalLoginWindow->setUsernameLabel(labelUsername);
normalLoginWindow->setPasswordLabel(labelPassword);
// Do login
connect(normalLoginWindow, &NormalLoginWindow::performLogin, this, &GatewayAuthenticator::onPerformNormalLogin);
connect(normalLoginWindow, &NormalLoginWindow::rejected, this, &GatewayAuthenticator::onLoginWindowRejected);
connect(normalLoginWindow, &NormalLoginWindow::finished, this, &GatewayAuthenticator::onLoginWindowFinished);
normalLoginWindow->show();
}
void GatewayAuthenticator::onPerformNormalLogin(const QString &username, const QString &password)
{
PLOGI << "Start to perform normal login...";
normalLoginWindow->setProcessing(true);
LoginParams loginParams { params.clientos() };
loginParams.setUser(username);
loginParams.setPassword(password);
login(loginParams);
}
void GatewayAuthenticator::onLoginWindowRejected()
{
emit fail();
}
void GatewayAuthenticator::onLoginWindowFinished()
{
delete normalLoginWindow;
normalLoginWindow = nullptr;
}
void GatewayAuthenticator::samlAuth(QString samlMethod, QString samlRequest, QString preloginUrl)
{
PLOGI << "Trying to perform SAML login with saml-method " << samlMethod;
SAMLLoginWindow *loginWindow = new SAMLLoginWindow;
connect(loginWindow, &SAMLLoginWindow::success, this, &GatewayAuthenticator::onSAMLLoginSuccess);
connect(loginWindow, &SAMLLoginWindow::fail, this, &GatewayAuthenticator::onSAMLLoginFail);
connect(loginWindow, &SAMLLoginWindow::rejected, this, &GatewayAuthenticator::onLoginWindowRejected);
loginWindow->login(samlMethod, samlRequest, preloginUrl);
}
void GatewayAuthenticator::onSAMLLoginSuccess(const QMap<QString, QString> &samlResult)
{
if (samlResult.contains("preloginCookie")) {
PLOGI << "SAML login succeeded, got the prelogin-cookie " << samlResult.value("preloginCookie");
} else {
PLOGI << "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 msg)
{
emit fail(msg);
}

View File

@@ -1,46 +0,0 @@
#ifndef GATEWAYAUTHENTICATOR_H
#define GATEWAYAUTHENTICATOR_H
#include <QtCore/QObject>
#include "normalloginwindow.h"
#include "loginparams.h"
#include "gatewayauthenticatorparams.h"
class GatewayAuthenticator : public QObject
{
Q_OBJECT
public:
explicit GatewayAuthenticator(const QString& gateway, const GatewayAuthenticatorParams params);
~GatewayAuthenticator();
void authenticate();
signals:
void success(const QString& authCookie);
void fail(const QString& msg = "");
private slots:
void onLoginFinished();
void onPreloginFinished();
void onPerformNormalLogin(const QString &username, const QString &password);
void onLoginWindowRejected();
void onLoginWindowFinished();
void onSAMLLoginSuccess(const QMap<QString, QString> &samlResult);
void onSAMLLoginFail(const QString msg);
private:
QString gateway;
const GatewayAuthenticatorParams params;
QString preloginUrl;
QString loginUrl;
NormalLoginWindow *normalLoginWindow{ nullptr };
void login(const LoginParams& loginParams);
void doAuth();
void normalAuth(QString labelUsername, QString labelPassword, QString authMessage);
void samlAuth(QString samlMethod, QString samlRequest, QString preloginUrl = "");
};
#endif // GATEWAYAUTHENTICATOR_H

View File

@@ -1,57 +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;
}

View File

@@ -1,34 +0,0 @@
#ifndef GATEWAYAUTHENTICATORPARAMS_H
#define GATEWAYAUTHENTICATORPARAMS_H
#include <QtCore/QString>
#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);
private:
QString m_username;
QString m_password;
QString m_userAuthCookie;
QString m_clientos;
};
#endif // GATEWAYAUTHENTICATORPARAMS_H

View File

@@ -1,490 +0,0 @@
#include <QtGui/QIcon>
#include <plog/Log.h>
#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)
: QMainWindow(parent)
, ui(new Ui::GPClient)
, settingsDialog(new SettingsDialog(this))
{
ui->setupUi(this);
setWindowTitle("GlobalProtect");
setFixedSize(width(), height());
gpclient::helper::moveCenter(this);
setupSettings();
// Restore portal from the previous settings
ui->portalInput->setText(settings::get("portal", "").toString());
// DBus service setup
vpn = new com::yuezk::qt::GPService("com.yuezk.qt.GPService", "/", QDBusConnection::systemBus(), this);
connect(vpn, &com::yuezk::qt::GPService::connected, this, &GPClient::onVPNConnected);
connect(vpn, &com::yuezk::qt::GPService::disconnected, this, &GPClient::onVPNDisconnected);
connect(vpn, &com::yuezk::qt::GPService::error, this, &GPClient::onVPNError);
connect(vpn, &com::yuezk::qt::GPService::logAvailable, this, &GPClient::onVPNLogAvailable);
// Initiallize the context menu of system tray.
initSystemTrayIcon();
initVpnStatus();
}
GPClient::~GPClient()
{
delete ui;
delete vpn;
delete settingsDialog;
delete settingsButton;
}
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->setExtraArgs(settings::get("extraArgs", "").toString());
settingsDialog->setClientos(settings::get("clientos", "Linux").toString());
settingsDialog->show();
}
void GPClient::onSettingsAccepted()
{
settings::save("extraArgs", settingsDialog->extraArgs());
settings::save("clientos", settingsDialog->clientos());
}
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 Settings", this, &GPClient::clearSettings);
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()
{
PLOGI << "Populating the Switch Gateway menu...";
const QList<GPGateway> gateways = allGateways();
gatewaySwitchMenu->clear();
if (gateways.isEmpty()) {
gatewaySwitchMenu->addAction("<None>")->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), g.name())->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 GPGateway 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()
{
PLOGI << "Start connecting...";
const QString btnText = ui->connectButton->text();
const QString 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()) {
PLOGI << "Start gateway login using the previously saved gateway...";
isQuickConnect = true;
gatewayLogin();
} else {
// Perform the portal login
PLOGI << "Start portal login...";
portalLogin();
}
} else {
PLOGI << "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()
{
PortalAuthenticator *portalAuth = new PortalAuthenticator(portal(), settings::get("clientos", "Linux").toString());
connect(portalAuth, &PortalAuthenticator::success, this, &GPClient::onPortalSuccess);
// Prelogin failed on the portal interface, try to treat the portal as a gateway interface
connect(portalAuth, &PortalAuthenticator::preloginFailed, this, &GPClient::onPortalPreloginFail);
connect(portalAuth, &PortalAuthenticator::portalConfigFailed, this, &GPClient::onPortalConfigFail);
// Portal login failed
connect(portalAuth, &PortalAuthenticator::fail, this, &GPClient::onPortalFail);
ui->statusLabel->setText("Authenticating...");
updateConnectionStatus(VpnStatus::pending);
portalAuth->authenticate();
}
void GPClient::onPortalSuccess(const PortalConfigResponse portalConfig, const QString region)
{
PLOGI << "Portal authentication succeeded.";
// No gateway found in protal configuration
if (portalConfig.allGateways().size() == 0) {
PLOGI << "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)
{
PLOGI << "Portal prelogin failed: " << msg;
tryGatewayLogin();
}
void GPClient::onPortalConfigFail(const QString msg)
{
PLOGI << "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()
{
PLOGI << "Try to preform login on the the gateway interface...";
// Treat the portal input as the gateway address
GPGateway g;
g.setName(portal());
g.setAddress(portal());
QList<GPGateway> gateways;
gateways.append(g);
setAllGateways(gateways);
setCurrentGateway(g);
gatewayLogin();
}
// Login to the gateway
void GPClient::gatewayLogin()
{
PLOGI << "Performing gateway login...";
GatewayAuthenticatorParams params = GatewayAuthenticatorParams::fromPortalConfigResponse(portalConfig);
params.setClientos(settings::get("clientos", "Linux").toString());
GatewayAuthenticator *gatewayAuth = new GatewayAuthenticator(currentGateway().address(), params);
connect(gatewayAuth, &GatewayAuthenticator::success, this, &GPClient::onGatewaySuccess);
connect(gatewayAuth, &GatewayAuthenticator::fail, this, &GPClient::onGatewayFail);
ui->statusLabel->setText("Authenticating...");
updateConnectionStatus(VpnStatus::pending);
gatewayAuth->authenticate();
}
void GPClient::onGatewaySuccess(const QString &authCookie)
{
PLOGI << "Gateway login succeeded, got the cookie " << authCookie;
isQuickConnect = false;
vpn->connect(currentGateway().address(), portalConfig.username(), authCookie, settings::get("extraArgs", "").toString());
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;
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;
}
bool GPClient::connected() const
{
const QString statusText = ui->statusLabel->text();
return statusText.contains("Connected") && !statusText.contains("Not");
}
QList<GPGateway> GPClient::allGateways() const
{
const QString gatewaysJson = settings::get(portal() + "_gateways").toString();
return GPGateway::fromJson(gatewaysJson);
}
void GPClient::setAllGateways(QList<GPGateway> gateways)
{
PLOGI << "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)
{
PLOGI << "Updating the current gateway to " << gateway.name();
settings::save(portal() + "_selectedGateway", gateway.name());
populateGatewayMenu();
}
void GPClient::clearSettings()
{
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)
{
PLOGI << log;
}

View File

@@ -1,102 +0,0 @@
#ifndef GPCLIENT_H
#define GPCLIENT_H
#include <QtWidgets/QMainWindow>
#include <QtWidgets/QSystemTrayIcon>
#include <QtWidgets/QMenu>
#include <QtWidgets/QPushButton>
#include "gpserviceinterface.h"
#include "portalconfigresponse.h"
#include "settingsdialog.h"
QT_BEGIN_NAMESPACE
namespace Ui { class GPClient; }
QT_END_NAMESPACE
class GPClient : public QMainWindow
{
Q_OBJECT
public:
GPClient(QWidget *parent = nullptr);
~GPClient();
void activate();
void quit();
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;
com::yuezk::qt::GPService *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 doConnect();
void portalLogin();
void tryGatewayLogin();
void gatewayLogin();
QString portal() const;
bool connected() const;
QList<GPGateway> allGateways() const;
void setAllGateways(QList<GPGateway> gateways);
GPGateway currentGateway() const;
void setCurrentGateway(const GPGateway gateway);
void clearSettings();
};
#endif // GPCLIENT_H

View File

@@ -1,143 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>GPClient</class>
<widget class="QMainWindow" name="GPClient">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>260</width>
<height>362</height>
</rect>
</property>
<property name="windowTitle">
<string>GlobalProtect OpenConnect</string>
</property>
<property name="windowIcon">
<iconset resource="resources.qrc">
<normaloff>:/images/logo.svg</normaloff>:/images/logo.svg</iconset>
</property>
<property name="styleSheet">
<string notr="true"/>
</property>
<property name="iconSize">
<size>
<width>22</width>
<height>22</height>
</size>
</property>
<widget class="QWidget" name="centralwidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="layoutDirection">
<enum>Qt::LeftToRight</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3" stretch="1,0,0">
<property name="leftMargin">
<number>15</number>
</property>
<property name="topMargin">
<number>15</number>
</property>
<property name="rightMargin">
<number>15</number>
</property>
<property name="bottomMargin">
<number>15</number>
</property>
<item>
<layout class="QVBoxLayout" name="verticalLayout" stretch="1,0">
<property name="bottomMargin">
<number>15</number>
</property>
<item>
<widget class="QLabel" name="statusImage">
<property name="styleSheet">
<string notr="true">#statusImage {
image: url(:/images/not_connected.png);
padding: 15
}</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="statusLabel">
<property name="font">
<font>
<pointsize>14</pointsize>
<weight>50</weight>
<bold>false</bold>
<underline>false</underline>
</font>
</property>
<property name="text">
<string>Not Connected</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLineEdit" name="portalInput">
<property name="text">
<string/>
</property>
<property name="placeholderText">
<string>Please enter your portal address</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="connectButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Connect</string>
</property>
<property name="autoDefault">
<bool>true</bool>
</property>
<property name="default">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p align=&quot;center&quot;&gt;&lt;a href=&quot;https://bit.ly/3g5DHqy&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#4c6b8a;&quot;&gt;Report a bug&lt;/span&gt;&lt;/a&gt; / &lt;a href=&quot;https://bit.ly/3jQYfEi&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#4c6b8a;&quot;&gt;Buy me a coffee&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<resources>
<include location="resources.qrc"/>
</resources>
<connections/>
</ui>

View File

@@ -1,97 +0,0 @@
#include <QtCore/QJsonObject>
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonArray>
#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<QString, int> &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<GPGateway> &gateways)
{
QJsonArray arr;
for (auto g : gateways) {
arr.append(g.toJsonObject());
}
QJsonDocument jsonDoc{ arr };
return QString::fromUtf8(jsonDoc.toJson());
}
QList<GPGateway> GPGateway::fromJson(const QString &jsonString)
{
QList<GPGateway> 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;
}

View File

@@ -1,33 +0,0 @@
#ifndef GPGATEWAY_H
#define GPGATEWAY_H
#include <QtCore/QString>
#include <QtCore/QMap>
#include <QtCore/QJsonObject>
class GPGateway
{
public:
GPGateway();
QString name() const;
QString address() const;
void setName(const QString &name);
void setAddress(const QString &address);
void setPriorityRules(const QMap<QString, int> &priorityRules);
int priorityOf(QString ruleName) const;
QJsonObject toJsonObject() const;
QString toString() const;
static QString serialize(QList<GPGateway> &gateways);
static QList<GPGateway> fromJson(const QString &jsonString);
static GPGateway fromJsonObject(const QJsonObject &jsonObj);
private:
QString _name;
QString _address;
QMap<QString, int> _priorityRules;
};
#endif // GPGATEWAY_H

View File

@@ -1,129 +0,0 @@
#include <QtCore/QXmlStreamReader>
#include <QtWidgets/QMessageBox>
#include <QtWidgets/QDesktopWidget>
#include <QtWidgets/QApplication>
#include <QtWidgets/QWidget>
#include <QtNetwork/QNetworkRequest>
#include <QtNetwork/QSslConfiguration>
#include <QtNetwork/QSslSocket>
#include <plog/Log.h>
#include "gphelper.h"
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);
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<GPGateway> gateways, const QString ruleName)
{
PLOGI << gateways.size() << " gateway(s) avaiable, filter the gateways with rule: " << ruleName;
GPGateway gateway = gateways.first();
for (GPGateway g : gateways) {
if (g.priorityOf(ruleName) > gateway.priorityOf(ruleName)) {
PLOGI << "Find a preferred gateway: " << g.name();
gateway = g;
}
}
return gateway;
}
QUrlQuery gpclient::helper::parseGatewayResponse(const QByteArray &xml)
{
PLOGI << "Start parsing the gateway response...";
PLOGI << "The gateway response is: " << xml;
QXmlStreamReader xmlReader{xml};
QList<QString> 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);
}
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);
}
}
}

View File

@@ -1,43 +0,0 @@
#ifndef GPHELPER_H
#define GPHELPER_H
#include <QtCore/QObject>
#include <QtCore/QUrlQuery>
#include <QtCore/QSettings>
#include <QtNetwork/QNetworkAccessManager>
#include <QtNetwork/QNetworkRequest>
#include <QtNetwork/QNetworkReply>
#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<GPGateway> 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());
void save(const QString &key, const QVariant &value);
void clear();
}
}
}
#endif // GPHELPER_H

View File

@@ -1,75 +0,0 @@
#include <QtCore/QUrlQuery>
#include "loginparams.h"
LoginParams::LoginParams(const QString clientos)
{
params.addQueryItem("prot", QUrl::toPercentEncoding("https:"));
params.addQueryItem("server", "");
params.addQueryItem("inputSrc", "");
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");
params.addQueryItem("os-version", QUrl::toPercentEncoding(QSysInfo::prettyProductName()));
// add the clientos parameter if not empty
if (!clientos.isEmpty()) {
params.addQueryItem("clientos", clientos);
}
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);
}
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));
}

View File

@@ -1,27 +0,0 @@
#ifndef LOGINPARAMS_H
#define LOGINPARAMS_H
#include <QtCore/QUrlQuery>
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);
QByteArray toUtf8() const;
private:
QUrlQuery params;
void updateQueryItem(const QString key, const QString value);
};
#endif // LOGINPARAMS_H

View File

@@ -1,49 +0,0 @@
#include <QtCore/QObject>
#include <QtCore/QString>
#include <QtCore/QDir>
#include <QtCore/QStandardPaths>
#include <plog/Log.h>
#include <plog/Appenders/ColorConsoleAppender.h>
#include "singleapplication.h"
#include "gpclient.h"
#include "enhancedwebview.h"
#include "sigwatch.h"
#include "version.h"
int main(int argc, char *argv[])
{
const QDir path = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + "/GlobalProtect-openconnect";
const QString logFile = path.path() + "/gpclient.log";
if (!path.exists()) {
path.mkpath(".");
}
static plog::ColorConsoleAppender<plog::TxtFormatter> consoleAppender;
plog::init(plog::debug, logFile.toUtf8()).addAppender(&consoleAppender);
PLOGI << "GlobalProtect started, version: " << VERSION;
QString port = QString::fromLocal8Bit(qgetenv(ENV_CDP_PORT));
if (port == "") {
qputenv(ENV_CDP_PORT, "12315");
}
SingleApplication app(argc, argv);
app.setQuitOnLastWindowClosed(false);
GPClient w;
w.show();
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);
return app.exec();
}

View File

@@ -1,64 +0,0 @@
#include <QtGui/QCloseEvent>
#include "normalloginwindow.h"
#include "ui_normalloginwindow.h"
NormalLoginWindow::NormalLoginWindow(QWidget *parent) :
QDialog(parent),
ui(new Ui::NormalLoginWindow)
{
ui->setupUi(this);
setWindowTitle("GlobalProtect Login");
setFixedSize(width(), height());
setModal(true);
}
NormalLoginWindow::~NormalLoginWindow()
{
delete ui;
}
void NormalLoginWindow::setAuthMessage(QString message)
{
ui->authMessage->setText(message);
}
void NormalLoginWindow::setUsernameLabel(QString label)
{
ui->username->setPlaceholderText(label);
}
void NormalLoginWindow::setPasswordLabel(QString label)
{
ui->password->setPlaceholderText(label);
}
void NormalLoginWindow::setPortalAddress(QString portal)
{
ui->portalAddress->setText(portal);
}
void NormalLoginWindow::setProcessing(bool isProcessing)
{
ui->username->setReadOnly(isProcessing);
ui->password->setReadOnly(isProcessing);
ui->loginButton->setDisabled(isProcessing);
}
void NormalLoginWindow::on_loginButton_clicked()
{
const QString username = ui->username->text().trimmed();
const QString password = ui->password->text().trimmed();
if (username.isEmpty() || password.isEmpty()) {
return;
}
emit performLogin(username, password);
}
void NormalLoginWindow::closeEvent(QCloseEvent *event)
{
event->accept();
reject();
}

View File

@@ -1,37 +0,0 @@
#ifndef PORTALAUTHWINDOW_H
#define PORTALAUTHWINDOW_H
#include <QtWidgets/QDialog>
namespace Ui {
class NormalLoginWindow;
}
class NormalLoginWindow : public QDialog
{
Q_OBJECT
public:
explicit NormalLoginWindow(QWidget *parent = nullptr);
~NormalLoginWindow();
void setAuthMessage(QString);
void setUsernameLabel(QString);
void setPasswordLabel(QString);
void setPortalAddress(QString);
void setProcessing(bool isProcessing);
private slots:
void on_loginButton_clicked();
signals:
void performLogin(QString username, QString password);
private:
Ui::NormalLoginWindow *ui;
void closeEvent(QCloseEvent *event);
};
#endif // PORTALAUTHWINDOW_H

View File

@@ -1,148 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>NormalLoginWindow</class>
<widget class="QDialog" name="NormalLoginWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>255</width>
<height>269</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="cursor">
<cursorShape>ArrowCursor</cursorShape>
</property>
<property name="windowTitle">
<string>Login</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<layout class="QVBoxLayout" name="verticalLayout_4" stretch="1,0,0">
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="font">
<font>
<pointsize>20</pointsize>
</font>
</property>
<property name="text">
<string>Login</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="authMessage">
<property name="enabled">
<bool>true</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>2</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Please enter the login credentials</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>6</number>
</property>
<item>
<widget class="QLabel" name="portalLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Portal:</string>
</property>
<property name="margin">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="portalAddress">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>vpn.example.com</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLineEdit" name="username">
<property name="placeholderText">
<string>Username</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="password">
<property name="text">
<string/>
</property>
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
<property name="placeholderText">
<string>Password</string>
</property>
<property name="clearButtonEnabled">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="loginButton">
<property name="text">
<string>Login</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -1,210 +0,0 @@
#include <QtNetwork/QNetworkReply>
#include <plog/Log.h>
#include "portalauthenticator.h"
#include "gphelper.h"
#include "normalloginwindow.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 normalLoginWindow;
}
void PortalAuthenticator::authenticate()
{
PLOGI << "Preform portal prelogin at " << preloginUrl;
QNetworkReply *reply = createRequest(preloginUrl);
connect(reply, &QNetworkReply::finished, this, &PortalAuthenticator::onPreloginFinished);
}
void PortalAuthenticator::onPreloginFinished()
{
QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
if (reply->error()) {
PLOGE << QString("Error occurred while accessing %1, %2").arg(preloginUrl).arg(reply->errorString());
emit preloginFailed("Error occurred on the portal prelogin interface.");
delete reply;
return;
}
PLOGI << "Portal prelogin succeeded.";
preloginResponse = PreloginResponse::parse(reply->readAll());
PLOGI << "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 {
PLOGE << 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()) {
PLOGI << "Trying auto login using the saved credentials";
isAutoLogin = true;
fetchConfig(settings::get("username").toString(), settings::get("password").toString());
} else {
normalAuth();
}
}
void PortalAuthenticator::normalAuth()
{
PLOGI << "Trying to launch the normal login window...";
normalLoginWindow = new NormalLoginWindow;
normalLoginWindow->setPortalAddress(portal);
normalLoginWindow->setAuthMessage(preloginResponse.authMessage());
normalLoginWindow->setUsernameLabel(preloginResponse.labelUsername());
normalLoginWindow->setPasswordLabel(preloginResponse.labelPassword());
// Do login
connect(normalLoginWindow, &NormalLoginWindow::performLogin, this, &PortalAuthenticator::onPerformNormalLogin);
connect(normalLoginWindow, &NormalLoginWindow::rejected, this, &PortalAuthenticator::onLoginWindowRejected);
connect(normalLoginWindow, &NormalLoginWindow::finished, this, &PortalAuthenticator::onLoginWindowFinished);
normalLoginWindow->show();
}
void PortalAuthenticator::onPerformNormalLogin(const QString &username, const QString &password)
{
normalLoginWindow->setProcessing(true);
fetchConfig(username, password);
}
void PortalAuthenticator::onLoginWindowRejected()
{
emitFail();
}
void PortalAuthenticator::onLoginWindowFinished()
{
delete normalLoginWindow;
normalLoginWindow = nullptr;
}
void PortalAuthenticator::samlAuth()
{
PLOGI << "Trying to perform SAML login with saml-method " << preloginResponse.samlMethod();
SAMLLoginWindow *loginWindow = new SAMLLoginWindow;
connect(loginWindow, &SAMLLoginWindow::success, this, &PortalAuthenticator::onSAMLLoginSuccess);
connect(loginWindow, &SAMLLoginWindow::fail, this, &PortalAuthenticator::onSAMLLoginFail);
connect(loginWindow, &SAMLLoginWindow::rejected, this, &PortalAuthenticator::onLoginWindowRejected);
loginWindow->login(preloginResponse.samlMethod(), preloginResponse.samlRequest(), preloginUrl);
}
void PortalAuthenticator::onSAMLLoginSuccess(const QMap<QString, QString> samlResult)
{
if (samlResult.contains("preloginCookie")) {
PLOGI << "SAML login succeeded, got the prelogin-cookie " << samlResult.value("preloginCookie");
} else {
PLOGI << "SAML login succeeded, got the portal-userauthcookie " << samlResult.value("userAuthCookie");
}
fetchConfig(samlResult.value("username"), "", samlResult.value("preloginCookie"), samlResult.value("userAuthCookie"));
}
void PortalAuthenticator::onSAMLLoginFail(const QString msg)
{
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;
PLOGI << "Fetching the portal config from " << configUrl << " for user: " << username;
QNetworkReply *reply = createRequest(configUrl, loginParams.toUtf8());
connect(reply, &QNetworkReply::finished, this, &PortalAuthenticator::onFetchConfigFinished);
}
void PortalAuthenticator::onFetchConfigFinished()
{
QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
if (reply->error()) {
PLOGE << 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 (normalLoginWindow) {
normalLoginWindow->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;
}
PLOGI << "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 (normalLoginWindow) {
PLOGI << "Closing the NormalLoginWindow...";
// Save the credentials for reuse
settings::save("username", username);
settings::save("password", password);
normalLoginWindow->close();
}
emit success(response, preloginResponse.region());
}
void PortalAuthenticator::emitFail(const QString& msg)
{
emit fail(msg);
}

View File

@@ -1,57 +0,0 @@
#ifndef PORTALAUTHENTICATOR_H
#define PORTALAUTHENTICATOR_H
#include <QtCore/QObject>
#include "portalconfigresponse.h"
#include "normalloginwindow.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<QString, QString> samlResult);
void onSAMLLoginFail(const QString msg);
void onFetchConfigFinished();
private:
QString portal;
QString clientos;
QString preloginUrl;
QString configUrl;
QString username;
QString password;
PreloginResponse preloginResponse;
bool isAutoLogin { false };
NormalLoginWindow *normalLoginWindow{ 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

View File

@@ -1,178 +0,0 @@
#include <QtCore/QXmlStreamReader>
#include <plog/Log.h>
#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)
{
PLOGI << "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) {
PLOGI << "Start reading " << name;
response.setUserAuthCookie(xmlReader.readElementText());
} else if (name == xmlPrelogonUserAuthCookie) {
PLOGI << "Start reading " << name;
response.setPrelogonUserAuthCookie(xmlReader.readElementText());
} else if (name == xmlGateways) {
response.setAllGateways(parseGateways(xmlReader));
}
}
PLOGI << "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<GPGateway> PortalConfigResponse::parseGateways(QXmlStreamReader &xmlReader)
{
PLOGI << "Start parsing the gateways from portal configuration...";
QList<GPGateway> 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;
QString address = xmlReader.attributes().value("name").toString();
g.setAddress(address);
g.setPriorityRules(parsePriorityRules(xmlReader));
g.setName(parseGatewayName(xmlReader));
gateways.append(g);
}
}
PLOGI << "Finished parsing the gateways.";
return gateways;
}
QMap<QString, int> PortalConfigResponse::parsePriorityRules(QXmlStreamReader &xmlReader)
{
PLOGI << "Start parsing the priority rules...";
QMap<QString, int> priorityRules;
while ((xmlReader.name() != "priority-rule" || !xmlReader.isEndElement()) && !xmlReader.hasError()) {
xmlReader.readNext();
if (xmlReader.name() == "entry" && xmlReader.isStartElement()) {
QString ruleName = xmlReader.attributes().value("name").toString();
// Read the priority tag
while (xmlReader.name() != "priority"){
xmlReader.readNext();
}
int ruleValue = xmlReader.readElementText().toUInt();
priorityRules.insert(ruleName, ruleValue);
}
}
PLOGI << "Finished parsing the priority rules.";
return priorityRules;
}
QString PortalConfigResponse::parseGatewayName(QXmlStreamReader &xmlReader)
{
PLOGI << "Start parsing the gateway name...";
while (xmlReader.name() != "description" || !xmlReader.isEndElement()) {
xmlReader.readNext();
if (xmlReader.name() == "description" && xmlReader.tokenType() == xmlReader.StartElement) {
PLOGI << "Finished parsing the gateway name";
return xmlReader.readElementText();
}
}
PLOGE << "Error: <description> tag not found";
return "";
}
QString PortalConfigResponse::userAuthCookie() const
{
return m_userAuthCookie;
}
QString PortalConfigResponse::prelogonUserAuthCookie() const
{
return m_prelogonAuthCookie;
}
QList<GPGateway> PortalConfigResponse::allGateways() const
{
return m_gateways;
}
void PortalConfigResponse::setAllGateways(QList<GPGateway> 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;
}

View File

@@ -1,51 +0,0 @@
#ifndef PORTALCONFIGRESPONSE_H
#define PORTALCONFIGRESPONSE_H
#include <QtCore/QString>
#include <QtCore/QList>
#include <QtCore/QXmlStreamReader>
#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;
QString prelogonUserAuthCookie() const;
QList<GPGateway> allGateways() const;
void setAllGateways(QList<GPGateway> 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<GPGateway> m_gateways;
void setRawResponse(const QByteArray response);
void setUserAuthCookie(const QString cookie);
void setPrelogonUserAuthCookie(const QString cookie);
static QList<GPGateway> parseGateways(QXmlStreamReader &xmlReader);
static QMap<QString, int> parsePriorityRules(QXmlStreamReader &xmlReader);
static QString parseGatewayName(QXmlStreamReader &xmlReader);
};
#endif // PORTALCONFIGRESPONSE_H

View File

@@ -1,100 +0,0 @@
#include <QtCore/QXmlStreamReader>
#include <QtCore/QMap>
#include <plog/Log.h>
#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)
{
PLOGI << "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);
}

View File

@@ -1,41 +0,0 @@
#ifndef PRELOGINRESPONSE_H
#define PRELOGINRESPONSE_H
#include <QtCore/QString>
#include <QtCore/QMap>
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<QString, QString> 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 993 B

View File

@@ -1,11 +0,0 @@
<RCC>
<qresource prefix="/images">
<file alias="logo.svg">com.yuezk.qt.gpclient.svg</file>
<file>connected.png</file>
<file>pending.png</file>
<file>not_connected.png</file>
<file>radio_unselected.png</file>
<file>radio_selected.png</file>
<file>settings_icon.png</file>
</qresource>
</RCC>

View File

@@ -1,99 +0,0 @@
#include <QtWidgets/QVBoxLayout>
#include <QtWebEngineWidgets/QWebEngineProfile>
#include <QtWebEngineWidgets/QWebEngineView>
#include <plog/Log.h>
#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->page()->profile()->setPersistentCookiesPolicy(QWebEngineProfile::NoPersistentCookies);
verticalLayout->addWidget(webView);
webView->initialize();
connect(webView, &EnhancedWebView::responseReceived, this, &SAMLLoginWindow::onResponseReceived);
connect(webView, &EnhancedWebView::loadFinished, this, &SAMLLoginWindow::onLoadFinished);
}
SAMLLoginWindow::~SAMLLoginWindow()
{
delete webView;
}
void SAMLLoginWindow::closeEvent(QCloseEvent *event)
{
event->accept();
reject();
}
void SAMLLoginWindow::login(const QString samlMethod, const QString samlRequest, const QString preloingUrl)
{
if (samlMethod == "POST") {
webView->setHtml(samlRequest, preloingUrl);
} else if (samlMethod == "REDIRECT") {
webView->load(samlRequest);
} else {
PLOGE << "Unknown saml-auth-method expected POST or REDIRECT, got " << samlMethod;
emit fail("Unknown saml-auth-method, got " + samlMethod);
}
}
void SAMLLoginWindow::onResponseReceived(QJsonObject params)
{
QString type = params.value("type").toString();
// Skip non-document response
if (type != "Document") {
return;
}
QJsonObject response = params.value("response").toObject();
QJsonObject headers = response.value("headers").toObject();
const QString username = headers.value("saml-username").toString();
const QString preloginCookie = headers.value("prelogin-cookie").toString();
const QString userAuthCookie = headers.value("portal-userauthcookie").toString();
LOGI << "Response received from " << response.value("url").toString();
if (!username.isEmpty()) {
LOGI << "Got username from SAML response headers " << username;
samlResult.insert("username", username);
}
if (!preloginCookie.isEmpty()) {
LOGI << "Got prelogin-cookie from SAML response headers " << preloginCookie;
samlResult.insert("preloginCookie", preloginCookie);
}
if (!userAuthCookie.isEmpty()) {
LOGI << "Got portal-userauthcookie from SAML response headers " << userAuthCookie;
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();
} else {
this->show();
}
}
void SAMLLoginWindow::onLoadFinished()
{
LOGI << "Load finished " << this->webView->page()->url().toString();
}

View File

@@ -1,35 +0,0 @@
#ifndef SAMLLOGINWINDOW_H
#define SAMLLOGINWINDOW_H
#include <QtCore/QMap>
#include <QtGui/QCloseEvent>
#include <QtWidgets/QDialog>
#include "enhancedwebview.h"
class SAMLLoginWindow : public QDialog
{
Q_OBJECT
public:
explicit SAMLLoginWindow(QWidget *parent = nullptr);
~SAMLLoginWindow();
void login(const QString samlMethod, const QString samlRequest, const QString preloingUrl);
signals:
void success(QMap<QString, QString> samlResult);
void fail(const QString msg);
private slots:
void onResponseReceived(QJsonObject params);
void onLoadFinished();
private:
EnhancedWebView *webView;
QMap<QString, QString> samlResult;
void closeEvent(QCloseEvent *event);
};
#endif // SAMLLOGINWINDOW_H

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,34 +0,0 @@
#include "settingsdialog.h"
#include "ui_settingsdialog.h"
SettingsDialog::SettingsDialog(QWidget *parent) :
QDialog(parent),
ui(new Ui::SettingsDialog)
{
ui->setupUi(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();
}

View File

@@ -1,28 +0,0 @@
#ifndef SETTINGSDIALOG_H
#define SETTINGSDIALOG_H
#include <QtWidgets/QDialog>
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();
private:
Ui::SettingsDialog *ui;
};
#endif // SETTINGSDIALOG_H

View File

@@ -1,104 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SettingsDialog</class>
<widget class="QDialog" name="SettingsDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>488</width>
<height>177</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Settings</string>
</property>
<property name="windowIcon">
<iconset resource="resources.qrc">
<normaloff>:/images/connected.png</normaloff>:/images/connected.png</iconset>
</property>
<layout class="QFormLayout" name="formLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Custom Parameters:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QPlainTextEdit" name="extraArgsInput">
<property name="placeholderText">
<string extracomment="Tokens with spaces can be surrounded by double quotes">e.g. --name=value --script=&quot;vpn-slice xxx&quot;</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Value of &quot;clientos&quot;:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="clientosInput">
<property name="placeholderText">
<string>e.g., Windows</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="resources.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>SettingsDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>SettingsDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -1,74 +0,0 @@
include("${CMAKE_SOURCE_DIR}/cmake/Add3rdParty.cmake")
project(GPService)
set(gpservice_GENERATED_SOURCES)
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.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
)
target_compile_definitions(gpservice PUBLIC QAPPLICATION_CLASS=QCoreApplication)
install(TARGETS gpservice DESTINATION bin)
install(FILES "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)
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()

View File

@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE busconfig PUBLIC
"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<policy user="root">
<allow own="com.yuezk.qt.GPService"/>
</policy>
<policy context="default">
<allow send_destination="com.yuezk.qt.GPService"
send_interface="com.yuezk.qt.GPService"
/>
<allow send_destination="com.yuezk.qt.GPService"
send_interface="org.freedesktop.DBus.Introspectable"
/>
</policy>
</busconfig>

View File

@@ -1,5 +0,0 @@
[D-BUS Service]
Name=com.yuezk.qt.GPService
Exec=@CMAKE_INSTALL_PREFIX@/bin/gpservice
User=root
SystemdService=gpservice.service

View File

@@ -1,209 +0,0 @@
#include <QtCore/QFileInfo>
#include <QtCore/QDateTime>
#include <QtCore/QVariant>
#include <QtCore/QRegularExpression>
#include <QtCore/QRegularExpressionMatch>
#include <QtDBus/QtDBus>
#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<int, QProcess::ExitStatus>::of(&QProcess::finished), this, &GPService::onProcessFinished);
}
GPService::~GPService()
{
delete openconnect;
}
QString GPService::findBinary()
{
for (int i = 0; i < binaryPaths->length(); i++) {
if (QFileInfo::exists(binaryPaths[i])) {
return binaryPaths[i];
}
}
return nullptr;
}
/* Port from https://github.com/qt/qtbase/blob/11d1dcc6e263c5059f34b44d531c9ccdf7c0b1d6/src/corelib/io/qprocess.cpp#L2115 */
QStringList GPService::splitCommand(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, QString extraArgs)
{
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;
}
QStringList args;
args << QCoreApplication::arguments().mid(1)
<< "--protocol=gp"
<< splitCommand(extraArgs)
<< "-u" << username
<< "--cookie-on-stdin"
<< server;
log("Start process with arugments: " + 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) {
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);
}

View File

@@ -1,62 +0,0 @@
#ifndef GLOBALPROTECTSERVICE_H
#define GLOBALPROTECTSERVICE_H
#include <QtCore/QObject>
#include <QtCore/QProcess>
static const QString binaryPaths[] {
"/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, QString extraArgs);
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 QStringList splitCommand(QString command);
};
#endif // GLOBALPROTECTSERVICE_H

View File

@@ -1,27 +0,0 @@
#include <QtDBus/QtDBus>
#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();
}

View File

@@ -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

195
README.md
View File

@@ -1,161 +1,128 @@
# GlobalProtect-openconnect
A GlobalProtect VPN client (GUI) for Linux based on Openconnect and built with Qt5, supports SAML auth mode, inspired by [gp-saml-gui](https://github.com/dlenski/gp-saml-gui).
A GUI for GlobalProtect VPN, based on OpenConnect, supports the SSO authentication method. Inspired by [gp-saml-gui](https://github.com/dlenski/gp-saml-gui).
<p align="center">
<img src="https://user-images.githubusercontent.com/3297602/133869036-5c02b0d9-c2d9-4f87-8c81-e44f68cfd6ac.png">
<img width="300" src="https://github.com/yuezk/GlobalProtect-openconnect/assets/3297602/9242df9c-217d-42ab-8c21-8f9f69cd4eb5">
</p>
<a href="https://ko-fi.com/M4M75PYKZ" target="_blank"><img src="https://ko-fi.com/img/githubbutton_sm.svg" alt="ko-fi" style="height: 35px !important;width: auto !important;"></a>
<a href="https://www.buymeacoffee.com/yuezk" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 35px !important;width: auto !important;" ></a>
## Features
- Similar user experience as the official client in macOS.
- Supports both SAML and non-SAML authentication modes.
- Supports automatically selecting the preferred gateway from the multiple gateways.
- Supports switching gateway from the system tray menu manually.
- [x] Better Linux support
- [x] Support both CLI and GUI
- [x] Support both SSO and non-SSO authentication
- [x] Support authentication using default browser
- [x] Support multiple portals
- [x] Support gateway selection
- [x] Support auto-connect on startup
- [x] Support system tray icon
## Future plan
## Usage
- [ ] Improve the release process
- [ ] Process bugs and feature requests
- [ ] Support for bypassing the `gpclient` parameters
- [ ] Support the CLI mode
### CLI
## Passing the Custom Parameters to `OpenConnect` CLI
The CLI version is always free and open source in this repo. It has almost the same features as the GUI version.
Custom parameters can be appended to the `OpenConnect` CLI with the following settings.
```
Usage: gpclient [OPTIONS] <COMMAND>
> Tokens with spaces can be surrounded by double quotes; three consecutive double quotes represent the quote character itself.
Commands:
connect Connect to a portal server
disconnect Disconnect from the server
launch-gui Launch the GUI
help Print this message or the help of the given subcommand(s)
Options:
--fix-openssl Get around the OpenSSL `unsafe legacy renegotiation` error
--ignore-tls-errors Ignore the TLS errors
-h, --help Print help
-V, --version Print version
<p align="center">
<img src="https://user-images.githubusercontent.com/3297602/130319209-744be02b-d657-4f49-a76d-d2c81b5c46d5.png" />
<p>
## Display the system tray icon on Gnome 40
Install the [AppIndicator and KStatusNotifierItem Support](https://extensions.gnome.org/extension/615/appindicator-support/) extension and you will see the system try icon (Restart the system after the installation).
<p align="center">
<img src="https://user-images.githubusercontent.com/3297602/130831022-b93492fd-46dd-4a8e-94a4-13b5747120b7.png" />
<p>
## Prerequisites
- Openconnect v8.x
- Qt5, qt5-webengine, qt5-websockets
## Build & Install
Clone this repo with:
```sh
git clone https://github.com/yuezk/GlobalProtect-openconnect.git
cd GlobalProtect-openconnect
See 'gpclient help <command>' for more information on a specific command.
```
### Arch/Manjaro
### GUI
Install from the [globalprotect-openconnect](https://aur.archlinux.org/packages/globalprotect-openconnect/) AUR.
The GUI version is also available after you installed it. You can launch it from the application menu or run `gpclient launch-gui` in the terminal.
### Ubuntu/Mint
> [!Note]
>
> The GUI version is partially open source. Its background service is open sourced in this repo as [gpservice](./apps/gpservice/). The GUI part is a wrapper of the background service, which is not open sourced.
> **⚠️ REQUIRED for Ubuntu 18.04 ⚠️**
>
> Add this [dwmw2/openconnect](https://launchpad.net/~dwmw2/+archive/ubuntu/openconnect) PPA first to install the latest openconnect.
>
> ```sh
> sudo add-apt-repository ppa:dwmw2/openconnect
> sudo apt update
> ```
Build and install with:
## Installation
> [!Note]
>
> This instruction is for the 2.x version. The 1.x version is still available on the [1.x](https://github.com/yuezk/GlobalProtect-openconnect/tree/1.x) branch, you can build it from the source code by following the instructions in the `README.md` file.
> [!Warning]
>
> The client requires `openconnect >= 8.20`, please make sure you have it installed, you can check it with `openconnect --version`.
> Installing the client from PPA will automatically install the required version of `openconnect`.
### Debian/Ubuntu based distributions
#### Install from PPA
```sh
./scripts/install-ubuntu.sh
```
### openSUSE
Build and install with:
```sh
./scripts/install-opensuse.sh
sudo add-apt-repository ppa:yuezk/globalprotect-openconnect
sudo apt-get update
sudo apt-get install globalprotect-openconnect
```
### Fedora
> [!Note]
>
> For Linux Mint, you might need to import the GPG key with: `sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 7937C393082992E5D6E4A60453FC26B43838D761` if you encountered an error `gpg: keyserver receive failed: General error`.
Build and install with:
#### Install from deb package
```sh
./scripts/install-fedora.sh
Download the latest deb package from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. Then install it with `dpkg`:
```bash
sudo dpkg -i globalprotect-openconnect_*.deb
```
### Other Linux
### Arch Linux / Manjaro
Install the Qt5 dependencies and OpenConnect:
#### Install from AUR
- QtCore
- QtWebEngine
- QtWebSockets
- QtDBus
- openconnect v8.x
Install from AUR: [globalprotect-openconnect-git](https://aur.archlinux.org/packages/globalprotect-openconnect-git/)
...then build and install with:
```sh
./scripts/install.sh
```
yay -S globalprotect-openconnect-git
```
### Debian package
#### Install from package
Relatively manual process for now:
Download the latest package from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. Then install it with `pacman`:
* Clone the source tree
```bash
sudo pacman -U globalprotect-openconnect-*.pkg.tar.zst
```
```
git clone https://github.com/yuezk/GlobalProtect-openconnect.git
cd GlobalProtect-openconnect
```
### Fedora/OpenSUSE/CentOS/RHEL
* Install git-archive-all using the pip. Remember to adjust the version numbers etc.
#### Install from COPR
```
pip install git-archive-all
```
The package is available on [COPR](https://copr.fedorainfracloud.org/coprs/yuezk/globalprotect-openconnect/) for various RPM-based distributions. You can install it with the following commands:
* Next create an upstream source tree using git archive.
```
sudo dnf copr enable yuezk/globalprotect-openconnect
sudo dnf install globalprotect-openconnect
```
```
git-archive-all --force-submodules --prefix=globalprotect-openconnect-1.3.0/ ../globalprotect-openconnect_1.3.0.orig.tar.gz
```
#### Install from OBS
* Finally extract the source tree, build the debian package, and install it.
The package is also available on [OBS](https://build.opensuse.org/package/show/home:yuezk/globalprotect-openconnect) for various RPM-based distributions. You can follow the instructions [on this page](https://software.opensuse.org//download.html?project=home%3Ayuezk&package=globalprotect-openconnect) to install it.
```
cd ..
tar -xzvf globalprotect-openconnect_1.3.0.orig.tar.gz
cd globalprotect-openconnect-1.3.0
fakeroot dpkg-buildpackage -uc -us -sa 2>&1 | tee ../build.log
sudo dpkg -i globalprotect-openconnect_1.3.0-1ppa1_amd64.deb
```
#### Install from RPM package
### NixOS
In `configuration.nix`:
Download the latest RPM package from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page.
```
services.globalprotect = {
enable = true;
# if you need a Host Integrity Protection report
csdWrapper = "${pkgs.openconnect}/libexec/openconnect/hipreport.sh";
};
environment.systemPackages = [ globalprotect-openconnect ];
```
### Other distributions
## Troubleshooting
The application logs can be found at: `~/.cache/GlobalProtect-openconnect/gpclient.log`
The project depends on `openconnect >= 8.20`, `webkit2gtk`, `libsecret`, `libayatana-appindicator` or `libappindicator-gtk3`. You can install them first and then download the latest binary release (i.e., `*.bin.tar.gz`) from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page.
## [License](./LICENSE)
GPLv3

View File

@@ -1 +0,0 @@
1.3.4

View File

23
apps/gpauth/Cargo.toml Normal file
View File

@@ -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", "clap"] }
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

3
apps/gpauth/build.rs Normal file
View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
apps/gpauth/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

BIN
apps/gpauth/icons/icon.icns Normal file

Binary file not shown.

BIN
apps/gpauth/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
apps/gpauth/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

11
apps/gpauth/index.html Normal file
View File

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GlobalProtect Login</title>
</head>
<body>
<p>Redirecting to GlobalProtect Login...</p>
</body>
</html>

View File

@@ -0,0 +1,486 @@
use std::{
rc::Rc,
sync::Arc,
time::{Duration, Instant},
};
use anyhow::bail;
use gpapi::{
auth::SamlAuthData,
gp_params::GpParams,
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, SettingsExt, TLSErrorsPolicy, URIResponse, URIResponseExt, WebContextExt, WebResource,
WebResourceExt, WebView, WebViewExt, WebsiteDataManagerExtManual, WebsiteDataTypes,
};
enum AuthDataError {
/// Failed to load page due to TLS error
TlsError,
/// 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<SamlAuthData, AuthDataError>;
pub(crate) struct AuthWindow<'a> {
app_handle: AppHandle,
server: &'a str,
saml_request: &'a str,
user_agent: &'a str,
gp_params: Option<GpParams>,
clean: bool,
}
impl<'a> AuthWindow<'a> {
pub fn new(app_handle: AppHandle) -> Self {
Self {
app_handle,
server: "",
saml_request: "",
user_agent: "",
gp_params: None,
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 gp_params(mut self, gp_params: GpParams) -> Self {
self.gp_params.replace(gp_params);
self
}
pub fn clean(mut self, clean: bool) -> Self {
self.clean = clean;
self
}
pub async fn open(&self) -> anyhow::Result<SamlAuthData> {
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<Window>) -> anyhow::Result<SamlAuthData> {
let saml_request = self.saml_request.to_string();
let (auth_result_tx, mut auth_result_rx) = mpsc::unbounded_channel::<AuthResult>();
let raise_window_cancel_token: Arc<RwLock<Option<CancellationToken>>> = Default::default();
let gp_params = self.gp_params.as_ref().unwrap();
let tls_err_policy = if gp_params.ignore_tls_errors() {
TLSErrorsPolicy::Ignore
} else {
TLSErrorsPolicy::Fail
};
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();
if let Some(context) = wv.context() {
context.set_tls_errors_policy(tls_err_policy);
}
if let Some(settings) = wv.settings() {
let ua = settings.user_agent().unwrap_or("".into());
info!("Auth window user agent: {}", ua);
}
// 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());
}
});
let auth_result_tx_clone = auth_result_tx.clone();
wv.connect_load_failed_with_tls_errors(move |_wv, uri, cert, err| {
let redacted_uri = redact_uri(uri);
warn!(
"Failed to load uri: {} with error: {}, cert: {}",
redacted_uri, err, cert
);
send_auth_result(&auth_result_tx_clone, Err(AuthDataError::TlsError));
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);
// NOTE: Don't send error here, since load_changed event will be triggered after this
// 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();
loop {
if let Some(auth_result) = auth_result_rx.recv().await {
match auth_result {
Ok(auth_data) => return Ok(auth_data),
Err(AuthDataError::TlsError) => {
return Err(anyhow::anyhow!("TLS error: certificate verify failed"))
}
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 = '<div style="position: absolute; width: 100%; text-align: center; font-size: 20px; font-weight: bold; top: 50%; left: 50%; transform: translate(-50%, -50%);">Got invalid token, retrying...</div>';
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, gp_params).await?;
window.with_webview(move |wv| {
let wv = wv.inner();
load_saml_request(&wv, &saml_request);
})?;
}
}
}
}
}
}
fn raise_window(window: &Arc<Window>) {
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, gp_params: &GpParams) -> anyhow::Result<String> {
info!("Portal prelogin...");
match prelogin(portal, gp_params).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<AuthResult>, 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<WebView>, 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<F>(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<AuthResult>) {
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)
});
}
Err(AuthDataError::TlsError) => {
// NOTE: This is unreachable
info!("TLS error found in headers, trying to read from body...");
send_auth_result(&auth_result_tx, Err(AuthDataError::TlsError));
}
}
}
fn parse_xml_tag(html: &str, tag: &str) -> Option<String> {
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::<Result<(), String>>();
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))
}

164
apps/gpauth/src/cli.rs Normal file
View File

@@ -0,0 +1,164 @@
use clap::Parser;
use gpapi::{
auth::{SamlAuthData, SamlAuthResult},
clap::args::Os,
gp_params::{ClientOs, GpParams},
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<String>,
#[arg(long, default_value = GP_USER_AGENT)]
user_agent: String,
#[arg(long, default_value = "Linux")]
os: Os,
#[arg(long)]
os_version: Option<String>,
#[arg(long)]
hidpi: bool,
#[arg(long)]
fix_openssl: bool,
#[arg(long)]
ignore_tls_errors: bool,
#[arg(long)]
clean: bool,
}
impl Cli {
async fn run(&mut self) -> anyhow::Result<()> {
if self.ignore_tls_errors {
info!("TLS errors will be ignored");
}
let mut openssl_conf = self.prepare_env()?;
self.server = normalize_server(&self.server)?;
let gp_params = self.build_gp_params();
// Get the initial SAML request
let saml_request = match self.saml_request {
Some(ref saml_request) => saml_request.clone(),
None => portal_prelogin(&self.server, &gp_params).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<Option<NamedTempFile>> {
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)
}
fn build_gp_params(&self) -> GpParams {
let gp_params = GpParams::builder()
.user_agent(&self.user_agent)
.client_os(ClientOs::from(&self.os))
.os_version(self.os_version.clone())
.ignore_tls_errors(self.ignore_tls_errors)
.build();
gp_params
}
async fn saml_auth(&self, app_handle: AppHandle) -> anyhow::Result<SamlAuthData> {
let auth_window = AuthWindow::new(app_handle)
.server(&self.server)
.user_agent(&self.user_agent)
.gp_params(self.build_gp_params())
.saml_request(self.saml_request.as_ref().unwrap())
.clean(self.clean);
auth_window.open().await
}
}
fn create_app(cli: Cli) -> anyhow::Result<App> {
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::<Vec<_>>();
eprintln!("{} --fix-openssl {}\n", args[0], args[1..].join(" "));
}
std::process::exit(1);
}
}

9
apps/gpauth/src/main.rs Normal file
View File

@@ -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;
}

View File

@@ -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": []
}
}

23
apps/gpclient/Cargo.toml Normal file
View File

@@ -0,0 +1,23 @@
[package]
name = "gpclient"
authors.workspace = true
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
gpapi = { path = "../../crates/gpapi", features = ["clap"] }
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

129
apps/gpclient/src/cli.rs Normal file
View File

@@ -0,0 +1,129 @@
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!(),
")"
);
pub(crate) struct SharedArgs {
pub(crate) fix_openssl: bool,
pub(crate) ignore_tls_errors: bool,
}
#[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}
See 'gpclient help <command>' for more information on a specific command.
"
)]
struct Cli {
#[command(subcommand)]
command: CliCommand,
#[arg(
long,
help = "Get around the OpenSSL `unsafe legacy renegotiation` error"
)]
fix_openssl: bool,
#[arg(long, help = "Ignore the TLS errors")]
ignore_tls_errors: bool,
}
impl Cli {
fn fix_openssl(&self) -> anyhow::Result<Option<NamedTempFile>> {
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()?;
let shared_args = SharedArgs {
fix_openssl: self.fix_openssl,
ignore_tls_errors: self.ignore_tls_errors,
};
if self.ignore_tls_errors {
info!("TLS errors will be ignored");
}
match &self.command {
CliCommand::Connect(args) => ConnectHandler::new(args, &shared_args).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);
let err = err.to_string();
if err.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::<Vec<_>>();
eprintln!("{} --fix-openssl {}\n", args[0], args[1..].join(" "));
}
if err.contains("certificate verify failed") && !cli.ignore_tls_errors {
eprintln!(
"\nRe-run it with the `--ignore-tls-errors` option to ignore the certificate error, e.g.:\n"
);
// Print the command
let args = std::env::args().collect::<Vec<_>>();
eprintln!("{} --ignore-tls-errors {}\n", args[0], args[1..].join(" "));
}
std::process::exit(1);
}
}

View File

@@ -0,0 +1,175 @@
use std::{fs, sync::Arc};
use clap::Args;
use gpapi::{
clap::args::Os,
credential::{Credential, PasswordCredential},
gateway::gateway_login,
gp_params::{ClientOs, 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::{cli::SharedArgs, 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<String>,
#[arg(
short,
long,
help = "The username to use, it will prompt if not specified"
)]
user: Option<String>,
#[arg(long, short, help = "The VPNC script to use")]
script: Option<String>,
#[arg(long, default_value = GP_USER_AGENT, help = "The user agent to use")]
user_agent: String,
#[arg(long, default_value = "Linux")]
os: Os,
#[arg(long)]
os_version: Option<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,
}
impl ConnectArgs {
fn os_version(&self) -> String {
if let Some(os_version) = &self.os_version {
return os_version.to_owned();
}
match self.os {
Os::Linux => format!("Linux {}", whoami::distro()),
Os::Windows => String::from("Microsoft Windows 11 Pro , 64-bit"),
Os::Mac => String::from("Apple Mac OS X 13.4.0"),
}
}
}
pub(crate) struct ConnectHandler<'a> {
args: &'a ConnectArgs,
shared_args: &'a SharedArgs,
}
impl<'a> ConnectHandler<'a> {
pub(crate) fn new(args: &'a ConnectArgs, shared_args: &'a SharedArgs) -> Self {
Self { args, shared_args }
}
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)
.client_os(ClientOs::from(&self.args.os))
.os_version(self.args.os_version())
.ignore_tls_errors(self.shared_args.ignore_tls_errors)
.build();
let prelogin = prelogin(&portal, &gp_params).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<Credential> {
match prelogin {
Prelogin::Saml(prelogin) => {
SamlAuthLauncher::new(&self.args.server)
.saml_request(prelogin.saml_request())
.user_agent(&self.args.user_agent)
.os(self.args.os.as_str())
.os_version(Some(&self.args.os_version()))
.hidpi(self.args.hidpi)
.fix_openssl(self.shared_args.fix_openssl)
.ignore_tls_errors(self.shared_args.ignore_tls_errors)
.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);
}

View File

@@ -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::<usize>()?;
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(())
}
}

View File

@@ -0,0 +1,112 @@
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 {
#[arg(
required = false,
help = "The authentication data, used for the default browser authentication"
)]
auth_data: Option<String>,
#[arg(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");
}
let auth_data = self.args.auth_data.as_deref().unwrap_or_default();
if !auth_data.is_empty() {
// Process the authentication data, its format is `globalprotectcallback:<data>`
return feed_auth_data(auth_data).await;
}
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::<String, String>::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 feed_auth_data(auth_data: &str) -> anyhow::Result<()> {
let service_endpoint = http_endpoint().await?;
reqwest::Client::default()
.post(format!("{}/auth-data", service_endpoint))
.json(&auth_data)
.send()
.await?
.error_for_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<PathBuf> {
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"))
}

11
apps/gpclient/src/main.rs Normal file
View File

@@ -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;
}

19
apps/gpservice/Cargo.toml Normal file
View File

@@ -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

182
apps/gpservice/src/cli.rs Normal file
View File

@@ -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<String>,
#[cfg(debug_assertions)]
#[clap(long)]
no_gui: bool,
}
impl Cli {
async fn run(&mut self, redaction: Arc<Redaction>) -> 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::<WsRequest>(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<u8> {
#[cfg(debug_assertions)]
if self.no_gui {
return gpapi::GP_API_KEY.to_vec();
}
generate_key().to_vec()
}
}
fn init_logger() -> Arc<Redaction> {
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<HashMap<String, String>>, api_key: Vec<u8>, 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);
}
}

View File

@@ -0,0 +1,101 @@
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<Arc<WsServerContext>>) -> impl IntoResponse {
ctx.send_event(WsEvent::ActiveGui).await;
}
pub(crate) async fn auth_data(
State(ctx): State<Arc<WsServerContext>>,
body: String,
) -> impl IntoResponse {
ctx.send_event(WsEvent::AuthData(body)).await;
}
pub(crate) async fn ws_handler(
ws: WebSocketUpgrade,
State(ctx): State<Arc<WsServerContext>>,
) -> impl IntoResponse {
ws.on_upgrade(move |socket| handle_socket(socket, ctx))
}
async fn handle_socket(mut socket: WebSocket, ctx: Arc<WsServerContext>) {
// 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;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,14 @@
use std::sync::Arc;
use axum::{routing::{get, post}, Router};
use crate::{handlers, ws_server::WsServerContext};
pub(crate) fn routes(ctx: Arc<WsServerContext>) -> Router {
Router::new()
.route("/health", get(handlers::health))
.route("/active-gui", post(handlers::active_gui))
.route("/auth-data", post(handlers::auth_data))
.route("/ws", get(handlers::ws_handler))
.with_state(ctx)
}

Some files were not shown because too many files have changed in this diff Show More