Compare commits

..

112 Commits

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

* update packaging

* update packaging

* update packaging

* update packaging

* update packaging

* update packaging

* snap worked

* fix locale warning

* polish code

* update metainfo

* update icon

* update icon

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

* move the 3rd party libs

* organize 3rdparty

* update the 3rd party version

* refine the CMakeLists.txt

* update install command

* update install command

* update install command

* update install command

* update dependency

* update the dependency

* update the dependency

* remove CPM.cmake

* remove QtCreator project file

* update cmake file

* improve cmake file

* add cmakew

* use wget

* remove echo

* update the doc

* remove the screenshot

* update the doc

* update the install steps

* check the openconnect version

* update the doc

* update install scripts

* fix install scripts

* improve message

* improve message

* improve install scripts

* improve the version check

* improve the version check

* improve install script

* add version

* organize includes

* add version bump

* update CI

* update CI

* add the release flag

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

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

* update CI

* update icon format

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

* Make clientos configurable

* Update readme.md

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

* Add support for custom parameters

* Ignore auto generated files

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

* Removed architecture specification.

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

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

as of ubuntu 21.04 it doesn't exist anymore

* update readme

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

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

* Removing comment with dead code

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

* Update version

* Add notes to build debian package

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

* Add more logs

* Add version number
2020-05-30 11:22:05 +08:00
113 changed files with 3302 additions and 582 deletions

16
.editorconfig Normal file
View File

@@ -0,0 +1,16 @@
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = false
trim_trailing_whitespace=true
indent_style = space
indent_size = 4
[{VERSION,VERSION_SUFFIX}]
insert_final_newline = false
[*.sh]
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"]

