Compare commits

..

7 Commits

Author SHA1 Message Date
Kevin Yue
9655b735a1 Fix ignore TLS errors 2024-01-22 23:20:25 -05:00
Kevin Yue
c3bd7aeb93 Support SSO using default browser 2024-01-22 09:43:44 -05:00
Kevin Yue
0b55a80317 Bump version 2.0.0-beta4 2024-01-21 11:05:15 -05:00
Kevin Yue
c6315bf384 Handle auth window auth fail 2024-01-21 11:04:35 -05:00
Kevin Yue
87b965f80c Add default os-version for CLI 2024-01-21 08:54:08 -05:00
Kevin Yue
b09b21ae0f Bump 2.0.0-beta3 2024-01-21 05:43:49 -05:00
Kevin Yue
7e372cd113 Align with the old behavior of the portal config request (#293) 2024-01-21 18:31:39 +08:00
21 changed files with 272 additions and 98 deletions

View File

@@ -11,8 +11,10 @@
"dotenvy", "dotenvy",
"getconfig", "getconfig",
"globalprotect", "globalprotect",
"globalprotectcallback",
"gpapi", "gpapi",
"gpauth", "gpauth",
"gpcallback",
"gpclient", "gpclient",
"gpcommon", "gpcommon",
"gpgui", "gpgui",
@@ -48,6 +50,8 @@
"vpnc", "vpnc",
"vpninfo", "vpninfo",
"wmctrl", "wmctrl",
"XAUTHORITY" "XAUTHORITY",
] "yuezk"
],
"rust-analyzer.cargo.features": "all",
} }

47
Cargo.lock generated
View File

