From 81c5a6160c8de29de128311a88dc0ba86b1afc01 Mon Sep 17 00:00:00 2001 From: Kevin Yue Date: Fri, 3 Jan 2025 22:16:23 +0800 Subject: [PATCH] feat: support macos --- .vscode/settings.json | 5 ++ Cargo.lock | 4 + crates/auth/Cargo.toml | 10 +++ crates/auth/src/webview.rs | 6 +- crates/auth/src/webview/auth_response.rs | 5 +- crates/auth/src/webview/macos.rs | 90 +++++++++++++++++++ .../auth/src/webview/navigation_delegate.rs | 75 ++++++++++++++++ crates/auth/src/webview/webview_auth.rs | 5 +- 8 files changed, 196 insertions(+), 4 deletions(-) create mode 100644 crates/auth/src/webview/macos.rs create mode 100644 crates/auth/src/webview/navigation_delegate.rs diff --git a/.vscode/settings.json b/.vscode/settings.json index de5d7ee..5b37155 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -23,8 +23,12 @@ "gpgui", "gpservice", "hidpi", + "Ivars", "jnlp", "LOGNAME", + "NSHTTPURL", + "NSURL", + "objc", "oneshot", "openconnect", "pkcs", @@ -55,6 +59,7 @@ "Vite", "vpnc", "vpninfo", + "webbrowser", "wmctrl", "XAUTHORITY", "yuezk" diff --git a/Cargo.lock b/Cargo.lock index 6effbec..a243bd0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -178,9 +178,13 @@ name = "auth" version = "2.4.0" dependencies = [ "anyhow", + "block2", "gpapi", "html-escape", "log", + "objc2", + "objc2-foundation", + "objc2-web-kit", "open", "regex", "tao 0.31.0", diff --git a/crates/auth/Cargo.toml b/crates/auth/Cargo.toml index 8221f8d..9e11afb 100644 --- a/crates/auth/Cargo.toml +++ b/crates/auth/Cargo.toml @@ -32,6 +32,12 @@ html-escape = { version = "0.2.13", optional = true } [target.'cfg(not(target_os = "macos"))'.dependencies] webkit2gtk = { version = "2", optional = true } +[target.'cfg(target_os = "macos")'.dependencies] +block2 = { version = "0.5", optional = true } +objc2 = { version = "0.5", optional = true } +objc2-foundation = { version = "0.2", optional = true } +objc2-web-kit = { version = "0.2", optional = true } + [features] browser-auth = [ "dep:webbrowser", @@ -47,4 +53,8 @@ webview-auth = [ "dep:tokio-util", "dep:html-escape", "dep:webkit2gtk", + "dep:block2", + "dep:objc2", + "dep:objc2-foundation", + "dep:objc2-web-kit", ] diff --git a/crates/auth/src/webview.rs b/crates/auth/src/webview.rs index e56c4c1..4b0c337 100644 --- a/crates/auth/src/webview.rs +++ b/crates/auth/src/webview.rs @@ -1,9 +1,13 @@ mod auth_messenger; mod auth_response; +mod webview_auth; #[cfg_attr(not(target_os = "macos"), path = "webview/unix.rs")] +#[cfg_attr(target_os = "macos", path = "webview/macos.rs")] mod platform_impl; -mod webview_auth; + +#[cfg(target_os = "macos")] +mod navigation_delegate; pub use webview_auth::WebviewAuthenticator; pub use webview_auth::WebviewAuthenticatorBuilder; diff --git a/crates/auth/src/webview/auth_response.rs b/crates/auth/src/webview/auth_response.rs index 854ca73..1b20aee 100644 --- a/crates/auth/src/webview/auth_response.rs +++ b/crates/auth/src/webview/auth_response.rs @@ -71,8 +71,9 @@ where { auth_response.get_body(|body| match body { Ok(body) => { - let html = String::from_utf8_lossy(&body); - cb(read_from_html(&html)) + if let Some(html) = body { + cb(read_from_html(&html)) + } } Err(err) => { info!("Failed to read body: {}", err); diff --git a/crates/auth/src/webview/macos.rs b/crates/auth/src/webview/macos.rs new file mode 100644 index 0000000..43c4395 --- /dev/null +++ b/crates/auth/src/webview/macos.rs @@ -0,0 +1,90 @@ +use std::{borrow::Cow, sync::Arc}; + +use block2::RcBlock; +use objc2::{ + rc::Retained, + runtime::{AnyObject, ProtocolObject}, +}; +use objc2_foundation::{NSError, NSHTTPURLResponse, NSString}; +use wry::WebViewExtMacOS; + +use super::{auth_messenger::AuthError, navigation_delegate::NavigationDelegate}; + +pub struct AuthResponse { + response: Option>, + body: Option, +} + +impl AuthResponse { + pub fn url(&self) -> Option { + let response = self.response.as_ref()?; + let url = unsafe { response.URL().and_then(|url| url.absoluteString()) }; + + url.map(|u| u.to_string()) + } + + pub fn get_header(&self, key: &str) -> Option { + let response = self.response.as_ref()?; + let value = unsafe { response.valueForHTTPHeaderField(&NSString::from_str(key)) }; + + value.map(|v| v.to_string()) + } + + pub fn get_body(&self, cb: F) + where + F: FnOnce(anyhow::Result>>) + 'static, + { + if let Some(body) = self.body.as_deref() { + cb(Ok(Some(Cow::Borrowed(body)))); + } else { + cb(Ok(None)); + } + } +} + +pub fn connect_webview_response(wv: &wry::WebView, cb: F) +where + F: Fn(anyhow::Result) + 'static, +{ + let wv = wv.webview(); + let wv_clone = Retained::clone(&wv); + + let callback = Arc::new(cb); + + let delegate = NavigationDelegate::new(move |response| { + let callback_clone = Arc::clone(&callback); + + if let Some(response) = response { + callback_clone(Ok(AuthResponse { + response: Some(response), + body: None, + })); + return; + } + + unsafe { + let callback_clone = Arc::clone(&callback); + let js_callback = RcBlock::new(move |body: *mut AnyObject, _err: *mut NSError| { + let body = body as *mut NSString; + let body = body.as_ref().unwrap(); + + callback_clone(Ok(AuthResponse { + response: None, + body: Some(body.to_string()), + })); + }); + + wv_clone.evaluateJavaScript_completionHandler( + &NSString::from_str("document.documentElement.outerHTML"), + Some(&js_callback), + ); + } + }); + + let proto_delegate = ProtocolObject::from_ref(delegate.as_ref()); + unsafe { + wv.setNavigationDelegate(Some(proto_delegate)); + // The UI will freeze if we don't call this method + let _ = wv.navigationDelegate(); + }; +} diff --git a/crates/auth/src/webview/navigation_delegate.rs b/crates/auth/src/webview/navigation_delegate.rs new file mode 100644 index 0000000..1985e79 --- /dev/null +++ b/crates/auth/src/webview/navigation_delegate.rs @@ -0,0 +1,75 @@ +use objc2::{ + declare_class, msg_send_id, + mutability::MainThreadOnly, + rc::Retained, + runtime::{NSObject, NSObjectProtocol}, + ClassType, DeclaredClass, +}; +use objc2_foundation::{MainThreadMarker, NSHTTPURLResponse, NSURLResponse}; +use objc2_web_kit::{WKNavigation, WKNavigationDelegate, WKNavigationResponse, WKNavigationResponsePolicy, WKWebView}; + +pub struct NavigationDelegateIvars { + pub on_response: Box>) + 'static>, +} + +declare_class!( + pub struct NavigationDelegate; + + unsafe impl ClassType for NavigationDelegate { + type Super = NSObject; + type Mutability = MainThreadOnly; + const NAME: &'static str = "NavigationDelegate"; + } + + impl DeclaredClass for NavigationDelegate { + type Ivars = NavigationDelegateIvars; + } + + unsafe impl NSObjectProtocol for NavigationDelegate {} + + unsafe impl WKNavigationDelegate for NavigationDelegate { + #[method(webView:decidePolicyForNavigationResponse:decisionHandler:)] + fn navigation_policy_response( + &self, + _wv: &WKWebView, + response: &WKNavigationResponse, + handler: &block2::Block, + ) { + println!("navigation_policy_response start"); + + unsafe { + if response.isForMainFrame() { + let url_response: Retained = response.response(); + let http_response = Retained::cast::(url_response); + (self.ivars().on_response)(Some(http_response)); + } + } + + println!("navigation_policy_response end"); + (*handler).call((WKNavigationResponsePolicy::Allow,)); + } + + #[method(webView:didFinishNavigation:)] + fn webview_did_finish_navigation( + &self, + _wv: &WKWebView, + _navigation: Option<&WKNavigation>, + ) { + (self.ivars().on_response)(None); + } + } +); + +impl NavigationDelegate { + pub fn new(on_response: F) -> Retained + where + F: Fn(Option>) + 'static, + { + let mtm = MainThreadMarker::new().expect("Not on main thread"); + let delegate = mtm.alloc::().set_ivars(NavigationDelegateIvars { + on_response: Box::new(on_response), + }); + + unsafe { msg_send_id![super(delegate), init] } + } +} diff --git a/crates/auth/src/webview/webview_auth.rs b/crates/auth/src/webview/webview_auth.rs index 13bf66c..a5e5fc2 100644 --- a/crates/auth/src/webview/webview_auth.rs +++ b/crates/auth/src/webview/webview_auth.rs @@ -76,12 +76,15 @@ impl<'a> WebviewAuthenticatorBuilder<'a> { event_loop: &'a EventLoop>, ) -> anyhow::Result> { let window = WindowBuilder::new() - .with_title("GlobalProtect Authentication") + .with_title("GlobalProtect Login") .with_focused(true) .build(event_loop)?; let builder = WebViewBuilder::new(); + #[cfg(target_os = "macos")] + let webview = builder.build(&window)?; + #[cfg(not(target_os = "macos"))] let webview = { use tao::platform::unix::WindowExtUnix;