267
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,267 @@
name: Build
on:
push:
branches:
- develop
tags:
- "v*.*.*"
paths-ignore:
- LICENSE
- "*.md"
- .vscode
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
build:
strategy:
matrix:
os: [ubuntu-18.04, ubuntu-20.04]
runs-on: ${{ matrix.os }}
steps:
# Checkout repository and submodules
- uses: actions/checkout@v2
with:
submodules: recursive
- name: Build
run: |
./scripts/install-ubuntu.sh
snapshot-archive-all:
if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/develop' }}
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
submodules: recursive
fetch-depth: 0
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install git-archive-all
- name: Archive all
run: |
./scripts/snapshot-archive-all.sh
- uses: actions/upload-artifact@v2
with:
name: snapshot-source-code
path: ./artifacts/*
snapshot-ppa:
if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/develop' }}
needs: snapshot-archive-all
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v2
with:
name: snapshot-source-code
path: artifacts
- name: Extract source code
run: |
cd $GITHUB_WORKSPACE/artifacts
mkdir deb-build && cp *.tar.gz deb-build && cd deb-build
tar xf *.tar.gz
- name: Publish PPA
uses: yuezk/publish-ppa-package@develop
with:
repository: 'ppa:yuezk/globalprotect-openconnect-snapshot'
gpg_private_key: ${{ secrets.PPA_GPG_PRIVATE_KEY }}
gpg_passphrase: ${{ secrets.PPA_GPG_PASSPHRASE }}
pkgdir: '${{ github.workspace }}/artifacts/deb-build/globalprotect-openconnect*/'
snapshot-aur:
if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/develop' }}
needs: snapshot-archive-all
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v2
with:
name: snapshot-source-code
path: artifacts
- name: Publish AUR package
env:
VERSION: $(cat ./artifacts/VERSION)
uses: yuezk/github-actions-deploy-aur@update-pkgver
with:
pkgname: globalprotect-openconnect-git
pkgbuild: ./artifacts/aur/PKGBUILD
assets: ./artifacts/aur/*.tar.gz
update_pkgver: true
commit_username: ${{ secrets.AUR_USERNAME }}
commit_email: ${{ secrets.AUR_EMAIL }}
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
commit_message: 'Snapshot release: git#${{ github.sha }}'
snapshot-obs:
if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/develop' }}
needs: snapshot-archive-all
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v2
with:
name: snapshot-source-code
path: artifacts
- uses: yuezk/publish-obs-package@main
with:
project: home:yuezk
package: globalprotect-openconnect-snapshot
username: yuezk
password: ${{ secrets.OBS_PASSWORD }}
files: ./artifacts/obs/*
snapshot-snap:
if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/develop' }}
needs: snapshot-archive-all
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v2
with:
name: snapshot-source-code
path: artifacts
- name: Extract source code
run: |
mkdir snap-source
tar xvf ./artifacts/globalprotect-openconnect-*tar.gz \
--directory snap-source \
--strip 1
- uses: snapcore/action-build@v1
id: build
with:
path: ./snap-source
- uses: snapcore/action-publish@v1
with:
store_login: ${{ secrets.SNAPSTORE_LOGIN }}
snap: ${{ steps.build.outputs.snap }}
release: edge
release-archive-all:
if: startsWith(github.ref, 'refs/tags/v')
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
submodules: recursive
fetch-depth: 0
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install git-archive-all
- name: Archive all
run: |
./scripts/release-archive-all.sh
- uses: actions/upload-artifact@v2
with:
name: release-source-code
path: ./artifacts/*
release-ppa:
if: startsWith(github.ref, 'refs/tags/v')
needs: release-archive-all
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v2
with:
name: release-source-code
path: artifacts
- name: Extract source code
run: |
cd $GITHUB_WORKSPACE/artifacts
mkdir deb-build && cp *.tar.gz deb-build && cd deb-build
tar xf *.tar.gz
- name: Publish PPA
uses: yuezk/publish-ppa-package@develop
with:
repository: 'ppa:yuezk/globalprotect-openconnect'
gpg_private_key: ${{ secrets.PPA_GPG_PRIVATE_KEY }}
gpg_passphrase: ${{ secrets.PPA_GPG_PASSPHRASE }}
pkgdir: '${{ github.workspace }}/artifacts/deb-build/globalprotect-openconnect*/'
release-aur:
if: startsWith(github.ref, 'refs/tags/v')
needs: release-archive-all
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v2
with:
name: release-source-code
path: artifacts
- name: Publish AUR package
env:
VERSION: $(cat ./artifacts/VERSION)
uses: yuezk/github-actions-deploy-aur@update-pkgver
with:
pkgname: globalprotect-openconnect-git
pkgbuild: ./artifacts/aur/PKGBUILD
assets: ./artifacts/aur/*.tar.gz
update_pkgver: true
commit_username: ${{ secrets.AUR_USERNAME }}
commit_email: ${{ secrets.AUR_EMAIL }}
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
commit_message: 'Release ${{ github.ref }}'
release-obs:
if: startsWith(github.ref, 'refs/tags/v')
needs: release-archive-all
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v2
with:
name: release-source-code
path: artifacts
- uses: yuezk/publish-obs-package@main
with:
project: home:yuezk
package: globalprotect-openconnect
username: yuezk
password: ${{ secrets.OBS_PASSWORD }}
files: ./artifacts/obs/*
release-github:
if: startsWith(github.ref, 'refs/tags/v')
needs:
- release-ppa
- release-aur
- release-obs
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v2
with:
name: release-source-code
path: artifacts
- uses: softprops/action-gh-release@v1
with:
files: |
./artifacts/*.tar.gz

31
.github/workflows/pr.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
name: PR Build
on:
pull_request:
branches:
- master
- develop
paths-ignore:
- LICENSE
- "*.md"
- .vscode
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
build:
strategy:
matrix:
os: [ubuntu-18.04, ubuntu-20.04]
runs-on: ${{ matrix.os }}
steps:
# Checkout repository and submodules
- uses: actions/checkout@v2
with:
submodules: recursive
- name: Build
run: |
./scripts/install-ubuntu.sh

13
.gitignore vendored
View File

@@ -1,11 +1,20 @@
# Binaries # Binaries
gpclient *.rpm
gpservice *.gz
*.snap
.DS_Store
build-debian
build
artifacts
.cmake
# Auto generated DBus files # Auto generated DBus files
*_adaptor.cpp *_adaptor.cpp
*_adaptor.h *_adaptor.h
gpservice_interface.*
# C++ objects and libs # C++ objects and libs
*.slo *.slo
*.lo *.lo

4
.gitmodules vendored
View File

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

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

@@ -0,0 +1,26 @@
{
"files.watcherExclude": {
"**/artifacts/**": true,
},
"files.associations": {
"qregularexpression": "cpp",
"qfileinfo": "cpp",
"qregularexpressionmatch": "cpp",
"qdatetime": "cpp",
"qprocess": "cpp",
"qobject": "cpp",
"qstandardpaths": "cpp",
"qmainwindow": "cpp",
"qsystemtrayicon": "cpp",
"qpushbutton": "cpp",
"qmenu": "cpp",
"qjsondocument": "cpp",
"qnetworkaccessmanager": "cpp",
"qwebengineview": "cpp",
"qprocessenvironment": "cpp",
"qnetworkreply": "cpp",
"qicon": "cpp",
"qsslsocket": "cpp",
"qapplication": "cpp"
}
}

1
3rdparty/SingleApplication vendored Submodule

1
3rdparty/plog vendored Submodule

Submodule 3rdparty/plog added at 914e799d2b

14
3rdparty/qt-unix-signals/CMakeLists.txt vendored Normal file
View File

@@ -0,0 +1,14 @@
cmake_minimum_required(VERSION 3.1.0)
project(QtSignals LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Instruct CMake to run moc automatically when needed.
set(CMAKE_AUTOMOC ON)
find_package(Qt5 REQUIRED COMPONENTS Core)
add_library(QtSignals STATIC sigwatch.cpp)
target_include_directories(QtSignals INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(QtSignals Qt5::Core)

21
3rdparty/qt-unix-signals/LICENCE vendored Normal file
View File

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

36
CMakeLists.txt Normal file
View File

@@ -0,0 +1,36 @@
cmake_minimum_required(VERSION 3.10.0)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
file(STRINGS "VERSION" ver)
file(STRINGS "VERSION_SUFFIX" VERSION_SUFFIX)
project(GlobalProtect-openconnect VERSION ${ver} LANGUAGES CXX)
# Set the CMAKE_INSTALL_PREFIX to /usr if not specified
if (CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "/usr" CACHE PATH "The default install prefix" FORCE)
endif()
message(STATUS "CMAKE_INSTALL_PREFIX was set to: ${CMAKE_INSTALL_PREFIX}")
configure_file(version.h.in version.h)
find_package(Qt5 REQUIRED COMPONENTS
Core
Widgets
Network
WebSockets
WebEngine
WebEngineWidgets
DBus
)
add_subdirectory(3rdparty/qt-unix-signals)
add_subdirectory(GPService)
add_subdirectory(GPClient)
add_dependencies(gpclient gpservice)

104
GPClient/CMakeLists.txt Normal file
View File

@@ -0,0 +1,104 @@
include("${CMAKE_SOURCE_DIR}/cmake/Add3rdParty.cmake")
project(GPClient)
set(gpclient_GENERATED_SOURCES)
configure_file(com.yuezk.qt.gpclient.desktop.in com.yuezk.qt.gpclient.desktop)
configure_file(com.yuezk.qt.gpclient.metainfo.xml.in com.yuezk.qt.gpclient.metainfo.xml)
qt5_add_dbus_interface(
gpclient_GENERATED_SOURCES
${CMAKE_BINARY_DIR}/com.yuezk.qt.GPService.xml
gpserviceinterface
)
add_executable(gpclient
cdpcommand.cpp
cdpcommandmanager.cpp
enhancedwebview.cpp
gatewayauthenticator.cpp
gatewayauthenticatorparams.cpp
gpgateway.cpp
gphelper.cpp
loginparams.cpp
main.cpp
normalloginwindow.cpp
portalauthenticator.cpp
portalconfigresponse.cpp
preloginresponse.cpp
samlloginwindow.cpp
gpclient.cpp
settingsdialog.cpp
gpclient.ui
normalloginwindow.ui
settingsdialog.ui
challengedialog.h
challengedialog.cpp
challengedialog.ui
vpn_dbus.cpp
vpn_json.cpp
resources.qrc
${gpclient_GENERATED_SOURCES}
)
add_3rdparty(
SingleApplication
GIT_REPOSITORY https://github.com/itay-grudev/SingleApplication.git
GIT_TAG v3.3.0
CMAKE_ARGS
-DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
-DCMAKE_CXX_FLAGS_RELEASE=${CMAKE_CXX_FLAGS_RELEASE}
-DCMAKE_FIND_ROOT_PATH=${CMAKE_FIND_ROOT_PATH}
-DCMAKE_PREFIX_PATH=$ENV{CMAKE_PREFIX_PATH}
-DQAPPLICATION_CLASS=QApplication
)
add_3rdparty(
plog
GIT_REPOSITORY https://github.com/SergiusTheBest/plog.git
GIT_TAG master
CMAKE_ARGS
-DPLOG_BUILD_SAMPLES=OFF
-DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
-DCMAKE_CXX_FLAGS_RELEASE=${CMAKE_CXX_FLAGS_RELEASE}
)
ExternalProject_Get_Property(SingleApplication-${PROJECT_NAME} SOURCE_DIR BINARY_DIR)
set(SingleApplication_INCLUDE_DIR ${SOURCE_DIR})
set(SingleApplication_LIBRARY ${BINARY_DIR}/libSingleApplication.a)
ExternalProject_Get_Property(plog-${PROJECT_NAME} SOURCE_DIR)
set(plog_INCLUDE_DIR "${SOURCE_DIR}/include")
add_dependencies(gpclient SingleApplication-${PROJECT_NAME} plog-${PROJECT_NAME})
target_include_directories(gpclient PRIVATE
${CMAKE_BINARY_DIR}
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_BINARY_DIR}
${SingleApplication_INCLUDE_DIR}
${plog_INCLUDE_DIR}
)
target_link_libraries(gpclient
${SingleApplication_LIBRARY}
Qt5::Widgets
Qt5::Network
Qt5::WebSockets
Qt5::WebEngine
Qt5::WebEngineWidgets
Qt5::DBus
QtSignals
)
if (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 8.0)
target_compile_options(gpclient PUBLIC "-ffile-prefix-map=${CMAKE_SOURCE_DIR}=.")
endif()
target_compile_definitions(gpclient PUBLIC QAPPLICATION_CLASS=QApplication)
install(TARGETS gpclient DESTINATION bin)
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/com.yuezk.qt.gpclient.metainfo.xml" DESTINATION share/metainfo)
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/com.yuezk.qt.gpclient.desktop" DESTINATION share/applications)
install(FILES "com.yuezk.qt.gpclient.svg" DESTINATION share/icons/hicolor/scalable/apps)

View File

@@ -1,78 +0,0 @@
TARGET = gpclient
QT += core gui network websockets dbus webenginewidgets
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++11
include(../singleapplication/singleapplication.pri)
DEFINES += QAPPLICATION_CLASS=QApplication
# The following define makes your compiler emit warnings if you use
# any Qt feature that has been marked deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS
INCLUDEPATH += ../plog/include
# You can also make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES += \
cdpcommand.cpp \
cdpcommandmanager.cpp \
enhancedwebview.cpp \
gatewayauthenticator.cpp \
gpgateway.cpp \
gphelper.cpp \
loginparams.cpp \
main.cpp \
normalloginwindow.cpp \
portalauthenticator.cpp \
portalconfigresponse.cpp \
preloginresponse.cpp \
samlloginwindow.cpp \
gpclient.cpp
HEADERS += \
cdpcommand.h \
cdpcommandmanager.h \
enhancedwebview.h \
gatewayauthenticator.h \
gpgateway.h \
gphelper.h \
loginparams.h \
normalloginwindow.h \
portalauthenticator.h \
portalconfigresponse.h \
preloginresponse.h \
samlloginwindow.h \
gpclient.h
FORMS += \
gpclient.ui \
normalloginwindow.ui
DBUS_INTERFACES += ../GPService/gpservice.xml
# Default rules for deployment.
target.path = /usr/bin
INSTALLS += target
DISTFILES += \
com.yuezk.qt.GPClient.svg \
com.yuezk.qt.gpclient.desktop
desktop_entry.path = /usr/share/applications/
desktop_entry.files = com.yuezk.qt.gpclient.desktop
desktop_icon.path = /usr/share/pixmaps/
desktop_icon.files = com.yuezk.qt.GPClient.svg
INSTALLS += desktop_entry desktop_icon
RESOURCES += \
resources.qrc

View File

@@ -1,8 +1,8 @@
#include "cdpcommand.h" #include <QtCore/QVariantMap>
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject>
#include <QVariantMap> #include "cdpcommand.h"
#include <QJsonDocument>
#include <QJsonObject>
CDPCommand::CDPCommand(QObject *parent) : QObject(parent) CDPCommand::CDPCommand(QObject *parent) : QObject(parent)
{ {

View File

@@ -1,7 +1,7 @@
#ifndef CDPCOMMAND_H #ifndef CDPCOMMAND_H
#define CDPCOMMAND_H #define CDPCOMMAND_H
#include <QObject> #include <QtCore/QObject>
class CDPCommand : public QObject class CDPCommand : public QObject
{ {

View File

@@ -1,7 +1,8 @@
#include "cdpcommandmanager.h" #include <QtCore/QVariantMap>
#include <QVariantMap>
#include <plog/Log.h> #include <plog/Log.h>
#include "cdpcommandmanager.h"
CDPCommandManager::CDPCommandManager(QObject *parent) CDPCommandManager::CDPCommandManager(QObject *parent)
: QObject(parent) : QObject(parent)
, networkManager(new QNetworkAccessManager) , networkManager(new QNetworkAccessManager)

View File

@@ -1,11 +1,12 @@
#ifndef CDPCOMMANDMANAGER_H #ifndef CDPCOMMANDMANAGER_H
#define CDPCOMMANDMANAGER_H #define CDPCOMMANDMANAGER_H
#include <QtCore/QObject>
#include <QtCore/QHash>
#include <QtWebSockets/QtWebSockets>
#include <QtNetwork/QNetworkAccessManager>
#include "cdpcommand.h" #include "cdpcommand.h"
#include <QObject>
#include <QHash>
#include <QtWebSockets>
#include <QNetworkAccessManager>
class CDPCommandManager : public QObject class CDPCommandManager : public QObject
{ {

View File

@@ -0,0 +1,38 @@
#include <QtWidgets/QDialogButtonBox>
#include <QtWidgets/QPushButton>
#include "challengedialog.h"
#include "ui_challengedialog.h"
ChallengeDialog::ChallengeDialog(QWidget *parent) :
QDialog(parent),
ui(new Ui::ChallengeDialog)
{
ui->setupUi(this);
ui->buttonBox->button(QDialogButtonBox::Ok)->setDisabled(true);
}
ChallengeDialog::~ChallengeDialog()
{
delete ui;
}
void ChallengeDialog::setMessage(const QString &message)
{
ui->challengeMessage->setText(message);
}
const QString ChallengeDialog::getChallenge()
{
return ui->challengeInput->text();
}
void ChallengeDialog::on_challengeInput_textChanged(const QString &value)
{
QPushButton *okBtn = ui->buttonBox->button(QDialogButtonBox::Ok);
if (value.isEmpty()) {
okBtn->setDisabled(true);
} else {
okBtn->setEnabled(true);
}
}

View File

@@ -0,0 +1,28 @@
#ifndef CHALLENGEDIALOG_H
#define CHALLENGEDIALOG_H
#include <QDialog>
namespace Ui {
class ChallengeDialog;
}
class ChallengeDialog : public QDialog
{
Q_OBJECT
public:
explicit ChallengeDialog(QWidget *parent = nullptr);
~ChallengeDialog();
void setMessage(const QString &message);
const QString getChallenge();
private slots:
void on_challengeInput_textChanged(const QString &arg1);
private:
Ui::ChallengeDialog *ui;
};
#endif // CHALLENGEDIALOG_H

111
GPClient/challengedialog.ui Normal file
View File

@@ -0,0 +1,111 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ChallengeDialog</class>
<widget class="QDialog" name="ChallengeDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>405</width>
<height>200</height>
</rect>
</property>
<property name="windowTitle">
<string>GlobalProtect Challenge</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QVBoxLayout" name="verticalLayout_2" stretch="1,1">
<item>
<widget class="QLabel" name="label">
<property name="font">
<font>
<pointsize>14</pointsize>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>Sign In</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="challengeMessage">
<property name="text">
<string>Duo two-factor login for [redacted] Enter a passcode or select one of the following options: 1. Duo Push to XXX-XXX-[redacted] 2. SMS passcodes to XXX-XXX-[redacted] Passcode or option (1-2): </string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLineEdit" name="challengeInput">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="layoutDirection">
<enum>Qt::LeftToRight</enum>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
<property name="centerButtons">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>ChallengeDialog</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>ChallengeDialog</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,10 +0,0 @@
[Desktop Entry]
Type=Application
Version=1.0.0
Name=GlobalProtect VPN
Comment=GlobalProtect VPN client, supports SAML auth mode
Exec=/usr/bin/gpclient
Icon=com.yuezk.qt.GPClient
Categories=Network;VPN;Utility;Qt;
Keywords=GlobalProtect;Openconnect;SAML;connection;VPN;

View File

@@ -0,0 +1,12 @@
[Desktop Entry]
Type=Application
Version=1.0
Name=GlobalProtect VPN
Comment=A GlobalProtect VPN client (GUI) for Linux based on OpenConnect and built with Qt5, supports SAML auth mode.
GenericName=GlobalProtect VPN client, supports SAML auth mode
Categories=Network;Dialup;
Exec=@CMAKE_INSTALL_PREFIX@/bin/gpclient
Icon=com.yuezk.qt.gpclient
Keywords=GlobalProtect;Openconnect;SAML;connection;VPN;
StartupWMClass=gpclient

View File

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

View File

@@ -16,7 +16,7 @@
viewBox="0 0 96 96" viewBox="0 0 96 96"
style="enable-background:new 0 0 96 96;" style="enable-background:new 0 0 96 96;"
xml:space="preserve" xml:space="preserve"
sodipodi:docname="com.yuezk.qt.GPClient.svg" sodipodi:docname="com.yuezk.qt.gpclient.svg"
inkscape:version="0.92.4 5da689c313, 2019-01-14"><metadata inkscape:version="0.92.4 5da689c313, 2019-01-14"><metadata
id="metadata14"><rdf:RDF><cc:Work id="metadata14"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -1,9 +1,9 @@
#include <QtCore/QProcessEnvironment>
#include <QtWebEngineWidgets/QWebEngineView>
#include "enhancedwebview.h" #include "enhancedwebview.h"
#include "cdpcommandmanager.h" #include "cdpcommandmanager.h"
#include <QtWebEngineWidgets/QWebEngineView>
#include <QProcessEnvironment>
EnhancedWebView::EnhancedWebView(QWidget *parent) EnhancedWebView::EnhancedWebView(QWidget *parent)
: QWebEngineView(parent) : QWebEngineView(parent)
, cdp(new CDPCommandManager) , cdp(new CDPCommandManager)

View File

@@ -1,9 +1,10 @@
#ifndef ENHANCEDWEBVIEW_H #ifndef ENHANCEDWEBVIEW_H
#define ENHANCEDWEBVIEW_H #define ENHANCEDWEBVIEW_H
#include "cdpcommandmanager.h"
#include <QtWebEngineWidgets/QWebEngineView> #include <QtWebEngineWidgets/QWebEngineView>
#include "cdpcommandmanager.h"
#define ENV_CDP_PORT "QTWEBENGINE_REMOTE_DEBUGGING" #define ENV_CDP_PORT "QTWEBENGINE_REMOTE_DEBUGGING"
class EnhancedWebView : public QWebEngineView class EnhancedWebView : public QWebEngineView

View File

@@ -1,19 +1,26 @@
#include <QtNetwork/QNetworkReply>
#include <QtCore/QRegularExpression>
#include <QtCore/QRegularExpressionMatch>
#include <plog/Log.h>
#include "gatewayauthenticator.h" #include "gatewayauthenticator.h"
#include "gphelper.h" #include "gphelper.h"
#include "loginparams.h" #include "loginparams.h"
#include "preloginresponse.h" #include "preloginresponse.h"
#include "challengedialog.h"
#include <QNetworkReply>
#include <plog/Log.h>
using namespace gpclient::helper; using namespace gpclient::helper;
GatewayAuthenticator::GatewayAuthenticator(const QString& gateway, const PortalConfigResponse& portalConfig) GatewayAuthenticator::GatewayAuthenticator(const QString& gateway, GatewayAuthenticatorParams params)
: QObject() : QObject()
, preloginUrl("https://" + gateway + "/ssl-vpn/prelogin.esp?tmp=tmp&kerberos-support=yes&ipv6-support=yes&clientVer=4100&clientos=Linux") , gateway(gateway)
, params(params)
, preloginUrl("https://" + gateway + "/ssl-vpn/prelogin.esp?tmp=tmp&kerberos-support=yes&ipv6-support=yes&clientVer=4100")
, loginUrl("https://" + gateway + "/ssl-vpn/login.esp") , loginUrl("https://" + gateway + "/ssl-vpn/login.esp")
, portalConfig(portalConfig)
{ {
if (!params.clientos().isEmpty()) {
preloginUrl = preloginUrl + "&clientos=" + params.clientos();
}
} }
GatewayAuthenticator::~GatewayAuthenticator() GatewayAuthenticator::~GatewayAuthenticator()
@@ -23,28 +30,32 @@ GatewayAuthenticator::~GatewayAuthenticator()
void GatewayAuthenticator::authenticate() void GatewayAuthenticator::authenticate()
{ {
LoginParams params; PLOGI << "Start gateway authentication...";
params.setUser(portalConfig.username());
params.setPassword(portalConfig.password());
params.setUserAuthCookie(portalConfig.userAuthCookie());
login(params); LoginParams loginParams { params.clientos() };
loginParams.setUser(params.username());
loginParams.setPassword(params.password());
loginParams.setUserAuthCookie(params.userAuthCookie());
loginParams.setInputStr(params.inputStr());
login(loginParams);
} }
void GatewayAuthenticator::login(const LoginParams &params) void GatewayAuthenticator::login(const LoginParams &loginParams)
{ {
PLOGI << "Trying to login the gateway at " << loginUrl << " with " << params.toUtf8(); PLOGI << "Trying to login the gateway at " << loginUrl << " with " << loginParams.toUtf8();
QNetworkReply *reply = createRequest(loginUrl, params.toUtf8()); QNetworkReply *reply = createRequest(loginUrl, loginParams.toUtf8());
connect(reply, &QNetworkReply::finished, this, &GatewayAuthenticator::onLoginFinished); connect(reply, &QNetworkReply::finished, this, &GatewayAuthenticator::onLoginFinished);
} }
void GatewayAuthenticator::onLoginFinished() void GatewayAuthenticator::onLoginFinished()
{ {
QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender()); QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
QByteArray response = reply->readAll();
if (reply->error()) { if (reply->error() || response.contains("Authentication failure")) {
PLOGE << QString("Failed to login the gateway at %1, %2").arg(loginUrl).arg(reply->errorString()); PLOGE << QString("Failed to login the gateway at %1, %2").arg(loginUrl, reply->errorString());
if (normalLoginWindow) { if (normalLoginWindow) {
normalLoginWindow->setProcessing(false); normalLoginWindow->setProcessing(false);
@@ -55,11 +66,18 @@ void GatewayAuthenticator::onLoginFinished()
return; return;
} }
// 2FA
if (response.contains("Challenge")) {
PLOGI << "The server need input the challenge...";
showChallenge(response);
return;
}
if (normalLoginWindow) { if (normalLoginWindow) {
normalLoginWindow->close(); normalLoginWindow->close();
} }
const QUrlQuery params = gpclient::helper::parseGatewayResponse(reply->readAll()); const QUrlQuery params = gpclient::helper::parseGatewayResponse(response);
emit success(params.toString()); emit success(params.toString());
} }
@@ -76,7 +94,7 @@ void GatewayAuthenticator::onPreloginFinished()
QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender()); QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
if (reply->error()) { if (reply->error()) {
PLOGE << QString("Failed to prelogin the gateway at %1, %2").arg(preloginUrl).arg(reply->errorString()); PLOGE << QString("Failed to prelogin the gateway at %1, %2").arg(preloginUrl, reply->errorString());
emit fail("Error occurred on the gateway prelogin interface."); emit fail("Error occurred on the gateway prelogin interface.");
return; return;
@@ -91,7 +109,7 @@ void GatewayAuthenticator::onPreloginFinished()
} else if (response.hasNormalAuthFields()) { } else if (response.hasNormalAuthFields()) {
normalAuth(response.labelUsername(), response.labelPassword(), response.authMessage()); normalAuth(response.labelUsername(), response.labelPassword(), response.authMessage());
} else { } else {
PLOGE << QString("Unknown prelogin response for %1, got %2").arg(preloginUrl).arg(QString::fromUtf8(response.rawResponse())); PLOGE << QString("Unknown prelogin response for %1, got %2").arg(preloginUrl, QString::fromUtf8(response.rawResponse()));
emit fail("Unknown response for gateway prelogin interface."); emit fail("Unknown response for gateway prelogin interface.");
} }
@@ -100,7 +118,7 @@ void GatewayAuthenticator::onPreloginFinished()
void GatewayAuthenticator::normalAuth(QString labelUsername, QString labelPassword, QString authMessage) 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); PLOGI << QString("Trying to perform the normal login with %1 / %2 credentials").arg(labelUsername, labelPassword);
normalLoginWindow = new NormalLoginWindow; normalLoginWindow = new NormalLoginWindow;
normalLoginWindow->setPortalAddress(gateway); normalLoginWindow->setPortalAddress(gateway);
@@ -118,11 +136,13 @@ void GatewayAuthenticator::normalAuth(QString labelUsername, QString labelPasswo
void GatewayAuthenticator::onPerformNormalLogin(const QString &username, const QString &password) void GatewayAuthenticator::onPerformNormalLogin(const QString &username, const QString &password)
{ {
PLOGI << "Start to perform normal login...";
normalLoginWindow->setProcessing(true); normalLoginWindow->setProcessing(true);
LoginParams params; params.setUsername(username);
params.setUser(username);
params.setPassword(password); params.setPassword(password);
login(params);
authenticate();
} }
void GatewayAuthenticator::onLoginWindowRejected() void GatewayAuthenticator::onLoginWindowRejected()
@@ -157,15 +177,50 @@ void GatewayAuthenticator::onSAMLLoginSuccess(const QMap<QString, QString> &saml
PLOGI << "SAML login succeeded, got the portal-userauthcookie " << samlResult.value("userAuthCookie"); PLOGI << "SAML login succeeded, got the portal-userauthcookie " << samlResult.value("userAuthCookie");
} }
LoginParams params; LoginParams loginParams { params.clientos() };
params.setUser(samlResult.value("username")); loginParams.setUser(samlResult.value("username"));
params.setPreloginCookie(samlResult.value("preloginCookie")); loginParams.setPreloginCookie(samlResult.value("preloginCookie"));
params.setUserAuthCookie(samlResult.value("userAuthCookie")); loginParams.setUserAuthCookie(samlResult.value("userAuthCookie"));
login(params); login(loginParams);
} }
void GatewayAuthenticator::onSAMLLoginFail(const QString msg) void GatewayAuthenticator::onSAMLLoginFail(const QString msg)
{ {
emit fail(msg); emit fail(msg);
} }
void GatewayAuthenticator::showChallenge(const QString &responseText)
{
QRegularExpression re("\"(.*?)\";");
QRegularExpressionMatchIterator i = re.globalMatch(responseText);
i.next(); // Skip the status value
QString message = i.next().captured(1);
QString inputStr = i.next().captured(1);
// update the inputSrc field
params.setInputStr(inputStr);
challengeDialog = new ChallengeDialog;
challengeDialog->setMessage(message);
connect(challengeDialog, &ChallengeDialog::accepted, this, [this] {
params.setPassword(challengeDialog->getChallenge());
PLOGI << "Challenge submitted, try to re-authenticate...";
authenticate();
});
connect(challengeDialog, &ChallengeDialog::rejected, this, [this] {
if (normalLoginWindow) {
normalLoginWindow->close();
}
emit fail();
});
connect(challengeDialog, &ChallengeDialog::finished, this, [this] {
delete challengeDialog;
challengeDialog = nullptr;
});
challengeDialog->show();
}

View File

@@ -1,16 +1,18 @@
#ifndef GATEWAYAUTHENTICATOR_H #ifndef GATEWAYAUTHENTICATOR_H
#define GATEWAYAUTHENTICATOR_H #define GATEWAYAUTHENTICATOR_H
#include "portalconfigresponse.h" #include <QtCore/QObject>
#include "normalloginwindow.h" #include "normalloginwindow.h"
#include "challengedialog.h"
#include "loginparams.h" #include "loginparams.h"
#include <QObject> #include "gatewayauthenticatorparams.h"
class GatewayAuthenticator : public QObject class GatewayAuthenticator : public QObject
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit GatewayAuthenticator(const QString& gateway, const PortalConfigResponse& portalConfig); explicit GatewayAuthenticator(const QString& gateway, GatewayAuthenticatorParams params);
~GatewayAuthenticator(); ~GatewayAuthenticator();
void authenticate(); void authenticate();
@@ -30,17 +32,18 @@ private slots:
private: private:
QString gateway; QString gateway;
GatewayAuthenticatorParams params;
QString preloginUrl; QString preloginUrl;
QString loginUrl; QString loginUrl;
const PortalConfigResponse& portalConfig;
NormalLoginWindow *normalLoginWindow{ nullptr }; NormalLoginWindow *normalLoginWindow{ nullptr };
ChallengeDialog *challengeDialog{ nullptr };
void login(const LoginParams& params); void login(const LoginParams& loginParams);
void doAuth(); void doAuth();
void normalAuth(QString labelUsername, QString labelPassword, QString authMessage); void normalAuth(QString labelUsername, QString labelPassword, QString authMessage);
void samlAuth(QString samlMethod, QString samlRequest, QString preloginUrl = ""); void samlAuth(QString samlMethod, QString samlRequest, QString preloginUrl = "");
void showChallenge(const QString &responseText);
}; };
#endif // GATEWAYAUTHENTICATOR_H #endif // GATEWAYAUTHENTICATOR_H

View File

@@ -0,0 +1,67 @@
#include "gatewayauthenticatorparams.h"
GatewayAuthenticatorParams::GatewayAuthenticatorParams()
{
}
GatewayAuthenticatorParams GatewayAuthenticatorParams::fromPortalConfigResponse(const PortalConfigResponse &portalConfig)
{
GatewayAuthenticatorParams params;
params.setUsername(portalConfig.username());
params.setPassword(portalConfig.password());
params.setUserAuthCookie(portalConfig.userAuthCookie());
return params;
}
const QString &GatewayAuthenticatorParams::username() const
{
return m_username;
}
void GatewayAuthenticatorParams::setUsername(const QString &newUsername)
{
m_username = newUsername;
}
const QString &GatewayAuthenticatorParams::password() const
{
return m_password;
}
void GatewayAuthenticatorParams::setPassword(const QString &newPassword)
{
m_password = newPassword;
}
const QString &GatewayAuthenticatorParams::userAuthCookie() const
{
return m_userAuthCookie;
}
void GatewayAuthenticatorParams::setUserAuthCookie(const QString &newUserAuthCookie)
{
m_userAuthCookie = newUserAuthCookie;
}
const QString &GatewayAuthenticatorParams::clientos() const
{
return m_clientos;
}
void GatewayAuthenticatorParams::setClientos(const QString &newClientos)
{
m_clientos = newClientos;
}
const QString &GatewayAuthenticatorParams::inputStr() const
{
return m_inputStr;
}
void GatewayAuthenticatorParams::setInputStr(const QString &inputStr)
{
m_inputStr = inputStr;
}

View File

@@ -0,0 +1,38 @@
#ifndef GATEWAYAUTHENTICATORPARAMS_H
#define GATEWAYAUTHENTICATORPARAMS_H
#include <QtCore/QString>
#include "portalconfigresponse.h"
class GatewayAuthenticatorParams
{
public:
GatewayAuthenticatorParams();
static GatewayAuthenticatorParams fromPortalConfigResponse(const PortalConfigResponse &portalConfig);
const QString &username() const;
void setUsername(const QString &newUsername);
const QString &password() const;
void setPassword(const QString &newPassword);
const QString &userAuthCookie() const;
void setUserAuthCookie(const QString &newUserAuthCookie);
const QString &clientos() const;
void setClientos(const QString &newClientos);
const QString &inputStr() const;
void setInputStr(const QString &inputStr);
private:
QString m_username;
QString m_password;
QString m_userAuthCookie;
QString m_clientos;
QString m_inputStr;
};
#endif // GATEWAYAUTHENTICATORPARAMS_H

View File

@@ -1,31 +1,39 @@
#include <QtGui/QIcon>
#include <plog/Log.h>
#include "gpclient.h" #include "gpclient.h"
#include "gphelper.h" #include "gphelper.h"
#include "ui_gpclient.h" #include "ui_gpclient.h"
#include "portalauthenticator.h" #include "portalauthenticator.h"
#include "gatewayauthenticator.h" #include "gatewayauthenticator.h"
#include "settingsdialog.h"
#include <plog/Log.h> #include "gatewayauthenticatorparams.h"
#include <QIcon>
using namespace gpclient::helper; using namespace gpclient::helper;
GPClient::GPClient(QWidget *parent) GPClient::GPClient(QWidget *parent, IVpn *vpn)
: QMainWindow(parent) : QMainWindow(parent)
, ui(new Ui::GPClient) , ui(new Ui::GPClient)
, vpn(vpn)
, settingsDialog(new SettingsDialog(this))
{ {
ui->setupUi(this); ui->setupUi(this);
setWindowTitle("GlobalProtect"); setWindowTitle("GlobalProtect");
setFixedSize(width(), height()); setFixedSize(width(), height());
gpclient::helper::moveCenter(this); gpclient::helper::moveCenter(this);
setupSettings();
// Restore portal from the previous settings // Restore portal from the previous settings
ui->portalInput->setText(settings::get("portal", "").toString()); this->portal(settings::get("portal", "").toString());
// DBus service setup // DBus service setup
vpn = new com::yuezk::qt::GPService("com.yuezk.qt.GPService", "/", QDBusConnection::systemBus(), this); QObject *ov = dynamic_cast<QObject*>(vpn);
connect(vpn, &com::yuezk::qt::GPService::connected, this, &GPClient::onVPNConnected); connect(ov, SIGNAL(connected()), this, SLOT(onVPNConnected()));
connect(vpn, &com::yuezk::qt::GPService::disconnected, this, &GPClient::onVPNDisconnected); connect(ov, SIGNAL(disconnected()), this, SLOT(onVPNDisconnected()));
connect(vpn, &com::yuezk::qt::GPService::logAvailable, this, &GPClient::onVPNLogAvailable); connect(ov, SIGNAL(error(QString)), this, SLOT(onVPNError(QString)));
connect(ov, SIGNAL(logAvailable(QString)), this, SLOT(onVPNLogAvailable(QString)));
// Initiallize the context menu of system tray. // Initiallize the context menu of system tray.
initSystemTrayIcon(); initSystemTrayIcon();
@@ -36,6 +44,39 @@ GPClient::~GPClient()
{ {
delete ui; delete ui;
delete vpn; delete vpn;
delete settingsDialog;
delete settingsButton;
}
void GPClient::setupSettings()
{
settingsButton = new QPushButton(this);
settingsButton->setIcon(QIcon(":/images/settings_icon.png"));
settingsButton->setFixedSize(QSize(28, 28));
QRect rect = this->geometry();
settingsButton->setGeometry(
rect.width() - settingsButton->width() - 15,
15,
settingsButton->geometry().width(),
settingsButton->geometry().height()
);
connect(settingsButton, &QPushButton::clicked, this, &GPClient::onSettingsButtonClicked);
connect(settingsDialog, &QDialog::accepted, this, &GPClient::onSettingsAccepted);
}
void GPClient::onSettingsButtonClicked()
{
settingsDialog->setExtraArgs(settings::get("extraArgs", "").toString());
settingsDialog->setClientos(settings::get("clientos", "Linux").toString());
settingsDialog->show();
}
void GPClient::onSettingsAccepted()
{
settings::save("extraArgs", settingsDialog->extraArgs());
settings::save("clientos", settingsDialog->clientos());
} }
void GPClient::on_connectButton_clicked() void GPClient::on_connectButton_clicked()
@@ -69,7 +110,7 @@ void GPClient::initSystemTrayIcon()
connect(systemTrayIcon, &QSystemTrayIcon::activated, this, &GPClient::onSystemTrayActivated); connect(systemTrayIcon, &QSystemTrayIcon::activated, this, &GPClient::onSystemTrayActivated);
connect(gatewaySwitchMenu, &QMenu::triggered, this, &GPClient::onGatewayChanged); connect(gatewaySwitchMenu, &QMenu::triggered, this, &GPClient::onGatewayChanged);
openAction = contextMenu->addAction(QIcon::fromTheme("window-new"), "Open", this, &GPClient::activiate); openAction = contextMenu->addAction(QIcon::fromTheme("window-new"), "Open", this, &GPClient::activate);
connectAction = contextMenu->addAction(QIcon::fromTheme("preferences-system-network"), "Connect", this, &GPClient::doConnect); connectAction = contextMenu->addAction(QIcon::fromTheme("preferences-system-network"), "Connect", this, &GPClient::doConnect);
contextMenu->addMenu(gatewaySwitchMenu); contextMenu->addMenu(gatewaySwitchMenu);
contextMenu->addSeparator(); contextMenu->addSeparator();
@@ -97,6 +138,8 @@ void GPClient::initVpnStatus() {
void GPClient::populateGatewayMenu() void GPClient::populateGatewayMenu()
{ {
PLOGI << "Populating the Switch Gateway menu...";
const QList<GPGateway> gateways = allGateways(); const QList<GPGateway> gateways = allGateways();
gatewaySwitchMenu->clear(); gatewaySwitchMenu->clear();
@@ -165,7 +208,7 @@ void GPClient::onSystemTrayActivated(QSystemTrayIcon::ActivationReason reason)
switch (reason) { switch (reason) {
case QSystemTrayIcon::Trigger: case QSystemTrayIcon::Trigger:
case QSystemTrayIcon::DoubleClick: case QSystemTrayIcon::DoubleClick:
this->activiate(); this->activate();
break; break;
default: default:
break; break;
@@ -200,12 +243,14 @@ void GPClient::onGatewayChanged(QAction *action)
void GPClient::doConnect() void GPClient::doConnect()
{ {
PLOGI << "Start connecting...";
const QString btnText = ui->connectButton->text(); const QString btnText = ui->connectButton->text();
const QString portal = this->portal(); const QString portal = this->portal();
// Display the main window if portal is empty // Display the main window if portal is empty
if (portal.isEmpty()) { if (portal.isEmpty()) {
activiate(); activate();
return; return;
} }
@@ -214,16 +259,19 @@ void GPClient::doConnect()
// Login to the previously saved gateway // Login to the previously saved gateway
if (!currentGateway().name().isEmpty()) { if (!currentGateway().name().isEmpty()) {
PLOGI << "Start gateway login using the previously saved gateway...";
isQuickConnect = true; isQuickConnect = true;
gatewayLogin(); gatewayLogin();
} else { } else {
// Perform the portal login // Perform the portal login
PLOGI << "Start portal login...";
portalLogin(); portalLogin();
} }
} else { } else {
PLOGI << "Start disconnecting the VPN...";
ui->statusLabel->setText("Disconnecting..."); ui->statusLabel->setText("Disconnecting...");
updateConnectionStatus(VpnStatus::pending); updateConnectionStatus(VpnStatus::pending);
vpn->disconnect(); vpn->disconnect();
} }
} }
@@ -231,11 +279,12 @@ void GPClient::doConnect()
// Login to the portal interface to get the portal config and preferred gateway // Login to the portal interface to get the portal config and preferred gateway
void GPClient::portalLogin() void GPClient::portalLogin()
{ {
PortalAuthenticator *portalAuth = new PortalAuthenticator(portal()); PortalAuthenticator *portalAuth = new PortalAuthenticator(portal(), settings::get("clientos", "Linux").toString());
connect(portalAuth, &PortalAuthenticator::success, this, &GPClient::onPortalSuccess); connect(portalAuth, &PortalAuthenticator::success, this, &GPClient::onPortalSuccess);
// Prelogin failed on the portal interface, try to treat the portal as a gateway interface // 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::preloginFailed, this, &GPClient::onPortalPreloginFail);
connect(portalAuth, &PortalAuthenticator::portalConfigFailed, this, &GPClient::onPortalConfigFail);
// Portal login failed // Portal login failed
connect(portalAuth, &PortalAuthenticator::fail, this, &GPClient::onPortalFail); connect(portalAuth, &PortalAuthenticator::fail, this, &GPClient::onPortalFail);
@@ -244,18 +293,49 @@ void GPClient::portalLogin()
portalAuth->authenticate(); portalAuth->authenticate();
} }
void GPClient::onPortalSuccess(const PortalConfigResponse portalConfig, const GPGateway gateway, QList<GPGateway> allGateways) void GPClient::onPortalSuccess(const PortalConfigResponse portalConfig, const QString region)
{ {
this->portalConfig = portalConfig; PLOGI << "Portal authentication succeeded.";
setAllGateways(allGateways);
// 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); setCurrentGateway(gateway);
this->portalConfig = portalConfig;
gatewayLogin(); gatewayLogin();
} }
void GPClient::onPortalPreloginFail() void GPClient::onPortalPreloginFail(const QString msg)
{ {
PLOGI << "Portal prelogin failed, try to preform login on the the gateway interface..."; 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 // Treat the portal input as the gateway address
GPGateway g; GPGateway g;
@@ -271,19 +351,15 @@ void GPClient::onPortalPreloginFail()
gatewayLogin(); gatewayLogin();
} }
void GPClient::onPortalFail(const QString &msg)
{
if (!msg.isEmpty()) {
openMessageBox("Portal authentication failed.", msg);
}
updateConnectionStatus(VpnStatus::disconnected);
}
// Login to the gateway // Login to the gateway
void GPClient::gatewayLogin() void GPClient::gatewayLogin()
{ {
GatewayAuthenticator *gatewayAuth = new GatewayAuthenticator(currentGateway().address(), portalConfig); PLOGI << "Performing gateway login...";
GatewayAuthenticatorParams params = GatewayAuthenticatorParams::fromPortalConfigResponse(portalConfig);
params.setClientos(settings::get("clientos", "Linux").toString());
GatewayAuthenticator *gatewayAuth = new GatewayAuthenticator(currentGateway().address(), params);
connect(gatewayAuth, &GatewayAuthenticator::success, this, &GPClient::onGatewaySuccess); connect(gatewayAuth, &GatewayAuthenticator::success, this, &GPClient::onGatewaySuccess);
connect(gatewayAuth, &GatewayAuthenticator::fail, this, &GPClient::onGatewayFail); connect(gatewayAuth, &GatewayAuthenticator::fail, this, &GPClient::onGatewayFail);
@@ -298,7 +374,11 @@ void GPClient::onGatewaySuccess(const QString &authCookie)
PLOGI << "Gateway login succeeded, got the cookie " << authCookie; PLOGI << "Gateway login succeeded, got the cookie " << authCookie;
isQuickConnect = false; isQuickConnect = false;
vpn->connect(currentGateway().address(), portalConfig.username(), authCookie); QList<QString> gatewayAddresses;
for (GPGateway &gw : allGateways()) {
gatewayAddresses.push_back(gw.address());
}
vpn->connect(currentGateway().address(), gatewayAddresses, portalConfig.username(), authCookie, settings::get("extraArgs", "").toString());
ui->statusLabel->setText("Connecting..."); ui->statusLabel->setText("Connecting...");
updateConnectionStatus(VpnStatus::pending); updateConnectionStatus(VpnStatus::pending);
} }
@@ -319,7 +399,7 @@ void GPClient::onGatewayFail(const QString &msg)
updateConnectionStatus(VpnStatus::disconnected); updateConnectionStatus(VpnStatus::disconnected);
} }
void GPClient::activiate() void GPClient::activate()
{ {
activateWindow(); activateWindow();
showNormal(); showNormal();
@@ -335,13 +415,17 @@ QString GPClient::portal() const
return input; return input;
} }
void GPClient::portal(QString p)
{
ui->portalInput->setText(p);
}
bool GPClient::connected() const bool GPClient::connected() const
{ {
const QString statusText = ui->statusLabel->text(); const QString statusText = ui->statusLabel->text();
return statusText.contains("Connected") && !statusText.contains("Not"); return statusText.contains("Connected") && !statusText.contains("Not");
} }
QList<GPGateway> GPClient::allGateways() const QList<GPGateway> GPClient::allGateways() const
{ {
const QString gatewaysJson = settings::get(portal() + "_gateways").toString(); const QString gatewaysJson = settings::get(portal() + "_gateways").toString();
@@ -350,6 +434,8 @@ QList<GPGateway> GPClient::allGateways() const
void GPClient::setAllGateways(QList<GPGateway> gateways) void GPClient::setAllGateways(QList<GPGateway> gateways)
{ {
PLOGI << "Updating all the gateways...";
settings::save(portal() + "_gateways", GPGateway::serialize(gateways)); settings::save(portal() + "_gateways", GPGateway::serialize(gateways));
populateGatewayMenu(); populateGatewayMenu();
} }
@@ -368,6 +454,8 @@ GPGateway GPClient::currentGateway() const
void GPClient::setCurrentGateway(const GPGateway gateway) void GPClient::setCurrentGateway(const GPGateway gateway)
{ {
PLOGI << "Updating the current gateway to " << gateway.name();
settings::save(portal() + "_selectedGateway", gateway.name()); settings::save(portal() + "_selectedGateway", gateway.name());
populateGatewayMenu(); populateGatewayMenu();
} }
@@ -400,6 +488,12 @@ void GPClient::onVPNDisconnected()
} }
} }
void GPClient::onVPNError(QString errorMessage)
{
updateConnectionStatus(VpnStatus::disconnected);
openMessageBox("Failed to connect", errorMessage);
}
void GPClient::onVPNLogAvailable(QString log) void GPClient::onVPNLogAvailable(QString log)
{ {
PLOGI << log; PLOGI << log;

View File

@@ -1,12 +1,14 @@
#ifndef GPCLIENT_H #ifndef GPCLIENT_H
#define GPCLIENT_H #define GPCLIENT_H
#include "gpservice_interface.h" #include <QtWidgets/QMainWindow>
#include "portalconfigresponse.h" #include <QtWidgets/QSystemTrayIcon>
#include <QtWidgets/QMenu>
#include <QtWidgets/QPushButton>
#include <QMainWindow> #include "portalconfigresponse.h"
#include <QSystemTrayIcon> #include "settingsdialog.h"
#include <QMenu> #include "vpn.h"
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
namespace Ui { class GPClient; } namespace Ui { class GPClient; }
@@ -17,12 +19,24 @@ class GPClient : public QMainWindow
Q_OBJECT Q_OBJECT
public: public:
GPClient(QWidget *parent = nullptr); GPClient(QWidget *parent, IVpn *vpn);
~GPClient(); ~GPClient();
void activiate(); void activate();
void quit();
QString portal() const;
void portal(QString);
GPGateway currentGateway() const;
void setCurrentGateway(const GPGateway gateway);
void doConnect();
private slots: private slots:
void onSettingsButtonClicked();
void onSettingsAccepted();
void on_connectButton_clicked(); void on_connectButton_clicked();
void on_portalInput_returnPressed(); void on_portalInput_returnPressed();
void on_portalInput_editingFinished(); void on_portalInput_editingFinished();
@@ -30,8 +44,9 @@ private slots:
void onSystemTrayActivated(QSystemTrayIcon::ActivationReason reason); void onSystemTrayActivated(QSystemTrayIcon::ActivationReason reason);
void onGatewayChanged(QAction *action); void onGatewayChanged(QAction *action);
void onPortalSuccess(const PortalConfigResponse portalConfig, const GPGateway gateway, QList<GPGateway> allGateways); void onPortalSuccess(const PortalConfigResponse portalConfig, const QString region);
void onPortalPreloginFail(); void onPortalPreloginFail(const QString msg);
void onPortalConfigFail(const QString msg);
void onPortalFail(const QString &msg); void onPortalFail(const QString &msg);
void onGatewaySuccess(const QString &authCookie); void onGatewaySuccess(const QString &authCookie);
@@ -39,6 +54,7 @@ private slots:
void onVPNConnected(); void onVPNConnected();
void onVPNDisconnected(); void onVPNDisconnected();
void onVPNError(QString errorMessage);
void onVPNLogAvailable(QString log); void onVPNLogAvailable(QString log);
private: private:
@@ -50,7 +66,7 @@ private:
}; };
Ui::GPClient *ui; Ui::GPClient *ui;
com::yuezk::qt::GPService *vpn; IVpn *vpn;
QSystemTrayIcon *systemTrayIcon; QSystemTrayIcon *systemTrayIcon;
QMenu *contextMenu; QMenu *contextMenu;
@@ -61,29 +77,29 @@ private:
QAction *clearAction; QAction *clearAction;
QAction *quitAction; QAction *quitAction;
SettingsDialog *settingsDialog;
QPushButton *settingsButton;
bool isQuickConnect { false }; bool isQuickConnect { false };
bool isSwitchingGateway { false }; bool isSwitchingGateway { false };
PortalConfigResponse portalConfig; PortalConfigResponse portalConfig;
void setupSettings();
void initSystemTrayIcon(); void initSystemTrayIcon();
void initVpnStatus(); void initVpnStatus();
void populateGatewayMenu(); void populateGatewayMenu();
void updateConnectionStatus(const VpnStatus &status); void updateConnectionStatus(const VpnStatus &status);
void doConnect();
void portalLogin(); void portalLogin();
void tryGatewayLogin();
void gatewayLogin(); void gatewayLogin();
QString portal() const;
bool connected() const; bool connected() const;
QList<GPGateway> allGateways() const; QList<GPGateway> allGateways() const;
void setAllGateways(QList<GPGateway> gateways); void setAllGateways(QList<GPGateway> gateways);
GPGateway currentGateway() const;
void setCurrentGateway(const GPGateway gateway);
void clearSettings(); void clearSettings();
void quit();
}; };
#endif // GPCLIENT_H #endif // GPCLIENT_H

View File

@@ -7,7 +7,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>260</width> <width>260</width>
<height>338</height> <height>362</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@@ -36,7 +36,7 @@
<property name="layoutDirection"> <property name="layoutDirection">
<enum>Qt::LeftToRight</enum> <enum>Qt::LeftToRight</enum>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_3" stretch="1,0"> <layout class="QVBoxLayout" name="verticalLayout_3" stretch="1,0,0">
<property name="leftMargin"> <property name="leftMargin">
<number>15</number> <number>15</number>
</property> </property>
@@ -123,6 +123,16 @@
</item> </item>
</layout> </layout>
</item> </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> </layout>
</widget> </widget>
</widget> </widget>

View File

@@ -1,8 +1,8 @@
#include "gpgateway.h" #include <QtCore/QJsonObject>
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonArray>
#include <QJsonObject> #include "gpgateway.h"
#include <QJsonDocument>
#include <QJsonArray>
GPGateway::GPGateway() GPGateway::GPGateway()
{ {

View File

@@ -1,9 +1,9 @@
#ifndef GPGATEWAY_H #ifndef GPGATEWAY_H
#define GPGATEWAY_H #define GPGATEWAY_H
#include <QString> #include <QtCore/QString>
#include <QMap> #include <QtCore/QMap>
#include <QJsonObject> #include <QtCore/QJsonObject>
class GPGateway class GPGateway
{ {

View File

@@ -1,17 +1,27 @@
#include "gphelper.h" #include <QtCore/QXmlStreamReader>
#include <QNetworkRequest> #include <QtWidgets/QMessageBox>
#include <QXmlStreamReader> #include <QtWidgets/QDesktopWidget>
#include <QMessageBox> #include <QtWidgets/QApplication>
#include <QDesktopWidget> #include <QtWidgets/QWidget>
#include <QApplication> #include <QtNetwork/QNetworkRequest>
#include <QWidget> #include <QtNetwork/QSslConfiguration>
#include <QtNetwork/QSslSocket>
#include <plog/Log.h> #include <plog/Log.h>
#include "gphelper.h"
QNetworkAccessManager* gpclient::helper::networkManager = new QNetworkAccessManager; QNetworkAccessManager* gpclient::helper::networkManager = new QNetworkAccessManager;
QNetworkReply* gpclient::helper::createRequest(QString url, QByteArray params) QNetworkReply* gpclient::helper::createRequest(QString url, QByteArray params)
{ {
QNetworkRequest request(url); QNetworkRequest request(url);
// Skip the ssl verifying
QSslConfiguration conf = request.sslConfiguration();
conf.setPeerVerifyMode(QSslSocket::VerifyNone);
conf.setSslOption(QSsl::SslOptionDisableLegacyRenegotiation, false);
request.setSslConfiguration(conf);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
request.setHeader(QNetworkRequest::UserAgentHeader, UA); request.setHeader(QNetworkRequest::UserAgentHeader, UA);
@@ -23,10 +33,13 @@ QNetworkReply* gpclient::helper::createRequest(QString url, QByteArray params)
GPGateway gpclient::helper::filterPreferredGateway(QList<GPGateway> gateways, const QString ruleName) 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(); GPGateway gateway = gateways.first();
for (GPGateway g : gateways) { for (GPGateway g : gateways) {
if (g.priorityOf(ruleName) > gateway.priorityOf(ruleName)) { if (g.priorityOf(ruleName) > gateway.priorityOf(ruleName)) {
PLOGI << "Find a preferred gateway: " << g.name();
gateway = g; gateway = g;
} }
} }
@@ -36,6 +49,9 @@ GPGateway gpclient::helper::filterPreferredGateway(QList<GPGateway> gateways, co
QUrlQuery gpclient::helper::parseGatewayResponse(const QByteArray &xml) QUrlQuery gpclient::helper::parseGatewayResponse(const QByteArray &xml)
{ {
PLOGI << "Start parsing the gateway response...";
PLOGI << "The gateway response is: " << xml;
QXmlStreamReader xmlReader{xml}; QXmlStreamReader xmlReader{xml};
QList<QString> args; QList<QString> args;
@@ -102,7 +118,13 @@ void gpclient::helper::settings::save(const QString &key, const QVariant &value)
_settings->setValue(key, value); _settings->setValue(key, value);
} }
void gpclient::helper::settings::clear() void gpclient::helper::settings::clear()
{ {
_settings->clear(); QStringList keys = _settings->allKeys();
for (const auto &key : qAsConst(keys)) {
if (!reservedKeys.contains(key)) {
_settings->remove(key);
}
}
} }

View File

@@ -1,16 +1,16 @@
#ifndef GPHELPER_H #ifndef GPHELPER_H
#define GPHELPER_H #define GPHELPER_H
#include <QtCore/QObject>
#include <QtCore/QUrlQuery>
#include <QtCore/QSettings>
#include <QtNetwork/QNetworkAccessManager>
#include <QtNetwork/QNetworkRequest>
#include <QtNetwork/QNetworkReply>
#include "samlloginwindow.h" #include "samlloginwindow.h"
#include "gpgateway.h" #include "gpgateway.h"
#include <QObject>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QUrlQuery>
#include <QSettings>
const QString UA = "PAN GlobalProtect"; const QString UA = "PAN GlobalProtect";
@@ -31,6 +31,7 @@ namespace gpclient {
namespace settings { namespace settings {
extern QSettings *_settings; extern QSettings *_settings;
static const QStringList reservedKeys {"extraArgs", "clientos"};
QVariant get(const QString &key, const QVariant &defaultValue = QVariant()); QVariant get(const QString &key, const QVariant &defaultValue = QVariant());
void save(const QString &key, const QVariant &value); void save(const QString &key, const QVariant &value);

View File

@@ -1,25 +0,0 @@
/*
* This file was generated by qdbusxml2cpp version 0.8
* Command line was: qdbusxml2cpp -i gpservice_interface.h -p :gpservice_interface.cpp ../GPService/gpservice.xml
*
* qdbusxml2cpp is Copyright (C) 2020 The Qt Company Ltd.
*
* This is an auto-generated file.
* This file may have been hand-edited. Look for HAND-EDIT comments
* before re-generating it.
*/
#include "gpservice_interface.h"
/*
* Implementation of interface class ComYuezkQtGPServiceInterface
*/
ComYuezkQtGPServiceInterface::ComYuezkQtGPServiceInterface(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent)
: QDBusAbstractInterface(service, path, staticInterfaceName(), connection, parent)
{
}
ComYuezkQtGPServiceInterface::~ComYuezkQtGPServiceInterface()
{
}

