upgrade gpauth

This commit is contained in:
Kevin Yue
2024-12-13 10:58:39 +00:00
parent 32cb582e78
commit 11235243d6
66 changed files with 4564 additions and 6614 deletions

View File

@@ -0,0 +1,60 @@
use std::borrow::Cow;
use anyhow::bail;
use gpapi::{
gp_params::GpParams,
portal::{prelogin, Prelogin},
};
pub struct AuthWindow<'a> {
server: &'a str,
auth_request: Option<&'a str>,
pub(crate) gp_params: &'a GpParams,
#[cfg(feature = "webview-auth")]
pub(crate) clean: bool,
#[cfg(feature = "webview-auth")]
pub(crate) is_retrying: tokio::sync::RwLock<bool>,
}
impl<'a> AuthWindow<'a> {
pub fn new(server: &'a str, gp_params: &'a GpParams) -> Self {
Self {
server,
gp_params,
auth_request: None,
#[cfg(feature = "webview-auth")]
clean: false,
#[cfg(feature = "webview-auth")]
is_retrying: Default::default(),
}
}
pub fn with_auth_request(mut self, auth_request: &'a str) -> Self {
if !auth_request.is_empty() {
self.auth_request = Some(auth_request);
}
self
}
pub(crate) async fn initial_auth_request(&self) -> anyhow::Result<Cow<'a, str>> {
if let Some(auth_request) = self.auth_request {
return Ok(Cow::Borrowed(auth_request));
}
let auth_request = self.portal_prelogin().await?;
Ok(Cow::Owned(auth_request))
}
pub(crate) async fn portal_prelogin(&self) -> anyhow::Result<String> {
auth_prelogin(self.server, self.gp_params).await
}
}
pub async fn auth_prelogin(server: &str, gp_params: &GpParams) -> anyhow::Result<String> {
match prelogin(server, gp_params).await? {
Prelogin::Saml(prelogin) => Ok(prelogin.saml_request().to_string()),
Prelogin::Standard(_) => bail!("Received non-SAML prelogin response"),
}
}

View File

@@ -0,0 +1,5 @@
mod auth_server;
mod browser_auth_ext;
mod browser_auth_impl;
pub use browser_auth_ext::BrowserAuthenticator;

View File

@@ -0,0 +1,57 @@
use std::io::Cursor;
use log::info;
use tiny_http::{Header, Response, Server};
use uuid::Uuid;
pub(super) struct AuthServer {
server: Server,
auth_id: String,
}
impl AuthServer {
pub fn new() -> anyhow::Result<Self> {
let server = Server::http("127.0.0.1:0").map_err(|err| anyhow::anyhow!(err))?;
let auth_id = Uuid::new_v4().to_string();
Ok(Self { server, auth_id })
}
pub fn auth_url(&self) -> String {
format!("http://{}/{}", self.server.server_addr(), self.auth_id)
}
pub fn serve_request(&self, auth_request: &str) {
info!("auth server started at: {}", self.auth_url());
for req in self.server.incoming_requests() {
info!("received request, method: {}, url: {}", req.method(), req.url());
if req.url() != format!("/{}", self.auth_id) {
let forbidden = Response::from_string("forbidden").with_status_code(403);
let _ = req.respond(forbidden);
} else {
let auth_response = build_auth_response(auth_request);
if let Err(err) = req.respond(auth_response) {
info!("failed to respond to request: {}", err);
} else {
info!("stop the auth server");
break;
}
}
}
}
}
fn build_auth_response(auth_request: &str) -> Response<Cursor<Vec<u8>>> {
if auth_request.starts_with("http") {
let header = format!("location: {}", auth_request);
let header: Header = header.parse().unwrap();
Response::from_string("redirect")
.with_status_code(302)
.with_header(header)
} else {
let content_type: Header = "content-type: text/html".parse().unwrap();
Response::from_string(auth_request).with_header(content_type)
}
}

View File

