From a652a7ac07ba22d0bdc4659bfe03ab2e2c090c44 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Sun, 26 Mar 2023 14:05:49 +0300 Subject: [PATCH] add ubus api for 'send', 'call', 'list' and 'wait_for' --- Cargo.lock | 294 ++++++++++++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 8 ++ src/main.rs | 40 ++++++- src/ubus.rs | 220 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 551 insertions(+), 11 deletions(-) create mode 100644 src/ubus.rs diff --git a/Cargo.lock b/Cargo.lock index 4de1319..937c51a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,7 +31,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cee8cf1202a4f94d31837f1902ab0a75c77b65bf59719e093703abe83efd74ec" dependencies = [ "accesskit", - "parking_lot", + "parking_lot 0.12.1", ] [[package]] @@ -44,7 +44,7 @@ dependencies = [ "accesskit_consumer", "objc2", "once_cell", - "parking_lot", + "parking_lot 0.12.1", ] [[package]] @@ -58,7 +58,7 @@ dependencies = [ "async-channel", "atspi", "futures-lite", - "parking_lot", + "parking_lot 0.12.1", "serde", "zbus", ] @@ -73,7 +73,7 @@ dependencies = [ "accesskit_consumer", "arrayvec", "once_cell", - "parking_lot", + "parking_lot 0.12.1", "paste", "windows", ] @@ -88,7 +88,7 @@ dependencies = [ "accesskit_macos", "accesskit_unix", "accesskit_windows", - "parking_lot", + "parking_lot 0.12.1", "winit", ] @@ -160,7 +160,7 @@ dependencies = [ "objc-foundation", "objc_id", "once_cell", - "parking_lot", + "parking_lot 0.12.1", "thiserror", "winapi", "x11rb", @@ -254,6 +254,36 @@ dependencies = [ "event-listener", ] +[[package]] +name = "async-net" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4051e67316bc7eff608fe723df5d32ed639946adcd69e07df41fd42a7b411f1f" +dependencies = [ + "async-io", + "autocfg", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-process" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6381ead98388605d0d9ff86371043b5aa922a3905824244de40dc263a14fcba4" +dependencies = [ + "async-io", + "async-lock", + "autocfg", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "libc", + "signal-hook", + "windows-sys 0.42.0", +] + [[package]] name = "async-recursion" version = "1.0.4" @@ -265,6 +295,19 @@ dependencies = [ "syn 2.0.10", ] +[[package]] +name = "async-ssh2-lite" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "068cabec74fe85e7a689c96fe3eecd5f3bce99d7358beeecd7d690372bc4de0e" +dependencies = [ + "async-io", + "async-trait", + "futures-util", + "libssh2-sys", + "ssh2", +] + [[package]] name = "async-task" version = "4.4.0" @@ -785,7 +828,7 @@ dependencies = [ "ecolor", "emath", "nohash-hasher", - "parking_lot", + "parking_lot 0.12.1", ] [[package]] @@ -906,6 +949,17 @@ dependencies = [ "waker-fn", ] +[[package]] +name = "futures-macro" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb14ed937631bd8b8b8977f2c198443447a8355b6e3ca599f38c975e5a963b6" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "futures-sink" version = "0.3.27" @@ -926,6 +980,7 @@ checksum = "3ef6b17e481503ec85211fed8f39d1970f128935ca1f814cd32ac4a6842e84ab" dependencies = [ "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -1058,6 +1113,15 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + [[package]] name = "hermit-abi" version = "0.3.1" @@ -1108,11 +1172,17 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09270fd4fa1111bc614ed2246c7ef56239a3063d5be0d1ec3b589c505d400aeb" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.1", "libc", "windows-sys 0.45.0", ] +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + [[package]] name = "jni" version = "0.21.1" @@ -1159,6 +1229,29 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" +[[package]] +name = "lazy-regex" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff63c423c68ea6814b7da9e88ce585f793c87ddd9e78f646970891769c8235d4" +dependencies = [ + "lazy-regex-proc_macros", + "once_cell", + "regex", +] + +[[package]] +name = "lazy-regex-proc_macros" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8edfc11b8f56ce85e207e62ea21557cfa09bb24a8f6b04ae181b086ff8611c22" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 1.0.109", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -1181,6 +1274,32 @@ dependencies = [ "winapi", ] +[[package]] +name = "libssh2-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.1.4" @@ -1365,6 +1484,16 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi 0.2.6", + "libc", +] + [[package]] name = "num_enum" version = "0.5.11" @@ -1447,6 +1576,19 @@ version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +[[package]] +name = "openssl-sys" +version = "0.9.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666416d899cf077260dac8698d60a60b435a46d57e82acb1be3d0dad87284e5b" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "orbclient" version = "0.3.43" @@ -1484,6 +1626,17 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -1491,7 +1644,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core", + "parking_lot_core 0.9.7", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", ] [[package]] @@ -1709,6 +1876,12 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + [[package]] name = "same-file" version = "1.0.6" @@ -1775,6 +1948,17 @@ dependencies = [ "syn 2.0.10", ] +[[package]] +name = "serde_json" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_repr" version = "0.1.12" @@ -1806,6 +1990,31 @@ dependencies = [ "digest", ] +[[package]] +name = "shell-escape" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f" + +[[package]] +name = "signal-hook" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + [[package]] name = "slab" version = "0.4.8" @@ -1859,6 +2068,23 @@ dependencies = [ "wayland-client", ] +[[package]] +name = "smol" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13f2b548cd8447f8de0fdf1c592929f70f4fc7039a05e47404b0d096ec6987a1" +dependencies = [ + "async-channel", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-net", + "async-process", + "blocking", + "futures-lite", +] + [[package]] name = "socket2" version = "0.4.9" @@ -1869,6 +2095,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "ssh2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7fe461910559f6d5604c3731d00d2aafc4a83d1665922e280f42f9a168d5455" +dependencies = [ + "bitflags", + "libc", + "libssh2-sys", + "parking_lot 0.11.2", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -1982,6 +2220,30 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tokio" +version = "1.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03201d01c3c27a29c8a5cee5b55a93ddae1ccf6f08f65365c2c918f8c1b76f64" +dependencies = [ + "autocfg", + "num_cpus", + "pin-project-lite", + "tokio-macros", + "windows-sys 0.45.0", +] + +[[package]] +name = "tokio-macros" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "toml" version = "0.7.3" @@ -2065,11 +2327,19 @@ name = "ubusman" version = "0.1.0" dependencies = [ "anyhow", + "async-ssh2-lite", "directories-next", "eframe", "egui", + "hex", + "lazy-regex", "serde", + "serde_json", + "shell-escape", + "smol", + "ssh2", "thiserror", + "tokio", "toml", ] @@ -2115,6 +2385,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "vec_map" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index a114ea7..5410dd6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,9 +7,17 @@ edition = "2021" [dependencies] anyhow = "1.0.70" +async-ssh2-lite = { version = "0.4.5", features = ["async-io"] } directories-next = "2.0.0" eframe = "0.21.3" egui = "0.21.0" +hex = "0.4.3" +lazy-regex = "2.5.0" serde = { version = "1.0.158", features = ["derive"] } +serde_json = "1.0.94" +shell-escape = "0.1.5" +smol = "1.3.0" +ssh2 = "0.9.4" thiserror = "1.0.40" +tokio = { version = "1.26.0", features = ["macros", "rt-multi-thread"] } toml = "0.7.3" diff --git a/src/main.rs b/src/main.rs index 46176da..86e7e2b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,42 @@ +use std::net::ToSocketAddrs; + #[cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release + +use anyhow::Result; +use smol::Async; +use async_ssh2_lite::{AsyncSession, AsyncSessionStream}; use app::App; -mod app; +use crate::ubus::Ubus; -fn main() -> eframe::Result<()> { +mod app; +mod ubus; + +#[tokio::main] +async fn main() -> Result<()> { + let address = "172.24.224.1:22"; + let username = "root"; + let password = "admin01"; + + let mut session = AsyncSession::::connect( + address.to_socket_addrs()?.next().unwrap(), + None, + ).await?; + + session.handshake().await?; + session.userauth_password(username, password).await?; + + let ubus = Ubus::new(session); + // dbg!(ubus.call( + // "container", + // "get_features", + // None + // ).await? + // ); + + dbg!(ubus.wait_for(&vec!["network"]).await?); + + /* let mut native_options = eframe::NativeOptions::default(); native_options.decorated = true; native_options.resizable = true; @@ -17,5 +50,8 @@ fn main() -> eframe::Result<()> { Box::new(app) }) ) + */ + + Ok(()) } diff --git a/src/ubus.rs b/src/ubus.rs new file mode 100644 index 0000000..19a8e03 --- /dev/null +++ b/src/ubus.rs @@ -0,0 +1,220 @@ +use std::{io::Read, borrow::Cow, net::TcpStream}; + +use async_ssh2_lite::AsyncSession; +use lazy_regex::regex_captures; +use serde_json::Value; +use shell_escape::unix::escape; +use hex::FromHex; +use anyhow::{Result, bail, anyhow}; +use smol::{Async, io::AsyncReadExt}; +use thiserror::Error; + +pub struct Ubus { + session: AsyncSession> +} + +#[derive(Debug)] +pub enum UbusParamType { + Unknown, + Integer, + Boolean, + Table, + String, + Array, + Double +} + +#[derive(Error, Debug)] +pub enum UbusError { + #[error("Invalid command")] + InvalidCommand, + #[error("Invalid argument")] + InvalidArgument, + #[error("Method not found")] + MethodNotFound, + #[error("Not found")] + NotFound, + #[error("No response")] + NoData, + #[error("Permission denied")] + PermissionDenied, + #[error("Request timed out")] + Timeout, + #[error("Operation not supported")] + NotSupported, + #[error("Unknown error")] + UnknownError, + #[error("Connection failed")] + ConnectionFailed, + #[error("Out of memory")] + NoMemory, + #[error("Parsing message data failed")] + ParseError, + #[error("System error")] + SystemError, +} + +#[derive(Debug)] +pub struct UbusObject { + pub name: String, + pub id: u32, + pub methods: Vec<(String, Vec<(String, UbusParamType)>)> +} + +fn parse_parameter_type(param: &str) -> Option { + use UbusParamType::*; + match param { + "String" => Some(String), + "Boolean" => Some(Boolean), + "Integer" => Some(Integer), + "Double" => Some(Double), + "Table" => Some(Table), + "Array" => Some(Array), + "(unknown)" => Some(Unknown), + _ => None + } +} + +fn parse_error_code(code: i32) -> Option { + use UbusError::*; + + match code { + 0 => None, + 1 => Some(InvalidCommand), + 2 => Some(InvalidArgument), + 3 => Some(MethodNotFound), + 4 => Some(NotFound), + 5 => Some(NoData), + 6 => Some(PermissionDenied), + 7 => Some(Timeout), + 8 => Some(NotSupported), + 9 => Some(UnknownError), + 10 => Some(ConnectionFailed), + 11 => Some(NoMemory), + 12 => Some(ParseError), + 13 => Some(SystemError), + _ => Some(UnknownError) + } +} + +fn parse_hex_id(id: &str) -> Result { + let [byte1, byte2, byte3, byte4] = <[u8; 4]>::from_hex(id)?; + let byte1 = (byte1 as u32) << 24; + let byte2 = (byte2 as u32) << 16; + let byte3 = (byte3 as u32) << 8; + let byte4 = (byte4 as u32) << 0; + + return Ok(byte1 + byte2 + byte3 + byte4); +} + +fn escape_json(json: &Value) -> String { + escape(Cow::from(json.to_string())).into() +} + +impl Ubus { + pub fn new(session: AsyncSession>) -> Ubus { + Ubus { + session + } + } + + async fn exec_cmd(&self, cmd: &str) -> Result { + let mut channel = self.session.channel_session().await?; + channel.exec(cmd).await?; + channel.close().await?; + if let Some(err) = parse_error_code(channel.exit_status()?) { + return Err(err.into()) + } + + let mut output = String::new(); + channel.read_to_string(&mut output).await?; + Ok(output) + } + + pub async fn list(self, path: Option<&str>) -> Result> { + let output = match path { + Some(path) => self.exec_cmd(&format!("ubus list {}", path)).await?, + None => self.exec_cmd("ubus list").await?, + }; + Ok(output.lines().map(ToOwned::to_owned).collect::>()) + } + + pub async fn list_verbose(self, path: Option<&str>) -> Result> { + let output = match path { + Some(path) => self.exec_cmd(&format!("ubus -v list {}", path)).await?, + None => self.exec_cmd("ubus -v list").await?, + }; + + let mut cur_name = None; + let mut cur_id = None; + let mut cur_methods = vec![]; + + let mut objects = vec![]; + for line in output.lines() { + if let Some((_, name, id)) = regex_captures!(r"^'([\w.-]+)' @([0-9a-zA-Z]+)$", line) { + if cur_name.is_some() && cur_id.is_some() { + objects.push(UbusObject { + id: cur_id.unwrap(), + name: cur_name.unwrap(), + methods: cur_methods + }); + cur_methods = vec![]; + } + + cur_name = Some(name.into()); + cur_id = Some(parse_hex_id(id)?); + } else if let Some((_, name, params_body)) = regex_captures!(r#"^\s+"([\w-]+)":\{(.*)}$"#, line) { + let mut params = vec![]; + if !params_body.is_empty() { + for param in params_body.split(",") { + let (_, name, param_type_name) = regex_captures!(r#"^"([\w-]+)":"(\w+)"$"#, param) + .ok_or(anyhow!("Failed to parse parameter '{}' in line '{}'", param, line))?; + + let param_type = parse_parameter_type(param_type_name) + .ok_or(anyhow!("Unknown parameter type '{}'", param_type_name))?; + + params.push((name.into(), param_type)); + } + } + cur_methods.push((name.into(), params)); + } else { + bail!("Failed to parse line '{}'", line); + } + } + Ok(objects) + } + + pub async fn call(self, path: &str, method: &str, message: Option<&Value>) -> Result { + let cmd = match message { + Some(msg) => format!("ubus call {} {} {}", path, method, escape_json(msg)), + None => format!("ubus call {} {}", path, method), + }; + + let output = self.exec_cmd(&cmd).await?; + let value = serde_json::from_str::(&output)?; + Ok(value) + } + + pub async fn send(self, event_type: &str, message: Option<&Value>) -> Result<()> { + let cmd = match message { + Some(msg) => format!("ubus send {} {}", event_type, escape_json(msg)), + None => format!("ubus send {}", event_type), + }; + + self.exec_cmd(&cmd).await?; + + Ok(()) + } + + pub async fn wait_for(self, objects: &[&str]) -> Result<()> { + if objects.len() < 1 { + bail!("At least 1 object is required") + } + let cmd = format!("ubus wait_for {}", objects.join(" ")); + let mut channel = self.session.channel_session().await?; + channel.exec(&cmd).await?; + channel.close().await?; + + Ok(()) + } +}