diff --git a/snx-rs-gui/src/prompt.rs b/snx-rs-gui/src/prompt.rs index d1e9d09..7a85365 100644 --- a/snx-rs-gui/src/prompt.rs +++ b/snx-rs-gui/src/prompt.rs @@ -6,7 +6,7 @@ use gtk::{ prelude::{BoxExt, DialogExt, EntryExt, GtkWindowExt, WidgetExt}, Align, Orientation, ResponseType, WindowPosition, }; - +use snxcore::model::AuthPrompt; use snxcore::prompt::SecurePrompt; use crate::dbus::send_notification; @@ -14,7 +14,7 @@ use crate::dbus::send_notification; pub struct GtkPrompt; impl GtkPrompt { - fn get_input(&self, prompt: &str, secure: bool) -> anyhow::Result { + fn get_input(&self, prompt: &AuthPrompt, secure: bool) -> anyhow::Result { let (tx, rx) = mpsc::channel(); let prompt = prompt.to_owned(); @@ -34,8 +34,16 @@ impl GtkPrompt { let content = dialog.content_area(); let inner = gtk::Box::builder().orientation(Orientation::Vertical).margin(6).build(); + if !prompt.header.is_empty() { + inner.pack_start( + >k::Label::builder().label(&prompt.header).halign(Align::Start).build(), + false, + true, + 12, + ); + } inner.pack_start( - >k::Label::builder().label(&prompt).halign(Align::Start).build(), + >k::Label::builder().label(&prompt.prompt).halign(Align::Start).build(), false, true, 6, @@ -68,11 +76,11 @@ impl GtkPrompt { } impl SecurePrompt for GtkPrompt { - fn get_secure_input(&self, prompt: &str) -> anyhow::Result { + fn get_secure_input(&self, prompt: &AuthPrompt) -> anyhow::Result { self.get_input(prompt, true) } - fn get_plain_input(&self, prompt: &str) -> anyhow::Result { + fn get_plain_input(&self, prompt: &AuthPrompt) -> anyhow::Result { self.get_input(prompt, false) } diff --git a/snx-rs/src/main.rs b/snx-rs/src/main.rs index 5a31263..078915b 100644 --- a/snx-rs/src/main.rs +++ b/snx-rs/src/main.rs @@ -7,7 +7,7 @@ use tracing::{debug, metadata::LevelFilter, warn}; use crate::cmdline::CmdlineParams; use snxcore::model::params::TunnelType; -use snxcore::model::LoginPrompt; +use snxcore::model::AuthPrompt; use snxcore::{ browser::spawn_otp_listener, ccc::CccHttpClient, @@ -146,13 +146,13 @@ async fn main_standalone(params: TunnelParams) -> anyhow::Result<()> { MfaType::PasswordInput => { let prompt = mfa_prompts .pop_front() - .unwrap_or_else(|| LoginPrompt::new_password(&challenge.prompt)); + .unwrap_or_else(|| AuthPrompt::new_password(&challenge.prompt)); let input = if !params.password.is_empty() && first_mfa && prompt.is_password() { first_mfa = false; Ok(params.password.clone()) } else { - TtyPrompt.get_secure_input(&prompt.prompt) + TtyPrompt.get_secure_input(&prompt) }; match input { @@ -172,7 +172,8 @@ async fn main_standalone(params: TunnelParams) -> anyhow::Result<()> { session = connector.challenge_code(session, &otp).await?; } MfaType::UserNameInput => { - let input = TtyPrompt.get_plain_input(&challenge.prompt)?; + let prompt = AuthPrompt::new("", "username", &challenge.prompt); + let input = TtyPrompt.get_plain_input(&prompt)?; session = connector.challenge_code(session, &input).await?; } } diff --git a/snxcore/src/controller.rs b/snxcore/src/controller.rs index b6452b3..04b58cf 100644 --- a/snxcore/src/controller.rs +++ b/snxcore/src/controller.rs @@ -7,7 +7,7 @@ use crate::{ browser::{spawn_otp_listener, BrowserController}, ccc::CccHttpClient, model::{ - params::TunnelParams, ConnectionStatus, LoginPrompt, MfaChallenge, MfaType, TunnelServiceRequest, + params::TunnelParams, AuthPrompt, ConnectionStatus, MfaChallenge, MfaType, TunnelServiceRequest, TunnelServiceResponse, }, platform::{self, UdpSocketExt}, @@ -45,7 +45,7 @@ impl FromStr for ServiceCommand { pub struct ServiceController { pub params: Arc, prompt: P, - mfa_prompts: Option>, + mfa_prompts: Option>, password_from_keychain: String, username: String, first_mfa: bool, @@ -134,7 +134,7 @@ where .mfa_prompts .as_mut() .and_then(|p| p.pop_front()) - .unwrap_or_else(|| LoginPrompt::new_password(&mfa.prompt)); + .unwrap_or_else(|| AuthPrompt::new_password(&mfa.prompt)); if !self.params.password.is_empty() && self.first_mfa && prompt.is_password() { self.first_mfa = false; @@ -144,11 +144,11 @@ where Ok(self.password_from_keychain.clone()) } else { let prompt = if self.params.server_prompt { - &prompt.prompt + prompt } else { - &mfa.prompt + AuthPrompt::new_password(&mfa.prompt) }; - let input = self.prompt.get_secure_input(prompt)?; + let input = self.prompt.get_secure_input(&prompt)?; Ok(input) } } @@ -169,7 +169,8 @@ where } } MfaType::UserNameInput => { - let input = self.prompt.get_plain_input(&mfa.prompt)?; + let prompt = AuthPrompt::new("", "username", &mfa.prompt); + let input = self.prompt.get_plain_input(&prompt)?; self.username = input.clone(); if !self.username.is_empty() && !self.params.no_keychain && self.params.password.is_empty() { diff --git a/snxcore/src/model.rs b/snxcore/src/model.rs index b8f07a1..bfd2544 100644 --- a/snxcore/src/model.rs +++ b/snxcore/src/model.rs @@ -134,18 +134,21 @@ pub enum TunnelServiceResponse { } #[derive(Debug, Clone, PartialEq)] -pub struct LoginPrompt { +pub struct AuthPrompt { + pub header: String, pub factor_type: String, pub prompt: String, } -impl LoginPrompt { - pub fn new(factor_type: F, prompt: S) -> Self +impl AuthPrompt { + pub fn new(header: H, factor_type: F, prompt: S) -> Self where + H: AsRef, F: AsRef, S: AsRef, { Self { + header: header.as_ref().to_owned(), factor_type: factor_type.as_ref().to_owned(), prompt: prompt.as_ref().to_owned(), } @@ -153,6 +156,7 @@ impl LoginPrompt { pub fn new_password>(prompt: S) -> Self { Self { + header: String::new(), factor_type: "password".to_owned(), prompt: prompt.as_ref().to_owned(), } diff --git a/snxcore/src/model/proto.rs b/snxcore/src/model/proto.rs index 655bda4..067cff8 100644 --- a/snxcore/src/model/proto.rs +++ b/snxcore/src/model/proto.rs @@ -339,13 +339,6 @@ pub enum LoginDisplayLabelSelect { Empty(String), } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct LoginDisplayLabel { - pub header: String, - pub username: Option, - pub password: Option, -} - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct AuthenticationRealm { #[serde(rename = "clientType")] diff --git a/snxcore/src/prompt.rs b/snxcore/src/prompt.rs index 03a9922..a82ae5b 100644 --- a/snxcore/src/prompt.rs +++ b/snxcore/src/prompt.rs @@ -1,11 +1,12 @@ +use crate::model::AuthPrompt; use anyhow::anyhow; use std::io::Write; use std::io::{stderr, stdin, IsTerminal}; pub trait SecurePrompt { - fn get_secure_input(&self, prompt: &str) -> anyhow::Result; + fn get_secure_input(&self, prompt: &AuthPrompt) -> anyhow::Result; - fn get_plain_input(&self, prompt: &str) -> anyhow::Result; + fn get_plain_input(&self, prompt: &AuthPrompt) -> anyhow::Result; fn show_notification(&self, summary: &str, message: &str) -> anyhow::Result<()>; } @@ -13,17 +14,26 @@ pub trait SecurePrompt { pub struct TtyPrompt; impl SecurePrompt for TtyPrompt { - fn get_secure_input(&self, prompt: &str) -> anyhow::Result { + fn get_secure_input(&self, prompt: &AuthPrompt) -> anyhow::Result { if stdin().is_terminal() && stderr().is_terminal() { - Ok(passterm::prompt_password_stdin(Some(prompt), passterm::Stream::Stderr)?) + if !prompt.header.is_empty() { + println!("{}", prompt.header); + } + Ok(passterm::prompt_password_stdin( + Some(&prompt.prompt), + passterm::Stream::Stderr, + )?) } else { Err(anyhow!("No attached TTY to get user input!")) } } - fn get_plain_input(&self, prompt: &str) -> anyhow::Result { + fn get_plain_input(&self, prompt: &AuthPrompt) -> anyhow::Result { if stdin().is_terminal() && stderr().is_terminal() { - eprint!("{}", prompt); + if !prompt.header.is_empty() { + println!("{}", prompt.header); + } + eprint!("{}", prompt.prompt); stderr().flush()?; let mut line = String::new(); stdin().read_line(&mut line)?; diff --git a/snxcore/src/server_info.rs b/snxcore/src/server_info.rs index 3e448e1..53e4189 100644 --- a/snxcore/src/server_info.rs +++ b/snxcore/src/server_info.rs @@ -1,5 +1,5 @@ use crate::model::proto::LoginDisplayLabelSelect; -use crate::model::LoginPrompt; +use crate::model::AuthPrompt; use crate::{ ccc::CccHttpClient, model::{ @@ -23,15 +23,19 @@ pub async fn get(params: &TunnelParams) -> anyhow::Result { .try_into() } -pub async fn get_mfa_prompts(params: &TunnelParams) -> anyhow::Result> { +pub async fn get_mfa_prompts(params: &TunnelParams) -> anyhow::Result> { let factors = get_login_factors(params).await?; let result = factors .into_iter() .filter_map(|factor| match factor.custom_display_labels { - LoginDisplayLabelSelect::LoginDisplayLabel(map) => map - .get("password") - .map(|label| LoginPrompt::new(factor.factor_type, format!("{}: ", label))), + LoginDisplayLabelSelect::LoginDisplayLabel(map) => map.get("password").map(|label| { + AuthPrompt::new( + map.get("header").map(ToOwned::to_owned).unwrap_or_default(), + factor.factor_type, + format!("{}: ", label), + ) + }), LoginDisplayLabelSelect::Empty(_) => None, }) .collect();