Initial Commit
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
#[tauri::command]
|
||||
pub async fn launch_url(url: String) -> Result<(), String> {
|
||||
open::that(&url).map_err(|e| format!("Failed to open URL: {}", e))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn check_privileges() -> Result<bool, String> {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
// Check if effective user ID is 0 (root)
|
||||
Ok(unsafe { libc::geteuid() } == 0)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// On Windows, check if running as administrator
|
||||
Ok(is_elevated::is_elevated())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
use serde::Deserialize;
|
||||
use std::fs::File;
|
||||
|
||||
use crate::models::host::HostResult;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ExportRequest {
|
||||
pub path: String,
|
||||
pub hosts: Vec<HostResult>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn export_csv(request: ExportRequest) -> Result<(), String> {
|
||||
let file = File::create(&request.path)
|
||||
.map_err(|e| format!("Failed to create file: {}", e))?;
|
||||
|
||||
let mut writer = csv::Writer::from_writer(file);
|
||||
|
||||
// Header
|
||||
writer
|
||||
.write_record(["IP Address", "Status", "Latency (ms)", "Hostname"])
|
||||
.map_err(|e| format!("Failed to write header: {}", e))?;
|
||||
|
||||
// Rows
|
||||
for host in &request.hosts {
|
||||
let ip = host.ip.to_string();
|
||||
let status = format!("{:?}", host.status);
|
||||
let rtt = match &host.ping {
|
||||
Some(p) => format!("{:.1}", p.rtt_ms),
|
||||
None => String::from("-"),
|
||||
};
|
||||
let hostname = host.hostname.clone().unwrap_or_default();
|
||||
|
||||
writer
|
||||
.write_record([&ip, &status, &rtt, &hostname])
|
||||
.map_err(|e| format!("Failed to write row: {}", e))?;
|
||||
}
|
||||
|
||||
writer
|
||||
.flush()
|
||||
.map_err(|e| format!("Failed to flush: {}", e))?;
|
||||
|
||||
tracing::info!("Exported {} hosts to {}", request.hosts.len(), request.path);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
pub mod actions;
|
||||
pub mod export;
|
||||
pub mod monitor;
|
||||
pub mod nmap;
|
||||
pub mod scan;
|
||||
pub mod traceroute;
|
||||
pub mod updater;
|
||||
@@ -0,0 +1,182 @@
|
||||
use std::collections::HashMap;
|
||||
use std::net::IpAddr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use ping_async::IcmpEchoRequestor;
|
||||
use serde::Deserialize;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
use crate::models::monitor::MonitorEntry;
|
||||
|
||||
struct MonitorState {
|
||||
entries: HashMap<IpAddr, MonitorEntry>,
|
||||
task: Option<JoinHandle<()>>,
|
||||
running: bool,
|
||||
}
|
||||
|
||||
static MONITOR: std::sync::OnceLock<Arc<Mutex<MonitorState>>> = std::sync::OnceLock::new();
|
||||
|
||||
fn get_monitor() -> Arc<Mutex<MonitorState>> {
|
||||
MONITOR
|
||||
.get_or_init(|| {
|
||||
Arc::new(Mutex::new(MonitorState {
|
||||
entries: HashMap::new(),
|
||||
task: None,
|
||||
running: false,
|
||||
}))
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct MonitorHost {
|
||||
pub ip: String,
|
||||
pub label: Option<String>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn monitor_add_hosts(hosts: Vec<MonitorHost>) -> Result<(), String> {
|
||||
let state = get_monitor();
|
||||
let mut guard = state.lock().await;
|
||||
|
||||
for host in hosts {
|
||||
let ip: IpAddr = host
|
||||
.ip
|
||||
.parse()
|
||||
.map_err(|e| format!("Invalid IP '{}': {}", host.ip, e))?;
|
||||
guard
|
||||
.entries
|
||||
.entry(ip)
|
||||
.or_insert_with(|| MonitorEntry::new(ip, host.label));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn monitor_remove_host(ip: String) -> Result<(), String> {
|
||||
let parsed: IpAddr = ip.parse().map_err(|e| format!("Invalid IP: {}", e))?;
|
||||
let state = get_monitor();
|
||||
let mut guard = state.lock().await;
|
||||
guard.entries.remove(&parsed);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn monitor_clear() -> Result<(), String> {
|
||||
let state = get_monitor();
|
||||
let mut guard = state.lock().await;
|
||||
guard.entries.clear();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn monitor_get_entries() -> Result<Vec<MonitorEntry>, String> {
|
||||
let state = get_monitor();
|
||||
let guard = state.lock().await;
|
||||
let mut entries: Vec<MonitorEntry> = guard.entries.values().cloned().collect();
|
||||
entries.sort_by(|a, b| a.ip.to_string().cmp(&b.ip.to_string()));
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn monitor_start(app: AppHandle, interval_ms: Option<u64>) -> Result<(), String> {
|
||||
let state = get_monitor();
|
||||
let mut guard = state.lock().await;
|
||||
|
||||
if guard.running {
|
||||
return Err("Monitor already running".to_string());
|
||||
}
|
||||
|
||||
if guard.entries.is_empty() {
|
||||
return Err("No hosts to monitor".to_string());
|
||||
}
|
||||
|
||||
guard.running = true;
|
||||
let interval = Duration::from_millis(interval_ms.unwrap_or(2000));
|
||||
let state2 = get_monitor();
|
||||
|
||||
let task = tokio::spawn(async move {
|
||||
loop {
|
||||
{
|
||||
let guard = state2.lock().await;
|
||||
if !guard.running {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Collect IPs to ping
|
||||
let ips: Vec<IpAddr> = {
|
||||
let guard = state2.lock().await;
|
||||
guard.entries.keys().cloned().collect()
|
||||
};
|
||||
|
||||
// Ping all hosts concurrently
|
||||
let mut join_set = tokio::task::JoinSet::new();
|
||||
for ip in ips {
|
||||
join_set.spawn(async move {
|
||||
let timeout = Duration::from_millis(1500);
|
||||
let requestor = match IcmpEchoRequestor::new(ip, None, None, Some(timeout)) {
|
||||
Ok(r) => r,
|
||||
Err(_) => return (ip, None),
|
||||
};
|
||||
match requestor.send().await {
|
||||
Ok(reply) => match reply.status() {
|
||||
ping_async::IcmpEchoStatus::Success => {
|
||||
let rtt = reply.round_trip_time().as_secs_f64() * 1000.0;
|
||||
(ip, Some(rtt))
|
||||
}
|
||||
_ => (ip, None),
|
||||
},
|
||||
Err(_) => (ip, None),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Process results
|
||||
while let Some(result) = join_set.join_next().await {
|
||||
if let Ok((ip, rtt)) = result {
|
||||
let mut guard = state2.lock().await;
|
||||
if let Some(entry) = guard.entries.get_mut(&ip) {
|
||||
let old_status = entry.status.clone();
|
||||
match rtt {
|
||||
Some(ms) => entry.record_success(ms),
|
||||
None => entry.record_failure(),
|
||||
}
|
||||
// Emit state change event
|
||||
if entry.status != old_status {
|
||||
let _ = app.emit(
|
||||
"monitor:state-change",
|
||||
serde_json::json!({
|
||||
"ip": ip.to_string(),
|
||||
"old": format!("{:?}", old_status),
|
||||
"new": format!("{:?}", entry.status),
|
||||
}),
|
||||
);
|
||||
}
|
||||
let _ = app.emit("monitor:update", &entry.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tokio::time::sleep(interval).await;
|
||||
}
|
||||
});
|
||||
|
||||
guard.task = Some(task);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn monitor_stop() -> Result<(), String> {
|
||||
let state = get_monitor();
|
||||
let mut guard = state.lock().await;
|
||||
guard.running = false;
|
||||
if let Some(task) = guard.task.take() {
|
||||
task.abort();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
use std::process::Stdio;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::Deserialize;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
use tokio::process::Command;
|
||||
|
||||
static NMAP_CANCEL: std::sync::OnceLock<Arc<AtomicBool>> = std::sync::OnceLock::new();
|
||||
|
||||
fn get_nmap_cancel() -> Arc<AtomicBool> {
|
||||
NMAP_CANCEL
|
||||
.get_or_init(|| Arc::new(AtomicBool::new(false)))
|
||||
.clone()
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct NmapConfig {
|
||||
pub target: String,
|
||||
pub args: Vec<String>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn run_nmap(app: AppHandle, config: NmapConfig) -> Result<(), String> {
|
||||
// Check nmap exists
|
||||
let check = Command::new("nmap")
|
||||
.arg("--version")
|
||||
.output()
|
||||
.await
|
||||
.map_err(|_| "nmap not found. Please install nmap.".to_string())?;
|
||||
|
||||
if !check.status.success() {
|
||||
return Err("nmap not found. Please install nmap.".to_string());
|
||||
}
|
||||
|
||||
let cancel = get_nmap_cancel();
|
||||
cancel.store(false, Ordering::Relaxed);
|
||||
|
||||
let _ = app.emit("nmap:start", &config.target);
|
||||
|
||||
let mut cmd = Command::new("nmap");
|
||||
for arg in &config.args {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
for target in config.target.split_whitespace() {
|
||||
cmd.arg(target);
|
||||
}
|
||||
cmd.stdout(Stdio::piped());
|
||||
cmd.stderr(Stdio::piped());
|
||||
|
||||
let mut child = cmd
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to start nmap: {}", e))?;
|
||||
|
||||
let stdout = child
|
||||
.stdout
|
||||
.take()
|
||||
.ok_or_else(|| "Failed to capture nmap stdout".to_string())?;
|
||||
|
||||
let stderr = child
|
||||
.stderr
|
||||
.take()
|
||||
.ok_or_else(|| "Failed to capture nmap stderr".to_string())?;
|
||||
|
||||
// Stream stdout
|
||||
let app2 = app.clone();
|
||||
let cancel2 = cancel.clone();
|
||||
let stdout_handle = tokio::spawn(async move {
|
||||
let mut reader = BufReader::new(stdout).lines();
|
||||
while let Ok(Some(line)) = reader.next_line().await {
|
||||
if cancel2.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
let _ = app2.emit("nmap:line", &line);
|
||||
}
|
||||
});
|
||||
|
||||
// Stream stderr
|
||||
let app3 = app.clone();
|
||||
let stderr_handle = tokio::spawn(async move {
|
||||
let mut reader = BufReader::new(stderr).lines();
|
||||
while let Ok(Some(line)) = reader.next_line().await {
|
||||
let _ = app3.emit("nmap:line", &line);
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for completion or cancellation
|
||||
tokio::select! {
|
||||
status = child.wait() => {
|
||||
match status {
|
||||
Ok(s) if s.success() => {
|
||||
let _ = app.emit("nmap:complete", "Scan complete");
|
||||
}
|
||||
Ok(s) => {
|
||||
let _ = app.emit("nmap:complete",
|
||||
format!("nmap exited with code {}", s.code().unwrap_or(-1)));
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = app.emit("nmap:error", format!("nmap error: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = async {
|
||||
loop {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
if cancel.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} => {
|
||||
let _ = child.kill().await;
|
||||
let _ = app.emit("nmap:complete", "Scan cancelled");
|
||||
}
|
||||
}
|
||||
|
||||
let _ = stdout_handle.await;
|
||||
let _ = stderr_handle.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn stop_nmap() -> Result<(), String> {
|
||||
let cancel = get_nmap_cancel();
|
||||
cancel.store(true, Ordering::Relaxed);
|
||||
tracing::info!("Nmap scan cancelled");
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::Deserialize;
|
||||
use tauri::AppHandle;
|
||||
|
||||
use crate::net::range::parse_ip_range;
|
||||
use crate::scanner::ping::ping_sweep;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct ScanConfig {
|
||||
pub range: String,
|
||||
pub timeout_ms: Option<u64>,
|
||||
pub port_scan: Option<bool>,
|
||||
pub ports: Option<Vec<u16>>,
|
||||
pub port_timeout_ms: Option<u64>,
|
||||
}
|
||||
|
||||
static CANCEL_FLAG: std::sync::OnceLock<Arc<AtomicBool>> = std::sync::OnceLock::new();
|
||||
|
||||
fn get_cancel_flag() -> Arc<AtomicBool> {
|
||||
CANCEL_FLAG
|
||||
.get_or_init(|| Arc::new(AtomicBool::new(false)))
|
||||
.clone()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn start_scan(app: AppHandle, config: ScanConfig) -> Result<(), String> {
|
||||
let ips = parse_ip_range(&config.range).map_err(|e| format!("Invalid IP range: {}", e))?;
|
||||
|
||||
let timeout_ms = config.timeout_ms.unwrap_or(1500);
|
||||
let cancel = get_cancel_flag();
|
||||
|
||||
// Reset cancel flag from any previous scan
|
||||
cancel.store(false, Ordering::Relaxed);
|
||||
|
||||
// Spawn the scan as a background task so this command returns immediately
|
||||
let port_scan = config.port_scan.unwrap_or(false);
|
||||
let ports = config.ports.clone();
|
||||
let port_timeout_ms = config.port_timeout_ms.unwrap_or(1000);
|
||||
|
||||
tokio::spawn(async move {
|
||||
ping_sweep(&app, &ips, timeout_ms, cancel, port_scan, ports, port_timeout_ms).await;
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn stop_scan() -> Result<(), String> {
|
||||
let cancel = get_cancel_flag();
|
||||
cancel.store(true, Ordering::Relaxed);
|
||||
tracing::info!("Scan cancelled");
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
use std::process::Stdio;
|
||||
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
use tokio::process::Command;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn run_traceroute(app: AppHandle, ip: String) -> Result<(), String> {
|
||||
let _ = app.emit("traceroute:start", &ip);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
let mut child = Command::new("tracepath")
|
||||
.arg("-b")
|
||||
.arg(&ip)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to start tracepath: {}. Is iputils-tracepath installed?", e))?;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let mut child = Command::new("tracert")
|
||||
.arg("-d")
|
||||
.arg(&ip)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to start tracert: {}", e))?;
|
||||
|
||||
let stdout = child.stdout.take()
|
||||
.ok_or_else(|| "Failed to capture stdout".to_string())?;
|
||||
|
||||
let mut reader = BufReader::new(stdout).lines();
|
||||
|
||||
while let Ok(Some(line)) = reader.next_line().await {
|
||||
let _ = app.emit("traceroute:hop", &line);
|
||||
}
|
||||
|
||||
let status = child.wait().await
|
||||
.map_err(|e| format!("tracepath failed: {}", e))?;
|
||||
|
||||
if status.success() {
|
||||
let _ = app.emit("traceroute:complete", &ip);
|
||||
} else {
|
||||
let _ = app.emit("traceroute:error", "tracepath exited with an error");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AppConfig {
|
||||
pub update_url: String,
|
||||
}
|
||||
|
||||
impl Default for AppConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
update_url: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn config_path(app: &AppHandle) -> Result<PathBuf, String> {
|
||||
let dir = app
|
||||
.path()
|
||||
.app_config_dir()
|
||||
.map_err(|e| format!("Failed to get config dir: {}", e))?;
|
||||
std::fs::create_dir_all(&dir)
|
||||
.map_err(|e| format!("Failed to create config dir: {}", e))?;
|
||||
Ok(dir.join("config.json"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_config(app: AppHandle) -> Result<AppConfig, String> {
|
||||
let path = config_path(&app)?;
|
||||
if !path.exists() {
|
||||
return Ok(AppConfig::default());
|
||||
}
|
||||
let data = std::fs::read_to_string(&path)
|
||||
.map_err(|e| format!("Failed to read config: {}", e))?;
|
||||
serde_json::from_str(&data)
|
||||
.map_err(|e| format!("Failed to parse config: {}", e))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_config(app: AppHandle, config: AppConfig) -> Result<(), String> {
|
||||
let path = config_path(&app)?;
|
||||
let data = serde_json::to_string_pretty(&config)
|
||||
.map_err(|e| format!("Failed to serialize config: {}", e))?;
|
||||
std::fs::write(&path, data)
|
||||
.map_err(|e| format!("Failed to write config: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateInfo {
|
||||
pub version: String,
|
||||
pub url: String,
|
||||
pub notes: Option<String>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn check_for_update(app: AppHandle) -> Result<Option<UpdateInfo>, String> {
|
||||
let config = get_config(app.clone()).await?;
|
||||
|
||||
if config.update_url.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let response = reqwest::get(&config.update_url)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to check for updates: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(format!("Update server returned {}", response.status()));
|
||||
}
|
||||
|
||||
let body: serde_json::Value = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("Invalid update response: {}", e))?;
|
||||
|
||||
// Parse Gitea/GitHub release format
|
||||
let tag = body["tag_name"]
|
||||
.as_str()
|
||||
.ok_or_else(|| "No tag_name in response".to_string())?;
|
||||
|
||||
let html_url = body["html_url"]
|
||||
.as_str()
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
|
||||
let notes = body["body"]
|
||||
.as_str()
|
||||
.map(|s| s.to_string())
|
||||
.filter(|s| !s.is_empty());
|
||||
|
||||
// Strip leading 'v' from tag if present (v0.2.0 -> 0.2.0)
|
||||
let remote_version = tag.strip_prefix('v').unwrap_or(tag);
|
||||
let current = app.package_info().version.to_string();
|
||||
|
||||
if remote_version != current {
|
||||
Ok(Some(UpdateInfo {
|
||||
version: remote_version.to_string(),
|
||||
url: html_url,
|
||||
notes,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user