Compare commits

...

5 Commits

Author SHA1 Message Date
Kevin Yue
3b384a199a Update changelog 2024-04-29 21:56:50 -04:00
Kevin Yue
b62b024a8b Release 2.2.0 2024-04-29 21:05:36 -04:00
Kevin Yue
4fbd373e29 chore: update logging 2024-04-17 21:25:25 +08:00
Kevin Yue
ae211a923a refactor: refine the logging 2024-04-15 22:31:50 +08:00
Kevin Yue
d94d730a44 feat: support default browser for CLI (#345) 2024-04-15 20:27:33 +08:00
16 changed files with 141 additions and 29 deletions

16
Cargo.lock generated
View File

@@ -564,7 +564,7 @@ dependencies = [
[[package]] [[package]]
name = "common" name = "common"
version = "2.1.4" version = "2.2.0"
dependencies = [ dependencies = [
"is_executable", "is_executable",
] ]
@@ -1430,7 +1430,7 @@ dependencies = [
[[package]] [[package]]
name = "gpapi" name = "gpapi"
version = "2.1.4" version = "2.2.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64 0.21.5", "base64 0.21.5",
@@ -1439,7 +1439,6 @@ dependencies = [
"dotenvy_macro", "dotenvy_macro",
"log", "log",
"md5", "md5",
"open",
"redact-engine", "redact-engine",
"regex", "regex",
"reqwest", "reqwest",
@@ -1462,7 +1461,7 @@ dependencies = [
[[package]] [[package]]
name = "gpauth" name = "gpauth"
version = "2.1.4" version = "2.2.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@@ -1471,6 +1470,7 @@ dependencies = [
"gpapi", "gpapi",
"html-escape", "html-escape",
"log", "log",
"open",
"regex", "regex",
"serde_json", "serde_json",
"tauri", "tauri",
@@ -1483,7 +1483,7 @@ dependencies = [
[[package]] [[package]]
name = "gpclient" name = "gpclient"
version = "2.1.4" version = "2.2.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@@ -1505,7 +1505,7 @@ dependencies = [
[[package]] [[package]]
name = "gpgui-helper" name = "gpgui-helper"
version = "2.1.4" version = "2.2.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@@ -1523,7 +1523,7 @@ dependencies = [
[[package]] [[package]]
name = "gpservice" name = "gpservice"
version = "2.1.4" version = "2.2.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum", "axum",
@@ -2537,7 +2537,7 @@ dependencies = [
[[package]] [[package]]
name = "openconnect" name = "openconnect"
version = "2.1.4" version = "2.2.0"
dependencies = [ dependencies = [
"cc", "cc",
"common", "common",

View File

@@ -5,7 +5,7 @@ members = ["crates/*", "apps/gpclient", "apps/gpservice", "apps/gpauth", "apps/g
[workspace.package] [workspace.package]
rust-version = "1.70" rust-version = "1.70"
version = "2.1.4" version = "2.2.0"
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"
@@ -44,6 +44,7 @@ compile-time = "0.2"
serde_urlencoded = "0.7" serde_urlencoded = "0.7"
md5="0.7" md5="0.7"
sha256="1" sha256="1"
open = "5"
# Tauri dependencies # Tauri dependencies
tauri = { version = "1.5" } tauri = { version = "1.5" }

View File

@@ -55,7 +55,7 @@ The GUI version is also available after you installed it. You can launch it from
### Debian/Ubuntu based distributions ### Debian/Ubuntu based distributions
#### Install from PPA #### Install from PPA (Ubuntu 18.04 and later, except 24.04)
``` ```
sudo apt-get install gir1.2-gtk-3.0 gir1.2-webkit2-4.0 sudo apt-get install gir1.2-gtk-3.0 gir1.2-webkit2-4.0
@@ -68,6 +68,23 @@ sudo apt-get install globalprotect-openconnect
> >
> For Linux Mint, you might need to import the GPG key with: `sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 7937C393082992E5D6E4A60453FC26B43838D761` if you encountered an error `gpg: keyserver receive failed: General error`. > For Linux Mint, you might need to import the GPG key with: `sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 7937C393082992E5D6E4A60453FC26B43838D761` if you encountered an error `gpg: keyserver receive failed: General error`.
#### **Ubuntu 24.04**
The `libwebkit2gtk-4.0-37` was [removed](https://bugs.launchpad.net/ubuntu/+source/webkit2gtk/+bug/2061914) from its repo, before [the issue](https://github.com/yuezk/GlobalProtect-openconnect/issues/351) gets resolved, you need to install them manually:
```bash
wget http://launchpadlibrarian.net/704701349/libwebkit2gtk-4.0-37_2.43.3-1_amd64.deb
wget http://launchpadlibrarian.net/704701345/libjavascriptcoregtk-4.0-18_2.43.3-1_amd64.deb
sudo dpkg --install *.deb
```
And the latest package is not available in the PPA, you can follow the [Install from deb package](#install-from-deb-package) section to install the latest package.
#### **Ubuntu 18.04**
The latest package is not available in the PPA either, but you still needs to add the `ppa:yuezk/globalprotect-openconnect` repo beforehand to use the required `openconnect` package. Then you can follow the [Install from deb package](#install-from-deb-package) section to install the latest package.
#### Install from deb package #### Install from deb package
Download the latest deb package from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. Then install it with `dpkg`: Download the latest deb package from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. Then install it with `dpkg`:
@@ -151,6 +168,8 @@ You can also build the client from source, steps are as follows:
1. How to deal with error `Secure Storage not ready` 1. How to deal with error `Secure Storage not ready`
Try upgrade the client to `2.2.0` or later, which will use a file-based storage as a fallback.
You need to install the `gnome-keyring` package, and restart the system (See [#321](https://github.com/yuezk/GlobalProtect-openconnect/issues/321), [#316](https://github.com/yuezk/GlobalProtect-openconnect/issues/316)). You need to install the `gnome-keyring` package, and restart the system (See [#321](https://github.com/yuezk/GlobalProtect-openconnect/issues/321), [#316](https://github.com/yuezk/GlobalProtect-openconnect/issues/316)).
2. How to deal with error `(gpauth:18869): Gtk-WARNING **: 10:33:37.566: cannot open display:` 2. How to deal with error `(gpauth:18869): Gtk-WARNING **: 10:33:37.566: cannot open display:`

View File

@@ -22,3 +22,4 @@ html-escape = "0.2.13"
webkit2gtk = "0.18.2" webkit2gtk = "0.18.2"
tauri = { workspace = true, features = ["http-all"] } tauri = { workspace = true, features = ["http-all"] }
compile-time.workspace = true compile-time.workspace = true
open.workspace = true

View File

@@ -11,7 +11,10 @@ use serde_json::json;
use tauri::{App, AppHandle, RunEvent}; use tauri::{App, AppHandle, RunEvent};
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
use crate::auth_window::{portal_prelogin, AuthWindow}; use crate::{
auth_window::{portal_prelogin, AuthWindow},
browser_authenticator::BrowserAuthenticator,
};
const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), " (", compile_time::date_str!(), ")"); const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), " (", compile_time::date_str!(), ")");
@@ -37,6 +40,8 @@ struct Cli {
ignore_tls_errors: bool, ignore_tls_errors: bool,
#[arg(long)] #[arg(long)]
clean: bool, clean: bool,
#[arg(long)]
default_browser: bool,
} }
impl Cli { impl Cli {
@@ -56,6 +61,15 @@ impl Cli {
None => portal_prelogin(&self.server, &gp_params).await?, None => portal_prelogin(&self.server, &gp_params).await?,
}; };
if self.default_browser {
let browser_auth = BrowserAuthenticator::new(&saml_request);
browser_auth.authenticate()?;
info!("Please continue the authentication process in the default browser");
return Ok(());
}
self.saml_request.replace(saml_request); self.saml_request.replace(saml_request);
let app = create_app(self.clone())?; let app = create_app(self.clone())?;

View File

@@ -1,6 +1,7 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod auth_window; mod auth_window;
mod browser_authenticator;
mod cli; mod cli;
#[tokio::main] #[tokio::main]

View File

@@ -19,8 +19,9 @@ use gpapi::{
use inquire::{Password, PasswordDisplayMode, Select, Text}; use inquire::{Password, PasswordDisplayMode, Select, Text};
use log::info; use log::info;
use openconnect::Vpn; use openconnect::Vpn;
use tokio::{io::AsyncReadExt, net::TcpListener};
use crate::{cli::SharedArgs, GP_CLIENT_LOCK_FILE}; use crate::{cli::SharedArgs, GP_CLIENT_LOCK_FILE, GP_CLIENT_PORT_FILE};
#[derive(Args)] #[derive(Args)]
pub(crate) struct ConnectArgs { pub(crate) struct ConnectArgs {
@@ -60,6 +61,8 @@ pub(crate) struct ConnectArgs {
hidpi: bool, hidpi: bool,
#[arg(long, help = "Do not reuse the remembered authentication cookie")] #[arg(long, help = "Do not reuse the remembered authentication cookie")]
clean: bool, clean: bool,
#[arg(long, help = "Use the default browser to authenticate")]
default_browser: bool,
} }
impl ConnectArgs { impl ConnectArgs {
@@ -240,7 +243,9 @@ impl<'a> ConnectHandler<'a> {
match prelogin { match prelogin {
Prelogin::Saml(prelogin) => { Prelogin::Saml(prelogin) => {
SamlAuthLauncher::new(&self.args.server) let use_default_browser = prelogin.support_default_browser() && self.args.default_browser;
let cred = SamlAuthLauncher::new(&self.args.server)
.gateway(is_gateway) .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)
@@ -250,8 +255,21 @@ impl<'a> ConnectHandler<'a> {
.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)
.clean(self.args.clean) .clean(self.args.clean)
.default_browser(use_default_browser)
.launch() .launch()
.await .await?;
if let Some(cred) = cred {
return Ok(cred);
}
if !use_default_browser {
// This should never happen
unreachable!("SAML authentication failed without using the default browser");
}
info!("Waiting for the browser authentication to complete...");
wait_credentials().await
} }
Prelogin::Standard(prelogin) => { Prelogin::Standard(prelogin) => {
let prefix = if is_gateway { "Gateway" } else { "Portal" }; let prefix = if is_gateway { "Gateway" } else { "Portal" };
@@ -274,6 +292,27 @@ impl<'a> ConnectHandler<'a> {
} }
} }
async fn wait_credentials() -> anyhow::Result<Credential> {
// Start a local server to receive the browser authentication data
let listener = TcpListener::bind("127.0.0.1:0").await?;
let port = listener.local_addr()?.port();
// Write the port to a file
fs::write(GP_CLIENT_PORT_FILE, port.to_string())?;
info!("Listening authentication data on port {}", port);
let (mut socket, _) = listener.accept().await?;
info!("Received the browser authentication data from the socket");
let mut data = String::new();
socket.read_to_string(&mut data).await?;
// Remove the port file
fs::remove_file(GP_CLIENT_PORT_FILE)?;
Credential::from_gpcallback(&data)
}
fn write_pid_file() { fn write_pid_file() {
let pid = std::process::id(); let pid = std::process::id();

View File

@@ -7,6 +7,9 @@ use gpapi::{
utils::{endpoint::http_endpoint, env_file, shutdown_signal}, utils::{endpoint::http_endpoint, env_file, shutdown_signal},
}; };
use log::info; use log::info;
use tokio::io::AsyncWriteExt;
use crate::GP_CLIENT_PORT_FILE;
#[derive(Args)] #[derive(Args)]
pub(crate) struct LaunchGuiArgs { pub(crate) struct LaunchGuiArgs {
@@ -78,6 +81,11 @@ impl<'a> LaunchGuiHandler<'a> {
} }
async fn feed_auth_data(auth_data: &str) -> anyhow::Result<()> { async fn feed_auth_data(auth_data: &str) -> anyhow::Result<()> {
let _ = tokio::join!(feed_auth_data_gui(auth_data), feed_auth_data_cli(auth_data));
Ok(())
}
async fn feed_auth_data_gui(auth_data: &str) -> anyhow::Result<()> {
let service_endpoint = http_endpoint().await?; let service_endpoint = http_endpoint().await?;
reqwest::Client::default() reqwest::Client::default()
@@ -90,6 +98,15 @@ async fn feed_auth_data(auth_data: &str) -> anyhow::Result<()> {
Ok(()) Ok(())
} }
async fn feed_auth_data_cli(auth_data: &str) -> anyhow::Result<()> {
let port = tokio::fs::read_to_string(GP_CLIENT_PORT_FILE).await?;
let mut stream = tokio::net::TcpStream::connect(format!("127.0.0.1:{}", port.trim())).await?;
stream.write_all(auth_data.as_bytes()).await?;
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

@@ -4,6 +4,7 @@ mod disconnect;
mod launch_gui; mod launch_gui;
pub(crate) const GP_CLIENT_LOCK_FILE: &str = "/var/run/gpclient.lock"; pub(crate) const GP_CLIENT_LOCK_FILE: &str = "/var/run/gpclient.lock";
pub(crate) const GP_CLIENT_PORT_FILE: &str = "/var/run/gpclient.port";
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {

View File

@@ -1,5 +1,10 @@
# Changelog # Changelog
## 2.2.0 - 2024-04-30
- CLI: support authentication with external browser (fix [#298](https://github.com/yuezk/GlobalProtect-openconnect/issues/298))
- GUI: support using file-based storage when the system keyring is not available.
## 2.1.4 - 2024-04-10 ## 2.1.4 - 2024-04-10
- Support MFA authentication (fix [#343](https://github.com/yuezk/GlobalProtect-openconnect/issues/343)) - Support MFA authentication (fix [#343](https://github.com/yuezk/GlobalProtect-openconnect/issues/343))

View File

@@ -31,9 +31,7 @@ sha256.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

@@ -2,7 +2,7 @@ use thiserror::Error;
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum PortalError { pub enum PortalError {
#[error("Portal prelogin error: {0}")] #[error("Prelogin error: {0}")]
PreloginError(String), PreloginError(String),
#[error("Portal config error: {0}")] #[error("Portal config error: {0}")]
ConfigError(String), ConfigError(String),

View File

@@ -98,10 +98,12 @@ impl Prelogin {
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!("Prelogin with user_agent: {}", user_agent); let is_gateway = gp_params.is_gateway();
let prelogin_type = if is_gateway { "Gateway" } else { "Portal" };
info!("{} prelogin with user_agent: {}", prelogin_type, user_agent);
let portal = normalize_server(portal)?; let portal = normalize_server(portal)?;
let is_gateway = gp_params.is_gateway();
let path = if is_gateway { "ssl-vpn" } else { "global-protect" }; let path = if is_gateway { "ssl-vpn" } else { "global-protect" };
let prelogin_url = format!("{portal}/{}/prelogin.esp", path); let prelogin_url = format!("{portal}/{}/prelogin.esp", path);
let mut params = gp_params.to_params(); let mut params = gp_params.to_params();
@@ -112,8 +114,6 @@ pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prel
params.retain(|k, _| REQUIRED_PARAMS.iter().any(|required_param| required_param == k)); params.retain(|k, _| REQUIRED_PARAMS.iter().any(|required_param| required_param == k));
info!("Prelogin with params: {:?}", params);
let client = Client::builder() let client = Client::builder()
.danger_accept_invalid_certs(gp_params.ignore_tls_errors()) .danger_accept_invalid_certs(gp_params.ignore_tls_errors())
.user_agent(user_agent) .user_agent(user_agent)
@@ -139,20 +139,23 @@ pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prel
Err(anyhow!(PortalError::PreloginError(err.reason))) Err(anyhow!(PortalError::PreloginError(err.reason)))
})?; })?;
let prelogin = parse_res_xml(res_xml, is_gateway).map_err(|e| PortalError::PreloginError(e.to_string()))?; let prelogin = parse_res_xml(&res_xml, is_gateway).map_err(|err| {
warn!("Parse response error, response: {}", res_xml);
PortalError::PreloginError(err.to_string())
})?;
Ok(prelogin) Ok(prelogin)
} }
fn parse_res_xml(res_xml: String, is_gateway: bool) -> anyhow::Result<Prelogin> { fn parse_res_xml(res_xml: &str, 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")
.ok_or_else(|| anyhow::anyhow!("Prelogin response does not contain status element"))?; .ok_or_else(|| anyhow::anyhow!("Prelogin response does not contain status element"))?;
// Check the status of the prelogin response // Check the status of the prelogin response
if status.to_uppercase() != "SUCCESS" { if status.to_uppercase() != "SUCCESS" {
let msg = xml::get_child_text(&doc, "msg").unwrap_or(String::from("Unknown error")); let msg = xml::get_child_text(&doc, "msg").unwrap_or(String::from("Unknown error"));
bail!("Prelogin failed: {}", msg) bail!("{}", msg)
} }
let region = xml::get_child_text(&doc, "region").unwrap_or_else(|| { let region = xml::get_child_text(&doc, "region").unwrap_or_else(|| {

View File

@@ -18,6 +18,7 @@ pub struct SamlAuthLauncher<'a> {
fix_openssl: bool, fix_openssl: bool,
ignore_tls_errors: bool, ignore_tls_errors: bool,
clean: bool, clean: bool,
default_browser: bool,
} }
impl<'a> SamlAuthLauncher<'a> { impl<'a> SamlAuthLauncher<'a> {
@@ -33,6 +34,7 @@ impl<'a> SamlAuthLauncher<'a> {
fix_openssl: false, fix_openssl: false,
ignore_tls_errors: false, ignore_tls_errors: false,
clean: false, clean: false,
default_browser: false,
} }
} }
@@ -81,8 +83,13 @@ impl<'a> SamlAuthLauncher<'a> {
self self
} }
pub fn default_browser(mut self, default_browser: bool) -> Self {
self.default_browser = default_browser;
self
}
/// Launch the authenticator binary as the current user or SUDO_USER if available. /// Launch the authenticator binary as the current user or SUDO_USER if available.
pub async fn launch(self) -> anyhow::Result<Credential> { pub async fn launch(self) -> anyhow::Result<Option<Credential>> {
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);
@@ -122,6 +129,10 @@ impl<'a> SamlAuthLauncher<'a> {
auth_cmd.arg("--clean"); auth_cmd.arg("--clean");
} }
if self.default_browser {
auth_cmd.arg("--default-browser");
}
let mut non_root_cmd = auth_cmd.into_non_root()?; let mut non_root_cmd = auth_cmd.into_non_root()?;
let output = non_root_cmd let output = non_root_cmd
.kill_on_drop(true) .kill_on_drop(true)
@@ -130,12 +141,16 @@ impl<'a> SamlAuthLauncher<'a> {
.wait_with_output() .wait_with_output()
.await?; .await?;
if self.default_browser {
return Ok(None);
}
let Ok(auth_result) = serde_json::from_slice::<SamlAuthResult>(&output.stdout) else { let Ok(auth_result) = serde_json::from_slice::<SamlAuthResult>(&output.stdout) else {
bail!("Failed to parse auth data") bail!("Failed to parse auth data")
}; };
match auth_result { match auth_result {
SamlAuthResult::Success(auth_data) => Ok(Credential::from(auth_data)), SamlAuthResult::Success(auth_data) => Ok(Some(Credential::from(auth_data))),
SamlAuthResult::Failure(msg) => bail!(msg), SamlAuthResult::Failure(msg) => bail!(msg),
} }
} }

View File

@@ -2,8 +2,6 @@ pub(crate) mod command_traits;
pub(crate) mod gui_helper_launcher; pub(crate) mod gui_helper_launcher;
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 hip_launcher; pub mod hip_launcher;
pub mod service_launcher; pub mod service_launcher;