mirror of
https://github.com/yuezk/GlobalProtect-openconnect.git
synced 2025-04-02 18:31:50 -04:00
Compare commits
9 Commits
0c411a542f
...
597e633587
Author | SHA1 | Date | |
---|---|---|---|
|
597e633587 | ||
|
0c1d239736 | ||
|
f474ab36c0 | ||
|
ed7c8ca1a1 | ||
|
f71e29de5c | ||
|
3316fdf722 | ||
|
a641453388 | ||
|
366b95ce1f | ||
|
136c870d1f |
8
.github/workflows/build.yaml
vendored
8
.github/workflows/build.yaml
vendored
@ -89,13 +89,13 @@ jobs:
|
||||
run: |
|
||||
docker run --rm \
|
||||
-v $(pwd)/build-gp-${{ matrix.package }}:/${{ matrix.package }} \
|
||||
yuezk/gpdev:${{ matrix.package }}-builder
|
||||
yuezk/gpdev:${{ matrix.package }}-builder-tauri2
|
||||
- name: Install ${{ matrix.package }} package in Docker
|
||||
run: |
|
||||
docker run --rm \
|
||||
-e GPGUI_INSTALLED=0 \
|
||||
-v $(pwd)/build-gp-${{ matrix.package }}:/${{ matrix.package }} \
|
||||
yuezk/gpdev:${{ matrix.package }}-builder \
|
||||
yuezk/gpdev:${{ matrix.package }}-builder-tauri2 \
|
||||
bash install.sh
|
||||
- name: Upload ${{ matrix.package }} package
|
||||
uses: actions/upload-artifact@v3
|
||||
@ -141,12 +141,12 @@ jobs:
|
||||
run: echo ${{ secrets.DOCKER_HUB_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin
|
||||
- name: Build gpgui in Docker
|
||||
run: |
|
||||
docker run --rm -v $(pwd)/gpgui-source:/gpgui yuezk/gpdev:gpgui-builder
|
||||
docker run --rm -v $(pwd)/gpgui-source:/gpgui yuezk/gpdev:gpgui-builder-tauri2
|
||||
- name: Install gpgui in Docker
|
||||
run: |
|
||||
cd gpgui-source
|
||||
tar -xJf *.bin.tar.xz
|
||||
docker run --rm -v $(pwd):/gpgui yuezk/gpdev:gpgui-builder \
|
||||
docker run --rm -v $(pwd):/gpgui yuezk/gpdev:gpgui-builder-tauri2 \
|
||||
bash -c "cd /gpgui/gpgui_*/ && ./gpgui --version"
|
||||
- name: Upload gpgui
|
||||
uses: actions/upload-artifact@v3
|
||||
|
6
.github/workflows/publish.yaml
vendored
6
.github/workflows/publish.yaml
vendored
@ -76,7 +76,7 @@ jobs:
|
||||
cp packaging/deb/postrm .build/debian/postrm
|
||||
|
||||
- name: Publish to PPA
|
||||
uses: yuezk/publish-ppa-package@dev
|
||||
uses: yuezk/publish-ppa-package@gp
|
||||
with:
|
||||
repository: "yuezk/globalprotect-openconnect"
|
||||
gpg_private_key: ${{ secrets.PPA_GPG_PRIVATE_KEY }}
|
||||
@ -85,5 +85,7 @@ jobs:
|
||||
debian_dir: publish-ppa/globalprotect-openconnect-*/.build/debian
|
||||
deb_email: "k3vinyue@gmail.com"
|
||||
deb_fullname: "Kevin Yue"
|
||||
extra_ppa: "liushuyu-011/rust-bpo-1.75"
|
||||
extra_ppa: "yuezk/globalprotect-openconnect liushuyu-011/rust-updates-1.80"
|
||||
# Ubuntu 18.04 and 20.04 are excluded because tauri2 no longer supports them
|
||||
excluded_series: "bionic focal"
|
||||
revision: ${{ inputs.revision }}
|
||||
|
6
.github/workflows/release.yaml
vendored
6
.github/workflows/release.yaml
vendored
@ -1,4 +1,4 @@
|
||||
name: Release Packages
|
||||
name: GH Release Packages
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@ -112,13 +112,13 @@ jobs:
|
||||
docker run --rm \
|
||||
-v $(pwd)/build-${{ matrix.package }}:/${{ matrix.package }} \
|
||||
-e INCLUDE_GUI=1 \
|
||||
yuezk/gpdev:${{ matrix.package }}-builder
|
||||
yuezk/gpdev:${{ matrix.package }}-builder-tauri2
|
||||
|
||||
- name: Install ${{ matrix.package }} package in Docker
|
||||
run: |
|
||||
docker run --rm \
|
||||
-v $(pwd)/build-${{ matrix.package }}:/${{ matrix.package }} \
|
||||
yuezk/gpdev:${{ matrix.package }}-builder \
|
||||
yuezk/gpdev:${{ matrix.package }}-builder-tauri2 \
|
||||
bash install.sh
|
||||
|
||||
- name: Upload ${{ matrix.package }} package
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -8,3 +8,6 @@
|
||||
.cargo
|
||||
.build
|
||||
SNAPSHOT
|
||||
|
||||
# Tauri generated files
|
||||
gen
|
||||
|
3355
Cargo.lock
generated
3355
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
28
Cargo.toml
28
Cargo.toml
@ -4,8 +4,8 @@ resolver = "2"
|
||||
members = ["crates/*", "apps/gpclient", "apps/gpservice", "apps/gpauth", "apps/gpgui-helper/src-tauri"]
|
||||
|
||||
[workspace.package]
|
||||
rust-version = "1.70"
|
||||
version = "2.3.8"
|
||||
rust-version = "1.80"
|
||||
version = "2.3.9"
|
||||
authors = ["Kevin Yue <k3vinyue@gmail.com>"]
|
||||
homepage = "https://github.com/yuezk/GlobalProtect-openconnect"
|
||||
edition = "2021"
|
||||
@ -13,22 +13,22 @@ license = "GPL-3.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0"
|
||||
base64 = "0.21"
|
||||
clap = { version = "4.4.2", features = ["derive"] }
|
||||
base64 = "0.22"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
ctrlc = "3.4"
|
||||
directories = "5.0"
|
||||
dns-lookup = "2.0.4"
|
||||
env_logger = "0.10"
|
||||
env_logger = "0.11"
|
||||
is_executable = "1.0"
|
||||
log = "0.4"
|
||||
regex = "1"
|
||||
reqwest = { version = "0.11", features = ["native-tls-vendored", "json"] }
|
||||
reqwest = { version = "0.12", features = ["native-tls-vendored", "json"] }
|
||||
openssl = "0.10"
|
||||
pem = "3"
|
||||
roxmltree = "0.18"
|
||||
roxmltree = "0.20"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
sysinfo = "0.29"
|
||||
sysinfo = "0.33"
|
||||
tempfile = "3.8"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-util = "0.7"
|
||||
@ -38,21 +38,19 @@ axum = "0.7"
|
||||
futures = "0.3"
|
||||
futures-util = "0.3"
|
||||
tokio-tungstenite = "0.20.1"
|
||||
uzers = "0.11"
|
||||
uzers = "0.12"
|
||||
whoami = "1"
|
||||
thiserror = "1"
|
||||
thiserror = "2"
|
||||
redact-engine = "0.1"
|
||||
compile-time = "0.2"
|
||||
serde_urlencoded = "0.7"
|
||||
md5="0.7"
|
||||
sha256="1"
|
||||
which="6"
|
||||
which="7"
|
||||
|
||||
# Tauri dependencies
|
||||
tauri = { version = "1.5" }
|
||||
specta = "=2.0.0-rc.1"
|
||||
specta-macros = "=2.0.0-rc.1"
|
||||
rspc = { version = "1.0.0-rc.5", features = ["tauri"] }
|
||||
tauri = { version = "2" }
|
||||
specta = "=2.0.0-rc.20"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 'z' # Optimize for size
|
||||
|
40
README.md
40
README.md
@ -85,17 +85,13 @@ sudo apt-get install globalprotect-openconnect
|
||||
|
||||
#### **Ubuntu 24.04 and later**
|
||||
|
||||
The `libwebkit2gtk-4.0-37` package 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:
|
||||
The `libwebkit2gtk-4.0-37` package was [removed](https://bugs.launchpad.net/ubuntu/+source/webkit2gtk/+bug/2061914) from its repo. You can use the [`deb-install.sh`](./scripts/deb-install.sh) script to install the package:
|
||||
|
||||
```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
|
||||
curl -o- https://raw.githubusercontent.com/yuezk/GlobalProtect-openconnect/main/scripts/deb-install.sh \
|
||||
| bash -s -- 2.3.9
|
||||
```
|
||||
|
||||
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.
|
||||
@ -114,7 +110,7 @@ sudo apt install --fix-broken globalprotect-openconnect_*.deb
|
||||
|
||||
Install from AUR: [globalprotect-openconnect-git](https://aur.archlinux.org/packages/globalprotect-openconnect-git/)
|
||||
|
||||
```
|
||||
```bash
|
||||
yay -S globalprotect-openconnect-git
|
||||
```
|
||||
|
||||
@ -132,7 +128,7 @@ sudo pacman -U globalprotect-openconnect-*.pkg.tar.zst
|
||||
|
||||
The package is available on [COPR](https://copr.fedorainfracloud.org/coprs/yuezk/globalprotect-openconnect/) for various RPM-based distributions. You can install it with the following commands:
|
||||
|
||||
```
|
||||
```bash
|
||||
sudo dnf copr enable yuezk/globalprotect-openconnect
|
||||
sudo dnf install globalprotect-openconnect
|
||||
```
|
||||
@ -152,30 +148,16 @@ Download the latest RPM package from [releases](https://github.com/yuezk/GlobalP
|
||||
```bash
|
||||
sudo rpm -i globalprotect-openconnect-*.rpm
|
||||
```
|
||||
|
||||
### Gentoo
|
||||
|
||||
Install from the ```rios``` or ```slonko``` overlays. Example using rios:
|
||||
It is available via `guru` and `lamdness` overlays.
|
||||
|
||||
#### 1. Enable the overlay
|
||||
```bash
|
||||
sudo eselect repository enable guru
|
||||
sudo emerge -r guru sync
|
||||
sudo emerge -av net-vpn/globalprotect-openconnect
|
||||
```
|
||||
sudo eselect repository enable rios
|
||||
```
|
||||
|
||||
#### 2. Sync with the repository
|
||||
|
||||
- If you have eix installed, use it:
|
||||
```
|
||||
sudo eix-sync
|
||||
```
|
||||
- Otherwise, use:
|
||||
```
|
||||
sudo emerge --sync
|
||||
```
|
||||
|
||||
#### 3. Install
|
||||
|
||||
```sudo emerge globalprotect-openconnect```
|
||||
|
||||
|
||||
### Other distributions
|
||||
|
||||
|
@ -1,12 +1,13 @@
|
||||
[package]
|
||||
name = "gpauth"
|
||||
rust-version.workspace = true
|
||||
authors.workspace = true
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1.5", features = [] }
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
gpapi = { path = "../../crates/gpapi", features = [
|
||||
@ -14,6 +15,9 @@ gpapi = { path = "../../crates/gpapi", features = [
|
||||
"clap",
|
||||
"browser-auth",
|
||||
] }
|
||||
|
||||
tauri = { workspace = true }
|
||||
|
||||
anyhow.workspace = true
|
||||
clap.workspace = true
|
||||
env_logger.workspace = true
|
||||
@ -24,6 +28,7 @@ tokio.workspace = true
|
||||
tokio-util.workspace = true
|
||||
tempfile.workspace = true
|
||||
html-escape = "0.2.13"
|
||||
webkit2gtk = "0.18.2"
|
||||
tauri = { workspace = true, features = ["http-all"] }
|
||||
compile-time.workspace = true
|
||||
|
||||
[target.'cfg(not(target_os = "macos"))'.dependencies]
|
||||
webkit2gtk = "2"
|
||||
|
108
apps/gpauth/src/auth_messenger.rs
Normal file
108
apps/gpauth/src/auth_messenger.rs
Normal file
@ -0,0 +1,108 @@
|
||||
use anyhow::bail;
|
||||
use gpapi::auth::SamlAuthData;
|
||||
use log::{error, info};
|
||||
use tokio::sync::{mpsc, RwLock};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
pub enum AuthError {
|
||||
/// Failed to load page due to TLS error
|
||||
TlsError,
|
||||
/// 1. Found auth data in headers/body but it's invalid
|
||||
/// 2. Loaded an empty page, failed to load page. etc.
|
||||
Invalid,
|
||||
/// No auth data found in headers/body
|
||||
NotFound,
|
||||
}
|
||||
|
||||
pub type AuthResult = anyhow::Result<SamlAuthData, AuthError>;
|
||||
|
||||
pub enum AuthEvent {
|
||||
Data(SamlAuthData),
|
||||
Error(AuthError),
|
||||
RaiseWindow,
|
||||
Close,
|
||||
}
|
||||
|
||||
pub struct AuthMessenger {
|
||||
tx: mpsc::UnboundedSender<AuthEvent>,
|
||||
rx: RwLock<mpsc::UnboundedReceiver<AuthEvent>>,
|
||||
raise_window_cancel_token: RwLock<Option<CancellationToken>>,
|
||||
}
|
||||
|
||||
impl AuthMessenger {
|
||||
pub fn new() -> Self {
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
|
||||
Self {
|
||||
tx,
|
||||
rx: RwLock::new(rx),
|
||||
raise_window_cancel_token: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn subscribe(&self) -> anyhow::Result<AuthEvent> {
|
||||
let mut rx = self.rx.write().await;
|
||||
if let Some(event) = rx.recv().await {
|
||||
return Ok(event);
|
||||
}
|
||||
bail!("Failed to receive auth event");
|
||||
}
|
||||
|
||||
pub fn send_auth_event(&self, event: AuthEvent) {
|
||||
if let Err(event) = self.tx.send(event) {
|
||||
error!("Failed to send auth event: {}", event);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_auth_result(&self, result: AuthResult) {
|
||||
match result {
|
||||
Ok(data) => self.send_auth_data(data),
|
||||
Err(err) => self.send_auth_error(err),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_auth_error(&self, err: AuthError) {
|
||||
self.send_auth_event(AuthEvent::Error(err));
|
||||
}
|
||||
|
||||
pub fn send_auth_data(&self, data: SamlAuthData) {
|
||||
self.send_auth_event(AuthEvent::Data(data));
|
||||
}
|
||||
|
||||
pub fn schedule_raise_window(&self, delay: u64) {
|
||||
let cancel_token = CancellationToken::new();
|
||||
let cancel_token_clone = cancel_token.clone();
|
||||
|
||||
if let Ok(mut guard) = self.raise_window_cancel_token.try_write() {
|
||||
// Cancel the previous raise window task if it exists
|
||||
if let Some(token) = guard.take() {
|
||||
token.cancel();
|
||||
}
|
||||
*guard = Some(cancel_token_clone);
|
||||
}
|
||||
|
||||
let tx = self.tx.clone();
|
||||
tokio::spawn(async move {
|
||||
info!("Displaying the window in {} second(s)...", delay);
|
||||
|
||||
tokio::select! {
|
||||
_ = tokio::time::sleep(tokio::time::Duration::from_secs(delay)) => {
|
||||
if let Err(err) = tx.send(AuthEvent::RaiseWindow) {
|
||||
error!("Failed to send raise window event: {}", err);
|
||||
}
|
||||
}
|
||||
_ = cancel_token.cancelled() => {
|
||||
info!("Cancelled raise window task");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn cancel_raise_window(&self) {
|
||||
if let Ok(mut cancel_token) = self.raise_window_cancel_token.try_write() {
|
||||
if let Some(token) = cancel_token.take() {
|
||||
token.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,8 @@
|
||||
use std::{
|
||||
rc::Rc,
|
||||
borrow::Cow,
|
||||
env::temp_dir,
|
||||
fs,
|
||||
os::unix::fs::PermissionsExt,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
@ -7,517 +10,278 @@ use std::{
|
||||
use anyhow::bail;
|
||||
use gpapi::{
|
||||
auth::SamlAuthData,
|
||||
error::AuthDataParseError,
|
||||
error::PortalError,
|
||||
gp_params::GpParams,
|
||||
portal::{prelogin, Prelogin},
|
||||
utils::{redact::redact_uri, window::WindowExt},
|
||||
process::browser_authenticator::BrowserAuthenticator,
|
||||
utils::window::WindowExt,
|
||||
GP_CALLBACK_PORT_FILENAME,
|
||||
};
|
||||
use log::{info, warn};
|
||||
use regex::Regex;
|
||||
use tauri::{AppHandle, Window, WindowEvent, WindowUrl};
|
||||
use tokio::sync::{mpsc, oneshot, RwLock};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use webkit2gtk::{
|
||||
gio::Cancellable,
|
||||
glib::{GString, TimeSpan},
|
||||
LoadEvent, SettingsExt, TLSErrorsPolicy, URIResponse, URIResponseExt, WebContextExt, WebResource, WebResourceExt,
|
||||
WebView, WebViewExt, WebsiteDataManagerExtManual, WebsiteDataTypes,
|
||||
use tauri::{AppHandle, WebviewUrl, WebviewWindow, WindowEvent};
|
||||
use tokio::{
|
||||
io::AsyncReadExt,
|
||||
net::TcpListener,
|
||||
sync::{oneshot, RwLock},
|
||||
time,
|
||||
};
|
||||
|
||||
enum AuthDataError {
|
||||
/// Failed to load page due to TLS error
|
||||
TlsError,
|
||||
/// 1. Found auth data in headers/body but it's invalid
|
||||
/// 2. Loaded an empty page, failed to load page. etc.
|
||||
Invalid,
|
||||
/// No auth data found in headers/body
|
||||
NotFound,
|
||||
}
|
||||
use crate::{
|
||||
auth_messenger::{AuthError, AuthEvent, AuthMessenger},
|
||||
common::{AuthRequest, AuthSettings},
|
||||
platform_impl,
|
||||
};
|
||||
|
||||
type AuthResult = Result<SamlAuthData, AuthDataError>;
|
||||
|
||||
pub(crate) struct AuthWindow<'a> {
|
||||
app_handle: AppHandle,
|
||||
pub struct AuthWindow<'a> {
|
||||
server: &'a str,
|
||||
saml_request: &'a str,
|
||||
user_agent: &'a str,
|
||||
gp_params: Option<GpParams>,
|
||||
gp_params: &'a GpParams,
|
||||
auth_request: Option<&'a str>,
|
||||
clean: bool,
|
||||
is_retrying: RwLock<bool>,
|
||||
}
|
||||
|
||||
impl<'a> AuthWindow<'a> {
|
||||
pub fn new(app_handle: AppHandle) -> Self {
|
||||
pub fn new(server: &'a str, gp_params: &'a GpParams) -> Self {
|
||||
Self {
|
||||
app_handle,
|
||||
server: "",
|
||||
saml_request: "",
|
||||
user_agent: "",
|
||||
gp_params: None,
|
||||
server,
|
||||
gp_params,
|
||||
auth_request: None,
|
||||
clean: false,
|
||||
is_retrying: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn server(mut self, server: &'a str) -> Self {
|
||||
self.server = server;
|
||||
pub fn with_auth_request(mut self, auth_request: &'a str) -> Self {
|
||||
if !auth_request.is_empty() {
|
||||
self.auth_request = Some(auth_request);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn saml_request(mut self, saml_request: &'a str) -> Self {
|
||||
self.saml_request = saml_request;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn user_agent(mut self, user_agent: &'a str) -> Self {
|
||||
self.user_agent = user_agent;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn gp_params(mut self, gp_params: GpParams) -> Self {
|
||||
self.gp_params.replace(gp_params);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn clean(mut self, clean: bool) -> Self {
|
||||
pub fn with_clean(mut self, clean: bool) -> Self {
|
||||
self.clean = clean;
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn open(&self) -> anyhow::Result<SamlAuthData> {
|
||||
info!("Open auth window, user_agent: {}", self.user_agent);
|
||||
pub async fn browser_authenticate(&self, browser: Option<&str>) -> anyhow::Result<SamlAuthData> {
|
||||
let auth_request = self.initial_auth_request().await?;
|
||||
let browser_auth = if let Some(browser) = browser {
|
||||
BrowserAuthenticator::new_with_browser(&auth_request, browser)
|
||||
} else {
|
||||
BrowserAuthenticator::new(&auth_request)
|
||||
};
|
||||
|
||||
let window = Window::builder(&self.app_handle, "auth_window", WindowUrl::default())
|
||||
browser_auth.authenticate()?;
|
||||
info!("Please continue the authentication process in the default browser");
|
||||
|
||||
wait_auth_data().await
|
||||
}
|
||||
|
||||
pub async fn webview_authenticate(&self, app_handle: &AppHandle) -> anyhow::Result<SamlAuthData> {
|
||||
let auth_window = WebviewWindow::builder(app_handle, "auth_window", WebviewUrl::default())
|
||||
.title("GlobalProtect Login")
|
||||
// .user_agent(self.user_agent)
|
||||
.focused(true)
|
||||
.visible(false)
|
||||
.center()
|
||||
.build()?;
|
||||
|
||||
let window = Arc::new(window);
|
||||
|
||||
let cancel_token = CancellationToken::new();
|
||||
let cancel_token_clone = cancel_token.clone();
|
||||
|
||||
window.on_window_event(move |event| {
|
||||
if let WindowEvent::CloseRequested { .. } = event {
|
||||
cancel_token_clone.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
let window_clone = Arc::clone(&window);
|
||||
let timeout_secs = 15;
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_secs(timeout_secs)).await;
|
||||
let visible = window_clone.is_visible().unwrap_or(false);
|
||||
if !visible {
|
||||
info!("Try to raise auth window after {} seconds", timeout_secs);
|
||||
raise_window(&window_clone);
|
||||
}
|
||||
});
|
||||
|
||||
tokio::select! {
|
||||
_ = cancel_token.cancelled() => {
|
||||
bail!("Auth cancelled");
|
||||
}
|
||||
saml_result = self.auth_loop(&window) => {
|
||||
window.close()?;
|
||||
saml_result
|
||||
}
|
||||
}
|
||||
self.auth_loop(&auth_window).await
|
||||
}
|
||||
|
||||
async fn auth_loop(&self, window: &Arc<Window>) -> anyhow::Result<SamlAuthData> {
|
||||
let saml_request = self.saml_request.to_string();
|
||||
let (auth_result_tx, mut auth_result_rx) = mpsc::unbounded_channel::<AuthResult>();
|
||||
let raise_window_cancel_token: Arc<RwLock<Option<CancellationToken>>> = Default::default();
|
||||
let gp_params = self.gp_params.as_ref().unwrap();
|
||||
let tls_err_policy = if gp_params.ignore_tls_errors() {
|
||||
TLSErrorsPolicy::Ignore
|
||||
} else {
|
||||
TLSErrorsPolicy::Fail
|
||||
};
|
||||
|
||||
async fn auth_loop(&self, auth_window: &WebviewWindow) -> anyhow::Result<SamlAuthData> {
|
||||
if self.clean {
|
||||
clear_webview_cookies(window).await?;
|
||||
self.clear_webview_data(&auth_window).await?;
|
||||
}
|
||||
|
||||
let raise_window_cancel_token_clone = Arc::clone(&raise_window_cancel_token);
|
||||
window.with_webview(move |wv| {
|
||||
let wv = wv.inner();
|
||||
|
||||
if let Some(context) = wv.context() {
|
||||
context.set_tls_errors_policy(tls_err_policy);
|
||||
}
|
||||
|
||||
if let Some(settings) = wv.settings() {
|
||||
let ua = settings.user_agent().unwrap_or("".into());
|
||||
info!("Auth window user agent: {}", ua);
|
||||
}
|
||||
|
||||
// Load the initial SAML request
|
||||
load_saml_request(&wv, &saml_request);
|
||||
|
||||
let auth_result_tx_clone = auth_result_tx.clone();
|
||||
wv.connect_load_changed(move |wv, event| {
|
||||
if event == LoadEvent::Started {
|
||||
let Ok(mut cancel_token) = raise_window_cancel_token_clone.try_write() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Cancel the raise window task
|
||||
if let Some(cancel_token) = cancel_token.take() {
|
||||
cancel_token.cancel();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if event != LoadEvent::Finished {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(main_resource) = wv.main_resource() {
|
||||
let uri = main_resource.uri().unwrap_or("".into());
|
||||
|
||||
if uri.is_empty() {
|
||||
warn!("Loaded an empty uri");
|
||||
send_auth_result(&auth_result_tx_clone, Err(AuthDataError::Invalid));
|
||||
return;
|
||||
}
|
||||
|
||||
info!("Loaded uri: {}", redact_uri(&uri));
|
||||
if uri.starts_with("globalprotectcallback:") {
|
||||
return;
|
||||
}
|
||||
|
||||
read_auth_data(&main_resource, auth_result_tx_clone.clone());
|
||||
}
|
||||
});
|
||||
|
||||
let auth_result_tx_clone = auth_result_tx.clone();
|
||||
wv.connect_load_failed_with_tls_errors(move |_wv, uri, cert, err| {
|
||||
let redacted_uri = redact_uri(uri);
|
||||
warn!(
|
||||
"Failed to load uri: {} with error: {}, cert: {}",
|
||||
redacted_uri, err, cert
|
||||
);
|
||||
|
||||
send_auth_result(&auth_result_tx_clone, Err(AuthDataError::TlsError));
|
||||
true
|
||||
});
|
||||
|
||||
wv.connect_load_failed(move |_wv, _event, uri, err| {
|
||||
let redacted_uri = redact_uri(uri);
|
||||
if !uri.starts_with("globalprotectcallback:") {
|
||||
warn!("Failed to load uri: {} with error: {}", redacted_uri, err);
|
||||
}
|
||||
// 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
|
||||
});
|
||||
})?;
|
||||
|
||||
let portal = self.server.to_string();
|
||||
let auth_messenger = self.setup_auth_window(&auth_window).await?;
|
||||
|
||||
loop {
|
||||
if let Some(auth_result) = auth_result_rx.recv().await {
|
||||
match auth_result {
|
||||
Ok(auth_data) => return Ok(auth_data),
|
||||
Err(AuthDataError::TlsError) => bail!("TLS error: certificate verify failed"),
|
||||
Err(AuthDataError::NotFound) => {
|
||||
info!("No auth data found, it may not be the /SAML20/SP/ACS endpoint");
|
||||
|
||||
// The user may need to interact with the auth window, raise it in 3 seconds
|
||||
if !window.is_visible().unwrap_or(false) {
|
||||
let window = Arc::clone(window);
|
||||
let cancel_token = CancellationToken::new();
|
||||
|
||||
raise_window_cancel_token.write().await.replace(cancel_token.clone());
|
||||
|
||||
tokio::spawn(async move {
|
||||
let delay_secs = 1;
|
||||
|
||||
info!("Raise window in {} second(s)", delay_secs);
|
||||
tokio::select! {
|
||||
_ = tokio::time::sleep(Duration::from_secs(delay_secs)) => {
|
||||
raise_window(&window);
|
||||
}
|
||||
_ = cancel_token.cancelled() => {
|
||||
info!("Raise window cancelled");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(AuthDataError::Invalid) => {
|
||||
info!("Got invalid auth data, retrying...");
|
||||
|
||||
window.with_webview(|wv| {
|
||||
let wv = wv.inner();
|
||||
wv.run_javascript(r#"
|
||||
var loading = document.createElement("div");
|
||||
loading.innerHTML = '<div style="position: absolute; width: 100%; text-align: center; font-size: 20px; font-weight: bold; top: 50%; left: 50%; transform: translate(-50%, -50%);">Got invalid token, retrying...</div>';
|
||||
loading.style = "position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255, 255, 255, 0.85); z-index: 99999;";
|
||||
document.body.appendChild(loading);
|
||||
"#,
|
||||
Cancellable::NONE,
|
||||
|_| info!("Injected loading element successfully"),
|
||||
);
|
||||
})?;
|
||||
|
||||
let saml_request = portal_prelogin(&portal, gp_params).await?;
|
||||
window.with_webview(move |wv| {
|
||||
let wv = wv.inner();
|
||||
load_saml_request(&wv, &saml_request);
|
||||
})?;
|
||||
}
|
||||
match auth_messenger.subscribe().await? {
|
||||
AuthEvent::Close => bail!("Authentication cancelled"),
|
||||
AuthEvent::RaiseWindow => self.raise_window(auth_window),
|
||||
AuthEvent::Error(AuthError::TlsError) => bail!(PortalError::TlsError),
|
||||
AuthEvent::Error(AuthError::NotFound) => self.handle_not_found(auth_window, &auth_messenger),
|
||||
AuthEvent::Error(AuthError::Invalid) => self.retry_auth(auth_window).await,
|
||||
AuthEvent::Data(auth_data) => {
|
||||
auth_window.close()?;
|
||||
return Ok(auth_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn raise_window(window: &Arc<Window>) {
|
||||
let visible = window.is_visible().unwrap_or(false);
|
||||
if !visible {
|
||||
if let Err(err) = window.raise() {
|
||||
async fn initial_auth_request(&self) -> anyhow::Result<Cow<'a, str>> {
|
||||
if let Some(auth_request) = self.auth_request {
|
||||
return Ok(Cow::Borrowed(auth_request));
|
||||
}
|
||||
|
||||
let auth_request = portal_prelogin(&self.server, &self.gp_params).await?;
|
||||
Ok(Cow::Owned(auth_request))
|
||||
}
|
||||
|
||||
async fn clear_webview_data(&self, auth_window: &WebviewWindow) -> anyhow::Result<()> {
|
||||
info!("Clearing webview data...");
|
||||
|
||||
let (tx, rx) = oneshot::channel::<anyhow::Result<()>>();
|
||||
let now = Instant::now();
|
||||
auth_window.with_webview(|webview| {
|
||||
platform_impl::clear_data(&webview.inner(), |result| {
|
||||
if let Err(result) = tx.send(result) {
|
||||
warn!("Failed to send clear data result: {:?}", result);
|
||||
}
|
||||
})
|
||||
})?;
|
||||
|
||||
rx.await??;
|
||||
info!("Webview data cleared in {:?}", now.elapsed());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn setup_auth_window(&self, auth_window: &WebviewWindow) -> anyhow::Result<Arc<AuthMessenger>> {
|
||||
info!("Setting up auth window...");
|
||||
|
||||
let auth_messenger = Arc::new(AuthMessenger::new());
|
||||
let auth_request = self.initial_auth_request().await?.into_owned();
|
||||
let ignore_tls_errors = self.gp_params.ignore_tls_errors();
|
||||
|
||||
// Handle window close event
|
||||
let auth_messenger_clone = Arc::clone(&auth_messenger);
|
||||
auth_window.on_window_event(move |event| {
|
||||
if let WindowEvent::CloseRequested { .. } = event {
|
||||
auth_messenger_clone.send_auth_event(AuthEvent::Close);
|
||||
}
|
||||
});
|
||||
|
||||
// Show the window after 10 seconds, so that the user can see the window if the auth process is stuck
|
||||
let auth_messenger_clone = Arc::clone(&auth_messenger);
|
||||
tokio::spawn(async move {
|
||||
time::sleep(Duration::from_secs(10)).await;
|
||||
auth_messenger_clone.send_auth_event(AuthEvent::RaiseWindow);
|
||||
});
|
||||
|
||||
// setup webview
|
||||
let auth_messenger_clone = Arc::clone(&auth_messenger);
|
||||
let (tx, rx) = oneshot::channel::<anyhow::Result<()>>();
|
||||
|
||||
auth_window.with_webview(move |webview| {
|
||||
let auth_settings = AuthSettings {
|
||||
auth_request: AuthRequest::new(&auth_request),
|
||||
auth_messenger: auth_messenger_clone,
|
||||
ignore_tls_errors,
|
||||
};
|
||||
|
||||
let result = platform_impl::setup_webview(&webview.inner(), auth_settings);
|
||||
if let Err(result) = tx.send(result) {
|
||||
warn!("Failed to send setup auth window result: {:?}", result);
|
||||
}
|
||||
})?;
|
||||
|
||||
rx.await??;
|
||||
info!("Auth window setup completed");
|
||||
|
||||
Ok(auth_messenger)
|
||||
}
|
||||
|
||||
fn handle_not_found(&self, auth_window: &WebviewWindow, auth_messenger: &Arc<AuthMessenger>) {
|
||||
info!("No auth data found, it may not be the /SAML20/SP/ACS endpoint");
|
||||
|
||||
let visible = auth_window.is_visible().unwrap_or(false);
|
||||
if visible {
|
||||
return;
|
||||
}
|
||||
|
||||
auth_messenger.schedule_raise_window(1);
|
||||
}
|
||||
|
||||
async fn retry_auth(&self, auth_window: &WebviewWindow) {
|
||||
let mut is_retrying = self.is_retrying.write().await;
|
||||
if *is_retrying {
|
||||
info!("Already retrying authentication, skipping...");
|
||||
return;
|
||||
}
|
||||
|
||||
*is_retrying = true;
|
||||
drop(is_retrying);
|
||||
|
||||
if let Err(err) = self.retry_auth_impl(auth_window).await {
|
||||
warn!("Failed to retry authentication: {}", err);
|
||||
}
|
||||
|
||||
*self.is_retrying.write().await = false;
|
||||
}
|
||||
|
||||
async fn retry_auth_impl(&self, auth_window: &WebviewWindow) -> anyhow::Result<()> {
|
||||
info!("Retrying authentication...");
|
||||
|
||||
auth_window.eval( r#"
|
||||
var loading = document.createElement("div");
|
||||
loading.innerHTML = '<div style="position: absolute; width: 100%; text-align: center; font-size: 20px; font-weight: bold; top: 50%; left: 50%; transform: translate(-50%, -50%);">Got invalid token, retrying...</div>';
|
||||
loading.style = "position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255, 255, 255, 0.85); z-index: 99999;";
|
||||
document.body.appendChild(loading);
|
||||
"#)?;
|
||||
|
||||
let auth_request = portal_prelogin(&self.server, &self.gp_params).await?;
|
||||
let (tx, rx) = oneshot::channel::<()>();
|
||||
auth_window.with_webview(move |webview| {
|
||||
let auth_request = AuthRequest::new(&auth_request);
|
||||
platform_impl::load_auth_request(&webview.inner(), &auth_request);
|
||||
|
||||
tx.send(()).expect("Failed to send message to the channel")
|
||||
})?;
|
||||
|
||||
rx.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn raise_window(&self, auth_window: &WebviewWindow) {
|
||||
let visible = auth_window.is_visible().unwrap_or(false);
|
||||
if visible {
|
||||
return;
|
||||
}
|
||||
|
||||
info!("Raising auth window...");
|
||||
if let Err(err) = auth_window.raise() {
|
||||
warn!("Failed to raise window: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn portal_prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<String> {
|
||||
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(_) => bail!("Received non-SAML prelogin response"),
|
||||
}
|
||||
}
|
||||
|
||||
fn send_auth_result(auth_result_tx: &mpsc::UnboundedSender<AuthResult>, auth_result: AuthResult) {
|
||||
if let Err(err) = auth_result_tx.send(auth_result) {
|
||||
warn!("Failed to send auth event: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
fn load_saml_request(wv: &Rc<WebView>, saml_request: &str) {
|
||||
if saml_request.starts_with("http") {
|
||||
info!("Load the SAML request as URI...");
|
||||
wv.load_uri(saml_request);
|
||||
} else {
|
||||
info!("Load the SAML request as HTML...");
|
||||
wv.load_html(saml_request, None);
|
||||
}
|
||||
}
|
||||
|
||||
fn read_auth_data_from_headers(response: &URIResponse) -> AuthResult {
|
||||
response.http_headers().map_or_else(
|
||||
|| {
|
||||
info!("No headers found in response");
|
||||
Err(AuthDataError::NotFound)
|
||||
},
|
||||
|mut headers| match headers.get("saml-auth-status") {
|
||||
Some(status) if status == "1" => {
|
||||
let username = headers.get("saml-username").map(GString::into);
|
||||
let prelogin_cookie = headers.get("prelogin-cookie").map(GString::into);
|
||||
let portal_userauthcookie = headers.get("portal-userauthcookie").map(GString::into);
|
||||
|
||||
if SamlAuthData::check(&username, &prelogin_cookie, &portal_userauthcookie) {
|
||||
return Ok(SamlAuthData::new(
|
||||
username.unwrap(),
|
||||
prelogin_cookie,
|
||||
portal_userauthcookie,
|
||||
));
|
||||
}
|
||||
|
||||
info!("Found invalid auth data in headers");
|
||||
Err(AuthDataError::Invalid)
|
||||
}
|
||||
Some(status) => {
|
||||
info!("Found invalid SAML status: {} in headers", status);
|
||||
Err(AuthDataError::Invalid)
|
||||
}
|
||||
None => {
|
||||
info!("No saml-auth-status header found");
|
||||
Err(AuthDataError::NotFound)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn read_auth_data_from_body<F>(main_resource: &WebResource, callback: F)
|
||||
where
|
||||
F: FnOnce(Result<SamlAuthData, AuthDataParseError>) + Send + 'static,
|
||||
{
|
||||
main_resource.data(Cancellable::NONE, |data| match data {
|
||||
Ok(data) => {
|
||||
let html = String::from_utf8_lossy(&data);
|
||||
callback(read_auth_data_from_html(&html));
|
||||
}
|
||||
Err(err) => {
|
||||
info!("Failed to read response body: {}", err);
|
||||
callback(Err(AuthDataParseError::Invalid))
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn read_auth_data_from_html(html: &str) -> Result<SamlAuthData, AuthDataParseError> {
|
||||
if html.contains("Temporarily Unavailable") {
|
||||
info!("Found 'Temporarily Unavailable' in HTML, auth failed");
|
||||
return Err(AuthDataParseError::Invalid);
|
||||
}
|
||||
|
||||
SamlAuthData::from_html(html).or_else(|err| {
|
||||
if let Some(gpcallback) = extract_gpcallback(html) {
|
||||
info!("Found gpcallback from html...");
|
||||
SamlAuthData::from_gpcallback(&gpcallback)
|
||||
} else {
|
||||
Err(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn extract_gpcallback(html: &str) -> Option<String> {
|
||||
let re = Regex::new(r#"globalprotectcallback:[^"]+"#).unwrap();
|
||||
re.captures(html)
|
||||
.and_then(|captures| captures.get(0))
|
||||
.map(|m| html_escape::decode_html_entities(m.as_str()).to_string())
|
||||
}
|
||||
|
||||
fn read_auth_data(main_resource: &WebResource, auth_result_tx: mpsc::UnboundedSender<AuthResult>) {
|
||||
let Some(response) = main_resource.response() else {
|
||||
info!("No response found in main resource");
|
||||
send_auth_result(&auth_result_tx, Err(AuthDataError::Invalid));
|
||||
return;
|
||||
};
|
||||
|
||||
info!("Trying to read auth data from response headers...");
|
||||
|
||||
match read_auth_data_from_headers(&response) {
|
||||
Ok(auth_data) => {
|
||||
info!("Got auth data from headers");
|
||||
send_auth_result(&auth_result_tx, Ok(auth_data));
|
||||
}
|
||||
Err(AuthDataError::Invalid) => {
|
||||
info!("Found invalid auth data in headers, trying to read from body...");
|
||||
read_auth_data_from_body(main_resource, move |auth_result| {
|
||||
// Since we have already found invalid auth data in headers, which means this could be the `/SAML20/SP/ACS` endpoint
|
||||
// any error result from body should be considered as invalid, and trigger a retry
|
||||
let auth_result = auth_result.map_err(|err| {
|
||||
info!("Failed to read auth data from body: {}", err);
|
||||
AuthDataError::Invalid
|
||||
});
|
||||
send_auth_result(&auth_result_tx, auth_result);
|
||||
});
|
||||
}
|
||||
Err(AuthDataError::NotFound) => {
|
||||
info!("No auth data found in headers, trying to read from body...");
|
||||
|
||||
let is_acs_endpoint = main_resource.uri().map_or(false, |uri| uri.contains("/SAML20/SP/ACS"));
|
||||
|
||||
read_auth_data_from_body(main_resource, move |auth_result| {
|
||||
// If the endpoint is `/SAML20/SP/ACS` and no auth data found in body, it should be considered as invalid
|
||||
let auth_result = auth_result.map_err(|err| {
|
||||
info!("Failed to read auth data from body: {}", err);
|
||||
|
||||
if !is_acs_endpoint && matches!(err, AuthDataParseError::NotFound) {
|
||||
AuthDataError::NotFound
|
||||
} else {
|
||||
AuthDataError::Invalid
|
||||
}
|
||||
});
|
||||
|
||||
send_auth_result(&auth_result_tx, auth_result)
|
||||
});
|
||||
}
|
||||
Err(AuthDataError::TlsError) => {
|
||||
// NOTE: This is unreachable
|
||||
info!("TLS error found in headers, trying to read from body...");
|
||||
send_auth_result(&auth_result_tx, Err(AuthDataError::TlsError));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn clear_webview_cookies(window: &Window) -> anyhow::Result<()> {
|
||||
let (tx, rx) = oneshot::channel::<Result<(), String>>();
|
||||
|
||||
window.with_webview(|wv| {
|
||||
let send_result = move |result: Result<(), String>| {
|
||||
if let Err(err) = tx.send(result) {
|
||||
info!("Failed to send result: {:?}", err);
|
||||
}
|
||||
};
|
||||
|
||||
let wv = wv.inner();
|
||||
let context = match wv.context() {
|
||||
Some(context) => context,
|
||||
None => {
|
||||
send_result(Err("No webview context found".into()));
|
||||
return;
|
||||
}
|
||||
};
|
||||
let data_manager = match context.website_data_manager() {
|
||||
Some(manager) => manager,
|
||||
None => {
|
||||
send_result(Err("No data manager found".into()));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let now = Instant::now();
|
||||
data_manager.clear(
|
||||
WebsiteDataTypes::COOKIES,
|
||||
TimeSpan(0),
|
||||
Cancellable::NONE,
|
||||
move |result| match result {
|
||||
Err(err) => {
|
||||
send_result(Err(err.to_string()));
|
||||
}
|
||||
Ok(_) => {
|
||||
info!("Cookies cleared in {} ms", now.elapsed().as_millis());
|
||||
send_result(Ok(()));
|
||||
}
|
||||
},
|
||||
);
|
||||
})?;
|
||||
|
||||
rx.await?.map_err(|err| anyhow::anyhow!(err))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn extract_gpcallback_some() {
|
||||
let html = r#"
|
||||
<meta http-equiv="refresh" content="0; URL=globalprotectcallback:PGh0bWw+PCEtLSA8c">
|
||||
<meta http-equiv="refresh" content="0; URL=globalprotectcallback:PGh0bWw+PCEtLSA8c">
|
||||
"#;
|
||||
|
||||
assert_eq!(
|
||||
extract_gpcallback(html).as_deref(),
|
||||
Some("globalprotectcallback:PGh0bWw+PCEtLSA8c")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_gpcallback_cas() {
|
||||
let html = r#"
|
||||
<meta http-equiv="refresh" content="0; URL=globalprotectcallback:cas-as=1&un=xyz@email.com&token=very_long_string">
|
||||
"#;
|
||||
|
||||
assert_eq!(
|
||||
extract_gpcallback(html).as_deref(),
|
||||
Some("globalprotectcallback:cas-as=1&un=xyz@email.com&token=very_long_string")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_gpcallback_none() {
|
||||
let html = r#"
|
||||
<meta http-equiv="refresh" content="0; URL=PGh0bWw+PCEtLSA8c">
|
||||
"#;
|
||||
|
||||
assert_eq!(extract_gpcallback(html), None);
|
||||
}
|
||||
async fn wait_auth_data() -> anyhow::Result<SamlAuthData> {
|
||||
// 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();
|
||||
let port_file = temp_dir().join(GP_CALLBACK_PORT_FILENAME);
|
||||
|
||||
// Write the port to a file
|
||||
fs::write(&port_file, port.to_string())?;
|
||||
fs::set_permissions(&port_file, fs::Permissions::from_mode(0o600))?;
|
||||
|
||||
// Remove the previous log file
|
||||
let callback_log = temp_dir().join("gpcallback.log");
|
||||
let _ = fs::remove_file(&callback_log);
|
||||
|
||||
info!("Listening authentication data on port {}", port);
|
||||
info!(
|
||||
"If it hangs, please check the logs at `{}` for more information",
|
||||
callback_log.display()
|
||||
);
|
||||
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(&port_file)?;
|
||||
|
||||
let auth_data = SamlAuthData::from_gpcallback(&data)?;
|
||||
Ok(auth_data)
|
||||
}
|
||||
|
@ -1,21 +1,16 @@
|
||||
use std::{env::temp_dir, fs, os::unix::fs::PermissionsExt};
|
||||
|
||||
use clap::Parser;
|
||||
use gpapi::{
|
||||
auth::{SamlAuthData, SamlAuthResult},
|
||||
clap::args::Os,
|
||||
clap::{args::Os, handle_error, Args},
|
||||
gp_params::{ClientOs, GpParams},
|
||||
process::browser_authenticator::BrowserAuthenticator,
|
||||
utils::{normalize_server, openssl},
|
||||
utils::{env_utils, normalize_server, openssl},
|
||||
GP_USER_AGENT,
|
||||
};
|
||||
use gpauth::auth_window::AuthWindow;
|
||||
use log::{info, LevelFilter};
|
||||
use serde_json::json;
|
||||
use tauri::{App, AppHandle, RunEvent};
|
||||
use tauri::RunEvent;
|
||||
use tempfile::NamedTempFile;
|
||||
use tokio::{io::AsyncReadExt, net::TcpListener};
|
||||
|
||||
use crate::auth_window::{portal_prelogin, AuthWindow};
|
||||
|
||||
const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), " (", compile_time::date_str!(), ")");
|
||||
|
||||
@ -78,74 +73,19 @@ struct Cli {
|
||||
browser: Option<String>,
|
||||
}
|
||||
|
||||
impl Cli {
|
||||
async fn run(&mut self) -> anyhow::Result<()> {
|
||||
if self.ignore_tls_errors {
|
||||
info!("TLS errors will be ignored");
|
||||
}
|
||||
|
||||
let mut openssl_conf = self.prepare_env()?;
|
||||
|
||||
self.server = normalize_server(&self.server)?;
|
||||
let gp_params = self.build_gp_params();
|
||||
|
||||
// Get the initial SAML request
|
||||
let saml_request = match self.saml_request {
|
||||
Some(ref saml_request) => saml_request.clone(),
|
||||
None => portal_prelogin(&self.server, &gp_params).await?,
|
||||
};
|
||||
|
||||
let browser_auth = if let Some(browser) = &self.browser {
|
||||
Some(BrowserAuthenticator::new_with_browser(&saml_request, browser))
|
||||
} else if self.default_browser {
|
||||
Some(BrowserAuthenticator::new(&saml_request))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(browser_auth) = browser_auth {
|
||||
browser_auth.authenticate()?;
|
||||
|
||||
info!("Please continue the authentication process in the default browser");
|
||||
|
||||
let auth_result = match wait_auth_data().await {
|
||||
Ok(auth_data) => SamlAuthResult::Success(auth_data),
|
||||
Err(err) => SamlAuthResult::Failure(format!("{}", err)),
|
||||
};
|
||||
|
||||
info!("Authentication completed");
|
||||
|
||||
println!("{}", json!(auth_result));
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.saml_request.replace(saml_request);
|
||||
|
||||
let app = create_app(self.clone())?;
|
||||
|
||||
app.run(move |_app_handle, event| {
|
||||
if let RunEvent::Exit = event {
|
||||
if let Some(file) = openssl_conf.take() {
|
||||
if let Err(err) = file.close() {
|
||||
info!("Error closing OpenSSL config file: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
impl Args for Cli {
|
||||
fn fix_openssl(&self) -> bool {
|
||||
self.fix_openssl
|
||||
}
|
||||
|
||||
fn ignore_tls_errors(&self) -> bool {
|
||||
self.ignore_tls_errors
|
||||
}
|
||||
}
|
||||
|
||||
impl Cli {
|
||||
fn prepare_env(&self) -> anyhow::Result<Option<NamedTempFile>> {
|
||||
std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1");
|
||||
|
||||
if self.hidpi {
|
||||
info!("Setting GDK_SCALE=2 and GDK_DPI_SCALE=0.5");
|
||||
|
||||
std::env::set_var("GDK_SCALE", "2");
|
||||
std::env::set_var("GDK_DPI_SCALE", "0.5");
|
||||
}
|
||||
env_utils::patch_gui_runtime_env(self.hidpi);
|
||||
|
||||
if self.fix_openssl {
|
||||
info!("Fixing OpenSSL environment");
|
||||
@ -157,6 +97,64 @@ impl Cli {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn run(&self) -> anyhow::Result<()> {
|
||||
if self.ignore_tls_errors {
|
||||
info!("TLS errors will be ignored");
|
||||
}
|
||||
|
||||
let mut openssl_conf = self.prepare_env()?;
|
||||
|
||||
let server = normalize_server(&self.server)?;
|
||||
let server: &'static str = Box::leak(server.into_boxed_str());
|
||||
let gp_params: &'static GpParams = Box::leak(Box::new(self.build_gp_params()));
|
||||
|
||||
let auth_request = self.saml_request.clone().unwrap_or_default();
|
||||
let auth_request: &'static str = Box::leak(Box::new(auth_request));
|
||||
|
||||
let auth_window = AuthWindow::new(&server, gp_params)
|
||||
.with_auth_request(&auth_request)
|
||||
.with_clean(self.clean);
|
||||
|
||||
let browser = if let Some(browser) = self.browser.as_deref() {
|
||||
Some(browser)
|
||||
} else if self.default_browser {
|
||||
Some("default")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if browser.is_some() {
|
||||
let auth_result = auth_window.browser_authenticate(browser).await;
|
||||
print_auth_result(auth_result);
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
tauri::Builder::default()
|
||||
.setup(move |app| {
|
||||
let app_handle = app.handle().clone();
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let auth_result = auth_window.webview_authenticate(&app_handle).await;
|
||||
print_auth_result(auth_result);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.build(tauri::generate_context!())?
|
||||
.run(move |_app_handle, event| {
|
||||
if let RunEvent::Exit = event {
|
||||
if let Some(file) = openssl_conf.take() {
|
||||
if let Err(err) = file.close() {
|
||||
info!("Error closing OpenSSL config file: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_gp_params(&self) -> GpParams {
|
||||
let gp_params = GpParams::builder()
|
||||
.user_agent(&self.user_agent)
|
||||
@ -168,37 +166,6 @@ impl Cli {
|
||||
|
||||
gp_params
|
||||
}
|
||||
|
||||
async fn saml_auth(&self, app_handle: AppHandle) -> anyhow::Result<SamlAuthData> {
|
||||
let auth_window = AuthWindow::new(app_handle)
|
||||
.server(&self.server)
|
||||
.user_agent(&self.user_agent)
|
||||
.gp_params(self.build_gp_params())
|
||||
.saml_request(self.saml_request.as_ref().unwrap())
|
||||
.clean(self.clean);
|
||||
|
||||
auth_window.open().await
|
||||
}
|
||||
}
|
||||
|
||||
fn create_app(cli: Cli) -> anyhow::Result<App> {
|
||||
let app = tauri::Builder::default()
|
||||
.setup(|app| {
|
||||
let app_handle = app.handle();
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let auth_result = match cli.saml_auth(app_handle.clone()).await {
|
||||
Ok(auth_data) => SamlAuthResult::Success(auth_data),
|
||||
Err(err) => SamlAuthResult::Failure(format!("{}", err)),
|
||||
};
|
||||
|
||||
println!("{}", json!(auth_result));
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
.build(tauri::generate_context!())?;
|
||||
|
||||
Ok(app)
|
||||
}
|
||||
|
||||
fn init_logger() {
|
||||
@ -206,53 +173,22 @@ fn init_logger() {
|
||||
}
|
||||
|
||||
pub async fn run() {
|
||||
let mut cli = Cli::parse();
|
||||
let cli = Cli::parse();
|
||||
|
||||
init_logger();
|
||||
info!("gpauth started: {}", VERSION);
|
||||
|
||||
if let Err(err) = cli.run().await {
|
||||
eprintln!("\nError: {}", err);
|
||||
|
||||
if err.to_string().contains("unsafe legacy renegotiation") && !cli.fix_openssl {
|
||||
eprintln!("\nRe-run it with the `--fix-openssl` option to work around this issue, e.g.:\n");
|
||||
// Print the command
|
||||
let args = std::env::args().collect::<Vec<_>>();
|
||||
eprintln!("{} --fix-openssl {}\n", args[0], args[1..].join(" "));
|
||||
}
|
||||
|
||||
handle_error(err, &cli);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_auth_data() -> anyhow::Result<SamlAuthData> {
|
||||
// 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();
|
||||
let port_file = temp_dir().join("gpcallback.port");
|
||||
fn print_auth_result(auth_result: anyhow::Result<SamlAuthData>) {
|
||||
let auth_result = match auth_result {
|
||||
Ok(auth_data) => SamlAuthResult::Success(auth_data),
|
||||
Err(err) => SamlAuthResult::Failure(format!("{}", err)),
|
||||
};
|
||||
|
||||
// Write the port to a file
|
||||
fs::write(&port_file, port.to_string())?;
|
||||
fs::set_permissions(&port_file, fs::Permissions::from_mode(0o600))?;
|
||||
|
||||
// Remove the previous log file
|
||||
let callback_log = temp_dir().join("gpcallback.log");
|
||||
let _ = fs::remove_file(&callback_log);
|
||||
|
||||
info!("Listening authentication data on port {}", port);
|
||||
info!(
|
||||
"If it hangs, please check the logs at `{}` for more information",
|
||||
callback_log.display()
|
||||
);
|
||||
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(&port_file)?;
|
||||
|
||||
let auth_data = SamlAuthData::from_gpcallback(&data)?;
|
||||
Ok(auth_data)
|
||||
println!("{}", json!(auth_result));
|
||||
}
|
||||
|
174
apps/gpauth/src/common.rs
Normal file
174
apps/gpauth/src/common.rs
Normal file
@ -0,0 +1,174 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpapi::{
|
||||
auth::{AuthDataParseResult, SamlAuthData},
|
||||
error::AuthDataParseError,
|
||||
};
|
||||
use log::{info, warn};
|
||||
use regex::Regex;
|
||||
|
||||
use crate::auth_messenger::{AuthError, AuthMessenger};
|
||||
|
||||
pub struct AuthSettings<'a> {
|
||||
pub auth_request: AuthRequest<'a>,
|
||||
pub auth_messenger: Arc<AuthMessenger>,
|
||||
pub ignore_tls_errors: bool,
|
||||
}
|
||||
|
||||
pub struct AuthRequest<'a>(&'a str);
|
||||
|
||||
impl<'a> AuthRequest<'a> {
|
||||
pub fn new(auth_request: &'a str) -> Self {
|
||||
Self(auth_request)
|
||||
}
|
||||
|
||||
pub fn is_url(&self) -> bool {
|
||||
self.0.starts_with("http")
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for handling authentication response
|
||||
pub trait AuthResponse {
|
||||
fn get_header(&self, key: &str) -> Option<String>;
|
||||
fn get_body<F>(&self, cb: F)
|
||||
where
|
||||
F: FnOnce(anyhow::Result<Vec<u8>>) + 'static;
|
||||
|
||||
fn url(&self) -> Option<String>;
|
||||
|
||||
fn is_acs_endpoint(&self) -> bool {
|
||||
self.url().map_or(false, |url| url.ends_with("/SAML20/SP/ACS"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_auth_data(auth_response: &impl AuthResponse, auth_messenger: &Arc<AuthMessenger>) {
|
||||
let auth_messenger = Arc::clone(auth_messenger);
|
||||
|
||||
match read_from_headers(auth_response) {
|
||||
Ok(auth_data) => {
|
||||
info!("Found auth data in headers");
|
||||
auth_messenger.send_auth_data(auth_data);
|
||||
}
|
||||
Err(header_err) => {
|
||||
info!("Failed to read auth data from headers: {}", header_err);
|
||||
|
||||
let is_acs_endpoint = auth_response.is_acs_endpoint();
|
||||
read_from_body(auth_response, move |auth_result| {
|
||||
// If the endpoint is `/SAML20/SP/ACS` and no auth data found in body, it should be considered as invalid
|
||||
let auth_result = auth_result.map_err(move |e| {
|
||||
info!("Failed to read auth data from body: {}", e);
|
||||
if is_acs_endpoint || e.is_invalid() || header_err.is_invalid() {
|
||||
AuthError::Invalid
|
||||
} else {
|
||||
AuthError::NotFound
|
||||
}
|
||||
});
|
||||
|
||||
auth_messenger.send_auth_result(auth_result);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_from_headers(auth_response: &impl AuthResponse) -> AuthDataParseResult {
|
||||
let Some(status) = auth_response.get_header("saml-auth-status") else {
|
||||
info!("No SAML auth status found in headers");
|
||||
return Err(AuthDataParseError::NotFound);
|
||||
};
|
||||
|
||||
if status != "1" {
|
||||
info!("Found invalid auth status: {}", status);
|
||||
return Err(AuthDataParseError::Invalid);
|
||||
}
|
||||
|
||||
let username = auth_response.get_header("saml-username");
|
||||
let prelogin_cookie = auth_response.get_header("prelogin-cookie");
|
||||
let portal_userauthcookie = auth_response.get_header("portal-userauthcookie");
|
||||
|
||||
SamlAuthData::new(username, prelogin_cookie, portal_userauthcookie).map_err(|e| {
|
||||
warn!("Found invalid auth data: {}", e);
|
||||
AuthDataParseError::Invalid
|
||||
})
|
||||
}
|
||||
|
||||
fn read_from_body<F>(auth_response: &impl AuthResponse, cb: F)
|
||||
where
|
||||
F: FnOnce(AuthDataParseResult) + 'static,
|
||||
{
|
||||
auth_response.get_body(|body| match body {
|
||||
Ok(body) => {
|
||||
let html = String::from_utf8_lossy(&body);
|
||||
cb(read_from_html(&html))
|
||||
}
|
||||
Err(err) => {
|
||||
info!("Failed to read body: {}", err);
|
||||
cb(Err(AuthDataParseError::Invalid))
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn read_from_html(html: &str) -> AuthDataParseResult {
|
||||
if html.contains("Temporarily Unavailable") {
|
||||
info!("Found 'Temporarily Unavailable' in HTML, auth failed");
|
||||
return Err(AuthDataParseError::Invalid);
|
||||
}
|
||||
|
||||
SamlAuthData::from_html(html).or_else(|err| {
|
||||
if let Some(gpcallback) = extract_gpcallback(html) {
|
||||
info!("Found gpcallback from html...");
|
||||
SamlAuthData::from_gpcallback(&gpcallback)
|
||||
} else {
|
||||
Err(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn extract_gpcallback(html: &str) -> Option<String> {
|
||||
let re = Regex::new(r#"globalprotectcallback:[^"]+"#).unwrap();
|
||||
re.captures(html)
|
||||
.and_then(|captures| captures.get(0))
|
||||
.map(|m| html_escape::decode_html_entities(m.as_str()).to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn extract_gpcallback_some() {
|
||||
let html = r#"
|
||||
<meta http-equiv="refresh" content="0; URL=globalprotectcallback:PGh0bWw+PCEtLSA8c">
|
||||
<meta http-equiv="refresh" content="0; URL=globalprotectcallback:PGh0bWw+PCEtLSA8c">
|
||||
"#;
|
||||
|
||||
assert_eq!(
|
||||
extract_gpcallback(html).as_deref(),
|
||||
Some("globalprotectcallback:PGh0bWw+PCEtLSA8c")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_gpcallback_cas() {
|
||||
let html = r#"
|
||||
<meta http-equiv="refresh" content="0; URL=globalprotectcallback:cas-as=1&un=xyz@email.com&token=very_long_string">
|
||||
"#;
|
||||
|
||||
assert_eq!(
|
||||
extract_gpcallback(html).as_deref(),
|
||||
Some("globalprotectcallback:cas-as=1&un=xyz@email.com&token=very_long_string")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_gpcallback_none() {
|
||||
let html = r#"
|
||||
<meta http-equiv="refresh" content="0; URL=PGh0bWw+PCEtLSA8c">
|
||||
"#;
|
||||
|
||||
assert_eq!(extract_gpcallback(html), None);
|
||||
}
|
||||
}
|
7
apps/gpauth/src/lib.rs
Normal file
7
apps/gpauth/src/lib.rs
Normal file
@ -0,0 +1,7 @@
|
||||
mod auth_messenger;
|
||||
mod common;
|
||||
|
||||
pub mod auth_window;
|
||||
|
||||
#[cfg_attr(not(target_os = "macos"), path = "unix.rs")]
|
||||
mod platform_impl;
|
@ -1,6 +1,5 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
mod auth_window;
|
||||
mod cli;
|
||||
|
||||
#[tokio::main]
|
||||
|
133
apps/gpauth/src/unix.rs
Normal file
133
apps/gpauth/src/unix.rs
Normal file
@ -0,0 +1,133 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::bail;
|
||||
use gpapi::utils::redact::redact_uri;
|
||||
use log::{info, warn};
|
||||
use webkit2gtk::{
|
||||
gio::Cancellable,
|
||||
glib::{GString, TimeSpan},
|
||||
LoadEvent, TLSErrorsPolicy, URIResponseExt, WebResource, WebResourceExt, WebView, WebViewExt, WebsiteDataManagerExt,
|
||||
WebsiteDataManagerExtManual, WebsiteDataTypes,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
auth_messenger::AuthError,
|
||||
common::{read_auth_data, AuthRequest, AuthResponse, AuthSettings},
|
||||
};
|
||||
|
||||
impl AuthResponse for WebResource {
|
||||
fn get_header(&self, key: &str) -> Option<String> {
|
||||
self
|
||||
.response()
|
||||
.and_then(|response| response.http_headers())
|
||||
.and_then(|headers| headers.one(key))
|
||||
.map(GString::into)
|
||||
}
|
||||
|
||||
fn get_body<F>(&self, cb: F)
|
||||
where
|
||||
F: FnOnce(anyhow::Result<Vec<u8>>) + 'static,
|
||||
{
|
||||
let cancellable = Cancellable::NONE;
|
||||
self.data(cancellable, |data| cb(data.map_err(|e| anyhow::anyhow!(e))));
|
||||
}
|
||||
|
||||
fn url(&self) -> Option<String> {
|
||||
self.uri().map(GString::into)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_data<F>(wv: &WebView, cb: F)
|
||||
where
|
||||
F: FnOnce(anyhow::Result<()>) + Send + 'static,
|
||||
{
|
||||
let Some(data_manager) = wv.website_data_manager() else {
|
||||
cb(Err(anyhow::anyhow!("Failed to get website data manager")));
|
||||
return;
|
||||
};
|
||||
|
||||
data_manager.clear(
|
||||
WebsiteDataTypes::COOKIES,
|
||||
TimeSpan(0),
|
||||
Cancellable::NONE,
|
||||
move |result| {
|
||||
cb(result.map_err(|e| anyhow::anyhow!(e)));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn setup_webview(wv: &WebView, auth_settings: AuthSettings) -> anyhow::Result<()> {
|
||||
let AuthSettings {
|
||||
auth_request,
|
||||
auth_messenger,
|
||||
ignore_tls_errors,
|
||||
} = auth_settings;
|
||||
let auth_messenger_clone = Arc::clone(&auth_messenger);
|
||||
|
||||
let Some(data_manager) = wv.website_data_manager() else {
|
||||
bail!("Failed to get website data manager");
|
||||
};
|
||||
|
||||
if ignore_tls_errors {
|
||||
data_manager.set_tls_errors_policy(TLSErrorsPolicy::Ignore);
|
||||
}
|
||||
|
||||
wv.connect_load_changed(move |wv, event| {
|
||||
if event == LoadEvent::Started {
|
||||
auth_messenger_clone.cancel_raise_window();
|
||||
return;
|
||||
}
|
||||
|
||||
if event != LoadEvent::Finished {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(main_resource) = wv.main_resource() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let uri = main_resource.uri().unwrap_or("".into());
|
||||
if uri.is_empty() {
|
||||
warn!("Loaded an empty URI");
|
||||
auth_messenger_clone.send_auth_error(AuthError::Invalid);
|
||||
return;
|
||||
}
|
||||
|
||||
read_auth_data(&main_resource, &auth_messenger_clone);
|
||||
});
|
||||
|
||||
wv.connect_load_failed_with_tls_errors(move |_wv, uri, cert, err| {
|
||||
let redacted_uri = redact_uri(uri);
|
||||
warn!(
|
||||
"Failed to load uri: {} with error: {}, cert: {}",
|
||||
redacted_uri, err, cert
|
||||
);
|
||||
|
||||
auth_messenger.send_auth_error(AuthError::TlsError);
|
||||
true
|
||||
});
|
||||
|
||||
wv.connect_load_failed(move |_wv, _event, uri, err| {
|
||||
let redacted_uri = redact_uri(uri);
|
||||
if !uri.starts_with("globalprotectcallback:") {
|
||||
warn!("Failed to load uri: {} with error: {}", redacted_uri, err);
|
||||
}
|
||||
// NOTE: Don't send error here, since load_changed event will be triggered after this
|
||||
// true to stop other handlers from being invoked for the event. false to propagate the event further.
|
||||
true
|
||||
});
|
||||
|
||||
load_auth_request(wv, &auth_request);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_auth_request(wv: &WebView, auth_request: &AuthRequest) {
|
||||
if auth_request.is_url() {
|
||||
info!("Loading auth request as URI...");
|
||||
wv.load_uri(auth_request.as_str());
|
||||
} else {
|
||||
info!("Loading auth request as HTML...");
|
||||
wv.load_html(auth_request.as_str(), None);
|
||||
}
|
||||
}
|
@ -1,47 +1,16 @@
|
||||
{
|
||||
"$schema": "https://cdn.jsdelivr.net/gh/tauri-apps/tauri@tauri-v1.5.0/tooling/cli/schema.json",
|
||||
"$schema": "https://cdn.jsdelivr.net/gh/tauri-apps/tauri@tauri-v2.1.1/crates/tauri-cli/config.schema.json",
|
||||
"build": {
|
||||
"distDir": [
|
||||
"index.html"
|
||||
],
|
||||
"devPath": [
|
||||
"index.html"
|
||||
],
|
||||
"frontendDist": ["index.html"],
|
||||
"beforeDevCommand": "",
|
||||
"beforeBuildCommand": "",
|
||||
"withGlobalTauri": false
|
||||
"beforeBuildCommand": ""
|
||||
},
|
||||
"package": {
|
||||
"productName": "gpauth",
|
||||
"version": "0.0.0"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
"all": false,
|
||||
"http": {
|
||||
"all": true,
|
||||
"request": true,
|
||||
"scope": [
|
||||
"http://*",
|
||||
"https://*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "deb",
|
||||
"identifier": "com.yuezk.gpauth",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
},
|
||||
"identifier": "com.yuezk.gpauth",
|
||||
"productName": "gpauth",
|
||||
"app": {
|
||||
"withGlobalTauri": false,
|
||||
"security": {
|
||||
"csp": null
|
||||
},
|
||||
"windows": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
[package]
|
||||
name = "gpclient"
|
||||
rust-version.workspace = true
|
||||
authors.workspace = true
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
@ -1,7 +1,10 @@
|
||||
use std::{env::temp_dir, fs::File};
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use gpapi::utils::openssl;
|
||||
use gpapi::{
|
||||
clap::{handle_error, Args},
|
||||
utils::openssl,
|
||||
};
|
||||
use log::{info, LevelFilter};
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
@ -50,12 +53,25 @@ struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: CliCommand,
|
||||
|
||||
#[arg(long, help = "Uses extended compatibility mode for OpenSSL operations to support a broader range of systems and formats.")]
|
||||
#[arg(
|
||||
long,
|
||||
help = "Uses extended compatibility mode for OpenSSL operations to support a broader range of systems and formats."
|
||||
)]
|
||||
fix_openssl: bool,
|
||||
#[arg(long, help = "Ignore the TLS errors")]
|
||||
ignore_tls_errors: bool,
|
||||
}
|
||||
|
||||
impl Args for Cli {
|
||||
fn fix_openssl(&self) -> bool {
|
||||
self.fix_openssl
|
||||
}
|
||||
|
||||
fn ignore_tls_errors(&self) -> bool {
|
||||
self.ignore_tls_errors
|
||||
}
|
||||
}
|
||||
|
||||
impl Cli {
|
||||
fn fix_openssl(&self) -> anyhow::Result<Option<NamedTempFile>> {
|
||||
if self.fix_openssl {
|
||||
@ -113,24 +129,7 @@ pub(crate) async fn run() {
|
||||
info!("gpclient started: {}", VERSION);
|
||||
|
||||
if let Err(err) = cli.run().await {
|
||||
eprintln!("\nError: {}", err);
|
||||
|
||||
let err = err.to_string();
|
||||
|
||||
if err.contains("unsafe legacy renegotiation") && !cli.fix_openssl {
|
||||
eprintln!("\nRe-run it with the `--fix-openssl` option to work around this issue, e.g.:\n");
|
||||
// Print the command
|
||||
let args = std::env::args().collect::<Vec<_>>();
|
||||
eprintln!("{} --fix-openssl {}\n", args[0], args[1..].join(" "));
|
||||
}
|
||||
|
||||
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");
|
||||
// Print the command
|
||||
let args = std::env::args().collect::<Vec<_>>();
|
||||
eprintln!("{} --ignore-tls-errors {}\n", args[0], args[1..].join(" "));
|
||||
}
|
||||
|
||||
handle_error(err, &cli);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
use crate::GP_CLIENT_LOCK_FILE;
|
||||
use log::{info, warn};
|
||||
use std::fs;
|
||||
use sysinfo::{Pid, ProcessExt, Signal, System, SystemExt};
|
||||
use sysinfo::{Pid, Signal, System};
|
||||
|
||||
pub(crate) struct DisconnectHandler;
|
||||
|
||||
|
@ -4,7 +4,8 @@ use clap::Args;
|
||||
use directories::ProjectDirs;
|
||||
use gpapi::{
|
||||
process::service_launcher::ServiceLauncher,
|
||||
utils::{endpoint::http_endpoint, env_file, shutdown_signal},
|
||||
utils::{endpoint::http_endpoint, env_utils, shutdown_signal},
|
||||
GP_CALLBACK_PORT_FILENAME,
|
||||
};
|
||||
use log::info;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
@ -62,7 +63,7 @@ impl<'a> LaunchGuiHandler<'a> {
|
||||
extra_envs.insert("GP_LOG_FILE".into(), log_file_path.clone());
|
||||
|
||||
// Persist the environment variables to a file
|
||||
let env_file = env_file::persist_env_vars(Some(extra_envs))?;
|
||||
let env_file = env_utils::persist_env_vars(Some(extra_envs))?;
|
||||
let env_file = env_file.into_temp_path();
|
||||
let env_file_path = env_file.to_string_lossy().to_string();
|
||||
|
||||
@ -115,7 +116,7 @@ async fn feed_auth_data_gui(auth_data: &str) -> anyhow::Result<()> {
|
||||
async fn feed_auth_data_cli(auth_data: &str) -> anyhow::Result<()> {
|
||||
info!("Feeding auth data to the CLI");
|
||||
|
||||
let port_file = temp_dir().join("gpcallback.port");
|
||||
let port_file = temp_dir().join(GP_CALLBACK_PORT_FILENAME);
|
||||
let port = tokio::fs::read_to_string(port_file).await?;
|
||||
let mut stream = tokio::net::TcpStream::connect(format!("127.0.0.1:{}", port.trim())).await?;
|
||||
|
||||
|
1
apps/gpgui-helper/.gitignore
vendored
1
apps/gpgui-helper/.gitignore
vendored
@ -8,7 +8,6 @@ pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
|
99
apps/gpgui-helper/dist/assets/icon-BlfaAlWe.svg
vendored
Normal file
99
apps/gpgui-helper/dist/assets/icon-BlfaAlWe.svg
vendored
Normal file
@ -0,0 +1,99 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 96 96"
|
||||
style="enable-background:new 0 0 96 96;"
|
||||
xml:space="preserve"
|
||||
sodipodi:docname="com.yuezk.qt.gpclient.svg"
|
||||
inkscape:version="0.92.4 5da689c313, 2019-01-14"><metadata
|
||||
id="metadata14"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs12" /><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1006"
|
||||
id="namedview10"
|
||||
showgrid="false"
|
||||
inkscape:zoom="6.9532168"
|
||||
inkscape:cx="7.9545315"
|
||||
inkscape:cy="59.062386"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="g8499" />
|
||||
<style
|
||||
type="text/css"
|
||||
id="style2">
|
||||
.st0{fill:#2980B9;}
|
||||
.st1{fill:#3498DB;}
|
||||
.st2{fill:#2ECC71;}
|
||||
.st3{fill:#27AE60;}
|
||||
</style>
|
||||
|
||||
<g
|
||||
id="g8499"
|
||||
transform="matrix(1.3407388,0,0,1.3407388,-16.409202,-16.355463)"><g
|
||||
id="XMLID_1_">
|
||||
<circle
|
||||
r="32.5"
|
||||
cy="48"
|
||||
cx="48"
|
||||
class="st0"
|
||||
id="XMLID_3_"
|
||||
style="fill:#2980b9" />
|
||||
<path
|
||||
d="m 48,15.5 v 65 C 65.9,80.5 80.5,65.7 80.5,48 80.5,30 65.9,15.5 48,15.5 Z"
|
||||
class="st1"
|
||||
id="XMLID_4_"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#3498db" />
|
||||
<path
|
||||
d="m 48,15.5 v 0.6 l 1.2,-0.3 c 0.3,-0.3 0.4,-0.3 0.6,-0.3 h -1.1 z m 7.3,0.9 c -0.1,0 0.4,0.9 1.1,1.8 0.8,1.5 1.1,2.1 1.3,2.1 0.3,-0.3 1.9,-1.2 3,-2.1 -1.7,-0.9 -3.5,-1.5 -5.4,-1.8 z m 10.3,6.2 c -0.1,0 -0.4,0 -0.9,0.6 l -0.8,0.9 0.6,0.6 c 0.3,0.6 0.8,0.9 1,1.2 0.5,0.6 0.6,0.6 0.1,1.5 -0.2,0.6 -0.3,0.9 -0.3,0.9 0.1,0.3 0.3,0.3 1.4,0.3 h 1.6 c 0.1,0 0.3,-0.6 0.4,-1.2 l 0.1,-0.9 -1.1,-0.9 c -1,-0.9 -1,-0.9 -1.4,-1.8 -0.3,-0.6 -0.6,-1.2 -0.7,-1.2 z m -3,2.4 c -0.2,0 -1.3,2.1 -1.3,2.4 0,0 0.3,0.6 0.7,0.9 0.4,0.3 0.7,0.6 0.7,0.6 0.1,0 1.2,-1.2 1.4,-1.5 C 64.2,27.1 64,26.8 63.5,26.2 63.1,25.5 62.7,25 62.6,25 Z m 9.5,1.1 0.2,0.3 c 0,0.3 -0.7,0.9 -1.4,1.5 -1.2,0.9 -1.4,1.2 -2,1.2 -0.6,0 -0.9,0.3 -1.8,0.9 -0.6,0.6 -1.2,0.9 -1.2,1.2 0,0 0.2,0.3 0.6,0.9 0.7,0.6 0.7,0.9 0.2,1.8 l -0.4,0.3 h -1.1 c -0.6,0 -1.5,0 -1.8,-0.3 -0.9,0 -0.8,0 -0.1,2.1 1,3 1.1,3.2 1.3,3.2 0.1,0 1.3,-1.2 2.8,-2.4 1.5,-1.2 2.7,-2.4 2.8,-2.4 l 0.6,0.3 c 0.4,0.3 0.5,0 1.3,-0.6 l 0.8,-0.6 0.8,0.6 c 1.9,1.2 2.2,1.5 2.3,2.4 0.2,1.5 0.3,1.8 0.5,1.8 0.1,0 1.3,-1.5 1.6,-1.8 0.1,-0.3 -0.1,-0.6 -1.1,-2.1 -0.7,-0.9 -1.1,-1.8 -1.1,-2.1 0,0 0.1,0 0.3,-0.3 0.2,0 0.4,0.3 1,0.9 -1.6,-2.3 -3.2,-4.7 -5.1,-6.8 z m 2.8,10.7 c -0.2,0 -0.9,0.9 -0.8,1.2 l 0.5,0.3 H 75 c 0.2,0 0.3,0 0.2,-0.3 C 75.1,37.4 75,36.8 74.9,36.8 Z M 72.3,38 h -2.4 l -2.4,0.3 -4.5,3.5 -4.4,3.8 v 3.5 c 0,2.1 0,3.8 0.1,3.8 0.1,0 0.7,0.9 1.5,1.5 0.8,0.9 1.5,1.5 1.8,1.8 0.4,0.3 0.5,0.3 4,0.6 l 3.4,0.3 1.6,0.9 c 0.8,0.6 1.5,1.2 1.6,1.2 0.1,0 -0.3,0.3 -0.6,0.6 l -0.6,0.6 1,1.2 c 0.5,0.6 1.3,1.5 1.7,1.8 l 0.6,0.9 v 1.7 0.9 c 3.7,-5 5.9,-11.5 6.1,-18.3 0.1,-2.7 -0.3,-5.3 -0.8,-8 l -0.6,-0.3 c -0.1,0 -0.5,0.3 -1,0.6 -0.5,0.3 -1,0.9 -1.1,0.9 -0.1,0 -0.8,-0.3 -1.8,-0.6 l -1.8,-0.6 v -0.9 c 0,-0.6 0,-0.9 -0.6,-1.5 z M 48,63.7 V 64 h 0.2 z"
|
||||
class="st2"
|
||||
id="XMLID_13_"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#2ecc71" />
|
||||
<path
|
||||
d="m 48,15.5 c -3.1,0 -6.2,0.5 -9,1.3 0.3,0.4 0.3,0.4 0.6,0.9 1.5,2.5 1.7,2.8 2.1,2.9 0.3,0 0.9,0.1 1.6,0.1 h 1.2 l 0.9,-2 0.8,-1.9 1.8,-0.6 z m -16.9,4.7 c -2.8,1.7 -5.4,3.9 -7.6,6.4 -3.8,4.3 -6.3,9.6 -7.4,15.4 0.5,0 0.9,-0.1 1.8,-0.1 2.8,0.1 2.5,0 3.4,1.4 0.5,0.8 0.6,0.8 1.4,0.8 1,0.1 0.9,0 0.5,-1.6 -0.2,-0.6 -0.3,-1.2 -0.3,-1.4 0,-0.2 0.5,-0.7 1.7,-1.6 1.9,-1.5 1.8,-1.3 1.5,-2.9 -0.1,-0.3 0.1,-0.6 0.6,-1.2 0.7,-0.7 0.7,-0.6 1.4,-0.6 h 0.7 l 0.1,-1.2 c 0.1,-0.7 0.1,-1.3 0.2,-1.3 0,0 1.9,-1.1 4.1,-2.3 2.2,-1.2 4.1,-2.2 4.2,-2.3 0.2,-0.2 -0.3,-0.8 -2.7,-3.8 -1.5,-1.9 -2.8,-3.6 -2.9,-3.7 z m -5.8,23 c -0.1,0 -0.1,0.3 -0.1,0.6 0,0.6 0,0.7 0.6,1 0.8,0.4 0.9,0.5 0.8,0.2 -0.1,-0.4 -1.2,-1.9 -1.3,-1.8 z m -3.4,2.1 -0.5,1.8 c 0.1,0.1 0.9,0.3 1.8,0.5 1,0.2 1.6,0.4 1.8,0.3 l 0.5,-1.3 z m -3.8,1 -1.1,0.6 c -0.6,0.3 -1.2,0.6 -1.4,0.6 h -0.1 c 0,1.4 0.1,2.8 0.3,4.2 l 0.6,0.4 1,-0.1 h 1 l 0.6,1.4 c 0.3,0.7 0.7,1.4 0.8,1.5 0.1,0.1 1,0.1 1.8,0.1 h 1.5 L 23,56.2 c 0,1.2 0,1.3 -0.6,2.2 -0.4,0.5 -0.6,1.2 -0.6,1.4 0,0.2 0.7,2.1 1.6,4.3 l 1.5,4 1.6,0.8 c 1.2,0.6 1.5,0.8 1.5,1 0,0.1 -0.4,2.1 -0.6,3.1 3,2.5 6.4,4.5 10.2,5.8 3.5,-3.6 6.8,-7.1 7.3,-7.6 l 0.7,-0.7 0.2,-1.9 c 0.2,-1.1 0.4,-2.1 0.4,-2.2 0,-0.1 0.5,-0.6 1,-1.2 0.5,-0.5 0.8,-1 0.8,-1.1 v -0.2 c -0.1,-0.1 -1.4,-1.1 -3,-2.2 l -3.1,-2.1 -1.1,-0.1 c -0.8,0 -1.2,0 -1.3,-0.2 C 39.4,59.2 39.2,58.5 39.1,57.7 39,56.9 38.9,56.2 38.8,56.1 38.8,56 38,56 37.1,56 36.2,56 35.4,55.9 35.3,55.8 35.2,55.7 35.2,55.1 35.1,54.3 35,53.6 34.9,53 34.8,52.9 34.7,52.8 33.7,52.7 32.5,52.6 30.5,52.5 30.1,52.5 29.1,52 l -1.2,-0.6 -1.6,0.7 -1.7,0.9 -1.8,-0.1 c -2,0 -1.9,0.2 -2.1,-1.6 C 20.6,50.7 20.6,50.1 20.5,50.1 20.4,50 20,50 19.6,49.9 L 18.9,49.7 19,49.2 c 0,-0.3 0,-1 0.1,-1.4 L 19.2,47 18.7,46.5 Z m 9.1,1.1 C 27.1,47.5 27.1,47.8 27,48 l -0.1,0.5 2.9,1.2 c 2.9,1.1 3.4,1.2 3.9,0.7 0.2,-0.2 0.1,-0.2 -0.3,-0.4 -0.3,-0.1 -1.7,-0.9 -3.2,-1.6 -1.7,-0.7 -2.9,-1.1 -3,-1 z"
|
||||
class="st3"
|
||||
id="XMLID_20_"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#27ae60" />
|
||||
</g><g
|
||||
transform="matrix(1.458069,0,0,1.458069,-22.631538,-19.615144)"
|
||||
id="g7664"><path
|
||||
inkscape:connector-curvature="0"
|
||||
id="XMLID_6_"
|
||||
class="st3"
|
||||
d="m 38.8,56.1 c 0,1.2 1,2.2 2.2,2.2 h 15.2 c 1.2,0 2.2,-1 2.2,-2.2 V 45.3 c 0,-1.2 -1,-2.2 -2.2,-2.2 H 40.9 c -1.2,0 -2.2,1 -2.2,2.2 v 10.8 z"
|
||||
style="fill:#f1aa27;fill-opacity:1" /><path
|
||||
style="fill:#e6e6e6"
|
||||
inkscape:connector-curvature="0"
|
||||
id="XMLID_7_"
|
||||
class="st4"
|
||||
d="m 55.5,43.1 h -3.3 v -3.7 c 0,-2.1 -1.7,-3.8 -3.8,-3.8 -2.1,0 -3.8,1.7 -3.8,3.8 v 3.8 h -3.1 v -3.8 c 0,-3.9 3.2,-7 7,-7 3.9,0 7,3.2 7,7 z" /><path
|
||||
style="fill:#e6e6e6;fill-opacity:1"
|
||||
inkscape:connector-curvature="0"
|
||||
id="XMLID_8_"
|
||||
class="st5"
|
||||
d="m 50.35,48.2 c 0,-1 -0.8,-1.8 -1.8,-1.8 -1,0 -1.8,0.8 -1.8,1.8 0,0.7 0.4,1.3 1,1.6 l -1,5.2 h 3.6 l -1,-5.2 c 0.6,-0.3 1,-0.9 1,-1.6 z" /></g></g></svg>
|
After Width: | Height: | Size: 6.7 KiB |
1
apps/gpgui-helper/dist/assets/main-B3YRsHQ2.css
vendored
Normal file
1
apps/gpgui-helper/dist/assets/main-B3YRsHQ2.css
vendored
Normal file
@ -0,0 +1 @@
|
||||
html,body,#root{height:100%;margin:0;padding:0;-webkit-user-select:none;user-select:none;cursor:default}
|
185
apps/gpgui-helper/dist/assets/main-DJgDj3te.js
vendored
Normal file
185
apps/gpgui-helper/dist/assets/main-DJgDj3te.js
vendored
Normal file
File diff suppressed because one or more lines are too long
20
apps/gpgui-helper/dist/index.html
vendored
Normal file
20
apps/gpgui-helper/dist/index.html
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>GlobalProtect</title>
|
||||
<script type="module" crossorigin src="/assets/main-DJgDj3te.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-B3YRsHQ2.css">
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
/* workaround to webview font size auto scaling */
|
||||
var htmlFontSize = getComputedStyle(document.documentElement).fontSize;
|
||||
var ratio = parseInt(htmlFontSize, 10) / 16;
|
||||
document.documentElement.style.fontSize = 16 / ratio + "px";
|
||||
</script>
|
||||
<div id="root" data-tauri-drag-region></div>
|
||||
</body>
|
||||
</html>
|
6
apps/gpgui-helper/dist/tauri.svg
vendored
Normal file
6
apps/gpgui-helper/dist/tauri.svg
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
|
||||
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.5 KiB |
1
apps/gpgui-helper/dist/vite.svg
vendored
Normal file
1
apps/gpgui-helper/dist/vite.svg
vendored
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
@ -9,29 +9,29 @@
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.13.0",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@mui/icons-material": "^5.16.7",
|
||||
"@mui/material": "^5.16.7",
|
||||
"@tauri-apps/api": "^1.6.0",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@mui/icons-material": "^6.2.0",
|
||||
"@mui/material": "^6.2.0",
|
||||
"@tauri-apps/api": "^2.1.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^1.6.0",
|
||||
"@types/node": "^20.14.15",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"eslint": "^8.57.0",
|
||||
"@tauri-apps/cli": "^2.1.0",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.18.0",
|
||||
"@typescript-eslint/parser": "^8.18.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint": "^9.16.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-react": "^7.35.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"prettier": "3.1.0",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^4.5.3"
|
||||
"eslint-plugin-react": "^7.37.2",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
"prettier": "3.4.2",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^6.0.3"
|
||||
},
|
||||
"packageManager": "pnpm@8.15.7"
|
||||
}
|
||||
|
2093
apps/gpgui-helper/pnpm-lock.yaml
generated
2093
apps/gpgui-helper/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,16 +1,18 @@
|
||||
[package]
|
||||
name = "gpgui-helper"
|
||||
rust-version.workspace = true
|
||||
authors.workspace = true
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1.5", features = [] }
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
gpapi = { path = "../../../crates/gpapi", features = ["tauri"] }
|
||||
tauri = { workspace = true, features = ["window-start-dragging"] }
|
||||
tauri.workspace = true
|
||||
|
||||
tokio.workspace = true
|
||||
anyhow.workspace = true
|
||||
log.workspace = true
|
||||
|
12
apps/gpgui-helper/src-tauri/capabilities/default.json
Normal file
12
apps/gpgui-helper/src-tauri/capabilities/default.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:window:allow-start-dragging",
|
||||
"core:event:allow-listen",
|
||||
"core:event:allow-emit",
|
||||
"core:event:allow-unlisten"
|
||||
]
|
||||
}
|
@ -1,8 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpapi::utils::window::WindowExt;
|
||||
use log::info;
|
||||
use tauri::Manager;
|
||||
use tauri::{Listener, Manager};
|
||||
|
||||
use crate::updater::{GuiUpdater, Installer, ProgressNotifier};
|
||||
|
||||
@ -25,15 +24,15 @@ impl App {
|
||||
|
||||
tauri::Builder::default()
|
||||
.setup(move |app| {
|
||||
let win = app.get_window("main").expect("no main window");
|
||||
win.hide_menu();
|
||||
let win = app.get_webview_window("main").expect("no main window");
|
||||
let _ = win.hide_menu();
|
||||
|
||||
let notifier = ProgressNotifier::new(win.clone());
|
||||
let installer = Installer::new(api_key);
|
||||
let updater = Arc::new(GuiUpdater::new(gui_version, notifier, installer));
|
||||
|
||||
let win_clone = win.clone();
|
||||
app.listen_global("app://update-done", move |_event| {
|
||||
app.listen_any("app://update-done", move |_event| {
|
||||
info!("Update done");
|
||||
let _ = win_clone.close();
|
||||
});
|
||||
@ -41,12 +40,15 @@ impl App {
|
||||
// Listen for the update event
|
||||
win.listen("app://update", move |_event| {
|
||||
let updater = Arc::clone(&updater);
|
||||
if updater.is_in_progress() {
|
||||
info!("Update already in progress");
|
||||
updater.notify_progress();
|
||||
return;
|
||||
}
|
||||
|
||||
tokio::spawn(async move { updater.update().await });
|
||||
});
|
||||
|
||||
// Update the GUI on startup
|
||||
win.trigger("app://update", None);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())?;
|
||||
|
@ -1,5 +1,5 @@
|
||||
use clap::Parser;
|
||||
use gpapi::utils::base64;
|
||||
use gpapi::utils::{base64, env_utils};
|
||||
use log::{info, LevelFilter};
|
||||
|
||||
use crate::app::App;
|
||||
@ -22,6 +22,8 @@ impl Cli {
|
||||
let api_key = self.read_api_key()?;
|
||||
let app = App::new(api_key, &self.gui_version);
|
||||
|
||||
env_utils::patch_gui_runtime_env(false);
|
||||
|
||||
app.run()
|
||||
}
|
||||
|
||||
|
@ -1,39 +1,39 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use gpapi::{
|
||||
service::request::UpdateGuiRequest,
|
||||
utils::{checksum::verify_checksum, crypto::Crypto, endpoint::http_endpoint},
|
||||
};
|
||||
use log::{info, warn};
|
||||
use tauri::{Manager, Window};
|
||||
use tauri::{Emitter, WebviewWindow};
|
||||
|
||||
use crate::downloader::{ChecksumFetcher, FileDownloader};
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
const SNAPSHOT: &str = match option_env!("SNAPSHOT") {
|
||||
Some(val) => val,
|
||||
None => "false"
|
||||
Some(val) => val,
|
||||
None => "false",
|
||||
};
|
||||
|
||||
pub struct ProgressNotifier {
|
||||
win: Window,
|
||||
win: WebviewWindow,
|
||||
}
|
||||
|
||||
impl ProgressNotifier {
|
||||
pub fn new(win: Window) -> Self {
|
||||
pub fn new(win: WebviewWindow) -> Self {
|
||||
Self { win }
|
||||
}
|
||||
|
||||
fn notify(&self, progress: Option<f64>) {
|
||||
let _ = self.win.emit_all("app://update-progress", progress);
|
||||
let _ = self.win.emit("app://update-progress", progress);
|
||||
}
|
||||
|
||||
fn notify_error(&self) {
|
||||
let _ = self.win.emit_all("app://update-error", ());
|
||||
let _ = self.win.emit("app://update-error", ());
|
||||
}
|
||||
|
||||
fn notify_done(&self) {
|
||||
let _ = self.win.emit_and_trigger("app://update-done", ());
|
||||
let _ = self.win.emit("app://update-done", ());
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,6 +72,8 @@ pub struct GuiUpdater {
|
||||
version: String,
|
||||
notifier: Arc<ProgressNotifier>,
|
||||
installer: Installer,
|
||||
in_progress: RwLock<bool>,
|
||||
progress: Arc<RwLock<Option<f64>>>,
|
||||
}
|
||||
|
||||
impl GuiUpdater {
|
||||
@ -80,6 +82,8 @@ impl GuiUpdater {
|
||||
version,
|
||||
notifier: Arc::new(notifier),
|
||||
installer,
|
||||
in_progress: Default::default(),
|
||||
progress: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -112,15 +116,23 @@ impl GuiUpdater {
|
||||
let cf = ChecksumFetcher::new(&checksum_url);
|
||||
let notifier = Arc::clone(&self.notifier);
|
||||
|
||||
dl.on_progress(move |progress| notifier.notify(progress));
|
||||
let progress_ref = Arc::clone(&self.progress);
|
||||
dl.on_progress(move |progress| {
|
||||
// Save progress to shared state so that it can be notified to the UI when needed
|
||||
if let Ok(mut guard) = progress_ref.try_write() {
|
||||
*guard = progress;
|
||||
}
|
||||
notifier.notify(progress);
|
||||
});
|
||||
|
||||
self.set_in_progress(true);
|
||||
let res = tokio::try_join!(dl.download(), cf.fetch());
|
||||
|
||||
let (file, checksum) = match res {
|
||||
Ok((file, checksum)) => (file, checksum),
|
||||
Err(err) => {
|
||||
warn!("Download error: {}", err);
|
||||
self.notifier.notify_error();
|
||||
self.notify_error();
|
||||
return;
|
||||
}
|
||||
};
|
||||
@ -130,7 +142,7 @@ impl GuiUpdater {
|
||||
|
||||
if let Err(err) = verify_checksum(&file_path, &checksum) {
|
||||
warn!("Checksum error: {}", err);
|
||||
self.notifier.notify_error();
|
||||
self.notify_error();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -138,10 +150,48 @@ impl GuiUpdater {
|
||||
|
||||
if let Err(err) = self.installer.install(&file_path, &checksum).await {
|
||||
warn!("Install error: {}", err);
|
||||
self.notifier.notify_error();
|
||||
self.notify_error();
|
||||
} else {
|
||||
info!("Install success");
|
||||
self.notifier.notify_done();
|
||||
self.notify_done();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_in_progress(&self) -> bool {
|
||||
if let Ok(guard) = self.in_progress.try_read() {
|
||||
*guard
|
||||
} else {
|
||||
info!("Failed to acquire in_progress lock");
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn set_in_progress(&self, in_progress: bool) {
|
||||
if let Ok(mut guard) = self.in_progress.try_write() {
|
||||
*guard = in_progress;
|
||||
} else {
|
||||
info!("Failed to acquire in_progress lock");
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_error(&self) {
|
||||
self.set_in_progress(false);
|
||||
self.notifier.notify_error();
|
||||
}
|
||||
|
||||
fn notify_done(&self) {
|
||||
self.set_in_progress(false);
|
||||
self.notifier.notify_done();
|
||||
}
|
||||
|
||||
pub fn notify_progress(&self) {
|
||||
let progress = if let Ok(guard) = self.progress.try_read() {
|
||||
*guard
|
||||
} else {
|
||||
info!("Failed to acquire progress lock");
|
||||
None
|
||||
};
|
||||
|
||||
self.notifier.notify(progress);
|
||||
}
|
||||
}
|
||||
|
@ -1,35 +1,15 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
"beforeBuildCommand": "pnpm build",
|
||||
"devPath": "http://localhost:1421",
|
||||
"distDir": "../dist",
|
||||
"withGlobalTauri": false
|
||||
"devUrl": "http://localhost:1421",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"package": {
|
||||
"productName": "gpgui-helper"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
"all": false,
|
||||
"window": {
|
||||
"all": false,
|
||||
"startDragging": true
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": false,
|
||||
"targets": "deb",
|
||||
"identifier": "com.yuezk.gpgui-helper",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
},
|
||||
"identifier": "com.yuezk.gpgui-helper",
|
||||
"productName": "gpgui-helper",
|
||||
"app": {
|
||||
"withGlobalTauri": false,
|
||||
"security": {
|
||||
"csp": null
|
||||
},
|
||||
@ -48,5 +28,16 @@
|
||||
"decorations": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"bundle": {
|
||||
"active": false,
|
||||
"targets": "deb",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { Box, Button, CssBaseline, LinearProgress, Typography } from "@mui/material";
|
||||
import { appWindow } from "@tauri-apps/api/window";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import logo from "../../assets/icon.svg";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import "./styles.css";
|
||||
|
||||
const appWindow = getCurrentWindow();
|
||||
|
||||
function useUpdateProgress() {
|
||||
const [progress, setProgress] = useState<number | null>(null);
|
||||
|
||||
@ -25,6 +27,8 @@ export default function App() {
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
appWindow.emit("app://update");
|
||||
|
||||
const unlisten = appWindow.listen("app://update-error", () => {
|
||||
setError(true);
|
||||
});
|
||||
|
@ -6,7 +6,7 @@ use clap::Parser;
|
||||
use gpapi::{
|
||||
process::gui_launcher::GuiLauncher,
|
||||
service::{request::WsRequest, vpn_state::VpnState},
|
||||
utils::{crypto::generate_key, env_file, lock_file::LockFile, redact::Redaction, shutdown_signal},
|
||||
utils::{crypto::generate_key, env_utils, lock_file::LockFile, redact::Redaction, shutdown_signal},
|
||||
GP_SERVICE_LOCK_FILE,
|
||||
};
|
||||
use log::{info, warn, LevelFilter};
|
||||
@ -63,7 +63,7 @@ impl Cli {
|
||||
if no_gui {
|
||||
info!("GUI is disabled");
|
||||
} else {
|
||||
let envs = self.env_file.as_ref().map(env_file::load_env_vars).transpose()?;
|
||||
let envs = self.env_file.as_ref().map(env_utils::load_env_vars).transpose()?;
|
||||
|
||||
let minimized = self.minimized;
|
||||
|
||||
|
@ -1,5 +1,9 @@
|
||||
# Changelog
|
||||
|
||||
## 2.3.9 - 2024-11-02
|
||||
|
||||
- Enhance the OpenSSL compatibility mode (fix [#437](https://github.com/yuezk/GlobalProtect-openconnect/issues/437))
|
||||
|
||||
## 2.3.8 - 2024-10-31
|
||||
|
||||
- GUI: support configure the external browser to use for authentication (fix [#423](https://github.com/yuezk/GlobalProtect-openconnect/issues/423))
|
||||
|
@ -1,5 +1,6 @@
|
||||
[package]
|
||||
name = "gpapi"
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license = "MIT"
|
||||
@ -14,8 +15,7 @@ openssl.workspace = true
|
||||
pem.workspace = true
|
||||
roxmltree.workspace = true
|
||||
serde.workspace = true
|
||||
specta.workspace = true
|
||||
specta-macros.workspace = true
|
||||
specta = { workspace = true, features = ["derive"] }
|
||||
urlencoding.workspace = true
|
||||
tokio.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
@ -1,11 +1,14 @@
|
||||
use std::borrow::{Borrow, Cow};
|
||||
|
||||
use anyhow::bail;
|
||||
use log::{info, warn};
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{error::AuthDataParseError, utils::base64::decode_to_string};
|
||||
|
||||
pub type AuthDataParseResult = anyhow::Result<SamlAuthData, AuthDataParseError>;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SamlAuthData {
|
||||
@ -33,33 +36,51 @@ impl SamlAuthResult {
|
||||
}
|
||||
|
||||
impl SamlAuthData {
|
||||
pub fn new(username: String, prelogin_cookie: Option<String>, portal_userauthcookie: Option<String>) -> Self {
|
||||
Self {
|
||||
username,
|
||||
prelogin_cookie,
|
||||
portal_userauthcookie,
|
||||
token: None,
|
||||
pub fn new(
|
||||
username: Option<String>,
|
||||
prelogin_cookie: Option<String>,
|
||||
portal_userauthcookie: Option<String>,
|
||||
) -> anyhow::Result<Self> {
|
||||
let username = username.unwrap_or_default();
|
||||
if username.is_empty() {
|
||||
bail!("Invalid username: <empty>");
|
||||
}
|
||||
|
||||
let prelogin_cookie = prelogin_cookie.unwrap_or_default();
|
||||
let portal_userauthcookie = portal_userauthcookie.unwrap_or_default();
|
||||
|
||||
if prelogin_cookie.len() <= 5 && portal_userauthcookie.len() <= 5 {
|
||||
bail!(
|
||||
"Invalid prelogin-cookie: {}, portal-userauthcookie: {}",
|
||||
prelogin_cookie,
|
||||
portal_userauthcookie
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
username,
|
||||
prelogin_cookie: Some(prelogin_cookie),
|
||||
portal_userauthcookie: Some(portal_userauthcookie),
|
||||
token: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_html(html: &str) -> anyhow::Result<SamlAuthData, AuthDataParseError> {
|
||||
pub fn from_html(html: &str) -> AuthDataParseResult {
|
||||
match parse_xml_tag(html, "saml-auth-status") {
|
||||
Some(saml_status) if saml_status == "1" => {
|
||||
Some(status) if 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) {
|
||||
Ok(SamlAuthData::new(
|
||||
username.unwrap(),
|
||||
prelogin_cookie,
|
||||
portal_userauthcookie,
|
||||
))
|
||||
} else {
|
||||
Err(AuthDataParseError::Invalid)
|
||||
}
|
||||
SamlAuthData::new(username, prelogin_cookie, portal_userauthcookie).map_err(|e| {
|
||||
warn!("Failed to parse auth data: {}", e);
|
||||
AuthDataParseError::Invalid
|
||||
})
|
||||
}
|
||||
Some(status) => {
|
||||
warn!("Found invalid auth status: {}", status);
|
||||
Err(AuthDataParseError::Invalid)
|
||||
}
|
||||
Some(_) => Err(AuthDataParseError::Invalid),
|
||||
None => Err(AuthDataParseError::NotFound),
|
||||
}
|
||||
}
|
||||
@ -105,27 +126,6 @@ impl SamlAuthData {
|
||||
pub fn token(&self) -> Option<&str> {
|
||||
self.token.as_deref()
|
||||
}
|
||||
|
||||
pub fn check(
|
||||
username: &Option<String>,
|
||||
prelogin_cookie: &Option<String>,
|
||||
portal_userauthcookie: &Option<String>,
|
||||
) -> bool {
|
||||
let username_valid = username.as_ref().is_some_and(|username| !username.is_empty());
|
||||
let prelogin_cookie_valid = prelogin_cookie.as_ref().is_some_and(|val| val.len() > 5);
|
||||
let portal_userauthcookie_valid = portal_userauthcookie.as_ref().is_some_and(|val| val.len() > 5);
|
||||
|
||||
let is_valid = username_valid && (prelogin_cookie_valid || portal_userauthcookie_valid);
|
||||
|
||||
if !is_valid {
|
||||
warn!(
|
||||
"Invalid SAML auth data: username: {:?}, prelogin-cookie: {:?}, portal-userauthcookie: {:?}",
|
||||
username, prelogin_cookie, portal_userauthcookie
|
||||
);
|
||||
}
|
||||
|
||||
is_valid
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_xml_tag(html: &str, tag: &str) -> Option<String> {
|
||||
|
@ -1 +1,28 @@
|
||||
use crate::error::PortalError;
|
||||
|
||||
pub mod args;
|
||||
|
||||
pub trait Args {
|
||||
fn fix_openssl(&self) -> bool;
|
||||
fn ignore_tls_errors(&self) -> bool;
|
||||
}
|
||||
|
||||
pub fn handle_error(err: anyhow::Error, args: &impl Args) {
|
||||
eprintln!("\nError: {}", err);
|
||||
|
||||
let Some(err) = err.downcast_ref::<PortalError>() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if err.is_legacy_openssl_error() && !args.fix_openssl() {
|
||||
eprintln!("\nRe-run it with the `--fix-openssl` option to work around this issue, e.g.:\n");
|
||||
let args = std::env::args().collect::<Vec<_>>();
|
||||
eprintln!("{} --fix-openssl {}\n", args[0], args[1..].join(" "));
|
||||
}
|
||||
|
||||
if err.is_tls_error() && !args.ignore_tls_errors() {
|
||||
eprintln!("\nRe-run it with the `--ignore-tls-errors` option to ignore the certificate error, e.g.:\n");
|
||||
let args = std::env::args().collect::<Vec<_>>();
|
||||
eprintln!("{} --ignore-tls-errors {}\n", args[0], args[1..].join(" "));
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,19 @@ pub enum PortalError {
|
||||
#[error("Portal config error: {0}")]
|
||||
ConfigError(String),
|
||||
#[error("Network error: {0}")]
|
||||
NetworkError(String),
|
||||
NetworkError(#[from] reqwest::Error),
|
||||
#[error("TLS error")]
|
||||
TlsError,
|
||||
}
|
||||
|
||||
impl PortalError {
|
||||
pub fn is_legacy_openssl_error(&self) -> bool {
|
||||
format!("{:?}", self).contains("unsafe legacy renegotiation")
|
||||
}
|
||||
|
||||
pub fn is_tls_error(&self) -> bool {
|
||||
matches!(self, PortalError::TlsError) || format!("{:?}", self).contains("certificate verify failed")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
@ -17,3 +29,9 @@ pub enum AuthDataParseError {
|
||||
#[error("Invalid auth data")]
|
||||
Invalid,
|
||||
}
|
||||
|
||||
impl AuthDataParseError {
|
||||
pub fn is_invalid(&self) -> bool {
|
||||
matches!(self, AuthDataParseError::Invalid)
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ pub async fn gateway_login(gateway: &str, cred: &Credential, gp_params: &GpParam
|
||||
.form(¶ms)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!(PortalError::NetworkError(e.to_string())))?;
|
||||
.map_err(|e| anyhow::anyhow!(PortalError::NetworkError(e)))?;
|
||||
|
||||
let res = parse_gp_response(res).await.map_err(|err| {
|
||||
warn!("{err}");
|
||||
|
@ -16,6 +16,7 @@ pub const GP_API_KEY: &[u8; 32] = &[0; 32];
|
||||
|
||||
pub const GP_USER_AGENT: &str = "PAN GlobalProtect";
|
||||
pub const GP_SERVICE_LOCK_FILE: &str = "/var/run/gpservice.lock";
|
||||
pub const GP_CALLBACK_PORT_FILENAME: &str = "gpcallback.port";
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
pub const GP_CLIENT_BINARY: &str = "/usr/bin/gpclient";
|
||||
|
@ -116,7 +116,7 @@ pub async fn retrieve_config(portal: &str, cred: &Credential, gp_params: &GpPara
|
||||
.form(¶ms)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!(PortalError::NetworkError(e.to_string())))?;
|
||||
.map_err(|e| anyhow::anyhow!(PortalError::NetworkError(e)))?;
|
||||
|
||||
let res_xml = parse_gp_response(res).await.or_else(|err| {
|
||||
if err.status == StatusCode::NOT_FOUND {
|
||||
|
@ -116,14 +116,12 @@ pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prel
|
||||
|
||||
let client = Client::try_from(gp_params)?;
|
||||
|
||||
info!("Perform prelogin, user_agent: {}", gp_params.user_agent());
|
||||
|
||||
let res = client
|
||||
.post(&prelogin_url)
|
||||
.form(¶ms)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!(PortalError::NetworkError(e.to_string())))?;
|
||||
.map_err(|e| anyhow::anyhow!(PortalError::NetworkError(e)))?;
|
||||
|
||||
let res_xml = parse_gp_response(res).await.or_else(|err| {
|
||||
if err.status == StatusCode::NOT_FOUND {
|
||||
|
@ -3,6 +3,7 @@ use std::env;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
||||
use log::info;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
pub fn persist_env_vars(extra: Option<HashMap<String, String>>) -> anyhow::Result<NamedTempFile> {
|
||||
@ -35,3 +36,20 @@ pub fn load_env_vars<T: AsRef<Path>>(env_file: T) -> anyhow::Result<HashMap<Stri
|
||||
|
||||
Ok(env_vars)
|
||||
}
|
||||
|
||||
pub fn patch_gui_runtime_env(hidpi: bool) {
|
||||
// This is to avoid blank screen on some systems
|
||||
std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1");
|
||||
|
||||
// Workaround for https://github.com/tauri-apps/tao/issues/929
|
||||
let desktop = env::var("XDG_CURRENT_DESKTOP").unwrap_or_default().to_lowercase();
|
||||
if desktop.contains("gnome") {
|
||||
env::set_var("GDK_BACKEND", "x11");
|
||||
}
|
||||
|
||||
if hidpi {
|
||||
info!("Setting GDK_SCALE=2 and GDK_DPI_SCALE=0.5");
|
||||
std::env::set_var("GDK_SCALE", "2");
|
||||
std::env::set_var("GDK_DPI_SCALE", "0.5");
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ pub mod base64;
|
||||
pub mod checksum;
|
||||
pub mod crypto;
|
||||
pub mod endpoint;
|
||||
pub mod env_file;
|
||||
pub mod env_utils;
|
||||
pub mod lock_file;
|
||||
pub mod openssl;
|
||||
pub mod redact;
|
||||
|
@ -2,25 +2,20 @@ use std::{process::ExitStatus, time::Duration};
|
||||
|
||||
use anyhow::bail;
|
||||
use log::info;
|
||||
use tauri::Window;
|
||||
use tauri::WebviewWindow;
|
||||
use tokio::process::Command;
|
||||
|
||||
pub trait WindowExt {
|
||||
fn raise(&self) -> anyhow::Result<()>;
|
||||
fn hide_menu(&self);
|
||||
}
|
||||
|
||||
impl WindowExt for Window {
|
||||
impl WindowExt for WebviewWindow {
|
||||
fn raise(&self) -> anyhow::Result<()> {
|
||||
raise_window(self)
|
||||
}
|
||||
|
||||
fn hide_menu(&self) {
|
||||
hide_menu(self);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn raise_window(win: &Window) -> anyhow::Result<()> {
|
||||
pub fn raise_window(win: &WebviewWindow) -> anyhow::Result<()> {
|
||||
let is_wayland = std::env::var("XDG_SESSION_TYPE").unwrap_or_default() == "wayland";
|
||||
|
||||
if is_wayland {
|
||||
@ -40,7 +35,7 @@ pub fn raise_window(win: &Window) -> anyhow::Result<()> {
|
||||
|
||||
// Calling window.show() on Windows will cause the menu to be shown.
|
||||
// We need to hide it again.
|
||||
hide_menu(win);
|
||||
win.hide_menu()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -76,22 +71,3 @@ async fn wmctrl_try_raise_window(title: &str) -> anyhow::Result<ExitStatus> {
|
||||
|
||||
Ok(exit_status)
|
||||
}
|
||||
|
||||
fn hide_menu(win: &Window) {
|
||||
let menu_handle = win.menu_handle();
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let menu_visible = menu_handle.is_visible().unwrap_or(false);
|
||||
|
||||
if !menu_visible {
|
||||
break;
|
||||
}
|
||||
|
||||
if menu_visible {
|
||||
let _ = menu_handle.hide();
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ Build-Depends: debhelper (>= 9),
|
||||
libsecret-1-0,
|
||||
libayatana-appindicator3-1,
|
||||
gnome-keyring,
|
||||
libwebkit2gtk-4.0-dev,
|
||||
libwebkit2gtk-4.1-dev,
|
||||
libopenconnect-dev (>= 8.20),@RUST@
|
||||
Homepage: https://github.com/yuezk/GlobalProtect-openconnect
|
||||
|
||||
|
@ -8,8 +8,8 @@ pkgdesc="A GUI for GlobalProtect VPN, based on OpenConnect, supports the SSO aut
|
||||
arch=('x86_64' 'aarch64')
|
||||
url="https://github.com/yuezk/GlobalProtect-openconnect"
|
||||
license=('GPL3')
|
||||
makedepends=('make' 'pkg-config' 'rust' 'cargo' 'jq' 'webkit2gtk' 'curl' 'wget' 'file' 'openssl' 'appmenu-gtk-module' 'gtk3' 'libappindicator-gtk3' 'librsvg' 'libvips' 'libayatana-appindicator' 'openconnect' 'libsecret')
|
||||
depends=('openconnect>=8.20' webkit2gtk libappindicator-gtk3 libayatana-appindicator libsecret libxml2)
|
||||
makedepends=('make' 'pkg-config' 'rust' 'cargo' 'jq' 'webkit2gtk-4.1' 'curl' 'wget' 'file' 'openssl' 'appmenu-gtk-module' 'libappindicator-gtk3' 'librsvg' 'openconnect' 'libsecret')
|
||||
depends=('openconnect>=8.20' webkit2gtk-4.1 libappindicator-gtk3 libsecret libxml2)
|
||||
optdepends=('wmctrl: for window management')
|
||||
|
||||
provides=('globalprotect-openconnect' 'gpclient' 'gpservice' 'gpauth' 'gpgui')
|
||||
|
@ -19,11 +19,11 @@ BuildRequires: wget
|
||||
BuildRequires: file
|
||||
BuildRequires: perl
|
||||
|
||||
BuildRequires: (webkit2gtk4.0-devel or webkit2gtk3-soup2-devel)
|
||||
BuildRequires: (webkit2gtk4.1-devel or webkit2gtk3-soup2-devel)
|
||||
BuildRequires: (libappindicator-gtk3-devel or libappindicator3-1)
|
||||
BuildRequires: (librsvg2-devel or librsvg-devel)
|
||||
|
||||
Requires: openconnect >= 8.20, (libayatana-appindicator or libappindicator-gtk3)
|
||||
Requires: openconnect >= 8.20, (libappindicator-gtk3 or libayatana-appindicator)
|
||||
Conflicts: globalprotect-openconnect-snapshot
|
||||
|
||||
%global debug_package %{nil}
|
||||
|
59
scripts/deb-install.sh
Executable file
59
scripts/deb-install.sh
Executable file
@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
# Usage: ./deb-install.sh <version>
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 <version>"
|
||||
echo "Example: $0 2.3.9"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [ $# -ne 1 ]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
VERSION=$1
|
||||
|
||||
# Check the architecture, only support x86_64 and aarch64/arm64
|
||||
ARCH=$(uname -m)
|
||||
|
||||
# Normalize the architecture name
|
||||
if [ "$ARCH" == "x86_64" ]; then
|
||||
ARCH="amd64"
|
||||
elif [ "$ARCH" == "aarch64" ] || [ "$ARCH" == "arm64" ]; then
|
||||
ARCH="arm64"
|
||||
else
|
||||
echo "Unsupported architecture: $ARCH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LIB_JAVASCRIPT_x86="http://launchpadlibrarian.net/704701345/libjavascriptcoregtk-4.0-18_2.43.3-1_amd64.deb"
|
||||
LIB_WEBKIT_x86="http://launchpadlibrarian.net/704701349/libwebkit2gtk-4.0-37_2.43.3-1_amd64.deb"
|
||||
|
||||
LIB_JAVASCRIPT_arm="http://launchpadlibrarian.net/704735771/libjavascriptcoregtk-4.0-18_2.43.3-1_arm64.deb"
|
||||
LIB_WEBKIT_arm="http://launchpadlibrarian.net/704735777/libwebkit2gtk-4.0-37_2.43.3-1_arm64.deb"
|
||||
|
||||
DEB_URL="https://github.com/yuezk/GlobalProtect-openconnect/releases/download/v${VERSION}/globalprotect-openconnect_${VERSION}-1_${ARCH}.deb"
|
||||
|
||||
# Install the dependencies
|
||||
if [ "$ARCH" == "amd64" ]; then
|
||||
wget -O /tmp/libjavascriptcoregtk.deb $LIB_JAVASCRIPT_x86
|
||||
wget -O /tmp/libwebkit2gtk.deb $LIB_WEBKIT_x86
|
||||
else
|
||||
wget -O /tmp/libjavascriptcoregtk.deb $LIB_JAVASCRIPT_arm
|
||||
wget -O /tmp/libwebkit2gtk.deb $LIB_WEBKIT_arm
|
||||
fi
|
||||
|
||||
sudo dpkg -i /tmp/libjavascriptcoregtk.deb /tmp/libwebkit2gtk.deb
|
||||
|
||||
# Install the package
|
||||
wget -O /tmp/globalprotect-openconnect.deb $DEB_URL
|
||||
sudo apt install --fix-broken -y /tmp/globalprotect-openconnect.deb
|
||||
|
||||
# Clean up
|
||||
rm /tmp/libjavascriptcoregtk.deb /tmp/libwebkit2gtk.deb /tmp/globalprotect-openconnect.deb
|
||||
|
||||
echo ""
|
||||
echo "GlobalProtect OpenConnect VPN client has been installed successfully."
|
Loading…
Reference in New Issue
Block a user