|
|
|
|
@@ -7,9 +7,9 @@ use rand_core::OsRng;
|
|
|
|
|
use reqwest::Client;
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
use tauri::{
|
|
|
|
|
menu::{MenuBuilder, MenuItemBuilder},
|
|
|
|
|
menu::{MenuBuilder, MenuItem, MenuItemBuilder},
|
|
|
|
|
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
|
|
|
|
|
AppHandle, Manager, State, WebviewWindow, Window, WindowEvent,
|
|
|
|
|
AppHandle, Manager, State, WebviewWindow, Window, WindowEvent, Wry,
|
|
|
|
|
};
|
|
|
|
|
use x25519_dalek::{PublicKey, StaticSecret};
|
|
|
|
|
|
|
|
|
|
@@ -19,9 +19,17 @@ const SINGLE_INSTANCE_ADDR: &str = "127.0.0.1:53190";
|
|
|
|
|
|
|
|
|
|
struct AppState {
|
|
|
|
|
session: Mutex<Option<SessionState>>,
|
|
|
|
|
tray: Mutex<Option<TrayState>>,
|
|
|
|
|
single_instance_lock: TcpListener,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct TrayState {
|
|
|
|
|
status_item: MenuItem<Wry>,
|
|
|
|
|
received_item: MenuItem<Wry>,
|
|
|
|
|
sent_item: MenuItem<Wry>,
|
|
|
|
|
toggle_item: MenuItem<Wry>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
#[serde(rename_all = "camelCase")]
|
|
|
|
|
struct SessionState {
|
|
|
|
|
@@ -214,6 +222,8 @@ async fn enroll_device(
|
|
|
|
|
write_session_state(&app, &session_state)?;
|
|
|
|
|
let mut session = state.session.lock().map_err(|_| "Unable to store client state".to_string())?;
|
|
|
|
|
*session = Some(session_state);
|
|
|
|
|
drop(session);
|
|
|
|
|
refresh_tray_menu(&app);
|
|
|
|
|
|
|
|
|
|
Ok(result)
|
|
|
|
|
}
|
|
|
|
|
@@ -223,6 +233,8 @@ fn load_state(app: AppHandle, state: State<'_, AppState>) -> Result<Option<Enrol
|
|
|
|
|
let loaded = read_session_state(&app)?;
|
|
|
|
|
let mut session = state.session.lock().map_err(|_| "Unable to read client state".to_string())?;
|
|
|
|
|
*session = loaded.clone();
|
|
|
|
|
drop(session);
|
|
|
|
|
refresh_tray_menu(&app);
|
|
|
|
|
Ok(loaded.map(|value| value.enrollment))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -241,6 +253,8 @@ fn clear_session(app: AppHandle, state: State<'_, AppState>) -> Result<(), Strin
|
|
|
|
|
|
|
|
|
|
let mut session = state.session.lock().map_err(|_| "Unable to clear client state".to_string())?;
|
|
|
|
|
*session = None;
|
|
|
|
|
drop(session);
|
|
|
|
|
refresh_tray_menu(&app);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -301,41 +315,61 @@ async fn sync_profile(app: AppHandle, state: State<'_, AppState>) -> Result<Enro
|
|
|
|
|
write_session_state(&app, &session_state)?;
|
|
|
|
|
let mut session = state.session.lock().map_err(|_| "Unable to store client state".to_string())?;
|
|
|
|
|
*session = Some(session_state);
|
|
|
|
|
drop(session);
|
|
|
|
|
refresh_tray_menu(&app);
|
|
|
|
|
|
|
|
|
|
Ok(result)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
fn connect_tunnel(app: AppHandle, state: State<'_, AppState>) -> Result<(), String> {
|
|
|
|
|
let profile_path = {
|
|
|
|
|
let session = state.session.lock().map_err(|_| "Unable to read client state".to_string())?;
|
|
|
|
|
let session = session.as_ref().ok_or_else(|| "No enrolled profile is available yet".to_string())?;
|
|
|
|
|
tunnel_manager::connect(&app, std::path::Path::new(&session.profile_path))
|
|
|
|
|
session.profile_path.clone()
|
|
|
|
|
};
|
|
|
|
|
let result = tunnel_manager::connect(&app, std::path::Path::new(&profile_path));
|
|
|
|
|
refresh_tray_menu(&app);
|
|
|
|
|
result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
fn disconnect_tunnel(app: AppHandle, state: State<'_, AppState>) -> Result<(), String> {
|
|
|
|
|
let profile_path = {
|
|
|
|
|
let session = state.session.lock().map_err(|_| "Unable to read client state".to_string())?;
|
|
|
|
|
let session = session.as_ref().ok_or_else(|| "No active session is available".to_string())?;
|
|
|
|
|
tunnel_manager::disconnect(&app, std::path::Path::new(&session.profile_path))
|
|
|
|
|
session.profile_path.clone()
|
|
|
|
|
};
|
|
|
|
|
let result = tunnel_manager::disconnect(&app, std::path::Path::new(&profile_path));
|
|
|
|
|
refresh_tray_menu(&app);
|
|
|
|
|
result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
fn tunnel_status(app: AppHandle, state: State<'_, AppState>) -> Result<bool, String> {
|
|
|
|
|
let profile_path = {
|
|
|
|
|
let session = state.session.lock().map_err(|_| "Unable to read client state".to_string())?;
|
|
|
|
|
let session = session.as_ref().ok_or_else(|| "No active session is available".to_string())?;
|
|
|
|
|
tunnel_manager::is_active(&app, std::path::Path::new(&session.profile_path))
|
|
|
|
|
session.profile_path.clone()
|
|
|
|
|
};
|
|
|
|
|
tunnel_manager::is_active(&app, std::path::Path::new(&profile_path))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
fn tunnel_metrics(app: AppHandle, state: State<'_, AppState>) -> Result<TunnelMetrics, String> {
|
|
|
|
|
let profile_path = {
|
|
|
|
|
let session = state.session.lock().map_err(|_| "Unable to read client state".to_string())?;
|
|
|
|
|
let session = session.as_ref().ok_or_else(|| "No active session is available".to_string())?;
|
|
|
|
|
let metrics = tunnel_manager::metrics(&app, std::path::Path::new(&session.profile_path))?;
|
|
|
|
|
Ok(TunnelMetrics {
|
|
|
|
|
session.profile_path.clone()
|
|
|
|
|
};
|
|
|
|
|
let metrics = tunnel_manager::metrics(&app, std::path::Path::new(&profile_path))?;
|
|
|
|
|
let mapped = TunnelMetrics {
|
|
|
|
|
active: metrics.active,
|
|
|
|
|
rx_bytes: metrics.rx_bytes,
|
|
|
|
|
tx_bytes: metrics.tx_bytes,
|
|
|
|
|
})
|
|
|
|
|
};
|
|
|
|
|
let _ = update_tray_menu(&app, mapped);
|
|
|
|
|
Ok(mapped)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn generate_keypair() -> (String, String) {
|
|
|
|
|
@@ -395,6 +429,98 @@ fn ensure_app_dir(app: &AppHandle) -> Result<PathBuf, String> {
|
|
|
|
|
Ok(dir)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn format_data_size(bytes: u64) -> String {
|
|
|
|
|
if bytes == 0 {
|
|
|
|
|
return "0 B".into();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let units = ["B", "KB", "MB", "GB", "TB"];
|
|
|
|
|
let mut value = bytes as f64;
|
|
|
|
|
let mut unit_index = 0;
|
|
|
|
|
|
|
|
|
|
while value >= 1024.0 && unit_index < units.len() - 1 {
|
|
|
|
|
value /= 1024.0;
|
|
|
|
|
unit_index += 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if value >= 100.0 || unit_index == 0 {
|
|
|
|
|
format!("{value:.0} {}", units[unit_index])
|
|
|
|
|
} else {
|
|
|
|
|
format!("{value:.1} {}", units[unit_index])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn current_metrics(app: &AppHandle) -> Result<TunnelMetrics, String> {
|
|
|
|
|
let state = app.state::<AppState>();
|
|
|
|
|
let profile_path = {
|
|
|
|
|
let session = state.session.lock().map_err(|_| "Unable to read client state".to_string())?;
|
|
|
|
|
let session = session.as_ref().ok_or_else(|| "No active session is available".to_string())?;
|
|
|
|
|
session.profile_path.clone()
|
|
|
|
|
};
|
|
|
|
|
tunnel_manager::metrics(app, std::path::Path::new(&profile_path)).map(|metrics| TunnelMetrics {
|
|
|
|
|
active: metrics.active,
|
|
|
|
|
rx_bytes: metrics.rx_bytes,
|
|
|
|
|
tx_bytes: metrics.tx_bytes,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn update_tray_menu(app: &AppHandle, metrics: TunnelMetrics) -> Result<(), String> {
|
|
|
|
|
let state = app.state::<AppState>();
|
|
|
|
|
let tray = state.tray.lock().map_err(|_| "Unable to update tray state".to_string())?;
|
|
|
|
|
let Some(tray) = tray.as_ref() else {
|
|
|
|
|
return Ok(());
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let status_text = if metrics.active {
|
|
|
|
|
"Status: Connected"
|
|
|
|
|
} else {
|
|
|
|
|
"Status: Disconnected"
|
|
|
|
|
};
|
|
|
|
|
let toggle_text = if metrics.active { "Disconnect NexaVPN" } else { "Connect NexaVPN" };
|
|
|
|
|
|
|
|
|
|
let _ = tray.status_item.set_text(status_text);
|
|
|
|
|
let _ = tray.received_item.set_text(format!("Received: {}", format_data_size(metrics.rx_bytes)));
|
|
|
|
|
let _ = tray.sent_item.set_text(format!("Sent: {}", format_data_size(metrics.tx_bytes)));
|
|
|
|
|
let _ = tray.toggle_item.set_text(toggle_text);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn refresh_tray_menu(app: &AppHandle) {
|
|
|
|
|
let metrics = current_metrics(app).unwrap_or(TunnelMetrics {
|
|
|
|
|
active: false,
|
|
|
|
|
rx_bytes: 0,
|
|
|
|
|
tx_bytes: 0,
|
|
|
|
|
});
|
|
|
|
|
let _ = update_tray_menu(app, metrics);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn toggle_tray_connection(app: &AppHandle) {
|
|
|
|
|
let metrics = current_metrics(app).unwrap_or(TunnelMetrics {
|
|
|
|
|
active: false,
|
|
|
|
|
rx_bytes: 0,
|
|
|
|
|
tx_bytes: 0,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let state = app.state::<AppState>();
|
|
|
|
|
let profile_path = match state.session.lock() {
|
|
|
|
|
Ok(session) => match session.as_ref() {
|
|
|
|
|
Some(session) => session.profile_path.clone(),
|
|
|
|
|
None => return,
|
|
|
|
|
},
|
|
|
|
|
Err(_) => return,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let result = if metrics.active {
|
|
|
|
|
tunnel_manager::disconnect(app, std::path::Path::new(&profile_path))
|
|
|
|
|
} else {
|
|
|
|
|
tunnel_manager::connect(app, std::path::Path::new(&profile_path))
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if result.is_ok() {
|
|
|
|
|
refresh_tray_menu(app);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn restore_webview_window(window: &WebviewWindow) {
|
|
|
|
|
let _ = window.show();
|
|
|
|
|
let _ = window.unminimize();
|
|
|
|
|
@@ -412,9 +538,15 @@ pub fn run() {
|
|
|
|
|
let single_instance_lock = TcpListener::bind(SINGLE_INSTANCE_ADDR)
|
|
|
|
|
.map_err(|_| format!("{} is already running.", app.package_info().name))?;
|
|
|
|
|
|
|
|
|
|
let status_item = MenuItemBuilder::with_id("status", "Status: Disconnected").build(app)?;
|
|
|
|
|
let received_item = MenuItemBuilder::with_id("received", "Received: 0 B").build(app)?;
|
|
|
|
|
let sent_item = MenuItemBuilder::with_id("sent", "Sent: 0 B").build(app)?;
|
|
|
|
|
let toggle_item = MenuItemBuilder::with_id("toggle", "Connect NexaVPN").build(app)?;
|
|
|
|
|
let open_item = MenuItemBuilder::with_id("open", "Open NexaVPN").build(app)?;
|
|
|
|
|
let quit_item = MenuItemBuilder::with_id("quit", "Quit NexaVPN").build(app)?;
|
|
|
|
|
let menu = MenuBuilder::new(app).items(&[&open_item, &quit_item]).build()?;
|
|
|
|
|
let menu = MenuBuilder::new(app)
|
|
|
|
|
.items(&[&status_item, &received_item, &sent_item, &toggle_item, &open_item, &quit_item])
|
|
|
|
|
.build()?;
|
|
|
|
|
|
|
|
|
|
let mut tray = TrayIconBuilder::new().menu(&menu).show_menu_on_left_click(false);
|
|
|
|
|
if let Some(icon) = app.default_window_icon() {
|
|
|
|
|
@@ -423,6 +555,9 @@ pub fn run() {
|
|
|
|
|
|
|
|
|
|
tray
|
|
|
|
|
.on_menu_event(|app, event| match event.id().as_ref() {
|
|
|
|
|
"toggle" => {
|
|
|
|
|
toggle_tray_connection(&app);
|
|
|
|
|
}
|
|
|
|
|
"open" => {
|
|
|
|
|
if let Some(window) = app.get_webview_window(MAIN_WINDOW_LABEL) {
|
|
|
|
|
restore_webview_window(&window);
|
|
|
|
|
@@ -450,8 +585,15 @@ pub fn run() {
|
|
|
|
|
|
|
|
|
|
app.manage(AppState {
|
|
|
|
|
session: Mutex::new(None),
|
|
|
|
|
tray: Mutex::new(Some(TrayState {
|
|
|
|
|
status_item,
|
|
|
|
|
received_item,
|
|
|
|
|
sent_item,
|
|
|
|
|
toggle_item,
|
|
|
|
|
})),
|
|
|
|
|
single_instance_lock,
|
|
|
|
|
});
|
|
|
|
|
refresh_tray_menu(app.handle());
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
})
|
|
|
|
|
|