Support connect gateway (#306)

This commit is contained in:
Kevin Yue 2024-01-28 11:41:48 +08:00 committed by GitHub
parent 6fe6a1387a
commit b2bb35994f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 220 additions and 92 deletions

View File

@ -284,12 +284,10 @@ fn raise_window(window: &Arc<Window>) {
}
}
pub(crate) async fn portal_prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<String> {
info!("Portal prelogin...");
pub async fn portal_prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<String> {
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"),
}
}

View File

@ -25,6 +25,8 @@ const VERSION: &str = concat!(
struct Cli {
server: String,
#[arg(long)]
gateway: bool,
#[arg(long)]
saml_request: Option<String>,
#[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

View File

@ -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::<Vec<_>>();
eprintln!("{} --ignore-tls-errors {}\n", args[0], args[1..].join(" "));

View File

@ -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::<PortalError>().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<String> {
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<Credential> {
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<Credential> {
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(),

View File

@ -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() {

View File

@ -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};

View File

@ -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<String>) {
self.password = password.map(|s| s.to_string());
}
}
impl From<PasswordCredential> 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)]

View File

@ -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<String> {
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(&params).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())

View File

@ -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
}

View File

@ -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<Gateway>,
config_digest: Option<String>,
}
impl PortalConfig {
pub fn new(
portal: String,
auth_cookie: AuthCookieCredential,
gateways: Vec<Gateway>,
config_digest: Option<String>,
) -> 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(&params).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://", "")
})
}

View File

@ -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),
}

View File

@ -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<Prelogin> {
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() {
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<Prel
.build()?;
let res = client.post(&prelogin_url).form(&params).send().await?;
let res_xml = res.error_for_status()?.text().await?;
let status = res.status();
trace!("Prelogin response: {}", res_xml);
if status == StatusCode::NOT_FOUND {
bail!(PortalError::PreloginError(
"Prelogin endpoint not found".to_string()
))
}
if status.is_client_error() || status.is_server_error() {
bail!("Prelogin error: {}", status)
}
let res_xml = res
.text()
.await
.map_err(|e| PortalError::PreloginError(e.to_string()))?;
let prelogin =
parse_res_xml(res_xml, is_gateway).map_err(|e| PortalError::PreloginError(e.to_string()))?;
Ok(prelogin)
}
fn parse_res_xml(res_xml: String, is_gateway: bool) -> anyhow::Result<Prelogin> {
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<Prel
let saml_prelogin = SamlPrelogin {
region,
is_gateway,
saml_request,
support_default_browser,
};
@ -161,6 +192,7 @@ pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prel
.unwrap_or(String::from("Please enter the login credentials"));
let standard_prelogin = StandardPrelogin {
region,
is_gateway,
auth_message,
label_username: label_username.unwrap(),
label_password: label_password.unwrap(),

View File

@ -8,6 +8,7 @@ use super::command_traits::CommandExt;
pub struct SamlAuthLauncher<'a> {
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);
}

View File

@ -38,3 +38,7 @@ pub fn normalize_server(server: &str) -> anyhow::Result<String> {
Ok(normalized_url)
}
pub fn remove_url_scheme(s: &str) -> String {
s.replace("http://", "").replace("https://", "")
}