more refactor

This commit is contained in:
Kevin Yue 2023-11-13 10:05:06 +08:00
parent 0b4829a610
commit bf2d327687
20 changed files with 965 additions and 64 deletions

2
.gitignore vendored
View File

@ -33,3 +33,5 @@ target/
.ionide
# End of https://www.toptal.com/developers/gitignore/api/rust,visualstudiocode
*.profraw

View File

@ -9,6 +9,7 @@
"consts",
"devicename",
"distro",
"gpauth",
"gpclient",
"gpcommon",
"gpconf",
@ -27,6 +28,7 @@
"prelogonuserauthcookie",
"repr",
"reqwest",
"roxmltree",
"rustc",
"servercert",
"shlex",

271
Cargo.lock generated
View File

@ -148,6 +148,7 @@ version = "0.1.0"
dependencies = [
"aes-gcm",
"anyhow",
"gpauth",
"gpcommon",
"hex",
"keyring",
@ -167,6 +168,16 @@ dependencies = [
"whoami",
]
[[package]]
name = "assert-json-diff"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "async-broadcast"
version = "0.5.1"
@ -319,22 +330,6 @@ version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3"
[[package]]
name = "attohttpc"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fcf00bc6d5abb29b5f97e3c61a90b6d3caa12f3faf897d4a3e3607c050a35a7"
dependencies = [
"flate2",
"http",
"log",
"native-tls",
"serde",
"serde_json",
"serde_urlencoded",
"url",
]
[[package]]
name = "atty"
version = "0.2.14"
@ -486,6 +481,9 @@ name = "bytes"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
dependencies = [
"serde",
]
[[package]]
name = "cairo-rs"
@ -681,6 +679,17 @@ dependencies = [
"winapi",
]
[[package]]
name = "colored"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6"
dependencies = [
"is-terminal",
"lazy_static",
"windows-sys 0.48.0",
]
[[package]]
name = "combine"
version = "4.6.6"
@ -935,6 +944,15 @@ dependencies = [
"subtle",
]
[[package]]
name = "directories"
version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-next"
version = "2.0.0"
@ -945,6 +963,18 @@ dependencies = [
"dirs-sys-next",
]
[[package]]
name = "dirs-sys"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.48.0",
]
[[package]]
name = "dirs-sys-next"
version = "0.1.2"
@ -1032,6 +1062,12 @@ dependencies = [
"syn 2.0.16",
]
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.1"
@ -1080,7 +1116,7 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9f0c14694cbd524c8720dd69b0e3179344f04ebb5f90f2e4a440c6ea3b2f1ee"
dependencies = [
"colored",
"colored 1.9.3",
"log",
]
@ -1156,6 +1192,21 @@ dependencies = [
"new_debug_unreachable",
]
[[package]]
name = "futures"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.26"
@ -1163,6 +1214,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
@ -1232,6 +1284,7 @@ version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
@ -1498,13 +1551,35 @@ dependencies = [
"system-deps 6.0.3",
]
[[package]]
name = "gpauth"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"directories",
"fern",
"gpcommon",
"humantime",
"log",
"regex",
"reqwest",
"serde",
"serde_json",
"tokio",
"webkit2gtk",
"wry",
]
[[package]]
name = "gpclient"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"gpauth",
"gpcommon",
"libc",
"reqwest",
"tokio",
"url",
@ -1516,6 +1591,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"base64 0.21.0",
"bytes",
"cc",
"configparser",
@ -1523,7 +1599,10 @@ dependencies = [
"is_executable",
"lexopt",
"log",
"mockito",
"reqwest",
"ring",
"roxmltree",
"serde",
"serde_json",
"shlex",
@ -1611,7 +1690,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http",
"indexmap",
"indexmap 1.9.2",
"slab",
"tokio",
"tokio-util",
@ -1624,6 +1703,12 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
[[package]]
name = "heck"
version = "0.3.3"
@ -1871,7 +1956,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399"
dependencies = [
"autocfg",
"hashbrown",
"hashbrown 0.12.3",
"serde",
]
[[package]]
name = "indexmap"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
dependencies = [
"equivalent",
"hashbrown 0.14.0",
"serde",
]
@ -1919,6 +2015,17 @@ version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6"
[[package]]
name = "is-terminal"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
dependencies = [
"hermit-abi 0.3.1",
"rustix 0.38.8",
"windows-sys 0.48.0",
]
[[package]]
name = "is_executable"
version = "1.0.1"
@ -2209,6 +2316,26 @@ dependencies = [
"windows-sys 0.42.0",
]
[[package]]
name = "mockito"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09c762b6267c4593555bb38f1df19e9318985bc4de60b5e8462890856a9a5b4c"
dependencies = [
"assert-json-diff",
"colored 2.0.4",
"futures",
"hyper",
"lazy_static",
"log",
"rand 0.8.5",
"regex",
"serde_json",
"serde_urlencoded",
"similar",
"tokio",
]
[[package]]
name = "native-tls"
version = "0.2.11"
@ -2501,6 +2628,12 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "ordered-stream"
version = "0.2.0"
@ -2727,7 +2860,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9469799ca90293a376f68f6fcb8f11990d9cff55602cfba0ba83893c973a7f46"
dependencies = [
"base64 0.21.0",
"indexmap",
"indexmap 1.9.2",
"line-wrap",
"quick-xml",
"serde",
@ -3027,10 +3160,12 @@ dependencies = [
"serde_urlencoded",
"tokio",
"tokio-native-tls",
"tokio-util",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-streams",
"web-sys",
"winreg 0.50.0",
]
@ -3050,6 +3185,15 @@ dependencies = [
"winapi",
]
[[package]]
name = "roxmltree"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8f595a457b6b8c6cda66a48503e92ee8d19342f905948f29c383200ec9eb1d8"
dependencies = [
"xmlparser",
]
[[package]]
name = "rustc_version"
version = "0.3.3"
@ -3297,14 +3441,15 @@ dependencies = [
[[package]]
name = "serde_with"
version = "2.3.3"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07ff71d2c147a7b57362cead5e22f772cd52f6ab31cfcd9edcd7f6aeb2a0afbe"
checksum = "1ca3b16a3d82c4088f343b7480a93550b3eabe1a358569c2dfe38bbcead07237"
dependencies = [
"base64 0.13.1",
"base64 0.21.0",
"chrono",
"hex",
"indexmap",
"indexmap 1.9.2",
"indexmap 2.0.0",
"serde",
"serde_json",
"serde_with_macros",
@ -3313,9 +3458,9 @@ dependencies = [
[[package]]
name = "serde_with_macros"
version = "2.3.3"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "881b6f881b17d13214e5d494c939ebab463d01264ce1811e9d4ac3a882e7695f"
checksum = "2e6be15c453eb305019bfa438b1593c731f36a289a7853f7707ee29e870b3b3c"
dependencies = [
"darling",
"proc-macro2",
@ -3411,6 +3556,12 @@ dependencies = [
"libc",
]
[[package]]
name = "similar"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf"
[[package]]
name = "siphasher"
version = "0.3.10"
@ -3557,6 +3708,19 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "sys-locale"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8a11bd9c338fdba09f7881ab41551932ad42e405f61d01e8406baea71c07aee"
dependencies = [
"js-sys",
"libc",
"wasm-bindgen",
"web-sys",
"windows-sys 0.45.0",
]
[[package]]
name = "system-deps"
version = "5.0.0"
@ -3654,12 +3818,12 @@ dependencies = [
[[package]]
name = "tauri"
version = "1.3.0"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d42ba3a2e8556722f31336a0750c10dbb6a81396a1c452977f515da83f69f842"
checksum = "7fbe522898e35407a8e60dc3870f7579fea2fc262a6a6072eccdd37ae1e1d91e"
dependencies = [
"anyhow",
"attohttpc",
"bytes",
"cocoa",
"data-url",
"dirs-next",
@ -3681,12 +3845,14 @@ dependencies = [
"rand 0.8.5",
"raw-window-handle",
"regex",
"reqwest",
"semver 1.0.16",
"serde",
"serde_json",
"serde_repr",
"serialize-to-javascript",
"state",
"sys-locale",
"tar",
"tauri-macros",
"tauri-runtime",
@ -3704,9 +3870,9 @@ dependencies = [
[[package]]
name = "tauri-build"
version = "1.3.0"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "929b3bd1248afc07b63e33a6a53c3f82c32d0b0a5e216e4530e94c467e019389"
checksum = "7d2edd6a259b5591c8efdeb9d5702cb53515b82a6affebd55c7fd6d3a27b7d1b"
dependencies = [
"anyhow",
"cargo_toml",
@ -3717,14 +3883,13 @@ dependencies = [
"serde_json",
"tauri-utils",
"tauri-winres",
"winnow",
]
[[package]]
name = "tauri-codegen"
version = "1.3.0"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5a2105f807c6f50b2fa2ce5abd62ef207bc6f14c9fcc6b8caec437f6fb13bde"
checksum = "54ad2d49fdeab4a08717f5b49a163bdc72efc3b1950b6758245fcde79b645e1a"
dependencies = [
"base64 0.21.0",
"brotli",
@ -3748,9 +3913,9 @@ dependencies = [
[[package]]
name = "tauri-macros"
version = "1.3.0"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8784cfe6f5444097e93c69107d1ac5e8f13d02850efa8d8f2a40fe79674cef46"
checksum = "8eb12a2454e747896929338d93b0642144bb51e0dddbb36e579035731f0d76b7"
dependencies = [
"heck 0.4.1",
"proc-macro2",
@ -3789,9 +3954,9 @@ dependencies = [
[[package]]
name = "tauri-runtime"
version = "0.13.0"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3b80ea3fcd5fefb60739a3b577b277e8fc30434538a2f5bba82ad7d4368c422"
checksum = "108683199cb18f96d2d4134187bb789964143c845d2d154848dda209191fd769"
dependencies = [
"gtk",
"http",
@ -3810,9 +3975,9 @@ dependencies = [
[[package]]
name = "tauri-runtime-wry"
version = "0.13.0"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1c396950b1ba06aee1b4ffe6c7cd305ff433ca0e30acbc5fa1a2f92a4ce70f1"
checksum = "0b7aa256a1407a3a091b5d843eccc1a5042289baf0a43d1179d9f0fcfea37c1b"
dependencies = [
"cocoa",
"gtk",
@ -3830,12 +3995,13 @@ dependencies = [
[[package]]
name = "tauri-utils"
version = "1.3.0"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6f9c2dafef5cbcf52926af57ce9561bd33bb41d7394f8bb849c0330260d864"
checksum = "03fc02bb6072bb397e1d473c6f76c953cda48b4a2d0cce605df284aa74a12e84"
dependencies = [
"brotli",
"ctor",
"dunce",
"glob",
"heck 0.4.1",
"html5ever",
@ -4044,7 +4210,7 @@ version = "0.19.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13"
dependencies = [
"indexmap",
"indexmap 1.9.2",
"serde",
"serde_spanned",
"toml_datetime",
@ -4423,6 +4589,19 @@ version = "0.2.84"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
[[package]]
name = "wasm-streams"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4609d447824375f43e1ffbc051b50ad8f4b3ae8219680c94452ea05eb240ac7"
dependencies = [
"futures-util",
"js-sys",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "web-sys"
version = "0.3.61"
@ -4907,6 +5086,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "xmlparser"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d25c75bf9ea12c4040a97f829154768bbbce366287e2dc044af160cd79a13fd"
[[package]]
name = "zbus"
version = "3.14.1"

View File

@ -12,16 +12,22 @@ opt-level = "z"
[workspace.dependencies]
anyhow = "1.0"
async-trait = "0.1"
base64 = "0.21"
bytes = "1.0"
clap = "4.4.2"
configparser = "3.0"
data-encoding = "2.3"
directories="5"
fern = "0.6"
humantime = "2.1"
is_executable = "1.0"
lexopt = "0.3.0"
log = "0.4"
mockito = "1.1"
regex = "1"
reqwest = "0.11"
ring = "0.16"
roxmltree = "0.18"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
shlex = "1.0"

22
gpauth/Cargo.toml Normal file
View File

@ -0,0 +1,22 @@
[package]
name = "gpauth"
version.workspace = true
edition = "2021"
[dependencies]
gpcommon = { path = "../gpcommon" }
anyhow.workspace = true
clap.workspace = true
directories.workspace = true
fern.workspace = true
humantime.workspace = true
log.workspace = true
regex.workspace = true
reqwest.workspace = true
serde_json.workspace = true
serde.workspace = true
tokio.workspace = true
webkit2gtk = "0.18.2"
wry = "0.24"
[dev-dependencies]

395
gpauth/src/auth_window.rs Normal file
View File

@ -0,0 +1,395 @@
use directories::ProjectDirs;
use gpcommon::portal::{Portal, Prelogin, SamlPrelogin};
use log::{info, warn};
use regex::Regex;
use serde::Serialize;
use std::{borrow::Cow, cell::RefCell, path::PathBuf, rc::Rc};
use tokio::sync::mpsc;
use webkit2gtk::{
gio::Cancellable, glib::GString, LoadEvent, URIResponse, URIResponseExt, WebResource,
WebResourceExt, WebViewExt,
};
use wry::{
application::{
event::{Event, StartCause, WindowEvent},
event_loop::{ControlFlow, EventLoop, EventLoopProxy},
platform::run_return::EventLoopExtRunReturn,
window::WindowBuilder,
},
webview::{WebContext, WebView, WebViewBuilder, WebviewExtUnix},
};
enum UserEvent {
AuthFailed,
AuthSuccess(SamlAuthData),
AuthRequest(String),
Exit,
}
enum AuthEvent {
FetchAuthRequest,
Exit,
}
#[derive(Debug, Serialize)]
pub struct SamlAuthData {
username: String,
prelogin_cookie: Option<String>,
portal_userauthcookie: Option<String>,
}
impl SamlAuthData {
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);
username_valid && (prelogin_cookie_valid || portal_userauthcookie_valid)
}
}
enum AuthResult {
NotFound,
Invalid,
Success(SamlAuthData),
}
impl AuthResult {
fn new(
username: Option<String>,
prelogin_cookie: Option<String>,
portal_userauthcookie: Option<String>,
) -> Self {
if SamlAuthData::check(&username, &prelogin_cookie, &portal_userauthcookie) {
AuthResult::Success(SamlAuthData {
username: username.unwrap(),
prelogin_cookie,
portal_userauthcookie,
})
} else {
AuthResult::Invalid
}
}
}
pub(crate) struct AuthWindow {
portal: Portal,
event_loop: EventLoop<UserEvent>,
webview: WebView,
}
impl AuthWindow {
pub fn new(portal: Portal, user_agent: &str) -> anyhow::Result<Self> {
let event_loop = EventLoop::with_user_event();
let window = WindowBuilder::new()
.with_title("GlobalProtect Login")
.build(&event_loop)?;
let mut web_context = WebContext::new(data_dir());
let webview = WebViewBuilder::new(window)?
.with_user_agent(user_agent)
.with_web_context(&mut web_context)
.build()?;
Ok(Self {
portal,
event_loop,
webview,
})
}
pub async fn run(&mut self, saml_request: Option<&str>) -> anyhow::Result<SamlAuthData> {
let saml_request = match saml_request {
Some(saml_request) => Cow::Borrowed(saml_request),
None => Cow::Owned(Self::saml_request(&self.portal).await?),
};
self.setup_webview(&saml_request);
let (sender, receiver) = mpsc::unbounded_channel::<AuthEvent>();
self.setup_channel(receiver);
let auth_data_ret: Rc<RefCell<Option<SamlAuthData>>> = Default::default();
let wv = self.webview.webview();
self.event_loop.run_return(|event, _, control_flow| {
*control_flow = ControlFlow::Wait;
match event {
Event::NewEvents(StartCause::Init) => info!("Auth window is ready"),
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => {
info!("Close requested, exiting the auth window...");
Self::send_auth_event(&sender, AuthEvent::Exit);
*control_flow = ControlFlow::Exit;
}
Event::UserEvent(UserEvent::AuthRequest(saml_request)) => {
Self::load_saml_request(wv.clone(), &saml_request);
}
Event::UserEvent(UserEvent::AuthFailed) => {
info!("Auth failed, retrying...");
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"),
);
Self::send_auth_event(&sender, AuthEvent::FetchAuthRequest);
}
Event::UserEvent(UserEvent::AuthSuccess(auth_data)) => {
info!("Auth success, exit the auth window...");
*auth_data_ret.borrow_mut() = Some(auth_data);
Self::send_auth_event(&sender, AuthEvent::Exit);
*control_flow = ControlFlow::Exit;
}
Event::UserEvent(UserEvent::Exit) => {
info!("Exit event received, exiting the auth window...");
Self::send_auth_event(&sender, AuthEvent::Exit);
*control_flow = ControlFlow::Exit;
}
_ => (),
}
});
auth_data_ret
.take()
.ok_or_else(|| anyhow::anyhow!("Auth window exited without auth data"))
}
fn send_user_event(event_proxy: &EventLoopProxy<UserEvent>, event: UserEvent) {
if let Err(err) = event_proxy.send_event(event) {
warn!("Failed to send user event: {}", err);
}
}
fn send_auth_event(sender: &mpsc::UnboundedSender<AuthEvent>, event: AuthEvent) {
if let Err(err) = sender.send(event) {
warn!("Failed to send auth event: {}", err);
}
}
async fn saml_request(portal: &Portal) -> anyhow::Result<String> {
let prelogin = portal.prelogin().await?;
if let Prelogin::Saml(SamlPrelogin {
method, request, ..
}) = prelogin
{
info!("Received SAML prelogin response, method: {}", method);
Ok(request)
} else {
Err(anyhow::anyhow!("Received non-SAML prelogin response"))
}
}
fn load_saml_request(wv: Rc<webkit2gtk::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 setup_webview(&self, saml_request: &str) {
let wv = self.webview.webview();
let event_proxy = self.event_loop.create_proxy();
Self::load_saml_request(wv.clone(), saml_request);
wv.connect_load_changed(move |wv, event| {
if event != LoadEvent::Finished {
return;
}
let uri = wv.uri().unwrap_or("".into());
if uri.is_empty() {
warn!("Loaded an empty URI, auth failed");
Self::send_user_event(&event_proxy, UserEvent::AuthFailed);
return;
}
if uri == "about:blank" {
info!("Loaded about:blank, skipping");
return;
}
info!("Loaded URI: {}", uri);
if let Some(main_resource) = wv.main_resource() {
Self::read_auth_data(&main_resource, event_proxy.clone());
} else {
warn!("No main resource found for {}, skipping", uri);
}
});
let event_proxy = self.event_loop.create_proxy();
wv.connect_load_failed(move |_wv, _event, uri, err| {
warn!("Load failed: {:?}, {:?}", uri, err);
Self::send_user_event(&event_proxy, UserEvent::AuthFailed);
false
});
}
fn setup_channel(&self, mut receiver: mpsc::UnboundedReceiver<AuthEvent>) {
let portal = self.portal.clone();
let event_proxy = self.event_loop.create_proxy();
tokio::spawn(async move {
loop {
match receiver.recv().await {
Some(AuthEvent::FetchAuthRequest) => {
info!("Fetching auth request...");
match Self::saml_request(&portal).await {
Ok(request) => {
Self::send_user_event(&event_proxy, UserEvent::AuthRequest(request))
}
Err(err) => {
warn!("Failed to fetch prelogin response: {}", err);
Self::send_user_event(&event_proxy, UserEvent::Exit);
}
}
}
Some(AuthEvent::Exit) => {
info!("Auth window exited, exiting the receiver...");
break;
}
None => {
info!("Auth event channel closed, exiting the receiver...");
break;
}
}
}
});
}
fn read_auth_data(main_resource: &WebResource, event_proxy: EventLoopProxy<UserEvent>) {
if main_resource.response().is_none() {
info!("No response found for main resource");
return;
}
let response = main_resource.response().unwrap();
info!("Trying to read auth data from response headers...");
match read_auth_data_from_headers(&response) {
AuthResult::Success(auth_data) => {
Self::send_user_event(&event_proxy, UserEvent::AuthSuccess(auth_data));
}
AuthResult::NotFound => {
info!("No auth data found in response headers, trying to read from HTML...");
read_auth_data_from_body(main_resource, move |auth_result| match auth_result {
AuthResult::Success(auth_data) => {
Self::send_user_event(&event_proxy, UserEvent::AuthSuccess(auth_data));
}
AuthResult::Invalid => {
Self::send_user_event(&event_proxy, UserEvent::AuthFailed);
}
AuthResult::NotFound => {
info!("No auth data found in HTML, it may not be the '/SAML20/SP/ACS' endpoint");
}
});
}
AuthResult::Invalid => {
info!("Found invalid auth data in response headers, trying to read from HTML...");
read_auth_data_from_body(main_resource, move |auth_result| {
if let AuthResult::Success(auth_data) = auth_result {
Self::send_user_event(&event_proxy, UserEvent::AuthSuccess(auth_data));
} else {
Self::send_user_event(&event_proxy, UserEvent::AuthFailed);
}
});
}
}
}
}
fn data_dir() -> Option<PathBuf> {
ProjectDirs::from("com.yuezk", "GlobalProtect-openconnect", "gpauth")
.map(|dirs| dirs.data_dir().into())
}
fn read_auth_data_from_headers(response: &URIResponse) -> AuthResult {
response
.http_headers()
.map_or(AuthResult::NotFound, |mut headers| {
match headers.get("saml-auth-status") {
Some(saml_status) if saml_status == "1" => {
info!("Found valid SAML status in header");
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);
AuthResult::new(username, prelogin_cookie, portal_userauthcookie)
}
Some(saml_status) => {
info!("Found invalid SAML status in header: {}", saml_status);
AuthResult::Invalid
}
None => {
info!("No auth data found in response headers");
AuthResult::NotFound
}
}
})
}
fn read_auth_data_from_body<F>(main_resource: &WebResource, callback: F)
where
F: FnOnce(AuthResult) + Send + 'static,
{
main_resource.data(Cancellable::NONE, |data| {
let auth_result = data.map_or(AuthResult::NotFound, |data| {
let html = String::from_utf8_lossy(&data).to_string();
read_auth_data_from_html(&html)
});
callback(auth_result);
});
}
fn read_auth_data_from_html(html: &str) -> AuthResult {
if html.contains("Temporarily Unavailable") {
info!("Found 'Temporarily Unavailable' in HTML, auth failed");
return AuthResult::Invalid;
}
match parse_xml_tag(html, "saml-auth-status") {
Some(saml_status) if saml_status == "1" => {
info!("Found valid status in HTML");
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");
AuthResult::new(username, prelogin_cookie, portal_userauthcookie)
}
Some(saml_status) => {
info!("Found invalid SAML status in HTML: {}", saml_status);
AuthResult::Invalid
}
None => {
info!("No auth data found in HTML");
AuthResult::NotFound
}
}
}
fn parse_xml_tag(html: &str, tag: &str) -> Option<String> {
let re = Regex::new(&format!("<{}>(.*)</{}>", tag, tag)).unwrap();
re.captures(html)
.and_then(|captures| captures.get(1))
.map(|m| m.as_str().to_string())
}

6
gpauth/src/lib.rs Normal file
View File

@ -0,0 +1,6 @@
mod auth_window;
mod saml_auth;
mod standard_auth;
pub use saml_auth::saml_auth;
pub use standard_auth::standard_auth;

48
gpauth/src/main.rs Normal file
View File

@ -0,0 +1,48 @@
use clap::{arg, Command};
use gpauth::saml_auth;
use gpcommon::portal::{Portal, Prelogin, SamlPrelogin};
use serde_json::json;
fn cli() -> Command {
Command::new("gpauth")
.about("GlobalProtect-openconnect authentication helper")
.arg_required_else_help(true)
.arg(arg!(<SERVER> "The GlobalProtect server"))
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
fern::Dispatch::new()
.format(|out, message, record| {
out.finish(format_args!(
"[{} {} {}] {}",
humantime::format_rfc3339_millis(std::time::SystemTime::now()),
record.level(),
record.target(),
message
))
})
.level(log::LevelFilter::Info)
.chain(std::io::stderr())
.apply()?;
let matches = cli().get_matches();
let server = matches.get_one::<String>("SERVER").expect("Missing server");
let address = if server.starts_with("https://") || server.starts_with("http://") {
server.to_string()
} else {
format!("https://{}", server)
};
let portal = Portal::new(&address);
let saml_request = match portal.prelogin().await? {
Prelogin::Saml(SamlPrelogin { request, .. }) => request,
_ => anyhow::bail!("Prelogin response is not SAML"),
};
let auth_data = saml_auth(&portal, Some(&saml_request)).await?;
// Output the auth data as JSON, so that the client can parse it
println!("{}", json!(auth_data));
Ok(())
}

9
gpauth/src/saml_auth.rs Normal file
View File

@ -0,0 +1,9 @@
use crate::auth_window::{SamlAuthData, AuthWindow};
use gpcommon::portal::Portal;
pub async fn saml_auth(portal: &Portal, saml_request: Option<&str>) -> anyhow::Result<SamlAuthData> {
let user_agent = "PAN GlobalProtect";
let mut auth_window = AuthWindow::new(portal.clone(), user_agent)?;
auth_window.run(saml_request).await
}

View File

@ -0,0 +1,3 @@
pub fn standard_auth(server: &str) {
}

View File

@ -7,8 +7,10 @@ edition = "2021"
[dependencies]
gpcommon = { path = "../gpcommon" }
gpauth = { path = "../gpauth" }
anyhow.workspace = true
clap.workspace = true
reqwest.workspace = true
tokio.workspace = true
url.workspace = true
libc = "0.2"

View File

@ -1,11 +1,8 @@
use clap::{arg, Command, Parser};
use gpcommon::{Client, SOCKET_PATH};
use portal::Portal;
use tokio::{io::AsyncReadExt, net::UnixStream, sync::mpsc};
use url::Url;
mod portal;
fn cli() -> Command {
Command::new("gpclient")
.about("GlobalProtect-openconnect CLI client")
@ -55,7 +52,8 @@ async fn main() {
let url = Url::parse(&server).expect("Invalid server URL");
let host = url.host_str().expect("Invalid server URL");
let portal = Portal::new(host);
// let portal = Portal::new(host);
// let prelogin = portal.prelogin().await;
}
Some(("disconnect", _)) => {
println!("Disconnecting...");

View File

@ -4,11 +4,14 @@ pub(crate) struct Portal<'a> {
address: &'a str,
}
struct SamlPrelogin {}
#[derive(Debug)]
pub(crate) struct SamlPrelogin {}
struct StandardPrelogin {}
#[derive(Debug)]
pub(crate) struct StandardPrelogin {}
enum Prelogin {
#[derive(Debug)]
pub(crate) enum Prelogin {
Saml(SamlPrelogin),
Standard(StandardPrelogin),
}
@ -19,9 +22,10 @@ impl<'a> Portal<'a> {
}
// Preform the Portal prelogin
async fn prelogin(&self) -> Result<Prelogin> {
pub async fn prelogin(&self) -> Result<Prelogin> {
let prelogin_url = format!("https://{}/global-protect/prelogin.esp", self.address);
let res_xml = reqwest::get(prelogin_url).await?.text().await?;
let res_xml = reqwest::get(&prelogin_url).await?.text().await?;
println!("{}", res_xml);
Ok(Prelogin::Standard(StandardPrelogin {}))
@ -38,9 +42,11 @@ mod tests {
assert_eq!(portal.address, "vpn.example.com");
}
#[test]
fn test_prelogin() {
let portal = Portal::new("vpn.example.com");
let prelogin = portal.prelogin();
#[tokio::test]
async fn test_prelogin() {
let portal = Portal::new("vpn.microstrategy.com");
let prelogin = portal.prelogin().await;
assert!(prelogin.is_ok());
}
}

View File

@ -3,18 +3,19 @@ name = "gpcommon"
version.workspace = true
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow.workspace = true
async-trait.workspace = true
base64.workspace = true
bytes.workspace = true
configparser.workspace = true
data-encoding.workspace = true
is_executable.workspace = true
lexopt.workspace = true
log.workspace = true
reqwest.workspace = true
ring.workspace = true
roxmltree.workspace = true
serde_json.workspace = true
serde.workspace = true
shlex.workspace = true
@ -23,5 +24,8 @@ thiserror.workspace = true
tokio-util.workspace = true
tokio.workspace = true
[dev-dependencies]
mockito.workspace = true
[build-dependencies]
cc = "1.0"

View File

@ -17,6 +17,7 @@ mod response;
pub mod server;
mod vpn;
mod writer;
pub mod portal;
pub(crate) use request::Request;
pub(crate) use request::RequestPool;

201
gpcommon/src/portal.rs Normal file
View File

@ -0,0 +1,201 @@
use anyhow::bail;
use base64::{engine::general_purpose, Engine};
use roxmltree::Document;
#[derive(Debug, Clone)]
pub struct Portal {
address: String,
}
pub enum PortalCredential {
Standard(String, String),
Prelogin(String),
Cached(String, String),
}
#[derive(Debug)]
pub struct SamlPrelogin {
pub region: String,
pub method: String,
pub request: String,
}
#[derive(Debug)]
pub struct StandardPrelogin {
pub region: String,
pub label_username: String,
pub label_password: String,
pub auth_message: String,
}
#[derive(Debug)]
pub enum Prelogin {
Saml(SamlPrelogin),
Standard(StandardPrelogin),
}
impl Portal {
pub fn new(address: &str) -> Self {
Self {
address: address.to_string(),
}
}
pub async fn prelogin(&self) -> anyhow::Result<Prelogin> {
let prelogin_url = format!("{}/global-protect/prelogin.esp", self.address);
let client = reqwest::Client::builder()
.user_agent("PAN GlobalProtect")
.build()?;
let res_xml = client.get(&prelogin_url).send().await?.text().await?;
let doc = Document::parse(&res_xml)?;
let status = get_child_text(&doc, "status")
.ok_or_else(|| anyhow::anyhow!("Prelogin response does not contain status element"))?;
// Check the status of the prelogin response
if status.to_uppercase() != "SUCCESS" {
let msg = get_child_text(&doc, "msg").unwrap_or(String::from("Unknown error"));
bail!("Prelogin failed: {}", msg)
}
let region = get_child_text(&doc, "region")
.ok_or_else(|| anyhow::anyhow!("Prelogin response does not contain region element"))?;
let saml_method = get_child_text(&doc, "saml-auth-method");
let saml_request = get_child_text(&doc, "saml-request");
// Check if the prelogin response is SAML
if saml_method.is_some() && saml_request.is_some() {
return Ok(Prelogin::Saml(SamlPrelogin {
region,
method: saml_method.unwrap(),
request: base64_decode(&saml_request.unwrap())?,
}));
}
let label_username = get_child_text(&doc, "username-label");
let label_password = get_child_text(&doc, "password-label");
// Check if the prelogin response is standard login
if label_username.is_some() && label_password.is_some() {
let auth_message = get_child_text(&doc, "authentication-message")
.unwrap_or(String::from("Please enter the login credentials"));
return Ok(Prelogin::Standard(StandardPrelogin {
region,
auth_message,
label_username: label_username.unwrap(),
label_password: label_password.unwrap(),
}));
}
bail!("Unknown prelogin response");
}
pub fn retrieve_config(&self, credential: &PortalCredential) {
todo!()
}
}
fn get_child_text(doc: &Document, name: &str) -> Option<String> {
let node = doc.descendants().find(|n| n.has_tag_name(name))?;
node.text().map(|s| s.to_string())
}
fn base64_decode(s: &str) -> anyhow::Result<String> {
let engine = general_purpose::STANDARD;
let decoded = engine.decode(s)?;
Ok(String::from_utf8(decoded)?)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new() {
let portal = Portal::new("vpn.example.com");
assert_eq!(portal.address, "vpn.example.com");
}
#[tokio::test]
async fn test_prelogin_saml() {
let mut s = mockito::Server::new();
let mock = s
.mock("GET", "/global-protect/prelogin.esp")
.with_body(
r#"<?xml version="1.0" encoding="UTF-8" ?>
<prelogin-response>
<status>Success</status>
<ccusername></ccusername>
<autosubmit>false</autosubmit>
<msg></msg>
<newmsg></newmsg>
<authentication-message>Enter login credentials</authentication-message>
<username-label>Username</username-label>
<password-label>Password</password-label>
<panos-version>1</panos-version>
<saml-default-browser>yes</saml-default-browser>
<cas-auth></cas-auth>
<saml-auth-status>0</saml-auth-status>
<saml-auth-method>REDIRECT</saml-auth-method>
<saml-request-timeout>600</saml-request-timeout>
<saml-request-id>0</saml-request-id>
<saml-request>U0FNTFJlcXVlc3Q9eHh4</saml-request>
<auth-api>no</auth-api><region>CN</region>
</prelogin-response>"#,
)
.create();
let url = s.url();
let portal = Portal::new(&url);
let prelogin = portal.prelogin().await.unwrap();
let saml = match prelogin {
Prelogin::Saml(saml) => saml,
_ => panic!("Prelogin is not SAML"),
};
mock.assert();
assert!(saml.method == "REDIRECT");
assert!(saml.request.contains("SAMLRequest"));
assert!(saml.region == "CN")
}
#[tokio::test]
async fn test_prelogin_standard() {
let mut s = mockito::Server::new();
let mock = s
.mock("GET", "/global-protect/prelogin.esp")
.with_body(
r#"<?xml version="1.0" encoding="UTF-8" ?>
<prelogin-response>
<status>Success</status>
<ccusername></ccusername>
<autosubmit>false</autosubmit>
<msg></msg>
<newmsg></newmsg>
<authentication-message>Enter login credentials</authentication-message>
<username-label>Username</username-label>
<password-label>Password</password-label>
<panos-version>1</panos-version>
<saml-default-browser>yes</saml-default-browser><auth-api>no</auth-api><region>US</region>
</prelogin-response>"#,
)
.create();
let url = s.url();
let portal = Portal::new(&url);
let prelogin = portal.prelogin().await.unwrap();
let standard = match prelogin {
Prelogin::Standard(standard) => standard,
_ => panic!("Prelogin is not standard"),
};
mock.assert();
assert!(standard.label_username == "Username");
assert!(standard.label_password == "Password");
assert!(standard.auth_message == "Enter login credentials");
assert!(standard.region == "US");
}
#[tokio::test]
async fn test_retrieve_config_standard_credential() {}
}

View File

@ -12,11 +12,12 @@ rust-version = "1.59"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "1.3", features = [] }
tauri-build = { version = "1.4.0", features = [] }
[dependencies]
gpcommon = { path = "../../gpcommon" }
tauri = { version = "1.3", features = ["fs-write-file", "http-all", "os-all", "process-exit", "shell-open", "window-all", "window-data-url"] }
gpauth = { path = "../../gpauth" }
tauri = { version = "1.4.0", features = ["fs-write-file", "http-all", "os-all", "process-exit", "shell-open", "window-all", "window-data-url"] }
tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1", features = [
"colored",
] }

View File

@ -147,7 +147,7 @@ fn build_window(app_handle: &AppHandle, ua: &str) -> tauri::Result<Window> {
.title("GlobalProtect Login")
.inner_size(600.0, 500.0)
.min_inner_size(370.0, 600.0)
.user_agent(ua)
.user_agent("PAN GlobalProtect")
.always_on_top(true)
.focused(true)
.center()

View File

@ -0,0 +1,10 @@
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub(crate) enum Error {
#[error("Failed to encrypt data. {0}")]
Encrypt(String),
#[error("Failed to decrypt data. {0}")]
Decrypt(String),
#[error(transparent)]
Other(#[from] anyhow::Error),
}

View File

@ -5,10 +5,10 @@ edition = "2021"
[dependencies]
gpcommon = { path = "../gpcommon" }
tokio.workspace = true
fern.workspace = true
humantime.workspace = true
log.workspace = true
fern = "0.6"
humantime = "2.1"
tokio.workspace = true
[build-dependencies]
gpcommon = { path = "../gpcommon" }