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