mirror of
https://github.com/yuezk/GlobalProtect-openconnect.git
synced 2025-05-20 07:26:58 -04:00
feat: gpauth support macos
This commit is contained in:
@@ -12,6 +12,7 @@ dns-lookup.workspace = true
|
||||
log.workspace = true
|
||||
reqwest.workspace = true
|
||||
openssl.workspace = true
|
||||
version-compare = "0.2"
|
||||
pem.workspace = true
|
||||
roxmltree.workspace = true
|
||||
serde.workspace = true
|
||||
@@ -33,8 +34,13 @@ sha256.workspace = true
|
||||
|
||||
tauri = { workspace = true, optional = true }
|
||||
clap = { workspace = true, optional = true }
|
||||
clap-verbosity-flag = { workspace = true, optional = true }
|
||||
|
||||
env_logger = { workspace = true, optional = true }
|
||||
log-reload = { version = "0.1", optional = true }
|
||||
|
||||
[features]
|
||||
tauri = ["dep:tauri"]
|
||||
clap = ["dep:clap"]
|
||||
clap = ["dep:clap", "dep:clap-verbosity-flag"]
|
||||
webview-auth = []
|
||||
logger = ["dep:env_logger", "dep:log-reload"]
|
||||
|
@@ -72,15 +72,12 @@ impl SamlAuthData {
|
||||
let prelogin_cookie = parse_xml_tag(html, "prelogin-cookie");
|
||||
let portal_userauthcookie = parse_xml_tag(html, "portal-userauthcookie");
|
||||
|
||||
SamlAuthData::new(username, prelogin_cookie, portal_userauthcookie).map_err(|e| {
|
||||
warn!("Failed to parse auth data: {}", e);
|
||||
AuthDataParseError::Invalid
|
||||
})
|
||||
}
|
||||
Some(status) => {
|
||||
warn!("Found invalid auth status: {}", status);
|
||||
Err(AuthDataParseError::Invalid)
|
||||
SamlAuthData::new(username, prelogin_cookie, portal_userauthcookie).map_err(AuthDataParseError::Invalid)
|
||||
}
|
||||
Some(status) => Err(AuthDataParseError::Invalid(anyhow::anyhow!(
|
||||
"SAML auth status: {}",
|
||||
status
|
||||
))),
|
||||
None => Err(AuthDataParseError::NotFound),
|
||||
}
|
||||
}
|
||||
@@ -100,7 +97,7 @@ impl SamlAuthData {
|
||||
let auth_data: SamlAuthData = serde_urlencoded::from_str(auth_data.borrow()).map_err(|e| {
|
||||
warn!("Failed to parse token auth data: {}", e);
|
||||
warn!("Auth data: {}", auth_data);
|
||||
AuthDataParseError::Invalid
|
||||
AuthDataParseError::Invalid(anyhow::anyhow!(e))
|
||||
})?;
|
||||
|
||||
return Ok(auth_data);
|
||||
@@ -108,7 +105,7 @@ impl SamlAuthData {
|
||||
|
||||
let auth_data = decode_to_string(auth_data).map_err(|e| {
|
||||
warn!("Failed to decode SAML auth data: {}", e);
|
||||
AuthDataParseError::Invalid
|
||||
AuthDataParseError::Invalid(anyhow::anyhow!(e))
|
||||
})?;
|
||||
let auth_data = Self::from_html(&auth_data)?;
|
||||
|
||||
@@ -128,7 +125,7 @@ impl SamlAuthData {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_xml_tag(html: &str, tag: &str) -> Option<String> {
|
||||
fn parse_xml_tag(html: &str, tag: &str) -> Option<String> {
|
||||
let re = Regex::new(&format!("<{}>(.*)</{}>", tag, tag)).unwrap();
|
||||
re.captures(html)
|
||||
.and_then(|captures| captures.get(1))
|
||||
|
@@ -1,3 +1,6 @@
|
||||
use clap_verbosity_flag::{LogLevel, Verbosity, VerbosityFilter};
|
||||
use log::Level;
|
||||
|
||||
use crate::error::PortalError;
|
||||
|
||||
pub mod args;
|
||||
@@ -8,7 +11,7 @@ pub trait Args {
|
||||
}
|
||||
|
||||
pub fn handle_error(err: anyhow::Error, args: &impl Args) {
|
||||
eprintln!("\nError: {}", err);
|
||||
eprintln!("\nError: {:?}", err);
|
||||
|
||||
let Some(err) = err.downcast_ref::<PortalError>() else {
|
||||
return;
|
||||
@@ -26,3 +29,53 @@ pub fn handle_error(err: anyhow::Error, args: &impl Args) {
|
||||
eprintln!("{} --ignore-tls-errors {}\n", args[0], args[1..].join(" "));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InfoLevel;
|
||||
|
||||
pub type InfoLevelVerbosity = Verbosity<InfoLevel>;
|
||||
|
||||
impl LogLevel for InfoLevel {
|
||||
fn default_filter() -> VerbosityFilter {
|
||||
VerbosityFilter::Info
|
||||
}
|
||||
|
||||
fn verbose_help() -> Option<&'static str> {
|
||||
Some("Enable verbose output, -v for debug, -vv for trace")
|
||||
}
|
||||
|
||||
fn quiet_help() -> Option<&'static str> {
|
||||
Some("Decrease logging verbosity, -q for warnings, -qq for errors")
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ToVerboseArg {
|
||||
fn to_verbose_arg(&self) -> Option<&'static str>;
|
||||
}
|
||||
|
||||
/// Convert the verbosity to the CLI argument value
|
||||
/// The default verbosity is `Info`, which means no argument is needed
|
||||
impl ToVerboseArg for InfoLevelVerbosity {
|
||||
fn to_verbose_arg(&self) -> Option<&'static str> {
|
||||
match self.filter() {
|
||||
VerbosityFilter::Off => Some("-qqq"),
|
||||
VerbosityFilter::Error => Some("-qq"),
|
||||
VerbosityFilter::Warn => Some("-q"),
|
||||
VerbosityFilter::Info => None,
|
||||
VerbosityFilter::Debug => Some("-v"),
|
||||
VerbosityFilter::Trace => Some("-vv"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToVerboseArg for Level {
|
||||
fn to_verbose_arg(&self) -> Option<&'static str> {
|
||||
match self {
|
||||
Level::Error => Some("-qq"),
|
||||
Level::Warn => Some("-q"),
|
||||
Level::Info => None,
|
||||
Level::Debug => Some("-v"),
|
||||
Level::Trace => Some("-vv"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -26,12 +26,12 @@ impl PortalError {
|
||||
pub enum AuthDataParseError {
|
||||
#[error("No auth data found")]
|
||||
NotFound,
|
||||
#[error("Invalid auth data")]
|
||||
Invalid,
|
||||
#[error(transparent)]
|
||||
Invalid(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
impl AuthDataParseError {
|
||||
pub fn is_invalid(&self) -> bool {
|
||||
matches!(self, AuthDataParseError::Invalid)
|
||||
matches!(self, AuthDataParseError::Invalid(_))
|
||||
}
|
||||
}
|
||||
|
@@ -8,6 +8,9 @@ pub mod process;
|
||||
pub mod service;
|
||||
pub mod utils;
|
||||
|
||||
#[cfg(feature = "logger")]
|
||||
pub mod logger;
|
||||
|
||||
#[cfg(feature = "clap")]
|
||||
pub mod clap;
|
||||
|
||||
|
49
crates/gpapi/src/logger.rs
Normal file
49
crates/gpapi/src/logger.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use anyhow::bail;
|
||||
use env_logger::Logger;
|
||||
use log::Level;
|
||||
use log_reload::{ReloadHandle, ReloadLog};
|
||||
|
||||
static LOG_HANDLE: OnceLock<ReloadHandle<log_reload::LevelFilter<Logger>>> = OnceLock::new();
|
||||
|
||||
pub fn init(level: Level) -> anyhow::Result<()> {
|
||||
// Initialize the env_logger and global max level to trace, the logs will be
|
||||
// filtered by the outer logger
|
||||
let logger = env_logger::builder().filter_level(log::LevelFilter::Trace).build();
|
||||
init_with_logger(level, logger)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn init_with_logger(level: Level, logger: Logger) -> anyhow::Result<()> {
|
||||
if let Some(_) = LOG_HANDLE.get() {
|
||||
bail!("Logger already initialized")
|
||||
} else {
|
||||
log::set_max_level(log::LevelFilter::Trace);
|
||||
|
||||
// Create a new logger that will filter the logs based on the max level
|
||||
let level_filter_logger = log_reload::LevelFilter::new(level, logger);
|
||||
|
||||
let reload_log = ReloadLog::new(level_filter_logger);
|
||||
let handle = reload_log.handle();
|
||||
|
||||
// Register the logger to be used by the log crate
|
||||
log::set_boxed_logger(Box::new(reload_log))?;
|
||||
LOG_HANDLE
|
||||
.set(handle)
|
||||
.map_err(|_| anyhow::anyhow!("Failed to set the logger"))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_max_level(level: Level) -> anyhow::Result<()> {
|
||||
let Some(handle) = LOG_HANDLE.get() else {
|
||||
bail!("Logger not initialized")
|
||||
};
|
||||
|
||||
handle
|
||||
.modify(|logger| logger.set_level(level))
|
||||
.map_err(|e| anyhow::anyhow!(e))
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
use anyhow::bail;
|
||||
use dns_lookup::lookup_addr;
|
||||
use log::{debug, info, warn};
|
||||
use log::{info, warn};
|
||||
use reqwest::{Client, StatusCode};
|
||||
use roxmltree::{Document, Node};
|
||||
use serde::Serialize;
|
||||
@@ -135,8 +135,6 @@ pub async fn retrieve_config(portal: &str, cred: &Credential, gp_params: &GpPara
|
||||
bail!(PortalError::ConfigError("Empty portal config response".to_string()))
|
||||
}
|
||||
|
||||
debug!("Portal config response: {}", res_xml);
|
||||
|
||||
let doc = Document::parse(&res_xml).map_err(|e| PortalError::ConfigError(e.to_string()))?;
|
||||
let root = doc.root();
|
||||
|
||||
|
@@ -23,6 +23,7 @@ pub struct SamlAuthLauncher<'a> {
|
||||
#[cfg(feature = "webview-auth")]
|
||||
default_browser: bool,
|
||||
browser: Option<&'a str>,
|
||||
verbose: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> SamlAuthLauncher<'a> {
|
||||
@@ -43,6 +44,7 @@ impl<'a> SamlAuthLauncher<'a> {
|
||||
#[cfg(feature = "webview-auth")]
|
||||
default_browser: false,
|
||||
browser: None,
|
||||
verbose: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +106,11 @@ impl<'a> SamlAuthLauncher<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn verbose(mut self, verbose: Option<&'a str>) -> Self {
|
||||
self.verbose = verbose;
|
||||
self
|
||||
}
|
||||
|
||||
/// Launch the authenticator binary as the current user or SUDO_USER if available.
|
||||
pub async fn launch(self) -> anyhow::Result<Credential> {
|
||||
let mut auth_cmd = Command::new(GP_AUTH_BINARY);
|
||||
@@ -156,6 +163,10 @@ impl<'a> SamlAuthLauncher<'a> {
|
||||
auth_cmd.arg("--browser").arg(browser);
|
||||
}
|
||||
|
||||
if let Some(verbose) = self.verbose {
|
||||
auth_cmd.arg(verbose);
|
||||
}
|
||||
|
||||
let mut non_root_cmd = auth_cmd.into_non_root()?;
|
||||
let output = non_root_cmd
|
||||
.kill_on_drop(true)
|
||||
|
@@ -10,26 +10,28 @@ use crate::GP_SERVICE_BINARY;
|
||||
|
||||
use super::command_traits::CommandExt;
|
||||
|
||||
pub struct ServiceLauncher {
|
||||
pub struct ServiceLauncher<'a> {
|
||||
program: PathBuf,
|
||||
minimized: bool,
|
||||
env_file: Option<String>,
|
||||
log_file: Option<String>,
|
||||
verbose: Option<&'a str>
|
||||
}
|
||||
|
||||
impl Default for ServiceLauncher {
|
||||
impl Default for ServiceLauncher<'_> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ServiceLauncher {
|
||||
impl<'a> ServiceLauncher<'a> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
program: GP_SERVICE_BINARY.into(),
|
||||
minimized: false,
|
||||
env_file: None,
|
||||
log_file: None,
|
||||
verbose: None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +50,11 @@ impl ServiceLauncher {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn verbose(mut self, verbose: Option<&'a str>) -> Self {
|
||||
self.verbose = verbose;
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn launch(&self) -> anyhow::Result<ExitStatus> {
|
||||
let mut cmd = Command::new_pkexec(&self.program);
|
||||
|
||||
@@ -59,6 +66,10 @@ impl ServiceLauncher {
|
||||
cmd.arg("--env-file").arg(env_file);
|
||||
}
|
||||
|
||||
if let Some(verbose) = self.verbose {
|
||||
cmd.arg(verbose);
|
||||
}
|
||||
|
||||
if let Some(log_file) = &self.log_file {
|
||||
let log_file = File::create(log_file)?;
|
||||
let stdio = Stdio::from(log_file);
|
||||
|
@@ -206,11 +206,15 @@ impl ConnectRequest {
|
||||
#[derive(Debug, Deserialize, Serialize, Type)]
|
||||
pub struct DisconnectRequest;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct UpdateLogLevelRequest(pub String);
|
||||
|
||||
/// Requests that can be sent to the service
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub enum WsRequest {
|
||||
Connect(Box<ConnectRequest>),
|
||||
Disconnect(DisconnectRequest),
|
||||
UpdateLogLevel(UpdateLogLevelRequest),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
|
@@ -1,9 +1,12 @@
|
||||
use std::path::Path;
|
||||
|
||||
use log::{info, warn};
|
||||
use regex::Regex;
|
||||
use tempfile::NamedTempFile;
|
||||
use version_compare::{compare_to, Cmp};
|
||||
|
||||
pub fn openssl_conf() -> String {
|
||||
let option = "UnsafeLegacyServerConnect";
|
||||
let option = get_openssl_option();
|
||||
|
||||
format!(
|
||||
"openssl_conf = openssl_init
|
||||
@@ -47,3 +50,58 @@ pub fn fix_openssl_env() -> anyhow::Result<NamedTempFile> {
|
||||
|
||||
Ok(openssl_conf)
|
||||
}
|
||||
|
||||
// See: https://stackoverflow.com/questions/75763525/curl-35-error0a000152ssl-routinesunsafe-legacy-renegotiation-disabled
|
||||
fn get_openssl_option() -> &'static str {
|
||||
let version_str = openssl::version::version();
|
||||
let default_option = "UnsafeLegacyServerConnect";
|
||||
|
||||
let Some(version) = extract_openssl_version(version_str) else {
|
||||
warn!("Failed to extract OpenSSL version from '{}'", version_str);
|
||||
return default_option;
|
||||
};
|
||||
|
||||
let older_than_3_0_4 = match compare_to(version, "3.0.4", Cmp::Lt) {
|
||||
Ok(result) => result,
|
||||
Err(_) => {
|
||||
warn!("Failed to compare OpenSSL version: {}", version);
|
||||
return default_option;
|
||||
}
|
||||
};
|
||||
|
||||
if older_than_3_0_4 {
|
||||
info!("Using 'UnsafeLegacyRenegotiation' option");
|
||||
"UnsafeLegacyRenegotiation"
|
||||
} else {
|
||||
info!("Using 'UnsafeLegacyServerConnect' option");
|
||||
default_option
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_openssl_version(version: &str) -> Option<&str> {
|
||||
let re = Regex::new(r"OpenSSL (\d+\.\d+\.\d+[^\s]*)").unwrap();
|
||||
re.captures(version).and_then(|caps| caps.get(1)).map(|m| m.as_str())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_extract_version() {
|
||||
let input = "OpenSSL 3.4.0 22 Oct 2024 (Library: OpenSSL 3.4.0 22 Oct 2024)";
|
||||
assert_eq!(extract_openssl_version(input), Some("3.4.0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_format() {
|
||||
let input = "OpenSSL 1.1.1t 7 Feb 2023";
|
||||
assert_eq!(extract_openssl_version(input), Some("1.1.1t"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_input() {
|
||||
let input = "Invalid string without version";
|
||||
assert_eq!(extract_openssl_version(input), None);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user