// Evita a janela de console extra no Windows em release. #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] use sabia_core::engine::{Engine, ScanOptions}; use std::sync::Arc; struct State(Arc); #[tauri::command] fn version(state: tauri::State<'_, State>) -> String { state.0.version().to_string() } #[tauri::command] fn list_ifaces(state: tauri::State<'_, State>) -> Result { let list = state.0.list_ifaces().map_err(|e| e.to_string())?; serde_json::to_value(list).map_err(|e| e.to_string()) } #[tauri::command] fn set_monitor(state: tauri::State<'_, State>, iface: String, enable: bool) -> Result<(), String> { state .0 .set_monitor(&iface, enable) .map_err(|e| e.to_string()) } #[tauri::command] fn start_scan(state: tauri::State<'_, State>, iface: String) -> Result<(), String> { state.0.start_scan(&iface).map_err(|e| e.to_string()) } #[tauri::command] fn stop_scan(state: tauri::State<'_, State>, iface: String) { state.0.stop_scan(&iface); } #[tauri::command] fn scan_results( state: tauri::State<'_, State>, iface: String, ) -> Result { serde_json::to_value(state.0.scan_results(&iface)).map_err(|e| e.to_string()) } #[tauri::command] fn scan_snapshot( state: tauri::State<'_, State>, iface: String, ) -> Result { let snapshot = state.0.scan_snapshot(&iface).map_err(|e| e.to_string())?; serde_json::to_value(snapshot).map_err(|e| e.to_string()) } #[tauri::command] fn scan_clients( state: tauri::State<'_, State>, iface: String, ) -> Result { serde_json::to_value(state.0.scan_clients(&iface)).map_err(|e| e.to_string()) } #[tauri::command] fn handshakes( state: tauri::State<'_, State>, iface: Option, ) -> Result { serde_json::to_value(state.0.handshakes(iface.as_deref())).map_err(|e| e.to_string()) } #[tauri::command] fn export_handshake( state: tauri::State<'_, State>, iface: String, bssid: String, station: String, out: String, ) -> Result { let report = state .0 .export_handshake(&iface, &bssid, &station, &out) .map_err(|e| e.to_string())?; serde_json::to_value(report).map_err(|e| e.to_string()) } #[tauri::command] fn export_all_handshakes( state: tauri::State<'_, State>, iface: String, out: String, ) -> Result { let report = state .0 .export_all_handshakes(&iface, &out) .map_err(|e| e.to_string())?; serde_json::to_value(report).map_err(|e| e.to_string()) } #[tauri::command] fn start_crack( state: tauri::State<'_, State>, iface: String, wordlist: Option, bssid: Option, station: Option, rules: Option, ) -> Result { let rules = rules.unwrap_or(false); let result = match wordlist.filter(|w| !w.trim().is_empty()) { Some(wordlist) => state.0.start_crack( &iface, bssid.as_deref(), station.as_deref(), &wordlist, rules, ), None => state .0 .start_crack_default(&iface, bssid.as_deref(), station.as_deref(), rules), }; result.map_err(|e| e.to_string()) } #[tauri::command] fn start_crack_mask( state: tauri::State<'_, State>, iface: String, mask: String, bssid: Option, station: Option, max_candidates: Option, ) -> Result { state .0 .start_crack_mask( &iface, bssid.as_deref(), station.as_deref(), &mask, max_candidates.unwrap_or(50_000_000), ) .map_err(|e| e.to_string()) } #[tauri::command] fn crack_job(state: tauri::State<'_, State>, id: String) -> Result { match state.0.crack_job(&id) { Some(job) => serde_json::to_value(job).map_err(|e| e.to_string()), None => Err("job não encontrado".into()), } } #[tauri::command] fn crack_jobs(state: tauri::State<'_, State>) -> Result { serde_json::to_value(state.0.crack_jobs()).map_err(|e| e.to_string()) } #[tauri::command] fn deauth( state: tauri::State<'_, State>, iface: String, bssid: String, station: Option, count: Option, reason: Option, ) -> Result { let report = state .0 .deauth( &iface, &bssid, station.as_deref(), count.unwrap_or(5), reason.unwrap_or(7), ) .map_err(|e| e.to_string())?; serde_json::to_value(report).map_err(|e| e.to_string()) } #[tauri::command] #[allow(clippy::too_many_arguments)] fn deauth_directional( state: tauri::State<'_, State>, iface: String, bssid: String, station: Option, count: Option, reason: Option, direction: Option, frame_kind: Option, ) -> Result { let report = state .0 .deauth_directional( &iface, &bssid, station.as_deref(), count.unwrap_or(5), reason.unwrap_or(7), direction.as_deref().unwrap_or("ap"), frame_kind.as_deref().unwrap_or("deauth"), ) .map_err(|e| e.to_string())?; serde_json::to_value(report).map_err(|e| e.to_string()) } #[tauri::command] #[allow(clippy::too_many_arguments)] fn start_deauth_job( state: tauri::State<'_, State>, iface: String, bssid: String, station: Option, reason: Option, direction: Option, frame_kind: Option, interval_ms: Option, max_secs: Option, stop_on_handshake: Option, ) -> Result { state .0 .start_deauth_job( &iface, &bssid, station.as_deref(), reason.unwrap_or(7), direction.as_deref().unwrap_or("both"), frame_kind.as_deref().unwrap_or("deauth"), interval_ms.unwrap_or(120), max_secs, stop_on_handshake.unwrap_or(false), ) .map_err(|e| e.to_string()) } #[tauri::command] fn stop_deauth_job(state: tauri::State<'_, State>, id: String) -> Result<(), String> { state.0.stop_deauth_job(&id).map_err(|e| e.to_string()) } #[tauri::command] fn deauth_job(state: tauri::State<'_, State>, id: String) -> Result { match state.0.deauth_job(&id) { Some(job) => serde_json::to_value(job).map_err(|e| e.to_string()), None => Err("job de deauth não encontrado".into()), } } #[tauri::command] fn deauth_jobs( state: tauri::State<'_, State>, iface: Option, ) -> Result { let jobs = state.0.deauth_jobs(); let jobs: Vec<_> = match iface { Some(iface) => jobs .into_iter() .filter(|j| j.iface.eq_ignore_ascii_case(&iface)) .collect(), None => jobs, }; serde_json::to_value(jobs).map_err(|e| e.to_string()) } #[tauri::command] fn capture_handshake( state: tauri::State<'_, State>, iface: String, bssid: String, station: Option, seconds: Option, ) -> Result { state .0 .capture_handshake(&iface, &bssid, station.as_deref(), seconds) .map_err(|e| e.to_string()) } #[tauri::command] fn report( state: tauri::State<'_, State>, iface: String, ) -> Result { let model = state.0.report(&iface).map_err(|e| e.to_string())?; serde_json::to_value(model).map_err(|e| e.to_string()) } #[tauri::command] fn report_html( state: tauri::State<'_, State>, iface: String, inline: Option, ) -> Result { let model = state.0.report(&iface).map_err(|e| e.to_string())?; let options = sabia_core::report::ReportOptions { inline_pagedjs: inline.unwrap_or(false), ..Default::default() }; Ok(sabia_core::report::render(&model, options)) } #[tauri::command] fn export_hc22000( state: tauri::State<'_, State>, iface: String, bssid: String, station: String, out: String, ) -> Result { let report = state .0 .export_hc22000(&iface, &bssid, &station, &out) .map_err(|e| e.to_string())?; serde_json::to_value(report).map_err(|e| e.to_string()) } #[tauri::command] fn export_all_hc22000( state: tauri::State<'_, State>, iface: String, out: String, ) -> Result { let report = state .0 .export_all_hc22000(&iface, &out) .map_err(|e| e.to_string())?; serde_json::to_value(report).map_err(|e| e.to_string()) } #[tauri::command] fn pmkid_hunt( state: tauri::State<'_, State>, iface: String, bssid: String, attempts: Option, ) -> Result { let report = state .0 .pmkid_hunt(&iface, &bssid, attempts.unwrap_or(5)) .map_err(|e| e.to_string())?; serde_json::to_value(report).map_err(|e| e.to_string()) } #[tauri::command] fn pmf_test( state: tauri::State<'_, State>, iface: String, bssid: String, station: String, reason: Option, ) -> Result { let report = state .0 .pmf_test(&iface, &bssid, &station, reason.unwrap_or(7)) .map_err(|e| e.to_string())?; serde_json::to_value(report).map_err(|e| e.to_string()) } #[tauri::command] fn wps_tools(state: tauri::State<'_, State>) -> Result { serde_json::to_value(state.0.wps_tools()).map_err(|e| e.to_string()) } #[tauri::command] fn start_wps( state: tauri::State<'_, State>, iface: String, bssid: String, channel: u32, tool: String, pin: Option, ) -> Result { state .0 .start_wps(&iface, &bssid, channel, &tool, pin.as_deref()) .map_err(|e| e.to_string()) } #[tauri::command] fn wps_job(state: tauri::State<'_, State>, id: String) -> Result { match state.0.wps_job(&id) { Some(job) => serde_json::to_value(job).map_err(|e| e.to_string()), None => Err("job WPS não encontrado".into()), } } #[tauri::command] fn wps_jobs(state: tauri::State<'_, State>) -> Result { serde_json::to_value(state.0.wps_jobs()).map_err(|e| e.to_string()) } #[tauri::command] fn stop_wps(state: tauri::State<'_, State>, id: String) -> Result<(), String> { state.0.stop_wps(&id).map_err(|e| e.to_string()) } #[tauri::command] fn start_evil_twin( state: tauri::State<'_, State>, iface: String, ap_iface: String, bssid: String, portal_port: Option, deauth: Option, ) -> Result { state .0 .start_evil_twin( &iface, &ap_iface, &bssid, portal_port.unwrap_or(8080), deauth.unwrap_or(false), ) .map_err(|e| e.to_string()) } #[tauri::command] fn eviltwin_job(state: tauri::State<'_, State>, id: String) -> Result { match state.0.eviltwin_job(&id) { Some(job) => serde_json::to_value(job).map_err(|e| e.to_string()), None => Err("job de Evil Twin não encontrado".into()), } } #[tauri::command] fn eviltwin_jobs(state: tauri::State<'_, State>) -> Result { serde_json::to_value(state.0.eviltwin_jobs()).map_err(|e| e.to_string()) } #[tauri::command] fn stop_evil_twin(state: tauri::State<'_, State>, id: String) -> Result<(), String> { state.0.stop_evil_twin(&id).map_err(|e| e.to_string()) } #[tauri::command] fn start_evil_twin_enterprise( state: tauri::State<'_, State>, iface: String, ap_iface: String, bssid: String, ) -> Result { state .0 .start_evil_twin_enterprise(&iface, &ap_iface, &bssid) .map_err(|e| e.to_string()) } #[tauri::command] fn eviltwin_enterprise_job( state: tauri::State<'_, State>, id: String, ) -> Result { match state.0.enterprise_twin_job(&id) { Some(job) => serde_json::to_value(job).map_err(|e| e.to_string()), None => Err("job de Evil Twin Enterprise não encontrado".into()), } } #[tauri::command] fn eviltwin_enterprise_jobs(state: tauri::State<'_, State>) -> Result { serde_json::to_value(state.0.enterprise_twin_jobs()).map_err(|e| e.to_string()) } #[tauri::command] fn stop_evil_twin_enterprise(state: tauri::State<'_, State>, id: String) -> Result<(), String> { state .0 .stop_evil_twin_enterprise(&id) .map_err(|e| e.to_string()) } #[tauri::command] #[allow(clippy::too_many_arguments)] fn start_scan_opts( state: tauri::State<'_, State>, iface: String, channel: Option, band: Option, hop_ms: Option, pcap_out: Option, seconds: Option, ) -> Result<(), String> { let options = ScanOptions { channel, band, hop_ms: hop_ms.unwrap_or(350), pcap_out: pcap_out.map(Into::into), duration_secs: seconds, }; state .0 .start_scan_with(&iface, options) .map_err(|e| e.to_string()) } #[tauri::command] fn update_check(state: tauri::State<'_, State>) -> Result { let status = state.0.update_check().map_err(|e| e.to_string())?; serde_json::to_value(status).map_err(|e| e.to_string()) } #[tauri::command] fn update_apply(state: tauri::State<'_, State>) -> Result { state.0.update_apply().map_err(|e| e.to_string()) } /// `true` quando o processo já tem privilégio suficiente pra tudo que o /// Sabiá faz (modo monitor, captura crua). No Windows o manifesto já /// exige administrador antes mesmo do app abrir, então aqui é sempre `true`. #[tauri::command] fn is_elevated() -> bool { #[cfg(unix)] { unsafe { libc::geteuid() == 0 } } #[cfg(windows)] { true } } /// Reabre o app com privilégios de administrador (Linux/macOS). O app novo /// encerra este quando abre; se a autorização falhar ou for cancelada, a /// janela continua aqui e a interface mostra o motivo. #[tauri::command] fn relaunch_elevated(app: tauri::AppHandle) -> Result<(), String> { #[cfg(any(target_os = "linux", target_os = "macos"))] { do_relaunch_elevated(app) } #[cfg(windows)] { let _ = app; Err("o Sabiá já roda como administrador no Windows".into()) } } #[cfg(any(target_os = "linux", target_os = "macos"))] fn do_relaunch_elevated(app: tauri::AppHandle) -> Result<(), String> { use tauri::Emitter; let exe = elevated_exe()?; let log = std::env::temp_dir().join(format!("sabia-elevate-{}.log", std::process::id())); let _ = std::fs::write(&log, b""); let out = std::fs::OpenOptions::new() .append(true) .open(&log) .map_err(|e| e.to_string())?; let err = out.try_clone().map_err(|e| e.to_string())?; let pid = std::process::id().to_string(); #[cfg(target_os = "linux")] let mut cmd = { let mut c = std::process::Command::new("pkexec"); c.arg("/usr/bin/env"); for k in [ "DISPLAY", "WAYLAND_DISPLAY", "XAUTHORITY", "XDG_RUNTIME_DIR", "DBUS_SESSION_BUS_ADDRESS", "GDK_BACKEND", ] { if let Ok(v) = std::env::var(k) { c.arg(format!("{k}={v}")); } } c.arg(format!("SABIA_REPLACE_PID={pid}")); c.arg(&exe); c }; #[cfg(target_os = "macos")] let mut cmd = { let shell = format!( "SABIA_REPLACE_PID={} '{}' >/dev/null 2>&1 &", pid, exe.display() ); let script = format!( "do shell script \"{}\" with administrator privileges", shell.replace('\\', "\\\\").replace('"', "\\\"") ); let mut c = std::process::Command::new("osascript"); c.args(["-e", &script]); c }; cmd.stdout(out).stderr(err); let mut child = cmd .spawn() .map_err(|e| format!("não foi possível pedir privilégios: {e}"))?; std::thread::spawn(move || { let status = child.wait(); if matches!(&status, Ok(s) if s.success()) { let _ = std::fs::remove_file(&log); return; } let detail = std::fs::read_to_string(&log).unwrap_or_default(); let msg = match status.ok().and_then(|s| s.code()) { Some(126) => "Autorização cancelada.".to_string(), Some(127) => "Não foi possível autenticar como administrador.".to_string(), _ => format!( "Não foi possível reiniciar como administrador. {}", detail.lines().last().unwrap_or("").trim() ), }; let _ = app.emit("elevate-failed", msg); let _ = std::fs::remove_file(&log); }); Ok(()) } /// Caminho que o root consegue executar. No AppImage, `current_exe` aponta /// pra dentro de um ponto de montagem FUSE do usuário, inacessível ao root: /// usa o próprio arquivo `.AppImage` (variável `APPIMAGE`). #[cfg(any(target_os = "linux", target_os = "macos"))] fn elevated_exe() -> Result { if let Some(p) = std::env::var_os("APPIMAGE") .map(std::path::PathBuf::from) .filter(|p| p.is_file()) { return Ok(p); } std::env::current_exe().map_err(|e| e.to_string()) } fn main() { let engine = Arc::new(Engine::new()); tauri::Builder::default() .manage(State(engine)) .setup(|app| { // Quando abrimos elevados a pedido do app normal, ele fecha o app antigo. #[cfg(unix)] if let Some(pid) = std::env::var("SABIA_REPLACE_PID") .ok() .and_then(|p| p.parse::().ok()) { std::thread::spawn(move || { std::thread::sleep(std::time::Duration::from_millis(1200)); // SAFETY: só envia SIGTERM ao processo anterior do próprio usuário. unsafe { if libc::geteuid() == 0 { libc::kill(pid, libc::SIGTERM); } } }); } // Captura crua e modo monitor exigem root: pede elevação assim // que o app abre, sem esperar o usuário procurar um botão. Se // a autorização falhar, a janela continua aberta e a interface // mostra o aviso (com um botão pra tentar de novo). #[cfg(any(target_os = "linux", target_os = "macos"))] if unsafe { libc::geteuid() != 0 } && std::env::var("SABIA_REPLACE_PID").is_err() { let handle = app.handle().clone(); std::thread::spawn(move || { let _ = do_relaunch_elevated(handle); }); } Ok(()) }) .invoke_handler(tauri::generate_handler![ version, list_ifaces, set_monitor, start_scan, stop_scan, scan_results, scan_snapshot, scan_clients, handshakes, export_handshake, export_all_handshakes, start_crack, start_crack_mask, crack_job, crack_jobs, deauth, deauth_directional, start_deauth_job, stop_deauth_job, deauth_job, deauth_jobs, capture_handshake, report, report_html, export_hc22000, export_all_hc22000, pmkid_hunt, pmf_test, wps_tools, start_wps, wps_job, wps_jobs, stop_wps, start_evil_twin, eviltwin_job, eviltwin_jobs, stop_evil_twin, start_evil_twin_enterprise, eviltwin_enterprise_job, eviltwin_enterprise_jobs, stop_evil_twin_enterprise, start_scan_opts, update_check, update_apply, is_elevated, relaunch_elevated, ]) .run(tauri::generate_context!()) .expect("erro ao iniciar o Sabiá"); }