Compare commits

...

227 Commits

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

* add configuration file for gpservice

* Disable the UI configuration for extra args

* remove VERSION_SUFFIX

* remove ppa-publish.sh

* Use Git repo as the source for PKGBUILD

* remove VERSION_SUFFIX

* Use Git repo as the source for PKGBUILD

* add .install for PKGBUILD

* add configuration file

* Fix cmake

* Fix cmake

* Disable snap job

* update AUR packaging

* Disable the UI configuration for extra args

* improve packaging script

* update README.md

* restart gpservice after package upgrading
2022-05-09 21:58:58 +08:00
Kevin Yue
04d180e11a Release 1.4.2 2022-05-06 22:18:19 +08:00
Kevin Yue
6d3b127569 Updated VERSION, Bumped 1.4.1 –> 1.4.2 2022-05-06 22:17:49 +08:00
Erik Lindblad
e72b25e415 Clear SSL_OP_LEGACY_SERVER_CONNECT (#146)
Co-authored-by: Erik Lindblad <erili@spotify.com>
2022-05-06 21:26:27 +08:00
Kevin Yue
37a511c24d Release 1.4.1 2022-03-03 21:58:59 +08:00
Kevin Yue
ad7db36c92 Updated VERSION, Bumped 1.4.0 –> 1.4.1 2022-03-03 21:58:27 +08:00
Kevin Yue
11dc5920ef print the gpservice logs 2022-03-03 21:30:33 +08:00
Kevin Yue
e6383916c7 update AUR packaging 2022-03-02 22:11:47 +08:00
Kevin Yue
1d9d928b26 update AUR packaging 2022-03-02 22:06:26 +08:00
Kevin Yue
c02ad5d46d Release 1.4.0 2022-03-02 21:34:19 +08:00
Kevin Yue
2319c7c49c Updated VERSION, Bumped 1.3.4 –> 1.4.0 2022-03-02 21:28:02 +08:00
David Cohen
e0c2c14dc3 Fix gpservice after openconnect v8.20 (#124) 2022-03-01 15:41:29 +08:00
Kevin Yue
8f27c92e7b Add 2FA support (#112) 2021-12-20 22:20:02 +08:00
Karolin Varner
9d6ec84c14 Add a scripting mode to GPClient (#110) 2021-12-20 18:46:16 +08:00
Kevin Yue
dd81ed9519 Stop saving credentials (#111) 2021-12-20 18:43:37 +08:00
Kevin Yue
32bd713965 update CI 2021-12-20 18:32:18 +08:00
Kevin Yue
ba92517141 add editorconfig 2021-12-20 18:31:56 +08:00
Kevin Yue
0e4e082594 Update README.md 2021-11-30 16:42:04 +08:00
Kevin Yue
3e590cab7b Update README.md 2021-11-30 10:44:38 +08:00
Aloïs de Gouvello
3e0e4cff12 Add a run entry (#108)
Fix #107
2021-11-30 10:43:56 +08:00
Kevin Yue
692df2f2c5 update the installation instruction of Arch Linux 2021-11-01 17:12:15 +08:00
Kevin Yue
f2b9ffddde Update README.md 2021-10-30 09:41:32 +08:00
Kevin Yue
ca38925066 Update README.md 2021-10-24 17:26:02 +08:00
Kevin Yue
8591dd7e81 Update README.md 2021-10-24 16:43:01 +08:00
Kevin Yue
b07880930e update AUR packaging 2021-10-24 13:09:44 +08:00
Kevin Yue
fceb80e10e update CI scripts 2021-10-24 12:28:18 +08:00
Kevin Yue
d802c56d8f Release 1.3.4 2021-10-24 12:13:24 +08:00
Kevin Yue
386f08d0e8 Updated VERSION, Bumped 1.3.3 –> 1.3.4 2021-10-24 12:13:15 +08:00
Kevin Yue
9e7fb17bd3 update packaging (#100) 2021-10-24 12:11:54 +08:00
Kevin Yue
36d9753008 shorten the sponsor links 2021-10-15 19:21:35 +08:00
Kevin Yue
e5b3df9cda Update README.md 2021-10-14 19:17:47 +08:00
Kevin Yue
0dd705d0c0 add sponsor links 2021-10-14 19:09:39 +08:00
Antoine Allard
ce2360be61 Adding application logs location in the README (#95)
Co-authored-by: ALLARD Antoine <Antoine.ALLARD@murex.com>
2021-09-24 17:33:36 +08:00
Kevin Yue
b5b7033eee Update README.md 2021-09-22 23:34:52 +08:00
Kevin Yue
9e7db4eb86 improve the doc 2021-09-22 11:17:37 +08:00
Kevin Yue
bc07e3d496 Add snap packaging (#93)
* snapcraft init

* update packaging

* update packaging

* update packaging

* update packaging

* update packaging

* update packaging

* snap worked

* fix locale warning

* polish code

* update metainfo

* update icon

* update icon

* update message
2021-09-20 20:48:24 +08:00
Kevin Yue
452fe2f189 update doc 2021-09-19 15:40:20 +08:00
Kevin Yue
8a65099ca7 Migrate to cmake and refine the code structure (#92)
* migrate to cmake

* move the 3rd party libs

* organize 3rdparty

* update the 3rd party version

* refine the CMakeLists.txt

* update install command

* update install command

* update install command

* update install command

* update dependency

* update the dependency

* update the dependency

* remove CPM.cmake

* remove QtCreator project file

* update cmake file

* improve cmake file

* add cmakew

* use wget

* remove echo

* update the doc

* remove the screenshot

* update the doc

* update the install steps

* check the openconnect version

* update the doc

* update install scripts

* fix install scripts

* improve message

* improve message

* improve install scripts

* improve the version check

* improve the version check

* improve install script

* add version

* organize includes

* add version bump

* update CI

* update CI

* add the release flag

* update message
2021-09-19 14:32:12 +08:00
Kevin Yue
5c97b2df7a QStringView -> QString 2021-09-14 00:32:05 +08:00
Kevin Yue
0d4485d754 Update README.md 2021-09-10 22:49:54 +08:00
Kevin Yue
98e641e99d release 1.3.3 2021-09-04 19:03:01 +08:00
Kevin Yue
6fa77cdbd2 Fix the clientos param (#87)
* fix the clientos param

* fix the clientos param
2021-09-04 18:56:17 +08:00
Kevin Yue
64e6487e7e release 1.3.2 2021-09-02 21:11:47 +08:00
Kevin Yue
e8b2c1606f Add default value to client os (#86)
* add default value for clientos

* update CI

* update icon format

* change the icon format
2021-09-02 21:08:56 +08:00
Kevin Yue
84f1480653 release 1.3.1 2021-08-31 20:54:04 +08:00
Kevin Yue
3175855122 add rpm packaging (#83) 2021-08-31 20:52:08 +08:00
Kevin Yue
fa8b5c1528 Update CI scripts 2021-08-29 20:06:13 +08:00
Kevin Yue
7b9942c7e6 Update README.md 2021-08-26 00:42:25 +08:00
Kevin Yue
011a1a0dec Update README.md 2021-08-26 00:39:13 +08:00
Kevin Yue
4a53033023 [ci] use action-automatic-releases 2021-08-23 08:53:41 +08:00
Kevin Yue
9c6ea1c4b5 [ci] replace artifacts 2021-08-23 08:32:12 +08:00
Kevin Yue
3369ad4c1d [ci] update release action 2021-08-23 08:13:01 +08:00
Kevin Yue
25c9f2291a Update pre-release.yml 2021-08-23 01:35:12 +08:00
Kevin Yue
bba3bc7e4f [ci] improve action script 2021-08-23 01:04:17 +08:00
Kevin Yue
b12b692090 [ci] update action script 2021-08-23 00:30:01 +08:00
Kevin Yue
1300a0cc43 [ci] install qt 2021-08-22 23:56:05 +08:00
Kevin Yue
165080b476 [ci] build debian package 2021-08-22 23:46:20 +08:00
Kevin Yue
d6af8a1598 [ci] Update the changlog 2021-08-22 22:41:47 +08:00
Kevin Yue
eef92b1d31 Update action script 2021-08-22 21:07:52 +08:00
Kevin Yue
946ead24a4 Bump the changelog 2021-08-22 20:05:59 +08:00
Kevin Yue
39e57c8598 Add version suffix 2021-08-22 19:30:34 +08:00
Kevin Yue
4e2e423c27 Update the branch 2021-08-22 18:39:31 +08:00
Kevin Yue
732a62f1ee Add pre-release action 2021-08-22 18:34:56 +08:00
Kevin Yue
9f9444a72b Display error when OpenConnect was not found (#81) 2021-08-21 19:32:13 +08:00
Kevin Yue
6352e1fb2b Make the clientos configurable and improve Reset Settings (#80)
* Set the gateway

* Make clientos configurable

* Update readme.md

* Update README.md
2021-08-21 18:44:16 +08:00
Kevin Yue
42cae3ff26 Port the splitCommand method (#79) 2021-08-19 19:10:05 +08:00
Kevin Yue
53c8572cf6 Update main.yml 2021-08-19 18:42:26 +08:00
Kevin Yue
3f6467321f Update main.yml 2021-08-19 18:33:01 +08:00
Kevin Yue
563ec48c8c Update main.yml 2021-08-19 18:26:05 +08:00
Kevin Yue
3787ae164c Update main.yml 2021-08-19 18:24:30 +08:00
Kevin Yue
04a24c34e8 Update future plan 2021-08-18 16:16:52 +08:00
Kevin Yue
fe68248b1f Add future plan 2021-08-18 16:03:08 +08:00
Kevin Yue
47013033ec Release 1.3.0 2021-08-15 20:44:18 +08:00
Kevin Yue
05fb9a26bd Update links 2021-08-15 20:40:13 +08:00
Kevin Yue
96962f957c Add links (#77) 2021-08-15 17:36:51 +08:00
Kevin Yue
b4f9cfae67 Support custom parameters (#76)
* Add the setting icon

* Add support for custom parameters

* Ignore auto generated files

* Update README.md
2021-08-15 12:47:02 +08:00
Jan Vlug
c8942984a8 Added missing dependency for Fedora 34 (#75)
* Added missing dependency for Fedora 34.

* Removed architecture specification.

* Whitespace.
2021-08-08 19:17:15 +08:00
Darío Cutillas
3907827d0e Add pre-requisites for Fedora (#73) 2021-08-03 22:25:09 +08:00
Kevin Yue
f089996cdc Release 1.2.9 2021-08-03 22:20:36 +08:00
Tom Almeida
260b557238 Properly handle gateway responses that return Javascript errors (#74)
This was previously causing a segmentation fault, as the gateway
response would not be valid XML to be parsed.

Closes: #38, #71
2021-08-03 22:17:50 +08:00
Robert M Flight
3495dbfe18 Remove qt5 default (#68)
* removing qt5-default

as of ubuntu 21.04 it doesn't exist anymore

* update readme

based on ubuntu 21, and actually installing the deb for ubuntu

* missed the other package
2021-07-04 18:31:28 +08:00
Matt McHenry
cdf193024c README.md: add section for NixOS (#65) 2021-06-18 21:35:28 +08:00
Kevin Yue
76de070d78 Update publish.yml 2021-05-07 13:42:33 +08:00
Kevin Yue
420ae27888 Update publish.yml 2021-05-07 13:34:22 +08:00
Tobias Sarnowski
6a347746cc Mark for inclusion in aarch64 archlinux repository (#58)
This version of the software was tested on current Manjaro as of this
commit date on a Pinebook Pro with the official aarch64 KDE image.
2021-05-06 15:19:07 +08:00
Kevin Yue
624babb380 Update publish.yml 2021-04-25 21:24:08 +08:00
Kevin Yue
511b20fdcd Update publish.yml 2021-04-25 10:36:44 +08:00
Kevin Yue
abe33c7407 Update publish.yml 2021-04-25 10:29:03 +08:00
Kevin Yue
99a82c8641 Update publish.yml 2021-04-24 23:08:10 +08:00
Kevin Yue
e5d0acad3c Update publish.yml 2021-04-24 22:44:24 +08:00
Kevin Yue
38a1eded19 Release 1.2.8 2021-04-24 22:21:29 +08:00
Kevin Yue
3e23e7eaae Update publish action 2021-04-24 22:20:33 +08:00
Kevin Yue
cf46848e63 Merge branch 'add-github-actions' 2021-04-24 22:15:42 +08:00
Kevin Yue
2e826201d2 Create publish.yml 2021-04-24 22:13:48 +08:00
Kevin Yue
adba408dc3 Add PKGBUILD.template 2021-04-24 21:13:33 +08:00
Kevin Yue
5d613369ee Create main.yml 2021-04-24 19:02:07 +08:00
hakasapl
ebd3de6f63 added startupwmclass to desktop file (#51) 2021-04-15 16:15:13 +08:00
Michaël Arnauts
266ab65892 Update README.md (#45)
Add qttools5-dev as required package for Ubuntu.
2021-03-03 14:31:23 +08:00
Kevin Yue
ccaf93ec31 Release 1.2.7 2020-12-29 21:32:04 +08:00
Mike Gelfand
e08d7d7c4d Don't quit the app when closing the main window (#40)
Fixes: #15
2020-12-24 10:40:35 +08:00
Raphael Sant'Anna
c14a6ad1d2 Fix parsing of Gateways and Priority Rules (#35)
* Fix gateways and priority rules parsing

* Removing comment with dead code

Co-authored-by: Raphael Sant'Anna <raphael.santanna@exame.com>
2020-11-01 09:55:27 +08:00
Kevin Yue
d91fad089f Release 1.2.5 2020-07-19 18:32:34 +08:00
Kevin Yue
2c1036ff10 Add log entry for the gateway response 2020-07-19 18:26:08 +08:00
Kevin Yue
d5f9283b93 Skip the ssl certificate verifying (#26) 2020-07-19 17:26:54 +08:00
Kevin Yue
fe7b96ce9b Update changelog 2020-07-09 10:22:38 +08:00
Kevin Yue
790865c060 Update debian scripts 2020-06-09 22:20:35 +08:00
Kevin Yue
7f056c98ce Remove auto generated files 2020-06-07 22:05:01 +08:00
Amit Joshi
70816a9600 Add debian packaging instructions and code (#19)
* First cut of creating a debian package

* Update version

* Add notes to build debian package

Co-authored-by: amit.joshi@markit.com <amit.joshi@markit.com>
2020-06-01 13:36:43 +08:00
Kevin Yue
337a94efcd Release 1.2.4 2020-05-31 12:15:06 +08:00
Kevin Yue
cf34f9f70f Improve the authentication workflow (#18) 2020-05-31 12:14:56 +08:00
Kevin Yue
3a790cdc63 Release v1.2.3 2020-05-30 23:00:38 +08:00
Kevin Yue
73925fd1e2 Add more logs to debug the portal login (#16) 2020-05-30 22:58:58 +08:00
Kevin Yue
e12613d9a4 Try to fix saml hang (#14)
* Try to fix saml login hang

* Add more logs

* Add version number
2020-05-30 11:22:05 +08:00
Kevin Yue
86ad51b0ad Add extra parameters to prelogin request 2020-05-29 23:44:02 +08:00
Kevin Yue
1e2322b938 Fix saml login for portal-userauthcookie (#12) 2020-05-29 23:38:51 +08:00
Kevin Yue
4313b9d0e7 Update README.md 2020-05-25 10:05:11 +08:00
140 changed files with 10766 additions and 3405 deletions

62
.devcontainer/Dockerfile Normal file
View File

@@ -0,0 +1,62 @@
FROM ubuntu:18.04
ARG USERNAME=vscode
ARG USER_UID=1000
ARG USER_GID=$USER_UID
ENV RUSTUP_HOME=/usr/local/rustup \
CARGO_HOME=/usr/local/cargo \
PATH=/usr/local/cargo/bin:$PATH \
RUST_VERSION=1.75.0
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends \
sudo \
ca-certificates \
curl \
gnupg \
git \
less \
software-properties-common \
# Tauri dependencies
libwebkit2gtk-4.0-dev build-essential wget libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev; \
# Install openconnect
add-apt-repository ppa:yuezk/globalprotect-openconnect; \
apt-get update; \
apt-get install -y openconnect libopenconnect-dev; \
# Create a non-root user
groupadd --gid $USER_GID $USERNAME; \
useradd --uid $USER_UID --gid $USER_GID -m $USERNAME; \
echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME; \
chmod 0440 /etc/sudoers.d/$USERNAME; \
# Install Node.js
mkdir -p /etc/apt/keyrings; \
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg; \
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_16.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list; \
apt-get update; \
apt-get install -y nodejs; \
corepack enable; \
# Install diff-so-fancy
npm install -g diff-so-fancy; \
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain $RUST_VERSION; \
chown -R $USERNAME:$USERNAME $RUSTUP_HOME $CARGO_HOME; \
rustup --version; \
cargo --version; \
rustc --version
USER $USERNAME
# Install Oh My Zsh
RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/v1.1.5/zsh-in-docker.sh)" -- \
-t https://github.com/denysdovhan/spaceship-prompt \
-a 'SPACESHIP_PROMPT_ADD_NEWLINE="false"' \
-a 'SPACESHIP_PROMPT_SEPARATE_LINE="false"' \
-p git \
-p https://github.com/zsh-users/zsh-autosuggestions \
-p https://github.com/zsh-users/zsh-completions; \
# Change the default shell
sudo chsh -s /bin/zsh $USERNAME; \
# Change the XTERM to xterm-256color
sed -i 's/TERM=xterm/TERM=xterm-256color/g' $HOME/.zshrc;

View File

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

9
.editorconfig Normal file
View File

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

2
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
ko_fi: yuezk
custom: ["https://buymeacoffee.com/yuezk", "https://paypal.me/zongkun"]

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

@@ -0,0 +1,308 @@
name: Build GPGUI
on:
push:
paths-ignore:
- LICENSE
- "*.md"
- .vscode
- .devcontainer
branches:
- main
tags:
- latest
- v*.*.*
jobs:
# Include arm64 if ref is a tag
setup-matrix:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Set up matrix
id: set-matrix
run: |
if [[ "${{ github.ref }}" == "refs/tags/"* ]]; then
echo "matrix=[\"amd64\", \"arm64\"]" >> $GITHUB_OUTPUT
else
echo "matrix=[\"amd64\"]" >> $GITHUB_OUTPUT
fi
build-fe:
runs-on: ubuntu-latest
steps:
- name: Checkout gpgui repo
uses: actions/checkout@v3
with:
token: ${{ secrets.GH_PAT }}
repository: yuezk/gpgui
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 18
- uses: pnpm/action-setup@v2
with:
version: 8
- name: Install dependencies
run: |
cd app
pnpm install
- name: Build
run: |
cd app
pnpm run build
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: gpgui-fe
path: app/dist
build-tauri-amd64:
needs: [build-fe]
runs-on: ubuntu-latest
steps:
- name: Checkout gpgui repo
uses: actions/checkout@v3
with:
token: ${{ secrets.GH_PAT }}
repository: yuezk/gpgui
path: gpgui
- name: Checkout gp repo
uses: actions/checkout@v3
with:
token: ${{ secrets.GH_PAT }}
repository: yuezk/GlobalProtect-openconnect
path: gp
- name: Download gpgui-fe artifact
uses: actions/download-artifact@v3
with:
name: gpgui-fe
path: gpgui/app/dist
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Build Tauri in Docker
run: |
docker run \
--rm \
-v $(pwd):/${{ github.workspace }} \
-w ${{ github.workspace }} \
-e CI=true \
yuezk/gpdev:main \
"./gpgui/scripts/build.sh"
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: artifact-amd64-tauri
path: |
gpgui/.tmp/artifact
build-tauri-arm64:
if: startsWith(github.ref, 'refs/tags/')
needs: [build-fe]
runs-on: self-hosted
steps:
- name: Checkout gpgui repo
uses: actions/checkout@v3
with:
token: ${{ secrets.GH_PAT }}
repository: yuezk/gpgui
path: gpgui
- name: Checkout gp repo
uses: actions/checkout@v3
with:
token: ${{ secrets.GH_PAT }}
repository: yuezk/GlobalProtect-openconnect
path: gp
- name: Download gpgui-fe artifact
uses: actions/download-artifact@v3
with:
name: gpgui-fe
path: gpgui/app/dist
- name: Build Tauri
run: |
./gpgui/scripts/build.sh
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: artifact-arm64-tauri
path: |
gpgui/.tmp/artifact
package-tarball:
needs: [build-tauri-amd64, build-tauri-arm64]
runs-on: ubuntu-latest
steps:
- name: Checkout gpgui repo
uses: actions/checkout@v3
with:
token: ${{ secrets.GH_PAT }}
repository: yuezk/gpgui
path: gpgui
- name: Download artifact-amd64-tauri
uses: actions/download-artifact@v3
with:
name: artifact-amd64-tauri
path: gpgui/.tmp/artifact
- name: Download artifact-arm64-tauri
uses: actions/download-artifact@v3
with:
name: artifact-arm64-tauri
path: gpgui/.tmp/artifact
- name: Create tarball
run: |
./gpgui/scripts/build-tarball.sh
- name: Upload tarball
uses: actions/upload-artifact@v3
with:
name: artifact-tarball
path: |
gpgui/.tmp/tarball/*.tar.gz
package-rpm:
needs: [setup-matrix, package-tarball]
runs-on: ubuntu-latest
strategy:
matrix:
arch: ${{ fromJson(needs.setup-matrix.outputs.matrix) }}
steps:
- name: Checkout gpgui repo
uses: actions/checkout@v3
with:
token: ${{ secrets.GH_PAT }}
repository: yuezk/gpgui
path: gpgui
- name: Download package tarball
uses: actions/download-artifact@v3
with:
name: artifact-tarball
path: gpgui/.tmp/artifact
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: ${{ matrix.arch }}
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Create RPM package
run: |
docker run \
--rm \
-v $(pwd):/${{ github.workspace }} \
-w ${{ github.workspace }} \
--platform linux/${{ matrix.arch }} \
yuezk/gpdev:rpm-builder \
"./gpgui/scripts/build-rpm.sh"
- name: Upload rpm artifacts
uses: actions/upload-artifact@v3
with:
name: artifact-${{ matrix.arch }}-rpm
path: |
gpgui/.tmp/artifact/*.rpm
package-pkgbuild:
needs: [setup-matrix, build-tauri-amd64, build-tauri-arm64]
runs-on: ubuntu-latest
strategy:
matrix:
arch: ${{ fromJson(needs.setup-matrix.outputs.matrix) }}
steps:
- name: Checkout gpgui repo
uses: actions/checkout@v3
with:
token: ${{ secrets.GH_PAT }}
repository: yuezk/gpgui
path: gpgui
- name: Download artifact-${{ matrix.arch }}
uses: actions/download-artifact@v3
with:
name: artifact-${{ matrix.arch }}-tauri
path: gpgui/.tmp/artifact
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: ${{ matrix.arch }}
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Generate PKGBUILD
run: |
export CI_ARCH=${{ matrix.arch }}
./gpgui/scripts/generate-pkgbuild.sh
- name: Build PKGBUILD package
run: |
# Build package
docker run \
--rm \
-v $(pwd)/gpgui/.tmp/pkgbuild:/pkgbuild \
--platform linux/${{ matrix.arch }} \
yuezk/gpdev:pkgbuild
- name: Upload pkgbuild artifacts
uses: actions/upload-artifact@v3
with:
name: artifact-${{ matrix.arch }}-pkgbuild
path: |
gpgui/.tmp/pkgbuild/*.pkg.tar.zst
gh-release:
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
needs:
- package-rpm
- package-pkgbuild
steps:
- name: Download artifact
uses: actions/download-artifact@v3
with:
path: artifact
# pattern: artifact-*
# merge-multiple: true
# - name: Generate checksum
# uses: jmgilman/actions-generate-checksum@v1
# with:
# output: checksums.txt
# patterns: |
# artifact/*
- name: Create GH release
uses: softprops/action-gh-release@v1
with:
token: ${{ secrets.GH_PAT }}
prerelease: ${{ contains(github.ref, 'latest') }}
fail_on_unmatched_files: true
files: |
artifact/artifact-*/*

64
.gitignore vendored
View File

@@ -1,60 +1,4 @@
# Binaries
gpclient
gpservice
# Auto generated DBus files
*_adaptor.cpp
*_adaptor.h
# C++ objects and libs
*.slo
*.lo
*.o
*.a
*.la
*.lai
*.so
*.so.*
*.dll
*.dylib
# Qt-es
object_script.*.Release
object_script.*.Debug
*_plugin_import.cpp
/.qmake.cache
/.qmake.stash
*.pro.user
*.pro.user.*
*.qbs.user
*.qbs.user.*
*.moc
moc_*.cpp
moc_*.h
qrc_*.cpp
ui_*.h
*.qmlc
*.jsc
Makefile*
*build-*
*.qm
*.prl
# Qt unit tests
target_wrapper.*
# QtCreator
*.autosave
# QtCreator Qml
*.qmlproject.user
*.qmlproject.user.*
# QtCreator CMake
CMakeLists.txt.user*
# QtCreator 4.8< compilation database
compile_commands.json
# QtCreator local machine specific files for imported projects
*creator.user*
.idea
/target
.pnpm-store
.env

7
.gitmodules vendored
View File

@@ -1,7 +0,0 @@
[submodule "singleapplication"]
path = singleapplication
url = https://github.com/itay-grudev/SingleApplication.git
[submodule "plog"]
path = plog
url = https://github.com/SergiusTheBest/plog.git

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

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

57
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,57 @@
{
"cSpell.words": [
"authcookie",
"bincode",
"chacha",
"clientos",
"datetime",
"disconnectable",
"distro",
"dotenv",
"dotenvy",
"getconfig",
"globalprotect",
"globalprotectcallback",
"gpapi",
"gpauth",
"gpcallback",
"gpclient",
"gpcommon",
"gpgui",
"gpservice",
"hidpi",
"jnlp",
"LOGNAME",
"oneshot",
"openconnect",
"pkexec",
"Prelogin",
"prelogon",
"prelogonuserauthcookie",
"repr",
"reqwest",
"roxmltree",
"rspc",
"servercert",
"specta",
"sysinfo",
"tanstack",
"tauri",
"tempfile",
"thiserror",
"tungstenite",
"unistd",
"unlisten",
"urlencoding",
"userauthcookie",
"utsbuf",
"uzers",
"Vite",
"vpnc",
"vpninfo",
"wmctrl",
"XAUTHORITY",
"yuezk"
],
"rust-analyzer.cargo.features": "all",
}

5090
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

52
Cargo.toml Normal file
View File

@@ -0,0 +1,52 @@
[workspace]
resolver = "2"
members = ["crates/*", "apps/gpclient", "apps/gpservice", "apps/gpauth"]
[workspace.package]
version = "2.0.0-beta8"
authors = ["Kevin Yue <k3vinyue@gmail.com>"]
homepage = "https://github.com/yuezk/GlobalProtect-openconnect"
edition = "2021"
license = "GPL-3.0"
[workspace.dependencies]
anyhow = "1.0"
base64 = "0.21"
clap = { version = "4.4.2", features = ["derive"] }
ctrlc = "3.4"
directories = "5.0"
env_logger = "0.10"
is_executable = "1.0"
log = "0.4"
regex = "1"
reqwest = { version = "0.11", features = ["native-tls-vendored", "json"] }
roxmltree = "0.18"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sysinfo = "0.29"
tempfile = "3.8"
tokio = { version = "1", features = ["full"] }
tokio-util = "0.7"
url = "2.4"
urlencoding = "2.1.3"
axum = "0.7"
futures = "0.3"
futures-util = "0.3"
tokio-tungstenite = "0.20.1"
specta = "=2.0.0-rc.1"
specta-macros = "=2.0.0-rc.1"
uzers = "0.11"
whoami = "1"
tauri = { version = "1.5" }
thiserror = "1"
redact-engine = "0.1"
dotenvy_macro = "0.15"
compile-time = "0.2"
[profile.release]
opt-level = 'z' # Optimize for size
lto = true # Enable link-time optimization
codegen-units = 1 # Reduce number of codegen units to increase optimizations
panic = 'abort' # Abort on panic
strip = true # Strip symbols from binary*

View File

@@ -1,78 +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
INCLUDEPATH += ../plog/include
# 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 \
gatewayauthenticator.cpp \
gpgateway.cpp \
gphelper.cpp \
loginparams.cpp \
main.cpp \
normalloginwindow.cpp \
portalauthenticator.cpp \
portalconfigresponse.cpp \
preloginresponse.cpp \
samlloginwindow.cpp \
gpclient.cpp
HEADERS += \
cdpcommand.h \
cdpcommandmanager.h \
enhancedwebview.h \
gatewayauthenticator.h \
gpgateway.h \
gphelper.h \
loginparams.h \
normalloginwindow.h \
portalauthenticator.h \
portalconfigresponse.h \
preloginresponse.h \
samlloginwindow.h \
gpclient.h
FORMS += \
gpclient.ui \
normalloginwindow.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

View File

@@ -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(&params)
{
}
QByteArray CDPCommand::toJson()
{
QVariantMap payloadMap;
payloadMap["id"] = id;
payloadMap["method"] = cmd;
payloadMap["params"] = *params;
QJsonObject payloadJsonObject = QJsonObject::fromVariantMap(payloadMap);
QJsonDocument payloadJson(payloadJsonObject);
return payloadJson.toJson();
}

View File

@@ -1,24 +0,0 @@
#ifndef CDPCOMMAND_H
#define CDPCOMMAND_H
#include <QObject>
class CDPCommand : public QObject
{
Q_OBJECT
public:
explicit CDPCommand(QObject *parent = nullptr);
CDPCommand(int id, QString cmd, QVariantMap& params);
QByteArray toJson();
signals:
void finished();
private:
int id;
QString cmd;
QVariantMap *params;
};
#endif // CDPCOMMAND_H

View File

@@ -1,86 +0,0 @@
#include "cdpcommandmanager.h"
#include <QVariantMap>
#include <plog/Log.h>
CDPCommandManager::CDPCommandManager(QObject *parent)
: QObject(parent)
, networkManager(new QNetworkAccessManager)
, socket(new QWebSocket)
{
// WebSocket setup
QObject::connect(socket, &QWebSocket::connected, this, &CDPCommandManager::ready);
QObject::connect(socket, &QWebSocket::textMessageReceived, this, &CDPCommandManager::onTextMessageReceived);
QObject::connect(socket, &QWebSocket::disconnected, this, &CDPCommandManager::onSocketDisconnected);
QObject::connect(socket, QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::error), this, &CDPCommandManager::onSocketError);
}
CDPCommandManager::~CDPCommandManager()
{
delete networkManager;
delete socket;
}
void CDPCommandManager::initialize(QString endpoint)
{
QNetworkReply *reply = networkManager->get(QNetworkRequest(endpoint));
QObject::connect(
reply, &QNetworkReply::finished,
[reply, this]() {
if (reply->error()) {
PLOGE << "CDP request error";
return;
}
QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
QJsonArray pages = doc.array();
QJsonObject page = pages.first().toObject();
QString wsUrl = page.value("webSocketDebuggerUrl").toString();
socket->open(wsUrl);
}
);
}
CDPCommand *CDPCommandManager::sendCommand(QString cmd)
{
QVariantMap emptyParams;
return sendCommend(cmd, emptyParams);
}
CDPCommand *CDPCommandManager::sendCommend(QString cmd, QVariantMap &params)
{
int id = ++commandId;
CDPCommand *command = new CDPCommand(id, cmd, params);
socket->sendTextMessage(command->toJson());
commandPool.insert(id, command);
return command;
}
void CDPCommandManager::onTextMessageReceived(QString message)
{
QJsonDocument responseDoc = QJsonDocument::fromJson(message.toUtf8());
QJsonObject response = responseDoc.object();
// Response for method
if (response.contains("id")) {
int id = response.value("id").toInt();
if (commandPool.contains(id)) {
CDPCommand *command = commandPool.take(id);
command->finished();
}
} else { // Response for event
emit eventReceived(response.value("method").toString(), response.value("params").toObject());
}
}
void CDPCommandManager::onSocketDisconnected()
{
PLOGI << "WebSocket disconnected";
}
void CDPCommandManager::onSocketError(QAbstractSocket::SocketError error)
{
PLOGE << "WebSocket error" << error;
}

View File

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

View File

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

Before

Width:  |  Height:  |  Size: 6.7 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,406 +0,0 @@
#include "gpclient.h"
#include "gphelper.h"
#include "ui_gpclient.h"
#include "portalauthenticator.h"
#include "gatewayauthenticator.h"
#include <plog/Log.h>
#include <QIcon>
using namespace gpclient::helper;
GPClient::GPClient(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::GPClient)
{
ui->setupUi(this);
setWindowTitle("GlobalProtect");
setFixedSize(width(), height());
gpclient::helper::moveCenter(this);
// Restore portal from the previous settings
ui->portalInput->setText(settings::get("portal", "").toString());
// DBus service setup
vpn = new com::yuezk::qt::GPService("com.yuezk.qt.GPService", "/", QDBusConnection::systemBus(), this);
connect(vpn, &com::yuezk::qt::GPService::connected, this, &GPClient::onVPNConnected);
connect(vpn, &com::yuezk::qt::GPService::disconnected, this, &GPClient::onVPNDisconnected);
connect(vpn, &com::yuezk::qt::GPService::logAvailable, this, &GPClient::onVPNLogAvailable);
// Initiallize the context menu of system tray.
initSystemTrayIcon();
initVpnStatus();
}
GPClient::~GPClient()
{
delete ui;
delete vpn;
}
void GPClient::on_connectButton_clicked()
{
doConnect();
}
void GPClient::on_portalInput_returnPressed()
{
doConnect();
}
void GPClient::on_portalInput_editingFinished()
{
populateGatewayMenu();
}
void GPClient::initSystemTrayIcon()
{
systemTrayIcon = new QSystemTrayIcon(this);
contextMenu = new QMenu("GlobalProtect", this);
gatewaySwitchMenu = new QMenu("Switch Gateway", this);
gatewaySwitchMenu->setIcon(QIcon::fromTheme("network-workgroup"));
populateGatewayMenu();
systemTrayIcon->setIcon(QIcon(":/images/not_connected.png"));
systemTrayIcon->setToolTip("GlobalProtect");
systemTrayIcon->setContextMenu(contextMenu);
connect(systemTrayIcon, &QSystemTrayIcon::activated, this, &GPClient::onSystemTrayActivated);
connect(gatewaySwitchMenu, &QMenu::triggered, this, &GPClient::onGatewayChanged);
openAction = contextMenu->addAction(QIcon::fromTheme("window-new"), "Open", this, &GPClient::activiate);
connectAction = contextMenu->addAction(QIcon::fromTheme("preferences-system-network"), "Connect", this, &GPClient::doConnect);
contextMenu->addMenu(gatewaySwitchMenu);
contextMenu->addSeparator();
clearAction = contextMenu->addAction(QIcon::fromTheme("edit-clear"), "Reset Settings", this, &GPClient::clearSettings);
quitAction = contextMenu->addAction(QIcon::fromTheme("application-exit"), "Quit", this, &GPClient::quit);
systemTrayIcon->show();
}
void GPClient::initVpnStatus() {
int status = vpn->status();
if (status == 1) {
ui->statusLabel->setText("Connecting...");
updateConnectionStatus(VpnStatus::pending);
} else if (status == 2) {
updateConnectionStatus(VpnStatus::connected);
} else if (status == 3) {
ui->statusLabel->setText("Disconnecting...");
updateConnectionStatus(VpnStatus::pending);
} else {
updateConnectionStatus(VpnStatus::disconnected);
}
}
void GPClient::populateGatewayMenu()
{
const QList<GPGateway> gateways = allGateways();
gatewaySwitchMenu->clear();
if (gateways.isEmpty()) {
gatewaySwitchMenu->addAction("<None>")->setData(-1);
return;
}
const QString currentGatewayName = currentGateway().name();
for (int i = 0; i < gateways.length(); i++) {
const GPGateway g = gateways.at(i);
QString iconImage = ":/images/radio_unselected.png";
if (g.name() == currentGatewayName) {
iconImage = ":/images/radio_selected.png";
}
gatewaySwitchMenu->addAction(QIcon(iconImage), g.name())->setData(i);
}
}
void GPClient::updateConnectionStatus(const GPClient::VpnStatus &status)
{
switch (status) {
case VpnStatus::disconnected:
ui->statusLabel->setText("Not Connected");
ui->statusImage->setStyleSheet("image: url(:/images/not_connected.png); padding: 15;");
ui->connectButton->setText("Connect");
ui->connectButton->setDisabled(false);
ui->portalInput->setReadOnly(false);
systemTrayIcon->setIcon(QIcon{ ":/images/not_connected.png" });
connectAction->setEnabled(true);
connectAction->setText("Connect");
gatewaySwitchMenu->setEnabled(true);
clearAction->setEnabled(true);
break;
case VpnStatus::pending:
ui->statusImage->setStyleSheet("image: url(:/images/pending.png); padding: 15;");
ui->connectButton->setDisabled(true);
ui->portalInput->setReadOnly(true);
systemTrayIcon->setIcon(QIcon{ ":/images/pending.png" });
connectAction->setEnabled(false);
gatewaySwitchMenu->setEnabled(false);
clearAction->setEnabled(false);
break;
case VpnStatus::connected:
ui->statusLabel->setText("Connected");
ui->statusImage->setStyleSheet("image: url(:/images/connected.png); padding: 15;");
ui->connectButton->setText("Disconnect");
ui->connectButton->setDisabled(false);
ui->portalInput->setReadOnly(true);
systemTrayIcon->setIcon(QIcon{ ":/images/connected.png" });
connectAction->setEnabled(true);
connectAction->setText("Disconnect");
gatewaySwitchMenu->setEnabled(true);
clearAction->setEnabled(false);
break;
default:
break;
}
}
void GPClient::onSystemTrayActivated(QSystemTrayIcon::ActivationReason reason)
{
switch (reason) {
case QSystemTrayIcon::Trigger:
case QSystemTrayIcon::DoubleClick:
this->activiate();
break;
default:
break;
}
}
void GPClient::onGatewayChanged(QAction *action)
{
const int index = action->data().toInt();
if (index == -1) {
return;
}
const GPGateway g = allGateways().at(index);
// If the selected gateway is the same as the current gateway
if (g.name() == currentGateway().name()) {
return;
}
setCurrentGateway(g);
if (connected()) {
ui->statusLabel->setText("Switching Gateway...");
ui->connectButton->setEnabled(false);
vpn->disconnect();
isSwitchingGateway = true;
}
}
void GPClient::doConnect()
{
const QString btnText = ui->connectButton->text();
const QString portal = this->portal();
// Display the main window if portal is empty
if (portal.isEmpty()) {
activiate();
return;
}
if (btnText.endsWith("Connect")) {
settings::save("portal", portal);
// Login to the previously saved gateway
if (!currentGateway().name().isEmpty()) {
isQuickConnect = true;
gatewayLogin();
} else {
// Perform the portal login
portalLogin();
}
} else {
ui->statusLabel->setText("Disconnecting...");
updateConnectionStatus(VpnStatus::pending);
vpn->disconnect();
}
}
// Login to the portal interface to get the portal config and preferred gateway
void GPClient::portalLogin()
{
PortalAuthenticator *portalAuth = new PortalAuthenticator(portal());
connect(portalAuth, &PortalAuthenticator::success, this, &GPClient::onPortalSuccess);
// Prelogin failed on the portal interface, try to treat the portal as a gateway interface
connect(portalAuth, &PortalAuthenticator::preloginFailed, this, &GPClient::onPortalPreloginFail);
// Portal login failed
connect(portalAuth, &PortalAuthenticator::fail, this, &GPClient::onPortalFail);
ui->statusLabel->setText("Authenticating...");
updateConnectionStatus(VpnStatus::pending);
portalAuth->authenticate();
}
void GPClient::onPortalSuccess(const PortalConfigResponse portalConfig, const GPGateway gateway, QList<GPGateway> allGateways)
{
this->portalConfig = portalConfig;
setAllGateways(allGateways);
setCurrentGateway(gateway);
gatewayLogin();
}
void GPClient::onPortalPreloginFail()
{
PLOGI << "Portal prelogin failed, try to preform login on the the gateway interface...";
// Treat the portal input as the gateway address
GPGateway g;
g.setName(portal());
g.setAddress(portal());
QList<GPGateway> gateways;
gateways.append(g);
setAllGateways(gateways);
setCurrentGateway(g);
gatewayLogin();
}
void GPClient::onPortalFail(const QString &msg)
{
if (!msg.isEmpty()) {
openMessageBox("Portal authentication failed.", msg);
}
updateConnectionStatus(VpnStatus::disconnected);
}
// Login to the gateway
void GPClient::gatewayLogin()
{
GatewayAuthenticator *gatewayAuth = new GatewayAuthenticator(currentGateway().address(), portalConfig);
connect(gatewayAuth, &GatewayAuthenticator::success, this, &GPClient::onGatewaySuccess);
connect(gatewayAuth, &GatewayAuthenticator::fail, this, &GPClient::onGatewayFail);
ui->statusLabel->setText("Authenticating...");
updateConnectionStatus(VpnStatus::pending);
gatewayAuth->authenticate();
}
void GPClient::onGatewaySuccess(const QString &authCookie)
{
PLOGI << "Gateway login succeeded, got the cookie " << authCookie;
isQuickConnect = false;
vpn->connect(currentGateway().address(), portalConfig.username(), authCookie);
ui->statusLabel->setText("Connecting...");
updateConnectionStatus(VpnStatus::pending);
}
void GPClient::onGatewayFail(const QString &msg)
{
// If the quick connect on gateway failed, perform the portal login
if (isQuickConnect && !msg.isEmpty()) {
isQuickConnect = false;
portalLogin();
return;
}
if (!msg.isEmpty()) {
openMessageBox("Gateway authentication failed.", msg);
}
updateConnectionStatus(VpnStatus::disconnected);
}
void GPClient::activiate()
{
activateWindow();
showNormal();
}
QString GPClient::portal() const
{
const QString input = ui->portalInput->text().trimmed();
if (input.startsWith("http")) {
return QUrl(input).authority();
}
return input;
}
bool GPClient::connected() const
{
const QString statusText = ui->statusLabel->text();
return statusText.contains("Connected") && !statusText.contains("Not");
}
QList<GPGateway> GPClient::allGateways() const
{
const QString gatewaysJson = settings::get(portal() + "_gateways").toString();
return GPGateway::fromJson(gatewaysJson);
}
void GPClient::setAllGateways(QList<GPGateway> gateways)
{
settings::save(portal() + "_gateways", GPGateway::serialize(gateways));
populateGatewayMenu();
}
GPGateway GPClient::currentGateway() const
{
const QString selectedGateway = settings::get(portal() + "_selectedGateway").toString();
for (auto g : allGateways()) {
if (g.name() == selectedGateway) {
return g;
}
}
return GPGateway{};
}
void GPClient::setCurrentGateway(const GPGateway gateway)
{
settings::save(portal() + "_selectedGateway", gateway.name());
populateGatewayMenu();
}
void GPClient::clearSettings()
{
settings::clear();
populateGatewayMenu();
ui->portalInput->clear();
}
void GPClient::quit()
{
vpn->disconnect();
QApplication::quit();
}
void GPClient::onVPNConnected()
{
updateConnectionStatus(VpnStatus::connected);
}
void GPClient::onVPNDisconnected()
{
updateConnectionStatus(VpnStatus::disconnected);
if (isSwitchingGateway) {
gatewayLogin();
isSwitchingGateway = false;
}
}
void GPClient::onVPNLogAvailable(QString log)
{
PLOGI << log;
}

View File

@@ -1,89 +0,0 @@
#ifndef GPCLIENT_H
#define GPCLIENT_H
#include "gpservice_interface.h"
#include "portalconfigresponse.h"
#include <QMainWindow>
#include <QSystemTrayIcon>
#include <QMenu>
QT_BEGIN_NAMESPACE
namespace Ui { class GPClient; }
QT_END_NAMESPACE
class GPClient : public QMainWindow
{
Q_OBJECT
public:
GPClient(QWidget *parent = nullptr);
~GPClient();
void activiate();
private slots:
void on_connectButton_clicked();
void on_portalInput_returnPressed();
void on_portalInput_editingFinished();
void onSystemTrayActivated(QSystemTrayIcon::ActivationReason reason);
void onGatewayChanged(QAction *action);
void onPortalSuccess(const PortalConfigResponse portalConfig, const GPGateway gateway, QList<GPGateway> allGateways);
void onPortalPreloginFail();
void onPortalFail(const QString &msg);
void onGatewaySuccess(const QString &authCookie);
void onGatewayFail(const QString &msg);
void onVPNConnected();
void onVPNDisconnected();
void onVPNLogAvailable(QString log);
private:
enum class VpnStatus
{
disconnected,
pending,
connected
};
Ui::GPClient *ui;
com::yuezk::qt::GPService *vpn;
QSystemTrayIcon *systemTrayIcon;
QMenu *contextMenu;
QAction *openAction;
QAction *connectAction;
QMenu *gatewaySwitchMenu;
QAction *clearAction;
QAction *quitAction;
bool isQuickConnect { false };
bool isSwitchingGateway { false };
PortalConfigResponse portalConfig;
void initSystemTrayIcon();
void initVpnStatus();
void populateGatewayMenu();
void updateConnectionStatus(const VpnStatus &status);
void doConnect();
void portalLogin();
void gatewayLogin();
QString portal() const;
bool connected() const;
QList<GPGateway> allGateways() const;
void setAllGateways(QList<GPGateway> gateways);
GPGateway currentGateway() const;
void setCurrentGateway(const GPGateway gateway);
void clearSettings();
void quit();
};
#endif // GPCLIENT_H

View File

@@ -1,133 +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>GlobalProtect OpenConnect</string>
</property>
<property name="windowIcon">
<iconset resource="resources.qrc">
<normaloff>:/images/logo.svg</normaloff>:/images/logo.svg</iconset>
</property>
<property name="styleSheet">
<string notr="true"/>
</property>
<property name="iconSize">
<size>
<width>22</width>
<height>22</height>
</size>
</property>
<widget class="QWidget" name="centralwidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="layoutDirection">
<enum>Qt::LeftToRight</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3" stretch="1,0">
<property name="leftMargin">
<number>15</number>
</property>
<property name="topMargin">
<number>15</number>
</property>
<property name="rightMargin">
<number>15</number>
</property>
<property name="bottomMargin">
<number>15</number>
</property>
<item>
<layout class="QVBoxLayout" name="verticalLayout" stretch="1,0">
<property name="bottomMargin">
<number>15</number>
</property>
<item>
<widget class="QLabel" name="statusImage">
<property name="styleSheet">
<string notr="true">#statusImage {
image: url(:/images/not_connected.png);
padding: 15
}</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="statusLabel">
<property name="font">
<font>
<pointsize>14</pointsize>
<weight>50</weight>
<bold>false</bold>
<underline>false</underline>
</font>
</property>
<property name="text">
<string>Not Connected</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLineEdit" name="portalInput">
<property name="text">
<string/>
</property>
<property name="placeholderText">
<string>Please enter your portal address</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="connectButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Connect</string>
</property>
<property name="autoDefault">
<bool>true</bool>
</property>
<property name="default">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
<resources>
<include location="resources.qrc"/>
</resources>
<connections/>
</ui>

View File

@@ -1,97 +0,0 @@
#include "gpgateway.h"
#include <QJsonObject>
#include <QJsonDocument>
#include <QJsonArray>
GPGateway::GPGateway()
{
}
QString GPGateway::name() const
{
return _name;
}
QString GPGateway::address() const
{
return _address;
}
void GPGateway::setName(const QString &name)
{
_name = name;
}
void GPGateway::setAddress(const QString &address)
{
_address = address;
}
void GPGateway::setPriorityRules(const QMap<QString, int> &priorityRules)
{
_priorityRules = priorityRules;
}
int GPGateway::priorityOf(QString ruleName) const
{
if (_priorityRules.contains(ruleName)) {
return _priorityRules.value(ruleName);
}
return 0;
}
QJsonObject GPGateway::toJsonObject() const
{
QJsonObject obj;
obj.insert("name", name());
obj.insert("address", address());
return obj;
}
QString GPGateway::toString() const
{
QJsonDocument jsonDoc{ toJsonObject() };
return QString::fromUtf8(jsonDoc.toJson());
}
QString GPGateway::serialize(QList<GPGateway> &gateways)
{
QJsonArray arr;
for (auto g : gateways) {
arr.append(g.toJsonObject());
}
QJsonDocument jsonDoc{ arr };
return QString::fromUtf8(jsonDoc.toJson());
}
QList<GPGateway> GPGateway::fromJson(const QString &jsonString)
{
QList<GPGateway> gateways;
if (jsonString.isEmpty()) {
return gateways;
}
QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonString.toUtf8());
for (auto item : jsonDoc.array()) {
GPGateway g = GPGateway::fromJsonObject(item.toObject());
gateways.append(g);
}
return gateways;
}
GPGateway GPGateway::fromJsonObject(const QJsonObject &jsonObj)
{
GPGateway g;
g.setName(jsonObj.value("name").toString());
g.setAddress(jsonObj.value("address").toString());
return g;
}

View File

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

View File

@@ -1,108 +0,0 @@
#include "gphelper.h"
#include <QNetworkRequest>
#include <QXmlStreamReader>
#include <QMessageBox>
#include <QDesktopWidget>
#include <QApplication>
#include <QWidget>
#include <plog/Log.h>
QNetworkAccessManager* gpclient::helper::networkManager = new QNetworkAccessManager;
QNetworkReply* gpclient::helper::createRequest(QString url, QByteArray params)
{
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
request.setHeader(QNetworkRequest::UserAgentHeader, UA);
if (params == nullptr) {
return networkManager->post(request, QByteArray(nullptr));
}
return networkManager->post(request, params);
}
GPGateway gpclient::helper::filterPreferredGateway(QList<GPGateway> gateways, const QString ruleName)
{
GPGateway gateway = gateways.first();
for (GPGateway g : gateways) {
if (g.priorityOf(ruleName) > gateway.priorityOf(ruleName)) {
gateway = g;
}
}
return gateway;
}
QUrlQuery gpclient::helper::parseGatewayResponse(const QByteArray &xml)
{
QXmlStreamReader xmlReader{xml};
QList<QString> args;
while (!xmlReader.atEnd()) {
xmlReader.readNextStartElement();
if (xmlReader.name() == "argument") {
args.append(QUrl::toPercentEncoding(xmlReader.readElementText()));
}
}
QUrlQuery params{};
params.addQueryItem("authcookie", args.at(1));
params.addQueryItem("portal", args.at(3));
params.addQueryItem("user", args.at(4));
params.addQueryItem("domain", args.at(7));
params.addQueryItem("preferred-ip", args.at(15));
params.addQueryItem("computer", QUrl::toPercentEncoding(QSysInfo::machineHostName()));
return params;
}
void gpclient::helper::openMessageBox(const QString &message, const QString& informativeText)
{
QMessageBox msgBox;
msgBox.setWindowTitle("Notice");
msgBox.setText(message);
msgBox.setFixedWidth(500);
msgBox.setStyleSheet("QLabel{min-width: 250px}");
msgBox.setInformativeText(informativeText);
msgBox.exec();
}
void gpclient::helper::moveCenter(QWidget *widget)
{
QDesktopWidget *desktop = QApplication::desktop();
int screenWidth, width;
int screenHeight, height;
int x, y;
QSize windowSize;
screenWidth = desktop->width();
screenHeight = desktop->height();
windowSize = widget->size();
width = windowSize.width();
height = windowSize.height();
x = (screenWidth - width) / 2;
y = (screenHeight - height) / 2;
y -= 50;
widget->move(x, y);
}
QSettings *gpclient::helper::settings::_settings = new QSettings("com.yuezk.qt", "GPClient");
QVariant gpclient::helper::settings::get(const QString &key, const QVariant &defaultValue)
{
return _settings->value(key, defaultValue);
}
void gpclient::helper::settings::save(const QString &key, const QVariant &value)
{
_settings->setValue(key, value);
}
void gpclient::helper::settings::clear()
{
_settings->clear();
}

View File

@@ -1,42 +0,0 @@
#ifndef GPHELPER_H
#define GPHELPER_H
#include "samlloginwindow.h"
#include "gpgateway.h"
#include <QObject>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QUrlQuery>
#include <QSettings>
const QString UA = "PAN GlobalProtect";
namespace gpclient {
namespace helper {
extern QNetworkAccessManager *networkManager;
QNetworkReply* createRequest(QString url, QByteArray params = nullptr);
GPGateway filterPreferredGateway(QList<GPGateway> gateways, const QString ruleName);
QUrlQuery parseGatewayResponse(const QByteArray& xml);
void openMessageBox(const QString& message, const QString& informativeText = "");
void moveCenter(QWidget *widget);
namespace settings {
extern QSettings *_settings;
QVariant get(const QString &key, const QVariant &defaultValue = QVariant());
void save(const QString &key, const QVariant &value);
void clear();
}
}
}
#endif // GPHELPER_H

View File

@@ -1,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()
{
}

View File

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

View File

@@ -1,70 +0,0 @@
#include "loginparams.h"
#include <QUrlQuery>
LoginParams::LoginParams()
{
params.addQueryItem("prot", QUrl::toPercentEncoding("https:"));
params.addQueryItem("server", "");
params.addQueryItem("inputSrc", "");
params.addQueryItem("jnlpReady", "jnlpReady");
params.addQueryItem("user", "");
params.addQueryItem("passwd", "");
params.addQueryItem("computer", QUrl::toPercentEncoding(QSysInfo::machineHostName()));
params.addQueryItem("ok", "Login");
params.addQueryItem("direct", "yes");
params.addQueryItem("clientVer", "4100");
params.addQueryItem("os-version", QUrl::toPercentEncoding(QSysInfo::prettyProductName()));
params.addQueryItem("clientos", "Linux");
params.addQueryItem("portal-userauthcookie", "");
params.addQueryItem("portal-prelogonuserauthcookie", "");
params.addQueryItem("prelogin-cookie", "");
params.addQueryItem("ipv6-support", "yes");
}
LoginParams::~LoginParams()
{
}
void LoginParams::setUser(const QString &user)
{
updateQueryItem("user", user);
}
void LoginParams::setServer(const QString &server)
{
updateQueryItem("server", server);
}
void LoginParams::setPassword(const QString &password)
{
updateQueryItem("passwd", password);
}
void LoginParams::setUserAuthCookie(const QString &cookie)
{
updateQueryItem("portal-userauthcookie", cookie);
}
void LoginParams::setPrelogonAuthCookie(const QString &cookie)
{
updateQueryItem("portal-prelogonuserauthcookie", cookie);
}
void LoginParams::setPreloginCookie(const QString &cookie)
{
updateQueryItem("prelogin-cookie", cookie);
}
QByteArray LoginParams::toUtf8() const
{
return params.toString().toUtf8();
}
void LoginParams::updateQueryItem(const QString &key, const QString &value)
{
if (params.hasQueryItem(key)) {
params.removeQueryItem(key);
}
params.addQueryItem(key, QUrl::toPercentEncoding(value));
}

View File

@@ -1,27 +0,0 @@
#ifndef LOGINPARAMS_H
#define LOGINPARAMS_H
#include <QUrlQuery>
class LoginParams
{
public:
LoginParams();
~LoginParams();
void setUser(const QString &user);
void setServer(const QString &server);
void setPassword(const QString &password);
void setUserAuthCookie(const QString &cookie);
void setPrelogonAuthCookie(const QString &cookie);
void setPreloginCookie(const QString &cookie);
QByteArray toUtf8() const;
private:
QUrlQuery params;
void updateQueryItem(const QString &key, const QString &value);
};
#endif // LOGINPARAMS_H

View File

@@ -1,33 +0,0 @@
#include "singleapplication.h"
#include "gpclient.h"
#include "enhancedwebview.h"
#include <QStandardPaths>
#include <plog/Log.h>
#include <plog/Appenders/ColorConsoleAppender.h>
int main(int argc, char *argv[])
{
const QDir path = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + "/GlobalProtect-openconnect";
const QString logFile = path.path() + "/gpclient.log";
if (!path.exists()) {
path.mkpath(".");
}
static plog::ColorConsoleAppender<plog::TxtFormatter> consoleAppender;
plog::init(plog::debug, logFile.toUtf8()).addAppender(&consoleAppender);
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::activiate);
return app.exec();
}

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -1,196 +0,0 @@
#include "portalauthenticator.h"
#include "gphelper.h"
#include "normalloginwindow.h"
#include "samlloginwindow.h"
#include "loginparams.h"
#include "preloginresponse.h"
#include "portalconfigresponse.h"
#include "gpgateway.h"
#include <plog/Log.h>
#include <QNetworkReply>
using namespace gpclient::helper;
PortalAuthenticator::PortalAuthenticator(const QString& portal) : QObject()
, portal(portal)
, preloginUrl("https://" + portal + "/global-protect/prelogin.esp")
, configUrl("https://" + portal + "/global-protect/getconfig.esp")
{
}
PortalAuthenticator::~PortalAuthenticator()
{
delete normalLoginWindow;
}
void PortalAuthenticator::authenticate()
{
PLOGI << "Preform portal prelogin at " << preloginUrl;
QNetworkReply *reply = createRequest(preloginUrl);
connect(reply, &QNetworkReply::finished, this, &PortalAuthenticator::onPreloginFinished);
}
void PortalAuthenticator::onPreloginFinished()
{
QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
if (reply->error()) {
PLOGE << QString("Error occurred while accessing %1, %2").arg(preloginUrl).arg(reply->errorString());
emit preloginFailed("Error occurred on the portal prelogin interface.");
delete reply;
return;
}
PLOGI << "Portal prelogin succeeded.";
preloginResponse = PreloginResponse::parse(reply->readAll());
if (preloginResponse.hasSamlAuthFields()) {
// Do SAML authentication
samlAuth();
} else if (preloginResponse.hasNormalAuthFields()) {
// Do normal username/password authentication
tryAutoLogin();
} else {
PLOGE << QString("Unknown prelogin response for %1 got %2").arg(preloginUrl).arg(QString::fromUtf8(preloginResponse.rawResponse()));
emitFail("Unknown response for portal prelogin interface.");
}
delete reply;
}
void PortalAuthenticator::tryAutoLogin()
{
const QString username = settings::get("username").toString();
const QString password = settings::get("password").toString();
if (!username.isEmpty() && !password.isEmpty()) {
PLOGI << "Trying auto login using the saved credentials";
isAutoLogin = true;
fetchConfig(settings::get("username").toString(), settings::get("password").toString());
} else {
normalAuth();
}
}
void PortalAuthenticator::normalAuth()
{
PLOGI << "Trying to launch the normal login window...";
normalLoginWindow = new NormalLoginWindow;
normalLoginWindow->setPortalAddress(portal);
normalLoginWindow->setAuthMessage(preloginResponse.authMessage());
normalLoginWindow->setUsernameLabel(preloginResponse.labelUsername());
normalLoginWindow->setPasswordLabel(preloginResponse.labelPassword());
// Do login
connect(normalLoginWindow, &NormalLoginWindow::performLogin, this, &PortalAuthenticator::onPerformNormalLogin);
connect(normalLoginWindow, &NormalLoginWindow::rejected, this, &PortalAuthenticator::onLoginWindowRejected);
connect(normalLoginWindow, &NormalLoginWindow::finished, this, &PortalAuthenticator::onLoginWindowFinished);
normalLoginWindow->show();
}
void PortalAuthenticator::onPerformNormalLogin(const QString &username, const QString &password)
{
normalLoginWindow->setProcessing(true);
fetchConfig(username, password);
}
void PortalAuthenticator::onLoginWindowRejected()
{
emitFail();
}
void PortalAuthenticator::onLoginWindowFinished()
{
delete normalLoginWindow;
normalLoginWindow = nullptr;
}
void PortalAuthenticator::samlAuth()
{
PLOGI << "Trying to perform SAML login with saml-method " << preloginResponse.samlMethod();
SAMLLoginWindow *loginWindow = new SAMLLoginWindow;
connect(loginWindow, &SAMLLoginWindow::success, this, &PortalAuthenticator::onSAMLLoginSuccess);
connect(loginWindow, &SAMLLoginWindow::fail, this, &PortalAuthenticator::onSAMLLoginFail);
connect(loginWindow, &SAMLLoginWindow::rejected, this, &PortalAuthenticator::onLoginWindowRejected);
loginWindow->login(preloginResponse.samlMethod(), preloginResponse.samlRequest(), preloginUrl);
}
void PortalAuthenticator::onSAMLLoginSuccess(const QMap<QString, QString> samlResult)
{
PLOGI << "SAML login succeeded, got the prelogin cookie " << samlResult.value("preloginCookie");
fetchConfig(samlResult.value("username"), "", samlResult.value("preloginCookie"));
}
void PortalAuthenticator::onSAMLLoginFail(const QString msg)
{
emitFail(msg);
}
void PortalAuthenticator::fetchConfig(QString username, QString password, QString preloginCookie)
{
LoginParams params;
params.setServer(portal);
params.setUser(username);
params.setPassword(password);
params.setPreloginCookie(preloginCookie);
// Save the username and password for future use.
this->username = username;
this->password = password;
PLOGI << "Fetching the portal config from " << configUrl << " for user: " << username;
QNetworkReply *reply = createRequest(configUrl, params.toUtf8());
connect(reply, &QNetworkReply::finished, this, &PortalAuthenticator::onFetchConfigFinished);
}
void PortalAuthenticator::onFetchConfigFinished()
{
QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
if (reply->error()) {
PLOGE << QString("Failed to fetch the portal config from %1, %2").arg(configUrl).arg(reply->errorString());
// Login failed, enable the fields of the normal login window
if (normalLoginWindow) {
normalLoginWindow->setProcessing(false);
openMessageBox("Portal login failed.", "Please check your credentials and try again.");
} else if (isAutoLogin) {
isAutoLogin = false;
normalAuth();
} else {
emitFail("Failed to fetch the portal config.");
}
return;
}
PLOGI << "Fetch the portal config succeeded.";
PortalConfigResponse response = PortalConfigResponse::parse(reply->readAll());
// Add the username & password to the response object
response.setUsername(username);
response.setPassword(password);
// Close the login window
if (normalLoginWindow) {
// Save the credentials for reuse
settings::save("username", username);
settings::save("password", password);
normalLoginWindow->close();
}
emit success(response, filterPreferredGateway(response.allGateways(), preloginResponse.region()), response.allGateways());
}
void PortalAuthenticator::emitFail(const QString& msg)
{
emit fail(msg);
}

View File

@@ -1,54 +0,0 @@
#ifndef PORTALAUTHENTICATOR_H
#define PORTALAUTHENTICATOR_H
#include "portalconfigresponse.h"
#include "normalloginwindow.h"
#include "samlloginwindow.h"
#include "preloginresponse.h"
#include <QObject>
class PortalAuthenticator : public QObject
{
Q_OBJECT
public:
explicit PortalAuthenticator(const QString& portal);
~PortalAuthenticator();
void authenticate();
signals:
void success(const PortalConfigResponse, const GPGateway, QList<GPGateway> allGateways);
void fail(const QString& msg);
void preloginFailed(const QString& msg);
private slots:
void onPreloginFinished();
void onPerformNormalLogin(const QString &username, const QString &password);
void onLoginWindowRejected();
void onLoginWindowFinished();
void onSAMLLoginSuccess(const QMap<QString, QString> samlResult);
void onSAMLLoginFail(const QString msg);
void onFetchConfigFinished();
private:
QString portal;
QString preloginUrl;
QString configUrl;
QString username;
QString password;
PreloginResponse preloginResponse;
bool isAutoLogin { false };
NormalLoginWindow *normalLoginWindow{ nullptr };
void tryAutoLogin();
void normalAuth();
void samlAuth();
void fetchConfig(QString username, QString password, QString preloginCookie = "");
void emitFail(const QString& msg = "");
};
#endif // PORTALAUTHENTICATOR_H

View File

@@ -1,149 +0,0 @@
#include "portalconfigresponse.h"
#include <QXmlStreamReader>
#include <plog/Log.h>
QString PortalConfigResponse::xmlUserAuthCookie = "portal-userauthcookie";
QString PortalConfigResponse::xmlPrelogonUserAuthCookie = "portal-prelogonuserauthcookie";
QString PortalConfigResponse::xmlGateways = "gateways";
PortalConfigResponse::PortalConfigResponse()
{
}
PortalConfigResponse::~PortalConfigResponse()
{
}
PortalConfigResponse PortalConfigResponse::parse(const QByteArray& xml)
{
QXmlStreamReader xmlReader(xml);
PortalConfigResponse response;
response.setRawResponse(xml);
while (!xmlReader.atEnd()) {
xmlReader.readNextStartElement();
QString name = xmlReader.name().toString();
if (name == xmlUserAuthCookie) {
response.setUserAuthCookie(xmlReader.readElementText());
} else if (name == xmlPrelogonUserAuthCookie) {
response.setPrelogonUserAuthCookie(xmlReader.readElementText());
} else if (name == xmlGateways) {
response.setAllGateways(parseGateways(xmlReader));
}
}
return response;
}
const QByteArray& PortalConfigResponse::rawResponse() const
{
return _rawResponse;
}
QString PortalConfigResponse::username() const
{
return _username;
}
QString PortalConfigResponse::password() const
{
return _password;
}
QList<GPGateway> PortalConfigResponse::parseGateways(QXmlStreamReader &xmlReader)
{
QList<GPGateway> gateways;
while (xmlReader.name() != xmlGateways || !xmlReader.isEndElement()) {
xmlReader.readNext();
// Parse the gateways -> external -> list -> entry
if (xmlReader.name() == "entry" && xmlReader.isStartElement()) {
GPGateway g;
QString address = xmlReader.attributes().value("name").toString();
g.setAddress(address);
g.setPriorityRules(parsePriorityRules(xmlReader));
g.setName(parseGatewayName(xmlReader));
gateways.append(g);
}
}
return gateways;
}
QMap<QString, int> PortalConfigResponse::parsePriorityRules(QXmlStreamReader &xmlReader)
{
QMap<QString, int> priorityRules;
while (xmlReader.name() != "priority-rule" || !xmlReader.isEndElement()) {
xmlReader.readNext();
if (xmlReader.name() == "entry" && xmlReader.isStartElement()) {
QString ruleName = xmlReader.attributes().value("name").toString();
// Read the priority tag
xmlReader.readNextStartElement();
int ruleValue = xmlReader.readElementText().toUInt();
priorityRules.insert(ruleName, ruleValue);
}
}
return priorityRules;
}
QString PortalConfigResponse::parseGatewayName(QXmlStreamReader &xmlReader)
{
while (xmlReader.name() != "description" || !xmlReader.isEndElement()) {
xmlReader.readNext();
if (xmlReader.name() == "description" && xmlReader.tokenType() == xmlReader.StartElement) {
return xmlReader.readElementText();
}
}
PLOGE << "Error: <description> tag not found";
return "";
}
QString PortalConfigResponse::userAuthCookie() const
{
return _userAuthCookie;
}
QString PortalConfigResponse::prelogonUserAuthCookie() const
{
return _prelogonAuthCookie;
}
QList<GPGateway> PortalConfigResponse::allGateways()
{
return _gateways;
}
void PortalConfigResponse::setAllGateways(QList<GPGateway> gateways)
{
_gateways = gateways;
}
void PortalConfigResponse::setRawResponse(const QByteArray &response)
{
_rawResponse = response;
}
void PortalConfigResponse::setUsername(const QString& username)
{
_username = username;
}
void PortalConfigResponse::setPassword(const QString& password)
{
_password = password;
}
void PortalConfigResponse::setUserAuthCookie(const QString &cookie)
{
_userAuthCookie = cookie;
}
void PortalConfigResponse::setPrelogonUserAuthCookie(const QString &cookie)
{
_prelogonAuthCookie = cookie;
}

View File

@@ -1,51 +0,0 @@
#ifndef PORTALCONFIGRESPONSE_H
#define PORTALCONFIGRESPONSE_H
#include "gpgateway.h"
#include <QString>
#include <QList>
#include <QXmlStreamReader>
class PortalConfigResponse
{
public:
PortalConfigResponse();
~PortalConfigResponse();
static PortalConfigResponse parse(const QByteArray& xml);
const QByteArray& rawResponse() const;
QString username() const;
QString password() const;
QString userAuthCookie() const;
QString prelogonUserAuthCookie() const;
QList<GPGateway> allGateways();
void setAllGateways(QList<GPGateway> gateways);
void setUsername(const QString& username);
void setPassword(const QString& password);
private:
static QString xmlUserAuthCookie;
static QString xmlPrelogonUserAuthCookie;
static QString xmlGateways;
QByteArray _rawResponse;
QString _username;
QString _password;
QString _userAuthCookie;
QString _prelogonAuthCookie;
QList<GPGateway> _gateways;
void setRawResponse(const QByteArray& response);
void setUserAuthCookie(const QString& cookie);
void setPrelogonUserAuthCookie(const QString& cookie);
static QList<GPGateway> parseGateways(QXmlStreamReader &xmlReader);
static QMap<QString, int> parsePriorityRules(QXmlStreamReader &xmlReader);
static QString parseGatewayName(QXmlStreamReader &xmlReader);
};
#endif // PORTALCONFIGRESPONSE_H

View File

@@ -1,97 +0,0 @@
#include "preloginresponse.h"
#include <QXmlStreamReader>
#include <QMap>
QString PreloginResponse::xmlAuthMessage = "authentication-message";
QString PreloginResponse::xmlLabelUsername = "username-label";
QString PreloginResponse::xmlLabelPassword = "password-label";
QString PreloginResponse::xmlSamlMethod = "saml-auth-method";
QString PreloginResponse::xmlSamlRequest = "saml-request";
QString PreloginResponse::xmlRegion = "region";
PreloginResponse::PreloginResponse()
{
add(xmlAuthMessage, "");
add(xmlLabelUsername, "");
add(xmlLabelPassword, "");
add(xmlSamlMethod, "");
add(xmlSamlRequest, "");
add(xmlRegion, "");
}
PreloginResponse PreloginResponse::parse(const QByteArray& xml)
{
QXmlStreamReader xmlReader(xml);
PreloginResponse response;
response.setRawResponse(xml);
while (!xmlReader.atEnd()) {
xmlReader.readNextStartElement();
QString name = xmlReader.name().toString();
if (response.has(name)) {
response.add(name, xmlReader.readElementText());
}
}
return response;
}
const QByteArray& PreloginResponse::rawResponse() const
{
return _rawResponse;
}
QString PreloginResponse::authMessage() const
{
return resultMap.value(xmlAuthMessage);
}
QString PreloginResponse::labelUsername() const
{
return resultMap.value(xmlLabelUsername);
}
QString PreloginResponse::labelPassword() const
{
return resultMap.value(xmlLabelPassword);
}
QString PreloginResponse::samlMethod() const
{
return resultMap.value(xmlSamlMethod);
}
QString PreloginResponse::samlRequest() const
{
return QByteArray::fromBase64(resultMap.value(xmlSamlRequest).toUtf8());
}
QString PreloginResponse::region() const
{
return resultMap.value(xmlRegion);
}
bool PreloginResponse::hasSamlAuthFields() const
{
return !samlMethod().isEmpty() && !samlRequest().isEmpty();
}
bool PreloginResponse::hasNormalAuthFields() const
{
return !labelUsername().isEmpty() && !labelPassword().isEmpty();
}
void PreloginResponse::setRawResponse(const QByteArray &response)
{
_rawResponse = response;
}
bool PreloginResponse::has(const QString &name) const
{
return resultMap.contains(name);
}
void PreloginResponse::add(const QString &name, const QString &value)
{
resultMap.insert(name, value);
}

View File

@@ -1,41 +0,0 @@
#ifndef PRELOGINRESPONSE_H
#define PRELOGINRESPONSE_H
#include <QString>
#include <QMap>
class PreloginResponse
{
public:
PreloginResponse();
static PreloginResponse parse(const QByteArray& xml);
const QByteArray& rawResponse() const;
QString authMessage() const;
QString labelUsername() const;
QString labelPassword() const;
QString samlMethod() const;
QString samlRequest() const;
QString region() const;
bool hasSamlAuthFields() const;
bool hasNormalAuthFields() const;
private:
static QString xmlAuthMessage;
static QString xmlLabelUsername;
static QString xmlLabelPassword;
static QString xmlSamlMethod;
static QString xmlSamlRequest;
static QString xmlRegion;
QMap<QString, QString> resultMap;
QByteArray _rawResponse;
void setRawResponse(const QByteArray &response);
void add(const QString &name, const QString &value);
bool has(const QString &name) const;
};
#endif // PRELOGINRESPONSE_H

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 993 B

View File

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

View File

@@ -1,80 +0,0 @@
#include "samlloginwindow.h"
#include <QVBoxLayout>
#include <plog/Log.h>
#include <QWebEngineProfile>
#include <QWebEngineView>
SAMLLoginWindow::SAMLLoginWindow(QWidget *parent)
: QDialog(parent)
, webView(new EnhancedWebView(this))
{
setWindowTitle("GlobalProtect SAML Login");
setModal(true);
resize(700, 550);
QVBoxLayout *verticalLayout = new QVBoxLayout(this);
webView->setUrl(QUrl("about:blank"));
// webView->page()->profile()->setPersistentCookiesPolicy(QWebEngineProfile::NoPersistentCookies);
verticalLayout->addWidget(webView);
webView->initialize();
connect(webView, &EnhancedWebView::responseReceived, this, &SAMLLoginWindow::onResponseReceived);
connect(webView, &EnhancedWebView::loadFinished, this, &SAMLLoginWindow::onLoadFinished);
}
SAMLLoginWindow::~SAMLLoginWindow()
{
delete webView;
}
void SAMLLoginWindow::closeEvent(QCloseEvent *event)
{
event->accept();
reject();
}
void SAMLLoginWindow::login(const QString samlMethod, const QString samlRequest, const QString preloingUrl)
{
if (samlMethod == "POST") {
webView->setHtml(samlRequest, preloingUrl);
} else if (samlMethod == "REDIRECT") {
webView->load(samlRequest);
} else {
PLOGE << "Unknown saml-auth-method expected POST or REDIRECT, got " << samlMethod;
emit fail("Unknown saml-auth-method, got " + samlMethod);
}
}
void SAMLLoginWindow::onResponseReceived(QJsonObject params)
{
QString type = params.value("type").toString();
// Skip non-document response
if (type != "Document") {
return;
}
QJsonObject response = params.value("response").toObject();
QJsonObject headers = response.value("headers").toObject();
const QString username = headers.value("saml-username").toString();
const QString preloginCookie = headers.value("prelogin-cookie").toString();
if (!username.isEmpty() && !preloginCookie.isEmpty()) {
samlResult.insert("username", username);
samlResult.insert("preloginCookie", preloginCookie);
}
}
void SAMLLoginWindow::onLoadFinished()
{
LOGI << "Load finished " << this->webView->page()->url().toString();
// Check the SAML result
if (!samlResult.value("username").isEmpty() && !samlResult.value("preloginCookie").isEmpty()) {
emit success(samlResult);
accept();
} else {
this->show();
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,131 +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
<< "-C" << passwd
<< server;
openconnect->start(bin, args);
}
void GPService::disconnect()
{
if (openconnect->state() != QProcess::NotRunning) {
vpnStatus = GPService::VpnDisconnecting;
openconnect->terminate();
}
}
int GPService::status()
{
return vpnStatus;
}
void GPService::onProcessStarted()
{
log("Openconnect started successfully, PID=" + QString::number(openconnect->processId()));
vpnStatus = GPService::VpnConnecting;
}
void GPService::onProcessError(QProcess::ProcessError error)
{
log("Error occurred: " + QVariant::fromValue(error).toString());
vpnStatus = GPService::VpnNotConnected;
emit disconnected();
}
void GPService::onProcessStdout()
{
QString output = openconnect->readAllStandardOutput();
log(output);
if (output.indexOf("Connected as") >= 0) {
vpnStatus = GPService::VpnConnected;
emit connected();
}
}
void GPService::onProcessStderr()
{
log(openconnect->readAllStandardError());
}
void GPService::onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus)
{
log("Openconnect process exited with code " + QString::number(exitCode) + " and exit status " + QVariant::fromValue(exitStatus).toString());
vpnStatus = GPService::VpnNotConnected;
emit disconnected();
if (aboutToQuit) {
exit(0);
};
}
void GPService::log(QString msg)
{
emit logAvailable(msg);
}

View File

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

View File

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

View File

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

View File

@@ -1,176 +0,0 @@
/*
* Unix signal watcher for Qt.
*
* Copyright (C) 2014 Simon Knopp
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <sys/socket.h>
#include <unistd.h>
#include <errno.h>
#include <QMap>
#include <QSocketNotifier>
#include <QDebug>
#include "sigwatch.h"
/*!
* \brief The UnixSignalWatcherPrivate class implements the back-end signal
* handling for the UnixSignalWatcher.
*
* \see http://qt-project.org/doc/qt-5.0/qtdoc/unix-signals.html
*/
class UnixSignalWatcherPrivate : public QObject
{
UnixSignalWatcher * const q_ptr;
Q_DECLARE_PUBLIC(UnixSignalWatcher)
public:
UnixSignalWatcherPrivate(UnixSignalWatcher *q);
~UnixSignalWatcherPrivate();
void watchForSignal(int signal);
static void signalHandler(int signal);
void _q_onNotify(int sockfd);
private:
static int sockpair[2];
QSocketNotifier *notifier;
QList<int> watchedSignals;
};
int UnixSignalWatcherPrivate::sockpair[2];
UnixSignalWatcherPrivate::UnixSignalWatcherPrivate(UnixSignalWatcher *q) :
q_ptr(q)
{
// Create socket pair
if (::socketpair(AF_UNIX, SOCK_STREAM, 0, sockpair)) {
qDebug() << "UnixSignalWatcher: socketpair: " << ::strerror(errno);
return;
}
// Create a notifier for the read end of the pair
notifier = new QSocketNotifier(sockpair[1], QSocketNotifier::Read);
QObject::connect(notifier, SIGNAL(activated(int)), q, SLOT(_q_onNotify(int)));
notifier->setEnabled(true);
}
UnixSignalWatcherPrivate::~UnixSignalWatcherPrivate()
{
delete notifier;
}
/*!
* Registers a handler for the given Unix \a signal. The handler will write to
* a socket pair, the other end of which is connected to a QSocketNotifier.
* This provides a way to break out of the asynchronous context from which the
* signal handler is called and back into the Qt event loop.
*/
void UnixSignalWatcherPrivate::watchForSignal(int signal)
{
if (watchedSignals.contains(signal)) {
qDebug() << "Already watching for signal" << signal;
return;
}
// Register a sigaction which will write to the socket pair
struct sigaction sigact;
sigact.sa_handler = UnixSignalWatcherPrivate::signalHandler;
sigact.sa_flags = 0;
::sigemptyset(&sigact.sa_mask);
sigact.sa_flags |= SA_RESTART;
if (::sigaction(signal, &sigact, NULL)) {
qDebug() << "UnixSignalWatcher: sigaction: " << ::strerror(errno);
return;
}
watchedSignals.append(signal);
}
/*!
* Called when a Unix \a signal is received. Write to the socket to wake up the
* QSocketNotifier.
*/
void UnixSignalWatcherPrivate::signalHandler(int signal)
{
ssize_t nBytes = ::write(sockpair[0], &signal, sizeof(signal));
Q_UNUSED(nBytes);
}
/*!
* Called when the signal handler has written to the socket pair. Emits the Unix
* signal as a Qt signal.
*/
void UnixSignalWatcherPrivate::_q_onNotify(int sockfd)
{
Q_Q(UnixSignalWatcher);
int signal;
ssize_t nBytes = ::read(sockfd, &signal, sizeof(signal));
Q_UNUSED(nBytes);
qDebug() << "Caught signal:" << ::strsignal(signal);
emit q->unixSignal(signal);
}
/*!
* Create a new UnixSignalWatcher as a child of the given \a parent.
*/
UnixSignalWatcher::UnixSignalWatcher(QObject *parent) :
QObject(parent),
d_ptr(new UnixSignalWatcherPrivate(this))
{
}
/*!
* Destroy this UnixSignalWatcher.
*/
UnixSignalWatcher::~UnixSignalWatcher()
{
delete d_ptr;
}
/*!
* Register a signal handler for the given \a signal.
*
* After calling this method you can \c connect() to the unixSignal() Qt signal
* to be notified when the Unix signal is received.
*/
void UnixSignalWatcher::watchForSignal(int signal)
{
Q_D(UnixSignalWatcher);
d->watchForSignal(signal);
}
/*!
* \fn void UnixSignalWatcher::unixSignal(int signal)
* Emitted when the given Unix \a signal is received.
*
* watchForSignal() must be called for each Unix signal that you want to receive
* via the unixSignal() Qt signal. If a watcher is watching multiple signals,
* unixSignal() will be emitted whenever *any* of the watched Unix signals are
* received, and the \a signal argument can be inspected to find out which one
* was actually received.
*/
#include "moc_sigwatch.cpp"

View File

@@ -1,59 +0,0 @@
/*
* Unix signal watcher for Qt.
*
* Copyright (C) 2014 Simon Knopp
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#ifndef SIGWATCH_H
#define SIGWATCH_H
#include <QObject>
#include <signal.h>
class UnixSignalWatcherPrivate;
/*!
* \brief The UnixSignalWatcher class converts Unix signals to Qt signals.
*
* To watch for a given signal, e.g. \c SIGINT, call \c watchForSignal(SIGINT)
* and \c connect() your handler to unixSignal().
*/
class UnixSignalWatcher : public QObject
{
Q_OBJECT
public:
explicit UnixSignalWatcher(QObject *parent = 0);
~UnixSignalWatcher();
void watchForSignal(int signal);
signals:
void unixSignal(int signal);
private:
UnixSignalWatcherPrivate * const d_ptr;
Q_DECLARE_PRIVATE(UnixSignalWatcher)
Q_PRIVATE_SLOT(d_func(), void _q_onNotify(int))
};
#endif // SIGWATCH_H

View File

@@ -1,11 +0,0 @@
[Unit]
Description=GlobalProtect openconnect DBus service
[Service]
Environment="LANG=en_US.utf8"
Type=dbus
BusName=com.yuezk.qt.GPService
ExecStart=/usr/bin/gpservice
[Install]
WantedBy=multi-user.target

View File

@@ -1,5 +0,0 @@
TEMPLATE = subdirs
SUBDIRS += \
GPClient \
GPService

148
README.md
View File

@@ -1,57 +1,137 @@
# GlobalProtect-openconnect
A GlobalProtect VPN client (GUI) for Linux based on Openconnect and built with Qt5, supports SAML auth mode, inspired by [gp-saml-gui](https://github.com/dlenski/gp-saml-gui).
A GUI for GlobalProtect VPN, based on OpenConnect, supports the SSO authentication method. Inspired by [gp-saml-gui](https://github.com/dlenski/gp-saml-gui).
<p align="center">
<img src="screenshot.png">
<img width="300" src="https://github.com/yuezk/GlobalProtect-openconnect/assets/3297602/9242df9c-217d-42ab-8c21-8f9f69cd4eb5">
</p>
## Features
- Similar user experience as the offical client in macOS.
- Supports both SAML and non-SAML authentication modes.
- Supports automatically selecting the preferred gateway from the multiple gateways.
- Supports switching gateway manually.
- [x] Better Linux support
- [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 multiple portals
- [x] Support gateway selection
- [x] Support connect gateway directly
- [x] Support auto-connect on startup
- [x] Support system tray icon
## Prerequisites
## Usage
- Openconnect v8.x
- Qt5, qt5-webengine, qt5-websockets
### CLI
### Ubuntu
1. Install openconnect v8.x
The CLI version is always free and open source in this repo. It has almost the same features as the GUI version.
For Ubuntu 18.04 you might need to [build the latest openconnect from source code](https://gist.github.com/yuezk/ab9a4b87a9fa0182bdb2df41fab5f613).
2. Install the Qt dependencies
```sh
sudo apt install qt5-default libqt5websockets5-dev qtwebengine5-dev
```
### OpenSUSE
Install the Qt dependencies
```
Usage: gpclient [OPTIONS] <COMMAND>
```sh
sudo zypper install libqt5-qtbase-devel libqt5-qtwebsockets-devel libqt5-qtwebengine-devel
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.
```
## Install
### GUI
### Install from AUR (Arch/Manjaro)
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.
Install [globalprotect-openconnect](https://aur.archlinux.org/packages/globalprotect-openconnect/).
> [!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.
### Build from source code
## Installation
```sh
git clone https://github.com/yuezk/GlobalProtect-openconnect.git
cd GlobalProtect-openconnect
git submodule update --init
> [!Note]
>
> This instruction is for the 2.x version. The 1.x version is still available on the [1.x](https://github.com/yuezk/GlobalProtect-openconnect/tree/1.x) branch, you can build it from the source code by following the instructions in the `README.md` file.
> [!Warning]
>
> The client requires `openconnect >= 8.20`, please make sure you have it installed, you can check it with `openconnect --version`.
> Installing the client from PPA will automatically install the required version of `openconnect`.
### Debian/Ubuntu based distributions
#### Install from PPA
# qmake or qmake-qt5
qmake CONFIG+=release
make
sudo make install
```
Open `GlobalProtect VPN` in the application dashboard.
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`.
#### Install from deb package
Download the latest deb package from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. Then install it with `dpkg`:
```bash
sudo dpkg -i globalprotect-openconnect_*.deb
```
### 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/OpenSUSE/CentOS/RHEL
#### 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
```
#### Install from OBS
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.
#### Install from RPM package
Download the latest RPM package from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page.
### Other distributions
The project depends on `openconnect >= 8.20`, `webkit2gtk`, `libsecret`, `libayatana-appindicator` or `libappindicator-gtk3`. You can install them first and then download the latest binary release (i.e., `*.bin.tar.gz`) from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page.
## About Trial
The CLI version is always free, while the GUI version is paid. There 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)
GPLv3

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

@@ -0,0 +1,23 @@
[package]
name = "gpauth"
version.workspace = true
edition.workspace = true
license.workspace = true
[build-dependencies]
tauri-build = { version = "1.5", features = [] }
[dependencies]
gpapi = { path = "../../crates/gpapi", features = ["tauri", "clap"] }
anyhow.workspace = true
clap.workspace = true
env_logger.workspace = true
log.workspace = true
regex.workspace = true
serde_json.workspace = true
tokio.workspace = true
tokio-util.workspace = true
tempfile.workspace = true
webkit2gtk = "0.18.2"
tauri = { workspace = true, features = ["http-all"] }
compile-time.workspace = true

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

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

Binary file not shown.

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

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

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

View File

@@ -0,0 +1,479 @@
use std::{
rc::Rc,
sync::Arc,
time::{Duration, Instant},
};
use anyhow::bail;
use gpapi::{
auth::SamlAuthData,
gp_params::GpParams,
portal::{prelogin, Prelogin},
utils::{redact::redact_uri, window::WindowExt},
};
use log::{info, warn};
use regex::Regex;
use tauri::{AppHandle, Window, WindowEvent, WindowUrl};
use tokio::sync::{mpsc, oneshot, RwLock};
use tokio_util::sync::CancellationToken;
use webkit2gtk::{
gio::Cancellable,
glib::{GString, TimeSpan},
LoadEvent, SettingsExt, TLSErrorsPolicy, URIResponse, URIResponseExt, WebContextExt, WebResource, WebResourceExt,
WebView, WebViewExt, WebsiteDataManagerExtManual, WebsiteDataTypes,
};
enum AuthDataError {
/// Failed to load page due to TLS error
TlsError,
/// 1. Found auth data in headers/body but it's invalid
/// 2. Loaded an empty page, failed to load page. etc.
Invalid,
/// No auth data found in headers/body
NotFound,
}
type AuthResult = Result<SamlAuthData, AuthDataError>;
pub(crate) struct AuthWindow<'a> {
app_handle: AppHandle,
server: &'a str,
saml_request: &'a str,
user_agent: &'a str,
gp_params: Option<GpParams>,
clean: bool,
}
impl<'a> AuthWindow<'a> {
pub fn new(app_handle: AppHandle) -> Self {
Self {
app_handle,
server: "",
saml_request: "",
user_agent: "",
gp_params: None,
clean: false,
}
}
pub fn server(mut self, server: &'a str) -> Self {
self.server = server;
self
}
pub fn saml_request(mut self, saml_request: &'a str) -> Self {
self.saml_request = saml_request;
self
}
pub fn user_agent(mut self, user_agent: &'a str) -> Self {
self.user_agent = user_agent;
self
}
pub fn gp_params(mut self, gp_params: GpParams) -> Self {
self.gp_params.replace(gp_params);
self
}
pub fn clean(mut self, clean: bool) -> Self {
self.clean = clean;
self
}
pub async fn open(&self) -> anyhow::Result<SamlAuthData> {
info!("Open auth window, user_agent: {}", self.user_agent);
let window = Window::builder(&self.app_handle, "auth_window", WindowUrl::default())
.title("GlobalProtect Login")
// .user_agent(self.user_agent)
.focused(true)
.visible(false)
.center()
.build()?;
let window = Arc::new(window);
let cancel_token = CancellationToken::new();
let cancel_token_clone = cancel_token.clone();
window.on_window_event(move |event| {
if let WindowEvent::CloseRequested { .. } = event {
cancel_token_clone.cancel();
}
});
let window_clone = Arc::clone(&window);
let timeout_secs = 15;
tokio::spawn(async move {
tokio::time::sleep(Duration::from_secs(timeout_secs)).await;
let visible = window_clone.is_visible().unwrap_or(false);
if !visible {
info!("Try to raise auth window after {} seconds", timeout_secs);
raise_window(&window_clone);
}
});
tokio::select! {
_ = cancel_token.cancelled() => {
bail!("Auth cancelled");
}
saml_result = self.auth_loop(&window) => {
window.close()?;
saml_result
}
}
}
async fn auth_loop(&self, window: &Arc<Window>) -> anyhow::Result<SamlAuthData> {
let saml_request = self.saml_request.to_string();
let (auth_result_tx, mut auth_result_rx) = mpsc::unbounded_channel::<AuthResult>();
let raise_window_cancel_token: Arc<RwLock<Option<CancellationToken>>> = Default::default();
let gp_params = self.gp_params.as_ref().unwrap();
let tls_err_policy = if gp_params.ignore_tls_errors() {
TLSErrorsPolicy::Ignore
} else {
TLSErrorsPolicy::Fail
};
if self.clean {
clear_webview_cookies(window).await?;
}
let raise_window_cancel_token_clone = Arc::clone(&raise_window_cancel_token);
window.with_webview(move |wv| {
let wv = wv.inner();
if let Some(context) = wv.context() {
context.set_tls_errors_policy(tls_err_policy);
}
if let Some(settings) = wv.settings() {
let ua = settings.user_agent().unwrap_or("".into());
info!("Auth window user agent: {}", ua);
}
// Load the initial SAML request
load_saml_request(&wv, &saml_request);
let auth_result_tx_clone = auth_result_tx.clone();
wv.connect_load_changed(move |wv, event| {
if event == LoadEvent::Started {
let Ok(mut cancel_token) = raise_window_cancel_token_clone.try_write() else {
return;
};
// Cancel the raise window task
if let Some(cancel_token) = cancel_token.take() {
cancel_token.cancel();
}
return;
}
if event != LoadEvent::Finished {
return;
}
if let Some(main_resource) = wv.main_resource() {
let uri = main_resource.uri().unwrap_or("".into());
if uri.is_empty() {
warn!("Loaded an empty uri");
send_auth_result(&auth_result_tx_clone, Err(AuthDataError::Invalid));
return;
}
info!("Loaded uri: {}", redact_uri(&uri));
read_auth_data(&main_resource, auth_result_tx_clone.clone());
}
});
let auth_result_tx_clone = auth_result_tx.clone();
wv.connect_load_failed_with_tls_errors(move |_wv, uri, cert, err| {
let redacted_uri = redact_uri(uri);
warn!(
"Failed to load uri: {} with error: {}, cert: {}",
redacted_uri, err, cert
);
send_auth_result(&auth_result_tx_clone, Err(AuthDataError::TlsError));
true
});
wv.connect_load_failed(move |_wv, _event, uri, err| {
let redacted_uri = redact_uri(uri);
warn!("Failed to load uri: {} with error: {}", redacted_uri, err);
// NOTE: Don't send error here, since load_changed event will be triggered after this
// send_auth_result(&auth_result_tx, Err(AuthDataError::Invalid));
// true to stop other handlers from being invoked for the event. false to propagate the event further.
true
});
})?;
let portal = self.server.to_string();
loop {
if let Some(auth_result) = auth_result_rx.recv().await {
match auth_result {
Ok(auth_data) => return Ok(auth_data),
Err(AuthDataError::TlsError) => 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(AuthResult) + Send + 'static,
{
main_resource.data(Cancellable::NONE, |data| match data {
Ok(data) => {
let html = String::from_utf8_lossy(&data);
callback(read_auth_data_from_html(&html));
}
Err(err) => {
info!("Failed to read response body: {}", err);
callback(Err(AuthDataError::Invalid))
}
});
}
fn read_auth_data_from_html(html: &str) -> AuthResult {
if html.contains("Temporarily Unavailable") {
info!("Found 'Temporarily Unavailable' in HTML, auth failed");
return Err(AuthDataError::Invalid);
}
match parse_xml_tag(html, "saml-auth-status") {
Some(saml_status) if saml_status == "1" => {
let username = parse_xml_tag(html, "saml-username");
let prelogin_cookie = parse_xml_tag(html, "prelogin-cookie");
let portal_userauthcookie = parse_xml_tag(html, "portal-userauthcookie");
if SamlAuthData::check(&username, &prelogin_cookie, &portal_userauthcookie) {
return Ok(SamlAuthData::new(
username.unwrap(),
prelogin_cookie,
portal_userauthcookie,
));
}
info!("Found invalid auth data in HTML");
Err(AuthDataError::Invalid)
}
Some(status) => {
info!("Found invalid SAML status {} in HTML", status);
Err(AuthDataError::Invalid)
}
None => {
info!("No auth data found in HTML");
Err(AuthDataError::NotFound)
}
}
}
fn read_auth_data(main_resource: &WebResource, auth_result_tx: mpsc::UnboundedSender<AuthResult>) {
if main_resource.response().is_none() {
info!("No response found in main resource");
send_auth_result(&auth_result_tx, Err(AuthDataError::Invalid));
return;
}
let response = main_resource.response().unwrap();
info!("Trying to read auth data from response headers...");
match read_auth_data_from_headers(&response) {
Ok(auth_data) => {
info!("Got auth data from headers");
send_auth_result(&auth_result_tx, Ok(auth_data));
}
Err(AuthDataError::Invalid) => {
info!("Found invalid auth data in headers, trying to read from body...");
read_auth_data_from_body(main_resource, move |auth_result| {
// Since we have already found invalid auth data in headers, which means this could be the `/SAML20/SP/ACS` endpoint
// any error result from body should be considered as invalid, and trigger a retry
let auth_result = auth_result.map_err(|_| AuthDataError::Invalid);
send_auth_result(&auth_result_tx, auth_result);
});
}
Err(AuthDataError::NotFound) => {
info!("No auth data found in headers, trying to read from body...");
read_auth_data_from_body(main_resource, move |auth_result| {
send_auth_result(&auth_result_tx, auth_result)
});
}
Err(AuthDataError::TlsError) => {
// NOTE: This is unreachable
info!("TLS error found in headers, trying to read from body...");
send_auth_result(&auth_result_tx, Err(AuthDataError::TlsError));
}
}
}
fn parse_xml_tag(html: &str, tag: &str) -> Option<String> {
let re = Regex::new(&format!("<{}>(.*)</{}>", tag, tag)).unwrap();
re.captures(html)
.and_then(|captures| captures.get(1))
.map(|m| m.as_str().to_string())
}
pub(crate) async fn clear_webview_cookies(window: &Window) -> anyhow::Result<()> {
let (tx, rx) = oneshot::channel::<Result<(), String>>();
window.with_webview(|wv| {
let send_result = move |result: Result<(), String>| {
if let Err(err) = tx.send(result) {
info!("Failed to send result: {:?}", err);
}
};
let wv = wv.inner();
let context = match wv.context() {
Some(context) => context,
None => {
send_result(Err("No webview context found".into()));
return;
}
};
let data_manager = match context.website_data_manager() {
Some(manager) => manager,
None => {
send_result(Err("No data manager found".into()));
return;
}
};
let now = Instant::now();
data_manager.clear(
WebsiteDataTypes::COOKIES,
TimeSpan(0),
Cancellable::NONE,
move |result| match result {
Err(err) => {
send_result(Err(err.to_string()));
}
Ok(_) => {
info!("Cookies cleared in {} ms", now.elapsed().as_millis());
send_result(Ok(()));
}
},
);
})?;
rx.await?.map_err(|err| anyhow::anyhow!(err))
}

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

@@ -0,0 +1,162 @@
use clap::Parser;
use gpapi::{
auth::{SamlAuthData, SamlAuthResult},
clap::args::Os,
gp_params::{ClientOs, GpParams},
utils::{normalize_server, openssl},
GP_USER_AGENT,
};
use log::{info, LevelFilter};
use serde_json::json;
use tauri::{App, AppHandle, RunEvent};
use tempfile::NamedTempFile;
use crate::auth_window::{portal_prelogin, AuthWindow};
const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), " (", compile_time::date_str!(), ")");
#[derive(Parser, Clone)]
#[command(version = VERSION)]
struct Cli {
server: String,
#[arg(long)]
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,
}
impl Cli {
async fn run(&mut self) -> anyhow::Result<()> {
if self.ignore_tls_errors {
info!("TLS errors will be ignored");
}
let mut openssl_conf = self.prepare_env()?;
self.server = normalize_server(&self.server)?;
let gp_params = self.build_gp_params();
// Get the initial SAML request
let saml_request = match self.saml_request {
Some(ref saml_request) => saml_request.clone(),
None => portal_prelogin(&self.server, &gp_params).await?,
};
self.saml_request.replace(saml_request);
let app = create_app(self.clone())?;
app.run(move |_app_handle, event| {
if let RunEvent::Exit = event {
if let Some(file) = openssl_conf.take() {
if let Err(err) = file.close() {
info!("Error closing OpenSSL config file: {}", err);
}
}
}
});
Ok(())
}
fn prepare_env(&self) -> anyhow::Result<Option<NamedTempFile>> {
std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1");
if self.hidpi {
info!("Setting GDK_SCALE=2 and GDK_DPI_SCALE=0.5");
std::env::set_var("GDK_SCALE", "2");
std::env::set_var("GDK_DPI_SCALE", "0.5");
}
if self.fix_openssl {
info!("Fixing OpenSSL environment");
let file = openssl::fix_openssl_env()?;
return Ok(Some(file));
}
Ok(None)
}
fn build_gp_params(&self) -> GpParams {
let gp_params = GpParams::builder()
.user_agent(&self.user_agent)
.client_os(ClientOs::from(&self.os))
.os_version(self.os_version.clone())
.ignore_tls_errors(self.ignore_tls_errors)
.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
View File

@@ -0,0 +1,9 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod auth_window;
mod cli;
#[tokio::main]
async fn main() {
cli::run().await;
}

View File

@@ -0,0 +1,47 @@
{
"$schema": "https://cdn.jsdelivr.net/gh/tauri-apps/tauri@tauri-v1.5.0/tooling/cli/schema.json",
"build": {
"distDir": [
"index.html"
],
"devPath": [
"index.html"
],
"beforeDevCommand": "",
"beforeBuildCommand": "",
"withGlobalTauri": false
},
"package": {
"productName": "gpauth",
"version": "0.0.0"
},
"tauri": {
"allowlist": {
"all": false,
"http": {
"all": true,
"request": true,
"scope": [
"http://**",
"https://**"
]
}
},
"bundle": {
"active": true,
"targets": "deb",
"identifier": "com.yuezk.gpauth",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
},
"security": {
"csp": null
},
"windows": []
}
}

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

@@ -0,0 +1,23 @@
[package]
name = "gpclient"
authors.workspace = true
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
gpapi = { path = "../../crates/gpapi", features = ["clap"] }
openconnect = { path = "../../crates/openconnect" }
anyhow.workspace = true
clap.workspace = true
env_logger.workspace = true
inquire = "0.6.2"
log.workspace = true
tokio.workspace = true
sysinfo.workspace = true
serde_json.workspace = true
whoami.workspace = true
tempfile.workspace = true
reqwest.workspace = true
directories = "5.0"
compile-time.workspace = true

119
apps/gpclient/src/cli.rs Normal file
View 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);
}
}

View File

@@ -0,0 +1,213 @@
use std::{fs, sync::Arc};
use clap::Args;
use gpapi::{
clap::args::Os,
credential::{Credential, PasswordCredential},
gateway::gateway_login,
gp_params::{ClientOs, GpParams},
portal::{prelogin, retrieve_config, PortalError, Prelogin},
process::auth_launcher::SamlAuthLauncher,
utils::shutdown_signal,
GP_USER_AGENT,
};
use inquire::{Password, PasswordDisplayMode, Select, Text};
use log::info;
use openconnect::Vpn;
use crate::{cli::SharedArgs, GP_CLIENT_LOCK_FILE};
#[derive(Args)]
pub(crate) struct ConnectArgs {
#[arg(help = "The portal server to connect to")]
server: String,
#[arg(short, long, help = "The gateway to connect to, it will prompt if not specified")]
gateway: Option<String>,
#[arg(short, long, help = "The username to use, it will prompt if not specified")]
user: Option<String>,
#[arg(long, short, help = "The VPNC script to use")]
script: Option<String>,
#[arg(long, default_value = GP_USER_AGENT, help = "The user agent to use")]
user_agent: String,
#[arg(long, default_value = "Linux")]
os: Os,
#[arg(long)]
os_version: Option<String>,
#[arg(long, help = "The HiDPI mode, useful for high resolution screens")]
hidpi: bool,
#[arg(long, help = "Do not reuse the remembered authentication cookie")]
clean: bool,
}
impl ConnectArgs {
fn os_version(&self) -> String {
if let Some(os_version) = &self.os_version {
return os_version.to_owned();
}
match self.os {
Os::Linux => format!("Linux {}", whoami::distro()),
Os::Windows => String::from("Microsoft Windows 11 Pro , 64-bit"),
Os::Mac => String::from("Apple Mac OS X 13.4.0"),
}
}
}
pub(crate) struct ConnectHandler<'a> {
args: &'a ConnectArgs,
shared_args: &'a SharedArgs,
}
impl<'a> ConnectHandler<'a> {
pub(crate) fn new(args: &'a ConnectArgs, shared_args: &'a SharedArgs) -> Self {
Self { args, shared_args }
}
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)
.build()
}
pub(crate) async fn handle(&self) -> anyhow::Result<()> {
let server = self.args.server.as_str();
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...");
return self.connect_gateway_with_prelogin(server).await;
}
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 {}", gateway))?,
None => {
portal_config.sort_gateways(prelogin.region());
let gateways = portal_config.gateways();
if gateways.len() > 1 {
Select::new("Which gateway do you want to connect to?", gateways)
.with_vim_mode(true)
.prompt()?
} else {
gateways[0]
}
}
};
let gateway = selected_gateway.server();
let cred = portal_config.auth_cookie().into();
let cookie = match gateway_login(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<()> {
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 = gateway_login(gateway, &cred, &gp_params).await?;
self.connect_gateway(gateway, &cookie).await
}
async fn connect_gateway(&self, gateway: &str, cookie: &str) -> anyhow::Result<()> {
let vpn = Vpn::builder(gateway, cookie)
.user_agent(self.args.user_agent.clone())
.script(self.args.script.clone())
.build();
let vpn = Arc::new(vpn);
let vpn_clone = vpn.clone();
// Listen for the interrupt signal in the background
tokio::spawn(async move {
shutdown_signal().await;
info!("Received the interrupt signal, disconnecting...");
vpn_clone.disconnect();
});
vpn.connect(write_pid_file);
if fs::metadata(GP_CLIENT_LOCK_FILE).is_ok() {
info!("Removing PID file");
fs::remove_file(GP_CLIENT_LOCK_FILE)?;
}
Ok(())
}
async fn obtain_credential(&self, prelogin: &Prelogin, server: &str) -> anyhow::Result<Credential> {
let is_gateway = prelogin.is_gateway();
match prelogin {
Prelogin::Saml(prelogin) => {
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)
.launch()
.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())
}
}
}
}
fn write_pid_file() {
let pid = std::process::id();
fs::write(GP_CLIENT_LOCK_FILE, pid.to_string()).unwrap();
info!("Wrote PID {} to {}", pid, GP_CLIENT_LOCK_FILE);
}

View File

@@ -0,0 +1,31 @@
use crate::GP_CLIENT_LOCK_FILE;
use log::{info, warn};
use std::fs;
use sysinfo::{Pid, ProcessExt, Signal, System, SystemExt};
pub(crate) struct DisconnectHandler;
impl DisconnectHandler {
pub(crate) fn new() -> Self {
Self
}
pub(crate) fn handle(&self) -> anyhow::Result<()> {
if fs::metadata(GP_CLIENT_LOCK_FILE).is_err() {
warn!("PID file not found, maybe the client is not running");
return Ok(());
}
let pid = fs::read_to_string(GP_CLIENT_LOCK_FILE)?;
let pid = pid.trim().parse::<usize>()?;
let s = System::new_all();
if let Some(process) = s.process(Pid::from(pid)) {
info!("Found process {}, killing...", pid);
if process.kill_with(Signal::Interrupt).is_none() {
warn!("Failed to kill process {}", pid);
}
}
Ok(())
}
}

View File

@@ -0,0 +1,112 @@
use std::{collections::HashMap, fs, path::PathBuf};
use clap::Args;
use directories::ProjectDirs;
use gpapi::{
process::service_launcher::ServiceLauncher,
utils::{endpoint::http_endpoint, env_file, shutdown_signal},
};
use log::info;
#[derive(Args)]
pub(crate) struct LaunchGuiArgs {
#[arg(
required = false,
help = "The authentication data, used for the default browser authentication"
)]
auth_data: Option<String>,
#[arg(long, help = "Launch the GUI minimized")]
minimized: bool,
}
pub(crate) struct LaunchGuiHandler<'a> {
args: &'a LaunchGuiArgs,
}
impl<'a> LaunchGuiHandler<'a> {
pub(crate) fn new(args: &'a LaunchGuiArgs) -> Self {
Self { args }
}
pub(crate) async fn handle(&self) -> anyhow::Result<()> {
// `launch-gui`cannot be run as root
let user = whoami::username();
if user == "root" {
anyhow::bail!("`launch-gui` cannot be run as root");
}
let auth_data = self.args.auth_data.as_deref().unwrap_or_default();
if !auth_data.is_empty() {
// Process the authentication data, its format is `globalprotectcallback:<data>`
return feed_auth_data(auth_data).await;
}
if try_active_gui().await.is_ok() {
info!("The GUI is already running");
return Ok(());
}
tokio::spawn(async move {
shutdown_signal().await;
info!("Shutting down...");
});
let log_file = get_log_file()?;
let log_file_path = log_file.to_string_lossy().to_string();
info!("Log file: {}", log_file_path);
let mut extra_envs = HashMap::<String, String>::new();
extra_envs.insert("GP_LOG_FILE".into(), log_file_path.clone());
// Persist the environment variables to a file
let env_file = env_file::persist_env_vars(Some(extra_envs))?;
let env_file = env_file.into_temp_path();
let env_file_path = env_file.to_string_lossy().to_string();
let exit_status = ServiceLauncher::new()
.minimized(self.args.minimized)
.env_file(&env_file_path)
.log_file(&log_file_path)
.launch()
.await?;
info!("Service exited with status: {}", exit_status);
Ok(())
}
}
async fn feed_auth_data(auth_data: &str) -> anyhow::Result<()> {
let service_endpoint = http_endpoint().await?;
reqwest::Client::default()
.post(format!("{}/auth-data", service_endpoint))
.json(&auth_data)
.send()
.await?
.error_for_status()?;
Ok(())
}
async fn try_active_gui() -> anyhow::Result<()> {
let service_endpoint = http_endpoint().await?;
reqwest::Client::default()
.post(format!("{}/active-gui", service_endpoint))
.send()
.await?
.error_for_status()?;
Ok(())
}
pub fn get_log_file() -> anyhow::Result<PathBuf> {
let dirs = ProjectDirs::from("com.yuezk", "GlobalProtect-openconnect", "gpclient")
.ok_or_else(|| anyhow::anyhow!("Failed to get project dirs"))?;
fs::create_dir_all(dirs.data_dir())?;
Ok(dirs.data_dir().join("gpclient.log"))
}

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

@@ -0,0 +1,11 @@
mod cli;
mod connect;
mod disconnect;
mod launch_gui;
pub(crate) const GP_CLIENT_LOCK_FILE: &str = "/var/run/gpclient.lock";
#[tokio::main]
async fn main() {
cli::run().await;
}

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

@@ -0,0 +1,19 @@
[package]
name = "gpservice"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
gpapi = { path = "../../crates/gpapi" }
openconnect = { path = "../../crates/openconnect" }
clap.workspace = true
anyhow.workspace = true
tokio.workspace = true
tokio-util.workspace = true
axum = { workspace = true, features = ["ws"] }
futures.workspace = true
serde_json.workspace = true
env_logger.workspace = true
log.workspace = true
compile-time.workspace = true

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

@@ -0,0 +1,165 @@
use std::sync::Arc;
use std::{collections::HashMap, io::Write};
use anyhow::bail;
use clap::Parser;
use gpapi::{
process::gui_launcher::GuiLauncher,
service::{request::WsRequest, vpn_state::VpnState},
utils::{crypto::generate_key, env_file, lock_file::LockFile, redact::Redaction, shutdown_signal},
GP_SERVICE_LOCK_FILE,
};
use log::{info, warn, LevelFilter};
use tokio::sync::{mpsc, watch};
use crate::{vpn_task::VpnTask, ws_server::WsServer};
const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), " (", compile_time::date_str!(), ")");
#[derive(Parser)]
#[command(version = VERSION)]
struct Cli {
#[clap(long)]
minimized: bool,
#[clap(long)]
env_file: Option<String>,
#[cfg(debug_assertions)]
#[clap(long)]
no_gui: bool,
}
impl Cli {
async fn run(&mut self, redaction: Arc<Redaction>) -> anyhow::Result<()> {
let lock_file = Arc::new(LockFile::new(GP_SERVICE_LOCK_FILE));
if lock_file.check_health().await {
bail!("Another instance of the service is already running");
}
let api_key = self.prepare_api_key();
// Channel for sending requests to the VPN task
let (ws_req_tx, ws_req_rx) = mpsc::channel::<WsRequest>(32);
// Channel for receiving the VPN state from the VPN task
let (vpn_state_tx, vpn_state_rx) = watch::channel(VpnState::Disconnected);
let mut vpn_task = VpnTask::new(ws_req_rx, vpn_state_tx);
let ws_server = WsServer::new(api_key.clone(), ws_req_tx, vpn_state_rx, lock_file.clone(), redaction);
let (shutdown_tx, mut shutdown_rx) = mpsc::channel::<()>(4);
let shutdown_tx_clone = shutdown_tx.clone();
let vpn_task_token = vpn_task.cancel_token();
let server_token = ws_server.cancel_token();
let vpn_task_handle = tokio::spawn(async move { vpn_task.start(server_token).await });
let ws_server_handle = tokio::spawn(async move { ws_server.start(shutdown_tx_clone).await });
#[cfg(debug_assertions)]
let no_gui = self.no_gui;
#[cfg(not(debug_assertions))]
let no_gui = false;
if no_gui {
info!("GUI is disabled");
} else {
let envs = self.env_file.as_ref().map(env_file::load_env_vars).transpose()?;
let minimized = self.minimized;
tokio::spawn(async move {
launch_gui(envs, api_key, minimized).await;
let _ = shutdown_tx.send(()).await;
});
}
tokio::select! {
_ = shutdown_signal() => {
info!("Shutdown signal received");
}
_ = shutdown_rx.recv() => {
info!("Shutdown request received, shutting down");
}
}
vpn_task_token.cancel();
let _ = tokio::join!(vpn_task_handle, ws_server_handle);
lock_file.unlock()?;
info!("gpservice stopped");
Ok(())
}
fn prepare_api_key(&self) -> Vec<u8> {
#[cfg(debug_assertions)]
if self.no_gui {
return gpapi::GP_API_KEY.to_vec();
}
generate_key().to_vec()
}
}
fn init_logger() -> Arc<Redaction> {
let redaction = Arc::new(Redaction::new());
let redaction_clone = Arc::clone(&redaction);
// let target = Box::new(File::create("log.txt").expect("Can't create file"));
env_logger::builder()
.filter_level(LevelFilter::Info)
.format(move |buf, record| {
let timestamp = buf.timestamp();
writeln!(
buf,
"[{} {} {}] {}",
timestamp,
record.level(),
record.module_path().unwrap_or_default(),
redaction_clone.redact_str(&record.args().to_string())
)
})
// .target(env_logger::Target::Pipe(target))
.init();
redaction
}
async fn launch_gui(envs: Option<HashMap<String, String>>, api_key: Vec<u8>, mut minimized: bool) {
loop {
let api_key_clone = api_key.clone();
let gui_launcher = GuiLauncher::new()
.envs(envs.clone())
.api_key(api_key_clone)
.minimized(minimized);
match gui_launcher.launch().await {
Ok(exit_status) => {
// Exit code 99 means that the GUI needs to be restarted
if exit_status.code() != Some(99) {
info!("GUI exited with code {:?}", exit_status.code());
break;
}
info!("GUI exited with code 99, restarting");
minimized = false;
}
Err(err) => {
warn!("Failed to launch GUI: {}", err);
break;
}
}
}
}
pub async fn run() {
let mut cli = Cli::parse();
let redaction = init_logger();
info!("gpservice started: {}", VERSION);
if let Err(e) = cli.run(redaction).await {
eprintln!("Error: {}", e);
std::process::exit(1);
}
}

View File

@@ -0,0 +1,95 @@
use std::{borrow::Cow, ops::ControlFlow, sync::Arc};
use axum::{
extract::{
ws::{self, CloseFrame, Message, WebSocket},
State, WebSocketUpgrade,
},
response::IntoResponse,
};
use futures::{SinkExt, StreamExt};
use gpapi::service::event::WsEvent;
use log::{info, warn};
use crate::ws_server::WsServerContext;
pub(crate) async fn health() -> impl IntoResponse {
"OK"
}
pub(crate) async fn active_gui(State(ctx): State<Arc<WsServerContext>>) -> impl IntoResponse {
ctx.send_event(WsEvent::ActiveGui).await;
}
pub(crate) async fn auth_data(State(ctx): State<Arc<WsServerContext>>, body: String) -> impl IntoResponse {
ctx.send_event(WsEvent::AuthData(body)).await;
}
pub(crate) async fn ws_handler(ws: WebSocketUpgrade, State(ctx): State<Arc<WsServerContext>>) -> impl IntoResponse {
ws.on_upgrade(move |socket| handle_socket(socket, ctx))
}
async fn handle_socket(mut socket: WebSocket, ctx: Arc<WsServerContext>) {
// Send ping message
if let Err(err) = socket.send(Message::Ping("Hi".into())).await {
warn!("Failed to send ping: {}", err);
return;
}
// Wait for pong message
if socket.recv().await.is_none() {
warn!("Failed to receive pong");
return;
}
info!("New client connected");
let (mut sender, mut receiver) = socket.split();
let (connection, mut msg_rx) = ctx.add_connection().await;
let send_task = tokio::spawn(async move {
while let Some(msg) = msg_rx.recv().await {
if let Err(err) = sender.send(msg).await {
info!("Failed to send message: {}", err);
break;
}
}
let close_msg = Message::Close(Some(CloseFrame {
code: ws::close_code::NORMAL,
reason: Cow::from("Goodbye"),
}));
if let Err(err) = sender.send(close_msg).await {
warn!("Failed to close socket: {}", err);
}
});
let conn = Arc::clone(&connection);
let ctx_clone = Arc::clone(&ctx);
let recv_task = tokio::spawn(async move {
while let Some(Ok(msg)) = receiver.next().await {
let ControlFlow::Continue(ws_req) = conn.recv_msg(msg) else {
break;
};
if let Err(err) = ctx_clone.forward_req(ws_req).await {
info!("Failed to forward request: {}", err);
break;
}
}
});
tokio::select! {
_ = send_task => {
info!("WS server send task completed");
},
_ = recv_task => {
info!("WS server recv task completed");
}
}
info!("Client disconnected");
ctx.remove_connection(connection).await;
}

View File

@@ -0,0 +1,11 @@
mod cli;
mod handlers;
mod routes;
mod vpn_task;
mod ws_connection;
mod ws_server;
#[tokio::main]
async fn main() {
cli::run().await;
}

View File

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

View File

@@ -0,0 +1,144 @@
use std::{sync::Arc, thread};
use gpapi::service::{
request::{ConnectRequest, WsRequest},
vpn_state::VpnState,
};
use log::info;
use openconnect::Vpn;
use tokio::sync::{mpsc, oneshot, watch, RwLock};
use tokio_util::sync::CancellationToken;
pub(crate) struct VpnTaskContext {
vpn_handle: Arc<RwLock<Option<Vpn>>>,
vpn_state_tx: Arc<watch::Sender<VpnState>>,
disconnect_rx: RwLock<Option<oneshot::Receiver<()>>>,
}
impl VpnTaskContext {
pub fn new(vpn_state_tx: watch::Sender<VpnState>) -> Self {
Self {
vpn_handle: Default::default(),
vpn_state_tx: Arc::new(vpn_state_tx),
disconnect_rx: Default::default(),
}
}
pub async fn connect(&self, req: ConnectRequest) {
let vpn_state = self.vpn_state_tx.borrow().clone();
if !matches!(vpn_state, VpnState::Disconnected) {
info!("VPN is not disconnected, ignore the request");
return;
}
let info = req.info().clone();
let vpn_handle = self.vpn_handle.clone();
let args = req.args();
let vpn = Vpn::builder(req.gateway().server(), args.cookie())
.user_agent(args.user_agent())
.script(args.vpnc_script())
.os(args.openconnect_os())
.build();
// Save the VPN handle
vpn_handle.write().await.replace(vpn);
let vpn_state_tx = self.vpn_state_tx.clone();
let connect_info = Box::new(info.clone());
vpn_state_tx.send(VpnState::Connecting(connect_info)).ok();
let (disconnect_tx, disconnect_rx) = oneshot::channel::<()>();
self.disconnect_rx.write().await.replace(disconnect_rx);
// Spawn a new thread to process the VPN connection, cannot use tokio::spawn here.
// Otherwise, it will block the tokio runtime and cannot send the VPN state to the channel
thread::spawn(move || {
let vpn_state_tx_clone = vpn_state_tx.clone();
vpn_handle.blocking_read().as_ref().map(|vpn| {
vpn.connect(move || {
let connect_info = Box::new(info.clone());
vpn_state_tx.send(VpnState::Connected(connect_info)).ok();
})
});
// Notify the VPN is disconnected
vpn_state_tx_clone.send(VpnState::Disconnected).ok();
// Remove the VPN handle
vpn_handle.blocking_write().take();
disconnect_tx.send(()).ok();
});
}
pub async fn disconnect(&self) {
if let Some(disconnect_rx) = self.disconnect_rx.write().await.take() {
if let Some(vpn) = self.vpn_handle.read().await.as_ref() {
self.vpn_state_tx.send(VpnState::Disconnecting).ok();
vpn.disconnect()
}
// Wait for the VPN to be disconnected
disconnect_rx.await.ok();
info!("VPN disconnected");
} else {
info!("VPN is not connected, skip disconnect");
self.vpn_state_tx.send(VpnState::Disconnected).ok();
}
}
}
pub(crate) struct VpnTask {
ws_req_rx: mpsc::Receiver<WsRequest>,
ctx: Arc<VpnTaskContext>,
cancel_token: CancellationToken,
}
impl VpnTask {
pub fn new(ws_req_rx: mpsc::Receiver<WsRequest>, vpn_state_tx: watch::Sender<VpnState>) -> Self {
let ctx = Arc::new(VpnTaskContext::new(vpn_state_tx));
let cancel_token = CancellationToken::new();
Self {
ws_req_rx,
ctx,
cancel_token,
}
}
pub fn cancel_token(&self) -> CancellationToken {
self.cancel_token.clone()
}
pub async fn start(&mut self, server_cancel_token: CancellationToken) {
let cancel_token = self.cancel_token.clone();
tokio::select! {
_ = self.recv() => {
info!("VPN task stopped");
}
_ = cancel_token.cancelled() => {
info!("VPN task cancelled");
self.ctx.disconnect().await;
}
}
server_cancel_token.cancel();
}
async fn recv(&mut self) {
while let Some(req) = self.ws_req_rx.recv().await {
tokio::spawn(process_ws_req(req, self.ctx.clone()));
}
}
}
async fn process_ws_req(req: WsRequest, ctx: Arc<VpnTaskContext>) {
match req {
WsRequest::Connect(req) => {
ctx.connect(*req).await;
}
WsRequest::Disconnect(_) => {
ctx.disconnect().await;
}
}
}

View File

@@ -0,0 +1,53 @@
use std::{ops::ControlFlow, sync::Arc};
use axum::extract::ws::{CloseFrame, Message};
use gpapi::{
service::{event::WsEvent, request::WsRequest},
utils::crypto::Crypto,
};
use log::{info, warn};
use tokio::sync::mpsc;
pub(crate) struct WsConnection {
crypto: Arc<Crypto>,
tx: mpsc::Sender<Message>,
}
impl WsConnection {
pub fn new(crypto: Arc<Crypto>, tx: mpsc::Sender<Message>) -> Self {
Self { crypto, tx }
}
pub async fn send_event(&self, event: &WsEvent) -> anyhow::Result<()> {
let encrypted = self.crypto.encrypt(event)?;
let msg = Message::Binary(encrypted);
self.tx.send(msg).await?;
Ok(())
}
pub fn recv_msg(&self, msg: Message) -> ControlFlow<(), WsRequest> {
match msg {
Message::Binary(data) => match self.crypto.decrypt(data) {
Ok(ws_req) => ControlFlow::Continue(ws_req),
Err(err) => {
info!("Failed to decrypt message: {}", err);
ControlFlow::Break(())
}
},
Message::Close(cf) => {
if let Some(CloseFrame { code, reason }) = cf {
info!("Client sent close, code {} and reason `{}`", code, reason);
} else {
info!("Client somehow sent close message without CloseFrame");
}
ControlFlow::Break(())
}
_ => {
warn!("WS server received unexpected message: {:?}", msg);
ControlFlow::Break(())
}
}
}
}

View File

@@ -0,0 +1,153 @@
use std::sync::Arc;
use axum::extract::ws::Message;
use gpapi::{
service::{event::WsEvent, request::WsRequest, vpn_state::VpnState},
utils::{crypto::Crypto, lock_file::LockFile, redact::Redaction},
};
use log::{info, warn};
use tokio::{
net::TcpListener,
sync::{mpsc, watch, RwLock},
};
use tokio_util::sync::CancellationToken;
use crate::{routes, ws_connection::WsConnection};
pub(crate) struct WsServerContext {
crypto: Arc<Crypto>,
ws_req_tx: mpsc::Sender<WsRequest>,
vpn_state_rx: watch::Receiver<VpnState>,
redaction: Arc<Redaction>,
connections: RwLock<Vec<Arc<WsConnection>>>,
}
impl WsServerContext {
pub fn new(
api_key: Vec<u8>,
ws_req_tx: mpsc::Sender<WsRequest>,
vpn_state_rx: watch::Receiver<VpnState>,
redaction: Arc<Redaction>,
) -> Self {
Self {
crypto: Arc::new(Crypto::new(api_key)),
ws_req_tx,
vpn_state_rx,
redaction,
connections: Default::default(),
}
}
pub async fn send_event(&self, event: WsEvent) {
let connections = self.connections.read().await;
for conn in connections.iter() {
let _ = conn.send_event(&event).await;
}
}
pub async fn add_connection(&self) -> (Arc<WsConnection>, mpsc::Receiver<Message>) {
let (tx, rx) = mpsc::channel::<Message>(32);
let conn = Arc::new(WsConnection::new(Arc::clone(&self.crypto), tx));
// Send current VPN state to new client
info!("Sending current VPN state to new client");
let vpn_state = self.vpn_state_rx.borrow().clone();
if let Err(err) = conn.send_event(&WsEvent::VpnState(vpn_state)).await {
warn!("Failed to send VPN state to new client: {}", err);
}
self.connections.write().await.push(Arc::clone(&conn));
(conn, rx)
}
pub async fn remove_connection(&self, conn: Arc<WsConnection>) {
let mut connections = self.connections.write().await;
connections.retain(|c| !Arc::ptr_eq(c, &conn));
}
fn vpn_state_rx(&self) -> watch::Receiver<VpnState> {
self.vpn_state_rx.clone()
}
pub async fn forward_req(&self, req: WsRequest) -> anyhow::Result<()> {
if let WsRequest::Connect(ref req) = req {
self
.redaction
.add_values(&[req.gateway().server(), req.args().cookie()])?
}
self.ws_req_tx.send(req).await?;
Ok(())
}
}
pub(crate) struct WsServer {
ctx: Arc<WsServerContext>,
cancel_token: CancellationToken,
lock_file: Arc<LockFile>,
}
impl WsServer {
pub fn new(
api_key: Vec<u8>,
ws_req_tx: mpsc::Sender<WsRequest>,
vpn_state_rx: watch::Receiver<VpnState>,
lock_file: Arc<LockFile>,
redaction: Arc<Redaction>,
) -> Self {
let ctx = Arc::new(WsServerContext::new(api_key, ws_req_tx, vpn_state_rx, redaction));
let cancel_token = CancellationToken::new();
Self {
ctx,
cancel_token,
lock_file,
}
}
pub fn cancel_token(&self) -> CancellationToken {
self.cancel_token.clone()
}
pub async fn start(&self, shutdown_tx: mpsc::Sender<()>) {
if let Ok(listener) = TcpListener::bind("127.0.0.1:0").await {
let local_addr = listener.local_addr().unwrap();
self.lock_file.lock(local_addr.port().to_string()).unwrap();
info!("WS server listening on port: {}", local_addr.port());
tokio::select! {
_ = watch_vpn_state(self.ctx.vpn_state_rx(), Arc::clone(&self.ctx)) => {
info!("VPN state watch task completed");
}
_ = start_server(listener, self.ctx.clone()) => {
info!("WS server stopped");
}
_ = self.cancel_token.cancelled() => {
info!("WS server cancelled");
}
}
}
let _ = shutdown_tx.send(()).await;
}
}
async fn watch_vpn_state(mut vpn_state_rx: watch::Receiver<VpnState>, ctx: Arc<WsServerContext>) {
while vpn_state_rx.changed().await.is_ok() {
let vpn_state = vpn_state_rx.borrow().clone();
ctx.send_event(WsEvent::VpnState(vpn_state)).await;
}
}
async fn start_server(listener: TcpListener, ctx: Arc<WsServerContext>) -> anyhow::Result<()> {
let routes = routes::routes(ctx);
axum::serve(listener, routes).await?;
Ok(())
}

36
crates/gpapi/Cargo.toml Normal file
View File

@@ -0,0 +1,36 @@
[package]
name = "gpapi"
version.workspace = true
edition.workspace = true
license = "MIT"
[dependencies]
anyhow.workspace = true
base64.workspace = true
log.workspace = true
reqwest.workspace = true
roxmltree.workspace = true
serde.workspace = true
specta.workspace = true
specta-macros.workspace = true
urlencoding.workspace = true
tokio.workspace = true
serde_json.workspace = true
whoami.workspace = true
tempfile.workspace = true
thiserror.workspace = true
chacha20poly1305 = { version = "0.10", features = ["std"] }
redact-engine.workspace = true
url.workspace = true
regex.workspace = true
dotenvy_macro.workspace = true
uzers.workspace = true
tauri = { workspace = true, optional = true }
clap = { workspace = true, optional = true }
open = { version = "5", optional = true }
[features]
tauri = ["dep:tauri"]
clap = ["dep:clap"]
browser-auth = ["dep:open"]

90
crates/gpapi/src/auth.rs Normal file
View File

@@ -0,0 +1,90 @@
use anyhow::bail;
use regex::Regex;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SamlAuthData {
username: String,
prelogin_cookie: Option<String>,
portal_userauthcookie: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum SamlAuthResult {
Success(SamlAuthData),
Failure(String),
}
impl SamlAuthResult {
pub fn is_success(&self) -> bool {
match self {
SamlAuthResult::Success(_) => true,
SamlAuthResult::Failure(_) => false,
}
}
}
impl SamlAuthData {
pub fn new(username: String, prelogin_cookie: Option<String>, portal_userauthcookie: Option<String>) -> Self {
Self {
username,
prelogin_cookie,
portal_userauthcookie,
}
}
pub fn parse_html(html: &str) -> anyhow::Result<SamlAuthData> {
match parse_xml_tag(html, "saml-auth-status") {
Some(saml_status) if saml_status == "1" => {
let username = parse_xml_tag(html, "saml-username");
let prelogin_cookie = parse_xml_tag(html, "prelogin-cookie");
let portal_userauthcookie = parse_xml_tag(html, "portal-userauthcookie");
if SamlAuthData::check(&username, &prelogin_cookie, &portal_userauthcookie) {
return Ok(SamlAuthData::new(
username.unwrap(),
prelogin_cookie,
portal_userauthcookie,
));
}
bail!("Found invalid auth data in HTML");
}
Some(status) => {
bail!("Found invalid SAML status {} in HTML", status);
}
None => {
bail!("No auth data found in HTML");
}
}
}
pub fn username(&self) -> &str {
&self.username
}
pub fn prelogin_cookie(&self) -> Option<&str> {
self.prelogin_cookie.as_deref()
}
pub fn check(
username: &Option<String>,
prelogin_cookie: &Option<String>,
portal_userauthcookie: &Option<String>,
) -> bool {
let username_valid = username.as_ref().is_some_and(|username| !username.is_empty());
let prelogin_cookie_valid = prelogin_cookie.as_ref().is_some_and(|val| val.len() > 5);
let portal_userauthcookie_valid = portal_userauthcookie.as_ref().is_some_and(|val| val.len() > 5);
username_valid && (prelogin_cookie_valid || portal_userauthcookie_valid)
}
}
pub fn parse_xml_tag(html: &str, tag: &str) -> Option<String> {
let re = Regex::new(&format!("<{}>(.*)</{}>", tag, tag)).unwrap();
re.captures(html)
.and_then(|captures| captures.get(1))
.map(|m| m.as_str().to_string())
}

View File

@@ -0,0 +1,64 @@
use clap::{builder::PossibleValue, ValueEnum};
use crate::gp_params::ClientOs;
#[derive(Debug, Clone)]
pub enum Os {
Linux,
Windows,
Mac,
}
impl Os {
pub fn as_str(&self) -> &'static str {
match self {
Os::Linux => "Linux",
Os::Windows => "Windows",
Os::Mac => "Mac",
}
}
}
impl From<&str> for Os {
fn from(os: &str) -> Self {
match os.to_lowercase().as_str() {
"linux" => Os::Linux,
"windows" => Os::Windows,
"mac" => Os::Mac,
_ => Os::Linux,
}
}
}
impl From<&Os> for ClientOs {
fn from(value: &Os) -> Self {
match value {
Os::Linux => ClientOs::Linux,
Os::Windows => ClientOs::Windows,
Os::Mac => ClientOs::Mac,
}
}
}
impl ValueEnum for Os {
fn value_variants<'a>() -> &'a [Self] {
&[Os::Linux, Os::Windows, Os::Mac]
}
fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
match self {
Os::Linux => Some(PossibleValue::new("Linux")),
Os::Windows => Some(PossibleValue::new("Windows")),
Os::Mac => Some(PossibleValue::new("Mac")),
}
}
fn from_str(input: &str, _: bool) -> Result<Self, String> {
match input.to_lowercase().as_str() {
"linux" => Ok(Os::Linux),
"windows" => Ok(Os::Windows),
"mac" => Ok(Os::Mac),
_ => Err(format!("Invalid OS: {}", input)),
}
}
}

View File

@@ -0,0 +1 @@
pub mod args;

View File

@@ -0,0 +1,247 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use specta::Type;
use crate::{auth::SamlAuthData, utils::base64::decode_to_string};
#[derive(Debug, Serialize, Deserialize, Type, Clone)]
#[serde(rename_all = "camelCase")]
pub struct PasswordCredential {
username: String,
password: String,
}
impl PasswordCredential {
pub fn new(username: &str, password: &str) -> Self {
Self {
username: username.to_string(),
password: password.to_string(),
}
}
pub fn username(&self) -> &str {
&self.username
}
pub fn password(&self) -> &str {
&self.password
}
}
impl From<&CachedCredential> for PasswordCredential {
fn from(value: &CachedCredential) -> Self {
Self::new(value.username(), value.password().unwrap_or_default())
}
}
#[derive(Debug, Serialize, Deserialize, Type, Clone)]
#[serde(rename_all = "camelCase")]
pub struct PreloginCookieCredential {
username: String,
prelogin_cookie: String,
}
impl PreloginCookieCredential {
pub fn new(username: &str, prelogin_cookie: &str) -> Self {
Self {
username: username.to_string(),
prelogin_cookie: prelogin_cookie.to_string(),
}
}
pub fn username(&self) -> &str {
&self.username
}
pub fn prelogin_cookie(&self) -> &str {
&self.prelogin_cookie
}
}
impl TryFrom<SamlAuthData> for PreloginCookieCredential {
type Error = anyhow::Error;
fn try_from(value: SamlAuthData) -> Result<Self, Self::Error> {
let username = value.username().to_string();
let prelogin_cookie = value
.prelogin_cookie()
.ok_or_else(|| anyhow::anyhow!("Missing prelogin cookie"))?
.to_string();
Ok(Self::new(&username, &prelogin_cookie))
}
}
#[derive(Debug, Serialize, Deserialize, Type, Clone)]
#[serde(rename_all = "camelCase")]
pub struct AuthCookieCredential {
username: String,
user_auth_cookie: String,
prelogon_user_auth_cookie: String,
}
impl AuthCookieCredential {
pub fn new(username: &str, user_auth_cookie: &str, prelogon_user_auth_cookie: &str) -> Self {
Self {
username: username.to_string(),
user_auth_cookie: user_auth_cookie.to_string(),
prelogon_user_auth_cookie: prelogon_user_auth_cookie.to_string(),
}
}
pub fn username(&self) -> &str {
&self.username
}
pub fn user_auth_cookie(&self) -> &str {
&self.user_auth_cookie
}
pub fn prelogon_user_auth_cookie(&self) -> &str {
&self.prelogon_user_auth_cookie
}
}
#[derive(Debug, Serialize, Deserialize, Type, Clone)]
#[serde(rename_all = "camelCase")]
pub struct CachedCredential {
username: String,
password: Option<String>,
auth_cookie: AuthCookieCredential,
}
impl CachedCredential {
pub fn new(username: String, password: Option<String>, auth_cookie: AuthCookieCredential) -> Self {
Self {
username,
password,
auth_cookie,
}
}
pub fn username(&self) -> &str {
&self.username
}
pub fn password(&self) -> Option<&str> {
self.password.as_deref()
}
pub fn auth_cookie(&self) -> &AuthCookieCredential {
&self.auth_cookie
}
pub fn set_auth_cookie(&mut self, auth_cookie: AuthCookieCredential) {
self.auth_cookie = auth_cookie;
}
pub fn set_username(&mut self, username: String) {
self.username = username;
}
pub fn set_password(&mut self, password: Option<String>) {
self.password = password.map(|s| s.to_string());
}
}
impl From<PasswordCredential> for CachedCredential {
fn from(value: PasswordCredential) -> Self {
Self::new(
value.username().to_owned(),
Some(value.password().to_owned()),
AuthCookieCredential::new("", "", ""),
)
}
}
#[derive(Debug, Serialize, Deserialize, Type, Clone)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum Credential {
Password(PasswordCredential),
PreloginCookie(PreloginCookieCredential),
AuthCookie(AuthCookieCredential),
CachedCredential(CachedCredential),
}
impl Credential {
/// Create a credential from a globalprotectcallback:<base64 encoded string>
pub fn parse_gpcallback(auth_data: &str) -> anyhow::Result<Self> {
// Remove the surrounding quotes
let auth_data = auth_data.trim_matches('"');
let auth_data = auth_data.trim_start_matches("globalprotectcallback:");
let auth_data = decode_to_string(auth_data)?;
let auth_data = SamlAuthData::parse_html(&auth_data)?;
Self::try_from(auth_data)
}
pub fn username(&self) -> &str {
match self {
Credential::Password(cred) => cred.username(),
Credential::PreloginCookie(cred) => cred.username(),
Credential::AuthCookie(cred) => cred.username(),
Credential::CachedCredential(cred) => cred.username(),
}
}
pub fn to_params(&self) -> HashMap<&str, &str> {
let mut params = HashMap::new();
params.insert("user", self.username());
let (passwd, prelogin_cookie, portal_userauthcookie, portal_prelogonuserauthcookie) = match self {
Credential::Password(cred) => (Some(cred.password()), None, None, None),
Credential::PreloginCookie(cred) => (None, Some(cred.prelogin_cookie()), None, None),
Credential::AuthCookie(cred) => (
None,
None,
Some(cred.user_auth_cookie()),
Some(cred.prelogon_user_auth_cookie()),
),
Credential::CachedCredential(cred) => (
cred.password(),
None,
Some(cred.auth_cookie.user_auth_cookie()),
Some(cred.auth_cookie.prelogon_user_auth_cookie()),
),
};
params.insert("passwd", passwd.unwrap_or_default());
params.insert("prelogin-cookie", prelogin_cookie.unwrap_or_default());
params.insert("portal-userauthcookie", portal_userauthcookie.unwrap_or_default());
params.insert(
"portal-prelogonuserauthcookie",
portal_prelogonuserauthcookie.unwrap_or_default(),
);
params
}
}
impl TryFrom<SamlAuthData> for Credential {
type Error = anyhow::Error;
fn try_from(value: SamlAuthData) -> Result<Self, Self::Error> {
let prelogin_cookie = PreloginCookieCredential::try_from(value)?;
Ok(Self::PreloginCookie(prelogin_cookie))
}
}
impl From<PasswordCredential> for Credential {
fn from(value: PasswordCredential) -> Self {
Self::Password(value)
}
}
impl From<&AuthCookieCredential> for Credential {
fn from(value: &AuthCookieCredential) -> Self {
Self::AuthCookie(value.clone())
}
}
impl From<&CachedCredential> for Credential {
fn from(value: &CachedCredential) -> Self {
Self::CachedCredential(value.clone())
}
}

View File

@@ -0,0 +1,74 @@
use anyhow::bail;
use log::info;
use reqwest::Client;
use roxmltree::Document;
use urlencoding::encode;
use crate::{
credential::Credential,
gp_params::GpParams,
utils::{normalize_server, remove_url_scheme},
};
pub async fn gateway_login(gateway: &str, cred: &Credential, gp_params: &GpParams) -> anyhow::Result<String> {
let url = normalize_server(gateway)?;
let gateway = remove_url_scheme(&url);
let login_url = format!("{}/ssl-vpn/login.esp", url);
let client = Client::builder()
.danger_accept_invalid_certs(gp_params.ignore_tls_errors())
.user_agent(gp_params.user_agent())
.build()?;
let mut params = cred.to_params();
let extra_params = gp_params.to_params();
params.extend(extra_params);
params.insert("server", &gateway);
info!("Gateway login, user_agent: {}", gp_params.user_agent());
let res = client.post(&login_url).form(&params).send().await?;
let status = res.status();
if status.is_client_error() || status.is_server_error() {
bail!("Gateway login error: {}", status)
}
let res_xml = res.text().await?;
let doc = Document::parse(&res_xml)?;
build_gateway_token(&doc, gp_params.computer())
}
fn build_gateway_token(doc: &Document, computer: &str) -> anyhow::Result<String> {
let args = doc
.descendants()
.filter(|n| n.has_tag_name("argument"))
.map(|n| n.text().unwrap_or("").to_string())
.collect::<Vec<_>>();
let params = [
read_args(&args, 1, "authcookie")?,
read_args(&args, 3, "portal")?,
read_args(&args, 4, "user")?,
read_args(&args, 7, "domain")?,
read_args(&args, 15, "preferred-ip")?,
("computer", computer),
];
let token = params
.iter()
.map(|(k, v)| format!("{}={}", k, encode(v)))
.collect::<Vec<_>>()
.join("&");
Ok(token)
}
fn read_args<'a>(args: &'a [String], index: usize, key: &'a str) -> anyhow::Result<(&'a str, &'a str)> {
args
.get(index)
.ok_or_else(|| anyhow::anyhow!("Failed to read {key} from args"))
.map(|s| (key, s.as_ref()))
}

View File

@@ -0,0 +1,50 @@
mod login;
mod parse_gateways;
pub use login::*;
pub(crate) use parse_gateways::*;
use serde::{Deserialize, Serialize};
use specta::Type;
use std::fmt::Display;
#[derive(Debug, Serialize, Deserialize, Type, Clone)]
pub(crate) struct PriorityRule {
pub(crate) name: String,
pub(crate) priority: u32,
}
#[derive(Debug, Serialize, Deserialize, Type, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Gateway {
pub(crate) name: String,
pub(crate) address: String,
pub(crate) priority: u32,
pub(crate) priority_rules: Vec<PriorityRule>,
}
impl Display for Gateway {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} ({})", self.name, self.address)
}
}
impl Gateway {
pub fn new(name: String, address: String) -> Self {
Self {
name,
address,
priority: 0,
priority_rules: vec![],
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn server(&self) -> &str {
&self.address
}
}

View File

@@ -0,0 +1,61 @@
use roxmltree::Document;
use super::{Gateway, PriorityRule};
pub(crate) fn parse_gateways(doc: &Document) -> Option<Vec<Gateway>> {
let node_gateways = doc.descendants().find(|n| n.has_tag_name("gateways"))?;
let list_gateway = node_gateways.descendants().find(|n| n.has_tag_name("list"))?;
let gateways = list_gateway
.children()
.filter_map(|gateway_item| {
if !gateway_item.has_tag_name("entry") {
return None;
}
let address = gateway_item.attribute("name").unwrap_or("").to_string();
let name = gateway_item
.children()
.find(|n| n.has_tag_name("description"))
.and_then(|n| n.text())
.unwrap_or("")
.to_string();
let priority = gateway_item
.children()
.find(|n| n.has_tag_name("priority"))
.and_then(|n| n.text())
.and_then(|s| s.parse().ok())
.unwrap_or(u32::MAX);
let priority_rules = gateway_item
.children()
.find(|n| n.has_tag_name("priority-rule"))
.map(|n| {
n.children()
.filter_map(|n| {
if !n.has_tag_name("entry") {
return None;
}
let name = n.attribute("name").unwrap_or("").to_string();
let priority: u32 = n
.children()
.find(|n| n.has_tag_name("priority"))
.and_then(|n| n.text())
.and_then(|s| s.parse().ok())
.unwrap_or(u32::MAX);
Some(PriorityRule { name, priority })
})
.collect()
})
.unwrap_or_default();
Some(Gateway {
name,
address,
priority,
priority_rules,
})
})
.collect();
Some(gateways)
}

View File

@@ -0,0 +1,197 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use specta::Type;
use crate::GP_USER_AGENT;
#[derive(Debug, Serialize, Deserialize, Clone, Type, Default)]
pub enum ClientOs {
#[default]
Linux,
Windows,
Mac,
}
impl From<&str> for ClientOs {
fn from(os: &str) -> Self {
match os {
"Linux" => ClientOs::Linux,
"Windows" => ClientOs::Windows,
"Mac" => ClientOs::Mac,
_ => ClientOs::Linux,
}
}
}
impl ClientOs {
pub fn as_str(&self) -> &str {
match self {
ClientOs::Linux => "Linux",
ClientOs::Windows => "Windows",
ClientOs::Mac => "Mac",
}
}
pub fn to_openconnect_os(&self) -> &str {
match self {
ClientOs::Linux => "linux",
ClientOs::Windows => "win",
ClientOs::Mac => "mac-intel",
}
}
}
#[derive(Debug, Serialize, Deserialize, Type, Default)]
pub struct GpParams {
is_gateway: bool,
user_agent: String,
client_os: ClientOs,
os_version: Option<String>,
client_version: Option<String>,
computer: String,
ignore_tls_errors: bool,
prefer_default_browser: bool,
}
impl GpParams {
pub fn builder() -> GpParamsBuilder {
GpParamsBuilder::new()
}
pub(crate) fn is_gateway(&self) -> bool {
self.is_gateway
}
pub fn set_is_gateway(&mut self, is_gateway: bool) {
self.is_gateway = is_gateway;
}
pub(crate) fn user_agent(&self) -> &str {
&self.user_agent
}
pub(crate) fn computer(&self) -> &str {
&self.computer
}
pub fn ignore_tls_errors(&self) -> bool {
self.ignore_tls_errors
}
pub fn prefer_default_browser(&self) -> bool {
self.prefer_default_browser
}
pub(crate) fn to_params(&self) -> HashMap<&str, &str> {
let mut params: HashMap<&str, &str> = HashMap::new();
let client_os = self.client_os.as_str();
// Common params
params.insert("prot", "https:");
params.insert("jnlpReady", "jnlpReady");
params.insert("ok", "Login");
params.insert("direct", "yes");
params.insert("ipv6-support", "yes");
params.insert("inputStr", "");
params.insert("clientVer", "4100");
params.insert("clientos", client_os);
params.insert("computer", &self.computer);
if let Some(os_version) = &self.os_version {
params.insert("os-version", os_version);
}
// NOTE: Do not include clientgpversion for now
// if let Some(client_version) = &self.client_version {
// params.insert("clientgpversion", client_version);
// }
params
}
}
pub struct GpParamsBuilder {
is_gateway: bool,
user_agent: String,
client_os: ClientOs,
os_version: Option<String>,
client_version: Option<String>,
computer: String,
ignore_tls_errors: bool,
prefer_default_browser: bool,
}
impl GpParamsBuilder {
pub fn new() -> Self {
Self {
is_gateway: false,
user_agent: GP_USER_AGENT.to_string(),
client_os: ClientOs::Linux,
os_version: Default::default(),
client_version: Default::default(),
computer: whoami::hostname(),
ignore_tls_errors: false,
prefer_default_browser: false,
}
}
pub fn is_gateway(&mut self, is_gateway: bool) -> &mut Self {
self.is_gateway = is_gateway;
self
}
pub fn user_agent(&mut self, user_agent: &str) -> &mut Self {
self.user_agent = user_agent.to_string();
self
}
pub fn client_os(&mut self, client_os: ClientOs) -> &mut Self {
self.client_os = client_os;
self
}
pub fn os_version<T: Into<Option<String>>>(&mut self, os_version: T) -> &mut Self {
self.os_version = os_version.into();
self
}
pub fn client_version<T: Into<Option<String>>>(&mut self, client_version: T) -> &mut Self {
self.client_version = client_version.into();
self
}
pub fn computer(&mut self, computer: &str) -> &mut Self {
self.computer = computer.to_string();
self
}
pub fn ignore_tls_errors(&mut self, ignore_tls_errors: bool) -> &mut Self {
self.ignore_tls_errors = ignore_tls_errors;
self
}
pub fn prefer_default_browser(&mut self, prefer_default_browser: bool) -> &mut Self {
self.prefer_default_browser = prefer_default_browser;
self
}
pub fn build(&self) -> GpParams {
GpParams {
is_gateway: self.is_gateway,
user_agent: self.user_agent.clone(),
client_os: self.client_os.clone(),
os_version: self.os_version.clone(),
client_version: self.client_version.clone(),
computer: self.computer.clone(),
ignore_tls_errors: self.ignore_tls_errors,
prefer_default_browser: self.prefer_default_browser,
}
}
}
impl Default for GpParamsBuilder {
fn default() -> Self {
Self::new()
}
}

35
crates/gpapi/src/lib.rs Normal file
View File

@@ -0,0 +1,35 @@
pub mod auth;
pub mod credential;
pub mod gateway;
pub mod gp_params;
pub mod portal;
pub mod process;
pub mod service;
pub mod utils;
#[cfg(feature = "clap")]
pub mod clap;
#[cfg(debug_assertions)]
pub const GP_API_KEY: &[u8; 32] = &[0; 32];
pub const GP_USER_AGENT: &str = "PAN GlobalProtect";
pub const GP_SERVICE_LOCK_FILE: &str = "/var/run/gpservice.lock";
#[cfg(not(debug_assertions))]
pub const GP_CLIENT_BINARY: &str = "/usr/bin/gpclient";
#[cfg(not(debug_assertions))]
pub const GP_SERVICE_BINARY: &str = "/usr/bin/gpservice";
#[cfg(not(debug_assertions))]
pub const GP_GUI_BINARY: &str = "/usr/bin/gpgui";
#[cfg(not(debug_assertions))]
pub(crate) const GP_AUTH_BINARY: &str = "/usr/bin/gpauth";
#[cfg(debug_assertions)]
pub const GP_CLIENT_BINARY: &str = dotenvy_macro::dotenv!("GP_CLIENT_BINARY");
#[cfg(debug_assertions)]
pub const GP_SERVICE_BINARY: &str = dotenvy_macro::dotenv!("GP_SERVICE_BINARY");
#[cfg(debug_assertions)]
pub const GP_GUI_BINARY: &str = dotenvy_macro::dotenv!("GP_GUI_BINARY");
#[cfg(debug_assertions)]
pub(crate) const GP_AUTH_BINARY: &str = dotenvy_macro::dotenv!("GP_AUTH_BINARY");

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