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", |     "clientos", | ||||||
|     "gpcommon", |     "gpcommon", | ||||||
|     "jnlp", |     "jnlp", | ||||||
|  |     "oneshot", | ||||||
|     "openconnect", |     "openconnect", | ||||||
|     "prelogin", |     "prelogin", | ||||||
|     "prelogon", |     "prelogon", | ||||||
|   | |||||||
							
								
								
									
										22
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										22
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @@ -62,6 +62,7 @@ dependencies = [ | |||||||
|  "tauri-plugin-log", |  "tauri-plugin-log", | ||||||
|  "tokio", |  "tokio", | ||||||
|  "url", |  "url", | ||||||
|  |  "veil", | ||||||
|  "webkit2gtk", |  "webkit2gtk", | ||||||
| ] | ] | ||||||
|  |  | ||||||
| @@ -3407,6 +3408,27 @@ version = "0.2.15" | |||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" | 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]] | [[package]] | ||||||
| name = "version-compare" | name = "version-compare" | ||||||
| version = "0.0.11" | version = "0.0.11" | ||||||
|   | |||||||
| @@ -21,7 +21,7 @@ | |||||||
|     "tauri-plugin-log-api": "github:tauri-apps/tauri-plugin-log" |     "tauri-plugin-log-api": "github:tauri-apps/tauri-plugin-log" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@tauri-apps/cli": "^1.2.3", |     "@tauri-apps/cli": "^1.3.1", | ||||||
|     "@types/react": "^18.0.27", |     "@types/react": "^18.0.27", | ||||||
|     "@types/react-dom": "^18.0.10", |     "@types/react-dom": "^18.0.10", | ||||||
|     "@vitejs/plugin-react-swc": "^3.0.0", |     "@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: | devDependencies: | ||||||
|   '@tauri-apps/cli': |   '@tauri-apps/cli': | ||||||
|     specifier: ^1.2.3 |     specifier: ^1.3.1 | ||||||
|     version: 1.2.3 |     version: 1.3.1 | ||||||
|   '@types/react': |   '@types/react': | ||||||
|     specifier: ^18.0.27 |     specifier: ^18.0.27 | ||||||
|     version: 18.0.28 |     version: 18.0.28 | ||||||
| @@ -853,8 +853,8 @@ packages: | |||||||
|     engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'} |     engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'} | ||||||
|     dev: false |     dev: false | ||||||
|  |  | ||||||
|   /@tauri-apps/cli-darwin-arm64@1.2.3: |   /@tauri-apps/cli-darwin-arm64@1.3.1: | ||||||
|     resolution: {integrity: sha512-phJN3fN8FtZZwqXg08bcxfq1+X1JSDglLvRxOxB7VWPq+O5SuB8uLyssjJsu+PIhyZZnIhTGdjhzLSFhSXfLsw==} |     resolution: {integrity: sha512-QlepYVPgOgspcwA/u4kGG4ZUijlXfdRtno00zEy+LxinN/IRXtk+6ErVtsmoLi1ZC9WbuMwzAcsRvqsD+RtNAg==} | ||||||
|     engines: {node: '>= 10'} |     engines: {node: '>= 10'} | ||||||
|     cpu: [arm64] |     cpu: [arm64] | ||||||
|     os: [darwin] |     os: [darwin] | ||||||
| @@ -862,8 +862,8 @@ packages: | |||||||
|     dev: true |     dev: true | ||||||
|     optional: true |     optional: true | ||||||
|  |  | ||||||
|   /@tauri-apps/cli-darwin-x64@1.2.3: |   /@tauri-apps/cli-darwin-x64@1.3.1: | ||||||
|     resolution: {integrity: sha512-jFZ/y6z8z6v4yliIbXKBXA7BJgtZVMsITmEXSuD6s5+eCOpDhQxbRkr6CA+FFfr+/r96rWSDSgDenDQuSvPAKw==} |     resolution: {integrity: sha512-fKcAUPVFO3jfDKXCSDGY0MhZFF/wDtx3rgFnogWYu4knk38o9RaqRkvMvqJhLYPuWaEM5h6/z1dRrr9KKCbrVg==} | ||||||
|     engines: {node: '>= 10'} |     engines: {node: '>= 10'} | ||||||
|     cpu: [x64] |     cpu: [x64] | ||||||
|     os: [darwin] |     os: [darwin] | ||||||
| @@ -871,8 +871,8 @@ packages: | |||||||
|     dev: true |     dev: true | ||||||
|     optional: true |     optional: true | ||||||
|  |  | ||||||
|   /@tauri-apps/cli-linux-arm-gnueabihf@1.2.3: |   /@tauri-apps/cli-linux-arm-gnueabihf@1.3.1: | ||||||
|     resolution: {integrity: sha512-C7h5vqAwXzY0kRGSU00Fj8PudiDWFCiQqqUNI1N+fhCILrzWZB9TPBwdx33ZfXKt/U4+emdIoo/N34v3TiAOmQ==} |     resolution: {integrity: sha512-+4H0dv8ltJHYu/Ma1h9ixUPUWka9EjaYa8nJfiMsdCI4LJLNE6cPveE7RmhZ59v9GW1XB108/k083JUC/OtGvA==} | ||||||
|     engines: {node: '>= 10'} |     engines: {node: '>= 10'} | ||||||
|     cpu: [arm] |     cpu: [arm] | ||||||
|     os: [linux] |     os: [linux] | ||||||
| @@ -880,8 +880,8 @@ packages: | |||||||
|     dev: true |     dev: true | ||||||
|     optional: true |     optional: true | ||||||
|  |  | ||||||
|   /@tauri-apps/cli-linux-arm64-gnu@1.2.3: |   /@tauri-apps/cli-linux-arm64-gnu@1.3.1: | ||||||
|     resolution: {integrity: sha512-buf1c8sdkuUzVDkGPQpyUdAIIdn5r0UgXU6+H5fGPq/Xzt5K69JzXaeo6fHsZEZghbV0hOK+taKV4J0m30UUMQ==} |     resolution: {integrity: sha512-Pj3odVO1JAxLjYmoXKxcrpj/tPxcA8UP8N06finhNtBtBaxAjrjjxKjO4968KB0BUH7AASIss9EL4Tr0FGnDuw==} | ||||||
|     engines: {node: '>= 10'} |     engines: {node: '>= 10'} | ||||||
|     cpu: [arm64] |     cpu: [arm64] | ||||||
|     os: [linux] |     os: [linux] | ||||||
| @@ -889,8 +889,8 @@ packages: | |||||||
|     dev: true |     dev: true | ||||||
|     optional: true |     optional: true | ||||||
|  |  | ||||||
|   /@tauri-apps/cli-linux-arm64-musl@1.2.3: |   /@tauri-apps/cli-linux-arm64-musl@1.3.1: | ||||||
|     resolution: {integrity: sha512-x88wPS9W5xAyk392vc4uNHcKBBvCp0wf4H9JFMF9OBwB7vfd59LbQCFcPSu8f0BI7bPrOsyHqspWHuFL8ojQEA==} |     resolution: {integrity: sha512-tA0JdDLPFaj42UDIVcF2t8V0tSha40rppcmAR/MfQpTCxih6399iMjwihz9kZE1n4b5O4KTq9GliYo50a8zYlQ==} | ||||||
|     engines: {node: '>= 10'} |     engines: {node: '>= 10'} | ||||||
|     cpu: [arm64] |     cpu: [arm64] | ||||||
|     os: [linux] |     os: [linux] | ||||||
| @@ -898,8 +898,8 @@ packages: | |||||||
|     dev: true |     dev: true | ||||||
|     optional: true |     optional: true | ||||||
|  |  | ||||||
|   /@tauri-apps/cli-linux-x64-gnu@1.2.3: |   /@tauri-apps/cli-linux-x64-gnu@1.3.1: | ||||||
|     resolution: {integrity: sha512-ZMz1jxEVe0B4/7NJnlPHmwmSIuwiD6ViXKs8F+OWWz2Y4jn5TGxWKFg7DLx5OwQTRvEIZxxT7lXHi5CuTNAxKg==} |     resolution: {integrity: sha512-FDU+Mnvk6NLkqQimcNojdKpMN4Y3W51+SQl+NqG9AFCWprCcSg62yRb84751ujZuf2MGT8HQOfmd0i77F4Q3tQ==} | ||||||
|     engines: {node: '>= 10'} |     engines: {node: '>= 10'} | ||||||
|     cpu: [x64] |     cpu: [x64] | ||||||
|     os: [linux] |     os: [linux] | ||||||
| @@ -907,8 +907,8 @@ packages: | |||||||
|     dev: true |     dev: true | ||||||
|     optional: true |     optional: true | ||||||
|  |  | ||||||
|   /@tauri-apps/cli-linux-x64-musl@1.2.3: |   /@tauri-apps/cli-linux-x64-musl@1.3.1: | ||||||
|     resolution: {integrity: sha512-B/az59EjJhdbZDzawEVox0LQu2ZHCZlk8rJf85AMIktIUoAZPFbwyiUv7/zjzA/sY6Nb58OSJgaPL2/IBy7E0A==} |     resolution: {integrity: sha512-MpO3akXFmK8lZYEbyQRDfhdxz1JkTBhonVuz5rRqxwA7gnGWHa1aF1+/2zsy7ahjB2tQ9x8DDFDMdVE20o9HrA==} | ||||||
|     engines: {node: '>= 10'} |     engines: {node: '>= 10'} | ||||||
|     cpu: [x64] |     cpu: [x64] | ||||||
|     os: [linux] |     os: [linux] | ||||||
| @@ -916,8 +916,8 @@ packages: | |||||||
|     dev: true |     dev: true | ||||||
|     optional: true |     optional: true | ||||||
|  |  | ||||||
|   /@tauri-apps/cli-win32-ia32-msvc@1.2.3: |   /@tauri-apps/cli-win32-ia32-msvc@1.3.1: | ||||||
|     resolution: {integrity: sha512-ypdO1OdC5ugNJAKO2m3sb1nsd+0TSvMS9Tr5qN/ZSMvtSduaNwrcZ3D7G/iOIanrqu/Nl8t3LYlgPZGBKlw7Ng==} |     resolution: {integrity: sha512-9Boeo3K5sOrSBAZBuYyGkpV2RfnGQz3ZhGJt4hE6P+HxRd62lS6+qDKAiw1GmkZ0l1drc2INWrNeT50gwOKwIQ==} | ||||||
|     engines: {node: '>= 10'} |     engines: {node: '>= 10'} | ||||||
|     cpu: [ia32] |     cpu: [ia32] | ||||||
|     os: [win32] |     os: [win32] | ||||||
| @@ -925,8 +925,8 @@ packages: | |||||||
|     dev: true |     dev: true | ||||||
|     optional: true |     optional: true | ||||||
|  |  | ||||||
|   /@tauri-apps/cli-win32-x64-msvc@1.2.3: |   /@tauri-apps/cli-win32-x64-msvc@1.3.1: | ||||||
|     resolution: {integrity: sha512-CsbHQ+XhnV/2csOBBDVfH16cdK00gNyNYUW68isedmqcn8j+s0e9cQ1xXIqi+Hue3awp8g3ImYN5KPepf3UExw==} |     resolution: {integrity: sha512-wMrTo91hUu5CdpbElrOmcZEoJR4aooTG+fbtcc87SMyPGQy1Ux62b+ZdwLvL1sVTxnIm//7v6QLRIWGiUjCPwA==} | ||||||
|     engines: {node: '>= 10'} |     engines: {node: '>= 10'} | ||||||
|     cpu: [x64] |     cpu: [x64] | ||||||
|     os: [win32] |     os: [win32] | ||||||
| @@ -934,20 +934,20 @@ packages: | |||||||
|     dev: true |     dev: true | ||||||
|     optional: true |     optional: true | ||||||
|  |  | ||||||
|   /@tauri-apps/cli@1.2.3: |   /@tauri-apps/cli@1.3.1: | ||||||
|     resolution: {integrity: sha512-erxtXuPhMEGJPBtnhPILD4AjuT81GZsraqpFvXAmEJZ2p8P6t7MVBifCL8LznRknznM3jn90D3M8RNBP3wcXTw==} |     resolution: {integrity: sha512-o4I0JujdITsVRm3/0spfJX7FcKYrYV1DXJqzlWIn6IY25/RltjU6qbC1TPgVww3RsRX63jyVUTcWpj5wwFl+EQ==} | ||||||
|     engines: {node: '>= 10'} |     engines: {node: '>= 10'} | ||||||
|     hasBin: true |     hasBin: true | ||||||
|     optionalDependencies: |     optionalDependencies: | ||||||
|       '@tauri-apps/cli-darwin-arm64': 1.2.3 |       '@tauri-apps/cli-darwin-arm64': 1.3.1 | ||||||
|       '@tauri-apps/cli-darwin-x64': 1.2.3 |       '@tauri-apps/cli-darwin-x64': 1.3.1 | ||||||
|       '@tauri-apps/cli-linux-arm-gnueabihf': 1.2.3 |       '@tauri-apps/cli-linux-arm-gnueabihf': 1.3.1 | ||||||
|       '@tauri-apps/cli-linux-arm64-gnu': 1.2.3 |       '@tauri-apps/cli-linux-arm64-gnu': 1.3.1 | ||||||
|       '@tauri-apps/cli-linux-arm64-musl': 1.2.3 |       '@tauri-apps/cli-linux-arm64-musl': 1.3.1 | ||||||
|       '@tauri-apps/cli-linux-x64-gnu': 1.2.3 |       '@tauri-apps/cli-linux-x64-gnu': 1.3.1 | ||||||
|       '@tauri-apps/cli-linux-x64-musl': 1.2.3 |       '@tauri-apps/cli-linux-x64-musl': 1.3.1 | ||||||
|       '@tauri-apps/cli-win32-ia32-msvc': 1.2.3 |       '@tauri-apps/cli-win32-ia32-msvc': 1.3.1 | ||||||
|       '@tauri-apps/cli-win32-x64-msvc': 1.2.3 |       '@tauri-apps/cli-win32-x64-msvc': 1.3.1 | ||||||
|     dev: true |     dev: true | ||||||
|  |  | ||||||
|   /@types/parse-json@4.0.0: |   /@types/parse-json@4.0.0: | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ tauri-build = { version = "1.3", features = [] } | |||||||
|  |  | ||||||
| [dependencies] | [dependencies] | ||||||
| gpcommon = { path = "../../gpcommon" } | 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 = [ | tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1", features = [ | ||||||
|     "colored", |     "colored", | ||||||
| ] } | ] } | ||||||
| @@ -28,6 +28,7 @@ webkit2gtk = "0.18.2" | |||||||
| regex = "1" | regex = "1" | ||||||
| url = "2.3" | url = "2.3" | ||||||
| tokio = { version = "1.14", features = ["full"] } | tokio = { version = "1.14", features = ["full"] } | ||||||
|  | veil = "0.1.6" | ||||||
|  |  | ||||||
| [features] | [features] | ||||||
| # by default Tauri runs in production mode | # 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 log::{debug, info, warn}; | ||||||
| use regex::Regex; | use regex::Regex; | ||||||
|  | use serde::de::Error; | ||||||
| use serde::{Deserialize, Serialize}; | use serde::{Deserialize, Serialize}; | ||||||
| use std::{sync::Arc, time::Duration}; | use std::{sync::Arc, time::Duration}; | ||||||
| use tauri::EventHandler; | 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::sync::{mpsc, Mutex}; | ||||||
| use tokio::time::timeout; | use tokio::time::timeout; | ||||||
|  | use veil::Redact; | ||||||
| use webkit2gtk::gio::Cancellable; | use webkit2gtk::gio::Cancellable; | ||||||
| use webkit2gtk::glib::GString; | use webkit2gtk::glib::GString; | ||||||
| use webkit2gtk::traits::{URIResponseExt, WebViewExt}; | 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_WINDOW_LABEL: &str = "auth_window"; | ||||||
| const AUTH_ERROR_EVENT: &str = "auth-error"; | const AUTH_ERROR_EVENT: &str = "auth-error"; | ||||||
| @@ -28,10 +31,11 @@ pub(crate) enum SamlBinding { | |||||||
|     Post, |     Post, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Debug, Clone, Deserialize)] | #[derive(Redact, Clone, Deserialize)] | ||||||
| pub(crate) struct AuthRequest { | pub(crate) struct AuthRequest { | ||||||
|     #[serde(alias = "samlBinding")] |     #[serde(alias = "samlBinding")] | ||||||
|     saml_binding: SamlBinding, |     saml_binding: SamlBinding, | ||||||
|  |     #[redact(fixed = 10)] | ||||||
|     #[serde(alias = "samlRequest")] |     #[serde(alias = "samlRequest")] | ||||||
|     saml_request: String, |     saml_request: String, | ||||||
| } | } | ||||||
| @@ -49,14 +53,20 @@ impl TryFrom<Option<&str>> for AuthRequest { | |||||||
|     type Error = serde_json::Error; |     type Error = serde_json::Error; | ||||||
|  |  | ||||||
|     fn try_from(value: Option<&str>) -> Result<Self, Self::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 { | pub(crate) struct AuthData { | ||||||
|  |     #[redact] | ||||||
|     username: Option<String>, |     username: Option<String>, | ||||||
|  |     #[redact(fixed = 10)] | ||||||
|     prelogin_cookie: Option<String>, |     prelogin_cookie: Option<String>, | ||||||
|  |     #[redact(fixed = 10)] | ||||||
|     portal_userauthcookie: Option<String>, |     portal_userauthcookie: Option<String>, | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -93,27 +103,35 @@ enum AuthEvent { | |||||||
|     Cancel, |     Cancel, | ||||||
| } | } | ||||||
|  |  | ||||||
| pub(crate) async fn saml_login( | pub(crate) struct SamlLoginParams { | ||||||
|     auth_request: AuthRequest, |     pub auth_request: AuthRequest, | ||||||
|     ua: &str, |     pub user_agent: String, | ||||||
|     clear_cookies: bool, |     pub clear_cookies: bool, | ||||||
|     app_handle: &AppHandle, |     pub app_handle: AppHandle, | ||||||
| ) -> tauri::Result<Option<AuthData>> { | } | ||||||
|  |  | ||||||
|  | pub(crate) async fn saml_login(params: SamlLoginParams) -> tauri::Result<Option<AuthData>> { | ||||||
|     info!("Starting SAML login"); |     info!("Starting SAML login"); | ||||||
|  |  | ||||||
|     let (event_tx, event_rx) = mpsc::channel::<AuthEvent>(8); |     let (event_tx, event_rx) = mpsc::channel::<AuthEvent>(8); | ||||||
|     let window = build_window(app_handle, ua)?; |     let window = build_window(¶ms.app_handle, ¶ms.user_agent)?; | ||||||
|     setup_webview(&window, clear_cookies, event_tx.clone())?; |     setup_webview(&window, event_tx.clone())?; | ||||||
|     let handler = setup_window(&window, event_tx); |     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); |     window.unlisten(handler); | ||||||
|     result |     result | ||||||
| } | } | ||||||
|  |  | ||||||
| fn build_window(app_handle: &AppHandle, ua: &str) -> tauri::Result<Window> { | fn build_window(app_handle: &AppHandle, ua: &str) -> tauri::Result<Window> { | ||||||
|     let url = WindowUrl::App("auth.html".into()); |     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) |         .visible(false) | ||||||
|         .title("GlobalProtect Login") |         .title("GlobalProtect Login") | ||||||
|         .user_agent(ua) |         .user_agent(ua) | ||||||
| @@ -124,19 +142,11 @@ fn build_window(app_handle: &AppHandle, ua: &str) -> tauri::Result<Window> { | |||||||
| } | } | ||||||
|  |  | ||||||
| // Setup webview events | // Setup webview events | ||||||
| fn setup_webview( | fn setup_webview(window: &Window, event_tx: mpsc::Sender<AuthEvent>) -> tauri::Result<()> { | ||||||
|     window: &Window, |  | ||||||
|     clear_cookies: bool, |  | ||||||
|     event_tx: mpsc::Sender<AuthEvent>, |  | ||||||
| ) -> tauri::Result<()> { |  | ||||||
|     window.with_webview(move |wv| { |     window.with_webview(move |wv| { | ||||||
|         let wv = wv.inner(); |         let wv = wv.inner(); | ||||||
|         let event_tx_clone = event_tx.clone(); |         let event_tx_clone = event_tx.clone(); | ||||||
|  |  | ||||||
|         if clear_cookies { |  | ||||||
|             clear_webview_cookies(&wv); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         wv.connect_load_changed(move |wv, event| { |         wv.connect_load_changed(move |wv, event| { | ||||||
|             if LoadEvent::Finished != event { |             if LoadEvent::Finished != event { | ||||||
|                 return; |                 return; | ||||||
| @@ -145,12 +155,11 @@ fn setup_webview( | |||||||
|             let uri = wv.uri().unwrap_or("".into()); |             let uri = wv.uri().unwrap_or("".into()); | ||||||
|             // Empty URI indicates that an error occurred |             // Empty URI indicates that an error occurred | ||||||
|             if uri.is_empty() { |             if uri.is_empty() { | ||||||
|                 warn!("Empty URI loaded"); |                 warn!("Empty URI loaded, retrying"); | ||||||
|                 send_auth_error(&event_tx_clone, AuthError::TokenInvalid); |                 send_auth_error(event_tx_clone.clone(), AuthError::TokenInvalid); | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|             // TODO, redact URI |             info!("Loaded URI: {}", redact_url(&uri)); | ||||||
|             debug!("Loaded URI: {}", uri); |  | ||||||
|  |  | ||||||
|             if let Some(main_res) = wv.main_resource() { |             if let Some(main_res) = wv.main_resource() { | ||||||
|                 parse_auth_data(&main_res, event_tx_clone.clone()); |                 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| { |         wv.connect_load_failed(move |_wv, event, _uri, err| { | ||||||
|             warn!("Load failed: {:?}, {:?}", event, err); |             warn!("Load failed: {:?}, {:?}", event, err); | ||||||
|             send_auth_error(&event_tx, AuthError::TokenInvalid); |             send_auth_error(event_tx.clone(), AuthError::TokenInvalid); | ||||||
|             false |             false | ||||||
|         }); |         }); | ||||||
|     }) |     }) | ||||||
| @@ -170,20 +179,17 @@ fn setup_webview( | |||||||
| fn setup_window(window: &Window, event_tx: mpsc::Sender<AuthEvent>) -> EventHandler { | fn setup_window(window: &Window, event_tx: mpsc::Sender<AuthEvent>) -> EventHandler { | ||||||
|     let event_tx_clone = event_tx.clone(); |     let event_tx_clone = event_tx.clone(); | ||||||
|     window.on_window_event(move |event| { |     window.on_window_event(move |event| { | ||||||
|         if let CloseRequested { api, .. } = event { |         if let CloseRequested { .. } = event { | ||||||
|             api.prevent_close(); |             send_auth_event(event_tx_clone.clone(), AuthEvent::Cancel); | ||||||
|             send_auth_event(&event_tx_clone, AuthEvent::Cancel); |  | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     window.listen_global(AUTH_REQUEST_EVENT, move |event| { |     window.listen_global(AUTH_REQUEST_EVENT, move |event| { | ||||||
|         if let Ok(payload) = TryInto::<AuthRequest>::try_into(event.payload()) { |         if let Ok(payload) = TryInto::<AuthRequest>::try_into(event.payload()) { | ||||||
|             let event_tx = event_tx.clone(); |             let event_tx = event_tx.clone(); | ||||||
|             let _ = tokio::spawn(async move { |             send_auth_event(event_tx.clone(), AuthEvent::Request(payload)); | ||||||
|                 if let Err(err) = event_tx.send(AuthEvent::Request(payload)).await { |         } else { | ||||||
|                     warn!("Error sending event: {}", err); |             warn!("Invalid auth request payload"); | ||||||
|                 } |  | ||||||
|             }); |  | ||||||
|         } |         } | ||||||
|     }) |     }) | ||||||
| } | } | ||||||
| @@ -199,7 +205,10 @@ async fn process( | |||||||
|  |  | ||||||
|     let handle = tokio::spawn(show_window_after_timeout(window.clone())); |     let handle = tokio::spawn(show_window_after_timeout(window.clone())); | ||||||
|     let auth_data = process_auth_event(&window, event_rx).await; |     let auth_data = process_auth_event(&window, event_rx).await; | ||||||
|  |  | ||||||
|  |     if !handle.is_finished() { | ||||||
|         handle.abort(); |         handle.abort(); | ||||||
|  |     } | ||||||
|     Ok(auth_data) |     Ok(auth_data) | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -255,7 +264,6 @@ async fn process_auth_event( | |||||||
|                 } |                 } | ||||||
|                 AuthEvent::Cancel => { |                 AuthEvent::Cancel => { | ||||||
|                     info!("User cancelled the authentication process, closing window"); |                     info!("User cancelled the authentication process, closing window"); | ||||||
|                     close_window(window); |  | ||||||
|                     return None; |                     return None; | ||||||
|                 } |                 } | ||||||
|                 AuthEvent::Error(AuthError::TokenInvalid) => { |                 AuthEvent::Error(AuthError::TokenInvalid) => { | ||||||
| @@ -292,20 +300,20 @@ async fn process_auth_event( | |||||||
| /// we should show the window after a short timeout, it will be cancelled if the | /// 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. | /// 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<()>>>) { | async fn handle_token_not_found(window: Window, cancel_timeout_rx: Arc<Mutex<mpsc::Receiver<()>>>) { | ||||||
|     match cancel_timeout_rx.try_lock() { |     if let Ok(mut cancel_timeout_rx) = cancel_timeout_rx.try_lock() { | ||||||
|         Ok(mut cancel_timeout_rx) => { |  | ||||||
|         let duration = Duration::from_secs(SHOW_WINDOW_TIMEOUT); |         let duration = Duration::from_secs(SHOW_WINDOW_TIMEOUT); | ||||||
|             if let Err(_) = timeout(duration, cancel_timeout_rx.recv()).await { |         if timeout(duration, cancel_timeout_rx.recv()).await.is_err() { | ||||||
|                 info!("Timeout expired, showing window"); |             info!( | ||||||
|  |                 "Timeout expired after {} seconds, showing window", | ||||||
|  |                 SHOW_WINDOW_TIMEOUT | ||||||
|  |             ); | ||||||
|             show_window(&window); |             show_window(&window); | ||||||
|         } else { |         } else { | ||||||
|             info!("Showing window timeout cancelled"); |             info!("Showing window timeout cancelled"); | ||||||
|         } |         } | ||||||
|         } |     } else { | ||||||
|         Err(_) => { |  | ||||||
|         debug!("Window will be shown by another task, skipping"); |         debug!("Window will be shown by another task, skipping"); | ||||||
|     } |     } | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Parse the authentication data from the response headers or HTML content | /// Parse the authentication data from the response headers or HTML content | ||||||
| @@ -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(response) = main_res.response() { | ||||||
|         if let Some(auth_data) = read_auth_data_from_response(&response) { |         if let Some(auth_data) = read_auth_data_from_response(&response) { | ||||||
|             debug!("Got auth data from HTTP headers: {:?}", auth_data); |             debug!("Got auth data from HTTP headers: {:?}", auth_data); | ||||||
|             send_auth_data(&event_tx, auth_data); |             send_auth_data(event_tx, auth_data); | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @@ -326,11 +334,11 @@ fn parse_auth_data(main_res: &WebResource, event_tx: mpsc::Sender<AuthEvent>) { | |||||||
|             match read_auth_data_from_html(&html) { |             match read_auth_data_from_html(&html) { | ||||||
|                 Ok(auth_data) => { |                 Ok(auth_data) => { | ||||||
|                     debug!("Got auth data from HTML: {:?}", 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) => { |                 Err(err) => { | ||||||
|                     debug!("Error reading auth data from HTML: {:?}", 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()) |         .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)); |     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)); |     send_auth_event(event_tx, AuthEvent::Error(err)); | ||||||
| } | } | ||||||
|  |  | ||||||
| fn send_auth_event(event_tx: &mpsc::Sender<AuthEvent>, auth_event: AuthEvent) { | fn send_auth_event(event_tx: mpsc::Sender<AuthEvent>, auth_event: AuthEvent) { | ||||||
|     if let Err(err) = event_tx.blocking_send(auth_event) { |     let _ = tauri::async_runtime::spawn(async move { | ||||||
|         warn!("Error sending event: {}", err) |         if let Err(err) = event_tx.send(auth_event).await { | ||||||
|  |             warn!("Error sending event: {}", err); | ||||||
|         } |         } | ||||||
|  |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
| fn show_window(window: &Window) { | fn show_window(window: &Window) { | ||||||
| @@ -418,15 +428,3 @@ fn close_window(window: &Window) { | |||||||
|         warn!("Error closing window: {}", err); |         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 gpcommon::{Client, ServerApiError, VpnStatus}; | ||||||
| use std::sync::Arc; | use std::sync::Arc; | ||||||
| use tauri::{AppHandle, State}; | use tauri::{AppHandle, State}; | ||||||
| @@ -32,13 +32,13 @@ pub(crate) async fn saml_login( | |||||||
|     request: String, |     request: String, | ||||||
|     app_handle: AppHandle, |     app_handle: AppHandle, | ||||||
| ) -> tauri::Result<Option<AuthData>> { | ) -> tauri::Result<Option<AuthData>> { | ||||||
|     let ua = "PAN GlobalProtect"; |     let user_agent = String::from("PAN GlobalProtect"); | ||||||
|     let clear_cookies = false; |     let clear_cookies = false; | ||||||
|     auth::saml_login( |     let params = SamlLoginParams { | ||||||
|         AuthRequest::new(binding, request), |         auth_request: AuthRequest::new(binding, request), | ||||||
|         ua, |         user_agent, | ||||||
|         clear_cookies, |         clear_cookies, | ||||||
|         &app_handle, |         app_handle, | ||||||
|     ) |     }; | ||||||
|     .await |     auth::saml_login(params).await | ||||||
| } | } | ||||||
|   | |||||||
| @@ -13,6 +13,7 @@ use tauri_plugin_log::LogTarget; | |||||||
|  |  | ||||||
| mod auth; | mod auth; | ||||||
| mod commands; | mod commands; | ||||||
|  | mod utils; | ||||||
|  |  | ||||||
| #[derive(Debug, Clone, Serialize)] | #[derive(Debug, Clone, Serialize)] | ||||||
| struct StatusPayload { | 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, |         "all": true, | ||||||
|         "request": true, |         "request": true, | ||||||
|         "scope": ["https://**"] |         "scope": ["https://**"] | ||||||
|  |       }, | ||||||
|  |       "window": { | ||||||
|  |         "all": true | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "bundle": { |     "bundle": { | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
|  | import { WebviewWindow } from "@tauri-apps/api/window"; | ||||||
| import { Box, TextField } from "@mui/material"; | import { Box, TextField } from "@mui/material"; | ||||||
| import Button from "@mui/material/Button"; | 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 "./App.css"; | ||||||
| import ConnectionStatus, { Status } from "./components/ConnectionStatus"; | import ConnectionStatus, { Status } from "./components/ConnectionStatus"; | ||||||
| @@ -13,6 +14,7 @@ import gatewayService from "./services/gatewayService"; | |||||||
| import portalService from "./services/portalService"; | import portalService from "./services/portalService"; | ||||||
| import vpnService from "./services/vpnService"; | import vpnService from "./services/vpnService"; | ||||||
| import authService from "./services/authService"; | import authService from "./services/authService"; | ||||||
|  | import { Maybe } from "./types"; | ||||||
|  |  | ||||||
| export default function App() { | export default function App() { | ||||||
|   const [portalAddress, setPortalAddress] = useState("vpn.microstrategy.com"); // useState("220.191.185.154"); |   const [portalAddress, setPortalAddress] = useState("vpn.microstrategy.com"); // useState("220.191.185.154"); | ||||||
| @@ -25,6 +27,7 @@ export default function App() { | |||||||
|     open: false, |     open: false, | ||||||
|     message: "", |     message: "", | ||||||
|   }); |   }); | ||||||
|  |   const regionRef = useRef<Maybe<string>>(null); | ||||||
|  |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     return vpnService.onStatusChanged((latestStatus) => { |     return vpnService.onStatusChanged((latestStatus) => { | ||||||
| @@ -75,6 +78,8 @@ export default function App() { | |||||||
|  |  | ||||||
|     try { |     try { | ||||||
|       const response = await portalService.prelogin(portalAddress); |       const response = await portalService.prelogin(portalAddress); | ||||||
|  |       const { region } = response; | ||||||
|  |       regionRef.current = region; | ||||||
|  |  | ||||||
|       if (portalService.isSamlAuth(response)) { |       if (portalService.isSamlAuth(response)) { | ||||||
|         const { samlAuthMethod, samlAuthRequest } = response; |         const { samlAuthMethod, samlAuthRequest } = response; | ||||||
| @@ -97,6 +102,22 @@ export default function App() { | |||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         console.log("portalConfigResponse", portalConfigResponse); |         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)) { |       } else if (portalService.isPasswordAuth(response)) { | ||||||
|         setPasswordAuthOpen(true); |         setPasswordAuthOpen(true); | ||||||
|         setPasswordAuth({ |         setPasswordAuth({ | ||||||
| @@ -146,16 +167,18 @@ export default function App() { | |||||||
|         { user, passwd } |         { user, passwd } | ||||||
|       ); |       ); | ||||||
|  |  | ||||||
|       const { gateways, preferredGateway, userAuthCookie } = |       const { gateways, userAuthCookie } = portalConfigResponse; | ||||||
|         portalConfigResponse; |  | ||||||
|  |  | ||||||
|       if (gateways.length === 0) { |       if (gateways.length === 0) { | ||||||
|         // TODO handle no gateways, treat the portal as a gateway |         // TODO handle no gateways, treat the portal as a gateway | ||||||
|         throw new Error("No gateways found"); |         throw new Error("No gateways found"); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       const token = await gatewayService.login({ |       const preferredGateway = portalService.preferredGateway( | ||||||
|         gateway: preferredGateway, |         gateways, | ||||||
|  |         regionRef.current | ||||||
|  |       ); | ||||||
|  |       const token = await gatewayService.login(preferredGateway, { | ||||||
|         user, |         user, | ||||||
|         passwd, |         passwd, | ||||||
|         userAuthCookie, |         userAuthCookie, | ||||||
|   | |||||||
| @@ -1,31 +1,23 @@ | |||||||
| import { Body, ResponseType, fetch } from "@tauri-apps/api/http"; | import { Body, ResponseType, fetch } from "@tauri-apps/api/http"; | ||||||
| import { Maybe } from "../types"; |  | ||||||
| import { parseXml } from "../utils/parseXml"; | import { parseXml } from "../utils/parseXml"; | ||||||
| import { Gateway } from "./types"; | import { Gateway } from "./types"; | ||||||
|  |  | ||||||
| type LoginParams = { | type LoginParams = { | ||||||
|   gateway: Gateway; |  | ||||||
|   user: string; |   user: string; | ||||||
|   passwd: string; |   passwd?: string | null; | ||||||
|   userAuthCookie: Maybe<string>; |   userAuthCookie?: string | null; | ||||||
|  |   prelogonUserAuthCookie?: string | null; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| class GatewayService { | class GatewayService { | ||||||
|   async login(params: LoginParams) { |   async login(gateway: Gateway, params: LoginParams) { | ||||||
|     const { gateway, user, passwd, userAuthCookie } = params; |     const { user, passwd, userAuthCookie, prelogonUserAuthCookie } = params; | ||||||
|     if (!gateway.address) { |     if (!gateway.address) { | ||||||
|       throw new Error("Gateway address is required"); |       throw new Error("Gateway address is required"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const loginUrl = `https://${gateway.address}/ssl-vpn/login.esp`; |     const loginUrl = `https://${gateway.address}/ssl-vpn/login.esp`; | ||||||
|  |     const body = Body.form({ | ||||||
|     const response = await fetch<string>(loginUrl, { |  | ||||||
|       method: "POST", |  | ||||||
|       headers: { |  | ||||||
|         "User-Agent": "PAN GlobalProtect", |  | ||||||
|       }, |  | ||||||
|       responseType: ResponseType.Text, |  | ||||||
|       body: Body.form({ |  | ||||||
|       prot: "https:", |       prot: "https:", | ||||||
|       inputStr: "", |       inputStr: "", | ||||||
|       jnlpReady: "jnlpReady", |       jnlpReady: "jnlpReady", | ||||||
| @@ -38,11 +30,21 @@ class GatewayService { | |||||||
|       "os-version": "Linux", |       "os-version": "Linux", | ||||||
|       server: gateway.address, |       server: gateway.address, | ||||||
|       user, |       user, | ||||||
|         passwd, |       passwd: passwd || "", | ||||||
|         "portal-userauthcookie": userAuthCookie ?? "", |  | ||||||
|         "portal-prelogonuserauthcookie": "", |  | ||||||
|       "prelogin-cookie": "", |       "prelogin-cookie": "", | ||||||
|       }), |       "portal-userauthcookie": userAuthCookie || "", | ||||||
|  |       "portal-prelogonuserauthcookie": prelogonUserAuthCookie || "", | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     console.log("Login body", body); | ||||||
|  |  | ||||||
|  |     const response = await fetch<string>(loginUrl, { | ||||||
|  |       method: "POST", | ||||||
|  |       headers: { | ||||||
|  |         "User-Agent": "PAN GlobalProtect", | ||||||
|  |       }, | ||||||
|  |       responseType: ResponseType.Text, | ||||||
|  |       body, | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     if (!response.ok) { |     if (!response.ok) { | ||||||
|   | |||||||
| @@ -25,15 +25,9 @@ type PreloginResponse = MaybeProperties< | |||||||
| type ConfigResponse = { | type ConfigResponse = { | ||||||
|   userAuthCookie: Maybe<string>; |   userAuthCookie: Maybe<string>; | ||||||
|   prelogonUserAuthCookie: Maybe<string>; |   prelogonUserAuthCookie: Maybe<string>; | ||||||
|   preferredGateway: Gateway; |  | ||||||
|   gateways: Gateway[]; |   gateways: Gateway[]; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| // user: username, |  | ||||||
| // passwd: password, |  | ||||||
| // "prelogin-cookie": "", |  | ||||||
| // "portal-userauthcookie": "", |  | ||||||
| // "portal-prelogonuserauthcookie": "", |  | ||||||
| type PortalConfigParams = { | type PortalConfigParams = { | ||||||
|   user: string; |   user: string; | ||||||
|   passwd?: string | null; |   passwd?: string | null; | ||||||
| @@ -81,10 +75,7 @@ class PortalService { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   isSamlAuth(response: PreloginResponse): response is SamlPreloginResponse { |   isSamlAuth(response: PreloginResponse): response is SamlPreloginResponse { | ||||||
|     if (response.samlAuthMethod && response.samlAuthRequest) { |     return !!(response.samlAuthMethod && response.samlAuthRequest); | ||||||
|       return true; |  | ||||||
|     } |  | ||||||
|     return false; |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   isPasswordAuth( |   isPasswordAuth( | ||||||
| @@ -143,6 +134,8 @@ class PortalService { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private parsePortalConfigResponse(response: string): ConfigResponse { |   private parsePortalConfigResponse(response: string): ConfigResponse { | ||||||
|  |     console.log(response); | ||||||
|  |  | ||||||
|     const result = parseXml(response); |     const result = parseXml(response); | ||||||
|     const gateways = result.all("gateways list > entry").map((entry) => { |     const gateways = result.all("gateways list > entry").map((entry) => { | ||||||
|       const address = entry.attr("name"); |       const address = entry.attr("name"); | ||||||
| @@ -152,13 +145,13 @@ class PortalService { | |||||||
|       return { |       return { | ||||||
|         name, |         name, | ||||||
|         address, |         address, | ||||||
|         priority: priority ? parseInt(priority, 10) : undefined, |         priority: priority ? parseInt(priority, 10) : Infinity, | ||||||
|         priorityRules: entry.all("priority-rule > entry").map((entry) => { |         priorityRules: entry.all("priority-rule > entry").map((entry) => { | ||||||
|           const name = entry.attr("name"); |           const name = entry.attr("name"); | ||||||
|           const priority = entry.text("priority"); |           const priority = entry.text("priority"); | ||||||
|           return { |           return { | ||||||
|             name, |             name, | ||||||
|             priority: priority ? parseInt(priority, 10) : undefined, |             priority: priority ? parseInt(priority, 10) : Infinity, | ||||||
|           }; |           }; | ||||||
|         }), |         }), | ||||||
|       }; |       }; | ||||||
| @@ -167,10 +160,37 @@ class PortalService { | |||||||
|     return { |     return { | ||||||
|       userAuthCookie: result.text("portal-userauthcookie"), |       userAuthCookie: result.text("portal-userauthcookie"), | ||||||
|       prelogonUserAuthCookie: result.text("portal-prelogonuserauthcookie"), |       prelogonUserAuthCookie: result.text("portal-prelogonuserauthcookie"), | ||||||
|       preferredGateway: gateways[0], |  | ||||||
|       gateways, |       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(); | export default new PortalService(); | ||||||
|   | |||||||
| @@ -2,12 +2,12 @@ import { Maybe } from '../types'; | |||||||
|  |  | ||||||
| type PriorityRule = { | type PriorityRule = { | ||||||
|   name: Maybe<string>; |   name: Maybe<string>; | ||||||
|   priority: Maybe<number>; |   priority: number; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export type Gateway = { | export type Gateway = { | ||||||
|   name: Maybe<string>; |   name: Maybe<string>; | ||||||
|   address: Maybe<string>; |   address: Maybe<string>; | ||||||
|   priorityRules: PriorityRule[]; |   priorityRules: PriorityRule[]; | ||||||
|   priority: Maybe<number>; |   priority: number; | ||||||
| }; | }; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user