use std::{sync::Arc, time::Instant}; use anyhow::bail; use gpapi::{ auth::SamlAuthData, error::PortalError, gp_params::GpParams, portal::{prelogin, Prelogin}, }; use log::{info, warn}; use tauri::{AppHandle, WebviewUrl, WebviewWindow}; use tokio::sync::oneshot; use tokio_util::sync::CancellationToken; use crate::{ auth_messenger::{AuthError, AuthMessenger}, common::{AuthRequest, AuthSettings}, platform_impl, }; pub struct AuthWindow<'a> { app_handle: &'a AppHandle, server: &'a str, gp_params: Option<&'a GpParams>, saml_request: Option<&'a str>, clean: bool, } impl<'a> AuthWindow<'a> { pub fn new(app_handle: &'a AppHandle, server: &'a str) -> Self { Self { app_handle, server, gp_params: None, saml_request: None, clean: false, } } pub fn with_gp_params(mut self, gp_params: &'a GpParams) -> Self { self.gp_params = Some(gp_params); self } pub fn with_saml_request(mut self, saml_request: &'a str) -> Self { self.saml_request = Some(saml_request); self } pub fn with_clean(mut self, clean: bool) -> Self { self.clean = clean; self } pub async fn authenticate(&self) -> anyhow::Result { let auth_window = WebviewWindow::builder(self.app_handle, "auth_window", WebviewUrl::default()) .title("GlobalProtect Login") .focused(true) .visible(true) .center() .build()?; let cancel_token = CancellationToken::new(); tokio::select! { _ = cancel_token.cancelled() => bail!("Authentication cancelled"), result = self.auth_loop(&auth_window, &cancel_token) => { auth_window.close()?; result } } } async fn auth_loop( &self, auth_window: &WebviewWindow, cancel_token: &CancellationToken, ) -> anyhow::Result { if self.clean { self.clear_webview_data(&auth_window).await?; } let auth_messenger = self.setup_auth_window(&auth_window, cancel_token).await?; loop { match auth_messenger.recv_auth_data().await { Ok(auth_data) => return Ok(auth_data), Err(AuthError::TlsError) => bail!(PortalError::TlsError), Err(AuthError::NotFound) => self.handle_not_found(auth_window).await, Err(AuthError::Invalid) => self.retry_auth(auth_window).await?, Err(AuthError::Other) => bail!("Unknown error"), } } } async fn clear_webview_data(&self, auth_window: &WebviewWindow) -> anyhow::Result<()> { info!("Clearing webview data..."); let (tx, rx) = oneshot::channel::>(); let now = Instant::now(); auth_window.with_webview(|webview| { platform_impl::clear_data(&webview.inner(), |result| { if let Err(result) = tx.send(result) { warn!("Failed to send clear data result: {:?}", result); } }) })?; rx.await??; info!("Webview data cleared in {:?}", now.elapsed()); Ok(()) } async fn setup_auth_window( &self, auth_window: &WebviewWindow, cancel_token: &CancellationToken, ) -> anyhow::Result> { info!("Setting up auth window..."); let cancel_token = cancel_token.clone(); auth_window.on_window_event(move |event| { if let tauri::WindowEvent::CloseRequested { .. } = event { cancel_token.cancel(); } }); let saml_request = self.saml_request.expect("SAML request not set").to_string(); let gp_params = self.gp_params.expect("GP params not set"); let auth_messenger = Arc::new(AuthMessenger::new()); let auth_messenger_clone = Arc::clone(&auth_messenger); let ignore_tls_errors = gp_params.ignore_tls_errors(); let (tx, rx) = oneshot::channel::>(); auth_window.with_webview(move |webview| { let auth_settings = AuthSettings { auth_request: AuthRequest::new(&saml_request), auth_messenger: auth_messenger_clone, ignore_tls_errors, }; let result = platform_impl::setup_webview(&webview.inner(), auth_settings); if let Err(result) = tx.send(result) { warn!("Failed to send setup auth window result: {:?}", result); } })?; rx.await??; info!("Auth window setup completed"); Ok(auth_messenger) } async fn handle_not_found(&self, auth_window: &WebviewWindow) { info!("No auth data found, it may not be the /SAML20/SP/ACS endpoint"); let visible = auth_window.is_visible().unwrap_or(false); if visible { return; } info!("Displaying the window in 3 seconds"); // todo!("Display the window in 3 seconds") } async fn retry_auth(&self, auth_window: &WebviewWindow) -> anyhow::Result<()> { info!("Retrying authentication..."); auth_window.eval( r#" var loading = document.createElement("div"); loading.innerHTML = '
Got invalid token, retrying...
'; 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 saml_request = portal_prelogin(&self.server, self.gp_params.unwrap()).await?; auth_window.with_webview(move |webview| { let auth_request = AuthRequest::new(&saml_request); platform_impl::load_auth_request(&webview.inner(), &auth_request); })?; Ok(()) } } pub async fn portal_prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result { match prelogin(portal, gp_params).await? { Prelogin::Saml(prelogin) => Ok(prelogin.saml_request().to_string()), Prelogin::Standard(_) => bail!("Received non-SAML prelogin response"), } }