From 8bc4049a0ff6b682d1281701719fe14a7fa0f7f3 Mon Sep 17 00:00:00 2001 From: Kevin Yue Date: Sun, 21 Jan 2024 10:43:47 +0800 Subject: [PATCH] Enhancements and Bug Fixes: Align Pre-login Behavior, TLS Error Ignorance, GUI Auto-Launch, and Documentation Improvements (#291) --- .github/workflows/build.yaml | 138 +--------------------- Cargo.lock | 15 +-- README.md | 19 +-- apps/gpauth/Cargo.toml | 2 +- apps/gpauth/src/auth_window.rs | 45 +++++-- apps/gpauth/src/cli.rs | 28 ++++- apps/gpclient/Cargo.toml | 2 +- apps/gpclient/src/cli.rs | 32 ++++- apps/gpclient/src/connect.rs | 27 +++-- crates/gpapi/Cargo.toml | 2 + crates/gpapi/src/clap/args.rs | 64 ++++++++++ crates/gpapi/src/clap/mod.rs | 1 + crates/gpapi/src/gp_params.rs | 46 ++++++-- crates/gpapi/src/lib.rs | 7 ++ crates/gpapi/src/portal/prelogin.rs | 49 ++++++-- crates/gpapi/src/process/auth_launcher.rs | 49 ++++++-- crates/gpapi/src/utils/window.rs | 1 - 17 files changed, 323 insertions(+), 204 deletions(-) create mode 100644 crates/gpapi/src/clap/args.rs create mode 100644 crates/gpapi/src/clap/mod.rs diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index d2ee606..a400273 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -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/* diff --git a/Cargo.lock b/Cargo.lock index fab2080..178848a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1428,6 +1428,7 @@ dependencies = [ "anyhow", "base64 0.21.5", "chacha20poly1305", + "clap", "dotenvy_macro", "log", "redact-engine", @@ -1564,9 +1565,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 +1584,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 +1744,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 +1767,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", @@ -3070,7 +3071,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", diff --git a/README.md b/README.md index 0e71c8c..02f783f 100644 --- a/README.md +++ b/README.md @@ -32,12 +32,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 ' for more information on a specific command. +``` ### GUI @@ -49,6 +50,10 @@ 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. + ### Debian/Ubuntu based distributions #### 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. -### 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) GPLv3 diff --git a/apps/gpauth/Cargo.toml b/apps/gpauth/Cargo.toml index f429259..92f473d 100644 --- a/apps/gpauth/Cargo.toml +++ b/apps/gpauth/Cargo.toml @@ -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 diff --git a/apps/gpauth/src/auth_window.rs b/apps/gpauth/src/auth_window.rs index b335f4f..777c75c 100644 --- a/apps/gpauth/src/auth_window.rs +++ b/apps/gpauth/src/auth_window.rs @@ -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, 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::(); let raise_window_cancel_token: Arc>> = 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,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); warn!( "Failed to load uri: {} with error: {}, cert: {}", redacted_uri, err, cert ); + + send_auth_result(&auth_result_tx_clone, Err(AuthDataError::TlsError)); true }); @@ -187,12 +210,14 @@ impl<'a> AuthWindow<'a> { })?; 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 +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| { let wv = wv.inner(); load_saml_request(&wv, &saml_request); @@ -258,9 +283,10 @@ fn raise_window(window: &Arc) { } } -pub(crate) async fn portal_prelogin(portal: &str, user_agent: &str) -> anyhow::Result { +pub(crate) async fn portal_prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result { 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 +423,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)); + } } } diff --git a/apps/gpauth/src/cli.rs b/apps/gpauth/src/cli.rs index 9caf6a2..ef4c49d 100644 --- a/apps/gpauth/src/cli.rs +++ b/apps/gpauth/src/cli.rs @@ -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, #[arg(long, default_value = GP_USER_AGENT)] user_agent: String, + #[arg(long, default_value = "Linux")] + os: Os, + #[arg(long)] + os_version: Option, #[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 { 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); diff --git a/apps/gpclient/Cargo.toml b/apps/gpclient/Cargo.toml index 6e38bcc..da76af4 100644 --- a/apps/gpclient/Cargo.toml +++ b/apps/gpclient/Cargo.toml @@ -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 diff --git a/apps/gpclient/src/cli.rs b/apps/gpclient/src/cli.rs index b1475a2..00c0a5d 100644 --- a/apps/gpclient/src/cli.rs +++ b/apps/gpclient/src/cli.rs @@ -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 ' 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::>(); 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::>(); + eprintln!("{} --ignore-tls-errors {}\n", args[0], args[1..].join(" ")); + } + std::process::exit(1); } } diff --git a/apps/gpclient/src/connect.rs b/apps/gpclient/src/connect.rs index 8a02d19..c658e1a 100644 --- a/apps/gpclient/src/connect.rs +++ b/apps/gpclient/src/connect.rs @@ -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,6 +37,10 @@ pub(crate) struct ConnectArgs { script: Option, #[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, #[arg(long, help = "The HiDPI mode, useful for high resolution screens")] hidpi: bool, #[arg(long, help = "Do not reuse the remembered authentication cookie")] @@ -44,12 +49,12 @@ pub(crate) struct ConnectArgs { 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 +62,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.clone()) + .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 +122,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(self.args.os_version.as_deref()) .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 diff --git a/crates/gpapi/Cargo.toml b/crates/gpapi/Cargo.toml index 5df5b26..7e8ab92 100644 --- a/crates/gpapi/Cargo.toml +++ b/crates/gpapi/Cargo.toml @@ -27,6 +27,8 @@ dotenvy_macro.workspace = true uzers.workspace = true tauri = { workspace = true, optional = true } +clap = { workspace = true, optional = true } [features] tauri = ["dep:tauri"] +clap = ["dep:clap"] diff --git a/crates/gpapi/src/clap/args.rs b/crates/gpapi/src/clap/args.rs new file mode 100644 index 0000000..4ee0829 --- /dev/null +++ b/crates/gpapi/src/clap/args.rs @@ -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 { + 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 { + match input.to_lowercase().as_str() { + "linux" => Ok(Os::Linux), + "windows" => Ok(Os::Windows), + "mac" => Ok(Os::Mac), + _ => Err(format!("Invalid OS: {}", input)), + } + } +} diff --git a/crates/gpapi/src/clap/mod.rs b/crates/gpapi/src/clap/mod.rs new file mode 100644 index 0000000..6e10f4a --- /dev/null +++ b/crates/gpapi/src/clap/mod.rs @@ -0,0 +1 @@ +pub mod args; diff --git a/crates/gpapi/src/gp_params.rs b/crates/gpapi/src/gp_params.rs index 770ec4b..039f6b1 100644 --- a/crates/gpapi/src/gp_params.rs +++ b/crates/gpapi/src/gp_params.rs @@ -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", @@ -40,6 +49,7 @@ pub struct GpParams { os_version: Option, client_version: Option, computer: Option, + ignore_tls_errors: bool, } impl GpParams { @@ -54,13 +64,17 @@ impl GpParams { pub(crate) fn computer(&self) -> &str { match self.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> { 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:"); @@ -97,6 +111,7 @@ pub struct GpParamsBuilder { os_version: Option, client_version: Option, computer: Option, + ignore_tls_errors: bool, } impl GpParamsBuilder { @@ -107,6 +122,7 @@ impl GpParamsBuilder { os_version: Default::default(), client_version: Default::default(), computer: Default::default(), + ignore_tls_errors: false, } } @@ -120,13 +136,13 @@ 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>>(&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>>(&mut self, client_version: T) -> &mut Self { + self.client_version = client_version.into(); self } @@ -135,6 +151,11 @@ impl GpParamsBuilder { 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 { GpParams { user_agent: self.user_agent.clone(), @@ -142,6 +163,7 @@ 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, } } } diff --git a/crates/gpapi/src/lib.rs b/crates/gpapi/src/lib.rs index ea4e001..e413989 100644 --- a/crates/gpapi/src/lib.rs +++ b/crates/gpapi/src/lib.rs @@ -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)] diff --git a/crates/gpapi/src/portal/prelogin.rs b/crates/gpapi/src/portal/prelogin.rs index dbc4cab..a34522d 100644 --- a/crates/gpapi/src/portal/prelogin.rs +++ b/crates/gpapi/src/portal/prelogin.rs @@ -5,7 +5,21 @@ 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")] @@ -67,20 +81,33 @@ impl Prelogin { } } -pub async fn prelogin(portal: &str, user_agent: &str) -> anyhow::Result { +pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result { + 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 prelogin_url = format!( + "{}/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 - .get(&prelogin_url) - .send() - .await? - .error_for_status()? - .text() - .await?; + 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)?; diff --git a/crates/gpapi/src/process/auth_launcher.rs b/crates/gpapi/src/process/auth_launcher.rs index 05389ed..4728482 100644 --- a/crates/gpapi/src/process/auth_launcher.rs +++ b/crates/gpapi/src/process/auth_launcher.rs @@ -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 { diff --git a/crates/gpapi/src/utils/window.rs b/crates/gpapi/src/utils/window.rs index 1b10d41..35393ff 100644 --- a/crates/gpapi/src/utils/window.rs +++ b/crates/gpapi/src/utils/window.rs @@ -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); }