mirror of
https://github.com/yuezk/GlobalProtect-openconnect.git
synced 2025-05-20 07:26:58 -04:00
Compare commits
4 Commits
v2.0.0-bet
...
b2bb35994f
Author | SHA1 | Date | |
---|---|---|---|
|
b2bb35994f | ||
|
6fe6a1387a | ||
|
aac401e7ee | ||
|
9655b735a1 |
10
Cargo.lock
generated
10
Cargo.lock
generated
@@ -1423,7 +1423,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gpapi"
|
name = "gpapi"
|
||||||
version = "2.0.0-beta5"
|
version = "2.0.0-beta7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64 0.21.5",
|
"base64 0.21.5",
|
||||||
@@ -1452,7 +1452,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gpauth"
|
name = "gpauth"
|
||||||
version = "2.0.0-beta5"
|
version = "2.0.0-beta7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -1472,7 +1472,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gpclient"
|
name = "gpclient"
|
||||||
version = "2.0.0-beta5"
|
version = "2.0.0-beta7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -1493,7 +1493,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gpservice"
|
name = "gpservice"
|
||||||
version = "2.0.0-beta5"
|
version = "2.0.0-beta7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -2478,7 +2478,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openconnect"
|
name = "openconnect"
|
||||||
version = "2.0.0-beta5"
|
version = "2.0.0-beta7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"is_executable",
|
"is_executable",
|
||||||
|
@@ -4,7 +4,7 @@ resolver = "2"
|
|||||||
members = ["crates/*", "apps/gpclient", "apps/gpservice", "apps/gpauth"]
|
members = ["crates/*", "apps/gpclient", "apps/gpservice", "apps/gpauth"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "2.0.0-beta5"
|
version = "2.0.0-beta7"
|
||||||
authors = ["Kevin Yue <k3vinyue@gmail.com>"]
|
authors = ["Kevin Yue <k3vinyue@gmail.com>"]
|
||||||
homepage = "https://github.com/yuezk/GlobalProtect-openconnect"
|
homepage = "https://github.com/yuezk/GlobalProtect-openconnect"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
@@ -11,6 +11,7 @@ A GUI for GlobalProtect VPN, based on OpenConnect, supports the SSO authenticati
|
|||||||
- [x] Better Linux support
|
- [x] Better Linux support
|
||||||
- [x] Support both CLI and GUI
|
- [x] Support both CLI and GUI
|
||||||
- [x] Support both SSO and non-SSO authentication
|
- [x] Support both SSO and non-SSO authentication
|
||||||
|
- [x] Support the FIDO2 authentication (e.g., YubiKey)
|
||||||
- [x] Support authentication using default browser
|
- [x] Support authentication using default browser
|
||||||
- [x] Support multiple portals
|
- [x] Support multiple portals
|
||||||
- [x] Support gateway selection
|
- [x] Support gateway selection
|
||||||
|
@@ -284,12 +284,10 @@ fn raise_window(window: &Arc<Window>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn portal_prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<String> {
|
pub async fn portal_prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<String> {
|
||||||
info!("Portal prelogin...");
|
|
||||||
|
|
||||||
match prelogin(portal, gp_params).await? {
|
match prelogin(portal, gp_params).await? {
|
||||||
Prelogin::Saml(prelogin) => Ok(prelogin.saml_request().to_string()),
|
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"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -25,6 +25,8 @@ const VERSION: &str = concat!(
|
|||||||
struct Cli {
|
struct Cli {
|
||||||
server: String,
|
server: String,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
|
gateway: bool,
|
||||||
|
#[arg(long)]
|
||||||
saml_request: Option<String>,
|
saml_request: Option<String>,
|
||||||
#[arg(long, default_value = GP_USER_AGENT)]
|
#[arg(long, default_value = GP_USER_AGENT)]
|
||||||
user_agent: String,
|
user_agent: String,
|
||||||
@@ -102,6 +104,7 @@ impl Cli {
|
|||||||
.client_os(ClientOs::from(&self.os))
|
.client_os(ClientOs::from(&self.os))
|
||||||
.os_version(self.os_version.clone())
|
.os_version(self.os_version.clone())
|
||||||
.ignore_tls_errors(self.ignore_tls_errors)
|
.ignore_tls_errors(self.ignore_tls_errors)
|
||||||
|
.is_gateway(self.gateway)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
gp_params
|
gp_params
|
||||||
|
@@ -115,10 +115,8 @@ pub(crate) async fn run() {
|
|||||||
eprintln!("{} --fix-openssl {}\n", args[0], args[1..].join(" "));
|
eprintln!("{} --fix-openssl {}\n", args[0], args[1..].join(" "));
|
||||||
}
|
}
|
||||||
|
|
||||||
if err.contains("certificate verify failed") {
|
if err.contains("certificate verify failed") && !cli.ignore_tls_errors {
|
||||||
eprintln!(
|
eprintln!("\nRe-run it with the `--ignore-tls-errors` option to ignore the certificate error, e.g.:\n");
|
||||||
"\nRe-run it with the `--ignore-tls-errors` option to ignore the certificate error, e.g.:\n"
|
|
||||||
);
|
|
||||||
// Print the command
|
// Print the command
|
||||||
let args = std::env::args().collect::<Vec<_>>();
|
let args = std::env::args().collect::<Vec<_>>();
|
||||||
eprintln!("{} --ignore-tls-errors {}\n", args[0], args[1..].join(" "));
|
eprintln!("{} --ignore-tls-errors {}\n", args[0], args[1..].join(" "));
|
||||||
|
@@ -6,9 +6,9 @@ use gpapi::{
|
|||||||
credential::{Credential, PasswordCredential},
|
credential::{Credential, PasswordCredential},
|
||||||
gateway::gateway_login,
|
gateway::gateway_login,
|
||||||
gp_params::{ClientOs, GpParams},
|
gp_params::{ClientOs, GpParams},
|
||||||
portal::{prelogin, retrieve_config, Prelogin},
|
portal::{prelogin, retrieve_config, PortalError, Prelogin},
|
||||||
process::auth_launcher::SamlAuthLauncher,
|
process::auth_launcher::SamlAuthLauncher,
|
||||||
utils::{self, shutdown_signal},
|
utils::shutdown_signal,
|
||||||
GP_USER_AGENT,
|
GP_USER_AGENT,
|
||||||
};
|
};
|
||||||
use inquire::{Password, PasswordDisplayMode, Select, Text};
|
use inquire::{Password, PasswordDisplayMode, Select, Text};
|
||||||
@@ -71,19 +71,38 @@ impl<'a> ConnectHandler<'a> {
|
|||||||
Self { args, shared_args }
|
Self { args, shared_args }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn handle(&self) -> anyhow::Result<()> {
|
fn build_gp_params(&self) -> GpParams {
|
||||||
let portal = utils::normalize_server(self.args.server.as_str())?;
|
GpParams::builder()
|
||||||
|
|
||||||
let gp_params = GpParams::builder()
|
|
||||||
.user_agent(&self.args.user_agent)
|
.user_agent(&self.args.user_agent)
|
||||||
.client_os(ClientOs::from(&self.args.os))
|
.client_os(ClientOs::from(&self.args.os))
|
||||||
.os_version(self.args.os_version())
|
.os_version(self.args.os_version())
|
||||||
.ignore_tls_errors(self.shared_args.ignore_tls_errors)
|
.ignore_tls_errors(self.shared_args.ignore_tls_errors)
|
||||||
.build();
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
let prelogin = prelogin(&portal, &gp_params).await?;
|
pub(crate) async fn handle(&self) -> anyhow::Result<()> {
|
||||||
let portal_credential = self.obtain_portal_credential(&prelogin).await?;
|
let server = self.args.server.as_str();
|
||||||
let mut portal_config = retrieve_config(&portal, &portal_credential, &gp_params).await?;
|
|
||||||
|
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 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 {
|
let selected_gateway = match &self.args.gateway {
|
||||||
Some(gateway) => portal_config
|
Some(gateway) => portal_config
|
||||||
@@ -105,9 +124,32 @@ impl<'a> ConnectHandler<'a> {
|
|||||||
|
|
||||||
let gateway = selected_gateway.server();
|
let gateway = selected_gateway.server();
|
||||||
let cred = portal_config.auth_cookie().into();
|
let cred = portal_config.auth_cookie().into();
|
||||||
let token = gateway_login(gateway, &cred, &gp_params).await?;
|
|
||||||
|
|
||||||
let vpn = Vpn::builder(gateway, &token)
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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())
|
.user_agent(self.args.user_agent.clone())
|
||||||
.script(self.args.script.clone())
|
.script(self.args.script.clone())
|
||||||
.build();
|
.build();
|
||||||
@@ -132,10 +174,17 @@ impl<'a> ConnectHandler<'a> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn obtain_portal_credential(&self, prelogin: &Prelogin) -> anyhow::Result<Credential> {
|
async fn obtain_credential(
|
||||||
|
&self,
|
||||||
|
prelogin: &Prelogin,
|
||||||
|
server: &str,
|
||||||
|
) -> anyhow::Result<Credential> {
|
||||||
|
let is_gateway = prelogin.is_gateway();
|
||||||
|
|
||||||
match prelogin {
|
match prelogin {
|
||||||
Prelogin::Saml(prelogin) => {
|
Prelogin::Saml(prelogin) => {
|
||||||
SamlAuthLauncher::new(&self.args.server)
|
SamlAuthLauncher::new(&self.args.server)
|
||||||
|
.gateway(is_gateway)
|
||||||
.saml_request(prelogin.saml_request())
|
.saml_request(prelogin.saml_request())
|
||||||
.user_agent(&self.args.user_agent)
|
.user_agent(&self.args.user_agent)
|
||||||
.os(self.args.os.as_str())
|
.os(self.args.os.as_str())
|
||||||
@@ -148,7 +197,8 @@ impl<'a> ConnectHandler<'a> {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
Prelogin::Standard(prelogin) => {
|
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(
|
let user = self.args.user.as_ref().map_or_else(
|
||||||
|| Text::new(&format!("{}:", prelogin.label_username())).prompt(),
|
|| Text::new(&format!("{}:", prelogin.label_username())).prompt(),
|
||||||
|
@@ -2,8 +2,8 @@ mod cli;
|
|||||||
mod handlers;
|
mod handlers;
|
||||||
mod routes;
|
mod routes;
|
||||||
mod vpn_task;
|
mod vpn_task;
|
||||||
mod ws_server;
|
|
||||||
mod ws_connection;
|
mod ws_connection;
|
||||||
|
mod ws_server;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
@@ -1,6 +1,9 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use axum::{routing::{get, post}, Router};
|
use axum::{
|
||||||
|
routing::{get, post},
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{handlers, ws_server::WsServerContext};
|
use crate::{handlers, ws_server::WsServerContext};
|
||||||
|
|
||||||
|
@@ -139,6 +139,24 @@ impl CachedCredential {
|
|||||||
pub fn set_auth_cookie(&mut self, auth_cookie: AuthCookieCredential) {
|
pub fn set_auth_cookie(&mut self, auth_cookie: AuthCookieCredential) {
|
||||||
self.auth_cookie = auth_cookie;
|
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)]
|
#[derive(Debug, Serialize, Deserialize, Type, Clone)]
|
||||||
|
@@ -1,17 +1,26 @@
|
|||||||
|
use anyhow::bail;
|
||||||
use log::info;
|
use log::info;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use roxmltree::Document;
|
use roxmltree::Document;
|
||||||
use urlencoding::encode;
|
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(
|
pub async fn gateway_login(
|
||||||
gateway: &str,
|
gateway: &str,
|
||||||
cred: &Credential,
|
cred: &Credential,
|
||||||
gp_params: &GpParams,
|
gp_params: &GpParams,
|
||||||
) -> anyhow::Result<String> {
|
) -> 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()
|
let client = Client::builder()
|
||||||
|
.danger_accept_invalid_certs(gp_params.ignore_tls_errors())
|
||||||
.user_agent(gp_params.user_agent())
|
.user_agent(gp_params.user_agent())
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
@@ -19,13 +28,18 @@ pub async fn gateway_login(
|
|||||||
let extra_params = gp_params.to_params();
|
let extra_params = gp_params.to_params();
|
||||||
|
|
||||||
params.extend(extra_params);
|
params.extend(extra_params);
|
||||||
params.insert("server", gateway);
|
params.insert("server", &gateway);
|
||||||
|
|
||||||
info!("Gateway login, user_agent: {}", gp_params.user_agent());
|
info!("Gateway login, user_agent: {}", gp_params.user_agent());
|
||||||
|
|
||||||
let res = client.post(&login_url).form(¶ms).send().await?;
|
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)?;
|
let doc = Document::parse(&res_xml)?;
|
||||||
|
|
||||||
build_gateway_token(&doc, gp_params.computer())
|
build_gateway_token(&doc, gp_params.computer())
|
||||||
|
@@ -31,6 +31,15 @@ impl Display for Gateway {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Gateway {
|
impl Gateway {
|
||||||
|
pub fn new(name: String, address: String) -> Self {
|
||||||
|
Self {
|
||||||
|
name,
|
||||||
|
address,
|
||||||
|
priority: 0,
|
||||||
|
priority_rules: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn name(&self) -> &str {
|
pub fn name(&self) -> &str {
|
||||||
&self.name
|
&self.name
|
||||||
}
|
}
|
||||||
|
@@ -44,6 +44,7 @@ impl ClientOs {
|
|||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Type, Default)]
|
#[derive(Debug, Serialize, Deserialize, Type, Default)]
|
||||||
pub struct GpParams {
|
pub struct GpParams {
|
||||||
|
is_gateway: bool,
|
||||||
user_agent: String,
|
user_agent: String,
|
||||||
client_os: ClientOs,
|
client_os: ClientOs,
|
||||||
os_version: Option<String>,
|
os_version: Option<String>,
|
||||||
@@ -58,6 +59,14 @@ impl GpParams {
|
|||||||
GpParamsBuilder::new()
|
GpParamsBuilder::new()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_gateway(&self) -> bool {
|
||||||
|
self.is_gateway
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_is_gateway(&mut self, is_gateway: bool) {
|
||||||
|
self.is_gateway = is_gateway;
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn user_agent(&self) -> &str {
|
pub(crate) fn user_agent(&self) -> &str {
|
||||||
&self.user_agent
|
&self.user_agent
|
||||||
}
|
}
|
||||||
@@ -103,6 +112,7 @@ impl GpParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct GpParamsBuilder {
|
pub struct GpParamsBuilder {
|
||||||
|
is_gateway: bool,
|
||||||
user_agent: String,
|
user_agent: String,
|
||||||
client_os: ClientOs,
|
client_os: ClientOs,
|
||||||
os_version: Option<String>,
|
os_version: Option<String>,
|
||||||
@@ -115,6 +125,7 @@ pub struct GpParamsBuilder {
|
|||||||
impl GpParamsBuilder {
|
impl GpParamsBuilder {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
is_gateway: false,
|
||||||
user_agent: GP_USER_AGENT.to_string(),
|
user_agent: GP_USER_AGENT.to_string(),
|
||||||
client_os: ClientOs::Linux,
|
client_os: ClientOs::Linux,
|
||||||
os_version: Default::default(),
|
os_version: Default::default(),
|
||||||
@@ -125,6 +136,11 @@ impl GpParamsBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_gateway(&mut self, is_gateway: bool) -> &mut Self {
|
||||||
|
self.is_gateway = is_gateway;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn user_agent(&mut self, user_agent: &str) -> &mut Self {
|
pub fn user_agent(&mut self, user_agent: &str) -> &mut Self {
|
||||||
self.user_agent = user_agent.to_string();
|
self.user_agent = user_agent.to_string();
|
||||||
self
|
self
|
||||||
@@ -162,6 +178,7 @@ impl GpParamsBuilder {
|
|||||||
|
|
||||||
pub fn build(&self) -> GpParams {
|
pub fn build(&self) -> GpParams {
|
||||||
GpParams {
|
GpParams {
|
||||||
|
is_gateway: self.is_gateway,
|
||||||
user_agent: self.user_agent.clone(),
|
user_agent: self.user_agent.clone(),
|
||||||
client_os: self.client_os.clone(),
|
client_os: self.client_os.clone(),
|
||||||
os_version: self.os_version.clone(),
|
os_version: self.os_version.clone(),
|
||||||
|
@@ -1,16 +1,16 @@
|
|||||||
use anyhow::ensure;
|
use anyhow::bail;
|
||||||
use log::info;
|
use log::info;
|
||||||
use reqwest::Client;
|
use reqwest::{Client, StatusCode};
|
||||||
use roxmltree::Document;
|
use roxmltree::Document;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use specta::Type;
|
use specta::Type;
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
credential::{AuthCookieCredential, Credential},
|
credential::{AuthCookieCredential, Credential},
|
||||||
gateway::{parse_gateways, Gateway},
|
gateway::{parse_gateways, Gateway},
|
||||||
gp_params::GpParams,
|
gp_params::GpParams,
|
||||||
utils::{normalize_server, xml},
|
portal::PortalError,
|
||||||
|
utils::{normalize_server, remove_url_scheme, xml},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Type)]
|
#[derive(Debug, Serialize, Type)]
|
||||||
@@ -18,25 +18,12 @@ use crate::{
|
|||||||
pub struct PortalConfig {
|
pub struct PortalConfig {
|
||||||
portal: String,
|
portal: String,
|
||||||
auth_cookie: AuthCookieCredential,
|
auth_cookie: AuthCookieCredential,
|
||||||
|
config_cred: Credential,
|
||||||
gateways: Vec<Gateway>,
|
gateways: Vec<Gateway>,
|
||||||
config_digest: Option<String>,
|
config_digest: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PortalConfig {
|
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 {
|
pub fn portal(&self) -> &str {
|
||||||
&self.portal
|
&self.portal
|
||||||
}
|
}
|
||||||
@@ -49,6 +36,10 @@ impl PortalConfig {
|
|||||||
&self.auth_cookie
|
&self.auth_cookie
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn config_cred(&self) -> &Credential {
|
||||||
|
&self.config_cred
|
||||||
|
}
|
||||||
|
|
||||||
/// In-place sort the gateways by region
|
/// In-place sort the gateways by region
|
||||||
pub fn sort_gateways(&mut self, region: &str) {
|
pub fn sort_gateways(&mut self, region: &str) {
|
||||||
let preferred_gateway = self.find_preferred_gateway(region);
|
let preferred_gateway = self.find_preferred_gateway(region);
|
||||||
@@ -98,18 +89,6 @@ impl PortalConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum PortalConfigError {
|
|
||||||
#[error("Empty response, retrying can help")]
|
|
||||||
EmptyResponse,
|
|
||||||
#[error("Empty auth cookie, retrying can help")]
|
|
||||||
EmptyAuthCookie,
|
|
||||||
#[error("Invalid auth cookie, retrying can help")]
|
|
||||||
InvalidAuthCookie,
|
|
||||||
#[error("Empty gateways, retrying can help")]
|
|
||||||
EmptyGateways,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn retrieve_config(
|
pub async fn retrieve_config(
|
||||||
portal: &str,
|
portal: &str,
|
||||||
cred: &Credential,
|
cred: &Credential,
|
||||||
@@ -120,6 +99,7 @@ pub async fn retrieve_config(
|
|||||||
|
|
||||||
let url = format!("{}/global-protect/getconfig.esp", portal);
|
let url = format!("{}/global-protect/getconfig.esp", portal);
|
||||||
let client = Client::builder()
|
let client = Client::builder()
|
||||||
|
.danger_accept_invalid_certs(gp_params.ignore_tls_errors())
|
||||||
.user_agent(gp_params.user_agent())
|
.user_agent(gp_params.user_agent())
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
@@ -133,42 +113,54 @@ pub async fn retrieve_config(
|
|||||||
info!("Portal config, user_agent: {}", gp_params.user_agent());
|
info!("Portal config, user_agent: {}", gp_params.user_agent());
|
||||||
|
|
||||||
let res = client.post(&url).form(¶ms).send().await?;
|
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)?;
|
if status.is_client_error() || status.is_server_error() {
|
||||||
let gateways = parse_gateways(&doc).ok_or_else(|| anyhow::anyhow!("Failed to parse gateways"))?;
|
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 user_auth_cookie = xml::get_child_text(&doc, "portal-userauthcookie").unwrap_or_default();
|
||||||
let prelogon_user_auth_cookie =
|
let prelogon_user_auth_cookie =
|
||||||
xml::get_child_text(&doc, "portal-prelogonuserauthcookie").unwrap_or_default();
|
xml::get_child_text(&doc, "portal-prelogonuserauthcookie").unwrap_or_default();
|
||||||
let config_digest = xml::get_child_text(&doc, "config-digest");
|
let config_digest = xml::get_child_text(&doc, "config-digest");
|
||||||
|
|
||||||
ensure!(
|
if gateways.is_empty() {
|
||||||
!user_auth_cookie.is_empty() && !prelogon_user_auth_cookie.is_empty(),
|
gateways.push(Gateway::new(server.to_string(), server.to_string()));
|
||||||
PortalConfigError::EmptyAuthCookie
|
}
|
||||||
);
|
|
||||||
|
|
||||||
ensure!(
|
Ok(PortalConfig {
|
||||||
user_auth_cookie != "empty" && prelogon_user_auth_cookie != "empty",
|
portal: server.to_string(),
|
||||||
PortalConfigError::InvalidAuthCookie
|
auth_cookie: AuthCookieCredential::new(
|
||||||
);
|
|
||||||
|
|
||||||
ensure!(!gateways.is_empty(), PortalConfigError::EmptyGateways);
|
|
||||||
|
|
||||||
Ok(PortalConfig::new(
|
|
||||||
server.to_string(),
|
|
||||||
AuthCookieCredential::new(
|
|
||||||
cred.username(),
|
cred.username(),
|
||||||
&user_auth_cookie,
|
&user_auth_cookie,
|
||||||
&prelogon_user_auth_cookie,
|
&prelogon_user_auth_cookie,
|
||||||
),
|
),
|
||||||
|
config_cred: cred.clone(),
|
||||||
gateways,
|
gateways,
|
||||||
config_digest,
|
config_digest,
|
||||||
))
|
})
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_url_scheme(s: &str) -> String {
|
|
||||||
s.replace("http://", "").replace("https://", "")
|
|
||||||
}
|
}
|
||||||
|
@@ -3,3 +3,13 @@ mod prelogin;
|
|||||||
|
|
||||||
pub use config::*;
|
pub use config::*;
|
||||||
pub use prelogin::*;
|
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),
|
||||||
|
}
|
||||||
|
@@ -1,12 +1,13 @@
|
|||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
use log::{info, trace};
|
use log::info;
|
||||||
use reqwest::Client;
|
use reqwest::{Client, StatusCode};
|
||||||
use roxmltree::Document;
|
use roxmltree::Document;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use specta::Type;
|
use specta::Type;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
gp_params::GpParams,
|
gp_params::GpParams,
|
||||||
|
portal::PortalError,
|
||||||
utils::{base64, normalize_server, xml},
|
utils::{base64, normalize_server, xml},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -25,6 +26,7 @@ const REQUIRED_PARAMS: [&str; 8] = [
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct SamlPrelogin {
|
pub struct SamlPrelogin {
|
||||||
region: String,
|
region: String,
|
||||||
|
is_gateway: bool,
|
||||||
saml_request: String,
|
saml_request: String,
|
||||||
support_default_browser: bool,
|
support_default_browser: bool,
|
||||||
}
|
}
|
||||||
@@ -47,6 +49,7 @@ impl SamlPrelogin {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct StandardPrelogin {
|
pub struct StandardPrelogin {
|
||||||
region: String,
|
region: String,
|
||||||
|
is_gateway: bool,
|
||||||
auth_message: String,
|
auth_message: String,
|
||||||
label_username: String,
|
label_username: String,
|
||||||
label_password: String,
|
label_password: String,
|
||||||
@@ -84,18 +87,30 @@ impl Prelogin {
|
|||||||
Prelogin::Standard(standard) => standard.region(),
|
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> {
|
pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prelogin> {
|
||||||
let user_agent = gp_params.user_agent();
|
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 portal = normalize_server(portal)?;
|
||||||
let prelogin_url = format!("{}/global-protect/prelogin.esp", portal);
|
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();
|
let mut params = gp_params.to_params();
|
||||||
|
|
||||||
params.insert("tmp", "tmp");
|
params.insert("tmp", "tmp");
|
||||||
params.insert("cas-support", "yes");
|
|
||||||
if gp_params.prefer_default_browser() {
|
if gp_params.prefer_default_browser() {
|
||||||
params.insert("default-browser", "1");
|
params.insert("default-browser", "1");
|
||||||
}
|
}
|
||||||
@@ -112,9 +127,30 @@ pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prel
|
|||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
let res = client.post(&prelogin_url).form(¶ms).send().await?;
|
let res = client.post(&prelogin_url).form(¶ms).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 doc = Document::parse(&res_xml)?;
|
||||||
|
|
||||||
let status = xml::get_child_text(&doc, "status")
|
let status = xml::get_child_text(&doc, "status")
|
||||||
@@ -140,6 +176,7 @@ pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prel
|
|||||||
|
|
||||||
let saml_prelogin = SamlPrelogin {
|
let saml_prelogin = SamlPrelogin {
|
||||||
region,
|
region,
|
||||||
|
is_gateway,
|
||||||
saml_request,
|
saml_request,
|
||||||
support_default_browser,
|
support_default_browser,
|
||||||
};
|
};
|
||||||
@@ -155,6 +192,7 @@ pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prel
|
|||||||
.unwrap_or(String::from("Please enter the login credentials"));
|
.unwrap_or(String::from("Please enter the login credentials"));
|
||||||
let standard_prelogin = StandardPrelogin {
|
let standard_prelogin = StandardPrelogin {
|
||||||
region,
|
region,
|
||||||
|
is_gateway,
|
||||||
auth_message,
|
auth_message,
|
||||||
label_username: label_username.unwrap(),
|
label_username: label_username.unwrap(),
|
||||||
label_password: label_password.unwrap(),
|
label_password: label_password.unwrap(),
|
||||||
|
@@ -8,6 +8,7 @@ use super::command_traits::CommandExt;
|
|||||||
|
|
||||||
pub struct SamlAuthLauncher<'a> {
|
pub struct SamlAuthLauncher<'a> {
|
||||||
server: &'a str,
|
server: &'a str,
|
||||||
|
gateway: bool,
|
||||||
saml_request: Option<&'a str>,
|
saml_request: Option<&'a str>,
|
||||||
user_agent: Option<&'a str>,
|
user_agent: Option<&'a str>,
|
||||||
os: Option<&'a str>,
|
os: Option<&'a str>,
|
||||||
@@ -22,6 +23,7 @@ impl<'a> SamlAuthLauncher<'a> {
|
|||||||
pub fn new(server: &'a str) -> Self {
|
pub fn new(server: &'a str) -> Self {
|
||||||
Self {
|
Self {
|
||||||
server,
|
server,
|
||||||
|
gateway: false,
|
||||||
saml_request: None,
|
saml_request: None,
|
||||||
user_agent: None,
|
user_agent: None,
|
||||||
os: 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 {
|
pub fn saml_request(mut self, saml_request: &'a str) -> Self {
|
||||||
self.saml_request = Some(saml_request);
|
self.saml_request = Some(saml_request);
|
||||||
self
|
self
|
||||||
@@ -78,6 +85,10 @@ impl<'a> SamlAuthLauncher<'a> {
|
|||||||
let mut auth_cmd = Command::new(GP_AUTH_BINARY);
|
let mut auth_cmd = Command::new(GP_AUTH_BINARY);
|
||||||
auth_cmd.arg(self.server);
|
auth_cmd.arg(self.server);
|
||||||
|
|
||||||
|
if self.gateway {
|
||||||
|
auth_cmd.arg("--gateway");
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(saml_request) = self.saml_request {
|
if let Some(saml_request) = self.saml_request {
|
||||||
auth_cmd.arg("--saml-request").arg(saml_request);
|
auth_cmd.arg("--saml-request").arg(saml_request);
|
||||||
}
|
}
|
||||||
|
@@ -38,3 +38,7 @@ pub fn normalize_server(server: &str) -> anyhow::Result<String> {
|
|||||||
|
|
||||||
Ok(normalized_url)
|
Ok(normalized_url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn remove_url_scheme(s: &str) -> String {
|
||||||
|
s.replace("http://", "").replace("https://", "")
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user