648 lines
18 KiB
Rust
648 lines
18 KiB
Rust
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<String>,
|
|
method: String,
|
|
ssh_reachable: bool,
|
|
message: String,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub struct OpenWrtCompatibility {
|
|
compatible: bool,
|
|
version: Option<String>,
|
|
target: Option<String>,
|
|
arch: Option<String>,
|
|
reason: Option<String>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub struct OpenWrtSshResult {
|
|
connected: bool,
|
|
host: String,
|
|
hostname: Option<String>,
|
|
openwrt_release: Option<String>,
|
|
compatibility: Option<OpenWrtCompatibility>,
|
|
readiness: Option<OpenWrtReadiness>,
|
|
message: String,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub struct OpenWrtReadiness {
|
|
internet_ok: bool,
|
|
dns_ok: bool,
|
|
opkg_ok: bool,
|
|
openvpn_installed: bool,
|
|
free_space: Option<String>,
|
|
free_memory: Option<String>,
|
|
}
|
|
|
|
#[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::<SocketAddr>() {
|
|
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::<SocketAddr>() {
|
|
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::<SocketAddr>() {
|
|
Ok(socket) => TcpStream::connect_timeout(&socket, Duration::from_secs(2)).is_ok(),
|
|
Err(_) => false,
|
|
}
|
|
}
|
|
|
|
fn run_ssh_command(session: &Session, command: &str) -> Option<String> {
|
|
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<String> {
|
|
#[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<String> {
|
|
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<u32> = version
|
|
.split('.')
|
|
.filter_map(|part| part.parse::<u32>().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<String> {
|
|
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('\'', "'\\''")
|
|
}
|