feat(openwrt): build router detection and provisioning readiness flow
This commit is contained in:
Generated
+57
@@ -1928,6 +1928,32 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libssh2-sys"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"libz-sys",
|
||||||
|
"openssl-sys",
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libz-sys"
|
||||||
|
version = "1.1.28"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
@@ -1961,6 +1987,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"ssh2",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-dialog",
|
"tauri-plugin-dialog",
|
||||||
@@ -2323,6 +2350,18 @@ dependencies = [
|
|||||||
"pathdiff",
|
"pathdiff",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-sys"
|
||||||
|
version = "0.9.115"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "option-ext"
|
name = "option-ext"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -3242,6 +3281,18 @@ dependencies = [
|
|||||||
"system-deps",
|
"system-deps",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ssh2"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2f84d13b3b8a0d4e91a2629911e951db1bb8671512f5c09d7d4ba34500ba68c8"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.11.1",
|
||||||
|
"libc",
|
||||||
|
"libssh2-sys",
|
||||||
|
"parking_lot",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stable_deref_trait"
|
name = "stable_deref_trait"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
@@ -4179,6 +4230,12 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vcpkg"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version-compare"
|
name = "version-compare"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
|||||||
@@ -24,4 +24,4 @@ serde = { version = "1", features = ["derive"] }
|
|||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
tauri-plugin-dialog = "2.7.1"
|
tauri-plugin-dialog = "2.7.1"
|
||||||
tauri-plugin-fs = "2.5.1"
|
tauri-plugin-fs = "2.5.1"
|
||||||
|
ssh2 = "0.9"
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||||
#[tauri::command]
|
mod openwrt;
|
||||||
fn greet(name: &str) -> String {
|
|
||||||
format!("Hello, {}! You've been greeted from Rust!", name)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.invoke_handler(tauri::generate_handler![
|
||||||
.plugin(tauri_plugin_fs::init())
|
openwrt::detect_openwrt_router,
|
||||||
|
openwrt::test_openwrt_ssh
|
||||||
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,438 @@
|
|||||||
|
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>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
+46
@@ -1377,3 +1377,49 @@ td .small-action {
|
|||||||
background: white;
|
background: white;
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.openwrt-form {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr auto;
|
||||||
|
align-items: end;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openwrt-form label {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
color: #374151;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openwrt-form input {
|
||||||
|
height: 44px;
|
||||||
|
border: 1px solid #edf1f7;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 0 14px;
|
||||||
|
background: #f8fafc;
|
||||||
|
color: #111827;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openwrt-form input:focus {
|
||||||
|
outline: 0;
|
||||||
|
background: white;
|
||||||
|
border-color: #5da8ff;
|
||||||
|
box-shadow: 0 0 0 4px rgba(93, 168, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.openwrt-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openwrt-grid .dashboard-card {
|
||||||
|
padding: 26px;
|
||||||
|
border-radius: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openwrt-grid .card-header {
|
||||||
|
margin-bottom: 22px;
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ import type { OpenVpnStatus } from "./types/openVpnStatus";
|
|||||||
import type { OpenVpnHealthResponse } from "./types/openVpnHealthResponse";
|
import type { OpenVpnHealthResponse } from "./types/openVpnHealthResponse";
|
||||||
import { VpsServerPage } from "./pages/VpsServerPage";
|
import { VpsServerPage } from "./pages/VpsServerPage";
|
||||||
import { SettingsPage } from "./pages/SettingsPage";
|
import { SettingsPage } from "./pages/SettingsPage";
|
||||||
|
import { OpenWrtConfigPage } from "./pages/OpenWrtConfigPage";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
availableSubnets,
|
availableSubnets,
|
||||||
@@ -405,6 +406,10 @@ function App() {
|
|||||||
onRestartOpenVpn={() => setRestartOpenVpnConfirmOpen(true)}
|
onRestartOpenVpn={() => setRestartOpenVpnConfirmOpen(true)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{page === "openwrt" && (
|
||||||
|
<OpenWrtConfigPage />
|
||||||
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{createOpen && (
|
{createOpen && (
|
||||||
|
|||||||
@@ -59,11 +59,11 @@ export function Sidebar({
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className={page === "clients" ? "active" : ""}
|
className={page === "openwrt" ? "active" : ""}
|
||||||
onClick={() => onPageChange("clients")}
|
onClick={() => onPageChange("openwrt")}
|
||||||
>
|
>
|
||||||
<Shield size={18} />
|
<Shield size={18} />
|
||||||
Clientes OpenVPN
|
OpenWRT Config
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -0,0 +1,326 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import {
|
||||||
|
AlertTriangle,
|
||||||
|
CheckCircle2,
|
||||||
|
Router,
|
||||||
|
Search,
|
||||||
|
ShieldCheck,
|
||||||
|
XCircle,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
type DetectionResult = {
|
||||||
|
detected: boolean;
|
||||||
|
ip: string | null;
|
||||||
|
method: string;
|
||||||
|
ssh_reachable: boolean;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CompatibilityResult = {
|
||||||
|
compatible: boolean;
|
||||||
|
version: string | null;
|
||||||
|
target: string | null;
|
||||||
|
arch: string | null;
|
||||||
|
reason: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ReadinessResult = {
|
||||||
|
internet_ok: boolean;
|
||||||
|
dns_ok: boolean;
|
||||||
|
opkg_ok: boolean;
|
||||||
|
openvpn_installed: boolean;
|
||||||
|
free_space: string | null;
|
||||||
|
free_memory: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SshResult = {
|
||||||
|
connected: boolean;
|
||||||
|
host: string;
|
||||||
|
hostname: string | null;
|
||||||
|
openwrt_release: string | null;
|
||||||
|
compatibility: CompatibilityResult | null;
|
||||||
|
readiness: ReadinessResult | null;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function OpenWrtConfigPage() {
|
||||||
|
const [detecting, setDetecting] = useState(false);
|
||||||
|
const [connecting, setConnecting] = useState(false);
|
||||||
|
const [detection, setDetection] = useState<DetectionResult | null>(null);
|
||||||
|
const [sshResult, setSshResult] = useState<SshResult | null>(null);
|
||||||
|
|
||||||
|
const [host, setHost] = useState("");
|
||||||
|
const [username, setUsername] = useState("root");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
|
||||||
|
const compatibility = sshResult?.compatibility;
|
||||||
|
|
||||||
|
async function detectRouter() {
|
||||||
|
try {
|
||||||
|
setDetecting(true);
|
||||||
|
setSshResult(null);
|
||||||
|
|
||||||
|
const result = await invoke<DetectionResult>("detect_openwrt_router");
|
||||||
|
|
||||||
|
setDetection(result);
|
||||||
|
|
||||||
|
if (result.ip) {
|
||||||
|
setHost(result.ip);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setDetecting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connectSsh() {
|
||||||
|
try {
|
||||||
|
setConnecting(true);
|
||||||
|
|
||||||
|
const result = await invoke<SshResult>("test_openwrt_ssh", {
|
||||||
|
host,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
setSshResult(result);
|
||||||
|
} finally {
|
||||||
|
setConnecting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="page-stack">
|
||||||
|
<div className="page-header">
|
||||||
|
<div>
|
||||||
|
<h1>Configuração OpenWRT</h1>
|
||||||
|
<p>Detetar router local, testar SSH e validar compatibilidade</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button className="primary" onClick={detectRouter} disabled={detecting}>
|
||||||
|
<Search size={16} />
|
||||||
|
{detecting ? "A detetar..." : "Detetar Router"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="openwrt-grid">
|
||||||
|
<div className="dashboard-card">
|
||||||
|
<div className="card-header">
|
||||||
|
<div>
|
||||||
|
<h2>Deteção do Router</h2>
|
||||||
|
<p>Procura pelo gateway local ou pelo IP padrão OpenWRT</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="status-note">
|
||||||
|
<Router size={24} />
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{detection
|
||||||
|
? detection.message
|
||||||
|
: "Ainda não foi executada nenhuma deteção."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{detection?.ip && (
|
||||||
|
<div className="detail-list">
|
||||||
|
<DetailRow label="IP detetado" value={detection.ip} />
|
||||||
|
<DetailRow label="Método" value={detection.method} />
|
||||||
|
<DetailRow
|
||||||
|
label="SSH"
|
||||||
|
value={detection.ssh_reachable ? "Disponível" : "Indisponível"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="dashboard-card">
|
||||||
|
<div className="card-header">
|
||||||
|
<div>
|
||||||
|
<h2>Ligação SSH</h2>
|
||||||
|
<p>Introduza as credenciais do router OpenWRT</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="openwrt-form">
|
||||||
|
<label>
|
||||||
|
IP do Router
|
||||||
|
<input value={host} onChange={(e) => setHost(e.target.value)} />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Utilizador
|
||||||
|
<input
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Password
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
placeholder="Password SSH"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="primary"
|
||||||
|
onClick={connectSsh}
|
||||||
|
disabled={connecting || !host || !username}
|
||||||
|
>
|
||||||
|
<ShieldCheck size={16} />
|
||||||
|
{connecting ? "A ligar..." : "Ligar ao Router"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{sshResult && (
|
||||||
|
<div className="dashboard-card wide-panel">
|
||||||
|
<div className="card-header">
|
||||||
|
<div>
|
||||||
|
<h2>Resultado da Ligação</h2>
|
||||||
|
<p>Estado da sessão SSH com o router</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="detail-list">
|
||||||
|
<DetailRow
|
||||||
|
label="Estado"
|
||||||
|
value={sshResult.connected ? "Ligado" : "Falhou"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DetailRow label="Host" value={sshResult.host} />
|
||||||
|
<DetailRow label="Hostname" value={sshResult.hostname || "-"} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!sshResult.connected && (
|
||||||
|
<div className="status-note">
|
||||||
|
<XCircle size={24} />
|
||||||
|
<p>{sshResult.message}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{sshResult?.connected && compatibility && (
|
||||||
|
<div className="dashboard-card wide-panel">
|
||||||
|
<div className="card-header">
|
||||||
|
<div>
|
||||||
|
<h2>Compatibilidade OpenWRT</h2>
|
||||||
|
<p>Validação da versão e arquitetura do firmware</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span
|
||||||
|
className={`badge ${compatibility.compatible ? "success" : "failed"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{compatibility.compatible ? "Compatível" : "Não compatível"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`status-note ${compatibility.compatible ? "compatible" : "unsupported"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{compatibility.compatible ? (
|
||||||
|
<CheckCircle2 size={24} />
|
||||||
|
) : (
|
||||||
|
<AlertTriangle size={24} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{compatibility.compatible
|
||||||
|
? "Este router cumpre os requisitos mínimos para configuração OpenVPN."
|
||||||
|
: compatibility.reason ||
|
||||||
|
"Este router não cumpre os requisitos mínimos."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="detail-list">
|
||||||
|
<DetailRow
|
||||||
|
label="Versão OpenWRT"
|
||||||
|
value={compatibility.version || "-"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DetailRow
|
||||||
|
label="Target"
|
||||||
|
value={compatibility.target || "-"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DetailRow
|
||||||
|
label="Arquitetura"
|
||||||
|
value={compatibility.arch || "-"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DetailRow
|
||||||
|
label="Versão mínima"
|
||||||
|
value="21.02.2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{sshResult.openwrt_release && (
|
||||||
|
<div className="log-box">
|
||||||
|
<pre>{sshResult.openwrt_release}</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{sshResult?.connected && sshResult.readiness && (
|
||||||
|
<div className="dashboard-card wide-panel">
|
||||||
|
<div className="card-header">
|
||||||
|
<div>
|
||||||
|
<h2>Estado do Router</h2>
|
||||||
|
<p>Verificações antes da configuração OpenVPN</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="detail-list">
|
||||||
|
<DetailRow
|
||||||
|
label="Internet"
|
||||||
|
value={sshResult.readiness.internet_ok ? "Disponível" : "Indisponível"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DetailRow
|
||||||
|
label="DNS"
|
||||||
|
value={sshResult.readiness.dns_ok ? "Disponível" : "Indisponível"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DetailRow
|
||||||
|
label="OPKG"
|
||||||
|
value={sshResult.readiness.opkg_ok ? "Disponível" : "Indisponível"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DetailRow
|
||||||
|
label="OpenVPN"
|
||||||
|
value={sshResult.readiness.openvpn_installed ? "Instalado" : "Não instalado"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DetailRow
|
||||||
|
label="Espaço livre"
|
||||||
|
value={sshResult.readiness.free_space || "-"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DetailRow
|
||||||
|
label="Memória livre"
|
||||||
|
value={sshResult.readiness.free_memory || "-"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DetailRow({ label, value }: { label: string; value: string }) {
|
||||||
|
return (
|
||||||
|
<div className="detail-row">
|
||||||
|
<span>{label}</span>
|
||||||
|
<strong>{value}</strong>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user