From 3175d1083a6a6037f8539d89657ed5466ef3f908 Mon Sep 17 00:00:00 2001 From: Kevin Yue Date: Sun, 2 Feb 2025 18:37:18 +0800 Subject: [PATCH] feat: gpauth support Windows --- Cargo.lock | 4 + crates/auth/Cargo.toml | 14 ++- crates/auth/src/browser/browser_auth.rs | 8 +- crates/auth/src/webview.rs | 3 +- crates/auth/src/webview/auth_messenger.rs | 2 +- crates/auth/src/webview/webview_auth.rs | 6 +- crates/auth/src/webview/windows.rs | 142 ++++++++++++++++++++++ crates/gpapi/Cargo.toml | 5 +- crates/gpapi/src/auth.rs | 2 +- crates/gpapi/src/gateway/mod.rs | 2 + crates/gpapi/src/lib.rs | 3 + crates/gpapi/src/process/mod.rs | 3 + crates/gpapi/src/utils/mod.rs | 2 +- crates/gpapi/src/utils/shutdown_signal.rs | 24 ++-- 14 files changed, 199 insertions(+), 21 deletions(-) create mode 100644 crates/auth/src/webview/windows.rs diff --git a/Cargo.lock b/Cargo.lock index bc91e9f..426bc1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -188,6 +188,7 @@ dependencies = [ "objc2-web-kit", "open", "regex", + "serde_json", "tauri", "tiny_http", "tokio", @@ -195,7 +196,10 @@ dependencies = [ "uuid", "webbrowser", "webkit2gtk", + "webview2-com", "which", + "windows 0.58.0", + "windows-core 0.58.0", ] [[package]] diff --git a/crates/auth/Cargo.toml b/crates/auth/Cargo.toml index 21e456a..02ad25f 100644 --- a/crates/auth/Cargo.toml +++ b/crates/auth/Cargo.toml @@ -28,15 +28,21 @@ regex = { workspace = true, optional = true } tokio-util = { workspace = true, optional = true } html-escape = { version = "0.2.13", optional = true } -[target.'cfg(not(target_os = "macos"))'.dependencies] +[target.'cfg(not(any(target_os="macos", target_os="windows")))'.dependencies] webkit2gtk = { version = "2", optional = true } -[target.'cfg(target_os = "macos")'.dependencies] +[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 } +[target.'cfg(target_os="windows")'.dependencies] +webview2-com = { version = "0.34", optional = true } +windows-core = { version = "0.58", optional = true } +windows = { version = "0.58", optional = true } +serde_json = { workspace = true, optional = true } + [features] browser-auth = [ "dep:webbrowser", @@ -56,4 +62,8 @@ webview-auth = [ "dep:objc2", "dep:objc2-foundation", "dep:objc2-web-kit", + "dep:webview2-com", + "dep:windows-core", + "dep:windows", + "dep:serde_json", ] diff --git a/crates/auth/src/browser/browser_auth.rs b/crates/auth/src/browser/browser_auth.rs index c68e8b9..feabf62 100644 --- a/crates/auth/src/browser/browser_auth.rs +++ b/crates/auth/src/browser/browser_auth.rs @@ -1,4 +1,4 @@ -use std::{env::temp_dir, fs, os::unix::fs::PermissionsExt}; +use std::{env::temp_dir, fs}; use gpapi::{auth::SamlAuthData, GP_CALLBACK_PORT_FILENAME}; use log::info; @@ -96,7 +96,11 @@ async fn wait_auth_data() -> anyhow::Result { // Write the port to a file fs::write(&port_file, port.to_string())?; - fs::set_permissions(&port_file, fs::Permissions::from_mode(0o600))?; + #[cfg(unix)] + { + use os::unix::fs::PermissionsExt; + fs::set_permissions(&port_file, fs::Permissions::from_mode(0o600))?; + } // Remove the previous log file let callback_log = temp_dir().join("gpcallback.log"); diff --git a/crates/auth/src/webview.rs b/crates/auth/src/webview.rs index fc0a97a..22314fe 100644 --- a/crates/auth/src/webview.rs +++ b/crates/auth/src/webview.rs @@ -1,8 +1,9 @@ mod auth_messenger; mod webview_auth; -#[cfg_attr(not(target_os = "macos"), path = "webview/unix.rs")] +#[cfg_attr(not(any(target_os = "macos", target_os = "windows")), path = "webview/unix.rs")] #[cfg_attr(target_os = "macos", path = "webview/macos.rs")] +#[cfg_attr(windows, path = "webview/windows.rs")] mod platform_impl; pub use webview_auth::WebviewAuthenticator; diff --git a/crates/auth/src/webview/auth_messenger.rs b/crates/auth/src/webview/auth_messenger.rs index 5d70f41..0b3bc4c 100644 --- a/crates/auth/src/webview/auth_messenger.rs +++ b/crates/auth/src/webview/auth_messenger.rs @@ -15,7 +15,7 @@ pub(crate) enum AuthDataLocation { #[derive(Debug)] pub(crate) enum AuthError { /// Failed to load page due to TLS error - #[cfg(not(target_os = "macos"))] + #[cfg(not(any(target_os = "macos", target_os = "windows")))] TlsError, /// 1. Found auth data in headers/body but it's invalid /// 2. Loaded an empty page, failed to load page. etc. diff --git a/crates/auth/src/webview/webview_auth.rs b/crates/auth/src/webview/webview_auth.rs index 4f1ec71..270486f 100644 --- a/crates/auth/src/webview/webview_auth.rs +++ b/crates/auth/src/webview/webview_auth.rs @@ -115,7 +115,7 @@ impl<'a> WebviewAuthenticator<'a> { match auth_messenger.subscribe().await? { AuthEvent::Close => bail!("Authentication cancelled"), AuthEvent::RaiseWindow => self.raise_window(&auth_window), - #[cfg(not(target_os = "macos"))] + #[cfg(not(any(target_os = "macos", target_os = "windows")))] AuthEvent::Error(AuthError::TlsError) => bail!(gpapi::error::PortalError::TlsError), AuthEvent::Error(AuthError::NotFound(location)) => { info!( @@ -261,10 +261,10 @@ impl<'a> WebviewAuthenticator<'a> { info!("Raising auth window..."); - #[cfg(target_os = "macos")] + #[cfg(any(target_os = "macos", target_os = "windows"))] let result = auth_window.show(); - #[cfg(not(target_os = "macos"))] + #[cfg(not(any(target_os = "macos", target_os = "windows")))] let result = { use gpapi::utils::window::WindowExt; auth_window.raise() diff --git a/crates/auth/src/webview/windows.rs b/crates/auth/src/webview/windows.rs new file mode 100644 index 0000000..f453a5f --- /dev/null +++ b/crates/auth/src/webview/windows.rs @@ -0,0 +1,142 @@ +use log::warn; +use tauri::webview::PlatformWebview; +use webview2_com::{ + pwstr_from_str, take_pwstr, ExecuteScriptCompletedHandler, + Microsoft::Web::WebView2::Win32::{ + ICoreWebView2WebResourceResponseView, ICoreWebView2_14, ICoreWebView2_2, + COREWEBVIEW2_SERVER_CERTIFICATE_ERROR_ACTION_ALWAYS_ALLOW, + }, + ServerCertificateErrorDetectedEventHandler, WebResourceResponseReceivedEventHandler, +}; +use windows_core::{Interface, PWSTR}; + +use super::{ + auth_messenger::AuthError, + webview_auth::{GetHeader, PlatformWebviewExt}, +}; + +impl PlatformWebviewExt for PlatformWebview { + fn ignore_tls_errors(&self) -> anyhow::Result<()> { + unsafe { + let wv = self.controller().CoreWebView2()?.cast::()?; + let handler = ServerCertificateErrorDetectedEventHandler::create(Box::new(|_, e| { + if let Some(e) = e { + let _ = e.SetAction(COREWEBVIEW2_SERVER_CERTIFICATE_ERROR_ACTION_ALWAYS_ALLOW); + } + Ok(()) + })); + + wv.add_ServerCertificateErrorDetected(&handler, &mut Default::default())?; + } + + Ok(()) + } + + fn load_url(&self, url: &str) -> anyhow::Result<()> { + let url = pwstr_from_str(url); + + unsafe { self.controller().CoreWebView2()?.Navigate(url)? } + + Ok(()) + } + + fn load_html(&self, html: &str) -> anyhow::Result<()> { + let html = pwstr_from_str(html); + + unsafe { self.controller().CoreWebView2()?.NavigateToString(html)? } + + Ok(()) + } + + fn get_html(&self, callback: Box) + 'static>) { + unsafe { + match self.controller().CoreWebView2() { + Ok(wv) => { + let js = "document.documentElement.outerHTML"; + let js = pwstr_from_str(js); + + let handler = ExecuteScriptCompletedHandler::create(Box::new(move |err, html| { + if let Err(err) = err { + callback(Err(anyhow::anyhow!(err))); + return Ok(()); + } + + // The returned HTML is JSON.stringify'd string, so we need to parse it + let res = match serde_json::from_str(&html) { + Ok(Some(html)) => Ok(html), + Ok(None) => Err(anyhow::anyhow!("No HTML returned")), + Err(err) => Err(anyhow::anyhow!(err)), + }; + callback(res); + + Ok(()) + })); + + if let Err(err) = wv.ExecuteScript(js, &handler) { + warn!("Failed to execute script: {}", err); + } + } + Err(err) => callback(Err(anyhow::anyhow!(err))), + } + } + } +} + +impl GetHeader for ICoreWebView2WebResourceResponseView { + fn get_header(&self, key: &str) -> Option { + unsafe { + let headers = self.Headers().ok()?; + let key = pwstr_from_str(key); + + let mut contains = Default::default(); + headers.Contains(key, &mut contains).ok()?; + + if contains.as_bool() { + let mut value = PWSTR::null(); + headers.GetHeader(key, &mut value).ok()?; + let value = take_pwstr(value); + + Some(value) + } else { + None + } + } + } +} + +pub trait PlatformWebviewOnResponse { + fn on_response( + &self, + callback: Box) + 'static>, + ); +} + +impl PlatformWebviewOnResponse for PlatformWebview { + fn on_response( + &self, + callback: Box) + 'static>, + ) { + unsafe { + let _ = self + .controller() + .CoreWebView2() + .and_then(|wv| wv.cast::()) + .map(|wv| { + let handler = WebResourceResponseReceivedEventHandler::create(Box::new(move |_, e| { + let Some(e) = e else { + return Ok(()); + }; + + match e.Response() { + Ok(res) => callback(Ok(res)), + Err(err) => warn!("Failed to get response: {}", err), + } + + Ok(()) + })); + + let _ = wv.add_WebResourceResponseReceived(&handler, &mut Default::default()); + }); + } + } +} diff --git a/crates/gpapi/Cargo.toml b/crates/gpapi/Cargo.toml index c82ce00..01fc728 100644 --- a/crates/gpapi/Cargo.toml +++ b/crates/gpapi/Cargo.toml @@ -27,7 +27,7 @@ chacha20poly1305 = { version = "0.10", features = ["std"] } redact-engine.workspace = true url.workspace = true regex.workspace = true -uzers.workspace = true + serde_urlencoded.workspace = true md5.workspace = true sha256.workspace = true @@ -39,6 +39,9 @@ clap-verbosity-flag = { workspace = true, optional = true } env_logger = { workspace = true, optional = true } log-reload = { version = "0.1", optional = true } +[target.'cfg(target_family="unix")'.dependencies] +uzers.workspace = true + [features] tauri = ["dep:tauri"] clap = ["dep:clap", "dep:clap-verbosity-flag"] diff --git a/crates/gpapi/src/auth.rs b/crates/gpapi/src/auth.rs index aa6e559..7376d9a 100644 --- a/crates/gpapi/src/auth.rs +++ b/crates/gpapi/src/auth.rs @@ -104,7 +104,7 @@ impl SamlAuthData { } let auth_data = decode_to_string(auth_data).map_err(|e| { - warn!("Failed to decode SAML auth data: {}", e); + warn!("Failed to decode SAML auth data: {}", auth_data); AuthDataParseError::Invalid(anyhow::anyhow!(e)) })?; let auth_data = Self::from_html(&auth_data)?; diff --git a/crates/gpapi/src/gateway/mod.rs b/crates/gpapi/src/gateway/mod.rs index b768269..040f0a1 100644 --- a/crates/gpapi/src/gateway/mod.rs +++ b/crates/gpapi/src/gateway/mod.rs @@ -1,5 +1,7 @@ mod login; mod parse_gateways; + +#[cfg(unix)] pub mod hip; pub use login::*; diff --git a/crates/gpapi/src/lib.rs b/crates/gpapi/src/lib.rs index 7b085eb..1236078 100644 --- a/crates/gpapi/src/lib.rs +++ b/crates/gpapi/src/lib.rs @@ -4,7 +4,10 @@ pub mod error; pub mod gateway; pub mod gp_params; pub mod portal; + +#[cfg(unix)] pub mod process; + pub mod service; pub mod utils; diff --git a/crates/gpapi/src/process/mod.rs b/crates/gpapi/src/process/mod.rs index b5beb20..833db74 100644 --- a/crates/gpapi/src/process/mod.rs +++ b/crates/gpapi/src/process/mod.rs @@ -1,8 +1,11 @@ pub(crate) mod command_traits; + pub(crate) mod gui_helper_launcher; pub mod auth_launcher; pub mod gui_launcher; pub mod hip_launcher; pub mod service_launcher; + +#[cfg(unix)] pub mod users; diff --git a/crates/gpapi/src/utils/mod.rs b/crates/gpapi/src/utils/mod.rs index 7907512..ee800c2 100644 --- a/crates/gpapi/src/utils/mod.rs +++ b/crates/gpapi/src/utils/mod.rs @@ -9,7 +9,7 @@ pub mod lock_file; pub mod openssl; pub mod redact; pub mod request; -#[cfg(feature = "tauri")] +#[cfg(all(feature = "tauri", not(any(target_os = "macos", target_os = "windows"))))] pub mod window; mod shutdown_signal; diff --git a/crates/gpapi/src/utils/shutdown_signal.rs b/crates/gpapi/src/utils/shutdown_signal.rs index 1f1f1fc..097e96d 100644 --- a/crates/gpapi/src/utils/shutdown_signal.rs +++ b/crates/gpapi/src/utils/shutdown_signal.rs @@ -6,15 +6,21 @@ pub async fn shutdown_signal() { }; #[cfg(unix)] - let terminate = async { - signal::unix::signal(signal::unix::SignalKind::terminate()) - .expect("failed to install signal handler") - .recv() - .await; - }; + { + let terminate = async { + signal::unix::signal(signal::unix::SignalKind::terminate()) + .expect("failed to install signal handler") + .recv() + .await; + }; + tokio::select! { + _ = ctrl_c => {}, + _ = terminate => {}, + } + } - tokio::select! { - _ = ctrl_c => {}, - _ = terminate => {}, + #[cfg(not(unix))] + { + ctrl_c.await; } }