Compare commits
311 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
6119976027 | ||
|
a286b5e418 | ||
|
882ab4001d | ||
|
52b6fa6fbd | ||
|
3bb115bd2d | ||
|
e08f239176 | ||
|
a01c55e38d | ||
|
af51bc257b | ||
|
90a8c11acb | ||
|
92b858884c | ||
|
159673652c | ||
|
200d13ef15 | ||
|
ddeef46d2e | ||
|
97c3998383 | ||
|
93aea4ee60 | ||
|
546dbf542e | ||
|
005410d40b | ||
|
3b384a199a | ||
|
b62b024a8b | ||
|
4fbd373e29 | ||
|
ae211a923a | ||
|
d94d730a44 | ||
|
18ae1c5fa5 | ||
|
a0afabeb04 | ||
|
1158ab9095 | ||
|
54ccb761e5 | ||
|
f72dbd1dec | ||
|
0814c3153a | ||
|
9f085e8b8c | ||
|
0188752c0a | ||
|
a884c41813 | ||
|
879b977321 | ||
|
e9cb253be1 | ||
|
07eacae385 | ||
|
8446874290 | ||
|
c347f97b95 | ||
|
29cfa9e24b | ||
|
1b1ce882a5 | ||
|
e9f2dbf9ea | ||
|
7c6ae315e1 | ||
|
cec0d22dc8 | ||
|
b2ca82e105 | ||
|
5ba6b1d5fc | ||
|
a96e77c758 | ||
|
79e0f0c7c1 | ||
|
187ca778f2 | ||
|
2d1aa3ba8c | ||
|
08bd4efefa | ||
|
558485f5a9 | ||
|
cff2ff9dbe | ||
|
d5d92cfbee | ||
|
a00f6a8cba | ||
|
59dee3d767 | ||
|
e94661b213 | ||
|
9dea81bdff | ||
|
6ff552c1ec | ||
|
c1b1ea1a67 | ||
|
167a8f4037 | ||
|
47776d54d9 | ||
|
5767c252b7 | ||
|
a2efcada02 | ||
|
e68aa0ffa6 | ||
|
66bcccabe4 | ||
|
3736189308 | ||
|
c408482c55 | ||
|
00b0b8eb84 | ||
|
b14294f131 | ||
|
db9249bd61 | ||
|
662e4d0b8a | ||
|
13be9179f5 | ||
|
0a55506077 | ||
|
8860efa82e | ||
|
9bc0994a8e | ||
|
1f50e4d82b | ||
|
995d1216ea | ||
|
196e91289c | ||
|
b2bb35994f | ||
|
6fe6a1387a | ||
|
aac401e7ee | ||
|
9655b735a1 | ||
|
c3bd7aeb93 | ||
|
0b55a80317 | ||
|
c6315bf384 | ||
|
87b965f80c | ||
|
b09b21ae0f | ||
|
7e372cd113 | ||
|
1e211e8912 | ||
|
8bc4049a0f | ||
|
03f8c98cb5 | ||
|
5c56acc677 | ||
|
2d8393dcf7 | ||
|
04a916a3e1 | ||
|
edc13ed14d | ||
|
dd737bc8c5 | ||
|
939f2bd94a | ||
|
abffa21268 | ||
|
705b03c0bb | ||
|
7bef2ccc68 | ||
|
bffc5d733b | ||
|
8ca2610550 | ||
|
acf184134a | ||
|
4a3f74f1c3 | ||
|
b39983a0f8 | ||
|
d6fa32d95d | ||
|
7c299f6e68 | ||
|
25e8ccd07e | ||
|
092123b075 | ||
|
feb2956cc1 | ||
|
d356839859 | ||
|
2ff39fd14e | ||
|
c3d300c807 | ||
|
ef43d10a70 | ||
|
bd73466e48 | ||
|
cc2c0ae34e | ||
|
9207f7a798 | ||
|
2069b7fd8e | ||
|
f552ef6204 | ||
|
2761f7521a | ||
|
c3939a774b | ||
|
49e5242bf2 | ||
|
3181d37b20 | ||
|
6d788a5e91 | ||
|
74c7549444 | ||
|
c52ccb87f1 | ||
|
fab25848e1 | ||
|
75a24c89cd | ||
|
15a73b7dba | ||
|
0adeaf9c28 | ||
|
fe64b2cd19 | ||
|
5788474d7e | ||
|
3559834762 | ||
|
f9926b4026 | ||
|
cb457c4b09 | ||
|
5ebfe9b0f4 | ||
|
35266dd8bf | ||
|
bf03d375e0 | ||
|
6cf909e34f | ||
|
343a6d03c1 | ||
|
fab8e7591e | ||
|
5a485197b7 | ||
|
7bc02a4208 | ||
|
3067e6e911 | ||
|
5db77e8404 | ||
|
5714063457 | ||
|
41f88ed2e0 | ||
|
4fada9bd14 | ||
|
b57fb993ca | ||
|
f6d06ed978 | ||
|
cc67de3a2b | ||
|
e2d28c83b2 | ||
|
a489c5881b | ||
|
44fd2f1d3f | ||
|
9c9b42b87f | ||
|
fb2b148b72 | ||
|
64bec9660a | ||
|
0619e91bf5 | ||
|
048aa4799f | ||
|
db0e8b801d | ||
|
d03bbc339e | ||
|
1312d54d08 | ||
|
39f99d9143 | ||
|
7a4eb0def3 | ||
|
d9b2094edd | ||
|
e6118af9f3 | ||
|
108b4be3ec | ||
|
65c59e47ec | ||
|
177da7f3a2 | ||
|
d5cd90373b | ||
|
ffa99d3783 | ||
|
4940830885 | ||
|
ad178fe56c | ||
|
829298bb84 | ||
|
8fe717d844 | ||
|
dffbc64ef5 | ||
|
b99c5a8391 | ||
|
c2f7576d10 | ||
|
4327235093 | ||
|
0699878b92 | ||
|
e3aba11506 | ||
|
ff58258d5c | ||
|
991cf25a7b | ||
|
02c70150ba | ||
|
28d8321958 | ||
|
e1c9180cae | ||
|
57df34fd1e | ||
|
04d180e11a | ||
|
6d3b127569 | ||
|
e72b25e415 | ||
|
37a511c24d | ||
|
ad7db36c92 | ||
|
11dc5920ef | ||
|
e6383916c7 | ||
|
1d9d928b26 | ||
|
c02ad5d46d | ||
|
2319c7c49c | ||
|
e0c2c14dc3 | ||
|
8f27c92e7b | ||
|
9d6ec84c14 | ||
|
dd81ed9519 | ||
|
32bd713965 | ||
|
ba92517141 | ||
|
0e4e082594 | ||
|
3e590cab7b | ||
|
3e0e4cff12 | ||
|
692df2f2c5 | ||
|
f2b9ffddde | ||
|
ca38925066 | ||
|
8591dd7e81 | ||
|
b07880930e | ||
|
fceb80e10e | ||
|
d802c56d8f | ||
|
386f08d0e8 | ||
|
9e7fb17bd3 | ||
|
36d9753008 | ||
|
e5b3df9cda | ||
|
0dd705d0c0 | ||
|
ce2360be61 | ||
|
b5b7033eee | ||
|
9e7db4eb86 | ||
|
bc07e3d496 | ||
|
452fe2f189 | ||
|
8a65099ca7 | ||
|
5c97b2df7a | ||
|
0d4485d754 | ||
|
98e641e99d | ||
|
6fa77cdbd2 | ||
|
64e6487e7e | ||
|
e8b2c1606f | ||
|
84f1480653 | ||
|
3175855122 | ||
|
fa8b5c1528 | ||
|
7b9942c7e6 | ||
|
011a1a0dec | ||
|
4a53033023 | ||
|
9c6ea1c4b5 | ||
|
3369ad4c1d | ||
|
25c9f2291a | ||
|
bba3bc7e4f | ||
|
b12b692090 | ||
|
1300a0cc43 | ||
|
165080b476 | ||
|
d6af8a1598 | ||
|
eef92b1d31 | ||
|
946ead24a4 | ||
|
39e57c8598 | ||
|
4e2e423c27 | ||
|
732a62f1ee | ||
|
9f9444a72b | ||
|
6352e1fb2b | ||
|
42cae3ff26 | ||
|
53c8572cf6 | ||
|
3f6467321f | ||
|
563ec48c8c | ||
|
3787ae164c | ||
|
04a24c34e8 | ||
|
fe68248b1f | ||
|
47013033ec | ||
|
05fb9a26bd | ||
|
96962f957c | ||
|
b4f9cfae67 | ||
|
c8942984a8 | ||
|
3907827d0e | ||
|
f089996cdc | ||
|
260b557238 | ||
|
3495dbfe18 | ||
|
cdf193024c | ||
|
76de070d78 | ||
|
420ae27888 | ||
|
6a347746cc | ||
|
624babb380 | ||
|
511b20fdcd | ||
|
abe33c7407 | ||
|
99a82c8641 | ||
|
e5d0acad3c | ||
|
38a1eded19 | ||
|
3e23e7eaae | ||
|
cf46848e63 | ||
|
2e826201d2 | ||
|
adba408dc3 | ||
|
5d613369ee | ||
|
ebd3de6f63 | ||
|
266ab65892 | ||
|
ccaf93ec31 | ||
|
e08d7d7c4d | ||
|
c14a6ad1d2 | ||
|
d91fad089f | ||
|
2c1036ff10 | ||
|
d5f9283b93 | ||
|
fe7b96ce9b | ||
|
790865c060 | ||
|
7f056c98ce | ||
|
70816a9600 | ||
|
337a94efcd | ||
|
cf34f9f70f | ||
|
3a790cdc63 | ||
|
73925fd1e2 | ||
|
e12613d9a4 | ||
|
86ad51b0ad | ||
|
1e2322b938 | ||
|
4313b9d0e7 | ||
|
4fa08c7153 | ||
|
599ff3668f | ||
|
e22bb8e1b7 | ||
|
7f5bf0ce52 | ||
|
76a4977e92 | ||
|
246ef6d9ed | ||
|
0ccb1371ab | ||
|
81d4f9836f | ||
|
cf32e44366 | ||
|
bdad3ffe4d | ||
|
cc59f031b0 |
62
.devcontainer/Dockerfile
Normal 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;
|
10
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"build": {
|
||||||
|
"dockerfile": "Dockerfile"
|
||||||
|
},
|
||||||
|
"runArgs": [
|
||||||
|
"--privileged",
|
||||||
|
"--cap-add=NET_ADMIN",
|
||||||
|
"--device=/dev/net/tun"
|
||||||
|
]
|
||||||
|
}
|
12
.editorconfig
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[{Makefile,Makefile.in}]
|
||||||
|
indent_style = tab
|
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ko_fi: yuezk
|
||||||
|
custom: ["https://buymeacoffee.com/yuezk", "https://paypal.me/zongkun"]
|
30
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Logs**
|
||||||
|
- For the GUI version, you can find the logs at `~/.local/share/gpclient/gpclient.log`
|
||||||
|
- For the CLI version, copy the output of the `gpclient` command.
|
||||||
|
|
||||||
|
**Environment:**
|
||||||
|
- OS: [e.g. Ubuntu 22.04]
|
||||||
|
- Desktop Environment: [e.g. GNOME or KDE]
|
||||||
|
- Output of `ps aux | grep 'gnome-keyring\|kwalletd5' | grep -v grep`: [Required for secure store error]
|
||||||
|
- Is remote SSH? [Yes/No]
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
188
.github/workflows/build.yaml
vendored
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
name: Build
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths-ignore:
|
||||||
|
- LICENSE
|
||||||
|
- "*.md"
|
||||||
|
- .vscode
|
||||||
|
- .devcontainer
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
- hotfix/*
|
||||||
|
- feature/*
|
||||||
|
- release/*
|
||||||
|
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=[{"runner": "ubuntu-latest", "arch": "amd64"}, {"runner": "arm64", "arch": "arm64"}]' >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo 'matrix=[{"runner": "ubuntu-latest", "arch": "amd64"}]' >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
tarball:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [setup-matrix]
|
||||||
|
steps:
|
||||||
|
- uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: 8
|
||||||
|
- name: Prepare workspace
|
||||||
|
run: rm -rf source && mkdir source
|
||||||
|
- name: Checkout GlobalProtect-openconnect
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GH_PAT }}
|
||||||
|
repository: yuezk/GlobalProtect-openconnect
|
||||||
|
ref: ${{ github.ref }}
|
||||||
|
path: source/gp
|
||||||
|
- name: Create tarball
|
||||||
|
run: |
|
||||||
|
cd source/gp
|
||||||
|
# Generate the SNAPSHOT file for non-tagged commits
|
||||||
|
if [[ "${{ github.ref }}" != "refs/tags/"* ]]; then
|
||||||
|
touch SNAPSHOT
|
||||||
|
fi
|
||||||
|
make tarball
|
||||||
|
- name: Upload tarball
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: artifact-source
|
||||||
|
if-no-files-found: error
|
||||||
|
path: |
|
||||||
|
source/gp/.build/tarball/*.tar.gz
|
||||||
|
|
||||||
|
build-gp:
|
||||||
|
needs:
|
||||||
|
- setup-matrix
|
||||||
|
- tarball
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: ${{fromJson(needs.setup-matrix.outputs.matrix)}}
|
||||||
|
package: [deb, rpm, pkg, binary]
|
||||||
|
runs-on: ${{ matrix.os.runner }}
|
||||||
|
name: build-gp (${{ matrix.package }}, ${{ matrix.os.arch }})
|
||||||
|
steps:
|
||||||
|
- name: Prepare workspace
|
||||||
|
run: |
|
||||||
|
rm -rf build-gp-${{ matrix.package }}
|
||||||
|
mkdir -p build-gp-${{ matrix.package }}
|
||||||
|
- name: Download tarball
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: artifact-source
|
||||||
|
path: build-gp-${{ matrix.package }}
|
||||||
|
- name: Docker Login
|
||||||
|
run: echo ${{ secrets.DOCKER_HUB_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin
|
||||||
|
- name: Build ${{ matrix.package }} package in Docker
|
||||||
|
run: |
|
||||||
|
docker run --rm \
|
||||||
|
-v $(pwd)/build-gp-${{ matrix.package }}:/${{ matrix.package }} \
|
||||||
|
yuezk/gpdev:${{ matrix.package }}-builder
|
||||||
|
- name: Install ${{ matrix.package }} package in Docker
|
||||||
|
run: |
|
||||||
|
docker run --rm \
|
||||||
|
-e GPGUI_INSTALLED=0 \
|
||||||
|
-v $(pwd)/build-gp-${{ matrix.package }}:/${{ matrix.package }} \
|
||||||
|
yuezk/gpdev:${{ matrix.package }}-builder \
|
||||||
|
bash install.sh
|
||||||
|
- name: Upload ${{ matrix.package }} package
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: artifact-gp-${{ matrix.package }}-${{ matrix.os.arch }}
|
||||||
|
if-no-files-found: error
|
||||||
|
path: |
|
||||||
|
build-gp-${{ matrix.package }}/artifacts/*
|
||||||
|
|
||||||
|
build-gpgui:
|
||||||
|
needs:
|
||||||
|
- setup-matrix
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: ${{fromJson(needs.setup-matrix.outputs.matrix)}}
|
||||||
|
runs-on: ${{ matrix.os.runner }}
|
||||||
|
name: build-gpgui (${{ matrix.os.arch }})
|
||||||
|
steps:
|
||||||
|
- uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: 8
|
||||||
|
- name: Prepare workspace
|
||||||
|
run: rm -rf gpgui-source && mkdir gpgui-source
|
||||||
|
- name: Checkout GlobalProtect-openconnect
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GH_PAT }}
|
||||||
|
repository: yuezk/GlobalProtect-openconnect
|
||||||
|
ref: ${{ github.ref }}
|
||||||
|
path: gpgui-source/gp
|
||||||
|
- name: Checkout gpgui@${{ github.ref_name }}
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GH_PAT }}
|
||||||
|
repository: yuezk/gpgui
|
||||||
|
ref: ${{ github.ref_name }}
|
||||||
|
path: gpgui-source/gpgui
|
||||||
|
- name: Tarball
|
||||||
|
run: |
|
||||||
|
cd gpgui-source
|
||||||
|
tar -czf gpgui.tar.gz gpgui gp
|
||||||
|
- name: Docker Login
|
||||||
|
run: echo ${{ secrets.DOCKER_HUB_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin
|
||||||
|
- name: Build gpgui in Docker
|
||||||
|
run: |
|
||||||
|
docker run --rm -v $(pwd)/gpgui-source:/gpgui yuezk/gpdev:gpgui-builder
|
||||||
|
- name: Install gpgui in Docker
|
||||||
|
run: |
|
||||||
|
cd gpgui-source
|
||||||
|
tar -xJf *.bin.tar.xz
|
||||||
|
docker run --rm -v $(pwd):/gpgui yuezk/gpdev:gpgui-builder \
|
||||||
|
bash -c "cd /gpgui/gpgui_*/ && ./gpgui --version"
|
||||||
|
- name: Upload gpgui
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: artifact-gpgui-${{ matrix.os.arch }}
|
||||||
|
if-no-files-found: error
|
||||||
|
path: |
|
||||||
|
gpgui-source/*.bin.tar.xz
|
||||||
|
gpgui-source/*.bin.tar.xz.sha256
|
||||||
|
|
||||||
|
gh-release:
|
||||||
|
if: ${{ github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/') }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- tarball
|
||||||
|
- build-gp
|
||||||
|
- build-gpgui
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Prepare workspace
|
||||||
|
run: rm -rf gh-release && mkdir gh-release
|
||||||
|
- name: Download all artifacts
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
path: gh-release
|
||||||
|
- name: Create GH release
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GH_PAT }}
|
||||||
|
RELEASE_TAG: ${{ github.ref == 'refs/heads/dev' && 'snapshot' || github.ref_name }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
NOTES: ${{ github.ref == 'refs/heads/dev' && '**!!! DO NOT USE THIS RELEASE IN PRODUCTION !!!**' || format('Release {0}', github.ref_name) }}
|
||||||
|
run: |
|
||||||
|
gh -R "$REPO" release delete $RELEASE_TAG --yes --cleanup-tag || true
|
||||||
|
gh -R "$REPO" release create $RELEASE_TAG \
|
||||||
|
--title "$RELEASE_TAG" \
|
||||||
|
--notes "$NOTES" \
|
||||||
|
${{ github.ref == 'refs/heads/dev' && '--target dev' || '' }} \
|
||||||
|
${{ github.ref == 'refs/heads/dev' && '--prerelease' || '' }} \
|
||||||
|
gh-release/artifact-source/* \
|
||||||
|
gh-release/artifact-gpgui-*/*
|
89
.github/workflows/publish.yaml
vendored
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
name: Publish Packages
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: 'Tag to publish'
|
||||||
|
required: true
|
||||||
|
revision:
|
||||||
|
description: 'Package revision'
|
||||||
|
required: true
|
||||||
|
default: "1"
|
||||||
|
ppa:
|
||||||
|
description: 'Publish to PPA'
|
||||||
|
type: boolean
|
||||||
|
required: true
|
||||||
|
default: true
|
||||||
|
obs:
|
||||||
|
description: 'Publish to OBS'
|
||||||
|
type: boolean
|
||||||
|
required: true
|
||||||
|
default: true
|
||||||
|
aur:
|
||||||
|
description: 'Publish to AUR'
|
||||||
|
type: boolean
|
||||||
|
required: true
|
||||||
|
default: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check tag exists
|
||||||
|
uses: mukunku/tag-exists-action@v1.6.0
|
||||||
|
id: check-tag
|
||||||
|
with:
|
||||||
|
tag: ${{ inputs.tag }}
|
||||||
|
- name: Exit if tag does not exist
|
||||||
|
run: |
|
||||||
|
if [[ "${{ steps.check-tag.outputs.exists }}" == "false" ]]; then
|
||||||
|
echo "Tag ${{ inputs.tag }} does not exist"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
publish-ppa:
|
||||||
|
needs: check
|
||||||
|
if: ${{ inputs.ppa }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: 8
|
||||||
|
- name: Prepare workspace
|
||||||
|
run: rm -rf publish-ppa && mkdir publish-ppa
|
||||||
|
- name: Download ${{ inputs.tag }} source code
|
||||||
|
uses: robinraju/release-downloader@v1.9
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GH_PAT }}
|
||||||
|
tag: ${{ inputs.tag }}
|
||||||
|
fileName: globalprotect-openconnect-*.tar.gz
|
||||||
|
tarBall: false
|
||||||
|
zipBall: false
|
||||||
|
out-file-path: publish-ppa
|
||||||
|
- name: Make the offline tarball
|
||||||
|
run: |
|
||||||
|
cd publish-ppa
|
||||||
|
tar -xf globalprotect-openconnect-*.tar.gz
|
||||||
|
cd globalprotect-openconnect-*/
|
||||||
|
|
||||||
|
make tarball OFFLINE=1
|
||||||
|
|
||||||
|
# Prepare the debian directory with custom files
|
||||||
|
mkdir -p .build/debian
|
||||||
|
sed 's/@RUST@/rust-all(>=1.70)/g' packaging/deb/control.in > .build/debian/control
|
||||||
|
sed 's/@OFFLINE@/1/g' packaging/deb/rules.in > .build/debian/rules
|
||||||
|
cp packaging/deb/postrm .build/debian/postrm
|
||||||
|
|
||||||
|
- name: Publish to PPA
|
||||||
|
uses: yuezk/publish-ppa-package@dev
|
||||||
|
with:
|
||||||
|
repository: "yuezk/globalprotect-openconnect"
|
||||||
|
gpg_private_key: ${{ secrets.PPA_GPG_PRIVATE_KEY }}
|
||||||
|
gpg_passphrase: ${{ secrets.PPA_GPG_PASSPHRASE }}
|
||||||
|
tarball: publish-ppa/globalprotect-openconnect-*/.build/tarball/*.tar.gz
|
||||||
|
debian_dir: publish-ppa/globalprotect-openconnect-*/.build/debian
|
||||||
|
deb_email: "k3vinyue@gmail.com"
|
||||||
|
deb_fullname: "Kevin Yue"
|
||||||
|
extra_ppa: "liushuyu-011/rust-bpo-1.75"
|
||||||
|
revision: ${{ inputs.revision }}
|
153
.github/workflows/release.yaml
vendored
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
name: Release Packages
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: 'Tag to release'
|
||||||
|
required: true
|
||||||
|
arch:
|
||||||
|
type: choice
|
||||||
|
description: 'Architecture to build'
|
||||||
|
required: true
|
||||||
|
default: all
|
||||||
|
options:
|
||||||
|
- all
|
||||||
|
- x86_64
|
||||||
|
- arm64
|
||||||
|
release-deb:
|
||||||
|
type: boolean
|
||||||
|
description: 'Build DEB package'
|
||||||
|
required: true
|
||||||
|
default: true
|
||||||
|
release-rpm:
|
||||||
|
type: boolean
|
||||||
|
description: 'Build RPM package'
|
||||||
|
required: true
|
||||||
|
default: true
|
||||||
|
release-pkg:
|
||||||
|
type: boolean
|
||||||
|
description: 'Build PKG package'
|
||||||
|
required: true
|
||||||
|
default: true
|
||||||
|
release-binary:
|
||||||
|
type: boolean
|
||||||
|
description: 'Build binary package'
|
||||||
|
required: true
|
||||||
|
default: true
|
||||||
|
gh-release:
|
||||||
|
type: boolean
|
||||||
|
description: 'Update GitHub release'
|
||||||
|
required: true
|
||||||
|
default: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check tag exists
|
||||||
|
uses: mukunku/tag-exists-action@v1.6.0
|
||||||
|
id: check-tag
|
||||||
|
with:
|
||||||
|
tag: ${{ inputs.tag }}
|
||||||
|
- name: Exit if tag does not exist
|
||||||
|
run: |
|
||||||
|
if [[ "${{ steps.check-tag.outputs.exists }}" == "false" ]]; then
|
||||||
|
echo "Tag ${{ inputs.tag }} does not exist"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
setup-matrix:
|
||||||
|
needs:
|
||||||
|
- check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
matrix: ${{ steps.set-matrix.outputs.result }}
|
||||||
|
steps:
|
||||||
|
- name: Set up matrix
|
||||||
|
id: set-matrix
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
result-encoding: string
|
||||||
|
script: |
|
||||||
|
const inputs = ${{ toJson(inputs) }}
|
||||||
|
const { arch } = inputs
|
||||||
|
const osMap = {
|
||||||
|
"all": ["ubuntu-latest", "arm64"],
|
||||||
|
"x86_64": ["ubuntu-latest"],
|
||||||
|
"arm64": ["arm64"]
|
||||||
|
}
|
||||||
|
|
||||||
|
const package = Object.entries(inputs)
|
||||||
|
.filter(([key, value]) => key.startsWith('release-') && value)
|
||||||
|
.map(([key, value]) => key.replace('release-', ''))
|
||||||
|
|
||||||
|
return JSON.stringify({
|
||||||
|
os: osMap[arch],
|
||||||
|
package,
|
||||||
|
})
|
||||||
|
|
||||||
|
build:
|
||||||
|
needs:
|
||||||
|
- setup-matrix
|
||||||
|
strategy:
|
||||||
|
matrix: ${{ fromJson(needs.setup-matrix.outputs.matrix) }}
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Prepare workspace
|
||||||
|
run: rm -rf build-${{ matrix.package }} && mkdir -p build-${{ matrix.package }}
|
||||||
|
- name: Download ${{ inputs.tag }} source code
|
||||||
|
uses: robinraju/release-downloader@v1.9
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GH_PAT }}
|
||||||
|
tag: ${{ inputs.tag }}
|
||||||
|
fileName: globalprotect-openconnect-*.tar.gz
|
||||||
|
tarBall: false
|
||||||
|
zipBall: false
|
||||||
|
out-file-path: build-${{ matrix.package }}
|
||||||
|
- name: Docker Login
|
||||||
|
run: echo ${{ secrets.DOCKER_HUB_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin
|
||||||
|
- name: Build ${{ matrix.package }} package in Docker
|
||||||
|
run: |
|
||||||
|
docker run --rm \
|
||||||
|
-v $(pwd)/build-${{ matrix.package }}:/${{ matrix.package }} \
|
||||||
|
-e INCLUDE_GUI=1 \
|
||||||
|
yuezk/gpdev:${{ matrix.package }}-builder
|
||||||
|
|
||||||
|
- name: Install ${{ matrix.package }} package in Docker
|
||||||
|
run: |
|
||||||
|
docker run --rm \
|
||||||
|
-v $(pwd)/build-${{ matrix.package }}:/${{ matrix.package }} \
|
||||||
|
yuezk/gpdev:${{ matrix.package }}-builder \
|
||||||
|
bash install.sh
|
||||||
|
|
||||||
|
- name: Upload ${{ matrix.package }} package
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: artifact-${{ matrix.os }}-${{ matrix.package }}
|
||||||
|
if-no-files-found: error
|
||||||
|
path: |
|
||||||
|
build-${{ matrix.package }}/artifacts/*
|
||||||
|
|
||||||
|
gh-release:
|
||||||
|
needs:
|
||||||
|
- build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ inputs.gh-release }}
|
||||||
|
steps:
|
||||||
|
- name: Prepare workspace
|
||||||
|
run: rm -rf gh-release && mkdir gh-release
|
||||||
|
- name: Download artifact
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
path: gh-release
|
||||||
|
- name: Update release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GH_PAT }}
|
||||||
|
prerelease: ${{ contains(github.ref, 'snapshot') }}
|
||||||
|
fail_on_unmatched_files: true
|
||||||
|
tag_name: ${{ inputs.tag }}
|
||||||
|
files: |
|
||||||
|
gh-release/artifact-*/*
|
||||||
|
|
64
.gitignore
vendored
@@ -1,56 +1,10 @@
|
|||||||
# Binaries
|
.idea
|
||||||
gpclient
|
/target
|
||||||
gpservice
|
.pnpm-store
|
||||||
|
.env
|
||||||
|
.vendor
|
||||||
|
*.tar.xz
|
||||||
|
|
||||||
# C++ objects and libs
|
.cargo
|
||||||
*.slo
|
.build
|
||||||
*.lo
|
SNAPSHOT
|
||||||
*.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*
|
|
||||||
|
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
|||||||
[submodule "singleapplication"]
|
|
||||||
path = singleapplication
|
|
||||||
url = https://github.com/itay-grudev/SingleApplication.git
|
|
9
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"rust-lang.rust-analyzer",
|
||||||
|
"tamasfe.even-better-toml",
|
||||||
|
"eamodio.gitlens",
|
||||||
|
"EditorConfig.EditorConfig",
|
||||||
|
"streetsidesoftware.code-spell-checker",
|
||||||
|
]
|
||||||
|
}
|
62
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"cSpell.words": [
|
||||||
|
"authcookie",
|
||||||
|
"badssl",
|
||||||
|
"bincode",
|
||||||
|
"chacha",
|
||||||
|
"clientos",
|
||||||
|
"cstring",
|
||||||
|
"datetime",
|
||||||
|
"disconnectable",
|
||||||
|
"distro",
|
||||||
|
"dotenv",
|
||||||
|
"dotenvy",
|
||||||
|
"getconfig",
|
||||||
|
"globalprotect",
|
||||||
|
"globalprotectcallback",
|
||||||
|
"gpapi",
|
||||||
|
"gpauth",
|
||||||
|
"gpcallback",
|
||||||
|
"gpclient",
|
||||||
|
"gpcommon",
|
||||||
|
"gpgui",
|
||||||
|
"gpservice",
|
||||||
|
"hidpi",
|
||||||
|
"jnlp",
|
||||||
|
"LOGNAME",
|
||||||
|
"oneshot",
|
||||||
|
"openconnect",
|
||||||
|
"pkcs",
|
||||||
|
"pkexec",
|
||||||
|
"pkey",
|
||||||
|
"Prelogin",
|
||||||
|
"prelogon",
|
||||||
|
"prelogonuserauthcookie",
|
||||||
|
"repr",
|
||||||
|
"reqwest",
|
||||||
|
"roxmltree",
|
||||||
|
"rspc",
|
||||||
|
"servercert",
|
||||||
|
"specta",
|
||||||
|
"sslkey",
|
||||||
|
"sysinfo",
|
||||||
|
"tanstack",
|
||||||
|
"tauri",
|
||||||
|
"tempfile",
|
||||||
|
"thiserror",
|
||||||
|
"tungstenite",
|
||||||
|
"unistd",
|
||||||
|
"unlisten",
|
||||||
|
"urlencoding",
|
||||||
|
"userauthcookie",
|
||||||
|
"utsbuf",
|
||||||
|
"uzers",
|
||||||
|
"Vite",
|
||||||
|
"vpnc",
|
||||||
|
"vpninfo",
|
||||||
|
"wmctrl",
|
||||||
|
"XAUTHORITY",
|
||||||
|
"yuezk"
|
||||||
|
],
|
||||||
|
"rust-analyzer.cargo.features": "all",
|
||||||
|
}
|
5202
Cargo.lock
generated
Normal file
61
Cargo.toml
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
[workspace]
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
|
members = ["crates/*", "apps/gpclient", "apps/gpservice", "apps/gpauth", "apps/gpgui-helper/src-tauri"]
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
rust-version = "1.70"
|
||||||
|
version = "2.3.0"
|
||||||
|
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"] }
|
||||||
|
openssl = "0.10"
|
||||||
|
pem = "3"
|
||||||
|
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"
|
||||||
|
uzers = "0.11"
|
||||||
|
whoami = "1"
|
||||||
|
thiserror = "1"
|
||||||
|
redact-engine = "0.1"
|
||||||
|
dotenvy_macro = "0.15"
|
||||||
|
compile-time = "0.2"
|
||||||
|
serde_urlencoded = "0.7"
|
||||||
|
md5="0.7"
|
||||||
|
sha256="1"
|
||||||
|
|
||||||
|
# Tauri dependencies
|
||||||
|
tauri = { version = "1.5" }
|
||||||
|
specta = "=2.0.0-rc.1"
|
||||||
|
specta-macros = "=2.0.0-rc.1"
|
||||||
|
rspc = { version = "1.0.0-rc.5", features = ["tauri"] }
|
||||||
|
|
||||||
|
[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*
|
@@ -1,59 +0,0 @@
|
|||||||
TARGET = gpclient
|
|
||||||
|
|
||||||
QT += core gui network websockets dbus webenginewidgets
|
|
||||||
|
|
||||||
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
|
|
||||||
|
|
||||||
CONFIG += c++11
|
|
||||||
|
|
||||||
include(../singleapplication/singleapplication.pri)
|
|
||||||
DEFINES += QAPPLICATION_CLASS=QApplication
|
|
||||||
|
|
||||||
# The following define makes your compiler emit warnings if you use
|
|
||||||
# any Qt feature that has been marked deprecated (the exact warnings
|
|
||||||
# depend on your compiler). Please consult the documentation of the
|
|
||||||
# deprecated API in order to know how to port your code away from it.
|
|
||||||
DEFINES += QT_DEPRECATED_WARNINGS
|
|
||||||
|
|
||||||
# You can also make your code fail to compile if it uses deprecated APIs.
|
|
||||||
# In order to do so, uncomment the following line.
|
|
||||||
# You can also select to disable deprecated APIs only up to a certain version of Qt.
|
|
||||||
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
|
|
||||||
SOURCES += \
|
|
||||||
cdpcommand.cpp \
|
|
||||||
cdpcommandmanager.cpp \
|
|
||||||
enhancedwebview.cpp \
|
|
||||||
main.cpp \
|
|
||||||
samlloginwindow.cpp \
|
|
||||||
gpclient.cpp
|
|
||||||
|
|
||||||
HEADERS += \
|
|
||||||
cdpcommand.h \
|
|
||||||
cdpcommandmanager.h \
|
|
||||||
enhancedwebview.h \
|
|
||||||
samlloginwindow.h \
|
|
||||||
gpclient.h
|
|
||||||
|
|
||||||
FORMS += \
|
|
||||||
gpclient.ui
|
|
||||||
|
|
||||||
DBUS_INTERFACES += ../GPService/gpservice.xml
|
|
||||||
|
|
||||||
# Default rules for deployment.
|
|
||||||
target.path = /usr/bin
|
|
||||||
INSTALLS += target
|
|
||||||
|
|
||||||
DISTFILES += \
|
|
||||||
com.yuezk.qt.GPClient.svg \
|
|
||||||
com.yuezk.qt.gpclient.desktop
|
|
||||||
|
|
||||||
desktop_entry.path = /usr/share/applications/
|
|
||||||
desktop_entry.files = com.yuezk.qt.gpclient.desktop
|
|
||||||
|
|
||||||
desktop_icon.path = /usr/share/pixmaps/
|
|
||||||
desktop_icon.files = com.yuezk.qt.GPClient.svg
|
|
||||||
|
|
||||||
INSTALLS += desktop_entry desktop_icon
|
|
||||||
|
|
||||||
RESOURCES += \
|
|
||||||
resources.qrc
|
|
@@ -1,30 +0,0 @@
|
|||||||
#include "cdpcommand.h"
|
|
||||||
|
|
||||||
#include <QVariantMap>
|
|
||||||
#include <QJsonDocument>
|
|
||||||
#include <QJsonObject>
|
|
||||||
|
|
||||||
CDPCommand::CDPCommand(QObject *parent) : QObject(parent)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
CDPCommand::CDPCommand(int id, QString cmd, QVariantMap& params) :
|
|
||||||
QObject(nullptr),
|
|
||||||
id(id),
|
|
||||||
cmd(cmd),
|
|
||||||
params(¶ms)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
QByteArray CDPCommand::toJson()
|
|
||||||
{
|
|
||||||
QVariantMap payloadMap;
|
|
||||||
payloadMap["id"] = id;
|
|
||||||
payloadMap["method"] = cmd;
|
|
||||||
payloadMap["params"] = *params;
|
|
||||||
|
|
||||||
QJsonObject payloadJsonObject = QJsonObject::fromVariantMap(payloadMap);
|
|
||||||
QJsonDocument payloadJson(payloadJsonObject);
|
|
||||||
|
|
||||||
return payloadJson.toJson();
|
|
||||||
}
|
|
@@ -1,24 +0,0 @@
|
|||||||
#ifndef CDPCOMMAND_H
|
|
||||||
#define CDPCOMMAND_H
|
|
||||||
|
|
||||||
#include <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
|
|
@@ -1,85 +0,0 @@
|
|||||||
#include "cdpcommandmanager.h"
|
|
||||||
#include <QVariantMap>
|
|
||||||
|
|
||||||
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()) {
|
|
||||||
qDebug() << "CDP request error";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
|
|
||||||
QJsonArray pages = doc.array();
|
|
||||||
QJsonObject page = pages.first().toObject();
|
|
||||||
QString wsUrl = page.value("webSocketDebuggerUrl").toString();
|
|
||||||
|
|
||||||
socket->open(wsUrl);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
CDPCommand *CDPCommandManager::sendCommand(QString cmd)
|
|
||||||
{
|
|
||||||
QVariantMap emptyParams;
|
|
||||||
return sendCommend(cmd, emptyParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
CDPCommand *CDPCommandManager::sendCommend(QString cmd, QVariantMap ¶ms)
|
|
||||||
{
|
|
||||||
int id = ++commandId;
|
|
||||||
CDPCommand *command = new CDPCommand(id, cmd, params);
|
|
||||||
socket->sendTextMessage(command->toJson());
|
|
||||||
commandPool.insert(id, command);
|
|
||||||
|
|
||||||
return command;
|
|
||||||
}
|
|
||||||
|
|
||||||
void CDPCommandManager::onTextMessageReceived(QString message)
|
|
||||||
{
|
|
||||||
QJsonDocument responseDoc = QJsonDocument::fromJson(message.toUtf8());
|
|
||||||
QJsonObject response = responseDoc.object();
|
|
||||||
|
|
||||||
// Response for method
|
|
||||||
if (response.contains("id")) {
|
|
||||||
int id = response.value("id").toInt();
|
|
||||||
if (commandPool.contains(id)) {
|
|
||||||
CDPCommand *command = commandPool.take(id);
|
|
||||||
command->finished();
|
|
||||||
}
|
|
||||||
} else { // Response for event
|
|
||||||
emit eventReceived(response.value("method").toString(), response.value("params").toObject());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void CDPCommandManager::onSocketDisconnected()
|
|
||||||
{
|
|
||||||
qDebug() << "WebSocket disconnected";
|
|
||||||
}
|
|
||||||
|
|
||||||
void CDPCommandManager::onSocketError(QAbstractSocket::SocketError error)
|
|
||||||
{
|
|
||||||
qDebug() << "WebSocket error" << error;
|
|
||||||
}
|
|
@@ -1,39 +0,0 @@
|
|||||||
#ifndef CDPCOMMANDMANAGER_H
|
|
||||||
#define CDPCOMMANDMANAGER_H
|
|
||||||
|
|
||||||
#include "cdpcommand.h"
|
|
||||||
#include <QObject>
|
|
||||||
#include <QHash>
|
|
||||||
#include <QtWebSockets>
|
|
||||||
#include <QNetworkAccessManager>
|
|
||||||
|
|
||||||
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
|
|
@@ -1,10 +0,0 @@
|
|||||||
[Desktop Entry]
|
|
||||||
|
|
||||||
Type=Application
|
|
||||||
Version=1.0.0
|
|
||||||
Name=GlobalProtect VPN
|
|
||||||
Comment=GlobalProtect VPN client, supports SAML auth mode
|
|
||||||
Exec=/usr/bin/gpclient
|
|
||||||
Icon=com.yuezk.qt.GPClient
|
|
||||||
Categories=Network;VPN;Utility;Qt;
|
|
||||||
Keywords=GlobalProtect;Openconnect;SAML;connection;VPN;
|
|
Before Width: | Height: | Size: 18 KiB |
@@ -1,36 +0,0 @@
|
|||||||
#include "enhancedwebview.h"
|
|
||||||
#include "cdpcommandmanager.h"
|
|
||||||
|
|
||||||
#include <QtWebEngineWidgets/QWebEngineView>
|
|
||||||
#include <QProcessEnvironment>
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,29 +0,0 @@
|
|||||||
#ifndef ENHANCEDWEBVIEW_H
|
|
||||||
#define ENHANCEDWEBVIEW_H
|
|
||||||
|
|
||||||
#include "cdpcommandmanager.h"
|
|
||||||
#include <QtWebEngineWidgets/QWebEngineView>
|
|
||||||
|
|
||||||
#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
|
|
@@ -1,227 +0,0 @@
|
|||||||
#include "gpclient.h"
|
|
||||||
#include "ui_gpclient.h"
|
|
||||||
#include "samlloginwindow.h"
|
|
||||||
|
|
||||||
#include <QDesktopWidget>
|
|
||||||
#include <QGraphicsScene>
|
|
||||||
#include <QGraphicsView>
|
|
||||||
#include <QGraphicsPixmapItem>
|
|
||||||
#include <QImage>
|
|
||||||
#include <QStyle>
|
|
||||||
#include <QMessageBox>
|
|
||||||
|
|
||||||
GPClient::GPClient(QWidget *parent)
|
|
||||||
: QMainWindow(parent)
|
|
||||||
, ui(new Ui::GPClient)
|
|
||||||
{
|
|
||||||
ui->setupUi(this);
|
|
||||||
setFixedSize(width(), height());
|
|
||||||
moveCenter();
|
|
||||||
|
|
||||||
// Restore portal from the previous settings
|
|
||||||
settings = new QSettings("com.yuezk.qt", "GPClient");
|
|
||||||
ui->portalInput->setText(settings->value("portal", "").toString());
|
|
||||||
|
|
||||||
QObject::connect(this, &GPClient::connectFailed, [this]() {
|
|
||||||
updateConnectionStatus("not_connected");
|
|
||||||
});
|
|
||||||
|
|
||||||
// QNetworkAccessManager setup
|
|
||||||
networkManager = new QNetworkAccessManager(this);
|
|
||||||
|
|
||||||
// DBus service setup
|
|
||||||
vpn = new com::yuezk::qt::GPService("com.yuezk.qt.GPService", "/", QDBusConnection::systemBus(), this);
|
|
||||||
QObject::connect(vpn, &com::yuezk::qt::GPService::connected, this, &GPClient::onVPNConnected);
|
|
||||||
QObject::connect(vpn, &com::yuezk::qt::GPService::disconnected, this, &GPClient::onVPNDisconnected);
|
|
||||||
QObject::connect(vpn, &com::yuezk::qt::GPService::logAvailable, this, &GPClient::onVPNLogAvailable);
|
|
||||||
|
|
||||||
initVpnStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
GPClient::~GPClient()
|
|
||||||
{
|
|
||||||
delete ui;
|
|
||||||
delete networkManager;
|
|
||||||
delete reply;
|
|
||||||
delete vpn;
|
|
||||||
delete settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
void GPClient::on_connectButton_clicked()
|
|
||||||
{
|
|
||||||
QString btnText = ui->connectButton->text();
|
|
||||||
|
|
||||||
if (btnText == "Connect") {
|
|
||||||
QString portal = ui->portalInput->text();
|
|
||||||
settings->setValue("portal", portal);
|
|
||||||
ui->statusLabel->setText("Authenticating...");
|
|
||||||
updateConnectionStatus("pending");
|
|
||||||
doAuth(portal);
|
|
||||||
} else if (btnText == "Cancel") {
|
|
||||||
ui->statusLabel->setText("Canceling...");
|
|
||||||
updateConnectionStatus("pending");
|
|
||||||
|
|
||||||
if (reply->isRunning()) {
|
|
||||||
reply->abort();
|
|
||||||
}
|
|
||||||
vpn->disconnect();
|
|
||||||
} else {
|
|
||||||
ui->statusLabel->setText("Disconnecting...");
|
|
||||||
updateConnectionStatus("pending");
|
|
||||||
vpn->disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void GPClient::preloginResultFinished()
|
|
||||||
{
|
|
||||||
if (reply->error()) {
|
|
||||||
qWarning() << "Prelogin request error";
|
|
||||||
emit connectFailed();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QByteArray bytes = reply->readAll();
|
|
||||||
const QString tagMethod = "saml-auth-method";
|
|
||||||
const QString tagRequest = "saml-request";
|
|
||||||
QString samlMethod;
|
|
||||||
QString samlRequest;
|
|
||||||
|
|
||||||
QXmlStreamReader xml(bytes);
|
|
||||||
while (!xml.atEnd()) {
|
|
||||||
xml.readNext();
|
|
||||||
if (xml.tokenType() == xml.StartElement) {
|
|
||||||
if (xml.name() == tagMethod) {
|
|
||||||
samlMethod = xml.readElementText();
|
|
||||||
} else if (xml.name() == tagRequest) {
|
|
||||||
samlRequest = QByteArray::fromBase64(QByteArray::fromStdString(xml.readElementText().toStdString()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (samlMethod == nullptr || samlRequest == nullptr) {
|
|
||||||
qWarning("This does not appear to be a SAML prelogin response (<saml-auth-method> or <saml-request> tags missing)");
|
|
||||||
emit connectFailed();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (samlMethod == "POST") {
|
|
||||||
// TODO
|
|
||||||
emit connectFailed();
|
|
||||||
QMessageBox msgBox;
|
|
||||||
msgBox.setText("TODO: SAML method is POST");
|
|
||||||
msgBox.exec();
|
|
||||||
} else if (samlMethod == "REDIRECT") {
|
|
||||||
samlLogin(samlRequest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void GPClient::onLoginSuccess(QJsonObject loginResult)
|
|
||||||
{
|
|
||||||
QString fullpath = "/ssl-vpn/login.esp";
|
|
||||||
QString shortpath = "gateway";
|
|
||||||
QString user = loginResult.value("saml-username").toString();
|
|
||||||
QString cookieName;
|
|
||||||
QString cookieValue;
|
|
||||||
QString cookies[]{"prelogin-cookie", "portal-userauthcookie"};
|
|
||||||
|
|
||||||
for (int i = 0; i < cookies->length(); i++) {
|
|
||||||
cookieValue = loginResult.value(cookies[i]).toString();
|
|
||||||
if (cookieValue != nullptr) {
|
|
||||||
cookieName = cookies[i];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QString host = QString("https://%1/%2:%3").arg(loginResult.value("server").toString(), shortpath, cookieName);
|
|
||||||
vpn->connect(host, user, cookieValue);
|
|
||||||
ui->statusLabel->setText("Connecting...");
|
|
||||||
updateConnectionStatus("pending");
|
|
||||||
}
|
|
||||||
|
|
||||||
void GPClient::updateConnectionStatus(QString status)
|
|
||||||
{
|
|
||||||
if (status == "not_connected") {
|
|
||||||
ui->statusLabel->setText("Not Connected");
|
|
||||||
ui->statusImage->setStyleSheet("image: url(:/images/not_connected.png); padding: 15;");
|
|
||||||
ui->connectButton->setText("Connect");
|
|
||||||
ui->connectButton->setDisabled(false);
|
|
||||||
} else if (status == "pending") {
|
|
||||||
ui->statusImage->setStyleSheet("image: url(:/images/pending.png); padding: 15;");
|
|
||||||
ui->connectButton->setText("Cancel");
|
|
||||||
ui->connectButton->setDisabled(false);
|
|
||||||
} else if (status == "connected") {
|
|
||||||
ui->statusLabel->setText("Connected");
|
|
||||||
ui->statusImage->setStyleSheet("image: url(:/images/connected.png); padding: 15;");
|
|
||||||
ui->connectButton->setText("Disconnect");
|
|
||||||
ui->connectButton->setDisabled(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void GPClient::onVPNConnected()
|
|
||||||
{
|
|
||||||
updateConnectionStatus("connected");
|
|
||||||
}
|
|
||||||
|
|
||||||
void GPClient::onVPNDisconnected()
|
|
||||||
{
|
|
||||||
updateConnectionStatus("not_connected");
|
|
||||||
}
|
|
||||||
|
|
||||||
void GPClient::onVPNLogAvailable(QString log)
|
|
||||||
{
|
|
||||||
qInfo() << log;
|
|
||||||
}
|
|
||||||
|
|
||||||
void GPClient::initVpnStatus() {
|
|
||||||
int status = vpn->status();
|
|
||||||
if (status == 1) {
|
|
||||||
ui->statusLabel->setText("Connecting...");
|
|
||||||
updateConnectionStatus("pending");
|
|
||||||
} else if (status == 2) {
|
|
||||||
updateConnectionStatus("connected");
|
|
||||||
} else if (status == 3) {
|
|
||||||
ui->statusLabel->setText("Disconnecting...");
|
|
||||||
updateConnectionStatus("pending");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void GPClient::moveCenter()
|
|
||||||
{
|
|
||||||
QDesktopWidget *desktop = QApplication::desktop();
|
|
||||||
|
|
||||||
int screenWidth, width;
|
|
||||||
int screenHeight, height;
|
|
||||||
int x, y;
|
|
||||||
QSize windowSize;
|
|
||||||
|
|
||||||
screenWidth = desktop->width();
|
|
||||||
screenHeight = desktop->height();
|
|
||||||
|
|
||||||
windowSize = size();
|
|
||||||
width = windowSize.width();
|
|
||||||
height = windowSize.height();
|
|
||||||
|
|
||||||
x = (screenWidth - width) / 2;
|
|
||||||
y = (screenHeight - height) / 2;
|
|
||||||
y -= 50;
|
|
||||||
move(x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
void GPClient::doAuth(const QString portal)
|
|
||||||
{
|
|
||||||
const QString preloginUrl = "https://" + portal + "/ssl-vpn/prelogin.esp";
|
|
||||||
reply = networkManager->post(QNetworkRequest(preloginUrl), (QByteArray) nullptr);
|
|
||||||
connect(reply, &QNetworkReply::finished, this, &GPClient::preloginResultFinished);
|
|
||||||
}
|
|
||||||
|
|
||||||
void GPClient::samlLogin(const QString loginUrl)
|
|
||||||
{
|
|
||||||
SAMLLoginWindow *loginWindow = new SAMLLoginWindow(this);
|
|
||||||
|
|
||||||
QObject::connect(loginWindow, &SAMLLoginWindow::success, this, &GPClient::onLoginSuccess);
|
|
||||||
QObject::connect(loginWindow, &SAMLLoginWindow::rejected, this, &GPClient::connectFailed);
|
|
||||||
|
|
||||||
loginWindow->login(loginUrl);
|
|
||||||
loginWindow->exec();
|
|
||||||
delete loginWindow;
|
|
||||||
}
|
|
@@ -1,47 +0,0 @@
|
|||||||
#ifndef GPCLIENT_H
|
|
||||||
#define GPCLIENT_H
|
|
||||||
|
|
||||||
#include "gpservice_interface.h"
|
|
||||||
#include <QMainWindow>
|
|
||||||
#include <QNetworkAccessManager>
|
|
||||||
#include <QNetworkReply>
|
|
||||||
|
|
||||||
QT_BEGIN_NAMESPACE
|
|
||||||
namespace Ui { class GPClient; }
|
|
||||||
QT_END_NAMESPACE
|
|
||||||
|
|
||||||
class GPClient : public QMainWindow
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
GPClient(QWidget *parent = nullptr);
|
|
||||||
~GPClient();
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void connectFailed();
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
void on_connectButton_clicked();
|
|
||||||
void preloginResultFinished();
|
|
||||||
|
|
||||||
void onLoginSuccess(QJsonObject loginResult);
|
|
||||||
|
|
||||||
void onVPNConnected();
|
|
||||||
void onVPNDisconnected();
|
|
||||||
void onVPNLogAvailable(QString log);
|
|
||||||
|
|
||||||
private:
|
|
||||||
Ui::GPClient *ui;
|
|
||||||
QNetworkAccessManager *networkManager;
|
|
||||||
QNetworkReply *reply;
|
|
||||||
com::yuezk::qt::GPService *vpn;
|
|
||||||
QSettings *settings;
|
|
||||||
|
|
||||||
void initVpnStatus();
|
|
||||||
void moveCenter();
|
|
||||||
void updateConnectionStatus(QString status);
|
|
||||||
void doAuth(const QString portal);
|
|
||||||
void samlLogin(const QString loginUrl);
|
|
||||||
};
|
|
||||||
#endif // GPCLIENT_H
|
|
@@ -1,127 +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>338</height>
|
|
||||||
</rect>
|
|
||||||
</property>
|
|
||||||
<property name="windowTitle">
|
|
||||||
<string>GP VPN Client</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">
|
|
||||||
<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>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
</widget>
|
|
||||||
<resources>
|
|
||||||
<include location="resources.qrc"/>
|
|
||||||
</resources>
|
|
||||||
<connections/>
|
|
||||||
</ui>
|
|
@@ -1,25 +0,0 @@
|
|||||||
/*
|
|
||||||
* This file was generated by qdbusxml2cpp version 0.8
|
|
||||||
* Command line was: qdbusxml2cpp -i gpservice_interface.h -p :gpservice_interface.cpp ../GPService/gpservice.xml
|
|
||||||
*
|
|
||||||
* qdbusxml2cpp is Copyright (C) 2020 The Qt Company Ltd.
|
|
||||||
*
|
|
||||||
* This is an auto-generated file.
|
|
||||||
* This file may have been hand-edited. Look for HAND-EDIT comments
|
|
||||||
* before re-generating it.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "gpservice_interface.h"
|
|
||||||
/*
|
|
||||||
* Implementation of interface class ComYuezkQtGPServiceInterface
|
|
||||||
*/
|
|
||||||
|
|
||||||
ComYuezkQtGPServiceInterface::ComYuezkQtGPServiceInterface(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent)
|
|
||||||
: QDBusAbstractInterface(service, path, staticInterfaceName(), connection, parent)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
ComYuezkQtGPServiceInterface::~ComYuezkQtGPServiceInterface()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
@@ -1,71 +0,0 @@
|
|||||||
/*
|
|
||||||
* This file was generated by qdbusxml2cpp version 0.8
|
|
||||||
* Command line was: qdbusxml2cpp -p gpservice_interface.h: ../GPService/gpservice.xml
|
|
||||||
*
|
|
||||||
* qdbusxml2cpp is Copyright (C) 2020 The Qt Company Ltd.
|
|
||||||
*
|
|
||||||
* This is an auto-generated file.
|
|
||||||
* Do not edit! All changes made to it will be lost.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef GPSERVICE_INTERFACE_H
|
|
||||||
#define GPSERVICE_INTERFACE_H
|
|
||||||
|
|
||||||
#include <QtCore/QObject>
|
|
||||||
#include <QtCore/QByteArray>
|
|
||||||
#include <QtCore/QList>
|
|
||||||
#include <QtCore/QMap>
|
|
||||||
#include <QtCore/QString>
|
|
||||||
#include <QtCore/QStringList>
|
|
||||||
#include <QtCore/QVariant>
|
|
||||||
#include <QtDBus/QtDBus>
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Proxy class for interface com.yuezk.qt.GPService
|
|
||||||
*/
|
|
||||||
class ComYuezkQtGPServiceInterface: public QDBusAbstractInterface
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
static inline const char *staticInterfaceName()
|
|
||||||
{ return "com.yuezk.qt.GPService"; }
|
|
||||||
|
|
||||||
public:
|
|
||||||
ComYuezkQtGPServiceInterface(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent = nullptr);
|
|
||||||
|
|
||||||
~ComYuezkQtGPServiceInterface();
|
|
||||||
|
|
||||||
public Q_SLOTS: // METHODS
|
|
||||||
inline QDBusPendingReply<> connect(const QString &server, const QString &username, const QString &passwd)
|
|
||||||
{
|
|
||||||
QList<QVariant> argumentList;
|
|
||||||
argumentList << QVariant::fromValue(server) << QVariant::fromValue(username) << QVariant::fromValue(passwd);
|
|
||||||
return asyncCallWithArgumentList(QStringLiteral("connect"), argumentList);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline QDBusPendingReply<> disconnect()
|
|
||||||
{
|
|
||||||
QList<QVariant> argumentList;
|
|
||||||
return asyncCallWithArgumentList(QStringLiteral("disconnect"), argumentList);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline QDBusPendingReply<int> status()
|
|
||||||
{
|
|
||||||
QList<QVariant> argumentList;
|
|
||||||
return asyncCallWithArgumentList(QStringLiteral("status"), argumentList);
|
|
||||||
}
|
|
||||||
|
|
||||||
Q_SIGNALS: // SIGNALS
|
|
||||||
void connected();
|
|
||||||
void disconnected();
|
|
||||||
void logAvailable(const QString &log);
|
|
||||||
};
|
|
||||||
|
|
||||||
namespace com {
|
|
||||||
namespace yuezk {
|
|
||||||
namespace qt {
|
|
||||||
typedef ::ComYuezkQtGPServiceInterface GPService;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
@@ -1,18 +0,0 @@
|
|||||||
#include "singleapplication.h"
|
|
||||||
#include "gpclient.h"
|
|
||||||
#include "enhancedwebview.h"
|
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
|
||||||
{
|
|
||||||
QString port = QString::fromLocal8Bit(qgetenv(ENV_CDP_PORT));
|
|
||||||
if (port == "") {
|
|
||||||
qputenv(ENV_CDP_PORT, "12315");
|
|
||||||
}
|
|
||||||
SingleApplication app(argc, argv);
|
|
||||||
GPClient w;
|
|
||||||
w.show();
|
|
||||||
|
|
||||||
QObject::connect(&app, &SingleApplication::instanceStarted, &w, &GPClient::raise);
|
|
||||||
|
|
||||||
return app.exec();
|
|
||||||
}
|
|
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 16 KiB |
@@ -1,8 +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>
|
|
||||||
</qresource>
|
|
||||||
</RCC>
|
|
@@ -1,59 +0,0 @@
|
|||||||
#include "samlloginwindow.h"
|
|
||||||
|
|
||||||
#include <QVBoxLayout>
|
|
||||||
|
|
||||||
SAMLLoginWindow::SAMLLoginWindow(QWidget *parent)
|
|
||||||
: QDialog(parent)
|
|
||||||
{
|
|
||||||
setWindowTitle("SAML Login");
|
|
||||||
resize(610, 406);
|
|
||||||
QVBoxLayout *verticalLayout = new QVBoxLayout(this);
|
|
||||||
webView = new EnhancedWebView(this);
|
|
||||||
webView->setUrl(QUrl("about:blank"));
|
|
||||||
verticalLayout->addWidget(webView);
|
|
||||||
|
|
||||||
webView->initialize();
|
|
||||||
QObject::connect(webView, &EnhancedWebView::responseReceived, this, &SAMLLoginWindow::onResponseReceived);
|
|
||||||
}
|
|
||||||
|
|
||||||
SAMLLoginWindow::~SAMLLoginWindow()
|
|
||||||
{
|
|
||||||
delete webView;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SAMLLoginWindow::closeEvent(QCloseEvent *event)
|
|
||||||
{
|
|
||||||
event->accept();
|
|
||||||
reject();
|
|
||||||
}
|
|
||||||
|
|
||||||
void SAMLLoginWindow::login(QString url)
|
|
||||||
{
|
|
||||||
webView->load(QUrl(url));
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
foreach (const QString& key, headers.keys()) {
|
|
||||||
if (key.startsWith("saml-") || key == "prelogin-cookie" || key == "portal-userauthcookie") {
|
|
||||||
samlResult.insert(key, headers.value(key));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the SAML result
|
|
||||||
if (samlResult.contains("saml-username")
|
|
||||||
&& (samlResult.contains("prelogin-cookie") || samlResult.contains("portal-userauthcookie"))) {
|
|
||||||
samlResult.insert("server", QUrl(response.value("url").toString()).authority());
|
|
||||||
emit success(samlResult);
|
|
||||||
accept();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,33 +0,0 @@
|
|||||||
#ifndef SAMLLOGINWINDOW_H
|
|
||||||
#define SAMLLOGINWINDOW_H
|
|
||||||
|
|
||||||
#include "enhancedwebview.h"
|
|
||||||
|
|
||||||
#include <QDialog>
|
|
||||||
#include <QJsonObject>
|
|
||||||
#include <QCloseEvent>
|
|
||||||
|
|
||||||
class SAMLLoginWindow : public QDialog
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit SAMLLoginWindow(QWidget *parent = nullptr);
|
|
||||||
~SAMLLoginWindow();
|
|
||||||
|
|
||||||
void login(QString url);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void success(QJsonObject samlResult);
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
void onResponseReceived(QJsonObject params);
|
|
||||||
|
|
||||||
private:
|
|
||||||
EnhancedWebView *webView;
|
|
||||||
QJsonObject samlResult;
|
|
||||||
|
|
||||||
void closeEvent(QCloseEvent *event);
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // SAMLLOGINWINDOW_H
|
|
@@ -1,52 +0,0 @@
|
|||||||
TARGET = gpservice
|
|
||||||
|
|
||||||
QT += dbus
|
|
||||||
QT -= gui
|
|
||||||
|
|
||||||
CONFIG += c++11 console
|
|
||||||
CONFIG -= app_bundle
|
|
||||||
|
|
||||||
include(../singleapplication/singleapplication.pri)
|
|
||||||
DEFINES += QAPPLICATION_CLASS=QCoreApplication
|
|
||||||
|
|
||||||
# The following define makes your compiler emit warnings if you use
|
|
||||||
# any Qt feature that has been marked deprecated (the exact warnings
|
|
||||||
# depend on your compiler). Please consult the documentation of the
|
|
||||||
# deprecated API in order to know how to port your code away from it.
|
|
||||||
DEFINES += QT_DEPRECATED_WARNINGS
|
|
||||||
|
|
||||||
# You can also make your code fail to compile if it uses deprecated APIs.
|
|
||||||
# In order to do so, uncomment the following line.
|
|
||||||
# You can also select to disable deprecated APIs only up to a certain version of Qt.
|
|
||||||
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
|
|
||||||
|
|
||||||
HEADERS += \
|
|
||||||
gpservice.h \
|
|
||||||
sigwatch.h
|
|
||||||
|
|
||||||
SOURCES += \
|
|
||||||
gpservice.cpp \
|
|
||||||
main.cpp \
|
|
||||||
sigwatch.cpp
|
|
||||||
|
|
||||||
DBUS_ADAPTORS += gpservice.xml
|
|
||||||
|
|
||||||
# Default rules for deployment.
|
|
||||||
target.path = /usr/bin
|
|
||||||
INSTALLS += target
|
|
||||||
|
|
||||||
DISTFILES += \
|
|
||||||
dbus/com.yuezk.qt.GPService.conf \
|
|
||||||
dbus/com.yuezk.qt.GPService.service \
|
|
||||||
systemd/gpservice.service
|
|
||||||
|
|
||||||
dbus_config.path = /usr/share/dbus-1/system.d/
|
|
||||||
dbus_config.files = dbus/com.yuezk.qt.GPService.conf
|
|
||||||
|
|
||||||
dbus_service.path = /usr/share/dbus-1/system-services/
|
|
||||||
dbus_service.files = dbus/com.yuezk.qt.GPService.service
|
|
||||||
|
|
||||||
systemd_service.path = /etc/systemd/system/
|
|
||||||
systemd_service.files = systemd/gpservice.service
|
|
||||||
|
|
||||||
INSTALLS += dbus_config dbus_service systemd_service
|
|
@@ -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>
|
|
@@ -1,5 +0,0 @@
|
|||||||
[D-BUS Service]
|
|
||||||
Name=com.yuezk.qt.GPService
|
|
||||||
Exec=/usr/bin/gpservice
|
|
||||||
User=root
|
|
||||||
SystemdService=gpservice.service
|
|
@@ -1,135 +0,0 @@
|
|||||||
#include "gpservice.h"
|
|
||||||
#include "gpservice_adaptor.h"
|
|
||||||
|
|
||||||
#include <QFileInfo>
|
|
||||||
#include <QtDBus>
|
|
||||||
#include <QDateTime>
|
|
||||||
#include <QVariant>
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
void GPService::quit()
|
|
||||||
{
|
|
||||||
if (openconnect->state() == QProcess::NotRunning) {
|
|
||||||
exit(0);
|
|
||||||
} else {
|
|
||||||
aboutToQuit = true;
|
|
||||||
openconnect->terminate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void GPService::connect(QString server, QString username, QString passwd)
|
|
||||||
{
|
|
||||||
if (vpnStatus != GPService::VpnNotConnected) {
|
|
||||||
log("VPN status is: " + QVariant::fromValue(vpnStatus).toString());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString bin = findBinary();
|
|
||||||
if (bin == nullptr) {
|
|
||||||
log("Could not found openconnect binary, make sure openconnect is installed, exiting.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList args;
|
|
||||||
args << QCoreApplication::arguments().mid(1)
|
|
||||||
<< "--protocol=gp"
|
|
||||||
<< "-u" << username
|
|
||||||
<< "--passwd-on-stdin"
|
|
||||||
<< "--timestamp"
|
|
||||||
<< server;
|
|
||||||
|
|
||||||
openconnect->start(bin, args);
|
|
||||||
openconnect->write(passwd.toUtf8());
|
|
||||||
openconnect->closeWriteChannel();
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
qInfo() << msg;
|
|
||||||
emit logAvailable(msg);
|
|
||||||
}
|
|
@@ -1,58 +0,0 @@
|
|||||||
#ifndef GLOBALPROTECTSERVICE_H
|
|
||||||
#define GLOBALPROTECTSERVICE_H
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <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();
|
|
||||||
|
|
||||||
enum VpnStatus {
|
|
||||||
VpnNotConnected,
|
|
||||||
VpnConnecting,
|
|
||||||
VpnConnected,
|
|
||||||
VpnDisconnecting,
|
|
||||||
};
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void connected();
|
|
||||||
void disconnected();
|
|
||||||
void logAvailable(QString log);
|
|
||||||
|
|
||||||
public slots:
|
|
||||||
void connect(QString server, QString username, QString passwd);
|
|
||||||
void disconnect();
|
|
||||||
int status();
|
|
||||||
void quit();
|
|
||||||
|
|
||||||
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);
|
|
||||||
static QString findBinary();
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // GLOBALPROTECTSERVICE_H
|
|
@@ -1,22 +0,0 @@
|
|||||||
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
|
|
||||||
<node>
|
|
||||||
<interface name="com.yuezk.qt.GPService">
|
|
||||||
<signal name="connected">
|
|
||||||
</signal>
|
|
||||||
<signal name="disconnected">
|
|
||||||
</signal>
|
|
||||||
<signal name="logAvailable">
|
|
||||||
<arg name="log" type="s" />
|
|
||||||
</signal>
|
|
||||||
<method name="connect">
|
|
||||||
<arg name="server" type="s" direction="in"/>
|
|
||||||
<arg name="username" type="s" direction="in"/>
|
|
||||||
<arg name="passwd" type="s" direction="in"/>
|
|
||||||
</method>
|
|
||||||
<method name="disconnect">
|
|
||||||
</method>
|
|
||||||
<method name="status">
|
|
||||||
<arg type="i" direction="out"/>
|
|
||||||
</method>
|
|
||||||
</interface>
|
|
||||||
</node>
|
|
@@ -1,55 +0,0 @@
|
|||||||
/*
|
|
||||||
* This file was generated by qdbusxml2cpp version 0.8
|
|
||||||
* Command line was: qdbusxml2cpp -i gpservice_adaptor.h -a :gpservice_adaptor.cpp gpservice.xml
|
|
||||||
*
|
|
||||||
* qdbusxml2cpp is Copyright (C) 2020 The Qt Company Ltd.
|
|
||||||
*
|
|
||||||
* This is an auto-generated file.
|
|
||||||
* Do not edit! All changes made to it will be lost.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "gpservice_adaptor.h"
|
|
||||||
#include <QtCore/QMetaObject>
|
|
||||||
#include <QtCore/QByteArray>
|
|
||||||
#include <QtCore/QList>
|
|
||||||
#include <QtCore/QMap>
|
|
||||||
#include <QtCore/QString>
|
|
||||||
#include <QtCore/QStringList>
|
|
||||||
#include <QtCore/QVariant>
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Implementation of adaptor class GPServiceAdaptor
|
|
||||||
*/
|
|
||||||
|
|
||||||
GPServiceAdaptor::GPServiceAdaptor(QObject *parent)
|
|
||||||
: QDBusAbstractAdaptor(parent)
|
|
||||||
{
|
|
||||||
// constructor
|
|
||||||
setAutoRelaySignals(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
GPServiceAdaptor::~GPServiceAdaptor()
|
|
||||||
{
|
|
||||||
// destructor
|
|
||||||
}
|
|
||||||
|
|
||||||
void GPServiceAdaptor::connect(const QString &server, const QString &username, const QString &passwd)
|
|
||||||
{
|
|
||||||
// handle method call com.yuezk.qt.GPService.connect
|
|
||||||
QMetaObject::invokeMethod(parent(), "connect", Q_ARG(QString, server), Q_ARG(QString, username), Q_ARG(QString, passwd));
|
|
||||||
}
|
|
||||||
|
|
||||||
void GPServiceAdaptor::disconnect()
|
|
||||||
{
|
|
||||||
// handle method call com.yuezk.qt.GPService.disconnect
|
|
||||||
QMetaObject::invokeMethod(parent(), "disconnect");
|
|
||||||
}
|
|
||||||
|
|
||||||
int GPServiceAdaptor::status()
|
|
||||||
{
|
|
||||||
// handle method call com.yuezk.qt.GPService.status
|
|
||||||
int out0;
|
|
||||||
QMetaObject::invokeMethod(parent(), "status", Q_RETURN_ARG(int, out0));
|
|
||||||
return out0;
|
|
||||||
}
|
|
||||||
|
|
@@ -1,66 +0,0 @@
|
|||||||
/*
|
|
||||||
* This file was generated by qdbusxml2cpp version 0.8
|
|
||||||
* Command line was: qdbusxml2cpp -a gpservice_adaptor.h: gpservice.xml
|
|
||||||
*
|
|
||||||
* qdbusxml2cpp is Copyright (C) 2020 The Qt Company Ltd.
|
|
||||||
*
|
|
||||||
* This is an auto-generated file.
|
|
||||||
* This file may have been hand-edited. Look for HAND-EDIT comments
|
|
||||||
* before re-generating it.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef GPSERVICE_ADAPTOR_H
|
|
||||||
#define GPSERVICE_ADAPTOR_H
|
|
||||||
|
|
||||||
#include <QtCore/QObject>
|
|
||||||
#include <QtDBus/QtDBus>
|
|
||||||
QT_BEGIN_NAMESPACE
|
|
||||||
class QByteArray;
|
|
||||||
template<class T> class QList;
|
|
||||||
template<class Key, class Value> class QMap;
|
|
||||||
class QString;
|
|
||||||
class QStringList;
|
|
||||||
class QVariant;
|
|
||||||
QT_END_NAMESPACE
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Adaptor class for interface com.yuezk.qt.GPService
|
|
||||||
*/
|
|
||||||
class GPServiceAdaptor: public QDBusAbstractAdaptor
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
Q_CLASSINFO("D-Bus Interface", "com.yuezk.qt.GPService")
|
|
||||||
Q_CLASSINFO("D-Bus Introspection", ""
|
|
||||||
" <interface name=\"com.yuezk.qt.GPService\">\n"
|
|
||||||
" <signal name=\"connected\"/>\n"
|
|
||||||
" <signal name=\"disconnected\"/>\n"
|
|
||||||
" <signal name=\"logAvailable\">\n"
|
|
||||||
" <arg type=\"s\" name=\"log\"/>\n"
|
|
||||||
" </signal>\n"
|
|
||||||
" <method name=\"connect\">\n"
|
|
||||||
" <arg direction=\"in\" type=\"s\" name=\"server\"/>\n"
|
|
||||||
" <arg direction=\"in\" type=\"s\" name=\"username\"/>\n"
|
|
||||||
" <arg direction=\"in\" type=\"s\" name=\"passwd\"/>\n"
|
|
||||||
" </method>\n"
|
|
||||||
" <method name=\"disconnect\"/>\n"
|
|
||||||
" <method name=\"status\">\n"
|
|
||||||
" <arg direction=\"out\" type=\"i\"/>\n"
|
|
||||||
" </method>\n"
|
|
||||||
" </interface>\n"
|
|
||||||
"")
|
|
||||||
public:
|
|
||||||
GPServiceAdaptor(QObject *parent);
|
|
||||||
virtual ~GPServiceAdaptor();
|
|
||||||
|
|
||||||
public: // PROPERTIES
|
|
||||||
public Q_SLOTS: // METHODS
|
|
||||||
void connect(const QString &server, const QString &username, const QString &passwd);
|
|
||||||
void disconnect();
|
|
||||||
int status();
|
|
||||||
Q_SIGNALS: // SIGNALS
|
|
||||||
void connected();
|
|
||||||
void disconnected();
|
|
||||||
void logAvailable(const QString &log);
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
@@ -1,26 +0,0 @@
|
|||||||
#include <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();
|
|
||||||
}
|
|
@@ -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"
|
|
@@ -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
|
|
@@ -1,10 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=GlobalProtect openconnect DBus service
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=dbus
|
|
||||||
BusName=com.yuezk.qt.GPService
|
|
||||||
ExecStart=/usr/bin/gpservice
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
@@ -1,5 +0,0 @@
|
|||||||
TEMPLATE = subdirs
|
|
||||||
|
|
||||||
SUBDIRS += \
|
|
||||||
GPClient \
|
|
||||||
GPService
|
|
263
Makefile
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
.SHELLFLAGS += -e
|
||||||
|
|
||||||
|
OFFLINE ?= 0
|
||||||
|
BUILD_FE ?= 1
|
||||||
|
INCLUDE_GUI ?= 0
|
||||||
|
CARGO ?= cargo
|
||||||
|
|
||||||
|
VERSION = $(shell $(CARGO) metadata --no-deps --format-version 1 | jq -r '.packages[0].version')
|
||||||
|
REVISION ?= 1
|
||||||
|
PPA_REVISION ?= 1
|
||||||
|
PKG_NAME = globalprotect-openconnect
|
||||||
|
PKG = $(PKG_NAME)-$(VERSION)
|
||||||
|
SERIES ?= $(shell lsb_release -cs)
|
||||||
|
PUBLISH ?= 0
|
||||||
|
|
||||||
|
export DEBEMAIL = k3vinyue@gmail.com
|
||||||
|
export DEBFULLNAME = Kevin Yue
|
||||||
|
export SNAPSHOT = $(shell test -f SNAPSHOT && echo "true" || echo "false")
|
||||||
|
|
||||||
|
ifeq ($(SNAPSHOT), true)
|
||||||
|
RELEASE_TAG = snapshot
|
||||||
|
else
|
||||||
|
RELEASE_TAG = v$(VERSION)
|
||||||
|
endif
|
||||||
|
|
||||||
|
CARGO_BUILD_ARGS = --release
|
||||||
|
|
||||||
|
ifeq ($(OFFLINE), 1)
|
||||||
|
CARGO_BUILD_ARGS += --frozen
|
||||||
|
endif
|
||||||
|
|
||||||
|
default: build
|
||||||
|
|
||||||
|
version:
|
||||||
|
@echo $(VERSION)
|
||||||
|
|
||||||
|
clean-tarball:
|
||||||
|
rm -rf .build/tarball
|
||||||
|
rm -rf .vendor
|
||||||
|
rm -rf vendor.tar.xz
|
||||||
|
rm -rf .cargo
|
||||||
|
|
||||||
|
# Create a tarball, include the cargo dependencies if OFFLINE is set to 1
|
||||||
|
tarball: clean-tarball
|
||||||
|
if [ $(BUILD_FE) -eq 1 ]; then \
|
||||||
|
echo "Building frontend..."; \
|
||||||
|
cd apps/gpgui-helper && pnpm install && pnpm build; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove node_modules to reduce the tarball size
|
||||||
|
rm -rf apps/gpgui-helper/node_modules
|
||||||
|
|
||||||
|
mkdir -p .cargo
|
||||||
|
mkdir -p .build/tarball
|
||||||
|
|
||||||
|
# If OFFLINE is set to 1, vendor all cargo dependencies
|
||||||
|
if [ $(OFFLINE) -eq 1 ]; then \
|
||||||
|
$(CARGO) vendor .vendor > .cargo/config.toml; \
|
||||||
|
tar -cJf vendor.tar.xz .vendor; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
@echo "Creating tarball..."
|
||||||
|
tar --exclude .vendor --exclude target --transform 's,^,${PKG}/,' -czf .build/tarball/${PKG}.tar.gz * .cargo
|
||||||
|
|
||||||
|
download-gui:
|
||||||
|
rm -rf .build/gpgui
|
||||||
|
|
||||||
|
if [ $(INCLUDE_GUI) -eq 1 ]; then \
|
||||||
|
echo "Downloading GlobalProtect GUI..."; \
|
||||||
|
mkdir -p .build/gpgui; \
|
||||||
|
curl -sSL https://github.com/yuezk/GlobalProtect-openconnect/releases/download/$(RELEASE_TAG)/gpgui_$(shell uname -m).bin.tar.xz \
|
||||||
|
-o .build/gpgui/gpgui_$(shell uname -m).bin.tar.xz; \
|
||||||
|
tar -xJf .build/gpgui/*.tar.xz -C .build/gpgui; \
|
||||||
|
else \
|
||||||
|
echo "Skipping GlobalProtect GUI download (INCLUDE_GUI=0)"; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
build: download-gui build-fe build-rs
|
||||||
|
|
||||||
|
# Install and build the frontend
|
||||||
|
# If OFFLINE is set to 1, skip it
|
||||||
|
build-fe:
|
||||||
|
if [ $(OFFLINE) -eq 1 ] || [ $(BUILD_FE) -eq 0 ]; then \
|
||||||
|
echo "Skipping frontend build (OFFLINE=1 or BUILD_FE=0)"; \
|
||||||
|
else \
|
||||||
|
cd apps/gpgui-helper && pnpm install && pnpm build; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d apps/gpgui-helper/dist ]; then \
|
||||||
|
echo "Error: frontend build failed"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
build-rs:
|
||||||
|
if [ $(OFFLINE) -eq 1 ]; then \
|
||||||
|
tar -xJf vendor.tar.xz; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
$(CARGO) build $(CARGO_BUILD_ARGS) -p gpclient -p gpservice -p gpauth
|
||||||
|
$(CARGO) build $(CARGO_BUILD_ARGS) -p gpgui-helper --features "tauri/custom-protocol"
|
||||||
|
|
||||||
|
clean:
|
||||||
|
$(CARGO) clean
|
||||||
|
rm -rf .build
|
||||||
|
rm -rf .vendor
|
||||||
|
rm -rf apps/gpgui-helper/node_modules
|
||||||
|
|
||||||
|
install:
|
||||||
|
@echo "Installing $(PKG_NAME)..."
|
||||||
|
|
||||||
|
install -Dm755 target/release/gpclient $(DESTDIR)/usr/bin/gpclient
|
||||||
|
install -Dm755 target/release/gpauth $(DESTDIR)/usr/bin/gpauth
|
||||||
|
install -Dm755 target/release/gpservice $(DESTDIR)/usr/bin/gpservice
|
||||||
|
install -Dm755 target/release/gpgui-helper $(DESTDIR)/usr/bin/gpgui-helper
|
||||||
|
|
||||||
|
if [ -f .build/gpgui/gpgui_*/gpgui ]; then \
|
||||||
|
install -Dm755 .build/gpgui/gpgui_*/gpgui $(DESTDIR)/usr/bin/gpgui; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
install -Dm644 packaging/files/usr/share/applications/gpgui.desktop $(DESTDIR)/usr/share/applications/gpgui.desktop
|
||||||
|
install -Dm644 packaging/files/usr/share/icons/hicolor/scalable/apps/gpgui.svg $(DESTDIR)/usr/share/icons/hicolor/scalable/apps/gpgui.svg
|
||||||
|
install -Dm644 packaging/files/usr/share/icons/hicolor/32x32/apps/gpgui.png $(DESTDIR)/usr/share/icons/hicolor/32x32/apps/gpgui.png
|
||||||
|
install -Dm644 packaging/files/usr/share/icons/hicolor/128x128/apps/gpgui.png $(DESTDIR)/usr/share/icons/hicolor/128x128/apps/gpgui.png
|
||||||
|
install -Dm644 packaging/files/usr/share/icons/hicolor/256x256@2/apps/gpgui.png $(DESTDIR)/usr/share/icons/hicolor/256x256@2/apps/gpgui.png
|
||||||
|
install -Dm644 packaging/files/usr/share/polkit-1/actions/com.yuezk.gpgui.policy $(DESTDIR)/usr/share/polkit-1/actions/com.yuezk.gpgui.policy
|
||||||
|
|
||||||
|
uninstall:
|
||||||
|
@echo "Uninstalling $(PKG_NAME)..."
|
||||||
|
|
||||||
|
rm -f $(DESTDIR)/usr/bin/gpclient
|
||||||
|
rm -f $(DESTDIR)/usr/bin/gpauth
|
||||||
|
rm -f $(DESTDIR)/usr/bin/gpservice
|
||||||
|
rm -f $(DESTDIR)/usr/bin/gpgui-helper
|
||||||
|
rm -f $(DESTDIR)/usr/bin/gpgui
|
||||||
|
|
||||||
|
rm -f $(DESTDIR)/usr/share/applications/gpgui.desktop
|
||||||
|
rm -f $(DESTDIR)/usr/share/icons/hicolor/scalable/apps/gpgui.svg
|
||||||
|
rm -f $(DESTDIR)/usr/share/icons/hicolor/32x32/apps/gpgui.png
|
||||||
|
rm -f $(DESTDIR)/usr/share/icons/hicolor/128x128/apps/gpgui.png
|
||||||
|
rm -f $(DESTDIR)/usr/share/icons/hicolor/256x256@2/apps/gpgui.png
|
||||||
|
rm -f $(DESTDIR)/usr/share/polkit-1/actions/com.yuezk.gpgui.policy
|
||||||
|
|
||||||
|
clean-debian:
|
||||||
|
rm -rf .build/deb
|
||||||
|
|
||||||
|
# Generate the debian package structure, without the changelog
|
||||||
|
init-debian: clean-debian tarball
|
||||||
|
mkdir -p .build/deb
|
||||||
|
cp .build/tarball/${PKG}.tar.gz .build/deb
|
||||||
|
|
||||||
|
tar -xzf .build/deb/${PKG}.tar.gz -C .build/deb
|
||||||
|
cd .build/deb/${PKG} && debmake
|
||||||
|
|
||||||
|
cp -f packaging/deb/control.in .build/deb/$(PKG)/debian/control
|
||||||
|
cp -f packaging/deb/rules.in .build/deb/$(PKG)/debian/rules
|
||||||
|
cp -f packaging/deb/postrm .build/deb/$(PKG)/debian/postrm
|
||||||
|
|
||||||
|
sed -i "s/@OFFLINE@/$(OFFLINE)/g" .build/deb/$(PKG)/debian/rules
|
||||||
|
|
||||||
|
rm -f .build/deb/$(PKG)/debian/changelog
|
||||||
|
|
||||||
|
deb: init-debian
|
||||||
|
# Remove the rust build depdency from the control file
|
||||||
|
sed -i "s/@RUST@//g" .build/deb/$(PKG)/debian/control
|
||||||
|
|
||||||
|
cd .build/deb/$(PKG) && dch --create --distribution unstable --package $(PKG_NAME) --newversion $(VERSION)-$(REVISION) "Bugfix and improvements."
|
||||||
|
|
||||||
|
cd .build/deb/$(PKG) && debuild --preserve-env -e PATH -us -uc -b
|
||||||
|
|
||||||
|
check-ppa:
|
||||||
|
if [ $(OFFLINE) -eq 0 ]; then \
|
||||||
|
echo "Error: ppa build requires offline mode (OFFLINE=1)"; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Usage: make ppa SERIES=focal OFFLINE=1 PUBLISH=1
|
||||||
|
ppa: check-ppa init-debian
|
||||||
|
sed -i "s/@RUST@/rust-all(>=1.70)/g" .build/deb/$(PKG)/debian/control
|
||||||
|
|
||||||
|
$(eval SERIES_VER = $(shell distro-info --series $(SERIES) -r | cut -d' ' -f1))
|
||||||
|
@echo "Building for $(SERIES) $(SERIES_VER)"
|
||||||
|
|
||||||
|
rm -rf .build/deb/$(PKG)/debian/changelog
|
||||||
|
cd .build/deb/$(PKG) && dch --create --distribution $(SERIES) --package $(PKG_NAME) --newversion $(VERSION)-$(REVISION)ppa$(PPA_REVISION)~ubuntu$(SERIES_VER) "Bugfix and improvements."
|
||||||
|
|
||||||
|
cd .build/deb/$(PKG) && echo "y" | debuild -e PATH -S -sa -k"$(GPG_KEY_ID)" -p"gpg --batch --passphrase $(GPG_KEY_PASS) --pinentry-mode loopback"
|
||||||
|
|
||||||
|
if [ $(PUBLISH) -eq 1 ]; then \
|
||||||
|
cd .build/deb/$(PKG) && dput ppa:yuezk/globalprotect-openconnect ../*.changes; \
|
||||||
|
else \
|
||||||
|
echo "Skipping ppa publish (PUBLISH=0)"; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
clean-rpm:
|
||||||
|
rm -rf .build/rpm
|
||||||
|
|
||||||
|
# Generate RPM sepc file
|
||||||
|
init-rpm: clean-rpm
|
||||||
|
mkdir -p .build/rpm
|
||||||
|
|
||||||
|
cp packaging/rpm/globalprotect-openconnect.spec.in .build/rpm/globalprotect-openconnect.spec
|
||||||
|
cp packaging/rpm/globalprotect-openconnect.changes.in .build/rpm/globalprotect-openconnect.changes
|
||||||
|
|
||||||
|
sed -i "s/@VERSION@/$(VERSION)/g" .build/rpm/globalprotect-openconnect.spec
|
||||||
|
sed -i "s/@REVISION@/$(REVISION)/g" .build/rpm/globalprotect-openconnect.spec
|
||||||
|
sed -i "s/@OFFLINE@/$(OFFLINE)/g" .build/rpm/globalprotect-openconnect.spec
|
||||||
|
sed -i "s/@DATE@/$(shell LC_ALL=en.US date "+%a %b %d %Y")/g" .build/rpm/globalprotect-openconnect.spec
|
||||||
|
|
||||||
|
sed -i "s/@VERSION@/$(VERSION)/g" .build/rpm/globalprotect-openconnect.changes
|
||||||
|
sed -i "s/@DATE@/$(shell LC_ALL=en.US date -u "+%a %b %e %T %Z %Y")/g" .build/rpm/globalprotect-openconnect.changes
|
||||||
|
|
||||||
|
rpm: init-rpm tarball
|
||||||
|
rm -rf $(HOME)/rpmbuild
|
||||||
|
rpmdev-setuptree
|
||||||
|
|
||||||
|
cp .build/tarball/${PKG}.tar.gz $(HOME)/rpmbuild/SOURCES/${PKG_NAME}.tar.gz
|
||||||
|
rpmbuild -ba .build/rpm/globalprotect-openconnect.spec
|
||||||
|
|
||||||
|
# Copy RPM package from build directory
|
||||||
|
cp $(HOME)/rpmbuild/RPMS/$(shell uname -m)/$(PKG_NAME)*.rpm .build/rpm
|
||||||
|
|
||||||
|
# Copy the SRPM only for x86_64.
|
||||||
|
if [ "$(shell uname -m)" = "x86_64" ]; then \
|
||||||
|
cp $(HOME)/rpmbuild/SRPMS/$(PKG_NAME)*.rpm .build/rpm; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
clean-pkgbuild:
|
||||||
|
rm -rf .build/pkgbuild
|
||||||
|
|
||||||
|
init-pkgbuild: clean-pkgbuild tarball
|
||||||
|
mkdir -p .build/pkgbuild
|
||||||
|
|
||||||
|
cp .build/tarball/${PKG}.tar.gz .build/pkgbuild
|
||||||
|
cp packaging/pkgbuild/PKGBUILD.in .build/pkgbuild/PKGBUILD
|
||||||
|
|
||||||
|
sed -i "s/@PKG_NAME@/$(PKG_NAME)/g" .build/pkgbuild/PKGBUILD
|
||||||
|
sed -i "s/@VERSION@/$(VERSION)/g" .build/pkgbuild/PKGBUILD
|
||||||
|
sed -i "s/@REVISION@/$(REVISION)/g" .build/pkgbuild/PKGBUILD
|
||||||
|
sed -i "s/@OFFLINE@/$(OFFLINE)/g" .build/pkgbuild/PKGBUILD
|
||||||
|
|
||||||
|
pkgbuild: init-pkgbuild
|
||||||
|
cd .build/pkgbuild && makepkg -s --noconfirm
|
||||||
|
|
||||||
|
clean-binary:
|
||||||
|
rm -rf .build/binary
|
||||||
|
|
||||||
|
binary: clean-binary tarball
|
||||||
|
mkdir -p .build/binary
|
||||||
|
|
||||||
|
cp .build/tarball/${PKG}.tar.gz .build/binary
|
||||||
|
tar -xzf .build/binary/${PKG}.tar.gz -C .build/binary
|
||||||
|
|
||||||
|
mkdir -p .build/binary/$(PKG_NAME)_$(VERSION)/artifacts
|
||||||
|
|
||||||
|
make -C .build/binary/${PKG} build OFFLINE=$(OFFLINE) BUILD_FE=0 INCLUDE_GUI=$(INCLUDE_GUI)
|
||||||
|
make -C .build/binary/${PKG} install DESTDIR=$(PWD)/.build/binary/$(PKG_NAME)_$(VERSION)/artifacts
|
||||||
|
|
||||||
|
cp packaging/binary/Makefile.in .build/binary/$(PKG_NAME)_$(VERSION)/Makefile
|
||||||
|
|
||||||
|
# Create a tarball for the binary package
|
||||||
|
tar -cJf .build/binary/$(PKG_NAME)_$(VERSION)_$(shell uname -m).bin.tar.xz -C .build/binary $(PKG_NAME)_$(VERSION)
|
||||||
|
|
||||||
|
# Generate sha256sum
|
||||||
|
cd .build/binary && sha256sum $(PKG_NAME)_$(VERSION)_$(shell uname -m).bin.tar.xz | cut -d' ' -f1 > $(PKG_NAME)_$(VERSION)_$(shell uname -m).bin.tar.xz.sha256
|
228
README.md
@@ -1,33 +1,219 @@
|
|||||||
# GlobalProtect-openconnect
|
# 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">
|
<p align="center">
|
||||||
<img src="screenshot.png">
|
<img width="300" src="https://github.com/yuezk/GlobalProtect-openconnect/assets/3297602/9242df9c-217d-42ab-8c21-8f9f69cd4eb5">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Prerequisites
|
## Features
|
||||||
|
|
||||||
- Openconnect v8.x
|
- [x] Better Linux support
|
||||||
- Qt5, qt5-webengine, qt5-websockets
|
- [x] Support both CLI and GUI
|
||||||
|
- [x] Support both SSO and non-SSO authentication
|
||||||
|
- [x] Support the FIDO2 authentication (e.g., YubiKey)
|
||||||
|
- [x] Support authentication using default browser
|
||||||
|
- [x] Support client certificate authentication
|
||||||
|
- [x] Support multiple portals
|
||||||
|
- [x] Support gateway selection
|
||||||
|
- [x] Support connect gateway directly
|
||||||
|
- [x] Support auto-connect on startup
|
||||||
|
- [x] Support system tray icon
|
||||||
|
|
||||||
### Ubuntu
|
## Usage
|
||||||
1. Install openconnect v8.x
|
|
||||||
Update openconnect to 8.x, for ubuntu 18.04 you might need to [build the latest openconnect from source code](https://gist.github.com/yuezk/ab9a4b87a9fa0182bdb2df41fab5f613).
|
### CLI
|
||||||
2. Install the Qt dependencies
|
|
||||||
```sh
|
The CLI version is always free and open source in this repo. It has almost the same features as the GUI version.
|
||||||
sudo apt install qt5-default libqt5websockets5-dev qtwebengine5-dev
|
|
||||||
```
|
|
||||||
## Install
|
|
||||||
|
|
||||||
```sh
|
|
||||||
git clone https://github.com/yuezk/GlobalProtect-openconnect.git
|
|
||||||
cd GlobalProtect-openconnect
|
|
||||||
git submodule init && git submodule update
|
|
||||||
qmake CONFIG+=release
|
|
||||||
make
|
|
||||||
sudo make install
|
|
||||||
```
|
```
|
||||||
Open `GlobalProtect VPN` in the application dashboard.
|
Usage: gpclient [OPTIONS] <COMMAND>
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
See 'gpclient help <command>' for more information on a specific command.
|
||||||
|
```
|
||||||
|
|
||||||
|
To use the default browser for authentication with the CLI version, you need to use the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo -E gpclient connect --default-browser <portal>
|
||||||
|
```
|
||||||
|
|
||||||
|
### GUI
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
> [!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.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Debian/Ubuntu based distributions
|
||||||
|
|
||||||
|
#### Install from PPA (Ubuntu 18.04 and later, except 24.04)
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo apt-get install gir1.2-gtk-3.0 gir1.2-webkit2-4.0
|
||||||
|
sudo add-apt-repository ppa:yuezk/globalprotect-openconnect
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install globalprotect-openconnect
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!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`.
|
||||||
|
|
||||||
|
#### **Ubuntu 24.04 and later**
|
||||||
|
|
||||||
|
The `libwebkit2gtk-4.0-37` package was [removed](https://bugs.launchpad.net/ubuntu/+source/webkit2gtk/+bug/2061914) from its repo, before [the issue](https://github.com/yuezk/GlobalProtect-openconnect/issues/351) gets resolved, you need to install them manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wget http://launchpadlibrarian.net/704701349/libwebkit2gtk-4.0-37_2.43.3-1_amd64.deb
|
||||||
|
wget http://launchpadlibrarian.net/704701345/libjavascriptcoregtk-4.0-18_2.43.3-1_amd64.deb
|
||||||
|
|
||||||
|
sudo dpkg --install *.deb
|
||||||
|
```
|
||||||
|
|
||||||
|
And the latest package is not available in the PPA, you can follow the [Install from deb package](#install-from-deb-package) section to install the latest package.
|
||||||
|
|
||||||
|
#### **Ubuntu 18.04**
|
||||||
|
|
||||||
|
The latest package is not available in the PPA either, but you still needs to add the `ppa:yuezk/globalprotect-openconnect` repo beforehand to use the required `openconnect` package. Then you can follow the [Install from deb package](#install-from-deb-package) section to install the latest package.
|
||||||
|
|
||||||
|
#### Install from deb package
|
||||||
|
|
||||||
|
Download the latest deb package from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. Then install it with `apt`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install --fix-broken globalprotect-openconnect_*.deb
|
||||||
|
```
|
||||||
|
|
||||||
|
### Arch Linux / Manjaro
|
||||||
|
|
||||||
|
#### Install from AUR
|
||||||
|
|
||||||
|
Install from AUR: [globalprotect-openconnect-git](https://aur.archlinux.org/packages/globalprotect-openconnect-git/)
|
||||||
|
|
||||||
|
```
|
||||||
|
yay -S globalprotect-openconnect-git
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Install from package
|
||||||
|
|
||||||
|
Download the latest package from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. Then install it with `pacman`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo pacman -U globalprotect-openconnect-*.pkg.tar.zst
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fedora 38 and later / Fedora Rawhide
|
||||||
|
|
||||||
|
#### Install from COPR
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo dnf copr enable yuezk/globalprotect-openconnect
|
||||||
|
sudo dnf install globalprotect-openconnect
|
||||||
|
```
|
||||||
|
|
||||||
|
### openSUSE Leap 15.6 / openSUSE Tumbleweed
|
||||||
|
|
||||||
|
#### Install from OBS (openSUSE Build Service)
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
### Other RPM-based distributions
|
||||||
|
|
||||||
|
#### Install from RPM package
|
||||||
|
|
||||||
|
Download the latest RPM package from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo rpm -i globalprotect-openconnect-*.rpm
|
||||||
|
```
|
||||||
|
### Gentoo
|
||||||
|
|
||||||
|
Install from the ```rios``` or ```slonko``` overlays. Example using rios:
|
||||||
|
|
||||||
|
#### 1. Enable the overlay
|
||||||
|
```
|
||||||
|
sudo eselect repository enable rios
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Sync with the repository
|
||||||
|
|
||||||
|
- If you have eix installed, use it:
|
||||||
|
```
|
||||||
|
sudo eix-sync
|
||||||
|
```
|
||||||
|
- Otherwise, use:
|
||||||
|
```
|
||||||
|
sudo emerge --sync
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Install
|
||||||
|
|
||||||
|
```sudo emerge globalprotect-openconnect```
|
||||||
|
|
||||||
|
|
||||||
|
### Other distributions
|
||||||
|
|
||||||
|
- Install `openconnect >= 8.20`, `webkit2gtk`, `libsecret`, `libayatana-appindicator` or `libappindicator-gtk3`.
|
||||||
|
- Download `globalprotect-openconnect_${version}_${arch}.bin.tar.xz` from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page.
|
||||||
|
- Extract the tarball with `tar -xJf globalprotect-openconnect_${version}_${arch}.bin.tar.xz`.
|
||||||
|
- Run `sudo make install` to install the client.
|
||||||
|
|
||||||
|
## Build from source
|
||||||
|
|
||||||
|
You can also build the client from source, steps are as follows:
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- [Install Rust](https://www.rust-lang.org/tools/install)
|
||||||
|
- Install Tauri dependencies: https://tauri.app/v1/guides/getting-started/prerequisites/#setting-up-linux
|
||||||
|
- Install `perl`
|
||||||
|
- Install `openconnect >= 8.20` and `libopenconnect-dev` (or `openconnect-devel` on RPM-based distributions)
|
||||||
|
- Install `pkexec`, `gnome-keyring` (or `pam_kwallet` on KDE)
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
1. Download the source code tarball from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. Choose `globalprotect-openconnect-${version}.tar.gz`.
|
||||||
|
2. Extract the tarball with `tar -xzf globalprotect-openconnect-${version}.tar.gz`.
|
||||||
|
3. Enter the source directory and run `make build BUILD_FE=0` to build the client.
|
||||||
|
3. Run `sudo make install` to install the client. (Note, `DESTDIR` is not supported)
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
1. How to deal with error `Secure Storage not ready`
|
||||||
|
|
||||||
|
Try upgrade the client to `2.2.0` or later, which will use a file-based storage as a fallback.
|
||||||
|
|
||||||
|
You need to install the `gnome-keyring` package, and restart the system (See [#321](https://github.com/yuezk/GlobalProtect-openconnect/issues/321), [#316](https://github.com/yuezk/GlobalProtect-openconnect/issues/316)).
|
||||||
|
|
||||||
|
2. How to deal with error `(gpauth:18869): Gtk-WARNING **: 10:33:37.566: cannot open display:`
|
||||||
|
|
||||||
|
If you encounter this error when using the CLI version, try to run the command with `sudo -E` (See [#316](https://github.com/yuezk/GlobalProtect-openconnect/issues/316)).
|
||||||
|
|
||||||
|
## About Trial
|
||||||
|
|
||||||
|
The CLI version is always free, while the GUI version is paid. There are two trial modes for the GUI version:
|
||||||
|
|
||||||
|
1. 10-day trial: You can use the GUI stable release for 10 days after the installation.
|
||||||
|
2. 14-day trial: Each beta release has a fresh trial period (at most 14 days) after released.
|
||||||
|
|
||||||
## [License](./LICENSE)
|
## [License](./LICENSE)
|
||||||
|
|
||||||
GPLv3
|
GPLv3
|
||||||
|
28
apps/gpauth/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
[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",
|
||||||
|
"browser-auth",
|
||||||
|
] }
|
||||||
|
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
|
||||||
|
html-escape = "0.2.13"
|
||||||
|
webkit2gtk = "0.18.2"
|
||||||
|
tauri = { workspace = true, features = ["http-all"] }
|
||||||
|
compile-time.workspace = true
|
3
apps/gpauth/build.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
BIN
apps/gpauth/icons/128x128.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
apps/gpauth/icons/128x128@2x.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
apps/gpauth/icons/32x32.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
apps/gpauth/icons/icon.icns
Normal file
BIN
apps/gpauth/icons/icon.ico
Normal file
After Width: | Height: | Size: 44 KiB |
BIN
apps/gpauth/icons/icon.png
Normal file
After Width: | Height: | Size: 83 KiB |
11
apps/gpauth/index.html
Normal 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>
|
523
apps/gpauth/src/auth_window.rs
Normal file
@@ -0,0 +1,523 @@
|
|||||||
|
use std::{
|
||||||
|
rc::Rc,
|
||||||
|
sync::Arc,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::bail;
|
||||||
|
use gpapi::{
|
||||||
|
auth::SamlAuthData,
|
||||||
|
error::AuthDataParseError,
|
||||||
|
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));
|
||||||
|
if uri.starts_with("globalprotectcallback:") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
if !uri.starts_with("globalprotectcallback:") {
|
||||||
|
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) => bail!("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 async fn portal_prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<String> {
|
||||||
|
match prelogin(portal, gp_params).await? {
|
||||||
|
Prelogin::Saml(prelogin) => Ok(prelogin.saml_request().to_string()),
|
||||||
|
Prelogin::Standard(_) => bail!("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(Result<SamlAuthData, AuthDataParseError>) + 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(AuthDataParseError::Invalid))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_auth_data_from_html(html: &str) -> Result<SamlAuthData, AuthDataParseError> {
|
||||||
|
if html.contains("Temporarily Unavailable") {
|
||||||
|
info!("Found 'Temporarily Unavailable' in HTML, auth failed");
|
||||||
|
return Err(AuthDataParseError::Invalid);
|
||||||
|
}
|
||||||
|
|
||||||
|
SamlAuthData::from_html(html).or_else(|err| {
|
||||||
|
if let Some(gpcallback) = extract_gpcallback(html) {
|
||||||
|
info!("Found gpcallback from html...");
|
||||||
|
SamlAuthData::from_gpcallback(&gpcallback)
|
||||||
|
} else {
|
||||||
|
Err(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_gpcallback(html: &str) -> Option<String> {
|
||||||
|
let re = Regex::new(r#"globalprotectcallback:[^"]+"#).unwrap();
|
||||||
|
re.captures(html)
|
||||||
|
.and_then(|captures| captures.get(0))
|
||||||
|
.map(|m| html_escape::decode_html_entities(m.as_str()).to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_auth_data(main_resource: &WebResource, auth_result_tx: mpsc::UnboundedSender<AuthResult>) {
|
||||||
|
let Some(response) = main_resource.response() else {
|
||||||
|
info!("No response found in main resource");
|
||||||
|
send_auth_result(&auth_result_tx, Err(AuthDataError::Invalid));
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
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(|err| {
|
||||||
|
info!("Failed to read auth data from body: {}", 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...");
|
||||||
|
|
||||||
|
let is_acs_endpoint = main_resource.uri().map_or(false, |uri| uri.contains("/SAML20/SP/ACS"));
|
||||||
|
|
||||||
|
read_auth_data_from_body(main_resource, move |auth_result| {
|
||||||
|
// If the endpoint is `/SAML20/SP/ACS` and no auth data found in body, it should be considered as invalid
|
||||||
|
let auth_result = auth_result.map_err(|err| {
|
||||||
|
info!("Failed to read auth data from body: {}", err);
|
||||||
|
|
||||||
|
if !is_acs_endpoint && matches!(err, AuthDataParseError::NotFound) {
|
||||||
|
AuthDataError::NotFound
|
||||||
|
} else {
|
||||||
|
AuthDataError::Invalid
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_gpcallback_some() {
|
||||||
|
let html = r#"
|
||||||
|
<meta http-equiv="refresh" content="0; URL=globalprotectcallback:PGh0bWw+PCEtLSA8c">
|
||||||
|
<meta http-equiv="refresh" content="0; URL=globalprotectcallback:PGh0bWw+PCEtLSA8c">
|
||||||
|
"#;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
extract_gpcallback(html).as_deref(),
|
||||||
|
Some("globalprotectcallback:PGh0bWw+PCEtLSA8c")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_gpcallback_cas() {
|
||||||
|
let html = r#"
|
||||||
|
<meta http-equiv="refresh" content="0; URL=globalprotectcallback:cas-as=1&un=xyz@email.com&token=very_long_string">
|
||||||
|
"#;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
extract_gpcallback(html).as_deref(),
|
||||||
|
Some("globalprotectcallback:cas-as=1&un=xyz@email.com&token=very_long_string")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_gpcallback_none() {
|
||||||
|
let html = r#"
|
||||||
|
<meta http-equiv="refresh" content="0; URL=PGh0bWw+PCEtLSA8c">
|
||||||
|
"#;
|
||||||
|
|
||||||
|
assert_eq!(extract_gpcallback(html), None);
|
||||||
|
}
|
||||||
|
}
|
174
apps/gpauth/src/cli.rs
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
use clap::Parser;
|
||||||
|
use gpapi::{
|
||||||
|
auth::{SamlAuthData, SamlAuthResult},
|
||||||
|
clap::args::Os,
|
||||||
|
gp_params::{ClientOs, GpParams},
|
||||||
|
process::browser_authenticator::BrowserAuthenticator,
|
||||||
|
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)]
|
||||||
|
gateway: bool,
|
||||||
|
#[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,
|
||||||
|
#[arg(long)]
|
||||||
|
default_browser: 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?,
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.default_browser {
|
||||||
|
let browser_auth = BrowserAuthenticator::new(&saml_request);
|
||||||
|
browser_auth.authenticate()?;
|
||||||
|
|
||||||
|
info!("Please continue the authentication process in the default browser");
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
.is_gateway(self.gateway)
|
||||||
|
.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
@@ -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;
|
||||||
|
}
|
47
apps/gpauth/tauri.conf.json
Normal 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": []
|
||||||
|
}
|
||||||
|
}
|
24
apps/gpclient/Cargo.toml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[package]
|
||||||
|
name = "gpclient"
|
||||||
|
authors.workspace = true
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
common = { path = "../../crates/common" }
|
||||||
|
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
|
119
apps/gpclient/src/cli.rs
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
389
apps/gpclient/src/connect.rs
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
use std::{cell::RefCell, fs, sync::Arc};
|
||||||
|
|
||||||
|
use clap::Args;
|
||||||
|
use common::vpn_utils::find_csd_wrapper;
|
||||||
|
use gpapi::{
|
||||||
|
clap::args::Os,
|
||||||
|
credential::{Credential, PasswordCredential},
|
||||||
|
error::PortalError,
|
||||||
|
gateway::{gateway_login, GatewayLogin},
|
||||||
|
gp_params::{ClientOs, GpParams},
|
||||||
|
portal::{prelogin, retrieve_config, Prelogin},
|
||||||
|
process::{
|
||||||
|
auth_launcher::SamlAuthLauncher,
|
||||||
|
users::{get_non_root_user, get_user_by_name},
|
||||||
|
},
|
||||||
|
utils::{request::RequestIdentityError, shutdown_signal},
|
||||||
|
GP_USER_AGENT,
|
||||||
|
};
|
||||||
|
use inquire::{Password, PasswordDisplayMode, Select, Text};
|
||||||
|
use log::info;
|
||||||
|
use openconnect::Vpn;
|
||||||
|
use tokio::{io::AsyncReadExt, net::TcpListener};
|
||||||
|
|
||||||
|
use crate::{cli::SharedArgs, GP_CLIENT_LOCK_FILE, GP_CLIENT_PORT_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, help = "Connect the server as a gateway, instead of a portal")]
|
||||||
|
as_gateway: bool,
|
||||||
|
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "Use the default CSD wrapper to generate the HIP report and send it to the server"
|
||||||
|
)]
|
||||||
|
hip: bool,
|
||||||
|
|
||||||
|
#[arg(
|
||||||
|
short,
|
||||||
|
long,
|
||||||
|
help = "Use SSL client certificate file in pkcs#8 (.pem) or pkcs#12 (.p12, .pfx) format"
|
||||||
|
)]
|
||||||
|
certificate: Option<String>,
|
||||||
|
#[arg(short = 'k', long, help = "Use SSL private key file in pkcs#8 (.pem) format")]
|
||||||
|
sslkey: Option<String>,
|
||||||
|
#[arg(short = 'p', long, help = "The key passphrase of the private key")]
|
||||||
|
key_password: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long, help = "Same as the '--csd-user' option in the openconnect command")]
|
||||||
|
csd_user: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long, help = "Same as the '--csd-wrapper' option in the openconnect command")]
|
||||||
|
csd_wrapper: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long, default_value = "300", help = "Reconnection retry timeout in seconds")]
|
||||||
|
reconnect_timeout: u32,
|
||||||
|
#[arg(short, long, help = "Request MTU from server (legacy servers only)")]
|
||||||
|
mtu: Option<u32>,
|
||||||
|
#[arg(long, help = "Do not ask for IPv6 connectivity")]
|
||||||
|
disable_ipv6: bool,
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
#[arg(long, help = "Use the default browser to authenticate")]
|
||||||
|
default_browser: 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,
|
||||||
|
latest_key_password: RefCell<Option<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ConnectHandler<'a> {
|
||||||
|
pub(crate) fn new(args: &'a ConnectArgs, shared_args: &'a SharedArgs) -> Self {
|
||||||
|
Self {
|
||||||
|
args,
|
||||||
|
shared_args,
|
||||||
|
latest_key_password: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_gp_params(&self) -> GpParams {
|
||||||
|
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)
|
||||||
|
.certificate(self.args.certificate.clone())
|
||||||
|
.sslkey(self.args.sslkey.clone())
|
||||||
|
.key_password(self.latest_key_password.borrow().clone())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn handle(&self) -> anyhow::Result<()> {
|
||||||
|
self.latest_key_password.replace(self.args.key_password.clone());
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let Err(err) = self.handle_impl().await else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(root_cause) = err.root_cause().downcast_ref::<RequestIdentityError>() else {
|
||||||
|
return Err(err);
|
||||||
|
};
|
||||||
|
|
||||||
|
match root_cause {
|
||||||
|
RequestIdentityError::NoKey => {
|
||||||
|
eprintln!("ERROR: No private key found in the certificate file");
|
||||||
|
eprintln!("ERROR: Please provide the private key file using the `-k` option");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
RequestIdentityError::NoPassphrase(cert_type) | RequestIdentityError::DecryptError(cert_type) => {
|
||||||
|
// Decrypt the private key error, ask for the key password
|
||||||
|
let message = format!("Enter the {} passphrase:", cert_type);
|
||||||
|
let password = Password::new(&message)
|
||||||
|
.without_confirmation()
|
||||||
|
.with_display_mode(PasswordDisplayMode::Masked)
|
||||||
|
.prompt()?;
|
||||||
|
|
||||||
|
self.latest_key_password.replace(Some(password));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn handle_impl(&self) -> anyhow::Result<()> {
|
||||||
|
let server = self.args.server.as_str();
|
||||||
|
let as_gateway = self.args.as_gateway;
|
||||||
|
|
||||||
|
if as_gateway {
|
||||||
|
info!("Treating the server as a gateway");
|
||||||
|
return self.connect_gateway_with_prelogin(server).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Err(err) = self.connect_portal_with_prelogin(server).await else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("Failed to connect portal with prelogin: {}", err);
|
||||||
|
if err.root_cause().downcast_ref::<PortalError>().is_some() {
|
||||||
|
info!("Trying the gateway authentication workflow...");
|
||||||
|
self.connect_gateway_with_prelogin(server).await?;
|
||||||
|
|
||||||
|
eprintln!("\nNOTE: the server may be a gateway, not a portal.");
|
||||||
|
eprintln!("NOTE: try to use the `--as-gateway` option if you were authenticated twice.");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn connect_portal_with_prelogin(&self, portal: &str) -> anyhow::Result<()> {
|
||||||
|
let gp_params = self.build_gp_params();
|
||||||
|
|
||||||
|
let prelogin = prelogin(portal, &gp_params).await?;
|
||||||
|
|
||||||
|
let cred = self.obtain_credential(&prelogin, portal).await?;
|
||||||
|
let mut portal_config = retrieve_config(portal, &cred, &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 specified: {}", gateway))?,
|
||||||
|
None => {
|
||||||
|
portal_config.sort_gateways(prelogin.region());
|
||||||
|
let gateways = portal_config.gateways();
|
||||||
|
|
||||||
|
if gateways.len() > 1 {
|
||||||
|
let gateway = Select::new("Which gateway do you want to connect to?", gateways)
|
||||||
|
.with_vim_mode(true)
|
||||||
|
.prompt()?;
|
||||||
|
info!("Connecting to the selected gateway: {}", gateway);
|
||||||
|
gateway
|
||||||
|
} else {
|
||||||
|
info!("Connecting to the only available gateway: {}", gateways[0]);
|
||||||
|
gateways[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let gateway = selected_gateway.server();
|
||||||
|
let cred = portal_config.auth_cookie().into();
|
||||||
|
|
||||||
|
let cookie = match self.login_gateway(gateway, &cred, &gp_params).await {
|
||||||
|
Ok(cookie) => cookie,
|
||||||
|
Err(err) => {
|
||||||
|
info!("Gateway login failed: {}", err);
|
||||||
|
return self.connect_gateway_with_prelogin(gateway).await;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.connect_gateway(gateway, &cookie).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn connect_gateway_with_prelogin(&self, gateway: &str) -> anyhow::Result<()> {
|
||||||
|
info!("Performing the gateway authentication...");
|
||||||
|
|
||||||
|
let mut gp_params = self.build_gp_params();
|
||||||
|
gp_params.set_is_gateway(true);
|
||||||
|
|
||||||
|
let prelogin = prelogin(gateway, &gp_params).await?;
|
||||||
|
let cred = self.obtain_credential(&prelogin, gateway).await?;
|
||||||
|
|
||||||
|
let cookie = self.login_gateway(gateway, &cred, &gp_params).await?;
|
||||||
|
|
||||||
|
self.connect_gateway(gateway, &cookie).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn login_gateway(&self, gateway: &str, cred: &Credential, gp_params: &GpParams) -> anyhow::Result<String> {
|
||||||
|
let mut gp_params = gp_params.clone();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match gateway_login(gateway, cred, &gp_params).await? {
|
||||||
|
GatewayLogin::Cookie(cookie) => return Ok(cookie),
|
||||||
|
GatewayLogin::Mfa(message, input_str) => {
|
||||||
|
let otp = Text::new(&message).prompt()?;
|
||||||
|
gp_params.set_input_str(&input_str);
|
||||||
|
gp_params.set_otp(&otp);
|
||||||
|
|
||||||
|
info!("Retrying gateway login with MFA...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn connect_gateway(&self, gateway: &str, cookie: &str) -> anyhow::Result<()> {
|
||||||
|
let mtu = self.args.mtu.unwrap_or(0);
|
||||||
|
let csd_uid = get_csd_uid(&self.args.csd_user)?;
|
||||||
|
let csd_wrapper = if self.args.csd_wrapper.is_some() {
|
||||||
|
self.args.csd_wrapper.clone()
|
||||||
|
} else if self.args.hip {
|
||||||
|
find_csd_wrapper()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let vpn = Vpn::builder(gateway, cookie)
|
||||||
|
.script(self.args.script.clone())
|
||||||
|
.user_agent(self.args.user_agent.clone())
|
||||||
|
.certificate(self.args.certificate.clone())
|
||||||
|
.sslkey(self.args.sslkey.clone())
|
||||||
|
.key_password(self.latest_key_password.borrow().clone())
|
||||||
|
.csd_uid(csd_uid)
|
||||||
|
.csd_wrapper(csd_wrapper)
|
||||||
|
.reconnect_timeout(self.args.reconnect_timeout)
|
||||||
|
.mtu(mtu)
|
||||||
|
.disable_ipv6(self.args.disable_ipv6)
|
||||||
|
.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_credential(&self, prelogin: &Prelogin, server: &str) -> anyhow::Result<Credential> {
|
||||||
|
let is_gateway = prelogin.is_gateway();
|
||||||
|
|
||||||
|
match prelogin {
|
||||||
|
Prelogin::Saml(prelogin) => {
|
||||||
|
let use_default_browser = prelogin.support_default_browser() && self.args.default_browser;
|
||||||
|
|
||||||
|
let cred = SamlAuthLauncher::new(&self.args.server)
|
||||||
|
.gateway(is_gateway)
|
||||||
|
.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)
|
||||||
|
.default_browser(use_default_browser)
|
||||||
|
.launch()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if let Some(cred) = cred {
|
||||||
|
return Ok(cred);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !use_default_browser {
|
||||||
|
// This should never happen
|
||||||
|
unreachable!("SAML authentication failed without using the default browser");
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Waiting for the browser authentication to complete...");
|
||||||
|
wait_credentials().await
|
||||||
|
}
|
||||||
|
Prelogin::Standard(prelogin) => {
|
||||||
|
let prefix = if is_gateway { "Gateway" } else { "Portal" };
|
||||||
|
println!("{} ({}: {})", prelogin.auth_message(), prefix, server);
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn wait_credentials() -> anyhow::Result<Credential> {
|
||||||
|
// Start a local server to receive the browser authentication data
|
||||||
|
let listener = TcpListener::bind("127.0.0.1:0").await?;
|
||||||
|
let port = listener.local_addr()?.port();
|
||||||
|
|
||||||
|
// Write the port to a file
|
||||||
|
fs::write(GP_CLIENT_PORT_FILE, port.to_string())?;
|
||||||
|
|
||||||
|
info!("Listening authentication data on port {}", port);
|
||||||
|
let (mut socket, _) = listener.accept().await?;
|
||||||
|
|
||||||
|
info!("Received the browser authentication data from the socket");
|
||||||
|
let mut data = String::new();
|
||||||
|
socket.read_to_string(&mut data).await?;
|
||||||
|
|
||||||
|
// Remove the port file
|
||||||
|
fs::remove_file(GP_CLIENT_PORT_FILE)?;
|
||||||
|
|
||||||
|
Credential::from_gpcallback(&data)
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_csd_uid(csd_user: &Option<String>) -> anyhow::Result<u32> {
|
||||||
|
if let Some(csd_user) = csd_user {
|
||||||
|
get_user_by_name(csd_user).map(|user| user.uid())
|
||||||
|
} else {
|
||||||
|
get_non_root_user().map_or_else(|_| Ok(0), |user| Ok(user.uid()))
|
||||||
|
}
|
||||||
|
}
|
31
apps/gpclient/src/disconnect.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
129
apps/gpclient/src/launch_gui.rs
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
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;
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
|
||||||
|
use crate::GP_CLIENT_PORT_FILE;
|
||||||
|
|
||||||
|
#[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 _ = tokio::join!(feed_auth_data_gui(auth_data), feed_auth_data_cli(auth_data));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn feed_auth_data_gui(auth_data: &str) -> anyhow::Result<()> {
|
||||||
|
let service_endpoint = http_endpoint().await?;
|
||||||
|
|
||||||
|
reqwest::Client::default()
|
||||||
|
.post(format!("{}/auth-data", service_endpoint))
|
||||||
|
.body(auth_data.to_string())
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn feed_auth_data_cli(auth_data: &str) -> anyhow::Result<()> {
|
||||||
|
let port = tokio::fs::read_to_string(GP_CLIENT_PORT_FILE).await?;
|
||||||
|
let mut stream = tokio::net::TcpStream::connect(format!("127.0.0.1:{}", port.trim())).await?;
|
||||||
|
|
||||||
|
stream.write_all(auth_data.as_bytes()).await?;
|
||||||
|
|
||||||
|
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"))
|
||||||
|
}
|
12
apps/gpclient/src/main.rs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
mod cli;
|
||||||
|
mod connect;
|
||||||
|
mod disconnect;
|
||||||
|
mod launch_gui;
|
||||||
|
|
||||||
|
pub(crate) const GP_CLIENT_LOCK_FILE: &str = "/var/run/gpclient.lock";
|
||||||
|
pub(crate) const GP_CLIENT_PORT_FILE: &str = "/var/run/gpclient.port";
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
cli::run().await;
|
||||||
|
}
|
36
apps/gpgui-helper/.eslintrc.cjs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es2021: true,
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:react/recommended",
|
||||||
|
"plugin:react/jsx-runtime",
|
||||||
|
"plugin:react-hooks/recommended",
|
||||||
|
"prettier",
|
||||||
|
],
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
files: [".eslintrc.{js,cjs}"],
|
||||||
|
parserOptions: {
|
||||||
|
sourceType: "script",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: "latest",
|
||||||
|
sourceType: "module",
|
||||||
|
},
|
||||||
|
plugins: ["@typescript-eslint", "react"],
|
||||||
|
rules: {
|
||||||
|
"react-hooks/rules-of-hooks": "error",
|
||||||
|
"react-hooks/exhaustive-deps": "error",
|
||||||
|
"@typescript-eslint/no-unused-vars": "warn",
|
||||||
|
},
|
||||||
|
};
|
25
apps/gpgui-helper/.gitignore
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
.vite
|
0
apps/gpgui-helper/.prettierignore
Normal file
3
apps/gpgui-helper/.prettierrc
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 100
|
||||||
|
}
|
7
apps/gpgui-helper/README.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Tauri + React + Typescript
|
||||||
|
|
||||||
|
This template should help get you started developing with Tauri, React and Typescript in Vite.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
19
apps/gpgui-helper/index.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>GlobalProtect</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script>
|
||||||
|
/* workaround to webview font size auto scaling */
|
||||||
|
var htmlFontSize = getComputedStyle(document.documentElement).fontSize;
|
||||||
|
var ratio = parseInt(htmlFontSize, 10) / 16;
|
||||||
|
document.documentElement.style.fontSize = 16 / ratio + "px";
|
||||||
|
</script>
|
||||||
|
<div id="root" data-tauri-drag-region></div>
|
||||||
|
<script type="module" src="/src/pages/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
36
apps/gpgui-helper/package.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "gpgui",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"tauri": "tauri"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.11.1",
|
||||||
|
"@emotion/styled": "^11.11.0",
|
||||||
|
"@mui/icons-material": "^5.14.18",
|
||||||
|
"@mui/material": "^5.14.18",
|
||||||
|
"@tauri-apps/api": "^1.5.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tauri-apps/cli": "^1.5.6",
|
||||||
|
"@types/node": "^20.8.10",
|
||||||
|
"@types/react": "^18.2.15",
|
||||||
|
"@types/react-dom": "^18.2.7",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.12.0",
|
||||||
|
"@typescript-eslint/parser": "^6.12.0",
|
||||||
|
"@vitejs/plugin-react": "^4.0.3",
|
||||||
|
"eslint": "^8.54.0",
|
||||||
|
"eslint-config-prettier": "^9.0.0",
|
||||||
|
"eslint-plugin-react": "^7.33.2",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"prettier": "3.1.0",
|
||||||
|
"typescript": "^5.0.2",
|
||||||
|
"vite": "^4.5.3"
|
||||||
|
}
|
||||||
|
}
|
3094
apps/gpgui-helper/pnpm-lock.yaml
generated
Normal file
6
apps/gpgui-helper/public/tauri.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
|
||||||
|
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.5 KiB |
1
apps/gpgui-helper/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
4
apps/gpgui-helper/src-tauri/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
/target/
|
||||||
|
|
25
apps/gpgui-helper/src-tauri/Cargo.toml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
[package]
|
||||||
|
name = "gpgui-helper"
|
||||||
|
authors.workspace = true
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "1.5", features = [] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
gpapi = { path = "../../../crates/gpapi", features = ["tauri"] }
|
||||||
|
tauri = { workspace = true, features = ["window-start-dragging"] }
|
||||||
|
tokio.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
clap.workspace = true
|
||||||
|
compile-time.workspace = true
|
||||||
|
env_logger.workspace = true
|
||||||
|
futures-util.workspace = true
|
||||||
|
tempfile.workspace = true
|
||||||
|
reqwest = { workspace = true, features = ["stream"] }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
custom-protocol = ["tauri/custom-protocol"]
|
3
apps/gpgui-helper/src-tauri/build.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
BIN
apps/gpgui-helper/src-tauri/icons/128x128.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
apps/gpgui-helper/src-tauri/icons/128x128@2x.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
apps/gpgui-helper/src-tauri/icons/32x32.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
apps/gpgui-helper/src-tauri/icons/icon.icns
Normal file
BIN
apps/gpgui-helper/src-tauri/icons/icon.ico
Normal file
After Width: | Height: | Size: 44 KiB |
BIN
apps/gpgui-helper/src-tauri/icons/icon.png
Normal file
After Width: | Height: | Size: 83 KiB |
@@ -16,7 +16,7 @@
|
|||||||
viewBox="0 0 96 96"
|
viewBox="0 0 96 96"
|
||||||
style="enable-background:new 0 0 96 96;"
|
style="enable-background:new 0 0 96 96;"
|
||||||
xml:space="preserve"
|
xml:space="preserve"
|
||||||
sodipodi:docname="com.yuezk.qt.GPClient.svg"
|
sodipodi:docname="com.yuezk.qt.gpclient.svg"
|
||||||
inkscape:version="0.92.4 5da689c313, 2019-01-14"><metadata
|
inkscape:version="0.92.4 5da689c313, 2019-01-14"><metadata
|
||||||
id="metadata14"><rdf:RDF><cc:Work
|
id="metadata14"><rdf:RDF><cc:Work
|
||||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |
56
apps/gpgui-helper/src-tauri/src/app.rs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use gpapi::utils::window::WindowExt;
|
||||||
|
use log::info;
|
||||||
|
use tauri::Manager;
|
||||||
|
|
||||||
|
use crate::updater::{GuiUpdater, Installer, ProgressNotifier};
|
||||||
|
|
||||||
|
pub struct App {
|
||||||
|
api_key: Vec<u8>,
|
||||||
|
gui_version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub fn new(api_key: Vec<u8>, gui_version: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
api_key,
|
||||||
|
gui_version: gui_version.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(&self) -> anyhow::Result<()> {
|
||||||
|
let gui_version = self.gui_version.clone();
|
||||||
|
let api_key = self.api_key.clone();
|
||||||
|
|
||||||
|
tauri::Builder::default()
|
||||||
|
.setup(move |app| {
|
||||||
|
let win = app.get_window("main").expect("no main window");
|
||||||
|
win.hide_menu();
|
||||||
|
|
||||||
|
let notifier = ProgressNotifier::new(win.clone());
|
||||||
|
let installer = Installer::new(api_key);
|
||||||
|
let updater = Arc::new(GuiUpdater::new(gui_version, notifier, installer));
|
||||||
|
|
||||||
|
let win_clone = win.clone();
|
||||||
|
app.listen_global("app://update-done", move |_event| {
|
||||||
|
info!("Update done");
|
||||||
|
let _ = win_clone.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for the update event
|
||||||
|
win.listen("app://update", move |_event| {
|
||||||
|
let updater = Arc::clone(&updater);
|
||||||
|
tokio::spawn(async move { updater.update().await });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update the GUI on startup
|
||||||
|
win.trigger("app://update", None);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.run(tauri::generate_context!())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
56
apps/gpgui-helper/src-tauri/src/cli.rs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
use clap::Parser;
|
||||||
|
use gpapi::utils::base64;
|
||||||
|
use log::{info, LevelFilter};
|
||||||
|
|
||||||
|
use crate::app::App;
|
||||||
|
|
||||||
|
const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), " (", compile_time::date_str!(), ")");
|
||||||
|
const GP_API_KEY: &[u8; 32] = &[0; 32];
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(version = VERSION)]
|
||||||
|
struct Cli {
|
||||||
|
#[arg(long, help = "Read the API key from stdin")]
|
||||||
|
api_key_on_stdin: bool,
|
||||||
|
|
||||||
|
#[arg(long, default_value = env!("CARGO_PKG_VERSION"), help = "The version of the GUI")]
|
||||||
|
gui_version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cli {
|
||||||
|
fn run(&self) -> anyhow::Result<()> {
|
||||||
|
let api_key = self.read_api_key()?;
|
||||||
|
let app = App::new(api_key, &self.gui_version);
|
||||||
|
|
||||||
|
app.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_api_key(&self) -> anyhow::Result<Vec<u8>> {
|
||||||
|
if self.api_key_on_stdin {
|
||||||
|
let mut api_key = String::new();
|
||||||
|
std::io::stdin().read_line(&mut api_key)?;
|
||||||
|
|
||||||
|
let api_key = base64::decode_to_vec(api_key.trim())?;
|
||||||
|
|
||||||
|
Ok(api_key)
|
||||||
|
} else {
|
||||||
|
Ok(GP_API_KEY.to_vec())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_logger() {
|
||||||
|
env_logger::builder().filter_level(LevelFilter::Info).init();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run() {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
init_logger();
|
||||||
|
info!("gpgui-helper started: {}", VERSION);
|
||||||
|
|
||||||
|
if let Err(e) = cli.run() {
|
||||||
|
eprintln!("{}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
87
apps/gpgui-helper/src-tauri/src/downloader.rs
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
use anyhow::bail;
|
||||||
|
use futures_util::StreamExt;
|
||||||
|
use log::info;
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
|
type OnProgress = Box<dyn Fn(Option<f64>) + Send + Sync + 'static>;
|
||||||
|
|
||||||
|
pub struct FileDownloader<'a> {
|
||||||
|
url: &'a str,
|
||||||
|
on_progress: RwLock<Option<OnProgress>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> FileDownloader<'a> {
|
||||||
|
pub fn new(url: &'a str) -> Self {
|
||||||
|
Self {
|
||||||
|
url,
|
||||||
|
on_progress: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_progress<T>(&self, on_progress: T)
|
||||||
|
where
|
||||||
|
T: Fn(Option<f64>) + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
if let Ok(mut guard) = self.on_progress.try_write() {
|
||||||
|
*guard = Some(Box::new(on_progress));
|
||||||
|
} else {
|
||||||
|
info!("Failed to acquire on_progress lock");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn download(&self) -> anyhow::Result<NamedTempFile> {
|
||||||
|
let res = reqwest::get(self.url).await?.error_for_status()?;
|
||||||
|
let content_length = res.content_length().unwrap_or(0);
|
||||||
|
|
||||||
|
info!("Content length: {}", content_length);
|
||||||
|
|
||||||
|
let mut current_length = 0;
|
||||||
|
let mut stream = res.bytes_stream();
|
||||||
|
|
||||||
|
let mut file = NamedTempFile::new()?;
|
||||||
|
|
||||||
|
while let Some(item) = stream.next().await {
|
||||||
|
let chunk = item?;
|
||||||
|
let chunk_size = chunk.len() as u64;
|
||||||
|
|
||||||
|
file.write_all(&chunk)?;
|
||||||
|
|
||||||
|
current_length += chunk_size;
|
||||||
|
let progress = current_length as f64 / content_length as f64 * 100.0;
|
||||||
|
|
||||||
|
if let Some(on_progress) = &*self.on_progress.read().await {
|
||||||
|
let progress = if content_length > 0 { Some(progress) } else { None };
|
||||||
|
|
||||||
|
on_progress(progress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if content_length > 0 && current_length != content_length {
|
||||||
|
bail!("Download incomplete");
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Downloaded to: {:?}", file.path());
|
||||||
|
|
||||||
|
Ok(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ChecksumFetcher<'a> {
|
||||||
|
url: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ChecksumFetcher<'a> {
|
||||||
|
pub fn new(url: &'a str) -> Self {
|
||||||
|
Self { url }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn fetch(&self) -> anyhow::Result<String> {
|
||||||
|
let res = reqwest::get(self.url).await?.error_for_status()?;
|
||||||
|
let checksum = res.text().await?.trim().to_string();
|
||||||
|
|
||||||
|
Ok(checksum)
|
||||||
|
}
|
||||||
|
}
|
5
apps/gpgui-helper/src-tauri/src/lib.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
pub(crate) mod app;
|
||||||
|
pub(crate) mod downloader;
|
||||||
|
pub(crate) mod updater;
|
||||||
|
|
||||||
|
pub mod cli;
|
9
apps/gpgui-helper/src-tauri/src/main.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
use gpgui_helper::cli;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
cli::run()
|
||||||
|
}
|
147
apps/gpgui-helper/src-tauri/src/updater.rs
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use gpapi::{
|
||||||
|
service::request::UpdateGuiRequest,
|
||||||
|
utils::{checksum::verify_checksum, crypto::Crypto, endpoint::http_endpoint},
|
||||||
|
};
|
||||||
|
use log::{info, warn};
|
||||||
|
use tauri::{Manager, Window};
|
||||||
|
|
||||||
|
use crate::downloader::{ChecksumFetcher, FileDownloader};
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
const SNAPSHOT: &str = match option_env!("SNAPSHOT") {
|
||||||
|
Some(val) => val,
|
||||||
|
None => "false"
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct ProgressNotifier {
|
||||||
|
win: Window,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProgressNotifier {
|
||||||
|
pub fn new(win: Window) -> Self {
|
||||||
|
Self { win }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notify(&self, progress: Option<f64>) {
|
||||||
|
let _ = self.win.emit_all("app://update-progress", progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notify_error(&self) {
|
||||||
|
let _ = self.win.emit_all("app://update-error", ());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notify_done(&self) {
|
||||||
|
let _ = self.win.emit_and_trigger("app://update-done", ());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Installer {
|
||||||
|
crypto: Crypto,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Installer {
|
||||||
|
pub fn new(api_key: Vec<u8>) -> Self {
|
||||||
|
Self {
|
||||||
|
crypto: Crypto::new(api_key),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn install(&self, path: &str, checksum: &str) -> anyhow::Result<()> {
|
||||||
|
let service_endpoint = http_endpoint().await?;
|
||||||
|
|
||||||
|
let request = UpdateGuiRequest {
|
||||||
|
path: path.to_string(),
|
||||||
|
checksum: checksum.to_string(),
|
||||||
|
};
|
||||||
|
let payload = self.crypto.encrypt(&request)?;
|
||||||
|
|
||||||
|
reqwest::Client::default()
|
||||||
|
.post(format!("{}/update-gui", service_endpoint))
|
||||||
|
.body(payload)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GuiUpdater {
|
||||||
|
version: String,
|
||||||
|
notifier: Arc<ProgressNotifier>,
|
||||||
|
installer: Installer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GuiUpdater {
|
||||||
|
pub fn new(version: String, notifier: ProgressNotifier, installer: Installer) -> Self {
|
||||||
|
Self {
|
||||||
|
version,
|
||||||
|
notifier: Arc::new(notifier),
|
||||||
|
installer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update(&self) {
|
||||||
|
info!("Update GUI, version: {}", self.version);
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
let release_tag = "snapshot";
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
let release_tag = if SNAPSHOT == "true" {
|
||||||
|
String::from("snapshot")
|
||||||
|
} else {
|
||||||
|
format!("v{}", self.version)
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(target_arch = "x86_64")]
|
||||||
|
let arch = "x86_64";
|
||||||
|
#[cfg(target_arch = "aarch64")]
|
||||||
|
let arch = "aarch64";
|
||||||
|
|
||||||
|
let file_url = format!(
|
||||||
|
"https://github.com/yuezk/GlobalProtect-openconnect/releases/download/{}/gpgui_{}.bin.tar.xz",
|
||||||
|
release_tag, arch
|
||||||
|
);
|
||||||
|
let checksum_url = format!("{}.sha256", file_url);
|
||||||
|
|
||||||
|
info!("Downloading file: {}", file_url);
|
||||||
|
|
||||||
|
let dl = FileDownloader::new(&file_url);
|
||||||
|
let cf = ChecksumFetcher::new(&checksum_url);
|
||||||
|
let notifier = Arc::clone(&self.notifier);
|
||||||
|
|
||||||
|
dl.on_progress(move |progress| notifier.notify(progress));
|
||||||
|
|
||||||
|
let res = tokio::try_join!(dl.download(), cf.fetch());
|
||||||
|
|
||||||
|
let (file, checksum) = match res {
|
||||||
|
Ok((file, checksum)) => (file, checksum),
|
||||||
|
Err(err) => {
|
||||||
|
warn!("Download error: {}", err);
|
||||||
|
self.notifier.notify_error();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let path = file.into_temp_path();
|
||||||
|
let file_path = path.to_string_lossy();
|
||||||
|
|
||||||
|
if let Err(err) = verify_checksum(&file_path, &checksum) {
|
||||||
|
warn!("Checksum error: {}", err);
|
||||||
|
self.notifier.notify_error();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Checksum success");
|
||||||
|
|
||||||
|
if let Err(err) = self.installer.install(&file_path, &checksum).await {
|
||||||
|
warn!("Install error: {}", err);
|
||||||
|
self.notifier.notify_error();
|
||||||
|
} else {
|
||||||
|
info!("Install success");
|
||||||
|
self.notifier.notify_done();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
52
apps/gpgui-helper/src-tauri/tauri.conf.json
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
|
"build": {
|
||||||
|
"beforeDevCommand": "pnpm dev",
|
||||||
|
"beforeBuildCommand": "pnpm build",
|
||||||
|
"devPath": "http://localhost:1421",
|
||||||
|
"distDir": "../dist",
|
||||||
|
"withGlobalTauri": false
|
||||||
|
},
|
||||||
|
"package": {
|
||||||
|
"productName": "gpgui-helper"
|
||||||
|
},
|
||||||
|
"tauri": {
|
||||||
|
"allowlist": {
|
||||||
|
"all": false,
|
||||||
|
"window": {
|
||||||
|
"all": false,
|
||||||
|
"startDragging": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": false,
|
||||||
|
"targets": "deb",
|
||||||
|
"identifier": "com.yuezk.gpgui-helper",
|
||||||
|
"icon": [
|
||||||
|
"icons/32x32.png",
|
||||||
|
"icons/128x128.png",
|
||||||
|
"icons/128x128@2x.png",
|
||||||
|
"icons/icon.icns",
|
||||||
|
"icons/icon.ico"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"csp": null
|
||||||
|
},
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"title": "GlobalProtect GUI Helper",
|
||||||
|
"center": true,
|
||||||
|
"resizable": true,
|
||||||
|
"width": 500,
|
||||||
|
"height": 100,
|
||||||
|
"minWidth": 500,
|
||||||
|
"minHeight": 100,
|
||||||
|
"maxWidth": 500,
|
||||||
|
"maxHeight": 100,
|
||||||
|
"label": "main",
|
||||||
|
"decorations": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
99
apps/gpgui-helper/src/assets/icon.svg
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
<?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>
|
After Width: | Height: | Size: 6.7 KiB |
131
apps/gpgui-helper/src/components/App/App.tsx
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import { Box, Button, CssBaseline, LinearProgress, Typography } from "@mui/material";
|
||||||
|
import { appWindow } from "@tauri-apps/api/window";
|
||||||
|
import logo from "../../assets/icon.svg";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
function useUpdateProgress() {
|
||||||
|
const [progress, setProgress] = useState<number | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unlisten = appWindow.listen("app://update-progress", (event) => {
|
||||||
|
setProgress(event.payload as number);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unlisten.then((unlisten) => unlisten());
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const [error, setError] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unlisten = appWindow.listen("app://update-error", () => {
|
||||||
|
setError(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unlisten.then((unlisten) => unlisten());
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleRetry = () => {
|
||||||
|
setError(false);
|
||||||
|
appWindow.emit("app://update");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CssBaseline />
|
||||||
|
<Box
|
||||||
|
sx={{ position: "absolute", inset: 0 }}
|
||||||
|
display="flex"
|
||||||
|
alignItems="center"
|
||||||
|
px={2}
|
||||||
|
data-tauri-drag-region
|
||||||
|
>
|
||||||
|
<Box display="flex" alignItems="center" flex="1" data-tauri-drag-region>
|
||||||
|
<Box
|
||||||
|
component="img"
|
||||||
|
src={logo}
|
||||||
|
alt="logo"
|
||||||
|
sx={{ width: "4rem", height: "4rem" }}
|
||||||
|
data-tauri-drag-region
|
||||||
|
/>
|
||||||
|
<Box flex={1} ml={2}>
|
||||||
|
{error ? <DownloadFailed onRetry={handleRetry} /> : <DownloadIndicator />}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DownloadIndicator() {
|
||||||
|
const progress = useUpdateProgress();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Typography variant="h1" fontSize="1rem" data-tauri-drag-region>
|
||||||
|
Updating the GUI components...
|
||||||
|
</Typography>
|
||||||
|
<Box mt={1}>
|
||||||
|
<LinearProgressWithLabel value={progress} />
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DownloadFailed({ onRetry }: { onRetry: () => void }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Typography variant="h1" fontSize="1rem" data-tauri-drag-region>
|
||||||
|
Failed to update the GUI components.
|
||||||
|
</Typography>
|
||||||
|
<Box mt={1} data-tauri-drag-region>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
size="small"
|
||||||
|
onClick={onRetry}
|
||||||
|
sx={{
|
||||||
|
textTransform: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Retry
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LinearProgressWithLabel(props: { value: number | null }) {
|
||||||
|
const { value } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||||
|
<Box flex="1">
|
||||||
|
<LinearProgress
|
||||||
|
variant={value === null ? "indeterminate" : "determinate"}
|
||||||
|
value={value ?? 0}
|
||||||
|
sx={{
|
||||||
|
py: 1.2,
|
||||||
|
".MuiLinearProgress-bar": {
|
||||||
|
transition: "none",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
{value !== null && (
|
||||||
|
<Box sx={{ minWidth: 35, textAlign: "right", ml: 1 }}>
|
||||||
|
<Typography variant="body2" color="text.secondary">{`${Math.round(value)}%`}</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
10
apps/gpgui-helper/src/components/App/styles.css
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
html,
|
||||||
|
body,
|
||||||
|
#root {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
cursor: default;
|
||||||
|
}
|
6
apps/gpgui-helper/src/pages/main.tsx
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { createRoot } from "react-dom/client"
|
||||||
|
import App from "../components/App/App";
|
||||||
|
|
||||||
|
const rootApp = createRoot(document.getElementById('root') as HTMLElement);
|
||||||
|
|
||||||
|
rootApp.render(<App />);
|