mirror of
				https://github.com/yuezk/GlobalProtect-openconnect.git
				synced 2025-05-20 07:26:58 -04:00 
			
		
		
		
	Compare commits
	
		
			3 Commits
		
	
	
		
			v2.0.0-bet
			...
			v2.0.0-bet
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | c3bd7aeb93 | ||
|  | 0b55a80317 | ||
|  | c6315bf384 | 
							
								
								
									
										5
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							| @@ -11,8 +11,10 @@ | |||||||
|         "dotenvy", |         "dotenvy", | ||||||
|         "getconfig", |         "getconfig", | ||||||
|         "globalprotect", |         "globalprotect", | ||||||
|  |         "globalprotectcallback", | ||||||
|         "gpapi", |         "gpapi", | ||||||
|         "gpauth", |         "gpauth", | ||||||
|  |         "gpcallback", | ||||||
|         "gpclient", |         "gpclient", | ||||||
|         "gpcommon", |         "gpcommon", | ||||||
|         "gpgui", |         "gpgui", | ||||||
| @@ -50,5 +52,6 @@ | |||||||
|         "wmctrl", |         "wmctrl", | ||||||
|         "XAUTHORITY", |         "XAUTHORITY", | ||||||
|         "yuezk" |         "yuezk" | ||||||
|     ] |     ], | ||||||
|  |     "rust-analyzer.cargo.features": "all", | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										47
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										47
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @@ -1423,7 +1423,7 @@ dependencies = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "gpapi" | name = "gpapi" | ||||||
| version = "2.0.0-beta3" | version = "2.0.0-beta5" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "anyhow", |  "anyhow", | ||||||
|  "base64 0.21.5", |  "base64 0.21.5", | ||||||
| @@ -1431,6 +1431,7 @@ dependencies = [ | |||||||
|  "clap", |  "clap", | ||||||
|  "dotenvy_macro", |  "dotenvy_macro", | ||||||
|  "log", |  "log", | ||||||
|  |  "open", | ||||||
|  "redact-engine", |  "redact-engine", | ||||||
|  "regex", |  "regex", | ||||||
|  "reqwest", |  "reqwest", | ||||||
| @@ -1451,7 +1452,7 @@ dependencies = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "gpauth" | name = "gpauth" | ||||||
| version = "2.0.0-beta3" | version = "2.0.0-beta5" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "anyhow", |  "anyhow", | ||||||
|  "clap", |  "clap", | ||||||
| @@ -1471,7 +1472,7 @@ dependencies = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "gpclient" | name = "gpclient" | ||||||
| version = "2.0.0-beta3" | version = "2.0.0-beta5" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "anyhow", |  "anyhow", | ||||||
|  "clap", |  "clap", | ||||||
| @@ -1492,7 +1493,7 @@ dependencies = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "gpservice" | name = "gpservice" | ||||||
| version = "2.0.0-beta3" | version = "2.0.0-beta5" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "anyhow", |  "anyhow", | ||||||
|  "axum", |  "axum", | ||||||
| @@ -1963,6 +1964,15 @@ version = "2.9.0" | |||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" | checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "is-docker" | ||||||
|  | version = "0.2.0" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" | ||||||
|  | dependencies = [ | ||||||
|  |  "once_cell", | ||||||
|  | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "is-terminal" | name = "is-terminal" | ||||||
| version = "0.4.10" | version = "0.4.10" | ||||||
| @@ -1974,6 +1984,16 @@ dependencies = [ | |||||||
|  "windows-sys 0.52.0", |  "windows-sys 0.52.0", | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "is-wsl" | ||||||
|  | version = "0.4.0" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" | ||||||
|  | dependencies = [ | ||||||
|  |  "is-docker", | ||||||
|  |  "once_cell", | ||||||
|  | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "is_executable" | name = "is_executable" | ||||||
| version = "1.0.1" | version = "1.0.1" | ||||||
| @@ -2445,9 +2465,20 @@ version = "0.3.0" | |||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" | checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "open" | ||||||
|  | version = "5.0.1" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "90878fb664448b54c4e592455ad02831e23a3f7e157374a8b95654731aac7349" | ||||||
|  | dependencies = [ | ||||||
|  |  "is-wsl", | ||||||
|  |  "libc", | ||||||
|  |  "pathdiff", | ||||||
|  | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "openconnect" | name = "openconnect" | ||||||
| version = "2.0.0-beta3" | version = "2.0.0-beta5" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "cc", |  "cc", | ||||||
|  "is_executable", |  "is_executable", | ||||||
| @@ -2574,6 +2605,12 @@ version = "1.0.14" | |||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" | checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "pathdiff" | ||||||
|  | version = "0.2.1" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "percent-encoding" | name = "percent-encoding" | ||||||
| version = "2.3.1" | version = "2.3.1" | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ resolver = "2" | |||||||
| members = ["crates/*", "apps/gpclient", "apps/gpservice", "apps/gpauth"] | members = ["crates/*", "apps/gpclient", "apps/gpservice", "apps/gpauth"] | ||||||
|  |  | ||||||
| [workspace.package] | [workspace.package] | ||||||
| version = "2.0.0-beta3" | version = "2.0.0-beta5" | ||||||
| authors = ["Kevin Yue <k3vinyue@gmail.com>"] | authors = ["Kevin Yue <k3vinyue@gmail.com>"] | ||||||
| homepage = "https://github.com/yuezk/GlobalProtect-openconnect" | homepage = "https://github.com/yuezk/GlobalProtect-openconnect" | ||||||
| edition = "2021" | edition = "2021" | ||||||
|   | |||||||
| @@ -11,6 +11,7 @@ A GUI for GlobalProtect VPN, based on OpenConnect, supports the SSO authenticati | |||||||
| - [x] Better Linux support | - [x] Better Linux support | ||||||
| - [x] Support both CLI and GUI | - [x] Support both CLI and GUI | ||||||
| - [x] Support both SSO and non-SSO authentication | - [x] Support both SSO and non-SSO authentication | ||||||
|  | - [x] Support authentication using default browser | ||||||
| - [x] Support multiple portals | - [x] Support multiple portals | ||||||
| - [x] Support gateway selection | - [x] Support gateway selection | ||||||
| - [x] Support auto-connect on startup | - [x] Support auto-connect on startup | ||||||
|   | |||||||
| @@ -203,7 +203,8 @@ impl<'a> AuthWindow<'a> { | |||||||
|       wv.connect_load_failed(move |_wv, _event, uri, err| { |       wv.connect_load_failed(move |_wv, _event, uri, err| { | ||||||
|         let redacted_uri = redact_uri(uri); |         let redacted_uri = redact_uri(uri); | ||||||
|         warn!("Failed to load uri: {} with error: {}", redacted_uri, err); |         warn!("Failed to load uri: {} with error: {}", redacted_uri, err); | ||||||
|         send_auth_result(&auth_result_tx, Err(AuthDataError::Invalid)); |         // NOTE: Don't send error here, since load_changed event will be triggered after this | ||||||
|  |         // send_auth_result(&auth_result_tx, Err(AuthDataError::Invalid)); | ||||||
|         // true to stop other handlers from being invoked for the event. false to propagate the event further. |         // true to stop other handlers from being invoked for the event. false to propagate the event further. | ||||||
|         true |         true | ||||||
|       }); |       }); | ||||||
|   | |||||||
| @@ -10,7 +10,12 @@ use log::info; | |||||||
|  |  | ||||||
| #[derive(Args)] | #[derive(Args)] | ||||||
| pub(crate) struct LaunchGuiArgs { | pub(crate) struct LaunchGuiArgs { | ||||||
|   #[clap(long, help = "Launch the GUI minimized")] |   #[arg( | ||||||
|  |     required = false, | ||||||
|  |     help = "The authentication data, used for the default browser authentication" | ||||||
|  |   )] | ||||||
|  |   auth_data: Option<String>, | ||||||
|  |   #[arg(long, help = "Launch the GUI minimized")] | ||||||
|   minimized: bool, |   minimized: bool, | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -30,6 +35,12 @@ impl<'a> LaunchGuiHandler<'a> { | |||||||
|       anyhow::bail!("`launch-gui` cannot be run as root"); |       anyhow::bail!("`launch-gui` cannot be run as root"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     let auth_data = self.args.auth_data.as_deref().unwrap_or_default(); | ||||||
|  |     if !auth_data.is_empty() { | ||||||
|  |       // Process the authentication data, its format is `globalprotectcallback:<data>` | ||||||
|  |       return feed_auth_data(auth_data).await; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     if try_active_gui().await.is_ok() { |     if try_active_gui().await.is_ok() { | ||||||
|       info!("The GUI is already running"); |       info!("The GUI is already running"); | ||||||
|       return Ok(()); |       return Ok(()); | ||||||
| @@ -66,6 +77,19 @@ impl<'a> LaunchGuiHandler<'a> { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | async fn feed_auth_data(auth_data: &str) -> anyhow::Result<()> { | ||||||
|  |   let service_endpoint = http_endpoint().await?; | ||||||
|  |  | ||||||
|  |   reqwest::Client::default() | ||||||
|  |     .post(format!("{}/auth-data", service_endpoint)) | ||||||
|  |     .json(&auth_data) | ||||||
|  |     .send() | ||||||
|  |     .await? | ||||||
|  |     .error_for_status()?; | ||||||
|  |  | ||||||
|  |   Ok(()) | ||||||
|  | } | ||||||
|  |  | ||||||
| async fn try_active_gui() -> anyhow::Result<()> { | async fn try_active_gui() -> anyhow::Result<()> { | ||||||
|   let service_endpoint = http_endpoint().await?; |   let service_endpoint = http_endpoint().await?; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,19 +0,0 @@ | |||||||
| <?xml version="1.0" encoding="UTF-8"?> |  | ||||||
| <!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd"> |  | ||||||
| <policyconfig> |  | ||||||
|   <vendor>GlobalProtect-openconnect</vendor> |  | ||||||
|   <vendor_url>https://github.com/yuezk/GlobalProtect-openconnect</vendor_url> |  | ||||||
|   <icon_name>gpgui</icon_name> |  | ||||||
|   <action id="com.yuezk.gpservice"> |  | ||||||
|     <description>Run GPService as root</description> |  | ||||||
|     <message>Authentication is required to run the GPService as root</message> |  | ||||||
|     <defaults> |  | ||||||
|       <allow_any>yes</allow_any> |  | ||||||
|       <allow_inactive>yes</allow_inactive> |  | ||||||
|       <allow_active>yes</allow_active> |  | ||||||
|     </defaults> |  | ||||||
|     <annotate key="org.freedesktop.policykit.exec.path">/home/kevin/Documents/repos/gp/target/debug/gpservice</annotate> |  | ||||||
|     <annotate key="org.freedesktop.policykit.exec.argv1">--with-gui</annotate> |  | ||||||
|     <annotate key="org.freedesktop.policykit.exec.allow_gui">true</annotate> |  | ||||||
|   </action> |  | ||||||
| </policyconfig> |  | ||||||
| @@ -21,6 +21,13 @@ pub(crate) async fn active_gui(State(ctx): State<Arc<WsServerContext>>) -> impl | |||||||
|   ctx.send_event(WsEvent::ActiveGui).await; |   ctx.send_event(WsEvent::ActiveGui).await; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | pub(crate) async fn auth_data( | ||||||
|  |   State(ctx): State<Arc<WsServerContext>>, | ||||||
|  |   body: String, | ||||||
|  | ) -> impl IntoResponse { | ||||||
|  |   ctx.send_event(WsEvent::AuthData(body)).await; | ||||||
|  | } | ||||||
|  |  | ||||||
| pub(crate) async fn ws_handler( | pub(crate) async fn ws_handler( | ||||||
|   ws: WebSocketUpgrade, |   ws: WebSocketUpgrade, | ||||||
|   State(ctx): State<Arc<WsServerContext>>, |   State(ctx): State<Arc<WsServerContext>>, | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ pub(crate) fn routes(ctx: Arc<WsServerContext>) -> Router { | |||||||
|   Router::new() |   Router::new() | ||||||
|     .route("/health", get(handlers::health)) |     .route("/health", get(handlers::health)) | ||||||
|     .route("/active-gui", post(handlers::active_gui)) |     .route("/active-gui", post(handlers::active_gui)) | ||||||
|  |     .route("/auth-data", post(handlers::auth_data)) | ||||||
|     .route("/ws", get(handlers::ws_handler)) |     .route("/ws", get(handlers::ws_handler)) | ||||||
|     .with_state(ctx) |     .with_state(ctx) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -28,7 +28,9 @@ uzers.workspace = true | |||||||
|  |  | ||||||
| tauri = { workspace = true, optional = true } | tauri = { workspace = true, optional = true } | ||||||
| clap = { workspace = true, optional = true } | clap = { workspace = true, optional = true } | ||||||
|  | open = { version = "5", optional = true } | ||||||
|  |  | ||||||
| [features] | [features] | ||||||
| tauri = ["dep:tauri"] | tauri = ["dep:tauri"] | ||||||
| clap = ["dep:clap"] | clap = ["dep:clap"] | ||||||
|  | browser-auth = ["dep:open"] | ||||||
|   | |||||||
| @@ -1,3 +1,5 @@ | |||||||
|  | use anyhow::bail; | ||||||
|  | use regex::Regex; | ||||||
| use serde::{Deserialize, Serialize}; | use serde::{Deserialize, Serialize}; | ||||||
|  |  | ||||||
| #[derive(Debug, Serialize, Deserialize)] | #[derive(Debug, Serialize, Deserialize)] | ||||||
| @@ -37,6 +39,32 @@ impl SamlAuthData { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   pub fn parse_html(html: &str) -> anyhow::Result<SamlAuthData> { | ||||||
|  |     match parse_xml_tag(html, "saml-auth-status") { | ||||||
|  |       Some(saml_status) if saml_status == "1" => { | ||||||
|  |         let username = parse_xml_tag(html, "saml-username"); | ||||||
|  |         let prelogin_cookie = parse_xml_tag(html, "prelogin-cookie"); | ||||||
|  |         let portal_userauthcookie = parse_xml_tag(html, "portal-userauthcookie"); | ||||||
|  |  | ||||||
|  |         if SamlAuthData::check(&username, &prelogin_cookie, &portal_userauthcookie) { | ||||||
|  |           return Ok(SamlAuthData::new( | ||||||
|  |             username.unwrap(), | ||||||
|  |             prelogin_cookie, | ||||||
|  |             portal_userauthcookie, | ||||||
|  |           )); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         bail!("Found invalid auth data in HTML"); | ||||||
|  |       } | ||||||
|  |       Some(status) => { | ||||||
|  |         bail!("Found invalid SAML status {} in HTML", status); | ||||||
|  |       } | ||||||
|  |       None => { | ||||||
|  |         bail!("No auth data found in HTML"); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   pub fn username(&self) -> &str { |   pub fn username(&self) -> &str { | ||||||
|     &self.username |     &self.username | ||||||
|   } |   } | ||||||
| @@ -61,3 +89,10 @@ impl SamlAuthData { | |||||||
|     username_valid && (prelogin_cookie_valid || portal_userauthcookie_valid) |     username_valid && (prelogin_cookie_valid || portal_userauthcookie_valid) | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | pub fn parse_xml_tag(html: &str, tag: &str) -> Option<String> { | ||||||
|  |   let re = Regex::new(&format!("<{}>(.*)</{}>", tag, tag)).unwrap(); | ||||||
|  |   re.captures(html) | ||||||
|  |     .and_then(|captures| captures.get(1)) | ||||||
|  |     .map(|m| m.as_str().to_string()) | ||||||
|  | } | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ use std::collections::HashMap; | |||||||
| use serde::{Deserialize, Serialize}; | use serde::{Deserialize, Serialize}; | ||||||
| use specta::Type; | use specta::Type; | ||||||
|  |  | ||||||
| use crate::auth::SamlAuthData; | use crate::{auth::SamlAuthData, utils::base64::decode_to_string}; | ||||||
|  |  | ||||||
| #[derive(Debug, Serialize, Deserialize, Type, Clone)] | #[derive(Debug, Serialize, Deserialize, Type, Clone)] | ||||||
| #[serde(rename_all = "camelCase")] | #[serde(rename_all = "camelCase")] | ||||||
| @@ -151,6 +151,17 @@ pub enum Credential { | |||||||
| } | } | ||||||
|  |  | ||||||
| impl Credential { | impl Credential { | ||||||
|  |   /// Create a credential from a globalprotectcallback:<base64 encoded string> | ||||||
|  |   pub fn parse_gpcallback(auth_data: &str) -> anyhow::Result<Self> { | ||||||
|  |     // Remove the surrounding quotes | ||||||
|  |     let auth_data = auth_data.trim_matches('"'); | ||||||
|  |     let auth_data = auth_data.trim_start_matches("globalprotectcallback:"); | ||||||
|  |     let auth_data = decode_to_string(auth_data)?; | ||||||
|  |     let auth_data = SamlAuthData::parse_html(&auth_data)?; | ||||||
|  |  | ||||||
|  |     Self::try_from(auth_data) | ||||||
|  |   } | ||||||
|  |  | ||||||
|   pub fn username(&self) -> &str { |   pub fn username(&self) -> &str { | ||||||
|     match self { |     match self { | ||||||
|       Credential::Password(cred) => cred.username(), |       Credential::Password(cred) => cred.username(), | ||||||
|   | |||||||
| @@ -50,6 +50,7 @@ pub struct GpParams { | |||||||
|   client_version: Option<String>, |   client_version: Option<String>, | ||||||
|   computer: String, |   computer: String, | ||||||
|   ignore_tls_errors: bool, |   ignore_tls_errors: bool, | ||||||
|  |   prefer_default_browser: bool, | ||||||
| } | } | ||||||
|  |  | ||||||
| impl GpParams { | impl GpParams { | ||||||
| @@ -69,6 +70,10 @@ impl GpParams { | |||||||
|     self.ignore_tls_errors |     self.ignore_tls_errors | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   pub fn prefer_default_browser(&self) -> bool { | ||||||
|  |     self.prefer_default_browser | ||||||
|  |   } | ||||||
|  |  | ||||||
|   pub(crate) fn to_params(&self) -> HashMap<&str, &str> { |   pub(crate) fn to_params(&self) -> HashMap<&str, &str> { | ||||||
|     let mut params: HashMap<&str, &str> = HashMap::new(); |     let mut params: HashMap<&str, &str> = HashMap::new(); | ||||||
|     let client_os = self.client_os.as_str(); |     let client_os = self.client_os.as_str(); | ||||||
| @@ -88,9 +93,10 @@ impl GpParams { | |||||||
|       params.insert("os-version", os_version); |       params.insert("os-version", os_version); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if let Some(client_version) = &self.client_version { |     // NOTE: Do not include clientgpversion for now | ||||||
|       params.insert("clientgpversion", client_version); |     // if let Some(client_version) = &self.client_version { | ||||||
|     } |     //   params.insert("clientgpversion", client_version); | ||||||
|  |     // } | ||||||
|  |  | ||||||
|     params |     params | ||||||
|   } |   } | ||||||
| @@ -103,6 +109,7 @@ pub struct GpParamsBuilder { | |||||||
|   client_version: Option<String>, |   client_version: Option<String>, | ||||||
|   computer: String, |   computer: String, | ||||||
|   ignore_tls_errors: bool, |   ignore_tls_errors: bool, | ||||||
|  |   prefer_default_browser: bool, | ||||||
| } | } | ||||||
|  |  | ||||||
| impl GpParamsBuilder { | impl GpParamsBuilder { | ||||||
| @@ -114,6 +121,7 @@ impl GpParamsBuilder { | |||||||
|       client_version: Default::default(), |       client_version: Default::default(), | ||||||
|       computer: whoami::hostname(), |       computer: whoami::hostname(), | ||||||
|       ignore_tls_errors: false, |       ignore_tls_errors: false, | ||||||
|  |       prefer_default_browser: false, | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -147,6 +155,11 @@ impl GpParamsBuilder { | |||||||
|     self |     self | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   pub fn prefer_default_browser(&mut self, prefer_default_browser: bool) -> &mut Self { | ||||||
|  |     self.prefer_default_browser = prefer_default_browser; | ||||||
|  |     self | ||||||
|  |   } | ||||||
|  |  | ||||||
|   pub fn build(&self) -> GpParams { |   pub fn build(&self) -> GpParams { | ||||||
|     GpParams { |     GpParams { | ||||||
|       user_agent: self.user_agent.clone(), |       user_agent: self.user_agent.clone(), | ||||||
| @@ -155,6 +168,7 @@ impl GpParamsBuilder { | |||||||
|       client_version: self.client_version.clone(), |       client_version: self.client_version.clone(), | ||||||
|       computer: self.computer.clone(), |       computer: self.computer.clone(), | ||||||
|       ignore_tls_errors: self.ignore_tls_errors, |       ignore_tls_errors: self.ignore_tls_errors, | ||||||
|  |       prefer_default_browser: self.prefer_default_browser, | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -26,6 +26,7 @@ const REQUIRED_PARAMS: [&str; 8] = [ | |||||||
| pub struct SamlPrelogin { | pub struct SamlPrelogin { | ||||||
|   region: String, |   region: String, | ||||||
|   saml_request: String, |   saml_request: String, | ||||||
|  |   support_default_browser: bool, | ||||||
| } | } | ||||||
|  |  | ||||||
| impl SamlPrelogin { | impl SamlPrelogin { | ||||||
| @@ -36,6 +37,10 @@ impl SamlPrelogin { | |||||||
|   pub fn saml_request(&self) -> &str { |   pub fn saml_request(&self) -> &str { | ||||||
|     &self.saml_request |     &self.saml_request | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   pub fn support_default_browser(&self) -> bool { | ||||||
|  |     self.support_default_browser | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Debug, Serialize, Type, Clone)] | #[derive(Debug, Serialize, Type, Clone)] | ||||||
| @@ -86,14 +91,14 @@ pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prel | |||||||
|   info!("Portal prelogin, user_agent: {}", user_agent); |   info!("Portal prelogin, user_agent: {}", user_agent); | ||||||
|  |  | ||||||
|   let portal = normalize_server(portal)?; |   let portal = normalize_server(portal)?; | ||||||
|   let prelogin_url = format!( |   let prelogin_url = format!("{}/global-protect/prelogin.esp", portal); | ||||||
|     "{}/global-protect/prelogin.esp?kerberos-support=yes", |  | ||||||
|     portal |  | ||||||
|   ); |  | ||||||
|   let mut params = gp_params.to_params(); |   let mut params = gp_params.to_params(); | ||||||
|  |  | ||||||
|   params.insert("tmp", "tmp"); |   params.insert("tmp", "tmp"); | ||||||
|   params.insert("default-browser", "0"); |  | ||||||
|   params.insert("cas-support", "yes"); |   params.insert("cas-support", "yes"); | ||||||
|  |   if gp_params.prefer_default_browser() { | ||||||
|  |     params.insert("default-browser", "1"); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   params.retain(|k, _| { |   params.retain(|k, _| { | ||||||
|     REQUIRED_PARAMS |     REQUIRED_PARAMS | ||||||
| @@ -125,12 +130,18 @@ pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prel | |||||||
|  |  | ||||||
|   let saml_method = xml::get_child_text(&doc, "saml-auth-method"); |   let saml_method = xml::get_child_text(&doc, "saml-auth-method"); | ||||||
|   let saml_request = xml::get_child_text(&doc, "saml-request"); |   let saml_request = xml::get_child_text(&doc, "saml-request"); | ||||||
|  |   let saml_default_browser = xml::get_child_text(&doc, "saml-default-browser"); | ||||||
|   // Check if the prelogin response is SAML |   // Check if the prelogin response is SAML | ||||||
|   if saml_method.is_some() && saml_request.is_some() { |   if saml_method.is_some() && saml_request.is_some() { | ||||||
|     let saml_request = base64::decode_to_string(&saml_request.unwrap())?; |     let saml_request = base64::decode_to_string(&saml_request.unwrap())?; | ||||||
|  |     let support_default_browser = saml_default_browser | ||||||
|  |       .map(|s| s.to_lowercase() == "yes") | ||||||
|  |       .unwrap_or(false); | ||||||
|  |  | ||||||
|     let saml_prelogin = SamlPrelogin { |     let saml_prelogin = SamlPrelogin { | ||||||
|       region, |       region, | ||||||
|       saml_request, |       saml_request, | ||||||
|  |       support_default_browser, | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     return Ok(Prelogin::Saml(saml_prelogin)); |     return Ok(Prelogin::Saml(saml_prelogin)); | ||||||
|   | |||||||
							
								
								
									
										34
									
								
								crates/gpapi/src/process/browser_authenticator.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								crates/gpapi/src/process/browser_authenticator.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | |||||||
|  | use std::{env::temp_dir, io::Write}; | ||||||
|  |  | ||||||
|  | pub struct BrowserAuthenticator<'a> { | ||||||
|  |   auth_request: &'a str, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl BrowserAuthenticator<'_> { | ||||||
|  |   pub fn new(auth_request: &str) -> BrowserAuthenticator { | ||||||
|  |     BrowserAuthenticator { auth_request } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn authenticate(&self) -> anyhow::Result<()> { | ||||||
|  |     if self.auth_request.starts_with("http") { | ||||||
|  |       open::that_detached(self.auth_request)?; | ||||||
|  |     } else { | ||||||
|  |       let html_file = temp_dir().join("gpauth.html"); | ||||||
|  |       let mut file = std::fs::File::create(&html_file)?; | ||||||
|  |  | ||||||
|  |       file.write_all(self.auth_request.as_bytes())?; | ||||||
|  |  | ||||||
|  |       open::that_detached(html_file)?; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     Ok(()) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Drop for BrowserAuthenticator<'_> { | ||||||
|  |   fn drop(&mut self) { | ||||||
|  |     // Cleanup the temporary file | ||||||
|  |     let html_file = temp_dir().join("gpauth.html"); | ||||||
|  |     let _ = std::fs::remove_file(html_file); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,5 +1,7 @@ | |||||||
| pub(crate) mod command_traits; | pub(crate) mod command_traits; | ||||||
|  |  | ||||||
| pub mod auth_launcher; | pub mod auth_launcher; | ||||||
|  | #[cfg(feature = "browser-auth")] | ||||||
|  | pub mod browser_authenticator; | ||||||
| pub mod gui_launcher; | pub mod gui_launcher; | ||||||
| pub mod service_launcher; | pub mod service_launcher; | ||||||
|   | |||||||
| @@ -7,4 +7,6 @@ use super::vpn_state::VpnState; | |||||||
| pub enum WsEvent { | pub enum WsEvent { | ||||||
|   VpnState(VpnState), |   VpnState(VpnState), | ||||||
|   ActiveGui, |   ActiveGui, | ||||||
|  |   /// External authentication data | ||||||
|  |   AuthData(String), | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user