mirror of
https://github.com/yuezk/GlobalProtect-openconnect.git
synced 2025-04-02 18:31:50 -04:00
193 lines
5.7 KiB
Rust
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"),
|
|
}
|
|
}
|