@@ -0,0 +1,22 @@
use std::future::Future;
use gpapi::auth::SamlAuthData;
use crate::{browser_auth::browser_auth_impl::BrowserAuthenticatorImpl, AuthWindow};
pub trait BrowserAuthenticator {
fn browser_authenticate(&self, browser: Option<&str>) -> impl Future<Output = anyhow::Result<SamlAuthData>> + Send;
}
impl BrowserAuthenticator for AuthWindow<'_> {
async fn browser_authenticate(&self, browser: Option<&str>) -> anyhow::Result<SamlAuthData> {
let auth_request = self.initial_auth_request().await?;
let browser_auth = if let Some(browser) = browser {
BrowserAuthenticatorImpl::new_with_browser(&auth_request, browser)
} else {
BrowserAuthenticatorImpl::new(&auth_request)
};
browser_auth.authenticate().await
}
}

View File

@@ -0,0 +1,100 @@
use std::{env::temp_dir, fs, os::unix::fs::PermissionsExt};
use gpapi::{auth::SamlAuthData, GP_CALLBACK_PORT_FILENAME};
use log::info;
use tokio::{io::AsyncReadExt, net::TcpListener};
use super::auth_server::AuthServer;
pub struct BrowserAuthenticatorImpl<'a> {
auth_request: &'a str,
browser: Option<&'a str>,
}
impl BrowserAuthenticatorImpl<'_> {
pub fn new(auth_request: &str) -> BrowserAuthenticatorImpl {
BrowserAuthenticatorImpl {
auth_request,
browser: None,
}
}
pub fn new_with_browser<'a>(auth_request: &'a str, browser: &'a str) -> BrowserAuthenticatorImpl<'a> {
let browser = browser.trim();
BrowserAuthenticatorImpl {
auth_request,
browser: if browser.is_empty() || browser == "default" {
None
} else {
Some(browser)
},
}
}
pub async fn authenticate(&self) -> anyhow::Result<SamlAuthData> {
let auth_server = AuthServer::new()?;
let auth_url = auth_server.auth_url();
let auth_request = self.auth_request.to_string();
tokio::spawn(async move {
auth_server.serve_request(&auth_request);
});
if let Some(browser) = self.browser {
let app = find_browser_path(browser);
info!("Launching browser: {}", app);
open::with_detached(auth_url, app)?;
} else {
info!("Launching the default browser...");
webbrowser::open(&auth_url)?;
}
info!("Please continue the authentication process in the default browser");
wait_auth_data().await
}
}
fn find_browser_path(browser: &str) -> String {
if browser == "chrome" {
which::which("google-chrome-stable")
.or_else(|_| which::which("google-chrome"))
.or_else(|_| which::which("chromium"))
.map(|path| path.to_string_lossy().to_string())
.unwrap_or_else(|_| browser.to_string())
} else {
browser.into()
}
}
async fn wait_auth_data() -> anyhow::Result<SamlAuthData> {
// Start a local server to receive the browser authentication data
let listener = TcpListener::bind("127.0.0.1:0").await?;
let port = listener.local_addr()?.port();
let port_file = temp_dir().join(GP_CALLBACK_PORT_FILENAME);
// Write the port to a file
fs::write(&port_file, port.to_string())?;
fs::set_permissions(&port_file, fs::Permissions::from_mode(0o600))?;
// Remove the previous log file
let callback_log = temp_dir().join("gpcallback.log");
let _ = fs::remove_file(&callback_log);
info!("Listening authentication data on port {}", port);
info!(
"If it hangs, please check the logs at `{}` for more information",
callback_log.display()
);
let (mut socket, _) = listener.accept().await?;
info!("Received the browser authentication data from the socket");
let mut data = String::new();
socket.read_to_string(&mut data).await?;
// Remove the port file
fs::remove_file(&port_file)?;
let auth_data = SamlAuthData::from_gpcallback(&data)?;
Ok(auth_data)
}

13
crates/auth/src/lib.rs Normal file
View File

@@ -0,0 +1,13 @@
mod auth_window;
pub use auth_window::AuthWindow;
pub use auth_window::auth_prelogin;
#[cfg(feature = "browser-auth")]
mod browser_auth;
#[cfg(feature = "browser-auth")]
pub use browser_auth::BrowserAuthenticator;
#[cfg(feature = "webview-auth")]
mod webview_auth;
#[cfg(feature = "webview-auth")]
pub use webview_auth::WebviewAuthenticator;

