mirror of
				https://github.com/yuezk/GlobalProtect-openconnect.git
				synced 2025-05-20 07:26:58 -04:00 
			
		
		
		
	refactor: add auth window
This commit is contained in:
		| @@ -8,3 +8,5 @@ edition = "2021" | ||||
| [dependencies] | ||||
| wry = "0.24.3" | ||||
| 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 { | ||||
|     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; | ||||
| @@ -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
									
								
							
							
						
						
									
										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); | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user