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