Compare commits

..

256 Commits

Author SHA1 Message Date
Kevin Yue
6119976027 Release 2.3.0 2024-05-20 21:31:26 +08:00
Kevin Yue
a286b5e418 feat: improve client certificate authentication 2024-05-20 09:08:47 -04:00
Kevin Yue
882ab4001d chore: improve error message 2024-05-19 22:30:40 +08:00
Kevin Yue
52b6fa6fbd feat: support client certificate authentication (related #363) 2024-05-19 18:44:07 +08:00
Kevin Yue
3bb115bd2d Merge branch 'main' into dev 2024-05-19 10:23:00 +08:00
Kevin Yue
e08f239176 fix: do not panic when failed to start service (fix #362) 2024-05-19 10:21:18 +08:00
Kevin Yue
a01c55e38d fix: do not panic when failed to start service (fix #362) 2024-05-19 10:19:21 +08:00
Kevin Yue
af51bc257b feat: add the --reconnect-timeout option 2024-05-19 09:59:25 +08:00
Kevin Yue
90a8c11acb feat: add disable_ipv6 option (related #364) 2024-05-19 09:04:45 +08:00
Kevin Yue
92b858884c fix: check executable for file 2024-05-10 10:26:45 -04:00
Kevin Yue
159673652c Refactor prelogin.rs to use default labels for username and password 2024-05-09 01:48:02 -04:00
Kevin Yue
200d13ef15 Release 2.2.1 2024-05-07 11:58:15 -04:00
Kevin Yue
ddeef46d2e Restore the browser auth, related #360 2024-05-07 11:40:44 -04:00
Dr. Larry D. Pyeatt
97c3998383 Install instructions for Gentoo (#352)
* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

Add install instructions for Gentoo
2024-05-06 19:00:17 +08:00
Kevin Yue
93aea4ee60 doc: using the default browser for CLI 2024-04-30 18:47:38 +08:00
Kevin Yue
546dbf542e Update README.md 2024-04-30 13:28:20 +08:00
Kevin Yue
005410d40b Update README.md 2024-04-30 13:19:52 +08:00
Kevin Yue
3b384a199a Update changelog 2024-04-29 21:56:50 -04:00
Kevin Yue
b62b024a8b Release 2.2.0 2024-04-29 21:05:36 -04:00
Kevin Yue
4fbd373e29 chore: update logging 2024-04-17 21:25:25 +08:00
Kevin Yue
ae211a923a refactor: refine the logging 2024-04-15 22:31:50 +08:00
Kevin Yue
d94d730a44 feat: support default browser for CLI (#345) 2024-04-15 20:27:33 +08:00
Kevin Yue
18ae1c5fa5 refactor: improve gp response parsing 2024-04-14 17:22:37 +08:00
Kevin Yue
a0afabeb04 Release 2.1.4 2024-04-10 10:13:37 -04:00
Kevin Yue
1158ab9095 Add MFA support 2024-04-10 10:07:37 -04:00
Kevin Yue
54ccb761e5 Fix CI 2024-04-07 09:42:00 -04:00
Kevin Yue
f72dbd1dec Release 2.1.3 2024-04-07 20:46:23 +08:00
Kevin Yue
0814c3153a Merge branch 'feature/as_gateway' into release/2.1.3 2024-04-07 20:44:29 +08:00
Kevin Yue
9f085e8b8c Improve code style 2024-04-07 20:31:05 +08:00
Kevin Yue
0188752c0a Bump version 2.1.3 2024-04-06 20:07:57 +08:00
Kevin Yue
a884c41813 Rename PreloginCredential 2024-04-06 19:40:08 +08:00
Kevin Yue
879b977321 Add message for the '--as-gateway' option 2024-04-06 19:26:42 +08:00
Kevin Yue
e9cb253be1 Update dependencies 2024-04-06 19:14:31 +08:00
Kevin Yue
07eacae385 Add '--as-gateway' option (#318) 2024-04-06 19:07:09 +08:00
Kevin Yue
8446874290 Decode extracted gpcallback 2024-04-05 18:01:09 +08:00
Kevin Yue
c347f97b95 Update vite 2024-04-04 18:34:58 +08:00
Kevin Yue
29cfa9e24b Polish authentication 2024-04-04 18:31:48 +08:00
Kevin Yue
1b1ce882a5 Update CI 2024-04-03 21:17:24 +08:00
Kevin Yue
e9f2dbf9ea Support CAS authentication 2024-04-03 06:40:40 -04:00
Kevin Yue
7c6ae315e1 Fix CI 2024-04-02 21:46:30 +08:00
Kevin Yue
cec0d22dc8 Support CAS authentication 2024-04-02 20:06:00 +08:00
Kevin Yue
b2ca82e105 Update changelog 2024-03-29 07:55:10 -04:00
Kevin Yue
5ba6b1d5fc Merge branch 'hotfix/handle_network_error' into release/2.1.2 2024-03-29 07:52:17 -04:00
Kevin Yue
a96e77c758 Bump version 2.1.2 2024-03-29 07:48:02 -04:00
Kevin Yue
79e0f0c7c1 Handle portal endpoint network error 2024-03-29 01:57:53 -04:00
Kevin Yue
187ca778f2 Release 2.1.1 2024-03-25 21:42:16 +08:00
Kevin Yue
2d1aa3ba8c Handle the gateway endpoint error
Related: #338
2024-03-25 21:03:54 +08:00
Kevin Yue
08bd4efefa Improve the error message
Related #327
2024-03-23 20:05:54 +08:00
Kevin Yue
558485f5a9 Add the --hip option 2024-03-17 18:41:42 +08:00
Kevin Yue
cff2ff9dbe Update dependencies 2024-03-16 21:24:41 +08:00
Kevin Yue
d5d92cfbee Ensure vpnc_script and csd_wrapper executable 2024-03-16 21:06:49 +08:00
Kevin Yue
a00f6a8cba Add vpnc_script location, fix #336 2024-03-16 12:05:09 +08:00
Kevin Yue
59dee3d767 Update packaging script 2024-03-11 07:55:49 -04:00
Kevin Yue
e94661b213 Fix build-depends 2024-03-10 08:32:35 -04:00
Kevin Yue
9dea81bdff Update CI 2024-03-10 16:31:18 +08:00
Kevin Yue
6ff552c1ec Update packaging 2024-03-05 08:12:26 -05:00
Kevin Yue
c1b1ea1a67 Update install instructions 2024-02-27 21:05:52 +08:00
Kevin Yue
167a8f4037 Release 2.1.0 2024-02-26 23:45:37 +08:00
Kevin Yue
47776d54d9 Improve packaging (#328)
* Add gpgui-helper (#326)

* Add packaging
2024-02-26 23:33:39 +08:00
Kevin Yue
5767c252b7 Update issue templates 2024-02-17 20:39:11 +08:00
Kevin Yue
a2efcada02 Update README.md 2024-02-13 04:07:18 -05:00
Kevin Yue
e68aa0ffa6 Update README.md 2024-02-13 03:24:20 -05:00
Kevin Yue
66bcccabe4 Add mtu option 2024-02-10 18:19:37 +08:00
Kevin Yue
3736189308 Retry auth if failed to obtain the auth cookie 2024-02-07 19:33:58 +08:00
Kevin Yue
c408482c55 Update install instruction 2024-02-06 20:30:57 +08:00
Kevin Yue
00b0b8eb84 Update README.md 2024-02-06 12:44:18 +08:00
Wesley vieira
b14294f131 update readme with the prerequisites (#313) 2024-02-06 12:43:26 +08:00
Kevin Yue
db9249bd61 Support HIP report (#309) 2024-02-05 18:35:45 +08:00
Kevin Yue
662e4d0b8a Support specify csd-wrapper 2024-02-03 13:12:17 +08:00
Kevin Yue
13be9179f5 Bump version 2.0.0 2024-02-01 22:41:36 +08:00
Kevin Yue
0a55506077 Do not error when region is not found 2024-02-01 21:52:31 +08:00
Kevin Yue
8860efa82e Simplify code 2024-01-29 07:54:58 -05:00
Kevin Yue
9bc0994a8e Update gpauth app icon 2024-01-29 06:10:34 -05:00
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
219 changed files with 17368 additions and 4817 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"
]
}

12
.editorconfig Normal file
View File

@@ -0,0 +1,12 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[{Makefile,Makefile.in}]
indent_style = tab

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

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

30
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,30 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Logs**
- For the GUI version, you can find the logs at `~/.local/share/gpclient/gpclient.log`
- For the CLI version, copy the output of the `gpclient` command.
**Environment:**
- OS: [e.g. Ubuntu 22.04]
- Desktop Environment: [e.g. GNOME or KDE]
- Output of `ps aux | grep 'gnome-keyring\|kwalletd5' | grep -v grep`: [Required for secure store error]
- Is remote SSH? [Yes/No]
**Additional context**
Add any other context about the problem here.

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

@@ -0,0 +1,188 @@
name: Build
on:
push:
paths-ignore:
- LICENSE
- "*.md"
- .vscode
- .devcontainer
branches:
- main
- dev
- hotfix/*
- feature/*
- release/*
tags:
- v*.*.*
jobs:
# Include arm64 if ref is a tag
setup-matrix:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Set up matrix
id: set-matrix
run: |
if [[ "${{ github.ref }}" == "refs/tags/"* ]]; then
echo 'matrix=[{"runner": "ubuntu-latest", "arch": "amd64"}, {"runner": "arm64", "arch": "arm64"}]' >> $GITHUB_OUTPUT
else
echo 'matrix=[{"runner": "ubuntu-latest", "arch": "amd64"}]' >> $GITHUB_OUTPUT
fi
tarball:
runs-on: ubuntu-latest
needs: [setup-matrix]
steps:
- uses: pnpm/action-setup@v2
with:
version: 8
- name: Prepare workspace
run: rm -rf source && mkdir source
- name: Checkout GlobalProtect-openconnect
uses: actions/checkout@v3
with:
token: ${{ secrets.GH_PAT }}
repository: yuezk/GlobalProtect-openconnect
ref: ${{ github.ref }}
path: source/gp
- name: Create tarball
run: |
cd source/gp
# Generate the SNAPSHOT file for non-tagged commits
if [[ "${{ github.ref }}" != "refs/tags/"* ]]; then
touch SNAPSHOT
fi
make tarball
- name: Upload tarball
uses: actions/upload-artifact@v3
with:
name: artifact-source
if-no-files-found: error
path: |
source/gp/.build/tarball/*.tar.gz
build-gp:
needs:
- setup-matrix
- tarball
strategy:
matrix:
os: ${{fromJson(needs.setup-matrix.outputs.matrix)}}
package: [deb, rpm, pkg, binary]
runs-on: ${{ matrix.os.runner }}
name: build-gp (${{ matrix.package }}, ${{ matrix.os.arch }})
steps:
- name: Prepare workspace
run: |
rm -rf build-gp-${{ matrix.package }}
mkdir -p build-gp-${{ matrix.package }}
- name: Download tarball
uses: actions/download-artifact@v3
with:
name: artifact-source
path: build-gp-${{ matrix.package }}
- name: Docker Login
run: echo ${{ secrets.DOCKER_HUB_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin
- name: Build ${{ matrix.package }} package in Docker
run: |
docker run --rm \
-v $(pwd)/build-gp-${{ matrix.package }}:/${{ matrix.package }} \
yuezk/gpdev:${{ matrix.package }}-builder
- name: Install ${{ matrix.package }} package in Docker
run: |
docker run --rm \
-e GPGUI_INSTALLED=0 \
-v $(pwd)/build-gp-${{ matrix.package }}:/${{ matrix.package }} \
yuezk/gpdev:${{ matrix.package }}-builder \
bash install.sh
- name: Upload ${{ matrix.package }} package
uses: actions/upload-artifact@v3
with:
name: artifact-gp-${{ matrix.package }}-${{ matrix.os.arch }}
if-no-files-found: error
path: |
build-gp-${{ matrix.package }}/artifacts/*
build-gpgui:
needs:
- setup-matrix
strategy:
matrix:
os: ${{fromJson(needs.setup-matrix.outputs.matrix)}}
runs-on: ${{ matrix.os.runner }}
name: build-gpgui (${{ matrix.os.arch }})
steps:
- uses: pnpm/action-setup@v2
with:
version: 8
- name: Prepare workspace
run: rm -rf gpgui-source && mkdir gpgui-source
- name: Checkout GlobalProtect-openconnect
uses: actions/checkout@v3
with:
token: ${{ secrets.GH_PAT }}
repository: yuezk/GlobalProtect-openconnect
ref: ${{ github.ref }}
path: gpgui-source/gp
- name: Checkout gpgui@${{ github.ref_name }}
uses: actions/checkout@v3
with:
token: ${{ secrets.GH_PAT }}
repository: yuezk/gpgui
ref: ${{ github.ref_name }}
path: gpgui-source/gpgui
- name: Tarball
run: |
cd gpgui-source
tar -czf gpgui.tar.gz gpgui gp
- name: Docker Login
run: echo ${{ secrets.DOCKER_HUB_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin
- name: Build gpgui in Docker
run: |
docker run --rm -v $(pwd)/gpgui-source:/gpgui yuezk/gpdev:gpgui-builder
- name: Install gpgui in Docker
run: |
cd gpgui-source
tar -xJf *.bin.tar.xz
docker run --rm -v $(pwd):/gpgui yuezk/gpdev:gpgui-builder \
bash -c "cd /gpgui/gpgui_*/ && ./gpgui --version"
- name: Upload gpgui
uses: actions/upload-artifact@v3
with:
name: artifact-gpgui-${{ matrix.os.arch }}
if-no-files-found: error
path: |
gpgui-source/*.bin.tar.xz
gpgui-source/*.bin.tar.xz.sha256
gh-release:
if: ${{ github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/') }}
runs-on: ubuntu-latest
needs:
- tarball
- build-gp
- build-gpgui
steps:
- name: Prepare workspace
run: rm -rf gh-release && mkdir gh-release
- name: Download all artifacts
uses: actions/download-artifact@v3
with:
path: gh-release
- name: Create GH release
env:
GH_TOKEN: ${{ secrets.GH_PAT }}
RELEASE_TAG: ${{ github.ref == 'refs/heads/dev' && 'snapshot' || github.ref_name }}
REPO: ${{ github.repository }}
NOTES: ${{ github.ref == 'refs/heads/dev' && '**!!! DO NOT USE THIS RELEASE IN PRODUCTION !!!**' || format('Release {0}', github.ref_name) }}
run: |
gh -R "$REPO" release delete $RELEASE_TAG --yes --cleanup-tag || true
gh -R "$REPO" release create $RELEASE_TAG \
--title "$RELEASE_TAG" \
--notes "$NOTES" \
${{ github.ref == 'refs/heads/dev' && '--target dev' || '' }} \
${{ github.ref == 'refs/heads/dev' && '--prerelease' || '' }} \
gh-release/artifact-source/* \
gh-release/artifact-gpgui-*/*

View File

@@ -1,30 +0,0 @@
name: CI
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Install Qt
uses: jurplel/install-qt-action@v2
with:
version: '5.15.2'
modules: 'qtwebengine qtwebsockets'
# Checkout repository and submodules
- uses: actions/checkout@v2
with:
submodules: recursive
- name: Build
run: |
qmake CONFIG+=release
make

89
.github/workflows/publish.yaml vendored Normal file
View File

@@ -0,0 +1,89 @@
name: Publish Packages
on:
workflow_dispatch:
inputs:
tag:
description: 'Tag to publish'
required: true
revision:
description: 'Package revision'
required: true
default: "1"
ppa:
description: 'Publish to PPA'
type: boolean
required: true
default: true
obs:
description: 'Publish to OBS'
type: boolean
required: true
default: true
aur:
description: 'Publish to AUR'
type: boolean
required: true
default: true
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Check tag exists
uses: mukunku/tag-exists-action@v1.6.0
id: check-tag
with:
tag: ${{ inputs.tag }}
- name: Exit if tag does not exist
run: |
if [[ "${{ steps.check-tag.outputs.exists }}" == "false" ]]; then
echo "Tag ${{ inputs.tag }} does not exist"
exit 1
fi
publish-ppa:
needs: check
if: ${{ inputs.ppa }}
runs-on: ubuntu-latest
steps:
- uses: pnpm/action-setup@v2
with:
version: 8
- name: Prepare workspace
run: rm -rf publish-ppa && mkdir publish-ppa
- name: Download ${{ inputs.tag }} source code
uses: robinraju/release-downloader@v1.9
with:
token: ${{ secrets.GH_PAT }}
tag: ${{ inputs.tag }}
fileName: globalprotect-openconnect-*.tar.gz
tarBall: false
zipBall: false
out-file-path: publish-ppa
- name: Make the offline tarball
run: |
cd publish-ppa
tar -xf globalprotect-openconnect-*.tar.gz
cd globalprotect-openconnect-*/
make tarball OFFLINE=1
# Prepare the debian directory with custom files
mkdir -p .build/debian
sed 's/@RUST@/rust-all(>=1.70)/g' packaging/deb/control.in > .build/debian/control
sed 's/@OFFLINE@/1/g' packaging/deb/rules.in > .build/debian/rules
cp packaging/deb/postrm .build/debian/postrm
- name: Publish to PPA
uses: yuezk/publish-ppa-package@dev
with:
repository: "yuezk/globalprotect-openconnect"
gpg_private_key: ${{ secrets.PPA_GPG_PRIVATE_KEY }}
gpg_passphrase: ${{ secrets.PPA_GPG_PASSPHRASE }}
tarball: publish-ppa/globalprotect-openconnect-*/.build/tarball/*.tar.gz
debian_dir: publish-ppa/globalprotect-openconnect-*/.build/debian
deb_email: "k3vinyue@gmail.com"
deb_fullname: "Kevin Yue"
extra_ppa: "liushuyu-011/rust-bpo-1.75"
revision: ${{ inputs.revision }}

View File

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

153
.github/workflows/release.yaml vendored Normal file
View File

@@ -0,0 +1,153 @@
name: Release Packages
on:
workflow_dispatch:
inputs:
tag:
description: 'Tag to release'
required: true
arch:
type: choice
description: 'Architecture to build'
required: true
default: all
options:
- all
- x86_64
- arm64
release-deb:
type: boolean
description: 'Build DEB package'
required: true
default: true
release-rpm:
type: boolean
description: 'Build RPM package'
required: true
default: true
release-pkg:
type: boolean
description: 'Build PKG package'
required: true
default: true
release-binary:
type: boolean
description: 'Build binary package'
required: true
default: true
gh-release:
type: boolean
description: 'Update GitHub release'
required: true
default: true
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Check tag exists
uses: mukunku/tag-exists-action@v1.6.0
id: check-tag
with:
tag: ${{ inputs.tag }}
- name: Exit if tag does not exist
run: |
if [[ "${{ steps.check-tag.outputs.exists }}" == "false" ]]; then
echo "Tag ${{ inputs.tag }} does not exist"
exit 1
fi
setup-matrix:
needs:
- check
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.result }}
steps:
- name: Set up matrix
id: set-matrix
uses: actions/github-script@v7
with:
result-encoding: string
script: |
const inputs = ${{ toJson(inputs) }}
const { arch } = inputs
const osMap = {
"all": ["ubuntu-latest", "arm64"],
"x86_64": ["ubuntu-latest"],
"arm64": ["arm64"]
}
const package = Object.entries(inputs)
.filter(([key, value]) => key.startsWith('release-') && value)
.map(([key, value]) => key.replace('release-', ''))
return JSON.stringify({
os: osMap[arch],
package,
})
build:
needs:
- setup-matrix
strategy:
matrix: ${{ fromJson(needs.setup-matrix.outputs.matrix) }}
runs-on: ${{ matrix.os }}
steps:
- name: Prepare workspace
run: rm -rf build-${{ matrix.package }} && mkdir -p build-${{ matrix.package }}
- name: Download ${{ inputs.tag }} source code
uses: robinraju/release-downloader@v1.9
with:
token: ${{ secrets.GH_PAT }}
tag: ${{ inputs.tag }}
fileName: globalprotect-openconnect-*.tar.gz
tarBall: false
zipBall: false
out-file-path: build-${{ matrix.package }}
- name: Docker Login
run: echo ${{ secrets.DOCKER_HUB_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin
- name: Build ${{ matrix.package }} package in Docker
run: |
docker run --rm \
-v $(pwd)/build-${{ matrix.package }}:/${{ matrix.package }} \
-e INCLUDE_GUI=1 \
yuezk/gpdev:${{ matrix.package }}-builder
- name: Install ${{ matrix.package }} package in Docker
run: |
docker run --rm \
-v $(pwd)/build-${{ matrix.package }}:/${{ matrix.package }} \
yuezk/gpdev:${{ matrix.package }}-builder \
bash install.sh
- name: Upload ${{ matrix.package }} package
uses: actions/upload-artifact@v3
with:
name: artifact-${{ matrix.os }}-${{ matrix.package }}
if-no-files-found: error
path: |
build-${{ matrix.package }}/artifacts/*
gh-release:
needs:
- build
runs-on: ubuntu-latest
if: ${{ inputs.gh-release }}
steps:
- name: Prepare workspace
run: rm -rf gh-release && mkdir gh-release
- name: Download artifact
uses: actions/download-artifact@v3
with:
path: gh-release
- name: Update release
uses: softprops/action-gh-release@v1
with:
token: ${{ secrets.GH_PAT }}
prerelease: ${{ contains(github.ref, 'snapshot') }}
fail_on_unmatched_files: true
tag_name: ${{ inputs.tag }}
files: |
gh-release/artifact-*/*

70
.gitignore vendored
View File

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

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

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

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

5202
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

61
Cargo.toml Normal file
View File

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

View File

@@ -1,81 +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 \
settingsdialog.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 \
settingsdialog.h
FORMS += \
gpclient.ui \
normalloginwindow.ui \
settingsdialog.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,11 +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;
StartupWMClass=gpclient

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,176 +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?tmp=tmp&kerberos-support=yes&ipv6-support=yes&clientVer=4100&clientos=Linux")
, loginUrl("https://" + gateway + "/ssl-vpn/login.esp")
, portalConfig(portalConfig)
{
}
GatewayAuthenticator::~GatewayAuthenticator()
{
delete normalLoginWindow;
}
void GatewayAuthenticator::authenticate()
{
PLOGI << "Start gateway authentication...";
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());
QByteArray response;
if (reply->error() || (response = reply->readAll()).contains("Authentication failure")) {
PLOGE << QString("Failed to login the gateway at %1, %2").arg(loginUrl).arg(reply->errorString());
if (normalLoginWindow) {
normalLoginWindow->setProcessing(false);
openMessageBox("Gateway login failed.", "Please check your credentials and try again.");
} else {
doAuth();
}
return;
}
if (normalLoginWindow) {
normalLoginWindow->close();
}
const QUrlQuery params = gpclient::helper::parseGatewayResponse(response);
emit success(params.toString());
}
void GatewayAuthenticator::doAuth()
{
PLOGI << "Perform the gateway prelogin at " << preloginUrl;
QNetworkReply *reply = createRequest(preloginUrl);
connect(reply, &QNetworkReply::finished, this, &GatewayAuthenticator::onPreloginFinished);
}
void GatewayAuthenticator::onPreloginFinished()
{
QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
if (reply->error()) {
PLOGE << QString("Failed to prelogin the gateway at %1, %2").arg(preloginUrl).arg(reply->errorString());
emit fail("Error occurred on the gateway prelogin interface.");
return;
}
PLOGI << "Gateway prelogin succeeded.";
PreloginResponse response = PreloginResponse::parse(reply->readAll());
if (response.hasSamlAuthFields()) {
samlAuth(response.samlMethod(), response.samlRequest(), reply->url().toString());
} else if (response.hasNormalAuthFields()) {
normalAuth(response.labelUsername(), response.labelPassword(), response.authMessage());
} else {
PLOGE << QString("Unknown prelogin response for %1, got %2").arg(preloginUrl).arg(QString::fromUtf8(response.rawResponse()));
emit fail("Unknown response for gateway prelogin interface.");
}
delete reply;
}
void GatewayAuthenticator::normalAuth(QString labelUsername, QString labelPassword, QString authMessage)
{
PLOGI << QString("Trying to perform the normal login with %1 / %2 credentials").arg(labelUsername).arg(labelPassword);
normalLoginWindow = new NormalLoginWindow;
normalLoginWindow->setPortalAddress(gateway);
normalLoginWindow->setAuthMessage(authMessage);
normalLoginWindow->setUsernameLabel(labelUsername);
normalLoginWindow->setPasswordLabel(labelPassword);
// Do login
connect(normalLoginWindow, &NormalLoginWindow::performLogin, this, &GatewayAuthenticator::onPerformNormalLogin);
connect(normalLoginWindow, &NormalLoginWindow::rejected, this, &GatewayAuthenticator::onLoginWindowRejected);
connect(normalLoginWindow, &NormalLoginWindow::finished, this, &GatewayAuthenticator::onLoginWindowFinished);
normalLoginWindow->show();
}
void GatewayAuthenticator::onPerformNormalLogin(const QString &username, const QString &password)
{
PLOGI << "Start to perform normal login...";
normalLoginWindow->setProcessing(true);
LoginParams 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)
{
if (samlResult.contains("preloginCookie")) {
PLOGI << "SAML login succeeded, got the prelogin-cookie " << samlResult.value("preloginCookie");
} else {
PLOGI << "SAML login succeeded, got the portal-userauthcookie " << samlResult.value("userAuthCookie");
}
LoginParams params;
params.setUser(samlResult.value("username"));
params.setPreloginCookie(samlResult.value("preloginCookie"));
params.setUserAuthCookie(samlResult.value("userAuthCookie"));
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,477 +0,0 @@
#include "gpclient.h"
#include "gphelper.h"
#include "ui_gpclient.h"
#include "portalauthenticator.h"
#include "gatewayauthenticator.h"
#include "settingsdialog.h"
#include <plog/Log.h>
#include <QIcon>
using namespace gpclient::helper;
GPClient::GPClient(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::GPClient)
, settingsDialog(new SettingsDialog(this))
{
ui->setupUi(this);
setWindowTitle("GlobalProtect");
setFixedSize(width(), height());
gpclient::helper::moveCenter(this);
setupSettings();
// Restore portal from the previous settings
ui->portalInput->setText(settings::get("portal", "").toString());
// DBus service setup
vpn = new com::yuezk::qt::GPService("com.yuezk.qt.GPService", "/", QDBusConnection::systemBus(), this);
connect(vpn, &com::yuezk::qt::GPService::connected, this, &GPClient::onVPNConnected);
connect(vpn, &com::yuezk::qt::GPService::disconnected, this, &GPClient::onVPNDisconnected);
connect(vpn, &com::yuezk::qt::GPService::logAvailable, this, &GPClient::onVPNLogAvailable);
// Initiallize the context menu of system tray.
initSystemTrayIcon();
initVpnStatus();
}
GPClient::~GPClient()
{
delete ui;
delete vpn;
delete settingsDialog;
delete settingsButton;
}
void GPClient::setupSettings()
{
settingsButton = new QPushButton(this);
settingsButton->setIcon(QIcon(":/images/settings_icon.svg"));
settingsButton->setFixedSize(QSize(28, 28));
QRect rect = this->geometry();
settingsButton->setGeometry(
rect.width() - settingsButton->width() - 15,
15,
settingsButton->geometry().width(),
settingsButton->geometry().height()
);
connect(settingsButton, &QPushButton::clicked, this, &GPClient::onSettingsButtonClicked);
connect(settingsDialog, &QDialog::accepted, this, &GPClient::onSettingsAccepted);
}
void GPClient::onSettingsButtonClicked()
{
settingsDialog->setExtraArgs(settings::get("extraArgs", "").toString());
settingsDialog->show();
}
void GPClient::onSettingsAccepted()
{
settings::save("extraArgs", settingsDialog->extraArgs());
}
void GPClient::on_connectButton_clicked()
{
doConnect();
}
void GPClient::on_portalInput_returnPressed()
{
doConnect();
}
void GPClient::on_portalInput_editingFinished()
{
populateGatewayMenu();
}
void GPClient::initSystemTrayIcon()
{
systemTrayIcon = new QSystemTrayIcon(this);
contextMenu = new QMenu("GlobalProtect", this);
gatewaySwitchMenu = new QMenu("Switch Gateway", this);
gatewaySwitchMenu->setIcon(QIcon::fromTheme("network-workgroup"));
populateGatewayMenu();
systemTrayIcon->setIcon(QIcon(":/images/not_connected.png"));
systemTrayIcon->setToolTip("GlobalProtect");
systemTrayIcon->setContextMenu(contextMenu);
connect(systemTrayIcon, &QSystemTrayIcon::activated, this, &GPClient::onSystemTrayActivated);
connect(gatewaySwitchMenu, &QMenu::triggered, this, &GPClient::onGatewayChanged);
openAction = contextMenu->addAction(QIcon::fromTheme("window-new"), "Open", this, &GPClient::activate);
connectAction = contextMenu->addAction(QIcon::fromTheme("preferences-system-network"), "Connect", this, &GPClient::doConnect);
contextMenu->addMenu(gatewaySwitchMenu);
contextMenu->addSeparator();
clearAction = contextMenu->addAction(QIcon::fromTheme("edit-clear"), "Reset Settings", this, &GPClient::clearSettings);
quitAction = contextMenu->addAction(QIcon::fromTheme("application-exit"), "Quit", this, &GPClient::quit);
systemTrayIcon->show();
}
void GPClient::initVpnStatus() {
int status = vpn->status();
if (status == 1) {
ui->statusLabel->setText("Connecting...");
updateConnectionStatus(VpnStatus::pending);
} else if (status == 2) {
updateConnectionStatus(VpnStatus::connected);
} else if (status == 3) {
ui->statusLabel->setText("Disconnecting...");
updateConnectionStatus(VpnStatus::pending);
} else {
updateConnectionStatus(VpnStatus::disconnected);
}
}
void GPClient::populateGatewayMenu()
{
PLOGI << "Populating the Switch Gateway menu...";
const QList<GPGateway> gateways = allGateways();
gatewaySwitchMenu->clear();
if (gateways.isEmpty()) {
gatewaySwitchMenu->addAction("<None>")->setData(-1);
return;
}
const QString currentGatewayName = currentGateway().name();
for (int i = 0; i < gateways.length(); i++) {
const GPGateway g = gateways.at(i);
QString iconImage = ":/images/radio_unselected.png";
if (g.name() == currentGatewayName) {
iconImage = ":/images/radio_selected.png";
}
gatewaySwitchMenu->addAction(QIcon(iconImage), g.name())->setData(i);
}
}
void GPClient::updateConnectionStatus(const GPClient::VpnStatus &status)
{
switch (status) {
case VpnStatus::disconnected:
ui->statusLabel->setText("Not Connected");
ui->statusImage->setStyleSheet("image: url(:/images/not_connected.png); padding: 15;");
ui->connectButton->setText("Connect");
ui->connectButton->setDisabled(false);
ui->portalInput->setReadOnly(false);
systemTrayIcon->setIcon(QIcon{ ":/images/not_connected.png" });
connectAction->setEnabled(true);
connectAction->setText("Connect");
gatewaySwitchMenu->setEnabled(true);
clearAction->setEnabled(true);
break;
case VpnStatus::pending:
ui->statusImage->setStyleSheet("image: url(:/images/pending.png); padding: 15;");
ui->connectButton->setDisabled(true);
ui->portalInput->setReadOnly(true);
systemTrayIcon->setIcon(QIcon{ ":/images/pending.png" });
connectAction->setEnabled(false);
gatewaySwitchMenu->setEnabled(false);
clearAction->setEnabled(false);
break;
case VpnStatus::connected:
ui->statusLabel->setText("Connected");
ui->statusImage->setStyleSheet("image: url(:/images/connected.png); padding: 15;");
ui->connectButton->setText("Disconnect");
ui->connectButton->setDisabled(false);
ui->portalInput->setReadOnly(true);
systemTrayIcon->setIcon(QIcon{ ":/images/connected.png" });
connectAction->setEnabled(true);
connectAction->setText("Disconnect");
gatewaySwitchMenu->setEnabled(true);
clearAction->setEnabled(false);
break;
default:
break;
}
}
void GPClient::onSystemTrayActivated(QSystemTrayIcon::ActivationReason reason)
{
switch (reason) {
case QSystemTrayIcon::Trigger:
case QSystemTrayIcon::DoubleClick:
this->activate();
break;
default:
break;
}
}
void GPClient::onGatewayChanged(QAction *action)
{
const int index = action->data().toInt();
if (index == -1) {
return;
}
const GPGateway g = allGateways().at(index);
// If the selected gateway is the same as the current gateway
if (g.name() == currentGateway().name()) {
return;
}
setCurrentGateway(g);
if (connected()) {
ui->statusLabel->setText("Switching Gateway...");
ui->connectButton->setEnabled(false);
vpn->disconnect();
isSwitchingGateway = true;
}
}
void GPClient::doConnect()
{
PLOGI << "Start connecting...";
const QString btnText = ui->connectButton->text();
const QString portal = this->portal();
// Display the main window if portal is empty
if (portal.isEmpty()) {
activate();
return;
}
if (btnText.endsWith("Connect")) {
settings::save("portal", portal);
// Login to the previously saved gateway
if (!currentGateway().name().isEmpty()) {
PLOGI << "Start gateway login using the previously saved gateway...";
isQuickConnect = true;
gatewayLogin();
} else {
// Perform the portal login
PLOGI << "Start portal login...";
portalLogin();
}
} else {
PLOGI << "Start disconnecting the VPN...";
ui->statusLabel->setText("Disconnecting...");
updateConnectionStatus(VpnStatus::pending);
vpn->disconnect();
}
}
// Login to the portal interface to get the portal config and preferred gateway
void GPClient::portalLogin()
{
PortalAuthenticator *portalAuth = new PortalAuthenticator(portal());
connect(portalAuth, &PortalAuthenticator::success, this, &GPClient::onPortalSuccess);
// Prelogin failed on the portal interface, try to treat the portal as a gateway interface
connect(portalAuth, &PortalAuthenticator::preloginFailed, this, &GPClient::onPortalPreloginFail);
connect(portalAuth, &PortalAuthenticator::portalConfigFailed, this, &GPClient::onPortalConfigFail);
// Portal login failed
connect(portalAuth, &PortalAuthenticator::fail, this, &GPClient::onPortalFail);
ui->statusLabel->setText("Authenticating...");
updateConnectionStatus(VpnStatus::pending);
portalAuth->authenticate();
}
void GPClient::onPortalSuccess(const PortalConfigResponse portalConfig, const QString region)
{
PLOGI << "Portal authentication succeeded.";
// No gateway found in protal configuration
if (portalConfig.allGateways().size() == 0) {
PLOGI << "No gateway found in portal configuration, treat the portal address as a gateway.";
tryGatewayLogin();
return;
}
GPGateway gateway = filterPreferredGateway(portalConfig.allGateways(), region);
setAllGateways(portalConfig.allGateways());
setCurrentGateway(gateway);
this->portalConfig = portalConfig;
gatewayLogin();
}
void GPClient::onPortalPreloginFail(const QString msg)
{
PLOGI << "Portal prelogin failed: " << msg;
tryGatewayLogin();
}
void GPClient::onPortalConfigFail(const QString msg)
{
PLOGI << "Failed to get the portal configuration, " << msg << " Treat the portal address as gateway.";
tryGatewayLogin();
}
void GPClient::onPortalFail(const QString &msg)
{
if (!msg.isEmpty()) {
openMessageBox("Portal authentication failed.", msg);
}
updateConnectionStatus(VpnStatus::disconnected);
}
void GPClient::tryGatewayLogin()
{
PLOGI << "Try to preform login on the the gateway interface...";
// Treat the portal input as the gateway address
GPGateway g;
g.setName(portal());
g.setAddress(portal());
QList<GPGateway> gateways;
gateways.append(g);
setAllGateways(gateways);
setCurrentGateway(g);
gatewayLogin();
}
// Login to the gateway
void GPClient::gatewayLogin()
{
PLOGI << "Performing gateway login...";
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, settings::get("extraArgs", "").toString());
ui->statusLabel->setText("Connecting...");
updateConnectionStatus(VpnStatus::pending);
}
void GPClient::onGatewayFail(const QString &msg)
{
// If the quick connect on gateway failed, perform the portal login
if (isQuickConnect && !msg.isEmpty()) {
isQuickConnect = false;
portalLogin();
return;
}
if (!msg.isEmpty()) {
openMessageBox("Gateway authentication failed.", msg);
}
updateConnectionStatus(VpnStatus::disconnected);
}
void GPClient::activate()
{
activateWindow();
showNormal();
}
QString GPClient::portal() const
{
const QString input = ui->portalInput->text().trimmed();
if (input.startsWith("http")) {
return QUrl(input).authority();
}
return input;
}
bool GPClient::connected() const
{
const QString statusText = ui->statusLabel->text();
return statusText.contains("Connected") && !statusText.contains("Not");
}
QList<GPGateway> GPClient::allGateways() const
{
const QString gatewaysJson = settings::get(portal() + "_gateways").toString();
return GPGateway::fromJson(gatewaysJson);
}
void GPClient::setAllGateways(QList<GPGateway> gateways)
{
PLOGI << "Updating all the gateways...";
settings::save(portal() + "_gateways", GPGateway::serialize(gateways));
populateGatewayMenu();
}
GPGateway GPClient::currentGateway() const
{
const QString selectedGateway = settings::get(portal() + "_selectedGateway").toString();
for (auto g : allGateways()) {
if (g.name() == selectedGateway) {
return g;
}
}
return GPGateway{};
}
void GPClient::setCurrentGateway(const GPGateway gateway)
{
PLOGI << "Updating the current gateway to " << gateway.name();
settings::save(portal() + "_selectedGateway", gateway.name());
populateGatewayMenu();
}
void GPClient::clearSettings()
{
settings::clear();
populateGatewayMenu();
ui->portalInput->clear();
}
void GPClient::quit()
{
vpn->disconnect();
QApplication::quit();
}
void GPClient::onVPNConnected()
{
updateConnectionStatus(VpnStatus::connected);
}
void GPClient::onVPNDisconnected()
{
updateConnectionStatus(VpnStatus::disconnected);
if (isSwitchingGateway) {
gatewayLogin();
isSwitchingGateway = false;
}
}
void GPClient::onVPNLogAvailable(QString log)
{
PLOGI << log;
}

View File

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

View File

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

View File

@@ -1,97 +0,0 @@
#include "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,122 +0,0 @@
#include "gphelper.h"
#include <QNetworkRequest>
#include <QXmlStreamReader>
#include <QMessageBox>
#include <QDesktopWidget>
#include <QApplication>
#include <QWidget>
#include <QSslConfiguration>
#include <QSslSocket>
#include <plog/Log.h>
QNetworkAccessManager* gpclient::helper::networkManager = new QNetworkAccessManager;
QNetworkReply* gpclient::helper::createRequest(QString url, QByteArray params)
{
QNetworkRequest request(url);
// Skip the ssl verifying
QSslConfiguration conf = request.sslConfiguration();
conf.setPeerVerifyMode(QSslSocket::VerifyNone);
request.setSslConfiguration(conf);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
request.setHeader(QNetworkRequest::UserAgentHeader, UA);
if (params == nullptr) {
return networkManager->post(request, QByteArray(nullptr));
}
return networkManager->post(request, params);
}
GPGateway gpclient::helper::filterPreferredGateway(QList<GPGateway> gateways, const QString ruleName)
{
PLOGI << gateways.size() << " gateway(s) avaiable, filter the gateways with rule: " << ruleName;
GPGateway gateway = gateways.first();
for (GPGateway g : gateways) {
if (g.priorityOf(ruleName) > gateway.priorityOf(ruleName)) {
PLOGI << "Find a preferred gateway: " << g.name();
gateway = g;
}
}
return gateway;
}
QUrlQuery gpclient::helper::parseGatewayResponse(const QByteArray &xml)
{
PLOGI << "Start parsing the gateway response...";
PLOGI << "The gateway response is: " << xml;
QXmlStreamReader xmlReader{xml};
QList<QString> args;
while (!xmlReader.atEnd()) {
xmlReader.readNextStartElement();
if (xmlReader.name() == "argument") {
args.append(QUrl::toPercentEncoding(xmlReader.readElementText()));
}
}
QUrlQuery params{};
params.addQueryItem("authcookie", args.at(1));
params.addQueryItem("portal", args.at(3));
params.addQueryItem("user", args.at(4));
params.addQueryItem("domain", args.at(7));
params.addQueryItem("preferred-ip", args.at(15));
params.addQueryItem("computer", QUrl::toPercentEncoding(QSysInfo::machineHostName()));
return params;
}
void gpclient::helper::openMessageBox(const QString &message, const QString& informativeText)
{
QMessageBox msgBox;
msgBox.setWindowTitle("Notice");
msgBox.setText(message);
msgBox.setFixedWidth(500);
msgBox.setStyleSheet("QLabel{min-width: 250px}");
msgBox.setInformativeText(informativeText);
msgBox.exec();
}
void gpclient::helper::moveCenter(QWidget *widget)
{
QDesktopWidget *desktop = QApplication::desktop();
int screenWidth, width;
int screenHeight, height;
int x, y;
QSize windowSize;
screenWidth = desktop->width();
screenHeight = desktop->height();
windowSize = widget->size();
width = windowSize.width();
height = windowSize.height();
x = (screenWidth - width) / 2;
y = (screenHeight - height) / 2;
y -= 50;
widget->move(x, y);
}
QSettings *gpclient::helper::settings::_settings = new QSettings("com.yuezk.qt", "GPClient");
QVariant gpclient::helper::settings::get(const QString &key, const QVariant &defaultValue)
{
return _settings->value(key, defaultValue);
}
void gpclient::helper::settings::save(const QString &key, const QVariant &value)
{
_settings->setValue(key, value);
}
void gpclient::helper::settings::clear()
{
_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,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,39 +0,0 @@
#include "singleapplication.h"
#include "gpclient.h"
#include "enhancedwebview.h"
#include <QStandardPaths>
#include <plog/Log.h>
#include <plog/Appenders/ColorConsoleAppender.h>
static const QString version = "v1.3.0";
int main(int argc, char *argv[])
{
const QDir path = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + "/GlobalProtect-openconnect";
const QString logFile = path.path() + "/gpclient.log";
if (!path.exists()) {
path.mkpath(".");
}
static plog::ColorConsoleAppender<plog::TxtFormatter> consoleAppender;
plog::init(plog::debug, logFile.toUtf8()).addAppender(&consoleAppender);
PLOGI << "GlobalProtect started, version: " << version;
QString port = QString::fromLocal8Bit(qgetenv(ENV_CDP_PORT));
if (port == "") {
qputenv(ENV_CDP_PORT, "12315");
}
SingleApplication app(argc, argv);
app.setQuitOnLastWindowClosed(false);
GPClient w;
w.show();
QObject::connect(&app, &SingleApplication::instanceStarted, &w, &GPClient::activate);
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,206 +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?tmp=tmp&kerberos-support=yes&ipv6-support=yes&clientVer=4100&clientos=Linux")
, 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());
PLOGI << "Finished parsing the prelogin response. The region field is: " << preloginResponse.region();
if (preloginResponse.hasSamlAuthFields()) {
// Do SAML authentication
samlAuth();
} else if (preloginResponse.hasNormalAuthFields()) {
// Do normal username/password authentication
tryAutoLogin();
} else {
PLOGE << QString("Unknown prelogin response for %1 got %2").arg(preloginUrl).arg(QString::fromUtf8(preloginResponse.rawResponse()));
emit preloginFailed("Unknown response for portal prelogin interface.");
}
delete reply;
}
void PortalAuthenticator::tryAutoLogin()
{
const QString username = settings::get("username").toString();
const QString password = settings::get("password").toString();
if (!username.isEmpty() && !password.isEmpty()) {
PLOGI << "Trying auto login using the saved credentials";
isAutoLogin = true;
fetchConfig(settings::get("username").toString(), settings::get("password").toString());
} else {
normalAuth();
}
}
void PortalAuthenticator::normalAuth()
{
PLOGI << "Trying to launch the normal login window...";
normalLoginWindow = new NormalLoginWindow;
normalLoginWindow->setPortalAddress(portal);
normalLoginWindow->setAuthMessage(preloginResponse.authMessage());
normalLoginWindow->setUsernameLabel(preloginResponse.labelUsername());
normalLoginWindow->setPasswordLabel(preloginResponse.labelPassword());
// Do login
connect(normalLoginWindow, &NormalLoginWindow::performLogin, this, &PortalAuthenticator::onPerformNormalLogin);
connect(normalLoginWindow, &NormalLoginWindow::rejected, this, &PortalAuthenticator::onLoginWindowRejected);
connect(normalLoginWindow, &NormalLoginWindow::finished, this, &PortalAuthenticator::onLoginWindowFinished);
normalLoginWindow->show();
}
void PortalAuthenticator::onPerformNormalLogin(const QString &username, const QString &password)
{
normalLoginWindow->setProcessing(true);
fetchConfig(username, password);
}
void PortalAuthenticator::onLoginWindowRejected()
{
emitFail();
}
void PortalAuthenticator::onLoginWindowFinished()
{
delete normalLoginWindow;
normalLoginWindow = nullptr;
}
void PortalAuthenticator::samlAuth()
{
PLOGI << "Trying to perform SAML login with saml-method " << preloginResponse.samlMethod();
SAMLLoginWindow *loginWindow = new SAMLLoginWindow;
connect(loginWindow, &SAMLLoginWindow::success, this, &PortalAuthenticator::onSAMLLoginSuccess);
connect(loginWindow, &SAMLLoginWindow::fail, this, &PortalAuthenticator::onSAMLLoginFail);
connect(loginWindow, &SAMLLoginWindow::rejected, this, &PortalAuthenticator::onLoginWindowRejected);
loginWindow->login(preloginResponse.samlMethod(), preloginResponse.samlRequest(), preloginUrl);
}
void PortalAuthenticator::onSAMLLoginSuccess(const QMap<QString, QString> samlResult)
{
if (samlResult.contains("preloginCookie")) {
PLOGI << "SAML login succeeded, got the prelogin-cookie " << samlResult.value("preloginCookie");
} else {
PLOGI << "SAML login succeeded, got the portal-userauthcookie " << samlResult.value("userAuthCookie");
}
fetchConfig(samlResult.value("username"), "", samlResult.value("preloginCookie"), samlResult.value("userAuthCookie"));
}
void PortalAuthenticator::onSAMLLoginFail(const QString msg)
{
emitFail(msg);
}
void PortalAuthenticator::fetchConfig(QString username, QString password, QString preloginCookie, QString userAuthCookie)
{
LoginParams params;
params.setServer(portal);
params.setUser(username);
params.setPassword(password);
params.setPreloginCookie(preloginCookie);
params.setUserAuthCookie(userAuthCookie);
// Save the username and password for future use.
this->username = username;
this->password = password;
PLOGI << "Fetching the portal config from " << configUrl << " for user: " << username;
QNetworkReply *reply = createRequest(configUrl, 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 {
emit portalConfigFailed("Failed to fetch the portal config.");
}
return;
}
PLOGI << "Fetch the portal config succeeded.";
PortalConfigResponse response = PortalConfigResponse::parse(reply->readAll());
// Add the username & password to the response object
response.setUsername(username);
response.setPassword(password);
// Close the login window
if (normalLoginWindow) {
PLOGI << "Closing the NormalLoginWindow...";
// Save the credentials for reuse
settings::save("username", username);
settings::save("password", password);
normalLoginWindow->close();
}
emit success(response, preloginResponse.region());
}
void PortalAuthenticator::emitFail(const QString& msg)
{
emit fail(msg);
}

View File

@@ -1,55 +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 response, const QString region);
void fail(const QString& msg);
void preloginFailed(const QString& msg);
void portalConfigFailed(const QString msg);
private slots:
void onPreloginFinished();
void onPerformNormalLogin(const QString &username, const QString &password);
void onLoginWindowRejected();
void onLoginWindowFinished();
void onSAMLLoginSuccess(const QMap<QString, QString> samlResult);
void onSAMLLoginFail(const QString msg);
void onFetchConfigFinished();
private:
QString portal;
QString preloginUrl;
QString configUrl;
QString username;
QString password;
PreloginResponse preloginResponse;
bool isAutoLogin { false };
NormalLoginWindow *normalLoginWindow{ nullptr };
void tryAutoLogin();
void normalAuth();
void samlAuth();
void fetchConfig(QString username, QString password, QString preloginCookie = "", QString userAuthCookie = "");
void emitFail(const QString& msg = "");
};
#endif // PORTALAUTHENTICATOR_H

View File

@@ -1,178 +0,0 @@
#include "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)
{
PLOGI << "Start parsing the portal configuration...";
QXmlStreamReader xmlReader(xml);
PortalConfigResponse response;
response.setRawResponse(xml);
while (!xmlReader.atEnd()) {
xmlReader.readNextStartElement();
QString name = xmlReader.name().toString();
if (name == xmlUserAuthCookie) {
PLOGI << "Start reading " << name;
response.setUserAuthCookie(xmlReader.readElementText());
} else if (name == xmlPrelogonUserAuthCookie) {
PLOGI << "Start reading " << name;
response.setPrelogonUserAuthCookie(xmlReader.readElementText());
} else if (name == xmlGateways) {
response.setAllGateways(parseGateways(xmlReader));
}
}
PLOGI << "Finished parsing portal configuration.";
return response;
}
const QByteArray PortalConfigResponse::rawResponse() const
{
return _rawResponse;
}
QString PortalConfigResponse::username() const
{
return _username;
}
QString PortalConfigResponse::password() const
{
return _password;
}
QList<GPGateway> PortalConfigResponse::parseGateways(QXmlStreamReader &xmlReader)
{
PLOGI << "Start parsing the gateways from portal configuration...";
QList<GPGateway> gateways;
while (xmlReader.name() != "external"){
xmlReader.readNext();
}
while (xmlReader.name() != "list"){
xmlReader.readNext();
}
while (xmlReader.name() != xmlGateways || !xmlReader.isEndElement()) {
xmlReader.readNext();
// Parse the gateways -> external -> list -> entry
if (xmlReader.name() == "entry" && xmlReader.isStartElement()) {
GPGateway g;
QString address = xmlReader.attributes().value("name").toString();
g.setAddress(address);
g.setPriorityRules(parsePriorityRules(xmlReader));
g.setName(parseGatewayName(xmlReader));
gateways.append(g);
}
}
PLOGI << "Finished parsing the gateways.";
return gateways;
}
QMap<QString, int> PortalConfigResponse::parsePriorityRules(QXmlStreamReader &xmlReader)
{
PLOGI << "Start parsing the priority rules...";
QMap<QString, int> priorityRules;
while ((xmlReader.name() != "priority-rule" || !xmlReader.isEndElement()) && !xmlReader.hasError()) {
xmlReader.readNext();
if (xmlReader.name() == "entry" && xmlReader.isStartElement()) {
QString ruleName = xmlReader.attributes().value("name").toString();
// Read the priority tag
while (xmlReader.name() != "priority"){
xmlReader.readNext();
}
int ruleValue = xmlReader.readElementText().toUInt();
priorityRules.insert(ruleName, ruleValue);
}
}
PLOGI << "Finished parsing the priority rules.";
return priorityRules;
}
QString PortalConfigResponse::parseGatewayName(QXmlStreamReader &xmlReader)
{
PLOGI << "Start parsing the gateway name...";
while (xmlReader.name() != "description" || !xmlReader.isEndElement()) {
xmlReader.readNext();
if (xmlReader.name() == "description" && xmlReader.tokenType() == xmlReader.StartElement) {
PLOGI << "Finished parsing the gateway name";
return xmlReader.readElementText();
}
}
PLOGE << "Error: <description> tag not found";
return "";
}
QString PortalConfigResponse::userAuthCookie() const
{
return _userAuthCookie;
}
QString PortalConfigResponse::prelogonUserAuthCookie() const
{
return _prelogonAuthCookie;
}
QList<GPGateway> PortalConfigResponse::allGateways() const
{
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() const;
void setAllGateways(QList<GPGateway> gateways);
void setUsername(const QString username);
void setPassword(const QString password);
private:
static QString xmlUserAuthCookie;
static QString xmlPrelogonUserAuthCookie;
static QString xmlGateways;
QByteArray _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,100 +0,0 @@
#include "preloginresponse.h"
#include <QXmlStreamReader>
#include <QMap>
#include <plog/Log.h>
QString PreloginResponse::xmlAuthMessage = "authentication-message";
QString PreloginResponse::xmlLabelUsername = "username-label";
QString PreloginResponse::xmlLabelPassword = "password-label";
QString PreloginResponse::xmlSamlMethod = "saml-auth-method";
QString PreloginResponse::xmlSamlRequest = "saml-request";
QString PreloginResponse::xmlRegion = "region";
PreloginResponse::PreloginResponse()
{
add(xmlAuthMessage, "");
add(xmlLabelUsername, "");
add(xmlLabelPassword, "");
add(xmlSamlMethod, "");
add(xmlSamlRequest, "");
add(xmlRegion, "");
}
PreloginResponse PreloginResponse::parse(const QByteArray& xml)
{
PLOGI << "Start parsing the prelogin response...";
QXmlStreamReader xmlReader(xml);
PreloginResponse response;
response.setRawResponse(xml);
while (!xmlReader.atEnd()) {
xmlReader.readNextStartElement();
QString name = xmlReader.name().toString();
if (response.has(name)) {
response.add(name, xmlReader.readElementText());
}
}
return response;
}
const QByteArray& PreloginResponse::rawResponse() const
{
return _rawResponse;
}
QString PreloginResponse::authMessage() const
{
return resultMap.value(xmlAuthMessage);
}
QString PreloginResponse::labelUsername() const
{
return resultMap.value(xmlLabelUsername);
}
QString PreloginResponse::labelPassword() const
{
return resultMap.value(xmlLabelPassword);
}
QString PreloginResponse::samlMethod() const
{
return resultMap.value(xmlSamlMethod);
}
QString PreloginResponse::samlRequest() const
{
return QByteArray::fromBase64(resultMap.value(xmlSamlRequest).toUtf8());
}
QString PreloginResponse::region() const
{
return resultMap.value(xmlRegion);
}
bool PreloginResponse::hasSamlAuthFields() const
{
return !samlMethod().isEmpty() && !samlRequest().isEmpty();
}
bool PreloginResponse::hasNormalAuthFields() const
{
return !labelUsername().isEmpty() && !labelPassword().isEmpty();
}
void PreloginResponse::setRawResponse(const QByteArray response)
{
_rawResponse = response;
}
bool PreloginResponse::has(const QString name) const
{
return resultMap.contains(name);
}
void PreloginResponse::add(const QString name, const QString value)
{
resultMap.insert(name, value);
}

View File

@@ -1,41 +0,0 @@
#ifndef PRELOGINRESPONSE_H
#define PRELOGINRESPONSE_H
#include <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,11 +0,0 @@
<RCC>
<qresource prefix="/images">
<file alias="logo.svg">com.yuezk.qt.GPClient.svg</file>
<file>connected.png</file>
<file>pending.png</file>
<file>not_connected.png</file>
<file>radio_unselected.png</file>
<file>radio_selected.png</file>
<file>settings_icon.svg</file>
</qresource>
</RCC>

View File

@@ -1,99 +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();
const QString userAuthCookie = headers.value("portal-userauthcookie").toString();
LOGI << "Response received from " << response.value("url").toString();
if (!username.isEmpty()) {
LOGI << "Got username from SAML response headers " << username;
samlResult.insert("username", username);
}
if (!preloginCookie.isEmpty()) {
LOGI << "Got prelogin-cookie from SAML response headers " << preloginCookie;
samlResult.insert("preloginCookie", preloginCookie);
}
if (!userAuthCookie.isEmpty()) {
LOGI << "Got portal-userauthcookie from SAML response headers " << userAuthCookie;
samlResult.insert("userAuthCookie", userAuthCookie);
}
// Check the SAML result
if (samlResult.contains("username")
&& (samlResult.contains("preloginCookie") || samlResult.contains("userAuthCookie"))) {
LOGI << "Got the SAML authentication information successfully. "
<< "username: " << samlResult.value("username")
<< ", preloginCookie: " << samlResult.value("preloginCookie")
<< ", userAuthCookie: " << samlResult.value("userAuthCookie");
emit success(samlResult);
accept();
} else {
this->show();
}
}
void SAMLLoginWindow::onLoadFinished()
{
LOGI << "Load finished " << this->webView->page()->url().toString();
}

View File

@@ -1,35 +0,0 @@
#ifndef SAMLLOGINWINDOW_H
#define SAMLLOGINWINDOW_H
#include "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,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Icons" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
<g>
<path d="M16.5,19.9C16.5,19.9,16.5,19.9,16.5,19.9l3.1-3.1c0,0,0,0,0,0l2.3-2.3c2.2,0.6,4.5,0,6.2-1.6c1.8-1.8,2.3-4.4,1.4-6.8
c-0.1-0.3-0.4-0.5-0.7-0.6c-0.3-0.1-0.7,0-0.9,0.3L25.6,8l-1.3-0.3L24,6.4l2.2-2.2c0.2-0.2,0.3-0.6,0.3-0.9
c-0.1-0.3-0.3-0.6-0.6-0.7c-2.3-0.9-5-0.4-6.8,1.4c-1.6,1.6-2.2,4-1.6,6.2l-1.6,1.6l-2.6-2.6L11,5.3c-0.1-0.1-0.2-0.3-0.3-0.3
L6.8,2.7C6.4,2.4,5.9,2.5,5.5,2.8L2.5,5.9C2.1,6.2,2.1,6.7,2.3,7.1L4.6,11c0.1,0.1,0.2,0.3,0.3,0.3l3.7,2.2l2.6,2.6l-1.2,1.2
c-2.2-0.6-4.5,0-6.2,1.6c-1.8,1.8-2.3,4.4-1.4,6.8c0.1,0.3,0.4,0.5,0.7,0.6c0.3,0.1,0.7,0,0.9-0.3L6.4,24l1.3,0.3L8,25.6l-2.2,2.2
c-0.2,0.2-0.3,0.6-0.3,0.9c0.1,0.3,0.3,0.6,0.6,0.7c0.8,0.3,1.5,0.4,2.3,0.4c1.6,0,3.3-0.6,4.5-1.9c1.6-1.6,2.2-4,1.6-6.2
L16.5,19.9z"/>
<path d="M22.5,16.8l-6,6l6.1,6.1c0.8,0.8,1.9,1.3,3,1.3s2.2-0.4,3-1.3c0.8-0.8,1.3-1.9,1.3-3c0-1.1-0.4-2.2-1.3-3L22.5,16.8z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,26 +0,0 @@
#include "settingsdialog.h"
#include "ui_settingsdialog.h"
SettingsDialog::SettingsDialog(QWidget *parent) :
QDialog(parent),
ui(new Ui::SettingsDialog)
{
ui->setupUi(this);
ui->extraArgsInput->setPlaceholderText("e.g. --name=value");
}
SettingsDialog::~SettingsDialog()
{
delete ui;
}
void SettingsDialog::setExtraArgs(QString args)
{
ui->extraArgsInput->setPlainText(args);
}
QString SettingsDialog::extraArgs()
{
return ui->extraArgsInput->toPlainText().trimmed();
}

View File

@@ -1,25 +0,0 @@
#ifndef SETTINGSDIALOG_H
#define SETTINGSDIALOG_H
#include <QDialog>
namespace Ui {
class SettingsDialog;
}
class SettingsDialog : public QDialog
{
Q_OBJECT
public:
explicit SettingsDialog(QWidget *parent = nullptr);
~SettingsDialog();
void setExtraArgs(QString);
QString extraArgs();
private:
Ui::SettingsDialog *ui;
};
#endif // SETTINGSDIALOG_H

View File

@@ -1,86 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SettingsDialog</class>
<widget class="QDialog" name="SettingsDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>470</width>
<height>183</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Settings</string>
</property>
<property name="windowIcon">
<iconset resource="resources.qrc">
<normaloff>:/images/connected.png</normaloff>:/images/connected.png</iconset>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Custom Parameters:</string>
</property>
</widget>
</item>
<item>
<widget class="QPlainTextEdit" name="extraArgsInput"/>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="resources.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>SettingsDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>SettingsDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -1,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,134 +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, QString extraArgs)
{
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"
<< QProcess::splitCommand(extraArgs)
<< "-u" << username
<< "-C" << passwd
<< server;
log("Start process with arugments: " + args.join(" "));
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, QString extraArgs);
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,23 +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"/>
<arg name="extraArgs" 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

263
Makefile Normal file
View File

@@ -0,0 +1,263 @@
.SHELLFLAGS += -e
OFFLINE ?= 0
BUILD_FE ?= 1
INCLUDE_GUI ?= 0
CARGO ?= cargo
VERSION = $(shell $(CARGO) metadata --no-deps --format-version 1 | jq -r '.packages[0].version')
REVISION ?= 1
PPA_REVISION ?= 1
PKG_NAME = globalprotect-openconnect
PKG = $(PKG_NAME)-$(VERSION)
SERIES ?= $(shell lsb_release -cs)
PUBLISH ?= 0
export DEBEMAIL = k3vinyue@gmail.com
export DEBFULLNAME = Kevin Yue
export SNAPSHOT = $(shell test -f SNAPSHOT && echo "true" || echo "false")
ifeq ($(SNAPSHOT), true)
RELEASE_TAG = snapshot
else
RELEASE_TAG = v$(VERSION)
endif
CARGO_BUILD_ARGS = --release
ifeq ($(OFFLINE), 1)
CARGO_BUILD_ARGS += --frozen
endif
default: build
version:
@echo $(VERSION)
clean-tarball:
rm -rf .build/tarball
rm -rf .vendor
rm -rf vendor.tar.xz
rm -rf .cargo
# Create a tarball, include the cargo dependencies if OFFLINE is set to 1
tarball: clean-tarball
if [ $(BUILD_FE) -eq 1 ]; then \
echo "Building frontend..."; \
cd apps/gpgui-helper && pnpm install && pnpm build; \
fi
# Remove node_modules to reduce the tarball size
rm -rf apps/gpgui-helper/node_modules
mkdir -p .cargo
mkdir -p .build/tarball
# If OFFLINE is set to 1, vendor all cargo dependencies
if [ $(OFFLINE) -eq 1 ]; then \
$(CARGO) vendor .vendor > .cargo/config.toml; \
tar -cJf vendor.tar.xz .vendor; \
fi
@echo "Creating tarball..."
tar --exclude .vendor --exclude target --transform 's,^,${PKG}/,' -czf .build/tarball/${PKG}.tar.gz * .cargo
download-gui:
rm -rf .build/gpgui
if [ $(INCLUDE_GUI) -eq 1 ]; then \
echo "Downloading GlobalProtect GUI..."; \
mkdir -p .build/gpgui; \
curl -sSL https://github.com/yuezk/GlobalProtect-openconnect/releases/download/$(RELEASE_TAG)/gpgui_$(shell uname -m).bin.tar.xz \
-o .build/gpgui/gpgui_$(shell uname -m).bin.tar.xz; \
tar -xJf .build/gpgui/*.tar.xz -C .build/gpgui; \
else \
echo "Skipping GlobalProtect GUI download (INCLUDE_GUI=0)"; \
fi
build: download-gui build-fe build-rs
# Install and build the frontend
# If OFFLINE is set to 1, skip it
build-fe:
if [ $(OFFLINE) -eq 1 ] || [ $(BUILD_FE) -eq 0 ]; then \
echo "Skipping frontend build (OFFLINE=1 or BUILD_FE=0)"; \
else \
cd apps/gpgui-helper && pnpm install && pnpm build; \
fi
if [ ! -d apps/gpgui-helper/dist ]; then \
echo "Error: frontend build failed"; \
exit 1; \
fi
build-rs:
if [ $(OFFLINE) -eq 1 ]; then \
tar -xJf vendor.tar.xz; \
fi
$(CARGO) build $(CARGO_BUILD_ARGS) -p gpclient -p gpservice -p gpauth
$(CARGO) build $(CARGO_BUILD_ARGS) -p gpgui-helper --features "tauri/custom-protocol"
clean:
$(CARGO) clean
rm -rf .build
rm -rf .vendor
rm -rf apps/gpgui-helper/node_modules
install:
@echo "Installing $(PKG_NAME)..."
install -Dm755 target/release/gpclient $(DESTDIR)/usr/bin/gpclient
install -Dm755 target/release/gpauth $(DESTDIR)/usr/bin/gpauth
install -Dm755 target/release/gpservice $(DESTDIR)/usr/bin/gpservice
install -Dm755 target/release/gpgui-helper $(DESTDIR)/usr/bin/gpgui-helper
if [ -f .build/gpgui/gpgui_*/gpgui ]; then \
install -Dm755 .build/gpgui/gpgui_*/gpgui $(DESTDIR)/usr/bin/gpgui; \
fi
install -Dm644 packaging/files/usr/share/applications/gpgui.desktop $(DESTDIR)/usr/share/applications/gpgui.desktop
install -Dm644 packaging/files/usr/share/icons/hicolor/scalable/apps/gpgui.svg $(DESTDIR)/usr/share/icons/hicolor/scalable/apps/gpgui.svg
install -Dm644 packaging/files/usr/share/icons/hicolor/32x32/apps/gpgui.png $(DESTDIR)/usr/share/icons/hicolor/32x32/apps/gpgui.png
install -Dm644 packaging/files/usr/share/icons/hicolor/128x128/apps/gpgui.png $(DESTDIR)/usr/share/icons/hicolor/128x128/apps/gpgui.png
install -Dm644 packaging/files/usr/share/icons/hicolor/256x256@2/apps/gpgui.png $(DESTDIR)/usr/share/icons/hicolor/256x256@2/apps/gpgui.png
install -Dm644 packaging/files/usr/share/polkit-1/actions/com.yuezk.gpgui.policy $(DESTDIR)/usr/share/polkit-1/actions/com.yuezk.gpgui.policy
uninstall:
@echo "Uninstalling $(PKG_NAME)..."
rm -f $(DESTDIR)/usr/bin/gpclient
rm -f $(DESTDIR)/usr/bin/gpauth
rm -f $(DESTDIR)/usr/bin/gpservice
rm -f $(DESTDIR)/usr/bin/gpgui-helper
rm -f $(DESTDIR)/usr/bin/gpgui
rm -f $(DESTDIR)/usr/share/applications/gpgui.desktop
rm -f $(DESTDIR)/usr/share/icons/hicolor/scalable/apps/gpgui.svg
rm -f $(DESTDIR)/usr/share/icons/hicolor/32x32/apps/gpgui.png
rm -f $(DESTDIR)/usr/share/icons/hicolor/128x128/apps/gpgui.png
rm -f $(DESTDIR)/usr/share/icons/hicolor/256x256@2/apps/gpgui.png
rm -f $(DESTDIR)/usr/share/polkit-1/actions/com.yuezk.gpgui.policy
clean-debian:
rm -rf .build/deb
# Generate the debian package structure, without the changelog
init-debian: clean-debian tarball
mkdir -p .build/deb
cp .build/tarball/${PKG}.tar.gz .build/deb
tar -xzf .build/deb/${PKG}.tar.gz -C .build/deb
cd .build/deb/${PKG} && debmake
cp -f packaging/deb/control.in .build/deb/$(PKG)/debian/control
cp -f packaging/deb/rules.in .build/deb/$(PKG)/debian/rules
cp -f packaging/deb/postrm .build/deb/$(PKG)/debian/postrm
sed -i "s/@OFFLINE@/$(OFFLINE)/g" .build/deb/$(PKG)/debian/rules
rm -f .build/deb/$(PKG)/debian/changelog
deb: init-debian
# Remove the rust build depdency from the control file
sed -i "s/@RUST@//g" .build/deb/$(PKG)/debian/control
cd .build/deb/$(PKG) && dch --create --distribution unstable --package $(PKG_NAME) --newversion $(VERSION)-$(REVISION) "Bugfix and improvements."
cd .build/deb/$(PKG) && debuild --preserve-env -e PATH -us -uc -b
check-ppa:
if [ $(OFFLINE) -eq 0 ]; then \
echo "Error: ppa build requires offline mode (OFFLINE=1)"; \
fi
# Usage: make ppa SERIES=focal OFFLINE=1 PUBLISH=1
ppa: check-ppa init-debian
sed -i "s/@RUST@/rust-all(>=1.70)/g" .build/deb/$(PKG)/debian/control
$(eval SERIES_VER = $(shell distro-info --series $(SERIES) -r | cut -d' ' -f1))
@echo "Building for $(SERIES) $(SERIES_VER)"
rm -rf .build/deb/$(PKG)/debian/changelog
cd .build/deb/$(PKG) && dch --create --distribution $(SERIES) --package $(PKG_NAME) --newversion $(VERSION)-$(REVISION)ppa$(PPA_REVISION)~ubuntu$(SERIES_VER) "Bugfix and improvements."
cd .build/deb/$(PKG) && echo "y" | debuild -e PATH -S -sa -k"$(GPG_KEY_ID)" -p"gpg --batch --passphrase $(GPG_KEY_PASS) --pinentry-mode loopback"
if [ $(PUBLISH) -eq 1 ]; then \
cd .build/deb/$(PKG) && dput ppa:yuezk/globalprotect-openconnect ../*.changes; \
else \
echo "Skipping ppa publish (PUBLISH=0)"; \
fi
clean-rpm:
rm -rf .build/rpm
# Generate RPM sepc file
init-rpm: clean-rpm
mkdir -p .build/rpm
cp packaging/rpm/globalprotect-openconnect.spec.in .build/rpm/globalprotect-openconnect.spec
cp packaging/rpm/globalprotect-openconnect.changes.in .build/rpm/globalprotect-openconnect.changes
sed -i "s/@VERSION@/$(VERSION)/g" .build/rpm/globalprotect-openconnect.spec
sed -i "s/@REVISION@/$(REVISION)/g" .build/rpm/globalprotect-openconnect.spec
sed -i "s/@OFFLINE@/$(OFFLINE)/g" .build/rpm/globalprotect-openconnect.spec
sed -i "s/@DATE@/$(shell LC_ALL=en.US date "+%a %b %d %Y")/g" .build/rpm/globalprotect-openconnect.spec
sed -i "s/@VERSION@/$(VERSION)/g" .build/rpm/globalprotect-openconnect.changes
sed -i "s/@DATE@/$(shell LC_ALL=en.US date -u "+%a %b %e %T %Z %Y")/g" .build/rpm/globalprotect-openconnect.changes
rpm: init-rpm tarball
rm -rf $(HOME)/rpmbuild
rpmdev-setuptree
cp .build/tarball/${PKG}.tar.gz $(HOME)/rpmbuild/SOURCES/${PKG_NAME}.tar.gz
rpmbuild -ba .build/rpm/globalprotect-openconnect.spec
# Copy RPM package from build directory
cp $(HOME)/rpmbuild/RPMS/$(shell uname -m)/$(PKG_NAME)*.rpm .build/rpm
# Copy the SRPM only for x86_64.
if [ "$(shell uname -m)" = "x86_64" ]; then \
cp $(HOME)/rpmbuild/SRPMS/$(PKG_NAME)*.rpm .build/rpm; \
fi
clean-pkgbuild:
rm -rf .build/pkgbuild
init-pkgbuild: clean-pkgbuild tarball
mkdir -p .build/pkgbuild
cp .build/tarball/${PKG}.tar.gz .build/pkgbuild
cp packaging/pkgbuild/PKGBUILD.in .build/pkgbuild/PKGBUILD
sed -i "s/@PKG_NAME@/$(PKG_NAME)/g" .build/pkgbuild/PKGBUILD
sed -i "s/@VERSION@/$(VERSION)/g" .build/pkgbuild/PKGBUILD
sed -i "s/@REVISION@/$(REVISION)/g" .build/pkgbuild/PKGBUILD
sed -i "s/@OFFLINE@/$(OFFLINE)/g" .build/pkgbuild/PKGBUILD
pkgbuild: init-pkgbuild
cd .build/pkgbuild && makepkg -s --noconfirm
clean-binary:
rm -rf .build/binary
binary: clean-binary tarball
mkdir -p .build/binary
cp .build/tarball/${PKG}.tar.gz .build/binary
tar -xzf .build/binary/${PKG}.tar.gz -C .build/binary
mkdir -p .build/binary/$(PKG_NAME)_$(VERSION)/artifacts
make -C .build/binary/${PKG} build OFFLINE=$(OFFLINE) BUILD_FE=0 INCLUDE_GUI=$(INCLUDE_GUI)
make -C .build/binary/${PKG} install DESTDIR=$(PWD)/.build/binary/$(PKG_NAME)_$(VERSION)/artifacts
cp packaging/binary/Makefile.in .build/binary/$(PKG_NAME)_$(VERSION)/Makefile
# Create a tarball for the binary package
tar -cJf .build/binary/$(PKG_NAME)_$(VERSION)_$(shell uname -m).bin.tar.xz -C .build/binary $(PKG_NAME)_$(VERSION)
# Generate sha256sum
cd .build/binary && sha256sum $(PKG_NAME)_$(VERSION)_$(shell uname -m).bin.tar.xz | cut -d' ' -f1 > $(PKG_NAME)_$(VERSION)_$(shell uname -m).bin.tar.xz.sha256

View File

@@ -1,39 +0,0 @@
# Maintainer: Keinv Yue <yuezk001@gmail.com>
pkgname=globalprotect-openconnect
_gitname=GlobalProtect-openconnect
pkgver={PKG_VERSION}
pkgrel=1
pkgdesc="A GlobalProtect VPN client (GUI) for Linux based on Openconnect and built with Qt5, supports SAML auth mode."
arch=(x86_64 aarch64)
url="https://github.com/yuezk/${_gitname}"
license=('GPL3')
depends=('openconnect>=8.0.0' qt5-base qt5-webengine qt5-websockets)
makedepends=()
source=(
"${_gitname}-${pkgver}.tar.gz::${url}/archive/v${pkgver}.tar.gz"
"https://github.com/itay-grudev/SingleApplication/archive/v3.0.19.tar.gz"
"https://github.com/SergiusTheBest/plog/archive/1.1.5.tar.gz"
)
sha256sums=(
'{SOURCE_SHA}'
'9405fd259288b2a862e91e5135bccee936f0438e1b32c13603277132309d15e0'
'6c80b4701183d2415bec927e1f5ca9b1761b3b5c65d3e09fb29c743e016d5609'
);
prepare() {
mv "$srcdir/SingleApplication-3.0.19" -T "$srcdir/${_gitname}-${pkgver}/singleapplication"
mv "$srcdir/plog-1.1.5" -T "$srcdir/${_gitname}-${pkgver}/plog"
}
build() {
cd "$srcdir/${_gitname}-${pkgver}"
qmake CONFIG+=release "${srcdir}/${_gitname}-${pkgver}/GlobalProtect-openconnect.pro"
make
}
package() {
cd "$srcdir/${_gitname}-${pkgver}"
make INSTALL_ROOT="$pkgdir/" install
}

290
README.md
View File

@@ -1,133 +1,219 @@
# GlobalProtect-openconnect # GlobalProtect-openconnect
A GlobalProtect VPN client (GUI) for Linux based on Openconnect and built with Qt5, supports SAML auth mode, inspired by [gp-saml-gui](https://github.com/dlenski/gp-saml-gui).
A GUI for GlobalProtect VPN, based on OpenConnect, supports the SSO authentication method. Inspired by [gp-saml-gui](https://github.com/dlenski/gp-saml-gui).
<p align="center"> <p align="center">
<img src="screenshot.png"> <img width="300" src="https://github.com/yuezk/GlobalProtect-openconnect/assets/3297602/9242df9c-217d-42ab-8c21-8f9f69cd4eb5">
</p> </p>
## Features ## Features
- Similar user experience as the official client in macOS. - [x] Better Linux support
- Supports both SAML and non-SAML authentication modes. - [x] Support both CLI and GUI
- Supports automatically selecting the preferred gateway from the multiple gateways. - [x] Support both SSO and non-SSO authentication
- Supports switching gateway from the system tray menu manually. - [x] Support the FIDO2 authentication (e.g., YubiKey)
- [x] Support authentication using default browser
- [x] Support client certificate authentication
- [x] Support multiple portals
- [x] Support gateway selection
- [x] Support connect gateway directly
- [x] Support auto-connect on startup
- [x] Support system tray icon
## Passing the Custom Parameters to `OpenConnect` CLI ## Usage
Custom parameters can be appended to the `OpenConnect` CLI with the following settings. ### CLI
<p align="center">
<img src="https://user-images.githubusercontent.com/3297602/129464304-94eb8a2b-1c4a-47e1-b931-4422fff6eb81.png" />
<p>
## Prerequisites The CLI version is always free and open source in this repo. It has almost the same features as the GUI version.
- Openconnect v8.x
- Qt5, qt5-webengine, qt5-websockets
### Ubuntu
1. Install openconnect v8.x
```sh
sudo apt install openconnect
openconnect --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
For Ubuntu 20, this should work.
```sh
sudo apt install qtbase5-dev libqt5websockets5-dev qtwebengine5-dev qttools5-dev debhelper
```
For Ubuntu 21, you need to install the base pieces separately as QT5 is the default.
```sh
sudo apt install qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5websockets5-dev qtwebengine5-dev qttools5-dev debhelper
```
### OpenSUSE
Install the Qt dependencies
```sh
sudo zypper install libqt5-qtbase-devel libqt5-qtwebsockets-devel libqt5-qtwebengine-devel
```
### Fedora
Install the Qt dependencies:
```sh
sudo dnf install qt5-qtbase-devel qt5-qttools-devel qt5-qtwebengine qt5-qtwebsockets-devel qt5-qtwebengine-devel
```
## Install
### Install from AUR (Arch/Manjaro)
Install [globalprotect-openconnect](https://aur.archlinux.org/packages/globalprotect-openconnect/).
### Build from source code
```sh
git clone https://github.com/yuezk/GlobalProtect-openconnect.git
cd GlobalProtect-openconnect
git submodule update --init
# qmake or qmake-qt5
qmake CONFIG+=release
make
sudo make install
```
Open `GlobalProtect VPN` in the application dashboard.
### Debian package
Relatively manual process for now:
* Clone the source tree
``` ```
git clone https://github.com/yuezk/GlobalProtect-openconnect.git Usage: gpclient [OPTIONS] <COMMAND>
cd GlobalProtect-openconnect
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 git-archive-all using the pip. Remember to adjust the version numbers etc. To use the default browser for authentication with the CLI version, you need to use the following command:
``` ```bash
pip install git-archive-all sudo -E gpclient connect --default-browser <portal>
``` ```
* Next create an upstream source tree using git archive. ### GUI
The GUI version is also available after you installed it. You can launch it from the application menu or run `gpclient launch-gui` in the terminal.
> [!Note]
>
> The GUI version is partially open source. Its background service is open sourced in this repo as [gpservice](./apps/gpservice/). The GUI part is a wrapper of the background service, which is not open sourced.
## Installation
### Debian/Ubuntu based distributions
#### Install from PPA (Ubuntu 18.04 and later, except 24.04)
``` ```
git-archive-all --force-submodules --prefix=globalprotect-openconnect-1.3.0/ ../globalprotect-openconnect_1.3.0.orig.tar.gz sudo apt-get install gir1.2-gtk-3.0 gir1.2-webkit2-4.0
sudo add-apt-repository ppa:yuezk/globalprotect-openconnect
sudo apt-get update
sudo apt-get install globalprotect-openconnect
``` ```
* Finally extract the source tree, build the debian package, and install it. > [!Note]
>
> For Linux Mint, you might need to import the GPG key with: `sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 7937C393082992E5D6E4A60453FC26B43838D761` if you encountered an error `gpg: keyserver receive failed: General error`.
``` #### **Ubuntu 24.04 and later**
cd ..
tar -xzvf globalprotect-openconnect_1.3.0.orig.tar.gz The `libwebkit2gtk-4.0-37` package was [removed](https://bugs.launchpad.net/ubuntu/+source/webkit2gtk/+bug/2061914) from its repo, before [the issue](https://github.com/yuezk/GlobalProtect-openconnect/issues/351) gets resolved, you need to install them manually:
cd globalprotect-openconnect-1.3.0
fakeroot dpkg-buildpackage -uc -us -sa 2>&1 | tee ../build.log ```bash
sudo dpkg -i globalprotect-openconnect_1.3.0-1ppa1_amd64.deb wget http://launchpadlibrarian.net/704701349/libwebkit2gtk-4.0-37_2.43.3-1_amd64.deb
wget http://launchpadlibrarian.net/704701345/libjavascriptcoregtk-4.0-18_2.43.3-1_amd64.deb
sudo dpkg --install *.deb
``` ```
### NixOS And the latest package is not available in the PPA, you can follow the [Install from deb package](#install-from-deb-package) section to install the latest package.
In `configuration.nix`:
``` #### **Ubuntu 18.04**
services.globalprotect = {
enable = true;
# if you need a Host Integrity Protection report
csdWrapper = "${pkgs.openconnect}/libexec/openconnect/hipreport.sh";
};
environment.systemPackages = [ globalprotect-openconnect ]; The latest package is not available in the PPA either, but you still needs to add the `ppa:yuezk/globalprotect-openconnect` repo beforehand to use the required `openconnect` package. Then you can follow the [Install from deb package](#install-from-deb-package) section to install the latest package.
#### Install from deb package
Download the latest deb package from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. Then install it with `apt`:
```bash
sudo apt install --fix-broken globalprotect-openconnect_*.deb
``` ```
### Arch Linux / Manjaro
#### Install from AUR
Install from AUR: [globalprotect-openconnect-git](https://aur.archlinux.org/packages/globalprotect-openconnect-git/)
```
yay -S globalprotect-openconnect-git
```
#### Install from package
Download the latest package from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. Then install it with `pacman`:
```bash
sudo pacman -U globalprotect-openconnect-*.pkg.tar.zst
```
### Fedora 38 and later / Fedora Rawhide
#### Install from COPR
The package is available on [COPR](https://copr.fedorainfracloud.org/coprs/yuezk/globalprotect-openconnect/) for various RPM-based distributions. You can install it with the following commands:
```
sudo dnf copr enable yuezk/globalprotect-openconnect
sudo dnf install globalprotect-openconnect
```
### openSUSE Leap 15.6 / openSUSE Tumbleweed
#### Install from OBS (openSUSE Build Service)
The package is also available on [OBS](https://build.opensuse.org/package/show/home:yuezk/globalprotect-openconnect) for various RPM-based distributions. You can follow the instructions [on this page](https://software.opensuse.org//download.html?project=home%3Ayuezk&package=globalprotect-openconnect) to install it.
### Other RPM-based distributions
#### Install from RPM package
Download the latest RPM package from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page.
```bash
sudo rpm -i globalprotect-openconnect-*.rpm
```
### Gentoo
Install from the ```rios``` or ```slonko``` overlays. Example using rios:
#### 1. Enable the overlay
```
sudo eselect repository enable rios
```
#### 2. Sync with the repository
- If you have eix installed, use it:
```
sudo eix-sync
```
- Otherwise, use:
```
sudo emerge --sync
```
#### 3. Install
```sudo emerge globalprotect-openconnect```
### Other distributions
- Install `openconnect >= 8.20`, `webkit2gtk`, `libsecret`, `libayatana-appindicator` or `libappindicator-gtk3`.
- Download `globalprotect-openconnect_${version}_${arch}.bin.tar.xz` from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page.
- Extract the tarball with `tar -xJf globalprotect-openconnect_${version}_${arch}.bin.tar.xz`.
- Run `sudo make install` to install the client.
## Build from source
You can also build the client from source, steps are as follows:
### Prerequisites
- [Install Rust](https://www.rust-lang.org/tools/install)
- Install Tauri dependencies: https://tauri.app/v1/guides/getting-started/prerequisites/#setting-up-linux
- Install `perl`
- Install `openconnect >= 8.20` and `libopenconnect-dev` (or `openconnect-devel` on RPM-based distributions)
- Install `pkexec`, `gnome-keyring` (or `pam_kwallet` on KDE)
### Build
1. Download the source code tarball from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. Choose `globalprotect-openconnect-${version}.tar.gz`.
2. Extract the tarball with `tar -xzf globalprotect-openconnect-${version}.tar.gz`.
3. Enter the source directory and run `make build BUILD_FE=0` to build the client.
3. Run `sudo make install` to install the client. (Note, `DESTDIR` is not supported)
## FAQ
1. How to deal with error `Secure Storage not ready`
Try upgrade the client to `2.2.0` or later, which will use a file-based storage as a fallback.
You need to install the `gnome-keyring` package, and restart the system (See [#321](https://github.com/yuezk/GlobalProtect-openconnect/issues/321), [#316](https://github.com/yuezk/GlobalProtect-openconnect/issues/316)).
2. How to deal with error `(gpauth:18869): Gtk-WARNING **: 10:33:37.566: cannot open display:`
If you encounter this error when using the CLI version, try to run the command with `sudo -E` (See [#316](https://github.com/yuezk/GlobalProtect-openconnect/issues/316)).
## About Trial
The CLI version is always free, while the GUI version is paid. There are two trial modes for the GUI version:
1. 10-day trial: You can use the GUI stable release for 10 days after the installation.
2. 14-day trial: Each beta release has a fresh trial period (at most 14 days) after released.
## [License](./LICENSE) ## [License](./LICENSE)
GPLv3 GPLv3

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

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

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: 44 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 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,523 @@
use std::{
rc::Rc,
sync::Arc,
time::{Duration, Instant},
};
use anyhow::bail;
use gpapi::{
auth::SamlAuthData,
error::AuthDataParseError,
gp_params::GpParams,
portal::{prelogin, Prelogin},
utils::{redact::redact_uri, window::WindowExt},
};
use log::{info, warn};
use regex::Regex;
use tauri::{AppHandle, Window, WindowEvent, WindowUrl};
use tokio::sync::{mpsc, oneshot, RwLock};
use tokio_util::sync::CancellationToken;
use webkit2gtk::{
gio::Cancellable,
glib::{GString, TimeSpan},
LoadEvent, SettingsExt, TLSErrorsPolicy, URIResponse, URIResponseExt, WebContextExt, WebResource, WebResourceExt,
WebView, WebViewExt, WebsiteDataManagerExtManual, WebsiteDataTypes,
};
enum AuthDataError {
/// Failed to load page due to TLS error
TlsError,
/// 1. Found auth data in headers/body but it's invalid
/// 2. Loaded an empty page, failed to load page. etc.
Invalid,
/// No auth data found in headers/body
NotFound,
}
type AuthResult = Result<SamlAuthData, AuthDataError>;
pub(crate) struct AuthWindow<'a> {
app_handle: AppHandle,
server: &'a str,
saml_request: &'a str,
user_agent: &'a str,
gp_params: Option<GpParams>,
clean: bool,
}
impl<'a> AuthWindow<'a> {
pub fn new(app_handle: AppHandle) -> Self {
Self {
app_handle,
server: "",
saml_request: "",
user_agent: "",
gp_params: None,
clean: false,
}
}
pub fn server(mut self, server: &'a str) -> Self {
self.server = server;
self
}
pub fn saml_request(mut self, saml_request: &'a str) -> Self {
self.saml_request = saml_request;
self
}
pub fn user_agent(mut self, user_agent: &'a str) -> Self {
self.user_agent = user_agent;
self
}
pub fn gp_params(mut self, gp_params: GpParams) -> Self {
self.gp_params.replace(gp_params);
self
}
pub fn clean(mut self, clean: bool) -> Self {
self.clean = clean;
self
}
pub async fn open(&self) -> anyhow::Result<SamlAuthData> {
info!("Open auth window, user_agent: {}", self.user_agent);
let window = Window::builder(&self.app_handle, "auth_window", WindowUrl::default())
.title("GlobalProtect Login")
// .user_agent(self.user_agent)
.focused(true)
.visible(false)
.center()
.build()?;
let window = Arc::new(window);
let cancel_token = CancellationToken::new();
let cancel_token_clone = cancel_token.clone();
window.on_window_event(move |event| {
if let WindowEvent::CloseRequested { .. } = event {
cancel_token_clone.cancel();
}
});
let window_clone = Arc::clone(&window);
let timeout_secs = 15;
tokio::spawn(async move {
tokio::time::sleep(Duration::from_secs(timeout_secs)).await;
let visible = window_clone.is_visible().unwrap_or(false);
if !visible {
info!("Try to raise auth window after {} seconds", timeout_secs);
raise_window(&window_clone);
}
});
tokio::select! {
_ = cancel_token.cancelled() => {
bail!("Auth cancelled");
}
saml_result = self.auth_loop(&window) => {
window.close()?;
saml_result
}
}
}
async fn auth_loop(&self, window: &Arc<Window>) -> anyhow::Result<SamlAuthData> {
let saml_request = self.saml_request.to_string();
let (auth_result_tx, mut auth_result_rx) = mpsc::unbounded_channel::<AuthResult>();
let raise_window_cancel_token: Arc<RwLock<Option<CancellationToken>>> = Default::default();
let gp_params = self.gp_params.as_ref().unwrap();
let tls_err_policy = if gp_params.ignore_tls_errors() {
TLSErrorsPolicy::Ignore
} else {
TLSErrorsPolicy::Fail
};
if self.clean {
clear_webview_cookies(window).await?;
}
let raise_window_cancel_token_clone = Arc::clone(&raise_window_cancel_token);
window.with_webview(move |wv| {
let wv = wv.inner();
if let Some(context) = wv.context() {
context.set_tls_errors_policy(tls_err_policy);
}
if let Some(settings) = wv.settings() {
let ua = settings.user_agent().unwrap_or("".into());
info!("Auth window user agent: {}", ua);
}
// Load the initial SAML request
load_saml_request(&wv, &saml_request);
let auth_result_tx_clone = auth_result_tx.clone();
wv.connect_load_changed(move |wv, event| {
if event == LoadEvent::Started {
let Ok(mut cancel_token) = raise_window_cancel_token_clone.try_write() else {
return;
};
// Cancel the raise window task
if let Some(cancel_token) = cancel_token.take() {
cancel_token.cancel();
}
return;
}
if event != LoadEvent::Finished {
return;
}
if let Some(main_resource) = wv.main_resource() {
let uri = main_resource.uri().unwrap_or("".into());
if uri.is_empty() {
warn!("Loaded an empty uri");
send_auth_result(&auth_result_tx_clone, Err(AuthDataError::Invalid));
return;
}
info!("Loaded uri: {}", redact_uri(&uri));
if uri.starts_with("globalprotectcallback:") {
return;
}
read_auth_data(&main_resource, auth_result_tx_clone.clone());
}
});
let auth_result_tx_clone = auth_result_tx.clone();
wv.connect_load_failed_with_tls_errors(move |_wv, uri, cert, err| {
let redacted_uri = redact_uri(uri);
warn!(
"Failed to load uri: {} with error: {}, cert: {}",
redacted_uri, err, cert
);
send_auth_result(&auth_result_tx_clone, Err(AuthDataError::TlsError));
true
});
wv.connect_load_failed(move |_wv, _event, uri, err| {
let redacted_uri = redact_uri(uri);
if !uri.starts_with("globalprotectcallback:") {
warn!("Failed to load uri: {} with error: {}", redacted_uri, err);
}
// NOTE: Don't send error here, since load_changed event will be triggered after this
// send_auth_result(&auth_result_tx, Err(AuthDataError::Invalid));
// true to stop other handlers from being invoked for the event. false to propagate the event further.
true
});
})?;
let portal = self.server.to_string();
loop {
if let Some(auth_result) = auth_result_rx.recv().await {
match auth_result {
Ok(auth_data) => return Ok(auth_data),
Err(AuthDataError::TlsError) => bail!("TLS error: certificate verify failed"),
Err(AuthDataError::NotFound) => {
info!("No auth data found, it may not be the /SAML20/SP/ACS endpoint");
// The user may need to interact with the auth window, raise it in 3 seconds
if !window.is_visible().unwrap_or(false) {
let window = Arc::clone(window);
let cancel_token = CancellationToken::new();
raise_window_cancel_token.write().await.replace(cancel_token.clone());
tokio::spawn(async move {
let delay_secs = 1;
info!("Raise window in {} second(s)", delay_secs);
tokio::select! {
_ = tokio::time::sleep(Duration::from_secs(delay_secs)) => {
raise_window(&window);
}
_ = cancel_token.cancelled() => {
info!("Raise window cancelled");
}
}
});
}
}
Err(AuthDataError::Invalid) => {
info!("Got invalid auth data, retrying...");
window.with_webview(|wv| {
let wv = wv.inner();
wv.run_javascript(r#"
var loading = document.createElement("div");
loading.innerHTML = '<div style="position: absolute; width: 100%; text-align: center; font-size: 20px; font-weight: bold; top: 50%; left: 50%; transform: translate(-50%, -50%);">Got invalid token, retrying...</div>';
loading.style = "position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255, 255, 255, 0.85); z-index: 99999;";
document.body.appendChild(loading);
"#,
Cancellable::NONE,
|_| info!("Injected loading element successfully"),
);
})?;
let saml_request = portal_prelogin(&portal, gp_params).await?;
window.with_webview(move |wv| {
let wv = wv.inner();
load_saml_request(&wv, &saml_request);
})?;
}
}
}
}
}
}
fn raise_window(window: &Arc<Window>) {
let visible = window.is_visible().unwrap_or(false);
if !visible {
if let Err(err) = window.raise() {
warn!("Failed to raise window: {}", err);
}
}
}
pub async fn portal_prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<String> {
match prelogin(portal, gp_params).await? {
Prelogin::Saml(prelogin) => Ok(prelogin.saml_request().to_string()),
Prelogin::Standard(_) => bail!("Received non-SAML prelogin response"),
}
}
fn send_auth_result(auth_result_tx: &mpsc::UnboundedSender<AuthResult>, auth_result: AuthResult) {
if let Err(err) = auth_result_tx.send(auth_result) {
warn!("Failed to send auth event: {}", err);
}
}
fn load_saml_request(wv: &Rc<WebView>, saml_request: &str) {
if saml_request.starts_with("http") {
info!("Load the SAML request as URI...");
wv.load_uri(saml_request);
} else {
info!("Load the SAML request as HTML...");
wv.load_html(saml_request, None);
}
}
fn read_auth_data_from_headers(response: &URIResponse) -> AuthResult {
response.http_headers().map_or_else(
|| {
info!("No headers found in response");
Err(AuthDataError::NotFound)
},
|mut headers| match headers.get("saml-auth-status") {
Some(status) if status == "1" => {
let username = headers.get("saml-username").map(GString::into);
let prelogin_cookie = headers.get("prelogin-cookie").map(GString::into);
let portal_userauthcookie = headers.get("portal-userauthcookie").map(GString::into);
if SamlAuthData::check(&username, &prelogin_cookie, &portal_userauthcookie) {
return Ok(SamlAuthData::new(
username.unwrap(),
prelogin_cookie,
portal_userauthcookie,
));
}
info!("Found invalid auth data in headers");
Err(AuthDataError::Invalid)
}
Some(status) => {
info!("Found invalid SAML status: {} in headers", status);
Err(AuthDataError::Invalid)
}
None => {
info!("No saml-auth-status header found");
Err(AuthDataError::NotFound)
}
},
)
}
fn read_auth_data_from_body<F>(main_resource: &WebResource, callback: F)
where
F: FnOnce(Result<SamlAuthData, AuthDataParseError>) + Send + 'static,
{
main_resource.data(Cancellable::NONE, |data| match data {
Ok(data) => {
let html = String::from_utf8_lossy(&data);
callback(read_auth_data_from_html(&html));
}
Err(err) => {
info!("Failed to read response body: {}", err);
callback(Err(AuthDataParseError::Invalid))
}
});
}
fn read_auth_data_from_html(html: &str) -> Result<SamlAuthData, AuthDataParseError> {
if html.contains("Temporarily Unavailable") {
info!("Found 'Temporarily Unavailable' in HTML, auth failed");
return Err(AuthDataParseError::Invalid);
}
SamlAuthData::from_html(html).or_else(|err| {
if let Some(gpcallback) = extract_gpcallback(html) {
info!("Found gpcallback from html...");
SamlAuthData::from_gpcallback(&gpcallback)
} else {
Err(err)
}
})
}
fn extract_gpcallback(html: &str) -> Option<String> {
let re = Regex::new(r#"globalprotectcallback:[^"]+"#).unwrap();
re.captures(html)
.and_then(|captures| captures.get(0))
.map(|m| html_escape::decode_html_entities(m.as_str()).to_string())
}
fn read_auth_data(main_resource: &WebResource, auth_result_tx: mpsc::UnboundedSender<AuthResult>) {
let Some(response) = main_resource.response() else {
info!("No response found in main resource");
send_auth_result(&auth_result_tx, Err(AuthDataError::Invalid));
return;
};
info!("Trying to read auth data from response headers...");
match read_auth_data_from_headers(&response) {
Ok(auth_data) => {
info!("Got auth data from headers");
send_auth_result(&auth_result_tx, Ok(auth_data));
}
Err(AuthDataError::Invalid) => {
info!("Found invalid auth data in headers, trying to read from body...");
read_auth_data_from_body(main_resource, move |auth_result| {
// Since we have already found invalid auth data in headers, which means this could be the `/SAML20/SP/ACS` endpoint
// any error result from body should be considered as invalid, and trigger a retry
let auth_result = auth_result.map_err(|err| {
info!("Failed to read auth data from body: {}", err);
AuthDataError::Invalid
});
send_auth_result(&auth_result_tx, auth_result);
});
}
Err(AuthDataError::NotFound) => {
info!("No auth data found in headers, trying to read from body...");
let is_acs_endpoint = main_resource.uri().map_or(false, |uri| uri.contains("/SAML20/SP/ACS"));
read_auth_data_from_body(main_resource, move |auth_result| {
// If the endpoint is `/SAML20/SP/ACS` and no auth data found in body, it should be considered as invalid
let auth_result = auth_result.map_err(|err| {
info!("Failed to read auth data from body: {}", err);
if !is_acs_endpoint && matches!(err, AuthDataParseError::NotFound) {
AuthDataError::NotFound
} else {
AuthDataError::Invalid
}
});
send_auth_result(&auth_result_tx, auth_result)
});
}
Err(AuthDataError::TlsError) => {
// NOTE: This is unreachable
info!("TLS error found in headers, trying to read from body...");
send_auth_result(&auth_result_tx, Err(AuthDataError::TlsError));
}
}
}
pub(crate) async fn clear_webview_cookies(window: &Window) -> anyhow::Result<()> {
let (tx, rx) = oneshot::channel::<Result<(), String>>();
window.with_webview(|wv| {
let send_result = move |result: Result<(), String>| {
if let Err(err) = tx.send(result) {
info!("Failed to send result: {:?}", err);
}
};
let wv = wv.inner();
let context = match wv.context() {
Some(context) => context,
None => {
send_result(Err("No webview context found".into()));
return;
}
};
let data_manager = match context.website_data_manager() {
Some(manager) => manager,
None => {
send_result(Err("No data manager found".into()));
return;
}
};
let now = Instant::now();
data_manager.clear(
WebsiteDataTypes::COOKIES,
TimeSpan(0),
Cancellable::NONE,
move |result| match result {
Err(err) => {
send_result(Err(err.to_string()));
}
Ok(_) => {
info!("Cookies cleared in {} ms", now.elapsed().as_millis());
send_result(Ok(()));
}
},
);
})?;
rx.await?.map_err(|err| anyhow::anyhow!(err))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extract_gpcallback_some() {
let html = r#"
<meta http-equiv="refresh" content="0; URL=globalprotectcallback:PGh0bWw+PCEtLSA8c">
<meta http-equiv="refresh" content="0; URL=globalprotectcallback:PGh0bWw+PCEtLSA8c">
"#;
assert_eq!(
extract_gpcallback(html).as_deref(),
Some("globalprotectcallback:PGh0bWw+PCEtLSA8c")
);
}
#[test]
fn extract_gpcallback_cas() {
let html = r#"
<meta http-equiv="refresh" content="0; URL=globalprotectcallback:cas-as=1&amp;un=xyz@email.com&amp;token=very_long_string">
"#;
assert_eq!(
extract_gpcallback(html).as_deref(),
Some("globalprotectcallback:cas-as=1&un=xyz@email.com&token=very_long_string")
);
}
#[test]
fn extract_gpcallback_none() {
let html = r#"
<meta http-equiv="refresh" content="0; URL=PGh0bWw+PCEtLSA8c">
"#;
assert_eq!(extract_gpcallback(html), None);
}
}

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

@@ -0,0 +1,174 @@
use clap::Parser;
use gpapi::{
auth::{SamlAuthData, SamlAuthResult},
clap::args::Os,
gp_params::{ClientOs, GpParams},
process::browser_authenticator::BrowserAuthenticator,
utils::{normalize_server, openssl},
GP_USER_AGENT,
};
use log::{info, LevelFilter};
use serde_json::json;
use tauri::{App, AppHandle, RunEvent};
use tempfile::NamedTempFile;
use crate::auth_window::{portal_prelogin, AuthWindow};
const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), " (", compile_time::date_str!(), ")");
#[derive(Parser, Clone)]
#[command(version = VERSION)]
struct Cli {
server: String,
#[arg(long)]
gateway: bool,
#[arg(long)]
saml_request: Option<String>,
#[arg(long, default_value = GP_USER_AGENT)]
user_agent: String,
#[arg(long, default_value = "Linux")]
os: Os,
#[arg(long)]
os_version: Option<String>,
#[arg(long)]
hidpi: bool,
#[arg(long)]
fix_openssl: bool,
#[arg(long)]
ignore_tls_errors: bool,
#[arg(long)]
clean: bool,
#[arg(long)]
default_browser: bool,
}
impl Cli {
async fn run(&mut self) -> anyhow::Result<()> {
if self.ignore_tls_errors {
info!("TLS errors will be ignored");
}
let mut openssl_conf = self.prepare_env()?;
self.server = normalize_server(&self.server)?;
let gp_params = self.build_gp_params();
// Get the initial SAML request
let saml_request = match self.saml_request {
Some(ref saml_request) => saml_request.clone(),
None => portal_prelogin(&self.server, &gp_params).await?,
};
if self.default_browser {
let browser_auth = BrowserAuthenticator::new(&saml_request);
browser_auth.authenticate()?;
info!("Please continue the authentication process in the default browser");
return Ok(());
}
self.saml_request.replace(saml_request);
let app = create_app(self.clone())?;
app.run(move |_app_handle, event| {
if let RunEvent::Exit = event {
if let Some(file) = openssl_conf.take() {
if let Err(err) = file.close() {
info!("Error closing OpenSSL config file: {}", err);
}
}
}
});
Ok(())
}
fn prepare_env(&self) -> anyhow::Result<Option<NamedTempFile>> {
std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1");
if self.hidpi {
info!("Setting GDK_SCALE=2 and GDK_DPI_SCALE=0.5");
std::env::set_var("GDK_SCALE", "2");
std::env::set_var("GDK_DPI_SCALE", "0.5");
}
if self.fix_openssl {
info!("Fixing OpenSSL environment");
let file = openssl::fix_openssl_env()?;
return Ok(Some(file));
}
Ok(None)
}
fn build_gp_params(&self) -> GpParams {
let gp_params = GpParams::builder()
.user_agent(&self.user_agent)
.client_os(ClientOs::from(&self.os))
.os_version(self.os_version.clone())
.ignore_tls_errors(self.ignore_tls_errors)
.is_gateway(self.gateway)
.build();
gp_params
}
async fn saml_auth(&self, app_handle: AppHandle) -> anyhow::Result<SamlAuthData> {
let auth_window = AuthWindow::new(app_handle)
.server(&self.server)
.user_agent(&self.user_agent)
.gp_params(self.build_gp_params())
.saml_request(self.saml_request.as_ref().unwrap())
.clean(self.clean);
auth_window.open().await
}
}
fn create_app(cli: Cli) -> anyhow::Result<App> {
let app = tauri::Builder::default()
.setup(|app| {
let app_handle = app.handle();
tauri::async_runtime::spawn(async move {
let auth_result = match cli.saml_auth(app_handle.clone()).await {
Ok(auth_data) => SamlAuthResult::Success(auth_data),
Err(err) => SamlAuthResult::Failure(format!("{}", err)),
};
println!("{}", json!(auth_result));
});
Ok(())
})
.build(tauri::generate_context!())?;
Ok(app)
}
fn init_logger() {
env_logger::builder().filter_level(LevelFilter::Info).init();
}
pub async fn run() {
let mut cli = Cli::parse();
init_logger();
info!("gpauth started: {}", VERSION);
if let Err(err) = cli.run().await {
eprintln!("\nError: {}", err);
if err.to_string().contains("unsafe legacy renegotiation") && !cli.fix_openssl {
eprintln!("\nRe-run it with the `--fix-openssl` option to work around this issue, e.g.:\n");
// Print the command
let args = std::env::args().collect::<Vec<_>>();
eprintln!("{} --fix-openssl {}\n", args[0], args[1..].join(" "));
}
std::process::exit(1);
}
}

9
apps/gpauth/src/main.rs Normal file
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": []
}
}

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

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

119
apps/gpclient/src/cli.rs Normal file
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,389 @@
use std::{cell::RefCell, fs, sync::Arc};
use clap::Args;
use common::vpn_utils::find_csd_wrapper;
use gpapi::{
clap::args::Os,
credential::{Credential, PasswordCredential},
error::PortalError,
gateway::{gateway_login, GatewayLogin},
gp_params::{ClientOs, GpParams},
portal::{prelogin, retrieve_config, Prelogin},
process::{
auth_launcher::SamlAuthLauncher,
users::{get_non_root_user, get_user_by_name},
},
utils::{request::RequestIdentityError, shutdown_signal},
GP_USER_AGENT,
};
use inquire::{Password, PasswordDisplayMode, Select, Text};
use log::info;
use openconnect::Vpn;
use tokio::{io::AsyncReadExt, net::TcpListener};
use crate::{cli::SharedArgs, GP_CLIENT_LOCK_FILE, GP_CLIENT_PORT_FILE};
#[derive(Args)]
pub(crate) struct ConnectArgs {
#[arg(help = "The portal server to connect to")]
server: String,
#[arg(short, long, help = "The gateway to connect to, it will prompt if not specified")]
gateway: Option<String>,
#[arg(short, long, help = "The username to use, it will prompt if not specified")]
user: Option<String>,
#[arg(long, short, help = "The VPNC script to use")]
script: Option<String>,
#[arg(long, help = "Connect the server as a gateway, instead of a portal")]
as_gateway: bool,
#[arg(
long,
help = "Use the default CSD wrapper to generate the HIP report and send it to the server"
)]
hip: bool,
#[arg(
short,
long,
help = "Use SSL client certificate file in pkcs#8 (.pem) or pkcs#12 (.p12, .pfx) format"
)]
certificate: Option<String>,
#[arg(short = 'k', long, help = "Use SSL private key file in pkcs#8 (.pem) format")]
sslkey: Option<String>,
#[arg(short = 'p', long, help = "The key passphrase of the private key")]
key_password: Option<String>,
#[arg(long, help = "Same as the '--csd-user' option in the openconnect command")]
csd_user: Option<String>,
#[arg(long, help = "Same as the '--csd-wrapper' option in the openconnect command")]
csd_wrapper: Option<String>,
#[arg(long, default_value = "300", help = "Reconnection retry timeout in seconds")]
reconnect_timeout: u32,
#[arg(short, long, help = "Request MTU from server (legacy servers only)")]
mtu: Option<u32>,
#[arg(long, help = "Do not ask for IPv6 connectivity")]
disable_ipv6: bool,
#[arg(long, default_value = GP_USER_AGENT, help = "The user agent to use")]
user_agent: String,
#[arg(long, default_value = "Linux")]
os: Os,
#[arg(long)]
os_version: Option<String>,
#[arg(long, help = "The HiDPI mode, useful for high resolution screens")]
hidpi: bool,
#[arg(long, help = "Do not reuse the remembered authentication cookie")]
clean: bool,
#[arg(long, help = "Use the default browser to authenticate")]
default_browser: bool,
}
impl ConnectArgs {
fn os_version(&self) -> String {
if let Some(os_version) = &self.os_version {
return os_version.to_owned();
}
match self.os {
Os::Linux => format!("Linux {}", whoami::distro()),
Os::Windows => String::from("Microsoft Windows 11 Pro , 64-bit"),
Os::Mac => String::from("Apple Mac OS X 13.4.0"),
}
}
}
pub(crate) struct ConnectHandler<'a> {
args: &'a ConnectArgs,
shared_args: &'a SharedArgs,
latest_key_password: RefCell<Option<String>>,
}
impl<'a> ConnectHandler<'a> {
pub(crate) fn new(args: &'a ConnectArgs, shared_args: &'a SharedArgs) -> Self {
Self {
args,
shared_args,
latest_key_password: Default::default(),
}
}
fn build_gp_params(&self) -> GpParams {
GpParams::builder()
.user_agent(&self.args.user_agent)
.client_os(ClientOs::from(&self.args.os))
.os_version(self.args.os_version())
.ignore_tls_errors(self.shared_args.ignore_tls_errors)
.certificate(self.args.certificate.clone())
.sslkey(self.args.sslkey.clone())
.key_password(self.latest_key_password.borrow().clone())
.build()
}
pub(crate) async fn handle(&self) -> anyhow::Result<()> {
self.latest_key_password.replace(self.args.key_password.clone());
loop {
let Err(err) = self.handle_impl().await else {
return Ok(());
};
let Some(root_cause) = err.root_cause().downcast_ref::<RequestIdentityError>() else {
return Err(err);
};
match root_cause {
RequestIdentityError::NoKey => {
eprintln!("ERROR: No private key found in the certificate file");
eprintln!("ERROR: Please provide the private key file using the `-k` option");
return Ok(());
}
RequestIdentityError::NoPassphrase(cert_type) | RequestIdentityError::DecryptError(cert_type) => {
// Decrypt the private key error, ask for the key password
let message = format!("Enter the {} passphrase:", cert_type);
let password = Password::new(&message)
.without_confirmation()
.with_display_mode(PasswordDisplayMode::Masked)
.prompt()?;
self.latest_key_password.replace(Some(password));
}
}
}
}
pub(crate) async fn handle_impl(&self) -> anyhow::Result<()> {
let server = self.args.server.as_str();
let as_gateway = self.args.as_gateway;
if as_gateway {
info!("Treating the server as a gateway");
return self.connect_gateway_with_prelogin(server).await;
}
let Err(err) = self.connect_portal_with_prelogin(server).await else {
return Ok(());
};
info!("Failed to connect portal with prelogin: {}", err);
if err.root_cause().downcast_ref::<PortalError>().is_some() {
info!("Trying the gateway authentication workflow...");
self.connect_gateway_with_prelogin(server).await?;
eprintln!("\nNOTE: the server may be a gateway, not a portal.");
eprintln!("NOTE: try to use the `--as-gateway` option if you were authenticated twice.");
Ok(())
} else {
Err(err)
}
}
async fn connect_portal_with_prelogin(&self, portal: &str) -> anyhow::Result<()> {
let gp_params = self.build_gp_params();
let prelogin = prelogin(portal, &gp_params).await?;
let cred = self.obtain_credential(&prelogin, portal).await?;
let mut portal_config = retrieve_config(portal, &cred, &gp_params).await?;
let selected_gateway = match &self.args.gateway {
Some(gateway) => portal_config
.find_gateway(gateway)
.ok_or_else(|| anyhow::anyhow!("Cannot find gateway specified: {}", gateway))?,
None => {
portal_config.sort_gateways(prelogin.region());
let gateways = portal_config.gateways();
if gateways.len() > 1 {
let gateway = Select::new("Which gateway do you want to connect to?", gateways)
.with_vim_mode(true)
.prompt()?;
info!("Connecting to the selected gateway: {}", gateway);
gateway
} else {
info!("Connecting to the only available gateway: {}", gateways[0]);
gateways[0]
}
}
};
let gateway = selected_gateway.server();
let cred = portal_config.auth_cookie().into();
let cookie = match self.login_gateway(gateway, &cred, &gp_params).await {
Ok(cookie) => cookie,
Err(err) => {
info!("Gateway login failed: {}", err);
return self.connect_gateway_with_prelogin(gateway).await;
}
};
self.connect_gateway(gateway, &cookie).await
}
async fn connect_gateway_with_prelogin(&self, gateway: &str) -> anyhow::Result<()> {
info!("Performing the gateway authentication...");
let mut gp_params = self.build_gp_params();
gp_params.set_is_gateway(true);
let prelogin = prelogin(gateway, &gp_params).await?;
let cred = self.obtain_credential(&prelogin, gateway).await?;
let cookie = self.login_gateway(gateway, &cred, &gp_params).await?;
self.connect_gateway(gateway, &cookie).await
}
async fn login_gateway(&self, gateway: &str, cred: &Credential, gp_params: &GpParams) -> anyhow::Result<String> {
let mut gp_params = gp_params.clone();
loop {
match gateway_login(gateway, cred, &gp_params).await? {
GatewayLogin::Cookie(cookie) => return Ok(cookie),
GatewayLogin::Mfa(message, input_str) => {
let otp = Text::new(&message).prompt()?;
gp_params.set_input_str(&input_str);
gp_params.set_otp(&otp);
info!("Retrying gateway login with MFA...");
}
}
}
}
async fn connect_gateway(&self, gateway: &str, cookie: &str) -> anyhow::Result<()> {
let mtu = self.args.mtu.unwrap_or(0);
let csd_uid = get_csd_uid(&self.args.csd_user)?;
let csd_wrapper = if self.args.csd_wrapper.is_some() {
self.args.csd_wrapper.clone()
} else if self.args.hip {
find_csd_wrapper()
} else {
None
};
let vpn = Vpn::builder(gateway, cookie)
.script(self.args.script.clone())
.user_agent(self.args.user_agent.clone())
.certificate(self.args.certificate.clone())
.sslkey(self.args.sslkey.clone())
.key_password(self.latest_key_password.borrow().clone())
.csd_uid(csd_uid)
.csd_wrapper(csd_wrapper)
.reconnect_timeout(self.args.reconnect_timeout)
.mtu(mtu)
.disable_ipv6(self.args.disable_ipv6)
.build()?;
let vpn = Arc::new(vpn);
let vpn_clone = vpn.clone();
// Listen for the interrupt signal in the background
tokio::spawn(async move {
shutdown_signal().await;
info!("Received the interrupt signal, disconnecting...");
vpn_clone.disconnect();
});
vpn.connect(write_pid_file);
if fs::metadata(GP_CLIENT_LOCK_FILE).is_ok() {
info!("Removing PID file");
fs::remove_file(GP_CLIENT_LOCK_FILE)?;
}
Ok(())
}
async fn obtain_credential(&self, prelogin: &Prelogin, server: &str) -> anyhow::Result<Credential> {
let is_gateway = prelogin.is_gateway();
match prelogin {
Prelogin::Saml(prelogin) => {
let use_default_browser = prelogin.support_default_browser() && self.args.default_browser;
let cred = SamlAuthLauncher::new(&self.args.server)
.gateway(is_gateway)
.saml_request(prelogin.saml_request())
.user_agent(&self.args.user_agent)
.os(self.args.os.as_str())
.os_version(Some(&self.args.os_version()))
.hidpi(self.args.hidpi)
.fix_openssl(self.shared_args.fix_openssl)
.ignore_tls_errors(self.shared_args.ignore_tls_errors)
.clean(self.args.clean)
.default_browser(use_default_browser)
.launch()
.await?;
if let Some(cred) = cred {
return Ok(cred);
}
if !use_default_browser {
// This should never happen
unreachable!("SAML authentication failed without using the default browser");
}
info!("Waiting for the browser authentication to complete...");
wait_credentials().await
}
Prelogin::Standard(prelogin) => {
let prefix = if is_gateway { "Gateway" } else { "Portal" };
println!("{} ({}: {})", prelogin.auth_message(), prefix, server);
let user = self.args.user.as_ref().map_or_else(
|| Text::new(&format!("{}:", prelogin.label_username())).prompt(),
|user| Ok(user.to_owned()),
)?;
let password = Password::new(&format!("{}:", prelogin.label_password()))
.without_confirmation()
.with_display_mode(PasswordDisplayMode::Masked)
.prompt()?;
let password_cred = PasswordCredential::new(&user, &password);
Ok(password_cred.into())
}
}
}
}
async fn wait_credentials() -> anyhow::Result<Credential> {
// Start a local server to receive the browser authentication data
let listener = TcpListener::bind("127.0.0.1:0").await?;
let port = listener.local_addr()?.port();
// Write the port to a file
fs::write(GP_CLIENT_PORT_FILE, port.to_string())?;
info!("Listening authentication data on port {}", port);
let (mut socket, _) = listener.accept().await?;
info!("Received the browser authentication data from the socket");
let mut data = String::new();
socket.read_to_string(&mut data).await?;
// Remove the port file
fs::remove_file(GP_CLIENT_PORT_FILE)?;
Credential::from_gpcallback(&data)
}
fn write_pid_file() {
let pid = std::process::id();
fs::write(GP_CLIENT_LOCK_FILE, pid.to_string()).unwrap();
info!("Wrote PID {} to {}", pid, GP_CLIENT_LOCK_FILE);
}
fn get_csd_uid(csd_user: &Option<String>) -> anyhow::Result<u32> {
if let Some(csd_user) = csd_user {
get_user_by_name(csd_user).map(|user| user.uid())
} else {
get_non_root_user().map_or_else(|_| Ok(0), |user| Ok(user.uid()))
}
}

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

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

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

View File

@@ -0,0 +1,36 @@
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react/jsx-runtime",
"plugin:react-hooks/recommended",
"prettier",
],
overrides: [
{
env: {
node: true,
},
files: [".eslintrc.{js,cjs}"],
parserOptions: {
sourceType: "script",
},
},
],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
plugins: ["@typescript-eslint", "react"],
rules: {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "error",
"@typescript-eslint/no-unused-vars": "warn",
},
};

25
apps/gpgui-helper/.gitignore vendored Normal file
View File

@@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.vite

View File

View File

@@ -0,0 +1,3 @@
{
"printWidth": 100
}

View File

@@ -0,0 +1,7 @@
# Tauri + React + Typescript
This template should help get you started developing with Tauri, React and Typescript in Vite.
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)

View File

@@ -0,0 +1,19 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>GlobalProtect</title>
</head>
<body>
<script>
/* workaround to webview font size auto scaling */
var htmlFontSize = getComputedStyle(document.documentElement).fontSize;
var ratio = parseInt(htmlFontSize, 10) / 16;
document.documentElement.style.fontSize = 16 / ratio + "px";
</script>
<div id="root" data-tauri-drag-region></div>
<script type="module" src="/src/pages/main.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,36 @@
{
"name": "gpgui",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"tauri": "tauri"
},
"dependencies": {
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.14.18",
"@mui/material": "^5.14.18",
"@tauri-apps/api": "^1.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@tauri-apps/cli": "^1.5.6",
"@types/node": "^20.8.10",
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.12.0",
"@typescript-eslint/parser": "^6.12.0",
"@vitejs/plugin-react": "^4.0.3",
"eslint": "^8.54.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"prettier": "3.1.0",
"typescript": "^5.0.2",
"vite": "^4.5.3"
}
}

3094
apps/gpgui-helper/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,6 @@
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

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