mirror of
https://github.com/yuezk/GlobalProtect-openconnect.git
synced 2025-05-20 07:26:58 -04:00
Compare commits
8 Commits
v2.0.0-bet
...
v2.0.0-bet
Author | SHA1 | Date | |
---|---|---|---|
|
c3bd7aeb93 | ||
|
0b55a80317 | ||
|
c6315bf384 | ||
|
87b965f80c | ||
|
b09b21ae0f | ||
|
7e372cd113 | ||
|
1e211e8912 | ||
|
8bc4049a0f |
138
.github/workflows/build.yaml
vendored
138
.github/workflows/build.yaml
vendored
@@ -6,8 +6,8 @@ on:
|
||||
- "*.md"
|
||||
- .vscode
|
||||
- .devcontainer
|
||||
# branches:
|
||||
# - main
|
||||
branches:
|
||||
- main
|
||||
# tags:
|
||||
# - v*.*.*
|
||||
jobs:
|
||||
@@ -114,137 +114,3 @@ jobs:
|
||||
name: artifact-${{ matrix.arch }}-tauri
|
||||
path: |
|
||||
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/*
|
||||
|
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
@@ -11,8 +11,10 @@
|
||||
"dotenvy",
|
||||
"getconfig",
|
||||
"globalprotect",
|
||||
"globalprotectcallback",
|
||||
"gpapi",
|
||||
"gpauth",
|
||||
"gpcallback",
|
||||
"gpclient",
|
||||
"gpcommon",
|
||||
"gpgui",
|
||||
@@ -48,6 +50,8 @@
|
||||
"vpnc",
|
||||
"vpninfo",
|
||||
"wmctrl",
|
||||
"XAUTHORITY"
|
||||
]
|
||||
"XAUTHORITY",
|
||||
"yuezk"
|
||||
],
|
||||
"rust-analyzer.cargo.features": "all",
|
||||
}
|
||||
|
62
Cargo.lock
generated
62
Cargo.lock
generated
@@ -1423,13 +1423,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gpapi"
|
||||
version = "2.0.0-beta2"
|
||||
version = "2.0.0-beta5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.21.5",
|
||||
"chacha20poly1305",
|
||||
"clap",
|
||||
"dotenvy_macro",
|
||||
"log",
|
||||
"open",
|
||||
"redact-engine",
|
||||
"regex",
|
||||
"reqwest",
|
||||
@@ -1450,7 +1452,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gpauth"
|
||||
version = "2.0.0-beta2"
|
||||
version = "2.0.0-beta5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -1470,7 +1472,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gpclient"
|
||||
version = "2.0.0-beta2"
|
||||
version = "2.0.0-beta5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -1491,7 +1493,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gpservice"
|
||||
version = "2.0.0-beta2"
|
||||
version = "2.0.0-beta5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -1564,9 +1566,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.3.22"
|
||||
version = "0.3.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178"
|
||||
checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
@@ -1583,9 +1585,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.0"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1d308f63daf4181410c242d34c11f928dcb3aa105852019e043c9d1f4e4368a"
|
||||
checksum = "31d030e59af851932b72ceebadf4a2b5986dba4c3b99dd2493f8273a0f151943"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
@@ -1743,7 +1745,7 @@ dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2 0.3.22",
|
||||
"h2 0.3.24",
|
||||
"http 0.2.11",
|
||||
"http-body 0.4.6",
|
||||
"httparse",
|
||||
@@ -1766,7 +1768,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"h2 0.4.0",
|
||||
"h2 0.4.2",
|
||||
"http 1.0.0",
|
||||
"http-body 1.0.0",
|
||||
"httparse",
|
||||
@@ -1962,6 +1964,15 @@ version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "is-terminal"
|
||||
version = "0.4.10"
|
||||
@@ -1973,6 +1984,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "is_executable"
|
||||
version = "1.0.1"
|
||||
@@ -2444,9 +2465,20 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "openconnect"
|
||||
version = "2.0.0-beta2"
|
||||
version = "2.0.0-beta5"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"is_executable",
|
||||
@@ -2573,6 +2605,12 @@ version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
|
||||
|
||||
[[package]]
|
||||
name = "pathdiff"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.1"
|
||||
@@ -3070,7 +3108,7 @@ dependencies = [
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2 0.3.22",
|
||||
"h2 0.3.24",
|
||||
"http 0.2.11",
|
||||
"http-body 0.4.6",
|
||||
"hyper 0.14.28",
|
||||
|
@@ -4,7 +4,7 @@ resolver = "2"
|
||||
members = ["crates/*", "apps/gpclient", "apps/gpservice", "apps/gpauth"]
|
||||
|
||||
[workspace.package]
|
||||
version = "2.0.0-beta2"
|
||||
version = "2.0.0-beta5"
|
||||
authors = ["Kevin Yue <k3vinyue@gmail.com>"]
|
||||
homepage = "https://github.com/yuezk/GlobalProtect-openconnect"
|
||||
edition = "2021"
|
||||
|
35
README.md
35
README.md
@@ -11,6 +11,7 @@ A GUI for GlobalProtect VPN, based on OpenConnect, supports the SSO authenticati
|
||||
- [x] Better Linux support
|
||||
- [x] Support both CLI and GUI
|
||||
- [x] Support both SSO and non-SSO authentication
|
||||
- [x] Support authentication using default browser
|
||||
- [x] Support multiple portals
|
||||
- [x] Support gateway selection
|
||||
- [x] Support auto-connect on startup
|
||||
@@ -32,12 +33,13 @@ Commands:
|
||||
help Print this message or the help of the given subcommand(s)
|
||||
|
||||
Options:
|
||||
--fix-openssl Get around the OpenSSL `unsafe legacy renegotiation` error
|
||||
-h, --help Print help
|
||||
-V, --version Print version
|
||||
```
|
||||
--fix-openssl Get around the OpenSSL `unsafe legacy renegotiation` error
|
||||
--ignore-tls-errors Ignore the TLS errors
|
||||
-h, --help Print help
|
||||
-V, --version Print version
|
||||
|
||||
See `gpclient -h` for help.
|
||||
See 'gpclient help <command>' for more information on a specific command.
|
||||
```
|
||||
|
||||
### GUI
|
||||
|
||||
@@ -49,6 +51,15 @@ The GUI version is also available after you installed it. You can launch it from
|
||||
|
||||
## 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
|
||||
@@ -59,6 +70,10 @@ sudo apt-get update
|
||||
sudo apt-get install globalprotect-openconnect
|
||||
```
|
||||
|
||||
> [!Note]
|
||||
>
|
||||
> For Linux Mint, you might need to import the GPG key with: `sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 7937C393082992E5D6E4A60453FC26B43838D761` if you encountered an error `gpg: keyserver receive failed: General error`.
|
||||
|
||||
#### Install from deb package
|
||||
|
||||
Download the latest deb package from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. Then install it with `dpkg`:
|
||||
@@ -73,6 +88,10 @@ sudo dpkg -i globalprotect-openconnect_*.deb
|
||||
|
||||
Install from AUR: [globalprotect-openconnect-git](https://aur.archlinux.org/packages/globalprotect-openconnect-git/)
|
||||
|
||||
```
|
||||
yay -S globalprotect-openconnect-git
|
||||
```
|
||||
|
||||
#### Install from package
|
||||
|
||||
Download the latest package from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. Then install it with `pacman`:
|
||||
@@ -102,11 +121,7 @@ Download the latest RPM package from [releases](https://github.com/yuezk/GlobalP
|
||||
|
||||
### Other distributions
|
||||
|
||||
The project depends on `openconnect`, `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.
|
||||
|
||||
### Install the Old Version (v1.4.9)
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## [License](./LICENSE)
|
||||
|
||||
|
@@ -8,7 +8,7 @@ license.workspace = true
|
||||
tauri-build = { version = "1.5", features = [] }
|
||||
|
||||
[dependencies]
|
||||
gpapi = { path = "../../crates/gpapi", features = ["tauri"] }
|
||||
gpapi = { path = "../../crates/gpapi", features = ["tauri", "clap"] }
|
||||
anyhow.workspace = true
|
||||
clap.workspace = true
|
||||
env_logger.workspace = true
|
||||
|
@@ -7,6 +7,7 @@ use std::{
|
||||
use anyhow::bail;
|
||||
use gpapi::{
|
||||
auth::SamlAuthData,
|
||||
gp_params::GpParams,
|
||||
portal::{prelogin, Prelogin},
|
||||
utils::{redact::redact_uri, window::WindowExt},
|
||||
};
|
||||
@@ -18,11 +19,13 @@ use tokio_util::sync::CancellationToken;
|
||||
use webkit2gtk::{
|
||||
gio::Cancellable,
|
||||
glib::{GString, TimeSpan},
|
||||
LoadEvent, SettingsExt, URIResponse, URIResponseExt, WebContextExt, WebResource, WebResourceExt,
|
||||
WebView, WebViewExt, WebsiteDataManagerExtManual, WebsiteDataTypes,
|
||||
LoadEvent, SettingsExt, TLSErrorsPolicy, URIResponse, URIResponseExt, WebContextExt, WebResource,
|
||||
WebResourceExt, WebView, WebViewExt, WebsiteDataManagerExtManual, WebsiteDataTypes,
|
||||
};
|
||||
|
||||
enum AuthDataError {
|
||||
/// Failed to load page due to TLS error
|
||||
TlsError,
|
||||
/// 1. Found auth data in headers/body but it's invalid
|
||||
/// 2. Loaded an empty page, failed to load page. etc.
|
||||
Invalid,
|
||||
@@ -37,6 +40,7 @@ pub(crate) struct AuthWindow<'a> {
|
||||
server: &'a str,
|
||||
saml_request: &'a str,
|
||||
user_agent: &'a str,
|
||||
gp_params: Option<GpParams>,
|
||||
clean: bool,
|
||||
}
|
||||
|
||||
@@ -47,6 +51,7 @@ impl<'a> AuthWindow<'a> {
|
||||
server: "",
|
||||
saml_request: "",
|
||||
user_agent: "",
|
||||
gp_params: None,
|
||||
clean: false,
|
||||
}
|
||||
}
|
||||
@@ -66,6 +71,11 @@ impl<'a> AuthWindow<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn gp_params(mut self, gp_params: GpParams) -> Self {
|
||||
self.gp_params.replace(gp_params);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn clean(mut self, clean: bool) -> Self {
|
||||
self.clean = clean;
|
||||
self
|
||||
@@ -119,6 +129,12 @@ impl<'a> AuthWindow<'a> {
|
||||
let saml_request = self.saml_request.to_string();
|
||||
let (auth_result_tx, mut auth_result_rx) = mpsc::unbounded_channel::<AuthResult>();
|
||||
let raise_window_cancel_token: Arc<RwLock<Option<CancellationToken>>> = Default::default();
|
||||
let gp_params = self.gp_params.as_ref().unwrap();
|
||||
let tls_err_policy = if gp_params.ignore_tls_errors() {
|
||||
TLSErrorsPolicy::Ignore
|
||||
} else {
|
||||
TLSErrorsPolicy::Fail
|
||||
};
|
||||
|
||||
if self.clean {
|
||||
clear_webview_cookies(window).await?;
|
||||
@@ -128,6 +144,10 @@ impl<'a> AuthWindow<'a> {
|
||||
window.with_webview(move |wv| {
|
||||
let wv = wv.inner();
|
||||
|
||||
if let Some(context) = wv.context() {
|
||||
context.set_tls_errors_policy(tls_err_policy);
|
||||
}
|
||||
|
||||
if let Some(settings) = wv.settings() {
|
||||
let ua = settings.user_agent().unwrap_or("".into());
|
||||
info!("Auth window user agent: {}", ua);
|
||||
@@ -168,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);
|
||||
warn!(
|
||||
"Failed to load uri: {} with error: {}, cert: {}",
|
||||
redacted_uri, err, cert
|
||||
);
|
||||
|
||||
send_auth_result(&auth_result_tx_clone, Err(AuthDataError::TlsError));
|
||||
true
|
||||
});
|
||||
|
||||
wv.connect_load_failed(move |_wv, _event, uri, err| {
|
||||
let redacted_uri = redact_uri(uri);
|
||||
warn!("Failed to load uri: {} with error: {}", redacted_uri, err);
|
||||
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
|
||||
});
|
||||
})?;
|
||||
|
||||
let portal = self.server.to_string();
|
||||
let user_agent = self.user_agent.to_string();
|
||||
|
||||
loop {
|
||||
if let Some(auth_result) = auth_result_rx.recv().await {
|
||||
match auth_result {
|
||||
Ok(auth_data) => return Ok(auth_data),
|
||||
Err(AuthDataError::TlsError) => {
|
||||
return Err(anyhow::anyhow!("TLS error: certificate verify failed"))
|
||||
}
|
||||
Err(AuthDataError::NotFound) => {
|
||||
info!("No auth data found, it may not be the /SAML20/SP/ACS endpoint");
|
||||
|
||||
@@ -237,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| {
|
||||
let wv = wv.inner();
|
||||
load_saml_request(&wv, &saml_request);
|
||||
@@ -258,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...");
|
||||
match prelogin(portal, user_agent).await? {
|
||||
|
||||
match prelogin(portal, gp_params).await? {
|
||||
Prelogin::Saml(prelogin) => Ok(prelogin.saml_request().to_string()),
|
||||
Prelogin::Standard(_) => Err(anyhow::anyhow!("Received non-SAML prelogin response")),
|
||||
}
|
||||
@@ -397,6 +424,11 @@ fn read_auth_data(main_resource: &WebResource, auth_result_tx: mpsc::UnboundedSe
|
||||
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 gpapi::{
|
||||
auth::{SamlAuthData, SamlAuthResult},
|
||||
clap::args::Os,
|
||||
gp_params::{ClientOs, GpParams},
|
||||
utils::{normalize_server, openssl},
|
||||
GP_USER_AGENT,
|
||||
};
|
||||
@@ -26,23 +28,35 @@ struct Cli {
|
||||
saml_request: Option<String>,
|
||||
#[arg(long, default_value = GP_USER_AGENT)]
|
||||
user_agent: String,
|
||||
#[arg(long, default_value = "Linux")]
|
||||
os: Os,
|
||||
#[arg(long)]
|
||||
os_version: Option<String>,
|
||||
#[arg(long)]
|
||||
hidpi: bool,
|
||||
#[arg(long)]
|
||||
fix_openssl: bool,
|
||||
#[arg(long)]
|
||||
ignore_tls_errors: bool,
|
||||
#[arg(long)]
|
||||
clean: bool,
|
||||
}
|
||||
|
||||
impl Cli {
|
||||
async fn run(&mut self) -> anyhow::Result<()> {
|
||||
if self.ignore_tls_errors {
|
||||
info!("TLS errors will be ignored");
|
||||
}
|
||||
|
||||
let mut openssl_conf = self.prepare_env()?;
|
||||
|
||||
self.server = normalize_server(&self.server)?;
|
||||
let gp_params = self.build_gp_params();
|
||||
|
||||
// Get the initial SAML request
|
||||
let saml_request = match self.saml_request {
|
||||
Some(ref saml_request) => saml_request.clone(),
|
||||
None => portal_prelogin(&self.server, &self.user_agent).await?,
|
||||
None => portal_prelogin(&self.server, &gp_params).await?,
|
||||
};
|
||||
|
||||
self.saml_request.replace(saml_request);
|
||||
@@ -82,10 +96,22 @@ impl Cli {
|
||||
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> {
|
||||
let auth_window = AuthWindow::new(app_handle)
|
||||
.server(&self.server)
|
||||
.user_agent(&self.user_agent)
|
||||
.gp_params(self.build_gp_params())
|
||||
.saml_request(self.saml_request.as_ref().unwrap())
|
||||
.clean(self.clean);
|
||||
|
||||
|
@@ -6,7 +6,7 @@ edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
gpapi = { path = "../../crates/gpapi" }
|
||||
gpapi = { path = "../../crates/gpapi", features = ["clap"] }
|
||||
openconnect = { path = "../../crates/openconnect" }
|
||||
anyhow.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)]
|
||||
enum CliCommand {
|
||||
#[command(about = "Connect to a portal server")]
|
||||
@@ -40,6 +45,8 @@ enum CliCommand {
|
||||
{usage-heading} {usage}
|
||||
|
||||
{all-args}{after-help}
|
||||
|
||||
See 'gpclient help <command>' for more information on a specific command.
|
||||
"
|
||||
)]
|
||||
struct Cli {
|
||||
@@ -51,6 +58,8 @@ struct Cli {
|
||||
help = "Get around the OpenSSL `unsafe legacy renegotiation` error"
|
||||
)]
|
||||
fix_openssl: bool,
|
||||
#[arg(long, help = "Ignore the TLS errors")]
|
||||
ignore_tls_errors: bool,
|
||||
}
|
||||
|
||||
impl Cli {
|
||||
@@ -67,9 +76,17 @@ impl Cli {
|
||||
// The temp file will be dropped automatically when the file handle is dropped
|
||||
// So, declare it here to ensure it's not dropped
|
||||
let _file = self.fix_openssl()?;
|
||||
let shared_args = SharedArgs {
|
||||
fix_openssl: self.fix_openssl,
|
||||
ignore_tls_errors: self.ignore_tls_errors,
|
||||
};
|
||||
|
||||
if self.ignore_tls_errors {
|
||||
info!("TLS errors will be ignored");
|
||||
}
|
||||
|
||||
match &self.command {
|
||||
CliCommand::Connect(args) => ConnectHandler::new(args, self.fix_openssl).handle().await,
|
||||
CliCommand::Connect(args) => ConnectHandler::new(args, &shared_args).handle().await,
|
||||
CliCommand::Disconnect => DisconnectHandler::new().handle(),
|
||||
CliCommand::LaunchGui(args) => LaunchGuiHandler::new(args).handle().await,
|
||||
}
|
||||
@@ -89,13 +106,24 @@ pub(crate) async fn run() {
|
||||
if let Err(err) = cli.run().await {
|
||||
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");
|
||||
// Print the command
|
||||
let args = std::env::args().collect::<Vec<_>>();
|
||||
eprintln!("{} --fix-openssl {}\n", args[0], args[1..].join(" "));
|
||||
}
|
||||
|
||||
if err.contains("certificate verify failed") {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@@ -2,9 +2,10 @@ use std::{fs, sync::Arc};
|
||||
|
||||
use clap::Args;
|
||||
use gpapi::{
|
||||
clap::args::Os,
|
||||
credential::{Credential, PasswordCredential},
|
||||
gateway::gateway_login,
|
||||
gp_params::GpParams,
|
||||
gp_params::{ClientOs, GpParams},
|
||||
portal::{prelogin, retrieve_config, Prelogin},
|
||||
process::auth_launcher::SamlAuthLauncher,
|
||||
utils::{self, shutdown_signal},
|
||||
@@ -14,7 +15,7 @@ use inquire::{Password, PasswordDisplayMode, Select, Text};
|
||||
use log::info;
|
||||
use openconnect::Vpn;
|
||||
|
||||
use crate::GP_CLIENT_LOCK_FILE;
|
||||
use crate::{cli::SharedArgs, GP_CLIENT_LOCK_FILE};
|
||||
|
||||
#[derive(Args)]
|
||||
pub(crate) struct ConnectArgs {
|
||||
@@ -36,20 +37,38 @@ pub(crate) struct ConnectArgs {
|
||||
script: Option<String>,
|
||||
#[arg(long, default_value = GP_USER_AGENT, help = "The user agent to use")]
|
||||
user_agent: String,
|
||||
#[arg(long, default_value = "Linux")]
|
||||
os: Os,
|
||||
#[arg(long)]
|
||||
os_version: Option<String>,
|
||||
#[arg(long, help = "The HiDPI mode, useful for high resolution screens")]
|
||||
hidpi: bool,
|
||||
#[arg(long, help = "Do not reuse the remembered authentication cookie")]
|
||||
clean: bool,
|
||||
}
|
||||
|
||||
impl ConnectArgs {
|
||||
fn os_version(&self) -> String {
|
||||
if let Some(os_version) = &self.os_version {
|
||||
return os_version.to_owned();
|
||||
}
|
||||
|
||||
match self.os {
|
||||
Os::Linux => format!("Linux {}", whoami::distro()),
|
||||
Os::Windows => String::from("Microsoft Windows 11 Pro , 64-bit"),
|
||||
Os::Mac => String::from("Apple Mac OS X 13.4.0"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ConnectHandler<'a> {
|
||||
args: &'a ConnectArgs,
|
||||
fix_openssl: bool,
|
||||
shared_args: &'a SharedArgs,
|
||||
}
|
||||
|
||||
impl<'a> ConnectHandler<'a> {
|
||||
pub(crate) fn new(args: &'a ConnectArgs, fix_openssl: bool) -> Self {
|
||||
Self { args, fix_openssl }
|
||||
pub(crate) fn new(args: &'a ConnectArgs, shared_args: &'a SharedArgs) -> Self {
|
||||
Self { args, shared_args }
|
||||
}
|
||||
|
||||
pub(crate) async fn handle(&self) -> anyhow::Result<()> {
|
||||
@@ -57,9 +76,12 @@ impl<'a> ConnectHandler<'a> {
|
||||
|
||||
let gp_params = 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();
|
||||
|
||||
let prelogin = prelogin(&portal, &self.args.user_agent).await?;
|
||||
let prelogin = prelogin(&portal, &gp_params).await?;
|
||||
let portal_credential = self.obtain_portal_credential(&prelogin).await?;
|
||||
let mut portal_config = retrieve_config(&portal, &portal_credential, &gp_params).await?;
|
||||
|
||||
@@ -114,10 +136,13 @@ impl<'a> ConnectHandler<'a> {
|
||||
match prelogin {
|
||||
Prelogin::Saml(prelogin) => {
|
||||
SamlAuthLauncher::new(&self.args.server)
|
||||
.user_agent(&self.args.user_agent)
|
||||
.saml_request(prelogin.saml_request())
|
||||
.user_agent(&self.args.user_agent)
|
||||
.os(self.args.os.as_str())
|
||||
.os_version(Some(&self.args.os_version()))
|
||||
.hidpi(self.args.hidpi)
|
||||
.fix_openssl(self.fix_openssl)
|
||||
.fix_openssl(self.shared_args.fix_openssl)
|
||||
.ignore_tls_errors(self.shared_args.ignore_tls_errors)
|
||||
.clean(self.args.clean)
|
||||
.launch()
|
||||
.await
|
||||
|
@@ -10,7 +10,12 @@ use log::info;
|
||||
|
||||
#[derive(Args)]
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -30,6 +35,12 @@ impl<'a> LaunchGuiHandler<'a> {
|
||||
anyhow::bail!("`launch-gui` cannot be run as root");
|
||||
}
|
||||
|
||||
let auth_data = self.args.auth_data.as_deref().unwrap_or_default();
|
||||
if !auth_data.is_empty() {
|
||||
// Process the authentication data, its format is `globalprotectcallback:<data>`
|
||||
return feed_auth_data(auth_data).await;
|
||||
}
|
||||
|
||||
if try_active_gui().await.is_ok() {
|
||||
info!("The GUI is already running");
|
||||
return Ok(());
|
||||
@@ -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<()> {
|
||||
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;
|
||||
}
|
||||
|
||||
pub(crate) async fn auth_data(
|
||||
State(ctx): State<Arc<WsServerContext>>,
|
||||
body: String,
|
||||
) -> impl IntoResponse {
|
||||
ctx.send_event(WsEvent::AuthData(body)).await;
|
||||
}
|
||||
|
||||
pub(crate) async fn ws_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
State(ctx): State<Arc<WsServerContext>>,
|
||||
|
@@ -8,6 +8,7 @@ pub(crate) fn routes(ctx: Arc<WsServerContext>) -> Router {
|
||||
Router::new()
|
||||
.route("/health", get(handlers::health))
|
||||
.route("/active-gui", post(handlers::active_gui))
|
||||
.route("/auth-data", post(handlers::auth_data))
|
||||
.route("/ws", get(handlers::ws_handler))
|
||||
.with_state(ctx)
|
||||
}
|
||||
|
@@ -27,6 +27,10 @@ dotenvy_macro.workspace = true
|
||||
uzers.workspace = true
|
||||
|
||||
tauri = { workspace = true, optional = true }
|
||||
clap = { workspace = true, optional = true }
|
||||
open = { version = "5", optional = true }
|
||||
|
||||
[features]
|
||||
tauri = ["dep:tauri"]
|
||||
clap = ["dep:clap"]
|
||||
browser-auth = ["dep:open"]
|
||||
|
@@ -1,3 +1,5 @@
|
||||
use anyhow::bail;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[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 {
|
||||
&self.username
|
||||
}
|
||||
@@ -61,3 +89,10 @@ impl SamlAuthData {
|
||||
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 specta::Type;
|
||||
|
||||
use crate::auth::SamlAuthData;
|
||||
use crate::{auth::SamlAuthData, utils::base64::decode_to_string};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Type, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -151,6 +151,17 @@ pub enum 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 {
|
||||
match self {
|
||||
Credential::Password(cred) => cred.username(),
|
||||
@@ -164,31 +175,34 @@ impl Credential {
|
||||
let mut params = HashMap::new();
|
||||
params.insert("user", self.username());
|
||||
|
||||
match self {
|
||||
Credential::Password(cred) => {
|
||||
params.insert("passwd", cred.password());
|
||||
}
|
||||
Credential::PreloginCookie(cred) => {
|
||||
params.insert("prelogin-cookie", cred.prelogin_cookie());
|
||||
}
|
||||
Credential::AuthCookie(cred) => {
|
||||
params.insert("portal-userauthcookie", cred.user_auth_cookie());
|
||||
params.insert(
|
||||
"portal-prelogonuserauthcookie",
|
||||
cred.prelogon_user_auth_cookie(),
|
||||
);
|
||||
}
|
||||
Credential::CachedCredential(cred) => {
|
||||
if let Some(password) = cred.password() {
|
||||
params.insert("passwd", password);
|
||||
}
|
||||
params.insert("portal-userauthcookie", cred.auth_cookie.user_auth_cookie());
|
||||
params.insert(
|
||||
"portal-prelogonuserauthcookie",
|
||||
cred.auth_cookie.prelogon_user_auth_cookie(),
|
||||
);
|
||||
}
|
||||
}
|
||||
let (passwd, prelogin_cookie, portal_userauthcookie, portal_prelogonuserauthcookie) = match self
|
||||
{
|
||||
Credential::Password(cred) => (Some(cred.password()), None, None, None),
|
||||
Credential::PreloginCookie(cred) => (None, Some(cred.prelogin_cookie()), None, None),
|
||||
Credential::AuthCookie(cred) => (
|
||||
None,
|
||||
None,
|
||||
Some(cred.user_auth_cookie()),
|
||||
Some(cred.prelogon_user_auth_cookie()),
|
||||
),
|
||||
Credential::CachedCredential(cred) => (
|
||||
cred.password(),
|
||||
None,
|
||||
Some(cred.auth_cookie.user_auth_cookie()),
|
||||
Some(cred.auth_cookie.prelogon_user_auth_cookie()),
|
||||
),
|
||||
};
|
||||
|
||||
params.insert("passwd", passwd.unwrap_or_default());
|
||||
params.insert("prelogin-cookie", prelogin_cookie.unwrap_or_default());
|
||||
params.insert(
|
||||
"portal-userauthcookie",
|
||||
portal_userauthcookie.unwrap_or_default(),
|
||||
);
|
||||
params.insert(
|
||||
"portal-prelogonuserauthcookie",
|
||||
portal_prelogonuserauthcookie.unwrap_or_default(),
|
||||
);
|
||||
|
||||
params
|
||||
}
|
||||
|
@@ -23,14 +23,8 @@ pub async fn gateway_login(
|
||||
|
||||
info!("Gateway login, user_agent: {}", gp_params.user_agent());
|
||||
|
||||
let res_xml = client
|
||||
.post(&login_url)
|
||||
.form(¶ms)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.text()
|
||||
.await?;
|
||||
let res = client.post(&login_url).form(¶ms).send().await?;
|
||||
let res_xml = res.error_for_status()?.text().await?;
|
||||
|
||||
let doc = Document::parse(&res_xml)?;
|
||||
|
||||
|
@@ -7,23 +7,32 @@ use crate::GP_USER_AGENT;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Type, Default)]
|
||||
pub enum ClientOs {
|
||||
Linux,
|
||||
#[default]
|
||||
Linux,
|
||||
Windows,
|
||||
Mac,
|
||||
}
|
||||
|
||||
impl From<&ClientOs> for &str {
|
||||
fn from(os: &ClientOs) -> Self {
|
||||
impl From<&str> for ClientOs {
|
||||
fn from(os: &str) -> Self {
|
||||
match os {
|
||||
ClientOs::Linux => "Linux",
|
||||
ClientOs::Windows => "Windows",
|
||||
ClientOs::Mac => "Mac",
|
||||
"Linux" => ClientOs::Linux,
|
||||
"Windows" => ClientOs::Windows,
|
||||
"Mac" => ClientOs::Mac,
|
||||
_ => ClientOs::Linux,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientOs {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
ClientOs::Linux => "Linux",
|
||||
ClientOs::Windows => "Windows",
|
||||
ClientOs::Mac => "Mac",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_openconnect_os(&self) -> &str {
|
||||
match self {
|
||||
ClientOs::Linux => "linux",
|
||||
@@ -39,7 +48,9 @@ pub struct GpParams {
|
||||
client_os: ClientOs,
|
||||
os_version: Option<String>,
|
||||
client_version: Option<String>,
|
||||
computer: Option<String>,
|
||||
computer: String,
|
||||
ignore_tls_errors: bool,
|
||||
prefer_default_browser: bool,
|
||||
}
|
||||
|
||||
impl GpParams {
|
||||
@@ -52,15 +63,20 @@ impl GpParams {
|
||||
}
|
||||
|
||||
pub(crate) fn computer(&self) -> &str {
|
||||
match self.computer {
|
||||
Some(ref computer) => computer,
|
||||
None => (&self.client_os).into()
|
||||
}
|
||||
&self.computer
|
||||
}
|
||||
|
||||
pub fn ignore_tls_errors(&self) -> bool {
|
||||
self.ignore_tls_errors
|
||||
}
|
||||
|
||||
pub fn prefer_default_browser(&self) -> bool {
|
||||
self.prefer_default_browser
|
||||
}
|
||||
|
||||
pub(crate) fn to_params(&self) -> HashMap<&str, &str> {
|
||||
let mut params: HashMap<&str, &str> = HashMap::new();
|
||||
let client_os: &str = (&self.client_os).into();
|
||||
let client_os = self.client_os.as_str();
|
||||
|
||||
// Common params
|
||||
params.insert("prot", "https:");
|
||||
@@ -70,22 +86,17 @@ impl GpParams {
|
||||
params.insert("ipv6-support", "yes");
|
||||
params.insert("inputStr", "");
|
||||
params.insert("clientVer", "4100");
|
||||
|
||||
params.insert("clientos", client_os);
|
||||
|
||||
if let Some(computer) = &self.computer {
|
||||
params.insert("computer", computer);
|
||||
} else {
|
||||
params.insert("computer", client_os);
|
||||
}
|
||||
params.insert("computer", &self.computer);
|
||||
|
||||
if let Some(os_version) = &self.os_version {
|
||||
params.insert("os-version", os_version);
|
||||
}
|
||||
|
||||
if let Some(client_version) = &self.client_version {
|
||||
params.insert("clientgpversion", client_version);
|
||||
}
|
||||
// NOTE: Do not include clientgpversion for now
|
||||
// if let Some(client_version) = &self.client_version {
|
||||
// params.insert("clientgpversion", client_version);
|
||||
// }
|
||||
|
||||
params
|
||||
}
|
||||
@@ -96,7 +107,9 @@ pub struct GpParamsBuilder {
|
||||
client_os: ClientOs,
|
||||
os_version: Option<String>,
|
||||
client_version: Option<String>,
|
||||
computer: Option<String>,
|
||||
computer: String,
|
||||
ignore_tls_errors: bool,
|
||||
prefer_default_browser: bool,
|
||||
}
|
||||
|
||||
impl GpParamsBuilder {
|
||||
@@ -106,7 +119,9 @@ impl GpParamsBuilder {
|
||||
client_os: ClientOs::Linux,
|
||||
os_version: Default::default(),
|
||||
client_version: Default::default(),
|
||||
computer: Default::default(),
|
||||
computer: whoami::hostname(),
|
||||
ignore_tls_errors: false,
|
||||
prefer_default_browser: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,18 +135,28 @@ impl GpParamsBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn os_version(&mut self, os_version: &str) -> &mut Self {
|
||||
self.os_version = Some(os_version.to_string());
|
||||
pub fn os_version<T: Into<Option<String>>>(&mut self, os_version: T) -> &mut Self {
|
||||
self.os_version = os_version.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn client_version(&mut self, client_version: &str) -> &mut Self {
|
||||
self.client_version = Some(client_version.to_string());
|
||||
pub fn client_version<T: Into<Option<String>>>(&mut self, client_version: T) -> &mut Self {
|
||||
self.client_version = client_version.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn computer(&mut self, computer: &str) -> &mut Self {
|
||||
self.computer = 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
|
||||
}
|
||||
|
||||
@@ -142,6 +167,8 @@ impl GpParamsBuilder {
|
||||
os_version: self.os_version.clone(),
|
||||
client_version: self.client_version.clone(),
|
||||
computer: self.computer.clone(),
|
||||
ignore_tls_errors: self.ignore_tls_errors,
|
||||
prefer_default_browser: self.prefer_default_browser,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -7,12 +7,17 @@ pub mod process;
|
||||
pub mod service;
|
||||
pub mod utils;
|
||||
|
||||
#[cfg(feature = "clap")]
|
||||
pub mod clap;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub const GP_API_KEY: &[u8; 32] = &[0; 32];
|
||||
|
||||
pub const GP_USER_AGENT: &str = "PAN GlobalProtect";
|
||||
pub const GP_SERVICE_LOCK_FILE: &str = "/var/run/gpservice.lock";
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
pub const GP_CLIENT_BINARY: &str = "/usr/bin/gpclient";
|
||||
#[cfg(not(debug_assertions))]
|
||||
pub const GP_SERVICE_BINARY: &str = "/usr/bin/gpservice";
|
||||
#[cfg(not(debug_assertions))]
|
||||
@@ -20,6 +25,8 @@ pub const GP_GUI_BINARY: &str = "/usr/bin/gpgui";
|
||||
#[cfg(not(debug_assertions))]
|
||||
pub(crate) const GP_AUTH_BINARY: &str = "/usr/bin/gpauth";
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub const GP_CLIENT_BINARY: &str = dotenvy_macro::dotenv!("GP_CLIENT_BINARY");
|
||||
#[cfg(debug_assertions)]
|
||||
pub const GP_SERVICE_BINARY: &str = dotenvy_macro::dotenv!("GP_SERVICE_BINARY");
|
||||
#[cfg(debug_assertions)]
|
||||
|
@@ -132,14 +132,8 @@ pub async fn retrieve_config(
|
||||
|
||||
info!("Portal config, user_agent: {}", gp_params.user_agent());
|
||||
|
||||
let res_xml = client
|
||||
.post(&url)
|
||||
.form(¶ms)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.text()
|
||||
.await?;
|
||||
let res = client.post(&url).form(¶ms).send().await?;
|
||||
let res_xml = res.error_for_status()?.text().await?;
|
||||
|
||||
ensure!(!res_xml.is_empty(), PortalConfigError::EmptyResponse);
|
||||
|
||||
|
@@ -5,13 +5,28 @@ use roxmltree::Document;
|
||||
use serde::Serialize;
|
||||
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)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SamlPrelogin {
|
||||
region: String,
|
||||
saml_request: String,
|
||||
support_default_browser: bool,
|
||||
}
|
||||
|
||||
impl SamlPrelogin {
|
||||
@@ -22,6 +37,10 @@ impl SamlPrelogin {
|
||||
pub fn saml_request(&self) -> &str {
|
||||
&self.saml_request
|
||||
}
|
||||
|
||||
pub fn support_default_browser(&self) -> bool {
|
||||
self.support_default_browser
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Type, Clone)]
|
||||
@@ -67,20 +86,33 @@ 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);
|
||||
|
||||
let portal = normalize_server(portal)?;
|
||||
let prelogin_url = format!("{}/global-protect/prelogin.esp", portal);
|
||||
let client = Client::builder().user_agent(user_agent).build()?;
|
||||
let mut params = gp_params.to_params();
|
||||
|
||||
let res_xml = client
|
||||
.get(&prelogin_url)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.text()
|
||||
.await?;
|
||||
params.insert("tmp", "tmp");
|
||||
params.insert("cas-support", "yes");
|
||||
if gp_params.prefer_default_browser() {
|
||||
params.insert("default-browser", "1");
|
||||
}
|
||||
|
||||
params.retain(|k, _| {
|
||||
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);
|
||||
let doc = Document::parse(&res_xml)?;
|
||||
@@ -98,12 +130,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_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
|
||||
if saml_method.is_some() && saml_request.is_some() {
|
||||
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 {
|
||||
region,
|
||||
saml_request,
|
||||
support_default_browser,
|
||||
};
|
||||
|
||||
return Ok(Prelogin::Saml(saml_prelogin));
|
||||
|
@@ -8,10 +8,13 @@ use super::command_traits::CommandExt;
|
||||
|
||||
pub struct SamlAuthLauncher<'a> {
|
||||
server: &'a str,
|
||||
user_agent: Option<&'a str>,
|
||||
saml_request: Option<&'a str>,
|
||||
user_agent: Option<&'a str>,
|
||||
os: Option<&'a str>,
|
||||
os_version: Option<&'a str>,
|
||||
hidpi: bool,
|
||||
fix_openssl: bool,
|
||||
ignore_tls_errors: bool,
|
||||
clean: bool,
|
||||
}
|
||||
|
||||
@@ -19,21 +22,34 @@ impl<'a> SamlAuthLauncher<'a> {
|
||||
pub fn new(server: &'a str) -> Self {
|
||||
Self {
|
||||
server,
|
||||
user_agent: None,
|
||||
saml_request: None,
|
||||
user_agent: None,
|
||||
os: None,
|
||||
os_version: None,
|
||||
hidpi: false,
|
||||
fix_openssl: false,
|
||||
ignore_tls_errors: 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 {
|
||||
self.user_agent = Some(user_agent);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn saml_request(mut self, saml_request: &'a str) -> Self {
|
||||
self.saml_request = Some(saml_request);
|
||||
pub fn os(mut self, os: &'a str) -> Self {
|
||||
self.os = Some(os);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn os_version(mut self, os_version: Option<&'a str>) -> Self {
|
||||
self.os_version = os_version;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -47,6 +63,11 @@ impl<'a> SamlAuthLauncher<'a> {
|
||||
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 {
|
||||
self.clean = clean;
|
||||
self
|
||||
@@ -57,20 +78,32 @@ impl<'a> SamlAuthLauncher<'a> {
|
||||
let mut auth_cmd = Command::new(GP_AUTH_BINARY);
|
||||
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 {
|
||||
auth_cmd.arg("--user-agent").arg(user_agent);
|
||||
}
|
||||
|
||||
if let Some(saml_request) = self.saml_request {
|
||||
auth_cmd.arg("--saml-request").arg(saml_request);
|
||||
if let Some(os) = self.os {
|
||||
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 {
|
||||
auth_cmd.arg("--fix-openssl");
|
||||
}
|
||||
|
||||
if self.hidpi {
|
||||
auth_cmd.arg("--hidpi");
|
||||
if self.ignore_tls_errors {
|
||||
auth_cmd.arg("--ignore-tls-errors");
|
||||
}
|
||||
|
||||
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,5 +1,7 @@
|
||||
pub(crate) mod command_traits;
|
||||
|
||||
pub mod auth_launcher;
|
||||
#[cfg(feature = "browser-auth")]
|
||||
pub mod browser_authenticator;
|
||||
pub mod gui_launcher;
|
||||
pub mod service_launcher;
|
||||
|
@@ -7,4 +7,6 @@ use super::vpn_state::VpnState;
|
||||
pub enum WsEvent {
|
||||
VpnState(VpnState),
|
||||
ActiveGui,
|
||||
/// External authentication data
|
||||
AuthData(String),
|
||||
}
|
||||
|
@@ -27,7 +27,6 @@ pub fn raise_window(win: &Window) -> anyhow::Result<()> {
|
||||
}
|
||||
let title = win.title()?;
|
||||
tokio::spawn(async move {
|
||||
info!("Raising window: {}", title);
|
||||
if let Err(err) = wmctrl_raise_window(&title).await {
|
||||
warn!("Failed to raise window: {}", err);
|
||||
}
|
||||
|
Reference in New Issue
Block a user