mirror of
https://github.com/yuezk/GlobalProtect-openconnect.git
synced 2025-05-20 07:26:58 -04:00
Refactor using Tauri (#278)
This commit is contained in:
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