diff --git a/apps/gpauth/Cargo.toml b/apps/gpauth/Cargo.toml index 5dd859f8..9a8acd00 100644 --- a/apps/gpauth/Cargo.toml +++ b/apps/gpauth/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "gpauth" +authors.workspace = true version.workspace = true edition.workspace = true license.workspace = true diff --git a/apps/gpauth/src/cli.rs b/apps/gpauth/src/cli.rs index d59a775f..bf94362b 100644 --- a/apps/gpauth/src/cli.rs +++ b/apps/gpauth/src/cli.rs @@ -1,3 +1,5 @@ +use std::{env::temp_dir, fs, os::unix::fs::PermissionsExt}; + use clap::Parser; use gpapi::{ auth::{SamlAuthData, SamlAuthResult}, @@ -11,36 +13,68 @@ use log::{info, LevelFilter}; use serde_json::json; use tauri::{App, AppHandle, RunEvent}; use tempfile::NamedTempFile; +use tokio::{io::AsyncReadExt, net::TcpListener}; use crate::auth_window::{portal_prelogin, AuthWindow}; const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), " (", compile_time::date_str!(), ")"); #[derive(Parser, Clone)] -#[command(version = VERSION)] +#[command( + version = VERSION, + author, + about = "The authentication component for the GlobalProtect VPN client, supports the SSO authentication method.", + help_template = "\ +{before-help}{name} {version} +{author} + +{about} + +{usage-heading} {usage} + +{all-args}{after-help} + +See 'gpauth -h' for more information. +" +)] struct Cli { + #[arg(help = "The portal server to authenticate")] server: String, - #[arg(long)] + + #[arg(long, help = "Treating the server as a gateway")] gateway: bool, - #[arg(long)] + + #[arg(long, help = "The SAML authentication request")] saml_request: Option, - #[arg(long, default_value = GP_USER_AGENT)] + + #[arg(long, default_value = GP_USER_AGENT, help = "The user agent to use")] user_agent: String, + #[arg(long, default_value = "Linux")] os: Os, + #[arg(long)] os_version: Option, - #[arg(long)] + + #[arg(long, help = "The HiDPI mode, useful for high-resolution screens")] hidpi: bool, - #[arg(long)] + + #[arg(long, help = "Get around the OpenSSL `unsafe legacy renegotiation` error")] fix_openssl: bool, - #[arg(long)] + + #[arg(long, help = "Ignore TLS errors")] ignore_tls_errors: bool, - #[arg(long)] + + #[arg(long, help = "Clean the cache of the embedded browser")] clean: bool, - #[arg(long)] + + #[arg(long, help = "Use the default browser for authentication")] default_browser: bool, - #[arg(long)] + + #[arg( + long, + help = "The browser to use for authentication, e.g., `default`, `firefox`, `chrome`, `chromium`, or the path to the browser executable" + )] browser: Option, } @@ -74,6 +108,15 @@ impl Cli { info!("Please continue the authentication process in the default browser"); + let auth_result = match wait_auth_data().await { + Ok(auth_data) => SamlAuthResult::Success(auth_data), + Err(err) => SamlAuthResult::Failure(format!("{}", err)), + }; + + info!("Authentication completed"); + + println!("{}", json!(auth_result)); + return Ok(()); } @@ -181,3 +224,35 @@ pub async fn run() { std::process::exit(1); } } + +async fn wait_auth_data() -> anyhow::Result { + // Start a local server to receive the browser authentication data + let listener = TcpListener::bind("127.0.0.1:0").await?; + let port = listener.local_addr()?.port(); + let port_file = temp_dir().join("gpcallback.port"); + + // Write the port to a file + fs::write(&port_file, port.to_string())?; + fs::set_permissions(&port_file, fs::Permissions::from_mode(0o600))?; + + // Remove the previous log file + let callback_log = temp_dir().join("gpcallback.log"); + let _ = fs::remove_file(&callback_log); + + info!("Listening authentication data on port {}", port); + info!( + "If it hangs, please check the logs at `{}` for more information", + callback_log.display() + ); + let (mut socket, _) = listener.accept().await?; + + info!("Received the browser authentication data from the socket"); + let mut data = String::new(); + socket.read_to_string(&mut data).await?; + + // Remove the port file + fs::remove_file(&port_file)?; + + let auth_data = SamlAuthData::from_gpcallback(&data)?; + Ok(auth_data) +} diff --git a/apps/gpclient/src/cli.rs b/apps/gpclient/src/cli.rs index 24a399d7..8c377d79 100644 --- a/apps/gpclient/src/cli.rs +++ b/apps/gpclient/src/cli.rs @@ -1,3 +1,5 @@ +use std::{env::temp_dir, fs::File}; + use clap::{Parser, Subcommand}; use gpapi::utils::openssl; use log::{info, LevelFilter}; @@ -85,14 +87,29 @@ impl Cli { } } -fn init_logger() { - env_logger::builder().filter_level(LevelFilter::Info).init(); +fn init_logger(command: &CliCommand) { + let mut builder = env_logger::builder(); + builder.filter_level(LevelFilter::Info); + + // Output the log messages to a file if the command is the auth callback + if let CliCommand::LaunchGui(args) = command { + let auth_data = args.auth_data.as_deref().unwrap_or_default(); + if !auth_data.is_empty() { + if let Ok(log_file) = File::create(temp_dir().join("gpcallback.log")) { + let target = Box::new(log_file); + builder.target(env_logger::Target::Pipe(target)); + } + } + } + + builder.init(); } pub(crate) async fn run() { let cli = Cli::parse(); - init_logger(); + init_logger(&cli.command); + info!("gpclient started: {}", VERSION); if let Err(err) = cli.run().await { diff --git a/apps/gpclient/src/connect.rs b/apps/gpclient/src/connect.rs index d0536a23..15c24707 100644 --- a/apps/gpclient/src/connect.rs +++ b/apps/gpclient/src/connect.rs @@ -1,8 +1,10 @@ use std::{cell::RefCell, fs, sync::Arc}; +use anyhow::bail; use clap::Args; use common::vpn_utils::find_csd_wrapper; use gpapi::{ + auth::SamlAuthResult, clap::args::Os, credential::{Credential, PasswordCredential}, error::PortalError, @@ -19,9 +21,8 @@ use gpapi::{ use inquire::{Password, PasswordDisplayMode, Select, Text}; use log::info; use openconnect::Vpn; -use tokio::{io::AsyncReadExt, net::TcpListener}; -use crate::{cli::SharedArgs, GP_CLIENT_LOCK_FILE, GP_CLIENT_PORT_FILE}; +use crate::{cli::SharedArgs, GP_CLIENT_LOCK_FILE}; #[derive(Args)] pub(crate) struct ConnectArgs { @@ -37,6 +38,9 @@ pub(crate) struct ConnectArgs { #[arg(long, help = "Read the password from standard input")] passwd_on_stdin: bool, + #[arg(long, help = "Read the cookie from standard input")] + cookie_on_stdin: bool, + #[arg(long, short, help = "The VPNC script to use")] script: Option, @@ -89,7 +93,7 @@ pub(crate) struct ConnectArgs { #[arg(long, help = "Disable DTLS and ESP")] no_dtls: bool, - #[arg(long, help = "The HiDPI mode, useful for high resolution screens")] + #[arg(long, help = "The HiDPI mode, useful for high-resolution screens")] hidpi: bool, #[arg(long, help = "Do not reuse the remembered authentication cookie")] @@ -100,7 +104,7 @@ pub(crate) struct ConnectArgs { #[arg( long, - help = "Use the specified browser to authenticate, e.g., firefox, chromium, chrome, or the path to the browser" + help = "Use the specified browser to authenticate, e.g., `default`, `firefox`, `chrome`, `chromium`, or the path to the browser executable" )] browser: Option, } @@ -147,6 +151,10 @@ impl<'a> ConnectHandler<'a> { } pub(crate) async fn handle(&self) -> anyhow::Result<()> { + if self.args.default_browser && self.args.browser.is_some() { + bail!("Cannot use `--default-browser` and `--browser` options at the same time"); + } + self.latest_key_password.replace(self.args.key_password.clone()); loop { @@ -327,6 +335,10 @@ impl<'a> ConnectHandler<'a> { } async fn obtain_credential(&self, prelogin: &Prelogin, server: &str) -> anyhow::Result { + if self.args.cookie_on_stdin { + return read_cookie_from_stdin(); + } + let is_gateway = prelogin.is_gateway(); match prelogin { @@ -353,18 +365,9 @@ impl<'a> ConnectHandler<'a> { .launch() .await?; - if let Some(cred) = cred { - return Ok(cred); - } - - if !use_default_browser { - // This should never happen - unreachable!("SAML authentication failed without using the default browser"); - } - - info!("Waiting for the browser authentication to complete..."); - wait_credentials().await + Ok(cred) } + Prelogin::Standard(prelogin) => { let prefix = if is_gateway { "Gateway" } else { "Portal" }; println!("{} ({}: {})", prelogin.auth_message(), prefix, server); @@ -394,25 +397,17 @@ impl<'a> ConnectHandler<'a> { } } -async fn wait_credentials() -> anyhow::Result { - // Start a local server to receive the browser authentication data - let listener = TcpListener::bind("127.0.0.1:0").await?; - let port = listener.local_addr()?.port(); - - // Write the port to a file - fs::write(GP_CLIENT_PORT_FILE, port.to_string())?; - - info!("Listening authentication data on port {}", port); - let (mut socket, _) = listener.accept().await?; +fn read_cookie_from_stdin() -> anyhow::Result { + info!("Reading cookie from standard input"); - info!("Received the browser authentication data from the socket"); - let mut data = String::new(); - socket.read_to_string(&mut data).await?; + let mut cookie = String::new(); + std::io::stdin().read_line(&mut cookie)?; - // Remove the port file - fs::remove_file(GP_CLIENT_PORT_FILE)?; + let Ok(auth_result) = serde_json::from_str::(cookie.trim_end()) else { + bail!("Failed to parse auth data") + }; - Credential::from_gpcallback(&data) + Credential::try_from(auth_result) } fn write_pid_file() { diff --git a/apps/gpclient/src/launch_gui.rs b/apps/gpclient/src/launch_gui.rs index 4104d351..8cb6069d 100644 --- a/apps/gpclient/src/launch_gui.rs +++ b/apps/gpclient/src/launch_gui.rs @@ -9,15 +9,13 @@ use gpapi::{ use log::info; use tokio::io::AsyncWriteExt; -use crate::GP_CLIENT_PORT_FILE; - #[derive(Args)] pub(crate) struct LaunchGuiArgs { #[arg( required = false, help = "The authentication data, used for the default browser authentication" )] - auth_data: Option, + pub auth_data: Option, #[arg(long, help = "Launch the GUI minimized")] minimized: bool, } @@ -40,6 +38,7 @@ impl<'a> LaunchGuiHandler<'a> { let auth_data = self.args.auth_data.as_deref().unwrap_or_default(); if !auth_data.is_empty() { + info!("Received auth callback data"); // Process the authentication data, its format is `globalprotectcallback:` return feed_auth_data(auth_data).await; } @@ -81,16 +80,26 @@ impl<'a> LaunchGuiHandler<'a> { } async fn feed_auth_data(auth_data: &str) -> anyhow::Result<()> { - let _ = tokio::join!(feed_auth_data_gui(auth_data), feed_auth_data_cli(auth_data)); + let (res_gui, res_cli) = tokio::join!(feed_auth_data_gui(auth_data), feed_auth_data_cli(auth_data)); + if let Err(err) = res_gui { + info!("Failed to feed auth data to the GUI: {}", err); + } + + if let Err(err) = res_cli { + info!("Failed to feed auth data to the CLI: {}", err); + } // Cleanup the temporary file let html_file = temp_dir().join("gpauth.html"); - let _ = std::fs::remove_file(html_file); + if let Err(err) = std::fs::remove_file(&html_file) { + info!("Failed to remove {}: {}", html_file.display(), err); + } Ok(()) } async fn feed_auth_data_gui(auth_data: &str) -> anyhow::Result<()> { + info!("Feeding auth data to the GUI"); let service_endpoint = http_endpoint().await?; reqwest::Client::default() @@ -104,7 +113,10 @@ async fn feed_auth_data_gui(auth_data: &str) -> anyhow::Result<()> { } async fn feed_auth_data_cli(auth_data: &str) -> anyhow::Result<()> { - let port = tokio::fs::read_to_string(GP_CLIENT_PORT_FILE).await?; + info!("Feeding auth data to the CLI"); + + let port_file = temp_dir().join("gpcallback.port"); + let port = tokio::fs::read_to_string(port_file).await?; let mut stream = tokio::net::TcpStream::connect(format!("127.0.0.1:{}", port.trim())).await?; stream.write_all(auth_data.as_bytes()).await?; @@ -124,7 +136,7 @@ async fn try_active_gui() -> anyhow::Result<()> { Ok(()) } -pub fn get_log_file() -> anyhow::Result { +fn get_log_file() -> anyhow::Result { let dirs = ProjectDirs::from("com.yuezk", "GlobalProtect-openconnect", "gpclient") .ok_or_else(|| anyhow::anyhow!("Failed to get project dirs"))?; diff --git a/apps/gpclient/src/main.rs b/apps/gpclient/src/main.rs index 0c6e7634..41343ed2 100644 --- a/apps/gpclient/src/main.rs +++ b/apps/gpclient/src/main.rs @@ -4,7 +4,6 @@ mod disconnect; mod launch_gui; pub(crate) const GP_CLIENT_LOCK_FILE: &str = "/var/run/gpclient.lock"; -pub(crate) const GP_CLIENT_PORT_FILE: &str = "/var/run/gpclient.port"; #[tokio::main] async fn main() { diff --git a/crates/gpapi/src/auth.rs b/crates/gpapi/src/auth.rs index 245c7d01..86ddcd4f 100644 --- a/crates/gpapi/src/auth.rs +++ b/crates/gpapi/src/auth.rs @@ -85,7 +85,6 @@ impl SamlAuthData { return Ok(auth_data); } - info!("Parsing SAML auth data..."); let auth_data = decode_to_string(auth_data).map_err(|e| { warn!("Failed to decode SAML auth data: {}", e); AuthDataParseError::Invalid diff --git a/crates/gpapi/src/credential.rs b/crates/gpapi/src/credential.rs index 93a06df2..259385d3 100644 --- a/crates/gpapi/src/credential.rs +++ b/crates/gpapi/src/credential.rs @@ -1,9 +1,10 @@ use std::collections::HashMap; +use anyhow::bail; use serde::{Deserialize, Serialize}; use specta::Type; -use crate::auth::SamlAuthData; +use crate::auth::{SamlAuthData, SamlAuthResult}; #[derive(Debug, Serialize, Deserialize, Type, Clone)] #[serde(rename_all = "camelCase")] @@ -230,6 +231,17 @@ impl From for Credential { } } +impl TryFrom for Credential { + type Error = anyhow::Error; + + fn try_from(value: SamlAuthResult) -> anyhow::Result { + match value { + SamlAuthResult::Success(auth_data) => Ok(Self::from(auth_data)), + SamlAuthResult::Failure(err) => bail!(err), + } + } +} + impl From for Credential { fn from(value: PasswordCredential) -> Self { Self::Password(value) diff --git a/crates/gpapi/src/gateway/login.rs b/crates/gpapi/src/gateway/login.rs index 8c4cc684..0188e22b 100644 --- a/crates/gpapi/src/gateway/login.rs +++ b/crates/gpapi/src/gateway/login.rs @@ -29,7 +29,7 @@ pub async fn gateway_login(gateway: &str, cred: &Credential, gp_params: &GpParam params.extend(extra_params); params.insert("server", &gateway); - info!("Gateway login, user_agent: {}", gp_params.user_agent()); + info!("Perform gateway login, user_agent: {}", gp_params.user_agent()); let res = client .post(&login_url) diff --git a/crates/gpapi/src/portal/config.rs b/crates/gpapi/src/portal/config.rs index 709fd56f..f775d572 100644 --- a/crates/gpapi/src/portal/config.rs +++ b/crates/gpapi/src/portal/config.rs @@ -109,7 +109,7 @@ pub async fn retrieve_config(portal: &str, cred: &Credential, gp_params: &GpPara params.insert("server", &server); params.insert("host", &server); - info!("Portal config, user_agent: {}", gp_params.user_agent()); + info!("Retrieve the portal config, user_agent: {}", gp_params.user_agent()); let res = client .post(&url) diff --git a/crates/gpapi/src/portal/prelogin.rs b/crates/gpapi/src/portal/prelogin.rs index 3952a73e..4c10d2ab 100644 --- a/crates/gpapi/src/portal/prelogin.rs +++ b/crates/gpapi/src/portal/prelogin.rs @@ -116,6 +116,8 @@ pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result SamlAuthLauncher<'a> { } /// Launch the authenticator binary as the current user or SUDO_USER if available. - pub async fn launch(self) -> anyhow::Result> { + pub async fn launch(self) -> anyhow::Result { let mut auth_cmd = Command::new(GP_AUTH_BINARY); auth_cmd.arg(self.server); @@ -152,17 +152,10 @@ impl<'a> SamlAuthLauncher<'a> { .wait_with_output() .await?; - if self.default_browser { - return Ok(None); - } - let Ok(auth_result) = serde_json::from_slice::(&output.stdout) else { bail!("Failed to parse auth data") }; - match auth_result { - SamlAuthResult::Success(auth_data) => Ok(Some(Credential::from(auth_data))), - SamlAuthResult::Failure(msg) => bail!(msg), - } + Credential::try_from(auth_result) } } diff --git a/crates/gpapi/src/process/browser_authenticator.rs b/crates/gpapi/src/process/browser_authenticator.rs index ffdf0d87..9da35d06 100644 --- a/crates/gpapi/src/process/browser_authenticator.rs +++ b/crates/gpapi/src/process/browser_authenticator.rs @@ -19,7 +19,7 @@ impl BrowserAuthenticator<'_> { pub fn new_with_browser<'a>(auth_request: &'a str, browser: &'a str) -> BrowserAuthenticator<'a> { BrowserAuthenticator { auth_request, - browser: Some(browser), + browser: if browser == "default" { None } else { Some(browser) }, } }