mirror of
				https://github.com/yuezk/GlobalProtect-openconnect.git
				synced 2025-05-20 07:26:58 -04:00 
			
		
		
		
	Refactor using Tauri (#278)
This commit is contained in:
		
							
								
								
									
										32
									
								
								crates/gpapi/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								crates/gpapi/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| [package] | ||||
| name = "gpapi" | ||||
| version.workspace = true | ||||
| edition.workspace = true | ||||
| license = "MIT" | ||||
|  | ||||
| [dependencies] | ||||
| anyhow.workspace = true | ||||
| base64.workspace = true | ||||
| log.workspace = true | ||||
| reqwest.workspace = true | ||||
| roxmltree.workspace = true | ||||
| serde.workspace = true | ||||
| specta.workspace = true | ||||
| specta-macros.workspace = true | ||||
| urlencoding.workspace = true | ||||
| tokio.workspace = true | ||||
| serde_json.workspace = true | ||||
| whoami.workspace = true | ||||
| tempfile.workspace = true | ||||
| thiserror.workspace = true | ||||
| chacha20poly1305 = { version = "0.10", features = ["std"] } | ||||
| redact-engine.workspace = true | ||||
| url.workspace = true | ||||
| regex.workspace = true | ||||
| dotenvy_macro.workspace = true | ||||
| users.workspace = true | ||||
|  | ||||
| tauri = { workspace = true, optional = true } | ||||
|  | ||||
| [features] | ||||
| tauri = ["dep:tauri"] | ||||
							
								
								
									
										63
									
								
								crates/gpapi/src/auth.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								crates/gpapi/src/auth.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | ||||
| use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct SamlAuthData { | ||||
|   username: String, | ||||
|   prelogin_cookie: Option<String>, | ||||
|   portal_userauthcookie: Option<String>, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub enum SamlAuthResult { | ||||
|   Success(SamlAuthData), | ||||
|   Failure(String), | ||||
| } | ||||
|  | ||||
| impl SamlAuthResult { | ||||
|   pub fn is_success(&self) -> bool { | ||||
|     match self { | ||||
|       SamlAuthResult::Success(_) => true, | ||||
|       SamlAuthResult::Failure(_) => false, | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| impl SamlAuthData { | ||||
|   pub fn new( | ||||
|     username: String, | ||||
|     prelogin_cookie: Option<String>, | ||||
|     portal_userauthcookie: Option<String>, | ||||
|   ) -> Self { | ||||
|     Self { | ||||
|       username, | ||||
|       prelogin_cookie, | ||||
|       portal_userauthcookie, | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   pub fn username(&self) -> &str { | ||||
|     &self.username | ||||
|   } | ||||
|  | ||||
|   pub fn prelogin_cookie(&self) -> Option<&str> { | ||||
|     self.prelogin_cookie.as_deref() | ||||
|   } | ||||
|  | ||||
|   pub fn check( | ||||
|     username: &Option<String>, | ||||
|     prelogin_cookie: &Option<String>, | ||||
|     portal_userauthcookie: &Option<String>, | ||||
|   ) -> bool { | ||||
|     let username_valid = username | ||||
|       .as_ref() | ||||
|       .is_some_and(|username| !username.is_empty()); | ||||
|     let prelogin_cookie_valid = prelogin_cookie.as_ref().is_some_and(|val| val.len() > 5); | ||||
|     let portal_userauthcookie_valid = portal_userauthcookie | ||||
|       .as_ref() | ||||
|       .is_some_and(|val| val.len() > 5); | ||||
|  | ||||
|     username_valid && (prelogin_cookie_valid || portal_userauthcookie_valid) | ||||
|   } | ||||
| } | ||||
							
								
								
									
										223
									
								
								crates/gpapi/src/credential.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										223
									
								
								crates/gpapi/src/credential.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,223 @@ | ||||
| use std::collections::HashMap; | ||||
|  | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use specta::Type; | ||||
|  | ||||
| use crate::auth::SamlAuthData; | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize, Type, Clone)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct PasswordCredential { | ||||
|   username: String, | ||||
|   password: String, | ||||
| } | ||||
|  | ||||
| impl PasswordCredential { | ||||
|   pub fn new(username: &str, password: &str) -> Self { | ||||
|     Self { | ||||
|       username: username.to_string(), | ||||
|       password: password.to_string(), | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   pub fn username(&self) -> &str { | ||||
|     &self.username | ||||
|   } | ||||
|  | ||||
|   pub fn password(&self) -> &str { | ||||
|     &self.password | ||||
|   } | ||||
| } | ||||
|  | ||||
| impl From<&CachedCredential> for PasswordCredential { | ||||
|   fn from(value: &CachedCredential) -> Self { | ||||
|     Self::new(value.username(), value.password().unwrap_or_default()) | ||||
|   } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize, Type, Clone)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct PreloginCookieCredential { | ||||
|   username: String, | ||||
|   prelogin_cookie: String, | ||||
| } | ||||
|  | ||||
| impl PreloginCookieCredential { | ||||
|   pub fn new(username: &str, prelogin_cookie: &str) -> Self { | ||||
|     Self { | ||||
|       username: username.to_string(), | ||||
|       prelogin_cookie: prelogin_cookie.to_string(), | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   pub fn username(&self) -> &str { | ||||
|     &self.username | ||||
|   } | ||||
|  | ||||
|   pub fn prelogin_cookie(&self) -> &str { | ||||
|     &self.prelogin_cookie | ||||
|   } | ||||
| } | ||||
|  | ||||
| impl TryFrom<SamlAuthData> for PreloginCookieCredential { | ||||
|   type Error = anyhow::Error; | ||||
|  | ||||
|   fn try_from(value: SamlAuthData) -> Result<Self, Self::Error> { | ||||
|     let username = value.username().to_string(); | ||||
|     let prelogin_cookie = value | ||||
|       .prelogin_cookie() | ||||
|       .ok_or_else(|| anyhow::anyhow!("Missing prelogin cookie"))? | ||||
|       .to_string(); | ||||
|  | ||||
|     Ok(Self::new(&username, &prelogin_cookie)) | ||||
|   } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize, Type, Clone)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct AuthCookieCredential { | ||||
|   username: String, | ||||
|   user_auth_cookie: String, | ||||
|   prelogon_user_auth_cookie: String, | ||||
| } | ||||
|  | ||||
| impl AuthCookieCredential { | ||||
|   pub fn new(username: &str, user_auth_cookie: &str, prelogon_user_auth_cookie: &str) -> Self { | ||||
|     Self { | ||||
|       username: username.to_string(), | ||||
|       user_auth_cookie: user_auth_cookie.to_string(), | ||||
|       prelogon_user_auth_cookie: prelogon_user_auth_cookie.to_string(), | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   pub fn username(&self) -> &str { | ||||
|     &self.username | ||||
|   } | ||||
|  | ||||
|   pub fn user_auth_cookie(&self) -> &str { | ||||
|     &self.user_auth_cookie | ||||
|   } | ||||
|  | ||||
|   pub fn prelogon_user_auth_cookie(&self) -> &str { | ||||
|     &self.prelogon_user_auth_cookie | ||||
|   } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize, Type, Clone)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct CachedCredential { | ||||
|   username: String, | ||||
|   password: Option<String>, | ||||
|   auth_cookie: AuthCookieCredential, | ||||
| } | ||||
|  | ||||
| impl CachedCredential { | ||||
|   pub fn new( | ||||
|     username: String, | ||||
|     password: Option<String>, | ||||
|     auth_cookie: AuthCookieCredential, | ||||
|   ) -> Self { | ||||
|     Self { | ||||
|       username, | ||||
|       password, | ||||
|       auth_cookie, | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   pub fn username(&self) -> &str { | ||||
|     &self.username | ||||
|   } | ||||
|  | ||||
|   pub fn password(&self) -> Option<&str> { | ||||
|     self.password.as_deref() | ||||
|   } | ||||
|  | ||||
|   pub fn auth_cookie(&self) -> &AuthCookieCredential { | ||||
|     &self.auth_cookie | ||||
|   } | ||||
|  | ||||
|   pub fn set_auth_cookie(&mut self, auth_cookie: AuthCookieCredential) { | ||||
|     self.auth_cookie = auth_cookie; | ||||
|   } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize, Type, Clone)] | ||||
| #[serde(tag = "type", rename_all = "camelCase")] | ||||
| pub enum Credential { | ||||
|   Password(PasswordCredential), | ||||
|   PreloginCookie(PreloginCookieCredential), | ||||
|   AuthCookie(AuthCookieCredential), | ||||
|   CachedCredential(CachedCredential), | ||||
| } | ||||
|  | ||||
| impl Credential { | ||||
|   pub fn username(&self) -> &str { | ||||
|     match self { | ||||
|       Credential::Password(cred) => cred.username(), | ||||
|       Credential::PreloginCookie(cred) => cred.username(), | ||||
|       Credential::AuthCookie(cred) => cred.username(), | ||||
|       Credential::CachedCredential(cred) => cred.username(), | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   pub fn to_params(&self) -> HashMap<&str, &str> { | ||||
|     let mut params = HashMap::new(); | ||||
|     params.insert("user", self.username()); | ||||
|  | ||||
|     match self { | ||||
|       Credential::Password(cred) => { | ||||
|         params.insert("passwd", cred.password()); | ||||
|       } | ||||
|       Credential::PreloginCookie(cred) => { | ||||
|         params.insert("prelogin-cookie", cred.prelogin_cookie()); | ||||
|       } | ||||
|       Credential::AuthCookie(cred) => { | ||||
|         params.insert("portal-userauthcookie", cred.user_auth_cookie()); | ||||
|         params.insert( | ||||
|           "portal-prelogonuserauthcookie", | ||||
|           cred.prelogon_user_auth_cookie(), | ||||
|         ); | ||||
|       } | ||||
|       Credential::CachedCredential(cred) => { | ||||
|         if let Some(password) = cred.password() { | ||||
|           params.insert("passwd", password); | ||||
|         } | ||||
|         params.insert("portal-userauthcookie", cred.auth_cookie.user_auth_cookie()); | ||||
|         params.insert( | ||||
|           "portal-prelogonuserauthcookie", | ||||
|           cred.auth_cookie.prelogon_user_auth_cookie(), | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     params | ||||
|   } | ||||
| } | ||||
|  | ||||
| impl TryFrom<SamlAuthData> for Credential { | ||||
|   type Error = anyhow::Error; | ||||
|  | ||||
|   fn try_from(value: SamlAuthData) -> Result<Self, Self::Error> { | ||||
|     let prelogin_cookie = PreloginCookieCredential::try_from(value)?; | ||||
|  | ||||
|     Ok(Self::PreloginCookie(prelogin_cookie)) | ||||
|   } | ||||
| } | ||||
|  | ||||
| impl From<PasswordCredential> for Credential { | ||||
|   fn from(value: PasswordCredential) -> Self { | ||||
|     Self::Password(value) | ||||
|   } | ||||
| } | ||||
|  | ||||
| impl From<&AuthCookieCredential> for Credential { | ||||
|   fn from(value: &AuthCookieCredential) -> Self { | ||||
|     Self::AuthCookie(value.clone()) | ||||
|   } | ||||
| } | ||||
|  | ||||
| impl From<&CachedCredential> for Credential { | ||||
|   fn from(value: &CachedCredential) -> Self { | ||||
|     Self::CachedCredential(value.clone()) | ||||
|   } | ||||
| } | ||||
							
								
								
									
										74
									
								
								crates/gpapi/src/gateway/login.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								crates/gpapi/src/gateway/login.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | ||||
| use log::info; | ||||
| use reqwest::Client; | ||||
| use roxmltree::Document; | ||||
| use urlencoding::encode; | ||||
|  | ||||
| use crate::{credential::Credential, gp_params::GpParams}; | ||||
|  | ||||
| pub async fn gateway_login( | ||||
|   gateway: &str, | ||||
|   cred: &Credential, | ||||
|   gp_params: &GpParams, | ||||
| ) -> anyhow::Result<String> { | ||||
|   let login_url = format!("https://{}/ssl-vpn/login.esp", gateway); | ||||
|   let client = Client::builder() | ||||
|     .user_agent(gp_params.user_agent()) | ||||
|     .build()?; | ||||
|  | ||||
|   let mut params = cred.to_params(); | ||||
|   let extra_params = gp_params.to_params(); | ||||
|  | ||||
|   params.extend(extra_params); | ||||
|   params.insert("server", gateway); | ||||
|  | ||||
|   info!("Gateway login, user_agent: {}", gp_params.user_agent()); | ||||
|  | ||||
|   let res_xml = client | ||||
|     .post(&login_url) | ||||
|     .form(¶ms) | ||||
|     .send() | ||||
|     .await? | ||||
|     .error_for_status()? | ||||
|     .text() | ||||
|     .await?; | ||||
|  | ||||
|   let doc = Document::parse(&res_xml)?; | ||||
|  | ||||
|   build_gateway_token(&doc, gp_params.computer()) | ||||
| } | ||||
|  | ||||
| fn build_gateway_token(doc: &Document, computer: &str) -> anyhow::Result<String> { | ||||
|   let args = doc | ||||
|     .descendants() | ||||
|     .filter(|n| n.has_tag_name("argument")) | ||||
|     .map(|n| n.text().unwrap_or("").to_string()) | ||||
|     .collect::<Vec<_>>(); | ||||
|  | ||||
|   let params = [ | ||||
|     read_args(&args, 1, "authcookie")?, | ||||
|     read_args(&args, 3, "portal")?, | ||||
|     read_args(&args, 4, "user")?, | ||||
|     read_args(&args, 7, "domain")?, | ||||
|     read_args(&args, 15, "preferred-ip")?, | ||||
|     ("computer", computer), | ||||
|   ]; | ||||
|  | ||||
|   let token = params | ||||
|     .iter() | ||||
|     .map(|(k, v)| format!("{}={}", k, encode(v))) | ||||
|     .collect::<Vec<_>>() | ||||
|     .join("&"); | ||||
|  | ||||
|   Ok(token) | ||||
| } | ||||
|  | ||||
| fn read_args<'a>( | ||||
|   args: &'a [String], | ||||
|   index: usize, | ||||
|   key: &'a str, | ||||
| ) -> anyhow::Result<(&'a str, &'a str)> { | ||||
|   args | ||||
|     .get(index) | ||||
|     .ok_or_else(|| anyhow::anyhow!("Failed to read {key} from args")) | ||||
|     .map(|s| (key, s.as_ref())) | ||||
| } | ||||
							
								
								
									
										41
									
								
								crates/gpapi/src/gateway/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								crates/gpapi/src/gateway/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| mod login; | ||||
| mod parse_gateways; | ||||
|  | ||||
| pub use login::*; | ||||
| pub(crate) use parse_gateways::*; | ||||
|  | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use specta::Type; | ||||
|  | ||||
| use std::fmt::Display; | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize, Type, Clone)] | ||||
| pub(crate) struct PriorityRule { | ||||
|   pub(crate) name: String, | ||||
|   pub(crate) priority: u32, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize, Type, Clone)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct Gateway { | ||||
|   pub(crate) name: String, | ||||
|   pub(crate) address: String, | ||||
|   pub(crate) priority: u32, | ||||
|   pub(crate) priority_rules: Vec<PriorityRule>, | ||||
| } | ||||
|  | ||||
| impl Display for Gateway { | ||||
|   fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||
|     write!(f, "{} ({})", self.name, self.address) | ||||
|   } | ||||
| } | ||||
|  | ||||
| impl Gateway { | ||||
|   pub fn name(&self) -> &str { | ||||
|     &self.name | ||||
|   } | ||||
|  | ||||
|   pub fn server(&self) -> &str { | ||||
|     &self.address | ||||
|   } | ||||
| } | ||||
							
								
								
									
										63
									
								
								crates/gpapi/src/gateway/parse_gateways.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								crates/gpapi/src/gateway/parse_gateways.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | ||||
| use roxmltree::Document; | ||||
|  | ||||
| use super::{Gateway, PriorityRule}; | ||||
|  | ||||
| pub(crate) fn parse_gateways(doc: &Document) -> Option<Vec<Gateway>> { | ||||
|   let node_gateways = doc.descendants().find(|n| n.has_tag_name("gateways"))?; | ||||
|   let list_gateway = node_gateways | ||||
|     .descendants() | ||||
|     .find(|n| n.has_tag_name("list"))?; | ||||
|  | ||||
|   let gateways = list_gateway | ||||
|     .children() | ||||
|     .filter_map(|gateway_item| { | ||||
|       if !gateway_item.has_tag_name("entry") { | ||||
|         return None; | ||||
|       } | ||||
|       let address = gateway_item.attribute("name").unwrap_or("").to_string(); | ||||
|       let name = gateway_item | ||||
|         .children() | ||||
|         .find(|n| n.has_tag_name("description")) | ||||
|         .and_then(|n| n.text()) | ||||
|         .unwrap_or("") | ||||
|         .to_string(); | ||||
|       let priority = gateway_item | ||||
|         .children() | ||||
|         .find(|n| n.has_tag_name("priority")) | ||||
|         .and_then(|n| n.text()) | ||||
|         .and_then(|s| s.parse().ok()) | ||||
|         .unwrap_or(u32::MAX); | ||||
|       let priority_rules = gateway_item | ||||
|         .children() | ||||
|         .find(|n| n.has_tag_name("priority-rule")) | ||||
|         .map(|n| { | ||||
|           n.children() | ||||
|             .filter_map(|n| { | ||||
|               if !n.has_tag_name("entry") { | ||||
|                 return None; | ||||
|               } | ||||
|               let name = n.attribute("name").unwrap_or("").to_string(); | ||||
|               let priority: u32 = n | ||||
|                 .children() | ||||
|                 .find(|n| n.has_tag_name("priority")) | ||||
|                 .and_then(|n| n.text()) | ||||
|                 .and_then(|s| s.parse().ok()) | ||||
|                 .unwrap_or(u32::MAX); | ||||
|  | ||||
|               Some(PriorityRule { name, priority }) | ||||
|             }) | ||||
|             .collect() | ||||
|         }) | ||||
|         .unwrap_or_default(); | ||||
|  | ||||
|       Some(Gateway { | ||||
|         name, | ||||
|         address, | ||||
|         priority, | ||||
|         priority_rules, | ||||
|       }) | ||||
|     }) | ||||
|     .collect(); | ||||
|  | ||||
|   Some(gateways) | ||||
| } | ||||
							
								
								
									
										153
									
								
								crates/gpapi/src/gp_params.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								crates/gpapi/src/gp_params.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,153 @@ | ||||
| use std::collections::HashMap; | ||||
|  | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use specta::Type; | ||||
|  | ||||
| use crate::GP_USER_AGENT; | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize, Clone, Type, Default)] | ||||
| pub enum ClientOs { | ||||
|   Linux, | ||||
|   #[default] | ||||
|   Windows, | ||||
|   Mac, | ||||
| } | ||||
|  | ||||
| impl From<&ClientOs> for &str { | ||||
|   fn from(os: &ClientOs) -> Self { | ||||
|     match os { | ||||
|       ClientOs::Linux => "Linux", | ||||
|       ClientOs::Windows => "Windows", | ||||
|       ClientOs::Mac => "Mac", | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| impl ClientOs { | ||||
|   pub fn to_openconnect_os(&self) -> &str { | ||||
|     match self { | ||||
|       ClientOs::Linux => "linux", | ||||
|       ClientOs::Windows => "win", | ||||
|       ClientOs::Mac => "mac-intel", | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize, Type, Default)] | ||||
| pub struct GpParams { | ||||
|   user_agent: String, | ||||
|   client_os: ClientOs, | ||||
|   os_version: Option<String>, | ||||
|   client_version: Option<String>, | ||||
|   computer: Option<String>, | ||||
| } | ||||
|  | ||||
| impl GpParams { | ||||
|   pub fn builder() -> GpParamsBuilder { | ||||
|     GpParamsBuilder::new() | ||||
|   } | ||||
|  | ||||
|   pub(crate) fn user_agent(&self) -> &str { | ||||
|     &self.user_agent | ||||
|   } | ||||
|  | ||||
|   pub(crate) fn computer(&self) -> &str { | ||||
|     match self.computer { | ||||
|       Some(ref computer) => computer, | ||||
|       None => (&self.client_os).into() | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   pub(crate) fn to_params(&self) -> HashMap<&str, &str> { | ||||
|     let mut params: HashMap<&str, &str> = HashMap::new(); | ||||
|     let client_os: &str = (&self.client_os).into(); | ||||
|  | ||||
|     // Common params | ||||
|     params.insert("prot", "https:"); | ||||
|     params.insert("jnlpReady", "jnlpReady"); | ||||
|     params.insert("ok", "Login"); | ||||
|     params.insert("direct", "yes"); | ||||
|     params.insert("ipv6-support", "yes"); | ||||
|     params.insert("inputStr", ""); | ||||
|     params.insert("clientVer", "4100"); | ||||
|  | ||||
|     params.insert("clientos", client_os); | ||||
|  | ||||
|     if let Some(computer) = &self.computer { | ||||
|       params.insert("computer", computer); | ||||
|     } else { | ||||
|       params.insert("computer", client_os); | ||||
|     } | ||||
|  | ||||
|     if let Some(os_version) = &self.os_version { | ||||
|       params.insert("os-version", os_version); | ||||
|     } | ||||
|  | ||||
|     if let Some(client_version) = &self.client_version { | ||||
|       params.insert("clientgpversion", client_version); | ||||
|     } | ||||
|  | ||||
|     params | ||||
|   } | ||||
| } | ||||
|  | ||||
| pub struct GpParamsBuilder { | ||||
|   user_agent: String, | ||||
|   client_os: ClientOs, | ||||
|   os_version: Option<String>, | ||||
|   client_version: Option<String>, | ||||
|   computer: Option<String>, | ||||
| } | ||||
|  | ||||
| impl GpParamsBuilder { | ||||
|   pub fn new() -> Self { | ||||
|     Self { | ||||
|       user_agent: GP_USER_AGENT.to_string(), | ||||
|       client_os: ClientOs::Linux, | ||||
|       os_version: Default::default(), | ||||
|       client_version: Default::default(), | ||||
|       computer: Default::default(), | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   pub fn user_agent(&mut self, user_agent: &str) -> &mut Self { | ||||
|     self.user_agent = user_agent.to_string(); | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn client_os(&mut self, client_os: ClientOs) -> &mut Self { | ||||
|     self.client_os = client_os; | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn os_version(&mut self, os_version: &str) -> &mut Self { | ||||
|     self.os_version = Some(os_version.to_string()); | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn client_version(&mut self, client_version: &str) -> &mut Self { | ||||
|     self.client_version = Some(client_version.to_string()); | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn computer(&mut self, computer: &str) -> &mut Self { | ||||
|     self.computer = Some(computer.to_string()); | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn build(&self) -> GpParams { | ||||
|     GpParams { | ||||
|       user_agent: self.user_agent.clone(), | ||||
|       client_os: self.client_os.clone(), | ||||
|       os_version: self.os_version.clone(), | ||||
|       client_version: self.client_version.clone(), | ||||
|       computer: self.computer.clone(), | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| impl Default for GpParamsBuilder { | ||||
|   fn default() -> Self { | ||||
|     Self::new() | ||||
|   } | ||||
| } | ||||
							
								
								
									
										28
									
								
								crates/gpapi/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								crates/gpapi/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| pub mod auth; | ||||
| pub mod credential; | ||||
| pub mod gateway; | ||||
| pub mod gp_params; | ||||
| pub mod portal; | ||||
| pub mod process; | ||||
| pub mod service; | ||||
| pub mod utils; | ||||
|  | ||||
| #[cfg(debug_assertions)] | ||||
| pub const GP_API_KEY: &[u8; 32] = &[0; 32]; | ||||
|  | ||||
| pub const GP_USER_AGENT: &str = "PAN GlobalProtect"; | ||||
| pub const GP_SERVICE_LOCK_FILE: &str = "/var/run/gpservice.lock"; | ||||
|  | ||||
| #[cfg(not(debug_assertions))] | ||||
| pub const GP_SERVICE_BINARY: &str = "/usr/bin/gpservice"; | ||||
| #[cfg(not(debug_assertions))] | ||||
| pub const GP_GUI_BINARY: &str = "/usr/bin/gpgui"; | ||||
| #[cfg(not(debug_assertions))] | ||||
| pub(crate) const GP_AUTH_BINARY: &str = "/usr/bin/gpauth"; | ||||
|  | ||||
| #[cfg(debug_assertions)] | ||||
| pub const GP_SERVICE_BINARY: &str = dotenvy_macro::dotenv!("GP_SERVICE_BINARY"); | ||||
| #[cfg(debug_assertions)] | ||||
| pub const GP_GUI_BINARY: &str = dotenvy_macro::dotenv!("GP_GUI_BINARY"); | ||||
| #[cfg(debug_assertions)] | ||||
| pub(crate) const GP_AUTH_BINARY: &str = dotenvy_macro::dotenv!("GP_AUTH_BINARY"); | ||||
							
								
								
									
										180
									
								
								crates/gpapi/src/portal/config.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										180
									
								
								crates/gpapi/src/portal/config.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,180 @@ | ||||
| use anyhow::ensure; | ||||
| use log::info; | ||||
| use reqwest::Client; | ||||
| use roxmltree::Document; | ||||
| use serde::Serialize; | ||||
| use specta::Type; | ||||
| use thiserror::Error; | ||||
|  | ||||
| use crate::{ | ||||
|   credential::{AuthCookieCredential, Credential}, | ||||
|   gateway::{parse_gateways, Gateway}, | ||||
|   gp_params::GpParams, | ||||
|   utils::{normalize_server, xml}, | ||||
| }; | ||||
|  | ||||
| #[derive(Debug, Serialize, Type)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct PortalConfig { | ||||
|   portal: String, | ||||
|   auth_cookie: AuthCookieCredential, | ||||
|   gateways: Vec<Gateway>, | ||||
|   config_digest: Option<String>, | ||||
| } | ||||
|  | ||||
| impl PortalConfig { | ||||
|   pub fn new( | ||||
|     portal: String, | ||||
|     auth_cookie: AuthCookieCredential, | ||||
|     gateways: Vec<Gateway>, | ||||
|     config_digest: Option<String>, | ||||
|   ) -> Self { | ||||
|     Self { | ||||
|       portal, | ||||
|       auth_cookie, | ||||
|       gateways, | ||||
|       config_digest, | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   pub fn portal(&self) -> &str { | ||||
|     &self.portal | ||||
|   } | ||||
|  | ||||
|   pub fn gateways(&self) -> Vec<&Gateway> { | ||||
|     self.gateways.iter().collect() | ||||
|   } | ||||
|  | ||||
|   pub fn auth_cookie(&self) -> &AuthCookieCredential { | ||||
|     &self.auth_cookie | ||||
|   } | ||||
|  | ||||
|   /// In-place sort the gateways by region | ||||
|   pub fn sort_gateways(&mut self, region: &str) { | ||||
|     let preferred_gateway = self.find_preferred_gateway(region); | ||||
|     let preferred_gateway_index = self | ||||
|       .gateways() | ||||
|       .iter() | ||||
|       .position(|gateway| gateway.name == preferred_gateway.name) | ||||
|       .unwrap(); | ||||
|  | ||||
|     // Move the preferred gateway to the front of the list | ||||
|     self.gateways.swap(0, preferred_gateway_index); | ||||
|   } | ||||
|  | ||||
|   /// Find a gateway by name or address | ||||
|   pub fn find_gateway(&self, name_or_address: &str) -> Option<&Gateway> { | ||||
|     self | ||||
|       .gateways | ||||
|       .iter() | ||||
|       .find(|gateway| gateway.name == name_or_address || gateway.address == name_or_address) | ||||
|   } | ||||
|  | ||||
|   /// Find the preferred gateway for the given region | ||||
|   /// Iterates over the gateways and find the first one that | ||||
|   /// has the lowest priority for the given region. | ||||
|   /// If no gateway is found, returns the gateway with the lowest priority. | ||||
|   pub fn find_preferred_gateway(&self, region: &str) -> &Gateway { | ||||
|     let mut preferred_gateway: Option<&Gateway> = None; | ||||
|     let mut lowest_region_priority = u32::MAX; | ||||
|  | ||||
|     for gateway in &self.gateways { | ||||
|       for rule in &gateway.priority_rules { | ||||
|         if (rule.name == region || rule.name == "Any") && rule.priority < lowest_region_priority { | ||||
|           preferred_gateway = Some(gateway); | ||||
|           lowest_region_priority = rule.priority; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // If no gateway is found, return the gateway with the lowest priority | ||||
|     preferred_gateway.unwrap_or_else(|| { | ||||
|       self | ||||
|         .gateways | ||||
|         .iter() | ||||
|         .min_by_key(|gateway| gateway.priority) | ||||
|         .unwrap() | ||||
|     }) | ||||
|   } | ||||
| } | ||||
|  | ||||
| #[derive(Error, Debug)] | ||||
| pub enum PortalConfigError { | ||||
|   #[error("Empty response, retrying can help")] | ||||
|   EmptyResponse, | ||||
|   #[error("Empty auth cookie, retrying can help")] | ||||
|   EmptyAuthCookie, | ||||
|   #[error("Invalid auth cookie, retrying can help")] | ||||
|   InvalidAuthCookie, | ||||
|   #[error("Empty gateways, retrying can help")] | ||||
|   EmptyGateways, | ||||
| } | ||||
|  | ||||
| pub async fn retrieve_config( | ||||
|   portal: &str, | ||||
|   cred: &Credential, | ||||
|   gp_params: &GpParams, | ||||
| ) -> anyhow::Result<PortalConfig> { | ||||
|   let portal = normalize_server(portal)?; | ||||
|   let server = remove_url_scheme(&portal); | ||||
|  | ||||
|   let url = format!("{}/global-protect/getconfig.esp", portal); | ||||
|   let client = Client::builder() | ||||
|     .user_agent(gp_params.user_agent()) | ||||
|     .build()?; | ||||
|  | ||||
|   let mut params = cred.to_params(); | ||||
|   let extra_params = gp_params.to_params(); | ||||
|  | ||||
|   params.extend(extra_params); | ||||
|   params.insert("server", &server); | ||||
|   params.insert("host", &server); | ||||
|  | ||||
|   info!("Portal config, user_agent: {}", gp_params.user_agent()); | ||||
|  | ||||
|   let res_xml = client | ||||
|     .post(&url) | ||||
|     .form(¶ms) | ||||
|     .send() | ||||
|     .await? | ||||
|     .error_for_status()? | ||||
|     .text() | ||||
|     .await?; | ||||
|  | ||||
|   ensure!(!res_xml.is_empty(), PortalConfigError::EmptyResponse); | ||||
|  | ||||
|   let doc = Document::parse(&res_xml)?; | ||||
|   let gateways = parse_gateways(&doc).ok_or_else(|| anyhow::anyhow!("Failed to parse gateways"))?; | ||||
|  | ||||
|   let user_auth_cookie = xml::get_child_text(&doc, "portal-userauthcookie").unwrap_or_default(); | ||||
|   let prelogon_user_auth_cookie = | ||||
|     xml::get_child_text(&doc, "portal-prelogonuserauthcookie").unwrap_or_default(); | ||||
|   let config_digest = xml::get_child_text(&doc, "config-digest"); | ||||
|  | ||||
|   ensure!( | ||||
|     !user_auth_cookie.is_empty() && !prelogon_user_auth_cookie.is_empty(), | ||||
|     PortalConfigError::EmptyAuthCookie | ||||
|   ); | ||||
|  | ||||
|   ensure!( | ||||
|     user_auth_cookie != "empty" && prelogon_user_auth_cookie != "empty", | ||||
|     PortalConfigError::InvalidAuthCookie | ||||
|   ); | ||||
|  | ||||
|   ensure!(!gateways.is_empty(), PortalConfigError::EmptyGateways); | ||||
|  | ||||
|   Ok(PortalConfig::new( | ||||
|     server.to_string(), | ||||
|     AuthCookieCredential::new( | ||||
|       cred.username(), | ||||
|       &user_auth_cookie, | ||||
|       &prelogon_user_auth_cookie, | ||||
|     ), | ||||
|     gateways, | ||||
|     config_digest, | ||||
|   )) | ||||
| } | ||||
|  | ||||
| fn remove_url_scheme(s: &str) -> String { | ||||
|   s.replace("http://", "").replace("https://", "") | ||||
| } | ||||
							
								
								
									
										5
									
								
								crates/gpapi/src/portal/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								crates/gpapi/src/portal/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| mod config; | ||||
| mod prelogin; | ||||
|  | ||||
| pub use config::*; | ||||
| pub use prelogin::*; | ||||
							
								
								
									
										129
									
								
								crates/gpapi/src/portal/prelogin.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								crates/gpapi/src/portal/prelogin.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,129 @@ | ||||
| use anyhow::bail; | ||||
| use log::{info, trace}; | ||||
| use reqwest::Client; | ||||
| use roxmltree::Document; | ||||
| use serde::Serialize; | ||||
| use specta::Type; | ||||
|  | ||||
| use crate::utils::{base64, normalize_server, xml}; | ||||
|  | ||||
| #[derive(Debug, Serialize, Type, Clone)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct SamlPrelogin { | ||||
|   region: String, | ||||
|   saml_request: String, | ||||
| } | ||||
|  | ||||
| impl SamlPrelogin { | ||||
|   pub fn region(&self) -> &str { | ||||
|     &self.region | ||||
|   } | ||||
|  | ||||
|   pub fn saml_request(&self) -> &str { | ||||
|     &self.saml_request | ||||
|   } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize, Type, Clone)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct StandardPrelogin { | ||||
|   region: String, | ||||
|   auth_message: String, | ||||
|   label_username: String, | ||||
|   label_password: String, | ||||
| } | ||||
|  | ||||
| impl StandardPrelogin { | ||||
|   pub fn region(&self) -> &str { | ||||
|     &self.region | ||||
|   } | ||||
|  | ||||
|   pub fn auth_message(&self) -> &str { | ||||
|     &self.auth_message | ||||
|   } | ||||
|  | ||||
|   pub fn label_username(&self) -> &str { | ||||
|     &self.label_username | ||||
|   } | ||||
|  | ||||
|   pub fn label_password(&self) -> &str { | ||||
|     &self.label_password | ||||
|   } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize, Type, Clone)] | ||||
| #[serde(tag = "type", rename_all = "camelCase")] | ||||
| pub enum Prelogin { | ||||
|   Saml(SamlPrelogin), | ||||
|   Standard(StandardPrelogin), | ||||
| } | ||||
|  | ||||
| impl Prelogin { | ||||
|   pub fn region(&self) -> &str { | ||||
|     match self { | ||||
|       Prelogin::Saml(saml) => saml.region(), | ||||
|       Prelogin::Standard(standard) => standard.region(), | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| pub async fn prelogin(portal: &str, user_agent: &str) -> anyhow::Result<Prelogin> { | ||||
|   info!("Portal prelogin, user_agent: {}", user_agent); | ||||
|  | ||||
|   let portal = normalize_server(portal)?; | ||||
|   let prelogin_url = format!("{}/global-protect/prelogin.esp", portal); | ||||
|   let client = Client::builder().user_agent(user_agent).build()?; | ||||
|  | ||||
|   let res_xml = client | ||||
|     .get(&prelogin_url) | ||||
|     .send() | ||||
|     .await? | ||||
|     .error_for_status()? | ||||
|     .text() | ||||
|     .await?; | ||||
|  | ||||
|   trace!("Prelogin response: {}", res_xml); | ||||
|   let doc = Document::parse(&res_xml)?; | ||||
|  | ||||
|   let status = xml::get_child_text(&doc, "status") | ||||
|     .ok_or_else(|| anyhow::anyhow!("Prelogin response does not contain status element"))?; | ||||
|   // Check the status of the prelogin response | ||||
|   if status.to_uppercase() != "SUCCESS" { | ||||
|     let msg = xml::get_child_text(&doc, "msg").unwrap_or(String::from("Unknown error")); | ||||
|     bail!("Prelogin failed: {}", msg) | ||||
|   } | ||||
|  | ||||
|   let region = xml::get_child_text(&doc, "region") | ||||
|     .ok_or_else(|| anyhow::anyhow!("Prelogin response does not contain region element"))?; | ||||
|  | ||||
|   let saml_method = xml::get_child_text(&doc, "saml-auth-method"); | ||||
|   let saml_request = xml::get_child_text(&doc, "saml-request"); | ||||
|   // Check if the prelogin response is SAML | ||||
|   if saml_method.is_some() && saml_request.is_some() { | ||||
|     let saml_request = base64::decode_to_string(&saml_request.unwrap())?; | ||||
|     let saml_prelogin = SamlPrelogin { | ||||
|       region, | ||||
|       saml_request, | ||||
|     }; | ||||
|  | ||||
|     return Ok(Prelogin::Saml(saml_prelogin)); | ||||
|   } | ||||
|  | ||||
|   let label_username = xml::get_child_text(&doc, "username-label"); | ||||
|   let label_password = xml::get_child_text(&doc, "password-label"); | ||||
|   // Check if the prelogin response is standard login | ||||
|   if label_username.is_some() && label_password.is_some() { | ||||
|     let auth_message = xml::get_child_text(&doc, "authentication-message") | ||||
|       .unwrap_or(String::from("Please enter the login credentials")); | ||||
|     let standard_prelogin = StandardPrelogin { | ||||
|       region, | ||||
|       auth_message, | ||||
|       label_username: label_username.unwrap(), | ||||
|       label_password: label_password.unwrap(), | ||||
|     }; | ||||
|  | ||||
|     return Ok(Prelogin::Standard(standard_prelogin)); | ||||
|   } | ||||
|  | ||||
|   bail!("Invalid prelogin response"); | ||||
| } | ||||
							
								
								
									
										96
									
								
								crates/gpapi/src/process/auth_launcher.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								crates/gpapi/src/process/auth_launcher.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,96 @@ | ||||
| use std::process::Stdio; | ||||
|  | ||||
| use tokio::process::Command; | ||||
|  | ||||
| use crate::{auth::SamlAuthResult, credential::Credential, GP_AUTH_BINARY}; | ||||
|  | ||||
| use super::command_traits::CommandExt; | ||||
|  | ||||
| pub struct SamlAuthLauncher<'a> { | ||||
|   server: &'a str, | ||||
|   user_agent: Option<&'a str>, | ||||
|   saml_request: Option<&'a str>, | ||||
|   hidpi: bool, | ||||
|   fix_openssl: bool, | ||||
|   clean: bool, | ||||
| } | ||||
|  | ||||
| impl<'a> SamlAuthLauncher<'a> { | ||||
|   pub fn new(server: &'a str) -> Self { | ||||
|     Self { | ||||
|       server, | ||||
|       user_agent: None, | ||||
|       saml_request: None, | ||||
|       hidpi: false, | ||||
|       fix_openssl: false, | ||||
|       clean: false, | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   pub fn user_agent(mut self, user_agent: &'a str) -> Self { | ||||
|     self.user_agent = Some(user_agent); | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn saml_request(mut self, saml_request: &'a str) -> Self { | ||||
|     self.saml_request = Some(saml_request); | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn hidpi(mut self, hidpi: bool) -> Self { | ||||
|     self.hidpi = hidpi; | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn fix_openssl(mut self, fix_openssl: bool) -> Self { | ||||
|     self.fix_openssl = fix_openssl; | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn clean(mut self, clean: bool) -> Self { | ||||
|     self.clean = clean; | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   /// Launch the authenticator binary as the current user or SUDO_USER if available. | ||||
|   pub async fn launch(self) -> anyhow::Result<Credential> { | ||||
|     let mut auth_cmd = Command::new(GP_AUTH_BINARY); | ||||
|     auth_cmd.arg(self.server); | ||||
|  | ||||
|     if let Some(user_agent) = self.user_agent { | ||||
|       auth_cmd.arg("--user-agent").arg(user_agent); | ||||
|     } | ||||
|  | ||||
|     if let Some(saml_request) = self.saml_request { | ||||
|       auth_cmd.arg("--saml-request").arg(saml_request); | ||||
|     } | ||||
|  | ||||
|     if self.fix_openssl { | ||||
|       auth_cmd.arg("--fix-openssl"); | ||||
|     } | ||||
|  | ||||
|     if self.hidpi { | ||||
|       auth_cmd.arg("--hidpi"); | ||||
|     } | ||||
|  | ||||
|     if self.clean { | ||||
|       auth_cmd.arg("--clean"); | ||||
|     } | ||||
|  | ||||
|     let mut non_root_cmd = auth_cmd.into_non_root()?; | ||||
|     let output = non_root_cmd | ||||
|       .kill_on_drop(true) | ||||
|       .stdout(Stdio::piped()) | ||||
|       .spawn()? | ||||
|       .wait_with_output() | ||||
|       .await?; | ||||
|  | ||||
|     let auth_result: SamlAuthResult = serde_json::from_slice(&output.stdout) | ||||
|       .map_err(|_| anyhow::anyhow!("Failed to parse auth data"))?; | ||||
|  | ||||
|     match auth_result { | ||||
|       SamlAuthResult::Success(auth_data) => Credential::try_from(auth_data), | ||||
|       SamlAuthResult::Failure(msg) => Err(anyhow::anyhow!(msg)), | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										64
									
								
								crates/gpapi/src/process/command_traits.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								crates/gpapi/src/process/command_traits.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | ||||
| use anyhow::bail; | ||||
| use std::{env, ffi::OsStr}; | ||||
| use tokio::process::Command; | ||||
| use users::{os::unix::UserExt, User}; | ||||
|  | ||||
| pub trait CommandExt { | ||||
|   fn new_pkexec<S: AsRef<OsStr>>(program: S) -> Command; | ||||
|   fn into_non_root(self) -> anyhow::Result<Command>; | ||||
| } | ||||
|  | ||||
| impl CommandExt for Command { | ||||
|   fn new_pkexec<S: AsRef<OsStr>>(program: S) -> Command { | ||||
|     let mut cmd = Command::new("pkexec"); | ||||
|     cmd | ||||
|       .arg("--disable-internal-agent") | ||||
|       .arg("--user") | ||||
|       .arg("root") | ||||
|       .arg(program); | ||||
|  | ||||
|     cmd | ||||
|   } | ||||
|  | ||||
|   fn into_non_root(mut self) -> anyhow::Result<Command> { | ||||
|     let user = | ||||
|       get_non_root_user().map_err(|_| anyhow::anyhow!("{:?} cannot be run as root", self))?; | ||||
|  | ||||
|     self | ||||
|       .env("HOME", user.home_dir()) | ||||
|       .env("USER", user.name()) | ||||
|       .env("LOGNAME", user.name()) | ||||
|       .env("USERNAME", user.name()) | ||||
|       .uid(user.uid()) | ||||
|       .gid(user.primary_group_id()); | ||||
|  | ||||
|     Ok(self) | ||||
|   } | ||||
| } | ||||
|  | ||||
| fn get_non_root_user() -> anyhow::Result<User> { | ||||
|   let current_user = whoami::username(); | ||||
|  | ||||
|   let user = if current_user == "root" { | ||||
|     get_real_user()? | ||||
|   } else { | ||||
|     users::get_user_by_name(¤t_user) | ||||
|       .ok_or_else(|| anyhow::anyhow!("User ({}) not found", current_user))? | ||||
|   }; | ||||
|  | ||||
|   if user.uid() == 0 { | ||||
|     bail!("Non-root user not found") | ||||
|   } | ||||
|  | ||||
|   Ok(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") { | ||||
|     Ok(uid) => uid.parse::<u32>()?, | ||||
|     _ => env::var("PKEXEC_UID")?.parse::<u32>()?, | ||||
|   }; | ||||
|  | ||||
|   users::get_user_by_uid(uid).ok_or_else(|| anyhow::anyhow!("User not found")) | ||||
| } | ||||
							
								
								
									
										91
									
								
								crates/gpapi/src/process/gui_launcher.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								crates/gpapi/src/process/gui_launcher.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,91 @@ | ||||
| use std::{ | ||||
|   collections::HashMap, | ||||
|   path::PathBuf, | ||||
|   process::{ExitStatus, Stdio}, | ||||
| }; | ||||
|  | ||||
| use tokio::{io::AsyncWriteExt, process::Command}; | ||||
|  | ||||
| use crate::{utils::base64, GP_GUI_BINARY}; | ||||
|  | ||||
| use super::command_traits::CommandExt; | ||||
|  | ||||
| pub struct GuiLauncher { | ||||
|   program: PathBuf, | ||||
|   api_key: Option<Vec<u8>>, | ||||
|   minimized: bool, | ||||
|   envs: Option<HashMap<String, String>>, | ||||
| } | ||||
|  | ||||
| impl Default for GuiLauncher { | ||||
|   fn default() -> Self { | ||||
|     Self::new() | ||||
|   } | ||||
| } | ||||
|  | ||||
| impl GuiLauncher { | ||||
|   pub fn new() -> Self { | ||||
|     Self { | ||||
|       program: GP_GUI_BINARY.into(), | ||||
|       api_key: None, | ||||
|       minimized: false, | ||||
|       envs: None, | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   pub fn envs<T: Into<Option<HashMap<String, String>>>>(mut self, envs: T) -> Self { | ||||
|     self.envs = envs.into(); | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn api_key(mut self, api_key: Vec<u8>) -> Self { | ||||
|     self.api_key = Some(api_key); | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn minimized(mut self, minimized: bool) -> Self { | ||||
|     self.minimized = minimized; | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub async fn launch(&self) -> anyhow::Result<ExitStatus> { | ||||
|     let mut cmd = Command::new(&self.program); | ||||
|  | ||||
|     if let Some(envs) = &self.envs { | ||||
|       cmd.env_clear(); | ||||
|       cmd.envs(envs); | ||||
|     } | ||||
|  | ||||
|     if self.api_key.is_some() { | ||||
|       cmd.arg("--api-key-on-stdin"); | ||||
|     } | ||||
|  | ||||
|     if self.minimized { | ||||
|       cmd.arg("--minimized"); | ||||
|     } | ||||
|  | ||||
|     let mut non_root_cmd = cmd.into_non_root()?; | ||||
|  | ||||
|     let mut child = non_root_cmd | ||||
|       .kill_on_drop(true) | ||||
|       .stdin(Stdio::piped()) | ||||
|       .spawn()?; | ||||
|  | ||||
|     let mut stdin = child | ||||
|       .stdin | ||||
|       .take() | ||||
|       .ok_or_else(|| anyhow::anyhow!("Failed to open stdin"))?; | ||||
|  | ||||
|     if let Some(api_key) = &self.api_key { | ||||
|       let api_key = base64::encode(api_key); | ||||
|       tokio::spawn(async move { | ||||
|         stdin.write_all(api_key.as_bytes()).await.unwrap(); | ||||
|         drop(stdin); | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     let exit_status = child.wait().await?; | ||||
|  | ||||
|     Ok(exit_status) | ||||
|   } | ||||
| } | ||||
							
								
								
									
										5
									
								
								crates/gpapi/src/process/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								crates/gpapi/src/process/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| pub(crate) mod command_traits; | ||||
|  | ||||
| pub mod auth_launcher; | ||||
| pub mod gui_launcher; | ||||
| pub mod service_launcher; | ||||
							
								
								
									
										72
									
								
								crates/gpapi/src/process/service_launcher.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								crates/gpapi/src/process/service_launcher.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | ||||
| use std::{ | ||||
|   fs::File, | ||||
|   path::PathBuf, | ||||
|   process::{ExitStatus, Stdio}, | ||||
| }; | ||||
|  | ||||
| use tokio::process::Command; | ||||
|  | ||||
| use crate::GP_SERVICE_BINARY; | ||||
|  | ||||
| use super::command_traits::CommandExt; | ||||
|  | ||||
| pub struct ServiceLauncher { | ||||
|   program: PathBuf, | ||||
|   minimized: bool, | ||||
|   env_file: Option<String>, | ||||
|   log_file: Option<String>, | ||||
| } | ||||
|  | ||||
| impl Default for ServiceLauncher { | ||||
|   fn default() -> Self { | ||||
|     Self::new() | ||||
|   } | ||||
| } | ||||
|  | ||||
| impl ServiceLauncher { | ||||
|   pub fn new() -> Self { | ||||
|     Self { | ||||
|       program: GP_SERVICE_BINARY.into(), | ||||
|       minimized: false, | ||||
|       env_file: None, | ||||
|       log_file: None, | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   pub fn minimized(mut self, minimized: bool) -> Self { | ||||
|     self.minimized = minimized; | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn env_file(mut self, env_file: &str) -> Self { | ||||
|     self.env_file = Some(env_file.to_string()); | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn log_file(mut self, log_file: &str) -> Self { | ||||
|     self.log_file = Some(log_file.to_string()); | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub async fn launch(&self) -> anyhow::Result<ExitStatus> { | ||||
|     let mut cmd = Command::new_pkexec(&self.program); | ||||
|  | ||||
|     if self.minimized { | ||||
|       cmd.arg("--minimized"); | ||||
|     } | ||||
|  | ||||
|     if let Some(env_file) = &self.env_file { | ||||
|       cmd.arg("--env-file").arg(env_file); | ||||
|     } | ||||
|  | ||||
|     if let Some(log_file) = &self.log_file { | ||||
|       let log_file = File::create(log_file)?; | ||||
|       let stdio = Stdio::from(log_file); | ||||
|       cmd.stderr(stdio); | ||||
|     } | ||||
|  | ||||
|     let exit_status = cmd.kill_on_drop(true).spawn()?.wait().await?; | ||||
|  | ||||
|     Ok(exit_status) | ||||
|   } | ||||
| } | ||||
							
								
								
									
										10
									
								
								crates/gpapi/src/service/event.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								crates/gpapi/src/service/event.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| use super::vpn_state::VpnState; | ||||
|  | ||||
| /// Events that can be emitted by the service | ||||
| #[derive(Debug, Serialize, Deserialize, Clone)] | ||||
| pub enum WsEvent { | ||||
|   VpnState(VpnState), | ||||
|   ActiveGui, | ||||
| } | ||||
							
								
								
									
										3
									
								
								crates/gpapi/src/service/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								crates/gpapi/src/service/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| pub mod event; | ||||
| pub mod request; | ||||
| pub mod vpn_state; | ||||
							
								
								
									
										118
									
								
								crates/gpapi/src/service/request.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								crates/gpapi/src/service/request.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,118 @@ | ||||
| use std::collections::HashMap; | ||||
|  | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use specta::Type; | ||||
|  | ||||
| use crate::{gateway::Gateway, gp_params::ClientOs}; | ||||
|  | ||||
| use super::vpn_state::ConnectInfo; | ||||
|  | ||||
| #[derive(Debug, Deserialize, Serialize)] | ||||
| pub struct LaunchGuiRequest { | ||||
|   user: String, | ||||
|   envs: HashMap<String, String>, | ||||
| } | ||||
|  | ||||
| impl LaunchGuiRequest { | ||||
|   pub fn new(user: String, envs: HashMap<String, String>) -> Self { | ||||
|     Self { user, envs } | ||||
|   } | ||||
|  | ||||
|   pub fn user(&self) -> &str { | ||||
|     &self.user | ||||
|   } | ||||
|  | ||||
|   pub fn envs(&self) -> &HashMap<String, String> { | ||||
|     &self.envs | ||||
|   } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Deserialize, Serialize, Type)] | ||||
| pub struct ConnectArgs { | ||||
|   cookie: String, | ||||
|   vpnc_script: Option<String>, | ||||
|   user_agent: Option<String>, | ||||
|   os: Option<ClientOs>, | ||||
| } | ||||
|  | ||||
| impl ConnectArgs { | ||||
|   pub fn new(cookie: String) -> Self { | ||||
|     Self { | ||||
|       cookie, | ||||
|       vpnc_script: None, | ||||
|       user_agent: None, | ||||
|       os: None, | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   pub fn cookie(&self) -> &str { | ||||
|     &self.cookie | ||||
|   } | ||||
|  | ||||
|   pub fn vpnc_script(&self) -> Option<String> { | ||||
|     self.vpnc_script.clone() | ||||
|   } | ||||
|  | ||||
|   pub fn user_agent(&self) -> Option<String> { | ||||
|     self.user_agent.clone() | ||||
|   } | ||||
|  | ||||
|   pub fn openconnect_os(&self) -> Option<String> { | ||||
|     self | ||||
|       .os | ||||
|       .as_ref() | ||||
|       .map(|os| os.to_openconnect_os().to_string()) | ||||
|   } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Deserialize, Serialize, Type)] | ||||
| pub struct ConnectRequest { | ||||
|   info: ConnectInfo, | ||||
|   args: ConnectArgs, | ||||
| } | ||||
|  | ||||
| impl ConnectRequest { | ||||
|   pub fn new(info: ConnectInfo, cookie: String) -> Self { | ||||
|     Self { | ||||
|       info, | ||||
|       args: ConnectArgs::new(cookie), | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   pub fn with_vpnc_script<T: Into<Option<String>>>(mut self, vpnc_script: T) -> Self { | ||||
|     self.args.vpnc_script = vpnc_script.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 | ||||
|   } | ||||
|  | ||||
|   pub fn with_os<T: Into<Option<ClientOs>>>(mut self, os: T) -> Self { | ||||
|     self.args.os = os.into(); | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn gateway(&self) -> &Gateway { | ||||
|     self.info.gateway() | ||||
|   } | ||||
|  | ||||
|   pub fn info(&self) -> &ConnectInfo { | ||||
|     &self.info | ||||
|   } | ||||
|  | ||||
|   pub fn args(&self) -> &ConnectArgs { | ||||
|     &self.args | ||||
|   } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Deserialize, Serialize, Type)] | ||||
| pub struct DisconnectRequest; | ||||
|  | ||||
| /// Requests that can be sent to the service | ||||
| #[derive(Debug, Deserialize, Serialize)] | ||||
| pub enum WsRequest { | ||||
|   Connect(Box<ConnectRequest>), | ||||
|   Disconnect(DisconnectRequest), | ||||
| } | ||||
							
								
								
									
										34
									
								
								crates/gpapi/src/service/vpn_state.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								crates/gpapi/src/service/vpn_state.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use specta::Type; | ||||
|  | ||||
| use crate::gateway::Gateway; | ||||
|  | ||||
| #[derive(Debug, Deserialize, Serialize, Type, Clone)] | ||||
| pub struct ConnectInfo { | ||||
|   portal: String, | ||||
|   gateway: Gateway, | ||||
|   gateways: Vec<Gateway>, | ||||
| } | ||||
|  | ||||
| impl ConnectInfo { | ||||
|   pub fn new(portal: String, gateway: Gateway, gateways: Vec<Gateway>) -> Self { | ||||
|     Self { | ||||
|       portal, | ||||
|       gateway, | ||||
|       gateways, | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   pub fn gateway(&self) -> &Gateway { | ||||
|     &self.gateway | ||||
|   } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize, Clone)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub enum VpnState { | ||||
|   Disconnected, | ||||
|   Connecting(Box<ConnectInfo>), | ||||
|   Connected(Box<ConnectInfo>), | ||||
|   Disconnecting, | ||||
| } | ||||
							
								
								
									
										21
									
								
								crates/gpapi/src/utils/base64.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								crates/gpapi/src/utils/base64.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| use base64::{engine::general_purpose, Engine}; | ||||
|  | ||||
| pub fn encode(data: &[u8]) -> String { | ||||
|   let engine = general_purpose::STANDARD; | ||||
|  | ||||
|   engine.encode(data) | ||||
| } | ||||
|  | ||||
| pub fn decode_to_vec(s: &str) -> anyhow::Result<Vec<u8>> { | ||||
|   let engine = general_purpose::STANDARD; | ||||
|   let decoded = engine.decode(s)?; | ||||
|  | ||||
|   Ok(decoded) | ||||
| } | ||||
|  | ||||
| pub(crate) fn decode_to_string(s: &str) -> anyhow::Result<String> { | ||||
|   let decoded = decode_to_vec(s)?; | ||||
|   let decoded = String::from_utf8(decoded)?; | ||||
|  | ||||
|   Ok(decoded) | ||||
| } | ||||
							
								
								
									
										108
									
								
								crates/gpapi/src/utils/crypto.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								crates/gpapi/src/utils/crypto.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | ||||
| use chacha20poly1305::{ | ||||
|   aead::{Aead, OsRng}, | ||||
|   AeadCore, ChaCha20Poly1305, Key, KeyInit, Nonce, | ||||
| }; | ||||
| use serde::{de::DeserializeOwned, Serialize}; | ||||
|  | ||||
| pub fn generate_key() -> Key { | ||||
|   ChaCha20Poly1305::generate_key(&mut OsRng) | ||||
| } | ||||
|  | ||||
| pub fn encrypt<T>(key: &Key, value: &T) -> anyhow::Result<Vec<u8>> | ||||
| where | ||||
|   T: Serialize, | ||||
| { | ||||
|   let cipher = ChaCha20Poly1305::new(key); | ||||
|   let nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng); | ||||
|  | ||||
|   let data = serde_json::to_vec(value)?; | ||||
|   let cipher_text = cipher.encrypt(&nonce, data.as_ref())?; | ||||
|  | ||||
|   let mut encrypted = Vec::new(); | ||||
|   encrypted.extend_from_slice(&nonce); | ||||
|   encrypted.extend_from_slice(&cipher_text); | ||||
|  | ||||
|   Ok(encrypted) | ||||
| } | ||||
|  | ||||
| pub fn decrypt<T>(key: &Key, encrypted: Vec<u8>) -> anyhow::Result<T> | ||||
| where | ||||
|   T: DeserializeOwned, | ||||
| { | ||||
|   let cipher = ChaCha20Poly1305::new(key); | ||||
|  | ||||
|   let nonce = Nonce::from_slice(&encrypted[..12]); | ||||
|   let cipher_text = &encrypted[12..]; | ||||
|  | ||||
|   let plaintext = cipher.decrypt(nonce, cipher_text)?; | ||||
|  | ||||
|   let value = serde_json::from_slice(&plaintext)?; | ||||
|  | ||||
|   Ok(value) | ||||
| } | ||||
|  | ||||
| pub struct Crypto { | ||||
|   key: Vec<u8>, | ||||
| } | ||||
|  | ||||
| impl Crypto { | ||||
|   pub fn new(key: Vec<u8>) -> Self { | ||||
|     Self { key } | ||||
|   } | ||||
|  | ||||
|   pub fn encrypt<T: Serialize>(&self, plain: T) -> anyhow::Result<Vec<u8>> { | ||||
|     let key: &[u8] = &self.key; | ||||
|     let encrypted_data = encrypt(key.into(), &plain)?; | ||||
|  | ||||
|     Ok(encrypted_data) | ||||
|   } | ||||
|  | ||||
|   pub fn decrypt<T: DeserializeOwned>(&self, encrypted: Vec<u8>) -> anyhow::Result<T> { | ||||
|     let key: &[u8] = &self.key; | ||||
|     decrypt(key.into(), encrypted) | ||||
|   } | ||||
|  | ||||
|   pub fn encrypt_to<T: Serialize>(&self, path: &std::path::Path, plain: T) -> anyhow::Result<()> { | ||||
|     let encrypted_data = self.encrypt(plain)?; | ||||
|     std::fs::write(path, encrypted_data)?; | ||||
|  | ||||
|     Ok(()) | ||||
|   } | ||||
|  | ||||
|   pub fn decrypt_from<T: DeserializeOwned>(&self, path: &std::path::Path) -> anyhow::Result<T> { | ||||
|     let encrypted_data = std::fs::read(path)?; | ||||
|     self.decrypt(encrypted_data) | ||||
|   } | ||||
| } | ||||
|  | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|   use serde::Deserialize; | ||||
|  | ||||
|   use super::*; | ||||
|  | ||||
|   #[derive(Serialize, Deserialize)] | ||||
|   struct User { | ||||
|     name: String, | ||||
|     age: u8, | ||||
|   } | ||||
|  | ||||
|   #[test] | ||||
|   fn it_works() -> anyhow::Result<()> { | ||||
|     let key = generate_key(); | ||||
|  | ||||
|     let user = User { | ||||
|       name: "test".to_string(), | ||||
|       age: 18, | ||||
|     }; | ||||
|  | ||||
|     let encrypted = encrypt(&key, &user)?; | ||||
|  | ||||
|     let decrypted_user = decrypt::<User>(&key, encrypted)?; | ||||
|  | ||||
|     assert_eq!(user.name, decrypted_user.name); | ||||
|     assert_eq!(user.age, decrypted_user.age); | ||||
|  | ||||
|     Ok(()) | ||||
|   } | ||||
| } | ||||
							
								
								
									
										20
									
								
								crates/gpapi/src/utils/endpoint.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								crates/gpapi/src/utils/endpoint.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| use tokio::fs; | ||||
|  | ||||
| use crate::GP_SERVICE_LOCK_FILE; | ||||
|  | ||||
| async fn read_port() -> anyhow::Result<String> { | ||||
|   let port = fs::read_to_string(GP_SERVICE_LOCK_FILE).await?; | ||||
|   Ok(port.trim().to_string()) | ||||
| } | ||||
|  | ||||
| pub async fn http_endpoint() -> anyhow::Result<String> { | ||||
|   let port = read_port().await?; | ||||
|  | ||||
|   Ok(format!("http://127.0.0.1:{}", port)) | ||||
| } | ||||
|  | ||||
| pub async fn ws_endpoint() -> anyhow::Result<String> { | ||||
|   let port = read_port().await?; | ||||
|  | ||||
|   Ok(format!("ws://127.0.0.1:{}/ws", port)) | ||||
| } | ||||
							
								
								
									
										37
									
								
								crates/gpapi/src/utils/env_file.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								crates/gpapi/src/utils/env_file.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| use std::collections::HashMap; | ||||
| use std::env; | ||||
| use std::io::Write; | ||||
| use std::path::Path; | ||||
|  | ||||
| use tempfile::NamedTempFile; | ||||
|  | ||||
| pub fn persist_env_vars(extra: Option<HashMap<String, String>>) -> anyhow::Result<NamedTempFile> { | ||||
|   let mut env_file = NamedTempFile::new()?; | ||||
|   let content = env::vars() | ||||
|     .map(|(key, value)| format!("{}={}", key, value)) | ||||
|     .chain( | ||||
|       extra | ||||
|         .unwrap_or_default() | ||||
|         .into_iter() | ||||
|         .map(|(key, value)| format!("{}={}", key, value)), | ||||
|     ) | ||||
|     .collect::<Vec<String>>() | ||||
|     .join("\n"); | ||||
|  | ||||
|   writeln!(env_file, "{}", content)?; | ||||
|  | ||||
|   Ok(env_file) | ||||
| } | ||||
|  | ||||
| pub fn load_env_vars<T: AsRef<Path>>(env_file: T) -> anyhow::Result<HashMap<String, String>> { | ||||
|   let content = std::fs::read_to_string(env_file)?; | ||||
|   let mut env_vars: HashMap<String, String> = HashMap::new(); | ||||
|  | ||||
|   for line in content.lines() { | ||||
|     if let Some((key, value)) = line.split_once('=') { | ||||
|       env_vars.insert(key.to_string(), value.to_string()); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Ok(env_vars) | ||||
| } | ||||
							
								
								
									
										39
									
								
								crates/gpapi/src/utils/lock_file.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								crates/gpapi/src/utils/lock_file.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| use std::path::PathBuf; | ||||
|  | ||||
| pub struct LockFile { | ||||
|   path: PathBuf, | ||||
| } | ||||
|  | ||||
| impl LockFile { | ||||
|   pub fn new<P: Into<PathBuf>>(path: P) -> Self { | ||||
|     Self { path: path.into() } | ||||
|   } | ||||
|  | ||||
|   pub fn exists(&self) -> bool { | ||||
|     self.path.exists() | ||||
|   } | ||||
|  | ||||
|   pub fn lock(&self, content: impl AsRef<[u8]>) -> anyhow::Result<()> { | ||||
|     std::fs::write(&self.path, content)?; | ||||
|     Ok(()) | ||||
|   } | ||||
|  | ||||
|   pub fn unlock(&self) -> anyhow::Result<()> { | ||||
|     std::fs::remove_file(&self.path)?; | ||||
|     Ok(()) | ||||
|   } | ||||
|  | ||||
|   pub async fn check_health(&self) -> bool { | ||||
|     match std::fs::read_to_string(&self.path) { | ||||
|       Ok(content) => { | ||||
|         let url = format!("http://127.0.0.1:{}/health", content.trim()); | ||||
|  | ||||
|         match reqwest::get(&url).await { | ||||
|           Ok(resp) => resp.status().is_success(), | ||||
|           Err(_) => false, | ||||
|         } | ||||
|       } | ||||
|       Err(_) => false, | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										40
									
								
								crates/gpapi/src/utils/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								crates/gpapi/src/utils/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| use reqwest::Url; | ||||
|  | ||||
| pub(crate) mod xml; | ||||
|  | ||||
| pub mod base64; | ||||
| pub mod crypto; | ||||
| pub mod endpoint; | ||||
| pub mod env_file; | ||||
| pub mod lock_file; | ||||
| pub mod openssl; | ||||
| pub mod redact; | ||||
| #[cfg(feature = "tauri")] | ||||
| pub mod window; | ||||
|  | ||||
| mod shutdown_signal; | ||||
|  | ||||
| pub use shutdown_signal::shutdown_signal; | ||||
|  | ||||
| /// Normalize the server URL to the format `https://<host>:<port>` | ||||
| pub fn normalize_server(server: &str) -> anyhow::Result<String> { | ||||
|   let server = if server.starts_with("https://") || server.starts_with("http://") { | ||||
|     server.to_string() | ||||
|   } else { | ||||
|     format!("https://{}", server) | ||||
|   }; | ||||
|  | ||||
|   let normalized_url = Url::parse(&server)?; | ||||
|   let scheme = normalized_url.scheme(); | ||||
|   let host = normalized_url | ||||
|     .host_str() | ||||
|     .ok_or(anyhow::anyhow!("Invalid server URL: missing host"))?; | ||||
|  | ||||
|   let port: String = normalized_url | ||||
|     .port() | ||||
|     .map_or("".into(), |port| format!(":{}", port)); | ||||
|  | ||||
|   let normalized_url = format!("{}://{}{}", scheme, host, port); | ||||
|  | ||||
|   Ok(normalized_url) | ||||
| } | ||||
							
								
								
									
										37
									
								
								crates/gpapi/src/utils/openssl.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								crates/gpapi/src/utils/openssl.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| use std::path::Path; | ||||
|  | ||||
| use tempfile::NamedTempFile; | ||||
|  | ||||
| pub fn openssl_conf() -> String { | ||||
|   let option = "UnsafeLegacyServerConnect"; | ||||
|  | ||||
|   format!( | ||||
|     "openssl_conf = openssl_init | ||||
|  | ||||
| [openssl_init] | ||||
| ssl_conf = ssl_sect | ||||
|  | ||||
| [ssl_sect] | ||||
| system_default = system_default_sect | ||||
|  | ||||
| [system_default_sect] | ||||
| Options = {}", | ||||
|     option | ||||
|   ) | ||||
| } | ||||
|  | ||||
| pub fn fix_openssl<P: AsRef<Path>>(path: P) -> anyhow::Result<()> { | ||||
|   let content = openssl_conf(); | ||||
|   std::fs::write(path, content)?; | ||||
|   Ok(()) | ||||
| } | ||||
|  | ||||
| pub fn fix_openssl_env() -> anyhow::Result<NamedTempFile> { | ||||
|   let openssl_conf = NamedTempFile::new()?; | ||||
|   let openssl_conf_path = openssl_conf.path(); | ||||
|  | ||||
|   fix_openssl(openssl_conf_path)?; | ||||
|   std::env::set_var("OPENSSL_CONF", openssl_conf_path); | ||||
|  | ||||
|   Ok(openssl_conf) | ||||
| } | ||||
							
								
								
									
										227
									
								
								crates/gpapi/src/utils/redact.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										227
									
								
								crates/gpapi/src/utils/redact.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,227 @@ | ||||
| use std::sync::RwLock; | ||||
|  | ||||
| use redact_engine::{Pattern, Redaction as RedactEngine}; | ||||
| use regex::Regex; | ||||
| use url::{form_urlencoded, Url}; | ||||
|  | ||||
| pub struct Redaction { | ||||
|   redact_engine: RwLock<Option<RedactEngine>>, | ||||
| } | ||||
|  | ||||
| impl Default for Redaction { | ||||
|   fn default() -> Self { | ||||
|     Self::new() | ||||
|   } | ||||
| } | ||||
|  | ||||
| impl Redaction { | ||||
|   pub fn new() -> Self { | ||||
|     let redact_engine = RedactEngine::custom("[**********]").add_pattern(Pattern { | ||||
|       test: Regex::new("(((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4})").unwrap(), | ||||
|       group: 1, | ||||
|     }); | ||||
|  | ||||
|     Self { | ||||
|       redact_engine: RwLock::new(Some(redact_engine)), | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   pub fn add_value(&self, text: &str) -> anyhow::Result<()> { | ||||
|     let mut redact_engine = self | ||||
|       .redact_engine | ||||
|       .write() | ||||
|       .map_err(|_| anyhow::anyhow!("Failed to acquire write lock on redact engine"))?; | ||||
|  | ||||
|     *redact_engine = Some( | ||||
|       redact_engine | ||||
|         .take() | ||||
|         .ok_or_else(|| anyhow::anyhow!("Failed to take redact engine"))? | ||||
|         .add_value(text)?, | ||||
|     ); | ||||
|  | ||||
|     Ok(()) | ||||
|   } | ||||
|  | ||||
|   pub fn add_values(&self, texts: &[&str]) -> anyhow::Result<()> { | ||||
|     let mut redact_engine = self | ||||
|       .redact_engine | ||||
|       .write() | ||||
|       .map_err(|_| anyhow::anyhow!("Failed to acquire write lock on redact engine"))?; | ||||
|  | ||||
|     *redact_engine = Some( | ||||
|       redact_engine | ||||
|         .take() | ||||
|         .ok_or_else(|| anyhow::anyhow!("Failed to take redact engine"))? | ||||
|         .add_values(texts.to_vec())?, | ||||
|     ); | ||||
|  | ||||
|     Ok(()) | ||||
|   } | ||||
|  | ||||
|   pub fn redact_str(&self, text: &str) -> String { | ||||
|     self | ||||
|       .redact_engine | ||||
|       .read() | ||||
|       .expect("Failed to acquire read lock on redact engine") | ||||
|       .as_ref() | ||||
|       .expect("Failed to get redact engine") | ||||
|       .redact_str(text) | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// Redact a value by replacing all but the first and last character with asterisks, | ||||
| /// The length of the value to be redacted must be at least 3 characters. | ||||
| /// e.g. "foo" -> "f**********o" | ||||
| pub fn redact_value(text: &str) -> String { | ||||
|   if text.len() < 3 { | ||||
|     return text.to_string(); | ||||
|   } | ||||
|  | ||||
|   let mut redacted = String::new(); | ||||
|   redacted.push_str(&text[0..1]); | ||||
|   redacted.push_str(&"*".repeat(10)); | ||||
|   redacted.push_str(&text[text.len() - 1..]); | ||||
|  | ||||
|   redacted | ||||
| } | ||||
|  | ||||
| pub fn redact_uri(uri: &str) -> String { | ||||
|   let Ok(mut url) = Url::parse(uri) else { | ||||
|     return uri.to_string(); | ||||
|   }; | ||||
|  | ||||
|   // Could be a data: URI | ||||
|   if url.cannot_be_a_base() { | ||||
|     if url.scheme() == "about" { | ||||
|       return uri.to_string(); | ||||
|     } | ||||
|  | ||||
|     if url.path().len() > 15 { | ||||
|       return format!( | ||||
|         "{}:{}{}", | ||||
|         url.scheme(), | ||||
|         &url.path()[0..10], | ||||
|         redact_value(&url.path()[10..]) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return format!("{}:{}", url.scheme(), redact_value(url.path())); | ||||
|   } | ||||
|  | ||||
|   let host = url.host_str().unwrap_or_default(); | ||||
|   if url.set_host(Some(&redact_value(host))).is_err() { | ||||
|     let redacted_query = redact_query(url.query()) | ||||
|       .as_deref() | ||||
|       .map(|query| format!("?{}", query)) | ||||
|       .unwrap_or_default(); | ||||
|  | ||||
|     return format!( | ||||
|       "{}://[**********]{}{}", | ||||
|       url.scheme(), | ||||
|       url.path(), | ||||
|       redacted_query | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   let redacted_query = redact_query(url.query()); | ||||
|   url.set_query(redacted_query.as_deref()); | ||||
|   url.to_string() | ||||
| } | ||||
|  | ||||
| fn redact_query(query: Option<&str>) -> Option<String> { | ||||
|   let query = query?; | ||||
|  | ||||
|   let query_pairs = form_urlencoded::parse(query.as_bytes()); | ||||
|   let mut redacted_pairs = query_pairs.map(|(key, value)| (key, redact_value(&value))); | ||||
|  | ||||
|   let query = form_urlencoded::Serializer::new(String::new()) | ||||
|     .extend_pairs(redacted_pairs.by_ref()) | ||||
|     .finish(); | ||||
|  | ||||
|   Some(query) | ||||
| } | ||||
|  | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|   use super::*; | ||||
|  | ||||
|   #[test] | ||||
|   fn it_should_not_redact_value() { | ||||
|     let text = "fo"; | ||||
|  | ||||
|     assert_eq!(redact_value(text), "fo"); | ||||
|   } | ||||
|  | ||||
|   #[test] | ||||
|   fn it_should_redact_value() { | ||||
|     let text = "foo"; | ||||
|  | ||||
|     assert_eq!(redact_value(text), "f**********o"); | ||||
|   } | ||||
|  | ||||
|   #[test] | ||||
|   fn it_should_redact_dynamic_value() { | ||||
|     let redaction = Redaction::new(); | ||||
|  | ||||
|     redaction.add_value("foo").unwrap(); | ||||
|  | ||||
|     assert_eq!( | ||||
|       redaction.redact_str("hello, foo, bar"), | ||||
|       "hello, [**********], bar" | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   #[test] | ||||
|   fn it_should_redact_dynamic_values() { | ||||
|     let redaction = Redaction::new(); | ||||
|  | ||||
|     redaction.add_values(&["foo", "bar"]).unwrap(); | ||||
|  | ||||
|     assert_eq!( | ||||
|       redaction.redact_str("hello, foo, bar"), | ||||
|       "hello, [**********], [**********]" | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   #[test] | ||||
|   fn it_should_redact_uri() { | ||||
|     let uri = "https://foo.bar"; | ||||
|     assert_eq!(redact_uri(uri), "https://f**********r/"); | ||||
|  | ||||
|     let uri = "https://foo.bar/"; | ||||
|     assert_eq!(redact_uri(uri), "https://f**********r/"); | ||||
|  | ||||
|     let uri = "https://foo.bar/baz"; | ||||
|     assert_eq!(redact_uri(uri), "https://f**********r/baz"); | ||||
|  | ||||
|     let uri = "https://foo.bar/baz?qux=quux"; | ||||
|     assert_eq!(redact_uri(uri), "https://f**********r/baz?qux=q**********x"); | ||||
|   } | ||||
|  | ||||
|   #[test] | ||||
|   fn it_should_redact_data_uri() { | ||||
|     let uri = "data:text/plain;a"; | ||||
|     assert_eq!(redact_uri(uri), "data:t**********a"); | ||||
|  | ||||
|     let uri = "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ=="; | ||||
|     assert_eq!(redact_uri(uri), "data:text/plain;**********="); | ||||
|  | ||||
|     let uri = "about:blank"; | ||||
|     assert_eq!(redact_uri(uri), "about:blank"); | ||||
|   } | ||||
|  | ||||
|   #[test] | ||||
|   fn it_should_redact_ipv6() { | ||||
|     let uri = "https://[2001:db8::1]:8080"; | ||||
|     assert_eq!(redact_uri(uri), "https://[**********]/"); | ||||
|  | ||||
|     let uri = "https://[2001:db8::1]:8080/"; | ||||
|     assert_eq!(redact_uri(uri), "https://[**********]/"); | ||||
|  | ||||
|     let uri = "https://[2001:db8::1]:8080/baz"; | ||||
|     assert_eq!(redact_uri(uri), "https://[**********]/baz"); | ||||
|  | ||||
|     let uri = "https://[2001:db8::1]:8080/baz?qux=quux"; | ||||
|     assert_eq!(redact_uri(uri), "https://[**********]/baz?qux=q**********x"); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										22
									
								
								crates/gpapi/src/utils/shutdown_signal.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								crates/gpapi/src/utils/shutdown_signal.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| use tokio::signal; | ||||
|  | ||||
| pub async fn shutdown_signal() { | ||||
|   let ctrl_c = async { | ||||
|     signal::ctrl_c() | ||||
|       .await | ||||
|       .expect("failed to install Ctrl+C handler"); | ||||
|   }; | ||||
|  | ||||
|   #[cfg(unix)] | ||||
|   let terminate = async { | ||||
|     signal::unix::signal(signal::unix::SignalKind::terminate()) | ||||
|       .expect("failed to install signal handler") | ||||
|       .recv() | ||||
|       .await; | ||||
|   }; | ||||
|  | ||||
|   tokio::select! { | ||||
|       _ = ctrl_c => {}, | ||||
|       _ = terminate => {}, | ||||
|   } | ||||
| } | ||||
							
								
								
									
										90
									
								
								crates/gpapi/src/utils/window.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								crates/gpapi/src/utils/window.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | ||||
| use std::{process::ExitStatus, time::Duration}; | ||||
|  | ||||
| use anyhow::bail; | ||||
| use log::{info, warn}; | ||||
| use tauri::{window::MenuHandle, Window}; | ||||
| use tokio::process::Command; | ||||
|  | ||||
| pub trait WindowExt { | ||||
|   fn raise(&self) -> anyhow::Result<()>; | ||||
| } | ||||
|  | ||||
| impl WindowExt for Window { | ||||
|   fn raise(&self) -> anyhow::Result<()> { | ||||
|     raise_window(self) | ||||
|   } | ||||
| } | ||||
|  | ||||
| pub fn raise_window(win: &Window) -> anyhow::Result<()> { | ||||
|   let is_wayland = std::env::var("XDG_SESSION_TYPE").unwrap_or_default() == "wayland"; | ||||
|  | ||||
|   if is_wayland { | ||||
|     win.hide()?; | ||||
|     win.show()?; | ||||
|   } else { | ||||
|     if !win.is_visible()? { | ||||
|       win.show()?; | ||||
|     } | ||||
|     let title = win.title()?; | ||||
|     tokio::spawn(async move { | ||||
|       info!("Raising window: {}", title); | ||||
|       if let Err(err) = wmctrl_raise_window(&title).await { | ||||
|         warn!("Failed to raise window: {}", err); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   // Calling window.show() on Windows will cause the menu to be shown. | ||||
|   hide_menu(win.menu_handle()); | ||||
|  | ||||
|   Ok(()) | ||||
| } | ||||
|  | ||||
| async fn wmctrl_raise_window(title: &str) -> anyhow::Result<()> { | ||||
|   let mut counter = 0; | ||||
|  | ||||
|   loop { | ||||
|     if let Ok(exit_status) = wmctrl_try_raise_window(title).await { | ||||
|       if exit_status.success() { | ||||
|         info!("Window raised after {} attempts", counter + 1); | ||||
|         return Ok(()); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if counter >= 10 { | ||||
|       bail!("Failed to raise window: {}", title) | ||||
|     } | ||||
|  | ||||
|     counter += 1; | ||||
|     tokio::time::sleep(Duration::from_millis(100)).await; | ||||
|   } | ||||
| } | ||||
|  | ||||
| async fn wmctrl_try_raise_window(title: &str) -> anyhow::Result<ExitStatus> { | ||||
|   let exit_status = Command::new("wmctrl") | ||||
|     .arg("-F") | ||||
|     .arg("-a") | ||||
|     .arg(title) | ||||
|     .spawn()? | ||||
|     .wait() | ||||
|     .await?; | ||||
|  | ||||
|   Ok(exit_status) | ||||
| } | ||||
|  | ||||
| fn hide_menu(menu_handle: MenuHandle) { | ||||
|   tokio::spawn(async move { | ||||
|     loop { | ||||
|       let menu_visible = menu_handle.is_visible().unwrap_or(false); | ||||
|  | ||||
|       if !menu_visible { | ||||
|         break; | ||||
|       } | ||||
|  | ||||
|       if menu_visible { | ||||
|         let _ = menu_handle.hide(); | ||||
|         tokio::time::sleep(Duration::from_millis(10)).await; | ||||
|       } | ||||
|     } | ||||
|   }); | ||||
| } | ||||
							
								
								
									
										6
									
								
								crates/gpapi/src/utils/xml.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								crates/gpapi/src/utils/xml.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| use roxmltree::Document; | ||||
|  | ||||
| pub(crate) fn get_child_text(doc: &Document, name: &str) -> Option<String> { | ||||
|   let node = doc.descendants().find(|n| n.has_tag_name(name))?; | ||||
|   node.text().map(|s| s.to_string()) | ||||
| } | ||||
							
								
								
									
										27
									
								
								crates/gpapi/tests/files/gateway_login.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								crates/gpapi/tests/files/gateway_login.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <jnlp> | ||||
|     <application-desc> | ||||
|         <argument>(null)</argument> | ||||
|         <argument>xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</argument> | ||||
|         <argument>xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</argument> | ||||
|         <argument>XXX-GP-Gateway-N</argument> | ||||
|         <argument>user</argument> | ||||
|         <argument>AD_Authentication</argument> | ||||
|         <argument>vsys1</argument> | ||||
|         <argument>corp.example.com</argument> | ||||
|         <argument>(null)</argument> | ||||
|         <argument></argument> | ||||
|         <argument></argument> | ||||
|         <argument></argument> | ||||
|         <argument>tunnel</argument> | ||||
|         <argument>-1</argument> | ||||
|         <argument>4100</argument> | ||||
|         <argument></argument> | ||||
|         <argument>xxxxxx</argument> | ||||
|         <argument>aaaaaa</argument> | ||||
|         <argument></argument> | ||||
|         <argument>4</argument> | ||||
|         <argument>unknown</argument> | ||||
|         <argument></argument> | ||||
|     </application-desc> | ||||
| </jnlp> | ||||
							
								
								
									
										212
									
								
								crates/gpapi/tests/files/portal_config.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										212
									
								
								crates/gpapi/tests/files/portal_config.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,212 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <policy> | ||||
|     <portal-name>vpn.example.com</portal-name> | ||||
|     <portal-config-version>4100</portal-config-version> | ||||
|     <version>6.0.1-19 </version> | ||||
|     <client-role>global-protect-full</client-role> | ||||
|     <agent-user-override-key>****</agent-user-override-key> | ||||
|     <root-ca> | ||||
|         <entry name="DigiCert Global Root CA"> | ||||
|             <cert> | ||||
|                 -----BEGIN CERTIFICATE----- | ||||
|                 -----END CERTIFICATE----- | ||||
|             </cert> | ||||
|             <install-in-cert-store>yes</install-in-cert-store> | ||||
|         </entry> | ||||
|         <entry name="Thawte RSA CA 2018"> | ||||
|             <cert> | ||||
|                 -----BEGIN CERTIFICATE----- | ||||
|                 -----END CERTIFICATE----- | ||||
|             </cert> | ||||
|             <install-in-cert-store>yes</install-in-cert-store> | ||||
|         </entry> | ||||
|         <entry name="Temp_VPN_Root_Certificate"> | ||||
|             <cert> | ||||
|                 -----BEGIN CERTIFICATE----- | ||||
|                 -----END CERTIFICATE----- | ||||
|             </cert> | ||||
|             <install-in-cert-store>no</install-in-cert-store> | ||||
|         </entry> | ||||
|     </root-ca> | ||||
|     <connect-method>on-demand</connect-method> | ||||
|     <pre-logon-then-on-demand>yes</pre-logon-then-on-demand> | ||||
|     <refresh-config>yes</refresh-config> | ||||
|     <refresh-config-interval>24</refresh-config-interval> | ||||
|     <authentication-modifier> | ||||
|         <none /> | ||||
|     </authentication-modifier> | ||||
|     <authentication-override> | ||||
|         <accept-cookie>yes</accept-cookie> | ||||
|         <generate-cookie>yes</generate-cookie> | ||||
|         <cookie-lifetime> | ||||
|             <lifetime-in-days>365</lifetime-in-days> | ||||
|         </cookie-lifetime> | ||||
|         <cookie-encrypt-decrypt-cert>vpn.example.com</cookie-encrypt-decrypt-cert> | ||||
|     </authentication-override> | ||||
|     <use-sso>yes</use-sso> | ||||
|     <ip-address></ip-address> | ||||
|     <host></host> | ||||
|     <gateways> | ||||
|         <cutoff-time>5</cutoff-time> | ||||
|         <external> | ||||
|             <list> | ||||
|                 <entry name="xxx.xxx.xxx.xxx"> | ||||
|                     <priority-rule> | ||||
|                         <entry name="Any"> | ||||
|                             <priority>1</priority> | ||||
|                         </entry> | ||||
|                     </priority-rule> | ||||
|                     <priority>1</priority> | ||||
|                     <description>vpn_gateway</description> | ||||
|                 </entry> | ||||
|             </list> | ||||
|         </external> | ||||
|     </gateways> | ||||
|     <gateways-v6> | ||||
|         <cutoff-time>5</cutoff-time> | ||||
|         <external> | ||||
|             <list> | ||||
|                 <entry name="vpn_gateway"> | ||||
|                     <ipv4>xxx.xxx.xxx.xxx</ipv4> | ||||
|                     <priority-rule> | ||||
|                         <entry name="Any"> | ||||
|                             <priority>1</priority> | ||||
|                         </entry> | ||||
|                     </priority-rule> | ||||
|                     <priority>1</priority> | ||||
|                 </entry> | ||||
|             </list> | ||||
|         </external> | ||||
|     </gateways-v6> | ||||
|     <agent-ui> | ||||
|         <can-save-password>yes</can-save-password> | ||||
|         <passcode></passcode> | ||||
|         <uninstall-passwd></uninstall-passwd> | ||||
|         <agent-user-override-timeout>0</agent-user-override-timeout> | ||||
|         <max-agent-user-overrides>0</max-agent-user-overrides> | ||||
|         <help-page></help-page> | ||||
|         <help-page-2></help-page-2> | ||||
|         <welcome-page> | ||||
|             <display>no</display> | ||||
|             <page></page> | ||||
|         </welcome-page> | ||||
|         <agent-user-override>allowed</agent-user-override> | ||||
|         <enable-advanced-view>yes</enable-advanced-view> | ||||
|         <enable-do-not-display-this-welcome-page-again>yes</enable-do-not-display-this-welcome-page-again> | ||||
|         <can-change-portal>yes</can-change-portal> | ||||
|         <show-agent-icon>yes</show-agent-icon> | ||||
|         <password-expiry-message></password-expiry-message> | ||||
|         <init-panel>no</init-panel> | ||||
|         <user-input-on-top>no</user-input-on-top> | ||||
|     </agent-ui> | ||||
|     <hip-collection> | ||||
|         <hip-report-interval>3600</hip-report-interval> | ||||
|         <max-wait-time>20</max-wait-time> | ||||
|         <collect-hip-data>yes</collect-hip-data> | ||||
|         <default> | ||||
|             <category> | ||||
|                 <member>antivirus</member> | ||||
|                 <member>anti-spyware</member> | ||||
|                 <member>host-info</member> | ||||
|                 <member>data-loss-prevention</member> | ||||
|                 <member>patch-management</member> | ||||
|                 <member>firewall</member> | ||||
|                 <member>anti-malware</member> | ||||
|                 <member>disk-backup</member> | ||||
|                 <member>disk-encryption</member> | ||||
|             </category> | ||||
|         </default> | ||||
|     </hip-collection> | ||||
|     <agent-config> | ||||
|         <save-user-credentials>1</save-user-credentials> | ||||
|         <portal-2fa>no</portal-2fa> | ||||
|         <internal-gateway-2fa>no</internal-gateway-2fa> | ||||
|         <auto-discovery-external-gateway-2fa>no</auto-discovery-external-gateway-2fa> | ||||
|         <manual-only-gateway-2fa>no</manual-only-gateway-2fa> | ||||
|         <disconnect-reasons></disconnect-reasons> | ||||
|         <uninstall>allowed</uninstall> | ||||
|         <client-upgrade>prompt</client-upgrade> | ||||
|         <enable-signout>yes</enable-signout> | ||||
|         <use-sso-pin>no</use-sso-pin> | ||||
|         <use-sso-macos>no</use-sso-macos> | ||||
|         <logout-remove-sso>yes</logout-remove-sso> | ||||
|         <krb-auth-fail-fallback>yes</krb-auth-fail-fallback> | ||||
|         <default-browser>no</default-browser> | ||||
|         <retry-tunnel>30</retry-tunnel> | ||||
|         <retry-timeout>5</retry-timeout> | ||||
|         <traffic-enforcement>no</traffic-enforcement> | ||||
|         <enforce-globalprotect>no</enforce-globalprotect> | ||||
|         <enforcer-exception-list /> | ||||
|         <enforcer-exception-list-domain /> | ||||
|         <captive-portal-exception-timeout>0</captive-portal-exception-timeout> | ||||
|         <captive-portal-login-url></captive-portal-login-url> | ||||
|         <traffic-blocking-notification-delay>15</traffic-blocking-notification-delay> | ||||
|         <display-traffic-blocking-notification-msg>yes</display-traffic-blocking-notification-msg> | ||||
|         <traffic-blocking-notification-msg><div style="font-family:'Helvetica | ||||
|             Neue';"><h1 style="color:red;text-align:center; margin: 0; font-size: | ||||
|             30px;">Notice</h1><p style="margin: 0;font-size: 15px; | ||||
|             line-height: 1.2em;">To access the network, you must first connect to | ||||
|             GlobalProtect.</p></div></traffic-blocking-notification-msg> | ||||
|         <allow-traffic-blocking-notification-dismissal>yes</allow-traffic-blocking-notification-dismissal> | ||||
|         <display-captive-portal-detection-msg>no</display-captive-portal-detection-msg> | ||||
|         <captive-portal-detection-msg><div style="font-family:'Helvetica | ||||
|             Neue';"><h1 style="color:red;text-align:center; margin: 0; font-size: | ||||
|             30px;">Captive Portal Detected</h1><p style="margin: 0; font-size: | ||||
|             15px; line-height: 1.2em;">GlobalProtect has temporarily permitted network | ||||
|             access for you to connect to the Internet. Follow instructions from your internet | ||||
|             provider.</p><p style="margin: 0; font-size: 15px; line-height: | ||||
|             1.2em;">If you let the connection time out, open GlobalProtect and click Connect | ||||
|             to try again.</p></div></captive-portal-detection-msg> | ||||
|         <captive-portal-notification-delay>5</captive-portal-notification-delay> | ||||
|         <certificate-store-lookup>user-and-machine</certificate-store-lookup> | ||||
|         <scep-certificate-renewal-period>7</scep-certificate-renewal-period> | ||||
|         <ext-key-usage-oid-for-client-cert></ext-key-usage-oid-for-client-cert> | ||||
|         <retain-connection-smartcard-removal>yes</retain-connection-smartcard-removal> | ||||
|         <user-accept-terms-before-creating-tunnel>no</user-accept-terms-before-creating-tunnel> | ||||
|         <rediscover-network>yes</rediscover-network> | ||||
|         <resubmit-host-info>yes</resubmit-host-info> | ||||
|         <can-continue-if-portal-cert-invalid>yes</can-continue-if-portal-cert-invalid> | ||||
|         <user-switch-tunnel-rename-timeout>0</user-switch-tunnel-rename-timeout> | ||||
|         <pre-logon-tunnel-rename-timeout>0</pre-logon-tunnel-rename-timeout> | ||||
|         <preserve-tunnel-upon-user-logoff-timeout>0</preserve-tunnel-upon-user-logoff-timeout> | ||||
|         <ipsec-failover-ssl>0</ipsec-failover-ssl> | ||||
|         <display-tunnel-fallback-notification>yes</display-tunnel-fallback-notification> | ||||
|         <ssl-only-selection>0</ssl-only-selection> | ||||
|         <tunnel-mtu>1400</tunnel-mtu> | ||||
|         <max-internal-gateway-connection-attempts>0</max-internal-gateway-connection-attempts> | ||||
|         <adv-internal-host-detection>no</adv-internal-host-detection> | ||||
|         <portal-timeout>30</portal-timeout> | ||||
|         <connect-timeout>60</connect-timeout> | ||||
|         <receive-timeout>30</receive-timeout> | ||||
|         <split-tunnel-option>network-traffic</split-tunnel-option> | ||||
|         <enforce-dns>yes</enforce-dns> | ||||
|         <append-local-search-domain>no</append-local-search-domain> | ||||
|         <flush-dns>no</flush-dns> | ||||
|         <auto-proxy-pac></auto-proxy-pac> | ||||
|         <proxy-multiple-autodetect>no</proxy-multiple-autodetect> | ||||
|         <use-proxy>yes</use-proxy> | ||||
|         <wsc-autodetect>yes</wsc-autodetect> | ||||
|         <mfa-enabled>no</mfa-enabled> | ||||
|         <mfa-listening-port>4501</mfa-listening-port> | ||||
|         <mfa-trusted-host-list /> | ||||
|         <mfa-notification-msg>You have attempted to access a protected resource that requires | ||||
|             additional authentication. Proceed to authenticate at</mfa-notification-msg> | ||||
|         <mfa-prompt-suppress-time>0</mfa-prompt-suppress-time> | ||||
|         <ipv6-preferred>yes</ipv6-preferred> | ||||
|         <change-password-message></change-password-message> | ||||
|         <log-gateway>no</log-gateway> | ||||
|         <cdl-log>no</cdl-log> | ||||
|         <dem-notification>yes</dem-notification> | ||||
|         <diagnostic-servers /> | ||||
|         <dem-agent>not-install</dem-agent> | ||||
|         <quarantine-add-message>Access to the network from this device has been restricted as per | ||||
|             your organization's security policy. Please contact your IT Administrator.</quarantine-add-message> | ||||
|         <quarantine-remove-message>Access to the network from this device has been restored as per | ||||
|             your organization's security policy.</quarantine-remove-message> | ||||
|  | ||||
|     </agent-config> | ||||
|     <user-email>user@example.com</user-email> | ||||
|     <portal-userauthcookie>xxxxxx</portal-userauthcookie> | ||||
|     <portal-prelogonuserauthcookie>xxxxxx</portal-prelogonuserauthcookie> | ||||
|     <config-digest>2d8e997765a2f59cbf80284b2f2fbd38</config-digest> | ||||
| </policy> | ||||
							
								
								
									
										22
									
								
								crates/gpapi/tests/files/prelogin_saml.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								crates/gpapi/tests/files/prelogin_saml.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <prelogin-response> | ||||
|     <status>Success</status> | ||||
|     <ccusername></ccusername> | ||||
|     <autosubmit>false</autosubmit> | ||||
|     <msg></msg> | ||||
|     <newmsg></newmsg> | ||||
|     <authentication-message>Enter login credentials</authentication-message> | ||||
|     <username-label>Username</username-label> | ||||
|     <password-label>Password</password-label> | ||||
|     <panos-version>1</panos-version> | ||||
|     <saml-default-browser>yes</saml-default-browser> | ||||
|  | ||||
|     <cas-auth></cas-auth> | ||||
|     <saml-auth-status>0</saml-auth-status> | ||||
|     <saml-auth-method>REDIRECT</saml-auth-method> | ||||
|     <saml-request-timeout>600</saml-request-timeout> | ||||
|     <saml-request-id>0</saml-request-id> | ||||
|     <saml-request>U0FNTFJlcXVlc3Q9eHh4</saml-request> | ||||
|     <auth-api>no</auth-api> | ||||
|     <region>CN</region> | ||||
| </prelogin-response> | ||||
							
								
								
									
										15
									
								
								crates/gpapi/tests/files/prelogin_standard.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								crates/gpapi/tests/files/prelogin_standard.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <prelogin-response> | ||||
|     <status>Success</status> | ||||
|     <ccusername></ccusername> | ||||
|     <autosubmit>false</autosubmit> | ||||
|     <msg></msg> | ||||
|     <newmsg></newmsg> | ||||
|     <authentication-message>Enter login credentials</authentication-message> | ||||
|     <username-label>Username</username-label> | ||||
|     <password-label>Password</password-label> | ||||
|     <panos-version>1</panos-version> | ||||
|     <saml-default-browser>yes</saml-default-browser> | ||||
|     <auth-api>no</auth-api> | ||||
|     <region>US</region> | ||||
| </prelogin-response> | ||||
							
								
								
									
										13
									
								
								crates/openconnect/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								crates/openconnect/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| [package] | ||||
| name = "openconnect" | ||||
| version.workspace = true | ||||
| edition.workspace = true | ||||
| license.workspace = true | ||||
| links = "openconnect" | ||||
|  | ||||
| [dependencies] | ||||
| log.workspace = true | ||||
| is_executable.workspace = true | ||||
|  | ||||
| [build-dependencies] | ||||
| cc = "1" | ||||
							
								
								
									
										12
									
								
								crates/openconnect/build.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								crates/openconnect/build.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| fn main() { | ||||
|   // Link to the native openconnect library | ||||
|   println!("cargo:rustc-link-lib=openconnect"); | ||||
|   println!("cargo:rerun-if-changed=src/ffi/vpn.c"); | ||||
|   println!("cargo:rerun-if-changed=src/ffi/vpn.h"); | ||||
|  | ||||
|   // Compile the vpn.c file | ||||
|   cc::Build::new() | ||||
|     .file("src/ffi/vpn.c") | ||||
|     .include("src/ffi") | ||||
|     .compile("vpn"); | ||||
| } | ||||
							
								
								
									
										71
									
								
								crates/openconnect/src/ffi/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								crates/openconnect/src/ffi/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,71 @@ | ||||
| use crate::Vpn; | ||||
| use log::{debug, info, trace, warn}; | ||||
| use std::ffi::{c_char, c_int, c_void}; | ||||
|  | ||||
| #[repr(C)] | ||||
| #[derive(Debug)] | ||||
| pub(crate) struct ConnectOptions { | ||||
|   pub user_data: *mut c_void, | ||||
|  | ||||
|   pub server: *const c_char, | ||||
|   pub cookie: *const c_char, | ||||
|   pub user_agent: *const c_char, | ||||
|  | ||||
|   pub script: *const c_char, | ||||
|   pub os: *const c_char, | ||||
|   pub certificate: *const c_char, | ||||
|   pub servercert: *const c_char, | ||||
| } | ||||
|  | ||||
| #[link(name = "vpn")] | ||||
| extern "C" { | ||||
|   #[link_name = "vpn_connect"] | ||||
|   fn vpn_connect( | ||||
|     options: *const ConnectOptions, | ||||
|     callback: extern "C" fn(i32, *mut c_void), | ||||
|   ) -> c_int; | ||||
|  | ||||
|   #[link_name = "vpn_disconnect"] | ||||
|   fn vpn_disconnect(); | ||||
| } | ||||
|  | ||||
| pub(crate) fn connect(options: &ConnectOptions) -> i32 { | ||||
|   unsafe { vpn_connect(options, on_vpn_connected) } | ||||
| } | ||||
|  | ||||
| pub(crate) fn disconnect() { | ||||
|   unsafe { vpn_disconnect() } | ||||
| } | ||||
|  | ||||
| #[no_mangle] | ||||
| extern "C" fn on_vpn_connected(pipe_fd: i32, vpn: *mut c_void) { | ||||
|   let vpn = unsafe { &*(vpn as *const Vpn) }; | ||||
|   vpn.on_connected(pipe_fd); | ||||
| } | ||||
|  | ||||
| // Logger used in the C code. | ||||
| // level: 0 = error, 1 = info, 2 = debug, 3 = trace | ||||
| // map the error level log in openconnect to the warning level | ||||
| #[no_mangle] | ||||
| extern "C" fn vpn_log(level: i32, message: *const c_char) { | ||||
|   let message = unsafe { std::ffi::CStr::from_ptr(message) }; | ||||
|   let message = message.to_str().unwrap_or("Invalid log message"); | ||||
|   // Strip the trailing newline | ||||
|   let message = message.trim_end_matches('\n'); | ||||
|  | ||||
|   if level == 0 { | ||||
|     warn!("{}", message); | ||||
|   } else if level == 1 { | ||||
|     info!("{}", message); | ||||
|   } else if level == 2 { | ||||
|     debug!("{}", message); | ||||
|   } else if level == 3 { | ||||
|     trace!("{}", message); | ||||
|   } else { | ||||
|     warn!( | ||||
|       "Unknown log level: {}, enable DEBUG log level to see more details", | ||||
|       level | ||||
|     ); | ||||
|     debug!("{}", message); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										144
									
								
								crates/openconnect/src/ffi/vpn.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								crates/openconnect/src/ffi/vpn.c
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,144 @@ | ||||
| #include <stdio.h> | ||||
| #include <stdlib.h> | ||||
| #include <stdarg.h> | ||||
| #include <unistd.h> | ||||
| #include <sys/utsname.h> | ||||
| #include <openconnect.h> | ||||
|  | ||||
| #include "vpn.h" | ||||
|  | ||||
| void *g_user_data; | ||||
|  | ||||
| static int g_cmd_pipe_fd; | ||||
| static const char *g_vpnc_script; | ||||
| static vpn_connected_callback on_vpn_connected; | ||||
|  | ||||
| /* Validate the peer certificate */ | ||||
| static int validate_peer_cert(__attribute__((unused)) void *_vpninfo, const char *reason) | ||||
| { | ||||
|     INFO("Validating peer cert: %s", reason); | ||||
|     return 0; | ||||
| } | ||||
|  | ||||
| /* Print progress messages */ | ||||
| static void print_progress(__attribute__((unused)) void *_vpninfo, int level, const char *format, ...) | ||||
| { | ||||
|     va_list args; | ||||
|     va_start(args, format); | ||||
|     char *message = format_message(format, args); | ||||
|     va_end(args); | ||||
|  | ||||
|     if (message == NULL) | ||||
|     { | ||||
|         ERROR("Failed to format log message"); | ||||
|     } | ||||
|     else | ||||
|     { | ||||
|         LOG(level, message); | ||||
|         free(message); | ||||
|     } | ||||
| } | ||||
|  | ||||
| static void setup_tun_handler(void *_vpninfo) | ||||
| { | ||||
|     int ret = openconnect_setup_tun_device(_vpninfo, g_vpnc_script, NULL); | ||||
|     if (!ret) { | ||||
|         on_vpn_connected(g_cmd_pipe_fd, g_user_data); | ||||
|     } | ||||
| } | ||||
|  | ||||
| /* Initialize VPN connection */ | ||||
| int vpn_connect(const vpn_options *options, vpn_connected_callback callback) | ||||
| { | ||||
|     INFO("openconnect version: %s", openconnect_get_version()); | ||||
|     struct openconnect_info *vpninfo; | ||||
|     struct utsname utsbuf; | ||||
|  | ||||
|     g_user_data = options->user_data; | ||||
|     g_vpnc_script = options->script; | ||||
|     on_vpn_connected = callback; | ||||
|  | ||||
|     INFO("User agent: %s", options->user_agent); | ||||
|     INFO("VPNC script: %s", options->script); | ||||
|     INFO("OS: %s", options->os); | ||||
|  | ||||
|     vpninfo = openconnect_vpninfo_new(options->user_agent, validate_peer_cert, NULL, NULL, print_progress, NULL); | ||||
|  | ||||
|     if (!vpninfo) | ||||
|     { | ||||
|         ERROR("openconnect_vpninfo_new failed"); | ||||
|         return 1; | ||||
|     } | ||||
|  | ||||
|     openconnect_set_loglevel(vpninfo, PRG_TRACE); | ||||
|     openconnect_init_ssl(); | ||||
|     openconnect_set_protocol(vpninfo, "gp"); | ||||
|     openconnect_set_hostname(vpninfo, options->server); | ||||
|     openconnect_set_cookie(vpninfo, options->cookie); | ||||
|  | ||||
|     if (options->os) { | ||||
|         openconnect_set_reported_os(vpninfo, options->os); | ||||
|     } | ||||
|  | ||||
|     if (options->certificate) | ||||
|     { | ||||
|         INFO("Setting client certificate: %s", options->certificate); | ||||
|         openconnect_set_client_cert(vpninfo, options->certificate, NULL); | ||||
|     } | ||||
|  | ||||
|     if (options->servercert) { | ||||
|         INFO("Setting server certificate: %s", options->servercert); | ||||
|         openconnect_set_system_trust(vpninfo, 0); | ||||
|     } | ||||
|  | ||||
|     g_cmd_pipe_fd = openconnect_setup_cmd_pipe(vpninfo); | ||||
|     if (g_cmd_pipe_fd < 0) | ||||
|     { | ||||
|         ERROR("openconnect_setup_cmd_pipe failed"); | ||||
|         return 1; | ||||
|     } | ||||
|  | ||||
|     if (!uname(&utsbuf)) | ||||
|     { | ||||
|         openconnect_set_localname(vpninfo, utsbuf.nodename); | ||||
|     } | ||||
|  | ||||
|     // Essential step | ||||
|     if (openconnect_make_cstp_connection(vpninfo) != 0) | ||||
|     { | ||||
|         ERROR("openconnect_make_cstp_connection failed"); | ||||
|         return 1; | ||||
|     } | ||||
|  | ||||
|     if (openconnect_setup_dtls(vpninfo, 60) != 0) | ||||
|     { | ||||
|         openconnect_disable_dtls(vpninfo); | ||||
|     } | ||||
|  | ||||
|     // Essential step | ||||
|     openconnect_set_setup_tun_handler(vpninfo, setup_tun_handler); | ||||
|  | ||||
|     while (1) | ||||
|     { | ||||
|         int ret = openconnect_mainloop(vpninfo, 300, 10); | ||||
|  | ||||
|         if (ret) | ||||
|         { | ||||
|             INFO("openconnect_mainloop returned %d, exiting", ret); | ||||
|             openconnect_vpninfo_free(vpninfo); | ||||
|             return ret; | ||||
|         } | ||||
|  | ||||
|         INFO("openconnect_mainloop returned 0, reconnecting"); | ||||
|     } | ||||
| } | ||||
|  | ||||
| /* Stop the VPN connection */ | ||||
| void vpn_disconnect() | ||||
| { | ||||
|     char cmd = OC_CMD_CANCEL; | ||||
|     if (write(g_cmd_pipe_fd, &cmd, 1) < 0) | ||||
|     { | ||||
|         ERROR("Failed to write to command pipe, VPN connection may not be stopped"); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										68
									
								
								crates/openconnect/src/ffi/vpn.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								crates/openconnect/src/ffi/vpn.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | ||||
| #include <stdio.h> | ||||
| #include <stdlib.h> | ||||
| #include <stdarg.h> | ||||
| #include <openconnect.h> | ||||
|  | ||||
| typedef void (*vpn_connected_callback)(int cmd_pipe_fd, void *user_data); | ||||
|  | ||||
| typedef struct vpn_options | ||||
| { | ||||
|     void *user_data; | ||||
|     const char *server; | ||||
|     const char *cookie; | ||||
|     const char *user_agent; | ||||
|  | ||||
|     const char *script; | ||||
|     const char *os; | ||||
|     const char *certificate; | ||||
|     const char *servercert; | ||||
| } vpn_options; | ||||
|  | ||||
| int vpn_connect(const vpn_options *options, vpn_connected_callback callback); | ||||
| void vpn_disconnect(); | ||||
|  | ||||
| extern void vpn_log(int level, const char *msg); | ||||
|  | ||||
| static char *format_message(const char *format, va_list args) | ||||
| { | ||||
|     va_list args_copy; | ||||
|     va_copy(args_copy, args); | ||||
|     int len = vsnprintf(NULL, 0, format, args_copy); | ||||
|     va_end(args_copy); | ||||
|  | ||||
|     char *buffer = malloc(len + 1); | ||||
|     if (buffer == NULL) | ||||
|     { | ||||
|         return NULL; | ||||
|     } | ||||
|  | ||||
|     vsnprintf(buffer, len + 1, format, args); | ||||
|     return buffer; | ||||
| } | ||||
|  | ||||
| static void _log(int level, ...) | ||||
| { | ||||
|     va_list args; | ||||
|     va_start(args, level); | ||||
|  | ||||
|     char *format = va_arg(args, char *); | ||||
|     char *message = format_message(format, args); | ||||
|  | ||||
|     va_end(args); | ||||
|  | ||||
|     if (message == NULL) | ||||
|     { | ||||
|         vpn_log(PRG_ERR, "Failed to format log message"); | ||||
|     } | ||||
|     else | ||||
|     { | ||||
|         vpn_log(level, message); | ||||
|         free(message); | ||||
|     } | ||||
| } | ||||
|  | ||||
| #define LOG(level, ...) _log(level, __VA_ARGS__) | ||||
| #define ERROR(...) LOG(PRG_ERR, __VA_ARGS__) | ||||
| #define INFO(...) LOG(PRG_INFO, __VA_ARGS__) | ||||
| #define DEBUG(...) LOG(PRG_DEBUG, __VA_ARGS__) | ||||
| #define TRACE(...) LOG(PRG_TRACE, __VA_ARGS__) | ||||
							
								
								
									
										5
									
								
								crates/openconnect/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								crates/openconnect/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| mod ffi; | ||||
| mod vpn; | ||||
| mod vpnc_script; | ||||
|  | ||||
| pub use vpn::*; | ||||
							
								
								
									
										131
									
								
								crates/openconnect/src/vpn.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								crates/openconnect/src/vpn.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,131 @@ | ||||
| use std::{ | ||||
|   ffi::{c_char, CString}, | ||||
|   sync::{Arc, RwLock}, | ||||
| }; | ||||
|  | ||||
| use log::info; | ||||
|  | ||||
| use crate::{ffi, vpnc_script::find_default_vpnc_script}; | ||||
|  | ||||
| type OnConnectedCallback = Arc<RwLock<Option<Box<dyn FnOnce() + 'static + Send + Sync>>>>; | ||||
|  | ||||
| pub struct Vpn { | ||||
|   server: CString, | ||||
|   cookie: CString, | ||||
|   user_agent: CString, | ||||
|   script: CString, | ||||
|   os: CString, | ||||
|   certificate: Option<CString>, | ||||
|   servercert: Option<CString>, | ||||
|  | ||||
|   callback: OnConnectedCallback, | ||||
| } | ||||
|  | ||||
| impl Vpn { | ||||
|   pub fn builder(server: &str, cookie: &str) -> VpnBuilder { | ||||
|     VpnBuilder::new(server, cookie) | ||||
|   } | ||||
|  | ||||
|   pub fn connect(&self, on_connected: impl FnOnce() + 'static + Send + Sync) -> i32 { | ||||
|     self | ||||
|       .callback | ||||
|       .write() | ||||
|       .unwrap() | ||||
|       .replace(Box::new(on_connected)); | ||||
|     let options = self.build_connect_options(); | ||||
|  | ||||
|     ffi::connect(&options) | ||||
|   } | ||||
|  | ||||
|   pub(crate) fn on_connected(&self, pipe_fd: i32) { | ||||
|     info!("Connected to VPN, pipe_fd: {}", pipe_fd); | ||||
|  | ||||
|     if let Some(callback) = self.callback.write().unwrap().take() { | ||||
|       callback(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   pub fn disconnect(&self) { | ||||
|     ffi::disconnect(); | ||||
|   } | ||||
|  | ||||
|   fn build_connect_options(&self) -> ffi::ConnectOptions { | ||||
|     ffi::ConnectOptions { | ||||
|       user_data: self as *const _ as *mut _, | ||||
|  | ||||
|       server: self.server.as_ptr(), | ||||
|       cookie: self.cookie.as_ptr(), | ||||
|       user_agent: self.user_agent.as_ptr(), | ||||
|       script: self.script.as_ptr(), | ||||
|       os: self.os.as_ptr(), | ||||
|       certificate: Self::option_to_ptr(&self.certificate), | ||||
|       servercert: Self::option_to_ptr(&self.servercert), | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   fn option_to_ptr(option: &Option<CString>) -> *const c_char { | ||||
|     match option { | ||||
|       Some(value) => value.as_ptr(), | ||||
|       None => std::ptr::null(), | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| pub struct VpnBuilder { | ||||
|   server: String, | ||||
|   cookie: String, | ||||
|   user_agent: Option<String>, | ||||
|   script: Option<String>, | ||||
|   os: Option<String>, | ||||
| } | ||||
|  | ||||
| impl VpnBuilder { | ||||
|   fn new(server: &str, cookie: &str) -> Self { | ||||
|     Self { | ||||
|       server: server.to_string(), | ||||
|       cookie: cookie.to_string(), | ||||
|       user_agent: None, | ||||
|       script: None, | ||||
|       os: None, | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   pub fn user_agent<T: Into<Option<String>>>(mut self, user_agent: T) -> Self { | ||||
|     self.user_agent = user_agent.into(); | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn script<T: Into<Option<String>>>(mut self, script: T) -> Self { | ||||
|     self.script = script.into(); | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn os<T: Into<Option<String>>>(mut self, os: T) -> Self { | ||||
|     self.os = os.into(); | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn build(self) -> Vpn { | ||||
|     let user_agent = self.user_agent.unwrap_or_default(); | ||||
|     let script = self | ||||
|       .script | ||||
|       .or_else(find_default_vpnc_script) | ||||
|       .unwrap_or_default(); | ||||
|     let os = self.os.unwrap_or("linux".to_string()); | ||||
|  | ||||
|     Vpn { | ||||
|       server: Self::to_cstring(&self.server), | ||||
|       cookie: Self::to_cstring(&self.cookie), | ||||
|       user_agent: Self::to_cstring(&user_agent), | ||||
|       script: Self::to_cstring(&script), | ||||
|       os: Self::to_cstring(&os), | ||||
|       certificate: None, | ||||
|       servercert: None, | ||||
|       callback: Default::default(), | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   fn to_cstring(value: &str) -> CString { | ||||
|     CString::new(value.to_string()).expect("Failed to convert to CString") | ||||
|   } | ||||
| } | ||||
							
								
								
									
										23
									
								
								crates/openconnect/src/vpnc_script.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								crates/openconnect/src/vpnc_script.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| use is_executable::IsExecutable; | ||||
| use std::path::Path; | ||||
|  | ||||
| const VPNC_SCRIPT_LOCATIONS: [&str; 5] = [ | ||||
|   "/usr/local/share/vpnc-scripts/vpnc-script", | ||||
|   "/usr/local/sbin/vpnc-script", | ||||
|   "/usr/share/vpnc-scripts/vpnc-script", | ||||
|   "/usr/sbin/vpnc-script", | ||||
|   "/etc/vpnc/vpnc-script", | ||||
| ]; | ||||
|  | ||||
| pub(crate) fn find_default_vpnc_script() -> Option<String> { | ||||
|   for location in VPNC_SCRIPT_LOCATIONS.iter() { | ||||
|     let path = Path::new(location); | ||||
|     if path.is_executable() { | ||||
|       return Some(location.to_string()); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   log::warn!("vpnc-script not found"); | ||||
|  | ||||
|   None | ||||
| } | ||||
		Reference in New Issue
	
	Block a user