mirror of
				https://github.com/yuezk/GlobalProtect-openconnect.git
				synced 2025-05-20 07:26:58 -04:00 
			
		
		
		
	Support HIP report (#309)
This commit is contained in:
		
							
								
								
									
										178
									
								
								crates/gpapi/src/gateway/hip.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								crates/gpapi/src/gateway/hip.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,178 @@ | ||||
| use std::collections::HashMap; | ||||
|  | ||||
| use log::{info, warn}; | ||||
| use reqwest::Client; | ||||
| use roxmltree::Document; | ||||
|  | ||||
| use crate::{gp_params::GpParams, process::hip_launcher::HipLauncher, utils::normalize_server}; | ||||
|  | ||||
| struct HipReporter<'a> { | ||||
|   server: String, | ||||
|   cookie: &'a str, | ||||
|   md5: &'a str, | ||||
|   csd_wrapper: &'a str, | ||||
|   gp_params: &'a GpParams, | ||||
|   client: Client, | ||||
| } | ||||
|  | ||||
| impl HipReporter<'_> { | ||||
|   async fn report(&self) -> anyhow::Result<()> { | ||||
|     let client_ip = self.retrieve_client_ip().await?; | ||||
|  | ||||
|     let hip_needed = match self.check_hip(&client_ip).await { | ||||
|       Ok(hip_needed) => hip_needed, | ||||
|       Err(err) => { | ||||
|         warn!("Failed to check HIP: {}", err); | ||||
|         return Ok(()); | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     if !hip_needed { | ||||
|       info!("HIP report not needed"); | ||||
|       return Ok(()); | ||||
|     } | ||||
|  | ||||
|     info!("HIP report needed, generating report..."); | ||||
|     let report = self.generate_report(&client_ip).await?; | ||||
|  | ||||
|     if let Err(err) = self.submit_hip(&client_ip, &report).await { | ||||
|       warn!("Failed to submit HIP report: {}", err); | ||||
|     } | ||||
|  | ||||
|     Ok(()) | ||||
|   } | ||||
|  | ||||
|   async fn retrieve_client_ip(&self) -> anyhow::Result<String> { | ||||
|     let config_url = format!("{}/ssl-vpn/getconfig.esp", self.server); | ||||
|     let mut params: HashMap<&str, &str> = HashMap::new(); | ||||
|  | ||||
|     params.insert("client-type", "1"); | ||||
|     params.insert("protocol-version", "p1"); | ||||
|     params.insert("internal", "no"); | ||||
|     params.insert("ipv6-support", "yes"); | ||||
|     params.insert("clientos", self.gp_params.client_os()); | ||||
|     params.insert("hmac-algo", "sha1,md5,sha256"); | ||||
|     params.insert("enc-algo", "aes-128-cbc,aes-256-cbc"); | ||||
|  | ||||
|     if let Some(os_version) = self.gp_params.os_version() { | ||||
|       params.insert("os-version", os_version); | ||||
|     } | ||||
|     if let Some(client_version) = self.gp_params.client_version() { | ||||
|       params.insert("app-version", client_version); | ||||
|     } | ||||
|  | ||||
|     let params = merge_cookie_params(self.cookie, ¶ms)?; | ||||
|  | ||||
|     let res = self.client.post(&config_url).form(¶ms).send().await?; | ||||
|     let res_xml = res.error_for_status()?.text().await?; | ||||
|     let doc = Document::parse(&res_xml)?; | ||||
|  | ||||
|     // Get <ip-address> | ||||
|     let ip = doc | ||||
|       .descendants() | ||||
|       .find(|n| n.has_tag_name("ip-address")) | ||||
|       .and_then(|n| n.text()) | ||||
|       .ok_or_else(|| anyhow::anyhow!("ip-address not found"))?; | ||||
|  | ||||
|     Ok(ip.to_string()) | ||||
|   } | ||||
|  | ||||
|   async fn check_hip(&self, client_ip: &str) -> anyhow::Result<bool> { | ||||
|     let url = format!("{}/ssl-vpn/hipreportcheck.esp", self.server); | ||||
|     let mut params = HashMap::new(); | ||||
|  | ||||
|     params.insert("client-role", "global-protect-full"); | ||||
|     params.insert("client-ip", client_ip); | ||||
|     params.insert("md5", self.md5); | ||||
|  | ||||
|     let params = merge_cookie_params(self.cookie, ¶ms)?; | ||||
|     let res = self.client.post(&url).form(¶ms).send().await?; | ||||
|     let res_xml = res.error_for_status()?.text().await?; | ||||
|  | ||||
|     is_hip_needed(&res_xml) | ||||
|   } | ||||
|  | ||||
|   async fn generate_report(&self, client_ip: &str) -> anyhow::Result<String> { | ||||
|     let launcher = HipLauncher::new(self.csd_wrapper) | ||||
|       .cookie(self.cookie) | ||||
|       .md5(self.md5) | ||||
|       .client_ip(client_ip) | ||||
|       .client_os(self.gp_params.client_os()) | ||||
|       .client_version(self.gp_params.client_version()); | ||||
|  | ||||
|     launcher.launch().await | ||||
|   } | ||||
|  | ||||
|   async fn submit_hip(&self, client_ip: &str, report: &str) -> anyhow::Result<()> { | ||||
|     let url = format!("{}/ssl-vpn/hipreport.esp", self.server); | ||||
|  | ||||
|     let mut params = HashMap::new(); | ||||
|     params.insert("client-role", "global-protect-full"); | ||||
|     params.insert("client-ip", client_ip); | ||||
|     params.insert("report", report); | ||||
|  | ||||
|     let params = merge_cookie_params(self.cookie, ¶ms)?; | ||||
|     let res = self.client.post(&url).form(¶ms).send().await?; | ||||
|     let res_xml = res.error_for_status()?.text().await?; | ||||
|  | ||||
|     info!("HIP check response: {}", res_xml); | ||||
|  | ||||
|     Ok(()) | ||||
|   } | ||||
| } | ||||
|  | ||||
| fn is_hip_needed(res_xml: &str) -> anyhow::Result<bool> { | ||||
|   let doc = Document::parse(res_xml)?; | ||||
|  | ||||
|   let hip_needed = doc | ||||
|     .descendants() | ||||
|     .find(|n| n.has_tag_name("hip-report-needed")) | ||||
|     .and_then(|n| n.text()) | ||||
|     .ok_or_else(|| anyhow::anyhow!("hip-report-needed not found"))?; | ||||
|  | ||||
|   Ok(hip_needed == "yes") | ||||
| } | ||||
|  | ||||
| fn merge_cookie_params(cookie: &str, params: &HashMap<&str, &str>) -> anyhow::Result<HashMap<String, String>> { | ||||
|   let cookie_params = serde_urlencoded::from_str::<HashMap<String, String>>(cookie)?; | ||||
|   let params = params | ||||
|     .iter() | ||||
|     .map(|(k, v)| (k.to_string(), v.to_string())) | ||||
|     .chain(cookie_params) | ||||
|     .collect::<HashMap<String, String>>(); | ||||
|  | ||||
|   Ok(params) | ||||
| } | ||||
|  | ||||
| // Compute md5 for fields except authcookie,preferred-ip,preferred-ipv6 | ||||
| fn build_csd_token(cookie: &str) -> anyhow::Result<String> { | ||||
|   let mut cookie_params = serde_urlencoded::from_str::<Vec<(String, String)>>(cookie)?; | ||||
|   cookie_params.retain(|(k, _)| k != "authcookie" && k != "preferred-ip" && k != "preferred-ipv6"); | ||||
|  | ||||
|   let token = serde_urlencoded::to_string(cookie_params)?; | ||||
|   let md5 = format!("{:x}", md5::compute(token)); | ||||
|  | ||||
|   Ok(md5) | ||||
| } | ||||
|  | ||||
| pub async fn hip_report(gateway: &str, cookie: &str, csd_wrapper: &str, gp_params: &GpParams) -> anyhow::Result<()> { | ||||
|   let client = Client::builder() | ||||
|     .danger_accept_invalid_certs(gp_params.ignore_tls_errors()) | ||||
|     .user_agent(gp_params.user_agent()) | ||||
|     .build()?; | ||||
|  | ||||
|   let md5 = build_csd_token(cookie)?; | ||||
|  | ||||
|   info!("Submit HIP report md5: {}", md5); | ||||
|  | ||||
|   let reporter = HipReporter { | ||||
|     server: normalize_server(gateway)?, | ||||
|     cookie, | ||||
|     md5: &md5, | ||||
|     csd_wrapper, | ||||
|     gp_params, | ||||
|     client, | ||||
|   }; | ||||
|  | ||||
|   reporter.report().await | ||||
| } | ||||
| @@ -1,5 +1,6 @@ | ||||
| mod login; | ||||
| mod parse_gateways; | ||||
| pub mod hip; | ||||
|  | ||||
| pub use login::*; | ||||
| pub(crate) use parse_gateways::*; | ||||
|   | ||||
| @@ -83,6 +83,18 @@ impl GpParams { | ||||
|     self.prefer_default_browser | ||||
|   } | ||||
|  | ||||
|   pub fn client_os(&self) -> &str { | ||||
|     self.client_os.as_str() | ||||
|   } | ||||
|  | ||||
|   pub fn os_version(&self) -> Option<&str> { | ||||
|     self.os_version.as_deref() | ||||
|   } | ||||
|  | ||||
|   pub fn client_version(&self) -> Option<&str> { | ||||
|     self.client_version.as_deref() | ||||
|   } | ||||
|  | ||||
|   pub(crate) fn to_params(&self) -> HashMap<&str, &str> { | ||||
|     let mut params: HashMap<&str, &str> = HashMap::new(); | ||||
|     let client_os = self.client_os.as_str(); | ||||
|   | ||||
							
								
								
									
										94
									
								
								crates/gpapi/src/process/hip_launcher.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								crates/gpapi/src/process/hip_launcher.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,94 @@ | ||||
| use std::process::Stdio; | ||||
|  | ||||
| use anyhow::bail; | ||||
| use tokio::process::Command; | ||||
|  | ||||
| pub struct HipLauncher<'a> { | ||||
|   program: &'a str, | ||||
|   cookie: Option<&'a str>, | ||||
|   client_ip: Option<&'a str>, | ||||
|   md5: Option<&'a str>, | ||||
|   client_os: Option<&'a str>, | ||||
|   client_version: Option<&'a str>, | ||||
| } | ||||
|  | ||||
| impl<'a> HipLauncher<'a> { | ||||
|   pub fn new(program: &'a str) -> Self { | ||||
|     Self { | ||||
|       program, | ||||
|       cookie: None, | ||||
|       client_ip: None, | ||||
|       md5: None, | ||||
|       client_os: None, | ||||
|       client_version: None, | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   pub fn cookie(mut self, cookie: &'a str) -> Self { | ||||
|     self.cookie = Some(cookie); | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn client_ip(mut self, client_ip: &'a str) -> Self { | ||||
|     self.client_ip = Some(client_ip); | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn md5(mut self, md5: &'a str) -> Self { | ||||
|     self.md5 = Some(md5); | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn client_os(mut self, client_os: &'a str) -> Self { | ||||
|     self.client_os = Some(client_os); | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn client_version(mut self, client_version: Option<&'a str>) -> Self { | ||||
|     self.client_version = client_version; | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub async fn launch(&self) -> anyhow::Result<String> { | ||||
|     let mut cmd = Command::new(self.program); | ||||
|  | ||||
|     if let Some(cookie) = self.cookie { | ||||
|       cmd.arg("--cookie").arg(cookie); | ||||
|     } | ||||
|  | ||||
|     if let Some(client_ip) = self.client_ip { | ||||
|       cmd.arg("--client-ip").arg(client_ip); | ||||
|     } | ||||
|  | ||||
|     if let Some(md5) = self.md5 { | ||||
|       cmd.arg("--md5").arg(md5); | ||||
|     } | ||||
|  | ||||
|     if let Some(client_os) = self.client_os { | ||||
|       cmd.arg("--client-os").arg(client_os); | ||||
|     } | ||||
|  | ||||
|     if let Some(client_version) = self.client_version { | ||||
|       cmd.env("APP_VERSION", client_version); | ||||
|     } | ||||
|  | ||||
|     let output = cmd | ||||
|       .kill_on_drop(true) | ||||
|       .stdout(Stdio::piped()) | ||||
|       .spawn()? | ||||
|       .wait_with_output() | ||||
|       .await?; | ||||
|  | ||||
|     if let Some(exit_status) = output.status.code() { | ||||
|       if exit_status != 0 { | ||||
|         bail!("HIP report generation failed with exit code {}", exit_status); | ||||
|       } | ||||
|  | ||||
|       let report = String::from_utf8(output.stdout)?; | ||||
|  | ||||
|       Ok(report) | ||||
|     } else { | ||||
|       bail!("HIP report generation failed"); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -1,8 +1,9 @@ | ||||
| pub(crate) mod command_traits; | ||||
|  | ||||
| pub mod users; | ||||
| pub mod auth_launcher; | ||||
| #[cfg(feature = "browser-auth")] | ||||
| pub mod browser_authenticator; | ||||
| pub mod gui_launcher; | ||||
| pub mod hip_launcher; | ||||
| pub mod service_launcher; | ||||
| pub mod users; | ||||
|   | ||||
| @@ -23,6 +23,11 @@ pub fn get_non_root_user() -> anyhow::Result<User> { | ||||
|   Ok(user) | ||||
| } | ||||
|  | ||||
| pub fn get_current_user() -> anyhow::Result<User> { | ||||
|   let current_user = whoami::username(); | ||||
|   get_user_by_name(¤t_user) | ||||
| } | ||||
|  | ||||
| fn get_real_user() -> anyhow::Result<User> { | ||||
|   // Read the UID from SUDO_UID or PKEXEC_UID environment variable if available. | ||||
|   let uid = match env::var("SUDO_UID") { | ||||
|   | ||||
| @@ -32,6 +32,8 @@ pub struct ConnectArgs { | ||||
|   cookie: String, | ||||
|   vpnc_script: Option<String>, | ||||
|   user_agent: Option<String>, | ||||
|   csd_uid: u32, | ||||
|   csd_wrapper: Option<String>, | ||||
|   os: Option<ClientOs>, | ||||
| } | ||||
|  | ||||
| @@ -42,6 +44,8 @@ impl ConnectArgs { | ||||
|       vpnc_script: None, | ||||
|       user_agent: None, | ||||
|       os: None, | ||||
|       csd_uid: 0, | ||||
|       csd_wrapper: None, | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -60,6 +64,14 @@ impl ConnectArgs { | ||||
|   pub fn openconnect_os(&self) -> Option<String> { | ||||
|     self.os.as_ref().map(|os| os.to_openconnect_os().to_string()) | ||||
|   } | ||||
|  | ||||
|   pub fn csd_uid(&self) -> u32 { | ||||
|     self.csd_uid | ||||
|   } | ||||
|  | ||||
|   pub fn csd_wrapper(&self) -> Option<String> { | ||||
|     self.csd_wrapper.clone() | ||||
|   } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Deserialize, Serialize, Type)] | ||||
| @@ -81,6 +93,16 @@ impl ConnectRequest { | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn with_csd_uid(mut self, csd_uid: u32) -> Self { | ||||
|     self.args.csd_uid = csd_uid; | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn with_csd_wrapper<T: Into<Option<String>>>(mut self, csd_wrapper: T) -> Self { | ||||
|     self.args.csd_wrapper = csd_wrapper.into(); | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn with_user_agent<T: Into<Option<String>>>(mut self, user_agent: T) -> Self { | ||||
|     self.args.user_agent = user_agent.into(); | ||||
|     self | ||||
|   | ||||
		Reference in New Issue
	
	Block a user