diff --git a/.vscode/settings.json b/.vscode/settings.json index 5422e94..fe8d44a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,6 +4,7 @@ "clientos", "gpcommon", "jnlp", + "oneshot", "openconnect", "prelogin", "prelogon", diff --git a/Cargo.lock b/Cargo.lock index 98ea59d..927e444 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,6 +62,7 @@ dependencies = [ "tauri-plugin-log", "tokio", "url", + "veil", "webkit2gtk", ] @@ -3407,6 +3408,27 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "veil" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb8e42ca783c4c7ced40f4f0e11f13d545791c002a2e7adbe6d740b853087880" +dependencies = [ + "once_cell", + "veil-macros", +] + +[[package]] +name = "veil-macros" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1eef6b882bba6052c6ab6a751f8f765794de7f957cbf0c5a97e7d2b46a3ae60d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.16", +] + [[package]] name = "version-compare" version = "0.0.11" diff --git a/gpgui/package.json b/gpgui/package.json index b68e643..f62003e 100644 --- a/gpgui/package.json +++ b/gpgui/package.json @@ -21,7 +21,7 @@ "tauri-plugin-log-api": "github:tauri-apps/tauri-plugin-log" }, "devDependencies": { - "@tauri-apps/cli": "^1.2.3", + "@tauri-apps/cli": "^1.3.1", "@types/react": "^18.0.27", "@types/react-dom": "^18.0.10", "@vitejs/plugin-react-swc": "^3.0.0", diff --git a/gpgui/pnpm-lock.yaml b/gpgui/pnpm-lock.yaml index 3df117a..54527ec 100644 --- a/gpgui/pnpm-lock.yaml +++ b/gpgui/pnpm-lock.yaml @@ -34,8 +34,8 @@ dependencies: devDependencies: '@tauri-apps/cli': - specifier: ^1.2.3 - version: 1.2.3 + specifier: ^1.3.1 + version: 1.3.1 '@types/react': specifier: ^18.0.27 version: 18.0.28 @@ -853,8 +853,8 @@ packages: engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'} dev: false - /@tauri-apps/cli-darwin-arm64@1.2.3: - resolution: {integrity: sha512-phJN3fN8FtZZwqXg08bcxfq1+X1JSDglLvRxOxB7VWPq+O5SuB8uLyssjJsu+PIhyZZnIhTGdjhzLSFhSXfLsw==} + /@tauri-apps/cli-darwin-arm64@1.3.1: + resolution: {integrity: sha512-QlepYVPgOgspcwA/u4kGG4ZUijlXfdRtno00zEy+LxinN/IRXtk+6ErVtsmoLi1ZC9WbuMwzAcsRvqsD+RtNAg==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] @@ -862,8 +862,8 @@ packages: dev: true optional: true - /@tauri-apps/cli-darwin-x64@1.2.3: - resolution: {integrity: sha512-jFZ/y6z8z6v4yliIbXKBXA7BJgtZVMsITmEXSuD6s5+eCOpDhQxbRkr6CA+FFfr+/r96rWSDSgDenDQuSvPAKw==} + /@tauri-apps/cli-darwin-x64@1.3.1: + resolution: {integrity: sha512-fKcAUPVFO3jfDKXCSDGY0MhZFF/wDtx3rgFnogWYu4knk38o9RaqRkvMvqJhLYPuWaEM5h6/z1dRrr9KKCbrVg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] @@ -871,8 +871,8 @@ packages: dev: true optional: true - /@tauri-apps/cli-linux-arm-gnueabihf@1.2.3: - resolution: {integrity: sha512-C7h5vqAwXzY0kRGSU00Fj8PudiDWFCiQqqUNI1N+fhCILrzWZB9TPBwdx33ZfXKt/U4+emdIoo/N34v3TiAOmQ==} + /@tauri-apps/cli-linux-arm-gnueabihf@1.3.1: + resolution: {integrity: sha512-+4H0dv8ltJHYu/Ma1h9ixUPUWka9EjaYa8nJfiMsdCI4LJLNE6cPveE7RmhZ59v9GW1XB108/k083JUC/OtGvA==} engines: {node: '>= 10'} cpu: [arm] os: [linux] @@ -880,8 +880,8 @@ packages: dev: true optional: true - /@tauri-apps/cli-linux-arm64-gnu@1.2.3: - resolution: {integrity: sha512-buf1c8sdkuUzVDkGPQpyUdAIIdn5r0UgXU6+H5fGPq/Xzt5K69JzXaeo6fHsZEZghbV0hOK+taKV4J0m30UUMQ==} + /@tauri-apps/cli-linux-arm64-gnu@1.3.1: + resolution: {integrity: sha512-Pj3odVO1JAxLjYmoXKxcrpj/tPxcA8UP8N06finhNtBtBaxAjrjjxKjO4968KB0BUH7AASIss9EL4Tr0FGnDuw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -889,8 +889,8 @@ packages: dev: true optional: true - /@tauri-apps/cli-linux-arm64-musl@1.2.3: - resolution: {integrity: sha512-x88wPS9W5xAyk392vc4uNHcKBBvCp0wf4H9JFMF9OBwB7vfd59LbQCFcPSu8f0BI7bPrOsyHqspWHuFL8ojQEA==} + /@tauri-apps/cli-linux-arm64-musl@1.3.1: + resolution: {integrity: sha512-tA0JdDLPFaj42UDIVcF2t8V0tSha40rppcmAR/MfQpTCxih6399iMjwihz9kZE1n4b5O4KTq9GliYo50a8zYlQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -898,8 +898,8 @@ packages: dev: true optional: true - /@tauri-apps/cli-linux-x64-gnu@1.2.3: - resolution: {integrity: sha512-ZMz1jxEVe0B4/7NJnlPHmwmSIuwiD6ViXKs8F+OWWz2Y4jn5TGxWKFg7DLx5OwQTRvEIZxxT7lXHi5CuTNAxKg==} + /@tauri-apps/cli-linux-x64-gnu@1.3.1: + resolution: {integrity: sha512-FDU+Mnvk6NLkqQimcNojdKpMN4Y3W51+SQl+NqG9AFCWprCcSg62yRb84751ujZuf2MGT8HQOfmd0i77F4Q3tQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -907,8 +907,8 @@ packages: dev: true optional: true - /@tauri-apps/cli-linux-x64-musl@1.2.3: - resolution: {integrity: sha512-B/az59EjJhdbZDzawEVox0LQu2ZHCZlk8rJf85AMIktIUoAZPFbwyiUv7/zjzA/sY6Nb58OSJgaPL2/IBy7E0A==} + /@tauri-apps/cli-linux-x64-musl@1.3.1: + resolution: {integrity: sha512-MpO3akXFmK8lZYEbyQRDfhdxz1JkTBhonVuz5rRqxwA7gnGWHa1aF1+/2zsy7ahjB2tQ9x8DDFDMdVE20o9HrA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -916,8 +916,8 @@ packages: dev: true optional: true - /@tauri-apps/cli-win32-ia32-msvc@1.2.3: - resolution: {integrity: sha512-ypdO1OdC5ugNJAKO2m3sb1nsd+0TSvMS9Tr5qN/ZSMvtSduaNwrcZ3D7G/iOIanrqu/Nl8t3LYlgPZGBKlw7Ng==} + /@tauri-apps/cli-win32-ia32-msvc@1.3.1: + resolution: {integrity: sha512-9Boeo3K5sOrSBAZBuYyGkpV2RfnGQz3ZhGJt4hE6P+HxRd62lS6+qDKAiw1GmkZ0l1drc2INWrNeT50gwOKwIQ==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] @@ -925,8 +925,8 @@ packages: dev: true optional: true - /@tauri-apps/cli-win32-x64-msvc@1.2.3: - resolution: {integrity: sha512-CsbHQ+XhnV/2csOBBDVfH16cdK00gNyNYUW68isedmqcn8j+s0e9cQ1xXIqi+Hue3awp8g3ImYN5KPepf3UExw==} + /@tauri-apps/cli-win32-x64-msvc@1.3.1: + resolution: {integrity: sha512-wMrTo91hUu5CdpbElrOmcZEoJR4aooTG+fbtcc87SMyPGQy1Ux62b+ZdwLvL1sVTxnIm//7v6QLRIWGiUjCPwA==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -934,20 +934,20 @@ packages: dev: true optional: true - /@tauri-apps/cli@1.2.3: - resolution: {integrity: sha512-erxtXuPhMEGJPBtnhPILD4AjuT81GZsraqpFvXAmEJZ2p8P6t7MVBifCL8LznRknznM3jn90D3M8RNBP3wcXTw==} + /@tauri-apps/cli@1.3.1: + resolution: {integrity: sha512-o4I0JujdITsVRm3/0spfJX7FcKYrYV1DXJqzlWIn6IY25/RltjU6qbC1TPgVww3RsRX63jyVUTcWpj5wwFl+EQ==} engines: {node: '>= 10'} hasBin: true optionalDependencies: - '@tauri-apps/cli-darwin-arm64': 1.2.3 - '@tauri-apps/cli-darwin-x64': 1.2.3 - '@tauri-apps/cli-linux-arm-gnueabihf': 1.2.3 - '@tauri-apps/cli-linux-arm64-gnu': 1.2.3 - '@tauri-apps/cli-linux-arm64-musl': 1.2.3 - '@tauri-apps/cli-linux-x64-gnu': 1.2.3 - '@tauri-apps/cli-linux-x64-musl': 1.2.3 - '@tauri-apps/cli-win32-ia32-msvc': 1.2.3 - '@tauri-apps/cli-win32-x64-msvc': 1.2.3 + '@tauri-apps/cli-darwin-arm64': 1.3.1 + '@tauri-apps/cli-darwin-x64': 1.3.1 + '@tauri-apps/cli-linux-arm-gnueabihf': 1.3.1 + '@tauri-apps/cli-linux-arm64-gnu': 1.3.1 + '@tauri-apps/cli-linux-arm64-musl': 1.3.1 + '@tauri-apps/cli-linux-x64-gnu': 1.3.1 + '@tauri-apps/cli-linux-x64-musl': 1.3.1 + '@tauri-apps/cli-win32-ia32-msvc': 1.3.1 + '@tauri-apps/cli-win32-x64-msvc': 1.3.1 dev: true /@types/parse-json@4.0.0: diff --git a/gpgui/src-tauri/Cargo.toml b/gpgui/src-tauri/Cargo.toml index 6b21661..75ae110 100644 --- a/gpgui/src-tauri/Cargo.toml +++ b/gpgui/src-tauri/Cargo.toml @@ -16,7 +16,7 @@ tauri-build = { version = "1.3", features = [] } [dependencies] gpcommon = { path = "../../gpcommon" } -tauri = { version = "1.3", features = ["http-all", "window-data-url"] } +tauri = { version = "1.3", features = ["http-all", "window-all", "window-data-url"] } tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1", features = [ "colored", ] } @@ -28,6 +28,7 @@ webkit2gtk = "0.18.2" regex = "1" url = "2.3" tokio = { version = "1.14", features = ["full"] } +veil = "0.1.6" [features] # by default Tauri runs in production mode diff --git a/gpgui/src-tauri/src/auth.rs b/gpgui/src-tauri/src/auth.rs index 2cb9afe..315f443 100644 --- a/gpgui/src-tauri/src/auth.rs +++ b/gpgui/src-tauri/src/auth.rs @@ -1,15 +1,18 @@ +use crate::utils::{clear_webview_cookies, redact_url}; use log::{debug, info, warn}; use regex::Regex; +use serde::de::Error; use serde::{Deserialize, Serialize}; use std::{sync::Arc, time::Duration}; use tauri::EventHandler; -use tauri::{AppHandle, Manager, Window, WindowBuilder, WindowEvent::CloseRequested, WindowUrl}; +use tauri::{AppHandle, Manager, Window, WindowEvent::CloseRequested, WindowUrl}; use tokio::sync::{mpsc, Mutex}; use tokio::time::timeout; +use veil::Redact; use webkit2gtk::gio::Cancellable; use webkit2gtk::glib::GString; use webkit2gtk::traits::{URIResponseExt, WebViewExt}; -use webkit2gtk::{CookieManagerExt, LoadEvent, WebContextExt, WebResource, WebResourceExt}; +use webkit2gtk::{LoadEvent, WebResource, WebResourceExt}; const AUTH_WINDOW_LABEL: &str = "auth_window"; const AUTH_ERROR_EVENT: &str = "auth-error"; @@ -28,10 +31,11 @@ pub(crate) enum SamlBinding { Post, } -#[derive(Debug, Clone, Deserialize)] +#[derive(Redact, Clone, Deserialize)] pub(crate) struct AuthRequest { #[serde(alias = "samlBinding")] saml_binding: SamlBinding, + #[redact(fixed = 10)] #[serde(alias = "samlRequest")] saml_request: String, } @@ -49,14 +53,20 @@ impl TryFrom> for AuthRequest { type Error = serde_json::Error; fn try_from(value: Option<&str>) -> Result { - serde_json::from_str(value.unwrap_or("{}")) + match value { + Some(value) => serde_json::from_str(value), + None => Err(Error::custom("No auth request provided")), + } } } -#[derive(Debug, Clone, Serialize)] +#[derive(Redact, Clone, Serialize)] pub(crate) struct AuthData { + #[redact] username: Option, + #[redact(fixed = 10)] prelogin_cookie: Option, + #[redact(fixed = 10)] portal_userauthcookie: Option, } @@ -93,27 +103,35 @@ enum AuthEvent { Cancel, } -pub(crate) async fn saml_login( - auth_request: AuthRequest, - ua: &str, - clear_cookies: bool, - app_handle: &AppHandle, -) -> tauri::Result> { +pub(crate) struct SamlLoginParams { + pub auth_request: AuthRequest, + pub user_agent: String, + pub clear_cookies: bool, + pub app_handle: AppHandle, +} + +pub(crate) async fn saml_login(params: SamlLoginParams) -> tauri::Result> { info!("Starting SAML login"); let (event_tx, event_rx) = mpsc::channel::(8); - let window = build_window(app_handle, ua)?; - setup_webview(&window, clear_cookies, event_tx.clone())?; + let window = build_window(¶ms.app_handle, ¶ms.user_agent)?; + setup_webview(&window, event_tx.clone())?; let handler = setup_window(&window, event_tx); - let result = process(&window, auth_request, event_rx).await; + if params.clear_cookies { + if let Err(err) = clear_webview_cookies(&window).await { + warn!("Failed to clear webview cookies: {}", err); + } + } + + let result = process(&window, params.auth_request, event_rx).await; window.unlisten(handler); result } fn build_window(app_handle: &AppHandle, ua: &str) -> tauri::Result { let url = WindowUrl::App("auth.html".into()); - WindowBuilder::new(app_handle, AUTH_WINDOW_LABEL, url) + Window::builder(app_handle, AUTH_WINDOW_LABEL, url) .visible(false) .title("GlobalProtect Login") .user_agent(ua) @@ -124,19 +142,11 @@ fn build_window(app_handle: &AppHandle, ua: &str) -> tauri::Result { } // Setup webview events -fn setup_webview( - window: &Window, - clear_cookies: bool, - event_tx: mpsc::Sender, -) -> tauri::Result<()> { +fn setup_webview(window: &Window, event_tx: mpsc::Sender) -> tauri::Result<()> { window.with_webview(move |wv| { let wv = wv.inner(); let event_tx_clone = event_tx.clone(); - if clear_cookies { - clear_webview_cookies(&wv); - } - wv.connect_load_changed(move |wv, event| { if LoadEvent::Finished != event { return; @@ -145,12 +155,11 @@ fn setup_webview( let uri = wv.uri().unwrap_or("".into()); // Empty URI indicates that an error occurred if uri.is_empty() { - warn!("Empty URI loaded"); - send_auth_error(&event_tx_clone, AuthError::TokenInvalid); + warn!("Empty URI loaded, retrying"); + send_auth_error(event_tx_clone.clone(), AuthError::TokenInvalid); return; } - // TODO, redact URI - debug!("Loaded URI: {}", uri); + info!("Loaded URI: {}", redact_url(&uri)); if let Some(main_res) = wv.main_resource() { parse_auth_data(&main_res, event_tx_clone.clone()); @@ -161,7 +170,7 @@ fn setup_webview( wv.connect_load_failed(move |_wv, event, _uri, err| { warn!("Load failed: {:?}, {:?}", event, err); - send_auth_error(&event_tx, AuthError::TokenInvalid); + send_auth_error(event_tx.clone(), AuthError::TokenInvalid); false }); }) @@ -170,20 +179,17 @@ fn setup_webview( fn setup_window(window: &Window, event_tx: mpsc::Sender) -> EventHandler { let event_tx_clone = event_tx.clone(); window.on_window_event(move |event| { - if let CloseRequested { api, .. } = event { - api.prevent_close(); - send_auth_event(&event_tx_clone, AuthEvent::Cancel); + if let CloseRequested { .. } = event { + send_auth_event(event_tx_clone.clone(), AuthEvent::Cancel); } }); window.listen_global(AUTH_REQUEST_EVENT, move |event| { if let Ok(payload) = TryInto::::try_into(event.payload()) { let event_tx = event_tx.clone(); - let _ = tokio::spawn(async move { - if let Err(err) = event_tx.send(AuthEvent::Request(payload)).await { - warn!("Error sending event: {}", err); - } - }); + send_auth_event(event_tx.clone(), AuthEvent::Request(payload)); + } else { + warn!("Invalid auth request payload"); } }) } @@ -199,7 +205,10 @@ async fn process( let handle = tokio::spawn(show_window_after_timeout(window.clone())); let auth_data = process_auth_event(&window, event_rx).await; - handle.abort(); + + if !handle.is_finished() { + handle.abort(); + } Ok(auth_data) } @@ -255,7 +264,6 @@ async fn process_auth_event( } AuthEvent::Cancel => { info!("User cancelled the authentication process, closing window"); - close_window(window); return None; } AuthEvent::Error(AuthError::TokenInvalid) => { @@ -292,19 +300,19 @@ async fn process_auth_event( /// we should show the window after a short timeout, it will be cancelled if the /// token is found in the response, no matter it's valid or not. async fn handle_token_not_found(window: Window, cancel_timeout_rx: Arc>>) { - match cancel_timeout_rx.try_lock() { - Ok(mut cancel_timeout_rx) => { - let duration = Duration::from_secs(SHOW_WINDOW_TIMEOUT); - if let Err(_) = timeout(duration, cancel_timeout_rx.recv()).await { - info!("Timeout expired, showing window"); - show_window(&window); - } else { - info!("Showing window timeout cancelled"); - } - } - Err(_) => { - debug!("Window will be shown by another task, skipping"); + if let Ok(mut cancel_timeout_rx) = cancel_timeout_rx.try_lock() { + let duration = Duration::from_secs(SHOW_WINDOW_TIMEOUT); + if timeout(duration, cancel_timeout_rx.recv()).await.is_err() { + info!( + "Timeout expired after {} seconds, showing window", + SHOW_WINDOW_TIMEOUT + ); + show_window(&window); + } else { + info!("Showing window timeout cancelled"); } + } else { + debug!("Window will be shown by another task, skipping"); } } @@ -314,7 +322,7 @@ fn parse_auth_data(main_res: &WebResource, event_tx: mpsc::Sender) { if let Some(response) = main_res.response() { if let Some(auth_data) = read_auth_data_from_response(&response) { debug!("Got auth data from HTTP headers: {:?}", auth_data); - send_auth_data(&event_tx, auth_data); + send_auth_data(event_tx, auth_data); return; } } @@ -326,11 +334,11 @@ fn parse_auth_data(main_res: &WebResource, event_tx: mpsc::Sender) { match read_auth_data_from_html(&html) { Ok(auth_data) => { debug!("Got auth data from HTML: {:?}", auth_data); - send_auth_data(&event_tx, auth_data); + send_auth_data(event_tx, auth_data); } Err(err) => { debug!("Error reading auth data from HTML: {:?}", err); - send_auth_error(&event_tx, err); + send_auth_error(event_tx, err); } } } @@ -387,18 +395,20 @@ fn parse_xml_tag(html: &str, tag: &str) -> Option { .map(|m| m.as_str().to_string()) } -fn send_auth_data(event_tx: &mpsc::Sender, auth_data: AuthData) { +fn send_auth_data(event_tx: mpsc::Sender, auth_data: AuthData) { send_auth_event(event_tx, AuthEvent::Success(auth_data)); } -fn send_auth_error(event_tx: &mpsc::Sender, err: AuthError) { +fn send_auth_error(event_tx: mpsc::Sender, err: AuthError) { send_auth_event(event_tx, AuthEvent::Error(err)); } -fn send_auth_event(event_tx: &mpsc::Sender, auth_event: AuthEvent) { - if let Err(err) = event_tx.blocking_send(auth_event) { - warn!("Error sending event: {}", err) - } +fn send_auth_event(event_tx: mpsc::Sender, auth_event: AuthEvent) { + let _ = tauri::async_runtime::spawn(async move { + if let Err(err) = event_tx.send(auth_event).await { + warn!("Error sending event: {}", err); + } + }); } fn show_window(window: &Window) { @@ -418,15 +428,3 @@ fn close_window(window: &Window) { warn!("Error closing window: {}", err); } } - -fn clear_webview_cookies(wv: &webkit2gtk::WebView) { - if let Some(context) = wv.context() { - if let Some(cookie_manager) = context.cookie_manager() { - #[allow(deprecated)] - cookie_manager.delete_all_cookies(); - info!("Cookies cleared"); - return; - } - } - warn!("No cookie manager found"); -} diff --git a/gpgui/src-tauri/src/commands.rs b/gpgui/src-tauri/src/commands.rs index 66078c8..384a214 100644 --- a/gpgui/src-tauri/src/commands.rs +++ b/gpgui/src-tauri/src/commands.rs @@ -1,4 +1,4 @@ -use crate::auth::{self, AuthData, AuthRequest, SamlBinding}; +use crate::auth::{self, AuthData, AuthRequest, SamlBinding, SamlLoginParams}; use gpcommon::{Client, ServerApiError, VpnStatus}; use std::sync::Arc; use tauri::{AppHandle, State}; @@ -32,13 +32,13 @@ pub(crate) async fn saml_login( request: String, app_handle: AppHandle, ) -> tauri::Result> { - let ua = "PAN GlobalProtect"; + let user_agent = String::from("PAN GlobalProtect"); let clear_cookies = false; - auth::saml_login( - AuthRequest::new(binding, request), - ua, + let params = SamlLoginParams { + auth_request: AuthRequest::new(binding, request), + user_agent, clear_cookies, - &app_handle, - ) - .await + app_handle, + }; + auth::saml_login(params).await } diff --git a/gpgui/src-tauri/src/main.rs b/gpgui/src-tauri/src/main.rs index 0191ba8..6e20310 100644 --- a/gpgui/src-tauri/src/main.rs +++ b/gpgui/src-tauri/src/main.rs @@ -13,6 +13,7 @@ use tauri_plugin_log::LogTarget; mod auth; mod commands; +mod utils; #[derive(Debug, Clone, Serialize)] struct StatusPayload { diff --git a/gpgui/src-tauri/src/utils.rs b/gpgui/src-tauri/src/utils.rs new file mode 100644 index 0000000..6e8ef95 --- /dev/null +++ b/gpgui/src-tauri/src/utils.rs @@ -0,0 +1,88 @@ +use log::{info, warn}; +use std::time::Instant; +use tauri::Window; +use tokio::sync::oneshot; +use url::{form_urlencoded, Url}; +use webkit2gtk::{ + gio::Cancellable, glib::TimeSpan, WebContextExt, WebViewExt, WebsiteDataManagerExtManual, + WebsiteDataTypes, +}; + +pub(crate) fn redact_url(url: &str) -> String { + if let Ok(mut url) = Url::parse(&url) { + if let Err(err) = url.set_host(Some("redacted")) { + warn!("Error redacting URL: {}", err); + } + + let query = url.query().unwrap_or_default(); + if !query.is_empty() { + // Replace the query value with for each key. + let redacted_query = redact_query(url.query().unwrap_or("")); + url.set_query(Some(&redacted_query)); + } + return url.to_string(); + } else { + warn!("Error parsing URL: {}", url); + url.to_string() + } +} + +fn redact_query(query: &str) -> String { + let query_pairs = form_urlencoded::parse(query.as_bytes()); + let mut redacted_pairs = query_pairs.map(|(key, _)| (key, "__redacted__")); + + form_urlencoded::Serializer::new(String::new()) + .extend_pairs(redacted_pairs.by_ref()) + .finish() +} + +pub(crate) async fn clear_webview_cookies(window: &Window) -> Result<(), tauri::Error> { + let (tx, rx) = oneshot::channel::<()>(); + + window.with_webview(|wv| { + let wv = wv.inner(); + let context = match wv.context() { + Some(context) => context, + None => { + return send_error(tx, "No context found"); + } + }; + let data_manager = match context.website_data_manager() { + Some(manager) => manager, + None => { + return send_error(tx, "No data manager found"); + } + }; + + let now = Instant::now(); + data_manager.clear( + WebsiteDataTypes::COOKIES, + TimeSpan(0), + Cancellable::NONE, + move |result| match result { + Err(err) => { + send_error(tx, &err.to_string()); + } + Ok(_) => { + info!("Cookies cleared in {} ms", now.elapsed().as_millis()); + send_result(tx); + } + }, + ); + })?; + + rx.await.map_err(|_| tauri::Error::FailedToSendMessage) +} + +fn send_error(tx: oneshot::Sender<()>, message: &str) { + warn!("Error clearing cookies: {}", message); + if tx.send(()).is_err() { + warn!("Error sending clear cookies result"); + } +} + +fn send_result(tx: oneshot::Sender<()>) { + if tx.send(()).is_err() { + warn!("Error sending clear cookies result"); + } +} diff --git a/gpgui/src-tauri/tauri.conf.json b/gpgui/src-tauri/tauri.conf.json index 941b835..126a741 100644 --- a/gpgui/src-tauri/tauri.conf.json +++ b/gpgui/src-tauri/tauri.conf.json @@ -16,6 +16,9 @@ "all": true, "request": true, "scope": ["https://**"] + }, + "window": { + "all": true } }, "bundle": { diff --git a/gpgui/src/App.tsx b/gpgui/src/App.tsx index 5fc4c26..22d9768 100644 --- a/gpgui/src/App.tsx +++ b/gpgui/src/App.tsx @@ -1,6 +1,7 @@ +import { WebviewWindow } from "@tauri-apps/api/window"; import { Box, TextField } from "@mui/material"; import Button from "@mui/material/Button"; -import { ChangeEvent, FormEvent, useEffect, useState } from "react"; +import { ChangeEvent, FormEvent, useEffect, useRef, useState } from "react"; import "./App.css"; import ConnectionStatus, { Status } from "./components/ConnectionStatus"; @@ -13,6 +14,7 @@ import gatewayService from "./services/gatewayService"; import portalService from "./services/portalService"; import vpnService from "./services/vpnService"; import authService from "./services/authService"; +import { Maybe } from "./types"; export default function App() { const [portalAddress, setPortalAddress] = useState("vpn.microstrategy.com"); // useState("220.191.185.154"); @@ -25,6 +27,7 @@ export default function App() { open: false, message: "", }); + const regionRef = useRef>(null); useEffect(() => { return vpnService.onStatusChanged((latestStatus) => { @@ -75,6 +78,8 @@ export default function App() { try { const response = await portalService.prelogin(portalAddress); + const { region } = response; + regionRef.current = region; if (portalService.isSamlAuth(response)) { const { samlAuthMethod, samlAuthRequest } = response; @@ -97,6 +102,22 @@ export default function App() { ); console.log("portalConfigResponse", portalConfigResponse); + + const { gateways, userAuthCookie, prelogonUserAuthCookie } = + portalConfigResponse; + + const preferredGateway = portalService.preferredGateway( + gateways, + regionRef.current + ); + + const token = await gatewayService.login(preferredGateway, { + user: authData.username, + userAuthCookie, + prelogonUserAuthCookie, + }); + + await vpnService.connect(preferredGateway.address!, token); } else if (portalService.isPasswordAuth(response)) { setPasswordAuthOpen(true); setPasswordAuth({ @@ -146,16 +167,18 @@ export default function App() { { user, passwd } ); - const { gateways, preferredGateway, userAuthCookie } = - portalConfigResponse; + const { gateways, userAuthCookie } = portalConfigResponse; if (gateways.length === 0) { // TODO handle no gateways, treat the portal as a gateway throw new Error("No gateways found"); } - const token = await gatewayService.login({ - gateway: preferredGateway, + const preferredGateway = portalService.preferredGateway( + gateways, + regionRef.current + ); + const token = await gatewayService.login(preferredGateway, { user, passwd, userAuthCookie, diff --git a/gpgui/src/services/gatewayService.ts b/gpgui/src/services/gatewayService.ts index bc3ae3f..41c31c7 100644 --- a/gpgui/src/services/gatewayService.ts +++ b/gpgui/src/services/gatewayService.ts @@ -1,23 +1,42 @@ import { Body, ResponseType, fetch } from "@tauri-apps/api/http"; -import { Maybe } from "../types"; import { parseXml } from "../utils/parseXml"; import { Gateway } from "./types"; type LoginParams = { - gateway: Gateway; user: string; - passwd: string; - userAuthCookie: Maybe; + passwd?: string | null; + userAuthCookie?: string | null; + prelogonUserAuthCookie?: string | null; }; class GatewayService { - async login(params: LoginParams) { - const { gateway, user, passwd, userAuthCookie } = params; + async login(gateway: Gateway, params: LoginParams) { + const { user, passwd, userAuthCookie, prelogonUserAuthCookie } = params; if (!gateway.address) { throw new Error("Gateway address is required"); } const loginUrl = `https://${gateway.address}/ssl-vpn/login.esp`; + const body = Body.form({ + prot: "https:", + inputStr: "", + jnlpReady: "jnlpReady", + computer: "Linux", // TODO + ok: "Login", + direct: "yes", + "ipv6-support": "yes", + clientVer: "4100", + clientos: "Linux", + "os-version": "Linux", + server: gateway.address, + user, + passwd: passwd || "", + "prelogin-cookie": "", + "portal-userauthcookie": userAuthCookie || "", + "portal-prelogonuserauthcookie": prelogonUserAuthCookie || "", + }); + + console.log("Login body", body); const response = await fetch(loginUrl, { method: "POST", @@ -25,24 +44,7 @@ class GatewayService { "User-Agent": "PAN GlobalProtect", }, responseType: ResponseType.Text, - body: Body.form({ - prot: "https:", - inputStr: "", - jnlpReady: "jnlpReady", - computer: "Linux", // TODO - ok: "Login", - direct: "yes", - "ipv6-support": "yes", - clientVer: "4100", - clientos: "Linux", - "os-version": "Linux", - server: gateway.address, - user, - passwd, - "portal-userauthcookie": userAuthCookie ?? "", - "portal-prelogonuserauthcookie": "", - "prelogin-cookie": "", - }), + body, }); if (!response.ok) { diff --git a/gpgui/src/services/portalService.ts b/gpgui/src/services/portalService.ts index 7acfa93..d5fed33 100644 --- a/gpgui/src/services/portalService.ts +++ b/gpgui/src/services/portalService.ts @@ -25,15 +25,9 @@ type PreloginResponse = MaybeProperties< type ConfigResponse = { userAuthCookie: Maybe; prelogonUserAuthCookie: Maybe; - preferredGateway: Gateway; gateways: Gateway[]; }; -// user: username, -// passwd: password, -// "prelogin-cookie": "", -// "portal-userauthcookie": "", -// "portal-prelogonuserauthcookie": "", type PortalConfigParams = { user: string; passwd?: string | null; @@ -81,10 +75,7 @@ class PortalService { } isSamlAuth(response: PreloginResponse): response is SamlPreloginResponse { - if (response.samlAuthMethod && response.samlAuthRequest) { - return true; - } - return false; + return !!(response.samlAuthMethod && response.samlAuthRequest); } isPasswordAuth( @@ -143,6 +134,8 @@ class PortalService { } private parsePortalConfigResponse(response: string): ConfigResponse { + console.log(response); + const result = parseXml(response); const gateways = result.all("gateways list > entry").map((entry) => { const address = entry.attr("name"); @@ -152,13 +145,13 @@ class PortalService { return { name, address, - priority: priority ? parseInt(priority, 10) : undefined, + priority: priority ? parseInt(priority, 10) : Infinity, priorityRules: entry.all("priority-rule > entry").map((entry) => { const name = entry.attr("name"); const priority = entry.text("priority"); return { name, - priority: priority ? parseInt(priority, 10) : undefined, + priority: priority ? parseInt(priority, 10) : Infinity, }; }), }; @@ -167,10 +160,37 @@ class PortalService { return { userAuthCookie: result.text("portal-userauthcookie"), prelogonUserAuthCookie: result.text("portal-prelogonuserauthcookie"), - preferredGateway: gateways[0], gateways, }; } + + preferredGateway(gateways: Gateway[], region: Maybe) { + console.log(gateways); + let defaultGateway = gateways[0]; + for (const gateway of gateways) { + if (gateway.priority < defaultGateway.priority) { + defaultGateway = gateway; + } + } + + if (!region) { + return defaultGateway; + } + + let preferredGateway = defaultGateway; + let currentPriority = Infinity; + for (const gateway of gateways) { + const priorityRule = gateway.priorityRules.find( + ({ name }) => name === region + ); + + if (priorityRule && priorityRule.priority < currentPriority) { + preferredGateway = gateway; + currentPriority = priorityRule.priority; + } + } + return preferredGateway; + } } export default new PortalService(); diff --git a/gpgui/src/services/types.ts b/gpgui/src/services/types.ts index 5bbc6de..640964d 100644 --- a/gpgui/src/services/types.ts +++ b/gpgui/src/services/types.ts @@ -2,12 +2,12 @@ import { Maybe } from '../types'; type PriorityRule = { name: Maybe; - priority: Maybe; + priority: number; }; export type Gateway = { name: Maybe; address: Maybe; priorityRules: PriorityRule[]; - priority: Maybe; + priority: number; };