Compare commits
	
		
			14 Commits
		
	
	
		
			v2.0.0-bet
			...
			5767c252b7
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 5767c252b7 | ||
|  | a2efcada02 | ||
|  | e68aa0ffa6 | ||
|  | 66bcccabe4 | ||
|  | 3736189308 | ||
|  | c408482c55 | ||
|  | 00b0b8eb84 | ||
|  | b14294f131 | ||
|  | db9249bd61 | ||
|  | 662e4d0b8a | ||
|  | 13be9179f5 | ||
|  | 0a55506077 | ||
|  | 8860efa82e | ||
|  | 9bc0994a8e | 
							
								
								
									
										30
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,30 @@ | |||||||
|  | --- | ||||||
|  | name: Bug report | ||||||
|  | about: Create a report to help us improve | ||||||
|  | title: '' | ||||||
|  | labels: '' | ||||||
|  | assignees: '' | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | **Describe the bug** | ||||||
|  | A clear and concise description of what the bug is. | ||||||
|  |  | ||||||
|  | **Expected behavior** | ||||||
|  | A clear and concise description of what you expected to happen. | ||||||
|  |  | ||||||
|  | **Screenshots** | ||||||
|  | If applicable, add screenshots to help explain your problem. | ||||||
|  |  | ||||||
|  | **Logs** | ||||||
|  | - For the GUI version, you can find the logs at `~/.local/share/gpclient/gpclient.log` | ||||||
|  | - For the CLI version, copy the output of the `gpclient` command. | ||||||
|  |  | ||||||
|  | **Environment:** | ||||||
|  |  - OS: [e.g. Ubuntu 22.04] | ||||||
|  |  - Desktop Environment: [e.g. GNOME or KDE] | ||||||
|  |  - Output of `ps aux | grep 'gnome-keyring\|kwalletd5' | grep -v grep`: [Required for secure store error] | ||||||
|  |  - Is remote SSH? [Yes/No] | ||||||
|  |  | ||||||
|  | **Additional context** | ||||||
|  | Add any other context about the problem here. | ||||||
							
								
								
									
										1
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -4,6 +4,7 @@ | |||||||
|         "bincode", |         "bincode", | ||||||
|         "chacha", |         "chacha", | ||||||
|         "clientos", |         "clientos", | ||||||
|  |         "cstring", | ||||||
|         "datetime", |         "datetime", | ||||||
|         "disconnectable", |         "disconnectable", | ||||||
|         "distro", |         "distro", | ||||||
|   | |||||||
							
								
								
									
										18
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						| @@ -1423,7 +1423,7 @@ dependencies = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "gpapi" | name = "gpapi" | ||||||
| version = "2.0.0-beta8" | version = "2.0.0" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "anyhow", |  "anyhow", | ||||||
|  "base64 0.21.5", |  "base64 0.21.5", | ||||||
| @@ -1431,6 +1431,7 @@ dependencies = [ | |||||||
|  "clap", |  "clap", | ||||||
|  "dotenvy_macro", |  "dotenvy_macro", | ||||||
|  "log", |  "log", | ||||||
|  |  "md5", | ||||||
|  "open", |  "open", | ||||||
|  "redact-engine", |  "redact-engine", | ||||||
|  "regex", |  "regex", | ||||||
| @@ -1438,6 +1439,7 @@ dependencies = [ | |||||||
|  "roxmltree", |  "roxmltree", | ||||||
|  "serde", |  "serde", | ||||||
|  "serde_json", |  "serde_json", | ||||||
|  |  "serde_urlencoded", | ||||||
|  "specta", |  "specta", | ||||||
|  "specta-macros", |  "specta-macros", | ||||||
|  "tauri", |  "tauri", | ||||||
| @@ -1452,7 +1454,7 @@ dependencies = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "gpauth" | name = "gpauth" | ||||||
| version = "2.0.0-beta8" | version = "2.0.0" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "anyhow", |  "anyhow", | ||||||
|  "clap", |  "clap", | ||||||
| @@ -1472,7 +1474,7 @@ dependencies = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "gpclient" | name = "gpclient" | ||||||
| version = "2.0.0-beta8" | version = "2.0.0" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "anyhow", |  "anyhow", | ||||||
|  "clap", |  "clap", | ||||||
| @@ -1493,7 +1495,7 @@ dependencies = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "gpservice" | name = "gpservice" | ||||||
| version = "2.0.0-beta8" | version = "2.0.0" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "anyhow", |  "anyhow", | ||||||
|  "axum", |  "axum", | ||||||
| @@ -2226,6 +2228,12 @@ version = "0.7.3" | |||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" | checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "md5" | ||||||
|  | version = "0.7.0" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "memchr" | name = "memchr" | ||||||
| version = "2.7.1" | version = "2.7.1" | ||||||
| @@ -2478,7 +2486,7 @@ dependencies = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "openconnect" | name = "openconnect" | ||||||
| version = "2.0.0-beta8" | version = "2.0.0" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "cc", |  "cc", | ||||||
|  "is_executable", |  "is_executable", | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ resolver = "2" | |||||||
| members = ["crates/*", "apps/gpclient", "apps/gpservice", "apps/gpauth"] | members = ["crates/*", "apps/gpclient", "apps/gpservice", "apps/gpauth"] | ||||||
|  |  | ||||||
| [workspace.package] | [workspace.package] | ||||||
| version = "2.0.0-beta8" | version = "2.0.0" | ||||||
| authors = ["Kevin Yue <k3vinyue@gmail.com>"] | authors = ["Kevin Yue <k3vinyue@gmail.com>"] | ||||||
| homepage = "https://github.com/yuezk/GlobalProtect-openconnect" | homepage = "https://github.com/yuezk/GlobalProtect-openconnect" | ||||||
| edition = "2021" | edition = "2021" | ||||||
| @@ -43,6 +43,8 @@ thiserror = "1" | |||||||
| redact-engine = "0.1" | redact-engine = "0.1" | ||||||
| dotenvy_macro = "0.15" | dotenvy_macro = "0.15" | ||||||
| compile-time = "0.2" | compile-time = "0.2" | ||||||
|  | serde_urlencoded = "0.7" | ||||||
|  | md5="0.7" | ||||||
|  |  | ||||||
| [profile.release] | [profile.release] | ||||||
| opt-level = 'z'   # Optimize for size | opt-level = 'z'   # Optimize for size | ||||||
|   | |||||||
							
								
								
									
										22
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -59,7 +59,7 @@ The GUI version is also available after you installed it. You can launch it from | |||||||
|  |  | ||||||
| > [!Warning] | > [!Warning] | ||||||
| > | > | ||||||
| > The client requires `openconnect >= 8.20`, please make sure you have it installed, you can check it with `openconnect --version`. | > The client requires `openconnect >= 8.20, pkexec, and gnome-keyring`, please make sure you have them installed. | ||||||
| > Installing the client from PPA will automatically install the required version of `openconnect`. | > Installing the client from PPA will automatically install the required version of `openconnect`. | ||||||
|  |  | ||||||
| ### Debian/Ubuntu based distributions | ### Debian/Ubuntu based distributions | ||||||
| @@ -67,6 +67,7 @@ The GUI version is also available after you installed it. You can launch it from | |||||||
| #### Install from PPA | #### Install from PPA | ||||||
|  |  | ||||||
| ``` | ``` | ||||||
|  | sudo apt-get install gir1.2-gtk-3.0 gir1.2-webkit2-4.0 | ||||||
| sudo add-apt-repository ppa:yuezk/globalprotect-openconnect | sudo add-apt-repository ppa:yuezk/globalprotect-openconnect | ||||||
| sudo apt-get update | sudo apt-get update | ||||||
| sudo apt-get install globalprotect-openconnect | sudo apt-get install globalprotect-openconnect | ||||||
| @@ -113,7 +114,7 @@ sudo dnf copr enable yuezk/globalprotect-openconnect | |||||||
| sudo dnf install globalprotect-openconnect | sudo dnf install globalprotect-openconnect | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| #### Install from OBS | #### Install from OBS (OpenSUSE Build Service) | ||||||
|  |  | ||||||
| The package is also available on [OBS](https://build.opensuse.org/package/show/home:yuezk/globalprotect-openconnect) for various RPM-based distributions. You can follow the instructions [on this page](https://software.opensuse.org//download.html?project=home%3Ayuezk&package=globalprotect-openconnect) to install it. | The package is also available on [OBS](https://build.opensuse.org/package/show/home:yuezk/globalprotect-openconnect) for various RPM-based distributions. You can follow the instructions [on this page](https://software.opensuse.org//download.html?project=home%3Ayuezk&package=globalprotect-openconnect) to install it. | ||||||
|  |  | ||||||
| @@ -123,11 +124,24 @@ Download the latest RPM package from [releases](https://github.com/yuezk/GlobalP | |||||||
|  |  | ||||||
| ### Other distributions | ### Other distributions | ||||||
|  |  | ||||||
| The project depends on `openconnect >= 8.20`, `webkit2gtk`, `libsecret`, `libayatana-appindicator` or `libappindicator-gtk3`. You can install them first and then download the latest binary release (i.e., `*.bin.tar.gz`) from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. | - Install `openconnect >= 8.20`, `webkit2gtk`, `libsecret`, `libayatana-appindicator` or `libappindicator-gtk3`. | ||||||
|  | - Download `globalprotect-openconnect.tar.gz` from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. | ||||||
|  | - Extract the tarball and run `make build` to build the client. | ||||||
|  | - Run `make install` to install the client. | ||||||
|  |  | ||||||
|  | ## FAQ | ||||||
|  |  | ||||||
|  | 1. How to deal with error `Secure Storage not ready` | ||||||
|  |    | ||||||
|  |    You need to install the `gnome-keyring` package, and restart the system (See [#321](https://github.com/yuezk/GlobalProtect-openconnect/issues/321), [#316](https://github.com/yuezk/GlobalProtect-openconnect/issues/316)). | ||||||
|  |  | ||||||
|  | 2. How to deal with error `(gpauth:18869): Gtk-WARNING **: 10:33:37.566: cannot open display:` | ||||||
|  |    | ||||||
|  |    If you encounter this error when using the CLI version, try to run the command with `sudo -E` (See [#316](https://github.com/yuezk/GlobalProtect-openconnect/issues/316)). | ||||||
|  |  | ||||||
| ## About Trial | ## About Trial | ||||||
|  |  | ||||||
| The CLI version is always free, while the GUI version is paid. There two trial modes for the GUI version: | The CLI version is always free, while the GUI version is paid. There are two trial modes for the GUI version: | ||||||
|  |  | ||||||
| 1. 10-day trial: You can use the GUI stable release for 10 days after the installation. | 1. 10-day trial: You can use the GUI stable release for 10 days after the installation. | ||||||
| 2. 14-day trial: Each beta release has a fresh trial period (at most 14 days) after released. | 2. 14-day trial: Each beta release has a fresh trial period (at most 14 days) after released. | ||||||
|   | |||||||
| Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 13 KiB | 
| Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 28 KiB | 
| Before Width: | Height: | Size: 974 B After Width: | Height: | Size: 2.5 KiB | 
| Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 44 KiB | 
| Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 83 KiB | 
| @@ -413,7 +413,19 @@ fn read_auth_data(main_resource: &WebResource, auth_result_tx: mpsc::UnboundedSe | |||||||
|     } |     } | ||||||
|     Err(AuthDataError::NotFound) => { |     Err(AuthDataError::NotFound) => { | ||||||
|       info!("No auth data found in headers, trying to read from body..."); |       info!("No auth data found in headers, trying to read from body..."); | ||||||
|  |       let url = main_resource.uri().unwrap_or("".into()); | ||||||
|  |       let is_acs_endpoint = url.contains("/SAML20/SP/ACS"); | ||||||
|  |  | ||||||
|       read_auth_data_from_body(main_resource, move |auth_result| { |       read_auth_data_from_body(main_resource, move |auth_result| { | ||||||
|  |         // If the endpoint is `/SAML20/SP/ACS` and no auth data found in body, it should be considered as invalid | ||||||
|  |         let auth_result = auth_result.map_err(|err| { | ||||||
|  |           if matches!(err, AuthDataError::NotFound) && is_acs_endpoint { | ||||||
|  |             AuthDataError::Invalid | ||||||
|  |           } else { | ||||||
|  |             err | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |  | ||||||
|         send_auth_result(&auth_result_tx, auth_result) |         send_auth_result(&auth_result_tx, auth_result) | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -7,7 +7,10 @@ use gpapi::{ | |||||||
|   gateway::gateway_login, |   gateway::gateway_login, | ||||||
|   gp_params::{ClientOs, GpParams}, |   gp_params::{ClientOs, GpParams}, | ||||||
|   portal::{prelogin, retrieve_config, PortalError, Prelogin}, |   portal::{prelogin, retrieve_config, PortalError, Prelogin}, | ||||||
|   process::auth_launcher::SamlAuthLauncher, |   process::{ | ||||||
|  |     auth_launcher::SamlAuthLauncher, | ||||||
|  |     users::{get_non_root_user, get_user_by_name}, | ||||||
|  |   }, | ||||||
|   utils::shutdown_signal, |   utils::shutdown_signal, | ||||||
|   GP_USER_AGENT, |   GP_USER_AGENT, | ||||||
| }; | }; | ||||||
| @@ -27,6 +30,16 @@ pub(crate) struct ConnectArgs { | |||||||
|   user: Option<String>, |   user: Option<String>, | ||||||
|   #[arg(long, short, help = "The VPNC script to use")] |   #[arg(long, short, help = "The VPNC script to use")] | ||||||
|   script: Option<String>, |   script: Option<String>, | ||||||
|  |  | ||||||
|  |   #[arg(long, help = "Same as the '--csd-user' option in the openconnect command")] | ||||||
|  |   csd_user: Option<String>, | ||||||
|  |  | ||||||
|  |   #[arg(long, help = "Same as the '--csd-wrapper' option in the openconnect command")] | ||||||
|  |   csd_wrapper: Option<String>, | ||||||
|  |  | ||||||
|  |   #[arg(short, long, help = "Request MTU from server (legacy servers only)")] | ||||||
|  |   mtu: Option<u32>, | ||||||
|  |  | ||||||
|   #[arg(long, default_value = GP_USER_AGENT, help = "The user agent to use")] |   #[arg(long, default_value = GP_USER_AGENT, help = "The user agent to use")] | ||||||
|   user_agent: String, |   user_agent: String, | ||||||
|   #[arg(long, default_value = "Linux")] |   #[arg(long, default_value = "Linux")] | ||||||
| @@ -133,7 +146,7 @@ impl<'a> ConnectHandler<'a> { | |||||||
|     gp_params.set_is_gateway(true); |     gp_params.set_is_gateway(true); | ||||||
|  |  | ||||||
|     let prelogin = prelogin(gateway, &gp_params).await?; |     let prelogin = prelogin(gateway, &gp_params).await?; | ||||||
|     let cred = self.obtain_credential(&prelogin, &gateway).await?; |     let cred = self.obtain_credential(&prelogin, gateway).await?; | ||||||
|  |  | ||||||
|     let cookie = gateway_login(gateway, &cred, &gp_params).await?; |     let cookie = gateway_login(gateway, &cred, &gp_params).await?; | ||||||
|  |  | ||||||
| @@ -141,9 +154,15 @@ impl<'a> ConnectHandler<'a> { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   async fn connect_gateway(&self, gateway: &str, cookie: &str) -> anyhow::Result<()> { |   async fn connect_gateway(&self, gateway: &str, cookie: &str) -> anyhow::Result<()> { | ||||||
|  |     let csd_uid = get_csd_uid(&self.args.csd_user)?; | ||||||
|  |     let mtu = self.args.mtu.unwrap_or(0); | ||||||
|  |  | ||||||
|     let vpn = Vpn::builder(gateway, cookie) |     let vpn = Vpn::builder(gateway, cookie) | ||||||
|       .user_agent(self.args.user_agent.clone()) |       .user_agent(self.args.user_agent.clone()) | ||||||
|       .script(self.args.script.clone()) |       .script(self.args.script.clone()) | ||||||
|  |       .csd_uid(csd_uid) | ||||||
|  |       .csd_wrapper(self.args.csd_wrapper.clone()) | ||||||
|  |       .mtu(mtu) | ||||||
|       .build(); |       .build(); | ||||||
|  |  | ||||||
|     let vpn = Arc::new(vpn); |     let vpn = Arc::new(vpn); | ||||||
| @@ -211,3 +230,11 @@ fn write_pid_file() { | |||||||
|   fs::write(GP_CLIENT_LOCK_FILE, pid.to_string()).unwrap(); |   fs::write(GP_CLIENT_LOCK_FILE, pid.to_string()).unwrap(); | ||||||
|   info!("Wrote PID {} to {}", pid, GP_CLIENT_LOCK_FILE); |   info!("Wrote PID {} to {}", pid, GP_CLIENT_LOCK_FILE); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | fn get_csd_uid(csd_user: &Option<String>) -> anyhow::Result<u32> { | ||||||
|  |   if let Some(csd_user) = csd_user { | ||||||
|  |     get_user_by_name(csd_user).map(|user| user.uid()) | ||||||
|  |   } else { | ||||||
|  |     get_non_root_user().map_or_else(|_| Ok(0), |user| Ok(user.uid())) | ||||||
|  |   } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -32,11 +32,14 @@ impl VpnTaskContext { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     let info = req.info().clone(); |     let info = req.info().clone(); | ||||||
|     let vpn_handle = self.vpn_handle.clone(); |     let vpn_handle = Arc::clone(&self.vpn_handle); | ||||||
|     let args = req.args(); |     let args = req.args(); | ||||||
|     let vpn = Vpn::builder(req.gateway().server(), args.cookie()) |     let vpn = Vpn::builder(req.gateway().server(), args.cookie()) | ||||||
|       .user_agent(args.user_agent()) |       .user_agent(args.user_agent()) | ||||||
|       .script(args.vpnc_script()) |       .script(args.vpnc_script()) | ||||||
|  |       .csd_uid(args.csd_uid()) | ||||||
|  |       .csd_wrapper(args.csd_wrapper()) | ||||||
|  |       .mtu(args.mtu()) | ||||||
|       .os(args.openconnect_os()) |       .os(args.openconnect_os()) | ||||||
|       .build(); |       .build(); | ||||||
|  |  | ||||||
| @@ -73,7 +76,9 @@ impl VpnTaskContext { | |||||||
|  |  | ||||||
|   pub async fn disconnect(&self) { |   pub async fn disconnect(&self) { | ||||||
|     if let Some(disconnect_rx) = self.disconnect_rx.write().await.take() { |     if let Some(disconnect_rx) = self.disconnect_rx.write().await.take() { | ||||||
|  |       info!("Disconnecting VPN..."); | ||||||
|       if let Some(vpn) = self.vpn_handle.read().await.as_ref() { |       if let Some(vpn) = self.vpn_handle.read().await.as_ref() { | ||||||
|  |         info!("VPN is connected, start disconnecting..."); | ||||||
|         self.vpn_state_tx.send(VpnState::Disconnecting).ok(); |         self.vpn_state_tx.send(VpnState::Disconnecting).ok(); | ||||||
|         vpn.disconnect() |         vpn.disconnect() | ||||||
|       } |       } | ||||||
|   | |||||||
| @@ -25,6 +25,8 @@ url.workspace = true | |||||||
| regex.workspace = true | regex.workspace = true | ||||||
| dotenvy_macro.workspace = true | dotenvy_macro.workspace = true | ||||||
| uzers.workspace = true | uzers.workspace = true | ||||||
|  | serde_urlencoded.workspace = true | ||||||
|  | md5.workspace = true | ||||||
|  |  | ||||||
| tauri = { workspace = true, optional = true } | tauri = { workspace = true, optional = true } | ||||||
| clap = { workspace = true, optional = true } | clap = { workspace = true, optional = true } | ||||||
|   | |||||||
							
								
								
									
										178
									
								
								crates/gpapi/src/gateway/hip.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,178 @@ | |||||||
|  | use std::collections::HashMap; | ||||||
|  |  | ||||||
|  | use log::{info, warn}; | ||||||
|  | use reqwest::Client; | ||||||
|  | use roxmltree::Document; | ||||||
|  |  | ||||||
|  | use crate::{gp_params::GpParams, process::hip_launcher::HipLauncher, utils::normalize_server}; | ||||||
|  |  | ||||||
|  | struct HipReporter<'a> { | ||||||
|  |   server: String, | ||||||
|  |   cookie: &'a str, | ||||||
|  |   md5: &'a str, | ||||||
|  |   csd_wrapper: &'a str, | ||||||
|  |   gp_params: &'a GpParams, | ||||||
|  |   client: Client, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl HipReporter<'_> { | ||||||
|  |   async fn report(&self) -> anyhow::Result<()> { | ||||||
|  |     let client_ip = self.retrieve_client_ip().await?; | ||||||
|  |  | ||||||
|  |     let hip_needed = match self.check_hip(&client_ip).await { | ||||||
|  |       Ok(hip_needed) => hip_needed, | ||||||
|  |       Err(err) => { | ||||||
|  |         warn!("Failed to check HIP: {}", err); | ||||||
|  |         return Ok(()); | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     if !hip_needed { | ||||||
|  |       info!("HIP report not needed"); | ||||||
|  |       return Ok(()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     info!("HIP report needed, generating report..."); | ||||||
|  |     let report = self.generate_report(&client_ip).await?; | ||||||
|  |  | ||||||
|  |     if let Err(err) = self.submit_hip(&client_ip, &report).await { | ||||||
|  |       warn!("Failed to submit HIP report: {}", err); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     Ok(()) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async fn retrieve_client_ip(&self) -> anyhow::Result<String> { | ||||||
|  |     let config_url = format!("{}/ssl-vpn/getconfig.esp", self.server); | ||||||
|  |     let mut params: HashMap<&str, &str> = HashMap::new(); | ||||||
|  |  | ||||||
|  |     params.insert("client-type", "1"); | ||||||
|  |     params.insert("protocol-version", "p1"); | ||||||
|  |     params.insert("internal", "no"); | ||||||
|  |     params.insert("ipv6-support", "yes"); | ||||||
|  |     params.insert("clientos", self.gp_params.client_os()); | ||||||
|  |     params.insert("hmac-algo", "sha1,md5,sha256"); | ||||||
|  |     params.insert("enc-algo", "aes-128-cbc,aes-256-cbc"); | ||||||
|  |  | ||||||
|  |     if let Some(os_version) = self.gp_params.os_version() { | ||||||
|  |       params.insert("os-version", os_version); | ||||||
|  |     } | ||||||
|  |     if let Some(client_version) = self.gp_params.client_version() { | ||||||
|  |       params.insert("app-version", client_version); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let params = merge_cookie_params(self.cookie, ¶ms)?; | ||||||
|  |  | ||||||
|  |     let res = self.client.post(&config_url).form(¶ms).send().await?; | ||||||
|  |     let res_xml = res.error_for_status()?.text().await?; | ||||||
|  |     let doc = Document::parse(&res_xml)?; | ||||||
|  |  | ||||||
|  |     // Get <ip-address> | ||||||
|  |     let ip = doc | ||||||
|  |       .descendants() | ||||||
|  |       .find(|n| n.has_tag_name("ip-address")) | ||||||
|  |       .and_then(|n| n.text()) | ||||||
|  |       .ok_or_else(|| anyhow::anyhow!("ip-address not found"))?; | ||||||
|  |  | ||||||
|  |     Ok(ip.to_string()) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async fn check_hip(&self, client_ip: &str) -> anyhow::Result<bool> { | ||||||
|  |     let url = format!("{}/ssl-vpn/hipreportcheck.esp", self.server); | ||||||
|  |     let mut params = HashMap::new(); | ||||||
|  |  | ||||||
|  |     params.insert("client-role", "global-protect-full"); | ||||||
|  |     params.insert("client-ip", client_ip); | ||||||
|  |     params.insert("md5", self.md5); | ||||||
|  |  | ||||||
|  |     let params = merge_cookie_params(self.cookie, ¶ms)?; | ||||||
|  |     let res = self.client.post(&url).form(¶ms).send().await?; | ||||||
|  |     let res_xml = res.error_for_status()?.text().await?; | ||||||
|  |  | ||||||
|  |     is_hip_needed(&res_xml) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async fn generate_report(&self, client_ip: &str) -> anyhow::Result<String> { | ||||||
|  |     let launcher = HipLauncher::new(self.csd_wrapper) | ||||||
|  |       .cookie(self.cookie) | ||||||
|  |       .md5(self.md5) | ||||||
|  |       .client_ip(client_ip) | ||||||
|  |       .client_os(self.gp_params.client_os()) | ||||||
|  |       .client_version(self.gp_params.client_version()); | ||||||
|  |  | ||||||
|  |     launcher.launch().await | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async fn submit_hip(&self, client_ip: &str, report: &str) -> anyhow::Result<()> { | ||||||
|  |     let url = format!("{}/ssl-vpn/hipreport.esp", self.server); | ||||||
|  |  | ||||||
|  |     let mut params = HashMap::new(); | ||||||
|  |     params.insert("client-role", "global-protect-full"); | ||||||
|  |     params.insert("client-ip", client_ip); | ||||||
|  |     params.insert("report", report); | ||||||
|  |  | ||||||
|  |     let params = merge_cookie_params(self.cookie, ¶ms)?; | ||||||
|  |     let res = self.client.post(&url).form(¶ms).send().await?; | ||||||
|  |     let res_xml = res.error_for_status()?.text().await?; | ||||||
|  |  | ||||||
|  |     info!("HIP check response: {}", res_xml); | ||||||
|  |  | ||||||
|  |     Ok(()) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn is_hip_needed(res_xml: &str) -> anyhow::Result<bool> { | ||||||
|  |   let doc = Document::parse(res_xml)?; | ||||||
|  |  | ||||||
|  |   let hip_needed = doc | ||||||
|  |     .descendants() | ||||||
|  |     .find(|n| n.has_tag_name("hip-report-needed")) | ||||||
|  |     .and_then(|n| n.text()) | ||||||
|  |     .ok_or_else(|| anyhow::anyhow!("hip-report-needed not found"))?; | ||||||
|  |  | ||||||
|  |   Ok(hip_needed == "yes") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn merge_cookie_params(cookie: &str, params: &HashMap<&str, &str>) -> anyhow::Result<HashMap<String, String>> { | ||||||
|  |   let cookie_params = serde_urlencoded::from_str::<HashMap<String, String>>(cookie)?; | ||||||
|  |   let params = params | ||||||
|  |     .iter() | ||||||
|  |     .map(|(k, v)| (k.to_string(), v.to_string())) | ||||||
|  |     .chain(cookie_params) | ||||||
|  |     .collect::<HashMap<String, String>>(); | ||||||
|  |  | ||||||
|  |   Ok(params) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Compute md5 for fields except authcookie,preferred-ip,preferred-ipv6 | ||||||
|  | fn build_csd_token(cookie: &str) -> anyhow::Result<String> { | ||||||
|  |   let mut cookie_params = serde_urlencoded::from_str::<Vec<(String, String)>>(cookie)?; | ||||||
|  |   cookie_params.retain(|(k, _)| k != "authcookie" && k != "preferred-ip" && k != "preferred-ipv6"); | ||||||
|  |  | ||||||
|  |   let token = serde_urlencoded::to_string(cookie_params)?; | ||||||
|  |   let md5 = format!("{:x}", md5::compute(token)); | ||||||
|  |  | ||||||
|  |   Ok(md5) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub async fn hip_report(gateway: &str, cookie: &str, csd_wrapper: &str, gp_params: &GpParams) -> anyhow::Result<()> { | ||||||
|  |   let client = Client::builder() | ||||||
|  |     .danger_accept_invalid_certs(gp_params.ignore_tls_errors()) | ||||||
|  |     .user_agent(gp_params.user_agent()) | ||||||
|  |     .build()?; | ||||||
|  |  | ||||||
|  |   let md5 = build_csd_token(cookie)?; | ||||||
|  |  | ||||||
|  |   info!("Submit HIP report md5: {}", md5); | ||||||
|  |  | ||||||
|  |   let reporter = HipReporter { | ||||||
|  |     server: normalize_server(gateway)?, | ||||||
|  |     cookie, | ||||||
|  |     md5: &md5, | ||||||
|  |     csd_wrapper, | ||||||
|  |     gp_params, | ||||||
|  |     client, | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   reporter.report().await | ||||||
|  | } | ||||||
| @@ -1,5 +1,6 @@ | |||||||
| mod login; | mod login; | ||||||
| mod parse_gateways; | mod parse_gateways; | ||||||
|  | pub mod hip; | ||||||
|  |  | ||||||
| pub use login::*; | pub use login::*; | ||||||
| pub(crate) use parse_gateways::*; | pub(crate) use parse_gateways::*; | ||||||
|   | |||||||
| @@ -83,6 +83,18 @@ impl GpParams { | |||||||
|     self.prefer_default_browser |     self.prefer_default_browser | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   pub fn client_os(&self) -> &str { | ||||||
|  |     self.client_os.as_str() | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn os_version(&self) -> Option<&str> { | ||||||
|  |     self.os_version.as_deref() | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn client_version(&self) -> Option<&str> { | ||||||
|  |     self.client_version.as_deref() | ||||||
|  |   } | ||||||
|  |  | ||||||
|   pub(crate) fn to_params(&self) -> HashMap<&str, &str> { |   pub(crate) fn to_params(&self) -> HashMap<&str, &str> { | ||||||
|     let mut params: HashMap<&str, &str> = HashMap::new(); |     let mut params: HashMap<&str, &str> = HashMap::new(); | ||||||
|     let client_os = self.client_os.as_str(); |     let client_os = self.client_os.as_str(); | ||||||
|   | |||||||
| @@ -150,8 +150,10 @@ fn parse_res_xml(res_xml: String, is_gateway: bool) -> anyhow::Result<Prelogin> | |||||||
|     bail!("Prelogin failed: {}", msg) |     bail!("Prelogin failed: {}", msg) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   let region = xml::get_child_text(&doc, "region") |   let region = xml::get_child_text(&doc, "region").unwrap_or_else(|| { | ||||||
|     .ok_or_else(|| anyhow::anyhow!("Prelogin response does not contain region element"))?; |     info!("Prelogin response does not contain region element"); | ||||||
|  |     String::from("Unknown") | ||||||
|  |   }); | ||||||
|  |  | ||||||
|   let saml_method = xml::get_child_text(&doc, "saml-auth-method"); |   let saml_method = xml::get_child_text(&doc, "saml-auth-method"); | ||||||
|   let saml_request = xml::get_child_text(&doc, "saml-request"); |   let saml_request = xml::get_child_text(&doc, "saml-request"); | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| use std::process::Stdio; | use std::process::Stdio; | ||||||
|  |  | ||||||
|  | use anyhow::bail; | ||||||
| use tokio::process::Command; | use tokio::process::Command; | ||||||
|  |  | ||||||
| use crate::{auth::SamlAuthResult, credential::Credential, GP_AUTH_BINARY}; | use crate::{auth::SamlAuthResult, credential::Credential, GP_AUTH_BINARY}; | ||||||
| @@ -129,12 +130,13 @@ impl<'a> SamlAuthLauncher<'a> { | |||||||
|       .wait_with_output() |       .wait_with_output() | ||||||
|       .await?; |       .await?; | ||||||
|  |  | ||||||
|     let auth_result = serde_json::from_slice::<SamlAuthResult>(&output.stdout) |     let Ok(auth_result) = serde_json::from_slice::<SamlAuthResult>(&output.stdout) else { | ||||||
|       .map_err(|_| anyhow::anyhow!("Failed to parse auth data"))?; |       bail!("Failed to parse auth data") | ||||||
|  |     }; | ||||||
|  |  | ||||||
|     match auth_result { |     match auth_result { | ||||||
|       SamlAuthResult::Success(auth_data) => Credential::try_from(auth_data), |       SamlAuthResult::Success(auth_data) => Credential::try_from(auth_data), | ||||||
|       SamlAuthResult::Failure(msg) => Err(anyhow::anyhow!(msg)), |       SamlAuthResult::Failure(msg) => bail!(msg), | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,7 +1,8 @@ | |||||||
| use anyhow::bail; | use std::ffi::OsStr; | ||||||
| use std::{env, ffi::OsStr}; |  | ||||||
| use tokio::process::Command; | use tokio::process::Command; | ||||||
| use uzers::{os::unix::UserExt, User}; | use uzers::os::unix::UserExt; | ||||||
|  |  | ||||||
|  | use super::users::get_non_root_user; | ||||||
|  |  | ||||||
| pub trait CommandExt { | pub trait CommandExt { | ||||||
|   fn new_pkexec<S: AsRef<OsStr>>(program: S) -> Command; |   fn new_pkexec<S: AsRef<OsStr>>(program: S) -> Command; | ||||||
| @@ -34,29 +35,3 @@ impl CommandExt for Command { | |||||||
|     Ok(self) |     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 { |  | ||||||
|     uzers::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>()?, |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   uzers::get_user_by_uid(uid).ok_or_else(|| anyhow::anyhow!("User not found")) |  | ||||||
| } |  | ||||||
|   | |||||||
							
								
								
									
										94
									
								
								crates/gpapi/src/process/hip_launcher.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,94 @@ | |||||||
|  | use std::process::Stdio; | ||||||
|  |  | ||||||
|  | use anyhow::bail; | ||||||
|  | use tokio::process::Command; | ||||||
|  |  | ||||||
|  | pub struct HipLauncher<'a> { | ||||||
|  |   program: &'a str, | ||||||
|  |   cookie: Option<&'a str>, | ||||||
|  |   client_ip: Option<&'a str>, | ||||||
|  |   md5: Option<&'a str>, | ||||||
|  |   client_os: Option<&'a str>, | ||||||
|  |   client_version: Option<&'a str>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl<'a> HipLauncher<'a> { | ||||||
|  |   pub fn new(program: &'a str) -> Self { | ||||||
|  |     Self { | ||||||
|  |       program, | ||||||
|  |       cookie: None, | ||||||
|  |       client_ip: None, | ||||||
|  |       md5: None, | ||||||
|  |       client_os: None, | ||||||
|  |       client_version: None, | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn cookie(mut self, cookie: &'a str) -> Self { | ||||||
|  |     self.cookie = Some(cookie); | ||||||
|  |     self | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn client_ip(mut self, client_ip: &'a str) -> Self { | ||||||
|  |     self.client_ip = Some(client_ip); | ||||||
|  |     self | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn md5(mut self, md5: &'a str) -> Self { | ||||||
|  |     self.md5 = Some(md5); | ||||||
|  |     self | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn client_os(mut self, client_os: &'a str) -> Self { | ||||||
|  |     self.client_os = Some(client_os); | ||||||
|  |     self | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn client_version(mut self, client_version: Option<&'a str>) -> Self { | ||||||
|  |     self.client_version = client_version; | ||||||
|  |     self | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub async fn launch(&self) -> anyhow::Result<String> { | ||||||
|  |     let mut cmd = Command::new(self.program); | ||||||
|  |  | ||||||
|  |     if let Some(cookie) = self.cookie { | ||||||
|  |       cmd.arg("--cookie").arg(cookie); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if let Some(client_ip) = self.client_ip { | ||||||
|  |       cmd.arg("--client-ip").arg(client_ip); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if let Some(md5) = self.md5 { | ||||||
|  |       cmd.arg("--md5").arg(md5); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if let Some(client_os) = self.client_os { | ||||||
|  |       cmd.arg("--client-os").arg(client_os); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if let Some(client_version) = self.client_version { | ||||||
|  |       cmd.env("APP_VERSION", client_version); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let output = cmd | ||||||
|  |       .kill_on_drop(true) | ||||||
|  |       .stdout(Stdio::piped()) | ||||||
|  |       .spawn()? | ||||||
|  |       .wait_with_output() | ||||||
|  |       .await?; | ||||||
|  |  | ||||||
|  |     if let Some(exit_status) = output.status.code() { | ||||||
|  |       if exit_status != 0 { | ||||||
|  |         bail!("HIP report generation failed with exit code {}", exit_status); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       let report = String::from_utf8(output.stdout)?; | ||||||
|  |  | ||||||
|  |       Ok(report) | ||||||
|  |     } else { | ||||||
|  |       bail!("HIP report generation failed"); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -4,4 +4,6 @@ pub mod auth_launcher; | |||||||
| #[cfg(feature = "browser-auth")] | #[cfg(feature = "browser-auth")] | ||||||
| pub mod browser_authenticator; | pub mod browser_authenticator; | ||||||
| pub mod gui_launcher; | pub mod gui_launcher; | ||||||
|  | pub mod hip_launcher; | ||||||
| pub mod service_launcher; | pub mod service_launcher; | ||||||
|  | pub mod users; | ||||||
|   | |||||||
							
								
								
									
										39
									
								
								crates/gpapi/src/process/users.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,39 @@ | |||||||
|  | use std::env; | ||||||
|  |  | ||||||
|  | use anyhow::bail; | ||||||
|  | use uzers::User; | ||||||
|  |  | ||||||
|  | pub fn get_user_by_name(username: &str) -> anyhow::Result<User> { | ||||||
|  |   uzers::get_user_by_name(username).ok_or_else(|| anyhow::anyhow!("User ({}) not found", username)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub fn get_non_root_user() -> anyhow::Result<User> { | ||||||
|  |   let current_user = whoami::username(); | ||||||
|  |  | ||||||
|  |   let user = if current_user == "root" { | ||||||
|  |     get_real_user()? | ||||||
|  |   } else { | ||||||
|  |     get_user_by_name(¤t_user)? | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   if user.uid() == 0 { | ||||||
|  |     bail!("Non-root user not found") | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Ok(user) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub fn get_current_user() -> anyhow::Result<User> { | ||||||
|  |   let current_user = whoami::username(); | ||||||
|  |   get_user_by_name(¤t_user) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn get_real_user() -> anyhow::Result<User> { | ||||||
|  |   // Read the UID from SUDO_UID or PKEXEC_UID environment variable if available. | ||||||
|  |   let uid = match env::var("SUDO_UID") { | ||||||
|  |     Ok(uid) => uid.parse::<u32>()?, | ||||||
|  |     _ => env::var("PKEXEC_UID")?.parse::<u32>()?, | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   uzers::get_user_by_uid(uid).ok_or_else(|| anyhow::anyhow!("User not found")) | ||||||
|  | } | ||||||
| @@ -32,6 +32,9 @@ pub struct ConnectArgs { | |||||||
|   cookie: String, |   cookie: String, | ||||||
|   vpnc_script: Option<String>, |   vpnc_script: Option<String>, | ||||||
|   user_agent: Option<String>, |   user_agent: Option<String>, | ||||||
|  |   csd_uid: u32, | ||||||
|  |   csd_wrapper: Option<String>, | ||||||
|  |   mtu: u32, | ||||||
|   os: Option<ClientOs>, |   os: Option<ClientOs>, | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -42,6 +45,9 @@ impl ConnectArgs { | |||||||
|       vpnc_script: None, |       vpnc_script: None, | ||||||
|       user_agent: None, |       user_agent: None, | ||||||
|       os: None, |       os: None, | ||||||
|  |       csd_uid: 0, | ||||||
|  |       csd_wrapper: None, | ||||||
|  |       mtu: 0, | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -60,6 +66,18 @@ impl ConnectArgs { | |||||||
|   pub fn openconnect_os(&self) -> Option<String> { |   pub fn openconnect_os(&self) -> Option<String> { | ||||||
|     self.os.as_ref().map(|os| os.to_openconnect_os().to_string()) |     self.os.as_ref().map(|os| os.to_openconnect_os().to_string()) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   pub fn csd_uid(&self) -> u32 { | ||||||
|  |     self.csd_uid | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn csd_wrapper(&self) -> Option<String> { | ||||||
|  |     self.csd_wrapper.clone() | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn mtu(&self) -> u32 { | ||||||
|  |     self.mtu | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Debug, Deserialize, Serialize, Type)] | #[derive(Debug, Deserialize, Serialize, Type)] | ||||||
| @@ -81,6 +99,21 @@ impl ConnectRequest { | |||||||
|     self |     self | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   pub fn with_csd_uid(mut self, csd_uid: u32) -> Self { | ||||||
|  |     self.args.csd_uid = csd_uid; | ||||||
|  |     self | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn with_csd_wrapper<T: Into<Option<String>>>(mut self, csd_wrapper: T) -> Self { | ||||||
|  |     self.args.csd_wrapper = csd_wrapper.into(); | ||||||
|  |     self | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn with_mtu(mut self, mtu: u32) -> Self { | ||||||
|  |     self.args.mtu = mtu; | ||||||
|  |     self | ||||||
|  |   } | ||||||
|  |  | ||||||
|   pub fn with_user_agent<T: Into<Option<String>>>(mut self, user_agent: T) -> Self { |   pub fn with_user_agent<T: Into<Option<String>>>(mut self, user_agent: T) -> Self { | ||||||
|     self.args.user_agent = user_agent.into(); |     self.args.user_agent = user_agent.into(); | ||||||
|     self |     self | ||||||
|   | |||||||
| @@ -15,6 +15,11 @@ pub(crate) struct ConnectOptions { | |||||||
|   pub os: *const c_char, |   pub os: *const c_char, | ||||||
|   pub certificate: *const c_char, |   pub certificate: *const c_char, | ||||||
|   pub servercert: *const c_char, |   pub servercert: *const c_char, | ||||||
|  |  | ||||||
|  |   pub csd_uid: u32, | ||||||
|  |   pub csd_wrapper: *const c_char, | ||||||
|  |  | ||||||
|  |   pub mtu: u32, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[link(name = "vpn")] | #[link(name = "vpn")] | ||||||
|   | |||||||
| @@ -61,6 +61,9 @@ int vpn_connect(const vpn_options *options, vpn_connected_callback callback) | |||||||
|     INFO("User agent: %s", options->user_agent); |     INFO("User agent: %s", options->user_agent); | ||||||
|     INFO("VPNC script: %s", options->script); |     INFO("VPNC script: %s", options->script); | ||||||
|     INFO("OS: %s", options->os); |     INFO("OS: %s", options->os); | ||||||
|  |     INFO("CSD_USER: %d", options->csd_uid); | ||||||
|  |     INFO("CSD_WRAPPER: %s", options->csd_wrapper); | ||||||
|  |     INFO("MTU: %d", options->mtu); | ||||||
|  |  | ||||||
|     vpninfo = openconnect_vpninfo_new(options->user_agent, validate_peer_cert, NULL, NULL, print_progress, NULL); |     vpninfo = openconnect_vpninfo_new(options->user_agent, validate_peer_cert, NULL, NULL, print_progress, NULL); | ||||||
|  |  | ||||||
| @@ -91,6 +94,15 @@ int vpn_connect(const vpn_options *options, vpn_connected_callback callback) | |||||||
|         openconnect_set_system_trust(vpninfo, 0); |         openconnect_set_system_trust(vpninfo, 0); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     if (options->csd_wrapper) { | ||||||
|  |         openconnect_setup_csd(vpninfo, options->csd_uid, 1, options->csd_wrapper); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (options->mtu > 0) { | ||||||
|  |         int mtu = options->mtu < 576 ? 576 : options->mtu; | ||||||
|  |         openconnect_set_reqmtu(vpninfo, mtu); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     g_cmd_pipe_fd = openconnect_setup_cmd_pipe(vpninfo); |     g_cmd_pipe_fd = openconnect_setup_cmd_pipe(vpninfo); | ||||||
|     if (g_cmd_pipe_fd < 0) |     if (g_cmd_pipe_fd < 0) | ||||||
|     { |     { | ||||||
| @@ -137,6 +149,9 @@ int vpn_connect(const vpn_options *options, vpn_connected_callback callback) | |||||||
| void vpn_disconnect() | void vpn_disconnect() | ||||||
| { | { | ||||||
|     char cmd = OC_CMD_CANCEL; |     char cmd = OC_CMD_CANCEL; | ||||||
|  |  | ||||||
|  |     INFO("Stopping VPN connection: %d", g_cmd_pipe_fd); | ||||||
|  |  | ||||||
|     if (write(g_cmd_pipe_fd, &cmd, 1) < 0) |     if (write(g_cmd_pipe_fd, &cmd, 1) < 0) | ||||||
|     { |     { | ||||||
|         ERROR("Failed to write to command pipe, VPN connection may not be stopped"); |         ERROR("Failed to write to command pipe, VPN connection may not be stopped"); | ||||||
|   | |||||||
| @@ -16,6 +16,11 @@ typedef struct vpn_options | |||||||
|     const char *os; |     const char *os; | ||||||
|     const char *certificate; |     const char *certificate; | ||||||
|     const char *servercert; |     const char *servercert; | ||||||
|  |  | ||||||
|  |     const uid_t csd_uid; | ||||||
|  |     const char *csd_wrapper; | ||||||
|  |  | ||||||
|  |     const int mtu; | ||||||
| } vpn_options; | } vpn_options; | ||||||
|  |  | ||||||
| int vpn_connect(const vpn_options *options, vpn_connected_callback callback); | int vpn_connect(const vpn_options *options, vpn_connected_callback callback); | ||||||
|   | |||||||
| @@ -18,6 +18,11 @@ pub struct Vpn { | |||||||
|   certificate: Option<CString>, |   certificate: Option<CString>, | ||||||
|   servercert: Option<CString>, |   servercert: Option<CString>, | ||||||
|  |  | ||||||
|  |   csd_uid: u32, | ||||||
|  |   csd_wrapper: Option<CString>, | ||||||
|  |  | ||||||
|  |   mtu: u32, | ||||||
|  |  | ||||||
|   callback: OnConnectedCallback, |   callback: OnConnectedCallback, | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -56,6 +61,11 @@ impl Vpn { | |||||||
|       os: self.os.as_ptr(), |       os: self.os.as_ptr(), | ||||||
|       certificate: Self::option_to_ptr(&self.certificate), |       certificate: Self::option_to_ptr(&self.certificate), | ||||||
|       servercert: Self::option_to_ptr(&self.servercert), |       servercert: Self::option_to_ptr(&self.servercert), | ||||||
|  |  | ||||||
|  |       csd_uid: self.csd_uid, | ||||||
|  |       csd_wrapper: Self::option_to_ptr(&self.csd_wrapper), | ||||||
|  |  | ||||||
|  |       mtu: self.mtu, | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -73,6 +83,11 @@ pub struct VpnBuilder { | |||||||
|   user_agent: Option<String>, |   user_agent: Option<String>, | ||||||
|   script: Option<String>, |   script: Option<String>, | ||||||
|   os: Option<String>, |   os: Option<String>, | ||||||
|  |  | ||||||
|  |   csd_uid: u32, | ||||||
|  |   csd_wrapper: Option<String>, | ||||||
|  |  | ||||||
|  |   mtu: u32, | ||||||
| } | } | ||||||
|  |  | ||||||
| impl VpnBuilder { | impl VpnBuilder { | ||||||
| @@ -83,6 +98,9 @@ impl VpnBuilder { | |||||||
|       user_agent: None, |       user_agent: None, | ||||||
|       script: None, |       script: None, | ||||||
|       os: None, |       os: None, | ||||||
|  |       csd_uid: 0, | ||||||
|  |       csd_wrapper: None, | ||||||
|  |       mtu: 0, | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -101,6 +119,21 @@ impl VpnBuilder { | |||||||
|     self |     self | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   pub fn csd_uid(mut self, csd_uid: u32) -> Self { | ||||||
|  |     self.csd_uid = csd_uid; | ||||||
|  |     self | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn csd_wrapper<T: Into<Option<String>>>(mut self, csd_wrapper: T) -> Self { | ||||||
|  |     self.csd_wrapper = csd_wrapper.into(); | ||||||
|  |     self | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn mtu(mut self, mtu: u32) -> Self { | ||||||
|  |     self.mtu = mtu; | ||||||
|  |     self | ||||||
|  |   } | ||||||
|  |  | ||||||
|   pub fn build(self) -> Vpn { |   pub fn build(self) -> Vpn { | ||||||
|     let user_agent = self.user_agent.unwrap_or_default(); |     let user_agent = self.user_agent.unwrap_or_default(); | ||||||
|     let script = self.script.or_else(find_default_vpnc_script).unwrap_or_default(); |     let script = self.script.or_else(find_default_vpnc_script).unwrap_or_default(); | ||||||
| @@ -114,6 +147,12 @@ impl VpnBuilder { | |||||||
|       os: Self::to_cstring(&os), |       os: Self::to_cstring(&os), | ||||||
|       certificate: None, |       certificate: None, | ||||||
|       servercert: None, |       servercert: None, | ||||||
|  |  | ||||||
|  |       csd_uid: self.csd_uid, | ||||||
|  |       csd_wrapper: self.csd_wrapper.as_deref().map(Self::to_cstring), | ||||||
|  |  | ||||||
|  |       mtu: self.mtu, | ||||||
|  |  | ||||||
|       callback: Default::default(), |       callback: Default::default(), | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   | |||||||