mirror of
				https://github.com/yuezk/GlobalProtect-openconnect.git
				synced 2025-05-20 07:26:58 -04:00 
			
		
		
		
	Add csd wrapper
This commit is contained in:
		
							
								
								
									
										7
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										7
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							@@ -1431,6 +1431,7 @@ dependencies = [
 | 
				
			|||||||
 "clap",
 | 
					 "clap",
 | 
				
			||||||
 "dotenvy_macro",
 | 
					 "dotenvy_macro",
 | 
				
			||||||
 "log",
 | 
					 "log",
 | 
				
			||||||
 | 
					 "md5",
 | 
				
			||||||
 "open",
 | 
					 "open",
 | 
				
			||||||
 "redact-engine",
 | 
					 "redact-engine",
 | 
				
			||||||
 "regex",
 | 
					 "regex",
 | 
				
			||||||
@@ -2227,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"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -44,6 +44,7 @@ redact-engine = "0.1"
 | 
				
			|||||||
dotenvy_macro = "0.15"
 | 
					dotenvy_macro = "0.15"
 | 
				
			||||||
compile-time = "0.2"
 | 
					compile-time = "0.2"
 | 
				
			||||||
serde_urlencoded = "0.7"
 | 
					serde_urlencoded = "0.7"
 | 
				
			||||||
 | 
					md5="0.7"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[profile.release]
 | 
					[profile.release]
 | 
				
			||||||
opt-level = 'z'   # Optimize for size
 | 
					opt-level = 'z'   # Optimize for size
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,6 +26,7 @@ regex.workspace = true
 | 
				
			|||||||
dotenvy_macro.workspace = true
 | 
					dotenvy_macro.workspace = true
 | 
				
			||||||
uzers.workspace = true
 | 
					uzers.workspace = true
 | 
				
			||||||
serde_urlencoded.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 }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,51 +1,178 @@
 | 
				
			|||||||
use std::collections::HashMap;
 | 
					use std::collections::HashMap;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use anyhow::bail;
 | 
					use log::{info, warn};
 | 
				
			||||||
