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