Enhancements and Bug Fixes: Align Pre-login Behavior, TLS Error Ignorance, GUI Auto-Launch, and Documentation Improvements (#291)

This commit is contained in:
Kevin Yue
2024-01-21 10:43:47 +08:00
committed by GitHub
parent 03f8c98cb5
commit 8bc4049a0f
17 changed files with 323 additions and 204 deletions

View File

@@ -8,7 +8,7 @@ license.workspace = true
tauri-build = { version = "1.5", features = [] }
[dependencies]
gpapi = { path = "../../crates/gpapi", features = ["tauri"] }
gpapi = { path = "../../crates/gpapi", features = ["tauri", "clap"] }
anyhow.workspace = true
clap.workspace = true
env_logger.workspace = true

View File

@@ -7,6 +7,7 @@ use std::{
use anyhow::bail;
use gpapi::{
auth::SamlAuthData,
gp_params::GpParams,
portal::{prelogin, Prelogin},
utils::{redact::redact_uri, window::WindowExt},
};
@@ -18,11 +19,13 @@ use tokio_util::sync::CancellationToken;
use webkit2gtk::{
gio::Cancellable,
glib::{GString, TimeSpan},
LoadEvent, SettingsExt, URIResponse, URIResponseExt, WebContextExt, WebResource, WebResourceExt,
WebView, WebViewExt, WebsiteDataManagerExtManual, WebsiteDataTypes,
LoadEvent, SettingsExt, TLSErrorsPolicy, URIResponse, URIResponseExt, WebContextExt, WebResource,
WebResourceExt, WebView, WebViewExt, WebsiteDataManagerExtManual, WebsiteDataTypes,
};
enum AuthDataError {
/// Failed to load page due to TLS error
TlsError,
/// 1. Found auth data in headers/body but it's invalid
/// 2. Loaded an empty page, failed to load page. etc.
Invalid,
@@ -37,6 +40,7 @@ pub(crate) struct AuthWindow<'a> {
server: &'a str,
saml_request: &'a str,
user_agent: &'a str,
gp_params: Option<GpParams>,
clean: bool,
}
@@ -47,6 +51,7 @@ impl<'a> AuthWindow<'a> {
server: "",
saml_request: "",
user_agent: "",
gp_params: None,
clean: false,
}
}
@@ -66,6 +71,11 @@ impl<'a> AuthWindow<'a> {
self
}
pub fn gp_params(mut self, gp_params: GpParams) -> Self {
self.gp_params.replace(gp_params);
self
}
pub fn clean(mut self, clean: bool) -> Self {
self.clean = clean;
self
@@ -119,6 +129,12 @@ impl<'a> AuthWindow<'a> {
let saml_request = self.saml_request.to_string();
let (auth_result_tx, mut auth_result_rx) = mpsc::unbounded_channel::<AuthResult>();
let raise_window_cancel_token: Arc<RwLock<Option<CancellationToken>>> = Default::default();
let gp_params = self.gp_params.as_ref().unwrap();
let tls_err_policy = if gp_params.ignore_tls_errors() {
TLSErrorsPolicy::Ignore
} else {
TLSErrorsPolicy::Fail
};
if self.clean {
clear_webview_cookies(window).await?;
@@ -128,6 +144,10 @@ impl<'a> AuthWindow<'a> {
window.with_webview(move |wv| {
let wv = wv.inner();
if let Some(context) = wv.context() {
context.set_tls_errors_policy(tls_err_policy);
}
if let Some(settings) = wv.settings() {
let ua = settings.user_agent().unwrap_or("".into());
info!("Auth window user agent: {}", ua);
@@ -168,12 +188,15 @@ impl<'a> AuthWindow<'a> {
}
});
wv.connect_load_failed_with_tls_errors(|_wv, uri, cert, err| {
let auth_result_tx_clone = auth_result_tx.clone();
wv.connect_load_failed_with_tls_errors(move |_wv, uri, cert, err| {
let redacted_uri = redact_uri(uri);
warn!(
"Failed to load uri: {} with error: {}, cert: {}",
redacted_uri, err, cert
);
send_auth_result(&auth_result_tx_clone, Err(AuthDataError::TlsError));
true
});
@@ -187,12 +210,14 @@ impl<'a> AuthWindow<'a> {
})?;
let portal = self.server.to_string();
let user_agent = self.user_agent.to_string();
loop {
if let Some(auth_result) = auth_result_rx.recv().await {
match auth_result {
Ok(auth_data) => return Ok(auth_data),
Err(AuthDataError::TlsError) => {
return Err(anyhow::anyhow!("TLS error: certificate verify failed"))
}
Err(AuthDataError::NotFound) => {
info!("No auth data found, it may not be the /SAML20/SP/ACS endpoint");
@@ -237,7 +262,7 @@ impl<'a> AuthWindow<'a> {
);
})?;
let saml_request = portal_prelogin(&portal, &user_agent).await?;
let saml_request = portal_prelogin(&portal, gp_params).await?;
window.with_webview(move |wv| {
let wv = wv.inner();
load_saml_request(&wv, &saml_request);
@@ -258,9 +283,10 @@ fn raise_window(window: &Arc<Window>) {
}
}
pub(crate) async fn portal_prelogin(portal: &str, user_agent: &str) -> anyhow::Result<String> {
pub(crate) async fn portal_prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<String> {
info!("Portal prelogin...");
match prelogin(portal, user_agent).await? {
match prelogin(portal, gp_params).await? {
Prelogin::Saml(prelogin) => Ok(prelogin.saml_request().to_string()),
Prelogin::Standard(_) => Err(anyhow::anyhow!("Received non-SAML prelogin response")),
}
@@ -397,6 +423,11 @@ fn read_auth_data(main_resource: &WebResource, auth_result_tx: mpsc::UnboundedSe
send_auth_result(&auth_result_tx, auth_result)
});
}
Err(AuthDataError::TlsError) => {
// NOTE: This is unreachable
info!("TLS error found in headers, trying to read from body...");
send_auth_result(&auth_result_tx, Err(AuthDataError::TlsError));
}
}
}

View File

@@ -1,6 +1,8 @@
use clap::Parser;
use gpapi::{
auth::{SamlAuthData, SamlAuthResult},
clap::args::Os,
gp_params::{ClientOs, GpParams},
utils::{normalize_server, openssl},
GP_USER_AGENT,
};
@@ -26,23 +28,35 @@ struct Cli {
saml_request: Option<String>,
#[arg(long, default_value = GP_USER_AGENT)]
user_agent: String,
#[arg(long, default_value = "Linux")]
os: Os,
#[arg(long)]
os_version: Option<String>,
#[arg(long)]
hidpi: bool,
#[arg(long)]
fix_openssl: bool,
#[arg(long)]
ignore_tls_errors: bool,
#[arg(long)]
clean: bool,
}
impl Cli {
async fn run(&mut self) -> anyhow::Result<()> {
if self.ignore_tls_errors {
info!("TLS errors will be ignored");
}
let mut openssl_conf = self.prepare_env()?;
self.server = normalize_server(&self.server)?;
let gp_params = self.build_gp_params();
// Get the initial SAML request
let saml_request = match self.saml_request {
Some(ref saml_request) => saml_request.clone(),
None => portal_prelogin(&self.server, &self.user_agent).await?,
None => portal_prelogin(&self.server, &gp_params).await?,
};
self.saml_request.replace(saml_request);
@@ -82,10 +96,22 @@ impl Cli {
Ok(None)
}
fn build_gp_params(&self) -> GpParams {
let gp_params = GpParams::builder()
.user_agent(&self.user_agent)
.client_os(ClientOs::from(&self.os))
.os_version(self.os_version.clone())
.ignore_tls_errors(self.ignore_tls_errors)
.build();
gp_params
}
async fn saml_auth(&self, app_handle: AppHandle) -> anyhow::Result<SamlAuthData> {
let auth_window = AuthWindow::new(app_handle)
.server(&self.server)
.user_agent(&self.user_agent)
.gp_params(self.build_gp_params())
.saml_request(self.saml_request.as_ref().unwrap())
.clean(self.clean);

View File

@@ -6,7 +6,7 @@ edition.workspace = true
license.workspace = true
[dependencies]
gpapi = { path = "../../crates/gpapi" }
gpapi = { path = "../../crates/gpapi", features = ["clap"] }
openconnect = { path = "../../crates/openconnect" }
anyhow.workspace = true
clap.workspace = true

View File

@@ -16,6 +16,11 @@ const VERSION: &str = concat!(
")"
);
pub(crate) struct SharedArgs {
pub(crate) fix_openssl: bool,
pub(crate) ignore_tls_errors: bool,
}
#[derive(Subcommand)]
enum CliCommand {
#[command(about = "Connect to a portal server")]
@@ -40,6 +45,8 @@ enum CliCommand {
{usage-heading} {usage}
{all-args}{after-help}
See 'gpclient help <command>' for more information on a specific command.
"
)]
struct Cli {
@@ -51,6 +58,8 @@ struct Cli {
help = "Get around the OpenSSL `unsafe legacy renegotiation` error"
)]
fix_openssl: bool,
#[arg(long, help = "Ignore the TLS errors")]
ignore_tls_errors: bool,
}
impl Cli {
@@ -67,9 +76,17 @@ impl Cli {
// The temp file will be dropped automatically when the file handle is dropped
// So, declare it here to ensure it's not dropped
let _file = self.fix_openssl()?;
let shared_args = SharedArgs {
fix_openssl: self.fix_openssl,
ignore_tls_errors: self.ignore_tls_errors,
};
if self.ignore_tls_errors {
info!("TLS errors will be ignored");
}
match &self.command {
CliCommand::Connect(args) => ConnectHandler::new(args, self.fix_openssl).handle().await,
CliCommand::Connect(args) => ConnectHandler::new(args, &shared_args).handle().await,
CliCommand::Disconnect => DisconnectHandler::new().handle(),
CliCommand::LaunchGui(args) => LaunchGuiHandler::new(args).handle().await,
}
@@ -89,13 +106,24 @@ pub(crate) async fn run() {
if let Err(err) = cli.run().await {
eprintln!("\nError: {}", err);
if err.to_string().contains("unsafe legacy renegotiation") && !cli.fix_openssl {
let err = err.to_string();
if err.contains("unsafe legacy renegotiation") && !cli.fix_openssl {
eprintln!("\nRe-run it with the `--fix-openssl` option to work around this issue, e.g.:\n");
// Print the command
let args = std::env::args().collect::<Vec<_>>();
eprintln!("{} --fix-openssl {}\n", args[0], args[1..].join(" "));
}
if err.contains("certificate verify failed") {
eprintln!(
"\nRe-run it with the `--ignore-tls-errors` option to ignore the certificate error, e.g.:\n"
);
// Print the command
let args = std::env::args().collect::<Vec<_>>();
eprintln!("{} --ignore-tls-errors {}\n", args[0], args[1..].join(" "));
}
std::process::exit(1);
}
}

View File

@@ -2,9 +2,10 @@ use std::{fs, sync::Arc};
use clap::Args;
use gpapi::{
clap::args::Os,
credential::{Credential, PasswordCredential},
gateway::gateway_login,
gp_params::GpParams,
gp_params::{ClientOs, GpParams},
portal::{prelogin, retrieve_config, Prelogin},
process::auth_launcher::SamlAuthLauncher,
utils::{self, shutdown_signal},
@@ -14,7 +15,7 @@ use inquire::{Password, PasswordDisplayMode, Select, Text};
use log::info;
use openconnect::Vpn;
use crate::GP_CLIENT_LOCK_FILE;
use crate::{cli::SharedArgs, GP_CLIENT_LOCK_FILE};
#[derive(Args)]
pub(crate) struct ConnectArgs {
@@ -36,6 +37,10 @@ pub(crate) struct ConnectArgs {
script: Option<String>,
#[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, help = "The HiDPI mode, useful for high resolution screens")]
hidpi: bool,
#[arg(long, help = "Do not reuse the remembered authentication cookie")]
@@ -44,12 +49,12 @@ pub(crate) struct ConnectArgs {
pub(crate) struct ConnectHandler<'a> {
args: &'a ConnectArgs,
fix_openssl: bool,
shared_args: &'a SharedArgs,
}
impl<'a> ConnectHandler<'a> {
pub(crate) fn new(args: &'a ConnectArgs, fix_openssl: bool) -> Self {
Self { args, fix_openssl }
pub(crate) fn new(args: &'a ConnectArgs, shared_args: &'a SharedArgs) -> Self {
Self { args, shared_args }
}
pub(crate) async fn handle(&self) -> anyhow::Result<()> {
@@ -57,9 +62,12 @@ impl<'a> ConnectHandler<'a> {
let gp_params = GpParams::builder()
.user_agent(&self.args.user_agent)
.client_os(ClientOs::from(&self.args.os))
.os_version(self.args.os_version.clone())
.ignore_tls_errors(self.shared_args.ignore_tls_errors)
.build();
let prelogin = prelogin(&portal, &self.args.user_agent).await?;
let prelogin = prelogin(&portal, &gp_params).await?;
let portal_credential = self.obtain_portal_credential(&prelogin).await?;
let mut portal_config = retrieve_config(&portal, &portal_credential, &gp_params).await?;
@@ -114,10 +122,13 @@ impl<'a> ConnectHandler<'a> {
match prelogin {
Prelogin::Saml(prelogin) => {
SamlAuthLauncher::new(&self.args.server)
.user_agent(&self.args.user_agent)
.saml_request(prelogin.saml_request())
.user_agent(&self.args.user_agent)
.os(self.args.os.as_str())
.os_version(self.args.os_version.as_deref())
.hidpi(self.args.hidpi)
.fix_openssl(self.fix_openssl)
.fix_openssl(self.shared_args.fix_openssl)
.ignore_tls_errors(self.shared_args.ignore_tls_errors)
.clean(self.args.clean)
.launch()
.await