View File

@@ -1,71 +0,0 @@
/*
* This file was generated by qdbusxml2cpp version 0.8
* Command line was: qdbusxml2cpp -p gpservice_interface.h: ../GPService/gpservice.xml
*
* qdbusxml2cpp is Copyright (C) 2020 The Qt Company Ltd.
*
* This is an auto-generated file.
* Do not edit! All changes made to it will be lost.
*/
#ifndef GPSERVICE_INTERFACE_H
#define GPSERVICE_INTERFACE_H
#include <QtCore/QObject>
#include <QtCore/QByteArray>
#include <QtCore/QList>
#include <QtCore/QMap>
#include <QtCore/QString>
#include <QtCore/QStringList>
#include <QtCore/QVariant>
#include <QtDBus/QtDBus>
/*
* Proxy class for interface com.yuezk.qt.GPService
*/
class ComYuezkQtGPServiceInterface: public QDBusAbstractInterface
{
Q_OBJECT
public:
static inline const char *staticInterfaceName()
{ return "com.yuezk.qt.GPService"; }
public:
ComYuezkQtGPServiceInterface(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent = nullptr);
~ComYuezkQtGPServiceInterface();
public Q_SLOTS: // METHODS
inline QDBusPendingReply<> connect(const QString &server, const QString &username, const QString &passwd)
{
QList<QVariant> argumentList;
argumentList << QVariant::fromValue(server) << QVariant::fromValue(username) << QVariant::fromValue(passwd);
return asyncCallWithArgumentList(QStringLiteral("connect"), argumentList);
}
inline QDBusPendingReply<> disconnect()
{
QList<QVariant> argumentList;
return asyncCallWithArgumentList(QStringLiteral("disconnect"), argumentList);
}
inline QDBusPendingReply<int> status()
{
QList<QVariant> argumentList;
return asyncCallWithArgumentList(QStringLiteral("status"), argumentList);
}
Q_SIGNALS: // SIGNALS
void connected();
void disconnected();
void logAvailable(const QString &log);
};
namespace com {
namespace yuezk {
namespace qt {
typedef ::ComYuezkQtGPServiceInterface GPService;
}
}
}
#endif

