Skip to content

Commit

Permalink
fix: enhance gpauth to support browser authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
yuezk committed Aug 15, 2024
1 parent 9317430 commit 57e20fe
Show file tree
Hide file tree
Showing 13 changed files with 171 additions and 66 deletions.
1 change: 1 addition & 0 deletions apps/gpauth/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[package]
name = "gpauth"
authors.workspace = true
version.workspace = true
edition.workspace = true
license.workspace = true
Expand Down
95 changes: 85 additions & 10 deletions apps/gpauth/src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::{env::temp_dir, fs, os::unix::fs::PermissionsExt};

use clap::Parser;
use gpapi::{
auth::{SamlAuthData, SamlAuthResult},
Expand All @@ -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<String>,
#[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<String>,
#[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<String>,
}

Expand Down Expand Up @@ -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(());
}

Expand Down Expand Up @@ -181,3 +224,35 @@ pub async fn run() {
std::process::exit(1);
}
}

async fn wait_auth_data() -> anyhow::Result<SamlAuthData> {
// 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)
}
23 changes: 20 additions & 3 deletions apps/gpclient/src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::{env::temp_dir, fs::File};

use clap::{Parser, Subcommand};
use gpapi::utils::openssl;
use log::{info, LevelFilter};
Expand Down Expand Up @@ -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 {
Expand Down
57 changes: 26 additions & 31 deletions apps/gpclient/src/connect.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 {
Expand All @@ -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<String>,

Expand Down Expand Up @@ -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")]
Expand All @@ -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<String>,
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -327,6 +335,10 @@ impl<'a> ConnectHandler<'a> {
}

async fn obtain_credential(&self, prelogin: &Prelogin, server: &str) -> anyhow::Result<Credential> {
if self.args.cookie_on_stdin {
return read_cookie_from_stdin();
}

let is_gateway = prelogin.is_gateway();

match prelogin {
Expand All @@ -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);
Expand Down Expand Up @@ -394,25 +397,17 @@ impl<'a> ConnectHandler<'a> {
}
}

async fn wait_credentials() -> anyhow::Result<Credential> {
// 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<Credential> {
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::<SamlAuthResult>(cookie.trim_end()) else {
bail!("Failed to parse auth data")
};

Credential::from_gpcallback(&data)
Credential::try_from(auth_result)
}

fn write_pid_file() {
Expand Down
26 changes: 19 additions & 7 deletions apps/gpclient/src/launch_gui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
pub auth_data: Option<String>,
#[arg(long, help = "Launch the GUI minimized")]
minimized: bool,
}
Expand All @@ -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:<data>`
return feed_auth_data(auth_data).await;
}
Expand Down Expand Up @@ -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()
Expand All @@ -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?;
Expand All @@ -124,7 +136,7 @@ async fn try_active_gui() -> anyhow::Result<()> {
Ok(())
}

pub fn get_log_file() -> anyhow::Result<PathBuf> {
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"))?;

Expand Down
1 change: 0 additions & 1 deletion apps/gpclient/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Loading

0 comments on commit 57e20fe

Please sign in to comment.