mirror of
				https://github.com/yuezk/GlobalProtect-openconnect.git
				synced 2025-05-20 07:26:58 -04:00 
			
		
		
		
	refactor: encrypt the sensitive data
This commit is contained in:
		| @@ -23,8 +23,7 @@ | ||||
|     "react": "^18.2.0", | ||||
|     "react-dom": "^18.2.0", | ||||
|     "react-spinners": "^0.13.8", | ||||
|     "tauri-plugin-log-api": "github:tauri-apps/tauri-plugin-log", | ||||
|     "tauri-plugin-store-api": "github:tauri-apps/tauri-plugin-store#v1" | ||||
|     "tauri-plugin-log-api": "github:tauri-apps/tauri-plugin-log#v1" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@tauri-apps/cli": "^1.3.1", | ||||
|   | ||||
							
								
								
									
										28
									
								
								gpgui/pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										28
									
								
								gpgui/pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							| @@ -1,4 +1,4 @@ | ||||
| lockfileVersion: '6.1' | ||||
| lockfileVersion: '6.0' | ||||
|  | ||||
| settings: | ||||
|   autoInstallPeers: true | ||||
| @@ -48,11 +48,8 @@ dependencies: | ||||
|     specifier: ^0.13.8 | ||||
|     version: 0.13.8(react-dom@18.2.0)(react@18.2.0) | ||||
|   tauri-plugin-log-api: | ||||
|     specifier: github:tauri-apps/tauri-plugin-log | ||||
|     version: github.com/tauri-apps/tauri-plugin-log/21921031d74f871180381317a338559f588ad8e9 | ||||
|   tauri-plugin-store-api: | ||||
|     specifier: github:tauri-apps/tauri-plugin-store#v1 | ||||
|     version: github.com/tauri-apps/tauri-plugin-store/1467ba770623ab1d41d825841c3d9435d9eaa0f1 | ||||
|     specifier: github:tauri-apps/tauri-plugin-log#v1 | ||||
|     version: github.com/tauri-apps/tauri-plugin-log/fbbb126e6d7fba7a7e6772d33f99c0fb689f32b6 | ||||
|  | ||||
| devDependencies: | ||||
|   '@tauri-apps/cli': | ||||
| @@ -878,6 +875,11 @@ packages: | ||||
|     engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'} | ||||
|     dev: false | ||||
|  | ||||
|   /@tauri-apps/api@1.4.0: | ||||
|     resolution: {integrity: sha512-Jd6HPoTM1PZSFIzq7FB8VmMu3qSSyo/3lSwLpoapW+lQ41CL5Dow2KryLg+gyazA/58DRWI9vu/XpEeHK4uMdw==} | ||||
|     engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'} | ||||
|     dev: false | ||||
|  | ||||
|   /@tauri-apps/cli-darwin-arm64@1.3.1: | ||||
|     resolution: {integrity: sha512-QlepYVPgOgspcwA/u4kGG4ZUijlXfdRtno00zEy+LxinN/IRXtk+6ErVtsmoLi1ZC9WbuMwzAcsRvqsD+RtNAg==} | ||||
|     engines: {node: '>= 10'} | ||||
| @@ -1465,18 +1467,10 @@ packages: | ||||
|     engines: {node: '>= 6'} | ||||
|     dev: false | ||||
|  | ||||
|   github.com/tauri-apps/tauri-plugin-log/21921031d74f871180381317a338559f588ad8e9: | ||||
|     resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-log/tar.gz/21921031d74f871180381317a338559f588ad8e9} | ||||
|   github.com/tauri-apps/tauri-plugin-log/fbbb126e6d7fba7a7e6772d33f99c0fb689f32b6: | ||||
|     resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-log/tar.gz/fbbb126e6d7fba7a7e6772d33f99c0fb689f32b6} | ||||
|     name: tauri-plugin-log-api | ||||
|     version: 0.0.0 | ||||
|     dependencies: | ||||
|       '@tauri-apps/api': 1.3.0 | ||||
|     dev: false | ||||
|  | ||||
|   github.com/tauri-apps/tauri-plugin-store/1467ba770623ab1d41d825841c3d9435d9eaa0f1: | ||||
|     resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-store/tar.gz/1467ba770623ab1d41d825841c3d9435d9eaa0f1} | ||||
|     name: tauri-plugin-store-api | ||||
|     version: 0.0.0 | ||||
|     dependencies: | ||||
|       '@tauri-apps/api': 1.3.0 | ||||
|       '@tauri-apps/api': 1.4.0 | ||||
|     dev: false | ||||
|   | ||||
| @@ -23,7 +23,6 @@ tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", br | ||||
| serde_json = "1.0" | ||||
| serde = { version = "1.0", features = ["derive"] } | ||||
| log = "0.4" | ||||
| env_logger = "0.10" | ||||
| webkit2gtk = "0.18.2" | ||||
| regex = "1" | ||||
| url = "2.3" | ||||
| @@ -32,6 +31,10 @@ veil = "0.1.6" | ||||
| whoami = "1.4.1" | ||||
| tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } | ||||
| openssl = "0.10" | ||||
| keyring = "2" | ||||
| aes-gcm = { version = "0.10", features = ["std"] } | ||||
| hex = "0.4" | ||||
| anyhow = "1.0" | ||||
|  | ||||
| [features] | ||||
| # by default Tauri runs in production mode | ||||
|   | ||||
| @@ -133,7 +133,7 @@ fn build_window(app_handle: &AppHandle, ua: &str) -> tauri::Result<Window> { | ||||
|     Window::builder(app_handle, AUTH_WINDOW_LABEL, url) | ||||
|         .visible(false) | ||||
|         .title("GlobalProtect Login") | ||||
|         .inner_size(400.0, 647.0) | ||||
|         .inner_size(600.0, 500.0) | ||||
|         .min_inner_size(370.0, 600.0) | ||||
|         .user_agent(ua) | ||||
|         .always_on_top(true) | ||||
|   | ||||
| @@ -1,9 +1,11 @@ | ||||
| use crate::{ | ||||
|     auth::{self, AuthData, AuthRequest, SamlBinding, SamlLoginParams}, | ||||
|     storage::{AppStorage, KeyHint}, | ||||
|     utils::get_openssl_conf, | ||||
|     utils::get_openssl_conf_path, | ||||
| }; | ||||
| use gpcommon::{Client, ServerApiError, VpnStatus}; | ||||
| use serde_json::Value; | ||||
| use std::sync::Arc; | ||||
| use tauri::{AppHandle, State}; | ||||
| use tokio::fs; | ||||
| @@ -24,9 +26,10 @@ pub(crate) async fn vpn_status<'a>( | ||||
| pub(crate) async fn vpn_connect<'a>( | ||||
|     server: String, | ||||
|     cookie: String, | ||||
|     user_agent: String, | ||||
|     client: State<'a, Arc<Client>>, | ||||
| ) -> Result<(), ServerApiError> { | ||||
|     client.connect(server, cookie).await | ||||
|     client.connect(server, cookie, user_agent).await | ||||
| } | ||||
|  | ||||
| #[tauri::command] | ||||
| @@ -40,10 +43,10 @@ pub(crate) async fn vpn_disconnect<'a>( | ||||
| pub(crate) async fn saml_login( | ||||
|     binding: SamlBinding, | ||||
|     request: String, | ||||
|     user_agent: String, | ||||
|     clear_cookies: bool, | ||||
|     app_handle: AppHandle, | ||||
| ) -> tauri::Result<Option<AuthData>> { | ||||
|     let user_agent = String::from("PAN GlobalProtect"); | ||||
|     let params = SamlLoginParams { | ||||
|         auth_request: AuthRequest::new(binding, request), | ||||
|         user_agent, | ||||
| @@ -71,3 +74,29 @@ pub(crate) async fn update_openssl_config(app_handle: AppHandle) -> tauri::Resul | ||||
|     fs::write(openssl_conf_path, openssl_conf).await?; | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| #[tauri::command] | ||||
| pub(crate) async fn store_get<'a>( | ||||
|     hint: KeyHint<'_>, | ||||
|     app_storage: State<'_, AppStorage<'_>>, | ||||
| ) -> Result<Option<Value>, ()> { | ||||
|     Ok(app_storage.get(hint)) | ||||
| } | ||||
|  | ||||
| #[tauri::command] | ||||
| pub(crate) fn store_set( | ||||
|     hint: KeyHint, | ||||
|     value: Value, | ||||
|     app_storage: State<'_, AppStorage>, | ||||
| ) -> Result<(), tauri_plugin_store::Error> { | ||||
|     app_storage.set(hint, &value)?; | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| #[tauri::command] | ||||
| pub(crate) fn store_save( | ||||
|     app_storage: State<'_, AppStorage>, | ||||
| ) -> Result<(), tauri_plugin_store::Error> { | ||||
|     app_storage.save()?; | ||||
|     Ok(()) | ||||
| } | ||||
|   | ||||
							
								
								
									
										46
									
								
								gpgui/src-tauri/src/crypto.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								gpgui/src-tauri/src/crypto.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| use aes_gcm::{ | ||||
|     aead::{consts::U12, Aead, OsRng}, | ||||
|     AeadCore, Aes256Gcm, Key, KeyInit, Nonce, | ||||
| }; | ||||
| use keyring::Entry; | ||||
|  | ||||
| const SERVICE_NAME: &str = "GlobalProtect-openconnect"; | ||||
| const ENTRY_KEY: &str = "master-key"; | ||||
|  | ||||
| fn get_master_key() -> Result<Key<Aes256Gcm>, anyhow::Error> { | ||||
|     let key_entry = Entry::new(SERVICE_NAME, ENTRY_KEY)?; | ||||
|  | ||||
|     if let Ok(key) = key_entry.get_password() { | ||||
|         let key = hex::decode(key)?; | ||||
|         return Ok(Key::<Aes256Gcm>::clone_from_slice(&key)); | ||||
|     } | ||||
|  | ||||
|     let key = Aes256Gcm::generate_key(OsRng); | ||||
|     let encoded_key = hex::encode(key); | ||||
|  | ||||
|     key_entry.set_password(&encoded_key)?; | ||||
|  | ||||
|     Ok(key) | ||||
| } | ||||
|  | ||||
| pub(crate) fn encrypt(data: &str) -> Result<String, anyhow::Error> { | ||||
|     let master_key = get_master_key()?; | ||||
|     let cipher = Aes256Gcm::new(&master_key); | ||||
|     let nonce = Aes256Gcm::generate_nonce(&mut OsRng); | ||||
|     let cipher_text = cipher.encrypt(&nonce, data.as_bytes())?; | ||||
|  | ||||
|     let mut encrypted = nonce.to_vec(); | ||||
|     encrypted.extend_from_slice(&cipher_text); | ||||
|     Ok(hex::encode(encrypted)) | ||||
| } | ||||
|  | ||||
| pub(crate) fn decrypt(encrypted: &str) -> Result<String, anyhow::Error> { | ||||
|     let master_key = get_master_key()?; | ||||
|     let encrypted = hex::decode(encrypted)?; | ||||
|     let nonce = Nonce::<U12>::from_slice(&encrypted[..12]); | ||||
|     let cipher_text = &encrypted[12..]; | ||||
|     let cipher = Aes256Gcm::new(&master_key); | ||||
|     let plain_text = cipher.decrypt(nonce, cipher_text)?; | ||||
|  | ||||
|     String::from_utf8(plain_text).map_err(|err| err.into()) | ||||
| } | ||||
| @@ -2,93 +2,26 @@ | ||||
|     all(not(debug_assertions), target_os = "windows"), | ||||
|     windows_subsystem = "windows" | ||||
| )] | ||||
|  | ||||
| use crate::utils::get_openssl_conf_path; | ||||
| use env_logger::Env; | ||||
| use gpcommon::{Client, ClientStatus, VpnStatus}; | ||||
| use log::{info, warn}; | ||||
| use serde::Serialize; | ||||
| use std::{path::PathBuf, sync::Arc}; | ||||
| use tauri::{Manager, Wry}; | ||||
| use tauri_plugin_log::LogTarget; | ||||
| use tauri_plugin_store::{with_store, StoreCollection}; | ||||
|  | ||||
| mod auth; | ||||
| mod commands; | ||||
| mod crypto; | ||||
| mod settings; | ||||
| mod setup; | ||||
| mod storage; | ||||
| mod utils; | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize)] | ||||
| struct VpnStatusPayload { | ||||
|     status: VpnStatus, | ||||
| } | ||||
|  | ||||
| fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> { | ||||
|     let client = Arc::new(Client::default()); | ||||
|     let client_clone = client.clone(); | ||||
|     let app_handle = app.handle(); | ||||
|  | ||||
|     let stores = app.state::<StoreCollection<Wry>>(); | ||||
|     let path = PathBuf::from(".settings.dat"); | ||||
|     let _ = with_store(app_handle.clone(), stores, path, |store| { | ||||
|         let settings_data = store.get("SETTINGS_DATA"); | ||||
|         let custom_openssl = settings_data.map_or(false, |data| { | ||||
|             data["customOpenSSL"].as_bool().unwrap_or(false) | ||||
|         }); | ||||
|  | ||||
|         if custom_openssl { | ||||
|             info!("Using custom OpenSSL config"); | ||||
|             let openssl_conf = get_openssl_conf_path(&app_handle).into_os_string(); | ||||
|             std::env::set_var("OPENSSL_CONF", openssl_conf); | ||||
|         } | ||||
|         Ok(()) | ||||
|     }); | ||||
|  | ||||
|     tauri::async_runtime::spawn(async move { | ||||
|         client_clone.subscribe_status(move |client_status| match client_status { | ||||
|             ClientStatus::Vpn(vpn_status) => { | ||||
|                 let payload = VpnStatusPayload { status: vpn_status }; | ||||
|                 if let Err(err) = app_handle.emit_all("vpn-status-received", payload) { | ||||
|                     warn!("Error emitting event: {}", err); | ||||
|                 } | ||||
|             } | ||||
|             ClientStatus::Service(is_online) => { | ||||
|                 if let Err(err) = app_handle.emit_all("service-status-changed", is_online) { | ||||
|                     warn!("Error emitting event: {}", err); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         let _ = client_clone.run().await; | ||||
|     }); | ||||
|  | ||||
|     app.manage(client); | ||||
|  | ||||
|     if let Ok(desktop) = std::env::var("XDG_CURRENT_DESKTOP") { | ||||
|         if desktop == "KDE" { | ||||
|             if let Some(main_window) = app.get_window("main") { | ||||
|                 let _ = main_window.set_decorations(false); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| fn main() { | ||||
|     // env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); | ||||
|     tauri::Builder::default() | ||||
|         .plugin( | ||||
|             tauri_plugin_log::Builder::default() | ||||
|                 .targets([ | ||||
|                     LogTarget::LogDir, | ||||
|                     LogTarget::Stdout, /*LogTarget::Webview*/ | ||||
|                 ]) | ||||
|                 .targets([LogTarget::LogDir, LogTarget::Stdout]) | ||||
|                 .level(log::LevelFilter::Info) | ||||
|                 .with_colors(Default::default()) | ||||
|                 .build(), | ||||
|         ) | ||||
|         .plugin(tauri_plugin_store::Builder::default().build()) | ||||
|         .setup(setup) | ||||
|         .setup(setup::setup) | ||||
|         .invoke_handler(tauri::generate_handler![ | ||||
|             commands::service_online, | ||||
|             commands::vpn_status, | ||||
| @@ -98,6 +31,9 @@ fn main() { | ||||
|             commands::os_version, | ||||
|             commands::openssl_config, | ||||
|             commands::update_openssl_config, | ||||
|             commands::store_get, | ||||
|             commands::store_set, | ||||
|             commands::store_save, | ||||
|         ]) | ||||
|         .run(tauri::generate_context!()) | ||||
|         .expect("error while running tauri application"); | ||||
|   | ||||
							
								
								
									
										19
									
								
								gpgui/src-tauri/src/settings.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								gpgui/src-tauri/src/settings.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| use crate::storage::{AppStorage, KeyHint}; | ||||
| use serde::Deserialize; | ||||
| use tauri::{AppHandle, Manager}; | ||||
|  | ||||
| const STORAGE_KEY: &str = "SETTINGS_DATA"; | ||||
|  | ||||
| #[derive(Debug, Deserialize)] | ||||
| struct Settings { | ||||
|     #[serde(rename = "customOpenSSL")] | ||||
|     custom_openssl: bool, | ||||
| } | ||||
|  | ||||
| pub(crate) fn is_custom_openssl_enabled(app_handle: &AppHandle) -> bool { | ||||
|     let app_storage = app_handle.state::<AppStorage>(); | ||||
|     let hint = KeyHint::new(STORAGE_KEY, false); | ||||
|     let settings = app_storage.get::<Settings>(hint); | ||||
|  | ||||
|     settings.map_or(false, |settings| settings.custom_openssl) | ||||
| } | ||||
							
								
								
									
										81
									
								
								gpgui/src-tauri/src/setup.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								gpgui/src-tauri/src/setup.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | ||||
| use crate::{settings, storage::AppStorage, utils::get_openssl_conf_path}; | ||||
| use gpcommon::{Client, ClientStatus, VpnStatus}; | ||||
| use log::{info, warn}; | ||||
| use serde::Serialize; | ||||
| use std::sync::Arc; | ||||
| use tauri::Manager; | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize)] | ||||
| struct VpnStatusPayload { | ||||
|     status: VpnStatus, | ||||
| } | ||||
|  | ||||
| fn setup_window(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> { | ||||
|     let desktop = std::env::var("XDG_CURRENT_DESKTOP")?; | ||||
|     if desktop == "KDE" { | ||||
|         if let Some(main_window) = app.get_window("main") { | ||||
|             let _ = main_window.set_decorations(false); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| fn setup_app_storage(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> { | ||||
|     let app_handle = app.app_handle(); | ||||
|     let app_storage = AppStorage::new(app_handle); | ||||
|  | ||||
|     app.manage(app_storage); | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| fn setup_app_env(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> { | ||||
|     let app_handle = app.app_handle(); | ||||
|     let use_custom_openssl = settings::is_custom_openssl_enabled(&app_handle); | ||||
|  | ||||
|     if use_custom_openssl { | ||||
|         info!("Using custom OpenSSL config"); | ||||
|  | ||||
|         let openssl_conf = get_openssl_conf_path(&app_handle).into_os_string(); | ||||
|         std::env::set_var("OPENSSL_CONF", openssl_conf); | ||||
|     } | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| fn setup_vpn_client(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> { | ||||
|     let app_handle = app.handle(); | ||||
|     let client = Arc::new(Client::default()); | ||||
|     let client_clone = client.clone(); | ||||
|  | ||||
|     app.manage(client_clone); | ||||
|  | ||||
|     tauri::async_runtime::spawn(async move { | ||||
|         client.subscribe_status(move |client_status| match client_status { | ||||
|             ClientStatus::Vpn(vpn_status) => { | ||||
|                 let payload = VpnStatusPayload { status: vpn_status }; | ||||
|                 if let Err(err) = app_handle.emit_all("vpn-status-received", payload) { | ||||
|                     warn!("Error emitting event: {}", err); | ||||
|                 } | ||||
|             } | ||||
|             ClientStatus::Service(is_online) => { | ||||
|                 if let Err(err) = app_handle.emit_all("service-status-changed", is_online) { | ||||
|                     warn!("Error emitting event: {}", err); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|         let _ = client.run().await; | ||||
|     }); | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| pub(crate) fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> { | ||||
|     setup_window(app)?; | ||||
|     setup_app_storage(app)?; | ||||
|     setup_app_env(app)?; | ||||
|     setup_vpn_client(app)?; | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
							
								
								
									
										87
									
								
								gpgui/src-tauri/src/storage.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								gpgui/src-tauri/src/storage.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,87 @@ | ||||
| use crate::crypto::{decrypt, encrypt}; | ||||
| use log::warn; | ||||
| use serde::{de::DeserializeOwned, Deserialize}; | ||||
| use std::fmt::Debug; | ||||
| use tauri::{AppHandle, Manager, Wry}; | ||||
| use tauri_plugin_store::{with_store, Error, StoreCollection}; | ||||
|  | ||||
| const STORE_PATH: &str = ".data.json"; | ||||
|  | ||||
| #[derive(Debug, Deserialize)] | ||||
| pub(crate) struct KeyHint<'a> { | ||||
|     key: &'a str, | ||||
|     encrypted: bool, | ||||
| } | ||||
|  | ||||
| impl<'a> KeyHint<'a> { | ||||
|     pub(crate) fn new(key: &'a str, encrypted: bool) -> Self { | ||||
|         Self { key, encrypted } | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub(crate) struct AppStorage<'a> { | ||||
|     path: &'a str, | ||||
|     app_handle: AppHandle<Wry>, | ||||
| } | ||||
|  | ||||
| impl AppStorage<'_> { | ||||
|     pub(crate) fn new(app_handle: AppHandle<Wry>) -> Self { | ||||
|         Self { | ||||
|             path: STORE_PATH, | ||||
|             app_handle, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn get<T: DeserializeOwned + Debug>(&self, hint: KeyHint) -> Option<T> { | ||||
|         let stores = self.app_handle.state::<StoreCollection<Wry>>(); | ||||
|         with_store(self.app_handle.clone(), stores, self.path, |store| { | ||||
|             store | ||||
|                 .get(hint.key) | ||||
|                 .ok_or_else(|| Error::Deserialize("Value not found".into())) | ||||
|                 .and_then(|value| { | ||||
|                     if !hint.encrypted { | ||||
|                         return Ok(serde_json::from_value::<T>(value.clone())?); | ||||
|                     } | ||||
|  | ||||
|                     let value = value | ||||
|                         .as_str() | ||||
|                         .ok_or_else(|| Error::Deserialize("Value is not a string".into()))?; | ||||
|                     let value = decrypt(value).map_err(|err| { | ||||
|                         Error::Deserialize(format!("Failed to decrypt value: {}", err).into()) | ||||
|                     })?; | ||||
|  | ||||
|                     Ok(serde_json::from_str::<T>(&value)?) | ||||
|                 }) | ||||
|         }) | ||||
|         .map_err(|err| warn!("Error getting value: {:?}", err)) | ||||
|         .ok() | ||||
|     } | ||||
|  | ||||
|     pub fn set<T: serde::Serialize>(&self, hint: KeyHint, value: &T) -> Result<(), Error> { | ||||
|         let stores = self.app_handle.state::<StoreCollection<Wry>>(); | ||||
|  | ||||
|         with_store(self.app_handle.clone(), stores, self.path, |store| { | ||||
|             let value = if hint.encrypted { | ||||
|                 let json_str = serde_json::to_string(value)?; | ||||
|                 let encrypted = encrypt(&json_str).map_err(|err| { | ||||
|                     Error::Serialize(format!("Failed to encrypt value: {}", err).into()) | ||||
|                 })?; | ||||
|                 serde_json::to_value(encrypted)? | ||||
|             } else { | ||||
|                 serde_json::to_value(value)? | ||||
|             }; | ||||
|  | ||||
|             store.insert(hint.key.to_string(), value)?; | ||||
|             Ok(()) | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     pub fn save(&self) -> Result<(), Error> { | ||||
|         let stores = self.app_handle.state::<StoreCollection<Wry>>(); | ||||
|          | ||||
|         with_store(self.app_handle.clone(), stores, self.path, |store| { | ||||
|             store.save()?; | ||||
|             Ok(()) | ||||
|         }) | ||||
|     } | ||||
| } | ||||
| @@ -14,7 +14,11 @@ export const portalGatewaysAtom = atom<GatewayData[]>((get) => { | ||||
| }); | ||||
|  | ||||
| export const selectedGatewayAtom = atom( | ||||
|   (get) => get(currentPortalDataAtom).selectedGateway, | ||||
|   (get) => { | ||||
|     const { selectedGateway } = get(currentPortalDataAtom); | ||||
|     const gateways = get(portalGatewaysAtom); | ||||
|     return gateways.find(({ name }) => name === selectedGateway); | ||||
|   }, | ||||
|   async (get, set, update: string) => { | ||||
|     const portalData = get(currentPortalDataAtom); | ||||
|     await set(updatePortalDataAtom, { ...portalData, selectedGateway: update }); | ||||
|   | ||||
| @@ -62,7 +62,7 @@ export const loginPortalAtom = atom( | ||||
|     } | ||||
|  | ||||
|     // Here, we have got the portal config successfully, refresh the cached portal data | ||||
|     const previousSelectedGateway = get(selectedGatewayAtom); | ||||
|     const previousSelectedGateway = get(selectedGatewayAtom)?.name; | ||||
|     const selectedGateway = gateways.find( | ||||
|       ({ name }) => name === previousSelectedGateway | ||||
|     ); | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { atom } from "jotai"; | ||||
| import { atomWithDefault } from "jotai/utils"; | ||||
| import { PortalCredential } from "../services/portalService"; | ||||
| import { atomWithTauriStorage } from "../services/storeService"; | ||||
| import { atomWithTauriStorage } from "../services/storageService"; | ||||
| import { unwrap } from "./unwrap"; | ||||
|  | ||||
| export type GatewayData = { | ||||
| @@ -31,7 +31,11 @@ const DEFAULT_APP_DATA: AppData = { | ||||
|   clearCookies: true, | ||||
| }; | ||||
|  | ||||
| export const appDataAtom = atomWithTauriStorage("APP_DATA", DEFAULT_APP_DATA); | ||||
| const keyHint = { | ||||
|   key: "APP_DATA", | ||||
|   encrypted: true, | ||||
| }; | ||||
| export const appDataAtom = atomWithTauriStorage(keyHint, DEFAULT_APP_DATA); | ||||
| const unwrappedAppDataAtom = atom( | ||||
|   (get) => get(unwrap(appDataAtom)) || DEFAULT_APP_DATA | ||||
| ); | ||||
| @@ -51,6 +55,11 @@ export const currentPortalDataAtom = atom<PortalData>((get) => { | ||||
|   return portalData || { address: portalAddress, gateways: [] }; | ||||
| }); | ||||
|  | ||||
| export const allPortalsAtom = atom((get) => { | ||||
|   const { portals } = get(unwrappedAppDataAtom); | ||||
|   return portals.map(({ address }) => address); | ||||
| }); | ||||
|  | ||||
| export const updatePortalDataAtom = atom( | ||||
|   null, | ||||
|   async (get, set, update: PortalData) => { | ||||
|   | ||||
| @@ -5,7 +5,7 @@ import settingsService, { | ||||
|   DEFAULT_SETTINGS_DATA, | ||||
|   SETTINGS_DATA, | ||||
| } from "../services/settingsService"; | ||||
| import { atomWithTauriStorage } from "../services/storeService"; | ||||
| import { atomWithTauriStorage } from "../services/storageService"; | ||||
| import { unwrap } from "./unwrap"; | ||||
|  | ||||
| const settingsDataAtom = atomWithTauriStorage( | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import vpnService from "../services/vpnService"; | ||||
| import { selectedGatewayAtom, switchGatewayAtom } from "./gateway"; | ||||
| import { notifyErrorAtom, notifySuccessAtom } from "./notification"; | ||||
| import { unwrap } from "./unwrap"; | ||||
| import { portalAddressAtom } from "./portal"; | ||||
|  | ||||
| export type Status = | ||||
|   | "disconnected" | ||||
| @@ -75,14 +76,16 @@ export const statusTextAtom = atom<string>((get) => { | ||||
|  | ||||
|   if (status === "connected") { | ||||
|     const selectedGateway = get(selectedGatewayAtom); | ||||
|     return selectedGateway | ||||
|       ? `Gateway: ${selectedGateway}` | ||||
|       : statusTextMap[status]; | ||||
|     const portalAddress = get(portalAddressAtom); | ||||
|  | ||||
|     return selectedGateway?.address === portalAddress | ||||
|       ? statusTextMap[status] | ||||
|       : selectedGateway?.address!; | ||||
|   } | ||||
|  | ||||
|   if (switchingGateway) { | ||||
|     const selectedGateway = get(selectedGatewayAtom); | ||||
|     return `Switching to ${selectedGateway}`; | ||||
|     return `Switching to ${selectedGateway?.name}`; | ||||
|   } | ||||
|  | ||||
|   return statusTextMap[status]; | ||||
|   | ||||
| @@ -1,13 +1,8 @@ | ||||
| import { | ||||
|   Box, | ||||
|   CssBaseline, | ||||
|   ThemeProvider, | ||||
|   createTheme, | ||||
|   useMediaQuery, | ||||
| } from "@mui/material"; | ||||
| import React, { Suspense, useMemo } from "react"; | ||||
| import { Box, CssBaseline, ThemeProvider } from "@mui/material"; | ||||
| import React, { Suspense } from "react"; | ||||
| import { createRoot } from "react-dom/client"; | ||||
| import "./styles.css"; | ||||
| import useGlobalTheme from "./useGlobalTheme"; | ||||
|  | ||||
| function Loading() { | ||||
|   console.warn("Loading rendered"); | ||||
| @@ -27,16 +22,7 @@ function Loading() { | ||||
| } | ||||
|  | ||||
| function AppShell({ children }: { children: React.ReactNode }) { | ||||
|   const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)"); | ||||
|   const theme = useMemo( | ||||
|     () => | ||||
|       createTheme({ | ||||
|         palette: { | ||||
|           mode: prefersDarkMode ? "dark" : "light", | ||||
|         }, | ||||
|       }), | ||||
|     [prefersDarkMode] | ||||
|   ); | ||||
|   const theme = useGlobalTheme(); | ||||
|  | ||||
|   return ( | ||||
|     <React.StrictMode> | ||||
|   | ||||
							
								
								
									
										31
									
								
								gpgui/src/components/AppShell/useGlobalTheme.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								gpgui/src/components/AppShell/useGlobalTheme.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| import { createTheme, useMediaQuery } from "@mui/material"; | ||||
| import { useMemo } from "react"; | ||||
|  | ||||
| export default function useGlobalTheme() { | ||||
|   const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)"); | ||||
|   return useMemo( | ||||
|     () => | ||||
|       createTheme({ | ||||
|         palette: { | ||||
|           mode: prefersDarkMode ? "dark" : "light", | ||||
|         }, | ||||
|         components: { | ||||
|           MuiButton: { | ||||
|             styleOverrides: { | ||||
|               root: { | ||||
|                 textTransform: "none", | ||||
|               }, | ||||
|             }, | ||||
|           }, | ||||
|           MuiTab: { | ||||
|             styleOverrides: { | ||||
|               root: { | ||||
|                 textTransform: "none", | ||||
|               }, | ||||
|             }, | ||||
|           }, | ||||
|         }, | ||||
|       }), | ||||
|     [prefersDarkMode] | ||||
|   ); | ||||
| } | ||||
| @@ -57,7 +57,7 @@ export default function PasswordAuth() { | ||||
|             <Button | ||||
|               variant="outlined" | ||||
|               onClick={cancelPasswordAuth} | ||||
|               sx={{ flex: 1, textTransform: "none" }} | ||||
|               sx={{ flex: 1 }} | ||||
|             > | ||||
|               Cancel | ||||
|             </Button> | ||||
| @@ -66,7 +66,6 @@ export default function PasswordAuth() { | ||||
|               type="submit" | ||||
|               loading={loading} | ||||
|               disabled={loading} | ||||
|               sx={{ flex: 1, textTransform: "none" }} | ||||
|             > | ||||
|               Login | ||||
|             </LoadingButton> | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| import { Button, TextField } from "@mui/material"; | ||||
| import { Autocomplete, Button, TextField } from "@mui/material"; | ||||
| import { useAtom, useAtomValue, useSetAtom } from "jotai"; | ||||
| import { ChangeEvent } from "react"; | ||||
| import { ChangeEvent, useState } from "react"; | ||||
| import { | ||||
|   cancelConnectPortalAtom, | ||||
|   connectPortalAtom, | ||||
| } from "../../atoms/connectPortal"; | ||||
| import { switchGatewayAtom } from "../../atoms/gateway"; | ||||
| import { portalAddressAtom } from "../../atoms/portal"; | ||||
| import { allPortalsAtom, portalAddressAtom } from "../../atoms/portal"; | ||||
| import { | ||||
|   backgroundServiceStartedAtom, | ||||
|   isProcessingAtom, | ||||
| @@ -26,6 +26,7 @@ function normalizePortalAddress(input: string) { | ||||
|  | ||||
| export default function PortalForm() { | ||||
|   const backgroundServiceStarted = useAtomValue(backgroundServiceStartedAtom); | ||||
|   const allPortals = useAtomValue(allPortalsAtom); | ||||
|   const [portalAddress, setPortalAddress] = useAtom(portalAddressAtom); | ||||
|   // Use useAtom instead of useSetAtom, otherwise the onMount of the atom is not triggered | ||||
|   const [, connectPortal] = useAtom(connectPortalAtom); | ||||
| @@ -35,8 +36,10 @@ export default function PortalForm() { | ||||
|   const disconnectVpn = useSetAtom(disconnectVpnAtom); | ||||
|   const switchingGateway = useAtomValue(switchGatewayAtom); | ||||
|  | ||||
|   function handlePortalAddressChange(e: ChangeEvent<HTMLInputElement>) { | ||||
|     setPortalAddress(normalizePortalAddress(e.target.value)); | ||||
|   const readOnly = status !== "disconnected" || switchingGateway; | ||||
|  | ||||
|   function handlePortalAddressChange(e: unknown, value: string) { | ||||
|     setPortalAddress(normalizePortalAddress(value)); | ||||
|   } | ||||
|  | ||||
|   function handleSubmit(e: ChangeEvent<HTMLFormElement>) { | ||||
| @@ -46,16 +49,35 @@ export default function PortalForm() { | ||||
|  | ||||
|   return ( | ||||
|     <form onSubmit={handleSubmit} data-tauri-drag-region> | ||||
|       <TextField | ||||
|         autoFocus | ||||
|         label="Portal address" | ||||
|         placeholder="Hostname or IP address" | ||||
|         fullWidth | ||||
|       <Autocomplete | ||||
|         freeSolo | ||||
|         options={allPortals} | ||||
|         inputValue={portalAddress} | ||||
|         onInputChange={handlePortalAddressChange} | ||||
|         readOnly={readOnly} | ||||
|         forcePopupIcon={!readOnly} | ||||
|         disableClearable | ||||
|         size="small" | ||||
|         value={portalAddress} | ||||
|         onChange={handlePortalAddressChange} | ||||
|         InputProps={{ readOnly: status !== "disconnected" || switchingGateway }} | ||||
|         sx={{ mb: 1 }} | ||||
|         sx={{ | ||||
|           mb: 1, | ||||
|         }} | ||||
|         componentsProps={{ | ||||
|           paper: { | ||||
|             sx: { | ||||
|               "& .MuiAutocomplete-listbox .MuiAutocomplete-option": { | ||||
|                 minHeight: "auto", | ||||
|               }, | ||||
|             }, | ||||
|           }, | ||||
|         }} | ||||
|         renderInput={(params) => ( | ||||
|           <TextField | ||||
|             {...params} | ||||
|             autoFocus | ||||
|             label="Portal address" | ||||
|             placeholder="Hostname or IP address" | ||||
|           /> | ||||
|         )} | ||||
|       /> | ||||
|  | ||||
|       {status === "disconnected" && !switchingGateway && ( | ||||
| @@ -64,7 +86,6 @@ export default function PortalForm() { | ||||
|           type="submit" | ||||
|           variant="contained" | ||||
|           disabled={!backgroundServiceStarted} | ||||
|           sx={{ textTransform: "none" }} | ||||
|         > | ||||
|           Connect | ||||
|         </Button> | ||||
| @@ -81,19 +102,13 @@ export default function PortalForm() { | ||||
|             switchingGateway | ||||
|           } | ||||
|           onClick={cancelConnectPortal} | ||||
|           sx={{ textTransform: "none" }} | ||||
|         > | ||||
|           Cancel | ||||
|         </Button> | ||||
|       )} | ||||
|  | ||||
|       {status === "connected" && ( | ||||
|         <Button | ||||
|           fullWidth | ||||
|           variant="contained" | ||||
|           onClick={disconnectVpn} | ||||
|           sx={{ textTransform: "none" }} | ||||
|         > | ||||
|         <Button fullWidth variant="contained" onClick={disconnectVpn}> | ||||
|           Disconnect | ||||
|         </Button> | ||||
|       )} | ||||
|   | ||||
| @@ -1,7 +1,18 @@ | ||||
| import { GppBad, VerifiedUser as VerifiedIcon } from "@mui/icons-material"; | ||||
| import { Box, CircularProgress, styled, useTheme } from "@mui/material"; | ||||
| import { useAtomValue } from "jotai"; | ||||
| import { | ||||
|   Box, | ||||
|   Button, | ||||
|   CircularProgress, | ||||
|   Tooltip, | ||||
|   styled, | ||||
|   useTheme, | ||||
| } from "@mui/material"; | ||||
| import { useAtomValue, useSetAtom } from "jotai"; | ||||
| import { BeatLoader } from "react-spinners"; | ||||
| import { | ||||
|   openGatewaySwitcherAtom, | ||||
|   selectedGatewayAtom, | ||||
| } from "../../atoms/gateway"; | ||||
| import { isProcessingAtom, statusAtom } from "../../atoms/status"; | ||||
|  | ||||
| function useStatusColor() { | ||||
| @@ -59,11 +70,48 @@ function ProcessingIcon() { | ||||
|   return <BeatLoader color={theme.palette.info.main} />; | ||||
| } | ||||
|  | ||||
| const ConnectedIcon = styled(VerifiedIcon)(({ theme }) => ({ | ||||
|   position: "relative", | ||||
|   fontSize: 80, | ||||
|   color: theme.palette.success.main, | ||||
| })); | ||||
| const ConnectedIcon = () => { | ||||
|   const selectedGateway = useAtomValue(selectedGatewayAtom); | ||||
|   const openGatewaySwitcher = useSetAtom(openGatewaySwitcherAtom); | ||||
|  | ||||
|   return ( | ||||
|     <Box | ||||
|       sx={{ | ||||
|         display: "flex", | ||||
|         flexDirection: "column", | ||||
|         alignItems: "center", | ||||
|       }} | ||||
|     > | ||||
|       <VerifiedIcon | ||||
|         sx={{ | ||||
|           fontSize: 70, | ||||
|           color: (theme) => theme.palette.success.main, | ||||
|         }} | ||||
|       /> | ||||
|       <Tooltip title={`Connected to ${selectedGateway?.name}`}> | ||||
|         <Button | ||||
|           sx={{ | ||||
|             position: "relative", | ||||
|             zIndex: 1, | ||||
|             fontSize: "0.75rem", | ||||
|             fontWeight: "bold", | ||||
|             display: "block", | ||||
|             width: 100, | ||||
|             mt: 0.2, | ||||
|             padding: 0.2, | ||||
|             overflow: "hidden", | ||||
|             textOverflow: "ellipsis", | ||||
|           }} | ||||
|           size="small" | ||||
|           color="success" | ||||
|           onClick={openGatewaySwitcher} | ||||
|         > | ||||
|           {selectedGateway?.name} | ||||
|         </Button> | ||||
|       </Tooltip> | ||||
|     </Box> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| const IconContainer = styled(Box)(({ theme }) => | ||||
|   theme.unstable_sx({ | ||||
|   | ||||
| @@ -20,7 +20,7 @@ export default function GatewaySwitcher() { | ||||
|     gatewaySwitcherVisibleAtom | ||||
|   ); | ||||
|   const gateways = useAtomValue(portalGatewaysAtom); | ||||
|   const selectedGateway = useAtomValue(selectedGatewayAtom); | ||||
|   const selectedGateway = useAtomValue(selectedGatewayAtom)?.name; | ||||
|   const switchGateway = useSetAtom(switchGatewayAtom); | ||||
|  | ||||
|   const handleClose = () => { | ||||
|   | ||||
| @@ -43,14 +43,12 @@ export default function SettingsPanel() { | ||||
|               value="simulation" | ||||
|               icon={<Devices />} | ||||
|               iconPosition="start" | ||||
|               sx={{ textTransform: "none" }} | ||||
|             /> | ||||
|             <Tab | ||||
|               label="OpenSSL" | ||||
|               value="openssl" | ||||
|               icon={<Https />} | ||||
|               iconPosition="start" | ||||
|               sx={{ textTransform: "none" }} | ||||
|             /> | ||||
|           </TabList> | ||||
|           <Box sx={{ flex: 1 }}> | ||||
| @@ -61,12 +59,8 @@ export default function SettingsPanel() { | ||||
|       </Box> | ||||
|       <Box sx={{ flexShrink: 0, borderTop: 1, borderColor: "divider" }}> | ||||
|         <DialogActions> | ||||
|           <Button sx={{ textTransform: "none" }} onClick={closeWindow}> | ||||
|             Cancel | ||||
|           </Button> | ||||
|           <Button sx={{ textTransform: "none" }} onClick={save}> | ||||
|             Save | ||||
|           </Button> | ||||
|           <Button onClick={closeWindow}>Cancel</Button> | ||||
|           <Button onClick={save}>Save</Button> | ||||
|         </DialogActions> | ||||
|       </Box> | ||||
|     </Box> | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| import { Box } from "@mui/material"; | ||||
| import { useAtomValue } from "jotai"; | ||||
| import { appDataAtom } from "../atoms/portal"; | ||||
| import { renderToRoot } from "../components/AppShell"; | ||||
| import ConnectForm from "../components/ConnectForm"; | ||||
| import ConnectionStatus from "../components/ConnectionStatus"; | ||||
| @@ -8,6 +10,10 @@ import MainMenu from "../components/MainMenu"; | ||||
| import Notification from "../components/Notification"; | ||||
|  | ||||
| export default function App() { | ||||
|   // Use the this atom to trigger loading data from the storage | ||||
|   // And render the loading indicator | ||||
|   useAtomValue(appDataAtom); | ||||
|  | ||||
|   return ( | ||||
|     <Box data-tauri-drag-region padding={2} paddingBottom={0}> | ||||
|       <MainMenu /> | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { emit, listen } from "@tauri-apps/api/event"; | ||||
| import invokeCommand from "../utils/invokeCommand"; | ||||
| import settingsService from "./settingsService"; | ||||
|  | ||||
| export type AuthData = { | ||||
|   username: string; | ||||
| @@ -30,9 +31,12 @@ class AuthService { | ||||
|  | ||||
|   // binding: "POST" | "REDIRECT" | ||||
|   async samlLogin(binding: string, request: string, clearCookies: boolean) { | ||||
|     const { userAgent } = await settingsService.getSimulation(); | ||||
|  | ||||
|     return invokeCommand<AuthData>("saml_login", { | ||||
|       binding, | ||||
|       request, | ||||
|       userAgent: `${userAgent} ${navigator.userAgent}`, | ||||
|       clearCookies, | ||||
|     }); | ||||
|   } | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { Body, ResponseType, fetch } from "@tauri-apps/api/http"; | ||||
| import { parseXml } from "../utils/parseXml"; | ||||
| import settingsService from "./settingsService"; | ||||
|  | ||||
| type LoginParams = { | ||||
|   user: string; | ||||
| @@ -15,6 +16,9 @@ class GatewayService { | ||||
|       throw new Error("Gateway address is required"); | ||||
|     } | ||||
|  | ||||
|     const { userAgent, clientOS, osVersion } = | ||||
|       await settingsService.getSimulation(); | ||||
|  | ||||
|     const loginUrl = `https://${gateway}/ssl-vpn/login.esp`; | ||||
|     const body = Body.form({ | ||||
|       prot: "https:", | ||||
| @@ -25,8 +29,8 @@ class GatewayService { | ||||
|       direct: "yes", | ||||
|       "ipv6-support": "yes", | ||||
|       clientVer: "4100", | ||||
|       clientos: "Linux", | ||||
|       "os-version": "Linux", | ||||
|       clientos: clientOS, | ||||
|       "os-version": osVersion, | ||||
|       server: gateway, | ||||
|       user, | ||||
|       passwd: passwd || "", | ||||
| @@ -38,7 +42,7 @@ class GatewayService { | ||||
|     const response = await fetch<string>(loginUrl, { | ||||
|       method: "POST", | ||||
|       headers: { | ||||
|         "User-Agent": "PAN GlobalProtect", | ||||
|         "User-Agent": userAgent, | ||||
|       }, | ||||
|       responseType: ResponseType.Text, | ||||
|       body, | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import { Body, ResponseType, fetch } from "@tauri-apps/api/http"; | ||||
| import ErrorWithTitle from "../utils/ErrorWithTitle"; | ||||
| import { parseXml } from "../utils/parseXml"; | ||||
| import { Gateway } from "./types"; | ||||
| import settingsService from "./settingsService"; | ||||
|  | ||||
| export type SamlPrelogin = { | ||||
|   isSamlAuth: true; | ||||
| @@ -37,12 +38,15 @@ export type PortalCredential = { | ||||
| class PortalService { | ||||
|   async prelogin(portal: string): Promise<Prelogin> { | ||||
|     const preloginUrl = `https://${portal}/global-protect/prelogin.esp`; | ||||
|     const { userAgent, clientOS, osVersion } = | ||||
|       await settingsService.getSimulation(); | ||||
|  | ||||
|     let response; | ||||
|     try { | ||||
|       response = await fetch<string>(preloginUrl, { | ||||
|         method: "POST", | ||||
|         headers: { | ||||
|           "User-Agent": "PAN GlobalProtect", | ||||
|           "User-Agent": userAgent, | ||||
|         }, | ||||
|         responseType: ResponseType.Text, | ||||
|         query: { | ||||
| @@ -51,8 +55,8 @@ class PortalService { | ||||
|         body: Body.form({ | ||||
|           tmp: "tmp", | ||||
|           clientVer: "4100", | ||||
|           clientos: "Linux", | ||||
|           "os-version": "Linux", | ||||
|           clientos: clientOS, | ||||
|           "os-version": osVersion, | ||||
|           "ipv6-support": "yes", | ||||
|           "default-browser": "0", | ||||
|           "cas-support": "yes", | ||||
| @@ -118,6 +122,9 @@ class PortalService { | ||||
|   } | ||||
|  | ||||
|   async fetchConfig(portal: string, params: PortalCredential) { | ||||
|     const { userAgent, clientOS, osVersion, clientVersion } = | ||||
|       await settingsService.getSimulation(); | ||||
|  | ||||
|     const { | ||||
|       user, | ||||
|       passwd, | ||||
| @@ -132,12 +139,12 @@ class PortalService { | ||||
|       inputStr: "", | ||||
|       jnlpReady: "jnlpReady", | ||||
|       computer: "Linux", // TODO | ||||
|       clientos: "Linux", | ||||
|       clientos: clientOS, | ||||
|       ok: "Login", | ||||
|       direct: "yes", | ||||
|       clientVer: "4100", | ||||
|       "os-version": "Linux", | ||||
|       clientgpversion: "6.0.1-19", | ||||
|       "os-version": osVersion, | ||||
|       clientgpversion: clientVersion, | ||||
|       "ipv6-support": "yes", | ||||
|       server: portal, | ||||
|       host: portal, | ||||
| @@ -151,7 +158,7 @@ class PortalService { | ||||
|     const response = await fetch<string>(configUrl, { | ||||
|       method: "POST", | ||||
|       headers: { | ||||
|         "User-Agent": "PAN GlobalProtect", | ||||
|         "User-Agent": userAgent, | ||||
|       }, | ||||
|       responseType: ResponseType.Text, | ||||
|       body, | ||||
| @@ -166,8 +173,6 @@ class PortalService { | ||||
|   } | ||||
|  | ||||
|   private parsePortalConfigResponse(response: string): PortalConfig { | ||||
|     // console.log(response); | ||||
|  | ||||
|     const result = parseXml(response); | ||||
|     const gateways = result.all("gateways list > entry").map((entry) => { | ||||
|       const address = entry.attr("name"); | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import { UserAttentionType, WebviewWindow } from "@tauri-apps/api/window"; | ||||
| import invokeCommand from "../utils/invokeCommand"; | ||||
| import { appStore } from "./storeService"; | ||||
| import { appStorage } from "./storageService"; | ||||
|  | ||||
| export type TabValue = "simulation" | "openssl"; | ||||
| const SETTINGS_WINDOW_LABEL = "settings"; | ||||
| @@ -68,9 +68,10 @@ export const DEFAULT_SETTINGS_DATA: SettingsData = { | ||||
|   customOpenSSL: false, | ||||
| }; | ||||
|  | ||||
| async function getSimulationSettings(): Promise<SimulationSettings> { | ||||
| async function getSimulation(): Promise<SimulationSettings> { | ||||
|   const { clientOS, osVersion, clientVersion } = | ||||
|     (await appStore.get<SettingsData>(SETTINGS_DATA)) || DEFAULT_SETTINGS_DATA; | ||||
|     (await appStorage.get<SettingsData>(SETTINGS_DATA)) || | ||||
|     DEFAULT_SETTINGS_DATA; | ||||
|   const currentOsVersion = await getCurrentOsVersion(); | ||||
|  | ||||
|   return { | ||||
| @@ -81,8 +82,8 @@ async function getSimulationSettings(): Promise<SimulationSettings> { | ||||
|       clientVersion | ||||
|     ), | ||||
|     clientOS, | ||||
|     osVersion, | ||||
|     clientVersion, | ||||
|     osVersion: determineOsVersion(clientOS, osVersion, currentOsVersion), | ||||
|     clientVersion: clientVersion || DEFAULT_CLIENT_VERSION, | ||||
|   }; | ||||
| } | ||||
|  | ||||
| @@ -131,7 +132,7 @@ export default { | ||||
|   openSettings, | ||||
|   closeSettings, | ||||
|   getCurrentOsVersion, | ||||
|   getSimulationSettings, | ||||
|   getSimulation, | ||||
|   buildUserAgent, | ||||
|   determineOsVersion, | ||||
|   getOpenSSLConfig, | ||||
|   | ||||
							
								
								
									
										70
									
								
								gpgui/src/services/storageService.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								gpgui/src/services/storageService.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | ||||
| import { atom } from "jotai"; | ||||
| import { RESET, atomWithDefault } from "jotai/utils"; | ||||
| import invokeCommand from "../utils/invokeCommand"; | ||||
|  | ||||
| type SetStateActionWithReset<T> = | ||||
|   | T | ||||
|   | typeof RESET | ||||
|   | ((prev: T) => T | typeof RESET); | ||||
|  | ||||
| type KeyHint = | ||||
|   | { | ||||
|       key: string; | ||||
|       encrypted: boolean; | ||||
|     } | ||||
|   | string; | ||||
|  | ||||
| type AppStorage = { | ||||
|   get: <T>(key: KeyHint) => Promise<T | undefined>; | ||||
|   set: <T>(key: KeyHint, value: T) => Promise<void>; | ||||
|   save: () => Promise<void>; | ||||
| }; | ||||
|  | ||||
| export const appStorage: AppStorage = { | ||||
|   get: async (key) => { | ||||
|     const hint = typeof key === "string" ? { key, encrypted: false } : key; | ||||
|     return invokeCommand("store_get", { hint }); | ||||
|   }, | ||||
|   set: async (key, value) => { | ||||
|     const hint = typeof key === "string" ? { key, encrypted: false } : key; | ||||
|     return invokeCommand("store_set", { hint, value }); | ||||
|   }, | ||||
|   save: async () => { | ||||
|     return invokeCommand("store_save"); | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| export function atomWithTauriStorage<T>(key: KeyHint, initialValue: T) { | ||||
|   const baseAtom = atomWithDefault<T | Promise<T>>(async () => { | ||||
|     const storedValue = await appStorage.get<T>(key); | ||||
|     if (!storedValue) { | ||||
|       return initialValue; | ||||
|     } | ||||
|     return storedValue; | ||||
|   }); | ||||
|  | ||||
|   const anAtom = atom( | ||||
|     (get) => get(baseAtom), | ||||
|     async (get, set, update: SetStateActionWithReset<T>) => { | ||||
|       const value = await get(baseAtom); | ||||
|       let newValue: T | typeof RESET; | ||||
|       if (typeof update === "function") { | ||||
|         newValue = (update as (prev: T) => T | typeof RESET)(value); | ||||
|       } else { | ||||
|         newValue = update as T | typeof RESET; | ||||
|       } | ||||
|  | ||||
|       if (newValue === RESET) { | ||||
|         set(baseAtom, initialValue); | ||||
|         await appStorage.set(key, initialValue); | ||||
|       } else { | ||||
|         set(baseAtom, newValue); | ||||
|         await appStorage.set(key, newValue); | ||||
|       } | ||||
|  | ||||
|       await appStorage.save(); | ||||
|     } | ||||
|   ); | ||||
|  | ||||
|   return anAtom; | ||||
| } | ||||
| @@ -1,45 +0,0 @@ | ||||
| import { atom } from "jotai"; | ||||
| import { RESET, atomWithDefault } from "jotai/utils"; | ||||
| import { Store } from "tauri-plugin-store-api"; | ||||
|  | ||||
| type SetStateActionWithReset<T> = | ||||
|   | T | ||||
|   | typeof RESET | ||||
|   | ((prev: T) => T | typeof RESET); | ||||
|  | ||||
| export const appStore = new Store(".settings.dat"); | ||||
|  | ||||
| export function atomWithTauriStorage<T>(key: string, initialValue: T) { | ||||
|   const baseAtom = atomWithDefault<T | Promise<T>>(async () => { | ||||
|     const storedValue = await appStore.get<T>(key); | ||||
|     if (!storedValue) { | ||||
|       return initialValue; | ||||
|     } | ||||
|     return storedValue; | ||||
|   }); | ||||
|  | ||||
|   const anAtom = atom( | ||||
|     (get) => get(baseAtom), | ||||
|     async (get, set, update: SetStateActionWithReset<T>) => { | ||||
|       const value = await get(baseAtom); | ||||
|       let newValue: T | typeof RESET; | ||||
|       if (typeof update === "function") { | ||||
|         newValue = (update as (prev: T) => T | typeof RESET)(value); | ||||
|       } else { | ||||
|         newValue = update as T | typeof RESET; | ||||
|       } | ||||
|  | ||||
|       if (newValue === RESET) { | ||||
|         set(baseAtom, initialValue); | ||||
|         await appStore.set(key, initialValue); | ||||
|       } else { | ||||
|         set(baseAtom, newValue); | ||||
|         await appStore.set(key, newValue); | ||||
|       } | ||||
|  | ||||
|       await appStore.save(); | ||||
|     } | ||||
|   ); | ||||
|  | ||||
|   return anAtom; | ||||
| } | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { Event, listen } from "@tauri-apps/api/event"; | ||||
| import invokeCommand from "../utils/invokeCommand"; | ||||
| import settingsService from "./settingsService"; | ||||
|  | ||||
| type VpnStatus = "disconnected" | "connecting" | "connected" | "disconnecting"; | ||||
| type VpnStatusCallback = (status: VpnStatus) => void; | ||||
| @@ -64,7 +65,8 @@ class VpnService { | ||||
|   } | ||||
|  | ||||
|   async connect(server: string, cookie: string) { | ||||
|     return invokeCommand("vpn_connect", { server, cookie }); | ||||
|     const { userAgent } = await settingsService.getSimulation(); | ||||
|     return invokeCommand("vpn_connect", { server, cookie, userAgent }); | ||||
|   } | ||||
|  | ||||
|   async disconnect() { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user