feat: gpauth support macos

This commit is contained in:
Kevin Yue
2025-01-05 23:42:03 +08:00
parent 0c9b8e6c63
commit 298d788752
46 changed files with 1161 additions and 856 deletions

View File

@@ -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"]

View File

@@ -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))

View File

@@ -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"),
}
}
}

View File

@@ -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(_))
}
}

View File

@@ -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;

View 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))
}

View File

@@ -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();

View File

@@ -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)

View File

@@ -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);

View 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)]

View File

@@ -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);
}
}