@@ -1423,7 +1423,7 @@ dependencies = [
[[package]] [[package]]
name = "gpapi" name = "gpapi"
version = "2.0.0-beta2" version = "2.0.0-beta6"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64 0.21.5", "base64 0.21.5",
@@ -1431,6 +1431,7 @@ dependencies = [
"clap", "clap",
"dotenvy_macro", "dotenvy_macro",
"log", "log",
"open",
"redact-engine", "redact-engine",
"regex", "regex",
"reqwest", "reqwest",
@@ -1451,7 +1452,7 @@ dependencies = [
[[package]] [[package]]
name = "gpauth" name = "gpauth"
version = "2.0.0-beta2" version = "2.0.0-beta6"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@@ -1471,7 +1472,7 @@ dependencies = [
[[package]] [[package]]
name = "gpclient" name = "gpclient"
version = "2.0.0-beta2" version = "2.0.0-beta6"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@@ -1492,7 +1493,7 @@ dependencies = [
[[package]] [[package]]
name = "gpservice" name = "gpservice"
version = "2.0.0-beta2" version = "2.0.0-beta6"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum", "axum",
@@ -1963,6 +1964,15 @@ version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
[[package]]
name = "is-docker"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
dependencies = [
"once_cell",
]
[[package]] [[package]]
name = "is-terminal" name = "is-terminal"
version = "0.4.10" version = "0.4.10"
@@ -1974,6 +1984,16 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "is-wsl"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
dependencies = [
"is-docker",
"once_cell",
]
[[package]] [[package]]
name = "is_executable" name = "is_executable"
version = "1.0.1" version = "1.0.1"
@@ -2445,9 +2465,20 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "open"
version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90878fb664448b54c4e592455ad02831e23a3f7e157374a8b95654731aac7349"
dependencies = [
"is-wsl",
"libc",
"pathdiff",
]
[[package]] [[package]]
name = "openconnect" name = "openconnect"
version = "2.0.0-beta2" version = "2.0.0-beta6"
dependencies = [ dependencies = [
"cc", "cc",
"is_executable", "is_executable",
@@ -2574,6 +2605,12 @@ version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
[[package]]
name = "pathdiff"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.1" version = "2.3.1"

View File

@@ -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-beta2" version = "2.0.0-beta6"
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"

View File

@@ -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 authentication using default browser
- [x] Support multiple portals - [x] Support multiple portals
- [x] Support gateway selection - [x] Support gateway selection
- [x] Support auto-connect on startup - [x] Support auto-connect on startup
@@ -54,6 +55,11 @@ The GUI version is also available after you installed it. You can launch it from
> >
> This instruction is for the 2.x version. The 1.x version is still available on the [1.x](https://github.com/yuezk/GlobalProtect-openconnect/tree/1.x) branch, you can build it from the source code by following the instructions in the `README.md` file. > This instruction is for the 2.x version. The 1.x version is still available on the [1.x](https://github.com/yuezk/GlobalProtect-openconnect/tree/1.x) branch, you can build it from the source code by following the instructions in the `README.md` file.
> [!Warning]
>
> The client requires `openconnect >= 8.20`, please make sure you have it installed, you can check it with `openconnect --version`.
> Installing the client from PPA will automatically install the required version of `openconnect`.
### Debian/Ubuntu based distributions ### Debian/Ubuntu based distributions
#### Install from PPA #### Install from PPA
@@ -82,6 +88,10 @@ sudo dpkg -i globalprotect-openconnect_*.deb
Install from AUR: [globalprotect-openconnect-git](https://aur.archlinux.org/packages/globalprotect-openconnect-git/) Install from AUR: [globalprotect-openconnect-git](https://aur.archlinux.org/packages/globalprotect-openconnect-git/)
```
yay -S globalprotect-openconnect-git
```
#### Install from package #### Install from package
Download the latest package from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. Then install it with `pacman`: Download the latest package from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. Then install it with `pacman`:
@@ -111,7 +121,7 @@ Download the latest RPM package from [releases](https://github.com/yuezk/GlobalP
### Other distributions ### Other distributions
The project depends on `openconnect`, `webkit2gtk`, `libsecret`, `libayatana-appindicator` or `libappindicator-gtk3`. You can install them first and then download the latest binary release (i.e., `*.bin.tar.gz`) from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. The project depends on `openconnect >= 8.20`, `webkit2gtk`, `libsecret`, `libayatana-appindicator` or `libappindicator-gtk3`. You can install them first and then download the latest binary release (i.e., `*.bin.tar.gz`) from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page.
## [License](./LICENSE) ## [License](./LICENSE)

View File

@@ -203,7 +203,8 @@ impl<'a> AuthWindow<'a> {
wv.connect_load_failed(move |_wv, _event, uri, err| { wv.connect_load_failed(move |_wv, _event, uri, err| {
let redacted_uri = redact_uri(uri); let redacted_uri = redact_uri(uri);
warn!("Failed to load uri: {} with error: {}", redacted_uri, err); warn!("Failed to load uri: {} with error: {}", redacted_uri, err);
send_auth_result(&auth_result_tx, Err(AuthDataError::Invalid)); // NOTE: Don't send error here, since load_changed event will be triggered after this
// send_auth_result(&auth_result_tx, Err(AuthDataError::Invalid));
// true to stop other handlers from being invoked for the event. false to propagate the event further. // true to stop other handlers from being invoked for the event. false to propagate the event further.
true true
}); });

View File

@@ -115,7 +115,7 @@ 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"
); );

View File

@@ -47,6 +47,20 @@ pub(crate) struct ConnectArgs {
clean: bool, clean: bool,
} }
impl ConnectArgs {
fn os_version(&self) -> String {
if let Some(os_version) = &self.os_version {
return os_version.to_owned();
}
match self.os {
Os::Linux => format!("Linux {}", whoami::distro()),
Os::Windows => String::from("Microsoft Windows 11 Pro , 64-bit"),
Os::Mac => String::from("Apple Mac OS X 13.4.0"),
}
}
}
pub(crate) struct ConnectHandler<'a> { pub(crate) struct ConnectHandler<'a> {
args: &'a ConnectArgs, args: &'a ConnectArgs,
shared_args: &'a SharedArgs, shared_args: &'a SharedArgs,
@@ -63,7 +77,7 @@ impl<'a> ConnectHandler<'a> {
let gp_params = 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.clone()) .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();
@@ -125,7 +139,7 @@ impl<'a> ConnectHandler<'a> {
.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())
.os_version(self.args.os_version.as_deref()) .os_version(Some(&self.args.os_version()))
.hidpi(self.args.hidpi) .hidpi(self.args.hidpi)
.fix_openssl(self.shared_args.fix_openssl) .fix_openssl(self.shared_args.fix_openssl)
.ignore_tls_errors(self.shared_args.ignore_tls_errors) .ignore_tls_errors(self.shared_args.ignore_tls_errors)

View File

@@ -10,7 +10,12 @@ use log::info;
#[derive(Args)] #[derive(Args)]
pub(crate) struct LaunchGuiArgs { pub(crate) struct LaunchGuiArgs {
#[clap(long, help = "Launch the GUI minimized")] #[arg(
required = false,
help = "The authentication data, used for the default browser authentication"
)]
auth_data: Option<String>,
#[arg(long, help = "Launch the GUI minimized")]
minimized: bool, minimized: bool,
} }
@@ -30,6 +35,12 @@ impl<'a> LaunchGuiHandler<'a> {
anyhow::bail!("`launch-gui` cannot be run as root"); anyhow::bail!("`launch-gui` cannot be run as root");
} }
let auth_data = self.args.auth_data.as_deref().unwrap_or_default();
if !auth_data.is_empty() {
// Process the authentication data, its format is `globalprotectcallback:<data>`
return feed_auth_data(auth_data).await;
}
if try_active_gui().await.is_ok() { if try_active_gui().await.is_ok() {
info!("The GUI is already running"); info!("The GUI is already running");
return Ok(()); return Ok(());
@@ -66,6 +77,19 @@ impl<'a> LaunchGuiHandler<'a> {
} }
} }
async fn feed_auth_data(auth_data: &str) -> anyhow::Result<()> {
let service_endpoint = http_endpoint().await?;
reqwest::Client::default()
.post(format!("{}/auth-data", service_endpoint))
.json(&auth_data)
.send()
.await?
.error_for_status()?;
Ok(())
}
async fn try_active_gui() -> anyhow::Result<()> { async fn try_active_gui() -> anyhow::Result<()> {
let service_endpoint = http_endpoint().await?; let service_endpoint = http_endpoint().await?;

View File

@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
<policyconfig>
<vendor>GlobalProtect-openconnect</vendor>
<vendor_url>https://github.com/yuezk/GlobalProtect-openconnect</vendor_url>
<icon_name>gpgui</icon_name>
<action id="com.yuezk.gpservice">
<description>Run GPService as root</description>
<message>Authentication is required to run the GPService as root</message>
<defaults>
<allow_any>yes</allow_any>
<allow_inactive>yes</allow_inactive>
<allow_active>yes</allow_active>
</defaults>
<annotate key="org.freedesktop.policykit.exec.path">/home/kevin/Documents/repos/gp/target/debug/gpservice</annotate>
<annotate key="org.freedesktop.policykit.exec.argv1">--with-gui</annotate>
<annotate key="org.freedesktop.policykit.exec.allow_gui">true</annotate>
</action>
</policyconfig>

View File

@@ -21,6 +21,13 @@ pub(crate) async fn active_gui(State(ctx): State<Arc<WsServerContext>>) -> impl
ctx.send_event(WsEvent::ActiveGui).await; ctx.send_event(WsEvent::ActiveGui).await;
} }
pub(crate) async fn auth_data(
State(ctx): State<Arc<WsServerContext>>,
body: String,
) -> impl IntoResponse {
ctx.send_event(WsEvent::AuthData(body)).await;
}
pub(crate) async fn ws_handler( pub(crate) async fn ws_handler(
ws: WebSocketUpgrade, ws: WebSocketUpgrade,
State(ctx): State<Arc<WsServerContext>>, State(ctx): State<Arc<WsServerContext>>,

View File

@@ -8,6 +8,7 @@ pub(crate) fn routes(ctx: Arc<WsServerContext>) -> Router {
Router::new() Router::new()
.route("/health", get(handlers::health)) .route("/health", get(handlers::health))
.route("/active-gui", post(handlers::active_gui)) .route("/active-gui", post(handlers::active_gui))
.route("/auth-data", post(handlers::auth_data))
.route("/ws", get(handlers::ws_handler)) .route("/ws", get(handlers::ws_handler))
.with_state(ctx) .with_state(ctx)
} }

View File

@@ -28,7 +28,9 @@ uzers.workspace = true
tauri = { workspace = true, optional = true } tauri = { workspace = true, optional = true }
clap = { workspace = true, optional = true } clap = { workspace = true, optional = true }
open = { version = "5", optional = true }
[features] [features]
tauri = ["dep:tauri"] tauri = ["dep:tauri"]
clap = ["dep:clap"] clap = ["dep:clap"]
browser-auth = ["dep:open"]

View File

@@ -1,3 +1,5 @@
use anyhow::bail;
use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
@@ -37,6 +39,32 @@ impl SamlAuthData {
} }
} }
pub fn parse_html(html: &str) -> anyhow::Result<SamlAuthData> {
match parse_xml_tag(html, "saml-auth-status") {
Some(saml_status) if saml_status == "1" => {
let username = parse_xml_tag(html, "saml-username");
let prelogin_cookie = parse_xml_tag(html, "prelogin-cookie");
let portal_userauthcookie = parse_xml_tag(html, "portal-userauthcookie");
if SamlAuthData::check(&username, &prelogin_cookie, &portal_userauthcookie) {
return Ok(SamlAuthData::new(
username.unwrap(),
prelogin_cookie,
portal_userauthcookie,
));
}
bail!("Found invalid auth data in HTML");
}
Some(status) => {
bail!("Found invalid SAML status {} in HTML", status);
}
None => {
bail!("No auth data found in HTML");
}
}
}
pub fn username(&self) -> &str { pub fn username(&self) -> &str {
&self.username &self.username
} }
@@ -61,3 +89,10 @@ impl SamlAuthData {
username_valid && (prelogin_cookie_valid || portal_userauthcookie_valid) username_valid && (prelogin_cookie_valid || portal_userauthcookie_valid)
} }
} }
pub fn parse_xml_tag(html: &str, tag: &str) -> Option<String> {
let re = Regex::new(&format!("<{}>(.*)</{}>", tag, tag)).unwrap();
re.captures(html)
.and_then(|captures| captures.get(1))
.map(|m| m.as_str().to_string())
}