View File

@@ -1,12 +1,12 @@
#include <QtCore/QUrlQuery>
#include "loginparams.h" #include "loginparams.h"
#include <QUrlQuery> LoginParams::LoginParams(const QString clientos)
LoginParams::LoginParams()
{ {
params.addQueryItem("prot", QUrl::toPercentEncoding("https:")); params.addQueryItem("prot", QUrl::toPercentEncoding("https:"));
params.addQueryItem("server", ""); params.addQueryItem("server", "");
params.addQueryItem("inputSrc", ""); params.addQueryItem("inputStr", "");
params.addQueryItem("jnlpReady", "jnlpReady"); params.addQueryItem("jnlpReady", "jnlpReady");
params.addQueryItem("user", ""); params.addQueryItem("user", "");
params.addQueryItem("passwd", ""); params.addQueryItem("passwd", "");
@@ -15,7 +15,12 @@ LoginParams::LoginParams()
params.addQueryItem("direct", "yes"); params.addQueryItem("direct", "yes");
params.addQueryItem("clientVer", "4100"); params.addQueryItem("clientVer", "4100");
params.addQueryItem("os-version", QUrl::toPercentEncoding(QSysInfo::prettyProductName())); params.addQueryItem("os-version", QUrl::toPercentEncoding(QSysInfo::prettyProductName()));
params.addQueryItem("clientos", "Linux");
// add the clientos parameter if not empty
if (!clientos.isEmpty()) {
params.addQueryItem("clientos", clientos);
}
params.addQueryItem("portal-userauthcookie", ""); params.addQueryItem("portal-userauthcookie", "");
params.addQueryItem("portal-prelogonuserauthcookie", ""); params.addQueryItem("portal-prelogonuserauthcookie", "");
params.addQueryItem("prelogin-cookie", ""); params.addQueryItem("prelogin-cookie", "");
@@ -26,42 +31,47 @@ LoginParams::~LoginParams()
{ {
} }
void LoginParams::setUser(const QString &user) void LoginParams::setUser(const QString user)
{ {
updateQueryItem("user", user); updateQueryItem("user", user);
} }
void LoginParams::setServer(const QString &server) void LoginParams::setServer(const QString server)
{ {
updateQueryItem("server", server); updateQueryItem("server", server);
} }
void LoginParams::setPassword(const QString &password) void LoginParams::setPassword(const QString password)
{ {
updateQueryItem("passwd", password); updateQueryItem("passwd", password);
} }
void LoginParams::setUserAuthCookie(const QString &cookie) void LoginParams::setUserAuthCookie(const QString cookie)
{ {
updateQueryItem("portal-userauthcookie", cookie); updateQueryItem("portal-userauthcookie", cookie);
} }
void LoginParams::setPrelogonAuthCookie(const QString &cookie) void LoginParams::setPrelogonAuthCookie(const QString cookie)
{ {
updateQueryItem("portal-prelogonuserauthcookie", cookie); updateQueryItem("portal-prelogonuserauthcookie", cookie);
} }
void LoginParams::setPreloginCookie(const QString &cookie) void LoginParams::setPreloginCookie(const QString cookie)
{ {
updateQueryItem("prelogin-cookie", cookie); updateQueryItem("prelogin-cookie", cookie);
} }
void LoginParams::setInputStr(const QString inputStr)
{
updateQueryItem("inputStr", inputStr);
}
QByteArray LoginParams::toUtf8() const QByteArray LoginParams::toUtf8() const
{ {
return params.toString().toUtf8(); return params.toString().toUtf8();
} }
void LoginParams::updateQueryItem(const QString &key, const QString &value) void LoginParams::updateQueryItem(const QString key, const QString value)
{ {
if (params.hasQueryItem(key)) { if (params.hasQueryItem(key)) {
params.removeQueryItem(key); params.removeQueryItem(key);

View File

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

View File

@@ -1,21 +1,26 @@
#include <QtCore/QObject>
#include <QtCore/QString>
#include <QtCore/QDir>
#include <QtCore/QStandardPaths>
#include <plog/Log.h>
#include <plog/Init.h>
#include <plog/Appenders/ColorConsoleAppender.h>
#include <plog/Formatters/TxtFormatter.h>
#include "singleapplication.h" #include "singleapplication.h"
#include "gpclient.h" #include "gpclient.h"
#include "vpn_dbus.h"
#include "vpn_json.h"
#include "enhancedwebview.h" #include "enhancedwebview.h"
#include "sigwatch.h"
#include <QStandardPaths> #include "version.h"
#include <plog/Log.h>
#include <plog/Appenders/ColorConsoleAppender.h>
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
const QDir path = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + "/GlobalProtect-openconnect"; plog::ColorConsoleAppender<plog::TxtFormatter> consoleAppender(plog::streamStdErr);
const QString logFile = path.path() + "/gpclient.log"; plog::init(plog::debug, &consoleAppender);
if (!path.exists()) {
path.mkpath(".");
}
static plog::ColorConsoleAppender<plog::TxtFormatter> consoleAppender; PLOGI << "GlobalProtect started, version: " << VERSION;
plog::init(plog::debug, logFile.toUtf8()).addAppender(&consoleAppender);
QString port = QString::fromLocal8Bit(qgetenv(ENV_CDP_PORT)); QString port = QString::fromLocal8Bit(qgetenv(ENV_CDP_PORT));
@@ -24,10 +29,52 @@ int main(int argc, char *argv[])
} }
SingleApplication app(argc, argv); SingleApplication app(argc, argv);
GPClient w; app.setQuitOnLastWindowClosed(false);
QCommandLineParser parser;
parser.addHelpOption();
parser.addVersionOption();
parser.addPositionalArgument("server", "The URL of the VPN server. Optional.");
parser.addPositionalArgument("gateway", "The URL of the specific VPN gateway. Optional.");
parser.addOptions({
{"json", "Write the result of the handshake with the GlobalConnect server to stdout as JSON and terminate. Useful for scripting."},
{"now", "Do not show the dialog with the connect button; connect immediately instead."},
});
parser.process(app);
const QStringList positional = parser.positionalArguments();
IVpn *vpn = parser.isSet("json") // yes it leaks, but this is cleared on exit anyway
? static_cast<IVpn*>(new VpnJson(nullptr)) // Print to stdout and exit
: static_cast<IVpn*>(new VpnDbus(nullptr)); // Contact GPService daemon via dbus
GPClient w(nullptr, vpn);
w.show(); w.show();
QObject::connect(&app, &SingleApplication::instanceStarted, &w, &GPClient::activiate); if (positional.size() > 0) {
w.portal(positional.at(0));
}
if (positional.size() > 1) {
GPGateway gw;
gw.setName(positional.at(1));
gw.setAddress(positional.at(1));
w.setCurrentGateway(gw);
}
QObject::connect(&app, &SingleApplication::instanceStarted, &w, &GPClient::activate);
UnixSignalWatcher sigwatch;
sigwatch.watchForSignal(SIGINT);
sigwatch.watchForSignal(SIGTERM);
sigwatch.watchForSignal(SIGQUIT);
sigwatch.watchForSignal(SIGHUP);
QObject::connect(&sigwatch, &UnixSignalWatcher::unixSignal, &w, &GPClient::quit);
if (parser.isSet("now")) {
w.doConnect();
}
if (parser.isSet("json")) {
QObject::connect(static_cast<VpnJson*>(vpn), &VpnJson::connected, &w, &GPClient::quit);
}
return app.exec(); return app.exec();
} }

View File

@@ -1,8 +1,8 @@
#include <QtGui/QCloseEvent>
#include "normalloginwindow.h" #include "normalloginwindow.h"
#include "ui_normalloginwindow.h" #include "ui_normalloginwindow.h"
#include <QCloseEvent>
NormalLoginWindow::NormalLoginWindow(QWidget *parent) : NormalLoginWindow::NormalLoginWindow(QWidget *parent) :
QDialog(parent), QDialog(parent),
ui(new Ui::NormalLoginWindow) ui(new Ui::NormalLoginWindow)

View File

@@ -1,7 +1,7 @@
#ifndef PORTALAUTHWINDOW_H #ifndef PORTALAUTHWINDOW_H
#define PORTALAUTHWINDOW_H #define PORTALAUTHWINDOW_H
#include <QDialog> #include <QtWidgets/QDialog>
namespace Ui { namespace Ui {
class NormalLoginWindow; class NormalLoginWindow;

View File

@@ -1,3 +1,6 @@
#include <QtNetwork/QNetworkReply>
#include <plog/Log.h>
#include "portalauthenticator.h" #include "portalauthenticator.h"
#include "gphelper.h" #include "gphelper.h"
#include "normalloginwindow.h" #include "normalloginwindow.h"
@@ -7,16 +10,17 @@
#include "portalconfigresponse.h" #include "portalconfigresponse.h"
#include "gpgateway.h" #include "gpgateway.h"
#include <plog/Log.h>
#include <QNetworkReply>
using namespace gpclient::helper; using namespace gpclient::helper;
PortalAuthenticator::PortalAuthenticator(const QString& portal) : QObject() PortalAuthenticator::PortalAuthenticator(const QString& portal, const QString& clientos) : QObject()
, portal(portal) , portal(portal)
, preloginUrl("https://" + portal + "/global-protect/prelogin.esp?tmp=tmp&kerberos-support=yes&ipv6-support=yes&clientVer=4100&clientos=Linux") , clientos(clientos)
, preloginUrl("https://" + portal + "/global-protect/prelogin.esp?tmp=tmp&kerberos-support=yes&ipv6-support=yes&clientVer=4100")
, configUrl("https://" + portal + "/global-protect/getconfig.esp") , configUrl("https://" + portal + "/global-protect/getconfig.esp")
{ {
if (!clientos.isEmpty()) {
preloginUrl = preloginUrl + "&clientos=" + clientos;
}
} }
PortalAuthenticator::~PortalAuthenticator() PortalAuthenticator::~PortalAuthenticator()
@@ -37,7 +41,7 @@ void PortalAuthenticator::onPreloginFinished()
QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender()); QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
if (reply->error()) { if (reply->error()) {
PLOGE << QString("Error occurred while accessing %1, %2").arg(preloginUrl).arg(reply->errorString()); PLOGE << QString("Error occurred while accessing %1, %2").arg(preloginUrl, reply->errorString());
emit preloginFailed("Error occurred on the portal prelogin interface."); emit preloginFailed("Error occurred on the portal prelogin interface.");
delete reply; delete reply;
return; return;
@@ -46,6 +50,9 @@ void PortalAuthenticator::onPreloginFinished()
PLOGI << "Portal prelogin succeeded."; PLOGI << "Portal prelogin succeeded.";
preloginResponse = PreloginResponse::parse(reply->readAll()); preloginResponse = PreloginResponse::parse(reply->readAll());
PLOGI << "Finished parsing the prelogin response. The region field is: " << preloginResponse.region();
if (preloginResponse.hasSamlAuthFields()) { if (preloginResponse.hasSamlAuthFields()) {
// Do SAML authentication // Do SAML authentication
samlAuth(); samlAuth();
@@ -54,7 +61,7 @@ void PortalAuthenticator::onPreloginFinished()
tryAutoLogin(); tryAutoLogin();
} else { } else {
PLOGE << QString("Unknown prelogin response for %1 got %2").arg(preloginUrl).arg(QString::fromUtf8(preloginResponse.rawResponse())); PLOGE << QString("Unknown prelogin response for %1 got %2").arg(preloginUrl).arg(QString::fromUtf8(preloginResponse.rawResponse()));
emitFail("Unknown response for portal prelogin interface."); emit preloginFailed("Unknown response for portal prelogin interface.");
} }
delete reply; delete reply;
@@ -140,12 +147,12 @@ void PortalAuthenticator::onSAMLLoginFail(const QString msg)
void PortalAuthenticator::fetchConfig(QString username, QString password, QString preloginCookie, QString userAuthCookie) void PortalAuthenticator::fetchConfig(QString username, QString password, QString preloginCookie, QString userAuthCookie)
{ {
LoginParams params; LoginParams loginParams { clientos };
params.setServer(portal); loginParams.setServer(portal);
params.setUser(username); loginParams.setUser(username);
params.setPassword(password); loginParams.setPassword(password);
params.setPreloginCookie(preloginCookie); loginParams.setPreloginCookie(preloginCookie);
params.setUserAuthCookie(userAuthCookie); loginParams.setUserAuthCookie(userAuthCookie);
// Save the username and password for future use. // Save the username and password for future use.
this->username = username; this->username = username;
@@ -153,7 +160,7 @@ void PortalAuthenticator::fetchConfig(QString username, QString password, QStrin
PLOGI << "Fetching the portal config from " << configUrl << " for user: " << username; PLOGI << "Fetching the portal config from " << configUrl << " for user: " << username;
QNetworkReply *reply = createRequest(configUrl, params.toUtf8()); QNetworkReply *reply = createRequest(configUrl, loginParams.toUtf8());
connect(reply, &QNetworkReply::finished, this, &PortalAuthenticator::onFetchConfigFinished); connect(reply, &QNetworkReply::finished, this, &PortalAuthenticator::onFetchConfigFinished);
} }
@@ -172,27 +179,26 @@ void PortalAuthenticator::onFetchConfigFinished()
isAutoLogin = false; isAutoLogin = false;
normalAuth(); normalAuth();
} else { } else {
emitFail("Failed to fetch the portal config."); emit portalConfigFailed("Failed to fetch the portal config.");
} }
return; return;
} }
PLOGI << "Fetch the portal config succeeded."; PLOGI << "Fetch the portal config succeeded.";
PortalConfigResponse response = PortalConfigResponse::parse(reply->readAll()); PortalConfigResponse response = PortalConfigResponse::parse(reply->readAll());
// Add the username & password to the response object // Add the username & password to the response object
response.setUsername(username); response.setUsername(username);
response.setPassword(password); response.setPassword(password);
// Close the login window // Close the login window
if (normalLoginWindow) { if (normalLoginWindow) {
// Save the credentials for reuse PLOGI << "Closing the NormalLoginWindow...";
settings::save("username", username);
settings::save("password", password);
normalLoginWindow->close(); normalLoginWindow->close();
} }
emit success(response, filterPreferredGateway(response.allGateways(), preloginResponse.region()), response.allGateways()); emit success(response, preloginResponse.region());
} }
void PortalAuthenticator::emitFail(const QString& msg) void PortalAuthenticator::emitFail(const QString& msg)

