use serde::Serialize; use ssh2::Session; use std::net::{ SocketAddr, TcpStream }; use std::process::Command; use std::time::Duration; #[derive(Serialize)] pub struct OpenWrtDetectionResult { detected: bool, ip: Option, method: String, ssh_reachable: bool, message: String, } #[derive(Serialize)] pub struct OpenWrtCompatibility { compatible: bool, version: Option, target: Option, arch: Option, reason: Option, } #[derive(Serialize)] pub struct OpenWrtSshResult { connected: bool, host: String, hostname: Option, openwrt_release: Option, compatibility: Option, readiness: Option, message: String, } #[derive(Serialize)] pub struct OpenWrtReadiness { internet_ok: bool, dns_ok: bool, opkg_ok: bool, openvpn_installed: bool, free_space: Option, free_memory: Option, } #[derive(Serialize)] pub struct OpenWrtPrepareResult { success: bool, hostname_updated: bool, password_updated: bool, message: String, logs: String, } #[tauri::command] pub fn detect_openwrt_router() -> OpenWrtDetectionResult { let candidates = vec![detect_default_gateway(), Some("192.168.1.1".to_string())]; for candidate in candidates.into_iter().flatten() { if tcp_port_open(&candidate, 22) { return OpenWrtDetectionResult { detected: true, ip: Some(candidate), method: "SSH_PORT_22".to_string(), ssh_reachable: true, message: "Router detectado com SSH disponível.".to_string(), }; } } OpenWrtDetectionResult { detected: false, ip: None, method: "NONE".to_string(), ssh_reachable: false, message: "Nenhum router OpenWRT detetado automaticamente.".to_string(), } } #[tauri::command] pub fn test_openwrt_ssh(host: String, username: String, password: String) -> OpenWrtSshResult { let address = format!("{}:22", host); let socket = match address.parse::() { Ok(socket) => socket, Err(err) => { return OpenWrtSshResult { connected: false, host, hostname: None, openwrt_release: None, compatibility: None, readiness: None, message: format!("Endereço inválido: {}", err), }; } }; let tcp = match TcpStream::connect_timeout(&socket, Duration::from_secs(5)) { Ok(stream) => stream, Err(err) => { return OpenWrtSshResult { connected: false, host, hostname: None, openwrt_release: None, compatibility: None, readiness: None, message: format!("Falha ao ligar por TCP/SSH: {}", err), }; } }; let mut session = match Session::new() { Ok(session) => session, Err(err) => { return OpenWrtSshResult { connected: false, host, hostname: None, openwrt_release: None, compatibility: None, readiness: None, message: format!("Falha ao criar sessão SSH: {}", err), }; } }; session.set_tcp_stream(tcp); if let Err(err) = session.handshake() { return OpenWrtSshResult { connected: false, host, hostname: None, openwrt_release: None, compatibility: None, readiness: None, message: format!("Falha no handshake SSH: {}", err), }; } if password.trim().is_empty() { return test_openwrt_ssh_with_system_ssh(host, username); } if let Err(err) = session.userauth_password(&username, &password) { return OpenWrtSshResult { connected: false, host, hostname: None, openwrt_release: None, compatibility: None, readiness: None, message: format!("Falha na autenticação SSH: {}", err), }; } let hostname = run_ssh_command( &session, "uci get system.@system[0].hostname 2>/dev/null || hostname" ); let release = run_ssh_command(&session, "cat /etc/openwrt_release 2>/dev/null"); let compatibility = release.as_ref().map(|content| build_compatibility(content)); let readiness = Some(build_readiness(&session)); OpenWrtSshResult { connected: true, host, hostname, openwrt_release: release, compatibility, readiness, message: "Ligação SSH estabelecida com sucesso.".to_string(), } } #[tauri::command] pub fn prepare_openwrt_router( host: String, username: String, password: String, target_hostname: String, new_password: String ) -> OpenWrtPrepareResult { if target_hostname.trim().is_empty() { return OpenWrtPrepareResult { success: false, hostname_updated: false, password_updated: false, message: "Hostname inválido.".to_string(), logs: "".to_string(), }; } if new_password.trim().is_empty() { return OpenWrtPrepareResult { success: false, hostname_updated: false, password_updated: false, message: "Password root inválida.".to_string(), logs: "".to_string(), }; } if password.trim().is_empty() { return prepare_openwrt_router_with_system_ssh( host, username, target_hostname, new_password ); } let address = format!("{}:22", host); let socket = match address.parse::() { Ok(socket) => socket, Err(err) => { return OpenWrtPrepareResult { success: false, hostname_updated: false, password_updated: false, message: format!("Endereço inválido: {}", err), logs: "".to_string(), }; } }; let tcp = match TcpStream::connect_timeout(&socket, Duration::from_secs(5)) { Ok(stream) => stream, Err(err) => { return OpenWrtPrepareResult { success: false, hostname_updated: false, password_updated: false, message: format!("Falha ao ligar por TCP/SSH: {}", err), logs: "".to_string(), }; } }; let mut session = match Session::new() { Ok(session) => session, Err(err) => { return OpenWrtPrepareResult { success: false, hostname_updated: false, password_updated: false, message: format!("Falha ao criar sessão SSH: {}", err), logs: "".to_string(), }; } }; session.set_tcp_stream(tcp); if let Err(err) = session.handshake() { return OpenWrtPrepareResult { success: false, hostname_updated: false, password_updated: false, message: format!("Falha no handshake SSH: {}", err), logs: "".to_string(), }; } if let Err(err) = session.userauth_password(&username, &password) { return OpenWrtPrepareResult { success: false, hostname_updated: false, password_updated: false, message: format!("Falha na autenticação SSH: {}", err), logs: "".to_string(), }; } let command = build_prepare_command(&target_hostname, &new_password); let logs = run_ssh_command(&session, &command).unwrap_or_default(); let hostname_updated = logs.contains("HOSTNAME_UPDATED"); let password_updated = logs.contains("PASSWORD_UPDATED"); let success = hostname_updated && password_updated; OpenWrtPrepareResult { success, hostname_updated, password_updated, message: if success { "Router preparado com sucesso.".to_string() } else { "Preparação do router incompleta. Verifique os logs.".to_string() }, logs, } } fn tcp_port_open(host: &str, port: u16) -> bool { let address = format!("{}:{}", host, port); match address.parse::() { Ok(socket) => TcpStream::connect_timeout(&socket, Duration::from_secs(2)).is_ok(), Err(_) => false, } } fn run_ssh_command(session: &Session, command: &str) -> Option { let mut channel = session.channel_session().ok()?; channel.exec(command).ok()?; let mut output = String::new(); std::io::Read::read_to_string(&mut channel, &mut output).ok()?; channel.wait_close().ok()?; let trimmed = output.trim().to_string(); if trimmed.is_empty() { None } else { Some(trimmed) } } fn detect_default_gateway() -> Option { #[cfg(target_os = "windows")] { let output = Command::new("cmd").args(["/C", "ipconfig"]).output().ok()?; let text = String::from_utf8_lossy(&output.stdout); for line in text.lines() { if line.contains("Default Gateway") { if let Some(ip) = line.split(':').nth(1) { let ip = ip.trim(); if !ip.is_empty() && ip.contains('.') { return Some(ip.to_string()); } } } } } None } fn test_openwrt_ssh_with_system_ssh(host: String, username: String) -> OpenWrtSshResult { let target = format!("{}@{}", username, host); let output = Command::new("ssh") .args([ "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=NUL", "-o", "BatchMode=yes", "-o", "ConnectTimeout=5", &target, "uci get system.@system[0].hostname 2>/dev/null || hostname; \ echo '---OPENWRT_RELEASE---'; \ cat /etc/openwrt_release 2>/dev/null; \ echo '---READINESS---'; \ ping -c 1 -W 2 1.1.1.1 >/dev/null 2>&1 && echo INTERNET_OK || echo INTERNET_FAIL; \ nslookup openwrt.org >/dev/null 2>&1 && echo DNS_OK || echo DNS_FAIL; \ command -v opkg >/dev/null 2>&1 && echo OPKG_OK || echo OPKG_FAIL; \ command -v openvpn >/dev/null 2>&1 && echo OPENVPN_OK || echo OPENVPN_FAIL; \ echo FREE_SPACE=$(df -h /overlay 2>/dev/null | awk 'NR==2 {print $4}'); \ echo FREE_MEMORY=$(free -h 2>/dev/null | awk '/Mem:/ {print $4}')", ]) .output(); let output = match output { Ok(output) => output, Err(err) => { return OpenWrtSshResult { connected: false, host, hostname: None, openwrt_release: None, compatibility: None, readiness: None, message: format!("Falha ao executar ssh local: {}", err), }; } }; if !output.status.success() { return OpenWrtSshResult { connected: false, host, hostname: None, openwrt_release: None, compatibility: None, readiness: None, message: String::from_utf8_lossy(&output.stderr).trim().to_string(), }; } let stdout = String::from_utf8_lossy(&output.stdout).to_string(); let parts: Vec<&str> = stdout.split("---OPENWRT_RELEASE---").collect(); let hostname = parts .get(0) .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()); let release_and_readiness = parts.get(1).copied().unwrap_or(""); let release_parts: Vec<&str> = release_and_readiness.split("---READINESS---").collect(); let openwrt_release = release_parts .get(0) .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()); let readiness_output = release_parts .get(1) .map(|value| value.trim().to_string()) .unwrap_or_default(); let compatibility = openwrt_release.as_ref().map(|content| build_compatibility(content)); let readiness = Some(build_readiness_from_output(&readiness_output)); OpenWrtSshResult { connected: true, host, hostname, openwrt_release, compatibility, readiness, message: "Ligação SSH estabelecida com sucesso.".to_string(), } } fn build_compatibility(release_content: &str) -> OpenWrtCompatibility { let version = extract_release_value(release_content, "DISTRIB_RELEASE"); let target = extract_release_value(release_content, "DISTRIB_TARGET"); let arch = extract_release_value(release_content, "DISTRIB_ARCH"); let compatible = version .as_ref() .map(|value| is_supported_openwrt_version(value)) .unwrap_or(false); let reason = if compatible { None } else { Some("Versão OpenWRT não suportada. É necessária a versão 21.02.2 ou superior.".to_string()) }; OpenWrtCompatibility { compatible, version, target, arch, reason, } } fn extract_release_value(content: &str, key: &str) -> Option { content .lines() .find(|line| line.starts_with(key)) .and_then(|line| line.split('=').nth(1)) .map(|value| value.trim().trim_matches('\'').to_string()) .filter(|value| !value.is_empty()) } fn is_supported_openwrt_version(version: &str) -> bool { let parts: Vec = version .split('.') .filter_map(|part| part.parse::().ok()) .collect(); if parts.len() < 2 { return false; } let major = parts[0]; let minor = parts[1]; let patch = parts.get(2).copied().unwrap_or(0); if major > 21 { return true; } if major == 21 && minor > 2 { return true; } if major == 21 && minor == 2 && patch >= 2 { return true; } false } fn build_readiness(session: &Session) -> OpenWrtReadiness { let internet_ok = run_ssh_command( session, "ping -c 1 -W 2 1.1.1.1 >/dev/null 2>&1 && echo OK || echo FAIL" ) .map(|value| value == "OK") .unwrap_or(false); let dns_ok = run_ssh_command( session, "nslookup openwrt.org >/dev/null 2>&1 && echo OK || echo FAIL" ) .map(|value| value == "OK") .unwrap_or(false); let opkg_ok = run_ssh_command( session, "command -v opkg >/dev/null 2>&1 && echo OK || echo FAIL" ) .map(|value| value == "OK") .unwrap_or(false); let openvpn_installed = run_ssh_command( session, "command -v openvpn >/dev/null 2>&1 && echo OK || echo FAIL" ) .map(|value| value == "OK") .unwrap_or(false); let free_space = run_ssh_command( session, "df -h /overlay 2>/dev/null | awk 'NR==2 {print $4}'" ); let free_memory = run_ssh_command(session, "free -h 2>/dev/null | awk '/Mem:/ {print $4}'"); OpenWrtReadiness { internet_ok, dns_ok, opkg_ok, openvpn_installed, free_space, free_memory, } } fn build_readiness_from_output(output: &str) -> OpenWrtReadiness { OpenWrtReadiness { internet_ok: output.contains("INTERNET_OK"), dns_ok: output.contains("DNS_OK"), opkg_ok: output.contains("OPKG_OK"), openvpn_installed: output.contains("OPENVPN_OK"), free_space: extract_prefixed_value(output, "FREE_SPACE="), free_memory: extract_prefixed_value(output, "FREE_MEMORY="), } } fn extract_prefixed_value(content: &str, prefix: &str) -> Option { content .lines() .find(|line| line.starts_with(prefix)) .map(|line| line.trim_start_matches(prefix).trim().to_string()) .filter(|value| !value.is_empty()) } fn prepare_openwrt_router_with_system_ssh( host: String, username: String, target_hostname: String, new_password: String ) -> OpenWrtPrepareResult { let target = format!("{}@{}", username, host); let command = build_prepare_command(&target_hostname, &new_password); let output = Command::new("ssh") .args([ "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=NUL", "-o", "BatchMode=yes", "-o", "ConnectTimeout=5", &target, &command, ]) .output(); let output = match output { Ok(output) => output, Err(err) => { return OpenWrtPrepareResult { success: false, hostname_updated: false, password_updated: false, message: format!("Falha ao executar ssh local: {}", err), logs: "".to_string(), }; } }; let stdout = String::from_utf8_lossy(&output.stdout).to_string(); let stderr = String::from_utf8_lossy(&output.stderr).to_string(); let logs = format!("{}\n{}", stdout, stderr); let hostname_updated = logs.contains("HOSTNAME_UPDATED"); let password_updated = logs.contains("PASSWORD_UPDATED"); let success = output.status.success() && hostname_updated && password_updated; OpenWrtPrepareResult { success, hostname_updated, password_updated, message: if success { "Router preparado com sucesso.".to_string() } else { "Preparação do router incompleta. Verifique os logs.".to_string() }, logs, } } fn build_prepare_command(target_hostname: &str, new_password: &str) -> String { let safe_hostname = shell_escape_single_quotes(target_hostname); let safe_password = shell_escape_single_quotes(new_password); format!( "set -e; \ uci set system.@system[0].hostname='{hostname}'; \ uci commit system; \ /etc/init.d/system reload >/dev/null 2>&1 || true; \ echo HOSTNAME_UPDATED; \ printf '%s\\n%s\\n' '{password}' '{password}' | passwd root; \ echo PASSWORD_UPDATED", hostname = safe_hostname, password = safe_password ) } fn shell_escape_single_quotes(value: &str) -> String { value.replace('\'', "'\\''") }