Compare commits

..

10 Commits

Author SHA1 Message Date
Kevin Yue
0b55a80317 Bump version 2.0.0-beta4 2024-01-21 11:05:15 -05:00
Kevin Yue
c6315bf384 Handle auth window auth fail 2024-01-21 11:04:35 -05:00
Kevin Yue
87b965f80c Add default os-version for CLI 2024-01-21 08:54:08 -05:00
Kevin Yue
b09b21ae0f Bump 2.0.0-beta3 2024-01-21 05:43:49 -05:00
Kevin Yue
7e372cd113 Align with the old behavior of the portal config request (#293) 2024-01-21 18:31:39 +08:00
Kevin Yue
1e211e8912 Update README.md 2024-01-20 22:55:35 -05:00
Kevin Yue
8bc4049a0f Enhancements and Bug Fixes: Align Pre-login Behavior, TLS Error Ignorance, GUI Auto-Launch, and Documentation Improvements (#291) 2024-01-21 10:43:47 +08:00
Kevin Yue
03f8c98cb5 Use uzers crate 2024-01-18 08:54:08 -05:00
Kevin Yue
5c56acc677 Bump version 2.0.0-beta2 2024-01-18 08:51:11 -05:00
Kevin Yue
2d8393dcf7 Update doc (#282) 2024-01-18 20:48:40 +08:00
23 changed files with 483 additions and 427 deletions

View File

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

View File

@@ -10,6 +10,7 @@
"dotenv", "dotenv",
"dotenvy", "dotenvy",
"getconfig", "getconfig",
"globalprotect",
"gpapi", "gpapi",
"gpauth", "gpauth",
"gpclient", "gpclient",
@@ -42,10 +43,12 @@
"urlencoding", "urlencoding",
"userauthcookie", "userauthcookie",
"utsbuf", "utsbuf",
"uzers",
"Vite", "Vite",
"vpnc", "vpnc",
"vpninfo", "vpninfo",
"wmctrl", "wmctrl",
"XAUTHORITY" "XAUTHORITY",
"yuezk"
] ]
} }

47
Cargo.lock generated
View File