View File

@@ -1,26 +1,28 @@
#ifndef PORTALAUTHENTICATOR_H #ifndef PORTALAUTHENTICATOR_H
#define PORTALAUTHENTICATOR_H #define PORTALAUTHENTICATOR_H
#include <QtCore/QObject>
#include "portalconfigresponse.h" #include "portalconfigresponse.h"
#include "normalloginwindow.h" #include "normalloginwindow.h"
#include "samlloginwindow.h" #include "samlloginwindow.h"
#include "preloginresponse.h" #include "preloginresponse.h"
#include <QObject>
class PortalAuthenticator : public QObject class PortalAuthenticator : public QObject
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit PortalAuthenticator(const QString& portal); explicit PortalAuthenticator(const QString& portal, const QString& clientos);
~PortalAuthenticator(); ~PortalAuthenticator();
void authenticate(); void authenticate();
signals: signals:
void success(const PortalConfigResponse, const GPGateway, QList<GPGateway> allGateways); void success(const PortalConfigResponse response, const QString region);
void fail(const QString& msg); void fail(const QString& msg);
void preloginFailed(const QString& msg); void preloginFailed(const QString& msg);
void portalConfigFailed(const QString msg);
private slots: private slots:
void onPreloginFinished(); void onPreloginFinished();
@@ -33,6 +35,7 @@ private slots:
private: private:
QString portal; QString portal;
QString clientos;
QString preloginUrl; QString preloginUrl;
QString configUrl; QString configUrl;
QString username; QString username;

View File

