mirror of
https://github.com/yuezk/GlobalProtect-openconnect.git
synced 2025-04-02 18:31:50 -04:00
feat: support macos
This commit is contained in:
parent
d199d9e331
commit
81c5a6160c
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@ -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"
|
||||
|
4
Cargo.lock
generated
4
Cargo.lock
generated
@ -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",
|
||||
|
@ -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",
|
||||
]
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
90
crates/auth/src/webview/macos.rs
Normal file
90
crates/auth/src/webview/macos.rs
Normal 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();
|
||||
};
|
||||
}
|
75
crates/auth/src/webview/navigation_delegate.rs
Normal file
75
crates/auth/src/webview/navigation_delegate.rs
Normal 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] }
|
||||
}
|
||||
}
|
@ -76,12 +76,15 @@ impl<'a> WebviewAuthenticatorBuilder<'a> {
|
||||
event_loop: &'a EventLoop<anyhow::Result<SamlAuthData>>,
|
||||
) -> anyhow::Result<WebviewAuthenticator<'a>> {
|
||||
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;
|
||||
|
Loading…
Reference in New Issue
Block a user