feat: gpauth support Windows

This commit is contained in:
Kevin Yue 2025-02-02 18:37:18 +08:00
parent fe3d3df662
commit 3175d1083a
14 changed files with 199 additions and 21 deletions

4
Cargo.lock generated
View File

@ -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]]

View File

@ -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",
]

View File

@ -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<SamlAuthData> {
// 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");

View File

@ -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;

View File

@ -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.

View File

@ -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()

View File

@ -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::<ICoreWebView2_14>()?;
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<dyn Fn(anyhow::Result<String>) + '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<String> {
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<dyn Fn(anyhow::Result<ICoreWebView2WebResourceResponseView, AuthError>) + 'static>,
);
}
impl PlatformWebviewOnResponse for PlatformWebview {
fn on_response(
&self,
callback: Box<dyn Fn(anyhow::Result<ICoreWebView2WebResourceResponseView, AuthError>) + 'static>,
) {
unsafe {
let _ = self
.controller()
.CoreWebView2()
.and_then(|wv| wv.cast::<ICoreWebView2_2>())
.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());
});
}
}
}

View File

@ -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"]

View File

@ -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)?;

View File

@ -1,5 +1,7 @@
mod login;
mod parse_gateways;
#[cfg(unix)]
pub mod hip;
pub use login::*;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;
}
}