GlobalProtect-openconnect/apps/gpauth/src/auth_window.rs
2024-12-14 15:17:32 +00:00

193 lines
5.7 KiB
Rust

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<SamlAuthData> {
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<SamlAuthData> {
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::<anyhow::Result<()>>();
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<Arc<AuthMessenger>> {
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::<anyhow::Result<()>>();
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 = '<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 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<String> {
match prelogin(portal, gp_params).await? {
Prelogin::Saml(prelogin) => Ok(prelogin.saml_request().to_string()),
Prelogin::Standard(_) => bail!("Received non-SAML prelogin response"),
}
}