mirror of
				https://github.com/yuezk/GlobalProtect-openconnect.git
				synced 2025-05-20 07:26:58 -04:00 
			
		
		
		
	feat: gpauth support macos
This commit is contained in:
		
							
								
								
									
										229
									
								
								crates/auth/src/webview/auth_messenger.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										229
									
								
								crates/auth/src/webview/auth_messenger.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,229 @@ | ||||
| use anyhow::bail; | ||||
| use gpapi::{auth::SamlAuthData, error::AuthDataParseError}; | ||||
| use log::{error, info}; | ||||
| use regex::Regex; | ||||
| use tokio::sync::{mpsc, RwLock}; | ||||
| use tokio_util::sync::CancellationToken; | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub(crate) enum AuthDataLocation { | ||||
|   #[cfg(not(target_os = "macos"))] | ||||
|   Headers, | ||||
|   Body, | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub(crate) enum AuthError { | ||||
|   /// Failed to load page due to TLS error | ||||
|   #[cfg(not(target_os = "macos"))] | ||||
|   TlsError, | ||||
|   /// 1. Found auth data in headers/body but it's invalid | ||||
|   /// 2. Loaded an empty page, failed to load page. etc. | ||||
|   Invalid(anyhow::Error, AuthDataLocation), | ||||
|   /// No auth data found in headers/body | ||||
|   NotFound(AuthDataLocation), | ||||
| } | ||||
|  | ||||
| impl AuthError { | ||||
|   pub fn invalid_from_body(err: anyhow::Error) -> Self { | ||||
|     Self::Invalid(err, AuthDataLocation::Body) | ||||
|   } | ||||
|  | ||||
|   pub fn not_found_in_body() -> Self { | ||||
|     Self::NotFound(AuthDataLocation::Body) | ||||
|   } | ||||
| } | ||||
|  | ||||
| #[cfg(not(target_os = "macos"))] | ||||
| impl AuthError { | ||||
|   pub fn not_found_in_headers() -> Self { | ||||
|     Self::NotFound(AuthDataLocation::Headers) | ||||
|   } | ||||
| } | ||||
|  | ||||
| pub(crate) enum AuthEvent { | ||||
|   Data(SamlAuthData, AuthDataLocation), | ||||
|   Error(AuthError), | ||||
|   RaiseWindow, | ||||
|   Close, | ||||
| } | ||||
|  | ||||
| pub struct AuthMessenger { | ||||
|   tx: mpsc::UnboundedSender<AuthEvent>, | ||||
|   rx: RwLock<mpsc::UnboundedReceiver<AuthEvent>>, | ||||
|   raise_window_cancel_token: RwLock<Option<CancellationToken>>, | ||||
| } | ||||
|  | ||||
| impl AuthMessenger { | ||||
|   pub fn new() -> Self { | ||||
|     let (tx, rx) = mpsc::unbounded_channel(); | ||||
|  | ||||
|     Self { | ||||
|       tx, | ||||
|       rx: RwLock::new(rx), | ||||
|       raise_window_cancel_token: Default::default(), | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   pub async fn subscribe(&self) -> anyhow::Result<AuthEvent> { | ||||
|     let mut rx = self.rx.write().await; | ||||
|     if let Some(event) = rx.recv().await { | ||||
|       return Ok(event); | ||||
|     } | ||||
|     bail!("Failed to receive auth event"); | ||||
|   } | ||||
|  | ||||
|   pub fn send_auth_event(&self, event: AuthEvent) { | ||||
|     if let Err(event) = self.tx.send(event) { | ||||
|       error!("Failed to send auth event: {}", event); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   pub fn send_auth_error(&self, err: AuthError) { | ||||
|     self.send_auth_event(AuthEvent::Error(err)); | ||||
|   } | ||||
|  | ||||
|   fn send_auth_data(&self, data: SamlAuthData, location: AuthDataLocation) { | ||||
|     self.send_auth_event(AuthEvent::Data(data, location)); | ||||
|   } | ||||
|  | ||||
|   pub fn schedule_raise_window(&self, delay: u64) { | ||||
|     let Ok(mut guard) = self.raise_window_cancel_token.try_write() else { | ||||
|       return; | ||||
|     }; | ||||
|  | ||||
|     // Return if the previous raise window task is still running | ||||
|     if let Some(token) = guard.as_ref() { | ||||
|       if !token.is_cancelled() { | ||||
|         info!("Raise window task is still running, skipping..."); | ||||
|         return; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     let cancel_token = CancellationToken::new(); | ||||
|     let cancel_token_clone = cancel_token.clone(); | ||||
|  | ||||
|     *guard = Some(cancel_token_clone); | ||||
|  | ||||
|     let tx = self.tx.clone(); | ||||
|     tokio::spawn(async move { | ||||
|       info!("Displaying the window in {} second(s)...", delay); | ||||
|  | ||||
|       tokio::select! { | ||||
|         _ = tokio::time::sleep(tokio::time::Duration::from_secs(delay)) => { | ||||
|           cancel_token.cancel(); | ||||
|  | ||||
|           if let Err(err) = tx.send(AuthEvent::RaiseWindow) { | ||||
|             error!("Failed to send raise window event: {}", err); | ||||
|           } | ||||
|         } | ||||
|         _ = cancel_token.cancelled() => { | ||||
|           info!("Cancelled raise window task"); | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   pub fn cancel_raise_window(&self) { | ||||
|     if let Ok(mut cancel_token) = self.raise_window_cancel_token.try_write() { | ||||
|       if let Some(token) = cancel_token.take() { | ||||
|         token.cancel(); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   pub fn read_from_html(&self, html: &str) { | ||||
|     if html.contains("Temporarily Unavailable") { | ||||
|       return self.send_auth_error(AuthError::invalid_from_body(anyhow::anyhow!("Temporarily Unavailable"))); | ||||
|     } | ||||
|  | ||||
|     let auth_result = SamlAuthData::from_html(html).or_else(|err| { | ||||
|       info!("Read auth data from html failed: {}, extracting gpcallback...", err); | ||||
|  | ||||
|       if let Some(gpcallback) = extract_gpcallback(html) { | ||||
|         info!("Found gpcallback from html..."); | ||||
|         SamlAuthData::from_gpcallback(&gpcallback) | ||||
|       } else { | ||||
|         Err(err) | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     match auth_result { | ||||
|       Ok(data) => self.send_auth_data(data, AuthDataLocation::Body), | ||||
|       Err(AuthDataParseError::Invalid(err)) => self.send_auth_error(AuthError::invalid_from_body(err)), | ||||
|       Err(AuthDataParseError::NotFound) => self.send_auth_error(AuthError::not_found_in_body()), | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   #[cfg(not(target_os = "macos"))] | ||||
|   pub fn read_from_response(&self, auth_response: &impl super::webview_auth::GetHeader) { | ||||
|     use log::warn; | ||||
|  | ||||
|     let Some(status) = auth_response.get_header("saml-auth-status") else { | ||||
|       return self.send_auth_error(AuthError::not_found_in_headers()); | ||||
|     }; | ||||
|  | ||||
|     // Do not send auth error when reading from headers, as the html body may contain the auth data | ||||
|     if status != "1" { | ||||
|       warn!("Found invalid saml-auth-status in headers: {}", status); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     let username = auth_response.get_header("saml-username"); | ||||
|     let prelogin_cookie = auth_response.get_header("prelogin-cookie"); | ||||
|     let portal_userauthcookie = auth_response.get_header("portal-userauthcookie"); | ||||
|  | ||||
|     match SamlAuthData::new(username, prelogin_cookie, portal_userauthcookie) { | ||||
|       Ok(auth_data) => self.send_auth_data(auth_data, AuthDataLocation::Headers), | ||||
|       Err(err) => { | ||||
|         warn!("Failed to read auth data from headers: {}", err); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| fn extract_gpcallback(html: &str) -> Option<String> { | ||||
|   let re = Regex::new(r#"globalprotectcallback:[^"]+"#).unwrap(); | ||||
|   re.captures(html) | ||||
|     .and_then(|captures| captures.get(0)) | ||||
|     .map(|m| html_escape::decode_html_entities(m.as_str()).to_string()) | ||||
| } | ||||
|  | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|   use super::*; | ||||
|  | ||||
|   #[test] | ||||
|   fn extract_gpcallback_some() { | ||||
|     let html = r#" | ||||
|       <meta http-equiv="refresh" content="0; URL=globalprotectcallback:PGh0bWw+PCEtLSA8c"> | ||||
|       <meta http-equiv="refresh" content="0; URL=globalprotectcallback:PGh0bWw+PCEtLSA8c"> | ||||
|     "#; | ||||
|  | ||||
|     assert_eq!( | ||||
|       extract_gpcallback(html).as_deref(), | ||||
|       Some("globalprotectcallback:PGh0bWw+PCEtLSA8c") | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   #[test] | ||||
|   fn extract_gpcallback_cas() { | ||||
|     let html = r#" | ||||
|       <meta http-equiv="refresh" content="0; URL=globalprotectcallback:cas-as=1&un=xyz@email.com&token=very_long_string"> | ||||
|     "#; | ||||
|  | ||||
|     assert_eq!( | ||||
|       extract_gpcallback(html).as_deref(), | ||||
|       Some("globalprotectcallback:cas-as=1&un=xyz@email.com&token=very_long_string") | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   #[test] | ||||
|   fn extract_gpcallback_none() { | ||||
|     let html = r#" | ||||
|       <meta http-equiv="refresh" content="0; URL=PGh0bWw+PCEtLSA8c"> | ||||
|     "#; | ||||
|  | ||||
|     assert_eq!(extract_gpcallback(html), None); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										58
									
								
								crates/auth/src/webview/macos.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								crates/auth/src/webview/macos.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| use block2::RcBlock; | ||||
| use log::warn; | ||||
| use objc2::runtime::AnyObject; | ||||
| use objc2_foundation::{NSError, NSString, NSURLRequest, NSURL}; | ||||
| use objc2_web_kit::WKWebView; | ||||
| use tauri::webview::PlatformWebview; | ||||
|  | ||||
| use super::webview_auth::PlatformWebviewExt; | ||||
|  | ||||
| impl PlatformWebviewExt for PlatformWebview { | ||||
|   fn ignore_tls_errors(&self) -> anyhow::Result<()> { | ||||
|     warn!("Ignoring TLS errors is not supported on macOS"); | ||||
|     Ok(()) | ||||
|   } | ||||
|  | ||||
|   fn load_url(&self, url: &str) -> anyhow::Result<()> { | ||||
|     unsafe { | ||||
|       let wv: &WKWebView = &*self.inner().cast(); | ||||
|       let url = NSURL::URLWithString(&NSString::from_str(url)).ok_or_else(|| anyhow::anyhow!("Invalid URL"))?; | ||||
|       let request = NSURLRequest::requestWithURL(&url); | ||||
|  | ||||
|       wv.loadRequest(&request); | ||||
|     } | ||||
|  | ||||
|     Ok(()) | ||||
|   } | ||||
|  | ||||
|   fn load_html(&self, html: &str) -> anyhow::Result<()> { | ||||
|     unsafe { | ||||
|       let wv: &WKWebView = &*self.inner().cast(); | ||||
|       wv.loadHTMLString_baseURL(&NSString::from_str(html), None); | ||||
|     } | ||||
|  | ||||
|     Ok(()) | ||||
|   } | ||||
|  | ||||
|   fn get_html(&self, callback: Box<dyn Fn(anyhow::Result<String>) + 'static>) { | ||||
|     unsafe { | ||||
|       let wv: &WKWebView = &*self.inner().cast(); | ||||
|  | ||||
|       let js_callback = RcBlock::new(move |body: *mut AnyObject, err: *mut NSError| { | ||||
|         if let Some(err) = err.as_ref() { | ||||
|           let code = err.code(); | ||||
|           let message = err.localizedDescription(); | ||||
|           callback(Err(anyhow::anyhow!("Error {}: {}", code, message))); | ||||
|         } else { | ||||
|           let body: &NSString = &*body.cast(); | ||||
|           callback(Ok(body.to_string())); | ||||
|         } | ||||
|       }); | ||||
|  | ||||
|       wv.evaluateJavaScript_completionHandler( | ||||
|         &NSString::from_str("document.documentElement.outerHTML"), | ||||
|         Some(&js_callback), | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										105
									
								
								crates/auth/src/webview/unix.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								crates/auth/src/webview/unix.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,105 @@ | ||||
| use std::sync::Arc; | ||||
|  | ||||
| use anyhow::bail; | ||||
| use gpapi::utils::redact::redact_uri; | ||||
| use log::warn; | ||||
| use tauri::webview::PlatformWebview; | ||||
| use webkit2gtk::{ | ||||
|   gio::Cancellable, glib::GString, LoadEvent, TLSErrorsPolicy, URIResponseExt, WebResource, WebResourceExt, WebViewExt, | ||||
|   WebsiteDataManagerExt, | ||||
| }; | ||||
|  | ||||
| use super::{ | ||||
|   auth_messenger::AuthError, | ||||
|   webview_auth::{GetHeader, PlatformWebviewExt}, | ||||
| }; | ||||
|  | ||||
| impl GetHeader for WebResource { | ||||
|   fn get_header(&self, key: &str) -> Option<String> { | ||||
|     self | ||||
|       .response() | ||||
|       .and_then(|response| response.http_headers()) | ||||
|       .and_then(|headers| headers.one(key)) | ||||
|       .map(GString::into) | ||||
|   } | ||||
| } | ||||
|  | ||||
| impl PlatformWebviewExt for PlatformWebview { | ||||
|   fn ignore_tls_errors(&self) -> anyhow::Result<()> { | ||||
|     if let Some(manager) = self.inner().website_data_manager() { | ||||
|       manager.set_tls_errors_policy(TLSErrorsPolicy::Ignore); | ||||
|       return Ok(()); | ||||
|     } | ||||
|     bail!("Failed to get website data manager"); | ||||
|   } | ||||
|  | ||||
|   fn load_url(&self, url: &str) -> anyhow::Result<()> { | ||||
|     self.inner().load_uri(url); | ||||
|     Ok(()) | ||||
|   } | ||||
|  | ||||
|   fn load_html(&self, html: &str) -> anyhow::Result<()> { | ||||
|     self.inner().load_html(html, None); | ||||
|     Ok(()) | ||||
|   } | ||||
|  | ||||
|   fn get_html(&self, callback: Box<dyn Fn(anyhow::Result<String>) + 'static>) { | ||||
|     let script = "document.documentElement.outerHTML"; | ||||
|     self | ||||
|       .inner() | ||||
|       .evaluate_javascript(script, None, None, Cancellable::NONE, move |result| match result { | ||||
|         Ok(value) => callback(Ok(value.to_string())), | ||||
|         Err(err) => callback(Err(anyhow::anyhow!(err))), | ||||
|       }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| pub trait PlatformWebviewOnResponse { | ||||
|   fn on_response(&self, callback: Box<dyn Fn(anyhow::Result<WebResource, AuthError>) + 'static>); | ||||
| } | ||||
|  | ||||
| impl PlatformWebviewOnResponse for PlatformWebview { | ||||
|   fn on_response(&self, callback: Box<dyn Fn(anyhow::Result<WebResource, AuthError>) + 'static>) { | ||||
|     let wv = self.inner(); | ||||
|     let callback = Arc::new(callback); | ||||
|     let callback_clone = Arc::clone(&callback); | ||||
|  | ||||
|     wv.connect_load_changed(move |wv, event| { | ||||
|       if event != LoadEvent::Finished { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       let Some(web_resource) = wv.main_resource() else { | ||||
|         return; | ||||
|       }; | ||||
|  | ||||
|       let uri = web_resource.uri().unwrap_or("".into()); | ||||
|       if uri.is_empty() { | ||||
|         callback_clone(Err(AuthError::invalid_from_body(anyhow::anyhow!("Empty URI")))); | ||||
|       } else { | ||||
|         callback_clone(Ok(web_resource)); | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     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 | ||||
|       ); | ||||
|  | ||||
|       callback(Err(AuthError::TlsError)); | ||||
|       true | ||||
|     }); | ||||
|  | ||||
|     wv.connect_load_failed(move |_wv, _event, uri, err| { | ||||
|       let redacted_uri = redact_uri(uri); | ||||
|       if !uri.starts_with("globalprotectcallback:") { | ||||
|         warn!("Failed to load uri: {} with error: {}", redacted_uri, err); | ||||
|       } | ||||
|       // NOTE: Don't send error here, since load_changed event will be triggered after this | ||||
|       // true to stop other handlers from being invoked for the event. false to propagate the event further. | ||||
|       true | ||||
|     }); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										277
									
								
								crates/auth/src/webview/webview_auth.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										277
									
								
								crates/auth/src/webview/webview_auth.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,277 @@ | ||||
| use std::{sync::Arc, time::Duration}; | ||||
|  | ||||
| use anyhow::bail; | ||||
| use gpapi::{auth::SamlAuthData, gp_params::GpParams, utils::redact::redact_uri}; | ||||
| use log::{info, warn}; | ||||
| use tauri::{ | ||||
|   webview::{PageLoadEvent, PageLoadPayload}, | ||||
|   AppHandle, WebviewUrl, WebviewWindow, WindowEvent, | ||||
| }; | ||||
| use tokio::{sync::oneshot, time}; | ||||
|  | ||||
| use crate::auth_prelogin; | ||||
|  | ||||
| use super::auth_messenger::{AuthError, AuthEvent, AuthMessenger}; | ||||
|  | ||||
| pub trait PlatformWebviewExt { | ||||
|   fn ignore_tls_errors(&self) -> anyhow::Result<()>; | ||||
|  | ||||
|   fn load_url(&self, url: &str) -> anyhow::Result<()>; | ||||
|  | ||||
|   fn load_html(&self, html: &str) -> anyhow::Result<()>; | ||||
|  | ||||
|   fn get_html(&self, callback: Box<dyn Fn(anyhow::Result<String>) + 'static>); | ||||
|  | ||||
|   fn load_auth_request(&self, auth_request: &str) -> anyhow::Result<()> { | ||||
|     if auth_request.starts_with("http") { | ||||
|       info!("Loading auth request as URL: {}", redact_uri(auth_request)); | ||||
|       self.load_url(auth_request) | ||||
|     } else { | ||||
|       info!("Loading auth request as HTML..."); | ||||
|       self.load_html(auth_request) | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| #[cfg(not(target_os = "macos"))] | ||||
| pub trait GetHeader { | ||||
|   fn get_header(&self, key: &str) -> Option<String>; | ||||
| } | ||||
|  | ||||
| pub struct WebviewAuthenticator<'a> { | ||||
|   server: &'a str, | ||||
|   gp_params: &'a GpParams, | ||||
|   auth_request: Option<&'a str>, | ||||
|   clean: bool, | ||||
|  | ||||
|   is_retrying: tokio::sync::RwLock<bool>, | ||||
| } | ||||
|  | ||||
| impl<'a> WebviewAuthenticator<'a> { | ||||
|   pub fn new(server: &'a str, gp_params: &'a GpParams) -> Self { | ||||
|     Self { | ||||
|       server, | ||||
|       gp_params, | ||||
|       auth_request: None, | ||||
|       clean: false, | ||||
|       is_retrying: Default::default(), | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   pub fn with_auth_request(mut self, auth_request: &'a str) -> Self { | ||||
|     self.auth_request = Some(auth_request); | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn with_clean(mut self, clean: bool) -> Self { | ||||
|     self.clean = clean; | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub async fn authenticate(&self, app_handle: &AppHandle) -> anyhow::Result<SamlAuthData> { | ||||
|     let auth_messenger = Arc::new(AuthMessenger::new()); | ||||
|     let auth_messenger_clone = Arc::clone(&auth_messenger); | ||||
|  | ||||
|     let on_page_load = move |auth_window: WebviewWindow, event: PageLoadPayload<'_>| { | ||||
|       let auth_messenger_clone = Arc::clone(&auth_messenger_clone); | ||||
|       let redacted_url = redact_uri(event.url().as_str()); | ||||
|  | ||||
|       match event.event() { | ||||
|         PageLoadEvent::Started => { | ||||
|           info!("Started loading page: {}", redacted_url); | ||||
|           auth_messenger_clone.cancel_raise_window(); | ||||
|         } | ||||
|         PageLoadEvent::Finished => { | ||||
|           info!("Finished loading page: {}", redacted_url); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       // Read auth data from the page no matter whether it's finished loading or not | ||||
|       // Because we found that the finished event may not be triggered in some cases (e.g., on macOS) | ||||
|       let _ = auth_window.with_webview(move |wv| { | ||||
|         wv.get_html(Box::new(move |html| match html { | ||||
|           Ok(html) => auth_messenger_clone.read_from_html(&html), | ||||
|           Err(err) => warn!("Failed to get html: {}", err), | ||||
|         })); | ||||
|       }); | ||||
|     }; | ||||
|  | ||||
|     let title_bar_height = if cfg!(target_os = "macos") { 28.0 } else { 0.0 }; | ||||
|  | ||||
|     let auth_window = WebviewWindow::builder(app_handle, "auth_window", WebviewUrl::default()) | ||||
|       .on_page_load(on_page_load) | ||||
|       .title("GlobalProtect Login") | ||||
|       .inner_size(900.0, 650.0 + title_bar_height) | ||||
|       .focused(true) | ||||
|       .visible(false) | ||||
|       .center() | ||||
|       .build()?; | ||||
|  | ||||
|     self | ||||
|       .setup_auth_window(&auth_window, Arc::clone(&auth_messenger)) | ||||
|       .await?; | ||||
|  | ||||
|     loop { | ||||
|       match auth_messenger.subscribe().await? { | ||||
|         AuthEvent::Close => bail!("Authentication cancelled"), | ||||
|         AuthEvent::RaiseWindow => self.raise_window(&auth_window), | ||||
|         #[cfg(not(target_os = "macos"))] | ||||
|         AuthEvent::Error(AuthError::TlsError) => bail!(gpapi::error::PortalError::TlsError), | ||||
|         AuthEvent::Error(AuthError::NotFound(location)) => { | ||||
|           info!( | ||||
|             "No auth data found in {:?}, it may not be the /SAML20/SP/ACS endpoint", | ||||
|             location | ||||
|           ); | ||||
|           self.handle_not_found(&auth_window, &auth_messenger); | ||||
|         } | ||||
|         AuthEvent::Error(AuthError::Invalid(err, location)) => { | ||||
|           warn!("Got invalid auth data in {:?}: {}", location, err); | ||||
|           self.retry_auth(&auth_window).await; | ||||
|         } | ||||
|         AuthEvent::Data(auth_data, location) => { | ||||
|           info!("Got auth data from {:?}", location); | ||||
|  | ||||
|           auth_window.close()?; | ||||
|           return Ok(auth_data); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   async fn setup_auth_window( | ||||
|     &self, | ||||
|     auth_window: &WebviewWindow, | ||||
|     auth_messenger: Arc<AuthMessenger>, | ||||
|   ) -> anyhow::Result<()> { | ||||
|     info!("Setting up auth window..."); | ||||
|  | ||||
|     if self.clean { | ||||
|       info!("Clearing all browsing data..."); | ||||
|       auth_window.clear_all_browsing_data()?; | ||||
|     } | ||||
|  | ||||
|     // Handle window close event | ||||
|     let auth_messenger_clone = Arc::clone(&auth_messenger); | ||||
|     auth_window.on_window_event(move |event| { | ||||
|       if let WindowEvent::CloseRequested { .. } = event { | ||||
|         auth_messenger_clone.send_auth_event(AuthEvent::Close); | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     // Show the window after 10 seconds, so that the user can see the window if the auth process is stuck | ||||
|     let auth_messenger_clone = Arc::clone(&auth_messenger); | ||||
|     tokio::spawn(async move { | ||||
|       time::sleep(Duration::from_secs(10)).await; | ||||
|       auth_messenger_clone.send_auth_event(AuthEvent::RaiseWindow); | ||||
|     }); | ||||
|  | ||||
|     let auth_request = match self.auth_request { | ||||
|       Some(auth_request) => auth_request.to_string(), | ||||
|       None => auth_prelogin(&self.server, &self.gp_params).await?, | ||||
|     }; | ||||
|  | ||||
|     let (tx, rx) = oneshot::channel::<anyhow::Result<()>>(); | ||||
|     let ignore_tls_errors = self.gp_params.ignore_tls_errors(); | ||||
|  | ||||
|     // Set up webview | ||||
|     auth_window.with_webview(move |wv| { | ||||
|       #[cfg(not(target_os = "macos"))] | ||||
|       { | ||||
|         use super::platform_impl::PlatformWebviewOnResponse; | ||||
|         wv.on_response(Box::new(move |response| match response { | ||||
|           Ok(response) => auth_messenger.read_from_response(&response), | ||||
|           Err(err) => auth_messenger.send_auth_error(err), | ||||
|         })); | ||||
|       } | ||||
|  | ||||
|       let result = || -> anyhow::Result<()> { | ||||
|         if ignore_tls_errors { | ||||
|           wv.ignore_tls_errors()?; | ||||
|         } | ||||
|  | ||||
|         wv.load_auth_request(&auth_request) | ||||
|       }(); | ||||
|  | ||||
|       if let Err(result) = tx.send(result) { | ||||
|         warn!("Failed to send setup auth window result: {:?}", result); | ||||
|       } | ||||
|     })?; | ||||
|  | ||||
|     rx.await??; | ||||
|     info!("Auth window setup completed"); | ||||
|  | ||||
|     Ok(()) | ||||
|   } | ||||
|  | ||||
|   fn handle_not_found(&self, auth_window: &WebviewWindow, auth_messenger: &Arc<AuthMessenger>) { | ||||
|     let visible = auth_window.is_visible().unwrap_or(false); | ||||
|     if visible { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     auth_messenger.schedule_raise_window(2); | ||||
|   } | ||||
|  | ||||
|   async fn retry_auth(&self, auth_window: &WebviewWindow) { | ||||
|     let mut is_retrying = self.is_retrying.write().await; | ||||
|     if *is_retrying { | ||||
|       info!("Already retrying authentication, skipping..."); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     *is_retrying = true; | ||||
|     drop(is_retrying); | ||||
|  | ||||
|     if let Err(err) = self.retry_auth_impl(auth_window).await { | ||||
|       warn!("Failed to retry authentication: {}", err); | ||||
|     } | ||||
|  | ||||
|     *self.is_retrying.write().await = false; | ||||
|   } | ||||
|  | ||||
|   async fn retry_auth_impl(&self, auth_window: &WebviewWindow) -> anyhow::Result<()> { | ||||
|     info!("Retrying authentication..."); | ||||
|  | ||||
|     auth_window.eval( r#" | ||||
|       var loading = document.createElement("div"); | ||||
|       loading.innerHTML = '<div style="position: absolute; width: 100%; text-align: center; font-size: 20px; font-weight: bold; top: 50%; left: 50%; transform: translate(-50%, -50%);">Got invalid token, retrying...</div>'; | ||||
|       loading.style = "position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255, 255, 255, 0.85); z-index: 99999;"; | ||||
|       document.body.appendChild(loading); | ||||
|     "#)?; | ||||
|  | ||||
|     let auth_request = auth_prelogin(&self.server, &self.gp_params).await?; | ||||
|     let (tx, rx) = oneshot::channel::<anyhow::Result<()>>(); | ||||
|     auth_window.with_webview(move |wv| { | ||||
|       let result = wv.load_auth_request(&auth_request); | ||||
|       if let Err(result) = tx.send(result) { | ||||
|         warn!("Failed to send retry auth result: {:?}", result); | ||||
|       } | ||||
|     })?; | ||||
|  | ||||
|     rx.await??; | ||||
|  | ||||
|     Ok(()) | ||||
|   } | ||||
|  | ||||
|   fn raise_window(&self, auth_window: &WebviewWindow) { | ||||
|     let visible = auth_window.is_visible().unwrap_or(false); | ||||
|     if visible { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     info!("Raising auth window..."); | ||||
|  | ||||
|     #[cfg(target_os = "macos")] | ||||
|     let result = auth_window.show(); | ||||
|  | ||||
|     #[cfg(not(target_os = "macos"))] | ||||
|     let result = { | ||||
|       use gpapi::utils::window::WindowExt; | ||||
|       auth_window.raise() | ||||
|     }; | ||||
|  | ||||
|     if let Err(err) = result { | ||||
|       warn!("Failed to raise window: {}", err); | ||||
|     } | ||||
|   } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user