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,9 @@ sha256.workspace = true
|
||||
|
||||
tauri = { workspace = true, optional = true }
|
||||
clap = { workspace = true, optional = true }
|
||||
clap-verbosity-flag = { workspace = true, optional = true }
|
||||
|
||||
[features]
|
||||
tauri = ["dep:tauri"]
|
||||
clap = ["dep:clap"]
|
||||
clap = ["dep:clap", "dep:clap-verbosity-flag"]
|
||||
webview-auth = []
|
||||
|
||||
@@ -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,5 @@
|
||||
use clap_verbosity_flag::{LogLevel, Verbosity, VerbosityFilter};
|
||||
|
||||
use crate::error::PortalError;
|
||||
|
||||
pub mod args;
|
||||
@@ -8,7 +10,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 +28,41 @@ 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 VerbosityToCliArg {
|
||||
fn to_cli_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 VerbosityToCliArg for InfoLevelVerbosity {
|
||||
fn to_cli_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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(_))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -3,7 +3,12 @@ use std::process::Stdio;
|
||||
use anyhow::bail;
|
||||
use tokio::process::Command;
|
||||
|
||||
use crate::{auth::SamlAuthResult, credential::Credential, GP_AUTH_BINARY};
|
||||
use crate::{
|
||||
auth::SamlAuthResult,
|
||||
clap::{InfoLevelVerbosity, VerbosityToCliArg},
|
||||
credential::Credential,
|
||||
GP_AUTH_BINARY,
|
||||
};
|
||||
|
||||
use super::command_traits::CommandExt;
|
||||
|
||||
@@ -23,6 +28,7 @@ pub struct SamlAuthLauncher<'a> {
|
||||
#[cfg(feature = "webview-auth")]
|
||||
default_browser: bool,
|
||||
browser: Option<&'a str>,
|
||||
verbose: Option<&'a InfoLevelVerbosity>,
|
||||
}
|
||||
|
||||
impl<'a> SamlAuthLauncher<'a> {
|
||||
@@ -43,6 +49,7 @@ impl<'a> SamlAuthLauncher<'a> {
|
||||
#[cfg(feature = "webview-auth")]
|
||||
default_browser: false,
|
||||
browser: None,
|
||||
verbose: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +111,11 @@ impl<'a> SamlAuthLauncher<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn verbose(mut self, verbose: &'a InfoLevelVerbosity) -> Self {
|
||||
self.verbose = Some(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 +168,11 @@ impl<'a> SamlAuthLauncher<'a> {
|
||||
auth_cmd.arg("--browser").arg(browser);
|
||||
}
|
||||
|
||||
if let Some(verbose) = self.verbose {
|
||||
let arg = verbose.to_cli_arg();
|
||||
arg.map(|arg| auth_cmd.arg(arg));
|
||||
}
|
||||
|
||||
let mut non_root_cmd = auth_cmd.into_non_root()?;
|
||||
let output = non_root_cmd
|
||||
.kill_on_drop(true)
|
||||
|
||||
@@ -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