refactor: add auth window

This commit is contained in:
Kevin Yue
2023-05-28 14:04:22 +08:00
parent f42f0d248e
commit a1b49fde47
16 changed files with 821 additions and 82 deletions

View File

@@ -8,3 +8,5 @@ edition = "2021"
[dependencies]
wry = "0.24.3"
webkit2gtk = "0.18.2"
tokio = { version = "1.14", features = ["full"] }
regex="1"

View 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
View 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),
)
}

View File

@@ -1,14 +1,8 @@
pub fn add(left: usize, right: usize) -> usize {
left + right
}
mod auth_service;
mod saml;
mod duplex;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
pub use auth_service::AuthService;
pub use duplex::DuplexStreamHandle;
pub use saml::saml_login;
pub use saml::SamlBinding;

View File

@@ -1,45 +1,55 @@
use webkit2gtk::LoadEvent;
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;
use gpauth::{AuthService, saml_login, SamlBinding};
fn main() -> wry::Result<()> {
let event_loop = EventLoop::new();
let window = WindowBuilder::new()
.with_title("Hello World")
.build(&event_loop)?;
let _webview = WebViewBuilder::new(window)?
.with_url("https://tauri.studio")?
.build()?;
#[tokio::main]
async fn main() {
let url = String::from("https://globalprotect.kochind.com/global-protect/prelogin.esp?tmp=tmp&kerberos-support=yes&ipv6-support=yes&clientVer=4100&clientos=Linux");
let _html = String::from(
r#"<html>
<body>
<form id="myform" method="POST" action="https://auth.kochid.com/idp/SSO.saml2">
<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();
wv.connect_load_changed(|wv, load_event| {
if load_event == LoadEvent::Finished {
let response = wv.main_resource().unwrap().response().unwrap();
response.http_headers().unwrap().foreach(|k, v| {
println!("{}: {}", k, v);
});
let ua = String::from("PAN GlobalProtect");
match saml_login(SamlBinding::Redirect, url, ua) {
Ok(saml_result) => {
println!("SAML result: {:?}", saml_result);
}
});
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,
_ => (),
Err(err) => {
println!("Error: {:?}", err);
}
});
}
// 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
View 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);
}
}