feat: support macos

This commit is contained in:
Kevin Yue 2025-01-03 22:16:23 +08:00
parent d199d9e331
commit 81c5a6160c
No known key found for this signature in database
GPG Key ID: 4D3A6EE977B15AC4
8 changed files with 196 additions and 4 deletions

View File

@ -23,8 +23,12 @@
"gpgui", "gpgui",
"gpservice", "gpservice",
"hidpi", "hidpi",
"Ivars",
"jnlp", "jnlp",
"LOGNAME", "LOGNAME",
"NSHTTPURL",
"NSURL",
"objc",
"oneshot", "oneshot",
"openconnect", "openconnect",
"pkcs", "pkcs",
@ -55,6 +59,7 @@
"Vite", "Vite",
"vpnc", "vpnc",
"vpninfo", "vpninfo",
"webbrowser",
"wmctrl", "wmctrl",
"XAUTHORITY", "XAUTHORITY",
"yuezk" "yuezk"

4
Cargo.lock generated
View File

@ -178,9 +178,13 @@ name = "auth"
version = "2.4.0" version = "2.4.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"block2",
"gpapi", "gpapi",
"html-escape", "html-escape",
"log", "log",
"objc2",
"objc2-foundation",
"objc2-web-kit",
"open", "open",
"regex", "regex",
"tao 0.31.0", "tao 0.31.0",

View File

@ -32,6 +32,12 @@ html-escape = { version = "0.2.13", optional = true }
[target.'cfg(not(target_os = "macos"))'.dependencies] [target.'cfg(not(target_os = "macos"))'.dependencies]
webkit2gtk = { version = "2", optional = true } 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] [features]
browser-auth = [ browser-auth = [
"dep:webbrowser", "dep:webbrowser",
@ -47,4 +53,8 @@ webview-auth = [
"dep:tokio-util", "dep:tokio-util",
"dep:html-escape", "dep:html-escape",
"dep:webkit2gtk", "dep:webkit2gtk",
"dep:block2",
"dep:objc2",
"dep:objc2-foundation",
"dep:objc2-web-kit",
] ]

View File

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

View File

@ -71,9 +71,10 @@ where
{ {
auth_response.get_body(|body| match body { auth_response.get_body(|body| match body {
Ok(body) => { Ok(body) => {
let html = String::from_utf8_lossy(&body); if let Some(html) = body {
cb(read_from_html(&html)) cb(read_from_html(&html))
} }
}
Err(err) => { Err(err) => {
info!("Failed to read body: {}", err); info!("Failed to read body: {}", err);
cb(Err(AuthDataParseError::Invalid)) cb(Err(AuthDataParseError::Invalid))

View File

@ -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<Retained<NSHTTPURLResponse>>,
body: Option<String>,
}
impl AuthResponse {
pub fn url(&self) -> Option<String> {
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<String> {
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<F>(&self, cb: F)
where
F: FnOnce(anyhow::Result<Option<Cow<'_, str>>>) + 'static,
{
if let Some(body) = self.body.as_deref() {
cb(Ok(Some(Cow::Borrowed(body))));
} else {
cb(Ok(None));
}
}
}
pub fn connect_webview_response<F>(wv: &wry::WebView, cb: F)
where
F: Fn(anyhow::Result<AuthResponse, AuthError>) + '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();
};
}

View File

@ -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<dyn Fn(Option<Retained<NSHTTPURLResponse>>) + '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<dyn Fn(WKNavigationResponsePolicy)>,
) {
println!("navigation_policy_response start");
unsafe {
if response.isForMainFrame() {
let url_response: Retained<NSURLResponse> = response.response();
let http_response = Retained::cast::<NSHTTPURLResponse>(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<F>(on_response: F) -> Retained<Self>
where
F: Fn(Option<Retained<NSHTTPURLResponse>>) + 'static,
{
let mtm = MainThreadMarker::new().expect("Not on main thread");
let delegate = mtm.alloc::<NavigationDelegate>().set_ivars(NavigationDelegateIvars {
on_response: Box::new(on_response),
});
unsafe { msg_send_id![super(delegate), init] }
}
}

View File

@ -76,12 +76,15 @@ impl<'a> WebviewAuthenticatorBuilder<'a> {
event_loop: &'a EventLoop<anyhow::Result<SamlAuthData>>, event_loop: &'a EventLoop<anyhow::Result<SamlAuthData>>,
) -> anyhow::Result<WebviewAuthenticator<'a>> { ) -> anyhow::Result<WebviewAuthenticator<'a>> {
let window = WindowBuilder::new() let window = WindowBuilder::new()
.with_title("GlobalProtect Authentication") .with_title("GlobalProtect Login")
.with_focused(true) .with_focused(true)
.build(event_loop)?; .build(event_loop)?;
let builder = WebViewBuilder::new(); let builder = WebViewBuilder::new();
#[cfg(target_os = "macos")]
let webview = builder.build(&window)?;
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
let webview = { let webview = {
use tao::platform::unix::WindowExtUnix; use tao::platform::unix::WindowExtUnix;