Enhancements and Bug Fixes: Align Pre-login Behavior, TLS Error Ignorance, GUI Auto-Launch, and Documentation Improvements (#291)

This commit is contained in:
Kevin Yue 2024-01-21 10:43:47 +08:00 committed by GitHub
parent 03f8c98cb5
commit 8bc4049a0f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 323 additions and 204 deletions

View File

@ -6,8 +6,8 @@ on:
- "*.md" - "*.md"
- .vscode - .vscode
- .devcontainer - .devcontainer
# branches: branches:
# - main - main
# tags: # tags:
# - v*.*.* # - v*.*.*
jobs: jobs:
@ -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/*

15
Cargo.lock generated
View File

@ -1428,6 +1428,7 @@ dependencies = [
"anyhow", "anyhow",
"base64 0.21.5", "base64 0.21.5",
"chacha20poly1305", "chacha20poly1305",
"clap",
"dotenvy_macro", "dotenvy_macro",
"log", "log",
"redact-engine", "redact-engine",
@ -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",
@ -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",

View File

@ -33,11 +33,12 @@ Commands:
Options: Options:
--fix-openssl Get around the OpenSSL `unsafe legacy renegotiation` error --fix-openssl Get around the OpenSSL `unsafe legacy renegotiation` error
--ignore-tls-errors Ignore the TLS errors
-h, --help Print help -h, --help Print help
-V, --version Print version -V, --version Print version
```
See `gpclient -h` for help. See 'gpclient help <command>' for more information on a specific command.
```
### GUI ### GUI
@ -49,6 +50,10 @@ The GUI version is also available after you installed it. You can launch it from
## Installation ## 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.
### Debian/Ubuntu based distributions ### Debian/Ubuntu based distributions
#### Install from PPA #### Install from PPA
@ -104,10 +109,6 @@ Download the latest RPM package from [releases](https://github.com/yuezk/GlobalP
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. 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.
## [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, SettingsExt, URIResponse, URIResponseExt, WebContextExt, WebResource, WebResourceExt, LoadEvent, SettingsExt, TLSErrorsPolicy, URIResponse, URIResponseExt, WebContextExt, WebResource,
WebView, 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
@ -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,10 @@ 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() { if let Some(settings) = wv.settings() {
let ua = settings.user_agent().unwrap_or("".into()); let ua = settings.user_agent().unwrap_or("".into());
info!("Auth window user agent: {}", ua); info!("Auth window user agent: {}", ua);
@ -168,12 +188,15 @@ 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
}); });
@ -187,12 +210,14 @@ impl<'a> AuthWindow<'a> {
})?; })?;
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");
@ -237,7 +262,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);
@ -258,9 +283,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")),
} }
@ -397,6 +423,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,6 +37,10 @@ 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")]
@ -44,12 +49,12 @@ pub(crate) struct ConnectArgs {
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 +62,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.clone())
.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 +122,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(self.args.os_version.as_deref())
.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

@ -27,6 +27,8 @@ dotenvy_macro.workspace = true
uzers.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

@ -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",
@ -40,6 +49,7 @@ pub struct GpParams {
os_version: Option<String>, os_version: Option<String>,
client_version: Option<String>, client_version: Option<String>,
computer: Option<String>, computer: Option<String>,
ignore_tls_errors: bool,
} }
impl GpParams { impl GpParams {
@ -54,13 +64,17 @@ impl GpParams {
pub(crate) fn computer(&self) -> &str { pub(crate) fn computer(&self) -> &str {
match self.computer { match self.computer {
Some(ref computer) => computer, Some(ref computer) => computer,
None => (&self.client_os).into() None => self.client_os.as_str(),
} }
} }
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:");
@ -97,6 +111,7 @@ pub struct GpParamsBuilder {
os_version: Option<String>, os_version: Option<String>,
client_version: Option<String>, client_version: Option<String>,
computer: Option<String>, computer: Option<String>,
ignore_tls_errors: bool,
} }
impl GpParamsBuilder { impl GpParamsBuilder {
@ -107,6 +122,7 @@ impl GpParamsBuilder {
os_version: Default::default(), os_version: Default::default(),
client_version: Default::default(), client_version: Default::default(),
computer: Default::default(), computer: Default::default(),
ignore_tls_errors: false,
} }
} }
@ -120,13 +136,13 @@ 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
} }
@ -135,6 +151,11 @@ impl GpParamsBuilder {
self self
} }
pub fn ignore_tls_errors(&mut self, ignore_tls_errors: bool) -> &mut Self {
self.ignore_tls_errors = ignore_tls_errors;
self
}
pub fn build(&self) -> GpParams { pub fn build(&self) -> GpParams {
GpParams { GpParams {
user_agent: self.user_agent.clone(), user_agent: self.user_agent.clone(),
@ -142,6 +163,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

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

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