From b2bb35994f6661ede0f9891ec0ae581a973d99a5 Mon Sep 17 00:00:00 2001 From: Kevin Yue Date: Sun, 28 Jan 2024 11:41:48 +0800 Subject: [PATCH] Support connect gateway (#306) --- apps/gpauth/src/auth_window.rs | 6 +- apps/gpauth/src/cli.rs | 3 + apps/gpclient/src/cli.rs | 4 +- apps/gpclient/src/connect.rs | 76 ++++++++++++++------ apps/gpservice/src/main.rs | 2 +- apps/gpservice/src/routes.rs | 5 +- crates/gpapi/src/credential.rs | 18 +++++ crates/gpapi/src/gateway/login.rs | 21 ++++-- crates/gpapi/src/gateway/mod.rs | 9 +++ crates/gpapi/src/portal/config.rs | 85 +++++++++++------------ crates/gpapi/src/portal/mod.rs | 10 +++ crates/gpapi/src/portal/prelogin.rs | 58 ++++++++++++---- crates/gpapi/src/process/auth_launcher.rs | 11 +++ crates/gpapi/src/utils/mod.rs | 4 ++ 14 files changed, 220 insertions(+), 92 deletions(-) diff --git a/apps/gpauth/src/auth_window.rs b/apps/gpauth/src/auth_window.rs index 39ae738..ed81026 100644 --- a/apps/gpauth/src/auth_window.rs +++ b/apps/gpauth/src/auth_window.rs @@ -284,12 +284,10 @@ fn raise_window(window: &Arc) { } } -pub(crate) async fn portal_prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result { - info!("Portal prelogin..."); - +pub async fn portal_prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result { 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")), + Prelogin::Standard(_) => bail!("Received non-SAML prelogin response"), } } diff --git a/apps/gpauth/src/cli.rs b/apps/gpauth/src/cli.rs index ef4c49d..5bb7c99 100644 --- a/apps/gpauth/src/cli.rs +++ b/apps/gpauth/src/cli.rs @@ -25,6 +25,8 @@ const VERSION: &str = concat!( struct Cli { server: String, #[arg(long)] + gateway: bool, + #[arg(long)] saml_request: Option, #[arg(long, default_value = GP_USER_AGENT)] user_agent: String, @@ -102,6 +104,7 @@ impl Cli { .client_os(ClientOs::from(&self.os)) .os_version(self.os_version.clone()) .ignore_tls_errors(self.ignore_tls_errors) + .is_gateway(self.gateway) .build(); gp_params diff --git a/apps/gpclient/src/cli.rs b/apps/gpclient/src/cli.rs index 9d68582..35228d9 100644 --- a/apps/gpclient/src/cli.rs +++ b/apps/gpclient/src/cli.rs @@ -116,9 +116,7 @@ pub(crate) async fn run() { } if err.contains("certificate verify failed") && !cli.ignore_tls_errors { - eprintln!( - "\nRe-run it with the `--ignore-tls-errors` option to ignore the certificate error, e.g.:\n" - ); + 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::>(); eprintln!("{} --ignore-tls-errors {}\n", args[0], args[1..].join(" ")); diff --git a/apps/gpclient/src/connect.rs b/apps/gpclient/src/connect.rs index 3d5c9fb..c973480 100644 --- a/apps/gpclient/src/connect.rs +++ b/apps/gpclient/src/connect.rs @@ -6,9 +6,9 @@ use gpapi::{ credential::{Credential, PasswordCredential}, gateway::gateway_login, gp_params::{ClientOs, GpParams}, - portal::{prelogin, retrieve_config, Prelogin}, + portal::{prelogin, retrieve_config, PortalError, Prelogin}, process::auth_launcher::SamlAuthLauncher, - utils::{self, shutdown_signal}, + utils::shutdown_signal, GP_USER_AGENT, }; use inquire::{Password, PasswordDisplayMode, Select, Text}; @@ -81,12 +81,28 @@ impl<'a> ConnectHandler<'a> { } pub(crate) async fn handle(&self) -> anyhow::Result<()> { - let portal = utils::normalize_server(self.args.server.as_str())?; + let server = self.args.server.as_str(); + + let Err(err) = self.connect_portal_with_prelogin(server).await else { + return Ok(()); + }; + + info!("Failed to connect portal with prelogin: {}", err); + if err.root_cause().downcast_ref::().is_some() { + info!("Trying the gateway authentication workflow..."); + return self.connect_gateway_with_prelogin(server).await; + } + + Err(err) + } + + async fn connect_portal_with_prelogin(&self, portal: &str) -> anyhow::Result<()> { let gp_params = self.build_gp_params(); - let prelogin = prelogin(&portal, &gp_params).await?; - let portal_credential = self.obtain_credential(&prelogin).await?; - let mut portal_config = retrieve_config(&portal, &portal_credential, &gp_params).await?; + let prelogin = prelogin(portal, &gp_params).await?; + + let cred = self.obtain_credential(&prelogin, portal).await?; + let mut portal_config = retrieve_config(portal, &cred, &gp_params).await?; let selected_gateway = match &self.args.gateway { Some(gateway) => portal_config @@ -109,15 +125,31 @@ impl<'a> ConnectHandler<'a> { let gateway = selected_gateway.server(); let cred = portal_config.auth_cookie().into(); - let token = match gateway_login(gateway, &cred, &gp_params).await { - Ok(token) => token, - Err(_) => { - info!("Gateway login failed, retrying with prelogin"); - self.gateway_login_with_prelogin(gateway).await? + let cookie = match gateway_login(gateway, &cred, &gp_params).await { + Ok(cookie) => cookie, + Err(err) => { + info!("Gateway login failed: {}", err); + return self.connect_gateway_with_prelogin(gateway).await; } }; - let vpn = Vpn::builder(gateway, &token) + self.connect_gateway(gateway, &cookie).await + } + + async fn connect_gateway_with_prelogin(&self, gateway: &str) -> anyhow::Result<()> { + let mut gp_params = self.build_gp_params(); + gp_params.set_is_gateway(true); + + let prelogin = prelogin(gateway, &gp_params).await?; + let cred = self.obtain_credential(&prelogin, &gateway).await?; + + let cookie = gateway_login(gateway, &cred, &gp_params).await?; + + self.connect_gateway(gateway, &cookie).await + } + + async fn connect_gateway(&self, gateway: &str, cookie: &str) -> anyhow::Result<()> { + let vpn = Vpn::builder(gateway, cookie) .user_agent(self.args.user_agent.clone()) .script(self.args.script.clone()) .build(); @@ -142,20 +174,17 @@ impl<'a> ConnectHandler<'a> { Ok(()) } - async fn gateway_login_with_prelogin(&self, gateway: &str) -> anyhow::Result { - let mut gp_params = self.build_gp_params(); - gp_params.set_is_gateway(true); + async fn obtain_credential( + &self, + prelogin: &Prelogin, + server: &str, + ) -> anyhow::Result { + let is_gateway = prelogin.is_gateway(); - let prelogin = prelogin(gateway, &gp_params).await?; - let cred = self.obtain_credential(&prelogin).await?; - - gateway_login(gateway, &cred, &gp_params).await - } - - async fn obtain_credential(&self, prelogin: &Prelogin) -> anyhow::Result { match prelogin { Prelogin::Saml(prelogin) => { SamlAuthLauncher::new(&self.args.server) + .gateway(is_gateway) .saml_request(prelogin.saml_request()) .user_agent(&self.args.user_agent) .os(self.args.os.as_str()) @@ -168,7 +197,8 @@ impl<'a> ConnectHandler<'a> { .await } Prelogin::Standard(prelogin) => { - println!("{}", prelogin.auth_message()); + let prefix = if is_gateway { "Gateway" } else { "Portal" }; + println!("{} ({}: {})", prelogin.auth_message(), prefix, server); let user = self.args.user.as_ref().map_or_else( || Text::new(&format!("{}:", prelogin.label_username())).prompt(), diff --git a/apps/gpservice/src/main.rs b/apps/gpservice/src/main.rs index aa7daa3..88602aa 100644 --- a/apps/gpservice/src/main.rs +++ b/apps/gpservice/src/main.rs @@ -2,8 +2,8 @@ mod cli; mod handlers; mod routes; mod vpn_task; -mod ws_server; mod ws_connection; +mod ws_server; #[tokio::main] async fn main() { diff --git a/apps/gpservice/src/routes.rs b/apps/gpservice/src/routes.rs index 5ea5f65..45e1393 100644 --- a/apps/gpservice/src/routes.rs +++ b/apps/gpservice/src/routes.rs @@ -1,6 +1,9 @@ use std::sync::Arc; -use axum::{routing::{get, post}, Router}; +use axum::{ + routing::{get, post}, + Router, +}; use crate::{handlers, ws_server::WsServerContext}; diff --git a/crates/gpapi/src/credential.rs b/crates/gpapi/src/credential.rs index 7890283..2aba774 100644 --- a/crates/gpapi/src/credential.rs +++ b/crates/gpapi/src/credential.rs @@ -139,6 +139,24 @@ impl CachedCredential { pub fn set_auth_cookie(&mut self, auth_cookie: AuthCookieCredential) { self.auth_cookie = auth_cookie; } + + pub fn set_username(&mut self, username: String) { + self.username = username; + } + + pub fn set_password(&mut self, password: Option) { + self.password = password.map(|s| s.to_string()); + } +} + +impl From for CachedCredential { + fn from(value: PasswordCredential) -> Self { + Self::new( + value.username().to_owned(), + Some(value.password().to_owned()), + AuthCookieCredential::new("", "", ""), + ) + } } #[derive(Debug, Serialize, Deserialize, Type, Clone)] diff --git a/crates/gpapi/src/gateway/login.rs b/crates/gpapi/src/gateway/login.rs index fe737c2..e3aa2f2 100644 --- a/crates/gpapi/src/gateway/login.rs +++ b/crates/gpapi/src/gateway/login.rs @@ -1,16 +1,24 @@ +use anyhow::bail; use log::info; use reqwest::Client; use roxmltree::Document; use urlencoding::encode; -use crate::{credential::Credential, gp_params::GpParams}; +use crate::{ + credential::Credential, + gp_params::GpParams, + utils::{normalize_server, remove_url_scheme}, +}; pub async fn gateway_login( gateway: &str, cred: &Credential, gp_params: &GpParams, ) -> anyhow::Result { - let login_url = format!("https://{}/ssl-vpn/login.esp", gateway); + let url = normalize_server(gateway)?; + let gateway = remove_url_scheme(&url); + + let login_url = format!("{}/ssl-vpn/login.esp", url); let client = Client::builder() .danger_accept_invalid_certs(gp_params.ignore_tls_errors()) .user_agent(gp_params.user_agent()) @@ -20,13 +28,18 @@ pub async fn gateway_login( let extra_params = gp_params.to_params(); params.extend(extra_params); - params.insert("server", gateway); + params.insert("server", &gateway); info!("Gateway login, user_agent: {}", gp_params.user_agent()); let res = client.post(&login_url).form(¶ms).send().await?; - let res_xml = res.error_for_status()?.text().await?; + let status = res.status(); + if status.is_client_error() || status.is_server_error() { + bail!("Gateway login error: {}", status) + } + + let res_xml = res.text().await?; let doc = Document::parse(&res_xml)?; build_gateway_token(&doc, gp_params.computer()) diff --git a/crates/gpapi/src/gateway/mod.rs b/crates/gpapi/src/gateway/mod.rs index 7db09bc..ab84223 100644 --- a/crates/gpapi/src/gateway/mod.rs +++ b/crates/gpapi/src/gateway/mod.rs @@ -31,6 +31,15 @@ impl Display for Gateway { } impl Gateway { + pub fn new(name: String, address: String) -> Self { + Self { + name, + address, + priority: 0, + priority_rules: vec![], + } + } + pub fn name(&self) -> &str { &self.name } diff --git a/crates/gpapi/src/portal/config.rs b/crates/gpapi/src/portal/config.rs index 262934f..0efba34 100644 --- a/crates/gpapi/src/portal/config.rs +++ b/crates/gpapi/src/portal/config.rs @@ -1,16 +1,16 @@ -use anyhow::ensure; +use anyhow::bail; use log::info; -use reqwest::Client; +use reqwest::{Client, StatusCode}; use roxmltree::Document; use serde::Serialize; use specta::Type; -use thiserror::Error; use crate::{ credential::{AuthCookieCredential, Credential}, gateway::{parse_gateways, Gateway}, gp_params::GpParams, - utils::{normalize_server, xml}, + portal::PortalError, + utils::{normalize_server, remove_url_scheme, xml}, }; #[derive(Debug, Serialize, Type)] @@ -18,25 +18,12 @@ use crate::{ pub struct PortalConfig { portal: String, auth_cookie: AuthCookieCredential, + config_cred: Credential, gateways: Vec, config_digest: Option, } impl PortalConfig { - pub fn new( - portal: String, - auth_cookie: AuthCookieCredential, - gateways: Vec, - config_digest: Option, - ) -> Self { - Self { - portal, - auth_cookie, - gateways, - config_digest, - } - } - pub fn portal(&self) -> &str { &self.portal } @@ -49,6 +36,10 @@ impl PortalConfig { &self.auth_cookie } + pub fn config_cred(&self) -> &Credential { + &self.config_cred + } + /// In-place sort the gateways by region pub fn sort_gateways(&mut self, region: &str) { let preferred_gateway = self.find_preferred_gateway(region); @@ -98,12 +89,6 @@ impl PortalConfig { } } -#[derive(Error, Debug)] -pub enum PortalConfigError { - #[error("Empty response, retrying can help")] - EmptyResponse, -} - pub async fn retrieve_config( portal: &str, cred: &Credential, @@ -128,13 +113,35 @@ pub async fn retrieve_config( info!("Portal config, user_agent: {}", gp_params.user_agent()); let res = client.post(&url).form(¶ms).send().await?; - let res_xml = res.error_for_status()?.text().await?; + let status = res.status(); - ensure!(!res_xml.is_empty(), PortalConfigError::EmptyResponse); + if status == StatusCode::NOT_FOUND { + bail!(PortalError::ConfigError( + "Config endpoint not found".to_string() + )) + } - let doc = Document::parse(&res_xml)?; - let mut gateways = - parse_gateways(&doc).ok_or_else(|| anyhow::anyhow!("Failed to parse gateways"))?; + if status.is_client_error() || status.is_server_error() { + bail!("Portal config error: {}", status) + } + + let res_xml = res + .text() + .await + .map_err(|e| PortalError::ConfigError(e.to_string()))?; + + if res_xml.is_empty() { + bail!(PortalError::ConfigError( + "Empty portal config response".to_string() + )) + } + + let doc = Document::parse(&res_xml).map_err(|e| PortalError::ConfigError(e.to_string()))?; + + let mut gateways = parse_gateways(&doc).unwrap_or_else(|| { + info!("No gateways found in portal config"); + vec![] + }); let user_auth_cookie = xml::get_child_text(&doc, "portal-userauthcookie").unwrap_or_default(); let prelogon_user_auth_cookie = @@ -142,26 +149,18 @@ pub async fn retrieve_config( let config_digest = xml::get_child_text(&doc, "config-digest"); if gateways.is_empty() { - gateways.push(Gateway { - name: server.to_string(), - address: server.to_string(), - priority: 0, - priority_rules: vec![], - }); + gateways.push(Gateway::new(server.to_string(), server.to_string())); } - Ok(PortalConfig::new( - server.to_string(), - AuthCookieCredential::new( + Ok(PortalConfig { + portal: server.to_string(), + auth_cookie: AuthCookieCredential::new( cred.username(), &user_auth_cookie, &prelogon_user_auth_cookie, ), + config_cred: cred.clone(), gateways, config_digest, - )) -} - -fn remove_url_scheme(s: &str) -> String { - s.replace("http://", "").replace("https://", "") + }) } diff --git a/crates/gpapi/src/portal/mod.rs b/crates/gpapi/src/portal/mod.rs index 8c111db..a4194d3 100644 --- a/crates/gpapi/src/portal/mod.rs +++ b/crates/gpapi/src/portal/mod.rs @@ -3,3 +3,13 @@ mod prelogin; pub use config::*; pub use prelogin::*; + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum PortalError { + #[error("Portal prelogin error: {0}")] + PreloginError(String), + #[error("Portal config error: {0}")] + ConfigError(String), +} diff --git a/crates/gpapi/src/portal/prelogin.rs b/crates/gpapi/src/portal/prelogin.rs index 292ffab..018d8e0 100644 --- a/crates/gpapi/src/portal/prelogin.rs +++ b/crates/gpapi/src/portal/prelogin.rs @@ -1,12 +1,13 @@ use anyhow::bail; -use log::{info, trace}; -use reqwest::Client; +use log::info; +use reqwest::{Client, StatusCode}; use roxmltree::Document; use serde::Serialize; use specta::Type; use crate::{ gp_params::GpParams, + portal::PortalError, utils::{base64, normalize_server, xml}, }; @@ -25,6 +26,7 @@ const REQUIRED_PARAMS: [&str; 8] = [ #[serde(rename_all = "camelCase")] pub struct SamlPrelogin { region: String, + is_gateway: bool, saml_request: String, support_default_browser: bool, } @@ -47,6 +49,7 @@ impl SamlPrelogin { #[serde(rename_all = "camelCase")] pub struct StandardPrelogin { region: String, + is_gateway: bool, auth_message: String, label_username: String, label_password: String, @@ -84,21 +87,27 @@ impl Prelogin { Prelogin::Standard(standard) => standard.region(), } } + + pub fn is_gateway(&self) -> bool { + match self { + Prelogin::Saml(saml) => saml.is_gateway, + Prelogin::Standard(standard) => standard.is_gateway, + } + } } pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result { let user_agent = gp_params.user_agent(); - info!("Portal prelogin, user_agent: {}", user_agent); + info!("Prelogin with user_agent: {}", user_agent); let portal = normalize_server(portal)?; - let prelogin_url = format!( - "{portal}/{}/prelogin.esp", - if gp_params.is_gateway() { - "ssl-vpn" - } else { - "global-protect" - } - ); + let is_gateway = gp_params.is_gateway(); + let path = if is_gateway { + "ssl-vpn" + } else { + "global-protect" + }; + let prelogin_url = format!("{portal}/{}/prelogin.esp", path); let mut params = gp_params.to_params(); params.insert("tmp", "tmp"); @@ -118,9 +127,30 @@ pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result anyhow::Result { let doc = Document::parse(&res_xml)?; let status = xml::get_child_text(&doc, "status") @@ -146,6 +176,7 @@ pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result anyhow::Result { server: &'a str, + gateway: bool, saml_request: Option<&'a str>, user_agent: Option<&'a str>, os: Option<&'a str>, @@ -22,6 +23,7 @@ impl<'a> SamlAuthLauncher<'a> { pub fn new(server: &'a str) -> Self { Self { server, + gateway: false, saml_request: None, user_agent: None, os: None, @@ -33,6 +35,11 @@ impl<'a> SamlAuthLauncher<'a> { } } + pub fn gateway(mut self, gateway: bool) -> Self { + self.gateway = gateway; + self + } + pub fn saml_request(mut self, saml_request: &'a str) -> Self { self.saml_request = Some(saml_request); self @@ -78,6 +85,10 @@ impl<'a> SamlAuthLauncher<'a> { let mut auth_cmd = Command::new(GP_AUTH_BINARY); auth_cmd.arg(self.server); + if self.gateway { + auth_cmd.arg("--gateway"); + } + if let Some(saml_request) = self.saml_request { auth_cmd.arg("--saml-request").arg(saml_request); } diff --git a/crates/gpapi/src/utils/mod.rs b/crates/gpapi/src/utils/mod.rs index 048c23c..834266e 100644 --- a/crates/gpapi/src/utils/mod.rs +++ b/crates/gpapi/src/utils/mod.rs @@ -38,3 +38,7 @@ pub fn normalize_server(server: &str) -> anyhow::Result { Ok(normalized_url) } + +pub fn remove_url_scheme(s: &str) -> String { + s.replace("http://", "").replace("https://", "") +}