mirror of
				https://github.com/yuezk/GlobalProtect-openconnect.git
				synced 2025-05-20 07:26:58 -04:00 
			
		
		
		
	Enhancements and Bug Fixes: Align Pre-login Behavior, TLS Error Ignorance, GUI Auto-Launch, and Documentation Improvements (#291)
This commit is contained in:
		| @@ -8,7 +8,7 @@ license.workspace = true | ||||
| tauri-build = { version = "1.5", features = [] } | ||||
|  | ||||
| [dependencies] | ||||
| gpapi = { path = "../../crates/gpapi", features = ["tauri"] } | ||||
| gpapi = { path = "../../crates/gpapi", features = ["tauri", "clap"] } | ||||
| anyhow.workspace = true | ||||
| clap.workspace = true | ||||
| env_logger.workspace = true | ||||
|   | ||||
| @@ -7,6 +7,7 @@ use std::{ | ||||
| use anyhow::bail; | ||||
| use gpapi::{ | ||||
|   auth::SamlAuthData, | ||||
|   gp_params::GpParams, | ||||
|   portal::{prelogin, Prelogin}, | ||||
|   utils::{redact::redact_uri, window::WindowExt}, | ||||
| }; | ||||
| @@ -18,11 +19,13 @@ use tokio_util::sync::CancellationToken; | ||||
| use webkit2gtk::{ | ||||
|   gio::Cancellable, | ||||
|   glib::{GString, TimeSpan}, | ||||
|   LoadEvent, SettingsExt, URIResponse, URIResponseExt, WebContextExt, WebResource, WebResourceExt, | ||||
|   WebView, WebViewExt, WebsiteDataManagerExtManual, WebsiteDataTypes, | ||||
|   LoadEvent, SettingsExt, TLSErrorsPolicy, URIResponse, URIResponseExt, WebContextExt, WebResource, | ||||
|   WebResourceExt, WebView, WebViewExt, WebsiteDataManagerExtManual, WebsiteDataTypes, | ||||
| }; | ||||
|  | ||||
| enum AuthDataError { | ||||
|   /// Failed to load page due to TLS error | ||||
|   TlsError, | ||||
|   /// 1. Found auth data in headers/body but it's invalid | ||||
|   /// 2. Loaded an empty page, failed to load page. etc. | ||||
|   Invalid, | ||||
| @@ -37,6 +40,7 @@ pub(crate) struct AuthWindow<'a> { | ||||
|   server: &'a str, | ||||
|   saml_request: &'a str, | ||||
|   user_agent: &'a str, | ||||
|   gp_params: Option<GpParams>, | ||||
|   clean: bool, | ||||
| } | ||||
|  | ||||
| @@ -47,6 +51,7 @@ impl<'a> AuthWindow<'a> { | ||||
|       server: "", | ||||
|       saml_request: "", | ||||
|       user_agent: "", | ||||
|       gp_params: None, | ||||
|       clean: false, | ||||
|     } | ||||
|   } | ||||
| @@ -66,6 +71,11 @@ impl<'a> AuthWindow<'a> { | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn gp_params(mut self, gp_params: GpParams) -> Self { | ||||
|     self.gp_params.replace(gp_params); | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn clean(mut self, clean: bool) -> Self { | ||||
|     self.clean = clean; | ||||
|     self | ||||
| @@ -119,6 +129,12 @@ impl<'a> AuthWindow<'a> { | ||||
|     let saml_request = self.saml_request.to_string(); | ||||
|     let (auth_result_tx, mut auth_result_rx) = mpsc::unbounded_channel::<AuthResult>(); | ||||
|     let raise_window_cancel_token: Arc<RwLock<Option<CancellationToken>>> = Default::default(); | ||||
|     let gp_params = self.gp_params.as_ref().unwrap(); | ||||
|     let tls_err_policy = if gp_params.ignore_tls_errors() { | ||||
|       TLSErrorsPolicy::Ignore | ||||
|     } else { | ||||
|       TLSErrorsPolicy::Fail | ||||
|     }; | ||||
|  | ||||
|     if self.clean { | ||||
|       clear_webview_cookies(window).await?; | ||||
| @@ -128,6 +144,10 @@ impl<'a> AuthWindow<'a> { | ||||
|     window.with_webview(move |wv| { | ||||
|       let wv = wv.inner(); | ||||
|  | ||||
|       if let Some(context) = wv.context() { | ||||
|         context.set_tls_errors_policy(tls_err_policy); | ||||
|       } | ||||
|  | ||||
|       if let Some(settings) = wv.settings() { | ||||
|         let ua = settings.user_agent().unwrap_or("".into()); | ||||
|         info!("Auth window user agent: {}", ua); | ||||
| @@ -168,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<Window>) { | ||||
|   } | ||||
| } | ||||
|  | ||||
| pub(crate) async fn portal_prelogin(portal: &str, user_agent: &str) -> anyhow::Result<String> { | ||||
| pub(crate) async fn portal_prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<String> { | ||||
|   info!("Portal prelogin..."); | ||||
|   match prelogin(portal, user_agent).await? { | ||||
|  | ||||
|   match prelogin(portal, gp_params).await? { | ||||
|     Prelogin::Saml(prelogin) => Ok(prelogin.saml_request().to_string()), | ||||
|     Prelogin::Standard(_) => Err(anyhow::anyhow!("Received non-SAML prelogin response")), | ||||
|   } | ||||
| @@ -397,6 +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)); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| use clap::Parser; | ||||
| use gpapi::{ | ||||
|   auth::{SamlAuthData, SamlAuthResult}, | ||||
|   clap::args::Os, | ||||
|   gp_params::{ClientOs, GpParams}, | ||||
|   utils::{normalize_server, openssl}, | ||||
|   GP_USER_AGENT, | ||||
| }; | ||||
| @@ -26,23 +28,35 @@ struct Cli { | ||||
|   saml_request: Option<String>, | ||||
|   #[arg(long, default_value = GP_USER_AGENT)] | ||||
|   user_agent: String, | ||||
|   #[arg(long, default_value = "Linux")] | ||||
|   os: Os, | ||||
|   #[arg(long)] | ||||
|   os_version: Option<String>, | ||||
|   #[arg(long)] | ||||
|   hidpi: bool, | ||||
|   #[arg(long)] | ||||
|   fix_openssl: bool, | ||||
|   #[arg(long)] | ||||
|   ignore_tls_errors: bool, | ||||
|   #[arg(long)] | ||||
|   clean: bool, | ||||
| } | ||||
|  | ||||
| impl Cli { | ||||
|   async fn run(&mut self) -> anyhow::Result<()> { | ||||
|     if self.ignore_tls_errors { | ||||
|       info!("TLS errors will be ignored"); | ||||
|     } | ||||
|  | ||||
|     let mut openssl_conf = self.prepare_env()?; | ||||
|  | ||||
|     self.server = normalize_server(&self.server)?; | ||||
|     let gp_params = self.build_gp_params(); | ||||
|  | ||||
|     // Get the initial SAML request | ||||
|     let saml_request = match self.saml_request { | ||||
|       Some(ref saml_request) => saml_request.clone(), | ||||
|       None => portal_prelogin(&self.server, &self.user_agent).await?, | ||||
|       None => portal_prelogin(&self.server, &gp_params).await?, | ||||
|     }; | ||||
|  | ||||
|     self.saml_request.replace(saml_request); | ||||
| @@ -82,10 +96,22 @@ impl Cli { | ||||
|     Ok(None) | ||||
|   } | ||||
|  | ||||
|   fn build_gp_params(&self) -> GpParams { | ||||
|     let gp_params = GpParams::builder() | ||||
|       .user_agent(&self.user_agent) | ||||
|       .client_os(ClientOs::from(&self.os)) | ||||
|       .os_version(self.os_version.clone()) | ||||
|       .ignore_tls_errors(self.ignore_tls_errors) | ||||
|       .build(); | ||||
|  | ||||
|     gp_params | ||||
|   } | ||||
|  | ||||
|   async fn saml_auth(&self, app_handle: AppHandle) -> anyhow::Result<SamlAuthData> { | ||||
|     let auth_window = AuthWindow::new(app_handle) | ||||
|       .server(&self.server) | ||||
|       .user_agent(&self.user_agent) | ||||
|       .gp_params(self.build_gp_params()) | ||||
|       .saml_request(self.saml_request.as_ref().unwrap()) | ||||
|       .clean(self.clean); | ||||
|  | ||||
|   | ||||
| @@ -6,7 +6,7 @@ edition.workspace = true | ||||
| license.workspace = true | ||||
|  | ||||
| [dependencies] | ||||
| gpapi = { path = "../../crates/gpapi" } | ||||
| gpapi = { path = "../../crates/gpapi", features = ["clap"] } | ||||
| openconnect = { path = "../../crates/openconnect" } | ||||
| anyhow.workspace = true | ||||
| clap.workspace = true | ||||
|   | ||||
| @@ -16,6 +16,11 @@ const VERSION: &str = concat!( | ||||
|   ")" | ||||
| ); | ||||
|  | ||||
| pub(crate) struct SharedArgs { | ||||
|   pub(crate) fix_openssl: bool, | ||||
|   pub(crate) ignore_tls_errors: bool, | ||||
| } | ||||
|  | ||||
| #[derive(Subcommand)] | ||||
| enum CliCommand { | ||||
|   #[command(about = "Connect to a portal server")] | ||||
| @@ -40,6 +45,8 @@ enum CliCommand { | ||||
| {usage-heading} {usage} | ||||
|  | ||||
| {all-args}{after-help} | ||||
|  | ||||
| See 'gpclient help <command>' for more information on a specific command. | ||||
| " | ||||
| )] | ||||
| struct Cli { | ||||
| @@ -51,6 +58,8 @@ struct Cli { | ||||
|     help = "Get around the OpenSSL `unsafe legacy renegotiation` error" | ||||
|   )] | ||||
|   fix_openssl: bool, | ||||
|   #[arg(long, help = "Ignore the TLS errors")] | ||||
|   ignore_tls_errors: bool, | ||||
| } | ||||
|  | ||||
| impl Cli { | ||||
| @@ -67,9 +76,17 @@ impl Cli { | ||||
|     // The temp file will be dropped automatically when the file handle is dropped | ||||
|     // So, declare it here to ensure it's not dropped | ||||
|     let _file = self.fix_openssl()?; | ||||
|     let shared_args = SharedArgs { | ||||
|       fix_openssl: self.fix_openssl, | ||||
|       ignore_tls_errors: self.ignore_tls_errors, | ||||
|     }; | ||||
|  | ||||
|     if self.ignore_tls_errors { | ||||
|       info!("TLS errors will be ignored"); | ||||
|     } | ||||
|  | ||||
|     match &self.command { | ||||
|       CliCommand::Connect(args) => ConnectHandler::new(args, self.fix_openssl).handle().await, | ||||
|       CliCommand::Connect(args) => ConnectHandler::new(args, &shared_args).handle().await, | ||||
|       CliCommand::Disconnect => DisconnectHandler::new().handle(), | ||||
|       CliCommand::LaunchGui(args) => LaunchGuiHandler::new(args).handle().await, | ||||
|     } | ||||
| @@ -89,13 +106,24 @@ pub(crate) async fn run() { | ||||
|   if let Err(err) = cli.run().await { | ||||
|     eprintln!("\nError: {}", err); | ||||
|  | ||||
|     if err.to_string().contains("unsafe legacy renegotiation") && !cli.fix_openssl { | ||||
|     let err = err.to_string(); | ||||
|  | ||||
|     if err.contains("unsafe legacy renegotiation") && !cli.fix_openssl { | ||||
|       eprintln!("\nRe-run it with the `--fix-openssl` option to work around this issue, e.g.:\n"); | ||||
|       // Print the command | ||||
|       let args = std::env::args().collect::<Vec<_>>(); | ||||
|       eprintln!("{} --fix-openssl {}\n", args[0], args[1..].join(" ")); | ||||
|     } | ||||
|  | ||||
|     if err.contains("certificate verify failed") { | ||||
|       eprintln!( | ||||
|         "\nRe-run it with the `--ignore-tls-errors` option to ignore the certificate error, e.g.:\n" | ||||
|       ); | ||||
|       // Print the command | ||||
|       let args = std::env::args().collect::<Vec<_>>(); | ||||
|       eprintln!("{} --ignore-tls-errors {}\n", args[0], args[1..].join(" ")); | ||||
|     } | ||||
|  | ||||
|     std::process::exit(1); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -2,9 +2,10 @@ use std::{fs, sync::Arc}; | ||||
|  | ||||
| use clap::Args; | ||||
| use gpapi::{ | ||||
|   clap::args::Os, | ||||
|   credential::{Credential, PasswordCredential}, | ||||
|   gateway::gateway_login, | ||||
|   gp_params::GpParams, | ||||
|   gp_params::{ClientOs, GpParams}, | ||||
|   portal::{prelogin, retrieve_config, Prelogin}, | ||||
|   process::auth_launcher::SamlAuthLauncher, | ||||
|   utils::{self, shutdown_signal}, | ||||
| @@ -14,7 +15,7 @@ use inquire::{Password, PasswordDisplayMode, Select, Text}; | ||||
| use log::info; | ||||
| use openconnect::Vpn; | ||||
|  | ||||
| use crate::GP_CLIENT_LOCK_FILE; | ||||
| use crate::{cli::SharedArgs, GP_CLIENT_LOCK_FILE}; | ||||
|  | ||||
| #[derive(Args)] | ||||
| pub(crate) struct ConnectArgs { | ||||
| @@ -36,6 +37,10 @@ pub(crate) struct ConnectArgs { | ||||
|   script: Option<String>, | ||||
|   #[arg(long, default_value = GP_USER_AGENT, help = "The user agent to use")] | ||||
|   user_agent: String, | ||||
|   #[arg(long, default_value = "Linux")] | ||||
|   os: Os, | ||||
|   #[arg(long)] | ||||
|   os_version: Option<String>, | ||||
|   #[arg(long, help = "The HiDPI mode, useful for high resolution screens")] | ||||
|   hidpi: bool, | ||||
|   #[arg(long, help = "Do not reuse the remembered authentication cookie")] | ||||
| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user