View File

@@ -0,0 +1,9 @@
mod auth_messenger;
mod auth_response;
mod auth_settings;
mod webview_auth_ext;
#[cfg_attr(not(target_os = "macos"), path = "webview_auth/unix.rs")]
mod platform_impl;
pub use webview_auth_ext::WebviewAuthenticator;

View File

@@ -0,0 +1,108 @@
use anyhow::bail;
use gpapi::auth::SamlAuthData;
use log::{error, info};
use tokio::sync::{mpsc, RwLock};
use tokio_util::sync::CancellationToken;
pub enum AuthError {
/// 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,
/// No auth data found in headers/body
NotFound,
}
pub type AuthResult = anyhow::Result<SamlAuthData, AuthError>;
pub enum AuthEvent {
Data(SamlAuthData),
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_result(&self, result: AuthResult) {
match result {
Ok(data) => self.send_auth_data(data),
Err(err) => self.send_auth_error(err),
}
}
pub fn send_auth_error(&self, err: AuthError) {
self.send_auth_event(AuthEvent::Error(err));
}
pub fn send_auth_data(&self, data: SamlAuthData) {
self.send_auth_event(AuthEvent::Data(data));
}
pub fn schedule_raise_window(&self, delay: u64) {
let cancel_token = CancellationToken::new();
let cancel_token_clone = cancel_token.clone();
if let Ok(mut guard) = self.raise_window_cancel_token.try_write() {
// Cancel the previous raise window task if it exists
if let Some(token) = guard.take() {
token.cancel();
}
*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)) => {
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();
}
}
}
}

View File

@@ -0,0 +1,152 @@
use std::sync::Arc;
use gpapi::{
auth::{AuthDataParseResult, SamlAuthData},
error::AuthDataParseError,
};
use log::{info, warn};
use regex::Regex;
use crate::webview_auth::auth_messenger::{AuthError, AuthMessenger};
/// Trait for handling authentication response
pub trait AuthResponse {
fn get_header(&self, key: &str) -> Option<String>;
fn get_body<F>(&self, cb: F)
where
F: FnOnce(anyhow::Result<Vec<u8>>) + 'static;
fn url(&self) -> Option<String>;
fn is_acs_endpoint(&self) -> bool {
self.url().map_or(false, |url| url.ends_with("/SAML20/SP/ACS"))
}
}
pub fn read_auth_data(auth_response: &impl AuthResponse, auth_messenger: &Arc<AuthMessenger>) {
let auth_messenger = Arc::clone(auth_messenger);
match read_from_headers(auth_response) {
Ok(auth_data) => {
info!("Found auth data in headers");
auth_messenger.send_auth_data(auth_data);
}
Err(header_err) => {
info!("Failed to read auth data from headers: {}", header_err);
let is_acs_endpoint = auth_response.is_acs_endpoint();
read_from_body(auth_response, move |auth_result| {
// If the endpoint is `/SAML20/SP/ACS` and no auth data found in body, it should be considered as invalid
let auth_result = auth_result.map_err(move |e| {
info!("Failed to read auth data from body: {}", e);
if is_acs_endpoint || e.is_invalid() || header_err.is_invalid() {
AuthError::Invalid
} else {
AuthError::NotFound
}
});
auth_messenger.send_auth_result(auth_result);
});
}
}
}
fn read_from_headers(auth_response: &impl AuthResponse) -> AuthDataParseResult {
let Some(status) = auth_response.get_header("saml-auth-status") else {
info!("No SAML auth status found in headers");
return Err(AuthDataParseError::NotFound);
};
if status != "1" {
info!("Found invalid auth status: {}", status);
return Err(AuthDataParseError::Invalid);
}
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");
SamlAuthData::new(username, prelogin_cookie, portal_userauthcookie).map_err(|e| {
warn!("Found invalid auth data: {}", e);
AuthDataParseError::Invalid
})
}
fn read_from_body<F>(auth_response: &impl AuthResponse, cb: F)
where
F: FnOnce(AuthDataParseResult) + 'static,
{
auth_response.get_body(|body| match body {
Ok(body) => {
let html = String::from_utf8_lossy(&body);
cb(read_from_html(&html))
}
Err(err) => {
info!("Failed to read body: {}", err);
cb(Err(AuthDataParseError::Invalid))
}
});
}
fn read_from_html(html: &str) -> AuthDataParseResult {
if html.contains("Temporarily Unavailable") {
info!("Found 'Temporarily Unavailable' in HTML, auth failed");
return Err(AuthDataParseError::Invalid);
}
SamlAuthData::from_html(html).or_else(|err| {
if let Some(gpcallback) = extract_gpcallback(html) {
info!("Found gpcallback from html...");
SamlAuthData::from_gpcallback(&gpcallback)
} else {
Err(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&amp;un=xyz@email.com&amp;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);
}
}

View File

@@ -0,0 +1,25 @@
use std::sync::Arc;
use super::auth_messenger::AuthMessenger;
pub struct AuthRequest<'a>(&'a str);
impl<'a> AuthRequest<'a> {
pub fn new(auth_request: &'a str) -> Self {
Self(auth_request)
}
pub fn is_url(&self) -> bool {
self.0.starts_with("http")
}
pub fn as_str(&self) -> &str {
self.0
}
}
pub struct AuthSettings<'a> {
pub auth_request: AuthRequest<'a>,
pub auth_messenger: Arc<AuthMessenger>,
pub ignore_tls_errors: bool,
}

