Skip to content

Commit

Permalink
Support HIP report (#309)
Browse files Browse the repository at this point in the history
  • Loading branch information
yuezk authored Feb 5, 2024
1 parent 662e4d0 commit db9249b
Show file tree
Hide file tree
Showing 12 changed files with 334 additions and 2 deletions.
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ thiserror = "1"
redact-engine = "0.1"
dotenvy_macro = "0.15"
compile-time = "0.2"
serde_urlencoded = "0.7"
md5="0.7"

[profile.release]
opt-level = 'z' # Optimize for size
Expand Down
6 changes: 5 additions & 1 deletion apps/gpservice/src/vpn_task.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,13 @@ impl VpnTaskContext {
}

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 vpn = Vpn::builder(req.gateway().server(), args.cookie())
.user_agent(args.user_agent())
.script(args.vpnc_script())
.csd_uid(args.csd_uid())
.csd_wrapper(args.csd_wrapper())
.os(args.openconnect_os())
.build();

Expand Down Expand Up @@ -73,7 +75,9 @@ impl VpnTaskContext {

pub async fn disconnect(&self) {
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() {
info!("VPN is connected, start disconnecting...");
self.vpn_state_tx.send(VpnState::Disconnecting).ok();
vpn.disconnect()
}
Expand Down
2 changes: 2 additions & 0 deletions crates/gpapi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ url.workspace = true
regex.workspace = true
dotenvy_macro.workspace = true
uzers.workspace = true
serde_urlencoded.workspace = true
md5.workspace = true

tauri = { workspace = true, optional = true }
clap = { workspace = true, optional = true }
Expand Down
178 changes: 178 additions & 0 deletions crates/gpapi/src/gateway/hip.rs
Original file line number Diff line number Diff line change
@@ -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, &params)?;

let res = self.client.post(&config_url).form(&params).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, &params)?;
let res = self.client.post(&url).form(&params).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, &params)?;
let res = self.client.post(&url).form(&params).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 change: 1 addition & 0 deletions crates/gpapi/src/gateway/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod login;
mod parse_gateways;
pub mod hip;

pub use login::*;
pub(crate) use parse_gateways::*;
Expand Down
12 changes: 12 additions & 0 deletions crates/gpapi/src/gp_params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,18 @@ impl GpParams {
self.prefer_default_browser
}

pub fn client_os(&self) -> &str {
self.client_os.as_str()
}

pub fn os_version(&self) -> Option<&str> {
self.os_version.as_deref()
}

pub fn client_version(&self) -> Option<&str> {
self.client_version.as_deref()
}

pub(crate) fn to_params(&self) -> HashMap<&str, &str> {
let mut params: HashMap<&str, &str> = HashMap::new();
let client_os = self.client_os.as_str();
Expand Down
94 changes: 94 additions & 0 deletions crates/gpapi/src/process/hip_launcher.rs
Original file line number Diff line number Diff line change
@@ -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");
}
}
}
3 changes: 2 additions & 1 deletion crates/gpapi/src/process/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
pub(crate) mod command_traits;

pub mod users;
pub mod auth_launcher;
#[cfg(feature = "browser-auth")]
pub mod browser_authenticator;
pub mod gui_launcher;
pub mod hip_launcher;
pub mod service_launcher;
pub mod users;
5 changes: 5 additions & 0 deletions crates/gpapi/src/process/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ pub fn get_non_root_user() -> anyhow::Result<User> {
Ok(user)
}

pub fn get_current_user() -> anyhow::Result<User> {
let current_user = whoami::username();
get_user_by_name(&current_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") {
Expand Down
Loading

0 comments on commit db9249b

Please sign in to comment.