mirror of
https://github.com/yuezk/GlobalProtect-openconnect.git
synced 2025-04-02 18:31:50 -04:00
refactor: add auth window
This commit is contained in:
parent
f42f0d248e
commit
a1b49fde47
12
Cargo.lock
generated
12
Cargo.lock
generated
@ -54,11 +54,14 @@ dependencies = [
|
|||||||
"env_logger",
|
"env_logger",
|
||||||
"gpcommon",
|
"gpcommon",
|
||||||
"log",
|
"log",
|
||||||
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-log",
|
"tauri-plugin-log",
|
||||||
|
"url",
|
||||||
|
"webkit2gtk",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -535,6 +538,12 @@ version = "2.3.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb"
|
checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "data-url"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8d7439c3735f405729d52c3fbbe4de140eaf938a1fe47d227c27f8254d4302a5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive_more"
|
name = "derive_more"
|
||||||
version = "0.99.17"
|
version = "0.99.17"
|
||||||
@ -1076,6 +1085,8 @@ dependencies = [
|
|||||||
name = "gpauth"
|
name = "gpauth"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"regex",
|
||||||
|
"tokio",
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"wry",
|
"wry",
|
||||||
]
|
]
|
||||||
@ -2819,6 +2830,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"attohttpc",
|
"attohttpc",
|
||||||
"cocoa",
|
"cocoa",
|
||||||
|
"data-url",
|
||||||
"dirs-next",
|
"dirs-next",
|
||||||
"embed_plist",
|
"embed_plist",
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
|
@ -8,3 +8,5 @@ edition = "2021"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
wry = "0.24.3"
|
wry = "0.24.3"
|
||||||
webkit2gtk = "0.18.2"
|
webkit2gtk = "0.18.2"
|
||||||
|
tokio = { version = "1.14", features = ["full"] }
|
||||||
|
regex="1"
|
54
gpauth/src/auth_service.rs
Normal file
54
gpauth/src/auth_service.rs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
use crate::{
|
||||||
|
duplex::duplex,
|
||||||
|
saml::{SamlAuth, SamlBinding, SamlOptions},
|
||||||
|
DuplexStreamHandle,
|
||||||
|
};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AuthService {
|
||||||
|
server: DuplexStreamHandle,
|
||||||
|
client: Arc<Mutex<DuplexStreamHandle>>,
|
||||||
|
saml_auth: Arc<SamlAuth>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AuthService {
|
||||||
|
fn default() -> Self {
|
||||||
|
let (client, server) = duplex(4096);
|
||||||
|
Self {
|
||||||
|
client: Arc::new(Mutex::new(client)),
|
||||||
|
server,
|
||||||
|
saml_auth: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthService {
|
||||||
|
pub async fn run(&mut self) {
|
||||||
|
loop {
|
||||||
|
println!("Server waiting for data");
|
||||||
|
match self.server.read().await {
|
||||||
|
Ok(data) => {
|
||||||
|
println!("Server received: {}", data);
|
||||||
|
let target = String::from("https://login.microsoftonline.com/901c038b-4638-4259-b115-c1753c7735aa/saml2?SAMLRequest=lVLBbsIwDP2VKveSNGlaiGilDg5DYlpFux12mdIQIFKbdEmKtr8fhaGxC9Lkk%2BXnZ79nzx3v2p4Vgz%2FojfwYpPPBZ9dqx86FDAxWM8OdckzzTjrmBauKpzXDE8R6a7wRpgVB4Zy0Xhm9MNoNnbSVtEcl5MtmnYGD971jEB57PemUsMZ5y73cf02E6VgcEzgyYgSrEhaLCgTL0xZK85Hvt7s1e3XtNztvdKu0HBngDEUCkWkTxgmZhjGms7CJIhqKKKVEpCmhnMNRDgbBapmBdzpL5BZFEu0oalKMpglttukpaBLHuEEnmHODXGnnufYZwAiTENEQ0xoljBJGyBsIyh%2F1D0pvld7ft6q5gBx7rOsyLJ%2BrGgSv0rqzxBMA5PNxQ3YebG9OcJ%2BWX30H%2BT9cnsObWfkl%2B%2FsD%2BTc%3D&RelayState=HEgCAOLrNmRmZTBkM2FlNDE2MDQyMDhjZTVmMTZlMTdiZTdiMTliNg%3D%3D");
|
||||||
|
let ua = String::from("PAN GlobalProtect");
|
||||||
|
|
||||||
|
let saml_options = SamlOptions::new(SamlBinding::Redirect, target, ua);
|
||||||
|
let saml_auth = self.saml_auth.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
saml_auth.process(saml_options).await;
|
||||||
|
});
|
||||||
|
// self.server.write(&data).await.expect("write failed");
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
println!("Server error: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn client(&self) -> Arc<Mutex<DuplexStreamHandle>> {
|
||||||
|
self.client.clone()
|
||||||
|
}
|
||||||
|
}
|
35
gpauth/src/duplex.rs
Normal file
35
gpauth/src/duplex.rs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt, DuplexStream};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct DuplexStreamHandle {
|
||||||
|
stream: DuplexStream,
|
||||||
|
buf_size: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DuplexStreamHandle {
|
||||||
|
fn new(stream: DuplexStream, buf_size: usize) -> Self {
|
||||||
|
Self { stream, buf_size }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn write(&mut self, data: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
self.stream.write_all(data.as_bytes()).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn read(&mut self) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
|
let mut buffer = vec![0; self.buf_size];
|
||||||
|
match self.stream.read(&mut buffer).await {
|
||||||
|
Ok(0) => Err("EOF".into()),
|
||||||
|
Ok(n) => Ok(String::from_utf8_lossy(&buffer[..n]).to_string()),
|
||||||
|
Err(err) => Err(err.to_string().into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn duplex(max_buf_size: usize) -> (DuplexStreamHandle, DuplexStreamHandle) {
|
||||||
|
let (a, b) = tokio::io::duplex(max_buf_size);
|
||||||
|
(
|
||||||
|
DuplexStreamHandle::new(a, max_buf_size),
|
||||||
|
DuplexStreamHandle::new(b, max_buf_size),
|
||||||
|
)
|
||||||
|
}
|
@ -1,14 +1,8 @@
|
|||||||
pub fn add(left: usize, right: usize) -> usize {
|
mod auth_service;
|
||||||
left + right
|
mod saml;
|
||||||
}
|
mod duplex;
|
||||||
|
|
||||||
#[cfg(test)]
|
pub use auth_service::AuthService;
|
||||||
mod tests {
|
pub use duplex::DuplexStreamHandle;
|
||||||
use super::*;
|
pub use saml::saml_login;
|
||||||
|
pub use saml::SamlBinding;
|
||||||
#[test]
|
|
||||||
fn it_works() {
|
|
||||||
let result = add(2, 2);
|
|
||||||
assert_eq!(result, 4);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,45 +1,55 @@
|
|||||||
use webkit2gtk::LoadEvent;
|
use gpauth::{AuthService, saml_login, SamlBinding};
|
||||||
use webkit2gtk::URIResponseExt;
|
|
||||||
use webkit2gtk::WebResourceExt;
|
|
||||||
use webkit2gtk::WebViewExt;
|
|
||||||
use wry::application::event::Event;
|
|
||||||
use wry::application::event::StartCause;
|
|
||||||
use wry::application::event::WindowEvent;
|
|
||||||
use wry::application::event_loop::ControlFlow;
|
|
||||||
use wry::application::event_loop::EventLoop;
|
|
||||||
use wry::application::window::WindowBuilder;
|
|
||||||
use wry::webview::WebViewBuilder;
|
|
||||||
use wry::webview::WebviewExtUnix;
|
|
||||||
|
|
||||||
fn main() -> wry::Result<()> {
|
#[tokio::main]
|
||||||
let event_loop = EventLoop::new();
|
async fn main() {
|
||||||
let window = WindowBuilder::new()
|
let url = String::from("https://globalprotect.kochind.com/global-protect/prelogin.esp?tmp=tmp&kerberos-support=yes&ipv6-support=yes&clientVer=4100&clientos=Linux");
|
||||||
.with_title("Hello World")
|
let _html = String::from(
|
||||||
.build(&event_loop)?;
|
r#"<html>
|
||||||
let _webview = WebViewBuilder::new(window)?
|
<body>
|
||||||
.with_url("https://tauri.studio")?
|
<form id="myform" method="POST" action="https://auth.kochid.com/idp/SSO.saml2">
|
||||||
.build()?;
|
<input type="hidden" name="SAMLRequest" value="PHNhbWxwOkF1dGhuUmVxdWVzdCB4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBBc3NlcnRpb25Db25zdW1lclNlcnZpY2VVUkw9Imh0dHBzOi8vZ2xvYmFscHJvdGVjdC5rb2NoaW5kLmNvbTo0NDMvU0FNTDIwL1NQL0FDUyIgRGVzdGluYXRpb249Imh0dHBzOi8vYXV0aC5rb2NoaWQuY29tL2lkcC9TU08uc2FtbDIiIElEPSJfZmEzZTA4NDE5NjdkZTdlYzUyNzc4Nzc4YzBkOTViMDEiIElzc3VlSW5zdGFudD0iMjAyMy0wNS0yNFQwNToyNDo1OVoiIFByb3RvY29sQmluZGluZz0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmJpbmRpbmdzOkhUVFAtUE9TVCIgVmVyc2lvbj0iMi4wIj48c2FtbDpJc3N1ZXIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI+aHR0cHM6Ly9nbG9iYWxwcm90ZWN0LmtvY2hpbmQuY29tOjQ0My9TQU1MMjAvU1A8L3NhbWw6SXNzdWVyPjwvc2FtbHA6QXV0aG5SZXF1ZXN0Pg==" />
|
||||||
|
<input type="hidden" name="RelayState" value="rgbNAP1wSGI0NGE1ZDZjOGM4YTkzNjk5NWNhY2JlZjkwMWJmMzIwYg==" />
|
||||||
|
</form>
|
||||||
|
<script>
|
||||||
|
document.getElementById('myform').subm{
|
||||||
|
let (client, server) = duplex(1);
|
||||||
|
AuthService { client, server }
|
||||||
|
}>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
let wv = _webview.webview();
|
let ua = String::from("PAN GlobalProtect");
|
||||||
wv.connect_load_changed(|wv, load_event| {
|
match saml_login(SamlBinding::Redirect, url, ua) {
|
||||||
if load_event == LoadEvent::Finished {
|
Ok(saml_result) => {
|
||||||
let response = wv.main_resource().unwrap().response().unwrap();
|
println!("SAML result: {:?}", saml_result);
|
||||||
response.http_headers().unwrap().foreach(|k, v| {
|
|
||||||
println!("{}: {}", k, v);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
Err(err) => {
|
||||||
|
println!("Error: {:?}", err);
|
||||||
event_loop.run(move |event, _, control_flow| {
|
|
||||||
*control_flow = ControlFlow::Wait;
|
|
||||||
|
|
||||||
match event {
|
|
||||||
Event::NewEvents(StartCause::Init) => println!("Wry has started!"),
|
|
||||||
Event::WindowEvent {
|
|
||||||
event: WindowEvent::CloseRequested,
|
|
||||||
..
|
|
||||||
} => *control_flow = ControlFlow::Exit,
|
|
||||||
_ => (),
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
// let mut auth_service = AuthService::default();
|
||||||
|
// let client = auth_service.client();
|
||||||
|
|
||||||
|
// tokio::spawn(async move {
|
||||||
|
// let mut client = client.lock().await;
|
||||||
|
// client.write("Hello").await.expect("write failed");
|
||||||
|
|
||||||
|
// loop {
|
||||||
|
// if let Ok(data) = client.read().await {
|
||||||
|
// println!("Received: {}", data);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// tokio::select! {
|
||||||
|
// _ = auth_service.run() => {
|
||||||
|
// println!("AuthService exited");
|
||||||
|
// }
|
||||||
|
// _ = tokio::signal::ctrl_c() => {
|
||||||
|
// println!("Ctrl-C received, exiting");
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
233
gpauth/src/saml.rs
Normal file
233
gpauth/src/saml.rs
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
use regex::Regex;
|
||||||
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
use webkit2gtk::gio::Cancellable;
|
||||||
|
use webkit2gtk::glib::GString;
|
||||||
|
use webkit2gtk::{LoadEvent, URIResponseExt, WebResourceExt, WebViewExt};
|
||||||
|
use wry::application::event::{Event, StartCause, WindowEvent};
|
||||||
|
use wry::application::event_loop::{ControlFlow, EventLoop, EventLoopProxy};
|
||||||
|
use wry::application::platform::run_return::EventLoopExtRunReturn;
|
||||||
|
use wry::application::window::WindowBuilder;
|
||||||
|
use wry::webview::{WebViewBuilder, WebviewExtUnix};
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub(crate) struct SamlAuth {}
|
||||||
|
|
||||||
|
pub(crate) struct SamlOptions {
|
||||||
|
binding: SamlBinding,
|
||||||
|
target: String,
|
||||||
|
user_agent: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SamlOptions {
|
||||||
|
pub fn new(binding: SamlBinding, target: String, user_agent: String) -> Self {
|
||||||
|
Self {
|
||||||
|
binding,
|
||||||
|
target,
|
||||||
|
user_agent,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SamlAuth {
|
||||||
|
pub async fn process(&self, options: SamlOptions) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let saml_result = saml_login(options.binding, options.target, options.user_agent);
|
||||||
|
println!("SAML result: {:?}", saml_result);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum SamlBinding {
|
||||||
|
Redirect,
|
||||||
|
Post,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SamlResult {
|
||||||
|
username: Option<String>,
|
||||||
|
prelogin_cookie: Option<String>,
|
||||||
|
portal_userauthcookie: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SamlResult {
|
||||||
|
fn new(
|
||||||
|
username: Option<String>,
|
||||||
|
prelogin_cookie: Option<String>,
|
||||||
|
portal_userauthcookie: Option<String>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
username,
|
||||||
|
prelogin_cookie,
|
||||||
|
portal_userauthcookie,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check(&self) -> bool {
|
||||||
|
self.username.is_some()
|
||||||
|
&& (self.prelogin_cookie.is_some() || self.portal_userauthcookie.is_some())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
enum SamlResultError {
|
||||||
|
NotFound,
|
||||||
|
Invalid,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum UserEvent {
|
||||||
|
SamlSuccess(SamlResult),
|
||||||
|
SamlError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn saml_login(
|
||||||
|
binding: SamlBinding,
|
||||||
|
target: String,
|
||||||
|
user_agent: String,
|
||||||
|
) -> Result<SamlResult, Box<dyn std::error::Error>> {
|
||||||
|
let mut event_loop: EventLoop<UserEvent> = EventLoop::with_user_event();
|
||||||
|
let event_proxy = event_loop.create_proxy();
|
||||||
|
|
||||||
|
let window = WindowBuilder::new()
|
||||||
|
.with_title("GlobalProtect Login")
|
||||||
|
.build(&event_loop)?;
|
||||||
|
|
||||||
|
let wv_builder = WebViewBuilder::new(window)?.with_user_agent(&user_agent);
|
||||||
|
let wv_builder = if let SamlBinding::Redirect = binding {
|
||||||
|
wv_builder.with_url(&target)?
|
||||||
|
} else {
|
||||||
|
wv_builder.with_html(&target)?
|
||||||
|
};
|
||||||
|
|
||||||
|
let wv = wv_builder.build()?;
|
||||||
|
let wv = wv.webview();
|
||||||
|
|
||||||
|
wv.connect_load_changed(move |webview, event| {
|
||||||
|
if let LoadEvent::Finished = event {
|
||||||
|
if let Some(main_resource) = webview.main_resource() {
|
||||||
|
// Read the SAML result from the HTTP headers
|
||||||
|
if let Some(response) = main_resource.response() {
|
||||||
|
if let Some(saml_result) = read_saml_result_from_response(&response) {
|
||||||
|
println!("Got SAML result from HTTP headers");
|
||||||
|
return emit_event(&event_proxy, UserEvent::SamlSuccess(saml_result));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the SAML result from the HTTP body
|
||||||
|
let event_proxy = event_proxy.clone();
|
||||||
|
main_resource.data(Cancellable::NONE, move |data| {
|
||||||
|
if let Ok(data) = data {
|
||||||
|
match read_saml_result_from_html(&data) {
|
||||||
|
Ok(saml_result) => {
|
||||||
|
println!("Got SAML result from HTTP body");
|
||||||
|
emit_event(&event_proxy, UserEvent::SamlSuccess(saml_result));
|
||||||
|
}
|
||||||
|
Err(err) if err == SamlResultError::Invalid => {
|
||||||
|
println!("Error reading SAML result from HTTP body: {:?}", err);
|
||||||
|
emit_event(
|
||||||
|
&event_proxy,
|
||||||
|
UserEvent::SamlError("Invalid SAML result".into()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
println!("SAML result not found in HTTP body");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let saml_result: Rc<RefCell<Option<SamlResult>>> = Rc::new(RefCell::new(None));
|
||||||
|
let saml_result_clone = saml_result.clone();
|
||||||
|
|
||||||
|
let exit_code = event_loop.run_return(move |event, _, control_flow| {
|
||||||
|
*control_flow = ControlFlow::Wait;
|
||||||
|
|
||||||
|
match event {
|
||||||
|
Event::NewEvents(StartCause::Init) => println!("Wry has started!"),
|
||||||
|
Event::WindowEvent {
|
||||||
|
event: WindowEvent::CloseRequested,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
println!("User closed the window");
|
||||||
|
*control_flow = ControlFlow::Exit
|
||||||
|
}
|
||||||
|
Event::UserEvent(UserEvent::SamlSuccess(result)) => {
|
||||||
|
*saml_result_clone.borrow_mut() = Some(result);
|
||||||
|
*control_flow = ControlFlow::Exit;
|
||||||
|
}
|
||||||
|
Event::UserEvent(UserEvent::SamlError(_)) => {
|
||||||
|
println!("Error reading SAML result");
|
||||||
|
wv.load_uri("https://baidu.com");
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
println!("Exit code: {:?}", exit_code);
|
||||||
|
|
||||||
|
let saml_result = if let Some(saml_result) = saml_result.borrow().clone() {
|
||||||
|
println!("SAML result: {:?}", saml_result);
|
||||||
|
Ok(saml_result)
|
||||||
|
} else {
|
||||||
|
println!("SAML result: None");
|
||||||
|
// TODO: Return a proper error
|
||||||
|
Err("SAML result not found".into())
|
||||||
|
};
|
||||||
|
|
||||||
|
saml_result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_saml_result_from_response(response: &webkit2gtk::URIResponse) -> Option<SamlResult> {
|
||||||
|
response.http_headers().and_then(|mut headers| {
|
||||||
|
let saml_result = SamlResult::new(
|
||||||
|
headers.get("saml-username").map(GString::into),
|
||||||
|
headers.get("prelogin-cookie").map(GString::into),
|
||||||
|
headers.get("portal-userauthcookie").map(GString::into),
|
||||||
|
);
|
||||||
|
|
||||||
|
if saml_result.check() {
|
||||||
|
Some(saml_result)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_saml_result_from_html(data: &[u8]) -> Result<SamlResult, SamlResultError> {
|
||||||
|
let body = String::from_utf8_lossy(data);
|
||||||
|
let saml_auth_status = parse_saml_tag(&body, "saml-auth-status");
|
||||||
|
|
||||||
|
match saml_auth_status {
|
||||||
|
Some(status) if status == "1" => extract_saml_result(&body).ok_or(SamlResultError::Invalid),
|
||||||
|
Some(status) if status == "-1" => Err(SamlResultError::Invalid),
|
||||||
|
_ => Err(SamlResultError::NotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_saml_result(body: &str) -> Option<SamlResult> {
|
||||||
|
let saml_result = SamlResult::new(
|
||||||
|
parse_saml_tag(body, "saml-username"),
|
||||||
|
parse_saml_tag(body, "prelogin-cookie"),
|
||||||
|
parse_saml_tag(body, "portal-userauthcookie"),
|
||||||
|
);
|
||||||
|
|
||||||
|
if saml_result.check() {
|
||||||
|
Some(saml_result)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_saml_tag(body: &str, tag: &str) -> Option<String> {
|
||||||
|
let re = Regex::new(&format!("<{}>(.*)</{}>", tag, tag)).unwrap();
|
||||||
|
re.captures(body)
|
||||||
|
.and_then(|captures| captures.get(1))
|
||||||
|
.map(|m| m.as_str().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_event(event_proxy: &EventLoopProxy<UserEvent>, event: UserEvent) {
|
||||||
|
if let Err(err) = event_proxy.send_event(event) {
|
||||||
|
println!("Error sending event: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
12
gpgui/public/auth.html
Normal file
12
gpgui/public/auth.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>GlobalProtect Login</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>Redirecting...</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -15,13 +15,16 @@ rust-version = "1.59"
|
|||||||
tauri-build = { version = "1.3", features = [] }
|
tauri-build = { version = "1.3", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tauri = { version = "1.3", features = ["http-all"] }
|
gpcommon = { path = "../../gpcommon" }
|
||||||
|
tauri = { version = "1.3", features = ["http-all", "window-data-url"] }
|
||||||
tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
env_logger = "0.10"
|
env_logger = "0.10"
|
||||||
gpcommon = { path = "../../gpcommon" }
|
webkit2gtk = "0.18.2"
|
||||||
|
regex = "1"
|
||||||
|
url = "2.3"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# by default Tauri runs in production mode
|
# by default Tauri runs in production mode
|
||||||
|
297
gpgui/src-tauri/src/auth.rs
Normal file
297
gpgui/src-tauri/src/auth.rs
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
use regex::Regex;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::sync::{Arc};
|
||||||
|
use tauri::{AppHandle, Manager, WindowBuilder, WindowEvent::CloseRequested, WindowUrl};
|
||||||
|
use url::Url;
|
||||||
|
use webkit2gtk::{
|
||||||
|
gio::Cancellable, glib::GString, traits::WebViewExt, LoadEvent, URIResponseExt, WebResource,
|
||||||
|
WebResourceExt,
|
||||||
|
};
|
||||||
|
|
||||||
|
const AUTH_WINDOW_LABEL: &str = "auth_window";
|
||||||
|
const AUTH_SUCCESS_EVENT: &str = "auth-success";
|
||||||
|
const AUTH_ERROR_EVENT: &str = "auth-error";
|
||||||
|
const AUTH_CANCEL_EVENT: &str = "auth-cancel";
|
||||||
|
const AUTH_REQUEST_EVENT: &str = "auth-request";
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub(crate) enum SamlBinding {
|
||||||
|
#[serde(rename = "REDIRECT")]
|
||||||
|
Redirect,
|
||||||
|
#[serde(rename = "POST")]
|
||||||
|
Post,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct AuthOptions {
|
||||||
|
saml_binding: SamlBinding,
|
||||||
|
saml_request: String,
|
||||||
|
user_agent: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct AuthRequestPayload {
|
||||||
|
#[serde(alias = "samlRequest")]
|
||||||
|
saml_request: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthOptions {
|
||||||
|
pub fn new(saml_binding: SamlBinding, saml_request: String, user_agent: String) -> Self {
|
||||||
|
Self {
|
||||||
|
saml_binding,
|
||||||
|
saml_request,
|
||||||
|
user_agent,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct AuthData {
|
||||||
|
username: Option<String>,
|
||||||
|
prelogin_cookie: Option<String>,
|
||||||
|
portal_userauthcookie: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthData {
|
||||||
|
fn new(
|
||||||
|
username: Option<String>,
|
||||||
|
prelogin_cookie: Option<String>,
|
||||||
|
portal_userauthcookie: Option<String>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
username,
|
||||||
|
prelogin_cookie,
|
||||||
|
portal_userauthcookie,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check(&self) -> bool {
|
||||||
|
self.username.is_some()
|
||||||
|
&& (self.prelogin_cookie.is_some() || self.portal_userauthcookie.is_some())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum AuthError {
|
||||||
|
NotFound,
|
||||||
|
Invalid,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct AuthEventEmitter {
|
||||||
|
app_handle: AppHandle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthEventEmitter {
|
||||||
|
fn new(app_handle: AppHandle) -> Self {
|
||||||
|
Self { app_handle }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_success(&self, saml_result: AuthData) {
|
||||||
|
self.app_handle.emit_all(AUTH_SUCCESS_EVENT, saml_result);
|
||||||
|
if let Some(window) = self.app_handle.get_window(AUTH_WINDOW_LABEL) {
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_error(&self, error: String) {
|
||||||
|
self.app_handle.emit_all(AUTH_ERROR_EVENT, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_cancel(&self) {
|
||||||
|
self.app_handle.emit_all(AUTH_CANCEL_EVENT, ());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct AuthWindow {
|
||||||
|
event_emitter: Arc<AuthEventEmitter>,
|
||||||
|
app_handle: AppHandle,
|
||||||
|
saml_binding: SamlBinding,
|
||||||
|
user_agent: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthWindow {
|
||||||
|
pub fn new(app_handle: AppHandle, saml_binding: SamlBinding, user_agent: String) -> Self {
|
||||||
|
Self {
|
||||||
|
event_emitter: Arc::new(AuthEventEmitter::new(app_handle.clone())),
|
||||||
|
app_handle,
|
||||||
|
saml_binding,
|
||||||
|
user_agent,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn process(&self, saml_request: String) -> tauri::Result<()> {
|
||||||
|
let url = self.window_url(&saml_request)?;
|
||||||
|
let window = WindowBuilder::new(&self.app_handle, AUTH_WINDOW_LABEL, url)
|
||||||
|
.title("GlobalProtect Login")
|
||||||
|
.user_agent(&self.user_agent)
|
||||||
|
.always_on_top(true)
|
||||||
|
.focused(true)
|
||||||
|
.center()
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
let event_emitter = self.event_emitter.clone();
|
||||||
|
let is_post = matches!(self.saml_binding, SamlBinding::Post);
|
||||||
|
|
||||||
|
window.with_webview(move |wv| {
|
||||||
|
let wv = wv.inner();
|
||||||
|
// Load SAML request as HTML if POST binding is used
|
||||||
|
if is_post {
|
||||||
|
wv.load_html(&saml_request, None);
|
||||||
|
}
|
||||||
|
wv.connect_load_changed(move |wv, event| {
|
||||||
|
if LoadEvent::Finished == event {
|
||||||
|
if let Some(uri) = wv.uri() {
|
||||||
|
if uri.is_empty() {
|
||||||
|
println!("Empty URI");
|
||||||
|
event_emitter.emit_error("Empty URI".to_string());
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
println!("Loaded URI: {}", uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(main_res) = wv.main_resource() {
|
||||||
|
AuthResultParser::new(&event_emitter).parse(&main_res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let event_emitter = self.event_emitter.clone();
|
||||||
|
window.on_window_event(move |event| {
|
||||||
|
if let CloseRequested { .. } = event {
|
||||||
|
event_emitter.emit_cancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let window_clone = window.clone();
|
||||||
|
window.listen_global(AUTH_REQUEST_EVENT, move |event| {
|
||||||
|
let auth_request_payload: AuthRequestPayload = serde_json::from_str(event.payload().unwrap()).unwrap();
|
||||||
|
let saml_request = auth_request_payload.saml_request;
|
||||||
|
|
||||||
|
window_clone.with_webview(move |wv| {
|
||||||
|
let wv = wv.inner();
|
||||||
|
if is_post {
|
||||||
|
// Load SAML request as HTML if POST binding is used
|
||||||
|
wv.load_html(&saml_request, None);
|
||||||
|
} else {
|
||||||
|
println!("Redirecting to SAML request URL: {}", saml_request);
|
||||||
|
// Redirect to SAML request URL if REDIRECT binding is used
|
||||||
|
wv.load_uri(&saml_request);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_url(&self, saml_request: &String) -> tauri::Result<WindowUrl> {
|
||||||
|
match self.saml_binding {
|
||||||
|
SamlBinding::Redirect => match Url::parse(saml_request) {
|
||||||
|
Ok(url) => Ok(WindowUrl::External(url)),
|
||||||
|
Err(err) => Err(tauri::Error::InvalidUrl(err)),
|
||||||
|
},
|
||||||
|
SamlBinding::Post => Ok(WindowUrl::App("auth.html".into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AuthResultParser<'a> {
|
||||||
|
event_emitter: &'a Arc<AuthEventEmitter>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> AuthResultParser<'a> {
|
||||||
|
fn new(event_emitter: &'a Arc<AuthEventEmitter>) -> Self {
|
||||||
|
Self { event_emitter }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse(&self, main_res: &WebResource) {
|
||||||
|
if let Some(response) = main_res.response() {
|
||||||
|
if let Some(saml_result) = read_auth_result_from_response(&response) {
|
||||||
|
// Got SAML result from HTTP headers
|
||||||
|
println!("SAML result: {:?}", saml_result);
|
||||||
|
self.event_emitter.emit_success(saml_result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let event_emitter = self.event_emitter.clone();
|
||||||
|
main_res.data(Cancellable::NONE, move |data| {
|
||||||
|
if let Ok(data) = data {
|
||||||
|
let html = String::from_utf8_lossy(&data);
|
||||||
|
match read_auth_result_from_html(&html) {
|
||||||
|
Ok(saml_result) => {
|
||||||
|
// Got SAML result from HTML
|
||||||
|
println!("SAML result: {:?}", saml_result);
|
||||||
|
event_emitter.emit_success(saml_result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(AuthError::Invalid) => {
|
||||||
|
// Invalid SAML result
|
||||||
|
println!("Invalid SAML result");
|
||||||
|
event_emitter.emit_error("Invalid SAML result".to_string())
|
||||||
|
}
|
||||||
|
Err(AuthError::NotFound) => {
|
||||||
|
let has_form = html.contains("</form>");
|
||||||
|
if has_form {
|
||||||
|
// SAML form found
|
||||||
|
println!("SAML form found");
|
||||||
|
} else {
|
||||||
|
// No SAML form found
|
||||||
|
println!("No SAML form found");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_auth_result_from_response(response: &webkit2gtk::URIResponse) -> Option<AuthData> {
|
||||||
|
response.http_headers().and_then(|mut headers| {
|
||||||
|
let saml_result = AuthData::new(
|
||||||
|
headers.get("saml-username").map(GString::into),
|
||||||
|
headers.get("prelogin-cookie").map(GString::into),
|
||||||
|
headers.get("portal-userauthcookie").map(GString::into),
|
||||||
|
);
|
||||||
|
|
||||||
|
if saml_result.check() {
|
||||||
|
Some(saml_result)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_auth_result_from_html(html: &str) -> Result<AuthData, AuthError> {
|
||||||
|
let saml_auth_status = parse_xml_tag(html, "saml-auth-status");
|
||||||
|
|
||||||
|
|
||||||
|
match saml_auth_status {
|
||||||
|
Some(status) if status == "1" => extract_auth_data(html).ok_or(AuthError::Invalid),
|
||||||
|
Some(status) if status == "-1" => Err(AuthError::Invalid),
|
||||||
|
_ => Err(AuthError::NotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_auth_data(html: &str) -> Option<AuthData> {
|
||||||
|
let auth_data = AuthData::new(
|
||||||
|
parse_xml_tag(html, "saml-username"),
|
||||||
|
parse_xml_tag(html, "prelogin-cookie"),
|
||||||
|
parse_xml_tag(html, "portal-userauthcookie"),
|
||||||
|
);
|
||||||
|
|
||||||
|
if auth_data.check() {
|
||||||
|
Some(auth_data)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
@ -3,13 +3,16 @@
|
|||||||
windows_subsystem = "windows"
|
windows_subsystem = "windows"
|
||||||
)]
|
)]
|
||||||
|
|
||||||
use gpcommon::{Client, ServerApiError, VpnStatus};
|
use auth::{SamlBinding, AuthWindow};
|
||||||
use env_logger::Env;
|
use env_logger::Env;
|
||||||
|
use gpcommon::{Client, ServerApiError, VpnStatus};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tauri::{Manager, State};
|
use tauri::{AppHandle, Manager, State};
|
||||||
use tauri_plugin_log::LogTarget;
|
use tauri_plugin_log::LogTarget;
|
||||||
|
|
||||||
|
mod auth;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn vpn_status<'a>(client: State<'a, Arc<Client>>) -> Result<VpnStatus, ServerApiError> {
|
async fn vpn_status<'a>(client: State<'a, Arc<Client>>) -> Result<VpnStatus, ServerApiError> {
|
||||||
client.status().await
|
client.status().await
|
||||||
@ -29,6 +32,20 @@ async fn vpn_disconnect<'a>(client: State<'a, Arc<Client>>) -> Result<(), Server
|
|||||||
client.disconnect().await
|
client.disconnect().await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn saml_login(
|
||||||
|
binding: SamlBinding,
|
||||||
|
request: String,
|
||||||
|
app_handle: AppHandle,
|
||||||
|
) -> tauri::Result<()> {
|
||||||
|
let auth_window = AuthWindow::new(app_handle, binding, String::from("PAN GlobalProtect"));
|
||||||
|
if let Err(err) = auth_window.process(request) {
|
||||||
|
println!("Error processing auth window: {}", err);
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
struct StatusPayload {
|
struct StatusPayload {
|
||||||
status: VpnStatus,
|
status: VpnStatus,
|
||||||
@ -43,11 +60,11 @@ fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let _ = client_clone.subscribe_status(move |status| {
|
let _ = client_clone.subscribe_status(move |status| {
|
||||||
let payload = StatusPayload { status };
|
let payload = StatusPayload { status };
|
||||||
if let Err(err) = app_handle.emit_all("vpn-status-received", payload) {
|
if let Err(err) = app_handle.emit_all("vpn-status-received", payload) {
|
||||||
println!("Error emmiting event: {}", err);
|
println!("Error emitting event: {}", err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let _ = client_clone.run().await;
|
// let _ = client_clone.run().await;
|
||||||
});
|
});
|
||||||
|
|
||||||
app.manage(client);
|
app.manage(client);
|
||||||
@ -70,7 +87,8 @@ fn main() {
|
|||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
vpn_status,
|
vpn_status,
|
||||||
vpn_connect,
|
vpn_connect,
|
||||||
vpn_disconnect
|
vpn_disconnect,
|
||||||
|
saml_login,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
@ -12,9 +12,10 @@ import PasswordAuth, {
|
|||||||
import gatewayService from "./services/gatewayService";
|
import gatewayService from "./services/gatewayService";
|
||||||
import portalService from "./services/portalService";
|
import portalService from "./services/portalService";
|
||||||
import vpnService from "./services/vpnService";
|
import vpnService from "./services/vpnService";
|
||||||
|
import authService from "./services/authService";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [portalAddress, setPortalAddress] = useState("220.191.185.154");
|
const [portalAddress, setPortalAddress] = useState("vpn.microstrategy.com"); // useState("220.191.185.154");
|
||||||
const [status, setStatus] = useState<Status>("disconnected");
|
const [status, setStatus] = useState<Status>("disconnected");
|
||||||
const [processing, setProcessing] = useState(false);
|
const [processing, setProcessing] = useState(false);
|
||||||
const [passwordAuthOpen, setPasswordAuthOpen] = useState(false);
|
const [passwordAuthOpen, setPasswordAuthOpen] = useState(false);
|
||||||
@ -35,6 +36,16 @@ export default function App() {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
authService.onAuthSuccess((data) => {});
|
||||||
|
authService.onAuthError(async () => {
|
||||||
|
const preloginResponse = await portalService.prelogin(portalAddress);
|
||||||
|
// Retry SAML login when auth error occurs
|
||||||
|
authService.emitAuthRequest(preloginResponse.samlAuthRequest!);
|
||||||
|
});
|
||||||
|
authService.onAuthCancel(() => {});
|
||||||
|
}, [portalAddress]);
|
||||||
|
|
||||||
function closeNotification() {
|
function closeNotification() {
|
||||||
setNotification((notification) => ({
|
setNotification((notification) => ({
|
||||||
...notification,
|
...notification,
|
||||||
@ -62,7 +73,8 @@ export default function App() {
|
|||||||
const response = await portalService.prelogin(portalAddress);
|
const response = await portalService.prelogin(portalAddress);
|
||||||
|
|
||||||
if (portalService.isSamlAuth(response)) {
|
if (portalService.isSamlAuth(response)) {
|
||||||
// TODO SAML login
|
const { samlAuthMethod, samlAuthRequest } = response;
|
||||||
|
await authService.samlLogin(samlAuthMethod, samlAuthRequest);
|
||||||
} else if (portalService.isPasswordAuth(response)) {
|
} else if (portalService.isPasswordAuth(response)) {
|
||||||
setPasswordAuthOpen(true);
|
setPasswordAuthOpen(true);
|
||||||
setPasswordAuth({
|
setPasswordAuth({
|
||||||
@ -74,6 +86,7 @@ export default function App() {
|
|||||||
throw new Error("Unsupported portal login method");
|
throw new Error("Unsupported portal login method");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
53
gpgui/src/services/authService.ts
Normal file
53
gpgui/src/services/authService.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { Event, emit, listen } from "@tauri-apps/api/event";
|
||||||
|
import invokeCommand from "../utils/invokeCommand";
|
||||||
|
|
||||||
|
type AuthData = {
|
||||||
|
username: string;
|
||||||
|
prelogin_cookie: string | null;
|
||||||
|
portal_userauthcookie: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
class AuthService {
|
||||||
|
private authSuccessCallback: ((data: AuthData) => void) | undefined;
|
||||||
|
private authErrorCallback: (() => void) | undefined;
|
||||||
|
private authCancelCallback: (() => void) | undefined;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async init() {
|
||||||
|
await listen("auth-success", (event: Event<AuthData>) => {
|
||||||
|
this.authSuccessCallback?.(event.payload);
|
||||||
|
});
|
||||||
|
await listen("auth-error", (event) => {
|
||||||
|
this.authErrorCallback?.();
|
||||||
|
});
|
||||||
|
await listen("auth-cancel", (event) => {
|
||||||
|
this.authCancelCallback?.();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onAuthSuccess(callback: (data: AuthData) => void) {
|
||||||
|
this.authSuccessCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
onAuthError(callback: () => void) {
|
||||||
|
this.authErrorCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
onAuthCancel(callback: () => void) {
|
||||||
|
this.authCancelCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
// binding: "POST" | "REDIRECT"
|
||||||
|
async samlLogin(binding: string, request: string) {
|
||||||
|
return invokeCommand("saml_login", { binding, request });
|
||||||
|
}
|
||||||
|
|
||||||
|
emitAuthRequest(authRequest: string) {
|
||||||
|
emit("auth-request", { samlRequest: authRequest });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new AuthService();
|
@ -1,6 +1,7 @@
|
|||||||
import { Body, ResponseType, fetch } from "@tauri-apps/api/http";
|
import { Body, ResponseType, fetch } from "@tauri-apps/api/http";
|
||||||
import { Maybe, MaybeProperties } from "../types";
|
import { Maybe, MaybeProperties } from "../types";
|
||||||
import { parseXml } from "../utils/parseXml";
|
import { parseXml } from "../utils/parseXml";
|
||||||
|
import authService from "./authService";
|
||||||
import { Gateway } from "./types";
|
import { Gateway } from "./types";
|
||||||
|
|
||||||
type SamlPreloginResponse = {
|
type SamlPreloginResponse = {
|
||||||
@ -35,6 +36,9 @@ class PortalService {
|
|||||||
|
|
||||||
const response = await fetch<string>(preloginUrl, {
|
const response = await fetch<string>(preloginUrl, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"User-Agent": "PAN GlobalProtect",
|
||||||
|
},
|
||||||
responseType: ResponseType.Text,
|
responseType: ResponseType.Text,
|
||||||
query: {
|
query: {
|
||||||
tmp: "tmp",
|
tmp: "tmp",
|
||||||
@ -55,8 +59,8 @@ class PortalService {
|
|||||||
const doc = parseXml(response);
|
const doc = parseXml(response);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
samlAuthMethod: doc.text("saml-auth-method"),
|
samlAuthMethod: doc.text("saml-auth-method").toUpperCase(),
|
||||||
samlAuthRequest: doc.text("saml-auth-request"),
|
samlAuthRequest: atob(doc.text("saml-request")),
|
||||||
labelUsername: doc.text("username-label"),
|
labelUsername: doc.text("username-label"),
|
||||||
labelPassword: doc.text("password-label"),
|
labelPassword: doc.text("password-label"),
|
||||||
authMessage: doc.text("authentication-message"),
|
authMessage: doc.text("authentication-message"),
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
import { invoke } from "@tauri-apps/api";
|
import { Event, listen } from "@tauri-apps/api/event";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import invokeCommand from "../utils/invokeCommand";
|
||||||
|
|
||||||
type Status = "disconnected" | "connecting" | "connected" | "disconnecting";
|
type Status = "disconnected" | "connecting" | "connected" | "disconnecting";
|
||||||
type StatusCallback = (status: Status) => void;
|
type StatusCallback = (status: Status) => void;
|
||||||
type StatusEvent = {
|
type StatusPayload = {
|
||||||
payload: {
|
status: Status;
|
||||||
status: Status;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class VpnService {
|
class VpnService {
|
||||||
@ -18,7 +16,7 @@ class VpnService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async init() {
|
private async init() {
|
||||||
await listen("vpn-status-received", (event: StatusEvent) => {
|
await listen("vpn-status-received", (event: Event<StatusPayload>) => {
|
||||||
console.log("vpn-status-received", event.payload);
|
console.log("vpn-status-received", event.payload);
|
||||||
this.setStatus(event.payload.status);
|
this.setStatus(event.payload.status);
|
||||||
});
|
});
|
||||||
@ -35,15 +33,15 @@ class VpnService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async status(): Promise<Status> {
|
private async status(): Promise<Status> {
|
||||||
return this.invokeCommand<Status>("vpn_status");
|
return invokeCommand<Status>("vpn_status");
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect(server: string, cookie: string) {
|
async connect(server: string, cookie: string) {
|
||||||
return this.invokeCommand("vpn_connect", { server, cookie });
|
return invokeCommand("vpn_connect", { server, cookie });
|
||||||
}
|
}
|
||||||
|
|
||||||
async disconnect() {
|
async disconnect() {
|
||||||
return this.invokeCommand("vpn_disconnect");
|
return invokeCommand("vpn_disconnect");
|
||||||
}
|
}
|
||||||
|
|
||||||
onStatusChanged(callback: StatusCallback) {
|
onStatusChanged(callback: StatusCallback) {
|
||||||
@ -59,14 +57,6 @@ class VpnService {
|
|||||||
private removeStatusCallback(callback: StatusCallback) {
|
private removeStatusCallback(callback: StatusCallback) {
|
||||||
this.statusCallbacks = this.statusCallbacks.filter((cb) => cb !== callback);
|
this.statusCallbacks = this.statusCallbacks.filter((cb) => cb !== callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async invokeCommand<T>(command: string, args?: any) {
|
|
||||||
try {
|
|
||||||
return await invoke<T>(command, args);
|
|
||||||
} catch (err: any) {
|
|
||||||
throw new Error(err.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new VpnService();
|
export default new VpnService();
|
||||||
|
9
gpgui/src/utils/invokeCommand.ts
Normal file
9
gpgui/src/utils/invokeCommand.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { invoke } from "@tauri-apps/api";
|
||||||
|
|
||||||
|
export default async function invokeCommand<T>(command: string, args?: any) {
|
||||||
|
try {
|
||||||
|
return await invoke<T>(command, args);
|
||||||
|
} catch (err: any) {
|
||||||
|
throw new Error(err.message);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user