mirror of
https://github.com/yuezk/GlobalProtect-openconnect.git
synced 2025-05-20 07:26:58 -04:00
upgrade gpauth
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
[package]
|
||||
name = "gpapi"
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license = "MIT"
|
||||
@@ -29,14 +30,10 @@ uzers.workspace = true
|
||||
serde_urlencoded.workspace = true
|
||||
md5.workspace = true
|
||||
sha256.workspace = true
|
||||
which.workspace = true
|
||||
|
||||
tauri = { workspace = true, optional = true }
|
||||
clap = { workspace = true, optional = true }
|
||||
open = { version = "5", optional = true }
|
||||
webbrowser = { version = "1", optional = true }
|
||||
|
||||
[features]
|
||||
tauri = ["dep:tauri"]
|
||||
clap = ["dep:clap"]
|
||||
browser-auth = ["dep:open", "dep:webbrowser"]
|
||||
|
@@ -1,11 +1,14 @@
|
||||
use std::borrow::{Borrow, Cow};
|
||||
|
||||
use anyhow::bail;
|
||||
use log::{info, warn};
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{error::AuthDataParseError, utils::base64::decode_to_string};
|
||||
|
||||
pub type AuthDataParseResult = anyhow::Result<SamlAuthData, AuthDataParseError>;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SamlAuthData {
|
||||
@@ -33,33 +36,51 @@ impl SamlAuthResult {
|
||||
}
|
||||
|
||||
impl SamlAuthData {
|
||||
pub fn new(username: String, prelogin_cookie: Option<String>, portal_userauthcookie: Option<String>) -> Self {
|
||||
Self {
|
||||
username,
|
||||
prelogin_cookie,
|
||||
portal_userauthcookie,
|
||||
token: None,
|
||||
pub fn new(
|
||||
username: Option<String>,
|
||||
prelogin_cookie: Option<String>,
|
||||
portal_userauthcookie: Option<String>,
|
||||
) -> anyhow::Result<Self> {
|
||||
let username = username.unwrap_or_default();
|
||||
if username.is_empty() {
|
||||
bail!("Invalid username: <empty>");
|
||||
}
|
||||
|
||||
let prelogin_cookie = prelogin_cookie.unwrap_or_default();
|
||||
let portal_userauthcookie = portal_userauthcookie.unwrap_or_default();
|
||||
|
||||
if prelogin_cookie.len() <= 5 && portal_userauthcookie.len() <= 5 {
|
||||
bail!(
|
||||
"Invalid prelogin-cookie: {}, portal-userauthcookie: {}",
|
||||
prelogin_cookie,
|
||||
portal_userauthcookie
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
username,
|
||||
prelogin_cookie: Some(prelogin_cookie),
|
||||
portal_userauthcookie: Some(portal_userauthcookie),
|
||||
token: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_html(html: &str) -> anyhow::Result<SamlAuthData, AuthDataParseError> {
|
||||
pub fn from_html(html: &str) -> AuthDataParseResult {
|
||||
match parse_xml_tag(html, "saml-auth-status") {
|
||||
Some(saml_status) if saml_status == "1" => {
|
||||
Some(status) if status == "1" => {
|
||||
let username = parse_xml_tag(html, "saml-username");
|
||||
let prelogin_cookie = parse_xml_tag(html, "prelogin-cookie");
|
||||
let portal_userauthcookie = parse_xml_tag(html, "portal-userauthcookie");
|
||||
|
||||
if SamlAuthData::check(&username, &prelogin_cookie, &portal_userauthcookie) {
|
||||
Ok(SamlAuthData::new(
|
||||
username.unwrap(),
|
||||
prelogin_cookie,
|
||||
portal_userauthcookie,
|
||||
))
|
||||
} else {
|
||||
Err(AuthDataParseError::Invalid)
|
||||
}
|
||||
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)
|
||||
}
|
||||
Some(_) => Err(AuthDataParseError::Invalid),
|
||||
None => Err(AuthDataParseError::NotFound),
|
||||
}
|
||||
}
|
||||
@@ -105,27 +126,6 @@ impl SamlAuthData {
|
||||
pub fn token(&self) -> Option<&str> {
|
||||
self.token.as_deref()
|
||||
}
|
||||
|
||||
pub fn check(
|
||||
username: &Option<String>,
|
||||
prelogin_cookie: &Option<String>,
|
||||
portal_userauthcookie: &Option<String>,
|
||||
) -> bool {
|
||||
let username_valid = username.as_ref().is_some_and(|username| !username.is_empty());
|
||||
let prelogin_cookie_valid = prelogin_cookie.as_ref().is_some_and(|val| val.len() > 5);
|
||||
let portal_userauthcookie_valid = portal_userauthcookie.as_ref().is_some_and(|val| val.len() > 5);
|
||||
|
||||
let is_valid = username_valid && (prelogin_cookie_valid || portal_userauthcookie_valid);
|
||||
|
||||
if !is_valid {
|
||||
warn!(
|
||||
"Invalid SAML auth data: username: {:?}, prelogin-cookie: {:?}, portal-userauthcookie: {:?}",
|
||||
username, prelogin_cookie, portal_userauthcookie
|
||||
);
|
||||
}
|
||||
|
||||
is_valid
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_xml_tag(html: &str, tag: &str) -> Option<String> {
|
||||
|
@@ -1 +1,28 @@
|
||||
use crate::error::PortalError;
|
||||
|
||||
pub mod args;
|
||||
|
||||
pub trait Args {
|
||||
fn fix_openssl(&self) -> bool;
|
||||
fn ignore_tls_errors(&self) -> bool;
|
||||
}
|
||||
|
||||
pub fn handle_error(err: anyhow::Error, args: &impl Args) {
|
||||
eprintln!("\nError: {}", err);
|
||||
|
||||
let Some(err) = err.downcast_ref::<PortalError>() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if err.is_legacy_openssl_error() && !args.fix_openssl() {
|
||||
eprintln!("\nRe-run it with the `--fix-openssl` option to work around this issue, e.g.:\n");
|
||||
let args = std::env::args().collect::<Vec<_>>();
|
||||
eprintln!("{} --fix-openssl {}\n", args[0], args[1..].join(" "));
|
||||
}
|
||||
|
||||
if err.is_tls_error() && !args.ignore_tls_errors() {
|
||||
eprintln!("\nRe-run it with the `--ignore-tls-errors` option to ignore the certificate error, e.g.:\n");
|
||||
let args = std::env::args().collect::<Vec<_>>();
|
||||
eprintln!("{} --ignore-tls-errors {}\n", args[0], args[1..].join(" "));
|
||||
}
|
||||
}
|
||||
|
@@ -7,7 +7,19 @@ pub enum PortalError {
|
||||
#[error("Portal config error: {0}")]
|
||||
ConfigError(String),
|
||||
#[error("Network error: {0}")]
|
||||
NetworkError(String),
|
||||
NetworkError(#[from] reqwest::Error),
|
||||
#[error("TLS error")]
|
||||
TlsError,
|
||||
}
|
||||
|
||||
impl PortalError {
|
||||
pub fn is_legacy_openssl_error(&self) -> bool {
|
||||
format!("{:?}", self).contains("unsafe legacy renegotiation")
|
||||
}
|
||||
|
||||
pub fn is_tls_error(&self) -> bool {
|
||||
matches!(self, PortalError::TlsError) || format!("{:?}", self).contains("certificate verify failed")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
@@ -17,3 +29,9 @@ pub enum AuthDataParseError {
|
||||
#[error("Invalid auth data")]
|
||||
Invalid,
|
||||
}
|
||||
|
||||
impl AuthDataParseError {
|
||||
pub fn is_invalid(&self) -> bool {
|
||||
matches!(self, AuthDataParseError::Invalid)
|
||||
}
|
||||
}
|
||||
|
@@ -36,7 +36,7 @@ pub async fn gateway_login(gateway: &str, cred: &Credential, gp_params: &GpParam
|
||||
.form(¶ms)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!(PortalError::NetworkError(e.to_string())))?;
|
||||
.map_err(|e| anyhow::anyhow!(PortalError::NetworkError(e)))?;
|
||||
|
||||
let res = parse_gp_response(res).await.map_err(|err| {
|
||||
warn!("{err}");
|
||||
|
@@ -16,6 +16,7 @@ pub const GP_API_KEY: &[u8; 32] = &[0; 32];
|
||||
|
||||
pub const GP_USER_AGENT: &str = "PAN GlobalProtect";
|
||||
pub const GP_SERVICE_LOCK_FILE: &str = "/var/run/gpservice.lock";
|
||||
pub const GP_CALLBACK_PORT_FILENAME: &str = "gpcallback.port";
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
pub const GP_CLIENT_BINARY: &str = "/usr/bin/gpclient";
|
||||
|
@@ -116,7 +116,7 @@ pub async fn retrieve_config(portal: &str, cred: &Credential, gp_params: &GpPara
|
||||
.form(¶ms)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!(PortalError::NetworkError(e.to_string())))?;
|
||||
.map_err(|e| anyhow::anyhow!(PortalError::NetworkError(e)))?;
|
||||
|
||||
let res_xml = parse_gp_response(res).await.or_else(|err| {
|
||||
if err.status == StatusCode::NOT_FOUND {
|
||||
|
@@ -116,14 +116,12 @@ pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prel
|
||||
|
||||
let client = Client::try_from(gp_params)?;
|
||||
|
||||
info!("Perform prelogin, user_agent: {}", gp_params.user_agent());
|
||||
|
||||
let res = client
|
||||
.post(&prelogin_url)
|
||||
.form(¶ms)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!(PortalError::NetworkError(e.to_string())))?;
|
||||
.map_err(|e| anyhow::anyhow!(PortalError::NetworkError(e)))?;
|
||||
|
||||
let res_xml = parse_gp_response(res).await.or_else(|err| {
|
||||
if err.status == StatusCode::NOT_FOUND {
|
||||
|
@@ -1,77 +0,0 @@
|
||||
use std::{borrow::Cow, env::temp_dir, fs, io::Write, os::unix::fs::PermissionsExt};
|
||||
|
||||
use anyhow::bail;
|
||||
use log::{info, warn};
|
||||
|
||||
pub struct BrowserAuthenticator<'a> {
|
||||
auth_request: &'a str,
|
||||
browser: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl BrowserAuthenticator<'_> {
|
||||
pub fn new(auth_request: &str) -> BrowserAuthenticator {
|
||||
BrowserAuthenticator {
|
||||
auth_request,
|
||||
browser: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_browser<'a>(auth_request: &'a str, browser: &'a str) -> BrowserAuthenticator<'a> {
|
||||
let browser = browser.trim();
|
||||
BrowserAuthenticator {
|
||||
auth_request,
|
||||
browser: if browser.is_empty() || browser == "default" {
|
||||
None
|
||||
} else {
|
||||
Some(browser)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn authenticate(&self) -> anyhow::Result<()> {
|
||||
let path = if self.auth_request.starts_with("http") {
|
||||
Cow::Borrowed(self.auth_request)
|
||||
} else {
|
||||
let html_file = temp_dir().join("gpauth.html");
|
||||
|
||||
// Remove the file and error if permission denied
|
||||
if let Err(err) = fs::remove_file(&html_file) {
|
||||
if err.kind() != std::io::ErrorKind::NotFound {
|
||||
warn!("Failed to remove the temporary file: {}", err);
|
||||
bail!("Please remove the file manually: {:?}", html_file);
|
||||
}
|
||||
}
|
||||
|
||||
let mut file = fs::File::create(&html_file)?;
|
||||
|
||||
file.set_permissions(fs::Permissions::from_mode(0o600))?;
|
||||
file.write_all(self.auth_request.as_bytes())?;
|
||||
|
||||
Cow::Owned(html_file.to_string_lossy().to_string())
|
||||
};
|
||||
|
||||
if let Some(browser) = self.browser {
|
||||
let app = find_browser_path(browser);
|
||||
|
||||
info!("Launching browser: {}", app);
|
||||
open::with_detached(path.as_ref(), app)?;
|
||||
} else {
|
||||
info!("Launching the default browser...");
|
||||
webbrowser::open(path.as_ref())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn find_browser_path(browser: &str) -> String {
|
||||
if browser == "chrome" {
|
||||
which::which("google-chrome-stable")
|
||||
.or_else(|_| which::which("google-chrome"))
|
||||
.or_else(|_| which::which("chromium"))
|
||||
.map(|path| path.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|_| browser.to_string())
|
||||
} else {
|
||||
browser.into()
|
||||
}
|
||||
}
|
@@ -2,8 +2,6 @@ pub(crate) mod command_traits;
|
||||
pub(crate) mod gui_helper_launcher;
|
||||
|
||||
pub mod auth_launcher;
|
||||
#[cfg(feature = "browser-auth")]
|
||||
pub mod browser_authenticator;
|
||||
pub mod gui_launcher;
|
||||
pub mod hip_launcher;
|
||||
pub mod service_launcher;
|
||||
|
@@ -7,6 +7,4 @@ use super::vpn_state::VpnState;
|
||||
pub enum WsEvent {
|
||||
VpnState(VpnState),
|
||||
ActiveGui,
|
||||
/// External authentication data
|
||||
AuthData(String),
|
||||
}
|
||||
|
@@ -7,17 +7,12 @@ use tokio::process::Command;
|
||||
|
||||
pub trait WindowExt {
|
||||
fn raise(&self) -> anyhow::Result<()>;
|
||||
fn hide_menu(&self);
|
||||
}
|
||||
|
||||
impl WindowExt for WebviewWindow {
|
||||
fn raise(&self) -> anyhow::Result<()> {
|
||||
raise_window(self)
|
||||
}
|
||||
|
||||
fn hide_menu(&self) {
|
||||
hide_menu(self);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn raise_window(win: &WebviewWindow) -> anyhow::Result<()> {
|
||||
@@ -40,7 +35,7 @@ pub fn raise_window(win: &WebviewWindow) -> anyhow::Result<()> {
|
||||
|
||||
// Calling window.show() on Windows will cause the menu to be shown.
|
||||
// We need to hide it again.
|
||||
hide_menu(win);
|
||||
win.hide_menu()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -76,22 +71,3 @@ async fn wmctrl_try_raise_window(title: &str) -> anyhow::Result<ExitStatus> {
|
||||
|
||||
Ok(exit_status)
|
||||
}
|
||||
|
||||
fn hide_menu(win: &WebviewWindow) {
|
||||
// let menu_handle = win.menu_handle();
|
||||
|
||||
// tokio::spawn(async move {
|
||||
// loop {
|
||||
// let menu_visible = menu_handle.is_visible().unwrap_or(false);
|
||||
|
||||
// if !menu_visible {
|
||||
// break;
|
||||
// }
|
||||
|
||||
// if menu_visible {
|
||||
// let _ = menu_handle.hide();
|
||||
// tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
}
|
||||
|
Reference in New Issue
Block a user