mirror of
				https://github.com/yuezk/GlobalProtect-openconnect.git
				synced 2025-05-20 07:26:58 -04:00 
			
		
		
		
	Move new code
This commit is contained in:
		
							
								
								
									
										23
									
								
								apps/gpclient/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								apps/gpclient/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
[package]
 | 
			
		||||
name = "gpclient"
 | 
			
		||||
authors.workspace = true
 | 
			
		||||
version.workspace = true
 | 
			
		||||
edition.workspace = true
 | 
			
		||||
license.workspace = true
 | 
			
		||||
 | 
			
		||||
[dependencies]
 | 
			
		||||
gpapi = { path = "../../crates/gpapi" }
 | 
			
		||||
openconnect = { path = "../../crates/openconnect" }
 | 
			
		||||
anyhow.workspace = true
 | 
			
		||||
clap.workspace = true
 | 
			
		||||
env_logger.workspace = true
 | 
			
		||||
inquire = "0.6.2"
 | 
			
		||||
log.workspace = true
 | 
			
		||||
tokio.workspace = true
 | 
			
		||||
sysinfo.workspace = true
 | 
			
		||||
serde_json.workspace = true
 | 
			
		||||
whoami.workspace = true
 | 
			
		||||
tempfile.workspace = true
 | 
			
		||||
reqwest.workspace = true
 | 
			
		||||
directories = "5.0"
 | 
			
		||||
compile-time.workspace = true
 | 
			
		||||
							
								
								
									
										101
									
								
								apps/gpclient/src/cli.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								apps/gpclient/src/cli.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,101 @@
 | 
			
		||||
use clap::{Parser, Subcommand};
 | 
			
		||||
use gpapi::utils::openssl;
 | 
			
		||||
use log::{info, LevelFilter};
 | 
			
		||||
use tempfile::NamedTempFile;
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
  connect::{ConnectArgs, ConnectHandler},
 | 
			
		||||
  disconnect::DisconnectHandler,
 | 
			
		||||
  launch_gui::{LaunchGuiArgs, LaunchGuiHandler},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const VERSION: &str = concat!(
 | 
			
		||||
  env!("CARGO_PKG_VERSION"),
 | 
			
		||||
  " (",
 | 
			
		||||
  compile_time::date_str!(),
 | 
			
		||||
  ")"
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
#[derive(Subcommand)]
 | 
			
		||||
enum CliCommand {
 | 
			
		||||
  #[command(about = "Connect to a portal server")]
 | 
			
		||||
  Connect(ConnectArgs),
 | 
			
		||||
  #[command(about = "Disconnect from the server")]
 | 
			
		||||
  Disconnect,
 | 
			
		||||
  #[command(about = "Launch the GUI")]
 | 
			
		||||
  LaunchGui(LaunchGuiArgs),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Parser)]
 | 
			
		||||
#[command(
 | 
			
		||||
  version = VERSION,
 | 
			
		||||
  author,
 | 
			
		||||
  about = "The GlobalProtect VPN client, based on OpenConnect, supports the SSO authentication method.",
 | 
			
		||||
  help_template = "\
 | 
			
		||||
{before-help}{name} {version}
 | 
			
		||||
{author}
 | 
			
		||||
 | 
			
		||||
{about}
 | 
			
		||||
 | 
			
		||||
{usage-heading} {usage}
 | 
			
		||||
 | 
			
		||||
{all-args}{after-help}
 | 
			
		||||
"
 | 
			
		||||
)]
 | 
			
		||||
struct Cli {
 | 
			
		||||
  #[command(subcommand)]
 | 
			
		||||
  command: CliCommand,
 | 
			
		||||
 | 
			
		||||
  #[arg(
 | 
			
		||||
    long,
 | 
			
		||||
    help = "Get around the OpenSSL `unsafe legacy renegotiation` error"
 | 
			