@@ -1,8 +1,8 @@
#include "portalconfigresponse.h" #include <QtCore/QXmlStreamReader>
#include <QXmlStreamReader>
#include <plog/Log.h> #include <plog/Log.h>
#include "portalconfigresponse.h"
QString PortalConfigResponse::xmlUserAuthCookie = "portal-userauthcookie"; QString PortalConfigResponse::xmlUserAuthCookie = "portal-userauthcookie";
QString PortalConfigResponse::xmlPrelogonUserAuthCookie = "portal-prelogonuserauthcookie"; QString PortalConfigResponse::xmlPrelogonUserAuthCookie = "portal-prelogonuserauthcookie";
QString PortalConfigResponse::xmlGateways = "gateways"; QString PortalConfigResponse::xmlGateways = "gateways";
@@ -15,8 +15,10 @@ PortalConfigResponse::~PortalConfigResponse()
{ {
} }
PortalConfigResponse PortalConfigResponse::parse(const QByteArray& xml) PortalConfigResponse PortalConfigResponse::parse(const QByteArray xml)
{ {
PLOGI << "Start parsing the portal configuration...";
QXmlStreamReader xmlReader(xml); QXmlStreamReader xmlReader(xml);
PortalConfigResponse response; PortalConfigResponse response;
response.setRawResponse(xml); response.setRawResponse(xml);
@@ -27,36 +29,50 @@ PortalConfigResponse PortalConfigResponse::parse(const QByteArray& xml)
QString name = xmlReader.name().toString(); QString name = xmlReader.name().toString();
if (name == xmlUserAuthCookie) { if (name == xmlUserAuthCookie) {
PLOGI << "Start reading " << name;
response.setUserAuthCookie(xmlReader.readElementText()); response.setUserAuthCookie(xmlReader.readElementText());
} else if (name == xmlPrelogonUserAuthCookie) { } else if (name == xmlPrelogonUserAuthCookie) {
PLOGI << "Start reading " << name;
response.setPrelogonUserAuthCookie(xmlReader.readElementText()); response.setPrelogonUserAuthCookie(xmlReader.readElementText());
} else if (name == xmlGateways) { } else if (name == xmlGateways) {
response.setAllGateways(parseGateways(xmlReader)); response.setAllGateways(parseGateways(xmlReader));
} }
} }
PLOGI << "Finished parsing portal configuration.";
return response; return response;
} }
const QByteArray& PortalConfigResponse::rawResponse() const const QByteArray PortalConfigResponse::rawResponse() const
{ {
return _rawResponse; return m_rawResponse;
} }
QString PortalConfigResponse::username() const const QString &PortalConfigResponse::username() const
{ {
return _username; return m_username;
} }
QString PortalConfigResponse::password() const QString PortalConfigResponse::password() const
{ {
return _password; return m_password;
} }
QList<GPGateway> PortalConfigResponse::parseGateways(QXmlStreamReader &xmlReader) QList<GPGateway> PortalConfigResponse::parseGateways(QXmlStreamReader &xmlReader)
{ {
PLOGI << "Start parsing the gateways from portal configuration...";
QList<GPGateway> gateways; QList<GPGateway> gateways;
while (xmlReader.name() != "external"){
xmlReader.readNext();
}
while (xmlReader.name() != "list"){
xmlReader.readNext();
}
while (xmlReader.name() != xmlGateways || !xmlReader.isEndElement()) { while (xmlReader.name() != xmlGateways || !xmlReader.isEndElement()) {
xmlReader.readNext(); xmlReader.readNext();
// Parse the gateways -> external -> list -> entry // Parse the gateways -> external -> list -> entry
@@ -69,81 +85,94 @@ QList<GPGateway> PortalConfigResponse::parseGateways(QXmlStreamReader &xmlReader
gateways.append(g); gateways.append(g);
} }
} }
PLOGI << "Finished parsing the gateways.";
return gateways; return gateways;
} }
QMap<QString, int> PortalConfigResponse::parsePriorityRules(QXmlStreamReader &xmlReader) QMap<QString, int> PortalConfigResponse::parsePriorityRules(QXmlStreamReader &xmlReader)
{ {
PLOGI << "Start parsing the priority rules...";
QMap<QString, int> priorityRules; QMap<QString, int> priorityRules;
while (xmlReader.name() != "priority-rule" || !xmlReader.isEndElement()) { while ((xmlReader.name() != "priority-rule" || !xmlReader.isEndElement()) && !xmlReader.hasError()) {
xmlReader.readNext(); xmlReader.readNext();
if (xmlReader.name() == "entry" && xmlReader.isStartElement()) { if (xmlReader.name() == "entry" && xmlReader.isStartElement()) {
QString ruleName = xmlReader.attributes().value("name").toString(); QString ruleName = xmlReader.attributes().value("name").toString();
// Read the priority tag // Read the priority tag
xmlReader.readNextStartElement(); while (xmlReader.name() != "priority"){
xmlReader.readNext();
}
int ruleValue = xmlReader.readElementText().toUInt(); int ruleValue = xmlReader.readElementText().toUInt();
priorityRules.insert(ruleName, ruleValue); priorityRules.insert(ruleName, ruleValue);
} }
} }
PLOGI << "Finished parsing the priority rules.";
return priorityRules; return priorityRules;
} }
QString PortalConfigResponse::parseGatewayName(QXmlStreamReader &xmlReader) QString PortalConfigResponse::parseGatewayName(QXmlStreamReader &xmlReader)
{ {
while (xmlReader.name() != "description" || !xmlReader.isEndElement()) { PLOGI << "Start parsing the gateway name...";
xmlReader.readNext();
if (xmlReader.name() == "description" && xmlReader.tokenType() == xmlReader.StartElement) {
return xmlReader.readElementText();
}
}
PLOGE << "Error: <description> tag not found"; while (xmlReader.name() != "description" || !xmlReader.isEndElement()) {
return ""; 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 QString PortalConfigResponse::userAuthCookie() const
{ {
return _userAuthCookie; return m_userAuthCookie;
} }
QString PortalConfigResponse::prelogonUserAuthCookie() const QString PortalConfigResponse::prelogonUserAuthCookie() const
{ {
return _prelogonAuthCookie; return m_prelogonAuthCookie;
} }
QList<GPGateway> PortalConfigResponse::allGateways() QList<GPGateway> PortalConfigResponse::allGateways() const
{ {
return _gateways; return m_gateways;
} }
void PortalConfigResponse::setAllGateways(QList<GPGateway> gateways) void PortalConfigResponse::setAllGateways(QList<GPGateway> gateways)
{ {
_gateways = gateways; m_gateways = gateways;
} }
void PortalConfigResponse::setRawResponse(const QByteArray &response) void PortalConfigResponse::setRawResponse(const QByteArray response)
{ {
_rawResponse = response; m_rawResponse = response;
} }
void PortalConfigResponse::setUsername(const QString& username) void PortalConfigResponse::setUsername(const QString username)
{ {
_username = username; m_username = username;
} }
void PortalConfigResponse::setPassword(const QString& password) void PortalConfigResponse::setPassword(const QString password)
{ {
_password = password; m_password = password;
} }
void PortalConfigResponse::setUserAuthCookie(const QString &cookie) void PortalConfigResponse::setUserAuthCookie(const QString cookie)
{ {
_userAuthCookie = cookie; m_userAuthCookie = cookie;
} }
void PortalConfigResponse::setPrelogonUserAuthCookie(const QString &cookie) void PortalConfigResponse::setPrelogonUserAuthCookie(const QString cookie)
{ {
_prelogonAuthCookie = cookie; m_prelogonAuthCookie = cookie;
} }

View File

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

View File

@@ -1,7 +1,8 @@
#include "preloginresponse.h" #include <QtCore/QXmlStreamReader>
#include <QtCore/QMap>
#include <plog/Log.h>
#include <QXmlStreamReader> #include "preloginresponse.h"
#include <QMap>
QString PreloginResponse::xmlAuthMessage = "authentication-message"; QString PreloginResponse::xmlAuthMessage = "authentication-message";
QString PreloginResponse::xmlLabelUsername = "username-label"; QString PreloginResponse::xmlLabelUsername = "username-label";
@@ -22,6 +23,8 @@ PreloginResponse::PreloginResponse()
PreloginResponse PreloginResponse::parse(const QByteArray& xml) PreloginResponse PreloginResponse::parse(const QByteArray& xml)
{ {
PLOGI << "Start parsing the prelogin response...";
QXmlStreamReader xmlReader(xml); QXmlStreamReader xmlReader(xml);
PreloginResponse response; PreloginResponse response;
response.setRawResponse(xml); response.setRawResponse(xml);
@@ -81,17 +84,17 @@ bool PreloginResponse::hasNormalAuthFields() const
return !labelUsername().isEmpty() && !labelPassword().isEmpty(); return !labelUsername().isEmpty() && !labelPassword().isEmpty();
} }
void PreloginResponse::setRawResponse(const QByteArray &response) void PreloginResponse::setRawResponse(const QByteArray response)
{ {
_rawResponse = response; _rawResponse = response;
} }
bool PreloginResponse::has(const QString &name) const bool PreloginResponse::has(const QString name) const
{ {
return resultMap.contains(name); return resultMap.contains(name);
} }
void PreloginResponse::add(const QString &name, const QString &value) void PreloginResponse::add(const QString name, const QString value)
{ {
resultMap.insert(name, value); resultMap.insert(name, value);
} }

View File

@@ -1,8 +1,8 @@
#ifndef PRELOGINRESPONSE_H #ifndef PRELOGINRESPONSE_H
#define PRELOGINRESPONSE_H #define PRELOGINRESPONSE_H
#include <QString> #include <QtCore/QString>
#include <QMap> #include <QtCore/QMap>
class PreloginResponse class PreloginResponse
{ {
@@ -33,9 +33,9 @@ private:
QMap<QString, QString> resultMap; QMap<QString, QString> resultMap;
QByteArray _rawResponse; QByteArray _rawResponse;
void setRawResponse(const QByteArray &response); void setRawResponse(const QByteArray response);
void add(const QString &name, const QString &value); void add(const QString name, const QString value);
bool has(const QString &name) const; bool has(const QString name) const;
}; };
#endif // PRELOGINRESPONSE_H #endif // PRELOGINRESPONSE_H

View File

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

View File

@@ -1,15 +1,15 @@
#include "samlloginwindow.h" #include <QtWidgets/QVBoxLayout>
#include <QtWebEngineWidgets/QWebEngineProfile>
#include <QVBoxLayout> #include <QtWebEngineWidgets/QWebEngineView>
#include <plog/Log.h> #include <plog/Log.h>
#include <QWebEngineProfile>
#include <QWebEngineView> #include "samlloginwindow.h"
SAMLLoginWindow::SAMLLoginWindow(QWidget *parent) SAMLLoginWindow::SAMLLoginWindow(QWidget *parent)
: QDialog(parent) : QDialog(parent)
, webView(new EnhancedWebView(this)) , webView(new EnhancedWebView(this))
{ {
setWindowTitle("GlobalProtect SAML Login"); setWindowTitle("GlobalProtect Login");
setModal(true); setModal(true);
resize(700, 550); resize(700, 550);
@@ -61,29 +61,39 @@ void SAMLLoginWindow::onResponseReceived(QJsonObject params)
const QString preloginCookie = headers.value("prelogin-cookie").toString(); const QString preloginCookie = headers.value("prelogin-cookie").toString();
const QString userAuthCookie = headers.value("portal-userauthcookie").toString(); const QString userAuthCookie = headers.value("portal-userauthcookie").toString();
LOGI << "Response received from " << response.value("url").toString();
if (!username.isEmpty()) { if (!username.isEmpty()) {
LOGI << "Got username from SAML response headers " << username;
samlResult.insert("username", username); samlResult.insert("username", username);
} }
if (!preloginCookie.isEmpty()) { if (!preloginCookie.isEmpty()) {
LOGI << "Got prelogin-cookie from SAML response headers " << preloginCookie;
samlResult.insert("preloginCookie", preloginCookie); samlResult.insert("preloginCookie", preloginCookie);
} }
if (!userAuthCookie.isEmpty()) { if (!userAuthCookie.isEmpty()) {
LOGI << "Got portal-userauthcookie from SAML response headers " << userAuthCookie;
samlResult.insert("userAuthCookie", userAuthCookie); samlResult.insert("userAuthCookie", userAuthCookie);
} }
}
void SAMLLoginWindow::onLoadFinished()
{
LOGI << "Load finished " << this->webView->page()->url().toString();
// Check the SAML result // Check the SAML result
if (samlResult.contains("username") if (samlResult.contains("username")
&& (samlResult.contains("preloginCookie") || samlResult.contains("userAuthCookie"))) { && (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); emit success(samlResult);
accept(); accept();
} else { } else {
this->show(); this->show();
} }
} }
void SAMLLoginWindow::onLoadFinished()
{
LOGI << "Load finished " << this->webView->page()->url().toString();
}

View File

@@ -1,11 +1,11 @@
#ifndef SAMLLOGINWINDOW_H #ifndef SAMLLOGINWINDOW_H
#define SAMLLOGINWINDOW_H #define SAMLLOGINWINDOW_H
#include "enhancedwebview.h" #include <QtCore/QMap>
#include <QtGui/QCloseEvent>
#include <QtWidgets/QDialog>
#include <QDialog> #include "enhancedwebview.h"
#include <QMap>
#include <QCloseEvent>
class SAMLLoginWindow : public QDialog class SAMLLoginWindow : public QDialog
{ {

BIN
GPClient/settings_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,34 @@
#include "settingsdialog.h"
#include "ui_settingsdialog.h"
SettingsDialog::SettingsDialog(QWidget *parent) :
QDialog(parent),
ui(new Ui::SettingsDialog)
{
ui->setupUi(this);
}
SettingsDialog::~SettingsDialog()
{
delete ui;
}
void SettingsDialog::setExtraArgs(QString extraArgs)
{
ui->extraArgsInput->setPlainText(extraArgs);
}
QString SettingsDialog::extraArgs()
{
return ui->extraArgsInput->toPlainText().trimmed();
}
void SettingsDialog::setClientos(QString clientos)
{
ui->clientosInput->setText(clientos);
}
QString SettingsDialog::clientos()
{
return ui->clientosInput->text();
}

28
GPClient/settingsdialog.h Normal file
View File

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

104
GPClient/settingsdialog.ui Normal file
View File

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

24
GPClient/vpn.h Normal file
View File

@@ -0,0 +1,24 @@
#ifndef VPN_H
#define VPN_H
#include <QtCore/QObject>
#include <QtCore/QString>
class IVpn
{
public:
virtual ~IVpn() = default;
virtual void connect(const QString &preferredServer, const QList<QString> &servers, const QString &username, const QString &passwd, const QString &extraArgs) = 0;
virtual void disconnect() = 0;
virtual int status() = 0;
// signals: // SIGNALS
// virtual void connected();
// virtual void disconnected();
// virtual void error(const QString &errorMessage);
// virtual void logAvailable(const QString &log);
};
Q_DECLARE_INTERFACE(IVpn, "IVpn") // define this out of namespace scope
#endif

13
GPClient/vpn_dbus.cpp Normal file
View File

@@ -0,0 +1,13 @@
#include "vpn_dbus.h"
void VpnDbus::connect(const QString &preferredServer, const QList<QString> &servers, const QString &username, const QString &passwd, const QString &extraArgs) {
inner->connect(preferredServer, username, passwd, extraArgs);
}
void VpnDbus::disconnect() {
inner->disconnect();
}
int VpnDbus::status() {
return inner->status();
}

33
GPClient/vpn_dbus.h Normal file
View File

@@ -0,0 +1,33 @@
#ifndef VPN_DBUS_H
#define VPN_DBUS_H
#include "vpn.h"
#include "gpserviceinterface.h"
class VpnDbus : public QObject, public IVpn
{
Q_OBJECT
Q_INTERFACES(IVpn)
private:
com::yuezk::qt::GPService *inner;
public:
VpnDbus(QObject *parent) : QObject(parent) {
inner = new com::yuezk::qt::GPService("com.yuezk.qt.GPService", "/", QDBusConnection::systemBus(), this);
QObject::connect(inner, &com::yuezk::qt::GPService::connected, this, &VpnDbus::connected);
QObject::connect(inner, &com::yuezk::qt::GPService::disconnected, this, &VpnDbus::disconnected);
QObject::connect(inner, &com::yuezk::qt::GPService::error, this, &VpnDbus::error);
QObject::connect(inner, &com::yuezk::qt::GPService::logAvailable, this, &VpnDbus::logAvailable);
}
void connect(const QString &preferredServer, const QList<QString> &servers, const QString &username, const QString &passwd, const QString &extraArgs);
void disconnect();
int status();
signals: // SIGNALS
void connected();
void disconnected();
void error(QString errorMessage);
void logAvailable(QString log);
};
#endif

24
GPClient/vpn_json.cpp Normal file
View File

@@ -0,0 +1,24 @@
#include "vpn_json.h"
#include <QTextStream>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
void VpnJson::connect(const QString &preferredServer, const QList<QString> &servers, const QString &username, const QString &passwd, const QString &extraArgs) {
QJsonArray sl;
for (const QString &srv : servers) {
sl.push_back(QJsonValue(srv));
}
QJsonObject j;
j["server"] = preferredServer;
j["availableServers"] = sl;
j["cookie"] = passwd;
QTextStream(stdout) << QJsonDocument(j).toJson(QJsonDocument::Compact) << "\n";
emit connected();
}
void VpnJson::disconnect() { /* nop */ }
int VpnJson::status() {
return 4; // disconnected
}

23
GPClient/vpn_json.h Normal file
View File

@@ -0,0 +1,23 @@
#ifndef VPN_JSON_H
#define VPN_JSON_H
#include "vpn.h"
class VpnJson : public QObject, public IVpn
{
Q_OBJECT
Q_INTERFACES(IVpn)
public:
VpnJson(QObject *parent) : QObject(parent) {}
void connect(const QString &preferredServer, const QList<QString> &servers, const QString &username, const QString &passwd, const QString &extraArgs);
void disconnect();
int status();
signals: // SIGNALS
void connected();
void disconnected();
void error(const QString &errorMessage);
void logAvailable(const QString &log);
};
#endif

74
GPService/CMakeLists.txt Normal file
View File

@@ -0,0 +1,74 @@
include("${CMAKE_SOURCE_DIR}/cmake/Add3rdParty.cmake")
project(GPService)
set(gpservice_GENERATED_SOURCES)
configure_file(dbus/com.yuezk.qt.GPService.service.in dbus/com.yuezk.qt.GPService.service)
configure_file(systemd/gpservice.service.in systemd/gpservice.service)
# generate the dbus xml definition
qt5_generate_dbus_interface(
gpservice.h
${CMAKE_BINARY_DIR}/com.yuezk.qt.GPService.xml
)
# generate dbus adaptor
qt5_add_dbus_adaptor(
gpservice_GENERATED_SOURCES
${CMAKE_BINARY_DIR}/com.yuezk.qt.GPService.xml
gpservice.h
GPService
)
add_executable(gpservice
gpservice.cpp
main.cpp
${gpservice_GENERATED_SOURCES}
)
add_3rdparty(
SingleApplication
GIT_REPOSITORY https://github.com/itay-grudev/SingleApplication.git
GIT_TAG v3.3.0
CMAKE_ARGS
-DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
-DCMAKE_CXX_FLAGS_RELEASE=${CMAKE_CXX_FLAGS_RELEASE}
-DCMAKE_FIND_ROOT_PATH=${CMAKE_FIND_ROOT_PATH}
-DCMAKE_PREFIX_PATH=$ENV{CMAKE_PREFIX_PATH}
-DQAPPLICATION_CLASS=QCoreApplication
)
ExternalProject_Get_Property(SingleApplication-${PROJECT_NAME} SOURCE_DIR BINARY_DIR)
set(SingleApplication_INCLUDE_DIR ${SOURCE_DIR})
set(SingleApplication_LIBRARY ${BINARY_DIR}/libSingleApplication.a)
add_dependencies(gpservice SingleApplication-${PROJECT_NAME})
target_include_directories(gpservice PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_BINARY_DIR}
${SingleApplication_INCLUDE_DIR}
)
target_link_libraries(gpservice
${SingleApplication_LIBRARY}
Qt5::Core
Qt5::Network
Qt5::DBus
QtSignals
)
target_compile_definitions(gpservice PUBLIC QAPPLICATION_CLASS=QCoreApplication)
install(TARGETS gpservice DESTINATION bin)
install(FILES "dbus/com.yuezk.qt.GPService.conf" DESTINATION share/dbus-1/system.d )
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/dbus/com.yuezk.qt.GPService.service" DESTINATION share/dbus-1/system-services)
if("$ENV{DEBIAN_PACKAGE}")
# Install the systemd unit files to /lib/systemd/system for debian package
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/systemd/gpservice.service" DESTINATION /lib/systemd/system)
else()
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/systemd/gpservice.service" DESTINATION lib/systemd/system)
endif()

View File

@@ -1,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,5 +1,5 @@
[D-BUS Service] [D-BUS Service]
Name=com.yuezk.qt.GPService Name=com.yuezk.qt.GPService
Exec=/usr/bin/gpservice Exec=@CMAKE_INSTALL_PREFIX@/bin/gpservice
User=root User=root
SystemdService=gpservice.service SystemdService=gpservice.service

View File

@@ -1,10 +1,12 @@
#include "gpservice.h" #include <QtCore/QFileInfo>
#include "gpservice_adaptor.h" #include <QtCore/QDateTime>
#include <QtCore/QVariant>
#include <QtCore/QRegularExpression>
#include <QtCore/QRegularExpressionMatch>
#include <QtDBus/QtDBus>
#include <QFileInfo> #include "gpservice.h"
#include <QtDBus> #include "gpserviceadaptor.h"
#include <QDateTime>
#include <QVariant>
GPService::GPService(QObject *parent) GPService::GPService(QObject *parent)
: QObject(parent) : QObject(parent)
@@ -39,6 +41,47 @@ QString GPService::findBinary()
return nullptr; return nullptr;
} }
/* Port from https://github.com/qt/qtbase/blob/11d1dcc6e263c5059f34b44d531c9ccdf7c0b1d6/src/corelib/io/qprocess.cpp#L2115 */
QStringList GPService::splitCommand(QString command)
{
QStringList args;
QString tmp;
int quoteCount = 0;
bool inQuote = false;
// handle quoting. tokens can be surrounded by double quotes
// "hello world". three consecutive double quotes represent
// the quote character itself.
for (int i = 0; i < command.size(); ++i) {
if (command.at(i) == QLatin1Char('"')) {
++quoteCount;
if (quoteCount == 3) {
// third consecutive quote
quoteCount = 0;
tmp += command.at(i);
}
continue;
}
if (quoteCount) {
if (quoteCount == 1)
inQuote = !inQuote;
quoteCount = 0;
}
if (!inQuote && command.at(i).isSpace()) {
if (!tmp.isEmpty()) {
args += tmp;
tmp.clear();
}
} else {
tmp += command.at(i);
}
}
if (!tmp.isEmpty())
args += tmp;
return args;
}
void GPService::quit() void GPService::quit()
{ {
if (openconnect->state() == QProcess::NotRunning) { if (openconnect->state() == QProcess::NotRunning) {
@@ -49,7 +92,7 @@ void GPService::quit()
} }
} }
void GPService::connect(QString server, QString username, QString passwd) void GPService::connect(QString server, QString username, QString passwd, QString extraArgs)
{ {
if (vpnStatus != GPService::VpnNotConnected) { if (vpnStatus != GPService::VpnNotConnected) {
log("VPN status is: " + QVariant::fromValue(vpnStatus).toString()); log("VPN status is: " + QVariant::fromValue(vpnStatus).toString());
@@ -58,18 +101,53 @@ void GPService::connect(QString server, QString username, QString passwd)
QString bin = findBinary(); QString bin = findBinary();
if (bin == nullptr) { if (bin == nullptr) {
log("Could not found openconnect binary, make sure openconnect is installed, exiting."); log("Could not find openconnect binary, make sure openconnect is installed, exiting.");
emit error("The OpenConect CLI was not found, make sure it has been installed!");
return;
}
if (!isValidVersion(bin)) {
return; return;
} }
QStringList args; QStringList args;
args << QCoreApplication::arguments().mid(1) args << QCoreApplication::arguments().mid(1)
<< "--protocol=gp" << "--protocol=gp"
<< "-u" << username << splitCommand(extraArgs)
<< "-C" << passwd << "-u" << username
<< server; << "--cookie-on-stdin"
<< server;
log("Start process with arugments: " + args.join(" "));
openconnect->start(bin, args); openconnect->start(bin, args);
openconnect->write((passwd + "\n").toUtf8());
}
bool GPService::isValidVersion(QString &bin) {
QProcess p;
p.start(bin, QStringList("--version"));
p.waitForFinished();
QString output = p.readAllStandardError() + p.readAllStandardOutput();
QRegularExpression re("v(\\d+).*?(\\s|\\n)");
QRegularExpressionMatch match = re.match(output);
if (match.hasMatch()) {
log("Output of `openconnect --version`: " + output);
QString fullVersion = match.captured(0);
QString majorVersion = match.captured(1);
if (majorVersion.toInt() < 8) {
emit error("The OpenConnect version must greater than v8.0.0, got " + fullVersion);
return false;
}
} else {
log("Failed to parse the OpenConnect version from " + output);
}
return true;
} }
void GPService::disconnect() void GPService::disconnect()
@@ -103,7 +181,7 @@ void GPService::onProcessStdout()
QString output = openconnect->readAllStandardOutput(); QString output = openconnect->readAllStandardOutput();
log(output); log(output);
if (output.indexOf("Connected as") >= 0) { if (output.indexOf("Connected as") >= 0 || output.indexOf("Configured as") >= 0) {
vpnStatus = GPService::VpnConnected; vpnStatus = GPService::VpnConnected;
emit connected(); emit connected();
} }

View File

@@ -1,8 +1,8 @@
#ifndef GLOBALPROTECTSERVICE_H #ifndef GLOBALPROTECTSERVICE_H
#define GLOBALPROTECTSERVICE_H #define GLOBALPROTECTSERVICE_H
#include <QObject> #include <QtCore/QObject>
#include <QProcess> #include <QtCore/QProcess>
static const QString binaryPaths[] { static const QString binaryPaths[] {
"/usr/local/bin/openconnect", "/usr/local/bin/openconnect",
@@ -21,6 +21,8 @@ public:
explicit GPService(QObject *parent = nullptr); explicit GPService(QObject *parent = nullptr);
~GPService(); ~GPService();
void quit();
enum VpnStatus { enum VpnStatus {
VpnNotConnected, VpnNotConnected,
VpnConnecting, VpnConnecting,
@@ -31,13 +33,13 @@ public:
signals: signals:
void connected(); void connected();
void disconnected(); void disconnected();
void error(QString errorMessage);
void logAvailable(QString log); void logAvailable(QString log);
public slots: public slots:
void connect(QString server, QString username, QString passwd); void connect(QString server, QString username, QString passwd, QString extraArgs);
void disconnect(); void disconnect();
int status(); int status();
void quit();
private slots: private slots:
void onProcessStarted(); void onProcessStarted();
@@ -52,7 +54,9 @@ private:
int vpnStatus = GPService::VpnNotConnected; int vpnStatus = GPService::VpnNotConnected;
void log(QString msg); void log(QString msg);
bool isValidVersion(QString &bin);
static QString findBinary(); static QString findBinary();
static QStringList splitCommand(QString command);
}; };
#endif // GLOBALPROTECTSERVICE_H #endif // GLOBALPROTECTSERVICE_H

View File

@@ -1,22 +0,0 @@
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="com.yuezk.qt.GPService">
<signal name="connected">
</signal>
<signal name="disconnected">
</signal>
<signal name="logAvailable">
<arg name="log" type="s" />
</signal>
<method name="connect">
<arg name="server" type="s" direction="in"/>
<arg name="username" type="s" direction="in"/>
<arg name="passwd" type="s" direction="in"/>
</method>
<method name="disconnect">
</method>
<method name="status">
<arg type="i" direction="out"/>
</method>
</interface>
</node>

View File

@@ -1,4 +1,5 @@
#include <QtDBus> #include <QtDBus/QtDBus>
#include "gpservice.h" #include "gpservice.h"
#include "singleapplication.h" #include "singleapplication.h"
#include "sigwatch.h" #include "sigwatch.h"

View File

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

View File

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

204
README.md
View File

@@ -2,9 +2,14 @@
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 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).
<p align="center"> <p align="center">
<img src="screenshot.png"> <img src="https://user-images.githubusercontent.com/3297602/133869036-5c02b0d9-c2d9-4f87-8c81-e44f68cfd6ac.png">
</p> </p>
<a href="https://paypal.me/zongkun" target="_blank"><img src="https://cdn.jsdelivr.net/gh/everdrone/coolbadge@5ea5937cabca5ecbfc45d6b30592bd81f219bc8d/badges/Paypal/Coffee/Blue/Small.png" alt="Buy me a coffee via Paypal" style="height: 32px; width: 268px;" ></a>
<a href="https://ko-fi.com/M4M75PYKZ" target="_blank"><img src="https://ko-fi.com/img/githubbutton_sm.svg" alt="Support me on Ko-fi" style="height: 32px; width: 238px;"></a>
<a href="https://www.buymeacoffee.com/yuezk" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 32px; width: 114px;" ></a>
## Features ## Features
- Similar user experience as the official client in macOS. - Similar user experience as the official client in macOS.
@@ -12,46 +17,183 @@ A GlobalProtect VPN client (GUI) for Linux based on Openconnect and built with Q
- Supports automatically selecting the preferred gateway from the multiple gateways. - Supports automatically selecting the preferred gateway from the multiple gateways.
- Supports switching gateway from the system tray menu manually. - Supports switching gateway from the system tray menu manually.
## Prerequisites
- Openconnect v8.x
- Qt5, qt5-webengine, qt5-websockets
### Ubuntu
1. Install openconnect v8.x
For Ubuntu 18.04 you might need to [build the latest openconnect from source code](https://gist.github.com/yuezk/ab9a4b87a9fa0182bdb2df41fab5f613).
2. Install the Qt dependencies
```sh
sudo apt install qt5-default libqt5websockets5-dev qtwebengine5-dev
```
### OpenSUSE
Install the Qt dependencies
```sh
sudo zypper install libqt5-qtbase-devel libqt5-qtwebsockets-devel libqt5-qtwebengine-devel
```
## Install ## Install
### Install from AUR (Arch/Manjaro) |OS|Stable version | Development version|
|---|--------------|--------------------|
|Linux Mint, Ubuntu 18.04 or later|[ppa:yuezk/globalprotect-openconnect](https://launchpad.net/~yuezk/+archive/ubuntu/globalprotect-openconnect)|[ppa:yuezk/globalprotect-openconnect-snapshot](https://launchpad.net/~yuezk/+archive/ubuntu/globalprotect-openconnect-snapshot)|
|Arch, Manjaro|[globalprotect-openconnect](https://archlinux.org/packages/community/x86_64/globalprotect-openconnect/)|[AUR: globalprotect-openconnect-git](https://aur.archlinux.org/packages/globalprotect-openconnect-git/)|
|Fedora|[copr: yuezk/globalprotect-openconnect](https://copr.fedorainfracloud.org/coprs/yuezk/globalprotect-openconnect/)|[copr: yuezk/globalprotect-openconnect](https://copr.fedorainfracloud.org/coprs/yuezk/globalprotect-openconnect/)|
|openSUSE, CentOS 8|[OBS: globalprotect-openconnect](https://build.opensuse.org/package/show/home:yuezk/globalprotect-openconnect)|[OBS: globalprotect-openconnect-snapshot](https://build.opensuse.org/package/show/home:yuezk/globalprotect-openconnect-snapshot)|
Install [globalprotect-openconnect](https://aur.archlinux.org/packages/globalprotect-openconnect/). Add the repository in the above table and install it with your favorite package manager tool.
### Build from source code [![Arch package](https://repology.org/badge/version-for-repo/arch/globalprotect-openconnect.svg)](https://repology.org/project/globalprotect-openconnect/versions)
[![AUR package](https://repology.org/badge/version-for-repo/aur/globalprotect-openconnect.svg)](https://repology.org/project/globalprotect-openconnect/versions)
[![Manjaro Stable package](https://repology.org/badge/version-for-repo/manjaro_stable/globalprotect-openconnect.svg)](https://repology.org/project/globalprotect-openconnect/versions)
[![Manjaro Testing package](https://repology.org/badge/version-for-repo/manjaro_testing/globalprotect-openconnect.svg)](https://repology.org/project/globalprotect-openconnect/versions)
[![Manjaro Unstable package](https://repology.org/badge/version-for-repo/manjaro_unstable/globalprotect-openconnect.svg)](https://repology.org/project/globalprotect-openconnect/versions)
[![nixpkgs unstable package](https://repology.org/badge/version-for-repo/nix_unstable/globalprotect-openconnect.svg)](https://repology.org/project/globalprotect-openconnect/versions)
[![Parabola package](https://repology.org/badge/version-for-repo/parabola/globalprotect-openconnect.svg)](https://repology.org/project/globalprotect-openconnect/versions)
### Linux Mint, Ubuntu 18.04 or later
```sh
sudo add-apt-repository ppa:yuezk/globalprotect-openconnect
sudo apt-get update
sudo apt install globalprotect-openconnect
```
> 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`.
### Arch Linux / Manjaro
```sh
sudo pacman -S globalprotect-openconnect
```
### AUR snapshot version
```sh
yay -S globalprotect-openconnect-git
```
### Fedora
```sh
sudo dnf copr enable yuezk/globalprotect-openconnect
sudo dnf install globalprotect-openconnect
```
### openSUSE
- openSUSE Tumbleweed
```sh
sudo zypper ar https://download.opensuse.org/repositories/home:/yuezk/openSUSE_Tumbleweed/home:yuezk.repo
sudo zypper ref
sudo zypper install globalprotect-openconnect
```
- openSUSE Leap
```sh
sudo zypper ar https://download.opensuse.org/repositories/home:/yuezk/openSUSE_Leap_15.2/home:yuezk.repo
sudo zypper ref
sudo zypper install globalprotect-openconnect
```
### CentOS 8
1. Add the repository: `https://download.opensuse.org/repositories/home:/yuezk/CentOS_8/home:yuezk.repo`
1. Install `globalprotect-openconnect`
## Build & Install from source code
Clone this repo with:
```sh ```sh
git clone https://github.com/yuezk/GlobalProtect-openconnect.git git clone https://github.com/yuezk/GlobalProtect-openconnect.git
cd GlobalProtect-openconnect 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.
### Ubuntu/Mint
> **⚠️ REQUIRED for Ubuntu 18.04 ⚠️**
>
> Add this [dwmw2/openconnect](https://launchpad.net/~dwmw2/+archive/ubuntu/openconnect) PPA first to install the latest openconnect.
>
> ```sh
> sudo add-apt-repository ppa:dwmw2/openconnect
> sudo apt update
> ```
Build and install with:
```sh
./scripts/install-ubuntu.sh
```
### openSUSE
Build and install with:
```sh
./scripts/install-opensuse.sh
```
### Fedora
Build and install with:
```sh
./scripts/install-fedora.sh
```
### Other Linux
Install the Qt5 dependencies and OpenConnect:
- QtCore
- QtWebEngine
- QtWebSockets
- QtDBus
- openconnect v8.x
...then build and install with:
```sh
./scripts/install.sh
```
### NixOS
In `configuration.nix`:
```
services.globalprotect = {
enable = true;
# if you need a Host Integrity Protection report
csdWrapper = "${pkgs.openconnect}/libexec/openconnect/hipreport.sh";
};
environment.systemPackages = [ globalprotect-openconnect ];
```
## Run
Once the software is installed, you can run `gpclient` to start the UI.
## Passing the Custom Parameters to `OpenConnect` CLI
Custom parameters can be appended to the `OpenConnect` CLI with the following settings.
> Tokens with spaces can be surrounded by double quotes; three consecutive double quotes represent the quote character itself.
<p align="center">
<img src="https://user-images.githubusercontent.com/3297602/130319209-744be02b-d657-4f49-a76d-d2c81b5c46d5.png" />
<p>
## Display the system tray icon on Gnome 40
Install the [AppIndicator and KStatusNotifierItem Support](https://extensions.gnome.org/extension/615/appindicator-support/) extension and you will see the system try icon (Restart the system after the installation).
<p align="center">
<img src="https://user-images.githubusercontent.com/3297602/130831022-b93492fd-46dd-4a8e-94a4-13b5747120b7.png" />
<p>
## Future plan
- [x] Improve the release process
- [ ] Process bugs and feature requests
- [ ] Support for bypassing the `gpclient` parameters
- [ ] Support the CLI mode
## Troubleshooting
The application logs can be found at: `~/.cache/GlobalProtect-openconnect/gpclient.log`
## [License](./LICENSE) ## [License](./LICENSE)
GPLv3 GPLv3

1
VERSION Normal file
View File

@@ -0,0 +1 @@
1.4.2

0
VERSION_SUFFIX Normal file
View File

27
cmake/Add3rdParty.cmake Normal file
View File

@@ -0,0 +1,27 @@
include(ExternalProject)
function(add_3rdparty NAME)
set(oneValueArgs GIT_REPOSITORY GIT_TAG)
cmake_parse_arguments(add_3rdparty_args "" "${oneValueArgs}" "" ${ARGN})
if(EXISTS "${CMAKE_SOURCE_DIR}/3rdparty/${NAME}/CMakeLists.txt")
message(STATUS "Found third party locally for ${NAME}")
ExternalProject_Add(
${NAME}-${PROJECT_NAME}
PREFIX ${CMAKE_CURRENT_BINARY_DIR}/${NAME}
SOURCE_DIR "${CMAKE_SOURCE_DIR}/3rdparty/${NAME}"
INSTALL_COMMAND ""
"${add_3rdparty_args_UNPARSED_ARGUMENTS}"
)
return()
endif()
message(STATUS "Using ExternalProject to download ${NAME}")
ExternalProject_Add(
${NAME}-${PROJECT_NAME}
PREFIX ${CMAKE_CURRENT_BINARY_DIR}/${NAME}
INSTALL_COMMAND ""
"${ARGN}"
)
endfunction()

102
cmakew Executable file
View File

@@ -0,0 +1,102 @@
#!/bin/bash
cmake_version="3.21.2"
arr_cmake_v=(${cmake_version//./ })
cmake_version_major=(${arr_cmake_v[0]})
cmake_version_minor=(${arr_cmake_v[1]})
cmake_version_patch=(${arr_cmake_v[2]})
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
cmake_base="./.cmake"
cmake_bin="${cmake_base}/cmake-$cmake_version/bin/cmake"
# download cmake if neccessary
if [ ! -f "$cmake_bin" ]; then
download_link=""
if [ "$darwin" = true ]; then
download_link="https://cmake.org/files/v$cmake_version_major.$cmake_version_minor/cmake-$cmake_version-Darwin-x86_64.tar.gz"
else
download_link="https://github.com/Kitware/CMake/releases/download/v${cmake_version}/cmake-${cmake_version}-linux-x86_64.tar.gz"
fi
wget -nv --show-progress "$download_link" -O "/tmp/cmake-$cmake_version.tar.gz"
mkdir -p "${cmake_base}/cmake-$cmake_version"
tar -xzf "/tmp/cmake-$cmake_version.tar.gz" -C "${cmake_base}/cmake-$cmake_version" --strip-components=1
rm "/tmp/cmake-$cmake_version.tar.gz"
fi
# We build the pattern for arguments to be converted via cygpath
if [ "$cygwin" = true ]; then
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# run cmake
exec "$cmake_bin" "$@"

5
debian/README.Debian vendored Normal file
View File

@@ -0,0 +1,5 @@
globalprotect-openconnect for Debian
Added debian packaging
-- Amit Joshi <> Fri, 29 May 2020 21:52:59 -0400

51
debian/changelog vendored Normal file
View File

@@ -0,0 +1,51 @@
globalprotect-openconnect (1.4.2-1) unstable; urgency=medium
* Updated VERSION, Bumped 1.4.1 > 1.4.2
* Clear SSL_OP_LEGACY_SERVER_CONNECT (#146)
-- Kevin Yue <k3vinyue@gmail.com> Fri, 06 May 2022 22:18:19 +0800
globalprotect-openconnect (1.4.1-1) unstable; urgency=medium
* Updated VERSION, Bumped 1.4.0 > 1.4.1
* print the gpservice logs
* update AUR packaging
-- Kevin Yue <k3vinyue@gmail.com> Thu, 03 Mar 2022 21:58:59 +0800
globalprotect-openconnect (1.4.0-1) unstable; urgency=medium
* Updated VERSION, Bumped 1.3.4 > 1.4.0
* Fix gpservice after openconnect v8.20 (#124)
* Add 2FA support (#112)
* Add a scripting mode to GPClient (#110)
* Stop saving credentials (#111)
* update CI
* add editorconfig
* Update README.md
* Add a run entry (#108)
* update the installation instruction of Arch Linux
-- Kevin Yue <k3vinyue@gmail.com> Wed, 02 Mar 2022 21:34:19 +0800
globalprotect-openconnect (1.3.4-1) unstable; urgency=medium
* Updated VERSION, Bumped 1.3.3 > 1.3.4
* update packaging (#100)
* shorten the sponsor links
* Update README.md
* add sponsor links
* Adding application logs location in the README (#95)
* improve the doc
* Add snap packaging (#93)
* update doc
* Migrate to cmake and refine the code structure (#92)
* QStringView -> QString
-- Kevin Yue <k3vinyue@gmail.com> Sun, 24 Oct 2021 12:13:24 +0800
globalprotect-openconnect (1.3.0-1) unstable; urgency=medium
* Bump version to 1.3.0
-- Kevin Yue <k3vinyue@gmail.com> Thu, 09 Jul 2020 10:13:46 +0800

1
debian/compat vendored Normal file
View File

@@ -0,0 +1 @@
11

13
debian/control vendored Normal file
View File

@@ -0,0 +1,13 @@
Source: globalprotect-openconnect
Section: net
Priority: optional
Maintainer: Kevin Yue <k3vinyue@gmail.com>
Build-Depends: cmake (>=3.10), debhelper (>=11~), qtbase5-dev, libqt5websockets5-dev (>=5.9), qtwebengine5-dev (>=5.9)
Standards-Version: 4.1.4
Homepage: https://github.com/yuezk/GlobalProtect-openconnect
Package: globalprotect-openconnect
Architecture: any
Multi-Arch: foreign
Depends: ${misc:Depends}, ${shlibs:Depends}, openconnect (>=8.0), libqt5websockets5 (>=5.9), libqt5webengine5 (>=5.9)
Description: A GlobalProtect VPN client (GUI) based on OpenConnect.

15
debian/copyright vendored Normal file
View File

@@ -0,0 +1,15 @@
Files: *
Copyright: 1975-present Kevin Yue
License: GPL-3+
Files: 3rdparty/plog
Copyright: 2016 Sergey Podobry (sergey.podobry at gmail.com)
License: MPL-2.0
Files: 3rdparty/qt-unix-signals
Copyright: 2014 Simon Knopp
License: MIT
Files: 3rdparty/SingleApplication
Copyright: Itay Grudev 2015 - 2020
License: MIT

1
debian/patches/series vendored Normal file
View File

@@ -0,0 +1 @@
# You must remove unused comment lines for the released package.

13
debian/rules vendored Executable file
View File

@@ -0,0 +1,13 @@
#!/usr/bin/make -f
# You must remove unused comment lines for the released package.
export DH_VERBOSE = 1
export DEB_BUILD_MAINT_OPTIONS = hardening=+all
export DEB_CFLAGS_MAINT_APPEND = -Wall -pedantic
export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed
export DEBIAN_PACKAGE=1
%:
dh $@
override_dh_installsystemd:
dh_installsystemd gpservice.service

1
debian/source/format vendored Normal file
View File

@@ -0,0 +1 @@
3.0 (native)

2
debian/source/local-options vendored Normal file
View File

@@ -0,0 +1,2 @@
#abort-on-upstream-changes
#unapply-patches

3
debian/watch vendored Normal file
View File

@@ -0,0 +1,3 @@
version=4
opts="mode=git,pgpmode=gittag" \
https://github.com/yuezk/GlobalProtect-openconnect.git refs/tags/v([\d\.]+)

33
packaging/aur/PKGBUILD Normal file
View File

@@ -0,0 +1,33 @@
# Maintainer: Keinv Yue <yuezk001@gmail.com>
pkgname=globalprotect-openconnect
pkgver=0
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/GlobalProtect-openconnect"
license=('GPL3')
depends=('openconnect>=8.0.0' qt5-base qt5-webengine qt5-websockets)
makedepends=(cmake)
provides=('gpclient' 'gpservice')
source=("${pkgname}.tar.gz")
sha256sums=('SKIP')
pkgver() {
cd $srcdir/$pkgname-*/
cat VERSION VERSION_SUFFIX
}
build() {
cd $srcdir/$pkgname-*/
cmake -B build \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_FLAGS_RELEASE=-s
make -j$(nproc) -C build
}
package() {
cd $srcdir/$pkgname-*/
make DESTDIR="$pkgdir/" install -C build
}

View File

@@ -0,0 +1,35 @@
# Maintainer: Keinv Yue <yuezk001@gmail.com>
pkgname=globalprotect-openconnect-git
_pkgname=globalprotect-openconnect
pkgver=0
pkgrel=1
pkgdesc="A GlobalProtect VPN client (GUI) for Linux based on Openconnect and built with Qt5, supports SAML auth mode. (development version)"
arch=(x86_64 aarch64)
url="https://github.com/yuezk/GlobalProtect-openconnect"
license=('GPL3')
depends=('openconnect>=8.0.0' qt5-base qt5-webengine qt5-websockets)
makedepends=(cmake)
conflicts=('globalprotect-openconnect')
provides=('gpclient' 'gpservice')
source=("${_pkgname}.tar.gz")
sha256sums=('SKIP')
pkgver() {
cd $srcdir/$_pkgname-*/
cat VERSION VERSION_SUFFIX
}
build() {
cd $srcdir/${_pkgname}-*/
cmake -B build \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_FLAGS_RELEASE=-s
make -j$(nproc) -C build
}
package() {
cd $srcdir/${_pkgname}-*/
make DESTDIR="$pkgdir/" install -C build
}

View File

@@ -0,0 +1,26 @@
app-id: com.yuezk.qt.gpclient
base: io.qt.qtwebengine.BaseApp
base-version: '5.15'
runtime: org.kde.Platform
runtime-version: '5.15'
sdk: org.kde.Sdk
command: gpclient
finish-args:
- --share=network
- --share=ipc
- --socket=x11
- --socket=wayland
- --filesystem=host
- --device=dri
- --talk-name=org.kde.StatusNotifierWatcher
- --own-name=org.kde.*
- --system-own-name=com.yuezk.qt.GPService
modules:
- name: gpclient
buildsystem: cmake
config-opts:
- -DCMAKE_BUILD_TYPE=Release
- -DCMAKE_CXX_FLAGS_RELEASE=-s
sources:
- type: archive
path: globalprotect-openconnect.tar.gz

View File

@@ -0,0 +1 @@
setBadness('suse-dbus-unauthorized-service', 0)

View File

@@ -0,0 +1,51 @@
-------------------------------------------------------------------
Fri May 6 14:18:19 UTC 2022 - k3vinyue@gmail.com - 1.4.2
- Update to 1.4.2
* Updated VERSION, Bumped 1.4.1 > 1.4.2
* Clear SSL_OP_LEGACY_SERVER_CONNECT (#146)
-------------------------------------------------------------------
Thu Mar 3 13:58:59 UTC 2022 - k3vinyue@gmail.com - 1.4.1
- Update to 1.4.1
* Updated VERSION, Bumped 1.4.0 > 1.4.1
* print the gpservice logs
* update AUR packaging
-------------------------------------------------------------------
Wed Mar 2 13:34:19 UTC 2022 - k3vinyue@gmail.com - 1.4.0
- Update to 1.4.0
* Updated VERSION, Bumped 1.3.4 > 1.4.0
* Fix gpservice after openconnect v8.20 (#124)
* Add 2FA support (#112)
* Add a scripting mode to GPClient (#110)
* Stop saving credentials (#111)
* update CI
* add editorconfig
* Update README.md
* Add a run entry (#108)
* update the installation instruction of Arch Linux
-------------------------------------------------------------------
Sun Oct 24 04:13:24 UTC 2021 - k3vinyue@gmail.com - 1.3.4
- Update to 1.3.4
* Updated VERSION, Bumped 1.3.3 > 1.3.4
* update packaging (#100)
* shorten the sponsor links
* Update README.md
* add sponsor links
* Adding application logs location in the README (#95)
* improve the doc
* Add snap packaging (#93)
* update doc
* Migrate to cmake and refine the code structure (#92)
* QStringView -> QString
-------------------------------------------------------------------
Thu Jul 9 02:13:46 UTC 2020 - k3vinyue@gmail.com - 1.3.0
- Update to 1.3.0
* Bump version to 1.3.0

View File

@@ -0,0 +1,96 @@
Name: globalprotect-openconnect
Version: 1.4.2
Release: 1
Summary: A GlobalProtect VPN client powered by OpenConnect
Group: Productivity/Networking/PPP
BuildRoot: %{_tmppath}/%{name}-%{version}-build
License: GPL-3.0
URL: https://github.com/yuezk/GlobalProtect-openconnect
Source0: %{name}.tar.gz
BuildRequires: cmake cmake(Qt5) cmake(Qt5Gui) cmake(Qt5WebEngine) cmake(Qt5WebSockets) cmake(Qt5DBus)
BuildRequires: systemd-rpm-macros
Requires: openconnect >= 8.0
Conflicts: globalprotect-openconnect-snapshot
%global debug_package %{nil}
%description
A GlobalProtect VPN client (GUI) for Linux based on OpenConnect and built with Qt5, supports SAML auth mode.
%prep
%autosetup -n "globalprotect-openconnect-%{version}"
%pre
%if 0%{?suse_version}
%service_add_pre gpservice.service
%endif
%post
%if 0%{?suse_version}
%service_add_post gpservice.service
%else
%systemd_post gpservice.service
%endif
%preun
%if 0%{?suse_version}
%service_del_preun gpservice.service
%else
%systemd_preun gpservice.service
%endif
%postun
%if 0%{?suse_version}
%service_del_postun gpservice.service
%else
%systemd_postun gpservice.service
%endif
%build
%cmake -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_FLAGS_RELEASE=-s
%if 0%{?fedora_version} && 0%{?fedora_version} <= 32
%make_build
%else
%cmake_build
%endif
%install
%if 0%{?fedora_version} && 0%{?fedora_version} <= 32
%make_install
%else
%cmake_install
%endif
%files
%defattr(-,root,root)
%{_unitdir}/gpservice.service
%{_bindir}/gpclient
%{_bindir}/gpservice
%{_datadir}/applications/com.yuezk.qt.gpclient.desktop
%{_datadir}/dbus-1/system-services/com.yuezk.qt.GPService.service
%{_datadir}/dbus-1/system.d/com.yuezk.qt.GPService.conf
%{_datadir}/icons/hicolor/scalable/apps/com.yuezk.qt.gpclient.svg
%{_datadir}/metainfo/com.yuezk.qt.gpclient.metainfo.xml
%dir %{_datadir}/icons/hicolor
%dir %{_datadir}/icons/hicolor/scalable
%dir %{_datadir}/icons/hicolor/scalable/apps
%changelog

1
plog

Submodule plog deleted from fda4a26c26

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

26
scripts/_archive-all.sh Executable file
View File

@@ -0,0 +1,26 @@
#!/bin/bash -e
VERSION=$(cat VERSION VERSION_SUFFIX)
rm -rf ./artifacts && mkdir -p ./artifacts/{obs,aur,flatpak}
# Add the version file
echo $VERSION > ./artifacts/VERSION
# Archive the source code
git-archive-all \
--force-submodules \
--prefix=globalprotect-openconnect-${VERSION}/ \
./artifacts/globalprotect-openconnect-${VERSION}.tar.gz
# Prepare the OBS package
cp -r ./packaging/obs ./artifacts
cp ./artifacts/*.tar.gz ./artifacts/obs/globalprotect-openconnect.tar.gz
# Prepare the AUR package
cp ./packaging/aur/PKGBUILD-git ./artifacts/aur/PKGBUILD
cp ./artifacts/*.tar.gz ./artifacts/aur/globalprotect-openconnect.tar.gz
# Prepare the flatpak package
cp ./packaging/flatpak/com.yuezk.qt.gpclient.yml ./artifacts/flatpak
cp ./artifacts/*.tar.gz ./artifacts/flatpak/globalprotect-openconnect.tar.gz

7
scripts/build.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/bash -e
./cmakew -B build \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_FLAGS_RELEASE=-s
MAKEFLAGS=-j$(nproc) ./cmakew --build build

452
scripts/bump-version.sh Executable file
View File

@@ -0,0 +1,452 @@
#!/bin/bash
#
# █▄▄ █░█ █▀▄▀█ █▀█ ▄▄ █░█ █▀▀ █▀█ █▀ █ █▀█ █▄░█
# █▄█ █▄█ █░▀░█ █▀▀ ░░ ▀▄▀ ██▄ █▀▄ ▄█ █ █▄█ █░▀█
#
# Description:
# - This script automates bumping the git software project's version using automation.
# - It does several things that are typically required for releasing a Git repository, like git tagging,
# automatic updating of CHANGELOG.md, and incrementing the version number in various JSON files.
# - Increments / suggests the current software project's version number
# - Adds a Git tag, named after the chosen version number
# - Updates CHANGELOG.md
# - Updates VERSION file
# - Commits files to a new branch
# - Pushes to remote (optionally)
# - Updates "version" : "x.x.x" tag in JSON files if [-v file1 -v file2...] argument is supplied.
#
# Usage:
# ./bump-version.sh [-v <version number>] [-m <release message>] [-j <file1>] [-j <file2>].. [-n] [-p] [-b] [-h]
#
# Options:
# -v <version number> Specify a manual version number
# -m <release message> Custom release message.
# -f <filename.json> Update version number inside JSON files.
# * For multiple files, add a separate -f option for each one,
# * For example: ./bump-version.sh -f src/plugin/package.json -f composer.json
# -p <repository alias> Push commits to remote repository, eg `-p origin`
# -n Don't perform a commit automatically.
# * You may want to do that yourself, for example.
# -b Don't create automatic `release-<version>` branch
# -h Show help message.
#
# Detailed notes:
# The contents of the `VERSION` file which should be a semantic version number such as "1.2.3"
# or even "1.2.3-beta+001.ab"
#
# It pulls a list of changes from git history & prepends to a file called CHANGELOG.md
# under the title of the new version # number, allows the user to review and update the changelist
#
# Creates a Git tag with the version number
#
# - Creates automatic `release-<version>` branch
#
# Commits the new version to the current repository
#
# Optionally pushes the commit to remote repository
#
# Make sure to set execute permissions for the script, eg `$ chmod 755 bump-version.sh`
#
# Credits:
# https://github.com/jv-k/bump-version
#
# - Inspired by the scripts from @pete-otaqui and @mareksuscak
# https://gist.github.com/pete-otaqui/4188238
# https://gist.github.com/mareksuscak/1f206fbc3bb9d97dec9c
#
NOW="$(date +'%B %d, %Y')"
# ANSI/VT100 colours
YELLOW='\033[1;33m'
LIGHTYELLOW='\033[0;33m'
RED='\033[0;31m'
LIGHTRED='\033[1;31m'
GREEN='\033[0;32m'
LIGHTGREEN='\033[1;32m'
BLUE='\033[0;34m'
LIGHTBLUE='\033[1;34m'
PURPLE='\033[0;35m'
LIGHTPURPLE='\033[1;35m'
CYAN='\033[0;36m'
LIGHTCYAN='\033[1;36m'
WHITE='\033[1;37m'
LIGHTGRAY='\033[0;37m'
DARKGRAY='\033[1;30m'
BOLD="\033[1m"
INVERT="\033[7m"
RESET='\033[0m'
# Default options
FLAG_JSON="false"
FLAG_PUSH="false"
I_OK="✅"; I_STOP="🚫"; I_ERROR="❌"; I_END="👋🏻"
S_NORM="${WHITE}"
S_LIGHT="${LIGHTGRAY}"
S_NOTICE="${GREEN}"
S_QUESTION="${YELLOW}"
S_WARN="${LIGHTRED}"
S_ERROR="${RED}"
V_SUGGEST="0.1.0" # This is suggested in case VERSION file or user supplied version via -v is missing
GIT_MSG=""
REL_NOTE=""
REL_PREFIX="release-"
PUSH_DEST="origin"
# Show credits & help
usage() {
echo -e "$GREEN"\
"\n █▄▄ █░█ █▀▄▀█ █▀█ ▄▄ █░█ █▀▀ █▀█ █▀ █ █▀█ █▄░█ "\
"\n █▄█ █▄█ █░▀░█ █▀▀ ░░ ▀▄▀ ██▄ █▀▄ ▄█ █ █▄█ █░▀█ "\
"\n\t\t\t\t\t$LIGHTGRAY v${SCRIPT_VER}"\
echo -e " ${S_NORM}${BOLD}Usage:${RESET}"\
"\n $0 [-v <version number>] [-m <release message>] [-j <file1>] [-j <file2>].. [-n] [-p] [-h]" 1>&2;
echo -e "\n ${S_NORM}${BOLD}Options:${RESET}"
echo -e " $S_WARN-v$S_NORM <version number>\tSpecify a manual version number"
echo -e " $S_WARN-m$S_NORM <release message>\tCustom release message."
echo -e " $S_WARN-f$S_NORM <filename.json>\tUpdate version number inside JSON files."\
"\n\t\t\t* For multiple files, add a separate -f option for each one,"\
"\n\t\t\t* For example: ./bump-version.sh -f src/plugin/package.json -f composer.json"
echo -e " $S_WARN-p$S_NORM \t\t\tPush commits to ORIGIN. "
echo -e " $S_WARN-n$S_NORM \t\t\tDon't perform a commit automatically. "\
"\n\t\t\t* You may want to do that manually after checking everything, for example."
echo -e " $S_WARN-b$S_NORM \t\t\tDon't create automatic \`release-<version>\` branch"
echo -e " $S_WARN-h$S_NORM \t\t\tShow this help message. "
echo -e "\n ${S_NORM}${BOLD}Author:$S_LIGHT https://github.com/jv-t/bump-version $RESET\n"
}
# If there are no commits in repo, quit, because you can't tag with zero commits.
check-commits-exist() {
git rev-parse HEAD &> /dev/null
if [ ! "$?" -eq 0 ]; then
echo -e "\n${I_STOP} ${S_ERROR}Your current branch doesn't have any commits yet. Can't tag without at least one commit." >&2
echo
exit 1
fi
}
get-commit-msg() {
echo Bumped $([ -n "${V_PREV}" ] && echo "${V_PREV} >" || echo "to ") "$V_USR_INPUT"
}
exit_abnormal() {
echo -e " ${S_LIGHT}"
usage # Show help
exit 1
}
# Process script options
process-arguments() {
local OPTIONS OPTIND OPTARG
# Get positional parameters
JSON_FILES=( )
while getopts ":v:p:m:f:hbn" OPTIONS; do # Note: Adding the first : before the flags takes control of flags and prevents default error msgs.
case "$OPTIONS" in
h )
# Show help
exit_abnormal
;;
v )
# User has supplied a version number
V_USR_SUPPLIED=$OPTARG
;;
m )
REL_NOTE=$OPTARG
# Custom release note
echo -e "\n${S_LIGHT}Option set: ${S_NOTICE}Release note:" ${S_NORM}"'"$REL_NOTE"'"
;;
f )
FLAG_JSON=true
echo -e "\n${S_LIGHT}Option set: ${S_NOTICE}JSON file via [-f]: <${S_NORM}${OPTARG}${S_LIGHT}>"
# Store JSON filenames(s)
JSON_FILES+=($OPTARG)
;;
p )
FLAG_PUSH=true
PUSH_DEST=${OPTARG} # Replace default with user input
echo -e "\n${S_LIGHT}Option set: ${S_NOTICE}Pushing to <${S_NORM}${PUSH_DEST}${S_LIGHT}>, as the last action in this script."
;;
n )
FLAG_NOCOMMIT=true
echo -e "\n${S_LIGHT}Option set: ${S_NOTICE}Disable commit after tagging."
;;
b )
FLAG_NOBRANCH=true
echo -e "\n${S_LIGHT}Option set: ${S_NOTICE}Disable committing to new branch."
;;
\? )
echo -e "\n${I_ERROR}${S_ERROR} Invalid option: ${S_WARN}-$OPTARG" >&2
echo
exit_abnormal
;;
: )
echo -e "\n${I_ERROR}${S_ERROR} Option ${S_WARN}-$OPTARG ${S_ERROR}requires an argument." >&2
echo
exit_abnormal
;;
esac
done
}
# Suggests version from VERSION file, or grabs from user supplied -v <version>.
# If none is set, suggest default from options.
process-version() {
if [ -f VERSION ] && [ -s VERSION ]; then
V_PREV=`cat VERSION`
echo -e "\n${S_NOTICE}Current version from <${S_NORM}VERSION${S_NOTICE}> file: ${S_NORM}$V_PREV"
# Suggest incremented value from VERSION file
V_PREV_LIST=(`echo $V_PREV | tr '.' ' '`)
V_MAJOR=${V_PREV_LIST[0]}; V_MINOR=${V_PREV_LIST[1]}; V_PATCH=${V_PREV_LIST[2]};
# Test if V_PATCH is a number, then increment it. Otherwise, do nothing
if [ "$V_PATCH" -eq "$V_PATCH" ] 2>/dev/null; then # discard stderr (2) output to black hole (suppress it)
V_PATCH=$((V_PATCH + 1)) # Increment
fi
V_SUGGEST="$V_MAJOR.$V_MINOR.$V_PATCH"
else
echo -ne "\n${S_WARN}The [${S_NORM}VERSION${S_WARN}] "
if [ ! -f VERSION ]; then
echo "file was not found.";
elif [ ! -s VERSION ]; then
echo "file is empty.";
fi
fi
# If a version number is supplied by the user with [-v <version number>], then use it
if [ -n "$V_USR_SUPPLIED" ]; then
echo -e "\n${S_NOTICE}You selected version using [-v]:" "${S_WARN}${V_USR_SUPPLIED}"
V_USR_INPUT="${V_USR_SUPPLIED}"
else
echo -ne "\n${S_QUESTION}Enter a new version number [${S_NORM}$V_SUGGEST${S_QUESTION}]: "
echo -ne "$S_WARN"
read V_USR_INPUT
if [ "$V_USR_INPUT" = "" ]; then
V_USR_INPUT="${V_SUGGEST}"
fi
fi
# echo -e "${S_NOTICE}Setting version to [${S_NORM}${V_USR_INPUT}${S_NOTICE}] ...."
}
# Only tag if tag doesn't already exist
check-tag-exists() {
TAG_CHECK_EXISTS=`git tag -l v"$V_USR_INPUT"`
if [ -n "$TAG_CHECK_EXISTS" ]; then
echo -e "\n${I_STOP} ${S_ERROR}Error: A release with that tag version number already exists!\n"
exit 0
fi
}
# $1 : version
# $2 : release note
tag() {
if [ -z "$2" ]; then
# Default release note
git tag -a "v$1" -m "Tag version $1."
else
# Custom release note
git tag -a "v$1" -m "$2"
fi
echo -e "\n${I_OK} ${S_NOTICE}Added GIT tag"
}
# Change `version:` value in JSON files, like packager.json, composer.json, etc
bump-json-files() {
if [ "$FLAG_JSON" != true ]; then return; fi
JSON_PROCESSED=( ) # holds filenames after they've been changed
for FILE in "${JSON_FILES[@]}"; do
if [ -f $FILE ]; then
# Get the existing version number
V_OLD=$( sed -n 's/.*"version": "\(.*\)",/\1/p' $FILE )
if [ "$V_OLD" = "$V_USR_INPUT" ]; then
echo -e "\n${S_WARN}File <${S_NORM}$FILE${S_WARN}> already contains version: ${S_NORM}$V_OLD"
else
# Write to output file
FILE_MSG=`sed -i .temp "s/\"version\": \"$V_OLD\"/\"version\": \"$V_USR_INPUT\"/g" $FILE 2>&1`
if [ "$?" -eq 0 ]; then
echo -e "\n${I_OK} ${S_NOTICE}Updated file: <${S_NOTICE}$FILE${S_LIGHT}> from ${S_NORM}$V_OLD -> $V_USR_INPUT"
rm -f ${FILE}.temp
# Add file change to commit message:
GIT_MSG+="${GIT_MSG}Updated $FILE, "
else
echo -e "\n${I_STOP} ${S_ERROR}Error\n$PUSH_MSG\n"
fi
fi
JSON_PROCESSED+=($FILE)
else
echo -e "\n${S_WARN}File <${S_NORM}$FILE${S_WARN}> not found."
fi
done
# Stage files that were changed:
[ -n "${JSON_PROCESSED}" ] && git add "${JSON_PROCESSED[@]}"
}
# Handle VERSION file
do-versionfile() {
[ -f VERSION ] && ACTION_MSG="Updated" || ACTION_MSG="Created"
GIT_MSG+="${ACTION_MSG} VERSION, "
echo $V_USR_INPUT | tr -d "\n" > VERSION # Create file
echo -e "\n${I_OK} ${S_NOTICE}${ACTION_MSG} [${S_NORM}VERSION${S_NOTICE}] file"
# Stage file for commit
git add VERSION
}
# Dump git log history to CHANGELOG.md
do-changelog() {
# Log latest commits to CHANGELOG.md:
# Get latest commits
LOG_MSG=`git log --pretty=format:"- %s" $([ -n "$V_PREV" ] && echo "v${V_PREV}...HEAD") 2>&1`
if [ ! "$?" -eq 0 ]; then
echo -e "\n${I_STOP} ${S_ERROR}Error getting commit history for logging to CHANGELOG.\n$LOG_MSG\n"
exit 1
fi
[ -f CHANGELOG.md ] && ACTION_MSG="Updated" || ACTION_MSG="Created"
# Add info to commit message for later:
GIT_MSG+="${ACTION_MSG} CHANGELOG.md, "
# Add heading
echo "## $V_USR_INPUT ($NOW)" > tmpfile
# Log the bumping commit:
# - The final commit is done after do-changelog(), so we need to create the log entry for it manually:
echo "- ${GIT_MSG}$(get-commit-msg)" >> tmpfile
# Add previous commits
[ -n "$LOG_MSG" ] && echo "$LOG_MSG" >> tmpfile
echo -en "\n" >> tmpfile
if [ -f CHANGELOG.md ]; then
# Append existing log
cat CHANGELOG.md >> tmpfile
else
echo -e "\n${S_WARN}A [${S_NORM}CHANGELOG.md${S_WARN}] file was not found."
fi
mv tmpfile CHANGELOG.md
# User prompts
echo -e "\n${I_OK} ${S_NOTICE}${ACTION_MSG} [${S_NORM}CHANGELOG.md${S_NOTICE}] file"
# Pause & allow user to open and edit the file:
echo -en "\n${S_QUESTION}Make adjustments to [${S_NORM}CHANGELOG.md${S_QUESTION}] if required now. Press <enter> to continue."
read
# Stage log file, to commit later
git add CHANGELOG.md
}
#
check-branch-exist() {
[ "$FLAG_NOBRANCH" = true ] && return
BRANCH_MSG=`git rev-parse --verify "${REL_PREFIX}${V_USR_INPUT}" 2>&1`
if [ "$?" -eq 0 ]; then
echo -e "\n${I_STOP} ${S_ERROR}Error: Branch <${S_NORM}${REL_PREFIX}${V_USR_INPUT}${S_ERROR}> already exists!\n"
exit 1
fi
}
#
do-branch() {
[ "$FLAG_NOBRANCH" = true ] && return
echo -e "\n${S_NOTICE}Creating new release branch..."
BRANCH_MSG=`git branch "${REL_PREFIX}${V_USR_INPUT}" 2>&1`
if [ ! "$?" -eq 0 ]; then
echo -e "\n${I_STOP} ${S_ERROR}Error\n$BRANCH_MSG\n"
exit 1
else
BRANCH_MSG=`git checkout "${REL_PREFIX}${V_USR_INPUT}" 2>&1`
echo -e "\n${I_OK} ${S_NOTICE}${BRANCH_MSG}"
fi
# REL_PREFIX
}
# Stage & commit all files modified by this script
do-commit() {
[ "$FLAG_NOCOMMIT" = true ] && return
GIT_MSG+="$(get-commit-msg)"
echo -e "\n${S_NOTICE}Committing..."
COMMIT_MSG=`git commit -m "${GIT_MSG}" 2>&1`
if [ ! "$?" -eq 0 ]; then
echo -e "\n${I_STOP} ${S_ERROR}Error\n$COMMIT_MSG\n"
exit 1
else
echo -e "\n${I_OK} ${S_NOTICE}$COMMIT_MSG"
fi
}
# Pushes files + tags to remote repo. Changes are staged by earlier functions
do-push() {
[ "$FLAG_NOCOMMIT" = true ] && return
if [ "$FLAG_PUSH" = true ]; then
CONFIRM="Y"
else
echo -ne "\n${S_QUESTION}Push tags to <${S_NORM}${PUSH_DEST}${S_QUESTION}>? [${S_NORM}N/y${S_QUESTION}]: "
read CONFIRM
fi
case "$CONFIRM" in
[yY][eE][sS]|[yY] )
echo -e "\n${S_NOTICE}Pushing files + tags to <${S_NORM}${PUSH_DEST}${S_NOTICE}>..."
PUSH_MSG=`git push "${PUSH_DEST}" v"$V_USR_INPUT" 2>&1` # Push new tag
if [ ! "$?" -eq 0 ]; then
echo -e "\n${I_STOP} ${S_WARN}Warning\n$PUSH_MSG"
# exit 1
else
echo -e "\n${I_OK} ${S_NOTICE}$PUSH_MSG"
fi
;;
esac
}
#### Initiate Script ###########################
check-commits-exist
# Process and prepare
process-arguments "$@"
process-version
check-branch-exist
check-tag-exists
echo -e "\n${S_LIGHT}"
# Update files
bump-json-files
do-versionfile
# do-changelog
# do-branch
do-commit
# tag "${V_USR_INPUT}" "${REL_NOTE}"
do-push
echo -e "\n${S_LIGHT}"
echo -e "\n${I_OK} ${S_NOTICE}"Bumped $([ -n "${V_PREV}" ] && echo "${V_PREV} >" || echo "to ") "$V_USR_INPUT"
echo -e "\n${GREEN}Done ${I_END}\n"

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