diff --git a/apps/gpclient/src/connect.rs b/apps/gpclient/src/connect.rs index ea19756..085a056 100644 --- a/apps/gpclient/src/connect.rs +++ b/apps/gpclient/src/connect.rs @@ -6,7 +6,7 @@ use gpapi::{ clap::args::Os, credential::{Credential, PasswordCredential}, error::PortalError, - gateway::gateway_login, + gateway::{gateway_login, GatewayLogin}, gp_params::{ClientOs, GpParams}, portal::{prelogin, retrieve_config, Prelogin}, process::{ @@ -154,7 +154,7 @@ impl<'a> ConnectHandler<'a> { let gateway = selected_gateway.server(); let cred = portal_config.auth_cookie().into(); - let cookie = match gateway_login(gateway, &cred, &gp_params).await { + let cookie = match self.login_gateway(gateway, &cred, &gp_params).await { Ok(cookie) => cookie, Err(err) => { info!("Gateway login failed: {}", err); @@ -174,11 +174,28 @@ impl<'a> ConnectHandler<'a> { let prelogin = prelogin(gateway, &gp_params).await?; let cred = self.obtain_credential(&prelogin, gateway).await?; - let cookie = gateway_login(gateway, &cred, &gp_params).await?; + let cookie = self.login_gateway(gateway, &cred, &gp_params).await?; self.connect_gateway(gateway, &cookie).await } + async fn login_gateway(&self, gateway: &str, cred: &Credential, gp_params: &GpParams) -> anyhow::Result { + let mut gp_params = gp_params.clone(); + + loop { + match gateway_login(gateway, cred, &gp_params).await? { + GatewayLogin::Cookie(cookie) => return Ok(cookie), + GatewayLogin::Mfa(message, input_str) => { + let otp = Text::new(&message).prompt()?; + gp_params.set_input_str(&input_str); + gp_params.set_otp(&otp); + + info!("Retrying gateway login with MFA..."); + } + } + } + } + async fn connect_gateway(&self, gateway: &str, cookie: &str) -> anyhow::Result<()> { let mtu = self.args.mtu.unwrap_or(0); let csd_uid = get_csd_uid(&self.args.csd_user)?; diff --git a/crates/gpapi/src/gateway/login.rs b/crates/gpapi/src/gateway/login.rs index 233082f..c25c102 100644 --- a/crates/gpapi/src/gateway/login.rs +++ b/crates/gpapi/src/gateway/login.rs @@ -11,7 +11,12 @@ use crate::{ utils::{normalize_server, parse_gp_error, remove_url_scheme}, }; -pub async fn gateway_login(gateway: &str, cred: &Credential, gp_params: &GpParams) -> anyhow::Result { +pub enum GatewayLogin { + Cookie(String), + Mfa(String, String), +} + +pub async fn gateway_login(gateway: &str, cred: &Credential, gp_params: &GpParams) -> anyhow::Result { let url = normalize_server(gateway)?; let gateway = remove_url_scheme(&url); @@ -49,10 +54,22 @@ pub async fn gateway_login(gateway: &str, cred: &Credential, gp_params: &GpParam bail!("Gateway login error, reason: {}", reason); } - let res_xml = res.text().await?; - let doc = Document::parse(&res_xml)?; + let res = res.text().await?; - build_gateway_token(&doc, gp_params.computer()) + // MFA detected + if res.contains("Challenge") { + let Some((message, input_str)) = parse_mfa(&res) else { + bail!("Failed to parse MFA challenge: {res}"); + }; + + return Ok(GatewayLogin::Mfa(message, input_str)); + } + + let doc = Document::parse(&res)?; + + let cookie = build_gateway_token(&doc, gp_params.computer())?; + + Ok(GatewayLogin::Cookie(cookie)) } fn build_gateway_token(doc: &Document, computer: &str) -> anyhow::Result { @@ -86,3 +103,33 @@ fn read_args<'a>(args: &'a [String], index: usize, key: &'a str) -> anyhow::Resu .ok_or_else(|| anyhow::anyhow!("Failed to read {key} from args")) .map(|s| (key, s.as_ref())) } + +fn parse_mfa(res: &str) -> Option<(String, String)> { + let message = res + .lines() + .find(|l| l.contains("respMsg")) + .and_then(|l| l.split('"').nth(1).map(|s| s.to_string()))?; + + let input_str = res + .lines() + .find(|l| l.contains("inputStr")) + .and_then(|l| l.split('"').nth(1).map(|s| s.to_string()))?; + + Some((message, input_str)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn mfa() { + let res = r#"var respStatus = "Challenge"; +var respMsg = "MFA message"; +thisForm.inputStr.value = "5ef64e83000119ed";"#; + + let (message, input_str) = parse_mfa(res).unwrap(); + assert_eq!(message, "MFA message"); + assert_eq!(input_str, "5ef64e83000119ed"); + } +} diff --git a/crates/gpapi/src/gp_params.rs b/crates/gpapi/src/gp_params.rs index 59b9f34..3184b18 100644 --- a/crates/gpapi/src/gp_params.rs +++ b/crates/gpapi/src/gp_params.rs @@ -42,7 +42,7 @@ impl ClientOs { } } -#[derive(Debug, Serialize, Deserialize, Type, Default)] +#[derive(Debug, Serialize, Deserialize, Type, Default, Clone)] pub struct GpParams { is_gateway: bool, user_agent: String, @@ -51,6 +51,9 @@ pub struct GpParams { client_version: Option, computer: String, ignore_tls_errors: bool, + // Used for MFA + input_str: Option, + otp: Option, } impl GpParams { @@ -90,6 +93,14 @@ impl GpParams { self.client_version.as_deref() } + pub fn set_input_str(&mut self, input_str: &str) { + self.input_str = Some(input_str.to_string()); + } + + pub fn set_otp(&mut self, otp: &str) { + self.otp = Some(otp.to_string()); + } + pub(crate) fn to_params(&self) -> HashMap<&str, &str> { let mut params: HashMap<&str, &str> = HashMap::new(); let client_os = self.client_os.as_str(); @@ -100,11 +111,16 @@ impl GpParams { params.insert("ok", "Login"); params.insert("direct", "yes"); params.insert("ipv6-support", "yes"); - params.insert("inputStr", ""); params.insert("clientVer", "4100"); params.insert("clientos", client_os); params.insert("computer", &self.computer); + // MFA + params.insert("inputStr", self.input_str.as_deref().unwrap_or_default()); + if let Some(otp) = &self.otp { + params.insert("passwd", otp); + } + if let Some(os_version) = &self.os_version { params.insert("os-version", os_version); } @@ -187,6 +203,8 @@ impl GpParamsBuilder { client_version: self.client_version.clone(), computer: self.computer.clone(), ignore_tls_errors: self.ignore_tls_errors, + input_str: Default::default(), + otp: Default::default(), } } }