Finished controllers UI

This commit is contained in:
litoral05
2026-05-15 08:54:51 +01:00
parent 7e1eeba970
commit d9b4c081ce
10 changed files with 1608 additions and 345 deletions
+147
View File
@@ -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",
)
}
+1
View File
@@ -2,3 +2,4 @@ pub mod files;
pub mod network; pub mod network;
pub mod router; pub mod router;
pub mod ssh; pub mod ssh;
pub mod controllers;
+184 -313
View File
@@ -2,17 +2,25 @@ use ssh2::Session;
use std::process::Command; use std::process::Command;
use std::{ use std::{
fs::File, fs::File,
io::{ Read, Write }, io::{Read, Write},
net::{ TcpStream, ToSocketAddrs }, net::{TcpStream, ToSocketAddrs},
path::Path, path::{Path, PathBuf},
thread, thread,
time::Duration, time::Duration,
}; };
use tauri::{AppHandle, Manager};
fn delay() { fn delay() {
thread::sleep(Duration::from_millis(350)); 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] #[tauri::command]
pub async fn detect_router(ip: String) -> Result<bool, String> { pub async fn detect_router(ip: String) -> Result<bool, String> {
delay(); delay();
@@ -64,9 +72,12 @@ fn run_system_ssh(ip: &str, command: &str) -> Result<String, String> {
return Ok(format!("{}{}", stdout, stderr)); return Ok(format!("{}{}", stdout, stderr));
} }
Err( Err(format!(
format!("system ssh failed with status {:?}:\n{}\n{}", output.status.code(), stderr, stdout) "system ssh failed with status {:?}:\n{}\n{}",
) output.status.code(),
stderr,
stdout
))
} }
fn open_router_session(ip: &str, password: &str) -> Result<Session, String> { 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() .next()
.ok_or_else(|| format!("failed to resolve router address {}", address))?; .ok_or_else(|| format!("failed to resolve router address {}", address))?;
let tcp = TcpStream::connect_timeout(&socket_address, Duration::from_secs(8)).map_err(|error| let tcp = TcpStream::connect_timeout(&socket_address, Duration::from_secs(8))
format!("failed to connect to SSH on {}: {}", ip, error) .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_read_timeout(Some(Duration::from_secs(30)));
let _ = tcp.set_write_timeout(Some(Duration::from_secs(30))); let _ = tcp.set_write_timeout(Some(Duration::from_secs(30)));
let mut session = Session::new().map_err(|error| let mut session =
format!("failed to create SSH session: {}", error) Session::new().map_err(|error| format!("failed to create SSH session: {}", error))?;
)?;
session.set_tcp_stream(tcp); 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))?; authenticate_router(&session, password).map_err(|error| format!("root@{}: {}", ip, error))?;
@@ -102,11 +113,10 @@ fn scp_file_from_disk(
session: &Session, session: &Session,
local_path: &str, local_path: &str,
remote_path: &str, remote_path: &str,
mode: i32 mode: i32,
) -> Result<(), String> { ) -> Result<(), String> {
let mut local_file = File::open(local_path).map_err(|error| let mut local_file =
format!("failed to open {}: {}", local_path, error) File::open(local_path).map_err(|error| format!("failed to open {}: {}", local_path, error))?;
)?;
let file_size = local_file let file_size = local_file
.metadata() .metadata()
@@ -135,12 +145,7 @@ fn scp_file_from_disk(
Ok(()) Ok(())
} }
fn scp_string( fn scp_string(session: &Session, content: &str, remote_path: &str, mode: i32) -> Result<(), String> {
session: &Session,
content: &str,
remote_path: &str,
mode: i32
) -> Result<(), String> {
let bytes = content.as_bytes(); let bytes = content.as_bytes();
let mut remote_file = session let mut remote_file = session
@@ -260,9 +265,15 @@ pub async fn verify_router(ip: String) -> Result<bool, String> {
} }
#[tauri::command] #[tauri::command]
pub async fn upload_firmware_to_router(ip: String, password: String) -> Result<String, String> { pub async fn upload_firmware_to_router(
let local_firmware_path = app: AppHandle,
"resources/firmware/openwrt-23.05.5-zbt-we826-16m-litoral-golden-sysupgrade.bin"; 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"; 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()); return Err("router IP is required".into());
} }
/*
* FACTORY / FRESH OPENWRT PATH
* Uses system scp when no password is set.
*/
if password.trim().is_empty() { if password.trim().is_empty() {
let target = format!("root@{}:{}", ip, remote_firmware_path); 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 let output = Command::new("scp")
::new("scp")
.args([ .args([
"-O", "-O",
"-o", "-o",
@@ -288,103 +294,80 @@ pub async fn upload_firmware_to_router(ip: String, password: String) -> Result<S
"ConnectTimeout=10", "ConnectTimeout=10",
"-o", "-o",
"StrictHostKeyChecking=accept-new", "StrictHostKeyChecking=accept-new",
local_firmware_path, &local_firmware_path_string,
&target, &target,
]) ])
.output() .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() { if output.status.success() {
return Ok( return Ok(format!(
format!("Firmware uploaded to {}:{} using system scp", ip, remote_firmware_path) "Firmware uploaded to {}:{} using system scp",
); ip, remote_firmware_path
));
} }
return Err( return Err(format!(
format!(
"system scp failed:\n{}\n{}", "system scp failed:\n{}\n{}",
String::from_utf8_lossy(&output.stderr), String::from_utf8_lossy(&output.stderr),
String::from_utf8_lossy(&output.stdout) String::from_utf8_lossy(&output.stdout)
));
}
let session = open_router_session(&ip, &password)?;
let mut local_file = File::open(&local_firmware_path).map_err(|error| {
format!(
"failed to open local firmware file {}: {}",
local_firmware_path.display(),
error
) )
);
}
/*
* PASSWORD SSH PATH
*/
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 firmware_size = local_file let firmware_size = local_file
.metadata() .metadata()
.map_err(|error| { format!("failed to read firmware metadata: {}", error) })? .map_err(|error| format!("failed to read firmware metadata: {}", error))?
.len(); .len();
let mut buffer = Vec::new(); let mut buffer = Vec::new();
local_file local_file
.read_to_end(&mut buffer) .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 let mut remote_file = session
.scp_send(Path::new(remote_firmware_path), 0o644, firmware_size, None) .scp_send(Path::new(remote_firmware_path), 0o644, firmware_size, None)
.map_err(|error| { .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 remote_file
.write_all(&buffer) .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 remote_file
.wait_close() .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] #[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); let output = run_system_ssh(&ip, flash_command);
return match output { return match output {
Ok(output) => Ok(output) => Ok(format!(
Ok(format!("sysupgrade command submitted on {} using system ssh. {}", ip, output)), "sysupgrade command submitted on {} using system ssh. {}",
ip, output
)),
Err(error) => { Err(error) => {
let lowered = error.to_lowercase(); let lowered = error.to_lowercase();
if if lowered.contains("commencing upgrade")
lowered.contains("commencing upgrade") || || lowered.contains("closing all shell sessions")
lowered.contains("closing all shell sessions") || || lowered.contains("connection failed")
lowered.contains("connection failed") || || lowered.contains("connection reset")
lowered.contains("connection reset") || || lowered.contains("broken pipe")
lowered.contains("broken pipe") || || lowered.contains("closed")
lowered.contains("closed") || || lowered.contains("disconnect")
lowered.contains("disconnect") || || lowered.contains("sysupgrade")
lowered.contains("sysupgrade")
{ {
Ok(format!("sysupgrade started on {}; SSH disconnected as expected.", ip)) Ok(format!(
"sysupgrade started on {}; SSH disconnected as expected.",
ip
))
} else { } else {
Err(error) Err(error)
} }
@@ -422,40 +409,15 @@ pub async fn flash_router_sysupgrade(ip: String, password: String) -> Result<Str
}; };
} }
let address = format!("{}:22", ip); let session = open_router_session(&ip, &password)?;
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 mut channel = session let mut channel = session
.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 channel
.exec(flash_command) .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 mut output = String::new();
let _ = channel.read_to_string(&mut output); 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(); let lowered = output.to_lowercase();
if if lowered.contains("commencing upgrade")
lowered.contains("commencing upgrade") || || lowered.contains("closing all shell sessions")
lowered.contains("closing all shell sessions") || || lowered.contains("sysupgrade")
lowered.contains("sysupgrade")
{ {
return Ok( return Ok(format!(
format!("sysupgrade started on {}; router should reboot shortly. {}", ip, output) "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] #[tauri::command]
@@ -489,14 +454,10 @@ pub async fn reconnect_router_after_flash(ip: String, password: String) -> Resul
if password.trim().is_empty() { if password.trim().is_empty() {
match run_system_ssh(&ip, "ubus call system board") { match run_system_ssh(&ip, "ubus call system board") {
Ok(output) => { Ok(output) => {
return Ok( return Ok(format!(
format!(
"Router reconnected after flash on attempt {}/{} using system ssh.\n{}", "Router reconnected after flash on attempt {}/{} using system ssh.\n{}",
attempt, attempt, max_attempts, output
max_attempts, ));
output
)
);
} }
Err(_) => { Err(_) => {
thread::sleep(wait_between_attempts); 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); match open_router_session(&ip, &password) {
Ok(session) => match run_ssh_command(&session, "ubus call system board") {
let socket_address = match address.to_socket_addrs() { Ok(output) => {
Ok(mut addresses) => return Ok(format!(
match addresses.next() {
Some(socket_address) => socket_address,
None => {
thread::sleep(wait_between_attempts);
continue;
}
}
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;
}
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{}", "Router reconnected after flash on attempt {}/{}.\n{}",
attempt, attempt, max_attempts, output
max_attempts, ));
output }
) Err(_) => thread::sleep(wait_between_attempts),
); },
Err(_) => thread::sleep(wait_between_attempts),
}
} }
Err( Err(format!(
format!(
"Router did not become reachable over SSH at {} after {} attempts. Replug Ethernet and retry.", "Router did not become reachable over SSH at {} after {} attempts. Replug Ethernet and retry.",
ip, ip, max_attempts
max_attempts ))
)
)
} }
#[tauri::command] #[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"); return run_system_ssh(&ip, "ubus call system board");
} }
let address = format!("{}:22", ip); let session = open_router_session(&ip, &password)?;
run_ssh_command(&session, "ubus call system board")
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)
} }
#[tauri::command] #[tauri::command]
pub async fn upload_provisioning_bundle( pub async fn upload_provisioning_bundle(
app: AppHandle,
ip: String, ip: String,
password: String, password: String,
env_content: String env_content: String,
) -> Result<String, String> { ) -> Result<String, String> {
if ip.trim().is_empty() { if ip.trim().is_empty() {
return Err("router IP is required".into()); 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_script_path = "/tmp/provision.sh";
let remote_env_path = "/tmp/router.env"; let remote_env_path = "/tmp/router.env";
if password.trim().is_empty() { if password.trim().is_empty() {
let script_target = format!("root@{}:{}", ip, remote_script_path); 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") let script_upload = Command::new("scp")
.args([ .args([
@@ -676,37 +530,41 @@ pub async fn upload_provisioning_bundle(
"StrictHostKeyChecking=no", "StrictHostKeyChecking=no",
"-o", "-o",
"UserKnownHostsFile=NUL", "UserKnownHostsFile=NUL",
local_script_path, &local_script_path_string,
&script_target, &script_target,
]) ])
.output() .output()
.map_err(|error| format!("failed to run scp for provision.sh: {}", error))?; .map_err(|error| format!("failed to run scp for provision.sh: {}", error))?;
if !script_upload.status.success() { if !script_upload.status.success() {
return Err( return Err(format!(
format!(
"failed to upload provision.sh:\n{}\n{}", "failed to upload provision.sh:\n{}\n{}",
String::from_utf8_lossy(&script_upload.stderr), String::from_utf8_lossy(&script_upload.stderr),
String::from_utf8_lossy(&script_upload.stdout) String::from_utf8_lossy(&script_upload.stdout)
) ));
);
} }
let env_command = format!( let env_command = format!(
"cat > {} <<'EOF'\n{}\nEOF\nchmod +x {}", "cat > {} <<'EOF'\n{}\nEOF\nchmod +x {}",
remote_env_path, remote_env_path, env_content, remote_script_path
env_content,
remote_script_path
); );
run_system_ssh(&ip, &env_command)?; 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)?; 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)?; scp_string(&session, &env_content, remote_env_path, 0o600)?;
@@ -716,16 +574,21 @@ pub async fn upload_provisioning_bundle(
} }
#[tauri::command] #[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() { if ip.trim().is_empty() {
return Err("router IP is required".into()); 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"; let remote_script_path = "/tmp/setup_udp2raw.sh";
if password.trim().is_empty() { if password.trim().is_empty() {
let target = format!("root@{}:{}", ip, remote_script_path); 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") let output = Command::new("scp")
.args([ .args([
@@ -738,20 +601,18 @@ pub async fn upload_udp2raw_setup_script(ip: String, password: String) -> Result
"StrictHostKeyChecking=no", "StrictHostKeyChecking=no",
"-o", "-o",
"UserKnownHostsFile=NUL", "UserKnownHostsFile=NUL",
local_script_path, &local_script_path_string,
&target, &target,
]) ])
.output() .output()
.map_err(|error| format!("failed to run scp for setup_udp2raw.sh: {}", error))?; .map_err(|error| format!("failed to run scp for setup_udp2raw.sh: {}", error))?;
if !output.status.success() { if !output.status.success() {
return Err( return Err(format!(
format!(
"failed to upload setup_udp2raw.sh:\n{}\n{}", "failed to upload setup_udp2raw.sh:\n{}\n{}",
String::from_utf8_lossy(&output.stderr), String::from_utf8_lossy(&output.stderr),
String::from_utf8_lossy(&output.stdout) String::from_utf8_lossy(&output.stdout)
) ));
);
} }
run_system_ssh(&ip, "chmod +x /tmp/setup_udp2raw.sh")?; 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)?; 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")?; 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()); return Err("router IP is required".into());
} }
let command = let command = r#"
r#"
echo "== udp2raw binary ==" echo "== udp2raw binary =="
if command -v udp2raw >/dev/null 2>&1; then if command -v udp2raw >/dev/null 2>&1; then
command -v udp2raw 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); return run_ssh_command(&session, command);
} }
Err(error) => { Err(error) => {
last_error = format!( last_error = format!("SSH attempt {}/5 failed: {}", attempt, error);
"SSH attempt {}/5 failed: {}",
attempt,
error
);
thread::sleep(Duration::from_secs(2)); thread::sleep(Duration::from_secs(2));
} }
} }
@@ -914,16 +774,21 @@ pub async fn test_udp2raw_tunnel(ip: String, password: String) -> Result<String,
} }
#[tauri::command] #[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() { if ip.trim().is_empty() {
return Err("router IP is required".into()); 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"; let remote_binary_path = "/usr/bin/udp2raw";
if password.trim().is_empty() { if password.trim().is_empty() {
let target = format!("root@{}:{}", ip, remote_binary_path); 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") let output = Command::new("scp")
.args([ .args([
@@ -936,25 +801,23 @@ pub async fn upload_udp2raw_binary(ip: String, password: String) -> Result<Strin
"StrictHostKeyChecking=no", "StrictHostKeyChecking=no",
"-o", "-o",
"UserKnownHostsFile=NUL", "UserKnownHostsFile=NUL",
local_binary_path, &local_binary_path_string,
&target, &target,
]) ])
.output() .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() { if !output.status.success() {
return Err( return Err(format!(
format!(
"failed to upload udp2raw binary:\n{}\n{}", "failed to upload udp2raw binary:\n{}\n{}",
String::from_utf8_lossy(&output.stderr), String::from_utf8_lossy(&output.stderr),
String::from_utf8_lossy(&output.stdout) String::from_utf8_lossy(&output.stdout)
) ));
);
} }
run_system_ssh( run_system_ssh(
&ip, &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()); 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)?; 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()) Ok("uploaded udp2raw binary to /usr/bin/udp2raw".into())
} }
+3 -1
View File
@@ -23,6 +23,7 @@ use commands::{
upload_udp2raw_binary upload_udp2raw_binary
}, },
ssh::{ inspect_router_with_password, probe_router_ssh, remove_known_host }, ssh::{ inspect_router_with_password, probe_router_ssh, remove_known_host },
controllers::{ list_controller_clients}
}; };
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
@@ -54,7 +55,8 @@ pub fn run() {
run_udp2raw_setup, run_udp2raw_setup,
test_udp2raw_tunnel, test_udp2raw_tunnel,
check_udp2raw_router_status, check_udp2raw_router_status,
upload_udp2raw_binary upload_udp2raw_binary,
list_controller_clients
] ]
) )
.run(tauri::generate_context!()) .run(tauri::generate_context!())
+3
View File
@@ -30,6 +30,9 @@
"targets": "all", "targets": "all",
"icon": [ "icon": [
"icons/icon.ico" "icons/icon.ico"
],
"resources": [
"resources/**/*"
] ]
} }
} }
+5
View File
@@ -18,6 +18,7 @@ import { BackendSettings } from '@/components/settings/BackendSettings';
import { ActivityLogs } from '@/components/activity/ActivityLogs'; import { ActivityLogs } from '@/components/activity/ActivityLogs';
import { Card } from '@/components/ui/Card'; import { Card } from '@/components/ui/Card';
import { Udp2rawConfig } from '@/components/udp2raw/Udp2rawConfig'; import { Udp2rawConfig } from '@/components/udp2raw/Udp2rawConfig';
import { ControllersRoute } from '@/components/controllers/ControllersRoute';
import { vpnApi } from '@/services/vpnApi'; import { vpnApi } from '@/services/vpnApi';
import { vpsApi } from '@/services/vpsApi'; import { vpsApi } from '@/services/vpsApi';
@@ -281,6 +282,10 @@ export function RouteView({
return <DashboardRoute />; return <DashboardRoute />;
} }
if (active === 'Controladores') {
return <ControllersRoute />;
}
if (active === 'Provisionamento') { if (active === 'Provisionamento') {
return <ProvisioningWizard />; return <ProvisioningWizard />;
} }
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -5,12 +5,14 @@ import {
RadioTower, RadioTower,
Settings, Settings,
Wrench, Wrench,
Router
} from 'lucide-react'; } from 'lucide-react';
import logoIcon from '@/assets/logo-icon.png'; import logoIcon from '@/assets/logo-icon.png';
const items = [ const items = [
['Painel', Gauge], ['Painel', Gauge],
['Controladores', Router],
['Configuração UDP2RAW', RadioTower], ['Configuração UDP2RAW', RadioTower],
['Provisionamento', Wrench], ['Provisionamento', Wrench],
['Registos de Atividade', FileClock], ['Registos de Atividade', FileClock],
@@ -57,15 +59,13 @@ export function Sidebar({
key={label} key={label}
type="button" type="button"
onClick={() => onSelect(label)} 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 ${ 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
isActive
? 'border border-blue-500/20 bg-blue-500/15 text-white shadow-[0_0_25px_rgba(59,130,246,0.12)]' ? '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' : 'border border-transparent text-slate-300 hover:border-white/5 hover:bg-white/[0.035] hover:text-white'
}`} }`}
> >
<div <div
className={`rounded-xl p-2 transition ${ className={`rounded-xl p-2 transition ${isActive
isActive
? 'bg-blue-500/10 text-blue-300' ? 'bg-blue-500/10 text-blue-300'
: 'bg-white/[0.03] text-slate-400 group-hover:text-slate-200' : 'bg-white/[0.03] text-slate-400 group-hover:text-slate-200'
}`} }`}
+4 -4
View File
@@ -1,9 +1,9 @@
import type { AppSettings } from '@/types/api'; import type { AppSettings } from '@/types/api';
const defaults: AppSettings = { const defaults: AppSettings = { // FOR DEBUG AND DEV PURPOSES ONLY ==========================>
backendUrl: 'http://146.59.230.190:8080', backendUrl: 'http://localhost:8080',// FOR DEBUG AND DEV PURPOSES ONLY ==========================>
apiKey: apiKey:// FOR DEBUG AND DEV PURPOSES ONLY ==========================>
'b8184608fcab419da2ce9a220ee6017daaff637b627e43ebac31b6b7c3344349', 'dev-api-key',// FOR DEBUG AND DEV PURPOSES ONLY ==========================>
}; };
const SETTINGS_KEY = const SETTINGS_KEY =
+32 -9
View File
@@ -5,21 +5,32 @@ import type {
VpsHealth, VpsHealth,
} from '@/types/api'; } 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 = { export const vpsApi = {
health: () => health: () =>
apiRequest<VpsHealth>( apiRequest<VpsHealth>('/api/vps/health'),
'/api/vps/health',
),
networkTraffic: () => networkTraffic: () =>
apiRequest<NetworkTrafficResponse>( apiRequest<NetworkTrafficResponse>('/api/vps/network-traffic'),
'/api/vps/network-traffic',
),
udp2rawTraffic: () => udp2rawTraffic: () =>
apiRequest<NetworkTrafficResponse>( apiRequest<NetworkTrafficResponse>('/api/vps/udp2raw-traffic'),
'/api/vps/udp2raw-traffic',
),
rollbackLastBackup: () => rollbackLastBackup: () =>
apiRequest<{ restored: boolean }>( apiRequest<{ restored: boolean }>(
@@ -28,4 +39,16 @@ export const vpsApi = {
method: 'POST', 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)}`,
),
}; };