mirror of
				https://github.com/yuezk/GlobalProtect-openconnect.git
				synced 2025-05-20 07:26:58 -04:00 
			
		
		
		
	refactor: Improve the saml auth
This commit is contained in:
		
							
								
								
									
										1
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							| @@ -4,6 +4,7 @@ | ||||
|     "clientos", | ||||
|     "gpcommon", | ||||
|     "jnlp", | ||||
|     "oneshot", | ||||
|     "openconnect", | ||||
|     "prelogin", | ||||
|     "prelogon", | ||||
|   | ||||
							
								
								
									
										22
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										22
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @@ -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" | ||||
|   | ||||
| @@ -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", | ||||
|   | ||||
							
								
								
									
										62
									
								
								gpgui/pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										62
									
								
								gpgui/pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							| @@ -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: | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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<Option<&str>> for AuthRequest { | ||||
|     type Error = serde_json::Error; | ||||
|  | ||||
|     fn try_from(value: Option<&str>) -> Result<Self, Self::Error> { | ||||
|         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<String>, | ||||
|     #[redact(fixed = 10)] | ||||
|     prelogin_cookie: Option<String>, | ||||
|     #[redact(fixed = 10)] | ||||
|     portal_userauthcookie: Option<String>, | ||||
| } | ||||
|  | ||||
| @@ -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<Option<AuthData>> { | ||||
| 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<Option<AuthData>> { | ||||
|     info!("Starting SAML login"); | ||||
|  | ||||
|     let (event_tx, event_rx) = mpsc::channel::<AuthEvent>(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<Window> { | ||||
|     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<Window> { | ||||
| } | ||||
|  | ||||
| // Setup webview events | ||||
| fn setup_webview( | ||||
|     window: &Window, | ||||
|     clear_cookies: bool, | ||||
|     event_tx: mpsc::Sender<AuthEvent>, | ||||
| ) -> tauri::Result<()> { | ||||
| fn setup_webview(window: &Window, event_tx: mpsc::Sender<AuthEvent>) -> 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<AuthEvent>) -> 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::<AuthRequest>::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<Mutex<mpsc::Receiver<()>>>) { | ||||
|     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<AuthEvent>) { | ||||
|     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<AuthEvent>) { | ||||
|             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<String> { | ||||
|         .map(|m| m.as_str().to_string()) | ||||
| } | ||||
|  | ||||
| fn send_auth_data(event_tx: &mpsc::Sender<AuthEvent>, auth_data: AuthData) { | ||||
| fn send_auth_data(event_tx: mpsc::Sender<AuthEvent>, auth_data: AuthData) { | ||||
|     send_auth_event(event_tx, AuthEvent::Success(auth_data)); | ||||
| } | ||||
|  | ||||
| fn send_auth_error(event_tx: &mpsc::Sender<AuthEvent>, err: AuthError) { | ||||
| fn send_auth_error(event_tx: mpsc::Sender<AuthEvent>, err: AuthError) { | ||||
|     send_auth_event(event_tx, AuthEvent::Error(err)); | ||||
| } | ||||
|  | ||||
| fn send_auth_event(event_tx: &mpsc::Sender<AuthEvent>, 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<AuthEvent>, 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"); | ||||
| } | ||||
|   | ||||
| @@ -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<Option<AuthData>> { | ||||
|     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 | ||||
| } | ||||
|   | ||||
| @@ -13,6 +13,7 @@ use tauri_plugin_log::LogTarget; | ||||
|  | ||||
| mod auth; | ||||
| mod commands; | ||||
| mod utils; | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize)] | ||||
| struct StatusPayload { | ||||
|   | ||||
							
								
								
									
										88
									
								
								gpgui/src-tauri/src/utils.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								gpgui/src-tauri/src/utils.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -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 <redacted> 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"); | ||||
|     } | ||||
| } | ||||
| @@ -16,6 +16,9 @@ | ||||
|         "all": true, | ||||
|         "request": true, | ||||
|         "scope": ["https://**"] | ||||
|       }, | ||||
|       "window": { | ||||
|         "all": true | ||||
|       } | ||||
|     }, | ||||
|     "bundle": { | ||||
|   | ||||
| @@ -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<Maybe<string>>(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, | ||||
|   | ||||
| @@ -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<string>; | ||||
|   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<string>(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) { | ||||
|   | ||||
| @@ -25,15 +25,9 @@ type PreloginResponse = MaybeProperties< | ||||
| type ConfigResponse = { | ||||
|   userAuthCookie: Maybe<string>; | ||||
|   prelogonUserAuthCookie: Maybe<string>; | ||||
|   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<string>) { | ||||
|     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(); | ||||
|   | ||||
| @@ -2,12 +2,12 @@ import { Maybe } from '../types'; | ||||
|  | ||||
| type PriorityRule = { | ||||
|   name: Maybe<string>; | ||||
|   priority: Maybe<number>; | ||||
|   priority: number; | ||||
| }; | ||||
|  | ||||
| export type Gateway = { | ||||
|   name: Maybe<string>; | ||||
|   address: Maybe<string>; | ||||
|   priorityRules: PriorityRule[]; | ||||
|   priority: Maybe<number>; | ||||
|   priority: number; | ||||
| }; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user