Finished controllers UI
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
use ssh2::Session;
|
||||
use std::{
|
||||
io::Read,
|
||||
net::{TcpStream, ToSocketAddrs},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
fn run_vps_command(
|
||||
password: &str,
|
||||
command: &str,
|
||||
) -> Result<String, String> {
|
||||
let address = "146.59.230.190:22";
|
||||
|
||||
let socket_address = address
|
||||
.to_socket_addrs()
|
||||
.map_err(|error| {
|
||||
format!(
|
||||
"failed to resolve VPS address: {}",
|
||||
error
|
||||
)
|
||||
})?
|
||||
.next()
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"failed to resolve VPS address {}",
|
||||
address
|
||||
)
|
||||
})?;
|
||||
|
||||
let tcp = TcpStream::connect_timeout(
|
||||
&socket_address,
|
||||
Duration::from_secs(8),
|
||||
)
|
||||
.map_err(|error| {
|
||||
format!(
|
||||
"failed to connect to VPS SSH: {}",
|
||||
error
|
||||
)
|
||||
})?;
|
||||
|
||||
let _ = tcp.set_read_timeout(Some(
|
||||
Duration::from_secs(30),
|
||||
));
|
||||
|
||||
let _ = tcp.set_write_timeout(Some(
|
||||
Duration::from_secs(30),
|
||||
));
|
||||
|
||||
let mut session = Session::new()
|
||||
.map_err(|error| {
|
||||
format!(
|
||||
"failed to create VPS SSH session: {}",
|
||||
error
|
||||
)
|
||||
})?;
|
||||
|
||||
session.set_tcp_stream(tcp);
|
||||
|
||||
session.handshake().map_err(|error| {
|
||||
format!(
|
||||
"VPS SSH handshake failed: {}",
|
||||
error
|
||||
)
|
||||
})?;
|
||||
|
||||
session
|
||||
.userauth_password("lr-vpn", password)
|
||||
.map_err(|error| {
|
||||
format!(
|
||||
"VPS SSH authentication failed: {}",
|
||||
error
|
||||
)
|
||||
})?;
|
||||
|
||||
if !session.authenticated() {
|
||||
return Err(
|
||||
"VPS SSH authentication failed"
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
let mut channel = session
|
||||
.channel_session()
|
||||
.map_err(|error| {
|
||||
format!(
|
||||
"failed to open VPS SSH channel: {}",
|
||||
error
|
||||
)
|
||||
})?;
|
||||
|
||||
channel
|
||||
.exec(command)
|
||||
.map_err(|error| {
|
||||
format!(
|
||||
"failed to execute VPS command: {}",
|
||||
error
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut stdout = String::new();
|
||||
|
||||
channel
|
||||
.read_to_string(&mut stdout)
|
||||
.map_err(|error| {
|
||||
format!(
|
||||
"failed to read VPS command output: {}",
|
||||
error
|
||||
)
|
||||
})?;
|
||||
|
||||
channel.wait_close().ok();
|
||||
|
||||
let exit_status = channel
|
||||
.exit_status()
|
||||
.map_err(|error| {
|
||||
format!(
|
||||
"failed to read VPS command exit status: {}",
|
||||
error
|
||||
)
|
||||
})?;
|
||||
|
||||
if exit_status != 0 {
|
||||
return Err(format!(
|
||||
"VPS command failed with exit code {}:\n{}",
|
||||
exit_status,
|
||||
stdout
|
||||
));
|
||||
}
|
||||
|
||||
Ok(stdout)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_controller_clients(
|
||||
password: String,
|
||||
) -> Result<String, String> {
|
||||
if password.trim().is_empty() {
|
||||
return Err(
|
||||
"VPS password is required".into(),
|
||||
);
|
||||
}
|
||||
|
||||
run_vps_command(
|
||||
&password,
|
||||
"sudo /usr/local/sbin/lr-controllers-list",
|
||||
)
|
||||
}
|
||||
@@ -2,3 +2,4 @@ pub mod files;
|
||||
pub mod network;
|
||||
pub mod router;
|
||||
pub mod ssh;
|
||||
pub mod controllers;
|
||||
+196
-325
@@ -2,17 +2,25 @@ use ssh2::Session;
|
||||
use std::process::Command;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{ Read, Write },
|
||||
net::{ TcpStream, ToSocketAddrs },
|
||||
path::Path,
|
||||
io::{Read, Write},
|
||||
net::{TcpStream, ToSocketAddrs},
|
||||
path::{Path, PathBuf},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
fn delay() {
|
||||
thread::sleep(Duration::from_millis(350));
|
||||
}
|
||||
|
||||
fn resource_path(app: &AppHandle, relative_path: &str) -> Result<PathBuf, String> {
|
||||
app.path()
|
||||
.resource_dir()
|
||||
.map_err(|error| format!("failed to resolve resource directory: {}", error))
|
||||
.map(|resource_dir| resource_dir.join("resources").join(relative_path))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn detect_router(ip: String) -> Result<bool, String> {
|
||||
delay();
|
||||
@@ -64,9 +72,12 @@ fn run_system_ssh(ip: &str, command: &str) -> Result<String, String> {
|
||||
return Ok(format!("{}{}", stdout, stderr));
|
||||
}
|
||||
|
||||
Err(
|
||||
format!("system ssh failed with status {:?}:\n{}\n{}", output.status.code(), stderr, stdout)
|
||||
)
|
||||
Err(format!(
|
||||
"system ssh failed with status {:?}:\n{}\n{}",
|
||||
output.status.code(),
|
||||
stderr,
|
||||
stdout
|
||||
))
|
||||
}
|
||||
|
||||
fn open_router_session(ip: &str, password: &str) -> Result<Session, String> {
|
||||
@@ -78,20 +89,20 @@ fn open_router_session(ip: &str, password: &str) -> Result<Session, String> {
|
||||
.next()
|
||||
.ok_or_else(|| format!("failed to resolve router address {}", address))?;
|
||||
|
||||
let tcp = TcpStream::connect_timeout(&socket_address, Duration::from_secs(8)).map_err(|error|
|
||||
format!("failed to connect to SSH on {}: {}", ip, error)
|
||||
)?;
|
||||
let tcp = TcpStream::connect_timeout(&socket_address, Duration::from_secs(8))
|
||||
.map_err(|error| format!("failed to connect to SSH on {}: {}", ip, error))?;
|
||||
|
||||
let _ = tcp.set_read_timeout(Some(Duration::from_secs(30)));
|
||||
let _ = tcp.set_write_timeout(Some(Duration::from_secs(30)));
|
||||
|
||||
let mut session = Session::new().map_err(|error|
|
||||
format!("failed to create SSH session: {}", error)
|
||||
)?;
|
||||
let mut session =
|
||||
Session::new().map_err(|error| format!("failed to create SSH session: {}", error))?;
|
||||
|
||||
session.set_tcp_stream(tcp);
|
||||
|
||||
session.handshake().map_err(|error| format!("SSH handshake failed for {}: {}", ip, error))?;
|
||||
session
|
||||
.handshake()
|
||||
.map_err(|error| format!("SSH handshake failed for {}: {}", ip, error))?;
|
||||
|
||||
authenticate_router(&session, password).map_err(|error| format!("root@{}: {}", ip, error))?;
|
||||
|
||||
@@ -102,11 +113,10 @@ fn scp_file_from_disk(
|
||||
session: &Session,
|
||||
local_path: &str,
|
||||
remote_path: &str,
|
||||
mode: i32
|
||||
mode: i32,
|
||||
) -> Result<(), String> {
|
||||
let mut local_file = File::open(local_path).map_err(|error|
|
||||
format!("failed to open {}: {}", local_path, error)
|
||||
)?;
|
||||
let mut local_file =
|
||||
File::open(local_path).map_err(|error| format!("failed to open {}: {}", local_path, error))?;
|
||||
|
||||
let file_size = local_file
|
||||
.metadata()
|
||||
@@ -135,12 +145,7 @@ fn scp_file_from_disk(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn scp_string(
|
||||
session: &Session,
|
||||
content: &str,
|
||||
remote_path: &str,
|
||||
mode: i32
|
||||
) -> Result<(), String> {
|
||||
fn scp_string(session: &Session, content: &str, remote_path: &str, mode: i32) -> Result<(), String> {
|
||||
let bytes = content.as_bytes();
|
||||
|
||||
let mut remote_file = session
|
||||
@@ -260,9 +265,15 @@ pub async fn verify_router(ip: String) -> Result<bool, String> {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn upload_firmware_to_router(ip: String, password: String) -> Result<String, String> {
|
||||
let local_firmware_path =
|
||||
"resources/firmware/openwrt-23.05.5-zbt-we826-16m-litoral-golden-sysupgrade.bin";
|
||||
pub async fn upload_firmware_to_router(
|
||||
app: AppHandle,
|
||||
ip: String,
|
||||
password: String,
|
||||
) -> Result<String, String> {
|
||||
let local_firmware_path = resource_path(
|
||||
&app,
|
||||
"firmware/openwrt-23.05.5-zbt-we826-16m-litoral-golden-sysupgrade.bin",
|
||||
)?;
|
||||
|
||||
let remote_firmware_path = "/tmp/firmware.bin";
|
||||
|
||||
@@ -270,16 +281,11 @@ pub async fn upload_firmware_to_router(ip: String, password: String) -> Result<S
|
||||
return Err("router IP is required".into());
|
||||
}
|
||||
|
||||
/*
|
||||
* FACTORY / FRESH OPENWRT PATH
|
||||
* Uses system scp when no password is set.
|
||||
*/
|
||||
|
||||
if password.trim().is_empty() {
|
||||
let target = format!("root@{}:{}", ip, remote_firmware_path);
|
||||
let local_firmware_path_string = local_firmware_path.to_string_lossy().to_string();
|
||||
|
||||
let output = std::process::Command
|
||||
::new("scp")
|
||||
let output = Command::new("scp")
|
||||
.args([
|
||||
"-O",
|
||||
"-o",
|
||||
@@ -288,103 +294,80 @@ pub async fn upload_firmware_to_router(ip: String, password: String) -> Result<S
|
||||
"ConnectTimeout=10",
|
||||
"-o",
|
||||
"StrictHostKeyChecking=accept-new",
|
||||
local_firmware_path,
|
||||
&local_firmware_path_string,
|
||||
&target,
|
||||
])
|
||||
.output()
|
||||
.map_err(|error| { format!("failed to run system scp: {}", error) })?;
|
||||
.map_err(|error| format!("failed to run system scp: {}", error))?;
|
||||
|
||||
if output.status.success() {
|
||||
return Ok(
|
||||
format!("Firmware uploaded to {}:{} using system scp", ip, remote_firmware_path)
|
||||
);
|
||||
return Ok(format!(
|
||||
"Firmware uploaded to {}:{} using system scp",
|
||||
ip, remote_firmware_path
|
||||
));
|
||||
}
|
||||
|
||||
return Err(
|
||||
format!(
|
||||
"system scp failed:\n{}\n{}",
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
)
|
||||
);
|
||||
return Err(format!(
|
||||
"system scp failed:\n{}\n{}",
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
));
|
||||
}
|
||||
|
||||
/*
|
||||
* PASSWORD SSH PATH
|
||||
*/
|
||||
let session = open_router_session(&ip, &password)?;
|
||||
|
||||
let address = format!("{}:22", ip);
|
||||
|
||||
let socket_address = address
|
||||
.to_socket_addrs()
|
||||
.map_err(|error| { format!("failed to resolve router address: {}", error) })?
|
||||
.next()
|
||||
.ok_or_else(|| { format!("failed to resolve router address {}", address) })?;
|
||||
|
||||
let tcp = TcpStream::connect_timeout(&socket_address, Duration::from_secs(8)).map_err(|error| {
|
||||
format!("failed to connect to SSH on {}: {}", ip, error)
|
||||
})?;
|
||||
|
||||
tcp
|
||||
.set_read_timeout(Some(Duration::from_secs(30)))
|
||||
.map_err(|error| { format!("failed to set read timeout: {}", error) })?;
|
||||
|
||||
tcp
|
||||
.set_write_timeout(Some(Duration::from_secs(30)))
|
||||
.map_err(|error| { format!("failed to set write timeout: {}", error) })?;
|
||||
|
||||
let mut session = Session::new().map_err(|error| {
|
||||
format!("failed to create SSH session: {}", error)
|
||||
})?;
|
||||
|
||||
session.set_tcp_stream(tcp);
|
||||
|
||||
session.handshake().map_err(|error| { format!("SSH handshake failed for {}: {}", ip, error) })?;
|
||||
|
||||
session
|
||||
.userauth_password("root", &password)
|
||||
.map_err(|error| { format!("SSH authentication failed for root@{}: {}", ip, error) })?;
|
||||
|
||||
if !session.authenticated() {
|
||||
return Err(format!("SSH authentication failed for root@{}", ip));
|
||||
}
|
||||
|
||||
let mut local_file = File::open(local_firmware_path).map_err(|error| {
|
||||
format!("failed to open local firmware file {}: {}", local_firmware_path, error)
|
||||
let mut local_file = File::open(&local_firmware_path).map_err(|error| {
|
||||
format!(
|
||||
"failed to open local firmware file {}: {}",
|
||||
local_firmware_path.display(),
|
||||
error
|
||||
)
|
||||
})?;
|
||||
|
||||
let firmware_size = local_file
|
||||
.metadata()
|
||||
.map_err(|error| { format!("failed to read firmware metadata: {}", error) })?
|
||||
.map_err(|error| format!("failed to read firmware metadata: {}", error))?
|
||||
.len();
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
|
||||
local_file
|
||||
.read_to_end(&mut buffer)
|
||||
.map_err(|error| { format!("failed to read firmware file: {}", error) })?;
|
||||
.map_err(|error| format!("failed to read firmware file: {}", error))?;
|
||||
|
||||
let mut remote_file = session
|
||||
.scp_send(Path::new(remote_firmware_path), 0o644, firmware_size, None)
|
||||
.map_err(|error| {
|
||||
format!("failed to start SCP upload to {}: {}", remote_firmware_path, error)
|
||||
format!(
|
||||
"failed to start SCP upload to {}: {}",
|
||||
remote_firmware_path, error
|
||||
)
|
||||
})?;
|
||||
|
||||
remote_file
|
||||
.write_all(&buffer)
|
||||
.map_err(|error| { format!("failed to upload firmware via SCP: {}", error) })?;
|
||||
.map_err(|error| format!("failed to upload firmware via SCP: {}", error))?;
|
||||
|
||||
remote_file.send_eof().map_err(|error| { format!("failed to send SCP EOF: {}", error) })?;
|
||||
remote_file
|
||||
.send_eof()
|
||||
.map_err(|error| format!("failed to send SCP EOF: {}", error))?;
|
||||
|
||||
remote_file.wait_eof().map_err(|error| { format!("failed waiting for SCP EOF: {}", error) })?;
|
||||
remote_file
|
||||
.wait_eof()
|
||||
.map_err(|error| format!("failed waiting for SCP EOF: {}", error))?;
|
||||
|
||||
remote_file.close().map_err(|error| { format!("failed closing SCP channel: {}", error) })?;
|
||||
remote_file
|
||||
.close()
|
||||
.map_err(|error| format!("failed closing SCP channel: {}", error))?;
|
||||
|
||||
remote_file
|
||||
.wait_close()
|
||||
.map_err(|error| { format!("failed waiting for SCP close: {}", error) })?;
|
||||
.map_err(|error| format!("failed waiting for SCP close: {}", error))?;
|
||||
|
||||
Ok(format!("Firmware uploaded to {}:{} ({} bytes)", ip, remote_firmware_path, firmware_size))
|
||||
Ok(format!(
|
||||
"Firmware uploaded to {}:{} ({} bytes)",
|
||||
ip, remote_firmware_path, firmware_size
|
||||
))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -399,22 +382,26 @@ pub async fn flash_router_sysupgrade(ip: String, password: String) -> Result<Str
|
||||
let output = run_system_ssh(&ip, flash_command);
|
||||
|
||||
return match output {
|
||||
Ok(output) =>
|
||||
Ok(format!("sysupgrade command submitted on {} using system ssh. {}", ip, output)),
|
||||
Ok(output) => Ok(format!(
|
||||
"sysupgrade command submitted on {} using system ssh. {}",
|
||||
ip, output
|
||||
)),
|
||||
Err(error) => {
|
||||
let lowered = error.to_lowercase();
|
||||
|
||||
if
|
||||
lowered.contains("commencing upgrade") ||
|
||||
lowered.contains("closing all shell sessions") ||
|
||||
lowered.contains("connection failed") ||
|
||||
lowered.contains("connection reset") ||
|
||||
lowered.contains("broken pipe") ||
|
||||
lowered.contains("closed") ||
|
||||
lowered.contains("disconnect") ||
|
||||
lowered.contains("sysupgrade")
|
||||
if lowered.contains("commencing upgrade")
|
||||
|| lowered.contains("closing all shell sessions")
|
||||
|| lowered.contains("connection failed")
|
||||
|| lowered.contains("connection reset")
|
||||
|| lowered.contains("broken pipe")
|
||||
|| lowered.contains("closed")
|
||||
|| lowered.contains("disconnect")
|
||||
|| lowered.contains("sysupgrade")
|
||||
{
|
||||
Ok(format!("sysupgrade started on {}; SSH disconnected as expected.", ip))
|
||||
Ok(format!(
|
||||
"sysupgrade started on {}; SSH disconnected as expected.",
|
||||
ip
|
||||
))
|
||||
} else {
|
||||
Err(error)
|
||||
}
|
||||
@@ -422,40 +409,15 @@ pub async fn flash_router_sysupgrade(ip: String, password: String) -> Result<Str
|
||||
};
|
||||
}
|
||||
|
||||
let address = format!("{}:22", ip);
|
||||
|
||||
let socket_address = address
|
||||
.to_socket_addrs()
|
||||
.map_err(|error| { format!("failed to resolve router address: {}", error) })?
|
||||
.next()
|
||||
.ok_or_else(|| { format!("failed to resolve router address {}", address) })?;
|
||||
|
||||
let tcp = TcpStream::connect_timeout(&socket_address, Duration::from_secs(8)).map_err(|error| {
|
||||
format!("failed to connect to SSH on {}: {}", ip, error)
|
||||
})?;
|
||||
|
||||
tcp.set_read_timeout(Some(Duration::from_secs(8))).ok();
|
||||
tcp.set_write_timeout(Some(Duration::from_secs(8))).ok();
|
||||
|
||||
let mut session = Session::new().map_err(|error| {
|
||||
format!("failed to create SSH session: {}", error)
|
||||
})?;
|
||||
|
||||
session.set_tcp_stream(tcp);
|
||||
|
||||
session.handshake().map_err(|error| { format!("SSH handshake failed for {}: {}", ip, error) })?;
|
||||
|
||||
authenticate_router(&session, &password).map_err(|error| {
|
||||
format!("root@{}: {}", ip, error)
|
||||
})?;
|
||||
let session = open_router_session(&ip, &password)?;
|
||||
|
||||
let mut channel = session
|
||||
.channel_session()
|
||||
.map_err(|error| { format!("failed to open SSH channel: {}", error) })?;
|
||||
.map_err(|error| format!("failed to open SSH channel: {}", error))?;
|
||||
|
||||
channel
|
||||
.exec(flash_command)
|
||||
.map_err(|error| { format!("failed to start sysupgrade: {}", error) })?;
|
||||
.map_err(|error| format!("failed to start sysupgrade: {}", error))?;
|
||||
|
||||
let mut output = String::new();
|
||||
let _ = channel.read_to_string(&mut output);
|
||||
@@ -463,17 +425,20 @@ pub async fn flash_router_sysupgrade(ip: String, password: String) -> Result<Str
|
||||
|
||||
let lowered = output.to_lowercase();
|
||||
|
||||
if
|
||||
lowered.contains("commencing upgrade") ||
|
||||
lowered.contains("closing all shell sessions") ||
|
||||
lowered.contains("sysupgrade")
|
||||
if lowered.contains("commencing upgrade")
|
||||
|| lowered.contains("closing all shell sessions")
|
||||
|| lowered.contains("sysupgrade")
|
||||
{
|
||||
return Ok(
|
||||
format!("sysupgrade started on {}; router should reboot shortly. {}", ip, output)
|
||||
);
|
||||
return Ok(format!(
|
||||
"sysupgrade started on {}; router should reboot shortly. {}",
|
||||
ip, output
|
||||
));
|
||||
}
|
||||
|
||||
Ok(format!("sysupgrade command submitted on {}. Router should reboot shortly. {}", ip, output))
|
||||
Ok(format!(
|
||||
"sysupgrade command submitted on {}. Router should reboot shortly. {}",
|
||||
ip, output
|
||||
))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -489,14 +454,10 @@ pub async fn reconnect_router_after_flash(ip: String, password: String) -> Resul
|
||||
if password.trim().is_empty() {
|
||||
match run_system_ssh(&ip, "ubus call system board") {
|
||||
Ok(output) => {
|
||||
return Ok(
|
||||
format!(
|
||||
"Router reconnected after flash on attempt {}/{} using system ssh.\n{}",
|
||||
attempt,
|
||||
max_attempts,
|
||||
output
|
||||
)
|
||||
);
|
||||
return Ok(format!(
|
||||
"Router reconnected after flash on attempt {}/{} using system ssh.\n{}",
|
||||
attempt, max_attempts, output
|
||||
));
|
||||
}
|
||||
Err(_) => {
|
||||
thread::sleep(wait_between_attempts);
|
||||
@@ -505,93 +466,24 @@ pub async fn reconnect_router_after_flash(ip: String, password: String) -> Resul
|
||||
}
|
||||
}
|
||||
|
||||
let address = format!("{}:22", ip);
|
||||
|
||||
let socket_address = match address.to_socket_addrs() {
|
||||
Ok(mut addresses) =>
|
||||
match addresses.next() {
|
||||
Some(socket_address) => socket_address,
|
||||
None => {
|
||||
thread::sleep(wait_between_attempts);
|
||||
continue;
|
||||
}
|
||||
match open_router_session(&ip, &password) {
|
||||
Ok(session) => match run_ssh_command(&session, "ubus call system board") {
|
||||
Ok(output) => {
|
||||
return Ok(format!(
|
||||
"Router reconnected after flash on attempt {}/{}.\n{}",
|
||||
attempt, max_attempts, output
|
||||
));
|
||||
}
|
||||
Err(_) => {
|
||||
thread::sleep(wait_between_attempts);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let tcp = match TcpStream::connect_timeout(&socket_address, Duration::from_secs(4)) {
|
||||
Ok(tcp) => tcp,
|
||||
Err(_) => {
|
||||
thread::sleep(wait_between_attempts);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let _ = tcp.set_read_timeout(Some(Duration::from_secs(8)));
|
||||
let _ = tcp.set_write_timeout(Some(Duration::from_secs(8)));
|
||||
|
||||
let mut session = match Session::new() {
|
||||
Ok(session) => session,
|
||||
Err(_) => {
|
||||
thread::sleep(wait_between_attempts);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
session.set_tcp_stream(tcp);
|
||||
|
||||
if session.handshake().is_err() {
|
||||
thread::sleep(wait_between_attempts);
|
||||
continue;
|
||||
Err(_) => thread::sleep(wait_between_attempts),
|
||||
},
|
||||
Err(_) => thread::sleep(wait_between_attempts),
|
||||
}
|
||||
|
||||
if authenticate_router(&session, &password).is_err() {
|
||||
thread::sleep(wait_between_attempts);
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut channel = match session.channel_session() {
|
||||
Ok(channel) => channel,
|
||||
Err(_) => {
|
||||
thread::sleep(wait_between_attempts);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if channel.exec("ubus call system board").is_err() {
|
||||
thread::sleep(wait_between_attempts);
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut output = String::new();
|
||||
|
||||
if channel.read_to_string(&mut output).is_err() {
|
||||
thread::sleep(wait_between_attempts);
|
||||
continue;
|
||||
}
|
||||
|
||||
let _ = channel.wait_close();
|
||||
|
||||
return Ok(
|
||||
format!(
|
||||
"Router reconnected after flash on attempt {}/{}.\n{}",
|
||||
attempt,
|
||||
max_attempts,
|
||||
output
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Err(
|
||||
format!(
|
||||
"Router did not become reachable over SSH at {} after {} attempts. Replug Ethernet and retry.",
|
||||
ip,
|
||||
max_attempts
|
||||
)
|
||||
)
|
||||
Err(format!(
|
||||
"Router did not become reachable over SSH at {} after {} attempts. Replug Ethernet and retry.",
|
||||
ip, max_attempts
|
||||
))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -604,66 +496,28 @@ pub async fn check_router_after_flash(ip: String, password: String) -> Result<St
|
||||
return run_system_ssh(&ip, "ubus call system board");
|
||||
}
|
||||
|
||||
let address = format!("{}:22", ip);
|
||||
|
||||
let socket_address = address
|
||||
.to_socket_addrs()
|
||||
.map_err(|error| format!("failed to resolve router address: {}", error))?
|
||||
.next()
|
||||
.ok_or_else(|| format!("failed to resolve router address {}", address))?;
|
||||
|
||||
let tcp = TcpStream::connect_timeout(&socket_address, Duration::from_secs(4)).map_err(|error|
|
||||
format!("SSH not ready at {}: {}", ip, error)
|
||||
)?;
|
||||
|
||||
let _ = tcp.set_read_timeout(Some(Duration::from_secs(8)));
|
||||
let _ = tcp.set_write_timeout(Some(Duration::from_secs(8)));
|
||||
|
||||
let mut session = Session::new().map_err(|error|
|
||||
format!("failed to create SSH session: {}", error)
|
||||
)?;
|
||||
|
||||
session.set_tcp_stream(tcp);
|
||||
|
||||
session.handshake().map_err(|error| format!("SSH handshake failed: {}", error))?;
|
||||
|
||||
authenticate_router(&session, &password).map_err(|error| format!("root@{}: {}", ip, error))?;
|
||||
|
||||
let mut channel = session
|
||||
.channel_session()
|
||||
.map_err(|error| format!("failed to open SSH channel: {}", error))?;
|
||||
|
||||
channel
|
||||
.exec("ubus call system board")
|
||||
.map_err(|error| format!("failed to run ubus: {}", error))?;
|
||||
|
||||
let mut output = String::new();
|
||||
|
||||
channel
|
||||
.read_to_string(&mut output)
|
||||
.map_err(|error| format!("failed to read router info: {}", error))?;
|
||||
|
||||
let _ = channel.wait_close();
|
||||
|
||||
Ok(output)
|
||||
let session = open_router_session(&ip, &password)?;
|
||||
run_ssh_command(&session, "ubus call system board")
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn upload_provisioning_bundle(
|
||||
app: AppHandle,
|
||||
ip: String,
|
||||
password: String,
|
||||
env_content: String
|
||||
env_content: String,
|
||||
) -> Result<String, String> {
|
||||
if ip.trim().is_empty() {
|
||||
return Err("router IP is required".into());
|
||||
}
|
||||
|
||||
let local_script_path = "resources/provisioning/provision.sh";
|
||||
let local_script_path = resource_path(&app, "provisioning/provision.sh")?;
|
||||
let remote_script_path = "/tmp/provision.sh";
|
||||
let remote_env_path = "/tmp/router.env";
|
||||
|
||||
if password.trim().is_empty() {
|
||||
let script_target = format!("root@{}:{}", ip, remote_script_path);
|
||||
let local_script_path_string = local_script_path.to_string_lossy().to_string();
|
||||
|
||||
let script_upload = Command::new("scp")
|
||||
.args([
|
||||
@@ -676,37 +530,41 @@ pub async fn upload_provisioning_bundle(
|
||||
"StrictHostKeyChecking=no",
|
||||
"-o",
|
||||
"UserKnownHostsFile=NUL",
|
||||
local_script_path,
|
||||
&local_script_path_string,
|
||||
&script_target,
|
||||
])
|
||||
.output()
|
||||
.map_err(|error| format!("failed to run scp for provision.sh: {}", error))?;
|
||||
|
||||
if !script_upload.status.success() {
|
||||
return Err(
|
||||
format!(
|
||||
"failed to upload provision.sh:\n{}\n{}",
|
||||
String::from_utf8_lossy(&script_upload.stderr),
|
||||
String::from_utf8_lossy(&script_upload.stdout)
|
||||
)
|
||||
);
|
||||
return Err(format!(
|
||||
"failed to upload provision.sh:\n{}\n{}",
|
||||
String::from_utf8_lossy(&script_upload.stderr),
|
||||
String::from_utf8_lossy(&script_upload.stdout)
|
||||
));
|
||||
}
|
||||
|
||||
let env_command = format!(
|
||||
"cat > {} <<'EOF'\n{}\nEOF\nchmod +x {}",
|
||||
remote_env_path,
|
||||
env_content,
|
||||
remote_script_path
|
||||
remote_env_path, env_content, remote_script_path
|
||||
);
|
||||
|
||||
run_system_ssh(&ip, &env_command)?;
|
||||
|
||||
return Ok(format!("uploaded provision.sh and router.env to {} using system ssh/scp", ip));
|
||||
return Ok(format!(
|
||||
"uploaded provision.sh and router.env to {} using system ssh/scp",
|
||||
ip
|
||||
));
|
||||
}
|
||||
|
||||
let session = open_router_session(&ip, &password)?;
|
||||
|
||||
scp_file_from_disk(&session, local_script_path, remote_script_path, 0o755)?;
|
||||
scp_file_from_disk(
|
||||
&session,
|
||||
local_script_path.to_string_lossy().as_ref(),
|
||||
remote_script_path,
|
||||
0o755,
|
||||
)?;
|
||||
|
||||
scp_string(&session, &env_content, remote_env_path, 0o600)?;
|
||||
|
||||
@@ -716,16 +574,21 @@ pub async fn upload_provisioning_bundle(
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn upload_udp2raw_setup_script(ip: String, password: String) -> Result<String, String> {
|
||||
pub async fn upload_udp2raw_setup_script(
|
||||
app: AppHandle,
|
||||
ip: String,
|
||||
password: String,
|
||||
) -> Result<String, String> {
|
||||
if ip.trim().is_empty() {
|
||||
return Err("router IP is required".into());
|
||||
}
|
||||
|
||||
let local_script_path = "resources/udp2raw/setup_udp2raw.sh";
|
||||
let local_script_path = resource_path(&app, "udp2raw/setup_udp2raw.sh")?;
|
||||
let remote_script_path = "/tmp/setup_udp2raw.sh";
|
||||
|
||||
if password.trim().is_empty() {
|
||||
let target = format!("root@{}:{}", ip, remote_script_path);
|
||||
let local_script_path_string = local_script_path.to_string_lossy().to_string();
|
||||
|
||||
let output = Command::new("scp")
|
||||
.args([
|
||||
@@ -738,20 +601,18 @@ pub async fn upload_udp2raw_setup_script(ip: String, password: String) -> Result
|
||||
"StrictHostKeyChecking=no",
|
||||
"-o",
|
||||
"UserKnownHostsFile=NUL",
|
||||
local_script_path,
|
||||
&local_script_path_string,
|
||||
&target,
|
||||
])
|
||||
.output()
|
||||
.map_err(|error| format!("failed to run scp for setup_udp2raw.sh: {}", error))?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(
|
||||
format!(
|
||||
"failed to upload setup_udp2raw.sh:\n{}\n{}",
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
)
|
||||
);
|
||||
return Err(format!(
|
||||
"failed to upload setup_udp2raw.sh:\n{}\n{}",
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
));
|
||||
}
|
||||
|
||||
run_system_ssh(&ip, "chmod +x /tmp/setup_udp2raw.sh")?;
|
||||
@@ -761,7 +622,12 @@ pub async fn upload_udp2raw_setup_script(ip: String, password: String) -> Result
|
||||
|
||||
let session = open_router_session(&ip, &password)?;
|
||||
|
||||
scp_file_from_disk(&session, local_script_path, remote_script_path, 0o755)?;
|
||||
scp_file_from_disk(
|
||||
&session,
|
||||
local_script_path.to_string_lossy().as_ref(),
|
||||
remote_script_path,
|
||||
0o755,
|
||||
)?;
|
||||
|
||||
run_ssh_command(&session, "chmod +x /tmp/setup_udp2raw.sh")?;
|
||||
|
||||
@@ -791,8 +657,7 @@ pub async fn check_udp2raw_router_status(ip: String, password: String) -> Result
|
||||
return Err("router IP is required".into());
|
||||
}
|
||||
|
||||
let command =
|
||||
r#"
|
||||
let command = r#"
|
||||
echo "== udp2raw binary =="
|
||||
if command -v udp2raw >/dev/null 2>&1; then
|
||||
command -v udp2raw
|
||||
@@ -899,12 +764,7 @@ pub async fn test_udp2raw_tunnel(ip: String, password: String) -> Result<String,
|
||||
return run_ssh_command(&session, command);
|
||||
}
|
||||
Err(error) => {
|
||||
last_error = format!(
|
||||
"SSH attempt {}/5 failed: {}",
|
||||
attempt,
|
||||
error
|
||||
);
|
||||
|
||||
last_error = format!("SSH attempt {}/5 failed: {}", attempt, error);
|
||||
thread::sleep(Duration::from_secs(2));
|
||||
}
|
||||
}
|
||||
@@ -914,16 +774,21 @@ pub async fn test_udp2raw_tunnel(ip: String, password: String) -> Result<String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn upload_udp2raw_binary(ip: String, password: String) -> Result<String, String> {
|
||||
pub async fn upload_udp2raw_binary(
|
||||
app: AppHandle,
|
||||
ip: String,
|
||||
password: String,
|
||||
) -> Result<String, String> {
|
||||
if ip.trim().is_empty() {
|
||||
return Err("router IP is required".into());
|
||||
}
|
||||
|
||||
let local_binary_path = "resources/udp2raw/udp2raw";
|
||||
let local_binary_path = resource_path(&app, "udp2raw/udp2raw")?;
|
||||
let remote_binary_path = "/usr/bin/udp2raw";
|
||||
|
||||
if password.trim().is_empty() {
|
||||
let target = format!("root@{}:{}", ip, remote_binary_path);
|
||||
let local_binary_path_string = local_binary_path.to_string_lossy().to_string();
|
||||
|
||||
let output = Command::new("scp")
|
||||
.args([
|
||||
@@ -936,25 +801,23 @@ pub async fn upload_udp2raw_binary(ip: String, password: String) -> Result<Strin
|
||||
"StrictHostKeyChecking=no",
|
||||
"-o",
|
||||
"UserKnownHostsFile=NUL",
|
||||
local_binary_path,
|
||||
&local_binary_path_string,
|
||||
&target,
|
||||
])
|
||||
.output()
|
||||
.map_err(|error| { format!("failed to run scp for udp2raw binary: {}", error) })?;
|
||||
.map_err(|error| format!("failed to run scp for udp2raw binary: {}", error))?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(
|
||||
format!(
|
||||
"failed to upload udp2raw binary:\n{}\n{}",
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
)
|
||||
);
|
||||
return Err(format!(
|
||||
"failed to upload udp2raw binary:\n{}\n{}",
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
));
|
||||
}
|
||||
|
||||
run_system_ssh(
|
||||
&ip,
|
||||
"chmod +x /usr/bin/udp2raw && /usr/bin/udp2raw --help >/dev/null 2>&1 || true"
|
||||
"chmod +x /usr/bin/udp2raw && /usr/bin/udp2raw --help >/dev/null 2>&1 || true",
|
||||
)?;
|
||||
|
||||
return Ok("uploaded udp2raw binary to /usr/bin/udp2raw".into());
|
||||
@@ -962,9 +825,17 @@ pub async fn upload_udp2raw_binary(ip: String, password: String) -> Result<Strin
|
||||
|
||||
let session = open_router_session(&ip, &password)?;
|
||||
|
||||
scp_file_from_disk(&session, local_binary_path, remote_binary_path, 0o755)?;
|
||||
scp_file_from_disk(
|
||||
&session,
|
||||
local_binary_path.to_string_lossy().as_ref(),
|
||||
remote_binary_path,
|
||||
0o755,
|
||||
)?;
|
||||
|
||||
run_ssh_command(&session, "chmod +x /usr/bin/udp2raw && ls -l /usr/bin/udp2raw")?;
|
||||
run_ssh_command(
|
||||
&session,
|
||||
"chmod +x /usr/bin/udp2raw && ls -l /usr/bin/udp2raw",
|
||||
)?;
|
||||
|
||||
Ok("uploaded udp2raw binary to /usr/bin/udp2raw".into())
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ use commands::{
|
||||
upload_udp2raw_binary
|
||||
},
|
||||
ssh::{ inspect_router_with_password, probe_router_ssh, remove_known_host },
|
||||
controllers::{ list_controller_clients}
|
||||
};
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
@@ -54,7 +55,8 @@ pub fn run() {
|
||||
run_udp2raw_setup,
|
||||
test_udp2raw_tunnel,
|
||||
check_udp2raw_router_status,
|
||||
upload_udp2raw_binary
|
||||
upload_udp2raw_binary,
|
||||
list_controller_clients
|
||||
]
|
||||
)
|
||||
.run(tauri::generate_context!())
|
||||
|
||||
@@ -30,6 +30,9 @@
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"resources": [
|
||||
"resources/**/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import { BackendSettings } from '@/components/settings/BackendSettings';
|
||||
import { ActivityLogs } from '@/components/activity/ActivityLogs';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Udp2rawConfig } from '@/components/udp2raw/Udp2rawConfig';
|
||||
import { ControllersRoute } from '@/components/controllers/ControllersRoute';
|
||||
|
||||
import { vpnApi } from '@/services/vpnApi';
|
||||
import { vpsApi } from '@/services/vpsApi';
|
||||
@@ -281,6 +282,10 @@ export function RouteView({
|
||||
return <DashboardRoute />;
|
||||
}
|
||||
|
||||
if (active === 'Controladores') {
|
||||
return <ControllersRoute />;
|
||||
}
|
||||
|
||||
if (active === 'Provisionamento') {
|
||||
return <ProvisioningWizard />;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,12 +5,14 @@ import {
|
||||
RadioTower,
|
||||
Settings,
|
||||
Wrench,
|
||||
Router
|
||||
} from 'lucide-react';
|
||||
|
||||
import logoIcon from '@/assets/logo-icon.png';
|
||||
|
||||
const items = [
|
||||
['Painel', Gauge],
|
||||
['Controladores', Router],
|
||||
['Configuração UDP2RAW', RadioTower],
|
||||
['Provisionamento', Wrench],
|
||||
['Registos de Atividade', FileClock],
|
||||
@@ -57,18 +59,16 @@ export function Sidebar({
|
||||
key={label}
|
||||
type="button"
|
||||
onClick={() => onSelect(label)}
|
||||
className={`group flex w-full items-center gap-3 rounded-2xl px-4 py-3 text-left text-sm font-semibold transition-all duration-200 ${
|
||||
isActive
|
||||
className={`group flex w-full items-center gap-3 rounded-2xl px-4 py-3 text-left text-sm font-semibold transition-all duration-200 ${isActive
|
||||
? 'border border-blue-500/20 bg-blue-500/15 text-white shadow-[0_0_25px_rgba(59,130,246,0.12)]'
|
||||
: 'border border-transparent text-slate-300 hover:border-white/5 hover:bg-white/[0.035] hover:text-white'
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`rounded-xl p-2 transition ${
|
||||
isActive
|
||||
className={`rounded-xl p-2 transition ${isActive
|
||||
? 'bg-blue-500/10 text-blue-300'
|
||||
: 'bg-white/[0.03] text-slate-400 group-hover:text-slate-200'
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
<Icon size={17} />
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { AppSettings } from '@/types/api';
|
||||
|
||||
const defaults: AppSettings = {
|
||||
backendUrl: 'http://146.59.230.190:8080',
|
||||
apiKey:
|
||||
'b8184608fcab419da2ce9a220ee6017daaff637b627e43ebac31b6b7c3344349',
|
||||
const defaults: AppSettings = { // FOR DEBUG AND DEV PURPOSES ONLY ==========================>
|
||||
backendUrl: 'http://localhost:8080',// FOR DEBUG AND DEV PURPOSES ONLY ==========================>
|
||||
apiKey:// FOR DEBUG AND DEV PURPOSES ONLY ==========================>
|
||||
'dev-api-key',// FOR DEBUG AND DEV PURPOSES ONLY ==========================>
|
||||
};
|
||||
|
||||
const SETTINGS_KEY =
|
||||
|
||||
+32
-9
@@ -5,21 +5,32 @@ import type {
|
||||
VpsHealth,
|
||||
} from '@/types/api';
|
||||
|
||||
export type ControllerClient = {
|
||||
id: string;
|
||||
name: string;
|
||||
file: string;
|
||||
path: string;
|
||||
controller_count: number;
|
||||
modified_at: string;
|
||||
};
|
||||
|
||||
export type ControllerClientsResponse = {
|
||||
clients: ControllerClient[];
|
||||
};
|
||||
|
||||
export type ControllerClientFileResponse = {
|
||||
content: string;
|
||||
};
|
||||
|
||||
export const vpsApi = {
|
||||
health: () =>
|
||||
apiRequest<VpsHealth>(
|
||||
'/api/vps/health',
|
||||
),
|
||||
apiRequest<VpsHealth>('/api/vps/health'),
|
||||
|
||||
networkTraffic: () =>
|
||||
apiRequest<NetworkTrafficResponse>(
|
||||
'/api/vps/network-traffic',
|
||||
),
|
||||
apiRequest<NetworkTrafficResponse>('/api/vps/network-traffic'),
|
||||
|
||||
udp2rawTraffic: () =>
|
||||
apiRequest<NetworkTrafficResponse>(
|
||||
'/api/vps/udp2raw-traffic',
|
||||
),
|
||||
apiRequest<NetworkTrafficResponse>('/api/vps/udp2raw-traffic'),
|
||||
|
||||
rollbackLastBackup: () =>
|
||||
apiRequest<{ restored: boolean }>(
|
||||
@@ -28,4 +39,16 @@ export const vpsApi = {
|
||||
method: 'POST',
|
||||
},
|
||||
),
|
||||
|
||||
// ALL clients
|
||||
listControllerClients: () =>
|
||||
apiRequest<ControllerClientsResponse>(
|
||||
'/api/vps/controllers/clients',
|
||||
),
|
||||
|
||||
// ONE client's .txt content
|
||||
readControllerClient: (clientId: string) =>
|
||||
apiRequest<ControllerClientFileResponse>(
|
||||
`/api/vps/controllers/clients/${encodeURIComponent(clientId)}`,
|
||||
),
|
||||
};
|
||||
Reference in New Issue
Block a user