use reqwest::Client;
 | 
					use reqwest::Client;
 | 
				
			||||||
 | 
					use roxmltree::Document;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{gp_params::GpParams, utils::normalize_server};
 | 
					use crate::{gp_params::GpParams, process::hip_launcher::HipLauncher, utils::normalize_server};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async fn retrieve_config(gateway: &str, cookie: &str, gp_params: &GpParams) -> anyhow::Result<()> {
 | 
					struct HipReporter<'a> {
 | 
				
			||||||
  let url = normalize_server(gateway)?;
 | 
					  server: String,
 | 
				
			||||||
 | 
					  cookie: &'a str,
 | 
				
			||||||
 | 
					  md5: &'a str,
 | 
				
			||||||
 | 
					  csd_wrapper: &'a str,
 | 
				
			||||||
 | 
					  gp_params: &'a GpParams,
 | 
				
			||||||
 | 
					  client: Client,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let config_url = format!("{}/ssl-vpn/getconfig.esp", url);
 | 
					impl HipReporter<'_> {
 | 
				
			||||||
  let client = Client::builder()
 | 
					  async fn report(&self) -> anyhow::Result<()> {
 | 
				
			||||||
    .danger_accept_invalid_certs(gp_params.ignore_tls_errors())
 | 
					    let client_ip = self.retrieve_client_ip().await?;
 | 
				
			||||||
    .user_agent(gp_params.user_agent())
 | 
					 | 
				
			||||||
    .build()?;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let mut params = serde_urlencoded::from_str::<HashMap<&str, &str>>(cookie)?;
 | 
					    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(());
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  println!("{:?}", params);
 | 
					    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("client-type", "1");
 | 
				
			||||||
    params.insert("protocol-version", "p1");
 | 
					    params.insert("protocol-version", "p1");
 | 
				
			||||||
    params.insert("internal", "no");
 | 
					    params.insert("internal", "no");
 | 
				
			||||||
    params.insert("ipv6-support", "yes");
 | 
					    params.insert("ipv6-support", "yes");
 | 
				
			||||||
  params.insert("clientos", gp_params.client_os());
 | 
					    params.insert("clientos", self.gp_params.client_os());
 | 
				
			||||||
    params.insert("hmac-algo", "sha1,md5,sha256");
 | 
					    params.insert("hmac-algo", "sha1,md5,sha256");
 | 
				
			||||||
    params.insert("enc-algo", "aes-128-cbc,aes-256-cbc");
 | 
					    params.insert("enc-algo", "aes-128-cbc,aes-256-cbc");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if let Some(os_version) = gp_params.os_version() {
 | 
					    if let Some(os_version) = self.gp_params.os_version() {
 | 
				
			||||||
      params.insert("os-version", os_version);
 | 
					      params.insert("os-version", os_version);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  if let Some(client_version) = gp_params.client_version() {
 | 
					    if let Some(client_version) = self.gp_params.client_version() {
 | 
				
			||||||
      params.insert("app-version", client_version);
 | 
					      params.insert("app-version", client_version);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let res = client.post(&config_url).form(¶ms).send().await?;
 | 
					    let params = merge_cookie_params(self.cookie, ¶ms)?;
 | 
				
			||||||
  let status = res.status();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if status.is_client_error() || status.is_server_error() {
 | 
					    let res = self.client.post(&config_url).form(¶ms).send().await?;
 | 
				
			||||||
    bail!("Retrieve config error: {}", status)
 | 
					    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())
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let res_xml = res.text().await?;
 | 
					  async fn check_hip(&self, client_ip: &str) -> anyhow::Result<bool> {
 | 
				
			||||||
  println!("{}", res_xml);
 | 
					    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(())
 | 
					    Ok(())
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
pub async fn hip_report(gateway: &str, cookie: &str, gp_params: &GpParams) -> anyhow::Result<()> {
 | 
					
 | 
				
			||||||
  retrieve_config(gateway, cookie, gp_params).await
 | 
					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
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										94
									
								
								crates/gpapi/src/process/hip_launcher.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								crates/gpapi/src/process/hip_launcher.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,94 @@
 | 
				
			|||||||
 | 
					use std::process::Stdio;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use anyhow::bail;
 | 
				
			||||||
 | 
					use tokio::process::Command;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct HipLauncher<'a> {
 | 
				
			||||||
 | 
					  program: &'a str,
 | 
				
			||||||
 | 
					  cookie: Option<&'a str>,
 | 
				
			||||||
 | 
					  client_ip: Option<&'a str>,
 | 
				
			||||||
 | 
					  md5: Option<&'a str>,
 | 
				
			||||||
 | 
					  client_os: Option<&'a str>,
 | 
				
			||||||
 | 
					  client_version: Option<&'a str>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<'a> HipLauncher<'a> {
 | 
				
			||||||
 | 
					  pub fn new(program: &'a str) -> Self {
 | 
				
			||||||
 | 
					    Self {
 | 
				
			||||||
 | 
					      program,
 | 
				
			||||||
 | 
					      cookie: None,
 | 
				
			||||||
 | 
					      client_ip: None,
 | 
				
			||||||
 | 
					      md5: None,
 | 
				
			||||||
 | 
					      client_os: None,
 | 
				
			||||||
 | 
					      client_version: None,
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  pub fn cookie(mut self, cookie: &'a str) -> Self {
 | 
				
			||||||
 | 
					    self.cookie = Some(cookie);
 | 
				
			||||||
 | 
					    self
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  pub fn client_ip(mut self, client_ip: &'a str) -> Self {
 | 
				
			||||||
 | 
					    self.client_ip = Some(client_ip);
 | 
				
			||||||
 | 
					    self
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  pub fn md5(mut self, md5: &'a str) -> Self {
 | 
				
			||||||
 | 
					    self.md5 = Some(md5);
 | 
				
			||||||
 | 
					    self
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  pub fn client_os(mut self, client_os: &'a str) -> Self {
 | 
				
			||||||
 | 
					    self.client_os = Some(client_os);
 | 
				
			||||||
 | 
					    self
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  pub fn client_version(mut self, client_version: Option<&'a str>) -> Self {
 | 
				
			||||||
 | 
					    self.client_version = client_version;
 | 
				
			||||||
 | 
					    self
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  pub async fn launch(&self) -> anyhow::Result<String> {
 | 
				
			||||||
 | 
					    let mut cmd = Command::new(self.program);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if let Some(cookie) = self.cookie {
 | 
				
			||||||
 | 
					      cmd.arg("--cookie").arg(cookie);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if let Some(client_ip) = self.client_ip {
 | 
				
			||||||
 | 
					      cmd.arg("--client-ip").arg(client_ip);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if let Some(md5) = self.md5 {
 | 
				
			||||||
 | 
					      cmd.arg("--md5").arg(md5);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if let Some(client_os) = self.client_os {
 | 
				
			||||||
 | 
					      cmd.arg("--client-os").arg(client_os);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if let Some(client_version) = self.client_version {
 | 
				
			||||||
 | 
					      cmd.env("APP_VERSION", client_version);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let output = cmd
 | 
				
			||||||
 | 
					      .kill_on_drop(true)
 | 
				
			||||||
 | 
					      .stdout(Stdio::piped())
 | 
				
			||||||
 | 
					      .spawn()?
 | 
				
			||||||
 | 
					      .wait_with_output()
 | 
				
			||||||
 | 
					      .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if let Some(exit_status) = output.status.code() {
 | 
				
			||||||
 | 
					      if exit_status != 0 {
 | 
				
			||||||
 | 
					        bail!("HIP report generation failed with exit code {}", exit_status);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      let report = String::from_utf8(output.stdout)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      Ok(report)
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      bail!("HIP report generation failed");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,8 +1,9 @@
 | 
				
			|||||||
pub(crate) mod command_traits;
 | 
					pub(crate) mod command_traits;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub mod users;
 | 
					 | 
				
			||||||
pub mod auth_launcher;
 | 
					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;
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user