View File

@@ -0,0 +1,136 @@
use std::sync::Arc;
use anyhow::bail;
use gpapi::utils::redact::redact_uri;
use log::{info, warn};
use webkit2gtk::{
gio::Cancellable,
glib::{GString, TimeSpan},
LoadEvent, TLSErrorsPolicy, URIResponseExt, WebResource, WebResourceExt, WebView, WebViewExt, WebsiteDataManagerExt,
WebsiteDataManagerExtManual, WebsiteDataTypes,
};
use crate::webview_auth::{
auth_messenger::AuthError,
auth_response::read_auth_data,
auth_settings::{AuthRequest, AuthSettings},
};
use super::auth_response::AuthResponse;
impl AuthResponse 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)
}
fn get_body<F>(&self, cb: F)
where
F: FnOnce(anyhow::Result<Vec<u8>>) + 'static,
{
let cancellable = Cancellable::NONE;
self.data(cancellable, |data| cb(data.map_err(|e| anyhow::anyhow!(e))));
}
fn url(&self) -> Option<String> {
self.uri().map(GString::into)
}
}
pub fn clear_data<F>(wv: &WebView, cb: F)
where
F: FnOnce(anyhow::Result<()>) + Send + 'static,
{
let Some(data_manager) = wv.website_data_manager() else {
cb(Err(anyhow::anyhow!("Failed to get website data manager")));
return;
};
data_manager.clear(
WebsiteDataTypes::COOKIES,
TimeSpan(0),
Cancellable::NONE,
move |result| {
cb(result.map_err(|e| anyhow::anyhow!(e)));
},
);
}
pub fn setup_webview(wv: &WebView, auth_settings: AuthSettings) -> anyhow::Result<()> {
let AuthSettings {
auth_request,
auth_messenger,
ignore_tls_errors,
} = auth_settings;
let auth_messenger_clone = Arc::clone(&auth_messenger);
let Some(data_manager) = wv.website_data_manager() else {
bail!("Failed to get website data manager");
};
if ignore_tls_errors {
data_manager.set_tls_errors_policy(TLSErrorsPolicy::Ignore);
}
wv.connect_load_changed(move |wv, event| {
if event == LoadEvent::Started {
auth_messenger_clone.cancel_raise_window();
return;
}
if event != LoadEvent::Finished {
return;
}
let Some(main_resource) = wv.main_resource() else {
return;
};
let uri = main_resource.uri().unwrap_or("".into());
if uri.is_empty() {
warn!("Loaded an empty URI");
auth_messenger_clone.send_auth_error(AuthError::Invalid);
return;
}
read_auth_data(&main_resource, &auth_messenger_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
);
auth_messenger.send_auth_error(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
});
load_auth_request(wv, &auth_request);
Ok(())
}
pub fn load_auth_request(wv: &WebView, auth_request: &AuthRequest) {
if auth_request.is_url() {
info!("Loading auth request as URI...");
wv.load_uri(auth_request.as_str());
} else {
info!("Loading auth request as HTML...");
wv.load_html(auth_request.as_str(), None);
}
}

View File

@@ -0,0 +1,194 @@
use std::{
future::Future,
sync::Arc,
time::{Duration, Instant},
};
use anyhow::bail;
use gpapi::{auth::SamlAuthData, error::PortalError, utils::window::WindowExt};
use log::{info, warn};
use tauri::{AppHandle, WebviewUrl, WebviewWindow, WindowEvent};
use tokio::{sync::oneshot, time};
use crate::{
webview_auth::{
auth_messenger::{AuthError, AuthEvent, AuthMessenger},
auth_settings::{AuthRequest, AuthSettings},
platform_impl,
},
AuthWindow,
};
pub trait WebviewAuthenticator {
fn with_clean(self, clean: bool) -> Self;
fn webview_authenticate(&self, app_handle: &AppHandle) -> impl Future<Output = anyhow::Result<SamlAuthData>> + Send;
}
impl WebviewAuthenticator for AuthWindow<'_> {
fn with_clean(mut self, clean: bool) -> Self {
self.clean = clean;
self
}
async fn webview_authenticate(&self, app_handle: &AppHandle) -> anyhow::Result<SamlAuthData> {
let auth_window = WebviewWindow::builder(app_handle, "auth_window", WebviewUrl::default())
.title("GlobalProtect Login")
.focused(true)
.visible(false)
.center()
.build()?;
self.auth_loop(&auth_window).await
}
}
impl AuthWindow<'_> {
async fn auth_loop(&self, auth_window: &WebviewWindow) -> anyhow::Result<SamlAuthData> {
if self.clean {
self.clear_webview_data(&auth_window).await?;
}
let auth_messenger = self.setup_auth_window(&auth_window).await?;
loop {
match auth_messenger.subscribe().await? {
AuthEvent::Close => bail!("Authentication cancelled"),
AuthEvent::RaiseWindow => self.raise_window(auth_window),
AuthEvent::Error(AuthError::TlsError) => bail!(PortalError::TlsError),
AuthEvent::Error(AuthError::NotFound) => self.handle_not_found(auth_window, &auth_messenger),
AuthEvent::Error(AuthError::Invalid) => self.retry_auth(auth_window).await,
AuthEvent::Data(auth_data) => {
auth_window.close()?;
return Ok(auth_data);
}
}
}
}
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) -> anyhow::Result<Arc<AuthMessenger>> {
info!("Setting up auth window...");
let auth_messenger = Arc::new(AuthMessenger::new());
let auth_request = self.initial_auth_request().await?.into_owned();
let ignore_tls_errors = self.gp_params.ignore_tls_errors();
// 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);
});
// setup webview
let auth_messenger_clone = Arc::clone(&auth_messenger);
let (tx, rx) = oneshot::channel::<anyhow::Result<()>>();
auth_window.with_webview(move |webview| {
let auth_settings = AuthSettings {
auth_request: AuthRequest::new(&auth_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)
}
fn handle_not_found(&self, auth_window: &WebviewWindow, auth_messenger: &Arc<AuthMessenger>) {
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;
}
auth_messenger.schedule_raise_window(1);
}
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 = self.portal_prelogin().await?;
let (tx, rx) = oneshot::channel::<()>();
auth_window.with_webview(move |webview| {
let auth_request = AuthRequest::new(&auth_request);
platform_impl::load_auth_request(&webview.inner(), &auth_request);
tx.send(()).expect("Failed to send message to the channel")
})?;
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...");
if let Err(err) = auth_window.raise() {
warn!("Failed to raise window: {}", err);
}
}
}