mirror of
https://github.com/yuezk/GlobalProtect-openconnect.git
synced 2025-05-20 07:26:58 -04:00
Compare commits
13 Commits
v2.0.0-bet
...
v2.0.0-bet
Author | SHA1 | Date | |
---|---|---|---|
|
aac401e7ee | ||
|
9655b735a1 | ||
|
c3bd7aeb93 | ||
|
0b55a80317 | ||
|
c6315bf384 | ||
|
87b965f80c | ||
|
b09b21ae0f | ||
|
7e372cd113 | ||
|
1e211e8912 | ||
|
8bc4049a0f | ||
|
03f8c98cb5 | ||
|
5c56acc677 | ||
|
2d8393dcf7 |
134
.github/workflows/build.yaml
vendored
134
.github/workflows/build.yaml
vendored
@@ -114,137 +114,3 @@ jobs:
|
|||||||
name: artifact-${{ matrix.arch }}-tauri
|
name: artifact-${{ matrix.arch }}-tauri
|
||||||
path: |
|
path: |
|
||||||
gpgui/.tmp/artifact
|
gpgui/.tmp/artifact
|
||||||
|
|
||||||
package-rpm:
|
|
||||||
needs: [setup-matrix, build-tauri]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
arch: ${{ fromJson(needs.setup-matrix.outputs.matrix) }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout gpgui repo
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GH_PAT }}
|
|
||||||
repository: yuezk/gpgui
|
|
||||||
path: gpgui
|
|
||||||
|
|
||||||
- name: Download artifact-${{ matrix.arch }}
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: artifact-${{ matrix.arch }}-tauri
|
|
||||||
path: gpgui/.tmp/artifact
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
with:
|
|
||||||
platforms: ${{ matrix.arch }}
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Create RPM package
|
|
||||||
run: |
|
|
||||||
docker run \
|
|
||||||
--rm \
|
|
||||||
-v $(pwd):/${{ github.workspace }} \
|
|
||||||
-w ${{ github.workspace }} \
|
|
||||||
--platform linux/${{ matrix.arch }} \
|
|
||||||
yuezk/gpdev:rpm-builder \
|
|
||||||
"./gpgui/scripts/build-rpm.sh"
|
|
||||||
|
|
||||||
- name: Upload rpm artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: artifact-${{ matrix.arch }}-rpm
|
|
||||||
path: |
|
|
||||||
gpgui/.tmp/artifact/*.rpm
|
|
||||||
|
|
||||||
package-pkgbuild:
|
|
||||||
needs: [setup-matrix, build-tauri]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
arch: ${{ fromJson(needs.setup-matrix.outputs.matrix) }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout gpgui repo
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GH_PAT }}
|
|
||||||
repository: yuezk/gpgui
|
|
||||||
path: gpgui
|
|
||||||
|
|
||||||
- name: Download artifact-${{ matrix.arch }}
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: artifact-${{ matrix.arch }}-tauri
|
|
||||||
path: gpgui/.tmp/artifact
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
with:
|
|
||||||
platforms: ${{ matrix.arch }}
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Generate PKGBUILD
|
|
||||||
run: |
|
|
||||||
./gpgui/scripts/generate-pkgbuild.sh
|
|
||||||
|
|
||||||
- name: Build PKGBUILD package
|
|
||||||
run: |
|
|
||||||
# Generate PKGBUILD to .tmp/pkgbuild
|
|
||||||
./gpgui/scripts/generate-pkgbuild.sh
|
|
||||||
|
|
||||||
# Build package
|
|
||||||
docker run \
|
|
||||||
--rm \
|
|
||||||
-v $(pwd)/gpgui/.tmp/pkgbuild:/pkgbuild \
|
|
||||||
--platform linux/${{ matrix.arch }} \
|
|
||||||
yuezk/gpdev:pkgbuild
|
|
||||||
|
|
||||||
- name: Upload pkgbuild artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: artifact-${{ matrix.arch }}-pkgbuild
|
|
||||||
path: |
|
|
||||||
gpgui/.tmp/pkgbuild/*.pkg.tar.zst
|
|
||||||
|
|
||||||
gh-release:
|
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs:
|
|
||||||
- package-rpm
|
|
||||||
- package-pkgbuild
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Download artifact
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
path: artifact
|
|
||||||
pattern: artifact-*
|
|
||||||
merge-multiple: true
|
|
||||||
|
|
||||||
- name: Generate checksum
|
|
||||||
uses: jmgilman/actions-generate-checksum@v1
|
|
||||||
with:
|
|
||||||
output: checksums.txt
|
|
||||||
patterns: |
|
|
||||||
artifact/*
|
|
||||||
|
|
||||||
- name: Create GH release
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GH_PAT }}
|
|
||||||
prerelease: contains(github.ref, 'latest')
|
|
||||||
fail_on_unmatched_files: true
|
|
||||||
files: |
|
|
||||||
checksums.txt
|
|
||||||
artifact/*
|
|
||||||
|
10
.vscode/settings.json
vendored
10
.vscode/settings.json
vendored
@@ -10,8 +10,11 @@
|
|||||||
"dotenv",
|
"dotenv",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"getconfig",
|
"getconfig",
|
||||||
|
"globalprotect",
|
||||||
|
"globalprotectcallback",
|
||||||
"gpapi",
|
"gpapi",
|
||||||
"gpauth",
|
"gpauth",
|
||||||
|
"gpcallback",
|
||||||
"gpclient",
|
"gpclient",
|
||||||
"gpcommon",
|
"gpcommon",
|
||||||
"gpgui",
|
"gpgui",
|
||||||
@@ -42,10 +45,13 @@
|
|||||||
"urlencoding",
|
"urlencoding",
|
||||||
"userauthcookie",
|
"userauthcookie",
|
||||||
"utsbuf",
|
"utsbuf",
|
||||||
|
"uzers",
|
||||||
"Vite",
|
"Vite",
|
||||||
"vpnc",
|
"vpnc",
|
||||||
"vpninfo",
|
"vpninfo",
|
||||||
"wmctrl",
|
"wmctrl",
|
||||||
"XAUTHORITY"
|
"XAUTHORITY",
|
||||||
]
|
"yuezk"
|
||||||
|
],
|
||||||
|
"rust-analyzer.cargo.features": "all",
|
||||||
}
|
}
|
||||||
|
84
Cargo.lock
generated
84
Cargo.lock
generated
@@ -1423,13 +1423,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gpapi"
|
name = "gpapi"
|
||||||
version = "2.0.0-beta.1"
|
version = "2.0.0-beta7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64 0.21.5",
|
"base64 0.21.5",
|
||||||
"chacha20poly1305",
|
"chacha20poly1305",
|
||||||
|
"clap",
|
||||||
"dotenvy_macro",
|
"dotenvy_macro",
|
||||||
"log",
|
"log",
|
||||||
|
"open",
|
||||||
"redact-engine",
|
"redact-engine",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
@@ -1444,13 +1446,13 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"url",
|
"url",
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
"users",
|
"uzers",
|
||||||
"whoami",
|
"whoami",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gpauth"
|
name = "gpauth"
|
||||||
version = "2.0.0-beta.1"
|
version = "2.0.0-beta7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -1470,7 +1472,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gpclient"
|
name = "gpclient"
|
||||||
version = "2.0.0-beta.1"
|
version = "2.0.0-beta7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -1491,7 +1493,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gpservice"
|
name = "gpservice"
|
||||||
version = "2.0.0-beta.1"
|
version = "2.0.0-beta7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -1564,9 +1566,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.3.22"
|
version = "0.3.24"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178"
|
checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"fnv",
|
"fnv",
|
||||||
@@ -1583,9 +1585,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.4.0"
|
version = "0.4.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e1d308f63daf4181410c242d34c11f928dcb3aa105852019e043c9d1f4e4368a"
|
checksum = "31d030e59af851932b72ceebadf4a2b5986dba4c3b99dd2493f8273a0f151943"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"fnv",
|
"fnv",
|
||||||
@@ -1743,7 +1745,7 @@ dependencies = [
|
|||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"h2 0.3.22",
|
"h2 0.3.24",
|
||||||
"http 0.2.11",
|
"http 0.2.11",
|
||||||
"http-body 0.4.6",
|
"http-body 0.4.6",
|
||||||
"httparse",
|
"httparse",
|
||||||
@@ -1766,7 +1768,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"h2 0.4.0",
|
"h2 0.4.2",
|
||||||
"http 1.0.0",
|
"http 1.0.0",
|
||||||
"http-body 1.0.0",
|
"http-body 1.0.0",
|
||||||
"httparse",
|
"httparse",
|
||||||
@@ -1962,6 +1964,15 @@ version = "2.9.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is-docker"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "is-terminal"
|
name = "is-terminal"
|
||||||
version = "0.4.10"
|
version = "0.4.10"
|
||||||
@@ -1973,6 +1984,16 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is-wsl"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
|
||||||
|
dependencies = [
|
||||||
|
"is-docker",
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "is_executable"
|
name = "is_executable"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
@@ -2444,9 +2465,20 @@ version = "0.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "open"
|
||||||
|
version = "5.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "90878fb664448b54c4e592455ad02831e23a3f7e157374a8b95654731aac7349"
|
||||||
|
dependencies = [
|
||||||
|
"is-wsl",
|
||||||
|
"libc",
|
||||||
|
"pathdiff",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openconnect"
|
name = "openconnect"
|
||||||
version = "2.0.0-beta.1"
|
version = "2.0.0-beta7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"is_executable",
|
"is_executable",
|
||||||
@@ -2573,6 +2605,12 @@ version = "1.0.14"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
|
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pathdiff"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.1"
|
version = "2.3.1"
|
||||||
@@ -3070,7 +3108,7 @@ dependencies = [
|
|||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"h2 0.3.22",
|
"h2 0.3.24",
|
||||||
"http 0.2.11",
|
"http 0.2.11",
|
||||||
"http-body 0.4.6",
|
"http-body 0.4.6",
|
||||||
"hyper 0.14.28",
|
"hyper 0.14.28",
|
||||||
@@ -4378,16 +4416,6 @@ version = "2.1.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "users"
|
|
||||||
version = "0.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"log",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf-8"
|
name = "utf-8"
|
||||||
version = "0.7.6"
|
version = "0.7.6"
|
||||||
@@ -4409,6 +4437,16 @@ dependencies = [
|
|||||||
"getrandom 0.2.11",
|
"getrandom 0.2.11",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uzers"
|
||||||
|
version = "0.11.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76d283dc7e8c901e79e32d077866eaf599156cbf427fffa8289aecc52c5c3f63"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "valuable"
|
name = "valuable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@@ -4,7 +4,7 @@ resolver = "2"
|
|||||||
members = ["crates/*", "apps/gpclient", "apps/gpservice", "apps/gpauth"]
|
members = ["crates/*", "apps/gpclient", "apps/gpservice", "apps/gpauth"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "2.0.0-beta.1"
|
version = "2.0.0-beta7"
|
||||||
authors = ["Kevin Yue <k3vinyue@gmail.com>"]
|
authors = ["Kevin Yue <k3vinyue@gmail.com>"]
|
||||||
homepage = "https://github.com/yuezk/GlobalProtect-openconnect"
|
homepage = "https://github.com/yuezk/GlobalProtect-openconnect"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
@@ -36,7 +36,7 @@ futures-util = "0.3"
|
|||||||
tokio-tungstenite = "0.20.1"
|
tokio-tungstenite = "0.20.1"
|
||||||
specta = "=2.0.0-rc.1"
|
specta = "=2.0.0-rc.1"
|
||||||
specta-macros = "=2.0.0-rc.1"
|
specta-macros = "=2.0.0-rc.1"
|
||||||
users = "0.11"
|
uzers = "0.11"
|
||||||
whoami = "1"
|
whoami = "1"
|
||||||
tauri = { version = "1.5" }
|
tauri = { version = "1.5" }
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
|
242
README.md
242
README.md
@@ -1,194 +1,128 @@
|
|||||||
# GlobalProtect-openconnect
|
# GlobalProtect-openconnect
|
||||||
A GlobalProtect VPN client (GUI) for Linux based on Openconnect and built with Qt5, supports SAML auth mode, inspired by [gp-saml-gui](https://github.com/dlenski/gp-saml-gui).
|
|
||||||
|
A GUI for GlobalProtect VPN, based on OpenConnect, supports the SSO authentication method. Inspired by [gp-saml-gui](https://github.com/dlenski/gp-saml-gui).
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://user-images.githubusercontent.com/3297602/133869036-5c02b0d9-c2d9-4f87-8c81-e44f68cfd6ac.png">
|
<img width="300" src="https://github.com/yuezk/GlobalProtect-openconnect/assets/3297602/9242df9c-217d-42ab-8c21-8f9f69cd4eb5">
|
||||||
</p>
|
</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.
|
- [x] Better Linux support
|
||||||
- Supports both SAML and non-SAML authentication modes.
|
- [x] Support both CLI and GUI
|
||||||
- Supports automatically selecting the preferred gateway from the multiple gateways.
|
- [x] Support both SSO and non-SSO authentication
|
||||||
- Supports switching gateway from the system tray menu manually.
|
- [x] Support authentication using default browser
|
||||||
|
- [x] Support multiple portals
|
||||||
|
- [x] Support gateway selection
|
||||||
|
- [x] Support auto-connect on startup
|
||||||
|
- [x] Support system tray icon
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
## Install
|
### CLI
|
||||||
|
|
||||||
|OS|Stable version | Development version|
|
The CLI version is always free and open source in this repo. It has almost the same features as the GUI 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/extra/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)|
|
|
||||||
|
|
||||||
Add the repository in the above table and install it with your favorite package manager tool.
|
```
|
||||||
|
Usage: gpclient [OPTIONS] <COMMAND>
|
||||||
|
|
||||||
[](https://repology.org/project/globalprotect-openconnect/versions)
|
Commands:
|
||||||
[](https://repology.org/project/globalprotect-openconnect/versions)
|
connect Connect to a portal server
|
||||||
[](https://repology.org/project/globalprotect-openconnect/versions)
|
disconnect Disconnect from the server
|
||||||
[](https://repology.org/project/globalprotect-openconnect/versions)
|
launch-gui Launch the GUI
|
||||||
[](https://repology.org/project/globalprotect-openconnect/versions)
|
help Print this message or the help of the given subcommand(s)
|
||||||
[](https://repology.org/project/globalprotect-openconnect/versions)
|
|
||||||
[](https://repology.org/project/globalprotect-openconnect/versions)
|
|
||||||
|
|
||||||
### Linux Mint, Ubuntu 18.04 or later
|
Options:
|
||||||
|
--fix-openssl Get around the OpenSSL `unsafe legacy renegotiation` error
|
||||||
|
--ignore-tls-errors Ignore the TLS errors
|
||||||
|
-h, --help Print help
|
||||||
|
-V, --version Print version
|
||||||
|
|
||||||
```sh
|
See 'gpclient help <command>' for more information on a specific command.
|
||||||
|
```
|
||||||
|
|
||||||
|
### GUI
|
||||||
|
|
||||||
|
The GUI version is also available after you installed it. You can launch it from the application menu or run `gpclient launch-gui` in the terminal.
|
||||||
|
|
||||||
|
> [!Note]
|
||||||
|
>
|
||||||
|
> The GUI version is partially open source. Its background service is open sourced in this repo as [gpservice](./apps/gpservice/). The GUI part is a wrapper of the background service, which is not open sourced.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
> [!Note]
|
||||||
|
>
|
||||||
|
> This instruction is for the 2.x version. The 1.x version is still available on the [1.x](https://github.com/yuezk/GlobalProtect-openconnect/tree/1.x) branch, you can build it from the source code by following the instructions in the `README.md` file.
|
||||||
|
|
||||||
|
> [!Warning]
|
||||||
|
>
|
||||||
|
> The client requires `openconnect >= 8.20`, please make sure you have it installed, you can check it with `openconnect --version`.
|
||||||
|
> Installing the client from PPA will automatically install the required version of `openconnect`.
|
||||||
|
|
||||||
|
### Debian/Ubuntu based distributions
|
||||||
|
|
||||||
|
#### Install from PPA
|
||||||
|
|
||||||
|
```
|
||||||
sudo add-apt-repository ppa:yuezk/globalprotect-openconnect
|
sudo add-apt-repository ppa:yuezk/globalprotect-openconnect
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install globalprotect-openconnect
|
sudo apt-get install globalprotect-openconnect
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> [!Note]
|
||||||
|
>
|
||||||
> For Linux Mint, you might need to import the GPG key with: `sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 7937C393082992E5D6E4A60453FC26B43838D761` if you encountered an error `gpg: keyserver receive failed: General error`.
|
> For Linux Mint, you might need to import the GPG key with: `sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 7937C393082992E5D6E4A60453FC26B43838D761` if you encountered an error `gpg: keyserver receive failed: General error`.
|
||||||
|
|
||||||
|
#### Install from deb package
|
||||||
|
|
||||||
|
Download the latest deb package from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. Then install it with `dpkg`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo dpkg -i globalprotect-openconnect_*.deb
|
||||||
|
```
|
||||||
|
|
||||||
### Arch Linux / Manjaro
|
### Arch Linux / Manjaro
|
||||||
|
|
||||||
```sh
|
#### Install from AUR
|
||||||
sudo pacman -S globalprotect-openconnect
|
|
||||||
|
Install from AUR: [globalprotect-openconnect-git](https://aur.archlinux.org/packages/globalprotect-openconnect-git/)
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### AUR snapshot version
|
|
||||||
|
|
||||||
```sh
|
|
||||||
yay -S globalprotect-openconnect-git
|
yay -S globalprotect-openconnect-git
|
||||||
```
|
```
|
||||||
|
|
||||||
### Fedora
|
#### Install from package
|
||||||
|
|
||||||
```sh
|
Download the latest package from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. Then install it with `pacman`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo pacman -U globalprotect-openconnect-*.pkg.tar.zst
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fedora/OpenSUSE/CentOS/RHEL
|
||||||
|
|
||||||
|
#### Install from COPR
|
||||||
|
|
||||||
|
The package is available on [COPR](https://copr.fedorainfracloud.org/coprs/yuezk/globalprotect-openconnect/) for various RPM-based distributions. You can install it with the following commands:
|
||||||
|
|
||||||
|
```
|
||||||
sudo dnf copr enable yuezk/globalprotect-openconnect
|
sudo dnf copr enable yuezk/globalprotect-openconnect
|
||||||
sudo dnf install globalprotect-openconnect
|
sudo dnf install globalprotect-openconnect
|
||||||
```
|
```
|
||||||
|
|
||||||
### openSUSE
|
#### Install from OBS
|
||||||
|
|
||||||
- openSUSE Tumbleweed
|
The package is also available on [OBS](https://build.opensuse.org/package/show/home:yuezk/globalprotect-openconnect) for various RPM-based distributions. You can follow the instructions [on this page](https://software.opensuse.org//download.html?project=home%3Ayuezk&package=globalprotect-openconnect) to install it.
|
||||||
```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
|
#### Install from RPM package
|
||||||
|
|
||||||
```sh
|
Download the latest RPM package from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page.
|
||||||
sudo zypper ar https://download.opensuse.org/repositories/home:/yuezk/15.4/home:yuezk.repo
|
|
||||||
|
|
||||||
sudo zypper ref
|
### Other distributions
|
||||||
sudo zypper install globalprotect-openconnect
|
|
||||||
```
|
|
||||||
### CentOS 8
|
|
||||||
|
|
||||||
1. Add the repository: `https://download.opensuse.org/repositories/home:/yuezk/CentOS_8/home:yuezk.repo`
|
The project depends on `openconnect >= 8.20`, `webkit2gtk`, `libsecret`, `libayatana-appindicator` or `libappindicator-gtk3`. You can install them first and then download the latest binary release (i.e., `*.bin.tar.gz`) from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page.
|
||||||
1. Install `globalprotect-openconnect`
|
|
||||||
|
|
||||||
|
|
||||||
## Build & Install from source code
|
|
||||||
|
|
||||||
Clone this repo with:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
git clone https://github.com/yuezk/GlobalProtect-openconnect.git
|
|
||||||
cd GlobalProtect-openconnect
|
|
||||||
```
|
|
||||||
|
|
||||||
### MX Linux
|
|
||||||
The following instructions are for **MX-21.2.1_x64 KDE**.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
sudo apt install qttools5-dev libsecret-1-dev libqt5keychain1
|
|
||||||
./scripts/install-debian.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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-get 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
|
|
||||||
- qtkeychain
|
|
||||||
|
|
||||||
...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
|
|
||||||
|
|
||||||
See [Configuration](https://github.com/yuezk/GlobalProtect-openconnect/wiki/Configuration)
|
|
||||||
|
|
||||||
## 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>
|
|
||||||
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
Run `gpclient` in the Terminal and collect the logs.
|
|
||||||
|
|
||||||
## [License](./LICENSE)
|
## [License](./LICENSE)
|
||||||
|
|
||||||
GPLv3
|
GPLv3
|
||||||
|
@@ -8,7 +8,7 @@ license.workspace = true
|
|||||||
tauri-build = { version = "1.5", features = [] }
|
tauri-build = { version = "1.5", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
gpapi = { path = "../../crates/gpapi", features = ["tauri"] }
|
gpapi = { path = "../../crates/gpapi", features = ["tauri", "clap"] }
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
env_logger.workspace = true
|
env_logger.workspace = true
|
||||||
|
@@ -7,6 +7,7 @@ use std::{
|
|||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
use gpapi::{
|
use gpapi::{
|
||||||
auth::SamlAuthData,
|
auth::SamlAuthData,
|
||||||
|
gp_params::GpParams,
|
||||||
portal::{prelogin, Prelogin},
|
portal::{prelogin, Prelogin},
|
||||||
utils::{redact::redact_uri, window::WindowExt},
|
utils::{redact::redact_uri, window::WindowExt},
|
||||||
};
|
};
|
||||||
@@ -18,11 +19,13 @@ use tokio_util::sync::CancellationToken;
|
|||||||
use webkit2gtk::{
|
use webkit2gtk::{
|
||||||
gio::Cancellable,
|
gio::Cancellable,
|
||||||
glib::{GString, TimeSpan},
|
glib::{GString, TimeSpan},
|
||||||
LoadEvent, URIResponse, URIResponseExt, WebContextExt, WebResource, WebResourceExt, WebView,
|
LoadEvent, SettingsExt, TLSErrorsPolicy, URIResponse, URIResponseExt, WebContextExt, WebResource,
|
||||||
WebViewExt, WebsiteDataManagerExtManual, WebsiteDataTypes,
|
WebResourceExt, WebView, WebViewExt, WebsiteDataManagerExtManual, WebsiteDataTypes,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum AuthDataError {
|
enum AuthDataError {
|
||||||
|
/// Failed to load page due to TLS error
|
||||||
|
TlsError,
|
||||||
/// 1. Found auth data in headers/body but it's invalid
|
/// 1. Found auth data in headers/body but it's invalid
|
||||||
/// 2. Loaded an empty page, failed to load page. etc.
|
/// 2. Loaded an empty page, failed to load page. etc.
|
||||||
Invalid,
|
Invalid,
|
||||||
@@ -37,6 +40,7 @@ pub(crate) struct AuthWindow<'a> {
|
|||||||
server: &'a str,
|
server: &'a str,
|
||||||
saml_request: &'a str,
|
saml_request: &'a str,
|
||||||
user_agent: &'a str,
|
user_agent: &'a str,
|
||||||
|
gp_params: Option<GpParams>,
|
||||||
clean: bool,
|
clean: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,6 +51,7 @@ impl<'a> AuthWindow<'a> {
|
|||||||
server: "",
|
server: "",
|
||||||
saml_request: "",
|
saml_request: "",
|
||||||
user_agent: "",
|
user_agent: "",
|
||||||
|
gp_params: None,
|
||||||
clean: false,
|
clean: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,6 +71,11 @@ impl<'a> AuthWindow<'a> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn gp_params(mut self, gp_params: GpParams) -> Self {
|
||||||
|
self.gp_params.replace(gp_params);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn clean(mut self, clean: bool) -> Self {
|
pub fn clean(mut self, clean: bool) -> Self {
|
||||||
self.clean = clean;
|
self.clean = clean;
|
||||||
self
|
self
|
||||||
@@ -76,7 +86,7 @@ impl<'a> AuthWindow<'a> {
|
|||||||
|
|
||||||
let window = Window::builder(&self.app_handle, "auth_window", WindowUrl::default())
|
let window = Window::builder(&self.app_handle, "auth_window", WindowUrl::default())
|
||||||
.title("GlobalProtect Login")
|
.title("GlobalProtect Login")
|
||||||
.user_agent(self.user_agent)
|
// .user_agent(self.user_agent)
|
||||||
.focused(true)
|
.focused(true)
|
||||||
.visible(false)
|
.visible(false)
|
||||||
.center()
|
.center()
|
||||||
@@ -119,6 +129,12 @@ impl<'a> AuthWindow<'a> {
|
|||||||
let saml_request = self.saml_request.to_string();
|
let saml_request = self.saml_request.to_string();
|
||||||
let (auth_result_tx, mut auth_result_rx) = mpsc::unbounded_channel::<AuthResult>();
|
let (auth_result_tx, mut auth_result_rx) = mpsc::unbounded_channel::<AuthResult>();
|
||||||
let raise_window_cancel_token: Arc<RwLock<Option<CancellationToken>>> = Default::default();
|
let raise_window_cancel_token: Arc<RwLock<Option<CancellationToken>>> = Default::default();
|
||||||
|
let gp_params = self.gp_params.as_ref().unwrap();
|
||||||
|
let tls_err_policy = if gp_params.ignore_tls_errors() {
|
||||||
|
TLSErrorsPolicy::Ignore
|
||||||
|
} else {
|
||||||
|
TLSErrorsPolicy::Fail
|
||||||
|
};
|
||||||
|
|
||||||
if self.clean {
|
if self.clean {
|
||||||
clear_webview_cookies(window).await?;
|
clear_webview_cookies(window).await?;
|
||||||
@@ -128,6 +144,15 @@ impl<'a> AuthWindow<'a> {
|
|||||||
window.with_webview(move |wv| {
|
window.with_webview(move |wv| {
|
||||||
let wv = wv.inner();
|
let wv = wv.inner();
|
||||||
|
|
||||||
|
if let Some(context) = wv.context() {
|
||||||
|
context.set_tls_errors_policy(tls_err_policy);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(settings) = wv.settings() {
|
||||||
|
let ua = settings.user_agent().unwrap_or("".into());
|
||||||
|
info!("Auth window user agent: {}", ua);
|
||||||
|
}
|
||||||
|
|
||||||
// Load the initial SAML request
|
// Load the initial SAML request
|
||||||
load_saml_request(&wv, &saml_request);
|
load_saml_request(&wv, &saml_request);
|
||||||
|
|
||||||
@@ -163,31 +188,37 @@ impl<'a> AuthWindow<'a> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
wv.connect_load_failed_with_tls_errors(|_wv, uri, cert, err| {
|
let auth_result_tx_clone = auth_result_tx.clone();
|
||||||
|
wv.connect_load_failed_with_tls_errors(move |_wv, uri, cert, err| {
|
||||||
let redacted_uri = redact_uri(uri);
|
let redacted_uri = redact_uri(uri);
|
||||||
warn!(
|
warn!(
|
||||||
"Failed to load uri: {} with error: {}, cert: {}",
|
"Failed to load uri: {} with error: {}, cert: {}",
|
||||||
redacted_uri, err, cert
|
redacted_uri, err, cert
|
||||||
);
|
);
|
||||||
|
|
||||||
|
send_auth_result(&auth_result_tx_clone, Err(AuthDataError::TlsError));
|
||||||
true
|
true
|
||||||
});
|
});
|
||||||
|
|
||||||
wv.connect_load_failed(move |_wv, _event, uri, err| {
|
wv.connect_load_failed(move |_wv, _event, uri, err| {
|
||||||
let redacted_uri = redact_uri(uri);
|
let redacted_uri = redact_uri(uri);
|
||||||
warn!("Failed to load uri: {} with error: {}", redacted_uri, err);
|
warn!("Failed to load uri: {} with error: {}", redacted_uri, err);
|
||||||
send_auth_result(&auth_result_tx, Err(AuthDataError::Invalid));
|
// NOTE: Don't send error here, since load_changed event will be triggered after this
|
||||||
|
// send_auth_result(&auth_result_tx, Err(AuthDataError::Invalid));
|
||||||
// true to stop other handlers from being invoked for the event. false to propagate the event further.
|
// true to stop other handlers from being invoked for the event. false to propagate the event further.
|
||||||
true
|
true
|
||||||
});
|
});
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let portal = self.server.to_string();
|
let portal = self.server.to_string();
|
||||||
let user_agent = self.user_agent.to_string();
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if let Some(auth_result) = auth_result_rx.recv().await {
|
if let Some(auth_result) = auth_result_rx.recv().await {
|
||||||
match auth_result {
|
match auth_result {
|
||||||
Ok(auth_data) => return Ok(auth_data),
|
Ok(auth_data) => return Ok(auth_data),
|
||||||
|
Err(AuthDataError::TlsError) => {
|
||||||
|
return Err(anyhow::anyhow!("TLS error: certificate verify failed"))
|
||||||
|
}
|
||||||
Err(AuthDataError::NotFound) => {
|
Err(AuthDataError::NotFound) => {
|
||||||
info!("No auth data found, it may not be the /SAML20/SP/ACS endpoint");
|
info!("No auth data found, it may not be the /SAML20/SP/ACS endpoint");
|
||||||
|
|
||||||
@@ -232,7 +263,7 @@ impl<'a> AuthWindow<'a> {
|
|||||||
);
|
);
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let saml_request = portal_prelogin(&portal, &user_agent).await?;
|
let saml_request = portal_prelogin(&portal, gp_params).await?;
|
||||||
window.with_webview(move |wv| {
|
window.with_webview(move |wv| {
|
||||||
let wv = wv.inner();
|
let wv = wv.inner();
|
||||||
load_saml_request(&wv, &saml_request);
|
load_saml_request(&wv, &saml_request);
|
||||||
@@ -253,9 +284,10 @@ fn raise_window(window: &Arc<Window>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn portal_prelogin(portal: &str, user_agent: &str) -> anyhow::Result<String> {
|
pub(crate) async fn portal_prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<String> {
|
||||||
info!("Portal prelogin...");
|
info!("Portal prelogin...");
|
||||||
match prelogin(portal, user_agent).await? {
|
|
||||||
|
match prelogin(portal, gp_params).await? {
|
||||||
Prelogin::Saml(prelogin) => Ok(prelogin.saml_request().to_string()),
|
Prelogin::Saml(prelogin) => Ok(prelogin.saml_request().to_string()),
|
||||||
Prelogin::Standard(_) => Err(anyhow::anyhow!("Received non-SAML prelogin response")),
|
Prelogin::Standard(_) => Err(anyhow::anyhow!("Received non-SAML prelogin response")),
|
||||||
}
|
}
|
||||||
@@ -392,6 +424,11 @@ fn read_auth_data(main_resource: &WebResource, auth_result_tx: mpsc::UnboundedSe
|
|||||||
send_auth_result(&auth_result_tx, auth_result)
|
send_auth_result(&auth_result_tx, auth_result)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Err(AuthDataError::TlsError) => {
|
||||||
|
// NOTE: This is unreachable
|
||||||
|
info!("TLS error found in headers, trying to read from body...");
|
||||||
|
send_auth_result(&auth_result_tx, Err(AuthDataError::TlsError));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use gpapi::{
|
use gpapi::{
|
||||||
auth::{SamlAuthData, SamlAuthResult},
|
auth::{SamlAuthData, SamlAuthResult},
|
||||||
|
clap::args::Os,
|
||||||
|
gp_params::{ClientOs, GpParams},
|
||||||
utils::{normalize_server, openssl},
|
utils::{normalize_server, openssl},
|
||||||
GP_USER_AGENT,
|
GP_USER_AGENT,
|
||||||
};
|
};
|
||||||
@@ -26,23 +28,35 @@ struct Cli {
|
|||||||
saml_request: Option<String>,
|
saml_request: Option<String>,
|
||||||
#[arg(long, default_value = GP_USER_AGENT)]
|
#[arg(long, default_value = GP_USER_AGENT)]
|
||||||
user_agent: String,
|
user_agent: String,
|
||||||
|
#[arg(long, default_value = "Linux")]
|
||||||
|
os: Os,
|
||||||
|
#[arg(long)]
|
||||||
|
os_version: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
hidpi: bool,
|
hidpi: bool,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
fix_openssl: bool,
|
fix_openssl: bool,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
|
ignore_tls_errors: bool,
|
||||||
|
#[arg(long)]
|
||||||
clean: bool,
|
clean: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cli {
|
impl Cli {
|
||||||
async fn run(&mut self) -> anyhow::Result<()> {
|
async fn run(&mut self) -> anyhow::Result<()> {
|
||||||
|
if self.ignore_tls_errors {
|
||||||
|
info!("TLS errors will be ignored");
|
||||||
|
}
|
||||||
|
|
||||||
let mut openssl_conf = self.prepare_env()?;
|
let mut openssl_conf = self.prepare_env()?;
|
||||||
|
|
||||||
self.server = normalize_server(&self.server)?;
|
self.server = normalize_server(&self.server)?;
|
||||||
|
let gp_params = self.build_gp_params();
|
||||||
|
|
||||||
// Get the initial SAML request
|
// Get the initial SAML request
|
||||||
let saml_request = match self.saml_request {
|
let saml_request = match self.saml_request {
|
||||||
Some(ref saml_request) => saml_request.clone(),
|
Some(ref saml_request) => saml_request.clone(),
|
||||||
None => portal_prelogin(&self.server, &self.user_agent).await?,
|
None => portal_prelogin(&self.server, &gp_params).await?,
|
||||||
};
|
};
|
||||||
|
|
||||||
self.saml_request.replace(saml_request);
|
self.saml_request.replace(saml_request);
|
||||||
@@ -82,10 +96,22 @@ impl Cli {
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_gp_params(&self) -> GpParams {
|
||||||
|
let gp_params = GpParams::builder()
|
||||||
|
.user_agent(&self.user_agent)
|
||||||
|
.client_os(ClientOs::from(&self.os))
|
||||||
|
.os_version(self.os_version.clone())
|
||||||
|
.ignore_tls_errors(self.ignore_tls_errors)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
gp_params
|
||||||
|
}
|
||||||
|
|
||||||
async fn saml_auth(&self, app_handle: AppHandle) -> anyhow::Result<SamlAuthData> {
|
async fn saml_auth(&self, app_handle: AppHandle) -> anyhow::Result<SamlAuthData> {
|
||||||
let auth_window = AuthWindow::new(app_handle)
|
let auth_window = AuthWindow::new(app_handle)
|
||||||
.server(&self.server)
|
.server(&self.server)
|
||||||
.user_agent(&self.user_agent)
|
.user_agent(&self.user_agent)
|
||||||
|
.gp_params(self.build_gp_params())
|
||||||
.saml_request(self.saml_request.as_ref().unwrap())
|
.saml_request(self.saml_request.as_ref().unwrap())
|
||||||
.clean(self.clean);
|
.clean(self.clean);
|
||||||
|
|
||||||
|
@@ -6,7 +6,7 @@ edition.workspace = true
|
|||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
gpapi = { path = "../../crates/gpapi" }
|
gpapi = { path = "../../crates/gpapi", features = ["clap"] }
|
||||||
openconnect = { path = "../../crates/openconnect" }
|
openconnect = { path = "../../crates/openconnect" }
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
|
@@ -16,6 +16,11 @@ const VERSION: &str = concat!(
|
|||||||
")"
|
")"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
pub(crate) struct SharedArgs {
|
||||||
|
pub(crate) fix_openssl: bool,
|
||||||
|
pub(crate) ignore_tls_errors: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum CliCommand {
|
enum CliCommand {
|
||||||
#[command(about = "Connect to a portal server")]
|
#[command(about = "Connect to a portal server")]
|
||||||
@@ -40,6 +45,8 @@ enum CliCommand {
|
|||||||
{usage-heading} {usage}
|
{usage-heading} {usage}
|
||||||
|
|
||||||
{all-args}{after-help}
|
{all-args}{after-help}
|
||||||
|
|
||||||
|
See 'gpclient help <command>' for more information on a specific command.
|
||||||
"
|
"
|
||||||
)]
|
)]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
@@ -51,6 +58,8 @@ struct Cli {
|
|||||||
help = "Get around the OpenSSL `unsafe legacy renegotiation` error"
|
help = "Get around the OpenSSL `unsafe legacy renegotiation` error"
|
||||||
)]
|
)]
|
||||||
fix_openssl: bool,
|
fix_openssl: bool,
|
||||||
|
#[arg(long, help = "Ignore the TLS errors")]
|
||||||
|
ignore_tls_errors: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cli {
|
impl Cli {
|
||||||
@@ -67,9 +76,17 @@ impl Cli {
|
|||||||
// The temp file will be dropped automatically when the file handle is dropped
|
// The temp file will be dropped automatically when the file handle is dropped
|
||||||
// So, declare it here to ensure it's not dropped
|
// So, declare it here to ensure it's not dropped
|
||||||
let _file = self.fix_openssl()?;
|
let _file = self.fix_openssl()?;
|
||||||
|
let shared_args = SharedArgs {
|
||||||
|
fix_openssl: self.fix_openssl,
|
||||||
|
ignore_tls_errors: self.ignore_tls_errors,
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.ignore_tls_errors {
|
||||||
|
info!("TLS errors will be ignored");
|
||||||
|
}
|
||||||
|
|
||||||
match &self.command {
|
match &self.command {
|
||||||
CliCommand::Connect(args) => ConnectHandler::new(args, self.fix_openssl).handle().await,
|
CliCommand::Connect(args) => ConnectHandler::new(args, &shared_args).handle().await,
|
||||||
CliCommand::Disconnect => DisconnectHandler::new().handle(),
|
CliCommand::Disconnect => DisconnectHandler::new().handle(),
|
||||||
CliCommand::LaunchGui(args) => LaunchGuiHandler::new(args).handle().await,
|
CliCommand::LaunchGui(args) => LaunchGuiHandler::new(args).handle().await,
|
||||||
}
|
}
|
||||||
@@ -89,13 +106,24 @@ pub(crate) async fn run() {
|
|||||||
if let Err(err) = cli.run().await {
|
if let Err(err) = cli.run().await {
|
||||||
eprintln!("\nError: {}", err);
|
eprintln!("\nError: {}", err);
|
||||||
|
|
||||||
if err.to_string().contains("unsafe legacy renegotiation") && !cli.fix_openssl {
|
let err = err.to_string();
|
||||||
|
|
||||||
|
if err.contains("unsafe legacy renegotiation") && !cli.fix_openssl {
|
||||||
eprintln!("\nRe-run it with the `--fix-openssl` option to work around this issue, e.g.:\n");
|
eprintln!("\nRe-run it with the `--fix-openssl` option to work around this issue, e.g.:\n");
|
||||||
// Print the command
|
// Print the command
|
||||||
let args = std::env::args().collect::<Vec<_>>();
|
let args = std::env::args().collect::<Vec<_>>();
|
||||||
eprintln!("{} --fix-openssl {}\n", args[0], args[1..].join(" "));
|
eprintln!("{} --fix-openssl {}\n", args[0], args[1..].join(" "));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err.contains("certificate verify failed") && !cli.ignore_tls_errors {
|
||||||
|
eprintln!(
|
||||||
|
"\nRe-run it with the `--ignore-tls-errors` option to ignore the certificate error, e.g.:\n"
|
||||||
|
);
|
||||||
|
// Print the command
|
||||||
|
let args = std::env::args().collect::<Vec<_>>();
|
||||||
|
eprintln!("{} --ignore-tls-errors {}\n", args[0], args[1..].join(" "));
|
||||||
|
}
|
||||||
|
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,9 +2,10 @@ use std::{fs, sync::Arc};
|
|||||||
|
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
use gpapi::{
|
use gpapi::{
|
||||||
|
clap::args::Os,
|
||||||
credential::{Credential, PasswordCredential},
|
credential::{Credential, PasswordCredential},
|
||||||
gateway::gateway_login,
|
gateway::gateway_login,
|
||||||
gp_params::GpParams,
|
gp_params::{ClientOs, GpParams},
|
||||||
portal::{prelogin, retrieve_config, Prelogin},
|
portal::{prelogin, retrieve_config, Prelogin},
|
||||||
process::auth_launcher::SamlAuthLauncher,
|
process::auth_launcher::SamlAuthLauncher,
|
||||||
utils::{self, shutdown_signal},
|
utils::{self, shutdown_signal},
|
||||||
@@ -14,7 +15,7 @@ use inquire::{Password, PasswordDisplayMode, Select, Text};
|
|||||||
use log::info;
|
use log::info;
|
||||||
use openconnect::Vpn;
|
use openconnect::Vpn;
|
||||||
|
|
||||||
use crate::GP_CLIENT_LOCK_FILE;
|
use crate::{cli::SharedArgs, GP_CLIENT_LOCK_FILE};
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
pub(crate) struct ConnectArgs {
|
pub(crate) struct ConnectArgs {
|
||||||
@@ -36,31 +37,55 @@ pub(crate) struct ConnectArgs {
|
|||||||
script: Option<String>,
|
script: Option<String>,
|
||||||
#[arg(long, default_value = GP_USER_AGENT, help = "The user agent to use")]
|
#[arg(long, default_value = GP_USER_AGENT, help = "The user agent to use")]
|
||||||
user_agent: String,
|
user_agent: String,
|
||||||
|
#[arg(long, default_value = "Linux")]
|
||||||
|
os: Os,
|
||||||
|
#[arg(long)]
|
||||||
|
os_version: Option<String>,
|
||||||
#[arg(long, help = "The HiDPI mode, useful for high resolution screens")]
|
#[arg(long, help = "The HiDPI mode, useful for high resolution screens")]
|
||||||
hidpi: bool,
|
hidpi: bool,
|
||||||
#[arg(long, help = "Do not reuse the remembered authentication cookie")]
|
#[arg(long, help = "Do not reuse the remembered authentication cookie")]
|
||||||
clean: bool,
|
clean: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ConnectArgs {
|
||||||
|
fn os_version(&self) -> String {
|
||||||
|
if let Some(os_version) = &self.os_version {
|
||||||
|
return os_version.to_owned();
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.os {
|
||||||
|
Os::Linux => format!("Linux {}", whoami::distro()),
|
||||||
|
Os::Windows => String::from("Microsoft Windows 11 Pro , 64-bit"),
|
||||||
|
Os::Mac => String::from("Apple Mac OS X 13.4.0"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) struct ConnectHandler<'a> {
|
pub(crate) struct ConnectHandler<'a> {
|
||||||
args: &'a ConnectArgs,
|
args: &'a ConnectArgs,
|
||||||
fix_openssl: bool,
|
shared_args: &'a SharedArgs,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> ConnectHandler<'a> {
|
impl<'a> ConnectHandler<'a> {
|
||||||
pub(crate) fn new(args: &'a ConnectArgs, fix_openssl: bool) -> Self {
|
pub(crate) fn new(args: &'a ConnectArgs, shared_args: &'a SharedArgs) -> Self {
|
||||||
Self { args, fix_openssl }
|
Self { args, shared_args }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_gp_params(&self) -> GpParams {
|
||||||
|
GpParams::builder()
|
||||||
|
.user_agent(&self.args.user_agent)
|
||||||
|
.client_os(ClientOs::from(&self.args.os))
|
||||||
|
.os_version(self.args.os_version())
|
||||||
|
.ignore_tls_errors(self.shared_args.ignore_tls_errors)
|
||||||
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn handle(&self) -> anyhow::Result<()> {
|
pub(crate) async fn handle(&self) -> anyhow::Result<()> {
|
||||||
let portal = utils::normalize_server(self.args.server.as_str())?;
|
let portal = utils::normalize_server(self.args.server.as_str())?;
|
||||||
|
let gp_params = self.build_gp_params();
|
||||||
|
|
||||||
let gp_params = GpParams::builder()
|
let prelogin = prelogin(&portal, &gp_params).await?;
|
||||||
.user_agent(&self.args.user_agent)
|
let portal_credential = self.obtain_credential(&prelogin).await?;
|
||||||
.build();
|
|
||||||
|
|
||||||
let prelogin = prelogin(&portal, &self.args.user_agent).await?;
|
|
||||||
let portal_credential = self.obtain_portal_credential(&prelogin).await?;
|
|
||||||
let mut portal_config = retrieve_config(&portal, &portal_credential, &gp_params).await?;
|
let mut portal_config = retrieve_config(&portal, &portal_credential, &gp_params).await?;
|
||||||
|
|
||||||
let selected_gateway = match &self.args.gateway {
|
let selected_gateway = match &self.args.gateway {
|
||||||
@@ -83,7 +108,14 @@ impl<'a> ConnectHandler<'a> {
|
|||||||
|
|
||||||
let gateway = selected_gateway.server();
|
let gateway = selected_gateway.server();
|
||||||
let cred = portal_config.auth_cookie().into();
|
let cred = portal_config.auth_cookie().into();
|
||||||
let token = gateway_login(gateway, &cred, &gp_params).await?;
|
|
||||||
|
let token = match gateway_login(gateway, &cred, &gp_params).await {
|
||||||
|
Ok(token) => token,
|
||||||
|
Err(_) => {
|
||||||
|
info!("Gateway login failed, retrying with prelogin");
|
||||||
|
self.gateway_login_with_prelogin(gateway).await?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let vpn = Vpn::builder(gateway, &token)
|
let vpn = Vpn::builder(gateway, &token)
|
||||||
.user_agent(self.args.user_agent.clone())
|
.user_agent(self.args.user_agent.clone())
|
||||||
@@ -110,14 +142,27 @@ impl<'a> ConnectHandler<'a> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn obtain_portal_credential(&self, prelogin: &Prelogin) -> anyhow::Result<Credential> {
|
async fn gateway_login_with_prelogin(&self, gateway: &str) -> anyhow::Result<String> {
|
||||||
|
let mut gp_params = self.build_gp_params();
|
||||||
|
gp_params.set_is_gateway(true);
|
||||||
|
|
||||||
|
let prelogin = prelogin(gateway, &gp_params).await?;
|
||||||
|
let cred = self.obtain_credential(&prelogin).await?;
|
||||||
|
|
||||||
|
gateway_login(gateway, &cred, &gp_params).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn obtain_credential(&self, prelogin: &Prelogin) -> anyhow::Result<Credential> {
|
||||||
match prelogin {
|
match prelogin {
|
||||||
Prelogin::Saml(prelogin) => {
|
Prelogin::Saml(prelogin) => {
|
||||||
SamlAuthLauncher::new(&self.args.server)
|
SamlAuthLauncher::new(&self.args.server)
|
||||||
.user_agent(&self.args.user_agent)
|
|
||||||
.saml_request(prelogin.saml_request())
|
.saml_request(prelogin.saml_request())
|
||||||
|
.user_agent(&self.args.user_agent)
|
||||||
|
.os(self.args.os.as_str())
|
||||||
|
.os_version(Some(&self.args.os_version()))
|
||||||
.hidpi(self.args.hidpi)
|
.hidpi(self.args.hidpi)
|
||||||
.fix_openssl(self.fix_openssl)
|
.fix_openssl(self.shared_args.fix_openssl)
|
||||||
|
.ignore_tls_errors(self.shared_args.ignore_tls_errors)
|
||||||
.clean(self.args.clean)
|
.clean(self.args.clean)
|
||||||
.launch()
|
.launch()
|
||||||
.await
|
.await
|
||||||
|
@@ -10,7 +10,12 @@ use log::info;
|
|||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
pub(crate) struct LaunchGuiArgs {
|
pub(crate) struct LaunchGuiArgs {
|
||||||
#[clap(long, help = "Launch the GUI minimized")]
|
#[arg(
|
||||||
|
required = false,
|
||||||
|
help = "The authentication data, used for the default browser authentication"
|
||||||
|
)]
|
||||||
|
auth_data: Option<String>,
|
||||||
|
#[arg(long, help = "Launch the GUI minimized")]
|
||||||
minimized: bool,
|
minimized: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,6 +35,12 @@ impl<'a> LaunchGuiHandler<'a> {
|
|||||||
anyhow::bail!("`launch-gui` cannot be run as root");
|
anyhow::bail!("`launch-gui` cannot be run as root");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let auth_data = self.args.auth_data.as_deref().unwrap_or_default();
|
||||||
|
if !auth_data.is_empty() {
|
||||||
|
// Process the authentication data, its format is `globalprotectcallback:<data>`
|
||||||
|
return feed_auth_data(auth_data).await;
|
||||||
|
}
|
||||||
|
|
||||||
if try_active_gui().await.is_ok() {
|
if try_active_gui().await.is_ok() {
|
||||||
info!("The GUI is already running");
|
info!("The GUI is already running");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -66,6 +77,19 @@ impl<'a> LaunchGuiHandler<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn feed_auth_data(auth_data: &str) -> anyhow::Result<()> {
|
||||||
|
let service_endpoint = http_endpoint().await?;
|
||||||
|
|
||||||
|
reqwest::Client::default()
|
||||||
|
.post(format!("{}/auth-data", service_endpoint))
|
||||||
|
.json(&auth_data)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn try_active_gui() -> anyhow::Result<()> {
|
async fn try_active_gui() -> anyhow::Result<()> {
|
||||||
let service_endpoint = http_endpoint().await?;
|
let service_endpoint = http_endpoint().await?;
|
||||||
|
|
||||||
|
@@ -1,19 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
|
|
||||||
<policyconfig>
|
|
||||||
<vendor>GlobalProtect-openconnect</vendor>
|
|
||||||
<vendor_url>https://github.com/yuezk/GlobalProtect-openconnect</vendor_url>
|
|
||||||
<icon_name>gpgui</icon_name>
|
|
||||||
<action id="com.yuezk.gpservice">
|
|
||||||
<description>Run GPService as root</description>
|
|
||||||
<message>Authentication is required to run the GPService as root</message>
|
|
||||||
<defaults>
|
|
||||||
<allow_any>yes</allow_any>
|
|
||||||
<allow_inactive>yes</allow_inactive>
|
|
||||||
<allow_active>yes</allow_active>
|
|
||||||
</defaults>
|
|
||||||
<annotate key="org.freedesktop.policykit.exec.path">/home/kevin/Documents/repos/gp/target/debug/gpservice</annotate>
|
|
||||||
<annotate key="org.freedesktop.policykit.exec.argv1">--with-gui</annotate>
|
|
||||||
<annotate key="org.freedesktop.policykit.exec.allow_gui">true</annotate>
|
|
||||||
</action>
|
|
||||||
</policyconfig>
|
|
@@ -21,6 +21,13 @@ pub(crate) async fn active_gui(State(ctx): State<Arc<WsServerContext>>) -> impl
|
|||||||
ctx.send_event(WsEvent::ActiveGui).await;
|
ctx.send_event(WsEvent::ActiveGui).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn auth_data(
|
||||||
|
State(ctx): State<Arc<WsServerContext>>,
|
||||||
|
body: String,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
ctx.send_event(WsEvent::AuthData(body)).await;
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn ws_handler(
|
pub(crate) async fn ws_handler(
|
||||||
ws: WebSocketUpgrade,
|
ws: WebSocketUpgrade,
|
||||||
State(ctx): State<Arc<WsServerContext>>,
|
State(ctx): State<Arc<WsServerContext>>,
|
||||||
|
@@ -8,6 +8,7 @@ pub(crate) fn routes(ctx: Arc<WsServerContext>) -> Router {
|
|||||||
Router::new()
|
Router::new()
|
||||||
.route("/health", get(handlers::health))
|
.route("/health", get(handlers::health))
|
||||||
.route("/active-gui", post(handlers::active_gui))
|
.route("/active-gui", post(handlers::active_gui))
|
||||||
|
.route("/auth-data", post(handlers::auth_data))
|
||||||
.route("/ws", get(handlers::ws_handler))
|
.route("/ws", get(handlers::ws_handler))
|
||||||
.with_state(ctx)
|
.with_state(ctx)
|
||||||
}
|
}
|
||||||
|
@@ -24,9 +24,13 @@ redact-engine.workspace = true
|
|||||||
url.workspace = true
|
url.workspace = true
|
||||||
regex.workspace = true
|
regex.workspace = true
|
||||||
dotenvy_macro.workspace = true
|
dotenvy_macro.workspace = true
|
||||||
users.workspace = true
|
uzers.workspace = true
|
||||||
|
|
||||||
tauri = { workspace = true, optional = true }
|
tauri = { workspace = true, optional = true }
|
||||||
|
clap = { workspace = true, optional = true }
|
||||||
|
open = { version = "5", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
tauri = ["dep:tauri"]
|
tauri = ["dep:tauri"]
|
||||||
|
clap = ["dep:clap"]
|
||||||
|
browser-auth = ["dep:open"]
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
use anyhow::bail;
|
||||||
|
use regex::Regex;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
@@ -37,6 +39,32 @@ impl SamlAuthData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn parse_html(html: &str) -> anyhow::Result<SamlAuthData> {
|
||||||
|
match parse_xml_tag(html, "saml-auth-status") {
|
||||||
|
Some(saml_status) if saml_status == "1" => {
|
||||||
|
let username = parse_xml_tag(html, "saml-username");
|
||||||
|
let prelogin_cookie = parse_xml_tag(html, "prelogin-cookie");
|
||||||
|
let portal_userauthcookie = parse_xml_tag(html, "portal-userauthcookie");
|
||||||
|
|
||||||
|
if SamlAuthData::check(&username, &prelogin_cookie, &portal_userauthcookie) {
|
||||||
|
return Ok(SamlAuthData::new(
|
||||||
|
username.unwrap(),
|
||||||
|
prelogin_cookie,
|
||||||
|
portal_userauthcookie,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
bail!("Found invalid auth data in HTML");
|
||||||
|
}
|
||||||
|
Some(status) => {
|
||||||
|
bail!("Found invalid SAML status {} in HTML", status);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
bail!("No auth data found in HTML");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn username(&self) -> &str {
|
pub fn username(&self) -> &str {
|
||||||
&self.username
|
&self.username
|
||||||
}
|
}
|
||||||
@@ -61,3 +89,10 @@ impl SamlAuthData {
|
|||||||
username_valid && (prelogin_cookie_valid || portal_userauthcookie_valid)
|
username_valid && (prelogin_cookie_valid || portal_userauthcookie_valid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn parse_xml_tag(html: &str, tag: &str) -> Option<String> {
|
||||||
|
let re = Regex::new(&format!("<{}>(.*)</{}>", tag, tag)).unwrap();
|
||||||
|
re.captures(html)
|
||||||
|
.and_then(|captures| captures.get(1))
|
||||||
|
.map(|m| m.as_str().to_string())
|
||||||
|
}
|
||||||
|
64
crates/gpapi/src/clap/args.rs
Normal file
64
crates/gpapi/src/clap/args.rs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
use clap::{builder::PossibleValue, ValueEnum};
|
||||||
|
|
||||||
|
use crate::gp_params::ClientOs;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Os {
|
||||||
|
Linux,
|
||||||
|
Windows,
|
||||||
|
Mac,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Os {
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Os::Linux => "Linux",
|
||||||
|
Os::Windows => "Windows",
|
||||||
|
Os::Mac => "Mac",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for Os {
|
||||||
|
fn from(os: &str) -> Self {
|
||||||
|
match os.to_lowercase().as_str() {
|
||||||
|
"linux" => Os::Linux,
|
||||||
|
"windows" => Os::Windows,
|
||||||
|
"mac" => Os::Mac,
|
||||||
|
_ => Os::Linux,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Os> for ClientOs {
|
||||||
|
fn from(value: &Os) -> Self {
|
||||||
|
match value {
|
||||||
|
Os::Linux => ClientOs::Linux,
|
||||||
|
Os::Windows => ClientOs::Windows,
|
||||||
|
Os::Mac => ClientOs::Mac,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValueEnum for Os {
|
||||||
|
fn value_variants<'a>() -> &'a [Self] {
|
||||||
|
&[Os::Linux, Os::Windows, Os::Mac]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
|
||||||
|
match self {
|
||||||
|
Os::Linux => Some(PossibleValue::new("Linux")),
|
||||||
|
Os::Windows => Some(PossibleValue::new("Windows")),
|
||||||
|
Os::Mac => Some(PossibleValue::new("Mac")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_str(input: &str, _: bool) -> Result<Self, String> {
|
||||||
|
match input.to_lowercase().as_str() {
|
||||||
|
"linux" => Ok(Os::Linux),
|
||||||
|
"windows" => Ok(Os::Windows),
|
||||||
|
"mac" => Ok(Os::Mac),
|
||||||
|
_ => Err(format!("Invalid OS: {}", input)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
crates/gpapi/src/clap/mod.rs
Normal file
1
crates/gpapi/src/clap/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod args;
|
@@ -3,7 +3,7 @@ use std::collections::HashMap;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use specta::Type;
|
use specta::Type;
|
||||||
|
|
||||||
use crate::auth::SamlAuthData;
|
use crate::{auth::SamlAuthData, utils::base64::decode_to_string};
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Type, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Type, Clone)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@@ -151,6 +151,17 @@ pub enum Credential {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Credential {
|
impl Credential {
|
||||||
|
/// Create a credential from a globalprotectcallback:<base64 encoded string>
|
||||||
|
pub fn parse_gpcallback(auth_data: &str) -> anyhow::Result<Self> {
|
||||||
|
// Remove the surrounding quotes
|
||||||
|
let auth_data = auth_data.trim_matches('"');
|
||||||
|
let auth_data = auth_data.trim_start_matches("globalprotectcallback:");
|
||||||
|
let auth_data = decode_to_string(auth_data)?;
|
||||||
|
let auth_data = SamlAuthData::parse_html(&auth_data)?;
|
||||||
|
|
||||||
|
Self::try_from(auth_data)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn username(&self) -> &str {
|
pub fn username(&self) -> &str {
|
||||||
match self {
|
match self {
|
||||||
Credential::Password(cred) => cred.username(),
|
Credential::Password(cred) => cred.username(),
|
||||||
@@ -164,31 +175,34 @@ impl Credential {
|
|||||||
let mut params = HashMap::new();
|
let mut params = HashMap::new();
|
||||||
params.insert("user", self.username());
|
params.insert("user", self.username());
|
||||||
|
|
||||||
match self {
|
let (passwd, prelogin_cookie, portal_userauthcookie, portal_prelogonuserauthcookie) = match self
|
||||||
Credential::Password(cred) => {
|
{
|
||||||
params.insert("passwd", cred.password());
|
Credential::Password(cred) => (Some(cred.password()), None, None, None),
|
||||||
}
|
Credential::PreloginCookie(cred) => (None, Some(cred.prelogin_cookie()), None, None),
|
||||||
Credential::PreloginCookie(cred) => {
|
Credential::AuthCookie(cred) => (
|
||||||
params.insert("prelogin-cookie", cred.prelogin_cookie());
|
None,
|
||||||
}
|
None,
|
||||||
Credential::AuthCookie(cred) => {
|
Some(cred.user_auth_cookie()),
|
||||||
params.insert("portal-userauthcookie", cred.user_auth_cookie());
|
Some(cred.prelogon_user_auth_cookie()),
|
||||||
params.insert(
|
),
|
||||||
"portal-prelogonuserauthcookie",
|
Credential::CachedCredential(cred) => (
|
||||||
cred.prelogon_user_auth_cookie(),
|
cred.password(),
|
||||||
);
|
None,
|
||||||
}
|
Some(cred.auth_cookie.user_auth_cookie()),
|
||||||
Credential::CachedCredential(cred) => {
|
Some(cred.auth_cookie.prelogon_user_auth_cookie()),
|
||||||
if let Some(password) = cred.password() {
|
),
|
||||||
params.insert("passwd", password);
|
};
|
||||||
}
|
|
||||||
params.insert("portal-userauthcookie", cred.auth_cookie.user_auth_cookie());
|
params.insert("passwd", passwd.unwrap_or_default());
|
||||||
params.insert(
|
params.insert("prelogin-cookie", prelogin_cookie.unwrap_or_default());
|
||||||
"portal-prelogonuserauthcookie",
|
params.insert(
|
||||||
cred.auth_cookie.prelogon_user_auth_cookie(),
|
"portal-userauthcookie",
|
||||||
);
|
portal_userauthcookie.unwrap_or_default(),
|
||||||
}
|
);
|
||||||
}
|
params.insert(
|
||||||
|
"portal-prelogonuserauthcookie",
|
||||||
|
portal_prelogonuserauthcookie.unwrap_or_default(),
|
||||||
|
);
|
||||||
|
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
|
@@ -12,6 +12,7 @@ pub async fn gateway_login(
|
|||||||
) -> anyhow::Result<String> {
|
) -> anyhow::Result<String> {
|
||||||
let login_url = format!("https://{}/ssl-vpn/login.esp", gateway);
|
let login_url = format!("https://{}/ssl-vpn/login.esp", gateway);
|
||||||
let client = Client::builder()
|
let client = Client::builder()
|
||||||
|
.danger_accept_invalid_certs(gp_params.ignore_tls_errors())
|
||||||
.user_agent(gp_params.user_agent())
|
.user_agent(gp_params.user_agent())
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
@@ -23,14 +24,8 @@ pub async fn gateway_login(
|
|||||||
|
|
||||||
info!("Gateway login, user_agent: {}", gp_params.user_agent());
|
info!("Gateway login, user_agent: {}", gp_params.user_agent());
|
||||||
|
|
||||||
let res_xml = client
|
let res = client.post(&login_url).form(¶ms).send().await?;
|
||||||
.post(&login_url)
|
let res_xml = res.error_for_status()?.text().await?;
|
||||||
.form(¶ms)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.error_for_status()?
|
|
||||||
.text()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let doc = Document::parse(&res_xml)?;
|
let doc = Document::parse(&res_xml)?;
|
||||||
|
|
||||||
|
@@ -7,23 +7,32 @@ use crate::GP_USER_AGENT;
|
|||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, Type, Default)]
|
#[derive(Debug, Serialize, Deserialize, Clone, Type, Default)]
|
||||||
pub enum ClientOs {
|
pub enum ClientOs {
|
||||||
Linux,
|
|
||||||
#[default]
|
#[default]
|
||||||
|
Linux,
|
||||||
Windows,
|
Windows,
|
||||||
Mac,
|
Mac,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&ClientOs> for &str {
|
impl From<&str> for ClientOs {
|
||||||
fn from(os: &ClientOs) -> Self {
|
fn from(os: &str) -> Self {
|
||||||
match os {
|
match os {
|
||||||
ClientOs::Linux => "Linux",
|
"Linux" => ClientOs::Linux,
|
||||||
ClientOs::Windows => "Windows",
|
"Windows" => ClientOs::Windows,
|
||||||
ClientOs::Mac => "Mac",
|
"Mac" => ClientOs::Mac,
|
||||||
|
_ => ClientOs::Linux,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClientOs {
|
impl ClientOs {
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
ClientOs::Linux => "Linux",
|
||||||
|
ClientOs::Windows => "Windows",
|
||||||
|
ClientOs::Mac => "Mac",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn to_openconnect_os(&self) -> &str {
|
pub fn to_openconnect_os(&self) -> &str {
|
||||||
match self {
|
match self {
|
||||||
ClientOs::Linux => "linux",
|
ClientOs::Linux => "linux",
|
||||||
@@ -35,11 +44,14 @@ impl ClientOs {
|
|||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Type, Default)]
|
#[derive(Debug, Serialize, Deserialize, Type, Default)]
|
||||||
pub struct GpParams {
|
pub struct GpParams {
|
||||||
|
is_gateway: bool,
|
||||||
user_agent: String,
|
user_agent: String,
|
||||||
client_os: ClientOs,
|
client_os: ClientOs,
|
||||||
os_version: Option<String>,
|
os_version: Option<String>,
|
||||||
client_version: Option<String>,
|
client_version: Option<String>,
|
||||||
computer: Option<String>,
|
computer: String,
|
||||||
|
ignore_tls_errors: bool,
|
||||||
|
prefer_default_browser: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GpParams {
|
impl GpParams {
|
||||||
@@ -47,20 +59,33 @@ impl GpParams {
|
|||||||
GpParamsBuilder::new()
|
GpParamsBuilder::new()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_gateway(&self) -> bool {
|
||||||
|
self.is_gateway
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_is_gateway(&mut self, is_gateway: bool) {
|
||||||
|
self.is_gateway = is_gateway;
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn user_agent(&self) -> &str {
|
pub(crate) fn user_agent(&self) -> &str {
|
||||||
&self.user_agent
|
&self.user_agent
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn computer(&self) -> &str {
|
pub(crate) fn computer(&self) -> &str {
|
||||||
match self.computer {
|
&self.computer
|
||||||
Some(ref computer) => computer,
|
}
|
||||||
None => (&self.client_os).into()
|
|
||||||
}
|
pub fn ignore_tls_errors(&self) -> bool {
|
||||||
|
self.ignore_tls_errors
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prefer_default_browser(&self) -> bool {
|
||||||
|
self.prefer_default_browser
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn to_params(&self) -> HashMap<&str, &str> {
|
pub(crate) fn to_params(&self) -> HashMap<&str, &str> {
|
||||||
let mut params: HashMap<&str, &str> = HashMap::new();
|
let mut params: HashMap<&str, &str> = HashMap::new();
|
||||||
let client_os: &str = (&self.client_os).into();
|
let client_os = self.client_os.as_str();
|
||||||
|
|
||||||
// Common params
|
// Common params
|
||||||
params.insert("prot", "https:");
|
params.insert("prot", "https:");
|
||||||
@@ -70,46 +95,52 @@ impl GpParams {
|
|||||||
params.insert("ipv6-support", "yes");
|
params.insert("ipv6-support", "yes");
|
||||||
params.insert("inputStr", "");
|
params.insert("inputStr", "");
|
||||||
params.insert("clientVer", "4100");
|
params.insert("clientVer", "4100");
|
||||||
|
|
||||||
params.insert("clientos", client_os);
|
params.insert("clientos", client_os);
|
||||||
|
params.insert("computer", &self.computer);
|
||||||
if let Some(computer) = &self.computer {
|
|
||||||
params.insert("computer", computer);
|
|
||||||
} else {
|
|
||||||
params.insert("computer", client_os);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(os_version) = &self.os_version {
|
if let Some(os_version) = &self.os_version {
|
||||||
params.insert("os-version", os_version);
|
params.insert("os-version", os_version);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(client_version) = &self.client_version {
|
// NOTE: Do not include clientgpversion for now
|
||||||
params.insert("clientgpversion", client_version);
|
// if let Some(client_version) = &self.client_version {
|
||||||
}
|
// params.insert("clientgpversion", client_version);
|
||||||
|
// }
|
||||||
|
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct GpParamsBuilder {
|
pub struct GpParamsBuilder {
|
||||||
|
is_gateway: bool,
|
||||||
user_agent: String,
|
user_agent: String,
|
||||||
client_os: ClientOs,
|
client_os: ClientOs,
|
||||||
os_version: Option<String>,
|
os_version: Option<String>,
|
||||||
client_version: Option<String>,
|
client_version: Option<String>,
|
||||||
computer: Option<String>,
|
computer: String,
|
||||||
|
ignore_tls_errors: bool,
|
||||||
|
prefer_default_browser: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GpParamsBuilder {
|
impl GpParamsBuilder {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
is_gateway: false,
|
||||||
user_agent: GP_USER_AGENT.to_string(),
|
user_agent: GP_USER_AGENT.to_string(),
|
||||||
client_os: ClientOs::Linux,
|
client_os: ClientOs::Linux,
|
||||||
os_version: Default::default(),
|
os_version: Default::default(),
|
||||||
client_version: Default::default(),
|
client_version: Default::default(),
|
||||||
computer: Default::default(),
|
computer: whoami::hostname(),
|
||||||
|
ignore_tls_errors: false,
|
||||||
|
prefer_default_browser: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_gateway(&mut self, is_gateway: bool) -> &mut Self {
|
||||||
|
self.is_gateway = is_gateway;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn user_agent(&mut self, user_agent: &str) -> &mut Self {
|
pub fn user_agent(&mut self, user_agent: &str) -> &mut Self {
|
||||||
self.user_agent = user_agent.to_string();
|
self.user_agent = user_agent.to_string();
|
||||||
self
|
self
|
||||||
@@ -120,28 +151,41 @@ impl GpParamsBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn os_version(&mut self, os_version: &str) -> &mut Self {
|
pub fn os_version<T: Into<Option<String>>>(&mut self, os_version: T) -> &mut Self {
|
||||||
self.os_version = Some(os_version.to_string());
|
self.os_version = os_version.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn client_version(&mut self, client_version: &str) -> &mut Self {
|
pub fn client_version<T: Into<Option<String>>>(&mut self, client_version: T) -> &mut Self {
|
||||||
self.client_version = Some(client_version.to_string());
|
self.client_version = client_version.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn computer(&mut self, computer: &str) -> &mut Self {
|
pub fn computer(&mut self, computer: &str) -> &mut Self {
|
||||||
self.computer = Some(computer.to_string());
|
self.computer = computer.to_string();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ignore_tls_errors(&mut self, ignore_tls_errors: bool) -> &mut Self {
|
||||||
|
self.ignore_tls_errors = ignore_tls_errors;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prefer_default_browser(&mut self, prefer_default_browser: bool) -> &mut Self {
|
||||||
|
self.prefer_default_browser = prefer_default_browser;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build(&self) -> GpParams {
|
pub fn build(&self) -> GpParams {
|
||||||
GpParams {
|
GpParams {
|
||||||
|
is_gateway: self.is_gateway,
|
||||||
user_agent: self.user_agent.clone(),
|
user_agent: self.user_agent.clone(),
|
||||||
client_os: self.client_os.clone(),
|
client_os: self.client_os.clone(),
|
||||||
os_version: self.os_version.clone(),
|
os_version: self.os_version.clone(),
|
||||||
client_version: self.client_version.clone(),
|
client_version: self.client_version.clone(),
|
||||||
computer: self.computer.clone(),
|
computer: self.computer.clone(),
|
||||||
|
ignore_tls_errors: self.ignore_tls_errors,
|
||||||
|
prefer_default_browser: self.prefer_default_browser,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -7,12 +7,17 @@ pub mod process;
|
|||||||
pub mod service;
|
pub mod service;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
||||||
|
#[cfg(feature = "clap")]
|
||||||
|
pub mod clap;
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
pub const GP_API_KEY: &[u8; 32] = &[0; 32];
|
pub const GP_API_KEY: &[u8; 32] = &[0; 32];
|
||||||
|
|
||||||
pub const GP_USER_AGENT: &str = "PAN GlobalProtect";
|
pub const GP_USER_AGENT: &str = "PAN GlobalProtect";
|
||||||
pub const GP_SERVICE_LOCK_FILE: &str = "/var/run/gpservice.lock";
|
pub const GP_SERVICE_LOCK_FILE: &str = "/var/run/gpservice.lock";
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
pub const GP_CLIENT_BINARY: &str = "/usr/bin/gpclient";
|
||||||
#[cfg(not(debug_assertions))]
|
#[cfg(not(debug_assertions))]
|
||||||
pub const GP_SERVICE_BINARY: &str = "/usr/bin/gpservice";
|
pub const GP_SERVICE_BINARY: &str = "/usr/bin/gpservice";
|
||||||
#[cfg(not(debug_assertions))]
|
#[cfg(not(debug_assertions))]
|
||||||
@@ -20,6 +25,8 @@ pub const GP_GUI_BINARY: &str = "/usr/bin/gpgui";
|
|||||||
#[cfg(not(debug_assertions))]
|
#[cfg(not(debug_assertions))]
|
||||||
pub(crate) const GP_AUTH_BINARY: &str = "/usr/bin/gpauth";
|
pub(crate) const GP_AUTH_BINARY: &str = "/usr/bin/gpauth";
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
pub const GP_CLIENT_BINARY: &str = dotenvy_macro::dotenv!("GP_CLIENT_BINARY");
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
pub const GP_SERVICE_BINARY: &str = dotenvy_macro::dotenv!("GP_SERVICE_BINARY");
|
pub const GP_SERVICE_BINARY: &str = dotenvy_macro::dotenv!("GP_SERVICE_BINARY");
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
|
@@ -102,12 +102,6 @@ impl PortalConfig {
|
|||||||
pub enum PortalConfigError {
|
pub enum PortalConfigError {
|
||||||
#[error("Empty response, retrying can help")]
|
#[error("Empty response, retrying can help")]
|
||||||
EmptyResponse,
|
EmptyResponse,
|
||||||
#[error("Empty auth cookie, retrying can help")]
|
|
||||||
EmptyAuthCookie,
|
|
||||||
#[error("Invalid auth cookie, retrying can help")]
|
|
||||||
InvalidAuthCookie,
|
|
||||||
#[error("Empty gateways, retrying can help")]
|
|
||||||
EmptyGateways,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn retrieve_config(
|
pub async fn retrieve_config(
|
||||||
@@ -120,6 +114,7 @@ pub async fn retrieve_config(
|
|||||||
|
|
||||||
let url = format!("{}/global-protect/getconfig.esp", portal);
|
let url = format!("{}/global-protect/getconfig.esp", portal);
|
||||||
let client = Client::builder()
|
let client = Client::builder()
|
||||||
|
.danger_accept_invalid_certs(gp_params.ignore_tls_errors())
|
||||||
.user_agent(gp_params.user_agent())
|
.user_agent(gp_params.user_agent())
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
@@ -132,36 +127,28 @@ pub async fn retrieve_config(
|
|||||||
|
|
||||||
info!("Portal config, user_agent: {}", gp_params.user_agent());
|
info!("Portal config, user_agent: {}", gp_params.user_agent());
|
||||||
|
|
||||||
let res_xml = client
|
let res = client.post(&url).form(¶ms).send().await?;
|
||||||
.post(&url)
|
let res_xml = res.error_for_status()?.text().await?;
|
||||||
.form(¶ms)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.error_for_status()?
|
|
||||||
.text()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
ensure!(!res_xml.is_empty(), PortalConfigError::EmptyResponse);
|
ensure!(!res_xml.is_empty(), PortalConfigError::EmptyResponse);
|
||||||
|
|
||||||
let doc = Document::parse(&res_xml)?;
|
let doc = Document::parse(&res_xml)?;
|
||||||
let gateways = parse_gateways(&doc).ok_or_else(|| anyhow::anyhow!("Failed to parse gateways"))?;
|
let mut gateways =
|
||||||
|
parse_gateways(&doc).ok_or_else(|| anyhow::anyhow!("Failed to parse gateways"))?;
|
||||||
|
|
||||||
let user_auth_cookie = xml::get_child_text(&doc, "portal-userauthcookie").unwrap_or_default();
|
let user_auth_cookie = xml::get_child_text(&doc, "portal-userauthcookie").unwrap_or_default();
|
||||||
let prelogon_user_auth_cookie =
|
let prelogon_user_auth_cookie =
|
||||||
xml::get_child_text(&doc, "portal-prelogonuserauthcookie").unwrap_or_default();
|
xml::get_child_text(&doc, "portal-prelogonuserauthcookie").unwrap_or_default();
|
||||||
let config_digest = xml::get_child_text(&doc, "config-digest");
|
let config_digest = xml::get_child_text(&doc, "config-digest");
|
||||||
|
|
||||||
ensure!(
|
if gateways.is_empty() {
|
||||||
!user_auth_cookie.is_empty() && !prelogon_user_auth_cookie.is_empty(),
|
gateways.push(Gateway {
|
||||||
PortalConfigError::EmptyAuthCookie
|
name: server.to_string(),
|
||||||
);
|
address: server.to_string(),
|
||||||
|
priority: 0,
|
||||||
ensure!(
|
priority_rules: vec![],
|
||||||
user_auth_cookie != "empty" && prelogon_user_auth_cookie != "empty",
|
});
|
||||||
PortalConfigError::InvalidAuthCookie
|
}
|
||||||
);
|
|
||||||
|
|
||||||
ensure!(!gateways.is_empty(), PortalConfigError::EmptyGateways);
|
|
||||||
|
|
||||||
Ok(PortalConfig::new(
|
Ok(PortalConfig::new(
|
||||||
server.to_string(),
|
server.to_string(),
|
||||||
|
@@ -5,13 +5,28 @@ use roxmltree::Document;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use specta::Type;
|
use specta::Type;
|
||||||
|
|
||||||
use crate::utils::{base64, normalize_server, xml};
|
use crate::{
|
||||||
|
gp_params::GpParams,
|
||||||
|
utils::{base64, normalize_server, xml},
|
||||||
|
};
|
||||||
|
|
||||||
|
const REQUIRED_PARAMS: [&str; 8] = [
|
||||||
|
"tmp",
|
||||||
|
"clientVer",
|
||||||
|
"clientos",
|
||||||
|
"os-version",
|
||||||
|
"host-id",
|
||||||
|
"ipv6-support",
|
||||||
|
"default-browser",
|
||||||
|
"cas-support",
|
||||||
|
];
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Type, Clone)]
|
#[derive(Debug, Serialize, Type, Clone)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct SamlPrelogin {
|
pub struct SamlPrelogin {
|
||||||
region: String,
|
region: String,
|
||||||
saml_request: String,
|
saml_request: String,
|
||||||
|
support_default_browser: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SamlPrelogin {
|
impl SamlPrelogin {
|
||||||
@@ -22,6 +37,10 @@ impl SamlPrelogin {
|
|||||||
pub fn saml_request(&self) -> &str {
|
pub fn saml_request(&self) -> &str {
|
||||||
&self.saml_request
|
&self.saml_request
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn support_default_browser(&self) -> bool {
|
||||||
|
self.support_default_browser
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Type, Clone)]
|
#[derive(Debug, Serialize, Type, Clone)]
|
||||||
@@ -67,20 +86,39 @@ impl Prelogin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn prelogin(portal: &str, user_agent: &str) -> anyhow::Result<Prelogin> {
|
pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prelogin> {
|
||||||
|
let user_agent = gp_params.user_agent();
|
||||||
info!("Portal prelogin, user_agent: {}", user_agent);
|
info!("Portal prelogin, user_agent: {}", user_agent);
|
||||||
|
|
||||||
let portal = normalize_server(portal)?;
|
let portal = normalize_server(portal)?;
|
||||||
let prelogin_url = format!("{}/global-protect/prelogin.esp", portal);
|
let prelogin_url = format!(
|
||||||
let client = Client::builder().user_agent(user_agent).build()?;
|
"{portal}/{}/prelogin.esp",
|
||||||
|
if gp_params.is_gateway() {
|
||||||
|
"ssl-vpn"
|
||||||
|
} else {
|
||||||
|
"global-protect"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let mut params = gp_params.to_params();
|
||||||
|
|
||||||
let res_xml = client
|
params.insert("tmp", "tmp");
|
||||||
.get(&prelogin_url)
|
if gp_params.prefer_default_browser() {
|
||||||
.send()
|
params.insert("default-browser", "1");
|
||||||
.await?
|
}
|
||||||
.error_for_status()?
|
|
||||||
.text()
|
params.retain(|k, _| {
|
||||||
.await?;
|
REQUIRED_PARAMS
|
||||||
|
.iter()
|
||||||
|
.any(|required_param| required_param == k)
|
||||||
|
});
|
||||||
|
|
||||||
|
let client = Client::builder()
|
||||||
|
.danger_accept_invalid_certs(gp_params.ignore_tls_errors())
|
||||||
|
.user_agent(user_agent)
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
let res = client.post(&prelogin_url).form(¶ms).send().await?;
|
||||||
|
let res_xml = res.error_for_status()?.text().await?;
|
||||||
|
|
||||||
trace!("Prelogin response: {}", res_xml);
|
trace!("Prelogin response: {}", res_xml);
|
||||||
let doc = Document::parse(&res_xml)?;
|
let doc = Document::parse(&res_xml)?;
|
||||||
@@ -98,12 +136,18 @@ pub async fn prelogin(portal: &str, user_agent: &str) -> anyhow::Result<Prelogin
|
|||||||
|
|
||||||
let saml_method = xml::get_child_text(&doc, "saml-auth-method");
|
let saml_method = xml::get_child_text(&doc, "saml-auth-method");
|
||||||
let saml_request = xml::get_child_text(&doc, "saml-request");
|
let saml_request = xml::get_child_text(&doc, "saml-request");
|
||||||
|
let saml_default_browser = xml::get_child_text(&doc, "saml-default-browser");
|
||||||
// Check if the prelogin response is SAML
|
// Check if the prelogin response is SAML
|
||||||
if saml_method.is_some() && saml_request.is_some() {
|
if saml_method.is_some() && saml_request.is_some() {
|
||||||
let saml_request = base64::decode_to_string(&saml_request.unwrap())?;
|
let saml_request = base64::decode_to_string(&saml_request.unwrap())?;
|
||||||
|
let support_default_browser = saml_default_browser
|
||||||
|
.map(|s| s.to_lowercase() == "yes")
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
let saml_prelogin = SamlPrelogin {
|
let saml_prelogin = SamlPrelogin {
|
||||||
region,
|
region,
|
||||||
saml_request,
|
saml_request,
|
||||||
|
support_default_browser,
|
||||||
};
|
};
|
||||||
|
|
||||||
return Ok(Prelogin::Saml(saml_prelogin));
|
return Ok(Prelogin::Saml(saml_prelogin));
|
||||||
|
@@ -8,10 +8,13 @@ use super::command_traits::CommandExt;
|
|||||||
|
|
||||||
pub struct SamlAuthLauncher<'a> {
|
pub struct SamlAuthLauncher<'a> {
|
||||||
server: &'a str,
|
server: &'a str,
|
||||||
user_agent: Option<&'a str>,
|
|
||||||
saml_request: Option<&'a str>,
|
saml_request: Option<&'a str>,
|
||||||
|
user_agent: Option<&'a str>,
|
||||||
|
os: Option<&'a str>,
|
||||||
|
os_version: Option<&'a str>,
|
||||||
hidpi: bool,
|
hidpi: bool,
|
||||||
fix_openssl: bool,
|
fix_openssl: bool,
|
||||||
|
ignore_tls_errors: bool,
|
||||||
clean: bool,
|
clean: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,21 +22,34 @@ impl<'a> SamlAuthLauncher<'a> {
|
|||||||
pub fn new(server: &'a str) -> Self {
|
pub fn new(server: &'a str) -> Self {
|
||||||
Self {
|
Self {
|
||||||
server,
|
server,
|
||||||
user_agent: None,
|
|
||||||
saml_request: None,
|
saml_request: None,
|
||||||
|
user_agent: None,
|
||||||
|
os: None,
|
||||||
|
os_version: None,
|
||||||
hidpi: false,
|
hidpi: false,
|
||||||
fix_openssl: false,
|
fix_openssl: false,
|
||||||
|
ignore_tls_errors: false,
|
||||||
clean: false,
|
clean: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn saml_request(mut self, saml_request: &'a str) -> Self {
|
||||||
|
self.saml_request = Some(saml_request);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn user_agent(mut self, user_agent: &'a str) -> Self {
|
pub fn user_agent(mut self, user_agent: &'a str) -> Self {
|
||||||
self.user_agent = Some(user_agent);
|
self.user_agent = Some(user_agent);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn saml_request(mut self, saml_request: &'a str) -> Self {
|
pub fn os(mut self, os: &'a str) -> Self {
|
||||||
self.saml_request = Some(saml_request);
|
self.os = Some(os);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn os_version(mut self, os_version: Option<&'a str>) -> Self {
|
||||||
|
self.os_version = os_version;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,6 +63,11 @@ impl<'a> SamlAuthLauncher<'a> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn ignore_tls_errors(mut self, ignore_tls_errors: bool) -> Self {
|
||||||
|
self.ignore_tls_errors = ignore_tls_errors;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn clean(mut self, clean: bool) -> Self {
|
pub fn clean(mut self, clean: bool) -> Self {
|
||||||
self.clean = clean;
|
self.clean = clean;
|
||||||
self
|
self
|
||||||
@@ -57,20 +78,32 @@ impl<'a> SamlAuthLauncher<'a> {
|
|||||||
let mut auth_cmd = Command::new(GP_AUTH_BINARY);
|
let mut auth_cmd = Command::new(GP_AUTH_BINARY);
|
||||||
auth_cmd.arg(self.server);
|
auth_cmd.arg(self.server);
|
||||||
|
|
||||||
|
if let Some(saml_request) = self.saml_request {
|
||||||
|
auth_cmd.arg("--saml-request").arg(saml_request);
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(user_agent) = self.user_agent {
|
if let Some(user_agent) = self.user_agent {
|
||||||
auth_cmd.arg("--user-agent").arg(user_agent);
|
auth_cmd.arg("--user-agent").arg(user_agent);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(saml_request) = self.saml_request {
|
if let Some(os) = self.os {
|
||||||
auth_cmd.arg("--saml-request").arg(saml_request);
|
auth_cmd.arg("--os").arg(os);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(os_version) = self.os_version {
|
||||||
|
auth_cmd.arg("--os-version").arg(os_version);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.hidpi {
|
||||||
|
auth_cmd.arg("--hidpi");
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.fix_openssl {
|
if self.fix_openssl {
|
||||||
auth_cmd.arg("--fix-openssl");
|
auth_cmd.arg("--fix-openssl");
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.hidpi {
|
if self.ignore_tls_errors {
|
||||||
auth_cmd.arg("--hidpi");
|
auth_cmd.arg("--ignore-tls-errors");
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.clean {
|
if self.clean {
|
||||||
|
34
crates/gpapi/src/process/browser_authenticator.rs
Normal file
34
crates/gpapi/src/process/browser_authenticator.rs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
use std::{env::temp_dir, io::Write};
|
||||||
|
|
||||||
|
pub struct BrowserAuthenticator<'a> {
|
||||||
|
auth_request: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BrowserAuthenticator<'_> {
|
||||||
|
pub fn new(auth_request: &str) -> BrowserAuthenticator {
|
||||||
|
BrowserAuthenticator { auth_request }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn authenticate(&self) -> anyhow::Result<()> {
|
||||||
|
if self.auth_request.starts_with("http") {
|
||||||
|
open::that_detached(self.auth_request)?;
|
||||||
|
} else {
|
||||||
|
let html_file = temp_dir().join("gpauth.html");
|
||||||
|
let mut file = std::fs::File::create(&html_file)?;
|
||||||
|
|
||||||
|
file.write_all(self.auth_request.as_bytes())?;
|
||||||
|
|
||||||
|
open::that_detached(html_file)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for BrowserAuthenticator<'_> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// Cleanup the temporary file
|
||||||
|
let html_file = temp_dir().join("gpauth.html");
|
||||||
|
let _ = std::fs::remove_file(html_file);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,7 +1,7 @@
|
|||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
use std::{env, ffi::OsStr};
|
use std::{env, ffi::OsStr};
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
use users::{os::unix::UserExt, User};
|
use uzers::{os::unix::UserExt, User};
|
||||||
|
|
||||||
pub trait CommandExt {
|
pub trait CommandExt {
|
||||||
fn new_pkexec<S: AsRef<OsStr>>(program: S) -> Command;
|
fn new_pkexec<S: AsRef<OsStr>>(program: S) -> Command;
|
||||||
@@ -42,7 +42,7 @@ fn get_non_root_user() -> anyhow::Result<User> {
|
|||||||
let user = if current_user == "root" {
|
let user = if current_user == "root" {
|
||||||
get_real_user()?
|
get_real_user()?
|
||||||
} else {
|
} else {
|
||||||
users::get_user_by_name(¤t_user)
|
uzers::get_user_by_name(¤t_user)
|
||||||
.ok_or_else(|| anyhow::anyhow!("User ({}) not found", current_user))?
|
.ok_or_else(|| anyhow::anyhow!("User ({}) not found", current_user))?
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -60,5 +60,5 @@ fn get_real_user() -> anyhow::Result<User> {
|
|||||||
_ => env::var("PKEXEC_UID")?.parse::<u32>()?,
|
_ => env::var("PKEXEC_UID")?.parse::<u32>()?,
|
||||||
};
|
};
|
||||||
|
|
||||||
users::get_user_by_uid(uid).ok_or_else(|| anyhow::anyhow!("User not found"))
|
uzers::get_user_by_uid(uid).ok_or_else(|| anyhow::anyhow!("User not found"))
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
pub(crate) mod command_traits;
|
pub(crate) mod command_traits;
|
||||||
|
|
||||||
pub mod auth_launcher;
|
pub mod auth_launcher;
|
||||||
|
#[cfg(feature = "browser-auth")]
|
||||||
|
pub mod browser_authenticator;
|
||||||
pub mod gui_launcher;
|
pub mod gui_launcher;
|
||||||
pub mod service_launcher;
|
pub mod service_launcher;
|
||||||
|
@@ -7,4 +7,6 @@ use super::vpn_state::VpnState;
|
|||||||
pub enum WsEvent {
|
pub enum WsEvent {
|
||||||
VpnState(VpnState),
|
VpnState(VpnState),
|
||||||
ActiveGui,
|
ActiveGui,
|
||||||
|
/// External authentication data
|
||||||
|
AuthData(String),
|
||||||
}
|
}
|
||||||
|
@@ -27,7 +27,6 @@ pub fn raise_window(win: &Window) -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
let title = win.title()?;
|
let title = win.title()?;
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
info!("Raising window: {}", title);
|
|
||||||
if let Err(err) = wmctrl_raise_window(&title).await {
|
if let Err(err) = wmctrl_raise_window(&title).await {
|
||||||
warn!("Failed to raise window: {}", err);
|
warn!("Failed to raise window: {}", err);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user