@@ -1423,11 +1423,12 @@ dependencies = [
[[package]] [[package]]
name = "gpapi" name = "gpapi"
version = "2.0.0-beta.1" version = "2.0.0-beta4"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64 0.21.5", "base64 0.21.5",
"chacha20poly1305", "chacha20poly1305",
"clap",
"dotenvy_macro", "dotenvy_macro",
"log", "log",
"redact-engine", "redact-engine",
@@ -1444,13 +1445,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-beta4"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@@ -1470,7 +1471,7 @@ dependencies = [
[[package]] [[package]]
name = "gpclient" name = "gpclient"
version = "2.0.0-beta.1" version = "2.0.0-beta4"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@@ -1491,7 +1492,7 @@ dependencies = [
[[package]] [[package]]
name = "gpservice" name = "gpservice"
version = "2.0.0-beta.1" version = "2.0.0-beta4"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum", "axum",
@@ -1564,9 +1565,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 +1584,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 +1744,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 +1767,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",
@@ -2446,7 +2447,7 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]] [[package]]
name = "openconnect" name = "openconnect"
version = "2.0.0-beta.1" version = "2.0.0-beta4"
dependencies = [ dependencies = [
"cc", "cc",
"is_executable", "is_executable",
@@ -3070,7 +3071,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 +4379,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 +4400,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"

View File

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

241
README.md
View File

@@ -1,194 +1,127 @@
# 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 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>
[![Arch package](https://repology.org/badge/version-for-repo/arch/globalprotect-openconnect.svg)](https://repology.org/project/globalprotect-openconnect/versions) Commands:
[![AUR package](https://repology.org/badge/version-for-repo/aur/globalprotect-openconnect.svg)](https://repology.org/project/globalprotect-openconnect/versions) connect Connect to a portal server
[![Manjaro Stable package](https://repology.org/badge/version-for-repo/manjaro_stable/globalprotect-openconnect.svg)](https://repology.org/project/globalprotect-openconnect/versions) disconnect Disconnect from the server
[![Manjaro Testing package](https://repology.org/badge/version-for-repo/manjaro_testing/globalprotect-openconnect.svg)](https://repology.org/project/globalprotect-openconnect/versions) launch-gui Launch the GUI
[![Manjaro Unstable package](https://repology.org/badge/version-for-repo/manjaro_unstable/globalprotect-openconnect.svg)](https://repology.org/project/globalprotect-openconnect/versions) help Print this message or the help of the given subcommand(s)
[![nixpkgs unstable package](https://repology.org/badge/version-for-repo/nix_unstable/globalprotect-openconnect.svg)](https://repology.org/project/globalprotect-openconnect/versions)
[![Parabola package](https://repology.org/badge/version-for-repo/parabola/globalprotect-openconnect.svg)](https://repology.org/project/globalprotect-openconnect/versions)
### Linux Mint, Ubuntu 18.04 or later 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,20 +37,38 @@ 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 }
} }
pub(crate) async fn handle(&self) -> anyhow::Result<()> { pub(crate) async fn handle(&self) -> anyhow::Result<()> {
@@ -57,9 +76,12 @@ impl<'a> ConnectHandler<'a> {
let gp_params = GpParams::builder() let gp_params = GpParams::builder()
.user_agent(&self.args.user_agent) .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(); .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 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?;
@@ -114,10 +136,13 @@ impl<'a> ConnectHandler<'a> {
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

View File

@@ -24,9 +24,11 @@ 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 }
[features] [features]
tauri = ["dep:tauri"] tauri = ["dep:tauri"]
clap = ["dep:clap"]

View File

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

View File

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

View File

@@ -164,31 +164,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
} }

View File

@@ -23,14 +23,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(&params).send().await?;
.post(&login_url) let res_xml = res.error_for_status()?.text().await?;
.form(&params)
.send()
.await?
.error_for_status()?
.text()
.await?;
let doc = Document::parse(&res_xml)?; let doc = Document::parse(&res_xml)?;

View File

@@ -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",
@@ -39,7 +48,8 @@ pub struct GpParams {
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,
} }
impl GpParams { impl GpParams {
@@ -52,15 +62,16 @@ impl GpParams {
} }
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(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,14 +81,8 @@ 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);
@@ -96,7 +101,8 @@ pub struct GpParamsBuilder {
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,
} }
impl GpParamsBuilder { impl GpParamsBuilder {
@@ -106,7 +112,8 @@ impl GpParamsBuilder {
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,
} }
} }
@@ -120,18 +127,23 @@ 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 self
} }
@@ -142,6 +154,7 @@ impl GpParamsBuilder {
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,
} }
} }
} }

View File

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

View File

@@ -132,14 +132,8 @@ 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(&params).send().await?;
.post(&url) let res_xml = res.error_for_status()?.text().await?;
.form(&params)
.send()
.await?
.error_for_status()?
.text()
.await?;
ensure!(!res_xml.is_empty(), PortalConfigError::EmptyResponse); ensure!(!res_xml.is_empty(), PortalConfigError::EmptyResponse);

View File

@@ -5,7 +5,21 @@ 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")]
@@ -67,20 +81,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); 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()?; "{}/global-protect/prelogin.esp?kerberos-support=yes",
portal
);
let mut params = gp_params.to_params();
params.insert("tmp", "tmp");
params.insert("default-browser", "0");
params.insert("cas-support", "yes");
let res_xml = client params.retain(|k, _| {
.get(&prelogin_url) REQUIRED_PARAMS
.send() .iter()
.await? .any(|required_param| required_param == k)
.error_for_status()? });
.text()
.await?; 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(&params).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)?;

View File

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

View 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(&current_user) uzers::get_user_by_name(&current_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"))
} }

View File

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