		||||
  )]
 | 
			
		||||
  fix_openssl: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Cli {
 | 
			
		||||
  fn fix_openssl(&self) -> anyhow::Result<Option<NamedTempFile>> {
 | 
			
		||||
    if self.fix_openssl {
 | 
			
		||||
      let file = openssl::fix_openssl_env()?;
 | 
			
		||||
      return Ok(Some(file));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(None)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async fn run(&self) -> anyhow::Result<()> {
 | 
			
		||||
    // The temp file will be dropped automatically when the file handle is dropped
 | 
			
		||||
    // So, declare it here to ensure it's not dropped
 | 
			
		||||
    let _file = self.fix_openssl()?;
 | 
			
		||||
 | 
			
		||||
    match &self.command {
 | 
			
		||||
      CliCommand::Connect(args) => ConnectHandler::new(args, self.fix_openssl).handle().await,
 | 
			
		||||
      CliCommand::Disconnect => DisconnectHandler::new().handle(),
 | 
			
		||||
      CliCommand::LaunchGui(args) => LaunchGuiHandler::new(args).handle().await,
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn init_logger() {
 | 
			
		||||
  env_logger::builder().filter_level(LevelFilter::Info).init();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub(crate) async fn run() {
 | 
			
		||||
  let cli = Cli::parse();
 | 
			
		||||
 | 
			
		||||
  init_logger();
 | 
			
		||||
  info!("gpclient started: {}", VERSION);
 | 
			
		||||
 | 
			
		||||
  if let Err(err) = cli.run().await {
 | 
			
		||||
    eprintln!("\nError: {}", err);
 | 
			
		||||
 | 
			
		||||
    if err.to_string().contains("unsafe legacy renegotiation") && !cli.fix_openssl {
 | 
			
		||||
      eprintln!("\nRe-run it with the `--fix-openssl` option to work around this issue, e.g.:\n");
 | 
			
		||||
      // Print the command
 | 
			
		||||
      let args = std::env::args().collect::<Vec<_>>();
 | 
			
		||||
      eprintln!("{} --fix-openssl {}\n", args[0], args[1..].join(" "));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    std::process::exit(1);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										150
									
								
								apps/gpclient/src/connect.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								apps/gpclient/src/connect.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,150 @@
 | 
			
		||||
use std::{fs, sync::Arc};
 | 
			
		||||
 | 
			
		||||
use clap::Args;
 | 
			
		||||
use gpapi::{
 | 
			
		||||
  credential::{Credential, PasswordCredential},
 | 
			
		||||
  gateway::gateway_login,
 | 
			
		||||
  gp_params::GpParams,
 | 
			
		||||
  portal::{prelogin, retrieve_config, Prelogin},
 | 
			
		||||
  process::auth_launcher::SamlAuthLauncher,
 | 
			
		||||
  utils::{self, shutdown_signal},
 | 
			
		||||
  GP_USER_AGENT,
 | 
			
		||||
};
 | 
			
		||||
use inquire::{Password, PasswordDisplayMode, Select, Text};
 | 
			
		||||
use log::info;
 | 
			
		||||
use openconnect::Vpn;
 | 
			
		||||
 | 
			
		||||
use crate::GP_CLIENT_LOCK_FILE;
 | 
			
		||||
 | 
			
		||||
#[derive(Args)]
 | 
			
		||||
pub(crate) struct ConnectArgs {
 | 
			
		||||
  #[arg(help = "The portal server to connect to")]
 | 
			
		||||
  server: String,
 | 
			
		||||
  #[arg(
 | 
			
		||||
    short,
 | 
			
		||||
    long,
 | 
			
		||||
    help = "The gateway to connect to, it will prompt if not specified"
 | 
			
		||||
  )]
 | 
			
		||||
  gateway: Option<String>,
 | 
			
		||||
  #[arg(
 | 
			
		||||
    short,
 | 
			
		||||
    long,
 | 
			
		||||
    help = "The username to use, it will prompt if not specified"
 | 
			
		||||
  )]
 | 
			
		||||
  user: Option<String>,
 | 
			
		||||
  #[arg(long, short, help = "The VPNC script to use")]
 | 
			
		||||
  script: Option<String>,
 | 
			
		||||
  #[arg(long, default_value = GP_USER_AGENT, help = "The user agent to use")]
 | 
			
		||||
  user_agent: String,
 | 
			
		||||
  #[arg(long, help = "The HiDPI mode, useful for high resolution screens")]
 | 
			
		||||
  hidpi: bool,
 | 
			
		||||
  #[arg(long, help = "Do not reuse the remembered authentication cookie")]
 | 
			
		||||
  clean: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub(crate) struct ConnectHandler<'a> {
 | 
			
		||||
  args: &'a ConnectArgs,
 | 
			
		||||
  fix_openssl: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<'a> ConnectHandler<'a> {
 | 
			
		||||
  pub(crate) fn new(args: &'a ConnectArgs, fix_openssl: bool) -> Self {
 | 
			
		||||
    Self { args, fix_openssl }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  pub(crate) async fn handle(&self) -> anyhow::Result<()> {
 | 
			
		||||
    let portal = utils::normalize_server(self.args.server.as_str())?;
 | 
			
		||||
 | 
			
		||||
    let gp_params = GpParams::builder()
 | 
			
		||||
      .user_agent(&self.args.user_agent)
 | 
			
		||||
      .build();
 | 
			
		||||
 | 
			
		||||
    let prelogin = prelogin(&portal, &self.args.user_agent).await?;
 | 
			
		||||
    let portal_credential = self.obtain_portal_credential(&prelogin).await?;
 | 
			
		||||
    let mut portal_config = retrieve_config(&portal, &portal_credential, &gp_params).await?;
 | 
			
		||||
 | 
			
		||||
    let selected_gateway = match &self.args.gateway {
 | 
			
		||||
      Some(gateway) => portal_config
 | 
			
		||||
        .find_gateway(gateway)
 | 
			
		||||
        .ok_or_else(|| anyhow::anyhow!("Cannot find gateway {}", gateway))?,
 | 
			
		||||
      None => {
 | 
			
		||||
        portal_config.sort_gateways(prelogin.region());
 | 
			
		||||
        let gateways = portal_config.gateways();
 | 
			
		||||
 | 
			
		||||
        if gateways.len() > 1 {
 | 
			
		||||
          Select::new("Which gateway do you want to connect to?", gateways)
 | 
			
		||||
            .with_vim_mode(true)
 | 
			
		||||
            .prompt()?
 | 
			
		||||
        } else {
 | 
			
		||||
          gateways[0]
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let gateway = selected_gateway.server();
 | 
			
		||||
    let cred = portal_config.auth_cookie().into();
 | 
			
		||||
    let token = gateway_login(gateway, &cred, &gp_params).await?;
 | 
			
		||||
 | 
			
		||||
    let vpn = Vpn::builder(gateway, &token)
 | 
			
		||||
      .user_agent(self.args.user_agent.clone())
 | 
			
		||||
      .script(self.args.script.clone())
 | 
			
		||||
      .build();
 | 
			
		||||
 | 
			
		||||
    let vpn = Arc::new(vpn);
 | 
			
		||||
    let vpn_clone = vpn.clone();
 | 
			
		||||
 | 
			
		||||
    // Listen for the interrupt signal in the background
 | 
			
		||||
    tokio::spawn(async move {
 | 
			
		||||
      shutdown_signal().await;
 | 
			
		||||
      info!("Received the interrupt signal, disconnecting...");
 | 
			
		||||
      vpn_clone.disconnect();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    vpn.connect(write_pid_file);
 | 
			
		||||
 | 
			
		||||
    if fs::metadata(GP_CLIENT_LOCK_FILE).is_ok() {
 | 
			
		||||
      info!("Removing PID file");
 | 
			
		||||
      fs::remove_file(GP_CLIENT_LOCK_FILE)?;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async fn obtain_portal_credential(&self, prelogin: &Prelogin) -> anyhow::Result<Credential> {
 | 
			
		||||
    match prelogin {
 | 
			
		||||
      Prelogin::Saml(prelogin) => {
 | 
			
		||||
        SamlAuthLauncher::new(&self.args.server)
 | 
			
		||||
          .user_agent(&self.args.user_agent)
 | 
			
		||||
          .saml_request(prelogin.saml_request())
 | 
			
		||||
          .hidpi(self.args.hidpi)
 | 
			
		||||
          .fix_openssl(self.fix_openssl)
 | 
			
		||||
          .clean(self.args.clean)
 | 
			
		||||
          .launch()
 | 
			
		||||
          .await
 | 
			
		||||
      }
 | 
			
		||||
      Prelogin::Standard(prelogin) => {
 | 
			
		||||
        println!("{}", prelogin.auth_message());
 | 
			
		||||
 | 
			
		||||
        let user = self.args.user.as_ref().map_or_else(
 | 
			
		||||
          || Text::new(&format!("{}:", prelogin.label_username())).prompt(),
 | 
			
		||||
          |user| Ok(user.to_owned()),
 | 
			
		||||
        )?;
 | 
			
		||||
        let password = Password::new(&format!("{}:", prelogin.label_password()))
 | 
			
		||||
          .without_confirmation()
 | 
			
		||||
          .with_display_mode(PasswordDisplayMode::Masked)
 | 
			
		||||
          .prompt()?;
 | 
			
		||||
 | 
			
		||||
        let password_cred = PasswordCredential::new(&user, &password);
 | 
			
		||||
 | 
			
		||||
        Ok(password_cred.into())
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn write_pid_file() {
 | 
			
		||||
  let pid = std::process::id();
 | 
			
		||||
 | 
			
		||||
  fs::write(GP_CLIENT_LOCK_FILE, pid.to_string()).unwrap();
 | 
			
		||||
  info!("Wrote PID {} to {}", pid, GP_CLIENT_LOCK_FILE);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										31
									
								
								apps/gpclient/src/disconnect.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								apps/gpclient/src/disconnect.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
			
		||||
use crate::GP_CLIENT_LOCK_FILE;
 | 
			
		||||
use log::{info, warn};
 | 
			
		||||
use std::fs;
 | 
			
		||||
use sysinfo::{Pid, ProcessExt, Signal, System, SystemExt};
 | 
			
		||||
 | 
			
		||||
pub(crate) struct DisconnectHandler;
 | 
			
		||||
 | 
			
		||||
impl DisconnectHandler {
 | 
			
		||||
  pub(crate) fn new() -> Self {
 | 
			
		||||
    Self
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  pub(crate) fn handle(&self) -> anyhow::Result<()> {
 | 
			
		||||
    if fs::metadata(GP_CLIENT_LOCK_FILE).is_err() {
 | 
			
		||||
      warn!("PID file not found, maybe the client is not running");
 | 
			
		||||
      return Ok(());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let pid = fs::read_to_string(GP_CLIENT_LOCK_FILE)?;
 | 
			
		||||
    let pid = pid.trim().parse::<usize>()?;
 | 
			
		||||
    let s = System::new_all();
 | 
			
		||||
 | 
			
		||||
    if let Some(process) = s.process(Pid::from(pid)) {
 | 
			
		||||
      info!("Found process {}, killing...", pid);
 | 
			
		||||
      if process.kill_with(Signal::Interrupt).is_none() {
 | 
			
		||||
        warn!("Failed to kill process {}", pid);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    Ok(())
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										88
									
								
								apps/gpclient/src/launch_gui.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								apps/gpclient/src/launch_gui.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,88 @@
 | 
			
		||||
use std::{collections::HashMap, fs, path::PathBuf};
 | 
			
		||||
 | 
			
		||||
use clap::Args;
 | 
			
		||||
use directories::ProjectDirs;
 | 
			
		||||
use gpapi::{
 | 
			
		||||
  process::service_launcher::ServiceLauncher,
 | 
			
		||||
  utils::{endpoint::http_endpoint, env_file, shutdown_signal},
 | 
			
		||||
};
 | 
			
		||||
use log::info;
 | 
			
		||||
 | 
			
		||||
#[derive(Args)]
 | 
			
		||||
pub(crate) struct LaunchGuiArgs {
 | 
			
		||||
  #[clap(long, help = "Launch the GUI minimized")]
 | 
			
		||||
  minimized: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub(crate) struct LaunchGuiHandler<'a> {
 | 
			
		||||
  args: &'a LaunchGuiArgs,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<'a> LaunchGuiHandler<'a> {
 | 
			
		||||
  pub(crate) fn new(args: &'a LaunchGuiArgs) -> Self {
 | 
			
		||||
    Self { args }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  pub(crate) async fn handle(&self) -> anyhow::Result<()> {
 | 
			
		||||
    // `launch-gui`cannot be run as root
 | 
			
		||||
    let user = whoami::username();
 | 
			
		||||
    if user == "root" {
 | 
			
		||||
      anyhow::bail!("`launch-gui` cannot be run as root");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if try_active_gui().await.is_ok() {
 | 
			
		||||
      info!("The GUI is already running");
 | 
			
		||||
      return Ok(());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    tokio::spawn(async move {
 | 
			
		||||
      shutdown_signal().await;
 | 
			
		||||
      info!("Shutting down...");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    let log_file = get_log_file()?;
 | 
			
		||||
    let log_file_path = log_file.to_string_lossy().to_string();
 | 
			
		||||
 | 
			
		||||
    info!("Log file: {}", log_file_path);
 | 
			
		||||
 | 
			
		||||
    let mut extra_envs = HashMap::<String, String>::new();
 | 
			
		||||
    extra_envs.insert("GP_LOG_FILE".into(), log_file_path.clone());
 | 
			
		||||
 | 
			
		||||
    // Persist the environment variables to a file
 | 
			
		||||
    let env_file = env_file::persist_env_vars(Some(extra_envs))?;
 | 
			
		||||
    let env_file = env_file.into_temp_path();
 | 
			
		||||
    let env_file_path = env_file.to_string_lossy().to_string();
 | 
			
		||||
 | 
			
		||||
    let exit_status = ServiceLauncher::new()
 | 
			
		||||
      .minimized(self.args.minimized)
 | 
			
		||||
      .env_file(&env_file_path)
 | 
			
		||||
      .log_file(&log_file_path)
 | 
			
		||||
      .launch()
 | 
			
		||||
      .await?;
 | 
			
		||||
 | 
			
		||||
    info!("Service exited with status: {}", exit_status);
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn try_active_gui() -> anyhow::Result<()> {
 | 
			
		||||
  let service_endpoint = http_endpoint().await?;
 | 
			
		||||
 | 
			
		||||
  reqwest::Client::default()
 | 
			
		||||
    .post(format!("{}/active-gui", service_endpoint))
 | 
			
		||||
    .send()
 | 
			
		||||
    .await?
 | 
			
		||||
    .error_for_status()?;
 | 
			
		||||
 | 
			
		||||
  Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn get_log_file() -> anyhow::Result<PathBuf> {
 | 
			
		||||
  let dirs = ProjectDirs::from("com.yuezk", "GlobalProtect-openconnect", "gpclient")
 | 
			
		||||
    .ok_or_else(|| anyhow::anyhow!("Failed to get project dirs"))?;
 | 
			
		||||
 | 
			
		||||
  fs::create_dir_all(dirs.data_dir())?;
 | 
			
		||||
 | 
			
		||||
  Ok(dirs.data_dir().join("gpclient.log"))
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										11
									
								
								apps/gpclient/src/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								apps/gpclient/src/main.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
mod cli;
 | 
			
		||||
mod connect;
 | 
			
		||||
mod disconnect;
 | 
			
		||||
mod launch_gui;
 | 
			
		||||
 | 
			
		||||
pub(crate) const GP_CLIENT_LOCK_FILE: &str = "/var/run/gpclient.lock";
 | 
			
		||||
 | 
			
		||||
#[tokio::main]
 | 
			
		||||
async fn main() {
 | 
			
		||||
  cli::run().await;
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user