View File

@@ -3,7 +3,7 @@ use std::collections::HashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use specta::Type; use specta::Type;
use crate::auth::SamlAuthData; use crate::{auth::SamlAuthData, utils::base64::decode_to_string};
#[derive(Debug, Serialize, Deserialize, Type, Clone)] #[derive(Debug, Serialize, Deserialize, Type, Clone)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@@ -151,6 +151,17 @@ pub enum Credential {
} }
impl Credential { impl Credential {
/// Create a credential from a globalprotectcallback:<base64 encoded string>
pub fn parse_gpcallback(auth_data: &str) -> anyhow::Result<Self> {
// Remove the surrounding quotes
let auth_data = auth_data.trim_matches('"');
let auth_data = auth_data.trim_start_matches("globalprotectcallback:");
let auth_data = decode_to_string(auth_data)?;
let auth_data = SamlAuthData::parse_html(&auth_data)?;
Self::try_from(auth_data)
}
pub fn username(&self) -> &str { pub fn username(&self) -> &str {
match self { match self {
Credential::Password(cred) => cred.username(), Credential::Password(cred) => cred.username(),
@@ -164,31 +175,34 @@ impl Credential {
let mut params = HashMap::new(); let mut params = HashMap::new();
params.insert("user", self.username()); params.insert("user", self.username());
match self { let (passwd, prelogin_cookie, portal_userauthcookie, portal_prelogonuserauthcookie) = match self
Credential::Password(cred) => { {
params.insert("passwd", cred.password()); Credential::Password(cred) => (Some(cred.password()), None, None, None),
} Credential::PreloginCookie(cred) => (None, Some(cred.prelogin_cookie()), None, None),
Credential::PreloginCookie(cred) => { Credential::AuthCookie(cred) => (
params.insert("prelogin-cookie", cred.prelogin_cookie()); None,
} None,
Credential::AuthCookie(cred) => { Some(cred.user_auth_cookie()),
params.insert("portal-userauthcookie", cred.user_auth_cookie()); Some(cred.prelogon_user_auth_cookie()),
params.insert( ),
"portal-prelogonuserauthcookie", Credential::CachedCredential(cred) => (
cred.prelogon_user_auth_cookie(), cred.password(),
); None,
} Some(cred.auth_cookie.user_auth_cookie()),
Credential::CachedCredential(cred) => { Some(cred.auth_cookie.prelogon_user_auth_cookie()),
if let Some(password) = cred.password() { ),
params.insert("passwd", password); };
}
params.insert("portal-userauthcookie", cred.auth_cookie.user_auth_cookie()); params.insert("passwd", passwd.unwrap_or_default());
params.insert( params.insert("prelogin-cookie", prelogin_cookie.unwrap_or_default());
"portal-prelogonuserauthcookie", params.insert(
cred.auth_cookie.prelogon_user_auth_cookie(), "portal-userauthcookie",
); portal_userauthcookie.unwrap_or_default(),
} );
} params.insert(
"portal-prelogonuserauthcookie",
portal_prelogonuserauthcookie.unwrap_or_default(),
);
params params
} }

View File

@@ -12,6 +12,7 @@ pub async fn gateway_login(
) -> anyhow::Result<String> { ) -> anyhow::Result<String> {
let login_url = format!("https://{}/ssl-vpn/login.esp", gateway); let login_url = format!("https://{}/ssl-vpn/login.esp", gateway);
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()?;
@@ -23,14 +24,8 @@ pub async fn gateway_login(
info!("Gateway login, user_agent: {}", gp_params.user_agent()); info!("Gateway login, user_agent: {}", gp_params.user_agent());
let res_xml = client let res = client.post(&login_url).form(&params).send().await?;
.post(&login_url) let res_xml = res.error_for_status()?.text().await?;
.form(&params)
.send()
.await?
.error_for_status()?
.text()
.await?;
let doc = Document::parse(&res_xml)?; let doc = Document::parse(&res_xml)?;

View File

@@ -48,8 +48,9 @@ pub struct GpParams {
client_os: ClientOs, client_os: ClientOs,
os_version: Option<String>, os_version: Option<String>,
client_version: Option<String>, client_version: Option<String>,
computer: Option<String>, computer: String,
ignore_tls_errors: bool, ignore_tls_errors: bool,
prefer_default_browser: bool,
} }
impl GpParams { impl GpParams {
@@ -62,16 +63,17 @@ impl GpParams {
} }
pub(crate) fn computer(&self) -> &str { pub(crate) fn computer(&self) -> &str {
match self.computer { &self.computer
Some(ref computer) => computer,
None => self.client_os.as_str(),
}
} }
pub fn ignore_tls_errors(&self) -> bool { pub fn ignore_tls_errors(&self) -> bool {
self.ignore_tls_errors self.ignore_tls_errors
} }
pub fn prefer_default_browser(&self) -> bool {
self.prefer_default_browser
}
pub(crate) fn to_params(&self) -> HashMap<&str, &str> { pub(crate) fn to_params(&self) -> HashMap<&str, &str> {
let mut params: HashMap<&str, &str> = HashMap::new(); let mut params: HashMap<&str, &str> = HashMap::new();
let client_os = self.client_os.as_str(); let client_os = self.client_os.as_str();
@@ -84,22 +86,17 @@ impl GpParams {
params.insert("ipv6-support", "yes"); params.insert("ipv6-support", "yes");
params.insert("inputStr", ""); params.insert("inputStr", "");
params.insert("clientVer", "4100"); params.insert("clientVer", "4100");
params.insert("clientos", client_os); params.insert("clientos", client_os);
params.insert("computer", &self.computer);
if let Some(computer) = &self.computer {
params.insert("computer", computer);
} else {
params.insert("computer", client_os);
}
if let Some(os_version) = &self.os_version { if let Some(os_version) = &self.os_version {
params.insert("os-version", os_version); params.insert("os-version", os_version);
} }
if let Some(client_version) = &self.client_version { // NOTE: Do not include clientgpversion for now
params.insert("clientgpversion", client_version); // if let Some(client_version) = &self.client_version {
} // params.insert("clientgpversion", client_version);
// }
params params
} }
@@ -110,8 +107,9 @@ pub struct GpParamsBuilder {
client_os: ClientOs, client_os: ClientOs,
os_version: Option<String>, os_version: Option<String>,
client_version: Option<String>, client_version: Option<String>,
computer: Option<String>, computer: String,
ignore_tls_errors: bool, ignore_tls_errors: bool,
prefer_default_browser: bool,
} }
impl GpParamsBuilder { impl GpParamsBuilder {
@@ -121,8 +119,9 @@ impl GpParamsBuilder {
client_os: ClientOs::Linux, client_os: ClientOs::Linux,
os_version: Default::default(), os_version: Default::default(),
client_version: Default::default(), client_version: Default::default(),
computer: Default::default(), computer: whoami::hostname(),
ignore_tls_errors: false, ignore_tls_errors: false,
prefer_default_browser: false,
} }
} }
@@ -147,7 +146,7 @@ impl GpParamsBuilder {
} }
pub fn computer(&mut self, computer: &str) -> &mut Self { pub fn computer(&mut self, computer: &str) -> &mut Self {
self.computer = Some(computer.to_string()); self.computer = computer.to_string();
self self
} }
@@ -156,6 +155,11 @@ impl GpParamsBuilder {
self self
} }
pub fn prefer_default_browser(&mut self, prefer_default_browser: bool) -> &mut Self {
self.prefer_default_browser = prefer_default_browser;
self
}
pub fn build(&self) -> GpParams { pub fn build(&self) -> GpParams {
GpParams { GpParams {
user_agent: self.user_agent.clone(), user_agent: self.user_agent.clone(),
@@ -164,6 +168,7 @@ impl GpParamsBuilder {
client_version: self.client_version.clone(), client_version: self.client_version.clone(),
computer: self.computer.clone(), computer: self.computer.clone(),
ignore_tls_errors: self.ignore_tls_errors, ignore_tls_errors: self.ignore_tls_errors,
prefer_default_browser: self.prefer_default_browser,
} }
} }
} }

View File

@@ -120,6 +120,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()?;
@@ -132,14 +133,8 @@ 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_xml = client let res = client.post(&url).form(&params).send().await?;
.post(&url) let res_xml = res.error_for_status()?.text().await?;
.form(&params)
.send()
.await?
.error_for_status()?
.text()
.await?;
ensure!(!res_xml.is_empty(), PortalConfigError::EmptyResponse); ensure!(!res_xml.is_empty(), PortalConfigError::EmptyResponse);

View File

@@ -26,6 +26,7 @@ const REQUIRED_PARAMS: [&str; 8] = [
pub struct SamlPrelogin { pub struct SamlPrelogin {
region: String, region: String,
saml_request: String, saml_request: String,
support_default_browser: bool,
} }
impl SamlPrelogin { impl SamlPrelogin {
@@ -36,6 +37,10 @@ impl SamlPrelogin {
pub fn saml_request(&self) -> &str { pub fn saml_request(&self) -> &str {
&self.saml_request &self.saml_request
} }
pub fn support_default_browser(&self) -> bool {
self.support_default_browser
}
} }
#[derive(Debug, Serialize, Type, Clone)] #[derive(Debug, Serialize, Type, Clone)]
@@ -86,14 +91,14 @@ pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prel
info!("Portal prelogin, user_agent: {}", user_agent); info!("Portal prelogin, user_agent: {}", user_agent);
let portal = normalize_server(portal)?; let portal = normalize_server(portal)?;
let prelogin_url = format!( let prelogin_url = format!("{}/global-protect/prelogin.esp", portal);
"{}/global-protect/prelogin.esp?kerberos-support=yes",
portal
);
let mut params = gp_params.to_params(); let mut params = gp_params.to_params();
params.insert("tmp", "tmp"); params.insert("tmp", "tmp");
params.insert("default-browser", "0");
params.insert("cas-support", "yes"); params.insert("cas-support", "yes");
if gp_params.prefer_default_browser() {
params.insert("default-browser", "1");
}
params.retain(|k, _| { params.retain(|k, _| {
REQUIRED_PARAMS REQUIRED_PARAMS
@@ -125,12 +130,18 @@ pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prel
let saml_method = xml::get_child_text(&doc, "saml-auth-method"); let saml_method = xml::get_child_text(&doc, "saml-auth-method");
let saml_request = xml::get_child_text(&doc, "saml-request"); let saml_request = xml::get_child_text(&doc, "saml-request");
let saml_default_browser = xml::get_child_text(&doc, "saml-default-browser");
// Check if the prelogin response is SAML // Check if the prelogin response is SAML
if saml_method.is_some() && saml_request.is_some() { if saml_method.is_some() && saml_request.is_some() {
let saml_request = base64::decode_to_string(&saml_request.unwrap())?; let saml_request = base64::decode_to_string(&saml_request.unwrap())?;
let support_default_browser = saml_default_browser
.map(|s| s.to_lowercase() == "yes")
.unwrap_or(false);
let saml_prelogin = SamlPrelogin { let saml_prelogin = SamlPrelogin {
region, region,
saml_request, saml_request,
support_default_browser,
}; };
return Ok(Prelogin::Saml(saml_prelogin)); return Ok(Prelogin::Saml(saml_prelogin));

View File

@@ -0,0 +1,34 @@
use std::{env::temp_dir, io::Write};
pub struct BrowserAuthenticator<'a> {
auth_request: &'a str,
}
impl BrowserAuthenticator<'_> {
pub fn new(auth_request: &str) -> BrowserAuthenticator {
BrowserAuthenticator { auth_request }
}
pub fn authenticate(&self) -> anyhow::Result<()> {
if self.auth_request.starts_with("http") {
open::that_detached(self.auth_request)?;
} else {
let html_file = temp_dir().join("gpauth.html");
let mut file = std::fs::File::create(&html_file)?;
file.write_all(self.auth_request.as_bytes())?;
open::that_detached(html_file)?;
}
Ok(())
}
}
impl Drop for BrowserAuthenticator<'_> {
fn drop(&mut self) {
// Cleanup the temporary file
let html_file = temp_dir().join("gpauth.html");
let _ = std::fs::remove_file(html_file);
}
}

View File

@@ -1,5 +1,7 @@
pub(crate) mod command_traits; pub(crate) mod command_traits;
pub mod auth_launcher; pub mod auth_launcher;
#[cfg(feature = "browser-auth")]
pub mod browser_authenticator;
pub mod gui_launcher; pub mod gui_launcher;
pub mod service_launcher; pub mod service_launcher;

View File

@@ -7,4 +7,6 @@ use super::vpn_state::VpnState;
pub enum WsEvent { pub enum WsEvent {
VpnState(VpnState), VpnState(VpnState),
ActiveGui, ActiveGui,
/// External authentication data
AuthData(String),
} }