feat: gpauth support macos

This commit is contained in:
Kevin Yue 2025-01-03 12:21:49 +00:00
parent 0c9b8e6c63
commit 25f1182556
No known key found for this signature in database
GPG Key ID: 4D3A6EE977B15AC4
23 changed files with 516 additions and 502 deletions

124
Cargo.lock generated
View File

@ -183,7 +183,7 @@ dependencies = [
"log",
"open",
"regex",
"tauri",
"tao 0.31.0",
"tiny_http",
"tokio",
"tokio-util",
@ -191,6 +191,7 @@ dependencies = [
"webbrowser",
"webkit2gtk",
"which",
"wry 0.48.0",
]
[[package]]
@ -1618,8 +1619,7 @@ dependencies = [
"gpapi",
"log",
"serde_json",
"tauri",
"tauri-build",
"tao 0.31.0",
"tempfile",
"tokio",
]
@ -4299,6 +4299,44 @@ dependencies = [
"x11-dl",
]
[[package]]
name = "tao"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc6b53216f32e60efc27dfa111268481e4dfba53e553e4cdebcaed9db36c11bb"
dependencies = [
"bitflags 2.6.0",
"cocoa",
"core-foundation 0.10.0",
"core-graphics",
"crossbeam-channel",
"dispatch",
"dlopen2",
"dpi",
"gdkwayland-sys",
"gdkx11-sys",
"gtk",
"jni",
"lazy_static",
"libc",
"log",
"ndk",
"ndk-context",
"ndk-sys",
"objc",
"once_cell",
"parking_lot",
"raw-window-handle",
"scopeguard",
"tao-macros",
"unicode-segmentation",
"url",
"windows 0.58.0",
"windows-core 0.58.0",
"windows-version",
"x11-dl",
]
[[package]]
name = "tao-macros"
version = "0.1.3"
@ -4372,7 +4410,7 @@ dependencies = [
"url",
"urlpattern",
"webkit2gtk",
"webview2-com",
"webview2-com 0.33.0",
"window-vibrancy",
"windows 0.58.0",
]
@ -4475,14 +4513,14 @@ dependencies = [
"percent-encoding",
"raw-window-handle",
"softbuffer",
"tao",
"tao 0.30.8",
"tauri-runtime",
"tauri-utils",
"url",
"webkit2gtk",
"webview2-com",
"webview2-com 0.33.0",
"windows 0.58.0",
"wry",
"wry 0.47.2",
]
[[package]]
@ -5292,7 +5330,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f61ff3d9d0ee4efcb461b14eb3acfda2702d10dc329f339303fc3e57215ae2c"
dependencies = [
"webview2-com-macros",
"webview2-com-sys",
"webview2-com-sys 0.33.0",
"windows 0.58.0",
"windows-core 0.58.0",
"windows-implement 0.58.0",
"windows-interface 0.58.0",
]
[[package]]
name = "webview2-com"
version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "823e7ebcfaea51e78f72c87fc3b65a1e602c321f407a0b36dbb327d7bb7cd921"
dependencies = [
"webview2-com-macros",
"webview2-com-sys 0.34.0",
"windows 0.58.0",
"windows-core 0.58.0",
"windows-implement 0.58.0",
@ -5321,6 +5373,17 @@ dependencies = [
"windows-core 0.58.0",
]
[[package]]
name = "webview2-com-sys"
version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a82bce72db6e5ee83c68b5de1e2cd6ea195b9fbff91cb37df5884cbe3222df4"
dependencies = [
"thiserror 1.0.69",
"windows 0.58.0",
"windows-core 0.58.0",
]
[[package]]
name = "which"
version = "7.0.1"
@ -5822,7 +5885,50 @@ dependencies = [
"url",
"webkit2gtk",
"webkit2gtk-sys",
"webview2-com",
"webview2-com 0.33.0",
"windows 0.58.0",
"windows-core 0.58.0",
"windows-version",
"x11-dl",
]
[[package]]
name = "wry"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e644bf458e27b11b0ecafc9e5633d1304fdae82baca1d42185669752fe6ca4f"
dependencies = [
"base64 0.22.1",
"block2",
"cookie",
"crossbeam-channel",
"dpi",
"dunce",
"gdkx11",
"gtk",
"html5ever",
"http",
"javascriptcore-rs",
"jni",
"kuchikiki",
"libc",
"ndk",
"objc2",
"objc2-app-kit",
"objc2-foundation",
"objc2-ui-kit",
"objc2-web-kit",
"once_cell",
"percent-encoding",
"raw-window-handle",
"sha2",
"soup3",
"tao-macros",
"thiserror 2.0.9",
"url",
"webkit2gtk",
"webkit2gtk-sys",
"webview2-com 0.34.0",
"windows 0.58.0",
"windows-core 0.58.0",
"windows-version",

View File

@ -6,8 +6,8 @@ version.workspace = true
edition.workspace = true
license.workspace = true
[build-dependencies]
tauri-build = { version = "2", features = [], optional = true }
# [build-dependencies]
# tauri-build = { version = "2", features = [], optional = true }
[dependencies]
gpapi = { path = "../../crates/gpapi", features = ["clap"] }
@ -25,8 +25,10 @@ tempfile.workspace = true
compile-time.workspace = true
# webview auth dependencies
tauri = { workspace = true, optional = true }
# tauri = { workspace = true, optional = true }
tao = { version = "0.31", optional = true }
[features]
default = ["webview-auth"]
webview-auth = ["auth/webview-auth", "dep:tauri", "dep:tauri-build"]
webview-auth = ["auth/webview-auth", "dep:tao"]

View File

@ -1,4 +1,4 @@
fn main() {
#[cfg(feature = "webview-auth")]
tauri_build::build()
// #[cfg(feature = "webview-auth")]
// tauri_build::build()
}

View File

@ -1,6 +1,6 @@
use std::borrow::Cow;
use auth::{auth_prelogin, Authenticator, BrowserAuthenticator};
use auth::{auth_prelogin, BrowserAuthenticator};
use clap::Parser;
use gpapi::{
auth::{SamlAuthData, SamlAuthResult},
@ -119,19 +119,20 @@ impl Cli {
};
let auth_request: &'static str = Box::leak(auth_request.into_owned().into_boxed_str());
let authenticator = Authenticator::new(&server, gp_params).with_auth_request(&auth_request);
#[cfg(feature = "webview-auth")]
let browser = self
.browser
.as_deref()
.or_else(|| self.default_browser.then_some("default"));
.or_else(|| self.default_browser.then(|| "default"));
#[cfg(not(feature = "webview-auth"))]
let browser = self.browser.as_deref().or(Some("default"));
if browser.is_some() {
let auth_result = authenticator.browser_authenticate(browser).await;
if let Some(browser) = browser {
let authenticator = BrowserAuthenticator::new(auth_request, browser);
let auth_result = authenticator.authenticate().await;
print_auth_result(auth_result);
// explicitly drop openssl_conf to avoid the unused variable warning
@ -140,7 +141,13 @@ impl Cli {
}
#[cfg(feature = "webview-auth")]
crate::webview_auth::authenticate(&self, authenticator, openssl_conf)?;
{
let builder = auth::WebviewAuthenticator::builder(server, gp_params)
.auth_request(auth_request)
.clean(self.clean);
crate::webview_auth::authenticate(builder, openssl_conf).await?;
}
// crate::webview_auth::authenticate(self, openssl_conf).await?;
Ok(())
}

View File

@ -1,6 +1,7 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod cli;
#[cfg(feature = "webview-auth")]
mod webview_auth;

View File

@ -1,34 +1,31 @@
use auth::{Authenticator, WebviewAuthenticator};
use auth::WebviewAuthenticatorBuilder;
use log::info;
use tauri::RunEvent;
use tao::{
event::{Event, WindowEvent},
event_loop::{ControlFlow, EventLoopBuilder},
};
use tempfile::NamedTempFile;
use crate::cli::{print_auth_result, Cli};
pub fn authenticate(
cli: &Cli,
authenticator: Authenticator<'static>,
pub async fn authenticate<'a>(
builder: WebviewAuthenticatorBuilder<'a>,
mut openssl_conf: Option<NamedTempFile>,
) -> anyhow::Result<()> {
let authenticator = authenticator.with_clean(cli.clean);
let event_loop = EventLoopBuilder::with_user_event().build();
let authenticator = builder.build(&event_loop)?;
tauri::Builder::default()
.setup(move |app| {
let app_handle = app.handle().clone();
authenticator.authenticate().await?;
tauri::async_runtime::spawn(async move {
let auth_result = authenticator.webview_authenticate(&app_handle).await;
print_auth_result(auth_result);
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
// Ensure the app exits after the authentication process
app_handle.exit(0);
});
Ok(())
})
.build(tauri::generate_context!())?
.run(move |_app_handle, event| {
if let RunEvent::Exit = event {
if let Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} = event
{
*control_flow = ControlFlow::Exit;
if let Some(file) = openssl_conf.take() {
if let Err(err) = file.close() {
info!("Error closing OpenSSL config file: {}", err);
@ -38,4 +35,29 @@ pub fn authenticate(
});
Ok(())
// tauri::Builder::default()
// .setup(move |app| {
// let app_handle = app.handle().clone();
// tauri::async_runtime::spawn(async move {
// let auth_result = authenticator.webview_authenticate(&app_handle).await;
// print_auth_result(auth_result);
// // Ensure the app exits after the authentication process
// app_handle.exit(0);
// });
// Ok(())
// })
// .build(tauri::generate_context!())?
// .run(move |_app_handle, event| {
// if let RunEvent::Exit = event {
// if let Some(file) = openssl_conf.take() {
// if let Err(err) = file.close() {
// info!("Error closing OpenSSL config file: {}", err);
// }
// }
// }
// });
}

View File

@ -23,7 +23,8 @@ tiny_http = { version = "0.12", optional = true }
uuid = { version = "1", optional = true, features = ["v4"] }
# Webview auth dependencies
tauri = { workspace = true, optional = true }
wry = { version = "0.48", optional = true }
tao = { version = "0.31", optional = true }
regex = { workspace = true, optional = true }
tokio-util = { workspace = true, optional = true }
html-escape = { version = "0.2.13", optional = true }
@ -40,10 +41,10 @@ browser-auth = [
"dep:uuid",
]
webview-auth = [
"dep:tauri",
"dep:wry",
"dep:tao",
"dep:regex",
"dep:tokio-util",
"dep:html-escape",
"dep:webkit2gtk",
"gpapi/tauri",
]

View File

@ -6,11 +6,20 @@ use gpapi::{
portal::{prelogin, Prelogin},
};
#[cfg(feature = "webview-auth")]
pub trait ResponseReader {
}
pub struct Authenticator<'a> {
server: &'a str,
auth_request: Option<&'a str>,
pub(crate) gp_params: &'a GpParams,
#[cfg(feature = "webview-auth")]
pub(crate) window: Option<tao::window::Window>,
// #[cfg(feature = "webview-auth")]
// pub(crate) response_reader: Option<Box<dyn ResponseReader>>,
#[cfg(feature = "webview-auth")]
pub(crate) clean: bool,
#[cfg(feature = "webview-auth")]
@ -24,6 +33,11 @@ impl<'a> Authenticator<'a> {
gp_params,
auth_request: None,
#[cfg(feature = "webview-auth")]
window: None,
// #[cfg(feature = "webview-auth")]
// response_reader: None,
#[cfg(feature = "webview-auth")]
clean: false,
#[cfg(feature = "webview-auth")]

View File

@ -0,0 +1,4 @@
mod auth_server;
mod browser_auth;
pub use browser_auth::BrowserAuthenticator;

View File

@ -4,30 +4,45 @@ use gpapi::{auth::SamlAuthData, GP_CALLBACK_PORT_FILENAME};
use log::info;
use tokio::{io::AsyncReadExt, net::TcpListener};
use super::auth_server::AuthServer;
use crate::browser::auth_server::AuthServer;
pub(super) struct BrowserAuthenticatorImpl<'a> {
pub enum Browser<'a> {
Default,
Chrome,
Firefox,
Other(&'a str),
}
impl<'a> Browser<'a> {
pub fn from_str(browser: &'a str) -> Self {
match browser.to_lowercase().as_str() {
"default" => Browser::Default,
"chrome" => Browser::Chrome,
"firefox" => Browser::Firefox,
_ => Browser::Other(browser),
}
}
fn as_str(&self) -> &str {
match self {
Browser::Default => "default",
Browser::Chrome => "chrome",
Browser::Firefox => "firefox",
Browser::Other(browser) => browser,
}
}
}
pub struct BrowserAuthenticator<'a> {
auth_request: &'a str,
browser: Option<&'a str>,
browser: Browser<'a>,
}
impl BrowserAuthenticatorImpl<'_> {
pub fn new(auth_request: &str) -> BrowserAuthenticatorImpl {
BrowserAuthenticatorImpl {
impl<'a> BrowserAuthenticator<'a> {
pub fn new(auth_request: &'a str, browser: &'a str) -> Self {
Self {
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)
},
browser: Browser::from_str(browser),
}
}
@ -40,14 +55,17 @@ impl BrowserAuthenticatorImpl<'_> {
auth_server.serve_request(&auth_request);
});
if let Some(browser) = self.browser {
let app = find_browser_path(browser);
match self.browser {
Browser::Default => {
info!("Launching the default browser...");
webbrowser::open(&auth_url)?;
}
_ => {
let app = find_browser_path(&self.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");
@ -55,15 +73,18 @@ impl BrowserAuthenticatorImpl<'_> {
}
}
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"))
fn find_browser_path(browser: &Browser) -> String {
match browser {
Browser::Chrome => {
const CHROME_VARIANTS: &[&str] = &["google-chrome-stable", "google-chrome", "chromium"];
CHROME_VARIANTS
.iter()
.find_map(|&browser_name| which::which(browser_name).ok())
.map(|path| path.to_string_lossy().to_string())
.unwrap_or_else(|_| browser.to_string())
} else {
browser.into()
.unwrap_or_else(|| browser.as_str().to_string())
}
_ => browser.as_str().to_string(),
}
}

View File

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

View File

@ -1,22 +0,0 @@
use std::future::Future;
use gpapi::auth::SamlAuthData;
use crate::{browser_auth::browser_auth_impl::BrowserAuthenticatorImpl, Authenticator};
pub trait BrowserAuthenticator {
fn browser_authenticate(&self, browser: Option<&str>) -> impl Future<Output = anyhow::Result<SamlAuthData>> + Send;
}
impl BrowserAuthenticator for Authenticator<'_> {
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

@ -3,11 +3,13 @@ pub use authenticator::auth_prelogin;
pub use authenticator::Authenticator;
#[cfg(feature = "browser-auth")]
mod browser_auth;
mod browser;
#[cfg(feature = "browser-auth")]
pub use browser_auth::BrowserAuthenticator;
pub use browser::*;
#[cfg(feature = "webview-auth")]
mod webview_auth;
mod webview;
#[cfg(feature = "webview-auth")]
pub use webview_auth::WebviewAuthenticator;
pub use webview::*;

View File

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

View File

@ -4,6 +4,7 @@ use log::{error, info};
use tokio::sync::{mpsc, RwLock};
use tokio_util::sync::CancellationToken;
#[derive(Debug)]
pub enum AuthError {
/// Failed to load page due to TLS error
TlsError,

View File

@ -1,5 +1,3 @@
use std::sync::Arc;
use gpapi::{
auth::{AuthDataParseResult, SamlAuthData},
error::AuthDataParseError,
@ -7,35 +5,29 @@ use gpapi::{
use log::{info, warn};
use regex::Regex;
use crate::webview_auth::auth_messenger::{AuthError, AuthMessenger};
use crate::webview::auth_messenger::AuthError;
/// Trait for handling authentication response
pub trait AuthResponse {
fn get_header(&self, key: &str) -> Option<String>;
fn get_body<F>(&self, cb: F)
use super::{auth_messenger::AuthResult, platform_impl::AuthResponse};
fn is_acs_endpoint(auth_response: &AuthResponse) -> bool {
auth_response.url().map_or(false, |url| url.ends_with("/SAML20/SP/ACS"))
}
pub fn read_auth_data<F>(auth_response: AuthResponse, 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) {
F: Fn(AuthResult) + 'static,
{
match read_from_headers(&auth_response) {
Ok(auth_data) => {
info!("Found auth data in headers");
auth_messenger.send_auth_data(auth_data);
cb(Ok(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| {
let is_acs_endpoint = is_acs_endpoint(&auth_response);
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);
@ -46,13 +38,13 @@ pub fn read_auth_data(auth_response: &impl AuthResponse, auth_messenger: &Arc<Au
}
});
auth_messenger.send_auth_result(auth_result);
cb(auth_result);
});
}
}
}
fn read_from_headers(auth_response: &impl AuthResponse) -> AuthDataParseResult {
fn read_from_headers(auth_response: &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);
@ -73,7 +65,7 @@ fn read_from_headers(auth_response: &impl AuthResponse) -> AuthDataParseResult {
})
}
fn read_from_body<F>(auth_response: &impl AuthResponse, cb: F)
fn read_from_body<F>(auth_response: &AuthResponse, cb: F)
where
F: FnOnce(AuthDataParseResult) + 'static,
{

View File

@ -0,0 +1,96 @@
use std::sync::Arc;
use gpapi::utils::redact::redact_uri;
use log::warn;
use webkit2gtk::{
gio::Cancellable, glib::GString, LoadEvent, URIResponseExt, WebResource, WebResourceExt, WebView, WebViewExt,
};
use wry::WebViewExtUnix;
use crate::webview::auth_messenger::AuthError;
pub struct AuthResponse {
web_resource: WebResource,
}
impl AuthResponse {
pub fn url(&self) -> Option<String> {
self.web_resource.uri().map(GString::into)
}
pub fn get_header(&self, key: &str) -> Option<String> {
self
.web_resource
.response()
.and_then(|response| response.http_headers())
.and_then(|headers| headers.one(key))
.map(GString::into)
}
pub fn get_body<F>(&self, cb: F)
where
F: FnOnce(anyhow::Result<Vec<u8>>) + 'static,
{
let cancellable = Cancellable::NONE;
self.web_resource.data(cancellable, move |data| {
cb(data.map_err(|e| anyhow::anyhow!(e)));
});
}
}
pub fn connect_webview_response<F>(wv: &wry::WebView, cb: F)
where
F: Fn(anyhow::Result<AuthResponse, AuthError>) + 'static,
{
let wv = wv.webview();
let cb = Arc::new(cb);
let cb_clone = Arc::clone(&cb);
wv.connect_load_changed(move |wv, event| {
if event == LoadEvent::Started {
// TODO;
// auth_messenger_clone.cancel_raise_window();
return;
}
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() {
warn!("Loaded an empty URI");
cb_clone(Err(AuthError::Invalid));
return;
}
let response = AuthResponse { web_resource };
cb_clone(Ok(response));
});
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
);
cb(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
});
}

View File

@ -0,0 +1,127 @@
use std::borrow::Cow;
use gpapi::{auth::SamlAuthData, gp_params::GpParams};
use tao::{
event_loop::EventLoop,
window::{Window, WindowBuilder},
};
use wry::WebViewBuilder;
use crate::{auth_prelogin, webview::auth_response::read_auth_data};
use super::platform_impl::connect_webview_response;
pub struct WebviewAuthenticator<'a> {
server: &'a str,
gp_params: &'a GpParams,
auth_request: Option<&'a str>,
clean: bool,
window: Window,
webview: wry::WebView,
// response_reader: AuthResponseReader,
// pub(crate) window: Option<tao::window::Window>,
// #[cfg(feature = "webview-auth")]
// pub(crate) response_reader: Option<Box<dyn ResponseReader>>,
// pub(crate) is_retrying: tokio::sync::RwLock<bool>,
}
impl<'a> WebviewAuthenticator<'a> {
pub fn builder(server: &'a str, gp_params: &'a GpParams) -> WebviewAuthenticatorBuilder<'a> {
WebviewAuthenticatorBuilder {
server,
gp_params,
auth_request: None,
clean: false,
}
}
pub async fn authenticate(&self) -> anyhow::Result<()> {
let auth_request = match self.auth_request {
Some(auth_request) => Cow::Borrowed(auth_request),
None => Cow::Owned(auth_prelogin(&self.server, &self.gp_params).await?),
};
if auth_request.starts_with("http") {
self.webview.load_url(&auth_request)?;
} else {
self.webview.load_html(&auth_request)?;
}
Ok(())
}
}
pub struct WebviewAuthenticatorBuilder<'a> {
server: &'a str,
gp_params: &'a GpParams,
auth_request: Option<&'a str>,
clean: bool,
}
impl<'a> WebviewAuthenticatorBuilder<'a> {
pub fn auth_request(mut self, auth_request: &'a str) -> Self {
self.auth_request = Some(auth_request);
self
}
pub fn clean(mut self, clean: bool) -> Self {
self.clean = clean;
self
}
pub fn build(
self,
event_loop: &'a EventLoop<anyhow::Result<SamlAuthData>>,
) -> anyhow::Result<WebviewAuthenticator<'a>> {
let window = WindowBuilder::new()
.with_title("GlobalProtect Authentication")
.with_focused(true)
.build(event_loop)?;
let builder = WebViewBuilder::new();
#[cfg(not(target_os = "macos"))]
let webview = {
use tao::platform::unix::WindowExtUnix;
use wry::WebViewBuilderExtUnix;
let vbox = window
.default_vbox()
.ok_or_else(|| anyhow::anyhow!("Failed to get default vbox"))?;
builder.build_gtk(vbox)?
};
connect_webview_response(&webview, |response| {
// println!("Received response: {:?}", response.unwrap().url());
match response {
Ok(response) => read_auth_data(response, |auth_result| {
println!("Auth result: {:?}", auth_result);
}),
Err(err) => todo!(),
}
});
// let event_proxy = event_loop.create_proxy();
// let response_reader = AuthResponseReader::new(&webview).on_response(move |response| {
// // println!("Received response: {:?}", response.unwrap().url());
// match response {
// Ok(response) => read_auth_data(response, |auth_result| {
// // println!("Auth result: {:?}", auth_result);
// }),
// Err(err) => todo!(),
// }
// });
Ok(WebviewAuthenticator {
server: self.server,
gp_params: self.gp_params,
auth_request: None,
clean: false,
window,
webview,
// response_reader,
})
}
}

View File

@ -1,9 +0,0 @@
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

@ -1,25 +0,0 @@
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

@ -1,136 +0,0 @@
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

@ -1,194 +0,0 @@
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,
},
Authenticator,
};
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 Authenticator<'_> {
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 Authenticator<'_> {
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);
}
}
}